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>
/// Ichimoku oscillator strategy converted from the MQL Exp_ICHI_OSC expert.
/// Generates entries based on color transitions of the smoothed oscillator derived from Ichimoku lines.
/// </summary>
public class IchiOscillatorStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _ichimokuBasePeriod;
private readonly StrategyParam<SmoothingMethods> _smoothingMethod;
private readonly StrategyParam<int> _smoothingLength;
private readonly StrategyParam<int> _smoothingPhase;
private readonly StrategyParam<int> _signalBar;
private readonly StrategyParam<bool> _buyEntriesEnabled;
private readonly StrategyParam<bool> _sellEntriesEnabled;
private readonly StrategyParam<bool> _buyExitsEnabled;
private readonly StrategyParam<bool> _sellExitsEnabled;
private readonly StrategyParam<int> _stopLossPoints;
private readonly StrategyParam<int> _takeProfitPoints;
private readonly StrategyParam<decimal> _orderVolume;
private Ichimoku _ichimoku = null!;
private DecimalLengthIndicator _smoother = null!;
private readonly List<int> _colorHistory = new();
private decimal? _previousSmoothed;
private TimeSpan _timeShift;
/// <summary>
/// Initializes a new instance of the <see cref="IchiOscillatorStrategy"/> class.
/// </summary>
public IchiOscillatorStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Timeframe used for Ichimoku calculations", "General");
_ichimokuBasePeriod = Param(nameof(IchimokuBasePeriod), 22)
.SetGreaterThanZero()
.SetDisplay("Ichimoku Base", "Base value to derive Tenkan, Kijun and Senkou spans", "Ichimoku")
.SetOptimize(10, 40, 2);
_smoothingMethod = Param(nameof(Smoothing), SmoothingMethods.Jurik)
.SetDisplay("Smoothing Method", "Moving average applied to the oscillator", "Oscillator");
_smoothingLength = Param(nameof(SmoothingLength), 5)
.SetGreaterThanZero()
.SetDisplay("Smoothing Length", "Length for oscillator smoothing", "Oscillator")
.SetOptimize(3, 25, 1);
_smoothingPhase = Param(nameof(SmoothingPhase), 15)
.SetDisplay("Smoothing Phase", "Additional phase parameter for selected smoothing", "Oscillator");
_signalBar = Param(nameof(SignalBar), 1)
.SetNotNegative()
.SetDisplay("Signal Bar", "Bar shift used for signal confirmation", "Logic");
_buyEntriesEnabled = Param(nameof(BuyEntriesEnabled), true)
.SetDisplay("Enable Buy Entries", "Allow opening long positions", "Logic");
_sellEntriesEnabled = Param(nameof(SellEntriesEnabled), true)
.SetDisplay("Enable Sell Entries", "Allow opening short positions", "Logic");
_buyExitsEnabled = Param(nameof(BuyExitsEnabled), true)
.SetDisplay("Enable Buy Exits", "Allow closing long positions", "Logic");
_sellExitsEnabled = Param(nameof(SellExitsEnabled), true)
.SetDisplay("Enable Sell Exits", "Allow closing short positions", "Logic");
_stopLossPoints = Param(nameof(StopLossPoints), 1000)
.SetNotNegative()
.SetDisplay("Stop Loss (points)", "Protective stop distance in price steps", "Risk Management")
.SetOptimize(200, 2000, 200);
_takeProfitPoints = Param(nameof(TakeProfitPoints), 2000)
.SetNotNegative()
.SetDisplay("Take Profit (points)", "Protective take-profit distance in price steps", "Risk Management")
.SetOptimize(200, 4000, 200);
_orderVolume = Param(nameof(OrderVolume), 1m)
.SetGreaterThanZero()
.SetDisplay("Order Volume", "Base order volume for market orders", "General");
}
/// <summary>
/// Candle data type used for indicator calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Base Ichimoku period that controls Tenkan, Kijun and Senkou lengths.
/// </summary>
public int IchimokuBasePeriod
{
get => _ichimokuBasePeriod.Value;
set => _ichimokuBasePeriod.Value = value;
}
/// <summary>
/// Smoothing method applied to the oscillator.
/// </summary>
public SmoothingMethods Smoothing
{
get => _smoothingMethod.Value;
set => _smoothingMethod.Value = value;
}
/// <summary>
/// Oscillator smoothing length.
/// </summary>
public int SmoothingLength
{
get => _smoothingLength.Value;
set => _smoothingLength.Value = value;
}
/// <summary>
/// Phase parameter for smoothing algorithms that support it.
/// </summary>
public int SmoothingPhase
{
get => _smoothingPhase.Value;
set => _smoothingPhase.Value = value;
}
/// <summary>
/// Bar offset used to confirm oscillator color transitions.
/// </summary>
public int SignalBar
{
get => _signalBar.Value;
set => _signalBar.Value = value;
}
/// <summary>
/// Enable opening of long positions.
/// </summary>
public bool BuyEntriesEnabled
{
get => _buyEntriesEnabled.Value;
set => _buyEntriesEnabled.Value = value;
}
/// <summary>
/// Enable opening of short positions.
/// </summary>
public bool SellEntriesEnabled
{
get => _sellEntriesEnabled.Value;
set => _sellEntriesEnabled.Value = value;
}
/// <summary>
/// Enable closing of existing long positions.
/// </summary>
public bool BuyExitsEnabled
{
get => _buyExitsEnabled.Value;
set => _buyExitsEnabled.Value = value;
}
/// <summary>
/// Enable closing of existing short positions.
/// </summary>
public bool SellExitsEnabled
{
get => _sellExitsEnabled.Value;
set => _sellExitsEnabled.Value = value;
}
/// <summary>
/// Stop-loss distance expressed in price steps.
/// </summary>
public int StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Take-profit distance expressed in price steps.
/// </summary>
public int TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Base volume used for market orders.
/// </summary>
public decimal OrderVolume
{
get => _orderVolume.Value;
set
{
_orderVolume.Value = value;
Volume = value;
}
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_colorHistory.Clear();
_previousSmoothed = null;
_ichimoku?.Reset();
_smoother?.Reset();
_timeShift = TimeSpan.Zero;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
Volume = OrderVolume;
var tenkanLength = Math.Max(1, (int)(IchimokuBasePeriod * 0.5m));
var kijunLength = Math.Max(1, (int)(IchimokuBasePeriod * 1.5m));
var senkouBLength = Math.Max(1, (int)(IchimokuBasePeriod * 3m));
_ichimoku = new Ichimoku
{
Tenkan = { Length = tenkanLength },
Kijun = { Length = kijunLength },
SenkouB = { Length = senkouBLength }
};
_smoother = CreateSmoother(Smoothing, SmoothingLength, SmoothingPhase);
_timeShift = CandleType.Arg is TimeSpan span && span > TimeSpan.Zero ? span : TimeSpan.Zero;
_colorHistory.Clear();
_previousSmoothed = null;
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(_ichimoku, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _smoother);
DrawOwnTrades(area);
}
StartProtection(
StopLossPoints > 0 ? new Unit(StopLossPoints, UnitTypes.Absolute) : null,
TakeProfitPoints > 0 ? new Unit(TakeProfitPoints, UnitTypes.Absolute) : null);
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue ichimokuValue)
{
if (candle.State != CandleStates.Finished)
return;
var ichimokuTyped = (IchimokuValue)ichimokuValue;
if (ichimokuTyped.Tenkan is not decimal tenkan ||
ichimokuTyped.Kijun is not decimal kijun ||
ichimokuTyped.SenkouA is not decimal senkouA)
{
return;
}
var step = Security?.PriceStep ?? 1m;
if (step == 0m)
step = 1m;
var markt = candle.ClosePrice - senkouA;
var trend = tenkan - kijun;
var rawOscillator = (markt - trend) / step;
var smoothValue = _smoother.Process(new DecimalIndicatorValue(_smoother, rawOscillator, candle.OpenTime) { IsFinal = true });
if (!smoothValue.IsFinal || smoothValue is not DecimalIndicatorValue smoothResult)
return;
var smoothed = smoothResult.Value;
UpdateColorHistory(smoothed);
if (_colorHistory.Count <= SignalBar + 1)
return;
var currentIndex = _colorHistory.Count - 1 - SignalBar;
var previousIndex = currentIndex - 1;
if (previousIndex < 0)
return;
var currentColor = _colorHistory[currentIndex];
var previousColor = _colorHistory[previousIndex];
var buyOpen = false;
var sellOpen = false;
var buyClose = false;
var sellClose = false;
if (previousColor == 0 || previousColor == 3)
{
sellClose = SellExitsEnabled;
if (BuyEntriesEnabled && (currentColor == 2 || currentColor == 1 || currentColor == 4))
buyOpen = true;
}
if (previousColor == 4 || previousColor == 1)
{
buyClose = BuyExitsEnabled;
if (SellEntriesEnabled && (currentColor == 0 || currentColor == 1 || currentColor == 3))
sellOpen = true;
}
var signalTime = candle.CloseTime + _timeShift;
if (buyClose && Position > 0)
{
SellMarket();
this.LogInfo($"[{signalTime}] Closing long at {candle.ClosePrice} due to oscillator color change {previousColor}->{currentColor}.");
}
if (sellClose && Position < 0)
{
BuyMarket();
this.LogInfo($"[{signalTime}] Closing short at {candle.ClosePrice} due to oscillator color change {previousColor}->{currentColor}.");
}
if (buyOpen && Position <= 0)
{
var volume = Volume + Math.Max(0m, -Position);
BuyMarket();
this.LogInfo($"[{signalTime}] Opening long at {candle.ClosePrice} with oscillator {smoothed:F5}.");
}
if (sellOpen && Position >= 0)
{
var volume = Volume + Math.Max(0m, Position);
SellMarket();
this.LogInfo($"[{signalTime}] Opening short at {candle.ClosePrice} with oscillator {smoothed:F5}.");
}
}
private void UpdateColorHistory(decimal smoothed)
{
var color = 2;
if (_previousSmoothed.HasValue)
{
var prev = _previousSmoothed.Value;
if (smoothed > 0m)
{
if (prev < smoothed)
color = 0;
else if (prev > smoothed)
color = 1;
}
else if (smoothed < 0m)
{
if (prev < smoothed)
color = 4;
else if (prev > smoothed)
color = 3;
}
}
else
{
if (smoothed > 0m)
color = 0;
else if (smoothed < 0m)
color = 3;
}
_colorHistory.Add(color);
_previousSmoothed = smoothed;
}
private DecimalLengthIndicator CreateSmoother(SmoothingMethods method, int length, int phase)
{
return method switch
{
SmoothingMethods.Simple => new SMA { Length = length },
SmoothingMethods.Exponential => new EMA { Length = length },
SmoothingMethods.Smoothed => new SmoothedMovingAverage { Length = length },
SmoothingMethods.Weighted => new WeightedMovingAverage { Length = length },
SmoothingMethods.Jurik => new JurikMovingAverage { Length = length },
SmoothingMethods.Kaufman => new KaufmanAdaptiveMovingAverage { Length = length },
_ => new JurikMovingAverage { Length = length }
};
}
/// <summary>
/// Supported smoothing algorithms for the oscillator.
/// </summary>
public enum SmoothingMethods
{
/// <summary>
/// Simple moving average.
/// </summary>
Simple,
/// <summary>
/// Exponential moving average.
/// </summary>
Exponential,
/// <summary>
/// Smoothed moving average.
/// </summary>
Smoothed,
/// <summary>
/// Weighted moving average.
/// </summary>
Weighted,
/// <summary>
/// Jurik moving average.
/// </summary>
Jurik,
/// <summary>
/// Kaufman adaptive moving average.
/// </summary>
Kaufman
}
}
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, Math, Decimal
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Indicators import (Ichimoku, SimpleMovingAverage, ExponentialMovingAverage,
SmoothedMovingAverage, WeightedMovingAverage, JurikMovingAverage, KaufmanAdaptiveMovingAverage)
from StockSharp.Algo.Strategies import Strategy
from indicator_extensions import *
SMOOTH_SIMPLE = 0
SMOOTH_EXPONENTIAL = 1
SMOOTH_SMOOTHED = 2
SMOOTH_WEIGHTED = 3
SMOOTH_JURIK = 4
SMOOTH_KAUFMAN = 5
class ichi_oscillator_strategy(Strategy):
def __init__(self):
super(ichi_oscillator_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4)))
self._ichimoku_base_period = self.Param("IchimokuBasePeriod", 22)
self._smoothing_method = self.Param("Smoothing", SMOOTH_JURIK)
self._smoothing_length = self.Param("SmoothingLength", 5)
self._smoothing_phase = self.Param("SmoothingPhase", 15)
self._signal_bar = self.Param("SignalBar", 1)
self._buy_entries_enabled = self.Param("BuyEntriesEnabled", True)
self._sell_entries_enabled = self.Param("SellEntriesEnabled", True)
self._buy_exits_enabled = self.Param("BuyExitsEnabled", True)
self._sell_exits_enabled = self.Param("SellExitsEnabled", True)
self._stop_loss_points = self.Param("StopLossPoints", 1000)
self._take_profit_points = self.Param("TakeProfitPoints", 2000)
self._order_volume = self.Param("OrderVolume", 1.0)
self._color_history = []
self._previous_smoothed = None
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def IchimokuBasePeriod(self):
return self._ichimoku_base_period.Value
@IchimokuBasePeriod.setter
def IchimokuBasePeriod(self, value):
self._ichimoku_base_period.Value = value
@property
def Smoothing(self):
return self._smoothing_method.Value
@Smoothing.setter
def Smoothing(self, value):
self._smoothing_method.Value = value
@property
def SmoothingLength(self):
return self._smoothing_length.Value
@SmoothingLength.setter
def SmoothingLength(self, value):
self._smoothing_length.Value = value
@property
def SmoothingPhase(self):
return self._smoothing_phase.Value
@SmoothingPhase.setter
def SmoothingPhase(self, value):
self._smoothing_phase.Value = value
@property
def SignalBar(self):
return self._signal_bar.Value
@SignalBar.setter
def SignalBar(self, value):
self._signal_bar.Value = value
@property
def BuyEntriesEnabled(self):
return self._buy_entries_enabled.Value
@BuyEntriesEnabled.setter
def BuyEntriesEnabled(self, value):
self._buy_entries_enabled.Value = value
@property
def SellEntriesEnabled(self):
return self._sell_entries_enabled.Value
@SellEntriesEnabled.setter
def SellEntriesEnabled(self, value):
self._sell_entries_enabled.Value = value
@property
def BuyExitsEnabled(self):
return self._buy_exits_enabled.Value
@BuyExitsEnabled.setter
def BuyExitsEnabled(self, value):
self._buy_exits_enabled.Value = value
@property
def SellExitsEnabled(self):
return self._sell_exits_enabled.Value
@SellExitsEnabled.setter
def SellExitsEnabled(self, value):
self._sell_exits_enabled.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 TakeProfitPoints(self):
return self._take_profit_points.Value
@TakeProfitPoints.setter
def TakeProfitPoints(self, value):
self._take_profit_points.Value = value
@property
def OrderVolume(self):
return self._order_volume.Value
@OrderVolume.setter
def OrderVolume(self, value):
self._order_volume.Value = value
def _create_smoother(self, method, length):
m = int(method)
if m == SMOOTH_SIMPLE:
ind = SimpleMovingAverage()
ind.Length = length
return ind
elif m == SMOOTH_EXPONENTIAL:
ind = ExponentialMovingAverage()
ind.Length = length
return ind
elif m == SMOOTH_SMOOTHED:
ind = SmoothedMovingAverage()
ind.Length = length
return ind
elif m == SMOOTH_WEIGHTED:
ind = WeightedMovingAverage()
ind.Length = length
return ind
elif m == SMOOTH_KAUFMAN:
ind = KaufmanAdaptiveMovingAverage()
ind.Length = length
return ind
else:
ind = JurikMovingAverage()
ind.Length = length
return ind
def OnStarted2(self, time):
super(ichi_oscillator_strategy, self).OnStarted2(time)
base_period = int(self.IchimokuBasePeriod)
tenkan_length = max(1, int(base_period * 0.5))
kijun_length = max(1, int(base_period * 1.5))
senkou_b_length = max(1, int(base_period * 3))
self._ichimoku = Ichimoku()
self._ichimoku.Tenkan.Length = tenkan_length
self._ichimoku.Kijun.Length = kijun_length
self._ichimoku.SenkouB.Length = senkou_b_length
self._smoother = self._create_smoother(self.Smoothing, int(self.SmoothingLength))
self._color_history = []
self._previous_smoothed = None
subscription = self.SubscribeCandles(self.CandleType)
subscription.BindEx(self._ichimoku, self.ProcessCandle).Start()
sl = int(self.StopLossPoints)
tp = int(self.TakeProfitPoints)
sl_unit = Unit(sl, UnitTypes.Absolute) if sl > 0 else None
tp_unit = Unit(tp, UnitTypes.Absolute) if tp > 0 else None
self.StartProtection(sl_unit, tp_unit)
def ProcessCandle(self, candle, ichimoku_value):
if candle.State != CandleStates.Finished:
return
tenkan = ichimoku_value.Tenkan
kijun = ichimoku_value.Kijun
senkou_a = ichimoku_value.SenkouA
if tenkan is None or kijun is None or senkou_a is None:
return
tenkan_val = float(tenkan)
kijun_val = float(kijun)
senkou_a_val = float(senkou_a)
close = float(candle.ClosePrice)
step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 1.0
if step == 0.0:
step = 1.0
markt = close - senkou_a_val
trend = tenkan_val - kijun_val
raw_oscillator = (markt - trend) / step
smooth_result = process_float(self._smoother, Decimal(raw_oscillator), candle.OpenTime, True)
if not smooth_result.IsFinal:
return
smoothed = float(smooth_result)
self._update_color_history(smoothed)
sb = int(self.SignalBar)
if len(self._color_history) <= sb + 1:
return
current_index = len(self._color_history) - 1 - sb
previous_index = current_index - 1
if previous_index < 0:
return
current_color = self._color_history[current_index]
previous_color = self._color_history[previous_index]
buy_open = False
sell_open = False
buy_close = False
sell_close = False
if previous_color == 0 or previous_color == 3:
sell_close = self.SellExitsEnabled
if self.BuyEntriesEnabled and (current_color == 2 or current_color == 1 or current_color == 4):
buy_open = True
if previous_color == 4 or previous_color == 1:
buy_close = self.BuyExitsEnabled
if self.SellEntriesEnabled and (current_color == 0 or current_color == 1 or current_color == 3):
sell_open = True
if buy_close and self.Position > 0:
self.SellMarket()
if sell_close and self.Position < 0:
self.BuyMarket()
if buy_open and self.Position <= 0:
self.BuyMarket()
if sell_open and self.Position >= 0:
self.SellMarket()
def _update_color_history(self, smoothed):
color = 2
if self._previous_smoothed is not None:
prev = self._previous_smoothed
if smoothed > 0.0:
if prev < smoothed:
color = 0
elif prev > smoothed:
color = 1
elif smoothed < 0.0:
if prev < smoothed:
color = 4
elif prev > smoothed:
color = 3
else:
if smoothed > 0.0:
color = 0
elif smoothed < 0.0:
color = 3
self._color_history.append(color)
self._previous_smoothed = smoothed
def OnReseted(self):
super(ichi_oscillator_strategy, self).OnReseted()
self._color_history = []
self._previous_smoothed = None
def CreateClone(self):
return ichi_oscillator_strategy()