xiaowei-system/scripts/stock_multi_account.py

288 lines
12 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
"""
小唯多账户模拟交易 — 按行业分配独立 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)
# 行业 → 账户配置
# 强势行业: 独立资金池,全仓交易
# 弱势行业: 独立资金池半仓交易2026-08-01 回测优化:全拦截损失 α,半仓最优)
# 2026-08-02 扩充:新增有色(紫金矿业动量+47.4% Sharpe1.41 最强标的)
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": "601899", "name": "紫金矿业", "capital": 100000, "blocked": False},
"石油": {"code": "601857", "name": "中国石油", "capital": 100000, "blocked": False},
"白酒": {"code": "000858", "name": "五粮液", "capital": 100000, "blocked": False},
"医药": {"code": "600276", "name": "恒瑞医药", "capital": 100000, "blocked": False},
"地产": {"code": "000002", "name": "万科A", "capital": 100000, "blocked": False},
}
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 get_dynamic_alloc(industry):
"""
动态仓位2026-08-02 增强:按行业动量强度调整)
从 industry_scan.json 读取行业动量:
动量 > 0 → 1.0(全仓,强势行业)
动量 -20% ~ 0 → 0.6(偏弱,降仓)
动量 -40% ~ -20%→ 0.5(弱势,半仓)
动量 < -40% → 0.3(深度弱势,轻仓)
无数据回退:弱势行业 0.5,其他 1.0
"""
f = OUTPUT.parent / "industry_scan.json"
mom = None
if f.exists():
try:
scan = json.load(open(f))
mom_map = scan.get("industries", {}).get("avg_mom", {})
mom = mom_map.get(industry)
except Exception:
mom = None
if mom is None:
return 0.5 if industry in WEAK_INDUSTRIES else 1.0
if mom > 0:
return 1.0
if mom > -0.20:
return 0.6
if mom > -0.40:
return 0.5
return 0.3
def execute_signal(sig, dry_run=False):
"""
信号执行(多账户路由版 v4 — 2026-08-02 止损保险丝)
sig: {"industry": "新能源", "signal": "BUY"/"SELL"/"STOP_LOSS"/..., "close": 价格, ...}
v4 变更:新增 STOP_LOSS 信号(熔断保险丝)
回测证据stock_stop_loss_validate.py
- MA20 死叉本身就是动态止损(比 8% 硬止损更早触发)
- 8%/10% 硬止损触发 0 次,收益不变
- 5% 止损触发 1-2 次,收益仍不变
- 结论:硬止损是"保险丝"——常规行情用不到,极端跳空/跌停时兜底
"""
if not sig:
return ("SKIP", "无信号")
industry = sig.get("industry", "")
signal = sig.get("signal", "")
price = sig.get("close") or sig.get("price") or 0
code = sig.get("code") or sig.get("stock_code") or ""
if industry not in ACCOUNTS:
return ("SKIP", f"未配置行业账户: {industry}")
# v5 修复2026-08-28行业路由张冠李戴防护
# 同一行业多只股票(如新能源=宁德时代+比亚迪、煤炭=中国神华+兖矿能源),
# 金叉信号若只按行业路由,会把触发股票的价格记到账户绑定股票名下(假盈亏)。
# 信号必须携带触发股票的 code且必须等于账户绑定的 code 才执行。
acct_cfg = ACCOUNTS.get(industry, {})
bound_code = str(acct_cfg.get("code", "") or "")
if code and bound_code and str(code).strip() != bound_code.strip():
return ("SKIP", f"信号股票{code}≠账户标的{bound_code},跳过(防张冠李戴)")
acct = load_account(industry)
has_position = bool(acct["positions"])
if signal == "BUY" and not has_position:
if acct.get("blocked"):
return ("BLOCK", f"行业{industry}账户已锁定")
# 动态仓位v3按行业动量强度调整2026-08-02
alloc = get_dynamic_alloc(industry)
is_weak = industry in WEAK_INDUSTRIES
shares = int(acct["current_capital"] * alloc // price) if price > 0 else 0
if shares <= 0:
return ("SKIP", "资金不足")
cost = shares * price
if dry_run:
return ("BUY", f"{industry}{'半仓' if is_weak else '全仓'}买入@{price:.2f} {shares}股(仓位{alloc:.0%})")
acct["positions"].append({"code": bound_code, "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)
mode = f"{alloc:.0%}仓(弱势行业)" if is_weak else "全仓"
return ("BUY", f"{industry}{mode}买入{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 == "STOP_LOSS":
if not has_position:
return ("HOLD", f"{industry}已空仓,无止损需要")
# 止损保险丝:极端行情兜底(正常由死叉触发,这里兜底)
shares = sum(p["shares"] for p in acct["positions"])
if dry_run:
return ("STOP_LOSS", 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 ("STOP_LOSS", f"{industry}止损卖出{shares}股@{price:.2f} 盈亏{pnl:+.0f}")
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__)