股票第四波: 多账户按行业分配(5强可交易/5弱拦截), 组合扫描--multi自动路由, 多账户日报cron, 周学习接入行业动量
This commit is contained in:
parent
e86e28fe86
commit
fe58e9d6a3
|
|
@ -145,6 +145,17 @@ PHASES = {
|
|||
}
|
||||
|
||||
|
||||
def load_industry_momentum():
|
||||
"""读取行业扫描结果(stock_industry_scan.py 生成)"""
|
||||
f = HERMES + "/stock_backtest/industry_scan.json"
|
||||
if not os.path.exists(f):
|
||||
return None
|
||||
try:
|
||||
return json.load(open(f))
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def generate_report(progress, topic):
|
||||
phase = progress["current_phase"]
|
||||
item_idx = progress["current_item"]
|
||||
|
|
@ -154,6 +165,23 @@ def generate_report(progress, topic):
|
|||
report += f"时间: {datetime.now().strftime('%Y-%m-%d %H:%M')}\n"
|
||||
report += f"当前阶段: Phase {phase} - {phase_data.get('title', '完成')}\n\n"
|
||||
|
||||
# 市场实时情报(2026-08-01 新增:行业动量 + 关注池机会)
|
||||
scan = load_industry_momentum()
|
||||
if scan and scan.get("industries"):
|
||||
report += "**🔭 市场行业动量(学术因子)**\n"
|
||||
try:
|
||||
mom_map = scan["industries"].get("avg_mom", {})
|
||||
ranked = sorted(mom_map.items(), key=lambda kv: kv[1], reverse=True)
|
||||
strong = [f"{ind}({mom:+.0%})" for ind, mom in ranked[:3] if mom > 0]
|
||||
weak = [f"{ind}({mom:+.0%})" for ind, mom in ranked[-3:]]
|
||||
if strong:
|
||||
report += f"🟢 强势行业: {' '.join(strong)}\n"
|
||||
if weak:
|
||||
report += f"🔴 弱势行业: {' '.join(weak)} (金叉硬拦截)\n"
|
||||
except Exception:
|
||||
pass
|
||||
report += "\n"
|
||||
|
||||
if phase == 5:
|
||||
report += "**✅ 理论学习完成,开始模拟交易阶段**\n\n"
|
||||
report += "下一步:选择模拟平台(小唯推荐聚宽/掘金),搭建回测环境\n"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,210 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
小唯多账户模拟交易 — 按行业分配独立 paper 账户
|
||||
============================================
|
||||
2026-08-01 行业扫描后新增:强势行业单独账户,弱势行业拦截
|
||||
|
||||
账户结构:
|
||||
强势行业账户: 新能源 / 科技 / 煤炭 / 半导体 / 证券(每个独立资金池)
|
||||
弱势行业账户: 白酒 / 医药 / 通信 / 汽车 / 地产(硬拦截,只记录不交易)
|
||||
|
||||
用法:
|
||||
python3 stock_multi_account.py status # 所有账户状态
|
||||
python3 stock_multi_account.py status <行业> # 指定行业账户
|
||||
python3 stock_multi_account.py execute <sig_json> # 信号执行(自动路由)
|
||||
python3 stock_multi_account.py report # 汇总报告
|
||||
"""
|
||||
import json, os, sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
OUTPUT = Path.home() / ".hermes" / "stock_backtest" / "multi_account"
|
||||
OUTPUT.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 行业 → 账户配置
|
||||
# 强势行业: 独立资金池,可交易
|
||||
# 弱势行业: 资金池 0(硬拦截),只记录
|
||||
ACCOUNTS = {
|
||||
"新能源": {"code": "300750", "name": "宁德时代", "capital": 100000, "blocked": False},
|
||||
"科技": {"code": "002415", "name": "海康威视", "capital": 100000, "blocked": False},
|
||||
"煤炭": {"code": "601088", "name": "中国神华", "capital": 100000, "blocked": False},
|
||||
"半导体": {"code": "688981", "name": "中芯国际", "capital": 100000, "blocked": False},
|
||||
"证券": {"code": "600030", "name": "中信证券", "capital": 100000, "blocked": False},
|
||||
"白酒": {"code": "000858", "name": "五粮液", "capital": 0, "blocked": True},
|
||||
"医药": {"code": "600276", "name": "恒瑞医药", "capital": 0, "blocked": True},
|
||||
"地产": {"code": "000002", "name": "万科A", "capital": 0, "blocked": True},
|
||||
}
|
||||
|
||||
WEAK_INDUSTRIES = {"白酒", "医药", "通信", "汽车", "地产"}
|
||||
|
||||
|
||||
def account_file(industry):
|
||||
return OUTPUT / f"account_{industry}.json"
|
||||
|
||||
|
||||
def load_account(industry):
|
||||
f = account_file(industry)
|
||||
if f.exists():
|
||||
with open(f) as fp:
|
||||
return json.load(fp)
|
||||
cfg = ACCOUNTS.get(industry, {})
|
||||
return {
|
||||
"industry": industry,
|
||||
"stock": cfg.get("name", ""),
|
||||
"code": cfg.get("code", ""),
|
||||
"blocked": cfg.get("blocked", industry in WEAK_INDUSTRIES),
|
||||
"initial_capital": cfg.get("capital", 0),
|
||||
"current_capital": cfg.get("capital", 0),
|
||||
"positions": [],
|
||||
"closed_trades": [],
|
||||
"last_signal": "空仓",
|
||||
"last_signal_date": "",
|
||||
"stats": {"total_trades": 0, "winning_trades": 0, "losing_trades": 0},
|
||||
"created": datetime.now().strftime("%Y-%m-%d"),
|
||||
}
|
||||
|
||||
|
||||
def save_account(acct):
|
||||
with open(account_file(acct["industry"]), "w") as f:
|
||||
json.dump(acct, f, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
def execute_signal(sig, dry_run=False):
|
||||
"""
|
||||
信号执行(多账户路由版)
|
||||
sig: {"industry": "新能源", "signal": "BUY"/"SELL"/..., "close": 价格, ...}
|
||||
"""
|
||||
if not sig:
|
||||
return ("SKIP", "无信号")
|
||||
|
||||
industry = sig.get("industry", "")
|
||||
signal = sig.get("signal", "")
|
||||
price = sig.get("close") or sig.get("price") or 0
|
||||
|
||||
# 弱势行业硬拦截
|
||||
if signal == "BUY" and industry in WEAK_INDUSTRIES:
|
||||
return ("BLOCK", f"弱势行业({industry}动量负)金叉,硬规则不自动买入")
|
||||
|
||||
if industry not in ACCOUNTS:
|
||||
return ("SKIP", f"未配置行业账户: {industry}")
|
||||
|
||||
acct = load_account(industry)
|
||||
has_position = bool(acct["positions"])
|
||||
|
||||
if signal == "BUY" and not has_position:
|
||||
if acct.get("blocked"):
|
||||
return ("BLOCK", f"行业{industry}账户已锁定")
|
||||
shares = int(acct["current_capital"] // price) if price > 0 else 0
|
||||
if shares <= 0:
|
||||
return ("SKIP", "资金不足")
|
||||
cost = shares * price
|
||||
if dry_run:
|
||||
return ("BUY", f"{industry}金叉买入@{price:.2f} {shares}股")
|
||||
acct["positions"].append({"shares": shares, "avg_cost": price})
|
||||
acct["current_capital"] -= cost
|
||||
acct["last_signal"] = "买入"
|
||||
acct["last_signal_date"] = datetime.now().strftime("%Y-%m-%d")
|
||||
save_account(acct)
|
||||
return ("BUY", f"{industry}买入{shares}股@{price:.2f} 剩余{acct['current_capital']:.0f}")
|
||||
|
||||
elif signal == "SELL" and has_position:
|
||||
shares = sum(p["shares"] for p in acct["positions"])
|
||||
if dry_run:
|
||||
return ("SELL", f"{industry}死叉卖出@{price:.2f} {shares}股")
|
||||
avg_cost = sum(p["shares"] * p["avg_cost"] for p in acct["positions"]) / shares
|
||||
pnl = (price - avg_cost) * shares
|
||||
acct["current_capital"] += price * shares
|
||||
acct["closed_trades"].append({
|
||||
"date": datetime.now().strftime("%Y-%m-%d"),
|
||||
"buy_price": avg_cost, "sell_price": price,
|
||||
"shares": shares, "pnl": pnl,
|
||||
"pnl_pct": pnl / (avg_cost * shares) * 100 if avg_cost * shares else 0,
|
||||
})
|
||||
acct["stats"]["total_trades"] += 1
|
||||
acct["stats"]["winning_trades"] += 1 if pnl > 0 else 0
|
||||
acct["stats"]["losing_trades"] += 1 if pnl < 0 else 0
|
||||
acct["positions"] = []
|
||||
acct["last_signal"] = "卖出"
|
||||
acct["last_signal_date"] = datetime.now().strftime("%Y-%m-%d")
|
||||
save_account(acct)
|
||||
return ("SELL", f"{industry}卖出{shares}股@{price:.2f} 盈亏{pnl:+.0f}")
|
||||
|
||||
elif signal == "BUY" and has_position:
|
||||
return ("HOLD", f"{industry}已持仓,忽略重复买入")
|
||||
|
||||
elif signal == "SELL" and not has_position:
|
||||
return ("HOLD", f"{industry}已空仓,忽略重复卖出")
|
||||
|
||||
elif signal == "HOLD_LONG":
|
||||
return ("HOLD", f"{industry}持仓中" if has_position else f"{industry}空仓(非金叉不买)")
|
||||
|
||||
return ("HOLD", f"{industry}空仓观望")
|
||||
|
||||
|
||||
def cmd_status(industry=None):
|
||||
print("=" * 60)
|
||||
print(f"小唯多账户模拟 — {datetime.now().strftime('%Y-%m-%d %H:%M')}")
|
||||
print("=" * 60)
|
||||
|
||||
industries = [industry] if industry else list(ACCOUNTS.keys())
|
||||
total_assets = 0
|
||||
for ind in industries:
|
||||
acct = load_account(ind)
|
||||
shares = sum(p["shares"] for p in acct["positions"])
|
||||
total_cost = sum(p["shares"] * p["avg_cost"] for p in acct["positions"])
|
||||
market_val = total_cost # 简化:成本计市值(真实市值需行情)
|
||||
total = acct["current_capital"] + (market_val if shares else 0)
|
||||
total_assets += total
|
||||
|
||||
tag = "⛔拦截" if acct.get("blocked") else "🟢可交易"
|
||||
pos = f"持仓{shares}股" if shares else "空仓"
|
||||
pnl = total - acct["initial_capital"]
|
||||
print(f"\n【{ind}】{tag} 标的:{acct['stock']}({acct['code']})")
|
||||
print(f" 资金: {acct['current_capital']:.0f} | {pos} | 盈亏: {pnl:+.0f}")
|
||||
if acct["closed_trades"]:
|
||||
wins = acct["stats"]["winning_trades"]
|
||||
tot = acct["stats"]["total_trades"]
|
||||
print(f" 已平仓: {tot}笔 | 胜率: {wins}/{tot}")
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f"总资产: {total_assets:.0f}")
|
||||
|
||||
|
||||
def cmd_report():
|
||||
"""汇总报告(飞书推送用)"""
|
||||
lines = [f"📊 多账户日报 {datetime.now().strftime('%Y-%m-%d')}"]
|
||||
total = 0
|
||||
for ind, cfg in ACCOUNTS.items():
|
||||
acct = load_account(ind)
|
||||
shares = sum(p["shares"] for p in acct["positions"])
|
||||
total_cost = sum(p["shares"] * p["avg_cost"] for p in acct["positions"])
|
||||
market_val = total_cost if shares else 0
|
||||
t = acct["current_capital"] + market_val
|
||||
total += t
|
||||
pnl = t - acct["initial_capital"]
|
||||
emoji = "⛔" if acct.get("blocked") else "🟢"
|
||||
lines.append(f"{emoji} {ind}: {acct['stock']} 资产{t:.0f} ({pnl:+.0f})")
|
||||
lines.append(f"💰 总资产: {total:.0f}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cmd = sys.argv[1] if len(sys.argv) > 1 else "status"
|
||||
|
||||
if cmd == "status":
|
||||
ind = sys.argv[2] if len(sys.argv) > 2 else None
|
||||
cmd_status(ind)
|
||||
elif cmd == "execute":
|
||||
# 从 stdin 或文件读取信号 JSON
|
||||
if len(sys.argv) > 2 and os.path.exists(sys.argv[2]):
|
||||
sig = json.load(open(sys.argv[2]))
|
||||
elif len(sys.argv) > 2:
|
||||
sig = json.loads(sys.argv[2])
|
||||
else:
|
||||
sig = json.load(sys.stdin)
|
||||
action, detail = execute_signal(sig)
|
||||
print(f"[{action}] {detail}")
|
||||
elif cmd == "report":
|
||||
print(cmd_report())
|
||||
else:
|
||||
print(__doc__)
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
#!/bin/bash
|
||||
# 每日多账户日报:按行业 paper 账户汇总(强势可交易/弱势拦截)
|
||||
# 被 cron 调用(工作日 18:00)
|
||||
cd ~/.hermes/scripts || exit 1
|
||||
exec python3 stock_multi_account.py report
|
||||
|
|
@ -319,8 +319,25 @@ def build_portfolio_report(push=False):
|
|||
if stats:
|
||||
stat_line = f"\n 📊 MA20策略: 历史胜率{stats.get('win_rate',0):.0f}% | α{stats.get('alpha',0):+.1f}% | 最大回撤{stats.get('max_drawdown',0):.0f}%"
|
||||
msg += f"🟡 {name}({code}) 金叉! 价格{r['price']:.2f} 偏离MA20 {diff:+.1f}%{stat_line}\n"
|
||||
# 多账户自动路由(2026-08-01 新增)
|
||||
if "--multi" in sys.argv:
|
||||
try:
|
||||
import stock_multi_account as sma
|
||||
action, detail = sma.execute_signal(
|
||||
{"industry": industry, "signal": "BUY", "close": r["price"]})
|
||||
msg += f" 📝 多账户: [{action}] {detail}\n"
|
||||
except Exception as e:
|
||||
msg += f" ⚠️ 多账户执行失败: {e}\n"
|
||||
elif r["dead_cross"]:
|
||||
msg += f"🔴 {name}({code}) 死叉! 平仓\n"
|
||||
if "--multi" in sys.argv:
|
||||
try:
|
||||
import stock_multi_account as sma
|
||||
action, detail = sma.execute_signal(
|
||||
{"industry": industry, "signal": "SELL", "close": r["price"]})
|
||||
msg += f" 📝 多账户: [{action}] {detail}\n"
|
||||
except Exception as e:
|
||||
msg += f" ⚠️ 多账户执行失败: {e}\n"
|
||||
if not signals:
|
||||
msg += "\n暂无金叉,继续空仓等待。\n"
|
||||
msg += f"\n生成: {datetime.now().strftime('%H:%M')}"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
#!/bin/bash
|
||||
# 每日组合信号 + 多账户自动路由
|
||||
# 被 cron c293eead6688 调用(工作日 09:00)
|
||||
cd ~/.hermes/scripts || exit 1
|
||||
exec python3 stock_portfolio.py --push --multi
|
||||
Loading…
Reference in New Issue