パーセンテージクロスオーバーチャネル戦略
概要
パーセンテージクロスオーバーチャネル戦略は、MetaTrader 5のエキスパートアドバイザー Percentage_Crossover_Channel_EA に由来します。高速移動平均を中心に構築されたカスタムチャネルに依存し、バンドタッチまたは中央線クロスオーバーに反応します。このStockSharp実装は、ハイレベルAPIを使用して完了済みローソク足を処理しながら同じロジックに従います。
チャネルの構築
基礎となるインジケーターは、選択された価格 (デフォルトでは終値) を中心に動的チャネルを構築します:
- 設定された Applied Price モードを使用してベース価格を計算する。
- 1期間の単純移動平均を適用して短期参照価格を得る。
- Percent パラメーターを使用して2つの境界を計算する (例:50 → ±0.5%)。
- 新しい境界内に前の中央線を制限して現在の中央値を得る。
- 上下のバンドは制限された中央値に ±パーセント係数を掛けたものです。
この再帰により、価格が強いトレンド中にチャネルが遅れる一方、価格が統合するときには密接なエンベロープを維持できます。
トレーディングロジック
2つの異なるシグナルモードが利用可能です:
- バンドタッチモード (デフォルト):
- 前のローソク足の安値が下限バンドを上回っており、最後に完了したローソク足がそれに触れるか貫通する場合にロングエントリー。
- 前のローソク足の高値が上限バンドを下回っており、最後に完了したローソク足がそれに触れるか貫通する場合にショートエントリー。
- 中央線クロスオーバーモード (TradeOnMiddleCross = true):
- 価格が中央線を上から下へクロスするときにロングエントリー。
- 価格が中央線を下から上へクロスするときにショートエントリー。
ReverseSignals フラグはロングとショートのルールを入れ替えます。戦略は常に既存のポジションを閉じて反転させ、設定された OrderVolume と現在のポジションの絶対値の合計のボリュームで単一の成行注文を送信します。
リスク管理
オプションの保護レベルは元のMT5のストップロスとテイクプロフィット設定をエミュレートします:
- StopLossPoints – 推定エントリー価格から差し引かれる (ロング) またはそれに追加される (ショート) 価格ステップでの距離。
- TakeProfitPoints – エントリー価格に追加される (ロング) またはそこから差し引かれる (ショート) 価格ステップでの距離。
いずれかのパラメーターがゼロの場合、対応する保護は無効になります。ストップは、ローソク足の高値と安値を保存されたレベルと比較することで、完了した各ローソク足で評価されます。トレーリングロジックは適用されません。
パラメーター
| パラメーター | 説明 |
|---|---|
CandleType |
サブスクライブするローソク足データタイプ (デフォルトは15分の時間軸)。 |
Percent |
価格のパーセントでのチャネル幅 (±パーセント/100係数に変換)。 |
PriceMode |
チャネルに使用する適用価格。オプション: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 |
バンドタッチロジックと中央線クロスオーバーロジックを切り替える。 |
ReverseSignals |
ロングとショートの条件を反転させる。 |
StopLossPoints |
証券の価格ステップで表現された保護ストップ距離。 |
TakeProfitPoints |
証券の価格ステップで表現された利益目標距離。 |
OrderVolume |
成行エントリーのベースボリューム。戦略は1つのトランザクションで反転するために絶対オープンポジションを追加します。 |
実装メモ
- 注文はローソク足が終了した後のみ発行されます。これは前のバーのデータを使って次のバーの開始時に行動したMT5エキスパートを反映しています。
- チャネルインジケーターは履歴コレクションを保存せずに戦略内で再作成され、スカラー状態変数に依存しています。
- 保護ストップとターゲットは手動で確認され、MT5からのプラットフォーム固有の注文処理を再現します。
- 選択したインストゥルメントが有効な
PriceStepを公開していることを確認してください。そうでない場合、ストップロスとテイクプロフィットの距離は無視されます。
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()