Открыть на GitHub

Open Close

Обзор

Open Close — порт советника MetaTrader 5 Open Close.mq5 (тикет 23090). Стратегия анализирует соотношение цен открытия и закрытия двух последних завершённых свечей. Одновременно удерживается только одна позиция: когда более свежая свеча разворачивается относительно предыдущей, стратегия открывает сделку, а когда обе свечи указывают в одну сторону — закрывает. Реализация на C# также сохраняет адаптивный расчёт объёма, уменьшающий экспозицию после серии убыточных сделок.

Логика стратегии

Свечной фильтр

  • Работа ведётся только по полностью сформированным свечам, поступающим из источника CandleType.
  • Поддерживается скользящее окно из двух последних завершённых свечей («текущая» и «предыдущая»).
  • Сравниваются цены открытия и закрытия этих свечей:
    • Бычий разворотprevious.Open > older.Open и previous.Close < older.Close.
    • Медвежий разворотprevious.Open < older.Open и previous.Close > older.Close.

Правила входа

  • При отсутствии позиции и появлении бычьего разворота выставляется рыночная заявка на покупку.
  • При отсутствии позиции и обнаружении медвежьего разворота подаётся рыночная заявка на продажу.
  • Одновременно допускается только одна сделка. Противоположные сигналы игнорируются, пока позиция не будет закрыта.

Правила выхода

  • Для длинной позиции выход выполняется, если обе свечи смещаются вниз (previous.Open < older.Open и previous.Close < older.Close).
  • Для короткой позиции условие симметрично (previous.Open > older.Open и previous.Close > older.Close).
  • В оригинальном советнике отсутствуют стоп-лосс и тейк-профит, поэтому закрытие целиком зависит от конфигурации последних свечей.

Расчёт объёма и серия убытков

  • Основной размер заявки определяется параметром MaximumRiskPercent — долей капитала, выделяемой на сделку. Базовая формула: Portfolio.CurrentValue × MaximumRiskPercent ÷ referencePrice, где в качестве цены используется последнее закрытие.
  • Если оценка портфеля или цена недоступны, используется резервный объём FallbackVolume.
  • После каждой полностью закрытой сделки фиксируется реализованный результат, а число подряд идущих убыточных сделок считается за последние HistoryDays дней.
    • Если подряд получилось более одного убытка, следующий объём уменьшается на volume × losses ÷ DecreaseFactor, что повторяет логику MT5.
  • Итоговый объём приводится к шагу объёма инструмента и ограничивается минимальными/максимальными значениями.

Дополнительные замечания по реализации

  • Обработка выполняется только для свечей с состоянием CandleStates.Finished, что гарантирует использование полных данных.
  • Проверки входа и выхода происходят на закрытии свежей свечи. В MetaTrader заявка отправлялась на открытии следующего бара; разница невелика на старших таймфреймах, но важна для очень коротких интервалов.
  • Портфельные метрики StockSharp приближенно заменяют AccountFreeMargin из MetaTrader. При отличающихся контрактных размерах подберите значения MaximumRiskPercent или FallbackVolume вручную.

Параметры

Параметр Тип Значение по умолчанию Описание
MaximumRiskPercent decimal 0.02 Доля капитала, вкладываемая в новую позицию (0.02 = 2%).
DecreaseFactor decimal 3 Делитель, уменьшающий объём после серии убытков. Чем больше значение, тем мягче снижение.
HistoryDays int 60 Количество календарных дней для учёта серии убыточных сделок.
FallbackVolume decimal 0.1 Резервный объём, используемый при невозможности расчёта по риску.
CandleType DataType TimeFrame(15m) Источник свечей, по которым формируются сигналы.

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

  • Проверка маржи опирается на Portfolio.CurrentValue; в оригинале использовался AccountFreeMargin. Совпадение поведения возможно только при схожей оценке капитала на обеих платформах.
  • История сделок формируется на основе исполнений самой стратегии, а не по глобальной истории терминала. Оставьте стратегию работать достаточно долго, чтобы накопить статистику серий.
  • Порт сохраняет модель одной позиции и отсутствие защитных ордеров, как и в оригинале. При необходимости добавьте стопы во внешних модулях управления риском.
