From 377192c4bf61e5a44105fd539ea5bc3d0a33aaf1 Mon Sep 17 00:00:00 2001 From: jaxperro Date: Mon, 20 Jul 2026 17:45:31 -0400 Subject: [PATCH] copybot: per-niche FAK re-quote waits from measured crater-refill times The flat 10s retry was calibrated to nothing; the tape now says craters refill at very different speeds (research requote_timing, 775k crater prints): crypto 94% within 4s, esports 83% within 10s, sports only 70% at 10s but 76% by 25s, geo/politics tails run minutes (price guard is the protection there, not the retry). Waits move to FAK_RETRY_NICHE_DEFAULT {crypto 4, esports 10, sports/geo/politics/other 25}, classified by the validated research niche patterns (esports before sports so 'LoL: A vs B' doesn't fall through on ' vs '). cfg fak_retry_niche_s overrides per key; fak_retry_s stays the fallback (tennis: no measurement yet) and 0 still disables the feature. Paper-parity: same map both bots. test t8 covers the map, first-match classing, fallback, and config override. Co-Authored-By: Claude Fable 5 --- HANDOFF.md | 6 +++-- config.live.example.json | 2 ++ copybot.py | 56 ++++++++++++++++++++++++++++++++++++---- tests/test_fak_retry.py | 18 +++++++++++++ 4 files changed, 75 insertions(+), 7 deletions(-) diff --git a/HANDOFF.md b/HANDOFF.md index 37cb8797..74a4fcea 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -41,8 +41,10 @@ push through. 07-19 reconcile. **wwf-copybot** (paper $1k): same set, FAK-parity fills. Both on the audit-hardened build (locks, chain-gated sweep, boot-id single-writer guard, TLS'd user-ws — HANDOFF_ARCHIVE rev 16). 2026-07-20: - FAK no-match OPENs get one 10s re-quote retry on both bots (`fak_retry_s`, - paper-parity; misses from a second rejection tag "twice (re-quote retry)"). + FAK no-match OPENs get one re-quote retry on both bots, PER-NICHE waits + from measured crater-refill times (crypto 4s / esports 10s / sports+slow + 25s — research/params/requote_timing.json; `fak_retry_niche_s` override, + `fak_retry_s` fallback+kill-switch; second rejection tags "twice"). - **wwf-recorder**: the FULL firehose (trades + order matches + comments + crypto ticks, ~8M events/day, dual-socket ~99.9% capture, 25GB volume, NOTHING deleted until the Mac verifiably ingests it) → nightly → diff --git a/config.live.example.json b/config.live.example.json index b59104d4..150df0e6 100644 --- a/config.live.example.json +++ b/config.live.example.json @@ -5,6 +5,8 @@ "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": { diff --git a/copybot.py b/copybot.py index f4259905..31e8597c 100644 --- a/copybot.py +++ b/copybot.py @@ -243,6 +243,40 @@ FEED_PUSH_MIN_S = 120 # min seconds between feed git-pushes # with "taker_fee_rate" in config.json if that changes. TAKER_FEE_RATE = 0.03 +# FAK re-quote retry, per market niche. Waits are the measured crater-refill +# times (research/params/requote_timing.json, 775k crater prints, 2026-07-20): +# crypto books refill <4s 94% of the time, esports 83% by 10s, sports needs +# ~25s (70% at 10s -> 76% at 25s); geo/politics/other tails run minutes, so +# 25s is the cap — beyond that the price guard is the protection, not the +# retry. Patterns are the validated research/tape.py classifier, first match +# wins (esports before sports: "LoL: A vs B" must not fall through to +# sports on " vs "). Config: fak_retry_niche_s overrides per key; +# fak_retry_s stays the fallback for unmatched niches and 0 disables the +# whole feature. +FAK_RETRY_NICHE_DEFAULT = {"crypto": 4.0, "esports": 10.0, "sports": 25.0, + "geo": 25.0, "politics": 25.0, "other": 25.0} +NICHE_PATTERNS = [ + ("esports", ["lol:", "dota", "cs2", "csgo", "valorant", "esports", + "bilibili", "map ", "game 1", "game 2", "game 3"]), + ("tennis", ["tennis", "atp", "wta", "wimbledon", "set winner"]), + ("sports", [" vs. ", " vs ", " @ ", "mlb", "nba", "nhl", "ufc", + "world cup", "f1", "grand prix", "fifa"]), + ("crypto", ["bitcoin", "btc", "ethereum", "solana", "xrp", "doge", + "price of", "up or down"]), + ("politics", ["election", "president", "senate", "governor", "mayor", + "nominee", "impeach", "tariff", "fed ", "rate cut"]), + ("geo", ["iran", "israel", "russia", "ukraine", "china", "taiwan", + "ceasefire", "strike", "nato"]), +] + + +def market_niche(title): + t = (title or "").lower() + for label, pats in NICHE_PATTERNS: + if any(p in t for p in pats): + return label + return "other" + def taker_fee(shares, price, rate): return shares * rate * price * (1.0 - price) @@ -894,8 +928,16 @@ class Copybot: engine.state["lag_recent"] = self._backfill_lag_recent() self.fee_rate = float(cfg.get("taker_fee_rate", TAKER_FEE_RATE)) # FAK no-match re-quote retry wait (s); 0 disables (miss recorded at - # the first rejection, pre-2026-07-20 behavior) + # the first rejection, pre-2026-07-20 behavior). Per-niche waits from + # the measured crater-refill times (see FAK_RETRY_NICHE_DEFAULT); + # fak_retry_s is the fallback for niches without a measurement. self.fak_retry_s = float(cfg.get("fak_retry_s", 10)) + self.fak_retry_niche = { + k: float(v) for k, v in {**FAK_RETRY_NICHE_DEFAULT, + **(cfg.get("fak_retry_niche_s") or {})}.items()} + + def _fak_wait(self, title): + return self.fak_retry_niche.get(market_niche(title), self.fak_retry_s) def _backfill_lag_recent(self, window_s=86400): """[[ts, lag_s, slip], …] for fills in the last window_s, read from the @@ -1162,9 +1204,13 @@ class Copybot: rows instead of surfacing later as an untracked orphan; a second rejection records the miss inside _handle_their_buy, tagged 'twice (re-quote retry)'. Installed on engine.on_fak_reject by - main() when cfg fak_retry_s > 0 (default 10).""" + main() when cfg fak_retry_s > 0. The wait is PER NICHE (_fak_wait): + measured crater-refill times, crypto 4s / esports 10s / sports+slow + books 25s (research requote_timing, 2026-07-20).""" + wait = self._fak_wait(ctx["title"]) + def run(): - time.sleep(self.fak_retry_s) + time.sleep(wait) tok = ctx["token"] try: with self.lock: @@ -1188,8 +1234,8 @@ class Copybot: self.check_book() except Exception as e: log(f"re-quote retry error ({ctx['label'][:36]}): {e}") - log(f"FAK no-match — re-quote retry in {self.fak_retry_s:.0f}s: " - f"{ctx['label'][:48]}") + log(f"FAK no-match — re-quote retry in {wait:.0f}s " + f"({market_niche(ctx['title'])}): {ctx['label'][:48]}") threading.Thread(target=run, daemon=True).start() def _ledger_buy_tokens(self): diff --git a/tests/test_fak_retry.py b/tests/test_fak_retry.py index 5a4ad351..71e8f256 100644 --- a/tests/test_fak_retry.py +++ b/tests/test_fak_retry.py @@ -159,6 +159,7 @@ def t7(): bot.here = "" bot.check_book = lambda: None # network-touching, not under test copybot.book_depth = lambda t: None # _record_lag's book snapshot + bot.fak_retry_niche = {} # force the scalar fallback bot.fak_retry_s = 0.05 eng.on_fak_reject = bot.fak_requote_retry their_buy(eng) # reject -> schedules the retry @@ -173,6 +174,23 @@ def t7(): assert not eng.state.get("missed"), eng.state["missed"] + +@case("per-niche retry waits: measured map, first-match classing, fallback") +def t8(): + eng = mkengine("fak") + bot = copybot.Copybot(eng.cfg, eng, filt=None) + assert bot._fak_wait("Ethereum above 1,900 on July 20, 12PM ET?") == 4.0 + assert bot._fak_wait("LoL: G2 Esports vs LYON - Game 2 Winner") == 10.0 + assert bot._fak_wait("Will Argentina win the 2026 FIFA World Cup?") == 25.0 + assert bot._fak_wait("Israel x Iran ceasefire continues through July 20?") == 25.0 + assert bot._fak_wait("Completely unclassifiable market") == 25.0 # other + assert bot._fak_wait("Wimbledon: Alcaraz set winner") == bot.fak_retry_s # tennis: no measurement -> fallback + eng.cfg["fak_retry_niche_s"] = {"crypto": 2} + bot2 = copybot.Copybot(eng.cfg, eng, filt=None) + assert bot2._fak_wait("Bitcoin above 63,400 on July 17, 3PM ET?") == 2.0 + + + print() print("FAILURES:", fails or "none") sys.exit(1 if fails else 0)