GitHub で見る

Two Per Bar 戦略

概要

MetaTrader のオリジナルエキスパート "Two PerBar" は、新しいバーの開始時にロングとショートのポジションを開き、次のバーでバスケット全体をクローズし、オプションでマルチンゲール的なボリューム乗数を適用します。StockSharp ポートは、両方のヘッジされた脚を明示的に追跡し、完了したローソク足ごとに一度反応することで同じリズムを維持します。すべての注文は高レベル API を通じて作成され、インストゥルメントのメタデータ (価格ステップ、ボリュームステップ、最小/最大ロット制限) を尊重します。

取引サイクル

  1. 新しいローソク足の検出 – 戦略は SubscribeCandles を通じて設定されたローソク足シリーズをサブスクライブします。State == CandleStates.Finished でローソク足が到着すると、新しいバーが始まりサイクルが実行されます。
  2. テイクプロフィットのヒットを評価 – 保存された各脚は独自のエントリー価格とテイクプロフィットレベルを保持します。完了したローソク足の高値または安値がそのレベルに触れると、脚は直ちに成行注文でクローズされ、追跡リストから削除されます。
  3. 残りの強制清算 – テイクプロフィットのスキャンを生き残った脚は、次のペアを開く前に市場で清算されます。これは MetaTrader コードが各バーオープン時に PositionClose を呼び出すことを反映します。
  4. 次のロットサイズを決定
    • 前のサイクルにまだ開いた脚があった場合、それらの中で最大のボリュームを VolumeMultiplier で掛け合わせます。
    • バスケットがフラットに終わった場合 (例えば、両脚がテイクプロフィットに達した場合)、サイクルは InitialVolume にリセットされます。
    • PrepareVolume は候補ロットを 2 小数点以下に丸め、インストゥルメントの VolumeStep に合わせ、取引所の MinVolume に対して確認し、ユーザー定義の MaxVolume または Security.MaxVolume のいずれかを超えた場合は最終的に InitialVolume にリセットして正規化します。
  5. デフォルト値を更新 – 計算されたロットは _lastCycleVolume 内に保存され、ヘルパーメソッドが同じ量を再利用できるよう Strategy.Volume に書き込まれます。
  6. 新しいヘッジペアを作成BuyMarket(volume) でロング脚を開き、SellMarket(volume) でショート脚を開きます。各脚は完了したローソク足のクローズ価格と絶対テイクプロフィットレベル (entry ± TakeProfitPoints * pointSize) を記憶します。ゼロまたは負の TakeProfitPoints はテイクプロフィットを無効にし、強制清算ステップだけがバスケットをクローズします。

結果は永続的なストラドルです: 各ローソク足はロング+ショートペアで始まり、両方がバー中に利益目標を検査され、次のサイクルの前にすべてがフラットになります。

資金管理と保護

  • マルチンゲール的なスケーリングVolumeMultiplier は MetaTrader の乗数を複製します。強制清算ステップまで脚が生き残ると、次のサイクルは最も重い脚のサイズにこの値を掛けたものを使用します。完了した利益サイクル (テイクプロフィット経由で両脚がクローズ) はロットを InitialVolume にリセットします。
  • ボリューム上限MaxVolume は、乗数がそれを超えた瞬間にロットを InitialVolume に戻す厳格な上限です。インストゥルメントがより厳しい Security.MaxVolume を報告した場合も同じリセットが発生します。
  • 取引所コンプライアンス – すべてのボリュームは VolumeStep に合わせられ、MinVolume を下回ると拒否されます。InitialVolume を取引可能なサイズに設定することで、リセットパスが常に有効であることが保証されます。
  • ポイント計算 – テイクプロフィットオフセットは Security.PriceStep (または MinPriceStep をフォールバックとして) を使用します。ステップが定義されていないインストゥルメントは、計算されたオフセットがゼロになるためテイクプロフィットを事実上無効にします。

パラメーター

名前 デフォルト 説明
CandleType DataType 1 分足の時間軸 バーごとのワークフローをトリガーするプライマリの時間軸。
InitialVolume decimal 1 生き残った脚がない新しいサイクルを開始する際に使用するロットサイズ。
VolumeMultiplier decimal 2 前のサイクルから生き残った最大の脚に適用される乗数。
MaxVolume decimal 10 InitialVolume にリセットする前に許可される最大ロットサイズ。
TakeProfitPoints int 50 脚ごとのテイクプロフィット目標を構築するために使用する価格ポイント単位の距離。0 はテイクプロフィットを無効にし、バークローズ清算のみに依存します。

