Auf GitHub ansehen

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;
	}
}