在 GitHub 上查看

均线与价格交叉策略

概述

该目录提供了 MQL/50198 中两个 MetaTrader 5 示例的 C# 移植版本:

  • MovingAveragePriceCrossStrategy —— 简洁的均线与收盘价交叉策略,仅持有一笔仓位。
  • MovingAverageMartingaleStrategy —— 在保持相同信号逻辑的同时,引入亏损后加倍的马丁加仓机制。

两种策略都基于 StockSharp 高级 API,使用蜡烛线订阅来计算信号,并以 MetaTrader “点” 的方式设置止损和止盈距离。

文件

文件 说明
CS/MovingAveragePriceCrossStrategy.cs 固定仓位与静态保护的基础均线交叉实现。
CS/MovingAverageMartingaleStrategy.cs 亏损后放大仓位和保护距离的马丁版本。

交易逻辑

MovingAveragePriceCrossStrategy

  1. 订阅指定周期的蜡烛线,并计算简单移动平均线 (SMA)。
  2. 仅在蜡烛线收盘后触发逻辑,与 MT5 专家顾问保持一致。
  3. 使用最近两根已完成蜡烛的收盘价与 SMA 位置判断交叉:
    • 做空:当 SMA 从下方穿越到价格上方(价格跌破均线)。
    • 做多:当 SMA 从上方穿越到价格下方(价格突破均线)。
  4. 若当前无持仓,则按信号提交市价单。
  5. 通过 StartProtection 将以点为单位的止损、止盈转换为绝对价格偏移并自动下单。

MovingAverageMartingaleStrategy

  1. 与基础策略共用蜡烛数据与 SMA 信号判断。
  2. 在仓位平仓后记录实现盈亏,保存最近一次交易结果。
  3. 当出现新的交叉信号且没有持仓时:
    • 最近一笔为亏损:按 VolumeMultiplier 放大下一笔仓位(不超过 MaxVolume),并将止损、止盈距离按 TargetMultiplier 扩大。
    • 最近一笔为盈利:将仓位和保护距离恢复为初始值。
  4. 在发送市价单之前,根据当前距离重新调用 StartProtection
  5. 始终只维持一笔仓位,等同于原版专家的 PositionsTotal() 检查。

风险控制

  • 止损与止盈距离以 MT5 “点”为单位,根据 PriceStep 自动转换为价格偏移;对于 3/5 位小数的外汇品种会额外乘以 10。
  • 马丁版本对距离倍数做了上限限制,避免无限扩张。
  • 下单量会根据 VolumeStepMinVolume 以及可选的 MaxVolume 自动对齐,确保报单有效。

参数

通用参数

参数 策略 默认值 说明
CandleType 两者 1 minute 用于计算信号的蜡烛类型。
MaPeriod 两者 50 简单移动平均线的周期。

MovingAveragePriceCrossStrategy

参数 默认值 说明
OrderVolume 1 每次下单的基础仓位,会按交易品种步长对齐。
TakeProfitPoints 150 止盈距离(点),0 表示不设置。
StopLossPoints 150 止损距离(点),0 表示不设置。

MovingAverageMartingaleStrategy

参数 默认值 说明
StartingVolume 1 盈利后恢复的初始仓位。
MaxVolume 5 在加倍后允许的最大仓位。
TakeProfitPoints 100 初始止盈距离(点)。
StopLossPoints 300 初始止损距离(点)。
VolumeMultiplier 2 亏损后用于放大下一笔仓位的倍数。
TargetMultiplier 2 亏损后用于放大止损与止盈距离的倍数。

使用提示

  • MT5 的“点”通常等于一个 PriceStep,策略会自动识别是否需要乘以 10 以匹配外汇报价。
  • 策略在持仓期间忽略所有新信号,与 PositionsTotal() 判断相一致。
  • 可以在 StockSharp 设计器中对暴露的参数进行优化,以复现 MT5 的参数调优流程。
namespace StockSharp.Samples.Strategies;

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;

using StockSharp.Algo;

/// <summary>
/// Moving average crossover strategy with martingale money management converted from the MT5 "MovingAverageMartinGale" expert advisor.
/// Scales trade volume and protective distances after losses while resetting to the base configuration after profitable trades.
/// </summary>
public class MovingAverageMartingaleStrategy : Strategy
{
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<decimal> _startingVolume;
	private readonly StrategyParam<decimal> _maxVolume;
	private readonly StrategyParam<int> _takeProfitPoints;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<decimal> _volumeMultiplier;
	private readonly StrategyParam<decimal> _targetMultiplier;
	private readonly StrategyParam<DataType> _candleType;

