Открыть на GitHub

Стратегия фиксированного риска

Общее описание

Стратегия фиксированного риска представляет собой перенос эксперта MetaTrader 5 Money Fixed Risk.mq5. Оригинальный советник регулярно определяет максимально допустимый объём позиции при ограничении риска фиксированным процентом от капитала, после чего открывает рыночную покупку с симметричными уровнями стоп-лосса и тейк-профита. Реализация на StockSharp повторяет этот алгоритм, используя подписку на тиковые сделки и встроенные средства управления риском.

Стратегия обрабатывает каждый тик выбранного инструмента. Когда внутренний счётчик достигает заданного количества тиков, стратегия переоценивает размер позиции: считывает текущую стоимость портфеля, переводит заданный стоп в пунктах в абсолютное расстояние по цене и вычисляет объём, соответствующий целевому проценту риска. Если вычисленный объём удовлетворяет биржевым ограничениям, отправляется рыночная покупка, а уровни стоп-лосса и тейк-профита фиксируются на равном расстоянии от цены входа. В дальнейшем на каждом тике контролируется достижение этих уровней и позиция закрывается при первом срабатывании.

Требования к данным

  • Необходим поток тиковых сделок (SubscribeTrades()), поскольку именно по тикам ведётся счётчик.
  • Для корректного расчёта объёма должны быть заданы свойства инструмента: PriceStep, StepPrice, VolumeStep, MinVolume и при необходимости MaxVolume.

Логика работы

  1. Подписаться на сделки по инструменту.
  2. На каждом тике увеличивать счётчик.
  3. При достижении счётчиком значения Ticks Interval выполнить пересчёт:
    • Определить размер пункта на основе PriceStep и количества знаков (Decimals). Для котировок с 3 или 5 знаками используется множитель 10.
    • Перевести стоп-лосс из пунктов в абсолютное расстояние по цене.
    • Получить величину капитала портфеля (CurrentValue, затем CurrentBalance, затем BeginValue).
    • Рассчитать денежный риск на один контракт и максимально допустимый объём с учётом заданного процента риска.
    • Привести объём к шагу торгов и ограничениям MinVolume/MaxVolume.
  4. Если нормализованный объём положительный, закрыть возможный шорт и открыть длинную позицию рассчитанного размера.
  5. Запомнить уровни стоп-лосса и тейк-профита и следить за их достижением по каждому тику.

Параметры

  • Stop Loss (pips) – величина стоп-лосса в пунктах (тейк-профит размещается на таком же расстоянии).
  • Risk % – доля капитала, которой стратегия готова рискнуть в одной сделке.
  • Ticks Interval – количество тиков между очередными расчётами позиции и попытками входа.

Все параметры проходят проверку на положительность и доступны для оптимизации.

Особенности управления риском

  • Риск на сделку = Equity * (Risk % / 100).
  • Расстояние стопа = Stop Loss (pips) * pip size, где pip size = PriceStep * 10 для инструментов с 3/5 знаками после запятой и PriceStep для остальных.
  • Денежный риск на контракт = (stop distance / PriceStep) * StepPrice.
  • Объём = risk amount / risk per contract, округляется вниз до ближайшего VolumeStep и ограничивается MinVolume/MaxVolume. Если результат меньше минимального объёма, сделка пропускается.

Отличия от оригинала

  • Полностью реализована на StockSharp и не требует библиотек MetaTrader.
  • Использует StartProtection() для подключения стандартных защитных механизмов платформы.
  • Данные об equity берутся из объекта портфеля стратегии, а не из MQL-классов.
  • Выход из позиции контролируется по тикам, поэтому для демонстрации не регистрируются отдельные стоп-заявки.

