TenPointThree MACD Grid Strategy
Overview
This strategy is a C# port of the MetaTrader expert advisor 10p3v003 (10point3.mq4). It combines a MACD crossover trigger with a martingale grid engine. The original logic was replicated using StockSharp's high level API with the following key behaviours:
- MACD signal logic – A trade direction is determined when the MACD main line crosses the signal line on the shifted bar (
SignalShift). Long entries require the previous signal value to be below -TradingRangePips, the current MACD value to stay below zero, and vice versa for shorts. Signals can optionally be inverted through ReverseSignal.
- Grid layering – After the first position is opened, additional entries in the same direction are only allowed once price moves against the last fill by at least
GridStepPips. Every new leg multiplies the volume by LotMultiplier (or by 1.5 if MaxTrades > 12), mimicking the martingale scaling from MQL4.
- Risk protection – The most recent leg is closed and no further entries are added when
OrdersToProtect or more trades are active and the floating profit exceeds the money threshold. The threshold is based on either the configured risk percent (money management enabled) or on the contract size heuristic (money management disabled).
- Per-leg exits – Each leg tracks its own take-profit, virtual stop-loss, and trailing stop. The stop distance matches the original formula:
InitialStopPips + (MaxTrades - existingOrders) * GridStepPips. Trailing activates only after price moves by TrailingStopPips + GridStepPips in favour of the position and closes the leg when price retraces by TrailingStopPips.
- Session filter – When
UseTimeFilter is enabled, no new grids are started while the candle time is strictly between StopHour and StartHour, reproducing the "danger time zone" guard from the script.
All money conversions use the security's PriceStep/StepPrice metadata. If the exchange does not expose a contract size, a fallback value of 100000 is applied, which matches the original Forex assumption.
Parameters
| Name |
Description |
CandleType |
Candle subscription used for MACD processing (default: 30 minute timeframe). |
Volume |
Base lot size for the first grid order. |
TakeProfitPips |
Distance in pips for each leg's take-profit (0 disables). |
InitialStopPips |
Base stop distance in pips. The actual stop grows with the number of free grid slots. |
TrailingStopPips |
Trailing stop distance in pips applied after the leg is sufficiently profitable (0 disables). |
MaxTrades |
Maximum number of simultaneous martingale entries. |
LotMultiplier |
Multiplier applied to the volume of each additional grid leg (overridden to 1.5 when MaxTrades > 12). |
GridStepPips |
Minimum adverse price move (in pips) required before opening the next grid entry. |
OrdersToProtect |
Minimum number of active legs before the floating-profit protection can close the latest trade. |
UseMoneyManagement |
Enables dynamic lot calculation based on account equity. |
AccountType |
Selects the risk formula: 0 – Standard (equity / 10,000); 1 – Normal (equity / 100,000); 2 – Nano (equity / 1,000). |
RiskPercent |
Percentage of equity used when money management is enabled. |
ReverseSignal |
Inverts long/short MACD signals. |
FastEmaLength, SlowEmaLength, SignalLength |
MACD periods (12/26/9 by default). |
SignalShift |
Number of closed bars back used for the crossover check (default: 1). |
TradingRangePips |
MACD signal band (in pips) that must be breached before a crossover is accepted. |
UseTimeFilter |
Enables the session guard based on StopHour/StartHour. |
StopHour, StartHour |
Exclusive range that blocks the creation of a new grid when UseTimeFilter is true. |
Money management notes
When UseMoneyManagement is disabled, the base lot (Volume) is used directly. Otherwise the EA calculates the lot size from the current equity using the same formulas as the original EA:
- Account type 0:
Ceil(risk% * equity / 10,000) / 10
- Account type 1:
risk% * equity / 100,000
- Account type 2:
risk% * equity / 1,000
Volumes are normalised with Security.VolumeStep, then capped by Security.MinVolume/MaxVolume.
Execution workflow
- Subscribe to the configured candle stream and feed the MACD indicator through
BindEx.
- On each finished candle, update trailing/stop logic for active legs.
- When the MACD crossover rules fire, ensure the session filter allows trading, the grid direction matches the existing position, and the price has moved by
GridStepPips against the last fill.
- Calculate the next leg volume using the martingale multiplier and send a market order.
- Monitor floating profit; once the protection threshold is reached, close the newest leg and pause until the next candle.
Conversion notes
- All comments have been rewritten in English as required.
- High-level StockSharp API (candles +
BindEx) is used. Direct indicator value access is avoided.
- Floating-profit calculations rely on
PriceStep/StepPrice. For exotic instruments make sure these fields are filled.
- The strategy maintains per-leg state internally to emulate MQL4 order management, because StockSharp aggregates positions by default.
using System;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// 10point3 MACD Grid: EMA crossover with RSI filter and ATR stops.
/// </summary>
public class TenPointThreeMacdGridStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _fastEmaLength;
private readonly StrategyParam<int> _slowEmaLength;
private readonly StrategyParam<int> _rsiLength;
private readonly StrategyParam<int> _atrLength;
private decimal _prevFast;
private decimal _prevSlow;
private decimal _entryPrice;
public TenPointThreeMacdGridStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Timeframe.", "General");
_fastEmaLength = Param(nameof(FastEmaLength), 12)
.SetDisplay("Fast EMA", "Fast EMA period.", "Indicators");
_slowEmaLength = Param(nameof(SlowEmaLength), 26)
.SetDisplay("Slow EMA", "Slow EMA period.", "Indicators");
_rsiLength = Param(nameof(RsiLength), 14)
.SetDisplay("RSI Length", "RSI period.", "Indicators");
_atrLength = Param(nameof(AtrLength), 14)
.SetDisplay("ATR Length", "ATR period.", "Indicators");
}
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public int FastEmaLength { get => _fastEmaLength.Value; set => _fastEmaLength.Value = value; }
public int SlowEmaLength { get => _slowEmaLength.Value; set => _slowEmaLength.Value = value; }
public int RsiLength { get => _rsiLength.Value; set => _rsiLength.Value = value; }
public int AtrLength { get => _atrLength.Value; set => _atrLength.Value = value; }
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_prevFast = 0; _prevSlow = 0; _entryPrice = 0;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_prevFast = 0; _prevSlow = 0; _entryPrice = 0;
var fastEma = new ExponentialMovingAverage { Length = FastEmaLength };
var slowEma = new ExponentialMovingAverage { Length = SlowEmaLength };
var rsi = new RelativeStrengthIndex { Length = RsiLength };
var atr = new AverageTrueRange { Length = AtrLength };
var subscription = SubscribeCandles(CandleType);
subscription.Bind(fastEma, slowEma, rsi, atr, ProcessCandle).Start();
var area = CreateChartArea();
if (area != null) { DrawCandles(area, subscription); DrawIndicator(area, fastEma); DrawIndicator(area, slowEma); DrawOwnTrades(area); }
}
private void ProcessCandle(ICandleMessage candle, decimal fastVal, decimal slowVal, decimal rsiVal, decimal atrVal)
{
if (candle.State != CandleStates.Finished) return;
if (_prevFast == 0 || _prevSlow == 0 || atrVal <= 0) { _prevFast = fastVal; _prevSlow = slowVal; return; }
var close = candle.ClosePrice;
if (Position > 0)
{
if ((fastVal < slowVal && _prevFast >= _prevSlow) || close <= _entryPrice - atrVal * 2m) { SellMarket(); _entryPrice = 0; }
}
else if (Position < 0)
{
if ((fastVal > slowVal && _prevFast <= _prevSlow) || close >= _entryPrice + atrVal * 2m) { BuyMarket(); _entryPrice = 0; }
}
if (Position == 0)
{
if (fastVal > slowVal && _prevFast <= _prevSlow && rsiVal > 50) { _entryPrice = close; BuyMarket(); }
else if (fastVal < slowVal && _prevFast >= _prevSlow && rsiVal < 50) { _entryPrice = close; SellMarket(); }
}
_prevFast = fastVal; _prevSlow = slowVal;
}
}
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, RelativeStrengthIndex, AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
class ten_point_three_macd_grid_strategy(Strategy):
"""EMA crossover with RSI filter and ATR stops."""
def __init__(self):
super(ten_point_three_macd_grid_strategy, self).__init__()
self._fast_ema = self.Param("FastEmaLength", 12).SetDisplay("Fast EMA", "Fast EMA period", "Indicators")
self._slow_ema = self.Param("SlowEmaLength", 26).SetDisplay("Slow EMA", "Slow EMA period", "Indicators")
self._rsi_length = self.Param("RsiLength", 14).SetDisplay("RSI Length", "RSI period", "Indicators")
self._atr_length = self.Param("AtrLength", 14).SetDisplay("ATR Length", "ATR period", "Indicators")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Timeframe", "General")
@property
def CandleType(self): return self._candle_type.Value
@CandleType.setter
def CandleType(self, value): self._candle_type.Value = value
def OnReseted(self):
super(ten_point_three_macd_grid_strategy, self).OnReseted()
self._prev_fast = 0
self._prev_slow = 0
self._entry_price = 0
def OnStarted2(self, time):
super(ten_point_three_macd_grid_strategy, self).OnStarted2(time)
self._prev_fast = 0
self._prev_slow = 0
self._entry_price = 0
fast = ExponentialMovingAverage()
fast.Length = self._fast_ema.Value
slow = ExponentialMovingAverage()
slow.Length = self._slow_ema.Value
rsi = RelativeStrengthIndex()
rsi.Length = self._rsi_length.Value
atr = AverageTrueRange()
atr.Length = self._atr_length.Value
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(fast, slow, rsi, atr, self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, sub)
self.DrawIndicator(area, fast)
self.DrawIndicator(area, slow)
self.DrawOwnTrades(area)
def OnProcess(self, candle, fast_val, slow_val, rsi_val, atr_val):
if candle.State != CandleStates.Finished:
return
if self._prev_fast == 0 or self._prev_slow == 0 or atr_val <= 0:
self._prev_fast = fast_val
self._prev_slow = slow_val
return
close = float(candle.ClosePrice)
if self.Position > 0:
if (fast_val < slow_val and self._prev_fast >= self._prev_slow) or close <= self._entry_price - atr_val * 2:
self.SellMarket()
self._entry_price = 0
elif self.Position < 0:
if (fast_val > slow_val and self._prev_fast <= self._prev_slow) or close >= self._entry_price + atr_val * 2:
self.BuyMarket()
self._entry_price = 0
if self.Position == 0:
if fast_val > slow_val and self._prev_fast <= self._prev_slow and rsi_val > 50:
self._entry_price = close
self.BuyMarket()
elif fast_val < slow_val and self._prev_fast >= self._prev_slow and rsi_val < 50:
self._entry_price = close
self.SellMarket()
self._prev_fast = fast_val
self._prev_slow = slow_val
def CreateClone(self):
return ten_point_three_macd_grid_strategy()