Files
momentum-system/backfill_journal.py
momentum devandClaude Fable 5 8c62804808 feat: broker-aware factory for reconcile/journal/health tools
eod_reconcile, backfill_journal and the admin health block were hardwired
to AlpacaPaperClient — on a BROKER=tradier stack they would silently
reconcile/report against the wrong broker. make_trading_client() honors
BROKER env with no silent cross-broker fallback (missing Tradier keys =>
skip with reason, never fall back to Alpaca). Admin UI shows the active
broker label; JSON key stays 'alpaca' for compatibility.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 13:13:01 -05:00

120 lines
4.6 KiB
Python

#!/usr/bin/env python3
"""One-time backfill of the trade journal from Alpaca's filled-order history.
The live journal (position_closed events from the sync snapshot-diff) is
forward-only, so past market days show 0 trades. This reconstructs every
completed round-trip from order history and emits the matching position_closed
events, timestamped (ET) at the exit fill so each lands on its own market day —
populating per-day win-rate / avg-R / the trade list retroactively.
Idempotent: re-running deletes prior backfill events first. Live-reconciled
closes are left untouched (deduped by symbol + exit-minute).
python backfill_journal.py --dry-run # preview, write nothing
python backfill_journal.py # write the events
"""
from __future__ import annotations
import argparse
import json
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
from dotenv import load_dotenv
ET = ZoneInfo("America/New_York")
BACKFILL_TAG = "journal_backfill"
def _to_et_naive(iso: str):
"""Alpaca filled_at is UTC; store as naive ET to match the loop's clock and
land each close on the correct market day."""
try:
dt = datetime.fromisoformat(str(iso).replace("Z", "+00:00"))
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return dt.astimezone(ET).replace(tzinfo=None)
except (TypeError, ValueError):
return None
def main() -> None:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--dry-run", action="store_true", help="preview, write nothing")
args = ap.parse_args()
load_dotenv()
from storage.event_schema import EventMode, PositionClosedEvent
from storage.event_store import EventStore
from storage.journal_backfill import flatten_fills, reconstruct_round_trips
from tradier_broker.factory import make_trading_client
store = EventStore("momentum")
client, label = make_trading_client()
if client is None:
raise SystemExit(f"[backfill_journal] {label} — aborting")
raw = client.get_orders(status="closed", limit=500, nested=True)
print(f"fetched {len(raw)} historical orders from {label}")
trips = reconstruct_round_trips(flatten_fills(raw))
print(f"reconstructed {len(trips)} completed round-trips")
# existing closes: collect prior-backfill ids (to clear) and live keys (to skip)
existing = store.query_events(event_type="position_closed", limit=None)
prior_backfill = [e["id"] for e in existing if e.get("correlation_id") == BACKFILL_TAG]
live_keys = set()
for e in existing:
if e.get("correlation_id") == BACKFILL_TAG:
continue
p = json.loads(e["payload_json"])
live_keys.add((p.get("symbol"), str(e["timestamp"])[:16]))
pending, by_day, skipped = [], {}, 0
for t in trips:
et = _to_et_naive(t["exit_time"])
if et is None:
continue
if (t["symbol"], et.isoformat()[:16]) in live_keys:
skipped += 1
continue
pending.append((t, et))
d = by_day.setdefault(et.date().isoformat(), {"n": 0, "pnl": 0.0, "w": 0})
d["n"] += 1
d["pnl"] += t["realized_pnl"]
d["w"] += 1 if t["realized_pnl"] > 0 else 0
print("\nper market day:")
for day in sorted(by_day):
d = by_day[day]
wr = (d["w"] / d["n"] * 100) if d["n"] else 0
print(f" {day}: {d['n']:>3} trades win {wr:>3.0f}% pnl {d['pnl']:+.2f}")
if skipped:
print(f"(skipped {skipped} already journaled live)")
if args.dry_run:
print(f"\n[dry-run] would write {len(pending)} events (no changes made)")
return
if prior_backfill:
store.con.execute(
"DELETE FROM events WHERE event_type = 'position_closed' "
"AND correlation_id = ?", [BACKFILL_TAG])
print(f"cleared {len(prior_backfill)} prior backfill events")
for t, et in pending:
store.emit(PositionClosedEvent(
timestamp=et, mode=EventMode.PAPER, correlation_id=BACKFILL_TAG,
message=(f"[backfill] {t['symbol']} closed @ {t['exit_price']} "
f"({t['exit_reason']}) pnl={t['realized_pnl']:+.2f}"),
position_id=f"backfill-{t['symbol']}-{et.isoformat()}",
symbol=t["symbol"], exit_price=t["exit_price"], exit_reason=t["exit_reason"],
realized_pnl=t["realized_pnl"], entry_price=t["entry_price"],
stop_loss_price=t["stop_loss_price"], side=t["side"], quantity=t["qty"],
))
print(f"\nemitted {len(pending)} position_closed events across {len(by_day)} market days")
if __name__ == "__main__":
main()