據(jù)獲取實戰(zhàn):實時與歷史API選型、復權處理及量化回測落地指南)
先把話放在前面做量化、做數(shù)據(jù)分析或者只是想把自己關注的一籃子A股做成自動盯盤小工具的朋友十有八九都會在行情數(shù)據(jù)獲取這一步卡住。我在做A股實時行情和歷史行情抓取的時候最開始也是到處翻網(wǎng)頁、找接口、看論壇帖子后來才慢慢理清頭條路實時行情靠什么API、歷史K線去哪拉、分鐘數(shù)據(jù)怎么落地、回測和初篩怎么用。這篇就把我折騰出來的整套方案和踩過的坑一起寫出來給正在跟行情API較勁的你做個參考。我不打算只貼幾個代碼片段就完事因為這類接口最大的問題從來不是“能不能調(diào)”而是“能不能穩(wěn)定調(diào)、調(diào)完能不能直接用”。比如數(shù)據(jù)缺字段、復權口徑不對、分鐘線缺失、盤中數(shù)據(jù)和收盤數(shù)據(jù)不一致這些小問題放到回測里都會變成大事故。下面我會從方案選型、環(huán)境準備、實時接口、歷史接口到常見問題和實戰(zhàn)場景整條鏈路拆開講一遍。1. 項目概述與方案選型先想清楚你要什么數(shù)據(jù)1.1 核心需求拆解實時行情和歷史行情其實是兩套事很多人一開始說“我要A股行情API”但真正落地的需求往往是分層的。我習慣把它拆成四類數(shù)據(jù)類型典型用途更新頻率典型來源實時快照盯盤、自選股監(jiān)控、盤中預警秒級到分鐘級新浪/騰訊公開接口、Tushare快照分時/逐筆數(shù)據(jù)盤口分析、短周期回測秒級數(shù)據(jù)商付費接口為主歷史日線選股、策略回測、業(yè)績統(tǒng)計每日收盤后更新Tushare、Baostock、AKShare歷史分鐘線日內(nèi)策略、精細化回測長時間跨度難拿Tushare Pro、AKShare、Baostock這個拆分很重要。因為實時行情和歷史行情的獲取難度、接口形態(tài)、數(shù)據(jù)成本完全不一樣。實時行情講究的是低延遲和持續(xù)輪詢歷史行情講究的是覆蓋度、復權準確性和存儲方式。如果你用抓實時行情的思路去拉歷史數(shù)據(jù)或者反過來用拉歷史數(shù)據(jù)的方式做實時盯盤都會很別扭。1.2 數(shù)據(jù)源選型免費接口、開放平臺和公開HTTP接口怎么挑我實際用過并且現(xiàn)在還在用的主要有四個方向各有各的脾氣。這里直接給結論性的對比數(shù)據(jù)源是否免費Token/認證歷史日線分鐘線穩(wěn)定程度Tushare Pro部分免費需要Token和積分很全需要高積分高Baostock免費無需Token全5、15、30、60分鐘高但接口風格老AKShare免費開源無需Token全1、5、15、30、60分鐘中上游頁面一變就掛新浪/騰訊公開HTTP接口免費無不提供長期歷史不提供適合實時快照如果你只是想快速驗證一個策略或者做學習Demo我建議先用AKShare一行pip install就能拿到大量數(shù)據(jù)代碼寫起來也順手。如果你打算把數(shù)據(jù)落地、長期維護、跑正經(jīng)回測那Tushare Pro和Baostock會更穩(wěn)。實時盤中監(jiān)控我目前的主力是新浪和騰訊的公開HTTP接口響應速度快字段也夠用。這里有一個我特別想強調(diào)的判斷不要為了省事去網(wǎng)上找那些“打包好的歷史數(shù)據(jù)下載包”。我踩過這個坑——網(wǎng)上流傳的所謂A股歷史分鐘數(shù)據(jù)打包下載下來之后要么字段缺失要么復權口徑說不清楚最要命的是你根本不知道它最后更新時間是哪一天。自己拉數(shù)據(jù)落庫雖然前期麻煩但后續(xù)的穩(wěn)定性和可追溯性完全值得。1.3 選型背后的“為什么”為什么不是只推薦一個數(shù)據(jù)源因為沒有任何一個免費渠道能同時包辦實時和歷史兩類需求。Tushare的分鐘線和財務數(shù)據(jù)很全但它的實時快照能力一般而且部分接口需要積分AkShare覆蓋廣但它的本質(zhì)是把公開頁面結構化上游接口一旦調(diào)整你的腳本可能第二天就報錯Baostock勝在接口穩(wěn)定但只覆蓋K線和基礎數(shù)據(jù)衍生指標基本都要自己算。所以我的長期方案是實時快照走新浪/騰訊公開HTTP接口歷史日線和分鐘線走Tushare和Baostock雙備份AKShare作為臨時補數(shù)工具。不是哪個最好而是每個渠道都有自己的強項組合起來才最穩(wěn)。這種多數(shù)據(jù)源冗余的做法在真實項目里能幫你省掉很多半夜跑批時間。2. 環(huán)境準備與實時行情API的接入細節(jié)2.1 環(huán)境準備和基礎依賴安裝無論你是Windows、macOS還是Linux先把Python環(huán)境弄干凈。我推薦用Python 3.8以上單獨的虛擬環(huán)境避免把系統(tǒng)Python搞亂。python -m venv market_env source market_env/bin/activate # Windows下用 market_env\Scripts\activate pip install pandas requests akshare tushare baostock有些接口返回的數(shù)據(jù)是GBK編碼如果你在Windows下跑建議順便把編碼相關的習慣養(yǎng)成拿到字節(jié)先判斷編碼再解碼成字符串。下面所有示例我都默認在UTF-8環(huán)境下運行但如果遇到中文亂碼優(yōu)先檢查requests返回的response.encoding。2.2 通過新浪公開HTTP接口獲取實時行情快照先來一個最直接的方案新浪的行情快照接口。它的URL格式很簡單把股票代碼拆成市場和代碼兩部分拼接。規(guī)則我整理了一下滬市6開頭、科創(chuàng)板688開頭用sh深市0和3開頭、創(chuàng)業(yè)板300開頭用sz指數(shù)類用對應的sh000001、sz399001這種方式。import requests def get_sina_quote(codes): code_str ,.join(codes) url fhttps://hq.sinajs.cn/list{code_str} headers { Referer: https://finance.sina.com.cn, User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) } resp requests.get(url, headersheaders, timeout5) resp.encoding gbk return resp.text lines get_sina_quote([sh600000, sz000001, sz399001]) print(lines)這里有兩個細節(jié)必須提醒第一新浪接口直接裸請求會返回403需要在請求頭里帶上正常的瀏覽器Referer否則服務端會拒絕響應。第二返回內(nèi)容默認是GBK編碼如果你不指定resp.encoding gbk長字符串里拆出來的第一個字段“股票名稱”十有八九是亂碼。2.3 騰訊公開接口作為備選方案新浪接口偶爾會出現(xiàn)空值或者斷連這時我一般切到騰訊的行情接口作為備選。它的地址是https://qt.gtimg.cn/q代碼參數(shù)格式保持一致只是返回字段順序跟新浪不太一樣。import requests def get_tencent_quote(codes): code_str ,.join(codes) url fhttps://qt.gtimg.cn/q{code_str} resp requests.get(url, timeout5) resp.encoding gbk return resp.text騰訊接口返回的字段比新浪更豐富除了最新價、漲跌幅、成交量之外還有買賣五檔、成交筆數(shù)、市凈率這些。我自己的經(jīng)驗是新浪和騰訊都會出現(xiàn)偶發(fā)超時比較好的做法是寫一個簡單的故障切換先請求新浪失敗或返回空就用騰訊間隔500毫秒重試一次。不要在同一秒內(nèi)瘋狂請求同一個接口公開接口不是專門給你家程序用的頻率太高很容易被臨時封禁。2.4 用Tushare獲取當日交易數(shù)據(jù)如果你已經(jīng)注冊了Tushare Pro也可以用它拿當日交易數(shù)據(jù)。Tushare的優(yōu)勢是數(shù)據(jù)結構規(guī)范字段都有官方說明不會像新浪騰訊那種用逗號分隔的字符串還得自己數(shù)下標。下面是拉取某一交易日全市場日線數(shù)據(jù)的示例import tushare as ts ts.set_token(你的token) pro ts.pro_api() df pro.daily(trade_date20240115) print(df.head())這個接口返回的字段包含ts_code、trade_date、open、high、low、close、pre_close、change、pct_chg、volume、amount字段命名規(guī)范清晰適合做量化入庫。需要注意這里的volume單位是手amount單位是千元如果后面要自己算成交額記得先做單位換算。Tushare的積分體系決定了你能調(diào)用哪些接口普通注冊用戶拉基礎日線沒問題但分鐘線、財務數(shù)據(jù)這類高級接口需要更多積分。2.5 實時輪詢的頻率控制和多標的數(shù)據(jù)組裝實時行情沒有免費推送基本靠輪詢。輪詢頻率怎么定我實踐下來的一個平衡點自選股數(shù)量在30只以內(nèi)每5秒輪詢一次沒問題股票數(shù)量超過50只建議放寬到10秒以上。頻繁輪詢不只是浪費帶寬更關鍵的是容易被服務端限流導致拿回來的數(shù)據(jù)出現(xiàn)整段缺失。import time import pandas as pd codes [sh600000, sz000001, sz300750, sh688981] all_rows [] while True: raw get_sina_quote(codes) for line in raw.strip().split(\n): parts line.split()[1].strip().split(,) if len(parts) 32: continue all_rows.append({ code: line.split()[0].replace(hq_str_, ), name: parts[0], open: float(parts[1]), pre_close: float(parts[2]), price: float(parts[3]), high: float(parts[4]), low: float(parts[5]), volume: float(parts[8]), amount: float(parts[9]), timestamp: time.strftime(%Y-%m-%d %H:%M:%S) }) df pd.DataFrame(all_rows) print(df) time.sleep(5)這里有一個單位細節(jié)新浪返回的volume單位是股amount單位是元跟Tushare的手和千元不同。你如果是多數(shù)據(jù)源混用一定要在入庫前統(tǒng)一單位我建議統(tǒng)一成“股”和“元”因為這是大部分策略庫默認的數(shù)據(jù)口徑。3. 歷史行情API的獲取、清洗與存儲3.1 日線歷史數(shù)據(jù)三條路徑歷史日線是選股和回測的基礎。我先給三個數(shù)據(jù)源的調(diào)用示例直接可跑。Tushare版本import tushare as ts pro ts.pro_api() df pro.daily(ts_code600000.SH, start_date20200101, end_date20231231) df df.sort_values(trade_date).reset_index(dropTrue) print(df.head())Baostock版本import baostock as bs import pandas as pd bs.login() rs bs.query_history_k_data_plus( sh.600000, date,code,open,high,low,close,volume,amount, start_date2020-01-01, end_date2023-12-31, frequencyd, adjustflag2 ) rows [] while rs.error_code 0 and rs.next(): rows.append(rs.get_row_data()) bs.logout() df pd.DataFrame(rows, columnsrs.fields) print(df.head())AKShare版本import akshare as ak df ak.stock_zh_a_hist( symbol600000, perioddaily, start_date20200101, end_date20231231, adjustqfq ) print(df.head())三種方式里Tushare的ts_code要用“600000.SH”這種帶交易所后綴的格式Baostock要用“sh.600000”格式AKShare只要6位數(shù)字代碼就行。我最早就是在這上面反復報錯其實不是代碼邏輯問題純粹是代碼格式不同。3.2 分鐘級數(shù)據(jù)怎么拉才劃算分鐘級數(shù)據(jù)的獲取要更謹慎因為數(shù)據(jù)量會隨著時間跨度快速膨脹。拿一只股票的一年5分鐘數(shù)據(jù)來算每天48根K線4小時交易時間每小時12根一年大約240個交易日就是11520根幾百只股票就是百萬級以上的記錄。先看Tushare的分鐘線接口需要足夠的積分import tushare as ts pro ts.pro_api() df ts.pro_bar(ts_code600000.SH, freq5min, start_date20240101, end_date20240131) print(df.head())Baostock的分鐘線相對親民支持5、15、30、60分鐘但我提醒一句它沒有1分鐘數(shù)據(jù)想做高頻策略的話需要另找渠道。AKShare的分鐘接口用的是東方財富的公開接口可以拿1分鐘數(shù)據(jù)import akshare as ak df ak.stock_zh_a_hist_min_em( symbol600000, start_date2024-01-01 09:30:00, end_date2024-01-31 15:00:00, period5, adjust ) print(df.head())分鐘數(shù)據(jù)的拉取我強烈建議分批按月份循環(huán)每次拿一個月中間加time.sleep(1)避免連續(xù)高頻請求。尤其是AKShare這種底層依賴公開頁面的工具打得太快會被上游限制甚至導致接口短期失效。3.3 復權處理回測數(shù)據(jù)必須過的一道關復權這個話題值得單獨說因為很多新手直接在原始K線上跑回測結果跟真實收益差了十萬八千里。先解釋什么是復權上市公司分紅送股之后股價會變低比如一只100塊的股票每股分紅5塊除息日股價直接少了5塊。表現(xiàn)在K線圖上就是一個向下的跳空缺口。如果不復權這個跳空會被均線、MACD、OBV這些指標誤判成“暴跌”但實際你的賬戶價值并沒有變。復權分為前復權和后復權。前復權是把歷史價格統(tǒng)一調(diào)整到當前價格水平圖形直觀但歷史價格會隨著每次除權除息不斷變化后復權是以最早價格為基準向后調(diào)整當前價格會顯示成一個很大的數(shù)字不太直觀但優(yōu)點是歷史數(shù)據(jù)一旦生成就不變適合長期回測對比。我自己的習慣是原始不復權數(shù)據(jù)必須留一份存檔回測時按需計算前復權或后復權千萬不要把復權數(shù)據(jù)覆蓋到原始庫里不然后面想重新核對都無從下手。三個數(shù)據(jù)源的復權參數(shù)也不一致Tushare的ts.pro_bar有adj參數(shù)qfq是前復權hfq是后復權Baostock的adjustflag中1是后復權2是前復權3是不復權AKShare的adjust參數(shù)里qfq和hfq分別是前復權和后復權。這些參數(shù)我建議每個人都自己驗證一次拿一只分紅大戶的股票分別拉不復權和前復權對比除權日的跳空有沒有消失比看文檔印象更深刻。3.4 數(shù)據(jù)增量更新與本地落庫歷史數(shù)據(jù)不是拉一次就完事A股每個交易日都會產(chǎn)生新數(shù)據(jù)所以增量更新能力才是數(shù)據(jù)鏈路里的核心競爭力。我推薦的存儲方式是SQLite輕量、單文件、方便備份對行情數(shù)據(jù)這種結構化的低并發(fā)數(shù)據(jù)完全夠用。下面是我常用的建表語句CREATE TABLE IF NOT EXISTS daily_kline ( ts_code TEXT NOT NULL, trade_date TEXT NOT NULL, open REAL, high REAL, low REAL, close REAL, pre_close REAL, volume REAL, amount REAL, PRIMARY KEY (ts_code, trade_date) );增量更新的邏輯很簡單先查詢本地表里某只股票的最大trade_date然后從那個日期的下一天開始拉數(shù)據(jù)最后用INSERT OR REPLACE寫回表里。這樣即使某天重復執(zhí)行也不會產(chǎn)生重復數(shù)據(jù)主鍵能天然過濾掉已存在的記錄。import sqlite3 conn sqlite3.connect(market.db) cur conn.cursor() sql SELECT MAX(trade_date) FROM daily_kline WHERE ts_code ? cur.execute(sql, (600000.SH,)) last_date cur.fetchone()[0] print(本地最新日期:, last_date)這個思路同樣可以擴展到分鐘表分鐘表的唯一鍵改成(ts_code, trade_time)就行。增量更新看起來簡單但它能保證你以后任何時候補數(shù)據(jù)都是可重復的這種規(guī)范化的數(shù)據(jù)管理習慣最后都會體現(xiàn)在策略開發(fā)的效率上。4. 常見報錯和數(shù)據(jù)問題的排查實錄4.1 權限類報錯Token、積分和接口權限如果你在用Tushare最常遇到的是“抱歉您沒有訪問該接口的權限”或者“積分不足”。這類問題不用慌先去官網(wǎng)看接口文檔對應的積分要求?;A日線一般注冊就有權限但分鐘線、高頻財務指標這些需要更高積分。解決方法沒什么捷徑主賬號積累積分或完成認證但實踐中有個更聰明的辦法把高頻需求拆到AKShare或Baostock上去完成別為了一個接口在一棵樹上吊死。4.2 頻率限制明明好好的突然返回空或報錯頻率限制是所有免費API都會遇到的隱性墻。我的經(jīng)驗是每次拉完數(shù)據(jù)固定sleep(1)批量下載時把任務拆到多個時間段執(zhí)行。曾經(jīng)我寫過一個循環(huán)腳本一次性下載幾百只股票的歷史數(shù)據(jù)結果跑到第30只就開始被限流報錯信息還不是直接說限流而是返回空DataFrame特別誤導人。后來我把循環(huán)改成每下載10只股票就暫停30秒再也沒出現(xiàn)過這種問題。4.3 代碼格式和返回字段變化接口切換時第一坑就是股票代碼前綴前面也提到了。第二種坑是字段順序。新浪行情接口返回40多個字段中間可能有空字符串如果你直接按下標取指數(shù)和個股的字段長度還不一樣很容易錯位。我建議拿到原始文本后先看len(parts)按長度做分支處理或者只取自己需要的幾個固定下標然后在循環(huán)里加一個try/except解析失敗就打印原始行方便定位。4.4 停牌、新股和缺失數(shù)據(jù)歷史數(shù)據(jù)里最煩人的不是代碼寫錯而是股票本身狀態(tài)導致的缺數(shù)據(jù)。停牌股票的區(qū)間自然沒有K線新股上市之前也沒有歷史數(shù)據(jù)還有退市股的代碼后面會加掛牌日期后綴比如600002.SH這種格式可能最后變成帶日期的樣子。處理這類問題的核心思路是接口返回多少就存多少不要自己強行補零填充?;販y時再根據(jù)策略需要決定是否填充比如用NaN標記停牌讓回測引擎自行跳過這比硬補價格更符合交易事實。4.5 常見問題速查表為了方便你快速定位我把遇到過的典型現(xiàn)象整理成一張表現(xiàn)象可能原因處理方法新浪接口返回403缺少Referer請求頭帶上https://finance.sina.com.cn的Referer返回中文亂碼接口是GBK編碼resp.encoding gbkTushare提示無權限積分不足查看接口積分門檻或換數(shù)據(jù)源AKShare返回空DataFrame上游接口變動或限流加sleep重試檢查代碼格式分鐘數(shù)據(jù)缺第一根部分源不含9:31第一分鐘按交易時段規(guī)則自行對齊回測收益明顯偏高用了未來函數(shù)或未復權檢查指標計算是否用了當日future數(shù)據(jù)4.6 盤中數(shù)據(jù)和收盤數(shù)據(jù)不一致這個話題我必須放在這里提醒盤中拿到的實時快照和收盤后通過歷史接口拿到的日線數(shù)據(jù)經(jīng)常不是完全一致的。原因很簡單歷史日線里的收盤價是最終確認價而盤中接口會包含臨時成交、尾盤集合競價階段的變動部分券商還會在收盤后做異常交易處理。做監(jiān)控可以但如果你把盤中收到的最后一筆價格直接當成收盤價入庫第二天回測可能對不上賬。我的做法是盤中數(shù)據(jù)單獨存到一張“盤中快照”表收盤后統(tǒng)一用歷史日線接口覆蓋當天的正式數(shù)據(jù)兩邊互不干擾。5. 行情數(shù)據(jù)的實戰(zhàn)應用與擴展方向5.1 把行情數(shù)據(jù)喂給backtrader做回測拿到干凈的歷史K線之后最常見的使用場景就是量化回測。Backtrader雖然用得人少了但它的架構清晰適合個人投研尤其是配合PandasData導入非常順手。喂數(shù)據(jù)之前先確認DataFrame的字段和順序import pandas as pd import backtrader as bt df pd.read_sql_query( SELECT trade_date, open, high, low, close, volume FROM daily_kline WHERE ts_code600000.SH ORDER BY trade_date, conn ) df[trade_date] pd.to_datetime(df[trade_date]) df.set_index(trade_date, inplaceTrue) class PandasData(bt.feeds.PandasData): params ( (open, open), (high, high), (low, low), (close, close), (volume, volume), ) data PandasData(datanamedf) cerebro bt.Cerebro() cerebro.adddata(data)這里有兩個容易踩的細節(jié)。第一DataFrame的索引必須是DatetimeIndex而且時間順序要升序不然Backtrader會認為數(shù)據(jù)沒有時間軸。第二Backtrader的PandasData默認字段名是open、high、low、close、volume、openinterest如果你的字段叫trade_date、vol這些需要通過params顯式映射。多股回測時數(shù)據(jù)對齊是最麻煩的。我的經(jīng)驗是先把所有股票的交易日并集拿出來然后對每只股票做reindex缺失的日期填NaN這樣Backtrader的adddata加入多只股票后系統(tǒng)內(nèi)部的日期對齊才不會錯亂。5.2 用實時行情做異動股初篩很多人會在各個論壇找所謂“妖股選股公式源碼”其實拆開看本質(zhì)就是一組量價異動條件的組合。我用行情API自己搭過一個初篩腳本用OBV和量比做過濾邏輯很清楚這里分享出來供你參考。OBV能量潮的核心思想是把成交量按漲跌方向累計上漲日的成交量算正貢獻下跌日算負貢獻等于把所有量能粘合在一條線上。代碼實現(xiàn)很短import numpy as np def calc_obv(close, volume): direction np.sign(close.diff()).fillna(0) return (volume * direction).cumsum()配合實時快照做異動初篩的思路是當日漲幅在2%到9%之間既保證活躍又排除一字漲停板量比大于2說明當日成交量顯著超過過去5日均量OBV創(chuàng)出近60日新高說明量能正在持續(xù)推動趨勢換手率在5%到20%之間太高可能已經(jīng)過熱每次盤中輪詢時跑一遍這個規(guī)則命中的標的單獨存一個觀察列表。這里必須重復強調(diào)一句這只是數(shù)據(jù)篩選規(guī)則不是薦股建議所有規(guī)則能否賺錢要用歷史數(shù)據(jù)嚴格回測之后才能知道。5.3 構建自己的分鐘數(shù)據(jù)倉庫最近總有人問那種“A股歷史分鐘數(shù)據(jù)打包下載”靠不靠譜。我的意見很直接如果是研究歷史行情與其花時間找別人打包好的舊數(shù)據(jù)不如自己維護一份干凈的數(shù)據(jù)倉庫。分鐘數(shù)據(jù)的建設和日線類似只是表換成五分鐘或一分鐘K線主鍵換成(ts_code, trade_time)。批量拉取時我的腳本框架是這樣import time code_list [600000, 000001, 300750] month_list [202401, 202402, 202403] for code in code_list: for month in month_list: df ak.stock_zh_a_hist_min_em( symbolcode, start_datef{month}-01 09:30:00, end_datef{month}-31 15:00:00, period5, adjustqfq ) save_to_sqlite(df, code) time.sleep(1)整個倉庫建設最耗時間的不是寫代碼而是等待數(shù)據(jù)一點點落庫。如果你需要很大的時間跨度建議每周跑一次增量別一次性全部拉取。我自己第一次建一分鐘數(shù)據(jù)倉庫的時候一次性拉了幾十只股票半年的數(shù)據(jù)跑了快一個晚上才完成中途斷網(wǎng)一次還要重新校驗斷點。另外如果你需要復權后的分鐘數(shù)據(jù)盡量用接口的adjust參數(shù)直接生成不要自己手動在前復權日線上插值。分鐘級別的復權計算涉及跳空處理自己做容易產(chǎn)生邊界誤差。最后再分享一個小經(jīng)驗無論你選哪個數(shù)據(jù)源都要給自己留一條“原始數(shù)據(jù)”的底倉。我在實際操作中體會最深的一點就是數(shù)據(jù)清洗和處理可以反復優(yōu)化但原始不復權數(shù)據(jù)一旦被污染后面所有策略結果都變得不可信。所以數(shù)據(jù)入庫時我永遠分兩張表一張原始表只存接口原始返回一張計算表專門放復權后的數(shù)據(jù)。這樣即使中途發(fā)現(xiàn)復權邏輯寫錯了也能重新算不用重新拉一遍歷史數(shù)據(jù)。搞A股行情數(shù)據(jù)這件事表面上是API調(diào)用真正拉開差距的其實是數(shù)據(jù)管理和調(diào)試的細致程度。希望這篇文章能讓你少走幾段彎路。