在 GitHub 上查看

EMA WMA 逆势策略

该策略以蜡烛的开盘价计算指数移动平均线(EMA)与加权移动平均线(WMA),在两者发生反向穿越时进行逆势交易。EMA 从上向下穿越 WMA 时买入,期待价格回归;EMA 从下向上穿越 WMA 时做空。仓位规模依据风险百分比和止损距离动态调整,同时提供固定止损、止盈和跟踪止损以控制风险。

细节

  • 入场条件
    • 多头:EMA(Open) 从上向下穿越 WMA(Open)
    • 空头:EMA(Open) 从下向上穿越 WMA(Open)
  • 方向:双向
  • 离场条件
    • 固定止损(按价格步长)
    • 固定止盈(按价格步长)
    • 当价格至少上涨 TrailingStopPoints + TrailingStepPoints 后,跟踪止损上移或下移
    • 反向穿越会关闭当前仓位并开出相反方向
  • 止损:止损、止盈和跟踪止损
  • 默认值
    • EmaPeriod = 28
    • WmaPeriod = 8
    • StopLossPoints = 50m
    • TakeProfitPoints = 50m
    • TrailingStopPoints = 50m
    • TrailingStepPoints = 10m
    • RiskPercent = 10m
    • BaseVolume = 1m
    • CandleType = TimeSpan.FromMinutes(1).TimeFrame()
  • 筛选标签
    • 类别:移动平均线,逆势
    • 方向:多头与空头
    • 指标:EMA (open)、WMA (open)
    • 止损:有(固定止损 + 跟踪止损)
    • 复杂度:中等
    • 周期:日内(默认 1 分钟)
    • 季节性:无
    • 神经网络:无
    • 背离:无
    • 风险等级:中等

参数

参数 说明
EmaPeriod, WmaPeriod 以开盘价计算的 EMA 与 WMA 周期。
StopLossPoints, TakeProfitPoints 止损与止盈距离(价格步长单位)。
TrailingStopPoints 跟踪止损与当前价格之间的距离。
TrailingStepPoints 跟踪止损调整前所需的额外盈利距离,启用跟踪时必须为正。
RiskPercent 每笔交易冒风险的账户百分比,头寸规模按 RiskPercent / (StopLossPoints * PriceStep) 计算。
BaseVolume 无法计算风险仓位时使用的最小下单量。
CandleType 参与计算的蜡烛类型(默认 1 分钟)。

说明

  • 两条均线都使用开盘价,与原始 MetaTrader 专家顾问保持一致。
  • 跟踪止损仅在价格向有利方向移动至少 TrailingStopPoints + TrailingStepPoints 后才开始移动。
  • 如果设置了 TrailingStopPointsTrailingStepPoints 小于等于 0,策略会立即停止以避免不一致的跟踪行为。
  • 当账户权益、价格步长或止损距离不可用时,会回退到 BaseVolume 作为下单数量。
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>
/// Contrarian crossover between EMA and WMA calculated on candle open prices.
/// Opens a long position when EMA crosses below WMA and a short position on the opposite cross.
/// Supports fixed stop-loss, take-profit, and trailing stop management plus risk-based position sizing.
/// </summary>
public class EmaWmaContrarianStrategy : Strategy
{
	private readonly StrategyParam<int> _emaPeriod;
	private readonly StrategyParam<int> _wmaPeriod;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _trailingStopPoints;
	private readonly StrategyParam<decimal> _trailingStepPoints;
	private readonly StrategyParam<decimal> _riskPercent;
	private readonly StrategyParam<decimal> _baseVolume;
	private readonly StrategyParam<DataType> _candleType;

	private ExponentialMovingAverage _ema;
	private WeightedMovingAverage _wma;
	private bool _hasPrevious;
	private decimal _previousEma;
	private decimal _previousWma;

	private decimal? _entryPrice;
	private decimal? _stopLossPrice;
	private decimal? _takeProfitPrice;

	/// <summary>
	/// EMA period.
	/// </summary>
	public int EmaPeriod
	{
		get => _emaPeriod.Value;
		set => _emaPeriod.Value = value;
	}

	/// <summary>
	/// WMA period.
	/// </summary>
	public int WmaPeriod
	{
		get => _wmaPeriod.Value;
		set => _wmaPeriod.Value = value;
	}

	/// <summary>
	/// Stop-loss in price steps.
	/// </summary>
	public decimal StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Take-profit in price steps.
	/// </summary>
	public decimal TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Trailing stop distance in price steps.
	/// </summary>
	public decimal TrailingStopPoints
	{
		get => _trailingStopPoints.Value;
		set => _trailingStopPoints.Value = value;
	}

	/// <summary>
	/// Trailing stop step in price steps.
	/// </summary>
	public decimal TrailingStepPoints
	{
		get => _trailingStepPoints.Value;
		set => _trailingStepPoints.Value = value;
	}

	/// <summary>
	/// Risk percentage used for position sizing.
	/// </summary>
	public decimal RiskPercent
	{
		get => _riskPercent.Value;
		set => _riskPercent.Value = value;
	}

