问题描述
解决方案
这是一个基于 PVT (Price Volume Trend, 价量趋势指标) 的交易策略设计。
策略原理
PVT 指标类似于 OBV(能量潮),但它考虑了价格变动的幅度,而不仅仅是方向。
- 公式:$PVT = \text{昨日PVT} + \text{成交量} \times \frac{\text{今日收盘价} - \text{昨日收盘价}}{\text{昨日收盘价}}$
- 交易逻辑:
- 计算 PVT 线。
- 计算 PVT 的 N 日移动平均线 (MA)。
- 买入信号:当 PVT 向上突破 PVT 均线(金叉)。
- 卖出信号:当 PVT 向下跌破 PVT 均线(死叉)。
策略代码
以下是完整的 JoinQuant 策略代码。该策略选取一只标的(如平安银行),使用 PVT 与其 20 日均线的交叉作为买卖信号。
# -*- coding: utf-8 -*-
import pandas as pd
import numpy as np
def initialize(context):
"""
初始化函数,设定基准、手续费、滑点、全局变量等
"""
# 设定沪深300作为基准
set_benchmark('000300.XSHG')
# 开启动态复权模式(真实价格)
set_option('use_real_price', True)
# 过滤掉order系列API产生的比error级别低的log
log.set_level('order', 'error')
# 股票类每笔交易时的手续费是:买入时佣金万分之三,卖出时佣金万分之三加千分之一印花税, 每笔交易佣金最低扣5块钱
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'
# PVT均线窗口
g.ma_window = 20
# 每天开盘时运行
run_daily(market_open, time='09:30')
def market_open(context):
"""
每日交易逻辑
"""
security = g.security
# 1. 获取数据
# 我们需要足够长的数据来计算PVT和它的均线
# 获取过去 100 天的数据,确保均线计算稳定
fetch_count = 100
h_data = attribute_history(security, fetch_count, '1d', ['close', 'volume'], skip_paused=True)
# 如果数据不足,直接返回
if len(h_data) < g.ma_window + 2:
return
# 2. 计算 PVT 指标
# 计算每日涨跌幅: (今日收盘 - 昨日收盘) / 昨日收盘
pct_change = h_data['close'].pct_change()
# 计算每日PVT增量: 涨跌幅 * 成交量
# 注意:pct_change的第一项是NaN,fillna(0)处理
pvt_change = pct_change * h_data['volume']
pvt_change = pvt_change.fillna(0)
# 计算累积 PVT 值
pvt_series = pvt_change.cumsum()
# 3. 计算 PVT 的移动平均线 (MA)
pvt_ma = pvt_series.rolling(window=g.ma_window).mean()
# 4. 获取当前的 PVT 和 MA 值,以及前一日的值(用于判断交叉)
curr_pvt = pvt_series.iloc[-1]
prev_pvt = pvt_series.iloc[-2]
curr_ma = pvt_ma.iloc[-1]
prev_ma = pvt_ma.iloc[-2]
# 检查是否计算出有效值(避免NaN)
if np.isnan(curr_ma) or np.isnan(prev_ma):
return
# 5. 获取当前账户资金和持仓
cash = context.portfolio.available_cash
position = context.portfolio.positions[security]
# 获取当前标的状态(停牌判断)
current_data = get_current_data()
if current_data[security].paused:
return
# 6. 生成交易信号
# 金叉:昨天PVT在均线下,今天PVT在均线上
cross_over = (prev_pvt < prev_ma) and (curr_pvt > curr_ma)
# 死叉:昨天PVT在均线上,今天PVT在均线下
cross_under = (prev_pvt > prev_ma) and (curr_pvt < curr_ma)
# 7. 执行交易
if cross_over and cash > 0:
# 买入信号:全仓买入
log.info(f"PVT金叉 (PVT: {curr_pvt:.2f}, MA: {curr_ma:.2f}) - 买入 {security}")
order_value(security, cash)
elif cross_under and position.closeable_amount > 0:
# 卖出信号:清仓
log.info(f"PVT死叉 (PVT: {curr_pvt:.2f}, MA: {curr_ma:.2f}) - 卖出 {security}")
order_target(security, 0)
# 8. 绘图记录(可选,用于回测分析)
record(PVT=curr_pvt, PVT_MA=curr_ma)
代码解析
-
数据获取 (
attribute_history):- 我们获取了
close(收盘价) 和volume(成交量)。 - 获取长度设为 100 天,虽然均线只需要 20 天,但 PVT 是累积指标,获取更多历史数据能让曲线形态更具参考性(尽管对于均线交叉策略,相对位置比绝对数值更重要)。
- 我们获取了
-
指标计算:
- 利用 Pandas 的
pct_change()快速计算价格变化率。 pvt_change = pct_change * volume计算每日的 PVT 变动量。cumsum()计算累积的 PVT 线。rolling(window=20).mean()计算 PVT 的 20 日均线。
- 利用 Pandas 的
-
信号判断:
- 通过比较
curr_pvt(当前值) 与curr_ma(当前均线值) 以及它们前一天的状态,来判断是否发生了“金叉”或“死叉”。
- 通过比较
-
交易执行:
- 使用
order_value进行买入。 - 使用
order_target进行卖出(清仓)。
- 使用
常见问题 (Q&A)
Q1: PVT 指标和 OBV 指标有什么区别?
A: OBV (能量潮) 只是简单地根据收盘价涨跌将当日全部成交量加减到累积值中(涨加跌减)。而 PVT 考虑了涨跌的幅度。例如,股价微涨 0.1% 和大涨 5%,在 OBV 中加的量是一样的,但在 PVT 中,大涨 5% 增加的权重会大得多。因此,PVT 通常比 OBV 更能反映资金的真实意图。
Q2: 这个策略可以用于多只股票吗?
A: 可以。目前的示例代码针对单只股票 (g.security)。如果要操作多只股票,可以将股票列表放入 g.stocks,然后在 market_open 中使用 for security in g.stocks: 循环遍历每只股票进行计算和下单。
Q3: 为什么计算 PVT 时要获取较长的历史数据?
A: PVT 是一个累积指标(Cumulative)。如果只获取最近 20 天的数据,第 1 天的 PVT 会从 0 开始,这可能导致计算出的均线在初期不稳定。虽然对于“交叉信号”来说,相对趋势比绝对值重要,但保留一定的历史缓冲区(如 100 天)可以减少计算初期的噪音。
Q4: 如何优化这个策略?
A:
- 参数优化:调整均线窗口
g.ma_window(如 10日、30日)。 - 结合价格趋势:仅在股价位于长期均线(如 60日线)之上时才响应 PVT 的买入信号。
- 止损止盈:加入固定的止损逻辑(如亏损 5% 止损)以控制风险。