Hercules A.T.C. 2006 Strategy
Overview
Hercules A.T.C. 2006 is a high time frame trend-following strategy that recreates the MetaTrader expert advisor published in 2006. The StockSharp version listens to completed candles on the primary time frame, watches for a bullish/bearish crossover between a fast EMA(1) and a slow SMA(72), and opens trades only when additional filters confirm the breakout. The strategy splits its position into two tranches with independent take-profit levels and trails the stop once price advances.
Indicators and Data
- Primary candles: configurable (defaults to 1-hour candles).
- Fast MA: EMA with length
FastMaPeriod(default 1). - Slow MA: SMA with length
SlowMaPeriod(default 72). - RSI filter: RSI of length
RsiLengthon theRsiTimeFrame(default 1-hour). - Daily envelope: SMA of length
DailyEnvelopePeriodonDailyEnvelopeTimeFramewith ±DailyEnvelopeDeviationpercent offset. - H4 envelope: SMA of length
H4EnvelopePeriodonH4EnvelopeTimeFramewith ±H4EnvelopeDeviationpercent offset. - Rolling high/low: highest high and lowest low for the past
HighLowHourshours on the primary time frame.
Parameters
| Name | Default | Description |
|---|---|---|
TriggerPips |
38 | Offset in pips added/subtracted to the crossover price before triggering an order. |
TrailingStopPips |
90 | Trailing stop distance in pips (0 disables trailing). |
TakeProfit1Pips |
210 | First take-profit distance in pips for scaling out half of the position. |
TakeProfit2Pips |
280 | Final take-profit distance in pips used to close the remaining position. |
FastMaPeriod |
1 | Length of the fast EMA used in the crossover detector. |
SlowMaPeriod |
72 | Length of the slow SMA baseline. |
StopLossLookback |
4 | Number of completed candles used to pull the initial stop price. |
HighLowHours |
10 | Size of the rolling window (in hours) used for the breakout filter. |
BlackoutHours |
144 | Cooldown period (in hours) after a trade closes before a new entry is allowed. |
RsiLength |
10 | RSI length on the higher time frame filter. |
RsiUpper |
55 | Minimum RSI value required to allow long entries. |
RsiLower |
45 | Maximum RSI value allowed before short entries are blocked. |
DailyEnvelopePeriod |
24 | SMA length for the daily envelope filter. |
DailyEnvelopeDeviation |
0.99 | Daily envelope deviation in percent. |
H4EnvelopePeriod |
96 | SMA length for the four-hour envelope filter. |
H4EnvelopeDeviation |
0.1 | Four-hour envelope deviation in percent. |
CandleType |
1 hour | Primary working candle type. |
RsiTimeFrame |
1 hour | Candle type used for the RSI filter. |
DailyEnvelopeTimeFrame |
1 day | Candle type used for the daily envelope. |
H4EnvelopeTimeFrame |
4 hours | Candle type used for the four-hour envelope. |
Trading Rules
Crossover detection
- Watch the EMA(1) and SMA(72) values from the last three completed bars.
- Detect a bullish signal when EMA crosses above SMA during either of the two previous bars.
- Detect a bearish signal when EMA crosses below SMA during either of the two previous bars.
- Store the crossover price (average of the fast and slow values) and start a two-bar trigger window.
Trigger condition
- Calculate
TriggerPrice = CrossPrice ± TriggerPips(converted to price units). - The trigger remains valid for two primary candles after the crossover time.
- Longs require the candle high to reach or exceed the bullish trigger price.
- Shorts require the candle low to reach or break the bearish trigger price.
- Calculate
Entry filters
- No existing position and no open cooldown (
BlackoutHours). - RSI filter:
RSI > RsiUpperfor longs,RSI < RsiLowerfor shorts. - Breakout filter: current close must exceed the rolling high for longs or fall below the rolling low for shorts.
- Envelope confirmation: current close must be above both upper envelope bands for longs or below both lower bands for shorts.
- No existing position and no open cooldown (
Order execution
- Submit a market order using the strategy volume (defaults to 2 units, meaning two equal sub-positions).
- Stop loss: previous
StopLossLookback-th candle low (long) or high (short). - Take-profit levels:
TakeProfit1Pipsfor the first half,TakeProfit2Pipsfor the remainder. - Start a blackout timer to block new entries for
BlackoutHourshours.
Position management
- Trailing stop activates immediately if
TrailingStopPips> 0 and moves in favor of the trade only. - Scale-out half of the position at the first take-profit level.
- Close the remaining position when the final take-profit triggers, the stop loss is hit, or price crosses the trailing stop.
- Trailing stop activates immediately if
Risk Management
- Stops are always derived from completed candles to reduce intrabar noise.
- Two take-profit targets lock in partial profits before letting the trade run.
- Trailing stops ensure gains are protected after the market moves in the desired direction.
- A long blackout period (default 144 hours) prevents rapid re-entry after a breakout and mirrors the original EA behaviour.
Notes
- The StockSharp port preserves the original money-management idea by defaulting the strategy volume to two units, so the partial exit leaves half of the position running.
- Envelope shift values from MetaTrader are approximated by using the most recent envelope values because forward shifting is not supported by the high-level API.
- The strategy requires price step information to translate pip distances correctly; ensure the security metadata is populated.
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()