diff --git a/judge/live.py b/judge/live.py index 8cbebf6..a79d7b8 100644 --- a/judge/live.py +++ b/judge/live.py @@ -141,7 +141,7 @@ class Trader: def _earlier_today(self) -> dict: """What earlier runs of today's session did (the task relaunches the trader if it dies): orders per symbol, so the per-symbol cap holds across restarts, and each symbol's last fill.""" - out = {"orders": {}, "fills": {}} + out = {"orders": {}, "fills": {}, "watched": {}} f = OUT / "journal" / f"{self.day}.jsonl" if not f.exists(): return out @@ -154,8 +154,22 @@ class Trader: out["orders"][r["symbol"]] = out["orders"].get(r["symbol"], 0) + 1 elif r.get("kind") == "fill": out["fills"][r["symbol"]] = r + elif r.get("kind") == "add" and r.get("prev_close"): + out["watched"][r["symbol"]] = float(r["prev_close"]) return out + async def rewatch(self): + """Re-load every symbol an earlier run of today was watching. Without this, a relaunched trader only knew the + names still passing the scanner, so it stopped deciding on the others, and their ticks were never saved for + the paper-vs-sim review (2026-10-02: CABO, APLX).""" + for sym, pc in self.earlier.get("watched", {}).items(): + if sym in self.books or len(self.books) >= 30: + continue + try: + await self.add_symbol(sym, pc) + except Exception as e: # noqa: BLE001 + self.j.log("error", where="rewatch", symbol=sym, err=str(e)[:200]) + async def adopt(self): """Take over positions left by an earlier run today, so they still get the trail, time stop and EOD flat. A position with no known stop (no journalled fill and no working stop order) is closed rather than guessed at.""" @@ -589,6 +603,7 @@ class Trader: tasks = [asyncio.create_task(self.stream_loop()), asyncio.create_task(self.scan_loop()), asyncio.create_task(self.decide_loop())] await self.adopt() + await self.rewatch() await self.manage_loop() for t in tasks: t.cancel() @@ -597,4 +612,8 @@ class Trader: if __name__ == "__main__": + if sys.stdout is None or sys.stderr is None: # pythonw (no console window): keep crash output in the log + (OUT / "logs").mkdir(parents=True, exist_ok=True) + _log = open(OUT / "logs" / f"live_{now_et():%Y-%m-%d}.log", "a", encoding="utf-8", buffering=1) + sys.stdout = sys.stderr = _log asyncio.run(Trader().run()) diff --git a/tests/unit/test_judge_live_exits.py b/tests/unit/test_judge_live_exits.py index 2a89d34..6163984 100644 --- a/tests/unit/test_judge_live_exits.py +++ b/tests/unit/test_judge_live_exits.py @@ -122,3 +122,21 @@ def test_soft_stop_walks_trades_in_order(): 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)]