在 GitHub 上查看

Nevalyashka Martingale 策略

概览

Nevalyashka Martingale 是 MetaTrader 5 专家顾问 “Nevalyashka3_1” 的移植版。策略只处理一个品种,通过在亏损后交替做多和做空来实现马丁格尔逻辑。启动时会先建立一个空头头寸。每次平仓后,策略都会检查账户权益:如果权益创出新高,下一笔交易使用基础手数并保持原方向;如果权益没有超过历史峰值,则将手数乘以系数并改变方向,以尝试弥补回撤。

运行机制

  • 首次交易:在第一根完成的 K 线收盘时,以基础手数开空。
  • 权益跟踪:策略保存权益峰值,只有在没有持仓时才比较当前权益和峰值。
    • 权益创新高 → 下一单沿用上一笔的方向,手数回到基础值。
    • 权益未创新高 → 下一单手数乘以系数,同时方向反向。
  • 止损/止盈:每个订单都带有固定距离的止损和止盈,单位为“点”(价格最小变动)。止损距离为 StopLossPoints,止盈距离为 TakeProfitPoints
  • 移动止损:当价格向有利方向运行 MoveProfitPoints 后,止损开始上移/下移。两次调整之间必须满足额外的 MoveStepPoints 缓冲,避免过于频繁地修改。止损超过进场价后,计划手数会除以系数,逐步回到基础规模。
  • 平仓:当蜡烛的最高价或最低价触及止损/止盈时立即市价平仓,然后根据最新权益决定下一次交易。

参数

  • BaseVolume:基础手数,适用于初始交易以及盈利周期(默认 0.1)。
  • VolumeMultiplier:在亏损后放大下一笔手数的倍数(默认 1.1)。
  • TakeProfitPoints:止盈距离,单位为点(默认 94)。
  • MoveProfitPoints:触发移动止损所需的最小盈利点数(默认 25)。
  • MoveStepPoints:两次移动止损之间必须额外满足的点差(默认 11)。
  • StopLossPoints:初始止损距离,单位为点(默认 70)。
  • CandleType:用于管理交易的蜡烛类型,默认使用 5 分钟周期。

持仓管理细节

  • _plannedVolume 对应 MT5 中的 Lot 变量,只会在平仓或止损超过进场价时更新。
  • AdjustVolume 会根据交易所的限制自动对齐手数,遵守 VolumeStepMinVolumeMaxVolume
  • GetPointValue 复刻 MT5 的 “adjusted point” 逻辑:如果报价有 3 或 5 位小数,则将点值乘以 10,以便以标准的 pip 为单位。
  • HandleLongPositionHandleShortPosition 通过蜡烛的最高价/最低价来模拟 MT5 中的止损移动与离场动作,无需额外的指标缓存。

使用提示

  • 策略默认只针对一个证券,请在启动前设置好 SecurityPortfolio
  • 马丁格尔在连续亏损时会迅速扩大风险,应谨慎调整 BaseVolumeVolumeMultiplier,并在有真实保证金约束的环境中测试。
  • 止损和止盈使用价格点差,请确保证券元数据(PriceStepVolumeStepMinVolume)完整,以避免与经纪商设置不一致。
  • 移动止损基于已完成的蜡烛计算,实盘中价格在蜡烛内的波动可能提前触发止损。
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>
/// Equity-based martingale strategy that alternates trade direction after losses.
/// Opens a short position on startup, resets volume after profitable cycles,
/// and increases exposure following drawdowns while managing fixed stops and targets.
/// </summary>
public class NevalyashkaMartingaleStrategy : Strategy
{
	private readonly StrategyParam<decimal> _baseVolume;
	private readonly StrategyParam<decimal> _volumeMultiplier;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _moveProfitPoints;
	private readonly StrategyParam<decimal> _moveStepPoints;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _plannedVolume;
	private decimal _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takePrice;
	private decimal _equityPeak;
	private bool _nextDirectionIsSell = true;
	private bool _initialOrderPlaced;

