Carbophos Grid Strategy
Overview
The Carbophos Grid Strategy is a direct conversion of the "Carbophos" MetaTrader 5 expert advisor. It continuously maintains a symmetric ladder of buy and sell limit orders around the current bid/ask prices. The strategy monitors the aggregated floating profit of the entire grid and closes all exposure once either the desired profit target or the maximum tolerated drawdown is reached. After the position is flattened and no working orders remain, the ladder is rebuilt automatically.
Trading Logic
- When the strategy starts and there are no active orders or open positions, it calculates the grid spacing in price units based on the configured step in pips and the instrument's price precision. Five (configurable) sell limit orders are placed above the best bid and the same number of buy limit orders are placed below the best ask.
- If any order is filled, the resulting position is monitored tick-by-tick using Level1 data. Floating PnL is computed from the difference between the current exit price (bid for long positions, ask for short positions) and the volume-weighted average entry price.
- Once the floating profit exceeds the configured target, or the floating loss breaches the protection threshold, the strategy submits a market order to close the open position and cancels all remaining limit orders. The state flag is cleared so that the ladder will be rebuilt on the next price update.
- If all orders are filled but the net position returns to zero (for example, because the market reverses through the grid), the next Level1 update triggers a new ladder placement.
Parameters
| Parameter |
Description |
ProfitTarget |
Floating profit (money) that triggers closing the entire grid. |
MaxLoss |
Floating loss (money) that forces an emergency exit. |
StepPips |
Distance between consecutive grid levels expressed in pips. Internally converted to price units using the symbol's tick size and decimal precision. |
OrdersPerSide |
Number of limit orders placed above and below the current market price. |
OrderVolume |
Volume for every grid order. |
All parameters support optimization ranges to simplify experimentation in the StockSharp optimizer.
Risk Management and Protections
The strategy uses the built-in StartProtection() hook and applies hard monetary stop/profit levels at the strategy level. The floating PnL calculation relies on the instrument's PriceStep and StepPrice settings. When either threshold is met, the strategy closes the position with a market order and cancels every working limit order before resetting the internal grid flag.
Conversion Notes
- The original MQL5 expert advisor adjusted pip values for three- and five-decimal Forex symbols. The StockSharp port replicates this behavior by multiplying the exchange
PriceStep by 10 whenever the security exposes three or five decimals.
- MetaTrader aggregates position profit, commission, and swap per magic number. In StockSharp the floating PnL is recomputed from the weighted average entry price and the current bid/ask price, so explicit commission handling is not required.
- Order placement, cancellation, and position management are implemented via the high-level
Strategy API (BuyLimit, SellLimit, CancelActiveOrders, BuyMarket, SellMarket) as required by the project guidelines.
- The grid is refreshed exclusively from Level1 updates, replicating the "OnTick" behaviour of the original code without introducing custom timers or collections.
Usage
- Assign the desired
Security and Portfolio to the strategy instance before starting it.
- Optionally adjust the parameters to match the target instrument's volatility and risk tolerance.
- Start the strategy. It immediately subscribes to Level1 data, builds the first grid once both bid and ask prices are available, and keeps managing exposure automatically.
- Monitor the log for messages such as "Profit target reached" or "Maximum loss reached" to know when the grid has been reset.
Ensure that the selected instrument provides Level1 updates with best bid and ask prices; otherwise the ladder will not be built.
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()