Files
winning-wallet-finder/tests/test_fak_retry.py
T
jaxperro 01687f4a44 copybot: one-shot 10s re-quote retry for FAK no-match OPENs
'no orders found to match with FAK order' is the #1 miss class (13 of the
last 48h's misses) — the copy lands in the crater the sharp just swept,
before makers requote. Instead of recording the miss at the first
rejection, hand the OPEN to Copybot.fak_requote_retry: sleep fak_retry_s
(default 10, 0 disables) OUTSIDE the bot lock, then re-run the whole gated
buy path (fresh quote, price guard, depth gate) exactly once, mirroring
the webhook call site's fill drain so a retry fill books lag + bet rows.
A second rejection records the miss tagged 'twice (re-quote retry)'.
Paper-parity: the hook is installed in both modes, and PaperExecutor's
FAK model re-decides on the fresh book. tests/test_fak_retry.py covers
hook scheduling, no-double-count sizing (their_size=0 re-entry), ADD and
non-FAK bypasses, and the end-to-end retry thread.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 14:52:16 -04:00

179 lines
6.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Tests for the FAK no-match re-quote retry (2026-07-20): a first-attempt
OPEN whose FAK dies unmatched is handed to Copybot.fak_requote_retry instead
of recording a miss; the retry re-enters _handle_their_buy(retry=True) with
their_size=0 once, and only a SECOND rejection records the miss (tagged).
Run: python3 tests/test_fak_retry.py — needs no network, no config.
"""
import os
import sys
import tempfile
import time
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
import copybot # noqa: E402
import copytrade # noqa: E402
_TMP = tempfile.mkdtemp(prefix="copybot_test_")
FAK_RESP = "exception: no orders found to match with FAK order. FAK orders..."
WALLET = "0xabc"
TOK = "T1"
fails = []
def case(name):
def deco(fn):
cp0, bd0 = copytrade.clob_price, copybot.book_depth
try:
fn()
print(f"PASS {name}")
except AssertionError as e:
print(f"FAIL {name}: {e}")
fails.append(name)
finally:
copytrade.clob_price, copybot.book_depth = cp0, bd0
return deco
class ScriptedEx:
"""Returns the scripted buy responses in order; repeats the last one."""
live = False
def __init__(self, *resps):
self.resps = list(resps)
self.fills = []
self.calls = 0
def buy(self, token_id, shares, price, meta):
self.calls += 1
r = self.resps.pop(0) if len(self.resps) > 1 else self.resps[0]
if r == "fak":
return {"ok": False, "filled_shares": 0.0, "price": price,
"resp": FAK_RESP, "paper": True}
if r == "other":
return {"ok": False, "filled_shares": 0.0, "price": price,
"resp": "some other rejection", "paper": True}
self.fills.append({"side": "BUY", "token": token_id,
"shares": shares, "price": price})
return {"ok": True, "filled_shares": shares, "price": price,
"paper": True}
def mkengine(*resps):
cfg = {"bankroll_usd": 100.0, "bankroll_pct": 0.04,
"watch": [{"wallet": WALLET, "name": "TestSharp"}],
"feed_path": os.path.join(_TMP, "feed.json"),
"fill_log": os.path.join(_TMP, "fills.jsonl"),
"risk": {"min_price": 0.02, "max_price": 0.99,
"max_open_positions": 50, "min_order_usd": 1.0,
"max_trade_usd": 100.0, "daily_spend_cap_usd": 1000.0,
"max_total_exposure_usd": 1000.0}}
state = copytrade.new_state()
state["cash"] = 100.0
eng = copytrade.CopyTrader(cfg, state, ScriptedEx(*resps),
os.path.join(_TMP, "state.json"))
copytrade.clob_price = lambda t, s: 0.50
return eng
def their_buy(eng, retry=False, their_size=100.0):
eng.state["their_pos"].setdefault(WALLET, {})
if retry: # host passes 0 — their_pos already counts it
eng.state["their_pos"][WALLET][TOK] = their_size
their_size = 0.0
eng._handle_their_buy(WALLET, TOK, their_size, 0.50, "Yes · Test market",
"Test market", "Yes", event="ev-1", cond="0xc0nd",
their_ts=123, retry=retry)
@case("first FAK reject on an OPEN -> hook fires with the copy ctx, NO miss")
def t1():
eng = mkengine("fak")
calls = []
eng.on_fak_reject = calls.append
their_buy(eng)
assert len(calls) == 1, f"hook calls: {len(calls)}"
ctx = calls[0]
assert ctx["token"] == TOK and ctx["wallet"] == WALLET
assert ctx["their_price"] == 0.50 and ctx["cond"] == "0xc0nd"
assert not eng.state.get("missed"), eng.state.get("missed")
assert TOK not in eng.state["my_pos"]
@case("retry=True rejection -> miss recorded 'twice (re-quote retry)', no re-schedule")
def t2():
eng = mkengine("fak")
calls = []
eng.on_fak_reject = calls.append
their_buy(eng, retry=True)
assert not calls, "retry must never re-schedule"
m = eng.state["missed"]
assert len(m) == 1 and "twice (re-quote retry)" in m[0]["reason"], m
@case("no hook installed -> miss recorded at once (pre-change behavior)")
def t3():
eng = mkengine("fak")
their_buy(eng)
m = eng.state["missed"]
assert len(m) == 1 and m[0]["reason"].startswith("order rejected: "), m
@case("non-FAK rejection -> no hook, miss recorded at once")
def t4():
eng = mkengine("other")
calls = []
eng.on_fak_reject = calls.append
their_buy(eng)
assert not calls
assert len(eng.state["missed"]) == 1
@case("FAK reject on an ADD -> no hook, no miss (adds were never miss-tracked)")
def t5():
eng = mkengine("fak")
calls = []
eng.on_fak_reject = calls.append
eng.state["my_pos"][TOK] = {"shares": 2.0, "cost": 1.0,
"title": "Test market", "outcome": "Yes"}
eng.state["their_pos"][WALLET] = {TOK: 100.0}
eng._handle_their_buy(WALLET, TOK, 100.0, 0.50, "Yes · Test market",
"Test market", "Yes")
assert not calls and not eng.state.get("missed")
@case("retry fill sizes off their FULL stake (their_size=0, no double count)")
def t6():
eng = mkengine("ok")
their_buy(eng, retry=True, their_size=5.0) # their whole bet: 5 shares
pos = eng.state["my_pos"].get(TOK)
assert pos, "retry fill should open the position"
# stake = min(4% × $100 equity, their 5) = $4, not capped by a doubled 10
assert abs(pos["cost"] - 4.0) < 1e-6, pos
@case("end-to-end: Copybot retry thread re-buys, books bet + lag, no miss")
def t7():
eng = mkengine("fak", "ok") # first buy dies, retry fills
bot = copybot.Copybot(eng.cfg, eng, filt=None)
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_s = 0.05
eng.on_fak_reject = bot.fak_requote_retry
their_buy(eng) # reject -> schedules the retry
assert TOK not in eng.state["my_pos"]
time.sleep(1.0) # let the daemon thread run
pos = eng.state["my_pos"].get(TOK)
assert pos and pos["shares"] > 0, "retry did not fill"
assert eng.ex.calls == 2, f"buy attempts: {eng.ex.calls}"
b = eng.state["bets"].get(TOK)
assert b and b["status"] == "open" and b["their_price"] == 0.50, b
assert not eng.ex.fills, "fill left undrained"
assert not eng.state.get("missed"), eng.state["missed"]
print()
print("FAILURES:", fails or "none")
sys.exit(1 if fails else 0)