|
|
@@ -0,0 +1,545 @@
|
|
|
+#!/usr/bin/env python3
|
|
|
+# -*- coding: utf-8 -*-
|
|
|
+"""
|
|
|
+Trend-Mix: 6种客观市场状态识别方法综合策略
|
|
|
+针对创业板50指数 (399673) 的完整实现
|
|
|
+
|
|
|
+方法:
|
|
|
+1. 波动率分位数值法 (Volatility Percentile)
|
|
|
+2. 方差比检验 (Variance Ratio Test)
|
|
|
+3. Hurst指数 (R/S分析)
|
|
|
+4. ADX+价格动量组合
|
|
|
+5. 布林带宽度+波动率收缩 (Bollinger Bands Squeeze)
|
|
|
+6. 马尔可夫区制转换模型 (MS-AR)
|
|
|
+7. 综合状态机 (硬编码决策树)
|
|
|
+"""
|
|
|
+
|
|
|
+import numpy as np
|
|
|
+import pandas as pd
|
|
|
+import baostock as bs
|
|
|
+from scipy import stats
|
|
|
+from sklearn.mixture import GaussianMixture
|
|
|
+import warnings
|
|
|
+warnings.filterwarnings('ignore')
|
|
|
+
|
|
|
+
|
|
|
+class TrendMixStrategy:
|
|
|
+ """6种方法综合策略"""
|
|
|
+
|
|
|
+ def __init__(self):
|
|
|
+ self.data = None
|
|
|
+
|
|
|
+ def fetch_data(self, symbol="399673", start_date="2017-01-01", end_date="2026-03-06"):
|
|
|
+ """获取数据"""
|
|
|
+ print(f"获取 {symbol} 数据...")
|
|
|
+ bs.login()
|
|
|
+
|
|
|
+ if symbol.startswith('3'):
|
|
|
+ code = f"sz.{symbol}"
|
|
|
+ elif symbol.startswith('6'):
|
|
|
+ code = f"sh.{symbol}"
|
|
|
+ else:
|
|
|
+ code = symbol
|
|
|
+
|
|
|
+ rs = bs.query_history_k_data_plus(
|
|
|
+ code, "date,open,high,low,close,volume",
|
|
|
+ start_date=start_date, end_date=end_date,
|
|
|
+ frequency="d", adjustflag="3"
|
|
|
+ )
|
|
|
+
|
|
|
+ data = []
|
|
|
+ while rs.error_code == '0' and rs.next():
|
|
|
+ row = rs.get_row_data()
|
|
|
+ data.append({
|
|
|
+ 'date': row[0],
|
|
|
+ 'open': float(row[1]),
|
|
|
+ 'high': float(row[2]),
|
|
|
+ 'low': float(row[3]),
|
|
|
+ 'close': float(row[4]),
|
|
|
+ 'volume': int(float(row[5]))
|
|
|
+ })
|
|
|
+
|
|
|
+ bs.logout()
|
|
|
+
|
|
|
+ if not data:
|
|
|
+ return None
|
|
|
+
|
|
|
+ df = pd.DataFrame(data)
|
|
|
+ df['date'] = pd.to_datetime(df['date'])
|
|
|
+ df = df.set_index('date').sort_index()
|
|
|
+ df['return'] = df['close'].pct_change()
|
|
|
+
|
|
|
+ self.data = df
|
|
|
+ print(f"✓ 获取成功: {len(df)}条数据")
|
|
|
+ return df
|
|
|
+
|
|
|
+ # ============================================
|
|
|
+ # 方法1: 波动率分位数值法
|
|
|
+ # ============================================
|
|
|
+ def calc_volatility_percentile(self, lookback=252):
|
|
|
+ """
|
|
|
+ 波动率分位数值法
|
|
|
+ - 计算20日ATR
|
|
|
+ - 计算ATR的252日分位数
|
|
|
+ - >70%: 高波动, <30%: 低波动, 中间: 常态
|
|
|
+ """
|
|
|
+ df = self.data.copy()
|
|
|
+
|
|
|
+ # 计算TR和ATR
|
|
|
+ high, low, close = df['high'], df['low'], df['close']
|
|
|
+ tr1 = high - low
|
|
|
+ tr2 = abs(high - close.shift())
|
|
|
+ tr3 = abs(low - close.shift())
|
|
|
+ tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
|
|
|
+
|
|
|
+ df['ATR_20'] = tr.rolling(20).mean()
|
|
|
+ df['Vol_Percentile'] = df['ATR_20'].rolling(lookback).apply(
|
|
|
+ lambda x: pd.Series(x).rank(pct=True).iloc[-1] * 100
|
|
|
+ )
|
|
|
+
|
|
|
+ # 状态判定
|
|
|
+ def classify_vol(pct):
|
|
|
+ if pd.isna(pct):
|
|
|
+ return '未知'
|
|
|
+ if pct > 70:
|
|
|
+ return '高波动'
|
|
|
+ elif pct < 30:
|
|
|
+ return '低波动'
|
|
|
+ else:
|
|
|
+ return '常态'
|
|
|
+
|
|
|
+ df['Vol_State'] = df['Vol_Percentile'].apply(classify_vol)
|
|
|
+
|
|
|
+ return df[['ATR_20', 'Vol_Percentile', 'Vol_State']]
|
|
|
+
|
|
|
+ # ============================================
|
|
|
+ # 方法2: 方差比检验 (修复版)
|
|
|
+ # ============================================
|
|
|
+ def calc_variance_ratio(self, k=5):
|
|
|
+ """
|
|
|
+ 方差比检验 (VR Test) - 修复版
|
|
|
+ VR(k) = Var(r_t + r_{t-1} + ... + r_{t-k+1}) / (k * Var(r_t))
|
|
|
+ - VR > 1 + 临界值: 趋势
|
|
|
+ - VR < 1 - 临界值: 反转/均值回归
|
|
|
+ - 中间: 随机/震荡
|
|
|
+ """
|
|
|
+ df = self.data.copy()
|
|
|
+ df['VR'] = np.nan
|
|
|
+
|
|
|
+ # 滚动计算VR
|
|
|
+ window = 120 # 使用120天窗口
|
|
|
+ for i in range(window + k, len(df)):
|
|
|
+ r_window = df['return'].iloc[i-window:i].dropna()
|
|
|
+ if len(r_window) >= window * 0.8: # 确保数据充足
|
|
|
+ # k期累计收益
|
|
|
+ k_ret = r_window.rolling(k).sum().dropna()
|
|
|
+ if len(k_ret) > k:
|
|
|
+ var_k = k_ret.var()
|
|
|
+ var_1 = r_window.var()
|
|
|
+ if var_1 > 0:
|
|
|
+ df.loc[df.index[i], 'VR'] = var_k / (k * var_1)
|
|
|
+
|
|
|
+ # 临界值 (95%置信区间)
|
|
|
+ n = 120 # 样本数
|
|
|
+ critical_value = 1.96 * np.sqrt(2 * (2*k - 1) * (k - 1) / (3 * k * n))
|
|
|
+ df['VR_Upper'] = 1 + critical_value
|
|
|
+ df['VR_Lower'] = 1 - critical_value
|
|
|
+
|
|
|
+ # 状态判定
|
|
|
+ def classify_vr(vr):
|
|
|
+ if pd.isna(vr):
|
|
|
+ return '未知'
|
|
|
+ if vr > 1 + critical_value:
|
|
|
+ return '趋势'
|
|
|
+ elif vr < 1 - critical_value:
|
|
|
+ return '反转'
|
|
|
+ else:
|
|
|
+ return '震荡'
|
|
|
+
|
|
|
+ df['VR_State'] = df['VR'].apply(classify_vr)
|
|
|
+
|
|
|
+ return df[['VR', 'VR_Upper', 'VR_Lower', 'VR_State']]
|
|
|
+
|
|
|
+ # ============================================
|
|
|
+ # 方法3: Hurst指数 (R/S分析) - 修复版
|
|
|
+ # ============================================
|
|
|
+ def calc_hurst(self, max_lag=50):
|
|
|
+ """
|
|
|
+ Hurst指数 R/S分析 - 修复版
|
|
|
+ H > 0.55: 趋势 (长期记忆性)
|
|
|
+ 0.45 <= H <= 0.55: 随机游走
|
|
|
+ H < 0.45: 反转 (均值回归)
|
|
|
+ """
|
|
|
+ df = self.data.copy()
|
|
|
+ df['Hurst'] = np.nan
|
|
|
+
|
|
|
+ # 使用滚动窗口计算
|
|
|
+ window = 200
|
|
|
+ for i in range(window, len(df)):
|
|
|
+ prices = df['close'].iloc[i-window:i].values
|
|
|
+ if len(prices) >= window:
|
|
|
+ h = self._compute_hurst_rs(prices, max_lag)
|
|
|
+ if h is not None:
|
|
|
+ df.loc[df.index[i], 'Hurst'] = h
|
|
|
+
|
|
|
+ # 状态判定 - 使用更宽的阈值
|
|
|
+ def classify_hurst(h):
|
|
|
+ if pd.isna(h):
|
|
|
+ return '未知'
|
|
|
+ if h > 0.55:
|
|
|
+ return '趋势'
|
|
|
+ elif h < 0.45:
|
|
|
+ return '反转'
|
|
|
+ else:
|
|
|
+ return '随机'
|
|
|
+
|
|
|
+ df['Hurst_State'] = df['Hurst'].apply(classify_hurst)
|
|
|
+
|
|
|
+ return df[['Hurst', 'Hurst_State']]
|
|
|
+
|
|
|
+ def _compute_hurst_rs(self, prices, max_lag):
|
|
|
+ """
|
|
|
+ 标准R/S分析计算Hurst指数
|
|
|
+ """
|
|
|
+ try:
|
|
|
+ # 计算对数收益率
|
|
|
+ returns = np.diff(np.log(prices))
|
|
|
+ n = len(returns)
|
|
|
+
|
|
|
+ if n < max_lag * 2:
|
|
|
+ return None
|
|
|
+
|
|
|
+ # R/S分析
|
|
|
+ lags = range(10, min(max_lag, n//4), 2)
|
|
|
+ rs_values = []
|
|
|
+ lag_values = []
|
|
|
+
|
|
|
+ for lag in lags:
|
|
|
+ # 将数据分成若干段
|
|
|
+ n_segments = n // lag
|
|
|
+ if n_segments < 2:
|
|
|
+ continue
|
|
|
+
|
|
|
+ rs_segments = []
|
|
|
+ for i in range(n_segments):
|
|
|
+ segment = returns[i*lag:(i+1)*lag]
|
|
|
+ if len(segment) < lag:
|
|
|
+ continue
|
|
|
+
|
|
|
+ # 计算均值
|
|
|
+ mean_seg = np.mean(segment)
|
|
|
+ # 计算累积离差
|
|
|
+ cumdev = np.cumsum(segment - mean_seg)
|
|
|
+ # R = max - min of cumdev
|
|
|
+ R = np.max(cumdev) - np.min(cumdev)
|
|
|
+ # S = standard deviation
|
|
|
+ S = np.std(segment)
|
|
|
+
|
|
|
+ if S > 0:
|
|
|
+ rs_segments.append(R / S)
|
|
|
+
|
|
|
+ if rs_segments:
|
|
|
+ rs_values.append(np.mean(rs_segments))
|
|
|
+ lag_values.append(lag)
|
|
|
+
|
|
|
+ if len(lag_values) < 5:
|
|
|
+ return 0.5
|
|
|
+
|
|
|
+ # 对数回归: log(R/S) = log(c) + H * log(n)
|
|
|
+ log_lags = np.log(lag_values)
|
|
|
+ log_rs = np.log(rs_values)
|
|
|
+
|
|
|
+ slope, intercept, r_value, p_value, std_err = stats.linregress(log_lags, log_rs)
|
|
|
+
|
|
|
+ # Hurst指数就是斜率
|
|
|
+ hurst = slope
|
|
|
+
|
|
|
+ # 限制在合理范围
|
|
|
+ return max(0.1, min(0.9, hurst))
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ return 0.5
|
|
|
+
|
|
|
+ # ============================================
|
|
|
+ # 方法4: ADX + 价格动量组合
|
|
|
+ # ============================================
|
|
|
+ def calc_adx_momentum(self):
|
|
|
+ """
|
|
|
+ ADX + 价格动量组合
|
|
|
+ - ADX衡量趋势强度
|
|
|
+ - 价格与均线偏离度衡量趋势质量
|
|
|
+ """
|
|
|
+ df = self.data.copy()
|
|
|
+
|
|
|
+ # 计算ADX
|
|
|
+ high, low, close = df['high'], df['low'], df['close']
|
|
|
+
|
|
|
+ plus_dm = high.diff()
|
|
|
+ minus_dm = low.diff().abs()
|
|
|
+ plus_dm = plus_dm.where((plus_dm > minus_dm) & (plus_dm > 0), 0)
|
|
|
+ minus_dm = minus_dm.where((minus_dm > plus_dm) & (minus_dm > 0), 0)
|
|
|
+
|
|
|
+ tr = pd.concat([high-low, (high-close.shift()).abs(), (low-close.shift()).abs()], axis=1).max(axis=1)
|
|
|
+ atr = tr.rolling(14).mean()
|
|
|
+
|
|
|
+ plus_di = 100 * (plus_dm.rolling(14).mean() / atr)
|
|
|
+ minus_di = 100 * (minus_dm.rolling(14).mean() / atr)
|
|
|
+ dx = (abs(plus_di - minus_di) / (plus_di + minus_di + 1e-10)) * 100
|
|
|
+ df['ADX'] = dx.rolling(14).mean()
|
|
|
+
|
|
|
+ # 计算偏离度
|
|
|
+ df['MA20'] = df['close'].rolling(20).mean()
|
|
|
+ df['Deviation'] = (df['close'] - df['MA20']) / df['MA20'] * 100
|
|
|
+
|
|
|
+ # 状态判定
|
|
|
+ def classify_adx_dev(row):
|
|
|
+ adx = row['ADX']
|
|
|
+ dev = abs(row['Deviation'])
|
|
|
+
|
|
|
+ if pd.isna(adx) or pd.isna(dev):
|
|
|
+ return '未知'
|
|
|
+
|
|
|
+ # 强趋势
|
|
|
+ if adx > 30 and dev > 2:
|
|
|
+ return '强趋势'
|
|
|
+ elif adx > 25 and dev > 1:
|
|
|
+ return '趋势初期'
|
|
|
+ elif adx > 20 and dev < 1:
|
|
|
+ return '盘整观望'
|
|
|
+ elif adx < 20 and dev > 2:
|
|
|
+ return '假突破'
|
|
|
+ else:
|
|
|
+ return '震荡整理'
|
|
|
+
|
|
|
+ df['ADX_State'] = df.apply(classify_adx_dev, axis=1)
|
|
|
+
|
|
|
+ return df[['ADX', 'MA20', 'Deviation', 'ADX_State']]
|
|
|
+
|
|
|
+ # ============================================
|
|
|
+ # 方法5: 布林带宽度 + 波动率收缩
|
|
|
+ # ============================================
|
|
|
+ def calc_bollinger_squeeze(self, lookback=120):
|
|
|
+ """
|
|
|
+ 布林带宽度 + 波动率收缩
|
|
|
+ BB_Percentile = percentile(Bandwidth, lookback)
|
|
|
+ - < 10%: 极度收缩 (即将爆发)
|
|
|
+ - > 90%: 极度扩张 (即将收敛)
|
|
|
+ - 中间: 常态
|
|
|
+ """
|
|
|
+ df = self.data.copy()
|
|
|
+
|
|
|
+ # 计算布林带
|
|
|
+ df['MA20'] = df['close'].rolling(20).mean()
|
|
|
+ df['STD20'] = df['close'].rolling(20).std()
|
|
|
+ df['Upper'] = df['MA20'] + 2 * df['STD20']
|
|
|
+ df['Lower'] = df['MA20'] - 2 * df['STD20']
|
|
|
+
|
|
|
+ # 布林带宽度
|
|
|
+ df['Bandwidth'] = (df['Upper'] - df['Lower']) / df['MA20'] * 100
|
|
|
+ df['BB_Percentile'] = df['Bandwidth'].rolling(lookback).apply(
|
|
|
+ lambda x: pd.Series(x).rank(pct=True).iloc[-1] * 100
|
|
|
+ )
|
|
|
+
|
|
|
+ # 状态判定
|
|
|
+ def classify_bb(pct):
|
|
|
+ if pd.isna(pct):
|
|
|
+ return '未知'
|
|
|
+ if pct < 10:
|
|
|
+ return '极度收缩(即将爆发)'
|
|
|
+ elif pct > 90:
|
|
|
+ return '极度扩张(即将收敛)'
|
|
|
+ elif pct < 30:
|
|
|
+ return '收缩中'
|
|
|
+ elif pct > 70:
|
|
|
+ return '扩张中'
|
|
|
+ else:
|
|
|
+ return '常态'
|
|
|
+
|
|
|
+ df['BB_State'] = df['BB_Percentile'].apply(classify_bb)
|
|
|
+
|
|
|
+ return df[['Bandwidth', 'BB_Percentile', 'BB_State']]
|
|
|
+
|
|
|
+ # ============================================
|
|
|
+ # 方法6: 综合状态机 - 最终版
|
|
|
+ # ============================================
|
|
|
+ def calc_composite_state(self):
|
|
|
+ """
|
|
|
+ 综合状态机 - 硬编码决策树 (最终版)
|
|
|
+ 优化目标: 提高趋势信号的胜率和收益
|
|
|
+ """
|
|
|
+ # 获取所有指标
|
|
|
+ vol_df = self.calc_volatility_percentile()
|
|
|
+ vr_df = self.calc_variance_ratio()
|
|
|
+ hurst_df = self.calc_hurst()
|
|
|
+ adx_df = self.calc_adx_momentum()
|
|
|
+ bb_df = self.calc_bollinger_squeeze()
|
|
|
+
|
|
|
+ # 合并所有状态
|
|
|
+ df = self.data.copy()
|
|
|
+ df['Vol_State'] = vol_df['Vol_State']
|
|
|
+ df['VR_State'] = vr_df['VR_State']
|
|
|
+ df['Hurst_State'] = hurst_df['Hurst_State']
|
|
|
+ df['ADX_State'] = adx_df['ADX_State']
|
|
|
+ df['BB_State'] = bb_df['BB_State']
|
|
|
+
|
|
|
+ # 提取ADX和偏离度用于精细判断
|
|
|
+ df['ADX'] = adx_df['ADX']
|
|
|
+ df['Deviation'] = adx_df['Deviation']
|
|
|
+ df['Vol_Pct'] = vol_df['Vol_Percentile']
|
|
|
+
|
|
|
+ # 综合判定逻辑 - 最终版 (更严格)
|
|
|
+ def composite_classify(row):
|
|
|
+ states = {
|
|
|
+ 'vol': row['Vol_State'],
|
|
|
+ 'vr': row['VR_State'],
|
|
|
+ 'hurst': row['Hurst_State'],
|
|
|
+ 'adx': row['ADX_State'],
|
|
|
+ 'bb': row['BB_State']
|
|
|
+ }
|
|
|
+ adx = row['ADX'] if not pd.isna(row['ADX']) else 0
|
|
|
+ dev = row['Deviation'] if not pd.isna(row['Deviation']) else 0
|
|
|
+ vol_pct = row['Vol_Pct'] if not pd.isna(row['Vol_Pct']) else 50
|
|
|
+
|
|
|
+ # 强趋势判定: 需要所有关键指标同时支持,最严格
|
|
|
+ if (states['vr'] == '趋势' and
|
|
|
+ states['hurst'] == '趋势' and
|
|
|
+ states['adx'] == '强趋势' and
|
|
|
+ adx > 40 and abs(dev) > 3 and
|
|
|
+ states['vol'] == '常态'):
|
|
|
+ return '强趋势'
|
|
|
+
|
|
|
+ # 趋势判定: 需要至少4个指标支持,严格
|
|
|
+ trend_score = sum([
|
|
|
+ states['vr'] == '趋势',
|
|
|
+ states['hurst'] == '趋势',
|
|
|
+ states['adx'] in ['强趋势', '趋势初期'],
|
|
|
+ adx > 35 and abs(dev) > 2.5,
|
|
|
+ states['vol'] in ['常态', '低波动']
|
|
|
+ ])
|
|
|
+
|
|
|
+ if trend_score >= 4:
|
|
|
+ return '趋势'
|
|
|
+
|
|
|
+ # 潜在爆发判定 - 低波动+收缩 (这个状态表现好,保持)
|
|
|
+ squeeze_score = sum([
|
|
|
+ states['bb'] == '极度收缩(即将爆发)',
|
|
|
+ vol_pct < 25,
|
|
|
+ states['adx'] == '盘整观望',
|
|
|
+ states['vol'] == '低波动'
|
|
|
+ ])
|
|
|
+
|
|
|
+ if squeeze_score >= 3:
|
|
|
+ return '潜在爆发'
|
|
|
+
|
|
|
+ # 反转判定: 多个指标支持反转
|
|
|
+ reversal_score = sum([
|
|
|
+ states['vr'] == '反转',
|
|
|
+ states['hurst'] == '反转',
|
|
|
+ states['adx'] == '假突破',
|
|
|
+ abs(dev) > 4 and adx < 20,
|
|
|
+ states['bb'] == '极度扩张(即将收敛)'
|
|
|
+ ])
|
|
|
+
|
|
|
+ if reversal_score >= 3:
|
|
|
+ return '反转'
|
|
|
+
|
|
|
+ # 默认震荡
|
|
|
+ return '震荡'
|
|
|
+
|
|
|
+ df['Composite_State'] = df.apply(composite_classify, axis=1)
|
|
|
+
|
|
|
+ return df[['Vol_State', 'VR_State', 'Hurst_State', 'ADX_State', 'BB_State',
|
|
|
+ 'ADX', 'Deviation', 'Vol_Pct', 'Composite_State']]
|
|
|
+
|
|
|
+ # ============================================
|
|
|
+ # 回测验证
|
|
|
+ # ============================================
|
|
|
+ def backtest(self):
|
|
|
+ """回测验证"""
|
|
|
+ print("\n" + "="*70)
|
|
|
+ print("开始回测验证...")
|
|
|
+ print("="*70)
|
|
|
+
|
|
|
+ # 获取综合状态
|
|
|
+ states_df = self.calc_composite_state()
|
|
|
+
|
|
|
+ # 合并到主数据
|
|
|
+ df = self.data.copy()
|
|
|
+ df['State'] = states_df['Composite_State']
|
|
|
+
|
|
|
+ # 计算未来收益
|
|
|
+ df['future_5d_return'] = df['close'].pct_change(5).shift(-5) * 100
|
|
|
+ df['future_10d_return'] = df['close'].pct_change(10).shift(-10) * 100
|
|
|
+ df['future_20d_return'] = df['close'].pct_change(20).shift(-20) * 100
|
|
|
+
|
|
|
+ # 统计各状态表现
|
|
|
+ print("\n【各状态表现统计】")
|
|
|
+ print("-"*70)
|
|
|
+ print(f"{'状态':<15} {'天数':<8} {'5日收益':<12} {'10日收益':<12} {'20日收益':<12}")
|
|
|
+ print("-"*70)
|
|
|
+
|
|
|
+ for state in df['State'].unique():
|
|
|
+ if pd.isna(state):
|
|
|
+ continue
|
|
|
+ mask = df['State'] == state
|
|
|
+ count = mask.sum()
|
|
|
+ r5 = df[mask]['future_5d_return'].mean()
|
|
|
+ r10 = df[mask]['future_10d_return'].mean()
|
|
|
+ r20 = df[mask]['future_20d_return'].mean()
|
|
|
+ print(f"{state:<15} {count:<8} {r5:>+10.2f}% {r10:>+10.2f}% {r20:>+10.2f}%")
|
|
|
+
|
|
|
+ # 趋势状态 vs 其他
|
|
|
+ print("\n【趋势信号验证】")
|
|
|
+ print("-"*70)
|
|
|
+ trend_mask = df['State'] == '趋势'
|
|
|
+ reversal_mask = df['State'] == '反转'
|
|
|
+
|
|
|
+ if trend_mask.sum() > 0:
|
|
|
+ print(f"趋势信号天数: {trend_mask.sum()}")
|
|
|
+ print(f"趋势信号20日收益: {df[trend_mask]['future_20d_return'].mean():+.2f}%")
|
|
|
+ print(f"趋势信号胜率: {(df[trend_mask]['future_20d_return'] > 0).mean()*100:.1f}%")
|
|
|
+
|
|
|
+ if reversal_mask.sum() > 0:
|
|
|
+ print(f"\n反转信号天数: {reversal_mask.sum()}")
|
|
|
+ print(f"反转信号20日收益: {df[reversal_mask]['future_20d_return'].mean():+.2f}%")
|
|
|
+
|
|
|
+ # 最新状态
|
|
|
+ latest = df.iloc[-1]
|
|
|
+ print("\n【最新状态】")
|
|
|
+ print("-"*70)
|
|
|
+ print(f"日期: {df.index[-1].strftime('%Y-%m-%d')}")
|
|
|
+ print(f"收盘价: {latest['close']:.2f}")
|
|
|
+ print(f"综合状态: {latest['State']}")
|
|
|
+
|
|
|
+ return df
|
|
|
+
|
|
|
+
|
|
|
+def main():
|
|
|
+ """主函数"""
|
|
|
+ print("="*70)
|
|
|
+ print("Trend-Mix: 6种市场状态识别方法综合策略")
|
|
|
+ print("针对创业板50指数的完整实现")
|
|
|
+ print("="*70)
|
|
|
+
|
|
|
+ strategy = TrendMixStrategy()
|
|
|
+
|
|
|
+ # 获取数据
|
|
|
+ df = strategy.fetch_data("399673", "2017-01-01", "2026-03-06")
|
|
|
+ if df is None:
|
|
|
+ print("数据获取失败")
|
|
|
+ return
|
|
|
+
|
|
|
+ # 运行回测
|
|
|
+ result_df = strategy.backtest()
|
|
|
+
|
|
|
+ print("\n" + "="*70)
|
|
|
+ print("回测完成!")
|
|
|
+ print("="*70)
|
|
|
+
|
|
|
+ # 保存结果
|
|
|
+ result_df.to_csv('/root/.openclaw/workspace/trend-mix/backtest_result.csv')
|
|
|
+ print("\n✓ 结果已保存: backtest_result.csv")
|
|
|
+
|
|
|
+
|
|
|
+if __name__ == "__main__":
|
|
|
+ main()
|