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 cloud retrace strategy.
/// Takes trades when price pulls back inside the cloud in the direction of the kumo slope.
/// Uses optional fixed offsets for stop-loss and take-profit management.
/// </summary>
public class IchimokuCloudRetraceStrategy : Strategy
{
private readonly StrategyParam<int> _tenkanPeriod;
private readonly StrategyParam<int> _kijunPeriod;
private readonly StrategyParam<int> _senkouSpanBPeriod;
private readonly StrategyParam<decimal> _stopLossOffset;
private readonly StrategyParam<decimal> _takeProfitOffset;
private readonly StrategyParam<DataType> _candleType;
private decimal _entryPrice;
/// <summary>
/// Tenkan-sen period.
/// </summary>
public int TenkanPeriod
{
get => _tenkanPeriod.Value;
set => _tenkanPeriod.Value = value;
}
/// <summary>
/// Kijun-sen period.
/// </summary>
public int KijunPeriod
{
get => _kijunPeriod.Value;
set => _kijunPeriod.Value = value;
}
/// <summary>
/// Senkou Span B period.
/// </summary>
public int SenkouSpanBPeriod
{
get => _senkouSpanBPeriod.Value;
set => _senkouSpanBPeriod.Value = value;
}
/// <summary>
/// Stop-loss offset in price units. Set to zero to disable.
/// </summary>
public decimal StopLossOffset
{
get => _stopLossOffset.Value;
set => _stopLossOffset.Value = value;
}
/// <summary>
/// Take-profit offset in price units. Set to zero to disable.
/// </summary>
public decimal TakeProfitOffset
{
get => _takeProfitOffset.Value;
set => _takeProfitOffset.Value = value;
}
/// <summary>
/// Candle type used for signal calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes strategy parameters.
/// </summary>
public IchimokuCloudRetraceStrategy()
{
_tenkanPeriod = Param(nameof(TenkanPeriod), 9)
.SetGreaterThanZero()
.SetDisplay("Tenkan Period", "Tenkan-sen length", "Ichimoku Settings")
.SetOptimize(5, 15, 1);
_kijunPeriod = Param(nameof(KijunPeriod), 26)
.SetGreaterThanZero()
.SetDisplay("Kijun Period", "Kijun-sen length", "Ichimoku Settings")
.SetOptimize(20, 40, 2);
_senkouSpanBPeriod = Param(nameof(SenkouSpanBPeriod), 52)
.SetGreaterThanZero()
.SetDisplay("Senkou Span B Period", "Senkou Span B length", "Ichimoku Settings")
.SetOptimize(40, 70, 5);
_stopLossOffset = Param(nameof(StopLossOffset), 0m)
.SetDisplay("Stop Loss Offset", "Distance from entry for stop-loss (price units)", "Risk Management");
_takeProfitOffset = Param(nameof(TakeProfitOffset), 0m)
.SetDisplay("Take Profit Offset", "Distance from entry for take-profit (price units)", "Risk Management");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Type of candles for analysis", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
// Reset internal state values.
_entryPrice = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
// Prepare Ichimoku indicator with user-defined lengths.
var ichimoku = new Ichimoku
{
Tenkan = { Length = TenkanPeriod },
Kijun = { Length = KijunPeriod },
SenkouB = { Length = SenkouSpanBPeriod }
};
// Subscribe to candle data and bind the indicator for processing.
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(ichimoku, ProcessCandle)
.Start();
// Draw helper visuals if a chart is available.
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, ichimoku);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue ichimokuValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!ichimokuValue.IsFinal)
return;
// Manage open positions using the latest close before looking for new entries.
ManageRisk(candle);
if (Position == 0)
_entryPrice = 0m;
var ichimoku = (IchimokuValue)ichimokuValue;
if (ichimoku.SenkouA is not decimal senkouA ||
ichimoku.SenkouB is not decimal senkouB)
return;
var open = candle.OpenPrice;
var close = candle.ClosePrice;
var lowerSpan = Math.Min(senkouA, senkouB);
var upperSpan = Math.Max(senkouA, senkouB);
var priceInsideCloud = close > lowerSpan && close < upperSpan;
var bullishCloud = senkouA > senkouB;
var bearishCloud = senkouB > senkouA;
var shouldBuy = bullishCloud && close > open && priceInsideCloud;
var shouldSell = bearishCloud && open > close && priceInsideCloud;
if (shouldBuy && Position <= 0)
{
// Combine reversal and new entry volume in a single market order.
var volume = Volume + (Position < 0 ? Math.Abs(Position) : 0m);
if (volume > 0)
{
_entryPrice = close;
BuyMarket(volume);
}
}
else if (shouldSell && Position >= 0)
{
// Combine reversal and new entry volume in a single market order.
var volume = Volume + (Position > 0 ? Math.Abs(Position) : 0m);
if (volume > 0)
{
_entryPrice = close;
SellMarket(volume);
}
}
}
private void ManageRisk(ICandleMessage candle)
{
if (_entryPrice == 0m)
return;
var close = candle.ClosePrice;
if (Position > 0)
{
if (StopLossOffset > 0m && close <= _entryPrice - StopLossOffset)
{
var volumeToClose = Math.Abs(Position);
if (volumeToClose > 0m)
{
SellMarket(volumeToClose);
_entryPrice = 0m;
return;
}
}
if (TakeProfitOffset > 0m && close >= _entryPrice + TakeProfitOffset)
{
var volumeToClose = Math.Abs(Position);
if (volumeToClose > 0m)
{
SellMarket(volumeToClose);
_entryPrice = 0m;
}
}
}
else if (Position < 0)
{
if (StopLossOffset > 0m && close >= _entryPrice + StopLossOffset)
{
var volumeToClose = Math.Abs(Position);
if (volumeToClose > 0m)
{
BuyMarket(volumeToClose);
_entryPrice = 0m;
return;
}
}
if (TakeProfitOffset > 0m && close <= _entryPrice - TakeProfitOffset)
{
var volumeToClose = Math.Abs(Position);
if (volumeToClose > 0m)
{
BuyMarket(volumeToClose);
_entryPrice = 0m;
}
}
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import Ichimoku
from StockSharp.Algo.Strategies import Strategy
class ichimoku_cloud_retrace_strategy(Strategy):
"""Ichimoku cloud retrace: trades when price pulls back inside the cloud in direction of kumo slope."""
def __init__(self):
super(ichimoku_cloud_retrace_strategy, self).__init__()
self._tenkan_period = self.Param("TenkanPeriod", 9) \
.SetGreaterThanZero() \
.SetDisplay("Tenkan Period", "Tenkan-sen length", "Ichimoku Settings")
self._kijun_period = self.Param("KijunPeriod", 26) \
.SetGreaterThanZero() \
.SetDisplay("Kijun Period", "Kijun-sen length", "Ichimoku Settings")
self._senkou_span_b_period = self.Param("SenkouSpanBPeriod", 52) \
.SetGreaterThanZero() \
.SetDisplay("Senkou Span B Period", "Senkou Span B length", "Ichimoku Settings")
self._stop_loss_offset = self.Param("StopLossOffset", 0.0) \
.SetDisplay("Stop Loss Offset", "Distance from entry for stop-loss (price units)", "Risk Management")
self._take_profit_offset = self.Param("TakeProfitOffset", 0.0) \
.SetDisplay("Take Profit Offset", "Distance from entry for take-profit (price units)", "Risk Management")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Candle Type", "Type of candles for analysis", "General")
self._entry_price = 0.0
@property
def TenkanPeriod(self):
return int(self._tenkan_period.Value)
@property
def KijunPeriod(self):
return int(self._kijun_period.Value)
@property
def SenkouSpanBPeriod(self):
return int(self._senkou_span_b_period.Value)
@property
def StopLossOffset(self):
return float(self._stop_loss_offset.Value)
@property
def TakeProfitOffset(self):
return float(self._take_profit_offset.Value)
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(ichimoku_cloud_retrace_strategy, self).OnStarted2(time)
self._entry_price = 0.0
ichimoku = Ichimoku()
ichimoku.Tenkan.Length = self.TenkanPeriod
ichimoku.Kijun.Length = self.KijunPeriod
ichimoku.SenkouB.Length = self.SenkouSpanBPeriod
self._ichimoku = ichimoku
subscription = self.SubscribeCandles(self.CandleType)
subscription.BindEx(ichimoku, self.process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, ichimoku)
self.DrawOwnTrades(area)
def process_candle(self, candle, ichimoku_value):
if candle.State != CandleStates.Finished:
return
if not ichimoku_value.IsFinal:
return
self._manage_risk(candle)
if self.Position == 0:
self._entry_price = 0.0
senkou_a_val = ichimoku_value.SenkouA
senkou_b_val = ichimoku_value.SenkouB
if senkou_a_val is None or senkou_b_val is None:
return
senkou_a = float(senkou_a_val)
senkou_b = float(senkou_b_val)
open_p = float(candle.OpenPrice)
close = float(candle.ClosePrice)
lower_span = min(senkou_a, senkou_b)
upper_span = max(senkou_a, senkou_b)
price_inside_cloud = close > lower_span and close < upper_span
bullish_cloud = senkou_a > senkou_b
bearish_cloud = senkou_b > senkou_a
should_buy = bullish_cloud and close > open_p and price_inside_cloud
should_sell = bearish_cloud and open_p > close and price_inside_cloud
if should_buy and self.Position <= 0:
self._entry_price = close
self.BuyMarket()
elif should_sell and self.Position >= 0:
self._entry_price = close
self.SellMarket()
def _manage_risk(self, candle):
if self._entry_price == 0.0:
return
close = float(candle.ClosePrice)
if self.Position > 0:
if self.StopLossOffset > 0 and close <= self._entry_price - self.StopLossOffset:
self.SellMarket()
self._entry_price = 0.0
return
if self.TakeProfitOffset > 0 and close >= self._entry_price + self.TakeProfitOffset:
self.SellMarket()
self._entry_price = 0.0
elif self.Position < 0:
if self.StopLossOffset > 0 and close >= self._entry_price + self.StopLossOffset:
self.BuyMarket()
self._entry_price = 0.0
return
if self.TakeProfitOffset > 0 and close <= self._entry_price - self.TakeProfitOffset:
self.BuyMarket()
self._entry_price = 0.0
def OnReseted(self):
super(ichimoku_cloud_retrace_strategy, self).OnReseted()
self._entry_price = 0.0
def CreateClone(self):
return ichimoku_cloud_retrace_strategy()