Оригинальный советник MetaTrader «TwoPerBar» автора Ron Thompson на каждой новой свече открывает две рыночные сделки — покупку и продажу. Как только одна из ног достигает заданной денежной цели (ProfitMade * Point в коде MQL), позиция закрывается. В начале следующей свечи остаточные сделки принудительно ликвидируются и создаётся новая хеджированная пара. Если предыдущая свеча завершилась с открытыми позициями, размер лота удваивается, но не превышает ограничения LotLimit. Порт на StockSharp воспроизводит эту схему при помощи высокоуровневого API, подписки на котировки Level 1 и явного учёта обеих ног.
Последовательность работы
Определение новой свечи – SubscribeCandles(CandleType) сообщает о завершении выбранного таймфрейма. Получение свечи со статусом CandleStates.Finished аналогично смене Time[0] в MetaTrader.
Контроль прибыли – непрерывно анализируются котировки Level 1 (best bid/best ask). Как только лучшая цена отдаляется от цены входа на величину цели, соответствующая нога закрывается методом SellMarket или BuyMarket.
Принудительное закрытие – в начале каждой свечи оставшиеся сделки закрываются по рынку. Это прямой аналог цикла OrderClose в исходном скрипте.
Масштабирование объёма – если в предыдущем цикле оставались открытые ноги, объём умножается на VolumeMultiplier (по умолчанию 2). В противном случае он сбрасывается до BaseVolume. Значение нормализуется по шагу объёма инструмента и ограничивается MaxVolume и биржевым Security.MaxVolume.
Формирование хеджа – через 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;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import ExponentialMovingAverage, Momentum
from StockSharp.Algo.Strategies import Strategy
class two_per_bar_ron_strategy(Strategy):
def __init__(self):
super(two_per_bar_ron_strategy, self).__init__()
self._ema_period = self.Param("EmaPeriod", 20).SetDisplay("EMA Period", "EMA trend filter", "Indicators")
self._momentum_period = self.Param("MomentumPeriod", 10).SetDisplay("Momentum Period", "Momentum lookback", "Indicators")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))).SetDisplay("Candle Type", "Candle timeframe", "General")
self._prev_mom = 0.0; self._has_prev = False
@property
def ema_period(self): return self._ema_period.Value
@property
def momentum_period(self): return self._momentum_period.Value
@property
def candle_type(self): return self._candle_type.Value
def OnReseted(self):
super(two_per_bar_ron_strategy, self).OnReseted()
self._prev_mom = 0.0; self._has_prev = False
def OnStarted2(self, time):
super(two_per_bar_ron_strategy, self).OnStarted2(time)
self._has_prev = False
ema = ExponentialMovingAverage(); ema.Length = self.ema_period
mom = Momentum(); mom.Length = self.momentum_period
sub = self.SubscribeCandles(self.candle_type)
sub.Bind(ema, mom, self.process_candle).Start()
def process_candle(self, candle, ema, mom):
if candle.State != CandleStates.Finished: return
close = float(candle.ClosePrice); ema_val = float(ema); mom_val = float(mom)
if not self._has_prev: self._prev_mom = mom_val; self._has_prev = True; return
if self._prev_mom <= 0 and mom_val > 0 and close > ema_val and self.Position <= 0:
if self.Position < 0: self.BuyMarket()
self.BuyMarket()
elif self._prev_mom >= 0 and mom_val < 0 and close < ema_val and self.Position >= 0:
if self.Position > 0: self.SellMarket()
self.SellMarket()
self._prev_mom = mom_val
def CreateClone(self): return two_per_bar_ron_strategy()