第二章:数据获取与清洗

做碳配额交易量化策略,第一步不是写模型,而是搞定数据。

我见过太多人一上来就搞复杂算法,结果数据源一塌糊涂,回测结果根本没法用。说白了,数据质量决定了策略的天花板。今天我们就拿湖北碳市场开刀,把数据获取和清洗这关彻底打通。

2.1 湖北碳市场数据源分析

湖北碳排放权交易中心是国内最早启动的区域碳市场之一,交易数据相对完整。我个人习惯从以下几个渠道获取历史数据:

  • 官方渠道:湖北碳排放权交易中心官网(hbets.cn)的"行情数据"板块
  • 第三方聚合:上海环境能源交易所、广州碳排放权交易所的公开数据接口
  • 数据服务商:Wind、东方Choice等金融终端(需付费)

这里有个坑——官方数据通常只提供最近一年的完整日频数据,更早的历史数据需要手动拼接。我在做湖北碳市场回测时就吃过这个亏,以为数据齐全,结果2018年的数据缺了整整两个月。

⚠️ 注意:湖北碳市场在2021年7月全国碳市场上线后,交易规则有过调整。获取数据时务必确认时间区间对应的交易机制是否一致。

2.2 数据获取实战:用Python爬取湖北碳市场数据

我们直接上代码。这里用requests库模拟浏览器请求,从湖北碳市场行情页面抓取日度交易数据。

import requests
import pandas as pd
from datetime import datetime, timedelta
import time

def fetch_hubei_carbon_data(start_date, end_date):
    """
    获取湖北碳市场日度交易数据
    """
    base_url = "https://www.hbets.cn/api/market/data/daily"
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
        'Referer': 'https://www.hbets.cn/'
    }
    
    all_data = []
    current_date = datetime.strptime(start_date, '%Y-%m-%d')
    end = datetime.strptime(end_date, '%Y-%m-%d')
    
    while current_date <= end:
        date_str = current_date.strftime('%Y-%m-%d')
        params = {'date': date_str}
        
        try:
            resp = requests.get(base_url, params=params, headers=headers, timeout=10)
            if resp.status_code == 200:
                data = resp.json()
                if data['code'] == 0 and data['data']:
                    all_data.append(data['data'])
                    print(f"✅ 成功获取 {date_str} 数据")
                else:
                    print(f"⚠️ {date_str} 无数据或非交易日")
            else:
                print(f"❌ {date_str} 请求失败,状态码:{resp.status_code}")
        except Exception as e:
            print(f"❌ {date_str} 异常:{str(e)}")
        
        current_date += timedelta(days=1)
        time.sleep(0.5)  # 礼貌性延迟,别把人家服务器搞崩了
    
    return pd.DataFrame(all_data)

# 获取2023年全年数据
df_raw = fetch_hubei_carbon_data('2023-01-01', '2023-12-31')
print(f"原始数据量:{len(df_raw)} 条")

嗯,这里要注意——湖北碳市场不是每天都有交易。实际交易日大概只占全年工作日的60%左右,所以返回的数据条数会远小于365。

2.3 数据清洗:把脏数据变成干净的时间序列

拿到原始数据后,你会发现字段名可能是中文的,格式也不统一。我一般按以下步骤清洗:

  1. 字段重命名:把中文列名转成英文,方便后续处理
  2. 类型转换:日期转datetime,价格和成交量转float
  3. 排序去重:按日期升序排列,删除重复行
  4. 异常值过滤:剔除价格为零或负数的记录
def clean_carbon_data(df):
    """
    清洗湖北碳市场数据
    """
    # 字段映射(根据实际返回字段调整)
    column_map = {
        '交易日期': 'date',
        '开盘价': 'open',
        '最高价': 'high',
        '最低价': 'low',
        '收盘价': 'close',
        '成交量': 'volume',
        '成交额': 'amount'
    }
    df = df.rename(columns=column_map)
    
    # 只保留需要的字段
    cols = ['date', 'open', 'high', 'low', 'close', 'volume', 'amount']
    df = df[cols]
    
    # 类型转换
    df['date'] = pd.to_datetime(df['date'])
    for col in ['open', 'high', 'low', 'close', 'volume', 'amount']:
        df[col] = pd.to_numeric(df[col], errors='coerce')
    
    # 排序去重
    df = df.sort_values('date').drop_duplicates(subset='date')
    
    # 异常值过滤
    df = df[(df['close'] > 0) & (df['volume'] >= 0)]
    
    # 重置索引
    df = df.reset_index(drop=True)
    
    return df

