Die Carbophos Grid Strategie ist eine direkte Konvertierung des MetaTrader 5 Expert Advisors "Carbophos". Sie pflegt kontinuierlich eine symmetrische Leiter von Kauf- und Verkauf-Limit-Orders rund um die aktuellen Bid/Ask-Preise. Die Strategie überwacht den aggregierten Floating-Profit des gesamten Grids und schließt alle Positionen, sobald entweder das gewünschte Gewinnziel oder der maximal tolerierte Drawdown erreicht wird. Nachdem die Position abgeflacht wurde und keine Pending Orders mehr vorhanden sind, wird die Leiter automatisch neu aufgebaut.
Handelslogik
Wenn die Strategie startet und keine aktiven Orders oder offenen Positionen vorhanden sind, berechnet sie den Grid-Abstand in Kurseinheiten basierend auf dem konfigurierten Schritt in Pips und der Kurspräzision des Instruments. Fünf (konfigurierbar) Sell-Limit-Orders werden über dem besten Bid und die gleiche Anzahl von Buy-Limit-Orders unter dem besten Ask platziert.
Wenn eine Order gefüllt wird, wird die resultierende Position tick-by-tick mit Level1-Daten überwacht. Der Floating-PnL wird aus der Differenz zwischen dem aktuellen Exit-Preis (Bid für Long-Positionen, Ask für Short-Positionen) und dem volumengewichteten durchschnittlichen Eintrittspreis berechnet.
Sobald der Floating-Profit das konfigurierte Ziel überschreitet, oder der Floating-Verlust den Schutzschwellenwert verletzt, gibt die Strategie eine Marktorder aus, um die offene Position zu schließen, und storniert alle verbleibenden Limit-Orders. Die Statuskennung wird gelöscht, damit die Leiter bei der nächsten Preisaktualisierung neu aufgebaut wird.
Wenn alle Orders gefüllt werden, aber die Nettoposition auf null zurückkehrt (zum Beispiel weil der Markt durch das Grid umkehrt), löst das nächste Level1-Update eine neue Leiterplatzierung aus.
Parameter
Parameter
Beschreibung
ProfitTarget
Floating-Profit (Geld), der das Schließen des gesamten Grids auslöst.
MaxLoss
Floating-Verlust (Geld), der einen Notausstieg erzwingt.
StepPips
Abstand zwischen aufeinanderfolgenden Grid-Levels in Pips. Wird intern in Kurseinheiten umgerechnet unter Verwendung der Tick-Größe und der Dezimalpräzision des Symbols.
OrdersPerSide
Anzahl der Limit-Orders, die über und unter dem aktuellen Marktpreis platziert werden.
OrderVolume
Volumen für jede Grid-Order.
Alle Parameter unterstützen Optimierungsbereiche zur Vereinfachung der Experimente im StockSharp-Optimierer.
Risikomanagement und Schutz
Die Strategie verwendet den eingebauten StartProtection()-Hook und wendet harte monetäre Stop/Profit-Levels auf Strategieebene an. Die Floating-PnL-Berechnung hängt von den PriceStep- und StepPrice-Einstellungen des Instruments ab. Wenn ein Schwellenwert erreicht wird, schließt die Strategie die Position mit einer Marktorder und storniert alle aktiven Limit-Orders, bevor die interne Grid-Kennung zurückgesetzt wird.
Konvertierungshinweise
Der ursprüngliche MQL5-Expert Advisor passte Pip-Werte für Drei- und Fünf-Dezimal-Forex-Symbole an. Der StockSharp-Port repliziert dieses Verhalten, indem der PriceStep des Exchanges mit 10 multipliziert wird, wenn das Instrument drei oder fünf Dezimalstellen hat.
MetaTrader aggregiert Positionsgewinn, Provision und Swap pro Magic Number. In StockSharp wird der Floating-PnL aus dem gewichteten durchschnittlichen Eintrittspreis und dem aktuellen Bid/Ask-Preis neu berechnet, sodass explizite Provisionsbehandlung nicht erforderlich ist.
Orderplatzierung, -stornierung und Positionsmanagement werden über die High-Level-Strategy-API implementiert (BuyLimit, SellLimit, CancelActiveOrders, BuyMarket, SellMarket).
Das Grid wird ausschließlich aus Level1-Updates aktualisiert und repliziert das "OnTick"-Verhalten des Originalcodes ohne Einführung benutzerdefinierter Timer oder Sammlungen.
Verwendung
Weisen Sie der Strategieinstanz die gewünschte Security und das gewünschte Portfolio zu, bevor Sie sie starten.
Passen Sie optional die Parameter an die Volatilität des Zielinstruments und die Risikobereitschaft an.
Starten Sie die Strategie. Sie abonniert sofort Level1-Daten, baut das erste Grid auf, sobald sowohl Bid- als auch Ask-Preise verfügbar sind, und verwaltet die Positionen automatisch weiter.
Überwachen Sie das Log auf Meldungen wie "Profit target reached" oder "Maximum loss reached", um zu wissen, wann das Grid zurückgesetzt wurde.
Stellen Sie sicher, dass das ausgewählte Instrument Level1-Updates mit den besten Bid- und Ask-Preisen liefert; andernfalls wird die Leiter nicht aufgebaut.
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>
/// Grid strategy converted from the Carbophos MetaTrader 5 expert advisor.
/// Simulates symmetric grid levels and manages profit and loss on the aggregated position.
/// </summary>
public class CarbophosGridStrategy : Strategy
{
private readonly StrategyParam<decimal> _profitTarget;
private readonly StrategyParam<decimal> _maxLoss;
private readonly StrategyParam<int> _stepPips;
private readonly StrategyParam<int> _ordersPerSide;
private readonly StrategyParam<decimal> _orderVolume;
private readonly StrategyParam<DataType> _candleType;
private decimal? _entryPrice;
private decimal _gridCenterPrice;
private bool _gridPlaced;
private int _cooldownRemaining;
private readonly List<decimal> _buyLevels = new();
private readonly List<decimal> _sellLevels = new();
/// <summary>
/// Floating profit level (in absolute price * volume) that triggers closing of all positions.
/// </summary>
public decimal ProfitTarget
{
get => _profitTarget.Value;
set => _profitTarget.Value = value;
}
/// <summary>
/// Maximum allowed floating loss before the grid is closed.
/// </summary>
public decimal MaxLoss
{
get => _maxLoss.Value;
set => _maxLoss.Value = value;
}
/// <summary>
/// Distance between grid levels expressed in pips.
/// </summary>
public int StepPips
{
get => _stepPips.Value;
set => _stepPips.Value = value;
}
/// <summary>
/// Number of limit orders to place above and below the market price.
/// </summary>
public int OrdersPerSide
{
get => _ordersPerSide.Value;
set => _ordersPerSide.Value = value;
}
/// <summary>
/// Volume for each grid level order.
/// </summary>
public decimal OrderVolume
{
get => _orderVolume.Value;
set => _orderVolume.Value = value;
}
/// <summary>
/// Candle type used by the strategy.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes <see cref="CarbophosGridStrategy"/>.
/// </summary>
public CarbophosGridStrategy()
{
_profitTarget = Param(nameof(ProfitTarget), 500m)
.SetGreaterThanZero()
.SetDisplay("Profit Target", "Floating profit target in money", "Risk")
.SetOptimize(100m, 1000m, 50m);
_maxLoss = Param(nameof(MaxLoss), 100m)
.SetGreaterThanZero()
.SetDisplay("Max Loss", "Maximum floating loss before closing", "Risk")
.SetOptimize(50m, 500m, 25m);
_stepPips = Param(nameof(StepPips), 2000)
.SetGreaterThanZero()
.SetDisplay("Step (pips)", "Distance between grid levels in pips", "Grid")
.SetOptimize(10, 150, 10);
_ordersPerSide = Param(nameof(OrdersPerSide), 1)
.SetGreaterThanZero()
.SetDisplay("Orders Per Side", "Number of pending orders on each side", "Grid")
.SetOptimize(1, 10, 1);
_orderVolume = Param(nameof(OrderVolume), 1m)
.SetGreaterThanZero()
.SetDisplay("Order Volume", "Volume for each pending order", "Trading");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_entryPrice = null;
_gridCenterPrice = 0m;
_gridPlaced = false;
_cooldownRemaining = 0;
_buyLevels.Clear();
_sellLevels.Clear();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
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;
var currentPrice = candle.ClosePrice;
// Check if any grid levels were hit by this candle
CheckGridFills(candle);
// Check profit/loss on position
if (Position != 0 && _entryPrice is decimal entry)
{
var floatingPnL = (currentPrice - entry) * Position;
if (floatingPnL >= ProfitTarget)
{
CloseAll("Profit target reached.");
return;
}
if (floatingPnL <= -MaxLoss)
{
CloseAll("Maximum loss reached.");
return;
}
}
// Cooldown after closing
if (_cooldownRemaining > 0)
{
_cooldownRemaining--;
return;
}
// Place grid if none is active
if (!_gridPlaced || (Position == 0 && _buyLevels.Count == 0 && _sellLevels.Count == 0))
{
PlaceGrid(currentPrice);
}
}
private void PlaceGrid(decimal centerPrice)
{
_buyLevels.Clear();
_sellLevels.Clear();
var stepSize = GetGridStep();
if (stepSize <= 0m || centerPrice <= 0m)
return;
for (var i = 1; i <= OrdersPerSide; i++)
{
var offset = stepSize * i;
var buyPrice = centerPrice - offset;
var sellPrice = centerPrice + offset;
if (buyPrice > 0m)
_buyLevels.Add(buyPrice);
_sellLevels.Add(sellPrice);
}
_gridCenterPrice = centerPrice;
_gridPlaced = true;
}
private void CheckGridFills(ICandleMessage candle)
{
// Check buy levels (price goes down to the level)
for (var i = _buyLevels.Count - 1; i >= 0; i--)
{
if (i >= _buyLevels.Count) continue;
if (candle.LowPrice <= _buyLevels[i])
{
var level = _buyLevels[i];
BuyMarket();
UpdateEntryPrice(level, OrderVolume, true);
try { _buyLevels.RemoveAt(i); } catch { }
}
}
// Check sell levels (price goes up to the level)
for (var i = _sellLevels.Count - 1; i >= 0; i--)
{
if (i >= _sellLevels.Count) continue;
if (candle.HighPrice >= _sellLevels[i])
{
var level = _sellLevels[i];
SellMarket();
UpdateEntryPrice(level, OrderVolume, false);
try { _sellLevels.RemoveAt(i); } catch { }
}
}
}
private void UpdateEntryPrice(decimal fillPrice, decimal volume, bool isBuy)
{
if (_entryPrice is not decimal existingEntry || Position == 0)
{
_entryPrice = fillPrice;
return;
}
// Weighted average entry price calculation
var existingPos = Position;
var newPos = isBuy ? existingPos + volume : existingPos - volume;
if (newPos == 0)
{
_entryPrice = null;
return;
}
// Only update if adding to position in same direction
if ((isBuy && existingPos > 0) || (!isBuy && existingPos < 0))
{
var totalVolume = Math.Abs(existingPos) + volume;
_entryPrice = (existingEntry * Math.Abs(existingPos) + fillPrice * volume) / totalVolume;
}
else
{
// Reducing position - keep same entry price
if (Math.Abs(newPos) > 0)
_entryPrice = existingEntry;
else
_entryPrice = null;
}
}
private void CloseAll(string reason)
{
if (Position > 0)
SellMarket();
else if (Position < 0)
BuyMarket();
_buyLevels.Clear();
_sellLevels.Clear();
_gridPlaced = false;
_entryPrice = null;
_cooldownRemaining = 10;
LogInfo(reason);
}
private decimal GetGridStep()
{
var security = Security;
var priceStep = security?.PriceStep ?? 0m;
if (priceStep <= 0m)
priceStep = 0.01m;
var decimals = security?.Decimals ?? 2;
var multiplier = (decimals == 3 || decimals == 5) ? 10m : 1m;
return StepPips * priceStep * multiplier;
}
}
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 carbophos_grid_strategy(Strategy):
def __init__(self):
super(carbophos_grid_strategy, self).__init__()
self._profit_target = self.Param("ProfitTarget", 500.0)
self._max_loss = self.Param("MaxLoss", 100.0)
self._step_pips = self.Param("StepPips", 2000)
self._orders_per_side = self.Param("OrdersPerSide", 1)
self._order_volume = self.Param("OrderVolume", 1.0)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4)))
self._entry_price = None
self._grid_center_price = 0.0
self._grid_placed = False
self._cooldown_remaining = 0
self._buy_levels = []
self._sell_levels = []
@property
def ProfitTarget(self):
return self._profit_target.Value
@property
def MaxLoss(self):
return self._max_loss.Value
@property
def StepPips(self):
return self._step_pips.Value
@property
def OrdersPerSide(self):
return self._orders_per_side.Value
@property
def OrderVolume(self):
return self._order_volume.Value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def OnStarted2(self, time):
super(carbophos_grid_strategy, self).OnStarted2(time)
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
current_price = float(candle.ClosePrice)
self._check_grid_fills(candle)
if self.Position != 0 and self._entry_price is not None:
floating_pnl = (current_price - self._entry_price) * float(self.Position)
if floating_pnl >= self.ProfitTarget:
self._close_all("Profit target reached.")
return
if floating_pnl <= -self.MaxLoss:
self._close_all("Maximum loss reached.")
return
if self._cooldown_remaining > 0:
self._cooldown_remaining -= 1
return
if not self._grid_placed or (self.Position == 0 and len(self._buy_levels) == 0 and len(self._sell_levels) == 0):
self._place_grid(current_price)
def _place_grid(self, center_price):
self._buy_levels = []
self._sell_levels = []
step_size = self._get_grid_step()
if step_size <= 0 or center_price <= 0:
return
for i in range(1, self.OrdersPerSide + 1):
offset = step_size * i
buy_price = center_price - offset
sell_price = center_price + offset
if buy_price > 0:
self._buy_levels.append(buy_price)
self._sell_levels.append(sell_price)
self._grid_center_price = center_price
self._grid_placed = True
def _check_grid_fills(self, candle):
low = float(candle.LowPrice)
high = float(candle.HighPrice)
i = len(self._buy_levels) - 1
while i >= 0 and i < len(self._buy_levels):
if low <= self._buy_levels[i]:
level = self._buy_levels[i]
self.BuyMarket()
self._update_entry_price(level, self.OrderVolume, True)
self._buy_levels.pop(i)
i -= 1
i = len(self._sell_levels) - 1
while i >= 0 and i < len(self._sell_levels):
if high >= self._sell_levels[i]:
level = self._sell_levels[i]
self.SellMarket()
self._update_entry_price(level, self.OrderVolume, False)
self._sell_levels.pop(i)
i -= 1
def _update_entry_price(self, fill_price, volume, is_buy):
if self._entry_price is None or self.Position == 0:
self._entry_price = fill_price
return
existing_entry = self._entry_price
existing_pos = float(self.Position)
new_pos = existing_pos + volume if is_buy else existing_pos - volume
if new_pos == 0:
self._entry_price = None
return
if (is_buy and existing_pos > 0) or (not is_buy and existing_pos < 0):
total_volume = abs(existing_pos) + volume
self._entry_price = (existing_entry * abs(existing_pos) + fill_price * volume) / total_volume
else:
if abs(new_pos) > 0:
self._entry_price = existing_entry
else:
self._entry_price = None
def _close_all(self, reason):
if self.Position > 0:
self.SellMarket()
elif self.Position < 0:
self.BuyMarket()
self._buy_levels = []
self._sell_levels = []
self._grid_placed = False
self._entry_price = None
self._cooldown_remaining = 10
self.LogInfo(reason)
def _get_grid_step(self):
sec = self.Security
price_step = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None else 0.0
if price_step <= 0:
price_step = 0.01
decimals = sec.Decimals if sec is not None and sec.Decimals is not None else 2
multiplier = 10.0 if (decimals == 3 or decimals == 5) else 1.0
return self.StepPips * price_step * multiplier
def OnReseted(self):
super(carbophos_grid_strategy, self).OnReseted()
self._entry_price = None
self._grid_center_price = 0.0
self._grid_placed = False
self._cooldown_remaining = 0
self._buy_levels = []
self._sell_levels = []
def CreateClone(self):
return carbophos_grid_strategy()