问题描述
解决方案
这是一个基于 CR 能量指标 (Energy Indicator) 的量化交易策略。
策略原理
CR 指标(中间意愿指标)通过比较当日最高价、最低价与昨日中间价的关系,来衡量买卖双方的力量对比。
计算公式:
- 中间价 (MID) = (昨日最高价 + 昨日最低价) / 2
- 上升值 (UP) = max(今日最高价 - 昨日中间价, 0)
- 下跌值 (DOWN) = max(昨日中间价 - 今日最低价, 0)
- CR = (N日内UP之和 / N日内DOWN之和) * 100
- CR_MA = CR 的 M 日移动平均线
交易逻辑:
本策略采用经典的 均线交叉 结合 超买超卖 逻辑:
- 买入信号:
- CR 向上突破 CR均线(金叉),且 CR < 150(避免高位追高)。
- 或者 CR < 40(严重超跌,尝试抄底)。
- 卖出信号:
- CR 向下跌破 CR均线(死叉)。
- 或者 CR > 300(严重超买,止盈离场)。
- 止损:亏损超过 10% 强制平仓。
策略代码
# -*- 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')
# 股票类每笔交易时的手续费是:买入时佣金万分之三,卖出时佣金万分之三加千分之一印花税, 每笔交易佣金最低扣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'
# CR指标参数 N
g.N = 26
# CR均线参数 M
g.M = 20
# 止损比例 (10%)
g.stop_loss_pct = 0.10
# 每天开盘时运行策略
run_daily(market_open, time='09:30')
def calculate_cr(security, n_days, m_days):
"""
计算CR指标及其均线
"""
# 获取历史数据
# 需要获取的数据长度 = 计算CR所需的N天 + 计算MA所需的M天 + 1天(用于计算昨日中间价) + 安全余量
fetch_len = n_days + m_days + 10
# 获取最高价和最低价
h_data = attribute_history(security, fetch_len, '1d', ['high', 'low', 'close'])
if len(h_data) < fetch_len:
return None, None
highs = h_data['high'].values
lows = h_data['low'].values
# 计算中间价 MID = (昨日最高 + 昨日最低) / 2
# shift(1) 代表昨日数据,但在numpy数组中,我们需要手动错位处理
# mid_prices[i] 对应的是 highs[i] 这一天的 "昨日中间价"
# 所以 mid_prices 的第 i 个元素应该是 (highs[i-1] + lows[i-1]) / 2
mid_prices = (highs[:-1] + lows[:-1]) / 2.0
# 对齐数据,去掉第一天(因为第一天没有"昨日")
curr_highs = highs[1:]
curr_lows = lows[1:]
# 计算 UP 和 DOWN
# UP = max(H - Mid, 0)
up_array = curr_highs - mid_prices
up_array[up_array < 0] = 0
# DOWN = max(Mid - L, 0)
down_array = mid_prices - curr_lows
down_array[down_array < 0] = 0
# 计算 CR
# 使用 pandas 的 rolling sum
up_series = pd.Series(up_array)
down_series = pd.Series(down_array)
sum_up = up_series.rolling(window=n_days).sum()
sum_down = down_series.rolling(window=n_days).sum()
# 处理分母为0的情况
cr_series = (sum_up / sum_down.replace(0, np.nan)) * 100
cr_series = cr_series.fillna(0) # 填充空值
# 计算 CR 的均线
cr_ma_series = cr_series.rolling(window=m_days).mean()
# 返回最近的 CR 值和 CR_MA 值 (包含当前和上一期,用于判断交叉)
return cr_series.values, cr_ma_series.values
def market_open(context):
"""
每日交易逻辑
"""
security = g.security
# 1. 计算指标
cr_values, cr_ma_values = calculate_cr(security, g.N, g.M)
if cr_values is None:
return
# 获取当前和前一天的指标值
curr_cr = cr_values[-1]
prev_cr = cr_values[-2]
curr_ma = cr_ma_values[-1]
prev_ma = cr_ma_values[-2]
# 检查数据是否有效(NaN检查)
if np.isnan(curr_ma) or np.isnan(prev_ma):
return
# 2. 获取账户信息
position = context.portfolio.positions[security]
cash = context.portfolio.available_cash
curr_price = get_current_data()[security].last_price
# 3. 止损逻辑
if position.total_amount > 0:
cost = position.avg_cost
if curr_price < cost * (1 - g.stop_loss_pct):
log.info(f"触发止损: 现价 {curr_price}, 成本 {cost}")
order_target(security, 0)
return
# 4. 信号判断
# 金叉:CR 上穿 MA
golden_cross = (prev_cr <= prev_ma) and (curr_cr > curr_ma)
# 死叉:CR 下穿 MA
dead_cross = (prev_cr >= prev_ma) and (curr_cr < curr_ma)
# 买入逻辑
# 条件A: 金叉 且 CR处于相对低位(小于150,防止高位接盘)
# 条件B: CR 极度超跌 (小于40)
if (golden_cross and curr_cr < 150) or (curr_cr < 40):
if cash > 0:
# 全仓买入
order_value(security, cash)
log.info(f"买入信号: CR={curr_cr:.2f}, MA={curr_ma:.2f}, 金叉或超跌")
# 卖出逻辑
# 条件A: 死叉
# 条件B: CR 极度超买 (大于300)
elif (dead_cross) or (curr_cr > 300):
if position.closeable_amount > 0:
# 清仓
order_target(security, 0)
log.info(f"卖出信号: CR={curr_cr:.2f}, MA={curr_ma:.2f}, 死叉或超买")
# 记录数据用于画图
record(CR=curr_cr, CR_MA=curr_ma)
常见问题 Q&A
Q1: 为什么 CR 指标比 AR/BR 指标更常用?
A: CR 指标在计算中间价时使用了“昨日”的最高价和最低价的平均值,而不是当日的开盘价。这使得 CR 指标在捕捉价格动量时比 AR/BR 稍微滞后一点,但也更加稳定,能够过滤掉部分日内的随机波动噪音。
Q2: 代码中的 attribute_history 为什么要取 n_days + m_days + 10 这么多天?
A:
- 计算 CR 需要过去 N 天的累积数据。
- 计算 CR 的均线 (MA) 需要过去 M 天的 CR 值。
- 计算 CR 本身需要用到“昨日”的中间价,所以需要额外多取 1 天数据。
- 加上 10 天是为了防止停牌等原因导致数据缺失,留出安全余量,确保 Pandas 的 rolling 计算不会因为数据不足产生 NaN。
Q3: 如何将此策略修改为多只股票轮动?
A:
- 在
initialize中定义一个股票池列表,例如g.stocks = get_index_stocks('000300.XSHG')。 - 在
market_open中使用for security in g.stocks:循环遍历每只股票。 - 将资金分配逻辑修改为等权重分配(例如
cash / len(g.stocks))或根据信号强度分配。
Q4: 策略中的参数 N=26, M=20 是怎么来的?
A: 这是 CR 指标最经典的默认参数设置。N=26 代表约一个月的交易日,M=20 代表 CR 的 20 日均线。你可以根据回测结果,在 initialize 函数中调整 g.N 和 g.M 来优化策略表现。