问题描述
解决方案
这是一个基于 QMT 平台的 OBV (On-Balance Volume, 能量潮) 指标交易策略。
策略逻辑
OBV 指标通过统计成交量的变动趋势来推测股价趋势。
- OBV 计算:
- 如果今日收盘价 > 昨日收盘价,则今日 OBV = 昨日 OBV + 今日成交量。
- 如果今日收盘价 < 昨日收盘价,则今日 OBV = 昨日 OBV - 今日成交量。
- 如果今日收盘价 = 昨日收盘价,则今日 OBV = 昨日 OBV。
- 交易信号(OBV 均线交叉策略):
- 买入信号:当 OBV 线 上穿 OBV 的 N 日移动平均线(MAOBV)时,视为资金流入,看涨买入。
- 卖出信号:当 OBV 线 下穿 OBV 的 N 日移动平均线(MAOBV)时,视为资金流出,看跌卖出。
策略代码
# -*- coding: gbk -*-
import pandas as pd
import numpy as np
def init(ContextInfo):
"""
策略初始化函数
"""
# 1. 设置操作标的(示例:平安银行)
ContextInfo.stock_code = '000001.SZ'
# 2. 设置股票池
ContextInfo.set_universe([ContextInfo.stock_code])
# 3. 策略参数设置
ContextInfo.obv_ma_period = 30 # OBV均线周期
ContextInfo.trade_amount = 100 # 每次交易股数
ContextInfo.account_id = 'YOUR_ACCOUNT_ID' # 请替换为您的资金账号
# 4. 设置账号(实盘/回测必须)
ContextInfo.set_account(ContextInfo.account_id)
print("策略初始化完成,监控标的: {}, OBV均线周期: {}".format(ContextInfo.stock_code, ContextInfo.obv_ma_period))
def handlebar(ContextInfo):
"""
K线周期运行函数
"""
# 获取当前正在处理的K线索引
index = ContextInfo.barpos
# 获取当前图表的时间
realtime = ContextInfo.get_bar_timetag(index)
# 仅在最后一根K线(最新行情)或回测模式下运行逻辑
# 注意:如果是实盘,通常只在最新K线运行;如果是回测,每根K线都运行
if not ContextInfo.is_last_bar():
# 回测模式下,为了提高速度,可以只在必要时计算,或者直接return让其按序列运行
# 这里为了演示逻辑完整性,不做return,但在实盘中通常需要判断 is_last_bar
pass
# 1. 获取历史行情数据
# 获取长度:均线周期 + 缓冲数据(用于计算OBV累积)
data_len = ContextInfo.obv_ma_period + 60
# 使用 get_market_data_ex 获取数据 (推荐接口)
# 注意:period='1d' 表示日线,dividend_type='front' 前复权
market_data = ContextInfo.get_market_data_ex(
fields=['close', 'volume'],
stock_code=[ContextInfo.stock_code],
period='1d',
count=data_len,
dividend_type='front'
)
if ContextInfo.stock_code not in market_data:
return
df = market_data[ContextInfo.stock_code]
# 数据长度不足无法计算指标时返回
if len(df) < ContextInfo.obv_ma_period + 1:
return
# 2. 计算 OBV 指标
# 计算价格变化:今日收盘 - 昨日收盘
df['price_change'] = df['close'].diff()
# 生成符号向量:涨为1,跌为-1,平为0
# np.sign: x>0 -> 1, x<0 -> -1, x=0 -> 0
df['direction'] = np.sign(df['price_change'])
# 修正:第一行 diff 后是 NaN,填充为 0
df['direction'] = df['direction'].fillna(0)
# 计算带符号的成交量
df['signed_volume'] = df['direction'] * df['volume']
# 计算 OBV (累积和)
df['obv'] = df['signed_volume'].cumsum()
# 计算 OBV 的移动平均线 (MAOBV)
df['obv_ma'] = df['obv'].rolling(window=ContextInfo.obv_ma_period).mean()
# 3. 获取最新信号数据
# 获取最后两个数据点用于判断交叉
curr_obv = df['obv'].iloc[-1]
curr_ma = df['obv_ma'].iloc[-1]
prev_obv = df['obv'].iloc[-2]
prev_ma = df['obv_ma'].iloc[-2]
# 打印调试信息 (可选)
# print("时间: {}, OBV: {:.2f}, MA: {:.2f}".format(timetag_to_datetime(realtime, '%Y-%m-%d'), curr_obv, curr_ma))
# 4. 获取当前持仓状态
positions = get_trade_detail_data(ContextInfo.account_id, 'stock', 'position')
current_vol = 0
for pos in positions:
if pos.m_strInstrumentID == ContextInfo.stock_code:
current_vol = pos.m_nVolume
break
# 5. 交易逻辑判断
# 金叉:OBV 上穿 MA (昨日 OBV < 昨日 MA 且 今日 OBV > 今日 MA)
buy_signal = (prev_obv < prev_ma) and (curr_obv > curr_ma)
# 死叉:OBV 下穿 MA (昨日 OBV > 昨日 MA 且 今日 OBV < 今日 MA)
sell_signal = (prev_obv > prev_ma) and (curr_obv < curr_ma)
# 执行买入
if buy_signal:
if current_vol == 0: # 简单逻辑:无持仓则买入
print(">>> 产生买入信号: OBV金叉")
# 23: 买入, 1101: 单股单账号, 5: 最新价
passorder(23, 1101, ContextInfo.account_id, ContextInfo.stock_code, 5, -1, ContextInfo.trade_amount, ContextInfo)
# 执行卖出
elif sell_signal:
if current_vol > 0: # 简单逻辑:有持仓则卖出
print(">>> 产生卖出信号: OBV死叉")
# 24: 卖出, 1101: 单股单账号, 5: 最新价
passorder(24, 1101, ContextInfo.account_id, ContextInfo.stock_code, 5, -1, current_vol, ContextInfo)
代码使用说明
- 账号设置:
- 请务必将代码中的
ContextInfo.account_id = 'YOUR_ACCOUNT_ID'替换为您真实的 QMT 资金账号。
- 请务必将代码中的
- 标的与参数:
ContextInfo.stock_code:修改为您想交易的股票代码(如 '600000.SH')。ContextInfo.obv_ma_period:OBV 均线的周期,默认为 30 日,可根据需要调整。
- 数据获取:
- 使用了
get_market_data_ex接口,这是 QMT 推荐的高效数据接口。 - 使用了
pandas库进行向量化计算,计算 OBV 及其均线。
- 使用了
- 交易执行:
- 使用了
passorder函数进行下单。 opType23 代表买入,24 代表卖出。prType5 代表使用最新价下单(实盘中可能需要考虑滑点或使用对手价)。
- 使用了
- 运行模式:
- 该策略可以在 回测模式 下查看历史表现。
- 在 实盘/模拟盘 模式下,请确保在“模型交易”界面运行,并勾选正确的账号。
常见问题 Q&A
Q: 为什么 OBV 计算需要获取比均线周期更多的数据?
A: OBV 是一个累积指标,虽然理论上它需要从上市第一天开始累积,但在量化计算中,我们通常取一段足够长的时间窗口来计算相对变化。此外,计算 MA(移动平均)需要前 N 天的数据,因此获取的数据长度必须大于 obv_ma_period。
Q: 如何在盘中实时触发交易?
A: 上述代码基于 K 线结束时的收盘价计算。如果需要在盘中实时触发(例如分钟线或 Tick 级),需要将 period 改为 '1m' 或更小周期,或者在 passorder 中设置 quickTrade=1(立即下单),但这需要更复杂的信号过滤以防止信号闪烁。
Q: 报错 NameError: name 'get_trade_detail_data' is not defined 怎么办?
A: 请确保您是在 QMT 的完整 Python 环境中运行。如果是回测,某些交易函数可能表现不同。在实盘模式下,这些函数是内置在全局命名空间中的。如果是在纯本地 Python IDE 调试,无法调用这些 QMT 专用函数。