"""对比 test 旧逻辑 vs auto_trade 逻辑的数据处理差异 两种预处理方式: - test旧逻辑:整个df一次性预处理,无create_labels,按索引遍历窗口 - auto_trade逻辑:截取到当前K线的df,含create_labels,取X[-1]预测 对比内容: 1. 预处理后的df行数差异 2. 同一时间点的特征值差异(RSI、MACD等) 3. scaler transform后的值差异 4. 最终输入窗口的数值差异 5. 模型输出logits/概率/预测结果差异 """ import sys import os sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import torch import numpy as np import pandas as pd import pickle import pandas_ta as ta from datetime import timedelta from sklearn.preprocessing import StandardScaler from data.fetcher import BinanceDataFetcher # ==================== pickle反序列化需要的占位类 ==================== class ClassificationPreprocessor: """pkl文件中保存的类,pickle.load时需要能找到这个类名""" def __init__(self, lookback=168, forecast_horizon=1): self.lookback = lookback self.forecast_horizon = forecast_horizon self.scaler = StandardScaler() self.is_fitted = False # ==================== 两种预处理器 ==================== class TestPreprocessor: """test旧逻辑:无create_labels""" def __init__(self, lookback=168): self.lookback = lookback self.scaler = StandardScaler() def preprocess(self, df, fit=True): 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] 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]) else: scaled_features = self.scaler.transform(df[feature_cols]) X = [] valid_indices = [] for i in range(len(scaled_features) - self.lookback): X.append(scaled_features[i:i + self.lookback]) valid_indices.append(i + self.lookback - 1) return np.array(X), df, valid_indices, scaled_features class AutoTradePreprocessor: """auto_trade逻辑:含create_labels""" def __init__(self, lookback=168, forecast_horizon=1): self.lookback = lookback self.forecast_horizon = forecast_horizon self.scaler = StandardScaler() def preprocess(self, df, fit=True): 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] # create_labels: shift + 砍最后一行 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) df = df[:-self.forecast_horizon] 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]) else: scaled_features = self.scaler.transform(df[feature_cols]) X = [] for i in range(len(scaled_features) - self.lookback): X.append(scaled_features[i:i + self.lookback]) return np.array(X), df, scaled_features def utc_to_beijing(ts): return ts + timedelta(hours=8) def main(): print("=" * 100) print("数据处理对比:test旧逻辑 vs auto_trade逻辑") print("=" * 100) device = torch.device('mps' if torch.backends.mps.is_available() else 'cpu') # 加载模型和训练时的scaler from models.dlinear.model import DLinear checkpoint = torch.load('./models/checkpoints/dlinear_classification_2026/best_model.pt', map_location=device, weights_only=False) model = DLinear(checkpoint['config']).to(device).eval() with open('./models/checkpoints/dlinear_classification_2026/preprocessor.pkl', 'rb') as f: trained_preprocessor = pickle.load(f) # 提取训练时的scaler参数 trained_scaler = trained_preprocessor.scaler print(f"训练时scaler均值: {trained_scaler.mean_}") print(f"训练时scaler方差: {trained_scaler.scale_}") # 获取数据 print("\n获取数据...") fetcher = BinanceDataFetcher() df_full = fetcher.fetch_ohlcv('BTC', '1h', start_date='2025-12-01', end_date='2026-03-01', use_cache=False) print(f"总行数: {len(df_full)}") # 选几个2月10日(北京时间)的时间点做对比 # 北京时间02-10 00:00 = UTC 02-09 16:00 test_targets_utc = [ '2026-02-09 16:00:00', # 北京 02-10 00:00 '2026-02-09 20:00:00', # 北京 02-10 04:00 '2026-02-10 00:00:00', # 北京 02-10 08:00 '2026-02-10 04:00:00', # 北京 02-10 12:00 '2026-02-10 08:00:00', # 北京 02-10 16:00 '2026-02-10 12:00:00', # 北京 02-10 20:00 ] feature_cols = ['open', 'high', 'low', 'close', 'volume', 'rsi', 'macd', 'macd_hist', 'macd_signal'] for target_utc_str in test_targets_utc: target_utc = pd.Timestamp(target_utc_str) target_bj = utc_to_beijing(target_utc) # 找到目标K线在df中的索引 target_mask = df_full['timestamp'] == target_utc if not target_mask.any(): print(f"\n⚠ 找不到 {target_utc_str},跳过") continue target_idx = df_full[target_mask].index[0] input_idx = target_idx - 1 # 输入的最后一根K线 input_row = df_full.iloc[input_idx] target_row = df_full.iloc[target_idx] print(f"\n{'=' * 100}") print(f"目标K线: 北京{target_bj.strftime('%m-%d %H:%M')} (UTC {target_utc_str})") print(f"输入末根: UTC {input_row['timestamp']} O:{input_row['open']:.2f} H:{input_row['high']:.2f} L:{input_row['low']:.2f} C:{input_row['close']:.2f}") print(f"目标K线: UTC {target_row['timestamp']} 收盘:{target_row['close']:.2f}") print("-" * 100) # ===== 方式A: test旧逻辑 ===== # 对整个df一次性预处理,按索引找到对应窗口 test_prep = TestPreprocessor(lookback=168) test_prep.scaler = trained_scaler # 使用训练时的scaler X_test, df_test, idx_test, scaled_test = test_prep.preprocess(df_full, fit=False) # 找到input_idx对应的窗口 # idx_test[i] = i + 168 - 1,所以 input_idx 在 df_test 中的位置需要查找 # df_test 是 dropna 后 reset_index 的,timestamp 可能对应不同的行号 test_input_mask = df_test['timestamp'] == input_row['timestamp'] if test_input_mask.any(): test_df_idx = df_test[test_input_mask].index[0] # 对应的窗口序号 test_window_i = test_df_idx - 168 + 1 if 0 <= test_window_i < len(X_test): X_test_window = X_test[test_window_i] else: X_test_window = None print(" [test] 窗口索引越界") else: test_df_idx = None X_test_window = None print(" [test] 在df_test中找不到对应时间") # ===== 方式B: auto_trade逻辑 ===== # 截取到input_idx为止,含create_labels df_up_to_t = df_full.iloc[:input_idx + 1] auto_prep = AutoTradePreprocessor(lookback=168) auto_prep.scaler = trained_scaler # 使用训练时的scaler X_auto, df_auto, scaled_auto = auto_prep.preprocess(df_up_to_t, fit=False) if len(X_auto) > 0: X_auto_window = X_auto[-1] # 取最后一个窗口 else: X_auto_window = None print(" [auto] X为空") # ===== 对比 ===== # 1. df行数对比 print(f"\n[1] 预处理后df行数") print(f" test: {len(df_test)} 行 (整个df, 无create_labels)") print(f" auto: {len(df_auto)} 行 (截取到input, 含create_labels砍尾)") # 2. 最后一行时间对比 print(f"\n[2] 预处理后df最后一行") print(f" test最后一行: {df_test.iloc[-1]['timestamp']} close={df_test.iloc[-1]['close']:.2f}") print(f" auto最后一行: {df_auto.iloc[-1]['timestamp']} close={df_auto.iloc[-1]['close']:.2f}") # 3. 输入末根K线的原始特征对比 if test_df_idx is not None: test_features = df_test.loc[test_df_idx, feature_cols] auto_last_row = df_auto.iloc[-1] auto_features = auto_last_row[feature_cols] print(f"\n[3] 输入末根K线原始特征值") print(f" {'特征':<15} {'test值':>15} {'auto值':>15} {'差异':>15}") print(f" {'-'*60}") for col in feature_cols: tv = test_features[col] av = auto_features[col] diff = tv - av marker = " ⚠" if abs(diff) > 1e-6 else "" print(f" {col:<15} {tv:>15.6f} {av:>15.6f} {diff:>15.8f}{marker}") # 4. scaler transform 后的值对比(输入末根K线) if test_df_idx is not None and X_test_window is not None and X_auto_window is not None: test_scaled_last = scaled_test[test_df_idx] auto_scaled_last = scaled_auto[-1] print(f"\n[4] scaler transform 后的值(输入末根K线)") print(f" {'特征':<15} {'test值':>15} {'auto值':>15} {'差异':>15}") print(f" {'-'*60}") for j, col in enumerate(feature_cols): tv = test_scaled_last[j] av = auto_scaled_last[j] diff = tv - av marker = " ⚠" if abs(diff) > 1e-6 else "" print(f" {col:<15} {tv:>15.6f} {av:>15.6f} {diff:>15.8f}{marker}") # 5. 输入窗口对比(整个168步窗口) if X_test_window is not None and X_auto_window is not None: window_diff = X_test_window - X_auto_window max_diff = np.max(np.abs(window_diff)) mean_diff = np.mean(np.abs(window_diff)) nonzero_count = np.count_nonzero(np.abs(window_diff) > 1e-6) print(f"\n[5] 输入窗口对比 (168步 × 9特征 = 1512个值)") print(f" 最大绝对差异: {max_diff:.10f}") print(f" 平均绝对差异: {mean_diff:.10f}") print(f" 差异>1e-6的值: {nonzero_count} / 1512") if nonzero_count > 0: # 找到差异最大的位置 max_pos = np.unravel_index(np.argmax(np.abs(window_diff)), window_diff.shape) print(f" 最大差异位置: 步={max_pos[0]} 特征={feature_cols[max_pos[1]]}") print(f" test值: {X_test_window[max_pos]:.10f}") print(f" auto值: {X_auto_window[max_pos]:.10f}") # 6. 模型推理对比 if X_test_window is not None and X_auto_window is not None: with torch.no_grad(): test_tensor = torch.FloatTensor(X_test_window[np.newaxis]).to(device) auto_tensor = torch.FloatTensor(X_auto_window[np.newaxis]).to(device) test_logits = model(test_tensor).cpu().numpy().item() auto_logits = model(auto_tensor).cpu().numpy().item() test_prob = 1 / (1 + np.exp(-test_logits)) auto_prob = 1 / (1 + np.exp(-auto_logits)) test_pred = "涨" if test_prob > 0.5 else "跌" auto_pred = "涨" if auto_prob > 0.5 else "跌" match = "✓ 一致" if test_pred == auto_pred else "✗ 不一致" print(f"\n[6] 模型推理结果") print(f" {'':15} {'test':>15} {'auto':>15} {'差异':>15}") print(f" {'-'*60}") print(f" {'logits':<15} {test_logits:>15.8f} {auto_logits:>15.8f} {test_logits-auto_logits:>15.8f}") print(f" {'概率':<15} {test_prob:>15.8f} {auto_prob:>15.8f} {test_prob-auto_prob:>15.8f}") print(f" {'预测':<15} {test_pred:>15} {auto_pred:>15} {match:>15}") # ===== 全量对比:2月所有小时 ===== print(f"\n\n{'=' * 100}") print("全量对比:2月份所有小时预测结果差异统计") print("=" * 100) # test方式:一次性预处理 test_prep_full = TestPreprocessor(lookback=168) test_prep_full.scaler = trained_scaler X_test_full, df_test_full, idx_test_full, _ = test_prep_full.preprocess(df_full, fit=False) # 构建 timestamp -> test窗口索引 的映射 test_ts_map = {} for i, idx in enumerate(idx_test_full): ts = df_test_full.iloc[idx]['timestamp'] test_ts_map[ts] = i total = 0 same = 0 diff_list = [] for target_idx in range(1, len(df_full)): target_time_utc = df_full.iloc[target_idx]['timestamp'] target_time_bj = utc_to_beijing(target_time_utc) if not (target_time_bj.year == 2026 and target_time_bj.month == 2): continue input_idx = target_idx - 1 input_ts = df_full.iloc[input_idx]['timestamp'] # test方式 test_i = test_ts_map.get(input_ts) if test_i is None or test_i >= len(X_test_full): continue # auto方式 df_up_to_t = df_full.iloc[:input_idx + 1] auto_prep_tmp = AutoTradePreprocessor(lookback=168) auto_prep_tmp.scaler = trained_scaler X_auto_tmp, _, _ = auto_prep_tmp.preprocess(df_up_to_t, fit=False) if len(X_auto_tmp) == 0: continue with torch.no_grad(): test_logits = model(torch.FloatTensor(X_test_full[test_i:test_i+1]).to(device)).cpu().numpy().item() auto_logits = model(torch.FloatTensor(X_auto_tmp[-1:]).to(device)).cpu().numpy().item() test_prob = 1 / (1 + np.exp(-test_logits)) auto_prob = 1 / (1 + np.exp(-auto_logits)) test_pred = 1 if test_prob > 0.5 else 0 auto_pred = 1 if auto_prob > 0.5 else 0 total += 1 if test_pred == auto_pred: same += 1 else: diff_list.append({ 'time_bj': target_time_bj.strftime('%m-%d %H:%M'), 'test_prob': test_prob, 'auto_prob': auto_prob, 'test_pred': '涨' if test_pred == 1 else '跌', 'auto_pred': '涨' if auto_pred == 1 else '跌', }) print(f"总预测数: {total}") print(f"结果一致: {same} ({same/total:.1%})") print(f"结果不同: {len(diff_list)} ({len(diff_list)/total:.1%})") if diff_list: print(f"\n不一致的时间点:") print(f" {'北京时间':<15} {'test概率':>10} {'test预测':>8} {'auto概率':>10} {'auto预测':>8}") print(f" {'-'*55}") for d in diff_list: print(f" {d['time_bj']:<15} {d['test_prob']:>10.6f} {d['test_pred']:>8} {d['auto_prob']:>10.6f} {d['auto_pred']:>8}") print("=" * 100) if __name__ == "__main__": main()