Terminator Strategy
Overview
The Terminator strategy reproduces the grid-based martingale logic from the MetaTrader 4 expert advisor "Terminator v2.0" using the StockSharp high level API. The strategy enters in the direction of the MACD slope and then builds an averaging basket whenever price moves against the position by a configurable number of pips. The basket is managed with optional stop-loss, take-profit, trailing stop and a secure-profit protection rule that can close the last trade when floating profit reaches a target.
Trading Logic
- Signal generation – On each finished candle the strategy evaluates the MACD histogram. When the MACD value increases compared to the previous value a bullish bias is assumed, while a decreasing MACD indicates a bearish bias. A
ReverseSignalsflag can invert the interpretation. - Initial entry – If there are no open trades and the schedule filter (
StartYear,StartMonth,EndYear,EndMonth) allows trading, the strategy submits a market order in the detected direction unlessManualTradingis enabled. - Martingale averaging – When there is an open basket the strategy waits for price to move adversely by
EntryDistancePips. Every additional entry doubles the previous volume (or multiplies it by 1.5 ifMaxTradesis greater than 12) up to theMaxTradeslimit. Position size can also be derived from account balance by enablingUseMoneyManagement. - Risk management –
- Take-profit:
TakeProfitPipsdefines the distance used to position the shared take-profit level. - Initial stop:
InitialStopPipsoptionally sets the initial protective stop for the complete basket. - Trailing stop:
TrailingStopPipsactivates after the basket gains at least the trailing distance plus one spacing step, and then moves the stop in the trade direction. - Account protection: when
UseAccountProtectionis enabled and the number of open trades reachesMaxTrades - OrdersToProtect, the floating profit is compared againstSecureProfit(or the current portfolio value ifProtectUsingBalanceis true). If the threshold is exceeded the last trade is closed to lock in gains and no new entries are allowed until the basket is reset.
- Take-profit:
- Basket reset – When the net position returns to zero all internal counters are cleared, allowing a new trading cycle.
Parameters
| Parameter | Description |
|---|---|
TakeProfitPips |
Distance in pips for the basket take-profit level. |
InitialStopPips |
Initial stop distance in pips. Set to zero to disable. |
TrailingStopPips |
Trailing stop distance in pips. Set to zero to disable. |
MaxTrades |
Maximum number of martingale entries allowed simultaneously. |
EntryDistancePips |
Minimum adverse move required before adding the next trade. |
SecureProfit |
Floating profit threshold used by the protection module. |
UseAccountProtection |
Enables the secure-profit protection block. |
ProtectUsingBalance |
When true the protection threshold equals the current portfolio value instead of SecureProfit. |
OrdersToProtect |
Number of final trades watched by the protection block (mirrors the original "Orders to Protect" input). |
ReverseSignals |
Inverts bullish and bearish MACD signals. |
ManualTrading |
Disables automatic entries while keeping basket management active. |
LotSize |
Fixed lot size when money management is disabled. |
UseMoneyManagement |
Enables balance-based sizing derived from RiskPercent. |
RiskPercent |
Risk percentage (per 100%) applied when money management is active. |
IsStandardAccount |
Toggles between standard and mini lot scaling. |
EurUsdPipValue, GbpUsdPipValue, UsdChfPipValue, UsdJpyPipValue, DefaultPipValue |
Pip value assumptions used to convert pips to currency for the protection rule. |
StartYear, StartMonth, EndYear, EndMonth |
Restrict the time window when new baskets can be opened. |
CandleType |
Timeframe used to build the MACD signal. |
MacdFastLength, MacdSlowLength, MacdSignalLength |
Period settings of the MACD indicator. |
Usage Notes
- The strategy subscribes to the candle type defined by
CandleTypeand only reacts to finished candles. - To mirror the original MT4 behaviour make sure the symbol pip value parameters match your broker specifications.
- When
ManualTradingis enabled you can still manage orders manually; the algorithm will continue trailing stops and enforcing account protection on the open basket. - The implementation focuses on the MACD-based entry method of the original expert advisor because the other modes relied on custom indicators that are not available in StockSharp.
Conversion Details
- Money management, pip spacing, martingale scaling and secure-profit logic follow the original MQ4 code structure.
- The MT4
AccountProtectionandAllSymbolsProtectoptions are combined intoUseAccountProtectionandProtectUsingBalanceparameters. ReverseConditionandManualflags from the source map toReverseSignalsandManualTradingrespectively.- Stop-loss and trailing rules operate on the aggregate basket rather than per order, similar to the source expert advisor behaviour.
How to Run
- Open the solution in Visual Studio.
- Add the strategy to a
StrategyRunnerorStrategyConnectorinstance. - Configure the parameters in the UI or via code.
- Start the strategy; it will automatically subscribe to the specified candle series and begin evaluating signals.
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-based martingale strategy converted from the MetaTrader "Terminator" expert advisor.
/// </summary>
public class TerminatorStrategy : Strategy
{
private readonly StrategyParam<decimal> _takeProfitPips;
private readonly StrategyParam<decimal> _lotSize;
private readonly StrategyParam<decimal> _initialStopPips;
private readonly StrategyParam<decimal> _trailingStopPips;
private readonly StrategyParam<int> _maxTrades;
private readonly StrategyParam<decimal> _entryDistancePips;
private readonly StrategyParam<decimal> _secureProfit;
private readonly StrategyParam<bool> _useAccountProtection;
private readonly StrategyParam<bool> _protectUsingBalance;
private readonly StrategyParam<int> _ordersToProtect;
private readonly StrategyParam<bool> _reverseSignals;
private readonly StrategyParam<bool> _manualTrading;
private readonly StrategyParam<bool> _useMoneyManagement;
private readonly StrategyParam<decimal> _riskPercent;
private readonly StrategyParam<bool> _isStandardAccount;
private readonly StrategyParam<decimal> _eurUsdPipValue;
private readonly StrategyParam<decimal> _gbpUsdPipValue;
private readonly StrategyParam<decimal> _usdChfPipValue;
private readonly StrategyParam<decimal> _usdJpyPipValue;
private readonly StrategyParam<decimal> _defaultPipValue;
private readonly StrategyParam<int> _startYear;
private readonly StrategyParam<int> _startMonth;
private readonly StrategyParam<int> _endYear;
private readonly StrategyParam<int> _endMonth;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _macdFastLength;
private readonly StrategyParam<int> _macdSlowLength;
private readonly StrategyParam<int> _macdSignalLength;
private MovingAverageConvergenceDivergenceSignal _macd;
private decimal? _previousMacd;
private decimal? _previousPreviousMacd;
private decimal _openVolume;
private decimal _averagePrice;
private int _openTrades;
private bool _isLongPosition;
private decimal _lastEntryPrice;
private decimal _lastEntryVolume;
private decimal? _stopLossPrice;
private decimal? _takeProfitPrice;
private decimal _pipSize;
private decimal _pipValue;
private bool _continueOpening;
private Sides? _currentDirection;
private decimal _martingaleBaseVolume;
/// <summary>
/// Initializes a new instance of <see cref="TerminatorStrategy"/>.
/// </summary>
public TerminatorStrategy()
{
_takeProfitPips = Param(nameof(TakeProfitPips), 38m)
.SetDisplay("Take Profit (pips)", "Distance of the take profit for each entry in pips", "Risk")
;
_lotSize = Param(nameof(LotSize), 0.1m)
.SetDisplay("Base Lot Size", "Fixed lot size used when money management is disabled", "Risk")
;
_initialStopPips = Param(nameof(InitialStopPips), 0m)
.SetDisplay("Initial Stop (pips)", "Initial protective stop distance in pips", "Risk")
;
_trailingStopPips = Param(nameof(TrailingStopPips), 0m)
.SetDisplay("Trailing Stop (pips)", "Trailing stop distance that activates after the threshold", "Risk")
;
_maxTrades = Param(nameof(MaxTrades), 1)
.SetGreaterThanZero()
.SetDisplay("Max Trades", "Maximum number of simultaneously open martingale trades", "General")
;
_entryDistancePips = Param(nameof(EntryDistancePips), 18m)
.SetGreaterThanZero()
.SetDisplay("Entry Distance (pips)", "Minimum adverse movement required before adding a new position", "General")
;
_secureProfit = Param(nameof(SecureProfit), 10m)
.SetDisplay("Secure Profit", "Floating profit in currency units required to protect the account", "Risk")
;
_useAccountProtection = Param(nameof(UseAccountProtection), true)
.SetDisplay("Use Account Protection", "Enable partial liquidation when floating profit exceeds the threshold", "Risk");
_protectUsingBalance = Param(nameof(ProtectUsingBalance), false)
.SetDisplay("Protect Using Balance", "Use the current account value instead of Secure Profit as the protection threshold", "Risk");
_ordersToProtect = Param(nameof(OrdersToProtect), 3)
.SetGreaterThanZero()
.SetDisplay("Orders To Protect", "Number of final trades protected by the secure profit rule", "Risk")
;
_reverseSignals = Param(nameof(ReverseSignals), false)
.SetDisplay("Reverse Signals", "Reverse the MACD slope interpretation", "Filters");
_manualTrading = Param(nameof(ManualTrading), false)
.SetDisplay("Manual Trading", "Disable automatic entries while keeping trade management active", "General");
_useMoneyManagement = Param(nameof(UseMoneyManagement), false)
.SetDisplay("Use Money Management", "Enable balance-based position sizing", "Risk");
_riskPercent = Param(nameof(RiskPercent), 1m)
.SetGreaterThanZero()
.SetDisplay("Risk Percent", "Risk percentage used to derive the base lot size", "Risk")
;
_isStandardAccount = Param(nameof(IsStandardAccount), false)
.SetDisplay("Standard Account", "Use standard lot calculations instead of mini account scaling", "Risk");
_eurUsdPipValue = Param(nameof(EurUsdPipValue), 10m)
.SetDisplay("EURUSD Pip Value", "Monetary value of one pip for EURUSD", "Currency")
;
_gbpUsdPipValue = Param(nameof(GbpUsdPipValue), 10m)
.SetDisplay("GBPUSD Pip Value", "Monetary value of one pip for GBPUSD", "Currency")
;
_usdChfPipValue = Param(nameof(UsdChfPipValue), 8.7m)
.SetDisplay("USDCHF Pip Value", "Monetary value of one pip for USDCHF", "Currency")
;
_usdJpyPipValue = Param(nameof(UsdJpyPipValue), 9.715m)
.SetDisplay("USDJPY Pip Value", "Monetary value of one pip for USDJPY", "Currency")
;
_defaultPipValue = Param(nameof(DefaultPipValue), 5m)
.SetDisplay("Default Pip Value", "Fallback pip value used for other symbols", "Currency")
;
_startYear = Param(nameof(StartYear), 2005)
.SetDisplay("Start Year", "First year when new trades are allowed", "Schedule")
;
_startMonth = Param(nameof(StartMonth), 1)
.SetDisplay("Start Month", "First month when new trades are allowed", "Schedule")
;
_endYear = Param(nameof(EndYear), 2030)
.SetDisplay("End Year", "Last year when new trades are allowed", "Schedule")
;
_endMonth = Param(nameof(EndMonth), 12)
.SetDisplay("End Month", "Last month when new trades are allowed", "Schedule")
;
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Timeframe used for signal generation", "General");
_macdFastLength = Param(nameof(MacdFastLength), 14)
.SetGreaterThanZero()
.SetDisplay("MACD Fast", "Fast EMA period used in MACD", "Filters")
;
_macdSlowLength = Param(nameof(MacdSlowLength), 26)
.SetGreaterThanZero()
.SetDisplay("MACD Slow", "Slow EMA period used in MACD", "Filters")
;
_macdSignalLength = Param(nameof(MacdSignalLength), 9)
.SetGreaterThanZero()
.SetDisplay("MACD Signal", "Signal EMA period used in MACD", "Filters")
;
}
/// <summary>
/// Take profit distance expressed in pips.
/// </summary>
public decimal TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Fixed lot size when money management is disabled.
/// </summary>
public decimal LotSize
{
get => _lotSize.Value;
set => _lotSize.Value = value;
}
/// <summary>
/// Initial protective stop distance in pips.
/// </summary>
public decimal InitialStopPips
{
get => _initialStopPips.Value;
set => _initialStopPips.Value = value;
}
/// <summary>
/// Trailing stop distance expressed in pips.
/// </summary>
public decimal TrailingStopPips
{
get => _trailingStopPips.Value;
set => _trailingStopPips.Value = value;
}
/// <summary>
/// Maximum number of averaging trades allowed.
/// </summary>
public int MaxTrades
{
get => _maxTrades.Value;
set => _maxTrades.Value = value;
}
/// <summary>
/// Minimum adverse move required to add a new position.
/// </summary>
public decimal EntryDistancePips
{
get => _entryDistancePips.Value;
set => _entryDistancePips.Value = value;
}
/// <summary>
/// Floating profit threshold used by the protection routine.
/// </summary>
public decimal SecureProfit
{
get => _secureProfit.Value;
set => _secureProfit.Value = value;
}
/// <summary>
/// Enable or disable the account protection block.
/// </summary>
public bool UseAccountProtection
{
get => _useAccountProtection.Value;
set => _useAccountProtection.Value = value;
}
/// <summary>
/// Use the portfolio value instead of the SecureProfit parameter when protecting.
/// </summary>
public bool ProtectUsingBalance
{
get => _protectUsingBalance.Value;
set => _protectUsingBalance.Value = value;
}
/// <summary>
/// Number of last trades considered when calculating secure profit.
/// </summary>
public int OrdersToProtect
{
get => _ordersToProtect.Value;
set => _ordersToProtect.Value = value;
}
/// <summary>
/// Reverse the MACD slope interpretation.
/// </summary>
public bool ReverseSignals
{
get => _reverseSignals.Value;
set => _reverseSignals.Value = value;
}
/// <summary>
/// Disable automatic entries while still managing open positions.
/// </summary>
public bool ManualTrading
{
get => _manualTrading.Value;
set => _manualTrading.Value = value;
}
/// <summary>
/// Enable balance based position sizing.
/// </summary>
public bool UseMoneyManagement
{
get => _useMoneyManagement.Value;
set => _useMoneyManagement.Value = value;
}
/// <summary>
/// Risk percentage used when money management is enabled.
/// </summary>
public decimal RiskPercent
{
get => _riskPercent.Value;
set => _riskPercent.Value = value;
}
/// <summary>
/// Indicates whether the account is standard (true) or mini (false).
/// </summary>
public bool IsStandardAccount
{
get => _isStandardAccount.Value;
set => _isStandardAccount.Value = value;
}
/// <summary>
/// Pip value for EURUSD.
/// </summary>
public decimal EurUsdPipValue
{
get => _eurUsdPipValue.Value;
set => _eurUsdPipValue.Value = value;
}
/// <summary>
/// Pip value for GBPUSD.
/// </summary>
public decimal GbpUsdPipValue
{
get => _gbpUsdPipValue.Value;
set => _gbpUsdPipValue.Value = value;
}
/// <summary>
/// Pip value for USDCHF.
/// </summary>
public decimal UsdChfPipValue
{
get => _usdChfPipValue.Value;
set => _usdChfPipValue.Value = value;
}
/// <summary>
/// Pip value for USDJPY.
/// </summary>
public decimal UsdJpyPipValue
{
get => _usdJpyPipValue.Value;
set => _usdJpyPipValue.Value = value;
}
/// <summary>
/// Default pip value used for other symbols.
/// </summary>
public decimal DefaultPipValue
{
get => _defaultPipValue.Value;
set => _defaultPipValue.Value = value;
}
/// <summary>
/// First year when new trades are allowed.
/// </summary>
public int StartYear
{
get => _startYear.Value;
set => _startYear.Value = value;
}
/// <summary>
/// First month when new trades are allowed.
/// </summary>
public int StartMonth
{
get => _startMonth.Value;
set => _startMonth.Value = value;
}
/// <summary>
/// Last year when new trades are allowed.
/// </summary>
public int EndYear
{
get => _endYear.Value;
set => _endYear.Value = value;
}
/// <summary>
/// Last month when new trades are allowed.
/// </summary>
public int EndMonth
{
get => _endMonth.Value;
set => _endMonth.Value = value;
}
/// <summary>
/// Timeframe used for signal generation.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Fast EMA length of the MACD indicator.
/// </summary>
public int MacdFastLength
{
get => _macdFastLength.Value;
set => _macdFastLength.Value = value;
}
/// <summary>
/// Slow EMA length of the MACD indicator.
/// </summary>
public int MacdSlowLength
{
get => _macdSlowLength.Value;
set => _macdSlowLength.Value = value;
}
/// <summary>
/// Signal EMA length of the MACD indicator.
/// </summary>
public int MacdSignalLength
{
get => _macdSignalLength.Value;
set => _macdSignalLength.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_macd = null;
_previousMacd = null;
_previousPreviousMacd = null;
_openVolume = 0m;
_averagePrice = 0m;
_openTrades = 0;
_isLongPosition = false;
_lastEntryPrice = 0m;
_lastEntryVolume = 0m;
_stopLossPrice = null;
_takeProfitPrice = null;
_pipSize = 0m;
_pipValue = 0m;
_continueOpening = false;
_currentDirection = null;
_martingaleBaseVolume = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
// Determine pip size for price to pip conversions.
_pipSize = Security?.PriceStep ?? 0m;
if (_pipSize <= 0m)
_pipSize = 0.0001m;
// Cache pip value for floating profit calculations.
_pipValue = DeterminePipValue();
_martingaleBaseVolume = CalculateBaseVolume();
_macd = new MovingAverageConvergenceDivergenceSignal
{
Macd =
{
ShortMa = { Length = MacdFastLength },
LongMa = { Length = MacdSlowLength },
},
SignalMa = { Length = MacdSignalLength }
};
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(_macd, ProcessCandle)
.Start();
// Enable built-in position protection monitoring.
StartProtection(null, null);
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue indicatorValue)
{
if (candle.State != CandleStates.Finished)
return;
if (indicatorValue is not MovingAverageConvergenceDivergenceSignalValue macdValue)
return;
var macdMain = macdValue.Macd;
var previousMacd = _previousMacd;
var previousPreviousMacd = _previousPreviousMacd;
_previousPreviousMacd = previousMacd;
_previousMacd = macdMain;
var time = candle.CloseTime;
if (!IsTradingWindowOpen(time))
return;
var currentPrice = candle.ClosePrice;
// Manage existing basket before looking for new entries.
if (_openTrades > 0)
{
ManageOpenPosition(currentPrice);
if (_openTrades == 0)
return;
}
_continueOpening = _openTrades < MaxTrades;
if (!_continueOpening)
return;
// Respect manual mode by skipping automatic entries.
if (ManualTrading)
return;
if (_openTrades == 0)
{
_currentDirection = DetermineDirection(previousMacd, previousPreviousMacd);
if (_currentDirection.HasValue)
TryOpenPosition(_currentDirection.Value, currentPrice);
}
else if (_currentDirection.HasValue)
{
TryAddPosition(_currentDirection.Value, currentPrice);
}
}
private void ManageOpenPosition(decimal currentPrice)
{
if (_openVolume <= 0m)
return;
// Exit immediately if price hits the protective stop.
if (_stopLossPrice.HasValue)
{
if (_isLongPosition && currentPrice <= _stopLossPrice.Value)
{
SellMarket();
return;
}
if (!_isLongPosition && currentPrice >= _stopLossPrice.Value)
{
BuyMarket();
return;
}
}
// Take profit closes the entire basket.
if (_takeProfitPrice.HasValue)
{
if (_isLongPosition && currentPrice >= _takeProfitPrice.Value)
{
SellMarket();
return;
}
if (!_isLongPosition && currentPrice <= _takeProfitPrice.Value)
{
BuyMarket();
return;
}
}
if (TrailingStopPips > 0m)
UpdateTrailingStop(currentPrice);
if (UseAccountProtection && _openTrades >= Math.Max(1, MaxTrades - OrdersToProtect))
{
var profit = CalculateFloatingProfit(currentPrice);
var threshold = ProtectUsingBalance ? (Portfolio?.CurrentValue ?? 0m) : SecureProfit;
if (profit >= threshold && _lastEntryVolume > 0m)
{
if (_isLongPosition)
SellMarket();
else
BuyMarket();
_continueOpening = false;
}
}
}
private void UpdateTrailingStop(decimal currentPrice)
{
var trailingDistance = ToPrice(TrailingStopPips);
var threshold = trailingDistance + ToPrice(EntryDistancePips);
if (_isLongPosition)
{
var profit = currentPrice - _averagePrice;
if (profit >= threshold)
{
var newStop = currentPrice - trailingDistance;
if (!_stopLossPrice.HasValue || newStop > _stopLossPrice.Value)
_stopLossPrice = newStop;
}
}
else
{
var profit = _averagePrice - currentPrice;
if (profit >= threshold)
{
var newStop = currentPrice + trailingDistance;
if (!_stopLossPrice.HasValue || newStop < _stopLossPrice.Value)
_stopLossPrice = newStop;
}
}
}
private void TryOpenPosition(Sides direction, decimal currentPrice)
{
var volume = CalculateNextVolume();
if (volume <= 0m)
return;
if (direction == Sides.Buy)
BuyMarket();
else if (direction == Sides.Sell)
SellMarket();
}
private void TryAddPosition(Sides direction, decimal currentPrice)
{
var distance = ToPrice(EntryDistancePips);
var canAdd = direction == Sides.Buy
? (_lastEntryPrice - currentPrice) >= distance
: (currentPrice - _lastEntryPrice) >= distance;
if (!canAdd)
return;
TryOpenPosition(direction, currentPrice);
}
private Sides? DetermineDirection(decimal? macdPrev, decimal? macdPrevPrev)
{
if (!macdPrev.HasValue || !macdPrevPrev.HasValue)
return null;
var isBullish = macdPrev.Value > macdPrevPrev.Value;
var isBearish = macdPrev.Value < macdPrevPrev.Value;
if (!isBullish && !isBearish)
return null;
if (ReverseSignals)
return isBullish ? Sides.Sell : Sides.Buy;
return isBullish ? Sides.Buy : Sides.Sell;
}
private bool IsTradingWindowOpen(DateTimeOffset time)
{
if (_openTrades > 0)
return true;
if (time.Year < StartYear)
return false;
if (time.Year == StartYear && time.Month < StartMonth)
return false;
if (time.Year > EndYear)
return false;
if (time.Year == EndYear && time.Month > EndMonth)
return false;
return true;
}
private decimal CalculateFloatingProfit(decimal currentPrice)
{
if (_openVolume <= 0m || _pipSize <= 0m)
return 0m;
var profitPips = _isLongPosition
? (currentPrice - _averagePrice) / _pipSize * _openVolume
: (_averagePrice - currentPrice) / _pipSize * _openVolume;
return profitPips * _pipValue;
}
private decimal CalculateBaseVolume()
{
var volume = LotSize;
if (UseMoneyManagement)
{
var balance = Portfolio?.CurrentValue ?? 0m;
if (balance > 0m)
{
var riskValue = balance * RiskPercent / 100m;
var rounded = Math.Ceiling(riskValue);
volume = IsStandardAccount ? rounded : rounded / 10m;
}
}
if (volume > 100m)
volume = 100m;
return volume;
}
private decimal CalculateNextVolume()
{
var volume = _martingaleBaseVolume > 0m ? _martingaleBaseVolume : CalculateBaseVolume();
if (_openTrades > 0)
{
for (var i = 0; i < _openTrades; i++)
{
volume = MaxTrades > 12
? Math.Round(volume * 1.5m, 2, MidpointRounding.AwayFromZero)
: Math.Round(volume * 2m, 2, MidpointRounding.AwayFromZero);
}
}
if (volume > 100m)
volume = 100m;
return volume;
}
private decimal DeterminePipValue()
{
var code = Security?.Code?.ToUpperInvariant();
return code switch
{
"EURUSD" => EurUsdPipValue,
"GBPUSD" => GbpUsdPipValue,
"USDCHF" => UsdChfPipValue,
"USDJPY" => UsdJpyPipValue,
_ => DefaultPipValue,
};
}
private decimal ToPrice(decimal pips)
{
return pips * _pipSize;
}
private void ResetPositionState()
{
_openVolume = 0m;
_averagePrice = 0m;
_openTrades = 0;
_stopLossPrice = null;
_takeProfitPrice = null;
_lastEntryPrice = 0m;
_lastEntryVolume = 0m;
_continueOpening = true;
_currentDirection = null;
}
private decimal? UpdateStopAfterEntry(bool isLong, decimal price)
{
if (InitialStopPips <= 0m)
return _stopLossPrice;
var stopOffset = ToPrice(InitialStopPips);
if (isLong)
{
var candidate = price - stopOffset;
return !_stopLossPrice.HasValue || candidate < _stopLossPrice.Value ? candidate : _stopLossPrice;
}
var candidateShort = price + stopOffset;
return !_stopLossPrice.HasValue || candidateShort > _stopLossPrice.Value ? candidateShort : _stopLossPrice;
}
private decimal? UpdateTakeProfitAfterEntry(bool isLong, decimal price)
{
if (TakeProfitPips <= 0m)
return _takeProfitPrice;
var takeOffset = ToPrice(TakeProfitPips);
if (isLong)
{
var candidate = price + takeOffset;
return !_takeProfitPrice.HasValue || candidate > _takeProfitPrice.Value ? candidate : _takeProfitPrice;
}
var candidateShort = price - takeOffset;
return !_takeProfitPrice.HasValue || candidateShort < _takeProfitPrice.Value ? candidateShort : _takeProfitPrice;
}
/// <inheritdoc />
protected override void OnOwnTradeReceived(MyTrade trade)
{
base.OnOwnTradeReceived(trade);
if (trade.Order == null)
return;
var volume = trade.Trade.Volume;
var price = trade.Trade.Price;
var side = trade.Order.Side;
if (side == Sides.Buy)
{
if (_openVolume > 0m && !_isLongPosition)
{
HandlePositionReduction(volume);
return;
}
var newVolume = _openVolume + volume;
_averagePrice = newVolume == 0m ? 0m : (_averagePrice * _openVolume + price * volume) / newVolume;
_openVolume = newVolume;
_isLongPosition = true;
_openTrades++;
_lastEntryPrice = price;
_lastEntryVolume = volume;
_stopLossPrice = UpdateStopAfterEntry(true, price);
_takeProfitPrice = UpdateTakeProfitAfterEntry(true, price);
_martingaleBaseVolume = CalculateBaseVolume();
}
else if (side == Sides.Sell)
{
if (_openVolume > 0m && _isLongPosition)
{
HandlePositionReduction(volume);
return;
}
var newVolume = _openVolume + volume;
_averagePrice = newVolume == 0m ? 0m : (_averagePrice * _openVolume + price * volume) / newVolume;
_openVolume = newVolume;
_isLongPosition = false;
_openTrades++;
_lastEntryPrice = price;
_lastEntryVolume = volume;
_stopLossPrice = UpdateStopAfterEntry(false, price);
_takeProfitPrice = UpdateTakeProfitAfterEntry(false, price);
_martingaleBaseVolume = CalculateBaseVolume();
}
_continueOpening = _openTrades < MaxTrades;
}
private void HandlePositionReduction(decimal volume)
{
var closingVolume = Math.Min(_openVolume, volume);
_openVolume -= closingVolume;
if (_openVolume <= 0m)
ResetPositionState();
else if (_openTrades > 0)
_openTrades--;
}
}
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
from StockSharp.Algo.Indicators import MovingAverageConvergenceDivergenceSignal
from StockSharp.Algo.Strategies import Strategy
# Direction constants
SIDE_BUY = 0
SIDE_SELL = 1
class terminator_strategy(Strategy):
"""Grid-based martingale strategy using MACD slope for direction.
Manages averaging entries with increasing lot sizes and protective stops."""
def __init__(self):
super(terminator_strategy, self).__init__()
self._take_profit_pips = self.Param("TakeProfitPips", 38.0) \
.SetDisplay("Take Profit (pips)", "Distance of the take profit for each entry in pips", "Risk")
self._lot_size = self.Param("LotSize", 0.1) \
.SetDisplay("Base Lot Size", "Fixed lot size when money management is disabled", "Risk")
self._initial_stop_pips = self.Param("InitialStopPips", 0.0) \
.SetDisplay("Initial Stop (pips)", "Initial protective stop distance in pips", "Risk")
self._trailing_stop_pips = self.Param("TrailingStopPips", 0.0) \
.SetDisplay("Trailing Stop (pips)", "Trailing stop distance", "Risk")
self._max_trades = self.Param("MaxTrades", 1) \
.SetGreaterThanZero() \
.SetDisplay("Max Trades", "Maximum simultaneous martingale trades", "General")
self._entry_distance_pips = self.Param("EntryDistancePips", 18.0) \
.SetGreaterThanZero() \
.SetDisplay("Entry Distance (pips)", "Adverse move required before adding a position", "General")
self._reverse_signals = self.Param("ReverseSignals", False) \
.SetDisplay("Reverse Signals", "Reverse the MACD slope interpretation", "Filters")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Timeframe used for signal generation", "General")
self._macd_fast_length = self.Param("MacdFastLength", 14) \
.SetGreaterThanZero() \
.SetDisplay("MACD Fast", "Fast EMA period used in MACD", "Filters")
self._macd_slow_length = self.Param("MacdSlowLength", 26) \
.SetGreaterThanZero() \
.SetDisplay("MACD Slow", "Slow EMA period used in MACD", "Filters")
self._macd_signal_length = self.Param("MacdSignalLength", 9) \
.SetGreaterThanZero() \
.SetDisplay("MACD Signal", "Signal EMA period used in MACD", "Filters")
self._previous_macd = None
self._previous_previous_macd = None
self._open_trades = 0
self._is_long_position = False
self._last_entry_price = 0.0
self._average_price = 0.0
self._open_volume = 0.0
self._stop_loss_price = None
self._take_profit_price = None
self._pip_size = 0.0
self._current_direction = None
self._continue_opening = True
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def TakeProfitPips(self):
return self._take_profit_pips.Value
@property
def LotSize(self):
return self._lot_size.Value
@property
def InitialStopPips(self):
return self._initial_stop_pips.Value
@property
def TrailingStopPips(self):
return self._trailing_stop_pips.Value
@property
def MaxTrades(self):
return self._max_trades.Value
@property
def EntryDistancePips(self):
return self._entry_distance_pips.Value
@property
def ReverseSignals(self):
return self._reverse_signals.Value
@property
def MacdFastLength(self):
return self._macd_fast_length.Value
@property
def MacdSlowLength(self):
return self._macd_slow_length.Value
@property
def MacdSignalLength(self):
return self._macd_signal_length.Value
def OnReseted(self):
super(terminator_strategy, self).OnReseted()
self._previous_macd = None
self._previous_previous_macd = None
self._open_trades = 0
self._is_long_position = False
self._last_entry_price = 0.0
self._average_price = 0.0
self._open_volume = 0.0
self._stop_loss_price = None
self._take_profit_price = None
self._pip_size = 0.0
self._current_direction = None
self._continue_opening = True
def _to_price(self, pips):
return float(pips) * self._pip_size
def OnStarted2(self, time):
super(terminator_strategy, self).OnStarted2(time)
step = self.Security.PriceStep if self.Security is not None else 0.0
if step is None or float(step) <= 0:
step = 0.0001
self._pip_size = float(step)
macd = MovingAverageConvergenceDivergenceSignal()
macd.Macd.ShortMa.Length = self.MacdFastLength
macd.Macd.LongMa.Length = self.MacdSlowLength
macd.SignalMa.Length = self.MacdSignalLength
subscription = self.SubscribeCandles(self.CandleType)
subscription.BindEx(macd, self._process_candle).Start()
def _process_candle(self, candle, indicator_value):
if candle.State != CandleStates.Finished:
return
macd_raw = indicator_value.Macd if hasattr(indicator_value, 'Macd') else None
if macd_raw is None:
return
macd_main = float(macd_raw)
prev_macd = self._previous_macd
prev_prev_macd = self._previous_previous_macd
self._previous_previous_macd = prev_macd
self._previous_macd = macd_main
current_price = float(candle.ClosePrice)
# Manage existing basket
if self._open_trades > 0:
self._manage_open_position(current_price)
if self._open_trades == 0:
return
self._continue_opening = self._open_trades < self.MaxTrades
if not self._continue_opening:
return
if self._open_trades == 0:
direction = self._determine_direction(prev_macd, prev_prev_macd)
if direction is not None:
self._current_direction = direction
self._try_open_position(direction, current_price)
elif self._current_direction is not None:
self._try_add_position(self._current_direction, current_price)
def _determine_direction(self, macd_prev, macd_prev_prev):
if macd_prev is None or macd_prev_prev is None:
return None
is_bullish = macd_prev > macd_prev_prev
is_bearish = macd_prev < macd_prev_prev
if not is_bullish and not is_bearish:
return None
if self.ReverseSignals:
return SIDE_SELL if is_bullish else SIDE_BUY
return SIDE_BUY if is_bullish else SIDE_SELL
def _try_open_position(self, direction, current_price):
if direction == SIDE_BUY:
self.BuyMarket()
self._record_entry(True, current_price)
elif direction == SIDE_SELL:
self.SellMarket()
self._record_entry(False, current_price)
def _record_entry(self, is_long, price):
vol = float(self.LotSize)
new_volume = self._open_volume + vol
if new_volume > 0:
self._average_price = (self._average_price * self._open_volume + price * vol) / new_volume
self._open_volume = new_volume
self._is_long_position = is_long
self._open_trades += 1
self._last_entry_price = price
# Update stop
if float(self.InitialStopPips) > 0:
stop_offset = self._to_price(self.InitialStopPips)
if is_long:
candidate = price - stop_offset
if self._stop_loss_price is None or candidate < self._stop_loss_price:
self._stop_loss_price = candidate
else:
candidate = price + stop_offset
if self._stop_loss_price is None or candidate > self._stop_loss_price:
self._stop_loss_price = candidate
# Update take profit
if float(self.TakeProfitPips) > 0:
tp_offset = self._to_price(self.TakeProfitPips)
if is_long:
candidate = price + tp_offset
if self._take_profit_price is None or candidate > self._take_profit_price:
self._take_profit_price = candidate
else:
candidate = price - tp_offset
if self._take_profit_price is None or candidate < self._take_profit_price:
self._take_profit_price = candidate
self._continue_opening = self._open_trades < self.MaxTrades
def _try_add_position(self, direction, current_price):
distance = self._to_price(self.EntryDistancePips)
if direction == SIDE_BUY:
can_add = (self._last_entry_price - current_price) >= distance
else:
can_add = (current_price - self._last_entry_price) >= distance
if not can_add:
return
self._try_open_position(direction, current_price)
def _manage_open_position(self, current_price):
if self._open_volume <= 0:
return
# Check stop loss
if self._stop_loss_price is not None:
if self._is_long_position and current_price <= self._stop_loss_price:
self.SellMarket()
self._reset_position_state()
return
if not self._is_long_position and current_price >= self._stop_loss_price:
self.BuyMarket()
self._reset_position_state()
return
# Check take profit
if self._take_profit_price is not None:
if self._is_long_position and current_price >= self._take_profit_price:
self.SellMarket()
self._reset_position_state()
return
if not self._is_long_position and current_price <= self._take_profit_price:
self.BuyMarket()
self._reset_position_state()
return
# Trailing stop
if float(self.TrailingStopPips) > 0:
self._update_trailing_stop(current_price)
def _update_trailing_stop(self, current_price):
trailing_distance = self._to_price(self.TrailingStopPips)
threshold = trailing_distance + self._to_price(self.EntryDistancePips)
if self._is_long_position:
profit = current_price - self._average_price
if profit >= threshold:
new_stop = current_price - trailing_distance
if self._stop_loss_price is None or new_stop > self._stop_loss_price:
self._stop_loss_price = new_stop
else:
profit = self._average_price - current_price
if profit >= threshold:
new_stop = current_price + trailing_distance
if self._stop_loss_price is None or new_stop < self._stop_loss_price:
self._stop_loss_price = new_stop
def _reset_position_state(self):
self._open_volume = 0.0
self._average_price = 0.0
self._open_trades = 0
self._stop_loss_price = None
self._take_profit_price = None
self._last_entry_price = 0.0
self._continue_opening = True
self._current_direction = None
def CreateClone(self):
return terminator_strategy()