using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Currencyprofits strategy that trades trend pullbacks into the recent channel extremes.
/// </summary>
public class CurrencyprofitsHighLowChannelStrategy : Strategy
{
private readonly StrategyParam<int> _fastLength;
private readonly StrategyParam<int> _slowLength;
private readonly StrategyParam<int> _channelLength;
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<decimal> _riskPercent;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<CandlePrices> _priceSource;
private readonly StrategyParam<MovingAverageTypes> _fastMaType;
private readonly StrategyParam<MovingAverageTypes> _slowMaType;
private readonly StrategyParam<int> _signalCooldownBars;
private decimal? _previousFast;
private decimal? _previousSlow;
private decimal? _previousHighest;
private decimal? _previousLowest;
private decimal _entryPrice;
private decimal _stopPrice;
private int _processedCandles;
private int _cooldownRemaining;
public int FastLength
{
get => _fastLength.Value;
set => _fastLength.Value = value;
}
public int SlowLength
{
get => _slowLength.Value;
set => _slowLength.Value = value;
}
public int ChannelLength
{
get => _channelLength.Value;
set => _channelLength.Value = value;
}
public decimal StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
public decimal RiskPercent
{
get => _riskPercent.Value;
set => _riskPercent.Value = value;
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public CandlePrices PriceSource
{
get => _priceSource.Value;
set => _priceSource.Value = value;
}
public MovingAverageTypes FastMaType
{
get => _fastMaType.Value;
set => _fastMaType.Value = value;
}
public MovingAverageTypes SlowMaType
{
get => _slowMaType.Value;
set => _slowMaType.Value = value;
}
public int SignalCooldownBars
{
get => _signalCooldownBars.Value;
set => _signalCooldownBars.Value = value;
}
private int RequiredBars => Math.Max(Math.Max(FastLength, SlowLength), ChannelLength) + 1;
public CurrencyprofitsHighLowChannelStrategy()
{
_fastLength = Param(nameof(FastLength), 32)
.SetDisplay("Fast MA Length", "Length of the fast moving average", "Indicators")
.SetOptimize(10, 120, 2);
_slowLength = Param(nameof(SlowLength), 86)
.SetDisplay("Slow MA Length", "Length of the slow moving average", "Indicators")
.SetOptimize(20, 200, 2);
_channelLength = Param(nameof(ChannelLength), 12)
.SetDisplay("Channel Lookback", "Number of previous candles for high/low channel", "Indicators")
.SetOptimize(3, 20, 1);
_stopLossPoints = Param(nameof(StopLossPoints), 170m)
.SetDisplay("Stop Loss (points)", "Distance to stop loss expressed in price steps", "Risk");
_riskPercent = Param(nameof(RiskPercent), 0.14m)
.SetDisplay("Risk Fraction", "Fraction of portfolio capital risked per trade", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
.SetDisplay("Candle Type", "Primary timeframe for calculations", "General");
_priceSource = Param(nameof(PriceSource), CandlePrices.Close)
.SetDisplay("MA Price Source", "Price source used by both moving averages", "Indicators");
_fastMaType = Param(nameof(FastMaType), MovingAverageTypes.Simple)
.SetDisplay("Fast MA Type", "Moving average algorithm for the fast line", "Indicators");
_slowMaType = Param(nameof(SlowMaType), MovingAverageTypes.Simple)
.SetDisplay("Slow MA Type", "Moving average algorithm for the slow line", "Indicators");
_signalCooldownBars = Param(nameof(SignalCooldownBars), 4)
.SetNotNegative()
.SetDisplay("Signal Cooldown Bars", "Closed candles to wait before the next entry", "Risk");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_previousFast = null;
_previousSlow = null;
_previousHighest = null;
_previousLowest = null;
_entryPrice = 0m;
_stopPrice = 0m;
_processedCandles = 0;
_cooldownRemaining = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var fastMa = CreateMovingAverage(FastMaType, FastLength, PriceSource);
var slowMa = CreateMovingAverage(SlowMaType, SlowLength, PriceSource);
var highest = new Highest { Length = ChannelLength };
var lowest = new Lowest { Length = ChannelLength };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(fastMa, slowMa, highest, lowest, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal fast, decimal slow, decimal channelHigh, decimal channelLow)
{
if (candle.State != CandleStates.Finished)
return;
if (_cooldownRemaining > 0)
_cooldownRemaining--;
_processedCandles++;
if (_processedCandles <= RequiredBars)
{
// Collect enough history before taking any decisions.
_previousFast = fast;
_previousSlow = slow;
_previousHighest = channelHigh;
_previousLowest = channelLow;
return;
}
if (_previousFast is null || _previousSlow is null || _previousHighest is null || _previousLowest is null)
{
_previousFast = fast;
_previousSlow = slow;
_previousHighest = channelHigh;
_previousLowest = channelLow;
return;
}
if (Position > 0)
{
// Exit long trades when price breaks the opposite channel or the protective stop.
var exitByChannel = candle.ClosePrice >= _previousHighest.Value;
var exitByStop = _stopPrice > 0m && candle.LowPrice <= _stopPrice;
if (exitByChannel || exitByStop)
{
SellMarket(Position);
ResetTradeState();
_cooldownRemaining = SignalCooldownBars;
}
}
else if (Position < 0)
{
// Exit short trades when price hits the lower boundary or the stop.
var exitByChannel = candle.ClosePrice <= _previousLowest.Value;
var exitByStop = _stopPrice > 0m && candle.HighPrice >= _stopPrice;
if (exitByChannel || exitByStop)
{
BuyMarket(-Position);
ResetTradeState();
_cooldownRemaining = SignalCooldownBars;
}
}
else if (_cooldownRemaining == 0)
{
var stopDistance = GetStopDistance();
if (stopDistance > 0m)
{
var bullishTrend = _previousFast.Value > _previousSlow.Value && fast > slow;
var bearishTrend = _previousFast.Value < _previousSlow.Value && fast < slow;
var bullishReversal = candle.LowPrice <= _previousLowest.Value && candle.ClosePrice > candle.OpenPrice && candle.ClosePrice > fast;
var bearishReversal = candle.HighPrice >= _previousHighest.Value && candle.ClosePrice < candle.OpenPrice && candle.ClosePrice < fast;
// Long entries require a bullish trend and a pullback to the recent low channel.
if (bullishTrend && bullishReversal)
{
var volume = CalculatePositionSize(stopDistance);
if (volume > 0m)
{
BuyMarket(volume);
_entryPrice = candle.ClosePrice;
_stopPrice = _entryPrice - stopDistance;
_cooldownRemaining = SignalCooldownBars;
}
}
// Short entries require a bearish trend and a retest of the recent high channel.
else if (bearishTrend && bearishReversal)
{
var volume = CalculatePositionSize(stopDistance);
if (volume > 0m)
{
SellMarket(volume);
_entryPrice = candle.ClosePrice;
_stopPrice = _entryPrice + stopDistance;
_cooldownRemaining = SignalCooldownBars;
}
}
}
}
_previousFast = fast;
_previousSlow = slow;
_previousHighest = channelHigh;
_previousLowest = channelLow;
}
private decimal GetStopDistance()
{
if (StopLossPoints <= 0m)
return 0m;
var priceStep = Security?.PriceStep ?? 0m;
if (priceStep > 0m)
return StopLossPoints * priceStep;
return StopLossPoints;
}
private decimal CalculatePositionSize(decimal stopDistance)
{
var defaultVolume = AdjustVolume(Volume);
if (stopDistance <= 0m)
return defaultVolume;
var portfolioValue = Portfolio?.CurrentValue;
if (portfolioValue is null || portfolioValue <= 0m || RiskPercent <= 0m)
return defaultVolume;
var riskCapital = portfolioValue.Value * RiskPercent;
var priceStep = Security?.PriceStep ?? 0m;
var stepPrice = GetSecurityValue<decimal?>(Level1Fields.StepPrice) ?? 0m;
decimal riskPerContract;
if (priceStep > 0m && stepPrice > 0m)
{
// Convert the stop distance into cash risk per contract using exchange specifications.
riskPerContract = stopDistance / priceStep * stepPrice;
}
else
{
// Fallback when the security does not expose step metadata.
riskPerContract = stopDistance;
}
if (riskPerContract <= 0m)
return defaultVolume;
var desiredVolume = riskCapital / riskPerContract;
return AdjustVolume(desiredVolume);
}
private decimal AdjustVolume(decimal volume)
{
if (volume <= 0m)
return 0m;
var step = Security?.VolumeStep ?? 0m;
if (step > 0m)
{
var steps = decimal.Floor(volume / step);
volume = steps * step;
}
var minVolume = Security?.MinVolume ?? 0m;
if (minVolume > 0m && volume < minVolume)
volume = minVolume;
var maxVolume = Security?.MaxVolume ?? 0m;
if (maxVolume > 0m && volume > maxVolume)
volume = maxVolume;
return volume;
}
private void ResetTradeState()
{
// Clear cached execution details after a position has been closed.
_entryPrice = 0m;
_stopPrice = 0m;
}
private static DecimalLengthIndicator CreateMovingAverage(MovingAverageTypes type, int length, CandlePrices price)
{
return type switch
{
MovingAverageTypes.Simple => new SMA { Length = length },
MovingAverageTypes.Exponential => new EMA { Length = length },
MovingAverageTypes.Smoothed => new SmoothedMovingAverage { Length = length },
MovingAverageTypes.Weighted => new WeightedMovingAverage { Length = length },
_ => new SMA { Length = length },
};
}
public enum MovingAverageTypes
{
Simple,
Exponential,
Smoothed,
Weighted,
}
public enum CandlePrices
{
Open,
High,
Low,
Close,
Median,
Typical,
Weighted
}
}
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, Unit, UnitTypes
from StockSharp.Algo.Indicators import (SimpleMovingAverage, ExponentialMovingAverage,
SmoothedMovingAverage, WeightedMovingAverage, Highest, Lowest)
from StockSharp.Algo.Strategies import Strategy
MA_SIMPLE = 0
MA_EXPONENTIAL = 1
MA_SMOOTHED = 2
MA_WEIGHTED = 3
class currencyprofits_high_low_channel_strategy(Strategy):
def __init__(self):
super(currencyprofits_high_low_channel_strategy, self).__init__()
self._fast_length = self.Param("FastLength", 32)
self._slow_length = self.Param("SlowLength", 86)
self._channel_length = self.Param("ChannelLength", 12)
self._stop_loss_points = self.Param("StopLossPoints", 170.0)
self._risk_percent = self.Param("RiskPercent", 0.14)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(30)))
self._fast_ma_type = self.Param("FastMaType", MA_SIMPLE)
self._slow_ma_type = self.Param("SlowMaType", MA_SIMPLE)
self._signal_cooldown_bars = self.Param("SignalCooldownBars", 4)
self._previous_fast = None
self._previous_slow = None
self._previous_highest = None
self._previous_lowest = None
self._entry_price = 0.0
self._stop_price = 0.0
self._processed_candles = 0
self._cooldown_remaining = 0
@property
def FastLength(self):
return self._fast_length.Value
@FastLength.setter
def FastLength(self, value):
self._fast_length.Value = value
@property
def SlowLength(self):
return self._slow_length.Value
@SlowLength.setter
def SlowLength(self, value):
self._slow_length.Value = value
@property
def ChannelLength(self):
return self._channel_length.Value
@ChannelLength.setter
def ChannelLength(self, value):
self._channel_length.Value = value
@property
def StopLossPoints(self):
return self._stop_loss_points.Value
@StopLossPoints.setter
def StopLossPoints(self, value):
self._stop_loss_points.Value = value
@property
def RiskPercent(self):
return self._risk_percent.Value
@RiskPercent.setter
def RiskPercent(self, value):
self._risk_percent.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def FastMaType(self):
return self._fast_ma_type.Value
@FastMaType.setter
def FastMaType(self, value):
self._fast_ma_type.Value = value
@property
def SlowMaType(self):
return self._slow_ma_type.Value
@SlowMaType.setter
def SlowMaType(self, value):
self._slow_ma_type.Value = value
@property
def SignalCooldownBars(self):
return self._signal_cooldown_bars.Value
@SignalCooldownBars.setter
def SignalCooldownBars(self, value):
self._signal_cooldown_bars.Value = value
def _create_ma(self, ma_type, length):
t = int(ma_type)
if t == MA_EXPONENTIAL:
ind = ExponentialMovingAverage()
ind.Length = length
return ind
elif t == MA_SMOOTHED:
ind = SmoothedMovingAverage()
ind.Length = length
return ind
elif t == MA_WEIGHTED:
ind = WeightedMovingAverage()
ind.Length = length
return ind
else:
ind = SimpleMovingAverage()
ind.Length = length
return ind
def OnStarted2(self, time):
super(currencyprofits_high_low_channel_strategy, self).OnStarted2(time)
fast_ma = self._create_ma(self.FastMaType, self.FastLength)
slow_ma = self._create_ma(self.SlowMaType, self.SlowLength)
highest = Highest()
highest.Length = self.ChannelLength
lowest = Lowest()
lowest.Length = self.ChannelLength
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(fast_ma, slow_ma, highest, lowest, self.ProcessCandle).Start()
self.StartProtection(
Unit(2000.0, UnitTypes.Absolute),
Unit(1000.0, UnitTypes.Absolute))
def ProcessCandle(self, candle, fast, slow, channel_high, channel_low):
if candle.State != CandleStates.Finished:
return
if self._cooldown_remaining > 0:
self._cooldown_remaining -= 1
self._processed_candles += 1
fast_val = float(fast)
slow_val = float(slow)
ch_high = float(channel_high)
ch_low = float(channel_low)
close = float(candle.ClosePrice)
open_price = float(candle.OpenPrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
required = max(max(int(self.FastLength), int(self.SlowLength)), int(self.ChannelLength)) + 1
if self._processed_candles <= required:
self._previous_fast = fast_val
self._previous_slow = slow_val
self._previous_highest = ch_high
self._previous_lowest = ch_low
return
if self._previous_fast is None or self._previous_slow is None or self._previous_highest is None or self._previous_lowest is None:
self._previous_fast = fast_val
self._previous_slow = slow_val
self._previous_highest = ch_high
self._previous_lowest = ch_low
return
if self.Position > 0:
exit_by_channel = close >= self._previous_highest
exit_by_stop = self._stop_price > 0.0 and low <= self._stop_price
if exit_by_channel or exit_by_stop:
self.SellMarket()
self._reset_trade_state()
self._cooldown_remaining = int(self.SignalCooldownBars)
elif self.Position < 0:
exit_by_channel = close <= self._previous_lowest
exit_by_stop = self._stop_price > 0.0 and high >= self._stop_price
if exit_by_channel or exit_by_stop:
self.BuyMarket()
self._reset_trade_state()
self._cooldown_remaining = int(self.SignalCooldownBars)
elif self._cooldown_remaining == 0:
stop_distance = self._get_stop_distance()
if stop_distance > 0.0:
bullish_trend = self._previous_fast > self._previous_slow and fast_val > slow_val
bearish_trend = self._previous_fast < self._previous_slow and fast_val < slow_val
bullish_reversal = low <= self._previous_lowest and close > open_price and close > fast_val
bearish_reversal = high >= self._previous_highest and close < open_price and close < fast_val
if bullish_trend and bullish_reversal:
self.BuyMarket()
self._entry_price = close
self._stop_price = self._entry_price - stop_distance
self._cooldown_remaining = int(self.SignalCooldownBars)
elif bearish_trend and bearish_reversal:
self.SellMarket()
self._entry_price = close
self._stop_price = self._entry_price + stop_distance
self._cooldown_remaining = int(self.SignalCooldownBars)
self._previous_fast = fast_val
self._previous_slow = slow_val
self._previous_highest = ch_high
self._previous_lowest = ch_low
def _get_stop_distance(self):
sl = float(self.StopLossPoints)
if sl <= 0.0:
return 0.0
ps = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 0.0
if ps > 0.0:
return sl * ps
return sl
def _reset_trade_state(self):
self._entry_price = 0.0
self._stop_price = 0.0
def OnReseted(self):
super(currencyprofits_high_low_channel_strategy, self).OnReseted()
self._previous_fast = None
self._previous_slow = None
self._previous_highest = None
self._previous_lowest = None
self._entry_price = 0.0
self._stop_price = 0.0
self._processed_candles = 0
self._cooldown_remaining = 0
def CreateClone(self):
return currencyprofits_high_low_channel_strategy()