问题描述
解决方案
这是一个非常经典的日内均值回归策略(Mean Reversion Strategy)。
策略逻辑分析
该策略的核心假设是:价格会围绕日内均线(通常使用VWAP,即成交量加权平均价)波动。当价格大幅偏离均线时,有较大概率回归均值。
- 基准指标:日内成交量加权平均价 (VWAP) = 当日累计成交额 / 当日累计成交量。
- 买入信号:当前价格 < 日内均线 * (1 - 阈值) -> 认为超卖,买入。
- 卖出信号:当前价格 > 日内均线 * (1 + 阈值) -> 认为超买,卖出。
- 风控与清算:
- T+1 限制:A股市场实行T+1制度,当日买入的股票当日无法卖出。要实现日内交易(T+0),你需要预先持有底仓(底仓套利),或者交易ETF/可转债等支持T+0的品种。
- 尾盘清仓:如果是做日内波段,通常会在尾盘(如14:55)强制平掉当日的投机仓位,或者恢复到底仓数量。
PTrade 策略代码实现
以下代码实现了一个基于 分钟级别 的日内均线偏离策略。
注意:为了演示方便,本策略假设你交易的是支持T+0的标的(如可转债、ETF),或者你已有底仓进行T+0操作。如果是普通A股且无底仓,当日买入后无法卖出。
import numpy as np
def initialize(context):
"""
初始化函数,设置策略参数和股票池
"""
# 设置要操作的标的,这里以恒生电子为例
# 如果是做T+0,建议选择波动大且成交活跃的标的
g.security = '600570.SS'
set_universe(g.security)
# 偏离阈值设置 (例如 1.5%)
# 当价格偏离均线超过这个比例时触发交易
g.threshold = 0.015
# 每次交易的数量
g.trade_amount = 500
# 设置每日尾盘定时任务,用于平仓或恢复仓位
run_daily(context, close_intraday_positions, time='14:55')
# 记录是否已经开仓的标记,避免同一方向重复下单
g.has_open_position = False
def before_trading_start(context, data):
"""
盘前处理
"""
# 每日开盘前重置开仓标记
g.has_open_position = False
log.info("盘前准备完毕,等待开盘...")
def handle_data(context, data):
"""
盘中每分钟运行一次
"""
# 1. 时间过滤:
# 开盘前几分钟数据波动剧烈且均线不稳定,跳过不交易
# 尾盘留给 close_intraday_positions 处理,此处不交易
current_time = context.blotter.current_dt.strftime('%H:%M')
if current_time < '09:40' or current_time >= '14:50':
return
security = g.security
# 2. 获取当日历史分钟数据,用于计算日内VWAP
# 获取从开盘到现在的数据,最大取240根分钟K线
# include=True 表示包含当前这一分钟的数据
hist_df = get_history(240, frequency='1m', field=['volume', 'money'], security_list=security, include=True)
# 过滤出今天的数据(PTrade返回的DataFrame索引是datetime)
today_date = context.blotter.current_dt.date()
today_data = hist_df[hist_df.index.date == today_date]
if len(today_data) == 0:
return
# 3. 计算日内成交量加权平均价 (VWAP)
total_money = today_data['money'].sum()
total_volume = today_data['volume'].sum()
if total_volume == 0:
return
vwap = total_money / total_volume
# 4. 获取当前最新价格
current_price = data[security]['close']
# 计算上下轨
upper_bound = vwap * (1 + g.threshold)
lower_bound = vwap * (1 - g.threshold)
# 5. 交易逻辑
# 获取当前持仓
position = get_position(security)
curr_amount = position.amount
enable_amount = position.enable_amount # 可用持仓(T+1限制下,只有昨仓可用)
# 情况A: 价格高于上轨 -> 卖出 (做空/止盈)
if current_price > upper_bound:
# 如果我们持有仓位(无论是底仓还是今日买入的),尝试卖出
# 注意:如果是A股,必须有 enable_amount > 0 才能卖
if enable_amount >= g.trade_amount:
log.info("价格 %.2f 高于日内均线 %.2f 超过阈值,触发卖出" % (current_price, vwap))
order(security, -g.trade_amount)
g.has_open_position = True
else:
# 如果没有可用持仓,说明可能是T+1限制或者空仓
pass
# 情况B: 价格低于下轨 -> 买入 (做多/抄底)
elif current_price < lower_bound:
# 检查资金是否足够
if context.portfolio.cash > current_price * g.trade_amount:
log.info("价格 %.2f 低于日内均线 %.2f 超过阈值,触发买入" % (current_price, vwap))
order(security, g.trade_amount)
g.has_open_position = True
def close_intraday_positions(context):
"""
尾盘处理函数:14:55 运行
如果是做日内T+0,这里需要把当日多出来的仓位卖掉,或者把卖出的仓位买回来。
这里演示简单的逻辑:如果有持仓,且想保持空仓过夜,则全部卖出。
"""
log.info("尾盘时间,开始检查仓位...")
security = g.security
position = get_position(security)
# 示例逻辑:假设我们不留隔夜仓(适用于ETF/可转债等T+0品种)
# 如果是股票做T+0,这里应该恢复到昨天的底仓数量
if position.amount > 0 and position.enable_amount > 0:
log.info("尾盘平仓: 卖出 %s" % security)
order_target(security, 0)
代码关键点解释
-
get_history获取当日数据:- 我们使用
get_history(240, ...)获取足够长的分钟线,然后通过hist_df.index.date == today_date筛选出仅属于今天的数据。这是计算日内VWAP(成交量加权均价)的标准方法。 VWAP = sum(money) / sum(volume):相比于简单的算术平均(MA),VWAP更能代表市场的平均持仓成本,是机构常用的日内基准。
- 我们使用
-
阈值 (
g.threshold):- 代码中设置为
0.015(1.5%)。这意味着价格必须偏离均线 1.5% 才会触发交易。 - 如果阈值太小,交易会过于频繁,手续费会吞噬利润;如果阈值太大,可能一天都没有交易机会。建议根据标的的历史波动率进行调整。
- 代码中设置为
-
时间过滤:
09:40之前不交易:开盘初期的均线计算样本太少,VWAP 极不稳定,容易产生误导信号。14:50之后不交易:临近收盘,防止产生新的被套仓位,准备进入清算阶段。
-
close_intraday_positions:- 这是日内策略的重要组成部分。如果你做的是纯日内投机,必须在收盘前平仓。
- 特别提示:如果你交易的是普通股票(T+1),你不能在买入当天卖出。这种策略通常用于底仓T+0(即你昨天持有1000股,今天低买500股,高卖500股,收盘时持仓仍为1000股,赚取差价)。
常见问题 Q&A
Q: 为什么我回测时没有成交?
A:
- 检查
g.threshold是否设置过大,导致价格从未触及触发线。 - 检查
g.security是否处于停牌状态。 - 如果是股票回测,检查是否有可用资金或可用持仓(卖出时需要昨仓)。
Q: 如何将此策略改为简单的算术平均线(MA)而不是VWAP?
A:
将 handle_data 中计算 VWAP 的部分修改为:
# 计算简单的日内均价
vwap = today_data['close'].mean()
Q: 如何在实盘中提高成交速度?
A:
上述代码是分钟级别的。如果需要更灵敏的反应,可以使用 tick_data 函数进行 Tick 级别的开发,或者在 initialize 中使用 run_interval(context, func, seconds=3) 来每3秒检测一次价格。
Q: 股票T+1怎么解决?
A:
你需要预先在 initialize 中设置底仓(回测中可用 set_yesterday_position),或者在实盘账户中预先买入股票。交易逻辑需要修改为:
- 高抛:卖出底仓。
- 低吸:买回底仓。
- 收盘:检查持仓数量是否等于底仓数量,如果不等则补齐或卖出。