Review sweep: money-path fixes + security + delete 805MB dead weight

Consolidated 4-tool review (2 correctness agents + Codex + over-eng audit).

Money-path (regression-tested):
- _flatten_all: moved the fresh-broker close from close_session INTO the
  shared function + added a broker-held guard on entry cancellation, so
  the daily-loss CIRCUIT BREAKER (which called _flatten_all raw) no
  longer strips a just-filled bracket's legs or leaves a sync-gap fill
  naked. One guard, all callers.
- restart state rebuild: _fresh_entries cap, _requested_symbols dedup,
  and losing benches now rebuild from today's events on boot — a
  mid-session restart no longer resets the cap to 0 or re-fires a
  resting order into a duplicate (the 2026-07-06 incident).
- POST /orders no longer retries a lost-response URLError (was
  re-sending duplicate live orders); GET/DELETE still retry.
- expire_stale_entries now @_locked (raced the trigger thread).
- _session_open: ET conversion instead of a hardcoded 13:30-13:33Z
  window that only matched EDT — the fast path armed nothing all winter.

Security:
- admin panel fail-CLOSED on empty ADMIN_TOKEN (was fail-open on a panel
  fronting the docker socket); header-only token, constant-time compare.
- admin API responses redact secret override VALUES (live keys would
  have echoed after a paper->live switch).
- dashboard: removed wildcard CORS (cross-origin drive-by could drive
  trade actions); optional DASHBOARD_TOKEN gate on POST.

Deletions (~805MB + dead code shipped in the prod image): momentum-
rebuilt.zip, archive venv/htmlcov, and schwab/ core/ telemetry/ models/
runtime/providers/ storage/replay.py controlroom/ + three dead
evaluation stubs + package.json — all verified zero-importer.

+4 regression tests. 99 money-path/trigger/exit tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
momentum devandClaude Opus 4.8 committed 2026-07-11 13:00:10 -04:00
1 parent cad9051a96
commit d54a2a930c
28 files changed
+210 -1789

No files matched your search

