2026-06-23 14:43:50 -06:00
|
|
|
|
#!/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%
|
2026-07-07 03:09:21 -04:00
|
|
|
|
stake). Exits MIRROR the signal, like the live bot (2026-07-07): when the wallet
|
|
|
|
|
|
fully closed a position pre-resolution, the replay sells there too — close time from
|
|
|
|
|
|
/closed-positions, exit price reconstructed as avgPrice + realizedPnl/totalBought,
|
|
|
|
|
|
exit taker fee + slippage haircut paid, status SOLD. Bets they held to resolution
|
|
|
|
|
|
settle at the chain-truth payout (1/0/0.5 — refunds are scratches, not losses).
|
|
|
|
|
|
Complete in-window round trips on still-unresolved markets are replayed as
|
|
|
|
|
|
entry+exit (the old hold-to-resolution model missed them entirely). Sizing is
|
|
|
|
|
|
DYNAMIC — each bet stakes PCT of
|
2026-07-02 10:54:07 -04:00
|
|
|
|
current equity (Kelly-style compounding), halved in a >20% drawdown, capped at
|
|
|
|
|
|
EVENT_CAP concurrent bets per real-world event — and entries pay the Polymarket taker
|
|
|
|
|
|
fee plus a lag-slippage price haircut (FEE_RATE / SLIP / LAG_EST_S), so the book
|
|
|
|
|
|
models what a real copier nets, not the idealized zero-cost mirror. One position per
|
|
|
|
|
|
market (first wallet to enter wins the slot); when capital is fully deployed a bet is
|
|
|
|
|
|
MISSED.
|
2026-06-23 14:43:50 -06:00
|
|
|
|
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.
|
|
|
|
|
|
"""
|
2026-07-05 22:15:37 -04:00
|
|
|
|
import argparse
|
2026-06-23 14:43:50 -06:00
|
|
|
|
import json
|
|
|
|
|
|
import os
|
2026-07-02 10:54:07 -04:00
|
|
|
|
import re
|
2026-06-23 14:43:50 -06:00
|
|
|
|
import ssl
|
|
|
|
|
|
import time
|
|
|
|
|
|
import urllib.request
|
2026-07-02 10:54:07 -04:00
|
|
|
|
from concurrent.futures import ThreadPoolExecutor
|
2026-06-23 14:43:50 -06:00
|
|
|
|
|
|
|
|
|
|
import cache
|
2026-07-07 01:54:58 -04:00
|
|
|
|
import payouts
|
2026-06-23 14:43:50 -06:00
|
|
|
|
import smart_money as sm
|
2026-07-05 22:15:37 -04:00
|
|
|
|
import trust
|
2026-06-23 14:43:50 -06:00
|
|
|
|
|
|
|
|
|
|
_SSL = ssl._create_unverified_context()
|
|
|
|
|
|
|
|
|
|
|
|
HERE = os.path.dirname(__file__)
|
2026-07-08 16:55:15 -04:00
|
|
|
|
# BANK is set after the arg/backtest.json parse below
|
2026-06-23 14:43:50 -06:00
|
|
|
|
GAMMA = "https://gamma-api.polymarket.com"
|
|
|
|
|
|
|
2026-07-05 22:15:37 -04:00
|
|
|
|
# ---- interchangeable-wallet replay: live/backtest.json ----------------------
|
|
|
|
|
|
# {days, stake_cap_usd, class_pct: {volume, whale}, wallets: [{wallet, name,
|
|
|
|
|
|
# class}]}. Rolling window: "what if I started following these wallets `days`
|
|
|
|
|
|
# ago." Class 'whale' replays EVERY trusted bet at class_pct.whale of equity;
|
|
|
|
|
|
# 'volume' (default) replays conviction bets only (top-20% by stake, threshold
|
|
|
|
|
|
# from PRE-window trusted bets so it can't peek) at class_pct.volume.
|
|
|
|
|
|
# Ad-hoc runs that leave the dashboard feed alone:
|
|
|
|
|
|
# python3 portfolio.py --wallets 0xabc,0xdef:whale --days 30 --out /tmp/t.json
|
|
|
|
|
|
_ap = argparse.ArgumentParser()
|
|
|
|
|
|
_ap.add_argument("--wallets", help="comma list of addresses; ':whale' suffix opts into whale class")
|
|
|
|
|
|
_ap.add_argument("--days", type=int, help="window length (default backtest.json's, else 30)")
|
2026-07-08 16:55:15 -04:00
|
|
|
|
_ap.add_argument("--bank", type=float, help="starting bankroll (default backtest.json's, else 1000)")
|
2026-07-05 22:15:37 -04:00
|
|
|
|
_ap.add_argument("--out", help="output path (default $PORTFOLIO_OUT or portfolio.json)")
|
|
|
|
|
|
_ARGS, _ = _ap.parse_known_args()
|
|
|
|
|
|
try:
|
|
|
|
|
|
_BT = json.load(open(os.path.join(HERE, "backtest.json")))
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
_BT = {}
|
|
|
|
|
|
DAYS = _ARGS.days or int(_BT.get("days", 30))
|
2026-07-08 16:55:15 -04:00
|
|
|
|
BANK = float(_ARGS.bank or _BT.get("bank", 1000.0))
|
2026-07-05 22:15:37 -04:00
|
|
|
|
START = time.time() - DAYS * 86400 # rolling: started following DAYS ago
|
|
|
|
|
|
CLASS_PCT = {"volume": 0.04, "whale": 0.12, **(_BT.get("class_pct") or {})}
|
|
|
|
|
|
BASE_PCT = CLASS_PCT.get("volume", 0.04) # sweep threshold stays on the base class
|
|
|
|
|
|
|
2026-07-02 10:54:07 -04:00
|
|
|
|
# ---- dynamic sizing (mirrors the live copybot) ------------------------------
|
|
|
|
|
|
# Each new bet stakes PCT of CURRENT equity (cash + open cost basis) so the book
|
|
|
|
|
|
# compounds in both directions; the stake is halved while equity sits below
|
|
|
|
|
|
# DD_THRESHOLD of its high-water mark, and clamped to [STAKE_MIN, STAKE_CAP].
|
2026-07-02 11:21:08 -04:00
|
|
|
|
# EVENT_CAP >0 limits concurrent bets whose markets belong to the same real-world
|
|
|
|
|
|
# event (a game's markets settle together — one correlated bet, not N diversified
|
|
|
|
|
|
# ones); 0 = off, mirror every conviction trade.
|
2026-07-05 23:41:59 -04:00
|
|
|
|
# per-class equity fractions come from CLASS_PCT (backtest.json). No stake cap
|
|
|
|
|
|
# and no banked reserve: the natural ceiling is the FOLLOWED WALLET'S OWN BET —
|
|
|
|
|
|
# a copy is never larger than what the wallet actually staked (you can't
|
|
|
|
|
|
# out-conviction the signal, and it keeps fills inside the size the market
|
|
|
|
|
|
# actually absorbed).
|
2026-07-05 22:15:37 -04:00
|
|
|
|
STAKE_MIN = 5.0
|
2026-07-02 11:21:08 -04:00
|
|
|
|
EVENT_CAP = 0
|
2026-07-02 10:54:07 -04:00
|
|
|
|
DD_THRESHOLD, DD_FACTOR = 0.80, 0.5
|
2026-07-02 23:35:51 -04:00
|
|
|
|
# skip entries above this price. High-price favorites win pennies and lose
|
|
|
|
|
|
# whole stakes: the June sweep (caps 0.75-1.0) peaked at 0.95 — >95¢ bets added
|
|
|
|
|
|
# ~23 wins yet LOWERED final equity (slip+fee eat the ~1-3% payouts, and the
|
|
|
|
|
|
# locked capital compounds better elsewhere). Deep caps (<=0.85) cut real
|
|
|
|
|
|
# winners. Mirrored by the bot's follow.max_entry; env-overridable for sweeps.
|
|
|
|
|
|
MAX_ENTRY = float(os.environ.get("MAX_ENTRY", 0.95))
|
2026-07-05 22:15:37 -04:00
|
|
|
|
OUT = _ARGS.out or os.environ.get("PORTFOLIO_OUT", "portfolio.json")
|
2026-07-02 10:54:07 -04:00
|
|
|
|
|
2026-07-02 09:51:35 -04:00
|
|
|
|
# ---- realism model (matches the live copybot) -------------------------------
|
|
|
|
|
|
# Taker fee (Polymarket V2, since 2026-03-30): fee = shares·rate·p·(1−p); for a
|
2026-07-02 10:54:07 -04:00
|
|
|
|
# $stake buy that's stake·rate·(1−p). Sports 0.03 — the follow set's category.
|
|
|
|
|
|
# Redeeming at resolution is fee-free, so only entries pay here
|
2026-07-02 09:51:35 -04:00
|
|
|
|
# (hold-to-resolution model, no mirrored exits).
|
|
|
|
|
|
FEE_RATE = 0.03
|
|
|
|
|
|
# Copy lag: we enter LAG_EST_S after the wallet does, at a slightly worse price.
|
|
|
|
|
|
# SLIP is the entry-price penalty estimate: the live bot measured +0.35% at ~5min
|
|
|
|
|
|
# lag; a 60s poller should see less — 0.5% is a conservative flat haircut.
|
|
|
|
|
|
LAG_EST_S = 90
|
|
|
|
|
|
SLIP = 0.005
|
|
|
|
|
|
|
2026-07-05 22:15:37 -04:00
|
|
|
|
# the replayed wallets — from --wallets, else live/backtest.json (editable:
|
|
|
|
|
|
# add/remove/swap any address there; classes default to 'volume')
|
|
|
|
|
|
if _ARGS.wallets:
|
|
|
|
|
|
WALLETS = []
|
|
|
|
|
|
for _tok in _ARGS.wallets.split(","):
|
|
|
|
|
|
_addr, _, _cls = _tok.strip().partition(":")
|
|
|
|
|
|
WALLETS.append({"wallet": _addr, "name": _addr[:10], "class": _cls or "volume"})
|
|
|
|
|
|
else:
|
|
|
|
|
|
WALLETS = [{"wallet": w["wallet"], "name": w.get("name", w["wallet"][:10]),
|
|
|
|
|
|
"class": w.get("class", "volume")} for w in _BT.get("wallets", [])]
|
|
|
|
|
|
if not WALLETS:
|
|
|
|
|
|
raise SystemExit("no wallets to replay: create live/backtest.json or pass --wallets")
|
2026-06-23 14:43:50 -06:00
|
|
|
|
|
2026-07-08 11:35:31 -04:00
|
|
|
|
# the live bot's pinned conviction floors (sync_floors -> copybot.paper.json).
|
|
|
|
|
|
# When a replayed wallet is one the bot follows, gate on the SAME floor the bot
|
|
|
|
|
|
# enforces — a recomputed p80 can drift a few % from the pin and then the two
|
|
|
|
|
|
# books take different bets (alignment audit 2026-07-08). Candidate sets passed
|
|
|
|
|
|
# via --wallets have no pin and fall back to the p80 recompute.
|
|
|
|
|
|
try:
|
|
|
|
|
|
_PINNED = {w["wallet"].lower(): w["floor"]
|
|
|
|
|
|
for w in json.load(open(os.path.join(HERE, "copybot.paper.json")))["wallets"]
|
|
|
|
|
|
if w.get("floor")}
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
_PINNED = {}
|
|
|
|
|
|
|
2026-07-02 09:51:35 -04:00
|
|
|
|
|
2026-07-02 10:54:07 -04:00
|
|
|
|
def entry_model(p, stake):
|
|
|
|
|
|
"""(effective entry price, entry fee, total cash cost) of a $stake copy:
|
2026-07-02 09:51:35 -04:00
|
|
|
|
price worsened by the lag-slippage haircut, taker fee on top of the stake."""
|
|
|
|
|
|
p_eff = min(0.999, p * (1 + SLIP))
|
2026-07-02 10:54:07 -04:00
|
|
|
|
fee = stake * FEE_RATE * (1 - p_eff)
|
|
|
|
|
|
return p_eff, fee, stake + fee
|
2026-07-02 09:51:35 -04:00
|
|
|
|
|
2026-07-08 11:35:31 -04:00
|
|
|
|
|
|
|
|
|
|
def _sold_pre_resolution(cx, won, res_t):
|
|
|
|
|
|
"""Did the wallet SELL this position (a mirrorable exit the live bot would
|
|
|
|
|
|
copy) rather than redeem it at resolution? The timestamp test only works
|
|
|
|
|
|
when res_t is a real resolution moment — for in-play markets res_t is the
|
|
|
|
|
|
market's endDate metadata (game-DAY midnight), which pre-dates the wallet's
|
|
|
|
|
|
own entry, so the exit can never test earlier and every in-play sell used
|
|
|
|
|
|
to book as held-to-resolution (Kruto's Jul-7 Brewers sells, found
|
|
|
|
|
|
2026-07-08). Price is the fallback truth: a genuine sell prints a mid
|
|
|
|
|
|
price; a redeem (or post-resolution dump) prints ≈ the payout. When the two
|
|
|
|
|
|
disagree we book the wallet's actual exit print — that is what happened on
|
|
|
|
|
|
Polymarket, and it also self-corrects a poisoned won flag. A 50/50 refund
|
|
|
|
|
|
redeemed at 0.5 books as sold-at-0.5 (same money as refund, label S not R)."""
|
|
|
|
|
|
if res_t and cx["ts"] < res_t - 300:
|
|
|
|
|
|
return True
|
|
|
|
|
|
xp = cx.get("exit_p")
|
|
|
|
|
|
if xp is None:
|
|
|
|
|
|
return False
|
|
|
|
|
|
# a redeem reconstructs at EXACTLY the payout (payoff×shares/shares); the
|
|
|
|
|
|
# 0.02 band only absorbs avg-price rounding artifacts (0.8999998…), so a
|
|
|
|
|
|
# 0.96 exit on a won market correctly reads as a pre-resolution sell
|
|
|
|
|
|
payout = 1.0 if won else 0.0
|
|
|
|
|
|
return abs(xp - payout) > 0.02
|
|
|
|
|
|
|
2026-06-23 14:43:50 -06:00
|
|
|
|
_MKT = {}
|
2026-07-02 10:54:07 -04:00
|
|
|
|
_SLUG_CACHE = os.path.join(HERE, "slug_cache.json")
|
|
|
|
|
|
try:
|
|
|
|
|
|
_MKT.update(json.load(open(_SLUG_CACHE)))
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-23 14:43:50 -06:00
|
|
|
|
def market_meta(cond):
|
2026-07-02 10:54:07 -04:00
|
|
|
|
"""Market title + slug from the CLOB market endpoint (gamma's condition_ids
|
|
|
|
|
|
filter returns nothing for resolved markets) — cached in-process AND on disk
|
|
|
|
|
|
(slug_cache.json), since the event cap needs a slug for every replayed market,
|
|
|
|
|
|
not just the top-60 displayed."""
|
2026-06-23 14:43:50 -06:00
|
|
|
|
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:
|
2026-07-02 10:54:07 -04:00
|
|
|
|
return {"title": "", "slug": ""} # transient failure — don't cache
|
2026-06-23 14:43:50 -06:00
|
|
|
|
return _MKT[cond]
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-02 10:54:07 -04:00
|
|
|
|
def save_slug_cache():
|
|
|
|
|
|
try:
|
|
|
|
|
|
json.dump({c: v for c, v in _MKT.items() if v.get("slug")},
|
|
|
|
|
|
open(_SLUG_CACHE, "w"))
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def event_key(slug):
|
|
|
|
|
|
"""Correlation-group id: Polymarket sub-splits one game across slugs
|
|
|
|
|
|
(`…-2026-07-01-more-markets`, `…-2026-07-01-second-half-result`), so dated
|
|
|
|
|
|
slugs collapse to their `…-YYYY-MM-DD` prefix; undated slugs stand as-is."""
|
|
|
|
|
|
m = re.match(r"(.*?\d{4}-\d{2}-\d{2})", slug or "")
|
|
|
|
|
|
return m.group(1) if m else (slug or None)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-05 22:15:37 -04:00
|
|
|
|
_WALLET_THR = {} # wallet -> conviction threshold used this run (0 for whales)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def window_bets():
|
|
|
|
|
|
"""Every replayed wallet's TRUSTED resolved bets entered inside the window,
|
|
|
|
|
|
with entry time. Trusted rows only (trust.py) — outcomes observed post-
|
|
|
|
|
|
resolution, res_t=ts poison excluded — so any pasted-in wallet is scored
|
|
|
|
|
|
honestly, not on cache marks. Class rules: 'whale' replays EVERY bet;
|
|
|
|
|
|
'volume' only conviction bets, with the p80 threshold computed from
|
|
|
|
|
|
PRE-window trusted bets (falls back to full history for wallets with no
|
|
|
|
|
|
pre-window sample) so the threshold can't peek at the window it scores."""
|
2026-06-23 14:43:50 -06:00
|
|
|
|
out = []
|
2026-07-01 23:15:14 -06:00
|
|
|
|
now = time.time()
|
2026-07-05 22:15:37 -04:00
|
|
|
|
trust.ensure_cons(cache.query)
|
2026-06-23 14:43:50 -06:00
|
|
|
|
for w in WALLETS:
|
2026-07-05 22:15:37 -04:00
|
|
|
|
cache.get_bets(w["wallet"]) # ensure pulled/fresh (pulls brand-new wallets)
|
2026-06-23 14:43:50 -06:00
|
|
|
|
ent = cache.get_entries(w["wallet"]) # cond -> first buy ts
|
2026-07-05 22:15:37 -04:00
|
|
|
|
rows = trust.trusted_wallet_rows(cache.query, w["wallet"], now)
|
|
|
|
|
|
best = {} # one bet per market: largest-stake token
|
|
|
|
|
|
for cond, asset, won, p, res_t, size in rows:
|
2026-07-07 01:54:58 -04:00
|
|
|
|
if cond not in best or size > best[cond][4]:
|
|
|
|
|
|
best[cond] = (asset, won, p, res_t, size)
|
2026-07-05 22:15:37 -04:00
|
|
|
|
if w.get("class") == "whale":
|
|
|
|
|
|
thr = 0.0
|
2026-07-08 11:35:31 -04:00
|
|
|
|
elif w["wallet"].lower() in _PINNED:
|
|
|
|
|
|
thr = _PINNED[w["wallet"].lower()]
|
2026-07-05 22:15:37 -04:00
|
|
|
|
else:
|
2026-07-07 01:54:58 -04:00
|
|
|
|
pre = [size for asset, won, p, res_t, size in best.values() if res_t < START]
|
2026-07-05 22:15:37 -04:00
|
|
|
|
thr = cache.conv_cutoff(pre if pre else
|
|
|
|
|
|
[size for *_, size in best.values()])
|
|
|
|
|
|
_WALLET_THR[w["wallet"]] = thr
|
2026-07-07 03:09:21 -04:00
|
|
|
|
# the wallet's fully-closed positions: close time + reconstructed exit
|
|
|
|
|
|
# price. The live bot MIRRORS exits, so the replay must too — a bet the
|
|
|
|
|
|
# signal sold pre-resolution exits the book right there (status SOLD),
|
|
|
|
|
|
# not at resolution (the old hold-to-resolution ceiling).
|
|
|
|
|
|
closed = closed_positions(w["wallet"])
|
2026-07-07 01:54:58 -04:00
|
|
|
|
for cond, (asset, won, p, res_t, size) in best.items():
|
2026-07-05 22:15:37 -04:00
|
|
|
|
if size < thr or p > MAX_ENTRY:
|
2026-07-01 23:15:14 -06:00
|
|
|
|
continue
|
2026-07-05 22:15:37 -04:00
|
|
|
|
et = ent.get(cond)
|
|
|
|
|
|
if not et or et < START: # only in-window entries
|
2026-06-23 14:43:50 -06:00
|
|
|
|
continue
|
2026-07-07 03:09:21 -04:00
|
|
|
|
b = {"wallet": w["wallet"], "name": w["name"], "cond": cond,
|
|
|
|
|
|
"asset": asset, "cls": w.get("class", "volume"),
|
|
|
|
|
|
"their": size, "entry_t": et, "p": p, "won": won,
|
|
|
|
|
|
"res_t": res_t or 0}
|
|
|
|
|
|
cx = closed.get(asset)
|
2026-07-08 11:35:31 -04:00
|
|
|
|
if cx and _sold_pre_resolution(cx, won, res_t):
|
2026-07-07 03:09:21 -04:00
|
|
|
|
b["exit_t"], b["exit_p"] = cx["ts"], cx["exit_p"]
|
|
|
|
|
|
out.append(b)
|
|
|
|
|
|
# complete round trips the cache can't see: entered AND fully exited
|
|
|
|
|
|
# in-window on a market that never resolved (or hasn't yet) — the live
|
|
|
|
|
|
# bot would have copied both legs, so the replay does too
|
|
|
|
|
|
for asset, cx in closed.items():
|
|
|
|
|
|
cond = cx["cond"]
|
|
|
|
|
|
if (cond in best or not cond or cx["ts"] < START
|
|
|
|
|
|
or (cx["iv"] or 0) < thr or cx["p"] > MAX_ENTRY):
|
|
|
|
|
|
continue
|
|
|
|
|
|
et = ent.get(cond)
|
|
|
|
|
|
if not et or et < START:
|
|
|
|
|
|
continue
|
2026-07-08 11:35:31 -04:00
|
|
|
|
wp = payouts.truth(cond)
|
|
|
|
|
|
if wp is not None and abs((cx["exit_p"] if cx["exit_p"] is not None
|
|
|
|
|
|
else wp) - wp) <= 0.02:
|
|
|
|
|
|
# resolved on-chain AND their close prints ≈ the payout: the
|
|
|
|
|
|
# close was the redeem, not a sell. The trusted cache row
|
|
|
|
|
|
# usually books this bet — but when res_t is bogus forward
|
|
|
|
|
|
# metadata (in-play/tennis endDate: Kruto's Chidekh total sat
|
|
|
|
|
|
# "unresolved until Jul 14" on a Jul-5 match) the row never
|
|
|
|
|
|
# qualifies and the bet used to vanish here. Book it as
|
|
|
|
|
|
# held-to-resolution at chain truth (2026-07-08).
|
|
|
|
|
|
best[cond] = None
|
|
|
|
|
|
out.append({"wallet": w["wallet"], "name": w["name"], "cond": cond,
|
|
|
|
|
|
"asset": asset, "cls": w.get("class", "volume"),
|
|
|
|
|
|
"their": cx["iv"], "entry_t": et, "p": cx["p"],
|
|
|
|
|
|
"won": wp > 0.5, "wp": wp, "res_t": cx["ts"],
|
|
|
|
|
|
"title": cx.get("title") or ""})
|
|
|
|
|
|
continue
|
|
|
|
|
|
# exit print ≠ payout (or market genuinely unresolved): a real
|
|
|
|
|
|
# pre-resolution sell — mirror it, like the live bot
|
2026-07-07 03:09:21 -04:00
|
|
|
|
best[cond] = None # one per market, same as everywhere
|
2026-07-05 22:15:37 -04:00
|
|
|
|
out.append({"wallet": w["wallet"], "name": w["name"], "cond": cond,
|
2026-07-07 01:54:58 -04:00
|
|
|
|
"asset": asset, "cls": w.get("class", "volume"),
|
2026-07-07 03:09:21 -04:00
|
|
|
|
"their": cx["iv"], "entry_t": et, "p": cx["p"],
|
|
|
|
|
|
"won": None, "res_t": 0,
|
|
|
|
|
|
"exit_t": cx["ts"], "exit_p": cx["exit_p"],
|
2026-07-07 07:27:30 -04:00
|
|
|
|
"title": cx.get("title") or ""})
|
2026-07-07 21:44:52 -04:00
|
|
|
|
# abandoned losers/winners: conviction bets entered in-window that the
|
|
|
|
|
|
# signal held to a decided outcome and never redeemed (curPrice 0/1 in
|
|
|
|
|
|
# /positions). Not trusted rows, not in /closed-positions -> the replay
|
|
|
|
|
|
# missed them and read optimistic. Settle at the decided outcome (a loser
|
|
|
|
|
|
# pays 0 = full loss), same fold-in as the sharps table.
|
|
|
|
|
|
for asset, r in resolved_unredeemed(w["wallet"]).items():
|
|
|
|
|
|
cond = r["cond"]
|
|
|
|
|
|
if cond in best or not cond:
|
|
|
|
|
|
continue # already a trusted row / round trip
|
|
|
|
|
|
if (r["iv"] or 0) < thr or r["p"] > MAX_ENTRY:
|
|
|
|
|
|
continue
|
|
|
|
|
|
et = ent.get(cond)
|
|
|
|
|
|
if not et or et < START:
|
|
|
|
|
|
continue
|
|
|
|
|
|
best[cond] = None
|
|
|
|
|
|
out.append({"wallet": w["wallet"], "name": w["name"], "cond": cond,
|
|
|
|
|
|
"asset": asset, "cls": w.get("class", "volume"),
|
|
|
|
|
|
"their": r["iv"], "entry_t": et, "p": r["p"],
|
|
|
|
|
|
"won": r["curp"] >= 0.5, "res_t": r["res_t"]})
|
2026-07-07 01:54:58 -04:00
|
|
|
|
# chain-truth payouts for the replayed markets: refunds pay 0.5/share, and
|
|
|
|
|
|
# a cache `won` mark can be wrong on operator-resolved markets — the
|
|
|
|
|
|
# replay must settle at what a redeem actually pays (see payouts.py)
|
|
|
|
|
|
payouts.ensure({b["cond"] for b in out})
|
|
|
|
|
|
for b in out:
|
|
|
|
|
|
b["wp"] = payouts.truth(b["cond"], b.get("asset"))
|
2026-06-23 14:43:50 -06:00
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-07 03:18:41 -04:00
|
|
|
|
def closed_positions(wallet):
|
2026-07-07 07:27:30 -04:00
|
|
|
|
"""The wallet's fully-closed positions — cache.closed_exits, the
|
|
|
|
|
|
incremental cached layer (validate_timing uses the same one, so the
|
|
|
|
|
|
backtest and the sharps stats mirror exits identically)."""
|
|
|
|
|
|
return cache.closed_exits(wallet)
|
2026-07-07 03:09:21 -04:00
|
|
|
|
|
|
|
|
|
|
|
2026-07-07 21:44:52 -04:00
|
|
|
|
def _end_ts(s):
|
|
|
|
|
|
if not s:
|
|
|
|
|
|
return 0
|
|
|
|
|
|
s = s.replace("Z", "")
|
|
|
|
|
|
for fmt in ("%Y-%m-%dT%H:%M:%S", "%Y-%m-%d"):
|
|
|
|
|
|
try:
|
|
|
|
|
|
return int(time.mktime(time.strptime(s, fmt)))
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
continue
|
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def resolved_unredeemed(wallet):
|
|
|
|
|
|
"""Conviction bets the signal LOST (or won) and never redeemed — they sit in
|
|
|
|
|
|
/positions at curPrice pinned 0/1, are NOT in /closed-positions, and for
|
|
|
|
|
|
operator-resolved markets are NOT trusted rows either, so the replay never
|
|
|
|
|
|
saw them. Mostly ABANDONED LOSERS. Folding them in is the same anti-
|
|
|
|
|
|
survivorship correction the sharps table uses (_open_split) — without it the
|
|
|
|
|
|
backtest silently drops these losses and reads optimistic.
|
|
|
|
|
|
{asset: {cond, iv, p, curp, res_t}}."""
|
|
|
|
|
|
out = {}
|
|
|
|
|
|
for off in range(0, 100000, 50):
|
|
|
|
|
|
pg = sm.get_json("/positions", {"user": wallet, "limit": 50, "offset": off,
|
|
|
|
|
|
"sizeThreshold": 0})
|
|
|
|
|
|
if not pg:
|
|
|
|
|
|
break
|
|
|
|
|
|
for pos in pg:
|
|
|
|
|
|
cp = pos.get("curPrice", 0) or 0
|
|
|
|
|
|
asset = pos.get("asset")
|
|
|
|
|
|
if not asset or 0.001 < cp < 0.999: # genuinely open -> not decided
|
|
|
|
|
|
continue
|
|
|
|
|
|
out[asset] = {"cond": pos.get("conditionId"),
|
|
|
|
|
|
"iv": pos.get("initialValue") or 0,
|
|
|
|
|
|
"p": max(0.001, min(0.999, pos.get("avgPrice") or 0)),
|
|
|
|
|
|
"curp": cp,
|
|
|
|
|
|
"res_t": _end_ts(pos.get("endDate")) or int(time.time())}
|
|
|
|
|
|
if len(pg) < 50:
|
|
|
|
|
|
break
|
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-23 14:43:50 -06:00
|
|
|
|
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 []
|
2026-07-05 22:15:37 -04:00
|
|
|
|
if w.get("class") == "whale":
|
|
|
|
|
|
thr = 0.0 # whales: every open position counts
|
|
|
|
|
|
else:
|
|
|
|
|
|
thr = _WALLET_THR.get(w["wallet"])
|
|
|
|
|
|
if thr is None:
|
|
|
|
|
|
thr = cache.conv_cutoff((p.get("initialValue") or 0) for p in ps)
|
2026-06-23 14:43:50 -06:00
|
|
|
|
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
|
2026-07-02 23:35:51 -04:00
|
|
|
|
if (p.get("avgPrice", 0) or 0) > MAX_ENTRY:
|
|
|
|
|
|
continue
|
2026-07-05 22:15:37 -04:00
|
|
|
|
et = ent.get(p.get("conditionId"))
|
|
|
|
|
|
if et is not None and et < START:
|
|
|
|
|
|
continue # position predates the window
|
|
|
|
|
|
# unknown entry time -> queue at the END of the replay (an open
|
|
|
|
|
|
# position is the newest thing in the book; entry_t=0 used to put
|
|
|
|
|
|
# it FIRST, draining the bankroll before any historical bet ran)
|
2026-06-23 14:43:50 -06:00
|
|
|
|
out.append({"wallet": w["wallet"], "name": w["name"], "cond": p.get("conditionId"),
|
2026-07-05 23:41:59 -04:00
|
|
|
|
"cls": w.get("class", "volume"), "their": p.get("initialValue") or 0,
|
2026-07-05 22:15:37 -04:00
|
|
|
|
"entry_t": et if et is not None else time.time(),
|
2026-06-23 14:43:50 -06:00
|
|
|
|
"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()
|
2026-07-05 22:15:37 -04:00
|
|
|
|
resolved_pool = window_bets()
|
2026-06-23 14:43:50 -06:00
|
|
|
|
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"])
|
|
|
|
|
|
|
2026-07-06 17:18:45 -04:00
|
|
|
|
# ONLY_CONDS=<json path>: replay only these markets — {cond: bool} or
|
|
|
|
|
|
# us_listable.py's {cond: {"listed": bool, ...}}. Models "same signal, but
|
|
|
|
|
|
# I can only execute the subset" (e.g. bets also listed on Polymarket US);
|
|
|
|
|
|
# thresholds/sizing still come from the full signal, capital only chases
|
|
|
|
|
|
# the executable bets.
|
|
|
|
|
|
_only = os.environ.get("ONLY_CONDS")
|
|
|
|
|
|
if _only:
|
|
|
|
|
|
_allow = {c for c, v in json.load(open(_only)).items()
|
|
|
|
|
|
if (v.get("listed") if isinstance(v, dict) else v)}
|
|
|
|
|
|
_pre = len(stream)
|
|
|
|
|
|
stream = [b for b in stream if b["cond"] in _allow]
|
|
|
|
|
|
print(f"portfolio: ONLY_CONDS filter kept {len(stream)}/{_pre} bets", flush=True)
|
|
|
|
|
|
|
2026-07-02 10:54:07 -04:00
|
|
|
|
# prefetch every replayed market's slug (threaded; disk-cached) so the
|
|
|
|
|
|
# event-correlation cap can group markets by real-world event
|
|
|
|
|
|
with ThreadPoolExecutor(max_workers=8) as ex:
|
|
|
|
|
|
list(ex.map(market_meta, {b["cond"] for b in stream}))
|
|
|
|
|
|
|
2026-06-23 14:43:50 -06:00
|
|
|
|
cash = BANK
|
|
|
|
|
|
realized = 0.0
|
2026-07-02 09:51:35 -04:00
|
|
|
|
fees_paid = 0.0
|
2026-07-02 10:54:07 -04:00
|
|
|
|
hwm = BANK
|
|
|
|
|
|
capped = 0
|
2026-07-03 00:49:21 -04:00
|
|
|
|
reserve = 0.0
|
2026-07-02 09:51:35 -04:00
|
|
|
|
held = [] # (free_t, cost, payoff) cost = stake + entry fee; payoff paid at free_t
|
2026-06-23 14:43:50 -06:00
|
|
|
|
perW = {w["wallet"]: {"name": w["name"], "wallet": w["wallet"], "bets": 0,
|
2026-07-07 03:09:21 -04:00
|
|
|
|
"won": 0, "lost": 0, "ref": 0, "sold": 0,
|
|
|
|
|
|
"class": w.get("class", "volume"),
|
2026-06-23 14:43:50 -06:00
|
|
|
|
"invested": 0.0, "realized": 0.0} for w in WALLETS}
|
|
|
|
|
|
resolved, current, missed = [], [], []
|
|
|
|
|
|
|
2026-07-05 23:41:59 -04:00
|
|
|
|
def cur_stake(frac=BASE_PCT, their=None):
|
|
|
|
|
|
"""The wallet-class fraction of current equity, drawdown-braked, and
|
|
|
|
|
|
NEVER larger than the followed wallet's own stake: when the percentage
|
|
|
|
|
|
works out to more than they actually bet, mirror their exact amount."""
|
|
|
|
|
|
nonlocal hwm
|
2026-07-02 10:54:07 -04:00
|
|
|
|
eq = cash + sum(c for _, c, _, _ in held)
|
|
|
|
|
|
hwm = max(hwm, eq)
|
2026-07-05 22:15:37 -04:00
|
|
|
|
if eq < DD_THRESHOLD * hwm:
|
|
|
|
|
|
frac *= DD_FACTOR
|
2026-07-05 23:41:59 -04:00
|
|
|
|
stake = max(STAKE_MIN, frac * eq)
|
|
|
|
|
|
if their and stake > their:
|
|
|
|
|
|
stake = their
|
|
|
|
|
|
return stake
|
2026-07-02 10:54:07 -04:00
|
|
|
|
|
2026-06-23 14:43:50 -06:00
|
|
|
|
def free(upto):
|
|
|
|
|
|
nonlocal cash, realized
|
|
|
|
|
|
keep = []
|
2026-07-02 09:51:35 -04:00
|
|
|
|
for ft, cost, payoff, rec in held:
|
2026-06-23 14:43:50 -06:00
|
|
|
|
if ft and ft <= upto and rec["kind"] == "res":
|
2026-07-02 09:51:35 -04:00
|
|
|
|
cash += payoff; realized += payoff - cost; perW[rec["wallet"]]["realized"] += payoff - cost
|
2026-07-07 03:09:21 -04:00
|
|
|
|
if rec.get("sold"):
|
|
|
|
|
|
# mirrored exit: neither won nor lost — its truth is the price
|
|
|
|
|
|
perW[rec["wallet"]]["sold"] = perW[rec["wallet"]].get("sold", 0) + 1
|
|
|
|
|
|
rec["won"] = None
|
|
|
|
|
|
else:
|
|
|
|
|
|
wp = rec.get("wp")
|
|
|
|
|
|
won = rec["won"] if wp is None else wp > 0.5
|
|
|
|
|
|
# refunds are scratches, not losses — count them apart
|
|
|
|
|
|
perW[rec["wallet"]]["won" if won else "ref" if wp == 0.5 else "lost"] += 1
|
|
|
|
|
|
rec["won"] = won # truth-adjusted for the feed
|
|
|
|
|
|
if wp == 0.5:
|
|
|
|
|
|
rec["refund"] = True
|
2026-07-02 09:51:35 -04:00
|
|
|
|
rec["pnl"] = payoff - cost
|
2026-06-23 14:43:50 -06:00
|
|
|
|
resolved.append(rec)
|
|
|
|
|
|
else:
|
2026-07-02 09:51:35 -04:00
|
|
|
|
keep.append((ft, cost, payoff, rec))
|
2026-06-23 14:43:50 -06:00
|
|
|
|
held[:] = keep
|
|
|
|
|
|
|
|
|
|
|
|
for b in stream:
|
|
|
|
|
|
free(b["entry_t"])
|
2026-07-05 23:41:59 -04:00
|
|
|
|
stake = cur_stake(CLASS_PCT.get(b.get("cls"), BASE_PCT), b.get("their"))
|
2026-07-02 10:54:07 -04:00
|
|
|
|
b["stake"] = round(stake, 2)
|
|
|
|
|
|
b["event"] = event_key(market_meta(b["cond"])["slug"])
|
2026-07-02 11:21:08 -04:00
|
|
|
|
# correlation cap (off when EVENT_CAP=0): skip a bet when we already hold
|
|
|
|
|
|
# EVENT_CAP positions on the same real-world event (deliberate risk skip)
|
|
|
|
|
|
if EVENT_CAP and b["event"] and sum(1 for _, _, _, r in held
|
|
|
|
|
|
if r.get("event") == b["event"]) >= EVENT_CAP:
|
2026-07-02 10:54:07 -04:00
|
|
|
|
b["capped"] = True; capped += 1
|
|
|
|
|
|
missed.append(b)
|
|
|
|
|
|
continue
|
|
|
|
|
|
p_eff, fee, cost = entry_model(b["p"], stake)
|
2026-07-02 09:51:35 -04:00
|
|
|
|
if cash >= cost:
|
|
|
|
|
|
cash -= cost; fees_paid += fee; perW[b["wallet"]]["bets"] += 1
|
2026-07-02 10:54:07 -04:00
|
|
|
|
shares = stake / p_eff # lag-adjusted entry price
|
2026-06-23 14:43:50 -06:00
|
|
|
|
if b["kind"] == "res":
|
2026-07-07 03:09:21 -04:00
|
|
|
|
if b.get("exit_t"):
|
|
|
|
|
|
# the signal SOLD pre-resolution -> mirror the exit, like the
|
|
|
|
|
|
# live bot: their exit price with the slippage haircut against
|
|
|
|
|
|
# us, minus the taker fee (sells pay it; redeems don't)
|
|
|
|
|
|
xp = max(0.001, b["exit_p"] * (1 - SLIP))
|
|
|
|
|
|
fee_out = shares * FEE_RATE * xp * (1 - xp)
|
|
|
|
|
|
fees_paid += fee_out
|
|
|
|
|
|
b["sold"] = True
|
|
|
|
|
|
held.append((b["exit_t"], cost, shares * xp - fee_out, b))
|
|
|
|
|
|
else:
|
|
|
|
|
|
# held to resolution: chain-truth payout (1/0/0.5) when
|
|
|
|
|
|
# known, else the cache mark; redeem is fee-free
|
|
|
|
|
|
wp = b.get("wp")
|
|
|
|
|
|
if wp is None:
|
|
|
|
|
|
wp = 1.0 if b["won"] else 0.0
|
|
|
|
|
|
held.append((b["res_t"] or now, cost, shares * wp, b))
|
2026-06-23 14:43:50 -06:00
|
|
|
|
else: # currently open -> mark to market, no free yet
|
2026-07-02 09:51:35 -04:00
|
|
|
|
held.append((None, cost, 0.0, b))
|
2026-07-02 10:54:07 -04:00
|
|
|
|
b["val"] = shares * b["cur"]
|
2026-06-23 14:43:50 -06:00
|
|
|
|
else:
|
|
|
|
|
|
missed.append(b)
|
|
|
|
|
|
free(now)
|
|
|
|
|
|
# finalize open (still held with kind==open): mark to market
|
|
|
|
|
|
invested = 0.0
|
2026-07-02 09:51:35 -04:00
|
|
|
|
open_cost = 0.0
|
|
|
|
|
|
for ft, cost, payoff, rec in held:
|
2026-06-23 14:43:50 -06:00
|
|
|
|
if rec["kind"] == "open":
|
2026-07-02 09:51:35 -04:00
|
|
|
|
invested += rec["val"]; rec["pnl"] = rec["val"] - cost
|
|
|
|
|
|
open_cost += cost
|
2026-06-23 14:43:50 -06:00
|
|
|
|
perW[rec["wallet"]]["invested"] += rec["val"]
|
|
|
|
|
|
current.append(rec)
|
|
|
|
|
|
|
|
|
|
|
|
# enrich resolved + missed with titles, keep most-recent 60
|
2026-07-07 03:09:21 -04:00
|
|
|
|
resolved.sort(key=lambda r: r.get("exit_t") or r.get("res_t") or 0, reverse=True)
|
2026-07-08 11:35:31 -04:00
|
|
|
|
for r in resolved[:250]:
|
2026-07-07 03:09:21 -04:00
|
|
|
|
m = market_meta(r["cond"])
|
|
|
|
|
|
if m["title"]: # round-trip recs already carry a title
|
|
|
|
|
|
r["title"] = m["title"]
|
2026-07-02 09:51:35 -04:00
|
|
|
|
# hypothetical P&L had we been able to afford it — same fee + lag model as the
|
|
|
|
|
|
# placed bets: resolved bets at their outcome, still-open bets marked to the
|
|
|
|
|
|
# current price. Missed bets can be kind=="open" (no "won"/"res_t" keys) —
|
|
|
|
|
|
# indexing m["won"] here used to KeyError and kill the whole portfolio step
|
|
|
|
|
|
# the first time capital ran out while a followed wallet had a live position.
|
2026-07-01 23:15:14 -06:00
|
|
|
|
def hypo_pnl(m):
|
2026-07-02 10:54:07 -04:00
|
|
|
|
stake = m.get("stake") or STAKE_MIN
|
|
|
|
|
|
p_eff, fee, cost = entry_model(m["p"], stake)
|
2026-07-07 03:09:21 -04:00
|
|
|
|
shares = stake / p_eff
|
|
|
|
|
|
if m.get("exit_t"): # would have mirrored their exit
|
|
|
|
|
|
xp = max(0.001, m["exit_p"] * (1 - SLIP))
|
|
|
|
|
|
return shares * xp - shares * FEE_RATE * xp * (1 - xp) - cost
|
2026-07-01 23:15:14 -06:00
|
|
|
|
if "won" in m:
|
2026-07-07 01:54:58 -04:00
|
|
|
|
wp = m.get("wp")
|
|
|
|
|
|
if wp is None:
|
|
|
|
|
|
wp = 1.0 if m["won"] else 0.0
|
2026-07-07 03:09:21 -04:00
|
|
|
|
return shares * wp - cost
|
2026-07-02 10:54:07 -04:00
|
|
|
|
return stake * (m.get("cur", p_eff) / p_eff) - cost
|
2026-07-01 23:15:14 -06:00
|
|
|
|
|
2026-06-23 14:43:50 -06:00
|
|
|
|
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"]
|
2026-07-01 23:15:14 -06:00
|
|
|
|
m["pnl"] = hypo_pnl(m)
|
2026-06-23 14:43:50 -06:00
|
|
|
|
wins = sum(1 for r in resolved if r.get("won"))
|
2026-07-07 02:50:46 -04:00
|
|
|
|
refunds = sum(1 for r in resolved if r.get("refund"))
|
2026-07-07 03:09:21 -04:00
|
|
|
|
solds = sum(1 for r in resolved if r.get("sold"))
|
2026-06-23 15:16:13 -06:00
|
|
|
|
# per-wallet conviction threshold (cache p80) so the dashboard can filter LIVE open
|
|
|
|
|
|
# positions the same way; 1e12 = "no sized bets" (nothing qualifies)
|
|
|
|
|
|
conv_thr = {}
|
|
|
|
|
|
for w in WALLETS:
|
2026-07-05 22:15:37 -04:00
|
|
|
|
t = _WALLET_THR.get(w["wallet"])
|
|
|
|
|
|
if t is None:
|
|
|
|
|
|
t = cache.conv_cutoff(b["size"] for b in cache.get_bets(w["wallet"]) if (b["size"] or 0) > 0)
|
2026-06-23 15:16:13 -06:00
|
|
|
|
conv_thr[w["wallet"]] = round(t) if t != float("inf") else 1e12
|
2026-07-03 00:49:21 -04:00
|
|
|
|
equity = cash + invested + reserve
|
2026-06-23 14:43:50 -06:00
|
|
|
|
out = {
|
2026-07-05 22:15:37 -04:00
|
|
|
|
"started": START, "updated": now, "days": DAYS,
|
|
|
|
|
|
"bank": BANK, "stake": round(cur_stake(), 2), # the NEXT bet's size (base class)
|
|
|
|
|
|
"stake_pct": BASE_PCT, "class_pct": CLASS_PCT,
|
2026-07-05 23:41:59 -04:00
|
|
|
|
"event_cap": EVENT_CAP,
|
2026-07-03 00:49:21 -04:00
|
|
|
|
"hwm": round(hwm, 2),
|
2026-07-02 10:54:07 -04:00
|
|
|
|
"dd_threshold": DD_THRESHOLD, "capped_count": capped,
|
2026-07-02 23:35:51 -04:00
|
|
|
|
"max_entry": MAX_ENTRY,
|
2026-07-02 09:51:35 -04:00
|
|
|
|
"fee_rate": FEE_RATE, "slip": SLIP, "lag_est_s": LAG_EST_S,
|
|
|
|
|
|
"fees_paid": round(fees_paid, 2),
|
2026-06-23 14:43:50 -06:00
|
|
|
|
"equity": round(equity, 2), "liquid": round(cash, 2), "invested": round(invested, 2),
|
2026-07-03 00:49:21 -04:00
|
|
|
|
"reserve": round(reserve, 2), # banked profit, never bet
|
2026-06-23 14:43:50 -06:00
|
|
|
|
"realized": round(realized, 2), "pnl": round(equity - BANK, 2),
|
2026-07-02 09:51:35 -04:00
|
|
|
|
"unreal": round(invested - open_cost, 2),
|
2026-07-07 02:50:46 -04:00
|
|
|
|
"resolved_count": len(resolved), "wins": wins,
|
2026-07-07 03:09:21 -04:00
|
|
|
|
"losses": len(resolved) - wins - refunds - solds,
|
|
|
|
|
|
"refunds": refunds, "sold": solds,
|
2026-06-23 14:43:50 -06:00
|
|
|
|
"open_count": len(current), "missed_count": len(missed),
|
|
|
|
|
|
"wallets": [{"name": v["name"], "wallet": v["wallet"], "bets": v["bets"],
|
2026-07-07 02:50:46 -04:00
|
|
|
|
"won": v["won"], "lost": v["lost"], "ref": v.get("ref", 0),
|
2026-07-07 03:09:21 -04:00
|
|
|
|
"sold": v.get("sold", 0), "class": v.get("class", "volume"),
|
2026-06-23 15:16:13 -06:00
|
|
|
|
"invested": round(v["invested"], 2), "realized": round(v["realized"], 2),
|
|
|
|
|
|
"conv_thr": conv_thr.get(v["wallet"], 1e12)}
|
2026-06-23 14:43:50 -06:00
|
|
|
|
for v in perW.values()],
|
|
|
|
|
|
"current": [{"title": c.get("title", ""), "name": c["name"], "outcome": c.get("outcome", ""),
|
2026-07-02 10:54:07 -04:00
|
|
|
|
"stake": c.get("stake"), "val": round(c["val"], 2), "pnl": round(c["pnl"], 2),
|
2026-06-23 14:43:50 -06:00
|
|
|
|
"end": c.get("end")} for c in sorted(current, key=lambda c: c["entry_t"])],
|
2026-07-07 02:59:13 -04:00
|
|
|
|
# status mirrors the live bot's vocabulary: won / lost / refund (50/50
|
|
|
|
|
|
# scratch — pays $0.50/share, so "not a win" can still be P&L-positive
|
2026-07-07 03:09:21 -04:00
|
|
|
|
# below 50¢ entries) / sold (the signal exited pre-resolution and the
|
|
|
|
|
|
# replay mirrored it, exit fee + slip paid — same as the live bot).
|
2026-06-23 14:43:50 -06:00
|
|
|
|
"resolved": [{"title": r.get("title", ""), "name": r["name"], "won": r["won"],
|
2026-07-07 03:09:21 -04:00
|
|
|
|
"status": ("sold" if r.get("sold")
|
|
|
|
|
|
else "refund" if r.get("refund")
|
2026-07-07 02:59:13 -04:00
|
|
|
|
else "won" if r["won"] else "lost"),
|
2026-07-07 03:09:21 -04:00
|
|
|
|
"stake": r.get("stake"), "pnl": round(r["pnl"], 2),
|
|
|
|
|
|
"date": r.get("exit_t") or r.get("res_t")}
|
2026-07-08 11:35:31 -04:00
|
|
|
|
for r in resolved[:250]],
|
2026-07-07 02:59:13 -04:00
|
|
|
|
"missed": [{"title": m.get("title", ""), "name": m["name"],
|
2026-07-07 03:09:21 -04:00
|
|
|
|
"won": (None if "won" not in m or m.get("exit_t")
|
2026-07-07 02:59:13 -04:00
|
|
|
|
else (m["won"] if m.get("wp") is None else m["wp"] > 0.5)),
|
2026-07-07 03:09:21 -04:00
|
|
|
|
"status": ("sold" if m.get("exit_t")
|
|
|
|
|
|
else None if "won" not in m
|
2026-07-07 02:59:13 -04:00
|
|
|
|
else "refund" if m.get("wp") == 0.5
|
|
|
|
|
|
else "won" if (m["won"] if m.get("wp") is None else m["wp"] > 0.5)
|
|
|
|
|
|
else "lost"),
|
2026-07-02 10:54:07 -04:00
|
|
|
|
"stake": m.get("stake"), "capped": bool(m.get("capped")),
|
2026-07-07 03:09:21 -04:00
|
|
|
|
"pnl": round(m["pnl"], 2), "date": m.get("exit_t") or m.get("res_t")}
|
2026-06-23 14:43:50 -06:00
|
|
|
|
for m in missed[:60]],
|
2026-07-01 23:15:14 -06:00
|
|
|
|
"missed_pnl": round(sum(hypo_pnl(m) for m in missed), 2),
|
2026-06-23 14:43:50 -06:00
|
|
|
|
}
|
2026-07-02 23:35:51 -04:00
|
|
|
|
json.dump(out, open(os.path.join(HERE, OUT) if not os.path.isabs(OUT) else OUT, "w"),
|
|
|
|
|
|
separators=(",", ":"))
|
2026-07-02 10:54:07 -04:00
|
|
|
|
save_slug_cache()
|
2026-07-05 22:15:37 -04:00
|
|
|
|
print(f"portfolio[{DAYS}d rolling]: equity ${equity:,.0f} ({(equity-BANK)/BANK*100:+.0f}%) | banked ${reserve:,.0f} "
|
2026-07-03 00:49:21 -04:00
|
|
|
|
f"| realized ${realized:+,.0f} | fees ${fees_paid:,.0f} | next stake ${cur_stake():,.0f} "
|
2026-07-07 03:09:21 -04:00
|
|
|
|
f"| {len(resolved)} resolved ({wins}W/{len(resolved)-wins-refunds-solds}L/{refunds}R/{solds}S) | {len(current)} open "
|
2026-07-05 22:15:37 -04:00
|
|
|
|
f"| {len(missed)} missed ({capped} event-capped) | -> {os.path.basename(OUT)}", flush=True)
|
2026-06-23 14:43:50 -06:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
main()
|