Открыть на GitHub

Стратегия WOC 0.1.2 (моментум)

Обзор

Стратегия представляет собой порт MetaTrader-советника «WOC.0.1.2» на высокоуровневый API StockSharp. Логика работает с обновлениями котировок Level 1, отслеживает последовательности быстрого роста или падения цены спроса (Ask) и при выполнении условий открывает позицию в сторону прорыва. Одновременно может быть открыта только одна позиция, что полностью повторяет поведение исходного MQL-робота.

Данные и исполнение

  • Источники данных: лучшие цены Bid и Ask (Level 1). Дополнительные индикаторы и свечи не требуются.
  • Исполнение: рыночные заявки. Стоп-ордеры моделируются внутри стратегии посредством контроля текущих котировок.

Логика входа

  1. Хранится последняя цена Ask и количество подряд идущих новых максимумов (счётчик up) и минимумов (счётчик down).
  2. Если любой счётчик достигает значения SequenceLength, проверяется длительность серии — она должна быть не больше SequenceTimeoutSeconds секунд.
  3. Если down больше up, открывается короткая позиция, иначе — длинная. Таким образом воспроизводится условие if (up < down) из MQL-версии.
  4. После каждой попытки входа оба счётчика и исходные цены сбрасываются.

Управление позицией

  • Начальный стоп: сразу после открытия сделки рассчитывается уровень стоп-лосса на расстоянии StopLossTicks шагов цены от текущего Bid (для лонга) или Ask (для шорта).
  • Трейлинг-стоп: когда цена проходит в прибыль TrailingStopTicks шагов, стоп подтягивается к цене так, чтобы находиться на расстоянии TrailingStopTicks, но только если старый стоп был не ближе, чем две такие дистанции. Это повторяет условие StopLoss < Bid - 2 * TrailingStop / StopLoss > Ask + 2 * TrailingStop оригинала.
  • Выход: при пересечении сохранённого стоп-уровня текущим Bid/Ask позиция закрывается рыночной заявкой, после чего состояние сбрасывается.

Размер позиции

Доступны два режима:

  • Фиксированный лот — используется значение параметра LotSize.
  • Автолоты — при включении UseAutoLotSizing объём рассчитывается по ступенчатой шкале в зависимости от стоимости портфеля (Portfolio.CurrentValue, либо Portfolio.BeginValue).
Баланс (больше) Объём
0 (по умолчанию) LotSize
200 0.04
300 0.05
400 0.06
500 0.07
600 0.08
700 0.09
800 0.10
900 0.20
1 000 0.30
2 000 0.40
3 000 0.50
4 000 0.60
5 000 0.70
6 000 0.80
7 000 0.90
8 000 1.00
9 000 2.00
10 000 3.00
11 000 4.00
12 000 5.00
13 000 6.00
14 000 7.00
15 000 8.00
20 000 9.00
30 000 10.00
40 000 11.00
50 000 12.00
60 000 13.00
70 000 14.00
80 000 15.00
90 000 16.00
100 000 17.00
110 000 18.00
120 000 19.00
130 000 20.00

Параметры

  • StopLossTicks — расстояние стоп-лосса в шагах цены.
  • TrailingStopTicks — расстояние трейлинг-стопа в шагах цены (0 отключает трейлинг).
  • SequenceLength — требуемое количество последовательных изменений Ask.
  • SequenceTimeoutSeconds — максимально допустимое время формирования серии (в секундах).
  • LotSize — объём сделки при выключенной автолотовости.
  • UseAutoLotSizing — включает таблицу расчёта объёма по балансу.