	/// <summary>
	/// Minimum contract volume used when risk sizing cannot be applied.
	/// </summary>
	public decimal BaseVolume
	{
		get => _baseVolume.Value;
		set => _baseVolume.Value = value;
	}

	/// <summary>
	/// Candle type used for calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of <see cref="EmaWmaContrarianStrategy"/>.
	/// </summary>
	public EmaWmaContrarianStrategy()
	{
		_emaPeriod = Param(nameof(EmaPeriod), 28)
			.SetGreaterThanZero()
			.SetDisplay("EMA Period", "EMA length calculated on candle open prices", "Indicators")
			
			.SetOptimize(10, 60, 2);

		_wmaPeriod = Param(nameof(WmaPeriod), 8)
			.SetGreaterThanZero()
			.SetDisplay("WMA Period", "WMA length calculated on candle open prices", "Indicators")
			
			.SetOptimize(4, 40, 2);

		_stopLossPoints = Param(nameof(StopLossPoints), 50m)
			.SetDisplay("Stop Loss (points)", "Stop-loss distance expressed in price steps", "Risk")
			
			.SetOptimize(10m, 150m, 10m);

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 50m)
			.SetDisplay("Take Profit (points)", "Take-profit distance expressed in price steps", "Risk")
			
			.SetOptimize(10m, 200m, 10m);

		_trailingStopPoints = Param(nameof(TrailingStopPoints), 50m)
			.SetDisplay("Trailing Stop (points)", "Trailing stop distance expressed in price steps", "Risk")
			
			.SetOptimize(10m, 150m, 10m);

		_trailingStepPoints = Param(nameof(TrailingStepPoints), 10m)
			.SetDisplay("Trailing Step (points)", "Minimal favorable move before the trailing stop is advanced", "Risk")
			
			.SetOptimize(5m, 50m, 5m);

		_riskPercent = Param(nameof(RiskPercent), 10m)
			.SetDisplay("Risk Percent", "Portfolio percentage risked per trade", "Position Sizing")
			
			.SetOptimize(2m, 20m, 2m);

		_baseVolume = Param(nameof(BaseVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Base Volume", "Fallback volume when risk sizing is unavailable", "Position Sizing");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Primary candle type used for indicators", "General");
	}

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

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

