GitHub で見る

コイン投げ戦略

概要

コイン投げ戦略は、コイン投げをシミュレートして買いか売りかを決める古典的なMetaTraderのエキスパートアドバイザーをそのまま移植したものです。戦略がポジションを持っていない間、完了した各ローソク足が新しい決定を引き起こすため、システムは継続的な独立したトレードのシリーズを繰り返します。StockSharpへの変換は動作を意図的にシンプルに保ちます:一度に1つのポジションのみ保持し、各トレードはpipsで表現された対称なテイクプロフィットとストップロスとペアになっています。

核心的なアイデアは意図的に単純ですが、この例は非常に小さなエキスパートアドバイザーでもStockSharpのハイレベルAPIに変換できることを示しています。この戦略は、サブスクリプション、資金管理ヘルパー、保護注文を接続するための教材として役立ちます。

トレーディングロジック

  1. 戦略の開始時に、乱数ジェネレーターが現在の環境ティックカウントでシードされ、MQLの元の MathSrand(GetTickCount()) 呼び出しの精神を反映します。
  2. 完了した各ローソク足 (デフォルトの時間軸は1分ですが、任意のローソク足タイプを指定可能) に対して、戦略はトレードが許可されているかどうかと、現在ポジションが開いていないかを確認します。
  3. ポジションがない場合、ジェネレーターは0または1を生成します。値が0の場合は成行買い注文が、1の場合は成行売り注文が発動します。ボリュームは設定されたリスクパーセンテージとストップロス距離に基づいて動的に計算されます。
  4. StartProtection によって作成された保護注文は、各ポジションにストップロスとテイクプロフィットを付加し、退場管理が自動的に行われるようにします。

他のフィルターは使用されません:ポジションが閉じられるたびに、次のローソク足がすぐに新しいトレードを作成します。

ポジションサイジング

StockSharpバージョンは、ポートフォリオ値で機能するようにロットサイズの計算式を再解釈します。リスク金額は Portfolio.CurrentValue * RiskPercent / 100 として計算されます。この資本は価格単位のストップロス距離 (証券の価格ステップを使って変換したpips) で割られ、コントラクト数が導き出されます。ヘルパーはその後、最も近い許容ボリュームステップにサイズを丸め、MinVolumeMaxVolume を通じて取引所の制限を適用します。

これにより元のコードの精神 — トレードごとにエクイティの固定パーセンテージをリスクにさらす — を維持しながら、注文サイズがStockSharpのセキュリティメタデータを尊重することが確保されます。

パラメーター

パラメーター 説明 デフォルト 備考
RiskPercent 各トレードでリスクにさらすポートフォリオのパーセンテージ。 2 この数値を増やすとボリュームが増加し、減らすと注文が小さくなります。
TakeProfitPips エントリーとテイクプロフィットレベル間のpips距離。 20 インストゥルメントの価格ステップを使って絶対価格に変換され、StartProtection に渡されます。
StopLossPips エントリーとストップロスレベル間のpips距離。 10 価格単位にも変換されます。同じ値がポジションサイジングに使用されます。
CandleType 決定ループをスケジュールするローソク足サブスクリプション。 1分時間軸 任意のStockSharpローソク足タイプを指定可能です。より高い間隔はトレーディングペースを遅くします。

リスク管理

StartProtectionOnStarted 中に計算されたテイクプロフィットとストップロス距離で一度起動されます。StockSharpはその後、保護注文を自動的に管理し、MQLスクリプトの OrderSend 引数を反映します。戦略は Position == 0 の場合のみ取引するため、既存の注文を手動でキャンセルまたは再送信する必要はありません。ポジションが閉じられると、プラットフォームが保護注文をキャンセルします。

実装メモ

  • ローソク足処理は明確さと簡潔さのためにハイレベルの SubscribeCandles().Bind(...) パターンを使用します。
  • ログステートメントは選択された方向とボリュームを記述するため、バックテストで疑似乱数ジェネレーターがどのように動作するかを明確に示します。
  • ボリューム正規化は VolumeStepMinVolumeMaxVolume を考慮し、生成されたサイズがインストゥルメントの仕様に準拠することを確保します。
  • コードはすべてのコメントを英語で保持し、リポジトリのガイドラインが要求する構造を反映します。

使用上の注意

  • トレードの方向がランダムであるため、長期的な収益性は期待されません。デモンストレーションまたはテスト目的で戦略を使用してください。
  • 戦略に割り当てられたポートフォリオが正の CurrentValue を持っていることを確認してください。そうでない場合、リスク計算はゼロを返し、トレードは行われません。
  • コイン投げの頻度を減らしたい場合 (例:時間足ローソク足) または増やしたい場合 (例:ティックローソク足) は、ローソク足タイプを調整してください。
  • 最適化する際には、テイクプロフィットとストップロスの代替距離を探索したり、リスクパーセンテージを下げてドローダウンを管理可能に保つことができます。
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 coin flipping strategy that alternates between buying and selling based on a pseudo-random generator.
/// Mimics the original MetaTrader expert advisor by opening a single position at a time with symmetric risk controls.
/// </summary>
public class CoinFlippingStrategy : Strategy
{
	private readonly StrategyParam<decimal> _riskPercent;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<DataType> _candleType;

