Открыть на GitHub

Стратегия Genie Pivot

Стратегия Genie Pivot основана на идее разворота по пивотам из исходного советника MQL4. Она отслеживает формирование разворотного паттерна из семи последовательных свечей и управляет позицией при помощи фиксированного тейк‑профита и трейлинг‑стопа.

Логика стратегии

  1. Поиск паттерна – сигнал на покупку появляется, когда семь предыдущих минимумов строго убывают, а последняя завершённая свеча делает более высокий минимум и закрывается выше предыдущего максимума. Сигнал на продажу формируется зеркальными условиями по максимумам.
  2. Открытие позиции – после подтверждения сигнала стратегия открывает рыночный ордер с объёмом, рассчитанным из величины капитала и параметров риска.
  3. Сопровождение сделки – после входа выставляются тейк‑профит и трейлинг‑стоп. Трейлинг активируется только после достижения прибыли, превышающей расстояние трейлинга. Если следующая свеча разворачивается (медвежья для лонга, бычья для шорта), позиция закрывается.
  4. Снижение объёма – серия убыточных сделок уменьшает объём согласно параметру Decrease Factor.

Параметры

Имя Описание
TakeProfit Цель по прибыли в шагах цены от цены входа.
TrailingStop Дистанция трейлинг‑стопа в шагах цены.
MaximumRisk Доля капитала, используемая для расчёта объёма.
DecreaseFactor Снижение объёма после серии убыточных сделок.
BaseVolume Резервный объём, когда стоимость портфеля неизвестна.
CandleType Таймфрейм свечей.

Примечания

Стратегия обрабатывает только завершённые свечи. Версия на Python пока отсутствует.

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>
/// Pivot point reversal scalping strategy.
/// </summary>
public class GeniePivotStrategy : Strategy
{
	private readonly StrategyParam<decimal> _takeProfit;
	private readonly StrategyParam<decimal> _trailingStop;
	private readonly StrategyParam<decimal> _maximumRisk;
	private readonly StrategyParam<decimal> _decreaseFactor;
	private readonly StrategyParam<decimal> _baseVolume;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _cooldownBars;

	private readonly decimal[] _lows = new decimal[8];
	private readonly decimal[] _highs = new decimal[8];
	private int _filled;

	private decimal _entryPrice;
	private decimal _stopPrice;
	private decimal _targetPrice;
	private int _lossCount;
	private int _cooldownRemaining;

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

	/// <summary>
	/// Trailing stop distance in price steps.
	/// </summary>
	public decimal TrailingStop
	{
		get => _trailingStop.Value;
		set => _trailingStop.Value = value;
	}

	/// <summary>
	/// Maximum risk used to calculate volume.
	/// </summary>
	public decimal MaximumRisk
	{
		get => _maximumRisk.Value;
		set => _maximumRisk.Value = value;
	}

	/// <summary>
	/// Factor to decrease volume after consecutive losses.
	/// </summary>
	public decimal DecreaseFactor
	{
		get => _decreaseFactor.Value;
		set => _decreaseFactor.Value = value;
	}

	/// <summary>
	/// Base volume used when account value is unknown.
	/// </summary>
	public decimal BaseVolume
	{
		get => _baseVolume.Value;
		set => _baseVolume.Value = value;
	}

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

