Die Strategie reproduziert das Verhalten des ursprünglichen "Martin"-Expertenberaters aus MQL, indem sie ein abgesichertes Martingale-Grid um den aktuellen Preis betreibt. Sie wechselt kontinuierlich zwischen Long- und Short-Positionen und verdoppelt das gehandelte Volumen bei jeder Umkehr, bis der kumulierte Gewinn des gesamten Korbs das konfigurierte Ziel erreicht. Kerzen werden nur als Treiber für die Entscheidungslogik verwendet, während die tatsächlichen Ausführungen auf Marktorders und Stop-Orders basieren, die durch die High-Level-API von StockSharp bereitgestellt werden.
Funktionsweise
Beim Start liest die Strategie den PriceStep des Instruments, um die Parameter EntryOffsetPoints und StepPoints in absolute Preisabstände umzurechnen. Fehlt der Preisschritt, wird der Wert 1 angenommen.
Wenn keine offene Position und kein aktiver Martingale-Zyklus vorhanden ist, platziert die Strategie eine Kauf-Stop-Order und eine Verkauf-Stop-Order um den letzten Schlusskurs. Die Abstände betragen EntryOffsetPoints * PriceStep, was dem 10-Punkte-Abstand im ursprünglichen MQL-Code entspricht.
Wenn eine der Stop-Orders ausgeführt wird, wird die entgegengesetzte ausstehende Order storniert. Die Ausführung definiert den ersten Trade der Martingale-Sequenz: Die Strategie speichert Preis, Volumen und Richtung und setzt den internen Level-Zähler auf 1.
Bei jedem anschließenden Kerzenschluss wird der aktuelle Schlusskurs mit dem Preis der zuletzt ausgeführten Order verglichen. Hat sich der Markt mindestens martingaleLevel * StepPoints * PriceStep gegen diese Order bewegt, wird eine Marktorder in entgegengesetzter Richtung mit doppeltem Volumen gegenüber dem vorherigen Trade eingereicht. Die letzte Trade-Information wird nach jeder Ausführung aktualisiert.
Der unrealisierte Gewinn wird als PnL + Position * (closePrice - PositionPrice) bewertet. Wenn dieser aggregierte Gewinn den Parameter ProfitTarget übersteigt, sendet die Strategie CloseAll(), um jede Position im Korb zu schließen, storniert alle verbleibenden Orders und setzt den Zyklus zurück, damit ein neues Stop-Order-Paar platziert werden kann.
Dieselbe Zurücksetzung erfolgt automatisch, wenn alle Positionen manuell geschlossen werden: Die internen Zähler werden gelöscht und neue Stop-Orders werden bei der nächsten Kerze erstellt.
Dieser Workflow spiegelt die alternierende Kauf-/Verkaufslogik des ursprünglichen Expertenberaters wider und hält die Implementierung vollständig innerhalb der High-Level-API von StockSharp.
Parameter
StepPoints – Anzahl der Preisschritte zur Berechnung des Umkehrschwellenwerts für die nächste Durchschnittsorder. Standardmäßig 10 und kann optimiert werden.
EntryOffsetPoints – Abstand für die initialen Kauf-/Verkauf-Stop-Orders in Preisschritten. Ebenfalls standardmäßig 10 Punkte wie die MQL-Version.
ProfitTarget – absoluter Währungsgewinn, der zum Schließen des gesamten Martingale-Korbs erforderlich ist. Sobald der kombinierte realisierte und unrealisierte PnL diesen Wert übersteigt, werden alle Positionen liquidiert.
CandleType – Kerzenabonnement zum Antreiben der Strategielogik. Der Standard ist der Ein-Minuten-Zeitrahmen, aber jeder vom Marktplatz unterstützte DataType kann ausgewählt werden.
Die Basis-Handelsgröße wird aus der Volume-Eigenschaft der Strategie entnommen. Jede neue Umkehr multipliziert diese Basis nach der klassischen Martingale-Methode mit Zweierpotenzen.
Praktische Hinweise
Konfigurieren Sie Volume immer entsprechend der minimalen Lotgröße des Brokers. Das Verdoppelungsschema erhöht das Exposure schnell, daher sollten Risikolimits extern durchgesetzt werden.
Da die Orderplatzierung durch Kerzenschlüsse gesteuert wird, können schnelle Preisbewegungen innerhalb der Kerze Einstiege etwas später auslösen als die tickbasierte MQL-Version. Die Stop-Orders halten jedoch die Einstiegspreise im Einklang mit der ursprünglichen Logik.
Die Strategie zeichnet Preiskerzen und eigene Trades im Standarddiagrammbereich für einfachere visuelle Verfolgung.
Es wird kein automatischer Stop-Loss verwendet. Die einzige Ausstiegsbedingung ist der ProfitTarget, daher sollten Instrument und Zeitrahmen sorgfältig gewählt werden, um das Risiko großer adverser Trends zu kontrollieren.
Unterschiede zum MQL-Experten
StockSharp verwendet Nettopositionen, daher wird jede Umkehr mit einer Marktorder ausgeführt, die sowohl das vorherige Exposure schließt als auch die neue Position in einem einzigen Trade eröffnet. Der kumulative PnL des Korbs bleibt identisch mit der abgesicherten Implementierung.
Die Tick-für-Tick-Logik wurde durch Kerzenschlüsse für die Signalauswertung ersetzt, um im Rahmen der empfohlenen High-Level-API-Nutzung zu bleiben.
Order-Identifikatoren werden verfolgt, um die Verarbeitung partieller Ausführungen mehrfach zu vermeiden und so sicherzustellen, dass die Volumen-Verdoppelungslogik konsistent bleibt.
Diese Änderungen halten das Handelsverhalten der Quellstrategie treu, während sie an das StockSharp-Framework angepasst werden.
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>
/// Martingale grid that alternates long and short entries while doubling volume.
/// </summary>
public class MartinMartingaleStrategy : Strategy
{
private readonly StrategyParam<int> _stepPoints;
private readonly StrategyParam<int> _entryOffsetPoints;
private readonly StrategyParam<decimal> _profitTarget;
private readonly StrategyParam<int> _maxLevel;
private readonly StrategyParam<DataType> _candleType;
private decimal _stepSize;
private decimal _entryOffset;
private decimal _lastTradePrice;
private decimal _lastTradeVolume;
private int _martingaleLevel;
private Sides? _lastTradeSide;
private bool _isClosing;
private decimal? _initialPrice;
/// <summary>
/// Distance in points that defines when the next reversal is triggered.
/// </summary>
public int StepPoints
{
get => _stepPoints.Value;
set => _stepPoints.Value = value;
}
/// <summary>
/// Offset in points for the initial breakout entry.
/// </summary>
public int EntryOffsetPoints
{
get => _entryOffsetPoints.Value;
set => _entryOffsetPoints.Value = value;
}
/// <summary>
/// Aggregated profit required to close the entire martingale cycle.
/// </summary>
public decimal ProfitTarget
{
get => _profitTarget.Value;
set => _profitTarget.Value = value;
}
/// <summary>
/// Maximum martingale doubling level before resetting.
/// </summary>
public int MaxLevel
{
get => _maxLevel.Value;
set => _maxLevel.Value = value;
}
/// <summary>
/// Candle type used to monitor the price.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of <see cref="MartinMartingaleStrategy"/>.
/// </summary>
public MartinMartingaleStrategy()
{
_stepPoints = Param(nameof(StepPoints), 10)
.SetGreaterThanZero()
.SetDisplay("Step (points)", "Distance multiplier for reversals", "General")
;
_entryOffsetPoints = Param(nameof(EntryOffsetPoints), 10)
.SetGreaterThanZero()
.SetDisplay("Entry Offset (points)", "Offset for initial breakout entry", "General")
;
_profitTarget = Param(nameof(ProfitTarget), 5m)
.SetGreaterThanZero()
.SetDisplay("Profit Target", "Total profit to close all positions", "Risk")
;
_maxLevel = Param(nameof(MaxLevel), 5)
.SetGreaterThanZero()
.SetDisplay("Max Level", "Maximum martingale levels", "Risk")
;
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Candles for price monitoring", "Data");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
ResetCycle();
_isClosing = false;
_initialPrice = null;
_stepSize = 0;
_entryOffset = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
UpdateStepSettings();
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
UpdateStepSettings();
if (_stepSize <= 0m || Volume <= 0m)
return;
var price = candle.ClosePrice;
// If closing, flatten and wait
if (_isClosing)
{
if (Position == 0)
{
_isClosing = false;
ResetCycle();
}
return;
}
// If flat after a cycle, reset
if (Position == 0 && _martingaleLevel > 0)
{
ResetCycle();
}
// Check profit target
if (ProfitTarget > 0m && PnL >= ProfitTarget && Position != 0)
{
_isClosing = true;
if (Position > 0)
SellMarket();
else if (Position < 0)
BuyMarket();
return;
}
// Max level reached -> close and reset
if (_martingaleLevel >= MaxLevel && Position != 0)
{
_isClosing = true;
if (Position > 0)
SellMarket();
else if (Position < 0)
BuyMarket();
return;
}
// Initial entry: wait for breakout from first candle
if (_martingaleLevel == 0 && Position == 0)
{
if (!_initialPrice.HasValue)
{
_initialPrice = price;
return;
}
if (_entryOffset <= 0m)
return;
if (price >= _initialPrice.Value + _entryOffset)
{
BuyMarket();
_lastTradePrice = price;
_lastTradeVolume = Volume;
_lastTradeSide = Sides.Buy;
_martingaleLevel = 1;
_initialPrice = null;
}
else if (price <= _initialPrice.Value - _entryOffset)
{
SellMarket();
_lastTradePrice = price;
_lastTradeVolume = Volume;
_lastTradeSide = Sides.Sell;
_martingaleLevel = 1;
_initialPrice = null;
}
return;
}
if (_lastTradeSide is null || _martingaleLevel == 0)
return;
var threshold = _stepSize;
if (_lastTradeSide == Sides.Buy)
{
if (price <= _lastTradePrice - threshold)
{
var nextVolume = _lastTradeVolume * 2m;
var totalVolume = nextVolume + Math.Abs(Position);
SellMarket();
_lastTradePrice = price;
_lastTradeVolume = nextVolume;
_lastTradeSide = Sides.Sell;
_martingaleLevel++;
}
}
else
{
if (price >= _lastTradePrice + threshold)
{
var nextVolume = _lastTradeVolume * 2m;
var totalVolume = nextVolume + Math.Abs(Position);
BuyMarket();
_lastTradePrice = price;
_lastTradeVolume = nextVolume;
_lastTradeSide = Sides.Buy;
_martingaleLevel++;
}
}
}
private void UpdateStepSettings()
{
var priceStep = Security?.PriceStep ?? 0m;
if (priceStep <= 0m)
{
priceStep = 1m;
}
_stepSize = StepPoints * priceStep;
_entryOffset = EntryOffsetPoints * priceStep;
}
private void ResetCycle()
{
_martingaleLevel = 0;
_lastTradePrice = 0m;
_lastTradeVolume = 0m;
_lastTradeSide = null;
}
}
import clr
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 martin_martingale_strategy(Strategy):
"""Martingale grid that alternates long/short entries while doubling volume."""
def __init__(self):
super(martin_martingale_strategy, self).__init__()
self._step_points = self.Param("StepPoints", 10) \
.SetGreaterThanZero() \
.SetDisplay("Step (points)", "Distance multiplier for reversals", "General")
self._entry_offset_points = self.Param("EntryOffsetPoints", 10) \
.SetGreaterThanZero() \
.SetDisplay("Entry Offset (points)", "Offset for initial breakout entry", "General")
self._profit_target = self.Param("ProfitTarget", 5.0) \
.SetGreaterThanZero() \
.SetDisplay("Profit Target", "Total profit to close all positions", "Risk")
self._max_level = self.Param("MaxLevel", 5) \
.SetGreaterThanZero() \
.SetDisplay("Max Level", "Maximum martingale levels", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Candles for price monitoring", "Data")
self._step_size = 0.0
self._entry_offset = 0.0
self._last_trade_price = 0.0
self._last_trade_volume = 0.0
self._martingale_level = 0
self._last_trade_side = 0 # 0=none, 1=buy, -1=sell
self._is_closing = False
self._initial_price = None
@property
def StepPoints(self):
return int(self._step_points.Value)
@property
def EntryOffsetPoints(self):
return int(self._entry_offset_points.Value)
@property
def ProfitTarget(self):
return self._profit_target.Value
@property
def MaxLevel(self):
return int(self._max_level.Value)
@property
def CandleType(self):
return self._candle_type.Value
def _update_step_settings(self):
sec = self.Security
step = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None and float(sec.PriceStep) > 0 else 1.0
self._step_size = self.StepPoints * step
self._entry_offset = self.EntryOffsetPoints * step
def _reset_cycle(self):
self._martingale_level = 0
self._last_trade_price = 0.0
self._last_trade_volume = 0.0
self._last_trade_side = 0
def OnStarted2(self, time):
super(martin_martingale_strategy, self).OnStarted2(time)
self._update_step_settings()
self._reset_cycle()
self._is_closing = False
self._initial_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
self._update_step_settings()
if self._step_size <= 0 or float(self.Volume) <= 0:
return
price = float(candle.ClosePrice)
# If closing, flatten and wait
if self._is_closing:
if self.Position == 0:
self._is_closing = False
self._reset_cycle()
return
# If flat after a cycle, reset
if self.Position == 0 and self._martingale_level > 0:
self._reset_cycle()
# Check profit target
if float(self.ProfitTarget) > 0 and float(self.PnL) >= float(self.ProfitTarget) and self.Position != 0:
self._is_closing = True
if self.Position > 0:
self.SellMarket()
elif self.Position < 0:
self.BuyMarket()
return
# Max level reached
if self._martingale_level >= self.MaxLevel and self.Position != 0:
self._is_closing = True
if self.Position > 0:
self.SellMarket()
elif self.Position < 0:
self.BuyMarket()
return
# Initial entry: wait for breakout from first candle
if self._martingale_level == 0 and self.Position == 0:
if self._initial_price is None:
self._initial_price = price
return
if self._entry_offset <= 0:
return
if price >= self._initial_price + self._entry_offset:
self.BuyMarket()
self._last_trade_price = price
self._last_trade_volume = float(self.Volume)
self._last_trade_side = 1
self._martingale_level = 1
self._initial_price = None
elif price <= self._initial_price - self._entry_offset:
self.SellMarket()
self._last_trade_price = price
self._last_trade_volume = float(self.Volume)
self._last_trade_side = -1
self._martingale_level = 1
self._initial_price = None
return
if self._last_trade_side == 0 or self._martingale_level == 0:
return
threshold = self._step_size
if self._last_trade_side == 1:
if price <= self._last_trade_price - threshold:
self.SellMarket()
self._last_trade_price = price
self._last_trade_volume *= 2.0
self._last_trade_side = -1
self._martingale_level += 1
else:
if price >= self._last_trade_price + threshold:
self.BuyMarket()
self._last_trade_price = price
self._last_trade_volume *= 2.0
self._last_trade_side = 1
self._martingale_level += 1
def OnReseted(self):
super(martin_martingale_strategy, self).OnReseted()
self._reset_cycle()
self._is_closing = False
self._initial_price = None
self._step_size = 0.0
self._entry_offset = 0.0
def CreateClone(self):
return martin_martingale_strategy()