Встроенное соотношение Келли
Стратегия пробоя канала на основе скользящей средней и ATR с управлением размером позиции по критерию Келли.
Подробности
- Критерии входа: цена пересекает верхнюю или нижнюю границу канала ATR.
- Длинные/короткие: оба направления.
- Критерии выхода: опциональные тейк-профит и стоп-лосс.
- Стопы: опционально.
- Параметры по умолчанию:
Length= 20Multiplier= 1AtrLength= 10UseEma= trueUseKelly= trueUseTakeProfit= falseUseStopLoss= falseTakeProfit= 10StopLoss= 1CandleType= TimeSpan.FromMinutes(1)
- Фильтры:
- Категория: Пробой
- Направление: Оба
- Индикаторы: MA, ATR
- Стопы: Опционально
- Сложность: Базовая
- Таймфрейм: Внутридневной (1м)
- Сезонность: Нет
- Нейронные сети: Нет
- Дивергенция: Нет
- Уровень риска: Средний
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>
/// Built-in Kelly Ratio strategy.
/// The channel is an EMA (or SMA when UseEma is off) of Length closes plus and minus Multiplier times ATR(AtrLength). A close
/// crossing above the upper band goes long and a close crossing below the lower band goes short, reversing an opposite position.
/// With UseKelly the order size is Volume times the Kelly ratio W - (1 - W) / R of the closed trades (W = win rate, R = average
/// win / average loss); until both a win and a loss exist the full Volume is used, and a non-positive ratio opens nothing.
/// Optional percent take profit and stop loss protect the position.
/// </summary>
public class BuiltInKellyRatioStrategy : Strategy
{
private readonly StrategyParam<int> _length;
private readonly StrategyParam<decimal> _multiplier;
private readonly StrategyParam<int> _atrLength;
private readonly StrategyParam<bool> _useEma;
private readonly StrategyParam<bool> _useKelly;
private readonly StrategyParam<bool> _useTakeProfit;
private readonly StrategyParam<bool> _useStopLoss;
private readonly StrategyParam<decimal> _takeProfit;
private readonly StrategyParam<decimal> _stopLoss;
private readonly StrategyParam<DataType> _candleType;
private decimal? _prevClose;
private decimal? _prevUpper;
private decimal? _prevLower;
private decimal _lastPosition;
private decimal _pnlAtOpen;
private int _wins;
private int _losses;
private decimal _grossWin;
private decimal _grossLoss;
/// <summary>
/// Moving average period.
/// </summary>
public int Length
{
get => _length.Value;
set => _length.Value = value;
}
/// <summary>
/// ATR multiple of the band width.
/// </summary>
public decimal Multiplier
{
get => _multiplier.Value;
set => _multiplier.Value = value;
}
/// <summary>
/// ATR period.
/// </summary>
public int AtrLength
{
get => _atrLength.Value;
set => _atrLength.Value = value;
}
/// <summary>
/// Use EMA instead of SMA as the channel center.
/// </summary>
public bool UseEma
{
get => _useEma.Value;
set => _useEma.Value = value;
}
/// <summary>
/// Size orders by the Kelly ratio.
/// </summary>
public bool UseKelly
{
get => _useKelly.Value;
set => _useKelly.Value = value;
}
/// <summary>
/// Enable the take profit.
/// </summary>
public bool UseTakeProfit
{
get => _useTakeProfit.Value;
set => _useTakeProfit.Value = value;
}
/// <summary>
/// Enable the stop loss.
/// </summary>
public bool UseStopLoss
{
get => _useStopLoss.Value;
set => _useStopLoss.Value = value;
}
/// <summary>
/// Take profit in percent.
/// </summary>
public decimal TakeProfit
{
get => _takeProfit.Value;
set => _takeProfit.Value = value;
}
/// <summary>
/// Stop loss in percent.
/// </summary>
public decimal StopLoss
{
get => _stopLoss.Value;
set => _stopLoss.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public BuiltInKellyRatioStrategy()
{
_length = Param(nameof(Length), 20)
.SetGreaterThanZero()
.SetDisplay("Length", "Moving average period", "Channel");
_multiplier = Param(nameof(Multiplier), 1m)
.SetGreaterThanZero()
.SetDisplay("Multiplier", "ATR multiple of the band width", "Channel");
_atrLength = Param(nameof(AtrLength), 10)
.SetGreaterThanZero()
.SetDisplay("ATR Length", "ATR period", "Channel");
_useEma = Param(nameof(UseEma), true)
.SetDisplay("Use EMA", "Use EMA instead of SMA as the channel center", "Channel");
_useKelly = Param(nameof(UseKelly), true)
.SetDisplay("Use Kelly", "Size orders by the Kelly ratio", "Money Management");
_useTakeProfit = Param(nameof(UseTakeProfit), false)
.SetDisplay("Use Take Profit", "Enable the take profit", "Risk");
_useStopLoss = Param(nameof(UseStopLoss), false)
.SetDisplay("Use Stop Loss", "Enable the stop loss", "Risk");
_takeProfit = Param(nameof(TakeProfit), 10m)
.SetNotNegative()
.SetDisplay("Take Profit %", "Take profit in percent", "Risk");
_stopLoss = Param(nameof(StopLoss), 1m)
.SetNotNegative()
.SetDisplay("Stop Loss %", "Stop loss in percent", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(1).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
ResetState();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
ResetState();
IIndicator ma = UseEma
? new ExponentialMovingAverage { Length = Length }
: new SimpleMovingAverage { Length = Length };
var atr = new AverageTrueRange { Length = AtrLength };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ma, atr, ProcessCandle)
.Start();
var take = UseTakeProfit && TakeProfit > 0 ? new Unit(TakeProfit, UnitTypes.Percent) : new Unit();
var stop = UseStopLoss && StopLoss > 0 ? new Unit(StopLoss, UnitTypes.Percent) : new Unit();
if (UseTakeProfit || UseStopLoss)
StartProtection(take, stop, useMarketOrders: true);
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, ma);
DrawOwnTrades(area);
}
}
private void ResetState()
{
_prevClose = null;
_prevUpper = null;
_prevLower = null;
_lastPosition = 0;
_pnlAtOpen = 0;
_wins = 0;
_losses = 0;
_grossWin = 0;
_grossLoss = 0;
}
private void ProcessCandle(ICandleMessage candle, decimal ma, decimal atr)
{
if (candle.State != CandleStates.Finished)
return;
var close = candle.ClosePrice;
var upper = ma + atr * Multiplier;
var lower = ma - atr * Multiplier;
var prevClose = _prevClose;
var prevUpper = _prevUpper;
var prevLower = _prevLower;
_prevClose = close;
_prevUpper = upper;
_prevLower = lower;
if (prevClose is not decimal pc || prevUpper is not decimal pu || prevLower is not decimal pl)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (pc <= pu && close > upper && Position <= 0)
{
var volume = GetEntryVolume() + Math.Abs(Position);
if (volume > 0)
BuyMarket(volume);
}
else if (pc >= pl && close < lower && Position >= 0)
{
var volume = GetEntryVolume() + Math.Abs(Position);
if (volume > 0)
SellMarket(volume);
}
}
private decimal GetEntryVolume()
{
if (!UseKelly || _wins == 0 || _losses == 0)
return Volume;
var winRate = (decimal)_wins / (_wins + _losses);
var avgWin = _grossWin / _wins;
var avgLoss = _grossLoss / _losses;
if (avgLoss <= 0)
return Volume;
var kelly = winRate - (1m - winRate) / (avgWin / avgLoss);
if (kelly <= 0)
return 0;
var volume = Volume * Math.Min(kelly, 1m);
if (Security?.VolumeStep is decimal step && step > 0)
volume = Math.Floor(volume / step) * step;
if (Security?.MinVolume is decimal minVolume && volume < minVolume)
volume = minVolume;
return volume;
}
/// <inheritdoc />
protected override void OnOwnTradeReceived(MyTrade trade)
{
base.OnOwnTradeReceived(trade);
var position = Position;
// A trade is complete when the position returns to flat or flips its side.
if (_lastPosition != 0 && (position == 0 || Math.Sign(position) != Math.Sign(_lastPosition)))
{
var result = PnL - _pnlAtOpen;
if (result > 0)
{
_wins++;
_grossWin += result;
}
else if (result < 0)
{
_losses++;
_grossLoss -= result;
}
_pnlAtOpen = PnL;
}
else if (_lastPosition == 0 && position != 0)
{
_pnlAtOpen = PnL;
}
_lastPosition = position;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Decimal, Math
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Indicators import ExponentialMovingAverage, SimpleMovingAverage, AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
class built_in_kelly_ratio_strategy(Strategy):
"""
Built-in Kelly Ratio strategy.
The channel is an EMA (or SMA when UseEma is off) of Length closes plus and minus Multiplier times ATR(AtrLength). A close
crossing above the upper band goes long and a close crossing below the lower band goes short, reversing an opposite position.
With UseKelly the order size is Volume times the Kelly ratio W - (1 - W) / R of the closed trades (W = win rate, R = average
win / average loss); until both a win and a loss exist the full Volume is used, and a non-positive ratio opens nothing.
Optional percent take profit and stop loss protect the position.
"""
def __init__(self):
super(built_in_kelly_ratio_strategy, self).__init__()
self._length = self.Param("Length", 20).SetGreaterThanZero().SetDisplay("Length", "Moving average period", "Channel")
self._multiplier = self.Param("Multiplier", 1.0).SetGreaterThanZero().SetDisplay("Multiplier", "ATR multiple of the band width", "Channel")
self._atr_length = self.Param("AtrLength", 10).SetGreaterThanZero().SetDisplay("ATR Length", "ATR period", "Channel")
self._use_ema = self.Param("UseEma", True).SetDisplay("Use EMA", "Use EMA instead of SMA as the channel center", "Channel")
self._use_kelly = self.Param("UseKelly", True).SetDisplay("Use Kelly", "Size orders by the Kelly ratio", "Money Management")
self._use_take_profit = self.Param("UseTakeProfit", False).SetDisplay("Use Take Profit", "Enable the take profit", "Risk")
self._use_stop_loss = self.Param("UseStopLoss", False).SetDisplay("Use Stop Loss", "Enable the stop loss", "Risk")
self._take_profit = self.Param("TakeProfit", 10.0).SetNotNegative().SetDisplay("Take Profit %", "Take profit in percent", "Risk")
self._stop_loss = self.Param("StopLoss", 1.0).SetNotNegative().SetDisplay("Stop Loss %", "Stop loss in percent", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(1))).SetDisplay("Candle Type", "Type of candles to use", "General")
self._reset_state()
@property
def candle_type(self):
return self._candle_type.Value
def _reset_state(self):
self._prev_close = None
self._prev_upper = None
self._prev_lower = None
self._last_position = Decimal(0)
self._pnl_at_open = Decimal(0)
self._wins = 0
self._losses = 0
self._gross_win = Decimal(0)
self._gross_loss = Decimal(0)
def OnReseted(self):
super(built_in_kelly_ratio_strategy, self).OnReseted()
self._reset_state()
def OnStarted2(self, time):
super(built_in_kelly_ratio_strategy, self).OnStarted2(time)
self._reset_state()
ma = ExponentialMovingAverage() if self._use_ema.Value else SimpleMovingAverage()
ma.Length = self._length.Value
atr = AverageTrueRange()
atr.Length = self._atr_length.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(ma, atr, self._process_candle).Start()
use_tp = self._use_take_profit.Value
use_sl = self._use_stop_loss.Value
tp = self._take_profit.Value
sl = self._stop_loss.Value
take = Unit(Decimal(tp), UnitTypes.Percent) if use_tp and tp > 0 else Unit()
stop = Unit(Decimal(sl), UnitTypes.Percent) if use_sl and sl > 0 else Unit()
if use_tp or use_sl:
self.StartProtection(take, stop, useMarketOrders=True)
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, ma)
self.DrawOwnTrades(area)
def _process_candle(self, candle, ma, atr):
if candle.State != CandleStates.Finished:
return
close = candle.ClosePrice
multiplier = Decimal(self._multiplier.Value)
upper = ma + atr * multiplier
lower = ma - atr * multiplier
prev_close = self._prev_close
prev_upper = self._prev_upper
prev_lower = self._prev_lower
self._prev_close = close
self._prev_upper = upper
self._prev_lower = lower
if prev_close is None or prev_upper is None or prev_lower is None:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
if prev_close <= prev_upper and close > upper and self.Position <= 0:
volume = self._get_entry_volume() + abs(self.Position)
if volume > 0:
self.BuyMarket(volume)
elif prev_close >= prev_lower and close < lower and self.Position >= 0:
volume = self._get_entry_volume() + abs(self.Position)
if volume > 0:
self.SellMarket(volume)
def _get_entry_volume(self):
if not self._use_kelly.Value or self._wins == 0 or self._losses == 0:
return self.Volume
win_rate = Decimal(self._wins) / Decimal(self._wins + self._losses)
avg_win = self._gross_win / Decimal(self._wins)
avg_loss = self._gross_loss / Decimal(self._losses)
if avg_loss <= 0:
return self.Volume
kelly = win_rate - (Decimal(1) - win_rate) / (avg_win / avg_loss)
if kelly <= 0:
return Decimal(0)
volume = self.Volume * min(kelly, Decimal(1))
security = self.Security
if security is not None:
step = security.VolumeStep
if step is not None and step > 0:
volume = Math.Floor(volume / step) * step
min_volume = security.MinVolume
if min_volume is not None and volume < min_volume:
volume = min_volume
return volume
def OnOwnTradeReceived(self, trade):
super(built_in_kelly_ratio_strategy, self).OnOwnTradeReceived(trade)
position = self.Position
last = self._last_position
# A trade is complete when the position returns to flat or flips its side.
if last != 0 and (position == 0 or (position > 0) != (last > 0)):
result = self.PnL - self._pnl_at_open
if result > 0:
self._wins += 1
self._gross_win += result
elif result < 0:
self._losses += 1
self._gross_loss -= result
self._pnl_at_open = self.PnL
elif last == 0 and position != 0:
self._pnl_at_open = self.PnL
self._last_position = position
def CreateClone(self):
return built_in_kelly_ratio_strategy()