GitHub で見る

RRS カオス戦略

概要

オリジナルの RRS Chaotic EA は、ティックごとにサイコロを振り続け、成行注文を発行する前にランダムなシンボルとポジション サイズを選択します。 StockSharp ポートは、設定されたセキュリティ上のキャンドル ストリームからエントリを駆動することで、制御されたカオスの精神を維持します。閉じたローソク足はそれぞれ、エキスパートアドバイザーの資金管理ルールを反映しながら、方向と量の両方について新しいランダムな決定をトリガーします。

主な特長

  • ランダムエントリー – 完了したローソク足ごとに 0 から 10 までのランダムな整数が生成されます。値 6 または 9 はロングポジションをオープンし、3 または 8 はショートポジションをオープンし、MT4 ロジックと一致します。
  • 可変ボリューム – 取引ボリュームは、MinVolume パラメータと MaxVolume パラメータの間で均一にサンプリングされ、証券のボリュームステップに合わせて調整されます。
  • スプレッド フィルター – 現在のスプレッド (ポイント単位) が MaxSpreadPoints を超えるたびに、新しいポジションはブロックされます。
  • テイクプロフィットとストップロス – エキスパートの注文レベルの設定を再現するオプションのポイントベースのエグジット。
  • ドローダウン ガード – 未実現損失は、固定の現金限度額またはポートフォリオ価値の割合と継続的に比較されます。制限に違反するとアクティブな注文がキャンセルされ、ポジションが平坦になります。

パラメーター

名前 説明
CandleType 戦略をトリガーするために使用されるローソク足シリーズ (デフォルトの 1 分ローソク足)。
MinVolume / MaxVolume ランダムなロット生成の範囲。
TakeProfitPoints 価格ポイントでの利食い距離。無効にするには、0 に設定します。
StopLossPoints 価格ポイントでのストップロス距離。無効にするには、0 に設定します。
MaxOpenTrades 同時に開いたままにすることができるボリュームステップで測定された最大正味ボリューム。
MaxSpreadPoints 価格ポイントで表される最大許容スプレッド。
SlippagePoints 情報的なスリッページ パラメータ (完全を期すために保持)。
RiskControlMode FixedMoneyBalancePercentage のリスク モデルを選択します。
RiskValue モードに応じて、リスクを負う金額または資本の割合のいずれかになります。
TradeComment 監査を容易にするために、生成された注文にタグが追加されます。

戦略ロジック

  1. 設定されたキャンドル シリーズを購読し、完成したキャンドルを待ちます。
  2. ドローダウン制御を適用します。含み損がしきい値を超えた場合は、アクティブな注文をキャンセルし、現在のポジションを閉じます。
  3. MT4 の注文設定を反映するオプションのストップロスとテイクプロフィットのターゲットを維持します。
  4. 取引が許可され、スプレッドが許容できる場合は、乱数をロールして、ロングポジションをオープンするかショートポジションをオープンするかを決定します。
  5. 音量ステップ数を MaxOpenTrades に制限することで、累積露出を制限します。

MQL4 バージョンとの違い

  • 元の専門家は複数のランダムなシンボルを取引しました。 StockSharp 戦略は単一の証券で動作します。したがって、ランダム性は方向とサイズにのみ適用されます。
  • プロテクティブストップは、ネイティブのストップロス/テイクプロフィット注文パラメーターではなく、ローソク足の終値で成行注文を通じて実行されます。
  • スプレッドの評価では、MT4 MarketInfo 関数の代わりに現在の最高の買値/売値が使用されます。
  • 生成されたすべての注文には TradeComment テキストが含まれており、MT4 マジック ナンバーと同様のコンテキストを提供します。

使用上の注意

  • 正確なポイントから価格への変換のために、接続されたセキュリティが有効な PriceStepMinStep、および VolumeStep の値を公開していることを確認します。
  • バックテスト パイプラインに負担をかけずにティック レベルのランダム性をエミュレートするために、デフォルトのローソク足の時間枠は 1 分です。取引頻度を減らすには、時間枠を増やします。
  • リスク管理は、集約されたポジションから導き出される未実現損益に依存します。 MT4 バージョンに見られるようなロング/ショートの混合バスケットは、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>
