Стратегия II Outbreak
Обзор
II Outbreak — высокочастотная пробойная стратегия, изначально написанная для MetaTrader 4. Она объединяет фирменный осциллятор тайминга с индикатором волатильности для поиска импульсных движений и сопровождает позиции с помощью адаптивного трейлинг-стопа и пирамидинга. Эта конвертация переносит исходную логику на высокоуровневый API StockSharp, сохраняя фильтры по спреду, волатильности и календарю.
Логика, перенесённая в StockSharp
Осциллятор тайминга
- Каждая новая свеча M1 добавляет «типичную цену» (среднее High/Low/Close × 100) в каскад сглаживаний исходного робота.
- Каскад восстанавливает цепочку экспоненциальных сглаживаний (буферы dtemp/atemp), формируя тайминговое значение в диапазоне от 0 до 100.
- Сигнал на покупку: значение переходит вверх через предыдущий уровень (buffer[0] > buffer[1] и buffer[1] ≤ buffer[2]).
- Сигнал на продажу: значение переходит вниз (buffer[0] < buffer[1] и buffer[1] ≥ buffer[2]).
Фильтр волатильности
- Стандартное отклонение с периодом 10 по ценам закрытия должно оставаться ниже
StdDevLimit. Превышение порога запрещает новые входы и при включённомWarningAlertsвыводит предупреждение в журнал. - Дополнительно рассчитывается исходный показатель давления волатильности: совокупность перекрытия двух минутных свечей и средней «плотности тиков» (объём / время). Значение должно превысить
VolatilityThreshold.
Правила входа
- Стратегия работает с одной связкой инструмент/таймфрейм, задаваемой параметром
CandleType(по умолчанию 1-минутные свечи). - При отсутствии позиции и разрешении календарного фильтра пересчитывается рабочий объём через
CalculateOrderVolume()и проверяется текущий спред относительноSpreadThreshold(используются заявки лучшего Bid/Ask). - Длинная позиция открывается при одновременном появлении сигнала осциллятора и подтверждённой волатильности. Короткая позиция — зеркально. При входе ставится статический стоп на расстоянии, равном удвоенному
TrailStopPoints.
Пирамидинг и трейлинг
- Трейлинг активируется, когда суммарная позиция достигает прибыли не менее
TrailStopPoints + int(Commission) + SpreadThresholdпунктов. - Стоп подтягивается на
TrailStopPointsпозади последней цены закрытия (отдельно для лонга и шорта). Любое улучшение больше одного пункта обновляет уровень стопа. - Пока соблюдаются условия по волатильности, таймингу и спреду, стратегия добавляет ордера через каждые
max(10, SpreadThreshold + 1)пунктов дополнительной прибыли. После первой доливки статический стоп отключается и остаётся только трейлинг.
Управление рисками и капиталом
- Размер позиции пересчитывается перед каждой заявкой:
баланс × MaximumRisk ÷ (500000 / AccountLeverage)с округлением к шагу объёма. При отсутствии данных по балансу используется значениеVolumeили минимальный лот инструмента. - Проверка маржи приближённо повторяет логику MetaTrader:
volume × price / leverage × (1 + MaximumRisk × 190). Если портфель не покрывает требуемую сумму, заявка игнорируется. - После активации пирамидинга контролируется плавающий убыток. Если просадка превышает
TotalEquityRiskпроцентов от стоимости счёта, все позиции закрываются.
Календарь и контроль спреда
- Торговля останавливается по пятницам после 23:00 серверного времени и в последние торговые дни года (дни 358, 359, 365 или 366) после 16:00.
- Каждый вход и доливка проверяют текущий спред и пропускают сделку при превышении допустимого порога.
Параметры
| Параметр | Значение по умолчанию | Описание |
|---|---|---|
Commission |
4 | Комиссия за круговой лот в пунктах; участвует в расчёте активации трейлинга. |
SpreadThreshold |
6 | Максимальный допустимый спред в пунктах для входа и пирамидинга. |
TrailStopPoints |
20 | Дистанция трейлинг-стопа в пунктах; стартовый стоп ставится вдвое дальше. |
TotalEquityRisk |
0.5 | Допустимая просадка счёта (в процентах), после которой все позиции принудительно закрываются. |
MaximumRisk |
0.1 | Доля капитала, выделяемая под одну заявку при расчёте объёма. |
StdDevLimit |
0.002 | Максимальное значение стандартного отклонения (10 периодов), допускающее новые входы. |
VolatilityThreshold |
800 | Минимальный показатель волатильности (амплитуда × плотность тиков) для торговли. |
AccountLeverage |
100 | Плечо счёта, используемое при оценке маржи и объёма. |
WarningAlerts |
true | Включает вывод предупреждений при блокировке сделок фильтром StdDev. |
CandleType |
1 минута | Тип свечей, используемый во всех расчётах. |
Индикаторы
StandardDeviation(Length = 10)по ценам закрытия для фильтра волатильности.- Встроенный осциллятор тайминга, воссозданный по формулам исходного эксперта.
Особенности реализации
- Для фильтрации по спреду необходим поток котировок уровня 1 (
Security.BestBid/BestAsk). При отсутствии данных предполагается нулевой спред. - Проверки маржи и капитала являются приближенными: в MetaTrader использовались специфические параметры счёта и размеры контрактов. При необходимости корректируйте
AccountLeverage,MaximumRiskилиVolumeпод условия брокера. - Конвертация выполнена с использованием высокоуровневого API StockSharp (
SubscribeCandles+Bind). Все комментарии в коде оставлены на английском языке. Python-версия стратегии не создавалась.
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()