用Python实战DMI指标:量化识别趋势强度的进阶策略

在量化交易领域,技术指标的应用往往陷入两个极端:要么过度依赖简单的金叉死叉信号,要么陷入复杂的理论分析而缺乏可执行的代码实现。今天我们将打破这种局面,聚焦DMI指标(Directional Movement Index)的Python实战应用,从数据获取到策略回测,手把手教你构建一个能有效识别趋势强度的量化系统。

1. 环境准备与数据获取

工欲善其事,必先利其器。我们需要先搭建好Python量化分析环境,并获取高质量的金融数据。推荐使用Jupyter Notebook或VSCode作为开发环境,它们能提供良好的交互体验和代码调试能力。

首先安装必要的Python库:

pip install pandas numpy matplotlib akshare backtrader

这里我们选择akshare作为数据源,它提供了丰富且免费的金融数据接口。下面是一个获取股票历史数据的函数示例:

import akshare as ak

def get_stock_data(stock_code, start_date, end_date):
    """
    获取股票历史数据
    :param stock_code: 股票代码,如'sh600000'
    :param start_date: 开始日期,格式'YYYY-MM-DD'
    :param end_date: 结束日期,格式'YYYY-MM-DD'
    :return: DataFrame格式的股票数据
    """
    df = ak.stock_zh_a_hist(symbol=stock_code, period="daily", 
                           start_date=start_date, end_date=end_date, adjust="hfq")
    df['date'] = pd.to_datetime(df['日期'])
    df.set_index('date', inplace=True)
    df = df[['开盘', '最高', '最低', '收盘', '成交量']]
    df.columns = ['open', 'high', 'low', 'close', 'volume']
    return df

提示:在实际应用中,建议将获取的数据本地化存储,避免频繁请求接口。可以使用df.to_csv()保存数据,再用pd.read_csv()读取。

2. DMI指标的核心计算逻辑

DMI指标由三个核心部分组成:+DI(正向指标)、-DI(负向指标)和ADX(平均趋向指数)。理解它们的计算原理对于后续的策略优化至关重要。

2.1 真实波动范围(TR)与方向运动(DM)

计算DMI的第一步是确定真实波动范围(True Range)和方向运动(Directional Movement)。以下是Python实现:

def calculate_tr(df):
    """计算真实波动范围(True Range)"""
    tr1 = df['high'] - df['low']
    tr2 = abs(df['high'] - df['close'].shift(1))
    tr3 = abs(df['low'] - df['close'].shift(1))
    df['tr'] = np.maximum.reduce([tr1, tr2, tr3])
    return df

def calculate_dm(df):
    """计算方向运动(+DM和-DM)"""
    up_move = df['high'] - df['high'].shift(1)
    down_move = df['low'].shift(1) - df['low']
    
    df['+dm'] = np.where((up_move > down_move) & (up_move > 0), up_move, 0)
    df['-dm'] = np.where((down_move > up_move) & (down_move > 0), down_move, 0)
    return df

2.2 平滑处理与DI值计算

DMI指标使用平滑移动平均而非简单移动平均,这是其独特之处。下面是平滑处理函数:

def smoothed_ma(series, window, prev_smoothed):
    """
    计算平滑移动平均
    :param series: 待计算序列
    :param window: 窗口大小
    :param prev_smoothed: 前一日平滑值
    """
    return prev_smoothed - (prev_smoothed / window) + series

基于平滑值,我们可以计算+DI和-DI:

