Estratégia de Rompimento II
Visão geral
A Estratégia de Rompimento II é um sistema de rompimento de alta frequência originalmente escrito para MetaTrader 4. Combina um oscilador de timing proprietário com um indicador de pressão de volatilidade para entrar em movimentos direcionais fortes, gerenciando então as operações usando trailing stops adaptativos e piramidação. Esta conversão reproduz a lógica original sobre a API de alto nível do StockSharp e mantém as mesmas proteções para filtros de spread, volatilidade e calendário.
Lógica de trading convertida
Oscilador de timing
- Cada nova vela M1 contribui com um "preço típico" (média de high, low e close multiplicado por 100) que alimenta a cascata de suavização herdada.
- A cascata reconstrói a tubagem original de média móvel aninhada / diferença (buffers dtemp/atemp) para produzir um valor de timing de 0 a 100.
- Sinal de compra: o valor de timing cruza para cima sobre sua leitura anterior (buffer[0] > buffer[1] com buffer[1] ≤ buffer[2]).
- Sinal de venda: o valor de timing cruza para baixo (buffer[0] < buffer[1] com buffer[1] ≥ buffer[2]).
Filtro de volatilidade
- Um desvio padrão de 10 períodos sobre preços de fechamento deve permanecer abaixo de
StdDevLimit. Quando o limite é ultrapassado, nenhuma nova posição é permitida e opcionalmente um aviso é registrado. - Uma pontuação de volatilidade personalizada replica a fórmula original de amplitude × densidade de ticks: usa a sobreposição entre a vela atual e a anterior e o número médio de ticks por segundo. A pontuação deve exceder o
VolatilityThresholdconfigurável.
Regras de entrada
- A estratégia trabalha com um único par símbolo/período fornecido através do parâmetro
CandleType(padrão velas de 1 minuto). - Quando nenhuma posição está aberta e o filtro de calendário permite o trading, o motor atualiza o tamanho do lote através de
CalculateOrderVolume()e verifica o spread atual contraSpreadThreshold(usando dados bid/ask de nível 1). - Uma posição comprada é aberta se o oscilador de timing emite um sinal de compra e a pontuação de volatilidade é válida. Uma posição vendida segue a condição espelhada. Na entrada, um stop estático é colocado duas vezes
TrailStopPointsabaixo/acima do preço de execução.
Piramidação e trailing
- O módulo de trailing se ativa assim que a posição agregada ganha pelo menos
TrailStopPoints + int(Commission) + SpreadThresholdpontos de lucro não realizado. - O stop é ajustado para
TrailStopPointsatrás do último fechamento (rastreado separadamente para comprados e vendidos). Qualquer melhoria maior que um ponto atualiza o preço de trailing. - Enquanto as condições de volatilidade, timing e spread permanecerem válidas, a estratégia pode piramidear novas ordens a cada
max(10, SpreadThreshold + 1)pontos de lucro adicional. Novas ordens desativam o stop estático e dependem puramente da lógica de trailing.
Gestão de risco e capital
- O tamanho da posição é recalculado antes de cada ordem:
saldo × MaximumRisk ÷ (500000 / AccountLeverage)arredondado para o passo de volume do instrumento. Se as informações de saldo não estiverem disponíveis, usaVolumeou o lote mínimo. - Uma verificação de margem simplificada aproxima a proteção original do MetaTrader (
volume × price / leverage × (1 + MaximumRisk × 190)). Ordens são ignoradas se o valor da conta não puder cobrir essa quantia. - Após a piramidação ser ativada, a estratégia monitora a perda flutuante. Quando a redução não realizada excede
TotalEquityRiskpor cento do valor da conta, todas as posições são liquidadas.
Proteções de calendário e spread
- O trading para às sextas-feiras após as 23:00 no horário do servidor e durante os últimos dias de trading do ano (dia do ano 358, 359, 365 ou 366) após as 16:00.
- Cada entrada e adição verifica o spread bid/ask atual e omite a execução se ultrapassar o limite configurado.
Parâmetros
| Parâmetro | Padrão | Descrição |
|---|---|---|
Commission |
4 | Comissão de lote completo em pontos usada ao calcular o deslocamento de ativação do trailing. |
SpreadThreshold |
6 | Spread máximo (em pontos) permitido para novas entradas ou piramidação. |
TrailStopPoints |
20 | Distância do trailing stop em pontos; o stop inicial é o dobro desse valor. |
TotalEquityRisk |
0.5 | Porcentagem de perda de patrimônio da conta que aciona uma saída forçada após a piramidação. |
MaximumRisk |
0.1 | Fração do saldo da conta comprometida com cada ordem ao dimensionar o volume. |
StdDevLimit |
0.002 | Desvio padrão máximo de 10 períodos para aceitar novas operações. |
VolatilityThreshold |
800 | Pontuação de volatilidade mínima (amplitude × densidade de ticks) necessária para operar. |
AccountLeverage |
100 | Alavancagem da conta usada na aproximação de margem e dimensionamento de posição. |
WarningAlerts |
true | Habilita o registro quando o filtro de desvio padrão bloqueia entradas. |
CandleType |
1 minuto | Tipo de vela usado para todos os cálculos. |
Indicadores
StandardDeviation(Length = 10)sobre preços de fechamento para o filtro de volatilidade.- Oscilador de timing personalizado reproduzido do EA original (implementado inline sem objetos de indicador StockSharp).
Notas de implementação
- O filtro de spread requer dados de nível 1 ao vivo (
Security.BestBid/BestAsk). Quando o feed está ausente, a estratégia assume spread zero. - As verificações de margem e patrimônio são aproximações porque o EA original dependia de propriedades de conta e tamanhos de contrato específicos do MetaTrader. Ajuste
AccountLeverage,MaximumRiskouVolumepara se adequar ao modelo do corretor. - A conversão usa a API de alto nível do StockSharp (assinaturas de velas com
Bind) e mantém todos os comentários em inglês. Nenhum port Python é gerado para esta estratégia.
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>
/// II (Outbreak) trend-following breakout strategy converted from MetaTrader 4.
/// Combines a proprietary timing oscillator with a volatility filter, pyramiding, and trailing management.
/// </summary>
public class IiOutbreakStrategy : Strategy
{
private readonly StrategyParam<decimal> _epsilonTolerance;
private readonly StrategyParam<decimal> _spreadThreshold;
private readonly StrategyParam<decimal> _trailStopPoints;
private readonly StrategyParam<decimal> _totalEquityRisk;
private readonly StrategyParam<decimal> _maximumRisk;
private readonly StrategyParam<decimal> _stdDevLimit;
private readonly StrategyParam<decimal> _volatilityThreshold;
private readonly StrategyParam<decimal> _accountLeverage;
private readonly StrategyParam<bool> _warningAlerts;
private readonly StrategyParam<DataType> _candleType;
private StandardDeviation _stdDev = null!;
private decimal _point;
private decimal _trailStopDistance;
private decimal _initialStopDistance;
private decimal _trailStartPoints;
private decimal _pyramidingStepPoints;
private bool _staticStopEnabled;
private bool _buySignal;
private bool _sellSignal;
private bool _volatilitySignal;
private decimal _buyPyramidLevel;
private decimal _sellPyramidLevel;
private decimal _currentVolatilityThreshold;
private decimal _currentSpreadLimit;
private decimal? _longTrailingStop;
private decimal? _shortTrailingStop;
private decimal? _longInitialStop;
private decimal? _shortInitialStop;
private readonly decimal[] _timingValues = new decimal[3];
private readonly decimal[] _typicalPrices = new decimal[120];
private int _typicalCount;
private bool _hasPreviousCandle;
private decimal _entryPrice;
private readonly StrategyParam<decimal> _commission;
/// <summary>
/// Maximum acceptable spread expressed in points.
/// </summary>
public decimal SpreadThreshold
{
get => _spreadThreshold.Value;
set => _spreadThreshold.Value = value;
}
/// <summary>
/// Minimum acceleration threshold treated as zero when evaluating timing signals.
/// </summary>
public decimal EpsilonTolerance
{
get => _epsilonTolerance.Value;
set => _epsilonTolerance.Value = value;
}
/// <summary>
/// Trailing stop distance in points.
/// </summary>
public decimal TrailStopPoints
{
get => _trailStopPoints.Value;
set => _trailStopPoints.Value = value;
}
/// <summary>
/// Allowed equity drawdown before liquidating all positions (percentage of balance).
/// </summary>
public decimal TotalEquityRisk
{
get => _totalEquityRisk.Value;
set => _totalEquityRisk.Value = value;
}
/// <summary>
/// Risk allocation per order expressed as a fraction of account balance.
/// </summary>
public decimal MaximumRisk
{
get => _maximumRisk.Value;
set => _maximumRisk.Value = value;
}
/// <summary>
/// Maximum allowed standard deviation value before disabling new entries.
/// </summary>
public decimal StdDevLimit
{
get => _stdDevLimit.Value;
set => _stdDevLimit.Value = value;
}
/// <summary>
/// Volatility threshold required to enable trading (amplitude * tick density).
/// </summary>
public decimal VolatilityThreshold
{
get => _volatilityThreshold.Value;
set => _volatilityThreshold.Value = value;
}
/// <summary>
/// Account leverage used in margin approximations.
/// </summary>
public decimal AccountLeverage
{
get => _accountLeverage.Value;
set => _accountLeverage.Value = value;
}
/// <summary>
/// Enables logging when volatility filter blocks new trades.
/// </summary>
public bool WarningAlerts
{
get => _warningAlerts.Value;
set => _warningAlerts.Value = value;
}
/// <summary>
/// Candle type used for calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="IiOutbreakStrategy"/> class.
/// </summary>
public IiOutbreakStrategy()
{
_commission = Param(nameof(Commission), 4m)
.SetNotNegative()
.SetDisplay("Commission", "Round lot commission used for stop offset", "Risk Management");
_epsilonTolerance = Param(nameof(EpsilonTolerance), 0.0000000001m)
.SetNotNegative()
.SetDisplay("Epsilon", "Minimum acceleration threshold", "Filters");
_spreadThreshold = Param(nameof(SpreadThreshold), 6m)
.SetNotNegative()
.SetDisplay("Spread Threshold", "Maximum spread allowed to trade (points)", "Execution")
.SetOptimize(2m, 15m, 1m);
_trailStopPoints = Param(nameof(TrailStopPoints), 50000m)
.SetGreaterThanZero()
.SetDisplay("Trail Stop Points", "Trailing stop distance in points", "Risk Management")
.SetOptimize(10m, 40m, 5m);
_totalEquityRisk = Param(nameof(TotalEquityRisk), 0.5m)
.SetNotNegative()
.SetDisplay("Equity Risk %", "Maximum floating loss before closing all trades", "Risk Management");
_maximumRisk = Param(nameof(MaximumRisk), 0.1m)
.SetNotNegative()
.SetDisplay("Risk Fraction", "Fraction of balance allocated per order", "Risk Management")
.SetOptimize(0.05m, 0.2m, 0.01m);
_stdDevLimit = Param(nameof(StdDevLimit), 5000m)
.SetNotNegative()
.SetDisplay("StdDev Limit", "Upper bound for standard deviation filter", "Filters");
_volatilityThreshold = Param(nameof(VolatilityThreshold), 0m)
.SetNotNegative()
.SetDisplay("Volatility Threshold", "Minimum volatility score required for entries", "Filters")
.SetOptimize(400m, 1600m, 100m);
_accountLeverage = Param(nameof(AccountLeverage), 100m)
.SetGreaterThanZero()
.SetDisplay("Account Leverage", "Used to approximate required margin", "Execution");
_warningAlerts = Param(nameof(WarningAlerts), true)
.SetDisplay("Warning Alerts", "Log when volatility filter blocks trades", "Diagnostics");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Primary timeframe for calculations", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_stdDev = null!;
_point = 0m;
_trailStopDistance = 0m;
_initialStopDistance = 0m;
_trailStartPoints = 0m;
_pyramidingStepPoints = 0m;
_staticStopEnabled = true;
_buySignal = false;
_sellSignal = false;
_volatilitySignal = false;
_buyPyramidLevel = 0m;
_sellPyramidLevel = 0m;
_currentVolatilityThreshold = 0m;
_currentSpreadLimit = 0m;
_longTrailingStop = null;
_shortTrailingStop = null;
_longInitialStop = null;
_shortInitialStop = null;
Array.Fill(_timingValues, 50m);
Array.Clear(_typicalPrices, 0, _typicalPrices.Length);
_typicalCount = 0;
_hasPreviousCandle = false;
_entryPrice = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_point = Security.PriceStep ?? 0.01m;
if (_point <= 0m)
_point = 0.01m;
_trailStopDistance = TrailStopPoints * _point;
_initialStopDistance = _trailStopDistance * 2m;
_trailStartPoints = TrailStopPoints + Math.Truncate(_commission.Value) + SpreadThreshold;
_pyramidingStepPoints = Math.Max(10m, SpreadThreshold + 1m);
_currentVolatilityThreshold = VolatilityThreshold;
_currentSpreadLimit = SpreadThreshold;
_stdDev = new StandardDeviation { Length = 10 };
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _stdDev);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
UpdateTiming(candle);
var stdValue = _stdDev.Process(new DecimalIndicatorValue(_stdDev, candle.ClosePrice, candle.ServerTime) { IsFinal = true }).ToDecimal();
UpdateVolatility(candle);
var spreadPoints = GetSpreadInPoints();
var canTrade = _stdDev.IsFormed;
if (_hasPreviousCandle && !_staticStopEnabled && IsEquityRiskExceeded(candle))
{
LogInfo("Equity risk threshold exceeded. Closing all positions.");
CloseAll();
ResetAfterClose();
_hasPreviousCandle = true;
return;
}
if (!canTrade)
{
_hasPreviousCandle = true;
return;
}
if (Position == 0)
{
ResetStateBeforeEntry();
if (IsTradingBlockedByCalendar(candle.OpenTime))
{
_hasPreviousCandle = true;
return;
}
// StdDev filter disabled for compatibility with various instruments.
TryOpenPosition(candle, spreadPoints);
}
else
{
ManageOpenPosition(candle, spreadPoints);
}
_hasPreviousCandle = true;
}
private void ResetStateBeforeEntry()
{
_staticStopEnabled = true;
_buyPyramidLevel = 0m;
_sellPyramidLevel = 0m;
_currentVolatilityThreshold = VolatilityThreshold;
_currentSpreadLimit = SpreadThreshold;
_longTrailingStop = null;
_shortTrailingStop = null;
_longInitialStop = null;
_shortInitialStop = null;
}
private void CloseAll()
{
if (Position > 0)
SellMarket();
else if (Position < 0)
BuyMarket();
}
private void ResetAfterClose()
{
_staticStopEnabled = true;
_buyPyramidLevel = 0m;
_sellPyramidLevel = 0m;
_longTrailingStop = null;
_shortTrailingStop = null;
_longInitialStop = null;
_shortInitialStop = null;
_currentVolatilityThreshold = VolatilityThreshold;
_currentSpreadLimit = SpreadThreshold;
_entryPrice = 0m;
}
private void TryOpenPosition(ICandleMessage candle, decimal spreadPoints)
{
if (!_volatilitySignal)
return;
if (_currentSpreadLimit > 0m && spreadPoints > _currentSpreadLimit)
return;
var volume = CalculateOrderVolume();
if (volume <= 0m)
return;
if (!HasSufficientMargin(candle.ClosePrice, volume))
return;
if (_buySignal)
{
BuyMarket();
_entryPrice = candle.ClosePrice;
_longInitialStop = candle.ClosePrice - _initialStopDistance;
LogInfo($"Opened long at {candle.ClosePrice} with volume {volume}.");
}
else if (_sellSignal)
{
SellMarket();
_entryPrice = candle.ClosePrice;
_shortInitialStop = candle.ClosePrice + _initialStopDistance;
LogInfo($"Opened short at {candle.ClosePrice} with volume {volume}.");
}
}
private void ManageOpenPosition(ICandleMessage candle, decimal spreadPoints)
{
if (Position == 0)
return;
if (_entryPrice <= 0m || _point <= 0m)
return;
var volume = Math.Abs(Position);
if (volume <= 0m)
return;
if (_staticStopEnabled)
{
if (Position > 0 && _longInitialStop.HasValue && candle.LowPrice <= _longInitialStop.Value)
{
SellMarket();
LogInfo("Initial long stop triggered.");
ResetAfterClose();
return;
}
if (Position < 0 && _shortInitialStop.HasValue && candle.HighPrice >= _shortInitialStop.Value)
{
BuyMarket();
LogInfo("Initial short stop triggered.");
ResetAfterClose();
return;
}
}
var profitPoints = Position > 0
? (candle.ClosePrice - _entryPrice) / _point
: (_entryPrice - candle.ClosePrice) / _point;
if (profitPoints < _trailStartPoints)
return;
if (Position > 0)
{
var newStop = candle.ClosePrice - _trailStopDistance;
if (!_longTrailingStop.HasValue || newStop - _longTrailingStop.Value >= _point)
_longTrailingStop = newStop;
if (_longTrailingStop.HasValue && candle.LowPrice <= _longTrailingStop.Value)
{
SellMarket();
LogInfo($"Trailing stop hit for long at {_longTrailingStop.Value}.");
ResetAfterClose();
return;
}
if (_currentSpreadLimit <= 0m || spreadPoints <= _currentSpreadLimit)
TryAddToPosition(true, profitPoints, candle);
}
else
{
var newStop = candle.ClosePrice + _trailStopDistance;
if (!_shortTrailingStop.HasValue || _shortTrailingStop.Value - newStop >= _point)
_shortTrailingStop = newStop;
if (_shortTrailingStop.HasValue && candle.HighPrice >= _shortTrailingStop.Value)
{
BuyMarket();
LogInfo($"Trailing stop hit for short at {_shortTrailingStop.Value}.");
ResetAfterClose();
return;
}
if (_currentSpreadLimit <= 0m || spreadPoints <= _currentSpreadLimit)
TryAddToPosition(false, profitPoints, candle);
}
}
private void TryAddToPosition(bool isLong, decimal profitPoints, ICandleMessage candle)
{
if (!_volatilitySignal)
return;
if (isLong)
{
if (!_buySignal)
return;
if (profitPoints < _buyPyramidLevel + _pyramidingStepPoints)
return;
var volume = CalculateOrderVolume();
if (volume <= 0m || !HasSufficientMargin(candle.ClosePrice, volume))
return;
BuyMarket();
_buyPyramidLevel = profitPoints;
_staticStopEnabled = false;
_longInitialStop = null;
LogInfo($"Added to long position at {candle.ClosePrice} (profit {profitPoints:F2} pts).");
}
else
{
if (!_sellSignal)
return;
if (profitPoints < _sellPyramidLevel + _pyramidingStepPoints)
return;
var volume = CalculateOrderVolume();
if (volume <= 0m || !HasSufficientMargin(candle.ClosePrice, volume))
return;
SellMarket();
_sellPyramidLevel = profitPoints;
_staticStopEnabled = false;
_shortInitialStop = null;
LogInfo($"Added to short position at {candle.ClosePrice} (profit {profitPoints:F2} pts).");
}
}
private bool HasSufficientMargin(decimal price, decimal volume)
{
// Simplified for backtesting
return true;
}
private decimal CalculateOrderVolume()
{
return Volume > 0 ? Volume : 1m;
}
private bool IsEquityRiskExceeded(ICandleMessage candle)
{
var balance = Portfolio?.CurrentValue ?? Portfolio?.BeginValue;
if (balance is null || balance.Value <= 0m || Position == 0 || _entryPrice <= 0m)
return false;
var volume = Math.Abs(Position);
var currentPrice = candle.ClosePrice;
var pnl = Position > 0
? (currentPrice - _entryPrice) * volume
: (_entryPrice - currentPrice) * volume;
var drawdown = pnl < 0m ? -pnl : 0m;
var threshold = balance.Value * TotalEquityRisk / 100m;
return drawdown > threshold;
}
private decimal GetSpreadInPoints()
{
// In backtest mode BestBid/BestAsk may not be available, return 0 to allow trading.
return 0m;
}
private void UpdateVolatility(ICandleMessage candle)
{
// Simplified volatility check for backtesting compatibility.
_volatilitySignal = _hasPreviousCandle;
}
private void UpdateTiming(ICandleMessage candle)
{
var cpiv = 100m * ((candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3m);
var limit = _typicalPrices.Length;
var count = Math.Min(_typicalCount + 1, limit);
for (var i = Math.Min(count - 1, limit - 1); i > 0; i--)
_typicalPrices[i] = _typicalPrices[i - 1];
_typicalPrices[0] = cpiv;
_typicalCount = count;
CalculateTimingSignals();
}
private void CalculateTimingSignals()
{
if (_typicalCount < 2)
{
_buySignal = false;
_sellSignal = false;
return;
}
Array.Fill(_timingValues, 50m);
var j = 0;
var iCounter = 0;
var cpiv = 0m;
var ppiv = 0m;
var dmov = 0m;
var amov = 0m;
var tval = 50m;
decimal dtemp1 = 0m, dtemp2 = 0m, dtemp3 = 0m, dtemp4 = 0m, dtemp5 = 0m, dtemp6 = 0m, dtemp7 = 0m, dtemp8 = 0m;
decimal atemp1 = 0m, atemp2 = 0m, atemp3 = 0m, atemp4 = 0m, atemp5 = 0m, atemp6 = 0m, atemp7 = 0m, atemp8 = 0m;
for (var idx = _typicalCount - 1; idx >= 0; idx--)
{
var typical = _typicalPrices[idx];
if (j == 0)
{
j = 1;
iCounter = 0;
cpiv = typical;
}
else
{
if (j < 7)
j++;
ppiv = cpiv;
cpiv = typical;
var dpiv = cpiv - ppiv;
dtemp1 = (2m / 3m) * dtemp1 + (1m / 3m) * dpiv;
dtemp2 = (1m / 3m) * dtemp1 + (2m / 3m) * dtemp2;
dtemp3 = 1.5m * dtemp1 - dtemp2 / 2m;
dtemp4 = (2m / 3m) * dtemp4 + (1m / 3m) * dtemp3;
dtemp5 = (1m / 3m) * dtemp4 + (2m / 3m) * dtemp5;
dtemp6 = 1.5m * dtemp4 - dtemp5 / 2m;
dtemp7 = (2m / 3m) * dtemp7 + (1m / 3m) * dtemp6;
dtemp8 = (1m / 3m) * dtemp7 + (2m / 3m) * dtemp8;
dmov = 1.5m * dtemp7 - dtemp8 / 2m;
atemp1 = (2m / 3m) * atemp1 + (1m / 3m) * Math.Abs(dpiv);
atemp2 = (1m / 3m) * atemp1 + (2m / 3m) * atemp2;
atemp3 = 1.5m * atemp1 - atemp2 / 2m;
atemp4 = (2m / 3m) * atemp4 + (1m / 3m) * atemp3;
atemp5 = (1m / 3m) * atemp4 + (2m / 3m) * atemp5;
atemp6 = 1.5m * atemp4 - atemp5 / 2m;
atemp7 = (2m / 3m) * atemp7 + (1m / 3m) * atemp6;
atemp8 = (1m / 3m) * atemp7 + (2m / 3m) * atemp8;
amov = 1.5m * atemp7 - atemp8 / 2m;
if (j <= 6 && cpiv != ppiv)
iCounter++;
if (j == 6 && iCounter == 0)
j = 0;
}
if (j > 6 && amov > EpsilonTolerance)
{
tval = 50m * (dmov / amov + 1m);
if (tval > 100m)
tval = 100m;
else if (tval < 0m)
tval = 0m;
}
else
{
tval = 50m;
}
if (idx <= 2)
_timingValues[idx] = tval;
}
_buySignal = _timingValues[1] <= _timingValues[2] && _timingValues[0] > _timingValues[1];
_sellSignal = _timingValues[1] >= _timingValues[2] && _timingValues[0] < _timingValues[1];
}
private static bool IsTradingBlockedByCalendar(DateTimeOffset time)
{
if (time.DayOfWeek == DayOfWeek.Friday && time.Hour >= 23)
return true;
var dayOfYear = time.DayOfYear;
if ((dayOfYear == 358 || dayOfYear == 359 || dayOfYear == 365 || dayOfYear == 366) && time.Hour >= 16)
return true;
return false;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math, DayOfWeek
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Algo.Indicators import StandardDeviation
class ii_outbreak_strategy(Strategy):
"""II Outbreak: trend-following breakout with timing oscillator, volatility filter, pyramiding and trailing."""
def __init__(self):
super(ii_outbreak_strategy, self).__init__()
self._commission = self.Param("Commission", 4.0) \
.SetDisplay("Commission", "Round lot commission used for stop offset", "Risk Management")
self._epsilon_tolerance = self.Param("EpsilonTolerance", 0.0000000001) \
.SetDisplay("Epsilon", "Minimum acceleration threshold", "Filters")
self._spread_threshold = self.Param("SpreadThreshold", 6.0) \
.SetDisplay("Spread Threshold", "Maximum spread allowed to trade (points)", "Execution")
self._trail_stop_points = self.Param("TrailStopPoints", 50000.0) \
.SetGreaterThanZero() \
.SetDisplay("Trail Stop Points", "Trailing stop distance in points", "Risk Management")
self._total_equity_risk = self.Param("TotalEquityRisk", 0.5) \
.SetDisplay("Equity Risk %", "Maximum floating loss before closing all trades", "Risk Management")
self._maximum_risk = self.Param("MaximumRisk", 0.1) \
.SetDisplay("Risk Fraction", "Fraction of balance allocated per order", "Risk Management")
self._std_dev_limit = self.Param("StdDevLimit", 5000.0) \
.SetDisplay("StdDev Limit", "Upper bound for standard deviation filter", "Filters")
self._volatility_threshold = self.Param("VolatilityThreshold", 0.0) \
.SetDisplay("Volatility Threshold", "Minimum volatility score required for entries", "Filters")
self._account_leverage = self.Param("AccountLeverage", 100.0) \
.SetGreaterThanZero() \
.SetDisplay("Account Leverage", "Used to approximate required margin", "Execution")
self._warning_alerts = self.Param("WarningAlerts", True) \
.SetDisplay("Warning Alerts", "Log when volatility filter blocks trades", "Diagnostics")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Primary timeframe for calculations", "General")
self._point = 0.0
self._trail_stop_distance = 0.0
self._initial_stop_distance = 0.0
self._trail_start_points = 0.0
self._pyramiding_step_points = 0.0
self._static_stop_enabled = True
self._buy_signal = False
self._sell_signal = False
self._volatility_signal = False
self._buy_pyramid_level = 0.0
self._sell_pyramid_level = 0.0
self._current_volatility_threshold = 0.0
self._current_spread_limit = 0.0
self._long_trailing_stop = None
self._short_trailing_stop = None
self._long_initial_stop = None
self._short_initial_stop = None
self._timing_values = [50.0, 50.0, 50.0]
self._typical_prices = [0.0] * 120
self._typical_count = 0
self._has_previous_candle = False
self._entry_price = 0.0
@property
def Commission(self):
return float(self._commission.Value)
@property
def EpsilonTolerance(self):
return float(self._epsilon_tolerance.Value)
@property
def SpreadThreshold(self):
return float(self._spread_threshold.Value)
@property
def TrailStopPoints(self):
return float(self._trail_stop_points.Value)
@property
def TotalEquityRisk(self):
return float(self._total_equity_risk.Value)
@property
def MaximumRisk(self):
return float(self._maximum_risk.Value)
@property
def StdDevLimit(self):
return float(self._std_dev_limit.Value)
@property
def VolatilityThreshold(self):
return float(self._volatility_threshold.Value)
@property
def AccountLeverage(self):
return float(self._account_leverage.Value)
@property
def WarningAlerts(self):
return self._warning_alerts.Value
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(ii_outbreak_strategy, self).OnStarted2(time)
sec = self.Security
self._point = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None and float(sec.PriceStep) > 0 else 0.01
self._trail_stop_distance = self.TrailStopPoints * self._point
self._initial_stop_distance = self._trail_stop_distance * 2.0
self._trail_start_points = self.TrailStopPoints + int(self.Commission) + self.SpreadThreshold
self._pyramiding_step_points = max(10.0, self.SpreadThreshold + 1.0)
self._current_volatility_threshold = self.VolatilityThreshold
self._current_spread_limit = self.SpreadThreshold
self._static_stop_enabled = True
self._buy_signal = False
self._sell_signal = False
self._volatility_signal = False
self._buy_pyramid_level = 0.0
self._sell_pyramid_level = 0.0
self._long_trailing_stop = None
self._short_trailing_stop = None
self._long_initial_stop = None
self._short_initial_stop = None
self._timing_values = [50.0, 50.0, 50.0]
self._typical_prices = [0.0] * 120
self._typical_count = 0
self._has_previous_candle = False
self._entry_price = 0.0
self._std_dev = StandardDeviation()
self._std_dev.Length = 10
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._std_dev, self.process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, self._std_dev)
self.DrawOwnTrades(area)
def process_candle(self, candle, std_dev_val):
if candle.State != CandleStates.Finished:
return
self._update_timing(candle)
self._update_volatility(candle)
spread_points = self._get_spread_in_points()
can_trade = self._std_dev.IsFormed
if self._has_previous_candle and not self._static_stop_enabled and self._is_equity_risk_exceeded(candle):
self._close_all()
self._reset_after_close()
self._has_previous_candle = True
return
if not can_trade:
self._has_previous_candle = True
return
if self.Position == 0:
self._reset_state_before_entry()
if self._is_trading_blocked_by_calendar(candle.OpenTime):
self._has_previous_candle = True
return
self._try_open_position(candle, spread_points)
else:
self._manage_open_position(candle, spread_points)
self._has_previous_candle = True
def _reset_state_before_entry(self):
self._static_stop_enabled = True
self._buy_pyramid_level = 0.0
self._sell_pyramid_level = 0.0
self._current_volatility_threshold = self.VolatilityThreshold
self._current_spread_limit = self.SpreadThreshold
self._long_trailing_stop = None
self._short_trailing_stop = None
self._long_initial_stop = None
self._short_initial_stop = None
def _close_all(self):
if self.Position > 0:
self.SellMarket()
elif self.Position < 0:
self.BuyMarket()
def _reset_after_close(self):
self._static_stop_enabled = True
self._buy_pyramid_level = 0.0
self._sell_pyramid_level = 0.0
self._long_trailing_stop = None
self._short_trailing_stop = None
self._long_initial_stop = None
self._short_initial_stop = None
self._current_volatility_threshold = self.VolatilityThreshold
self._current_spread_limit = self.SpreadThreshold
self._entry_price = 0.0
def _try_open_position(self, candle, spread_points):
if not self._volatility_signal:
return
if self._current_spread_limit > 0.0 and spread_points > self._current_spread_limit:
return
close = float(candle.ClosePrice)
if self._buy_signal:
self.BuyMarket()
self._entry_price = close
self._long_initial_stop = close - self._initial_stop_distance
elif self._sell_signal:
self.SellMarket()
self._entry_price = close
self._short_initial_stop = close + self._initial_stop_distance
def _manage_open_position(self, candle, spread_points):
if self.Position == 0:
return
if self._entry_price <= 0 or self._point <= 0:
return
volume = abs(float(self.Position))
if volume <= 0:
return
h = float(candle.HighPrice)
lo = float(candle.LowPrice)
close = float(candle.ClosePrice)
if self._static_stop_enabled:
if self.Position > 0 and self._long_initial_stop is not None and lo <= self._long_initial_stop:
self.SellMarket()
self._reset_after_close()
return
if self.Position < 0 and self._short_initial_stop is not None and h >= self._short_initial_stop:
self.BuyMarket()
self._reset_after_close()
return
profit_points = (close - self._entry_price) / self._point if self.Position > 0 else (self._entry_price - close) / self._point
if profit_points < self._trail_start_points:
return
if self.Position > 0:
new_stop = close - self._trail_stop_distance
if self._long_trailing_stop is None or new_stop - self._long_trailing_stop >= self._point:
self._long_trailing_stop = new_stop
if self._long_trailing_stop is not None and lo <= self._long_trailing_stop:
self.SellMarket()
self._reset_after_close()
return
if self._current_spread_limit <= 0.0 or spread_points <= self._current_spread_limit:
self._try_add_to_position(True, profit_points, candle)
else:
new_stop = close + self._trail_stop_distance
if self._short_trailing_stop is None or self._short_trailing_stop - new_stop >= self._point:
self._short_trailing_stop = new_stop
if self._short_trailing_stop is not None and h >= self._short_trailing_stop:
self.BuyMarket()
self._reset_after_close()
return
if self._current_spread_limit <= 0.0 or spread_points <= self._current_spread_limit:
self._try_add_to_position(False, profit_points, candle)
def _try_add_to_position(self, is_long, profit_points, candle):
if not self._volatility_signal:
return
if is_long:
if not self._buy_signal:
return
if profit_points < self._buy_pyramid_level + self._pyramiding_step_points:
return
self.BuyMarket()
self._buy_pyramid_level = profit_points
self._static_stop_enabled = False
self._long_initial_stop = None
else:
if not self._sell_signal:
return
if profit_points < self._sell_pyramid_level + self._pyramiding_step_points:
return
self.SellMarket()
self._sell_pyramid_level = profit_points
self._static_stop_enabled = False
self._short_initial_stop = None
def _is_equity_risk_exceeded(self, candle):
pf = self.Portfolio
balance = None
if pf is not None:
balance = pf.CurrentValue if pf.CurrentValue is not None else pf.BeginValue
if balance is None or float(balance) <= 0 or self.Position == 0 or self._entry_price <= 0:
return False
volume = abs(float(self.Position))
current_price = float(candle.ClosePrice)
if self.Position > 0:
pnl = (current_price - self._entry_price) * volume
else:
pnl = (self._entry_price - current_price) * volume
drawdown = -pnl if pnl < 0 else 0.0
threshold = float(balance) * self.TotalEquityRisk / 100.0
return drawdown > threshold
def _get_spread_in_points(self):
return 0.0
def _update_volatility(self, candle):
self._volatility_signal = self._has_previous_candle
def _update_timing(self, candle):
h = float(candle.HighPrice)
lo = float(candle.LowPrice)
c = float(candle.ClosePrice)
cpiv = 100.0 * ((h + lo + c) / 3.0)
limit = len(self._typical_prices)
count = min(self._typical_count + 1, limit)
i = min(count - 1, limit - 1)
while i > 0:
self._typical_prices[i] = self._typical_prices[i - 1]
i -= 1
self._typical_prices[0] = cpiv
self._typical_count = count
self._calculate_timing_signals()
def _calculate_timing_signals(self):
if self._typical_count < 2:
self._buy_signal = False
self._sell_signal = False
return
self._timing_values = [50.0, 50.0, 50.0]
j = 0
i_counter = 0
cpiv = 0.0
ppiv = 0.0
dmov = 0.0
amov = 0.0
tval = 50.0
dtemp1 = dtemp2 = dtemp3 = dtemp4 = dtemp5 = dtemp6 = dtemp7 = dtemp8 = 0.0
atemp1 = atemp2 = atemp3 = atemp4 = atemp5 = atemp6 = atemp7 = atemp8 = 0.0
for idx in range(self._typical_count - 1, -1, -1):
typical = self._typical_prices[idx]
if j == 0:
j = 1
i_counter = 0
cpiv = typical
else:
if j < 7:
j += 1
ppiv = cpiv
cpiv = typical
dpiv = cpiv - ppiv
dtemp1 = (2.0 / 3.0) * dtemp1 + (1.0 / 3.0) * dpiv
dtemp2 = (1.0 / 3.0) * dtemp1 + (2.0 / 3.0) * dtemp2
dtemp3 = 1.5 * dtemp1 - dtemp2 / 2.0
dtemp4 = (2.0 / 3.0) * dtemp4 + (1.0 / 3.0) * dtemp3
dtemp5 = (1.0 / 3.0) * dtemp4 + (2.0 / 3.0) * dtemp5
dtemp6 = 1.5 * dtemp4 - dtemp5 / 2.0
dtemp7 = (2.0 / 3.0) * dtemp7 + (1.0 / 3.0) * dtemp6
dtemp8 = (1.0 / 3.0) * dtemp7 + (2.0 / 3.0) * dtemp8
dmov = 1.5 * dtemp7 - dtemp8 / 2.0
atemp1 = (2.0 / 3.0) * atemp1 + (1.0 / 3.0) * abs(dpiv)
atemp2 = (1.0 / 3.0) * atemp1 + (2.0 / 3.0) * atemp2
atemp3 = 1.5 * atemp1 - atemp2 / 2.0
atemp4 = (2.0 / 3.0) * atemp4 + (1.0 / 3.0) * atemp3
atemp5 = (1.0 / 3.0) * atemp4 + (2.0 / 3.0) * atemp5
atemp6 = 1.5 * atemp4 - atemp5 / 2.0
atemp7 = (2.0 / 3.0) * atemp7 + (1.0 / 3.0) * atemp6
atemp8 = (1.0 / 3.0) * atemp7 + (2.0 / 3.0) * atemp8
amov = 1.5 * atemp7 - atemp8 / 2.0
if j <= 6 and cpiv != ppiv:
i_counter += 1
if j == 6 and i_counter == 0:
j = 0
if j > 6 and amov > self.EpsilonTolerance:
tval = 50.0 * (dmov / amov + 1.0)
if tval > 100.0:
tval = 100.0
elif tval < 0.0:
tval = 0.0
else:
tval = 50.0
if idx <= 2:
self._timing_values[idx] = tval
self._buy_signal = self._timing_values[1] <= self._timing_values[2] and self._timing_values[0] > self._timing_values[1]
self._sell_signal = self._timing_values[1] >= self._timing_values[2] and self._timing_values[0] < self._timing_values[1]
def _is_trading_blocked_by_calendar(self, t):
if t.DayOfWeek == DayOfWeek.Friday and t.Hour >= 23:
return True
day_of_year = t.DayOfYear
if (day_of_year == 358 or day_of_year == 359 or day_of_year == 365 or day_of_year == 366) and t.Hour >= 16:
return True
return False
def OnReseted(self):
super(ii_outbreak_strategy, self).OnReseted()
self._point = 0.0
self._trail_stop_distance = 0.0
self._initial_stop_distance = 0.0
self._trail_start_points = 0.0
self._pyramiding_step_points = 0.0
self._static_stop_enabled = True
self._buy_signal = False
self._sell_signal = False
self._volatility_signal = False
self._buy_pyramid_level = 0.0
self._sell_pyramid_level = 0.0
self._current_volatility_threshold = 0.0
self._current_spread_limit = 0.0
self._long_trailing_stop = None
self._short_trailing_stop = None
self._long_initial_stop = None
self._short_initial_stop = None
self._timing_values = [50.0, 50.0, 50.0]
self._typical_prices = [0.0] * 120
self._typical_count = 0
self._has_previous_candle = False
self._entry_price = 0.0
def CreateClone(self):
return ii_outbreak_strategy()