実装上の注意と違い

  • ヘッジされた脚は _legs 内で手動で追跡されるため、StockSharp がネットポジションのみを報告しても、戦略は個々のロング/ショートエクスポージャーについて推論できます。
  • 個々のティックに依存する代わりに、テイクプロフィットロジックは完了したローソク足の高値/安値の範囲をチェックします。これにより、元の「バーごと」の動作に忠実でありながら、実装が確定的に保たれます。
  • MetaTrader のスリッページとマジックナンバーの設定は公開されていません。StockSharp が注文ルーティングの詳細を処理し、戦略は親戦略インスタンスに関連するポートフォリオで実行されます。
  • 注文配置は Strategy ヘルパーメソッド (BuyMarketSellMarket) を使用し、インジケーターを Strategy.Indicators に直接追加しないことで、リポジトリのガイドラインに準拠しています。

使用上のヒント

  • 戦略を開始する前に InitialVolume をインストゥルメントのロットステップに合わせてください。コンストラクターは入力を自動的に丸めようとしません。
  • インストゥルメントの価格ステップが非常に小さい場合は、TakeProfitPoints を減らすことを検討してください。そうでなければ、計算されたテイクプロフィットが非現実的に遠くに置かれる可能性があります。
  • 戦略は同時に反対方向の注文を開くため、ヘッジポジションを許可するコネクター/取引所で実行してください。ポジションを即座にネッティングする環境では、_legs リストはまだ意図したロジックを反映しますが、実際のブローカーの動作は異なる場合があります。
  • チャートに戦略を追加してローソク足と実行された取引を視覚化してください (DrawCandles + DrawOwnTradesOnStarted で有効になっています)。
namespace StockSharp.Samples.Strategies;

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;

/// <summary>
/// Strategy that opens a hedged pair of market orders on every new bar.
/// </summary>
public class TwoPerBarStrategy : Strategy
{
	private sealed class HedgeLeg
	{
		public bool IsLong;
		public decimal Volume;
		public decimal EntryPrice;
		public decimal? TakeProfitPrice;
	}

	private readonly List<HedgeLeg> _legs = new();

	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _initialVolume;
	private readonly StrategyParam<decimal> _volumeMultiplier;
	private readonly StrategyParam<decimal> _maxVolume;
	private readonly StrategyParam<int> _takeProfitPoints;

	private decimal _pointSize;
	private decimal _lastCycleVolume;

	/// <summary>
	/// Initializes a new instance of <see cref="TwoPerBarStrategy"/>.
	/// </summary>
	public TwoPerBarStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(1).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe used to detect new bars.", "General");

		_initialVolume = Param(nameof(InitialVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Initial Volume", "Lot size used when no previous positions exist.", "Trading")
			;

		_volumeMultiplier = Param(nameof(VolumeMultiplier), 2m)
			.SetGreaterThanZero()
			.SetDisplay("Volume Multiplier", "Factor applied to the heaviest remaining leg after closing a cycle.", "Trading")
			;

		_maxVolume = Param(nameof(MaxVolume), 10m)
			.SetGreaterThanZero()
			.SetDisplay("Maximum Volume", "Upper limit for the calculated lot size before resetting to the initial value.", "Risk")
			;

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 50)
			.SetNotNegative()
			.SetDisplay("Take Profit (points)", "Distance to the take profit expressed in instrument points.", "Risk")
			;
	}

	/// <summary>
	/// Candle type that drives the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Base lot size for a fresh cycle.
	/// </summary>
	public decimal InitialVolume
	{
		get => _initialVolume.Value;
		set => _initialVolume.Value = value;
	}

	/// <summary>
	/// Multiplier applied to the previous maximum lot size.
	/// </summary>
	public decimal VolumeMultiplier
	{
		get => _volumeMultiplier.Value;
		set => _volumeMultiplier.Value = value;
	}

	/// <summary>
	/// Hard limit for the calculated lot size.
	/// </summary>
	public decimal MaxVolume
	{
		get => _maxVolume.Value;
		set => _maxVolume.Value = value;
	}

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

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

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

