Die Münzwurf-Strategie ist ein wörtlicher Port des klassischen MetaTrader-Expertenberaters, der entscheidet, ob er kaufen oder verkaufen soll, indem er einen Münzwurf simuliert. Jede abgeschlossene Kerze löst eine neue Entscheidung aus, wenn die Strategie keine Position hält, sodass das System eine kontinuierliche Reihe unabhängiger Trades durchläuft. Die StockSharp-Konvertierung behält das Verhalten absichtlich einfach bei: Es wird nur eine Position gleichzeitig gehalten, und jeder Trade ist mit einem symmetrischen Take-Profit und Stop-Loss in Pips verbunden.
Obwohl die Kernidee absichtlich naiv ist, demonstriert das Beispiel, wie man auch sehr kleine Expert Advisors in die StockSharp High-Level-API übersetzen kann. Die Strategie ist als Lehrhilfe für die Verkabelung von Abonnements, Geldmanagement-Helfern und Schutzaufträgen nützlich.
Handelslogik
Beim Start der Strategie wird der Zufallszahlengenerator mit dem aktuellen Umgebungs-Tick-Count initialisiert, was dem ursprünglichen MathSrand(GetTickCount())-Aufruf aus MQL entspricht.
Für jede abgeschlossene Kerze (der Standard-Zeitrahmen ist 1 Minute, aber jeder Kerzentyp kann angegeben werden) prüft die Strategie, ob der Handel erlaubt ist und ob keine Position aktuell offen ist.
Wenn keine Position offen ist, produziert der Generator entweder 0 oder 1. Ein Wert von 0 führt zu einem Markt-Kaufauftrag, während 1 einen Markt-Verkaufsauftrag auslöst. Das Volumen wird dynamisch basierend auf dem konfigurierten Risikoprozentsatz und dem Stop-Loss-Abstand berechnet.
Von StartProtection erstellte Schutzaufträge fügen jeder Position einen Stop-Loss und Take-Profit hinzu, sodass die Exit-Verwaltung automatisch bleibt.
Es werden keine anderen Filter verwendet: Jedes Mal wenn eine Position geschlossen wird, erstellt die nächste Kerze sofort einen neuen Trade.
Positionsgröße
Die StockSharp-Version interpretiert die Losgröße-Formel neu, um mit Portfolio-Werten zu arbeiten. Der Risikobetrag wird als Portfolio.CurrentValue * RiskPercent / 100 berechnet. Dieses Kapital wird durch den Stop-Loss-Abstand in Preiseinheiten (Pips konvertiert unter Verwendung des Sicherheitspreisschritts) geteilt, um die Anzahl der Kontrakte abzuleiten. Der Helfer rundet dann die Größe auf den nächsten zulässigen Volumenschritt und setzt Börsenlimits durch MinVolume und MaxVolume durch.
Dies bewahrt den Geist des Originalcodes — ein festes Prozentsatz des Eigenkapitals pro Trade riskieren — und stellt gleichzeitig sicher, dass die Auftragsgröße die StockSharp-Sicherheitsmetadaten respektiert.
Parameter
Parameter
Beschreibung
Standard
Hinweise
RiskPercent
Prozentsatz des Portfolios, der bei jedem Trade riskiert wird.
2
Erhöhung dieser Zahl amplifies das Volumen; Reduzierungen machen die Aufträge kleiner.
TakeProfitPips
Abstand zwischen Einstieg und Take-Profit-Level in Pips.
20
In absoluten Preis umgerechnet unter Verwendung des Instrument-Preisschritts und an StartProtection übergeben.
StopLossPips
Abstand zwischen Einstieg und Stop-Loss-Level in Pips.
10
Ebenfalls in Preiseinheiten umgerechnet; derselbe Wert wird für die Positionsgrößenbestimmung verwendet.
CandleType
Kerzenabonnement, das die Entscheidungsschleife plant.
1-Minuten-Zeitrahmen
Es kann jeder StockSharp-Kerzentyp angegeben werden; höhere Intervalle verlangsamen das Handelstempo.
Risikomanagement
StartProtection wird einmal während OnStarted mit den berechneten Take-Profit- und Stop-Loss-Abständen gestartet. StockSharp verwaltet dann die Schutzaufträge automatisch und spiegelt die OrderSend-Argumente im MQL-Skript wider. Da die Strategie nur handelt, wenn Position == 0, müssen vorhandene Aufträge nicht manuell storniert oder neu eingereicht werden; die Plattform storniert die Schutzaufträge, sobald die Position geschlossen ist.
Implementierungshinweise
Die Kerzenverarbeitung verwendet das High-Level SubscribeCandles().Bind(...)-Muster für Klarheit und Einfachheit.
Log-Anweisungen beschreiben die gewählte Richtung und das Volumen, damit Backtests deutlich zeigen, wie sich der Pseudozufallsgenerator verhält.
Die Volumen-Normalisierung berücksichtigt VolumeStep, MinVolume und MaxVolume, wodurch sichergestellt wird, dass generierte Größen der Instrumentenspezifikation entsprechen.
Der Code hält alle Kommentare auf Englisch, wie erforderlich, und spiegelt die vom Repository verlangte Struktur wider.
Verwendungshinweise
Da die Handelsrichtung zufällig ist, wird keine langfristige Rentabilität erwartet. Die Strategie für Demonstrations- oder Testzwecke verwenden.
Sicherstellen, dass das der Strategie zugewiesene Portfolio einen positiven CurrentValue hat, sonst gibt die Risikoberechnung null zurück und es werden keine Trades platziert.
Den Kerzentyp anpassen, wenn man möchte, dass der Münzwurf seltener (z.B. stündliche Kerzen) oder häufiger (z.B. Tick-Kerzen) stattfindet.
Bei der Optimierung können alternative Take-Profit- und Stop-Loss-Abstände erkundet oder der Risikoprozentsatz gesenkt werden, um Drawdowns handhabbar zu halten.
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()