🚀 AI 一键生成 joinquant 策略代码

JoinQuant 实战:CR 中间意愿指标策略的 Python 实现

joinquant | 发布时间: 2025/12/24
以下内容由 EasyQuant 生成。

问题描述

帮我写一个 CR 能量指标的量化交易策略

解决方案

这是一个基于 CR 能量指标 (Energy Indicator) 的量化交易策略。

策略原理

CR 指标(中间意愿指标)通过比较当日最高价、最低价与昨日中间价的关系,来衡量买卖双方的力量对比。

计算公式:

  1. 中间价 (MID) = (昨日最高价 + 昨日最低价) / 2
  2. 上升值 (UP) = max(今日最高价 - 昨日中间价, 0)
  3. 下跌值 (DOWN) = max(昨日中间价 - 今日最低价, 0)
  4. CR = (N日内UP之和 / N日内DOWN之和) * 100
  5. CR_MA = CR 的 M 日移动平均线

交易逻辑:
本策略采用经典的 均线交叉 结合 超买超卖 逻辑:

  1. 买入信号
    • CR 向上突破 CR均线(金叉),且 CR < 150(避免高位追高)。
    • 或者 CR < 40(严重超跌,尝试抄底)。
  2. 卖出信号
    • CR 向下跌破 CR均线(死叉)。
    • 或者 CR > 300(严重超买,止盈离场)。
  3. 止损:亏损超过 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:

  1. 计算 CR 需要过去 N 天的累积数据。
  2. 计算 CR 的均线 (MA) 需要过去 M 天的 CR 值。
  3. 计算 CR 本身需要用到“昨日”的中间价,所以需要额外多取 1 天数据。
  4. 加上 10 天是为了防止停牌等原因导致数据缺失,留出安全余量,确保 Pandas 的 rolling 计算不会因为数据不足产生 NaN。

Q3: 如何将此策略修改为多只股票轮动?
A:

  1. initialize 中定义一个股票池列表,例如 g.stocks = get_index_stocks('000300.XSHG')
  2. market_open 中使用 for security in g.stocks: 循环遍历每只股票。
  3. 将资金分配逻辑修改为等权重分配(例如 cash / len(g.stocks))或根据信号强度分配。

Q4: 策略中的参数 N=26, M=20 是怎么来的?
A: 这是 CR 指标最经典的默认参数设置。N=26 代表约一个月的交易日,M=20 代表 CR 的 20 日均线。你可以根据回测结果,在 initialize 函数中调整 g.Ng.M 来优化策略表现。