diff options
Diffstat (limited to 'internal/game/predictions.go')
| -rw-r--r-- | internal/game/predictions.go | 737 |
1 files changed, 737 insertions, 0 deletions
diff --git a/internal/game/predictions.go b/internal/game/predictions.go new file mode 100644 index 0000000..c61e64e --- /dev/null +++ b/internal/game/predictions.go @@ -0,0 +1,737 @@ +package game + +import ( + "context" + "fmt" + "log" + "sort" + "strconv" + "strings" + "sync" + "time" + + "thehouseoficarus/internal/color" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" + "thehouseoficarus/internal/prediction" +) + +const ( + predictionPageSize = 10 + predictionClosedPageSize = 15 +) + +type predictionRequest struct { + action, id, search, outcomeName string + page, outcome, quantity, lot int + showHints bool +} +type predictionResult struct { + list []prediction.Market + markets map[string]prediction.Market + history []prediction.Point + err error +} + +func (g *Game) atPredictionCounter(p *player.Player) bool { + if p == nil { + return false + } + r, err := g.World.LoadRoom(p.RoomID) + return err == nil && r.PredictionExchange +} + +// normalizeOutcome lower-cases and collapses an outcome name so players can +// match it with any spacing or capitalization. +func normalizeOutcome(s string) string { + return strings.ToLower(strings.Join(strings.Fields(s), " ")) +} + +func (g *Game) predictionHelp(s *net.Session) { + g.predictionFrame(s, "HOW TO PLAY", []string{ + "Pick a market number, choose an outcome, and back your prediction!", + "Winning shares pay 100 credits each. Losing shares pay 0. No chips or real money.", + "The house uses Polymarket indicative prices, not exchange fills. No fees.", + "Each share's buy price rounds UP and sell price rounds DOWN to whole credits, before multiplying by shares.", + "buy SHARES OUTCOME = buy at the house's live quote; OUTCOME is a number or name, e.g. buy 10 yes.", + "sell CONTRACT = sell all shares of a contract on your active list.", + "Resolved contracts pay 100 credits per winning share. Type collect to withdraw them.", + "Up to 50 active contracts; 1-10,000 shares per purchase.", + "Closed, paused and expired markets cannot trade. Finalized results settle on your next visit. Disputed results wait.", + "active lists your open contracts; closed lists sold, resolved and collected ones. holding N opens a contract's market.", + "rules shows the selected market's full resolution text.", + "stop leaves the counter without selling anything. No automatic background polling.", + }) + g.predictionHints(s, true) +} + +func (g *Game) executePredictions(s *net.Session, args []string, origin string) { + if !s.PredictionActive { + s.WriteLine("Use predictions to approach the counter.") + return + } + if !g.atPredictionCounter(s.Player) { + s.WriteLine("There is no Prediction Exchange here. Look for the east arch in the casino vestibule.") + return + } + if s.PredictionBusy { + s.WriteLine("Updating markets... please wait for your current request.") + return + } + r := predictionRequest{action: "list", page: 1, lot: -1, showHints: origin == "open" || origin == "look"} + if len(args) > 0 { + r.action = strings.ToLower(args[0]) + } + number := func(i int) int { + if i >= len(args) { + return 0 + } + n, _ := strconv.Atoi(args[i]) + return n + } + switch r.action { + case "help": + g.predictionHelp(s) + return + case "collect": + g.collectPredictions(s) + return + case "rules": + r.id = s.PredictionSelected + if r.id == "" || s.PredictionScreen != "view" { + g.predictionHelp(s) + return + } + case "back": + r.action = "list" + r.page = max(1, s.PredictionPage) + r.search = s.PredictionSearch + case "list": + if len(args) > 1 { + r.page = number(1) + } + case "refresh", "next", "prev": + r.action = s.PredictionScreen + if r.action == "" { + r.action = "list" + } + switch r.action { + case "view": + r.id = s.PredictionSelected + case "closed": + r.page = s.PredictionBoardPage + if args[0] == "next" { + r.page++ + } else if args[0] == "prev" { + r.page = max(1, r.page-1) + } + case "active": + // Active contracts always fit on a single page. + default: + r.action = "list" + r.page = s.PredictionPage + r.search = s.PredictionSearch + if args[0] == "next" { + r.page++ + } else if args[0] == "prev" { + r.page = max(1, r.page-1) + } + } + case "find": + r.action = "list" + r.search = strings.ToLower(strings.Join(args[1:], " ")) + case "active", "closed": + // Page state lives on the session; the renderer clamps it. + case "board": + r.action = "active" + case "view": + if len(args) == 1 && s.PredictionSelected != "" { + r.id = s.PredictionSelected + break + } + n := number(1) + if n < 1 || n > len(s.PredictionIDs) { + s.WriteLine("Choose a market number from markets.") + g.predictionHints(s) + return + } + r.id = s.PredictionIDs[n-1] + case "buy": + r.id = s.PredictionSelected + if r.id == "" || s.PredictionScreen != "view" { + s.WriteLine("Choose a market first: view N (or just its number).") + g.predictionHints(s) + return + } + r.quantity = number(1) + if len(args) < 3 || r.quantity < 1 || r.quantity > 10000 { + s.WriteLine("buy SHARES OUTCOME - for example: buy 10 yes or buy 10 2") + g.predictionHints(s) + return + } + outcomeTok := normalizeOutcome(strings.Join(args[2:], " ")) + if n, err := strconv.Atoi(outcomeTok); err == nil { + r.outcome = n + } else { + r.outcomeName = outcomeTok + } + case "holding", "sell": + r.lot = number(1) - 1 + if r.lot < 0 || r.lot >= len(s.Player.PredictionContracts) { + s.WriteLine("Use a contract number from active.") + return + } + r.id = s.Player.PredictionContracts[r.lot].MarketID + if r.action == "holding" { + r.action = "view" + } else if len(args) != 2 { + s.WriteLine("sell CONTRACT - for example: sell 3") + g.predictionHints(s) + return + } + default: + g.predictionHelp(s) + return + } + if r.page < 1 { + s.WriteLine("Page numbers start at 1.") + return + } + if g.PredictionClient == nil { + g.PredictionClient = prediction.NewClient() + } + client := g.PredictionClient + p, roomID := s.Player, s.Player.RoomID + epoch := s.PredictionEpoch + ids := map[string]bool{} + for _, c := range p.PredictionContracts { + if c.Status == "open" { + ids[c.MarketID] = true + } + } + if r.id != "" { + ids[r.id] = true + } + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + s.PredictionCancel = cancel + s.PredictionBusy = true + s.WriteLine(g.colorize(s, "casino_menu", g.predictionGlyph(s, "↻", ">")+" Updating markets...")) + // Only immutable IDs and an HTTP client cross the owner boundary. All player, + // credit, session, and persistence work happens back on the game owner. + work := func() { + defer cancel() + result := fetchPredictions(ctx, client, r, ids) + g.run(func() { + if !s.PredictionActive || s.PredictionEpoch != epoch { + return + } + s.PredictionBusy = false + s.PredictionCancel = nil + if g.isShuttingDown() || s.Player != p || s.State() != net.StateGame || s.IsDisconnecting() || p.RoomID != roomID || !g.atPredictionCounter(p) { + return + } + if g.owner != nil && g.loggedInChars[p.Name] != s { + return + } + if g.owner != nil { + defer g.reprompt(s) + } + s.PredictionShowHints = r.showHints + s.PredictionUnavailable = result.err != nil + if result.err != nil { + log.Printf("prediction market refresh failed: %v", result.err) + g.predictionFrame(s, "COUNTER CLOSED", []string{g.colorize(s, "casino_result_loss", "Live market data is unavailable."), "No trades or settlements were made.", "Your existing contracts are safe. Try refresh or stop."}) + g.predictionHints(s) + return + } + if !g.settlePredictions(s, result.markets) { + g.predictionHints(s) + return + } + switch r.action { + case "list": + g.predictionList(s, result.list, r.page, r.search) + case "active": + g.predictionActive(s, result.markets) + case "closed": + g.predictionClosed(s, result.markets, r.page) + case "view": + if _, ok := result.markets[r.id]; ok { + g.predictionView(s, result.markets[r.id], result.history) + } else { + g.predictionFrame(s, "MARKET UNAVAILABLE", []string{"That market is no longer available."}) + g.predictionHints(s) + } + case "rules": + g.predictionRules(s, result.markets[r.id]) + case "buy", "sell": + traded := g.tradePrediction(s, r, result.markets[r.id]) + if traded && r.action == "sell" { + g.predictionActive(s, result.markets) + } else { + g.predictionHints(s) + } + } + }) + } + if g.owner == nil { + work() + } else { + go work() + } +} + +func fetchPredictions(ctx context.Context, client *prediction.Client, r predictionRequest, ids map[string]bool) predictionResult { + result := predictionResult{markets: make(map[string]prediction.Market)} + if r.action == "list" || len(ids) == 0 { + result.list, result.err = client.List(ctx) + if result.err != nil { + return result + } + } + var mu sync.Mutex + var wg sync.WaitGroup + slots := make(chan struct{}, 4) + for id := range ids { + wg.Add(1) + go func(id string) { + defer wg.Done() + select { + case slots <- struct{}{}: + case <-ctx.Done(): + mu.Lock() + result.err = ctx.Err() + mu.Unlock() + return + } + defer func() { <-slots }() + m, err := client.Market(ctx, id) + mu.Lock() + defer mu.Unlock() + if err != nil { + result.err = err + } else { + result.markets[id] = m + } + }(id) + } + wg.Wait() + if result.err == nil && r.action == "view" { + _, tokens, _, err := result.markets[r.id].Values() + if err != nil { + result.err = err + } else { + result.history, result.err = client.History(ctx, tokens[0]) + } + } + return result +} + +// Persist ledger and credits together. Roll back only when replacement did not +// happen; casinoWallet.save already handles durability-uncertain replacements. +func (g *Game) savePredictions(s *net.Session, before []prediction.Contract, credits int) bool { + if err := (casinoWallet{g: g, p: s.Player}).save("prediction market"); err != nil { + log.Printf("prediction market save failed for %s: %v", s.Player.Name, err) + s.Player.PredictionContracts = before + s.Player.Credits = credits + s.WriteLine("The counter cannot record this transaction. Your credits and contracts are unchanged.") + return false + } + return true +} + +// settlePredictions marks finalized contracts as resolved and records their +// payout, but does not move any credits. Winnings stay at the counter until the +// player withdraws them with collect, so a resolved contract is never silently +// spent and a losing contract leaves a visible closed record. +func (g *Game) settlePredictions(s *net.Session, markets map[string]prediction.Market) bool { + p := s.Player + before := append([]prediction.Contract(nil), p.PredictionContracts...) + credits := p.Credits + count, won := 0, 0 + for i, c := range p.PredictionContracts { + if c.Status != "open" { + continue + } + price, ok := markets[c.MarketID].Settlement(c.Token) + if !ok { + continue + } + payout := prediction.SellValue(price, c.Quantity) + c.Status = "resolved" + c.Proceeds = payout + c.Closed = time.Now().UTC().Format(time.RFC3339) + p.PredictionContracts[i] = c + count++ + won += payout + } + if count == 0 { + return true + } + if !g.savePredictions(s, before, credits) { + return false + } + if won > 0 { + s.WriteLine(g.colorize(s, "casino_result_win", fmt.Sprintf("%d contract(s) resolved; %d credits await collection. Type collect to withdraw.", count, won))) + } else { + s.WriteLine(g.colorize(s, "casino_result_push", fmt.Sprintf("%d contract(s) resolved without a payout. Type collect to clear them.", count))) + } + return true +} + +// collectPredictions withdraws every resolved contract's payout and moves the +// contracts to collected. It needs no market data, so winnings remain reachable +// even when the market API is down. +func (g *Game) collectPredictions(s *net.Session) { + p := s.Player + before := append([]prediction.Contract(nil), p.PredictionContracts...) + credits := p.Credits + total, count := 0, 0 + for i, c := range p.PredictionContracts { + if c.Status != "resolved" { + continue + } + total += c.Proceeds + c.Status = "collected" + p.PredictionContracts[i] = c + count++ + } + if count == 0 { + s.WriteLine(g.colorize(s, "casino_action", "You have no resolved winnings to collect.")) + g.predictionHints(s) + return + } + balance, err := addCreditsChecked(p.Credits, total) + if err != nil { + s.WriteLine("Collecting would exceed your credit limit. Bank or spend some credits and try again.") + return + } + p.Credits = balance + if !g.savePredictions(s, before, credits) { + return + } + if total > 0 { + s.WriteLine(g.colorize(s, "casino_result_win", fmt.Sprintf("Collected %d credits from %d resolved contract(s).", total, count))) + } else { + s.WriteLine(g.colorize(s, "casino_result_push", fmt.Sprintf("Cleared %d resolved contract(s); no payout this time.", count))) + } + g.predictionClosed(s, nil, 1) +} +func (g *Game) tradePrediction(s *net.Session, r predictionRequest, m prediction.Market) (traded bool) { + p := s.Player + if !m.Tradable(time.Now()) { + s.WriteLine("This market is closed, paused, expired, or awaiting resolution. The house cannot trade it.") + return + } + before := append([]prediction.Contract(nil), p.PredictionContracts...) + credits := p.Credits + if r.action == "buy" { + names, tokens, prices, err := m.Values() + if err != nil { + s.WriteLine("Invalid outcome. Read the market again.") + return + } + n := -1 + if r.outcome >= 1 && r.outcome <= len(names) { + n = r.outcome - 1 + } + if n < 0 && r.outcomeName != "" { + // Accept exact, prefix or partial outcome names (buy 10 natus). + wanted := normalizeOutcome(r.outcomeName) + for _, pass := range []int{0, 1, 2} { + for i, name := range names { + candidate := normalizeOutcome(prediction.Clean(name, 80)) + match := candidate == wanted + if pass == 1 { + match = strings.HasPrefix(candidate, wanted) + } else if pass == 2 { + match = strings.Contains(candidate, wanted) + } + if match { + n = i + break + } + } + if n >= 0 { + break + } + } + } + if n < 0 { + s.WriteLine("Unknown outcome. Choose a number or name from the outcome list.") + g.predictionHints(s) + return + } + if prices[n] <= 0 || prices[n] >= 1 { + s.WriteLine("The house is not accepting trades at a terminal quote.") + return + } + active := 0 + for _, c := range before { + if c.Status == "open" { + active++ + } + } + if active >= prediction.MaxContracts { + s.WriteLine("Your portfolio is full. Sell or settle a contract first.") + return + } + cost := prediction.BuyCost(prices[n], r.quantity) + if cost > p.Credits { + s.WriteLine(fmt.Sprintf("You need %d carried credits. Chips and banked credits are not used.", cost)) + return + } + p.Credits -= cost + p.PredictionContracts = append(p.PredictionContracts, prediction.Contract{MarketID: m.ID, Title: prediction.Clean(m.Question, 200), Outcome: prediction.Clean(names[n], 80), Token: tokens[n], Quantity: r.quantity, Cost: cost, Status: "open", Opened: time.Now().UTC().Format(time.RFC3339)}) + if g.savePredictions(s, before, credits) { + traded = true + s.WriteLine(g.colorize(s, "casino_result_win", fmt.Sprintf("Bought %d shares of %s for %d credits; pays %d credits if it wins.", r.quantity, prediction.Clean(names[n], 80), cost, r.quantity*prediction.FaceValue))) + } + } else { + c := p.PredictionContracts[r.lot] + if c.Status != "open" { + s.WriteLine("That contract is no longer active.") + return + } + price, err := m.Price(c.Token) + if err != nil || price <= 0 || price >= 1 { + s.WriteLine("No tradable quote for this outcome.") + return + } + proceeds := prediction.SellValue(price, c.Quantity) + balance, err := addCreditsChecked(p.Credits, proceeds) + if err != nil { + s.WriteLine("This sale would exceed your credit limit.") + return + } + p.Credits = balance + c.Status = "sold" + c.Proceeds = proceeds + c.Closed = time.Now().UTC().Format(time.RFC3339) + p.PredictionContracts[r.lot] = c + if g.savePredictions(s, before, credits) { + traded = true + pl := g.colorize(s, "casino_result_win", fmt.Sprintf("Selling contract #%d paid %d credits.", r.lot+1, proceeds)) + if proceeds < c.Cost { + pl = g.colorize(s, "casino_result_loss", fmt.Sprintf("Selling contract #%d paid %d credits.", r.lot+1, proceeds)) + } + s.WriteLine(pl) + } + } + return +} + +func (g *Game) predictionList(s *net.Session, markets []prediction.Market, page int, search string) { + filtered := []prediction.Market{} + for _, m := range markets { + if search == "" || strings.Contains(strings.ToLower(m.Question+" "+m.Description), search) { + filtered = append(filtered, m) + } + } + pages := max(1, (len(filtered)+predictionPageSize-1)/predictionPageSize) + page = max(1, min(page, pages)) + s.PredictionPage = page + s.PredictionScreen = "list" + s.PredictionSelected = "" + s.PredictionSearch = search + s.PredictionIDs = nil + if len(filtered) == 0 { + g.predictionFrame(s, "NO OPEN MARKETS", []string{"No eligible markets matched this selection.", "Try markets to clear the search, or refresh to check again."}) + g.predictionHints(s) + return + } + width := predictionWidth(s) + oddsTarget := min(36, max(20, width-32)) + marketTarget := max(10, width-13-oddsTarget) + sep := g.predictionSep(s) + title := fmt.Sprintf("PREDICTION EXCHANGE%sMARKETS %d/%d", sep, page, pages) + if search != "" { + title = fmt.Sprintf("PREDICTION EXCHANGE%s%q %d/%d", sep, prediction.Clean(search, 24), page, pages) + } + rows := [][]string{} + for _, m := range filtered { + s.PredictionIDs = append(s.PredictionIDs, m.ID) + } + for i := (page - 1) * predictionPageSize; i < min(page*predictionPageSize, len(filtered)); i++ { + m := filtered[i] + rows = append(rows, []string{ + g.colorize(s, "casino_menu", strconv.Itoa(i+1)), + g.colorize(s, "casino_emphasis", prediction.Clean(m.Question, marketTarget)), + g.predictionOddsCell(s, m, oddsTarget), + }) + } + headers := []string{g.colorize(s, "casino_menu", "#"), g.colorize(s, "casino_menu", "Market"), g.colorize(s, "casino_menu", "Odds")} + g.predictionTable(s, g.colorize(s, "casino_emphasis", title), headers, rows, 1) + g.predictionHints(s) +} + +func (g *Game) predictionView(s *net.Session, m prediction.Market, history []prediction.Point) { + names, _, prices, err := m.Values() + if err != nil { + g.predictionFrame(s, "MARKET UNAVAILABLE", []string{"The market returned invalid data."}) + g.predictionHints(s) + return + } + state := "CLOSED / awaiting finalized result" + if m.Tradable(time.Now()) { + state = "OPEN" + } + if m.Resolution == "resolved" { + state = "RESOLVED" + } + s.PredictionScreen = "view" + s.PredictionSelected = m.ID + marketLines := []string{ + g.colorize(s, "casino_emphasis", prediction.Clean(m.Question, 200)), + g.colorize(s, "casino_action", "Scheduled end: "+prediction.Clean(m.End, 40)) + g.colorize(s, "casino_credits", " | "+state), + } + if bar := g.predictionOddsCell(s, m, predictionWidth(s)-6); bar != "" { + marketLines = append(marketLines, "", bar) + } + g.predictionFrame(s, "THE MARKET", marketLines) + outcomeRows := [][]string{} + for i, n := range names { + outcomeRows = append(outcomeRows, []string{ + g.colorize(s, "casino_menu", strconv.Itoa(i+1)), + g.colorize(s, "casino_emphasis", prediction.Clean(n, 40)), + g.colorize(s, "casino_result_loss", strconv.Itoa(prediction.BuyPrice(prices[i]))), + g.colorize(s, "casino_result_win", strconv.Itoa(prediction.SellPrice(prices[i]))), + }) + } + headers := []string{g.colorize(s, "casino_menu", "#"), g.colorize(s, "casino_menu", "Outcome"), g.colorize(s, "casino_menu", "Buy"), g.colorize(s, "casino_menu", "Sell")} + g.predictionTable(s, g.colorize(s, "casino_emphasis", "CHOOSE YOUR OUTCOME"), headers, outcomeRows, 1) + graphLines := g.predictionGraph(s, history, max(4, min(48, predictionWidth(s)-12))) + if len(history) > 1 { + graphLines = append(graphLines, "Period move: "+g.predictionDelta(s, (history[len(history)-1].P-history[0].P)*100, "pp")) + } + g.predictionFrame(s, "ODDS HISTORY / "+prediction.Clean(names[0], 22)+" / 1 WEEK", graphLines) + g.predictionHints(s) +} + +func (g *Game) predictionRules(s *net.Session, m prediction.Market) { + lines := []string{g.colorize(s, "casino_emphasis", prediction.Clean(m.Question, 200)), ""} + description := strings.ReplaceAll(m.Description, "\r\n", "\n") + if strings.TrimSpace(description) == "" { + lines = append(lines, "No resolution rules supplied. Consider avoiding this market.") + } else { + for _, paragraph := range strings.Split(description, "\n") { + lines = append(lines, prediction.Clean(paragraph, len([]rune(paragraph)))) + } + } + g.predictionFrame(s, "RESOLUTION RULES", lines) + g.predictionHints(s) +} + +func (g *Game) predictionActive(s *net.Session, markets map[string]prediction.Market) { + p := s.Player + s.PredictionScreen = "active" + s.PredictionSelected = "" + active := []int{} + for i, c := range p.PredictionContracts { + if c.Status == "open" { + active = append(active, i) + } + } + if len(active) == 0 { + g.predictionFrame(s, "ACTIVE CONTRACTS", []string{"You hold no active contracts.", "Browse the markets to place a prediction."}) + g.predictionHints(s) + return + } + width := predictionWidth(s) + budget := width - (3*6 + 1) + pickTarget := max(10, budget-30) + unicode := s.Player.OptionBool("unicode") + rows := [][]string{} + for _, i := range active { + c := p.PredictionContracts[i] + m := markets[c.MarketID] + price, err := m.Price(c.Token) + value := 0 + status := "" + if err != nil { + value = c.Cost + status = g.predictionSep(s) + "quote unavailable" + } else { + value = prediction.SellValue(price, c.Quantity) + if !m.Tradable(time.Now()) { + status = g.predictionSep(s) + "locked" + } + } + pl := value - c.Cost + category := "casino_result_push" + if pl > 0 { + category = "casino_result_win" + } else if pl < 0 { + category = "casino_result_loss" + } + pick := color.TruncateVisible(prediction.Clean(c.Outcome, 12)+g.predictionSep(s)+prediction.Clean(c.Title, 200)+status, pickTarget, unicode) + rows = append(rows, []string{ + g.colorize(s, "casino_menu", strconv.Itoa(i+1)), + g.colorize(s, "casino_emphasis", pick), + strconv.Itoa(c.Quantity), + strconv.Itoa(c.Cost), + strconv.Itoa(value), + g.colorize(s, category, fmt.Sprintf("%+d", pl)), + }) + } + headers := []string{g.colorize(s, "casino_menu", "#"), g.colorize(s, "casino_menu", "Pick"), g.colorize(s, "casino_menu", "Shares"), g.colorize(s, "casino_menu", "Cost"), g.colorize(s, "casino_menu", "Value"), g.colorize(s, "casino_menu", "P/L")} + g.predictionTable(s, g.colorize(s, "casino_emphasis", fmt.Sprintf("ACTIVE CONTRACTS (%d)", len(active))), headers, rows, 1) + g.predictionHints(s) +} + +func (g *Game) predictionClosed(s *net.Session, markets map[string]prediction.Market, page int) { + p := s.Player + s.PredictionScreen = "closed" + s.PredictionSelected = "" + closed := []int{} + for i, c := range p.PredictionContracts { + if c.Status != "open" { + closed = append(closed, i) + } + } + if len(closed) == 0 { + g.predictionFrame(s, "CLOSED CONTRACTS", []string{"You have no sold, resolved or collected contracts."}) + g.predictionHints(s) + return + } + // Newest first; contract numbers never change. + sort.SliceStable(closed, func(i, j int) bool { + a, b := p.PredictionContracts[closed[i]], p.PredictionContracts[closed[j]] + if a.Closed == b.Closed { + return closed[i] > closed[j] + } + return a.Closed > b.Closed + }) + pages := max(1, (len(closed)+predictionClosedPageSize-1)/predictionClosedPageSize) + page = max(1, min(page, pages)) + s.PredictionBoardPage = page + width := predictionWidth(s) + budget := width - (3*6 + 1) + pickTarget := max(10, budget-30) + unicode := s.Player.OptionBool("unicode") + rows := [][]string{} + start := (page - 1) * predictionClosedPageSize + for j := start; j < min(start+predictionClosedPageSize, len(closed)); j++ { + i := closed[j] + c := p.PredictionContracts[i] + pl := c.Proceeds - c.Cost + category := "casino_result_push" + if pl > 0 { + category = "casino_result_win" + } else if pl < 0 { + category = "casino_result_loss" + } + pick := color.TruncateVisible(prediction.Clean(c.Outcome, 12)+g.predictionSep(s)+prediction.Clean(c.Title, 200)+g.predictionSep(s)+c.Status, pickTarget, unicode) + rows = append(rows, []string{ + g.colorize(s, "casino_menu", strconv.Itoa(i+1)), + g.colorize(s, "casino_emphasis", pick), + strconv.Itoa(c.Quantity), + strconv.Itoa(c.Cost), + strconv.Itoa(c.Proceeds), + g.colorize(s, category, fmt.Sprintf("%+d", pl)), + }) + } + headers := []string{g.colorize(s, "casino_menu", "#"), g.colorize(s, "casino_menu", "Pick"), g.colorize(s, "casino_menu", "Shares"), g.colorize(s, "casino_menu", "Cost"), g.colorize(s, "casino_menu", "Value"), g.colorize(s, "casino_menu", "P/L")} + g.predictionTable(s, g.colorize(s, "casino_emphasis", fmt.Sprintf("CLOSED CONTRACTS%s%d/%d", g.predictionSep(s), page, pages)), headers, rows, 1) + g.predictionHints(s) +} |
