測實戰(zhàn):從特征工程到回測避坑與基線驗證)
簡介這是一份關(guān)于LSTM神經(jīng)網(wǎng)絡(luò)股票預(yù)測算法的學(xué)術(shù)PDF論文面向關(guān)注量化投資、金融數(shù)據(jù)建模和深度學(xué)習(xí)的讀者用于理解如何借助循環(huán)神經(jīng)網(wǎng)絡(luò)預(yù)測股票最高價。文中從LSTM的選擇記憶機(jī)制與三門結(jié)構(gòu)入手介紹了PyTorch框架下的模型搭建、數(shù)據(jù)預(yù)處理、z-score標(biāo)準(zhǔn)化、梯度爆炸應(yīng)對策略并給出股票指數(shù)預(yù)測實驗及擬合曲線可直接作為時序預(yù)測項目的方法參考。壓縮包內(nèi)共1個PDF文件大小約897KB內(nèi)容為完整期刊論文包含算法實現(xiàn)流程、實驗原理和結(jié)果分析。該資源已有294人瀏覽學(xué)習(xí)適合作為深度學(xué)習(xí)建模與金融預(yù)測方向的入門研讀資料。1. 股票預(yù)測不是讓LSTM背K線這篇研究在解決什么你可能在論文庫或技術(shù)社區(qū)見過“基于LSTM神經(jīng)網(wǎng)絡(luò)的股票預(yù)測算法研究”這個標(biāo)題。它是很多量化入門項目最常選的方向但真正照著標(biāo)題去復(fù)現(xiàn)的人第一周就會碰到訓(xùn)練 loss 不降、預(yù)測曲線比真實行情慢半拍、回測很漂亮實盤卻連續(xù)虧損。問題不在 LSTM 本身而在輸入特征怎么選、標(biāo)簽怎么定義、訓(xùn)練集怎么切、評估怎么設(shè)計。LSTM 神經(jīng)網(wǎng)絡(luò)有記憶序列的能力可股票行情噪聲極大算法如果不圍繞“可落地的預(yù)測目標(biāo)”設(shè)計模型就是個黑匣子。下面按我跑通這類研究的順序拆解特征與窗口、訓(xùn)練參數(shù)、回測評估、避坑、最后用隨機(jī)基線審計模型。適合復(fù)現(xiàn)論文的在校生、量化學(xué)徒以及所有想用時間序列模型做預(yù)測的開發(fā)者。2. 把LSTM搬進(jìn)股票預(yù)測輸入特征與序列窗口怎么定LSTM 的輸入不是一整張行情表而是一段“按時間排序的特征切片”。很多新手只把收盤價丟進(jìn)去模型自然學(xué)不到穩(wěn)定規(guī)律。這個階段要解決三件事構(gòu)造哪些特征、用多長的歷史窗口、預(yù)測哪個標(biāo)簽。它們決定了模型能力的上限后面的調(diào)參只是在逼近這個上限。2.1 特征表OHLCV之外我給模型加了哪些衍生量股票原始行情只有開高低收和成交量直接喂給 LSTM 也能跑但效果通常一般。原因有兩個一是絕對價格具有非平穩(wěn)性不同股票的價位區(qū)間完全不同模型很難跨樣本學(xué)習(xí)二是單維價格序列攜帶的信息太少LSTM 找不到足夠的模式。常見做法是構(gòu)造一組衍生特征把絕對價格轉(zhuǎn)換成相對變化量。下面是我在類似算法研究中默認(rèn)使用的特征集特征計算方式設(shè)計意圖日對數(shù)收益率log(close / close.shift(1))平穩(wěn)化價格序列減少長期趨勢干擾當(dāng)日振幅(high - low) / close刻畫日內(nèi)波動狀態(tài)日內(nèi)位置(close - low) / (high - low)表示收盤價在當(dāng)日區(qū)間的強(qiáng)弱成交量變化率volume.pct_change()捕捉放量縮量行為5日與20日均線差MA5 - MA20給模型一個短期趨勢信號5日波動率近5日對數(shù)收益率標(biāo)準(zhǔn)差觀察風(fēng)險狀態(tài)變化需要特別說明不是特征越多越好。股票數(shù)據(jù)信噪比低幾十個強(qiáng)相關(guān)特征會把 LSTM 訓(xùn)練推向過擬合。我一般先構(gòu)建上表約 6 到 8 個特征再觀察相關(guān)性矩陣把與收盤價幾乎完全線性相關(guān)的原始 close 列刪掉只保留衍生量。這樣既保留了 LSTM 需要的序列結(jié)構(gòu)又避免了輸入維度膨脹。特征確定后要在切分?jǐn)?shù)據(jù)集之后再歸一化。我習(xí)慣用 sklearn 的 StandardScaler對每個特征列分別做 z-score 標(biāo)準(zhǔn)化。這里的注意點是只用訓(xùn)練集統(tǒng)計量去 fit驗證集和測試集只做 transform否則未來信息會泄漏進(jìn)訓(xùn)練過程。泄漏帶來的后果我會在第 5 章專門展開。2.2 窗口、標(biāo)簽與訓(xùn)練/驗證劃分LSTM 依賴“窗口長度”來讀取歷史。窗口太短模型看不到一個完整的短期形態(tài)窗口太長噪聲累積反而掩蓋真實規(guī)律。我跑這類股票預(yù)測研究時主要用兩個檔位預(yù)測未來一天的回歸任務(wù)窗口取 30 個交易日預(yù)測未來五天甚至更遠(yuǎn)時窗口取 60 個交易日。30 天大約一個半月的交易數(shù)據(jù)剛好覆蓋一波短期趨勢也不會引入太多噪聲。標(biāo)簽定義是第二個關(guān)鍵決策。論文里最常見的是回歸任務(wù)用前 30 個交易日的特征預(yù)測第 31 天的收盤價。但我后來更傾向于把標(biāo)簽改成“未來第 1 天的收盤價相對當(dāng)日收盤價的漲跌幅”或者直接做分類未來一天漲還是跌。原因后面在回測章節(jié)會講?;貧w任務(wù)適合做算法研究方向判斷更適合做投資決策。切分?jǐn)?shù)據(jù)集時絕不能隨機(jī)打亂。正確的做法是按時間順序切成三段前 70% 訓(xùn)練中間 20% 驗證最后 10% 作為測試集。驗證集用來選早停和超參數(shù)測試集只在最終評估時碰一次。這是時間序列任務(wù)的基本紀(jì)律任何像train_test_split(X, y, shuffleTrue)的默認(rèn)行為都會讓股票預(yù)測變成記憶游戲。2.3 用PyTorch搭一個能跑的LSTM預(yù)測模型這里給出最精簡的 LSTM 預(yù)測模型。我用 PyTorch 而不是 Keras是因為訓(xùn)練循環(huán)更透明調(diào)試梯度問題時能直接看到中間結(jié)果。import numpy as np import torch import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_size, hidden_size64, num_layers2, dropout0.2): super().__init__() # input_size: 每個時間步的特征數(shù)量 # hidden_size: LSTM單元數(shù)量, 太大容易過擬合 # num_layers: 疊層數(shù), 股票場景盡量不超過2層 self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout if num_layers 1 else 0, ) self.fc nn.Linear(hidden_size, 1) def forward(self, x): # x: (batch, seq_len, input_size) out, _ self.lstm(x) # 取最后一個時間步的輸出作為序列表示 last_hidden out[:, -1, :] # 線性層輸出預(yù)測值 return self.fc(last_hidden)這段代碼的關(guān)鍵點有兩個。第一batch_firstTrue讓輸入形狀變成(batch, seq_len, features)更符合直覺也方便從 pandas 構(gòu)造數(shù)據(jù)。第二LSTM 每一時間步都會輸出一個隱藏狀態(tài)預(yù)測用的是最后一個時間步的隱藏狀態(tài)因為它理論上匯總了整段 30 日歷史的信息。如果想讓模型輸出漲跌方向把fc的輸出維度改成 2 并接一個 CrossEntropyLoss 就行。接下來是數(shù)據(jù)準(zhǔn)備。假設(shè)已經(jīng)構(gòu)造好一個二維數(shù)組df_feat每一行是一個交易日的特征列最后一列是標(biāo)簽。用滑窗切片生成 LSTM 樣本def make_sequences(df, feature_cols, label_colreturn_t1, seq_len30): data df[feature_cols [label_col]].values xs, ys [], [] for i in range(seq_len, len(data)): # 取前seq_len天的特征, 預(yù)測第i天的標(biāo)簽 xs.append(data[i-seq_len:i, :-1].astype(float32)) ys.append(data[i, -1].astype(float32)) return np.array(xs), np.array(ys)這里要強(qiáng)調(diào)一個容易寫錯的小細(xì)節(jié)data[i-seq_len:i]是不包括第i天的即用第i-30到第i-1天預(yù)測第i天。這樣才能保證訓(xùn)練數(shù)據(jù)里沒有“用明天預(yù)測今天”的未來函數(shù)。生成完樣本后再按 7:2:1 時間順序切分即可不需要額外處理。3. 訓(xùn)練LSTM股票模型的核心參數(shù)學(xué)習(xí)率、批次、隱藏單元和早停模型結(jié)構(gòu)只是骨架訓(xùn)練參數(shù)才是真正讓 LSTM 收斂的手段。推薦配置里學(xué)習(xí)率、批次大小、隱藏單元數(shù)、層數(shù)和早停都必須調(diào)過。這里不是套一個 Adam 就完事股票數(shù)據(jù)的梯度噪聲非常大參數(shù)配不好訓(xùn)練曲線會在幾個 epoch 后突然發(fā)散。3.1 學(xué)習(xí)率與優(yōu)化器Adam不是萬能我也會試SGDAdam 是我在這個場景下的默認(rèn)優(yōu)化器學(xué)習(xí)率從 1e-3 起步。但這個學(xué)習(xí)率并不總是合適特征維度多時1e-3 容易讓 loss 震蕩特征只用了 6 到 8 個基礎(chǔ)量時1e-3 收斂又偏慢。我一般先用 Adam 跑 20 個 epoch觀察驗證集 loss 的走勢如果 loss 在早期就出現(xiàn)尖刺就把初始學(xué)習(xí)率降到 3e-4。訓(xùn)練循環(huán)里還有兩個股票場景下必須加的步驟梯度裁剪和驗證集調(diào)度。梯度裁剪解決的是長序列上梯度爆炸的問題調(diào)度器則能在驗證集 loss 進(jìn)入平臺期時自動降低學(xué)習(xí)率。參考代碼criterion nn.MSELoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, factor0.5, patience5 ) for epoch in range(200): model.train() total_loss 0.0 for start in range(0, len(X_train), 64): X_batch torch.tensor(X_train[start:start64], dtypetorch.float32) y_batch torch.tensor(y_train[start:start64], dtypetorch.float32) optimizer.zero_grad() pred model(X_batch).squeeze() loss criterion(pred, y_batch) loss.backward() # 防止梯度爆炸, 原因為行情數(shù)據(jù)偶爾出現(xiàn)異常跳變 nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0) optimizer.step() total_loss loss.item() model.eval() # 假設(shè)有一個compute_val_loss函數(shù) val_loss compute_val_loss(model, X_val, y_val) scheduler.step(val_loss)參數(shù)說明max_norm5.0表示把所有參數(shù)的梯度模長限制在 5 以內(nèi)超過則等比縮放。太大起不到保護(hù)作用太小又會讓訓(xùn)練緩慢在 LSTM 股票預(yù)測里 5 是一個比較穩(wěn)的起點。ReduceLROnPlateau的 patience 設(shè) 5表示驗證集 loss 連續(xù) 5 個 epoch 不創(chuàng)新低就把學(xué)習(xí)率減半。這樣不需要人工盯著 loss 曲線改學(xué)習(xí)率。3.2 隱藏單元數(shù)與層數(shù)通用神經(jīng)網(wǎng)絡(luò)里的“容量陷阱”LSTM 不是層數(shù)越多越聰明。股票數(shù)據(jù)的有效信號非常弱模型容量一旦超出必要范圍就會把噪聲當(dāng)規(guī)律。我在歷史項目里見過最典型的例子把num_layers從 1 提到 3訓(xùn)練集 loss 降得很漂亮但驗證集方向準(zhǔn)確率反而從 54% 掉到 49%這就是過擬合。我的默認(rèn)配置是hidden_size64, num_layers2, dropout0.2。如果訓(xùn)練樣本只有幾千條hidden_size要降到 32如果用了大量股票做聯(lián)合訓(xùn)練可以提升到 128。層數(shù)方面絕大多數(shù)股票預(yù)測研究用一層或兩層就足夠。多層的收益在金融噪聲面前非常有限但參數(shù)量和訓(xùn)練時間會成倍增長。訓(xùn)練時建議固定隨機(jī)種子。LSTM 權(quán)重初始化對收斂影響明顯固定種子至少保證同一份代碼兩次運行結(jié)果一致調(diào)參時才能判斷改進(jìn)來自參數(shù)還是來自運氣。import random import numpy as np import torch random.seed(42) np.random.seed(42) torch.manual_seed(42) torch.cuda.manual_seed_all(42)這段代碼放在數(shù)據(jù)切分之后、模型創(chuàng)建之前即可。注意它不能消除所有隨機(jī)性在 GPU 上某些原子操作仍會有微小差異但對復(fù)現(xiàn)實驗已經(jīng)足夠。3.3 早停與模型保存用驗證集而不是測試集挑模型訓(xùn)練 LSTM 股票模型最容易犯把測試集當(dāng)“調(diào)參助手”的錯。正確流程是用訓(xùn)練集更新權(quán)重每輪在驗證集上計算 loss當(dāng)驗證集 loss 連續(xù)多個 epoch 不下降就停止訓(xùn)練并回退到歷史最優(yōu)參數(shù)。測試集只能最后碰一次。早停代碼可以這樣寫best_val_loss float(inf) best_epoch 0 patience_counter 0 patience 15 for epoch in range(200): # 上面的訓(xùn)練循環(huán) train_loss total_loss / len(X_train) val_loss compute_val_loss(model, X_val, y_val) if val_loss best_val_loss: best_val_loss val_loss best_epoch epoch patience_counter 0 torch.save(model.state_dict(), best_lstm.pt) else: patience_counter 1 if patience_counter patience: print(fearly stop at epoch {epoch}, best epoch {best_epoch}) breakpatience15是一個經(jīng)驗值太小會導(dǎo)致早停在訓(xùn)練初期太大則失去早停意義。保存時只保存state_dict不要保存整個 model 對象方便后續(xù)加載和做 walk-forward 驗證。加載方式就是model.load_state_dict(torch.load(best_lstm.pt))。如果嫌手寫循環(huán)麻煩建議用一個超參速查表作為默認(rèn)起點參數(shù)常見范圍默認(rèn)值學(xué)習(xí)率1e-4 ~ 1e-21e-3batch_size32 ~ 12864hidden_size32 ~ 12864num_layers1 ~ 22dropout0.1 ~ 0.50.2seq_len20 ~ 6030patience10 ~ 2515這個表不是萬能參數(shù)而是排錯起點。任何一組參數(shù)被驗證集證明有效之后都應(yīng)該把測試集上的結(jié)果單獨記錄避免陷入“調(diào)參調(diào)得好看但實盤失效”的循環(huán)。4. 回測與評估別拿RMSE當(dāng)交易信號訓(xùn)練完模型很多研究文章習(xí)慣只在測試集上報告 RMSE 或 R2。但對股票預(yù)測來說RMSE 低不等于能落地。更常見的現(xiàn)象是模型把昨天的價格預(yù)測成今天的價格RMSE 很小時實際策略卻無法操作。所以回測與評估必須圍繞“方向”和“收益”來設(shè)計。4.1 回測框架預(yù)測方向比預(yù)測價格更接近落地股票買入賣出靠的是方向判斷不是精確到下一位的價格預(yù)測。一個模型預(yù)測次日收盤價為 10.01 元今日收盤 10.00 元它告訴你要漲另一個模型預(yù)測準(zhǔn)確到 10.005 元但方向是跌那第一次操作就會虧損。因此把回歸輸出轉(zhuǎn)為方向信號是常規(guī)做法# pred: 模型預(yù)測的下一個交易日收盤價 # close: 測試集當(dāng)日真實收盤價 signal np.where(pred close, 1, 0)signal1表示次日持有或買入signal0表示空倉。這里的關(guān)鍵是必須使用“下一個交易日”的預(yù)測結(jié)果和“當(dāng)日”價格比較不能拿當(dāng)日預(yù)測當(dāng)日。如果預(yù)測目標(biāo)本身就是未來一天漲跌幅那 signal 邏輯變成pred 0更直接。方向信號只是第一步回測還需要一個可執(zhí)行的時間線。第 T 日收盤產(chǎn)生信號第 T1 日開盤執(zhí)行收益從第 T1 日到第 T2 日實現(xiàn)。這樣處理是為了避免“收盤看到信號、當(dāng)日按收盤價成交”這種實盤根本不存在的未來函數(shù)。4.2 評估指標(biāo)用收益率、最大回撤和換手率一起看只有累計收益率的策略報告沒有說服力。我通常至少記錄五個指標(biāo)指標(biāo)定義關(guān)注原因累計收益率策略凈值最后值減 1衡量總體收益年化波動率日收益標(biāo)準(zhǔn)差乘 sqrt(252)衡量風(fēng)險最大回撤凈值從峰值到谷底的最大跌幅決定能否拿住夏普比率策略超額收益除以波動率衡量風(fēng)險調(diào)整后收益換手率調(diào)倉次數(shù)除以持倉天數(shù)估算交易成本在股票預(yù)測算法研究里夏普比率和最大回撤比 RMSE 更接近任務(wù)目標(biāo)。一個累計收益很高但最大回撤超過 30% 的策略實盤很容易被震下車換手率過高則意味著手續(xù)費會吃掉大部分利潤?;販y時要加入單邊手續(xù)費和滑點按 A 股萬 2.5 傭金加千 1 印花稅的簡化模型單次換倉成本約 0.1% 到 0.15%。4.3 一個簡單的策略回測腳本基于前面的信號向量可以寫一個最簡回測。假設(shè)close是測試集日收盤價數(shù)組signal是已經(jīng)產(chǎn)生的倉位信號# 日收益: 當(dāng)日close相對前日close的漲跌幅 daily_ret np.diff(close) / close[:-1] # signal[i]表示第i日持倉狀態(tài), 收益從第i日到第i1日實現(xiàn) # 所以對齊時去掉signal最后一個元素 strategy_ret daily_ret * signal[:-1] # 加入換手成本: 倉位變化時才收費 turnover np.abs(np.diff(signal)) strategy_ret strategy_ret - 0.001 * turnover[:len(strategy_ret)] # 累計凈值曲線 equity np.cumprod(1 strategy_ret)這段代碼的邏輯說明signal[:-1]與daily_ret對齊含義是“第 i 天的持倉狀態(tài)拿到第 i1 天的收益”。turnover計算倉位從 0 到 1 或 1 到 0 的次數(shù)每次扣 0.1% 交易成本。然后累計乘積就是策略凈值。買入持有基準(zhǔn)可以直接用np.cumprod(1 daily_ret)得到策略凈值只有持續(xù)跑贏這個基準(zhǔn)才有意義。回測結(jié)果如果出現(xiàn)累計收益率很高但最大回撤集中在幾個極值日那要先檢查是否有復(fù)權(quán)問題或漲跌停樣本。金融數(shù)據(jù)的極端值會讓 LSTM 學(xué)到“暴力模式”這種模式在實盤中往往不可交易具體排查放下一章。5. LSTM股票預(yù)測避坑指南從數(shù)據(jù)泄漏到歸一化翻車這一章來自實際操作中翻過車的點。每一條都按照“現(xiàn)象 → 原因 → 解決”來寫希望讀者少走彎路。5.1 數(shù)據(jù)泄漏歸一化用了全局統(tǒng)計量預(yù)測結(jié)果虛高現(xiàn)象回測凈值曲線漂亮得驚人測試集方向命中率超過 80%但換一段新行情就完全失效。原因在切分訓(xùn)練集和測試集之前直接對整個數(shù)據(jù)集做了歸一化。StandardScaler在全部數(shù)據(jù)上 fit 完后測試集的均值和方差混進(jìn)了訓(xùn)練過程等價于讓模型偷偷看到了未來數(shù)據(jù)的分布特征。這種情況在股票預(yù)測研究里非常常見。解決先按時間順序切分再只用訓(xùn)練集 fit 縮放器驗證集和測試集只做 transform。代碼示例from sklearn.preprocessing import StandardScaler X_train_raw, X_val_raw, X_test_raw split_by_time(data) scaler StandardScaler() X_train scaler.fit_transform(X_train_raw) X_val scaler.transform(X_val_raw) X_test scaler.transform(X_test_raw)這里必須將scaler保存下來線上預(yù)測時加載同一個對象再用與訓(xùn)練集相同的方式處理新樣本。否則未來新數(shù)據(jù)無法套用任何全局統(tǒng)計量。5.2 時間序列亂劃分隨機(jī)打亂訓(xùn)練集等于作弊現(xiàn)象測試集準(zhǔn)確率極高訓(xùn)練曲線幾乎無波動。仔細(xì)檢查發(fā)現(xiàn)數(shù)據(jù)加載時用了帶shuffleTrue的劃分函數(shù)。原因股票相鄰時間的樣本高度相關(guān)。第 t 天的特征與第 t1 天的標(biāo)簽幾乎包含了相同的信息隨機(jī)打亂后訓(xùn)練集和驗證集可能同時出現(xiàn)這兩條樣本模型實際上把鄰近樣本背了下來而不是學(xué)到規(guī)律。解決使用順序切分并在切分處留出一個間隙。預(yù)測目標(biāo)是未來 1 天時訓(xùn)練集末端與驗證集起點之間至少空出 5 個交易日避免因滑窗重疊導(dǎo)致的標(biāo)簽間泄漏。代碼gap 5 train_end 700 val_start train_end gap val_end 900 test_start val_end gapgap的大小要大于預(yù)測步長。如果預(yù)測未來 5 天gap至少設(shè)為 5甚至 10 更穩(wěn)妥。5.3 預(yù)測值滯后LSTM把昨天的價格搬出來現(xiàn)象預(yù)測曲線幾乎貼著真實收盤價但比真實曲線整體右移一天。RMSE 很低方向命中率卻只有 50% 左右。原因干凈的價格序列接近隨機(jī)游走。如果模型輸入包含前一日收盤價輸出被訓(xùn)練成“盡可能接近當(dāng)前價”那么最優(yōu)預(yù)測就是“上一日收盤價”。LSTM 也能學(xué)會這一點所以預(yù)測曲線就像把真實價格平移了一天。解決不要直接回歸絕對收盤價改為預(yù)測收益率或漲跌方向。同時必須加入一個樸素基線假設(shè)預(yù)測值等于今日收盤價。如果模型的方向命中率不能顯著超過這個基線說明學(xué)到的大部分是滯后信息。以下代碼可以打印基線與模型的方向命中率baseline_pred close[:-1] model_pred pred[:-1] baseline_acc np.mean((np.diff(close) 0) (np.diff(baseline_pred) 0)) model_acc np.mean((np.diff(close) 0) (np.diff(model_pred) 0))注意模型預(yù)測序列是下一日的價格所以在比較方向時也要把軸對齊。模型至少要比基線高 3 到 5 個百分點才有繼續(xù)優(yōu)化的價值。5.4 歸一化反向轉(zhuǎn)換不當(dāng)序列被壓縮成一條直線現(xiàn)象把模型輸出的歸一化數(shù)值還原成價格后預(yù)測序列是一條近似水平線或者范圍極窄。原因訓(xùn)練時把特征和目標(biāo)放一起歸一化預(yù)測時又拿特征縮放器去還原目標(biāo)。特征的均值和方差與價格標(biāo)簽完全不同還原后的結(jié)果自然不對。另一個常見問題是忘記保存目標(biāo)縮放器直接在預(yù)測時手動減均值除方差。解決特征和目標(biāo)各用獨立的縮放器。目標(biāo)通常是單列數(shù)組可以這樣處理feature_scaler StandardScaler() target_scaler StandardScaler() X_train feature_scaler.fit_transform(X_train_raw) y_train target_scaler.fit_transform(y_train.reshape(-1, 1)).ravel() # 預(yù)測后 pred_real target_scaler.inverse_transform(pred_scaled.reshape(-1, 1)).ravel()target_scaler要和模型權(quán)重一起保存。預(yù)測階段加載模型權(quán)重后也必須加載這個目標(biāo)縮放器否則所有預(yù)測都停留在歸一化空間無法投入實際使用。5.5 漲跌停與停牌未復(fù)權(quán)數(shù)據(jù)帶來的假信號現(xiàn)象策略回測在某幾天收益突然大幅拉升但復(fù)盤發(fā)現(xiàn)那幾天對應(yīng)的是漲停一字板實盤根本買不進(jìn)去。原因數(shù)據(jù)沒有排除漲跌停和非交易停牌樣本也沒有做復(fù)權(quán)處理。漲停板個股的可用流通籌碼極少回測按收盤價成交是不現(xiàn)實的。此外未復(fù)權(quán)價格在除權(quán)除息日會出現(xiàn)人為下跌LSTM 會把這個“價格跳空”當(dāng)成正常行情。解決統(tǒng)一使用前復(fù)權(quán)行情剔除停牌日。對連續(xù)一字漲?;蜷_盤即漲停的樣本把對應(yīng)信號強(qiáng)制設(shè)為 0。簡化寫法# 漲停判定: 當(dāng)日漲幅超過9.8%, 近似判斷 df[limit_up] (df[close] df[close].shift(1) * 1.098) # 出現(xiàn)漲停后的交易信號不允許“當(dāng)日收盤買入” tradable ~df[limit_up]還需要過濾 ST 股和上市不足半年的次新股。這類標(biāo)的的漲跌幅規(guī)則與普通股不同噪音也更大會讓 LSTM 的注意力被異常樣本帶偏。6. 落地驗證的最后一公里用隨機(jī)標(biāo)簽基線檢驗LSTM是否在學(xué)規(guī)律當(dāng)模型在回測里跑出不錯的結(jié)果別急著總結(jié)。傳統(tǒng)論文驗證方式只做訓(xùn)練集和測試集切分但股票模型很容易出現(xiàn)過擬合而不自知。一個可靠的驗證方法是引入隨機(jī)標(biāo)簽基線也就是把訓(xùn)練標(biāo)簽打亂后重訓(xùn)同一個模型反復(fù)多次得到一組“瞎猜”的成績再判斷真實模型是否顯著優(yōu)于這組成績。6.1 隨機(jī)標(biāo)簽基線最簡單的過擬合檢測隨機(jī)標(biāo)簽不影響特征和窗口只是把標(biāo)簽到樣本的對應(yīng)關(guān)系破壞掉。如果原模型有效它會把數(shù)據(jù)中的真實規(guī)律保存下來如果模型只是記住了噪聲那么在打亂標(biāo)簽后它仍然能從特征里“看”出一些訓(xùn)練樣本的記憶驗證集成績也不會差太多。實現(xiàn)思路很直接# 對回歸模型, 記錄驗證集方向命中率 random_scores [] for seed in range(10): np.random.seed(seed) y_train_shuffled np.random.permutation(y_train) # 用相同的超參數(shù)重新訓(xùn)練模型 model train_lstm(X_train, y_train_shuffled, X_val, y_val) acc direction_accuracy(model, X_val, y_val_close) random_scores.append(acc) random_mean np.mean(random_scores) real_acc direction_accuracy(best_model, X_val, y_val_close)如果real_acc比random_mean高 2 個百分點以內(nèi)說明當(dāng)前特征和模型并沒有學(xué)到超越噪聲的有效信息。我習(xí)慣把隨機(jī)基線結(jié)果直接寫進(jìn)實驗記錄真實方向命中率 56%隨機(jī)基線 50% ± 1%說明模型有一點信號如果真實只有 52%隨機(jī)基線 50%那大概率是過擬合或數(shù)據(jù)泄漏。這個測試跑起來通常只要幾十分鐘比繼續(xù)調(diào)參省時間。6.2 滾動前推驗證更接近線上環(huán)境隨機(jī)標(biāo)簽基線只驗證了靜態(tài)切分下的穩(wěn)定性還需要做滾動前推驗證。做法是不斷把訓(xùn)練數(shù)據(jù)向后移動讓模型在不同行情段上重復(fù)訓(xùn)練和測試。例如第一次用 2018 到 2020 訓(xùn)練2021 驗證2022 測試第二次用 2018 到 2021 訓(xùn)練2022 驗證2023 測試如此類推。每個滾動窗口都記錄方向命中率和隨機(jī)基線得分。如果某個窗口內(nèi)真實模型不能穩(wěn)定超過隨機(jī)基線說明模型對市場風(fēng)格變化敏感研究結(jié)論需要謹(jǐn)慎。最后說一句大實話我每次報告實驗結(jié)果都會附上隨機(jī)標(biāo)簽基線否則再漂亮的收益曲線都經(jīng)不起追問。多寫這十幾行驗證邏輯能替你省下無數(shù)次自我感動式的調(diào)參。希望幫到你。本文還有配套的精品資源點擊獲取