4. 时间序列对齐:时区处理、非交易时间过滤、多品种时间戳对齐

做市商这行,数据对齐是基本功。我见过太多新手,策略逻辑写得天花乱坠,结果一跑回测,发现EUR/USD和GBP/USD的时间戳差了5个小时——那还做个毛线?

说白了,时间序列对齐就是三件事:把时间搞对、把垃圾时间扔掉、把不同品种的钟表调成同步。今天咱们就掰开揉碎聊这个。

4.1 时区处理:别让你的数据活在平行宇宙

外汇市场是全球24小时滚动交易的。你拿到的数据,可能是GMT、EST、甚至服务器本地时间。我刚开始做的时候,就吃过这个亏——把GMT+2的欧元数据跟GMT-5的美元数据直接合并,结果相关性分析全乱套。

我个人习惯,所有数据统一转成UTC。为什么?因为UTC没有夏令时,不会出现一年跳两次钟的破事。

核心原则:入库前统一时区,分析时再按需转换。

来看代码,怎么把乱七八糟的时区统一掉:

import pandas as pd
import pytz

def unify_timezone(df, time_col='timestamp', source_tz='US/Eastern', target_tz='UTC'):
    """
    统一时区到UTC
    df: DataFrame,必须包含时间列
    source_tz: 源时区,比如 'US/Eastern', 'Europe/London'
    target_tz: 目标时区,默认UTC
    """
    df = df.copy()
    # 先确保时间列是datetime类型
    df[time_col] = pd.to_datetime(df[time_col])
    
    # 本地化到源时区,再转换到目标时区
    df[time_col] = df[time_col].dt.tz_localize(source_tz, ambiguous='infer')
    df[time_col] = df[time_col].dt.tz_convert(target_tz)
    
    return df

# 示例:把美东时间的数据转成UTC
df_eur = pd.read_csv('eur_usd_2024.csv')
df_eur = unify_timezone(df_eur, source_tz='US/Eastern')
print(df_eur['timestamp'].head())

避坑指南:我曾经遇到过某家数据商,凌晨2:00的数据时间戳写的是2:00 AM,但实际是夏令时切换那天——2:00 AM根本不存在!pandas的ambiguous参数就是用来处理这种歧义的,设成'infer'让它自动推断,或者设成'NaT'把歧义数据标为缺失。

4.2 非交易时间过滤:别把垃圾当宝贝

外汇市场虽然24小时,但每个品种都有自己的活跃时段。比如USD/JPY,东京开盘和纽约开盘的波动性完全不是一个量级。你把流动性极差的时间段的数据喂进模型,相当于在训练数据里掺沙子。

我一般过滤掉以下时间段:

  • 周末休市:周五22:00 UTC到周日22:00 UTC(外汇市场实际休市)
  • 主要节假日:比如圣诞节、元旦,流动性几乎为零
  • 低流动性时段:比如悉尼早盘(22:00-00:00 UTC),点差大、成交稀

怎么过滤?直接上代码:

def filter_non_trading_hours(df, time_col='timestamp', 
                              exclude_weekends=True,
                              exclude_holidays=True):
    """
    过滤非交易时间
    """
    df = df.copy()
    df[time_col] = pd.to_datetime(df[time_col])
    
    # 1. 过滤周末
    if exclude_weekends:
        # 外汇市场周末休市:周五22:00 UTC 到 周日22:00 UTC
        df['day_of_week'] = df[time_col].dt.dayofweek  # 周一=0, 周日=6
        df['hour_utc'] = df[time_col].dt.hour
        
        # 周五22:00之后 或 周六全天 或 周日22:00之前
        weekend_mask = (
            ((df['day_of_week'] == 4) & (df['hour_utc'] >= 22)) |  # 周五
            (df['day_of_week'] == 5) |  # 周六
            ((df['day_of_week'] == 6) & (df['hour_utc'] < 22))  # 周日
        )
        df = df[~weekend_mask]
    
    # 2. 过滤节假日(示例:圣诞节和元旦)
    if exclude_holidays:
        holiday_dates = [
            '2024-12-25', '2024-12-26',  # 圣诞节
            '2024-01-01', '2025-01-01'   # 元旦
        ]
        holiday_mask = df[time_col].dt.date.isin(
            [pd.to_datetime(d).date() for d in holiday_dates]
        )
        df = df[~holiday_mask]
    
    # 清理辅助列
    df = df.drop(columns=['day_of_week', 'hour_utc'], errors='ignore')
    
    return df

# 使用
df_clean = filter_non_trading_hours(df_eur)
print(f"过滤前: {len(df_eur)} 行, 过滤后: {len(df_clean)} 行")

