3. 数据准备与清洗:打好地基才能盖高楼

说实话,很多做量化的人,一上来就急着写策略、调参数。我见过太多人,模型跑得飞起,结果一查数据,全是坑。嗯,数据准备这事儿,看着不起眼,但它是整个策略的根基。

我个人习惯,花在数据清洗上的时间,至少占整个项目时间的40%。你想想看,如果持仓盈亏状态都算错了,那后面的处置效应识别还有什么意义?

核心逻辑: 数据准备阶段,我们要完成四件事——获取原始数据、清洗脏数据、计算持仓盈亏、标记盈利/亏损状态。

3.1 获取历史交易数据

我一般从两个渠道拿数据:一是本地数据库(比如MySQL里存好的日线数据),二是直接从交易所API拉。这里我习惯用 pandas-datareader 或者 akshare 这类库。

举个例子,用 akshare 拿A股数据,代码很简单:

import akshare as ak
import pandas as pd

# 获取个股历史行情
df = ak.stock_zh_a_hist(symbol="000001", period="daily", 
                        start_date="20200101", end_date="20231231")
df.columns = ['date', 'open', 'close', 'high', 'low', 'volume', 
              'amount', 'amplitude', 'pct_change', 'change', 'turnover']
print(df.head())

这里有个坑,我踩过好几次——不同数据源返回的字段名不一样。有的叫 trade_date,有的叫 date。我建议你拿到数据后,第一时间统一字段命名规范。

我的习惯: 所有日期字段统一为 date,价格字段统一为 open/high/low/close,成交量用 volume。这样后续写策略时,不用反复查字段名。

3.2 数据清洗与预处理

原始数据拿回来,第一件事不是算指标,而是检查数据质量。我总结了三步走:

  1. 缺失值处理——看看有没有空值。尤其是停牌日,价格和成交量都是NaN。
  2. 异常值检测——比如某天涨幅突然变成100%,那多半是数据错误。
  3. 时间序列对齐——确保日期是连续的,没有跳空。

代码实现起来也不复杂:

# 1. 检查缺失值
print(df.isnull().sum())

# 2. 填充或删除缺失值
df = df.dropna(subset=['close'])  # 删除收盘价为空的记录

# 3. 确保日期排序
df['date'] = pd.to_datetime(df['date'])
df = df.sort_values('date').reset_index(drop=True)

# 4. 检查日期连续性
date_range = pd.date_range(start=df['date'].min(), end=df['date'].max(), freq='B')
missing_dates = date_range.difference(df['date'])
if len(missing_dates) > 0:
    print(f"缺失交易日: {missing_dates[:5]}")

我曾经遇到过一个项目,数据里连续三天都是同一个价格。一开始以为是行情没波动,后来发现是数据源接口出了问题。嗯,从那以后,我每次清洗完数据,都会先画个K线图看一眼。

注意: 不要盲目填充缺失值。比如停牌日的价格,用前一日价格填充是可以的,但成交量填0更合理。具体怎么处理,取决于你的策略逻辑。

3.3 计算持仓盈亏状态

这一步是识别处置效应的关键。说白了,我们要知道每一笔持仓,当前是盈利还是亏损。

我习惯用「持仓成本」和「当前价格」来计算浮动盈亏。假设我们有一笔交易记录:

# 模拟持仓数据
positions = pd.DataFrame({
    'stock': ['000001', '000002', '000003'],
    'buy_date': ['2023-01-10', '2023-02-15', '2023-03-20'],
    'buy_price': [12.5, 8.3, 25.0],
    'quantity': [1000, 2000, 500],
    'current_price': [13.2, 7.9, 26.5]  # 假设当前价格
})

# 计算浮动盈亏
positions['pnl'] = (positions['current_price'] - positions['buy_price']) * positions['quantity']
positions['pnl_pct'] = (positions['current_price'] - positions['buy_price']) / positions['buy_price']

print(positions[['stock', 'pnl', 'pnl_pct']])

这里有个细节:持仓成本要不要考虑交易费用? 我个人建议,如果是做高频或者短线,佣金和印花税会影响盈亏判断,最好算进去。如果是做中长线,可以忽略,因为影响不大。

3.4 标记盈利/亏损持仓

有了盈亏数据,标记就很简单了。我一般用布尔值或者分类标签:

# 标记盈利/亏损
positions['is_profit'] = positions['pnl'] > 0
positions['status'] = positions['pnl'].apply(
    lambda x: '盈利' if x > 0 else ('亏损' if x < 0 else '持平')
)

print(positions[['stock', 'pnl', 'status']])

你可能会问:为什么还要标记「持平」? 嗯,在实际交易中,持平状态其实很常见。尤其是做网格交易或者套利策略,经常会出现盈亏在零附近波动的情况。我个人习惯把「持平」单独拎出来,因为处置效应研究的主要是盈利和亏损两种状态下的行为差异。

关键点: 处置效应的核心假设是——投资者在盈利时倾向于卖出(落袋为安),在亏损时倾向于持有(等待回本)。所以,准确标记每一笔持仓的盈亏状态,是后续分析的基础。

3.5 本章知识体系总览

下面这张图,是我自己画的数据准备流程。你看一眼,就能明白整个链条是怎么串起来的:

数据准备与清洗流程 获取历史交易数据 API / 数据库 / CSV 数据清洗与预处理 缺失值 / 异常值 / 对齐 计算持仓盈亏状态 成本 vs 当前价格 标记盈利/亏损持仓 布尔值 / 分类标签 输出:清洗后的持仓数据 常见问题与避坑 • 停牌日数据缺失 • 复权价格不一致 • 交易费用影响盈亏 • 日期对齐遗漏 • 分红送股未处理

这张图里,我特意把「常见问题与避坑」单独列出来了。因为在实际项目中,数据清洗的坑远比策略逻辑多。你想想看,如果分红送股没处理,持仓成本算出来就是错的,那后面的处置效应分析全白搭。

一个小技巧: 我每次清洗完数据,都会做一个「数据质量报告」——统计缺失率、异常值数量、日期连续性等指标。这样心里有底,后续分析出了问题,也知道是数据的问题还是策略的问题。

好了,数据准备这块就聊到这儿。记住一句话:数据干净了,策略才能跑得稳。


专注资料整理