fix(stock): stock_paper.py 硬编码五粮液账户 → 按股票代码动态定位

根因: ACCOUNT_FILE='paper_trades_000858.json' 硬编码,
execute_signal(sig) 不管传入什么股票代码都写入五粮液文件。
平安银行(000001)金叉 → 写入五粮液文件 → 产生假交易(+334万)。

修复:
- load_account(code)/save_account(acct, code) 按代码动态定位文件
- execute_signal 从 sig['code'] 提取代码,传入 load/save
- cmd_buy/cmd_sell/cmd_status/cmd_report 全部支持 --code 参数
- 新建账户自动写入 stock_code 字段
- 清除五粮液文件中的假交易数据
- 修复 stock_signal.py 行业判断 bug(股票名当行业名)
This commit is contained in:
小唯 A06 2026-08-20 16:06:11 +08:00
parent f02d5567f2
commit 430bb46340
2 changed files with 150 additions and 56 deletions

View File

@ -23,13 +23,49 @@ from pathlib import Path
OUTPUT = Path.home() / ".hermes" / "stock_backtest"
OUTPUT.mkdir(exist_ok=True)
ACCOUNT_FILE = OUTPUT / "paper_trades_000858.json"
ACCOUNT_FILE = OUTPUT / "paper_trades_000858.json" # 默认五粮液execute_signal 会按代码动态覆盖
# 股票名称映射(与 stock_signal.py / stock_portfolio.py 一致)
STOCK_NAMES = {
"000858": "五粮液",
"600519": "贵州茅台",
"000568": "泸州老窖",
"002304": "洋河股份",
"600036": "招商银行",
"601318": "中国平安",
"000001": "平安银行",
"300750": "宁德时代",
"002594": "比亚迪",
"002415": "海康威视",
"600030": "中信证券",
"601899": "紫金矿业",
"601857": "中国石油",
"601088": "中国神华",
"600188": "兖矿能源",
"688981": "中芯国际",
"000333": "美的集团",
"600276": "恒瑞医药",
"600941": "中国移动",
"000002": "万科A",
"601012": "隆基绿能",
"000063": "中兴通讯",
"600900": "长江电力",
"601398": "工商银行",
"601166": "兴业银行",
}
def _account_file(code=None):
"""根据股票代码返回账户文件路径None 时用默认五粮液"""
if code:
return OUTPUT / f"paper_trades_{code}.json"
return ACCOUNT_FILE
FEISHU_WEBHOOK = "https://open.feishu.cn/open-apis/bot/v2/hook/446db983-e392-4d2c-bfb8-f9060e5df3ad"
def load_account():
if ACCOUNT_FILE.exists():
with open(ACCOUNT_FILE) as f:
def load_account(code=None):
acct_file = _account_file(code)
if acct_file.exists():
with open(acct_file) as f:
d = json.load(f)
# 确保字段完整(兼容旧格式)
d.setdefault("current_capital", d.get("current_capital", 100000))
@ -41,8 +77,9 @@ def load_account():
return d
return {
"strategy": "MA20突破",
"stock": "五粮液(000858)",
"start_date": "2026-07-12",
"stock": STOCK_NAMES.get(code, f"未知({code})") if code else "五粮液(000858)",
"stock_code": code or "000858",
"start_date": datetime.now().strftime("%Y-%m-%d"),
"initial_capital": 100000,
"current_capital": 100000,
"positions": [], # [{"shares": N, "avg_cost": P}]
@ -53,8 +90,9 @@ def load_account():
}
def save_account(acct):
with open(ACCOUNT_FILE, "w") as f:
def save_account(acct, code=None):
acct_file = _account_file(code)
with open(acct_file, "w") as f:
json.dump(acct, f, ensure_ascii=False, indent=2)
@ -82,10 +120,11 @@ def get_current_price(code="000858"):
return None, None
def cmd_buy(price, shares=None):
def cmd_buy(price, shares=None, code=None):
"""模拟买入"""
acct = load_account()
acct = load_account(code)
price = float(price)
name = STOCK_NAMES.get(code, acct.get("stock", "五粮液")) if code else acct.get("stock", "五粮液")
shares = shares or int(acct["current_capital"] // price)
cost = shares * price
@ -102,9 +141,9 @@ def cmd_buy(price, shares=None):
acct["current_capital"] -= cost
acct["last_signal"] = "买入"
acct["last_signal_date"] = today
save_account(acct)
save_account(acct, code)
msg = f"""🟢 模拟买入 — 五粮液(000858)
msg = f"""🟢 模拟买入 — {name}
日期: {today}
价格: {price:.2f}
@ -128,14 +167,16 @@ def execute_signal(sig, dry_run=False):
- BUY: 无持仓才买避免重复买入
- SELL: 有持仓才卖避免空卖
- HOLD_LONG / HOLD_SHORT: 不动
- 弱势行业 BUY 硬拦截2026-08-01 行业动量过滤升级
- 弱势行业 BUY 半仓买入2026-08-01 回测优化全拦截损失 α半仓最优
返回: (action, detail)
"""
if not sig:
return ("SKIP", "无信号")
acct = load_account()
# 关键修复:从信号中提取股票代码,动态定位正确的账户文件
code = sig.get("code") or sig.get("stock_code", "000858")
acct = load_account(code)
has_position = bool(acct["positions"])
signal = sig.get("signal")
@ -145,10 +186,11 @@ def execute_signal(sig, dry_run=False):
if signal == "BUY" and not has_position:
price = sig.get("close") or sig.get("price")
name = STOCK_NAMES.get(code, code)
if dry_run:
mode = "半仓(弱势行业)" if is_weak else "全仓"
print(f"[DRY-RUN] 金叉{mode}买入 @ {price:.2f}")
return ("BUY", f"{mode}金叉买入@{price:.2f}")
print(f"[DRY-RUN] {name} 金叉{mode}买入 @ {price:.2f}")
return ("BUY", f"{name} {mode}金叉买入@{price:.2f}")
if is_weak:
# 半仓:只用一半资金
shares = int((acct["current_capital"] * 0.5) // price)
@ -159,25 +201,71 @@ def execute_signal(sig, dry_run=False):
acct["current_capital"] -= cost
acct["last_signal"] = "半仓买入"
acct["last_signal_date"] = datetime.now().strftime("%Y-%m-%d")
save_account(acct)
msg = (f"⚠️ 模拟半仓买入 — 弱势行业金叉\n\n"
save_account(acct, code)
msg = (f"⚠️ 模拟半仓买入 — {name} 弱势行业金叉\n\n"
f"日期: {datetime.now().strftime('%Y-%m-%d')}\n"
f"价格: {price:.2f}\n数量: {shares}\n金额: {cost:.2f}\n"
f"剩余现金: {acct['current_capital']:.2f}\n\n"
f"弱势行业(白酒动量负)金叉,按半仓规则买入。")
f"弱势行业金叉,按半仓规则买入。")
send_feishu(msg)
print(msg)
return ("BUY_HALF", f"弱势行业半仓买入@{price:.2f}")
cmd_buy(price)
return ("BUY", f"金叉买入@{price:.2f}")
return ("BUY_HALF", f"{name} 弱势行业半仓买入@{price:.2f}")
# 全仓买入
shares = int(acct["current_capital"] // price)
if shares <= 0:
return ("SKIP", "资金不足")
cost = shares * price
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, code)
msg = (f"🟢 模拟买入 — {name}({code})\n\n"
f"日期: {datetime.now().strftime('%Y-%m-%d')}\n"
f"价格: {price:.2f}\n数量: {shares}\n金额: {cost:.2f}\n"
f"剩余现金: {acct['current_capital']:.2f}\n\n"
f"MA20金叉触发已记录纸上持仓。")
send_feishu(msg)
print(msg)
return ("BUY", f"{name} 金叉买入@{price:.2f}")
elif signal == "SELL" and has_position:
price = sig.get("close") or sig.get("price")
name = STOCK_NAMES.get(code, code)
if dry_run:
print(f"[DRY-RUN] 死叉卖出 @ {price:.2f}")
return ("SELL", f"死叉卖出@{price:.2f}")
cmd_sell(price)
return ("SELL", f"死叉卖出@{price:.2f}")
print(f"[DRY-RUN] {name} 死叉卖出 @ {price:.2f}")
return ("SELL", f"{name} 死叉卖出@{price:.2f}")
# 卖出:平掉所有持仓
total_shares = sum(p["shares"] for p in acct["positions"])
total_cost = sum(p["shares"] * p["avg_cost"] for p in acct["positions"])
revenue = total_shares * price
pnl = revenue - total_cost
acct["current_capital"] += revenue
acct["positions"] = []
acct["last_signal"] = "卖出"
acct["last_signal_date"] = datetime.now().strftime("%Y-%m-%d")
acct["closed_trades"].append({
"date": datetime.now().strftime("%Y-%m-%d"),
"buy_price": total_cost / total_shares if total_shares else 0,
"sell_price": price,
"shares": total_shares,
"pnl": pnl,
})
acct["stats"]["total_trades"] += 1
if pnl > 0:
acct["stats"]["winning_trades"] += 1
else:
acct["stats"]["losing_trades"] += 1
acct["stats"]["total_pnl"] = acct["stats"].get("total_pnl", 0) + pnl
save_account(acct, code)
msg = (f"🔴 模拟卖出 — {name}({code})\n\n"
f"日期: {datetime.now().strftime('%Y-%m-%d')}\n"
f"卖出价: {price:.2f}\n数量: {total_shares}\n"
f"盈亏: {pnl:+.2f}\n\n"
f"MA20死叉触发已平仓。")
send_feishu(msg)
print(msg)
return ("SELL", f"{name} 死叉卖出@{price:.2f} 盈亏{pnl:+.0f}")
elif signal == "BUY" and has_position:
return ("HOLD", "已持仓,忽略重复买入")
@ -194,9 +282,10 @@ def execute_signal(sig, dry_run=False):
return ("HOLD", "空仓观望")
def cmd_sell(price):
def cmd_sell(price, code=None):
"""模拟卖出"""
acct = load_account()
acct = load_account(code)
name = STOCK_NAMES.get(code, acct.get("stock", "五粮液")) if code else acct.get("stock", "五粮液")
price = float(price)
today = datetime.now().strftime("%Y-%m-%d")
@ -231,9 +320,9 @@ def cmd_sell(price):
else:
stats["losing_trades"] += 1
save_account(acct)
save_account(acct, code)
msg = f"""🔴 模拟卖出 — 五粮液(000858)
msg = f"""🔴 模拟卖出 — {name}
日期: {today}
卖出价格: {price:.2f}
@ -249,15 +338,17 @@ MA20死叉触发已平仓。
print(msg)
def cmd_status():
def cmd_status(code=None):
"""查看当前账户状态"""
acct = load_account()
acct = load_account(code)
name = acct.get("stock", "五粮液")
stock_code = code or acct.get("stock_code", "000858")
total_shares = sum(p["shares"] for p in acct["positions"])
total_cost = sum(p["shares"] * p["avg_cost"] for p in acct["positions"])
current_price, _ = get_current_price("000858")
current_price, _ = get_current_price(stock_code)
print(f"\n{'='*50}")
print(f"小唯模拟账户 — 五粮液(000858) MA20突破策略")
print(f"小唯模拟账户 — {name} MA20突破策略")
print(f"{'='*50}")
print(f"初始资金: {acct['initial_capital']:.2f}")
print(f"当前现金: {acct['current_capital']:.2f}")
@ -293,13 +384,15 @@ def cmd_status():
return acct
def cmd_report():
def cmd_report(code=None):
"""生成每日账户报告"""
acct = load_account()
acct = load_account(code)
stats = acct["stats"]
name = acct.get("stock", "五粮液")
stock_code = code or acct.get("stock_code", "000858")
total_shares = sum(p["shares"] for p in acct["positions"])
total_cost = sum(p["shares"] * p["avg_cost"] for p in acct["positions"])
current_price, _ = get_current_price("000858")
current_price, _ = get_current_price(stock_code)
if total_shares > 0 and current_price:
market_val = total_shares * current_price
@ -326,19 +419,23 @@ def cmd_report():
if __name__ == "__main__":
if len(sys.argv) < 2:
cmd_status()
elif sys.argv[1] == "status":
cmd_status()
code = None
if "--code" in sys.argv:
idx = sys.argv.index("--code")
if idx + 1 < len(sys.argv):
code = sys.argv[idx + 1]
if len(sys.argv) < 2 or sys.argv[1] == "status":
cmd_status(code)
elif sys.argv[1] == "buy" and len(sys.argv) >= 3:
cmd_buy(sys.argv[2])
cmd_buy(sys.argv[2], code=code)
elif sys.argv[1] == "sell" and len(sys.argv) >= 3:
cmd_sell(sys.argv[2])
cmd_sell(sys.argv[2], code=code)
elif sys.argv[1] == "report":
cmd_report()
cmd_report(code)
else:
print("用法:")
print(" python3 stock_paper.py status # 账户状态")
print(" python3 stock_paper.py buy <价格> # 模拟买入")
print(" python3 stock_paper.py sell <价格> # 模拟卖出")
print(" python3 stock_paper.py report # 每日报告")
print(" python3 stock_paper.py status [--code 000001]")
print(" python3 stock_paper.py buy <价格> [--code 000001]")
print(" python3 stock_paper.py sell <价格> [--code 000001]")
print(" python3 stock_paper.py report [--code 000001]")

View File

@ -223,14 +223,11 @@ def send_daily_signal(code="000858", auto_trade=False):
if auto_trade:
import stock_paper
# v2弱势行业金叉半仓参与2026-08-01 回测优化:全拦截损失 α,半仓最优)
industry = STOCK_NAMES.get(code, "")
weak_industry = {"白酒", "医药", "通信", "汽车", "地产"} # 与 stock_portfolio.WEAK_TREND_INDUSTRIES 同步
if sig["signal"] == "BUY" and industry in weak_industry:
action, detail = ("BUY_HALF", f"弱势行业({industry}动量负)金叉,半仓买入")
msg += f"\n\n📝 模拟账户: [⚠️ {action}] {detail}"
else:
action, detail = stock_paper.execute_signal(sig)
msg += f"\n\n📝 模拟账户: [{action}] {detail}"
# 注意industry 应该是行业名(如"白酒"),不是股票名(如"五粮液"
# stock_signal.py 的 STOCK_NAMES 映射的是股票名,不是行业
# 弱势行业判断已在 stock_paper.execute_signal 内部完成,这里直接调用
action, detail = stock_paper.execute_signal(sig)
msg += f"\n\n📝 模拟账户: [{action}] {detail}"
send_feishu(msg)
print(msg)