Kijun-Sen-Robot-Strategie
Überblick
Die Kijun-Sen-Robot-Strategie ist eine direkte Konvertierung des MetaTrader 5 Expert Advisors "Kijun Sen Robot" in die StockSharp High-Level-Strategie-API. Sie arbeitet standardmäßig auf 30-Minuten-Kerzen und konzentriert sich auf Ichimoku Kijun-sen-Preiskreuzungen, die durch einen 20-Perioden-linear-gewichteten gleitenden Durchschnitt (LWMA) bestätigt werden. Die Strategie behält die ursprüngliche Idee des Experten bei, nur während der aktivsten Stunden zu handeln und Positionsschutz mit dynamischer Stop-, Breakeven- und Trailing-Logik durchzusetzen.
Indikatoren und Daten
- Ichimoku mit Tenkan, Kijun und Senkou Span B konfiguriert auf 6/12/24 Perioden.
- Linear-gewichteter gleitender Durchschnitt (LWMA) über 20 Bars für Steigungsbestätigung und Distanzfilterung.
- 30-Minuten-Kerzen (Standard) für die Signalerzeugung. Jeder andere Zeitrahmen kann über den Parameter
CandleType ausgewählt werden.
Handelslogik
Long-Einstieg
- Die Kerze handelt die Kijun-Linie von unten durch. Die Kerze muss entweder unterhalb der Linie öffnen, oberhalb schließen oder sie während der Bar berühren, während der vorherige Schlusskurs ebenfalls unterhalb lag.
- Das aktuelle Kijun ist flach oder steigend im Vergleich zu zwei Bars zurück.
- Die LWMA liegt mindestens
MaFilterPips (in Preiseinheiten umgerechnet) unterhalb des Kijun-Niveaus und hält die Basislinie über dem gleitenden Durchschnitt.
- Die LWMA-Steigung ist positiv (aktuelle LWMA größer als der vorherige Wert).
- Die Handelszeit liegt innerhalb von
[TradingStartHour, TradingEndHour), Standard 07:00–19:00 Börsenzeitzone.
Wenn alle Bedingungen erfüllt sind und die Strategie nicht bereits netto-long ist, wird eine Markt-Kauforder gesendet (ein vorhandenes Short wird zuerst gedeckt). Der Einstiegspreis ist der Kerzenschlusskurs.
Short-Einstieg
- Die Kerze handelt die Kijun-Linie von oben durch (Spiegelung der Long-Logik).
- Kijun ist flach oder fällt relativ zu zwei Bars zurück.
- Die LWMA liegt mindestens
MaFilterPips oberhalb des Kijun-Niveaus.
- Die LWMA-Steigung ist negativ (aktuelle LWMA niedriger als der vorherige Wert).
- Der Einstieg erfolgt nur innerhalb des erlaubten Handelsfensters.
Eine Markt-Verkaufsorder wird platziert (vorhandenes Long-Exposure wird geschlossen, bevor ein Short eröffnet wird).
Positionsmanagement und Ausstiege
- Initialer Stop-Loss –
StopLossPips unterhalb/oberhalb des Einstiegspreises platziert (in Preiseinheiten über den Preisschritt des Instruments umgerechnet). Dies reproduziert den Schutzstop der MQL-Version.
- Breakeven-Bewegung – sobald der nicht realisierte Gewinn
BreakEvenPips überschreitet, wird der Stop auf den Einstiegspreis plus einen Pip (Long) oder minus einen Pip (Short) verschoben. Der Schwellenwert wird mit derselben Pip-Konvertierungslogik gemessen.
- Trailing-Stop – nachdem der Kurs um
TrailingStopPips vorgerückt ist, folgt der Stop dem Kurs auf diesem Abstand, nur in der günstigen Richtung.
- Fester Take-Profit – optionales Ziel definiert durch
TakeProfitPips. Auf null setzen zum Deaktivieren.
- Kijun-Steigungsausstieg – wenn die LWMA sich gegen den Trade dreht, bevor der Stop über den Breakeven hinausbewegt wurde, wird die Position sofort geschlossen, entsprechend dem Notausstieg des ursprünglichen Experten.
- Zeitfilter – neue Trades werden außerhalb des konfigurierten Fensters ignoriert, aber offene Trades werden weiterhin verwaltet, bis sie durch die oben genannten Regeln geschlossen werden.
- Orderhandling – die StockSharp-Strategie verwendet ausschließlich Marktorders; die komplexe Limit-vs-Markt-Einstiegslogik des ursprünglichen EA wird vereinfacht, da Kerzendaten statt Tick-Daten verwendet werden.
Wenn sowohl der Stop-Loss- als auch der Take-Profit-Level innerhalb derselben Bar verletzt werden würden, hat der Stop-Loss Vorrang, um ohne Intrabar-Information konservativ zu bleiben.
Parameter
| Parameter |
Standardwerte |
Beschreibung |
TenkanPeriod |
6 |
Ichimoku Tenkan-sen-Länge. |
KijunPeriod |
12 |
Ichimoku Kijun-sen-Länge. |
SenkouSpanBPeriod |
24 |
Ichimoku Senkou Span B-Länge. |
LwmaPeriod |
20 |
Länge des LWMA-Bestätigungsfilters. |
MaFilterPips |
6 |
Minimaler LWMA-zu-Kijun-Abstand in Pips. |
StopLossPips |
50 |
Initialer Schutzstop-Abstand. |
BreakEvenPips |
9 |
Benötigter Gewinn, um den Stop auf Breakeven zu verschieben. |
TrailingStopPips |
10 |
Abstand für Trailing-Stop-Bewegung. |
TakeProfitPips |
120 |
Optionaler fester Take-Profit-Abstand. |
TradingStartHour |
7 |
Erste erlaubte Handelsstunde (inklusive). |
TradingEndHour |
19 |
Letzte erlaubte Handelsstunde (exklusive). |
CandleType |
30-Minuten-Zeitrahmen |
Für die Signalauswertung verwendeter Datentyp. |
Alle pip-basierten Parameter werden in Preiseinheiten unter Verwendung des Instrument-PriceStep übersetzt. Instrumente mit 3 oder 5 Dezimalstellen erhalten automatisch einen Faktor von 10, um die klassische FX-Pip-Größe zu replizieren.
Implementierungshinweise
- Die Konvertierung hält die zustandsbehafteten Strategievariablen (Verhalten
longcross, shortcross) über _pendingLongLevel und _pendingShortLevel, wodurch neue Positionen eine frische Kijun-Kreuzung erfordern.
- Intrabar-Prüfungen wie "letzter Bid/Ask" aus der MT5-Version werden mit Bedingungen auf Kerzenebene (
Open, Close, High, Low) angenähert. Dies macht die Logik deterministisch für Backtesting in StockSharp.
- Der Positionsschutz verwendet
ClosePosition() und manuelles Stop-Tracking anstelle von MT5-Ordermodifikationen. Die Breakeven- und Trailing-Anpassungen werden einmal pro abgeschlossener Kerze ausgeführt.
- Die Hilfsmethode
ConvertPips führt die Pip-zu-Preis-Konvertierung mit Security.PriceStep oder Security.MinPriceStep durch und wendet einen 10×-Multiplikator für Tick-Größen mit 3 oder 5 Dezimalstellen an, um die MT5-digits_adjust-Regel zu emulieren.
- Da die Strategie an die High-Level-API gebunden ist, werden Indikatoren über
SubscribeCandles().BindEx(...) gebunden, und Chart-Zeichnungen werden automatisch konfiguriert (Kerzen, Ichimoku, LWMA, eigene Trades).
Verwendungsrichtlinien
- Die Strategie an ein Wertpapier anhängen, das 30-Minuten-Kerzen unterstützt (oder einen anderen
CandleType einstellen).
Volume auf der Strategieinstanz auf die gewünschte Ordergröße konfigurieren, bevor gestartet wird.
- Optional pip-basierte Parameter anpassen, um die Instrumentvolatilität zu berücksichtigen oder optimierte Einstellungen für spezifische Währungspaare zu reproduzieren.
- Im High-Level-Backtester oder in der Live-Umgebung ausführen; die Strategie wird dasselbe Handelsfenster, dieselben Stop- und Trailing-Regeln wie der ursprüngliche Experte durchsetzen.
- Log oder Chart überwachen, um Breakeven- und Trailing-Updates zu sehen. Alle Kommentare im Code sind auf Englisch für Klarheit, wie angefordert.
Implementierungen sind in C# und Python verfügbar.
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()