状态空间设计:价格序列的归一化与特征工程

做强化学习交易系统,我最常被问到的一个问题是:「状态空间到底该怎么设计?」

说实话,这个问题没有标准答案。但我可以告诉你,状态空间的质量,直接决定了你的智能体能不能学会赚钱。我见过太多人把原始价格直接扔进网络,结果训练了三天,策略还不如一个简单的均线系统。

为什么会这样?因为强化学习对状态空间的敏感度,远超你的想象。你想想看,一个几万维的原始价格序列,里面充满了噪声和冗余信息。智能体根本分不清哪些是信号,哪些是噪声。

核心观点:状态空间设计的本质,是把市场信息压缩成智能体能理解的低维表征。好的状态空间,应该像一张清晰的地图,而不是一堆乱码。

价格序列的归一化:别让量纲毁了你的模型

我记得第一次做RL交易系统时,直接把沪深300的日收盘价扔进了网络。结果训练到一半,loss突然爆炸了。排查了半天才发现——价格从3000涨到5000,数值范围变了,网络权重也跟着剧烈震荡。

归一化,说白了就是让所有特征都在同一个量级上。我个人习惯用两种方法:

  1. Z-score标准化:减去均值,除以标准差。适合价格序列相对稳定的情况。
  2. Min-Max缩放:缩放到[0,1]区间。适合有明显上下边界的数据。

但这里有个坑——千万别用全局的均值和标准差。为什么?因为未来的数据你还没看到,用全局统计量会导致未来信息泄露。我一般用滚动窗口的方式,比如用过去60天的数据做标准化。

# 滚动窗口归一化示例
def rolling_normalize(price_series, window=60):
    """
    用过去window天的数据做标准化
    避免未来信息泄露
    """
    mean = price_series.rolling(window).mean()
    std = price_series.rolling(window).std()
    return (price_series - mean) / (std + 1e-8)  # 加小常数防止除零

我的经验:对于高频交易场景,我更喜欢用对数收益率代替价格本身。对数收益率天然具有平稳性,而且能消除价格水平的影响。你想想看,茅台从100涨到110,和从1000涨到1010,虽然绝对涨幅不同,但对数收益率是一样的——这才符合交易者的直觉。

特征工程:技术指标的有效性筛选

技术指标这东西,用好了是神器,用多了就是灾难。我曾经见过一个交易系统,状态空间里塞了50多个技术指标,结果训练出来的策略还不如随机策略。

为什么会这样?因为很多技术指标之间高度相关。比如RSI和随机指标,本质上都在衡量超买超卖。你把它们都放进去,不仅增加了维度,还引入了冗余信息。

我个人筛选技术指标的原则很简单:

  • 每个指标必须回答一个独立的问题。比如:趋势方向(均线)、波动率(ATR)、动量(ROC)、超买超卖(RSI)。
  • 相关性高的指标只保留一个。我一般会先算一个相关性矩阵,把相关系数超过0.8的指标合并或剔除。
  • 优先选择计算简单的指标。强化学习训练时,每一步都要重新计算状态。复杂的指标会拖慢训练速度。
指标类别 推荐指标 解决的问题 计算复杂度
趋势 EMA(12,26) 当前处于上升还是下降趋势
波动率 ATR(14) 市场波动有多大
动量 ROC(10) 价格变化的速度
超买超卖 RSI(14) 是否处于极端状态
成交量 OBV 资金流向

避坑指南:我曾经在回测中加入了「未来函数」类的指标,比如用未来数据计算的布林带宽度。结果回测曲线漂亮得不像话,实盘直接亏成狗。记住:任何用到未来数据的指标,都是毒药

市场微观结构数据:订单簿与成交量分布

如果你只做日线级别的交易,那价格和技术指标可能就够了。但如果你做日内交易,甚至高频交易,订单簿数据就是你的命根子

订单簿,说白了就是市场上所有未成交的买单和卖单。它能告诉你:

  • 买卖力量的对比:买一和卖一的挂单量差距,反映了短期供需失衡。
  • 价格支撑和阻力:大单密集区往往是价格难以突破的位置。
  • 市场深度:挂单量越大,市场越有深度,价格越不容易被操纵。

我记得有一次做期货的RL策略,只用了价格和成交量,策略表现一直不温不火。后来我把订单簿的「买卖价差」和「深度不平衡指标」加进去,策略的夏普比率直接从0.8跳到了1.5。为什么?因为订单簿数据能提前反映价格的短期走势。

成交量分布(Volume Profile)也是个好东西。它不像传统成交量那样按时间平均分布,而是按价格水平分布。你能看到:

  • 价值区域:成交量最集中的价格区间,往往是市场的公允价值。
  • 高成交量节点:价格在这里容易获得支撑或遇到阻力。
  • 低成交量区域:价格在这里容易快速通过,形成「真空地带」。
# 订单簿特征提取示例
def extract_orderbook_features(orderbook):
    """
    从订单簿数据中提取关键特征
    """
    bid_volumes = [level['volume'] for level in orderbook['bids']]
    ask_volumes = [level['volume'] for level in orderbook['asks']]
    
    # 买卖不平衡指标
    imbalance = sum(bid_volumes[:5]) / (sum(ask_volumes[:5]) + 1e-8)
    
    # 买卖价差
    spread = orderbook['asks'][0]['price'] - orderbook['bids'][0]['price']
    
    # 价格深度(前10档的总挂单量)
    depth = sum(bid_volumes[:10]) + sum(ask_volumes[:10])
    
    return {
        'imbalance': imbalance,
        'spread': spread,
        'depth': depth
    }

我的建议:不要直接把原始订单簿数据(比如所有档位的价格和数量)扔进网络。那样维度太高,而且大部分信息是冗余的。我一般只提取3-5个关键特征,比如买卖不平衡、价差、深度、以及前几档的加权平均价格。

知识体系结构图

下面这张图,是我对状态空间设计核心逻辑的总结。你可以把它当作一个检查清单:

状态空间设计核心逻辑 原始市场数据 价格序列归一化 Z-score / Min-Max 滚动窗口避免未来信息 对数收益率替代价格 技术指标有效性筛选 趋势/波动率/动量/超买超卖 相关性分析去冗余 避免未来函数 市场微观结构数据 订单簿:买卖不平衡/价差/深度 成交量分布:价值区域/高量节点 提取3-5个关键特征 最终状态向量(低维、去噪、信息丰富)

嗯,这张图把整个状态空间设计的流程串起来了。从原始数据出发,经过三条处理路径,最终汇聚成一个低维、去噪、信息丰富的状态向量。你每次设计状态空间时,都可以对照这张图检查一下——有没有遗漏哪个环节?

最后说一句:状态空间设计没有银弹。不同的市场、不同的交易频率、不同的策略类型,需要的状态空间都不一样。我的建议是:从最简单的开始,逐步增加特征,每次只改一个变量。这样你才能知道,到底是哪个特征真正提升了策略表现。

专注资料整理