Открыть на GitHub

Стратегия Lucky

Стратегия Lucky — это скальпер на пробой, который отслеживает резкие изменения между лучшими ценами спроса и предложения. Покупка выполняется, когда цена ask подпрыгивает вверх минимум на заданное количество пунктов, а продажа — когда цена bid падает на ту же величину. Позиции закрываются сразу после появления прибыли или при достижении допустимого неблагоприятного отклонения.

Данные и исполнение

  • Тип данных: необходим поток котировок Level 1 для получения лучших bid/ask.
  • Тип заявок: используются только рыночные ордера, чтобы успевать на быстрые импульсы.
  • Режим позиции: стратегия создавалась под хеджинговые счета, но работает и с неттингом, суммируя чистую позицию.

Параметры

  • Shift points – минимальное количество пунктов между последовательными котировками, которое запускает новую сделку. Большое значение отсекает шум, малое реагирует даже на мелкие скачки.
  • Limit points – максимально допустимый неблагоприятный ход (в пунктах) перед принудительным закрытием позиции. Автоматически масштабируется с учетом шага цены инструмента.
  • Reverse mode – инвертирует направление сделок. При включении рост ask приводит к продаже, а падение bid — к покупке.

Логика торговли

  1. Инициализация
    • Переводит параметр «в пунктах» в абсолютные ценовые расстояния с учетом шага цены инструмента.
    • Подписывается на поток Level 1 и обнуляет буферы предыдущих bid/ask.
  2. Вход
    • Если ask вырос как минимум на величину shift относительно предыдущего значения, открывается длинная позиция (или короткая при включенном реверсе).
    • Если bid упал на ту же величину, открывается короткая позиция (или длинная в режиме реверса).
  3. Размер позиции
    • Базовый объем берётся из свойства Volume стратегии.
    • Если доступна стоимость портфеля, имитирует логику MetaTrader: выделяет примерно FreeMargin / 10 000, округляя до десятых лота, чтобы большие счета торговали большим объёмом.
  4. Выход
    • Лонг закрывается при превышении bid средней цены входа или если ask опустился ниже неё на величину Limit.
    • Шорт закрывается при падении ask ниже цены входа либо при росте bid выше неё на Limit.

Примечания и рекомендации

  • Лучше всего работает на ликвидных валютных парах и индексных CFD с заметными скачками котировок.
  • Добавьте дополнительные правила риск-менеджмента (например, общепортфельный стоп) перед торговлей на реальном счёте.
  • Переключатель Reverse mode позволяет быстро превратить пробойную систему в контртрендовую без изменений других настроек.
  • Из-за реакции на каждое подходящее обновление котировок стоит ограничить поток данных или увеличить параметр shift на «шумных» инструментах.
using System;
using System.Collections.Generic;

using Ecng.Common;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Breakout strategy that reacts to fast price shifts (candle-to-candle high/low jumps)
/// and closes trades on profit target or adverse move (stop loss).
/// </summary>
public class LuckyStrategy : Strategy
{
	private readonly StrategyParam<decimal> _shiftPct;
	private readonly StrategyParam<decimal> _profitPct;
	private readonly StrategyParam<decimal> _stopPct;
	private readonly StrategyParam<bool> _reverse;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _entryPrice;
	private decimal? _previousHigh;
	private decimal? _previousLow;
	private bool _isReady;

	/// <summary>
	/// Minimum percentage shift in high/low to trigger entry.
	/// </summary>
	public decimal ShiftPct
	{
		get => _shiftPct.Value;
		set => _shiftPct.Value = value;
	}

	/// <summary>
	/// Profit target as percentage of entry price.
	/// </summary>
	public decimal ProfitPct
	{
		get => _profitPct.Value;
		set => _profitPct.Value = value;
	}

	/// <summary>
	/// Stop loss as percentage of entry price.
	/// </summary>
	public decimal StopPct
	{
		get => _stopPct.Value;
		set => _stopPct.Value = value;
	}

	/// <summary>
	/// Switch to invert the trading direction.
	/// </summary>
	public bool Reverse
	{
		get => _reverse.Value;
		set => _reverse.Value = value;
	}

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

	/// <summary>
	/// Initializes the strategy parameters.
	/// </summary>
	public LuckyStrategy()
	{
		_shiftPct = Param(nameof(ShiftPct), 1.5m)
			.SetDisplay("Shift %", "Minimum percentage shift in high/low to trigger entry", "Trading")
			.SetOptimize(0.5m, 3.0m, 0.5m);

		_profitPct = Param(nameof(ProfitPct), 2.0m)
			.SetDisplay("Profit %", "Profit target as percentage of entry price", "Risk management")
			.SetOptimize(1.0m, 5.0m, 0.5m);

		_stopPct = Param(nameof(StopPct), 3.0m)
			.SetDisplay("Stop %", "Stop loss as percentage of entry price", "Risk management")
			.SetOptimize(1.0m, 5.0m, 0.5m);

		_reverse = Param(nameof(Reverse), false)
			.SetDisplay("Reverse mode", "Invert the direction of new trades", "Trading");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle type", "Candle timeframe", "General");
	}

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

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

		_previousHigh = null;
		_previousLow = null;
		_entryPrice = 0m;
		_isReady = false;
	}

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

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

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

		var high = candle.HighPrice;
		var low = candle.LowPrice;
		var close = candle.ClosePrice;

		if (!_isReady)
		{
			_previousHigh = high;
			_previousLow = low;
			_isReady = true;
			return;
		}

		// Try to close existing position first
		TryClosePosition(close);

		// Only open new positions if flat
		if (Position == 0 && _previousHigh.HasValue && _previousLow.HasValue)
		{
			var prevH = _previousHigh.Value;
			var prevL = _previousLow.Value;

			// Check for upward breakout: high moved up sharply relative to previous high
			if (prevH > 0m && (high - prevH) / prevH * 100m >= ShiftPct)
			{
				if (Reverse)
					OpenShort(close);
				else
					OpenLong(close);
			}
			// Check for downward breakdown: low moved down sharply relative to previous low
			else if (prevL > 0m && (prevL - low) / prevL * 100m >= ShiftPct)
			{
				if (Reverse)
					OpenLong(close);
				else
					OpenShort(close);
			}
		}

		_previousHigh = high;
		_previousLow = low;
	}

	private void OpenLong(decimal price)
	{
		BuyMarket(Volume);
		_entryPrice = price;
	}

	private void OpenShort(decimal price)
	{
		SellMarket(Volume);
		_entryPrice = price;
	}

	private void TryClosePosition(decimal currentPrice)
	{
		if (Position == 0 || _entryPrice <= 0m)
			return;

		if (Position > 0)
		{
			var pctChange = (currentPrice - _entryPrice) / _entryPrice * 100m;

			if (pctChange >= ProfitPct || pctChange <= -StopPct)
				SellMarket(Position);
		}
		else if (Position < 0)
		{
			var pctChange = (_entryPrice - currentPrice) / _entryPrice * 100m;

			if (pctChange >= ProfitPct || pctChange <= -StopPct)
				BuyMarket(Math.Abs(Position));
		}
	}
}