Ver no GitHub

Estratégia de Cara ou Coroa

Visão geral

A Estratégia de Cara ou Coroa é um port literal do clássico consultor especialista do MetaTrader que decide se comprar ou vender simulando um lançamento de moeda. Cada vela concluída desencadeia uma nova decisão quando a estratégia está sem posição, de modo que o sistema alterna por uma série contínua de negociações independentes. A conversão do StockSharp mantém o comportamento intencionalmente simples: apenas uma posição é mantida por vez e cada negociação é combinada com um take-profit e stop-loss simétricos expressos em pips.

Embora a ideia central seja intencionalmente ingênua, o exemplo demonstra como traduzir até mesmo Consultores Especialistas muito pequenos para a API de alto nível do StockSharp. A estratégia é útil como auxílio didático para configurar assinaturas, auxiliares de gestão de dinheiro e ordens protetoras.

Lógica de trading

  1. Ao iniciar a estratégia, o gerador de números aleatórios é semeado com a contagem de ticks do ambiente atual, correspondendo ao espírito da chamada original MathSrand(GetTickCount()) do MQL.
  2. Para cada vela finalizada (o período padrão é 1 minuto, mas qualquer tipo de vela pode ser fornecido), a estratégia verifica se o trading é permitido e se não há posição aberta no momento.
  3. Quando sem posição, o gerador produz 0 ou 1. Um valor de 0 resulta em uma ordem de compra a mercado, enquanto 1 aciona uma ordem de venda a mercado. O volume é calculado dinamicamente com base na porcentagem de risco configurada e na distância do stop-loss.
  4. As ordens protetoras criadas por StartProtection anexam um stop-loss e take-profit a cada posição para que o gerenciamento de saída permaneça automático.

Nenhum outro filtro é usado: toda vez que uma posição é fechada, a próxima vela cria imediatamente uma nova negociação.

Dimensionamento de posição

A versão do StockSharp reinterpreta a fórmula do tamanho do lote para trabalhar com valores de portfólio. O valor de risco é calculado como Portfolio.CurrentValue * RiskPercent / 100. Este capital é dividido pela distância do stop-loss em unidades de preço (pips convertidos usando o passo de preço do instrumento) para derivar o número de contratos. O auxiliar então arredonda o tamanho para o passo de volume admissível mais próximo e aplica limites da bolsa através de MinVolume e MaxVolume.

Isso mantém o espírito do código original — arriscar uma porcentagem fixa do patrimônio por negociação — garantindo que o tamanho da ordem respeite os metadados de segurança do StockSharp.

Parâmetros

Parâmetro Descrição Padrão Notas
RiskPercent Porcentagem do portfólio arriscada em cada negociação. 2 Aumentar este número amplifica o volume; reduções tornam as ordens menores.
TakeProfitPips Distância entre a entrada e o nível de take-profit em pips. 20 Convertido para preço absoluto usando o passo de preço do instrumento e passado para StartProtection.
StopLossPips Distância entre a entrada e o nível de stop-loss em pips. 10 Também convertido em unidades de preço; o mesmo valor é usado para o dimensionamento de posição.
CandleType Assinatura de velas que programa o loop de decisão. período de 1 minuto Qualquer tipo de vela do StockSharp pode ser fornecido; intervalos maiores desaceleram o ritmo de trading.

Gestão de risco

StartProtection é iniciado uma vez durante OnStarted com as distâncias de take-profit e stop-loss calculadas. O StockSharp então gerencia as ordens protetoras automaticamente, espelhando os argumentos OrderSend no script MQL. Como a estratégia só opera quando Position == 0, não é necessário cancelar ou reenviar manualmente as ordens existentes; a plataforma cancela as ordens protetoras assim que a posição é fechada.

Notas de implementação

  • O processamento de velas usa o padrão de alto nível SubscribeCandles().Bind(...) para clareza e simplicidade.
  • As declarações de log descrevem a direção escolhida e o volume para que os backtests mostrem claramente como o gerador pseudoaleatório se comporta.
  • A normalização do volume leva em conta VolumeStep, MinVolume e MaxVolume, garantindo que os tamanhos gerados cumpram a especificação do instrumento.
  • O código mantém todos os comentários em inglês, conforme necessário, e espelha a estrutura exigida pelas diretrizes do repositório.

Notas de uso

  • Como a direção de trading é aleatória, não se espera lucratividade a longo prazo. Usar a estratégia para fins de demonstração ou teste.
  • Garantir que o portfólio atribuído à estratégia tenha um CurrentValue positivo; caso contrário, o cálculo de risco retorna zero e nenhuma negociação será feita.
  • Ajustar o tipo de vela se preferir que o lançamento de moeda ocorra com menos frequência (por exemplo, velas horárias) ou com mais frequência (por exemplo, velas de tick).
  • Ao otimizar, é possível explorar distâncias alternativas de take-profit e stop-loss ou reduzir a porcentagem de risco para manter as reduções gerenciáveis.
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>
/// Randomized coin flipping strategy that alternates between buying and selling based on a pseudo-random generator.
/// Mimics the original MetaTrader expert advisor by opening a single position at a time with symmetric risk controls.
/// </summary>
public class CoinFlippingStrategy : Strategy
{
	private readonly StrategyParam<decimal> _riskPercent;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<DataType> _candleType;

