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>
/// Trades when a configurable number of consecutive candles share the same direction.
/// Applies fixed stop-loss, take-profit and optional trailing stop in price steps.
/// </summary>
public class NCandlesV2Strategy : Strategy
{
private readonly StrategyParam<int> _candlesCount;
private readonly StrategyParam<decimal> _lotSize;
private readonly StrategyParam<int> _takeProfitPips;
private readonly StrategyParam<int> _stopLossPips;
private readonly StrategyParam<int> _trailingStopPips;
private readonly StrategyParam<int> _trailingStepPips;
private readonly StrategyParam<DataType> _candleType;
private int _streakLength;
private int _streakDirection;
private int _currentPositionDirection;
private decimal _entryPrice;
private decimal? _stopPrice;
private decimal? _takePrice;
public int CandlesCount
{
get => _candlesCount.Value;
set => _candlesCount.Value = value;
}
public decimal LotSize
{
get => _lotSize.Value;
set => _lotSize.Value = value;
}
public int TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
public int StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
public int TrailingStopPips
{
get => _trailingStopPips.Value;
set => _trailingStopPips.Value = value;
}
public int TrailingStepPips
{
get => _trailingStepPips.Value;
set => _trailingStepPips.Value = value;
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public NCandlesV2Strategy()
{
_candlesCount = Param(nameof(CandlesCount), 3)
.SetGreaterThanZero()
.SetDisplay("Candles in Row", "Number of identical candles required", "Entry");
_lotSize = Param(nameof(LotSize), 1m)
.SetGreaterThanZero()
.SetDisplay("Lot Size", "Position size used for entries", "Risk");
_takeProfitPips = Param(nameof(TakeProfitPips), 50)
.SetNotNegative()
.SetDisplay("Take Profit (pips)", "Take-profit distance in price steps", "Risk");
_stopLossPips = Param(nameof(StopLossPips), 50)
.SetNotNegative()
.SetDisplay("Stop Loss (pips)", "Stop-loss distance in price steps", "Risk");
_trailingStopPips = Param(nameof(TrailingStopPips), 10)
.SetNotNegative()
.SetDisplay("Trailing Stop (pips)", "Trailing stop distance in price steps", "Risk");
_trailingStepPips = Param(nameof(TrailingStepPips), 4)
.SetNotNegative()
.SetDisplay("Trailing Step (pips)", "Additional move required to tighten trailing stop", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Time frame used for analysis", "General");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
protected override void OnReseted()
{
base.OnReseted();
ResetState();
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle).Start();
}
private void ProcessCandle(ICandleMessage candle)
{
// Process only completed candles to avoid premature decisions.
if (candle.State != CandleStates.Finished)
return;
// Wait until the strategy is fully initialized and allowed to trade.
// Update trailing logic and close the position if protective levels are hit.
if (ManageOpenPosition(candle))
return;
var direction = GetCandleDirection(candle);
// Doji candles reset the streak because they do not show clear direction.
if (direction == 0)
{
ResetStreak();
return;
}
// Maintain the running count of identical candles.
if (direction == _streakDirection)
_streakLength++;
else
{
_streakDirection = direction;
_streakLength = 1;
}
// Enter only after the required number of matching candles is observed.
if (_streakLength < CandlesCount)
return;
if (direction > 0)
TryOpenLong(candle);
else
TryOpenShort(candle);
}
private bool ManageOpenPosition(ICandleMessage candle)
{
// Reset cached values once the position is flat.
if (Position == 0)
{
_currentPositionDirection = 0;
_stopPrice = null;
_takePrice = null;
_entryPrice = 0m;
return false;
}
var pip = GetPipSize();
var trailingStep = TrailingStepPips * pip;
if (_currentPositionDirection > 0)
{
// Raise the stop for long trades when price advances far enough.
if (TrailingStopPips > 0)
{
var desired = candle.ClosePrice - TrailingStopPips * pip;
if (_stopPrice is decimal stop && desired - trailingStep > stop)
_stopPrice = desired;
}
// Close long positions if take-profit or stop-loss levels are reached.
if (_takePrice is decimal take && candle.HighPrice >= take)
return ExitPosition();
if (_stopPrice is decimal stopLoss && candle.LowPrice <= stopLoss)
return ExitPosition();
}
else if (_currentPositionDirection < 0)
{
// Lower the stop for short trades when price keeps moving down.
if (TrailingStopPips > 0)
{
var desired = candle.ClosePrice + TrailingStopPips * pip;
if (_stopPrice is decimal stop && desired + trailingStep < stop)
_stopPrice = desired;
}
// Close short positions if take-profit or stop-loss levels are reached.
if (_takePrice is decimal take && candle.LowPrice <= take)
return ExitPosition();
if (_stopPrice is decimal stopLoss && candle.HighPrice >= stopLoss)
return ExitPosition();
}
return false;
}
private void TryOpenLong(ICandleMessage candle)
{
if (Position > 0)
return;
if (Position < 0)
BuyMarket();
BuyMarket();
SetPositionState(candle.ClosePrice, 1);
}
private void TryOpenShort(ICandleMessage candle)
{
if (Position < 0)
return;
if (Position > 0)
SellMarket();
SellMarket();
SetPositionState(candle.ClosePrice, -1);
}
private bool ExitPosition()
{
// Close the active position and clear the cached trade state.
if (Position > 0)
SellMarket();
else if (Position < 0)
BuyMarket();
ResetState();
return true;
}
private void SetPositionState(decimal price, int direction)
{
// Remember the entry direction and compute initial protective levels.
_currentPositionDirection = direction;
_entryPrice = price;
var pip = GetPipSize();
if (direction > 0)
{
_stopPrice = StopLossPips > 0 ? price - StopLossPips * pip : (TrailingStopPips > 0 ? price : null);
_takePrice = TakeProfitPips > 0 ? price + TakeProfitPips * pip : null;
}
else
{
_stopPrice = StopLossPips > 0 ? price + StopLossPips * pip : (TrailingStopPips > 0 ? price : null);
_takePrice = TakeProfitPips > 0 ? price - TakeProfitPips * pip : null;
}
}
private void ResetState()
{
ResetStreak();
_currentPositionDirection = 0;
_entryPrice = 0m;
_stopPrice = null;
_takePrice = null;
}
private void ResetStreak()
{
_streakLength = 0;
_streakDirection = 0;
}
private static int GetCandleDirection(ICandleMessage candle)
{
return candle.ClosePrice > candle.OpenPrice ? 1 : candle.ClosePrice < candle.OpenPrice ? -1 : 0;
}
private decimal GetPipSize()
{
var step = Security?.PriceStep ?? 0m;
return step > 0m ? step : 1m;
}
}
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
class n_candles_v2_strategy(Strategy):
"""
N Candles v2: trades after N consecutive same-direction candles with manual SL/TP/trailing.
"""
def __init__(self):
super(n_candles_v2_strategy, self).__init__()
self._candles_count = self.Param("CandlesCount", 3) \
.SetGreaterThanZero() \
.SetDisplay("Candles in Row", "Number of identical candles required", "Entry")
self._lot_size = self.Param("LotSize", 1.0) \
.SetGreaterThanZero() \
.SetDisplay("Lot Size", "Position size used for entries", "Risk")
self._tp_pips = self.Param("TakeProfitPips", 50) \
.SetDisplay("Take Profit (pips)", "Take-profit distance in price steps", "Risk")
self._sl_pips = self.Param("StopLossPips", 50) \
.SetDisplay("Stop Loss (pips)", "Stop-loss distance in price steps", "Risk")
self._trailing_stop_pips = self.Param("TrailingStopPips", 10) \
.SetDisplay("Trailing Stop (pips)", "Trailing stop distance in price steps", "Risk")
self._trailing_step_pips = self.Param("TrailingStepPips", 4) \
.SetDisplay("Trailing Step (pips)", "Additional move required to tighten trailing stop", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Candle Type", "Time frame used for analysis", "General")
self._streak_len = 0
self._streak_dir = 0
self._pos_dir = 0
self._entry_price = 0.0
self._stop_price = None
self._take_price = None
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def _get_pip_size(self):
step = 0.0
if self.Security is not None and self.Security.PriceStep is not None:
step = float(self.Security.PriceStep)
return step if step > 0.0 else 1.0
def _reset_state(self):
self._streak_len = 0
self._streak_dir = 0
self._pos_dir = 0
self._entry_price = 0.0
self._stop_price = None
self._take_price = None
def OnReseted(self):
super(n_candles_v2_strategy, self).OnReseted()
self._reset_state()
def OnStarted2(self, time):
super(n_candles_v2_strategy, self).OnStarted2(time)
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._process_candle).Start()
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
if self._manage_open_position(candle):
return
close = float(candle.ClosePrice)
open_p = float(candle.OpenPrice)
direction = 1 if close > open_p else (-1 if close < open_p else 0)
if direction == 0:
self._streak_len = 0
self._streak_dir = 0
return
if direction == self._streak_dir:
self._streak_len += 1
else:
self._streak_dir = direction
self._streak_len = 1
if self._streak_len < int(self._candles_count.Value):
return
if direction > 0:
self._try_open_long(candle)
else:
self._try_open_short(candle)
def _manage_open_position(self, candle):
if self.Position == 0:
self._pos_dir = 0
self._stop_price = None
self._take_price = None
self._entry_price = 0.0
return False
pip = self._get_pip_size()
trailing_step = int(self._trailing_step_pips.Value) * pip
trailing_stop_pips = int(self._trailing_stop_pips.Value)
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
if self._pos_dir > 0:
if trailing_stop_pips > 0:
desired = close - trailing_stop_pips * pip
if self._stop_price is not None and desired - trailing_step > self._stop_price:
self._stop_price = desired
if self._take_price is not None and high >= self._take_price:
return self._exit_position()
if self._stop_price is not None and low <= self._stop_price:
return self._exit_position()
elif self._pos_dir < 0:
if trailing_stop_pips > 0:
desired = close + trailing_stop_pips * pip
if self._stop_price is not None and desired + trailing_step < self._stop_price:
self._stop_price = desired
if self._take_price is not None and low <= self._take_price:
return self._exit_position()
if self._stop_price is not None and high >= self._stop_price:
return self._exit_position()
return False
def _try_open_long(self, candle):
if self.Position > 0:
return
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
self._set_position_state(float(candle.ClosePrice), 1)
def _try_open_short(self, candle):
if self.Position < 0:
return
if self.Position > 0:
self.SellMarket()
self.SellMarket()
self._set_position_state(float(candle.ClosePrice), -1)
def _exit_position(self):
if self.Position > 0:
self.SellMarket()
elif self.Position < 0:
self.BuyMarket()
self._reset_state()
return True
def _set_position_state(self, price, direction):
self._pos_dir = direction
self._entry_price = price
pip = self._get_pip_size()
tp_pips = int(self._tp_pips.Value)
sl_pips = int(self._sl_pips.Value)
trailing_stop_pips = int(self._trailing_stop_pips.Value)
if direction > 0:
self._stop_price = (price - sl_pips * pip) if sl_pips > 0 else (price if trailing_stop_pips > 0 else None)
self._take_price = (price + tp_pips * pip) if tp_pips > 0 else None
else:
self._stop_price = (price + sl_pips * pip) if sl_pips > 0 else (price if trailing_stop_pips > 0 else None)
self._take_price = (price - tp_pips * pip) if tp_pips > 0 else None
def CreateClone(self):
return n_candles_v2_strategy()