在 GitHub 上查看

Ivan CCI Averaging 策略

将 MetaTrader “Ivan” 智能交易系统移植到 StockSharp 的版本,使用 CCI 极值信号结合加仓与平滑移动平均止损。策略监控长期 CCI(100) 以确认全局多空方向,可在 CCI(13) 回调时按需加仓,并通过平滑均线的止损、保本和跟踪逻辑管理风险。仓位规模沿用原策略的账户风险百分比模型,利润保护系数在权益倍增时强制平仓。

细节

  • 入场条件
    • 多头全局信号:当 CCI(100) 上穿 GlobalSignalLevel 且没有活跃的买入状态时,按市价做多,并把初始止损设在平滑均线上,前提是止损至少低于价格 MinStopDistance
    • 多头加仓:在启用 UseAveraging 且全局多头标志为真时,只要 CCI(13) 跌破 -GlobalSignalLevel 就按同样模板再加一笔多头。
    • 空头全局信号:当 CCI(100) 下穿 -GlobalSignalLevel 且没有活跃的卖出状态时,按市价做空,只要均线止损至少高出价格 MinStopDistance
    • 空头加仓:启用 UseAveraging 时,全局空头状态下只要 CCI(13) 上穿 GlobalSignalLevel 就增加空单。
  • 多空方向:双向交易,并可在当前偏向内金字塔加仓。
  • 出场条件
    • CCI(100) 回到 ±ReverseLevel 区间内会清除多空标志并强制平仓。
    • 投资组合权益超过初始权益的 ProfitProtectionFactor 倍时立即平仓锁定收益。
    • 达到跟踪止损(保本价或更新后的均线止损)时平掉相应方向。
  • 止损
    • 初始止损来自周期为 StopLossMaPeriod 的平滑移动平均(SMMA)。
    • 当价格运行 BreakEvenDistance 后,止损移动到入场价(设为 0 可关闭保本功能)。
    • 只有当均线上移/下移超过 TrailingStep 时才会推进跟踪止损。
  • 过滤条件
    • UseZeroBar 复刻 MT5 选项,可选择使用刚开启的当前 K 线或上一根收盘 K 线的数值。
    • MinStopDistance 防止均线止损过于靠近入场价。
  • 仓位规模
    • 每次下单都会以 RiskPercent 的账户权益除以入场价与止损价的差值来计算手数,MinimumVolume 作为下限。

参数

  • Use Averaging (bool,默认: true) — 是否允许在全局信号下进行加仓。
  • Stop MA Period (int,默认: 36) — 平滑均线的周期,用于生成止损。
  • Risk % (decimal,默认: 10) — 每次交易愿意承担的账户权益百分比。
  • Use Zero Bar (bool,默认: true) — 是否使用当前形成的 K 线数据;为 false 时使用上一根收盘 K 线。
  • Reverse Level (decimal,默认: 100) — CCI 回撤到该绝对值以内时清除所有仓位。
  • Global Level (decimal,默认: 100) — 触发全局买入或卖出信号的 CCI 绝对阈值。
  • Min Stop Distance (decimal,默认: 0.005) — 入场价与均线止损之间的最小价差(0.005 ≈ 外汇五位报价的 50 点)。
  • Trailing Step (decimal,默认: 0.001) — 推进跟踪止损所需的最小均线进展。
  • BreakEven Distance (decimal,默认: 0.0005) — 把止损移到入场价所需的利润幅度;0 表示禁用。
  • Profit Protection (decimal,默认: 1.5) — 当权益达到该倍数时强制平仓以保护利润。
  • Minimum Volume (decimal,默认: 1) — 风险模型计算出过小手数时使用的最小交易量。
  • Candle Type (DataType) — 指标使用的 K 线类型(默认 15 分钟)。

备注

  • MinStopDistanceTrailingStepBreakEvenDistance 以价格单位表示,应根据标的的最小变动价位调整。
  • 策略假设 BuyMarket/SellMarket 指令即时成交;若预期滑点或部分成交,请调整执行设置。
  • 需要可用的投资组合适配器才能进行基于权益的仓位计算,否则始终使用 MinimumVolume
using System;
using System.Linq;
using System.Collections.Generic;

using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// CCI based averaging strategy converted from the Ivan expert advisor.
/// </summary>
public class IvanCciAveragingStrategy : Strategy
{
	private readonly StrategyParam<bool> _useAveraging;
	private readonly StrategyParam<int> _stopLossMaPeriod;
	private readonly StrategyParam<decimal> _riskPercent;
	private readonly StrategyParam<bool> _useZeroBar;
	private readonly StrategyParam<decimal> _reverseLevel;
	private readonly StrategyParam<decimal> _globalSignalLevel;
	private readonly StrategyParam<decimal> _minStopDistance;
	private readonly StrategyParam<decimal> _trailingStep;
	private readonly StrategyParam<decimal> _breakEvenDistance;
	private readonly StrategyParam<decimal> _profitProtectionFactor;
	private readonly StrategyParam<decimal> _minimumVolume;
	private readonly StrategyParam<DataType> _candleType;

