#!/usr/bin/env python """Daily trade report: today's fills + realized P&L from the trade ledger. Usage: .venv/bin/python daily_report.py [YYYY-MM-DD] ./report.sh [YYYY-MM-DD] Data source: trades.jsonl (append-only ledger written by PositionTracker). P&L method: FIFO per (strategy, symbol). Current open lots shown at the end; if the IB Gateway is reachable, live market values are included (read-only). """ import json import os import sys from collections import defaultdict, deque from datetime import date from pathlib import Path LEDGER = Path(os.environ.get("TRADES_LEDGER", Path(__file__).resolve().parent / "trades.jsonl")) STRATEGY_SHORT = { "MAStockStrategy": "MAStock", "ShortTermMAVWAPStrategy": "ShortTerm", "MeanReversionStrategy": "MeanRev", "ForexMAStrategy": "Forex", } def short(name: str) -> str: return STRATEGY_SHORT.get(name, name[:12]) def load_and_replay(): """Replay the whole ledger; return (today fills, realized maps, open lots).""" lots = defaultdict(deque) # (strategy, symbol) -> deque([qty, price]) fills_today = [] # display rows for the requested day realized_sym = defaultdict(float) realized_strat = defaultdict(float) unknown_cost = 0.0 day = sys.argv[1] if len(sys.argv) > 1 else date.today().isoformat() for line in LEDGER.read_text().splitlines(): if not line.strip(): continue r = json.loads(line) key = (r["strategy"], r["symbol"]) ts_day, ts_time = r["ts"][:10], r["ts"][11:19] if r["type"] in ("seed", "buy"): lots[key].append([r["qty"], r["price"]]) if r["type"] == "buy" and ts_day == day: fills_today.append((ts_time, r["strategy"], r["symbol"], "BUY", r["qty"], r["price"], None)) elif r["type"] in ("sell", "sell_external"): pnl = 0.0 remain = r["qty"] dq = lots[key] while remain > 1e-9 and dq: take = min(dq[0][0], remain) pnl += (r["price"] - dq[0][1]) * take dq[0][0] -= take remain -= take if dq[0][0] <= 1e-9: dq.popleft() if remain > 1e-9: pnl = None # cost basis unknown (lot predates the ledger) if ts_day == day: action = "SELL*" if r.get("est") else "SELL" fills_today.append((ts_time, r["strategy"], r["symbol"], action, r["qty"], r["price"], pnl)) if pnl is not None: realized_sym[r["symbol"]] += pnl realized_strat[r["strategy"]] += pnl else: unknown_cost += r["qty"] return day, fills_today, realized_sym, realized_strat, lots, unknown_cost def live_positions(): """Read-only: current positions + market values from IB. None if unreachable.""" try: import asyncio from ib_insync import IB from config import config as cfg async def _fetch(): ib = IB() await ib.connectAsync(cfg.ib.host, cfg.ib.port, clientId=77, timeout=10) data = { item.contract.symbol: (item.position, item.marketPrice, item.unrealizedPNL) for item in ib.portfolio() if item.contract.secType == "STK" and item.position > 0 } ib.disconnect() return data return asyncio.run(_fetch()) except Exception: return None def main(): if not LEDGER.exists(): print("还没有交易记录(trades.jsonl 不存在)") return day, fills, realized_sym, realized_strat, lots, unknown_cost = load_and_replay() print(f"========== {day} 成交明细 ==========") if not fills: print(" 当天无成交") for ts, strat, sym, action, qty, price, pnl in fills: line = f"{ts} {short(strat):10s} {sym:6s} {action:4s} x{qty:g} @ {price:>10.2f}" if pnl is not None: line += f" 盈亏 {pnl:+.2f}" elif action == "SELL": line += " 盈亏 ? (成本未知)" print(line) print(f"\n========== {day} 已实现盈亏 ==========") if realized_sym: print(" 按标的:") for sym, v in sorted(realized_sym.items()): print(f" {sym:6s} {v:+10.2f}") if realized_strat: print(" 按策略:") for strat, v in sorted(realized_strat.items()): print(f" {short(strat):10s} {v:+10.2f}") total = sum(realized_sym.values()) print(f" 合计: {total:+.2f}") if unknown_cost: print(f" (另有 {unknown_cost:g} 股卖出成本未知,未计入)") open_lots = [(s, sym, q, p) for (s, sym), dq in lots.items() for q, p in dq if q > 1e-9] print("\n========== 当前持仓(成本价) ==========") if not open_lots: print(" 无") else: live = live_positions() for strat, sym, qty, price in sorted(open_lots, key=lambda x: (x[1], x[0])): line = f" {short(strat):10s} {sym:6s} x{qty:g} @ {price:>10.2f}" if live and sym in live: _, mkt, _ = live[sym] line += f" 现价 {mkt:>9.2f} 浮动 {(mkt - price) * qty:+.2f}" print(line) if not live: print(" (实时价格不可用)") if __name__ == "__main__": main()