Files
momentum-system/judge/decide.py
T
momentum devandClaude Opus 5.5 f2c7e52c79 feat: judgement-model (v2) paper trader on the Alpaca "Momentum V2" account
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
2026-09-28 18:25:26 -05:00

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