Ver no GitHub

Estratégia Inicial 2005

Visão geral

A Estratégia Starter 2005 é uma conversão StockSharp de alto nível API do clássico MetaTrader 4 consultor especialista Starter.mq4 lançado em 2005. O sistema original misturava um oscilador Laguerre, um filtro de inclinação de média móvel exponencial e uma confirmação de índice de canal de commodities. Esta porta mantém a árvore de decisão intacta enquanto adapta o gerenciamento de dinheiro e a execução às convenções StockSharp:

  • Um proxy Laguerre RSI replica o buffer iCustom("Laguerre") que oscila entre 0 e 1.
  • Um EMA de 5 períodos calculado sobre o preço médio fornece a mesma confirmação de inclinação ascendente/descendente usada pelo especialista MT4.
  • Um CCI de 14 períodos medido nos preços de fechamento filtra configurações fracas, assim como a variável Alpha original.
  • A rotina adaptativa de dimensionamento de lote reflete a função histórica LotsOptimized(), incluindo reduções baseadas em sequências após perdas consecutivas.
  • As saídas de posição são acionadas pela reversão de Laguerre para fora da zona extrema ou pela negociação atingindo uma distância de lucro configurável equivalente a Point * Stop.

Lógica de negociação

  1. Preparação de indicadores
    • O valor de Laguerre RSI é reconstruído através de um filtro Laguerre de quatro estágios com Gamma configurável.
    • O comprimento de EMA é padrão para cinco velas e opera em (High + Low) / 2 para corresponder a PRICE_MEDIAN em MQL4.
    • O período CCI é padronizado como 14 nos preços de fechamento, e um limite muito pequeno (±5) é mantido para permanecer fiel ao código legado.
  2. Configuração longa
    • Laguerre deve ficar próximo de zero (LaguerreEntryTolerance emula a comparação estrita == 0).
    • EMA deve estar subindo em comparação com a vela finalizada anterior.
    • CCI deve ficar abaixo de -CciThreshold.
  3. Configuração curta
    • Laguerre deve sentar-se perto de um (1 - LaguerreEntryTolerance aproxima-se de == 1).
    • EMA deve estar caindo.
    • CCI deve subir acima de +CciThreshold.
  4. Saídas
    • As posições compradas fecham quando Laguerre sobe acima de LaguerreExitHigh (padrão 0.9) ou quando o preço avança TakeProfitPoints * PriceStep a partir da entrada.
    • As vendas fecham quando Laguerre cai abaixo de LaguerreExitLow (padrão 0.1) ou quando o preço cai na mesma distância.
    • Qualquer outra posição plana manual redefine automaticamente o estado interno para evitar dados de entrada obsoletos.

Gestão de dinheiro

O auxiliar CalculateOrderVolume reproduz o comportamento LotsOptimized() do MT4:

  1. Dimensionamento baseado em risco – O patrimônio líquido multiplicado por MaximumRisk é dividido por RiskDivider (padrão 500, como na regra original /500). Quando dividido pelo preço atual, resulta o tamanho do lote ajustado ao risco.
  2. Lote substituto – Se o dimensionamento do risco produzir um número menor que BaseVolume, o algoritmo mantém o lote base.
  3. Redução da sequência de perdas – Após duas ou mais negociações consecutivas com perdas, o volume é reduzido em volume * losses / DecreaseFactor, correspondendo exatamente ao loop MQL que inspecionou o histórico de negociações.
  4. Normalização – Os volumes são normalizados para o VolumeStep do instrumento e fixados entre MinVolume e MaxVolume para evitar pedidos rejeitados.

O rastreamento de perdas consecutivas é redefinido após qualquer saída lucrativa e incrementos após perdas nas negociações; os resultados do ponto de equilíbrio deixam o contador intocado, espelhando o comportamento original que ignorou os tickets de lucro zero.

Parâmetros