def calculate_dmi(df, window=14):
    """计算+DI和-DI"""
    # 初始化平滑值
    df['smoothed_tr'] = 0.0
    df['smoothed_+dm'] = 0.0
    df['smoothed_-dm'] = 0.0
    
    # 计算前window天的简单移动平均作为初始值
    df.loc[df.index[window-1], 'smoothed_tr'] = df['tr'].iloc[:window].mean()
    df.loc[df.index[window-1], 'smoothed_+dm'] = df['+dm'].iloc[:window].mean()
    df.loc[df.index[window-1], 'smoothed_-dm'] = df['-dm'].iloc[:window].mean()
    
    # 计算平滑值
    for i in range(window, len(df)):
        prev_idx = df.index[i-1]
        curr_idx = df.index[i]
        
        df.loc[curr_idx, 'smoothed_tr'] = smoothed_ma(
            df.loc[curr_idx, 'tr'], window, df.loc[prev_idx, 'smoothed_tr'])
        df.loc[curr_idx, 'smoothed_+dm'] = smoothed_ma(
            df.loc[curr_idx, '+dm'], window, df.loc[prev_idx, 'smoothed_+dm'])
        df.loc[curr_idx, 'smoothed_-dm'] = smoothed_ma(
            df.loc[curr_idx, '-dm'], window, df.loc[prev_idx, 'smoothed_-dm'])
    
    # 计算DI值
    df['+di'] = (df['smoothed_+dm'] / df['smoothed_tr']) * 100
    df['-di'] = (df['smoothed_-dm'] / df['smoothed_tr']) * 100
    
    return df

2.3 ADX的计算与趋势强度判断

ADX是判断趋势强度的关键指标,其计算需要先得到DX(趋向指数):

def calculate_adx(df, window=14):
    """计算ADX"""
    # 计算DX
    df['dx'] = (abs(df['+di'] - df['-di']) / (df['+di'] + df['-di'])) * 100
    
    # 初始化ADX
    df['adx'] = 0.0
    df.loc[df.index[window*2-2], 'adx'] = df['dx'].iloc[window-1:window*2-1].mean()
    
    # 计算ADX平滑值
    for i in range(window*2-1, len(df)):
        prev_idx = df.index[i-1]
        curr_idx = df.index[i]
        df.loc[curr_idx, 'adx'] = smoothed_ma(
            df.loc[curr_idx, 'dx'], window, df.loc[prev_idx, 'adx'])
    
    return df

3. 策略构建与信号生成

单纯的DI交叉信号容易产生大量假信号,我们需要结合ADX进行过滤,构建更可靠的交易策略。

3.1 基础信号生成

首先实现基本的DI交叉信号:

def generate_basic_signals(df):
    """生成基础交易信号"""
    # DI交叉信号
    df['+di_cross'] = (df['+di'] > df['-di']) & (df['+di'].shift(1) <= df['-di'].shift(1))
    df['-di_cross'] = (df['-di'] > df['+di']) & (df['-di'].shift(1) <= df['+di'].shift(1))
    
    # 初始化信号列
    df['signal'] = 0  # 0表示无信号,1表示买入,-1表示卖出
    
    # 生成信号
    df.loc[df['+di_cross'], 'signal'] = 1
    df.loc[df['-di_cross'], 'signal'] = -1
    
    return df

3.2 使用ADX过滤假信号

加入ADX过滤可以显著提高信号质量:

def filter_signals_with_adx(df, adx_threshold=25):
    """使用ADX过滤信号"""
    # 复制基础信号
    df['filtered_signal'] = df['signal'].copy()
    
    # 过滤条件:ADX必须大于阈值且呈上升趋势
    weak_trend = (df['adx'] < adx_threshold) | (df['adx'] < df['adx'].shift(1))
    
    # 应用过滤
    df.loc[weak_trend & (df['signal'] == 1), 'filtered_signal'] = 0
    df.loc[weak_trend & (df['signal'] == -1), 'filtered_signal'] = 0
    
    return df

3.3 多时间周期参数优化

不同时间周期参数会显著影响信号质量,我们可以比较14日和28日参数的效果:

参数类型 信号频率 信号质量 适用场景
14日周期 较高 中等 趋势明显的市场
28日周期 较低 较高 震荡或趋势不明显的市场

实现多周期DMI计算:

