Files
momentum-system/tests/unit/test_judge_live_exits.py
momentum devandClaude Opus 5.5 7e6f881b17 fix: judge paper trader runs windowless and re-watches earlier symbols after a restart
On 2026-10-02 the trader died 5 times (13:15-15:05 ET). It left no traceback, and
each death came within the task's 5-min re-fire window, which fits its console
window being closed (as on 2026-09-29) while the PC was in use. Every relaunch popped
a new window.
- Task Scheduler now launches it with pythonw: no console window to close. Under
  pythonw, stdout/stderr go to data/judge/logs/live_<date>.log, so a real crash
  still leaves its traceback.
- A relaunched trader re-loads every symbol today's earlier runs were watching (from
  the journal's "add" rows). It used to know only the names still passing the
  scanner, so it stopped deciding on the others and never saved their ticks; the
  review then had nothing to compare CABO and APLX against.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B3PXXn2tHBzdv6rYtGPz86
2026-10-02 16:08:55 -05:00

143 lines
5.5 KiB
Python

"""judge.live exit/stop bookkeeping against a fake broker. Each case is a failure seen on the paper account:
2026-09-29 BKYI (stop already filled -> endless 422 exit retries), MSGY (partial fill left unprotected);
2026-09-30 PWCM (a moved stop is a new order id; the stale id made the trader double-arm and block its own exit)."""
import asyncio
import pandas as pd
import pytest
pytest.importorskip("websockets")
from judge import live # noqa: E402
from judge.live import Pos, Trader # noqa: E402
class J:
def __init__(self):
self.rows = []
def log(self, kind, **kw):
self.rows.append((kind, kw))
class Broker:
def __init__(self, orders=None, open_orders=None, held=33, submit_ok=True):
self.orders, self.open, self.held, self.submit_ok = orders or {}, open_orders or [], held, submit_ok
self.subs, self.cancelled, self.closed = [], [], []
def _trading(self, method, path, params=None):
return self.orders[path.split("/")[-1]]
def get_orders(self, status, limit, nested, symbols):
return self.open
def get_positions(self, fresh=False):
return [{"symbol": "X", "qty": str(self.held)}] if self.held else []
def submit_order(self, *a):
self.subs.append(a)
if not self.submit_ok:
raise RuntimeError("403 insufficient qty")
return {"id": "armed"}
def replace_order(self, oid, stop_price=None):
return {"id": "new2"}
def cancel_order(self, oid):
self.cancelled.append(oid)
def close_position(self, sym):
self.closed.append(sym)
def trader(broker, leg="old"):
t = Trader.__new__(Trader)
t.j, t.client, t.books = J(), broker, {}
pz = Pos("X", 33, 1.30, 1.20, 0.10, live.now_et(), "o", leg, 1.30, "burst", 0.4)
t.pos = {"X": pz}
return t, pz
def test_filled_stop_is_recorded_as_exit():
b = Broker({"old": {"status": "filled", "filled_avg_price": "1.21"}}, held=0)
t, pz = trader(b)
asyncio.run(t.exit_pos(pz, "trail", 1.25))
assert "X" not in t.pos and t.j.rows[-1][1]["reason"] == "stop"
def test_cancelled_leg_with_shares_rearms_a_stop():
t, pz = trader(Broker({"old": {"status": "canceled"}}))
assert asyncio.run(t._reconcile(pz, 1.28)) is False
assert pz.leg_id == "armed" and t.client.subs
def test_replaced_leg_follows_the_chain():
b = Broker({"old": {"status": "replaced", "replaced_by": "new1"}, "new1": {"status": "new"}}, submit_ok=False)
t, pz = trader(b)
asyncio.run(t._reconcile(pz, 1.28))
assert pz.leg_id == "new1" and not b.subs
def test_working_stop_at_broker_is_adopted_not_doubled():
legs = [{"id": "legX", "side": "sell", "type": "stop", "status": "new"}]
b = Broker({"old": {"status": "canceled"}}, [{"id": "p", "side": "buy", "type": "limit", "legs": legs}], submit_ok=False)
t, pz = trader(b)
asyncio.run(t._reconcile(pz, 1.28))
assert pz.leg_id == "legX" and not b.subs
def test_price_through_stop_closes_instead_of_arming():
b = Broker({"old": {"status": "canceled"}})
t, pz = trader(b)
asyncio.run(t._reconcile(pz, 1.19))
assert b.closed == ["X"] and not b.subs
def test_exit_cancels_every_open_order_first():
opens = [{"id": "a", "side": "sell", "type": "stop", "status": "new"}, {"id": "b", "side": "sell", "type": "stop", "status": "new"}]
b = Broker({"old": {"status": "new"}}, opens)
t, pz = trader(b)
asyncio.run(t.exit_pos(pz, "trail", 1.36))
assert b.cancelled == ["a", "b"] and b.closed == ["X"] and "X" not in t.pos
def test_earlier_today_counts_orders_and_last_fill(tmp_path, monkeypatch):
(tmp_path / "journal").mkdir()
(tmp_path / "journal" / "2026-09-30.jsonl").write_text(
'{"kind":"order","symbol":"X"}\n{"kind":"order","symbol":"X"}\n'
'{"kind":"fill","symbol":"X","stop":1.2,"at":"2026-09-30 09:40:00"}\nnot json\n', encoding="utf-8")
monkeypatch.setattr(live, "OUT", tmp_path)
t = Trader.__new__(Trader)
t.day = "2026-09-30"
e = t._earlier_today()
assert e["orders"] == {"X": 2} and e["fills"]["X"]["stop"] == 1.2
assert pd.Timestamp(e["fills"]["X"]["at"]).hour == 9
def test_soft_stop_walks_trades_in_order():
pz = Pos("X", 100, 10.0, 9.5, 0.5, live.now_et(), "o", "leg", 10.0, "burst", 0.4)
assert Trader._soft_stop_hit(pz, [10.1, 9.8, 9.6]) is None # above the stop
assert Trader._soft_stop_hit(pz, [9.7, 9.5]) == "soft_stop" # a print at the stop
pz = Pos("X", 100, 10.0, 9.5, 0.5, live.now_et(), "o", "leg", 10.0, "burst", 0.4)
assert Trader._soft_stop_hit(pz, [10.6, 12.0, 11.5]) is None # +1R reached; trail 6% under 12.0 = 11.28
assert Trader._soft_stop_hit(pz, [11.25]) == "soft_trail"
pz = Pos("X", 100, 10.0, 9.5, 0.5, live.now_et(), "o", "leg", 10.0, "burst", 0.4)
assert Trader._soft_stop_hit(pz, [9.4, 12.0]) == "soft_stop" # order matters: the dip came first
def test_restart_rewatches_symbols_from_earlier_runs(tmp_path, monkeypatch):
(tmp_path / "journal").mkdir()
(tmp_path / "journal" / "2026-10-02.jsonl").write_text(
'{"kind":"add","symbol":"CABO","prev_close":11.5}\n{"kind":"add","symbol":"APLX","prev_close":7.2}\n',
encoding="utf-8")
monkeypatch.setattr(live, "OUT", tmp_path)
t = Trader.__new__(Trader)
t.day, t.books, t.j = "2026-10-02", {}, J()
t.earlier = t._earlier_today()
added = []
async def fake_add(sym, pc):
added.append((sym, pc)); t.books[sym] = object()
t.add_symbol = fake_add
asyncio.run(t.rewatch())
assert added == [("CABO", 11.5), ("APLX", 7.2)]