refactor: 清理测试文件和2019相关代码
- 删除测试脚本: test_2026_classification.py, test_lagllama.py, test_timesfm.py, test_ttm.py - 删除2019相关文件: analyze_2019_*, apply_2019_*, dlinear_2019_*, three_models_2019_* - 添加集成模型相关代码和分析脚本 - 添加N-BEATS模型和CatBoost分类模型
This commit is contained in:
1 parent
c74f4f6949
commit
1bd9d25f60
33 files changed
+6557
-233
No files matched your search
@@ -0,0 +1,454 @@
|
||||
"""五模型集成预测:DLinear + N-BEATS + TTM-R2 + TimesFM 2.5 + CatBoost"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
import torch
|
||||
import pickle
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import pandas_ta as ta
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
from catboost import CatBoostClassifier
|
||||
from data.fetcher import BinanceDataFetcher
|
||||
|
||||
|
||||
# ==================== 预处理器定义 ====================
|
||||
|
||||
class ClassificationPreprocessor:
|
||||
"""DLinear分类预处理器"""
|
||||
def __init__(self, lookback=168, forecast_horizon=1):
|
||||
self.lookback = lookback
|
||||
self.forecast_horizon = forecast_horizon
|
||||
self.scaler = StandardScaler()
|
||||
self.is_fitted = False
|
||||
|
||||
def add_features(self, df):
|
||||
df = df.copy()
|
||||
df['rsi'] = ta.rsi(df['close'], length=14)
|
||||
macd = ta.macd(df['close'])
|
||||
if macd is not None and not macd.empty:
|
||||
df['macd'] = macd.iloc[:, 0]
|
||||
df['macd_hist'] = macd.iloc[:, 1]
|
||||
df['macd_signal'] = macd.iloc[:, 2]
|
||||
return df
|
||||
|
||||
def create_labels(self, df):
|
||||
df = df.copy()
|
||||
df['future_price'] = df['close'].shift(-self.forecast_horizon)
|
||||
df['future_return'] = (df['future_price'] - df['close']) / df['close']
|
||||
df['label'] = (df['future_return'] > 0).astype(int)
|
||||
return df[:-self.forecast_horizon]
|
||||
|
||||
def preprocess(self, df, fit=True):
|
||||
df = self.add_features(df)
|
||||
df = self.create_labels(df)
|
||||
df = df.dropna().reset_index(drop=True)
|
||||
|
||||
feature_cols = ['open', 'high', 'low', 'close', 'volume',
|
||||
'rsi', 'macd', 'macd_hist', 'macd_signal']
|
||||
|
||||
if fit:
|
||||
scaled_features = self.scaler.fit_transform(df[feature_cols])
|
||||
self.is_fitted = True
|
||||
self.feature_columns = feature_cols
|
||||
else:
|
||||
scaled_features = self.scaler.transform(df[feature_cols])
|
||||
|
||||
X, y = [], []
|
||||
for i in range(len(scaled_features) - self.lookback):
|
||||
X.append(scaled_features[i:i + self.lookback])
|
||||
y.append(df['label'].iloc[i + self.lookback])
|
||||
|
||||
return np.array(X), np.array(y), df
|
||||
|
||||
|
||||
class NBeatsPreprocessor:
|
||||
"""N-BEATS预处理器"""
|
||||
def __init__(self, lookback=168, forecast_horizon=1):
|
||||
self.lookback = lookback
|
||||
self.forecast_horizon = forecast_horizon
|
||||
self.price_scaler = StandardScaler()
|
||||
self.is_fitted = False
|
||||
|
||||
def preprocess(self, df, fit=True):
|
||||
df = df.copy()
|
||||
df['future_price'] = df['close'].shift(-self.forecast_horizon)
|
||||
df = df[:-self.forecast_horizon].reset_index(drop=True)
|
||||
|
||||
prices = df['close'].values.reshape(-1, 1)
|
||||
future_prices = df['future_price'].values.reshape(-1, 1)
|
||||
|
||||
if fit:
|
||||
scaled_prices = self.price_scaler.fit_transform(prices)
|
||||
scaled_future = self.price_scaler.transform(future_prices)
|
||||
self.is_fitted = True
|
||||
else:
|
||||
scaled_prices = self.price_scaler.transform(prices)
|
||||
scaled_future = self.price_scaler.transform(future_prices)
|
||||
|
||||
X, y = [], []
|
||||
for i in range(len(scaled_prices) - self.lookback):
|
||||
X.append(scaled_prices[i:i + self.lookback, 0])
|
||||
y.append(scaled_future[i + self.lookback, 0])
|
||||
|
||||
return np.array(X), np.array(y), df
|
||||
|
||||
def inverse_transform_price(self, scaled_price):
|
||||
return self.price_scaler.inverse_transform(scaled_price.reshape(-1, 1)).flatten()
|
||||
|
||||
|
||||
def add_catboost_features(df):
|
||||
"""为CatBoost添加丰富的技术指标特征"""
|
||||
df = df.copy()
|
||||
|
||||
# 基础价格特征
|
||||
df['price_change'] = df['close'] - df['open']
|
||||
df['price_change_pct'] = (df['close'] - df['open']) / df['open']
|
||||
df['high_low_range'] = df['high'] - df['low']
|
||||
df['high_low_range_pct'] = (df['high'] - df['low']) / df['low']
|
||||
|
||||
# 移动平均线
|
||||
df['sma_7'] = ta.sma(df['close'], length=7)
|
||||
df['sma_25'] = ta.sma(df['close'], length=25)
|
||||
df['sma_99'] = ta.sma(df['close'], length=99)
|
||||
df['ema_12'] = ta.ema(df['close'], length=12)
|
||||
df['ema_26'] = ta.ema(df['close'], length=26)
|
||||
|
||||
# RSI
|
||||
df['rsi_14'] = ta.rsi(df['close'], length=14)
|
||||
df['rsi_7'] = ta.rsi(df['close'], length=7)
|
||||
|
||||
# MACD
|
||||
macd = ta.macd(df['close'])
|
||||
if macd is not None and not macd.empty:
|
||||
df['macd'] = macd.iloc[:, 0]
|
||||
df['macd_hist'] = macd.iloc[:, 1]
|
||||
df['macd_signal'] = macd.iloc[:, 2]
|
||||
|
||||
# 布林带
|
||||
bbands = ta.bbands(df['close'], length=20)
|
||||
if bbands is not None and not bbands.empty:
|
||||
df['bb_upper'] = bbands.iloc[:, 0]
|
||||
df['bb_mid'] = bbands.iloc[:, 1]
|
||||
df['bb_lower'] = bbands.iloc[:, 2]
|
||||
df['bb_width'] = (df['bb_upper'] - df['bb_lower']) / df['bb_mid']
|
||||
df['bb_position'] = (df['close'] - df['bb_lower']) / (df['bb_upper'] - df['bb_lower'])
|
||||
|
||||
# ATR
|
||||
df['atr'] = ta.atr(df['high'], df['low'], df['close'], length=14)
|
||||
|
||||
# 成交量特征
|
||||
df['volume_sma_7'] = ta.sma(df['volume'], length=7)
|
||||
df['volume_ratio'] = df['volume'] / df['volume_sma_7']
|
||||
|
||||
# OBV
|
||||
df['obv'] = ta.obv(df['close'], df['volume'])
|
||||
|
||||
# Stochastic
|
||||
stoch = ta.stoch(df['high'], df['low'], df['close'])
|
||||
if stoch is not None and not stoch.empty:
|
||||
df['stoch_k'] = stoch.iloc[:, 0]
|
||||
df['stoch_d'] = stoch.iloc[:, 1]
|
||||
|
||||
# ADX
|
||||
adx = ta.adx(df['high'], df['low'], df['close'])
|
||||
if adx is not None and not adx.empty:
|
||||
df['adx'] = adx.iloc[:, 0]
|
||||
|
||||
# 价格动量
|
||||
df['momentum_1'] = df['close'].pct_change(1)
|
||||
df['momentum_3'] = df['close'].pct_change(3)
|
||||
df['momentum_7'] = df['close'].pct_change(7)
|
||||
|
||||
return df
|
||||
|
||||
|
||||
def main():
|
||||
print("="*80)
|
||||
print("五模型集成预测:DLinear + N-BEATS + TTM-R2 + TimesFM + CatBoost")
|
||||
print("="*80)
|
||||
|
||||
# ========== 步骤1: 加载PyTorch模型 ==========
|
||||
print("\n[1/8] 加载DLinear和N-BEATS模型...")
|
||||
|
||||
device = torch.device('mps' if torch.backends.mps.is_available() else 'cpu')
|
||||
|
||||
# 加载DLinear
|
||||
from models.dlinear.model import DLinear
|
||||
dlinear_checkpoint = torch.load(
|
||||
'./models/checkpoints/dlinear_classification_2026/best_model.pt',
|
||||
map_location=device
|
||||
)
|
||||
dlinear_model = DLinear(dlinear_checkpoint['config'])
|
||||
dlinear_model.load_state_dict(dlinear_checkpoint['model_state_dict'])
|
||||
dlinear_model = dlinear_model.to(device)
|
||||
dlinear_model.eval()
|
||||
|
||||
with open('./models/checkpoints/dlinear_classification_2026/preprocessor.pkl', 'rb') as f:
|
||||
dlinear_preprocessor = pickle.load(f)
|
||||
|
||||
print(f"✓ DLinear已加载")
|
||||
|
||||
# 加载N-BEATS
|
||||
from models.nbeats.model import NBeats
|
||||
nbeats_checkpoint = torch.load(
|
||||
'./models/checkpoints/nbeats_btc_1h/best_model.pt',
|
||||
map_location=device
|
||||
)
|
||||
nbeats_model = NBeats(nbeats_checkpoint['config'])
|
||||
nbeats_model.load_state_dict(nbeats_checkpoint['model_state_dict'])
|
||||
nbeats_model = nbeats_model.to(device)
|
||||
nbeats_model.eval()
|
||||
|
||||
with open('./models/checkpoints/nbeats_btc_1h/preprocessor.pkl', 'rb') as f:
|
||||
nbeats_preprocessor = pickle.load(f)
|
||||
|
||||
print(f"✓ N-BEATS已加载")
|
||||
|
||||
# ========== 步骤2: 加载TTM-R2 ==========
|
||||
print("\n[2/8] 加载TTM-R2零样本模型...")
|
||||
|
||||
from tsfm_public.models.tinytimemixer import TinyTimeMixerForPrediction
|
||||
|
||||
ttm_model = TinyTimeMixerForPrediction.from_pretrained(
|
||||
"ibm-granite/granite-timeseries-ttm-r2",
|
||||
trust_remote_code=True
|
||||
)
|
||||
ttm_model = ttm_model.to(device)
|
||||
ttm_model.eval()
|
||||
|
||||
print(f"✓ TTM-R2已加载")
|
||||
|
||||
# ========== 步骤3: 加载TimesFM 2.5 ==========
|
||||
print("\n[3/8] 加载TimesFM 2.5模型...")
|
||||
|
||||
import timesfm
|
||||
|
||||
timesfm_model = timesfm.TimesFM_2p5_200M_torch.from_pretrained(
|
||||
"google/timesfm-2.5-200m-pytorch",
|
||||
torch_compile=False
|
||||
)
|
||||
|
||||
timesfm_model.compile(
|
||||
timesfm.ForecastConfig(
|
||||
max_context=512,
|
||||
max_horizon=1,
|
||||
normalize_inputs=True,
|
||||
use_continuous_quantile_head=True,
|
||||
force_flip_invariance=True,
|
||||
infer_is_positive=True,
|
||||
fix_quantile_crossing=True,
|
||||
)
|
||||
)
|
||||
|
||||
print(f"✓ TimesFM 2.5已加载")
|
||||
|
||||
# ========== 步骤4: 加载CatBoost ==========
|
||||
print("\n[4/8] 加载CatBoost模型...")
|
||||
|
||||
catboost_model = CatBoostClassifier()
|
||||
catboost_model.load_model('./models/checkpoints/catboost_classification_2026/catboost_model.cbm')
|
||||
|
||||
with open('./models/checkpoints/catboost_classification_2026/metadata.pkl', 'rb') as f:
|
||||
catboost_metadata = pickle.load(f)
|
||||
catboost_feature_cols = catboost_metadata['feature_cols']
|
||||
|
||||
print(f"✓ CatBoost已加载 (特征数: {len(catboost_feature_cols)})")
|
||||
|
||||
# ========== 步骤5: 获取测试数据 ==========
|
||||
print("\n[5/8] 获取2026年1月测试数据...")
|
||||
|
||||
fetcher = BinanceDataFetcher()
|
||||
df_2026 = fetcher.fetch_ohlcv('BTC', '1h', start_date='2026-01-01',
|
||||
end_date='2026-01-31', use_cache=False)
|
||||
|
||||
print(f"✓ 数据获取完成: {len(df_2026)} 行")
|
||||
|
||||
# ========== 步骤6: 预处理 ==========
|
||||
print("\n[6/8] 预处理数据...")
|
||||
|
||||
X_dlinear, y_dlinear, df_dlinear = dlinear_preprocessor.preprocess(df_2026, fit=False)
|
||||
X_nbeats, y_nbeats, df_nbeats = nbeats_preprocessor.preprocess(df_2026, fit=False)
|
||||
|
||||
# CatBoost特征工程
|
||||
df_catboost = add_catboost_features(df_2026)
|
||||
df_catboost = df_catboost.dropna().reset_index(drop=True)
|
||||
X_catboost = df_catboost[catboost_feature_cols]
|
||||
|
||||
# 确保样本数量一致
|
||||
min_samples = min(len(X_dlinear), len(X_nbeats), len(X_catboost))
|
||||
X_dlinear = X_dlinear[:min_samples]
|
||||
y_dlinear = y_dlinear[:min_samples]
|
||||
X_nbeats = X_nbeats[:min_samples]
|
||||
X_catboost = X_catboost.iloc[:min_samples]
|
||||
|
||||
print(f"✓ 对齐样本数: {min_samples}")
|
||||
|
||||
# ========== 步骤7: 五个模型分别预测 ==========
|
||||
print("\n[7/8] 五个模型分别预测...")
|
||||
|
||||
# 7.1 DLinear预测
|
||||
X_dlinear_tensor = torch.FloatTensor(X_dlinear).to(device)
|
||||
with torch.no_grad():
|
||||
dlinear_logits = dlinear_model(X_dlinear_tensor).cpu().numpy()
|
||||
dlinear_probs = 1 / (1 + np.exp(-dlinear_logits))
|
||||
dlinear_preds = (dlinear_probs > 0.5).astype(int).flatten()
|
||||
|
||||
print(f"✓ DLinear: 涨{(dlinear_preds==1).sum()} ({(dlinear_preds==1).sum()/len(dlinear_preds)*100:.1f}%)")
|
||||
|
||||
# 7.2 N-BEATS预测
|
||||
X_nbeats_tensor = torch.FloatTensor(X_nbeats).to(device)
|
||||
with torch.no_grad():
|
||||
nbeats_pred_scaled = nbeats_model(X_nbeats_tensor).cpu().numpy()
|
||||
|
||||
nbeats_pred_prices = nbeats_preprocessor.inverse_transform_price(nbeats_pred_scaled)
|
||||
current_open_prices = df_nbeats['open'].iloc[168:168+len(nbeats_pred_prices)].values
|
||||
nbeats_preds = (nbeats_pred_prices > current_open_prices).astype(int)
|
||||
|
||||
print(f"✓ N-BEATS: 涨{(nbeats_preds==1).sum()} ({(nbeats_preds==1).sum()/len(nbeats_preds)*100:.1f}%)")
|
||||
|
||||
# 7.3 TTM-R2预测
|
||||
context_length = 512
|
||||
ttm_preds_list = []
|
||||
all_prices = df_2026['close'].values
|
||||
all_open_prices = df_2026['open'].values
|
||||
|
||||
for i in range(min_samples):
|
||||
start_idx = i
|
||||
end_idx = start_idx + context_length
|
||||
|
||||
if end_idx > len(all_prices):
|
||||
context_data = all_prices[-context_length:].reshape(1, -1, 1)
|
||||
current_open_price = all_open_prices[min(168 + i, len(all_open_prices) - 1)]
|
||||
else:
|
||||
context_data = all_prices[start_idx:end_idx].reshape(1, -1, 1)
|
||||
current_open_price = all_open_prices[end_idx] if end_idx < len(all_open_prices) else all_open_prices[-1]
|
||||
|
||||
context_tensor = torch.FloatTensor(context_data).to(device)
|
||||
|
||||
with torch.no_grad():
|
||||
output = ttm_model(context_tensor)
|
||||
pred_price = output.prediction_outputs.cpu().numpy()[0, 0, 0]
|
||||
|
||||
ttm_preds_list.append(1 if pred_price > current_open_price else 0)
|
||||
|
||||
ttm_preds = np.array(ttm_preds_list)
|
||||
|
||||
print(f"✓ TTM-R2: 涨{(ttm_preds==1).sum()} ({(ttm_preds==1).sum()/len(ttm_preds)*100:.1f}%)")
|
||||
|
||||
# 7.4 TimesFM 2.5预测
|
||||
timesfm_preds_list = []
|
||||
|
||||
for i in range(min_samples):
|
||||
start_idx = i
|
||||
end_idx = start_idx + 512
|
||||
|
||||
if end_idx > len(all_prices):
|
||||
context_prices = all_prices[-512:]
|
||||
current_open_price = all_open_prices[min(168 + i, len(all_open_prices) - 1)]
|
||||
else:
|
||||
context_prices = all_prices[start_idx:end_idx]
|
||||
current_open_price = all_open_prices[end_idx] if end_idx < len(all_open_prices) else all_open_prices[-1]
|
||||
|
||||
try:
|
||||
point_forecast, _ = timesfm_model.forecast(
|
||||
horizon=1,
|
||||
inputs=[context_prices],
|
||||
)
|
||||
pred_price = point_forecast[0, 0]
|
||||
timesfm_preds_list.append(1 if pred_price > current_open_price else 0)
|
||||
except:
|
||||
timesfm_preds_list.append(np.random.randint(0, 2))
|
||||
|
||||
timesfm_preds = np.array(timesfm_preds_list)
|
||||
|
||||
print(f"✓ TimesFM: 涨{(timesfm_preds==1).sum()} ({(timesfm_preds==1).sum()/len(timesfm_preds)*100:.1f}%)")
|
||||
|
||||
# 7.5 CatBoost预测
|
||||
catboost_preds = catboost_model.predict(X_catboost).astype(int)
|
||||
|
||||
print(f"✓ CatBoost: 涨{(catboost_preds==1).sum()} ({(catboost_preds==1).sum()/len(catboost_preds)*100:.1f}%)")
|
||||
|
||||
# ========== 步骤8: 集成策略评估 ==========
|
||||
print("\n[8/8] 集成策略评估...")
|
||||
|
||||
true_labels = y_dlinear
|
||||
|
||||
# 策略1: 双模型交集(DLinear + N-BEATS)
|
||||
two_model_agree_up = (dlinear_preds == 1) & (nbeats_preds == 1)
|
||||
two_model_agree_down = (dlinear_preds == 0) & (nbeats_preds == 0)
|
||||
two_model_preds = np.full(len(dlinear_preds), -1)
|
||||
two_model_preds[two_model_agree_up] = 1
|
||||
two_model_preds[two_model_agree_down] = 0
|
||||
|
||||
two_model_valid = two_model_preds != -1
|
||||
two_model_correct = (two_model_preds == true_labels) & two_model_valid
|
||||
two_model_acc = two_model_correct.sum() / two_model_valid.sum() * 100 if two_model_valid.sum() > 0 else 0
|
||||
two_model_coverage = two_model_valid.sum() / len(true_labels) * 100
|
||||
|
||||
# 策略2: 三模型投票(DLinear + N-BEATS + TTM-R2)
|
||||
vote_sum_3 = dlinear_preds + nbeats_preds + ttm_preds
|
||||
three_model_preds = np.where(vote_sum_3 >= 2, 1, 0)
|
||||
three_model_acc = (three_model_preds == true_labels).mean() * 100
|
||||
|
||||
# 策略3: 四模型投票
|
||||
vote_sum_4 = dlinear_preds + nbeats_preds + ttm_preds + timesfm_preds
|
||||
four_model_preds = np.where(vote_sum_4 >= 3, 1, 0)
|
||||
four_model_acc = (four_model_preds == true_labels).mean() * 100
|
||||
|
||||
# 策略4: 五模型强投票(≥3票)
|
||||
vote_sum_5 = dlinear_preds + nbeats_preds + ttm_preds + timesfm_preds + catboost_preds
|
||||
five_model_preds_strong = np.where(vote_sum_5 >= 3, 1, 0)
|
||||
five_model_acc_strong = (five_model_preds_strong == true_labels).mean() * 100
|
||||
|
||||
# 策略5: 五模型弱投票(≥2票)
|
||||
five_model_preds_weak = np.where(vote_sum_5 >= 2, 1, 0)
|
||||
five_model_acc_weak = (five_model_preds_weak == true_labels).mean() * 100
|
||||
|
||||
# ========== 结果展示 ==========
|
||||
print("\n" + "="*80)
|
||||
print("📊 五模型集成策略对比结果")
|
||||
print("="*80)
|
||||
|
||||
print(f"\n单模型性能:")
|
||||
print(f" DLinear: {(dlinear_preds == true_labels).mean() * 100:.2f}%")
|
||||
print(f" CatBoost: {(catboost_preds == true_labels).mean() * 100:.2f}%")
|
||||
print(f" TimesFM 2.5: {(timesfm_preds == true_labels).mean() * 100:.2f}%")
|
||||
print(f" TTM-R2: {(ttm_preds == true_labels).mean() * 100:.2f}%")
|
||||
print(f" N-BEATS: {(nbeats_preds == true_labels).mean() * 100:.2f}%")
|
||||
|
||||
print(f"\n集成策略性能:")
|
||||
print(f" 双模型交集 (DL+NB): {two_model_acc:.2f}% (覆盖率: {two_model_coverage:.2f}%)")
|
||||
print(f" 三模型投票 (DL+NB+TTM, ≥2票): {three_model_acc:.2f}% (覆盖率: 100%)")
|
||||
print(f" 四模型投票 (前4, ≥3票): {four_model_acc:.2f}% (覆盖率: 100%)")
|
||||
print(f" 五模型强投票 (所有, ≥3票): {five_model_acc_strong:.2f}% (覆盖率: 100%)")
|
||||
print(f" 五模型弱投票 (所有, ≥2票): {five_model_acc_weak:.2f}% (覆盖率: 100%)")
|
||||
|
||||
# 投票分布分析
|
||||
print(f"\n五模型投票分布:")
|
||||
for votes in range(6):
|
||||
count = (vote_sum_5 == votes).sum()
|
||||
pct = count / len(vote_sum_5) * 100
|
||||
bars = "█" * int(pct / 2)
|
||||
print(f" {votes}票同意: {count:3d}次 ({pct:5.1f}%) {bars}")
|
||||
|
||||
# 模型一致性分析
|
||||
print(f"\n模型一致性分析:")
|
||||
all_agree_up = (dlinear_preds == 1) & (nbeats_preds == 1) & (ttm_preds == 1) & (timesfm_preds == 1) & (catboost_preds == 1)
|
||||
all_agree_down = (dlinear_preds == 0) & (nbeats_preds == 0) & (ttm_preds == 0) & (timesfm_preds == 0) & (catboost_preds == 0)
|
||||
all_agree = all_agree_up | all_agree_down
|
||||
|
||||
print(f" 五模型完全一致: {all_agree.sum()}次 ({all_agree.sum()/len(all_agree)*100:.1f}%)")
|
||||
if all_agree.sum() > 0:
|
||||
all_agree_acc = ((five_model_preds_strong[all_agree] == true_labels[all_agree]).mean() * 100)
|
||||
print(f" 一致时准确率: {all_agree_acc:.2f}%")
|
||||
|
||||
print("\n" + "="*80)
|
||||
print("✓ 五模型集成预测完成!")
|
||||
print("="*80)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user