using System;
using System.Collections.Generic;
using System.Linq;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Multi-filter risk/reward strategy.
/// </summary>
public class RiskRewardRatioStrategy : Strategy
{
private readonly StrategyParam<decimal> _tradeVolume;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _fastMaPeriod;
private readonly StrategyParam<int> _slowMaPeriod;
private readonly StrategyParam<decimal> _momentumThreshold;
private readonly StrategyParam<decimal> _rewardRatio;
private readonly StrategyParam<int> _stopLossPips;
private readonly StrategyParam<int> _maxPositions;
private readonly StrategyParam<bool> _enableTrailing;
private readonly StrategyParam<int> _trailingStopPips;
private readonly StrategyParam<bool> _enableBreakEven;
private readonly StrategyParam<int> _breakEvenTriggerPips;
private readonly StrategyParam<int> _breakEvenOffsetPips;
private readonly StrategyParam<bool> _exitSwitch;
private readonly List<Leg> _legs = [];
private readonly List<decimal> _momentumDistances = [];
private WeightedMovingAverage _fastLwma;
private WeightedMovingAverage _slowLwma;
private RelativeStrengthIndex _rsi;
private MovingAverageConvergenceDivergenceSignal _macd;
private RateOfChange _momentum;
private StochasticK _fastStochasticK;
private StochasticK _slowStochasticK;
private SimpleMovingAverage _fastStochasticMain;
private SimpleMovingAverage _slowStochasticMain;
private SimpleMovingAverage _slowStochasticSignal;
public decimal TradeVolume { get => _tradeVolume.Value; set => _tradeVolume.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public int FastMaPeriod { get => _fastMaPeriod.Value; set => _fastMaPeriod.Value = value; }
public int SlowMaPeriod { get => _slowMaPeriod.Value; set => _slowMaPeriod.Value = value; }
public decimal MomentumThreshold { get => _momentumThreshold.Value; set => _momentumThreshold.Value = value; }
public decimal RewardRatio { get => _rewardRatio.Value; set => _rewardRatio.Value = value; }
public int StopLossPips { get => _stopLossPips.Value; set => _stopLossPips.Value = value; }
public int MaxPositions { get => _maxPositions.Value; set => _maxPositions.Value = value; }
public bool EnableTrailing { get => _enableTrailing.Value; set => _enableTrailing.Value = value; }
public int TrailingStopPips { get => _trailingStopPips.Value; set => _trailingStopPips.Value = value; }
public bool EnableBreakEven { get => _enableBreakEven.Value; set => _enableBreakEven.Value = value; }
public int BreakEvenTriggerPips { get => _breakEvenTriggerPips.Value; set => _breakEvenTriggerPips.Value = value; }
public int BreakEvenOffsetPips { get => _breakEvenOffsetPips.Value; set => _breakEvenOffsetPips.Value = value; }
public bool ExitSwitch { get => _exitSwitch.Value; set => _exitSwitch.Value = value; }
public RiskRewardRatioStrategy()
{
_tradeVolume = Param(nameof(TradeVolume), 0.1m).SetGreaterThanZero();
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame());
_fastMaPeriod = Param(nameof(FastMaPeriod), 6).SetGreaterThanZero();
_slowMaPeriod = Param(nameof(SlowMaPeriod), 85).SetGreaterThanZero();
_momentumThreshold = Param(nameof(MomentumThreshold), 0.3m).SetNotNegative();
_rewardRatio = Param(nameof(RewardRatio), 2m).SetGreaterThanZero();
_stopLossPips = Param(nameof(StopLossPips), 20).SetGreaterThanZero();
_maxPositions = Param(nameof(MaxPositions), 10).SetGreaterThanZero();
_enableTrailing = Param(nameof(EnableTrailing), true);
_trailingStopPips = Param(nameof(TrailingStopPips), 40).SetNotNegative();
_enableBreakEven = Param(nameof(EnableBreakEven), true);
_breakEvenTriggerPips = Param(nameof(BreakEvenTriggerPips), 30).SetNotNegative();
_breakEvenOffsetPips = Param(nameof(BreakEvenOffsetPips), 30).SetNotNegative();
_exitSwitch = Param(nameof(ExitSwitch), false);
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType)];
protected override void OnReseted()
{
base.OnReseted();
_legs.Clear();
_momentumDistances.Clear();
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_fastLwma = new() { Length = FastMaPeriod };
_slowLwma = new() { Length = SlowMaPeriod };
_rsi = new() { Length = 14 };
_macd = new()
{
Macd =
{
ShortMa = { Length = 12 },
LongMa = { Length = 26 },
},
SignalMa = { Length = 9 },
};
_momentum = new() { Length = 14 };
_fastStochasticK = new() { Length = 5 };
_slowStochasticK = new() { Length = 21 };
_fastStochasticMain = new() { Length = 2 };
_slowStochasticMain = new() { Length = 4 };
_slowStochasticSignal = new() { Length = 10 };
SubscribeCandles(CandleType)
.BindEx(_fastLwma, _slowLwma, _rsi, _macd, _momentum, _fastStochasticK, _slowStochasticK, ProcessCandle, true)
.Start();
}
protected override void OnOwnTradeReceived(MyTrade trade)
{
base.OnOwnTradeReceived(trade);
if (trade?.Order is null || trade.Trade is null)
return;
var leg = _legs.FirstOrDefault(l => l.Order == trade.Order);
if (leg is not null)
AddFill(leg, trade);
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue fastLwmaValue, IIndicatorValue slowLwmaValue, IIndicatorValue rsiValue,
IIndicatorValue macdValue, IIndicatorValue momentumValue, IIndicatorValue fastKValue, IIndicatorValue slowKValue)
{
if (candle.State != CandleStates.Finished)
return;
var fastMain = UpdateFastStochastic(fastKValue);
var slowSignal = UpdateSlowStochastic(slowKValue);
UpdateMomentum(momentumValue);
if (ExitSwitch)
{
Flatten();
return;
}
if (ApplyRisk(candle))
return;
if (fastMain is not decimal fastK || slowSignal is not decimal slowD)
return;
if (!_fastLwma.IsFormed || !_slowLwma.IsFormed || !_rsi.IsFormed || !_macd.IsFormed || !_momentum.IsFormed)
return;
var macd = (IMovingAverageConvergenceDivergenceSignalValue)macdValue;
if (macd.Macd is not decimal macdMain || macd.Signal is not decimal macdSignal)
return;
var rsi = rsiValue.ToDecimal();
var fastLwma = fastLwmaValue.ToDecimal();
var slowLwma = slowLwmaValue.ToDecimal();
var momentumBurst = _momentumDistances.Count == 0 ? 0m : _momentumDistances.Max();
var longSignal =
fastK > slowD &&
rsi > 50m &&
fastLwma > slowLwma &&
macdMain > macdSignal && macdMain > 0m &&
momentumBurst >= MomentumThreshold;
var shortSignal =
fastK < slowD &&
rsi < 50m &&
fastLwma < slowLwma &&
macdMain < macdSignal && macdMain < 0m &&
momentumBurst >= MomentumThreshold;
var maxExposure = MaxPositions * TradeVolume;
if (longSignal && Position >= 0m && Position + TradeVolume <= maxExposure)
Enter(Sides.Buy, candle.OpenTime);
else if (shortSignal && Position <= 0m && Math.Abs(Position - TradeVolume) <= maxExposure)
Enter(Sides.Sell, candle.OpenTime);
}
private decimal? UpdateFastStochastic(IIndicatorValue fastKValue)
{
if (!_fastStochasticK.IsFormed || fastKValue.IsEmpty)
return null;
var main = _fastStochasticMain.Process(fastKValue);
return _fastStochasticMain.IsFormed && !main.IsEmpty ? main.ToDecimal() : null;
}
private decimal? UpdateSlowStochastic(IIndicatorValue slowKValue)
{
if (!_slowStochasticK.IsFormed || slowKValue.IsEmpty)
return null;
var main = _slowStochasticMain.Process(slowKValue);
if (!_slowStochasticMain.IsFormed || main.IsEmpty)
return null;
var signal = _slowStochasticSignal.Process(main);
return _slowStochasticSignal.IsFormed && !signal.IsEmpty ? signal.ToDecimal() : null;
}
private void UpdateMomentum(IIndicatorValue momentumValue)
{
if (!_momentum.IsFormed || momentumValue.IsEmpty)
return;
// |ROC| equals the momentum's distance from 100.
_momentumDistances.Add(Math.Abs(momentumValue.ToDecimal()));
if (_momentumDistances.Count > 3)
_momentumDistances.RemoveAt(0);
}
private void Enter(Sides side, DateTime candleTime)
{
var order = side == Sides.Buy ? BuyMarket(TradeVolume) : SellMarket(TradeVolume);
var leg = new Leg
{
Order = order,
Side = side,
EntryTime = candleTime,
};
_legs.Add(leg);
// The emulator can fill a market order before BuyMarket or SellMarket returns it.
foreach (var trade in MyTrades.Where(t => t.Order == order && t.Trade is not null).ToArray())
AddFill(leg, trade);
}
private void AddFill(Leg leg, MyTrade trade)
{
leg.FilledVolume += trade.Trade.Volume;
leg.FilledValue += trade.Trade.Price * trade.Trade.Volume;
leg.FillPrice = leg.FilledValue / leg.FilledVolume;
SetInitialLevels(leg, leg.FillPrice.Value);
}
private void SetInitialLevels(Leg leg, decimal fill)
{
leg.BestPrice = fill;
if (GetPip() is not decimal pip)
{
leg.StopPrice = null;
leg.TakePrice = null;
return;
}
var stopDistance = StopLossPips * pip;
var takeDistance = stopDistance * RewardRatio;
if (leg.Side == Sides.Buy)
{
leg.StopPrice = fill - stopDistance;
leg.TakePrice = fill + takeDistance;
}
else
{
leg.StopPrice = fill + stopDistance;
leg.TakePrice = fill - takeDistance;
}
}
private bool ApplyRisk(ICandleMessage candle)
{
var pip = GetPip();
var exitSell = 0m;
var exitBuy = 0m;
for (var i = _legs.Count - 1; i >= 0; i--)
{
var leg = _legs[i];
if (leg.FillPrice is not decimal fill || candle.OpenTime <= leg.EntryTime)
continue;
var isLong = leg.Side == Sides.Buy;
leg.BestPrice = isLong ? Math.Max(leg.BestPrice, candle.HighPrice) : Math.Min(leg.BestPrice, candle.LowPrice);
if (pip is decimal step)
MoveStop(leg, fill, step);
var hit = isLong
? (leg.StopPrice is decimal stop && candle.LowPrice <= stop) || (leg.TakePrice is decimal take && candle.HighPrice >= take)
: (leg.StopPrice is decimal shortStop && candle.HighPrice >= shortStop) || (leg.TakePrice is decimal shortTake && candle.LowPrice <= shortTake);
if (!hit)
continue;
if (isLong)
exitSell += leg.FilledVolume;
else
exitBuy += leg.FilledVolume;
_legs.RemoveAt(i);
}
if (exitSell > 0m)
SellMarket(exitSell);
if (exitBuy > 0m)
BuyMarket(exitBuy);
return exitSell > 0m || exitBuy > 0m;
}
private void MoveStop(Leg leg, decimal fill, decimal pip)
{
var isLong = leg.Side == Sides.Buy;
var advance = isLong ? leg.BestPrice - fill : fill - leg.BestPrice;
if (EnableBreakEven && advance >= BreakEvenTriggerPips * pip)
{
var offset = BreakEvenOffsetPips * pip;
TightenStop(leg, isLong ? fill + offset : fill - offset);
}
if (EnableTrailing && TrailingStopPips > 0)
{
var trail = TrailingStopPips * pip;
if (advance >= trail)
TightenStop(leg, isLong ? leg.BestPrice - trail : leg.BestPrice + trail);
}
}
private static void TightenStop(Leg leg, decimal candidate)
{
if (leg.StopPrice is not decimal stop || (leg.Side == Sides.Buy ? candidate > stop : candidate < stop))
leg.StopPrice = candidate;
}
private void Flatten()
{
if (Position > 0m)
SellMarket(Math.Abs(Position));
else if (Position < 0m)
BuyMarket(Math.Abs(Position));
_legs.Clear();
}
private decimal? GetPip()
{
var step = Security?.PriceStep;
return step > 0m ? step : null;
}
private sealed class Leg
{
public Order Order { get; init; }
public Sides Side { get; init; }
public DateTime EntryTime { get; init; }
public decimal FilledVolume { get; set; }
public decimal FilledValue { get; set; }
public decimal? FillPrice { get; set; }
public decimal BestPrice { get; set; }
public decimal? StopPrice { get; set; }
public decimal? TakePrice { get; set; }
}
}
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, Sides
from StockSharp.Algo.Indicators import (WeightedMovingAverage, RelativeStrengthIndex,
MovingAverageConvergenceDivergenceSignal, RateOfChange,
StochasticK, SimpleMovingAverage)
from StockSharp.Algo.Strategies import Strategy
class _leg(object):
def __init__(self, order, side, entry_time):
self.order = order
self.side = side
self.entry_time = entry_time
self.filled_volume = 0.0
self.filled_value = 0.0
self.fill_price = None
self.best_price = 0.0
self.stop_price = None
self.take_price = None
class risk_reward_ratio_strategy(Strategy):
def __init__(self):
super(risk_reward_ratio_strategy, self).__init__()
self._trade_volume = self.Param("TradeVolume", 0.1).SetGreaterThanZero()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15)))
self._fast_ma = self.Param("FastMaPeriod", 6).SetGreaterThanZero()
self._slow_ma = self.Param("SlowMaPeriod", 85).SetGreaterThanZero()
self._momentum_threshold = self.Param("MomentumThreshold", 0.3).SetNotNegative()
self._reward_ratio = self.Param("RewardRatio", 2.0).SetGreaterThanZero()
self._stop_loss = self.Param("StopLossPips", 20).SetGreaterThanZero()
self._max_positions = self.Param("MaxPositions", 10).SetGreaterThanZero()
self._enable_trailing = self.Param("EnableTrailing", True)
self._trailing_stop = self.Param("TrailingStopPips", 40).SetNotNegative()
self._enable_break_even = self.Param("EnableBreakEven", True)
self._break_even_trigger = self.Param("BreakEvenTriggerPips", 30).SetNotNegative()
self._break_even_offset = self.Param("BreakEvenOffsetPips", 30).SetNotNegative()
self._exit_switch = self.Param("ExitSwitch", False)
self._legs = []
self._momentum_distances = []
self._fast_lwma = None
self._slow_lwma = None
self._rsi = None
self._macd = None
self._momentum = None
self._fast_stochastic_k = None
self._slow_stochastic_k = None
self._fast_stochastic_main = None
self._slow_stochastic_main = None
self._slow_stochastic_signal = None
def GetWorkingSecurities(self):
return [(self.Security, self._candle_type.Value)]
def OnReseted(self):
super(risk_reward_ratio_strategy, self).OnReseted()
self._legs = []
self._momentum_distances = []
def OnStarted2(self, time):
super(risk_reward_ratio_strategy, self).OnStarted2(time)
self._fast_lwma = WeightedMovingAverage()
self._fast_lwma.Length = int(self._fast_ma.Value)
self._slow_lwma = WeightedMovingAverage()
self._slow_lwma.Length = int(self._slow_ma.Value)
self._rsi = RelativeStrengthIndex()
self._rsi.Length = 14
self._macd = MovingAverageConvergenceDivergenceSignal()
self._macd.Macd.ShortMa.Length = 12
self._macd.Macd.LongMa.Length = 26
self._macd.SignalMa.Length = 9
self._momentum = RateOfChange()
self._momentum.Length = 14
self._fast_stochastic_k = StochasticK()
self._fast_stochastic_k.Length = 5
self._slow_stochastic_k = StochasticK()
self._slow_stochastic_k.Length = 21
self._fast_stochastic_main = SimpleMovingAverage()
self._fast_stochastic_main.Length = 2
self._slow_stochastic_main = SimpleMovingAverage()
self._slow_stochastic_main.Length = 4
self._slow_stochastic_signal = SimpleMovingAverage()
self._slow_stochastic_signal.Length = 10
self.SubscribeCandles(self._candle_type.Value).BindEx(
self._fast_lwma, self._slow_lwma, self._rsi, self._macd, self._momentum,
self._fast_stochastic_k, self._slow_stochastic_k, self._process, True).Start()
def OnOwnTradeReceived(self, trade):
super(risk_reward_ratio_strategy, self).OnOwnTradeReceived(trade)
if trade is None or trade.Order is None or trade.Trade is None:
return
for leg in self._legs:
if leg.order == trade.Order:
self._add_fill(leg, trade)
break
def _process(self, candle, fast_lwma_value, slow_lwma_value, rsi_value, macd_value, momentum_value,
fast_k_value, slow_k_value):
if candle.State != CandleStates.Finished:
return
fast_main = self._update_fast_stochastic(fast_k_value)
slow_signal = self._update_slow_stochastic(slow_k_value)
self._update_momentum(momentum_value)
if bool(self._exit_switch.Value):
self._flatten()
return
if self._apply_risk(candle):
return
if fast_main is None or slow_signal is None:
return
if (not self._fast_lwma.IsFormed or not self._slow_lwma.IsFormed or not self._rsi.IsFormed
or not self._macd.IsFormed or not self._momentum.IsFormed):
return
macd_main = macd_value.Macd
macd_signal = macd_value.Signal
if macd_main is None or macd_signal is None:
return
macd_main = float(macd_main)
macd_signal = float(macd_signal)
rsi = float(rsi_value)
fast_lwma = float(fast_lwma_value)
slow_lwma = float(slow_lwma_value)
burst = max(self._momentum_distances) if self._momentum_distances else 0.0
threshold = float(self._momentum_threshold.Value)
long_signal = (fast_main > slow_signal and rsi > 50.0 and fast_lwma > slow_lwma and
macd_main > macd_signal and macd_main > 0 and burst >= threshold)
short_signal = (fast_main < slow_signal and rsi < 50.0 and fast_lwma < slow_lwma and
macd_main < macd_signal and macd_main < 0 and burst >= threshold)
volume = float(self._trade_volume.Value)
max_exposure = int(self._max_positions.Value) * volume
if long_signal and self.Position >= 0 and float(self.Position) + volume <= max_exposure:
self._enter(Sides.Buy, candle.OpenTime)
elif short_signal and self.Position <= 0 and abs(float(self.Position) - volume) <= max_exposure:
self._enter(Sides.Sell, candle.OpenTime)
def _update_fast_stochastic(self, fast_k_value):
if not self._fast_stochastic_k.IsFormed or fast_k_value.IsEmpty:
return None
main = self._fast_stochastic_main.Process(fast_k_value)
if not self._fast_stochastic_main.IsFormed or main.IsEmpty:
return None
return float(main)
def _update_slow_stochastic(self, slow_k_value):
if not self._slow_stochastic_k.IsFormed or slow_k_value.IsEmpty:
return None
main = self._slow_stochastic_main.Process(slow_k_value)
if not self._slow_stochastic_main.IsFormed or main.IsEmpty:
return None
signal = self._slow_stochastic_signal.Process(main)
if not self._slow_stochastic_signal.IsFormed or signal.IsEmpty:
return None
return float(signal)
def _update_momentum(self, momentum_value):
if not self._momentum.IsFormed or momentum_value.IsEmpty:
return
# |ROC| equals the momentum's distance from 100.
self._momentum_distances.append(abs(float(momentum_value)))
if len(self._momentum_distances) > 3:
self._momentum_distances.pop(0)
def _enter(self, side, candle_time):
volume = self._trade_volume.Value
order = self.BuyMarket(volume) if side == Sides.Buy else self.SellMarket(volume)
leg = _leg(order, side, candle_time)
self._legs.append(leg)
# The emulator can fill a market order before BuyMarket or SellMarket returns it.
for trade in list(self.MyTrades):
if trade.Order == order and trade.Trade is not None:
self._add_fill(leg, trade)
def _add_fill(self, leg, trade):
price = float(trade.Trade.TradePrice)
volume = float(trade.Trade.TradeVolume)
leg.filled_volume += volume
leg.filled_value += price * volume
leg.fill_price = leg.filled_value / leg.filled_volume
self._set_initial_levels(leg, leg.fill_price)
def _set_initial_levels(self, leg, fill):
leg.best_price = fill
pip = self._pip()
if pip is None:
leg.stop_price = None
leg.take_price = None
return
stop_distance = int(self._stop_loss.Value) * pip
take_distance = stop_distance * float(self._reward_ratio.Value)
if leg.side == Sides.Buy:
leg.stop_price = fill - stop_distance
leg.take_price = fill + take_distance
else:
leg.stop_price = fill + stop_distance
leg.take_price = fill - take_distance
def _apply_risk(self, candle):
pip = self._pip()
high = float(candle.HighPrice)
low = float(candle.LowPrice)
exit_sell = 0.0
exit_buy = 0.0
for i in range(len(self._legs) - 1, -1, -1):
leg = self._legs[i]
if leg.fill_price is None or candle.OpenTime <= leg.entry_time:
continue
fill = leg.fill_price
is_long = leg.side == Sides.Buy
leg.best_price = max(leg.best_price, high) if is_long else min(leg.best_price, low)
if pip is not None:
self._move_stop(leg, fill, pip)
if is_long:
hit = ((leg.stop_price is not None and low <= leg.stop_price) or
(leg.take_price is not None and high >= leg.take_price))
else:
hit = ((leg.stop_price is not None and high >= leg.stop_price) or
(leg.take_price is not None and low <= leg.take_price))
if not hit:
continue
if is_long:
exit_sell += leg.filled_volume
else:
exit_buy += leg.filled_volume
del self._legs[i]
if exit_sell > 0:
self.SellMarket(exit_sell)
if exit_buy > 0:
self.BuyMarket(exit_buy)
return exit_sell > 0 or exit_buy > 0
def _move_stop(self, leg, fill, pip):
is_long = leg.side == Sides.Buy
advance = leg.best_price - fill if is_long else fill - leg.best_price
if bool(self._enable_break_even.Value) and advance >= int(self._break_even_trigger.Value) * pip:
offset = int(self._break_even_offset.Value) * pip
self._tighten_stop(leg, fill + offset if is_long else fill - offset)
if bool(self._enable_trailing.Value) and int(self._trailing_stop.Value) > 0:
trail = int(self._trailing_stop.Value) * pip
if advance >= trail:
self._tighten_stop(leg, leg.best_price - trail if is_long else leg.best_price + trail)
@staticmethod
def _tighten_stop(leg, candidate):
if leg.stop_price is None or (candidate > leg.stop_price if leg.side == Sides.Buy else candidate < leg.stop_price):
leg.stop_price = candidate
def _flatten(self):
if self.Position > 0:
self.SellMarket(Math.Abs(self.Position))
elif self.Position < 0:
self.BuyMarket(Math.Abs(self.Position))
self._legs = []
def _pip(self):
security = self.Security
if security is None or security.PriceStep is None:
return None
step = float(security.PriceStep)
return step if step > 0 else None
def CreateClone(self):
return risk_reward_ratio_strategy()