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