xiaowei-system/scripts/stock_global.py

347 lines
14 KiB
Python
Raw Permalink 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
"""
全球多市场模拟交易 — 港股 + 美股(做多方向)
====================================================
2026-08-02 新增(金融工具多样性 — 多市场)
核心逻辑MA20 策略复用到港股/美股(已验证 K 线数据可用):
港股: hk00700 腾讯 / hk03690 美团 / hk09988 阿里(港股通标的)
美股: usAAPL.OQ 苹果 / usTSLA.OQ 特斯拉 / usNVDA.OQ 英伟达
⚠️ 时差注意:美股盘 22:30-05:00北京时间只做日线级扫描收盘后
不做盘中监测。港股盘 9:30-16:00 与 A 股重叠,可盘中。
用法:
python3 stock_global.py status # 全部全球账户状态
python3 stock_global.py scan # 扫描金叉/死叉信号
python3 stock_global.py report # 汇总报告
python3 stock_global.py backtest <market> <code> <name> # 单只回测
"""
import json, os, subprocess, sys, time
from datetime import datetime
from pathlib import Path
HOME = Path.home()
OUTPUT = HOME / ".hermes" / "stock_backtest" / "global_account"
OUTPUT.mkdir(parents=True, exist_ok=True)
# 全球市场标的(已验证 K 线数据可用)
# ⚠️ 2026-08-02 回测教训MA20 在港股/美股整体跑输买入持有(成熟市场趋势强,
# 频繁进出磨损收益)。策略有市场适用边界,不能盲目复制 A 股经验:
# - 买入持有更优: 腾讯(-17α) 美团(-43α) 阿里(-5α) 英伟达(-5α) 苹果(-0.4α)
# - MA20 有正 α 仅: 特斯拉(+11.6α,高波动个股)
# → 港股/美股用"买入持有为主 + 高波动个股 MA20"混合策略:
# strategy = "buyhold"(默认)或 "ma20"(仅特斯拉)
GLOBAL_TARGETS = {
# 港股(腾讯接口 hk 前缀)— 买入持有
"港股腾讯": {"code": "00700", "name": "腾讯控股", "market": "hk", "capital": 100000, "strategy": "buyhold"},
"港股美团": {"code": "03690", "name": "美团", "market": "hk", "capital": 100000, "strategy": "buyhold"},
"港股阿里": {"code": "09988", "name": "阿里巴巴", "market": "hk", "capital": 100000, "strategy": "buyhold"},
# 美股(腾讯接口 usXXX.OQ 格式)— 高波动个股用 MA20
"美股苹果": {"code": "AAPL.OQ", "name": "苹果", "market": "us", "capital": 100000, "strategy": "buyhold"},
"美股特斯拉": {"code": "TSLA.OQ", "name": "特斯拉", "market": "us", "capital": 100000, "strategy": "ma20"},
"美股英伟达": {"code": "NVDA.OQ", "name": "英伟达", "market": "us", "capital": 100000, "strategy": "buyhold"},
}
MA_PERIOD = 20
def get_url(url, timeout=8, enc="utf-8", retries=1):
env = dict(os.environ)
for k in ["http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY"]:
env.pop(k, None)
for attempt in range(1 + retries):
try:
r = subprocess.run(["curl", "-s", "--max-time", str(timeout), "--compressed", url],
capture_output=True, timeout=timeout+2, env=env)
text = r.stdout.decode(enc, errors="ignore")
if text.strip():
return text
except Exception:
pass
if attempt < retries:
time.sleep(5)
return ""
def market_code(market, code):
"""市场 → 腾讯行情代码"""
if market == "hk":
return f"hk{code}"
if market == "us":
return f"us{code}"
return ("sh" if code.startswith(("6", "5")) else "sz") + code
def get_klines(market, code, days=120):
"""获取日K前复权 → [(date, close), ...]"""
mc = market_code(market, code)
today = datetime.now().strftime("%Y-%m-%d")
url = (f"https://web.ifzq.gtimg.cn/appstock/app/fqkline/get?"
f"_var=kline_dayqfq&param={mc},day,2026-01-01,{today},{days},qfq")
raw = get_url(url)
if "=" not in raw:
return []
try:
d = json.loads(raw[raw.index("="):].lstrip("="))
data = d.get("data", {}).get(mc, {})
kl = data.get("qfqday") or data.get("day", [])
return [(row[0], float(row[2])) for row in kl]
except Exception:
return []
def get_quote(market, code):
mc = market_code(market, code)
raw = get_url(f"https://qt.gtimg.cn/q={mc}", enc="gbk")
if "~" not in raw:
return None
parts = raw.split("~")
return {"price": float(parts[3]), "name": parts[1]}
def account_key(market, code):
return f"{market}_{code.replace('.', '_')}"
def account_file(key):
return OUTPUT / f"global_{key}.json"
def load_account(key):
f = account_file(key)
if f.exists():
return json.load(open(f))
return {
"key": key,
"stock": "",
"code": "",
"market": "",
"initial_capital": 100000,
"current_capital": 100000,
"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["key"]), "w") as f:
json.dump(acct, f, ensure_ascii=False, indent=2)
def compute_ma20_signal(klines):
"""金叉/死叉信号:金叉买入(做多)"""
if len(klines) < MA_PERIOD + 2:
return "HOLD", 0, 0
closes = [k[1] for k in klines]
prev_close = closes[-2]
cur_close = closes[-1]
prev_ma20 = sum(closes[-(MA_PERIOD+1):-1]) / MA_PERIOD
cur_ma20 = sum(closes[-MA_PERIOD:]) / MA_PERIOD
prev_above = prev_close > prev_ma20
cur_above = cur_close > cur_ma20
if not prev_above and cur_above:
return "BUY", cur_close, cur_ma20
if prev_above and not cur_above:
return "SELL", cur_close, cur_ma20
return "HOLD", cur_close, cur_ma20
def execute_signal(sig, dry_run=False):
"""信号执行: sig = {"market": "hk", "code": "00700", "signal": "BUY"/"SELL", "close": price}
strategy 支持:
- buyhold: 买入后长期持有(死叉不卖,除非信号标 HOLD_BUYHOLD 强制)
- ma20: 金叉买/死叉卖(仅特斯拉)
"""
market = sig.get("market")
code = sig.get("code")
key = account_key(market, code)
acct = load_account(key)
cfg = next((c for c in GLOBAL_TARGETS.values()
if c["market"] == market and c["code"] == code), {})
strategy = cfg.get("strategy", "buyhold")
if not acct["stock"]:
acct["stock"] = cfg.get("name", "")
acct["code"] = code
acct["market"] = market
acct["strategy"] = strategy
signal = sig.get("signal")
price = sig.get("close", 0)
# 买入持有策略:死叉信号 → 忽略(长期持有,除非持仓已卖出)
if strategy == "buyhold" and signal == "SELL" and acct["positions"]:
return "HOLD", f"买入持有策略:死叉忽略,继续持有 {acct['stock']}"
if signal == "BUY":
if acct["positions"]:
return "SKIP", "已有持仓"
shares = int(acct["current_capital"] // price)
if shares <= 0:
return "SKIP", "资金不足"
if dry_run:
return "BUY", f"[DRY-RUN] 买入 {shares}股 @{price:.2f}"
acct["positions"].append({"shares": shares, "avg_cost": price,
"open_date": datetime.now().strftime("%Y-%m-%d")})
acct["current_capital"] -= shares * price # 2026-08-28 修复:买入扣减现金(之前不扣导致资产虚高)
acct["last_signal"] = f"买入 {shares}股 @{price:.2f}"
acct["last_signal_date"] = datetime.now().strftime("%Y-%m-%d")
save_account(acct)
return "BUY", f"买入 {shares}股 @{price:.2f}"
if signal == "SELL":
if not acct["positions"]:
return "SKIP", "空仓无持仓"
pos = acct["positions"][0]
shares = pos["shares"]
pnl = (price - pos["avg_cost"]) * shares
pnl_pct = pnl / (pos["avg_cost"] * shares) * 100
if dry_run:
return "SELL", f"[DRY-RUN] 卖出 {shares}股 盈亏{pnl:+,.0f} ({pnl_pct:+.2f}%)"
acct["closed_trades"].append({
"shares": shares, "avg_cost": pos["avg_cost"], "close_price": price,
"pnl": round(pnl, 2), "open_date": pos["open_date"],
"close_date": datetime.now().strftime("%Y-%m-%d"),
})
acct["current_capital"] += price * shares # 2026-08-28 修复:卖出回收全部现金(原 +pnl 在买入不扣时才是对的,买入扣款后必须 +卖出收入)
acct["positions"] = []
acct["stats"]["total_trades"] += 1
if pnl >= 0:
acct["stats"]["winning_trades"] += 1
else:
acct["stats"]["losing_trades"] += 1
acct["last_signal"] = f"卖出 盈亏{pnl:+,.0f} ({pnl_pct:+.2f}%)"
acct["last_signal_date"] = datetime.now().strftime("%Y-%m-%d")
save_account(acct)
return "SELL", f"卖出 {shares}股 盈亏{pnl:+,.0f} ({pnl_pct:+.2f}%)"
return "SKIP", f"未知信号 {signal}"
def scan_signals():
results = []
for key, cfg in GLOBAL_TARGETS.items():
klines = get_klines(cfg["market"], cfg["code"])
if not klines:
results.append({"key": key, "market": cfg["market"], "code": cfg["code"],
"name": cfg["name"], "signal": "DATA_ERR", "close": 0})
continue
sig, close, ma20 = compute_ma20_signal(klines)
dev = (close - ma20) / ma20 * 100 if ma20 else 0
# buyhold 策略:死叉不提示卖出,只提示 BUY建仓持仓状态由账户决定
if cfg.get("strategy") == "buyhold" and sig == "SELL":
sig = "HOLD"
results.append({"key": key, "market": cfg["market"], "code": cfg["code"],
"name": cfg["name"], "signal": sig, "close": close,
"ma20": ma20, "dev": dev})
return results
def backtest(market, code, name):
"""MA20 回测(做多方向)"""
klines = get_klines(market, code, 120)
if len(klines) < MA_PERIOD + 2:
return None
closes = [k[1] for k in klines]
dates = [k[0] for k in klines]
capital = 100000
in_pos = False
buy_price = 0
buy_date = ""
trades = []
for i in range(MA_PERIOD, len(closes)):
prev_close = closes[i-1]
cur_close = closes[i]
prev_ma20 = sum(closes[i-MA_PERIOD:i]) / MA_PERIOD
cur_ma20 = sum(closes[i-MA_PERIOD+1:i+1]) / MA_PERIOD
prev_above = prev_close > prev_ma20
cur_above = cur_close > cur_ma20
if not prev_above and cur_above and not in_pos:
in_pos = True
buy_price = cur_close
buy_date = dates[i]
elif prev_above and not cur_above and in_pos:
pnl = (cur_close - buy_price) * int(100000 / buy_price)
trades.append({"buy": buy_date, "sell": dates[i], "buy_p": buy_price,
"sell_p": cur_close, "pnl": pnl})
capital += pnl
in_pos = False
total_pnl = capital - 100000
win = sum(1 for t in trades if t["pnl"] > 0)
# 买入持有对比
buyhold = (closes[-1] - closes[MA_PERIOD]) / closes[MA_PERIOD] * 100
return {
"code": code, "name": name, "market": market,
"strategy_return": total_pnl / 100000 * 100,
"buyhold_return": buyhold,
"alpha": total_pnl / 100000 * 100 - buyhold,
"total_trades": len(trades),
"win_rate": win / len(trades) * 100 if trades else 0,
"total_pnl": total_pnl,
"in_position": in_pos,
"last_trades": trades[-3:],
}
def cmd_status():
print(f"小唯全球市场模拟 — {datetime.now().strftime('%Y-%m-%d %H:%M')}")
print("=" * 52)
total_cap = 0
for key, cfg in GLOBAL_TARGETS.items():
acct = load_account(account_key(cfg["market"], cfg["code"]))
total_cap += acct["current_capital"]
m = {"hk": "港股", "us": "美股"}.get(cfg["market"], cfg["market"])
pos = acct["positions"]
if pos:
p = pos[0]
print(f"{m}·{cfg['name']}】🟢持仓 {p['shares']}股 @{p['avg_cost']:.2f}")
else:
print(f"{m}·{cfg['name']}】⚪空仓 资金{acct['current_capital']:,.0f}")
print(f"总资金: {total_cap:,.0f}")
def cmd_report():
print(f"📊 全球市场日报 {datetime.now().strftime('%Y-%m-%d')}")
total_cap = 0
total_win = total_lose = 0
for key, cfg in GLOBAL_TARGETS.items():
acct = load_account(account_key(cfg["market"], cfg["code"]))
total_cap += acct["current_capital"]
total_win += acct["stats"]["winning_trades"]
total_lose += acct["stats"]["losing_trades"]
m = {"hk": "港股", "us": "美股"}.get(cfg["market"], cfg["market"])
mark = "🟢" if acct["positions"] else ""
print(f"{mark} {m}·{cfg['name']}: 资产{acct['current_capital']:,.0f} "
f"(交易{acct['stats']['total_trades']}{acct['stats']['winning_trades']}{acct['stats']['losing_trades']})")
print(f"💰 总资产: {total_cap:,.0f}")
if total_win + total_lose > 0:
print(f"📈 胜率: {total_win/(total_win+total_lose)*100:.0f}%")
if __name__ == "__main__":
cmd = sys.argv[1] if len(sys.argv) > 1 else "status"
if cmd == "status":
cmd_status()
elif cmd == "report":
cmd_report()
elif cmd == "scan":
for s in scan_signals():
mark = {"BUY": "🟢金叉", "SELL": "🔴死叉", "HOLD": "⚪持有", "DATA_ERR": "❌数据"}.get(s["signal"], "?")
print(f"{mark} {s['market']}·{s['name']}: 收盘{s.get('close',0):.2f} MA20{s.get('ma20',0):.2f} 偏离{s.get('dev',0):+.2f}%")
elif cmd == "backtest" and len(sys.argv) >= 4:
market = sys.argv[2]
r = backtest(market, sys.argv[3], sys.argv[4] if len(sys.argv) > 4 else sys.argv[3])
if r:
print(f"MA20回测 {r['name']}({r['code']}) [{r['market']}]")
print(f" 策略收益: {r['strategy_return']:+.2f}% | 买入持有: {r['buyhold_return']:+.2f}% | α: {r['alpha']:+.2f}%")
print(f" 交易{r['total_trades']}次 | 胜率{r['win_rate']:.0f}% | 总盈亏{r['total_pnl']:+,.0f}")
if r["last_trades"]:
print(" 最近交易:")
for t in r["last_trades"]:
print(f" {t['buy']} 买@{t['buy_p']:.2f}{t['sell']} 卖@{t['sell_p']:.2f} 盈亏{t['pnl']:+,.0f}")
else:
print("数据不足")
else:
print(__doc__)