	/// <summary>
	/// Number of completed candles to wait after closing a position.
	/// </summary>
	public int CooldownBars
	{
		get => _cooldownBars.Value;
		set => _cooldownBars.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of <see cref="GeniePivotStrategy"/>.
	/// </summary>
	public GeniePivotStrategy()
	{
		_takeProfit = Param(nameof(TakeProfit), 500m).SetDisplay("Take Profit", "Profit target in points", "Risk");

		_trailingStop =
			Param(nameof(TrailingStop), 200m).SetDisplay("Trailing Stop", "Trailing distance in points", "Risk");

		_maximumRisk = Param(nameof(MaximumRisk), 0.02m).SetDisplay("Maximum Risk", "Risk per trade", "Risk");

		_decreaseFactor =
			Param(nameof(DecreaseFactor), 3m).SetDisplay("Decrease Factor", "Volume reduction after losses", "Risk");

		_baseVolume = Param(nameof(BaseVolume), 0.1m).SetDisplay("Base Volume", "Fallback volume", "General");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
						  .SetDisplay("Candle Type", "Type of candles", "General");

		_cooldownBars = Param(nameof(CooldownBars), 4)
			.SetDisplay("Cooldown Bars", "Completed candles to wait after closing a position", "Risk");
	}

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

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

		_filled = 0;
		_entryPrice = default;
		_stopPrice = default;
		_targetPrice = default;
		_lossCount = 0;
		_cooldownRemaining = 0;

		Array.Clear(_lows);
		Array.Clear(_highs);
	}

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

		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(ProcessCandle).Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawOwnTrades(area);
		}
	}

	private decimal GetVolume()
	{
		var lot = BaseVolume;

		if (MaximumRisk > 0m && Portfolio.CurrentValue is decimal value)
			lot = Math.Round(value * MaximumRisk / 1000m, 1);

		if (DecreaseFactor > 0m && _lossCount > 1)
			lot -= lot * _lossCount / DecreaseFactor;

		return lot < 0.1m ? 0.1m : lot;
	}

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

		if (_cooldownRemaining > 0)
			_cooldownRemaining--;

		var step = Security.PriceStep ?? 1m;
		var close = candle.ClosePrice;
		var open = candle.OpenPrice;
		var high = candle.HighPrice;
		var low = candle.LowPrice;

		// shift history
		for (var i = _lows.Length - 1; i > 0; i--)
		{
			_lows[i] = _lows[i - 1];
			_highs[i] = _highs[i - 1];
		}
		_lows[0] = low;
		_highs[0] = high;

		if (_filled < 4)
		{
			_filled++;
			return;
		}

		if (Position == 0)
		{
			if (_cooldownRemaining > 0)
				return;

			// Buy when 3 consecutive declining lows followed by a higher low (reversal)
			var buyCond = _lows[4] > _lows[3] && _lows[3] > _lows[2] && _lows[2] > _lows[1] &&
						  _lows[1] < _lows[0] && close > _highs[1] && close > open;

			// Sell when 3 consecutive rising highs followed by a lower high (reversal)
			var sellCond = _highs[4] < _highs[3] && _highs[3] < _highs[2] && _highs[2] < _highs[1] &&
						   _highs[1] > _highs[0] && close < _lows[1] && close < open;

			if (buyCond)
			{
				BuyMarket();
				_entryPrice = close;
				_stopPrice = _entryPrice - TrailingStop * step;
				_targetPrice = _entryPrice + TakeProfit * step;
			}
			else if (sellCond)
			{
				SellMarket();
				_entryPrice = close;
				_stopPrice = _entryPrice + TrailingStop * step;
				_targetPrice = _entryPrice - TakeProfit * step;
			}
		}
		else if (Position > 0)
		{
			if (close >= _targetPrice)
			{
				SellMarket();
				_lossCount = 0;
				_cooldownRemaining = CooldownBars;
			}
			else
			{
				if (close - _entryPrice > TrailingStop * step)
				{
					var newStop = close - TrailingStop * step;
					if (newStop > _stopPrice)
						_stopPrice = newStop;
				}

				if (low <= _stopPrice)
				{
					SellMarket();
					_lossCount++;
					_cooldownRemaining = CooldownBars;
				}
			}
		}
		else if (Position < 0)
		{
			if (close <= _targetPrice)
			{
				BuyMarket();
				_lossCount = 0;
				_cooldownRemaining = CooldownBars;
			}
			else
			{
				if (_entryPrice - close > TrailingStop * step)
				{
					var newStop = close + TrailingStop * step;
					if (newStop < _stopPrice)
						_stopPrice = newStop;
				}

				if (high >= _stopPrice)
				{
					BuyMarket();
					_lossCount++;
					_cooldownRemaining = CooldownBars;
				}
			}
		}
	}
}