问题描述
解决方案
这是一个基于 OBV (On-Balance Volume, 能量潮) 指标的量化交易策略。
策略原理
OBV 指标通过统计成交量的变动趋势来推测股价趋势。
- OBV计算:
- 如果今日收盘价 > 昨日收盘价,则今日成交量为正,累加到 OBV。
- 如果今日收盘价 < 昨日收盘价,则今日成交量为负,从 OBV 中减去。
- 如果今日收盘价 = 昨日收盘价,OBV 保持不变。
- 交易信号:
- 买入信号:当 OBV 指标向上突破 OBV 的均线(MA_OBV)时,视为资金流入,看涨。
- 卖出信号:当 OBV 指标向下向跌破 OBV 的均线(MA_OBV)时,视为资金流出,看跌。
策略代码
# -*- coding: utf-8 -*-
import numpy as np
import pandas as pd
def initialize(context):
"""
初始化函数,设定基准、手续费、滑点、全局变量等
"""
# 设定沪深300作为基准
set_benchmark('000300.XSHG')
# 开启动态复权模式(真实价格)
set_option('use_real_price', True)
# 过滤掉order系列API产生的比error级别低的log
log.set_level('order', 'error')
# 设定股票类每笔交易时的手续费
set_order_cost(OrderCost(close_tax=0.001, open_commission=0.0003, close_commission=0.0003, min_commission=5), type='stock')
# 设定要操作的股票(这里以平安银行为例)
g.security = '000001.XSHE'
# 设定OBV均线的时间窗口
g.obv_ma_window = 20
# 每天开盘时运行
run_daily(market_open, time='every_bar')
def market_open(context):
"""
每日交易逻辑
"""
security = g.security
# 1. 获取历史数据
# 我们需要计算 OBV 的均线,假设窗口为 N,我们需要获取 N+1 天的数据来计算 N 个价格变动
# 为了计算 OBV 序列的均值,我们需要足够长的历史数据来构建 OBV 序列
# 这里获取 2 * g.obv_ma_window 天的数据以确保数据的稳定性
fetch_len = 2 * g.obv_ma_window
# 获取收盘价和成交量
df = attribute_history(security, fetch_len, '1d', ['close', 'volume'], df=True)
# 如果数据不足,直接返回
if len(df) < fetch_len:
return
# 2. 计算 OBV 指标
# 计算每日收盘价的变化:diff = 今日收盘 - 昨日收盘
# diff > 0, 取 volume; diff < 0, 取 -volume; diff = 0, 取 0
# 计算价格变化
df['price_change'] = df['close'].diff()
# 根据价格变化确定成交量的符号
# np.sign: >0返回1, <0返回-1, =0返回0
df['signed_volume'] = np.sign(df['price_change']) * df['volume']
# 填充第一天的 NaN (diff产生的) 为 0
df['signed_volume'] = df['signed_volume'].fillna(0)
# 计算累积 OBV
df['OBV'] = df['signed_volume'].cumsum()
# 3. 计算 OBV 的均线 (MA_OBV)
# 计算过去 g.obv_ma_window 天的 OBV 均值
obv_ma = df['OBV'].rolling(window=g.obv_ma_window).mean().iloc[-1]
# 获取当前的 OBV 值
current_obv = df['OBV'].iloc[-1]
# 获取上一期的 OBV 值和均值(用于判断交叉)
prev_obv = df['OBV'].iloc[-2]
prev_obv_ma = df['OBV'].rolling(window=g.obv_ma_window).mean().iloc[-2]
# 4. 获取账户资金和持仓情况
cash = context.portfolio.available_cash
position = context.portfolio.positions[security].closeable_amount
# 5. 生成交易信号并下单
# 金叉:OBV 上穿 OBV均线 (当前OBV > 均线 且 上一期OBV < 上一期均线)
# 或者简化逻辑:当前 OBV > 均线 且 持仓为0
if current_obv > obv_ma and prev_obv <= prev_obv_ma:
if cash > 0:
# 全仓买入
order_value(security, cash)
log.info(f"买入信号触发: OBV({current_obv:.2f}) 上穿 MA({obv_ma:.2f})")
# 死叉:OBV 下穿 OBV均线 (当前OBV < 均线 且 上一期OBV > 上一期均线)
elif current_obv < obv_ma and prev_obv >= prev_obv_ma:
if position > 0:
# 清仓卖出
order_target(security, 0)
log.info(f"卖出信号触发: OBV({current_obv:.2f}) 下穿 MA({obv_ma:.2f})")
# 记录数据以便在回测图中查看
record(OBV=current_obv, MA_OBV=obv_ma)
代码解析
-
初始化 (
initialize):- 设置了基准(沪深300)、真实价格模式和手续费。
- 定义了全局变量
g.security(操作标的) 和g.obv_ma_window(OBV均线周期,设为20天)。
-
数据获取与计算 (
market_open):- 使用
attribute_history获取历史收盘价和成交量。 - OBV核心逻辑:
- 使用
diff()计算价格涨跌。 - 使用
np.sign()判断成交量的正负方向。 - 使用
cumsum()计算累积的 OBV 值。
- 使用
- 使用
rolling().mean()计算 OBV 的移动平均线。
- 使用
-
交易逻辑:
- 买入:当 OBV 线从下方向上穿越 OBV 均线(金叉)时,全仓买入。
- 卖出:当 OBV 线从上方向下穿越 OBV 均线(死叉)时,清仓卖出。
-
绘图 (
record):- 调用
record函数将 OBV 值和均线值绘制在回测结果的图表中,方便可视化分析策略表现。
- 调用
注意事项
- OBV的相对性:OBV 的绝对数值取决于计算的起始点,绝对值本身没有意义,有意义的是其走势和与均线的相对位置。
- 适用场景:OBV 策略通常在趋势明显的市场中表现较好,在震荡市中可能会频繁触发假信号,建议结合其他指标(如 MACD 或 KDJ)进行过滤。
Q&A
Q: 如何修改策略以操作多只股票?
A: 可以将 g.security 修改为一个列表(例如 g.stocks = ['000001.XSHE', '600519.XSHG']),然后在 market_open 函数中使用 for security in g.stocks: 循环,对每一只股票分别执行获取数据、计算指标和下单的逻辑。
Q: 为什么计算 OBV 时要获取比均线周期更长的数据?
A: OBV 是一个累积指标。虽然我们只需要最近 20 天的均值,但为了让 OBV 的曲线形态稳定,通常需要更长的数据窗口来计算累积值。此外,rolling(window=20) 计算均值时,前 19 个数据会产生 NaN,因此必须获取超过 20 天的数据才能得到有效的均值。
Q: 如何调整策略的灵敏度?
A: 修改 g.obv_ma_window 的值。减小该值(如改为 10)会使策略更灵敏,交易更频繁,但可能误触假信号;增大该值(如改为 60)会使策略更平稳,但可能滞后于市场变化。