View on GitHub

KA-Gold Bot Strategy

The KA-Gold Bot Strategy is a high-level StockSharp conversion of the original MetaTrader 4 "KA-Gold Bot" expert advisor. It combines a Keltner-style channel with trend filters and aggressive risk management that includes fixed stop-loss, take-profit, and multi-stage trailing protection. Trading is allowed only during a configurable intraday window and new positions are blocked when the live spread exceeds a threshold.

Trading Logic

  1. Indicator preparation

    • An exponential moving average (EMA) with length KeltnerPeriod builds the channel midline.
    • A simple moving average of candle ranges (high minus low) with the same period estimates the channel half-width.
    • Short-term and long-term exponential moving averages (EmaShortPeriod and EmaLongPeriod) track fast momentum and the higher-timeframe trend respectively.
    • All indicator values are recorded for the two most recent completed candles to mirror the MT4 shift-based calculations.
  2. Entry conditions

    • Calculations run only when the current candle closes and the strategy is connected to the market with trading permissions granted.
    • The channel upper and lower bands are derived by adding/subtracting the averaged range from the EMA midline for both the previous (shift = 1) and the earlier (shift = 2) candle.
    • Long setup:
      • The previous close breaks above the most recent upper band.
      • The same close is above the long EMA, confirming an uptrend.
      • The short EMA crosses from below the older upper band to above the latest one (EMA_short[2] < Upper[2] and EMA_short[1] > Upper[1]).
    • Short setup:
      • The previous close falls below the recent lower band.
      • The same close is below the long EMA, confirming a downtrend.
      • The short EMA crosses from above the older lower band to below the latest one (EMA_short[2] > Lower[2] and EMA_short[1] < Lower[1]).
    • Only one position is allowed at a time. If a trade is already open, the signal is ignored.
  3. Timing and spread filters

    • When UseTimeFilter is enabled, new entries are restricted to the [StartHour:StartMinute, EndHour:EndMinute) window using the exchange-local time. Overnight sessions are supported if the end time is earlier than the start time.
    • Level-1 quote subscriptions keep track of the best bid/ask prices. Before placing an order, the strategy converts the current spread into instrument points and compares it against MaxSpreadPoints. Orders are skipped, with logging, whenever the threshold is breached.
  4. Risk management

    • Position sizing defaults to FixedVolume. If UseRiskPercent is true, the trade size is recalculated from the portfolio equity as RiskPercent% / (riskPips * PipValue), where riskPips equals StopLossPips (fallback to TrailingStopPips when no fixed stop is defined). The final result is normalized to the instrument volume step and clamped between the minimum and maximum exchange limits.
    • When a long position is opened, the strategy stores:
      • Initial stop-loss at entry - StopLossPips * pipSize (if defined).
      • Initial take-profit at entry + TakeProfitPips * pipSize (if defined).
      • Trailing state flags, which reset the short-side trackers.
    • Short trades mirror the same logic with inverted price directions.
  5. Trailing protection

    • Live bid/ask updates feed two trailing engines:
      • Once the floating profit exceeds TrailingTriggerPips, trailing becomes active.
      • The trailing stop is positioned TrailingStopPips away from the current favourable price and is only advanced when the move exceeds TrailingStopPips + TrailingStepPips beyond the previous stop level.
      • For long positions the trailing stop never drops below the original protective stop, and for shorts it never rises above it.
    • Exit monitoring is performed both on incoming quotes and on finished candles:
      • A position is closed immediately when price reaches the active stop (original or trailing).
      • Profits are also locked once the candle’s high/low touches the stored take-profit level.
    • After closing a position the protection state is fully reset to avoid stale data.

Parameters

Parameter Description Default
CandleType Data type describing the execution timeframe. 1-minute time frame
KeltnerPeriod Period for the EMA midline and the range average of the channel. 50
EmaShortPeriod Fast EMA length used for crossover confirmation. 10
EmaLongPeriod Slow EMA length acting as trend filter. 200
FixedVolume Fallback order volume when percentage sizing is disabled. 1
UseRiskPercent Enable percentage-based position sizing. true
RiskPercent Percentage of equity risked per trade. 1
StopLossPips Distance of the fixed stop-loss in pips (0 disables). 500
TakeProfitPips Distance of the fixed take-profit in pips (0 disables). 500
TrailingTriggerPips Profit in pips required to activate the trailing stop. 300
TrailingStopPips Distance between price and trailing stop once active. 300
TrailingStepPips Minimum additional profit (in pips) before the trailing stop is advanced. 100
UseTimeFilter Toggle for the trading session filter. true
StartHour / StartMinute Session start in exchange-local time. 02:30
EndHour / EndMinute Session end in exchange-local time. 21:00
MaxSpreadPoints Maximum allowed spread in instrument points (0 disables the check). 65
PipValue Monetary value of one pip, used for risk-based position sizing. 1

Additional Notes

  • Pip conversion follows the exchange instrument decimals: a five-digit quote (odd number of decimals) multiplies the price step by 10 to emulate the MT4 pip size logic.
  • The strategy subscribes to both candles and level-1 data but does not register additional indicators on the chart, complying with the high-level API guidelines.
  • Protective exits rely on market orders issued by the strategy; no separate stop or limit orders are placed on the exchange.
  • Python support is not included in this delivery, matching the original request.
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();
	}
}