Стратегия Kijun Sen Robot
Общее описание
Kijun Sen Robot — это перенос советника MetaTrader 5 «Kijun Sen Robot» на высокоуровневый API StockSharp. По умолчанию стратегия работает на 30‑минутных свечах и ищет пробой цены через линию Kijun индикатора Ichimoku, подтверждая импульс 20‑периодной линейно-взвешенной скользящей средней (LWMA). Стратегия сохраняет идею оригинала: торговать только в активные часы и постоянно защищать позицию с помощью стоп-лосса, перевода в безубыток и трейлинг-стопа.
Используемые данные и индикаторы
- Ichimoku с периодами Tenkan/Kijun/Senkou Span B = 6/12/24.
- Линейно-взвешенная скользящая средняя длиной 20 баров — фильтр направления и расстояния до Kijun.
- Свечной поток (по умолчанию 30 минут), задаётся параметром
CandleType.
Логика торговли
Вход в длинную позицию
- Текущая свеча пробивает Kijun снизу вверх: открытие ниже линии, закрытие выше либо касание линии внутри бара при условии, что предыдущая свеча закрылась ниже.
- Значение Kijun не падает относительно уровня двумя барами ранее.
- LWMA находится минимум на
MaFilterPipsпунктов ниже Kijun (значение автоматически переводится в цены). - Наклон LWMA положительный: текущее значение выше предыдущего.
- Время находится в пределах
[TradingStartHour, TradingEndHour)(по умолчанию 07:00–19:00 по времени площадки).
Если условия выполнены и нет открытого лонга (Position ≤ 0), отправляется рыночная покупка. При наличии шорта он закрывается тем же ордером. Цена входа соответствует закрытию свечи.
Вход в короткую позицию
- Свеча пробивает Kijun сверху вниз (зеркальное условие).
- Kijun не растёт относительно значения двумя барами ранее.
- LWMA минимум на
MaFilterPipsпунктов выше линии Kijun. - LWMA снижается (текущее значение ниже предыдущего).
- Сигналы принимаются только в разрешённый временной интервал.
При выполнении условий и отсутствии действующего шорта (Position ≥ 0) стратегия продаёт по рынку, закрывая возможные лонги.
Управление позицией и выходы
- Начальный стоп-лосс выставляется на расстоянии
StopLossPips, переведённом в цену черезPriceStep. Это прямой аналог стопа в исходном советнике. - Перевод в безубыток срабатывает, когда прибыль достигает
BreakEvenPips: стоп переносится на цену входа плюс/минус один пункт. - Трейлинг-стоп активируется после прохождения
TrailingStopPipsпунктов прибыли и двигается только в сторону фиксации прибыли. - Фиксированный тейк-профит (
TakeProfitPips) опционален, значение 0 отключает цель. - Защитное закрытие по наклону LWMA: если до перевода стопа в безубыток скользящая разворачивается против позиции, сделка закрывается немедленно.
- Временной фильтр запрещает новые входы вне торгового окна, но текущая позиция продолжает сопровождаться.
- Типы ордеров — только рыночные; сложный выбор между лимитными и рыночными заявками из MT5 упрощён из-за работы с свечами, а не тиками.
Если внутри одной свечи выполнены и условия стопа, и тейк-профита, приоритет отдаётся стоп-лоссу — консервативная оценка без внутрибара.
Параметры
| Параметр | Значение по умолчанию | Назначение |
|---|---|---|
TenkanPeriod |
6 | Период линии Tenkan-сен. |
KijunPeriod |
12 | Период линии Kijun-сен. |
SenkouSpanBPeriod |
24 | Период 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 минут | Тип свечей для расчётов. |
Все параметры, заданные в пунктах, автоматически переводятся в ценовые значения через Security.PriceStep (или Security.MinPriceStep). Для инструментов с точностью 3 или 5 знаков шаг умножается на 10 — аналог MT5 digits_adjust.
Особенности реализации
- Переменные
_pendingLongLevelи_pendingShortLevelвоспроизводят механикуlongcross/shortcross, не позволяя повторно входить без нового пробоя. - Проверки «последнего бид/аск» заменены на условия по свечам (Open/High/Low/Close), что делает поведение стабильным в тестере.
- Стопы сопровождаются внутри стратегии: используется
ClosePosition(), а не модификация заявок на сервере. - Метод
ConvertPipsвыполняет перевод пунктов в цену, учитывая корректировку для пятизначных/трёхзначных инструментов. - Индикаторы подключаются через
SubscribeCandles().BindEx(...), на графике отображаются свечи, Ichimoku, LWMA и собственные сделки.
Рекомендации по применению
- Выберите инструмент с доступными 30‑минутными свечами (при необходимости измените
CandleType). - До старта задайте нужный объём сделки в свойстве
Volume. - Подстройте параметры в пунктах под волатильность конкретного инструмента или используйте найденные оптимизации.
- Запускайте стратегию в бэктестере или в реальном времени — управление позициями и расписание выполняется автоматически.
- Следите за журналом или графиком, чтобы видеть срабатывание перевода в безубыток и трейлинга. Все комментарии в коде даны на английском языке.
Python-версия сознательно не создавалась, в каталоге присутствует только реализация на C#.
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()