BotDeepSeek/daily_report.py

152 lines
5.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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()