状态空间设计:价格序列的归一化与特征工程
做强化学习交易系统,我最常被问到的一个问题是:「状态空间到底该怎么设计?」
说实话,这个问题没有标准答案。但我可以告诉你,状态空间的质量,直接决定了你的智能体能不能学会赚钱。我见过太多人把原始价格直接扔进网络,结果训练了三天,策略还不如一个简单的均线系统。
为什么会这样?因为强化学习对状态空间的敏感度,远超你的想象。你想想看,一个几万维的原始价格序列,里面充满了噪声和冗余信息。智能体根本分不清哪些是信号,哪些是噪声。
核心观点:状态空间设计的本质,是把市场信息压缩成智能体能理解的低维表征。好的状态空间,应该像一张清晰的地图,而不是一堆乱码。
价格序列的归一化:别让量纲毁了你的模型
我记得第一次做RL交易系统时,直接把沪深300的日收盘价扔进了网络。结果训练到一半,loss突然爆炸了。排查了半天才发现——价格从3000涨到5000,数值范围变了,网络权重也跟着剧烈震荡。
归一化,说白了就是让所有特征都在同一个量级上。我个人习惯用两种方法:
- Z-score标准化:减去均值,除以标准差。适合价格序列相对稳定的情况。
- 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个关键特征,比如买卖不平衡、价差、深度、以及前几档的加权平均价格。
知识体系结构图
下面这张图,是我对状态空间设计核心逻辑的总结。你可以把它当作一个检查清单:
嗯,这张图把整个状态空间设计的流程串起来了。从原始数据出发,经过三条处理路径,最终汇聚成一个低维、去噪、信息丰富的状态向量。你每次设计状态空间时,都可以对照这张图检查一下——有没有遗漏哪个环节?
最后说一句:状态空间设计没有银弹。不同的市场、不同的交易频率、不同的策略类型,需要的状态空间都不一样。我的建议是:从最简单的开始,逐步增加特征,每次只改一个变量。这样你才能知道,到底是哪个特征真正提升了策略表现。