Nome Tipo Padrão Descrição
BaseVolume decimal 1.2 Tamanho mínimo do lote utilizado quando o dimensionamento do risco sugere um valor menor.
MaximumRisk decimal 0.036 Fração do patrimônio líquido exposta em uma nova posição antes da aplicação do divisor.
RiskDivider decimal 500 Divisor aplicado ao capital de risco, reproduzindo a regra original AccountFreeMargin() * MaximumRisk / 500.
DecreaseFactor decimal 2 Divisor de sequência usado para diminuir o volume após perdas consecutivas.
MaPeriod int 5 Comprimento de EMA no preço médio da vela.
CciPeriod int 14 Retrospectiva do índice de canais de commodities.
CciThreshold decimal 5 Nível absoluto CCI necessário para acionar um sinal.
LaguerreGamma decimal 0.66 Fator de suavização do filtro Laguerre.
LaguerreEntryTolerance decimal 0.02 Tolerância em torno de 0/1 usada para imitar as verificações de igualdade originais.
LaguerreExitHigh decimal 0.9 Nível de saída superior para posições longas.
LaguerreExitLow decimal 0.1 Nível de saída mais baixo para posições curtas.
TakeProfitPoints decimal 10 Meta de lucro expressa em faixas de preço (Point * Stop em MQL).
CandleType DataType TimeFrame(5m) Assinatura de velas processada pela estratégia.

Notas de implementação

  • Laguerre RSI é implementado em linha usando a recursão de quatro níveis do indicador original; nenhuma chamada para GetValue() é necessária.
  • Os indicadores EMA e CCI são atualizados manualmente dentro do callback da vela para garantir que o feed de preço médio corresponda à opção PRICE_MEDIAN de MetaTrader.
  • As entradas no mercado respeitam as sinalizações AllowLong() / AllowShort() e garantem que nenhuma ordem ativa esteja pendente, preservando o design de posição única da fonte EA.
  • O rastreamento dos resultados comerciais usa o preço de decisão da vela (último preço, fechamento ou abertura) para estimar a direção do PnL e manter o contador da seqüência de perdas.
  • Os comentários embutidos em inglês descrevem todos os principais blocos de decisão para ajudar na manutenção futura.

Dicas de uso

  • O EA original foi planejado para gráficos FX intradiários; comece com instrumentos líquidos que oferecem pequenas etapas de preço para que a meta de lucro de 10 pontos se alinhe com um pip.
  • Como o script MT4 mantém apenas uma posição, execute a estratégia em ambientes onde o preenchimento parcial e os pedidos simultâneos são improváveis (testes históricos ou mercados líquidos).
  • Ajuste LaguerreEntryTolerance se o oscilador raramente tocar exatamente 0 ou 1 em seu conjunto de dados.
  • Ajuste RiskDivider e DecreaseFactor juntos para equilibrar o crescimento do risco e a mitigação de perdas.
namespace StockSharp.Samples.Strategies;

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;

/// <summary>
/// Conversion of the MetaTrader 4 expert advisor "Starter" (2005 release).
/// Combines a Laguerre RSI proxy, EMA slope confirmation and a CCI filter.
/// Implements adaptive lot sizing inspired by the original LotsOptimized routine.
/// </summary>
public class Starter2005Strategy : Strategy
{
	private readonly StrategyParam<decimal> _baseVolume;
	private readonly StrategyParam<decimal> _maximumRisk;
	private readonly StrategyParam<decimal> _riskDivider;
	private readonly StrategyParam<decimal> _decreaseFactor;
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<int> _cciPeriod;
	private readonly StrategyParam<decimal> _cciThreshold;
	private readonly StrategyParam<decimal> _laguerreGamma;
	private readonly StrategyParam<decimal> _laguerreEntryTolerance;
	private readonly StrategyParam<decimal> _laguerreExitHigh;
	private readonly StrategyParam<decimal> _laguerreExitLow;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<DataType> _candleType;

	private ExponentialMovingAverage _ema = null!;
	private CommodityChannelIndex _cci = null!;

	private decimal? _previousMa;
	private decimal _lagL0;
	private decimal _lagL1;
	private decimal _lagL2;
	private decimal _lagL3;
	private bool _laguerreFormed;

	private decimal? _entryPrice;
	private decimal _entryVolume;
	private Sides? _entrySide;
	private int _consecutiveLosses;

