校正均线通道策略
概述
校正均线通道策略 是 MetaTrader 专家顾问 e-CA-5 的 C# 版本。策略在每根 K 线收盘后重新计算“校正均线”(Corrected Average)指标,只要价格向上或向下突破校正均线一定的西格玛偏移量,就会开仓。移植后的实现完全基于 StockSharp 的高级蜡烛图 API,使用市价单,并在策略内部管理止损、止盈和移动止损,以复制原始 EA 的行为。
校正均线指标
CA 指标结合了移动平均与波动率反馈,MetaTrader 版本提供三个输入:均线周期、均线类型和应用价格。StockSharp 版本的处理方式如下:
MaTypeOption参数决定均线类型(SMA、EMA、SMMA、LWMA),MaPeriod控制周期长度。- 使用同样周期的
StandardDeviation指标衡量当前波动率。 - 每当新的蜡烛完成时按以下步骤计算校正值:
- 记
M_t为最新一根 K 线的均线数值,CA_{t-1}为上一根 K 线的校正值。 - 计算
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})。 - 第一根有效 K 线直接使用均线数值作为初始校正值。
- 记
这一递推方式可在震荡期抑制均线的变化,同时在价格显著偏离时快速跟随。
交易逻辑
- 策略订阅
CandleType参数指定的蜡烛序列,并等待均线与标准差全部形成。 - 当蜡烛收盘后,重新计算校正值,并将上一根蜡烛的收盘价与上一根校正值比较。
SigmaBuyPoints与SigmaSellPoints会通过标的物的PriceStep转换为价格距离。- 入场条件基于上一根收盘价与最新校正值:
- 做多:若上一根收盘价低于“校正值 + 买入西格玛”,且当前收盘价收于该上轨之上,则买入。
- 做空:若上一根收盘价高于“校正值 − 卖出西格玛”,且当前收盘价收于该下轨之下,则卖出。
- 策略始终保持净头寸为单向,不会在已有持仓时再次开仓。
由于移植版本基于收盘价运行,每根蜡烛仅触发一次决策,便于回测和在蜡烛级别的数据源上实盘交易。
风险控制
移植版本完整保留了原始 EA 的三种保护机制:
- 固定止损:
StopLossPoints乘以价格步长得到入场价与止损价的距离,触发后以市价单平仓。 - 固定止盈:
TakeProfitPoints转换为目标利润距离,价格达到时立即市价平仓。 - 移动止损:当
TrailingPoints大于 0 时,策略会跟踪浮动盈利;一旦盈利超过该距离,就在最新收盘价后方记录一条移动止损线。移动止损只会朝盈利方向推进,并受到TrailingStepPoints的限制,要求新止损至少比旧值多出指定步长。所有价格都会通过Security.ShrinkPrice对齐至交易所允许的最小跳动点。
任何保护性平仓都会重置内部风险状态。下一次进场时,策略会基于新的成交价重新计算止损、止盈以及移动止损,复刻 EA 修改订单保护的行为。
参数说明
| 参数 | 含义 |
|---|---|
OrderVolume |
市价单开仓手数,必须大于 0。 |
TakeProfitPoints |
止盈距离(价格步长单位,0 表示禁用)。 |
StopLossPoints |
止损距离(价格步长单位,0 表示禁用)。 |
TrailingPoints |
启动移动止损所需的盈利距离(价格步长单位)。 |
TrailingStepPoints |
每次调整移动止损所需的最小额外盈利(价格步长单位)。 |
MaPeriod |
移动平均与标准差的共同周期。 |
MaTypeOption |
移动平均类型:SMA、EMA、SMMA、LWMA。 |
SigmaBuyPoints |
在校正均线上方触发买入的西格玛偏移量。 |
SigmaSellPoints |
在校正均线下方触发卖出的西格玛偏移量。 |
CandleType |
用于计算指标和产生信号的蜡烛数据类型。 |
所有数值参数都调用了 SetCanOptimize(true),可直接在 StockSharp 中进行参数优化。
使用建议
- 默认时间框架为 1 小时,可根据历史调优结果调整。
- 所有“点数”参数均使用
PriceStep转换为真实价格距离;若标的未设置跳动点,则退化为 1,保证在指数或加密资产上也能合理运行。 - 策略仅在蜡烛结束时执行逻辑,如需更高分辨率请降低时间框架。
- 移动止损通过检测价格突破后使用市价单离场,与原始 EA 修改订单止损的思路一致,同时避免额外挂单。
- 根据任务要求,本次移植未提供 Python 版本。
与原始 EA 的差异
- 采用 StockSharp 的蜡烛 API,所有决策在蜡烛收盘时进行,而非逐笔报价。
- 策略保持单向净头寸,不会同时持有多空仓位,符合原始 EA 仅持单单的逻辑。
- 止损、止盈与移动止损通过市价单执行,而不是修改已有订单票据,在净头寸账户中能得到等效结果,同时契合 StockSharp 常见的策略结构。
上述改动在遵循仓位与风险管理准则的前提下,最大程度保留了 e-CA-5 的核心思想。
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()