	private Random _random;
	private decimal _priceStep;
	private decimal _takeProfitDistance;
	private decimal _stopLossDistance;
	private decimal _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takePrice;

	/// <summary>
	/// Portfolio share allocated to every trade in percent.
	/// </summary>
	public decimal RiskPercent
	{
		get => _riskPercent.Value;
		set => _riskPercent.Value = value;
	}

	/// <summary>
	/// Take profit distance measured in pips.
	/// </summary>
	public int TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Stop loss distance measured in pips.
	/// </summary>
	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Candle type used for scheduling trade attempts.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initialize strategy parameters.
	/// </summary>
	public CoinFlippingStrategy()
	{
		_riskPercent = Param(nameof(RiskPercent), 2m)
			.SetGreaterThanZero()
			.SetDisplay("Risk %", "Portfolio percentage allocated per trade", "Risk Management")
			
			.SetOptimize(1m, 10m, 1m);

		_takeProfitPips = Param(nameof(TakeProfitPips), 5000)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit (pips)", "Target distance expressed in pips", "Risk Management")

			.SetOptimize(10, 50, 5);

		_stopLossPips = Param(nameof(StopLossPips), 3000)
			.SetGreaterThanZero()
			.SetDisplay("Stop Loss (pips)", "Protective stop distance expressed in pips", "Risk Management")

			.SetOptimize(5, 30, 5);

		_candleType = Param(nameof(CandleType), TimeSpan.FromDays(1).TimeFrame())
			.SetDisplay("Candle Type", "Candle type used for trade timing", "Data");
	}

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

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

		// Reset cached state when the strategy is reset.
		_random = null;
		_priceStep = 0m;
		_takeProfitDistance = 0m;
		_stopLossDistance = 0m;
		_entryPrice = 0m;
		_stopPrice = null;
		_takePrice = null;
	}

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

		// Seed the pseudo-random generator similarly to the MQL expert.
		_random = new Random(System.Environment.TickCount);

		// Determine price step information for translating pips into price units.
		_priceStep = Security?.PriceStep ?? 1m;
		if (_priceStep <= 0m)
			_priceStep = 1m;

		_takeProfitDistance = TakeProfitPips * _priceStep;
		_stopLossDistance = StopLossPips * _priceStep;

		// Subscribe to candle data to trigger decision making once per bar.
		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(ProcessCandle).Start();

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

	private void ProcessCandle(ICandleMessage candle)
	{
		// Only use completed candles to avoid duplicate executions while a bar is forming.
		if (candle.State != CandleStates.Finished)
			return;

		// Check risk management first.
		if (Position > 0)
		{
			if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
			{
				SellMarket(Position);
				ResetTargets();
			}
			else if (_takePrice.HasValue && candle.HighPrice >= _takePrice.Value)
			{
				SellMarket(Position);
				ResetTargets();
			}
		}
		else if (Position < 0)
		{
			if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
			{
				BuyMarket(Math.Abs(Position));
				ResetTargets();
			}
			else if (_takePrice.HasValue && candle.LowPrice <= _takePrice.Value)
			{
				BuyMarket(Math.Abs(Position));
				ResetTargets();
			}
		}

		// The strategy maintains at most one position at a time.
		if (Position != 0)
			return;

		if (_random == null)
			return;

		var entryPrice = candle.ClosePrice;
		if (entryPrice <= 0m)
			return;

		var volume = CalculateOrderVolume(entryPrice);
		if (volume <= 0m)
			return;

		var isBuy = _random.Next(0, 2) == 0;
		if (isBuy)
		{
			BuyMarket(volume);
			_entryPrice = entryPrice;
			_stopPrice = _stopLossDistance > 0m ? entryPrice - _stopLossDistance : null;
			_takePrice = _takeProfitDistance > 0m ? entryPrice + _takeProfitDistance : null;
		}
		else
		{
			SellMarket(volume);
			_entryPrice = entryPrice;
			_stopPrice = _stopLossDistance > 0m ? entryPrice + _stopLossDistance : null;
			_takePrice = _takeProfitDistance > 0m ? entryPrice - _takeProfitDistance : null;
		}
	}

	private decimal CalculateOrderVolume(decimal entryPrice)
	{
		var balance = Portfolio?.CurrentValue ?? 0m;
		if (balance <= 0m)
			return 0m;

		var riskAmount = balance * RiskPercent / 100m;
		if (riskAmount <= 0m)
			return 0m;

		var stopDistance = _stopLossDistance;
		if (stopDistance <= 0m)
		{
			stopDistance = StopLossPips * _priceStep;
		}

		if (stopDistance <= 0m)
			return 0m;

		// Risk per unit equals the stop distance; divide to get the number of contracts.
		var rawVolume = riskAmount / stopDistance;
		var volume = NormalizeVolume(rawVolume);

		if (volume <= 0m)
		{
			volume = Volume > 0m ? Volume : 1m;
			volume = NormalizeVolume(volume);
		}

		return volume;
	}

	private decimal NormalizeVolume(decimal volume)
	{
		if (volume <= 0m)
			return 0m;

		var step = Security?.VolumeStep;
		if (step.HasValue && step.Value > 0m)
		{
			volume = Math.Floor(volume / step.Value) * step.Value;
		}

		return volume > 0m ? volume : 1m;
	}

	private void ResetTargets()
	{
		_entryPrice = 0m;
		_stopPrice = null;
		_takePrice = null;
	}
}