Strategie des prozentualen Crossover-Kanals
Übersicht
Die Strategie des prozentualen Crossover-Kanals stammt aus dem MetaTrader 5-Expertenberater Percentage_Crossover_Channel_EA. Sie basiert auf einem benutzerdefinierten Kanal, der um einen schnellen gleitenden Durchschnitt konstruiert wird, und reagiert entweder auf Bandberührungen oder Kreuzungen der Mittellinie. Diese StockSharp-Implementierung folgt derselben Logik und verwendet die High-Level-API zur Verarbeitung abgeschlossener Kerzen.
Kanalaufbau
Der zugrunde liegende Indikator baut einen dynamischen Kanal um den ausgewählten Preis (standardmäßig Schluss) auf:
- Den Basispreis mit dem konfigurierten Applied Price-Modus berechnen.
- Einen 1-Perioden-einfachen gleitenden Durchschnitt anwenden, um den kurzfristigen Referenzpreis zu erhalten.
- Zwei Grenzen mithilfe des Percent-Parameters berechnen (z.B. 50 → ±0,5%).
- Die vorherige Mittellinie innerhalb der neuen Grenzen begrenzen, um den aktuellen Mittelwert zu erhalten.
- Die oberen und unteren Bänder sind der begrenzte Mittelwert multipliziert mit den ±Prozent-Faktoren.
Diese Rekursion ermöglicht es dem Kanal, während starker Trends zu verzögern, während er bei Preiskonsolidierung ein enges Envelope beibehält.
Handelslogik
Zwei verschiedene Signalmodi sind verfügbar:
- Bandberührungs-Modus (Standard):
- Long-Einstieg, wenn das Tief der vorherigen Kerze über dem unteren Band lag und die letzte abgeschlossene Kerze es berührt oder durchbricht.
- Short-Einstieg, wenn das Hoch der vorherigen Kerze unter dem oberen Band lag und die letzte abgeschlossene Kerze es berührt oder durchbricht.
- Mittellinie-Kreuzungs-Modus (TradeOnMiddleCross = true):
- Long-Einstieg, wenn der Preis die Mittellinie von oben nach unten kreuzt.
- Short-Einstieg, wenn der Preis die Mittellinie von unten nach oben kreuzt.
Der ReverseSignals-Flag tauscht Long- und Short-Regeln aus. Die Strategie schließt und kehrt bestehende Positionen immer um, indem ein einzelner Marktauftrag mit einem Volumen gesendet wird, das dem konfigurierten OrderVolume plus dem absoluten Wert der aktuellen Position entspricht.
Risikomanagement
Optionale Schutzlevel emulieren die ursprünglichen MT5-Stop-Loss- und Take-Profit-Einstellungen:
- StopLossPoints – Abstand in Preisschritten, der vom geschätzten Einstiegspreis subtrahiert (Long) oder addiert (Short) wird.
- TakeProfitPoints – Abstand in Preisschritten, der zum Einstiegspreis addiert (Long) oder subtrahiert (Short) wird.
Wenn ein Parameter null ist, wird der entsprechende Schutz deaktiviert. Stops werden auf jeder abgeschlossenen Kerze ausgewertet, indem Kerzen-Hochs und -Tiefs mit den gespeicherten Levels verglichen werden. Keine Trailing-Logik wird angewendet.
Parameter
| Parameter |
Beschreibung |
CandleType |
Kerzen-Datentyp zum Abonnieren (15-Minuten-Zeitrahmen standardmäßig). |
Percent |
Kanalbreite in Prozent des Preises (in ±Prozent/100-Faktoren umgerechnet). |
PriceMode |
Angewandter Preis für den Kanal. Optionen: Close, Open, High, Low, Median (H+L)/2, Typical (H+L+C)/3, Weighted (H+L+2C)/4, Average (O+H+L+C)/4. |
TradeOnMiddleCross |
Umschalten zwischen Bandberührungs-Logik und Mittellinie-Kreuzungs-Logik. |
ReverseSignals |
Long- und Short-Bedingungen invertieren. |
StopLossPoints |
Schutz-Stop-Abstand in Sicherheits-Preisschritten ausgedrückt. |
TakeProfitPoints |
Gewinnziel-Abstand in Sicherheits-Preisschritten ausgedrückt. |
OrderVolume |
Basisvolumen für Markteinträge. Die Strategie addiert die absolute offene Position zum Umkehren in einer Transaktion. |
Implementierungshinweise
- Aufträge werden nur nach Beendigung von Kerzen ausgegeben, was dem MT5-Experten entspricht, der zu Beginn der nächsten Bar mit Daten der vorherigen Bar handelte.
- Der Kanalindikator wird innerhalb der Strategie ohne Speicherung historischer Sammlungen neu erstellt und basiert auf skalaren Zustandsvariablen.
- Schutz-Stops und -Ziele werden manuell überprüft, um die plattformspezifische Auftragsbehandlung aus MT5 zu replizieren.
- Sicherstellen, dass die ausgewählte Sicherheit einen gültigen
PriceStep aufweist; andernfalls werden Stop-Loss- und Take-Profit-Abstände ignoriert.
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>
/// Percentage Crossover Channel strategy converted from MetaTrader 5.
/// </summary>
public class PercentageCrossoverChannelStrategy : Strategy
{
public enum PercentageChannelPriceModes
{
Close,
Open,
High,
Low,
Median,
Typical,
Weighted,
Average
}
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<decimal> _percent;
private readonly StrategyParam<PercentageChannelPriceModes> _priceMode;
private readonly StrategyParam<bool> _tradeOnMiddleCross;
private readonly StrategyParam<bool> _reverseSignals;
private readonly StrategyParam<int> _stopLossPoints;
private readonly StrategyParam<int> _takeProfitPoints;
private readonly StrategyParam<decimal> _orderVolume;
// Cached indicator values for the previous two finished candles.
private decimal? _prevUpper;
private decimal? _prevMiddle;
private decimal? _prevLower;
private decimal? _prevPrevUpper;
private decimal? _prevPrevMiddle;
private decimal? _prevPrevLower;
// Stored price data for signal evaluation.
private decimal? _prevClose;
private decimal? _prevHigh;
private decimal? _prevLow;
private decimal? _prevPrevClose;
private decimal? _prevPrevHigh;
private decimal? _prevPrevLow;
// Internal state of the channel middle line recursion.
private decimal _lastMiddle;
private bool _hasIndicatorState;
// Protective levels that mimic MT5 stop loss and take profit requests.
private decimal? _stopPrice;
private decimal? _takePrice;
private decimal _entryPrice;
public PercentageCrossoverChannelStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Timeframe for processing", "General");
_percent = Param(nameof(Percent), 1m)
.SetDisplay("Percent", "Channel width percent", "Channel")
.SetGreaterThanZero();
_priceMode = Param(nameof(PriceMode), PercentageChannelPriceModes.Close)
.SetDisplay("Applied Price", "Price source for channel calculations", "Channel");
_tradeOnMiddleCross = Param(nameof(TradeOnMiddleCross), false)
.SetDisplay("Trade Middle Cross", "Use middle line crossovers instead of band touches", "Signals");
_reverseSignals = Param(nameof(ReverseSignals), false)
.SetDisplay("Reverse Signals", "Invert long and short logic", "Signals");
_stopLossPoints = Param(nameof(StopLossPoints), 0)
.SetDisplay("Stop Loss (points)", "Protective stop distance in points", "Risk")
.SetNotNegative();
_takeProfitPoints = Param(nameof(TakeProfitPoints), 0)
.SetDisplay("Take Profit (points)", "Target profit distance in points", "Risk")
.SetNotNegative();
_orderVolume = Param(nameof(OrderVolume), 1m)
.SetDisplay("Order Volume", "Base volume for market entries", "Trading")
.SetGreaterThanZero();
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public decimal Percent
{
get => _percent.Value;
set => _percent.Value = value;
}
public PercentageChannelPriceModes PriceMode
{
get => _priceMode.Value;
set => _priceMode.Value = value;
}
public bool TradeOnMiddleCross
{
get => _tradeOnMiddleCross.Value;
set => _tradeOnMiddleCross.Value = value;
}
public bool ReverseSignals
{
get => _reverseSignals.Value;
set => _reverseSignals.Value = value;
}
public int StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
public int TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
public decimal OrderVolume
{
get => _orderVolume.Value;
set => _orderVolume.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_prevUpper = null;
_prevMiddle = null;
_prevLower = null;
_prevPrevUpper = null;
_prevPrevMiddle = null;
_prevPrevLower = null;
_prevClose = null;
_prevHigh = null;
_prevLow = null;
_prevPrevClose = null;
_prevPrevHigh = null;
_prevPrevLow = null;
_lastMiddle = 0m;
_hasIndicatorState = false;
ResetProtection();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
Volume = OrderVolume;
// Subscribe to candle updates that will drive the high level logic.
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
// Work only with completed candles to stay consistent with the MT5 implementation.
if (candle.State != CandleStates.Finished)
return;
var exitTriggered = CheckProtection(candle);
if (!exitTriggered)
TryEnterPositions(candle);
UpdateChannelState(candle);
}
private void TryEnterPositions(ICandleMessage candle)
{
// Wait until the channel has valid values for two completed candles.
if (!_prevLower.HasValue || !_prevPrevLower.HasValue)
return;
if (!_prevClose.HasValue || !_prevPrevClose.HasValue || !_prevHigh.HasValue || !_prevPrevHigh.HasValue || !_prevLow.HasValue || !_prevPrevLow.HasValue)
return;
var openLong = false;
var openShort = false;
if (TradeOnMiddleCross)
{
// Evaluate crossovers of the price and the middle channel line.
var crossDown = _prevPrevClose.Value > _prevPrevMiddle.Value && _prevClose.Value < _prevMiddle.Value;
var crossUp = _prevPrevClose.Value < _prevPrevMiddle.Value && _prevClose.Value > _prevMiddle.Value;
if (!ReverseSignals)
{
if (crossDown)
openLong = true;
if (crossUp)
openShort = true;
}
else
{
if (crossDown)
openShort = true;
if (crossUp)
openLong = true;
}
}
else
{
// Default mode trades touches of the outer channel boundaries.
var touchLower = _prevPrevLow.Value > _prevPrevLower.Value && _prevLow.Value <= _prevLower.Value;
var touchUpper = _prevPrevHigh.Value < _prevPrevUpper.Value && _prevHigh.Value >= _prevUpper.Value;
if (!ReverseSignals)
{
if (touchLower)
openLong = true;
if (touchUpper)
openShort = true;
}
else
{
if (touchLower)
openShort = true;
if (touchUpper)
openLong = true;
}
}
if (openLong)
{
EnterLong(candle);
}
else if (openShort)
{
EnterShort(candle);
}
}
private void EnterLong(ICandleMessage candle)
{
// Combine base order volume with the size required to flatten shorts.
var volume = OrderVolume + (Position < 0 ? Math.Abs(Position) : 0m);
if (volume <= 0m)
return;
BuyMarket(volume);
_entryPrice = candle.OpenPrice;
_stopPrice = CalculateStopPrice(Sides.Buy, _entryPrice);
_takePrice = CalculateTakePrice(Sides.Buy, _entryPrice);
}
private void EnterShort(ICandleMessage candle)
{
// Combine base order volume with the size required to flatten longs.
var volume = OrderVolume + (Position > 0 ? Position : 0m);
if (volume <= 0m)
return;
SellMarket(volume);
_entryPrice = candle.OpenPrice;
_stopPrice = CalculateStopPrice(Sides.Sell, _entryPrice);
_takePrice = CalculateTakePrice(Sides.Sell, _entryPrice);
}
private bool CheckProtection(ICandleMessage candle)
{
// Emulate MT5 protective stop and take profit that were attached to market orders.
if (Position > 0)
{
if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
{
SellMarket(Math.Abs(Position));
ResetProtection();
return true;
}
if (_takePrice.HasValue && candle.HighPrice >= _takePrice.Value)
{
SellMarket(Math.Abs(Position));
ResetProtection();
return true;
}
}
else if (Position < 0)
{
if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
{
BuyMarket(Math.Abs(Position));
ResetProtection();
return true;
}
if (_takePrice.HasValue && candle.LowPrice <= _takePrice.Value)
{
BuyMarket(Math.Abs(Position));
ResetProtection();
return true;
}
}
else
{
ResetProtection();
}
return false;
}
private void UpdateChannelState(ICandleMessage candle)
{
// Recreate the Percentage Crossover Channel middle line recursion.
var percent = Percent <= 0m ? 0.001m : Percent;
var plusFactor = 1m + percent / 100m;
var minusFactor = 1m - percent / 100m;
var price = GetAppliedPrice(candle);
decimal currentMiddle;
if (!_hasIndicatorState)
{
currentMiddle = price;
_hasIndicatorState = true;
}
else
{
var lowerBound = price * minusFactor;
var upperBound = price * plusFactor;
var previousMiddle = _lastMiddle;
currentMiddle = previousMiddle;
if (lowerBound > previousMiddle)
currentMiddle = lowerBound;
else if (upperBound < previousMiddle)
currentMiddle = upperBound;
}
var currentUpper = currentMiddle * plusFactor;
var currentLower = currentMiddle * minusFactor;
if (_prevUpper.HasValue)
{
_prevPrevUpper = _prevUpper;
_prevPrevMiddle = _prevMiddle;
_prevPrevLower = _prevLower;
_prevPrevClose = _prevClose;
_prevPrevHigh = _prevHigh;
_prevPrevLow = _prevLow;
}
_prevUpper = currentUpper;
_prevMiddle = currentMiddle;
_prevLower = currentLower;
_prevClose = candle.ClosePrice;
_prevHigh = candle.HighPrice;
_prevLow = candle.LowPrice;
_lastMiddle = currentMiddle;
}
private decimal GetAppliedPrice(ICandleMessage candle)
{
// Convert the selected price mode into a candle value.
return PriceMode switch
{
PercentageChannelPriceModes.Open => candle.OpenPrice,
PercentageChannelPriceModes.High => candle.HighPrice,
PercentageChannelPriceModes.Low => candle.LowPrice,
PercentageChannelPriceModes.Median => (candle.HighPrice + candle.LowPrice) / 2m,
PercentageChannelPriceModes.Typical => (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3m,
PercentageChannelPriceModes.Weighted => (candle.HighPrice + candle.LowPrice + (2m * candle.ClosePrice)) / 4m,
PercentageChannelPriceModes.Average => (candle.OpenPrice + candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 4m,
_ => candle.ClosePrice,
};
}
private decimal? CalculateStopPrice(Sides side, decimal entryPrice)
{
if (StopLossPoints <= 0)
return null;
var step = Security?.PriceStep ?? 0m;
if (step <= 0m)
return null;
var offset = StopLossPoints * step;
return side == Sides.Buy ? entryPrice - offset : entryPrice + offset;
}
private decimal? CalculateTakePrice(Sides side, decimal entryPrice)
{
if (TakeProfitPoints <= 0)
return null;
var step = Security?.PriceStep ?? 0m;
if (step <= 0m)
return null;
var offset = TakeProfitPoints * step;
return side == Sides.Buy ? entryPrice + offset : entryPrice - offset;
}
private void ResetProtection()
{
_stopPrice = null;
_takePrice = null;
_entryPrice = 0m;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
from datatype_extensions import *
class percentage_crossover_channel_strategy(Strategy):
def __init__(self):
super(percentage_crossover_channel_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Timeframe", "General")
self._percent = self.Param("Percent", 1.0).SetGreaterThanZero().SetDisplay("Percent", "Channel width percent", "Channel")
self._sl_points = self.Param("StopLossPoints", 0).SetNotNegative().SetDisplay("Stop Loss (points)", "Protective stop distance", "Risk")
self._tp_points = self.Param("TakeProfitPoints", 0).SetNotNegative().SetDisplay("Take Profit (points)", "Target profit distance", "Risk")
@property
def CandleType(self): return self._candle_type.Value
@CandleType.setter
def CandleType(self, value): self._candle_type.Value = value
def OnReseted(self):
super(percentage_crossover_channel_strategy, self).OnReseted()
self._last_middle = 0
self._has_state = False
self._prev_upper = None
self._prev_lower = None
self._prev_close = None
self._prev_high = None
self._prev_low = None
self._prev_prev_upper = None
self._prev_prev_lower = None
self._prev_prev_close = None
self._prev_prev_high = None
self._prev_prev_low = None
self._stop_price = None
self._take_price = None
self._entry_price = 0
def OnStarted2(self, time):
super(percentage_crossover_channel_strategy, self).OnStarted2(time)
self._last_middle = 0
self._has_state = False
self._prev_upper = None
self._prev_lower = None
self._prev_close = None
self._prev_high = None
self._prev_low = None
self._prev_prev_upper = None
self._prev_prev_lower = None
self._prev_prev_close = None
self._prev_prev_high = None
self._prev_prev_low = None
self._stop_price = None
self._take_price = None
self._entry_price = 0
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, sub)
self.DrawOwnTrades(area)
def _get_step(self):
if self.Security is not None and self.Security.PriceStep is not None and self.Security.PriceStep > 0:
return float(self.Security.PriceStep)
return 0.01
def OnProcess(self, candle):
if candle.State != CandleStates.Finished:
return
exit_triggered = self._check_protection(candle)
if not exit_triggered:
self._try_enter(candle)
self._update_channel(candle)
def _try_enter(self, candle):
if self._prev_lower is None or self._prev_prev_lower is None:
return
if self._prev_close is None or self._prev_prev_close is None:
return
touch_lower = self._prev_prev_low > self._prev_prev_lower and self._prev_low <= self._prev_lower
touch_upper = self._prev_prev_high < self._prev_prev_upper and self._prev_high >= self._prev_upper
if touch_lower:
self._enter_long(candle)
elif touch_upper:
self._enter_short(candle)
def _enter_long(self, candle):
volume = 1 + (Math.Abs(self.Position) if self.Position < 0 else 0)
self.BuyMarket(volume)
self._entry_price = float(candle.OpenPrice)
step = self._get_step()
self._stop_price = self._entry_price - self._sl_points.Value * step if self._sl_points.Value > 0 and step > 0 else None
self._take_price = self._entry_price + self._tp_points.Value * step if self._tp_points.Value > 0 and step > 0 else None
def _enter_short(self, candle):
volume = 1 + (self.Position if self.Position > 0 else 0)
self.SellMarket(volume)
self._entry_price = float(candle.OpenPrice)
step = self._get_step()
self._stop_price = self._entry_price + self._sl_points.Value * step if self._sl_points.Value > 0 and step > 0 else None
self._take_price = self._entry_price - self._tp_points.Value * step if self._tp_points.Value > 0 and step > 0 else None
def _check_protection(self, candle):
if self.Position > 0:
if self._stop_price is not None and candle.LowPrice <= self._stop_price:
self.SellMarket(Math.Abs(self.Position))
self._reset_protection()
return True
if self._take_price is not None and candle.HighPrice >= self._take_price:
self.SellMarket(Math.Abs(self.Position))
self._reset_protection()
return True
elif self.Position < 0:
if self._stop_price is not None and candle.HighPrice >= self._stop_price:
self.BuyMarket(Math.Abs(self.Position))
self._reset_protection()
return True
if self._take_price is not None and candle.LowPrice <= self._take_price:
self.BuyMarket(Math.Abs(self.Position))
self._reset_protection()
return True
else:
self._reset_protection()
return False
def _reset_protection(self):
self._stop_price = None
self._take_price = None
self._entry_price = 0
def _update_channel(self, candle):
pct = self._percent.Value if self._percent.Value > 0 else 0.001
plus_factor = 1.0 + pct / 100.0
minus_factor = 1.0 - pct / 100.0
price = float(candle.ClosePrice)
if not self._has_state:
current_middle = price
self._has_state = True
else:
lower_bound = price * minus_factor
upper_bound = price * plus_factor
current_middle = self._last_middle
if lower_bound > current_middle:
current_middle = lower_bound
elif upper_bound < current_middle:
current_middle = upper_bound
current_upper = current_middle * plus_factor
current_lower = current_middle * minus_factor
if self._prev_upper is not None:
self._prev_prev_upper = self._prev_upper
self._prev_prev_lower = self._prev_lower
self._prev_prev_close = self._prev_close
self._prev_prev_high = self._prev_high
self._prev_prev_low = self._prev_low
self._prev_upper = current_upper
self._prev_lower = current_lower
self._prev_close = float(candle.ClosePrice)
self._prev_high = float(candle.HighPrice)
self._prev_low = float(candle.LowPrice)
self._last_middle = current_middle
def CreateClone(self):
return percentage_crossover_channel_strategy()