From 004911bba3018b2162c1dcfe0f10837b96c89f2d Mon Sep 17 00:00:00 2001 From: jaxperro Date: Thu, 23 Jul 2026 18:09:11 -0400 Subject: [PATCH] backtest execution follows the paper bot's follow flags (#20/#21 parity): maker entries at the sharp's price, hold-through exits; sold-unresolved carry as open at exit print Co-Authored-By: Claude Fable 5 --- live/portfolio.py | 75 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 62 insertions(+), 13 deletions(-) diff --git a/live/portfolio.py b/live/portfolio.py index 4e25c88b..e2b9ea45 100644 --- a/live/portfolio.py +++ b/live/portfolio.py @@ -132,9 +132,30 @@ except Exception: _PINNED = {} +# ---- execution mode: mirrors the PAPER BOT's follow flags (2026-07-23) ------ +# The backtest replays the same execution the paper test runs (#20/#21): +# entry_mode "maker": fill at the sharp's own price, no fee, no slip (the +# deployed bot rests at their price, 60s TTL; T3/T11 measured 90% touch +# with the never-filled tail being winners — so 100%-fill-at-their-price +# is the DECLARED optimistic bound; wallet-selection bench, not an EV +# certification — verdicts stay with the bots' own #20/#21 windows) +# exit_mode "hold": mirrored sells ignored; every position rides to chain +# truth (sold-but-unresolved positions carry as open, marked at the +# signal's exit print). Flip copybot.paper.json back -> backtest follows. +try: + _FOLLOW = json.load(open(os.path.join(HERE, "copybot.paper.json")))["follow"] +except Exception: + _FOLLOW = {} +ENTRY_MODE = os.environ.get("BT_ENTRY_MODE", _FOLLOW.get("entry_mode", "taker")) +EXIT_MODE = os.environ.get("BT_EXIT_MODE", _FOLLOW.get("exit_mode", "mirror")) + + def entry_model(p, stake): - """(effective entry price, entry fee, total cash cost) of a $stake copy: - price worsened by the lag-slippage haircut, taker fee on top of the stake.""" + """(effective entry price, entry fee, total cash cost) of a $stake copy. + taker: price worsened by the lag-slippage haircut, taker fee on top. + maker: the sharp's own price, feeless (see ENTRY_MODE note above).""" + if ENTRY_MODE == "maker": + return p, 0.0, stake p_eff = min(0.999, p * (1 + SLIP)) fee = stake * FEE_RATE * (1 - p_eff) return p_eff, fee, stake + fee @@ -504,7 +525,7 @@ def main(): cash -= cost; fees_paid += fee; perW[b["wallet"]]["bets"] += 1 shares = stake / p_eff # lag-adjusted entry price if b["kind"] == "res": - if b.get("exit_t"): + if b.get("exit_t") and EXIT_MODE != "hold": # 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) @@ -513,6 +534,15 @@ def main(): fees_paid += fee_out b["sold"] = True held.append((b["exit_t"], cost, shares * xp - fee_out, b)) + elif b.get("wp") is None and b.get("won") is None: + # hold mode, signal sold, market NOT resolved: no truth to + # grade against yet -> carry as an open position marked at + # the signal's exit print (best known price; conservative: + # capital stays locked until real resolution) + b["kind"] = "open" + b["cur"] = b.get("exit_p") or b["p"] + held.append((None, cost, 0.0, b)) + b["val"] = shares * b["cur"] else: # held to resolution: chain-truth payout (1/0/0.5) when # known, else the cache mark; redeem is fee-free @@ -551,15 +581,38 @@ def main(): stake = m.get("stake") or STAKE_MIN p_eff, fee, cost = entry_model(m["p"], stake) shares = stake / p_eff - if m.get("exit_t"): # would have mirrored their exit + if m.get("exit_t") and EXIT_MODE != "hold": # would have mirrored xp = max(0.001, m["exit_p"] * (1 - SLIP)) return shares * xp - shares * FEE_RATE * xp * (1 - xp) - cost - if "won" in m: + if m.get("wp") is not None or m.get("won") is not None: wp = m.get("wp") if wp is None: wp = 1.0 if m["won"] else 0.0 return shares * wp - cost - return stake * (m.get("cur", p_eff) / p_eff) - cost + # no truth yet (open, or hold-mode sold-unresolved): mark to price + mark = m.get("cur") or m.get("exit_p") or p_eff + return shares * mark - cost + + def _truth_won(m): + """Chain/cache-truth won for a missed rec, None when undecided.""" + if m.get("wp") is not None: + return m["wp"] > 0.5 + return m.get("won") + + def _missed_won(m): + if m.get("exit_t") and EXIT_MODE != "hold": + return None # mirrored exit: truth is the price + return _truth_won(m) + + def _missed_status(m): + if m.get("exit_t") and EXIT_MODE != "hold": + return "sold" + w = _truth_won(m) + if w is None: + return None + if m.get("wp") == 0.5: + return "refund" + return "won" if w else "lost" missed.sort(key=lambda m: m.get("res_t") or 0, reverse=True) for m in missed[:60]: @@ -623,17 +676,13 @@ def main(): "date": r.get("exit_t") or r.get("res_t")} for r in resolved[:250]], "missed": [{"title": m.get("title", ""), "name": m["name"], - "won": (None if "won" not in m or m.get("exit_t") - else (m["won"] if m.get("wp") is None else m["wp"] > 0.5)), - "status": ("sold" if m.get("exit_t") - else None if "won" not in m - 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"), + "won": _missed_won(m), + "status": _missed_status(m), "stake": m.get("stake"), "capped": bool(m.get("capped")), "pnl": round(m["pnl"], 2), "date": m.get("exit_t") or m.get("res_t")} for m in missed[:60]], "missed_pnl": round(sum(hypo_pnl(m) for m in missed), 2), + "entry_mode": ENTRY_MODE, "exit_mode": EXIT_MODE, } json.dump(out, open(os.path.join(HERE, OUT) if not os.path.isabs(OUT) else OUT, "w"), separators=(",", ":"))