	private CommodityChannelIndex _cci100 = null!;
	private CommodityChannelIndex _cci13 = null!;
	private SmoothedMovingAverage _stopMa = null!;

	private decimal? _lastCci100;
	private decimal? _prevCci100;
	private decimal? _lastCci13;
	private decimal? _prevCci13;

	private bool _globalBuySignal;
	private bool _globalSellSignal;
	private bool _closeAll;

	private decimal? _initialBalance;

	private decimal _longEntryPrice;
	private decimal _shortEntryPrice;
	private decimal _longStop;
	private decimal _shortStop;
	private bool _longBreakEvenActivated;
	private bool _shortBreakEvenActivated;
	private bool _hasLongEntry;
	private bool _hasShortEntry;

	/// <summary>
	/// Enables additional averaging entries when short CCI pulls back.
	/// </summary>
	public bool UseAveraging
	{
		get => _useAveraging.Value;
		set => _useAveraging.Value = value;
	}

	/// <summary>
	/// Period for the smoothed moving average used as stop reference.
	/// </summary>
	public int StopLossMaPeriod
	{
		get => _stopLossMaPeriod.Value;
		set => _stopLossMaPeriod.Value = value;
	}

	/// <summary>
	/// Portfolio risk percent used to size new entries.
	/// </summary>
	public decimal RiskPercent
	{
		get => _riskPercent.Value;
		set => _riskPercent.Value = value;
	}

	/// <summary>
	/// Use the latest candle (zero bar) instead of the previous closed bar for signals.
	/// </summary>
	public bool UseZeroBar
	{
		get => _useZeroBar.Value;
		set => _useZeroBar.Value = value;
	}

	/// <summary>
	/// Reverse level for the long term CCI that triggers full liquidation.
	/// </summary>
	public decimal ReverseLevel
	{
		get => _reverseLevel.Value;
		set => _reverseLevel.Value = value;
	}

	/// <summary>
	/// Threshold for the long term CCI global signal.
	/// </summary>
	public decimal GlobalSignalLevel
	{
		get => _globalSignalLevel.Value;
		set => _globalSignalLevel.Value = value;
	}

	/// <summary>
	/// Minimum distance between price and stop when entering a position.
	/// </summary>
	public decimal MinStopDistance
	{
		get => _minStopDistance.Value;
		set => _minStopDistance.Value = value;
	}

	/// <summary>
	/// Minimum improvement required before trailing the stop with the MA.
	/// </summary>
	public decimal TrailingStep
	{
		get => _trailingStep.Value;
		set => _trailingStep.Value = value;
	}

	/// <summary>
	/// Profit distance that moves the stop to break-even. Zero disables break-even.
	/// </summary>
	public decimal BreakEvenDistance
	{
		get => _breakEvenDistance.Value;
		set => _breakEvenDistance.Value = value;
	}

	/// <summary>
	/// Equity multiple that forces liquidation of all positions.
	/// </summary>
	public decimal ProfitProtectionFactor
	{
		get => _profitProtectionFactor.Value;
		set => _profitProtectionFactor.Value = value;
	}

	/// <summary>
	/// Minimum trading volume used when risk based sizing is not available.
	/// </summary>
	public decimal MinimumVolume
	{
		get => _minimumVolume.Value;
		set => _minimumVolume.Value = value;
	}

