问题描述
解决方案
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)来判断买卖点:
- 开仓条件:CHOP < 38.2(确认有趋势) 且 收盘价 > MA20(确认是上涨趋势)。
- 平仓条件: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. 策略分析与注意事项
- 滞后性:CHOP 是一个基于过去 $N$ 天数据的统计指标,因此它确认趋势时通常会有一定的滞后。当 CHOP 跌破 38.2 时,趋势通常已经运行了一段时间。
- 方向辅助:代码中必须引入均线(MA)或其他方向性指标。因为 CHOP < 38.2 既可能是暴涨也可能是暴跌,它只代表“有趋势”。
- 参数调整:
- 周期 $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)。