Estrategia MACD Stochastic
Descripción General
Esta estrategia es un port a StockSharp del sistema MetaTrader 5 "MACD Stochastic". Combina un cruce clásico de MACD con un filtro de confirmación estocástico opcional y opera solo durante tres sesiones intradía configurables. Cada posición usa controles de riesgo basados en pips con lógica de trailing stop opcional que puede mover el stop hacia el punto de equilibrio una vez que la operación ha alcanzado una ganancia especificada.
Indicadores
- MACD (Convergencia/Divergencia de Medias Móviles) – genera las señales primarias de reversión de tendencia siguiendo el cruce entre las medias móviles exponenciales rápida y lenta y su línea de señal.
- Oscilador Stochastic – filtro opcional que confirma las señales del MACD verificando que las líneas %K y %D hayan cruzado recientemente en la misma dirección que la operación.
Lógica de Trading
Entradas Largas
- La línea principal del MACD cruza por encima de la línea de señal y ambas líneas están por debajo de cero, indicando una posible reversión alcista.
- La posición más reciente se abrió en una barra anterior (solo se permite una entrada por barra).
- La hora actual (hora local del instrumento) cae dentro de una de las sesiones de trading configuradas.
- Si el filtro estocástico está habilitado, el valor actual de %K debe estar por encima de %D y el valor de StochasticBarsToCheck barras atrás debe mostrar la relación opuesta (%K por debajo de %D), confirmando un cruce alcista reciente.
Entradas Cortas
- La línea principal del MACD cruza por debajo de la línea de señal y ambas líneas están por encima de cero, señalando una reversión bajista.
- La estrategia no tiene posición abierta y no abrió ya una operación en la barra actual.
- La hora actual está dentro de al menos una ventana de sesión activa.
- Cuando el filtro estocástico está activo, el %K actual debe estar por debajo de %D y el valor de StochasticBarsToCheck barras atrás debe estar por encima de %D, confirmando un cruce bajista.
Gestión de Posición
- Stop-Loss / Take-Profit – los niveles iniciales se calculan en pips usando el paso de precio del instrumento. La implementación ajusta automáticamente las cotizaciones de 3 y 5 dígitos multiplicando el paso de precio por 10 para aproximar un pip estándar.
- Trailing Stop – una vez que la posición ha ganado al menos WhenSetNoLossStopPips de ganancia, el stop puede seguir al mercado:
- Las posiciones largas requieren un stop inicial. El stop se incrementa por TrailingStopPips cuando permanece al menos TrailingStepPips + TrailingStopPips alejado del cierre actual y permanece por encima del buffer de punto de equilibrio definido por NoLossStopPips.
- Las posiciones cortas mueven el stop hacia abajo bajo restricciones similares. Si no existe stop inicial, el algoritmo puede colocar un stop de punto de equilibrio en NoLossStopPips una vez que el precio ha avanzado lo suficiente.
- Activación de Take-Profit / Stop – si el máximo o mínimo de una vela toca los niveles de salida almacenados, la posición se cierra a mercado y el estado interno se reinicia.
Parámetros
- MacdFastPeriod, MacdSlowPeriod, MacdSignalPeriod – configuración del MACD.
- UseStochastic – habilita el filtro de confirmación estocástico.
- StochasticBarsToCheck, StochasticLength, StochasticKPeriod, StochasticDPeriod – configuración del oscilador estocástico.
- Volume – tamaño del trade en lotes.
- StopLossPips, TakeProfitPips – distancias en pips para salidas iniciales.
- TrailingStopPips, TrailingStepPips – configuración del trailing stop.
- NoLossStopPips, WhenSetNoLossStopPips – umbrales de punto de equilibrio y activación para la lógica de trailing.
- MaxPositions – retenido por compatibilidad; StockSharp trabaja con posiciones netas, por lo que la estrategia mantiene solo una posición abierta a la vez.
- Session1/2/3 Start-End – ventanas intradía cuando el trading está permitido. Establezca inicio y fin en
00:00para deshabilitar una ventana. - CandleType – serie de velas utilizada para la generación de señales.
Notas Adicionales
- Las entradas se procesan solo en velas completadas. La estrategia no abrirá más de una posición por vela, reflejando el comportamiento original del EA.
- Las distancias basadas en pips dependen del paso de precio del instrumento. Asegúrese de que los metadatos del símbolo proporcionen un
PriceStepválido. - El filtro estocástico almacena un pequeño historial rotativo para evaluar valores pasados sin usar acceso de indicador de bajo nivel, cumpliendo con las mejores prácticas de la API de alto nivel.
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 strategy with optional stochastic confirmation and timed trading sessions.
/// </summary>
public class MacdStochasticStrategy : Strategy
{
private readonly StrategyParam<int> _macdFastPeriod;
private readonly StrategyParam<int> _macdSlowPeriod;
private readonly StrategyParam<int> _macdSignalPeriod;
private readonly StrategyParam<bool> _useStochastic;
private readonly StrategyParam<int> _stochasticBarsToCheck;
private readonly StrategyParam<int> _stochasticLength;
private readonly StrategyParam<int> _stochasticKPeriod;
private readonly StrategyParam<int> _stochasticDPeriod;
private readonly StrategyParam<int> _stopLossPips;
private readonly StrategyParam<int> _takeProfitPips;
private readonly StrategyParam<int> _trailingStopPips;
private readonly StrategyParam<int> _trailingStepPips;
private readonly StrategyParam<int> _maxPositions;
private readonly StrategyParam<int> _noLossStopPips;
private readonly StrategyParam<int> _whenSetNoLossStopPips;
private readonly StrategyParam<TimeSpan> _session1Start;
private readonly StrategyParam<TimeSpan> _session1End;
private readonly StrategyParam<TimeSpan> _session2Start;
private readonly StrategyParam<TimeSpan> _session2End;
private readonly StrategyParam<TimeSpan> _session3Start;
private readonly StrategyParam<TimeSpan> _session3End;
private readonly StrategyParam<DataType> _candleType;
private MovingAverageConvergenceDivergenceSignal _macd = null!;
private StochasticOscillator _stochastic = null!;
private readonly List<(decimal K, decimal D)> _stochasticHistory = new();
private decimal _prevMacd;
private decimal _prevSignal;
private bool _hasPrevMacd;
private decimal _entryPrice;
private decimal _stopPrice;
private decimal _takePrice;
private decimal _pipSize;
private DateTimeOffset _lastEntryBarTime;
/// <summary>
/// Fast EMA period used inside MACD.
/// </summary>
public int MacdFastPeriod
{
get => _macdFastPeriod.Value;
set => _macdFastPeriod.Value = value;
}
/// <summary>
/// Slow EMA period used inside MACD.
/// </summary>
public int MacdSlowPeriod
{
get => _macdSlowPeriod.Value;
set => _macdSlowPeriod.Value = value;
}
/// <summary>
/// Signal line period of MACD.
/// </summary>
public int MacdSignalPeriod
{
get => _macdSignalPeriod.Value;
set => _macdSignalPeriod.Value = value;
}
/// <summary>
/// Use stochastic oscillator as additional confirmation.
/// </summary>
public bool UseStochastic
{
get => _useStochastic.Value;
set => _useStochastic.Value = value;
}
/// <summary>
/// Number of historical bars used for stochastic crossover validation.
/// </summary>
public int StochasticBarsToCheck
{
get => _stochasticBarsToCheck.Value;
set => _stochasticBarsToCheck.Value = value;
}
/// <summary>
/// Base length for the stochastic oscillator.
/// </summary>
public int StochasticLength
{
get => _stochasticLength.Value;
set => _stochasticLength.Value = value;
}
/// <summary>
/// Smoothing applied to %K line.
/// </summary>
public int StochasticKPeriod
{
get => _stochasticKPeriod.Value;
set => _stochasticKPeriod.Value = value;
}
/// <summary>
/// Period used to calculate %D line.
/// </summary>
public int StochasticDPeriod
{
get => _stochasticDPeriod.Value;
set => _stochasticDPeriod.Value = value;
}
/// <summary>
/// Stop-loss distance expressed in pips.
/// </summary>
public int StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Take-profit distance expressed in pips.
/// </summary>
public int TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Trailing stop distance in pips.
/// </summary>
public int TrailingStopPips
{
get => _trailingStopPips.Value;
set => _trailingStopPips.Value = value;
}
/// <summary>
/// Minimum price move required before updating trailing stop.
/// </summary>
public int TrailingStepPips
{
get => _trailingStepPips.Value;
set => _trailingStepPips.Value = value;
}
/// <summary>
/// Maximum number of simultaneous positions.
/// </summary>
public int MaxPositions
{
get => _maxPositions.Value;
set => _maxPositions.Value = value;
}
/// <summary>
/// Offset applied to break-even stop in pips.
/// </summary>
public int NoLossStopPips
{
get => _noLossStopPips.Value;
set => _noLossStopPips.Value = value;
}
/// <summary>
/// Profit required before activating break-even stop in pips.
/// </summary>
public int WhenSetNoLossStopPips
{
get => _whenSetNoLossStopPips.Value;
set => _whenSetNoLossStopPips.Value = value;
}
/// <summary>
/// Start time for the first trading session.
/// </summary>
public TimeSpan Session1Start
{
get => _session1Start.Value;
set => _session1Start.Value = value;
}
/// <summary>
/// End time for the first trading session.
/// </summary>
public TimeSpan Session1End
{
get => _session1End.Value;
set => _session1End.Value = value;
}
/// <summary>
/// Start time for the second trading session.
/// </summary>
public TimeSpan Session2Start
{
get => _session2Start.Value;
set => _session2Start.Value = value;
}
/// <summary>
/// End time for the second trading session.
/// </summary>
public TimeSpan Session2End
{
get => _session2End.Value;
set => _session2End.Value = value;
}
/// <summary>
/// Start time for the third trading session.
/// </summary>
public TimeSpan Session3Start
{
get => _session3Start.Value;
set => _session3Start.Value = value;
}
/// <summary>
/// End time for the third trading session.
/// </summary>
public TimeSpan Session3End
{
get => _session3End.Value;
set => _session3End.Value = value;
}
/// <summary>
/// Candle type used for generating signals.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes <see cref="MacdStochasticStrategy"/>.
/// </summary>
public MacdStochasticStrategy()
{
_macdFastPeriod = Param(nameof(MacdFastPeriod), 12)
.SetDisplay("MACD Fast Period", "Fast EMA length for MACD", "MACD")
.SetOptimize(6, 18, 1);
_macdSlowPeriod = Param(nameof(MacdSlowPeriod), 26)
.SetDisplay("MACD Slow Period", "Slow EMA length for MACD", "MACD")
.SetOptimize(20, 40, 1);
_macdSignalPeriod = Param(nameof(MacdSignalPeriod), 9)
.SetDisplay("MACD Signal Period", "Signal line length for MACD", "MACD")
.SetOptimize(5, 15, 1);
_useStochastic = Param(nameof(UseStochastic), false)
.SetDisplay("Use Stochastic Filter", "Enable stochastic confirmation", "Stochastic");
_stochasticBarsToCheck = Param(nameof(StochasticBarsToCheck), 5)
.SetDisplay("Stochastic Bars", "History depth for stochastic confirmation", "Stochastic")
.SetGreaterThanZero()
.SetOptimize(2, 8, 1);
_stochasticLength = Param(nameof(StochasticLength), 5)
.SetDisplay("Stochastic Length", "Number of bars for %K calculation", "Stochastic")
.SetGreaterThanZero()
.SetOptimize(5, 14, 1);
_stochasticKPeriod = Param(nameof(StochasticKPeriod), 3)
.SetDisplay("Stochastic %K Smoothing", "Smoothing period for %K line", "Stochastic")
.SetGreaterThanZero()
.SetOptimize(2, 5, 1);
_stochasticDPeriod = Param(nameof(StochasticDPeriod), 3)
.SetDisplay("Stochastic %D Period", "Smoothing period for %D line", "Stochastic")
.SetGreaterThanZero()
.SetOptimize(2, 5, 1);
_stopLossPips = Param(nameof(StopLossPips), 100)
.SetDisplay("Stop Loss (pips)", "Initial stop-loss distance", "Risk")
.SetOptimize(50, 200, 10);
_takeProfitPips = Param(nameof(TakeProfitPips), 100)
.SetDisplay("Take Profit (pips)", "Initial take-profit distance", "Risk")
.SetOptimize(50, 200, 10);
_trailingStopPips = Param(nameof(TrailingStopPips), 0)
.SetDisplay("Trailing Stop (pips)", "Trailing stop distance", "Risk");
_trailingStepPips = Param(nameof(TrailingStepPips), 5)
.SetDisplay("Trailing Step (pips)", "Minimum move before trailing", "Risk");
_maxPositions = Param(nameof(MaxPositions), 1)
.SetDisplay("Max Positions", "Maximum simultaneous positions", "Trading")
.SetGreaterThanZero();
_noLossStopPips = Param(nameof(NoLossStopPips), 1)
.SetDisplay("No Loss Stop (pips)", "Break-even offset for trailing", "Risk");
_whenSetNoLossStopPips = Param(nameof(WhenSetNoLossStopPips), 25)
.SetDisplay("Activation Profit (pips)", "Profit before enabling trailing", "Risk");
_session1Start = Param(nameof(Session1Start), new TimeSpan(0, 0, 0))
.SetDisplay("Session 1 Start", "Start time of first window", "Sessions");
_session1End = Param(nameof(Session1End), new TimeSpan(23, 59, 59))
.SetDisplay("Session 1 End", "End time of first window", "Sessions");
_session2Start = Param(nameof(Session2Start), new TimeSpan(0, 0, 0))
.SetDisplay("Session 2 Start", "Start time of second window", "Sessions");
_session2End = Param(nameof(Session2End), new TimeSpan(0, 0, 0))
.SetDisplay("Session 2 End", "End time of second window", "Sessions");
_session3Start = Param(nameof(Session3Start), new TimeSpan(0, 0, 0))
.SetDisplay("Session 3 Start", "Start time of third window", "Sessions");
_session3End = Param(nameof(Session3End), new TimeSpan(0, 0, 0))
.SetDisplay("Session 3 End", "End time of third window", "Sessions");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Candles used for analysis", "General");
ResetState();
}
/// <summary>
/// Securities required by the strategy.
/// </summary>
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <summary>
/// Reset cached state when strategy is reset.
/// </summary>
protected override void OnReseted()
{
base.OnReseted();
ResetState();
}
/// <summary>
/// Start indicator subscriptions and chart visualization.
/// </summary>
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_macd = new MovingAverageConvergenceDivergenceSignal
{
Macd =
{
ShortMa = { Length = MacdFastPeriod },
LongMa = { Length = MacdSlowPeriod }
},
SignalMa = { Length = MacdSignalPeriod }
};
_stochastic = new StochasticOscillator();
_stochastic.K.Length = StochasticLength;
_stochastic.D.Length = StochasticDPeriod;
UpdatePipSize();
var subscription = SubscribeCandles(CandleType);
subscription.BindEx(_macd, _stochastic, ProcessCandle).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _macd);
DrawIndicator(area, _stochastic);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue macdValue, IIndicatorValue stochasticValue)
{
if (candle.State != CandleStates.Finished)
return;
if (Position == 0 && _entryPrice != 0m)
ResetPositionState();
if (_pipSize == 0m)
UpdatePipSize();
ManagePosition(candle);
var macdTyped = (MovingAverageConvergenceDivergenceSignalValue)macdValue;
if (macdTyped.Macd is not decimal macd || macdTyped.Signal is not decimal signal)
return;
decimal? currentK = null;
decimal? currentD = null;
var stochasticTyped = (StochasticOscillatorValue)stochasticValue;
if (stochasticTyped.K is decimal kValue && stochasticTyped.D is decimal dValue)
{
currentK = kValue;
currentD = dValue;
UpdateStochasticHistory(kValue, dValue);
}
var allowTrading = _macd.IsFormed && Volume > 0m && MaxPositions > 0;
var macdCrossUp = _hasPrevMacd && _prevMacd <= _prevSignal && macd > signal && macd < 0m && _prevMacd < 0m;
var macdCrossDown = _hasPrevMacd && _prevMacd >= _prevSignal && macd < signal && macd > 0m && _prevMacd > 0m;
if (allowTrading && Position == 0 && candle.OpenTime > _lastEntryBarTime && IsWithinTradingSession(candle.OpenTime))
{
if (macdCrossUp && PassesStochasticFilter(true, currentK, currentD))
{
EnterLong(candle);
}
else if (macdCrossDown && PassesStochasticFilter(false, currentK, currentD))
{
EnterShort(candle);
}
}
_prevMacd = macd;
_prevSignal = signal;
_hasPrevMacd = true;
}
private void EnterLong(ICandleMessage candle)
{
// Open long position using close price of finished candle.
BuyMarket();
_entryPrice = candle.ClosePrice;
_stopPrice = StopLossPips > 0 && _pipSize > 0m ? _entryPrice - StopLossPips * _pipSize : 0m;
_takePrice = TakeProfitPips > 0 && _pipSize > 0m ? _entryPrice + TakeProfitPips * _pipSize : 0m;
_lastEntryBarTime = candle.OpenTime;
}
private void EnterShort(ICandleMessage candle)
{
// Open short position using close price of finished candle.
SellMarket();
_entryPrice = candle.ClosePrice;
_stopPrice = StopLossPips > 0 && _pipSize > 0m ? _entryPrice + StopLossPips * _pipSize : 0m;
_takePrice = TakeProfitPips > 0 && _pipSize > 0m ? _entryPrice - TakeProfitPips * _pipSize : 0m;
_lastEntryBarTime = candle.OpenTime;
}
private void ManagePosition(ICandleMessage candle)
{
if (Position > 0)
{
UpdateLongTrailing(candle);
CheckLongExits(candle);
}
else if (Position < 0)
{
UpdateShortTrailing(candle);
CheckShortExits(candle);
}
}
private void CheckLongExits(ICandleMessage candle)
{
// Close long position if stop or take profit levels are reached.
if (_stopPrice > 0m && candle.LowPrice <= _stopPrice)
{
SellMarket();
ResetPositionState();
return;
}
if (_takePrice > 0m && candle.HighPrice >= _takePrice)
{
SellMarket();
ResetPositionState();
}
}
private void CheckShortExits(ICandleMessage candle)
{
// Close short position if stop or take profit levels are reached.
if (_stopPrice > 0m && candle.HighPrice >= _stopPrice)
{
BuyMarket();
ResetPositionState();
return;
}
if (_takePrice > 0m && candle.LowPrice <= _takePrice)
{
BuyMarket();
ResetPositionState();
}
}
private void UpdateLongTrailing(ICandleMessage candle)
{
// Move long stop towards break-even based on trailing parameters.
if (TrailingStopPips <= 0 || _pipSize <= 0m || _stopPrice <= 0m)
return;
var profit = candle.ClosePrice - _entryPrice;
if (profit <= WhenSetNoLossStopPips * _pipSize)
return;
var newStop = _stopPrice + TrailingStopPips * _pipSize;
var minStop = _entryPrice + NoLossStopPips * _pipSize;
var maxStop = candle.ClosePrice - (TrailingStepPips + TrailingStopPips) * _pipSize;
if (newStop <= _stopPrice)
return;
if (newStop <= minStop)
return;
if (newStop >= maxStop)
return;
_stopPrice = newStop;
}
private void UpdateShortTrailing(ICandleMessage candle)
{
// Move short stop towards break-even based on trailing parameters.
if (TrailingStopPips <= 0 || _pipSize <= 0m)
return;
var profit = _entryPrice - candle.ClosePrice;
if (profit <= WhenSetNoLossStopPips * _pipSize)
return;
if (_stopPrice > 0m)
{
var newStop = _stopPrice - TrailingStopPips * _pipSize;
var maxStop = _entryPrice - NoLossStopPips * _pipSize;
var minStop = candle.ClosePrice + (TrailingStepPips + TrailingStopPips) * _pipSize;
if (newStop >= _stopPrice)
return;
if (newStop >= maxStop)
return;
if (newStop <= minStop)
return;
_stopPrice = newStop;
}
else
{
var candidate = _entryPrice - NoLossStopPips * _pipSize;
var threshold = candle.ClosePrice + WhenSetNoLossStopPips * _pipSize;
if (candidate <= 0m)
return;
if (candidate <= threshold)
return;
_stopPrice = candidate;
}
}
private bool PassesStochasticFilter(bool isBuy, decimal? currentK, decimal? currentD)
{
// Validate stochastic crossover when the filter is enabled.
if (!UseStochastic)
return true;
if (currentK is null || currentD is null)
return false;
var bars = Math.Max(1, StochasticBarsToCheck);
if (_stochasticHistory.Count < bars)
return false;
if (bars <= 1)
return isBuy ? currentD < currentK : currentD > currentK;
var (oldK, oldD) = _stochasticHistory[0];
return isBuy ? currentD < currentK && oldD > oldK : currentD > currentK && oldD < oldK;
}
private void UpdateStochasticHistory(decimal k, decimal d)
{
// Maintain rolling history for stochastic confirmation.
var max = Math.Max(1, StochasticBarsToCheck);
_stochasticHistory.Add((k, d));
while (_stochasticHistory.Count > max)
_stochasticHistory.RemoveAt(0);
}
private bool IsWithinTradingSession(DateTimeOffset time)
{
// Check whether local time is inside any allowed window.
var tod = time.TimeOfDay;
return IsWithinSession(tod, Session1Start, Session1End)
|| IsWithinSession(tod, Session2Start, Session2End)
|| IsWithinSession(tod, Session3Start, Session3End);
}
private static bool IsWithinSession(TimeSpan time, TimeSpan start, TimeSpan end)
{
if (start == end && start == TimeSpan.Zero)
return false;
return start <= end
? time >= start && time <= end
: time >= start || time <= end;
}
private void UpdatePipSize()
{
// Convert pip-based settings to price values using security price step.
var priceStep = Security?.PriceStep ?? 0m;
if (priceStep <= 0m)
{
_pipSize = 0m;
return;
}
var ratio = 1m / priceStep;
var digits = (int)Math.Round(Math.Log10((double)ratio));
_pipSize = digits == 3 || digits == 5 ? priceStep * 10m : priceStep;
}
private void ResetState()
{
// Clear cached values when strategy is reset or initialized.
_stochasticHistory.Clear();
_prevMacd = 0m;
_prevSignal = 0m;
_hasPrevMacd = false;
ResetPositionState();
_pipSize = 0m;
_lastEntryBarTime = DateTimeOffset.MinValue;
}
private void ResetPositionState()
{
// Reset position-specific tracking variables.
_entryPrice = 0m;
_stopPrice = 0m;
_takePrice = 0m;
}
}
import clr
import math
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Algo.Indicators import MovingAverageConvergenceDivergenceSignal, StochasticOscillator
class macd_stochastic_strategy(Strategy):
"""MACD crossover with optional stochastic confirmation, trailing stop, and session windows."""
def __init__(self):
super(macd_stochastic_strategy, self).__init__()
self._macd_fast_period = self.Param("MacdFastPeriod", 12) \
.SetGreaterThanZero() \
.SetDisplay("MACD Fast Period", "Fast EMA length for MACD", "MACD")
self._macd_slow_period = self.Param("MacdSlowPeriod", 26) \
.SetGreaterThanZero() \
.SetDisplay("MACD Slow Period", "Slow EMA length for MACD", "MACD")
self._macd_signal_period = self.Param("MacdSignalPeriod", 9) \
.SetGreaterThanZero() \
.SetDisplay("MACD Signal Period", "Signal line length for MACD", "MACD")
self._use_stochastic = self.Param("UseStochastic", False) \
.SetDisplay("Use Stochastic Filter", "Enable stochastic confirmation", "Stochastic")
self._stochastic_bars_to_check = self.Param("StochasticBarsToCheck", 5) \
.SetGreaterThanZero() \
.SetDisplay("Stochastic Bars", "History depth for stochastic confirmation", "Stochastic")
self._stochastic_length = self.Param("StochasticLength", 5) \
.SetGreaterThanZero() \
.SetDisplay("Stochastic Length", "Number of bars for K calculation", "Stochastic")
self._stochastic_k_period = self.Param("StochasticKPeriod", 3) \
.SetGreaterThanZero() \
.SetDisplay("Stochastic K Smoothing", "Smoothing period for K line", "Stochastic")
self._stochastic_d_period = self.Param("StochasticDPeriod", 3) \
.SetGreaterThanZero() \
.SetDisplay("Stochastic D Period", "Smoothing period for D line", "Stochastic")
self._stop_loss_pips = self.Param("StopLossPips", 100) \
.SetDisplay("Stop Loss (pips)", "Initial stop-loss distance", "Risk")
self._take_profit_pips = self.Param("TakeProfitPips", 100) \
.SetDisplay("Take Profit (pips)", "Initial take-profit distance", "Risk")
self._trailing_stop_pips = self.Param("TrailingStopPips", 0) \
.SetDisplay("Trailing Stop (pips)", "Trailing stop distance", "Risk")
self._trailing_step_pips = self.Param("TrailingStepPips", 5) \
.SetDisplay("Trailing Step (pips)", "Minimum move before trailing", "Risk")
self._max_positions = self.Param("MaxPositions", 1) \
.SetGreaterThanZero() \
.SetDisplay("Max Positions", "Maximum simultaneous positions", "Trading")
self._no_loss_stop_pips = self.Param("NoLossStopPips", 1) \
.SetDisplay("No Loss Stop (pips)", "Break-even offset for trailing", "Risk")
self._when_set_no_loss_stop_pips = self.Param("WhenSetNoLossStopPips", 25) \
.SetDisplay("Activation Profit (pips)", "Profit before enabling trailing", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Candles used for analysis", "General")
self._stochastic_history = []
self._prev_macd = 0.0
self._prev_signal = 0.0
self._has_prev_macd = False
self._entry_price = 0.0
self._stop_price = 0.0
self._take_price = 0.0
self._pip_size = 0.0
self._last_entry_bar_time = None
@property
def MacdFastPeriod(self):
return int(self._macd_fast_period.Value)
@property
def MacdSlowPeriod(self):
return int(self._macd_slow_period.Value)
@property
def MacdSignalPeriod(self):
return int(self._macd_signal_period.Value)
@property
def UseStochastic(self):
return self._use_stochastic.Value
@property
def StochasticBarsToCheck(self):
return int(self._stochastic_bars_to_check.Value)
@property
def StochasticLength(self):
return int(self._stochastic_length.Value)
@property
def StochasticKPeriod(self):
return int(self._stochastic_k_period.Value)
@property
def StochasticDPeriod(self):
return int(self._stochastic_d_period.Value)
@property
def StopLossPips(self):
return int(self._stop_loss_pips.Value)
@property
def TakeProfitPips(self):
return int(self._take_profit_pips.Value)
@property
def TrailingStopPips(self):
return int(self._trailing_stop_pips.Value)
@property
def TrailingStepPips(self):
return int(self._trailing_step_pips.Value)
@property
def MaxPositions(self):
return int(self._max_positions.Value)
@property
def NoLossStopPips(self):
return int(self._no_loss_stop_pips.Value)
@property
def WhenSetNoLossStopPips(self):
return int(self._when_set_no_loss_stop_pips.Value)
@property
def CandleType(self):
return self._candle_type.Value
def _update_pip_size(self):
sec = self.Security
price_step = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None and float(sec.PriceStep) > 0 else 0.0
if price_step <= 0:
self._pip_size = 0.0
return
ratio = 1.0 / price_step
digits = int(round(math.log10(ratio)))
self._pip_size = price_step * 10.0 if (digits == 3 or digits == 5) else price_step
def OnStarted2(self, time):
super(macd_stochastic_strategy, self).OnStarted2(time)
self._stochastic_history = []
self._prev_macd = 0.0
self._prev_signal = 0.0
self._has_prev_macd = False
self._entry_price = 0.0
self._stop_price = 0.0
self._take_price = 0.0
self._last_entry_bar_time = None
self._macd_ind = MovingAverageConvergenceDivergenceSignal()
self._macd_ind.Macd.ShortMa.Length = self.MacdFastPeriod
self._macd_ind.Macd.LongMa.Length = self.MacdSlowPeriod
self._macd_ind.SignalMa.Length = self.MacdSignalPeriod
self._stoch_ind = StochasticOscillator()
self._stoch_ind.K.Length = self.StochasticLength
self._stoch_ind.D.Length = self.StochasticDPeriod
self._update_pip_size()
subscription = self.SubscribeCandles(self.CandleType)
subscription.BindEx(self._macd_ind, self._stoch_ind, self.process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, self._macd_ind)
self.DrawIndicator(area, self._stoch_ind)
self.DrawOwnTrades(area)
def process_candle(self, candle, macd_value, stochastic_value):
if candle.State != CandleStates.Finished:
return
if self.Position == 0 and self._entry_price != 0:
self._reset_position_state()
if self._pip_size == 0:
self._update_pip_size()
self._manage_position(candle)
macd_main_n = macd_value.Macd
signal_n = macd_value.Signal
if macd_main_n is None or signal_n is None:
return
macd_main = float(macd_main_n)
signal = float(signal_n)
current_k = None
current_d = None
stoch_k_n = stochastic_value.K
stoch_d_n = stochastic_value.D
if stoch_k_n is not None and stoch_d_n is not None:
current_k = float(stoch_k_n)
current_d = float(stoch_d_n)
self._update_stochastic_history(current_k, current_d)
allow_trading = self._macd_ind.IsFormed and self.MaxPositions > 0
macd_cross_up = (self._has_prev_macd and self._prev_macd <= self._prev_signal
and macd_main > signal and macd_main < 0 and self._prev_macd < 0)
macd_cross_down = (self._has_prev_macd and self._prev_macd >= self._prev_signal
and macd_main < signal and macd_main > 0 and self._prev_macd > 0)
if (allow_trading and self.Position == 0
and (self._last_entry_bar_time is None or candle.OpenTime > self._last_entry_bar_time)):
if macd_cross_up and self._passes_stochastic_filter(True, current_k, current_d):
self._enter_long(candle)
elif macd_cross_down and self._passes_stochastic_filter(False, current_k, current_d):
self._enter_short(candle)
self._prev_macd = macd_main
self._prev_signal = signal
self._has_prev_macd = True
def _enter_long(self, candle):
self.BuyMarket()
close = float(candle.ClosePrice)
self._entry_price = close
self._stop_price = close - self.StopLossPips * self._pip_size if self.StopLossPips > 0 and self._pip_size > 0 else 0.0
self._take_price = close + self.TakeProfitPips * self._pip_size if self.TakeProfitPips > 0 and self._pip_size > 0 else 0.0
self._last_entry_bar_time = candle.OpenTime
def _enter_short(self, candle):
self.SellMarket()
close = float(candle.ClosePrice)
self._entry_price = close
self._stop_price = close + self.StopLossPips * self._pip_size if self.StopLossPips > 0 and self._pip_size > 0 else 0.0
self._take_price = close - self.TakeProfitPips * self._pip_size if self.TakeProfitPips > 0 and self._pip_size > 0 else 0.0
self._last_entry_bar_time = candle.OpenTime
def _manage_position(self, candle):
if self.Position > 0:
self._update_long_trailing(candle)
self._check_long_exits(candle)
elif self.Position < 0:
self._update_short_trailing(candle)
self._check_short_exits(candle)
def _check_long_exits(self, candle):
lo = float(candle.LowPrice)
h = float(candle.HighPrice)
if self._stop_price > 0 and lo <= self._stop_price:
self.SellMarket()
self._reset_position_state()
return
if self._take_price > 0 and h >= self._take_price:
self.SellMarket()
self._reset_position_state()
def _check_short_exits(self, candle):
h = float(candle.HighPrice)
lo = float(candle.LowPrice)
if self._stop_price > 0 and h >= self._stop_price:
self.BuyMarket()
self._reset_position_state()
return
if self._take_price > 0 and lo <= self._take_price:
self.BuyMarket()
self._reset_position_state()
def _update_long_trailing(self, candle):
if self.TrailingStopPips <= 0 or self._pip_size <= 0 or self._stop_price <= 0:
return
close = float(candle.ClosePrice)
profit = close - self._entry_price
if profit <= self.WhenSetNoLossStopPips * self._pip_size:
return
new_stop = self._stop_price + self.TrailingStopPips * self._pip_size
min_stop = self._entry_price + self.NoLossStopPips * self._pip_size
max_stop = close - (self.TrailingStepPips + self.TrailingStopPips) * self._pip_size
if new_stop <= self._stop_price:
return
if new_stop <= min_stop:
return
if new_stop >= max_stop:
return
self._stop_price = new_stop
def _update_short_trailing(self, candle):
if self.TrailingStopPips <= 0 or self._pip_size <= 0:
return
close = float(candle.ClosePrice)
profit = self._entry_price - close
if profit <= self.WhenSetNoLossStopPips * self._pip_size:
return
if self._stop_price > 0:
new_stop = self._stop_price - self.TrailingStopPips * self._pip_size
max_stop = self._entry_price - self.NoLossStopPips * self._pip_size
min_stop = close + (self.TrailingStepPips + self.TrailingStopPips) * self._pip_size
if new_stop >= self._stop_price:
return
if new_stop >= max_stop:
return
if new_stop <= min_stop:
return
self._stop_price = new_stop
else:
candidate = self._entry_price - self.NoLossStopPips * self._pip_size
threshold = close + self.WhenSetNoLossStopPips * self._pip_size
if candidate <= 0:
return
if candidate <= threshold:
return
self._stop_price = candidate
def _passes_stochastic_filter(self, is_buy, current_k, current_d):
if not self.UseStochastic:
return True
if current_k is None or current_d is None:
return False
bars = max(1, self.StochasticBarsToCheck)
if len(self._stochastic_history) < bars:
return False
if bars <= 1:
return current_d < current_k if is_buy else current_d > current_k
old_k, old_d = self._stochastic_history[0]
if is_buy:
return current_d < current_k and old_d > old_k
else:
return current_d > current_k and old_d < old_k
def _update_stochastic_history(self, k, d):
mx = max(1, self.StochasticBarsToCheck)
self._stochastic_history.append((k, d))
while len(self._stochastic_history) > mx:
self._stochastic_history.pop(0)
def _reset_position_state(self):
self._entry_price = 0.0
self._stop_price = 0.0
self._take_price = 0.0
def OnReseted(self):
super(macd_stochastic_strategy, self).OnReseted()
self._stochastic_history = []
self._prev_macd = 0.0
self._prev_signal = 0.0
self._has_prev_macd = False
self._entry_price = 0.0
self._stop_price = 0.0
self._take_price = 0.0
self._pip_size = 0.0
self._last_entry_bar_time = None
def CreateClone(self):
return macd_stochastic_strategy()