Стратегия Hercules A.T.C. 2006
Описание
Hercules A.T.C. 2006 — порт легендарного советника MetaTrader, ориентированного на среднесрочный тренд. Стратегия отслеживает закрытые свечи базового таймфрейма, выявляет пересечение EMA(1) и SMA(72) за последние два бара и открывает позиции только при выполнении ряда фильтров. Сделка разбивается на две части с раздельными целями фиксации прибыли, а стоп-приказ подтягивается по мере развития тренда.
Используемые данные
- Основные свечи: параметр
CandleType, по умолчанию 1-часовые свечи. - Быстрая средняя: EMA длиной
FastMaPeriod(стандартное значение 1). - Медленная средняя: SMA длиной
SlowMaPeriod(стандартное значение 72). - RSI: индикатор длиной
RsiLength, рассчитываемый наRsiTimeFrame. - Дневной конверт: SMA длиной
DailyEnvelopePeriodнаDailyEnvelopeTimeFrameс отклонением ±DailyEnvelopeDeviationпроцентов. - Четырёхчасовой конверт: SMA длиной
H4EnvelopePeriodнаH4EnvelopeTimeFrameс отклонением ±H4EnvelopeDeviationпроцентов. - Плавающие экстремумы: максимум и минимум за последние
HighLowHoursчасов по основному таймфрейму.
Ключевые параметры
| Параметр | Значение по умолчанию | Назначение |
|---|---|---|
TriggerPips |
38 | Смещение (в пунктах) от цены пересечения до уровня срабатывания. |
TrailingStopPips |
90 | Дистанция трейлинг-стопа (0 — отключено). |
TakeProfit1Pips |
210 | Первый тейк-профит, закрывает половину позиции. |
TakeProfit2Pips |
280 | Второй тейк-профит, закрывает остаток позиции. |
FastMaPeriod |
1 | Период быстрой EMA. |
SlowMaPeriod |
72 | Период медленной SMA. |
StopLossLookback |
4 | Количество баров для расчёта первоначального стопа. |
HighLowHours |
10 | Размер окна (в часах) для фильтра «пробой максимума/минимума». |
BlackoutHours |
144 | Длительность «тёмного периода» после сделки. |
RsiLength |
10 | Период RSI. |
RsiUpper |
55 | Минимальное значение RSI для лонгов. |
RsiLower |
45 | Максимальное значение RSI для шортов. |
DailyEnvelopePeriod |
24 | Период SMA дневного конверта. |
DailyEnvelopeDeviation |
0.99 | Отклонение дневного конверта (в процентах). |
H4EnvelopePeriod |
96 | Период SMA четырёхчасового конверта. |
H4EnvelopeDeviation |
0.1 | Отклонение четырёхчасового конверта (в процентах). |
CandleType |
1 час | Основной рабочий таймфрейм. |
RsiTimeFrame |
1 час | Таймфрейм для RSI. |
DailyEnvelopeTimeFrame |
1 день | Таймфрейм для дневного конверта. |
H4EnvelopeTimeFrame |
4 часа | Таймфрейм для четырёхчасового конверта. |
Правила торговли
Определение пересечения
- Хранить значения EMA и SMA за три последних закрытых бара.
- Если EMA пересекает SMA вверх на одном из двух прошлых баров — формировать бычий сигнал.
- Если EMA пересекает SMA вниз — медвежий сигнал.
- Рассчитывать среднюю цену пересечения и задавать двухбаровое окно для входа.
Условия срабатывания
- Лонг возможен, если максимум текущей свечи достигает
TriggerPriceвыше цены пересечения. - Шорт возможен, если минимум опускается ниже соответствующего триггера.
- Окно актуально две свечи после момента пересечения.
- Лонг возможен, если максимум текущей свечи достигает
Фильтры
- Нет открытых позиций и не истёк период блокировки (
BlackoutHours). - RSI: для покупок
RSI > RsiUpper, для продажRSI < RsiLower. - Пробой: закрытие выше динамического максимума (лонг) или ниже минимума (шорт).
- Конверты: цена должна находиться выше обоих верхних конвертов для лонга и ниже нижних для шорта.
- Нет открытых позиций и не истёк период блокировки (
Вход и сопровождение
- Отправить рыночную заявку на объём стратегии (по умолчанию 2 лота).
- Стоп ставится на минимум (лонг) или максимум (шорт) бара, отстоящего на
StopLossLookbackпозиций назад. - Установить два тейк-профита в соответствии с параметрами.
- Запустить таймер блокировки новых входов.
Управление позицией
- Трейлинг-стоп сдвигается в сторону прибыли, если параметр включён.
- На первом тейке закрывается половина позиции; остаток сопровождается до второго тейка, стопа или трейлинга.
Управление риском
- Стопы рассчитываются только по закрытым барам, что снижает ложные срабатывания.
- Поэтапная фиксация прибыли улучшает устойчивость стратегии.
- Трейлинг-стоп защищает накопленную прибыль при сильных трендах.
- Длинный «blackout» предотвращает повторный вход сразу после выброса.
Дополнительная информация
- Значение
Volumeпо умолчанию равно 2, что имитирует двойной ордер оригинального советника. - Сдвиг конвертов в MetaTrader заменён на использование актуальных значений из-за ограничений высокоуровневого API.
- Для корректного пересчёта пунктов необходимо задать минимальный шаг цены инструмента.
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()