SMC Hilo MaxMin — стратегия пробоя диапазона
Общее описание
Стратегия повторяет логику советника MetaTrader SMC MaxMin at 1200. В заранее заданный час терминала (SetHour) она ставит
отложенный ордер Buy Stop выше максимума предыдущей свечи и Sell Stop ниже минимума. Уровни автоматически смещаются на минимальную
дистанцию, требуемую брокером, — значение задаётся в пунктах и переводится в цену через Security.PriceStep. После срабатывания
одного из стопов противоположный ордер отменяется, а позиция сопровождается фиксированным стоп-лоссом, тейк-профитом и, при
необходимости, трейлинг-стопом.
Основные отличия от исходного MQL4-скрипта:
- Вместо прямых вызовов
OrderSendиспользуются высокоуровневые методы StockSharp (BuyStop,SellStop,BuyLimit,SellLimit). - Все расстояния задаются в пунктах и приводятся к ценам по реальному шагу котировки (
Security.PriceStep). - Трейлинг-стоп подтягивается только после достижения прибыли, превышающей заданный буфер, что предотвращает лишние отмены.
- Работа ведётся через подписку на свечи, без ручного перебора истории и без собственных массивов значений.
Правила торговли
- Час запуска — когда серверное время по часам равно
SetHour, берётся предыдущая завершённая свеча. - Вход в лонг — Buy Stop выставляется на
high предыдущей свечи + минимальная дистанция + шаг цены. - Вход в шорт — Sell Stop ставится на
low предыдущей свечи - минимальная дистанция - шаг цены. - Взаимоисключение — после исполнения одного ордера противоположный немедленно отменяется.
- Стоп-лосс — для лонга уровень
low предыдущей свечи - StopLossPips, для шортаhigh + StopLossPips(после перевода в цену). - Тейк-профит — лонг закрывается Sell Limit по
entry + TakeProfitPips, шорт — Buy Limit поentry - TakeProfitPips. - Трейлинг-стоп — при прибыли больше
TrailingStopPipsстоп сдвигается, сохраняя ту же дистанцию от актуального bid/ask. - Отмена через 2 часа — если до
SetHour + 2стопы не активировались, они снимаются.
Параметры
| Имя | Назначение | Значение по умолчанию |
|---|---|---|
Volume |
Объём сделок для входных ордеров. | 0.1 |
SetHour |
Час терминала (0–23), когда строится конструкция пробоя. | 15 |
TakeProfitPips |
Дистанция тейк-профита в пунктах; 0 отключает тейк. |
500 |
StopLossPips |
Дистанция стоп-лосса в пунктах; 0 отключает стартовый стоп. |
30 |
TrailingStopPips |
Размер трейлинг-стопа в пунктах; 0 — без трейлинга. |
30 |
MinStopDistancePips |
Минимальная дистанция до стопов от брокера. | 0 |
CandleType |
Тип свечей для расчёта, по умолчанию часовые. | 1h |
Рекомендации по применению
- Нужны данные уровня 1, чтобы получать текущие bid/ask для трейлинг-стопа и корректных расчётов дистанций.
- Для инструментов с нестандартным шагом цены (например, кроссы JPY) корректируйте значения
TakeProfitPips,StopLossPipsиTrailingStopPips. - Если стоп или тейк отключены (значение
0), соответствующий ордер не выставляется, но трейлинг при включении всё равно будет работать. - Убедитесь, что час
SetHourсоответствует часовому поясу торгового сервера, иначе конструкция может быть построена не в то время.
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>
/// Breakout straddle that mirrors the "SMC MaxMin" MetaTrader expert.
/// Places stop orders around the previous bar's extremes at a chosen hour
/// and manages protective stop and take-profit levels with trailing updates.
/// </summary>
public class SmcHiloMaxMinStrategy : Strategy
{
private readonly StrategyParam<int> _setHour;
private readonly StrategyParam<decimal> _takeProfitPips;
private readonly StrategyParam<decimal> _stopLossPips;
private readonly StrategyParam<decimal> _trailingStopPips;
private readonly StrategyParam<decimal> _minStopDistancePips;
private readonly StrategyParam<DataType> _candleType;
private ICandleMessage _previousCandle;
private DateTime? _lastSetupDate;
private Order _buyStopOrder;
private Order _sellStopOrder;
private Order _longStopOrder;
private Order _longTakeProfitOrder;
private Order _shortStopOrder;
private Order _shortTakeProfitOrder;
private decimal? _bestBid;
private decimal? _bestAsk;
private decimal? _longEntryPrice;
private decimal? _shortEntryPrice;
private decimal? _longStopPrice;
private decimal? _shortStopPrice;
private decimal? _longTargetPrice;
private decimal? _shortTargetPrice;
private decimal? _pendingLongStop;
private decimal? _pendingShortStop;
private decimal? _pendingLongTarget;
private decimal? _pendingShortTarget;
private decimal _pipSize;
/// <summary>
/// Terminal hour when the breakout straddle is placed.
/// </summary>
public int SetHour
{
get => _setHour.Value;
set => _setHour.Value = value;
}
/// <summary>
/// Take-profit distance expressed in pips.
/// </summary>
public decimal TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Stop-loss distance expressed in pips.
/// </summary>
public decimal StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Trailing-stop distance expressed in pips.
/// </summary>
public decimal TrailingStopPips
{
get => _trailingStopPips.Value;
set => _trailingStopPips.Value = value;
}
/// <summary>
/// Minimum broker stop distance in pips.
/// </summary>
public decimal MinStopDistancePips
{
get => _minStopDistancePips.Value;
set => _minStopDistancePips.Value = value;
}
/// <summary>
/// Candle type used to evaluate the hourly session.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initialize strategy parameters with sensible defaults.
/// </summary>
public SmcHiloMaxMinStrategy()
{
_setHour = Param(nameof(SetHour), 15)
.SetDisplay("Trigger Hour", "Terminal hour when pending orders are created", "Timing");
_takeProfitPips = Param(nameof(TakeProfitPips), 500m)
.SetNotNegative()
.SetDisplay("Take Profit (pips)", "Distance from entry to the profit target", "Risk");
_stopLossPips = Param(nameof(StopLossPips), 30m)
.SetNotNegative()
.SetDisplay("Stop Loss (pips)", "Distance from entry to the protective stop", "Risk");
_trailingStopPips = Param(nameof(TrailingStopPips), 30m)
.SetNotNegative()
.SetDisplay("Trailing Stop (pips)", "Trailing stop distance that replaces the static stop", "Risk");
_minStopDistancePips = Param(nameof(MinStopDistancePips), 0m)
.SetNotNegative()
.SetDisplay("Min Stop Distance (pips)", "Broker minimum stop distance, used to pad breakout levels", "Timing");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Candles that define the hourly breakout window", "Timing");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_previousCandle = null;
_lastSetupDate = null;
_buyStopOrder = null;
_sellStopOrder = null;
_longStopOrder = null;
_longTakeProfitOrder = null;
_shortStopOrder = null;
_shortTakeProfitOrder = null;
_bestBid = null;
_bestAsk = null;
_longEntryPrice = null;
_shortEntryPrice = null;
_longStopPrice = null;
_shortStopPrice = null;
_longTargetPrice = null;
_shortTargetPrice = null;
_pendingLongStop = null;
_pendingShortStop = null;
_pendingLongTarget = null;
_pendingShortTarget = null;
_pipSize = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
UpdatePipSize();
var candleSubscription = SubscribeCandles(CandleType);
candleSubscription
.Bind(ProcessCandle)
.Start();
SubscribeLevel1()
.Bind(ProcessLevel1)
.Start();
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
UpdatePipSize();
var hour = candle.OpenTime.Hour;
var currentDate = candle.OpenTime.Date;
if (_previousCandle != null)
{
if (_lastSetupDate != currentDate && hour == NormalizeHour(SetHour))
{
PlaceStraddle(candle.OpenTime);
}
var cancelHour = NormalizeHour(SetHour + 2);
if (_lastSetupDate == currentDate && hour == cancelHour)
{
CancelEntryOrders();
}
}
_previousCandle = candle;
}
private void ProcessLevel1(Level1ChangeMessage message)
{
var bid = message.TryGetDecimal(Level1Fields.BestBidPrice);
var ask = message.TryGetDecimal(Level1Fields.BestAskPrice);
if (bid.HasValue && bid.Value > 0m)
_bestBid = bid.Value;
if (ask.HasValue && ask.Value > 0m)
_bestAsk = ask.Value;
CleanupInactiveOrders();
ManageActivePosition();
}
private void PlaceStraddle(DateTimeOffset triggerTime)
{
if (_previousCandle == null)
return;
if (Volume <= 0m)
return;
if (Position != 0m)
return;
if (IsOrderActive(_buyStopOrder) || IsOrderActive(_sellStopOrder))
return;
var previousHigh = _previousCandle.HighPrice;
var previousLow = _previousCandle.LowPrice;
if (previousHigh <= 0m || previousLow <= 0m)
return;
var priceStep = GetPriceStep();
var minDistance = MinStopDistancePips * _pipSize;
var ask = _bestAsk ?? _previousCandle.ClosePrice;
var bid = _bestBid ?? _previousCandle.ClosePrice;
var longTrigger = previousHigh;
if (minDistance > 0m && ask > 0m)
{
var distance = previousHigh - ask;
if (distance < minDistance)
longTrigger += minDistance - distance;
}
var shortTrigger = previousLow;
if (minDistance > 0m && bid > 0m)
{
var distance = bid - previousLow;
if (distance < minDistance)
shortTrigger -= minDistance - distance;
}
longTrigger = NormalizePrice(longTrigger + priceStep);
shortTrigger = NormalizePrice(shortTrigger - priceStep);
if (longTrigger > 0m)
{
CancelOrderIfActive(ref _buyStopOrder);
_buyStopOrder = BuyMarket(Volume);
_pendingLongStop = CalculateLongStopPrice();
_pendingLongTarget = CalculateLongTargetPrice(longTrigger);
}
if (shortTrigger > 0m)
{
CancelOrderIfActive(ref _sellStopOrder);
_sellStopOrder = SellMarket(Volume);
_pendingShortStop = CalculateShortStopPrice();
_pendingShortTarget = CalculateShortTargetPrice(shortTrigger);
}
if (_buyStopOrder != null || _sellStopOrder != null)
_lastSetupDate = triggerTime.Date;
}
private decimal? CalculateLongStopPrice()
{
if (_previousCandle == null)
return null;
var distance = StopLossPips * _pipSize;
if (distance <= 0m)
return null;
var stop = _previousCandle.LowPrice - distance;
return stop > 0m ? NormalizePrice(stop) : (decimal?)null;
}
private decimal? CalculateShortStopPrice()
{
if (_previousCandle == null)
return null;
var distance = StopLossPips * _pipSize;
if (distance <= 0m)
return null;
var stop = _previousCandle.HighPrice + distance;
return stop > 0m ? NormalizePrice(stop) : (decimal?)null;
}
private decimal? CalculateLongTargetPrice(decimal entryPrice)
{
var distance = TakeProfitPips * _pipSize;
if (distance <= 0m)
return null;
var target = entryPrice + distance;
return target > 0m ? NormalizePrice(target) : (decimal?)null;
}
private decimal? CalculateShortTargetPrice(decimal entryPrice)
{
var distance = TakeProfitPips * _pipSize;
if (distance <= 0m)
return null;
var target = entryPrice - distance;
return target > 0m ? NormalizePrice(target) : (decimal?)null;
}
private void ManageActivePosition()
{
if (Position > 0m)
{
EnsureLongProtection();
UpdateLongTrailing();
}
else if (Position < 0m)
{
EnsureShortProtection();
UpdateShortTrailing();
}
}
private void EnsureLongProtection()
{
var volume = Math.Abs(Position);
if (volume <= 0m)
return;
if (_longStopPrice is decimal stop && stop > 0m)
{
var normalized = NormalizePrice(stop);
if (_longStopOrder == null || !ArePricesEqual(_longStopOrder.Price, normalized))
{
CancelOrderIfActive(ref _longStopOrder);
_longStopOrder = SellMarket(volume);
}
}
else
{
CancelOrderIfActive(ref _longStopOrder);
}
if (_longTargetPrice is decimal target && target > 0m)
{
var normalized = NormalizePrice(target);
if (_longTakeProfitOrder == null || !ArePricesEqual(_longTakeProfitOrder.Price, normalized))
{
CancelOrderIfActive(ref _longTakeProfitOrder);
_longTakeProfitOrder = SellMarket(volume);
}
}
else
{
CancelOrderIfActive(ref _longTakeProfitOrder);
}
}
private void EnsureShortProtection()
{
var volume = Math.Abs(Position);
if (volume <= 0m)
return;
if (_shortStopPrice is decimal stop && stop > 0m)
{
var normalized = NormalizePrice(stop);
if (_shortStopOrder == null || !ArePricesEqual(_shortStopOrder.Price, normalized))
{
CancelOrderIfActive(ref _shortStopOrder);
_shortStopOrder = BuyMarket(volume);
}
}
else
{
CancelOrderIfActive(ref _shortStopOrder);
}
if (_shortTargetPrice is decimal target && target > 0m)
{
var normalized = NormalizePrice(target);
if (_shortTakeProfitOrder == null || !ArePricesEqual(_shortTakeProfitOrder.Price, normalized))
{
CancelOrderIfActive(ref _shortTakeProfitOrder);
_shortTakeProfitOrder = BuyMarket(volume);
}
}
else
{
CancelOrderIfActive(ref _shortTakeProfitOrder);
}
}
private void UpdateLongTrailing()
{
if (TrailingStopPips <= 0m)
return;
if (_longEntryPrice is not decimal entry)
return;
var bid = _bestBid ?? 0m;
if (bid <= 0m)
return;
var distance = TrailingStopPips * _pipSize;
if (distance <= 0m)
return;
var profit = bid - entry;
if (profit <= distance)
return;
var newStop = NormalizePrice(bid - distance);
if (_longStopPrice is decimal existing && !IsGreaterThan(newStop, existing))
return;
_longStopPrice = newStop;
EnsureLongProtection();
}
private void UpdateShortTrailing()
{
if (TrailingStopPips <= 0m)
return;
if (_shortEntryPrice is not decimal entry)
return;
var ask = _bestAsk ?? 0m;
if (ask <= 0m)
return;
var distance = TrailingStopPips * _pipSize;
if (distance <= 0m)
return;
var profit = entry - ask;
if (profit <= distance)
return;
var newStop = NormalizePrice(ask + distance);
if (_shortStopPrice is decimal existing && !IsLessThan(newStop, existing))
return;
_shortStopPrice = newStop;
EnsureShortProtection();
}
/// <inheritdoc />
protected override void OnOwnTradeReceived(MyTrade trade)
{
base.OnOwnTradeReceived(trade);
if (trade.Order.Security != Security)
return;
var tradeVolume = trade.Trade.Volume;
if (tradeVolume <= 0m)
return;
var signedDelta = trade.Order.Side == Sides.Buy ? tradeVolume : -tradeVolume;
var currentPosition = Position;
var previousPosition = currentPosition - signedDelta;
if (currentPosition > 0m && trade.Order.Side == Sides.Buy)
{
UpdateLongEntry(previousPosition, trade.Trade.Price, tradeVolume);
}
else if (currentPosition < 0m && trade.Order.Side == Sides.Sell)
{
UpdateShortEntry(previousPosition, trade.Trade.Price, tradeVolume);
}
else
{
if (previousPosition > 0m && currentPosition <= 0m)
ResetLongState();
if (previousPosition < 0m && currentPosition >= 0m)
ResetShortState();
}
if (trade.Order == _buyStopOrder)
{
_buyStopOrder = null;
CancelOrderIfActive(ref _sellStopOrder);
}
else if (trade.Order == _sellStopOrder)
{
_sellStopOrder = null;
CancelOrderIfActive(ref _buyStopOrder);
}
if (trade.Order == _longStopOrder || trade.Order == _longTakeProfitOrder)
{
ResetLongState();
}
if (trade.Order == _shortStopOrder || trade.Order == _shortTakeProfitOrder)
{
ResetShortState();
}
}
/// <inheritdoc />
protected override void OnPositionReceived(Position position)
{
base.OnPositionReceived(position);
if (Position == 0m)
{
ResetLongState();
ResetShortState();
}
}
private void UpdateLongEntry(decimal previousPosition, decimal price, decimal tradeVolume)
{
var positionBefore = Math.Abs(previousPosition);
var currentPosition = Math.Abs(Position);
if (positionBefore <= 0m)
{
_longEntryPrice = price;
}
else if (_longEntryPrice is decimal existing)
{
_longEntryPrice = (existing * positionBefore + price * tradeVolume) / currentPosition;
}
else
{
_longEntryPrice = price;
}
_longStopPrice = _pendingLongStop;
_longTargetPrice = _pendingLongTarget;
EnsureLongProtection();
}
private void UpdateShortEntry(decimal previousPosition, decimal price, decimal tradeVolume)
{
var positionBefore = Math.Abs(previousPosition);
var currentPosition = Math.Abs(Position);
if (positionBefore <= 0m)
{
_shortEntryPrice = price;
}
else if (_shortEntryPrice is decimal existing)
{
_shortEntryPrice = (existing * positionBefore + price * tradeVolume) / currentPosition;
}
else
{
_shortEntryPrice = price;
}
_shortStopPrice = _pendingShortStop;
_shortTargetPrice = _pendingShortTarget;
EnsureShortProtection();
}
private void ResetLongState()
{
CancelOrderIfActive(ref _longStopOrder);
CancelOrderIfActive(ref _longTakeProfitOrder);
_longEntryPrice = null;
_longStopPrice = null;
_longTargetPrice = null;
_pendingLongStop = null;
_pendingLongTarget = null;
}
private void ResetShortState()
{
CancelOrderIfActive(ref _shortStopOrder);
CancelOrderIfActive(ref _shortTakeProfitOrder);
_shortEntryPrice = null;
_shortStopPrice = null;
_shortTargetPrice = null;
_pendingShortStop = null;
_pendingShortTarget = null;
}
private void CancelEntryOrders()
{
CancelOrderIfActive(ref _buyStopOrder);
CancelOrderIfActive(ref _sellStopOrder);
}
private void CleanupInactiveOrders()
{
CleanupOrder(ref _buyStopOrder);
CleanupOrder(ref _sellStopOrder);
CleanupOrder(ref _longStopOrder);
CleanupOrder(ref _longTakeProfitOrder);
CleanupOrder(ref _shortStopOrder);
CleanupOrder(ref _shortTakeProfitOrder);
}
private void CleanupOrder(ref Order order)
{
if (order == null)
return;
if (!IsOrderActive(order))
order = null;
}
private void CancelOrderIfActive(ref Order order)
{
if (order == null)
return;
if (IsOrderActive(order))
CancelOrder(order);
order = null;
}
private static bool IsOrderActive(Order order)
{
return order != null && order.State == OrderStates.Active;
}
private int NormalizeHour(int hour)
{
if (hour < 0)
hour = 0;
return ((hour % 24) + 24) % 24;
}
private void UpdatePipSize()
{
var step = Security?.PriceStep ?? 0m;
if (step <= 0m)
return;
var digits = GetDecimalDigits(step);
_pipSize = (digits == 3 || digits == 5) ? step * 10m : step;
}
private decimal GetPriceStep()
{
var step = Security?.PriceStep ?? 0m;
if (step > 0m)
{
return step;
}
if (_pipSize > 0m)
{
return _pipSize;
}
return 0.0001m;
}
private decimal NormalizePrice(decimal price)
{
if (price <= 0m)
{
return price;
}
var step = Security?.PriceStep;
if (step == null || step.Value <= 0m)
{
return price;
}
var steps = Math.Round(price / step.Value, MidpointRounding.AwayFromZero);
return steps * step.Value;
}
private static int GetDecimalDigits(decimal value)
{
value = Math.Abs(value);
var digits = 0;
while (value != Math.Truncate(value) && digits < 10)
{
value *= 10m;
digits++;
}
return digits;
}
private bool ArePricesEqual(decimal first, decimal second)
{
var step = GetPriceStep();
if (step <= 0m)
{
step = 0.0000001m;
}
return Math.Abs(first - second) <= step / 2m;
}
private bool IsGreaterThan(decimal candidate, decimal reference)
{
var step = GetPriceStep();
return candidate > reference + step / 2m;
}
private bool IsLessThan(decimal candidate, decimal reference)
{
var step = GetPriceStep();
return candidate < reference - step / 2m;
}
}
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.Strategies import Strategy
class smc_hilo_max_min_strategy(Strategy):
"""Breakout straddle strategy. At a specified hour, uses previous candle high/low
as breakout levels. Enters long on upside breakout, short on downside breakout.
Manages stop-loss, take-profit, and trailing stop on candle close checks."""
def __init__(self):
super(smc_hilo_max_min_strategy, self).__init__()
self._set_hour = self.Param("SetHour", 15) \
.SetDisplay("Trigger Hour", "Terminal hour when breakout levels are set", "Timing")
self._take_profit_pips = self.Param("TakeProfitPips", 500.0) \
.SetDisplay("Take Profit (pips)", "Distance from entry to the profit target", "Risk")
self._stop_loss_pips = self.Param("StopLossPips", 30.0) \
.SetDisplay("Stop Loss (pips)", "Distance from entry to the protective stop", "Risk")
self._trailing_stop_pips = self.Param("TrailingStopPips", 30.0) \
.SetDisplay("Trailing Stop (pips)", "Trailing stop distance", "Risk")
self._min_stop_distance_pips = self.Param("MinStopDistancePips", 0.0) \
.SetDisplay("Min Stop Distance (pips)", "Broker minimum stop distance for level padding", "Timing")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Candle Type", "Candles that define the hourly breakout window", "Timing")
self._previous_high = 0.0
self._previous_low = 0.0
self._previous_close = 0.0
self._has_previous = False
self._last_setup_date = None
self._buy_level = 0.0
self._sell_level = 0.0
self._levels_ready = False
self._entry_price = 0.0
self._stop_price = None
self._target_price = None
self._pip_size = 0.0
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def SetHour(self):
return self._set_hour.Value
@property
def TakeProfitPips(self):
return self._take_profit_pips.Value
@property
def StopLossPips(self):
return self._stop_loss_pips.Value
@property
def TrailingStopPips(self):
return self._trailing_stop_pips.Value
@property
def MinStopDistancePips(self):
return self._min_stop_distance_pips.Value
def OnReseted(self):
super(smc_hilo_max_min_strategy, self).OnReseted()
self._previous_high = 0.0
self._previous_low = 0.0
self._previous_close = 0.0
self._has_previous = False
self._last_setup_date = None
self._buy_level = 0.0
self._sell_level = 0.0
self._levels_ready = False
self._entry_price = 0.0
self._stop_price = None
self._target_price = None
self._pip_size = 0.0
def _update_pip_size(self):
step = self.Security.PriceStep if self.Security is not None else 0.0
if step is None or float(step) <= 0:
return
step_val = float(step)
digits = self._get_decimal_digits(step_val)
if digits == 3 or digits == 5:
self._pip_size = step_val * 10.0
else:
self._pip_size = step_val
def _get_decimal_digits(self, value):
value = abs(value)
digits = 0
while value != int(value) and digits < 10:
value *= 10.0
digits += 1
return digits
def _to_price(self, pips):
return float(pips) * self._pip_size
def OnStarted2(self, time):
super(smc_hilo_max_min_strategy, self).OnStarted2(time)
self._update_pip_size()
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._process_candle).Start()
def _normalize_hour(self, hour):
if hour < 0:
hour = 0
return ((hour % 24) + 24) % 24
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
self._update_pip_size()
hour = candle.OpenTime.Hour
current_date = candle.OpenTime.Date
high = float(candle.HighPrice)
low = float(candle.LowPrice)
close = float(candle.ClosePrice)
# At the set hour, establish breakout levels from previous candle
if self._has_previous:
set_hour = self._normalize_hour(self.SetHour)
if self._last_setup_date != current_date and hour == set_hour:
self._setup_levels(close, current_date)
# Cancel levels 2 hours after setup hour
cancel_hour = self._normalize_hour(self.SetHour + 2)
if self._last_setup_date == current_date and hour == cancel_hour:
self._levels_ready = False
# Manage existing position: check SL/TP/trailing
if self.Position != 0:
self._manage_position(close, high, low)
# Try to enter on breakout
if self._levels_ready and self.Position == 0:
if close >= self._buy_level and self._buy_level > 0:
self.BuyMarket()
self._entry_price = close
self._set_long_protection(close)
self._levels_ready = False
elif close <= self._sell_level and self._sell_level > 0:
self.SellMarket()
self._entry_price = close
self._set_short_protection(close)
self._levels_ready = False
# Store for next candle
self._previous_high = high
self._previous_low = low
self._previous_close = close
self._has_previous = True
def _setup_levels(self, current_close, current_date):
if self.Position != 0:
return
prev_high = self._previous_high
prev_low = self._previous_low
if prev_high <= 0 or prev_low <= 0:
return
min_distance = self._to_price(self.MinStopDistancePips)
step = self.Security.PriceStep if self.Security is not None else 0.0
if step is None or float(step) <= 0:
step = 0.0001
step_val = float(step)
buy_trigger = prev_high
if min_distance > 0:
distance = prev_high - current_close
if distance < min_distance:
buy_trigger += min_distance - distance
sell_trigger = prev_low
if min_distance > 0:
distance = current_close - prev_low
if distance < min_distance:
sell_trigger -= min_distance - distance
self._buy_level = buy_trigger + step_val
self._sell_level = sell_trigger - step_val
self._levels_ready = True
self._last_setup_date = current_date
def _set_long_protection(self, entry_price):
sl_dist = self._to_price(self.StopLossPips)
tp_dist = self._to_price(self.TakeProfitPips)
if sl_dist > 0:
self._stop_price = self._previous_low - sl_dist
else:
self._stop_price = None
if tp_dist > 0:
self._target_price = entry_price + tp_dist
else:
self._target_price = None
def _set_short_protection(self, entry_price):
sl_dist = self._to_price(self.StopLossPips)
tp_dist = self._to_price(self.TakeProfitPips)
if sl_dist > 0:
self._stop_price = self._previous_high + sl_dist
else:
self._stop_price = None
if tp_dist > 0:
self._target_price = entry_price - tp_dist
else:
self._target_price = None
def _manage_position(self, close, high, low):
if self.Position > 0:
# Check stop loss
if self._stop_price is not None and low <= self._stop_price:
self.SellMarket()
self._reset_position()
return
# Check take profit
if self._target_price is not None and high >= self._target_price:
self.SellMarket()
self._reset_position()
return
# Trailing stop
self._update_long_trailing(close)
elif self.Position < 0:
# Check stop loss
if self._stop_price is not None and high >= self._stop_price:
self.BuyMarket()
self._reset_position()
return
# Check take profit
if self._target_price is not None and low <= self._target_price:
self.BuyMarket()
self._reset_position()
return
# Trailing stop
self._update_short_trailing(close)
def _update_long_trailing(self, close):
trailing_pips = float(self.TrailingStopPips)
if trailing_pips <= 0:
return
if self._entry_price <= 0:
return
distance = self._to_price(trailing_pips)
if distance <= 0:
return
profit = close - self._entry_price
if profit <= distance:
return
new_stop = close - distance
if self._stop_price is None or new_stop > self._stop_price:
self._stop_price = new_stop
def _update_short_trailing(self, close):
trailing_pips = float(self.TrailingStopPips)
if trailing_pips <= 0:
return
if self._entry_price <= 0:
return
distance = self._to_price(trailing_pips)
if distance <= 0:
return
profit = self._entry_price - close
if profit <= distance:
return
new_stop = close + distance
if self._stop_price is None or new_stop < self._stop_price:
self._stop_price = new_stop
def _reset_position(self):
self._entry_price = 0.0
self._stop_price = None
self._target_price = None
def CreateClone(self):
return smc_hilo_max_min_strategy()