パーセンテージ・クロスオーバー・チャンネル・システム戦略
この戦略はMetaTraderのエキスパートアドバイザー Exp_PercentageCrossoverChannel_System の直接ポートです。カスタムの「Percentage Crossover Channel」との価格の相互作用を追跡し、ローソク足が以前のブレイクアウト後にチャンネルに戻ってきたときに反応します。コードはStockSharpの高レベルAPIで書き直され、オリジナルのシグナルフローが保持されています。
トレードロジック
インジケーターの構築
- Percentage Crossover Channelは、価格に近く留まるが固定パーセンテージ(
Percent)より速くドリフトできない適応型中線を構築します。 - 上部と下部のバンドは同じパーセンテージ距離を使用して中線から導出されます。
- 完成した各ローソク足は
Shift本前のチャンネルとの関係に応じて色付けされます:- 色
3/4:上部バンドより上でのクローズ(それぞれ弱気/強気のローソク足ボディ)。 - 色
0/1:下部バンドより下でのクローズ(弱気/強気のボディ)。 - 色
2:ローソク足がチャンネル内で終了。
- 色
- Percentage Crossover Channelは、価格に近く留まるが固定パーセンテージ(
エントリーとエグジットのルール
- 最後の
SignalBarローソク足とその直前のものを評価します(MQLのCopyBuffer呼び出しを反映)。 - 強気シーケンス (
olderColor > 2):市場が最近チャンネルより上でクローズしました。最新のローソク足が内部に戻った場合(recentColor < 3)戦略は:SellPositionsCloseが有効な場合、アクティブなショートを閉じます。- 取引が開かれておらず
BuyPositionsOpenが有効な場合、ロングポジションを開きます。
- 弱気シーケンス (
olderColor < 2):市場が最近チャンネルより下でクローズしました。最新のローソク足が内部に戻った場合(recentColor > 1)戦略は:BuyPositionsCloseが有効な場合、ロングを閉じます。- 取引がアクティブでなく
SellPositionsOpenが有効な場合、ショートを開きます。
- ロジックはしたがって、ブレイクアウト方向にコミットする前にブレイクアウトとそれに続くチャンネルへの再エントリーを待ちます。
- 最後の
リスク管理
- オプションのストップロスとテイクプロフィットは価格ステップで表され、ローソク足の高値/安値で評価されます。
- 保護的な注文がトリガーされると、戦略は市場を離れ、同じバーに対する新しいエントリーを無視します。これはブローカー側のストップが最初に取引を閉じるMQLの動作を模倣します。
パラメーター
| 名前 | 説明 |
|---|---|
Percent |
チャンネル幅(パーセント)。MQLインジケーターの入力に対応します。 |
Shift |
ブレイクアウトと歴史的なバンドを比較するために使用するバーの数。 |
SignalBar |
シグナル評価のためのオフセット(バー単位)。値1はオリジナルEAのデフォルトと同様に「前のバー」を意味します。 |
BuyPositionsOpen / SellPositionsOpen |
対応する方向への取引の開始を有効または無効にします。 |
BuyPositionsClose / SellPositionsClose |
新しいシグナル時に反対ポジションを強制閉鎖することを有効または無効にします。 |
StopLoss |
Security.PriceStep の倍数で表されるストップロス距離。ゼロに設定すると無効になります。 |
TakeProfit |
価格ステップ単位のテイクプロフィット距離。ゼロに設定すると無効になります。 |
CandleType |
ローソク足サブスクリプションの時間軸。PERIOD_H4 を反映するために4時間足がデフォルトです。 |
実装に関する注意事項
- StockSharpはネイティブのPercentage Crossover Channelを提供していないため、インジケーターロジックはインラインで実装されています。中線計算、バンド導出、色割り当てはMQLソースアルゴリズムをステップバイステップで再現します。
- ポジション管理はオリジナルのヘルパー関数(
BuyPositionOpen、SellPositionOpenなど)に従い、新しいものを開く前に反対の取引を閉じて、反対のポジションがまだある場合はエントリーをスキップします。 - オリジナルのincludeファイルからのマネーマネジメント、偏差処理、マージンモード固有のロットサイジングは複製されていません。StockSharpユーザーは標準の
Strategyプロパティまたはホスティング環境を通じて戦略ボリュームを設定する必要があります。 - ストップロス/テイクプロフィットの値はMetaTraderの入力がポイントで指定されているため、価格ステップとして解釈されます。接続した銘柄が有効な
PriceStepを公開していることを確認してください。
使用のヒント
- MetaTraderと同一の動作を希望する場合は、信頼性の高い4時間足データを持つ楽器に戦略を接続してください。
CandleTypeを調整してイントラデイ操作を実験できます。 - エントリーロジックは有効な色情報を持つ2本の完成したローソク足を必要とするため、少なくとも
Shift + SignalBar + 1本のバーの履歴でウォームアップさせてください。 - チャンネルは
Percent入力に敏感です。小さな値は価格に近くフィットして取引頻度を増やし、大きな値はより強いブレイクアウトに集中します。 - ポートフォリオレベルのリスク管理と組み合わせる場合、この実装は一度に最大1つのポジションを開き、ロング、フラット、またはショート状態の間を切り替えることに注意してください。
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 breakout system translated from MQL.
/// </summary>
public class PercentageCrossoverChannelSystemStrategy : Strategy
{
private readonly StrategyParam<decimal> _percent;
private readonly StrategyParam<int> _shift;
private readonly StrategyParam<int> _signalBar;
private readonly StrategyParam<bool> _buyOpen;
private readonly StrategyParam<bool> _sellOpen;
private readonly StrategyParam<bool> _buyClose;
private readonly StrategyParam<bool> _sellClose;
private readonly StrategyParam<int> _stopLoss;
private readonly StrategyParam<int> _takeProfit;
private readonly StrategyParam<DataType> _candleType;
private readonly List<int> _colorHistory = new();
private readonly List<decimal> _upperHistory = new();
private readonly List<decimal> _lowerHistory = new();
private decimal _previousMiddle;
private bool _hasMiddle;
private decimal? _entryPrice;
public decimal Percent
{
get => _percent.Value;
set => _percent.Value = value;
}
public int Shift
{
get => _shift.Value;
set => _shift.Value = value;
}
public int SignalBar
{
get => _signalBar.Value;
set => _signalBar.Value = value;
}
public bool BuyPositionsOpen
{
get => _buyOpen.Value;
set => _buyOpen.Value = value;
}
public bool SellPositionsOpen
{
get => _sellOpen.Value;
set => _sellOpen.Value = value;
}
public bool BuyPositionsClose
{
get => _buyClose.Value;
set => _buyClose.Value = value;
}
public bool SellPositionsClose
{
get => _sellClose.Value;
set => _sellClose.Value = value;
}
public int StopLoss
{
get => _stopLoss.Value;
set => _stopLoss.Value = value;
}
public int TakeProfit
{
get => _takeProfit.Value;
set => _takeProfit.Value = value;
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public PercentageCrossoverChannelSystemStrategy()
{
_percent = Param(nameof(Percent), 1.0m)
.SetGreaterThanZero()
.SetDisplay("Channel Percent", "Percentage width of the channel", "Indicator");
_shift = Param(nameof(Shift), 1)
.SetGreaterThanZero()
.SetDisplay("Shift", "Number of bars used for crossover comparison", "Indicator");
_signalBar = Param(nameof(SignalBar), 1)
.SetGreaterThanZero()
.SetDisplay("Signal Bar", "Bars back to evaluate indicator colors", "Trading Rules");
_buyOpen = Param(nameof(BuyPositionsOpen), true)
.SetDisplay("Enable Long Entries", "Allow long position openings", "Trading Rules");
_sellOpen = Param(nameof(SellPositionsOpen), true)
.SetDisplay("Enable Short Entries", "Allow short position openings", "Trading Rules");
_buyClose = Param(nameof(BuyPositionsClose), true)
.SetDisplay("Allow Long Exits", "Permit closing long trades on bearish signals", "Trading Rules");
_sellClose = Param(nameof(SellPositionsClose), true)
.SetDisplay("Allow Short Exits", "Permit closing short trades on bullish signals", "Trading Rules");
_stopLoss = Param(nameof(StopLoss), 1000)
.SetDisplay("Stop Loss (steps)", "Protective stop loss distance in price steps", "Risk Management");
_takeProfit = Param(nameof(TakeProfit), 2000)
.SetDisplay("Take Profit (steps)", "Target profit distance in price steps", "Risk Management");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Primary timeframe for analysis", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_colorHistory.Clear();
_upperHistory.Clear();
_lowerHistory.Clear();
_hasMiddle = false;
_previousMiddle = 0m;
_entryPrice = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle).Start();
}
private void ProcessCandle(ICandleMessage candle)
{
// Ignore interim updates; we only react on closed candles.
if (candle.State != CandleStates.Finished)
return;
// Evaluate protective orders before generating new signals.
var stopTriggered = HandleRisk(candle);
// Mirror the MQL signal logic using cached indicator colors.
if (_colorHistory.Count > SignalBar)
{
// Equivalent to CopyBuffer(..., SignalBar, 2, ...) from the EA.
var recentIndex = _colorHistory.Count - SignalBar;
var olderIndex = recentIndex - 1;
if (olderIndex >= 0)
{
var recentColor = _colorHistory[recentIndex];
var olderColor = _colorHistory[olderIndex];
var shouldCloseShort = SellPositionsClose && olderColor > 2;
var shouldCloseLong = BuyPositionsClose && olderColor < 2;
var shouldOpenBuy = BuyPositionsOpen && olderColor > 2 && recentColor < 3;
var shouldOpenSell = SellPositionsOpen && olderColor < 2 && recentColor > 1;
// Close existing positions according to the original toggles.
if (shouldCloseLong && Position > 0)
{
SellMarket();
_entryPrice = null;
}
if (shouldCloseShort && Position < 0)
{
BuyMarket();
_entryPrice = null;
}
// Enter only when we are flat to match the EA behaviour.
if (!stopTriggered && Position == 0)
{
if (shouldOpenBuy)
{
BuyMarket();
_entryPrice = candle.ClosePrice;
}
else if (shouldOpenSell)
{
SellMarket();
_entryPrice = candle.ClosePrice;
}
}
}
}
// Update indicator state after trading decisions are made.
var color = CalculateColor(candle);
_colorHistory.Add(color);
TrimHistory();
}
private bool HandleRisk(ICandleMessage candle)
{
// Exit early if there is no stored entry price.
if (_entryPrice is null)
return false;
// Price step is required to translate MQL points into absolute prices.
if (Security?.PriceStep is not decimal step || step <= 0)
return false;
var triggered = false;
if (Position > 0)
{
// Long position risk checks.
if (StopLoss > 0)
{
var stopLevel = _entryPrice.Value - StopLoss * step;
if (candle.LowPrice <= stopLevel)
{
SellMarket();
_entryPrice = null;
triggered = true;
}
}
if (!triggered && TakeProfit > 0)
{
var takeLevel = _entryPrice.Value + TakeProfit * step;
if (candle.HighPrice >= takeLevel)
{
SellMarket();
_entryPrice = null;
triggered = true;
}
}
}
else if (Position < 0)
{
// Short position risk checks.
if (StopLoss > 0)
{
var stopLevel = _entryPrice.Value + StopLoss * step;
if (candle.HighPrice >= stopLevel)
{
BuyMarket();
_entryPrice = null;
triggered = true;
}
}
if (!triggered && TakeProfit > 0)
{
var takeLevel = _entryPrice.Value - TakeProfit * step;
if (candle.LowPrice <= takeLevel)
{
BuyMarket();
_entryPrice = null;
triggered = true;
}
}
}
// Reset cached entry price once we are flat.
if (Position == 0)
_entryPrice = null;
return triggered;
}
private int CalculateColor(ICandleMessage candle)
{
// Recreate the Percentage Crossover Channel midline and colour logic.
var percentFactor = Percent / 100m;
var plusVar = 1m + percentFactor;
var minusVar = 1m - percentFactor;
var close = candle.ClosePrice;
// Initialise the midline on the very first candle.
if (!_hasMiddle)
{
_previousMiddle = close;
_hasMiddle = true;
}
var middle = _previousMiddle;
var lowerCandidate = close * minusVar;
var upperCandidate = close * plusVar;
// Adjust the midline exactly as in the original indicator.
if (lowerCandidate > _previousMiddle)
{
middle = lowerCandidate;
}
else if (upperCandidate < _previousMiddle)
{
middle = upperCandidate;
}
var upper = middle + middle * percentFactor;
var lower = middle - middle * percentFactor;
_previousMiddle = middle;
var color = 2;
// Determine candle colour relative to past channel values.
if (_upperHistory.Count >= Shift)
{
var referenceIndex = _upperHistory.Count - Shift;
var referenceUpper = _upperHistory[referenceIndex];
var referenceLower = _lowerHistory[referenceIndex];
if (close > referenceUpper)
{
color = candle.OpenPrice <= close ? 4 : 3;
}
else if (close < referenceLower)
{
color = candle.OpenPrice > close ? 0 : 1;
}
}
// Persist channel history for future signal checks.
_upperHistory.Add(upper);
_lowerHistory.Add(lower);
return color;
}
private void TrimHistory()
{
// Keep only as much history as needed for Shift and SignalBar lookbacks.
var maxCapacity = Math.Max(Shift + SignalBar + 5, 16);
if (_colorHistory.Count <= maxCapacity)
return;
var removeCount = _colorHistory.Count - maxCapacity;
for (var i = 0; i < removeCount; i++)
{
try
{
_colorHistory.RemoveAt(0);
_upperHistory.RemoveAt(0);
_lowerHistory.RemoveAt(0);
}
catch { break; }
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class percentage_crossover_channel_system_strategy(Strategy):
"""Percentage Crossover Channel breakout system with SL/TP."""
def __init__(self):
super(percentage_crossover_channel_system_strategy, self).__init__()
self._percent = self.Param("Percent", 1.0) \
.SetGreaterThanZero() \
.SetDisplay("Channel Percent", "Percentage width of the channel", "Indicator")
self._shift = self.Param("Shift", 1) \
.SetGreaterThanZero() \
.SetDisplay("Shift", "Number of bars used for crossover comparison", "Indicator")
self._signal_bar = self.Param("SignalBar", 1) \
.SetGreaterThanZero() \
.SetDisplay("Signal Bar", "Bars back to evaluate indicator colors", "Trading Rules")
self._buy_open = self.Param("BuyPositionsOpen", True) \
.SetDisplay("Enable Long Entries", "Allow long position openings", "Trading Rules")
self._sell_open = self.Param("SellPositionsOpen", True) \
.SetDisplay("Enable Short Entries", "Allow short position openings", "Trading Rules")
self._buy_close = self.Param("BuyPositionsClose", True) \
.SetDisplay("Allow Long Exits", "Permit closing long trades on bearish signals", "Trading Rules")
self._sell_close = self.Param("SellPositionsClose", True) \
.SetDisplay("Allow Short Exits", "Permit closing short trades on bullish signals", "Trading Rules")
self._stop_loss = self.Param("StopLoss", 1000) \
.SetDisplay("Stop Loss (steps)", "Protective stop loss distance in price steps", "Risk Management")
self._take_profit = self.Param("TakeProfit", 2000) \
.SetDisplay("Take Profit (steps)", "Target profit distance in price steps", "Risk Management")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Candle Type", "Primary timeframe for analysis", "General")
self._color_history = []
self._upper_history = []
self._lower_history = []
self._prev_middle = 0.0
self._has_middle = False
self._entry_price = None
@property
def Percent(self):
return self._percent.Value
@property
def Shift(self):
return self._shift.Value
@property
def SignalBar(self):
return self._signal_bar.Value
@property
def BuyPositionsOpen(self):
return self._buy_open.Value
@property
def SellPositionsOpen(self):
return self._sell_open.Value
@property
def BuyPositionsClose(self):
return self._buy_close.Value
@property
def SellPositionsClose(self):
return self._sell_close.Value
@property
def StopLoss(self):
return self._stop_loss.Value
@property
def TakeProfit(self):
return self._take_profit.Value
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(percentage_crossover_channel_system_strategy, self).OnStarted2(time)
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.process_candle).Start()
def process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
stop_triggered = self._handle_risk(candle)
if len(self._color_history) > self.SignalBar:
ri = len(self._color_history) - self.SignalBar
oi = ri - 1
if oi >= 0:
rc = self._color_history[ri]
oc = self._color_history[oi]
should_close_short = self.SellPositionsClose and oc > 2
should_close_long = self.BuyPositionsClose and oc < 2
should_buy = self.BuyPositionsOpen and oc > 2 and rc < 3
should_sell = self.SellPositionsOpen and oc < 2 and rc > 1
if should_close_long and self.Position > 0:
self.SellMarket()
self._entry_price = None
if should_close_short and self.Position < 0:
self.BuyMarket()
self._entry_price = None
if not stop_triggered and self.Position == 0:
if should_buy:
self.BuyMarket()
self._entry_price = float(candle.ClosePrice)
elif should_sell:
self.SellMarket()
self._entry_price = float(candle.ClosePrice)
color = self._calc_color(candle)
self._color_history.append(color)
self._trim()
def _handle_risk(self, candle):
if self._entry_price is None:
return False
sec = self.Security
if sec is None or sec.PriceStep is None or float(sec.PriceStep) <= 0:
return False
step = float(sec.PriceStep)
triggered = False
if self.Position > 0:
if self.StopLoss > 0:
sl = self._entry_price - self.StopLoss * step
if float(candle.LowPrice) <= sl:
self.SellMarket()
self._entry_price = None
triggered = True
if not triggered and self.TakeProfit > 0:
tp = self._entry_price + self.TakeProfit * step
if float(candle.HighPrice) >= tp:
self.SellMarket()
self._entry_price = None
triggered = True
elif self.Position < 0:
if self.StopLoss > 0:
sl = self._entry_price + self.StopLoss * step
if float(candle.HighPrice) >= sl:
self.BuyMarket()
self._entry_price = None
triggered = True
if not triggered and self.TakeProfit > 0:
tp = self._entry_price - self.TakeProfit * step
if float(candle.LowPrice) <= tp:
self.BuyMarket()
self._entry_price = None
triggered = True
if self.Position == 0:
self._entry_price = None
return triggered
def _calc_color(self, candle):
pf = float(self.Percent) / 100.0
plus_var = 1.0 + pf
minus_var = 1.0 - pf
close = float(candle.ClosePrice)
if not self._has_middle:
self._prev_middle = close
self._has_middle = True
middle = self._prev_middle
lower_c = close * minus_var
upper_c = close * plus_var
if lower_c > self._prev_middle:
middle = lower_c
elif upper_c < self._prev_middle:
middle = upper_c
upper = middle + middle * pf
lower = middle - middle * pf
self._prev_middle = middle
color = 2
if len(self._upper_history) >= self.Shift:
ref_idx = len(self._upper_history) - self.Shift
ref_upper = self._upper_history[ref_idx]
ref_lower = self._lower_history[ref_idx]
if close > ref_upper:
color = 4 if float(candle.OpenPrice) <= close else 3
elif close < ref_lower:
color = 0 if float(candle.OpenPrice) > close else 1
self._upper_history.append(upper)
self._lower_history.append(lower)
return color
def _trim(self):
max_cap = max(self.Shift + self.SignalBar + 5, 16)
if len(self._color_history) <= max_cap:
return
remove = len(self._color_history) - max_cap
self._color_history = self._color_history[remove:]
self._upper_history = self._upper_history[remove:]
self._lower_history = self._lower_history[remove:]
def OnReseted(self):
super(percentage_crossover_channel_system_strategy, self).OnReseted()
self._color_history = []
self._upper_history = []
self._lower_history = []
self._has_middle = False
self._prev_middle = 0.0
self._entry_price = None
def CreateClone(self):
return percentage_crossover_channel_system_strategy()