詳解Python中量化數(shù)據(jù)的處理技巧(附實(shí)戰(zhàn)代碼)
量化研究中,拿到數(shù)據(jù)只是第一步。如果不理解復(fù)權(quán)、不處理缺失值、不會(huì)做多股票數(shù)據(jù)對(duì)齊,算出來(lái)的指標(biāo)可能全是錯(cuò)的。本文系統(tǒng)講解量化數(shù)據(jù)處理中最常見(jiàn)的幾個(gè)坑,以及對(duì)應(yīng)的解決方案。
一、這些問(wèn)題你一定遇到過(guò)
- 策略回測(cè)結(jié)果特別好,一查發(fā)現(xiàn)是沒(méi)復(fù)權(quán),除權(quán)日價(jià)格跳空導(dǎo)致假信號(hào)
- 多只股票做橫截面分析,日期對(duì)不齊,merge 后一堆 NaN
- 計(jì)算換手率但不知道流通股本怎么來(lái)
- 停牌股的空數(shù)據(jù)把整個(gè)分析搞亂
這些都是量化數(shù)據(jù)處理中的典型問(wèn)題,下面逐一解決。
二、環(huán)境準(zhǔn)備
pip install "tickflow[all]" --upgrade
from tickflow import TickFlow # 免費(fèi)服務(wù)(日 K 線夠用) tf_free = TickFlow.free() # 完整服務(wù)(需要實(shí)時(shí)行情、財(cái)務(wù)數(shù)據(jù)等) tf = TickFlow(api_key="your-api-key")
TickFlow 文檔:docs.tickflow.org
三、復(fù)權(quán)處理
為什么必須復(fù)權(quán)
上市公司會(huì)進(jìn)行分紅派息、送股轉(zhuǎn)增等操作,導(dǎo)致股價(jià)在除權(quán)日出現(xiàn)跳空。如果不復(fù)權(quán),技術(shù)指標(biāo)會(huì)在除權(quán)日產(chǎn)生錯(cuò)誤信號(hào)。
舉個(gè)例子:某股票原價(jià) 20 元,10 送 10 后變成 10 元。如果不復(fù)權(quán),均線系統(tǒng)會(huì)認(rèn)為股價(jià)"暴跌"了 50%。
TickFlow 支持的復(fù)權(quán)方式
from tickflow import TickFlow tf = TickFlow.free() symbol = "600519.SH" # 比例前復(fù)權(quán)(默認(rèn),推薦用于量化回測(cè)) df_forward = tf.klines.get(symbol, period="1d", count=2000, adjust="forward", as_dataframe=True) # 差值前復(fù)權(quán)(與東方財(cái)富/同花順價(jià)格一致) df_additive = tf.klines.get(symbol, period="1d", count=2000, adjust="forward_additive", as_dataframe=True) # 不復(fù)權(quán)(原始成交價(jià)格) df_none = tf.klines.get(symbol, period="1d", count=2000, adjust="none", as_dataframe=True) # 比例后復(fù)權(quán) df_backward = tf.klines.get(symbol, period="1d", count=2000, adjust="backward", as_dataframe=True) # 差值后復(fù)權(quán) df_backward_add = tf.klines.get(symbol, period="1d", count=2000, adjust="backward_additive", as_dataframe=True)
不同復(fù)權(quán)方式的對(duì)比
import pandas as pd
compare = pd.DataFrame({
"trade_date": df_none["trade_date"],
"不復(fù)權(quán)": df_none["close"],
"比例前復(fù)權(quán)": df_forward["close"],
"差值前復(fù)權(quán)": df_additive["close"],
"比例后復(fù)權(quán)": df_backward["close"],
})
print(compare.tail(5))
print(f"\n不復(fù)權(quán)最新價(jià): {df_none['close'].iloc[-1]}")
print(f"前復(fù)權(quán)最新價(jià): {df_forward['close'].iloc[-1]}")
print(f"后復(fù)權(quán)最新價(jià): {df_backward['close'].iloc[-1]}")
選哪種復(fù)權(quán)
| 場(chǎng)景 | 推薦復(fù)權(quán)方式 | 原因 |
|---|---|---|
| 收益率計(jì)算、量化回測(cè) | forward(比例前復(fù)權(quán)) | 收益率連續(xù),不失真 |
| 與東方財(cái)富/同花順價(jià)格核對(duì) | forward_additive(差值前復(fù)權(quán)) | 價(jià)格數(shù)值一致 |
| 長(zhǎng)期投資收益對(duì)比 | backward(后復(fù)權(quán)) | 能直觀看到真實(shí)漲幅 |
| 原始價(jià)格分析 | none(不復(fù)權(quán)) | 看真實(shí)成交價(jià)格 |
關(guān)鍵原則:做回測(cè)和計(jì)算收益率一定要用前復(fù)權(quán)。
查看除權(quán)因子
tf = TickFlow(api_key="your-api-key")
factors = tf.klines.ex_factors(["600519.SH", "000001.SZ"], as_dataframe=True)
# 按標(biāo)的分組查看
for symbol in ["600519.SH", "000001.SZ"]:
sym_factors = factors[factors["symbol"] == symbol]
print(f"\n{symbol} 最近 5 次除權(quán):")
print(sym_factors[["trade_date", "ex_factor"]].tail(5).to_string(index=False))
四、多股票日期對(duì)齊
問(wèn)題:日期不一致
不同股票可能因?yàn)橥E?、上市時(shí)間不同等原因,K 線日期不完全對(duì)齊。直接用 merge 會(huì)產(chǎn)生大量 NaN。
方案一:基于交易日歷對(duì)齊
import pandas as pd
from tickflow import TickFlow
tf = TickFlow.free()
symbols = ["600519.SH", "000858.SZ", "601318.SH"]
dfs = tf.klines.batch(symbols, period="1d", count=500, adjust="forward", as_dataframe=True)
# 提取所有交易日的并集
all_dates = set()
for df in dfs.values():
all_dates.update(df["trade_date"].tolist())
all_dates = sorted(all_dates)
# 對(duì)齊到統(tǒng)一日期索引
aligned = pd.DataFrame({"trade_date": all_dates})
for symbol, df in dfs.items():
close = df[["trade_date", "close"]].rename(columns={"close": symbol})
aligned = aligned.merge(close, on="trade_date", how="left")
print(f"對(duì)齊后形狀: {aligned.shape}")
print(aligned.tail())
方案二:只保留共有日期
# 取交集(所有股票都有數(shù)據(jù)的日期)
common_dates = None
for df in dfs.values():
dates = set(df["trade_date"].tolist())
common_dates = dates if common_dates is None else common_dates & dates
common_dates = sorted(common_dates)
aligned_common = pd.DataFrame({"trade_date": common_dates})
for symbol, df in dfs.items():
close = df[["trade_date", "close"]].rename(columns={"close": symbol})
aligned_common = aligned_common.merge(close, on="trade_date", how="inner")
print(f"共有交易日: {len(aligned_common)}")
print(aligned_common.tail())
方案三:前向填充停牌日
停牌期間用最后一個(gè)有效價(jià)格填充:
# 在方案一的基礎(chǔ)上,前向填充
aligned_filled = aligned.copy()
for symbol in symbols:
aligned_filled[symbol] = aligned_filled[symbol].ffill()
# 檢查填充效果
null_before = aligned[symbols].isnull().sum()
null_after = aligned_filled[symbols].isnull().sum()
print("填充前缺失值:")
print(null_before)
print("\n填充后缺失值:")
print(null_after)
五、缺失值處理
常見(jiàn)缺失值來(lái)源
- 停牌:股票停牌期間沒(méi)有交易數(shù)據(jù)
- 上市時(shí)間晚:某只股票的歷史數(shù)據(jù)比其他股票短
- 退市:股票退市后沒(méi)有新數(shù)據(jù)
- 數(shù)據(jù)異常:極少數(shù)情況下數(shù)據(jù)源缺失
檢查缺失值
import pandas as pd
from tickflow import TickFlow
tf = TickFlow.free()
symbols = ["600519.SH", "000858.SZ", "601318.SH", "000001.SZ", "600036.SH"]
dfs = tf.klines.batch(symbols, period="1d", count=1000, adjust="forward", as_dataframe=True)
print("各標(biāo)的數(shù)據(jù)量:")
for symbol, df in dfs.items():
start = df["trade_date"].iloc[0]
end = df["trade_date"].iloc[-1]
print(f" {symbol}: {len(df)} 根 K 線 ({start} ~ {end})")
處理策略
# 將多只股票收盤價(jià)合并為一個(gè) DataFrame
close_df = pd.DataFrame()
for symbol, df in dfs.items():
s = df.set_index("trade_date")["close"]
close_df[symbol] = s
# 方法 1:前向填充(適合停牌)
close_filled = close_df.ffill()
# 方法 2:刪除含有任何缺失值的行(保守但干凈)
close_clean = close_df.dropna()
print(f"原始行數(shù): {len(close_df)}, 清洗后: {len(close_clean)}")
# 方法 3:僅刪除缺失值過(guò)多的行(某天超過(guò)半數(shù)股票無(wú)數(shù)據(jù)才刪)
threshold = len(symbols) // 2
close_filtered = close_df.dropna(thresh=threshold)
print(f"過(guò)濾后: {len(close_filtered)}")
六、換手率計(jì)算
換手率是衡量股票活躍度的重要指標(biāo),但很多數(shù)據(jù)源不直接提供換手率,需要用成交量和流通股本自己算。
公式
換手率 = 成交量(手) × 100 / 流通股本(股)
A 股成交量單位為"手"(1 手 = 100 股),所以需要乘以 100 換算為股。
實(shí)現(xiàn)
import pandas as pd
from tickflow import TickFlow
tf = TickFlow(api_key="your-api-key")
symbol = "600000.SH"
# 獲取日 K 線
kdf = tf.klines.get(symbol, period="1d", count=2000, as_dataframe=True)
# 獲取股本數(shù)據(jù)
sdf = tf.financials.shares([symbol], as_dataframe=True)
# 轉(zhuǎn)為日期類型
kdf["date"] = pd.to_datetime(kdf["trade_date"])
sdf["date"] = pd.to_datetime(sdf["period_end"])
# 使用 merge_asof 為每根 K 線匹配當(dāng)時(shí)有效的流通股本
merged = pd.merge_asof(
kdf.sort_values("date"),
sdf[["date", "float_shares"]].sort_values("date"),
on="date",
direction="backward",
)
# 計(jì)算換手率
merged["turnover_rate"] = (merged["volume"] * 100 / merged["float_shares"]).round(4)
merged["turnover_pct"] = merged["turnover_rate"] * 100
print(merged[["trade_date", "close", "volume", "float_shares", "turnover_pct"]].tail(10))
輸出示例:
trade_date close volume float_shares turnover_pct
2026-04-22 9.61 685905 3.330584e+10 0.21
2026-04-23 9.54 806247 3.330584e+10 0.24
2026-04-24 9.45 848590 3.330584e+10 0.25
2026-04-27 9.36 872815 3.330584e+10 0.26
2026-04-28 9.37 594550 3.330584e+10 0.18
為什么用 merge_asof
流通股本不是每天都變。公司只在特定時(shí)間點(diǎn)(增發(fā)、回購(gòu)、限售股解禁等)更新股本數(shù)據(jù)。merge_asof 會(huì)為每個(gè)交易日匹配最近一次公布的流通股本,這是正確的做法。
批量計(jì)算換手率
symbols = ["600519.SH", "000858.SZ", "601318.SH"]
# 批量獲取 K 線
kline_dfs = tf.klines.batch(symbols, period="1d", count=60, as_dataframe=True)
# 批量獲取股本
shares_df = tf.financials.shares(symbols, as_dataframe=True)
for symbol in symbols:
kdf = kline_dfs.get(symbol)
sdf = shares_df[shares_df["symbol"] == symbol].copy()
if kdf is None or len(sdf) == 0:
continue
kdf["date"] = pd.to_datetime(kdf["trade_date"])
sdf["date"] = pd.to_datetime(sdf["period_end"])
merged = pd.merge_asof(
kdf.sort_values("date"),
sdf[["date", "float_shares"]].sort_values("date"),
on="date",
direction="backward",
)
merged["turnover_pct"] = (merged["volume"] * 100 / merged["float_shares"] * 100).round(2)
avg_turnover = merged["turnover_pct"].mean()
print(f"{symbol}: 近 60 日平均換手率 {avg_turnover:.2f}%")
七、收益率計(jì)算的正確姿勢(shì)
簡(jiǎn)單收益率 vs 對(duì)數(shù)收益率
import numpy as np
from tickflow import TickFlow
tf = TickFlow.free()
df = tf.klines.get("600519.SH", period="1d", count=250, adjust="forward", as_dataframe=True)
# 簡(jiǎn)單收益率(常用)
df["simple_return"] = df["close"].pct_change()
# 對(duì)數(shù)收益率(適合統(tǒng)計(jì)分析,可加性更好)
df["log_return"] = np.log(df["close"] / df["close"].shift(1))
print(df[["trade_date", "close", "simple_return", "log_return"]].tail(10))
什么時(shí)候用哪種
| 場(chǎng)景 | 推薦 |
|---|---|
| 策略收益計(jì)算 | 簡(jiǎn)單收益率 |
| 統(tǒng)計(jì)分析(相關(guān)性、因子收益) | 對(duì)數(shù)收益率 |
| 多期累計(jì)收益 | 簡(jiǎn)單收益率用乘法,對(duì)數(shù)收益率用加法 |
累計(jì)收益率
# 簡(jiǎn)單收益率的累計(jì)
df["cum_return"] = (1 + df["simple_return"]).cumprod() - 1
# 或用對(duì)數(shù)收益率
df["cum_return_log"] = np.exp(df["log_return"].cumsum()) - 1
print(f"期間總收益: {df['cum_return'].iloc[-1] * 100:.2f}%")
八、數(shù)據(jù)質(zhì)量檢查
拿到數(shù)據(jù)后,建議做一些基礎(chǔ)的數(shù)據(jù)質(zhì)量檢查:
from tickflow import TickFlow
tf = TickFlow.free()
df = tf.klines.get("600000.SH", period="1d", count=5000, adjust="forward", as_dataframe=True)
# 1. 檢查是否有重復(fù)日期
dup = df[df["trade_date"].duplicated()]
print(f"重復(fù)日期: {len(dup)} 個(gè)")
# 2. 檢查價(jià)格異常(收盤價(jià)為 0 或負(fù)數(shù))
bad_price = df[df["close"] <= 0]
print(f"異常價(jià)格: {len(bad_price)} 個(gè)")
# 3. 檢查 OHLC 邏輯(high >= low, high >= open, high >= close)
ohlc_err = df[(df["high"] < df["low"]) | (df["high"] < df["open"]) | (df["high"] < df["close"])]
print(f"OHLC 邏輯異常: {len(ohlc_err)} 個(gè)")
# 4. 檢查日期連續(xù)性(是否有異常缺失)
df["date"] = pd.to_datetime(df["trade_date"])
df["gap"] = df["date"].diff().dt.days
big_gaps = df[df["gap"] > 5] # 超過(guò) 5 天的間隔(排除周末和短假)
print(f"超過(guò) 5 天的間隔: {len(big_gaps)} 個(gè)")
if len(big_gaps) > 0:
print(big_gaps[["trade_date", "gap"]].head())
九、數(shù)據(jù)緩存與增量更新
每次運(yùn)行策略都重新下載全量數(shù)據(jù)會(huì)浪費(fèi)時(shí)間。可以做簡(jiǎn)單的本地緩存:
import os
import pandas as pd
from tickflow import TickFlow
CACHE_DIR = "./kline_cache"
os.makedirs(CACHE_DIR, exist_ok=True)
def get_klines_cached(tf, symbol, period="1d", count=10000):
"""帶本地緩存的 K 線獲取"""
cache_file = os.path.join(CACHE_DIR, f"{symbol}_{period}.csv")
if os.path.exists(cache_file):
cached = pd.read_csv(cache_file)
last_date = cached["trade_date"].iloc[-1]
print(f"{symbol}: 緩存到 {last_date},增量更新...")
# 獲取緩存之后的新數(shù)據(jù)
import datetime
last_ts = int(datetime.datetime.strptime(last_date, "%Y-%m-%d").timestamp() * 1000)
new_data = tf.klines.get(
symbol, period=period, start_time=last_ts + 86400000,
count=count, as_dataframe=True,
)
if len(new_data) > 0:
# 過(guò)濾掉可能重復(fù)的最后一天
new_data = new_data[new_data["trade_date"] > last_date]
combined = pd.concat([cached, new_data], ignore_index=True)
combined.to_csv(cache_file, index=False)
print(f" 新增 {len(new_data)} 根 K 線")
return combined
return cached
else:
print(f"{symbol}: 首次獲取...")
df = tf.klines.get(symbol, period=period, count=count, as_dataframe=True)
df.to_csv(cache_file, index=False)
return df
tf = TickFlow.free()
df = get_klines_cached(tf, "600519.SH")
print(f"總計(jì) {len(df)} 根 K 線")
十、總結(jié)
量化數(shù)據(jù)處理中最容易踩坑的幾個(gè)地方:
| 問(wèn)題 | 正確做法 |
|---|---|
| 不復(fù)權(quán)就算指標(biāo) | 回測(cè)必須用前復(fù)權(quán) |
| 多股票日期不對(duì)齊 | merge + ffill 或取交集 |
| 缺失值導(dǎo)致計(jì)算出錯(cuò) | 先檢查再?zèng)Q定填充或刪除 |
| 換手率不知道怎么算 | volume * 100 / float_shares,用 merge_asof |
| 收益率用錯(cuò)方法 | 策略用簡(jiǎn)單收益率,統(tǒng)計(jì)分析用對(duì)數(shù)收益率 |
| 每次都全量下載 | 做本地緩存 + 增量更新 |
這些技巧看似基礎(chǔ),但每一個(gè)處理不當(dāng)都可能導(dǎo)致回測(cè)失真或策略結(jié)論錯(cuò)誤。
TickFlow 在數(shù)據(jù)層面已經(jīng)做了很多:多種復(fù)權(quán)方式開(kāi)箱即用、批量接口高效穩(wěn)定、股本數(shù)據(jù)可直接獲取。在此基礎(chǔ)上做好數(shù)據(jù)處理,你的量化研究才有可靠的地基。
相關(guān)鏈接
- 官網(wǎng):tickflow.org
- 文檔:docs.tickflow.org
- Github:github.com/tickflow-org/tickflow
數(shù)據(jù)處理不性感,但它決定了你的回測(cè)結(jié)果是真是假。
以上就是詳解Python中量化數(shù)據(jù)的處理技巧(附實(shí)戰(zhàn)代碼)的詳細(xì)內(nèi)容,更多關(guān)于Python量化數(shù)據(jù)處理的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- Python量化交易實(shí)戰(zhàn)之使用Resample函數(shù)轉(zhuǎn)換“日K”數(shù)據(jù)
- Python數(shù)據(jù)處理工具之Pandas的使用詳細(xì)指南
- Python中處理樹(shù)狀分類數(shù)據(jù)的常見(jiàn)方法詳解
- 1行代碼搞定Python中的高頻數(shù)據(jù)處理場(chǎng)景
- Python格式化處理JSON數(shù)據(jù)的完整指南
- Python數(shù)據(jù)自動(dòng)化處理之?dāng)?shù)據(jù)清洗和報(bào)表生成詳解
- Python使用openpyxl自動(dòng)化處理Excel數(shù)據(jù)與文件詳解
相關(guān)文章
pycharm執(zhí)行python時(shí),填寫參數(shù)的方法
今天小編就為大家分享一篇pycharm執(zhí)行python時(shí),填寫參數(shù)的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-10-10
python神經(jīng)網(wǎng)絡(luò)MobileNetV3?small模型的復(fù)現(xiàn)詳解
這篇文章主要為大家介紹了python神經(jīng)網(wǎng)絡(luò)MobileNetV3?small模型的復(fù)現(xiàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-05-05
Python實(shí)現(xiàn)的爬取百度貼吧圖片功能完整示例
這篇文章主要介紹了Python實(shí)現(xiàn)的爬取百度貼吧圖片功能,結(jié)合完整實(shí)例形式分析了Python實(shí)現(xiàn)的百度貼吧圖片爬蟲(chóng)相關(guān)操作技巧,需要的朋友可以參考下2019-05-05
python數(shù)據(jù)爬下來(lái)保存的位置
在本篇文章里小編給大家整理的是關(guān)于python數(shù)據(jù)爬下來(lái)保存的位置,需要的朋友們可以參考下。2020-02-02
python3使用Flask實(shí)現(xiàn)api數(shù)據(jù)接口的示例詳解
這篇文章主要為大家詳細(xì)介紹了python3如何使用Flask實(shí)現(xiàn)api數(shù)據(jù)接口,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2025-09-09
Playwright的wait funtion測(cè)試的實(shí)現(xiàn)
本文主要介紹了Playwright的wait funtion測(cè)試的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2026-04-04
Python?GUI利用tkinter皮膚ttkbootstrap實(shí)現(xiàn)好看的窗口
這篇文章主要介紹了Python?GUI利用tkinter皮膚ttkbootstrap實(shí)現(xiàn)好看的窗口,文章基于python的相關(guān)資料展開(kāi)詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,感興趣的小伙伴可以參考一下2022-06-06
python3實(shí)現(xiàn)全角和半角字符轉(zhuǎn)換的方法示例
在自然語(yǔ)言處理過(guò)程中,全角、半角的的不一致會(huì)導(dǎo)致信息抽取不一致,因此需要統(tǒng)一,下面這篇文章主要給大家介紹了關(guān)于python3中全角和半角字符轉(zhuǎn)換的方法,需要的朋友可以參考借鑒,下面來(lái)一起看看吧。2017-09-09
Python實(shí)現(xiàn)圖像和辦公文檔處理的方法和技巧
本文介紹了Python實(shí)現(xiàn)圖像和辦公文檔處理的方法和技巧,包括使用Pillow庫(kù)處理圖像、使用OpenCV庫(kù)進(jìn)行圖像識(shí)別和處理、使用PyPDF2庫(kù)處理PDF文檔、使用docx和xlwt庫(kù)處理Word和Excel文檔等,幫助讀者更好地掌握Python在圖像和辦公文檔處理方面的應(yīng)用2023-05-05

