using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
using StockSharp.Algo;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Strategy based on EMA, CCI, and Stochastic oscillator signals.
/// Converts the original Kloss expert advisor from MetaTrader 4 to StockSharp.
/// </summary>
public class KlossSimpleStrategy : Strategy
{
private readonly StrategyParam<decimal> _orderVolume;
private readonly StrategyParam<int> _maPeriod;
private readonly StrategyParam<int> _cciPeriod;
private readonly StrategyParam<decimal> _cciLevel;
private readonly StrategyParam<int> _stochasticKPeriod;
private readonly StrategyParam<int> _stochasticDPeriod;
private readonly StrategyParam<int> _stochasticSmooth;
private readonly StrategyParam<decimal> _stochasticLevel;
private readonly StrategyParam<int> _maxOrders;
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<decimal> _takeProfitPoints;
private readonly StrategyParam<decimal> _riskPercentage;
private readonly StrategyParam<DataType> _candleType;
private ExponentialMovingAverage _ema;
private CommodityChannelIndex _cci;
private StochasticOscillator _stochastic;
private decimal? _previousCci;
/// <summary>
/// Initializes a new instance of the <see cref="KlossSimpleStrategy"/> class.
/// </summary>
public KlossSimpleStrategy()
{
_orderVolume = Param(nameof(OrderVolume), 0.1m)
.SetGreaterThanZero()
.SetDisplay("Volume", "Base order volume", "Trading");
_maPeriod = Param(nameof(MaPeriod), 5)
.SetGreaterThanZero()
.SetDisplay("EMA Period", "Length of the exponential moving average", "Indicators")
.SetOptimize(3, 20, 1);
_cciPeriod = Param(nameof(CciPeriod), 10)
.SetGreaterThanZero()
.SetDisplay("CCI Period", "Length of the commodity channel index", "Indicators")
.SetOptimize(5, 30, 5);
_cciLevel = Param(nameof(CciLevel), 200m)
.SetGreaterThanZero()
.SetDisplay("CCI Level", "Distance from zero to trigger signals", "Indicators")
.SetOptimize(50m, 200m, 10m);
_stochasticKPeriod = Param(nameof(StochasticKPeriod), 5)
.SetGreaterThanZero()
.SetDisplay("Stochastic %K", "Period of the %K line", "Indicators")
.SetOptimize(3, 20, 1);
_stochasticDPeriod = Param(nameof(StochasticDPeriod), 3)
.SetGreaterThanZero()
.SetDisplay("Stochastic %D", "Period of the %D line", "Indicators")
.SetOptimize(1, 10, 1);
_stochasticSmooth = Param(nameof(StochasticSmooth), 3)
.SetGreaterThanZero()
.SetDisplay("Stochastic Smooth", "Smoothing factor for %K", "Indicators")
.SetOptimize(1, 10, 1);
_stochasticLevel = Param(nameof(StochasticLevel), 30m)
.SetGreaterThanZero()
.SetDisplay("Stochastic Level", "Distance from 50 to trigger signals", "Indicators")
.SetOptimize(10m, 40m, 5m);
_maxOrders = Param(nameof(MaxOrders), 1)
.SetNotNegative()
.SetDisplay("Max Orders", "Maximum number of positions per direction", "Trading");
_stopLossPoints = Param(nameof(StopLossPoints), 0m)
.SetNotNegative()
.SetDisplay("Stop Loss (pts)", "Stop loss distance in points", "Risk");
_takeProfitPoints = Param(nameof(TakeProfitPoints), 0m)
.SetNotNegative()
.SetDisplay("Take Profit (pts)", "Take profit distance in points", "Risk");
_riskPercentage = Param(nameof(RiskPercentage), 10m)
.SetNotNegative()
.SetDisplay("Risk %", "Portfolio percentage for dynamic position sizing", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Primary candle series for calculations", "General");
}
/// <summary>Base order volume for new entries.</summary>
public decimal OrderVolume
{
get => _orderVolume.Value;
set => _orderVolume.Value = value;
}
/// <summary>EMA length used to filter price action.</summary>
public int MaPeriod
{
get => _maPeriod.Value;
set => _maPeriod.Value = value;
}
/// <summary>CCI period for momentum detection.</summary>
public int CciPeriod
{
get => _cciPeriod.Value;
set => _cciPeriod.Value = value;
}
/// <summary>Absolute level that CCI must exceed to signal an entry.</summary>
public decimal CciLevel
{
get => _cciLevel.Value;
set => _cciLevel.Value = value;
}
/// <summary>Stochastic %K period.</summary>
public int StochasticKPeriod
{
get => _stochasticKPeriod.Value;
set => _stochasticKPeriod.Value = value;
}
/// <summary>Stochastic %D period.</summary>
public int StochasticDPeriod
{
get => _stochasticDPeriod.Value;
set => _stochasticDPeriod.Value = value;
}
/// <summary>Smoothing applied to the %K line.</summary>
public int StochasticSmooth
{
get => _stochasticSmooth.Value;
set => _stochasticSmooth.Value = value;
}
/// <summary>Offset around 50 used for stochastic thresholds.</summary>
public decimal StochasticLevel
{
get => _stochasticLevel.Value;
set => _stochasticLevel.Value = value;
}
/// <summary>Maximum number of simultaneous entries per direction.</summary>
public int MaxOrders
{
get => _maxOrders.Value;
set => _maxOrders.Value = value;
}
/// <summary>Stop loss distance expressed in price points.</summary>
public decimal StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>Take profit distance expressed in price points.</summary>
public decimal TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>Portfolio percentage used to size positions dynamically.</summary>
public decimal RiskPercentage
{
get => _riskPercentage.Value;
set => _riskPercentage.Value = value;
}
/// <summary>Candle type used for indicator calculations.</summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_previousCci = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_ema = new EMA { Length = MaPeriod };
_cci = new CommodityChannelIndex { Length = CciPeriod };
_stochastic = new StochasticOscillator();
_stochastic.K.Length = StochasticKPeriod;
_stochastic.D.Length = StochasticDPeriod;
Volume = OrderVolume;
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
Unit stopLossUnit = null;
Unit takeProfitUnit = null;
var priceStep = Security?.PriceStep ?? 0m;
if (StopLossPoints > 0m && priceStep > 0m)
{
stopLossUnit = new Unit(StopLossPoints * priceStep, UnitTypes.Absolute);
}
if (TakeProfitPoints > 0m && priceStep > 0m)
{
takeProfitUnit = new Unit(TakeProfitPoints * priceStep, UnitTypes.Absolute);
}
if (stopLossUnit != null || takeProfitUnit != null)
{
StartProtection(stopLoss: stopLossUnit, takeProfit: takeProfitUnit);
}
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _ema);
DrawIndicator(area, _cci);
DrawIndicator(area, _stochastic);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
var maResult = _ema.Process(candle).ToNullableDecimal();
var cciResult = _cci.Process(candle).ToNullableDecimal();
var stochasticValue = (StochasticOscillatorValue)_stochastic.Process(candle);
if (maResult == null || cciResult == null)
return;
if (stochasticValue.K is not decimal stochasticK)
return;
if (!_ema.IsFormed || !_cci.IsFormed || !_stochastic.IsFormed)
return;
var maValue = maResult.Value;
var cciValue = cciResult.Value;
var lowerStochastic = 50m - StochasticLevel;
var upperStochastic = 50m + StochasticLevel;
// Buy signal: CCI crosses up through -level from oversold territory
var cciBuyXover = _previousCci != null && _previousCci.Value < -CciLevel && cciValue >= -CciLevel;
// Sell signal: CCI crosses down through +level from overbought territory
var cciSellXover = _previousCci != null && _previousCci.Value > CciLevel && cciValue <= CciLevel;
if (cciBuyXover && stochasticK < lowerStochastic)
{
CloseShortPositions();
TryEnterLong(candle);
}
else if (cciSellXover && stochasticK > upperStochastic)
{
CloseLongPositions();
TryEnterShort(candle);
}
_previousCci = cciValue;
}
private void CloseLongPositions()
{
var longVolume = Position > 0m ? Position : 0m;
if (longVolume <= 0m)
return;
// Close existing long volume before reversing into short trades.
SellMarket(longVolume);
}
private void CloseShortPositions()
{
var shortVolume = Position < 0m ? Position.Abs() : 0m;
if (shortVolume <= 0m)
return;
// Close existing short volume before opening new long trades.
BuyMarket(shortVolume);
}
private void TryEnterLong(ICandleMessage candle)
{
var volume = CalculateOrderVolume(candle.ClosePrice);
if (volume <= 0m)
return;
var currentLongVolume = Position > 0m ? Position : 0m;
if (MaxOrders > 0)
{
var maxVolume = volume * MaxOrders;
if (currentLongVolume >= maxVolume)
return;
var additionalVolume = volume.Min(maxVolume - currentLongVolume);
if (additionalVolume <= 0m)
return;
// Add new long exposure without exceeding MaxOrders limit.
BuyMarket(additionalVolume);
}
else
{
BuyMarket(volume);
}
}
private void TryEnterShort(ICandleMessage candle)
{
var volume = CalculateOrderVolume(candle.ClosePrice);
if (volume <= 0m)
return;
var currentShortVolume = Position < 0m ? Position.Abs() : 0m;
if (MaxOrders > 0)
{
var maxVolume = volume * MaxOrders;
if (currentShortVolume >= maxVolume)
return;
var additionalVolume = volume.Min(maxVolume - currentShortVolume);
if (additionalVolume <= 0m)
return;
// Add new short exposure without exceeding MaxOrders limit.
SellMarket(additionalVolume);
}
else
{
SellMarket(volume);
}
}
private decimal CalculateOrderVolume(decimal referencePrice)
{
var volume = OrderVolume;
if (RiskPercentage > 0m)
{
var portfolioValue = Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
var riskCapital = portfolioValue * RiskPercentage / 100m;
if (riskCapital > 0m)
{
var margin = GetSecurityValue<decimal?>(Level1Fields.MarginBuy) ?? GetSecurityValue<decimal?>(Level1Fields.MarginSell) ?? 0m;
if (margin > 0m)
{
volume = riskCapital / margin;
}
else if (referencePrice > 0m)
{
volume = riskCapital / referencePrice;
}
}
}
volume = RoundVolume(volume);
var minVolume = Security?.MinVolume;
if (minVolume != null && minVolume.Value > 0m && volume < minVolume.Value)
{
volume = minVolume.Value;
}
var maxVolume = Security?.MaxVolume;
if (maxVolume != null && maxVolume.Value > 0m && volume > maxVolume.Value)
{
volume = maxVolume.Value;
}
return volume;
}
private decimal RoundVolume(decimal volume)
{
if (volume <= 0m)
return 0m;
var step = Security?.VolumeStep ?? 0m;
if (step <= 0m)
return volume;
var steps = Math.Floor(volume / step);
var rounded = steps * step;
if (rounded <= 0m)
rounded = step;
return rounded;
}
}
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
from StockSharp.Messages import DataType, CandleStates, UnitTypes, Unit, Level1Fields
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Algo.Indicators import ExponentialMovingAverage, CommodityChannelIndex, StochasticOscillator
class kloss_simple_strategy(Strategy):
def __init__(self):
super(kloss_simple_strategy, self).__init__()
self._order_volume = self.Param("OrderVolume", 0.1).SetDisplay("Volume", "Base order volume", "Trading")
self._ma_period = self.Param("MaPeriod", 5).SetDisplay("EMA Period", "Length of the exponential moving average", "Indicators")
self._cci_period = self.Param("CciPeriod", 10).SetDisplay("CCI Period", "Length of the commodity channel index", "Indicators")
self._cci_level = self.Param("CciLevel", 200.0).SetDisplay("CCI Level", "Distance from zero to trigger signals", "Indicators")
self._stochastic_k_period = self.Param("StochasticKPeriod", 5).SetDisplay("Stochastic %K", "Period of the %K line", "Indicators")
self._stochastic_d_period = self.Param("StochasticDPeriod", 3).SetDisplay("Stochastic %D", "Period of the %D line", "Indicators")
self._stochastic_smooth = self.Param("StochasticSmooth", 3).SetDisplay("Stochastic Smooth", "Smoothing factor for %K", "Indicators")
self._stochastic_level = self.Param("StochasticLevel", 30.0).SetDisplay("Stochastic Level", "Distance from 50 to trigger signals", "Indicators")
self._max_orders = self.Param("MaxOrders", 1).SetDisplay("Max Orders", "Maximum number of positions per direction", "Trading")
self._stop_loss_points = self.Param("StopLossPoints", 0.0).SetDisplay("Stop Loss (pts)", "Stop loss distance in points", "Risk")
self._take_profit_points = self.Param("TakeProfitPoints", 0.0).SetDisplay("Take Profit (pts)", "Take profit distance in points", "Risk")
self._risk_percentage = self.Param("RiskPercentage", 10.0).SetDisplay("Risk %", "Portfolio percentage for dynamic position sizing", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Primary candle series for calculations", "General")
self._previous_cci = None
@property
def OrderVolume(self): return self._order_volume.Value
@property
def MaPeriod(self): return self._ma_period.Value
@property
def CciPeriod(self): return self._cci_period.Value
@property
def CciLevel(self): return self._cci_level.Value
@property
def StochasticKPeriod(self): return self._stochastic_k_period.Value
@property
def StochasticDPeriod(self): return self._stochastic_d_period.Value
@property
def StochasticSmooth(self): return self._stochastic_smooth.Value
@property
def StochasticLevel(self): return self._stochastic_level.Value
@property
def MaxOrders(self): return self._max_orders.Value
@property
def StopLossPoints(self): return self._stop_loss_points.Value
@property
def TakeProfitPoints(self): return self._take_profit_points.Value
@property
def RiskPercentage(self): return self._risk_percentage.Value
@property
def CandleType(self): return self._candle_type.Value
def OnStarted2(self, time):
super(kloss_simple_strategy, self).OnStarted2(time)
self._ema = ExponentialMovingAverage()
self._ema.Length = self.MaPeriod
self._cci = CommodityChannelIndex()
self._cci.Length = self.CciPeriod
self._stochastic = StochasticOscillator()
self._stochastic.K.Length = self.StochasticKPeriod
self._stochastic.D.Length = self.StochasticDPeriod
self.Volume = self.OrderVolume
subscription = self.SubscribeCandles(self.CandleType)
subscription.BindEx(self._ema, self._cci, self._stochastic, self.ProcessIndicators).Start()
price_step = 0.0
if self.Security is not None and self.Security.PriceStep is not None:
price_step = float(self.Security.PriceStep)
sl_unit = None
tp_unit = None
sl_pts = float(self.StopLossPoints)
tp_pts = float(self.TakeProfitPoints)
if sl_pts > 0 and price_step > 0:
sl_unit = Unit(sl_pts * price_step, UnitTypes.Absolute)
if tp_pts > 0 and price_step > 0:
tp_unit = Unit(tp_pts * price_step, UnitTypes.Absolute)
if sl_unit is not None or tp_unit is not None:
self.StartProtection(stopLoss=sl_unit, takeProfit=tp_unit)
def ProcessIndicators(self, candle, ema_value, cci_value, stoch_value):
if candle.State != CandleStates.Finished:
return
if ema_value.IsEmpty or cci_value.IsEmpty or stoch_value.IsEmpty:
return
stoch_k = stoch_value.K if hasattr(stoch_value, 'K') else None
if stoch_k is None:
return
cci_val = float(cci_value)
stoch_k_val = float(stoch_k)
lower_stoch = 50.0 - float(self.StochasticLevel)
upper_stoch = 50.0 + float(self.StochasticLevel)
cci_lev = float(self.CciLevel)
# Buy signal: CCI crosses up through -level from oversold territory
cci_buy_xover = self._previous_cci is not None and self._previous_cci < -cci_lev and cci_val >= -cci_lev
# Sell signal: CCI crosses down through +level from overbought territory
cci_sell_xover = self._previous_cci is not None and self._previous_cci > cci_lev and cci_val <= cci_lev
if cci_buy_xover and stoch_k_val < lower_stoch:
self._close_short_positions()
self._try_enter_long(candle)
elif cci_sell_xover and stoch_k_val > upper_stoch:
self._close_long_positions()
self._try_enter_short(candle)
self._previous_cci = cci_val
def _close_long_positions(self):
long_volume = float(self.Position) if self.Position > 0 else 0.0
if long_volume <= 0:
return
self.SellMarket(long_volume)
def _close_short_positions(self):
short_volume = abs(float(self.Position)) if self.Position < 0 else 0.0
if short_volume <= 0:
return
self.BuyMarket(short_volume)
def _try_enter_long(self, candle):
volume = self._calculate_order_volume(float(candle.ClosePrice))
if volume <= 0:
return
current_long = float(self.Position) if self.Position > 0 else 0.0
max_orders = int(self.MaxOrders)
if max_orders > 0:
max_vol = volume * max_orders
if current_long >= max_vol:
return
additional = min(volume, max_vol - current_long)
if additional <= 0:
return
self.BuyMarket(additional)
else:
self.BuyMarket(volume)
def _try_enter_short(self, candle):
volume = self._calculate_order_volume(float(candle.ClosePrice))
if volume <= 0:
return
current_short = abs(float(self.Position)) if self.Position < 0 else 0.0
max_orders = int(self.MaxOrders)
if max_orders > 0:
max_vol = volume * max_orders
if current_short >= max_vol:
return
additional = min(volume, max_vol - current_short)
if additional <= 0:
return
self.SellMarket(additional)
else:
self.SellMarket(volume)
def _calculate_order_volume(self, reference_price):
volume = float(self.OrderVolume)
risk_pct = float(self.RiskPercentage)
if risk_pct > 0:
portfolio_value = 0.0
if self.Portfolio is not None:
if self.Portfolio.CurrentValue is not None and float(self.Portfolio.CurrentValue) != 0:
portfolio_value = float(self.Portfolio.CurrentValue)
elif self.Portfolio.BeginValue is not None:
portfolio_value = float(self.Portfolio.BeginValue)
risk_capital = portfolio_value * risk_pct / 100.0
if risk_capital > 0:
margin = 0.0
if reference_price > 0:
volume = risk_capital / reference_price
volume = self._round_volume(volume)
if self.Security is not None:
min_vol = self.Security.MinVolume
if min_vol is not None and float(min_vol) > 0 and volume < float(min_vol):
volume = float(min_vol)
max_vol = self.Security.MaxVolume
if max_vol is not None and float(max_vol) > 0 and volume > float(max_vol):
volume = float(max_vol)
return volume
def _round_volume(self, volume):
if volume <= 0:
return 0.0
step = 0.0
if self.Security is not None and self.Security.VolumeStep is not None:
step = float(self.Security.VolumeStep)
if step <= 0:
return volume
steps = Math.Floor(volume / step)
rounded = steps * step
if rounded <= 0:
rounded = step
return rounded
def OnReseted(self):
super(kloss_simple_strategy, self).OnReseted()
self._previous_cci = None
def CreateClone(self):
return kloss_simple_strategy()