	private SMA _sma;
	private decimal? _previousClose;
	private decimal? _previousMa;
	private decimal? _currentClose;
	private decimal? _currentMa;
	private decimal _pipSize;

	private decimal _currentVolume;
	private decimal _currentTakeProfitPoints;
	private decimal _currentStopLossPoints;
	private decimal _lastRealizedPnL;
	private decimal _previousPosition;
	private decimal _lastTradeResult;

	/// <summary>
	/// Initializes a new instance of the <see cref="MovingAverageMartingaleStrategy"/> class.
	/// </summary>
	public MovingAverageMartingaleStrategy()
	{
		_maPeriod = Param(nameof(MaPeriod), 50)
			.SetGreaterThanZero()
			.SetDisplay("MA period", "Length of the simple moving average used for entries.", "Indicator")
			;

		_startingVolume = Param(nameof(StartingVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Starting volume", "Base order volume used after profitable trades.", "Money management")
			;

		_maxVolume = Param(nameof(MaxVolume), 5m)
			.SetGreaterThanZero()
			.SetDisplay("Maximum volume", "Upper limit for martingale scaling.", "Money management")
			;

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 100)
			.SetNotNegative()
			.SetDisplay("Take profit (points)", "Initial profit target distance expressed in MetaTrader points.", "Risk")
			;

		_stopLossPoints = Param(nameof(StopLossPoints), 300)
			.SetNotNegative()
			.SetDisplay("Stop loss (points)", "Initial stop-loss distance expressed in MetaTrader points.", "Risk")
			;

		_volumeMultiplier = Param(nameof(VolumeMultiplier), 2m)
			.SetGreaterThanZero()
			.SetDisplay("Volume multiplier", "Factor applied to the next trade volume after a loss.", "Money management")
			;

		_targetMultiplier = Param(nameof(TargetMultiplier), 2m)
			.SetGreaterThanZero()
			.SetDisplay("Target multiplier", "Factor applied to stop-loss and take-profit distances after a loss.", "Money management")
			;

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle type", "Primary timeframe processed by the strategy.", "General");
	}

	/// <summary>
	/// Moving average period used for generating signals.
	/// </summary>
	public int MaPeriod
	{
		get => _maPeriod.Value;
		set => _maPeriod.Value = value;
	}

	/// <summary>
	/// Base position volume restored after profitable trades.
	/// </summary>
	public decimal StartingVolume
	{
		get => _startingVolume.Value;
		set => _startingVolume.Value = value;
	}

	/// <summary>
	/// Maximum position volume allowed by the martingale logic.
	/// </summary>
	public decimal MaxVolume
	{
		get => _maxVolume.Value;
		set => _maxVolume.Value = value;
	}

	/// <summary>
	/// Initial take-profit distance expressed in MetaTrader points.
	/// </summary>
	public int TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Initial stop-loss distance expressed in MetaTrader points.
	/// </summary>
	public int StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Multiplier applied to the trade volume after a losing trade.
	/// </summary>
	public decimal VolumeMultiplier
	{
		get => _volumeMultiplier.Value;
		set => _volumeMultiplier.Value = value;
	}

	/// <summary>
	/// Multiplier applied to stop-loss and take-profit distances after a losing trade.
	/// </summary>
	public decimal TargetMultiplier
	{
		get => _targetMultiplier.Value;
		set => _targetMultiplier.Value = value;
	}

	/// <summary>
	/// Candle type used to read market data.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

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

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

		_sma = null;
		_previousClose = null;
		_previousMa = null;
		_currentClose = null;
		_currentMa = null;
		_pipSize = 0m;

		_currentVolume = 0m;
		_currentTakeProfitPoints = 0m;
		_currentStopLossPoints = 0m;
		_lastRealizedPnL = 0m;
		_previousPosition = 0m;
		_lastTradeResult = 0m;
	}

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

		_pipSize = CalculatePipSize();

		_currentVolume = NormalizeVolume(StartingVolume);
		_currentTakeProfitPoints = TakeProfitPoints;
		_currentStopLossPoints = StopLossPoints;
		_lastRealizedPnL = PnL;
		_previousPosition = Position;
		_lastTradeResult = 0m;

		_sma = new SMA { Length = MaPeriod };

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

