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