Auf GitHub ansehen

4026 – Pivots Strategy

Overview

This strategy ports the MetaTrader 4 files located in MQL/8550 (the Pivots indicator and the accompanying Pivots_test expert advisor) to StockSharp's high-level Strategy API. It keeps the original behaviour of calculating daily floor-pivot levels, staging a pair of opposing pending orders at the central pivot, and managing each resulting position with a fixed stop-loss, take-profit, and trailing stop.

Pivot calculation

  1. The strategy subscribes to a configurable pivot timeframe (PivotCandleType, daily by default).
  2. Whenever a candle of that timeframe finishes, it derives classic floor-pivot levels from the previous day's OHLC prices:
    • Pivot = (High + Low + Close) / 3
    • R1 = 2 × Pivot − Low
    • S1 = 2 × Pivot − High
    • R2 = Pivot + (High − Low) and S2 = Pivot − (High − Low)
    • R3 = 2 × Pivot + High − 2 × Low and S3 = 2 × Pivot − (2 × High − Low)
  3. The levels become active at the start of the next session. When this happens the strategy logs the values through AddInfoLog (for example: Pivot levels for 2024-04-05: P=1.0924, R1=1.0956, …).

Pending order workflow

Once pivot levels are active, the strategy continuously ensures that two pending orders exist at the pivot price:

  • Buy Limit @ Pivot with post-fill protection SellStop (stop-loss) at S2 and SellLimit (take-profit) at R2.
  • Sell Stop @ Pivot with post-fill protection BuyStop at R2 and BuyLimit at S2.

All orders are submitted via the high-level helper methods BuyLimit, SellStop, SellLimit, and BuyStop. If an order fills, the code recalculates the average entry price for that direction, cancels existing protective orders, and sends a fresh stop/limit pair that covers the entire open volume (mirroring the MetaTrader behaviour where each position inherits the same S2/R2 protection). If the protective stop or take-profit executes, the related helpers are cleared automatically.

The strategy uses a single net position, so opposite fills will offset each other (unlike MetaTrader's ticket-based hedging). This is the only intentional deviation from the original expert.

Trailing stop logic

  • TrailingStopPoints defines the distance in indicator points (multiplied by the instrument PriceStep).
  • For long positions the trailing stop activates once the price has moved more than that distance above the average entry. The protective SellStop is then moved closer to the market.
  • For short positions the mirror logic applies, lowering the BuyStop as price moves favourably.
  • Trailing updates are driven by the intraday series selected through CandleType (15-minute candles by default).

Parameters

Parameter Description Default
OrderVolume Volume of each pending order (lots/contracts). 0.1
TrailingStopPoints Trailing stop distance in points. 0 disables the trailing logic. 30
CandleType Intraday candle series used for trailing and for keeping the session schedule. 15m timeframe
PivotCandleType Timeframe used to compute daily pivot levels. 1D timeframe
LogPivotUpdates When true, pivot levels are written to the strategy log whenever they change. true

All numeric parameters are exposed through StrategyParam<T> so they can be optimised inside the StockSharp infrastructure.

Logging and diagnostics

  • Pivot updates are routed through AddInfoLog, which replaces the MetaTrader Comment/ObjectSetText output.
  • Protective order management, position handling, and trailing logic rely solely on StockSharp's high-level helpers; no low-level order registration or indicator buffers are used.

Usage notes

  1. Attach the strategy to a connector that provides both daily and intraday candles for the chosen security.
  2. Adjust the instrument's step if necessary (PriceStep is auto-detected; the fallback is 0.0001).
  3. Optionally tune OrderVolume, TrailingStopPoints, or the candle types to match the original MT4 setup.

No Python version is provided for this port as requested.

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 calculates classic floor pivot levels from daily candles and trades
/// breakouts around the central pivot. Goes long on close above pivot, short on close below.
/// Uses S2/R2 as stop/target levels.
/// </summary>
public class PivotsStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;

	private decimal _pivotLevel;
	private decimal _r1, _r2, _s1, _s2;
	private decimal? _previousClose;
	private decimal? _entryPrice;
	private bool _pivotReady;

	private readonly List<decimal> _dailyHighs = new();
	private readonly List<decimal> _dailyLows = new();
	private readonly List<decimal> _dailyCloses = new();
	private DateTime _currentDay;
	private decimal _dayHigh;
	private decimal _dayLow;
	private decimal _dayClose;

	public PivotsStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe for signal generation", "General");
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	protected override void OnReseted()
	{
		base.OnReseted();
		_pivotLevel = 0m;
		_r1 = _r2 = _s1 = _s2 = 0m;
		_previousClose = null;
		_entryPrice = null;
		_pivotReady = false;
		_dailyHighs.Clear();
		_dailyLows.Clear();
		_dailyCloses.Clear();
		_currentDay = DateTime.MinValue;
		_dayHigh = 0m;
		_dayLow = decimal.MaxValue;
		_dayClose = 0m;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		var sma = new SimpleMovingAverage { Length = 2 };
		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(sma, ProcessCandle).Start();
	}

	private void ProcessCandle(ICandleMessage candle, decimal smaValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		var candleDay = candle.OpenTime.Date;

		// Track daily OHLC
		if (candleDay != _currentDay)
		{
			if (_currentDay != DateTime.MinValue && _dayHigh > 0m)
			{
				// Previous day completed, calculate pivots
				var high = _dayHigh;
				var low = _dayLow;
				var close = _dayClose;

				_pivotLevel = (high + low + close) / 3m;
				_r1 = 2m * _pivotLevel - low;
				_s1 = 2m * _pivotLevel - high;
				_r2 = _pivotLevel + (high - low);
				_s2 = _pivotLevel - (high - low);
				_pivotReady = true;
			}

			_currentDay = candleDay;
			_dayHigh = candle.HighPrice;
			_dayLow = candle.LowPrice;
			_dayClose = candle.ClosePrice;
		}
		else
		{
			if (candle.HighPrice > _dayHigh) _dayHigh = candle.HighPrice;
			if (candle.LowPrice < _dayLow) _dayLow = candle.LowPrice;
			_dayClose = candle.ClosePrice;
		}

		if (!_pivotReady)
		{
			_previousClose = candle.ClosePrice;
			return;
		}

		if (_previousClose is null)
		{
			_previousClose = candle.ClosePrice;
			return;
		}

		// Manage open positions
		if (Position > 0)
		{
			// Exit long at R2 (take profit) or S1 (stop loss)
			if (candle.HighPrice >= _r2 || candle.LowPrice <= _s1)
			{
				SellMarket(Math.Abs(Position));
				_entryPrice = null;
			}
		}
		else if (Position < 0)
		{
			// Exit short at S2 (take profit) or R1 (stop loss)
			if (candle.LowPrice <= _s2 || candle.HighPrice >= _r1)
			{
				BuyMarket(Math.Abs(Position));
				_entryPrice = null;
			}
		}

		// Entry signals based on pivot cross
		if (Position == 0)
		{
			var crossAbovePivot = _previousClose.Value <= _pivotLevel && candle.ClosePrice > _pivotLevel;
			var crossBelowPivot = _previousClose.Value >= _pivotLevel && candle.ClosePrice < _pivotLevel;

			if (crossAbovePivot)
			{
				BuyMarket();
				_entryPrice = candle.ClosePrice;
			}
			else if (crossBelowPivot)
			{
				SellMarket();
				_entryPrice = candle.ClosePrice;
			}
		}

		_previousClose = candle.ClosePrice;
	}
}