Ichimoku Cloud Retrace 策略
本策略是 MetaTrader 专家顾问 “ichimok2005” 的 StockSharp 版本。它监控价格回落到 Ichimoku 云内部时的机会,并按照云层(kumo)的倾斜方向进场。所有判断均基于已完成的 K 线。
概览
- 适用于任何提供蜡烛图数据的品种和周期。
- 默认使用 Ichimoku 标准参数 9/26/52,所有周期均可调整。
- 支持做多和做空,仓位大小由策略的
Volume属性决定。 - 可选的止损/止盈以绝对价格单位设置,填 0 表示关闭。
指标与参数
- Ichimoku:Tenkan、Kijun、Senkou Span B 的周期可分别配置。
- 蜡烛类型:可选择连接所支持的任意聚合蜡烛类型(默认 1 小时)。
- Stop Loss Offset:距离开仓价的止损幅度,单位为价格,0 表示禁用。
- Take Profit Offset:距离开仓价的止盈幅度,单位为价格,0 表示禁用。
入场规则
做多条件
Senkou Span A高于Senkou Span B,表明云层为多头结构。- 当前完成的蜡烛为阳线(
Close > Open)。 - 收盘价位于云层内部(在两条 Span 之间)。
- 条件满足且当前无多头持仓时,策略发送市价买单,同时覆盖任意空头敞口并建立新的多头。
做空条件
Senkou Span B高于Senkou Span A,表明云层为空头结构。- 当前完成的蜡烛为阴线(
Open > Close)。 - 收盘价位于云层内部(在两条 Span 之间)。
- 条件满足且当前无空头持仓时,策略发送市价卖单,同时平掉多头并建立新的空头。
离场规则
- 反向信号会以一笔市价单同时完成平仓与反向开仓。
- 启用
Stop Loss Offset后,多单在EntryPrice - Offset平仓,空单在EntryPrice + Offset平仓(依据蜡烛收盘价)。 - 启用
Take Profit Offset后,多单在EntryPrice + Offset平仓,空单在EntryPrice - Offset平仓。 - 手动停止策略会清空内部记录的入场价格。
风险管理提示
- 偏移量以绝对价格表示,若使用点数/跳动点,请先换算成价格。
- 因为风险检查基于收盘价,短周期时建议使用较小的偏移量。
- 策略始终一次性平掉全部仓位,不包含分批或跟踪止损机制。
其他实现细节
- 通过高阶 API 订阅蜡烛,并使用
BindEx将 Ichimoku 指标绑定到订阅。 - 仅处理状态为
Finished的蜡烛,忽略过程中更新。 - 如果终端支持图表,会自动绘制价格、Ichimoku 云层以及成交记录。
ManageRisk在信号判定前执行,以确保保护性离场拥有更高优先级。
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()