Estratégia CH2010 Structure de Rompimento Multi-Período
Esta estratégia replica o comportamento do especialista original ch2010structure.mq5 rastreando múltiplos pares forex em dois períodos. Cada instrumento monitora a vela diária para determinar um viés direcional e depois observa velas de 30 minutos em busca de rompimentos além do intervalo diário anterior. Posições de mercado são abertas quando o rompimento se alinha com a tendência diária e fechadas usando níveis protetores de stop-loss e take-profit.
Lógica principal
Detecção de viés diário
- A estratégia subscreve às velas diárias para USDCHF, GBPUSD, AUDUSD, USDJPY e EURGBP.
- Quando uma vela diária termina, a relação fechamento/abertura define o viés: de alta, de baixa ou neutro.
- O máximo, mínimo e fechamento diário são armazenados junto com a data da sessão para que a lógica intradia possa confirmar que está operando a mesma sessão.
Execução de rompimentos intradia
- As velas de 30 minutos são avaliadas assim que fecham.
- Se o fechamento estiver acima do máximo diário anterior mais um buffer configurável e o viés não for de baixa, um trade comprado é acionado.
- Se o fechamento estiver abaixo do mínimo diário anterior menos o buffer e o viés não for de alta, um trade vendido é acionado.
- Apenas um rompimento comprado e um vendido pode ser ativado por instrumento a cada dia para evitar operar excessivamente.
Gerenciamento de risco inspirado nas funções helper originais
- Os volumes são limitados entre
MinTradeVolumeeMaxTradeVolumee a posição agregada em todos os instrumentos é restringida porMaxAggregateVolume. - Cada posição preenchida calcula imediatamente os níveis absolutos de stop-loss e take-profit usando offsets percentuais a partir do preço de entrada.
- As posições são fechadas por ordens de mercado assim que o stop ou alvo é atingido; ordens de saída repetidas são evitadas pelo flag
ExitInProgress.
- Os volumes são limitados entre
Rastreamento de estado
- Para cada instrumento, a estratégia rastreia seus próprios níveis diários, última posição conhecida, lado de entrada, ordens de saída e flags de rompimento em um
InstrumentContext. - Isso permite o fluxo de trabalho multi-símbolo sem ter que manter coleções personalizadas fora da classe de contexto.
- Para cada instrumento, a estratégia rastreia seus próprios níveis diários, última posição conhecida, lado de entrada, ordens de saída e flags de rompimento em um
Parâmetros da estratégia
| Parâmetro | Descrição |
|---|---|
TradeVolume |
Volume base usado para novas entradas, sujeito aos limites de volume. |
MinTradeVolume e MaxTradeVolume |
Limites que espelham o filtro de risco original do MQL. |
MaxAggregateVolume |
Soma máxima de posições absolutas em todos os pares operados. |
StopLossPercent |
Offset do stop de proteção em porcentagem a partir do preço de entrada detectado. |
TakeProfitPercent |
Offset do take-profit em porcentagem a partir do preço de entrada detectado. |
BreakoutBufferPercent |
Porcentagem do intervalo diário anterior adicionada aos gatilhos de rompimento. |
DailyCandleType |
DataType usado para solicitar as velas de período superior. |
IntradayCandleType |
DataType usado para solicitar as velas de período de execução. |
UsdChfSecurity .. EurGbpSecurity |
Objetos de instrumento para os cinco símbolos forex monitorados por padrão. |
Dados necessários
- Velas diárias para cada símbolo configurado (padrão: período de 1 dia).
- Velas intradia (padrão: 30 minutos) para os mesmos símbolos.
- Roteamento de ordens em tempo real para enviar ordens de mercado para cada instrumento.
Notas de uso
- Configurar os cinco parâmetros de instrumento antes de iniciar a estratégia. Podem ser substituídos por outros instrumentos se desejado.
- Definir o portfólio e conector como em outras estratégias StockSharp.
- Opcionalmente ajustar o buffer de rompimento ou parâmetros de risco para refletir as especificações de contrato do broker alvo.
- Iniciar a estratégia. Ela subscreverá automaticamente a ambos os fluxos de velas para cada instrumento, registrará a estrutura diária e aguardará rompimentos intradia válidos.
- Monitorar o log para entradas como
Daily candle capturedeEnter Buypara verificar o fluxo de decisões.
Diferenças vs. o Especialista MQL original
- Ordens pendentes são substituídas por ordens de mercado imediatas assim que a condição de rompimento é observada. Isso mantém a lógica compatível com a API de alto nível do StockSharp enquanto preserva a ideia de limitar a exposição e reagir apenas uma vez por direção a cada dia.
- As restrições de volume do helper
DebugOrderSendforam adaptadas em parâmetros que limitam os tamanhos de trades individuais e a exposição total. - Registro extensivo é adicionado para mostrar níveis diários, razões de entrada e gatilhos de saída em comentários em inglês para facilitar a depuração no StockSharp.
Isenção de responsabilidade
Este exemplo é destinado a propósitos educacionais. Parâmetros e instrumentos devem ser revisados e ajustados antes de usar a estratégia em trading de produção.
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()