	/// <summary>
	/// Type of candles used by the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="IvanCciAveragingStrategy"/> class.
	/// </summary>
	public IvanCciAveragingStrategy()
	{
		_useAveraging = Param(nameof(UseAveraging), false)
			.SetDisplay("Use Averaging", "Allow additional averaging entries", "Signals");

		_stopLossMaPeriod = Param(nameof(StopLossMaPeriod), 36)
			.SetGreaterThanZero()
			.SetDisplay("Stop MA Period", "Length of SMMA for stop placement", "Stops");

		_riskPercent = Param(nameof(RiskPercent), 10m)
			.SetGreaterThanZero()
			.SetDisplay("Risk %", "Portfolio percent risked per trade", "Risk");

		_useZeroBar = Param(nameof(UseZeroBar), false)
			.SetDisplay("Use Zero Bar", "Use current bar values instead of previous", "Signals");

		_reverseLevel = Param(nameof(ReverseLevel), 100m)
			.SetGreaterThanZero()
			.SetDisplay("Reverse Level", "CCI level that closes all trades", "Signals");

		_globalSignalLevel = Param(nameof(GlobalSignalLevel), 100m)
			.SetGreaterThanZero()
			.SetDisplay("Global Level", "CCI level that creates global signal", "Signals");

		_minStopDistance = Param(nameof(MinStopDistance), 0.005m)
			.SetGreaterThanZero()
			.SetDisplay("Min Stop Distance", "Minimum price gap between entry and stop", "Stops");

		_trailingStep = Param(nameof(TrailingStep), 0.001m)
			.SetGreaterThanZero()
			.SetDisplay("Trailing Step", "Minimum MA progress before trailing", "Stops");

		_breakEvenDistance = Param(nameof(BreakEvenDistance), 0.0005m)
			.SetDisplay("BreakEven Distance", "Distance to move stop to entry", "Stops");

		_profitProtectionFactor = Param(nameof(ProfitProtectionFactor), 1.5m)
			.SetGreaterThanZero()
			.SetDisplay("Profit Protection", "Equity multiple to flatten positions", "Risk");

		_minimumVolume = Param(nameof(MinimumVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Minimum Volume", "Fallback trade volume", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles for calculations", "General");
	}

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

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

		_cci100?.Reset();
		_cci13?.Reset();
		_stopMa?.Reset();
		_cci100 = null!;
		_cci13 = null!;
		_stopMa = null!;
		_lastCci100 = null;
		_prevCci100 = null;
		_lastCci13 = null;
		_prevCci13 = null;
		_globalBuySignal = false;
		_globalSellSignal = false;
		_closeAll = false;
		_initialBalance = null;
		_longEntryPrice = 0m;
		_shortEntryPrice = 0m;
		_longStop = 0m;
		_shortStop = 0m;
		_longBreakEvenActivated = false;
		_shortBreakEvenActivated = false;
		_hasLongEntry = false;
		_hasShortEntry = false;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		// Initialize indicators used for signal and stop logic.
		_cci100 = new CommodityChannelIndex { Length = 100 };
		_cci13 = new CommodityChannelIndex { Length = 13 };
		_stopMa = new SmoothedMovingAverage { Length = StopLossMaPeriod };

		// Reset state variables for a new run.
		_lastCci100 = null;
		_prevCci100 = null;
		_lastCci13 = null;
		_prevCci13 = null;
		_globalBuySignal = false;
		_globalSellSignal = false;
		_closeAll = false;
		_hasLongEntry = false;
		_hasShortEntry = false;
		_longBreakEvenActivated = false;
		_shortBreakEvenActivated = false;
		_longStop = 0m;
		_shortStop = 0m;

		_initialBalance = Portfolio?.CurrentValue ?? 0m;

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(_cci100, _cci13, _stopMa, ProcessCandle)
			.Start();
	}

	private void ProcessCandle(ICandleMessage candle, decimal cci100Value, decimal cci13Value, decimal stopMaValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		// Update profit protection flag based on equity growth.
		var equity = Portfolio?.CurrentValue ?? 0m;
		if (ProfitProtectionFactor > 1m && _initialBalance.HasValue && _initialBalance.Value > 0m)
		{
			if (equity >= _initialBalance.Value * ProfitProtectionFactor)
				_closeAll = true;
		}

		// Ensure indicators are ready before using their values.
		if (!_cci100.IsFormed || !_cci13.IsFormed || !_stopMa.IsFormed)
		{
			UpdateHistory(cci100Value, cci13Value);
			return;
		}

		decimal? currentCci;
		decimal? previousCci;
		decimal? shortCci;

		// Recreate the zero/first bar selection logic from MQL.
		if (UseZeroBar)
		{
			currentCci = cci100Value;
			previousCci = _lastCci100;
			shortCci = cci13Value;
		}
		else
		{
			currentCci = _lastCci100;
			previousCci = _prevCci100;
			shortCci = _lastCci13;
		}

		if (currentCci is null || previousCci is null || shortCci is null)
		{
			UpdateHistory(cci100Value, cci13Value);
			return;
		}

		// Detect reverse conditions that require flattening the book.
		if ((previousCci.Value > ReverseLevel && currentCci.Value < ReverseLevel) ||
			(previousCci.Value < -ReverseLevel && currentCci.Value > -ReverseLevel))
		{
			_globalBuySignal = false;
			_globalSellSignal = false;
			_closeAll = true;
		}
		else if (!_closeAll)
		{
			// Generate global signals and optional averaging entries.
			if (currentCci.Value > GlobalSignalLevel && !_globalBuySignal)
			{
				_globalBuySignal = true;
				_globalSellSignal = false;
				TryEnterLong(candle, stopMaValue);
			}
			else if (currentCci.Value < -GlobalSignalLevel && !_globalSellSignal)
			{
				_globalBuySignal = false;
				_globalSellSignal = true;
				TryEnterShort(candle, stopMaValue);
			}
			else if (UseAveraging)
			{
				if (_globalBuySignal && shortCci.Value < -GlobalSignalLevel)
					TryEnterLong(candle, stopMaValue);
				else if (_globalSellSignal && shortCci.Value > GlobalSignalLevel)
					TryEnterShort(candle, stopMaValue);
			}
		}

		ManagePositions(candle, stopMaValue);

		if (_closeAll)
		{
			ClosePosition();
			_closeAll = false;
		}

		UpdateHistory(cci100Value, cci13Value);
	}

