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
This commit is contained in:
1 parent
ac92280b9a
commit
7e6f881b17
2 files changed
+38
-1
No files matched your search
+20
-1
@@ -141,7 +141,7 @@ class Trader:
|
|||||||
def _earlier_today(self) -> dict:
|
def _earlier_today(self) -> dict:
|
||||||
"""What earlier runs of today's session did (the task relaunches the trader if it dies):
|
"""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."""
|
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"
|
f = OUT / "journal" / f"{self.day}.jsonl"
|
||||||
if not f.exists():
|
if not f.exists():
|
||||||
return out
|
return out
|
||||||
@@ -154,8 +154,22 @@ class Trader:
|
|||||||
out["orders"][r["symbol"]] = out["orders"].get(r["symbol"], 0) + 1
|
out["orders"][r["symbol"]] = out["orders"].get(r["symbol"], 0) + 1
|
||||||
elif r.get("kind") == "fill":
|
elif r.get("kind") == "fill":
|
||||||
out["fills"][r["symbol"]] = r
|
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
|
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):
|
async def adopt(self):
|
||||||
"""Take over positions left by an earlier run today, so they still get the trail, time stop and EOD flat.
|
"""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."""
|
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()),
|
tasks = [asyncio.create_task(self.stream_loop()), asyncio.create_task(self.scan_loop()),
|
||||||
asyncio.create_task(self.decide_loop())]
|
asyncio.create_task(self.decide_loop())]
|
||||||
await self.adopt()
|
await self.adopt()
|
||||||
|
await self.rewatch()
|
||||||
await self.manage_loop()
|
await self.manage_loop()
|
||||||
for t in tasks:
|
for t in tasks:
|
||||||
t.cancel()
|
t.cancel()
|
||||||
@@ -597,4 +612,8 @@ class Trader:
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
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())
|
asyncio.run(Trader().run())
|
||||||
@@ -122,3 +122,21 @@ def test_soft_stop_walks_trades_in_order():
|
|||||||
assert Trader._soft_stop_hit(pz, [11.25]) == "soft_trail"
|
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)
|
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
|
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)]
|
||||||
Reference in new issue
Block a user