#!/usr/bin/env python3 """ 股票模拟盘业绩汇报 — 自动收集/分析/汇总/建议 ============================================ 2026-08-02 牧尘批评:"项目运行一个月了,你从没有把数据收集、分析、汇总,给我建议。 每次都是我问你进度,你才开始看情况"——本脚本修复这个缺口: 不再等人问,定期自动把模拟盘业绩算清楚推给牧尘。 数据源: 1. paper 账户(~/.hermes/stock_backtest/paper_trades_*.json)— 早期单股模拟 2. 多行业账户(multi_account/account_*.json)— 当前主力模拟盘 3. 实时行情(腾讯 qt.gtimg.cn)— 持仓浮盈计算 4. 回测汇总(ma20_summary.json)— 策略历史有效性 用法: python3 stock_performance.py # 完整报告(默认) python3 stock_performance.py --weekly # 周报模式(周五收盘后) python3 stock_performance.py --monthly # 月报模式(月末) python3 stock_performance.py --json # JSON 输出 输出: 报告文本 + 推飞书(--push) """ import json, os, subprocess, sys from datetime import datetime, date from pathlib import Path HOME = Path.home() BACKTEST = HOME / ".hermes" / "stock_backtest" SCRIPTS = HOME / ".hermes" / "scripts" # 腾讯行情接口(curl subprocess 模式,铁律:urllib 在此目录挂起) def get_url(url, timeout=8, enc="utf-8"): env = dict(os.environ) for k in ["http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY"]: env.pop(k, None) r = subprocess.run(["curl", "-s", "--max-time", str(timeout), "--compressed", url], capture_output=True, timeout=timeout+2, env=env) return r.stdout.decode(enc, errors="ignore") def get_quote(code): """腾讯实时行情 → {price, name};code 如 000001 → sz000001""" mc = ("sh" if code.startswith(("6", "5")) else "sz") + code raw = get_url(f"https://qt.gtimg.cn/q={mc}", enc="gbk") if "~" not in raw: return None parts = raw.split("~") return {"price": float(parts[3]), "name": parts[1], "code": code} def load_json(path): if Path(path).exists(): try: return json.load(open(path)) except Exception: return None return None def calc_position_pnl(code, shares, avg_cost): """按实时价算浮盈""" q = get_quote(code) if not q: return None, None, None market_value = shares * q["price"] pnl = market_value - shares * avg_cost pnl_pct = pnl / (shares * avg_cost) * 100 return q["price"], pnl, pnl_pct def collect_paper_accounts(): """早期 paper 账户""" result = [] for f in sorted(BACKTEST.glob("paper_trades_*.json")): d = load_json(f) if not d: continue for pos in d.get("positions", []): code = f.stem.replace("paper_trades_", "") result.append({ "type": "paper", "stock": d.get("stock", code), "code": code, "shares": pos["shares"], "avg_cost": pos["avg_cost"], "start": d.get("start_date", ""), }) return result def collect_multi_accounts(): """多行业账户 + 做空账户 + 全球账户""" result = [] # 多行业账户 adir = BACKTEST / "multi_account" if adir.exists(): for f in sorted(adir.glob("account_*.json")): a = load_json(f) if not a: continue for pos in a.get("positions", []): result.append({ "type": "multi", "stock": a.get("stock", ""), "code": a.get("code", ""), "industry": a.get("industry", ""), "shares": pos.get("shares", 0), "avg_cost": pos.get("avg_cost", 0), "start": a.get("created", ""), }) # 做空账户(空头持仓:盈亏 = (开仓价 - 现价) * 股数) sdir = BACKTEST / "short_account" if sdir.exists(): for f in sorted(sdir.glob("short_*.json")): a = load_json(f) if not a: continue for pos in a.get("short_positions", []): result.append({ "type": "short", "stock": a.get("stock", ""), "code": a.get("code", ""), "industry": a.get("industry", ""), "shares": pos.get("shares", 0), "avg_cost": pos.get("open_price", 0), "start": pos.get("open_date", ""), }) # 全球账户 gdir = BACKTEST / "global_account" if gdir.exists(): for f in sorted(gdir.glob("global_*.json")): a = load_json(f) if not a: continue for pos in a.get("positions", []): result.append({ "type": "global", "stock": a.get("stock", ""), "code": a.get("code", ""), "market": a.get("market", ""), "shares": pos.get("shares", 0), "avg_cost": pos.get("avg_cost", 0), "start": a.get("created", ""), }) return result def build_report(): today = date.today().isoformat() lines = [] lines.append(f"📊 模拟盘业绩报告 {today}") lines.append("=" * 42) # 1. 当前持仓 + 浮盈 positions = collect_paper_accounts() + collect_multi_accounts() lines.append("📈 当前持仓") total_pnl = 0 total_cost = 0 if positions: for p in positions: price, pnl, pnl_pct = calc_position_pnl(p["code"], p["shares"], p["avg_cost"]) if price is None: lines.append(f" {p['stock']}({p['code']}) 行情获取失败") continue # 做空盈亏 = (开仓价 - 现价) * 股数,与做多方向相反 if p["type"] == "short": pnl = (p["avg_cost"] - price) * p["shares"] pnl_pct = pnl / (p["avg_cost"] * p["shares"]) * 100 if p["type"] == "paper": tag = "paper" elif p["type"] == "short": tag = f"做空[{p.get('industry','')}]" elif p["type"] == "global": tag = f"{p.get('market','')}[{p['stock']}]" else: tag = f"多账户[{p.get('industry','')}]" arrow = "🟢" if pnl >= 0 else "🔴" lines.append(f" {arrow} {p['stock']} {p['shares']}股 @{p['avg_cost']:.2f} → {price:.2f} " f"({pnl:+,.0f}元 / {pnl_pct:+.2f}%) [{tag}]") total_pnl += pnl total_cost += p["shares"] * p["avg_cost"] if total_cost > 0: lines.append(f" 合计持仓成本 {total_cost:,.0f}元 | 浮盈 {total_pnl:+,.0f}元 ({total_pnl/total_cost*100:+.2f}%)") else: lines.append(" 空仓(无持仓)") # 2. 多账户总资产(2026-08-28 修复:总资产 = 现金 + 持仓成本,之前只算现金导致持仓账户资产被低估) adir = BACKTEST / "multi_account" total_cap = 0 acct_count = 0 if adir.exists(): for f in sorted(adir.glob("account_*.json")): a = load_json(f) if a: total_cap += a.get("current_capital", 0) for p in a.get("positions", []): total_cap += p.get("shares", 0) * p.get("avg_cost", 0) acct_count += 1 lines.append(f"\n💰 多账户资产: {acct_count} 个账户 | 总资产 {total_cap:,.0f}元") # 有平仓交易的账户显示胜率 win_t = sum(load_json(f).get("stats", {}).get("winning_trades", 0) for f in adir.glob("account_*.json") if load_json(f)) lose_t = sum(load_json(f).get("stats", {}).get("losing_trades", 0) for f in adir.glob("account_*.json") if load_json(f)) if win_t + lose_t > 0: lines.append(f" 已平仓交易: 胜{win_t} 负{lose_t} 胜率 {win_t/(win_t+lose_t)*100:.0f}%") # 2b. 做空 + 全球账户资产(2026-08-28 同步修复持仓市值计入) for dname, label in [("short_account", "做空"), ("global_account", "全球")]: d = BACKTEST / dname if d.exists(): cap = 0 for f in d.glob("*.json"): a = load_json(f) if a: cap += a.get("current_capital", 0) for p in a.get("positions", []): cap += p.get("shares", 0) * p.get("avg_cost", 0) cnt = len(list(d.glob("*.json"))) lines.append(f"💰 {label}账户资产: {cnt} 个账户 | 总资产 {cap:,.0f}元") # 3. 策略回测有效性(ma20_summary) ms = load_json(BACKTEST / "ma20_summary.json") if ms: lines.append("\n📚 策略回测参考 (2024-06~2026 区间)") for s in ms.get("stocks", [])[:5]: lines.append(f" {s['name']}: α {s['alpha']:+.1f}% 胜率{s['win_rate']:.0f}% 交易{s['total_trades']}笔") lines.append(f" 有效板块: {', '.join(ms.get('effective', []))} | 低效: {', '.join(ms.get('ineffective', []))}") # 4. 建议 lines.append("\n💡 建议") if total_pnl > 0 and total_cost > 0: lines.append(f" ✅ 当前持仓浮盈 {total_pnl:+,.0f}元 — 持有策略有效,继续按 MA20 纪律(死叉卖出)") else: lines.append(" ⚠️ 当前空仓 — 等待强势行业金叉信号自动开仓") if positions: for p in positions: lines.append(f" • {p['stock']}: 关注 MA20 死叉信号({p['code']})") return "\n".join(lines) if __name__ == "__main__": args = sys.argv[1:] mode = "monthly" if "--monthly" in args else ("weekly" if "--weekly" in args else "daily") report = build_report() print(report) if "--push" in args: # 通过 send_message 推飞书(由外层 wrapper 处理) pass