Estrategia CH2010 Structure de Rompimiento Multi-Marco Temporal
Esta estrategia replica el comportamiento del experto original ch2010structure.mq5 rastreando múltiples pares forex en dos marcos temporales. Cada instrumento monitorea la vela diaria para determinar un sesgo direccional y luego observa velas de 30 minutos en busca de rompimientos más allá del rango diario anterior. Las posiciones de mercado se abren cuando el rompimiento se alinea con la tendencia diaria y se cierran usando niveles protectores de stop-loss y take-profit.
Lógica principal
Detección de sesgo diario
- La estrategia se suscribe a velas diarias para USDCHF, GBPUSD, AUDUSD, USDJPY y EURGBP.
- Cuando una vela diaria termina, la relación cierre/apertura define el sesgo: alcista, bajista o neutral.
- El máximo, mínimo y cierre diario se almacenan junto con la fecha de sesión para que la lógica intradía pueda confirmar que está operando en la misma sesión.
Ejecución de rompimientos intradía
- Las velas de 30 minutos se evalúan una vez que cierran.
- Si el cierre está por encima del máximo diario anterior más un búfer configurable y el sesgo no es bajista, se activa una operación larga.
- Si el cierre está por debajo del mínimo diario anterior menos el búfer y el sesgo no es alcista, se activa una operación corta.
- Solo se puede activar un rompimiento largo y uno corto por instrumento cada día para evitar operar en exceso.
Gestión de riesgo inspirada en las funciones helper originales
- Los volúmenes se limitan entre
MinTradeVolumeyMaxTradeVolumey la posición agregada en todos los instrumentos está restringida porMaxAggregateVolume. - Cada posición completada calcula inmediatamente los niveles absolutos de stop-loss y take-profit usando offsets porcentuales desde el precio de entrada.
- Las posiciones se cierran mediante órdenes de mercado tan pronto como se alcanza el stop o el objetivo; las órdenes de salida repetidas se evitan con el flag
ExitInProgress.
- Los volúmenes se limitan entre
Seguimiento de estado
- Para cada instrumento, la estrategia rastrea sus propios niveles diarios, última posición conocida, lado de entrada, órdenes de salida y flags de rompimiento en un
InstrumentContext. - Esto permite el flujo de trabajo multi-símbolo sin tener que mantener colecciones personalizadas fuera de la clase de contexto.
- Para cada instrumento, la estrategia rastrea sus propios niveles diarios, última posición conocida, lado de entrada, órdenes de salida y flags de rompimiento en un
Parámetros de la estrategia
| Parámetro | Descripción |
|---|---|
TradeVolume |
Volumen base usado para nuevas entradas, sujeto a los límites de volumen. |
MinTradeVolume y MaxTradeVolume |
Límites que reflejan el filtro de riesgo original de MQL. |
MaxAggregateVolume |
Suma máxima de posiciones absolutas en todos los pares operados. |
StopLossPercent |
Offset del stop de protección en porcentaje desde el precio de entrada detectado. |
TakeProfitPercent |
Offset del take-profit en porcentaje desde el precio de entrada detectado. |
BreakoutBufferPercent |
Porcentaje del rango diario anterior añadido a los disparadores de rompimiento. |
DailyCandleType |
DataType usado para solicitar las velas de marco temporal superior. |
IntradayCandleType |
DataType usado para solicitar las velas de marco temporal de ejecución. |
UsdChfSecurity .. EurGbpSecurity |
Objetos de instrumento para los cinco símbolos forex monitoreados por defecto. |
Datos requeridos
- Velas diarias para cada símbolo configurado (por defecto: marco temporal de 1 día).
- Velas intradía (por defecto: 30 minutos) para los mismos símbolos.
- Enrutamiento de órdenes en tiempo real para enviar órdenes de mercado para cada instrumento.
Notas de uso
- Configurar los cinco parámetros de instrumento antes de iniciar la estrategia. Pueden reemplazarse con otros instrumentos si se desea.
- Establecer el portafolio y conector como en otras estrategias de StockSharp.
- Opcionalmente ajustar el búfer de rompimiento o los parámetros de riesgo para reflejar las especificaciones de contrato del broker objetivo.
- Iniciar la estrategia. Se suscribirá automáticamente a ambos flujos de velas para cada instrumento, registrará la estructura diaria y esperará rompimientos intradía válidos.
- Monitorear el log para entradas como
Daily candle capturedyEnter Buypara verificar el flujo de decisiones.
Diferencias vs. el Experto MQL original
- Las órdenes pendientes se reemplazan con órdenes de mercado inmediatas una vez que se observa la condición de rompimiento. Esto mantiene la lógica compatible con la API de alto nivel de StockSharp mientras preserva la idea de limitar la exposición y reaccionar solo una vez por dirección cada día.
- Las restricciones de volumen del helper
DebugOrderSendse adaptaron en parámetros que limitan los tamaños de operaciones individuales y la exposición total. - Se agrega registro extenso para mostrar niveles diarios, razones de entrada y disparadores de salida en comentarios en inglés para facilitar la depuración en StockSharp.
Descargo de responsabilidad
Este ejemplo está destinado a propósitos educativos. Los parámetros e instrumentos deben revisarse y ajustarse antes de usar la estrategia en trading de producción.
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()