Bollinger K-Means Cluster
Bollinger K-Means Cluster 策略基于 Bollinger K-Means Cluster。
当 Bollinger confirms trend changes 在日内(5m)数据上得到确认时触发信号,适合积极交易者。
止损依赖于 ATR 倍数以及 BollingerLength, BollingerDeviation 等参数,可根据需要调整以平衡风险与收益。
详情
- 入场条件:参见指标条件实现.
- 多空方向:双向.
- 退出条件:反向信号或止损逻辑.
- 止损:是,基于指标计算.
- 默认值:
BollingerLength = 20BollingerDeviation = 2.0mCandleType = TimeSpan.FromMinutes(5).TimeFrame()KMeansHistoryLength = 50
- 过滤器:
- 分类: 趋势跟随
- 方向: 双向
- 指标: Bollinger
- 止损: 是
- 复杂度: 中等
- 时间框架: 日内 (5m)
- 季节性: 否
- 神经网络: 否
- 背离: 否
- 风险等级: 中等
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}");
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math, Decimal
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Indicators import BollingerBands, RelativeStrengthIndex, AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
class bollinger_kmeans_strategy(Strategy):
OVERSOLD = 0
NEUTRAL = 1
OVERBOUGHT = 2
def __init__(self):
super(bollinger_kmeans_strategy, self).__init__()
self._bollinger_length = self.Param("BollingerLength", 20) \
.SetDisplay("Bollinger Length", "Length of the Bollinger Bands indicator", "Indicators")
self._bollinger_deviation = self.Param("BollingerDeviation", 2.0) \
.SetDisplay("Bollinger Deviation", "Standard deviation multiplier for Bollinger Bands", "Indicators")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Type of candles to use", "General")
self._kmeans_history_length = self.Param("KMeansHistoryLength", 50) \
.SetDisplay("K-Means History Length", "Length of history for K-Means clustering", "Clustering")
self._atr_value = 0.0
self._current_cluster_state = bollinger_kmeans_strategy.NEUTRAL
self._rsi_values = []
self._price_values = []
self._volume_values = []
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(bollinger_kmeans_strategy, self).OnReseted()
self._atr_value = 0.0
self._current_cluster_state = bollinger_kmeans_strategy.NEUTRAL
self._rsi_values = []
self._price_values = []
self._volume_values = []
def OnStarted2(self, time):
super(bollinger_kmeans_strategy, self).OnStarted2(time)
bollinger = BollingerBands()
bollinger.Length = int(self._bollinger_length.Value)
bollinger.Width = Decimal(self._bollinger_deviation.Value)
rsi = RelativeStrengthIndex()
rsi.Length = 14
atr = AverageTrueRange()
atr.Length = 14
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(bollinger, rsi, atr, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, bollinger)
self.DrawOwnTrades(area)
self.StartProtection(
Unit(2, UnitTypes.Percent),
Unit(2, UnitTypes.Percent)
)
def _process_candle(self, candle, bollinger_value, rsi_value, atr_value):
if candle.State != CandleStates.Finished:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
up_band = bollinger_value.UpBand
low_band = bollinger_value.LowBand
moving_avg = bollinger_value.MovingAverage
if up_band is None or low_band is None or moving_avg is None:
return
upper = float(up_band)
middle = float(moving_avg)
lower = float(low_band)
rsi = float(rsi_value)
self._atr_value = float(atr_value)
self._update_cluster_data(candle, rsi)
self._calculate_clusters()
price_step = 0.0
if self.Security is not None and self.Security.PriceStep is not None:
price_step = float(self.Security.PriceStep)
band_buffer = max(self._atr_value * 0.1, price_step)
close_price = float(candle.ClosePrice)
if close_price < lower - band_buffer and self._current_cluster_state == bollinger_kmeans_strategy.OVERSOLD and self.Position <= 0:
self.BuyMarket(self.Volume)
elif close_price > upper + band_buffer and self._current_cluster_state == bollinger_kmeans_strategy.OVERBOUGHT and self.Position >= 0:
self.SellMarket(self.Volume + Math.Abs(self.Position))
elif self.Position > 0 and close_price > middle:
self.SellMarket(self.Position)
elif self.Position < 0 and close_price < middle:
self.BuyMarket(Math.Abs(self.Position))
def _update_cluster_data(self, candle, rsi):
kmeans_len = int(self._kmeans_history_length.Value)
self._price_values.append(float(candle.ClosePrice))
self._rsi_values.append(rsi)
self._volume_values.append(float(candle.TotalVolume))
while len(self._price_values) > kmeans_len:
self._price_values.pop(0)
self._rsi_values.pop(0)
self._volume_values.pop(0)
def _calculate_clusters(self):
kmeans_len = int(self._kmeans_history_length.Value)
if len(self._price_values) < kmeans_len:
return
normalized_rsi = self._rsi_values[-1] / 100.0
min_price = min(self._price_values)
max_price = max(self._price_values)
normalized_price = 0.5
if max_price != min_price:
normalized_price = (self._price_values[-1] - min_price) / (max_price - min_price)
if normalized_rsi < 0.3 and normalized_price < 0.3:
self._current_cluster_state = bollinger_kmeans_strategy.OVERSOLD
elif normalized_rsi > 0.7 and normalized_price > 0.7:
self._current_cluster_state = bollinger_kmeans_strategy.OVERBOUGHT
else:
self._current_cluster_state = bollinger_kmeans_strategy.NEUTRAL
def CreateClone(self):
return bollinger_kmeans_strategy()