🚀 AI 一键生成 joinquant 策略代码

使用 Choppiness Index 识别盘整与趋势:JoinQuant 策略优化

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

问题描述

如何用 Choppiness Index 区分震荡和趋势行情?

解决方案

Choppiness Index (CHOP) 策略应用指南

Choppiness Index(CHOP,波动指数)是由澳大利亚大宗商品交易员 E.W. Dreiss 开发的一种指标,用于确定市场是处于趋势中还是处于**震荡(横盘)**阶段。

CHOP 的核心理念是:它不预测市场方向,只衡量趋势的强度。

1. 原理与计算公式

CHOP 利用“真实波幅(True Range)”和“价格极差”之间的关系来通过分形几何原理评估市场的混乱程度。

公式逻辑:
$$ CHOP = 100 \times \frac{\log_{10}(\frac{\sum_{i=1}^{n} TR_i}{MaxHigh_n - MinLow_n})}{\log_{10}(n)} $$

其中:

  • $n$:回顾周期(通常为 14)。
  • $TR$:真实波幅(True Range)。
  • $\sum TR$:过去 $n$ 天真实波幅的总和。
  • $MaxHigh_n$:过去 $n$ 天的最高价。
  • $MinLow_n$:过去 $n$ 天的最低价。

2. 如何区分震荡与趋势

CHOP 的值介于 0 到 100 之间,通常使用斐波那契数列中的数值作为阈值:

  • 高数值(通常 > 61.8):表示市场处于深度震荡或横盘整理阶段。此时价格没有明确方向,适合观望或使用区间交易策略(如布林带回归)。
  • 低数值(通常 < 38.2):表示市场处于强趋势阶段。此时市场正在单边上涨或下跌,适合使用趋势跟随策略(如均线突破)。
  • 中间区域:市场处于过渡期。

3. JoinQuant 策略代码实现

以下是一个完整的策略示例。该策略结合了 CHOP 指标和简单的均线(MA)来判断买卖点:

  1. 开仓条件:CHOP < 38.2(确认有趋势) 且 收盘价 > MA20(确认是上涨趋势)。
  2. 平仓条件:CHOP > 61.8(确认进入震荡,趋势可能结束) 或 收盘价 < MA20(止损/止盈)。
# -*- coding: utf-8 -*-
import numpy as np
import pandas as pd
import math

def initialize(context):
    """
    初始化函数
    """
    # 设定要操作的股票(例如:沪深300ETF)
    g.security = '510300.XSHG'
    
    # 设定基准
    set_benchmark('000300.XSHG')
    
    # 开启动态复权模式(真实价格)
    set_option('use_real_price', True)
    
    # 过滤掉order系列API产生的比error级别低的log
    log.set_level('order', 'error')
    
    # CHOP指标参数
    g.n = 14  # 周期
    g.high_threshold = 61.8 # 震荡阈值
    g.low_threshold = 38.2  # 趋势阈值
    
    # 均线参数(用于辅助判断方向)
    g.ma_window = 20
    
    # 按天运行
    run_daily(market_open, time='every_bar')

def calculate_chop(security, n, context):
    """
    计算 Choppiness Index
    """
    # 获取过去 n+1 天的数据(计算TR需要前一日收盘价)
    # 字段: high, low, close
    data = attribute_history(security, n + 1, '1d', ['high', 'low', 'close'])
    
    if len(data) < n + 1:
        return None
        
    highs = data['high'].values
    lows = data['low'].values
    closes = data['close'].values
    
    # 计算真实波幅 (True Range)
    # TR = Max(High - Low, abs(High - PrevClose), abs(Low - PrevClose))
    tr_list = []
    # 从索引1开始遍历(因为需要前一天的收盘价)
    for i in range(1, len(closes)):
        h = highs[i]
        l = lows[i]
        prev_c = closes[i-1]
        
        tr = max(h - l, abs(h - prev_c), abs(l - prev_c))
        tr_list.append(tr)
    
    # 1. 计算过去 n 天 TR 的总和
    sum_tr = sum(tr_list[-n:])
    
    # 2. 计算过去 n 天的最高价和最低价之差 (Range)
    # 注意:这里取的是最近 n 天的数据
    max_high = max(highs[-n:])
    min_low = min(lows[-n:])
    range_hl = max_high - min_low
    
    # 避免除以零
    if range_hl == 0:
        return 50.0 # 返回中性值
        
    # 3. 计算 CHOP
    # 公式: 100 * LOG10(Sum(TR, n) / (MaxHigh(n) - MinLow(n))) / LOG10(n)
    try:
        chop = 100 * math.log10(sum_tr / range_hl) / math.log10(n)
    except ValueError:
        # 处理数学异常
        return 50.0
        
    return chop

