Carbophos 网格策略
概述
Carbophos 网格策略是对 MetaTrader 5 专家顾问“Carbophos”的直接移植。它会在当前买入/卖出价附近持续维护对称的限价单梯队,并持续监控整个网格的浮动盈亏。当达到既定盈利目标或者浮亏超过允许阈值时,策略会立即关闭所有头寸并撤销挂单;当市场重新回到空仓状态并且没有未完成订单时,网格会被重新建立。
交易逻辑
- 策略启动且当前没有持仓或挂单时,根据用户配置的“步长(以点为单位)”以及标的的价格精度计算出具体的价格间距,然后在最优买价上方放置若干(可配置)卖出限价单,并在最优卖价下方放置同样数量的买入限价单。
- 任意挂单成交后,策略会通过 Level1 行情逐笔跟踪仓位。浮动盈亏根据当前平仓价(多头使用买一价,空头使用卖一价)与加权平均持仓价格的差值计算得出。
- 当浮动盈利超过目标值,或浮动亏损突破保护阈值时,策略会发送市价单平掉剩余仓位,并撤销所有仍在排队的限价单,然后清空内部标记,以便在下一次价格更新时重新搭建网格。
- 如果所有挂单都成交但净持仓重新回到零(例如市场穿越整个网格来回震荡),下一笔 Level1 行情会触发新的网格布置。
参数说明
| 参数 | 说明 |
|---|---|
ProfitTarget |
触发整体平仓的浮动盈利金额。 |
MaxLoss |
触发紧急止损的最大浮动亏损金额。 |
StepPips |
相邻网格层之间的距离,单位为点。策略会结合交易品种的最小跳动单位自动换算成价格距离。 |
OrdersPerSide |
在当前价格上方和下方各自布置的限价单数量。 |
OrderVolume |
每一张网格挂单的下单数量。 |
所有参数都预设了优化区间,便于在 StockSharp 优化器中进行批量测试。
风险控制与保护
策略调用一次 StartProtection() 并在策略层面设置硬性的资金止盈/止损。当任一阈值触发时,会使用市价单关闭现有仓位并调用 CancelActiveOrders() 撤销所有挂单。浮动盈亏通过 PriceStep 与 StepPrice 计算,因此需要保证所选标的在连接端已正确配置这些交易参数。
转换说明
- 原始 MQL5 版本会针对 3 位或 5 位小数的外汇品种调整点值。StockSharp 版本检测标的的
Decimals字段,在为 3 或 5 时自动将PriceStep乘以 10,从而复现该行为。 - MQL5 会按魔术号统计仓位盈利、手续费与隔夜利息。StockSharp 版本直接通过当前买卖价与平均持仓价计算浮动盈亏,因此无需显式处理手续费。
- 订单的提交、撤销与头寸管理全部使用高层 API(
BuyLimit、SellLimit、BuyMarket、SellMarket、CancelActiveOrders),符合仓库的实现规范。 - 策略完全依赖 Level1 行情驱动,等价于原策略的
OnTick行为,未引入额外的计时器或自定义集合。
使用方法
- 在启动策略前为其实例指定
Security(交易标的)和Portfolio(账户)。 - 根据标的的波动性与风险偏好调整上述参数。
- 启动策略。策略会立即订阅 Level1 行情,在同时收到买一和卖一价格后搭建初始网格,并自动管理仓位。
- 关注日志中的提示,例如“Profit target reached”或“Maximum loss reached”,以了解网格何时被重置。
请确保所选交易品种能够提供实时的买卖价 Level1 行情,否则策略无法计算网格位置。
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()