Estratégia de ruptura SMC Hilo MaxMin
Visão geral
Esta estratégia reproduz o comportamento do especialista MetaTrader SMC MaxMin em 1200. Na hora terminal especificada, ele coloca um ordem de compra stop acima da máxima da vela anterior e uma ordem de venda abaixo da mínima da vela anterior. Pedidos pendentes são preenchidos pela distância mínima de stop da corretora, convertida de pips em unidades de preço do instrumento. Quando ocorre um rompimento, a ordem oposta é cancelado e a posição aberta é gerenciada através de paradas fixas, metas de lucro e um trailing stop opcional.
Principais diferenças em relação ao código MQL4 original:
- As primitivas de ordem StockSharp (
BuyStop,SellStop,BuyLimit,SellLimit) substituem as chamadas diretasOrderSend. - As entradas mínimas de distância de stop, stop-loss e take-profit são expressas em pips e convertidas por meio de
Security.PriceSteppara respeite o tamanho real do tick do instrumento. - O gerenciamento de trailing stop move a ordem de stop somente quando uma distância lucrativa maior que o trailing buffer é alcançada.
- Toda a lógica é conduzida pela assinatura de vela de alto nível API, portanto, nenhuma varredura direta de histórico ou buffers de indicadores manuais são usados.
Regras de negociação
- Hora de configuração – quando a hora do terminal for igual a
SetHour, use a vela concluída anteriormente como referência. - Entrada longa – coloque um stop de compra em
previous_high + min_stop_distance + price_step. - Entrada curta – coloque um sell-stop em
previous_low - min_stop_distance - price_step. - Exclusividade mútua – se um dos stop for preenchido, a ordem pendente oposta será cancelada imediatamente.
- Stop-loss – o stop longo é
previous_low - StopLossPips, o stop curto éprevious_high + StopLossPips(ambos convertidos para unidades de preço). - Take-profit – posições longas usam um limite de venda em
entry + TakeProfitPips; posições curtas usam um limite de compra ementry - TakeProfitPips. - Trailing stop – quando uma posição tem lucro superior a
TrailingStopPips, o stop é seguido para manter o mesmo pip distância do lance/pedido atual. - Tempo limite do pedido – duas horas após a configuração (
SetHour + 2), todas as paradas pendentes não preenchidas serão canceladas.
Parâmetros
| Nome | Descrição | Padrão |
|---|---|---|
Volume |
Volume de pedidos utilizado para ambos os pedidos de entrada. | 0.1 |
SetHour |
Hora terminal (0–23) quando o straddle de breakout é criado. | 15 |
TakeProfitPips |
Distância alvo de lucro em pips. Defina como 0 para desativar ordens de realização de lucro. |
500 |
StopLossPips |
Distância de parada protetora em pips. Defina como 0 para desativar a parada inicial. |
30 |
TrailingStopPips |
Distância para o trailing stop em pips. Defina como 0 para manter uma parada estática. |
30 |
MinStopDistancePips |
Distância mínima de parada do corretor usada para aumentar os preços de entrada. | 0 |
CandleType |
Tipo de vela que define a sessão de hora em hora, o padrão é o período de 1 hora. | 1h |
Notas de uso
- A estratégia requer dados de nível 1 para gerenciar os trailing stops e manter os preços de compra/venda mais recentes para cálculos de distância.
- Se o instrumento subjacente tiver tamanhos de ticks fora do padrão (por exemplo, JPY cruza com 0,01 pip), ajuste
TakeProfitPips,StopLossPipseTrailingStopPipsrespectivamente. - Quando
TakeProfitPipsouStopLossPipsfor zero, as respectivas ordens não serão enviadas, mas os trailing stops ainda poderão ser ativados se o parâmetro final é positivo. - Certifique-se de que o
SetHourconfigurado corresponda ao horário do servidor intermediário do feed de dados de entrada.
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()