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>
/// Trend-following strategy that replicates the Color XMUV expert advisor with a trading session filter.
/// </summary>
public class ColorXmuvTimeStrategy : Strategy
{
private readonly StrategyParam<int> _maxColorHistory;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<decimal> _orderVolume;
private readonly StrategyParam<bool> _enableBuyEntries;
private readonly StrategyParam<bool> _enableSellEntries;
private readonly StrategyParam<bool> _enableBuyExits;
private readonly StrategyParam<bool> _enableSellExits;
private readonly StrategyParam<bool> _useTimeFilter;
private readonly StrategyParam<int> _startHour;
private readonly StrategyParam<int> _startMinute;
private readonly StrategyParam<int> _endHour;
private readonly StrategyParam<int> _endMinute;
private readonly StrategyParam<SmoothMethods> _xmaMethod;
private readonly StrategyParam<int> _xLength;
private readonly StrategyParam<int> _xPhase;
private readonly StrategyParam<int> _signalBar;
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<decimal> _takeProfitPoints;
private readonly List<TrendColors> _colorHistory = new();
private IIndicator _xma = null!;
private decimal? _previousXmuv;
/// <summary>
/// Type of candles for the indicator calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Volume for new market orders.
/// </summary>
public decimal OrderVolume
{
get => _orderVolume.Value;
set => _orderVolume.Value = value;
}
/// <summary>
/// Allow opening long positions.
/// </summary>
public bool EnableBuyEntries
{
get => _enableBuyEntries.Value;
set => _enableBuyEntries.Value = value;
}
/// <summary>
/// Allow opening short positions.
/// </summary>
public bool EnableSellEntries
{
get => _enableSellEntries.Value;
set => _enableSellEntries.Value = value;
}
/// <summary>
/// Allow closing long positions on bearish signals.
/// </summary>
public bool EnableBuyExits
{
get => _enableBuyExits.Value;
set => _enableBuyExits.Value = value;
}
/// <summary>
/// Allow closing short positions on bullish signals.
/// </summary>
public bool EnableSellExits
{
get => _enableSellExits.Value;
set => _enableSellExits.Value = value;
}
/// <summary>
/// Enable restriction of trading by time window.
/// </summary>
public bool UseTimeFilter
{
get => _useTimeFilter.Value;
set => _useTimeFilter.Value = value;
}
/// <summary>
/// Start hour for the trading session (00-23).
/// </summary>
public int StartHour
{
get => _startHour.Value;
set => _startHour.Value = value;
}
/// <summary>
/// Start minute for the trading session (00-59).
/// </summary>
public int StartMinute
{
get => _startMinute.Value;
set => _startMinute.Value = value;
}
/// <summary>
/// End hour for the trading session (00-23).
/// </summary>
public int EndHour
{
get => _endHour.Value;
set => _endHour.Value = value;
}
/// <summary>
/// End minute for the trading session (00-59).
/// </summary>
public int EndMinute
{
get => _endMinute.Value;
set => _endMinute.Value = value;
}
/// <summary>
/// Smoothing method used by the Color XMUV line.
/// </summary>
public SmoothMethods XmaMethod
{
get => _xmaMethod.Value;
set => _xmaMethod.Value = value;
}
/// <summary>
/// Length of the smoothing window.
/// </summary>
public int XLength
{
get => _xLength.Value;
set => _xLength.Value = value;
}
/// <summary>
/// Auxiliary phase parameter retained from the original expert advisor.
/// </summary>
public int XPhase
{
get => _xPhase.Value;
set => _xPhase.Value = value;
}
/// <summary>
/// Number of completed bars to delay signal confirmation.
/// </summary>
public int SignalBar
{
get => _signalBar.Value;
set => _signalBar.Value = value;
}
/// <summary>
/// Maximum number of stored trend color values.
/// </summary>
public int MaxColorHistory
{
get => _maxColorHistory.Value;
set
{
_maxColorHistory.Value = value;
TrimColorHistory();
}
}
/// <summary>
/// Stop loss size in points (converted to absolute price using the instrument price step).
/// </summary>
public decimal StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Take profit size in points (converted to absolute price using the instrument price step).
/// </summary>
public decimal TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Initialize <see cref="ColorXmuvTimeStrategy"/>.
/// </summary>
public ColorXmuvTimeStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Source candles for the Color XMUV line", "General");
_orderVolume = Param(nameof(OrderVolume), 1m)
.SetGreaterThanZero()
.SetDisplay("Order Volume", "Size of market orders", "Trading");
_enableBuyEntries = Param(nameof(EnableBuyEntries), true)
.SetDisplay("Enable Long Entries", "Allow entering long positions", "Trading");
_enableSellEntries = Param(nameof(EnableSellEntries), true)
.SetDisplay("Enable Short Entries", "Allow entering short positions", "Trading");
_enableBuyExits = Param(nameof(EnableBuyExits), true)
.SetDisplay("Close Longs", "Close long positions on bearish flips", "Trading");
_enableSellExits = Param(nameof(EnableSellExits), true)
.SetDisplay("Close Shorts", "Close short positions on bullish flips", "Trading");
_useTimeFilter = Param(nameof(UseTimeFilter), false)
.SetDisplay("Use Time Filter", "Restrict trading to the specified session", "Time Filter");
_startHour = Param(nameof(StartHour), 0)
.SetRange(0, 23)
.SetDisplay("Start Hour", "Trading session start hour", "Time Filter");
_startMinute = Param(nameof(StartMinute), 0)
.SetRange(0, 59)
.SetDisplay("Start Minute", "Trading session start minute", "Time Filter");
_endHour = Param(nameof(EndHour), 23)
.SetRange(0, 23)
.SetDisplay("End Hour", "Trading session end hour", "Time Filter");
_endMinute = Param(nameof(EndMinute), 59)
.SetRange(0, 59)
.SetDisplay("End Minute", "Trading session end minute", "Time Filter");
_xmaMethod = Param(nameof(XmaMethod), SmoothMethods.Sma)
.SetDisplay("Smoothing Method", "Algorithm for the Color XMUV line", "Indicator");
_xLength = Param(nameof(XLength), 14)
.SetGreaterThanZero()
.SetDisplay("Length", "Smoothing length", "Indicator");
_xPhase = Param(nameof(XPhase), 15)
.SetDisplay("Phase", "Additional phase parameter for exotic smoothers", "Indicator");
_signalBar = Param(nameof(SignalBar), 1)
.SetRange(0, 10)
.SetDisplay("Signal Bar", "Number of completed bars to delay signals", "Indicator");
_maxColorHistory = Param(nameof(MaxColorHistory), 64)
.SetRange(2, 512)
.SetDisplay("Max Color History", "Maximum stored trend color values", "Indicator");
_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");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_colorHistory.Clear();
_previousXmuv = null;
_xma = null!;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_xma = CreateMovingAverage(XmaMethod, XLength);
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle).Start();
StartProtection(CreateTakeProfitUnit(), CreateStopLossUnit());
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
{
return;
}
var price = CalculateSignalPrice(candle);
var indicatorValue = _xma.Process(new DecimalIndicatorValue(_xma, price, candle.OpenTime) { IsFinal = true });
if (!_xma.IsFormed)
{
_previousXmuv = indicatorValue.ToDecimal();
return;
}
var xmuv = indicatorValue.ToDecimal();
var color = DetermineColor(xmuv);
StoreColor(color);
_previousXmuv = xmuv;
if (!TryGetSignalColors(SignalBar, out var currentColor, out var previousColor))
{
return;
}
// No bound indicators via .Bind, always allow.
var inSession = !UseTimeFilter || IsInsideSession(candle.CloseTime);
if (!inSession)
{
ForceExitIfNeeded();
return;
}
var bullishFlip = currentColor == TrendColors.Bullish && previousColor != TrendColors.Bullish;
var bearishFlip = currentColor == TrendColors.Bearish && previousColor != TrendColors.Bearish;
if (bullishFlip)
{
if (Position < 0 && EnableSellExits)
{
// Close short position
BuyMarket();
}
else if (Position == 0 && EnableBuyEntries)
{
// Open new long
BuyMarket();
}
}
else if (bearishFlip)
{
if (Position > 0 && EnableBuyExits)
{
// Close long position
SellMarket();
}
else if (Position == 0 && EnableSellEntries)
{
// Open new short
SellMarket();
}
}
}
private decimal CalculateSignalPrice(ICandleMessage candle)
{
if (candle.ClosePrice < candle.OpenPrice)
{
return (candle.LowPrice + candle.ClosePrice) / 2m;
}
if (candle.ClosePrice > candle.OpenPrice)
{
return (candle.HighPrice + candle.ClosePrice) / 2m;
}
return candle.ClosePrice;
}
private TrendColors DetermineColor(decimal currentXmuv)
{
if (_previousXmuv is not decimal previous)
{
return TrendColors.Neutral;
}
if (currentXmuv > previous)
{
return TrendColors.Bullish;
}
if (currentXmuv < previous)
{
return TrendColors.Bearish;
}
return TrendColors.Neutral;
}
private void StoreColor(TrendColors color)
{
var maxSize = Math.Clamp(SignalBar + 2, 2, MaxColorHistory);
_colorHistory.Add(color);
if (_colorHistory.Count > maxSize)
{
_colorHistory.RemoveAt(0);
}
}
private void TrimColorHistory()
{
var limit = Math.Max(2, MaxColorHistory);
while (_colorHistory.Count > limit)
{
_colorHistory.RemoveAt(0);
}
}
private bool TryGetSignalColors(int offset, out TrendColors current, out TrendColors previous)
{
current = TrendColors.Neutral;
previous = TrendColors.Neutral;
var count = _colorHistory.Count;
if (count <= offset)
{
return false;
}
var index = count - 1 - offset;
if (index <= 0)
{
return false;
}
current = _colorHistory[index];
previous = _colorHistory[index - 1];
return true;
}
private bool IsInsideSession(DateTimeOffset time)
{
var start = new TimeSpan(StartHour, StartMinute, 0);
var end = new TimeSpan(EndHour, EndMinute, 0);
var moment = time.TimeOfDay;
if (start == end)
{
return moment >= start && moment < end;
}
if (start < end)
{
return moment >= start && moment <= end;
}
return moment >= start || moment <= end;
}
private void ForceExitIfNeeded()
{
if (Position > 0 && EnableBuyExits)
{
SellMarket();
}
else if (Position < 0 && EnableSellExits)
{
BuyMarket();
}
}
private Unit CreateStopLossUnit()
{
if (StopLossPoints <= 0 || Security?.PriceStep is not decimal step || step <= 0)
{
return default;
}
return new Unit(step * StopLossPoints, UnitTypes.Absolute);
}
private Unit CreateTakeProfitUnit()
{
if (TakeProfitPoints <= 0 || Security?.PriceStep is not decimal step || step <= 0)
{
return default;
}
return new Unit(step * TakeProfitPoints, UnitTypes.Absolute);
}
private IIndicator CreateMovingAverage(SmoothMethods method, int length)
{
return method switch
{
SmoothMethods.Sma => new SMA { Length = length },
SmoothMethods.Ema => new EMA { Length = length },
SmoothMethods.Smma => new SmoothedMovingAverage { Length = length },
SmoothMethods.Lwma => new WeightedMovingAverage { Length = length },
SmoothMethods.Jjma => new EMA { Length = length },
SmoothMethods.Jurx => new EMA { Length = length },
SmoothMethods.Parma => new WeightedMovingAverage { Length = length },
SmoothMethods.T3 => new EMA { Length = length },
SmoothMethods.Vidya => new EMA { Length = length },
SmoothMethods.Ama => new KaufmanAdaptiveMovingAverage { Length = length },
_ => new EMA { Length = length },
};
}
private enum TrendColors
{
Bearish = 0,
Neutral = 1,
Bullish = 2
}
/// <summary>
/// Smoothing methods supported by the Color XMUV indicator.
/// </summary>
public enum SmoothMethods
{
Sma,
Ema,
Smma,
Lwma,
Jjma,
Jurx,
Parma,
T3,
Vidya,
Ama
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.BusinessEntities")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Decimal
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Algo.Indicators import SimpleMovingAverage
from indicator_extensions import *
# TrendColors: 0=Bearish, 1=Neutral, 2=Bullish
BEARISH = 0
NEUTRAL = 1
BULLISH = 2
class color_xmuv_time_strategy(Strategy):
"""Color XMUV trend-following strategy with session filter."""
def __init__(self):
super(color_xmuv_time_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Candle Type", "Source candles for the Color XMUV line", "General")
self._order_volume = self.Param("OrderVolume", Decimal(1)) \
.SetGreaterThanZero() \
.SetDisplay("Order Volume", "Size of market orders", "Trading")
self._enable_buy_entries = self.Param("EnableBuyEntries", True) \
.SetDisplay("Enable Long Entries", "Allow entering long positions", "Trading")
self._enable_sell_entries = self.Param("EnableSellEntries", True) \
.SetDisplay("Enable Short Entries", "Allow entering short positions", "Trading")
self._enable_buy_exits = self.Param("EnableBuyExits", True) \
.SetDisplay("Close Longs", "Close long positions on bearish flips", "Trading")
self._enable_sell_exits = self.Param("EnableSellExits", True) \
.SetDisplay("Close Shorts", "Close short positions on bullish flips", "Trading")
self._use_time_filter = self.Param("UseTimeFilter", False) \
.SetDisplay("Use Time Filter", "Restrict trading to the specified session", "Time Filter")
self._start_hour = self.Param("StartHour", 0) \
.SetDisplay("Start Hour", "Trading session start hour", "Time Filter")
self._start_minute = self.Param("StartMinute", 0) \
.SetDisplay("Start Minute", "Trading session start minute", "Time Filter")
self._end_hour = self.Param("EndHour", 23) \
.SetDisplay("End Hour", "Trading session end hour", "Time Filter")
self._end_minute = self.Param("EndMinute", 59) \
.SetDisplay("End Minute", "Trading session end minute", "Time Filter")
self._x_length = self.Param("XLength", 14) \
.SetGreaterThanZero() \
.SetDisplay("Length", "Smoothing length", "Indicator")
self._signal_bar = self.Param("SignalBar", 1) \
.SetDisplay("Signal Bar", "Number of completed bars to delay signals", "Indicator")
self._max_color_history = self.Param("MaxColorHistory", 64) \
.SetDisplay("Max Color History", "Maximum stored trend color values", "Indicator")
self._stop_loss_points = self.Param("StopLossPoints", Decimal(0)) \
.SetDisplay("Stop Loss (pts)", "Stop loss distance in points", "Risk")
self._take_profit_points = self.Param("TakeProfitPoints", Decimal(0)) \
.SetDisplay("Take Profit (pts)", "Take profit distance in points", "Risk")
self._color_history = []
self._previous_xmuv = None
self._xma = None
@property
def CandleType(self):
return self._candle_type.Value
@property
def OrderVolume(self):
return self._order_volume.Value
@property
def EnableBuyEntries(self):
return self._enable_buy_entries.Value
@property
def EnableSellEntries(self):
return self._enable_sell_entries.Value
@property
def EnableBuyExits(self):
return self._enable_buy_exits.Value
@property
def EnableSellExits(self):
return self._enable_sell_exits.Value
@property
def UseTimeFilter(self):
return self._use_time_filter.Value
@property
def StartHour(self):
return self._start_hour.Value
@property
def StartMinute(self):
return self._start_minute.Value
@property
def EndHour(self):
return self._end_hour.Value
@property
def EndMinute(self):
return self._end_minute.Value
@property
def XLength(self):
return self._x_length.Value
@property
def SignalBar(self):
return self._signal_bar.Value
@property
def MaxColorHistory(self):
return self._max_color_history.Value
@property
def StopLossPoints(self):
return self._stop_loss_points.Value
@property
def TakeProfitPoints(self):
return self._take_profit_points.Value
def OnStarted2(self, time):
super(color_xmuv_time_strategy, self).OnStarted2(time)
self._xma = SimpleMovingAverage()
self._xma.Length = self.XLength
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.process_candle).Start()
self.StartProtection(self._create_tp_unit(), self._create_sl_unit())
def _create_sl_unit(self):
sl = self.StopLossPoints
if sl <= 0:
return Unit()
sec = self.Security
if sec is None or sec.PriceStep is None:
return Unit()
step = sec.PriceStep
if step <= 0:
return Unit()
return Unit(step * sl, UnitTypes.Absolute)
def _create_tp_unit(self):
tp = self.TakeProfitPoints
if tp <= 0:
return Unit()
sec = self.Security
if sec is None or sec.PriceStep is None:
return Unit()
step = sec.PriceStep
if step <= 0:
return Unit()
return Unit(step * tp, UnitTypes.Absolute)
def process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
price = self._calc_signal_price(candle)
ind_out = process_float(self._xma, price, candle.OpenTime, True)
if not self._xma.IsFormed:
self._previous_xmuv = float(ind_out)
return
xmuv = float(ind_out)
color = self._determine_color(xmuv)
self._store_color(color)
self._previous_xmuv = xmuv
sb = self.SignalBar
count = len(self._color_history)
if count <= sb:
return
idx = count - 1 - sb
if idx <= 0:
return
current_color = self._color_history[idx]
previous_color = self._color_history[idx - 1]
in_session = (not self.UseTimeFilter) or self._is_inside_session(candle.CloseTime)
if not in_session:
self._force_exit_if_needed()
return
bullish_flip = current_color == BULLISH and previous_color != BULLISH
bearish_flip = current_color == BEARISH and previous_color != BEARISH
if bullish_flip:
if self.Position < 0 and self.EnableSellExits:
self.BuyMarket()
elif self.Position == 0 and self.EnableBuyEntries:
self.BuyMarket()
elif bearish_flip:
if self.Position > 0 and self.EnableBuyExits:
self.SellMarket()
elif self.Position == 0 and self.EnableSellEntries:
self.SellMarket()
def _calc_signal_price(self, candle):
c = float(candle.ClosePrice)
o = float(candle.OpenPrice)
h = float(candle.HighPrice)
lo = float(candle.LowPrice)
if c < o:
return Decimal((lo + c) / 2.0)
if c > o:
return Decimal((h + c) / 2.0)
return candle.ClosePrice
def _determine_color(self, current_xmuv):
if self._previous_xmuv is None:
return NEUTRAL
if current_xmuv > self._previous_xmuv:
return BULLISH
if current_xmuv < self._previous_xmuv:
return BEARISH
return NEUTRAL
def _store_color(self, color):
max_size = max(self.SignalBar + 2, 2)
if max_size > self.MaxColorHistory:
max_size = self.MaxColorHistory
self._color_history.append(color)
if len(self._color_history) > max_size:
self._color_history.pop(0)
def _force_exit_if_needed(self):
if self.Position > 0 and self.EnableBuyExits:
self.SellMarket()
elif self.Position < 0 and self.EnableSellExits:
self.BuyMarket()
def _is_inside_session(self, time):
start = TimeSpan(self.StartHour, self.StartMinute, 0)
end = TimeSpan(self.EndHour, self.EndMinute, 0)
moment = time.TimeOfDay
if start == end:
return moment >= start and moment < end
if start < end:
return moment >= start and moment <= end
return moment >= start or moment <= end
def OnReseted(self):
super(color_xmuv_time_strategy, self).OnReseted()
self._color_history = []
self._previous_xmuv = None
self._xma = None
def CreateClone(self):
return color_xmuv_time_strategy()