	private void ManagePositions(ICandleMessage candle, decimal stopMaValue)
	{
		if (Position > 0 && _hasLongEntry)
		{
			// Move the long stop to break-even when profit reaches the target distance.
			if (BreakEvenDistance > 0m && !_longBreakEvenActivated && candle.ClosePrice >= _longEntryPrice + BreakEvenDistance)
			{
				_longStop = _longEntryPrice;
				_longBreakEvenActivated = true;
			}

			// Trail the stop with the smoothed moving average if it keeps rising.
			if (stopMaValue < candle.ClosePrice)
			{
				if (stopMaValue - TrailingStep > _longStop)
					_longStop = stopMaValue;
			}

			if (_longStop > 0m && candle.ClosePrice <= _longStop)
			{
				SellMarket();
				_hasLongEntry = false;
				_longBreakEvenActivated = false;
			}
		}
		else if (Position < 0 && _hasShortEntry)
		{
			// Move the short stop to break-even when profit reaches the target distance.
			if (BreakEvenDistance > 0m && !_shortBreakEvenActivated && candle.ClosePrice <= _shortEntryPrice - BreakEvenDistance)
			{
				_shortStop = _shortEntryPrice;
				_shortBreakEvenActivated = true;
			}

			// Trail the stop with the smoothed moving average if it keeps falling.
			if (stopMaValue > candle.ClosePrice)
			{
				if (_shortStop == 0m || stopMaValue + TrailingStep < _shortStop)
					_shortStop = stopMaValue;
			}

			if (_shortStop > 0m && candle.ClosePrice >= _shortStop)
			{
				BuyMarket();
				_hasShortEntry = false;
				_shortBreakEvenActivated = false;
			}
		}
	}

	private void TryEnterLong(ICandleMessage candle, decimal stopMaValue)
	{
		if (stopMaValue >= candle.ClosePrice)
			return;

		var distance = candle.ClosePrice - stopMaValue;
		if (distance < MinStopDistance)
			return;

		var volume = CalculateVolume(candle.ClosePrice, stopMaValue);
		if (volume <= 0m)
			return;

		BuyMarket();
		_longEntryPrice = candle.ClosePrice;
		_longStop = stopMaValue;
		_longBreakEvenActivated = false;
		_hasLongEntry = true;
		_hasShortEntry = false;
	}

	private void TryEnterShort(ICandleMessage candle, decimal stopMaValue)
	{
		if (stopMaValue <= candle.ClosePrice)
			return;

		var distance = stopMaValue - candle.ClosePrice;
		if (distance < MinStopDistance)
			return;

		var volume = CalculateVolume(candle.ClosePrice, stopMaValue);
		if (volume <= 0m)
			return;

		SellMarket();
		_shortEntryPrice = candle.ClosePrice;
		_shortStop = stopMaValue;
		_shortBreakEvenActivated = false;
		_hasShortEntry = true;
		_hasLongEntry = false;
	}

	private decimal CalculateVolume(decimal entryPrice, decimal stopPrice)
	{
		var minimum = MinimumVolume > 0m ? MinimumVolume : 0m;

		if (entryPrice <= 0m || stopPrice <= 0m)
			return minimum;

		var riskPerUnit = Math.Abs(entryPrice - stopPrice);
		if (riskPerUnit <= 0m)
			return minimum;

		if (RiskPercent <= 0m)
			return minimum;

		var equity = Portfolio?.CurrentValue ?? 0m;
		if (equity <= 0m)
			return minimum;

		var riskCapital = equity * RiskPercent / 100m;
		if (riskCapital <= 0m)
			return minimum;

		var volume = riskCapital / riskPerUnit;
		if (volume <= 0m)
			return minimum;

		return Math.Max(minimum, volume);
	}

	private void ClosePosition()
	{
		if (Position > 0)
		{
			SellMarket();
		}
		else if (Position < 0)
		{
			BuyMarket();
		}

		_hasLongEntry = false;
		_hasShortEntry = false;
		_longBreakEvenActivated = false;
		_shortBreakEvenActivated = false;
		_longStop = 0m;
		_shortStop = 0m;
	}

	private void UpdateHistory(decimal cci100Value, decimal cci13Value)
	{
		_prevCci100 = _lastCci100;
		_lastCci100 = cci100Value;
		_prevCci13 = _lastCci13;
		_lastCci13 = cci13Value;
	}
}