+16 -5
View File
@@ -16,6 +16,7 @@ Run: python admin_api.py (ADMIN_HOST/ADMIN_PORT, default 0.0.0.0:8767)
from __future__ import annotations
import hmac
import http.client
import json
import os
@@ -70,6 +71,14 @@ SEED_PROFILES = {
}
def _safe_overrides() -> dict:
"""Overrides for API responses with secret VALUES redacted — a /api/mode live
switch writes ALPACA_LIVE_* into overrides.env; without this they'd echo back
in every /api/status and /api/config response."""
return {k: ("***" if any(m in k for m in SECRET_MARKERS) else v)
for k, v in env_overrides.read_overrides().items()}
def _editable(key: str) -> bool:
if key in EDITABLE_EXACT:
return True
@@ -194,10 +203,12 @@ class Handler(BaseHTTPRequestHandler):
self.wfile.write(raw)
def _authed(self) -> bool:
# FAIL CLOSED: no token configured => deny (this panel fronts the docker
# socket + the paper/live switch; an unset env var must not open it to the
# LAN). Header only — a ?token= query would leak into proxy/access logs.
if not ADMIN_TOKEN:
return True # explicit opt-out: no token configured
return (self.headers.get("X-Admin-Token") == ADMIN_TOKEN
or f"token={ADMIN_TOKEN}" in (self.path.split("?", 1) + [""])[1])
return False
return hmac.compare_digest(self.headers.get("X-Admin-Token", ""), ADMIN_TOKEN)
def do_GET(self): # noqa: N802
path = self.path.split("?")[0]
@@ -208,7 +219,7 @@ class Handler(BaseHTTPRequestHandler):
return self._send(401, {"error": "bad or missing X-Admin-Token"})
if path == "/api/status":
return self._send(200, {"containers": list_containers(), "checks": checks(),
"overrides": env_overrides.read_overrides()})
"overrides": _safe_overrides()})
if path == "/api/config":
eff = {k: v for k, v in sorted(os.environ.items()) if _editable(k)}
secrets = {k: bool(os.environ.get(k)) for k in
@@ -216,7 +227,7 @@ class Handler(BaseHTTPRequestHandler):
"ALPACA_LIVE_SECRET_KEY", "TELEGRAM_BOT_TOKEN", "YOUTUBE_API_KEY",
"FMP_API_KEY")}
return self._send(200, {"effective": eff, "secrets_present": secrets,
"overrides": env_overrides.read_overrides()})
"overrides": _safe_overrides()})
if path == "/api/profiles":
return self._send(200, profiles())
return self._send(404, {"error": "unknown route"})
+8 -2
View File
@@ -137,8 +137,14 @@ class AlpacaPaperClient:
continue
raise AlpacaApiError(exc.code, exc.read().decode(errors="replace")) from exc
except urllib.error.URLError:
# transient network/DNS (e.g. gaierror) — retry a couple times
if attempt >= self.max_retries:
# transient network/DNS (e.g. gaierror) — retry a couple times.
# NOT for POST: urllib wraps a lost RESPONSE (read timeout/reset)
# in URLError too, so attempt 1 may have SUCCEEDED at the broker
# with the reply lost; re-sending POST /orders then rests a
# duplicate live order (or 422s and looks like a failure while a
# real bracket rests untracked). Idempotent GET/DELETE retry; POST
# raises so the caller reconciles against broker positions.
if method == "POST" or attempt >= self.max_retries:
raise
_time.sleep(0.4 * (attempt + 1))
raise RuntimeError("request retries exhausted") # unreachable
+7 -3
View File
@@ -297,7 +297,6 @@ class DashboardHandler(BaseHTTPRequestHandler):
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.send_header("Access-Control-Allow-Origin", "*")
self.end_headers()
self.wfile.write(body)
except (BrokenPipeError, ConnectionResetError):
@@ -346,7 +345,6 @@ class DashboardHandler(BaseHTTPRequestHandler):
self.send_header("Content-Type", "text/event-stream")
self.send_header("Cache-Control", "no-cache")
self.send_header("Connection", "keep-alive")
self.send_header("Access-Control-Allow-Origin", "*")
self.end_headers()
shutdown = getattr(type(self), "shutdown_event", None)
@@ -410,6 +408,13 @@ class DashboardHandler(BaseHTTPRequestHandler):
self._send_json({"error": str(exc)}, 500)
def do_POST(self): # noqa: N802
# optional shared-secret gate on trade actions (approve/reject/exit/watch).
# Off by default (autonomous stack auto-approves); set DASHBOARD_TOKEN to
# require X-Dashboard-Token on every POST. CORS wildcard was removed so a
# cross-origin LAN page can't drive these either way.
_tok = os.environ.get("DASHBOARD_TOKEN")
if _tok and not hmac.compare_digest(self.headers.get("X-Dashboard-Token", ""), _tok):
return self._send_json({"error": "bad or missing X-Dashboard-Token"}, 401)
try:
route = urlparse(self.path).path
body = self._read_body()
@@ -443,7 +448,6 @@ class DashboardHandler(BaseHTTPRequestHandler):
def do_OPTIONS(self): # noqa: N802
self.send_response(204)
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
self.send_header("Access-Control-Allow-Headers", "Content-Type")
self.end_headers()
-399
View File
@@ -1,399 +0,0 @@
"""Momentum Trading Control Room — NiceGUI live-analysis dashboard (Milestone 5, core).
Phases 1+2 of the staged control-room plan, adapted to THIS backend: a real-time
operator view with LIVE TICKERS (SIP last-trade prices at 1s), per-symbol analysis
(state, gap, RVOL, trigger distance, quality, population-gate check), live positions
P&L, today's gate activity + attribution, and the approval queue with confirm-flow
actions. Runs as its OWN service on :8766 — the existing dashboard on :8765 stays
untouched until this reaches parity (their Phase 3-5: manual orders, settings dials,
audit trail).
Data flow: read-only projections from the shared Postgres event store + Alpaca SIP
for live prices; fully AUTONOMOUS ops (owner directive: no approvals/interaction needed) — the only
action is the emergency panic brake (cancel all open orders, typed confirmation)."""
from __future__ import annotations
import json
import os
from datetime import date, datetime
from dotenv import load_dotenv
load_dotenv(os.path.join(os.path.dirname(__file__), "..", ".env"))
from nicegui import run, ui # noqa: E402
from alpaca_paper.client import AlpacaPaperClient # noqa: E402
from alpaca_paper.settings import AlpacaPaperSettings # noqa: E402
from momentum_cli import _market_clock, _triggers_snapshot, _unprotected_positions # noqa: E402
from storage.db_pg import get_connection # noqa: E402
from storage.event_store import EventStore # noqa: E402
from storage.projections import query_approval_queue, query_ready_signals_snapshot # noqa: E402
POP_GAP, POP_VOL = 0.10, 300_000 # display mirror of the live population gate
import threading
con = get_connection("momentum")
store = EventStore("momentum")
client = AlpacaPaperClient(AlpacaPaperSettings.from_env())
DB_LOCK = threading.Lock() # fetchers run in io_bound THREADS; one psycopg2 conn is not thread-safe
# ---------------- data fetchers (sync; called via run.io_bound) ----------------
def fetch_board() -> dict:
with DB_LOCK:
trig_rows, snap_ts, armed_n = _triggers_snapshot(con)
signals = {s["symbol"]: s for s in query_ready_signals_snapshot(store)}
rows = []
for r in trig_rows:
sym = r.get("symbol")
sig = signals.get(sym, {})
gap = r.get("gap") # percent units from the book
gap_frac = (gap / 100.0) if isinstance(gap, (int, float)) else sig.get("gap_pct")
vol = sig.get("cum_volume")
pop_ok = None
if gap_frac is not None:
pop_ok = bool(gap_frac >= POP_GAP and (vol is None or vol >= POP_VOL))
rows.append({
"symbol": sym, "state": r.get("state"), "price": r.get("price"),
"gap": gap, "rvol": r.get("rvol"), "trigger": r.get("trigger"),
"to_trig": r.get("→trigger") or r.get("to_trigger"), "stop": r.get("stop"),
"quality": sig.get("quality_score"), "vwap": sig.get("vwap"),
"pop": ("✓" if pop_ok else "✗" if pop_ok is not None else "—"),
"catalyst": (r.get("catalyst") or "")[:40],
})
return {"rows": rows, "ts": str(snap_ts)[11:19] if snap_ts else "—", "armed": armed_n}
def fetch_prices(symbols: list[str]) -> dict[str, float]:
if not symbols:
return {}
try:
trades = client.get_latest_trades(symbols)
return {s: float(t.get("p")) for s, t in (trades or {}).items() if t and t.get("p")}
except Exception: # noqa: BLE001
return {}
def fetch_account() -> dict:
try:
a = client.get_account()
eq = float(a.get("equity") or 0)
last = float(a.get("last_equity") or 0)
return {"equity": eq, "day_pnl": (eq - last) if last else 0.0}
except Exception: # noqa: BLE001
return {}
def fetch_positions() -> list[dict]:
try:
pos = client.get_positions(fresh=True)
with DB_LOCK:
naked = {n["symbol"] for n in (_unprotected_positions(con) or [])}
return [{
"symbol": p.get("symbol"), "qty": p.get("qty"),
"avg": float(p.get("avg_entry_price") or 0),
"price": float(p.get("current_price") or 0),
"pnl": float(p.get("unrealized_pl") or 0),
"protected": "⚠ NAKED" if p.get("symbol") in naked else "✓",
} for p in pos]
except Exception: # noqa: BLE001
return []
def fetch_gates() -> list[dict]:
today = date.today().isoformat()
hist: dict[str, int] = {}
with DB_LOCK:
rows = con.execute(
"SELECT payload_json FROM events WHERE event_type='risk_rule_triggered' "
"AND timestamp >= ?", [today]).fetchall()
for (pj,) in rows:
rt = (json.loads(pj or "{}").get("rule_type")) or "?"
hist[rt] = hist.get(rt, 0) + 1
return [{"gate": k, "cuts": v} for k, v in sorted(hist.items(), key=lambda x: -x[1])]
def fetch_attribution() -> list[dict]:
from research.attribution import compute_attribution
att = compute_attribution(days=7)
out = []
for d in sorted(att["days"], reverse=True):
st = att["days"][d]
c, r = st.get("counts", {}), st.get("rating", {})
out.append({"day": d, "disc": c.get("symbol_discovered", 0),
"ready": c.get("signal_ready", 0),
"early": f"{r.get('signals_early_pct', '—')}%",
"trades": st.get("outcomes", {}).get("trades", 0),
"win": f"{r.get('win_rate_pct', '—')}%",
"pnl": r.get("pnl")})
return out
def fetch_activity(limit: int = 40) -> list[dict]:
"""Merged live event feed for autonomous ops: entries, exits, fills, gate cuts."""
kinds = ("position_closed", "order_submitted", "order_cancelled", "signal_ready",
"risk_rule_triggered", "order_filled")
ph = ",".join(["?"] * len(kinds))
with DB_LOCK:
rows = con.execute(
f"SELECT timestamp, event_type, message FROM events "
f"WHERE event_type IN ({ph}) ORDER BY timestamp DESC LIMIT ?",
[*kinds, limit]).fetchall()
return [{"time": str(ts)[11:19], "type": et, "message": (m or "")[:140]}
for ts, et, m in rows]
def fetch_summary() -> dict:
today = date.today().isoformat()
with DB_LOCK:
closes = con.execute(
"SELECT payload_json FROM events WHERE event_type='position_closed' "
"AND timestamp >= ?", [today]).fetchall()
subs = con.execute(
"SELECT count(*) FROM events WHERE event_type='order_submitted' "
"AND timestamp >= ?", [today]).fetchone()[0]
cuts = con.execute(
"SELECT count(*) FROM events WHERE event_type='risk_rule_triggered' "
"AND timestamp >= ?", [today]).fetchone()[0]
hb = con.execute(
"SELECT max(timestamp) FROM events WHERE event_type='module_tick'").fetchone()[0]
pnls = []
for (pj,) in closes:
try:
pnls.append(float(json.loads(pj or "{}").get("realized_pnl") or 0))
except Exception: # noqa: BLE001
pass
age = (datetime.now() - hb).total_seconds() if hb else None
return {"trades": len(pnls), "wins": sum(1 for x in pnls if x > 0),
"realized": round(sum(pnls), 2), "submissions": subs, "cuts": cuts,
"loop_age": age}
def cancel_all_orders() -> str:
return str(client._trading("DELETE", "/orders"))[:200]
# ---------------- UI ----------------
@ui.page("/")
def main_page():
ui.add_head_html("""
<style>
body { background: #0d1520; }
.font-mono { font-family: ui-monospace, Menlo, monospace; }
.text-green { color: #22C55E !important; font-weight: bold; }
.text-red { color: #EF4444 !important; font-weight: bold; }
@keyframes flash-green { 0% { background:#052e16; } 100% { background:transparent; } }
@keyframes flash-red { 0% { background:#450a0a; } 100% { background:transparent; } }
.flash-green { animation: flash-green 1s ease-out; }
.flash-red { animation: flash-red 1s ease-out; }
</style>""")
prices: dict[str, float] = {}
board_syms: list[str] = []
with ui.header().classes("items-center justify-between px-4 bg-blue-900 text-white"):
with ui.row().classes("items-center gap-3"):
ui.label("Momentum Control Room").classes("text-lg font-bold")
ui.badge("🔵 PAPER — Alpaca (Simulated)").classes("text-sm px-2")
with ui.row().classes("items-center gap-4"):
phase_lbl = ui.label("—").classes("text-sm")
eq_lbl = ui.label("equity —").classes("font-mono text-sm")
hb_lbl = ui.label("♥ —").classes("font-mono text-xs")
async def panic():
with ui.dialog() as d, ui.card().classes("p-6"):
ui.label("⛔ Cancel ALL open orders?").classes("text-lg font-bold")
ui.label('Type "STOP" to confirm:').classes("mt-2")
typed = ui.input("")
with ui.row().classes("justify-end gap-2 mt-4"):
btn = ui.button("CANCEL ALL", on_click=lambda: d.submit(True)) \
.props("color=negative")
btn.bind_enabled_from(typed, "value", backward=lambda v: v == "STOP")
ui.button("Back", on_click=lambda: d.submit(False))
if await d:
res = await run.io_bound(cancel_all_orders)
ui.notify(f"cancel-all sent: {res}", type="warning")
ui.button("⛔ STOP ALL", on_click=panic).props("color=negative outline") \
.classes("font-bold")
clock_lbl = ui.label("--:--:--").classes("font-mono")
with ui.tabs().classes("w-full") as tabs:
t_live = ui.tab("Live")
t_pos = ui.tab("Positions")
t_gates = ui.tab("Gates & Attribution")
t_act = ui.tab("Activity")
with ui.tab_panels(tabs, value=t_live).classes("w-full"):
# ---- LIVE: real-time tickers + analysis ----
with ui.tab_panel(t_live):
with ui.row().classes("gap-4 mb-2"):
def _card(title):
with ui.card().classes("p-3 min-w-32"):
ui.label(title).classes("text-xs text-grey-6")
return ui.label("—").classes("text-lg font-bold font-mono")
c_pnl = _card("Day P&L")
c_trades = _card("Trades today")
c_win = _card("Win rate")
c_subs = _card("Submissions")
c_cuts = _card("Gate cuts")
c_loop = _card("Loop ♥")
async def refresh_summary():
sm = await run.io_bound(fetch_summary)
acct = await run.io_bound(fetch_account)
pnl = acct.get("day_pnl", 0.0)
c_pnl.text = f"${pnl:+,.0f}"
c_pnl.classes(replace="text-lg font-bold font-mono " +
("text-green" if pnl >= 0 else "text-red"))
c_trades.text = str(sm["trades"])
c_win.text = (f"{100*sm['wins']/sm['trades']:.0f}%" if sm["trades"] else "—")
c_subs.text = str(sm["submissions"])
c_cuts.text = str(sm["cuts"])
age = sm.get("loop_age")
c_loop.text = f"{age:.0f}s" if age is not None else "—"
c_loop.classes(replace="text-lg font-bold font-mono " +
("text-green" if age is not None and age < 180 else "text-red"))
ui.timer(5.0, refresh_summary)
with ui.row().classes("items-center gap-4"):
ui.label("Live tickers").classes("text-lg font-bold")
snap_lbl = ui.label("snapshot —").classes("text-xs font-mono text-grey")
armed_lbl = ui.label("").classes("text-xs font-mono")
live_grid = ui.aggrid({
"columnDefs": [
{"headerName": "Sym", "field": "symbol", "pinned": "left", "width": 90},
{"headerName": "State", "field": "state", "width": 95, "cellClassRules": {
"text-green": 'value === "armed" || value === "fired"',
}},
{"headerName": "Price", "field": "price", "width": 95,
"cellClass": "font-mono", "cellClassRules": {
"flash-green": "data._d > 0", "flash-red": "data._d < 0"}},
{"headerName": "Gap%", "field": "gap", "width": 80, "cellClass": "font-mono"},
{"headerName": "RVOL", "field": "rvol", "width": 80, "cellClass": "font-mono"},
{"headerName": "Trigger", "field": "trigger", "width": 95, "cellClass": "font-mono"},
{"headerName": "→Trig", "field": "to_trig", "width": 85, "cellClass": "font-mono"},
{"headerName": "Stop", "field": "stop", "width": 90, "cellClass": "font-mono"},
{"headerName": "Qual", "field": "quality", "width": 75, "cellClassRules": {
"text-green": "value >= 0.5", "text-red": "value < 0.5"}},
{"headerName": "Pop", "field": "pop", "width": 65, "cellClassRules": {
"text-green": 'value === "✓"', "text-red": 'value === "✗"'}},
{"headerName": "VWAP", "field": "vwap", "width": 90, "cellClass": "font-mono"},
{"headerName": "Catalyst", "field": "catalyst", "flex": 1},
],
"rowData": [], "domLayout": "autoHeight", "animateRows": True,
}).classes("w-full")
async def refresh_board():
data = await run.io_bound(fetch_board)
board_syms[:] = [r["symbol"] for r in data["rows"] if r.get("symbol")]
for r in data["rows"]:
lp = prices.get(r["symbol"])
if lp:
old = r.get("price")
r["_d"] = (lp - old) if isinstance(old, (int, float)) else 0
r["price"] = lp
live_grid.options["rowData"] = data["rows"]
live_grid.update()
snap_lbl.text = f"loop snapshot {data['ts']}"
armed_lbl.text = f"{data['armed']} armed · {len(data['rows'])} tracked"
async def refresh_prices():
if board_syms:
fresh = await run.io_bound(fetch_prices, list(board_syms))
if fresh:
prices.update(fresh)
rows = live_grid.options["rowData"]
for r in rows:
lp = prices.get(r["symbol"])
if lp and isinstance(r.get("price"), (int, float)):
r["_d"] = round(lp - r["price"], 4)
r["price"] = lp
live_grid.update()
ui.timer(3.0, refresh_board)
ui.timer(1.0, refresh_prices)
# ---- POSITIONS ----
with ui.tab_panel(t_pos):
pos_grid = ui.aggrid({
"columnDefs": [
{"headerName": "Sym", "field": "symbol", "width": 90},
{"headerName": "Qty", "field": "qty", "width": 80, "cellClass": "font-mono"},
{"headerName": "Avg", "field": "avg", "width": 95, "cellClass": "font-mono"},
{"headerName": "Price", "field": "price", "width": 95, "cellClass": "font-mono"},
{"headerName": "uP&L $", "field": "pnl", "width": 110, "cellClass": "font-mono",
"cellClassRules": {"text-green": "value >= 0", "text-red": "value < 0"}},
{"headerName": "Stop", "field": "protected", "width": 110, "cellClassRules": {
"text-red": 'value !== "✓"'}},
], "rowData": [], "domLayout": "autoHeight",
}).classes("w-full")
async def refresh_pos():
pos_grid.options["rowData"] = await run.io_bound(fetch_positions)
pos_grid.update()
ui.timer(2.0, refresh_pos)
# ---- GATES + ATTRIBUTION ----
with ui.tab_panel(t_gates):
ui.label("Today's gate activity").classes("text-lg font-bold")
gates_grid = ui.aggrid({"columnDefs": [
{"headerName": "Gate", "field": "gate", "flex": 1},
{"headerName": "Cuts today", "field": "cuts", "width": 120}],
"rowData": [], "domLayout": "autoHeight"}).classes("w-1/2")
ui.label("Component attribution (7 days)").classes("text-lg font-bold mt-6")
att_grid = ui.aggrid({"columnDefs": [
{"headerName": "Day", "field": "day", "width": 110},
{"headerName": "Disc", "field": "disc", "width": 80},
{"headerName": "Ready", "field": "ready", "width": 80},
{"headerName": "Early%", "field": "early", "width": 90},
{"headerName": "Trades", "field": "trades", "width": 80},
{"headerName": "Win%", "field": "win", "width": 80},
{"headerName": "P&L", "field": "pnl", "width": 100, "cellClass": "font-mono",
"cellClassRules": {"text-green": "value >= 0", "text-red": "value < 0"}}],
"rowData": [], "domLayout": "autoHeight"}).classes("w-full")
async def refresh_gates():
gates_grid.options["rowData"] = await run.io_bound(fetch_gates)
gates_grid.update()
ui.timer(5.0, refresh_gates)
async def load_attr():
att_grid.options["rowData"] = await run.io_bound(fetch_attribution)
att_grid.update()
ui.button("Load / refresh attribution", on_click=load_attr).props("outline")
# ---- ACTIVITY: autonomous-ops live feed ----
with ui.tab_panel(t_act):
ui.label("Live activity (autonomous — no input needed)").classes("text-lg font-bold")
act_grid = ui.aggrid({"columnDefs": [
{"headerName": "Time", "field": "time", "width": 100, "cellClass": "font-mono"},
{"headerName": "Event", "field": "type", "width": 170, "cellClassRules": {
"text-green": 'value === "position_closed" || value === "order_filled"',
"text-red": 'value === "risk_rule_triggered"'}},
{"headerName": "Detail", "field": "message", "flex": 1}],
"rowData": [], "domLayout": "autoHeight"}).classes("w-full")
async def refresh_act():
act_grid.options["rowData"] = await run.io_bound(fetch_activity)
act_grid.update()
ui.timer(4.0, refresh_act)
# ---- global header timers ----
async def refresh_header():
acct = await run.io_bound(fetch_account)
if acct:
eq_lbl.text = f"equity ${acct['equity']:,.0f}"
phase, detail, _ = _market_clock()
phase_lbl.text = f"{phase} {detail}"
hb_lbl.text = f"♥ {datetime.now().strftime('%H:%M:%S')}"
ui.timer(2.0, refresh_header)
def tick_clock():
clock_lbl.text = datetime.now().strftime("%H:%M:%S")
ui.timer(1.0, tick_clock)
if __name__ in {"__main__", "__mp_main__"}:
ui.run(host="0.0.0.0", port=int(os.environ.get("CONTROLROOM_PORT", "8766")),
title="Momentum Control Room", dark=True, show=False, reload=False,
favicon="🎯")
-259
View File
@@ -1,259 +0,0 @@
"""Module contracts and interfaces for the momentum trading system.
Every module must implement the ModuleContract protocol, defining:
- input_schema: What the module expects
- output_schema: What the module produces
- run(): Execute the module
- validate_input(): Ensure data meets contracts
"""
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime, date
from typing import Any, Protocol, TypeVar
T_Input = TypeVar("T_Input")
T_Output = TypeVar("T_Output")
@dataclass
class ModuleMetrics:
"""Standard metrics every module should track."""
module_name: str
timestamp: datetime
duration_ms: float
input_count: int = 0
output_count: int = 0
error_count: int = 0
warnings: list[str] = field(default_factory=list)
custom_metrics: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict:
return {
"module_name": self.module_name,
"timestamp": self.timestamp.isoformat(),
"duration_ms": self.duration_ms,
"input_count": self.input_count,
"output_count": self.output_count,
"error_count": self.error_count,
"warnings": self.warnings,
"custom_metrics": self.custom_metrics,
}
@dataclass
class ValidationFailure:
"""Single validation failure with context."""
field: str
value: Any
reason: str
severity: str = "error" # "error" or "warning"
@dataclass
class ModuleResult:
"""Base class for all module results."""
success: bool
timestamp: datetime
metrics: ModuleMetrics
validation_failures: list[ValidationFailure] = field(default_factory=list)
def has_errors(self) -> bool:
return bool(self.validation_failures or not self.success)
def error_summary(self) -> str:
"""Human-readable error summary."""
if not self.validation_failures:
return "No errors"
errors = [f for f in self.validation_failures if f.severity == "error"]
if not errors:
return f"{len(self.validation_failures)} warnings only"
return f"{len(errors)} error(s), {len([f for f in self.validation_failures if f.severity == 'warning'])} warning(s)"
class ModuleContract(Protocol):
"""Contract every module must fulfill."""
def run(self, input: Any) -> ModuleResult:
"""Execute the module.
Args:
input: Module-specific input conforming to input_schema
Returns:
Module-specific result inheriting from ModuleResult
"""
...
def validate_input(self, input: Any) -> tuple[bool, list[ValidationFailure]]:
"""Validate input before processing.
Args:
input: Data to validate
Returns:
(is_valid, failures)
"""
...
def get_metrics(self) -> ModuleMetrics:
"""Get last execution metrics."""
...
# ============================================================================
# INGESTION MODULE CONTRACTS
# ============================================================================
@dataclass
class BarIngestRequest:
"""Input contract for ingestion module."""
symbols: list[str]
session_date: date
lookback_minutes: int = 120 # how many minutes of history to fetch
source: str = "alpaca" # "alpaca", "test", "file"
@dataclass
class BarIngestResult(ModuleResult):
"""Output contract for ingestion module."""
minute_rows_inserted: int = 0
symbols_updated: list[str] = field(default_factory=list)
source_health: dict[str, str] = field(default_factory=dict) # {source: "healthy"|"error"}
# ============================================================================
# RESEARCH MODULE CONTRACTS
# ============================================================================
@dataclass
class ResearchRequest:
"""Input contract for research module."""
symbols: list[str]
session_date: date
lookback_days: int = 30 # for daily history
@dataclass
class ResearchResult(ModuleResult):
"""Output contract for research module."""
symbols_researched: list[str] = field(default_factory=list)
symbols_with_data: list[str] = field(default_factory=list)
symbols_missing_data: list[str] = field(default_factory=list)
total_bars_available: int = 0
# ============================================================================
# EVALUATION MODULE CONTRACTS
# ============================================================================
@dataclass
class CriterionDetail:
"""Result of evaluating one criterion."""
name: str
passed: bool
value: float # actual metric value
threshold: float | None = None # minimum to pass
reason: str = ""
confidence: float = 1.0 # 0-1, how confident in this result
@dataclass
class EvaluationRequest:
"""Input contract for evaluation module."""
symbol: str
bars_df: Any # pd.DataFrame with {timestamp, open, high, low, close, volume}
previous_close: float
avg_daily_volume: float
session_date: date
@dataclass
class EvaluationResult(ModuleResult):
"""Output contract for evaluation module."""
symbol: str = ""
status: str = "late" # "ready", "blocked", "late", "error"
score: float = 0.0 # 0-100
criteria: list[CriterionDetail] = field(default_factory=list)
entry_price: float | None = None
stop_loss: float | None = None
target_price: float | None = None
quality_score: float = 0.0 # 0-1
blocking_reason: str | None = None
metrics_dict: dict[str, float] = field(default_factory=dict) # gap%, rvol, etc.
# ============================================================================
# EXECUTION MODULE CONTRACTS
# ============================================================================
@dataclass
class ExecutionRequest:
"""Input contract for execution module."""
symbol: str
entry_price: float
stop_price: float
target_price: float
score: float
criteria_count: int
account_equity: float
current_positions: int
today_realized_pnl: float
@dataclass
class ExecutionResult(ModuleResult):
"""Output contract for execution module."""
symbol: str = ""
status: str = "" # "approved", "rejected", "pending_approval", "error"
reason: str = "" # why it was approved/rejected
order_id: str | None = None
shares: int | None = None
risk_dollars: float | None = None
risk_checks_applied: list[str] = field(default_factory=list)
# ============================================================================
# BROKER MODULE CONTRACTS
# ============================================================================
@dataclass
class BrokerRequest:
"""Input contract for broker module."""
symbol: str
order_id: str
entry_price: float
stop_price: float
target_price: float
shares: int
order_type: str = "limit" # "limit" or "market"
@dataclass
class BrokerResult(ModuleResult):
"""Output contract for broker module."""
symbol: str = ""
status: str = "" # "submitted", "rejected", "error"
broker_order_id: str | None = None
submitted_at: datetime | None = None
-274
View File
@@ -1,274 +0,0 @@
"""Telemetry collection and event emission for module observability."""
from __future__ import annotations
import time
import logging
from dataclasses import dataclass, field
from datetime import datetime
from typing import TYPE_CHECKING, Any, Callable
if TYPE_CHECKING:
from core.contracts import ModuleMetrics
logger = logging.getLogger(__name__)
@dataclass
class TelemetryCollector:
"""Collect metrics during module execution."""
module_name: str
start_time: float = field(default_factory=time.time)
input_count: int = 0
output_count: int = 0
error_count: int = 0
warnings: list[str] = field(default_factory=list)
custom_metrics: dict[str, Any] = field(default_factory=dict)
def record_input(self, count: int = 1) -> None:
"""Record input items processed."""
self.input_count += count
def record_output(self, count: int = 1) -> None:
"""Record output items produced."""
self.output_count += count
def record_error(self) -> None:
"""Record an error occurred."""
self.error_count += 1
def record_warning(self, message: str) -> None:
"""Record a warning."""
self.warnings.append(message)
def record_metric(self, name: str, value: float | int | str) -> None:
"""Record a custom metric."""
self.custom_metrics[name] = value
def finalize(self) -> ModuleMetrics:
"""Finalize and return metrics."""
duration_ms = (time.time() - self.start_time) * 1000
return ModuleMetrics(
module_name=self.module_name,
timestamp=datetime.now(),
duration_ms=duration_ms,
input_count=self.input_count,
output_count=self.output_count,
error_count=self.error_count,
warnings=self.warnings,
custom_metrics=self.custom_metrics,
)
class TelemetryEmitter:
"""Emit telemetry events to event store and observability systems."""
def __init__(self, event_store=None):
"""Initialize emitter.
Args:
event_store: Optional EventStore for persisting telemetry events
"""
self.event_store = event_store
self.callbacks: list[Callable[[dict], None]] = []
def add_callback(self, callback: Callable[[dict], None]) -> None:
"""Add a callback to receive telemetry events."""
self.callbacks.append(callback)
def emit_module_tick(
self,
module_name: str,
stage: str,
metrics: ModuleMetrics | None = None,
errors: list[dict] | None = None,
correlation_id: str | None = None,
) -> None:
"""Emit a module tick event.
Args:
module_name: Name of module (e.g., "ingestion", "evaluation")
stage: Stage name (e.g., "started", "processing", "completed")
metrics: Optional ModuleMetrics object
errors: Optional list of error details
correlation_id: Correlation ID linking to signal_ready event
"""
event = {
"type": "module_tick",
"timestamp": datetime.now().isoformat(),
"module": module_name,
"stage": stage,
"metrics": metrics.to_dict() if metrics else None,
"errors": errors or [],
"correlation_id": correlation_id,
}
self._emit(event)
def emit_data_validation(
self,
module_name: str,
validation_type: str,
valid: bool,
details: dict,
correlation_id: str | None = None,
) -> None:
"""Emit a data validation event.
Args:
module_name: Name of module performing validation
validation_type: Type of validation ("schema", "checksum", "business_rules")
valid: Whether validation passed
details: Validation details (errors, warnings, statistics)
correlation_id: Optional correlation ID
"""
event = {
"type": "data_validation",
"timestamp": datetime.now().isoformat(),
"module": module_name,
"validation_type": validation_type,
"valid": valid,
"details": details,
"correlation_id": correlation_id,
}
self._emit(event)
def emit_symbol_evaluation_detail(
self,
symbol: str,
stage: str,
metrics: dict[str, Any],
passed: bool,
blocking_reason: str | None = None,
correlation_id: str | None = None,
) -> None:
"""Emit detailed evaluation event for a symbol.
Args:
symbol: Symbol being evaluated
stage: Evaluation stage (e.g., "bars_loaded", "gap_checked", "rvol_calculated")
metrics: Metrics at this stage
passed: Whether this stage passed
blocking_reason: If not passed, why
correlation_id: Optional correlation ID
"""
event = {
"type": "symbol_evaluation_detail",
"timestamp": datetime.now().isoformat(),
"symbol": symbol,
"stage": stage,
"metrics": metrics,
"passed": passed,
"blocking_reason": blocking_reason,
"correlation_id": correlation_id,
}
self._emit(event)
def _emit(self, event: dict) -> None:
"""Emit event to all registered callbacks and event store."""
logger.debug(f"Telemetry: {event['type']} - {event.get('module', event.get('symbol', 'n/a'))}")
# Call all registered callbacks
for callback in self.callbacks:
try:
callback(event)
except Exception as e:
logger.error(f"Telemetry callback failed: {e}")
# Emit to event store if available
if self.event_store:
try:
# Import here to avoid circular dependency
from storage.event_schema import ModuleTickEvent, DataValidationEvent
if event["type"] == "module_tick":
self.event_store.emit(
ModuleTickEvent(
timestamp=datetime.now(),
module=event["module"],
stage=event["stage"],
duration_ms=event["metrics"]["duration_ms"]
if event["metrics"]
else 0,
input_count=event["metrics"]["input_count"]
if event["metrics"]
else 0,
output_count=event["metrics"]["output_count"]
if event["metrics"]
else 0,
metrics=event["metrics"]["custom_metrics"]
if event["metrics"]
else {},
errors=event["errors"],
correlation_id=event.get("correlation_id"),
)
)
elif event["type"] == "data_validation":
self.event_store.emit(
DataValidationEvent(
timestamp=datetime.now(),
module=event["module"],
validation_type=event["validation_type"],
valid=event["valid"],
details=event["details"],
correlation_id=event.get("correlation_id"),
)
)
except Exception as e:
logger.error(f"Failed to emit telemetry event to store: {e}")
# Global instance (can be overridden in tests)
_telemetry_emitter: TelemetryEmitter | None = None
def set_telemetry_emitter(emitter: TelemetryEmitter) -> None:
"""Set the global telemetry emitter."""
global _telemetry_emitter
_telemetry_emitter = emitter
def get_telemetry_emitter() -> TelemetryEmitter:
"""Get the global telemetry emitter."""
global _telemetry_emitter
if _telemetry_emitter is None:
_telemetry_emitter = TelemetryEmitter()
return _telemetry_emitter
def emit_module_tick(
module_name: str,
stage: str,
metrics: ModuleMetrics | None = None,
errors: list[dict] | None = None,
correlation_id: str | None = None,
) -> None:
"""Convenience function to emit module tick."""
get_telemetry_emitter().emit_module_tick(module_name, stage, metrics, errors, correlation_id)
def emit_data_validation(
module_name: str,
validation_type: str,
valid: bool,
details: dict,
correlation_id: str | None = None,
) -> None:
"""Convenience function to emit data validation."""
get_telemetry_emitter().emit_data_validation(
module_name, validation_type, valid, details, correlation_id
)
def emit_symbol_evaluation_detail(
symbol: str,
stage: str,
metrics: dict[str, Any],
passed: bool,
blocking_reason: str | None = None,
correlation_id: str | None = None,
) -> None:
"""Convenience function to emit symbol evaluation detail."""
get_telemetry_emitter().emit_symbol_evaluation_detail(
symbol, stage, metrics, passed, blocking_reason, correlation_id
)
-284
View File
@@ -1,284 +0,0 @@
"""Data validation utilities for module boundaries."""
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Callable
if TYPE_CHECKING:
from core.contracts import ValidationFailure
@dataclass
class SchemaValidator:
"""Validate data against a schema."""
required_fields: dict[str, type] # {field_name: expected_type}
optional_fields: dict[str, type] | None = None
def __post_init__(self):
if self.optional_fields is None:
self.optional_fields = {}
def validate(self, data: dict) -> tuple[bool, list[dict]]:
"""Validate data against schema.
Args:
data: Dictionary to validate
Returns:
(is_valid, list of failure dicts)
"""
failures = []
# Check required fields
for field_name, expected_type in self.required_fields.items():
if field_name not in data:
failures.append(
{
"field": field_name,
"value": None,
"reason": "Required field missing",
"severity": "error",
}
)
elif not isinstance(data[field_name], expected_type):
failures.append(
{
"field": field_name,
"value": data[field_name],
"reason": f"Expected {expected_type.__name__}, got {type(data[field_name]).__name__}",
"severity": "error",
}
)
# Check optional fields if present
if self.optional_fields:
for field_name, expected_type in self.optional_fields.items():
if field_name in data and not isinstance(data[field_name], expected_type):
failures.append(
{
"field": field_name,
"value": data[field_name],
"reason": f"Expected {expected_type.__name__}, got {type(data[field_name]).__name__}",
"severity": "warning",
}
)
return len([f for f in failures if f.severity == "error"]) == 0, failures
class RangeValidator:
"""Validate numeric values are within expected ranges."""
def __init__(self, min_val: float | None = None, max_val: float | None = None):
self.min_val = min_val
self.max_val = max_val
def validate(self, value: float, field_name: str = "value") -> tuple[bool, dict | None]:
"""Validate value is in range.
Args:
value: Numeric value to validate
field_name: Name of field for error message
Returns:
(is_valid, failure dict or None)
"""
if self.min_val is not None and value < self.min_val:
return False, {
"field": field_name,
"value": value,
"reason": f"Value {value} below minimum {self.min_val}",
"severity": "error",
}
if self.max_val is not None and value > self.max_val:
return False, {
"field": field_name,
"value": value,
"reason": f"Value {value} above maximum {self.max_val}",
"severity": "error",
}
return True, None
class ChecksumValidator:
"""Validate data integrity using checksums."""
@staticmethod
def compute_checksum(data: dict | list) -> str:
"""Compute simple checksum for data.
Args:
data: Dictionary or list to checksum
Returns:
Hex checksum string
"""
import hashlib
import json
data_str = json.dumps(data, sort_keys=True, default=str)
return hashlib.md5(data_str.encode()).hexdigest()
@staticmethod
def validate_checksum(
data: dict | list, expected_checksum: str
) -> tuple[bool, dict | None]:
"""Validate data matches checksum.
Args:
data: Data to validate
expected_checksum: Expected checksum value
Returns:
(is_valid, failure dict or None)
"""
actual_checksum = ChecksumValidator.compute_checksum(data)
if actual_checksum != expected_checksum:
return False, {
"field": "checksum",
"value": actual_checksum,
"reason": f"Checksum mismatch: expected {expected_checksum}, got {actual_checksum}",
"severity": "error",
}
return True, None
class BusinessRuleValidator:
"""Validate data against business logic rules."""
def __init__(self):
self.rules: dict[str, Callable[[Any], tuple[bool, str]]] = {}
def add_rule(self, rule_name: str, rule_fn: Callable[[Any], tuple[bool, str]]) -> None:
"""Add a business rule.
Args:
rule_name: Name of the rule
rule_fn: Function that takes data and returns (is_valid, reason)
"""
self.rules[rule_name] = rule_fn
def validate(self, data: Any) -> tuple[bool, list[dict]]:
"""Apply all business rules.
Args:
data: Data to validate
Returns:
(is_valid, list of failure dicts)
"""
failures = []
for rule_name, rule_fn in self.rules.items():
try:
is_valid, reason = rule_fn(data)
if not is_valid:
failures.append(
{
"field": "business_rule",
"value": None,
"reason": f"{rule_name}: {reason}",
"severity": "error",
}
)
except Exception as e:
failures.append(
{
"field": "business_rule",
"value": None,
"reason": f"{rule_name} raised exception: {e}",
"severity": "error",
}
)
return len(failures) == 0, failures
# Concrete validators for common cases
class BarDataValidator:
"""Validate OHLCV bar data."""
@staticmethod
def validate_bar(bar: dict) -> tuple[bool, list[dict]]:
"""Validate a single bar.
Args:
bar: Bar dictionary with {timestamp, open, high, low, close, volume}
Returns:
(is_valid, list of failure dicts)
"""
failures = []
# Check required fields
required = {"timestamp", "open", "high", "low", "close", "volume"}
for field in required:
if field not in bar:
failures.append(
{
"field": field,
"value": None,
"reason": "Missing required field",
"severity": "error",
}
)
if failures:
return False, failures
# Check value consistency
open_val = bar["open"]
high_val = bar["high"]
low_val = bar["low"]
close_val = bar["close"]
volume_val = bar["volume"]
# High must be >= all others
if high_val < open_val or high_val < close_val or high_val < low_val:
failures.append(
{
"field": "high",
"value": high_val,
"reason": f"High ({high_val}) must be >= open ({open_val}), close ({close_val}), low ({low_val})",
"severity": "error",
}
)
# Low must be <= all others
if low_val > open_val or low_val > close_val or low_val > high_val:
failures.append(
{
"field": "low",
"value": low_val,
"reason": f"Low ({low_val}) must be <= open ({open_val}), close ({close_val}), high ({high_val})",
"severity": "error",
}
)
# Volume should be non-negative
if volume_val < 0:
failures.append(
{
"field": "volume",
"value": volume_val,
"reason": "Volume cannot be negative",
"severity": "error",
}
)
# Warn on zero volume
if volume_val == 0:
failures.append(
{
"field": "volume",
"value": volume_val,
"reason": "Zero volume bar",
"severity": "warning",
}
)
return len([f for f in failures if f.severity == "error"]) == 0, failures
@@ -1,17 +0,0 @@
[Unit]
Description=Momentum Trading Control Room (NiceGUI live-analysis dashboard, :8766)
StartLimitIntervalSec=0
[Service]
Type=simple
WorkingDirectory=/mnt/c/code/momentum
Environment=PYTHONPATH=/mnt/c/code/momentum
ExecStart=/home/philip/.venvs/momentum/bin/python -u controlroom/app.py
Restart=always
RestartSec=10
StandardOutput=append:/mnt/c/code/momentum/data/controlroom.log
StandardError=append:/mnt/c/code/momentum/data/controlroom.log
SyslogIdentifier=momentum-controlroom
[Install]
WantedBy=default.target
File renamed without changes.
File renamed without changes.
File renamed without changes.
View File
Whitespace-only changes.
-26
View File
@@ -1,26 +0,0 @@
"""Models for Milestone 2.
Event and data models for the event-first architecture.
"""
from pydantic import BaseModel, Field
class EventMode(str):
"""Runtime modes for events."""
REPLAY = "replay"
PAPER = "paper"
LIVE = "live"
class BaseEvent(BaseModel):
"""Base event with common fields."""
timestamp: str = Field(description="Event timestamp (ISO format)")
mode: EventMode = Field(description="Runtime mode (replay/paper/live)")
event_type: str = Field(description="Event type identifier")
correlation_id: str | None = Field(
default=None, description="Correlation ID for causal chain"
)
message: str = Field(description="Human-readable message")
-18
View File
@@ -1,18 +0,0 @@
{
"name": "momentum",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"claude": "^0.1.1"
}
},
"node_modules/claude": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/claude/-/claude-0.1.1.tgz",
"integrity": "sha512-j7oSibqQdIODNhkI1sEJzHMiPsF43L/GqNbcA+eDDyGM10+x2sH9NW/PK6vM3z0J2tLDKMBcc5ZjVaoRinhuCA==",
"license": "ISC"
}
}
}
-5
View File
@@ -1,5 +0,0 @@
{
"dependencies": {
"claude": "^0.1.1"
}
}
+15 -7
View File
@@ -411,13 +411,21 @@ def _session_open(con, symbol: str, session_date) -> float | None:
"AND is_regular_hours ORDER BY timestamp LIMIT 1", [symbol, session_date]
).fetchall()
if r:
# trust it ONLY if it is really the opening bar (<= 09:33 ET = 13:33Z):
# mid-day-discovered names briefly have bars starting mid-session, and
# treating that first bar as "the open" is how TVRD/FRTT armed as fake
# 20% gappers on 2026-07-08.
ts = str(r[0][1])
hhmm = ts[11:16]
if "13:30" <= hhmm <= "13:33":
# trust it ONLY if it is really the 09:30 ET opening bar: mid-day-
# discovered names briefly have bars starting mid-session, and treating
# that first bar as "the open" is how TVRD/FRTT armed as fake 20%
# gappers on 2026-07-08. minute_bars.timestamp is UTC-naive, so the
# open is 13:30Z in summer (EDT) but 14:30Z in winter (EST) — convert
# to ET instead of a fixed UTC window, else the whole fast path arms
# nothing all winter.
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
raw = r[0][1]
ts = raw if isinstance(raw, datetime) else datetime.fromisoformat(str(raw))
if ts.tzinfo is None:
ts = ts.replace(tzinfo=timezone.utc)
et_hhmm = ts.astimezone(ZoneInfo("America/New_York")).strftime("%H:%M")
if "09:30" <= et_hhmm <= "09:33":
_SESSION_OPEN_CACHE[key] = float(r[0][0])
return _SESSION_OPEN_CACHE[key]
except Exception: # noqa: BLE001 — fail-open; gap falls back to window/day_open
-10
View File
@@ -1,10 +0,0 @@
"""Market-data provider registry."""
from runtime.providers.registry import (
ProviderKind,
ProviderConfig,
ProviderRegistry,
default_registry,
)
__all__ = ["ProviderKind", "ProviderConfig", "ProviderRegistry", "default_registry"]
-112
View File
@@ -1,112 +0,0 @@
"""Provider registry for market-data sources.
(Repaired: ProviderConfig is a proper top-level dataclass and the registry
initializes its mapping correctly — the original nested the dataclass inside
the enum and used an invalid `self.providers: {}` annotation-as-assignment.)
"""
from __future__ import annotations
import os
from dataclasses import dataclass, field
from enum import Enum
class ProviderKind(str, Enum):
"""Supported market-data providers."""
ALPACA = "alpaca"
SCHWAB = "schwab"
POLYGON = "polygon"
FINNHUB = "finnhub"
SYNTHETIC = "synthetic"
@dataclass
class ProviderConfig:
"""Configuration for one provider."""
kind: ProviderKind
enabled: bool = True
priority: int = 100 # lower = preferred
rate_limit_per_minute: int = 200
supports_minute_bars: bool = True
supports_quotes: bool = True
env_keys: tuple[str, ...] = field(default_factory=tuple)
def is_configured(self, env: dict[str, str] | None = None) -> bool:
"""A provider is configured when all of its env keys are present."""
values = dict(os.environ)
if env is not None:
values.update(env)
return all(values.get(key) for key in self.env_keys)
class ProviderRegistry:
"""Registry of provider configs with priority-based selection."""
def __init__(self) -> None:
self.providers: dict[ProviderKind, ProviderConfig] = {}
def register(self, config: ProviderConfig) -> None:
self.providers[config.kind] = config
def get(self, kind: ProviderKind | str) -> ProviderConfig | None:
if isinstance(kind, str):
try:
kind = ProviderKind(kind)
except ValueError:
return None
return self.providers.get(kind)
def enabled_providers(self) -> list[ProviderConfig]:
return sorted(
(p for p in self.providers.values() if p.enabled),
key=lambda p: p.priority,
)
def best_configured(
self, env: dict[str, str] | None = None
) -> ProviderConfig | None:
for provider in self.enabled_providers():
if provider.is_configured(env):
return provider
return None
def default_registry() -> ProviderRegistry:
"""Registry with the standard provider stack."""
registry = ProviderRegistry()
registry.register(
ProviderConfig(
kind=ProviderKind.ALPACA,
priority=10,
env_keys=("ALPACA_API_KEY", "ALPACA_SECRET_KEY"),
)
)
registry.register(
ProviderConfig(
kind=ProviderKind.SCHWAB,
priority=20,
env_keys=("SCHWAB_MARKET_DATA_APP_KEY", "SCHWAB_MARKET_DATA_APP_SECRET"),
)
)
registry.register(
ProviderConfig(
kind=ProviderKind.POLYGON, priority=30, env_keys=("POLYGON_API_KEY",)
)
)
registry.register(
ProviderConfig(
kind=ProviderKind.FINNHUB, priority=40, env_keys=("FINNHUB_API_KEY",)
)
)
registry.register(
ProviderConfig(
kind=ProviderKind.SYNTHETIC,
priority=1000,
env_keys=(),
supports_quotes=False,
)
)
return registry
-148
View File
@@ -1,148 +0,0 @@
import json
"""Replay/rebuild tooling for Milestone 2.
Reconstruct state from event store for analysis and testing.
"""
import logging
from typing import TYPE_CHECKING
from .event_store import EventStore
if TYPE_CHECKING:
pass
logger = logging.getLogger(__name__)
class StateRebuilder:
"""Rebuild trading state from event store."""
def __init__(self, store: EventStore):
self.store = store
def rebuild_session(self, session_id: str) -> dict:
"""Rebuild complete state for a session from events."""
logger.info(f"Rebuilding session {session_id}")
events = self.store.query_events(session_id=session_id)
state = {
"session_id": session_id,
"symbols": {},
"orders": {},
"positions": {},
"events": [],
}
for event in events:
event_type = event["event_type"]
payload = json.loads(event.get("payload_json", "{}"))
symbol = payload.get("symbol")
self._process_event(state, event_type, symbol, payload, event)
logger.info(f"Rebuilt state for {len(events)} events")
return state
def _process_event(
self,
state: dict,
event_type: str,
symbol: str | None,
payload: dict,
event: dict,
):
"""Process individual event and update state."""
if event_type == "symbol_discovered":
state["symbols"][symbol] = {
"state": "discovered",
"discovered_at": event["timestamp"],
}
elif event_type == "symbol_state_changed":
if symbol not in state["symbols"]:
state["symbols"][symbol] = {
"state": payload.get("previous_state") or "discovered",
"discovered_at": event["timestamp"],
}
state["symbols"][symbol]["state"] = payload["new_state"]
state["symbols"][symbol]["state_history"] = state["symbols"][symbol].get(
"state_history", []
)
state["symbols"][symbol]["state_history"].append(
{
"state": payload["new_state"],
"timestamp": event["timestamp"],
"reason": payload.get("state_reason"),
}
)
elif event_type == "signal_ready":
if symbol in state["symbols"]:
state["symbols"][symbol]["signals"] = state["symbols"][symbol].get(
"signals", []
)
state["symbols"][symbol]["signals"].append(
{
"signal_type": payload["signal_type"],
"confidence": payload["confidence"],
"timestamp": event["timestamp"],
}
)
elif event_type == "position_opened":
position_id = payload["position_id"]
state["positions"][position_id] = {
"symbol": symbol,
"status": "open",
"entry_price": payload["entry_price"],
"quantity": payload["quantity"],
"stop_loss_price": payload["stop_loss_price"],
"opened_at": event["timestamp"],
}
elif event_type == "position_closed":
position_id = payload["position_id"]
if position_id in state["positions"]:
state["positions"][position_id].update(
{
"status": "closed",
"exit_price": payload["exit_price"],
"exit_reason": payload["exit_reason"],
"realized_pnl": payload["realized_pnl"],
"closed_at": event["timestamp"],
}
)
elif event_type == "order_filled":
state["events"].append(
{
"type": event_type,
"order_id": payload["order_id"],
"symbol": symbol,
"timestamp": event["timestamp"],
}
)
elif event_type == "order_submitted":
order_id = payload["order_id"]
state["orders"][order_id] = {
"symbol": symbol,
"status": "submitted",
"side": payload.get("side"),
"quantity": payload.get("quantity"),
"price": payload.get("price"),
"updated_at": event["timestamp"],
}
elif event_type == "order_cancelled":
order_id = payload["order_id"]
state["orders"][order_id] = {
**state["orders"].get(order_id, {}),
"symbol": symbol,
"status": "cancelled",
"cancel_reason": payload.get("cancel_reason"),
"updated_at": event["timestamp"],
}
-36
View File
@@ -1,36 +0,0 @@
"""Optional LLM (Ollama) enrichment hooks. Disabled unless configured."""
from __future__ import annotations
import json
import logging
import urllib.request
logger = logging.getLogger(__name__)
def summarize_setup_with_llm(
setup: dict,
host: str = "http://localhost:11434",
model: str = "mistral",
timeout: int = 15,
) -> str | None:
"""Ask a local Ollama model to summarize a setup. Returns None on failure."""
prompt = (
"Summarize this momentum trading setup in one sentence:\n"
+ json.dumps(setup, default=str)
)
try:
req = urllib.request.Request(
f"{host}/api/generate",
data=json.dumps(
{"model": model, "prompt": prompt, "stream": False}
).encode(),
headers={"Content-Type": "application/json"},
)
with urllib.request.urlopen(req, timeout=timeout) as resp:
body = json.loads(resp.read().decode())
return body.get("response")
except Exception:
logger.debug("LLM enrichment unavailable", exc_info=True)
return None
-33
View File
@@ -1,33 +0,0 @@
"""Persist setup evaluations into the research DB (setup_events table)."""
from __future__ import annotations
import uuid
from datetime import datetime
def store_setup_event(con, symbol: str, session_date, result: dict) -> str:
"""Insert one evaluation result into setup_events. Returns setup_id."""
setup_id = str(uuid.uuid4())
setups = result.get("setups") or []
best = setups[0] if setups else {}
con.execute(
"""
INSERT INTO setup_events
(setup_id, symbol, setup_time, session_date, setup_name,
entry_reference_price, invalidation_price, gap_pct, relative_volume)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
[
setup_id,
symbol,
datetime.now(),
session_date,
best.get("setup_type") or "first_pullback",
best.get("entry_price"),
best.get("stop_loss_price"),
result.get("gap_pct"),
result.get("relative_volume"),
],
)
return setup_id
-32
View File
@@ -1,32 +0,0 @@
"""Lightweight in-memory tracking of evaluation history per symbol."""
from __future__ import annotations
from collections import defaultdict
class EvaluationTracker:
"""Tracks recent evaluation results per symbol for debounce/analysis."""
def __init__(self, max_history: int = 50):
self.max_history = max_history
self._history: dict[str, list[dict]] = defaultdict(list)
def record(self, symbol: str, result: dict) -> None:
history = self._history[symbol]
history.append(result)
if len(history) > self.max_history:
del history[: len(history) - self.max_history]
def last(self, symbol: str) -> dict | None:
history = self._history.get(symbol)
return history[-1] if history else None
def consecutive_status(self, symbol: str, status: str) -> int:
count = 0
for result in reversed(self._history.get(symbol, [])):
if result.get("status") == status:
count += 1
else:
break
return count
View File
Whitespace-only changes.
-102
View File
@@ -1,102 +0,0 @@
"""Telemetry event emission and querying for runtime observability."""
from __future__ import annotations
import json
import logging
import uuid
from enum import Enum
logger = logging.getLogger(__name__)
class TelemetryEventType(str, Enum):
SOURCE_DEGRADED = "SOURCE_DEGRADED"
SOURCE_RECOVERED = "SOURCE_RECOVERED"
SOURCE_COOLDOWN = "SOURCE_COOLDOWN"
WATCHLIST_ADD = "WATCHLIST_ADD"
WATCHLIST_DROP = "WATCHLIST_DROP"
CANDIDATE_STATE_CHANGE = "CANDIDATE_STATE_CHANGE"
FETCH_ATTEMPT = "FETCH_ATTEMPT"
ENRICHMENT_QUEUED = "ENRICHMENT_QUEUED"
ENRICHMENT_DONE = "ENRICHMENT_DONE"
ENRICHMENT_FAILED = "ENRICHMENT_FAILED"
def emit_event(
con,
event_type: TelemetryEventType | str,
session_id: str | None = None,
symbol: str | None = None,
source: str | None = None,
from_state: str | None = None,
to_state: str | None = None,
reason: str | None = None,
metadata: dict | None = None,
) -> None:
event_id = str(uuid.uuid4())
event_type_val = (
event_type.value
if isinstance(event_type, TelemetryEventType)
else str(event_type)
)
metadata_json = json.dumps(metadata) if metadata is not None else None
try:
con.execute(
"""
INSERT INTO telemetry_events
(id, event_type, session_id, symbol, source, from_state, to_state, reason, metadata_json)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
[
event_id,
event_type_val,
session_id,
symbol,
source,
from_state,
to_state,
reason,
metadata_json,
],
)
except Exception:
logger.exception(
"telemetry emit_event failed — event_type=%s symbol=%s",
event_type_val,
symbol,
)
def query_events(
con,
session_id: str | None = None,
symbol: str | None = None,
source: str | None = None,
limit: int = 100,
) -> list[dict]:
clauses: list[str] = []
params: list = []
if session_id is not None:
clauses.append("session_id = ?")
params.append(session_id)
if symbol is not None:
clauses.append("symbol = ?")
params.append(symbol)
if source is not None:
clauses.append("source = ?")
params.append(source)
where = ("WHERE " + " AND ".join(clauses)) if clauses else ""
sql = f"SELECT * FROM telemetry_events {where} ORDER BY created_at DESC LIMIT ?"
params.append(limit)
rows = con.execute(sql, params).fetchall()
cols = [
desc[0]
for desc in con.execute(
f"SELECT * FROM telemetry_events {where} LIMIT 0", params[:-1]
).description
]
return [dict(zip(cols, row)) for row in rows]
@@ -231,3 +231,31 @@ def test_reconcile_seeds_from_persisted_snapshot_after_restart(store):
rows = _closed(store)
assert len(rows) == 1 and rows[0]["symbol"] == "REST"
assert rows[0]["realized_pnl"] == 5.0 # (2.5 - 2.0) * 10
def test_post_not_retried_on_urlerror(monkeypatch):
"""Lost-response URLError on POST /orders must NOT re-send (duplicate live
order, 2026-07-06). GET/DELETE still retry."""
import urllib.error
import urllib.request
from alpaca_paper.client import AlpacaPaperClient
from alpaca_paper.settings import AlpacaPaperSettings
cli = AlpacaPaperClient.__new__(AlpacaPaperClient)
cli.timeout = 1
cli.max_retries = 2
cli._headers = lambda: {}
calls = {"n": 0}
def boom(req, timeout=None):
calls["n"] += 1
raise urllib.error.URLError("connection reset")
monkeypatch.setattr(urllib.request, "urlopen", boom)
import pytest
with pytest.raises(urllib.error.URLError):
cli._request("POST", "https://x/v2/orders", body={"symbol": "AAA"})
assert calls["n"] == 1, "POST must not retry a lost-response URLError"
calls["n"] = 0
with pytest.raises(urllib.error.URLError):
cli._request("GET", "https://x/v2/account")
assert calls["n"] > 1, "idempotent GET should still retry"
File renamed without changes.
+56
View File
@@ -875,3 +875,59 @@ def test_session_gate_bars_after_hours_entries():
assert svc._outside_session(datetime(2026, 7, 9, 9, 30, tzinfo=ET)) is None # the open
svc.settings = ExecutionSettings(session_only=False)
assert svc._outside_session(datetime(2026, 7, 9, 20, 9, tzinfo=ET)) is None # opt-out works
def test_flatten_all_closes_broker_position_missed_by_snapshot(store):
"""Naked-through-breaker fix: a position that filled in the sync gap isn't in
the event-store held set, so _flatten_all's step-2 skips it. Step 3 (moved out
of close_session so the daily-loss breaker gets it too) closes it from fresh
broker positions."""
from alpaca_paper.execution import AlpacaPaperExecutor
class _Cli:
def __init__(self):
self.closed = []
self.pos = [{"symbol": "GAP", "qty": "100"}] # filled, snapshot missed it
def get_positions(self, fresh=False):
return list(self.pos)
def close_position(self, symbol, qty=None, percentage=None):
self.closed.append(symbol)
self.pos = [p for p in self.pos if p["symbol"] != symbol]
return {"id": "c"}
def get_orders(self, **kw):
return []
def cancel_order(self, oid):
pass
cli = _Cli()
svc = TradingExecutionService(
store, executor=AlpacaPaperExecutor(store, client=cli),
settings=ExecutionSettings(auto_approve=True), session_id="t")
result = svc._flatten_all("daily_loss_circuit_breaker")
assert "GAP" in cli.closed, "breaker flatten must close broker positions the snapshot missed"
assert "GAP" in result["closed_positions"]
def test_daily_state_rebuilds_from_events_on_restart(store):
"""Mid-session restart must not reset the entry cap to 0 or forget resting
orders (re-fire => duplicate order, 2026-07-06). A new service instance
rebuilds fresh-fill count, requested symbols, and losing benches from today."""
from storage.event_schema import OrderSubmittedEvent, OrderFilledEvent, PositionClosedEvent
now = datetime.now()
store.emit(OrderSubmittedEvent(timestamp=now, mode=EventMode.PAPER, correlation_id="t",
message="sub", order_id="o1", symbol="AAA", side="buy", quantity=10, price=5.0,
broker_order_id="b1"))
store.emit(OrderFilledEvent(timestamp=now, mode=EventMode.PAPER, correlation_id="t",
message="fill", order_id="o1", symbol="AAA", fill_price=5.0, fill_quantity=10,
broker_order_id="b1"))
store.emit(PositionClosedEvent(timestamp=now, mode=EventMode.PAPER, correlation_id="t",
message="closed", position_id="p1", symbol="AAA", side="buy", quantity=10,
entry_price=5.0, exit_price=4.5, realized_pnl=-5.0, exit_reason="stop"))
svc = make_service(store) # boot AFTER the events land
assert svc._fresh_entries["n"] == 1, "cap count must survive restart"
assert "AAA" in svc._requested_symbols, "resting-order dedup must survive restart"
assert "AAA" in svc._exited_today, "losing bench must survive restart"
+80 -17
View File
@@ -403,6 +403,59 @@ class TradingExecutionService:
)
# serializes state mutations across the main loop and the trigger thread
self._lock = threading.RLock()
# rebuild the memory-only daily state from today's events so a mid-session
# restart doesn't reset the entry cap to 0, un-bench stopped-out names, or
# forget which symbols already have a resting order (re-fire => duplicate
# order, the 2026-07-06 incident). Best-effort; a fresh DB is fine.
self._rebuild_daily_state()
def _rebuild_daily_state(self) -> None:
"""Restore per-day counters/benches from today's event stream on boot.
ponytail: rebuilds the SLOW-path dedup + cap + bench only. The fast
trigger book (run_live_paper) is separately memory-only; the standing
broker-open-orders diff after a restart is still the backstop for a
resting fast-path order (see momentum-live-loop-architecture memory).
"""
try:
import json as _json
today = self._now().date()
since = today.isoformat()
def _payload(e):
raw = e.get("payload") or e.get("payload_json")
if isinstance(raw, str):
try:
return _json.loads(raw)
except ValueError:
return {}
return raw or {}
# order_filled carries no side; map fills back to their BUY submit so
# sell/exit fills don't inflate the fresh-entry cap
buy_order_ids = set()
for e in self.store.query_events(event_type="order_submitted", since=since, limit=500):
p = _payload(e)
sym, oid = p.get("symbol"), p.get("order_id")
if sym:
self._requested_symbols.add(str(sym))
if oid and str(p.get("side", "buy")).lower() == "buy":
buy_order_ids.add(str(oid))
n_fresh = sum(
1 for e in self.store.query_events(event_type="order_filled", since=since, limit=500)
if str(_payload(e).get("order_id")) in buy_order_ids)
self._fresh_entries = {"date": today, "n": n_fresh}
for e in self.store.query_events(event_type="position_closed", since=since, limit=500):
p = _payload(e)
sym = p.get("symbol")
# bench only names that closed at a LOSS (mirror the live bench rule)
if sym and float(p.get("realized_pnl") or 0) < 0:
self._exited_today.add(str(sym))
if n_fresh or self._requested_symbols or self._exited_today:
logger.info("restored daily state: %d fresh fills, %d requested, %d benched",
n_fresh, len(self._requested_symbols), len(self._exited_today))
except Exception as exc: # noqa: BLE001 — a rebuild failure must not block boot
logger.warning("daily-state rebuild skipped: %s", exc)
# -- pipeline -----------------------------------------------------------
@@ -499,10 +552,18 @@ class TradingExecutionService:
block the rest. Resting bracket legs are cancelled before each close so
the held quantity is freed (else close_position 403s)."""
result: dict = {"cancelled_entries": 0, "closed_positions": [], "errors": []}
client = getattr(self.executor, "client", None)
# broker truth up front: an entry that FILLED in the sync gap is still in
# _armed; cancelling its bracket parent would strip the stop/TP legs and
# leave the position naked. Never cancel a name the broker holds — flatten
# it in step 2 instead. None => positions unreadable, stay conservative.
broker_held = self._broker_held_symbols() or set()
# 1) cancel unfilled (armed) entries we are tracking
for order_id in list(self._armed):
armed = self._armed.pop(order_id)
symbol = armed.get("symbol", "")
if symbol in broker_held:
continue # filled — leave the bracket intact; step 2/3 closes it
try:
self.executor.cancel_entry(
order_id, armed.get("broker_order_id"), symbol, reason
@@ -511,8 +572,7 @@ class TradingExecutionService:
except Exception as exc: # noqa: BLE001
result["errors"].append(f"cancel {symbol}: {exc}")
self._requested_symbols.discard(symbol)
# 2) market-close open positions
client = getattr(self.executor, "client", None)
# 2) market-close open positions from the store snapshot
for symbol in sorted(self._held_symbols()):
if client is None or not hasattr(client, "close_position"):
result["errors"].append(f"close {symbol}: no broker client to flatten")
@@ -522,6 +582,22 @@ class TradingExecutionService:
result["closed_positions"].append(symbol) # only on a confirmed close
except Exception as exc: # noqa: BLE001
result["errors"].append(f"close {symbol}: {exc}")
# 3) belt-and-suspenders: close any BROKER position the store snapshot
# missed (filled within the ~60s sync lag). Was in close_session only, so
# the daily-loss breaker flatten skipped it — moved here so EVERY caller
# (breaker + EOD) gets it. This is the naked-through-breaker fix.
if client is not None and hasattr(client, "get_positions"):
try:
for p in client.get_positions(fresh=True) or []:
sym = p.get("symbol")
if sym and sym not in result["closed_positions"]:
try:
self._release_and_close(client, sym)
result["closed_positions"].append(sym)
except Exception as exc: # noqa: BLE001
result["errors"].append(f"close {sym}: {exc}")
except Exception as exc: # noqa: BLE001
result["errors"].append(f"positions: {exc}")
return result
@_locked
@@ -1396,21 +1472,7 @@ class TradingExecutionService:
halt new entries for the rest of the session.
"""
self._session_closed = True
result = self._flatten_all(reason)
# belt-and-suspenders: close any broker position the store snapshot missed
client = getattr(self.executor, "client", None)
if client is not None and hasattr(client, "get_positions"):
try:
for p in client.get_positions():
sym = p.get("symbol")
if sym and sym not in result["closed_positions"]:
try:
self._release_and_close(client, sym)
result["closed_positions"].append(sym)
except Exception as exc: # noqa: BLE001
result["errors"].append(f"close {sym}: {exc}")
except Exception as exc: # noqa: BLE001
result["errors"].append(f"positions: {exc}")
result = self._flatten_all(reason) # fresh-broker belt now lives inside
self.store.emit(
RiskRuleTriggeredEvent(
timestamp=datetime.now(), mode=self.mode, correlation_id=self.session_id,
@@ -1576,6 +1638,7 @@ class TradingExecutionService:
and self._fresh_entries["n"] > 0):
self._fresh_entries["n"] -= 1
@_locked
def expire_stale_entries(self) -> list[dict]:
"""Back out of unfilled entries that timed out or broke their trigger.