Стратегия SV Daily Breakout
Обзор
SV Daily Breakout — точная конверсия советника MetaTrader 5 «SV v.4.2.5» на C#. Стратегия анализирует рынок после закрытия каждой свечи и допускает не более одной сделки в сутки. Входы разрешены только после наступления заданного времени. Алгоритм пропускает последние Shift баров, затем рассматривает следующие Interval баров и сравнивает их экстремумы с двумя сглаженными скользящими средними, чтобы определить глубину отклонения цены от средних значений.
Правила торговли
Условия входа
- Дневной фильтр — пока серверное время меньше Start Hour/Start Minute, сигналы игнорируются. В течение дня возможен только один вход.
- Окно анализа — из расчёта исключаются
Shiftпоследних баров, а для следующихIntervalбаров определяется максимум и минимум. - Покупка — если максимум анализируемого диапазона находится ниже медленной MA, а минимум — ниже быстрой MA, стратегия закрывает шорты и открывает длинную позицию.
- Продажа — если минимум диапазона выше медленной MA, а максимум — выше быстрой MA, закрываются лонги и открывается короткая позиция.
Управление позицией
- Стоп-лосс — задаётся на расстоянии
Stop Loss (pips)от цены входа. При достижении уровня позиция закрывается. - Тейк-профит — фиксируется на расстоянии
Take Profit (pips)от цены входа. Достижение уровня приводит к фиксации прибыли. - Трейлинг-стоп — активируется, если заданы ненулевые
Trailing StopиTrailing Step. Для лонга стоп переносится на уровеньClose − Trailing Stop, когда прибыль превышаетTrailing Stop + Trailing Step. Для шорта логика зеркальная. - Суточный запрет — после закрытия сделки в текущие сутки новых входов не будет.
Расчёт объёма
- Ручной режим — при
Use Manual Volume = trueиспользуется фиксированный объём Volume (корректируется под шаг объёма инструмента). - Режим по риску — при
Use Manual Volume = falseобъём рассчитывается по формуле рискаRisk %. Берётся доля капитала, делится на денежную стоимость стоп-лосса с учётом шага цены и шага стоимости шага.
Параметры
| Параметр | Значение по умолчанию | Описание |
|---|---|---|
| Use Manual Volume | false |
Использовать фиксированный объём вместо расчёта по риску. |
| Volume | 0.1 |
Объём сделки в ручном режиме. |
| Risk % | 5 |
Процент капитала, выделяемый на одну сделку. |
| Stop Loss (pips) | 50 |
Размер стоп-лосса в пунктах (0 — отключено). |
| Take Profit (pips) | 50 |
Размер тейк-профита в пунктах (0 — отключено). |
| Trailing Stop (pips) | 5 |
Размер трейлинг-стопа в пунктах. Требует ненулевого Trailing Step. |
| Trailing Step (pips) | 5 |
Минимальное приращение прибыли для сдвига трейлинг-стопа. |
| Start Hour | 19 |
Час начала поиска сигналов (время биржи). |
| Start Minute | 0 |
Минута начала поиска сигналов (время биржи). |
| Shift | 6 |
Число последних баров, которые исключаются из расчёта диапазона. |
| Interval | 27 |
Число баров, по которым вычисляются максимум и минимум. |
| Fast MA Period | 14 |
Период быстрой скользящей средней. |
| Fast MA Shift | 0 |
Горизонтальный сдвиг значения быстрой MA (в барах). |
| Fast MA Method | Smma |
Метод расчёта быстрой MA. |
| Fast Applied Price | Median |
Тип цены для быстрой MA. |
| Slow MA Period | 41 |
Период медленной скользящей средней. |
| Slow MA Shift | 0 |
Горизонтальный сдвиг значения медленной MA (в барах). |
| Slow MA Method | Smma |
Метод расчёта медленной MA. |
| Slow Applied Price | Median |
Тип цены для медленной MA. |
| Candle Type | 1 hour |
Используемый таймфрейм свечей. |
Дополнительная информация
- Алгоритм полностью сохраняет идею оригинального советника — анализ диапазона после задержки на
Shiftбаров позволяет фильтровать последние всплески волатильности. - Трейлинг-стоп обновляется по цене закрытия свечи, что имитирует тиковую логику MetaTrader. При необходимости скорректируйте параметры под специфику инструмента.
- Для корректного расчёта объёма в режиме риска заполните у инструмента поля
PriceStep,StepPriceиVolumeStep. - Метод
StartProtection()уже вызван, поэтому можно дополнять стратегию стандартными средствами риск-менеджмента StockSharp. - Чтобы результаты совпадали с версией MetaTrader, используйте одинаковый часовой пояс сервера для данных и торгового счёта, на который ориентированы параметры Start Hour и Start Minute.
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>
/// Daily breakout strategy converted from the "SV v.4.2.5" MetaTrader 5 expert advisor.
/// Evaluates one trade per day after a configurable start time using moving average filters.
/// </summary>
public class SvDailyBreakoutStrategy : Strategy
{
private readonly StrategyParam<bool> _useManualVolume;
private readonly StrategyParam<decimal> _riskPercent;
private readonly StrategyParam<int> _stopLossPips;
private readonly StrategyParam<int> _takeProfitPips;
private readonly StrategyParam<int> _trailingStopPips;
private readonly StrategyParam<int> _trailingStepPips;
private readonly StrategyParam<int> _startHour;
private readonly StrategyParam<int> _startMinute;
private readonly StrategyParam<int> _shift;
private readonly StrategyParam<int> _interval;
private readonly StrategyParam<int> _fastMaPeriod;
private readonly StrategyParam<int> _fastMaShift;
private readonly StrategyParam<MovingAverageMethods> _fastMaMethod;
private readonly StrategyParam<AppliedPrices> _fastAppliedPrice;
private readonly StrategyParam<int> _slowMaPeriod;
private readonly StrategyParam<int> _slowMaShift;
private readonly StrategyParam<MovingAverageMethods> _slowMaMethod;
private readonly StrategyParam<AppliedPrices> _slowAppliedPrice;
private readonly StrategyParam<DataType> _candleType;
private IIndicator _fastMa;
private IIndicator _slowMa;
private readonly List<decimal> _fastMaValues = new();
private readonly List<decimal> _slowMaValues = new();
private readonly List<decimal> _highHistory = new();
private readonly List<decimal> _lowHistory = new();
private decimal? _entryPrice;
private decimal? _stopPrice;
private decimal? _takeProfitPrice;
private decimal? _trailingStopPrice;
private DateTime? _currentDay;
private decimal _pipSize;
/// <summary>
/// Use manual volume instead of the risk-based sizing model.
/// </summary>
public bool UseManualVolume
{
get => _useManualVolume.Value;
set => _useManualVolume.Value = value;
}
/// <summary>
/// Risk percentage of account equity used when calculating the dynamic position size.
/// </summary>
public decimal RiskPercent
{
get => _riskPercent.Value;
set => _riskPercent.Value = value;
}
/// <summary>
/// Stop loss distance expressed in pips.
/// </summary>
public int StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Take profit distance expressed in pips.
/// </summary>
public int TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Trailing stop distance expressed in pips.
/// </summary>
public int TrailingStopPips
{
get => _trailingStopPips.Value;
set => _trailingStopPips.Value = value;
}
/// <summary>
/// Trailing step distance expressed in pips.
/// </summary>
public int TrailingStepPips
{
get => _trailingStepPips.Value;
set => _trailingStepPips.Value = value;
}
/// <summary>
/// Hour of the day (exchange time) when the strategy starts searching for entries.
/// </summary>
public int StartHour
{
get => _startHour.Value;
set => _startHour.Value = value;
}
/// <summary>
/// Minute of the hour when the strategy starts searching for entries.
/// </summary>
public int StartMinute
{
get => _startMinute.Value;
set => _startMinute.Value = value;
}
/// <summary>
/// Number of recent bars excluded from the high/low analysis window.
/// </summary>
public int Shift
{
get => _shift.Value;
set => _shift.Value = value;
}
/// <summary>
/// Number of bars that are analysed when computing the breakout range.
/// </summary>
public int Interval
{
get => _interval.Value;
set => _interval.Value = value;
}
/// <summary>
/// Fast moving average period.
/// </summary>
public int FastMaPeriod
{
get => _fastMaPeriod.Value;
set => _fastMaPeriod.Value = value;
}
/// <summary>
/// Fast moving average horizontal shift.
/// </summary>
public int FastMaShift
{
get => _fastMaShift.Value;
set => _fastMaShift.Value = value;
}
/// <summary>
/// Fast moving average calculation method.
/// </summary>
public MovingAverageMethods FastMaMethod
{
get => _fastMaMethod.Value;
set => _fastMaMethod.Value = value;
}
/// <summary>
/// Applied price used for the fast moving average.
/// </summary>
public AppliedPrices FastAppliedPrice
{
get => _fastAppliedPrice.Value;
set => _fastAppliedPrice.Value = value;
}
/// <summary>
/// Slow moving average period.
/// </summary>
public int SlowMaPeriod
{
get => _slowMaPeriod.Value;
set => _slowMaPeriod.Value = value;
}
/// <summary>
/// Slow moving average horizontal shift.
/// </summary>
public int SlowMaShift
{
get => _slowMaShift.Value;
set => _slowMaShift.Value = value;
}
/// <summary>
/// Slow moving average calculation method.
/// </summary>
public MovingAverageMethods SlowMaMethod
{
get => _slowMaMethod.Value;
set => _slowMaMethod.Value = value;
}
/// <summary>
/// Applied price used for the slow moving average.
/// </summary>
public AppliedPrices SlowAppliedPrice
{
get => _slowAppliedPrice.Value;
set => _slowAppliedPrice.Value = value;
}
/// <summary>
/// Candle type used by the strategy.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes strategy parameters with defaults that match the original expert advisor.
/// </summary>
public SvDailyBreakoutStrategy()
{
_useManualVolume = Param(nameof(UseManualVolume), false)
.SetDisplay("Use Manual Volume", "Use fixed volume instead of risk percentage", "Risk");
_riskPercent = Param(nameof(RiskPercent), 5m)
.SetGreaterThanZero()
.SetDisplay("Risk %", "Risk percentage of account equity", "Risk");
_stopLossPips = Param(nameof(StopLossPips), 50)
.SetNotNegative()
.SetDisplay("Stop Loss (pips)", "Stop loss distance in pips", "Risk");
_takeProfitPips = Param(nameof(TakeProfitPips), 50)
.SetNotNegative()
.SetDisplay("Take Profit (pips)", "Take profit distance in pips", "Risk");
_trailingStopPips = Param(nameof(TrailingStopPips), 5)
.SetNotNegative()
.SetDisplay("Trailing Stop (pips)", "Trailing stop distance in pips", "Risk");
_trailingStepPips = Param(nameof(TrailingStepPips), 5)
.SetNotNegative()
.SetDisplay("Trailing Step (pips)", "Trailing step increment in pips", "Risk");
_startHour = Param(nameof(StartHour), 0)
.SetDisplay("Start Hour", "Hour when trading may begin", "Trading Window");
_startMinute = Param(nameof(StartMinute), 0)
.SetDisplay("Start Minute", "Minute when trading may begin", "Trading Window");
_shift = Param(nameof(Shift), 2)
.SetNotNegative()
.SetDisplay("Shift", "Number of newest bars excluded from range analysis", "Logic");
_interval = Param(nameof(Interval), 10)
.SetGreaterThanZero()
.SetDisplay("Interval", "Number of historical bars analysed", "Logic");
_fastMaPeriod = Param(nameof(FastMaPeriod), 5)
.SetGreaterThanZero()
.SetDisplay("Fast MA Period", "Fast moving average length", "Indicators");
_fastMaShift = Param(nameof(FastMaShift), 0)
.SetNotNegative()
.SetDisplay("Fast MA Shift", "Horizontal shift for the fast moving average", "Indicators");
_fastMaMethod = Param(nameof(FastMaMethod), MovingAverageMethods.Ema)
.SetDisplay("Fast MA Method", "Calculation method for the fast moving average", "Indicators");
_fastAppliedPrice = Param(nameof(FastAppliedPrice), AppliedPrices.Median)
.SetDisplay("Fast Applied Price", "Price type used for the fast moving average", "Indicators");
_slowMaPeriod = Param(nameof(SlowMaPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("Slow MA Period", "Slow moving average length", "Indicators");
_slowMaShift = Param(nameof(SlowMaShift), 0)
.SetNotNegative()
.SetDisplay("Slow MA Shift", "Horizontal shift for the slow moving average", "Indicators");
_slowMaMethod = Param(nameof(SlowMaMethod), MovingAverageMethods.Ema)
.SetDisplay("Slow MA Method", "Calculation method for the slow moving average", "Indicators");
_slowAppliedPrice = Param(nameof(SlowAppliedPrice), AppliedPrices.Median)
.SetDisplay("Slow Applied Price", "Price type used for the slow moving average", "Indicators");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Candle series used for calculations", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_fastMaValues.Clear();
_slowMaValues.Clear();
_highHistory.Clear();
_lowHistory.Clear();
_entryPrice = null;
_pipSize = 0m;
_stopPrice = null;
_takeProfitPrice = null;
_trailingStopPrice = null;
_currentDay = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
// validation removed for flexibility
_fastMa = new ExponentialMovingAverage { Length = FastMaPeriod };
_slowMa = new ExponentialMovingAverage { Length = SlowMaPeriod };
var decimals = Security?.Decimals ?? 2;
var step = Security?.PriceStep ?? 0.01m;
var factor = decimals is 3 or 5 ? 10m : 1m;
_pipSize = step * factor;
if (_pipSize <= 0m)
_pipSize = step > 0m ? step : 0.01m;
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_fastMa, _slowMa, ProcessCandleWithMa)
.Start();
}
private void ProcessCandleWithMa(ICandleMessage candle, decimal fastValue, decimal slowValue)
{
if (candle.State != CandleStates.Finished)
return;
UpdateDailyState(candle.CloseTime);
UpdateRangeHistory(candle);
UpdateTrailing(candle);
if (CheckProtectiveExits(candle))
return;
if (Position != 0)
return;
if (!TryGetRangeExtremes(out var lowest, out var highest))
return;
if (highest < slowValue && lowest < fastValue)
{
EnterPosition(true, candle);
return;
}
if (lowest > slowValue && highest > fastValue)
{
EnterPosition(false, candle);
}
}
private void EnterPosition(bool isLong, ICandleMessage candle)
{
var entryPrice = candle.ClosePrice;
var stopDistance = StopLossPips > 0 ? StopLossPips * _pipSize : 0m;
var volume = UseManualVolume
? NormalizeVolume(Volume)
: CalculateRiskBasedVolume(stopDistance);
if (volume <= 0m)
return;
if (isLong)
{
var totalVolume = volume + (Position < 0 ? Math.Abs(Position) : 0m);
if (totalVolume <= 0m)
return;
BuyMarket(totalVolume);
_entryPrice = entryPrice;
_stopPrice = StopLossPips > 0 ? entryPrice - stopDistance : null;
_takeProfitPrice = TakeProfitPips > 0 ? entryPrice + TakeProfitPips * _pipSize : null;
}
else
{
var totalVolume = volume + (Position > 0 ? Position : 0m);
if (totalVolume <= 0m)
return;
SellMarket(totalVolume);
_entryPrice = entryPrice;
_stopPrice = StopLossPips > 0 ? entryPrice + stopDistance : null;
_takeProfitPrice = TakeProfitPips > 0 ? entryPrice - TakeProfitPips * _pipSize : null;
}
_trailingStopPrice = TrailingStopPips > 0 ? _stopPrice : null;
}
private void UpdateDailyState(DateTimeOffset time)
{
var day = time.Date;
if (_currentDay != day)
{
_currentDay = day;
}
}
private void UpdateRangeHistory(ICandleMessage candle)
{
_highHistory.Add(candle.HighPrice);
_lowHistory.Add(candle.LowPrice);
var maxCount = Math.Max(Shift + Interval + 5, 50);
if (_highHistory.Count > maxCount)
{
var remove = _highHistory.Count - maxCount;
_highHistory.RemoveRange(0, remove);
_lowHistory.RemoveRange(0, remove);
}
}
private decimal? ProcessMovingAverage(IIndicator indicator, AppliedPrices priceMode, List<decimal> buffer, int shift, ICandleMessage candle)
{
var price = GetAppliedPrice(candle, priceMode);
var result = indicator.Process(new DecimalIndicatorValue(indicator, price, candle.OpenTime));
if (!result.IsFormed)
return null;
var value = result.GetValue<decimal>();
buffer.Add(value);
var maxSize = Math.Max(shift + 1, 100);
if (buffer.Count > maxSize)
buffer.RemoveAt(0);
var index = buffer.Count - 1 - shift;
if (index < 0 || index >= buffer.Count)
return null;
return buffer[index];
}
private bool TryGetRangeExtremes(out decimal lowest, out decimal highest)
{
lowest = 0m;
highest = 0m;
var required = Shift + Interval;
if (required <= 0)
return false;
if (_lowHistory.Count < required || _highHistory.Count < required)
return false;
var low = decimal.MaxValue;
var high = decimal.MinValue;
var total = _lowHistory.Count;
for (var offset = Shift; offset < Shift + Interval; offset++)
{
var index = total - 1 - offset;
if (index < 0)
return false;
var lowValue = _lowHistory[index];
var highValue = _highHistory[index];
if (lowValue < low)
low = lowValue;
if (highValue > high)
high = highValue;
}
if (low == decimal.MaxValue || high == decimal.MinValue)
return false;
lowest = low;
highest = high;
return true;
}
private void UpdateTrailing(ICandleMessage candle)
{
if (TrailingStopPips <= 0 || TrailingStepPips <= 0 || _entryPrice is null)
return;
var trailDistance = TrailingStopPips * _pipSize;
var stepDistance = TrailingStepPips * _pipSize;
if (Position > 0)
{
var current = candle.ClosePrice;
var entry = _entryPrice.Value;
if (current - entry > trailDistance + stepDistance)
{
var threshold = current - (trailDistance + stepDistance);
if (_stopPrice is null || _stopPrice < threshold)
{
var newStop = current - trailDistance;
if (_stopPrice is null || newStop > _stopPrice)
{
_stopPrice = newStop;
_trailingStopPrice = newStop;
}
}
}
}
else if (Position < 0)
{
var current = candle.ClosePrice;
var entry = _entryPrice.Value;
if (entry - current > trailDistance + stepDistance)
{
var threshold = current + trailDistance + stepDistance;
if (_stopPrice is null || _stopPrice > threshold)
{
var newStop = current + trailDistance;
if (_stopPrice is null || newStop < _stopPrice)
{
_stopPrice = newStop;
_trailingStopPrice = newStop;
}
}
}
}
}
private bool CheckProtectiveExits(ICandleMessage candle)
{
if (Position > 0)
{
if (_stopPrice is decimal stop && candle.LowPrice <= stop)
{
SellMarket(Position);
ResetTradeState();
return true;
}
if (_takeProfitPrice is decimal take && candle.HighPrice >= take)
{
SellMarket(Position);
ResetTradeState();
return true;
}
}
else if (Position < 0)
{
var volume = Math.Abs(Position);
if (_stopPrice is decimal stop && candle.HighPrice >= stop)
{
BuyMarket(volume);
ResetTradeState();
return true;
}
if (_takeProfitPrice is decimal take && candle.LowPrice <= take)
{
BuyMarket(volume);
ResetTradeState();
return true;
}
}
else if (_entryPrice is not null)
{
ResetTradeState();
}
return false;
}
private decimal CalculateRiskBasedVolume(decimal stopDistance)
{
if (UseManualVolume || stopDistance <= 0m)
return NormalizeVolume(Volume);
var portfolioValue = Portfolio?.CurrentValue ?? 0m;
if (portfolioValue <= 0m)
return NormalizeVolume(Volume);
var riskAmount = portfolioValue * RiskPercent / 100m;
if (riskAmount <= 0m)
return NormalizeVolume(Volume);
var step = Security?.PriceStep ?? _pipSize;
if (step <= 0m)
step = _pipSize > 0m ? _pipSize : 1m;
var stepValue = GetSecurityValue<decimal?>(Level1Fields.StepPrice) ?? step;
if (stepValue <= 0m)
stepValue = step;
var steps = stopDistance / step;
if (steps <= 0m)
return NormalizeVolume(Volume);
var riskPerUnit = steps * stepValue;
if (riskPerUnit <= 0m)
return NormalizeVolume(Volume);
var rawVolume = riskAmount / riskPerUnit;
return NormalizeVolume(rawVolume);
}
private decimal NormalizeVolume(decimal volume)
{
if (Security is null)
return volume;
var step = Security.VolumeStep ?? 1m;
if (step > 0m)
volume = Math.Floor(volume / step) * step;
if (volume < step)
volume = step;
return volume;
}
private static decimal GetAppliedPrice(ICandleMessage candle, AppliedPrices mode)
{
return mode switch
{
AppliedPrices.Open => candle.OpenPrice,
AppliedPrices.High => candle.HighPrice,
AppliedPrices.Low => candle.LowPrice,
AppliedPrices.Median => (candle.HighPrice + candle.LowPrice) / 2m,
AppliedPrices.Typical => (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3m,
AppliedPrices.Weighted => (candle.HighPrice + candle.LowPrice + 2m * candle.ClosePrice) / 4m,
_ => candle.ClosePrice,
};
}
private static IIndicator CreateMovingAverage(MovingAverageMethods method, int length)
{
return method switch
{
MovingAverageMethods.Sma => new SimpleMovingAverage { Length = length },
MovingAverageMethods.Ema => new ExponentialMovingAverage { Length = length },
MovingAverageMethods.Smma => new SmoothedMovingAverage { Length = length },
MovingAverageMethods.Lwma => new WeightedMovingAverage { Length = length },
_ => new SimpleMovingAverage { Length = length },
};
}
private void ResetTradeState()
{
_entryPrice = null;
_stopPrice = null;
_takeProfitPrice = null;
_trailingStopPrice = null;
}
/// <summary>
/// Available moving average calculation methods.
/// </summary>
public enum MovingAverageMethods
{
/// <summary>
/// Simple moving average.
/// </summary>
Sma,
/// <summary>
/// Exponential moving average.
/// </summary>
Ema,
/// <summary>
/// Smoothed moving average (SMMA).
/// </summary>
Smma,
/// <summary>
/// Linear weighted moving average (LWMA).
/// </summary>
Lwma
}
/// <summary>
/// Price sources supported by the moving averages.
/// </summary>
public enum AppliedPrices
{
/// <summary>
/// Close price.
/// </summary>
Close,
/// <summary>
/// Open price.
/// </summary>
Open,
/// <summary>
/// High price.
/// </summary>
High,
/// <summary>
/// Low price.
/// </summary>
Low,
/// <summary>
/// Median price (high + low) / 2.
/// </summary>
Median,
/// <summary>
/// Typical price (high + low + close) / 3.
/// </summary>
Typical,
/// <summary>
/// Weighted close price (high + low + 2 * close) / 4.
/// </summary>
Weighted
}
}
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
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
class sv_daily_breakout_strategy(Strategy):
def __init__(self):
super(sv_daily_breakout_strategy, self).__init__()
self._use_manual_volume = self.Param("UseManualVolume", False)
self._risk_percent = self.Param("RiskPercent", 5.0)
self._stop_loss_pips = self.Param("StopLossPips", 50)
self._take_profit_pips = self.Param("TakeProfitPips", 50)
self._trailing_stop_pips = self.Param("TrailingStopPips", 5)
self._trailing_step_pips = self.Param("TrailingStepPips", 5)
self._start_hour = self.Param("StartHour", 0)
self._start_minute = self.Param("StartMinute", 0)
self._shift = self.Param("Shift", 2)
self._interval = self.Param("Interval", 10)
self._fast_ma_period = self.Param("FastMaPeriod", 5)
self._slow_ma_period = self.Param("SlowMaPeriod", 14)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._high_history = []
self._low_history = []
self._entry_price = None
self._stop_price = None
self._take_profit_price = None
self._trailing_stop_price = None
self._current_day = None
self._pip_size = 0.0
@property
def UseManualVolume(self):
return self._use_manual_volume.Value
@property
def RiskPercent(self):
return self._risk_percent.Value
@property
def StopLossPips(self):
return self._stop_loss_pips.Value
@property
def TakeProfitPips(self):
return self._take_profit_pips.Value
@property
def TrailingStopPips(self):
return self._trailing_stop_pips.Value
@property
def TrailingStepPips(self):
return self._trailing_step_pips.Value
@property
def StartHour(self):
return self._start_hour.Value
@property
def StartMinute(self):
return self._start_minute.Value
@property
def Shift(self):
return self._shift.Value
@property
def Interval(self):
return self._interval.Value
@property
def FastMaPeriod(self):
return self._fast_ma_period.Value
@property
def SlowMaPeriod(self):
return self._slow_ma_period.Value
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(sv_daily_breakout_strategy, self).OnStarted2(time)
sec = self.Security
decimals = int(sec.Decimals) if sec is not None and sec.Decimals is not None else 2
step = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None else 0.01
factor = 10.0 if decimals == 3 or decimals == 5 else 1.0
self._pip_size = step * factor
if self._pip_size <= 0:
self._pip_size = step if step > 0 else 0.01
fast_ma = ExponentialMovingAverage()
fast_ma.Length = self.FastMaPeriod
slow_ma = ExponentialMovingAverage()
slow_ma.Length = self.SlowMaPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(fast_ma, slow_ma, self._process_candle).Start()
def _process_candle(self, candle, fast_value, slow_value):
if candle.State != CandleStates.Finished:
return
fast_val = float(fast_value)
slow_val = float(slow_value)
self._update_range_history(candle)
self._update_trailing(candle)
if self._check_protective_exits(candle):
return
if float(self.Position) != 0:
return
lowest, highest = self._try_get_range_extremes()
if lowest is None or highest is None:
return
if highest < slow_val and lowest < fast_val:
self._enter_position(True, candle)
return
if lowest > slow_val and highest > fast_val:
self._enter_position(False, candle)
def _enter_position(self, is_long, candle):
entry_price = float(candle.ClosePrice)
stop_distance = self.StopLossPips * self._pip_size if self.StopLossPips > 0 else 0.0
volume = float(self.Volume)
if volume <= 0:
return
pos = float(self.Position)
if is_long:
total_volume = volume + (abs(pos) if pos < 0 else 0.0)
if total_volume <= 0:
return
self.BuyMarket(total_volume)
self._entry_price = entry_price
self._stop_price = entry_price - stop_distance if self.StopLossPips > 0 else None
self._take_profit_price = entry_price + self.TakeProfitPips * self._pip_size if self.TakeProfitPips > 0 else None
else:
total_volume = volume + (pos if pos > 0 else 0.0)
if total_volume <= 0:
return
self.SellMarket(total_volume)
self._entry_price = entry_price
self._stop_price = entry_price + stop_distance if self.StopLossPips > 0 else None
self._take_profit_price = entry_price - self.TakeProfitPips * self._pip_size if self.TakeProfitPips > 0 else None
self._trailing_stop_price = self._stop_price if self.TrailingStopPips > 0 else None
def _update_range_history(self, candle):
self._high_history.append(float(candle.HighPrice))
self._low_history.append(float(candle.LowPrice))
max_count = max(self.Shift + self.Interval + 5, 50)
if len(self._high_history) > max_count:
remove = len(self._high_history) - max_count
self._high_history = self._high_history[remove:]
self._low_history = self._low_history[remove:]
def _try_get_range_extremes(self):
required = self.Shift + self.Interval
if required <= 0:
return (None, None)
if len(self._low_history) < required or len(self._high_history) < required:
return (None, None)
low = float("inf")
high = float("-inf")
total = len(self._low_history)
for offset in range(self.Shift, self.Shift + self.Interval):
index = total - 1 - offset
if index < 0:
return (None, None)
low_val = self._low_history[index]
high_val = self._high_history[index]
if low_val < low:
low = low_val
if high_val > high:
high = high_val
if low == float("inf") or high == float("-inf"):
return (None, None)
return (low, high)
def _update_trailing(self, candle):
if self.TrailingStopPips <= 0 or self.TrailingStepPips <= 0 or self._entry_price is None:
return
trail_distance = self.TrailingStopPips * self._pip_size
step_distance = self.TrailingStepPips * self._pip_size
pos = float(self.Position)
if pos > 0:
current = float(candle.ClosePrice)
entry = self._entry_price
if current - entry > trail_distance + step_distance:
threshold = current - (trail_distance + step_distance)
if self._stop_price is None or self._stop_price < threshold:
new_stop = current - trail_distance
if self._stop_price is None or new_stop > self._stop_price:
self._stop_price = new_stop
self._trailing_stop_price = new_stop
elif pos < 0:
current = float(candle.ClosePrice)
entry = self._entry_price
if entry - current > trail_distance + step_distance:
threshold = current + trail_distance + step_distance
if self._stop_price is None or self._stop_price > threshold:
new_stop = current + trail_distance
if self._stop_price is None or new_stop < self._stop_price:
self._stop_price = new_stop
self._trailing_stop_price = new_stop
def _check_protective_exits(self, candle):
pos = float(self.Position)
if pos > 0:
if self._stop_price is not None and float(candle.LowPrice) <= self._stop_price:
self.SellMarket(pos)
self._reset_trade_state()
return True
if self._take_profit_price is not None and float(candle.HighPrice) >= self._take_profit_price:
self.SellMarket(pos)
self._reset_trade_state()
return True
elif pos < 0:
volume = abs(pos)
if self._stop_price is not None and float(candle.HighPrice) >= self._stop_price:
self.BuyMarket(volume)
self._reset_trade_state()
return True
if self._take_profit_price is not None and float(candle.LowPrice) <= self._take_profit_price:
self.BuyMarket(volume)
self._reset_trade_state()
return True
elif self._entry_price is not None:
self._reset_trade_state()
return False
def _reset_trade_state(self):
self._entry_price = None
self._stop_price = None
self._take_profit_price = None
self._trailing_stop_price = None
def OnReseted(self):
super(sv_daily_breakout_strategy, self).OnReseted()
self._high_history = []
self._low_history = []
self._entry_price = None
self._stop_price = None
self._take_profit_price = None
self._trailing_stop_price = None
self._current_day = None
self._pip_size = 0.0
def CreateClone(self):
return sv_daily_breakout_strategy()