Рекомендации

  • Подходит для инструментов с высокой частотой обновления стакана; рекомендуется тестирование на тиковых данных.
  • Стратегия предполагает хеджинговый тип счёта (нет одновременных разнонаправленных позиций).
  • Убедитесь, что у инструмента задан Security.PriceStep; иначе расчёт стопов будет использовать шаг «1».
  • Для повторения оригинальной логики не изменяйте порядок подписки и не добавляйте собственных индикаторов.
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>
/// Momentum strategy based on WOC 0.1.2 concept.
/// Detects consecutive candle close runs in one direction and enters on breakout.
/// Uses ATR-based stop loss and trailing stop.
/// </summary>
public class Woc012Strategy : Strategy
{
	private readonly StrategyParam<int> _sequenceLength;
	private readonly StrategyParam<decimal> _stopLossAtrMult;
	private readonly StrategyParam<decimal> _trailingAtrMult;
	private readonly StrategyParam<int> _atrPeriod;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _prevClose;
	private int _upCount;
	private int _downCount;
	private decimal _entryPrice;
	private decimal? _stopPrice;

	public int SequenceLength { get => _sequenceLength.Value; set => _sequenceLength.Value = value; }
	public decimal StopLossAtrMult { get => _stopLossAtrMult.Value; set => _stopLossAtrMult.Value = value; }
	public decimal TrailingAtrMult { get => _trailingAtrMult.Value; set => _trailingAtrMult.Value = value; }
	public int AtrPeriod { get => _atrPeriod.Value; set => _atrPeriod.Value = value; }
	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }

	public Woc012Strategy()
	{
		_sequenceLength = Param(nameof(SequenceLength), 6)
			.SetGreaterThanZero()
			.SetDisplay("Sequence Length", "Consecutive bars in same direction to trigger entry", "Signals");

		_stopLossAtrMult = Param(nameof(StopLossAtrMult), 1.5m)
			.SetGreaterThanZero()
			.SetDisplay("SL ATR Mult", "Stop loss as ATR multiple", "Risk");

		_trailingAtrMult = Param(nameof(TrailingAtrMult), 1.0m)
			.SetGreaterThanZero()
			.SetDisplay("Trail ATR Mult", "Trailing stop as ATR multiple", "Risk");

		_atrPeriod = Param(nameof(AtrPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("ATR Period", "ATR calculation length", "Risk");

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

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

	protected override void OnReseted()
	{
		base.OnReseted();
		_prevClose = 0;
		_upCount = 0;
		_downCount = 0;
		_entryPrice = 0;
		_stopPrice = null;
	}

	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);
			DrawOwnTrades(area);
		}
	}

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

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		var close = candle.ClosePrice;

		// Track consecutive direction
		if (_prevClose > 0)
		{
			if (close > _prevClose)
			{
				_upCount++;
				_downCount = 0;
			}
			else if (close < _prevClose)
			{
				_downCount++;
				_upCount = 0;
			}
			else
			{
				_upCount = 0;
				_downCount = 0;
			}
		}

		_prevClose = close;

		// Manage existing position
		if (Position != 0)
		{
			if (Position > 0)
			{
				// Trail up
				var trail = close - TrailingAtrMult * atr;
				if (_stopPrice == null || trail > _stopPrice)
					_stopPrice = trail;

				if (close <= _stopPrice)
				{
					SellMarket(Math.Abs(Position));
					_stopPrice = null;
					_entryPrice = 0;
					return;
				}
			}
			else
			{
				// Trail down
				var trail = close + TrailingAtrMult * atr;
				if (_stopPrice == null || trail < _stopPrice)
					_stopPrice = trail;

				if (close >= _stopPrice)
				{
					BuyMarket(Math.Abs(Position));
					_stopPrice = null;
					_entryPrice = 0;
					return;
				}
			}
		}

		// Entry: consecutive sequence completed
		if (_upCount >= SequenceLength && Position <= 0)
		{
			var vol = Volume + Math.Abs(Position);
			BuyMarket(vol);
			_entryPrice = close;
			_stopPrice = close - StopLossAtrMult * atr;
			_upCount = 0;
		}
		else if (_downCount >= SequenceLength && Position >= 0)
		{
			var vol = Volume + Math.Abs(Position);
			SellMarket(vol);
			_entryPrice = close;
			_stopPrice = close + StopLossAtrMult * atr;
			_downCount = 0;
		}
	}
}