	/// <summary>
	/// Initializes <see cref="NevalyashkaMartingaleStrategy"/>.
	/// </summary>
	public NevalyashkaMartingaleStrategy()
	{
		_baseVolume = Param(nameof(BaseVolume), 0.1m)
			.SetDisplay("Base Volume", "Initial trade volume", "Risk")
			.SetGreaterThanZero()
			;

		_volumeMultiplier = Param(nameof(VolumeMultiplier), 1.1m)
			.SetDisplay("Volume Multiplier", "Multiplier applied after losses", "Risk")
			.SetGreaterThanZero()
			;

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 500m)
			.SetDisplay("Take Profit Points", "Profit target in points", "Orders")
			.SetGreaterThanZero()
			;

		_moveProfitPoints = Param(nameof(MoveProfitPoints), 100m)
			.SetDisplay("Move Profit Points", "Profit buffer before trailing activates", "Orders")
			.SetGreaterThanZero()
			;

		_moveStepPoints = Param(nameof(MoveStepPoints), 50m)
			.SetDisplay("Move Step Points", "Extra buffer for trailing stop updates", "Orders")
			.SetGreaterThanZero()
			;

		_stopLossPoints = Param(nameof(StopLossPoints), 400m)
			.SetDisplay("Stop Loss Points", "Initial protective distance", "Orders")
			.SetGreaterThanZero()
			;

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe for trade management", "General");
	}

	/// <summary>
	/// Base order volume.
	/// </summary>
	public decimal BaseVolume
	{
		get => _baseVolume.Value;
		set => _baseVolume.Value = value;
	}

	/// <summary>
	/// Multiplier applied when recovering from a loss.
	/// </summary>
	public decimal VolumeMultiplier
	{
		get => _volumeMultiplier.Value;
		set => _volumeMultiplier.Value = value;
	}

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

	/// <summary>
	/// Minimum profit in points before the stop is tightened.
	/// </summary>
	public decimal MoveProfitPoints
	{
		get => _moveProfitPoints.Value;
		set => _moveProfitPoints.Value = value;
	}

	/// <summary>
	/// Additional margin in points required between stop adjustments.
	/// </summary>
	public decimal MoveStepPoints
	{
		get => _moveStepPoints.Value;
		set => _moveStepPoints.Value = value;
	}

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

	/// <summary>
	/// Candle type used to drive the strategy.
	/// </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();

		_plannedVolume = 0m;
		_entryPrice = 0m;
		_stopPrice = null;
		_takePrice = null;
		_equityPeak = 0m;
		_nextDirectionIsSell = true;
		_initialOrderPlaced = false;
	}

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

		_equityPeak = Portfolio?.CurrentValue ?? 0m;
		_plannedVolume = AdjustVolume(BaseVolume);

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

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

		var point = GetPointValue();

		HandleOpenPosition(candle, point);

		if (Position != 0)
			return;

		var equity = Portfolio?.CurrentValue ?? 0m;

		if (!_initialOrderPlaced)
		{
			if (_plannedVolume == 0m)
			{
				_plannedVolume = AdjustVolume(BaseVolume);
				if (_plannedVolume == 0m)
					return;
			}

			if (OpenPosition(true, candle.ClosePrice, point))
			{
				_initialOrderPlaced = true;
				_nextDirectionIsSell = true;
			}

			return;
		}

		if (equity > _equityPeak)
		{
			_equityPeak = equity;
			_plannedVolume = AdjustVolume(BaseVolume);

			if (_plannedVolume == 0m)
				return;

			if (_nextDirectionIsSell)
			{
				if (OpenPosition(true, candle.ClosePrice, point))
					return;
			}
			else
			{
				if (OpenPosition(false, candle.ClosePrice, point))
					return;
			}
		}
		else
		{
			var increased = VolumeMultiplier > 0m ? AdjustVolume(_plannedVolume * VolumeMultiplier) : 0m;

			if (increased == 0m)
				return;

			_plannedVolume = increased;

			if (_nextDirectionIsSell)
			{
				if (OpenPosition(false, candle.ClosePrice, point))
					_nextDirectionIsSell = false;
			}
			else
			{
				if (OpenPosition(true, candle.ClosePrice, point))
					_nextDirectionIsSell = true;
			}
		}
	}

	private void HandleOpenPosition(ICandleMessage candle, decimal point)
	{
		if (Position > 0)
		{
			HandleLongPosition(candle, point);
		}
		else if (Position < 0)
		{
			HandleShortPosition(candle, point);
		}
	}

	private void HandleLongPosition(ICandleMessage candle, decimal point)
	{
		if (_stopPrice is not decimal currentStop || _takePrice is not decimal currentTake)
			return;

		var price = candle.ClosePrice;
		var moveThreshold = MoveProfitPoints * point;

		if (price - _entryPrice > moveThreshold)
		{
			var candidate = price - (StopLossPoints + MoveStepPoints) * point;

			if (candidate > currentStop)
			{
				var newStop = price - StopLossPoints * point;
				_stopPrice = newStop;

				if (_plannedVolume > AdjustVolume(BaseVolume) && newStop > _entryPrice)
					ReduceVolume();
			}
		}

		if (candle.LowPrice <= _stopPrice)
		{
			SellMarket();
			ResetProtection();
			return;
		}

		if (candle.HighPrice >= currentTake)
		{
			SellMarket();
			ResetProtection();
		}
	}

	private void HandleShortPosition(ICandleMessage candle, decimal point)
	{
		if (_stopPrice is not decimal currentStop || _takePrice is not decimal currentTake)
			return;

		var price = candle.ClosePrice;
		var moveThreshold = MoveProfitPoints * point;

		if (_entryPrice - price > moveThreshold)
		{
			var candidate = price + (StopLossPoints + MoveStepPoints) * point;

			if (candidate < currentStop)
			{
				var newStop = price + StopLossPoints * point;
				_stopPrice = newStop;

				if (_plannedVolume > AdjustVolume(BaseVolume) && newStop < _entryPrice)
					ReduceVolume();
			}
		}

		if (candle.HighPrice >= _stopPrice)
		{
			BuyMarket();
			ResetProtection();
			return;
		}

		if (candle.LowPrice <= currentTake)
		{
			BuyMarket();
			ResetProtection();
		}
	}

	private bool OpenPosition(bool isSell, decimal price, decimal point)
	{
		if (_plannedVolume <= 0m)
			return false;

		if (point <= 0m)
			return false;

		var stopOffset = StopLossPoints * point;
		var takeOffset = TakeProfitPoints * point;

		if (stopOffset <= 0m || takeOffset <= 0m)
			return false;

		if (isSell)
		{
			SellMarket();
			_stopPrice = price + stopOffset;
			_takePrice = price - takeOffset;
		}
		else
		{
			BuyMarket();
			_stopPrice = price - stopOffset;
			_takePrice = price + takeOffset;
		}

		_entryPrice = price;
		return true;
	}

	private void ReduceVolume()
	{
		if (VolumeMultiplier <= 0m)
			return;

		var baseVolume = AdjustVolume(BaseVolume);

		if (baseVolume == 0m)
			return;

		var reduced = AdjustVolume(_plannedVolume / VolumeMultiplier);

		if (reduced < baseVolume)
			reduced = baseVolume;

		_plannedVolume = reduced;
	}

	private void ResetProtection()
	{
		_stopPrice = null;
		_takePrice = null;
		_entryPrice = 0m;
	}

	private decimal AdjustVolume(decimal volume)
	{
		if (Security is null)
			return volume;

		if (volume <= 0m)
			return 0m;

		var step = Security.VolumeStep ?? 0m;

		if (step > 0m)
			volume = Math.Floor(volume / step) * step;

		var min = Security.MinVolume ?? 0m;
		if (min > 0m && volume < min)
			return 0m;

		var max = Security.MaxVolume ?? 0m;
		if (max > 0m && volume > max)
			volume = max;

		return volume;
	}

	private decimal GetPointValue()
	{
		var step = Security?.PriceStep ?? 0m;

		if (step <= 0m)
			step = 1m;

		if (step <= 0m)
			return 1m;

		var digits = 0;
		var value = step;

		while (value < 1m && digits < 10)
		{
			value *= 10m;
			digits++;
		}

		if (digits == 3 || digits == 5)
			step *= 10m;

		return step;
	}
}