Auf GitHub ansehen

Bezier ReOpen Strategy

The Bezier ReOpen Strategy applies a custom Bezier curve indicator to follow trend direction. When the indicator turns upward and the latest value is above the previous one, the strategy can open a long position. When the indicator turns downward, it can open a short position. Existing positions are closed when the indicator changes direction. After entering, additional positions are re-opened each time price advances by a user-defined step, allowing scaling into the trend.

This implementation is based on the original MetaTrader Expert Advisor Exp_Bezier_ReOpen.mq5 (ID 16883).

Details

  • Indicator: Bezier curve constructed from the last BPeriod prices and parameter T defining curve tension.
  • Entry:
    • Long: indicator slope turns up and current value is above previous value.
    • Short: indicator slope turns down and current value is below previous value.
  • Exit:
    • Long: indicator slope turns down.
    • Short: indicator slope turns up.
  • Re-entry: after initial entry, an extra order is sent each time price moves PriceStep away from the last entry price, up to PosTotal orders.
  • Stops: optional stop-loss and take-profit defined in absolute price units.

Parameters

  • CandleType – candle timeframe used for calculations. Default: 4-hour.
  • BPeriod – number of bars for Bezier calculation. Default: 8.
  • T – Bezier curve tension (0..1). Default: 0.5.
  • PriceType – price source for the indicator (close, open, high, low, median, typical, weighted). Default: weighted.
  • PriceStep – price distance to send additional orders. Default: 300.
  • PosTotal – maximum number of positions in scaling sequence. Default: 10.
  • BuyPosOpen – allow opening long positions. Default: true.
  • SellPosOpen – allow opening short positions. Default: true.
  • BuyPosClose – allow closing longs on opposite signal. Default: true.
  • SellPosClose – allow closing shorts on opposite signal. Default: true.
  • StopLoss – stop-loss in price units. Default: 1000.
  • TakeProfit – take-profit in price units. Default: 2000.

