Files
momentum-system/tests/unit/research/test_live_scorecard.py
T
momentum devandClaude Fable 5.1 b5b38ab798 fix: scorecard counts round trips the broker filled but the journal never closed (TNON)
On 2026-10-02 the live bot bought 7 TNON at 4.62 and the Tradier sell-stop filled at
4.30 forty-nine seconds later, before a position record existed, so no position_closed
event was written and the 80-trade scorecard silently dropped a -1R loser. The daily
equity-vs-P&L gap ($2.31 that day, cents everywhere else) is what exposed it.

load_trades now merges the journal with a cross-check of the broker's filled orders
(account_orders_updated snapshots, one row per broker order id). For any session day and
symbol with a filled buy and sell but no journaled close, reconstruct_missing synthesises
the trade from the fills: fill VWAPs for entry/exit, the sell-stop's stop price for R.
Such trades are labelled "[broker fill; journal missed]" and the daily report alerts on a
same-day miss. The paper side of the exit A/B skips the check: that store's order
snapshots run to 2.2 GB over the window and its Alpaca journal is reconciled by
alpaca_paper/sync.

Corrected test state: 16/80, mean R -0.08, realized +$0.91 (was 15/80, -0.02, +$3.15).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LXxioW7HA18AY1gqnzA2HN
2026-10-08 12:58:53 -05:00

63 lines
2.9 KiB
Python

"""Live-test scorecard: pure verdict / drift logic."""
from research.live_scorecard import config_drift, load_freeze, verdict
def test_running_until_target():
assert verdict([0.5] * 10, 5.0, 0, target=80).startswith("RUNNING: 10/80")
def test_floor_halt_fails_immediately():
assert verdict([2.0] * 100, 50.0, 1).startswith("FAIL: the equity floor")
def test_pass_needs_positive_r_and_pnl():
assert verdict([0.2] * 80, 10.0, 0).startswith("PASS")
assert verdict([0.2] * 80, -1.0, 0).startswith("FAIL")
assert verdict([-0.1] * 80, 5.0, 0).startswith("FAIL")
def test_drift_ignores_safety_knobs_and_catches_new_strategy_keys():
frozen = {"TRADING_EXIT_TRAIL_PCT": "0.12", "TRADING_EQUITY_FLOOR": "215"}
current = {"TRADING_EXIT_TRAIL_PCT": "0.12", "TRADING_EQUITY_FLOOR": "190",
"TRIGGER_GAP_MAX": "40", "OLLAMA_MODEL": "x"}
assert config_drift(frozen, current) == {"TRIGGER_GAP_MAX": (None, "40")}
def test_freeze_file_is_valid():
f = load_freeze()
assert f["frozen_on"] == "2026-09-29"
assert f["settings"]["TRADING_EXIT_TRAIL_PCT"] == "0.12"
def _fill(ts, sym, side, typ, qty, price, stop=None):
return {"ts": ts, "symbol": sym, "side": side, "type": typ, "qty": qty, "price": price, "stop": stop}
def test_reconstruct_missing_counts_the_tnon_style_trade():
# 2026-10-02: bought 7 TNON @4.62, sell-stop 4.30 filled 49 s later, no position_closed ever written
from research.live_scorecard import reconstruct_missing
fills = [_fill("2026-10-02 09:38:40", "TNON", "buy", "limit", 7, 4.62),
_fill("2026-10-02 09:39:30", "TNON", "sell", "stop", 7, 4.30, stop=4.30),
# a journaled trade the same day must NOT be duplicated
_fill("2026-10-02 10:28:10", "CYCU", "buy", "limit", 5, 3.27),
_fill("2026-10-02 10:40:05", "CYCU", "sell", "stop", 5, 4.32, stop=4.33)]
journal = [{"ts": "2026-10-02 10:40:03", "symbol": "CYCU", "pnl": 5.25, "r": 1.8,
"reason": "stop_loss", "source": "journal"}]
out = reconstruct_missing(fills, journal)
assert [t["symbol"] for t in out] == ["TNON"]
t = out[0]
assert t["pnl"] == -2.24 and abs(t["r"] + 1.0) < 1e-9
assert t["source"] == "broker" and t["reason"].startswith("stop_loss")
assert str(t["ts"]).startswith("2026-10-02 09:39")
def test_reconstruct_missing_ignores_one_sided_and_journaled_days():
from research.live_scorecard import reconstruct_missing
fills = [_fill("2026-10-05 09:36:30", "SBS", "buy", "limit", 5, 6.33), # still open: no sell
_fill("2026-10-06 11:10:00", "AIFA", "buy", "limit", 4, 7.425),
_fill("2026-10-06 11:10:30", "AIFA", "sell", "market", 4, 7.88)] # journaled below
journal = [{"ts": "2026-10-06 11:10:31", "symbol": "AIFA", "pnl": 1.82, "r": 0.6,
"reason": "stop_loss", "source": "journal"}]
assert reconstruct_missing(fills, journal) == []