OCO Schwebende Aufträge-Strategie
Überblick
Die OCO Schwebende Aufträge-Strategie repliziert das Verhalten des MetaTrader4 Expert Advisors OCO_EA.mq4 innerhalb der StockSharp High-Level-API. Der Algorithmus ermöglicht es einem Trader, bis zu vier unabhängige Preisauslöser zu aktivieren (Buy Limit, Buy Stop, Sell Limit, Sell Stop). Wann immer das live beste Geld oder Brief den konfigurierten Preisniveau berührt, sendet die Strategie sofort eine Marktorder und storniert optional alle anderen ausstehenden Auslöser auf klassische "One-Cancels-the-Others" (OCO)-Weise.
Die Strategie basiert ausschließlich auf Level-1-Marktdaten – keine historischen Indikatoren sind erforderlich. Sie ist für diskretionäre oder halbautomatisierte Handelsabläufe gedacht, bei denen Trader manuell Preisniveaus definieren und möchten, dass die Plattform ausführt, sobald das Niveau erreicht wird, während sie auch schützende Ausstiegsorders anhängt.
Handelslogik
- Der Trader setzt eine beliebige Kombination der vier Auslöserpreise und schaltet den Parameter Armed auf
true.
- Die Strategie abonniert Level-1-Updates und hält das neueste beste Geld und Brief im Speicher.
- Bei jedem Update werden die gespeicherten Preise mit den konfigurierten Schwellen verglichen:
- Wenn der beste Brief kleiner oder gleich dem Buy Limit-Preis ist, wird eine Markt-Kauforder mit dem konfigurierten Volumen gesendet.
- Wenn der beste Brief größer oder gleich dem Buy Stop-Preis ist, wird eine Markt-Kauforder gesendet.
- Wenn das beste Geld größer oder gleich dem Sell Limit-Preis ist, wird eine Markt-Verkaufsorder gesendet.
- Wenn das beste Geld kleiner oder gleich dem Sell Stop-Preis ist, wird eine Markt-Verkaufsorder gesendet.
- Nach jedem ausgeführten Auslöser wird das entsprechende Niveau gelöscht (auf null zurückgesetzt). Wenn Use OCO Link aktiviert ist, werden alle anderen Niveaus sofort gelöscht, was das ursprüngliche MT4-Verhalten widerspiegelt. Wenn der OCO-Link deaktiviert ist, bleiben andere Niveaus aktiv, bis sie auslösen oder manuell gelöscht werden.
- Wenn alle Auslöserpreise null sind, deaktiviert sich die Strategie automatisch, indem sie Armed auf
false zurückschaltet.
Alle Ausführungen werden mit BuyMarket- und SellMarket-Aufrufen durchgeführt, um sofortige Füllungen zu gewährleisten, die das in der StockSharp-Umgebung konfigurierte Börsenrouting respektieren. Informative Protokolleinträge werden für jeden Auslöser erstellt, um die Überwachung zu vereinfachen.
Parameter
- Order volume – Volumen, das mit jeder Marktorder gesendet wird. Der Wert muss positiv sein.
- Buy limit price – Ask-Preisschwelle, die einen limitartigen Long-Einstieg aktiviert. Auf
0 setzen zum Deaktivieren.
- Buy stop price – Ask-Preisschwelle, die einen stopartigen Long-Einstieg aktiviert. Auf
0 setzen zum Deaktivieren.
- Sell limit price – Bid-Preisschwelle, die einen limitartigen Short-Einstieg aktiviert. Auf
0 setzen zum Deaktivieren.
- Sell stop price – Bid-Preisschwelle, die einen stopartigen Short-Einstieg aktiviert. Auf
0 setzen zum Deaktivieren.
- Stop loss (pips) – Abstand in Instrument-Punkten für den Schutz-Stop. In Preis umgerechnet durch Multiplikation mit
Security.PriceStep (Fallback 1, wenn das Instrument keine Tick-Größe meldet).
- Take profit (pips) – Abstand in Instrument-Punkten für das Gewinnziel. Dieselbe Konvertierungslogik wie für den Stop Loss wird verwendet.
- Use OCO link – wenn
true, löscht die erste gefüllte Order die verbleibenden Preisniveaus und deaktiviert die Strategie. Wenn false, bleiben verbleibende Niveaus aktiv, bis sie einzeln auslösen.
- Armed – Sicherheitsschalter, der die Handelslogik aktiviert oder deaktiviert. Die Strategie setzt ihn automatisch auf
false zurück, wenn keine aktiven Auslöser-Niveaus verbleiben.
Risikomanagement
StartProtection wird während OnStarted aktiviert und hängt absolute Preis-Stop-Loss- und Take-Profit-Offsets an jede offene Position an. Die Offsets werden aus den Parametern Stop loss (pips) und Take profit (pips) unter Verwendung der Instrument-Tick-Größe abgeleitet. Schutzorders werden immer als Marktorders gesendet, um die Ausstiegsausführung auch dann zu garantieren, wenn das zugrunde liegende Instrument illiquide ist.
Da die Strategie rein ereignisgesteuert ist, unterhält sie keine ausstehenden Limit-Orders an der Börse; sie reagiert auf Marktdaten und sendet Marktorders, genau wie die ursprüngliche MQL-Version, die sofortige Orders ausgab und sie dann modifizierte, um Stop-Loss- und Take-Profit-Abstände anzuwenden.
Verwendungstipps
- Das Wertpapier, Portfolio und die Verbindung innerhalb von StockSharp wie üblich konfigurieren.
- Order volume auf die gewünschte Lot-Größe setzen.
- Einen beliebigen Teilsatz der Auslöserpreise eingeben und Armed auf
true umschalten. Auf 0 belassene Werte werden ignoriert.
- Optional Use OCO link deaktivieren, um verbleibende Auslöser nach der ersten Füllung aktiv zu halten.
- Das Protokoll auf Nachrichten überwachen, die jeden Auslöser und den automatischen Rücksetzzustand bestätigen.
Denken Sie daran, dass die Strategie den vom Broker bereitgestellten Preisschritt verwendet. Wenn das Handelsinstrument in Bruchpips quotiert oder unkonventionelle Tick-Größen verwendet, passen Sie die pip-basierten Abstände entsprechend an, bevor Sie die Strategie aktivieren.
Unterschiede zum originalen MQL-Skript
- Die Strategie verwendet den StockSharp
StartProtection-Helfer anstatt Orders manuell zu modifizieren, um Stop-Loss- und Take-Profit-Niveaus anzuwenden.
- Level-1-Datenabonnements werden über High-Level-Bindings statt manuellem Polling der Werte
Bid, Ask, High und Low verarbeitet.
- Parameter werden über
StrategyParam<T> exponiert, damit sie direkt in der StockSharp-UI angepasst und optimiert werden können.
- Protokollierung ersetzt die MT4
Comment- und PlaySound-Benachrichtigungen und bietet Ausführungstransparenz innerhalb der StockSharp-Protokolle.
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Manually armed OCO price triggers driven by Level1 bid/ask updates.
/// </summary>
public class OcoPendingOrdersStrategy : Strategy
{
private readonly StrategyParam<decimal> _orderVolume;
private readonly StrategyParam<decimal> _buyLimitPrice;
private readonly StrategyParam<decimal> _buyStopPrice;
private readonly StrategyParam<decimal> _sellLimitPrice;
private readonly StrategyParam<decimal> _sellStopPrice;
private readonly StrategyParam<int> _stopLossPips;
private readonly StrategyParam<int> _takeProfitPips;
private readonly StrategyParam<bool> _useOcoLink;
private readonly StrategyParam<bool> _armed;
private decimal[] _initialLevels;
private bool _initialArmed;
private decimal? _bestBid;
private decimal? _bestAsk;
public decimal OrderVolume { get => _orderVolume.Value; set => _orderVolume.Value = value; }
public decimal BuyLimitPrice { get => _buyLimitPrice.Value; set => _buyLimitPrice.Value = value; }
public decimal BuyStopPrice { get => _buyStopPrice.Value; set => _buyStopPrice.Value = value; }
public decimal SellLimitPrice { get => _sellLimitPrice.Value; set => _sellLimitPrice.Value = value; }
public decimal SellStopPrice { get => _sellStopPrice.Value; set => _sellStopPrice.Value = value; }
public int StopLossPips { get => _stopLossPips.Value; set => _stopLossPips.Value = value; }
public int TakeProfitPips { get => _takeProfitPips.Value; set => _takeProfitPips.Value = value; }
public bool UseOcoLink { get => _useOcoLink.Value; set => _useOcoLink.Value = value; }
public bool Armed { get => _armed.Value; set => _armed.Value = value; }
public OcoPendingOrdersStrategy()
{
_orderVolume = Param(nameof(OrderVolume), 1m)
.SetGreaterThanZero()
.SetDisplay("Order volume", "Volume sent with each market order", "Trading");
_buyLimitPrice = Param(nameof(BuyLimitPrice), 0m)
.SetNotNegative()
.SetDisplay("Buy limit price", "Ask price threshold that activates a limit-style long entry, 0 disables", "Levels");
_buyStopPrice = Param(nameof(BuyStopPrice), 0m)
.SetNotNegative()
.SetDisplay("Buy stop price", "Ask price threshold that activates a stop-style long entry, 0 disables", "Levels");
_sellLimitPrice = Param(nameof(SellLimitPrice), 0m)
.SetNotNegative()
.SetDisplay("Sell limit price", "Bid price threshold that activates a limit-style short entry, 0 disables", "Levels");
_sellStopPrice = Param(nameof(SellStopPrice), 0m)
.SetNotNegative()
.SetDisplay("Sell stop price", "Bid price threshold that activates a stop-style short entry, 0 disables", "Levels");
_stopLossPips = Param(nameof(StopLossPips), 0)
.SetNotNegative()
.SetDisplay("Stop loss (pips)", "Protective stop distance in instrument points, multiplied by the price step", "Risk");
_takeProfitPips = Param(nameof(TakeProfitPips), 0)
.SetNotNegative()
.SetDisplay("Take profit (pips)", "Profit target distance in instrument points, multiplied by the price step", "Risk");
_useOcoLink = Param(nameof(UseOcoLink), true)
.SetDisplay("Use OCO link", "The first filled order clears the remaining price levels and disarms the strategy", "Control");
_armed = Param(nameof(Armed), false)
.SetDisplay("Armed", "Safety switch for the triggers, reset to false when no active level remains", "Control");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, DataType.Level1)];
protected override void OnReseted()
{
base.OnReseted();
if (_initialLevels != null)
{
BuyLimitPrice = _initialLevels[0];
BuyStopPrice = _initialLevels[1];
SellLimitPrice = _initialLevels[2];
SellStopPrice = _initialLevels[3];
Armed = _initialArmed;
}
_initialLevels = null;
_initialArmed = false;
_bestBid = null;
_bestAsk = null;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_initialLevels = [BuyLimitPrice, BuyStopPrice, SellLimitPrice, SellStopPrice];
_initialArmed = Armed;
var step = Security?.PriceStep ?? 1m;
if (step <= 0m)
step = 1m;
var take = TakeProfitPips > 0 ? new Unit(TakeProfitPips * step, UnitTypes.Absolute) : null;
var stop = StopLossPips > 0 ? new Unit(StopLossPips * step, UnitTypes.Absolute) : null;
if (take is not null || stop is not null)
StartProtection(takeProfit: take, stopLoss: stop, useMarketOrders: true);
// A Level1 binding only sees updates carrying its build field, so bid-only and ask-only updates need one binding each.
foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
{
var quotes = new Subscription(DataType.Level1, Security);
quotes.MarketData.BuildField = field;
SubscribeLevel1(quotes).Bind(ProcessLevel1).Start();
}
}
private void ProcessLevel1(Level1ChangeMessage message)
{
if (message.TryGetDecimal(Level1Fields.BestBidPrice) is decimal bid)
_bestBid = bid;
if (message.TryGetDecimal(Level1Fields.BestAskPrice) is decimal ask)
_bestAsk = ask;
if (!Armed)
return;
if (_bestAsk is decimal bestAsk)
{
if (BuyLimitPrice > 0m && bestAsk <= BuyLimitPrice)
{
var level = BuyLimitPrice;
BuyLimitPrice = 0m;
Execute(Sides.Buy, $"Buy limit {level} hit by ask {bestAsk}");
if (!Armed)
return;
}
if (BuyStopPrice > 0m && bestAsk >= BuyStopPrice)
{
var level = BuyStopPrice;
BuyStopPrice = 0m;
Execute(Sides.Buy, $"Buy stop {level} hit by ask {bestAsk}");
if (!Armed)
return;
}
}
if (_bestBid is decimal bestBid)
{
if (SellLimitPrice > 0m && bestBid >= SellLimitPrice)
{
var level = SellLimitPrice;
SellLimitPrice = 0m;
Execute(Sides.Sell, $"Sell limit {level} hit by bid {bestBid}");
if (!Armed)
return;
}
if (SellStopPrice > 0m && bestBid <= SellStopPrice)
{
var level = SellStopPrice;
SellStopPrice = 0m;
Execute(Sides.Sell, $"Sell stop {level} hit by bid {bestBid}");
if (!Armed)
return;
}
}
DisarmIfEmpty();
}
private void Execute(Sides side, string trigger)
{
LogInfo($"{trigger}: {side} {OrderVolume} at market.");
if (side == Sides.Buy)
BuyMarket(OrderVolume);
else
SellMarket(OrderVolume);
if (!UseOcoLink)
return;
ClearLevels();
Armed = false;
LogInfo("OCO link cleared all trigger levels; strategy disarmed.");
}
private void ClearLevels()
{
BuyLimitPrice = 0m;
BuyStopPrice = 0m;
SellLimitPrice = 0m;
SellStopPrice = 0m;
}
private void DisarmIfEmpty()
{
if (BuyLimitPrice > 0m || BuyStopPrice > 0m || SellLimitPrice > 0m || SellStopPrice > 0m)
return;
Armed = false;
LogInfo("No trigger levels remain; strategy disarmed.");
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Strategies")
clr.AddReference("StockSharp.BusinessEntities")
from System import Decimal
from StockSharp.Messages import DataType, Level1Fields, Sides, Unit, UnitTypes
from StockSharp.Algo.Strategies import Strategy
from StockSharp.BusinessEntities import Subscription
class oco_pending_orders_strategy(Strategy):
def __init__(self):
super(oco_pending_orders_strategy, self).__init__()
self._order_volume = self.Param("OrderVolume", 1.0) \
.SetGreaterThanZero() \
.SetDisplay("Order volume", "Volume sent with each market order", "Trading")
self._buy_limit = self.Param("BuyLimitPrice", 0.0) \
.SetNotNegative() \
.SetDisplay("Buy limit price", "Ask price threshold that activates a limit-style long entry, 0 disables", "Levels")
self._buy_stop = self.Param("BuyStopPrice", 0.0) \
.SetNotNegative() \
.SetDisplay("Buy stop price", "Ask price threshold that activates a stop-style long entry, 0 disables", "Levels")
self._sell_limit = self.Param("SellLimitPrice", 0.0) \
.SetNotNegative() \
.SetDisplay("Sell limit price", "Bid price threshold that activates a limit-style short entry, 0 disables", "Levels")
self._sell_stop = self.Param("SellStopPrice", 0.0) \
.SetNotNegative() \
.SetDisplay("Sell stop price", "Bid price threshold that activates a stop-style short entry, 0 disables", "Levels")
self._stop_loss = self.Param("StopLossPips", 0) \
.SetNotNegative() \
.SetDisplay("Stop loss (pips)", "Protective stop distance in instrument points, multiplied by the price step", "Risk")
self._take_profit = self.Param("TakeProfitPips", 0) \
.SetNotNegative() \
.SetDisplay("Take profit (pips)", "Profit target distance in instrument points, multiplied by the price step", "Risk")
self._use_oco = self.Param("UseOcoLink", True) \
.SetDisplay("Use OCO link", "The first filled order clears the remaining price levels and disarms the strategy", "Control")
self._armed = self.Param("Armed", False) \
.SetDisplay("Armed", "Safety switch for the triggers, reset to false when no active level remains", "Control")
self._initial_levels = None
self._initial_armed = False
self._best_bid = None
self._best_ask = None
def GetWorkingSecurities(self):
return [(self.Security, DataType.Level1)]
def OnStarted2(self, time):
super(oco_pending_orders_strategy, self).OnStarted2(time)
self._initial_levels = [self._buy_limit.Value, self._buy_stop.Value, self._sell_limit.Value, self._sell_stop.Value]
self._initial_armed = self._armed.Value
step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 1.0
if step <= 0:
step = 1.0
tp = int(self._take_profit.Value)
sl = int(self._stop_loss.Value)
take = Unit(tp * step, UnitTypes.Absolute) if tp > 0 else None
stop = Unit(sl * step, UnitTypes.Absolute) if sl > 0 else None
if take is not None or stop is not None:
self.StartProtection(takeProfit=take, stopLoss=stop, useMarketOrders=True)
# A Level1 binding only sees updates carrying its build field, so bid-only and ask-only updates need one binding each.
for field in (Level1Fields.BestBidPrice, Level1Fields.BestAskPrice):
quotes = Subscription(DataType.Level1, self.Security)
quotes.MarketData.BuildField = field
self.SubscribeLevel1(quotes).Bind(self._process_level1).Start()
def _process_level1(self, message):
if message.Changes.ContainsKey(Level1Fields.BestBidPrice):
self._best_bid = message.Changes[Level1Fields.BestBidPrice]
if message.Changes.ContainsKey(Level1Fields.BestAskPrice):
self._best_ask = message.Changes[Level1Fields.BestAskPrice]
if not bool(self._armed.Value):
return
if self._best_ask is not None:
best_ask = self._best_ask
level = Decimal(float(self._buy_limit.Value))
if level > 0 and best_ask <= level:
self._buy_limit.Value = 0.0
self._execute(Sides.Buy, "Buy limit {0} hit by ask {1}".format(level, best_ask))
if not bool(self._armed.Value):
return
level = Decimal(float(self._buy_stop.Value))
if level > 0 and best_ask >= level:
self._buy_stop.Value = 0.0
self._execute(Sides.Buy, "Buy stop {0} hit by ask {1}".format(level, best_ask))
if not bool(self._armed.Value):
return
if self._best_bid is not None:
best_bid = self._best_bid
level = Decimal(float(self._sell_limit.Value))
if level > 0 and best_bid >= level:
self._sell_limit.Value = 0.0
self._execute(Sides.Sell, "Sell limit {0} hit by bid {1}".format(level, best_bid))
if not bool(self._armed.Value):
return
level = Decimal(float(self._sell_stop.Value))
if level > 0 and best_bid <= level:
self._sell_stop.Value = 0.0
self._execute(Sides.Sell, "Sell stop {0} hit by bid {1}".format(level, best_bid))
if not bool(self._armed.Value):
return
self._disarm_if_empty()
def _execute(self, side, trigger):
volume = float(self._order_volume.Value)
self.LogInfo("{0}: {1} {2} at market.".format(trigger, "Buy" if side == Sides.Buy else "Sell", volume))
if side == Sides.Buy:
self.BuyMarket(volume)
else:
self.SellMarket(volume)
if not bool(self._use_oco.Value):
return
self._clear_levels()
self._armed.Value = False
self.LogInfo("OCO link cleared all trigger levels; strategy disarmed.")
def _clear_levels(self):
self._buy_limit.Value = 0.0
self._buy_stop.Value = 0.0
self._sell_limit.Value = 0.0
self._sell_stop.Value = 0.0
def _disarm_if_empty(self):
if (float(self._buy_limit.Value) > 0 or float(self._buy_stop.Value) > 0 or
float(self._sell_limit.Value) > 0 or float(self._sell_stop.Value) > 0):
return
self._armed.Value = False
self.LogInfo("No trigger levels remain; strategy disarmed.")
def OnReseted(self):
super(oco_pending_orders_strategy, self).OnReseted()
if self._initial_levels is not None:
self._buy_limit.Value, self._buy_stop.Value, self._sell_limit.Value, self._sell_stop.Value = self._initial_levels
self._armed.Value = self._initial_armed
self._initial_levels = None
self._initial_armed = False
self._best_bid = None
self._best_ask = None
def CreateClone(self):
return oco_pending_orders_strategy()