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>
/// Moving average grid strategy converted from the MetaTrader MAGrid expert.
/// It manages a symmetric basket of long and short orders around an EMA-based anchor level.
/// </summary>
public class MaGridStrategy : Strategy
{
private readonly StrategyParam<decimal> _volumeTolerance;
private readonly StrategyParam<int> _maPeriod;
private readonly StrategyParam<int> _gridAmount;
private readonly StrategyParam<decimal> _distance;
private readonly StrategyParam<decimal> _orderVolume;
private readonly StrategyParam<DataType> _candleType;
private readonly Dictionary<Order, OrderIntents> _orderIntents = new();
private ExponentialMovingAverage _ema;
private int _effectiveGridAmount;
private int _currentGrid;
private decimal _nextGridPrice;
private decimal _lastGridPrice;
private bool _isGridInitialized;
private decimal _longExposure;
private decimal _shortExposure;
private enum OrderIntents
{
OpenLong,
OpenShort,
CloseLong,
CloseShort
}
/// <summary>
/// Initializes a new instance of the <see cref="MaGridStrategy"/> class.
/// </summary>
public MaGridStrategy()
{
_volumeTolerance = Param(nameof(VolumeTolerance), 0.0000001m)
.SetNotNegative()
.SetDisplay("Volume Tolerance", "Small tolerance applied when balancing grid exposure.", "Risk");
_maPeriod = Param(nameof(MaPeriod), 48)
.SetRange(5, 400)
.SetDisplay("MA Period", "Exponential moving average length", "Grid")
;
_gridAmount = Param(nameof(GridAmount), 6)
.SetRange(2, 40)
.SetDisplay("Grid Amount", "Number of grid steps (will be forced to an even value)", "Grid")
;
_distance = Param(nameof(Distance), 0.005m)
.SetGreaterThanZero()
.SetDisplay("Distance", "Relative spacing between grid levels", "Grid")
;
_orderVolume = Param(nameof(OrderVolume), 0.1m)
.SetGreaterThanZero()
.SetDisplay("Order Volume", "Volume per grid order", "Risk")
;
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Primary candle type used by the strategy", "Data");
}
/// <summary>
/// Small tolerance used when comparing accumulated exposure.
/// </summary>
public decimal VolumeTolerance
{
get => _volumeTolerance.Value;
set => _volumeTolerance.Value = value;
}
/// <summary>
/// EMA period used for the anchor level.
/// </summary>
public int MaPeriod
{
get => _maPeriod.Value;
set => _maPeriod.Value = value;
}
/// <summary>
/// Total number of grid steps that will be mirrored around the EMA.
/// </summary>
public int GridAmount
{
get => _gridAmount.Value;
set => _gridAmount.Value = value;
}
/// <summary>
/// Relative distance between consecutive grid levels.
/// </summary>
public decimal Distance
{
get => _distance.Value;
set => _distance.Value = value;
}
/// <summary>
/// Volume submitted with each market order.
/// </summary>
public decimal OrderVolume
{
get => _orderVolume.Value;
set => _orderVolume.Value = value;
}
/// <summary>
/// Candle type used for data subscription.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_orderIntents.Clear();
_ema = null;
_effectiveGridAmount = 0;
_currentGrid = 0;
_nextGridPrice = 0m;
_lastGridPrice = 0m;
_isGridInitialized = false;
_longExposure = 0m;
_shortExposure = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_effectiveGridAmount = GetEffectiveGridAmount();
_currentGrid = 0;
_nextGridPrice = 0m;
_lastGridPrice = 0m;
_isGridInitialized = false;
_longExposure = 0m;
_shortExposure = 0m;
_orderIntents.Clear();
_ema = new EMA
{
Length = MaPeriod
};
SubscribeCandles(CandleType)
.Bind(_ema, ProcessCandle)
.Start();
}
/// <inheritdoc />
protected override void OnOwnTradeReceived(MyTrade trade)
{
base.OnOwnTradeReceived(trade);
if (trade?.Order is not { } order || !_orderIntents.TryGetValue(order, out var intent))
return;
var volume = trade.Trade.Volume;
switch (intent)
{
case OrderIntents.OpenLong:
_longExposure += volume;
break;
case OrderIntents.OpenShort:
_shortExposure += volume;
break;
case OrderIntents.CloseLong:
_longExposure = Math.Max(0m, _longExposure - volume);
break;
case OrderIntents.CloseShort:
_shortExposure = Math.Max(0m, _shortExposure - volume);
break;
}
if (order.Balance <= VolumeTolerance || (order.State == OrderStates.Done || order.State == OrderStates.Failed))
_orderIntents.Remove(order);
}
private void ProcessCandle(ICandleMessage candle, decimal emaValue)
{
if (candle.State != CandleStates.Finished)
return;
if (_ema?.IsFormed != true)
return;
CleanupCompletedOrders();
if (!_isGridInitialized)
{
InitializeGrid(candle.ClosePrice, emaValue);
return;
}
UpdateGridLevels(emaValue);
if (_nextGridPrice > 0m && candle.ClosePrice >= _nextGridPrice && _nextGridPrice < decimal.MaxValue)
{
_currentGrid++;
CloseLongExposure();
OpenShortExposure();
UpdateGridLevels(emaValue);
}
else if (_lastGridPrice > 0m && candle.ClosePrice <= _lastGridPrice && _lastGridPrice > decimal.MinValue)
{
_currentGrid--;
CloseShortExposure();
OpenLongExposure();
UpdateGridLevels(emaValue);
}
}
private int GetEffectiveGridAmount()
{
var amount = GridAmount;
if (amount < 2)
amount = 2;
if (amount % 2 != 0)
amount++;
return amount;
}
private void InitializeGrid(decimal closePrice, decimal ema)
{
_isGridInitialized = true;
_currentGrid = DetermineInitialGrid(closePrice, ema);
var half = _effectiveGridAmount / 2;
var buyCount = Math.Max(0, half - _currentGrid);
var sellCount = Math.Max(0, _effectiveGridAmount - buyCount);
for (var i = 0; i < buyCount; i++)
OpenLongExposure();
for (var i = 0; i < sellCount; i++)
OpenShortExposure();
UpdateGridLevels(ema);
}
private int DetermineInitialGrid(decimal price, decimal ema)
{
var half = _effectiveGridAmount / 2;
var distance = Distance;
if (price < ema)
{
for (var i = 1; i <= half; i++)
{
var level = ema * (1m - distance * i);
if (price > level)
return 1 - i;
}
return -half;
}
for (var i = 1; i <= half; i++)
{
var level = ema * (1m + distance * i);
if (price < level)
return i - 1;
}
return half;
}
private void UpdateGridLevels(decimal ema)
{
var distance = Distance;
if (_currentGrid < _effectiveGridAmount - 1)
_nextGridPrice = ema * (1m + distance * (1m + _currentGrid));
else
_nextGridPrice = 0m;
if (_currentGrid > 1 - _effectiveGridAmount)
_lastGridPrice = ema * (1m - distance * (1m - _currentGrid));
else
_lastGridPrice = 0m;
if (_longExposure <= VolumeTolerance)
_nextGridPrice = decimal.MaxValue;
if (_shortExposure <= VolumeTolerance)
_lastGridPrice = decimal.MinValue;
}
private void OpenLongExposure()
{
if (OrderVolume <= 0m)
return;
RegisterOrder(BuyMarket(OrderVolume), OrderIntents.OpenLong);
}
private void OpenShortExposure()
{
if (OrderVolume <= 0m)
return;
RegisterOrder(SellMarket(OrderVolume), OrderIntents.OpenShort);
}
private void CloseLongExposure()
{
if (_longExposure <= VolumeTolerance)
return;
var volume = Math.Min(OrderVolume, _longExposure);
if (volume <= VolumeTolerance)
return;
RegisterOrder(SellMarket(volume), OrderIntents.CloseLong);
}
private void CloseShortExposure()
{
if (_shortExposure <= VolumeTolerance)
return;
var volume = Math.Min(OrderVolume, _shortExposure);
if (volume <= VolumeTolerance)
return;
RegisterOrder(BuyMarket(volume), OrderIntents.CloseShort);
}
private void RegisterOrder(Order order, OrderIntents intent)
{
if (order == null)
return;
_orderIntents[order] = intent;
}
private void CleanupCompletedOrders()
{
if (_orderIntents.Count == 0)
return;
List<Order> completed = null;
foreach (var pair in _orderIntents)
{
if (!(pair.Key.State == OrderStates.Done || pair.Key.State == OrderStates.Failed))
continue;
completed ??= new List<Order>();
completed.Add(pair.Key);
}
if (completed == null)
return;
foreach (var order in completed)
_orderIntents.Remove(order);
}
}
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.Indicators import ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
class ma_grid_strategy(Strategy):
def __init__(self):
super(ma_grid_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._ma_period = self.Param("MaPeriod", 48)
self._grid_amount = self.Param("GridAmount", 6)
self._distance = self.Param("Distance", 0.005)
self._current_grid = 0
self._next_grid_price = 0.0
self._last_grid_price = 0.0
self._is_grid_initialized = False
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def MaPeriod(self):
return self._ma_period.Value
@MaPeriod.setter
def MaPeriod(self, value):
self._ma_period.Value = value
@property
def GridAmount(self):
return self._grid_amount.Value
@GridAmount.setter
def GridAmount(self, value):
self._grid_amount.Value = value
@property
def Distance(self):
return self._distance.Value
@Distance.setter
def Distance(self, value):
self._distance.Value = value
def _get_effective_grid_amount(self):
amount = self.GridAmount
if amount < 2:
amount = 2
if amount % 2 != 0:
amount += 1
return amount
def OnReseted(self):
super(ma_grid_strategy, self).OnReseted()
self._current_grid = 0
self._next_grid_price = 0.0
self._last_grid_price = 0.0
self._is_grid_initialized = False
def OnStarted2(self, time):
super(ma_grid_strategy, self).OnStarted2(time)
self._current_grid = 0
self._next_grid_price = 0.0
self._last_grid_price = 0.0
self._is_grid_initialized = False
ema = ExponentialMovingAverage()
ema.Length = self.MaPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(ema, self._process_candle).Start()
def _update_grid_levels(self, ema_val):
dist = float(self.Distance)
effective = self._get_effective_grid_amount()
if self._current_grid < effective - 1:
self._next_grid_price = ema_val * (1.0 + dist * (1.0 + self._current_grid))
else:
self._next_grid_price = 0.0
if self._current_grid > 1 - effective:
self._last_grid_price = ema_val * (1.0 - dist * (1.0 - self._current_grid))
else:
self._last_grid_price = 0.0
def _process_candle(self, candle, ema_value):
if candle.State != CandleStates.Finished:
return
ema_val = float(ema_value)
close = float(candle.ClosePrice)
if not self._is_grid_initialized:
self._is_grid_initialized = True
# Determine initial grid position
effective = self._get_effective_grid_amount()
half = effective // 2
dist = float(self.Distance)
if close < ema_val:
for i in range(1, half + 1):
level = ema_val * (1.0 - dist * i)
if close > level:
self._current_grid = 1 - i
break
else:
self._current_grid = -half
else:
for i in range(1, half + 1):
level = ema_val * (1.0 + dist * i)
if close < level:
self._current_grid = i - 1
break
else:
self._current_grid = half
# Initial entry based on grid position
if self._current_grid < 0 and self.Position <= 0:
self.BuyMarket()
elif self._current_grid > 0 and self.Position >= 0:
self.SellMarket()
self._update_grid_levels(ema_val)
return
self._update_grid_levels(ema_val)
if self._next_grid_price > 0 and close >= self._next_grid_price:
self._current_grid += 1
self.SellMarket()
self._update_grid_levels(ema_val)
elif self._last_grid_price > 0 and close <= self._last_grid_price:
self._current_grid -= 1
self.BuyMarket()
self._update_grid_levels(ema_val)
def CreateClone(self):
return ma_grid_strategy()