注意:不同数据商的周末定义可能不同。有的把周五17:00 EST(即22:00 UTC)作为收盘,有的用16:00 EST。我建议你拿到数据后,先画个成交量分布图,看看哪些时间段成交量骤降——那才是你该过滤的。

4.3 多品种时间戳对齐:把不同步的钟表调准

这是最头疼的部分。EUR/USD可能每秒都有报价,但USD/JPY可能隔几秒才更新一次。你直接按时间合并,会发现大量NaN值。

对齐的核心思路就两种:

  • 向前填充(Forward Fill):用最近的有效报价填充空缺。适合做市商场景——你实际交易时,用的就是最近一次看到的报价。
  • 重采样(Resample):把数据规整到固定频率,比如1分钟、5分钟。适合做策略回测。

我个人更推荐先重采样,再向前填充。为什么?因为原始数据的时间戳可能不均匀,重采样能保证所有品种在同一个时间网格上。

来看一个完整的对齐流程:

def align_multiple_assets(data_dict, freq='1min', method='ffill'):
    """
    多品种时间戳对齐
    data_dict: dict, key=品种名, value=DataFrame(含'timestamp'和'price'列)
    freq: 目标频率,如'1min', '5min'
    method: 填充方法,'ffill'向前填充,'bfill'向后填充
    """
    aligned_dfs = []
    
    for asset_name, df in data_dict.items():
        # 1. 设置时间索引
        df = df.set_index('timestamp')
        df.index = pd.to_datetime(df.index)
        
        # 2. 重采样到目标频率
        # 用中位数聚合(防止异常报价),然后向前填充
        resampled = df['price'].resample(freq).median()
        
        # 3. 填充缺失值
        if method == 'ffill':
            resampled = resampled.ffill()
        elif method == 'bfill':
            resampled = resampled.bfill()
        
        # 4. 重命名列
        resampled = resampled.to_frame(name=asset_name)
        aligned_dfs.append(resampled)
    
    # 5. 合并所有品种
    aligned_data = pd.concat(aligned_dfs, axis=1)
    
    # 6. 删除全为NaN的行(比如所有品种都缺失的时间点)
    aligned_data = aligned_data.dropna(how='all')
    
    return aligned_data

# 示例:对齐EUR/USD和USD/JPY到1分钟频率
data_dict = {
    'EUR_USD': df_eur[['timestamp', 'price']],
    'USD_JPY': df_jpy[['timestamp', 'price']]
}
aligned = align_multiple_assets(data_dict, freq='1min')
print(aligned.head())

4.4 实战中的坑与技巧

嗯,这里有几个我踩过的坑,你注意一下:

  1. 夏令时切换:每年3月和11月,美国欧洲的夏令时切换不同步。那几天的时间序列会有23小时或25小时。我建议直接丢弃那天的数据,或者用前后两天的均值填充。
  2. 报价延迟:不同数据商的报价到达时间可能差几毫秒。如果你做高频,这个延迟会致命。我一般用时间戳对齐+价格插值来处理——用前后两个报价的加权平均估算当前时刻的价格。
  3. 缺失值处理:如果某个品种连续缺失超过5分钟,别用向前填充了。那说明市场真的没流动性,填充出来的价格是假的。直接标记为NaN,让模型自己处理。

我的经验:做多品种对齐时,先检查每个品种的时间戳间隔分布。如果EUR/USD平均间隔0.5秒,USD/JPY平均间隔2秒,那你用1分钟频率对齐就太粗了——会丢失很多信息。我一般用所有品种中最慢的那个频率作为基准,再适当降低一点。

4.5 知识体系总览

下面这张图,把时间序列对齐的整个流程串起来了。你照着这个流程走,基本不会出大问题:

时间序列对齐核心流程 原始多品种数据 EUR/USD, USD/JPY, GBP/USD... 时区统一到UTC 消除夏令时、时区差异 过滤非交易时间 周末、节假日、低流动性 重采样到统一频率 1min / 5min / 1hour 缺失值填充 向前填充 / 插值 多品种合并 按时间索引横向拼接 对齐后的时间序列 可直接用于建模 常见问题与处理 • 夏令时切换 → 丢弃或均值填充 • 报价延迟 → 时间戳对齐 + 价格插值 • 连续缺失 > 5min → 标记为NaN • 不同品种频率差异 → 以最慢频率为基准

你看,整个流程其实不复杂。但每一步都有细节,稍不注意就会翻车。我个人建议,每次做数据对齐时,都先画个流程图,把每个品种的时区、频率、缺失情况列清楚。磨刀不误砍柴工嘛。

最后说一句:数据对齐这件事,做得好没人夸你,做错了模型直接崩。所以,耐心点,仔细点。你的策略会感谢你的。


专注资料整理