GitHub で見る

Bollinger K-Means Cluster

The Bollinger K-Means Cluster strategy is built around Bollinger K-Means Cluster.

Signals trigger when Bollinger confirms trend changes on intraday (5m) data. This makes the method suitable for active traders.

Stops rely on ATR multiples and factors like BollingerLength, BollingerDeviation. Adjust these defaults to balance risk and reward.

Details

  • Entry Criteria: see implementation for indicator conditions.
  • Long/Short: Both directions.
  • Exit Criteria: opposite signal or stop logic.
  • Stops: Yes, using indicator-based calculations.
  • Default Values:
    • BollingerLength = 20
    • BollingerDeviation = 2.0m
    • CandleType = TimeSpan.FromMinutes(5).TimeFrame()
    • KMeansHistoryLength = 50
  • Filters:
    • Category: Trend following
    • Direction: Both
    • Indicators: Bollinger
    • Stops: Yes
    • Complexity: Intermediate
    • Timeframe: Intraday (5m)
    • Seasonality: No
    • Neural Networks: No
    • Divergence: No
    • Risk Level: Medium
using System;
using System.Collections.Generic;

using Ecng.Common;
using Ecng.Serialization;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Bollinger Bands with K-Means clustering strategy.
/// Uses Bollinger Bands indicator along with a simple K-Means clustering algorithm
/// to identify overbought/oversold conditions.
/// </summary>
public class BollingerKMeansStrategy : Strategy
{
	private readonly StrategyParam<int> _bollingerLength;
	private readonly StrategyParam<decimal> _bollingerDeviation;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _kMeansHistoryLength;

	private BollingerBands _bollinger;
	private decimal _atrValue;

	// Cluster state tracking
	private enum ClusterStates
	{
		Oversold,
		Neutral,
		Overbought
	}

	private ClusterStates _currentClusterState = ClusterStates.Neutral;
	private readonly List<decimal> _rsiValues = [];
	private readonly List<decimal> _priceValues = [];
	private readonly List<decimal> _volumeValues = [];

	private RelativeStrengthIndex _rsi;
	private AverageTrueRange _atr;

	/// <summary>
	/// Bollinger Bands period.
	/// </summary>
	public int BollingerLength
	{
		get => _bollingerLength.Value;
		set => _bollingerLength.Value = value;
	}

	/// <summary>
	/// Bollinger Bands standard deviation multiplier.
	/// </summary>
	public decimal BollingerDeviation
	{
		get => _bollingerDeviation.Value;
		set => _bollingerDeviation.Value = value;
	}

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

	/// <summary>
	/// Length of history for K-Means clustering.
	/// </summary>
	public int KMeansHistoryLength
	{
		get => _kMeansHistoryLength.Value;
		set => _kMeansHistoryLength.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="BollingerKMeansStrategy"/>.
	/// </summary>
	public BollingerKMeansStrategy()
	{
		_bollingerLength = Param(nameof(BollingerLength), 20)
		.SetDisplay("Bollinger Length", "Length of the Bollinger Bands indicator", "Indicators")
		
		.SetOptimize(10, 50, 5);

		_bollingerDeviation = Param(nameof(BollingerDeviation), 2.0m)
		.SetDisplay("Bollinger Deviation", "Standard deviation multiplier for Bollinger Bands", "Indicators")
		
		.SetOptimize(1.0m, 3.0m, 0.5m);

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
		.SetDisplay("Candle Type", "Type of candles to use", "General");

		_kMeansHistoryLength = Param(nameof(KMeansHistoryLength), 50)
		.SetDisplay("K-Means History Length", "Length of history for K-Means clustering", "Clustering")
		
		.SetOptimize(30, 100, 10);
	}

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

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

		_atrValue = default;
_currentClusterState = ClusterStates.Neutral;
		_rsiValues.Clear();
		_priceValues.Clear();
		_volumeValues.Clear();

		_bollinger?.Reset();
		_rsi?.Reset();
		_atr?.Reset();
	}

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

		// Create indicators
		_bollinger = new BollingerBands
		{
			Length = BollingerLength,
			Width = BollingerDeviation
		};

		_rsi = new RelativeStrengthIndex
		{
			Length = 14
		};

		_atr = new AverageTrueRange
		{
			Length = 14
		};

		// Create and initialize subscription
		var subscription = SubscribeCandles(CandleType);

		subscription
		.BindEx(
		_bollinger, 
		_rsi,
		_atr,
		ProcessCandle)
		.Start();

