Стратегия Carbophos Grid
Обзор
Стратегия Carbophos Grid — это прямая конверсия советника MetaTrader 5 «Carbophos». Она постоянно поддерживает симметричную лестницу отложенных ордеров вокруг текущих цен bid/ask, отслеживает совокупную плавающую прибыль по всей сетке и закрывает позиции при достижении цели по прибыли либо при превышении допустимого убытка. После полной фиксации позиции и отмены оставшихся заявок сетка автоматически строится заново.
Логика торговли
- При старте стратегии, когда нет открытых позиций и активных ордеров, рассчитывается расстояние между уровнями сетки на основе шага в пунктах и точности котировки инструмента. Затем над лучшей ценой покупки размещается заданное количество (по умолчанию пять) заявок SellLimit, а под лучшей ценой продажи — такое же количество заявок BuyLimit.
- При исполнении любого ордера стратегия начинает контролировать позицию по каждой новой котировке Level1. Плавающий PnL вычисляется как разница между текущей ценой закрытия (для лонга — лучшая цена bid, для шорта — лучшая цена ask) и средневзвешенной ценой входа.
- Если плавающая прибыль превышает установленную цель или плавающий убыток достигает предельного значения, стратегия отправляет рыночный ордер для закрытия позиции и отменяет все оставшиеся лимитные заявки. После этого внутренний флаг сбрасывается, и при следующем обновлении цены сетка строится заново.
- Если все ордера были исполнены, но суммарная позиция вернулась к нулю (например, цена прошла сетку в обе стороны), следующее обновление Level1 приведёт к постановке новой сетки.
Параметры
| Параметр |
Описание |
ProfitTarget |
Уровень плавающей прибыли (в деньгах), при котором закрывается вся сетка. |
MaxLoss |
Максимально допустимый плавающий убыток (в деньгах), запускающий аварийное закрытие. |
StepPips |
Расстояние между соседними уровнями сетки в пунктах. Внутри стратегии переводится в цену с учётом PriceStep инструмента. |
OrdersPerSide |
Количество отложенных ордеров над и под текущей ценой. |
OrderVolume |
Объём каждой лимитной заявки сетки. |
Для всех параметров заданы диапазоны оптимизации, что облегчает исследование стратегии в оптимизаторе StockSharp.
Управление рисками и защита
Стратегия однократно вызывает StartProtection() и дополнительно контролирует жёсткие денежные уровни прибыли/убытка. Расчёт плавающего результата опирается на значения PriceStep и StepPrice. При срабатывании одного из порогов выполняется рыночное закрытие позиции и вызывается CancelActiveOrders() для отмены всех активных ордеров, после чего сетка может быть построена заново.
Особенности конверсии
- В оригинальном MQL5 для инструментов с тремя или пятью знаками после запятой точка пересчитывалась отдельно. Порт StockSharp выполняет ту же корректировку, умножая
PriceStep на 10, когда Security.Decimals равен 3 или 5.
- MetaTrader суммирует прибыль, комиссию и своп по «магическому» номеру. В версии StockSharp плавающий PnL пересчитывается по текущим ценам bid/ask и средневзвешенной цене входа, поэтому отдельная обработка комиссий не требуется.
- Управление ордерами реализовано через высокоуровневые методы
BuyLimit, SellLimit, BuyMarket, SellMarket и CancelActiveOrders, что соответствует требованиям репозитория.
- Обновление состояния происходит только по событиям Level1, что повторяет поведение
OnTick в MetaTrader и не требует дополнительных таймеров или самописных структур данных.
Использование
- Перед запуском стратегии укажите нужные
Security и Portfolio.
- При необходимости скорректируйте параметры под характеристики инструмента и допустимый риск.
- Запустите стратегию: она подпишется на Level1, построит первую сетку после получения лучшего bid и ask и дальше будет управлять позициями автоматически.
- Следите за сообщениями в журнале (например, «Profit target reached» или «Maximum loss reached»), чтобы понимать, когда сетка была закрыта и построена заново.
Убедитесь, что выбранный инструмент предоставляет поток котировок с лучшим bid/ask; без этих данных сетка не будет построена.
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()