diff --git a/scripts/stock_paper.py b/scripts/stock_paper.py index acc781b5..499bf1d7 100644 --- a/scripts/stock_paper.py +++ b/scripts/stock_paper.py @@ -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 # 每日报告") \ No newline at end of file + 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]") \ No newline at end of file diff --git a/scripts/stock_signal.py b/scripts/stock_signal.py index 76e75133..043576f1 100644 --- a/scripts/stock_signal.py +++ b/scripts/stock_signal.py @@ -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)