器學(xué)習(xí)量化投資策略源碼拆解:從特征工程到LightGBM回測(cè))
簡(jiǎn)介面向量化投資與機(jī)器學(xué)習(xí)實(shí)踐者這是一個(gè)可運(yùn)行的項(xiàng)目源碼演示了從行情數(shù)據(jù)獲取到策略回測(cè)的完整流程。項(xiàng)目圍繞梯度提升樹(shù)模型展開(kāi)涵蓋數(shù)據(jù)抓取、特征工程、模型訓(xùn)練與歷史回測(cè)四個(gè)核心環(huán)節(jié)可輸出每日建議買入的股票及對(duì)應(yīng)收益并計(jì)算累計(jì)收益、最大回撤、夏普率等常用量化指標(biāo)同時(shí)提供一鍵執(zhí)行入口、依賴安裝清單和說(shuō)明文檔適合希望快速搭建策略原型、理解量化回測(cè)流程的開(kāi)發(fā)者參考。資源包共十五個(gè)文件主體是五個(gè)腳本另有六張效果圖、兩個(gè)文本清單、說(shuō)明文檔和配置文件壓縮包僅七百三十五KB結(jié)構(gòu)簡(jiǎn)潔、依賴清晰便于直接運(yùn)行與二次修改。目前已有兩百六十七人學(xué)習(xí)配套依賴列表和使用說(shuō)明能幫助新手迅速上手。1. 用 Python 機(jī)器學(xué)習(xí)做量化投資策略這份源碼到底能讓你少走多少?gòu)澛肥掷镉幸惶?Python 機(jī)器學(xué)習(xí)量化投資策略的完整源碼從數(shù)據(jù)抓取、特征生成、lightGBM 訓(xùn)練到回測(cè)和評(píng)價(jià)指標(biāo)四步流程加一個(gè)一鍵執(zhí)行的 main.py 全給串起來(lái)了。拆完這個(gè)項(xiàng)目你收獲的不只是一段能跑的代碼而是“特征文件怎么存、模型怎么留檔、回測(cè)記錄怎么讀”這一整條數(shù)據(jù)鏈路。適合想第一次把機(jī)器學(xué)習(xí)量?jī)r(jià)策略跑通的新手也適合已經(jīng)手動(dòng)寫過(guò)單標(biāo)的預(yù)測(cè)、想看看完整選股框架的熟手——文末我會(huì)把真正影響回測(cè)可信度的幾個(gè)坑單獨(dú)列出來(lái)。先別急著調(diào)參數(shù)跟著數(shù)據(jù)流走一遍你就知道這套源碼每一條命令是在干什么。2. 數(shù)據(jù)準(zhǔn)備把歷史行情以統(tǒng)一格式落盤2.1 tusare 數(shù)據(jù)源怎么接字段、目錄與復(fù)權(quán)運(yùn)行 data.py 之前要確認(rèn)一件事file/stock_list.txt 里的每一支股票都能在 file/data/ 下對(duì)應(yīng)一個(gè)干凈的 CSV。源碼里寫的是 tusare 調(diào)用我按 tushare 這一類免費(fèi)行情接口來(lái)理解不同版本接口名有差異但落盤結(jié)構(gòu)一致每支股票一個(gè)文件字段至少包含 open、high、low、close、vol 這類量?jī)r(jià)信息。main.py 的作用只是按順序調(diào)用后面四個(gè)腳本正式排錯(cuò)時(shí)建議還是手動(dòng)分步跑每一步的產(chǎn)物都能單獨(dú)檢查。我一般會(huì)在循環(huán)里加一個(gè)簡(jiǎn)單重試避免免費(fèi)接口有頻率限制時(shí)中斷整批數(shù)據(jù)。下面是 data.py 里常見(jiàn)寫法# data.py 的核心結(jié)構(gòu)以我本地版本為例tusare 具體接口按你安裝的版本替換 import time from pathlib import Path stock_list Path(file/stock_list.txt).read_text().strip().splitlines() data_dir Path(file/data) data_dir.mkdir(parentsTrue, exist_okTrue) for code in stock_list: # fetch_daily_price 是占位函數(shù)替換成 tusare 實(shí)際拉行情返回 DataFrame 即可 df fetch_daily_price(code, start2018-01-01, end2023-12-31) if df is None or df.empty: continue df[code] code df.to_csv(data_dir / f{code}.csv, indexFalse) time.sleep(0.5) # 控制請(qǐng)求頻率免費(fèi)接口容易被限流這段邏輯里有兩個(gè)參數(shù)要留意時(shí)間范圍 start/end 決定樣本窗口窗口越長(zhǎng)特征越豐富但也要小心早期交易制度和漲跌停規(guī)則跟現(xiàn)在不同sleep 時(shí)間不是玄學(xué)是給免費(fèi)接口留緩沖本地演示 0.5 秒足夠。數(shù)據(jù)拉到本地后做一次常規(guī)體檢文件是否有內(nèi)容、日期列是否被統(tǒng)一成同一格式、是否需要前復(fù)權(quán)。復(fù)權(quán)這步最容易忽略遇到除權(quán)除息日原始價(jià)格會(huì)出現(xiàn)人為跳空模型會(huì)把這種跳空當(dāng)成真實(shí)漲跌信號(hào)回測(cè)結(jié)果立刻失真。常見(jiàn)做法是拉前復(fù)權(quán)數(shù)據(jù)保證歷史序列連續(xù)。2.2 股票清單與數(shù)據(jù)目錄demo 十支股票夠不夠file/stock_list.txt 決定你的研究范圍。demo 只放了 10 支股票是為了讓整套流程在幾分鐘內(nèi)跑完不是為了證明選股效果。實(shí)戰(zhàn)里股票池盡量放幾十支以上跨行業(yè)、跨風(fēng)格模型才能學(xué)到“為什么選 A 而不是選 B”而不是在 10 支里矮子拔高個(gè)。數(shù)據(jù)目錄結(jié)構(gòu)如下表路徑內(nèi)容說(shuō)明file/stock_list.txt股票代碼清單每行一個(gè)代碼數(shù)量決定股票池大小file/data/{code}.csv單支股票歷史行情data.py 輸出的中間產(chǎn)物file/feature/特征文件目錄feature.py 生成的 pickle 二進(jìn)制文件file/model.lgb.txt訓(xùn)練好的模型文本model.py 保存的輕量級(jí)模型文件file/record.csv回測(cè)交易記錄backtest.py 輸出逐日持倉(cāng)與收益這里有個(gè)我踩過(guò)的小地方拉完數(shù)據(jù)要順手檢查空文件。停牌時(shí)間長(zhǎng)的股票CSV 可能只有幾十行后面算 rolling 窗口時(shí)全被 dropna 丟掉等于白跑一遍??梢栽诿钚锌焖賿咭槐? 檢查是否存在空文件以及每個(gè)文件的大致行數(shù) find file/data -name *.csv -empty wc -l file/data/*.csv | sort -n | head第一行如果什么都沒(méi)輸出說(shuō)明沒(méi)有空文件第二行按行數(shù)排序行數(shù)異常少的代碼要單獨(dú)決定是保留還是剔除。我的習(xí)慣是給 stock_list 留一個(gè)黑名單概念回測(cè)時(shí)把上市不足一年、長(zhǎng)期停牌的股票過(guò)濾掉避免它們白白消耗特征窗口。數(shù)據(jù)落地這一步不需要高深技巧但目錄和字段約定一旦混亂后面每個(gè)環(huán)節(jié)都得回頭查代價(jià)最大。3. 特征生成把量?jī)r(jià)數(shù)據(jù)變成模型能學(xué)的輸入3.1 為什么不能把收盤價(jià)直接丟給模型機(jī)器學(xué)習(xí)模型默認(rèn)認(rèn)為輸入特征對(duì)標(biāo)簽有穩(wěn)定解釋力而原始價(jià)格序列不滿足這個(gè)前提不同股票股價(jià)區(qū)間差異大白酒和銀行不能直接比絕對(duì)價(jià)格價(jià)格本身是非平穩(wěn)序列直接訓(xùn)練容易讓模型學(xué)到“價(jià)格上漲所以未來(lái)上漲”這種缺乏依據(jù)的結(jié)論。所以 feature.py 存在的意義是把 file/data 下的基礎(chǔ)行情轉(zhuǎn)換成一組相對(duì)量漲跌幅、均線乖離、波動(dòng)率、量比、動(dòng)量。這些特征剔除了絕對(duì)價(jià)格的影響保留的是“當(dāng)前價(jià)格相對(duì)于近期均線高了多少、量能是否放大”這類橫截面對(duì)比信息。標(biāo)簽設(shè)計(jì)也是特征工程的一部分。我常用的做法是給每支股票計(jì)算未來(lái) N 日收益N 取 5 或 10然后轉(zhuǎn)成二分類標(biāo)簽未來(lái) 5 日收益為正記 1否則記 0。這個(gè)窗口不是拍腦袋它對(duì)應(yīng)的是“持有 5 個(gè)交易日后賣出”的調(diào)倉(cāng)周期。注意特征只能使用 T 日及之前的信息標(biāo)簽用的是 T 日之后的信息兩者之間一旦交叉后面回測(cè)一定翻車這部分我在第 5 章展開(kāi)。3.2 feature.py 落地滾動(dòng)窗口、分組計(jì)算與 pickle特征計(jì)算的細(xì)節(jié)在于“按股票分組”而不是把所有股票拼成一個(gè)表滾動(dòng)否則前一股票的最后幾行會(huì)跟后一股票開(kāi)頭幾行混在一起計(jì)算算出來(lái)的全是無(wú)效特征。以下是常見(jiàn)實(shí)現(xiàn)# feature.py 的核心邏輯讀取 CSV分組構(gòu)造特征落盤 pickle import pandas as pd from pathlib import Path data_dir Path(file/data) feature_dir Path(file/feature) feature_dir.mkdir(parentsTrue, exist_okTrue) frames [] for csv_file in sorted(data_dir.glob(*.csv)): df pd.read_csv(csv_file) df[trade_date] pd.to_datetime(df[trade_date]) df df.sort_values(trade_date).reset_index(dropTrue) # 漲跌幅是模型最基礎(chǔ)的輸入之一 df[ret] df[close].pct_change() # 均線乖離當(dāng)前價(jià)格離 20 日均線的偏離程度 df[ma20] df[close].rolling(20).mean() df[bias20] df[close] / df[ma20] - 1 # 量比當(dāng)日成交量相對(duì) 20 日均量的倍數(shù) df[vol_ratio] df[vol] / df[vol].rolling(20).mean() frames.append(df) all_df pd.concat(frames, ignore_indexTrue) all_df all_df.dropna(subset[ret, bias20, vol_ratio]) all_df.to_pickle(feature_dir / feature.pkl)這段代碼有三個(gè)地方值得拆開(kāi)說(shuō)。第一sort_values(trade_date)是硬性前置條件很多 CSV 從接口拿回來(lái)并不是嚴(yán)格時(shí)間升序不排序的話 rolling 窗口的方向就是亂的。第二rolling(20)這類窗口參數(shù)要跟調(diào)倉(cāng)周期對(duì)齊5 日調(diào)倉(cāng)用 5 日均線、20 日調(diào)倉(cāng)用 20 日均線都說(shuō)得通但固定用 20 卻按 5 日頻率調(diào)倉(cāng)特征和標(biāo)簽的周期就錯(cuò)位了常見(jiàn)做法是同時(shí)生成短周期和長(zhǎng)周期兩組特征。第三dropna會(huì)把每個(gè)股票前 20 個(gè)交易日的數(shù)據(jù)丟掉這是滾動(dòng)窗口的正常代價(jià)數(shù)據(jù)量足夠大時(shí)不用心疼。存儲(chǔ)格式我特別要說(shuō)一下all_df用to_pickle而不是to_csv。pickle 是二進(jìn)制格式讀取速度比 CSV 快一個(gè)量級(jí)而且能保留 DataFrame 里列的數(shù)據(jù)類型——日期還是 datetime、數(shù)值還是 float存進(jìn)去什么樣讀出來(lái)就是什么樣。CSV 一旦日期列被讀成字符串后面訓(xùn)練前又要做一輪轉(zhuǎn)換這個(gè)轉(zhuǎn)換往往成為 bug 源頭。文件放在 file/feature 下后續(xù) model.py 直接用read_pickle讀回省去重復(fù)加工。4. 訓(xùn)練 lightGBM 分類器參數(shù)怎么設(shè)模型文件怎么留4.1 選型理由表格特征場(chǎng)景樹(shù)模型比深度網(wǎng)絡(luò)更順手量化選股的特征基本上都是表格數(shù)據(jù)行數(shù)是交易日列是量?jī)r(jià)派生特征。這類場(chǎng)景里 lightGBM 是優(yōu)先級(jí)靠前的選擇訓(xùn)練速度快幾萬(wàn)行數(shù)據(jù)在 CPU 上幾十秒能出結(jié)果自帶對(duì)缺失值的處理不需要提前做復(fù)雜的填充支持特征重要性輸出方便后續(xù)刪減無(wú)效特征。對(duì)比深度學(xué)習(xí)樹(shù)模型還有個(gè)實(shí)際優(yōu)勢(shì)是調(diào)參維度少即使參數(shù)不全優(yōu)默認(rèn)值也不會(huì)差到完全不能用。這套源碼把模型訓(xùn)練放在 model.py最終模型保存為 file/model.lgb.txt。保存成 txt 而不是二進(jìn)制.model對(duì)復(fù)盤很友好模型文件是文本格式可以直接打開(kāi)看樹(shù)結(jié)構(gòu)、葉子節(jié)點(diǎn)權(quán)重也能方便地 diff 兩個(gè)版本的差異。生產(chǎn)環(huán)境為了加載速度可能用二進(jìn)制但這個(gè) demo 保留 txt 本身就是教學(xué)意圖。4.2 訓(xùn)練流程標(biāo)簽、數(shù)據(jù)切分、參數(shù)與早停訓(xùn)練環(huán)節(jié)的代碼我拆成標(biāo)簽生成、切分、訓(xùn)練三塊講因?yàn)檫@三個(gè)環(huán)節(jié)的錯(cuò)誤比參數(shù)沒(méi)調(diào)好更容易導(dǎo)致回測(cè)失真。# model.py 的核心邏輯讀特征、造標(biāo)簽、按時(shí)間切分、訓(xùn)練保存 import lightgbm as lgb import pandas as pd feature_df pd.read_pickle(file/feature/feature.pkl) feature_df[trade_date] pd.to_datetime(feature_df[trade_date]) # 標(biāo)簽未來(lái) 5 日收益為正則記為 1 feature_df[future_ret] feature_df.groupby(code)[close].shift(-5) / feature_df[close] - 1 feature_df[label] (feature_df[future_ret] 0).astype(int) feature_cols [ret, bias20, vol_ratio] # 實(shí)際請(qǐng)補(bǔ)足你在特征階段生成的全部列 feature_df feature_df.dropna(subset[label]) # 按時(shí)間切分杜絕用未來(lái)數(shù)據(jù)訓(xùn)練 last_date feature_df[trade_date].max() cutoff last_date - pd.Timedelta(days120) train_df feature_df[feature_df[trade_date] cutoff] valid_df feature_df[feature_df[trade_date] cutoff] params { objective: binary, metric: auc, learning_rate: 0.05, num_leaves: 31, feature_fraction: 0.8, } d_train lgb.Dataset(train_df[feature_cols], train_df[label]) d_valid lgb.Dataset(valid_df[feature_cols], valid_df[label]) model lgb.train(params, d_train, num_boost_round500, valid_sets[d_valid], early_stopping_rounds50) model.save_model(file/model.lgb.txt)標(biāo)簽生成用groupby(code)再shift(-5)是為了只拿同一支股票的“未來(lái) 5 個(gè)交易日”。shift 在原始行情上是按行移動(dòng)如果不分組上一支股票的最后 5 行會(huì)和下一支股票的前 5 行拼出一個(gè)假未來(lái)。切分這里我特意留了最后 120 天做驗(yàn)證集而不是隨機(jī)打亂——時(shí)間序列隨機(jī)切分是建模里最典型的錯(cuò)誤模型可能在訓(xùn)練時(shí)已經(jīng)見(jiàn)過(guò)驗(yàn)證集的鄰居。一個(gè)默認(rèn)可跑的參數(shù)組合如下參數(shù)示例值作用objectivebinary二分類標(biāo)簽為未來(lái)收益正負(fù)metricauc評(píng)價(jià)排序能力比準(zhǔn)確率更適合不平衡樣本learning_rate0.05學(xué)習(xí)率越小越穩(wěn)但需要更多輪數(shù)num_leaves31控制樹(shù)復(fù)雜度過(guò)大容易過(guò)擬合feature_fraction0.8每棵樹(shù)隨機(jī)抽 80% 特征增加多樣性num_boost_round500最大迭代輪數(shù)配合早停使用early_stopping_rounds50驗(yàn)證集指標(biāo)連續(xù) 50 輪不提升則停止這些參數(shù)不是最優(yōu)解但足夠完成第一版閉環(huán)。剛開(kāi)始調(diào)參時(shí)不要一上來(lái)搜索幾百組先固定這批參數(shù)跑通再單獨(dú)動(dòng) learning_rate 和 num_leaves。如果訓(xùn)練集 AUC 和驗(yàn)證集 AUC 差距很大優(yōu)先減少 num_leaves 或調(diào)小 feature_fraction而不是加更多輪數(shù)。訓(xùn)練完記得看兩個(gè)東西驗(yàn)證集 AUC 有沒(méi)有明顯高于 0.5以及模型輸出的特征重要性。如果某一個(gè)特征的重要性占比異常高比如只有 bias20 一家獨(dú)大要懷疑這個(gè)特征的構(gòu)造里是否包含了未來(lái)信息或者它只是在股票池失效時(shí)的幸存者。我一般會(huì)在 model.py 后加幾行打印特征重要性做成習(xí)慣而不只盯著最終 AUC 數(shù)字。5. 回測(cè)與常見(jiàn)問(wèn)題排查三指標(biāo)怎么算坑在哪5.1 回測(cè)記錄與三指標(biāo)計(jì)算邏輯回測(cè)是裁決模型能不能用的環(huán)節(jié)。backtest.py 的職責(zé)是模擬每天的組合動(dòng)作用訓(xùn)練好的模型對(duì)全市場(chǎng)股票打分按分?jǐn)?shù)買入得分最高的股票然后記錄當(dāng)天的實(shí)際收益逐日累計(jì)寫入 file/record.csv。每行代表一天的結(jié)果包含哪個(gè) code 被買入、當(dāng)天收益率、可能還有前一日持有的標(biāo)記。這樣的設(shè)計(jì)本質(zhì)上和 backtrader 這類多股回測(cè)框架做的事一樣只是用最小代碼量把底層數(shù)據(jù)流顯式暴露出來(lái)。三指標(biāo)的計(jì)算邏輯核心代碼如下# backtest.py 中指標(biāo)計(jì)算的偽代碼完整邏輯以源碼為準(zhǔn) import pandas as pd record pd.read_csv(file/record.csv) record[trade_date] pd.to_datetime(record[trade_date]) record record.sort_values(trade_date) # 凈值從 1 開(kāi)始逐日累乘 record[nav] (1 record[daily_return]).cumprod() record[drawdown] record[nav] / record[nav].cummax() - 1 total_return record[nav].iloc[-1] - 1 max_drawdown record[drawdown].min() sharpe record[daily_return].mean() / record[daily_return].std() * (252 ** 0.5) print(f累積收益: {total_return:.2%}) print(f最大回撤: {max_drawdown:.2%}) print(f夏普率: {sharpe:.2f})邏輯順序我建議記牢先排序再算凈值最后回撤。daily_return 是每一天策略組合的實(shí)際收益已經(jīng)是十進(jìn)制小數(shù)0.01 表示 1%所以 nav 累乘用 1 rate 的寫法。最大回撤這里的 drawdown 是負(fù)數(shù)取 min 得到最深的坑如果畫(huà)凈值曲線回撤就是曲線從高點(diǎn)到后續(xù)低點(diǎn)的落差。夏普率用日收益率均值除以日收益率標(biāo)準(zhǔn)差再乘 252 的平方根252 是 A 股一年大約的交易天數(shù)這個(gè)指標(biāo)衡量的是“每承擔(dān)一份波動(dòng)換來(lái)多少收益”不是越高越好要跟最大回撤放一起看一個(gè)夏普 2 但回撤 40% 的策略和夏普 1.2 但回撤 15% 的策略實(shí)際持有人體驗(yàn)完全不同后者可能更容易拿住?;販y(cè)環(huán)節(jié)還有一層容易忽略的口徑調(diào)倉(cāng)頻率。demo 是每天打分、每天換倉(cāng)的模式換倉(cāng)意味著手續(xù)費(fèi)和滑點(diǎn)。backtest.py 的第一版通常不含成本這樣得到的收益是理想摩擦為零的收益??椿販y(cè)時(shí)先別急著興奮我會(huì)把累積收益和換手次數(shù)連起來(lái)算一筆賬這個(gè)放到最后一張做。5.2 常見(jiàn)問(wèn)題排查現(xiàn)象、原因與處理以下五條全部來(lái)自我自己跑這類源碼的血淚經(jīng)驗(yàn)按出現(xiàn)頻率排序。1. 訓(xùn)練集 AUC 很高回測(cè)凈值卻一路陰跌?,F(xiàn)象訓(xùn)練和驗(yàn)證階段指標(biāo)都過(guò)得去但 record.csv 里的實(shí)際收益慘不忍睹。原因最常見(jiàn)的是特征泄漏比如用未來(lái) N 日數(shù)據(jù)構(gòu)造了某個(gè)特征或者切分時(shí)沒(méi)有按時(shí)間切。解決回到第 3、4 章的邏輯檢查每一個(gè)特征是否只用了 T 日及之前的數(shù)據(jù)并確認(rèn)訓(xùn)練集和回測(cè)區(qū)間在時(shí)間上嚴(yán)格不重疊。2. record.csv 里出現(xiàn)大段 NaN 或只有少數(shù)幾天有值。現(xiàn)象回測(cè)跑完記錄文件里很多行是空值凈值曲線中斷。原因股票停牌、上市日期晚于回測(cè)起點(diǎn)或者數(shù)據(jù)文件本身不完整導(dǎo)致回測(cè)日期配對(duì)失敗。解決在 data 階段就剔除長(zhǎng)期停牌標(biāo)的回測(cè)循環(huán)里遇到 NaN 行情跳過(guò)或沿用最近收盤價(jià)不要讓 NaN 參與凈值累乘。3. 最大回撤算出來(lái)接近 0怎么看都不對(duì)?,F(xiàn)象三指標(biāo)里回撤數(shù)字幾乎為 0但凈值曲線明顯有大坑。原因drawdown 計(jì)算前沒(méi)有按 trade_date 排序或者 daily_return 列取錯(cuò)把原始股價(jià)當(dāng)成了收益率。解決先看代碼里 nav 是否從 1 開(kāi)始、是否累乘確認(rèn) daily_return 是當(dāng)日漲跌幅而不是收盤價(jià)數(shù)值排序后再算 cummax。4. 回測(cè)賺錢模擬盤卻差得遠(yuǎn)。現(xiàn)象回測(cè)年化收益可觀實(shí)盤或模擬盤表現(xiàn)大幅縮水。原因沒(méi)有計(jì)手續(xù)費(fèi)和滑點(diǎn)假設(shè)每天賣出再買入實(shí)際傭金和價(jià)格沖擊很快把收益吃掉。解決在收益上扣掉雙邊成本傭金加印花稅按萬(wàn)五到千一量級(jí)每次換倉(cāng)扣一次模型換手越頻繁成本越不可忽視。5. 十支股票的回測(cè)結(jié)果波動(dòng)極大每次跑都不一樣?,F(xiàn)象新增或替換一兩支股票累積收益就從正變負(fù)。原因股票池太少樣本代表性不足模型學(xué)到的是個(gè)別股票的噪聲規(guī)律。解決擴(kuò)大 stock_list并增加行業(yè)分散度如果訓(xùn)練成本可控還可以把買一支持倉(cāng)改成買得分最高的前五支等權(quán)組合單票波動(dòng)對(duì)凈值的影響立刻降低。這五條里第一條和第四條直接決定回測(cè)有沒(méi)有參考價(jià)值其余幾條決定你排錯(cuò)的方向。遇到詭異結(jié)果時(shí)我習(xí)慣從數(shù)據(jù)流動(dòng)的方向逐層排查文件有沒(méi)有、特征對(duì)不對(duì)、標(biāo)簽有沒(méi)有泄漏、回測(cè)有沒(méi)有算成本而不是一上來(lái)就改模型參數(shù)。6. 進(jìn)階給策略加成本與滾動(dòng)訓(xùn)練讓回測(cè)結(jié)果更可信6.1 滾動(dòng)訓(xùn)練與交易成本回測(cè)可信度的兩個(gè)基本盤把 demo 往實(shí)盤方向推進(jìn)我不會(huì)先加更復(fù)雜的模型而是先做兩件樸素但決定生死的事情把交易成本寫進(jìn)回測(cè)把一次性訓(xùn)練改成滾動(dòng)訓(xùn)練。交易成本落地很簡(jiǎn)單每次換倉(cāng)時(shí)從當(dāng)日收益中扣掉雙邊成本。比如傭金加滑點(diǎn)合計(jì) 0.001那么當(dāng)日凈收益就不是 daily_return而是 daily_return - 0.001前提是當(dāng)天發(fā)生了調(diào)倉(cāng)沒(méi)有調(diào)倉(cāng)的日子不該扣。這一步把理想收益變成可執(zhí)行收益效果往往立竿見(jiàn)影——模型換手越勤凈值縮水越明顯這時(shí)候你才會(huì)認(rèn)真考慮降低調(diào)倉(cāng)頻率或者給買入增加置信度閾值。滾動(dòng)訓(xùn)練解決的是模型時(shí)效性問(wèn)題。一次性用 2018 到 2023 的歷史訓(xùn)練、再回測(cè)同區(qū)間本質(zhì)上是“用同一段歷史既出題又答題”容易高估模型能力。常見(jiàn)做法是 walk-forward每 30 個(gè)交易日向前滾動(dòng)一次用前面一段時(shí)間訓(xùn)練預(yù)測(cè)接下來(lái) 30 天然后重復(fù)。# 滾動(dòng)訓(xùn)練示意按日期切成多個(gè)階段逐步前移 for i in range(total_days // 30): train_slice feature_df[feature_df[trade_date] date_windows[i]] test_slice feature_df[ (feature_df[trade_date] date_windows[i]) (feature_df[trade_date] date_windows[i] pd.Timedelta(days30)) ] model train_model(train_slice, params) preds[i] model.predict(test_slice[feature_cols])核心參數(shù)是兩個(gè)窗口回看窗口決定訓(xùn)練樣本量一般不低于 120 個(gè)交易日預(yù)測(cè)窗口跟調(diào)倉(cāng)周期對(duì)齊5 日調(diào)倉(cāng)就只用未來(lái) 5 天的樣本驗(yàn)證。改成滾動(dòng)訓(xùn)練后回測(cè)結(jié)論更接近真實(shí)操作?;叵胛易龅谝话娌呗缘臅r(shí)候滿腦子都是把夏普率調(diào)高結(jié)果換了 window 參數(shù)后回測(cè)曲線忽好忽壞后來(lái)才發(fā)現(xiàn)問(wèn)題根本不在參數(shù)而在數(shù)據(jù)切分和成本口徑。從那以后我每次拿到這類源碼第一步永遠(yuǎn)是跑通最小數(shù)據(jù)閉環(huán)第二步加成本看真實(shí)收益第三步才談?wù){(diào)參。希望這份拆解能幫你在量化這條路上少踩幾個(gè)我踩過(guò)的坑。本文還有配套的精品資源點(diǎn)擊獲取