问题描述
解决方案
这是一个关于量化交易策略设计的问题。
什么是腾落线(ADL)?
腾落线 (Advance-Decline Line, ADL) 是一个衡量市场广度(Market Breadth)的累积指标。它通过计算每天上涨股票家数与下跌股票家数的差值,并将其累加到前一天的数值上,来判断大盘的内部强势或弱势。
计算公式:
$$今日ADL = 昨日ADL + (今日上涨家数 - 今日下跌家数)$$
指标含义:
- 趋势确认:当大盘指数上涨,且ADL也同步上涨时,说明上涨是由大多数股票推动的,趋势健康。
- 背离信号(买卖点):
- 顶背离(卖点):大盘指数创新高,但ADL未能创新高(甚至下降),说明上涨仅由少数权重股拉动,市场内部虚弱,可能见顶。
- 底背离(买点):大盘指数创新低,但ADL不再创新低(甚至上升),说明虽然指数在跌,但多数股票开始止跌回升,可能见底。
策略实现逻辑
在聚宽(JoinQuant)中实现ADL策略,我们需要以下步骤:
- 确定股票池:通常选择全市场股票或代表性指数(如沪深300或中证500)的成分股作为样本。
- 每日计算:在每日收盘前(如14:50),获取股票池中所有股票的当日涨跌情况。
- 累积ADL:计算净上涨家数,更新全局ADL值。
- 生成信号:
- 由于ADL是累积值,绝对值没有意义,通常配合**均线(MA)**使用。
- 买入:ADL值上穿ADL的N日均线。
- 卖出:ADL值下穿ADL的N日均线。
策略代码实现
以下是一个完整的策略代码。该策略以沪深300成分股为样本计算ADL,并对**沪深300指数(或ETF)**进行交易。
# -*- coding: utf-8 -*-
import jqdata
import pandas as pd
import numpy as np
def initialize(context):
"""
初始化函数
"""
# 设定基准
set_benchmark('000300.XSHG')
# 开启动态复权模式(真实价格)
set_option('use_real_price', True)
# 过滤掉order系列API产生的比error级别低的log
log.set_level('order', 'error')
# 设定手续费
set_order_cost(OrderCost(close_tax=0.001, open_commission=0.0003, close_commission=0.0003, min_commission=5), type='stock')
# --- 策略参数设置 ---
# 用于计算ADL的股票池范围:这里使用沪深300成分股代表大盘
# 如果想计算全A股,可以使用 get_all_securities(['stock']).index.tolist(),但回测速度会变慢
g.scope_index = '000300.XSHG'
# 要交易的标的(这里直接交易沪深300ETF作为示例,也可以交易股指期货或一篮子股票)
g.trade_target = '510300.XSHG'
# ADL指标的全局累积值,初始设为0
g.adl_value = 0
# 记录ADL历史值用于计算均线
g.adl_history = []
# ADL均线窗口,用于判断趋势
g.ma_window = 20
# 每日收盘前运行策略
run_daily(market_scan_and_trade, time='14:50')
def market_scan_and_trade(context):
"""
每日定时运行:计算ADL并交易
"""
# 1. 获取股票池(沪深300成分股)
stocks = get_index_stocks(g.scope_index)
# 2. 获取当前数据(最新价和昨收价)
# 使用 get_price 获取数据,因为 get_current_data 在回测中可能较慢且不易批量处理
# 获取当前分钟的快照数据是不够的,我们需要判断今日相对于昨日的涨跌
# 这里获取过去1天的数据是不够的,因为我们需要pre_close。
# 最简单的方法是获取当前快照
current_data = get_current_data()
advancers = 0 # 上涨家数
decliners = 0 # 下跌家数
unchanged = 0 # 平盘家数
valid_stocks = []
for stock in stocks:
# 过滤停牌股票
if current_data[stock].paused:
continue
# 获取价格
price = current_data[stock].last_price
pre_close = current_data[stock].day_open # 注意:get_current_data的day_open在回测中通常指今日开盘价,这里我们需要昨收
# 更准确的方式是使用 attribute_history 获取昨收,或者直接用 current_data[stock].high_limit / 1.1 (不推荐)
# 修正:使用 get_current_data 实际上包含 last_price 和 pre_close (但在某些回测模式下pre_close可能不准)
# 为了准确性,我们批量获取过去两个交易日的收盘价来比较
valid_stocks.append(stock)
if not valid_stocks:
return
# 批量获取历史收盘价来判断涨跌(获取过去1天的数据,包含今天尚未收盘的价格和昨天的收盘价是不行的,history取不到今天)
# 正确逻辑:获取当前价格,并获取昨天的收盘价
# 获取昨日收盘价
h_data = history(1, unit='1d', field='close', security_list=valid_stocks, df=True)
if h_data.empty:
return
# 遍历计算涨跌家数
for stock in valid_stocks:
if stock not in h_data.columns:
continue
current_price = current_data[stock].last_price
last_close = h_data[stock].iloc[-1] # 昨收
if current_price > last_close:
advancers += 1
elif current_price < last_close:
decliners += 1
else:
unchanged += 1
# 3. 计算今日净腾落数
net_advance = advancers - decliners
# 4. 更新累积ADL值
g.adl_value += net_advance
# 将今日ADL存入历史列表
g.adl_history.append(g.adl_value)
# 保持历史列表长度,避免无限增长占用内存,但要足够计算MA
if len(g.adl_history) > g.ma_window + 100:
g.adl_history = g.adl_history[-g.ma_window:]
# 5. 交易逻辑
# 如果历史数据不足以计算均线,则跳过
if len(g.adl_history) < g.ma_window:
return
# 计算ADL的均线
adl_series = pd.Series(g.adl_history)
adl_ma = adl_series.rolling(window=g.ma_window).mean().iloc[-1]
current_adl = g.adl_value
# 获取当前持仓
position = context.portfolio.positions[g.trade_target].total_amount
cash = context.portfolio.available_cash
# 打印日志方便调试
# log.info(f"Date: {context.current_dt.date()}, Adv: {advancers}, Dec: {decliners}, ADL: {current_adl}, MA_ADL: {adl_ma}")
# 信号判断:ADL 上穿 均线 -> 买入
if current_adl > adl_ma and position == 0:
log.info(f"ADL金叉均线 (ADL:{current_adl} > MA:{adl_ma}),市场广度转强,买入。")
order_value(g.trade_target, cash)
# 信号判断:ADL 下穿 均线 -> 卖出
elif current_adl < adl_ma and position > 0:
log.info(f"ADL死叉均线 (ADL:{current_adl} < MA:{adl_ma}),市场广度转弱,卖出。")
order_target(g.trade_target, 0)
# 6. 绘图记录
record(ADL=current_adl, ADL_MA=adl_ma)
代码关键点解析
-
数据获取 (
market_scan_and_trade):- 我们使用
get_current_data()获取当前的实时价格。 - 我们使用
history(1, ...)获取昨天的收盘价。 - 注意:不能直接用
current_data[stock].day_open当作昨收,因为那是今天的开盘价。比较last_price和history获取的昨日收盘价才是判断今日涨跌的正确方法。
- 我们使用
-
ADL 计算:
net_advance = 上涨家数 - 下跌家数。g.adl_value是一个全局变量,它会随着回测时间的推移不断累加。初始值设为0并不影响策略逻辑,因为我们看的是相对趋势(和均线的关系),而不是绝对值。
-
交易信号:
- 单纯看ADL涨跌容易产生过多杂波。
- 代码中使用了 ADL与其20日均线(MA20)的交叉 作为买卖信号。
- 当ADL站在均线之上,代表市场广度处于扩张期,做多;反之做空。
-
标的选择:
- 计算ADL用的是
000300.XSHG(沪深300) 的成分股。 - 实际交易的是
510300.XSHG(沪深300ETF)。
- 计算ADL用的是
如何优化?
- 扩大样本:将
g.scope_index改为中证800或全A股,更能反映整体市场情绪,但回测速度会变慢。 - 加入背离判断:代码目前是趋势跟随(均线交叉)。你可以进一步开发逻辑:当指数创新高(
current_index_price > 20_day_high)但ADL未创新高时,强制平仓。 - 结合指数均线:要求 ADL 金叉 且 指数本身也站上均线,形成双重确认,减少假信号。
Q&A
Q: 为什么初始ADL设为0,这会影响结果吗?
A: 不会。ADL是一个相对指标,它的绝对数值没有意义(取决于你从哪一天开始算)。我们关注的是它的斜率以及它与自身均线的相对位置。无论初始值是0还是10000,均线交叉的信号时间点是一样的。
Q: 可以在分钟级别回测中使用吗?
A: 可以,但ADL通常是日线级别的指标。如果在分钟级回测中,建议依然在每日收盘前(如14:50)计算一次并交易,或者使用 run_daily 配合分钟回测。如果在盘中实时计算ADL(例如每分钟计算),波动会非常剧烈,需要更平滑的处理逻辑。
Q: 如何查看ADL曲线?
A: 代码最后一行使用了 record(ADL=current_adl, ADL_MA=adl_ma)。回测结束后,在回测详情页面的收益图下方,会有一个“自定义数据”图表,勾选即可看到ADL和其均线的走势图。