From 9cbafabfddebf375daaa1dcd16870eacda4ebb59 Mon Sep 17 00:00:00 2001 From: jaxperro Date: Tue, 23 Jun 2026 14:43:50 -0600 Subject: [PATCH] live: precompute the $1k paper portfolio off the cache (portfolio.py) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New portfolio.py replays the followed wallets' conviction bets ($1k/$50, hold-to-resolution) from cache.duckdb — which has res_t, so capital RECYCLES at the true resolution time (no client-side clob storms, no phantom-locked capital) and covers the full bet history (~40 pages, not the browser's 4). Writes portfolio.json (equity, splits, current/ resolved/missed tables, per-wallet); wired into daily.sh + publish. The dashboard now renders the top page from this feed (1 request) with the client-side replay kept as a fallback. Co-Authored-By: Claude Opus 4.8 --- live/daily.sh | 20 +++-- live/portfolio.json | 1 + live/portfolio.py | 198 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 213 insertions(+), 6 deletions(-) create mode 100644 live/portfolio.json create mode 100644 live/portfolio.py diff --git a/live/daily.sh b/live/daily.sh index 3f59de98..258e48ec 100755 --- a/live/daily.sh +++ b/live/daily.sh @@ -9,8 +9,12 @@ # 4) sharps — conviction-profile scan + last-minute timing gate -> # conviction_wallets.json + watch_sharps.json (the set the live # trading dashboard reads via raw.githubusercontent) -# 5) dashboard — regenerate + snapshot for auditable forward history -# 6) publish — commit + push the refreshed outputs so the live dashboard +# 5) floors — recompute the copy bot's per-wallet p80 conviction floors from +# the (now-fresh) cache into ../config.json, so copybot.py stays +# in parity with the dashboard's top-20%-by-stake gate. Local-only +# (config.json is gitignored); never touches the curated watchlist. +# 6) dashboard — regenerate + snapshot for auditable forward history +# 7) publish — commit + push the refreshed outputs so the live dashboard # (jaxperro.com/trading) picks up the new sharp list # # Schedule with launchd/cron (Mac must be awake). Logs to daily.log. @@ -29,15 +33,19 @@ if wl: python3 collect.py echo "[daily] $(date '+%F %T') 3/6 re-score skilled (cache-backed, instant)" python3 skill.py -echo "[daily] $(date '+%F %T') 4/6 sharps: conviction scan + last-minute timing gate" +echo "[daily] $(date '+%F %T') 4/7 sharps: conviction scan + last-minute timing gate" python3 conviction_scan.py python3 validate_timing.py -echo "[daily] $(date '+%F %T') 5/6 dashboard" +echo "[daily] $(date '+%F %T') 5/7 floors: recompute copy-bot p80 conviction floors -> ../config.json" +python3 sync_floors.py || echo "[daily] floor sync skipped (no config/watchlist)" +echo "[daily] $(date '+%F %T') portfolio: cache-based \$1k book -> portfolio.json" +python3 portfolio.py || echo "[daily] portfolio skipped" +echo "[daily] $(date '+%F %T') 6/7 dashboard" python3 dashboard.py mkdir -p history && cp watch_skilled.json "history/watch_$(date '+%Y%m%d').json" 2>/dev/null -echo "[daily] $(date '+%F %T') 6/6 publish (commit + push refreshed outputs)" +echo "[daily] $(date '+%F %T') 7/7 publish (commit + push refreshed outputs)" PUBLISH="no changes" -git add watch_skilled.json watch_sharps.json conviction_wallets.json dashboard.html 2>/dev/null +git add watch_skilled.json watch_sharps.json conviction_wallets.json dashboard.html portfolio.json 2>/dev/null if git diff --cached --quiet 2>/dev/null; then echo "[daily] no output changes to publish" elif git commit -q -m "live: daily refresh — skilled + sharp wallets [skip ci]"; then diff --git a/live/portfolio.json b/live/portfolio.json new file mode 100644 index 00000000..49edec6c --- /dev/null +++ b/live/portfolio.json @@ -0,0 +1 @@ +{"started":1780293600.0,"updated":1782247084.59986,"bank":1000.0,"stake":50.0,"equity":5540.92,"liquid":5504.19,"invested":36.73,"realized":4704.19,"pnl":4540.92,"unreal":-13.27,"resolved_count":581,"wins":443,"losses":138,"open_count":1,"missed_count":9,"wallets":[{"name":"raid3r","wallet":"0xa1a77ea9382bb8c3610f3303b66e093f644aace4","bets":176,"invested":0.0,"realized":1192.99},{"name":"0x6d1A94f4","wallet":"0x6d1a94f4bdd53114ec483925d025367db68697fb","bets":244,"invested":0.0,"realized":1480.98},{"name":"Kruto2027","wallet":"0xe8ca3f758c93f44f3ec210542ab78afb7c0bcccb","bets":45,"invested":36.73,"realized":1097.34},{"name":"LSB1","wallet":"0x41558102a796ba971c7567cad41c307e59f8fa41","bets":120,"invested":0.0,"realized":932.88}],"current":[{"title":"Spread: Club ABB (-1.5)","name":"Kruto2027","outcome":"Bamin Real Potos\u00ed","stake":50.0,"val":36.73,"pnl":-13.27,"end":"2026-05-14"}],"resolved":[{"title":"Bellucci vs. Bublik: Match O/U 21.5","name":"Kruto2027","won":true,"stake":50.0,"pnl":22.99,"date":1782194400},{"title":"Will Norway win on 2026-06-22?","name":"raid3r","won":false,"stake":50.0,"pnl":-50.0,"date":1782194400},{"title":"Norway vs. Senegal: Draw at halftime?","name":"raid3r","won":true,"stake":50.0,"pnl":8.14,"date":1782194400},{"title":"New Zealand vs. Egypt: Egypt O/U 4.5 Corners","name":"raid3r","won":false,"stake":50.0,"pnl":-50.0,"date":1782108000},{"title":"Egypt to score first vs. New Zealand?","name":"LSB1","won":true,"stake":50.0,"pnl":8.92,"date":1782108000},{"title":"Egypt leading at halftime?","name":"raid3r","won":true,"stake":50.0,"pnl":9.52,"date":1782108000},{"title":"Will Egypt win on 2026-06-21?","name":"raid3r","won":true,"stake":50.0,"pnl":14.94,"date":1782108000},{"title":"Exact Score: New Zealand 2 - 2 Egypt?","name":"LSB1","won":true,"stake":50.0,"pnl":2.63,"date":1782108000},{"title":"Exact Score: New Zealand 1 - 2 Egypt?","name":"LSB1","won":true,"stake":50.0,"pnl":10.24,"date":1782108000},{"title":"Argentina vs. Austria: Draw at halftime?","name":"raid3r","won":true,"stake":50.0,"pnl":8.84,"date":1782108000},{"title":"Argentina leading at halftime?","name":"raid3r","won":true,"stake":50.0,"pnl":9.04,"date":1782108000},{"title":"Argentina vs. Austria: 1st Half O/U 0.5","name":"LSB1","won":true,"stake":50.0,"pnl":26.25,"date":1782108000},{"title":"Will France win on 2026-06-22?","name":"LSB1","won":true,"stake":50.0,"pnl":3.76,"date":1782108000},{"title":"Ecuador vs. Cura\u00e7ao: O/U 8.5 Total Corners","name":"raid3r","won":false,"stake":50.0,"pnl":-50.0,"date":1782021600},{"title":"Ecuador vs. Cura\u00e7ao: O/U 9.5 Total Corners","name":"raid3r","won":true,"stake":50.0,"pnl":19.31,"date":1782021600},{"title":"Will Japan win on 2026-06-21?","name":"LSB1","won":true,"stake":50.0,"pnl":14.38,"date":1782021600},{"title":"Exact Score: Tunisia 0 - 1 Japan?","name":"LSB1","won":true,"stake":50.0,"pnl":4.52,"date":1782021600},{"title":"Exact Score: Tunisia 1 - 2 Japan?","name":"LSB1","won":true,"stake":50.0,"pnl":4.79,"date":1782021600},{"title":"Tunisia vs. Japan: 2nd Half O/U 0.5","name":"LSB1","won":true,"stake":50.0,"pnl":10.56,"date":1782021600},{"title":"Exact Score: Tunisia 0 - 3 Japan?","name":"LSB1","won":true,"stake":50.0,"pnl":11.11,"date":1782021600},{"title":"Exact Score: Any Other Score?","name":"LSB1","won":true,"stake":50.0,"pnl":55.49,"date":1782021600},{"title":"Spain vs. Saudi Arabia: Saudi Arabia O/U 1.5 Corners","name":"raid3r","won":false,"stake":50.0,"pnl":-50.0,"date":1782021600},{"title":"Exact Score: Belgium 2 - 0 IR Iran?","name":"LSB1","won":true,"stake":50.0,"pnl":6.82,"date":1782021600},{"title":"Exact Score: Uruguay 0 - 2 Cabo Verde?","name":"LSB1","won":true,"stake":50.0,"pnl":2.38,"date":1782021600},{"title":"Exact Score: Uruguay 3 - 1 Cabo Verde?","name":"LSB1","won":true,"stake":50.0,"pnl":20.42,"date":1782021600},{"title":"Exact Score: Uruguay 2 - 2 Cabo Verde?","name":"raid3r","won":true,"stake":50.0,"pnl":100.1,"date":1782021600},{"title":"Uruguay vs. Cabo Verde: O/U 10.5 Total Corners","name":"raid3r","won":false,"stake":50.0,"pnl":-50.0,"date":1782021600},{"title":"Will Uruguay vs. Cabo Verde end in a draw?","name":"raid3r","won":true,"stake":50.0,"pnl":42.59,"date":1782021600},{"title":"Will Uruguay win on 2026-06-21?","name":"raid3r","won":true,"stake":50.0,"pnl":34.06,"date":1782021600},{"title":"Uruguay vs. Cabo Verde: O/U 11.5 Total Corners","name":"raid3r","won":false,"stake":50.0,"pnl":-50.0,"date":1782021600},{"title":"Brazil vs. Haiti: Haiti O/U 2.5 Corners","name":"raid3r","won":true,"stake":50.0,"pnl":96.75,"date":1781935200},{"title":"Brazil vs. Haiti: Brazil O/U 7.5 Corners","name":"raid3r","won":true,"stake":50.0,"pnl":60.15,"date":1781935200},{"title":"Brazil vs. Haiti: Haiti O/U 1.5 Corners","name":"raid3r","won":true,"stake":50.0,"pnl":26.12,"date":1781935200},{"title":"Brazil vs. Haiti: Brazil O/U 8.5 Corners","name":"raid3r","won":true,"stake":50.0,"pnl":22.98,"date":1781935200},{"title":"Brazil vs. Haiti: O/U 8.5 Total Corners","name":"raid3r","won":false,"stake":50.0,"pnl":-50.0,"date":1781935200},{"title":"Brazil vs. Haiti: O/U 7.5 Total Corners","name":"raid3r","won":true,"stake":50.0,"pnl":20.75,"date":1781935200},{"title":"Exact Score: Brazil 0 - 0 Haiti?","name":"LSB1","won":true,"stake":50.0,"pnl":2.26,"date":1781935200},{"title":"Haiti to score first vs. Brazil?","name":"LSB1","won":true,"stake":50.0,"pnl":3.76,"date":1781935200},{"title":"Brazil vs. Haiti: Brazil 1st Half O/U 1.5","name":"LSB1","won":true,"stake":50.0,"pnl":32.19,"date":1781935200},{"title":"Exact Score: Brazil 3 - 1 Haiti?","name":"LSB1","won":true,"stake":50.0,"pnl":3.35,"date":1781935200},{"title":"Brazil vs. Haiti: 2nd Half O/U 0.5","name":"LSB1","won":true,"stake":50.0,"pnl":1.55,"date":1781935200},{"title":"T\u00fcrkiye leading at halftime?","name":"raid3r","won":true,"stake":50.0,"pnl":11.73,"date":1781935200},{"title":"T\u00fcrkiye vs. Paraguay: Paraguay O/U 2.5 Corners","name":"raid3r","won":false,"stake":50.0,"pnl":-50.0,"date":1781935200},{"title":"Will Paraguay win on 2026-06-19?","name":"raid3r","won":true,"stake":50.0,"pnl":71.95,"date":1781935200},{"title":"T\u00fcrkiye vs. Paraguay: O/U 10.5 Total Corners","name":"raid3r","won":false,"stake":50.0,"pnl":-50.0,"date":1781935200},{"title":"T\u00fcrkiye vs. Paraguay: O/U 11.5 Total Corners","name":"raid3r","won":false,"stake":50.0,"pnl":-50.0,"date":1781935200},{"title":"Will T\u00fcrkiye win on 2026-06-19?","name":"raid3r","won":true,"stake":50.0,"pnl":31.7,"date":1781935200},{"title":"T\u00fcrkiye vs. Paraguay: O/U 12.5 Total Corners","name":"raid3r","won":true,"stake":50.0,"pnl":25.18,"date":1781935200},{"title":"Will Netherlands win on 2026-06-20?","name":"raid3r","won":true,"stake":50.0,"pnl":23.53,"date":1781935200},{"title":"Netherlands leading at halftime?","name":"LSB1","won":true,"stake":50.0,"pnl":4.95,"date":1781935200},{"title":"Exact Score: Netherlands 2 - 1 Sweden?","name":"LSB1","won":true,"stake":50.0,"pnl":8.14,"date":1781935200},{"title":"Exact Score: Any Other Score?","name":"LSB1","won":true,"stake":50.0,"pnl":22.93,"date":1781935200},{"title":"Will Germany vs. C\u00f4te d'Ivoire end in a draw?","name":"raid3r","won":true,"stake":50.0,"pnl":13.46,"date":1781935200},{"title":"Will Germany win on 2026-06-20?","name":"raid3r","won":true,"stake":50.0,"pnl":29.62,"date":1781935200},{"title":"Will C\u00f4te d'Ivoire win on 2026-06-20?","name":"raid3r","won":true,"stake":50.0,"pnl":9.9,"date":1781935200},{"title":"Germany vs. C\u00f4te d'Ivoire: Draw at halftime?","name":"raid3r","won":true,"stake":50.0,"pnl":28.01,"date":1781935200},{"title":"C\u00f4te d'Ivoire leading at halftime?","name":"raid3r","won":false,"stake":50.0,"pnl":-50.0,"date":1781935200},{"title":"Germany leading at halftime?","name":"raid3r","won":false,"stake":50.0,"pnl":-50.0,"date":1781935200},{"title":"Germany to score first vs. C\u00f4te d'Ivoire?","name":"LSB1","won":true,"stake":50.0,"pnl":9.25,"date":1781935200},{"title":"UD Almer\u00eda vs. M\u00e1laga CF: M\u00e1laga CF O/U 0.5","name":"LSB1","won":true,"stake":50.0,"pnl":10.27,"date":1781935200}],"missed":[{"title":"Bitcoin Up or Down - June 10, 12:55AM-1:00AM ET","name":"0x6d1A94f4","won":false,"stake":50.0,"pnl":-50.0,"date":1781071200},{"title":"Bitcoin Up or Down - June 10, 1:00AM-1:05AM ET","name":"0x6d1A94f4","won":false,"stake":50.0,"pnl":-50.0,"date":1781071200},{"title":"Bitcoin Up or Down - June 10, 1:15AM-1:20AM ET","name":"0x6d1A94f4","won":false,"stake":50.0,"pnl":-50.0,"date":1781071200},{"title":"Bitcoin Up or Down - June 10, 1:25AM-1:30AM ET","name":"0x6d1A94f4","won":true,"stake":50.0,"pnl":20.58,"date":1781071200},{"title":"Bitcoin Up or Down - June 10, 1:30AM-1:35AM ET","name":"0x6d1A94f4","won":true,"stake":50.0,"pnl":61.83,"date":1781071200},{"title":"Bitcoin Up or Down - June 10, 1:35AM-1:40AM ET","name":"0x6d1A94f4","won":false,"stake":50.0,"pnl":-50.0,"date":1781071200},{"title":"Bitcoin Up or Down - June 10, 1:40AM-1:45AM ET","name":"0x6d1A94f4","won":true,"stake":50.0,"pnl":20.23,"date":1781071200},{"title":"Bitcoin Up or Down - June 10, 1:45AM-1:50AM ET","name":"0x6d1A94f4","won":false,"stake":50.0,"pnl":-50.0,"date":1781071200},{"title":"Bitcoin Up or Down - June 10, 1:50AM-1:55AM ET","name":"0x6d1A94f4","won":true,"stake":50.0,"pnl":18.08,"date":1781071200}],"missed_pnl":-129.29} \ No newline at end of file diff --git a/live/portfolio.py b/live/portfolio.py new file mode 100644 index 00000000..d2af1c29 --- /dev/null +++ b/live/portfolio.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 +"""Precompute the $1,000 paper portfolio server-side, off the cache. + +The dashboard's top page used to replay the followed wallets' trades client-side, +which (a) hammered the data-api/clob from the browser and (b) phantom-locked capital +because the data-api misses resolution dates for high-volume wallets. This computes +the same book here instead, sourced from cache.duckdb — which already stores each +resolved bet's entry price, size, win/loss AND resolution time (res_t), so capital +RECYCLES correctly (cash frees at the true resolution moment). Output -> portfolio.json, +which the dashboard reads in one request. + +Model: a $1,000 account that mirrors each followed wallet's CONVICTION bets (top-20% +stake) at a flat $50, held to resolution (the cache has no sell events, which is the +right model for the hold-to-resolution wallets we follow). One position per market +(first wallet to enter wins the slot); when capital is fully deployed a bet is MISSED. +Resolved history + realized P&L come from the cache; currently-open bets come from a +small live /positions pull so the page can still show what's in flight. +""" +import json +import os +import ssl +import time +import urllib.request + +import cache +import smart_money as sm + +_SSL = ssl._create_unverified_context() + +HERE = os.path.dirname(__file__) +BANK = 1000.0 +STAKE = 50.0 +START = time.mktime(time.strptime("2026-06-01", "%Y-%m-%d")) # copy from June 1 +GAMMA = "https://gamma-api.polymarket.com" + +# the followed wallets — single source of truth (dashboard renders names from the feed) +WALLETS = [ + {"name": "raid3r", "wallet": "0xa1a77ea9382bb8c3610f3303b66e093f644aace4"}, + {"name": "0x6d1A94f4", "wallet": "0x6d1a94f4bdd53114ec483925d025367db68697fb"}, + {"name": "Kruto2027", "wallet": "0xe8ca3f758c93f44f3ec210542ab78afb7c0bcccb"}, + {"name": "LSB1", "wallet": "0x41558102a796ba971c7567cad41c307e59f8fa41"}, +] + +_MKT = {} +def market_meta(cond): + """Market title for display, from the CLOB market endpoint (gamma's condition_ids + filter returns nothing for resolved markets) — cached.""" + if cond not in _MKT: + try: + r = urllib.request.urlopen(urllib.request.Request( + f"https://clob.polymarket.com/markets/{cond}", headers={"User-Agent": "Mozilla/5.0"}), + timeout=20, context=_SSL) + m = json.loads(r.read()) + _MKT[cond] = {"title": m.get("question") or "", "slug": m.get("market_slug") or ""} + except Exception: + _MKT[cond] = {"title": "", "slug": ""} + return _MKT[cond] + + +def conviction_bets(): + """Every followed wallet's resolved conviction bets from the cache, with entry time.""" + out = [] + for w in WALLETS: + ent = cache.get_entries(w["wallet"]) # cond -> first buy ts + bets = [b for b in cache.get_bets(w["wallet"]) if (b["size"] or 0) > 0] + thr = cache.conv_cutoff(b["size"] for b in bets) + for b in bets: + if b["size"] < thr: + continue + et = ent.get(b["cond"]) + if not et or et < START: # only June 1+ entries + continue + out.append({"wallet": w["wallet"], "name": w["name"], "cond": b["cond"], + "entry_t": et, "p": max(0.001, min(0.999, b["p"] or 0)), + "won": b["won"], "res_t": b["res_t"] or 0}) + return out + + +def open_bets(): + """Currently-held conviction positions (live /positions pull, small) for the + 'current bets' panel — the cache only has resolved bets.""" + out = [] + for w in WALLETS: + ent = cache.get_entries(w["wallet"]) + ps = sm.get_json("/positions", {"user": w["wallet"], "limit": 500, "sizeThreshold": 0}) or [] + sizes = [(p.get("initialValue") or 0) for p in ps] + thr = cache.conv_cutoff(sizes) + for p in ps: + cp = p.get("curPrice", 0) or 0 + if cp <= 0.001 or cp >= 0.999: # resolved -> belongs to history, not open + continue + if (p.get("initialValue") or 0) < thr: + continue + out.append({"wallet": w["wallet"], "name": w["name"], "cond": p.get("conditionId"), + "entry_t": ent.get(p.get("conditionId"), 0), + "p": max(0.001, min(0.999, p.get("avgPrice", 0) or 0)), + "cur": cp, "title": p.get("title") or "", "outcome": p.get("outcome") or "", + "end": p.get("endDate")}) + return out + + +def main(): + now = time.time() + resolved_pool = conviction_bets() + open_pool = open_bets() + # merge into one entry-ordered stream; one position per market (earliest entry wins) + by_mkt = {} + for b in resolved_pool: + b["kind"] = "res" + if b["cond"] not in by_mkt or b["entry_t"] < by_mkt[b["cond"]]["entry_t"]: + by_mkt[b["cond"]] = b + for b in open_pool: + if b["cond"] and (b["cond"] not in by_mkt or b["entry_t"] < by_mkt[b["cond"]]["entry_t"]): + b["kind"] = "open"; by_mkt[b["cond"]] = b + stream = sorted(by_mkt.values(), key=lambda b: b["entry_t"]) + + cash = BANK + realized = 0.0 + held = [] # (free_t, stake, payoff) payoff paid at free_t + perW = {w["wallet"]: {"name": w["name"], "wallet": w["wallet"], "bets": 0, + "invested": 0.0, "realized": 0.0} for w in WALLETS} + resolved, current, missed = [], [], [] + + def free(upto): + nonlocal cash, realized + keep = [] + for ft, stake, payoff, rec in held: + if ft and ft <= upto and rec["kind"] == "res": + cash += payoff; realized += payoff - stake; perW[rec["wallet"]]["realized"] += payoff - stake + rec["pnl"] = payoff - stake + resolved.append(rec) + else: + keep.append((ft, stake, payoff, rec)) + held[:] = keep + + for b in stream: + free(b["entry_t"]) + if cash >= STAKE: + cash -= STAKE; perW[b["wallet"]]["bets"] += 1 + shares = STAKE / b["p"] + if b["kind"] == "res": + payoff = shares * (1.0 if b["won"] else 0.0) + held.append((b["res_t"] or now, STAKE, payoff, b)) + else: # currently open -> mark to market, no free yet + held.append((None, STAKE, 0.0, b)) + b["val"] = shares * b["cur"]; b["stake"] = STAKE + else: + missed.append(b) + free(now) + # finalize open (still held with kind==open): mark to market + invested = 0.0 + for ft, stake, payoff, rec in held: + if rec["kind"] == "open": + invested += rec["val"]; rec["pnl"] = rec["val"] - stake + perW[rec["wallet"]]["invested"] += rec["val"] + current.append(rec) + + # enrich resolved + missed with titles, keep most-recent 60 + resolved.sort(key=lambda r: r.get("res_t") or 0, reverse=True) + for r in resolved[:60]: + m = market_meta(r["cond"]); r["title"] = m["title"] + missed.sort(key=lambda m: m.get("res_t") or 0, reverse=True) + for m in missed[:60]: + m["title"] = market_meta(m["cond"])["title"] + # hypothetical P&L had we been able to afford it (held to resolution) + m["pnl"] = STAKE * ((1.0 / m["p"]) - 1) if m["won"] else -STAKE + wins = sum(1 for r in resolved if r.get("won")) + equity = cash + invested + out = { + "started": START, "updated": now, + "bank": BANK, "stake": STAKE, + "equity": round(equity, 2), "liquid": round(cash, 2), "invested": round(invested, 2), + "realized": round(realized, 2), "pnl": round(equity - BANK, 2), + "unreal": round(invested - STAKE * len(current), 2), + "resolved_count": len(resolved), "wins": wins, "losses": len(resolved) - wins, + "open_count": len(current), "missed_count": len(missed), + "wallets": [{"name": v["name"], "wallet": v["wallet"], "bets": v["bets"], + "invested": round(v["invested"], 2), "realized": round(v["realized"], 2)} + for v in perW.values()], + "current": [{"title": c.get("title", ""), "name": c["name"], "outcome": c.get("outcome", ""), + "stake": STAKE, "val": round(c["val"], 2), "pnl": round(c["pnl"], 2), + "end": c.get("end")} for c in sorted(current, key=lambda c: c["entry_t"])], + "resolved": [{"title": r.get("title", ""), "name": r["name"], "won": r["won"], + "stake": STAKE, "pnl": round(r["pnl"], 2), "date": r.get("res_t")} + for r in resolved[:60]], + "missed": [{"title": m.get("title", ""), "name": m["name"], "won": m["won"], + "stake": STAKE, "pnl": round(m["pnl"], 2), "date": m.get("res_t")} + for m in missed[:60]], + "missed_pnl": round(sum(STAKE * ((1.0 / m["p"]) - 1) if m["won"] else -STAKE for m in missed), 2), + } + json.dump(out, open(os.path.join(HERE, "portfolio.json"), "w"), separators=(",", ":")) + print(f"portfolio: equity ${equity:,.0f} ({(equity-BANK)/BANK*100:+.0f}%) | realized ${realized:+,.0f} " + f"| {len(resolved)} resolved ({wins}W/{len(resolved)-wins}L) | {len(current)} open " + f"| {len(missed)} missed | -> portfolio.json", flush=True) + + +if __name__ == "__main__": + main()