La Estrategia de Lanzamiento de Moneda es un port literal del clásico asesor experto de MetaTrader que decide si comprar o vender simulando un lanzamiento de moneda. Cada vela completada desencadena una nueva decisión cuando la estrategia está plana, por lo que el sistema alterna a través de una serie continua de operaciones independientes. La conversión a StockSharp mantiene el comportamiento intencionalmente simple: solo se mantiene una posición a la vez y cada operación va emparejada con un take-profit y stop-loss simétricos expresados en pips.
Aunque la idea central es intencionalmente ingenua, el ejemplo demuestra cómo traducir incluso asesores expertos muy pequeños a la API de alto nivel de StockSharp. La estrategia es útil como ayuda didáctica para conectar suscripciones, ayudantes de gestión de dinero y órdenes protectoras.
Lógica de trading
Al iniciar la estrategia, el generador de números aleatorios se inicializa con el recuento de ticks del entorno actual, reflejando el espíritu de la llamada original MathSrand(GetTickCount()) de MQL.
Para cada vela terminada (el marco temporal predeterminado es 1 minuto, pero se puede proporcionar cualquier tipo de vela) la estrategia verifica si está permitido operar y si no hay ninguna posición abierta actualmente.
Cuando está plana, el generador produce 0 o 1. Un valor de 0 resulta en una orden de compra a mercado, mientras que 1 activa una orden de venta a mercado. El volumen se calcula dinámicamente basado en el porcentaje de riesgo configurado y la distancia al stop-loss.
Las órdenes protectoras creadas por StartProtection adjuntan un stop-loss y take-profit a cada posición para que la gestión de salida permanezca automática.
No se usan otros filtros: cada vez que se cierra una posición, la siguiente vela crea inmediatamente una nueva operación.
Dimensionamiento de posición
La versión de StockSharp reinterpreta la fórmula del tamaño del lote para trabajar con valores de cartera. El monto de riesgo se calcula como Portfolio.CurrentValue * RiskPercent / 100. Este capital se divide por la distancia del stop-loss en unidades de precio (pips convertidos usando el paso de precio del instrumento) para derivar el número de contratos. El helper luego redondea el tamaño al paso de volumen admisible más cercano y aplica los límites del mercado a través de MinVolume y MaxVolume.
Esto mantiene el espíritu del código original — arriesgar un porcentaje fijo del patrimonio por operación — mientras se asegura de que el tamaño de la orden respete los metadatos de seguridad de StockSharp.
Parámetros
Parámetro
Descripción
Predeterminado
Notas
RiskPercent
Porcentaje de la cartera arriesgado en cada operación.
2
Aumentar este número amplifica el volumen; las reducciones hacen las órdenes más pequeñas.
TakeProfitPips
Distancia entre la entrada y el nivel de take-profit en pips.
20
Convertido a precio absoluto usando el paso de precio del instrumento y pasado a StartProtection.
StopLossPips
Distancia entre la entrada y el nivel de stop-loss en pips.
10
También convertido a unidades de precio; el mismo valor se usa para el dimensionamiento de posición.
CandleType
Suscripción de velas que programa el bucle de decisión.
marco temporal de 1 minuto
Se puede proporcionar cualquier tipo de vela de StockSharp; intervalos más altos ralentizan el ritmo de trading.
Gestión de riesgos
StartProtection se inicia una vez durante OnStarted con las distancias de take-profit y stop-loss calculadas. StockSharp entonces gestiona las órdenes protectoras automáticamente, reflejando los argumentos OrderSend en el script MQL. Debido a que la estrategia solo opera cuando Position == 0, no es necesario cancelar o reenviar manualmente las órdenes existentes; la plataforma cancela las órdenes protectoras una vez que se cierra la posición.
Notas de implementación
El procesamiento de velas usa el patrón de alto nivel SubscribeCandles().Bind(...) para mayor claridad y simplicidad.
Las declaraciones de registro describen la dirección elegida y el volumen para que los backtests muestren claramente cómo se comporta el generador pseudoaleatorio.
La normalización del volumen tiene en cuenta VolumeStep, MinVolume y MaxVolume, asegurando que los tamaños generados cumplan con la especificación del instrumento.
El código mantiene todos los comentarios en inglés, según lo requerido, y refleja la estructura exigida por las pautas del repositorio.
Notas de uso
Debido a que la dirección de trading es aleatoria, no se espera rentabilidad a largo plazo. Usar la estrategia con fines de demostración o prueba.
Asegurarse de que la cartera asignada a la estrategia tenga un CurrentValue positivo, de lo contrario el cálculo de riesgo devuelve cero y no se realizarán operaciones.
Ajustar el tipo de vela si se prefiere que el lanzamiento de moneda ocurra con menos frecuencia (por ejemplo, velas horarias) o más a menudo (por ejemplo, velas de tick).
Al optimizar, se pueden explorar distancias alternativas de take-profit y stop-loss o reducir el porcentaje de riesgo para mantener las pérdidas manejables.
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()