Открыть на GitHub

Стратегия TwoPerBar Ron

Обзор

Оригинальный советник MetaTrader «TwoPerBar» автора Ron Thompson на каждой новой свече открывает две рыночные сделки — покупку и продажу. Как только одна из ног достигает заданной денежной цели (ProfitMade * Point в коде MQL), позиция закрывается. В начале следующей свечи остаточные сделки принудительно ликвидируются и создаётся новая хеджированная пара. Если предыдущая свеча завершилась с открытыми позициями, размер лота удваивается, но не превышает ограничения LotLimit. Порт на StockSharp воспроизводит эту схему при помощи высокоуровневого API, подписки на котировки Level 1 и явного учёта обеих ног.

Последовательность работы

  1. Определение новой свечиSubscribeCandles(CandleType) сообщает о завершении выбранного таймфрейма. Получение свечи со статусом CandleStates.Finished аналогично смене Time[0] в MetaTrader.
  2. Контроль прибыли – непрерывно анализируются котировки Level 1 (best bid/best ask). Как только лучшая цена отдаляется от цены входа на величину цели, соответствующая нога закрывается методом SellMarket или BuyMarket.
  3. Принудительное закрытие – в начале каждой свечи оставшиеся сделки закрываются по рынку. Это прямой аналог цикла OrderClose в исходном скрипте.
  4. Масштабирование объёма – если в предыдущем цикле оставались открытые ноги, объём умножается на VolumeMultiplier (по умолчанию 2). В противном случае он сбрасывается до BaseVolume. Значение нормализуется по шагу объёма инструмента и ограничивается MaxVolume и биржевым Security.MaxVolume.
  5. Формирование хеджа – через BuyMarket и SellMarket размещаются две рыночные заявки. Каждая нога запоминает целевой объём, фактическое исполнение и средневзвешенную цену входа, что позволяет точно сравнивать текущее отклонение с целевой прибылью.

Управление рисками

  • Мартингейл-подобное масштабирование – удвоение объёма после неудачного цикла полностью повторяет оригинальную логику. Если обе ноги успели закрыться внутри бара, серия возвращается к базовому лоту.
  • Индивидуальные цели по прибыли – параметр ProfitTargetPoints соответствует MQL-параметру ProfitMade. Значение умножается на размер пункта инструмента и сравнивается с bid/ask для принятия решения о выходе.
  • Соответствие биржевым ограничениям – метод NormalizeVolume подстраивает объём под VolumeStep и MinVolume. Завышенные значения приводят к сбросу на ближайший доступный объём.
  • Учёт хеджированных позиций – стратегия хранит список ног самостоятельно, поскольку портфель StockSharp обычно предоставляет только суммарную позицию. Для корректного повторения поведения требуется брокер, поддерживающий встречные сделки.

Параметры

Имя Тип Значение по умолчанию Описание
CandleType DataType 1-минутные свечи Таймфрейм, по которому фиксируется начало нового бара.
BaseVolume decimal 0.1 Базовый лот для нового цикла.
VolumeMultiplier decimal 2 Множитель объёма после цикла с открытыми сделками.
MaxVolume decimal 12.8 Жёсткий потолок для мартингейлового объёма.
ProfitTargetPoints decimal 19 Цель по прибыли в пунктах; умножается на размер пункта и сравнивается с bid/ask.

Отличия от версии MQL

  • Используется SubscribeLevel1() вместо глобальных переменных Bid/Ask, но логика по лучшим котировкам сохранена.
  • Заявки отправляются через высокоуровневые методы (BuyMarket, SellMarket), поэтому округление выполняется движком StockSharp.
  • Объём автоматически подгоняется под VolumeStep, MinVolume и MaxVolume, тогда как оригинал работал с «сырыми» значениями типа double.
  • Учёт ног ведётся внутри стратегии; при работе на неттинговых счетах брокер может сводить встречные позиции, поэтому убедитесь в поддержке хеджирования.

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

  • Подберите BaseVolume в соответствии с минимальным лотом выбранного инструмента, иначе нормализация отключит торговлю.
  • Настраивайте ProfitTargetPoints с учётом размера пункта: слишком большие значения редко достигаются в пределах одной свечи.
  • Из-за одновременного открытия разнонаправленных позиций сначала протестируйте стратегию на демо или у брокера с режимом хеджирования.
  • Включите отображение на графике — метод OnStarted добавляет свечи и сделки (DrawCandles, DrawOwnTrades) для наглядного контроля.
using System;
using System.Collections.Generic;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Two Per Bar Ron strategy - momentum-based direction with EMA confirmation.
/// Buys when momentum crosses above zero and close is above EMA.
/// Sells when momentum crosses below zero and close is below EMA.
/// </summary>
public class TwoPerBarRonStrategy : Strategy
{
	private readonly StrategyParam<int> _emaPeriod;
	private readonly StrategyParam<int> _momentumPeriod;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _prevMom;
	private bool _hasPrev;

	public int EmaPeriod { get => _emaPeriod.Value; set => _emaPeriod.Value = value; }
	public int MomentumPeriod { get => _momentumPeriod.Value; set => _momentumPeriod.Value = value; }
	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }

	public TwoPerBarRonStrategy()
	{
		_emaPeriod = Param(nameof(EmaPeriod), 20)
			.SetDisplay("EMA Period", "EMA trend filter", "Indicators");

		_momentumPeriod = Param(nameof(MomentumPeriod), 10)
			.SetDisplay("Momentum Period", "Momentum lookback", "Indicators");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Candle timeframe", "General");
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities() => [(Security, CandleType)];
	protected override void OnReseted() { base.OnReseted(); _prevMom = 0m; _hasPrev = false; }

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_hasPrev = false;

		var ema = new ExponentialMovingAverage { Length = EmaPeriod };
		var mom = new Momentum { Length = MomentumPeriod };

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

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

		var close = candle.ClosePrice;

		if (!_hasPrev)
		{
			_prevMom = mom;
			_hasPrev = true;
			return;
		}

		if (_prevMom <= 0 && mom > 0 && close > ema && Position <= 0)
		{
			if (Position < 0)
				BuyMarket();
			BuyMarket();
		}
		else if (_prevMom >= 0 && mom < 0 && close < ema && Position >= 0)
		{
			if (Position > 0)
				SellMarket();
			SellMarket();
		}

		_prevMom = mom;
	}
}