		_legs.Clear();
		_lastCycleVolume = 0m;
	}

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

		_pointSize = CalculatePointSize();
		_lastCycleVolume = PrepareVolume(InitialVolume);

		if (_lastCycleVolume > 0m)
		Volume = _lastCycleVolume;

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

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

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

		CheckTakeProfitHits(candle);

		var hadLegs = _legs.Count > 0;
		var maxVolume = 0m;

		for (var i = 0; i < _legs.Count; i++)
		{
		var leg = _legs[i];

		if (leg.Volume > maxVolume)
		maxVolume = leg.Volume;
		}

		if (_legs.Count > 0)
		CloseAllLegs();

		var nextVolume = hadLegs ? maxVolume * VolumeMultiplier : InitialVolume;
		nextVolume = PrepareVolume(nextVolume);

		if (nextVolume <= 0m)
		return;

		_lastCycleVolume = nextVolume;
		Volume = nextVolume;

		if (!IsFormedAndOnlineAndAllowTrading())
		return;

		var offset = TakeProfitPoints > 0 && _pointSize > 0m ? TakeProfitPoints * _pointSize : 0m;
		OpenHedgePair(candle.ClosePrice, offset);
	}

	private void CheckTakeProfitHits(ICandleMessage candle)
	{
		if (TakeProfitPoints <= 0)
		return;

		for (var i = _legs.Count - 1; i >= 0; i--)
		{
		var leg = _legs[i];
		var target = leg.TakeProfitPrice;

		if (target is null)
		continue;

		if (leg.IsLong)
		{
		if (candle.HighPrice >= target.Value)
		{
		SellMarket(leg.Volume);
		_legs.RemoveAt(i);
		}
		}
		else
		{
		if (candle.LowPrice <= target.Value)
		{
		BuyMarket(leg.Volume);
		_legs.RemoveAt(i);
		}
		}
		}
	}

	private void CloseAllLegs()
	{
		for (var i = _legs.Count - 1; i >= 0; i--)
		{
		var leg = _legs[i];

		if (leg.IsLong)
		SellMarket(leg.Volume);
		else
		BuyMarket(leg.Volume);
		}

		_legs.Clear();
	}

	private void OpenHedgePair(decimal entryPrice, decimal takeProfitOffset)
	{
		var volume = _lastCycleVolume;
		if (volume <= 0m)
		return;

		var longOrder = BuyMarket(volume);
		if (longOrder is not null)
		{
		_legs.Add(new HedgeLeg
		{
		IsLong = true,
		Volume = volume,
		EntryPrice = entryPrice,
		TakeProfitPrice = takeProfitOffset > 0m ? entryPrice + takeProfitOffset : null
		});
		}

		var shortOrder = SellMarket(volume);
		if (shortOrder is not null)
		{
		_legs.Add(new HedgeLeg
		{
		IsLong = false,
		Volume = volume,
		EntryPrice = entryPrice,
		TakeProfitPrice = takeProfitOffset > 0m ? entryPrice - takeProfitOffset : null
		});
		}
	}

	private decimal PrepareVolume(decimal candidate)
	{
		if (candidate <= 0m)
		return 0m;

		if (ShouldResetVolume(candidate))
		candidate = InitialVolume;

		var normalized = NormalizeVolume(candidate);

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

		if (ShouldResetVolume(normalized))
		normalized = NormalizeVolume(InitialVolume);

		return normalized;
	}

	private bool ShouldResetVolume(decimal volume)
	{
		if (volume <= 0m)
		return false;

		if (MaxVolume > 0m && volume > MaxVolume)
		return true;

		var security = Security;
		var maxFromSecurity = security?.MaxVolume;

		return maxFromSecurity != null && volume > maxFromSecurity.Value;
	}

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

		var normalized = decimal.Round(volume, 2, MidpointRounding.ToZero);

		var security = Security;
		if (security != null)
		{
		var step = security.VolumeStep ?? 0m;
		if (step > 0m)
		normalized = step * Math.Floor(normalized / step);

		var min = security.MinVolume ?? 0m;
		if (min > 0m && normalized < min)
		return 0m;

		var max = security.MaxVolume;
		if (max != null && normalized > max.Value)
		normalized = max.Value;
		}

		return normalized > 0m ? normalized : 0m;
	}

	private decimal CalculatePointSize()
	{
		var security = Security;
		if (security?.PriceStep is decimal step && step > 0m)
		return step;

		return 0m;
	}
}