Terminator 策略
概述
Terminator 策略在 StockSharp 高级 API 中复现了 MetaTrader 4 "Terminator v2.0" 智能交易程序的网格马丁结构。策略根据 MACD 斜率判定方向,在行情向持仓不利移动指定点数时分批加仓。仓位通过可选的止损、止盈、移动止损以及安全利润保护规则进行管理,以便在浮动盈利达到目标时锁定收益。
交易逻辑
- 信号生成:在每根已完成的 K 线结束时读取 MACD 主线。如果当前值高于上一根柱体,则判定为多头倾向;如果低于上一根柱体,则判定为空头倾向。
ReverseSignals参数可以反转这种判断。 - 初始入场:当没有持仓并且时间过滤器(
StartYear、StartMonth、EndYear、EndMonth)允许交易时,策略按检测到的方向下单,除非启用了ManualTrading手动模式。 - 马丁加仓:若已存在网格仓位,策略等待价格向不利方向移动
EntryDistancePips点,然后再次入场。每次加仓的手数为上一次的两倍(MaxTrades大于 12 时使用 1.5 倍),直到达到MaxTrades限制。启用UseMoneyManagement后,初始手数可根据账户余额与RiskPercent计算。 - 风险控制:
- 止盈:
TakeProfitPips定义整个篮子的止盈距离。 - 初始止损:
InitialStopPips可为整篮持仓设置初始止损,设置为 0 则禁用。 - 移动止损:当利润至少达到移动距离加一次加仓间距时,
TrailingStopPips会推动止损沿趋势方向移动。 - 账户保护:启用
UseAccountProtection且持仓数量达到MaxTrades - OrdersToProtect时,会把浮动盈利与SecureProfit(若ProtectUsingBalance为真则使用当前账户权益)比较。若超过阈值,则平掉最后一次加仓并禁止继续开仓,以锁定收益。
- 止盈:
- 篮子重置:净持仓归零后会清除所有内部计数,等待下一轮交易机会。
参数
TakeProfitPips:整篮止盈点数。InitialStopPips:初始止损点数(0 表示关闭)。TrailingStopPips:移动止损点数(0 表示关闭)。MaxTrades:允许同时存在的最大马丁加仓次数。EntryDistancePips:每次加仓所需的不利移动点数。SecureProfit:安全利润阈值(货币单位)。UseAccountProtection:启用安全利润保护模块。ProtectUsingBalance:使用当前账户权益作为保护阈值,替代SecureProfit。OrdersToProtect:接近尾声的加仓数量,用于触发保护逻辑。ReverseSignals:反向解释 MACD 斜率。ManualTrading:关闭自动入场,仅保留仓位管理。LotSize:未启用资金管理时的固定手数。UseMoneyManagement:启用资金管理,根据账户余额和RiskPercent计算初始手数。RiskPercent:资金管理使用的风险百分比(基于 100%)。IsStandardAccount:选择标准账户还是迷你账户手数换算。EurUsdPipValue、GbpUsdPipValue、UsdChfPipValue、UsdJpyPipValue、DefaultPipValue:用于换算浮动盈亏的点值。StartYear、StartMonth、EndYear、EndMonth:限制允许开仓的时间范围。CandleType:用于计算信号的 K 线类型。MacdFastLength、MacdSlowLength、MacdSignalLength:MACD 指标参数。
使用说明
- 策略只处理
CandleType指定周期的已完成 K 线。 - 为了贴近原版 EA,请根据交易品种调整点值参数。
ManualTrading开启时仍会执行移动止损和账户保护,方便手动管理头寸。- 原 EA 的其他入场模式依赖自定义指标,因此本转换仅实现 MACD 方案。
转换细节
- 资金管理、网格间距、马丁倍数和安全利润逻辑严格参考 MQ4 源码。
- MT4 中的
AccountProtection与AllSymbolsProtect被映射为UseAccountProtection与ProtectUsingBalance。 ReverseCondition与Manual对应ReverseSignals与ManualTrading。- 止损与移动止损针对整个仓位而非单个订单,与原始 EA 行为一致。
运行步骤
- 在 Visual Studio 中打开解决方案。
- 将策略添加到
StrategyRunner或StrategyConnector。 - 在界面或代码中配置参数。
- 启动策略后,会自动订阅所需的 K 线并执行信号判断。
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()