From 54f4c511ab6bf0b24a37b58c2264a87e33d236c4 Mon Sep 17 00:00:00 2001 From: jaxperro Date: Thu, 23 Jul 2026 18:34:19 -0400 Subject: [PATCH] band-guarded mirror exits (#21 successor): mirror sells >=90c only, sub-band sells ignored+ledgered; sell-band study slice; backtest parity Sell-band evidence: every band <90c LOSES by mirroring (<30c +$79/sell given up, 50-70c +$7.29); >=90c SAVES (-$0.85/sell, n=68). Co-Authored-By: Claude Fable 5 --- config.live.example.json | 175 ++++++++++++++++++---------------- copytrade.py | 23 +++++ live/copybot.paper.json | 139 +++++++++++++-------------- live/portfolio.py | 20 +++- research/sell_mirror_study.py | 22 +++++ 5 files changed, 222 insertions(+), 157 deletions(-) diff --git a/config.live.example.json b/config.live.example.json index d6c77348..0efddf4c 100644 --- a/config.live.example.json +++ b/config.live.example.json @@ -1,89 +1,96 @@ { - "mode": "live", - "_comment": "Live-test config, SECRET-FREE and committed. The Fly live worker (wwf-copybot-live, COPYBOT_ROLE=live) reads THIS file and takes secrets from env (LIVE_PRIVATE_KEY/LIVE_FUNDER_ADDRESS + LIVE_CONFIRM to arm). Mac use: cp -> config.live.json and fill live.*. RULES: bankroll_usd MUST equal actual equity at rebase time (the 1.4 cash-anchor checks state cash vs chain). 2026-07-10 USER decision: hard caps retired \u2014 sizing is paper-parity 4%-of-equity (floored at the venue $1 min order); follow set mirrors copybot.paper.json (Set E) so paper and live books stay comparable.", - "bankroll_usd": 66.42, - "bankroll_pct": 0.04, - "price_guard_abs": 0.05, - "fak_retry_s": 10, - "fak_retry_niche_s": {"crypto": 4, "esports": 10, "sports": 25, - "geo": 25, "politics": 25, "other": 25}, - "taker_fee_rate": 0.03, - "discord_webhook": "", - "follow": { - "buy_only": true, - "entry_mode": "maker", - "exit_mode": "hold", - "min_their_usd": 25.0, - "min_entry": 0.0, - "max_entry": 0.95, - "class_pct": { - "volume": 0.04, - "whale": 0.12 - } + "mode": "live", + "_comment": "Live-test config, SECRET-FREE and committed. The Fly live worker (wwf-copybot-live, COPYBOT_ROLE=live) reads THIS file and takes secrets from env (LIVE_PRIVATE_KEY/LIVE_FUNDER_ADDRESS + LIVE_CONFIRM to arm). Mac use: cp -> config.live.json and fill live.*. RULES: bankroll_usd MUST equal actual equity at rebase time (the 1.4 cash-anchor checks state cash vs chain). 2026-07-10 USER decision: hard caps retired \u2014 sizing is paper-parity 4%-of-equity (floored at the venue $1 min order); follow set mirrors copybot.paper.json (Set E) so paper and live books stay comparable.", + "bankroll_usd": 66.42, + "bankroll_pct": 0.04, + "price_guard_abs": 0.05, + "fak_retry_s": 10, + "fak_retry_niche_s": { + "crypto": 4, + "esports": 10, + "sports": 25, + "geo": 25, + "politics": 25, + "other": 25 + }, + "taker_fee_rate": 0.03, + "discord_webhook": "", + "follow": { + "buy_only": true, + "entry_mode": "maker", + "exit_mode": "mirror", + "min_their_usd": 25.0, + "min_entry": 0.0, + "max_entry": 0.95, + "class_pct": { + "volume": 0.04, + "whale": 0.12 }, - "risk": { - "max_trade_usd": 1000000.0, - "max_position_usd": 1000000.0, - "daily_spend_cap_usd": 1000000.0, - "max_total_exposure_usd": 1000000.0, - "max_open_positions": 1000, - "max_per_event": 0, - "min_price": 0.0, - "max_price": 0.99, - "min_order_usd": 1.0 + "mirror_sell_min_p": 0.9 + }, + "risk": { + "max_trade_usd": 1000000.0, + "max_position_usd": 1000000.0, + "daily_spend_cap_usd": 1000000.0, + "max_total_exposure_usd": 1000000.0, + "max_open_positions": 1000, + "max_per_event": 0, + "min_price": 0.0, + "max_price": 0.99, + "min_order_usd": 1.0 + }, + "live": { + "private_key": "", + "funder_address": "", + "signature_type": 1, + "order_type": "FAK", + "auto_redeem": false, + "rpc_url": "" + }, + "wallets": [ + { + "wallet": "0xe8ca3f758c93f44f3ec210542ab78afb7c0bcccb", + "name": "Kruto2027", + "class": "volume", + "floor": 80.0 }, - "live": { - "private_key": "", - "funder_address": "", - "signature_type": 1, - "order_type": "FAK", - "auto_redeem": false, - "rpc_url": "" + { + "wallet": "0xbadaf319415c17f28824a43ae0cd912b9d84d874", + "name": "0xbadaf319", + "class": "volume", + "floor": 41.21 }, - "wallets": [ - { - "wallet": "0xe8ca3f758c93f44f3ec210542ab78afb7c0bcccb", - "name": "Kruto2027", - "class": "volume", - "floor": 80.0 - }, - { - "wallet": "0xbadaf319415c17f28824a43ae0cd912b9d84d874", - "name": "0xbadaf319", - "class": "volume", - "floor": 41.21 - }, - { - "wallet": "0xd7d36345c4aab150e59577e360696b01d01d698b", - "name": "gkmgkldfmg", - "class": "volume", - "floor": 470.51 - }, - { - "wallet": "0xa1d57d329227c75b12b09f927fb3d6d6ef8f1343", - "name": "1kto1m", - "class": "volume", - "floor": 328.77 - }, - { - "wallet": "0x215adbb63b47d0ca92f849fe2c2dc1adb0f6254c", - "name": "BikesAreTheBikes", - "class": "volume", - "floor": 227.2 - }, - { - "wallet": "0x921433c93558b9a4ba807ec824d02aad7ea2ddbf", - "name": "AIcAIc", - "class": "volume", - "floor": 341.23 - }, - { - "wallet": "0x40ce68f1564f3c751b12d88a393d8cc0651dbf90", - "name": "JuiceFarm", - "class": "volume" - } - ], - "feed_path": "live/copybot_live_real.json", - "fill_log": "copybot_fills.live.jsonl", - "shadow_log": "rtds_shadow.live.jsonl" + { + "wallet": "0xd7d36345c4aab150e59577e360696b01d01d698b", + "name": "gkmgkldfmg", + "class": "volume", + "floor": 470.51 + }, + { + "wallet": "0xa1d57d329227c75b12b09f927fb3d6d6ef8f1343", + "name": "1kto1m", + "class": "volume", + "floor": 328.77 + }, + { + "wallet": "0x215adbb63b47d0ca92f849fe2c2dc1adb0f6254c", + "name": "BikesAreTheBikes", + "class": "volume", + "floor": 227.2 + }, + { + "wallet": "0x921433c93558b9a4ba807ec824d02aad7ea2ddbf", + "name": "AIcAIc", + "class": "volume", + "floor": 341.23 + }, + { + "wallet": "0x40ce68f1564f3c751b12d88a393d8cc0651dbf90", + "name": "JuiceFarm", + "class": "volume" + } + ], + "feed_path": "live/copybot_live_real.json", + "fill_log": "copybot_fills.live.jsonl", + "shadow_log": "rtds_shadow.live.jsonl" } \ No newline at end of file diff --git a/copytrade.py b/copytrade.py index a57a8228..9304e284 100644 --- a/copytrade.py +++ b/copytrade.py @@ -732,6 +732,29 @@ class CopyTrader: price = self._live_price(token, "sell") if price is None: return + # band-guarded mirroring (2026-07-23, #21 successor): the sell-band + # study (research/sell_mirror_study.py) showed mirrored exits LOSE + # in every band below 90c (exits gave up value; <30c +$79/sell left + # on the table) and SAVE money at >=90c (near-resolution + # profit-taking, n=68 -$0.85/sell). Sells below the floor are + # ignored-and-ledgered exactly like hold mode, so the + # counterfactual stream keeps accruing. + min_p = (self.cfg.get("follow") or {}).get("mirror_sell_min_p", 0.0) + if price < min_p: + self.log(f"EXIT {label} โ€” BAND-SKIP (sell {price:.2f} < " + f"{min_p:.2f} floor, their frac {frac:.2f})") + if self.on_ignored_exit: + self.on_ignored_exit({ + "ts": round(time.time(), 1), "token": str(token), + "their_size": their_size, "their_prev": their_prev, + "frac": round(frac, 4), "reason": "band", + "sell_px": round(price, 4), + "shares_held": round(mine.get("shares", 0), 4), + "cost_held": round(mine.get("cost", 0), 2), + "title": (mine.get("title") or "")[:80], + "outcome": mine.get("outcome"), + "wallet": mine.get("wallet", "")}) + return if any(po.get("token") == token for po in self.state.get("pending_orders", [])): self.log(f"EXIT {label} โ€” skip (in-play hold already pending on " diff --git a/live/copybot.paper.json b/live/copybot.paper.json index 17e6e6af..a6c29d6e 100644 --- a/live/copybot.paper.json +++ b/live/copybot.paper.json @@ -1,73 +1,74 @@ { - "mode": "paper", - "bankroll_usd": 1000.0, - "bankroll_pct": 0.04, - "wallets": [ - { - "wallet": "0xe8ca3f758c93f44f3ec210542ab78afb7c0bcccb", - "name": "Kruto2027", - "class": "volume", - "floor": 80.0, - "floor_pin": 80.0 - }, - { - "wallet": "0xbadaf319415c17f28824a43ae0cd912b9d84d874", - "name": "0xbadaf319", - "class": "volume", - "floor": 30.71 - }, - { - "wallet": "0xd7d36345c4aab150e59577e360696b01d01d698b", - "name": "gkmgkldfmg", - "class": "volume", - "floor": 470.51 - }, - { - "wallet": "0xa1d57d329227c75b12b09f927fb3d6d6ef8f1343", - "name": "1kto1m", - "class": "volume", - "floor": 314.43 - }, - { - "wallet": "0x215adbb63b47d0ca92f849fe2c2dc1adb0f6254c", - "name": "BikesAreTheBikes", - "class": "volume", - "floor": 219.66 - }, - { - "wallet": "0x921433c93558b9a4ba807ec824d02aad7ea2ddbf", - "name": "AIcAIc", - "class": "volume", - "floor": 375.06 - }, - { - "wallet": "0x40ce68f1564f3c751b12d88a393d8cc0651dbf90", - "name": "JuiceFarm", - "class": "volume" - } - ], - "follow": { - "buy_only": true, - "entry_mode": "maker", - "exit_mode": "hold", - "min_their_usd": 25.0, - "min_entry": 0.0, - "max_entry": 0.95, - "class_pct": { - "volume": 0.04, - "whale": 0.12 - } + "mode": "paper", + "bankroll_usd": 1000.0, + "bankroll_pct": 0.04, + "wallets": [ + { + "wallet": "0xe8ca3f758c93f44f3ec210542ab78afb7c0bcccb", + "name": "Kruto2027", + "class": "volume", + "floor": 80.0, + "floor_pin": 80.0 }, - "risk": { - "max_trade_usd": 1000000.0, - "max_position_usd": 1000000.0, - "daily_spend_cap_usd": 1000000.0, - "max_total_exposure_usd": 1000000.0, - "max_open_positions": 1000, - "min_price": 0.0, - "max_price": 0.99, - "min_order_usd": 5.0, - "max_per_event": 0 + { + "wallet": "0xbadaf319415c17f28824a43ae0cd912b9d84d874", + "name": "0xbadaf319", + "class": "volume", + "floor": 30.71 }, - "price_guard_abs": 0.05 + { + "wallet": "0xd7d36345c4aab150e59577e360696b01d01d698b", + "name": "gkmgkldfmg", + "class": "volume", + "floor": 470.51 + }, + { + "wallet": "0xa1d57d329227c75b12b09f927fb3d6d6ef8f1343", + "name": "1kto1m", + "class": "volume", + "floor": 314.43 + }, + { + "wallet": "0x215adbb63b47d0ca92f849fe2c2dc1adb0f6254c", + "name": "BikesAreTheBikes", + "class": "volume", + "floor": 219.66 + }, + { + "wallet": "0x921433c93558b9a4ba807ec824d02aad7ea2ddbf", + "name": "AIcAIc", + "class": "volume", + "floor": 375.06 + }, + { + "wallet": "0x40ce68f1564f3c751b12d88a393d8cc0651dbf90", + "name": "JuiceFarm", + "class": "volume" + } + ], + "follow": { + "buy_only": true, + "entry_mode": "maker", + "exit_mode": "mirror", + "min_their_usd": 25.0, + "min_entry": 0.0, + "max_entry": 0.95, + "class_pct": { + "volume": 0.04, + "whale": 0.12 + }, + "mirror_sell_min_p": 0.9 + }, + "risk": { + "max_trade_usd": 1000000.0, + "max_position_usd": 1000000.0, + "daily_spend_cap_usd": 1000000.0, + "max_total_exposure_usd": 1000000.0, + "max_open_positions": 1000, + "min_price": 0.0, + "max_price": 0.99, + "min_order_usd": 5.0, + "max_per_event": 0 + }, + "price_guard_abs": 0.05 } \ No newline at end of file diff --git a/live/portfolio.py b/live/portfolio.py index e2b9ea45..1069760d 100644 --- a/live/portfolio.py +++ b/live/portfolio.py @@ -148,6 +148,17 @@ 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")) +# band-guarded mirroring (#21 successor): sells below this price are NOT +# mirrored โ€” held to truth like hold mode (sell-band study: every band +# <90c lost by mirroring; >=90c saved). 0.0 = mirror everything. +MIRROR_SELL_MIN_P = float(os.environ.get( + "BT_MIRROR_SELL_MIN_P", _FOLLOW.get("mirror_sell_min_p", 0.0))) + + +def _mirrors(exit_p): + """Would the bot mirror a sell at this price under the current flags?""" + return (EXIT_MODE != "hold" + and (exit_p or 0) >= MIRROR_SELL_MIN_P) def entry_model(p, stake): @@ -525,7 +536,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") and EXIT_MODE != "hold": + if b.get("exit_t") and _mirrors(b.get("exit_p")): # 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) @@ -581,7 +592,7 @@ 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") and EXIT_MODE != "hold": # would have mirrored + if m.get("exit_t") and _mirrors(m.get("exit_p")): # would have mirrored xp = max(0.001, m["exit_p"] * (1 - SLIP)) return shares * xp - shares * FEE_RATE * xp * (1 - xp) - cost if m.get("wp") is not None or m.get("won") is not None: @@ -600,12 +611,12 @@ def main(): return m.get("won") def _missed_won(m): - if m.get("exit_t") and EXIT_MODE != "hold": + if m.get("exit_t") and _mirrors(m.get("exit_p")): 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": + if m.get("exit_t") and _mirrors(m.get("exit_p")): return "sold" w = _truth_won(m) if w is None: @@ -683,6 +694,7 @@ def main(): 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, + "mirror_sell_min_p": MIRROR_SELL_MIN_P, } json.dump(out, open(os.path.join(HERE, OUT) if not os.path.isabs(OUT) else OUT, "w"), separators=(",", ":")) diff --git a/research/sell_mirror_study.py b/research/sell_mirror_study.py index 5efc8fe9..30363f0e 100644 --- a/research/sell_mirror_study.py +++ b/research/sell_mirror_study.py @@ -37,6 +37,28 @@ def main(): db = tape.connect() tape.build_resolved(db) pays = fwd.payouts_for(db, [str(s["token"]) for s in sells]) + # sell-price band slice: WHERE is mirroring unprofitable? delta>0 = the + # exit gave up value (holding won); delta<0 = the exit saved us. + BANDS = [(0, .30, "<30c"), (.30, .50, "30-50c"), (.50, .70, "50-70c"), + (.70, .90, "70-90c"), (.90, 1.01, ">=90c")] + print("\n== SELL-PRICE BANDS (both books pooled, chain-graded) ==") + for lo, hi, tag in BANDS: + bn = bd = 0 + for s2 in sells: + p2 = pays.get(str(s2["token"])) + if p2 is None or p2 == 0.5: + continue + sp = s2.get("price") or 0 + if not (lo <= sp < hi): + continue + sh2 = s2.get("shares") or 0 + bn += 1 + bd += (p2 - sp) * sh2 + if bn: + lbl = ("SKIP-MIRROR (exits gave up value)" if bd > 0 + else "mirror OK (exits saved money)") + print(f" {tag:>7}: n={bn:>3} ยท net delta {bd:+8.2f} " + f"(${bd/bn:+.2f}/sell) -> {lbl}") for book in ("live", "paper"): ss = [s for s in sells if s["_book"] == book] n = unk = refund = right = wrong = 0