Die durchschnittliche Kanalstrategie wurde korrigiert
Überblick
Die Corrected Average Channel Strategy ist eine C#-Portierung des MetaTrader-Expertenberaters e-CA-5. Das System erstellt den Indikator „Corrected Average“ (CA) jedes Mal neu, wenn eine Kerze schließt und eröffnet eine Position, wenn der Preis den korrigierten gleitenden Durchschnitt um einen konfigurierbaren Sigma-Offset kreuzt. Die konvertierte Implementierung basiert auf der High-Level-Kerze API von StockSharp, verwendet Marktaufträge und verwaltet intern schützende Exits (Stop-Loss, Take-Profit, Trailing Stop), um das Verhalten des ursprünglichen Expert Advisors widerzuspiegeln.
Korrigierter Durchschnittsindikator
Der CA-Filter kombiniert einen gleitenden Durchschnitt mit Volatilitätsrückmeldung. Die MQL-Version stellt drei Eingaben bereit: Länge des gleitenden Durchschnitts, Mittelungsmethode und angewandter Preis. Im StockSharp-Port:
- Der Typ des gleitenden Durchschnitts wird über den Parameter
MaTypeOption (SMA, EMA, SMMA, LWMA) und die Länge MaPeriod ausgewählt.
- Ein
StandardDeviation-Indikator mit demselben Zeitraum misst die aktuelle Volatilität.
- Für jede fertige Kerze wird der korrigierte Wert iterativ berechnet:
- Sei
M_t der MA-Wert des letzten Balkens und CA_{t-1} der korrigierte Wert des vorherigen Balkens.
- Berechnen Sie
v1 = StdDev_t^2 und v2 = (CA_{t-1} - M_t)^2.
- Wenn
v2 <= 0 oder v2 < v1, behalten Sie den Korrekturfaktor k = 0 bei. Andernfalls legen Sie k = 1 - v1 / v2 fest.
- Aktualisieren Sie
CA_t = CA_{t-1} + k * (M_t - CA_{t-1}).
- Der allererste korrigierte Wert ist standardmäßig der gleitende Durchschnitt selbst.
Diese Rückkopplungsschleife dämpft den MA in ruhigen Zeiten und ermöglicht schnelle Anpassungen, wenn der Preis über die aktuelle Volatilitätsschätzung hinaus abweicht.
Handelslogik
- Die Strategie abonniert den konfigurierten Kerzentyp (
CandleType) und wartet, bis sowohl der gleitende Durchschnitt als auch die Standardabweichung vollständig gebildet sind.
- Sobald eine Kerze endet, berechnet der Algorithmus den neuen korrigierten Wert und vergleicht den Schlusskurs der vorherigen Kerze mit dem vorherigen korrigierten Niveau.
- Zwei Sigma-Offsets,
SigmaBuyPoints und SigmaSellPoints, werden mithilfe des PriceStep des Instruments in Preisabstände umgewandelt.
- Die Einstiegsregeln verwenden den vorherigen Kerzenschluss und das neu berechnete korrigierte Niveau:
- Kaufen, wenn der vorherige Schlusskurs unter dem korrigierten Durchschnitt plus dem Kauf-Sigma lag und der aktuelle Schlusskurs über dieser Obergrenze endet.
- Verkaufen, wenn der vorherige Schlusskurs über dem korrigierten Durchschnitt minus dem Verkaufs-Sigma lag und der aktuelle Schlusskurs unterhalb dieser unteren Grenze endet.
- Es ist nur eine Nettoposition zulässig. Ein neuer Trade wird nur eingereicht, wenn kein Exposure vorliegt.
Da die StockSharp-Version mit fertigen Kerzen arbeitet, erfolgt die Ausbruchsbestätigung einmal pro Balken statt bei jedem Tick, wodurch ein deterministisches Verhalten bereitgestellt wird, das für Backtesting und Live-Automatisierung mit Kerzendaten geeignet ist.
Risikomanagement
Der Port reproduziert alle drei Schutzmechanismen des ursprünglichen Expert Advisors:
- Fester Stop-Loss:
StopLossPoints multipliziert mit dem Preisschritt definiert den Abstand zwischen dem Einstiegspreis und dem schützenden Stop. Ein ausgelöster Stop schließt die gesamte Position mit einer Marktorder.
- Fester Take-Profit:
TakeProfitPoints wird in eine Gewinnzielentfernung umgewandelt. Wenn der Preis während einer Kerze das Niveau erreicht, wird die Position mit einer Marktorder geschlossen.
- Trailing Stop: Wenn
TrailingPoints größer als Null ist, verfolgt die Strategie nicht realisierte Gewinne und speichert, sobald der Preis mindestens um diese Distanz gestiegen ist, einen Trailing-Level hinter dem letzten Schlusskurs. Der Trailing Stop bewegt sich nur vorwärts und berücksichtigt TrailingStepPoints, was die minimale Verbesserung darstellt, bevor ein neues Trailing-Level akzeptiert wird. Nachfolgende Level werden mit Security.ShrinkPrice gerundet, sodass sie mit der Tick-Größe des Instruments übereinstimmen.
Alle Exits setzen den internen Risikostatus zurück. Wenn das nächste Signal erscheint, werden die Stop-, Ziel- und Trailing-Levels aus dem neuen Füllpreis neu berechnet, wodurch ein Verhalten sichergestellt wird, das der MQL-Version ähnelt, die den ursprünglichen Orderschutz ändert.
Parameter
| Parameter |
Beschreibung |
OrderVolume |
Menge, die für Markteintritte verwendet wird. Muss positiv sein. |
TakeProfitPoints |
Gewinnziel in Preisschritten (0 deaktiviert den Take-Profit). |
StopLossPoints |
Stop-Loss-Distanz in Preisschritten (0 deaktiviert den Stop-Loss). |
TrailingPoints |
Erforderliche Gewinndistanz (in Preisschritten), bevor der Trailing Stop aktiviert wird. |
TrailingStepPoints |
Minimale zusätzliche Distanz, die erfasst werden muss, bevor der Trailing Stop erneut verschoben wird. |
MaPeriod |
Zeitraum sowohl des gleitenden Durchschnitts als auch der Standardabweichung. |
MaTypeOption |
Typ des gleitenden Durchschnitts: SMA, EMA, SMMA oder LWMA. |
SigmaBuyPoints |
Der Sigma-Offset wurde über dem korrigierten Durchschnitt hinzugefügt, bevor eine Long-Position eröffnet wurde. |
SigmaSellPoints |
Der Sigma-Offset wurde unter den korrigierten Durchschnitt subtrahiert, bevor eine Short-Position eröffnet wurde. |
CandleType |
Kerzenserien zur Indikatorberechnung und Signalauswertung. |
Alle numerischen Parameter unterstützen die Optimierung durch SetCanOptimize(true), sodass die Strategie direkt in der StockSharp-Umgebung kalibriert werden kann.
Nutzungshinweise
- Der Standardkerzentyp ist eine Stunde. Passen Sie es an den Zeitrahmen an, der bei der Optimierung der ursprünglichen MetaTrader-Strategie verwendet wurde.
Security.PriceStep wird verwendet, um alle „Punkte“-Eingaben in tatsächliche Preisentfernungen zu übersetzen. Instrumente ohne konfigurierten Schritt greifen auf 1 zurück, wodurch sinnvolles Verhalten für Indizes oder Kryptowährungen erhalten bleibt.
- Die Strategie wird nur bei fertigen Kerzen ausgeführt. Wenn Intrabar-Präzision erforderlich ist, verringern Sie den Zeitrahmen auf die gewünschte Granularität.
- Trailing-Stops werden bei Verletzung mit Marktaufträgen implementiert und ahmen den ursprünglichen EA nach, der die Stop-Loss-Preise modifizierte. Dieser Ansatz vermeidet die Platzierung zusätzlicher Stop-Orders und sorgt dafür, dass das Risikomanagement in der Strategie selbst enthalten bleibt.
- Gemäß den Aufgabenanforderungen wird für diese Konvertierung keine Python-Version bereitgestellt.
Unterschiede zum Original EA
- Das kerzenbasierte API von StockSharp ersetzt die Verarbeitung auf Tick-Ebene. Alle Entscheidungen werden getroffen, wenn eine Kerze schließt.
- Die Auftragsverwaltung erfolgt saldiert: Gegensätzliche Positionen werden nicht gleichzeitig gehalten, was der Einzelauftragslogik der MetaTrader-Version entspricht.
- Schutzstopps und Trailing-Exits werden über Marktaufträge ausgeführt, anstatt bestehende Auftragsscheine zu ändern. Dieses Verhalten ist bei Netting-Konten gleichwertig, wobei die Implementierung mit anderen StockSharp-Strategien konsistent bleibt.
Diese Anpassungen bewahren die Handelsidee von e-CA-5 und richten die Logik gleichzeitig an den Best Practices von StockSharp und den allgemeinen API-Konventionen aus, die in den Repository-Richtlinien beschrieben sind.
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>
/// Port of the MetaTrader expert e-CA-5 that trades breakouts around the Corrected Average indicator.
/// The strategy subscribes to candles, rebuilds the indicator and places market orders when price crosses
/// the corrected moving average by the configured sigma offsets.
/// </summary>
public class CorrectedAverageChannelStrategy : Strategy
{
private readonly StrategyParam<decimal> _orderVolume;
private readonly StrategyParam<int> _takeProfitPoints;
private readonly StrategyParam<int> _stopLossPoints;
private readonly StrategyParam<int> _trailingPoints;
private readonly StrategyParam<int> _trailingStepPoints;
private readonly StrategyParam<int> _maPeriod;
private readonly StrategyParam<MaTypes> _maType;
private readonly StrategyParam<int> _sigmaBuyPoints;
private readonly StrategyParam<int> _sigmaSellPoints;
private readonly StrategyParam<DataType> _candleType;
private DecimalLengthIndicator _ma;
private StandardDeviation _std;
private decimal _priceStep;
private decimal _sigmaBuyOffset;
private decimal _sigmaSellOffset;
private decimal _stopLossDistance;
private decimal _takeProfitDistance;
private decimal _trailingDistance;
private decimal _trailingStepDistance;
private decimal? _previousCorrected;
private decimal? _previousClose;
private decimal? _entryPrice;
private decimal? _stopLossPrice;
private decimal? _takeProfitPrice;
private decimal? _longTrailingStop;
private decimal? _shortTrailingStop;
private decimal _previousPosition;
private decimal? _lastTradePrice;
private Sides? _lastTradeSide;
/// <summary>
/// Order size used for market entries.
/// </summary>
public decimal OrderVolume
{
get => _orderVolume.Value;
set => _orderVolume.Value = value;
}
/// <summary>
/// Take profit distance expressed in price steps.
/// </summary>
public int TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Stop loss distance expressed in price steps.
/// </summary>
public int StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Trailing stop trigger expressed in price steps.
/// </summary>
public int TrailingPoints
{
get => _trailingPoints.Value;
set => _trailingPoints.Value = value;
}
/// <summary>
/// Minimum increment required to advance the trailing stop in price steps.
/// </summary>
public int TrailingStepPoints
{
get => _trailingStepPoints.Value;
set => _trailingStepPoints.Value = value;
}
/// <summary>
/// Moving average period used by the Corrected Average filter.
/// </summary>
public int MaPeriod
{
get => _maPeriod.Value;
set => _maPeriod.Value = value;
}
/// <summary>
/// Moving average type replicated from the MetaTrader input.
/// </summary>
public MaTypes MaTypesOption
{
get => _maType.Value;
set => _maType.Value = value;
}
/// <summary>
/// Buy-side sigma expressed in price steps.
/// </summary>
public int SigmaBuyPoints
{
get => _sigmaBuyPoints.Value;
set => _sigmaBuyPoints.Value = value;
}
/// <summary>
/// Sell-side sigma expressed in price steps.
/// </summary>
public int SigmaSellPoints
{
get => _sigmaSellPoints.Value;
set => _sigmaSellPoints.Value = value;
}
/// <summary>
/// Candle type used for indicator calculations and signal evaluation.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="CorrectedAverageChannelStrategy"/> class.
/// </summary>
public CorrectedAverageChannelStrategy()
{
_orderVolume = Param(nameof(OrderVolume), 0.1m)
.SetGreaterThanZero()
.SetDisplay("Order Volume", "Market order size used for entries", "Trading")
;
_takeProfitPoints = Param(nameof(TakeProfitPoints), 60)
.SetNotNegative()
.SetDisplay("Take Profit (points)", "Distance from entry to the profit target in price steps", "Risk")
;
_stopLossPoints = Param(nameof(StopLossPoints), 40)
.SetNotNegative()
.SetDisplay("Stop Loss (points)", "Distance from entry to the protective stop in price steps", "Risk")
;
_trailingPoints = Param(nameof(TrailingPoints), 0)
.SetNotNegative()
.SetDisplay("Trailing Trigger (points)", "Profit distance required before the trailing stop activates", "Risk")
;
_trailingStepPoints = Param(nameof(TrailingStepPoints), 0)
.SetNotNegative()
.SetDisplay("Trailing Step (points)", "Minimum advance in price steps before the trailing stop moves", "Risk")
;
_maPeriod = Param(nameof(MaPeriod), 35)
.SetRange(2, 500)
.SetDisplay("MA Period", "Period of the moving average and standard deviation", "Indicator")
;
_maType = Param(nameof(MaTypesOption), MaTypes.Sma)
.SetDisplay("MA Type", "Moving average type used inside the Corrected Average", "Indicator");
_sigmaBuyPoints = Param(nameof(SigmaBuyPoints), 5)
.SetNotNegative()
.SetDisplay("Sigma BUY (points)", "Offset added above the corrected average before buying", "Signal")
;
_sigmaSellPoints = Param(nameof(SigmaSellPoints), 5)
.SetNotNegative()
.SetDisplay("Sigma SELL (points)", "Offset subtracted from the corrected average before selling", "Signal")
;
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Timeframe used for calculations", "Data");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_ma = null;
_std = null;
_priceStep = 0m;
_sigmaBuyOffset = 0m;
_sigmaSellOffset = 0m;
_stopLossDistance = 0m;
_takeProfitDistance = 0m;
_trailingDistance = 0m;
_trailingStepDistance = 0m;
_previousCorrected = null;
_previousClose = null;
_entryPrice = null;
_stopLossPrice = null;
_takeProfitPrice = null;
_longTrailingStop = null;
_shortTrailingStop = null;
_previousPosition = 0m;
_lastTradePrice = null;
_lastTradeSide = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_ma = CreateMa(MaTypesOption, MaPeriod);
_std = new StandardDeviation
{
Length = MaPeriod
};
_priceStep = Security?.PriceStep ?? 0m;
if (_priceStep <= 0m)
{
_priceStep = 1m;
}
_sigmaBuyOffset = GetPriceOffset(SigmaBuyPoints);
_sigmaSellOffset = GetPriceOffset(SigmaSellPoints);
_stopLossDistance = GetPriceOffset(StopLossPoints);
_takeProfitDistance = GetPriceOffset(TakeProfitPoints);
_trailingDistance = GetPriceOffset(TrailingPoints);
_trailingStepDistance = GetPriceOffset(TrailingStepPoints);
Volume = OrderVolume;
var subscription = SubscribeCandles(CandleType);
subscription.Bind(_ma, _std, ProcessCandle).Start();
}
/// <inheritdoc />
protected override void OnOwnTradeReceived(MyTrade trade)
{
base.OnOwnTradeReceived(trade);
if (trade.Trade != null)
{
_lastTradePrice = trade.Trade.Price;
}
_lastTradeSide = trade.Order.Side;
}
/// <inheritdoc />
protected override void OnPositionReceived(Position position)
{
base.OnPositionReceived(position);
if (_previousPosition == 0m && Position != 0m)
{
var entryPrice = _lastTradePrice ?? _previousClose;
if (entryPrice is decimal price)
{
if (Position > 0m && _lastTradeSide == Sides.Buy)
{
InitializeRiskState(price, true);
}
else if (Position < 0m && _lastTradeSide == Sides.Sell)
{
InitializeRiskState(price, false);
}
}
}
else if (Position == 0m && _previousPosition != 0m)
{
ResetRiskState();
}
_previousPosition = Position;
}
private void ProcessCandle(ICandleMessage candle, decimal maValue, decimal stdValue)
{
if (candle.State != CandleStates.Finished)
return;
if (_ma is null || _std is null)
return;
if (!_ma.IsFormed || !_std.IsFormed)
{
_previousCorrected = maValue;
_previousClose = candle.ClosePrice;
return;
}
var previousCorrected = _previousCorrected;
var previousClose = _previousClose;
decimal corrected;
if (previousCorrected is not decimal prevCorrected)
{
corrected = maValue;
}
else
{
var diff = prevCorrected - maValue;
var v2 = diff * diff;
var v1 = stdValue * stdValue;
var k = (v2 <= 0m || v2 < v1) ? 0m : 1m - (v1 / v2);
corrected = prevCorrected + k * (maValue - prevCorrected);
}
if (HandleTrailing(candle))
{
_previousCorrected = corrected;
_previousClose = candle.ClosePrice;
return;
}
if (HandleRiskExit(candle))
{
_previousCorrected = corrected;
_previousClose = candle.ClosePrice;
return;
}
if (!IsFormedAndOnlineAndAllowTrading())
{
_previousCorrected = corrected;
_previousClose = candle.ClosePrice;
return;
}
if (Position == 0m && previousCorrected is decimal prevCorr && previousClose is decimal prevCls)
{
var buyThreshold = corrected + _sigmaBuyOffset;
var sellThreshold = corrected - _sigmaSellOffset;
var buySignal = prevCls < prevCorr + _sigmaBuyOffset && candle.ClosePrice >= buyThreshold;
var sellSignal = prevCls > prevCorr - _sigmaSellOffset && candle.ClosePrice <= sellThreshold;
if (buySignal)
{
BuyMarket();
}
else if (sellSignal)
{
SellMarket();
}
}
_previousCorrected = corrected;
_previousClose = candle.ClosePrice;
}
private bool HandleTrailing(ICandleMessage candle)
{
if (_trailingDistance <= 0m || _entryPrice is null)
return false;
var volume = Math.Abs(Position);
if (volume <= 0m)
return false;
if (Position > 0m)
{
var moved = candle.ClosePrice - _entryPrice.Value;
if (moved > _trailingDistance)
{
var candidate = candle.ClosePrice - _trailingDistance;
if (_longTrailingStop is null || candidate - _longTrailingStop.Value >= _trailingStepDistance)
{
_longTrailingStop = Security?.ShrinkPrice(candidate) ?? candidate;
}
}
if (_longTrailingStop is decimal trailing && candle.LowPrice <= trailing)
{
SellMarket(volume);
ResetRiskState();
return true;
}
}
else if (Position < 0m)
{
var moved = _entryPrice.Value - candle.ClosePrice;
if (moved > _trailingDistance)
{
var candidate = candle.ClosePrice + _trailingDistance;
if (_shortTrailingStop is null || _shortTrailingStop.Value - candidate >= _trailingStepDistance)
{
_shortTrailingStop = Security?.ShrinkPrice(candidate) ?? candidate;
}
}
if (_shortTrailingStop is decimal trailing && candle.HighPrice >= trailing)
{
BuyMarket(volume);
ResetRiskState();
return true;
}
}
return false;
}
private bool HandleRiskExit(ICandleMessage candle)
{
var volume = Math.Abs(Position);
if (volume <= 0m)
return false;
if (Position > 0m)
{
if (_stopLossPrice is decimal stop && candle.LowPrice <= stop)
{
SellMarket(volume);
ResetRiskState();
return true;
}
if (_takeProfitPrice is decimal target && candle.HighPrice >= target)
{
SellMarket(volume);
ResetRiskState();
return true;
}
}
else if (Position < 0m)
{
if (_stopLossPrice is decimal stop && candle.HighPrice >= stop)
{
BuyMarket(volume);
ResetRiskState();
return true;
}
if (_takeProfitPrice is decimal target && candle.LowPrice <= target)
{
BuyMarket(volume);
ResetRiskState();
return true;
}
}
return false;
}
private void InitializeRiskState(decimal entryPrice, bool isLong)
{
_entryPrice = entryPrice;
_stopLossPrice = null;
_takeProfitPrice = null;
_longTrailingStop = null;
_shortTrailingStop = null;
if (_stopLossDistance > 0m)
{
var rawPrice = isLong ? entryPrice - _stopLossDistance : entryPrice + _stopLossDistance;
_stopLossPrice = Security?.ShrinkPrice(rawPrice) ?? rawPrice;
}
if (_takeProfitDistance > 0m)
{
var rawPrice = isLong ? entryPrice + _takeProfitDistance : entryPrice - _takeProfitDistance;
_takeProfitPrice = Security?.ShrinkPrice(rawPrice) ?? rawPrice;
}
}
private void ResetRiskState()
{
_entryPrice = null;
_stopLossPrice = null;
_takeProfitPrice = null;
_longTrailingStop = null;
_shortTrailingStop = null;
}
private decimal GetPriceOffset(int points)
{
if (points <= 0 || _priceStep <= 0m)
return 0m;
return points * _priceStep;
}
private static DecimalLengthIndicator CreateMa(MaTypes type, int length)
{
return type switch
{
MaTypes.Sma => new SMA { Length = length },
MaTypes.Ema => new EMA { Length = length },
MaTypes.Smma => new SmoothedMovingAverage { Length = length },
MaTypes.Lwma => new WeightedMovingAverage { Length = length },
_ => throw new ArgumentOutOfRangeException(nameof(type))
};
}
/// <summary>
/// Supported moving average types.
/// </summary>
public enum MaTypes
{
Sma,
Ema,
Smma,
Lwma
}
}
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, Sides
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Algo.Indicators import (
SimpleMovingAverage,
ExponentialMovingAverage,
SmoothedMovingAverage,
WeightedMovingAverage,
StandardDeviation,
)
class corrected_average_channel_strategy(Strategy):
def __init__(self):
super(corrected_average_channel_strategy, self).__init__()
self._order_volume = self.Param("OrderVolume", 0.1) \
.SetDisplay("Order Volume", "Market order size used for entries", "Trading")
self._take_profit_points = self.Param("TakeProfitPoints", 60) \
.SetDisplay("Take Profit (points)", "Distance from entry to the profit target in price steps", "Risk")
self._stop_loss_points = self.Param("StopLossPoints", 40) \
.SetDisplay("Stop Loss (points)", "Distance from entry to the protective stop in price steps", "Risk")
self._trailing_points = self.Param("TrailingPoints", 0) \
.SetDisplay("Trailing Trigger (points)", "Profit distance required before the trailing stop activates", "Risk")
self._trailing_step_points = self.Param("TrailingStepPoints", 0) \
.SetDisplay("Trailing Step (points)", "Minimum advance in price steps before the trailing stop moves", "Risk")
self._ma_period = self.Param("MaPeriod", 35) \
.SetDisplay("MA Period", "Period of the moving average and standard deviation", "Indicator")
self._ma_type = self.Param("MaType", 0) \
.SetDisplay("MA Type", "0=SMA, 1=EMA, 2=SMMA, 3=LWMA", "Indicator")
self._sigma_buy_points = self.Param("SigmaBuyPoints", 5) \
.SetDisplay("Sigma BUY (points)", "Offset added above the corrected average before buying", "Signal")
self._sigma_sell_points = self.Param("SigmaSellPoints", 5) \
.SetDisplay("Sigma SELL (points)", "Offset subtracted from the corrected average before selling", "Signal")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Candle Type", "Timeframe used for calculations", "Data")
self._ma = None
self._std = None
self._price_step = 0.0
self._sigma_buy_offset = 0.0
self._sigma_sell_offset = 0.0
self._stop_loss_distance = 0.0
self._take_profit_distance = 0.0
self._trailing_distance = 0.0
self._trailing_step_distance = 0.0
self._previous_corrected = None
self._previous_close = None
self._entry_price = None
self._stop_loss_price = None
self._take_profit_price = None
self._long_trailing_stop = None
self._short_trailing_stop = None
self._previous_position = 0.0
self._last_trade_price = None
self._last_trade_side = None
@property
def OrderVolume(self):
return self._order_volume.Value
@property
def TakeProfitPoints(self):
return self._take_profit_points.Value
@property
def StopLossPoints(self):
return self._stop_loss_points.Value
@property
def TrailingPoints(self):
return self._trailing_points.Value
@property
def TrailingStepPoints(self):
return self._trailing_step_points.Value
@property
def MaPeriod(self):
return self._ma_period.Value
@property
def MaType(self):
return self._ma_type.Value
@property
def SigmaBuyPoints(self):
return self._sigma_buy_points.Value
@property
def SigmaSellPoints(self):
return self._sigma_sell_points.Value
@property
def CandleType(self):
return self._candle_type.Value
def _create_ma(self, ma_type, length):
if ma_type == 1:
ind = ExponentialMovingAverage()
elif ma_type == 2:
ind = SmoothedMovingAverage()
elif ma_type == 3:
ind = WeightedMovingAverage()
else:
ind = SimpleMovingAverage()
ind.Length = length
return ind
def _get_price_offset(self, points):
pts = int(points)
if pts <= 0 or self._price_step <= 0:
return 0.0
return pts * self._price_step
def OnStarted2(self, time):
super(corrected_average_channel_strategy, self).OnStarted2(time)
self._ma = self._create_ma(self.MaType, self.MaPeriod)
self._std = StandardDeviation()
self._std.Length = self.MaPeriod
self._price_step = 0.0
if self.Security is not None and self.Security.PriceStep is not None:
self._price_step = float(self.Security.PriceStep)
if self._price_step <= 0:
self._price_step = 1.0
self._sigma_buy_offset = self._get_price_offset(self.SigmaBuyPoints)
self._sigma_sell_offset = self._get_price_offset(self.SigmaSellPoints)
self._stop_loss_distance = self._get_price_offset(self.StopLossPoints)
self._take_profit_distance = self._get_price_offset(self.TakeProfitPoints)
self._trailing_distance = self._get_price_offset(self.TrailingPoints)
self._trailing_step_distance = self._get_price_offset(self.TrailingStepPoints)
self.Volume = float(self.OrderVolume)
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._ma, self._std, self.ProcessCandle).Start()
def OnOwnTradeReceived(self, trade):
super(corrected_average_channel_strategy, self).OnOwnTradeReceived(trade)
if trade is None or trade.Order is None:
return
if trade.Trade is not None:
self._last_trade_price = float(trade.Trade.Price)
self._last_trade_side = trade.Order.Side
prev_pos = self._previous_position
cur_pos = self.Position
if prev_pos == 0 and cur_pos != 0:
entry_price = self._last_trade_price if self._last_trade_price is not None else self._previous_close
if entry_price is not None:
if cur_pos > 0 and self._last_trade_side == Sides.Buy:
self._initialize_risk_state(entry_price, True)
elif cur_pos < 0 and self._last_trade_side == Sides.Sell:
self._initialize_risk_state(entry_price, False)
elif cur_pos == 0 and prev_pos != 0:
self._reset_risk_state()
self._previous_position = cur_pos
def ProcessCandle(self, candle, ma_value, std_value):
if candle.State != CandleStates.Finished:
return
ma_value = float(ma_value)
std_value = float(std_value)
if self._ma is None or self._std is None:
return
if not self._ma.IsFormed or not self._std.IsFormed:
self._previous_corrected = ma_value
self._previous_close = float(candle.ClosePrice)
return
previous_corrected = self._previous_corrected
previous_close = self._previous_close
if previous_corrected is None:
corrected = ma_value
else:
diff = previous_corrected - ma_value
v2 = diff * diff
v1 = std_value * std_value
if v2 <= 0 or v2 < v1:
k = 0.0
else:
k = 1.0 - (v1 / v2)
corrected = previous_corrected + k * (ma_value - previous_corrected)
if self._handle_trailing(candle):
self._previous_corrected = corrected
self._previous_close = float(candle.ClosePrice)
return
if self._handle_risk_exit(candle):
self._previous_corrected = corrected
self._previous_close = float(candle.ClosePrice)
return
if not self.IsFormedAndOnlineAndAllowTrading():
self._previous_corrected = corrected
self._previous_close = float(candle.ClosePrice)
return
if self.Position == 0 and previous_corrected is not None and previous_close is not None:
buy_threshold = corrected + self._sigma_buy_offset
sell_threshold = corrected - self._sigma_sell_offset
close_price = float(candle.ClosePrice)
buy_signal = previous_close < previous_corrected + self._sigma_buy_offset and close_price >= buy_threshold
sell_signal = previous_close > previous_corrected - self._sigma_sell_offset and close_price <= sell_threshold
if buy_signal:
self.BuyMarket()
elif sell_signal:
self.SellMarket()
self._previous_corrected = corrected
self._previous_close = float(candle.ClosePrice)
def _handle_trailing(self, candle):
if self._trailing_distance <= 0 or self._entry_price is None:
return False
volume = abs(self.Position)
if volume <= 0:
return False
close_price = float(candle.ClosePrice)
if self.Position > 0:
moved = close_price - self._entry_price
if moved > self._trailing_distance:
candidate = close_price - self._trailing_distance
if self._long_trailing_stop is None or candidate - self._long_trailing_stop >= self._trailing_step_distance:
self._long_trailing_stop = candidate
if self._long_trailing_stop is not None and float(candle.LowPrice) <= self._long_trailing_stop:
self.SellMarket(volume)
self._reset_risk_state()
return True
elif self.Position < 0:
moved = self._entry_price - close_price
if moved > self._trailing_distance:
candidate = close_price + self._trailing_distance
if self._short_trailing_stop is None or self._short_trailing_stop - candidate >= self._trailing_step_distance:
self._short_trailing_stop = candidate
if self._short_trailing_stop is not None and float(candle.HighPrice) >= self._short_trailing_stop:
self.BuyMarket(volume)
self._reset_risk_state()
return True
return False
def _handle_risk_exit(self, candle):
volume = abs(self.Position)
if volume <= 0:
return False
if self.Position > 0:
if self._stop_loss_price is not None and float(candle.LowPrice) <= self._stop_loss_price:
self.SellMarket(volume)
self._reset_risk_state()
return True
if self._take_profit_price is not None and float(candle.HighPrice) >= self._take_profit_price:
self.SellMarket(volume)
self._reset_risk_state()
return True
elif self.Position < 0:
if self._stop_loss_price is not None and float(candle.HighPrice) >= self._stop_loss_price:
self.BuyMarket(volume)
self._reset_risk_state()
return True
if self._take_profit_price is not None and float(candle.LowPrice) <= self._take_profit_price:
self.BuyMarket(volume)
self._reset_risk_state()
return True
return False
def _initialize_risk_state(self, entry_price, is_long):
self._entry_price = entry_price
self._stop_loss_price = None
self._take_profit_price = None
self._long_trailing_stop = None
self._short_trailing_stop = None
if self._stop_loss_distance > 0:
if is_long:
self._stop_loss_price = entry_price - self._stop_loss_distance
else:
self._stop_loss_price = entry_price + self._stop_loss_distance
if self._take_profit_distance > 0:
if is_long:
self._take_profit_price = entry_price + self._take_profit_distance
else:
self._take_profit_price = entry_price - self._take_profit_distance
def _reset_risk_state(self):
self._entry_price = None
self._stop_loss_price = None
self._take_profit_price = None
self._long_trailing_stop = None
self._short_trailing_stop = None
def OnReseted(self):
super(corrected_average_channel_strategy, self).OnReseted()
self._ma = None
self._std = None
self._price_step = 0.0
self._sigma_buy_offset = 0.0
self._sigma_sell_offset = 0.0
self._stop_loss_distance = 0.0
self._take_profit_distance = 0.0
self._trailing_distance = 0.0
self._trailing_step_distance = 0.0
self._previous_corrected = None
self._previous_close = None
self._entry_price = None
self._stop_loss_price = None
self._take_profit_price = None
self._long_trailing_stop = None
self._short_trailing_stop = None
self._previous_position = 0.0
self._last_trade_price = None
self._last_trade_side = None
def CreateClone(self):
return corrected_average_channel_strategy()