II Outbreak Strategy
Overview
The II Outbreak strategy is a high-frequency breakout system originally written for MetaTrader 4. It combines a proprietary timing oscillator with a volatility pressure gauge to enter strong directional moves, then manages trades using adaptive trailing stops and pyramiding. This conversion reproduces the original logic on top of the StockSharp high-level API and keeps the same guardrails for spread, volatility and calendar filters.
Converted trading logic
Timing oscillator
- Each new M1 candle contributes a "typical price" (average of high, low and close multiplied by 100) that feeds the legacy smoothing cascade.
- The cascade rebuilds the original nested moving average / difference pipeline (dtemp/atemp buffers) to produce a timing value from 0 to 100.
- Buy signal: timing value crosses upward over its previous reading (buffer[0] > buffer[1] with buffer[1] ≤ buffer[2]).
- Sell signal: timing value crosses downward (buffer[0] < buffer[1] with buffer[1] ≥ buffer[2]).
Volatility filter
- A 10-period standard deviation on closing prices must stay below the
StdDevLimit. When the limit is breached, no fresh positions are allowed and an optional warning is logged. - A custom volatility score replicates the original amplitude × tick density formula: it uses the overlap between the current and previous minute candle and the average number of ticks per second. The score must exceed the configurable
VolatilityThreshold.
Entry rules
- The strategy works on a single symbol/timeframe pair supplied through the
CandleTypeparameter (defaults to 1-minute candles). - When no position is open and the calendar filter allows trading, the engine refreshes lot size through
CalculateOrderVolume()and verifies current spread againstSpreadThreshold(using level 1 bid/ask data). - A long position is opened if the timing oscillator issues a buy signal and the volatility score is valid. A short position follows the mirrored condition. Upon entry, a static stop is placed two times the
TrailStopPointsbelow/above the fill price.
Pyramiding and trailing
- The trailing module activates once the aggregated position earns at least
TrailStopPoints + int(Commission) + SpreadThresholdpoints of unrealized profit. - The stop is tightened to
TrailStopPointsbehind the latest close (tracked separately for longs and shorts). Any improvement larger than one point updates the trailing price. - As long as volatility, timing and spread conditions remain valid, the strategy can pyramid new orders every
max(10, SpreadThreshold + 1)points of additional profit. New orders disable the static stop and rely purely on the trailing logic.
Risk and capital management
- Position size is recalculated before each order:
balance × MaximumRisk ÷ (500000 / AccountLeverage)rounded to the security volume step. If balance information is unavailable, it falls back to theVolumeor minimum lot. - A simplified margin check approximates the original MetaTrader guard (
volume × price / leverage × (1 + MaximumRisk × 190)). Orders are ignored if the account value cannot cover that amount. - After pyramiding is enabled, the strategy monitors floating loss. When the unrealized drawdown exceeds
TotalEquityRiskpercent of the account value, all positions are liquidated.
Calendar & spread guardrails
- Trading stops on Fridays after 23:00 server time and during the last trading days of the year (day of year 358, 359, 365 or 366) after 16:00.
- Every entry and add-on checks the current bid/ask spread and skips execution if it breaches the configured threshold.
Parameters
| Parameter | Default | Description |
|---|---|---|
Commission |
4 | Round-lot commission in points used when calculating the trailing activation offset. |
SpreadThreshold |
6 | Maximum spread (in points) allowed for new entries or pyramiding. |
TrailStopPoints |
20 | Trailing stop distance in points; the initial stop is twice this value. |
TotalEquityRisk |
0.5 | Percentage of account equity loss that triggers a forced exit after pyramiding. |
MaximumRisk |
0.1 | Fraction of account balance committed to each order when sizing volume. |
StdDevLimit |
0.002 | Maximum 10-period standard deviation to accept new trades. |
VolatilityThreshold |
800 | Minimum volatility score (amplitude × tick density) required for trading. |
AccountLeverage |
100 | Account leverage used in margin approximation and position sizing. |
WarningAlerts |
true | Enables logging when the standard deviation filter blocks entries. |
CandleType |
1 minute | Candle type used for all calculations. |
Indicators
StandardDeviation(Length = 10)on close prices for the volatility filter.- Custom timing oscillator reproduced from the original EA (implemented inline without StockSharp indicator objects).
Implementation notes
- Spread filtering requires live level 1 data (
Security.BestBid/BestAsk). When the feed is absent the strategy assumes zero spread. - Margin and equity checks are approximations because the original EA relied on MetaTrader-specific account properties and contract sizes. Adjust
AccountLeverage,MaximumRiskorVolumeto fit the broker model. - The conversion uses the StockSharp high-level API (candle subscriptions with
Bind) and keeps all comments in English as requested. No Python port is generated for this strategy.
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()