Ver no GitHub

Estratégia de Momentum WOC 0.1.2

Visão geral

Esta estratégia é uma versão StockSharp de alto nível do Expert Advisor do MetaTrader "WOC.0.1.2". Ela ouve atualizações de melhor bid/ask de Nível 1 e procura por séries rápidas de preço no lado do ask. Quando o preço do ask imprime um número configurável de ticks consecutivamente mais altos ou mais baixos dentro de uma janela de tempo limitada, a estratégia abre uma posição de mercado na direção do rompimento. Apenas uma posição pode estar aberta a qualquer momento, o que espelha o comportamento de posição única do código original.

Dados e execução

  • Dados de mercado: Melhor bid e melhor ask de Nível 1. O algoritmo não requer velas ou indicadores.
  • Execução: Ordens a mercado. As saídas de proteção são emuladas dentro da estratégia verificando as atualizações de bid/ask.

Lógica de sinal

  1. Rastrear o último preço do ask e medir quantos novos máximos consecutivos (série de alta) ou novos mínimos (série de baixa) foram impressos.
  2. Quando uma série de alta ou baixa atinge SequenceLength, verificar se a duração da série é menor ou igual a SequenceTimeoutSeconds segundos.
  3. Se a série de baixa for mais longa que a de alta, enviar uma ordem de venda; caso contrário, enviar uma ordem de compra. A verificação reproduz a lógica original do MetaTrader onde a série com o contador mais alto define a direção.
  4. Redefinir todos os contadores de série após cada tentativa de entrada para garantir que o próximo sinal comece do zero.

Gerenciamento de posição

  • Stop inicial: Após uma entrada, a estratégia registra imediatamente um preço de stop-loss que está StopLossTicks passos de preço afastado do bid atual (para comprados) ou do ask (para vendidos).
  • Stop móvel: Quando o preço se move a favor do trade mais de TrailingStopTicks passos de preço, o stop é ajustado para TrailingStopTicks atrás do último bid/ask, desde que o stop permaneça pelo menos o dobro da distância de trailing afastado do preço atual. Isso reproduz a condição de trailing de dois passos do Expert MQL.
  • Execução de saída: Quando o bid/ask rastreado cruza o nível de stop armazenado, a posição é fechada via uma ordem a mercado. Após a saída, o estado interno é redefinido para aceitar novas séries.

Gerenciamento de volume

Dois modos de dimensionamento de posição são suportados:

  • Lote fixo: Usar o parâmetro LotSize como volume de ordem absoluto.
  • Lotes automáticos: Habilitar UseAutoLotSizing para mapear o saldo da conta para níveis de volume. O saldo é obtido de Portfolio.CurrentValue e recorre a Portfolio.BeginValue se o valor atual não estiver disponível.
Saldo (maior que) Volume
0 (padrão) 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

Parâmetros

  • StopLossTicks – distância do stop-loss medida em passos de preço.
  • TrailingStopTicks – distância de trailing medida em passos de preço (pode ser zero para desabilitar o trailing).
  • SequenceLength – número de movimentos consecutivos do ask necessários antes de entrar em um trade.
  • SequenceTimeoutSeconds – duração máxima da série em segundos.
  • LotSize – tamanho de ordem fixo usado quando o dimensionamento automático está desabilitado.
  • UseAutoLotSizing – habilita a tabela de volume baseada em saldo mostrada acima.

Notas de uso

  • Funciona melhor em instrumentos rápidos onde o melhor ask se atualiza frequentemente; considere testar em feeds de dados em nível de tick.
  • A estratégia requer contas de hedge porque nunca mantém posições opostas simultaneamente.
  • Certifique-se de que Security.PriceStep está configurado; caso contrário, os cálculos de stop-loss e trailing recorrem a uma distância de 1 unidade monetária por tick.
  • Apenas uma posição aberta é suportada por vez, espelhando o comportamento MQL original.
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;
		}
	}
}