using System;
using System.Collections.Generic;

using Ecng.Common;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Open Close strategy using EMA crossover.
/// Buys when fast EMA crosses above slow EMA, sells on reverse.
/// </summary>
public class OpenClose23090Strategy : Strategy
{
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;

	private ExponentialMovingAverage _fast;
	private ExponentialMovingAverage _slow;

	private decimal _prevFast;
	private decimal _prevSlow;
	private decimal _entryPrice;
	private int _cooldown;

	public int FastPeriod { get => _fastPeriod.Value; set => _fastPeriod.Value = value; }
	public int SlowPeriod { get => _slowPeriod.Value; set => _slowPeriod.Value = value; }
	public int StopLossPoints { get => _stopLossPoints.Value; set => _stopLossPoints.Value = value; }
	public int TakeProfitPoints { get => _takeProfitPoints.Value; set => _takeProfitPoints.Value = value; }

	public OpenClose23090Strategy()
	{
		_fastPeriod = Param(nameof(FastPeriod), 15).SetGreaterThanZero().SetDisplay("Fast Period", "Fast EMA period", "Indicator");
		_slowPeriod = Param(nameof(SlowPeriod), 60).SetGreaterThanZero().SetDisplay("Slow Period", "Slow EMA period", "Indicator");
		_stopLossPoints = Param(nameof(StopLossPoints), 200).SetNotNegative().SetDisplay("Stop Loss", "Stop-loss in price steps", "Risk");
		_takeProfitPoints = Param(nameof(TakeProfitPoints), 400).SetNotNegative().SetDisplay("Take Profit", "Take-profit in price steps", "Risk");
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		yield return (Security, TimeSpan.FromMinutes(5).TimeFrame());
	}

	protected override void OnReseted()
	{
		base.OnReseted();
		_fast = null; _slow = null;
		_prevFast = 0; _prevSlow = 0; _entryPrice = 0; _cooldown = 0;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		_fast = new ExponentialMovingAverage { Length = FastPeriod };
		_slow = new ExponentialMovingAverage { Length = SlowPeriod };
		var subscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());
		subscription.Bind(_fast, _slow, ProcessCandle);
		subscription.Start();
	}

	private void ProcessCandle(ICandleMessage candle, decimal fastValue, decimal slowValue)
	{
		if (candle.State != CandleStates.Finished) return;
		if (!_fast.IsFormed || !_slow.IsFormed) { _prevFast = fastValue; _prevSlow = slowValue; return; }
		if (_cooldown > 0) { _cooldown--; _prevFast = fastValue; _prevSlow = slowValue; return; }

		var close = candle.ClosePrice;
		var step = Security?.PriceStep ?? 1m;

		if (Position > 0 && _entryPrice > 0)
		{
			if (StopLossPoints > 0 && close <= _entryPrice - StopLossPoints * step) { SellMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
			if (TakeProfitPoints > 0 && close >= _entryPrice + TakeProfitPoints * step) { SellMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
		}
		else if (Position < 0 && _entryPrice > 0)
		{
			if (StopLossPoints > 0 && close >= _entryPrice + StopLossPoints * step) { BuyMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
			if (TakeProfitPoints > 0 && close <= _entryPrice - TakeProfitPoints * step) { BuyMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
		}

		if (_prevFast <= _prevSlow && fastValue > slowValue && Position <= 0)
		{ if (Position < 0) BuyMarket(); BuyMarket(); _entryPrice = close; _cooldown = 100; }
		else if (_prevFast >= _prevSlow && fastValue < slowValue && Position >= 0)
		{ if (Position > 0) SellMarket(); SellMarket(); _entryPrice = close; _cooldown = 100; }

		_prevFast = fastValue; _prevSlow = slowValue;
	}
}