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>
/// Range-reversal strategy translated from the Pipso MQL5 expert advisor.
/// The system fades breakouts of the recent high/low range during a configurable trading session.
/// </summary>
public class PipsoStrategy : Strategy
{
private readonly StrategyParam<decimal> _orderVolume;
private readonly StrategyParam<int> _lookbackPeriod;
private readonly StrategyParam<int> _startHour;
private readonly StrategyParam<int> _endHour;
private readonly StrategyParam<decimal> _stopRangePercent;
private readonly StrategyParam<DataType> _candleType;
private Highest _highest = null!;
private Lowest _lowest = null!;
private decimal _previousHighest;
private decimal _previousLowest;
private bool _isChannelInitialized;
private decimal? _entryPrice;
private decimal? _stopPrice;
private Sides? _entrySide;
/// <summary>
/// Trade volume expressed in lots or contracts.
/// </summary>
public decimal OrderVolume
{
get => _orderVolume.Value;
set => _orderVolume.Value = value;
}
/// <summary>
/// Number of candles used to compute the high/low channel.
/// </summary>
public int LookbackPeriod
{
get => _lookbackPeriod.Value;
set => _lookbackPeriod.Value = value;
}
/// <summary>
/// Hour when the strategy is allowed to start trading.
/// </summary>
public int StartHour
{
get => _startHour.Value;
set => _startHour.Value = value;
}
/// <summary>
/// Hour when trading should stop.
/// </summary>
public int EndHour
{
get => _endHour.Value;
set => _endHour.Value = value;
}
/// <summary>
/// Multiplier applied to the channel width to compute the stop distance.
/// </summary>
public decimal StopRangePercent
{
get => _stopRangePercent.Value;
set => _stopRangePercent.Value = value;
}
/// <summary>
/// Candle type used for calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes strategy parameters.
/// </summary>
public PipsoStrategy()
{
_orderVolume = Param(nameof(OrderVolume), 1m)
.SetGreaterThanZero()
.SetDisplay("Volume", "Order volume per trade", "General");
_lookbackPeriod = Param(nameof(LookbackPeriod), 36)
.SetGreaterThanZero()
.SetDisplay("Lookback Period", "Number of candles used for high/low extremes", "Channel");
_startHour = Param(nameof(StartHour), 21)
.SetDisplay("Start Hour", "Session start hour (0-23)", "Session");
_endHour = Param(nameof(EndHour), 9)
.SetDisplay("End Hour", "Session end hour (0-23)", "Session");
_stopRangePercent = Param(nameof(StopRangePercent), 300m)
.SetGreaterThanZero()
.SetDisplay("Stop Range %", "Extra percentage of the channel width for stop distance", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Time frame used for calculations", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_previousHighest = 0m;
_previousLowest = 0m;
_isChannelInitialized = false;
ResetTradeState();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
Volume = OrderVolume;
_highest = new Highest { Length = LookbackPeriod };
_lowest = new Lowest { Length = LookbackPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_highest, _lowest, ProcessCandle)
.Start();
}
private void ProcessCandle(ICandleMessage candle, decimal highestValue, decimal lowestValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!_highest.IsFormed || !_lowest.IsFormed)
{
_previousHighest = highestValue;
_previousLowest = lowestValue;
return;
}
if (!_isChannelInitialized)
{
_previousHighest = highestValue;
_previousLowest = lowestValue;
_isChannelInitialized = true;
return;
}
// Indicators are bound via .Bind, no need for IsFormedAndOnlineAndAllowTrading.
var channelHigh = _previousHighest;
var channelLow = _previousLowest;
ManageStopLoss(candle);
var channelRange = channelHigh - channelLow;
var breakoutHigh = candle.HighPrice >= channelHigh && channelRange > 0m;
var breakoutLow = candle.LowPrice <= channelLow && channelRange > 0m;
var canTrade = IsWithinTradingWindow(candle.OpenTime);
if (breakoutHigh && Position > 0)
{
SellMarket();
ResetTradeState();
}
if (breakoutLow && Position < 0)
{
BuyMarket();
ResetTradeState();
}
if (channelRange > 0m)
{
var stopDistance = channelRange * (1m + StopRangePercent / 100m);
if (breakoutHigh && Position == 0 && canTrade)
{
SellMarket();
_entrySide = Sides.Sell;
_entryPrice = channelHigh;
_stopPrice = _entryPrice + stopDistance;
}
else if (breakoutLow && Position == 0 && canTrade)
{
BuyMarket();
_entrySide = Sides.Buy;
_entryPrice = channelLow;
_stopPrice = _entryPrice - stopDistance;
}
}
if (Position == 0)
ResetTradeState();
_previousHighest = highestValue;
_previousLowest = lowestValue;
}
private void ManageStopLoss(ICandleMessage candle)
{
if (_entrySide is null || _stopPrice is null)
return;
if (_entrySide == Sides.Buy)
{
if (Position <= 0)
{
ResetTradeState();
return;
}
if (candle.LowPrice <= _stopPrice.Value)
{
SellMarket();
ResetTradeState();
}
}
else if (_entrySide == Sides.Sell)
{
if (Position >= 0)
{
ResetTradeState();
return;
}
if (candle.HighPrice >= _stopPrice.Value)
{
BuyMarket();
ResetTradeState();
}
}
}
private bool IsWithinTradingWindow(DateTimeOffset time)
{
var normalizedStart = ((StartHour % 24) + 24) % 24;
var normalizedEnd = ((EndHour % 24) + 24) % 24;
if (normalizedStart == normalizedEnd)
return false;
var start = new TimeSpan(normalizedStart, 0, 0);
var end = new TimeSpan(normalizedEnd, 0, 0);
var current = time.TimeOfDay;
return normalizedStart < normalizedEnd
? current >= start && current <= end
: current >= start || current <= end;
}
private void ResetTradeState()
{
_entryPrice = null;
_stopPrice = null;
_entrySide = null;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Algo.Indicators import Highest, Lowest
class pipso_strategy(Strategy):
"""Range-reversal strategy that fades breakouts of recent high/low range during a session."""
def __init__(self):
super(pipso_strategy, self).__init__()
self._order_volume = self.Param("OrderVolume", 1.0) \
.SetGreaterThanZero() \
.SetDisplay("Volume", "Order volume per trade", "General")
self._lookback = self.Param("LookbackPeriod", 36) \
.SetGreaterThanZero() \
.SetDisplay("Lookback Period", "Number of candles for high/low extremes", "Channel")
self._start_hour = self.Param("StartHour", 21) \
.SetDisplay("Start Hour", "Session start hour (0-23)", "Session")
self._end_hour = self.Param("EndHour", 9) \
.SetDisplay("End Hour", "Session end hour (0-23)", "Session")
self._stop_range_pct = self.Param("StopRangePercent", 300.0) \
.SetGreaterThanZero() \
.SetDisplay("Stop Range %", "Extra percentage of channel width for stop", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Candle Type", "Time frame used for calculations", "General")
self._prev_highest = 0.0
self._prev_lowest = 0.0
self._channel_init = False
self._entry_price = None
self._stop_price = None
self._entry_side = None # 'buy' or 'sell'
@property
def OrderVolume(self):
return self._order_volume.Value
@property
def LookbackPeriod(self):
return self._lookback.Value
@property
def StartHour(self):
return self._start_hour.Value
@property
def EndHour(self):
return self._end_hour.Value
@property
def StopRangePercent(self):
return self._stop_range_pct.Value
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(pipso_strategy, self).OnStarted2(time)
self.Volume = self.OrderVolume
highest = Highest()
highest.Length = self.LookbackPeriod
lowest = Lowest()
lowest.Length = self.LookbackPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(highest, lowest, self.process_candle).Start()
def process_candle(self, candle, highest_val, lowest_val):
if candle.State != CandleStates.Finished:
return
hv = float(highest_val)
lv = float(lowest_val)
if not self._channel_init:
self._prev_highest = hv
self._prev_lowest = lv
self._channel_init = True
return
ch = self._prev_highest
cl = self._prev_lowest
self._manage_stop(candle)
rng = ch - cl
breakout_high = float(candle.HighPrice) >= ch and rng > 0
breakout_low = float(candle.LowPrice) <= cl and rng > 0
can_trade = self._in_window(candle.OpenTime)
if breakout_high and self.Position > 0:
self.SellMarket()
self._reset_trade()
if breakout_low and self.Position < 0:
self.BuyMarket()
self._reset_trade()
if rng > 0:
stop_dist = rng * (1.0 + float(self.StopRangePercent) / 100.0)
if breakout_high and self.Position == 0 and can_trade:
self.SellMarket()
self._entry_side = 'sell'
self._entry_price = ch
self._stop_price = self._entry_price + stop_dist
elif breakout_low and self.Position == 0 and can_trade:
self.BuyMarket()
self._entry_side = 'buy'
self._entry_price = cl
self._stop_price = self._entry_price - stop_dist
if self.Position == 0:
self._reset_trade()
self._prev_highest = hv
self._prev_lowest = lv
def _manage_stop(self, candle):
if self._entry_side is None or self._stop_price is None:
return
if self._entry_side == 'buy':
if self.Position <= 0:
self._reset_trade()
return
if float(candle.LowPrice) <= self._stop_price:
self.SellMarket()
self._reset_trade()
elif self._entry_side == 'sell':
if self.Position >= 0:
self._reset_trade()
return
if float(candle.HighPrice) >= self._stop_price:
self.BuyMarket()
self._reset_trade()
def _in_window(self, time):
ns = ((self.StartHour % 24) + 24) % 24
ne = ((self.EndHour % 24) + 24) % 24
if ns == ne:
return False
start = TimeSpan(ns, 0, 0)
end = TimeSpan(ne, 0, 0)
current = time.TimeOfDay
if ns < ne:
return current >= start and current <= end
return current >= start or current <= end
def _reset_trade(self):
self._entry_price = None
self._stop_price = None
self._entry_side = None
def OnReseted(self):
super(pipso_strategy, self).OnReseted()
self._prev_highest = 0.0
self._prev_lowest = 0.0
self._channel_init = False
self._reset_trade()
def CreateClone(self):
return pipso_strategy()