/// Randomized breakout/mean-reversion hybrid that mimics the behaviour of the RRS Chaotic EA.
/// The strategy opens random buy or sell trades with variable volume while enforcing a risk budget.
/// </summary>
public class RrsChaoticStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _minVolume;
	private readonly StrategyParam<decimal> _maxVolume;
	private readonly StrategyParam<int> _takeProfitPoints;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _maxOpenTrades;
	private readonly StrategyParam<int> _maxSpreadPoints;
	private readonly StrategyParam<int> _slippagePoints;
	private readonly StrategyParam<RiskModes> _riskMode;
	private readonly StrategyParam<decimal> _riskValue;
	private readonly StrategyParam<string> _tradeComment;

	private int _tradeCounter;

	private decimal? _longStopPrice;
	private decimal? _shortStopPrice;
	private decimal? _longTakePrice;
	private decimal? _shortTakePrice;
	private decimal _initialEquity;
	private decimal _entryPrice;

	/// <summary>
	/// Trade direction for risk sizing.
	/// </summary>
	public enum RiskModes
	{
		/// <summary>
		/// Risk a fixed cash amount.
		/// </summary>
		FixedMoney,

		/// <summary>
		/// Risk a percentage of the portfolio value.
		/// </summary>
		BalancePercentage
	}

	/// <summary>
	/// Initializes a new instance of <see cref="RrsChaoticStrategy"/>.
	/// </summary>
	public RrsChaoticStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Primary candle series used to drive random entries.", "General");

		_minVolume = Param(nameof(MinVolume), 0.01m)
			.SetDisplay("Minimum Volume", "Lower bound for the randomly generated order volume.", "Trading");

		_maxVolume = Param(nameof(MaxVolume), 0.5m)
			.SetDisplay("Maximum Volume", "Upper bound for the randomly generated order volume.", "Trading");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 50000)
			.SetDisplay("Take Profit", "Distance in points for the optional take-profit target.", "Risk");

		_stopLossPoints = Param(nameof(StopLossPoints), 50000)
			.SetDisplay("Stop Loss", "Distance in points for the protective stop-loss.", "Risk");

		_maxOpenTrades = Param(nameof(MaxOpenTrades), 10)
			.SetDisplay("Max Open Trades", "Maximum net volume measured in volume steps that may stay open.", "Trading");

		_maxSpreadPoints = Param(nameof(MaxSpreadPoints), 100)
			.SetDisplay("Max Spread", "Maximum allowed spread in points before new entries are blocked.", "Trading");

		_slippagePoints = Param(nameof(SlippagePoints), 3)
			.SetDisplay("Slippage", "Slippage tolerance in points (informational only).", "Trading")
			;

		_riskMode = Param(nameof(RiskControlMode), RiskModes.BalancePercentage)
			.SetDisplay("Risk Mode", "Choose between fixed cash or balance percentage drawdown control.", "Risk");

		_riskValue = Param(nameof(RiskValue), 5m)
			.SetDisplay("Risk Value", "Either percentage of equity or fixed cash to risk before flattening.", "Risk");

		_tradeComment = Param(nameof(TradeComment), "RRS")
			.SetDisplay("Trade Comment", "Tag attached to generated orders for traceability.", "General")
			;
	}

	/// <summary>
	/// Candle type used to trigger the strategy logic.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Minimum random volume.
	/// </summary>
	public decimal MinVolume
	{
		get => _minVolume.Value;
		set => _minVolume.Value = value;
	}

	/// <summary>
	/// Maximum random volume.
	/// </summary>
	public decimal MaxVolume
	{
		get => _maxVolume.Value;
		set => _maxVolume.Value = value;
	}

	/// <summary>
	/// Take-profit distance in points.
	/// </summary>
	public int TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Stop-loss distance in points.
	/// </summary>
	public int StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Maximum simultaneously open trades expressed in volume steps.
	/// </summary>
	public int MaxOpenTrades
	{
		get => _maxOpenTrades.Value;
		set => _maxOpenTrades.Value = value;
	}

	/// <summary>
	/// Maximum allowed spread in points before entries are skipped.
	/// </summary>
	public int MaxSpreadPoints
	{
		get => _maxSpreadPoints.Value;
		set => _maxSpreadPoints.Value = value;
	}

	/// <summary>
	/// Slippage tolerance in points (informational parameter).
	/// </summary>
	public int SlippagePoints
	{
		get => _slippagePoints.Value;
		set => _slippagePoints.Value = value;
	}

	/// <summary>
	/// Selected risk control mode.
	/// </summary>
	public RiskModes RiskControlMode
	{
		get => _riskMode.Value;
		set => _riskMode.Value = value;
	}

	/// <summary>
	/// Risk magnitude expressed either as percentage or cash.
	/// </summary>
	public decimal RiskValue
	{
		get => _riskValue.Value;
		set => _riskValue.Value = value;
	}

	/// <summary>
	/// Comment appended to generated orders.
	/// </summary>
	public string TradeComment
	{
		get => _tradeComment.Value;
		set => _tradeComment.Value = value;
	}

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

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

		_tradeCounter = 0;
		_longStopPrice = null;
		_shortStopPrice = null;
		_longTakePrice = null;
		_shortTakePrice = null;
		_initialEquity = 0m;
		_entryPrice = 0m;
	}

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

		_initialEquity = GetPortfolioValue();

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

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);

		// Update protective levels based on the latest execution price.
		var tradePrice = trade.Trade.Price;
		var direction = trade.Order.Side;

		if (Position != 0m && _entryPrice == 0m)
			_entryPrice = tradePrice;

		if (Position > 0m && direction == Sides.Buy)
		{
			_longStopPrice = CalculateStopPrice(true, tradePrice);
			_longTakePrice = CalculateTakePrice(true, tradePrice);
			_shortStopPrice = null;
			_shortTakePrice = null;
		}
		else if (Position < 0m && direction == Sides.Sell)
		{
			_shortStopPrice = CalculateStopPrice(false, tradePrice);
			_shortTakePrice = CalculateTakePrice(false, tradePrice);
			_longStopPrice = null;
			_longTakePrice = null;
		}

		if (Position == 0m)
		{
			_entryPrice = 0m;
			_longStopPrice = null;
			_longTakePrice = null;
			_shortStopPrice = null;
			_shortTakePrice = null;
		}
	}

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

		HandleRiskControl(candle);
		ApplyExitRules(candle);

		if (Position != 0m)
			return;

		if (_tradeCounter % 2 == 0)
		{
			BuyMarket(Volume);
		}
		else
		{
			SellMarket(Volume);
		}
		_tradeCounter++;
	}

	private void ApplyExitRules(ICandleMessage candle)
	{
		if (Position > 0m)
		{
			if (_longTakePrice.HasValue && candle.HighPrice >= _longTakePrice.Value)
			{
				SellMarket(Math.Abs(Position));
				return;
			}

			if (_longStopPrice.HasValue && candle.LowPrice <= _longStopPrice.Value)
			{
				SellMarket(Math.Abs(Position));
			}
		}
		else if (Position < 0m)
		{
			if (_shortTakePrice.HasValue && candle.LowPrice <= _shortTakePrice.Value)
			{
				BuyMarket(Math.Abs(Position));
				return;
			}

			if (_shortStopPrice.HasValue && candle.HighPrice >= _shortStopPrice.Value)
			{
				BuyMarket(Math.Abs(Position));
			}
		}
	}

	private void HandleRiskControl(ICandleMessage candle)
	{
		var threshold = CalculateRiskThreshold();
		if (!threshold.HasValue)
			return;

		var unrealized = GetUnrealizedPnL(candle.ClosePrice);
		if (unrealized <= threshold.Value)
		{
			ClosePosition();
		}
	}

	private decimal? CalculateRiskThreshold()
	{
		return RiskControlMode switch
		{
			RiskModes.BalancePercentage => CalculatePercentageThreshold(),
			RiskModes.FixedMoney => -Math.Abs(RiskValue),
			_ => null
		};
	}

	private decimal? CalculatePercentageThreshold()
	{
		var equity = GetPortfolioValue();
		if (equity <= 0m)
			return null;

		return -Math.Abs(equity * RiskValue / 100m);
	}

	private decimal GetPriceStep()
	{
		var security = Security;
		if (security == null)
			return 0.0001m;

		if ((security.PriceStep ?? 0m) > 0m)
			return security.PriceStep.Value;

		return 0.0001m;
	}

	private decimal? CalculateStopPrice(bool isLong, decimal entryPrice)
	{
		if (StopLossPoints <= 0)
			return null;

		var distance = StopLossPoints * GetPriceStep();
		return isLong ? entryPrice - distance : entryPrice + distance;
	}

	private decimal? CalculateTakePrice(bool isLong, decimal entryPrice)
	{
		if (TakeProfitPoints <= 0)
			return null;

		var distance = TakeProfitPoints * GetPriceStep();
		return isLong ? entryPrice + distance : entryPrice - distance;
	}

	private decimal GetUnrealizedPnL(decimal currentPrice)
	{
		if (Position == 0m)
			return 0m;

		var entry = _entryPrice;
		if (entry == 0m)
			return 0m;

		var diff = currentPrice - entry;
		return diff * Position;
	}

	private decimal GetPortfolioValue()
	{
		var portfolio = Portfolio;
		if ((portfolio?.CurrentValue ?? 0m) > 0m)
			return portfolio.CurrentValue.Value;

		if ((portfolio?.BeginValue ?? 0m) > 0m)
			return portfolio.BeginValue.Value;

		return _initialEquity <= 0m ? 10000m : _initialEquity;
	}

	private void ClosePosition()
	{
		if (Position > 0m)
		{
			SellMarket(Math.Abs(Position));
		}
		else if (Position < 0m)
		{
			BuyMarket(Math.Abs(Position));
		}
	}
}