SMC Hilo MaxMin Breakout-Strategie
Überblick
Diese Strategie reproduziert das Verhalten des MetaTrader-Experten SMC MaxMin bei 1200. Zur angegebenen Endzeit platziert es a Eine Buy-Stop-Order über dem Hoch der vorherigen Kerze und eine Sell-Stop-Order unter dem Tief der vorherigen Kerze. Ausstehende Bestellungen werden aufgefüllt durch den Mindeststoppabstand des Brokers, umgerechnet von Pips in Instrumentenpreiseinheiten. Bei einem Ausbruch erfolgt die umgekehrte Reihenfolge wird aufgehoben und die offene Position wird durch feste Stops, Gewinnziele und einen optionalen Trailing Stop verwaltet.
Hauptunterschiede zum ursprünglichen MQL4-Code:
- StockSharp-Bestellgrundelemente (
BuyStop,SellStop,BuyLimit,SellLimit) ersetzen die direktenOrderSend-Aufrufe. - Die Mindest-Stopp-Distanz, Stop-Loss- und Take-Profit-Eingaben werden in Pips ausgedrückt und über
Security.PriceStepin umgerechnet Beachten Sie die tatsächliche Tick-Größe des Instruments. - Das Trailing-Stop-Management verschiebt die Stop-Order nur, wenn eine profitable Distanz erreicht wird, die größer als der Trailing-Puffer ist.
- Die gesamte Logik wird durch das High-Level-Kerzenabonnement API gesteuert, sodass keine direkten Verlaufsscans oder manuellen Indikatorpuffer verwendet werden.
Handelsregeln
- Einrichtungsstunde – wenn die Endstunde
SetHourbeträgt, verwenden Sie die zuvor abgeschlossene Kerze als Referenz. - Langer Einstieg – Setzen Sie einen Kaufstopp bei
previous_high + min_stop_distance + price_step. - Short-Einstieg – setzen Sie einen Verkaufsstopp bei
previous_low - min_stop_distance - price_step. - Gegenseitige Ausschließlichkeit – wenn einer der Stopps erfüllt ist, wird die entgegengesetzte ausstehende Order sofort storniert.
- Stop-Loss – der Long-Stop ist
previous_low - StopLossPips, der Short-Stop istprevious_high + StopLossPips(beide umgerechnet). zu Preiseinheiten). - Take-Profit – Long-Positionen verwenden ein Verkaufslimit bei
entry + TakeProfitPips; Short-Positionen nutzen ein Kauflimit beientry - TakeProfitPips. - Trailing Stop – wenn eine Position einen Gewinn von mehr als
TrailingStopPipshat, wird der Stop nachgezogen, um den gleichen Pip beizubehalten Abstand vom aktuellen Geld-/Briefkurs. - Auftragszeitlimit – zwei Stunden nach der Einrichtung (
SetHour + 2) werden alle nicht gefüllten ausstehenden Stopps storniert.
Parameter
| Name | Beschreibung | Standard |
|---|---|---|
Volume |
Für beide Einstiegsaufträge verwendetes Auftragsvolumen. | 0.1 |
SetHour |
Endstunde (0–23), zu der der Breakout-Straddle erstellt wird. | 15 |
TakeProfitPips |
Gewinnzielentfernung in Pips. Auf 0 setzen, um Take-Profit-Bestellungen zu deaktivieren. |
500 |
StopLossPips |
Schutzstoppabstand in Pips. Auf 0 setzen, um den anfänglichen Stopp zu deaktivieren. |
30 |
TrailingStopPips |
Distanz für den Trailing Stop in Pips. Auf 0 setzen, um einen statischen Stopp beizubehalten. |
30 |
MinStopDistancePips |
Mindeststoppabstand des Brokers, der zur Aufstockung der Einstiegspreise verwendet wird. | 0 |
CandleType |
Der Kerzentyp, der die stündliche Sitzung definiert, ist standardmäßig auf einen Zeitrahmen von 1 Stunde eingestellt. | 1h |
Nutzungshinweise
- Die Strategie erfordert Level-1-Daten, um Trailing-Stops zu verwalten und die neuesten Geld-/Briefkurse für Distanzberechnungen beizubehalten.
- Wenn das zugrunde liegende Instrument nicht standardmäßige Tick-Größen aufweist (z. B. JPY-Kreuzungen mit 0,01 Pip), passen Sie
TakeProfitPipsan.StopLossPipsundTrailingStopPipsentsprechend. - Wenn
TakeProfitPipsoderStopLossPipsNull ist, werden die entsprechenden Aufträge nicht übermittelt, aber Trailing Stops können dennoch aktiviert werden, wenn Der nachgestellte Parameter ist positiv. - Stellen Sie sicher, dass der konfigurierte
SetHourmit der Broker-Serverzeit des eingehenden Datenfeeds übereinstimmt.
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()