戰(zhàn))
如何開通基于 AI 打造的 A 股量化策略上三層策略骨架與代碼落地“AI量化”這四個(gè)字最近幾乎快被說(shuō)爛了。但真有人問“那我到底該怎么從零開始搭一套能跑的AI量化策略”絕大多數(shù)人給出的答案要么是讓你去看機(jī)器學(xué)習(xí)課程要么是丟給你一個(gè)封裝好的黑盒框架跑完回測(cè)數(shù)據(jù)很好看真到實(shí)盤卻完全不是那么回事。這篇文章不講虛的我直接分享我拉通“AI A股量化”時(shí)踩出來(lái)的那條完整鏈路。今天先解決最核心的一件事先把三層策略骨架搭起來(lái)——信號(hào)層負(fù)責(zé)“買什么、賣什么”風(fēng)控層負(fù)責(zé)“買多少、多少止損”執(zhí)行層負(fù)責(zé)“成交清不清楚、滑點(diǎn)控制沒控制”最后附上能直接改的 Python 代碼。這篇主要面向兩類人一類是有一定 Python 基礎(chǔ)、想正經(jīng)入行量化但不知道第一行代碼寫哪的另一類是已經(jīng)手寫過(guò)簡(jiǎn)單均線策略、但發(fā)現(xiàn)“加了一個(gè)AI模型之后就不知道怎么和原來(lái)那套交易邏輯拼起來(lái)”的朋友??赐昴阒辽倌艽畛鲆粋€(gè)從“行情數(shù)據(jù)進(jìn)”到“委托單出”的完整策略雛形跑通一次模擬盤閉環(huán)。1. 三層策略骨架先想清楚系統(tǒng)怎么解耦1.1 為什么我們最先搭骨架而不是先找模型很多人一上來(lái)就抱著“我要用深度學(xué)習(xí)預(yù)測(cè)股價(jià)”的心態(tài)開始寫代碼結(jié)果通常都在數(shù)據(jù)清洗階段就放棄了。我自己的教訓(xùn)是A股量化、特別是偏中低頻的策略真正的挑戰(zhàn)從來(lái)不是“模型不夠聰明”而是“當(dāng)一個(gè)信號(hào)產(chǎn)生時(shí)整個(gè)鏈路能不能干凈、及時(shí)、可控地把它變成一筆交易”。最早我寫策略所有邏輯堆在一個(gè)文件里拉數(shù)據(jù)、算因子、出信號(hào)、算倉(cāng)位、模擬下單全揉在一起。初看挺爽改個(gè)參數(shù)重跑一遍也就幾分鐘。但問題很快暴露回測(cè)和模擬盤的邏輯混在一起模型一旦要換特征或者想對(duì)比兩套止損規(guī)則就得復(fù)制粘貼一大堆代碼改到后面自己都分不清哪段邏輯在生效。后來(lái)我把系統(tǒng)重新按三層拆開整個(gè)思路瞬間就清晰了信號(hào)層根據(jù)數(shù)據(jù)輸入輸出一個(gè)“看好/看淡/中性”的判斷以及對(duì)應(yīng)的置信度。風(fēng)控層拿到信號(hào)和當(dāng)前持倉(cāng)輸出“今天實(shí)際要交易多少倉(cāng)位、觸發(fā)什么條件必須撤”。執(zhí)行層根據(jù)風(fēng)控層的指令結(jié)合盤口和費(fèi)用約束生成實(shí)際委托單。三層之間要數(shù)據(jù)流向下傳遞不反向依賴。因?yàn)橹挥邪堰@三層徹底解耦你才能做到“換一個(gè)模型不換風(fēng)控、換一套風(fēng)控不動(dòng)信號(hào)、改一個(gè)執(zhí)行參數(shù)不影響策略邏輯”。1.2 信號(hào)層解決“買什么、賣什么”信號(hào)層是整個(gè)策略的“大腦”它的輸入是行情、基本面、輿情、乃至盤口數(shù)據(jù)輸出是方向判斷和強(qiáng)度打分。注意這里的輸出不直接等同于買入或賣出它更像一個(gè)“分析師”只表達(dá)觀點(diǎn)不負(fù)責(zé)動(dòng)手。為什么要把“觀點(diǎn)”和“決策”分開因?yàn)樵趯?shí)操中你會(huì)發(fā)現(xiàn)AI模型給出的原始輸出往往是很原始的數(shù)值比如某個(gè)分類概率、回歸預(yù)測(cè)的期望收益率。如果直接把這種數(shù)值拿去下單經(jīng)常會(huì)遇到“模型說(shuō)漲2%結(jié)果倉(cāng)位算出來(lái)占用了90%資金”的荒謬情況。所以信號(hào)層一般還要做一道“語(yǔ)義化”加工把模型的數(shù)值輸出映射成1看好、-1看淡、0觀望三檔同時(shí)附帶一個(gè)0到1的置信度供風(fēng)控層使用。我第一次搭信號(hào)層時(shí)犯過(guò)一個(gè)典型錯(cuò)誤直接把 XGBoost 的分類概率當(dāng)信號(hào)概率大于0.5就買入概率小于0.5就賣出。看上去很合理但實(shí)際回測(cè)下來(lái)?yè)Q手率極高手續(xù)費(fèi)把收益全吃光了。后來(lái)我在信號(hào)到交易的中間加了一個(gè)“死區(qū)”dead zone概率在0.55到0.6之間統(tǒng)統(tǒng)觀望才把換手壓下來(lái)。這就是信號(hào)層存在的意義——它天然承擔(dān)了“過(guò)濾噪音”的責(zé)任。1.3 風(fēng)控層解決“買多少、什么時(shí)候撤離”風(fēng)控層是很多散戶寫策略時(shí)最不重視、但對(duì)長(zhǎng)期收益影響最大的一層。它要回答三個(gè)問題第一給定信號(hào)的置信度當(dāng)前應(yīng)該下多少倉(cāng)位第二持倉(cāng)萬(wàn)一走反什么價(jià)位必須止損第三整個(gè)賬戶凈值回撤到什么程度策略必須降杠桿甚至停擺。這一層設(shè)計(jì)得好不好直接決定策略的“存活率”。我見過(guò)不少剛?cè)胄械呐笥研盘?hào)模型做得非?;ㄉ贚STM加注意力機(jī)制都用上了但倉(cāng)位永遠(yuǎn)是單票滿倉(cāng)止損從來(lái)不止結(jié)果一次極端行情就回到解放前。其實(shí)A股本身有漲跌停、T1、部分股票流動(dòng)性一般等約束風(fēng)控層如果不能主動(dòng)去適應(yīng)這些規(guī)則再聰明的信號(hào)也救不了賬戶。風(fēng)控層的一個(gè)核心思想是所有風(fēng)控規(guī)則必須獨(dú)立于當(dāng)前信號(hào)。你不能因?yàn)椤斑@個(gè)信號(hào)特別強(qiáng)”就臨時(shí)跳過(guò)止損線那是情緒化交易的根源。我在代碼里實(shí)現(xiàn)風(fēng)控層的習(xí)慣是先讓所有規(guī)則“無(wú)腦執(zhí)行”哪怕空倉(cāng)也要跑一遍風(fēng)控檢查邏輯——這樣能保證規(guī)則的完備性不會(huì)在極端行情下被漏掉。1.4 執(zhí)行層解決“以什么成本買到”執(zhí)行層是最容易被新手忽略、但同樣是最能體現(xiàn)系統(tǒng)是否“開通”了的一環(huán)。信號(hào)層說(shuō)“可以買”、風(fēng)控層說(shuō)“買3成倉(cāng)位”執(zhí)行層要負(fù)責(zé)把它變成“明天開盤后以不超過(guò)XX價(jià)格買入XX股”這樣一條可落地的委托。A股的執(zhí)行約束非常多最小交易單位是100股科創(chuàng)板是200股起、之后按1股遞增、買入申報(bào)價(jià)不能超過(guò)漲停價(jià)、T1規(guī)則下當(dāng)天買進(jìn)的股票當(dāng)天不能賣。這些細(xì)節(jié)如果不在執(zhí)行層處理好回測(cè)里利潤(rùn)豐厚實(shí)盤一跑就各種廢單。所以完整的執(zhí)行層應(yīng)當(dāng)至少包含根據(jù)可用資金和最小交易單位計(jì)算真實(shí)買入數(shù)量根據(jù)當(dāng)前價(jià)和可容忍滑點(diǎn)計(jì)算限價(jià)根據(jù)T1規(guī)則判斷某只票是否真的“可賣”必要時(shí)把大單拆成小單避免對(duì)流動(dòng)性不足的標(biāo)的造成沖擊。2. 數(shù)據(jù)準(zhǔn)備與AI信號(hào)生成讓模型先能“開口說(shuō)話”2.1 數(shù)據(jù)源的接入與清洗三層骨架里信號(hào)層的第一步是拿到干凈的數(shù)據(jù)。對(duì)A股日線級(jí)策略來(lái)說(shuō)最基礎(chǔ)的數(shù)據(jù)就是“OHLCV”開盤、最高、最低、收盤、成交量。國(guó)內(nèi)有很多開源數(shù)據(jù)接口數(shù)據(jù)質(zhì)量參差不齊我建議優(yōu)先選擇提供“前復(fù)權(quán)”數(shù)據(jù)的接口。原因很簡(jiǎn)單如果股票發(fā)生過(guò)除權(quán)除息不復(fù)權(quán)的K線會(huì)出現(xiàn)向下的跳空缺口直接拿去算收益率特征會(huì)得到大量虛假的“暴跌”信號(hào)。用前復(fù)權(quán)數(shù)據(jù)處理歷史回測(cè)再用“不復(fù)權(quán)價(jià)格 除權(quán)除息日修正”來(lái)撮合模擬盤是業(yè)界基本打法。拿到數(shù)據(jù)后我一般還會(huì)做三層清洗剔除上市不足半年的次新股這些股上市初期波動(dòng)異常很多因子會(huì)失真。剔除ST和長(zhǎng)期停牌股它們流動(dòng)性差、漲跌停規(guī)則特殊初期接入容易帶偏整體評(píng)估。對(duì)齊交易自然日把行情數(shù)據(jù)按交易日對(duì)齊非交易日的數(shù)據(jù)如果接口返回了直接丟棄。第一步的數(shù)據(jù)加載不用寫太多花活核心是把“日期索引 前復(fù)權(quán)價(jià)格 成交量”整理好供因子計(jì)算使用。import pandas as pd def load_daily_data(code, start2018-01-01, end2024-12-31): # 這里是一個(gè)典型的開源數(shù)據(jù)接口封裝示意實(shí)際使用時(shí)換成你的數(shù)據(jù)源即可 df get_daily_price(code, start_datestart, end_dateend, adjustqfq) df df[[date, open, high, low, close, volume]].copy() df[date] pd.to_datetime(df[date]) df df.set_index(date).sort_index() # 剔除停牌日成交量或成交額為0的行 df df[(df[volume] 0) (df[close] 0)] return df2.2 特征工程與標(biāo)簽構(gòu)造有了K線數(shù)據(jù)下一步是構(gòu)造特征。特征不用貪多我一開始只用了三組動(dòng)量類過(guò)去5、10、20個(gè)交易日的累計(jì)收益率衡量趨勢(shì)強(qiáng)度。波動(dòng)類近10、20日收益率標(biāo)準(zhǔn)差衡量不確定性。量?jī)r(jià)配合類近5日量能變化率和價(jià)格變化率的比值衡量放量上漲還是縮量下跌。這三組特征加起來(lái)不到10個(gè)足夠讓模型學(xué)出一些簡(jiǎn)單的非線性關(guān)系。真正重要的是標(biāo)簽怎么構(gòu)造。我的經(jīng)驗(yàn)是用“未來(lái)5個(gè)交易日收益率為正與否”作為二分類標(biāo)簽比“未來(lái)1日收益率方向”穩(wěn)定得多。5日窗口能過(guò)濾掉大量日內(nèi)噪音也更貼近A股投資者真實(shí)持有周期。標(biāo)簽構(gòu)造有一個(gè)最關(guān)鍵的細(xì)節(jié)防止未來(lái)函數(shù)泄漏。當(dāng)你用第T日的數(shù)據(jù)做特征去預(yù)測(cè)第T1到T5日的收益方向時(shí)標(biāo)簽里絕對(duì)不能包含第T日當(dāng)天收盤前無(wú)法獲得的信息。很多初學(xué)者在同一個(gè)DataFrame里既算特征又算標(biāo)簽忘記用shift把標(biāo)簽整體往后挪一天導(dǎo)致回測(cè)時(shí)模型“偷看”了未來(lái)數(shù)據(jù)。這類泄漏在實(shí)盤里根本無(wú)法復(fù)制屬于量化新手最容易踩的暗坑。def build_dataset(df): x df.copy() x[ret_1] x[close].pct_change() x[ret_5] x[close].pct_change(5) x[ret_10] x[close].pct_change(10) x[ret_20] x[close].pct_change(20) x[vol_10] x[ret_1].rolling(10).std() x[vol_20] x[ret_1].rolling(20).std() x[vol_ratio] x[volume] / x[volume].rolling(5).mean() x[price_vol_ratio] x[ret_5] / (x[vol_10] 1e-8) # 標(biāo)簽未來(lái)5日收益率是否為正。shift(-5) 表示取未來(lái)第5天的收益 x[future_ret] x[close].shift(-5) / x[close] - 1 x[label] (x[future_ret] 0).astype(int) # 特征必須在T日收盤時(shí)可得所以特征本身不需要shift # 但建模時(shí)要讓模型用T日特征預(yù)測(cè)T1T5的標(biāo)簽這里通過(guò)時(shí)序切分控制 return x.dropna()2.3 用XGBoost生成方向信號(hào)特征和標(biāo)簽準(zhǔn)備好之后模型選型我推薦先用 XGBoost。很多人覺得AI量化必須上深度學(xué)習(xí)其實(shí)中低頻表格數(shù)據(jù)場(chǎng)景樹模型往往更快、更穩(wěn)、更好解釋。我拿LSTM和XGBoost在同樣特征集上比過(guò)XGBoost的 AUC 普遍高一些而且調(diào)參成本低得多。訓(xùn)練時(shí)我用了一個(gè)非常重要的切分方式時(shí)間序列按順序切而不是隨機(jī)切。因?yàn)楣蓛r(jià)數(shù)據(jù)天然是時(shí)序的如果隨機(jī)把未來(lái)某一年數(shù)據(jù)混進(jìn)訓(xùn)練集相當(dāng)于模型提前見到了“答案”。我的習(xí)慣是前70%做訓(xùn)練、中間15%做驗(yàn)證、最后15%做測(cè)試驗(yàn)證集用來(lái)調(diào)超參數(shù)和確定買入閾值測(cè)試集只在最后跑一次避免反復(fù)試出來(lái)的“看上去很美”的結(jié)果。from xgboost import XGBClassifier features [ret_1, ret_5, ret_10, ret_20, vol_10, vol_20, vol_ratio, price_vol_ratio] full build_dataset(load_daily_data(000001)) train_end int(len(full) * 0.7) valid_end int(len(full) * 0.85) x_train, y_train full.iloc[:train_end][features], full.iloc[:train_end][label] x_valid, y_valid full.iloc[train_end:valid_end][features], full.iloc[train_end:valid_end][label] x_test, y_test full.iloc[valid_end:][features], full.iloc[valid_end:][label] model XGBClassifier(max_depth3, n_estimators100, learning_rate0.05, subsample0.8, random_state42) model.fit(x_train, y_train)這里我特意把random_state固定。為什么A股行情是確定的但模型訓(xùn)練過(guò)程中存在隨機(jī)性如果你不固定隨機(jī)種子今天跑出一個(gè)結(jié)果、明天跑出另一個(gè)結(jié)果后面所有風(fēng)控參數(shù)調(diào)整都會(huì)變得毫無(wú)意義。固定隨機(jī)種子是最基本、也最容易被忽略的實(shí)驗(yàn)紀(jì)律。2.4 從模型概率到交易信號(hào)模型輸出的是“未來(lái)5日上漲”的概率不能直接拿來(lái)下單。我的做法是把它轉(zhuǎn)換成一個(gè)帶死區(qū)的離散信號(hào)。具體來(lái)說(shuō)在驗(yàn)證集上跑出所有樣本的概率分布然后挑選兩個(gè)閾值當(dāng)概率大于等于0.62時(shí)給1看多概率小于等于0.42時(shí)給-1看空中間一律給0觀望。這兩個(gè)閾值不是拍腦袋定的而是讓驗(yàn)證集的F1分?jǐn)?shù)盡量高、同時(shí)換手率盡量低。這一步本質(zhì)上是壓縮模型的自由度模型不必每一次都給出明確觀點(diǎn)一個(gè)“猶豫”的0信號(hào)能夠大幅減少無(wú)效交易帶來(lái)的摩擦成本。prob model.predict_proba(x_valid)[:, 1] k_threshold_high 0.62 # 得在驗(yàn)證集上反復(fù)調(diào) k_threshold_low 0.42 signal pd.Series(0, indexx_valid.index) signal[prob k_threshold_high] 1 signal[prob k_threshold_low] -1這里也順帶說(shuō)明一個(gè)我常被問到的問題為什么閾值設(shè)得這么不對(duì)稱因?yàn)锳股有T1約束和做空限制沒有辦法直接做空所以“看空信號(hào)”更適合被解釋為“空倉(cāng)等待”而不是“賣出做空”。這樣一來(lái)看空閾值可以設(shè)得保守一些只有模型非常有把握時(shí)才應(yīng)該清倉(cāng)而看多閾值對(duì)應(yīng)的是可以入場(chǎng)的較強(qiáng)預(yù)期需要兼顧置信度和交易次數(shù)。3. 風(fēng)控與倉(cāng)位管理保證策略的“活下去”3.1 倉(cāng)位計(jì)算公式信號(hào)層輸出之后風(fēng)控層要接住。我最常用的倉(cāng)位模型是基于“置信度縮放”的固定比例倉(cāng)位先定一個(gè)單票最大倉(cāng)位上限比如10%然后乘以信號(hào)置信度得到實(shí)際倉(cāng)位。置信度我用的是模型概率與閾值之間的“距離”距離越大說(shuō)明模型越有把握持倉(cāng)可以略微加重距離小則只給底倉(cāng)。舉個(gè)例子看多閾值為0.62當(dāng)前概率0.8距離是0.18那么倉(cāng)位調(diào)整系數(shù)可以是1 0.18 * 2 1.36再乘上最大倉(cāng)位10%得到約13.6%的倉(cāng)位如果概率只有0.63距離很小那倉(cāng)位就只有10%附近。這么做的好處是模型大部分時(shí)間不會(huì)滿配一旦行情超出預(yù)期賬戶還有加倉(cāng)空間行情不及預(yù)期虧損頭寸也相對(duì)可控。def calc_position(max_weight, prob, threshold_high): distance max(prob - threshold_high, 0) scale 1 distance * 2.0 weight max_weight * min(scale, 1.5) return round(weight, 4)這里需要提醒一句任何單票倉(cāng)位上限都不能超過(guò)賬戶可承受的單筆虧損閾值。如果你單票最多只能虧總資金的1%且止損位距離入場(chǎng)價(jià)5%那最大倉(cāng)位就不能超過(guò)20%。倉(cāng)位和止損是兩個(gè)天然綁在一起的旋鈕單獨(dú)擰一個(gè)都會(huì)出事。3.2 ATR止損與移動(dòng)止損止損規(guī)則我在A股中低頻策略里最常用的是ATR止損。ATR平均真實(shí)波幅衡量的是個(gè)股近期的日內(nèi)真實(shí)波動(dòng)范圍比固定百分比止損更能適應(yīng)不同波動(dòng)率的股票。比如說(shuō)你定了2倍ATR止損意思是從買入價(jià)向下2倍的日波幅才止損股票波動(dòng)大就容忍多一點(diǎn)波動(dòng)小就嚴(yán)格一點(diǎn)。實(shí)際計(jì)算ATR需要用到最高價(jià)、最低價(jià)和前一收盤價(jià)。計(jì)算方式不復(fù)雜關(guān)鍵是回測(cè)和實(shí)盤都要用同一套算法不能回測(cè)時(shí)用日線ATR、實(shí)盤時(shí)用5分鐘ATR邏輯全錯(cuò)位。def calc_atr(df, period14): prev_close df[close].shift(1) tr np.maximum(df[high] - df[low], np.maximum(abs(df[high] - prev_close), abs(df[low] - prev_close))) return tr.rolling(period).mean()有了ATR止損價(jià)就是入場(chǎng)價(jià) - 止損倍數(shù) * ATR。如果信號(hào)還沒失效但價(jià)格一路上漲我建議把止損失水平移到“入場(chǎng)后最高價(jià) - 2倍ATR”這樣能鎖定利潤(rùn)同時(shí)給趨勢(shì)足夠空間。我見過(guò)很多人設(shè)了止盈線一漲到目標(biāo)價(jià)就賣飛結(jié)果放著放著立刻翻倍了。移動(dòng)止損比固定止盈更符合趨勢(shì)交易的邏輯。3.3 組合級(jí)回撤熔斷除了單票止損組合層面還得有一個(gè)總體的“熔斷機(jī)制”。我的規(guī)則很簡(jiǎn)單如果賬戶凈值距離最近高點(diǎn)的回撤超過(guò)15%次日開始強(qiáng)制空倉(cāng)5個(gè)交易日等情緒冷卻之后再重新開放交易?;爻芬坏┙咏?0%我再?gòu)?qiáng)制空倉(cāng)10個(gè)交易日??諅}(cāng)期間信號(hào)繼續(xù)記錄但不產(chǎn)生委托相當(dāng)于給策略一次“重新冷靜”的機(jī)會(huì)。這套規(guī)則的設(shè)計(jì)邏輯是當(dāng)策略連續(xù)虧損到一定幅度大概率是市場(chǎng)風(fēng)格發(fā)生了劇烈切換此時(shí)模型產(chǎn)出的信號(hào)可信度大幅下降。繼續(xù)硬扛只會(huì)讓虧損繼續(xù)放大不如干脆停下來(lái)觀察。風(fēng)控層里面回撤熔斷是最能避免“深度學(xué)習(xí)模型在某一年翻了車、卻把前三年利潤(rùn)全部回吐”悲劇的防線。def check_drawdown(net_value, max_dd0.15): peak np.maximum.accumulate(net_value) dd net_value / peak - 1 if dd.iloc[-1] -max_dd: return {action: cut_all, reason: portfolio_drawdown} return {action: normal, reason: }回撤熔斷不是越多越好。我見過(guò)有人把閾值設(shè)成5%結(jié)果一年內(nèi)大部分時(shí)間都在空倉(cāng)徹底錯(cuò)過(guò)了所有反彈。閾值本身要和策略的波動(dòng)特征匹配驗(yàn)證集上最大回撤如果常年是12%那閾值就別放到10%如果策略最大回撤經(jīng)常18%那15%才是合理位置。4. 執(zhí)行層與回測(cè)驗(yàn)證讓策略從紙面走向“模擬盤”4.1 從信號(hào)到委托單信號(hào)和倉(cāng)位都算好之后執(zhí)行層要把“該買某只股票10%倉(cāng)位”翻譯成“以什么價(jià)格、買多少股、哪一天能生效”。我處理A股模擬盤的經(jīng)驗(yàn)是信號(hào)在第T日收盤后產(chǎn)生那么委托單最早只能掛第T1日。也就是說(shuō)策略要有一個(gè)強(qiáng)制性的“次日生效”邏輯。千萬(wàn)別在回測(cè)里用當(dāng)天的收盤價(jià)直接成交那是典型的未來(lái)函數(shù)。代碼里最簡(jiǎn)單的處理是把信號(hào)整體shift(1)讓第T日算出來(lái)的目標(biāo)倉(cāng)位第T1日才參與撮合。股數(shù)計(jì)算也要考慮A股的“一手100股”。先根據(jù)目標(biāo)市值算出理論股數(shù)再向下取整到100的倍數(shù)。資金不足時(shí)剩余資金寧可留著當(dāng)現(xiàn)金也不能為了滿倉(cāng)去湊零股。def calc_share_count(target_value, current_price, lot_size100): raw_shares int(target_value / current_price / lot_size) * lot_size return max(raw_shares, 0)除此之外盤口約束必須硬性編碼。比如買入價(jià)不能高于漲停價(jià)賣出價(jià)不能低于跌停價(jià)如果當(dāng)前價(jià)已經(jīng)漲停意味著大概率買不進(jìn)去這時(shí)候委托單要直接作廢不能假裝成交。4.2 回測(cè)引擎的搭建在回測(cè)層面我建議先把“向量化回測(cè)”跑通再做事件驅(qū)動(dòng)回測(cè)。向量化回測(cè)適合快速驗(yàn)證策略邏輯對(duì)不對(duì)它假設(shè)所有信號(hào)在同一時(shí)間點(diǎn)批量作用在所有股票上省去逐筆撮合的復(fù)雜度。比如你有20只股票每只股票每天都有倉(cāng)位目標(biāo)直接按持倉(cāng)市值變化計(jì)算組合凈值即可。向量化回測(cè)最大的優(yōu)點(diǎn)是快、清晰適合每天磨信號(hào)和倉(cāng)位參數(shù)。但它的先天缺陷是忽略了沖擊成本和個(gè)股流動(dòng)性差異所以回測(cè)凈值只能當(dāng)參考。等策略邏輯基本定型我會(huì)再寫一個(gè)按日循環(huán)的模擬盤腳本逐日根據(jù)信號(hào)下單成交價(jià)采用“次日開盤價(jià) 滑點(diǎn)估計(jì)”來(lái)模擬這才是真正接近實(shí)盤環(huán)境的驗(yàn)證。position target_weight.shift(1) # T日信號(hào)T1日生效 ret daily_stock_ret 1 portfolio_ret (position * ret).sum(axis1) (1 - position.sum(axis1)) # 剩余現(xiàn)金部分無(wú)收益 net_value (1 portfolio_ret).cumprod()4.3 交易成本與滑點(diǎn)模擬盤里交易成本必須包含傭金、印花稅和滑點(diǎn)。傭金按成交金額的萬(wàn)二到萬(wàn)三估算買賣雙向都收印花稅只在賣出時(shí)收目前是千一滑點(diǎn)按買入價(jià)上浮0.1%、賣出價(jià)下浮0.1%估算。別小看這三個(gè)0.1%級(jí)別的數(shù)字中低頻策略一年的換手如果超過(guò)10倍成本合計(jì)就是總資金的3到5個(gè)點(diǎn)。很多回測(cè)里年化20%的策略扣完成本只剩10%這非?,F(xiàn)實(shí)。def estimate_trade_cost(buy_amount, sell_amount, commission_rate0.0003, stamp_tax0.001, slippage0.001): buy_cost buy_amount * (commission_rate slippage) sell_cost sell_amount * (commission_rate stamp_tax slippage) return buy_cost sell_cost4.4 評(píng)估指標(biāo)怎么讀最后回測(cè)結(jié)果不能只看“最終收益”。我最關(guān)注的指標(biāo)有三個(gè)年化收益、最大回撤、夏普比率。其中最大回撤決定了你的心理承受能力和加杠桿空間如果最大回撤30%說(shuō)明策略和你的風(fēng)險(xiǎn)偏好根本不匹配再高的年化也別碰。順便說(shuō)一句勝率這個(gè)指標(biāo)很容易誤導(dǎo)人。我的經(jīng)驗(yàn)是如果平均盈虧比能到1.5以上勝率只有40%的短線策略依然能賺錢如果勝率70%但盈虧比只有0.5長(zhǎng)期下來(lái)大概率虧。所以回測(cè)報(bào)告里我會(huì)把“平均盈利/平均虧損”和“勝率”并列看缺一不可。def evaluate(net_value): annual_return net_value.iloc[-1] ** (252 / len(net_value)) - 1 daily_ret net_value.pct_change().dropna() sharpe daily_ret.mean() / daily_ret.std() * np.sqrt(252) max_dd (net_value / np.maximum.accumulate(net_value) - 1).min() return {annual_return: annual_return, sharpe: sharpe, max_drawdown: max_dd}5. 常見問題與排查技巧實(shí)錄5.1 未來(lái)函數(shù)的三個(gè)典型雷區(qū)這是量化新手最容易犯也最隱蔽的錯(cuò)誤。我復(fù)盤自己踩過(guò)的雷總結(jié)出三個(gè)高頻雷區(qū)第一個(gè)標(biāo)簽泄漏。前面提過(guò)構(gòu)造未來(lái)收益標(biāo)簽時(shí)忘記整體后移導(dǎo)致模型提前學(xué)到了“未來(lái)信息”。判斷方法很簡(jiǎn)單用訓(xùn)練好的模型在測(cè)試集上算AUC如果高到離譜比如超過(guò)0.75大概率有泄漏。第二個(gè)成交價(jià)用未來(lái)價(jià)。很多人回測(cè)時(shí)直接拿“當(dāng)日收盤價(jià)”買入但信號(hào)是收盤后才產(chǎn)生的實(shí)盤根本買不到當(dāng)天的收盤價(jià)。正確做法是信號(hào)T日出買入價(jià)至少是T1日的開盤價(jià)。第三個(gè)幸存者偏差。選股票池時(shí)如果直接用“當(dāng)前還在上市的股票”就相當(dāng)于剔除了過(guò)去幾年退市的股票?;販y(cè)結(jié)果看起來(lái)很好實(shí)際是漏掉了大量失敗案例。正確做法是用歷史某一天的成分股名單去回測(cè)那一天而不是用今天的名單覆蓋全部歷史。5.2 過(guò)擬合的識(shí)別AI量化里過(guò)擬合的典型表現(xiàn)是“訓(xùn)練集收益一片大好、驗(yàn)證集收益斷崖下跌”。我的排查辦法是看驗(yàn)證集和訓(xùn)練集的年化收益差如果差距超過(guò)50%或者直接由正轉(zhuǎn)負(fù)先別急著怪市場(chǎng)大概率是特征或者模型參數(shù)過(guò)擬合了。常用的緩解手段包括增加樣本內(nèi)時(shí)間跨度、減少特征數(shù)量、限制數(shù)模型深度XGBoost的max_depth別超過(guò)4、增大subsample隨機(jī)采樣比例、把連續(xù)型收益率標(biāo)簽改為排序型標(biāo)簽等。但最粗暴也有效的一招是驗(yàn)證集指標(biāo)不達(dá)標(biāo)絕對(duì)不回測(cè)模型換參數(shù)。我之前就是因?yàn)樘狈磸?fù)在測(cè)試集上調(diào)參最后測(cè)出來(lái)的結(jié)果完全失真。5.3 數(shù)據(jù)對(duì)齊與停牌處理A股停牌非常常見尤其重組停牌動(dòng)輒一兩個(gè)月。如果回測(cè)時(shí)沒有對(duì)停牌做任何處理停牌期間持倉(cāng)的每日收益可能是0看上去“回避了下跌”也可能“錯(cuò)過(guò)了上漲”結(jié)果嚴(yán)重失真。我的處理方法是把停牌日的收益標(biāo)記為0同時(shí)在執(zhí)行層把停牌股票的委托單自動(dòng)掛起直到復(fù)牌才繼續(xù)處理。數(shù)據(jù)對(duì)齊還有一個(gè)隱蔽問題不同數(shù)據(jù)源的交易日歷不一樣。有的接口會(huì)把節(jié)假日非交易日也保留有的會(huì)剔除。我統(tǒng)一的做法是把所有股票數(shù)據(jù)對(duì)齊到同一個(gè)交易日歷上以全市場(chǎng)成交量最大的那些股票的交易日作為基準(zhǔn)。我把這套三層骨架在模擬盤上完整跑通之后最大的體會(huì)是AI量化真正難的不是模型而是把模型輸出放到一個(gè)受控的交易鏈路里。信號(hào)層負(fù)責(zé)“聰明”風(fēng)控層負(fù)責(zé)“活著”執(zhí)行層負(fù)責(zé)“落地”只要這三層界限清晰后面模型哪怕?lián)Q成圖神經(jīng)網(wǎng)絡(luò)或者說(shuō)加一個(gè)輿情因子都只是替換其中一塊積木而已。這篇是系列的上篇先把骨架和數(shù)據(jù)到信號(hào)這一段走通。后面有空我會(huì)單獨(dú)寫風(fēng)控參數(shù)那部分——尤其是ATR止損倍數(shù)怎么根據(jù)你交易頻率去調(diào)那又是一個(gè)能講一整篇的話題。