Hercules A.T.C. 2006-Strategie
Übersicht
Hercules A.T.C. 2006 ist eine Trendfolge-Strategie auf höheren Zeitrahmen, die den 2006 veröffentlichten MetaTrader Expert Advisor nachbildet. Die StockSharp-Version wartet auf abgeschlossene Kerzen im primären Zeitrahmen, erkennt bullische/bärische Kreuzungen zwischen einer schnellen EMA(1) und einer langsamen SMA(72) und öffnet Trades nur, wenn zusätzliche Filter den Ausbruch bestätigen. Die Strategie teilt ihre Position in zwei Tranchen mit unabhängigen Take-Profit-Niveaus auf und zieht den Stop nach, sobald der Preis voranschreitet.
Indikatoren und Daten
- Primäre Kerzen: konfigurierbar (Standard: 1-Stunden-Kerzen).
- Schnelle MA: EMA mit Länge
FastMaPeriod(Standard: 1). - Langsame MA: SMA mit Länge
SlowMaPeriod(Standard: 72). - RSI-Filter: RSI der Länge
RsiLengthauf demRsiTimeFrame(Standard: 1 Stunde). - Tages-Envelope: SMA der Länge
DailyEnvelopePeriodaufDailyEnvelopeTimeFramemit ±DailyEnvelopeDeviationProzent Versatz. - H4-Envelope: SMA der Länge
H4EnvelopePeriodaufH4EnvelopeTimeFramemit ±H4EnvelopeDeviationProzent Versatz. - Rollendes Hoch/Tief: höchstes Hoch und tiefstes Tief der letzten
HighLowHoursStunden im primären Zeitrahmen.
Parameter
| Name | Standard | Beschreibung |
|---|---|---|
TriggerPips |
38 | Versatz in Pips, der zum Kreuzungspreis addiert/subtrahiert wird, bevor eine Order ausgelöst wird. |
TrailingStopPips |
90 | Trailing-Stop-Abstand in Pips (0 deaktiviert das Trailing). |
TakeProfit1Pips |
210 | Erster Take-Profit-Abstand in Pips zum Skalieren der Hälfte der Position. |
TakeProfit2Pips |
280 | Letzter Take-Profit-Abstand in Pips zum Schließen der verbleibenden Position. |
FastMaPeriod |
1 | Länge der schnellen EMA im Kreuzungsdetektor. |
SlowMaPeriod |
72 | Länge der langsamen SMA-Basislinie. |
StopLossLookback |
4 | Anzahl abgeschlossener Kerzen für den anfänglichen Stop-Preis. |
HighLowHours |
10 | Größe des rollenden Fensters (in Stunden) für den Ausbruchsfilter. |
BlackoutHours |
144 | Abkühlzeit (in Stunden) nach dem Schließen eines Trades vor einem neuen Einstieg. |
RsiLength |
10 | RSI-Länge im höheren Zeitrahmen-Filter. |
RsiUpper |
55 | Minimaler RSI-Wert für Long-Einstiege. |
RsiLower |
45 | Maximaler RSI-Wert vor dem Blockieren von Short-Einstiegen. |
DailyEnvelopePeriod |
24 | SMA-Länge für den Tages-Envelope-Filter. |
DailyEnvelopeDeviation |
0.99 | Tages-Envelope-Abweichung in Prozent. |
H4EnvelopePeriod |
96 | SMA-Länge für den Vier-Stunden-Envelope-Filter. |
H4EnvelopeDeviation |
0.1 | Vier-Stunden-Envelope-Abweichung in Prozent. |
CandleType |
1 Stunde | Primärer Kerzentyp. |
RsiTimeFrame |
1 Stunde | Kerzentyp für den RSI-Filter. |
DailyEnvelopeTimeFrame |
1 Tag | Kerzentyp für den Tages-Envelope. |
H4EnvelopeTimeFrame |
4 Stunden | Kerzentyp für den Vier-Stunden-Envelope. |
Handelsregeln
Kreuzungserkennung
- EMA(1)- und SMA(72)-Werte der letzten drei abgeschlossenen Balken beobachten.
- Bullisches Signal erkennen, wenn EMA in einem der zwei vorherigen Balken über SMA kreuzt.
- Bärisches Signal erkennen, wenn EMA in einem der zwei vorherigen Balken unter SMA kreuzt.
- Den Kreuzungspreis (Durchschnitt der schnellen und langsamen Werte) speichern und ein Zwei-Balken-Triggerfenster starten.
Triggerbedingung
TriggerPrice = CrossPrice ± TriggerPipsberechnen (in Preiseinheiten umgerechnet).- Der Trigger bleibt zwei primäre Kerzen nach dem Kreuzungszeitpunkt gültig.
- Longs erfordern, dass das Kerzenhoch den bullischen Triggerpreis erreicht oder überschreitet.
- Shorts erfordern, dass das Kerzentief den bärischen Triggerpreis erreicht oder durchbricht.
Einstiegsfilter
- Keine offene Position und keine aktive Abkühlzeit (
BlackoutHours). - RSI-Filter:
RSI > RsiUpperfür Longs,RSI < RsiLowerfür Shorts. - Ausbruchsfilter: aktueller Schlusskurs muss für Longs das rollende Hoch überschreiten oder für Shorts unter das rollende Tief fallen.
- Envelope-Bestätigung: aktueller Schlusskurs muss für Longs über beiden oberen Envelope-Bändern liegen oder für Shorts unter beiden unteren Bändern.
- Keine offene Position und keine aktive Abkühlzeit (
Orderausführung
- Marktorder mit dem Strategie-Volumen senden (Standard: 2 Einheiten, d.h. zwei gleiche Teilpositionen).
- Stop-Loss: Tief (Long) oder Hoch (Short) der
StopLossLookback-ten Kerze. - Take-Profit-Niveaus:
TakeProfit1Pipsfür die erste Hälfte,TakeProfit2Pipsfür den Rest. - Blackout-Timer starten, um neue Einstiege für
BlackoutHoursStunden zu blockieren.
Positionsmanagement
- Trailing-Stop aktiviert sich sofort, wenn
TrailingStopPips> 0, und bewegt sich nur zugunsten des Trades. - Hälfte der Position beim ersten Take-Profit-Niveau skalieren.
- Verbleibende Position schließen, wenn der letzte Take-Profit ausgelöst wird, der Stop-Loss getroffen wird oder der Preis den Trailing-Stop kreuzt.
- Trailing-Stop aktiviert sich sofort, wenn
Risikomanagement
- Stops werden immer aus abgeschlossenen Kerzen abgeleitet, um Intrabar-Rauschen zu reduzieren.
- Zwei Take-Profit-Ziele sichern Teilgewinne, bevor der Trade weiterläuft.
- Trailing-Stops schützen Gewinne, nachdem sich der Markt in die gewünschte Richtung bewegt hat.
- Eine lange Sperrzeit (Standard: 144 Stunden) verhindert schnellen Wiedereinstieg nach einem Ausbruch und spiegelt das ursprüngliche EA-Verhalten wider.
Hinweise
- Der StockSharp-Port bewahrt die ursprüngliche Money-Management-Idee, indem er das Strategie-Volumen standardmäßig auf zwei Einheiten setzt, sodass der Teilausstieg die Hälfte der Position laufen lässt.
- Envelope-Versatzwerte aus MetaTrader werden durch die Verwendung der aktuellsten Envelope-Werte angenähert, da Vorwärtsverschiebung von der High-Level-API nicht unterstützt wird.
- Die Strategie benötigt Preisschrittinformationen für die korrekte Übersetzung von Pip-Abständen; sicherstellen, dass die Instrument-Metadaten vollständig sind.
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()