在 GitHub 上查看

雪崩策略

概览

雪崩策略是一种网格式均值回归系统,灵感来自 MetaTrader 上的 Avalanche v1.2 专家顾问。策略通过计算高时间框架的简单移动平均线 (ERP) 来评估价格与参考值的关系。当价格位于 ERP 下方时,系统预期价格回归均值并逐步加仓多单;当价格位于 ERP 上方时,系统则逐步累积空单。每一次加仓都按照可配置的点差间隔执行,并为每笔订单设置独立的止损与止盈。

本 StockSharp 实现专注于原始算法中的 "toward"(向均值)腿。由于 StockSharp 策略只能维护单一净仓位,MQL 版本中的对冲 "away" 订单没有复刻,但网格叠加、缓冲与利润管理逻辑保持一致。

工作流程

  1. 订阅两个 K 线序列:交易时间框架与 ERP 时间框架。
  2. 根据 ERP 时间框架计算简单移动平均,并通过缓冲区判断价格位于均线之上还是之下。
  3. ERP 方向发生变化时,平掉现有网格,等待新的信号。
  4. 如果启用了 OpenStartingOrders,在满足条件时立即开立第一笔向均值的仓位(下方做多,上方做空)。
  5. 当价格按照 IntervalToward 的距离向有利方向推进时继续加仓。
  6. 当价格向不利方向移动 IntervalToward + StackBufferToward 的距离时补仓以摊薄成本。
  7. 每笔订单都带有独立的止损与止盈,保证盈利腿可以独立出场,同时网格继续管理剩余仓位。

主要参数

名称 说明
BaseVolume 基础下单手数,在应用乘数前使用。
TowardMultiplier 标准向均值加仓时使用的乘数。
TowardInterestMultiplier 当该品种在当前方向具有正掉期时使用的乘数。
IntervalToward 顺势加仓所需的点差距离。
StackBufferToward 在不利方向补仓时附加的额外缓冲。
TakeProfitToward 每笔订单的止盈距离(点)。设为 0 表示禁用。
StopLossToward 每笔订单的止损距离(点)。设为 0 表示禁用。
ErpPeriod 计算 ERP 的简单移动平均周期数。
ErpChangeBuffer 切换 ERP 偏向前使用的缓冲区(点)。
CandleType 触发交易和管理网格的时间框架。
ErpCandleType 计算 ERP 的时间框架。
OpenStartingOrders 启用后,一旦条件满足立即开立初始仓位。

与原版 EA 的差异

  • 只实现向均值腿,未实现 MQL 中的反向对冲腿。
  • 使用市价单执行,而非原版的挂单。
  • 保留了根据品种掉期方向选择乘数的逻辑。

使用建议

  • 调整 IntervalTowardStackBufferToward,控制网格扩张速度。
  • 网格策略可能积累较大头寸,请选择流动性充足的交易品种与时间框架。
  • 建议结合外部风险控制(如权益止损、交易时段过滤)以提升实际运行的安全性。
using System;
using System.Collections.Generic;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Avalanche grid strategy - mean reversion around EMA.
/// Buys when price drops below EMA, sells when above.
/// Uses RSI to confirm oversold/overbought conditions.
/// </summary>
public class AvalancheStrategy : Strategy
{
	private readonly StrategyParam<int> _emaPeriod;
	private readonly StrategyParam<int> _rsiPeriod;
	private readonly StrategyParam<decimal> _rsiOversold;
	private readonly StrategyParam<decimal> _rsiOverbought;
	private readonly StrategyParam<DataType> _candleType;

	public int EmaPeriod { get => _emaPeriod.Value; set => _emaPeriod.Value = value; }
	public int RsiPeriod { get => _rsiPeriod.Value; set => _rsiPeriod.Value = value; }
	public decimal RsiOversold { get => _rsiOversold.Value; set => _rsiOversold.Value = value; }
	public decimal RsiOverbought { get => _rsiOverbought.Value; set => _rsiOverbought.Value = value; }
	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }

	public AvalancheStrategy()
	{
		_emaPeriod = Param(nameof(EmaPeriod), 20)
			.SetDisplay("EMA Period", "EMA period for equilibrium", "Indicators");

		_rsiPeriod = Param(nameof(RsiPeriod), 14)
			.SetDisplay("RSI Period", "RSI period", "Indicators");

		_rsiOversold = Param(nameof(RsiOversold), 35m)
			.SetDisplay("RSI Oversold", "RSI oversold level", "Indicators");

		_rsiOverbought = Param(nameof(RsiOverbought), 65m)
			.SetDisplay("RSI Overbought", "RSI overbought level", "Indicators");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Candle timeframe", "General");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		var ema = new ExponentialMovingAverage { Length = EmaPeriod };
		var rsi = new RelativeStrengthIndex { Length = RsiPeriod };

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(ema, rsi, ProcessCandle)
			.Start();
	}

	private void ProcessCandle(ICandleMessage candle, decimal emaValue, decimal rsiValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		var close = candle.ClosePrice;

		// Below EMA and oversold - buy
		if (close < emaValue && rsiValue <= RsiOversold && Position <= 0)
		{
			if (Position < 0)
				BuyMarket();
			BuyMarket();
		}
		// Above EMA and overbought - sell
		else if (close > emaValue && rsiValue >= RsiOverbought && Position >= 0)
		{
			if (Position > 0)
				SellMarket();
			SellMarket();
		}
	}
}