Рекомендации по использованию

  • Как и исходный эксперт, реализация открывает только длинные позиции. Для добавления шортов расширьте метод ProcessTrade.
  • При тестировании необходимо использовать тиковые данные достаточной плотности, иначе счётчик может не достигнуть порога.
  • Перед запуском на реальном рынке убедитесь, что шаг цены и допустимые объёмы инструмента настроены корректно.
  • Код не создаёт дополнительных коллекций, а хранит состояние во внутренних полях, что соответствует рекомендациям по конверсии.
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>
/// Recreates the Money Fixed Risk expert advisor using StockSharp's high level API.
/// Uses ATR to determine position sizing via risk percentage and opens long positions
/// with symmetric stop-loss and take-profit levels based on ATR.
/// </summary>
public class MoneyFixedRiskStrategy : Strategy
{
	private readonly StrategyParam<decimal> _atrMultiplier;
	private readonly StrategyParam<int> _atrPeriod;
	private readonly StrategyParam<int> _candleInterval;
	private readonly StrategyParam<DataType> _candleType;

	private int _candleCounter;
	private decimal _stopPrice;
	private decimal _takeProfitPrice;
	private decimal _entryPrice;

	/// <summary>
	/// ATR multiplier for stop distance.
	/// </summary>
	public decimal AtrMultiplier
	{
		get => _atrMultiplier.Value;
		set => _atrMultiplier.Value = value;
	}

	/// <summary>
	/// ATR calculation period.
	/// </summary>
	public int AtrPeriod
	{
		get => _atrPeriod.Value;
		set => _atrPeriod.Value = value;
	}

	/// <summary>
	/// Number of candles between position evaluations.
	/// </summary>
	public int CandleInterval
	{
		get => _candleInterval.Value;
		set => _candleInterval.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 the <see cref="MoneyFixedRiskStrategy"/>.
	/// </summary>
	public MoneyFixedRiskStrategy()
	{
		_atrMultiplier = Param(nameof(AtrMultiplier), 1.5m)
			.SetGreaterThanZero()
			.SetDisplay("ATR Multiplier", "Multiplier for ATR-based stop distance", "Risk")
			.SetOptimize(0.5m, 3m, 0.5m);

		_atrPeriod = Param(nameof(AtrPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("ATR Period", "Period for ATR calculation", "Indicators")
			.SetOptimize(7, 28, 7);

		_candleInterval = Param(nameof(CandleInterval), 10)
			.SetGreaterThanZero()
			.SetDisplay("Candle Interval", "Candles between position evaluations", "General")
			.SetOptimize(5, 30, 5);

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles for processing", "General");
	}

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

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

		_candleCounter = 0;
		_stopPrice = 0m;
		_takeProfitPrice = 0m;
		_entryPrice = 0m;
	}

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

		var atr = new AverageTrueRange { Length = AtrPeriod };

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

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

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

		var price = candle.ClosePrice;

		// Manage existing long position
		if (Position > 0 && _stopPrice > 0m)
		{
			if (candle.LowPrice <= _stopPrice || candle.HighPrice >= _takeProfitPrice)
			{
				SellMarket(Position);
				_stopPrice = 0m;
				_takeProfitPrice = 0m;
				_entryPrice = 0m;
			}
		}

		// Manage existing short position
		if (Position < 0 && _stopPrice > 0m)
		{
			if (candle.HighPrice >= _stopPrice || candle.LowPrice <= _takeProfitPrice)
			{
				BuyMarket(Math.Abs(Position));
				_stopPrice = 0m;
				_takeProfitPrice = 0m;
				_entryPrice = 0m;
			}
		}

		_candleCounter++;

		if (_candleCounter < CandleInterval)
			return;

		_candleCounter = 0;

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		if (Position != 0)
			return;

		if (atrValue <= 0m)
			return;

		var stopDistance = atrValue * AtrMultiplier;

		// Alternate between long and short based on price relative to previous entry
		var goLong = _entryPrice == 0m || price > _entryPrice;

		if (goLong)
		{
			BuyMarket(Volume);
			_entryPrice = price;
			_stopPrice = price - stopDistance;
			_takeProfitPrice = price + stopDistance;
		}
		else
		{
			SellMarket(Volume);
			_entryPrice = price;
			_stopPrice = price + stopDistance;
			_takeProfitPrice = price - stopDistance;
		}
	}
}