Открыть на GitHub

Стратегия KA-Gold Bot

KA-Gold Bot — высокоуровневая портированная версия MT4-советника «KA-Gold Bot» для платформы StockSharp. Торговая логика сочетает каналы Кельтнера, двойную EMA-фильтрацию тренда и многоступенчатое управление рисками: фиксированные стоп/тейк, ступенчатый трейлинг и контроль спреда по потоковым котировкам. Новые сделки разрешены только в заданный внутридневной интервал и при приемлемом спреде.

Логика работы

  1. Подготовка индикаторов

    • Экспоненциальная скользящая средняя длиной KeltnerPeriod формирует центральную линию канала.
    • Простая скользящая средняя диапазона свечи (High − Low) с тем же периодом задаёт полуширину канала.
    • EMA с периодами EmaShortPeriod и EmaLongPeriod отражают короткую динамику и долгосрочный тренд.
    • Значения индикаторов сохраняются для двух последних завершённых свечей, что повторяет адресацию shift из оригинального MQL-кода.
  2. Условия входа

    • Сигналы рассчитываются только после закрытия свечи при активном соединении и разрешённой торговле.
    • Верхняя и нижняя границы для shift = 1 и shift = 2 получаются суммой/разностью средней линии и усреднённого диапазона.
    • Покупка:
      • Предыдущая цена закрытия пробивает верхнюю границу;
      • Та же цена находится выше медленной EMA, что подтверждает восходящий тренд;
      • Быстрая EMA пересекает верхнюю границу снизу вверх между двумя последними свечами (EMA_short[2] < Upper[2] и EMA_short[1] > Upper[1]).
    • Продажа:
      • Предыдущая цена закрытия опускается ниже нижней границы;
      • Цена находится ниже медленной EMA, что подтверждает нисходящий тренд;
      • Быстрая EMA пересекает нижнюю границу сверху вниз (EMA_short[2] > Lower[2] и EMA_short[1] < Lower[1]).
    • Одновременно допускается только одна позиция: если она уже открыта, новый сигнал игнорируется.
  3. Фильтры по времени и спреду

    • При UseTimeFilter = true сделки открываются лишь в интервале [StartHour:StartMinute, EndHour:EndMinute) по локальному времени площадки. Если конец раньше начала, интервал считается ночным (через сутки).
    • Подписка на Level-1 позволяет отслеживать лучшие bid/ask. Перед отправкой заявки спред пересчитывается в пункты (PriceStep) и сравнивается с MaxSpreadPoints. Превышение лимита приводит к отказу от сделки и записи в лог.
  4. Управление позицией

    • По умолчанию используется объём FixedVolume. При UseRiskPercent = true размер позиции рассчитывается как RiskPercent% от капитала, делённый на riskPips * PipValue, где riskPips — значение StopLossPips (либо TrailingStopPips, если фиксированный стоп отключён). Результат приводится к шагу объёма и ограничивается биржевыми минимумами/максимумами.
    • Для длинных позиций фиксируются:
      • Стоп-лосс entry - StopLossPips * pipSize (если задан);
      • Тейк-профит entry + TakeProfitPips * pipSize (если задан);
      • Состояние трейлинга, при этом короткие защиты сбрасываются.
    • Короткие позиции настраиваются симметрично с обратным знаком.
  5. Трейлинг-стоп

    • Движение трейлинга контролируется потоковыми котировками:
      • После достижения прибыли TrailingTriggerPips трейлинг активируется;
      • Новый уровень выставляется на расстоянии TrailingStopPips от текущей выгодной цены и переносится только если прибыль превысила прошлый стоп минимум на TrailingStopPips + TrailingStepPips;
      • Для лонга трейлинг никогда не опускается ниже первоначального стопа, для шорта — не поднимается выше него.
    • Контроль выхода выполняется как по поступающим котировкам, так и по закрытым свечам:
      • При достижении стоп-уровня позиция закрывается рыночной заявкой;
      • При достижении тейк-профита свечой позиция также закрывается.
    • После закрытия позиции все защитные уровни и флаги очищаются.

Параметры

Параметр Описание По умолчанию
CandleType Тип данных свечи для исполнения. Таймфрейм 1 минута
KeltnerPeriod Период EMA и усреднения диапазона канала. 50
EmaShortPeriod Период быстрой EMA. 10
EmaLongPeriod Период медленной EMA, фильтр тренда. 200
FixedVolume Базовый объём при отключённом риск-менеджменте по проценту. 1
UseRiskPercent Включить расчёт объёма по проценту капитала. true
RiskPercent Доля капитала, которую допускается рискнуть в сделке. 1
StopLossPips Размер фиксированного стоп-лосса в пунктах (0 — отключить). 500
TakeProfitPips Размер фиксированного тейк-профита в пунктах (0 — отключить). 500
TrailingTriggerPips Прибыль в пунктах для активации трейлинга. 300
TrailingStopPips Расстояние трейлинг-стопа от цены. 300
TrailingStepPips Минимальное дополнительное улучшение прибыли перед переносом трейлинга. 100
UseTimeFilter Включение фильтра торговой сессии. true
StartHour / StartMinute Время начала сессии (локальное). 02:30
EndHour / EndMinute Время окончания сессии (локальное). 21:00
MaxSpreadPoints Максимально допустимый спред в шагах цены (0 — без проверки). 65
PipValue Денежная стоимость одного пункта для риск-менеджмента. 1

