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:
+4
-2
@@ -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 →
|
||||||
|
|||||||
@@ -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
@@ -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):
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user