Стратегия «Corrected Average Channel»
Общее описание
Corrected Average Channel — порт MetaTrader-советника e-CA-5, переписанный на C# для платформы StockSharp. Стратегия пересчитывает индикатор «Corrected Average» (CA) после закрытия каждой свечи и открывает сделку, когда цена пересекает скорректированную среднюю на заданное число пунктов (сигму). В новой реализации используются высокоуровневые свечи StockSharp, рыночные заявки и встроенное управление риском (стоп-лосс, тейк-профит и трейлинг-стоп), что сохраняет торговую логику оригинального советника.
Индикатор Corrected Average
CA сочетает сглаживание и волатильность. В MetaTrader задаются три параметра: период MA, тип MA и применяемая цена. Перенос в StockSharp реализован следующим образом:
- Тип MA выбирается через
MaTypeOption (SMA, EMA, SMMA, LWMA), период — MaPeriod.
- Параллельно рассчитывается
StandardDeviation с тем же периодом, чтобы измерить текущую волатильность.
- Для каждой завершённой свечи corrected average вычисляется рекуррентно:
- Пусть
M_t — значение MA на текущей свече, CA_{t-1} — скорректированная средняя на предыдущей свече.
v1 = StdDev_t^2, v2 = (CA_{t-1} - M_t)^2.
- Если
v2 <= 0 или v2 < v1, коэффициент коррекции k = 0, иначе k = 1 - v1 / v2.
- Новое значение
CA_t = CA_{t-1} + k * (M_t - CA_{t-1}).
- На первой свече corrected average совпадает с MA.
Такой механизм подавляет шум в спокойные периоды и ускоряет реакцию индикатора, когда цена резко отходит от средней.
Логика торговли
- Стратегия подписывается на свечи типа
CandleType и ждёт формирования MA и StdDev.
- После закрытия свечи вычисляется новый corrected average и сравнивается предыдущий close с предыдущим CA.
- Смещения
SigmaBuyPoints и SigmaSellPoints переводятся в цены через Security.PriceStep.
- Сигналы формируются по двум условиям:
- Покупка — если предыдущая свеча закрылась ниже
CA + sigma_buy, а текущая закрывается выше этого уровня.
- Продажа — если предыдущая свеча закрылась выше
CA - sigma_sell, а текущая закрывается ниже этого уровня.
- В рынке допускается только одна позиция. Новая заявка отправляется, когда текущая позиция равна нулю.
Работа по закрытым свечам делает результаты воспроизводимыми и упрощает тестирование.
Управление рисками
Перенос сохраняет все защитные механизмы EA:
- Фиксированный стоп-лосс (
StopLossPoints): расстояние от цены входа до стопа задаётся в шагах цены и реализуется рыночным закрытием при достижении уровня.
- Фиксированный тейк-профит (
TakeProfitPoints): аналогично задаётся в пунктах и закрывает позицию по рынку при достижении цели.
- Трейлинг-стоп (
TrailingPoints и TrailingStepPoints): когда прибыль превышает заданное расстояние, стратегия фиксирует новый уровень стопа за текущей ценой. Стоп двигается только в сторону профита и увеличивается не менее чем на TrailingStepPoints. Уровни приводятся к допустимым значениям через Security.ShrinkPrice.
После любого защитного выхода внутренние переменные сбрасываются. Новая сделка получает свежие значения стопов и трейлинга, что соответствует логике изменения ордеров в MetaTrader.
Параметры
| Параметр |
Описание |
OrderVolume |
Объём рыночных заявок. |
TakeProfitPoints |
Дистанция до тейк-профита в шагах цены (0 — отключить). |
StopLossPoints |
Дистанция до стоп-лосса в шагах цены (0 — отключить). |
TrailingPoints |
Прибыль в шагах цены, необходимая для активации трейлинг-стопа. |
TrailingStepPoints |
Минимальное дополнительное смещение трейлинг-стопа. |
MaPeriod |
Период MA и StdDev. |
MaTypeOption |
Тип MA: SMA, EMA, SMMA, LWMA. |
SigmaBuyPoints |
Смещение выше corrected average для входа в лонг. |
SigmaSellPoints |
Смещение ниже corrected average для входа в шорт. |
CandleType |
Тип свечей, используемых для расчётов и сигналов. |
Каждый числовой параметр помечен SetCanOptimize(true), что облегчает оптимизацию в StockSharp.
Рекомендации по применению
- По умолчанию используется часовой таймфрейм. При необходимости выберите другой интервал, соответствующий настройкам исходного советника.
- Все параметры в «пунктах» автоматически переводятся в цены через
PriceStep. Если тик не задан, принимается значение 1.
- Стратегия принимает решения только на закрытии свечей. Для внутридневного анализа используйте более короткие интервалы.
- Трейлинг реализован через рыночные заявки при пробитии уровня, что повторяет модификацию стопов в MetaTrader и не требует дополнительных отложенных ордеров.
- Согласно заданию, Python-реализация не создавалась.
Отличия от оригинала
- В StockSharp стратегия работает со свечами, а не с тиковой лентой, поэтому сигналы возникают один раз на свечу.
- Позиции ведутся в неттинговом режиме без встречных сделок, как и в MQL-версии.
- Стопы и тейк-профиты закрываются по рынку вместо изменения параметров существующих ордеров — такой подход удобен в инфраструктуре StockSharp и даёт эквивалентный результат на неттинговых счетах.
Эти изменения сохраняют идею e-CA-5, одновременно адаптируя стратегию под архитектуру StockSharp и рекомендации репозитория.
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()