Дополнительные замечания

  • Пересчёт pip основан на количестве знаков инструмента: для пятизначных котировок (нечётное число десятичных) цена шага умножается на 10, что соответствует логике MT4.
  • Стратегия подписывается на свечи и Level-1, но не добавляет индикаторы в Strategy.Indicators, соблюдая требования высокоуровневого API.
  • Защитные выходы выполняются рыночными ордерами со стороны стратегии, биржевые стоп-ордера не выставляются.
  • По запросу Python-версия не создавалась.
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>
/// Simplified from "KA Gold Bot" MetaTrader expert.
/// Uses Keltner channel (EMA + ATR-based bands) with EMA crossover for entries.
/// Buys when short EMA crosses above long EMA and close is above Keltner center.
/// Sells on reverse conditions.
/// </summary>
public class KaGoldBotStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _keltnerPeriod;
	private readonly StrategyParam<int> _emaShortPeriod;
	private readonly StrategyParam<int> _emaLongPeriod;

	private ExponentialMovingAverage _emaShort;
	private ExponentialMovingAverage _emaLong;

	// Manual ATR-like range average for Keltner
	private readonly Queue<decimal> _rangeQueue = new();
	private decimal _rangeSum;
	private ExponentialMovingAverage _emaKeltner;

	private decimal? _prevEmaShort;
	private decimal? _prevEmaLong;

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

	public int KeltnerPeriod
	{
		get => _keltnerPeriod.Value;
		set => _keltnerPeriod.Value = value;
	}

	public int EmaShortPeriod
	{
		get => _emaShortPeriod.Value;
		set => _emaShortPeriod.Value = value;
	}

	public int EmaLongPeriod
	{
		get => _emaLongPeriod.Value;
		set => _emaLongPeriod.Value = value;
	}

	public KaGoldBotStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe", "General");

		_keltnerPeriod = Param(nameof(KeltnerPeriod), 20)
			.SetGreaterThanZero()
			.SetDisplay("Keltner Period", "EMA period for Keltner channel center", "Indicators");

		_emaShortPeriod = Param(nameof(EmaShortPeriod), 10)
			.SetGreaterThanZero()
			.SetDisplay("EMA Short Period", "Short EMA for crossover signal", "Indicators");

		_emaLongPeriod = Param(nameof(EmaLongPeriod), 50)
			.SetGreaterThanZero()
			.SetDisplay("EMA Long Period", "Long EMA for crossover signal", "Indicators");
	}

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

		_emaShort = new ExponentialMovingAverage { Length = EmaShortPeriod };
		_emaLong = new ExponentialMovingAverage { Length = EmaLongPeriod };
		_emaKeltner = new ExponentialMovingAverage { Length = KeltnerPeriod };

		_rangeQueue.Clear();
		_rangeSum = 0;
		_prevEmaShort = null;
		_prevEmaLong = null;

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

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

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

		var close = candle.ClosePrice;

		// Process Keltner EMA manually
		var keltnerInput = new DecimalIndicatorValue(_emaKeltner, close, candle.OpenTime);
		var keltnerResult = _emaKeltner.Process(keltnerInput);
		var emaKeltnerValue = keltnerResult.IsEmpty ? close : keltnerResult.GetValue<decimal>();

		// Calculate range average (manual SMA of high-low) for Keltner bands
		var range = candle.HighPrice - candle.LowPrice;
		_rangeQueue.Enqueue(range);
		_rangeSum += range;
		while (_rangeQueue.Count > KeltnerPeriod)
			_rangeSum -= _rangeQueue.Dequeue();

		if (_prevEmaShort == null || _prevEmaLong == null)
		{
			_prevEmaShort = emaShortValue;
			_prevEmaLong = emaLongValue;
			return;
		}

		// Keltner bands
		var rangeAvg = _rangeQueue.Count > 0 ? _rangeSum / _rangeQueue.Count : 0;
		var upper = emaKeltnerValue + rangeAvg;
		var lower = emaKeltnerValue - rangeAvg;

		// Buy: short EMA crosses above long EMA and close above Keltner center
		var buySignal = _prevEmaShort.Value <= _prevEmaLong.Value && emaShortValue > emaLongValue
			&& close > emaKeltnerValue;

		// Sell: short EMA crosses below long EMA and close below Keltner center
		var sellSignal = _prevEmaShort.Value >= _prevEmaLong.Value && emaShortValue < emaLongValue
			&& close < emaKeltnerValue;

		if (buySignal)
		{
			if (Position < 0)
				BuyMarket();
			if (Position <= 0)
				BuyMarket();
		}
		else if (sellSignal)
		{
			if (Position > 0)
				SellMarket();
			if (Position >= 0)
				SellMarket();
		}

		_prevEmaShort = emaShortValue;
		_prevEmaLong = emaLongValue;
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		_emaShort = null;
		_emaLong = null;
		_emaKeltner = null;
		_rangeQueue.Clear();
		_rangeSum = 0;
		_prevEmaShort = null;
		_prevEmaLong = null;

		base.OnReseted();
	}
}