View on GitHub

Zakryvator Strategy

The Zakryvator strategy is a risk management module that monitors the current open position and closes it when the unrealized loss exceeds a predefined threshold. The allowed loss depends on the position volume, replicating the logic of the original MQL script where different lot sizes correspond to different maximum drawdowns.

This strategy does not generate entries by itself. Positions are expected to be opened manually or by another strategy. Zakryvator simply protects the account by exiting losing trades automatically.

Details

  • Entry Criteria: None. The strategy only manages existing positions.
  • Exit Criteria: Closes the current position once the loss reaches the configured threshold for its volume.
  • Long/Short: Both directions are supported.
  • Stops: Uses fixed monetary loss limits that vary with the position size.
  • Filters: No additional filters.

Parameters

Parameter Description
Min001002 Maximum loss for positions with volume ≤ 0.02 lots.
Min002005 Maximum loss for positions with volume between 0.02 and 0.05 lots.
Min00501 Maximum loss for positions with volume between 0.05 and 0.10 lots.
Min0103 Maximum loss for positions with volume between 0.10 and 0.30 lots.
Min0305 Maximum loss for positions with volume between 0.30 and 0.50 lots.
Min051 Maximum loss for positions with volume between 0.50 and 1 lot.
MinFrom1 Maximum loss for positions with volume greater than 1 lot.

Behavior

  1. The strategy subscribes to trade ticks to track real-time prices.
  2. On each tick it calculates the unrealized PnL using the current price and the average entry price.
  3. If the loss exceeds the threshold corresponding to the current position volume, the position is closed at market.

This makes Zakryvator a simple but effective tool for limiting drawdowns based on trade size.

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>
/// Strategy that opens positions using SMA crossover and closes them
/// when unrealized loss exceeds a volume-based threshold ("Zakryvator" = position closer on loss).
/// </summary>
public class ZakryvatorStrategy : Strategy
{
	private decimal _entryPrice;
	private decimal _lastPrice;
	private bool _prevShortAboveLong;

	private readonly SimpleMovingAverage _smaShort = new() { Length = 50 };
	private readonly SimpleMovingAverage _smaLong = new() { Length = 150 };

	private readonly StrategyParam<int> _shortPeriod;
	private readonly StrategyParam<int> _longPeriod;
	private readonly StrategyParam<decimal> _lossThreshold;

	/// <summary>Short SMA period.</summary>
	public int ShortPeriod { get => _shortPeriod.Value; set => _shortPeriod.Value = value; }

	/// <summary>Long SMA period.</summary>
	public int LongPeriod { get => _longPeriod.Value; set => _longPeriod.Value = value; }

	/// <summary>Maximum unrealized loss before closing position.</summary>
	public decimal LossThreshold { get => _lossThreshold.Value; set => _lossThreshold.Value = value; }

	/// <summary>Constructor.</summary>
	public ZakryvatorStrategy()
	{
		_shortPeriod = Param(nameof(ShortPeriod), 50)
			.SetDisplay("Short SMA", "Short SMA period for entry signal", "Entry");
		_longPeriod = Param(nameof(LongPeriod), 150)
			.SetDisplay("Long SMA", "Long SMA period for entry signal", "Entry");
		_lossThreshold = Param(nameof(LossThreshold), 500m)
			.SetDisplay("Loss Threshold", "Max unrealized loss before closing position", "Risk");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
		=> [(Security, TimeSpan.FromMinutes(5).TimeFrame())];

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

		_smaShort.Length = ShortPeriod;
		_smaLong.Length = LongPeriod;

		var subscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());

		subscription
			.Bind(_smaShort, _smaLong, ProcessCandle)
			.Start();
	}

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

		if (!_smaShort.IsFormed || !_smaLong.IsFormed)
			return;

		_lastPrice = candle.ClosePrice;

		var shortAboveLong = shortSma > longSma;

		// Check loss threshold for open position
		if (Position != 0 && _entryPrice != 0m)
		{
			var openPnL = Position * (_lastPrice - _entryPrice);

			if (openPnL <= -LossThreshold)
			{
				// Close on loss
				if (Position > 0)
					SellMarket();
				else
					BuyMarket();

				_entryPrice = 0m;
				_prevShortAboveLong = shortAboveLong;
				return;
			}
		}

		// SMA crossover entry/exit logic
		var crossUp = shortAboveLong && !_prevShortAboveLong;
		var crossDown = !shortAboveLong && _prevShortAboveLong;

		if (crossUp)
		{
			if (Position < 0)
			{
				BuyMarket();
				_entryPrice = 0m;
			}

			if (Position == 0)
			{
				BuyMarket();
				_entryPrice = _lastPrice;
			}
		}
		else if (crossDown)
		{
			if (Position > 0)
			{
				SellMarket();
				_entryPrice = 0m;
			}

			if (Position == 0)
			{
				SellMarket();
				_entryPrice = _lastPrice;
			}
		}

		_prevShortAboveLong = shortAboveLong;
	}

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

		_entryPrice = 0m;
		_lastPrice = 0m;
		_prevShortAboveLong = false;

		_smaShort.Reset();
		_smaLong.Reset();
	}
}