在 GitHub 上查看

iVIDyA Simple 策略

概述

该策略是 MetaTrader 专家顾问 “iVIDyA Simple” 的 StockSharp 高层 API 移植版。策略只交易一个品种,通过 Chande 动量振荡器(CMO)驱动的可变指数动态平均线(VIDYA)捕捉趋势。当最新完成的 K 线与经过偏移的 VIDYA 发生交叉时,策略按突破方向开立市价单,并可选地附加止损和止盈。

交易逻辑

  1. 按照参数 CandleType 订阅指定周期的 K 线。
  2. 将周期为 CmoPeriod 的 CMO 绑定到 K 线序列。其绝对值用于动态调整 VIDYA 的平滑系数,基础系数与原版一样为 2 / (EmaPeriod + 1)
  3. 每根收盘 K 线都会执行以下步骤:
    • AppliedPrice 选择用于计算的价格(收盘价、开盘价、中价等)。
    • 根据自适应系数更新 VIDYA。
    • 保留 VIDYA 历史值,以复刻 MetaTrader 指标的 ma_shift 参数效果。
  4. 将当前 K 线与向前偏移 MaShift 根的 VIDYA 比较:
    • 若开盘价在 VIDYA 下方、收盘价在 VIDYA 上方,则产生买入信号。
    • 若开盘价在 VIDYA 上方、收盘价在 VIDYA 下方,则产生卖出信号。
  5. 开仓前会先平掉相反方向的仓位,使结果头寸等于设定交易量。
  6. 如果止损或止盈距离大于零,则在每次进场后调用 SetStopLossSetTakeProfit

这完全复刻了原始 EA:仅在新柱上触发、通过 CMO 与 EMA 构建 VIDYA,并用点值表示止损/止盈距离。

参数

名称 默认值 说明
Volume 1 基础下单手数。策略在反手时会自动对冲现有仓位。
StopLossPoints 150 止损距离(价格步长)。设为 0 可关闭。
TakeProfitPoints 460 止盈距离(价格步长)。设为 0 可关闭。
CmoPeriod 15 控制 VIDYA 自适应权重的 CMO 周期。
EmaPeriod 12 定义 VIDYA 基础平滑系数的 EMA 周期。
MaShift 1 VIDYA 向前偏移的已完成 K 线数量,对应 MetaTrader 的 ma_shift
AppliedPrice Close VIDYA 使用的价格类型(CloseOpenHighLowMedianTypicalWeighted)。
CandleType TimeSpan.FromMinutes(5) 用于所有计算与信号的 K 线类型/周期。

其他说明

  • 止损和止盈通过高层 API (SetStopLoss/SetTakeProfit) 管理,而原始 MQL 代码需要手动检查冻结与最小距离。
  • 策略只处理已完成的 K 线,从而严格遵守“新柱执行”规则。
  • VIDYA 历史会自动截断,即使 MaShift 很大也不会占用过多内存。
  • 根据项目要求,代码中的所有注释均使用英文。
using System;

using Ecng.Common;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Simplified from "iVIDyA Simple" MetaTrader expert.
/// Computes a Variable Index Dynamic Average using CMO and EMA smoothing.
/// Trades when price crosses above/below the VIDYA line.
/// </summary>
public class IvidyaSimpleStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _cmoPeriod;
	private readonly StrategyParam<int> _emaPeriod;

	private ChandeMomentumOscillator _cmo;
	private decimal? _prevVidya;
	private decimal? _prevClose;

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public int CmoPeriod
	{
		get => _cmoPeriod.Value;
		set => _cmoPeriod.Value = value;
	}

	public int EmaPeriod
	{
		get => _emaPeriod.Value;
		set => _emaPeriod.Value = value;
	}

	public IvidyaSimpleStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(60).TimeFrame())
			.SetDisplay("Candle Type", "Primary candle series", "General");

		_cmoPeriod = Param(nameof(CmoPeriod), 20)
			.SetGreaterThanZero()
			.SetDisplay("CMO Period", "Chande Momentum Oscillator length", "Indicator");

		_emaPeriod = Param(nameof(EmaPeriod), 30)
			.SetGreaterThanZero()
			.SetDisplay("EMA Period", "Base EMA length used by VIDYA", "Indicator");
	}

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

		_cmo = new ChandeMomentumOscillator { Length = CmoPeriod };
		_prevVidya = null;
		_prevClose = null;

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(_cmo, ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawOwnTrades(area);
		}
	}

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

		if (!_cmo.IsFormed)
			return;

		var close = candle.ClosePrice;

		// VIDYA = alpha * |CMO/100| * price + (1 - alpha * |CMO/100|) * prevVidya
		var alpha = 2m / (EmaPeriod + 1m);
		var momentumFactor = Math.Abs(cmoValue) / 100m;
		var sf = alpha * momentumFactor;

		var prevVidya = _prevVidya ?? close;
		var currentVidya = sf * close + (1m - sf) * prevVidya;

		if (_prevVidya is null || _prevClose is null)
		{
			_prevVidya = currentVidya;
			_prevClose = close;
			return;
		}

		// Price crosses above VIDYA -> buy
		var crossUp = _prevClose <= _prevVidya && close > currentVidya;
		// Price crosses below VIDYA -> sell
		var crossDown = _prevClose >= _prevVidya && close < currentVidya;
		var minDistance = close * 0.001m;

		var volume = Volume;
		if (volume <= 0)
			volume = 1;

		if (crossUp && Math.Abs(close - currentVidya) >= minDistance)
		{
			if (Position <= 0)
				BuyMarket(Position < 0 ? Math.Abs(Position) + volume : volume);
		}
		else if (crossDown && Math.Abs(close - currentVidya) >= minDistance)
		{
			if (Position >= 0)
				SellMarket(Position > 0 ? Math.Abs(Position) + volume : volume);
		}

		_prevVidya = currentVidya;
		_prevClose = close;
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		_cmo = null;
		_prevVidya = null;
		_prevClose = null;

		base.OnReseted();
	}
}