Стратегия Build Your Grid
Build Your Grid — прямой перенос советника MetaTrader "BuildYourGridEA". Стратегия ведёт две независимые лестницы рыночных позиций (лонгов и шортов), добавляет новые уровни при каждом смещении цены на заданное количество пунктов и при необходимости увеличивает объёмы по заданной прогрессии. Корзина закрывается при достижении целевой прибыли, при превышении допустимой просадки в пунктах или с помощью защитных хеджирующих ордеров.
Принцип работы
- Первичное открытие. В зависимости от параметра Order Placement стратегия выставляет первый buy-, sell- или оба ордера, как только спред становится приемлемым.
- Расширение сетки. Дополнительные сделки совершаются по тренду или против него. Расстояние до следующего уровня измеряется в пунктах и может умножаться на количество уже открытых сделок либо на степень двойки.
- Прогрессия объёмов. Размер ордера следует выбранной схеме (фиксированной, геометрической или экспоненциальной) и ограничивается множителем Max Multiplier относительно первой позиции.
- Фиксация прибыли. Вся корзина закрывается, когда суммарный плавающий результат превышает целевой уровень, выраженный в пунктах или валюте счёта.
- Ограничение убытков. Если суммарная просадка по пунктам превышает порог Loss (pips), стратегия в зависимости от режима закрывает первые сделки с каждой стороны либо всю корзину.
- Хеджирование. При падении доходности ниже Hedge Threshold (%) выставляется балансирующий ордер объёмом, равным разнице между лонговым и шортовым объёмом, умноженной на Hedge Multiplier.
Параметры
| Параметр | Описание |
|---|---|
Order Placement |
Разрешённые направления открытия (оба, только покупки, только продажи). |
Grid Direction |
Расширять сетку по тренду или контртрендово. |
Grid Step (pips) |
Базовый шаг в пунктах до следующего уровня. |
Step Progression |
Способ увеличения шага: фиксированный, геометрический (× количество), экспоненциальный (× 2^(n-1)). |
Close Target |
Тип целевого ориентира (в пунктах или валюте счёта). |
Target (pips) / Target (currency) |
Порог для закрытия всей корзины в плюс. |
Loss Handling |
Действие при достижении лимита убытка (ничего, закрыть первые сделки, закрыть все). |
Loss (pips) |
Максимальный суммарный убыток в пунктах до запуска защиты. |
Use Hedge |
Включить ли автоматические хеджирующие сделки. |
Hedge Threshold (%) |
Просадка в процентах от баланса, при которой запускается хедж. |
Hedge Multiplier |
Множитель для расчёта объёма хедж-ордера. |
Auto Volume / Risk Factor |
Автоматический объём = Баланс × RiskFactor / 100000. |
Manual Volume |
Фиксированный объём при отключённом автолоте. |
Lot Progression |
Модель роста объёмов (фикс., геометрическая, экспоненциальная). |
Max Multiplier |
Ограничение максимального объёма первый лот × MaxMultiplier. |
Max Orders |
Лимит одновременно открытых сделок (0 = без ограничения). |
Max Spread |
Запрет на новые входы, пока спред превышает порог (0 = игнорировать). |
Use Completed Bar / Candle Type |
Обработка сигналов только после закрытия выбранной свечи, как в исходном советнике. |
Рекомендации по использованию
- Стратегия ориентируется на лучшие bid/ask цены, поэтому источник данных должен предоставлять точные котировки первого уровня.
- При активации хеджирования стратегия использует значение портфеля. В Designer/Testeр необходимо подключить портфель с актуальным балансом.
- Сеточные системы быстро наращивают риски. Начинайте с небольших объёмов и тестируйте конфигурацию в симуляции перед переходом на реальную торговлю.
- Включённый режим
Use Completed Barзаставляет стратегию принимать решения только после закрытия свечи, воспроизводя поведение оригинального алгоритма.
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 MetaTrader expert "BuildYourGridEA".
/// It maintains layered long and short positions, optionally increases volume geometrically
/// and supports profit/loss group exits together with hedge rebalancing.
/// </summary>
public class BuildYourGridStrategy : Strategy
{
public enum OrderPlacementModes
{
Both,
LongOnly,
ShortOnly,
}
public enum GridDirectionModes
{
WithTrend,
AgainstTrend,
}
public enum StepProgressionModes
{
Static,
Geometric,
Exponential,
}
public enum CloseTargetModes
{
Pips,
Currency,
}
public enum LossCloseModes
{
DoNothing,
CloseFirst,
CloseAll,
}
public enum LotProgressionModes
{
Static,
Geometric,
Exponential,
}
private sealed class PositionEntry
{
public PositionEntry(decimal price, decimal volume)
{
Price = price;
Volume = volume;
}
public decimal Price { get; set; }
public decimal Volume { get; set; }
}
private readonly StrategyParam<OrderPlacementModes> _orderPlacement;
private readonly StrategyParam<GridDirectionModes> _gridDirection;
private readonly StrategyParam<decimal> _pipsForNextOrder;
private readonly StrategyParam<StepProgressionModes> _stepProgression;
private readonly StrategyParam<CloseTargetModes> _closeTargetMode;
private readonly StrategyParam<decimal> _pipsCloseInProfit;
private readonly StrategyParam<decimal> _currencyCloseInProfit;
private readonly StrategyParam<LossCloseModes> _lossCloseMode;
private readonly StrategyParam<decimal> _pipsForCloseInLoss;
private readonly StrategyParam<bool> _placeHedgeOrder;
private readonly StrategyParam<decimal> _hedgeLossThreshold;
private readonly StrategyParam<decimal> _hedgeVolumeMultiplier;
private readonly StrategyParam<bool> _autoLotSize;
private readonly StrategyParam<decimal> _riskFactor;
private readonly StrategyParam<decimal> _manualLotSize;
private readonly StrategyParam<LotProgressionModes> _lotProgression;
private readonly StrategyParam<decimal> _maxMultiplierLot;
private readonly StrategyParam<int> _maxOrders;
private readonly StrategyParam<decimal> _maxSpread;
private readonly StrategyParam<DataType> _candleType;
private readonly List<PositionEntry> _longEntries = new();
private readonly List<PositionEntry> _shortEntries = new();
private decimal _currentPrice;
private decimal _pointSize;
private decimal _priceStep;
private decimal _stepPrice;
private int _totalOrders;
private int _buyOrders;
private int _sellOrders;
private decimal _totalLongVolume;
private decimal _totalShortVolume;
private decimal _buyProfit;
private decimal _sellProfit;
private decimal _buyPips;
private decimal _sellPips;
private decimal? _lastBuyPrice;
private decimal? _lastSellPrice;
private decimal _firstBuyVolume;
private decimal _firstSellVolume;
private decimal _lastBuyVolume;
private decimal _lastSellVolume;
private bool _isHedged;
private int _cooldownBars;
/// <summary>
/// Controls whether the strategy may open both directions or a single side only.
/// </summary>
public OrderPlacementModes OrderPlacement
{
get => _orderPlacement.Value;
set => _orderPlacement.Value = value;
}
/// <summary>
/// Determines if new grid layers follow the trend or fade the movement.
/// </summary>
public GridDirectionModes GridDirection
{
get => _gridDirection.Value;
set => _gridDirection.Value = value;
}
/// <summary>
/// Base distance between consecutive grid layers measured in pips.
/// </summary>
public decimal PipsForNextOrder
{
get => _pipsForNextOrder.Value;
set => _pipsForNextOrder.Value = value;
}
/// <summary>
/// Defines how the grid step evolves with each new order.
/// </summary>
public StepProgressionModes StepProgression
{
get => _stepProgression.Value;
set => _stepProgression.Value = value;
}
/// <summary>
/// Target type used for closing the basket in profit.
/// </summary>
public CloseTargetModes CloseTarget
{
get => _closeTargetMode.Value;
set => _closeTargetMode.Value = value;
}
/// <summary>
/// Profit target expressed in pips.
/// </summary>
public decimal PipsCloseInProfit
{
get => _pipsCloseInProfit.Value;
set => _pipsCloseInProfit.Value = value;
}
/// <summary>
/// Profit target expressed in account currency.
/// </summary>
public decimal CurrencyCloseInProfit
{
get => _currencyCloseInProfit.Value;
set => _currencyCloseInProfit.Value = value;
}
/// <summary>
/// Defines how the basket is closed when the floating loss limit is reached.
/// </summary>
public LossCloseModes LossMode
{
get => _lossCloseMode.Value;
set => _lossCloseMode.Value = value;
}
/// <summary>
/// Maximal allowed loss in pips before defensive actions.
/// </summary>
public decimal PipsForCloseInLoss
{
get => _pipsForCloseInLoss.Value;
set => _pipsForCloseInLoss.Value = value;
}
/// <summary>
/// Enables hedge orders when losses exceed a percentage of the balance.
/// </summary>
public bool PlaceHedgeOrder
{
get => _placeHedgeOrder.Value;
set => _placeHedgeOrder.Value = value;
}
/// <summary>
/// Loss percentage of the balance that triggers hedging.
/// </summary>
public decimal HedgeLossThreshold
{
get => _hedgeLossThreshold.Value;
set => _hedgeLossThreshold.Value = value;
}
/// <summary>
/// Multiplier applied to the imbalance volume when hedging.
/// </summary>
public decimal HedgeVolumeMultiplier
{
get => _hedgeVolumeMultiplier.Value;
set => _hedgeVolumeMultiplier.Value = value;
}
/// <summary>
/// Uses balance based sizing when enabled.
/// </summary>
public bool AutoLotSize
{
get => _autoLotSize.Value;
set => _autoLotSize.Value = value;
}
/// <summary>
/// Risk factor used for automatic volume calculation.
/// </summary>
public decimal RiskFactor
{
get => _riskFactor.Value;
set => _riskFactor.Value = value;
}
/// <summary>
/// Manual order volume when automatic sizing is disabled.
/// </summary>
public decimal ManualLotSize
{
get => _manualLotSize.Value;
set => _manualLotSize.Value = value;
}
/// <summary>
/// Controls how the lot size grows with new orders.
/// </summary>
public LotProgressionModes LotProgression
{
get => _lotProgression.Value;
set => _lotProgression.Value = value;
}
/// <summary>
/// Caps the lot size to a multiple of the first entry.
/// </summary>
public decimal MaxMultiplierLot
{
get => _maxMultiplierLot.Value;
set => _maxMultiplierLot.Value = value;
}
/// <summary>
/// Maximum amount of simultaneous orders (0 means unlimited).
/// </summary>
public int MaxOrders
{
get => _maxOrders.Value;
set => _maxOrders.Value = value;
}
/// <summary>
/// Maximum acceptable spread expressed in pips.
/// </summary>
public decimal MaxSpread
{
get => _maxSpread.Value;
set => _maxSpread.Value = value;
}
/// <summary>
/// Candle type used for price data.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="BuildYourGridStrategy"/> class.
/// </summary>
public BuildYourGridStrategy()
{
_orderPlacement = Param(nameof(OrderPlacement), OrderPlacementModes.LongOnly)
.SetDisplay("Order Placement", "Allowed entry direction", "General");
_gridDirection = Param(nameof(GridDirection), GridDirectionModes.AgainstTrend)
.SetDisplay("Grid Direction", "Whether layers follow or fade the trend", "Grid");
_pipsForNextOrder = Param(nameof(PipsForNextOrder), 500000m)
.SetDisplay("Grid Step (pips)", "Base spacing between grid levels", "Grid")
.SetGreaterThanZero();
_stepProgression = Param(nameof(StepProgression), StepProgressionModes.Static)
.SetDisplay("Step Progression", "How the distance grows with each layer", "Grid");
_closeTargetMode = Param(nameof(CloseTarget), CloseTargetModes.Pips)
.SetDisplay("Close Target", "Profit target type", "Risk");
_pipsCloseInProfit = Param(nameof(PipsCloseInProfit), 500000m)
.SetDisplay("Target (pips)", "Basket profit target in pips", "Risk")
.SetGreaterThanZero();
_currencyCloseInProfit = Param(nameof(CurrencyCloseInProfit), 10m)
.SetDisplay("Target (currency)", "Basket profit target in account currency", "Risk")
.SetGreaterThanZero();
_lossCloseMode = Param(nameof(LossMode), LossCloseModes.DoNothing)
.SetDisplay("Loss Handling", "Action when the loss threshold is hit", "Risk");
_pipsForCloseInLoss = Param(nameof(PipsForCloseInLoss), 200000m)
.SetDisplay("Loss (pips)", "Allowed drawdown before protective close", "Risk")
.SetGreaterThanZero();
_placeHedgeOrder = Param(nameof(PlaceHedgeOrder), false)
.SetDisplay("Use Hedge", "Enable hedge rebalancing", "Risk");
_hedgeLossThreshold = Param(nameof(HedgeLossThreshold), 10m)
.SetDisplay("Hedge Threshold (%)", "Loss percentage that triggers hedging", "Risk")
.SetGreaterThanZero();
_hedgeVolumeMultiplier = Param(nameof(HedgeVolumeMultiplier), 1m)
.SetDisplay("Hedge Multiplier", "Multiplier applied to imbalance volume", "Risk")
.SetGreaterThanZero();
_autoLotSize = Param(nameof(AutoLotSize), false)
.SetDisplay("Auto Volume", "Use balance driven order size", "Volume");
_riskFactor = Param(nameof(RiskFactor), 1m)
.SetDisplay("Risk Factor", "Risk factor for automatic sizing", "Volume")
.SetGreaterThanZero();
_manualLotSize = Param(nameof(ManualLotSize), 0.01m)
.SetDisplay("Manual Volume", "Order size when auto sizing is disabled", "Volume")
.SetGreaterThanZero();
_lotProgression = Param(nameof(LotProgression), LotProgressionModes.Static)
.SetDisplay("Lot Progression", "How volumes scale with each layer", "Volume");
_maxMultiplierLot = Param(nameof(MaxMultiplierLot), 50m)
.SetDisplay("Max Multiplier", "Cap for lot growth relative to the first entry", "Volume")
.SetGreaterThanZero();
_maxOrders = Param(nameof(MaxOrders), 2)
.SetDisplay("Max Orders", "Maximum simultaneous positions (0 = unlimited)", "General")
.SetRange(0, 1000);
_maxSpread = Param(nameof(MaxSpread), 0m)
.SetDisplay("Max Spread", "Maximum allowed spread in pips (0 = ignore)", "General")
.SetNotNegative();
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Candle series used for price data", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
yield return (Security, CandleType);
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_longEntries.Clear();
_shortEntries.Clear();
_currentPrice = 0m;
_pointSize = 0m;
_priceStep = 0m;
_stepPrice = 0m;
_totalOrders = 0;
_buyOrders = 0;
_sellOrders = 0;
_totalLongVolume = 0m;
_totalShortVolume = 0m;
_buyProfit = 0m;
_sellProfit = 0m;
_buyPips = 0m;
_sellPips = 0m;
_lastBuyPrice = null;
_lastSellPrice = null;
_firstBuyVolume = 0m;
_firstSellVolume = 0m;
_lastBuyVolume = 0m;
_lastSellVolume = 0m;
_isHedged = false;
_cooldownBars = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_pointSize = CalculatePointSize();
_priceStep = Security?.PriceStep ?? 0m;
_stepPrice = GetSecurityValue<decimal?>(Level1Fields.StepPrice) ?? 0m;
SubscribeCandles(CandleType)
.Bind(ProcessCandle)
.Start();
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
_currentPrice = candle.ClosePrice;
if (_currentPrice <= 0m)
return;
ProcessPrices();
}
private void ProcessPrices()
{
if (_cooldownBars > 0)
{
_cooldownBars--;
return;
}
UpdateAggregates();
if (_totalOrders > 0)
{
if (ShouldCloseInProfit())
{
if (CloseAllPositions())
{
_cooldownBars = 200;
return;
}
}
if (LossMode != LossCloseModes.DoNothing && ShouldCloseInLoss())
{
var closed = LossMode == LossCloseModes.CloseFirst ? CloseFirstPositions() : CloseAllPositions();
if (closed)
{
_cooldownBars = 200;
return;
}
}
if (ShouldHedge())
{
if (ExecuteHedgeOrder())
return;
}
}
if (TryOpenInitialOrders())
return;
TryOpenNextOrders();
}
private void UpdateAggregates()
{
_totalOrders = _longEntries.Count + _shortEntries.Count;
_buyOrders = _longEntries.Count;
_sellOrders = _shortEntries.Count;
_totalLongVolume = 0m;
_totalShortVolume = 0m;
_buyProfit = 0m;
_sellProfit = 0m;
_buyPips = 0m;
_sellPips = 0m;
_lastBuyPrice = null;
_lastSellPrice = null;
foreach (var entry in _longEntries)
{
_totalLongVolume += entry.Volume;
_lastBuyPrice = entry.Price;
var diff = _currentPrice - entry.Price;
_buyProfit += CalculateProfit(diff, entry.Volume);
_buyPips += CalculatePips(diff);
}
foreach (var entry in _shortEntries)
{
_totalShortVolume += entry.Volume;
_lastSellPrice = entry.Price;
var diff = entry.Price - _currentPrice;
_sellProfit += CalculateProfit(diff, entry.Volume);
_sellPips += CalculatePips(diff);
}
_firstBuyVolume = _longEntries.Count > 0 ? _longEntries[0].Volume : 0m;
_firstSellVolume = _shortEntries.Count > 0 ? _shortEntries[0].Volume : 0m;
_lastBuyVolume = _longEntries.Count > 0 ? _longEntries[^1].Volume : 0m;
_lastSellVolume = _shortEntries.Count > 0 ? _shortEntries[^1].Volume : 0m;
_isHedged = _buyOrders > 1 && _sellOrders > 1 && _totalLongVolume == _totalShortVolume && _totalLongVolume > 0m;
}
private decimal CalculateProfit(decimal diff, decimal volume)
{
if (_priceStep > 0m && _stepPrice > 0m)
return diff / _priceStep * _stepPrice * volume;
return diff * volume;
}
private decimal CalculatePips(decimal diff)
{
if (_pointSize > 0m)
return diff / _pointSize;
return diff;
}
private bool ShouldCloseInProfit()
{
var entries = _buyOrders + _sellOrders;
if (entries <= 0)
return false;
return CloseTarget switch
{
CloseTargetModes.Pips => (_buyPips + _sellPips) / entries >= PipsCloseInProfit,
CloseTargetModes.Currency => (_buyProfit + _sellProfit) >= CurrencyCloseInProfit,
_ => false,
};
}
private bool ShouldCloseInLoss()
{
var entries = _buyOrders + _sellOrders;
if (entries <= 0)
return false;
return (_buyPips + _sellPips) / entries <= -PipsForCloseInLoss;
}
private bool CloseAllPositions()
{
var closed = false;
if (_buyOrders > 0)
{
var volume = _totalLongVolume;
_longEntries.Clear();
if (volume > 0m)
{
SellMarket(volume);
closed = true;
}
}
if (_sellOrders > 0)
{
var volume = _totalShortVolume;
_shortEntries.Clear();
if (volume > 0m)
{
BuyMarket(volume);
closed = true;
}
}
return closed;
}
private bool CloseFirstPositions()
{
var closed = false;
if (_buyOrders > 0)
{
var volume = _longEntries[0].Volume;
_longEntries.RemoveAt(0);
if (volume > 0m)
{
SellMarket(volume);
closed = true;
}
}
if (_sellOrders > 0)
{
var volume = _shortEntries[0].Volume;
_shortEntries.RemoveAt(0);
if (volume > 0m)
{
BuyMarket(volume);
closed = true;
}
}
return closed;
}
private bool ShouldHedge()
{
if (!PlaceHedgeOrder || HedgeLossThreshold <= 0m)
return false;
var balance = Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
if (balance <= 0m)
return false;
var floating = _buyProfit + _sellProfit;
if (floating >= 0m)
return false;
var lossPercent = Math.Abs(floating) * 100m / balance;
return lossPercent >= HedgeLossThreshold && !_isHedged;
}
private bool ExecuteHedgeOrder()
{
var imbalance = _totalLongVolume - _totalShortVolume;
if (imbalance == 0m)
return false;
if (imbalance < 0m)
{
var volume = NormalizeVolume(Math.Abs(imbalance) * HedgeVolumeMultiplier);
if (volume <= 0m)
return false;
_longEntries.Add(new PositionEntry(_currentPrice, volume));
BuyMarket(volume);
return true;
}
var sellVolume = NormalizeVolume(imbalance * HedgeVolumeMultiplier);
if (sellVolume <= 0m)
return false;
_shortEntries.Add(new PositionEntry(_currentPrice, sellVolume));
SellMarket(sellVolume);
return true;
}
private bool TryOpenInitialOrders()
{
if (!CanOpenMoreOrders())
return false;
if (_buyOrders == 0 && (OrderPlacement == OrderPlacementModes.Both || OrderPlacement == OrderPlacementModes.LongOnly))
{
var volume = GetOrderVolume(Sides.Buy);
if (SendMarketOrder(Sides.Buy, volume))
return true;
}
if (_sellOrders == 0 && (OrderPlacement == OrderPlacementModes.Both || OrderPlacement == OrderPlacementModes.ShortOnly))
{
var volume = GetOrderVolume(Sides.Sell);
if (SendMarketOrder(Sides.Sell, volume))
return true;
}
return false;
}
private void TryOpenNextOrders()
{
if (!CanOpenMoreOrders())
return;
var allowBuy = OrderPlacement != OrderPlacementModes.ShortOnly;
var allowSell = OrderPlacement != OrderPlacementModes.LongOnly;
if (!allowBuy && !allowSell)
return;
if ((_buyOrders > 0 || OrderPlacement == OrderPlacementModes.ShortOnly)
&& (_sellOrders > 0 || OrderPlacement == OrderPlacementModes.LongOnly))
{
var buyDistance = allowBuy ? GetNextDistance(Sides.Buy) : 0m;
var sellDistance = allowSell ? GetNextDistance(Sides.Sell) : 0m;
if (GridDirection == GridDirectionModes.WithTrend)
{
if (allowBuy && _lastBuyPrice.HasValue && buyDistance > 0m)
{
var trigger = _lastBuyPrice.Value + buyDistance;
if (_currentPrice >= trigger)
{
var volume = GetOrderVolume(Sides.Buy);
if (SendMarketOrder(Sides.Buy, volume))
return;
}
}
if (allowSell && _lastSellPrice.HasValue && sellDistance > 0m)
{
var trigger = _lastSellPrice.Value - sellDistance;
if (_currentPrice <= trigger)
{
var volume = GetOrderVolume(Sides.Sell);
if (SendMarketOrder(Sides.Sell, volume))
return;
}
}
}
else
{
if (allowBuy && _lastBuyPrice.HasValue && buyDistance > 0m)
{
var trigger = _lastBuyPrice.Value - buyDistance;
if (_currentPrice <= trigger)
{
var volume = GetOrderVolume(Sides.Buy);
if (SendMarketOrder(Sides.Buy, volume))
return;
}
}
if (allowSell && _lastSellPrice.HasValue && sellDistance > 0m)
{
var trigger = _lastSellPrice.Value + sellDistance;
if (_currentPrice >= trigger)
{
var volume = GetOrderVolume(Sides.Sell);
if (SendMarketOrder(Sides.Sell, volume))
return;
}
}
}
}
}
private bool CanOpenMoreOrders()
{
if (MaxOrders <= 0)
return true;
return _totalOrders < MaxOrders;
}
private decimal GetNextDistance(Sides side)
{
var baseDistance = PipsForNextOrder;
var count = side == Sides.Buy ? _buyOrders : _sellOrders;
var multiplier = StepProgression switch
{
StepProgressionModes.Static => 1m,
StepProgressionModes.Geometric => Math.Max(1, count),
StepProgressionModes.Exponential => count <= 0 ? 1m : (decimal)Math.Max(1, Math.Pow(2, count - 1)),
_ => 1m,
};
return baseDistance * multiplier * _pointSize;
}
private decimal GetOrderVolume(Sides side)
{
var baseVolume = GetBaseVolume();
var firstVolume = side == Sides.Buy ? _firstBuyVolume : _firstSellVolume;
var lastVolume = side == Sides.Buy ? _lastBuyVolume : _lastSellVolume;
var orders = side == Sides.Buy ? _buyOrders : _sellOrders;
decimal result;
switch (LotProgression)
{
case LotProgressionModes.Static:
result = orders == 0 ? baseVolume : (firstVolume > 0m ? firstVolume : baseVolume);
break;
case LotProgressionModes.Geometric:
if (orders == 0)
{
result = baseVolume;
}
else if (orders == 1)
{
result = lastVolume * 2m;
}
else
{
result = lastVolume + (firstVolume > 0m ? firstVolume : baseVolume);
}
break;
case LotProgressionModes.Exponential:
result = orders == 0 ? baseVolume : lastVolume * 2m;
break;
default:
result = baseVolume;
break;
}
if (MaxMultiplierLot > 0m && orders > 0 && firstVolume > 0m)
{
var cap = firstVolume * MaxMultiplierLot;
if (result > cap)
result = cap;
}
return NormalizeVolume(result);
}
private decimal GetBaseVolume()
{
decimal volume;
if (AutoLotSize)
{
var balance = Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
volume = balance > 0m ? balance * RiskFactor / 100000m : ManualLotSize;
}
else
{
volume = ManualLotSize;
}
return NormalizeVolume(volume);
}
private bool SendMarketOrder(Sides side, decimal volume)
{
if (volume <= 0m)
return false;
if (side == Sides.Buy)
{
_longEntries.Add(new PositionEntry(_currentPrice, volume));
BuyMarket(volume);
}
else
{
_shortEntries.Add(new PositionEntry(_currentPrice, volume));
SellMarket(volume);
}
return true;
}
private decimal NormalizeVolume(decimal volume)
{
if (volume <= 0m)
return 0m;
var min = Security?.MinVolume ?? 0m;
var max = Security?.MaxVolume ?? 0m;
var step = Security?.VolumeStep ?? 0m;
if (step > 0m)
volume = Math.Round(volume / step, 0, MidpointRounding.AwayFromZero) * step;
if (min > 0m)
volume = Math.Max(volume, min);
if (max > 0m)
volume = Math.Min(volume, max);
return volume;
}
private decimal CalculatePointSize()
{
var step = Security?.PriceStep ?? 0m;
return step > 0m ? step : 0.0001m;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates, Level1Fields, Sides
from StockSharp.Algo.Strategies import Strategy
class build_your_grid_strategy(Strategy):
"""Grid strategy converted from the MetaTrader expert 'BuildYourGridEA'.
It maintains layered long and short positions, optionally increases volume geometrically
and supports profit/loss group exits together with hedge rebalancing."""
# OrderPlacementModes
OP_BOTH = 0
OP_LONG_ONLY = 1
OP_SHORT_ONLY = 2
# GridDirectionModes
GD_WITH_TREND = 0
GD_AGAINST_TREND = 1
# StepProgressionModes
SP_STATIC = 0
SP_GEOMETRIC = 1
SP_EXPONENTIAL = 2
# CloseTargetModes
CT_PIPS = 0
CT_CURRENCY = 1
# LossCloseModes
LC_DO_NOTHING = 0
LC_CLOSE_FIRST = 1
LC_CLOSE_ALL = 2
# LotProgressionModes
LP_STATIC = 0
LP_GEOMETRIC = 1
LP_EXPONENTIAL = 2
def __init__(self):
super(build_your_grid_strategy, self).__init__()
self._order_placement = self.Param("OrderPlacement", self.OP_LONG_ONLY) \
.SetDisplay("Order Placement", "Allowed entry direction", "General")
self._grid_direction = self.Param("GridDirection", self.GD_AGAINST_TREND) \
.SetDisplay("Grid Direction", "Whether layers follow or fade the trend", "Grid")
self._pips_for_next_order = self.Param("PipsForNextOrder", 500000.0) \
.SetGreaterThanZero() \
.SetDisplay("Grid Step (pips)", "Base spacing between grid levels", "Grid")
self._step_progression = self.Param("StepProgression", self.SP_STATIC) \
.SetDisplay("Step Progression", "How the distance grows with each layer", "Grid")
self._close_target_mode = self.Param("CloseTarget", self.CT_PIPS) \
.SetDisplay("Close Target", "Profit target type", "Risk")
self._pips_close_in_profit = self.Param("PipsCloseInProfit", 500000.0) \
.SetGreaterThanZero() \
.SetDisplay("Target (pips)", "Basket profit target in pips", "Risk")
self._currency_close_in_profit = self.Param("CurrencyCloseInProfit", 10.0) \
.SetGreaterThanZero() \
.SetDisplay("Target (currency)", "Basket profit target in account currency", "Risk")
self._loss_close_mode = self.Param("LossMode", self.LC_DO_NOTHING) \
.SetDisplay("Loss Handling", "Action when loss threshold is hit", "Risk")
self._pips_for_close_in_loss = self.Param("PipsForCloseInLoss", 200000.0) \
.SetGreaterThanZero() \
.SetDisplay("Loss (pips)", "Allowed drawdown before protective close", "Risk")
self._place_hedge_order = self.Param("PlaceHedgeOrder", False) \
.SetDisplay("Use Hedge", "Enable hedge rebalancing", "Risk")
self._hedge_loss_threshold = self.Param("HedgeLossThreshold", 10.0) \
.SetGreaterThanZero() \
.SetDisplay("Hedge Threshold (%)", "Loss percentage that triggers hedging", "Risk")
self._hedge_volume_multiplier = self.Param("HedgeVolumeMultiplier", 1.0) \
.SetGreaterThanZero() \
.SetDisplay("Hedge Multiplier", "Multiplier applied to imbalance volume", "Risk")
self._auto_lot_size = self.Param("AutoLotSize", False) \
.SetDisplay("Auto Volume", "Use balance driven order size", "Volume")
self._risk_factor = self.Param("RiskFactor", 1.0) \
.SetGreaterThanZero() \
.SetDisplay("Risk Factor", "Risk factor for automatic sizing", "Volume")
self._manual_lot_size = self.Param("ManualLotSize", 0.01) \
.SetGreaterThanZero() \
.SetDisplay("Manual Volume", "Order size when auto sizing is disabled", "Volume")
self._lot_progression = self.Param("LotProgression", self.LP_STATIC) \
.SetDisplay("Lot Progression", "How volumes scale with each layer", "Volume")
self._max_multiplier_lot = self.Param("MaxMultiplierLot", 50.0) \
.SetGreaterThanZero() \
.SetDisplay("Max Multiplier", "Cap for lot growth relative to first entry", "Volume")
self._max_orders = self.Param("MaxOrders", 2) \
.SetDisplay("Max Orders", "Maximum simultaneous positions (0 = unlimited)", "General")
self._max_spread = self.Param("MaxSpread", 0.0) \
.SetDisplay("Max Spread", "Maximum allowed spread in pips (0 = ignore)", "General")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Candle series used for price data", "General")
self._long_entries = [] # list of [price, volume]
self._short_entries = [] # list of [price, volume]
self._current_price = 0.0
self._point_size = 0.0
self._price_step = 0.0
self._step_price = 0.0
self._total_orders = 0
self._buy_orders = 0
self._sell_orders = 0
self._total_long_volume = 0.0
self._total_short_volume = 0.0
self._buy_profit = 0.0
self._sell_profit = 0.0
self._buy_pips = 0.0
self._sell_pips = 0.0
self._last_buy_price = None
self._last_sell_price = None
self._first_buy_volume = 0.0
self._first_sell_volume = 0.0
self._last_buy_volume = 0.0
self._last_sell_volume = 0.0
self._is_hedged = False
self._cooldown_bars = 0
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def OrderPlacement(self):
return self._order_placement.Value
@property
def GridDirection(self):
return self._grid_direction.Value
@property
def PipsForNextOrder(self):
return self._pips_for_next_order.Value
@property
def StepProgression(self):
return self._step_progression.Value
@property
def CloseTarget(self):
return self._close_target_mode.Value
@property
def PipsCloseInProfit(self):
return self._pips_close_in_profit.Value
@property
def CurrencyCloseInProfit(self):
return self._currency_close_in_profit.Value
@property
def LossMode(self):
return self._loss_close_mode.Value
@property
def PipsForCloseInLoss(self):
return self._pips_for_close_in_loss.Value
@property
def PlaceHedgeOrder(self):
return self._place_hedge_order.Value
@property
def HedgeLossThreshold(self):
return self._hedge_loss_threshold.Value
@property
def HedgeVolumeMultiplier(self):
return self._hedge_volume_multiplier.Value
@property
def AutoLotSize(self):
return self._auto_lot_size.Value
@property
def RiskFactor(self):
return self._risk_factor.Value
@property
def ManualLotSize(self):
return self._manual_lot_size.Value
@property
def LotProgression(self):
return self._lot_progression.Value
@property
def MaxMultiplierLot(self):
return self._max_multiplier_lot.Value
@property
def MaxOrders(self):
return self._max_orders.Value
@property
def MaxSpread(self):
return self._max_spread.Value
def OnReseted(self):
super(build_your_grid_strategy, self).OnReseted()
self._long_entries = []
self._short_entries = []
self._current_price = 0.0
self._point_size = 0.0
self._price_step = 0.0
self._step_price = 0.0
self._total_orders = 0
self._buy_orders = 0
self._sell_orders = 0
self._total_long_volume = 0.0
self._total_short_volume = 0.0
self._buy_profit = 0.0
self._sell_profit = 0.0
self._buy_pips = 0.0
self._sell_pips = 0.0
self._last_buy_price = None
self._last_sell_price = None
self._first_buy_volume = 0.0
self._first_sell_volume = 0.0
self._last_buy_volume = 0.0
self._last_sell_volume = 0.0
self._is_hedged = False
self._cooldown_bars = 0
def OnStarted2(self, time):
super(build_your_grid_strategy, self).OnStarted2(time)
self._point_size = self._calculate_point_size()
ps = self.Security.PriceStep if self.Security is not None else None
self._price_step = float(ps) if ps is not None else 0.0
sp = self.GetSecurityValue[object](Level1Fields.StepPrice)
self._step_price = float(sp) if sp is not None else 0.0
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._process_candle).Start()
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
self._current_price = float(candle.ClosePrice)
if self._current_price <= 0.0:
return
self._process_prices()
def _process_prices(self):
if self._cooldown_bars > 0:
self._cooldown_bars -= 1
return
self._update_aggregates()
if self._total_orders > 0:
if self._should_close_in_profit():
if self._close_all_positions():
self._cooldown_bars = 200
return
if self.LossMode != self.LC_DO_NOTHING and self._should_close_in_loss():
if self.LossMode == self.LC_CLOSE_FIRST:
closed = self._close_first_positions()
else:
closed = self._close_all_positions()
if closed:
self._cooldown_bars = 200
return
if self._should_hedge():
if self._execute_hedge_order():
return
if self._try_open_initial_orders():
return
self._try_open_next_orders()
def _update_aggregates(self):
self._total_orders = len(self._long_entries) + len(self._short_entries)
self._buy_orders = len(self._long_entries)
self._sell_orders = len(self._short_entries)
self._total_long_volume = 0.0
self._total_short_volume = 0.0
self._buy_profit = 0.0
self._sell_profit = 0.0
self._buy_pips = 0.0
self._sell_pips = 0.0
self._last_buy_price = None
self._last_sell_price = None
for entry in self._long_entries:
self._total_long_volume += entry[1]
self._last_buy_price = entry[0]
diff = self._current_price - entry[0]
self._buy_profit += self._calculate_profit(diff, entry[1])
self._buy_pips += self._calculate_pips(diff)
for entry in self._short_entries:
self._total_short_volume += entry[1]
self._last_sell_price = entry[0]
diff = entry[0] - self._current_price
self._sell_profit += self._calculate_profit(diff, entry[1])
self._sell_pips += self._calculate_pips(diff)
self._first_buy_volume = self._long_entries[0][1] if len(self._long_entries) > 0 else 0.0
self._first_sell_volume = self._short_entries[0][1] if len(self._short_entries) > 0 else 0.0
self._last_buy_volume = self._long_entries[-1][1] if len(self._long_entries) > 0 else 0.0
self._last_sell_volume = self._short_entries[-1][1] if len(self._short_entries) > 0 else 0.0
self._is_hedged = (self._buy_orders > 1 and self._sell_orders > 1
and self._total_long_volume == self._total_short_volume
and self._total_long_volume > 0.0)
def _calculate_profit(self, diff, volume):
if self._price_step > 0.0 and self._step_price > 0.0:
return diff / self._price_step * self._step_price * volume
return diff * volume
def _calculate_pips(self, diff):
if self._point_size > 0.0:
return diff / self._point_size
return diff
def _should_close_in_profit(self):
entries = self._buy_orders + self._sell_orders
if entries <= 0:
return False
if self.CloseTarget == self.CT_PIPS:
return (self._buy_pips + self._sell_pips) / float(entries) >= float(self.PipsCloseInProfit)
elif self.CloseTarget == self.CT_CURRENCY:
return (self._buy_profit + self._sell_profit) >= float(self.CurrencyCloseInProfit)
return False
def _should_close_in_loss(self):
entries = self._buy_orders + self._sell_orders
if entries <= 0:
return False
return (self._buy_pips + self._sell_pips) / float(entries) <= -float(self.PipsForCloseInLoss)
def _close_all_positions(self):
closed = False
if self._buy_orders > 0:
volume = self._total_long_volume
self._long_entries = []
if volume > 0.0:
self.SellMarket(volume)
closed = True
if self._sell_orders > 0:
volume = self._total_short_volume
self._short_entries = []
if volume > 0.0:
self.BuyMarket(volume)
closed = True
return closed
def _close_first_positions(self):
closed = False
if self._buy_orders > 0:
volume = self._long_entries[0][1]
self._long_entries.pop(0)
if volume > 0.0:
self.SellMarket(volume)
closed = True
if self._sell_orders > 0:
volume = self._short_entries[0][1]
self._short_entries.pop(0)
if volume > 0.0:
self.BuyMarket(volume)
closed = True
return closed
def _should_hedge(self):
if not self.PlaceHedgeOrder or float(self.HedgeLossThreshold) <= 0.0:
return False
portfolio = self.Portfolio
balance = 0.0
if portfolio is not None:
cv = portfolio.CurrentValue
bv = portfolio.BeginValue
if cv is not None:
balance = float(cv)
elif bv is not None:
balance = float(bv)
if balance <= 0.0:
return False
floating = self._buy_profit + self._sell_profit
if floating >= 0.0:
return False
loss_percent = abs(floating) * 100.0 / balance
return loss_percent >= float(self.HedgeLossThreshold) and not self._is_hedged
def _execute_hedge_order(self):
imbalance = self._total_long_volume - self._total_short_volume
if imbalance == 0.0:
return False
if imbalance < 0.0:
volume = self._normalize_volume(abs(imbalance) * float(self.HedgeVolumeMultiplier))
if volume <= 0.0:
return False
self._long_entries.append([self._current_price, volume])
self.BuyMarket(volume)
return True
sell_volume = self._normalize_volume(imbalance * float(self.HedgeVolumeMultiplier))
if sell_volume <= 0.0:
return False
self._short_entries.append([self._current_price, sell_volume])
self.SellMarket(sell_volume)
return True
def _can_open_more_orders(self):
if self.MaxOrders <= 0:
return True
return self._total_orders < self.MaxOrders
def _try_open_initial_orders(self):
if not self._can_open_more_orders():
return False
if self._buy_orders == 0 and (self.OrderPlacement == self.OP_BOTH or self.OrderPlacement == self.OP_LONG_ONLY):
volume = self._get_order_volume(Sides.Buy)
if self._send_market_order(Sides.Buy, volume):
return True
if self._sell_orders == 0 and (self.OrderPlacement == self.OP_BOTH or self.OrderPlacement == self.OP_SHORT_ONLY):
volume = self._get_order_volume(Sides.Sell)
if self._send_market_order(Sides.Sell, volume):
return True
return False
def _try_open_next_orders(self):
if not self._can_open_more_orders():
return
allow_buy = self.OrderPlacement != self.OP_SHORT_ONLY
allow_sell = self.OrderPlacement != self.OP_LONG_ONLY
if not allow_buy and not allow_sell:
return
has_buys = self._buy_orders > 0 or self.OrderPlacement == self.OP_SHORT_ONLY
has_sells = self._sell_orders > 0 or self.OrderPlacement == self.OP_LONG_ONLY
if not (has_buys and has_sells):
return
buy_distance = self._get_next_distance(Sides.Buy) if allow_buy else 0.0
sell_distance = self._get_next_distance(Sides.Sell) if allow_sell else 0.0
if self.GridDirection == self.GD_WITH_TREND:
if allow_buy and self._last_buy_price is not None and buy_distance > 0.0:
trigger = self._last_buy_price + buy_distance
if self._current_price >= trigger:
volume = self._get_order_volume(Sides.Buy)
if self._send_market_order(Sides.Buy, volume):
return
if allow_sell and self._last_sell_price is not None and sell_distance > 0.0:
trigger = self._last_sell_price - sell_distance
if self._current_price <= trigger:
volume = self._get_order_volume(Sides.Sell)
if self._send_market_order(Sides.Sell, volume):
return
else:
if allow_buy and self._last_buy_price is not None and buy_distance > 0.0:
trigger = self._last_buy_price - buy_distance
if self._current_price <= trigger:
volume = self._get_order_volume(Sides.Buy)
if self._send_market_order(Sides.Buy, volume):
return
if allow_sell and self._last_sell_price is not None and sell_distance > 0.0:
trigger = self._last_sell_price + sell_distance
if self._current_price >= trigger:
volume = self._get_order_volume(Sides.Sell)
if self._send_market_order(Sides.Sell, volume):
return
def _send_market_order(self, side, volume):
if volume <= 0.0:
return False
if side == Sides.Buy:
self._long_entries.append([self._current_price, volume])
self.BuyMarket(volume)
else:
self._short_entries.append([self._current_price, volume])
self.SellMarket(volume)
return True
def _get_next_distance(self, side):
base_distance = float(self.PipsForNextOrder)
count = self._buy_orders if side == Sides.Buy else self._sell_orders
prog = self.StepProgression
if prog == self.SP_STATIC:
multiplier = 1.0
elif prog == self.SP_GEOMETRIC:
multiplier = float(max(1, count))
elif prog == self.SP_EXPONENTIAL:
if count <= 0:
multiplier = 1.0
else:
multiplier = float(max(1, int(Math.Pow(2, count - 1))))
else:
multiplier = 1.0
return base_distance * multiplier * self._point_size
def _get_order_volume(self, side):
base_volume = self._get_base_volume()
is_buy = side == Sides.Buy
first_volume = self._first_buy_volume if is_buy else self._first_sell_volume
last_volume = self._last_buy_volume if is_buy else self._last_sell_volume
orders = self._buy_orders if is_buy else self._sell_orders
prog = self.LotProgression
if prog == self.LP_STATIC:
if orders == 0:
result = base_volume
else:
result = first_volume if first_volume > 0.0 else base_volume
elif prog == self.LP_GEOMETRIC:
if orders == 0:
result = base_volume
elif orders == 1:
result = last_volume * 2.0
else:
result = last_volume + (first_volume if first_volume > 0.0 else base_volume)
elif prog == self.LP_EXPONENTIAL:
if orders == 0:
result = base_volume
else:
result = last_volume * 2.0
else:
result = base_volume
max_mult = float(self.MaxMultiplierLot)
if max_mult > 0.0 and orders > 0 and first_volume > 0.0:
cap = first_volume * max_mult
if result > cap:
result = cap
return self._normalize_volume(result)
def _get_base_volume(self):
if self.AutoLotSize:
portfolio = self.Portfolio
balance = 0.0
if portfolio is not None:
cv = portfolio.CurrentValue
bv = portfolio.BeginValue
if cv is not None:
balance = float(cv)
elif bv is not None:
balance = float(bv)
if balance > 0.0:
volume = balance * float(self.RiskFactor) / 100000.0
else:
volume = float(self.ManualLotSize)
else:
volume = float(self.ManualLotSize)
return self._normalize_volume(volume)
def _normalize_volume(self, volume):
if volume <= 0.0:
return 0.0
sec = self.Security
min_vol = float(sec.MinVolume) if sec is not None and sec.MinVolume is not None else 0.0
max_vol = float(sec.MaxVolume) if sec is not None and sec.MaxVolume is not None else 0.0
step = float(sec.VolumeStep) if sec is not None and sec.VolumeStep is not None else 0.0
if step > 0.0:
volume = round(volume / step) * step
if min_vol > 0.0 and volume < min_vol:
volume = min_vol
if max_vol > 0.0 and volume > max_vol:
volume = max_vol
return volume
def _calculate_point_size(self):
if self.Security is not None and self.Security.PriceStep is not None:
step = float(self.Security.PriceStep)
if step > 0.0:
return step
return 0.0001
def CreateClone(self):
return build_your_grid_strategy()