paper FAK parity: no ask inside the protected band -> honest miss, not a pretend fill

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jaxperro
2026-07-15 23:48:56 -04:00
parent d833168c05
commit 646139dfa3
4 changed files with 158 additions and 5 deletions
+22 -1
View File
@@ -1,4 +1,4 @@
# Session handoff — 2026-07-14 (rev 12: chain-seed — fills decoded from the tx receipt)
# Session handoff — 2026-07-15 (rev 13: paper FAK parity; rev 12: chain-seed)
## Operating boundary set by the user (2026-07-13, "away for a while")
**Full autonomy on BOTH bots**; real-money bot **stays ARMED**. Never touch
@@ -7,6 +7,27 @@ the private key, never raise/loosen caps, never rotate the Discord webhook
DISARM the live bot (`flyctl secrets unset LIVE_CONFIRM`) rather than push
through it.
## Shipped since rev 12 (2026-07-15)
- **PAPER FAK PARITY**: PaperExecutor BUYs now model live FAK reality — if
the depth gate's book snapshot (threaded through meta["book"], no second
fetch) has no ask ≤ quote×1.05 (live.max_slippage_pct default), the paper
"fill" is a rejection and the engine records the same "order rejected: no
orders found to match" miss live logs. Rationale: post-chain-seed this is
live's #1 miss class (10/33) and paper pretended to fill them (it "filled"
the Claude Opus bet live's FAK couldn't) — biasing the live-vs-paper
per-signal ratio (the top-up number) OPTIMISTIC. Chain-seed verdict after
41h: detection misses 23→3 live / 35→2 paper; the residual tail is
no-trigger-at-all. Resting-limit fix for live FAK no-match evaluated and
REJECTED for now: recent 10-row sample is $0.97 would-be net + a resting
bid is adversely selected (fills when price falls through it, not when
we're right). Revisit only with paper-parity data showing real cost.
Fail-open on dead book fetch; SELLs stay optimistic (exits need the
retry/pending machinery to model honestly — out of scope). Live book
unaffected (meta ignored). Tests: tests/test_paper_fak.py (8 paths);
LedgerPaperExecutor no longer appends a fill row for a rejection.
Deploy: paper app restart only. Expect paper misses like "order rejected:
no orders found … (paper model: best ask 0.900 > cap 0.840)".
## Shipped since rev 11 (2026-07-14)
- **CHAIN SEED (T0b)**: the missed-bets review found the one remaining
detection leak — RTDS doesn't emit every market (the "Credible public
+3 -2
View File
@@ -336,8 +336,9 @@ class LedgerPaperExecutor(PaperExecutor):
def buy(self, token_id, shares, price, meta):
r = super().buy(token_id, shares, price, meta)
self.fills.append({"side": "BUY", "token": token_id,
"shares": r["filled_shares"], "price": r["price"]})
if r["ok"]: # a paper FAK no-match is not a fill
self.fills.append({"side": "BUY", "token": token_id,
"shares": r["filled_shares"], "price": r["price"]})
return r
def sell(self, token_id, shares, price, meta):
+29 -2
View File
@@ -223,10 +223,36 @@ def event_key(t):
# ── execution ────────────────────────────────────────────────────────────────
class PaperExecutor:
"""Simulates fills at the current best price. Places nothing."""
"""Simulates fills at the current best price. Places nothing.
BUYs model live FAK reality (2026-07-15): live sends fill-and-kill with a
protected cap of quote × (1 + max_slippage_pct); on a thin book with no
ask inside that band the order dies ('no orders found to match') the #1
live miss class once detection got fast (the copy arrives in the crater
the sharp just swept, before makers requote). Paper used to 'fill' those,
which biased the live-vs-paper per-signal ratio the bankroll top-up
number optimistic. Now the same book snapshot the depth gate fetched
(meta['book'], else fetched here) decides: no ask cap -> rejected, and
the engine records the same 'order rejected' miss live would. A failed
book fetch fails OPEN (fills, today's behavior) — a dead data source
shouldn't fabricate misses. SELLs stay optimistic-fill: exits are
proportional mirrors of a position we hold, and modeling exit failure
honestly needs the retry/pending machinery, not a one-line reject."""
live = False
max_slippage_pct = 0.05 # mirrors live.max_slippage_pct's default
def buy(self, token_id, shares, price, meta):
bk = (meta or {}).get("book")
if bk is None:
bk = book_depth(token_id)
if bk is not None:
ba, cap = bk.get("ba"), price * (1 + self.max_slippage_pct)
if ba is None or ba > cap:
gone = ("no asks" if ba is None
else f"best ask {ba:.3f} > cap {cap:.3f}")
return {"ok": False, "filled_shares": 0.0, "price": price,
"resp": "no orders found to match with FAK order "
f"(paper model: {gone})", "paper": True}
return {"ok": True, "filled_shares": shares, "price": price, "paper": True}
def sell(self, token_id, shares, price, meta):
@@ -540,6 +566,7 @@ class CopyTrader:
# to 10% of depth, skip dust/unreliable books. A failed book fetch
# declines to bind — guard + protected prices still bound the copy.
dg = self.cfg.get("depth_gate")
bk = None # reused by the paper FAK model below
if dg:
bk = book_depth(token)
if bk and bk.get("ask5c") is not None:
@@ -576,7 +603,7 @@ class CopyTrader:
"on this token)")
return
shares = allowed / price
res = self.ex.buy(token, shares, price, {"title": title})
res = self.ex.buy(token, shares, price, {"title": title, "book": bk})
if not res["ok"]:
# in-play books ACCEPT orders with a delayed hold — the executor
# reports those as pending (order id + pre-order balance) instead
+104
View File
@@ -0,0 +1,104 @@
"""Stub tests for the paper FAK no-match model (HANDOFF 2026-07-15):
PaperExecutor.buy rejects when no ask sits inside the live protected band,
reuses the depth gate's book snapshot, fails open on a dead book fetch, and
LedgerPaperExecutor records no fill row for a rejection. Run:
python3 tests/test_paper_fak.py needs no network, no config.
"""
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
import copytrade # noqa: E402
import copybot # noqa: E402
fails = []
def case(name):
def deco(fn):
bd0 = copytrade.book_depth
try:
fn()
print(f"PASS {name}")
except AssertionError as e:
print(f"FAIL {name}: {e}")
fails.append(name)
finally:
copytrade.book_depth = bd0
return deco
def boom(token_id):
raise AssertionError("book_depth called — meta['book'] should be reused")
@case("ask inside the band -> fills, and the meta book is reused (no refetch)")
def t1():
copytrade.book_depth = boom
ex = copytrade.PaperExecutor()
r = ex.buy("T", 10.0, 0.80, {"book": {"ba": 0.82, "ask5c": 200.0}})
assert r["ok"] and r["filled_shares"] == 10.0 and r["price"] == 0.80
@case("best ask above quote*(1+5%) -> FAK no-match rejection")
def t2():
copytrade.book_depth = boom
ex = copytrade.PaperExecutor()
r = ex.buy("T", 10.0, 0.80, {"book": {"ba": 0.90, "ask5c": 200.0}})
assert not r["ok"] and r["filled_shares"] == 0.0
assert "no orders found to match" in r["resp"], r["resp"]
@case("no asks at all -> FAK no-match rejection")
def t3():
copytrade.book_depth = boom
ex = copytrade.PaperExecutor()
r = ex.buy("T", 10.0, 0.80, {"book": {"ba": None, "ask5c": None}})
assert not r["ok"] and "no asks" in r["resp"], r["resp"]
@case("boundary: ask exactly at the cap still fills")
def t4():
copytrade.book_depth = boom
ex = copytrade.PaperExecutor()
r = ex.buy("T", 10.0, 0.80, {"book": {"ba": 0.84, "ask5c": 200.0}})
assert r["ok"], r
@case("no meta book + dead book fetch -> fail OPEN (fills, today's behavior)")
def t5():
copytrade.book_depth = lambda token_id: None
ex = copytrade.PaperExecutor()
r = ex.buy("T", 10.0, 0.80, {"title": "x", "book": None})
assert r["ok"] and r["filled_shares"] == 10.0
@case("no meta book -> fetches its own and applies the band")
def t6():
copytrade.book_depth = lambda token_id: {"ba": 0.95, "ask5c": 200.0}
ex = copytrade.PaperExecutor()
r = ex.buy("T", 10.0, 0.80, {"title": "x"})
assert not r["ok"] and "no orders found to match" in r["resp"]
@case("SELLs stay optimistic-fill (unchanged)")
def t7():
copytrade.book_depth = boom
ex = copytrade.PaperExecutor()
r = ex.sell("T", 10.0, 0.80, {"book": {"ba": None}})
assert r["ok"] and r["filled_shares"] == 10.0
@case("LedgerPaperExecutor: rejection appends NO fill row; success appends")
def t8():
copytrade.book_depth = boom
ex = copybot.LedgerPaperExecutor()
r = ex.buy("T", 10.0, 0.80, {"book": {"ba": 0.95, "ask5c": 200.0}})
assert not r["ok"] and ex.fills == [], ex.fills
r = ex.buy("T", 10.0, 0.80, {"book": {"ba": 0.81, "ask5c": 200.0}})
assert r["ok"] and len(ex.fills) == 1 and ex.fills[0]["shares"] == 10.0
print()
print("FAILURES:", fails or "none")
sys.exit(1 if fails else 0)