def market_open(context):
    security = g.security
    
    # 1. 计算 CHOP 指标
    chop_value = calculate_chop(security, g.n, context)
    
    if chop_value is None:
        return
        
    # 2. 获取均线数据用于判断方向 (CHOP本身不含方向信息)
    close_data = attribute_history(security, g.ma_window, '1d', ['close'])
    current_price = close_data['close'][-1]
    ma_value = close_data['close'].mean()
    
    # 获取当前持仓
    position = context.portfolio.positions[security]
    
    # 记录日志
    # log.info(f"日期: {context.current_dt.date()}, CHOP: {chop_value:.2f}, 价格: {current_price}, MA20: {ma_value}")
    
    # --- 交易逻辑 ---
    
    # 卖出逻辑:
    # 1. 如果 CHOP > 61.8,说明市场进入深度震荡,趋势可能终结,平仓观望
    # 2. 或者价格跌破均线(止损/趋势反转)
    if position.closeable_amount > 0:
        if chop_value > g.high_threshold:
            log.info(f"市场进入震荡 (CHOP={chop_value:.2f} > {g.high_threshold}),平仓")
            order_target(security, 0)
        elif current_price < ma_value:
            log.info(f"价格跌破均线,平仓")
            order_target(security, 0)
            
    # 买入逻辑:
    # 1. 如果 CHOP < 38.2,说明市场处于强趋势状态
    # 2. 并且 价格 > 均线,说明是上涨趋势
    else:
        if chop_value < g.low_threshold and current_price > ma_value:
            log.info(f"趋势确立 (CHOP={chop_value:.2f} < {g.low_threshold}) 且 价格在均线上方,全仓买入")
            order_value(security, context.portfolio.available_cash)

4. 策略分析与注意事项

  1. 滞后性:CHOP 是一个基于过去 $N$ 天数据的统计指标,因此它确认趋势时通常会有一定的滞后。当 CHOP 跌破 38.2 时,趋势通常已经运行了一段时间。
  2. 方向辅助:代码中必须引入均线(MA)或其他方向性指标。因为 CHOP < 38.2 既可能是暴涨也可能是暴跌,它只代表“有趋势”。
  3. 参数调整
    • 周期 $N=14$ 是标准设置。缩短周期会使指标更敏感,但假信号增多;延长周期会更平滑,但滞后更严重。
    • 阈值 61.8 和 38.2 是经典设置,但在高波动市场(如加密货币或某些小盘股)中,可能需要调整为 60/40 或 70/30。

Q&A

Q1: CHOP 指标能单独用来做买卖信号吗?
A1: 不能。CHOP 只能告诉你“现在有没有趋势”,不能告诉你“趋势是涨还是跌”。必须配合均线、MACD 或价格形态来确定方向。

Q2: 为什么代码中计算 TR 时要取 n+1 天的数据?
A2: 真实波幅(TR)的计算公式中包含 abs(High - PrevClose),即当天的最高价减去前一天的收盘价。因此,要计算过去 14 天的 TR,实际上需要获取 15 天的历史数据。

Q3: 当 CHOP 值在 38.2 到 61.8 之间时应该做什么?
A3: 这通常被视为过渡区域。如果是持仓状态,通常选择继续持有,直到触发平仓信号;如果是空仓状态,通常选择观望,等待明确的趋势信号(CHOP < 38.2)或明确的震荡边界(CHOP > 61.8)。