GitHub で見る

リスク軽減ストラテジー

概要

リスク軽減ストラテジーは、MetaTraderのエキスパートアドバイザー「Reduce_risks.mq5」から変換されたマルチ時間軸のトレンドフォローシステムです。1分足ローソク足を分析してエントリーをトリガーしながら、15分と1時間の平均で市場レジームをフィルタリングします。元のアルゴリズムは高流動性の主要外国為替ペア(EURUSD、USDCHF、USDJPY)向けに設計されており、ボラティリティが抑制されて構造が継続を確認するときのみトレンドに参入することに焦点を当てています。

市場と時間軸

  • プライマリー時間軸: シグナル生成のための1分足ローソク足。
  • 確認時間軸: モメンタムの検証とウェーブポジショニングのための15分足ローソク足。
  • トレンドフィルター: より広いトレンド方向での取引を確保するための1時間足ローソク足。
  • 推奨インスツルメント: EURUSD、USDCHF、USJPYまたは同様のpip構造のインスツルメント(4桁または5桁の価格)。

インジケーターとデータ

  • M1の4つの単純移動平均(SMA):典型価格で計算したピリオド5、8、13、60。
  • M15の3つのSMA:典型価格で計算したピリオド4、5、8。
  • H1の1つのSMA:典型価格で計算したピリオド24。
  • M1とM15の両方のローソク足統計(ボディサイズ、レンジ、ヒゲ)。
  • MQLのトレーリングロジックをエミュレートするために、エントリー以降の最高値または最安値を追跡する内部カウンター。

エントリールール

ロングセットアップ

  1. 最近のM1とM15のローソク足は低ボラティリティを示す必要があります:各時間軸の3本前のバーはそれぞれ20pipsと30pips未満のレンジを持ち、15分チャネル幅は30pipsに制限されます。
  2. 最後に完成したM1のローソク足は前のものより活発ですが3倍未満であり、現在の価格は最近のM1とM15の高値の両方をブレイクします(ローカルの抵抗がクリア)。
  3. SMAの階層が上向き:SMA5 > SMA8 > SMA13でSMA60が上昇;終値は4つの平均すべての上にあります。
  4. M15のSMA4は上昇しSMA8の上に位置し、終値はM15とH1の平均の両方の上にあります。
  5. ウェーブ確認:M1のSMA8は前の3本のローソク足のいずれかの中でクロスし、M15のSMA5は前のM15ローソク足のレンジ内にあります。
  6. ローソク足構造フィルター:前のM1とM15のローソク足はレンジの半分以上の強気のボディを持ち、より高い高値を維持し、許容できる押し目(前のローソク足レンジの25%未満)を示し、イントラバーのヒゲを含んでいます(マルボーズなし)。
  7. 上記のすべての条件がオープンポジションなしに同時に満たされなければ、成行買い注文を出しません。

ショートセットアップ

  1. 同じボラティリティフィルターが適用されますが、ブレイクアウトは最近の安値を下回って発生する必要があります(サポートの違反)。
  2. SMAの階層が反転:SMA5 < SMA8 < SMA13でSMA60が下降;終値は4つの平均すべての下にあります。
  3. M15のSMA4は下降しSMA8の下にあります;終値はM15とH1の平均の両方の下にあります。
  4. ウェーブ検証:M1のSMA8は前の3本のM1ローソク足レンジのいずれかの中にあり、M15のSMA5は最後のM15ローソク足の中にあり、最近のローソク足は持続的な弱気構造を示しています(より低い安値、弱気のボディ、限定的な押し目、ヒゲあり)。
  5. アクティブなポジションなしで、すべての条件が揃うと成行売り注文が送信されます。

エグジットルール

  • 保護的なストップロスとテイクプロフィットの注文は、設定されたpips距離を使用して自動的に追加されます(元のEA動作を反映)。
  • 追加の裁量的なエグジットはMQLロジックを再現します:
    • 現在のM1ローソク足がその始値から少なくとも10pips下落した場合、または取引が1分以上オープンになった後に強い弱気のM1ローソク足が現れた場合、ロングを閉じます。
    • 価格が少なくとも10pips進んだとき、またはトレーリングの反転が発生したときに早期に利益を確定します:エントリー後の最初のバーの後、価格がエントリー以降に到達した最高レベルから20pips引き戻す場合(その高値がエントリー価格を上回っている間)。
    • 20pipsの不利な逸脱またはポートフォリオの資本が設定されたドローダウンの閾値を下回るとロングを閉じます。ショートポジションは反転した比較による対称的なロジックを使用します。

