Estrategia Hercules A.T.C. 2006
Descripción general
Hercules A.T.C. 2006 es una estrategia de seguimiento de tendencia en marcos temporales altos que recrea el asesor experto de MetaTrader publicado en 2006. La versión de StockSharp escucha velas completadas en el marco temporal primario, detecta cruces alcistas/bajistas entre una EMA(1) rápida y una SMA(72) lenta, y abre operaciones solo cuando filtros adicionales confirman la ruptura. La estrategia divide su posición en dos tramos con niveles de take-profit independientes y ajusta el stop una vez que el precio avanza.
Indicadores y datos
- Velas primarias: configurables (por defecto velas de 1 hora).
- MA rápida: EMA con longitud
FastMaPeriod(por defecto 1). - MA lenta: SMA con longitud
SlowMaPeriod(por defecto 72). - Filtro RSI: RSI de longitud
RsiLengthen elRsiTimeFrame(por defecto 1 hora). - Envolvente diaria: SMA de longitud
DailyEnvelopePeriodenDailyEnvelopeTimeFramecon desviación de ±DailyEnvelopeDeviationpor ciento. - Envolvente H4: SMA de longitud
H4EnvelopePeriodenH4EnvelopeTimeFramecon desviación de ±H4EnvelopeDeviationpor ciento. - Máximo/mínimo rodante: máximo más alto y mínimo más bajo de las últimas
HighLowHourshoras en el marco temporal primario.
Parámetros
| Nombre | Predeterminado | Descripción |
|---|---|---|
TriggerPips |
38 | Desplazamiento en pips añadido/sustraído al precio de cruce antes de disparar una orden. |
TrailingStopPips |
90 | Distancia del stop móvil en pips (0 deshabilita el trailing). |
TakeProfit1Pips |
210 | Primera distancia de take-profit en pips para reducir la mitad de la posición. |
TakeProfit2Pips |
280 | Distancia de take-profit final en pips para cerrar la posición restante. |
FastMaPeriod |
1 | Longitud de la EMA rápida usada en el detector de cruce. |
SlowMaPeriod |
72 | Longitud de la SMA lenta de referencia. |
StopLossLookback |
4 | Número de velas completadas usadas para calcular el precio de stop inicial. |
HighLowHours |
10 | Tamaño de la ventana rodante (en horas) usada para el filtro de ruptura. |
BlackoutHours |
144 | Período de enfriamiento (en horas) después de cerrar una operación antes de permitir una nueva entrada. |
RsiLength |
10 | Longitud del RSI en el filtro de marco temporal superior. |
RsiUpper |
55 | Valor mínimo de RSI requerido para permitir entradas largas. |
RsiLower |
45 | Valor máximo de RSI permitido antes de bloquear entradas cortas. |
DailyEnvelopePeriod |
24 | Longitud de la SMA para el filtro de envolvente diaria. |
DailyEnvelopeDeviation |
0.99 | Desviación de la envolvente diaria en porcentaje. |
H4EnvelopePeriod |
96 | Longitud de la SMA para el filtro de envolvente de cuatro horas. |
H4EnvelopeDeviation |
0.1 | Desviación de la envolvente de cuatro horas en porcentaje. |
CandleType |
1 hora | Tipo de vela de trabajo primario. |
RsiTimeFrame |
1 hora | Tipo de vela usado para el filtro RSI. |
DailyEnvelopeTimeFrame |
1 día | Tipo de vela usado para la envolvente diaria. |
H4EnvelopeTimeFrame |
4 horas | Tipo de vela usado para la envolvente de cuatro horas. |
Reglas de trading
Detección de cruce
- Observar los valores de EMA(1) y SMA(72) de las últimas tres barras completadas.
- Detectar una señal alcista cuando la EMA cruza por encima de la SMA en cualquiera de las dos barras anteriores.
- Detectar una señal bajista cuando la EMA cruza por debajo de la SMA en cualquiera de las dos barras anteriores.
- Almacenar el precio de cruce (media de los valores rápido y lento) e iniciar una ventana de activación de dos barras.
Condición de activación
- Calcular
TriggerPrice = CrossPrice ± TriggerPips(convertido a unidades de precio). - La activación es válida durante dos velas primarias tras el momento del cruce.
- Los largos requieren que el máximo de la vela alcance o supere el precio de activación alcista.
- Los cortos requieren que el mínimo de la vela alcance o rompa el precio de activación bajista.
- Calcular
Filtros de entrada
- Sin posición existente y sin enfriamiento activo (
BlackoutHours). - Filtro RSI:
RSI > RsiUpperpara largos,RSI < RsiLowerpara cortos. - Filtro de ruptura: el cierre actual debe superar el máximo rodante para largos o caer por debajo del mínimo rodante para cortos.
- Confirmación de envolvente: el cierre actual debe estar por encima de ambas bandas superiores para largos o por debajo de ambas bandas inferiores para cortos.
- Sin posición existente y sin enfriamiento activo (
Ejecución de órdenes
- Enviar una orden de mercado usando el volumen de la estrategia (por defecto 2 unidades, lo que significa dos sub-posiciones iguales).
- Stop-loss: mínimo (largo) o máximo (corto) de la vela en la posición
StopLossLookback. - Niveles de take-profit:
TakeProfit1Pipspara la primera mitad,TakeProfit2Pipspara el resto. - Iniciar un temporizador de bloqueo para impedir nuevas entradas durante
BlackoutHourshoras.
Gestión de la posición
- El stop móvil se activa inmediatamente si
TrailingStopPips> 0 y se mueve solo a favor de la operación. - Reducir a la mitad la posición al primer nivel de take-profit.
- Cerrar la posición restante cuando se activa el take-profit final, se alcanza el stop-loss o el precio cruza el stop móvil.
- El stop móvil se activa inmediatamente si
Gestión del riesgo
- Los stops siempre se derivan de velas completadas para reducir el ruido intrabarra.
- Dos objetivos de take-profit aseguran ganancias parciales antes de dejar correr la operación.
- Los stops móviles garantizan que las ganancias queden protegidas después de que el mercado se mueva en la dirección deseada.
- Un largo período de bloqueo (por defecto 144 horas) previene la reentrada rápida tras una ruptura y replica el comportamiento del EA original.
Notas
- El port de StockSharp preserva la idea de gestión monetaria original al establecer el volumen de la estrategia en dos unidades por defecto, de modo que la salida parcial deja la mitad de la posición corriendo.
- Los valores de desplazamiento de la envolvente de MetaTrader se aproximan usando los valores más recientes porque el desplazamiento hacia adelante no está soportado por la API de alto nivel.
- La estrategia requiere información sobre el paso de precio para traducir correctamente las distancias en pips; asegúrese de que los metadatos del instrumento estén completados.
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>
/// Port of the Hercules A.T.C. 2006 MetaTrader expert advisor.
/// Detects EMA/SMA crossovers with trigger windows and multiple filters
/// before submitting two staged take-profit orders and applying a trailing stop.
/// </summary>
public class HerculesATC2006Strategy : Strategy
{
private readonly StrategyParam<int> _triggerPips;
private readonly StrategyParam<int> _trailingStopPips;
private readonly StrategyParam<int> _takeProfit1Pips;
private readonly StrategyParam<int> _takeProfit2Pips;
private readonly StrategyParam<int> _fastMaPeriod;
private readonly StrategyParam<int> _slowMaPeriod;
private readonly StrategyParam<int> _stopLossLookback;
private readonly StrategyParam<int> _highLowHours;
private readonly StrategyParam<int> _blackoutHours;
private readonly StrategyParam<int> _rsiLength;
private readonly StrategyParam<decimal> _rsiUpper;
private readonly StrategyParam<decimal> _rsiLower;
private readonly StrategyParam<int> _dailyEnvelopePeriod;
private readonly StrategyParam<decimal> _dailyEnvelopeDeviation;
private readonly StrategyParam<int> _h4EnvelopePeriod;
private readonly StrategyParam<decimal> _h4EnvelopeDeviation;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<DataType> _rsiTimeFrame;
private readonly StrategyParam<DataType> _dailyEnvelopeTimeFrame;
private readonly StrategyParam<DataType> _h4EnvelopeTimeFrame;
private readonly RelativeStrengthIndex _rsi = new();
private readonly SimpleMovingAverage _dailyEnvelopeMa = new();
private readonly SimpleMovingAverage _h4EnvelopeMa = new();
private readonly decimal[] _fastHistory = new decimal[4];
private readonly decimal[] _slowHistory = new decimal[4];
private readonly DateTimeOffset[] _timeHistory = new DateTimeOffset[4];
private int _historyCount;
private readonly decimal[] _highStopHistory = new decimal[5];
private readonly decimal[] _lowStopHistory = new decimal[5];
private int _stopHistoryCount;
private readonly Queue<decimal> _recentHighs = new();
private readonly Queue<decimal> _recentLows = new();
private decimal _rollingHigh;
private decimal _rollingLow;
private decimal _priceStep;
private decimal _pipSize;
private TimeSpan _primaryTimeFrame;
private int _highLowLength;
private int _pendingDirection;
private decimal _triggerPrice;
private DateTimeOffset? _windowEndTime;
private decimal _crossPrice;
private decimal _lastRsi;
private bool _rsiReady;
private decimal _dailyUpper;
private decimal _dailyLower;
private bool _dailyReady;
private decimal _h4Upper;
private decimal _h4Lower;
private bool _h4Ready;
private DateTimeOffset? _blackoutUntil;
private decimal? _entryPrice;
private decimal? _stopLoss;
private decimal? _tp1;
private decimal? _tp2;
private decimal? _trailingStop;
private bool _tp1Hit;
/// <summary>
/// Number of pips added to the crossover price to form the trigger level.
/// </summary>
public int TriggerPips
{
get => _triggerPips.Value;
set => _triggerPips.Value = value;
}
/// <summary>
/// Trailing stop distance expressed in pips.
/// </summary>
public int TrailingStopPips
{
get => _trailingStopPips.Value;
set => _trailingStopPips.Value = value;
}
/// <summary>
/// First take-profit distance in pips.
/// </summary>
public int TakeProfit1Pips
{
get => _takeProfit1Pips.Value;
set => _takeProfit1Pips.Value = value;
}
/// <summary>
/// Second take-profit distance in pips.
/// </summary>
public int TakeProfit2Pips
{
get => _takeProfit2Pips.Value;
set => _takeProfit2Pips.Value = value;
}
/// <summary>
/// Fast EMA period used for the trigger.
/// </summary>
public int FastMaPeriod
{
get => _fastMaPeriod.Value;
set => _fastMaPeriod.Value = value;
}
/// <summary>
/// Slow SMA period used as the baseline.
/// </summary>
public int SlowMaPeriod
{
get => _slowMaPeriod.Value;
set => _slowMaPeriod.Value = value;
}
/// <summary>
/// Number of completed candles used to fetch the stop-loss reference.
/// </summary>
public int StopLossLookback
{
get => _stopLossLookback.Value;
set => _stopLossLookback.Value = value;
}
/// <summary>
/// Number of hours used for the rolling high/low breakout filter.
/// </summary>
public int HighLowHours
{
get => _highLowHours.Value;
set => _highLowHours.Value = value;
}
/// <summary>
/// Cooldown duration in hours after a successful trade.
/// </summary>
public int BlackoutHours
{
get => _blackoutHours.Value;
set => _blackoutHours.Value = value;
}
/// <summary>
/// RSI length applied on the higher timeframe filter.
/// </summary>
public int RsiLength
{
get => _rsiLength.Value;
set => _rsiLength.Value = value;
}
/// <summary>
/// Upper RSI threshold required for long positions.
/// </summary>
public decimal RsiUpper
{
get => _rsiUpper.Value;
set => _rsiUpper.Value = value;
}
/// <summary>
/// Lower RSI threshold required for short positions.
/// </summary>
public decimal RsiLower
{
get => _rsiLower.Value;
set => _rsiLower.Value = value;
}
/// <summary>
/// Daily envelope moving average period.
/// </summary>
public int DailyEnvelopePeriod
{
get => _dailyEnvelopePeriod.Value;
set => _dailyEnvelopePeriod.Value = value;
}
/// <summary>
/// Daily envelope deviation in percent.
/// </summary>
public decimal DailyEnvelopeDeviation
{
get => _dailyEnvelopeDeviation.Value;
set => _dailyEnvelopeDeviation.Value = value;
}
/// <summary>
/// Four-hour envelope moving average period.
/// </summary>
public int H4EnvelopePeriod
{
get => _h4EnvelopePeriod.Value;
set => _h4EnvelopePeriod.Value = value;
}
/// <summary>
/// Four-hour envelope deviation in percent.
/// </summary>
public decimal H4EnvelopeDeviation
{
get => _h4EnvelopeDeviation.Value;
set => _h4EnvelopeDeviation.Value = value;
}
/// <summary>
/// Primary candle type that drives entries and exits.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Candle type used to compute RSI.
/// </summary>
public DataType RsiTimeFrame
{
get => _rsiTimeFrame.Value;
set => _rsiTimeFrame.Value = value;
}
/// <summary>
/// Candle type used for the daily envelope filter.
/// </summary>
public DataType DailyEnvelopeTimeFrame
{
get => _dailyEnvelopeTimeFrame.Value;
set => _dailyEnvelopeTimeFrame.Value = value;
}
/// <summary>
/// Candle type used for the four-hour envelope filter.
/// </summary>
public DataType H4EnvelopeTimeFrame
{
get => _h4EnvelopeTimeFrame.Value;
set => _h4EnvelopeTimeFrame.Value = value;
}
/// <summary>
/// Initializes a new instance of <see cref="HerculesATC2006Strategy"/>.
/// </summary>
public HerculesATC2006Strategy()
{
_triggerPips = Param(nameof(TriggerPips), 38)
.SetGreaterThanZero()
.SetDisplay("Trigger Pips", "Distance above/below crossover required to trigger", "Entries")
.SetOptimize(10, 80, 5);
_trailingStopPips = Param(nameof(TrailingStopPips), 90)
.SetNotNegative()
.SetDisplay("Trailing Stop (pips)", "Trailing stop distance in pips", "Risk Management")
.SetOptimize(20, 150, 10);
_takeProfit1Pips = Param(nameof(TakeProfit1Pips), 210)
.SetNotNegative()
.SetDisplay("Take Profit 1 (pips)", "First take-profit distance", "Risk Management")
.SetOptimize(100, 260, 10);
_takeProfit2Pips = Param(nameof(TakeProfit2Pips), 280)
.SetNotNegative()
.SetDisplay("Take Profit 2 (pips)", "Second take-profit distance", "Risk Management")
.SetOptimize(150, 360, 10);
_fastMaPeriod = Param(nameof(FastMaPeriod), 1)
.SetGreaterThanZero()
.SetDisplay("Fast EMA", "Length of the fast EMA", "Indicators");
_slowMaPeriod = Param(nameof(SlowMaPeriod), 72)
.SetGreaterThanZero()
.SetDisplay("Slow SMA", "Length of the slow SMA", "Indicators")
.SetOptimize(40, 120, 4);
_stopLossLookback = Param(nameof(StopLossLookback), 4)
.SetGreaterThanZero()
.SetDisplay("Stop-Loss Lookback", "Number of completed candles used for stop-loss", "Risk Management");
_highLowHours = Param(nameof(HighLowHours), 10)
.SetGreaterThanZero()
.SetDisplay("High/Low Window (hours)", "Duration used for breakout filter", "Filters");
_blackoutHours = Param(nameof(BlackoutHours), 4)
.SetGreaterThanZero()
.SetDisplay("Blackout Hours", "Cooldown after a trade", "Filters");
_rsiLength = Param(nameof(RsiLength), 10)
.SetGreaterThanZero()
.SetDisplay("RSI Length", "RSI period on higher timeframe", "Filters");
_rsiUpper = Param(nameof(RsiUpper), 55m)
.SetDisplay("RSI Upper", "Upper RSI threshold for longs", "Filters");
_rsiLower = Param(nameof(RsiLower), 45m)
.SetDisplay("RSI Lower", "Lower RSI threshold for shorts", "Filters");
_dailyEnvelopePeriod = Param(nameof(DailyEnvelopePeriod), 24)
.SetGreaterThanZero()
.SetDisplay("Daily Envelope Period", "Daily SMA length for envelope", "Filters");
_dailyEnvelopeDeviation = Param(nameof(DailyEnvelopeDeviation), 0.99m)
.SetGreaterThanZero()
.SetDisplay("Daily Envelope %", "Envelope deviation in percent", "Filters");
_h4EnvelopePeriod = Param(nameof(H4EnvelopePeriod), 96)
.SetGreaterThanZero()
.SetDisplay("H4 Envelope Period", "Four-hour SMA length for envelope", "Filters");
_h4EnvelopeDeviation = Param(nameof(H4EnvelopeDeviation), 0.1m)
.SetGreaterThanZero()
.SetDisplay("H4 Envelope %", "Envelope deviation in percent", "Filters");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Primary Candle", "Working timeframe for entries", "General");
_rsiTimeFrame = Param(nameof(RsiTimeFrame), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("RSI Candle", "Timeframe used for RSI filter", "Filters");
_dailyEnvelopeTimeFrame = Param(nameof(DailyEnvelopeTimeFrame), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Daily Envelope TF", "Timeframe for the daily envelope", "Filters");
_h4EnvelopeTimeFrame = Param(nameof(H4EnvelopeTimeFrame), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("H4 Envelope TF", "Timeframe for the four-hour envelope", "Filters");
Volume = 2m;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
var uniqueTypes = new HashSet<DataType> { CandleType, RsiTimeFrame, DailyEnvelopeTimeFrame, H4EnvelopeTimeFrame };
foreach (var type in uniqueTypes)
{
yield return (Security, type);
}
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_rsi.Reset();
_rsi.Length = RsiLength;
_dailyEnvelopeMa.Reset();
_dailyEnvelopeMa.Length = DailyEnvelopePeriod;
_h4EnvelopeMa.Reset();
_h4EnvelopeMa.Length = H4EnvelopePeriod;
Array.Clear(_fastHistory, 0, _fastHistory.Length);
Array.Clear(_slowHistory, 0, _slowHistory.Length);
Array.Clear(_timeHistory, 0, _timeHistory.Length);
Array.Clear(_highStopHistory, 0, _highStopHistory.Length);
Array.Clear(_lowStopHistory, 0, _lowStopHistory.Length);
_historyCount = 0;
_stopHistoryCount = 0;
_recentHighs.Clear();
_recentLows.Clear();
_rollingHigh = 0m;
_rollingLow = 0m;
_priceStep = 0m;
_pipSize = 0m;
_primaryTimeFrame = default;
_highLowLength = 0;
_lastRsi = 0m;
_rsiReady = false;
_dailyUpper = 0m;
_dailyLower = 0m;
_dailyReady = false;
_h4Upper = 0m;
_h4Lower = 0m;
_h4Ready = false;
_blackoutUntil = null;
_entryPrice = null;
_stopLoss = null;
_tp1 = null;
_tp2 = null;
_trailingStop = null;
_tp1Hit = false;
_pendingDirection = 0;
_triggerPrice = 0m;
_windowEndTime = null;
_crossPrice = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
StartProtection(null, null);
_priceStep = Security?.PriceStep ?? 1m;
var decimals = Security?.Decimals ?? 0;
var pipFactor = decimals is 3 or 5 ? 10m : 1m;
_pipSize = _priceStep * pipFactor;
_primaryTimeFrame = CandleType.Arg is TimeSpan span && span > TimeSpan.Zero ? span : TimeSpan.FromMinutes(1);
_highLowLength = Math.Max(1, (int)Math.Round(HighLowHours * 60m / (decimal)_primaryTimeFrame.TotalMinutes, MidpointRounding.AwayFromZero));
var fastMa = new EMA { Length = FastMaPeriod };
var slowMa = new SMA { Length = SlowMaPeriod };
var mainSubscription = SubscribeCandles(CandleType);
mainSubscription
.Bind(fastMa, slowMa, ProcessPrimary)
.Start();
_rsi.Length = RsiLength;
SubscribeCandles(RsiTimeFrame)
.Bind(_rsi, ProcessRsi)
.Start();
_dailyEnvelopeMa.Length = DailyEnvelopePeriod;
SubscribeCandles(DailyEnvelopeTimeFrame)
.Bind(_dailyEnvelopeMa, ProcessDailyEnvelope)
.Start();
_h4EnvelopeMa.Length = H4EnvelopePeriod;
SubscribeCandles(H4EnvelopeTimeFrame)
.Bind(_h4EnvelopeMa, ProcessH4Envelope)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, mainSubscription);
DrawIndicator(area, fastMa);
DrawIndicator(area, slowMa);
DrawOwnTrades(area);
}
}
private void ProcessPrimary(ICandleMessage candle, decimal fast, decimal slow)
{
if (candle.State != CandleStates.Finished)
return;
UpdateHighLow(candle);
UpdateStopHistory(candle);
UpdateHistory(candle, fast, slow);
UpdateBlackout(candle.OpenTime);
if (!IsFormedAndOnlineAndAllowTrading())
return;
EvaluateEntry(candle);
ManagePosition(candle);
}
private void ProcessRsi(ICandleMessage candle, decimal rsiValue)
{
if (candle.State != CandleStates.Finished)
return;
_lastRsi = rsiValue;
_rsiReady = true;
}
private void ProcessDailyEnvelope(ICandleMessage candle, decimal basis)
{
if (candle.State != CandleStates.Finished)
return;
var deviation = DailyEnvelopeDeviation / 100m;
_dailyUpper = basis * (1 + deviation);
_dailyLower = basis * (1 - deviation);
_dailyReady = _dailyEnvelopeMa.IsFormed;
}
private void ProcessH4Envelope(ICandleMessage candle, decimal basis)
{
if (candle.State != CandleStates.Finished)
return;
var deviation = H4EnvelopeDeviation / 100m;
_h4Upper = basis * (1 + deviation);
_h4Lower = basis * (1 - deviation);
_h4Ready = _h4EnvelopeMa.IsFormed;
}
private void UpdateBlackout(DateTimeOffset currentTime)
{
if (_blackoutUntil is DateTimeOffset until && currentTime >= until)
{
_blackoutUntil = null;
}
}
private void UpdateHistory(ICandleMessage candle, decimal fast, decimal slow)
{
ShiftHistory(_fastHistory, fast);
ShiftHistory(_slowHistory, slow);
ShiftHistory(_timeHistory, candle.OpenTime);
if (_historyCount < _fastHistory.Length)
{
_historyCount++;
}
if (_historyCount < _fastHistory.Length)
return;
var crossUp1 = _fastHistory[1] > _slowHistory[1] && _fastHistory[2] < _slowHistory[2];
var crossUp2 = _fastHistory[2] > _slowHistory[2] && _fastHistory[3] < _slowHistory[3];
var crossDown1 = _fastHistory[1] < _slowHistory[1] && _fastHistory[2] > _slowHistory[2];
var crossDown2 = _fastHistory[2] < _slowHistory[2] && _fastHistory[3] > _slowHistory[3];
if (crossUp1)
{
PrepareTrigger(1, (_fastHistory[1] + _fastHistory[2] + _slowHistory[1] + _slowHistory[2]) / 4m, _timeHistory[1]);
}
else if (crossUp2)
{
PrepareTrigger(1, (_fastHistory[2] + _fastHistory[3] + _slowHistory[2] + _slowHistory[3]) / 4m, _timeHistory[2]);
}
else if (crossDown1)
{
PrepareTrigger(-1, (_fastHistory[1] + _fastHistory[2] + _slowHistory[1] + _slowHistory[2]) / 4m, _timeHistory[1]);
}
else if (crossDown2)
{
PrepareTrigger(-1, (_fastHistory[2] + _fastHistory[3] + _slowHistory[2] + _slowHistory[3]) / 4m, _timeHistory[2]);
}
}
private void PrepareTrigger(int direction, decimal crossPrice, DateTimeOffset crossTime)
{
_pendingDirection = direction;
_crossPrice = crossPrice;
_triggerPrice = direction > 0 ? crossPrice + TriggerPips * _pipSize : crossPrice - TriggerPips * _pipSize;
_windowEndTime = crossTime + _primaryTimeFrame + _primaryTimeFrame;
}
private void UpdateStopHistory(ICandleMessage candle)
{
ShiftHistory(_highStopHistory, candle.HighPrice);
ShiftHistory(_lowStopHistory, candle.LowPrice);
if (_stopHistoryCount < _highStopHistory.Length)
{
_stopHistoryCount++;
}
}
private void UpdateHighLow(ICandleMessage candle)
{
lock (_recentHighs)
{
_recentHighs.Enqueue(candle.HighPrice);
TrimQueue(_recentHighs, _highLowLength);
if (_recentHighs.Count >= _highLowLength)
{
var highs = new decimal[_recentHighs.Count];
_recentHighs.CopyTo(highs, 0);
_rollingHigh = GetExtreme(highs, true);
}
}
lock (_recentLows)
{
_recentLows.Enqueue(candle.LowPrice);
TrimQueue(_recentLows, _highLowLength);
if (_recentLows.Count >= _highLowLength)
{
var lows = new decimal[_recentLows.Count];
_recentLows.CopyTo(lows, 0);
_rollingLow = GetExtreme(lows, false);
}
}
}
private void EvaluateEntry(ICandleMessage candle)
{
if (_pendingDirection == 0)
return;
if (_windowEndTime is DateTimeOffset end && candle.OpenTime > end)
{
_pendingDirection = 0;
return;
}
if (_blackoutUntil is not null && candle.OpenTime < _blackoutUntil)
return;
if (Position != 0 || _entryPrice.HasValue)
return;
if (!_rsiReady)
return;
var priceReached = _pendingDirection > 0
? candle.HighPrice >= _triggerPrice
: candle.LowPrice <= _triggerPrice;
if (!priceReached)
return;
if (_pendingDirection > 0)
{
if (_lastRsi <= RsiUpper)
return;
var stopLoss = GetStopPrice(false);
if (stopLoss is null)
return;
BuyMarket();
InitializePositionState(candle.ClosePrice, stopLoss.Value, true);
}
else
{
if (_lastRsi >= RsiLower)
return;
var stopLoss = GetStopPrice(true);
if (stopLoss is null)
return;
SellMarket();
InitializePositionState(candle.ClosePrice, stopLoss.Value, false);
}
_blackoutUntil = candle.OpenTime + TimeSpan.FromHours(BlackoutHours);
_pendingDirection = 0;
}
private decimal? GetStopPrice(bool isShort)
{
if (_stopHistoryCount <= StopLossLookback)
return null;
var index = StopLossLookback;
return isShort ? _highStopHistory[index] : _lowStopHistory[index];
}
private void InitializePositionState(decimal entryPrice, decimal stopPrice, bool isLong)
{
_entryPrice = entryPrice;
_stopLoss = stopPrice;
_tp1Hit = false;
_trailingStop = null;
if (TakeProfit1Pips > 0)
{
_tp1 = isLong ? entryPrice + TakeProfit1Pips * _pipSize : entryPrice - TakeProfit1Pips * _pipSize;
}
else
{
_tp1 = null;
}
if (TakeProfit2Pips > 0)
{
_tp2 = isLong ? entryPrice + TakeProfit2Pips * _pipSize : entryPrice - TakeProfit2Pips * _pipSize;
}
else
{
_tp2 = null;
}
}
private void ManagePosition(ICandleMessage candle)
{
if (_entryPrice is null)
return;
if (Position > 0)
{
UpdateTrailingStop(candle.ClosePrice, true);
if (_stopLoss is decimal stop && candle.LowPrice <= stop)
{
SellMarket(Position);
ResetPositionState();
return;
}
if (_trailingStop is decimal trail && candle.LowPrice <= trail)
{
SellMarket(Position);
ResetPositionState();
return;
}
if (!_tp1Hit && _tp1 is decimal tp1 && candle.HighPrice >= tp1)
{
SellMarket(Position / 2m);
_tp1Hit = true;
}
if (_tp2 is decimal tp2 && candle.HighPrice >= tp2)
{
SellMarket(Position);
ResetPositionState();
}
}
else if (Position < 0)
{
UpdateTrailingStop(candle.ClosePrice, false);
if (_stopLoss is decimal stop && candle.HighPrice >= stop)
{
BuyMarket(Math.Abs(Position));
ResetPositionState();
return;
}
if (_trailingStop is decimal trail && candle.HighPrice >= trail)
{
BuyMarket(Math.Abs(Position));
ResetPositionState();
return;
}
if (!_tp1Hit && _tp1 is decimal tp1 && candle.LowPrice <= tp1)
{
BuyMarket(Math.Abs(Position) / 2m);
_tp1Hit = true;
}
if (_tp2 is decimal tp2 && candle.LowPrice <= tp2)
{
BuyMarket(Math.Abs(Position));
ResetPositionState();
}
}
else
{
ResetPositionState();
}
}
private void UpdateTrailingStop(decimal closePrice, bool isLong)
{
if (TrailingStopPips <= 0)
return;
var candidate = isLong
? closePrice - TrailingStopPips * _pipSize
: closePrice + TrailingStopPips * _pipSize;
if (_trailingStop is null)
{
_trailingStop = candidate;
}
else if (isLong && candidate > _trailingStop)
{
_trailingStop = candidate;
}
else if (!isLong && candidate < _trailingStop)
{
_trailingStop = candidate;
}
}
private void ResetPositionState()
{
_entryPrice = null;
_stopLoss = null;
_tp1 = null;
_tp2 = null;
_trailingStop = null;
_tp1Hit = false;
}
private static void ShiftHistory<T>(T[] array, T value)
{
for (var i = array.Length - 1; i > 0; i--)
{
array[i] = array[i - 1];
}
array[0] = value;
}
private static void TrimQueue(Queue<decimal> queue, int maxLength)
{
while (queue.Count > maxLength)
{
queue.Dequeue();
}
}
private static decimal GetExtreme(IEnumerable<decimal> values, bool isMax)
{
var extreme = isMax ? decimal.MinValue : decimal.MaxValue;
foreach (var value in values)
{
extreme = isMax
? (value > extreme ? value : extreme)
: (value < extreme ? value : extreme);
}
return extreme;
}
}
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, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import ExponentialMovingAverage, SimpleMovingAverage, RelativeStrengthIndex
from StockSharp.Algo.Strategies import Strategy
class hercules_atc2006_strategy(Strategy):
def __init__(self):
super(hercules_atc2006_strategy, self).__init__()
self._trigger_pips = self.Param("TriggerPips", 38)
self._trailing_stop_pips = self.Param("TrailingStopPips", 90)
self._take_profit1_pips = self.Param("TakeProfit1Pips", 210)
self._take_profit2_pips = self.Param("TakeProfit2Pips", 280)
self._fast_ma_period = self.Param("FastMaPeriod", 1)
self._slow_ma_period = self.Param("SlowMaPeriod", 72)
self._stop_loss_lookback = self.Param("StopLossLookback", 4)
self._high_low_hours = self.Param("HighLowHours", 10)
self._blackout_hours = self.Param("BlackoutHours", 4)
self._rsi_length_param = self.Param("RsiLength", 10)
self._rsi_upper = self.Param("RsiUpper", 55.0)
self._rsi_lower = self.Param("RsiLower", 45.0)
self._daily_envelope_period = self.Param("DailyEnvelopePeriod", 24)
self._daily_envelope_deviation = self.Param("DailyEnvelopeDeviation", 0.99)
self._h4_envelope_period = self.Param("H4EnvelopePeriod", 96)
self._h4_envelope_deviation = self.Param("H4EnvelopeDeviation", 0.1)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._rsi_time_frame = self.Param("RsiTimeFrame", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._daily_envelope_tf = self.Param("DailyEnvelopeTimeFrame", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._h4_envelope_tf = self.Param("H4EnvelopeTimeFrame", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self.Volume = 2.0
self._fast_history = [0.0, 0.0, 0.0, 0.0]
self._slow_history = [0.0, 0.0, 0.0, 0.0]
self._time_history = [None, None, None, None]
self._high_stop_history = [0.0, 0.0, 0.0, 0.0, 0.0]
self._low_stop_history = [0.0, 0.0, 0.0, 0.0, 0.0]
self._history_count = 0
self._stop_history_count = 0
self._recent_highs = []
self._recent_lows = []
self._rolling_high = 0.0
self._rolling_low = 0.0
self._price_step = 1.0
self._pip_size = 1.0
self._primary_tf = TimeSpan.FromMinutes(5)
self._high_low_length = 1
self._pending_direction = 0
self._trigger_price = 0.0
self._window_end_time = None
self._cross_price = 0.0
self._last_rsi = 0.0
self._rsi_ready = False
self._daily_upper = 0.0
self._daily_lower = 0.0
self._daily_ready = False
self._h4_upper = 0.0
self._h4_lower = 0.0
self._h4_ready = False
self._blackout_until = None
self._entry_price = None
self._stop_loss = None
self._tp1 = None
self._tp2 = None
self._trailing_stop = None
self._tp1_hit = False
@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(hercules_atc2006_strategy, self).OnStarted2(time)
self._fast_history = [0.0, 0.0, 0.0, 0.0]
self._slow_history = [0.0, 0.0, 0.0, 0.0]
self._time_history = [None, None, None, None]
self._high_stop_history = [0.0, 0.0, 0.0, 0.0, 0.0]
self._low_stop_history = [0.0, 0.0, 0.0, 0.0, 0.0]
self._history_count = 0
self._stop_history_count = 0
self._recent_highs = []
self._recent_lows = []
self._rolling_high = 0.0
self._rolling_low = 0.0
self._pending_direction = 0
self._trigger_price = 0.0
self._window_end_time = None
self._cross_price = 0.0
self._last_rsi = 0.0
self._rsi_ready = False
self._daily_upper = 0.0
self._daily_lower = 0.0
self._daily_ready = False
self._h4_upper = 0.0
self._h4_lower = 0.0
self._h4_ready = False
self._blackout_until = None
self._entry_price = None
self._stop_loss = None
self._tp1 = None
self._tp2 = None
self._trailing_stop = None
self._tp1_hit = False
self.StartProtection(None, None)
self._price_step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 1.0
decimals = int(self.Security.Decimals) if self.Security is not None and self.Security.Decimals is not None else 0
pip_factor = 10.0 if decimals in (3, 5) else 1.0
self._pip_size = self._price_step * pip_factor
ct = self.CandleType
arg = ct.Arg
if arg is not None and hasattr(arg, 'TotalMinutes') and arg.TotalMinutes > 0:
self._primary_tf = arg
else:
self._primary_tf = TimeSpan.FromMinutes(1)
tf_minutes = self._primary_tf.TotalMinutes
if tf_minutes > 0:
self._high_low_length = max(1, int(round(float(self._high_low_hours.Value) * 60.0 / tf_minutes)))
else:
self._high_low_length = 1
fast_ma = ExponentialMovingAverage()
fast_ma.Length = int(self._fast_ma_period.Value)
slow_ma = SimpleMovingAverage()
slow_ma.Length = int(self._slow_ma_period.Value)
main_sub = self.SubscribeCandles(self.CandleType)
main_sub.Bind(fast_ma, slow_ma, self._process_primary).Start()
self._rsi_ind = RelativeStrengthIndex()
self._rsi_ind.Length = int(self._rsi_length_param.Value)
rsi_sub = self.SubscribeCandles(self._rsi_time_frame.Value)
rsi_sub.Bind(self._rsi_ind, self._process_rsi).Start()
self._daily_ma = SimpleMovingAverage()
self._daily_ma.Length = int(self._daily_envelope_period.Value)
daily_sub = self.SubscribeCandles(self._daily_envelope_tf.Value)
daily_sub.Bind(self._daily_ma, self._process_daily_envelope).Start()
self._h4_ma = SimpleMovingAverage()
self._h4_ma.Length = int(self._h4_envelope_period.Value)
h4_sub = self.SubscribeCandles(self._h4_envelope_tf.Value)
h4_sub.Bind(self._h4_ma, self._process_h4_envelope).Start()
def _process_rsi(self, candle, rsi_value):
if candle.State != CandleStates.Finished:
return
self._last_rsi = float(rsi_value)
self._rsi_ready = True
def _process_daily_envelope(self, candle, basis):
if candle.State != CandleStates.Finished:
return
dev = float(self._daily_envelope_deviation.Value) / 100.0
b = float(basis)
self._daily_upper = b * (1.0 + dev)
self._daily_lower = b * (1.0 - dev)
self._daily_ready = self._daily_ma.IsFormed
def _process_h4_envelope(self, candle, basis):
if candle.State != CandleStates.Finished:
return
dev = float(self._h4_envelope_deviation.Value) / 100.0
b = float(basis)
self._h4_upper = b * (1.0 + dev)
self._h4_lower = b * (1.0 - dev)
self._h4_ready = self._h4_ma.IsFormed
def _process_primary(self, candle, fast_value, slow_value):
if candle.State != CandleStates.Finished:
return
fast = float(fast_value)
slow = float(slow_value)
self._update_high_low(candle)
self._update_stop_history(candle)
self._update_history(candle, fast, slow)
self._update_blackout(candle.OpenTime)
if not self.IsFormedAndOnlineAndAllowTrading():
return
self._evaluate_entry(candle)
self._manage_position(candle)
def _shift_history(self, arr, value):
for i in range(len(arr) - 1, 0, -1):
arr[i] = arr[i - 1]
arr[0] = value
def _update_history(self, candle, fast, slow):
self._shift_history(self._fast_history, fast)
self._shift_history(self._slow_history, slow)
self._shift_history(self._time_history, candle.OpenTime)
if self._history_count < len(self._fast_history):
self._history_count += 1
if self._history_count < len(self._fast_history):
return
cross_up1 = self._fast_history[1] > self._slow_history[1] and self._fast_history[2] < self._slow_history[2]
cross_up2 = self._fast_history[2] > self._slow_history[2] and self._fast_history[3] < self._slow_history[3]
cross_down1 = self._fast_history[1] < self._slow_history[1] and self._fast_history[2] > self._slow_history[2]
cross_down2 = self._fast_history[2] < self._slow_history[2] and self._fast_history[3] > self._slow_history[3]
if cross_up1:
cp = (self._fast_history[1] + self._fast_history[2] + self._slow_history[1] + self._slow_history[2]) / 4.0
self._prepare_trigger(1, cp, self._time_history[1])
elif cross_up2:
cp = (self._fast_history[2] + self._fast_history[3] + self._slow_history[2] + self._slow_history[3]) / 4.0
self._prepare_trigger(1, cp, self._time_history[2])
elif cross_down1:
cp = (self._fast_history[1] + self._fast_history[2] + self._slow_history[1] + self._slow_history[2]) / 4.0
self._prepare_trigger(-1, cp, self._time_history[1])
elif cross_down2:
cp = (self._fast_history[2] + self._fast_history[3] + self._slow_history[2] + self._slow_history[3]) / 4.0
self._prepare_trigger(-1, cp, self._time_history[2])
def _prepare_trigger(self, direction, cross_price, cross_time):
self._pending_direction = direction
self._cross_price = cross_price
pip = self._pip_size
trigger_pips = float(self._trigger_pips.Value)
if direction > 0:
self._trigger_price = cross_price + trigger_pips * pip
else:
self._trigger_price = cross_price - trigger_pips * pip
self._window_end_time = cross_time + self._primary_tf + self._primary_tf
def _update_stop_history(self, candle):
self._shift_history(self._high_stop_history, float(candle.HighPrice))
self._shift_history(self._low_stop_history, float(candle.LowPrice))
if self._stop_history_count < len(self._high_stop_history):
self._stop_history_count += 1
def _update_high_low(self, candle):
high = float(candle.HighPrice)
low = float(candle.LowPrice)
self._recent_highs.append(high)
while len(self._recent_highs) > self._high_low_length:
self._recent_highs.pop(0)
if len(self._recent_highs) >= self._high_low_length:
self._rolling_high = max(self._recent_highs)
self._recent_lows.append(low)
while len(self._recent_lows) > self._high_low_length:
self._recent_lows.pop(0)
if len(self._recent_lows) >= self._high_low_length:
self._rolling_low = min(self._recent_lows)
def _update_blackout(self, current_time):
if self._blackout_until is not None and current_time >= self._blackout_until:
self._blackout_until = None
def _evaluate_entry(self, candle):
if self._pending_direction == 0:
return
if self._window_end_time is not None and candle.OpenTime > self._window_end_time:
self._pending_direction = 0
return
if self._blackout_until is not None and candle.OpenTime < self._blackout_until:
return
pos = float(self.Position)
if pos != 0 or self._entry_price is not None:
return
if not self._rsi_ready:
return
high = float(candle.HighPrice)
low = float(candle.LowPrice)
close = float(candle.ClosePrice)
if self._pending_direction > 0:
if high < self._trigger_price:
return
if self._last_rsi <= float(self._rsi_upper.Value):
return
stop_price = self._get_stop_price(False)
if stop_price is None:
return
self.BuyMarket()
self._init_position_state(close, stop_price, True)
else:
if low > self._trigger_price:
return
if self._last_rsi >= float(self._rsi_lower.Value):
return
stop_price = self._get_stop_price(True)
if stop_price is None:
return
self.SellMarket()
self._init_position_state(close, stop_price, False)
self._blackout_until = candle.OpenTime + TimeSpan.FromHours(float(self._blackout_hours.Value))
self._pending_direction = 0
def _get_stop_price(self, is_short):
lookback = int(self._stop_loss_lookback.Value)
if self._stop_history_count <= lookback:
return None
if is_short:
return self._high_stop_history[lookback]
else:
return self._low_stop_history[lookback]
def _init_position_state(self, entry_price, stop_price, is_long):
self._entry_price = entry_price
self._stop_loss = stop_price
self._tp1_hit = False
self._trailing_stop = None
pip = self._pip_size
tp1_pips = float(self._take_profit1_pips.Value)
tp2_pips = float(self._take_profit2_pips.Value)
if tp1_pips > 0:
self._tp1 = entry_price + tp1_pips * pip if is_long else entry_price - tp1_pips * pip
else:
self._tp1 = None
if tp2_pips > 0:
self._tp2 = entry_price + tp2_pips * pip if is_long else entry_price - tp2_pips * pip
else:
self._tp2 = None
def _manage_position(self, candle):
if self._entry_price is None:
return
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
pip = self._pip_size
trail_pips = float(self._trailing_stop_pips.Value)
pos = float(self.Position)
if pos > 0:
self._update_trailing_stop(close, True)
if self._stop_loss is not None and low <= self._stop_loss:
self.SellMarket(pos)
self._reset_position_state()
return
if self._trailing_stop is not None and low <= self._trailing_stop:
pos = float(self.Position)
self.SellMarket(pos)
self._reset_position_state()
return
if not self._tp1_hit and self._tp1 is not None and high >= self._tp1:
pos = float(self.Position)
half = pos / 2.0
if half > 0:
self.SellMarket(half)
self._tp1_hit = True
pos = float(self.Position)
if self._tp2 is not None and high >= self._tp2:
if pos > 0:
self.SellMarket(pos)
self._reset_position_state()
elif pos < 0:
self._update_trailing_stop(close, False)
if self._stop_loss is not None and high >= self._stop_loss:
self.BuyMarket(abs(pos))
self._reset_position_state()
return
if self._trailing_stop is not None and high >= self._trailing_stop:
pos = float(self.Position)
self.BuyMarket(abs(pos))
self._reset_position_state()
return
if not self._tp1_hit and self._tp1 is not None and low <= self._tp1:
pos = float(self.Position)
half = abs(pos) / 2.0
if half > 0:
self.BuyMarket(half)
self._tp1_hit = True
pos = float(self.Position)
if self._tp2 is not None and low <= self._tp2:
if pos < 0:
self.BuyMarket(abs(pos))
self._reset_position_state()
else:
self._reset_position_state()
def _update_trailing_stop(self, close_price, is_long):
if float(self._trailing_stop_pips.Value) <= 0:
return
pip = self._pip_size
trail_pips = float(self._trailing_stop_pips.Value)
if is_long:
candidate = close_price - trail_pips * pip
else:
candidate = close_price + trail_pips * pip
if self._trailing_stop is None:
self._trailing_stop = candidate
elif is_long and candidate > self._trailing_stop:
self._trailing_stop = candidate
elif not is_long and candidate < self._trailing_stop:
self._trailing_stop = candidate
def _reset_position_state(self):
self._entry_price = None
self._stop_loss = None
self._tp1 = None
self._tp2 = None
self._trailing_stop = None
self._tp1_hit = False
def OnReseted(self):
super(hercules_atc2006_strategy, self).OnReseted()
self._fast_history = [0.0, 0.0, 0.0, 0.0]
self._slow_history = [0.0, 0.0, 0.0, 0.0]
self._time_history = [None, None, None, None]
self._high_stop_history = [0.0, 0.0, 0.0, 0.0, 0.0]
self._low_stop_history = [0.0, 0.0, 0.0, 0.0, 0.0]
self._history_count = 0
self._stop_history_count = 0
self._recent_highs = []
self._recent_lows = []
self._rolling_high = 0.0
self._rolling_low = 0.0
self._price_step = 1.0
self._pip_size = 1.0
self._high_low_length = 1
self._pending_direction = 0
self._trigger_price = 0.0
self._window_end_time = None
self._cross_price = 0.0
self._last_rsi = 0.0
self._rsi_ready = False
self._daily_upper = 0.0
self._daily_lower = 0.0
self._daily_ready = False
self._h4_upper = 0.0
self._h4_lower = 0.0
self._h4_ready = False
self._blackout_until = None
self._entry_price = None
self._stop_loss = None
self._tp1 = None
self._tp2 = None
self._trailing_stop = None
self._tp1_hit = False
def CreateClone(self):
return hercules_atc2006_strategy()