	/// <summary>
	/// Initializes a new instance of the <see cref="Starter2005Strategy"/> class.
	/// </summary>
	public Starter2005Strategy()
	{
		_baseVolume = Param(nameof(BaseVolume), 1.2m)
			.SetGreaterThanZero()
			.SetDisplay("Base Volume", "Initial lot size used when risk-based sizing is unavailable", "Risk Management")
			
			.SetOptimize(0.1m, 5m, 0.1m);

		_maximumRisk = Param(nameof(MaximumRisk), 0.036m)
			.SetNotNegative()
			.SetDisplay("Maximum Risk", "Fraction of account equity considered for sizing", "Risk Management")
			
			.SetOptimize(0m, 0.1m, 0.005m);

		_riskDivider = Param(nameof(RiskDivider), 500m)
			.SetGreaterThanZero()
			.SetDisplay("Risk Divider", "Divisor applied to risk capital (mimics the original /500 rule)", "Risk Management")
			
			.SetOptimize(100m, 1000m, 50m);

		_decreaseFactor = Param(nameof(DecreaseFactor), 2m)
			.SetGreaterThanZero()
			.SetDisplay("Decrease Factor", "Lot reduction factor after consecutive losses", "Risk Management")
			
			.SetOptimize(1m, 5m, 0.5m);

		_maPeriod = Param(nameof(MaPeriod), 5)
			.SetGreaterThanZero()
			.SetDisplay("EMA Period", "Length of the exponential moving average applied to median price", "Indicators")
			
			.SetOptimize(3, 30, 1);

		_cciPeriod = Param(nameof(CciPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("CCI Period", "Commodity Channel Index lookback length", "Indicators")
			
			.SetOptimize(5, 40, 1);

		_cciThreshold = Param(nameof(CciThreshold), 5m)
			.SetNotNegative()
			.SetDisplay("CCI Threshold", "Absolute CCI level required for signals", "Indicators")
			
			.SetOptimize(1m, 50m, 1m);

		_laguerreGamma = Param(nameof(LaguerreGamma), 0.66m)
			.SetRange(0.1m, 0.9m)
			.SetDisplay("Laguerre Gamma", "Smoothing factor of the Laguerre RSI filter", "Indicators")
			
			.SetOptimize(0.3m, 0.9m, 0.05m);

		_laguerreEntryTolerance = Param(nameof(LaguerreEntryTolerance), 0.02m)
			.SetRange(0m, 0.3m)
			.SetDisplay("Laguerre Entry Tolerance", "Closeness to 0/1 required to mimic the original equality checks", "Signals")
			
			.SetOptimize(0.005m, 0.1m, 0.005m);

		_laguerreExitHigh = Param(nameof(LaguerreExitHigh), 0.9m)
			.SetRange(0.5m, 1m)
			.SetDisplay("Laguerre Exit High", "Upper exit level for long positions", "Signals")
			
			.SetOptimize(0.6m, 1m, 0.05m);

		_laguerreExitLow = Param(nameof(LaguerreExitLow), 0.1m)
			.SetRange(0m, 0.5m)
			.SetDisplay("Laguerre Exit Low", "Lower exit level for short positions", "Signals")
			
			.SetOptimize(0m, 0.4m, 0.05m);

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 10m)
			.SetNotNegative()
			.SetDisplay("Take Profit (points)", "Distance in price points before profit is locked", "Risk Management")
			
			.SetOptimize(0m, 50m, 5m);

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe processed by the strategy", "General");
	}

	/// <summary>
	/// Base lot size used when the risk model produces a smaller value.
	/// </summary>
	public decimal BaseVolume
	{
		get => _baseVolume.Value;
		set => _baseVolume.Value = value;
	}

	/// <summary>
	/// Fraction of the portfolio considered for risk-based sizing.
	/// </summary>
	public decimal MaximumRisk
	{
		get => _maximumRisk.Value;
		set => _maximumRisk.Value = value;
	}

	/// <summary>
	/// Divider applied to the risk capital (mirrors the /500 rule).
	/// </summary>
	public decimal RiskDivider
	{
		get => _riskDivider.Value;
		set => _riskDivider.Value = value;
	}

	/// <summary>
	/// Lot reduction factor after consecutive losses.
	/// </summary>
	public decimal DecreaseFactor
	{
		get => _decreaseFactor.Value;
		set => _decreaseFactor.Value = value;
	}

	/// <summary>
	/// EMA length applied to median price.
	/// </summary>
	public int MaPeriod
	{
		get => _maPeriod.Value;
		set => _maPeriod.Value = value;
	}

	/// <summary>
	/// CCI lookback period.
	/// </summary>
	public int CciPeriod
	{
		get => _cciPeriod.Value;
		set => _cciPeriod.Value = value;
	}

	/// <summary>
	/// Absolute CCI level required for entry.
	/// </summary>
	public decimal CciThreshold
	{
		get => _cciThreshold.Value;
		set => _cciThreshold.Value = value;
	}

	/// <summary>
	/// Laguerre smoothing factor (gamma).
	/// </summary>
	public decimal LaguerreGamma
	{
		get => _laguerreGamma.Value;
		set => _laguerreGamma.Value = value;
	}

	/// <summary>
	/// Tolerance applied when checking Laguerre against 0 or 1.
	/// </summary>
	public decimal LaguerreEntryTolerance
	{
		get => _laguerreEntryTolerance.Value;
		set => _laguerreEntryTolerance.Value = value;
	}

	/// <summary>
	/// Laguerre exit threshold for long positions.
	/// </summary>
	public decimal LaguerreExitHigh
	{
		get => _laguerreExitHigh.Value;
		set => _laguerreExitHigh.Value = value;
	}

	/// <summary>
	/// Laguerre exit threshold for short positions.
	/// </summary>
	public decimal LaguerreExitLow
	{
		get => _laguerreExitLow.Value;
		set => _laguerreExitLow.Value = value;
	}

	/// <summary>
	/// Profit distance expressed in price points.
	/// </summary>
	public decimal TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Candle type processed by the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_ema = null!;
		_cci = null!;
		_previousMa = null;
		_lagL0 = _lagL1 = _lagL2 = _lagL3 = 0m;
		_laguerreFormed = false;
		_entryPrice = null;
		_entryVolume = 0m;
		_entrySide = null;
		_consecutiveLosses = 0;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_ema = new EMA { Length = MaPeriod };
		_cci = new CommodityChannelIndex { Length = CciPeriod };

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(_ema, ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _ema);
			DrawOwnTrades(area);
		}
	}

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

		// Process CCI manually
		_cci.Process(candle);

		if (!_ema.IsFormed || !_cci.IsFormed)
		{
			_previousMa = ma;
			return;
		}

		var cci = _cci.GetCurrentValue<decimal>();

		var laguerre = CalculateLaguerre(candle.ClosePrice);
		if (!_laguerreFormed)
		{
			_previousMa = ma;
			return;
		}

		var previousMa = _previousMa;
		_previousMa = ma;
		if (!previousMa.HasValue)
			return;

		var maRising = ma > previousMa.Value;
		var maFalling = ma < previousMa.Value;
		var entryTolerance = LaguerreEntryTolerance;
		var takeProfitDistance = GetTakeProfitDistance();
		var price = GetDecisionPrice(candle);

		if (Position == 0m && !HasActiveOrders())
		{
			if (maRising && laguerre <= entryTolerance && cci < -CciThreshold)
			{
				var volume = CalculateOrderVolume(price);
				if (volume > 0m)
				{
					BuyMarket(volume);
					_entrySide = Sides.Buy;
					_entryPrice = price;
					_entryVolume = volume;
					LogInfo($"Opening long. Laguerre={laguerre:F4}, CCI={cci:F2}, EMA rising.");
				}
			}
			else if (maFalling && laguerre >= 1m - entryTolerance && cci > CciThreshold)
			{
				var volume = CalculateOrderVolume(price);
				if (volume > 0m)
				{
					SellMarket(volume);
					_entrySide = Sides.Sell;
					_entryPrice = price;
					_entryVolume = volume;
					LogInfo($"Opening short. Laguerre={laguerre:F4}, CCI={cci:F2}, EMA falling.");
				}
			}
		}

		if (_entrySide == Sides.Buy && Position > 0m && _entryPrice.HasValue)
		{
			var gain = price - _entryPrice.Value;
			if ((LaguerreExitHigh > 0m && laguerre >= LaguerreExitHigh) || (takeProfitDistance > 0m && gain >= takeProfitDistance))
			{
				var volume = Math.Abs(Position);
				if (volume <= 0m)
					volume = _entryVolume;

				if (volume > 0m && !HasActiveOrders())
				{
					SellMarket(volume);
					RegisterTradeResult(gain);
					ResetPositionState();
					LogInfo($"Closing long. Laguerre={laguerre:F4}, gain={gain:F5}.");
				}
			}
		}
		else if (_entrySide == Sides.Sell && Position < 0m && _entryPrice.HasValue)
		{
			var gain = _entryPrice.Value - price;
			if ((LaguerreExitLow > 0m && laguerre <= LaguerreExitLow) || (takeProfitDistance > 0m && gain >= takeProfitDistance))
			{
				var volume = Math.Abs(Position);
				if (volume <= 0m)
					volume = _entryVolume;

				if (volume > 0m && !HasActiveOrders())
				{
					BuyMarket(volume);
					RegisterTradeResult(gain);
					ResetPositionState();
					LogInfo($"Closing short. Laguerre={laguerre:F4}, gain={gain:F5}.");
				}
			}
		}
		else if (Position == 0m && !HasActiveOrders())
		{
			ResetPositionState();
		}
	}

	private decimal CalculateLaguerre(decimal price)
	{
		var gamma = LaguerreGamma;
		var l0Prev = _lagL0;
		var l1Prev = _lagL1;
		var l2Prev = _lagL2;
		var l3Prev = _lagL3;

		_lagL0 = (1m - gamma) * price + gamma * l0Prev;
		_lagL1 = -gamma * _lagL0 + l0Prev + gamma * l1Prev;
		_lagL2 = -gamma * _lagL1 + l1Prev + gamma * l2Prev;
		_lagL3 = -gamma * _lagL2 + l2Prev + gamma * l3Prev;

		decimal cu = 0m;
		decimal cd = 0m;

		if (_lagL0 >= _lagL1)
			cu = _lagL0 - _lagL1;
		else
			cd = _lagL1 - _lagL0;

		if (_lagL1 >= _lagL2)
			cu += _lagL1 - _lagL2;
		else
			cd += _lagL2 - _lagL1;

		if (_lagL2 >= _lagL3)
			cu += _lagL2 - _lagL3;
		else
			cd += _lagL3 - _lagL2;

		var denominator = cu + cd;
		var result = denominator == 0m ? 0m : cu / denominator;

		_laguerreFormed = true;
		return result;
	}

	private decimal CalculateOrderVolume(decimal price)
	{
		var volume = BaseVolume;

		if (MaximumRisk > 0m && RiskDivider > 0m)
		{
			var portfolio = Portfolio;
			var equity = portfolio?.CurrentValue ?? portfolio?.BeginValue ?? 0m;
			if (equity > 0m && price > 0m)
			{
				var riskVolume = equity * MaximumRisk / RiskDivider;
				riskVolume /= price;
				if (riskVolume > volume)
					volume = riskVolume;
			}
		}

		if (DecreaseFactor > 0m && _consecutiveLosses > 1)
		{
			var reduction = volume * _consecutiveLosses / DecreaseFactor;
			volume -= reduction;
		}

		return NormalizeVolume(volume);
	}

	private decimal NormalizeVolume(decimal volume)
	{
		var security = Security;
		if (security != null)
		{
			var step = security.VolumeStep ?? 0m;
			if (step <= 0m)
				step = 1m;

			var minVolume = security.MinVolume ?? step;
			var maxVolume = security.MaxVolume;

			var steps = decimal.Floor(volume / step);
			if (steps < 1m)
				steps = 1m;

			volume = steps * step;

			if (volume < minVolume)
				volume = minVolume;

			if (maxVolume is decimal max && max > 0m && volume > max)
				volume = max;
		}

		if (volume <= 0m)
			volume = 1m;

		return volume;
	}

	private decimal GetTakeProfitDistance()
	{
		if (TakeProfitPoints <= 0m)
			return 0m;

		var point = Security?.PriceStep ?? 0m;
		if (point <= 0m)
		{
			var decimals = Security?.Decimals ?? 4;
			point = 1m;
			for (var i = 0; i < decimals; i++)
				point /= 10m;
		}

		return TakeProfitPoints * point;
	}

	private decimal GetDecisionPrice(ICandleMessage candle)
	{
		if (candle.ClosePrice > 0m)
			return candle.ClosePrice;

		return candle.OpenPrice;
	}

	private bool HasActiveOrders()
	{
		foreach (var order in Orders)
		{
			if (order.State == OrderStates.Active)
				return true;
		}

		return false;
	}

	private void RegisterTradeResult(decimal gain)
	{
		if (gain > 0m)
		{
			if (_consecutiveLosses > 0)
				LogInfo($"Profit resets loss streak of {_consecutiveLosses} trades.");

			_consecutiveLosses = 0;
		}
		else if (gain < 0m)
		{
			_consecutiveLosses++;
			LogInfo($"Loss streak increased to {_consecutiveLosses}.");
		}
	}

	private void ResetPositionState()
	{
		_entryPrice = null;
		_entryVolume = 0m;
		_entrySide = null;
	}
}