152 lines
5.3 KiB
Python
152 lines
5.3 KiB
Python
#!/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()
|