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 cross strategy with zero line filter and fixed take profit.
/// </summary>
public class MacdZeroFilterTakeProfitStrategy : Strategy
{
private readonly StrategyParam<int> _macdFast;
private readonly StrategyParam<int> _macdSlow;
private readonly StrategyParam<int> _macdSignal;
private readonly StrategyParam<int> _takeProfitPoints;
private readonly StrategyParam<decimal> _volumePerTrade;
private readonly StrategyParam<decimal> _minimumCapitalPerVolume;
private readonly StrategyParam<DataType> _candleType;
private MovingAverageConvergenceDivergenceSignal _macd = null!;
private decimal? _previousMacd;
private decimal? _previousSignal;
/// <summary>
/// Initializes a new instance of the <see cref="MacdZeroFilterTakeProfitStrategy"/> class.
/// </summary>
public MacdZeroFilterTakeProfitStrategy()
{
_macdFast = Param(nameof(MacdFast), 12)
.SetGreaterThanZero()
.SetDisplay("MACD Fast Period", "Fast EMA period used by MACD", "Indicators")
.SetOptimize(8, 20, 2);
_macdSlow = Param(nameof(MacdSlow), 26)
.SetGreaterThanZero()
.SetDisplay("MACD Slow Period", "Slow EMA period used by MACD", "Indicators")
.SetOptimize(20, 40, 2);
_macdSignal = Param(nameof(MacdSignal), 9)
.SetGreaterThanZero()
.SetDisplay("MACD Signal Period", "Signal smoothing period for MACD", "Indicators")
.SetOptimize(6, 15, 1);
_takeProfitPoints = Param(nameof(TakeProfitPoints), 300)
.SetGreaterThanZero()
.SetDisplay("Take Profit (points)", "Take profit distance expressed in price points", "Risk Management")
.SetOptimize(100, 600, 50);
_volumePerTrade = Param(nameof(VolumePerTrade), 1m)
.SetGreaterThanZero()
.SetDisplay("Trade Volume", "Number of lots to trade on each entry", "General")
.SetOptimize(0.5m, 5m, 0.5m);
_minimumCapitalPerVolume = Param(nameof(MinimumCapitalPerVolume), 1000m)
.SetGreaterThanZero()
.SetDisplay("Capital per Volume", "Minimum portfolio value required per traded lot", "Risk Management")
.SetOptimize(500m, 5000m, 500m);
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Timeframe for MACD calculations", "General");
}
/// <summary>
/// Fast EMA period used by MACD.
/// </summary>
public int MacdFast
{
get => _macdFast.Value;
set => _macdFast.Value = value;
}
/// <summary>
/// Slow EMA period used by MACD.
/// </summary>
public int MacdSlow
{
get => _macdSlow.Value;
set => _macdSlow.Value = value;
}
/// <summary>
/// Signal smoothing period for MACD.
/// </summary>
public int MacdSignal
{
get => _macdSignal.Value;
set => _macdSignal.Value = value;
}
/// <summary>
/// Take profit distance expressed in price points.
/// </summary>
public int TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Number of lots to trade on each entry.
/// </summary>
public decimal VolumePerTrade
{
get => _volumePerTrade.Value;
set => _volumePerTrade.Value = value;
}
/// <summary>
/// Minimum portfolio value required per traded lot.
/// </summary>
public decimal MinimumCapitalPerVolume
{
get => _minimumCapitalPerVolume.Value;
set => _minimumCapitalPerVolume.Value = value;
}
/// <summary>
/// Timeframe for MACD calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_previousMacd = null;
_previousSignal = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
Volume = VolumePerTrade;
_macd = new MovingAverageConvergenceDivergenceSignal
{
Macd =
{
ShortMa = { Length = MacdFast },
LongMa = { Length = MacdSlow },
},
SignalMa = { Length = MacdSignal }
};
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(_macd, ProcessCandle)
.Start();
var step = Security?.PriceStep ?? 1m;
StartProtection(
takeProfit: new Unit(TakeProfitPoints * step, UnitTypes.Absolute),
stopLoss: new Unit(0m),
useMarketOrders: true);
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _macd);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue macdValue)
{
// Use only completed candles to avoid double counting signals.
if (candle.State != CandleStates.Finished)
return;
// Ensure the strategy is ready to trade and has a working connector.
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (macdValue is not MovingAverageConvergenceDivergenceSignalValue macdTyped)
return;
if (macdTyped.Macd is not decimal macd || macdTyped.Signal is not decimal signal)
return;
if (_previousMacd is null || _previousSignal is null)
{
_previousMacd = macd;
_previousSignal = signal;
return;
}
var previousMacd = _previousMacd.Value;
var previousSignal = _previousSignal.Value;
var crossedUp = previousMacd <= previousSignal && macd > signal;
var crossedDown = previousMacd >= previousSignal && macd < signal;
if (Position > 0 && crossedDown)
{
// Close long position on bearish crossover.
SellMarket();
}
else if (Position < 0 && crossedUp)
{
// Close short position on bullish crossover.
BuyMarket();
}
if (Position == 0)
{
var requiredCapital = MinimumCapitalPerVolume * VolumePerTrade;
if (HasEnoughCapital(requiredCapital))
{
if (crossedUp && macd < 0m && signal < 0m)
{
// Enter long when MACD crosses above signal under the zero line.
BuyMarket();
}
else if (crossedDown && macd > 0m && signal > 0m)
{
// Enter short when MACD crosses below signal above the zero line.
SellMarket();
}
}
}
_previousMacd = macd;
_previousSignal = signal;
}
private bool HasEnoughCapital(decimal requiredCapital)
{
var currentValue = Portfolio?.CurrentValue;
if (currentValue is null)
return true;
if (currentValue.Value >= requiredCapital)
return true;
this.LogInfo($"Insufficient capital: available {currentValue.Value:F2}, required {requiredCapital:F2}.");
return false;
}
}
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, Unit, UnitTypes
from StockSharp.Algo.Indicators import MovingAverageConvergenceDivergenceSignal
from StockSharp.Algo.Strategies import Strategy
class macd_zero_filter_take_profit_strategy(Strategy):
def __init__(self):
super(macd_zero_filter_take_profit_strategy, self).__init__()
self._macd_fast = self.Param("MacdFast", 12)
self._macd_slow = self.Param("MacdSlow", 26)
self._macd_signal = self.Param("MacdSignal", 9)
self._take_profit_points = self.Param("TakeProfitPoints", 300)
self._volume_per_trade = self.Param("VolumePerTrade", 1.0)
self._minimum_capital_per_volume = self.Param("MinimumCapitalPerVolume", 1000.0)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4)))
self._previous_macd = None
self._previous_signal = None
@property
def MacdFast(self):
return self._macd_fast.Value
@MacdFast.setter
def MacdFast(self, value):
self._macd_fast.Value = value
@property
def MacdSlow(self):
return self._macd_slow.Value
@MacdSlow.setter
def MacdSlow(self, value):
self._macd_slow.Value = value
@property
def MacdSignal(self):
return self._macd_signal.Value
@MacdSignal.setter
def MacdSignal(self, value):
self._macd_signal.Value = value
@property
def TakeProfitPoints(self):
return self._take_profit_points.Value
@TakeProfitPoints.setter
def TakeProfitPoints(self, value):
self._take_profit_points.Value = value
@property
def VolumePerTrade(self):
return self._volume_per_trade.Value
@VolumePerTrade.setter
def VolumePerTrade(self, value):
self._volume_per_trade.Value = value
@property
def MinimumCapitalPerVolume(self):
return self._minimum_capital_per_volume.Value
@MinimumCapitalPerVolume.setter
def MinimumCapitalPerVolume(self, value):
self._minimum_capital_per_volume.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def OnStarted2(self, time):
super(macd_zero_filter_take_profit_strategy, self).OnStarted2(time)
self._previous_macd = None
self._previous_signal = None
self.Volume = self._volume_per_trade.Value
macd = MovingAverageConvergenceDivergenceSignal()
macd.Macd.ShortMa.Length = self.MacdFast
macd.Macd.LongMa.Length = self.MacdSlow
macd.SignalMa.Length = self.MacdSignal
self._macd_ind = macd
subscription = self.SubscribeCandles(self.CandleType)
subscription.BindEx(macd, self.ProcessCandle).Start()
step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 1.0
if step <= 0.0:
step = 1.0
tp_distance = int(self.TakeProfitPoints) * step
self.StartProtection(
takeProfit=Unit(tp_distance, UnitTypes.Absolute),
stopLoss=Unit(0),
useMarketOrders=True)
def ProcessCandle(self, candle, macd_value):
if candle.State != CandleStates.Finished:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
macd_val = macd_value.Macd
signal_val = macd_value.Signal
if macd_val is None or signal_val is None:
return
macd_f = float(macd_val)
signal_f = float(signal_val)
if self._previous_macd is None or self._previous_signal is None:
self._previous_macd = macd_f
self._previous_signal = signal_f
return
prev_macd = self._previous_macd
prev_signal = self._previous_signal
crossed_up = prev_macd <= prev_signal and macd_f > signal_f
crossed_down = prev_macd >= prev_signal and macd_f < signal_f
if self.Position > 0 and crossed_down:
self.SellMarket()
elif self.Position < 0 and crossed_up:
self.BuyMarket()
if self.Position == 0:
required_capital = float(self.MinimumCapitalPerVolume) * float(self.VolumePerTrade)
portfolio = self.Portfolio
current_value = float(portfolio.CurrentValue) if portfolio is not None and portfolio.CurrentValue is not None else 0.0
has_capital = current_value >= required_capital or portfolio is None or portfolio.CurrentValue is None
if has_capital:
if crossed_up and macd_f < 0.0 and signal_f < 0.0:
self.BuyMarket()
elif crossed_down and macd_f > 0.0 and signal_f > 0.0:
self.SellMarket()
self._previous_macd = macd_f
self._previous_signal = signal_f
def OnReseted(self):
super(macd_zero_filter_take_profit_strategy, self).OnReseted()
self._previous_macd = None
self._previous_signal = None
def CreateClone(self):
return macd_zero_filter_take_profit_strategy()