168 lines
6.0 KiB
Python
168 lines
6.0 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
小唯股票自动复盘 — 每日检查多账户成交,生成复盘报告
|
||
==================================================
|
||
有平仓交易才推送(静默模式),无交易不打扰。
|
||
|
||
功能:
|
||
- 检查各行业账户今日平仓记录
|
||
- 汇总账户盈亏/胜率/持仓
|
||
- 生成复盘报告推飞书
|
||
- 周度复盘:周五包含本周总结
|
||
|
||
用法:python3 stock_review.py [--push]
|
||
"""
|
||
import json, sys, urllib.request
|
||
from datetime import datetime, timedelta
|
||
from pathlib import Path
|
||
|
||
OUTPUT = Path.home() / ".hermes" / "stock_backtest"
|
||
MULTI_DIR = OUTPUT / "multi_account"
|
||
FEISHU_WEBHOOK = "https://open.feishu.cn/open-apis/bot/v2/hook/446db983-e392-4d2c-bfb8-f9060e5df3ad"
|
||
STATE_FILE = OUTPUT / "review_state.json"
|
||
|
||
ACCOUNT_INDUSTRIES = ["新能源", "科技", "煤炭", "半导体", "证券", "有色", "石油",
|
||
"白酒", "医药", "地产"]
|
||
|
||
|
||
def load_state():
|
||
if STATE_FILE.exists():
|
||
try:
|
||
return json.load(open(STATE_FILE))
|
||
except Exception:
|
||
pass
|
||
return {"last_review": None, "last_week": None}
|
||
|
||
|
||
def save_state(state):
|
||
with open(STATE_FILE, "w") as f:
|
||
json.dump(state, f, ensure_ascii=False, indent=2)
|
||
|
||
|
||
def send_feishu(msg):
|
||
try:
|
||
payload = json.dumps({"msg_type": "text", "content": {"text": msg}}).encode()
|
||
req = urllib.request.Request(FEISHU_WEBHOOK, data=payload,
|
||
headers={"Content-Type": "application/json"})
|
||
urllib.request.urlopen(req, timeout=8)
|
||
return True
|
||
except Exception:
|
||
return False
|
||
|
||
|
||
def load_account(industry):
|
||
f = MULTI_DIR / f"account_{industry}.json"
|
||
if not f.exists():
|
||
return None
|
||
try:
|
||
return json.load(open(f))
|
||
except Exception:
|
||
return None
|
||
|
||
|
||
def generate_review():
|
||
"""生成复盘报告(有交易才返回内容)"""
|
||
today = datetime.now().strftime("%Y-%m-%d")
|
||
is_friday = datetime.now().weekday() == 4
|
||
state = load_state()
|
||
|
||
lines = [f"📋 股票复盘 {today}"]
|
||
|
||
# 1. 今日平仓记录
|
||
today_trades = []
|
||
total_pnl = 0
|
||
for ind in ACCOUNT_INDUSTRIES:
|
||
acct = load_account(ind)
|
||
if not acct:
|
||
continue
|
||
for t in acct.get("closed_trades", []):
|
||
if t.get("date") == today:
|
||
today_trades.append({"industry": ind, "stock": acct.get("stock", ind), **t})
|
||
total_pnl += t.get("pnl", 0)
|
||
|
||
if not today_trades:
|
||
# 无交易:检查是否有持仓需要展示
|
||
has_positions = False
|
||
pos_lines = []
|
||
total_assets = 0
|
||
for ind in ACCOUNT_INDUSTRIES:
|
||
acct = load_account(ind)
|
||
if not acct:
|
||
continue
|
||
total_assets += acct.get("current_capital", 0)
|
||
if acct.get("positions"):
|
||
has_positions = True
|
||
shares = sum(p["shares"] for p in acct["positions"])
|
||
cost = sum(p["shares"] * p["avg_cost"] for p in acct["positions"])
|
||
pos_lines.append(f" {acct.get('stock', ind)}({ind}): {shares}股 成本{cost:.0f}")
|
||
|
||
if not has_positions:
|
||
return None # 无交易无持仓,静默
|
||
|
||
lines.append(f"\n💰 总资产: {total_assets:.0f}")
|
||
lines.append(f"\n📦 当前持仓:")
|
||
lines.extend(pos_lines)
|
||
lines.append("\n无今日平仓,继续持有观察。")
|
||
else:
|
||
lines.append(f"\n📈 今日平仓 {len(today_trades)} 笔:")
|
||
wins = 0
|
||
for t in today_trades:
|
||
pnl_pct = t.get("pnl_pct", 0)
|
||
emoji = "🟢" if pnl_pct > 0 else "🔴"
|
||
if pnl_pct > 0:
|
||
wins += 1
|
||
lines.append(f" {emoji} {t['stock']}({t['industry']}): 买{t.get('buy_price',0):.2f}→卖{t.get('sell_price',0):.2f} "
|
||
f"{t.get('pnl',0):+.0f} ({pnl_pct:+.1f}%)")
|
||
lines.append(f"\n 胜率: {wins}/{len(today_trades)} | 今日盈亏: {total_pnl:+.0f}")
|
||
|
||
# 2. 账户汇总
|
||
lines.append(f"\n📊 账户汇总:")
|
||
total_assets = 0
|
||
for ind in ACCOUNT_INDUSTRIES:
|
||
acct = load_account(ind)
|
||
if not acct:
|
||
continue
|
||
shares = sum(p["shares"] for p in acct["positions"]) if acct.get("positions") else 0
|
||
total_cost = sum(p["shares"] * p["avg_cost"] for p in acct["positions"]) if acct.get("positions") else 0
|
||
total_assets += acct.get("current_capital", 0) + (total_cost if shares else 0)
|
||
pos_str = f"持仓{shares}股" if shares else "空仓"
|
||
lines.append(f" {acct.get('stock', ind)}({ind}): {pos_str} 资金{acct.get('current_capital',0):.0f}")
|
||
lines.append(f"\n💰 总资产: {total_assets:.0f}")
|
||
|
||
# 3. 周度总结(周五)
|
||
if is_friday:
|
||
week_key = datetime.now().isocalendar()[1]
|
||
if state.get("last_week") != week_key:
|
||
week_trades = []
|
||
week_pnl = 0
|
||
week_start = (datetime.now() - timedelta(days=5)).strftime("%Y-%m-%d")
|
||
for ind in ACCOUNT_INDUSTRIES:
|
||
acct = load_account(ind)
|
||
if not acct:
|
||
continue
|
||
for t in acct.get("closed_trades", []):
|
||
if t.get("date", "") >= week_start:
|
||
week_trades.append(t)
|
||
week_pnl += t.get("pnl", 0)
|
||
lines.append(f"\n📅 本周总结({len(week_trades)} 笔平仓,盈亏 {week_pnl:+.0f})")
|
||
lines.append(" - 复盘 MA20 信号有效性,检查行业动量变化")
|
||
lines.append(" - 参考周五 17:20 行业扫描 + 17:30 因子快照")
|
||
state["last_week"] = week_key
|
||
|
||
state["last_review"] = today
|
||
save_state(state)
|
||
|
||
return "\n".join(lines)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
push = "--push" in sys.argv
|
||
report = generate_review()
|
||
if report:
|
||
print(report)
|
||
if push:
|
||
ok = send_feishu(report)
|
||
print(f"\n{'✅ 已推送飞书' if ok else '⚠️ 推送失败'}")
|
||
else:
|
||
print("(无交易无持仓,静默)")
|