リスク管理

  • ポートフォリオの資本が(InitialDeposit * (100% - RiskPercent))を下回ると取引は自動的に停止します。制限はすべてのシグナル試行で確認され、資本が閾値を上回って回復したらリセットされます。
  • 元のMQLスクリプトには広範なターミナルチェックが含まれていました;それらはStockSharpが接続性と権限をネイティブに処理するため省略されています。

パラメーター

名前 説明 デフォルト
StopLossPips pips単位の保護ストップ距離(トレーリングロジックで反映)。 30
TakeProfitPips pips単位のテイクプロフィット距離。 60
InitialDeposit ドローダウンストップを計算するために使用する参照資本。 10000
RiskPercent 新しい取引をブロックしてアクティブなポジションを強制終了する前に失うことができる初期デポジットの最大パーセンテージ。 5
M1CandleType 1分足ローソク足サブスクリプションのデータタイプ。 1分の時間軸
M15CandleType 15分確認サブスクリプションのデータタイプ。 15分の時間軸
H1CandleType 1時間トレンドフィルターサブスクリプションのデータタイプ。 1時間の時間軸

注意事項

  • ストラテジーは主要な外国為替ペアと同様のpipサイズで見積もられたインスツルメントを期待します。他の市場を使用する際はpipベースのパラメーターを調整してください。
  • C#実装のみが提供されます;Python版は要件に従い意図的に省略されています。
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;

using StockSharp.Algo;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Trend-following strategy converted from the "Reduce risks" MQL5 expert.
/// Uses SMA hierarchy (short/medium/long) for trend detection with risk control exits.
/// Enters on confirmed SMA crossover, exits on reverse cross or stop/take profit.
/// </summary>
public class ReduceRisksStrategy : Strategy
{
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<decimal> _initialDeposit;
	private readonly StrategyParam<decimal> _riskPercent;
	private readonly StrategyParam<DataType> _candleType;

	private SimpleMovingAverage _smaShort;
	private SimpleMovingAverage _smaMedium;
	private SimpleMovingAverage _smaLong;

	private decimal? _smaShortCurr;
	private decimal? _smaShortPrev;
	private decimal? _smaMediumCurr;
	private decimal? _smaMediumPrev;
	private decimal? _smaLongCurr;
	private decimal? _smaLongPrev;

	private decimal _riskThreshold;
	private int _riskExceededCounter;
	private int _barsSinceEntry;
	private decimal _entryPrice;
	private int _barsShortAboveMedium;
	private int _barsShortBelowMedium;
	private bool _enteredLong;
	private bool _enteredShort;

	/// <summary>
	/// Stop loss distance expressed in pips.
	/// </summary>
	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Take profit distance expressed in pips.
	/// </summary>
	public int TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Reference initial deposit used for equity based risk limitation.
	/// </summary>
	public decimal InitialDeposit
	{
		get => _initialDeposit.Value;
		set => _initialDeposit.Value = value;
	}

	/// <summary>
	/// Percentage of the initial deposit allowed to be lost before new entries are blocked.
	/// </summary>
	public decimal RiskPercent
	{
		get => _riskPercent.Value;
		set => _riskPercent.Value = value;
	}

	/// <summary>
	/// Candle timeframe for trading.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes strategy parameters.
	/// </summary>
	public ReduceRisksStrategy()
	{
		_stopLossPips = Param(nameof(StopLossPips), 30)
			.SetNotNegative()
			.SetDisplay("Stop Loss", "Protective stop distance in pips", "Risk");

		_takeProfitPips = Param(nameof(TakeProfitPips), 60)
			.SetNotNegative()
			.SetDisplay("Take Profit", "Target distance in pips", "Risk");

		_initialDeposit = Param(nameof(InitialDeposit), 1000000m)
			.SetGreaterThanZero()
			.SetDisplay("Initial Deposit", "Reference equity for drawdown protection", "Risk");

		_riskPercent = Param(nameof(RiskPercent), 5m)
			.SetRange(0m, 100m)
			.SetDisplay("Risk Percent", "Maximum loss allowed relative to the initial deposit", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Timeframe", "Trading timeframe", "Timeframes");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return new[] { (Security, CandleType) };
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_smaShort = null;
		_smaMedium = null;
		_smaLong = null;

		_smaShortCurr = null;
		_smaShortPrev = null;
		_smaMediumCurr = null;
		_smaMediumPrev = null;
		_smaLongCurr = null;
		_smaLongPrev = null;

		_riskThreshold = 0m;
		_riskExceededCounter = 0;
		_barsSinceEntry = 0;
		_entryPrice = 0m;
		_barsShortAboveMedium = 0;
		_barsShortBelowMedium = 0;
		_enteredLong = false;
		_enteredShort = false;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_riskThreshold = InitialDeposit * (100m - RiskPercent) / 100m;

		// SMA periods: ~2h / ~6h / ~12h on 5-min candles
		_smaShort = new SimpleMovingAverage { Length = 24 };
		_smaMedium = new SimpleMovingAverage { Length = 72 };
		_smaLong = new SimpleMovingAverage { Length = 144 };

		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(ProcessCandle).Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _smaShort);
			DrawIndicator(area, _smaMedium);
			DrawIndicator(area, _smaLong);
			DrawOwnTrades(area);
		}
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (_smaShort is null || _smaMedium is null || _smaLong is null)
			return;

