xiaowei-system/scripts/stock_performance.py

236 lines
9.4 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 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. 多账户总资产
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)
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. 做空 + 全球账户资产
for dname, label in [("short_account", "做空"), ("global_account", "全球")]:
d = BACKTEST / dname
if d.exists():
cap = sum(load_json(f).get("current_capital", 0) for f in d.glob("*.json") if load_json(f))
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