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), 6m)
.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")
clr.AddReference("StockSharp.BusinessEntities")
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", 6.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()