A gradient-boosted model scores burst / micro-pullback / whole-dollar signals on
squeeze stocks from tick data, and only its top-scored regular-hours signals
are traded. Validated in data/ticksim before any live use:
- v1 on a fresh 600-day sample: real ranking (AUC 0.63), thin edge. FAIL.
- v2 (trained on Jun-Jul of samples 1+2), pre-registered, on 500 NEW Aug-Sep
symbol-days: +1.11%/trade after the spread (day-block 90% lo +0.54%),
n=332 over 38 days. PASS. It held at 2 s latency and +0.2% extra cost.
- judge/engine.py: the validated signals, execution gates and features, as one
shared source.
- judge/model.py: trains and freezes v2 (cutoff = the training top 20%).
- judge/decide.py: pure decision logic.
- judge/replay.py: a live-style replay of recorded days. Parity: 40 of 41 research
trades matched with identical model scores.
- judge/live.py: the paper trader. Watchlist from the snapshot scan with
backfill, SIP trade/quote websocket, decisions each second, a limit at the ask
with a broker OTO stop (cancelled if unfilled in 3 s), trail 6% after +1R,
20-min time stop, flat by 15:55. $20 risk and $1,000 max notional per trade.
Everything is journalled and the day's ticks are kept for paper-vs-simulation
review. Weekends and holidays exit.
- Runs from the workstation (Task Scheduler, weekdays 09:10 ET) with the
research venv. Paper only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E3S5EFKThFVrjmN4HHsHMN
82 lines
3.1 KiB
Python
82 lines
3.1 KiB
Python
"""Pure decision logic: trades/quotes up to `now` -> the scored, tradeable signals.
|
|
Shared by the live paper trader and the replay/parity check."""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
|
|
from judge import engine
|
|
from judge.model import score
|
|
|
|
|
|
@dataclass
|
|
class DayContext:
|
|
symbol: str
|
|
date: str # YYYY-MM-DD
|
|
prev_close: float
|
|
gap: float | None # today's open vs prior close (known after 09:30)
|
|
med20: float | None # median daily volume over the prior 20 sessions
|
|
windows: list = field(default_factory=list)
|
|
|
|
|
|
@dataclass
|
|
class Decision:
|
|
setup: str
|
|
ts: object # signal time (exact crossing trade for micropb)
|
|
sp: float # signal price
|
|
stop: float
|
|
p: float
|
|
take: bool
|
|
reason: str
|
|
feats: dict
|
|
|
|
|
|
def window_trades(T: pd.DataFrame, ctx: DayContext) -> pd.DataFrame:
|
|
"""Trades inside the model's data windows. Features and bars must see exactly what training saw."""
|
|
if T is None or T.empty:
|
|
return T
|
|
m = np.zeros(len(T), dtype=bool)
|
|
for s, e in ctx.windows:
|
|
m |= (T.t.values >= np.datetime64(s)) & (T.t.values < np.datetime64(e))
|
|
return T[m].reset_index(drop=True)
|
|
|
|
|
|
def scan(T: pd.DataFrame, Q: pd.DataFrame, ctx: DayContext, art: dict, now=None, after=None,
|
|
include_partial: bool = True) -> list[Decision]:
|
|
"""Every signal known by `now` (and newer than `after`), scored.
|
|
T and Q hold ALL buffered trades/quotes; windowing happens here. `now` defaults to the last trade."""
|
|
Tw = window_trades(T, ctx)
|
|
if Tw is None or len(Tw) < 50 or Q is None or len(Q) < 50:
|
|
return []
|
|
now = np.datetime64(now) if now is not None else Tw.t.values[-1]
|
|
Tw = Tw[Tw.t.values <= now].reset_index(drop=True)
|
|
b_all = engine.bars5(Tw)
|
|
closed = b_all[b_all.tc.values <= now].reset_index(drop=True)
|
|
tt = Tw.t.values; tp = Tw.p.values.astype(float)
|
|
out = []
|
|
for setup in engine.SETUPS:
|
|
P = dict(engine.EXEC, setup=setup)
|
|
bars = b_all if (setup == "micropb" and include_partial) else closed
|
|
for sig in engine.signals(bars, P):
|
|
ts, sp, stop = sig[0], float(sig[1]), float(sig[2])
|
|
if len(sig) > 3 and sig[3] == "cross":
|
|
ts = engine.cross_time(tt, tp, ts, sp)
|
|
if ts is None:
|
|
continue
|
|
if ts > now or (after is not None and ts <= np.datetime64(after)):
|
|
continue
|
|
tsn = pd.Timestamp(ts)
|
|
if tsn.time() < pd.Timestamp("09:30").time(): # the v2 rule trades regular hours only
|
|
continue
|
|
f = engine.feats_at(Tw, Q, ts, ctx.prev_close)
|
|
if f is None:
|
|
continue
|
|
f = engine.finish_features(f, setup, ctx.gap, ctx.med20)
|
|
p = score(art, f)
|
|
take = p >= art["cutoff"]
|
|
out.append(Decision(setup, ts, sp, stop, p, take, "score>=cutoff" if take else "score<cutoff", f))
|
|
out.sort(key=lambda d: d.ts)
|
|
return out
|