问题描述
解决方案
这是一个非常经典的微观结构交易策略。订单簿不平衡(Order Book Imbalance, OBI)通过衡量买单和卖单在数量上的差异,来预测短期价格的微小变动方向。
策略逻辑与原理
1. OBI 定义
OBI 是衡量买卖压力的指标。最基础的计算公式如下:
$$ OBI = \frac{V_{bid} - V_{ask}}{V_{bid} + V_{ask}} $$
- $V_{bid}$:买盘(Bid)前 N 档的总挂单量。
- $V_{ask}$:卖盘(Ask/Offer)前 N 档的总挂单量。
- 取值范围:[-1, 1]。
- 接近 1:买盘远大于卖盘,预示价格可能上涨。
- 接近 -1:卖盘远大于买盘,预示价格可能下跌。
- 接近 0:买卖力量均衡。
2. 交易逻辑
- 买入信号:当 $OBI > \text{阈值}$(例如 0.3),且当前无持仓时,以卖一价(Ask 1)买入。
- 卖出信号:当 $OBI < -\text{阈值}$(例如 -0.3),且当前有持仓时,以买一价(Bid 1)卖出。
3. PTrade 实现细节
- 使用
tick_data函数处理高频 Tick 数据(3秒一次)。 - 数据源:
data[security]['tick']中的bid_grp(买档)和offer_grp(卖档)。 - 注意:在 PTrade 的
tick_data中,bid_grp和offer_grp返回的是 DataFrame 中的一列,且内容通常是字符串格式的字典,需要使用eval()进行解析才能获取具体的量价数据。
策略代码实现
以下是完整的 PTrade 策略代码。该代码选取了前 5 档行情来计算 OBI。
def initialize(context):
"""
初始化函数
"""
# 设定关注的股票,这里以恒生电子为例
g.security = '600570.SS'
set_universe(g.security)
# 策略参数设置
g.obi_threshold = 0.3 # OBI 阈值,超过此值触发交易
g.trade_amount = 100 # 每次交易的数量
g.depth_level = 5 # 计算 OBI 使用的档位数(1-5档)
def tick_data(context, data):
"""
Tick 级别数据处理函数,每 3 秒触发一次
"""
security = g.security
# 1. 获取 Tick 数据
# data[security] 包含 'tick', 'order', 'transcation'
# 我们主要需要 'tick' 数据
if security not in data:
return
tick_df = data[security]['tick']
# 检查数据是否为空
if tick_df is None or len(tick_df) == 0:
return
# 2. 解析盘口数据
# PTrade 的 tick 数据中,bid_grp 和 offer_grp 往往是字符串形式的字典
# 格式示例: {1: [价格, 量, 笔数], 2: [...], ...}
try:
# 获取第一行数据(当前切片)
bid_grp_str = tick_df['bid_grp'][0]
offer_grp_str = tick_df['offer_grp'][0]
# 使用 eval 将字符串转换为字典
bid_grp = eval(bid_grp_str)
offer_grp = eval(offer_grp_str)
except Exception as e:
# 如果解析失败或数据格式异常,直接返回
return
# 3. 计算买盘和卖盘的总挂单量 (前 N 档)
total_bid_vol = 0.0
total_ask_vol = 0.0
# 遍历前 g.depth_level 档
for i in range(1, g.depth_level + 1):
# 字典的 key 是档位 (1, 2, 3, 4, 5)
# value 是 list: [价格, 数量, 笔数]
if i in bid_grp:
total_bid_vol += bid_grp[i][1] # 累加买单数量
if i in offer_grp:
total_ask_vol += offer_grp[i][1] # 累加卖单数量
# 4. 计算 OBI (Order Book Imbalance)
# 防止除以零
if (total_bid_vol + total_ask_vol) == 0:
obi = 0
else:
obi = (total_bid_vol - total_ask_vol) / (total_bid_vol + total_ask_vol)
# 打印日志方便调试 (生产环境可注释掉)
# log.info("Time: %s, OBI: %.4f, BidVol: %s, AskVol: %s" % (
# tick_df['hsTimeStamp'][0], obi, total_bid_vol, total_ask_vol))
# 5. 获取当前持仓
position = get_position(security)
curr_amount = position.amount
# 6. 交易逻辑
# 信号:买入
# 逻辑:OBI 大于阈值 (买压大) 且 当前无持仓
if obi > g.obi_threshold and curr_amount == 0:
# 使用 order_tick 下单
# priceGear='-1' 代表卖一价,即以对手价买入,保证成交速度
order_tick(security, g.trade_amount, priceGear='-1')
log.info("买入触发 - OBI: %.2f, 数量: %s" % (obi, g.trade_amount))
# 信号:卖出
# 逻辑:OBI 小于负阈值 (卖压大) 且 当前有持仓
elif obi < -g.obi_threshold and curr_amount > 0:
# priceGear='1' 代表买一价,即以对手价卖出
# 卖出时数量为负数
order_tick(security, -curr_amount, priceGear='1')
log.info("卖出触发 - OBI: %.2f, 数量: %s" % (obi, curr_amount))
def handle_data(context, data):
"""
必须实现的函数,但在 Tick 策略中通常留空
"""
pass
代码关键点解析
-
数据解析 (
eval):- PTrade 的
tick_data返回的 DataFrame 中,bid_grp和offer_grp字段存储的是字符串形式的字典(例如"{1:[10.5, 1000, 5], ...}")。 - 必须使用
eval()函数将其还原为 Python 字典对象,才能访问具体的档位数据。 - 字典结构通常为
{档位: [价格, 数量, 笔数]},我们取索引[1]来获取数量。
- PTrade 的
-
OBI 计算:
- 代码中使用了简单的等权重求和:$\sum_{i=1}^{5} Vol_{bid_i}$。
- 如果需要更精细的策略,可以给第一档(买一/卖一)更高的权重,例如:$Vol_{total} = 1.0 \times Vol_1 + 0.8 \times Vol_2 + ...$。
-
下单方式 (
order_tick):- 在
tick_data中必须使用order_tick。 priceGear='-1':表示以卖一价(对手方最优价)下单买入,这在追涨策略中很重要,因为我们需要立即成交。priceGear='1':表示以买一价下单卖出。
- 在
-
异常处理:
- 增加了
try...except块,防止因为行情数据缺失或格式错误导致策略崩溃。 - 增加了分母为 0 的判断(虽然在活跃股票中很少见,但在涨跌停或极度不活跃时可能发生)。
- 增加了
常见问题 Q&A
Q: 这个策略适合回测吗?
A: PTrade 支持 Tick 级别的回测,但速度较慢。OBI 策略依赖极短期的微观结构,回测时的撮合逻辑(Match Logic)可能与实盘有差异(回测通常假设你的小单能完全成交,不影响盘口),实盘中需要考虑滑点和冲击成本。
Q: 如何优化 OBI 指标?
A:
- 加权 OBI:距离中间价越近的档位,对价格影响越大,可以给予更高的权重衰减(如 $e^{-\alpha x}$)。
- 结合 VOI (Volume Order Imbalance):不仅看当前的挂单量,还要看挂单量的变化量(Delta)。
- 结合成交流:观察
transcation数据,看主动买入和主动卖出的成交量。
Q: 为什么使用 eval?
A: 这是 PTrade 接口特有的数据格式处理方式。在 tick_data 中,L2 的盘口数据被序列化为字符串存储在 DataFrame 中,必须反序列化才能使用。