CH2010 Structure Multi-Timeframe Breakout Strategy
This strategy replicates the behaviour of the original ch2010structure.mq5 expert by tracking multiple forex pairs on two timeframes. Each instrument monitors the daily candle to determine a directional bias and then watches 30-minute candles to look for breakouts beyond the prior daily range. Market positions are opened when the breakout aligns with the daily trend and closed using protective stop-loss and take-profit levels.
Core Logic
Daily bias detection
- The strategy subscribes to daily candles for USDCHF, GBPUSD, AUDUSD, USDJPY and EURGBP.
- When a daily candle finishes the close/open relationship defines the bias: bullish, bearish or neutral.
- The daily high, low and close are stored together with the session date so intraday logic can confirm it is trading the same session.
Intraday breakout execution
- 30-minute candles are evaluated once they close.
- If the close is above the previous daily high plus a configurable buffer and the bias is not bearish, a long trade is triggered.
- If the close is below the previous daily low minus the buffer and the bias is not bullish, a short trade is triggered.
- Only one long and one short breakout can be activated per instrument each day to avoid over-trading.
Risk management inspired by the original helper functions
- Volumes are clamped between
MinTradeVolumeandMaxTradeVolumeand the aggregated position across all instruments is restricted byMaxAggregateVolume. - Each filled position immediately calculates absolute stop-loss and take-profit levels using percentage offsets from the entry price.
- Positions are closed via market orders as soon as the stop or target is reached; repeated exit orders are prevented by the
ExitInProgressflag.
- Volumes are clamped between
State tracking
- For every instrument the strategy tracks its own daily levels, last known position, entry side, exit orders and breakout flags in an
InstrumentContext. - This allows the multi-symbol workflow without having to maintain custom collections outside of the context class.
- For every instrument the strategy tracks its own daily levels, last known position, entry side, exit orders and breakout flags in an
Strategy Parameters
| Parameter | Description |
|---|---|
TradeVolume |
Base volume used for new entries, subject to the volume limits. |
MinTradeVolume & MaxTradeVolume |
Bounds that mirror the original MQL risk filter. |
MaxAggregateVolume |
Maximum sum of absolute positions across all traded pairs. |
StopLossPercent |
Protective stop offset in percent from the detected entry price. |
TakeProfitPercent |
Take-profit offset in percent from the detected entry price. |
BreakoutBufferPercent |
Percentage of the prior daily range added to breakout triggers. |
DailyCandleType |
DataType used to request the higher timeframe candles. |
IntradayCandleType |
DataType used to request the execution timeframe candles. |
UsdChfSecurity .. EurGbpSecurity |
Security objects for the five forex symbols monitored by default. |
Required Data
- Daily candles for every configured symbol (default: 1-day time frame).
- Intraday candles (default: 30-minute) for the same symbols.
- Real-time order routing to submit market orders for each security.
Usage Notes
- Configure the five security parameters before starting the strategy. They can be replaced with other instruments if desired.
- Set the portfolio and connector as in other StockSharp strategies.
- Optionally adjust the breakout buffer or risk parameters to reflect the target broker's contract specifications.
- Start the strategy. It will automatically subscribe to both candle streams for each instrument, log the daily structure and wait for valid intraday breakouts.
- Monitor the log for entries such as
Daily candle capturedandEnter Buyto verify the decision flow.
Differences vs. the Original MQL Expert
- Pending orders are replaced with immediate market orders once the breakout condition is observed. This keeps the logic compatible with the StockSharp high-level API while preserving the idea of limiting exposure and reacting only once per direction each day.
- Volume restrictions from the
DebugOrderSendhelper were adapted into parameters that clamp single trade sizes and total exposure. - Extensive logging is added to show daily levels, entry reasons and exit triggers in English comments for easier debugging in StockSharp.
Disclaimer
This example is intended for educational purposes. Parameters and securities should be reviewed and adjusted before using the strategy in production trading.
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()