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
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.
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.
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.
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;
}
}
import clr
import random
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class coin_flipping_strategy(Strategy):
def __init__(self):
super(coin_flipping_strategy, self).__init__()
self._risk_percent = self.Param("RiskPercent", 2.0)
self._take_profit_pips = self.Param("TakeProfitPips", 5000)
self._stop_loss_pips = self.Param("StopLossPips", 3000)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromDays(1)))
self._rng = None
self._price_step = 1.0
self._tp_dist = 0.0
self._sl_dist = 0.0
self._entry_price = 0.0
self._stop_price = None
self._take_price = None
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def RiskPercent(self):
return self._risk_percent.Value
@property
def TakeProfitPips(self):
return self._take_profit_pips.Value
@property
def StopLossPips(self):
return self._stop_loss_pips.Value
def OnStarted2(self, time):
super(coin_flipping_strategy, self).OnStarted2(time)
self._rng = random.Random()
sec = self.Security
self._price_step = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None else 1.0
if self._price_step <= 0:
self._price_step = 1.0
self._tp_dist = self.TakeProfitPips * self._price_step
self._sl_dist = self.StopLossPips * self._price_step
self._entry_price = 0.0
self._stop_price = None
self._take_price = None
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
high = float(candle.HighPrice)
low = float(candle.LowPrice)
close = float(candle.ClosePrice)
if self.Position > 0:
if self._stop_price is not None and low <= self._stop_price:
self.SellMarket()
self._reset_targets()
elif self._take_price is not None and high >= self._take_price:
self.SellMarket()
self._reset_targets()
elif self.Position < 0:
if self._stop_price is not None and high >= self._stop_price:
self.BuyMarket()
self._reset_targets()
elif self._take_price is not None and low <= self._take_price:
self.BuyMarket()
self._reset_targets()
if self.Position != 0:
return
if self._rng is None:
return
if close <= 0:
return
is_buy = self._rng.randint(0, 1) == 0
if is_buy:
self.BuyMarket()
self._entry_price = close
self._stop_price = close - self._sl_dist if self._sl_dist > 0 else None
self._take_price = close + self._tp_dist if self._tp_dist > 0 else None
else:
self.SellMarket()
self._entry_price = close
self._stop_price = close + self._sl_dist if self._sl_dist > 0 else None
self._take_price = close - self._tp_dist if self._tp_dist > 0 else None
def _reset_targets(self):
self._entry_price = 0.0
self._stop_price = None
self._take_price = None
def OnReseted(self):
super(coin_flipping_strategy, self).OnReseted()
self._rng = None
self._price_step = 1.0
self._tp_dist = 0.0
self._sl_dist = 0.0
self._entry_price = 0.0
self._stop_price = None
self._take_price = None
def CreateClone(self):
return coin_flipping_strategy()