🚀 AI 一键生成 joinquant 策略代码

JoinQuant 择时策略:如何计算并使用市场宽度指标 ADL(腾落线)?

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

问题描述

如何计算市场的腾落线(ADL)指标,并用它来判断大盘的买卖点?

解决方案

这是一个关于量化交易策略设计的问题。

什么是腾落线(ADL)?

腾落线 (Advance-Decline Line, ADL) 是一个衡量市场广度(Market Breadth)的累积指标。它通过计算每天上涨股票家数与下跌股票家数的差值,并将其累加到前一天的数值上,来判断大盘的内部强势或弱势。

计算公式:
$$今日ADL = 昨日ADL + (今日上涨家数 - 今日下跌家数)$$

指标含义:

  1. 趋势确认:当大盘指数上涨,且ADL也同步上涨时,说明上涨是由大多数股票推动的,趋势健康。
  2. 背离信号(买卖点)
    • 顶背离(卖点):大盘指数创新高,但ADL未能创新高(甚至下降),说明上涨仅由少数权重股拉动,市场内部虚弱,可能见顶。
    • 底背离(买点):大盘指数创新低,但ADL不再创新低(甚至上升),说明虽然指数在跌,但多数股票开始止跌回升,可能见底。

策略实现逻辑

在聚宽(JoinQuant)中实现ADL策略,我们需要以下步骤:

  1. 确定股票池:通常选择全市场股票或代表性指数(如沪深300或中证500)的成分股作为样本。
  2. 每日计算:在每日收盘前(如14:50),获取股票池中所有股票的当日涨跌情况。
  3. 累积ADL:计算净上涨家数,更新全局ADL值。
  4. 生成信号
    • 由于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)

代码关键点解析

  1. 数据获取 (market_scan_and_trade):

    • 我们使用 get_current_data() 获取当前的实时价格。
    • 我们使用 history(1, ...) 获取昨天的收盘价。
    • 注意:不能直接用 current_data[stock].day_open 当作昨收,因为那是今天的开盘价。比较 last_pricehistory 获取的昨日收盘价才是判断今日涨跌的正确方法。
  2. ADL 计算:

    • net_advance = 上涨家数 - 下跌家数
    • g.adl_value 是一个全局变量,它会随着回测时间的推移不断累加。初始值设为0并不影响策略逻辑,因为我们看的是相对趋势(和均线的关系),而不是绝对值。
  3. 交易信号:

    • 单纯看ADL涨跌容易产生过多杂波。
    • 代码中使用了 ADL与其20日均线(MA20)的交叉 作为买卖信号。
    • 当ADL站在均线之上,代表市场广度处于扩张期,做多;反之做空。
  4. 标的选择:

    • 计算ADL用的是 000300.XSHG (沪深300) 的成分股。
    • 实际交易的是 510300.XSHG (沪深300ETF)。

如何优化?

  1. 扩大样本:将 g.scope_index 改为中证800或全A股,更能反映整体市场情绪,但回测速度会变慢。
  2. 加入背离判断:代码目前是趋势跟随(均线交叉)。你可以进一步开发逻辑:当指数创新高(current_index_price > 20_day_high)但ADL未创新高时,强制平仓。
  3. 结合指数均线:要求 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和其均线的走势图。