		_ema = null;
		_wma = null;
		_hasPrevious = false;
		_previousEma = 0m;
		_previousWma = 0m;
		ClearPositionState();
	}

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

		// Validate trailing configuration to match original expert advisor behaviour.
		if (TrailingStopPoints > 0 && TrailingStepPoints <= 0)
		{
			Stop();
			return;
		}

		_ema = new ExponentialMovingAverage { Length = EmaPeriod };
		_wma = new WeightedMovingAverage { Length = WmaPeriod };

		// Subscribe to candles and connect indicators.
		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(ProcessCandle);
		subscription.Start();

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

	private void ProcessCandle(ICandleMessage candle)
	{
		// Process only finished candles to avoid intrabar noise.
		if (candle.State != CandleStates.Finished)
			return;

		if (_ema == null || _wma == null)
			return;

		// Evaluate protective logic before generating new signals.
		ManageActivePosition(candle);

		var emaValue = _ema.Process(new DecimalIndicatorValue(_ema, candle.OpenPrice, candle.OpenTime) { IsFinal = true });
		var wmaValue = _wma.Process(new DecimalIndicatorValue(_wma, candle.OpenPrice, candle.OpenTime) { IsFinal = true });

		// Ensure indicators produced valid values.
		if (emaValue.IsEmpty || wmaValue.IsEmpty || !_ema.IsFormed || !_wma.IsFormed)
			return;

		var ema = emaValue.ToDecimal();
		var wma = wmaValue.ToDecimal();

		if (!_hasPrevious)
		{
			_previousEma = ema;
			_previousWma = wma;
			_hasPrevious = true;
			return;
		}

		// Detect crossovers on open-price moving averages.
		var buySignal = ema < wma && _previousEma > _previousWma;
		var sellSignal = ema > wma && _previousEma < _previousWma;

		if (buySignal)
		{
			EnterLong(candle);
		}
		else if (sellSignal)
		{
			EnterShort(candle);
		}

		_previousEma = ema;
		_previousWma = wma;
	}

	private void ManageActivePosition(ICandleMessage candle)
	{
		if (Position > 0)
		{
			// Manage long position exits.
			var currentPrice = candle.ClosePrice;

			// Exit long when take-profit is reached.
			if (_takeProfitPrice is decimal tp && currentPrice >= tp)
			{
				SellMarket(Position);
				ClearPositionState();
				return;
			}

			// Exit long when stop-loss is hit.
			if (_stopLossPrice is decimal sl && currentPrice <= sl)
			{
				SellMarket(Position);
				ClearPositionState();
				return;
			}

			// Advance trailing stop for long trades.
			if (_entryPrice is decimal entry)
				UpdateTrailingForLong(currentPrice, entry);
		}
		else if (Position < 0)
		{
			// Manage short position exits.
			var currentPrice = candle.ClosePrice;

			// Exit short when take-profit is reached.
			if (_takeProfitPrice is decimal tp && currentPrice <= tp)
			{
				BuyMarket(Math.Abs(Position));
				ClearPositionState();
				return;
			}

			// Exit short when stop-loss is hit.
			if (_stopLossPrice is decimal sl && currentPrice >= sl)
			{
				BuyMarket(Math.Abs(Position));
				ClearPositionState();
				return;
			}

			// Advance trailing stop for short trades.
			if (_entryPrice is decimal entry)
				UpdateTrailingForShort(currentPrice, entry);
		}
		else
		{
			ClearPositionState();
		}
	}

	private void EnterLong(ICandleMessage candle)
	{
		var entryPrice = candle.ClosePrice;
		var volume = CalculateTradeVolume();

		if (volume <= 0)
			return;

		if (Position < 0)
		{
			// Close an existing short position before opening a new long.
			BuyMarket(Math.Abs(Position));
			ClearPositionState();
		}

		// Open the new long trade.
		BuyMarket(volume);
		SetupRiskLevels(entryPrice, true);
	}

	private void EnterShort(ICandleMessage candle)
	{
		var entryPrice = candle.ClosePrice;
		var volume = CalculateTradeVolume();

		if (volume <= 0)
			return;

		if (Position > 0)
		{
			// Close an existing long position before opening a new short.
			SellMarket(Position);
			ClearPositionState();
		}

		// Open the new short trade.
		SellMarket(volume);
		SetupRiskLevels(entryPrice, false);
	}

	private void SetupRiskLevels(decimal entryPrice, bool isLong)
	{
		var priceStep = Security?.PriceStep ?? 1m;
		var stopDistance = StopLossPoints > 0 ? StopLossPoints * priceStep : (decimal?)null;
		var takeProfitDistance = TakeProfitPoints > 0 ? TakeProfitPoints * priceStep : (decimal?)null;

		// Remember entry price for managing exits.
		_entryPrice = entryPrice;
		_stopLossPrice = stopDistance.HasValue
			? isLong ? entryPrice - stopDistance.Value : entryPrice + stopDistance.Value
			: null;

		_takeProfitPrice = takeProfitDistance.HasValue
			? isLong ? entryPrice + takeProfitDistance.Value : entryPrice - takeProfitDistance.Value
			: null;
	}

	private decimal CalculateTradeVolume()
	{
		// Default to configured base volume.
		var volume = BaseVolume;
		var portfolioValue = Portfolio?.CurrentValue ?? 0m;
		var priceStep = Security?.PriceStep ?? 1m;
		var stopDistance = StopLossPoints * priceStep;

		// Risk-based sizing uses stop distance to allocate capital.
		if (RiskPercent > 0 && portfolioValue > 0 && stopDistance > 0)
		{
			var riskCapital = portfolioValue * (RiskPercent / 100m);
			if (riskCapital > 0)
			{
				var rawVolume = riskCapital / stopDistance;
				var adjusted = AdjustVolume(rawVolume);
				if (adjusted > 0)
					volume = adjusted;
			}
		}

		return volume;
	}

	private decimal AdjustVolume(decimal volume)
	{
		// Align volume with instrument volume step.
		var step = Security?.VolumeStep ?? 1m;
		if (step <= 0)
			step = 1m;

		var adjusted = Math.Floor(volume / step) * step;
		if (adjusted <= 0)
			adjusted = step;

		var minVolume = Security?.VolumeStep ?? step;
		if (minVolume > 0 && adjusted < minVolume)
			adjusted = minVolume;

		return adjusted;
	}

	private void UpdateTrailingForLong(decimal currentPrice, decimal entryPrice)
	{
		if (TrailingStopPoints <= 0)
			return;

		var priceStep = Security?.PriceStep ?? 1m;
		var trailingDistance = TrailingStopPoints * priceStep;
		var trailingStep = TrailingStepPoints * priceStep;

		// Trailing stop only applies after sufficient favorable movement.
		if (currentPrice - entryPrice <= trailingDistance + trailingStep)
			return;

		var comparisonLevel = currentPrice - (trailingDistance + trailingStep);
		// Raise stop-loss closer to current price.
		if (_stopLossPrice is not decimal existing || existing < comparisonLevel)
			_stopLossPrice = currentPrice - trailingDistance;
	}

	private void UpdateTrailingForShort(decimal currentPrice, decimal entryPrice)
	{
		if (TrailingStopPoints <= 0)
			return;

		var priceStep = Security?.PriceStep ?? 1m;
		var trailingDistance = TrailingStopPoints * priceStep;
		var trailingStep = TrailingStepPoints * priceStep;

		// Trailing stop only applies after sufficient favorable movement.
		if (entryPrice - currentPrice <= trailingDistance + trailingStep)
			return;

		var comparisonLevel = currentPrice + trailingDistance + trailingStep;
		// Lower stop-loss toward market for short trades.
		if (_stopLossPrice is not decimal existing || existing > comparisonLevel)
			_stopLossPrice = currentPrice + trailingDistance;
	}

	private void ClearPositionState()
	{
		_entryPrice = null;
		_stopLossPrice = null;
		_takeProfitPrice = null;
	}
}