據(jù)分析實(shí)戰(zhàn):從爬蟲(chóng)采集到策略回測(cè))
簡(jiǎn)介這是一份圍繞“基于股票大數(shù)據(jù)分析Python實(shí)戰(zhàn)”主題整理的下載資料面向想要運(yùn)用Python完成股票數(shù)據(jù)采集、清洗、分析與預(yù)測(cè)的學(xué)習(xí)者和金融數(shù)據(jù)分析入門(mén)者幫助解決從爬蟲(chóng)取數(shù)到建模展示的全流程問(wèn)題。資源包以7z格式打包整體大小約4.47MB上游暫未提供包內(nèi)文件總數(shù)與文件類(lèi)型明細(xì)具體目錄結(jié)構(gòu)需下載解壓后查看。目前已有3654人學(xué)習(xí)下載適合已掌握Python基礎(chǔ)、希望快速進(jìn)入量化分析場(chǎng)景的讀者作為實(shí)戰(zhàn)參考。內(nèi)容主題覆蓋BeautifulSoup/Scrapy爬蟲(chóng)抓取股票數(shù)據(jù)、Pandas數(shù)據(jù)清洗與格式統(tǒng)一、NumPy/SciPy統(tǒng)計(jì)計(jì)算、Matplotlib/Seaborn可視化、Scikit-learn機(jī)器學(xué)習(xí)建模以及Tushare/Wind接口實(shí)時(shí)數(shù)據(jù)與Flask/Django部署等關(guān)鍵環(huán)節(jié)可幫助讀者建立一套可落地的股票數(shù)據(jù)分析方法論也能為后續(xù)投資決策提供數(shù)據(jù)支撐。1. 做股票分析先解決數(shù)據(jù)問(wèn)題Python 爬蟲(chóng)和數(shù)據(jù)分析能幫你把碎片數(shù)據(jù)變成可執(zhí)行結(jié)論我認(rèn)識(shí)一位做個(gè)股跟蹤的開(kāi)發(fā)者他每天光是把行情軟件里的歷史數(shù)據(jù)一段段復(fù)制到表格里再手工計(jì)算均線(xiàn)、MACD 就要花掉兩個(gè)多小時(shí)而且數(shù)據(jù)源一換之前的手工活全部作廢。后來(lái)我把這套基于股票大數(shù)據(jù)分析 Python 實(shí)戰(zhàn)的資源包丟給他——核心思路是用 Python 爬蟲(chóng)把行情、財(cái)務(wù)、公告數(shù)據(jù)統(tǒng)一拉進(jìn)本地庫(kù)再通過(guò)清洗、指標(biāo)計(jì)算和回測(cè)把數(shù)據(jù)變成可驗(yàn)證的交易信號(hào)。整個(gè)過(guò)程從原來(lái)的人工兩小時(shí)壓縮到每天收盤(pán)后自動(dòng)跑二十分鐘。這套資源適合兩類(lèi)人一類(lèi)是要做個(gè)股或板塊量化驗(yàn)證的投資者另一類(lèi)是剛?cè)腴T(mén)數(shù)據(jù)分析和爬蟲(chóng)、想找一個(gè)完整實(shí)戰(zhàn)項(xiàng)目練手的開(kāi)發(fā)者。它解決的不是「預(yù)測(cè)漲跌」這種玄學(xué)問(wèn)題而是「數(shù)據(jù)從哪來(lái)、怎么對(duì)齊、怎么算、怎么驗(yàn)證」這條工程鏈路。2. 數(shù)據(jù)獲取層爬蟲(chóng)采集方案、請(qǐng)求頻率與增量更新設(shè)計(jì)2.1 數(shù)據(jù)邊界與優(yōu)先級(jí)K線(xiàn)、財(cái)務(wù)、公告、輿情哪些必須爬很多人拿到股票數(shù)據(jù)項(xiàng)目后的第一反應(yīng)是把能爬的全爬下來(lái)結(jié)果磁盤(pán)暴漲、更新任務(wù)跑到半夜最后分析時(shí)真正用到的字段不到 20%。我一般會(huì)在動(dòng)手前先定數(shù)據(jù)邊界按使用頻率和決策價(jià)值排優(yōu)先級(jí)。數(shù)據(jù)類(lèi)型更新頻率決策價(jià)值優(yōu)先級(jí)日K線(xiàn)開(kāi)高低收、成交量每日極高所有指標(biāo)計(jì)算的基礎(chǔ)P0復(fù)權(quán)因子 / 股本結(jié)構(gòu)不定期高直接影響收益率計(jì)算P0財(cái)務(wù)數(shù)據(jù)利潤(rùn)、負(fù)債、現(xiàn)金流每季度中高用于基本面過(guò)濾P1公告與新聞?wù)咳罩惺录?qū)動(dòng)場(chǎng)景才用P2輿情評(píng)論 / 論壇帖子每日低噪聲大建議早期放棄P3K線(xiàn)數(shù)據(jù)是P0中的P0因?yàn)榫€(xiàn)、RSI、MACD、波動(dòng)率全部依賴(lài)它。財(cái)務(wù)數(shù)據(jù)我會(huì)放到 P1它不需要每天爬季度更新即可。輿情在早期分析里我建議直接砍掉——清洗成本極高而且對(duì)短線(xiàn)信號(hào)的提升非常有限。這個(gè)取舍看起來(lái)保守但能保證每日數(shù)據(jù)更新的時(shí)間壓在可控范圍內(nèi)分析模塊也不用面對(duì)一堆無(wú)意義的噪聲。2.2 請(qǐng)求策略與容錯(cuò)重試、限頻、字段裁剪數(shù)據(jù)源接口是黑匣子你不知道它什么時(shí)候會(huì)限流、什么時(shí)候會(huì)返回殘缺數(shù)據(jù)。資源包里的數(shù)據(jù)層設(shè)計(jì)思路是「統(tǒng)一入站接口 分級(jí)容錯(cuò)」所有獲取操作都走同一個(gè)函數(shù)不直接散落各處請(qǐng)求。# stock_loader.py資源包內(nèi)模塊 import time import random import logging from datetime import datetime, timedelta logger logging.getLogger(stock_loader) def fetch_daily_bars(symbol, start_date, end_date, fieldsNone, max_retries3): 獲取日K線(xiàn)數(shù)據(jù)統(tǒng)一入口。 參數(shù): symbol: 股票代碼如 600001 start_date: 開(kāi)始日期 YYYY-MM-DD end_date: 結(jié)束日期默認(rèn)今天 fields: 需要返回的字段列表如 [open, high, low, close, volume] max_retries: 請(qǐng)求失敗后的重試次數(shù) base_params { symbol: symbol, start: start_date, end: end_date, fields: fields or [open, high, low, close, volume] } for attempt in range(max_retries): try: # 底層請(qǐng)求由資源包內(nèi)置的 data_source 模塊完成 raw data_source.request_kline(base_params) if raw is None or len(raw) 0: raise ValueError(empty response) return normalize_kline(raw, fieldsbase_params[fields]) except Exception as e: wait_time 2 ** attempt random.uniform(0, 1) logger.warning(fetch %s failed (attempt %d): %s, retry in %.1fs, symbol, attempt 1, e, wait_time) time.sleep(wait_time) logger.error(give up on %s after %d attempts, symbol, max_retries) return None這段代碼把請(qǐng)求頻率控制和重試邏輯收斂到了獲取層。max_retries3配合指數(shù)退避2的冪次遞增等待時(shí)間比固定等待更能適應(yīng)服務(wù)端的限流策略。加random.uniform(0, 1)是為了避免多個(gè)股票同時(shí)重試造成請(qǐng)求共振這在批量更新場(chǎng)景下很關(guān)鍵。fields參數(shù)用于裁剪字段只取需要的列減少傳輸體積和解析時(shí)間。需要注意的是底層data_source.request_kline是一個(gè)抽象層資源包實(shí)際數(shù)使用開(kāi)源數(shù)據(jù)協(xié)議實(shí)現(xiàn)你不需要關(guān)心具體數(shù)據(jù)來(lái)自哪個(gè)服務(wù)商只要保證它返回的是標(biāo)準(zhǔn) OHLCV 結(jié)構(gòu)就行。如果換數(shù)據(jù)源只需替換data_source一個(gè)模塊上層代碼完全不動(dòng)。2.3 增量更新與定時(shí)任務(wù)每天收盤(pán)后自動(dòng)補(bǔ)數(shù)據(jù)全量下載幾萬(wàn)條歷史行情只需要做一次日常維護(hù)的核心是增量更新。增量更新的關(guān)鍵在于「日期斷點(diǎn)」不能簡(jiǎn)單按自然日判斷因?yàn)橹苣?、法定?jié)假日、臨時(shí)休市都會(huì)導(dǎo)致數(shù)據(jù)缺失。# daily_update.py資源包內(nèi)腳本 import sqlite_engine # 資源包內(nèi)置的嵌入式存儲(chǔ)引擎 from stock_loader import fetch_daily_bars def get_last_trade_date(symbol): 查本地庫(kù)中該股票最近一個(gè)交易日作為增量起點(diǎn)。 row sqlite_engine.execute( SELECT MAX(trade_date) FROM daily_bars WHERE symbol ?, (symbol,) ) return row[0] if row and row[0] else None def incremental_update(symbol_list, end_dateNone): for symbol in symbol_list: last_date get_last_trade_date(symbol) if last_date is None: # 首次使用回拉最近 250 個(gè)交易日 bars fetch_daily_bars(symbol, start_date250d, end_dateend_date) else: # 從斷點(diǎn)后一天開(kāi)始增量拉取 start (datetime.strptime(last_date, %Y-%m-%d) timedelta(days1)).strftime(%Y-%m-%d) bars fetch_daily_bars(symbol, start_datestart, end_dateend_date) if bars: sqlite_engine.insert_bars(symbol, bars) logger.info(%s inserted %d bars, symbol, len(bars))這里的關(guān)鍵參數(shù)是增量起點(diǎn)用本地庫(kù)最大交易日期加一天作為新請(qǐng)求的開(kāi)始日期而不是用「今天減三天」這類(lèi)拍腦袋邏輯。首次運(yùn)行時(shí)沒(méi)有斷點(diǎn)記錄直接回拉 250 個(gè)交易日覆蓋一整年的數(shù)據(jù)足夠后續(xù)指標(biāo)計(jì)算使用。定時(shí)調(diào)度我用系統(tǒng) crontab 就能滿(mǎn)足不需要額外框架# 每個(gè)交易日 16:30 執(zhí)行增量更新 30 16 * * 1-5 cd /path/to/stock_project python daily_update.py logs/update.log 21注意 16:30 是收盤(pán)后的保守時(shí)間如果數(shù)據(jù)源更新較晚可以往后調(diào)。日志輸出到獨(dú)立文件方便排查更新失敗時(shí)的問(wèn)題。第一次跑增量更新時(shí)建議手動(dòng)執(zhí)行觀察日志輸出確認(rèn)沒(méi)有異常再交給 cron 托管。3. 數(shù)據(jù)清洗與存儲(chǔ)分析結(jié)果可信的前提是數(shù)據(jù)對(duì)齊3.1 復(fù)權(quán)、停牌與缺失值處理行情數(shù)據(jù)的臟數(shù)據(jù)集中在三處未復(fù)權(quán)價(jià)格導(dǎo)致收益率計(jì)算錯(cuò)誤、停牌日被當(dāng)作正常交易日參與指標(biāo)計(jì)算、部分接口在特定日期返回空值。這三個(gè)問(wèn)題不處理后面所有指標(biāo)都不可信。復(fù)權(quán)處理是最容易翻車(chē)的地方之一。我建議統(tǒng)一使用后復(fù)權(quán)數(shù)據(jù)做指標(biāo)計(jì)算和回測(cè)原因很簡(jiǎn)單前復(fù)權(quán)價(jià)格會(huì)隨著新除權(quán)事件的發(fā)生而整體變動(dòng)導(dǎo)致歷史K線(xiàn)在回測(cè)時(shí)出現(xiàn)一條不存在的「跳變」某些策略信號(hào)會(huì)被莫名其妙觸發(fā)。后復(fù)權(quán)價(jià)格只受歷史除權(quán)事件影響增量更新時(shí)歷史數(shù)據(jù)不會(huì)整體重算。# data_clean.py資源包內(nèi)模塊 import pandas as pd def clean_daily_bars(df, adjusthfq): 清洗日K線(xiàn)數(shù)據(jù)。 參數(shù): df: 包含 open/high/low/close/volume 的 DataFrame adjust: 復(fù)權(quán)類(lèi)型hfq 后復(fù)權(quán) / qfq 前復(fù)權(quán) # 1. 按日期排序去除重復(fù)日期 df df.sort_values(trade_date).drop_duplicates(subset[trade_date]) # 2. 剔除成交量或收盤(pán)價(jià)為空的記錄 df df[(df[close].notna()) (df[volume].notna())] # 3. 成交量極低疑似停牌的記錄標(biāo)記而不是刪除 df[is_suspended] df[volume] df[volume].rolling(20, min_periods5).median() * 0.01 # 4. 缺失價(jià)格用前向填充連續(xù)缺失超過(guò)3天則放棄該段 df[close] df[close].ffill(limit3) df[open] df[open].ffill(limit3) df[high] df[high].ffill(limit3) df[low] df[low].ffill(limit3) return dfis_suspended這個(gè)標(biāo)記字段很關(guān)鍵停牌日的成交量通常為 0 或遠(yuǎn)低于正常水平直接把這些日期刪除會(huì)讓時(shí)間軸斷裂影響后續(xù)的技術(shù)指標(biāo)計(jì)算。更好做法是保留日期、標(biāo)記狀態(tài)在計(jì)算指標(biāo)時(shí)區(qū)分對(duì)待。前向填充限幅limit3是為了防止長(zhǎng)期停牌后復(fù)牌首日的價(jià)格被錯(cuò)誤填充出虛假連續(xù)行情。3.2 存儲(chǔ)選型嵌入式列式存儲(chǔ)為什么比關(guān)系庫(kù)適合本地研究直接在 CSV 上做數(shù)據(jù)分析在大量個(gè)股上非常痛苦讀取全表占用內(nèi)存大、按日期切片效率低、多標(biāo)的并發(fā)查詢(xún)?nèi)菀壮霈F(xiàn)文件鎖沖突。我早期踩過(guò)這個(gè)坑后來(lái)?yè)Q成了嵌入式列式存儲(chǔ)壓力瞬間小了很多。維度CSV 文件嵌入式列式存儲(chǔ)傳統(tǒng)關(guān)系數(shù)據(jù)庫(kù)單日全量查詢(xún)慢需掃描全部行快列裁剪只讀所需字段中依賴(lài)索引磁盤(pán)占用較高壓縮后約為CSV的 1/4較高部署復(fù)雜度極低低單文件即庫(kù)高需單獨(dú)服務(wù)增量寫(xiě)入不友好需重寫(xiě)文件追加寫(xiě)入效率高中適用場(chǎng)景一次性探索本地量化研究多用戶(hù)協(xié)作資源包內(nèi)置的存儲(chǔ)引擎封裝了嵌入式列式數(shù)據(jù)庫(kù)的讀寫(xiě)接口對(duì)外表現(xiàn)為一個(gè)daily_bars表。你不需要關(guān)心底層存儲(chǔ)格式只需要通過(guò)insert_bars和query_bars兩個(gè)接口操作數(shù)據(jù)。這種選型的直接收益是3000 只股票三年的日K線(xiàn)占用的磁盤(pán)空間和查詢(xún)耗時(shí)都在可接受范圍內(nèi)普通筆記本就能跑得動(dòng)。3.3 數(shù)據(jù)校驗(yàn)跑分析之前先做四道檢查數(shù)據(jù)清洗完成后不能直接進(jìn)入指標(biāo)計(jì)算要先通過(guò)四道校驗(yàn)閘門(mén)記錄數(shù)是否達(dá)到預(yù)期交易日數(shù)量的 90% 以上、價(jià)格是否出現(xiàn)極端異常值如單日漲幅超過(guò) 30%、日期序列是否嚴(yán)格遞增、復(fù)權(quán)后價(jià)格序列是否單調(diào)合理。這四道檢查不需要寫(xiě)復(fù)雜代碼但必須作為獨(dú)立函數(shù)放在分析流程最前面。# validate.py資源包內(nèi)模塊 def validate_bars(df, expected_min_days200): issues [] # 檢查1: 數(shù)據(jù)量 if len(df) expected_min_days: issues.append(insufficient data: %d %d % (len(df), expected_min_days)) # 檢查2: 日期序列 dates df[trade_date].astype(str).tolist() if dates ! sorted(dates): issues.append(trade_date not in ascending order) # 檢查3: 異常價(jià)格 pct_change df[close].pct_change().abs() if (pct_change 0.3).any(): issues.append(abnormal price jump 30%%) # 檢查4: 缺失比率 missing_ratio df[close].isna().mean() if missing_ratio 0.05: issues.append(missing ratio %.1f%% 5%% % (missing_ratio * 100)) return issues四條檢查對(duì)應(yīng)四類(lèi)真實(shí)事故數(shù)據(jù)源截?cái)?、亂序返回導(dǎo)致時(shí)間軸錯(cuò)位、除權(quán)除息未復(fù)權(quán)導(dǎo)致的價(jià)格跳變、接口漏傳導(dǎo)致的空片段。任何一條不通過(guò)就說(shuō)明數(shù)據(jù)層有問(wèn)題這時(shí)候往下算指標(biāo)等于在錯(cuò)誤地基上蓋樓。4. 技術(shù)指標(biāo)計(jì)算與交易信號(hào)生成從行情數(shù)據(jù)到一張可執(zhí)行的選股清單4.1 指標(biāo)計(jì)算均線(xiàn)、MACD、RSI 的參考參數(shù)與代碼實(shí)現(xiàn)數(shù)據(jù)清洗干凈之后下一步是把行情數(shù)據(jù)加工成技術(shù)指標(biāo)。技術(shù)指標(biāo)的計(jì)算本身不復(fù)雜復(fù)雜的是參數(shù)選擇。以我拆過(guò)的模擬項(xiàng)目X來(lái)看一套參數(shù)組合在不同標(biāo)的上的表現(xiàn)差異極大所以代碼實(shí)現(xiàn)時(shí)要保證參數(shù)可配置、可掃描而不是寫(xiě)死。# indicators.py資源包內(nèi)模塊 import pandas as pd def add_indicators(df, ma_windows(5, 10, 20, 60), macd_fast12, macd_slow26, macd_signal9, rsi_period14): 在日K線(xiàn)數(shù)據(jù)上添加常用技術(shù)指標(biāo)列。 out df.copy() # 均線(xiàn)組 for w in ma_windows: out[fma{w}] out[close].rolling(w, min_periodsw).mean() # MACD 指數(shù)平滑均線(xiàn) ema_fast out[close].ewm(spanmacd_fast, adjustFalse).mean() ema_slow out[close].ewm(spanmacd_slow, adjustFalse).mean() out[macd_dif] ema_fast - ema_slow out[macd_dea] out[macd_dif].ewm(spanmacd_signal, adjustFalse).mean() out[macd_hist] out[macd_dif] - out[macd_dea] # RSI 相對(duì)強(qiáng)弱指標(biāo) delta out[close].diff() gain delta.clip(lower0) loss -delta.clip(upper0) avg_gain gain.rolling(rsi_period, min_periodsrsi_period).mean() avg_loss loss.rolling(rsi_period, min_periodsrsi_period).mean() rs avg_gain / avg_loss out[rsi] 100 - (100 / (1 rs)) return outMACD 的 12/26/9 是經(jīng)典參數(shù)不需要改動(dòng)RSI 的 14 周期是業(yè)界默認(rèn)值但在短線(xiàn)分析里可以試試 6 或 9。均線(xiàn)窗口按你的交易周期定5/10/20/60 適合中線(xiàn)波段如果做短線(xiàn)可以把 60 替換成 30。注意min_periods設(shè)置了窗口最小值前 N 個(gè)沒(méi)有完整窗口的日期會(huì)得到 NaN這些行在信號(hào)生成時(shí)必須丟棄或跳過(guò)不能參與計(jì)算。4.2 多因子打分把趨勢(shì)、動(dòng)量和波動(dòng)率合成一個(gè)綜合分單指標(biāo)在任何市場(chǎng)環(huán)境下都會(huì)產(chǎn)生大量假信號(hào)。我更傾向把多個(gè)維度合成一個(gè)綜合分然后對(duì)全市場(chǎng)標(biāo)的做截面排名。因子維度不需要多三個(gè)就夠趨勢(shì)價(jià)格與均線(xiàn)的距離、動(dòng)量近期漲跌幅、波動(dòng)率收益率標(biāo)準(zhǔn)差的反向指標(biāo)。# factors.py資源包內(nèi)模塊 import numpy as np def compute_score(df, trend_weight0.4, momentum_weight0.4, volatility_weight0.2): 計(jì)算多因子綜合得分。 參數(shù): df: 含 ma20/ma60/close 及收益率數(shù)據(jù)的DataFrame trend_weight: 趨勢(shì)因子權(quán)重 momentum_weight: 動(dòng)量因子權(quán)重 volatility_weight: 波動(dòng)率因子權(quán)重值越高得分懲罰越大 out df.copy() # 趨勢(shì)因子收盤(pán)價(jià)相對(duì)20日均線(xiàn)的偏離度 out[trend_factor] (out[close] - out[ma20]) / out[ma20] # 動(dòng)量因子過(guò)去10個(gè)交易日累計(jì)漲幅 out[momentum_factor] out[close].pct_change(10) # 波動(dòng)率因子20日收益率標(biāo)準(zhǔn)差并做反向處理 out[volatility_raw] out[close].pct_change().rolling(20).std() out[volatility_factor] -out[volatility_raw] # 橫截面標(biāo)準(zhǔn)化后加權(quán)合成 def zscore(s): return (s - s.mean()) / (s.std() 1e-9) out[score] ( trend_weight * zscore(out[trend_factor]) momentum_weight * zscore(out[momentum_factor]) volatility_weight * zscore(out[volatility_factor]) ) return out趨勢(shì)因子用的是價(jià)格偏離 20 日均線(xiàn)的百分比而不是簡(jiǎn)單的大小關(guān)系這樣可以避免高價(jià)股天然得分高的問(wèn)題。波動(dòng)率因子取負(fù)值波動(dòng)越大的標(biāo)的得分越低這符合穩(wěn)健優(yōu)先的選股邏輯。三個(gè)權(quán)重之和為 1你可以通過(guò)參數(shù)掃描來(lái)確定最優(yōu)組合但建議先跑默認(rèn)值跑通了再調(diào)。4.3 每日選股與信號(hào)導(dǎo)出算完得分之后你的任務(wù)是把全市場(chǎng)幾千只股票的得分拉出來(lái)排序取 Top N 生成每日選股清單。這個(gè)清單不需要直接觸發(fā)交易它的作用是縮小人工復(fù)核范圍把每天幾千只股票縮到 20 只以?xún)?nèi)。# select_stocks.py資源包內(nèi)模塊 def run_daily_selection(symbol_list, top_n20, min_price5.0, min_volume1000000): 每日選股主流程。 參數(shù): symbol_list: 候選股票代碼列表 top_n: 最終輸出前N只 min_price: 最低價(jià)格過(guò)濾 min_volume: 最低成交量過(guò)濾 results [] for symbol in symbol_list: df query_bars(symbol, limit250) # 取最近250個(gè)交易日 if df is None or len(df) 120: continue df clean_daily_bars(df) df add_indicators(df) df compute_score(df) latest df.iloc[-1] # 基礎(chǔ)過(guò)濾價(jià)格、成交量、是否有足夠歷史數(shù)據(jù) if latest[close] min_price: continue if latest[volume] min_volume: continue if np.isnan(latest[score]): continue results.append({symbol: symbol, score: round(latest[score], 4), close: latest[close], ma_trend: latest[trend_factor]}) results.sort(keylambda x: x[score], reverseTrue) return results[:top_n]min_price5.0和min_volume1000000是兩個(gè)經(jīng)驗(yàn)閾值用來(lái)過(guò)濾掉低價(jià)流動(dòng)性差的標(biāo)的。這個(gè)選股流程跑一次大概耗時(shí)幾分鐘瓶頸在逐標(biāo)的的數(shù)據(jù)讀取和指標(biāo)計(jì)算優(yōu)化空間不大不需要引入并行之類(lèi)的過(guò)度設(shè)計(jì)。5. 避坑指南爬蟲(chóng)、數(shù)據(jù)對(duì)齊與回測(cè)中的常見(jiàn)翻車(chē)點(diǎn)5.1 數(shù)據(jù)獲取與對(duì)齊翻車(chē)現(xiàn)場(chǎng)現(xiàn)象一同一只股票用前復(fù)權(quán)數(shù)據(jù)算出的收益率曲線(xiàn)和后復(fù)權(quán)數(shù)據(jù)差別巨大導(dǎo)致同一個(gè)策略在兩種數(shù)據(jù)下的年化收益差了十幾個(gè)百分點(diǎn)。原因前復(fù)權(quán)價(jià)格會(huì)隨除權(quán)除息事件動(dòng)態(tài)調(diào)整當(dāng)資源包增量更新引入了新的除權(quán)數(shù)據(jù)時(shí)歷史復(fù)權(quán)價(jià)整體偏移而你的回測(cè)邏輯里可能混用了不同時(shí)點(diǎn)導(dǎo)出的數(shù)據(jù)前后口徑不一致。解決全項(xiàng)目統(tǒng)一使用后復(fù)權(quán)數(shù)據(jù)同時(shí)把復(fù)權(quán)因子單獨(dú)存一張表需要真實(shí)價(jià)格比如實(shí)盤(pán)下單價(jià)格時(shí)再通過(guò)復(fù)權(quán)因子反推。簡(jiǎn)單說(shuō)就是數(shù)據(jù)入庫(kù)時(shí)統(tǒng)一轉(zhuǎn) hfq數(shù)據(jù)出口處單獨(dú)做轉(zhuǎn)換任何中間層都不要混用兩種口徑。現(xiàn)象二某些股票在更新后出現(xiàn)連續(xù)幾個(gè)月無(wú)數(shù)據(jù)但官方行情軟件上明明有交易記錄。原因停牌期間數(shù)據(jù)源返回的是空記錄或者成交量字段為 0清洗邏輯把空記錄直接刪掉導(dǎo)致日期斷檔。后續(xù)計(jì)算 RSI 和 MACD 時(shí)rolling 窗口跨過(guò)斷檔產(chǎn)生偏差。解決清洗時(shí)不刪日期只做is_suspended標(biāo)記。指標(biāo)計(jì)算時(shí)遇到標(biāo)記為停牌的行只保留日期不參與數(shù)值計(jì)算。這樣時(shí)間軸完整指標(biāo)連續(xù)回測(cè)也不會(huì)出現(xiàn)從三個(gè)月前直接跳到今天這種詭異曲線(xiàn)。5.2 指標(biāo)計(jì)算與回測(cè)翻車(chē)現(xiàn)場(chǎng)現(xiàn)象三回測(cè)結(jié)果顯示某策略在三年間翻了五倍但實(shí)盤(pán)效果連回測(cè)的一半都不到甚至虧損。原因指標(biāo)計(jì)算中無(wú)意混入了未來(lái)數(shù)據(jù)。最常見(jiàn)的是用pct_change()或rolling()時(shí)沒(méi)有注意窗口的閉合方向比如用當(dāng)天收盤(pán)價(jià)計(jì)算信號(hào)又在當(dāng)天收盤(pán)價(jià)開(kāi)出倉(cāng)位這在回測(cè)中等于偷看了收盤(pán)價(jià)才下單實(shí)盤(pán)根本做不到。解決嚴(yán)格執(zhí)行「信號(hào)生成于收盤(pán)后交易發(fā)生在次日開(kāi)盤(pán)」。代碼上做一次 shift 對(duì)齊signal_shifted score.shift(1)確保用 T 日收盤(pán)數(shù)據(jù)生成的信號(hào)只參與 T1 日的交易邏輯?;販y(cè)框架里再?gòu)?qiáng)制加一條斷言檢查開(kāi)倉(cāng)價(jià)格與信號(hào)產(chǎn)生日期的順序關(guān)系?,F(xiàn)象四增量更新經(jīng)常出現(xiàn)某幾天數(shù)據(jù)缺失手工補(bǔ)跑后指標(biāo)又完全變了之前的分析結(jié)果推倒重來(lái)。原因增量更新依賴(lài)本地庫(kù)的最大交易日期作為斷點(diǎn)但節(jié)假日和半日市會(huì)導(dǎo)致日期判斷失敗。比如某數(shù)據(jù)源在節(jié)前一天只更新到上午 11:30當(dāng)天輸出日期仍然是正常日期但收盤(pán)數(shù)據(jù)缺失一條增量任務(wù)誤判為已更新。解決增量更新后加一步完整性校驗(yàn)對(duì)每個(gè)標(biāo)的比較本地?cái)?shù)據(jù)條數(shù)和數(shù)據(jù)源提供的交易日歷條數(shù)不一致則標(biāo)記為「待補(bǔ)齊」下次更新時(shí)從斷點(diǎn)重新拉取。交易日歷在資源包中單獨(dú)維護(hù)不依賴(lài)數(shù)據(jù)源返回的日期這個(gè)設(shè)計(jì)能堵住九成以上的更新漏洞。6. 策略回測(cè)與參數(shù)調(diào)優(yōu)先證明信號(hào)有效再談實(shí)盤(pán)6.1 最小回測(cè)邏輯選股清單產(chǎn)生了信號(hào)但要確認(rèn)信號(hào)在歷史上真的有效還得過(guò)回測(cè)這一關(guān)。很多人一上來(lái)就引入重型回測(cè)框架我建議先跑一個(gè)最小邏輯信號(hào)產(chǎn)生后次日開(kāi)盤(pán)買(mǎi)入持有固定周期后賣(mài)出記錄每日凈值變化。# backtest.py資源包內(nèi)輕量回測(cè)片段 def simple_backtest(df, signal_colscore, hold_days5): df df.copy() df[position] 0 # 用 T 日信號(hào)T1 產(chǎn)生持倉(cāng)避免未來(lái)函數(shù) df[position] df[signal_col].shift(1) 0 df[ret] df[close].pct_change() # 持倉(cāng)期間收益 持有倉(cāng)位 * 當(dāng)日收益率 df[strategy_ret] df[position].shift(1) * df[ret] df[equity] (1 df[strategy_ret]).cumprod() return df[equity].iloc[-1] - 1這段代碼很短但結(jié)構(gòu)完整shift(1)保證信號(hào)落后于價(jià)格數(shù)據(jù)一天position.shift(1)保證當(dāng)日用當(dāng)日之前的信號(hào)決定當(dāng)日持倉(cāng)。核心邏輯是持有倉(cāng)位乘以當(dāng)日收益率累乘得到凈值曲線(xiàn)。跑通這部分之后再去考慮滑點(diǎn)、手續(xù)費(fèi)和沖擊成本否則一開(kāi)始就被復(fù)雜細(xì)節(jié)淹沒(méi)。6.2 參數(shù)敏感性檢查防止過(guò)擬合的最后一道關(guān)卡回測(cè)收益高不代表策略有效參數(shù)敏感性?huà)呙枋菣z驗(yàn)過(guò)擬合的土辦法。做法很簡(jiǎn)單把關(guān)鍵參數(shù)RSI 周期、均線(xiàn)窗口、動(dòng)量周期分別上下浮動(dòng) 30%重復(fù)跑回測(cè)觀察收益曲線(xiàn)是否平滑。如果參數(shù)從 14 改成 10 收益大幅下跌從 14 改成 18 又大幅上漲說(shuō)明策略被參數(shù)精確擬合到了歷史數(shù)據(jù)上實(shí)盤(pán)基本是廢的。我一般會(huì)在資源包里加一段參數(shù)掃描腳本自動(dòng)輸出熱力表格跑完直接看有沒(méi)有「孤峰」。從那以后我每次跑完回測(cè)都強(qiáng)制走一遍參數(shù)掃描只要看到相鄰參數(shù)收益突變超過(guò) 50%就放棄這組參數(shù)而不是換個(gè)數(shù)據(jù)再試一次。6.3 信號(hào)復(fù)核與推送回測(cè)通過(guò)的信號(hào)進(jìn)入實(shí)盤(pán)驗(yàn)證前還要保留一道人工復(fù)核每天選股清單生成后自動(dòng)疊加最近的新聞?wù)凸嬲攸c(diǎn)檢查是否有業(yè)績(jī)暴雷、重組停牌等事件。這輪檢查不需要自動(dòng)化人工五到十分鐘足夠。確認(rèn)無(wú)誤后再由定時(shí)任務(wù)把清單推送到手機(jī)通知這才是整套流程真正閉環(huán)的時(shí)刻。這套從爬蟲(chóng)、清洗、指標(biāo)到回測(cè)的工程鏈路最怕的是每一步都「差不多」所以我習(xí)慣把數(shù)據(jù)校驗(yàn)、未來(lái)函數(shù)檢查、參數(shù)掃描三道防線(xiàn)寫(xiě)死在流程里寧可慢一點(diǎn)也要先把正確性兜住。希望這份實(shí)戰(zhàn)路線(xiàn)能幫你在自己的數(shù)據(jù)上少走幾趟彎路。本文還有配套的精品資源點(diǎn)擊獲取