20 Pips Opposite Last N Hour Trend Strategy
This StockSharp strategy is a high-level port of the MetaTrader Expert Advisor
"20 Pips Opposite Last N Hour Trend". It observes hourly candles, gauges
how price behaved during the previous N hours, and then opens a position in
the opposite direction when the configured trading hour finishes. The trade is
managed using a fixed 20 pip take-profit target and an hourly time-out, while
a martingale-style volume ladder is applied after consecutive losses.
The implementation uses StockSharp's candle subscriptions, parameter system,
and order helpers (BuyMarket, SellMarket) so it can run unchanged inside
Designer, API, Runner, or Shell.
Trading Logic
- The strategy subscribes to the selected candle type (default: 1-hour bars).
- For each finished candle it keeps the close price inside an internal history.
- When a candle with
OpenTime.Hour == TradingHour is completed and enough
history is available:
- Compare the close that happened
HoursToCheckTrend bars ago with the
previous close (1 bar ago).
- If price decreased over that window (bearish drift) the strategy buys;
if price increased (bullish drift) it sells. Equal closes skip trading.
- Only one trade is opened per day and exclusively on the configured trading
hour. All other candles are used purely for management.
Position Management
- A 20-pip target (adjusted for 3/5 digit symbols) is computed right after the
entry. When any finished candle shows that the high/low touched the target the
position is closed at that level.
- If the target is not reached during the next hour, the position is closed at
the end of the following candle to avoid overnight exposure.
- Daily counters are reset automatically when a new trading day starts, so the
next eligible signal can fire on the following session.
Money Management
Volume sets the base order size. MaxVolume caps the resulting size of any
martingale step.
- After a losing exit the strategy increases the next position by the
appropriate multiplier: first loss →
FirstMultiplier, second loss →
SecondMultiplier, etc. Losing streaks beyond five trades reuse the fifth
multiplier. Any profitable or break-even close resets the sequence.
- Volume calculations rely on the last executed position price, so profit/loss
detection remains deterministic even without full broker PnL data.
Parameters
| Parameter |
Default |
Description |
MaxPositions |
9 |
Maximum trades allowed per day. Set to 0 to disable trading. |
Volume |
0.1 |
Base volume for the first trade of a streak. |
MaxVolume |
5 |
Hard cap for the adjusted volume after multipliers. |
TakeProfitPips |
20 |
Take-profit distance in pips. 0 disables the TP. |
TradingHour |
7 |
Hour of the day (0-23) that is eligible for opening a position. |
HoursToCheckTrend |
24 |
Number of hourly closes used to measure the prior trend. |
FirstMultiplier |
2 |
Multiplier applied after the first consecutive loss. |
SecondMultiplier |
4 |
Multiplier applied after the second consecutive loss. |
ThirdMultiplier |
8 |
Multiplier applied after the third consecutive loss. |
FourthMultiplier |
16 |
Multiplier applied after the fourth consecutive loss. |
FifthMultiplier |
32 |
Multiplier applied from the fifth loss onward. |
CandleType |
H1 |
Candle data type used for signal generation and management. |
Additional Notes
- Pip size is calculated from
Security.PriceStep and the number of decimals so
the 20-pip target behaves correctly on both 4- and 5-digit FX symbols.
StartProtection() is invoked when the strategy starts, enabling built-in
StockSharp protections (auto stop for unbound positions, portfolio resets).
- The logic only uses finished candles and never reads indicator values
directly, matching the guidelines from
AGENTS.md.
Risk Disclaimer: Martingale-style position sizing can lead to substantial
drawdowns. Always test the parameters on historical data and use prudential
risk limits before deploying to live trading.
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>
/// Strategy that trades against the last N hours trend with a fixed take profit.
/// </summary>
public class TwentyPipsOppositeLastNHourTrendStrategy : Strategy
{
private readonly StrategyParam<int> _maxPositions;
private readonly StrategyParam<decimal> _maxVolume;
private readonly StrategyParam<decimal> _takeProfitPips;
private readonly StrategyParam<int> _tradingHour;
private readonly StrategyParam<int> _hoursToCheckTrend;
private readonly StrategyParam<int> _firstMultiplier;
private readonly StrategyParam<int> _secondMultiplier;
private readonly StrategyParam<int> _thirdMultiplier;
private readonly StrategyParam<int> _fourthMultiplier;
private readonly StrategyParam<int> _fifthMultiplier;
private readonly StrategyParam<DataType> _candleType;
private readonly List<decimal> _closeHistory = new();
private decimal? _entryPrice;
private decimal? _takeProfitLevel;
private decimal _entryVolume;
private int _positionDirection;
private int _consecutiveLosses;
private DateTime? _currentDay;
private int _tradesToday;
public int MaxPositions
{
get => _maxPositions.Value;
set => _maxPositions.Value = value;
}
public decimal MaxVolume
{
get => _maxVolume.Value;
set => _maxVolume.Value = value;
}
public decimal TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
public int TradingHour
{
get => _tradingHour.Value;
set => _tradingHour.Value = value;
}
public int HoursToCheckTrend
{
get => _hoursToCheckTrend.Value;
set => _hoursToCheckTrend.Value = value;
}
public int FirstMultiplier
{
get => _firstMultiplier.Value;
set => _firstMultiplier.Value = value;
}
public int SecondMultiplier
{
get => _secondMultiplier.Value;
set => _secondMultiplier.Value = value;
}
public int ThirdMultiplier
{
get => _thirdMultiplier.Value;
set => _thirdMultiplier.Value = value;
}
public int FourthMultiplier
{
get => _fourthMultiplier.Value;
set => _fourthMultiplier.Value = value;
}
public int FifthMultiplier
{
get => _fifthMultiplier.Value;
set => _fifthMultiplier.Value = value;
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public TwentyPipsOppositeLastNHourTrendStrategy()
{
_maxPositions = Param(nameof(MaxPositions), 9)
.SetGreaterThanZero()
.SetDisplay("Max Positions", "Maximum trades per day", "Trading");
_maxVolume = Param(nameof(MaxVolume), 5m)
.SetGreaterThanZero()
.SetDisplay("Max Volume", "Maximum allowed volume", "Risk");
_takeProfitPips = Param(nameof(TakeProfitPips), 20m)
.SetGreaterThanZero()
.SetDisplay("Take Profit (pips)", "Take profit distance in pips", "Trading");
_tradingHour = Param(nameof(TradingHour), 8)
.SetRange(0, 23)
.SetDisplay("Trading Hour", "Hour (0-23) when entries are allowed", "Timing");
_hoursToCheckTrend = Param(nameof(HoursToCheckTrend), 6)
.SetRange(2, 240)
.SetDisplay("Hours To Check", "Lookback hours for trend calculation", "Signals");
_firstMultiplier = Param(nameof(FirstMultiplier), 2)
.SetGreaterThanZero()
.SetDisplay("First Multiplier", "Multiplier after first loss", "Money Management");
_secondMultiplier = Param(nameof(SecondMultiplier), 4)
.SetGreaterThanZero()
.SetDisplay("Second Multiplier", "Multiplier after second loss", "Money Management");
_thirdMultiplier = Param(nameof(ThirdMultiplier), 8)
.SetGreaterThanZero()
.SetDisplay("Third Multiplier", "Multiplier after third loss", "Money Management");
_fourthMultiplier = Param(nameof(FourthMultiplier), 16)
.SetGreaterThanZero()
.SetDisplay("Fourth Multiplier", "Multiplier after fourth loss", "Money Management");
_fifthMultiplier = Param(nameof(FifthMultiplier), 32)
.SetGreaterThanZero()
.SetDisplay("Fifth Multiplier", "Multiplier after fifth loss", "Money Management");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Candle timeframe to process", "Market Data");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
protected override void OnReseted()
{
base.OnReseted();
_closeHistory.Clear();
_entryPrice = null;
_takeProfitLevel = null;
_entryVolume = 0m;
_positionDirection = 0;
_consecutiveLosses = 0;
_currentDay = null;
_tradesToday = 0;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle).Start();
// no fixed protection needed
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var candleDay = candle.OpenTime.Date;
if (_currentDay != candleDay)
{
_currentDay = candleDay;
_tradesToday = 0;
}
if (_positionDirection != 0)
{
if (_takeProfitLevel is decimal target)
{
// Take profit when the candle range touches the desired level.
var hitTarget = _positionDirection > 0
? candle.HighPrice >= target
: candle.LowPrice <= target;
if (hitTarget)
{
ClosePosition(target);
}
}
if (_positionDirection != 0 && candle.OpenTime.Hour != TradingHour)
{
// Close remaining exposure when the configured session hour has passed.
ClosePosition(candle.ClosePrice);
}
}
if (_positionDirection != 0)
{
UpdateHistory(candle.ClosePrice);
return;
}
if (candle.OpenTime.Hour != TradingHour)
{
UpdateHistory(candle.ClosePrice);
return;
}
if (MaxPositions <= 0 || _tradesToday >= MaxPositions)
{
UpdateHistory(candle.ClosePrice);
return;
}
var requiredHistory = Math.Max(HoursToCheckTrend, 2);
if (_closeHistory.Count < requiredHistory)
{
UpdateHistory(candle.ClosePrice);
return;
}
var referenceClose = _closeHistory[_closeHistory.Count - HoursToCheckTrend];
var previousClose = _closeHistory[_closeHistory.Count - 1];
if (previousClose == referenceClose)
{
UpdateHistory(candle.ClosePrice);
return;
}
// Opposite trend logic: buy after bearish drift, sell after bullish drift.
var goLong = previousClose < referenceClose;
var orderVolume = CalculateOrderVolume();
if (orderVolume <= 0)
{
UpdateHistory(candle.ClosePrice);
return;
}
if (goLong)
{
Volume = orderVolume;
BuyMarket();
_positionDirection = 1;
}
else
{
Volume = orderVolume;
SellMarket();
_positionDirection = -1;
}
_entryPrice = candle.ClosePrice;
_entryVolume = orderVolume;
var distance = GetTakeProfitDistance();
if (distance > 0m)
{
_takeProfitLevel = _positionDirection > 0
? _entryPrice + distance
: _entryPrice - distance;
}
else
{
_takeProfitLevel = null;
}
_tradesToday++;
UpdateHistory(candle.ClosePrice);
}
private void ClosePosition(decimal exitPrice)
{
var direction = _positionDirection;
var entryPrice = _entryPrice;
var volume = Math.Abs(Position);
if (volume <= 0m && _entryVolume > 0m)
{
volume = _entryVolume;
}
if (volume <= 0m)
{
_positionDirection = 0;
_takeProfitLevel = null;
_entryPrice = null;
_entryVolume = 0m;
return;
}
if (direction > 0)
{
SellMarket();
}
else if (direction < 0)
{
BuyMarket();
}
if (entryPrice is decimal price)
{
var isLoss = direction > 0
? exitPrice < price
: exitPrice > price;
_consecutiveLosses = isLoss
? Math.Min(_consecutiveLosses + 1, 5)
: 0;
}
_positionDirection = 0;
_takeProfitLevel = null;
_entryPrice = null;
_entryVolume = 0m;
}
private void UpdateHistory(decimal closePrice)
{
_closeHistory.Add(closePrice);
var maxHistory = Math.Max(HoursToCheckTrend, 2);
if (_closeHistory.Count > maxHistory)
{
_closeHistory.RemoveRange(0, _closeHistory.Count - maxHistory);
}
}
private decimal CalculateOrderVolume()
{
if (Volume <= 0m)
{
return 0m;
}
var multiplier = _consecutiveLosses switch
{
>= 5 => (decimal)FifthMultiplier,
4 => (decimal)FourthMultiplier,
3 => (decimal)ThirdMultiplier,
2 => (decimal)SecondMultiplier,
1 => (decimal)FirstMultiplier,
_ => 1m
};
var desiredVolume = Volume * multiplier;
if (MaxVolume > 0m && desiredVolume > MaxVolume)
{
desiredVolume = MaxVolume;
}
return desiredVolume;
}
private decimal GetTakeProfitDistance()
{
var pipSize = GetPipSize();
return pipSize > 0m
? TakeProfitPips * pipSize
: 0m;
}
private decimal GetPipSize()
{
var priceStep = Security?.PriceStep ?? 0m;
if (priceStep <= 0m)
{
priceStep = 0.0001m;
}
var decimals = Security?.Decimals ?? 0;
if (decimals == 3 || decimals == 5)
{
return priceStep * 10m;
}
return priceStep;
}
}
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 twenty_pips_opposite_last_n_hour_trend_strategy(Strategy):
"""20 Pips Opposite Last N Hour Trend: counter-trend martingale strategy."""
def __init__(self):
super(twenty_pips_opposite_last_n_hour_trend_strategy, self).__init__()
self._max_positions = self.Param("MaxPositions", 9) \
.SetGreaterThanZero() \
.SetDisplay("Max Positions", "Maximum trades per day", "Trading")
self._max_volume = self.Param("MaxVolume", 5.0) \
.SetGreaterThanZero() \
.SetDisplay("Max Volume", "Maximum allowed volume", "Risk")
self._take_profit_pips = self.Param("TakeProfitPips", 20.0) \
.SetGreaterThanZero() \
.SetDisplay("Take Profit (pips)", "Take profit distance in pips", "Trading")
self._trading_hour = self.Param("TradingHour", 8) \
.SetDisplay("Trading Hour", "Hour (0-23) when entries are allowed", "Timing")
self._hours_to_check_trend = self.Param("HoursToCheckTrend", 6) \
.SetDisplay("Hours To Check", "Lookback hours for trend calculation", "Signals")
self._first_multiplier = self.Param("FirstMultiplier", 2) \
.SetGreaterThanZero() \
.SetDisplay("First Multiplier", "Multiplier after first loss", "Money Management")
self._second_multiplier = self.Param("SecondMultiplier", 4) \
.SetGreaterThanZero() \
.SetDisplay("Second Multiplier", "Multiplier after second loss", "Money Management")
self._third_multiplier = self.Param("ThirdMultiplier", 8) \
.SetGreaterThanZero() \
.SetDisplay("Third Multiplier", "Multiplier after third loss", "Money Management")
self._fourth_multiplier = self.Param("FourthMultiplier", 16) \
.SetGreaterThanZero() \
.SetDisplay("Fourth Multiplier", "Multiplier after fourth loss", "Money Management")
self._fifth_multiplier = self.Param("FifthMultiplier", 32) \
.SetGreaterThanZero() \
.SetDisplay("Fifth Multiplier", "Multiplier after fifth loss", "Money Management")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Candle timeframe to process", "Market Data")
self._close_history = []
self._entry_price = None
self._take_profit_level = None
self._entry_volume = 0.0
self._position_direction = 0
self._consecutive_losses = 0
self._current_day = None
self._trades_today = 0
@property
def MaxPositions(self):
return int(self._max_positions.Value)
@property
def MaxVolume(self):
return float(self._max_volume.Value)
@property
def TakeProfitPips(self):
return float(self._take_profit_pips.Value)
@property
def TradingHour(self):
return int(self._trading_hour.Value)
@property
def HoursToCheckTrend(self):
return int(self._hours_to_check_trend.Value)
@property
def FirstMultiplier(self):
return int(self._first_multiplier.Value)
@property
def SecondMultiplier(self):
return int(self._second_multiplier.Value)
@property
def ThirdMultiplier(self):
return int(self._third_multiplier.Value)
@property
def FourthMultiplier(self):
return int(self._fourth_multiplier.Value)
@property
def FifthMultiplier(self):
return int(self._fifth_multiplier.Value)
@property
def CandleType(self):
return self._candle_type.Value
def _get_pip_size(self):
sec = self.Security
if sec is None or sec.PriceStep is None:
return 0.0001
step = float(sec.PriceStep)
if step <= 0:
return 0.0001
decimals = 0
if sec.Decimals is not None:
decimals = int(sec.Decimals)
if decimals == 3 or decimals == 5:
return step * 10.0
return step
def OnStarted2(self, time):
super(twenty_pips_opposite_last_n_hour_trend_strategy, self).OnStarted2(time)
self._close_history = []
self._entry_price = None
self._take_profit_level = None
self._entry_volume = 0.0
self._position_direction = 0
self._consecutive_losses = 0
self._current_day = None
self._trades_today = 0
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.process_candle).Start()
def process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
candle_day = candle.OpenTime.Date
if self._current_day is None or self._current_day != candle_day:
self._current_day = candle_day
self._trades_today = 0
if self._position_direction != 0:
if self._take_profit_level is not None:
if self._position_direction > 0:
hit_target = float(candle.HighPrice) >= self._take_profit_level
else:
hit_target = float(candle.LowPrice) <= self._take_profit_level
if hit_target:
self._close_position(self._take_profit_level)
if self._position_direction != 0 and candle.OpenTime.Hour != self.TradingHour:
self._close_position(close)
if self._position_direction != 0:
self._update_history(close)
return
if candle.OpenTime.Hour != self.TradingHour:
self._update_history(close)
return
if self.MaxPositions <= 0 or self._trades_today >= self.MaxPositions:
self._update_history(close)
return
required_history = max(self.HoursToCheckTrend, 2)
if len(self._close_history) < required_history:
self._update_history(close)
return
reference_close = self._close_history[len(self._close_history) - self.HoursToCheckTrend]
previous_close = self._close_history[len(self._close_history) - 1]
if previous_close == reference_close:
self._update_history(close)
return
go_long = previous_close < reference_close
order_volume = self._calculate_order_volume()
if order_volume <= 0:
self._update_history(close)
return
if go_long:
self.BuyMarket()
self._position_direction = 1
else:
self.SellMarket()
self._position_direction = -1
self._entry_price = close
self._entry_volume = order_volume
distance = self._get_take_profit_distance()
if distance > 0:
if self._position_direction > 0:
self._take_profit_level = self._entry_price + distance
else:
self._take_profit_level = self._entry_price - distance
else:
self._take_profit_level = None
self._trades_today += 1
self._update_history(close)
def _close_position(self, exit_price):
direction = self._position_direction
entry_price = self._entry_price
volume = abs(self.Position)
if volume <= 0 and self._entry_volume > 0:
volume = self._entry_volume
if volume <= 0:
self._position_direction = 0
self._take_profit_level = None
self._entry_price = None
self._entry_volume = 0.0
return
if direction > 0:
self.SellMarket()
elif direction < 0:
self.BuyMarket()
if entry_price is not None:
if direction > 0:
is_loss = exit_price < entry_price
else:
is_loss = exit_price > entry_price
if is_loss:
self._consecutive_losses = min(self._consecutive_losses + 1, 5)
else:
self._consecutive_losses = 0
self._position_direction = 0
self._take_profit_level = None
self._entry_price = None
self._entry_volume = 0.0
def _update_history(self, close_price):
self._close_history.append(close_price)
max_history = max(self.HoursToCheckTrend, 2)
if len(self._close_history) > max_history:
self._close_history = self._close_history[len(self._close_history) - max_history:]
def _calculate_order_volume(self):
base_vol = self.Volume
if base_vol <= 0:
return 0.0
losses = self._consecutive_losses
if losses >= 5:
multiplier = float(self.FifthMultiplier)
elif losses == 4:
multiplier = float(self.FourthMultiplier)
elif losses == 3:
multiplier = float(self.ThirdMultiplier)
elif losses == 2:
multiplier = float(self.SecondMultiplier)
elif losses == 1:
multiplier = float(self.FirstMultiplier)
else:
multiplier = 1.0
desired_volume = float(base_vol) * multiplier
if self.MaxVolume > 0 and desired_volume > self.MaxVolume:
desired_volume = self.MaxVolume
return desired_volume
def _get_take_profit_distance(self):
pip_size = self._get_pip_size()
if pip_size > 0:
return self.TakeProfitPips * pip_size
return 0.0
def OnReseted(self):
super(twenty_pips_opposite_last_n_hour_trend_strategy, self).OnReseted()
self._close_history = []
self._entry_price = None
self._take_profit_level = None
self._entry_volume = 0.0
self._position_direction = 0
self._consecutive_losses = 0
self._current_day = None
self._trades_today = 0
def CreateClone(self):
return twenty_pips_opposite_last_n_hour_trend_strategy()