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>
/// MACD-based reversal strategy that reproduces the FORTRADER AOP pattern.
/// </summary>
public class MacdAoPatternStrategy : Strategy
{
private readonly StrategyParam<int> _takeProfitPips;
private readonly StrategyParam<int> _stopLossPips;
private readonly StrategyParam<decimal> _orderVolume;
private readonly StrategyParam<int> _macdFastPeriod;
private readonly StrategyParam<int> _macdSlowPeriod;
private readonly StrategyParam<int> _macdSignalPeriod;
private readonly StrategyParam<decimal> _bearishExtremeLevel;
private readonly StrategyParam<decimal> _bearishNeutralLevel;
private readonly StrategyParam<decimal> _bullishExtremeLevel;
private readonly StrategyParam<decimal> _bullishNeutralLevel;
private readonly StrategyParam<DataType> _candleType;
private MACD _macd = null!;
private decimal? _macdPrev1;
private decimal? _macdPrev2;
private decimal? _macdPrev3;
private bool _bearishStageArmed;
private bool _bearishTriggerReady;
private bool _bearishSignalPending;
private bool _bullishStageArmed;
private bool _bullishTriggerReady;
private bool _bullishSignalPending;
private decimal? _stopPrice;
private decimal? _takePrice;
/// <summary>
/// Distance to the take-profit level measured in pips.
/// </summary>
public int TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Distance to the stop-loss level measured in pips.
/// </summary>
public int StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Volume used for each market order.
/// </summary>
public decimal OrderVolume
{
get => _orderVolume.Value;
set => _orderVolume.Value = value;
}
/// <summary>
/// Fast EMA period for the MACD indicator.
/// </summary>
public int MacdFastPeriod
{
get => _macdFastPeriod.Value;
set => _macdFastPeriod.Value = value;
}
/// <summary>
/// Slow EMA period for the MACD indicator.
/// </summary>
public int MacdSlowPeriod
{
get => _macdSlowPeriod.Value;
set => _macdSlowPeriod.Value = value;
}
/// <summary>
/// Signal line EMA period for the MACD indicator.
/// </summary>
public int MacdSignalPeriod
{
get => _macdSignalPeriod.Value;
set => _macdSignalPeriod.Value = value;
}
/// <summary>
/// MACD level that arms the bearish setup when the oscillator stretches deeply negative.
/// </summary>
public decimal BearishExtremeLevel
{
get => _bearishExtremeLevel.Value;
set => _bearishExtremeLevel.Value = value;
}
/// <summary>
/// MACD level that confirms the bearish hook back toward the zero line.
/// </summary>
public decimal BearishNeutralLevel
{
get => _bearishNeutralLevel.Value;
set => _bearishNeutralLevel.Value = value;
}
/// <summary>
/// MACD level that arms the bullish setup when the oscillator stretches deeply positive.
/// </summary>
public decimal BullishExtremeLevel
{
get => _bullishExtremeLevel.Value;
set => _bullishExtremeLevel.Value = value;
}
/// <summary>
/// MACD level that confirms the bullish hook back toward the zero line.
/// </summary>
public decimal BullishNeutralLevel
{
get => _bullishNeutralLevel.Value;
set => _bullishNeutralLevel.Value = value;
}
/// <summary>
/// Candle data type processed by the strategy.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of <see cref="MacdAoPatternStrategy"/>.
/// </summary>
public MacdAoPatternStrategy()
{
_takeProfitPips = Param(nameof(TakeProfitPips), 60)
.SetGreaterThanZero()
.SetDisplay("Take Profit (pips)", "Take-profit distance in pips", "Risk")
;
_stopLossPips = Param(nameof(StopLossPips), 70)
.SetGreaterThanZero()
.SetDisplay("Stop Loss (pips)", "Stop-loss distance in pips", "Risk")
;
_orderVolume = Param(nameof(OrderVolume), 0.1m)
.SetGreaterThanZero()
.SetDisplay("Order Volume", "Volume for every market order", "Orders");
_macdFastPeriod = Param(nameof(MacdFastPeriod), 12)
.SetGreaterThanZero()
.SetDisplay("MACD Fast", "Fast EMA length", "Indicators");
_macdSlowPeriod = Param(nameof(MacdSlowPeriod), 26)
.SetGreaterThanZero()
.SetDisplay("MACD Slow", "Slow EMA length", "Indicators");
_macdSignalPeriod = Param(nameof(MacdSignalPeriod), 9)
.SetGreaterThanZero()
.SetDisplay("MACD Signal", "Signal EMA length", "Indicators");
_bearishExtremeLevel = Param(nameof(BearishExtremeLevel), -100m)
.SetDisplay("Bearish Extreme", "Negative MACD level that arms shorts", "Signals");
_bearishNeutralLevel = Param(nameof(BearishNeutralLevel), -30m)
.SetDisplay("Bearish Neutral", "Negative MACD level that confirms the hook", "Signals");
_bullishExtremeLevel = Param(nameof(BullishExtremeLevel), 100m)
.SetDisplay("Bullish Extreme", "Positive MACD level that arms longs", "Signals");
_bullishNeutralLevel = Param(nameof(BullishNeutralLevel), 30m)
.SetDisplay("Bullish Neutral", "Positive MACD level that confirms the hook", "Signals");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Source series for the strategy", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_macdPrev1 = null;
_macdPrev2 = null;
_macdPrev3 = null;
_bearishStageArmed = false;
_bearishTriggerReady = false;
_bearishSignalPending = false;
_bullishStageArmed = false;
_bullishTriggerReady = false;
_bullishSignalPending = false;
_stopPrice = null;
_takePrice = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
Volume = OrderVolume;
_macd = new MACD();
_macd.ShortMa.Length = MacdFastPeriod;
_macd.LongMa.Length = MacdSlowPeriod;
var subscription = SubscribeCandles(CandleType);
subscription.Bind(_macd, ProcessCandle).Start();
}
private void ProcessCandle(ICandleMessage candle, decimal macdLine)
{
if (candle.State != CandleStates.Finished)
return;
// First handle protective exits using the finished candle range.
HandlePositionExit(candle);
if (!_macd.IsFormed)
{
UpdateMacdHistory(macdLine);
return;
}
if (_macdPrev1 is null || _macdPrev2 is null || _macdPrev3 is null)
{
UpdateMacdHistory(macdLine);
return;
}
var macd1 = _macdPrev1.Value;
var macd2 = _macdPrev2.Value;
var macd3 = _macdPrev3.Value;
// --- Bearish sequence --------------------------------------------------
if (macd1 < BearishExtremeLevel && !_bearishStageArmed)
{
// Arm the bearish setup after a deep negative MACD reading.
_bearishStageArmed = true;
}
if (macd1 > BearishNeutralLevel && _bearishStageArmed)
{
// MACD returned toward zero, prepare for the hook confirmation.
_bearishStageArmed = false;
_bearishTriggerReady = true;
}
var bearishHook = _bearishTriggerReady &&
macd1 < macd2 &&
macd2 > macd3 &&
macd1 < BearishNeutralLevel &&
macd2 > BearishNeutralLevel;
if (bearishHook)
{
// Confirm the bearish hook pattern.
_bearishTriggerReady = false;
_bearishSignalPending = true;
}
if (macd1 > 0)
{
// Positive MACD invalidates the bearish scenario.
ResetBearishState();
}
if (_bearishSignalPending && Position <= 0)
{
// Execute the short entry with predefined stop-loss and take-profit.
SellMarket();
var pip = GetPipSize();
var entryPrice = candle.ClosePrice;
_stopPrice = entryPrice + StopLossPips * pip;
_takePrice = entryPrice - TakeProfitPips * pip;
ResetBearishState();
}
// --- Bullish sequence --------------------------------------------------
if (macd1 > BullishExtremeLevel && !_bullishStageArmed)
{
// Arm the bullish setup after a strong positive MACD expansion.
_bullishStageArmed = true;
}
if (macd1 < 0)
{
// Negative MACD cancels the bullish scenario immediately.
ResetBullishState();
}
else if (macd1 < BullishNeutralLevel && _bullishStageArmed)
{
// MACD retraced toward zero, allow the hook confirmation.
_bullishStageArmed = false;
_bullishTriggerReady = true;
}
var bullishHook = _bullishTriggerReady &&
macd1 > macd2 &&
macd2 < macd3 &&
macd1 > BullishNeutralLevel &&
macd2 < BullishNeutralLevel;
if (bullishHook)
{
// Confirm the bullish hook pattern.
_bullishTriggerReady = false;
_bullishSignalPending = true;
}
if (_bullishSignalPending && Position >= 0)
{
// Execute the long entry with the configured targets.
BuyMarket();
var pip = GetPipSize();
var entryPrice = candle.ClosePrice;
_stopPrice = entryPrice - StopLossPips * pip;
_takePrice = entryPrice + TakeProfitPips * pip;
ResetBullishState();
}
UpdateMacdHistory(macdLine);
}
private void HandlePositionExit(ICandleMessage candle)
{
if (Position > 0)
{
var exitVolume = Math.Abs(Position);
if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
{
// Long stop-loss hit inside the finished candle range.
SellMarket();
ResetProtectionLevels();
return;
}
if (_takePrice.HasValue && candle.HighPrice >= _takePrice.Value)
{
// Long take-profit reached.
SellMarket();
ResetProtectionLevels();
}
}
else if (Position < 0)
{
var exitVolume = Math.Abs(Position);
if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
{
// Short stop-loss triggered within the candle.
BuyMarket();
ResetProtectionLevels();
return;
}
if (_takePrice.HasValue && candle.LowPrice <= _takePrice.Value)
{
// Short take-profit reached.
BuyMarket();
ResetProtectionLevels();
}
}
}
private void UpdateMacdHistory(decimal macdValue)
{
_macdPrev3 = _macdPrev2;
_macdPrev2 = _macdPrev1;
_macdPrev1 = macdValue;
}
private void ResetBearishState()
{
_bearishStageArmed = false;
_bearishTriggerReady = false;
_bearishSignalPending = false;
}
private void ResetBullishState()
{
_bullishStageArmed = false;
_bullishTriggerReady = false;
_bullishSignalPending = false;
}
private void ResetProtectionLevels()
{
_stopPrice = null;
_takePrice = null;
}
private decimal GetPipSize()
{
var step = Security?.PriceStep ?? 0.0001m;
var decimals = Security?.Decimals;
if (decimals == 3 || decimals == 5)
return step * 10m;
return step;
}
}
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, Unit, UnitTypes
from StockSharp.Algo.Indicators import MovingAverageConvergenceDivergence as MACD
from StockSharp.Algo.Strategies import Strategy
class macd_ao_pattern_strategy(Strategy):
def __init__(self):
super(macd_ao_pattern_strategy, self).__init__()
self._take_profit_pips = self.Param("TakeProfitPips", 60)
self._stop_loss_pips = self.Param("StopLossPips", 70)
self._order_volume = self.Param("OrderVolume", 0.1)
self._macd_fast_period = self.Param("MacdFastPeriod", 12)
self._macd_slow_period = self.Param("MacdSlowPeriod", 26)
self._macd_signal_period = self.Param("MacdSignalPeriod", 9)
self._bearish_extreme_level = self.Param("BearishExtremeLevel", -100.0)
self._bearish_neutral_level = self.Param("BearishNeutralLevel", -30.0)
self._bullish_extreme_level = self.Param("BullishExtremeLevel", 100.0)
self._bullish_neutral_level = self.Param("BullishNeutralLevel", 30.0)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._macd_prev1 = None
self._macd_prev2 = None
self._macd_prev3 = None
self._bearish_stage_armed = False
self._bearish_trigger_ready = False
self._bearish_signal_pending = False
self._bullish_stage_armed = False
self._bullish_trigger_ready = False
self._bullish_signal_pending = False
self._stop_price = None
self._take_price = None
@property
def TakeProfitPips(self):
return self._take_profit_pips.Value
@TakeProfitPips.setter
def TakeProfitPips(self, value):
self._take_profit_pips.Value = value
@property
def StopLossPips(self):
return self._stop_loss_pips.Value
@StopLossPips.setter
def StopLossPips(self, value):
self._stop_loss_pips.Value = value
@property
def OrderVolume(self):
return self._order_volume.Value
@OrderVolume.setter
def OrderVolume(self, value):
self._order_volume.Value = value
@property
def MacdFastPeriod(self):
return self._macd_fast_period.Value
@MacdFastPeriod.setter
def MacdFastPeriod(self, value):
self._macd_fast_period.Value = value
@property
def MacdSlowPeriod(self):
return self._macd_slow_period.Value
@MacdSlowPeriod.setter
def MacdSlowPeriod(self, value):
self._macd_slow_period.Value = value
@property
def MacdSignalPeriod(self):
return self._macd_signal_period.Value
@MacdSignalPeriod.setter
def MacdSignalPeriod(self, value):
self._macd_signal_period.Value = value
@property
def BearishExtremeLevel(self):
return self._bearish_extreme_level.Value
@BearishExtremeLevel.setter
def BearishExtremeLevel(self, value):
self._bearish_extreme_level.Value = value
@property
def BearishNeutralLevel(self):
return self._bearish_neutral_level.Value
@BearishNeutralLevel.setter
def BearishNeutralLevel(self, value):
self._bearish_neutral_level.Value = value
@property
def BullishExtremeLevel(self):
return self._bullish_extreme_level.Value
@BullishExtremeLevel.setter
def BullishExtremeLevel(self, value):
self._bullish_extreme_level.Value = value
@property
def BullishNeutralLevel(self):
return self._bullish_neutral_level.Value
@BullishNeutralLevel.setter
def BullishNeutralLevel(self, value):
self._bullish_neutral_level.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def _get_pip_size(self):
step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 0.0001
decimals = self.Security.Decimals if self.Security is not None and self.Security.Decimals is not None else 0
if decimals == 3 or decimals == 5:
return step * 10.0
return step
def OnStarted2(self, time):
super(macd_ao_pattern_strategy, self).OnStarted2(time)
self.Volume = self.OrderVolume
self._macd = MACD()
self._macd.ShortMa.Length = self.MacdFastPeriod
self._macd.LongMa.Length = self.MacdSlowPeriod
self._macd_prev1 = None
self._macd_prev2 = None
self._macd_prev3 = None
self._bearish_stage_armed = False
self._bearish_trigger_ready = False
self._bearish_signal_pending = False
self._bullish_stage_armed = False
self._bullish_trigger_ready = False
self._bullish_signal_pending = False
self._stop_price = None
self._take_price = None
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._macd, self.ProcessCandle).Start()
def ProcessCandle(self, candle, macd_line):
if candle.State != CandleStates.Finished:
return
macd_val = float(macd_line)
self._handle_position_exit(candle)
if not self._macd.IsFormed:
self._update_macd_history(macd_val)
return
if self._macd_prev1 is None or self._macd_prev2 is None or self._macd_prev3 is None:
self._update_macd_history(macd_val)
return
macd1 = self._macd_prev1
macd2 = self._macd_prev2
macd3 = self._macd_prev3
bearish_extreme = float(self.BearishExtremeLevel)
bearish_neutral = float(self.BearishNeutralLevel)
bullish_extreme = float(self.BullishExtremeLevel)
bullish_neutral = float(self.BullishNeutralLevel)
# Bearish sequence
if macd1 < bearish_extreme and not self._bearish_stage_armed:
self._bearish_stage_armed = True
if macd1 > bearish_neutral and self._bearish_stage_armed:
self._bearish_stage_armed = False
self._bearish_trigger_ready = True
bearish_hook = (self._bearish_trigger_ready and
macd1 < macd2 and
macd2 > macd3 and
macd1 < bearish_neutral and
macd2 > bearish_neutral)
if bearish_hook:
self._bearish_trigger_ready = False
self._bearish_signal_pending = True
if macd1 > 0:
self._reset_bearish_state()
if self._bearish_signal_pending and self.Position <= 0:
self.SellMarket()
pip = self._get_pip_size()
entry_price = float(candle.ClosePrice)
self._stop_price = entry_price + int(self.StopLossPips) * pip
self._take_price = entry_price - int(self.TakeProfitPips) * pip
self._reset_bearish_state()
# Bullish sequence
if macd1 > bullish_extreme and not self._bullish_stage_armed:
self._bullish_stage_armed = True
if macd1 < 0:
self._reset_bullish_state()
elif macd1 < bullish_neutral and self._bullish_stage_armed:
self._bullish_stage_armed = False
self._bullish_trigger_ready = True
bullish_hook = (self._bullish_trigger_ready and
macd1 > macd2 and
macd2 < macd3 and
macd1 > bullish_neutral and
macd2 < bullish_neutral)
if bullish_hook:
self._bullish_trigger_ready = False
self._bullish_signal_pending = True
if self._bullish_signal_pending and self.Position >= 0:
self.BuyMarket()
pip = self._get_pip_size()
entry_price = float(candle.ClosePrice)
self._stop_price = entry_price - int(self.StopLossPips) * pip
self._take_price = entry_price + int(self.TakeProfitPips) * pip
self._reset_bullish_state()
self._update_macd_history(macd_val)
def _handle_position_exit(self, candle):
if self.Position > 0:
if self._stop_price is not None and float(candle.LowPrice) <= self._stop_price:
self.SellMarket()
self._reset_protection_levels()
return
if self._take_price is not None and float(candle.HighPrice) >= self._take_price:
self.SellMarket()
self._reset_protection_levels()
elif self.Position < 0:
if self._stop_price is not None and float(candle.HighPrice) >= self._stop_price:
self.BuyMarket()
self._reset_protection_levels()
return
if self._take_price is not None and float(candle.LowPrice) <= self._take_price:
self.BuyMarket()
self._reset_protection_levels()
def _update_macd_history(self, macd_value):
self._macd_prev3 = self._macd_prev2
self._macd_prev2 = self._macd_prev1
self._macd_prev1 = macd_value
def _reset_bearish_state(self):
self._bearish_stage_armed = False
self._bearish_trigger_ready = False
self._bearish_signal_pending = False
def _reset_bullish_state(self):
self._bullish_stage_armed = False
self._bullish_trigger_ready = False
self._bullish_signal_pending = False
def _reset_protection_levels(self):
self._stop_price = None
self._take_price = None
def OnReseted(self):
super(macd_ao_pattern_strategy, self).OnReseted()
self._macd_prev1 = None
self._macd_prev2 = None
self._macd_prev3 = None
self._reset_bearish_state()
self._reset_bullish_state()
self._reset_protection_levels()
def CreateClone(self):
return macd_ao_pattern_strategy()