		var typical = (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3m;

		UpdateSma(_smaShort, typical, candle.OpenTime, ref _smaShortCurr, ref _smaShortPrev);
		UpdateSma(_smaMedium, typical, candle.OpenTime, ref _smaMediumCurr, ref _smaMediumPrev);
		UpdateSma(_smaLong, typical, candle.OpenTime, ref _smaLongCurr, ref _smaLongPrev);

		if (_smaShortCurr is not decimal smaS ||
			_smaMediumCurr is not decimal smaM ||
			_smaLongCurr is not decimal smaL)
			return;

		// Track consecutive bars of SMA position
		if (smaS > smaM)
		{
			_barsShortAboveMedium++;
			_barsShortBelowMedium = 0;
		}
		else
		{
			_barsShortBelowMedium++;
			_barsShortAboveMedium = 0;
		}

		// Risk check
		var equity = Portfolio?.CurrentValue ?? InitialDeposit;
		var riskExceeded = equity <= _riskThreshold && InitialDeposit > 0m;

		if (riskExceeded)
		{
			if (_riskExceededCounter < 15)
			{
				LogWarning("Entry blocked. Risk limit of {0}% reached (equity={1:0.##}).", RiskPercent, equity);
				_riskExceededCounter++;
			}
		}
		else
		{
			_riskExceededCounter = 0;
		}

		// When SMA crosses in opposite direction, allow new entry of that type
		if (_barsShortBelowMedium >= 72)
			_enteredLong = false;
		if (_barsShortAboveMedium >= 72)
			_enteredShort = false;

		if (Position == 0 && !riskExceeded)
		{
			// LONG: short crosses above medium, not already entered on this cross
			if (_barsShortAboveMedium == 1 && candle.ClosePrice > smaS && !_enteredLong)
			{
				BuyMarket();
				_barsSinceEntry = 0;
				_enteredLong = true;
			}
			// SHORT: short crosses below medium, not already entered on this cross
			else if (_barsShortBelowMedium == 1 && candle.ClosePrice < smaS && !_enteredShort)
			{
				SellMarket();
				_barsSinceEntry = 0;
				_enteredShort = true;
			}
		}
		else if (Position != 0)
		{
			_barsSinceEntry++;

			if (Position > 0)
			{
				var entryPrice = _entryPrice;
				// Exit on reverse cross after min hold
				var reverseCross = _barsShortBelowMedium >= 3 && _barsSinceEntry >= 30;
				// Stop loss: 4%
				var stopLoss = entryPrice > 0 && candle.ClosePrice < entryPrice * 0.96m;
				// Take profit: 6%
				var takeProfit = entryPrice > 0 && candle.ClosePrice > entryPrice * 1.06m;

				if (reverseCross || stopLoss || takeProfit || riskExceeded)
				{
					SellMarket(Position.Abs());
				}
			}
			else if (Position < 0)
			{
				var entryPrice = _entryPrice;
				// Exit on reverse cross after min hold
				var reverseCross = _barsShortAboveMedium >= 3 && _barsSinceEntry >= 30;
				// Stop loss: 4%
				var stopLoss = entryPrice > 0 && candle.ClosePrice > entryPrice * 1.04m;
				// Take profit: 6%
				var takeProfit = entryPrice > 0 && candle.ClosePrice < entryPrice * 0.94m;

				if (reverseCross || stopLoss || takeProfit || riskExceeded)
				{
					BuyMarket(Position.Abs());
				}
			}
		}

		if (Position == 0)
		{
			_entryPrice = 0m;
			_barsSinceEntry = 0;
		}
	}

	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);
		if (trade?.Trade == null) return;
		if (Position != 0 && _entryPrice == 0m)
			_entryPrice = trade.Trade.Price;
	}

	private void UpdateSma(SimpleMovingAverage sma, decimal input, DateTimeOffset time, ref decimal? curr, ref decimal? prev)
	{
		var indicatorValue = sma.Process(new DecimalIndicatorValue(sma, input, time.UtcDateTime) { IsFinal = true });
		if (!sma.IsFormed || indicatorValue is not DecimalIndicatorValue decimalValue)
			return;

		prev = curr;
		curr = decimalValue.Value;
	}
}