df_clean = clean_carbon_data(df_raw)
print(f"清洗后数据量:{len(df_clean)} 条")
print(df_clean.head())
💡 经验之谈:我在处理湖北数据时发现,2022年6月有一周的数据连续出现"收盘价=开盘价"的情况。后来查证是系统故障导致的重复记录,直接删掉就好。建议清洗后手动抽查几个日期,和官网行情页面对比一下。

2.4 缺失值处理:碳市场数据的特殊之处

碳市场数据有个特点——非交易日不产生数据。这和股票市场不一样,股票市场非交易日直接跳过就行,但碳市场因为交易不活跃,经常出现"有交易日但无成交"的情况。

为什么会这样?说白了,碳配额交易是政策驱动型市场,企业只有在履约期临近时才会密集交易。平时可能一周都没几笔成交。

我建议按以下策略处理缺失值:

缺失类型 处理方式 适用场景
非交易日缺失 不填充,保持时间序列不连续 日频策略、事件驱动策略
交易日无成交 前向填充(ffill) 技术指标计算、连续时间序列模型
连续多日缺失 线性插值 低频策略、周度/月度模型
价格异常缺失 剔除该交易日 所有场景(避免引入噪声)
def handle_missing_values(df, method='ffill'):
    """
    处理缺失值
    method: 'ffill' 前向填充, 'linear' 线性插值, 'drop' 直接删除
    """
    df = df.copy()
    
    # 先生成完整的日历日期序列
    full_date_range = pd.date_range(start=df['date'].min(), 
                                    end=df['date'].max(), 
                                    freq='D')
    df_full = df.set_index('date').reindex(full_date_range)
    
    if method == 'ffill':
        # 前向填充:用上一个交易日的数据填充
        df_filled = df_full.fillna(method='ffill')
    elif method == 'linear':
        # 线性插值:适合连续多日缺失
        df_filled = df_full.interpolate(method='linear')
    elif method == 'drop':
        # 直接删除缺失行
        df_filled = df_full.dropna()
    else:
        raise ValueError("不支持的填充方法")
    
    # 重置索引
    df_filled = df_filled.reset_index().rename(columns={'index': 'date'})
    
    return df_filled

# 示例:使用前向填充
df_ffill = handle_missing_values(df_clean, method='ffill')
print(f"前向填充后数据量:{len(df_ffill)} 条")

# 示例:使用线性插值
df_linear = handle_missing_values(df_clean, method='linear')
print(f"线性插值后数据量:{len(df_linear)} 条")
📌 核心原则:碳市场数据清洗要遵循"最小干预"原则。不要为了追求数据连续性而过度填充,否则会引入虚假的交易信号。我个人在实盘策略中,对缺失超过5个交易日的数据直接做截断处理。

2.5 数据质量检查清单

数据清洗完成后,我习惯做一轮快速检查,确保数据可用:

  • 时间跨度检查:起始日期和结束日期是否覆盖目标区间
  • 价格合理性:收盘价是否在合理范围内(湖北碳市场历史价格约20-60元/吨)
  • 成交量检查:是否存在连续多日零成交(超过10天需标记)
  • 跳空检查:相邻交易日价格波动是否超过20%(可能是数据错误)
  • 复权检查:碳市场没有除权除息,但要注意是否有政策调整导致的"价格断层"

我曾经在检查湖北数据时发现,2021年7月16日全国碳市场上线当天,湖北市场的收盘价突然跳涨了15%。后来一查,是因为全国碳市场启动带来的政策利好。这种跳空是真实的,不能当异常值处理。

2.6 本章知识体系

下面这张图把数据获取与清洗的核心流程串起来了:

数据获取与清洗核心流程 数据源 官方/第三方/付费 数据获取 API爬取/文件导入 数据清洗 重命名/类型转换/去重 缺失值 处理 字段重命名 类型转换 排序去重 异常值过滤 前向填充 线性插值 直接删除 ✅ 干净可用的时间序列

数据获取与清洗是量化策略的基石。这一步做扎实了,后面的特征工程和模型训练才能站得住脚。记住:宁可数据少一点,也要保证数据真一点。

🔧 实用工具推荐:我常用pandas-profiling库做数据质量报告,一行代码就能生成HTML格式的数据概览,包括缺失值分布、异常值检测、相关性分析等。非常适合数据清洗后的快速验证。
专注资料整理