	/// <inheritdoc />
	protected override void OnPositionReceived(Position position)
	{
		base.OnPositionReceived(position);

		if (_previousPosition != 0m && Position == 0m)
		{
			var tradePnL = PnL - _lastRealizedPnL;
			_lastRealizedPnL = PnL;
			_lastTradeResult = tradePnL;
		}

		_previousPosition = Position;
	}

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

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		if (_currentClose is null)
		{
			_currentClose = candle.ClosePrice;
			_currentMa = maValue;
			return;
		}

		if (_previousClose is null)
		{
			_previousClose = _currentClose;
			_previousMa = _currentMa;
			_currentClose = candle.ClosePrice;
			_currentMa = maValue;
			return;
		}

		if (_sma?.IsFormed != true)
		{
			_previousClose = _currentClose;
			_previousMa = _currentMa;
			_currentClose = candle.ClosePrice;
			_currentMa = maValue;
			return;
		}

		var previousClose = _previousClose.Value;
		var previousMa = _previousMa!.Value;
		var currentClose = _currentClose.Value;
		var currentMa = _currentMa!.Value;

		var crossedBelowPrice = previousMa < previousClose && currentMa > currentClose;
		var crossedAbovePrice = previousMa > previousClose && currentMa < currentClose;

		if (Position == 0m && (crossedBelowPrice || crossedAbovePrice))
		{
			ApplyMartingaleAdjustments();
		}

		if (Position == 0m)
		{
			var volume = NormalizeVolume(_currentVolume);

			if (volume <= 0m)
			{
				ShiftBuffers(candle, maValue);
				return;
			}

			if (crossedBelowPrice)
			{
				ApplyProtection();
				SellMarket(volume);
			}
			else if (crossedAbovePrice)
			{
				ApplyProtection();
				BuyMarket(volume);
			}
		}

		ShiftBuffers(candle, maValue);
	}

	private void ApplyMartingaleAdjustments()
	{
		if (_lastTradeResult < 0m)
		{
			var nextVolume = Math.Min(_currentVolume * VolumeMultiplier, MaxVolume);
			_currentVolume = NormalizeVolume(nextVolume);

			_currentTakeProfitPoints = Math.Min(_currentTakeProfitPoints * TargetMultiplier, 100000m);
			_currentStopLossPoints = Math.Min(_currentStopLossPoints * TargetMultiplier, 100000m);
		}
		else if (_lastTradeResult > 0m)
		{
			_currentVolume = NormalizeVolume(StartingVolume);
			_currentTakeProfitPoints = TakeProfitPoints;
			_currentStopLossPoints = StopLossPoints;
		}

		_lastTradeResult = 0m;
	}

	private void ApplyProtection()
	{
		var stopDistance = _currentStopLossPoints > 0m ? _currentStopLossPoints * _pipSize : 0m;
		var takeDistance = _currentTakeProfitPoints > 0m ? _currentTakeProfitPoints * _pipSize : 0m;

		StartProtection(
			stopLoss: stopDistance > 0m ? new Unit(stopDistance, UnitTypes.Absolute) : null,
			takeProfit: takeDistance > 0m ? new Unit(takeDistance, UnitTypes.Absolute) : null);

		Volume = NormalizeVolume(_currentVolume);
	}

	private void ShiftBuffers(ICandleMessage candle, decimal maValue)
	{
		_previousClose = _currentClose;
		_previousMa = _currentMa;
		_currentClose = candle.ClosePrice;
		_currentMa = maValue;
	}

	private decimal NormalizeVolume(decimal volume)
	{
		var step = Security?.VolumeStep ?? 1m;
		if (step <= 0m)
			step = 1m;

		var minVolume = Security?.MinVolume ?? step;
		if (volume < minVolume)
			volume = minVolume;

		var multiplier = volume / step;
		var rounded = Math.Round(multiplier, MidpointRounding.AwayFromZero) * step;

		if (rounded < minVolume)
			rounded = minVolume;

		var maxVolume = Security?.MaxVolume;
		if (maxVolume is decimal max && rounded > max)
			rounded = max;

		rounded = Math.Min(rounded, MaxVolume);

		return Math.Max(rounded, step);
	}

	private decimal CalculatePipSize()
	{
		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m)
			step = 1m;

		var decimals = Security?.Decimals ?? 0;
		if (decimals == 3 || decimals == 5)
			step *= 10m;

		return step;
	}
}