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 <noreply@anthropic.com>
This commit is contained in:
jaxperro
2026-07-20 17:45:31 -04:00
parent 83d1d8397a
commit 377192c4bf
4 changed files with 75 additions and 7 deletions
+4 -2
View File
@@ -41,8 +41,10 @@ push through.
07-19 reconcile. **wwf-copybot** (paper $1k): same set, FAK-parity fills. 07-19 reconcile. **wwf-copybot** (paper $1k): same set, FAK-parity fills.
Both on the audit-hardened build (locks, chain-gated sweep, boot-id 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: 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`, FAK no-match OPENs get one re-quote retry on both bots, PER-NICHE waits
paper-parity; misses from a second rejection tag "twice (re-quote retry)"). 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 - **wwf-recorder**: the FULL firehose (trades + order matches + comments
+ crypto ticks, ~8M events/day, dual-socket ~99.9% capture, 25GB volume, + crypto ticks, ~8M events/day, dual-socket ~99.9% capture, 25GB volume,
NOTHING deleted until the Mac verifiably ingests it) → nightly → NOTHING deleted until the Mac verifiably ingests it) → nightly →
+2
View File
@@ -5,6 +5,8 @@
"bankroll_pct": 0.04, "bankroll_pct": 0.04,
"price_guard_abs": 0.05, "price_guard_abs": 0.05,
"fak_retry_s": 10, "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, "taker_fee_rate": 0.03,
"discord_webhook": "", "discord_webhook": "",
"follow": { "follow": {
+51 -5
View File
@@ -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. # with "taker_fee_rate" in config.json if that changes.
TAKER_FEE_RATE = 0.03 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): def taker_fee(shares, price, rate):
return shares * rate * price * (1.0 - price) return shares * rate * price * (1.0 - price)
@@ -894,8 +928,16 @@ class Copybot:
engine.state["lag_recent"] = self._backfill_lag_recent() engine.state["lag_recent"] = self._backfill_lag_recent()
self.fee_rate = float(cfg.get("taker_fee_rate", TAKER_FEE_RATE)) 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 # 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_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): def _backfill_lag_recent(self, window_s=86400):
"""[[ts, lag_s, slip], …] for fills in the last window_s, read from the """[[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 rows instead of surfacing later as an untracked orphan; a second
rejection records the miss inside _handle_their_buy, tagged rejection records the miss inside _handle_their_buy, tagged
'twice (re-quote retry)'. Installed on engine.on_fak_reject by '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(): def run():
time.sleep(self.fak_retry_s) time.sleep(wait)
tok = ctx["token"] tok = ctx["token"]
try: try:
with self.lock: with self.lock:
@@ -1188,8 +1234,8 @@ class Copybot:
self.check_book() self.check_book()
except Exception as e: except Exception as e:
log(f"re-quote retry error ({ctx['label'][:36]}): {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: " log(f"FAK no-match — re-quote retry in {wait:.0f}s "
f"{ctx['label'][:48]}") f"({market_niche(ctx['title'])}): {ctx['label'][:48]}")
threading.Thread(target=run, daemon=True).start() threading.Thread(target=run, daemon=True).start()
def _ledger_buy_tokens(self): def _ledger_buy_tokens(self):
+18
View File
@@ -159,6 +159,7 @@ def t7():
bot.here = "" bot.here = ""
bot.check_book = lambda: None # network-touching, not under test bot.check_book = lambda: None # network-touching, not under test
copybot.book_depth = lambda t: None # _record_lag's book snapshot 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 bot.fak_retry_s = 0.05
eng.on_fak_reject = bot.fak_requote_retry eng.on_fak_reject = bot.fak_requote_retry
their_buy(eng) # reject -> schedules the retry their_buy(eng) # reject -> schedules the retry
@@ -173,6 +174,23 @@ def t7():
assert not eng.state.get("missed"), eng.state["missed"] 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()
print("FAILURES:", fails or "none") print("FAILURES:", fails or "none")
sys.exit(1 if fails else 0) sys.exit(1 if fails else 0)