mirror of
https://github.com/jaxperro/winning-wallet-finder.git
synced 2026-07-27 15:57:47 +00:00
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:
+22
-1
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user