據(jù)分析實(shí)戰(zhàn):從爬蟲(chóng)采集到回測(cè)驗(yàn)證全流程)
簡(jiǎn)介面向金融數(shù)據(jù)分析初學(xué)者與有一定基礎(chǔ)的Python開(kāi)發(fā)者這份資源以股票市場(chǎng)數(shù)據(jù)為實(shí)戰(zhàn)場(chǎng)景系統(tǒng)串聯(lián)起數(shù)據(jù)采集、清洗、分析與建模全流程。內(nèi)容涉及Python爬蟲(chóng)編寫(xiě)與反爬應(yīng)對(duì)、Pandas處理缺失值和格式統(tǒng)一、NumPy/SciPy進(jìn)行統(tǒng)計(jì)計(jì)算、Matplotlib/Seaborn繪制K線(xiàn)與箱型圖以及利用Scikit-learn構(gòu)建回歸、決策樹(shù)或隨機(jī)森林等預(yù)測(cè)模型并延伸至Tushare實(shí)時(shí)數(shù)據(jù)獲取與Flask部署等環(huán)節(jié)。資源包為7z壓縮格式整體僅4.47MB便于下載后按主題查閱。目前已有3654人學(xué)習(xí)適合希望快速上手股票大數(shù)據(jù)分析并用于課程設(shè)計(jì)或個(gè)人量化研究參考的讀者。1. 從數(shù)據(jù)到?jīng)Q策這份股票大數(shù)據(jù)分析Python實(shí)戰(zhàn)包能解決什么用Python分析股票數(shù)據(jù)卡住大多數(shù)人的不是算法而是第一步數(shù)據(jù)從哪來(lái)、怎么清洗、怎么才能保證算出來(lái)的指標(biāo)沒(méi)被復(fù)權(quán)除權(quán)搞亂。我拆過(guò)幾個(gè)模擬項(xiàng)目后發(fā)現(xiàn)一份能落地的“股票大數(shù)據(jù)分析Python實(shí)戰(zhàn)包”真正起作用的是其中那條從爬蟲(chóng)采集、數(shù)據(jù)清洗到策略驗(yàn)證的完整鏈路。這個(gè)資源包的核心就是把“python 爬蟲(chóng)”和“股票數(shù)據(jù)分析”焊在一起讓你不用從頭去翻接口文檔、不用反復(fù)調(diào)試數(shù)據(jù)缺失的坑跟著代碼走就能把日線(xiàn)數(shù)據(jù)拉下來(lái)、算成特征、跑出信號(hào)。適合手里有Python基礎(chǔ)但沒(méi)完整做過(guò)量化數(shù)據(jù)處理的從業(yè)者也適合想拿真實(shí)行情練手Pandas和爬蟲(chóng)的同學(xué)。它解決的不是“股票會(huì)不會(huì)漲”這種玄學(xué)問(wèn)題而是“行情數(shù)據(jù)到手之后怎么處理才能用來(lái)做決策”的工程問(wèn)題——這部分恰恰是很多教程跳過(guò)的環(huán)節(jié)。2. 環(huán)境與數(shù)據(jù)獲取先把可復(fù)現(xiàn)的數(shù)據(jù)管道搭起來(lái)2.1 建立隔離的Python分析環(huán)境拿到資源包后我習(xí)慣先做一件事絕不直接往全局環(huán)境里裝依賴(lài)。原因很簡(jiǎn)單股票數(shù)據(jù)分析涉及的庫(kù)版本沖突很常見(jiàn)——Pandas 2.x 和 1.x 的 API 差異、NumPy 版本不兼容都可能讓昨天的代碼今天跑出完全不同的結(jié)果。資源包里一般會(huì)帶 requirements.txt但在安裝之前先用虛擬環(huán)境把依賴(lài)隔離出來(lái)。python -m venv stock_venv source stock_venv/bin/activate # Windows 下為 stock_venv\Scripts\activate pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple邏輯說(shuō)明venv創(chuàng)建獨(dú)立解釋器環(huán)境source激活后pip安裝的包只屬于該項(xiàng)目。用清華鏡像安裝是為了避免默認(rèn)源在大陸訪(fǎng)問(wèn)緩慢的問(wèn)題實(shí)測(cè)速度能差好幾倍。參數(shù)說(shuō)明stock_venv是虛擬環(huán)境目錄名可以按項(xiàng)目改如果資源包沒(méi)有requirements.txt手動(dòng)安裝時(shí)建議先把pandas、numpy、requests、matplotlib這四件套裝好其他的按需逐步補(bǔ)。環(huán)境裝好后先跑一下資源包自帶的入口腳本。一般是一個(gè)main.py或run_analysis.py它做的事情是驗(yàn)證數(shù)據(jù)源連通性。這個(gè)步驟很關(guān)鍵——如果數(shù)據(jù)接口的 token 過(guò)期或網(wǎng)絡(luò)不通后面所有分析都是空轉(zhuǎn)。我一般會(huì)在這個(gè)階段先確認(rèn) Python 版本股票分析相關(guān)的庫(kù)對(duì) 3.8 以下支持并不好建議用 3.9 以上。2.2 用 Pandas 對(duì)接行情接口從 Token 到 DataFrame這個(gè)實(shí)戰(zhàn)包通常以某財(cái)經(jīng)數(shù)據(jù)平臺(tái)的開(kāi)放接口作為默認(rèn)數(shù)據(jù)源代碼風(fēng)格是requests或官方 SDK 封裝。我先講講接口對(duì)接里的關(guān)鍵token。數(shù)據(jù)接口通過(guò) token 識(shí)別身份如果資源包里帶了 token 變量看下是不是寫(xiě)死在某配置文件里。我一般習(xí)慣把 token 放到環(huán)境變量里避免代碼提交到公開(kāi)倉(cāng)庫(kù)時(shí)泄露。import os import pandas as pd from requests import Session TOKEN os.getenv(STOCK_API_TOKEN) sess Session() sess.headers.update({Authorization: fBearer {TOKEN}}) def fetch_daily(code, start_date, end_date): url https://api.example-stock-data.com/daily params { ts_code: code, start_date: start_date, end_date: end_date, fields: trade_date,open,high,low,close,vol } resp sess.get(url, paramsparams, timeout10) resp.raise_for_status() data resp.json() df pd.DataFrame(data[items], columnsdata[fields]) return df邏輯說(shuō)明這里用Session復(fù)用 TCP 連接避免每次請(qǐng)求都重新握手token 從環(huán)境變量讀取不硬編碼在代碼里。timeout10是請(qǐng)求超時(shí)秒數(shù)數(shù)據(jù)量大或網(wǎng)絡(luò)差時(shí)建議調(diào)到 20。參數(shù)說(shuō)明ts_code是股票代碼參數(shù)通常格式為000001.SZ或600000.SHfields字段列表按需取別一股腦拉全字段——接口響應(yīng)體越大解析越慢token 額度也浪費(fèi)得越快。拿到DataFrame后先做兩件小事看一眼dtypes確認(rèn)trade_date被解析成了datetime64而不是字符串再看一眼isna().sum()確認(rèn)沒(méi)有整列缺失。這兩步能省后面大量排錯(cuò)時(shí)間。原始數(shù)據(jù)格式通常是{items: [], fields: []}這種結(jié)構(gòu)資源包里一般已經(jīng)封裝好了解析函數(shù)但你要知道它內(nèi)部做了什么——我見(jiàn)過(guò)不少人拿到的列是字符串時(shí)間排序按字典序排結(jié)果圖全亂了。2.3 增量更新與歷史全量數(shù)據(jù)完整性的保障股票數(shù)據(jù)接口最麻煩的問(wèn)題不是拉一次而是“怎么持續(xù)更新”。比如一個(gè)模擬項(xiàng)目的目標(biāo)是分析最近三年的日線(xiàn)數(shù)據(jù)首次全量拉取可能要跑很久之后每天只需補(bǔ)充最新一天的數(shù)據(jù)。def update_daily(code, csv_pathdaily.csv): try: old pd.read_csv(csv_path, parse_dates[trade_date]) except FileNotFoundError: old pd.DataFrame() latest old[trade_date].max() if len(old) else None end pd.Timestamp.today().strftime(%Y%m%d) start (latest pd.Timedelta(days1)).strftime(%Y%m%d) if latest else 20200101 new fetch_daily(code, start, end) merged pd.concat([old, new], ignore_indexTrue).drop_duplicates(subset[trade_date]).sort_values(trade_date) merged.to_csv(csv_path, indexFalse) return merged邏輯說(shuō)明這一段先讀本地已有 CSV取最大日期作為增量更新的起點(diǎn)避免重復(fù)拉取。drop_duplicates按trade_date去重防止某次請(qǐng)求超時(shí)重試后產(chǎn)生重復(fù)行。參數(shù)說(shuō)明csv_path是本地緩存文件路徑如果你改用數(shù)據(jù)庫(kù)比如 SQLite邏輯完全一樣只是讀寫(xiě)那兩行換成語(yǔ)句。start用字符串拼接是因?yàn)榻涌谕ǔR骙YYYMMDD格式Pandas 的Timestamp不能直接傳。增量更新的好處不止是省流量——更重要的是保持本地?cái)?shù)據(jù)和接口狀態(tài)一致。如果某天接口的數(shù)據(jù)源本身出了問(wèn)題比如某只股票停牌當(dāng)天沒(méi)返回記錄增量邏輯不會(huì)主動(dòng)發(fā)現(xiàn)。所以我一般每周做一次全量校驗(yàn)把本地最后一行的trade_date和接口最新的trade_date對(duì)比差超過(guò)一個(gè)交易日就觸發(fā)重新拉取。資源包里這個(gè)邏輯可能寫(xiě)得比較簡(jiǎn)單建議拿到后按實(shí)際需求補(bǔ)強(qiáng)。3. 爬蟲(chóng)實(shí)戰(zhàn)把請(qǐng)求頻率、斷點(diǎn)續(xù)爬和字段抽取做成工程3.1 為什么直接爬網(wǎng)頁(yè)通常不是好選擇“python 爬蟲(chóng)”這個(gè)詞容易讓人想到爬網(wǎng)頁(yè) HTML 然后解析表格但在股票數(shù)據(jù)場(chǎng)景里直接爬網(wǎng)頁(yè)是最差的選擇原因有兩點(diǎn)。第一財(cái)經(jīng)網(wǎng)站的頁(yè)面結(jié)構(gòu)變動(dòng)頻繁今天能解析的表格明天可能就換了 CSS 類(lèi)名維護(hù)成本極高。第二大量股票數(shù)據(jù)頁(yè)面是通過(guò) JavaScript 異步加載的直接requests.get拿到的 HTML 里根本沒(méi)有數(shù)據(jù)要么用 Selenium 這種重型工具要么破解 XHR 接口地址兩個(gè)方向都是血淚教訓(xùn)。但這個(gè)資源包里一般不會(huì)只用官方接口因?yàn)橛行└哳l因子和盤(pán)中數(shù)據(jù)接口不給普通用戶(hù)開(kāi)放。這時(shí)候就要寫(xiě)爬蟲(chóng)去抓公開(kāi)的網(wǎng)頁(yè)接口——注意是抓 JSON 接口不是抓 HTML。常見(jiàn)做法是用開(kāi)發(fā)者工具打開(kāi)任意財(cái)經(jīng)網(wǎng)站的數(shù)據(jù)頁(yè)在“網(wǎng)絡(luò)”標(biāo)簽里過(guò)濾XHR請(qǐng)求找到真正返回?cái)?shù)據(jù)的那條鏈接。它通常是個(gè)帶一堆參數(shù)的 GET 請(qǐng)求返回的是 JSON解析起來(lái)比 HTML 安穩(wěn)得多。3.2 用 Requests Session 封裝一個(gè)行情讀取器抓 JSON 接口的爬蟲(chóng)核心就三件事帶好 Headers、控制頻率、做好重試。我自己封裝的時(shí)候是這么寫(xiě)的import time import random from requests import Session class MarketReader: def __init__(self, base_url, min_interval1.0): self.sess Session() self.sess.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64), Referer: https://example-finance-site.com, Accept: application/json, text/plain, */* }) self.base_url base_url self.min_interval min_interval self._last_req 0 def get_json(self, path, params): now time.time() wait self._last_req self.min_interval - now if wait 0: time.sleep(wait random.uniform(0.1, 0.5)) resp self.sess.get(f{self.base_url}{path}, paramsparams, timeout15) self._last_req time.time() resp.raise_for_status() return resp.json() def fetch_kline(self, symbol, perioddaily): data self.get_json(/api/kline, { symbol: symbol, period: period, limit: 1000 }) records data.get(data, {}).get(items, []) return [{date: r[0], close: r[2], volume: r[5]} for r in records]邏輯說(shuō)明min_interval用來(lái)控制兩次請(qǐng)求的最小間隔默認(rèn) 1 秒對(duì)多數(shù)公開(kāi)接口都?jí)蛴?。random.uniform加一個(gè)隨機(jī)抖動(dòng)避免請(qǐng)求節(jié)奏太規(guī)律被限流。_last_req記錄上一次請(qǐng)求時(shí)間用于計(jì)算是否需要等待。參數(shù)說(shuō)明這里的period支持daily和weekly接口不同參數(shù)名可能不同具體要看抓到的 XHR 請(qǐng)求定義。很多翻車(chē)的爬蟲(chóng)都是因?yàn)闆](méi)帶Referer或者UA太假被拒絕。這個(gè)封裝里 UA 用的是 Chrome 的標(biāo)準(zhǔn)格式Referer 指向頁(yè)面地址——這兩個(gè)缺失或不對(duì)接口大概率返回 403。帶上之后通常能順利拿到 JSON。實(shí)際運(yùn)行時(shí)建議把timeout設(shè)大一點(diǎn)有些接口在拉長(zhǎng)周期數(shù)據(jù)時(shí)服務(wù)端要算很久15 秒比較穩(wěn)妥。3.3 多線(xiàn)程拉取與斷點(diǎn)續(xù)爬應(yīng)對(duì)大范圍歷史數(shù)據(jù)單線(xiàn)程拉全量歷史數(shù)據(jù)很慢。比如你要拉滬深兩市共 5000 多只股票的周線(xiàn)數(shù)據(jù)單線(xiàn)程按 1 秒 1 次算要跑一個(gè)多小時(shí)而且中間斷一次就前功盡棄。資源包里一般會(huì)帶多線(xiàn)程方案但我要提醒一句多線(xiàn)程不是越多越好接口限流是按 token 或 IP 算的開(kāi) 20 個(gè)線(xiàn)程如果被限流反而比單線(xiàn)程更慢。我一般把線(xiàn)程數(shù)壓在 5 到 8 個(gè)同時(shí)加一個(gè)進(jìn)度記錄文件做到“斷點(diǎn)續(xù)爬”。import threading import os import json from queue import Queue def worker(q, results, lock): while not q.empty(): try: symbol q.get(timeout1) data reader.fetch_kline(symbol, perioddaily) with lock: results[symbol] data done_list.append(symbol) save_progress(done_list) except Exception as e: print(ffailed: {symbol}: {e}) finally: q.task_done() def save_progress(done_list): with open(progress.json, w) as f: json.dump(done_list, f) q Queue() all_symbols load_symbols(symbols.txt) # 每行一個(gè)股票代碼 for s in all_symbols: q.put(s) done_list load_progress() if os.path.exists(progress.json) else [] results {} lock threading.Lock() for _ in range(6): t threading.Thread(targetworker, args(q, results, lock)) t.start() q.join()邏輯說(shuō)明Queue是線(xiàn)程安全的多個(gè) worker 從隊(duì)列里取任務(wù)不會(huì)重復(fù)處理。lock保護(hù)results字典和progress.json的寫(xiě)入避免多線(xiàn)程同時(shí)寫(xiě)文件導(dǎo)致 JSON 損壞。save_progress每完成一個(gè)股票就記錄一次程序崩了之后重新運(yùn)行時(shí)從done_list里剔除已完成項(xiàng)即可。參數(shù)說(shuō)明symbols.txt是股票代碼清單每行一個(gè)如果資源包自帶清單更好沒(méi)有的話(huà)可以用akshare或接口的stock_basic接口拉取。這里有一個(gè)重要的點(diǎn)斷點(diǎn)續(xù)爬的前提是“寫(xiě)盤(pán)夠快且夠頻繁”。save_progress每次完整重寫(xiě) JSON 文件在 5000 只股票的場(chǎng)景下文件不超過(guò) 100KB開(kāi)銷(xiāo)可以忽略。如果數(shù)據(jù)量更大建議改成 SQLite 記錄進(jìn)度。多線(xiàn)程踩坑的重點(diǎn)是千萬(wàn)別讓兩個(gè)線(xiàn)程同時(shí)寫(xiě)同一個(gè)文件lock就是干這個(gè)用的。另外q.empty()判斷在并發(fā)下不完全可靠但任務(wù)分發(fā)完成后消費(fèi)者不會(huì)再往里放任務(wù)所以這里的場(chǎng)景下夠用。4. 因子計(jì)算與預(yù)處理讓原始行情變成可分析的特征4.1 復(fù)權(quán)因子處理避免算出假信號(hào)原始行情里的close是未復(fù)權(quán)價(jià)遇到分紅送股價(jià)格會(huì)出現(xiàn)斷崖式跳空。如果沒(méi)有做復(fù)權(quán)處理任何基于價(jià)格的指標(biāo)都會(huì)失真。最常見(jiàn)的坑是某只股票 10 送 10除權(quán)日價(jià)格從 20 元變成 10 元均線(xiàn)系統(tǒng)會(huì)“檢測(cè)”出一個(gè)巨大下跌信號(hào)——但實(shí)際上這只是權(quán)益變動(dòng)不是市場(chǎng)下跌。資源包里的數(shù)據(jù)如果來(lái)自統(tǒng)一接口一般會(huì)提供adj_factor字段或qfq前復(fù)權(quán)選項(xiàng)。前復(fù)權(quán)會(huì)把歷史價(jià)格按最新股本調(diào)整保證歷史走勢(shì)連續(xù)。處理邏輯是把原始價(jià)格乘上復(fù)權(quán)因子再除以最新因子值或者直接用接口的adj參數(shù)。我拿到數(shù)據(jù)后會(huì)先做一個(gè)“除權(quán)跳空檢測(cè)”def detect_gap(df, threshold0.2): df df.sort_values(trade_date) pct df[close].pct_change() gap_days df[pct.abs() threshold].index.tolist() return gap_days邏輯說(shuō)明pct_change計(jì)算相鄰交易日的漲跌幅閾值為 20%。如果某天漲跌幅絕對(duì)值超過(guò) 20%注冊(cè)制股票除外幾乎可以斷定是未復(fù)權(quán)數(shù)據(jù)里的除權(quán)跳空。注意這個(gè)檢測(cè)只用于輔助判斷數(shù)據(jù)質(zhì)量不能替代正式的復(fù)權(quán)因子處理。參數(shù)說(shuō)明threshold按市場(chǎng)調(diào)整A 股普通股票設(shè) 0.2 夠用科創(chuàng)板/創(chuàng)業(yè)板個(gè)股日內(nèi)波動(dòng)大可以調(diào)到 0.3但再高就可能漏檢。4.2 高頻與低頻數(shù)據(jù)的重采樣對(duì)齊股票分析中經(jīng)常遇到不同頻率的數(shù)據(jù)要合并——比如把日線(xiàn)數(shù)據(jù)和分鐘線(xiàn)數(shù)據(jù)對(duì)齊到周線(xiàn)。Pandas 的resample能搞定但有個(gè)隱含陷阱交易日的“日”和自然日的“日”不是一回事。如果用resample(W)按自然周聚合法定節(jié)假日會(huì)導(dǎo)致某些周只有 2 個(gè)交易日周均線(xiàn)跟交易所口徑對(duì)不上。正確做法是先用交易日歷生成有效交易日的DatetimeIndex再對(duì)齊import pandas as pd def resample_trading(df, ruleW-FRI): df df.set_index(trade_date) df df.resample(rule).agg({ open: first, high: max, low: min, close: last, vol: sum }).dropna() return df邏輯說(shuō)明W-FRI表示按周聚合錨定在周五一周內(nèi)的交易日數(shù)據(jù)合并到周五這個(gè)索引上。open取周內(nèi)第一個(gè)交易日的開(kāi)盤(pán)價(jià)close取最后一個(gè)的收盤(pán)價(jià)high取周內(nèi)最高low取最低vol求和。dropna去掉沒(méi)有交易數(shù)據(jù)的自然周。參數(shù)說(shuō)明如果數(shù)據(jù)源沒(méi)有交易日歷字段可以用exchange_calendars庫(kù)生成或者直接用df.index里去重后的日期做后續(xù)匹配。這里容易翻車(chē)的點(diǎn)是resample默認(rèn)是按日歷日頻率遇到周末和節(jié)假日會(huì)創(chuàng)建空數(shù)據(jù)行dropna才能清掉。但注意agg字典里的first和last在空數(shù)據(jù)上會(huì)返回NaN不要用fillna去填否則會(huì)把不存在的交易周也算進(jìn)去。4.3 構(gòu)造常用技術(shù)指標(biāo)的 Pandas 實(shí)現(xiàn)資源包最實(shí)用的一部分就是指標(biāo)計(jì)算代碼。均線(xiàn)、RSI、MACD 這類(lèi)常用指標(biāo)的 Pandas 實(shí)現(xiàn)并不復(fù)雜難在邊界條件。我以 RSI 為例——它經(jīng)常被當(dāng)作買(mǎi)賣(mài)信號(hào)但不同平臺(tái)的計(jì)算差異導(dǎo)致結(jié)果不同def compute_rsi(close, period14): delta close.diff() gain delta.clip(lower0) loss -delta.clip(upper0) avg_gain gain.rolling(windowperiod, min_periodsperiod).mean() avg_loss loss.rolling(windowperiod, min_periodsperiod).mean() rs avg_gain / avg_loss rsi 100 - (100 / (1 rs)) return rsi邏輯說(shuō)明diff計(jì)算相鄰收盤(pán)價(jià)差clip(lower0)把負(fù)值歸零得到上漲幅度clip(upper0)取負(fù)并取絕對(duì)值得到下跌幅度。rolling(windowperiod, min_periodsperiod)要求窗口內(nèi)至少 14 個(gè)有效值前 13 個(gè)交易日輸出NaN。參數(shù)說(shuō)明period是回看天數(shù)默認(rèn) 14 是經(jīng)典參數(shù)有的平臺(tái)用 SMMA平滑移動(dòng)平均而非簡(jiǎn)單平均算avg_gain這是不同平臺(tái) RSI 數(shù)值不一致的主要原因。如果做跨平臺(tái)策略校驗(yàn)務(wù)必確認(rèn)對(duì)方用的是哪種平滑方式。這里有一個(gè)很玄學(xué)的點(diǎn)RSI 的公式里avg_loss如果為 0連續(xù)上漲rs會(huì)變成無(wú)窮大rsi會(huì)返回NaN。資源包里如果沒(méi)處理這個(gè)邊界回測(cè)時(shí)就會(huì)出現(xiàn)信號(hào)丟失。修正方法很簡(jiǎn)單rs計(jì)算前先把a(bǔ)vg_loss為 0 的行替換為極大值。我經(jīng)手過(guò)的模擬項(xiàng)目X里這個(gè)坑導(dǎo)致周末回測(cè)結(jié)果和實(shí)盤(pán)完全對(duì)不上查了半天才發(fā)現(xiàn)是 RSI 計(jì)算在極端行情下靜默失敗。拿到資源包后建議先跑一遍內(nèi)置的自檢函數(shù)確認(rèn)指標(biāo)邊界條件都處理過(guò)。5. 常見(jiàn)問(wèn)題與排查這五類(lèi)錯(cuò)我?guī)缀趺看味紩?huì)踩一遍5.1 現(xiàn)象回測(cè)結(jié)果里出現(xiàn)巨額收益但實(shí)際走勢(shì)沒(méi)有原因數(shù)據(jù)里包含了未復(fù)權(quán)價(jià)格或者復(fù)權(quán)因子計(jì)算錯(cuò)誤。某只股票經(jīng)歷多次分紅后未復(fù)權(quán)價(jià)格長(zhǎng)期看起來(lái)“陰跌不止”但策略按價(jià)格差算收益時(shí)會(huì)出現(xiàn)一個(gè)假的大額盈利。解決拿到數(shù)據(jù)后先做除權(quán)跳空檢測(cè)見(jiàn) 4.1再對(duì)應(yīng)復(fù)權(quán)因子檢查。資源包如果帶了復(fù)權(quán)接口直接調(diào)用接口的adj參數(shù)拉前復(fù)權(quán)數(shù)據(jù)不要自己在本地做除法——除數(shù)用錯(cuò)一步全錯(cuò)。5.2 現(xiàn)象K 線(xiàn)圖的日期明明是周一卻顯示的是周五的數(shù)據(jù)原因數(shù)據(jù)的時(shí)區(qū)是 UTC世界標(biāo)準(zhǔn)時(shí)間或者美東時(shí)間而本地日期是北京時(shí)間。某些國(guó)際數(shù)據(jù)接口返回的trade_date帶時(shí)區(qū)偏移to_pandas后沒(méi)做tz_convert導(dǎo)致日期整體偏移。特別是凌晨時(shí)段的數(shù)據(jù)UTC 日期比北京時(shí)間早 8 小時(shí)周末和交易日的對(duì)齊全亂。解決拉數(shù)據(jù)后統(tǒng)一執(zhí)行df[trade_date] df[trade_date].dt.tz_convert(Asia/Shanghai)或者在讀取時(shí)指定parse_dates并傳入utcTrue。如果接口返回的是無(wú)時(shí)區(qū)字符串但實(shí)際代表北京時(shí)間直接解析即可千萬(wàn)別畫(huà)蛇添足。5.3 現(xiàn)象爬蟲(chóng)跑到一半開(kāi)始大量返回 403 或 HTTP 429原因請(qǐng)求頻率太快或者 UA/Referer 被目標(biāo)服務(wù)器識(shí)別為“非正常訪(fǎng)客”。很多公開(kāi)接口的限流策略是按 IPUA 組合計(jì)算的即使你單線(xiàn)程運(yùn)行如果請(qǐng)求間隔小于 0.5 秒也可能觸發(fā)封禁。解決把min_interval調(diào)到 1 秒以上UA 換成真實(shí)的 Chrome UA 字符串Referer 設(shè)為頁(yè)面地址。如果已經(jīng)被封等 10 分鐘到半小時(shí)再試不要立刻重啟爬蟲(chóng)——那只會(huì)讓封禁時(shí)間延長(zhǎng)。我在模擬項(xiàng)目X中就是沒(méi)注意_last_req的初始值為 0導(dǎo)致第一次請(qǐng)求前沒(méi)有等待幾百次請(qǐng)求后直接被斷連。5.4 現(xiàn)象回測(cè)優(yōu)化的策略在實(shí)盤(pán)里完全失效原因前視偏差。最常見(jiàn)的是用當(dāng)天的收盤(pán)價(jià)算出信號(hào)然后當(dāng)天就成交——但真實(shí)情況下你得等收盤(pán)后才能知道當(dāng)天的收盤(pán)價(jià)交易指令最早也只能在次日開(kāi)盤(pán)執(zhí)行。數(shù)據(jù)在時(shí)間軸上“偷看”了未來(lái)信息回測(cè)自然好看實(shí)盤(pán)自然翻車(chē)。解決信號(hào)生成后必須shift(1)讓信號(hào)延遲一天生效或者嚴(yán)格按“次日開(kāi)盤(pán)價(jià)”成交。資源包如果自帶回測(cè)引擎先確認(rèn)它的成交價(jià)格是不是用了close.shift(1)。這個(gè)是最隱蔽的坑很多初學(xué)者完全意識(shí)不到。5.5 現(xiàn)象兩次運(yùn)行同一段代碼結(jié)果出現(xiàn)細(xì)微差異原因浮點(diǎn)精度。Pandas 的rolling.mean()用累加方式計(jì)算大數(shù)據(jù)量下會(huì)出現(xiàn)微小的精度差異另外resample聚合時(shí)多線(xiàn)程或并行模式下可能有誤差。解決做關(guān)鍵價(jià)格比較時(shí)用round(..., 6)或者用np.isclose做比較判斷。數(shù)據(jù)量在幾萬(wàn)行時(shí)這個(gè)誤差可能只有 1e-10但累積到信號(hào)判斷的閾值邊緣就可能造成一次交易的開(kāi)與不開(kāi)。不要相信“浮點(diǎn)誤差是小事”在策略回測(cè)里這就是大事。6. 從靜態(tài)分析到簡(jiǎn)單回測(cè)驗(yàn)證你的股票策略是否真的可靠6.1 用向量化回測(cè)快速驗(yàn)證 MA 均線(xiàn)策略資源包加載了干凈數(shù)據(jù)、算好了指標(biāo)之后最值得做的一件事就是跑一個(gè)最小可用的回測(cè)驗(yàn)證數(shù)據(jù)的時(shí)序邏輯是否自洽。我常用雙均線(xiàn)策略做“冒煙測(cè)試”——如果連這個(gè)策略都跑不出合理結(jié)果那數(shù)據(jù)或指標(biāo)一定有問(wèn)題。def backtest_ma(df, short5, long20): df df.copy().sort_values(trade_date) df[ma_short] df[close].rolling(short).mean() df[ma_long] df[close].rolling(long).mean() df[signal] (df[ma_short] df[ma_long]).astype(int) df[position] df[signal].shift(1) # 次日開(kāi)盤(pán)執(zhí)行 df[ret] df[close].pct_change() df[strategy_ret] df[position] * df[ret] df[cum_ret] (1 df[strategy_ret]).cumprod() - 1 return df邏輯說(shuō)明signal是當(dāng)日收盤(pán)后的判斷position用shift(1)延遲到次日生效避免前視偏差。strategy_ret是持倉(cāng)收益乘以當(dāng)日漲跌幅cumprod累積得到凈值曲線(xiàn)。參數(shù)說(shuō)明short和long分別是短、長(zhǎng)均線(xiàn)窗口5和20是經(jīng)典參數(shù)。注意這里沒(méi)有考慮手續(xù)費(fèi)和漲跌停限制結(jié)果偏樂(lè)觀(guān)但作為冒煙測(cè)試足夠。跑完看兩件事第一回測(cè)期末的累計(jì)凈值是不是在合理區(qū)間——如果翻了 100 倍得懷疑數(shù)據(jù)或邏輯第二position序列有沒(méi)有頻繁切換——一段趨勢(shì)行情里切了十幾次多半是數(shù)據(jù)有重復(fù)行或時(shí)間戳混亂。這兩點(diǎn)過(guò)了才放心做更復(fù)雜的策略驗(yàn)證。6.2 用回測(cè)結(jié)果反推數(shù)據(jù)質(zhì)量回測(cè)里經(jīng)常會(huì)發(fā)現(xiàn)隱藏的數(shù)據(jù)質(zhì)量問(wèn)題。比如某天close是 0pct_change會(huì)變成無(wú)窮大導(dǎo)致整條累積凈值線(xiàn)直接斷裂比如停牌日的價(jià)格沒(méi)有NaN而是重復(fù)了上一交易日導(dǎo)致rolling(5)計(jì)算出看似正常的均線(xiàn)但實(shí)際少了一天。我一般會(huì)在回測(cè)前加一個(gè)數(shù)據(jù)質(zhì)量斷言assert df[close].isna().sum() 0, 存在空收盤(pán)價(jià) assert (df[close] 0).sum() 0, 存在非正收盤(pán)價(jià) assert df[vol].dtype in (int64, float64), 成交量類(lèi)型異常這三條斷言能把大部分臟數(shù)據(jù)擋在回測(cè)之前。資源包如果自帶了內(nèi)置檢查雙擊運(yùn)行一下即可如果沒(méi)有建議自己加上成本極低收益極高。做完這個(gè)再跑策略心里踏實(shí)得多。6.3 可視化呈現(xiàn)把信號(hào)與凈值曲線(xiàn)畫(huà)出來(lái)回測(cè)結(jié)果最終要看圖才能定位問(wèn)題。我習(xí)慣把收盤(pán)價(jià)、均線(xiàn)、買(mǎi)賣(mài)信號(hào)點(diǎn)和凈值曲線(xiàn)畫(huà)在一張圖上一眼就能看出信號(hào)是否出現(xiàn)在合理的位置——如果買(mǎi)點(diǎn)全出現(xiàn)在大漲之后而不是啟動(dòng)之初說(shuō)明信號(hào)邏輯有未來(lái)函數(shù)或滯后問(wèn)題。import matplotlib.pyplot as plt def plot_strategy(df): fig, ax plt.subplots(2, 1, figsize(12, 8), sharexTrue) ax[0].plot(df[trade_date], df[close], labelClose, lw1) ax[0].plot(df[trade_date], df[ma_short], labelfMA{5}, lw1) ax[0].plot(df[trade_date], df[ma_long], labelfMA{20}, lw1) buy_idx df[df[signal].diff() 1].index sell_idx df[df[signal].diff() -1].index ax[0].scatter(df.loc[buy_idx, trade_date], df.loc[buy_idx, close], marker^, colorr, s50) ax[0].scatter(df.loc[sell_idx, trade_date], df.loc[sell_idx, close], markerv, colorg, s50) ax[0].legend() ax[1].plot(df[trade_date], df[cum_ret], labelStrategy Return, lw1) ax[1].legend() plt.tight_layout() plt.show()邏輯說(shuō)明signal.diff() 1表示信號(hào)從 0 變成 1 的轉(zhuǎn)折點(diǎn)也就是買(mǎi)入信號(hào)diff() -1是賣(mài)出信號(hào)。從信號(hào)點(diǎn)和價(jià)格的位置關(guān)系能直觀(guān)判斷是否有滯后或超前。參數(shù)說(shuō)明figsize控制圖幅buy_idx和sell_idx分別存儲(chǔ)買(mǎi)賣(mài)點(diǎn)索引。如果信號(hào)點(diǎn)全部出現(xiàn)在均線(xiàn)交叉后的第 N 根 K 線(xiàn)說(shuō)明shift層級(jí)可能不對(duì)?;販y(cè)不是終點(diǎn)而是數(shù)據(jù)質(zhì)量的試金石。從那以后我每次拿到新的數(shù)據(jù)資源都強(qiáng)制走一遍“跳空檢測(cè) → 時(shí)區(qū)對(duì)齊 → 冒煙策略 → 畫(huà)圖目測(cè)”這個(gè)流程不跑完這一步就不碰后續(xù)的任何高級(jí)模型。這套流程幫我擋下了至少三次因?yàn)閿?shù)據(jù)沒(méi)復(fù)權(quán)導(dǎo)致的假信號(hào)也讓我少走了很多彎路。希望這份實(shí)戰(zhàn)筆記里提到的坑和參數(shù)能讓你在跑自己的股票數(shù)據(jù)分析項(xiàng)目時(shí)更順一點(diǎn)。本文還有配套的精品資源點(diǎn)擊獲取