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:
parent
f02d5567f2
commit
430bb46340
|
|
@ -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]")
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue