Auf GitHub ansehen

Pendulum Strategy

A grid-based martingale system that swings between two price thresholds. The strategy opens a long position when price reaches the upper boundary of the grid and flips to a short position with increased volume when price moves to the lower boundary. It keeps alternating directions (up to a configurable number of layers) while expanding targets and reducing protective distances according to the original Pendulum expert advisor. After taking profit the engine resets the grid and schedules a fresh entry at the same level to keep the pendulum motion running.

Details

  • Entry logic
    • Aligns the grid to the candle close price using the configured StepSize.
    • Upper trigger hit → opens a long position with the base volume.
    • Lower trigger hit → opens a short position with the base volume.
    • When the active position moves to the opposite trigger the strategy reverses direction, multiplies the absolute volume by Multiplier, and updates take-profit / stop-loss distances like the MQL version.
    • Re-entries are scheduled after profitable exits so that the next candle can reopen at the same grid level once the previous orders are flat.
  • Exit logic
    • Each layer defines a dedicated take-profit: one step for the first layer, Multiplier steps for every subsequent layer.
    • Protective stops mirror the MQL logic: first layer uses a wide stop (StepSize * Multiplier), subsequent layers use a one-step stop against the new direction.
    • When the maximum number of layers is reached the strategy waits for either take-profit or stop-loss before resetting.
  • Position management
    • Uses netting: the StockSharp port closes and reverses the aggregate position instead of holding hedged longs and shorts. This preserves the exposure of the original expert while staying compatible with StockSharp portfolios.
    • Volume is rounded to the instrument volume step where available.
  • Data
    • Works with any symbol and timeframe. The default subscription uses 1-minute candles and relies on candle close prices for the grid checks.
  • Built-in protection
    • StartProtection() is enabled to guard unexpected positions left after disconnects or manual intervention.

Parameters

Parameter Default Description
StepSize 0.001 Distance between grid levels. The grid always snaps to multiples of this value.
Multiplier 2 Multiplies both the trade volume and extended targets whenever the direction flips to a new layer. Must be greater than 1.
MaxLayers 3 Maximum number of martingale layers before the strategy stops adding new reversals.
BaseVolume 1 Base trade size used for the first layer. Later layers scale by Multiplier.
CandleType 1 Minute TimeFrame Candle type used for subscription. Can be changed to any other timeframe supported by the data source.

Notes

  • The strategy recreates the behaviour of Pendulum.mq5 without relying on hedged positions. Because StockSharp consolidates exposure, the net position is reversed to emulate the MQL grids.
  • Take-profit completions trigger a deferred order so the next candle can reopen immediately at the same price level once the closing trade is processed.
  • Keep the configured step size aligned with the instrument price step to avoid excessive rounding of the grid levels.
using System;
using System.Collections.Generic;

using Ecng.Common;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Pendulum strategy using SMA crossover with mean reversion.
/// Buys when fast SMA crosses above slow SMA, sells on reverse.
/// </summary>
public class PendulumStrategy : Strategy
{
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;

	private ExponentialMovingAverage _fast;
	private ExponentialMovingAverage _slow;

	private decimal _prevFast;
	private decimal _prevSlow;
	private decimal _entryPrice;
	private int _cooldown;

	public int FastPeriod { get => _fastPeriod.Value; set => _fastPeriod.Value = value; }
	public int SlowPeriod { get => _slowPeriod.Value; set => _slowPeriod.Value = value; }
	public int StopLossPoints { get => _stopLossPoints.Value; set => _stopLossPoints.Value = value; }
	public int TakeProfitPoints { get => _takeProfitPoints.Value; set => _takeProfitPoints.Value = value; }

	public PendulumStrategy()
	{
		_fastPeriod = Param(nameof(FastPeriod), 20).SetGreaterThanZero().SetDisplay("Fast Period", "Fast EMA period", "Indicator");
		_slowPeriod = Param(nameof(SlowPeriod), 80).SetGreaterThanZero().SetDisplay("Slow Period", "Slow EMA period", "Indicator");
		_stopLossPoints = Param(nameof(StopLossPoints), 200).SetNotNegative().SetDisplay("Stop Loss", "Stop-loss in price steps", "Risk");
		_takeProfitPoints = Param(nameof(TakeProfitPoints), 400).SetNotNegative().SetDisplay("Take Profit", "Take-profit in price steps", "Risk");
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		yield return (Security, TimeSpan.FromMinutes(5).TimeFrame());
	}

	protected override void OnReseted()
	{
		base.OnReseted();
		_fast = null; _slow = null;
		_prevFast = 0; _prevSlow = 0; _entryPrice = 0; _cooldown = 0;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		_fast = new ExponentialMovingAverage { Length = FastPeriod };
		_slow = new ExponentialMovingAverage { Length = SlowPeriod };
		var subscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());
		subscription.Bind(_fast, _slow, ProcessCandle);
		subscription.Start();
	}

	private void ProcessCandle(ICandleMessage candle, decimal fastValue, decimal slowValue)
	{
		if (candle.State != CandleStates.Finished) return;
		if (!_fast.IsFormed || !_slow.IsFormed) { _prevFast = fastValue; _prevSlow = slowValue; return; }
		if (_cooldown > 0) { _cooldown--; _prevFast = fastValue; _prevSlow = slowValue; return; }

		var close = candle.ClosePrice;
		var step = Security?.PriceStep ?? 1m;

		if (Position > 0 && _entryPrice > 0)
		{
			if (StopLossPoints > 0 && close <= _entryPrice - StopLossPoints * step) { SellMarket(); _entryPrice = 0; _cooldown = 80; _prevFast = fastValue; _prevSlow = slowValue; return; }
			if (TakeProfitPoints > 0 && close >= _entryPrice + TakeProfitPoints * step) { SellMarket(); _entryPrice = 0; _cooldown = 80; _prevFast = fastValue; _prevSlow = slowValue; return; }
		}
		else if (Position < 0 && _entryPrice > 0)
		{
			if (StopLossPoints > 0 && close >= _entryPrice + StopLossPoints * step) { BuyMarket(); _entryPrice = 0; _cooldown = 80; _prevFast = fastValue; _prevSlow = slowValue; return; }
			if (TakeProfitPoints > 0 && close <= _entryPrice - TakeProfitPoints * step) { BuyMarket(); _entryPrice = 0; _cooldown = 80; _prevFast = fastValue; _prevSlow = slowValue; return; }
		}

		if (_prevFast <= _prevSlow && fastValue > slowValue && Position <= 0)
		{ if (Position < 0) BuyMarket(); BuyMarket(); _entryPrice = close; _cooldown = 80; }
		else if (_prevFast >= _prevSlow && fastValue < slowValue && Position >= 0)
		{ if (Position > 0) SellMarket(); SellMarket(); _entryPrice = close; _cooldown = 80; }

		_prevFast = fastValue; _prevSlow = slowValue;
	}
}