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>
/// Hans123 breakout strategy converted from MQL5.
/// Collects an intraday range and trades pending stop orders within a trading window.
/// Applies configurable stop-loss, take-profit, and trailing protection.
/// </summary>
public class Hans123TraderStrategy : Strategy
{
private readonly StrategyParam<decimal> _orderVolume;
private readonly StrategyParam<int> _rangeLength;
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> _endHour;
private readonly StrategyParam<DataType> _candleType;
private Highest _highest = null!;
private Lowest _lowest = null!;
private decimal _entryPrice;
private decimal _pipSize;
private decimal _highestSinceEntry;
private decimal _lowestSinceEntry;
/// <summary>
/// Volume used for breakout orders.
/// </summary>
public decimal OrderVolume
{
get => _orderVolume.Value;
set => _orderVolume.Value = value;
}
/// <summary>
/// Number of candles that form the breakout range.
/// </summary>
public int RangeLength
{
get => _rangeLength.Value;
set => _rangeLength.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 in pips.
/// </summary>
public int TrailingStopPips
{
get => _trailingStopPips.Value;
set => _trailingStopPips.Value = value;
}
/// <summary>
/// Extra move (in pips) before trailing activates again.
/// </summary>
public int TrailingStepPips
{
get => _trailingStepPips.Value;
set => _trailingStepPips.Value = value;
}
/// <summary>
/// Start hour (inclusive) of the trading window.
/// </summary>
public int StartHour
{
get => _startHour.Value;
set => _startHour.Value = value;
}
/// <summary>
/// End hour (exclusive) of the trading window.
/// </summary>
public int EndHour
{
get => _endHour.Value;
set => _endHour.Value = value;
}
/// <summary>
/// Candle type used for calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="Hans123TraderStrategy"/> class.
/// </summary>
public Hans123TraderStrategy()
{
_orderVolume = Param(nameof(OrderVolume), 0.1m)
.SetDisplay("Order Volume", "Breakout order volume", "General")
.SetOptimize(0.1m, 2m, 0.1m);
_rangeLength = Param(nameof(RangeLength), 40)
.SetGreaterThanZero()
.SetDisplay("Range Length", "Candles in breakout range", "General")
.SetOptimize(40, 120, 10);
_stopLossPips = Param(nameof(StopLossPips), 50)
.SetDisplay("Stop Loss (pips)", "Stop-loss distance in pips", "Risk Management")
.SetOptimize(0, 150, 10);
_takeProfitPips = Param(nameof(TakeProfitPips), 50)
.SetDisplay("Take Profit (pips)", "Take-profit distance in pips", "Risk Management")
.SetOptimize(0, 200, 10);
_trailingStopPips = Param(nameof(TrailingStopPips), 10)
.SetDisplay("Trailing Stop (pips)", "Trailing stop distance in pips", "Risk Management")
.SetOptimize(0, 100, 5);
_trailingStepPips = Param(nameof(TrailingStepPips), 5)
.SetDisplay("Trailing Step (pips)", "Extra pips before trailing updates", "Risk Management")
.SetOptimize(0, 50, 5);
_startHour = Param(nameof(StartHour), 0)
.SetDisplay("Start Hour", "Hour (UTC) when orders can be placed", "Schedule")
.SetOptimize(0, 23, 1);
_endHour = Param(nameof(EndHour), 24)
.SetDisplay("End Hour", "Hour (UTC) when orders stop", "Schedule")
.SetOptimize(1, 24, 1);
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(3).TimeFrame())
.SetDisplay("Candle Type", "Working candle timeframe", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_highest = null;
_lowest = null;
_entryPrice = 0m;
_pipSize = 0m;
_highestSinceEntry = 0m;
_lowestSinceEntry = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_pipSize = CalculatePipSize();
_highest = new Highest { Length = RangeLength };
_lowest = new Lowest { Length = RangeLength };
var subscription = SubscribeCandles(CandleType);
subscription.Bind(_highest, _lowest, ProcessCandle).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _highest);
DrawIndicator(area, _lowest);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal highest, decimal lowest)
{
if (candle.State != CandleStates.Finished)
return;
// Check protective levels
CheckProtection(candle);
if (!_highest.IsFormed || !_lowest.IsFormed)
return;
if (!IsWithinTradingWindow(candle.OpenTime))
return;
if (OrderVolume <= 0m || highest <= lowest)
return;
// Track extremes for trailing
if (Position > 0 && candle.HighPrice > _highestSinceEntry)
_highestSinceEntry = candle.HighPrice;
if (Position < 0 && (_lowestSinceEntry == 0 || candle.LowPrice < _lowestSinceEntry))
_lowestSinceEntry = candle.LowPrice;
// Breakout entry logic
if (Position == 0)
{
if (candle.HighPrice >= highest)
{
BuyMarket(OrderVolume);
}
else if (candle.LowPrice <= lowest)
{
SellMarket(OrderVolume);
}
}
}
protected override void OnOwnTradeReceived(MyTrade trade)
{
base.OnOwnTradeReceived(trade);
if (trade?.Trade == null) return;
if (Position != 0m && _entryPrice == 0m)
{
_entryPrice = trade.Trade.Price;
_highestSinceEntry = trade.Trade.Price;
_lowestSinceEntry = trade.Trade.Price;
}
if (Position == 0m)
{
_entryPrice = 0m;
_highestSinceEntry = 0m;
_lowestSinceEntry = 0m;
}
}
private void CheckProtection(ICandleMessage candle)
{
if (Position == 0 || _entryPrice == 0m)
return;
var stopDist = StopLossPips > 0 ? StopLossPips * _pipSize : 0m;
var takeDist = TakeProfitPips > 0 ? TakeProfitPips * _pipSize : 0m;
var trailDist = TrailingStopPips > 0 ? TrailingStopPips * _pipSize : 0m;
var activation = (TrailingStopPips + TrailingStepPips) * _pipSize;
if (Position > 0)
{
// Stop loss
if (stopDist > 0m && candle.LowPrice <= _entryPrice - stopDist)
{
SellMarket(Math.Abs(Position));
return;
}
// Take profit
if (takeDist > 0m && candle.HighPrice >= _entryPrice + takeDist)
{
SellMarket(Math.Abs(Position));
return;
}
// Trailing stop
if (trailDist > 0m && _highestSinceEntry - _entryPrice > activation)
{
var trailStop = _highestSinceEntry - trailDist;
if (candle.LowPrice <= trailStop)
{
SellMarket(Math.Abs(Position));
return;
}
}
}
else if (Position < 0)
{
if (stopDist > 0m && candle.HighPrice >= _entryPrice + stopDist)
{
BuyMarket(Math.Abs(Position));
return;
}
if (takeDist > 0m && candle.LowPrice <= _entryPrice - takeDist)
{
BuyMarket(Math.Abs(Position));
return;
}
if (trailDist > 0m && _lowestSinceEntry > 0m && _entryPrice - _lowestSinceEntry > activation)
{
var trailStop = _lowestSinceEntry + trailDist;
if (candle.HighPrice >= trailStop)
{
BuyMarket(Math.Abs(Position));
return;
}
}
}
}
private decimal CalculatePipSize()
{
var step = Security?.PriceStep ?? 1m;
return step;
}
private bool IsWithinTradingWindow(DateTimeOffset time)
{
return time.Hour >= StartHour && time.Hour < EndHour;
}
}
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 Highest, Lowest
from StockSharp.Algo.Strategies import Strategy
class hans123_trader_strategy(Strategy):
def __init__(self):
super(hans123_trader_strategy, self).__init__()
self._order_volume = self.Param("OrderVolume", 0.1)
self._range_length = self.Param("RangeLength", 40)
self._stop_loss_pips = self.Param("StopLossPips", 50)
self._take_profit_pips = self.Param("TakeProfitPips", 50)
self._trailing_stop_pips = self.Param("TrailingStopPips", 10)
self._trailing_step_pips = self.Param("TrailingStepPips", 5)
self._start_hour = self.Param("StartHour", 0)
self._end_hour = self.Param("EndHour", 24)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(3)))
self._highest = None
self._lowest = None
self._entry_price = 0.0
self._pip_size = 0.0
self._highest_since_entry = 0.0
self._lowest_since_entry = 0.0
@property
def OrderVolume(self):
return self._order_volume.Value
@property
def RangeLength(self):
return self._range_length.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 EndHour(self):
return self._end_hour.Value
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(hans123_trader_strategy, self).OnStarted2(time)
self._pip_size = self._calculate_pip_size()
self._highest = Highest()
self._highest.Length = self.RangeLength
self._lowest = Lowest()
self._lowest.Length = self.RangeLength
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._highest, self._lowest, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, self._highest)
self.DrawIndicator(area, self._lowest)
self.DrawOwnTrades(area)
def OnOwnTradeReceived(self, trade):
super(hans123_trader_strategy, self).OnOwnTradeReceived(trade)
if trade is None or trade.Trade is None:
return
pos = float(self.Position)
if pos != 0 and self._entry_price == 0.0:
self._entry_price = float(trade.Trade.Price)
self._highest_since_entry = float(trade.Trade.Price)
self._lowest_since_entry = float(trade.Trade.Price)
if pos == 0:
self._entry_price = 0.0
self._highest_since_entry = 0.0
self._lowest_since_entry = 0.0
def _process_candle(self, candle, highest, lowest):
if candle.State != CandleStates.Finished:
return
self._check_protection(candle)
if not self._highest.IsFormed or not self._lowest.IsFormed:
return
if not self._is_within_trading_window(candle.OpenTime):
return
highest_val = float(highest)
lowest_val = float(lowest)
if float(self.OrderVolume) <= 0 or highest_val <= lowest_val:
return
pos = float(self.Position)
if pos > 0 and float(candle.HighPrice) > self._highest_since_entry:
self._highest_since_entry = float(candle.HighPrice)
if pos < 0 and (self._lowest_since_entry == 0 or float(candle.LowPrice) < self._lowest_since_entry):
self._lowest_since_entry = float(candle.LowPrice)
if float(self.Position) == 0:
self._entry_price = 0.0
self._highest_since_entry = 0.0
self._lowest_since_entry = 0.0
close = float(candle.ClosePrice)
if float(candle.HighPrice) >= highest_val:
self.BuyMarket(float(self.OrderVolume))
self._entry_price = close
self._highest_since_entry = close
self._lowest_since_entry = close
elif float(candle.LowPrice) <= lowest_val:
self.SellMarket(float(self.OrderVolume))
self._entry_price = close
self._highest_since_entry = close
self._lowest_since_entry = close
def _check_protection(self, candle):
pos = float(self.Position)
if pos == 0 or self._entry_price == 0.0:
return
stop_dist = self.StopLossPips * self._pip_size if self.StopLossPips > 0 else 0.0
take_dist = self.TakeProfitPips * self._pip_size if self.TakeProfitPips > 0 else 0.0
trail_dist = self.TrailingStopPips * self._pip_size if self.TrailingStopPips > 0 else 0.0
activation = (self.TrailingStopPips + self.TrailingStepPips) * self._pip_size
if pos > 0:
if stop_dist > 0 and float(candle.LowPrice) <= self._entry_price - stop_dist:
self.SellMarket(abs(pos))
return
if take_dist > 0 and float(candle.HighPrice) >= self._entry_price + take_dist:
self.SellMarket(abs(pos))
return
if trail_dist > 0 and self._highest_since_entry - self._entry_price > activation:
trail_stop = self._highest_since_entry - trail_dist
if float(candle.LowPrice) <= trail_stop:
self.SellMarket(abs(pos))
return
elif pos < 0:
if stop_dist > 0 and float(candle.HighPrice) >= self._entry_price + stop_dist:
self.BuyMarket(abs(pos))
return
if take_dist > 0 and float(candle.LowPrice) <= self._entry_price - take_dist:
self.BuyMarket(abs(pos))
return
if trail_dist > 0 and self._lowest_since_entry > 0 and self._entry_price - self._lowest_since_entry > activation:
trail_stop = self._lowest_since_entry + trail_dist
if float(candle.HighPrice) >= trail_stop:
self.BuyMarket(abs(pos))
return
def _calculate_pip_size(self):
sec = self.Security
step = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None else 1.0
return step
def _is_within_trading_window(self, time):
return time.Hour >= self.StartHour and time.Hour < self.EndHour
def OnReseted(self):
super(hans123_trader_strategy, self).OnReseted()
self._highest = None
self._lowest = None
self._entry_price = 0.0
self._pip_size = 0.0
self._highest_since_entry = 0.0
self._lowest_since_entry = 0.0
def CreateClone(self):
return hans123_trader_strategy()