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>
/// Momentum breakout with rollback confirmation inspired by the gazonkos MT5 expert.
/// The strategy waits for a spread between two historical closes, then joins the trend after a pullback.
/// </summary>
public class GazonkosStrategy : Strategy
{
private readonly StrategyParam<decimal> _takeProfit;
private readonly StrategyParam<decimal> _rollback;
private readonly StrategyParam<decimal> _stopLoss;
private readonly StrategyParam<decimal> _delta;
private readonly StrategyParam<decimal> _tradeVolume;
private readonly StrategyParam<int> _firstShift;
private readonly StrategyParam<int> _secondShift;
private readonly StrategyParam<int> _activeTrades;
private readonly StrategyParam<DataType> _candleType;
private readonly List<decimal> _closeHistory = new();
private int _state;
private int _tradeDirection;
private decimal _maxPrice;
private decimal _minPrice;
private bool _canTrade;
private int _lastTradeHour;
private int _lastSignalHour;
private int _maxHistory;
/// <summary>
/// Take profit distance expressed in absolute price units.
/// </summary>
public decimal TakeProfit
{
get => _takeProfit.Value;
set => _takeProfit.Value = value;
}
/// <summary>
/// Rollback distance that confirms the entry.
/// </summary>
public decimal Rollback
{
get => _rollback.Value;
set => _rollback.Value = value;
}
/// <summary>
/// Stop loss distance expressed in absolute price units.
/// </summary>
public decimal StopLoss
{
get => _stopLoss.Value;
set => _stopLoss.Value = value;
}
/// <summary>
/// Minimum difference between historical closes to detect momentum.
/// </summary>
public decimal Delta
{
get => _delta.Value;
set => _delta.Value = value;
}
/// <summary>
/// Default volume for market orders.
/// </summary>
public decimal TradeVolume
{
get => _tradeVolume.Value;
set => _tradeVolume.Value = value;
}
/// <summary>
/// Older bar shift used in the close difference calculation.
/// </summary>
public int FirstShift
{
get => _firstShift.Value;
set => _firstShift.Value = value;
}
/// <summary>
/// Recent bar shift used in the close difference calculation.
/// </summary>
public int SecondShift
{
get => _secondShift.Value;
set => _secondShift.Value = value;
}
/// <summary>
/// Maximum simultaneous trades counted in volume units.
/// </summary>
public int ActiveTrades
{
get => _activeTrades.Value;
set => _activeTrades.Value = value;
}
/// <summary>
/// Candle type used by the strategy.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public GazonkosStrategy()
{
_takeProfit = Param(nameof(TakeProfit), 700m)
.SetDisplay("Take Profit", "Take profit distance in price units", "Risk Management")
;
_rollback = Param(nameof(Rollback), 300m)
.SetDisplay("Rollback", "Required pullback before entering", "Signals")
;
_stopLoss = Param(nameof(StopLoss), 1000m)
.SetDisplay("Stop Loss", "Stop loss distance in price units", "Risk Management")
;
_delta = Param(nameof(Delta), 200m)
.SetDisplay("Delta", "Minimum difference between closes", "Signals")
;
_tradeVolume = Param(nameof(TradeVolume), 0.1m)
.SetDisplay("Trade Volume", "Default volume for market orders", "Orders")
;
_firstShift = Param(nameof(FirstShift), 3)
.SetDisplay("First Shift", "Older close shift for the comparison", "Signals")
;
_secondShift = Param(nameof(SecondShift), 2)
.SetDisplay("Second Shift", "Recent close shift for the comparison", "Signals")
;
_activeTrades = Param(nameof(ActiveTrades), 1)
.SetDisplay("Active Trades", "Maximum simultaneous trades", "Risk Management")
;
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
.SetDisplay("Candle Type", "Candle series used for signals", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_state = 0;
_tradeDirection = 0;
_maxPrice = 0m;
_minPrice = decimal.MaxValue;
_canTrade = true;
_lastTradeHour = -1;
_lastSignalHour = -1;
_closeHistory.Clear();
UpdateHistorySize();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
Volume = TradeVolume;
UpdateHistorySize();
StartProtection(
takeProfit: new Unit(TakeProfit, UnitTypes.Absolute),
stopLoss: new Unit(StopLoss, UnitTypes.Absolute),
isStopTrailing: false,
useMarketOrders: true);
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
UpdateHistorySize();
AddClose(candle.ClosePrice);
var hour = candle.CloseTime.Hour;
if (_state == 0)
{
// Evaluate if another trade can be started during the current hour.
_canTrade = true;
if (_lastTradeHour == hour)
_canTrade = false;
if (ActiveTrades > 0 && Volume > 0 && Math.Abs(Position) >= ActiveTrades * Volume)
_canTrade = false;
if (_canTrade)
_state = 1;
}
if (_state == 1)
{
// Look for momentum using the difference between historical closes.
if (!TryGetClose(FirstShift, out var closeFirst) || !TryGetClose(SecondShift, out var closeSecond))
return;
if (closeSecond - closeFirst > Delta)
{
_tradeDirection = 1;
_maxPrice = candle.ClosePrice;
_lastSignalHour = hour;
_state = 2;
}
else if (closeFirst - closeSecond > Delta)
{
_tradeDirection = -1;
_minPrice = candle.ClosePrice;
_lastSignalHour = hour;
_state = 2;
}
}
if (_state == 2)
{
// Wait for a rollback confirmation during the same hour when the signal appeared.
if (_lastSignalHour != hour)
{
ResetToIdle();
return;
}
if (_tradeDirection == 1)
{
if (candle.HighPrice > _maxPrice)
_maxPrice = candle.HighPrice;
if (candle.LowPrice < _maxPrice - Rollback)
_state = 3;
}
else if (_tradeDirection == -1)
{
if (candle.LowPrice < _minPrice)
_minPrice = candle.LowPrice;
if (candle.HighPrice > _minPrice + Rollback)
_state = 3;
}
}
if (_state == 3)
{
// Execute the trade after rollback confirmation.
if (_tradeDirection == 1 && Position <= 0)
{
BuyMarket();
_lastTradeHour = hour;
ResetToIdle();
}
else if (_tradeDirection == -1 && Position >= 0)
{
SellMarket();
_lastTradeHour = hour;
ResetToIdle();
}
}
}
private void UpdateHistorySize()
{
var required = Math.Max(Math.Max(FirstShift, SecondShift) + 1, 1);
if (_maxHistory == required)
return;
_maxHistory = required;
if (_closeHistory.Count > _maxHistory)
_closeHistory.RemoveRange(_maxHistory, _closeHistory.Count - _maxHistory);
}
private void AddClose(decimal close)
{
_closeHistory.Insert(0, close);
if (_closeHistory.Count > _maxHistory)
_closeHistory.RemoveAt(_closeHistory.Count - 1);
}
private bool TryGetClose(int shift, out decimal close)
{
close = 0m;
if (shift < 0)
return false;
if (_closeHistory.Count <= shift)
return false;
close = _closeHistory[shift];
return true;
}
private void ResetToIdle()
{
_state = 0;
_tradeDirection = 0;
_maxPrice = 0m;
_minPrice = decimal.MaxValue;
_canTrade = true;
_lastSignalHour = -1;
}
}
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, Unit, UnitTypes
from StockSharp.Algo.Strategies import Strategy
class gazonkos_strategy(Strategy):
def __init__(self):
super(gazonkos_strategy, self).__init__()
self._take_profit = self.Param("TakeProfit", 700.0)
self._rollback = self.Param("Rollback", 300.0)
self._stop_loss = self.Param("StopLoss", 1000.0)
self._delta = self.Param("Delta", 200.0)
self._trade_volume = self.Param("TradeVolume", 0.1)
self._first_shift = self.Param("FirstShift", 3)
self._second_shift = self.Param("SecondShift", 2)
self._active_trades = self.Param("ActiveTrades", 1)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15)))
self._close_history = []
self._state = 0
self._trade_direction = 0
self._max_price = 0.0
self._min_price = 999999999.0
self._can_trade = True
self._last_trade_hour = -1
self._last_signal_hour = -1
self._max_history = 1
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def OnReseted(self):
super(gazonkos_strategy, self).OnReseted()
self._close_history = []
self._state = 0
self._trade_direction = 0
self._max_price = 0.0
self._min_price = 999999999.0
self._can_trade = True
self._last_trade_hour = -1
self._last_signal_hour = -1
self._update_history_size()
def OnStarted2(self, time):
super(gazonkos_strategy, self).OnStarted2(time)
self._close_history = []
self._state = 0
self._trade_direction = 0
self._max_price = 0.0
self._min_price = 999999999.0
self._can_trade = True
self._last_trade_hour = -1
self._last_signal_hour = -1
self.Volume = self._trade_volume.Value
self._update_history_size()
self.StartProtection(
takeProfit=Unit(float(self._take_profit.Value), UnitTypes.Absolute),
stopLoss=Unit(float(self._stop_loss.Value), UnitTypes.Absolute),
isStopTrailing=False,
useMarketOrders=True)
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._process_candle).Start()
def _update_history_size(self):
required = max(max(int(self._first_shift.Value), int(self._second_shift.Value)) + 1, 1)
if self._max_history == required:
return
self._max_history = required
if len(self._close_history) > self._max_history:
self._close_history = self._close_history[:self._max_history]
def _add_close(self, close):
self._close_history.insert(0, close)
if len(self._close_history) > self._max_history:
self._close_history.pop()
def _try_get_close(self, shift):
if shift < 0:
return None
if len(self._close_history) <= shift:
return None
return self._close_history[shift]
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
self._update_history_size()
self._add_close(float(candle.ClosePrice))
hour = candle.CloseTime.Hour
if self._state == 0:
self._can_trade = True
if self._last_trade_hour == hour:
self._can_trade = False
vol = float(self.Volume)
active = int(self._active_trades.Value)
if active > 0 and vol > 0 and abs(float(self.Position)) >= active * vol:
self._can_trade = False
if self._can_trade:
self._state = 1
if self._state == 1:
fs = int(self._first_shift.Value)
ss = int(self._second_shift.Value)
close_first = self._try_get_close(fs)
close_second = self._try_get_close(ss)
if close_first is None or close_second is None:
return
delta = float(self._delta.Value)
close = float(candle.ClosePrice)
if close_second - close_first > delta:
self._trade_direction = 1
self._max_price = close
self._last_signal_hour = hour
self._state = 2
elif close_first - close_second > delta:
self._trade_direction = -1
self._min_price = close
self._last_signal_hour = hour
self._state = 2
if self._state == 2:
if self._last_signal_hour != hour:
self._reset_to_idle()
return
rollback = float(self._rollback.Value)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
if self._trade_direction == 1:
if high > self._max_price:
self._max_price = high
if low < self._max_price - rollback:
self._state = 3
elif self._trade_direction == -1:
if low < self._min_price:
self._min_price = low
if high > self._min_price + rollback:
self._state = 3
if self._state == 3:
pos = float(self.Position)
if self._trade_direction == 1 and pos <= 0:
self.BuyMarket()
self._last_trade_hour = hour
self._reset_to_idle()
elif self._trade_direction == -1 and pos >= 0:
self.SellMarket()
self._last_trade_hour = hour
self._reset_to_idle()
def _reset_to_idle(self):
self._state = 0
self._trade_direction = 0
self._max_price = 0.0
self._min_price = 999999999.0
self._can_trade = True
self._last_signal_hour = -1
def CreateClone(self):
return gazonkos_strategy()