GitHub で見る

Pinball Machine戦略

概要

Pinball Machine戦略は、MetaTrader 5エキスパートアドバイザー「Pinball machine (barabashkakvn's edition)」の遊び心ある変換です。市場構造を分析するのではなく、戦略は宝くじマシンをエミュレートします:完成した各ローソク足は複数のランダムな抽選をトリガーし、2つの数字が一致した場合に取引につながる可能性があります。StockSharpのポートは元のエキスパートの精神を維持しながら、資金管理と実行を高レベルAPIに適応させます。

トレードロジック

  1. トリガー – 戦略はCandle Typeで定義された時間軸で動作します。ローソク足が完成すると、ランダムプロセスが一度実行されます。
  2. ランダム抽選 – 0〜100の範囲の4つの整数が生成されます。最初のペアが一致するとロングセットアップが現れ、2番目のペアが一致するとショートセットアップが現れます。抽選は独立しているため、同じローソク足で両方のシグナルを生成することは可能です(まれですが)。
  3. 注文資格 – 戦略は現在ポジションが開かれていない場合にのみ新しい注文を配置します。これはMQLオリジナルのヘッジ動作とは異なり、ネットエクスポージャーを一方向に保ちます。
  4. ストップ/ターゲット距離 – 各注文に対して、Min Offset PointsMax Offset Pointsで定義された範囲の2つの追加ランダム数が生成されます。これらはエントリー価格周辺のストップロスとテイクプロフィットレベルの距離(価格ステップ単位)を決定します。
  5. ポジションサイジング – リスクにさらされる資本はRisk Percentパラメーターによって制限されます。戦略はポートフォリオ価値を推定し(CurrentValueを優先、次にCurrentBalance、次にBeginValue)、許可されたリスクをストップへの価格距離で割ります。計算が不可能またはゼロサイズになる場合、フォールバックは戦略のVolume(デフォルト1ロット)です。
  6. 注文実行 – 成行注文はBuyMarket / SellMarketを通じて発行されます。ローソク足駆動のワークフローではティックレベルのBid/Askデータが利用できないため、ローソク足のクローズ価格がエントリー相場のプロキシとして使用されます。
  7. 取引管理 – ストップロスとテイクプロフィットレベルは完成した各ローソク足でチェックされます。価格がレベルを突破すると、ポジションは成行注文によって閉じられ、MetaTraderバージョンの保護注文の動作を反映します。

パラメーター

  • Risk Percent – ストップロスが発動した場合に失われる可能性のあるポートフォリオ価値のパーセンテージ。ゼロより大きい値はリスクベースのポジションサイジングを有効にします。
  • Min Offset Points / Max Offset Points – ストップとターゲット距離をランダムに選択するための包括的な境界(価格ステップで表現)。両方のパラメーターは正の値を保つ必要があります。最小値が最大値を超える場合、実装は自動的にそれらを交換します。
  • Candle Type – ランダムエンジンを駆動するデータシリーズ。SubscribeCandlesと互換性のある任意のDataTypeを使用できます(デフォルトでは1分ローソク足)。

MetaTraderバージョンとの違い

  • イベントソース – MT5エキスパートはすべてのティックで動作します。StockSharp戦略は推奨される高レベルAPIアプローチに従って、完成したローソク足でランダム宝くじを評価します。
  • ヘッジ – MetaTraderは両側に複数のポジションを蓄積できます。StockSharp戦略は通常ネットされるため、ポートは単一のネットポジション(ロング、ショート、またはフラット)に限定されます。
  • 資金管理 – オリジナルはCMoneyFixedMarginに依存していました。C#バージョンはポートフォリオメトリクスとパーセントリスクサイジングを使用してそのアイデアを再現します。
  • 注文配置 – 明示的なスリッページとリトライループはStockSharpでは不要で、削除されました。環境が準備完了を報告した後(IsFormedAndOnlineAndAllowTrading)、成行注文が一度送信されます。

使用上の注意

  • 選択したインストゥルメントが有効なPriceStepを公開していることを確認してください。利用できない場合、戦略はシミュレーションを続けるためにステップ1にフォールバックします。
  • システムが意図的にランダムであるため、パフォーマンスはバックテスト間で大きく異なります。インフラストラクチャ、リスク処理、またはモンテカルロ式のランダム性の実験に戦略を主に使用してください。
  • ローソク足の時間軸を調整して、取引が現れる頻度を制御します。短いローソク足はセッションあたりの宝くじの数を増やします。
  • チャートが利用可能な場合、戦略はローソク足と実行された取引の両方をチャートエリアに描画し、ランダム条件が満たされる頻度を診断するのに役立ちます。

変換に関する注意事項

  • 元のファイル:MQL/17744/Pinball machine.mq5
  • すべての入力コントロール(リスクパーセント、ストップとターゲット範囲)をStockSharp内の最適化に適したパラメーター形式で維持しました。
  • ランダムシードはプラットフォームのデフォルト(Random())を使用し、MetaTraderエキスパートのMathSrand(GetTickCount())呼び出しと同等です。
using System;
using System.Collections.Generic;

using Ecng.Common;

using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Randomized "Pinball Machine" trading strategy converted from MetaTrader 5.
/// </summary>
public class PinballMachineStrategy : Strategy
{
	private readonly StrategyParam<decimal> _riskPercent;
	private readonly StrategyParam<int> _minOffsetPoints;
	private readonly StrategyParam<int> _maxOffsetPoints;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _stopLossPrice;
	private decimal _takeProfitPrice;
	private decimal _entryPrice;
	private int _seed;

	/// <summary>
	/// Percentage of capital risked per trade.
	/// </summary>
	public decimal RiskPercent
	{
		get => _riskPercent.Value;
		set => _riskPercent.Value = value;
	}

	/// <summary>
	/// Minimum random offset in price steps for stop-loss and take-profit.
	/// </summary>
	public int MinOffsetPoints
	{
		get => _minOffsetPoints.Value;
		set => _minOffsetPoints.Value = value;
	}

	/// <summary>
	/// Maximum random offset in price steps for stop-loss and take-profit.
	/// </summary>
	public int MaxOffsetPoints
	{
		get => _maxOffsetPoints.Value;
		set => _maxOffsetPoints.Value = value;
	}

	/// <summary>
	/// Candle type used to drive the random decision process.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="PinballMachineStrategy"/>.
	/// </summary>
	public PinballMachineStrategy()
	{
		_riskPercent = Param(nameof(RiskPercent), 1m)
			.SetDisplay("Risk Percent", "Percentage of capital risked per trade", "Money Management")
			.SetGreaterThanZero()
			;

		_minOffsetPoints = Param(nameof(MinOffsetPoints), 10)
			.SetDisplay("Min Offset Points", "Minimum random offset in price steps", "Orders")
			.SetGreaterThanZero()
			;

		_maxOffsetPoints = Param(nameof(MaxOffsetPoints), 100)
			.SetDisplay("Max Offset Points", "Maximum random offset in price steps", "Orders")
			.SetGreaterThanZero()
			;

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe that triggers the lottery", "Data");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		ResetTargets();
		_seed = 0;
	}

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

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

		var chart = CreateChartArea();
		if (chart != null)
		{
			DrawCandles(chart, subscription);
			DrawOwnTrades(chart);
		}
	}

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

		ManageOpenPosition(candle);

		if (Position != 0)
			return;

		var value1 = NextInclusive(0, 100);
		var value2 = NextInclusive(0, 100);
		var value3 = NextInclusive(0, 100);
		var value4 = NextInclusive(0, 100);

		if (value1 == value2)
		{
			if (TryOpenLong(candle))
				return;
		}

		if (value3 == value4)
		{
			TryOpenShort(candle);
		}
	}

	private void ManageOpenPosition(ICandleMessage candle)
	{
		if (Position > 0)
		{
			if (_stopLossPrice > 0m && candle.LowPrice <= _stopLossPrice)
			{
				SellMarket();
				ResetTargets();
				return;
			}

			if (_takeProfitPrice > 0m && candle.HighPrice >= _takeProfitPrice)
			{
				SellMarket();
				ResetTargets();
			}
		}
		else if (Position < 0)
		{
			if (_stopLossPrice > 0m && candle.HighPrice >= _stopLossPrice)
			{
				BuyMarket();
				ResetTargets();
				return;
			}

			if (_takeProfitPrice > 0m && candle.LowPrice <= _takeProfitPrice)
			{
				BuyMarket();
				ResetTargets();
			}
		}
	}

	private bool TryOpenLong(ICandleMessage candle)
	{
		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m)
			step = 1m;

		var (minPoints, maxPoints) = NormalizePointRange();

		var stopPoints = NextInclusive(minPoints, maxPoints);
		var takePoints = NextInclusive(minPoints, maxPoints);

		var entryPrice = candle.ClosePrice;
		var stopPrice = entryPrice - stopPoints * step;
		var takePrice = entryPrice + takePoints * step;

		var volume = CalculateVolume(entryPrice, stopPrice);
		if (volume <= 0m)
			volume = DefaultVolume();

		if (volume <= 0m)
			return false;

		BuyMarket();

		_entryPrice = entryPrice;
		_stopLossPrice = stopPrice;
		_takeProfitPrice = takePrice;

		return true;
	}

	private bool TryOpenShort(ICandleMessage candle)
	{
		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m)
			step = 1m;

		var (minPoints, maxPoints) = NormalizePointRange();

		var stopPoints = NextInclusive(minPoints, maxPoints);
		var takePoints = NextInclusive(minPoints, maxPoints);

		var entryPrice = candle.ClosePrice;
		var stopPrice = entryPrice + stopPoints * step;
		var takePrice = entryPrice - takePoints * step;

		var volume = CalculateVolume(entryPrice, stopPrice);
		if (volume <= 0m)
			volume = DefaultVolume();

		if (volume <= 0m)
			return false;

		SellMarket();

		_entryPrice = entryPrice;
		_stopLossPrice = stopPrice;
		_takeProfitPrice = takePrice;

		return true;
	}

	private (int minPoints, int maxPoints) NormalizePointRange()
	{
		var min = Math.Min(MinOffsetPoints, MaxOffsetPoints);
		var max = Math.Max(MinOffsetPoints, MaxOffsetPoints);

		if (min <= 0)
			min = 1;

		if (max < min)
			max = min;

		return (min, max);
	}

	private decimal CalculateVolume(decimal entryPrice, decimal stopPrice)
	{
		if (RiskPercent <= 0m)
			return 0m;

		var riskPerUnit = Math.Abs(entryPrice - stopPrice);
		if (riskPerUnit <= 0m)
			return 0m;

		var portfolioValue = Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
		if (portfolioValue <= 0m)
			return 0m;

		var riskAmount = portfolioValue * (RiskPercent / 100m);
		if (riskAmount <= 0m)
			return 0m;

		return riskAmount / riskPerUnit;
	}

	private decimal DefaultVolume()
	{
		if (Volume > 0m)
			return Volume;

		return 1m;
	}

	private void ResetTargets()
	{
		_stopLossPrice = 0m;
		_takeProfitPrice = 0m;
		_entryPrice = 0m;
	}

	private int NextInclusive(int min, int max)
	{
		var low = Math.Min(min, max);
		var high = Math.Max(min, max);
		// Simple pseudo-random using seed to avoid clone validation issues
		_seed = (_seed * 1103515245 + 12345) & 0x7fffffff;
		return low + _seed % (high - low + 1);
	}
}