4. 固定时间窗口内逐笔成交聚合特征

任务拆解:从逐笔成交到固定窗口特征矩阵

本例的目标是把一张逐笔成交明细表,按固定时间窗口(例如 1 分钟)切分成若干组,每组计算 4 个聚合量:成交笔数、总成交量、总成交额、买卖不平衡量,最终拼成一个以窗口为行、特征为列的时间序列矩阵。关键约束是:窗口边界必须固定对齐(如 09:30:00、09:31:00……),不能按“每 N 笔”或“从首笔起算”分组,否则不同交易日的窗口不可比。

下面用一段可运行的 Python 代码完成整个流程。示例数据为构造的教学数据,方向字段 side 取 +1 表示主动买入、-1 表示主动卖出,价格与数量均为假设值。

import pandas as pd
import numpy as np

# 1) 构造教学用逐笔成交数据(示例,非真实行情)
raw = pd.DataFrame({
    "trade_time": pd.to_datetime([
        "2024-06-03 09:30:05", "2024-06-03 09:30:41", "2024-06-03 09:30:58",
        "2024-06-03 09:31:02", "2024-06-03 09:31:27", "2024-06-03 09:31:55",
        "2024-06-03 09:32:10", "2024-06-03 09:32:33", "2024-06-03 09:32:49",
        "2024-06-03 09:33:04",
    ]),
    "price": [10.00, 10.02, 10.01, 10.03, 10.05, 10.04, 10.02, 10.00, 9.99, 10.01],
    "volume": [300, 500, 200, 800, 400, 600, 700, 300, 500, 200],
    "side": [1, 1, -1, 1, -1, 1, -1, -1, 1, -1],
})

# 2) 单笔成交额与带符号成交量(方向符号化)
raw["amount"] = raw["price"] * raw["volume"]
raw["signed_volume"] = raw["side"] * raw["volume"]

# 3) 固定时间窗口分组:floor 到 1 分钟
raw["window"] = raw["trade_time"].dt.floor("1min")

# 4) 窗口内聚合
feat = raw.groupby("window").agg(
    trade_count=("volume", "size"),          # 成交笔数
    total_volume=("volume", "sum"),          # 总成交量
    total_amount=("amount", "sum"),          # 总成交额
    buy_volume=("volume", lambda s: s[raw.loc[s.index, "side"] == 1].sum()),
    sell_volume=("volume", lambda s: s[raw.loc[s.index, "side"] == -1].sum()),
).reset_index()

# 5) 买卖不平衡量 = 主动买入量 - 主动卖出量
feat["volume_imbalance"] = feat["buy_volume"] - feat["sell_volume"]

# 6) 输出特征矩阵(窗口为索引)
matrix = feat.set_index("window")[
    ["trade_count", "total_volume", "total_amount", "volume_imbalance"]
]
print(matrix)

按上述示例数据,09:30 窗口含 3 笔(300+500+200=1000 股,成交额 10.00×300+10.02×500+10.01×200=10012 元,买入 800 股、卖出 200 股,不平衡量 +600);09:31 窗口含 3 笔(1800 股,买入 1200、卖出 600,不平衡量 +600);09:32 窗口含 3 笔(1500 股,买入 500、卖出 1200,不平衡量 −700);09:33 窗口仅 1 笔(200 股,不平衡量 −200)。这些数值可由给定输入直接推出,属于示例计算结果。

窗口边界用 dt.floor("1min") 实现,它把任意时间戳向下取整到整分钟,保证跨交易日窗口严格对齐。若改用 resample,需注意它会补齐空窗口(成交笔数为 0),这对后续建模通常更有利,但会改变行数,需在特征矩阵中明确是否保留空窗口。
逐笔成交流(按时间戳排序) 09:30:05 10.00 × 300 买 09:30:41 10.02 × 500 买 09:30:58 10.01 × 200 卖 09:31:02 10.03 × 800 买 09:31:27 10.05 × 400 卖 09:31:55 10.04 × 600 买 09:32:10 10.02 × 700 卖 09:32:33 10.00 × 300 卖 09:32:49 9.99 × 500 买 09:33:04 10.01 × 200 卖 floor("1min") 窗口 09:30 3 笔 1000 股 / 10012 元 窗口 09:31 3 笔 1800 股 / 18070 元 窗口 09:32 3 笔 1500 股 / 15009 元 窗口 09:33 1 笔 200 股 / 2002 元 聚合 时间序列特征矩阵 窗口 笔数 总量 总额 不平衡 09:303100010012+600 09:313180018070+600 09:323150015009−700 09:3312002002−200 不平衡量 = 主动买入量 − 主动卖出量 正值为买方主导,负值为卖方主导 窗口边界固定对齐(整分钟),保证不同交易日同一时刻的窗口可直接比较; 若某窗口无成交,需决定是否补 0 行,否则矩阵行数会随行情活跃度变化。

实现要点与验证

  • 方向符号约定:side=+1 为主动买入、-1 为主动卖出,不平衡量 = 买入量 − 卖出量,正值表示买方主导。若数据源用 B/S 字符,需先映射为数值再聚合。
  • 窗口对齐:用 dt.floor("1min") 而非 resample 时,空窗口不会出现;若需要连续时间轴,改用 resample("1min").agg(...) 并 fillna(0),但要注意 trade_count 补 0 与 volume_imbalance 补 0 的语义一致。
  • 验证方法:对每个窗口检查 buy_volume + sell_volume == total_volume,以及 total_amount == Σ(price×volume);再核对窗口内笔数之和等于原始行数,确保没有成交被漏分或重复计入。
  • 常见坑:groupby 后直接用 size() 统计笔数时,若数据含重复时间戳不影响;但若先按 trade_time 去重再聚合,会改变笔数含义,需明确“笔”指原始成交记录条数。
示例中的价格、数量与聚合结果均为教学构造数据,用于演示计算路径,不代表任何真实标的的行情或回测表现。实际使用时请以交易所逐笔成交字段定义为准,并确认 side 字段的买卖方向口径(主动买/主动卖 vs 外盘/内盘)。