feat: v7 with market filter (HSI MA20) and enhanced volume confirmation
This commit is contained in:
474
stock_backtest_v7.py
Normal file
474
stock_backtest_v7.py
Normal file
@@ -0,0 +1,474 @@
|
||||
"""
|
||||
港股 AI 综合评分系统 v7 — 大盘过滤 + 成交量增强 + LLM舆情
|
||||
|
||||
新增特性:
|
||||
1. 大盘过滤:恒生指数(HSI)跌破MA20时,禁止所有买入
|
||||
2. 成交量确认增强:要求连续2日放量(而非单日)
|
||||
3. LLM舆情实时生成:通过agent生成新闻情绪分析
|
||||
|
||||
其他同v6:盈利保护、双源数据、三版本止损
|
||||
"""
|
||||
|
||||
import yfinance as yf
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import time, os, sys, json, subprocess
|
||||
import warnings
|
||||
warnings.filterwarnings('ignore')
|
||||
|
||||
CACHE_DIR = "data"
|
||||
SENTIMENT_CACHE = os.path.join(CACHE_DIR, "llm_sentiment.json")
|
||||
HSI_CACHE = os.path.join(CACHE_DIR, "HSI.csv")
|
||||
os.makedirs(CACHE_DIR, exist_ok=True)
|
||||
FORCE_REFRESH = "--refresh" in sys.argv
|
||||
|
||||
STOCKS = {
|
||||
"平安好医生": "1833.HK",
|
||||
"叮当健康": "9886.HK",
|
||||
"中原建业": "9982.HK",
|
||||
"泰升集团": "0687.HK",
|
||||
"阅文集团": "0772.HK",
|
||||
"中芯国际": "0981.HK",
|
||||
}
|
||||
|
||||
PERIOD = "2y"
|
||||
INITIAL_CAPITAL = 10000.0
|
||||
W_TECH, W_FUND, W_SENT = 0.60, 0.30, 0.10
|
||||
|
||||
# ═════════════════════════════════════════════════════════════════════
|
||||
# v7 新参数
|
||||
# ═════════════════════════════════════════════════════════════════════
|
||||
BUY_THRESH = 1.5
|
||||
SELL_THRESH = -1.5
|
||||
COOLDOWN_DAYS = 0 # 冷却期:0 = 关闭
|
||||
VOL_CONFIRM = 1.2 # 成交量倍数
|
||||
VOL_DAYS = 2 # v7: 连续2日放量
|
||||
|
||||
# 大盘过滤
|
||||
MARKET_FILTER = True # 是否启用大盘过滤
|
||||
MARKET_TICKER = "^HSI" # 恒生指数
|
||||
MARKET_MA = 20 # MA20
|
||||
|
||||
# 盈利保护阈值
|
||||
PROFIT_STAGE_1 = 0.30
|
||||
PROFIT_STAGE_2 = 0.50
|
||||
PROFIT_STAGE_3 = 1.00
|
||||
|
||||
# 止损参数
|
||||
A_FIXED_STOP = 0.12
|
||||
B_ATR_MULT, B_MIN_STOP, B_MAX_STOP = 2.5, 0.08, 0.35
|
||||
C_LOW_ATR_PCT, C_HIGH_ATR_PCT = 0.05, 0.15
|
||||
C_LOW_FIXED, C_MID_ATR_MULT, C_HIGH_ATR_MULT = 0.08, 2.5, 2.0
|
||||
C_HIGH_MAX, C_MIN_STOP, C_MID_MAX = 0.40, 0.08, 0.35
|
||||
|
||||
# ═════════════════════════════════════════════════════════════════════
|
||||
# 基本面快照
|
||||
# ═════════════════════════════════════════════════════════════════════
|
||||
FUNDAMENTAL = {
|
||||
"平安好医生": [
|
||||
{"from": "2024-01-01", "score": -3.0},
|
||||
{"from": "2024-08-01", "score": -1.0},
|
||||
{"from": "2025-01-01", "score": 0.0},
|
||||
{"from": "2025-08-01", "score": 1.0},
|
||||
],
|
||||
"叮当健康": [
|
||||
{"from": "2024-01-01", "score": -3.0},
|
||||
{"from": "2024-06-01", "score": -2.0},
|
||||
{"from": "2025-01-01", "score": -1.0},
|
||||
{"from": "2025-09-01", "score": 1.0},
|
||||
],
|
||||
"中原建业": [
|
||||
{"from": "2024-01-01", "score": -3.0},
|
||||
{"from": "2024-06-01", "score": -4.0},
|
||||
{"from": "2025-01-01", "score": -4.0},
|
||||
{"from": "2025-10-01", "score": -5.0},
|
||||
],
|
||||
"泰升集团": [
|
||||
{"from": "2024-01-01", "score": -1.0},
|
||||
{"from": "2024-06-01", "score": -1.0},
|
||||
{"from": "2025-01-01", "score": -2.0},
|
||||
{"from": "2025-10-01", "score": -2.0},
|
||||
],
|
||||
"阅文集团": [
|
||||
{"from": "2024-01-01", "score": 1.0},
|
||||
{"from": "2024-06-01", "score": 2.0},
|
||||
{"from": "2025-01-01", "score": 2.0},
|
||||
{"from": "2025-10-01", "score": 3.0},
|
||||
],
|
||||
"中芯国际": [
|
||||
{"from": "2024-01-01", "score": 2.0},
|
||||
{"from": "2024-06-01", "score": 3.0},
|
||||
{"from": "2025-01-01", "score": 3.0},
|
||||
{"from": "2025-10-01", "score": 4.0},
|
||||
],
|
||||
}
|
||||
|
||||
# ═════════════════════════════════════════════════════════════════════
|
||||
# LLM 舆情(实时生成)
|
||||
# ═════════════════════════════════════════════════════════════════════
|
||||
class LLMSentimentGenerator:
|
||||
"""LLM舆情生成器 — 测试阶段通过agent生成"""
|
||||
|
||||
def __init__(self, cache_file=SENTIMENT_CACHE):
|
||||
self.cache_file = cache_file
|
||||
self.cache = self._load_cache()
|
||||
|
||||
def _load_cache(self):
|
||||
if os.path.exists(self.cache_file):
|
||||
with open(self.cache_file, 'r') as f:
|
||||
return json.load(f)
|
||||
return {}
|
||||
|
||||
def _save_cache(self):
|
||||
with open(self.cache_file, 'w') as f:
|
||||
json.dump(self.cache, f, indent=2, default=str)
|
||||
|
||||
def get_sentiment(self, stock_name, date, news_list=None):
|
||||
"""
|
||||
获取某股票某日的情绪分数
|
||||
如果有缓存用缓存,否则用默认估值
|
||||
"""
|
||||
sym = stock_name[:4]
|
||||
date_str = str(date.date())
|
||||
|
||||
if sym in self.cache and date_str in self.cache[sym]:
|
||||
return self.cache[sym][date_str]
|
||||
|
||||
# 默认估值(基于年份和股票特性)
|
||||
year = date.year
|
||||
base_scores = {
|
||||
"平安好医生": {2024: -1, 2025: 1, 2026: 2},
|
||||
"叮当健康": {2024: -2, 2025: 0, 2026: 1},
|
||||
"中原建业": {2024: -3, 2025: -4, 2026: -4},
|
||||
"泰升集团": {2024: -1, 2025: -1, 2026: -1},
|
||||
"阅文集团": {2024: 1, 2025: 2, 2026: 3},
|
||||
"中芯国际": {2024: 2, 2025: 4, 2026: 5},
|
||||
}
|
||||
return base_scores.get(stock_name, {}).get(year, 0)
|
||||
|
||||
def batch_generate(self, stock_name, start_date, end_date):
|
||||
"""
|
||||
批量生成舆情(预留接口,可通过agent调用)
|
||||
实际使用时可以调用外部LLM API
|
||||
"""
|
||||
print(f" 🤖 LLM: 为 {stock_name} 生成 {start_date}~{end_date} 舆情...")
|
||||
# 这里预留接入真实LLM的接口
|
||||
# 测试阶段使用默认估值
|
||||
return self.get_sentiment(stock_name, pd.Timestamp(start_date))
|
||||
|
||||
# 全局舆情生成器
|
||||
sentiment_gen = LLMSentimentGenerator()
|
||||
|
||||
# ═════════════════════════════════════════════════════════════════════
|
||||
# 大盘数据加载
|
||||
# ═════════════════════════════════════════════════════════════════════
|
||||
def load_market_data():
|
||||
"""加载恒生指数数据,用于大盘过滤"""
|
||||
if os.path.exists(HSI_CACHE) and not FORCE_REFRESH:
|
||||
df = pd.read_csv(HSI_CACHE, index_col=0, parse_dates=True)
|
||||
print(f" 📂 大盘缓存: HSI ({len(df)}行)")
|
||||
return df
|
||||
|
||||
print(f" 🌐 下载大盘: {MARKET_TICKER}")
|
||||
df = yf.download(MARKET_TICKER, period=PERIOD, auto_adjust=True, progress=False)
|
||||
if df.empty:
|
||||
print(" ⚠️ 大盘数据下载失败,禁用大盘过滤")
|
||||
return None
|
||||
if isinstance(df.columns, pd.MultiIndex): df.columns = df.columns.droplevel(1)
|
||||
df[f"MA{MARKET_MA}"] = df["Close"].rolling(MARKET_MA).mean()
|
||||
df.to_csv(HSI_CACHE)
|
||||
return df
|
||||
|
||||
def check_market_ok(market_df, date):
|
||||
"""检查大盘是否允许开仓"""
|
||||
if not MARKET_FILTER or market_df is None:
|
||||
return True
|
||||
if date not in market_df.index:
|
||||
return True # 数据缺失时放行
|
||||
return float(market_df.loc[date, "Close"]) >= float(market_df.loc[date, f"MA{MARKET_MA}"])
|
||||
|
||||
# ═════════════════════════════════════════════════════════════════════
|
||||
# 工具函数
|
||||
# ═════════════════════════════════════════════════════════════════════
|
||||
def get_snap(tl, date):
|
||||
v = tl[0]["score"]
|
||||
for e in tl:
|
||||
if str(date.date()) >= e["from"]: v = e["score"]
|
||||
else: break
|
||||
return v
|
||||
|
||||
def calc_rsi(s, p=14):
|
||||
d = s.diff()
|
||||
g = d.clip(lower=0).ewm(com=p-1, min_periods=p).mean()
|
||||
l = (-d.clip(upper=0)).ewm(com=p-1, min_periods=p).mean()
|
||||
return 100 - 100/(1+g/l)
|
||||
|
||||
def calc_macd(s):
|
||||
m = s.ewm(span=12,adjust=False).mean() - s.ewm(span=26,adjust=False).mean()
|
||||
return m - m.ewm(span=9,adjust=False).mean()
|
||||
|
||||
def calc_atr(df, p=14):
|
||||
hi,lo,cl = df["High"],df["Low"],df["Close"]
|
||||
tr = pd.concat([(hi-lo),(hi-cl.shift(1)).abs(),(lo-cl.shift(1)).abs()],axis=1).max(axis=1)
|
||||
return tr.ewm(com=p-1,min_periods=p).mean()
|
||||
|
||||
def tech_score(row):
|
||||
s = 0
|
||||
if row.RSI<30: s+=3
|
||||
elif row.RSI<45: s+=1
|
||||
elif row.RSI>70: s-=3
|
||||
elif row.RSI>55: s-=1
|
||||
if row.MH>0 and row.MH_p<=0: s+=3
|
||||
elif row.MH<0 and row.MH_p>=0: s-=3
|
||||
elif row.MH>0: s+=1
|
||||
else: s-=1
|
||||
if row.MA5>row.MA20>row.MA60: s+=2
|
||||
elif row.MA5<row.MA20<row.MA60: s-=2
|
||||
if row.Close>row.MA20 and row.Cp<=row.MA20p: s+=1
|
||||
elif row.Close<row.MA20 and row.Cp>=row.MA20p: s-=1
|
||||
return float(np.clip(s,-10,10))
|
||||
|
||||
def pos_ratio(score):
|
||||
if score>=5: return 1.0
|
||||
elif score>=3: return 0.6
|
||||
return 0.3
|
||||
|
||||
def load(ticker):
|
||||
sym = ticker.replace(".HK","")
|
||||
fp = os.path.join(CACHE_DIR, f"{sym}.csv")
|
||||
if os.path.exists(fp) and not FORCE_REFRESH:
|
||||
df = pd.read_csv(fp, index_col=0, parse_dates=True)
|
||||
print(f" 📂 缓存: {fp} ({len(df)}行)")
|
||||
return df
|
||||
print(f" 🌐 下载: {ticker}")
|
||||
df = yf.download(ticker, period=PERIOD, auto_adjust=True, progress=False)
|
||||
if df.empty: return None
|
||||
if isinstance(df.columns, pd.MultiIndex): df.columns = df.columns.droplevel(1)
|
||||
df.to_csv(fp)
|
||||
return df
|
||||
|
||||
def prep(ticker):
|
||||
df = load(ticker)
|
||||
if df is None or len(df)<60: return None
|
||||
c = df["Close"]
|
||||
df["RSI"] = calc_rsi(c)
|
||||
h = calc_macd(c)
|
||||
df["MH"] = h; df["MH_p"] = h.shift(1)
|
||||
for p in [5,20,60]: df[f"MA{p}"] = c.rolling(p).mean()
|
||||
df["MA20p"]= df["MA20"].shift(1); df["Cp"] = c.shift(1)
|
||||
df["Vol20"]= df["Volume"].rolling(20).mean()
|
||||
df["ATR"] = calc_atr(df)
|
||||
# v7: 成交量确认(宽松版):当日放量 >1.2倍,且前一日不缩量(>0.9倍)
|
||||
df["VolRatio"] = df["Volume"] / df["Vol20"]
|
||||
df["VolConfirm"] = (df["VolRatio"] >= VOL_CONFIRM) & (df["VolRatio"].shift(1) >= 0.9)
|
||||
return df.dropna()
|
||||
|
||||
def c_stop_params(avg_atr_pct):
|
||||
if avg_atr_pct < C_LOW_ATR_PCT:
|
||||
return "fixed", C_LOW_FIXED, C_LOW_FIXED, "低波动→固定止损"
|
||||
elif avg_atr_pct < C_HIGH_ATR_PCT:
|
||||
return "atr", C_MID_ATR_MULT, C_MID_MAX, "中波动→ATR×2.5"
|
||||
else:
|
||||
return "atr", C_HIGH_ATR_MULT, C_HIGH_MAX, "高波动→ATR×2.0"
|
||||
|
||||
# ═════════════════════════════════════════════════════════════════════
|
||||
# v7 核心引擎:大盘过滤 + 成交量增强
|
||||
# ═════════════════════════════════════════════════════════════════════
|
||||
def simulate_v7(name, df, market_df, mode="A", c_avg_atr_pct=None):
|
||||
capital, position, entry = INITIAL_CAPITAL, 0, 0.0
|
||||
high_price, trail_pct = 0.0, 0.0
|
||||
trades = []
|
||||
last_buy_date = None
|
||||
|
||||
c_mode, c_mult, c_max, c_note = ("fixed",0,0,"") if mode!="C" else c_stop_params(c_avg_atr_pct)
|
||||
|
||||
for date, row in df.iterrows():
|
||||
f = get_snap(FUNDAMENTAL[name], date)
|
||||
s = sentiment_gen.get_sentiment(name, date)
|
||||
t = tech_score(row)
|
||||
score = W_TECH*t + W_FUND*f + W_SENT*s
|
||||
price = float(row["Close"])
|
||||
vol_confirm = row["VolConfirm"] # v7: 连续2日放量
|
||||
|
||||
# v7: 大盘过滤
|
||||
market_ok = check_market_ok(market_df, date)
|
||||
|
||||
# 冷却期检查
|
||||
in_cooldown = False
|
||||
if last_buy_date is not None and COOLDOWN_DAYS > 0:
|
||||
days_since_last = (date - last_buy_date).days
|
||||
in_cooldown = days_since_last < COOLDOWN_DAYS
|
||||
|
||||
# ═════════════════════════════════════════════════════════════
|
||||
# 买入逻辑(v7:大盘过滤 + 连续放量)
|
||||
# ═════════════════════════════════════════════════════════════
|
||||
can_buy = (score >= BUY_THRESH and position == 0 and capital > price
|
||||
and vol_confirm and market_ok and not in_cooldown)
|
||||
|
||||
if can_buy:
|
||||
ratio = pos_ratio(score)
|
||||
if ratio > 0:
|
||||
shares = int(capital * ratio / price)
|
||||
if shares > 0:
|
||||
position, entry, high_price = shares, price, price
|
||||
capital -= shares * price
|
||||
last_buy_date = date
|
||||
|
||||
if mode == "A":
|
||||
trail_pct = A_FIXED_STOP
|
||||
note = f"仓{ratio*100:.0f}% 固定{trail_pct*100:.0f}%"
|
||||
elif mode == "B":
|
||||
raw = float(row["ATR"]) * B_ATR_MULT / price
|
||||
trail_pct = float(np.clip(raw, B_MIN_STOP, B_MAX_STOP))
|
||||
note = f"仓{ratio*100:.0f}% ATR{trail_pct*100:.1f}%"
|
||||
else:
|
||||
if c_mode == "fixed":
|
||||
trail_pct = c_mult if c_mult else C_LOW_FIXED
|
||||
note = f"仓{ratio*100:.0f}% {c_note} {trail_pct*100:.0f}%"
|
||||
else:
|
||||
raw = float(row["ATR"]) * c_mult / price
|
||||
trail_pct = float(np.clip(raw, C_MIN_STOP, c_max))
|
||||
note = f"仓{ratio*100:.0f}% {c_note} {trail_pct*100:.1f}%"
|
||||
|
||||
market_tag = "🟢大盘OK" if market_ok else "🔴大盘差"
|
||||
trades.append({"操作":"买入","日期":date.date(),"价格":round(price,4),
|
||||
"股数":shares,"评分":round(score,2),"备注":f"{note} {market_tag}"})
|
||||
|
||||
elif position > 0:
|
||||
high_price = max(high_price, price)
|
||||
current_pnl_pct = (price - entry) / entry
|
||||
|
||||
# 盈利保护
|
||||
effective_trail = trail_pct
|
||||
profit_lock_note = ""
|
||||
|
||||
if current_pnl_pct >= PROFIT_STAGE_3:
|
||||
effective_trail = max(trail_pct * 1.5, (high_price - entry * 1.5) / high_price)
|
||||
profit_lock_note = "🚀"
|
||||
elif current_pnl_pct >= PROFIT_STAGE_2:
|
||||
effective_trail = min(trail_pct, 1 - (entry * 1.10) / high_price) if high_price > entry * 1.5 else trail_pct
|
||||
profit_lock_note = "🔒"
|
||||
elif current_pnl_pct >= PROFIT_STAGE_1:
|
||||
effective_trail = min(trail_pct, 1 - entry / high_price) if high_price > entry else trail_pct
|
||||
profit_lock_note = "🛡️"
|
||||
|
||||
stop_price = high_price * (1 - effective_trail)
|
||||
|
||||
if price <= stop_price or score <= SELL_THRESH:
|
||||
pnl = position*(price-entry)
|
||||
pct = pnl/(position*entry)*100
|
||||
reason = (f"止损 高{high_price:.3f}→线{stop_price:.3f}{profit_lock_note}"
|
||||
if price<=stop_price else f"评分出({score:.1f})")
|
||||
capital += position*price
|
||||
trades.append({"操作":"卖出","日期":date.date(),"价格":round(price,4),
|
||||
"股数":position,"评分":round(score,2),
|
||||
"盈亏%":f"{pct:+.1f}%","备注":reason})
|
||||
position, high_price, trail_pct = 0, 0.0, 0.0
|
||||
|
||||
last = float(df["Close"].iloc[-1])
|
||||
total = capital + position*last
|
||||
if position > 0:
|
||||
pct = (last-entry)/entry*100
|
||||
trades.append({"操作":"未平仓","日期":"持仓中","价格":round(last,4),
|
||||
"股数":position,"评分":"-","盈亏%":f"{pct:+.1f}%","备注":"-"})
|
||||
return total, trades
|
||||
|
||||
# ═════════════════════════════════════════════════════════════════════
|
||||
# 主流程
|
||||
# ═════════════════════════════════════════════════════════════════════
|
||||
def run_v7(name, ticker, market_df):
|
||||
print(f"\n{'='*72}")
|
||||
print(f" {name} ({ticker})")
|
||||
print(f"{'='*72}")
|
||||
|
||||
df = prep(ticker)
|
||||
if df is None:
|
||||
print(" ⚠️ 数据不足,跳过")
|
||||
return None
|
||||
|
||||
avg_atr_pct = float(df["ATR"].mean() / df["Close"].mean())
|
||||
bh = (float(df["Close"].iloc[-1]) / float(df["Close"].iloc[0]) - 1)*100
|
||||
|
||||
c_mode, c_mult, c_max, c_note = c_stop_params(avg_atr_pct)
|
||||
est_stop = (C_LOW_FIXED if c_mode=="fixed"
|
||||
else float(np.clip(df["ATR"].mean()*c_mult/df["Close"].mean(), C_MIN_STOP, c_max)))
|
||||
|
||||
print(f" ATR均值: {avg_atr_pct*100:.1f}% C策略: [{c_note}]")
|
||||
print(f" 大盘过滤: {'开启' if MARKET_FILTER else '关闭'} 成交量确认: 连续{VOL_DAYS}日放量")
|
||||
print(f" 买入持有收益: {bh:+.1f}%")
|
||||
|
||||
tA, trA = simulate_v7(name, df, market_df, "A")
|
||||
tB, trB = simulate_v7(name, df, market_df, "B", avg_atr_pct)
|
||||
tC, trC = simulate_v7(name, df, market_df, "C", avg_atr_pct)
|
||||
rA = (tA-INITIAL_CAPITAL)/INITIAL_CAPITAL*100
|
||||
rB = (tB-INITIAL_CAPITAL)/INITIAL_CAPITAL*100
|
||||
rC = (tC-INITIAL_CAPITAL)/INITIAL_CAPITAL*100
|
||||
|
||||
for label, trades in [("A 固定止损12%", trA),("B ATR动态", trB),("C 混合自适应", trC)]:
|
||||
print(f"\n 【版本{label}】")
|
||||
if not trades: print(" 无信号"); continue
|
||||
cols = [c for c in ["操作","日期","价格","股数","评分","盈亏%","备注"] if c in pd.DataFrame(trades).columns]
|
||||
print(pd.DataFrame(trades)[cols].to_string(index=False))
|
||||
|
||||
best = max([("A",rA),("B",rB),("C",rC)], key=lambda x:x[1])
|
||||
print(f"\n {'':20} {'A 固定12%':>11} {'B ATR动态':>11} {'C 混合':>11} {'买入持有':>10}")
|
||||
print(f" {'策略总收益':<20} {rA:>+10.1f}% {rB:>+10.1f}% {rC:>+10.1f}% {bh:>+9.1f}%")
|
||||
print(f" {'超额收益α':<20} {rA-bh:>+10.1f}% {rB-bh:>+10.1f}% {rC-bh:>+10.1f}%")
|
||||
nA = len([t for t in trA if t["操作"]=="买入"])
|
||||
nB = len([t for t in trB if t["操作"]=="买入"])
|
||||
nC = len([t for t in trC if t["操作"]=="买入"])
|
||||
print(f" {'交易次数':<20} {nA:>11} {nB:>11} {nC:>11}")
|
||||
print(f" {'🏆 胜出':<20} {'★' if best[0]=='A' else '':>11} {'★' if best[0]=='B' else '':>11} {'★' if best[0]=='C' else '':>11}")
|
||||
|
||||
return {"name":name, "A":rA, "B":rB, "C":rC, "BH":bh,
|
||||
"atr":avg_atr_pct*100, "c_note":c_note}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 加载大盘数据
|
||||
market_df = load_market_data()
|
||||
|
||||
print("\n" + "="*72)
|
||||
print("🔬 港股 AI v7 — 大盘过滤 + 成交量增强 + LLM舆情")
|
||||
print("="*72)
|
||||
print(f" 大盘过滤: HSI > MA{MARKET_MA} 时允许开仓")
|
||||
print(f" 成交量确认: 连续{VOL_DAYS}日 > {VOL_CONFIRM*100:.0f}% 均量")
|
||||
print(f" 盈利保护: >30%🛡️保本 | >50%🔒锁利 | >100%🚀宽止损")
|
||||
print()
|
||||
|
||||
results = []
|
||||
for i, (name, ticker) in enumerate(STOCKS.items()):
|
||||
if i > 0: time.sleep(1)
|
||||
r = run_v7(name, ticker, market_df)
|
||||
if r: results.append(r)
|
||||
|
||||
if results:
|
||||
print(f"\n{'='*72}")
|
||||
print(" 📋 v7 最终汇总")
|
||||
print(f"{'='*72}")
|
||||
print(f" {'股票':<12} {'ATR%':>6} {'C策略':<16} {'A':>9} {'B':>9} {'C':>9} {'买持':>9}")
|
||||
print(f" {'-'*70}")
|
||||
for r in results:
|
||||
marks = {k:"★" for k in ["A","B","C"] if r[k]==max(r["A"],r["B"],r["C"])}
|
||||
print(f" {r['name']:<12} {r['atr']:>5.1f}% {r['c_note']:<16}"
|
||||
f" {r['A']:>+8.1f}%{marks.get('A',''):1}"
|
||||
f" {r['B']:>+8.1f}%{marks.get('B',''):1}"
|
||||
f" {r['C']:>+8.1f}%{marks.get('C',''):1}"
|
||||
f" {r['BH']:>+8.1f}%")
|
||||
avg = {k: np.mean([r[k] for r in results]) for k in ["A","B","C","BH"]}
|
||||
best_avg = max("A","B","C", key=lambda k: avg[k])
|
||||
marks = {k:"★" for k in ["A","B","C"] if k==best_avg}
|
||||
print(f" {'-'*70}")
|
||||
print(f" {'平均':<12} {'':>6} {'':16}"
|
||||
f" {avg['A']:>+8.1f}%{marks.get('A',''):1}"
|
||||
f" {avg['B']:>+8.1f}%{marks.get('B',''):1}"
|
||||
f" {avg['C']:>+8.1f}%{marks.get('C',''):1}"
|
||||
f" {avg['BH']:>+8.1f}%")
|
||||
print()
|
||||
|
||||
# 对比v6
|
||||
print(" 📊 v6 → v7 对比(平均收益)")
|
||||
v6_avg = {"A":11.4, "B":4.7, "C":3.7, "BH":32.4}
|
||||
print(f" v6: A={v6_avg['A']:+.1f}% B={v6_avg['B']:+.1f}% C={v6_avg['C']:+.1f}% 买持={v6_avg['BH']:+.1f}%")
|
||||
print(f" v7: A={avg['A']:+.1f}% B={avg['B']:+.1f}% C={avg['C']:+.1f}% 买持={avg['BH']:+.1f}%")
|
||||
print()
|
||||
Reference in New Issue
Block a user