Kijun Sen Robot 策略
概述
Kijun Sen Robot 策略 是将 MetaTrader 5 专家顾问 "Kijun Sen Robot" 迁移到 StockSharp 高级策略 API 的版本。默认使用 30 分钟 K 线,通过观察价格突破 Ichimoku 基准线(Kijun-sen)并结合 20 周期线性加权均线(LWMA)确认趋势来执行交易。策略保留了原始 EA 仅在活跃交易时段操作、并使用动态止损、保本和移动止损保护仓位的理念。
指标与数据
- Ichimoku:Tenkan/Kijun/Senkou Span B 默认周期为 6/12/24。
- 线性加权移动平均线(LWMA):20 根 K 线,用于确认趋势方向及与 Kijun 的距离。
- K 线数据:默认使用 30 分钟周期,可通过
CandleType参数切换其他时间框架。
交易逻辑
多头入场
- 当根 K 线从下方穿越 Kijun 线(开盘在下方、收盘在上方或盘中触及),且上一根 K 线收盘也位于 Kijun 下方。
- 当前 Kijun 相比两根之前持平或抬升。
- LWMA 至少低于 Kijun
MaFilterPips(按品种最小价位换算)的距离。 - LWMA 斜率为正,即当前值高于上一根的值。
- 当前时间位于
TradingStartHour与TradingEndHour之间(默认 07:00–19:00)。
满足条件并且当前净头寸 ≤ 0 时,策略会以市价买入(若存在空头会先回补)。入场价格取当前 K 线收盘价。
空头入场
- 当前 K 线自上向下穿越 Kijun(逻辑与多头相反)。
- Kijun 相比两根之前持平或下移。
- LWMA 至少高于 Kijun
MaFilterPips的距离。 - LWMA 斜率为负,即当前值低于上一根的值。
- 仅在允许时间窗口内触发。
符合条件且净头寸 ≥ 0 时,策略以市价卖出(若存在多头会先平仓)。
仓位管理与退出
- 初始止损:在入场价下方(做多)或上方(做空)放置
StopLossPips的距离,按照品种PriceStep换算为价格,复刻原 EA 的保护性止损。 - 保本移动:当浮盈达到
BreakEvenPips后,将止损上调到入场价上方一个点(做多)或入场价下方一个点(做空)。 - 移动止损:浮盈达到
TrailingStopPips时,止损随价格按照该距离移动,仅朝有利方向调整。 - 固定止盈:
TakeProfitPips指定的盈利目标,设为 0 可关闭。 - Kijun 斜率退出:若 LWMA 在止损仍低于入场价前反向转折,则立即平仓,保留原 EA 的防御逻辑。
- 时间过滤:允许时段外不再开新仓,但已有仓位仍会持续执行上述保护规则。
- 订单类型:全部使用市价单;原策略根据报价选择挂单或市价的细节被简化,因为 StockSharp 使用 K 线数据而非逐笔行情。
若同一根 K 线同时触及止损与止盈,策略优先触发止损,以便在无盘中信息时采取保守处理。
参数
| 参数 | 默认值 | 说明 |
|---|---|---|
TenkanPeriod |
6 | Ichimoku Tenkan 周期。 |
KijunPeriod |
12 | Ichimoku Kijun 周期。 |
SenkouSpanBPeriod |
24 | Ichimoku Senkou Span B 周期。 |
LwmaPeriod |
20 | LWMA 趋势确认周期。 |
MaFilterPips |
6 | Kijun 与 LWMA 最小距离(单位:点)。 |
StopLossPips |
50 | 初始保护性止损。 |
BreakEvenPips |
9 | 触发保本的浮盈距离。 |
TrailingStopPips |
10 | 移动止损距离。 |
TakeProfitPips |
120 | 固定止盈距离(0 = 关闭)。 |
TradingStartHour |
7 | 允许交易起始小时(包含)。 |
TradingEndHour |
19 | 允许交易结束小时(不包含)。 |
CandleType |
30 分钟 | 信号使用的 K 线类型。 |
所有以点数表示的参数均根据品种的 PriceStep 转换成价格单位。当品种最小报价精度为 3 或 5 位小数时,会自动乘以 10,以模拟 MT5 中 digits_adjust 的处理方式。
实现说明
_pendingLongLevel与_pendingShortLevel用于模拟原 EA 中的longcross/shortcross状态变量,保证每次开仓都必须等待新的 Kijun 穿越。- MT5 中基于最后买/卖价的判断改为使用 K 线的开、高、低、收,适合在 StockSharp 回测环境下的确定性逻辑。
- 止损、保本和移动止损由策略内部跟踪并通过
ClosePosition()执行,而不是修改服务器端挂单。 ConvertPips使用Security.PriceStep或Security.MinPriceStep并结合 3/5 位小数的 10 倍调整,复刻原策略的点值换算。- 通过
SubscribeCandles().BindEx(...)绑定 Ichimoku 与 LWMA 指标,同时在图表区域绘制 K 线、指标与自有成交。
使用建议
- 选择支持 30 分钟 K 线的交易品种,可按需修改
CandleType。 - 启动前设置策略的
Volume属性为期望下单手数。 - 根据品种波动或已有优化结果调整相关点数参数。
- 在高阶回测或实时环境中运行,策略会自动执行时间过滤与仓位保护逻辑。
- 通过日志或图表观察保本/移动止损的触发情况。按照要求,代码中的注释全部使用英文。
本文件夹仅包含 C# 版本,未提供 Python 实现。
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 Kijun-sen robot converted from MetaTrader.
/// The strategy looks for price crossing the Kijun line while a 20-period LWMA confirms the trend.
/// It manages risk with time filters, configurable stop-loss, break-even and trailing rules.
/// </summary>
public class KijunSenRobotStrategy : Strategy
{
private readonly StrategyParam<int> _tenkanPeriod;
private readonly StrategyParam<int> _kijunPeriod;
private readonly StrategyParam<int> _senkouSpanBPeriod;
private readonly StrategyParam<int> _lwmaPeriod;
private readonly StrategyParam<decimal> _maFilterPips;
private readonly StrategyParam<decimal> _stopLossPips;
private readonly StrategyParam<decimal> _breakEvenPips;
private readonly StrategyParam<decimal> _trailingStopPips;
private readonly StrategyParam<decimal> _takeProfitPips;
private readonly StrategyParam<int> _tradingStartHour;
private readonly StrategyParam<int> _tradingEndHour;
private readonly StrategyParam<DataType> _candleType;
private Ichimoku _ichimoku = null!;
private WeightedMovingAverage _lwma = null!;
private decimal? _previousClose;
private decimal? _previousMa;
private decimal? _previousPrevMa;
private decimal? _previousKijun;
private decimal? _previousPrevKijun;
private decimal? _pendingLongLevel;
private decimal? _pendingShortLevel;
private bool? _isLongPosition;
private decimal? _entryPrice;
private decimal? _stopLossPrice;
private decimal? _takeProfitPrice;
private decimal _stopLossDistance;
private decimal _takeProfitDistance;
private decimal _breakEvenDistance;
private decimal _breakEvenStep;
private decimal _trailingDistance;
private bool _breakEvenApplied;
/// <summary>
/// Tenkan-sen calculation period.
/// </summary>
public int TenkanPeriod
{
get => _tenkanPeriod.Value;
set => _tenkanPeriod.Value = value;
}
/// <summary>
/// Kijun-sen calculation period.
/// </summary>
public int KijunPeriod
{
get => _kijunPeriod.Value;
set => _kijunPeriod.Value = value;
}
/// <summary>
/// Senkou Span B calculation period.
/// </summary>
public int SenkouSpanBPeriod
{
get => _senkouSpanBPeriod.Value;
set => _senkouSpanBPeriod.Value = value;
}
/// <summary>
/// Weighted moving average period used for slope confirmation.
/// </summary>
public int LwmaPeriod
{
get => _lwmaPeriod.Value;
set => _lwmaPeriod.Value = value;
}
/// <summary>
/// Minimum distance in pips between price and Kijun required by the LWMA filter.
/// </summary>
public decimal MaFilterPips
{
get => _maFilterPips.Value;
set => _maFilterPips.Value = value;
}
/// <summary>
/// Initial stop-loss in pips.
/// </summary>
public decimal StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Profit distance in pips required to move the stop-loss to break-even.
/// </summary>
public decimal BreakEvenPips
{
get => _breakEvenPips.Value;
set => _breakEvenPips.Value = value;
}
/// <summary>
/// Trailing stop distance in pips.
/// </summary>
public decimal TrailingStopPips
{
get => _trailingStopPips.Value;
set => _trailingStopPips.Value = value;
}
/// <summary>
/// Take-profit distance in pips.
/// </summary>
public decimal TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// First trading hour (inclusive) in exchange time.
/// </summary>
public int TradingStartHour
{
get => _tradingStartHour.Value;
set => _tradingStartHour.Value = value;
}
/// <summary>
/// Last trading hour (exclusive) in exchange time.
/// </summary>
public int TradingEndHour
{
get => _tradingEndHour.Value;
set => _tradingEndHour.Value = value;
}
/// <summary>
/// Candle type used by the strategy.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initialize <see cref="KijunSenRobotStrategy"/>.
/// </summary>
public KijunSenRobotStrategy()
{
_tenkanPeriod = Param(nameof(TenkanPeriod), 6)
.SetGreaterThanZero()
.SetDisplay("Tenkan Period", "Period for Ichimoku Tenkan line", "Ichimoku")
.SetOptimize(4, 12, 1);
_kijunPeriod = Param(nameof(KijunPeriod), 12)
.SetGreaterThanZero()
.SetDisplay("Kijun Period", "Period for Ichimoku Kijun line", "Ichimoku")
.SetOptimize(8, 20, 1);
_senkouSpanBPeriod = Param(nameof(SenkouSpanBPeriod), 24)
.SetGreaterThanZero()
.SetDisplay("Senkou Span B Period", "Period for Ichimoku Senkou Span B", "Ichimoku")
.SetOptimize(18, 30, 1);
_lwmaPeriod = Param(nameof(LwmaPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("LWMA Period", "Length of the confirmation LWMA", "Trend Filter")
.SetOptimize(10, 40, 2);
_maFilterPips = Param(nameof(MaFilterPips), 20m)
.SetNotNegative()
.SetDisplay("LWMA Filter (pips)", "Minimum distance between price and Kijun required by the LWMA", "Trend Filter")
.SetOptimize(0m, 20m, 1m);
_stopLossPips = Param(nameof(StopLossPips), 50m)
.SetNotNegative()
.SetDisplay("Stop Loss (pips)", "Initial protective stop distance", "Risk Management")
.SetOptimize(20m, 100m, 5m);
_breakEvenPips = Param(nameof(BreakEvenPips), 9m)
.SetNotNegative()
.SetDisplay("Break-even Trigger (pips)", "Profit distance required to protect the position", "Risk Management")
.SetOptimize(5m, 20m, 1m);
_trailingStopPips = Param(nameof(TrailingStopPips), 10m)
.SetNotNegative()
.SetDisplay("Trailing Stop (pips)", "Distance for the trailing stop after the position moves in profit", "Risk Management")
.SetOptimize(5m, 30m, 1m);
_takeProfitPips = Param(nameof(TakeProfitPips), 120m)
.SetNotNegative()
.SetDisplay("Take Profit (pips)", "Optional fixed profit target", "Risk Management")
.SetOptimize(40m, 200m, 10m);
_tradingStartHour = Param(nameof(TradingStartHour), 7)
.SetRange(0, 23)
.SetDisplay("Start Hour", "First trading hour (inclusive)", "Scheduling");
_tradingEndHour = Param(nameof(TradingEndHour), 19)
.SetRange(1, 24)
.SetDisplay("End Hour", "Last trading hour (exclusive)", "Scheduling");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Timeframe used for signal generation", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_ichimoku = null!;
_lwma = null!;
_previousClose = null;
_previousMa = null;
_previousPrevMa = null;
_previousKijun = null;
_previousPrevKijun = null;
_pendingLongLevel = null;
_pendingShortLevel = null;
ResetPositionState();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_ichimoku = new Ichimoku
{
Tenkan = { Length = TenkanPeriod },
Kijun = { Length = KijunPeriod },
SenkouB = { Length = SenkouSpanBPeriod }
};
_lwma = new WeightedMovingAverage
{
Length = LwmaPeriod
};
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(_ichimoku, _lwma, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _ichimoku);
DrawIndicator(area, _lwma);
DrawOwnTrades(area);
}
// protection handled manually via SL/TP/trailing
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue ichimokuValue, IIndicatorValue maValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!maValue.IsFinal)
return;
var ichimokuTyped = (IchimokuValue)ichimokuValue;
if (ichimokuTyped.Kijun is not decimal kijun)
return;
var maCurrent = maValue.ToDecimal();
ManageOpenPosition(candle, maCurrent);
if (IsFormedAndOnlineAndAllowTrading() && IsWithinTradingHours(candle.OpenTime))
{
EvaluateEntrySignals(candle, kijun, maCurrent);
}
UpdateHistory(candle, kijun, maCurrent);
}
private void ManageOpenPosition(ICandleMessage candle, decimal maCurrent)
{
if (Position == 0)
{
if (_isLongPosition != null || _entryPrice != null)
ResetPositionState();
return;
}
var isLong = Position > 0;
var actualEntry = _entryPrice ?? candle.ClosePrice;
if (_isLongPosition is null || _isLongPosition.Value != isLong || _entryPrice is null)
{
var entry = actualEntry != 0m ? actualEntry : candle.ClosePrice;
SetupPositionState(isLong, entry);
}
else if (actualEntry != 0m && _entryPrice.Value != actualEntry)
{
_entryPrice = actualEntry;
if (_isLongPosition.Value)
{
_stopLossPrice = _stopLossDistance > 0m ? _entryPrice - _stopLossDistance : null;
_takeProfitPrice = _takeProfitDistance > 0m ? _entryPrice + _takeProfitDistance : null;
}
else
{
_stopLossPrice = _stopLossDistance > 0m ? _entryPrice + _stopLossDistance : null;
_takeProfitPrice = _takeProfitDistance > 0m ? _entryPrice - _takeProfitDistance : null;
}
}
if (_entryPrice is not decimal entryPrice)
return;
_isLongPosition = isLong;
if (_previousMa.HasValue && _previousPrevMa.HasValue && _stopLossPrice.HasValue)
{
if (isLong && _stopLossPrice.Value < entryPrice && _previousMa.Value < _previousPrevMa.Value)
{
ClosePositionAndReset();
return;
}
if (!isLong && _stopLossPrice.Value > entryPrice && _previousMa.Value > _previousPrevMa.Value)
{
ClosePositionAndReset();
return;
}
}
if (isLong)
{
ApplyBreakEvenAndTrailingForLong(candle, entryPrice);
if (CheckStopLossHit(candle.LowPrice, _stopLossPrice))
{
ClosePositionAndReset();
return;
}
if (CheckTakeProfitHit(candle.HighPrice, _takeProfitPrice))
{
ClosePositionAndReset();
return;
}
}
else
{
ApplyBreakEvenAndTrailingForShort(candle, entryPrice);
if (CheckStopLossHitForShort(candle.HighPrice, _stopLossPrice))
{
ClosePositionAndReset();
return;
}
if (CheckTakeProfitHitForShort(candle.LowPrice, _takeProfitPrice))
{
ClosePositionAndReset();
return;
}
}
}
private void ApplyBreakEvenAndTrailingForLong(ICandleMessage candle, decimal entryPrice)
{
if (!_breakEvenApplied && _breakEvenDistance > 0m)
{
if (candle.ClosePrice - entryPrice >= _breakEvenDistance)
{
var newStop = entryPrice + (_breakEvenStep > 0m ? _breakEvenStep : 0m);
if (_stopLossPrice is not decimal currentStop || newStop > currentStop)
_stopLossPrice = newStop;
_breakEvenApplied = true;
}
}
if (_trailingDistance > 0m && candle.ClosePrice - entryPrice >= _trailingDistance)
{
var newStop = candle.ClosePrice - _trailingDistance;
if (_stopLossPrice is not decimal currentStop || newStop > currentStop)
_stopLossPrice = newStop;
}
}
private void ApplyBreakEvenAndTrailingForShort(ICandleMessage candle, decimal entryPrice)
{
if (!_breakEvenApplied && _breakEvenDistance > 0m)
{
if (entryPrice - candle.ClosePrice >= _breakEvenDistance)
{
var newStop = entryPrice - (_breakEvenStep > 0m ? _breakEvenStep : 0m);
if (_stopLossPrice is not decimal currentStop || newStop < currentStop)
_stopLossPrice = newStop;
_breakEvenApplied = true;
}
}
if (_trailingDistance > 0m && entryPrice - candle.ClosePrice >= _trailingDistance)
{
var newStop = candle.ClosePrice + _trailingDistance;
if (_stopLossPrice is not decimal currentStop || newStop < currentStop)
_stopLossPrice = newStop;
}
}
private static bool CheckStopLossHit(decimal lowPrice, decimal? stopPrice)
{
return stopPrice.HasValue && lowPrice <= stopPrice.Value;
}
private static bool CheckTakeProfitHit(decimal highPrice, decimal? takeProfitPrice)
{
return takeProfitPrice.HasValue && highPrice >= takeProfitPrice.Value;
}
private static bool CheckStopLossHitForShort(decimal highPrice, decimal? stopPrice)
{
return stopPrice.HasValue && highPrice >= stopPrice.Value;
}
private static bool CheckTakeProfitHitForShort(decimal lowPrice, decimal? takeProfitPrice)
{
return takeProfitPrice.HasValue && lowPrice <= takeProfitPrice.Value;
}
private void EvaluateEntrySignals(ICandleMessage candle, decimal kijun, decimal maCurrent)
{
if (_previousClose is not decimal previousClose ||
_previousMa is not decimal previousMa ||
_previousKijun is not decimal previousKijun)
{
return;
}
var maTrendUp = maCurrent > previousMa;
var maTrendDown = maCurrent < previousMa;
var filterOffset = ConvertPips(MaFilterPips);
var priceOpenedBelow = candle.OpenPrice < kijun;
var priceOpenedAbove = candle.OpenPrice > kijun;
var priceClosedAbove = candle.ClosePrice > kijun;
var priceClosedBelow = candle.ClosePrice < kijun;
var priceTouchedBelow = candle.LowPrice <= kijun;
var priceTouchedAbove = candle.HighPrice >= kijun;
var priceWasBelow = previousClose < previousKijun;
var priceWasAbove = previousClose > previousKijun;
var kijunNotFalling = !_previousPrevKijun.HasValue || kijun >= _previousPrevKijun.Value;
var kijunNotRising = !_previousPrevKijun.HasValue || kijun <= _previousPrevKijun.Value;
if (_pendingLongLevel is null)
{
if (priceClosedAbove && (priceOpenedBelow || priceWasBelow || priceTouchedBelow) && kijunNotFalling)
{
if (filterOffset <= 0m || maCurrent < kijun - filterOffset)
{
_pendingLongLevel = kijun;
_pendingShortLevel = null;
}
}
}
if (_pendingShortLevel is null)
{
if (priceClosedBelow && (priceOpenedAbove || priceWasAbove || priceTouchedAbove) && kijunNotRising)
{
if (filterOffset <= 0m || maCurrent > kijun + filterOffset)
{
_pendingShortLevel = kijun;
_pendingLongLevel = null;
}
}
}
var volume = Volume + Math.Abs(Position);
if (volume <= 0m)
return;
if (_pendingLongLevel.HasValue && maTrendUp && Position <= 0)
{
BuyMarket(volume);
SetupPositionState(true, candle.ClosePrice);
_pendingLongLevel = null;
_pendingShortLevel = null;
return;
}
if (_pendingShortLevel.HasValue && maTrendDown && Position >= 0)
{
SellMarket(volume);
SetupPositionState(false, candle.ClosePrice);
_pendingLongLevel = null;
_pendingShortLevel = null;
}
}
private void UpdateHistory(ICandleMessage candle, decimal kijun, decimal maCurrent)
{
_previousPrevKijun = _previousKijun;
_previousKijun = kijun;
_previousPrevMa = _previousMa;
_previousMa = maCurrent;
_previousClose = candle.ClosePrice;
}
private bool IsWithinTradingHours(DateTimeOffset time)
{
var hour = time.Hour;
return hour >= TradingStartHour && hour < TradingEndHour;
}
private void SetupPositionState(bool isLong, decimal entryPrice)
{
_isLongPosition = isLong;
_entryPrice = entryPrice;
_breakEvenApplied = false;
_stopLossDistance = ConvertPips(StopLossPips);
_takeProfitDistance = ConvertPips(TakeProfitPips);
_breakEvenDistance = ConvertPips(BreakEvenPips);
_breakEvenStep = ConvertPips(1m);
_trailingDistance = ConvertPips(TrailingStopPips);
_stopLossPrice = _stopLossDistance > 0m ? (isLong ? entryPrice - _stopLossDistance : entryPrice + _stopLossDistance) : null;
_takeProfitPrice = _takeProfitDistance > 0m ? (isLong ? entryPrice + _takeProfitDistance : entryPrice - _takeProfitDistance) : null;
}
private void ClosePositionAndReset()
{
if (Position != 0)
if (Position > 0) SellMarket(Math.Abs(Position)); else if (Position < 0) BuyMarket(Math.Abs(Position));
ResetPositionState();
}
private void ResetPositionState()
{
_isLongPosition = null;
_entryPrice = null;
_stopLossPrice = null;
_takeProfitPrice = null;
_stopLossDistance = 0m;
_takeProfitDistance = 0m;
_breakEvenDistance = 0m;
_breakEvenStep = 0m;
_trailingDistance = 0m;
_breakEvenApplied = false;
}
private decimal ConvertPips(decimal value)
{
if (value <= 0m)
return 0m;
var step = GetPipStep();
return value * step;
}
private decimal GetPipStep()
{
var priceStep = Security?.PriceStep;
if (priceStep is null || priceStep <= 0m)
return 1m;
var stepValue = priceStep.Value;
var decimals = GetDecimalPlaces(stepValue);
if (decimals == 3 || decimals == 5)
return stepValue * 10m;
return stepValue;
}
private static int GetDecimalPlaces(decimal value)
{
value = Math.Abs(value);
var decimals = 0;
while (value != Math.Truncate(value) && decimals < 10)
{
value *= 10m;
decimals++;
}
return decimals;
}
}
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 Ichimoku, WeightedMovingAverage, CandleIndicatorValue
from StockSharp.Algo.Strategies import Strategy
class kijun_sen_robot_strategy(Strategy):
def __init__(self):
super(kijun_sen_robot_strategy, self).__init__()
self._tenkan_period = self.Param("TenkanPeriod", 6)
self._kijun_period = self.Param("KijunPeriod", 12)
self._senkou_span_b_period = self.Param("SenkouSpanBPeriod", 24)
self._lwma_period = self.Param("LwmaPeriod", 20)
self._ma_filter_pips = self.Param("MaFilterPips", 20.0)
self._stop_loss_pips = self.Param("StopLossPips", 50.0)
self._break_even_pips = self.Param("BreakEvenPips", 9.0)
self._trailing_stop_pips = self.Param("TrailingStopPips", 10.0)
self._take_profit_pips = self.Param("TakeProfitPips", 120.0)
self._trading_start_hour = self.Param("TradingStartHour", 7)
self._trading_end_hour = self.Param("TradingEndHour", 19)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4)))
self._previous_close = None
self._previous_ma = None
self._previous_prev_ma = None
self._previous_kijun = None
self._previous_prev_kijun = None
self._pending_long_level = None
self._pending_short_level = None
self._is_long = None
self._entry_price = None
self._stop_loss_price = None
self._take_profit_price = None
self._sl_distance = 0.0
self._tp_distance = 0.0
self._be_distance = 0.0
self._be_step = 0.0
self._trail_distance = 0.0
self._be_applied = False
@property
def TenkanPeriod(self):
return self._tenkan_period.Value
@TenkanPeriod.setter
def TenkanPeriod(self, value):
self._tenkan_period.Value = value
@property
def KijunPeriod(self):
return self._kijun_period.Value
@KijunPeriod.setter
def KijunPeriod(self, value):
self._kijun_period.Value = value
@property
def SenkouSpanBPeriod(self):
return self._senkou_span_b_period.Value
@SenkouSpanBPeriod.setter
def SenkouSpanBPeriod(self, value):
self._senkou_span_b_period.Value = value
@property
def LwmaPeriod(self):
return self._lwma_period.Value
@LwmaPeriod.setter
def LwmaPeriod(self, value):
self._lwma_period.Value = value
@property
def MaFilterPips(self):
return self._ma_filter_pips.Value
@MaFilterPips.setter
def MaFilterPips(self, value):
self._ma_filter_pips.Value = value
@property
def StopLossPips(self):
return self._stop_loss_pips.Value
@StopLossPips.setter
def StopLossPips(self, value):
self._stop_loss_pips.Value = value
@property
def BreakEvenPips(self):
return self._break_even_pips.Value
@BreakEvenPips.setter
def BreakEvenPips(self, value):
self._break_even_pips.Value = value
@property
def TrailingStopPips(self):
return self._trailing_stop_pips.Value
@TrailingStopPips.setter
def TrailingStopPips(self, value):
self._trailing_stop_pips.Value = value
@property
def TakeProfitPips(self):
return self._take_profit_pips.Value
@TakeProfitPips.setter
def TakeProfitPips(self, value):
self._take_profit_pips.Value = value
@property
def TradingStartHour(self):
return self._trading_start_hour.Value
@TradingStartHour.setter
def TradingStartHour(self, value):
self._trading_start_hour.Value = value
@property
def TradingEndHour(self):
return self._trading_end_hour.Value
@TradingEndHour.setter
def TradingEndHour(self, value):
self._trading_end_hour.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def OnStarted2(self, time):
super(kijun_sen_robot_strategy, self).OnStarted2(time)
self._previous_close = None
self._previous_ma = None
self._previous_prev_ma = None
self._previous_kijun = None
self._previous_prev_kijun = None
self._pending_long_level = None
self._pending_short_level = None
self._reset_position_state()
ichimoku = Ichimoku()
ichimoku.Tenkan.Length = self.TenkanPeriod
ichimoku.Kijun.Length = self.KijunPeriod
ichimoku.SenkouB.Length = self.SenkouSpanBPeriod
lwma = WeightedMovingAverage()
lwma.Length = self.LwmaPeriod
self._ichimoku = ichimoku
self._lwma = lwma
subscription = self.SubscribeCandles(self.CandleType)
subscription.BindEx(ichimoku, lwma, self.ProcessCandle).Start()
# protection handled manually via SL/TP/trailing
def ProcessCandle(self, candle, ichi_value, ma_value):
if candle.State != CandleStates.Finished:
return
if not ma_value.IsFinal:
return
ma_current = float(ma_value)
if not ichi_value.IsFinal:
self._update_history(candle, None, ma_current)
return
kijun = ichi_value.Kijun
if kijun is None:
self._update_history(candle, None, ma_current)
return
kijun_val = float(kijun)
self._manage_open_position(candle, ma_current)
if self.IsFormedAndOnlineAndAllowTrading():
hour = candle.OpenTime.Hour
if hour >= int(self.TradingStartHour) and hour < int(self.TradingEndHour):
self._evaluate_entry_signals(candle, kijun_val, ma_current)
self._update_history(candle, kijun_val, ma_current)
def _manage_open_position(self, candle, ma_current):
if self.Position == 0:
if self._is_long is not None or self._entry_price is not None:
self._reset_position_state()
return
is_long = self.Position > 0
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
actual_entry = self._entry_price if self._entry_price is not None else close
if self._is_long is None or self._is_long != is_long or self._entry_price is None:
entry_val = actual_entry if actual_entry != 0.0 else close
self._setup_position_state(is_long, entry_val)
elif actual_entry != 0.0 and self._entry_price != actual_entry:
self._entry_price = actual_entry
if self._is_long:
self._stop_loss_price = self._entry_price - self._sl_distance if self._sl_distance > 0.0 else None
self._take_profit_price = self._entry_price + self._tp_distance if self._tp_distance > 0.0 else None
else:
self._stop_loss_price = self._entry_price + self._sl_distance if self._sl_distance > 0.0 else None
self._take_profit_price = self._entry_price - self._tp_distance if self._tp_distance > 0.0 else None
entry = self._entry_price if self._entry_price is not None else close
self._is_long = is_long
if self._previous_ma is not None and self._previous_prev_ma is not None and self._stop_loss_price is not None:
if is_long and self._stop_loss_price < entry and self._previous_ma < self._previous_prev_ma:
self._close_position_and_reset()
return
if not is_long and self._stop_loss_price > entry and self._previous_ma > self._previous_prev_ma:
self._close_position_and_reset()
return
if is_long:
self._apply_be_trailing_long(candle, entry)
if self._stop_loss_price is not None and low <= self._stop_loss_price:
self._close_position_and_reset()
return
if self._take_profit_price is not None and high >= self._take_profit_price:
self._close_position_and_reset()
return
else:
self._apply_be_trailing_short(candle, entry)
if self._stop_loss_price is not None and high >= self._stop_loss_price:
self._close_position_and_reset()
return
if self._take_profit_price is not None and low <= self._take_profit_price:
self._close_position_and_reset()
return
def _apply_be_trailing_long(self, candle, entry):
close = float(candle.ClosePrice)
if not self._be_applied and self._be_distance > 0.0:
if close - entry >= self._be_distance:
new_stop = entry + (self._be_step if self._be_step > 0.0 else 0.0)
if self._stop_loss_price is None or new_stop > self._stop_loss_price:
self._stop_loss_price = new_stop
self._be_applied = True
if self._trail_distance > 0.0 and close - entry >= self._trail_distance:
new_stop = close - self._trail_distance
if self._stop_loss_price is None or new_stop > self._stop_loss_price:
self._stop_loss_price = new_stop
def _apply_be_trailing_short(self, candle, entry):
close = float(candle.ClosePrice)
if not self._be_applied and self._be_distance > 0.0:
if entry - close >= self._be_distance:
new_stop = entry - (self._be_step if self._be_step > 0.0 else 0.0)
if self._stop_loss_price is None or new_stop < self._stop_loss_price:
self._stop_loss_price = new_stop
self._be_applied = True
if self._trail_distance > 0.0 and entry - close >= self._trail_distance:
new_stop = close + self._trail_distance
if self._stop_loss_price is None or new_stop < self._stop_loss_price:
self._stop_loss_price = new_stop
def _evaluate_entry_signals(self, candle, kijun, ma_current):
if self._previous_close is None or self._previous_ma is None or self._previous_kijun is None:
return
close = float(candle.ClosePrice)
open_price = float(candle.OpenPrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
prev_close = self._previous_close
prev_kijun = self._previous_kijun
prev_ma = self._previous_ma
filter_offset = self._convert_pips(float(self.MaFilterPips))
kijun_not_falling = self._previous_prev_kijun is None or kijun >= self._previous_prev_kijun
kijun_not_rising = self._previous_prev_kijun is None or kijun <= self._previous_prev_kijun
if self._pending_long_level is None:
price_closed_above = close > kijun
price_opened_below = open_price < kijun
price_was_below = prev_close < prev_kijun
price_touched_below = low <= kijun
if price_closed_above and (price_opened_below or price_was_below or price_touched_below) and kijun_not_falling:
if filter_offset <= 0.0 or ma_current < kijun - filter_offset:
self._pending_long_level = kijun
self._pending_short_level = None
if self._pending_short_level is None:
price_closed_below = close < kijun
price_opened_above = open_price > kijun
price_was_above = prev_close > prev_kijun
price_touched_above = high >= kijun
if price_closed_below and (price_opened_above or price_was_above or price_touched_above) and kijun_not_rising:
if filter_offset <= 0.0 or ma_current > kijun + filter_offset:
self._pending_short_level = kijun
self._pending_long_level = None
ma_trend_up = ma_current > prev_ma
ma_trend_down = ma_current < prev_ma
volume = float(self.Volume) + abs(float(self.Position))
if volume <= 0:
return
if self._pending_long_level is not None and ma_trend_up and self.Position <= 0:
self.BuyMarket(volume)
self._setup_position_state(True, close)
self._pending_long_level = None
self._pending_short_level = None
return
if self._pending_short_level is not None and ma_trend_down and self.Position >= 0:
self.SellMarket(volume)
self._setup_position_state(False, close)
self._pending_long_level = None
self._pending_short_level = None
def _update_history(self, candle, kijun, ma_current):
self._previous_prev_kijun = self._previous_kijun
self._previous_kijun = kijun
self._previous_prev_ma = self._previous_ma
self._previous_ma = ma_current
self._previous_close = float(candle.ClosePrice)
def _setup_position_state(self, is_long, entry_price):
self._is_long = is_long
self._entry_price = entry_price
self._be_applied = False
self._sl_distance = self._convert_pips(float(self.StopLossPips))
self._tp_distance = self._convert_pips(float(self.TakeProfitPips))
self._be_distance = self._convert_pips(float(self.BreakEvenPips))
self._be_step = self._convert_pips(1.0)
self._trail_distance = self._convert_pips(float(self.TrailingStopPips))
if self._sl_distance > 0.0:
self._stop_loss_price = entry_price - self._sl_distance if is_long else entry_price + self._sl_distance
else:
self._stop_loss_price = None
if self._tp_distance > 0.0:
self._take_profit_price = entry_price + self._tp_distance if is_long else entry_price - self._tp_distance
else:
self._take_profit_price = None
def _close_position_and_reset(self):
if self.Position > 0:
self.SellMarket(abs(float(self.Position)))
elif self.Position < 0:
self.BuyMarket(abs(float(self.Position)))
self._reset_position_state()
def _reset_position_state(self):
self._is_long = None
self._entry_price = None
self._stop_loss_price = None
self._take_profit_price = None
self._sl_distance = 0.0
self._tp_distance = 0.0
self._be_distance = 0.0
self._be_step = 0.0
self._trail_distance = 0.0
self._be_applied = False
def _convert_pips(self, value):
if value <= 0.0:
return 0.0
return value * self._get_pip_step()
def _get_pip_step(self):
if self.Security is None or self.Security.PriceStep is None:
return 1.0
step = float(self.Security.PriceStep)
if step <= 0.0:
return 1.0
decimals = self._get_decimal_places(step)
if decimals == 3 or decimals == 5:
return step * 10.0
return step
def _get_decimal_places(self, value):
value = abs(value)
decimals = 0
while value != int(value) and decimals < 10:
value *= 10.0
decimals += 1
return decimals
def OnReseted(self):
super(kijun_sen_robot_strategy, self).OnReseted()
self._previous_close = None
self._previous_ma = None
self._previous_prev_ma = None
self._previous_kijun = None
self._previous_prev_kijun = None
self._pending_long_level = None
self._pending_short_level = None
self._reset_position_state()
def CreateClone(self):
return kijun_sen_robot_strategy()