CH2010 Structure Multi-Zeitrahmen-Ausbruchsstrategie
Diese Strategie repliziert das Verhalten des ursprünglichen ch2010structure.mq5-Experten, indem sie mehrere Forex-Paare auf zwei Zeitrahmen verfolgt. Jedes Instrument überwacht die Tageskerze, um einen Richtungsbias zu bestimmen, und beobachtet dann 30-Minuten-Kerzen auf der Suche nach Ausbrüchen jenseits des vorherigen Tagesbereichs. Marktpositionen werden eröffnet, wenn der Ausbruch mit dem Tagestrend übereinstimmt, und mit schützenden Stop-Loss- und Take-Profit-Niveaus geschlossen.
Kernlogik
Tägliche Bias-Erkennung
- Die Strategie abonniert Tageskerzen für USDCHF, GBPUSD, AUDUSD, USDJPY und EURGBP.
- Wenn eine Tageskerze endet, definiert die Schluss/Eröffnungs-Beziehung den Bias: bullisch, bärisch oder neutral.
- Das tägliche Hoch, Tief und Schluss werden zusammen mit dem Sessionsdatum gespeichert, damit die Intraday-Logik bestätigen kann, dass die gleiche Session gehandelt wird.
Intraday-Ausbruchsausführung
- 30-Minuten-Kerzen werden ausgewertet, sobald sie schließen.
- Wenn der Schluss über dem vorherigen Tageshoch plus einem konfigurierbaren Puffer liegt und der Bias nicht bärisch ist, wird ein Long-Trade ausgelöst.
- Wenn der Schluss unter dem vorherigen Tagestief minus dem Puffer liegt und der Bias nicht bullisch ist, wird ein Short-Trade ausgelöst.
- Pro Instrument und Tag kann nur ein Long- und ein Short-Ausbruch aktiviert werden, um übermäßiges Trading zu vermeiden.
Risikomanagement inspiriert von den ursprünglichen Helper-Funktionen
- Volumina werden zwischen
MinTradeVolumeundMaxTradeVolumebegrenzt und die aggregierte Position über alle Instrumente wird durchMaxAggregateVolumeeingeschränkt. - Jede gefüllte Position berechnet sofort absolute Stop-Loss- und Take-Profit-Niveaus unter Verwendung prozentualer Offsets vom Einstiegspreis.
- Positionen werden über Marktorders geschlossen, sobald der Stop oder das Ziel erreicht wird; wiederholte Exit-Orders werden durch den
ExitInProgress-Flag verhindert.
- Volumina werden zwischen
Zustandsverfolgung
- Für jedes Instrument verfolgt die Strategie seine eigenen Tagesniveaus, letzte bekannte Position, Einstiegsseite, Exit-Orders und Ausbruchs-Flags in einem
InstrumentContext. - Dies ermöglicht den Multi-Symbol-Workflow, ohne benutzerdefinierte Sammlungen außerhalb der Kontextklasse pflegen zu müssen.
- Für jedes Instrument verfolgt die Strategie seine eigenen Tagesniveaus, letzte bekannte Position, Einstiegsseite, Exit-Orders und Ausbruchs-Flags in einem
Strategie-Parameter
| Parameter | Beschreibung |
|---|---|
TradeVolume |
Basis-Volumen für neue Einstiege, unterliegt den Volumengrenzen. |
MinTradeVolume und MaxTradeVolume |
Grenzen, die den ursprünglichen MQL-Risikofilter spiegeln. |
MaxAggregateVolume |
Maximale Summe absoluter Positionen über alle gehandelten Paare. |
StopLossPercent |
Schutzstopp-Offset in Prozent vom erkannten Einstiegspreis. |
TakeProfitPercent |
Take-Profit-Offset in Prozent vom erkannten Einstiegspreis. |
BreakoutBufferPercent |
Prozentsatz des vorherigen Tagesbereichs, der zu Ausbruchsauslösern hinzugefügt wird. |
DailyCandleType |
DataType zum Anfordern der höheren Zeitrahmen-Kerzen. |
IntradayCandleType |
DataType zum Anfordern der Ausführungszeitrahmen-Kerzen. |
UsdChfSecurity .. EurGbpSecurity |
Wertpapier-Objekte für die standardmäßig überwachten fünf Forex-Symbole. |
Erforderliche Daten
- Tageskerzen für jedes konfigurierte Symbol (Standard: 1-Tages-Zeitrahmen).
- Intraday-Kerzen (Standard: 30 Minuten) für dieselben Symbole.
- Echtzeit-Order-Routing zum Einreichen von Marktorders für jedes Wertpapier.
Verwendungshinweise
- Die fünf Wertpapier-Parameter vor dem Start der Strategie konfigurieren. Sie können bei Bedarf durch andere Instrumente ersetzt werden.
- Portfolio und Connector wie in anderen StockSharp-Strategien einrichten.
- Optional den Ausbruchspuffer oder Risikoparameter anpassen, um die Kontraktspezifikationen des Ziel-Brokers zu berücksichtigen.
- Die Strategie starten. Sie wird automatisch beide Kerzenströme für jedes Instrument abonnieren, die Tagesstruktur protokollieren und auf gültige Intraday-Ausbrüche warten.
- Das Log auf Einträge wie
Daily candle capturedundEnter Buyüberwachen, um den Entscheidungsfluss zu überprüfen.
Unterschiede vs. dem ursprünglichen MQL-Experten
- Ausstehende Orders werden durch sofortige Marktorders ersetzt, sobald die Ausbruchsbedingung beobachtet wird. Dies hält die Logik kompatibel mit der StockSharp High-Level-API und bewahrt die Idee, die Exposition zu begrenzen und nur einmal pro Richtung täglich zu reagieren.
- Volumenbeschränkungen aus dem
DebugOrderSend-Helper wurden in Parameter angepasst, die einzelne Trade-Größen und die Gesamtexposition begrenzen. - Umfangreiche Protokollierung wurde hinzugefügt, um Tagesniveaus, Einstiegsgründe und Exit-Auslöser in englischen Kommentaren für einfacheres Debugging in StockSharp anzuzeigen.
Haftungsausschluss
Dieses Beispiel dient zu Bildungszwecken. Parameter und Wertpapiere sollten überprüft und angepasst werden, bevor die Strategie im Produktionshandel eingesetzt wird.
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>
/// Multi-currency breakout strategy converted from the original CH2010 structure expert.
/// Watches daily candles to define trend bias and 30-minute candles for entries and exits.
/// </summary>
public class Ch2010StructureStrategy : Strategy
{
private readonly StrategyParam<Security> _usdChf;
private readonly StrategyParam<Security> _gbpUsd;
private readonly StrategyParam<Security> _audUsd;
private readonly StrategyParam<Security> _usdJpy;
private readonly StrategyParam<Security> _eurGbp;
private readonly StrategyParam<decimal> _tradeVolume;
private readonly StrategyParam<decimal> _minTradeVolume;
private readonly StrategyParam<decimal> _maxTradeVolume;
private readonly StrategyParam<decimal> _maxAggregateVolume;
private readonly StrategyParam<decimal> _stopLossPercent;
private readonly StrategyParam<decimal> _takeProfitPercent;
private readonly StrategyParam<decimal> _breakoutBufferPercent;
private readonly StrategyParam<DataType> _dailyCandleType;
private readonly StrategyParam<DataType> _intradayCandleType;
private readonly List<InstrumentContext> _contexts = new();
private static readonly (string Alias, Func<Ch2010StructureStrategy, Security> Getter)[] _instrumentSlots =
[
("USDCHF", s => s.UsdChfSecurity),
("GBPUSD", s => s.GbpUsdSecurity),
("AUDUSD", s => s.AudUsdSecurity),
("USDJPY", s => s.UsdJpySecurity),
("EURGBP", s => s.EurGbpSecurity),
];
/// <summary>
/// Initializes a new instance of the <see cref="Ch2010StructureStrategy"/> class.
/// </summary>
public Ch2010StructureStrategy()
{
_usdChf = Param<Security>(nameof(UsdChfSecurity), null);
_usdChf.SetDisplay("USD/CHF", "USDCHF symbol to trade", "Instruments");
_gbpUsd = Param<Security>(nameof(GbpUsdSecurity), null);
_gbpUsd.SetDisplay("GBP/USD", "GBPUSD symbol to trade", "Instruments");
_audUsd = Param<Security>(nameof(AudUsdSecurity), null);
_audUsd.SetDisplay("AUD/USD", "AUDUSD symbol to trade", "Instruments");
_usdJpy = Param<Security>(nameof(UsdJpySecurity), null);
_usdJpy.SetDisplay("USD/JPY", "USDJPY symbol to trade", "Instruments");
_eurGbp = Param<Security>(nameof(EurGbpSecurity), null);
_eurGbp.SetDisplay("EUR/GBP", "EURGBP symbol to trade", "Instruments");
_tradeVolume = Param(nameof(TradeVolume), 1m);
_tradeVolume.SetGreaterThanZero();
_tradeVolume.SetDisplay("Trade Volume", "Nominal volume used for entries", "Risk");
_minTradeVolume = Param(nameof(MinTradeVolume), 0.1m);
_minTradeVolume.SetGreaterThanZero();
_minTradeVolume.SetDisplay("Minimum Volume", "Lower bound that mirrors the MQL expert", "Risk");
_maxTradeVolume = Param(nameof(MaxTradeVolume), 5m);
_maxTradeVolume.SetGreaterThanZero();
_maxTradeVolume.SetDisplay("Maximum Volume", "Upper bound for a single position", "Risk");
_maxAggregateVolume = Param(nameof(MaxAggregateVolume), 15m);
_maxAggregateVolume.SetGreaterThanZero();
_maxAggregateVolume.SetDisplay("Aggregate Volume", "Cap across all instruments", "Risk");
_stopLossPercent = Param(nameof(StopLossPercent), 1.5m);
_stopLossPercent.SetGreaterThanZero();
_stopLossPercent.SetDisplay("Stop Loss %", "Protective stop percentage", "Risk");
_takeProfitPercent = Param(nameof(TakeProfitPercent), 3m);
_takeProfitPercent.SetGreaterThanZero();
_takeProfitPercent.SetDisplay("Take Profit %", "Profit target percentage", "Risk");
_breakoutBufferPercent = Param(nameof(BreakoutBufferPercent), 10m);
_breakoutBufferPercent.SetGreaterThanZero();
_breakoutBufferPercent.SetDisplay("Buffer %", "Percentage of daily range added above/below breakout", "Logic");
_dailyCandleType = Param(nameof(DailyCandleType), TimeSpan.FromMinutes(5).TimeFrame());
_dailyCandleType.SetDisplay("Daily Candle", "Time frame used for the daily bias", "Data");
_intradayCandleType = Param(nameof(IntradayCandleType), TimeSpan.FromMinutes(30).TimeFrame());
_intradayCandleType.SetDisplay("Intraday Candle", "Time frame used for intraday execution", "Data");
}
/// <summary>
/// USDCHF security parameter.
/// </summary>
public Security UsdChfSecurity
{
get => _usdChf.Value;
set => _usdChf.Value = value;
}
/// <summary>
/// GBPUSD security parameter.
/// </summary>
public Security GbpUsdSecurity
{
get => _gbpUsd.Value;
set => _gbpUsd.Value = value;
}
/// <summary>
/// AUDUSD security parameter.
/// </summary>
public Security AudUsdSecurity
{
get => _audUsd.Value;
set => _audUsd.Value = value;
}
/// <summary>
/// USDJPY security parameter.
/// </summary>
public Security UsdJpySecurity
{
get => _usdJpy.Value;
set => _usdJpy.Value = value;
}
/// <summary>
/// EURGBP security parameter.
/// </summary>
public Security EurGbpSecurity
{
get => _eurGbp.Value;
set => _eurGbp.Value = value;
}
/// <summary>
/// Nominal trade volume.
/// </summary>
public decimal TradeVolume
{
get => _tradeVolume.Value;
set => _tradeVolume.Value = value;
}
/// <summary>
/// Minimum allowed volume.
/// </summary>
public decimal MinTradeVolume
{
get => _minTradeVolume.Value;
set => _minTradeVolume.Value = value;
}
/// <summary>
/// Maximum allowed volume for a single position.
/// </summary>
public decimal MaxTradeVolume
{
get => _maxTradeVolume.Value;
set => _maxTradeVolume.Value = value;
}
/// <summary>
/// Maximum combined exposure across all instruments.
/// </summary>
public decimal MaxAggregateVolume
{
get => _maxAggregateVolume.Value;
set => _maxAggregateVolume.Value = value;
}
/// <summary>
/// Stop-loss percentage applied to entries.
/// </summary>
public decimal StopLossPercent
{
get => _stopLossPercent.Value;
set => _stopLossPercent.Value = value;
}
/// <summary>
/// Take-profit percentage applied to entries.
/// </summary>
public decimal TakeProfitPercent
{
get => _takeProfitPercent.Value;
set => _takeProfitPercent.Value = value;
}
/// <summary>
/// Buffer in percent of the daily range used to trigger breakouts.
/// </summary>
public decimal BreakoutBufferPercent
{
get => _breakoutBufferPercent.Value;
set => _breakoutBufferPercent.Value = value;
}
/// <summary>
/// Daily candle type.
/// </summary>
public DataType DailyCandleType
{
get => _dailyCandleType.Value;
set => _dailyCandleType.Value = value;
}
/// <summary>
/// Intraday candle type.
/// </summary>
public DataType IntradayCandleType
{
get => _intradayCandleType.Value;
set => _intradayCandleType.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
foreach (var (_, security) in GetTradedSlots())
{
yield return (security, DailyCandleType);
yield return (security, IntradayCandleType);
}
}
private IEnumerable<(string alias, Security security)> GetTradedSlots()
{
var configured = false;
foreach (var slot in _instrumentSlots)
{
var security = slot.Getter(this);
if (security == null)
continue;
configured = true;
yield return (slot.Alias, security);
}
// With no pair slot filled the example still trades the security it was started on.
if (!configured && Security != null)
yield return (Security.Id, Security);
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_contexts.Clear();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_contexts.Clear();
foreach (var (alias, security) in GetTradedSlots())
{
var context = new InstrumentContext(alias, security);
_contexts.Add(context);
var dailySubscription = SubscribeCandles(DailyCandleType, true, security);
dailySubscription.Bind(candle => ProcessDailyCandle(context, candle));
dailySubscription.Start();
var intradaySubscription = SubscribeCandles(IntradayCandleType, true, security);
intradaySubscription.Bind(candle => ProcessIntradayCandle(context, candle));
intradaySubscription.Start();
}
if (_contexts.Count == 0)
{
throw new InvalidOperationException("At least one security must be configured.");
}
}
private void ProcessDailyCandle(InstrumentContext context, ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
{
return;
}
context.DailyDate = candle.OpenTime.Date;
context.DailyHigh = candle.HighPrice;
context.DailyLow = candle.LowPrice;
context.DailyClose = candle.ClosePrice;
context.HasLevels = true;
context.LongTriggered = false;
context.ShortTriggered = false;
if (candle.ClosePrice > candle.OpenPrice)
{
context.Bias = BiasDirections.Long;
}
else if (candle.ClosePrice < candle.OpenPrice)
{
context.Bias = BiasDirections.Short;
}
else
{
context.Bias = BiasDirections.Neutral;
}
LogInfo($"[{context.Alias}] Daily candle captured. High={candle.HighPrice} Low={candle.LowPrice} Close={candle.ClosePrice}");
}
private void ProcessIntradayCandle(InstrumentContext context, ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
{
return;
}
if (!context.HasLevels)
{
return;
}
if (context.DailyDate != candle.OpenTime.Date)
{
return;
}
var security = context.Security;
if (security == null)
{
return;
}
var position = GetPositionValue(security, Portfolio) ?? 0m;
UpdatePositionSnapshot(context, candle.ClosePrice, position);
if (position != 0)
{
ManageOpenPosition(context, position, candle.ClosePrice);
return;
}
var range = context.DailyHigh - context.DailyLow;
if (range <= 0)
{
return;
}
var buffer = range * (BreakoutBufferPercent / 100m);
var longTrigger = context.DailyHigh + buffer;
var shortTrigger = context.DailyLow - buffer;
if (!context.LongTriggered && context.Bias != BiasDirections.Short)
{
if (candle.ClosePrice > longTrigger)
{
TryEnterPosition(context, Sides.Buy, candle.ClosePrice, "Daily breakout long");
context.LongTriggered = true;
}
}
if (!context.ShortTriggered && context.Bias != BiasDirections.Long)
{
if (candle.ClosePrice < shortTrigger)
{
TryEnterPosition(context, Sides.Sell, candle.ClosePrice, "Daily breakout short");
context.ShortTriggered = true;
}
}
}
private void TryEnterPosition(InstrumentContext context, Sides side, decimal price, string reason)
{
if (context.ExitInProgress)
{
return;
}
var security = context.Security;
if (security == null)
{
return;
}
var volume = AdjustVolumeForLimits(TradeVolume);
if (volume <= 0)
{
return;
}
RegisterOrder(new Order
{
Security = security,
Portfolio = Portfolio,
Side = side,
Volume = volume,
Type = OrderTypes.Market,
Comment = $"{context.Alias}:{reason}"
});
context.EntrySide = side;
context.EntryPrice = price;
context.StopPrice = null;
context.TakeProfitPrice = null;
context.ExitInProgress = false;
LogInfo($"[{context.Alias}] Enter {side} at {price} vol={volume}. Reason={reason}");
}
private void ManageOpenPosition(InstrumentContext context, decimal position, decimal closePrice)
{
if (context.EntrySide == null)
{
return;
}
var isLong = position > 0;
if (context.StopPrice == null || context.TakeProfitPrice == null)
{
var entryPrice = context.EntryPrice ?? closePrice;
var stopOffset = entryPrice * (StopLossPercent / 100m);
var takeOffset = entryPrice * (TakeProfitPercent / 100m);
if (isLong)
{
context.StopPrice = entryPrice - stopOffset;
context.TakeProfitPrice = entryPrice + takeOffset;
}
else
{
context.StopPrice = entryPrice + stopOffset;
context.TakeProfitPrice = entryPrice - takeOffset;
}
}
if (context.ExitInProgress)
{
return;
}
if (isLong)
{
if (context.StopPrice != null && closePrice <= context.StopPrice.Value)
{
ExitPosition(context, position, Sides.Sell, $"StopLoss at {context.StopPrice.Value}");
return;
}
if (context.TakeProfitPrice != null && closePrice >= context.TakeProfitPrice.Value)
{
ExitPosition(context, position, Sides.Sell, $"TakeProfit at {context.TakeProfitPrice.Value}");
}
}
else
{
var volume = Math.Abs(position);
if (context.StopPrice != null && closePrice >= context.StopPrice.Value)
{
ExitPosition(context, volume, Sides.Buy, $"StopLoss at {context.StopPrice.Value}");
return;
}
if (context.TakeProfitPrice != null && closePrice <= context.TakeProfitPrice.Value)
{
ExitPosition(context, volume, Sides.Buy, $"TakeProfit at {context.TakeProfitPrice.Value}");
}
}
}
private void ExitPosition(InstrumentContext context, decimal volume, Sides side, string reason)
{
if (volume <= 0)
{
return;
}
var security = context.Security;
if (security == null)
{
return;
}
context.ExitInProgress = true;
RegisterOrder(new Order
{
Security = security,
Portfolio = Portfolio,
Side = side,
Volume = volume,
Type = OrderTypes.Market,
Comment = $"{context.Alias}:{reason}"
});
LogInfo($"[{context.Alias}] Exit {side} vol={volume}. Reason={reason}");
}
private decimal AdjustVolumeForLimits(decimal desired)
{
if (desired <= 0)
{
return 0m;
}
var volume = Math.Min(desired, MaxTradeVolume);
if (volume < MinTradeVolume)
{
return 0m;
}
var totalExposure = 0m;
foreach (var context in _contexts)
{
var security = context.Security;
if (security == null)
{
continue;
}
var pos = GetPositionValue(security, Portfolio) ?? 0m;
totalExposure += Math.Abs(pos);
}
var remaining = MaxAggregateVolume - totalExposure;
if (remaining <= 0)
{
return 0m;
}
return Math.Min(volume, remaining);
}
private void UpdatePositionSnapshot(InstrumentContext context, decimal price, decimal position)
{
if (position == context.LastKnownPosition)
{
return;
}
if (position == 0)
{
context.ResetPosition();
return;
}
context.LastKnownPosition = position;
context.EntrySide = position > 0 ? Sides.Buy : Sides.Sell;
context.EntryPrice = price;
context.ExitInProgress = false;
var stopOffset = price * (StopLossPercent / 100m);
var takeOffset = price * (TakeProfitPercent / 100m);
if (position > 0)
{
context.StopPrice = price - stopOffset;
context.TakeProfitPrice = price + takeOffset;
}
else
{
context.StopPrice = price + stopOffset;
context.TakeProfitPrice = price - takeOffset;
}
}
private enum BiasDirections
{
Neutral,
Long,
Short
}
private sealed class InstrumentContext
{
public InstrumentContext(string alias, Security security)
{
Alias = alias;
Security = security;
}
public string Alias { get; }
public Security Security { get; }
public DateTime? DailyDate { get; set; }
public decimal DailyHigh { get; set; }
public decimal DailyLow { get; set; }
public decimal DailyClose { get; set; }
public BiasDirections Bias { get; set; }
public bool HasLevels { get; set; }
public bool LongTriggered { get; set; }
public bool ShortTriggered { get; set; }
public decimal LastKnownPosition { get; set; }
public Sides? EntrySide { get; set; }
public decimal? EntryPrice { get; set; }
public decimal? StopPrice { get; set; }
public decimal? TakeProfitPrice { get; set; }
public bool ExitInProgress { get; set; }
public void Reset()
{
DailyDate = null;
DailyHigh = 0m;
DailyLow = 0m;
DailyClose = 0m;
Bias = BiasDirections.Neutral;
HasLevels = false;
LongTriggered = false;
ShortTriggered = false;
ResetPosition();
}
public void ResetPosition()
{
LastKnownPosition = 0m;
EntrySide = null;
EntryPrice = null;
StopPrice = null;
TakeProfitPrice = null;
ExitInProgress = false;
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.BusinessEntities")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates, Sides, OrderTypes
from StockSharp.Algo.Strategies import Strategy
from StockSharp.BusinessEntities import Security, Order
# Daily bias produced by the close/open relationship of the higher time frame candle.
BIAS_NEUTRAL = 0
BIAS_LONG = 1
BIAS_SHORT = 2
class InstrumentContext:
"""Per-instrument state: the daily levels with their session date and the live position."""
def __init__(self, alias, security):
self.alias = alias
self.security = security
self.reset()
def reset(self):
self.daily_date = None
self.daily_high = 0.0
self.daily_low = 0.0
self.daily_close = 0.0
self.bias = BIAS_NEUTRAL
self.has_levels = False
self.long_triggered = False
self.short_triggered = False
self.reset_position()
def reset_position(self):
self.last_known_position = 0.0
self.entry_side = None
self.entry_price = None
self.stop_price = None
self.take_profit_price = None
self.exit_in_progress = False
class ch2010_structure_strategy(Strategy):
"""
Multi-currency breakout strategy converted from the original CH2010 structure expert.
Watches daily candles to define trend bias and 30-minute candles for entries and exits.
"""
def __init__(self):
super(ch2010_structure_strategy, self).__init__()
self._usd_chf = self.Param[Security]("UsdChfSecurity", None) \
.SetDisplay("USD/CHF", "USDCHF symbol to trade", "Instruments")
self._gbp_usd = self.Param[Security]("GbpUsdSecurity", None) \
.SetDisplay("GBP/USD", "GBPUSD symbol to trade", "Instruments")
self._aud_usd = self.Param[Security]("AudUsdSecurity", None) \
.SetDisplay("AUD/USD", "AUDUSD symbol to trade", "Instruments")
self._usd_jpy = self.Param[Security]("UsdJpySecurity", None) \
.SetDisplay("USD/JPY", "USDJPY symbol to trade", "Instruments")
self._eur_gbp = self.Param[Security]("EurGbpSecurity", None) \
.SetDisplay("EUR/GBP", "EURGBP symbol to trade", "Instruments")
self._trade_volume = self.Param("TradeVolume", 1.0) \
.SetGreaterThanZero() \
.SetDisplay("Trade Volume", "Nominal volume used for entries", "Risk")
self._min_trade_volume = self.Param("MinTradeVolume", 0.1) \
.SetGreaterThanZero() \
.SetDisplay("Minimum Volume", "Lower bound that mirrors the MQL expert", "Risk")
self._max_trade_volume = self.Param("MaxTradeVolume", 5.0) \
.SetGreaterThanZero() \
.SetDisplay("Maximum Volume", "Upper bound for a single position", "Risk")
self._max_aggregate_volume = self.Param("MaxAggregateVolume", 15.0) \
.SetGreaterThanZero() \
.SetDisplay("Aggregate Volume", "Cap across all instruments", "Risk")
self._stop_loss_percent = self.Param("StopLossPercent", 1.5) \
.SetGreaterThanZero() \
.SetDisplay("Stop Loss %", "Protective stop percentage", "Risk")
self._take_profit_percent = self.Param("TakeProfitPercent", 3.0) \
.SetGreaterThanZero() \
.SetDisplay("Take Profit %", "Profit target percentage", "Risk")
self._breakout_buffer_percent = self.Param("BreakoutBufferPercent", 10.0) \
.SetGreaterThanZero() \
.SetDisplay("Buffer %", "Percentage of daily range added above/below breakout", "Logic")
self._daily_candle_type = self.Param("DailyCandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Daily Candle", "Time frame used for the daily bias", "Data")
self._intraday_candle_type = self.Param("IntradayCandleType", DataType.TimeFrame(TimeSpan.FromMinutes(30))) \
.SetDisplay("Intraday Candle", "Time frame used for intraday execution", "Data")
self._contexts = []
@property
def UsdChfSecurity(self):
return self._usd_chf.Value
@UsdChfSecurity.setter
def UsdChfSecurity(self, value):
self._usd_chf.Value = value
@property
def GbpUsdSecurity(self):
return self._gbp_usd.Value
@GbpUsdSecurity.setter
def GbpUsdSecurity(self, value):
self._gbp_usd.Value = value
@property
def AudUsdSecurity(self):
return self._aud_usd.Value
@AudUsdSecurity.setter
def AudUsdSecurity(self, value):
self._aud_usd.Value = value
@property
def UsdJpySecurity(self):
return self._usd_jpy.Value
@UsdJpySecurity.setter
def UsdJpySecurity(self, value):
self._usd_jpy.Value = value
@property
def EurGbpSecurity(self):
return self._eur_gbp.Value
@EurGbpSecurity.setter
def EurGbpSecurity(self, value):
self._eur_gbp.Value = value
@property
def TradeVolume(self):
return float(self._trade_volume.Value)
@TradeVolume.setter
def TradeVolume(self, value):
self._trade_volume.Value = value
@property
def MinTradeVolume(self):
return float(self._min_trade_volume.Value)
@MinTradeVolume.setter
def MinTradeVolume(self, value):
self._min_trade_volume.Value = value
@property
def MaxTradeVolume(self):
return float(self._max_trade_volume.Value)
@MaxTradeVolume.setter
def MaxTradeVolume(self, value):
self._max_trade_volume.Value = value
@property
def MaxAggregateVolume(self):
return float(self._max_aggregate_volume.Value)
@MaxAggregateVolume.setter
def MaxAggregateVolume(self, value):
self._max_aggregate_volume.Value = value
@property
def StopLossPercent(self):
return float(self._stop_loss_percent.Value)
@StopLossPercent.setter
def StopLossPercent(self, value):
self._stop_loss_percent.Value = value
@property
def TakeProfitPercent(self):
return float(self._take_profit_percent.Value)
@TakeProfitPercent.setter
def TakeProfitPercent(self, value):
self._take_profit_percent.Value = value
@property
def BreakoutBufferPercent(self):
return float(self._breakout_buffer_percent.Value)
@BreakoutBufferPercent.setter
def BreakoutBufferPercent(self, value):
self._breakout_buffer_percent.Value = value
@property
def DailyCandleType(self):
return self._daily_candle_type.Value
@DailyCandleType.setter
def DailyCandleType(self, value):
self._daily_candle_type.Value = value
@property
def IntradayCandleType(self):
return self._intraday_candle_type.Value
@IntradayCandleType.setter
def IntradayCandleType(self, value):
self._intraday_candle_type.Value = value
def GetWorkingSecurities(self):
"""!! REQUIRED !! Returns securities for strategy."""
result = []
for _, security in self._traded_slots():
result.append((security, self.DailyCandleType))
result.append((security, self.IntradayCandleType))
return result
def OnReseted(self):
super(ch2010_structure_strategy, self).OnReseted()
self._contexts = []
def OnStarted2(self, time):
super(ch2010_structure_strategy, self).OnStarted2(time)
self._contexts = []
for alias, security in self._traded_slots():
context = InstrumentContext(alias, security)
self._contexts.append(context)
self.SubscribeCandles(self.DailyCandleType, True, security) \
.Bind(lambda candle, c=context: self._process_daily_candle(c, candle)) \
.Start()
self.SubscribeCandles(self.IntradayCandleType, True, security) \
.Bind(lambda candle, c=context: self._process_intraday_candle(c, candle)) \
.Start()
if len(self._contexts) == 0:
raise Exception("At least one security must be configured.")
def _traded_slots(self):
"""Configured currency pairs, or the security the strategy was started on."""
slots = []
pairs = [
("USDCHF", self.UsdChfSecurity),
("GBPUSD", self.GbpUsdSecurity),
("AUDUSD", self.AudUsdSecurity),
("USDJPY", self.UsdJpySecurity),
("EURGBP", self.EurGbpSecurity),
]
for alias, security in pairs:
if security is not None:
slots.append((alias, security))
# With no pair slot filled the example still trades the security it was started on.
if len(slots) == 0 and self.Security is not None:
slots.append((self.Security.Id, self.Security))
return slots
def _process_daily_candle(self, context, candle):
if candle.State != CandleStates.Finished:
return
context.daily_date = candle.OpenTime.Date
context.daily_high = float(candle.HighPrice)
context.daily_low = float(candle.LowPrice)
context.daily_close = float(candle.ClosePrice)
context.has_levels = True
context.long_triggered = False
context.short_triggered = False
if float(candle.ClosePrice) > float(candle.OpenPrice):
context.bias = BIAS_LONG
elif float(candle.ClosePrice) < float(candle.OpenPrice):
context.bias = BIAS_SHORT
else:
context.bias = BIAS_NEUTRAL
self.LogInfo("[{0}] Daily candle captured. High={1} Low={2} Close={3}".format(
context.alias, candle.HighPrice, candle.LowPrice, candle.ClosePrice))
def _process_intraday_candle(self, context, candle):
if candle.State != CandleStates.Finished:
return
if not context.has_levels:
return
# Levels captured on an earlier session say nothing about the current one.
if context.daily_date != candle.OpenTime.Date:
return
close = float(candle.ClosePrice)
position = self._position_of(context)
self._update_position_snapshot(context, close, position)
if position != 0:
self._manage_open_position(context, position, close)
return
rng = context.daily_high - context.daily_low
if rng <= 0:
return
buffer = rng * (self.BreakoutBufferPercent / 100.0)
long_trigger = context.daily_high + buffer
short_trigger = context.daily_low - buffer
if not context.long_triggered and context.bias != BIAS_SHORT:
if close > long_trigger:
self._try_enter_position(context, Sides.Buy, close, "Daily breakout long")
context.long_triggered = True
if not context.short_triggered and context.bias != BIAS_LONG:
if close < short_trigger:
self._try_enter_position(context, Sides.Sell, close, "Daily breakout short")
context.short_triggered = True
def _try_enter_position(self, context, side, price, reason):
if context.exit_in_progress:
return
volume = self._adjust_volume_for_limits(self.TradeVolume)
if volume <= 0:
return
self._register_market_order(context, side, volume, reason)
context.entry_side = side
context.entry_price = price
context.stop_price = None
context.take_profit_price = None
context.exit_in_progress = False
self.LogInfo("[{0}] Enter {1} at {2} vol={3}. Reason={4}".format(
context.alias, side, price, volume, reason))
def _manage_open_position(self, context, position, close_price):
if context.entry_side is None:
return
is_long = position > 0
if context.stop_price is None or context.take_profit_price is None:
entry_price = context.entry_price if context.entry_price is not None else close_price
stop_offset = entry_price * (self.StopLossPercent / 100.0)
take_offset = entry_price * (self.TakeProfitPercent / 100.0)
if is_long:
context.stop_price = entry_price - stop_offset
context.take_profit_price = entry_price + take_offset
else:
context.stop_price = entry_price + stop_offset
context.take_profit_price = entry_price - take_offset
if context.exit_in_progress:
return
if is_long:
if context.stop_price is not None and close_price <= context.stop_price:
self._exit_position(context, position, Sides.Sell, "StopLoss at {0}".format(context.stop_price))
return
if context.take_profit_price is not None and close_price >= context.take_profit_price:
self._exit_position(context, position, Sides.Sell, "TakeProfit at {0}".format(context.take_profit_price))
else:
volume = abs(position)
if context.stop_price is not None and close_price >= context.stop_price:
self._exit_position(context, volume, Sides.Buy, "StopLoss at {0}".format(context.stop_price))
return
if context.take_profit_price is not None and close_price <= context.take_profit_price:
self._exit_position(context, volume, Sides.Buy, "TakeProfit at {0}".format(context.take_profit_price))
def _exit_position(self, context, volume, side, reason):
if volume <= 0:
return
context.exit_in_progress = True
self._register_market_order(context, side, volume, reason)
self.LogInfo("[{0}] Exit {1} vol={2}. Reason={3}".format(context.alias, side, volume, reason))
def _register_market_order(self, context, side, volume, reason):
order = Order()
order.Security = context.security
order.Portfolio = self.Portfolio
order.Side = side
order.Volume = volume
order.Type = OrderTypes.Market
order.Comment = "{0}:{1}".format(context.alias, reason)
self.RegisterOrder(order)
def _adjust_volume_for_limits(self, desired):
"""Clamp one entry between the volume bounds and against the aggregate exposure."""
if desired <= 0:
return 0.0
volume = min(desired, self.MaxTradeVolume)
if volume < self.MinTradeVolume:
return 0.0
total_exposure = 0.0
for context in self._contexts:
total_exposure += abs(self._position_of(context))
remaining = self.MaxAggregateVolume - total_exposure
if remaining <= 0:
return 0.0
return min(volume, remaining)
def _update_position_snapshot(self, context, price, position):
if position == context.last_known_position:
return
if position == 0:
context.reset_position()
return
context.last_known_position = position
context.entry_side = Sides.Buy if position > 0 else Sides.Sell
context.entry_price = price
context.exit_in_progress = False
stop_offset = price * (self.StopLossPercent / 100.0)
take_offset = price * (self.TakeProfitPercent / 100.0)
if position > 0:
context.stop_price = price - stop_offset
context.take_profit_price = price + take_offset
else:
context.stop_price = price + stop_offset
context.take_profit_price = price - take_offset
def _position_of(self, context):
value = self.GetPositionValue(context.security, self.Portfolio)
return float(value) if value is not None else 0.0
def CreateClone(self):
return ch2010_structure_strategy()