def multi_period_dmi(df, periods=[14, 28]):
    """计算多周期DMI指标"""
    df = calculate_tr(df)
    df = calculate_dm(df)
    
    for period in periods:
        df = calculate_dmi(df, window=period)
        df = calculate_adx(df, window=period)
        
        # 重命名列以避免冲突
        df.rename(columns={
            '+di': f'+di_{period}',
            '-di': f'-di_{period}',
            'adx': f'adx_{period}'
        }, inplace=True)
    
    return df

4. 策略回测与绩效评估

策略构建完成后,我们需要通过历史数据回测来验证其有效性。这里使用backtrader回测框架。

4.1 回测策略实现

import backtrader as bt

class DMIStrategy(bt.Strategy):
    params = (
        ('adx_threshold', 25),
        ('period', 14),
    )
    
    def __init__(self):
        # 计算DMI指标
        self.dmi = DMI(
            self.data,
            period=self.p.period,
            adx_threshold=self.p.adx_threshold
        )
        
    def next(self):
        if not self.position:
            if self.dmi.buy_signal[0]:
                self.buy()
        elif self.dmi.sell_signal[0]:
            self.close()

4.2 回测执行与结果分析

完整的回测执行代码:

def run_backtest(data, initial_cash=100000, commission=0.001, adx_threshold=25, period=14):
    cerebro = bt.Cerebro()
    cerebro.addstrategy(DMIStrategy, adx_threshold=adx_threshold, period=period)
    
    # 添加数据
    data_feed = bt.feeds.PandasData(dataname=data)
    cerebro.adddata(data_feed)
    
    # 设置初始资金和手续费
    cerebro.broker.setcash(initial_cash)
    cerebro.broker.setcommission(commission=commission)
    
    # 添加分析器
    cerebro.addanalyzer(bt.analyzers.Returns)
    cerebro.addanalyzer(bt.analyzers.SharpeRatio)
    cerebro.addanalyzer(bt.analyzers.DrawDown)
    
    # 运行回测
    results = cerebro.run()
    
    # 打印结果
    print(f'最终资产价值: {cerebro.broker.getvalue():.2f}')
    print(f'年化收益率: {results[0].analyzers.returns.get_analysis()["rnorm100"]:.2f}%')
    print(f'夏普比率: {results[0].analyzers.sharperatio.get_analysis()["sharperatio"]:.2f}')
    print(f'最大回撤: {results[0].analyzers.drawdown.get_analysis()["max"]["drawdown"]:.2f}%')
    
    # 绘制结果
    cerebro.plot(style='candlestick')

4.3 参数优化与敏感性分析

通过网格搜索寻找最优参数组合:

def optimize_parameters(data):
    # 定义参数范围
    adx_thresholds = [20, 25, 30]
    periods = [10, 14, 20, 28]
    
    best_params = None
    best_sharpe = -np.inf
    
    for adx in adx_thresholds:
        for period in periods:
            cerebro = bt.Cerebro()
            cerebro.addstrategy(DMIStrategy, adx_threshold=adx, period=period)
            
            data_feed = bt.feeds.PandasData(dataname=data)
            cerebro.adddata(data_feed)
            
            cerebro.broker.setcash(100000)
            cerebro.broker.setcommission(commission=0.001)
            
            cerebro.addanalyzer(bt.analyzers.SharpeRatio)
            
            results = cerebro.run()
            sharpe = results[0].analyzers.sharperatio.get_analysis()['sharperatio']
            
            if sharpe > best_sharpe:
                best_sharpe = sharpe
                best_params = (adx, period)
    
    print(f'最优参数组合: ADX阈值={best_params[0]}, 周期={best_params[1]}')
    print(f'最优夏普比率: {best_sharpe:.2f}')
    
    return best_params

在实际应用中,我发现将DMI指标与简单的移动平均线结合使用效果更佳。例如,只有当价格在200日均线之上时才考虑做多信号,在200日均线之下时只考虑做空信号,这样可以进一步过滤掉不符合大趋势的信号。

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