		// Setup chart visualization if available
		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _bollinger);
			DrawOwnTrades(area);
		}

		// Setup position protection
		StartProtection(
		new Unit(2, UnitTypes.Percent), 
		new Unit(2, UnitTypes.Percent)
		);
	}

	private void ProcessCandle(ICandleMessage candle, IIndicatorValue bollingerValue, IIndicatorValue rsiValue, IIndicatorValue atrValue)
	{
		// Skip unfinished candles
		if (candle.State != CandleStates.Finished)
		return;

		// Check if strategy is ready to trade
		if (!IsFormedAndOnlineAndAllowTrading())
		return;

		if (bollingerValue is not IBollingerBandsValue bollingerTyped ||
			bollingerTyped.UpBand is not decimal bollingerUpper ||
			bollingerTyped.MovingAverage is not decimal bollingerMiddle ||
			bollingerTyped.LowBand is not decimal bollingerLower)
			return;

		var rsi = rsiValue.ToDecimal();
		_atrValue = atrValue.ToDecimal();

		// Update data for clustering
		UpdateClusterData(candle, rsi);

		// Calculate K-Means clusters and determine market state
		CalculateClusters();

		// Use ATR as a minimal band breach filter to avoid noise around the envelopes.
		var bandBuffer = Math.Max(_atrValue * 0.1m, Security?.PriceStep ?? 0m);

		// Trading logic
		if (candle.ClosePrice < bollingerLower - bandBuffer && _currentClusterState == ClusterStates.Oversold && Position <= 0)
		{
			// Buy signal - price below lower band and in oversold cluster
			BuyMarket(Volume);
			LogInfo($"Buy Signal: Price below lower band ({bollingerLower:F2}) in oversold cluster");
		}
		else if (candle.ClosePrice > bollingerUpper + bandBuffer && _currentClusterState == ClusterStates.Overbought && Position >= 0)
		{
			// Sell signal - price above upper band and in overbought cluster
			SellMarket(Volume + Math.Abs(Position));
			LogInfo($"Sell Signal: Price above upper band ({bollingerUpper:F2}) in overbought cluster");
		}
		else if (Position > 0 && candle.ClosePrice > bollingerMiddle)
		{
			// Exit long position when price returns to middle band
			SellMarket(Position);
			LogInfo($"Exit Long: Price returned to middle band ({bollingerMiddle:F2})");
		}
		else if (Position < 0 && candle.ClosePrice < bollingerMiddle)
		{
			// Exit short position when price returns to middle band
			BuyMarket(Math.Abs(Position));
			LogInfo($"Exit Short: Price returned to middle band ({bollingerMiddle:F2})");
		}
	}

	private void UpdateClusterData(ICandleMessage candle, decimal rsi)
	{
		// Add current values to the data series
		_priceValues.Add(candle.ClosePrice);
		_rsiValues.Add(rsi);
		_volumeValues.Add(candle.TotalVolume);

		// Maintain the desired history length
		while (_priceValues.Count > KMeansHistoryLength)
		{
			_priceValues.RemoveAt(0);
			_rsiValues.RemoveAt(0);
			_volumeValues.RemoveAt(0);
		}
	}

	private void CalculateClusters()
	{
		// Only perform clustering when we have enough data
		if (_priceValues.Count < KMeansHistoryLength)
		return;

		// Normalize the data (simple min-max normalization)
		var priceValues = _priceValues.ToArray();
		var rsiValues = _rsiValues.ToArray();
		var normalizedRsi = rsiValues[^1] / 100m;  // RSI is already 0-100

		// Find min/max for price normalization
		decimal? minPrice = null;
		decimal? maxPrice = null;

		foreach (var price in priceValues)
		{
			if (minPrice == null || price < minPrice.Value)
			minPrice = price;
			if (maxPrice == null || price > maxPrice.Value)
			maxPrice = price;
		}

		// Normalize the last price
		decimal normalizedPrice = 0.5m;
		if (minPrice.HasValue && maxPrice.HasValue && maxPrice.Value != minPrice.Value)
		{
			var priceRange = maxPrice.Value - minPrice.Value;
			normalizedPrice = (priceValues[^1] - minPrice.Value) / priceRange;
		}

		// Simple rules-based clustering (simplified K-means approximation)
		// Oversold: Low RSI (< 30) and price near bottom of range
		// Overbought: High RSI (> 70) and price near top of range
		// Neutral: Everything else

		if (normalizedRsi < 0.3m && normalizedPrice < 0.3m)
		{
			_currentClusterState = ClusterStates.Oversold;
		}
		else if (normalizedRsi > 0.7m && normalizedPrice > 0.7m)
		{
			_currentClusterState = ClusterStates.Overbought;
		}
		else
		{
			_currentClusterState = ClusterStates.Neutral;
		}

		LogInfo($"Cluster State: {_currentClusterState}, Normalized RSI: {normalizedRsi:F2}, Normalized Price: {normalizedPrice:F2}");
	}
}