Filter Tags

  • Category: Trend following
  • Direction: Both
  • Indicators: Custom
  • Stops: Optional
  • Complexity: Medium
  • Timeframe: Medium-term
  • Risk level: Moderate
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>
/// Bezier re-open strategy based on a custom Bezier curve calculation.
/// Opens positions when the Bezier value changes direction and re-enters
/// after price moves by a specified step.
/// </summary>
public class BezierReOpenStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _bPeriod;
	private readonly StrategyParam<decimal> _t;
	private readonly StrategyParam<decimal> _priceStep;
	private readonly StrategyParam<int> _posTotal;
	private readonly StrategyParam<bool> _buyPosOpen;
	private readonly StrategyParam<bool> _sellPosOpen;
	private readonly StrategyParam<bool> _buyPosClose;
	private readonly StrategyParam<bool> _sellPosClose;
	private readonly StrategyParam<decimal> _stopLoss;
	private readonly StrategyParam<decimal> _takeProfit;

	private readonly List<decimal> _prices = new();
	private decimal _prev1;
	private decimal _prev2;
	private int _orderCount;
	private decimal _lastEntryPrice;

	/// <summary>
	/// Candle type for calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Number of bars for Bezier calculation.
	/// </summary>
	public int BPeriod
	{
		get => _bPeriod.Value;
		set => _bPeriod.Value = value;
	}

	/// <summary>
	/// Bezier curve tension.
	/// </summary>
	public decimal T
	{
		get => _t.Value;
		set => _t.Value = value;
	}

	/// <summary>
	/// Price distance for additional entries.
	/// </summary>
	public decimal PriceStep
	{
		get => _priceStep.Value;
		set => _priceStep.Value = value;
	}

	/// <summary>
	/// Maximum number of positions in sequence.
	/// </summary>
	public int PosTotal
	{
		get => _posTotal.Value;
		set => _posTotal.Value = value;
	}

	/// <summary>
	/// Allow long entries.
	/// </summary>
	public bool BuyPosOpen
	{
		get => _buyPosOpen.Value;
		set => _buyPosOpen.Value = value;
	}

	/// <summary>
	/// Allow short entries.
	/// </summary>
	public bool SellPosOpen
	{
		get => _sellPosOpen.Value;
		set => _sellPosOpen.Value = value;
	}

	/// <summary>
	/// Close longs on opposite signal.
	/// </summary>
	public bool BuyPosClose
	{
		get => _buyPosClose.Value;
		set => _buyPosClose.Value = value;
	}

	/// <summary>
	/// Close shorts on opposite signal.
	/// </summary>
	public bool SellPosClose
	{
		get => _sellPosClose.Value;
		set => _sellPosClose.Value = value;
	}

	/// <summary>
	/// Stop-loss in price units.
	/// </summary>
	public decimal StopLoss
	{
		get => _stopLoss.Value;
		set => _stopLoss.Value = value;
	}

	/// <summary>
	/// Take-profit in price units.
	/// </summary>
	public decimal TakeProfit
	{
		get => _takeProfit.Value;
		set => _takeProfit.Value = value;
	}

	/// <summary>
	/// Constructor.
	/// </summary>
	public BezierReOpenStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles to use", "General");

		_bPeriod = Param(nameof(BPeriod), 8)
			.SetGreaterThanZero()
			.SetDisplay("Bezier Period", "Number of bars for Bezier calculation", "Indicator");

		_t = Param(nameof(T), 0.5m)
			.SetDisplay("T", "Bezier curve tension", "Indicator");

		_priceStep = Param(nameof(PriceStep), 300m)
			.SetGreaterThanZero()
			.SetDisplay("Price Step", "Price distance for additional entries", "Trading");

		_posTotal = Param(nameof(PosTotal), 1)
			.SetGreaterThanZero()
			.SetDisplay("Max Positions", "Maximum number of positions", "Trading");

		_buyPosOpen = Param(nameof(BuyPosOpen), true)
			.SetDisplay("Buy Enabled", "Allow long entries", "Trading");

		_sellPosOpen = Param(nameof(SellPosOpen), true)
			.SetDisplay("Sell Enabled", "Allow short entries", "Trading");

		_buyPosClose = Param(nameof(BuyPosClose), true)
			.SetDisplay("Close Long", "Close longs on opposite signal", "Trading");

		_sellPosClose = Param(nameof(SellPosClose), true)
			.SetDisplay("Close Short", "Close shorts on opposite signal", "Trading");

		_stopLoss = Param(nameof(StopLoss), 1000m)
			.SetDisplay("Stop Loss", "Stop-loss in price units", "Risk");

		_takeProfit = Param(nameof(TakeProfit), 2000m)
			.SetDisplay("Take Profit", "Take-profit in price units", "Risk");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_prices.Clear();
		_prev1 = 0m;
		_prev2 = 0m;
		_orderCount = 0;
		_lastEntryPrice = 0m;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		SubscribeCandles(CandleType)
			.Bind(ProcessCandle)
			.Start();
	}

	private decimal ComputeBezier()
	{
		var n = BPeriod;
		if (_prices.Count < n + 1)
			return 0m;

		var t = (double)T;
		double result = 0;
		for (var i = 0; i <= n; i++)
		{
			var priceVal = (double)_prices[_prices.Count - n - 1 + i];
			result += priceVal * Binomial(n, i) * Math.Pow(t, i) * Math.Pow(1 - t, n - i);
		}

		return (decimal)result;
	}

	private static double Binomial(int n, int k)
	{
		return Factorial(n) / (Factorial(k) * Factorial(n - k));
	}

	private static double Factorial(int value)
	{
		var res = 1d;
		for (var j = 2; j <= value; j++)
			res *= j;
		return res;
	}

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

		var price = (2m * candle.ClosePrice + candle.HighPrice + candle.LowPrice) / 4m;
		_prices.Add(price);
		if (_prices.Count > BPeriod + 2)
			_prices.RemoveAt(0);

		var bezierValue = ComputeBezier();
		if (bezierValue == 0m)
			return;

		_prev2 = _prev1;
		_prev1 = bezierValue;

		if (_prev2 == 0m)
			return;

		var openLong = false;
		var openShort = false;
		var closeLong = false;
		var closeShort = false;

		if (_prev1 > _prev2)
		{
			if (BuyPosOpen)
				openLong = true;
			if (SellPosClose && Position < 0)
				closeShort = true;
		}
		else if (_prev1 < _prev2)
		{
			if (SellPosOpen)
				openShort = true;
			if (BuyPosClose && Position > 0)
				closeLong = true;
		}

		if (closeLong)
			SellMarket();

		if (closeShort)
			BuyMarket();

		if (openLong && Position <= 0)
		{
			BuyMarket();
			_orderCount = 1;
			_lastEntryPrice = candle.ClosePrice;
			return;
		}

		if (openShort && Position >= 0)
		{
			SellMarket();
			_orderCount = 1;
			_lastEntryPrice = candle.ClosePrice;
			return;
		}

		if (Position > 0 && _orderCount < PosTotal)
		{
			if (candle.ClosePrice - _lastEntryPrice >= PriceStep)
			{
				BuyMarket();
				_lastEntryPrice = candle.ClosePrice;
				_orderCount++;
			}
		}
		else if (Position < 0 && _orderCount < PosTotal)
		{
			if (_lastEntryPrice - candle.ClosePrice >= PriceStep)
			{
				SellMarket();
				_lastEntryPrice = candle.ClosePrice;
				_orderCount++;
			}
		}

		CheckStops(candle.ClosePrice);
	}

	private void CheckStops(decimal price)
	{
		if (Position > 0)
		{
			if (StopLoss > 0 && price <= _lastEntryPrice - StopLoss)
				SellMarket();
			if (TakeProfit > 0 && price >= _lastEntryPrice + TakeProfit)
				SellMarket();
		}
		else if (Position < 0)
		{
			if (StopLoss > 0 && price >= _lastEntryPrice + StopLoss)
				BuyMarket();
			if (TakeProfit > 0 && price <= _lastEntryPrice - TakeProfit)
				BuyMarket();
		}
	}
}