	private Random _random;
	private decimal _priceStep;
	private decimal _takeProfitDistance;
	private decimal _stopLossDistance;
	private decimal _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takePrice;

	/// <summary>
	/// Portfolio share allocated to every trade in percent.
	/// </summary>
	public decimal RiskPercent
	{
		get => _riskPercent.Value;
		set => _riskPercent.Value = value;
	}

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

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

	/// <summary>
	/// Candle type used for scheduling trade attempts.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initialize strategy parameters.
	/// </summary>
	public CoinFlippingStrategy()
	{
		_riskPercent = Param(nameof(RiskPercent), 2m)
			.SetGreaterThanZero()
			.SetDisplay("Risk %", "Portfolio percentage allocated per trade", "Risk Management")
			
			.SetOptimize(1m, 10m, 1m);

		_takeProfitPips = Param(nameof(TakeProfitPips), 5000)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit (pips)", "Target distance expressed in pips", "Risk Management")

			.SetOptimize(10, 50, 5);

		_stopLossPips = Param(nameof(StopLossPips), 3000)
			.SetGreaterThanZero()
			.SetDisplay("Stop Loss (pips)", "Protective stop distance expressed in pips", "Risk Management")

			.SetOptimize(5, 30, 5);

		_candleType = Param(nameof(CandleType), TimeSpan.FromDays(1).TimeFrame())
			.SetDisplay("Candle Type", "Candle type used for trade timing", "Data");
	}

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

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

		// Reset cached state when the strategy is reset.
		_random = null;
		_priceStep = 0m;
		_takeProfitDistance = 0m;
		_stopLossDistance = 0m;
		_entryPrice = 0m;
		_stopPrice = null;
		_takePrice = null;
	}

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

		// Seed the pseudo-random generator similarly to the MQL expert.
		_random = new Random(System.Environment.TickCount);

		// Determine price step information for translating pips into price units.
		_priceStep = Security?.PriceStep ?? 1m;
		if (_priceStep <= 0m)
			_priceStep = 1m;

		_takeProfitDistance = TakeProfitPips * _priceStep;
		_stopLossDistance = StopLossPips * _priceStep;

		// Subscribe to candle data to trigger decision making once per bar.
		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(ProcessCandle).Start();

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

	private void ProcessCandle(ICandleMessage candle)
	{
		// Only use completed candles to avoid duplicate executions while a bar is forming.
		if (candle.State != CandleStates.Finished)
			return;

		// Check risk management first.
		if (Position > 0)
		{
			if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
			{
				SellMarket(Position);
				ResetTargets();
			}
			else if (_takePrice.HasValue && candle.HighPrice >= _takePrice.Value)
			{
				SellMarket(Position);
				ResetTargets();
			}
		}
		else if (Position < 0)
		{
			if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
			{
				BuyMarket(Math.Abs(Position));
				ResetTargets();
			}
			else if (_takePrice.HasValue && candle.LowPrice <= _takePrice.Value)
			{
				BuyMarket(Math.Abs(Position));
				ResetTargets();
			}
		}

		// The strategy maintains at most one position at a time.
		if (Position != 0)
			return;

		if (_random == null)
			return;

		var entryPrice = candle.ClosePrice;
		if (entryPrice <= 0m)
			return;

		var volume = CalculateOrderVolume(entryPrice);
		if (volume <= 0m)
			return;

		var isBuy = _random.Next(0, 2) == 0;
		if (isBuy)
		{
			BuyMarket(volume);
			_entryPrice = entryPrice;
			_stopPrice = _stopLossDistance > 0m ? entryPrice - _stopLossDistance : null;
			_takePrice = _takeProfitDistance > 0m ? entryPrice + _takeProfitDistance : null;
		}
		else
		{
			SellMarket(volume);
			_entryPrice = entryPrice;
			_stopPrice = _stopLossDistance > 0m ? entryPrice + _stopLossDistance : null;
			_takePrice = _takeProfitDistance > 0m ? entryPrice - _takeProfitDistance : null;
		}
	}

	private decimal CalculateOrderVolume(decimal entryPrice)
	{
		var balance = Portfolio?.CurrentValue ?? 0m;
		if (balance <= 0m)
			return 0m;

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

		var stopDistance = _stopLossDistance;
		if (stopDistance <= 0m)
		{
			stopDistance = StopLossPips * _priceStep;
		}

		if (stopDistance <= 0m)
			return 0m;

		// Risk per unit equals the stop distance; divide to get the number of contracts.
		var rawVolume = riskAmount / stopDistance;
		var volume = NormalizeVolume(rawVolume);

		if (volume <= 0m)
		{
			volume = Volume > 0m ? Volume : 1m;
			volume = NormalizeVolume(volume);
		}

		return volume;
	}

	private decimal NormalizeVolume(decimal volume)
	{
		if (volume <= 0m)
			return 0m;

		var step = Security?.VolumeStep;
		if (step.HasValue && step.Value > 0m)
		{
			volume = Math.Floor(volume / step.Value) * step.Value;
		}

		return volume > 0m ? volume : 1m;
	}

	private void ResetTargets()
	{
		_entryPrice = 0m;
		_stopPrice = null;
		_takePrice = null;
	}
}