習(xí)網(wǎng)絡(luò)(DQN)股票交易實戰(zhàn):從環(huán)境建模到回測避坑全解析)
簡介一份基于深度Q學(xué)習(xí)網(wǎng)絡(luò)的股票交易策略設(shè)計源碼面向量化交易入門者、金融AI研究者和Python算法開發(fā)人員解決如何利用強(qiáng)化學(xué)習(xí)對歷史行情建模并自動生成交易決策的問題。壓縮包共24個文件約12.21MB包含Python核心腳本、CSV行情數(shù)據(jù)、TensorFlow模型檢查點與說明文檔覆蓋數(shù)據(jù)預(yù)處理、訓(xùn)練、回測等完整鏈路。項目提供分鐘級與日級深證個股數(shù)據(jù)DQN_trade.py實現(xiàn)智能體決策邏輯stock_env.py構(gòu)建模擬交易環(huán)境test.py支持策略回測saved_network中保存了多個訓(xùn)練好的網(wǎng)絡(luò)權(quán)重可基于已有模型繼續(xù)訓(xùn)練或直接驗證效果。已有330人學(xué)習(xí)下載適合希望快速上手深度強(qiáng)化學(xué)習(xí)在股票交易中應(yīng)用的開發(fā)者源碼結(jié)構(gòu)清晰稍作修改即可遷移到其他市場或策略場景。1. 深度Q學(xué)習(xí)網(wǎng)絡(luò)做股票交易從“決策會反悔”開始的設(shè)計閉環(huán)把股票交易交給深度Q學(xué)習(xí)網(wǎng)絡(luò)DQN來做經(jīng)常被當(dāng)成玄學(xué)其實它的核心邏輯一句話能說清讓模型學(xué)會回答“在當(dāng)前市場狀態(tài)下做哪個動作最劃算并且把動作之后的未來收益也考慮進(jìn)去”。這套基于深度Q學(xué)習(xí)網(wǎng)絡(luò)的股票交易策略設(shè)計源碼落地的價值不在“預(yù)測漲跌”而在于把交易決策變成可訓(xùn)練、可回測、可排錯的序貫決策問題環(huán)境模塊模擬市場Q網(wǎng)絡(luò)估計動作價值經(jīng)驗回放和目標(biāo)網(wǎng)絡(luò)負(fù)責(zé)解決訓(xùn)練穩(wěn)定性的老問題。適合手里有 Python 和機(jī)器學(xué)習(xí)基礎(chǔ)、想認(rèn)真驗證強(qiáng)化學(xué)習(xí)在交易上有多大價值的人不適合指望導(dǎo)出模型就穩(wěn)賺的人。接下來我把建模、代碼落地和踩坑點按可復(fù)現(xiàn)的順序過一遍。2. 把股票交易改造成強(qiáng)化學(xué)習(xí)問題狀態(tài)、動作與獎勵的三步建模2.1 交易環(huán)境是第一個要寫死的模塊數(shù)據(jù)、撮合與手續(xù)費(fèi)很多人拿到源碼第一件事是看網(wǎng)絡(luò)結(jié)構(gòu)這是誤區(qū)。深度Q學(xué)習(xí)網(wǎng)絡(luò)在交易場景里翻車八成不是網(wǎng)絡(luò)的問題是環(huán)境寫錯了。環(huán)境在這里做的不是“預(yù)測行情”而是把“給定狀態(tài) → 執(zhí)行動作 → 得到獎勵 → 進(jìn)入下一個狀態(tài)”這個過程完整閉環(huán)。一個寫壞的環(huán)境會直接污染所有后續(xù)訓(xùn)練結(jié)果。我寫環(huán)境時遵循一個原則模擬器能多簡單就多簡單但三個東西必須嚴(yán)格——價格時序、手續(xù)費(fèi)、撮合時點。下面是一個能直接跑通最小訓(xùn)練循環(huán)的單標(biāo)的交易環(huán)境動作空間是 0空倉、1持有、2買入import numpy as np import pandas as pd class TradingEnv: 單標(biāo)的離散動作交易環(huán)境滿倉進(jìn)滿倉出T0 簡化撮合 def __init__(self, df, features, window30, cost0.001): self.df df.reset_index(dropTrue) self.features features self.window window self.cost cost # 單邊交易成本按比例扣 self.reset() def reset(self): self.t self.window self.position 0 # 0 空倉1 持有 self.cash 1.0 # 初始資金歸一化 self.shares 0.0 self.prev_price float(self.df[close].iloc[self.t - 1]) return self._get_state() def _get_state(self): # 取最近 window 根 K 線的特征矩陣壓平成一維 x self.df[self.features].iloc[self.t - self.window:self.t].values return x.flatten().astype(np.float32) def step(self, action): price float(self.df[close].iloc[self.t]) reward 0.0 # 第一步結(jié)算上一個 bar 到當(dāng)前 bar 的持倉浮盈 if self.position 1: log_ret np.log(price / self.prev_price) reward log_ret # 第二步執(zhí)行當(dāng)前動作扣交易成本 # 買入空倉狀態(tài)下才成交 if action 2 and self.position 0: self.shares self.cash / price self.cash 0.0 self.position 1 reward - self.cost # 賣出持有狀態(tài)下才成交 elif action 0 and self.position 1: self.cash self.shares * price self.shares 0.0 self.position 0 reward - self.cost # 動作 1 是持有不改變倉位也不額外扣費(fèi) self.prev_price price self.t 1 done self.t len(self.df) - 1 return self._get_state(), reward, done, {}代碼邏輯分三步走先結(jié)算浮盈再執(zhí)行動作最后推進(jìn)時間步。獎勵使用對數(shù)收益log(price / prev_price)好處是收益可以直接跨時間相加不會因為價格的絕對水平導(dǎo)致獎勵尺度漂移。注意買入和賣出都加入了reward - self.cost交易成本是 DQN 能不能學(xué)會“少動手”的關(guān)鍵約束去掉它模型會退化成高頻亂動。參數(shù)上有幾個點我習(xí)慣固定下來window30表示用過去 30 根 K 線做狀態(tài)特征數(shù)量由features決定cost0.001是單邊千一如果你做 A 股把印花稅和滑點一起算進(jìn)去單邊 0.001 到 0.002 都是常見取值。這個環(huán)境沒做漲跌停限制也沒區(qū)分 T1目的是先跑通訓(xùn)練真實約束后面再逐條加一次加太多約束會讓模型學(xué)不動。2.2 狀態(tài)空間設(shè)計行情窗口、技術(shù)特征與“不要碰未來數(shù)據(jù)”的紅線狀態(tài)空間決定了模型能看到什么。我見過很多人把網(wǎng)上那些指標(biāo)源碼里的金叉死叉信號直接拿來當(dāng)特征結(jié)果模型學(xué)到的是指標(biāo)的滯后轉(zhuǎn)譯而不是交易邏輯本身。這不是說技術(shù)指標(biāo)不能用而是用之前必須明確特征向量是給 Q 網(wǎng)絡(luò)做價值判斷的輸入不是信號生成器。我的常用特征是八到十二個維度的組合全部基于滾動窗口內(nèi)已知數(shù)據(jù)計算收益率、最高最低價相對收盤的位置、成交量變化率、ATR平均真實波幅、RSI、MACD 線與其信號線的差。關(guān)鍵約束是所有特征在 t 時刻必須只用 t 時刻之前的數(shù)據(jù)最穩(wěn)妥的辦法是先用shift(1)把整列特征向后平移一根 K 線再進(jìn)環(huán)境。def prepare_features(df): 構(gòu)造特征并統(tǒng)一做 z-score 標(biāo)準(zhǔn)化注意用 expand 窗口避免未來數(shù)據(jù)泄漏 df df.copy() df[ret] df[close].pct_change() df[high_low_pos] (df[close] - df[low]) / (df[high] - df[low]) df[vol_ratio] df[volume] / df[volume].rolling(20).mean() # 計算 RSI 這類指標(biāo)后統(tǒng)一 shift(1) df df.replace([np.inf, -np.inf], np.nan).fillna(0) return df這里有兩個坑。第一個是標(biāo)準(zhǔn)化不能用全樣本的均值和方差做 z-score那等于讓訓(xùn)練集偷看了測試集的分布正確做法是在訓(xùn)練集上fit一個StandardScaler再應(yīng)用到測試集。第二個是shift(1)它能擋住所有計算出來的指標(biāo)但對一些天生帶未來信息的字段沒用比如直接用close.shift(-1)做標(biāo)簽這種低級前視偏差我在別人的課程設(shè)計源碼里見過不少回測曲線好看得離譜實盤直接歸零。2.3 獎勵函數(shù)選型單步收益、持倉懲罰與折扣因子的配合獎勵函數(shù)是深度Q學(xué)習(xí)網(wǎng)絡(luò)在交易場景里最敏感的旋鈕。最直覺的做法是“賺錢給正獎勵、虧錢給負(fù)獎勵”但直接這么寫會讓模型變得極其短視它會抓住單步大波動去頻繁交易因為每步的即時獎勵比折扣后的長期價值更吸引人。我的做法是把獎勵拆成兩段持倉浮盈變化加上交易成本懲罰再疊加一個可選的“非必要不交易”懲罰項。環(huán)境代碼里已經(jīng)實現(xiàn)了前兩段第三段可以在訓(xùn)練時額外控制——每次判斷動作與當(dāng)前倉位不一致時額外減去一個小常數(shù)。下面是獎勵含義的對照關(guān)系方便你按自己的策略微調(diào)獎勵來源計算方式作用持倉浮盈log(price_t / price_{t-1})讓模型感知持倉收益變化交易成本單邊固定比例成本抑制過度交易動作切換懲罰每次買賣額外減 0.00020.0005降低無效換手終點大獎勵最后一刻結(jié)算累計收益強(qiáng)化長期目標(biāo)但只適合短周期折扣因子gamma在這里的作用比在游戲里更重要。金融市場是強(qiáng)非平穩(wěn)環(huán)境gamma0.99意味著模型會看重未來幾十步的收益這適合趨勢跟蹤如果做高頻gamma要降到 0.95 以下讓模型更關(guān)注眼前。我一般先固定gamma0.99看訓(xùn)練曲線的穩(wěn)定性再決定是否調(diào)整不要一開始就同時調(diào)三個旋鈕。3. 核心組件實現(xiàn)經(jīng)驗回放、目標(biāo)網(wǎng)絡(luò)和ε-greedy策略的代碼落地3.1 Q網(wǎng)絡(luò)與經(jīng)驗回放打破樣本相關(guān)性的第一個關(guān)鍵交易數(shù)據(jù)天生是時間序列相鄰樣本高度相關(guān)。如果像普通監(jiān)督學(xué)習(xí)那樣把相鄰狀態(tài)直接喂給網(wǎng)絡(luò)更新梯度Q值的估計會像醉漢走路一樣來回震蕩。經(jīng)驗回放就是為了解決這個問題把轉(zhuǎn)移樣本(s, a, r, s2, done)存進(jìn)一個大緩沖區(qū)訓(xùn)練時隨機(jī)抽樣一個小批量。隨機(jī)抽樣在數(shù)學(xué)上的作用是打破樣本間的時間相關(guān)性讓梯度更新近似滿足獨(dú)立同分布假設(shè)這是深度Q學(xué)習(xí)網(wǎng)絡(luò)能穩(wěn)定收斂的基石。網(wǎng)絡(luò)結(jié)構(gòu)我固定用最簡單的三層全連接別一開始就上 LSTM 或者 Transformer先讓整個訓(xùn)練循環(huán)能跑通再說。常見實現(xiàn)如下import torch import torch.nn as nn from collections import deque import random class QNetwork(nn.Module): 三層全連接 Q 網(wǎng)絡(luò)輸入狀態(tài)輸出每個動作的 Q 值 def __init__(self, state_dim, hidden128, n_actions3): super().__init__() self.net nn.Sequential( nn.Linear(state_dim, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, n_actions) ) def forward(self, x): return self.net(x) class ReplayBuffer: 環(huán)形經(jīng)驗回放緩沖區(qū)容量滿后自動覆蓋舊樣本 def __init__(self, capacity100_000): self.buffer deque(maxlencapacity) def push(self, s, a, r, s2, done): self.buffer.append((s, a, r, s2, done)) def sample(self, batch_size): batch random.sample(self.buffer, batch_size) states torch.from_numpy(np.stack([x[0] for x in batch])) actions torch.tensor([x[1] for x in batch], dtypetorch.int64) rewards torch.tensor([x[2] for x in batch], dtypetorch.float32) next_states torch.from_numpy(np.stack([x[3] for x in batch])) dones torch.tensor([x[4] for x in batch], dtypetorch.float32) return states, actions, rewards, next_states, dones回放容量的選擇有講究。容量太小緩沖區(qū)里全是近期交易經(jīng)驗時間相關(guān)性沒有被打散容量太大老樣本占多數(shù)模型跟不上市場近期的狀態(tài)分布變化。日線上做交易我的經(jīng)驗是5_000到50_000之間比較合適。選擇10_000時假設(shè)一個 episode 是 200 步緩沖區(qū)大約覆蓋 50 個完整交易周期足夠多樣化又不會讓舊經(jīng)驗把新趨勢蓋住。3.2 目標(biāo)網(wǎng)絡(luò)延遲復(fù)制參數(shù)為什么能穩(wěn)住訓(xùn)練如果不加目標(biāo)網(wǎng)絡(luò)Q 值的更新公式里r gamma * max(Q(s2))用的和預(yù)測用的是同一個網(wǎng)絡(luò)這會造成“自己追自己尾巴”的效應(yīng)目標(biāo)一直在變梯度更新方向不穩(wěn)定表現(xiàn)為 loss 曲線鋸齒狀劇烈震蕩、甚至發(fā)散。目標(biāo)網(wǎng)絡(luò)就是給更新目標(biāo)加了一層延遲——用一份舊參數(shù)的網(wǎng)絡(luò)去計算目標(biāo) Q 值每隔固定步數(shù)再把當(dāng)前網(wǎng)絡(luò)的參數(shù)復(fù)制過去。這種延遲參數(shù)復(fù)制機(jī)制在一個訓(xùn)練循環(huán)里就幾行代碼但它的作用被很多人低估。我用一個全局步數(shù)計數(shù)器控制同步節(jié)奏每UPDATE_TARGET200步同步一次讓目標(biāo)網(wǎng)絡(luò)比當(dāng)前網(wǎng)絡(luò)“慢半拍”Q 值更新時目標(biāo)相對穩(wěn)定訓(xùn)練自然就穩(wěn)了。以下是帶目標(biāo)網(wǎng)絡(luò)同步的最小訓(xùn)練主循環(huán)optimizer torch.optim.Adam(q_net.parameters(), lr1e-4) replay ReplayBuffer(capacity10_000) target_net QNetwork(state_dim) target_net.load_state_dict(q_net.state_dict()) UPDATE_TARGET 200 BATCH_SIZE 64 GAMMA 0.99 EPSILON 1.0 EPS_DECAY 0.998 EPS_MIN 0.05 global_step 0 for episode in range(200): state env.reset() done False while not done: # ε-greedy 選擇動作 if random.random() EPSILON: action random.randint(0, 2) else: with torch.no_grad(): state_t torch.from_numpy(state).unsqueeze(0) q_values q_net(state_t) action q_values.argmax().item() next_state, reward, done, _ env.step(action) replay.push(state, action, reward, next_state, done) if len(replay.buffer) BATCH_SIZE: s, a, r, s2, d replay.sample(BATCH_SIZE) q_pred q_net(s).gather(1, a.unsqueeze(1)).squeeze() with torch.no_grad(): q_next target_net(s2).max(dim1)[0] y r GAMMA * q_next * (1 - d) loss nn.functional.smooth_l1_loss(q_pred, y) optimizer.zero_grad() loss.backward() nn.utils.clip_grad_norm_(q_net.parameters(), 1.0) optimizer.step() global_step 1 if global_step % UPDATE_TARGET 0: target_net.load_state_dict(q_net.state_dict()) state next_state EPSILON max(EPS_MIN, EPSILON * EPS_DECAY)這段代碼里的smooth_l1_loss是個值得注意的選型它結(jié)合了 L1 和 L2 損失的特點Q 值目標(biāo)誤差大時梯度不會爆炸誤差小時收斂平穩(wěn)。如果不做梯度裁剪即使用了 Huber 損失早期訓(xùn)練的異常獎勵也可能讓權(quán)重一步跳壞。clip_grad_norm_的 1.0 取值是我從實際訓(xùn)練里調(diào)出來的日線級別獎勵通常很小裁剪閾值設(shè)太大等于沒設(shè)設(shè)太小會讓學(xué)習(xí)變慢。3.3 ε-greedy 與衰減探索策略不是拍腦袋是調(diào)參對象ε-greedy 控制模型在“按當(dāng)前策略行動”和“隨機(jī)探索”之間的切換。初始EPSILON1.0表示策略完全隨機(jī)隨著訓(xùn)練推進(jìn)降低到0.05最終 95% 的動作來自 Q 網(wǎng)絡(luò)、5% 的隨機(jī)動作兜底。這個 5% 的探索要保留因為在交易場景里市場狀態(tài)會變化完全放棄探索意味著模型在沒見過的行情下沒有逃生通道。衰減率EPS_DECAY0.998在 200 個 episode 的訓(xùn)練里大約 400 步后 ε 會降到 0.05 下限附近。對日線交易一個 episode 大約就是兩三個月的交易數(shù)據(jù)200 個 episode 覆蓋的數(shù)據(jù)量足夠長。你要注意一個反向誤區(qū)ε 降得太快模型過早固化到局部最優(yōu)降得太慢訓(xùn)練前期大量時間在隨機(jī)亂走樣本利用率低。我一般先在訓(xùn)練日志里同時打印“平均獎勵”和“ε 當(dāng)前值”如果發(fā)現(xiàn)平均獎勵在 ε 還很高的時候就開始上升說明當(dāng)前策略已經(jīng)有效可以略微提高衰減率。4. 從訓(xùn)練到回測數(shù)據(jù)切分、驗證流程和一組可用參數(shù)4.1 數(shù)據(jù)切分與信號執(zhí)行時序時間序列不能隨機(jī)打亂強(qiáng)化學(xué)習(xí)在交易里的數(shù)據(jù)切分比監(jiān)督學(xué)習(xí)嚴(yán)格得多。常見錯誤是直接用train_test_split(random_state42)這會把未來數(shù)據(jù)混進(jìn)訓(xùn)練集回測結(jié)果完全失真。時間序列必須按時間順序切前 70% 做訓(xùn)練集中間 15% 做驗證集用來選超參數(shù)最后 15% 做測試集只允許跑一次。信號執(zhí)行時序同樣有硬性要求策略在 t 時刻用收盤價算好特征、生成動作但動作要在 t1 時刻的 bar 上才能生效。環(huán)境代碼里step執(zhí)行動作前先按當(dāng)前 price 結(jié)算上一段持倉已經(jīng)隱式實現(xiàn)了這個時序但你要檢查自己從外部接數(shù)據(jù)時有沒有破壞這個順序。def evaluate(env, q_net, eps0.0): 評估模式關(guān)閉探索輸出累計收益和每次動作用于畫凈值曲線 state env.reset() done False total_return 0.0 actions_log [] while not done: with torch.no_grad(): state_t torch.from_numpy(state).unsqueeze(0) action q_net(state_t).argmax().item() if random.random() eps else random.randint(0, 2) next_state, reward, done, _ env.step(action) total_return reward actions_log.append(action) state next_state return total_return, actions_log這套時序規(guī)則帶來的一個直接效果是回測凈值曲線的最后一天會有“幸存者偏差”的假象因為策略在最后一天只結(jié)算不交易。真實做法是剔除最后一天的浮盈或者把評估期多留一根 bar 做緩沖。驗證集選參、測試集定論這是交易策略設(shè)計里最低限度的紀(jì)律。4.2 訓(xùn)練主循環(huán)與損失函數(shù)什么時候看哪個日志訓(xùn)練循環(huán)的日志設(shè)計是調(diào)試效率的分水嶺。我見過有人只在命令行打印 lossloss 從 0.5 慢慢降到 0.1 就覺得贏了結(jié)果去回測發(fā)現(xiàn)模型從頭到尾沒買過一手。訓(xùn)練日志至少要有四列全局步數(shù)、loss、累計 episode 獎勵、動作分布占比。動作分布是最重要的信號。如果一段時間內(nèi)動作 2買入的占比趨近于 0說明模型學(xué)成“永久空倉”策略——它在用“不虧錢”作為最優(yōu)解止損比止盈更狠。如果動作 0 占比接近 0模型又變成“無腦滿倉”。兩種情況都要停下來查獎勵函數(shù)和成本設(shè)置。訓(xùn)練中 loss 的正常形態(tài)不是單調(diào)下降而是先快速下降再在一個平臺附近波動。交易數(shù)據(jù)的非平穩(wěn)性決定了 Q 值不可能收斂到固定點loss 在一個范圍內(nèi)震蕩是正常的別看到小幅回升就慌。真正的異常是 loss 突然跳高一個量級那通常是因為某一步獎勵出現(xiàn)了極端值比如除權(quán)未復(fù)權(quán)造成的價格跳空。這種時候去查數(shù)據(jù)源不要改學(xué)習(xí)率硬壓。4.3 評估指標(biāo)和關(guān)鍵參數(shù)一組能直接當(dāng)起點的默認(rèn)配置回測評估不能只看累計收益。最大回撤、換手率、交易次數(shù)、夏普比率這四項必須一起看。我見過一個模型年化收益 80%但最大回撤 45%中間任意一次腰斬級別的回撤都足以讓實盤者心理崩潰、手動干預(yù)亂掉整個策略。下面這個參數(shù)表是我在滬深 300 日線上跑過的最小可用配置直接用它當(dāng)起點再按數(shù)據(jù)微調(diào)參數(shù)取值說明window30狀態(tài)窗口長度日線短周期夠用hidden128隱藏層神經(jīng)元數(shù)256 以上容易過擬合learning_rate1e-4日線場景推薦 1e-4不要用默認(rèn)的 1e-3batch_size64常用 32128小批量噪聲更大replay_capacity10_000控制舊經(jīng)驗占比日線 5k50k 區(qū)間調(diào)update_target200每 200 步同步目標(biāo)網(wǎng)絡(luò)參數(shù)gamma0.99趨勢策略首選高頻調(diào)低epsilon_decay0.998200 episode 內(nèi)在 1.0 → 0.05 之間完成衰減cost0.001交易成本按單邊千一A 股可提到 0.0016這組參數(shù)的調(diào)參邏輯是先把學(xué)習(xí)率壓住訓(xùn)練曲線穩(wěn)定之后再微調(diào)窗口和成本?;販y結(jié)果的可視化同樣重要如果你習(xí)慣把行情腳本以本地方式跑起來自建圖表直接把評估日志里的資金曲線和基準(zhǔn)指數(shù)曲線畫在一起能快速看出策略的超額收益集中在哪段行情、在震蕩期是否失效。把每一筆交易的持倉區(qū)間標(biāo)在圖上比盯著盈利數(shù)字更容易定位問題。5. 深度Q學(xué)習(xí)網(wǎng)絡(luò)做股票交易的常見問題和必看避坑點5.1 現(xiàn)象訓(xùn)練集收益曲線漂亮測試集直接虧到懷疑人生原因排除法按先后順序查第一查特征標(biāo)準(zhǔn)化是否用了全樣本統(tǒng)計量第二查是否用了未來數(shù)據(jù)特別是shift(-1)這種目標(biāo)泄漏第三查數(shù)據(jù)集里有沒有未復(fù)權(quán)的除權(quán)跳空。解決標(biāo)準(zhǔn)化器只在訓(xùn)練集fit然后transform驗證集和測試集把特征工程里所有滾動指標(biāo)統(tǒng)一shift(1)用后復(fù)權(quán)價格重新生成數(shù)據(jù)集。測試集只能跑一次跑完發(fā)現(xiàn)不行就回去改特征改完再回測試集等于把測試集當(dāng)驗證集用這個習(xí)慣會讓你對自己的策略產(chǎn)生系統(tǒng)性誤判。上一輪剛改完獎勵函數(shù)就立刻去跑測試集得到負(fù)收益后調(diào)整參數(shù)再跑一次這次結(jié)果變正了但我心里清楚這個“正收益”已經(jīng)不能說服任何人包括我自己。正確順序是訓(xùn)練集調(diào)整 → 驗證集選參 → 測試集只做一次最終確認(rèn)。5.2 現(xiàn)象loss 在某個平臺反復(fù)震蕩但不下降訓(xùn)練幾十輪沒有進(jìn)展原因通常是獎勵尺度與網(wǎng)絡(luò)初始化不匹配。對數(shù)單步收益的量級在 0.001 到 0.01 之間而 Q 網(wǎng)絡(luò)輸出的初始 Q 值通常在 0 附近、范圍在正負(fù)幾之間。兩個尺度差距過大TD 目標(biāo)里的r會被gamma * max(Q(s2))蓋住模型學(xué)不到任何來自獎勵的信號。解決把獎勵整體乘以一個放大系數(shù)比如reward * 20讓獎勵信號和目標(biāo) Q 值在同一個數(shù)量級。另一個常見做法是把初始輸出層權(quán)重調(diào)小讓網(wǎng)絡(luò)的初始 Q 值分布在 0 附近一個小范圍內(nèi)而不是標(biāo)準(zhǔn)的全連接初始化。我自己的習(xí)慣是先統(tǒng)計訓(xùn)練集里單步收益的均值與標(biāo)準(zhǔn)差把獎勵縮放到標(biāo)準(zhǔn)差為 1 左右再進(jìn)入訓(xùn)練循環(huán)。5.3 現(xiàn)象模型不交易或者一直滿倉不動先說“一直空倉”模型發(fā)現(xiàn)什么都不干的累計收益是 0而瞎交易的期望收益是負(fù)的于是學(xué)到空倉是最優(yōu)策略。這說明獎勵函數(shù)里缺少對機(jī)會成本的約束或者交易成本設(shè)得過高。解決檢查動作分布如果空倉動作占比超過 95%把cost調(diào)低一個數(shù)量級測試同時給持倉增加一個“行情上漲時未持倉的隱性懲罰”比如if position 0 and next_return 0: reward - 0.001。再說“一直滿倉”模型發(fā)現(xiàn)持有股票的長期收益期望為正于是決定永遠(yuǎn)不賣。這個策略在單邊上漲的訓(xùn)練集里是理性的但測試集一旦進(jìn)入熊市就全線崩潰。解決訓(xùn)練數(shù)據(jù)里必須包含熊市和震蕩市不要只挑一段牛市訓(xùn)練或者把獎勵從“單步收益”改成“收益減去最大回撤懲罰”讓模型被迫學(xué)會出逃。5.4 現(xiàn)象除權(quán)除息讓凈值曲線出現(xiàn)詭異的跳空模型在跳空位置瘋狂交易原因數(shù)據(jù)源沒有做復(fù)權(quán)處理。A 股除權(quán)除息會讓價格在除權(quán)日斷崖下跌看起來像暴跌模型會把它當(dāng)成做空信號實際上這只是權(quán)益分派導(dǎo)致的價格調(diào)整。解決全部使用后復(fù)權(quán)價格做訓(xùn)練與回測。后復(fù)權(quán)保證價格序列在歷史任意一天的相對比例是真實的實盤時再把信號映射回當(dāng)前價格即可。數(shù)據(jù)下載時如果只拿到原始價必須檢查每個交易日的漲跌幅是否超過 10%超過的多半是除權(quán)除息停牌復(fù)牌也要單獨(dú)標(biāo)記。5.5 現(xiàn)象同參數(shù)、同數(shù)據(jù)兩次訓(xùn)練結(jié)果完全不一樣回測收益差 10 個百分點原因沒有固定隨機(jī)種子。PyTorch 的模型初始化、PyTorch 的 drop 操作、Python 的 random 模塊、NumPy 的隨機(jī)數(shù)生成器四個隨機(jī)源都要在同一處固定。只固定torch.manual_seed是不夠的因為經(jīng)驗回放采樣用的是random.sample。import torch import numpy as np import random def seed_all(seed): torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) np.random.seed(seed) random.seed(seed) torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark False固定隨機(jī)種子不只是為了復(fù)現(xiàn)報告里的數(shù)字它更重要的作用是讓你在調(diào)參時能對照實驗改了一個參數(shù)收益變了到底是參數(shù)的效果還是隨機(jī)噪聲固定種子情況下收益變化可歸因于參數(shù)調(diào)整否則整個調(diào)參過程都是無效操作。多隨機(jī)種子評估應(yīng)該放到最后驗證階段而不是每次調(diào)試都換不同種子。6. 用多模型集成穩(wěn)住策略從單次訓(xùn)練到可信效果的進(jìn)階驗證6.1 多隨機(jī)種子集成用源碼基礎(chǔ)結(jié)構(gòu)把單模型變成策略組合單個 DQN 模型在日線交易上的穩(wěn)定性通常不夠隨機(jī)種子一變、結(jié)果就飄這其實是正?,F(xiàn)象。我的做法是從“訓(xùn)一個模型”升級成“訓(xùn) N 個模型再投票”。用 5 個不同的隨機(jī)種子訓(xùn)練 5 個 Q 網(wǎng)絡(luò)每個網(wǎng)絡(luò)獨(dú)立生成動作信號最終動作取多數(shù)投票。當(dāng)三個以上模型同時選擇買入時信號的一致性明顯提高假突破帶來的噪聲被中和掉。6.2 多空市場切片驗證不追求永遠(yuǎn)賺錢最后一個習(xí)慣是按行情狀態(tài)切片復(fù)盤把測試期按年或者按市場狀態(tài)分成若干段分別計算每段的收益、回撤和交易次數(shù)。如果模型的超額收益全部來自牛市段熊市段全靠躲跌那策略本質(zhì)上是一個擇時器如果收益分散在各類行情里說明模型學(xué)到的是更泛化的狀態(tài)-動作關(guān)系。這兩類策略的交易邏輯和實盤預(yù)期完全不同提前知道比事后吃大虧好。我現(xiàn)在再看一份交易策略源碼時第一眼不看它的收益曲線先看它的訓(xùn)練集與測試集有沒有嚴(yán)格隔離、隨機(jī)種子有沒有固定、交易成本有沒有扣干凈這三個基礎(chǔ)點在課程設(shè)計源碼和開源的免費(fèi) Python 源碼里恰恰最容易被跳過。用這套 DQN 源碼方案做交易策略真正的門檻不是算法本身而是把所有“看起來小”的細(xì)節(jié)釘死。希望幫到你。本文還有配套的精品資源點擊獲取