- 删除测试脚本: 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分类模型
452 lines
18 KiB
Python
452 lines
18 KiB
Python
"""智能休息策略回测:基于历史准确率动态决定是否交易"""
|
|
|
|
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 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 main():
|
|
print("="*80)
|
|
print("智能休息策略回测:基于历史准确率动态决定是否交易")
|
|
print("="*80)
|
|
print("\n策略规则:")
|
|
print(" 1. 如果前一天准确率超过70% → 当天休息")
|
|
print(" 2. 如果连续两天准确率超过60% → 第三天休息")
|
|
print(" 3. 其他情况 → 正常交易")
|
|
print("="*80)
|
|
|
|
# ========== 加载模型 ==========
|
|
print("\n[1/4] 加载模型...")
|
|
|
|
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)
|
|
|
|
# 加载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)
|
|
|
|
# 加载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"✓ 三个模型已加载")
|
|
|
|
# ========== 获取数据 ==========
|
|
print("\n[2/4] 获取2026年1月测试数据(1月1日-1月30日)...")
|
|
fetcher = BinanceDataFetcher()
|
|
|
|
df_2026 = fetcher.fetch_ohlcv('BTC', '1h', start_date='2026-01-01',
|
|
end_date='2026-01-30', use_cache=False)
|
|
|
|
print(f"✓ 测试数据: {len(df_2026)} 行")
|
|
print(f"✓ 时间范围: {df_2026['timestamp'].min()} 至 {df_2026['timestamp'].max()}")
|
|
|
|
# ========== 预处理 ==========
|
|
print("\n[3/4] 预处理数据...")
|
|
|
|
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)
|
|
|
|
min_samples = min(len(X_dlinear), len(X_nbeats))
|
|
X_dlinear = X_dlinear[:min_samples]
|
|
y_dlinear = y_dlinear[:min_samples]
|
|
X_nbeats = X_nbeats[:min_samples]
|
|
y_nbeats = y_nbeats[:min_samples]
|
|
|
|
# ========== 预测 ==========
|
|
print("\n[4/4] 执行预测...")
|
|
|
|
# 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()
|
|
|
|
# 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)
|
|
|
|
# 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)
|
|
|
|
# 双模型交集
|
|
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
|
|
|
|
# 三模型多数投票
|
|
vote_sum = dlinear_preds + nbeats_preds + ttm_preds
|
|
three_model_preds = np.where(vote_sum >= 2, 1, 0)
|
|
|
|
# 真实标签
|
|
true_labels = y_dlinear
|
|
|
|
# ========== 构建每日数据 ==========
|
|
timestamps = df_2026['timestamp'].iloc[168:168+min_samples].values
|
|
timestamps_utc = pd.to_datetime(timestamps)
|
|
timestamps_et = timestamps_utc.tz_localize('UTC').tz_convert('America/New_York')
|
|
dates = timestamps_et.date
|
|
|
|
results_df = pd.DataFrame({
|
|
'date': dates,
|
|
'timestamp_et': timestamps_et,
|
|
'true_label': true_labels,
|
|
'two_model_pred': two_model_preds,
|
|
'three_model_pred': three_model_preds,
|
|
'two_model_correct': (two_model_preds == true_labels) & (two_model_preds != -1),
|
|
'three_model_correct': (three_model_preds == true_labels)
|
|
})
|
|
|
|
# 按天统计基础数据
|
|
daily_stats = results_df.groupby('date').agg({
|
|
'true_label': 'count',
|
|
'two_model_pred': lambda x: (x != -1).sum(),
|
|
'two_model_correct': 'sum',
|
|
'three_model_correct': 'sum'
|
|
})
|
|
|
|
daily_stats.columns = ['total_samples', 'two_model_valid', 'two_model_correct', 'three_model_correct']
|
|
|
|
# 计算每日准确率
|
|
daily_stats['two_model_accuracy'] = daily_stats['two_model_correct'] / daily_stats['two_model_valid'] * 100
|
|
daily_stats['three_model_accuracy'] = daily_stats['three_model_correct'] / daily_stats['total_samples'] * 100
|
|
daily_stats['two_model_accuracy'] = daily_stats['two_model_accuracy'].fillna(0)
|
|
|
|
# ========== 应用智能休息策略 ==========
|
|
print("\n" + "="*80)
|
|
print("📊 应用智能休息策略")
|
|
print("="*80)
|
|
|
|
# 为每一天添加"是否休息"标记
|
|
daily_stats['two_model_rest'] = False
|
|
daily_stats['three_model_rest'] = False
|
|
daily_stats['rest_reason'] = ''
|
|
|
|
dates_list = daily_stats.index.tolist()
|
|
|
|
for i, date in enumerate(dates_list):
|
|
if i == 0:
|
|
continue # 第一天没有历史数据,不休息
|
|
|
|
# 获取前一天和前两天的数据
|
|
prev_date = dates_list[i-1]
|
|
prev2_date = dates_list[i-2] if i >= 2 else None
|
|
|
|
# 双模型休息逻辑
|
|
# 规则1:前一天准确率超过70%
|
|
if daily_stats.loc[prev_date, 'two_model_accuracy'] > 70:
|
|
daily_stats.loc[date, 'two_model_rest'] = True
|
|
daily_stats.loc[date, 'rest_reason'] = f'前日准确率{daily_stats.loc[prev_date, "two_model_accuracy"]:.1f}%>70%'
|
|
|
|
# 规则2:连续两天准确率超过60%
|
|
elif (prev2_date is not None and
|
|
daily_stats.loc[prev_date, 'two_model_accuracy'] > 60 and
|
|
daily_stats.loc[prev2_date, 'two_model_accuracy'] > 60):
|
|
daily_stats.loc[date, 'two_model_rest'] = True
|
|
daily_stats.loc[date, 'rest_reason'] = f'连续2天>60% ({daily_stats.loc[prev2_date, "two_model_accuracy"]:.1f}%, {daily_stats.loc[prev_date, "two_model_accuracy"]:.1f}%)'
|
|
|
|
# 三模型休息逻辑
|
|
# 规则1:前一天准确率超过70%
|
|
if daily_stats.loc[prev_date, 'three_model_accuracy'] > 70:
|
|
daily_stats.loc[date, 'three_model_rest'] = True
|
|
if not daily_stats.loc[date, 'rest_reason']:
|
|
daily_stats.loc[date, 'rest_reason'] = f'三模前日{daily_stats.loc[prev_date, "three_model_accuracy"]:.1f}%>70%'
|
|
|
|
# 规则2:连续两天准确率超过60%
|
|
elif (prev2_date is not None and
|
|
daily_stats.loc[prev_date, 'three_model_accuracy'] > 60 and
|
|
daily_stats.loc[prev2_date, 'three_model_accuracy'] > 60):
|
|
daily_stats.loc[date, 'three_model_rest'] = True
|
|
if not daily_stats.loc[date, 'rest_reason']:
|
|
daily_stats.loc[date, 'rest_reason'] = f'三模连续2天>60%'
|
|
|
|
# ========== 统计休息天数 ==========
|
|
two_model_rest_days = daily_stats['two_model_rest'].sum()
|
|
three_model_rest_days = daily_stats['three_model_rest'].sum()
|
|
total_days = len(daily_stats)
|
|
|
|
print(f"\n休息天数统计:")
|
|
print(f" 总交易日: {total_days}天")
|
|
print(f" 双模型休息: {two_model_rest_days}天 ({two_model_rest_days/total_days*100:.1f}%)")
|
|
print(f" 三模型休息: {three_model_rest_days}天 ({three_model_rest_days/total_days*100:.1f}%)")
|
|
|
|
# ========== 计算休息策略的实际表现 ==========
|
|
print("\n" + "="*80)
|
|
print("📈 策略对比:正常交易 vs 智能休息")
|
|
print("="*80)
|
|
|
|
# 双模型策略
|
|
print(f"\n🔵 双模型交集策略:")
|
|
|
|
# 正常交易(不休息)
|
|
two_normal_correct = daily_stats['two_model_correct'].sum()
|
|
two_normal_total = daily_stats['two_model_valid'].sum()
|
|
two_normal_acc = two_normal_correct / two_normal_total * 100 if two_normal_total > 0 else 0
|
|
|
|
print(f"\n 📊 正常交易(不休息):")
|
|
print(f" 交易天数: {total_days}天")
|
|
print(f" 有效预测: {two_normal_total}次")
|
|
print(f" 正确预测: {two_normal_correct}次")
|
|
print(f" 准确率: {two_normal_acc:.2f}%")
|
|
|
|
# 智能休息
|
|
two_rest_mask = ~daily_stats['two_model_rest']
|
|
two_rest_correct = daily_stats.loc[two_rest_mask, 'two_model_correct'].sum()
|
|
two_rest_total = daily_stats.loc[two_rest_mask, 'two_model_valid'].sum()
|
|
two_rest_acc = two_rest_correct / two_rest_total * 100 if two_rest_total > 0 else 0
|
|
two_trade_days = two_rest_mask.sum()
|
|
|
|
print(f"\n 🛌 智能休息策略:")
|
|
print(f" 交易天数: {two_trade_days}天 (休息{two_model_rest_days}天)")
|
|
print(f" 有效预测: {two_rest_total}次")
|
|
print(f" 正确预测: {two_rest_correct}次")
|
|
print(f" 准确率: {two_rest_acc:.2f}%")
|
|
|
|
two_acc_diff = two_rest_acc - two_normal_acc
|
|
two_diff_mark = "✅" if two_acc_diff > 0 else ("⚠️" if two_acc_diff < 0 else "")
|
|
print(f"\n 准确率变化: {two_acc_diff:+.2f}% {two_diff_mark}")
|
|
|
|
# 休息日的实际表现
|
|
if two_model_rest_days > 0:
|
|
two_rest_days_mask = daily_stats['two_model_rest']
|
|
two_rest_day_correct = daily_stats.loc[two_rest_days_mask, 'two_model_correct'].sum()
|
|
two_rest_day_total = daily_stats.loc[two_rest_days_mask, 'two_model_valid'].sum()
|
|
two_rest_day_acc = two_rest_day_correct / two_rest_day_total * 100 if two_rest_day_total > 0 else 0
|
|
|
|
print(f"\n 🔍 被休息的{two_model_rest_days}天实际表现:")
|
|
print(f" 准确率: {two_rest_day_acc:.2f}%")
|
|
avoided_loss = "避免了低准确率" if two_rest_day_acc < two_normal_acc else "⚠️ 错过了好机会"
|
|
print(f" 评价: {avoided_loss}")
|
|
|
|
# 三模型策略
|
|
print(f"\n🟢 三模型投票策略:")
|
|
|
|
# 正常交易
|
|
three_normal_correct = daily_stats['three_model_correct'].sum()
|
|
three_normal_total = daily_stats['total_samples'].sum()
|
|
three_normal_acc = three_normal_correct / three_normal_total * 100
|
|
|
|
print(f"\n 📊 正常交易(不休息):")
|
|
print(f" 交易天数: {total_days}天")
|
|
print(f" 有效预测: {three_normal_total}次")
|
|
print(f" 正确预测: {three_normal_correct}次")
|
|
print(f" 准确率: {three_normal_acc:.2f}%")
|
|
|
|
# 智能休息
|
|
three_rest_mask = ~daily_stats['three_model_rest']
|
|
three_rest_correct = daily_stats.loc[three_rest_mask, 'three_model_correct'].sum()
|
|
three_rest_total = daily_stats.loc[three_rest_mask, 'total_samples'].sum()
|
|
three_rest_acc = three_rest_correct / three_rest_total * 100 if three_rest_total > 0 else 0
|
|
three_trade_days = three_rest_mask.sum()
|
|
|
|
print(f"\n 🛌 智能休息策略:")
|
|
print(f" 交易天数: {three_trade_days}天 (休息{three_model_rest_days}天)")
|
|
print(f" 有效预测: {three_rest_total}次")
|
|
print(f" 正确预测: {three_rest_correct}次")
|
|
print(f" 准确率: {three_rest_acc:.2f}%")
|
|
|
|
three_acc_diff = three_rest_acc - three_normal_acc
|
|
three_diff_mark = "✅" if three_acc_diff > 0 else ("⚠️" if three_acc_diff < 0 else "")
|
|
print(f"\n 准确率变化: {three_acc_diff:+.2f}% {three_diff_mark}")
|
|
|
|
# 休息日的实际表现
|
|
if three_model_rest_days > 0:
|
|
three_rest_days_mask = daily_stats['three_model_rest']
|
|
three_rest_day_correct = daily_stats.loc[three_rest_days_mask, 'three_model_correct'].sum()
|
|
three_rest_day_total = daily_stats.loc[three_rest_days_mask, 'total_samples'].sum()
|
|
three_rest_day_acc = three_rest_day_correct / three_rest_day_total * 100
|
|
|
|
print(f"\n 🔍 被休息的{three_model_rest_days}天实际表现:")
|
|
print(f" 准确率: {three_rest_day_acc:.2f}%")
|
|
avoided_loss = "避免了低准确率" if three_rest_day_acc < three_normal_acc else "⚠️ 错过了好机会"
|
|
print(f" 评价: {avoided_loss}")
|
|
|
|
# ========== 详细日志 ==========
|
|
print("\n" + "="*80)
|
|
print("📅 每日详细记录")
|
|
print("="*80)
|
|
|
|
print(f"\n{'日期':<12} {'双模准确率':>10} {'三模准确率':>10} {'休息状态':<12} {'休息原因':<40}")
|
|
print("-" * 95)
|
|
|
|
for date, row in daily_stats.iterrows():
|
|
two_acc = row['two_model_accuracy']
|
|
three_acc = row['three_model_accuracy']
|
|
|
|
rest_status = ""
|
|
if row['two_model_rest']:
|
|
rest_status += "双休 "
|
|
if row['three_model_rest']:
|
|
rest_status += "三休"
|
|
|
|
if not rest_status:
|
|
rest_status = "正常交易"
|
|
|
|
# 准确率标记
|
|
two_mark = "🔥" if two_acc >= 70 else ("✅" if two_acc >= 60 else "")
|
|
three_mark = "🔥" if three_acc >= 70 else ("✅" if three_acc >= 60 else "")
|
|
|
|
print(f"{date} {two_acc:>9.1f}{two_mark} {three_acc:>9.1f}{three_mark} "
|
|
f"{rest_status:<12} {row['rest_reason']:<40}")
|
|
|
|
print("\n" + "="*80)
|
|
print("✓ 智能休息策略回测完成!")
|
|
print("="*80)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|