GitHub で見る

先物ポートフォリオ管理・満期制御戦略

概要

この戦略は、MetaTrader 5のエキスパートアドバイザー Futures Portfolio Control Expiration をStockSharpの高レベルAPI上に再構築したものです。3本の先物ポートフォリオを維持し、各レッグの希望するロング/ショートエクスポージャーを保ちながら、残存期間が設定可能なしきい値を下回った際に各コントラクトを次の限月へ自動ロールします。

実装はオリジナルのワークフローを再現します:

  1. 短いコード(例:MXIBR)を基に各先物ファミリーの現在取引可能なコントラクトを特定する。
  2. 実際のポートフォリオ数量が設定されたロット値と一致するようにポジションを開く、または調整する(正 = ロング、負 = ショート)。
  3. ハートビートサブスクリプションの完了した各ローソク足で満期時間を監視する。
  4. 満期になるコントラクトをクローズし、同じファミリーの次の限月を探し、新しいコントラクトで目標エクスポージャーを再構築する。

パラメーター

名前 説明 デフォルト
BoardCode 先物識別子に付加される取引所ボード(例:FORTS)。プロバイダーがボードサフィックスを必要としない場合は空のままにする。 FORTS
Symbol1, Symbol2, Symbol3 3つの先物ファミリーの短いコード。戦略は CODE-M.YY のような識別子を構築して先物限月を反復します。 MXI, BR, SBRF
Lot1, Lot2, Lot3 レッグごとの目標ポジションサイズ。正の値はロングエクスポージャー、負の値はショートエクスポージャーを作成します。 -4, -1, 5
HoursBeforeExpiration ロールを開始すべきコントラクト満期前の時間数。 25
MonitoringCandleType 満期チェックをトリガーするためのハートビートとしてのみ使用されるローソク足タイプ(例:1時間ローソク足)。 1H 時間軸

ロールとポジション管理

  • コントラクトの探索。 各レッグについて、戦略は最大12か月の連続したカレンダー月をスキャンします。複数の識別子フォーマット(CODE-M.YYCODE-MM.YYCODEMMYYCODEMYY)を試み、設定された BoardCode をオプションで付加します。参照時刻より後の満期日を持つ証券のみが対象となります。
  • ハートビート更新。 各アクティブコントラクトのローソク足サブスクリプションは、満期タイマーを再評価してポートフォリオエクスポージャーを同期する完了ローソク足コールバックを提供します。
  • ロールロジック。 残存期間が HoursBeforeExpiration 以下になると、戦略は現在のコントラクトのオープンポジションをクローズし、より後の満期を持つ次の先物を見つけ、ハートビートローソク足に再サブスクライブし、新しいコントラクトで目標ロットを復元します。
  • ポジション同期。 各ハートビート後、実際のポジションは目標ロットと比較されます。戦略はライブポジションが常に要求されたボリューム(ゼロを含む)と一致するよう成行注文でエクスポージャーを増減します。

使用上の注意

  1. 選択されたファミリーのすべての先物シンボルを SecurityProvider が把握していることを確認してください。データソースが Si-9.23@FORTS のような識別子を必要とする場合は BoardCode を設定してください。
  2. 希望するポートフォリオパラメーターで戦略を起動してください。ポジションは戦略がオンラインで取引が許可されている場合にのみ開かれます。
  3. 戦略はすべての割り当て、調整、ロールイベントを記録します。これらのメッセージを使用して短いコードと実際の先物のマッピングを確認してください。
  4. ハートビートサブスクリプションは単なるタイマーなので、取引する銘柄で一貫して利用可能な任意のローソク足タイプを選べます。

実装の詳細

  • 高レベルAPIコンポーネント(SubscribeCandlesStrategyParamBuyMarket/SellMarket)によりコードが簡潔に保たれ、プロジェクトのガイドラインに準拠しています。
  • 歴史的データのカスタムコレクションは保存されません。戦略は最新のローソク足イベントとポジション状態のみで動作します。
  • コード内の英語のコメントは、保守を容易にするためにすべての重要なステップを説明しています。
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>
/// Monitors position and rebalances to maintain a target exposure.
/// Simplified from the multi-leg futures portfolio controller to single security.
/// </summary>
public class FuturesPortfolioControlExpirationStrategy : Strategy
{
	private readonly StrategyParam<int> _targetPosition;
	private readonly StrategyParam<int> _rebalancePeriod;
	private readonly StrategyParam<DataType> _candleType;

	private SimpleMovingAverage _sma;
	private int _barCount;

	/// <summary>
	/// Target position size. Positive for long, negative for short.
	/// </summary>
	public int TargetPosition
	{
		get => _targetPosition.Value;
		set => _targetPosition.Value = value;
	}

	/// <summary>
	/// Number of bars between rebalance checks.
	/// </summary>
	public int RebalancePeriod
	{
		get => _rebalancePeriod.Value;
		set => _rebalancePeriod.Value = value;
	}

	/// <summary>
	/// Candle type used as heartbeat for monitoring.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes strategy parameters.
	/// </summary>
	public FuturesPortfolioControlExpirationStrategy()
	{
		_targetPosition = Param(nameof(TargetPosition), 1)
			.SetDisplay("Target Position", "Desired position size (positive=long, negative=short)", "Portfolio");

		_rebalancePeriod = Param(nameof(RebalancePeriod), 10)
			.SetGreaterThanZero()
			.SetDisplay("Rebalance Period", "Number of bars between rebalance checks", "General");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Candle series for monitoring", "General");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_sma = null;
		_barCount = 0;
	}

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

		_sma = new SimpleMovingAverage { Length = 20 };

		SubscribeCandles(CandleType)
			.Bind(_sma, ProcessCandle)
			.Start();
	}

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

		if (!IsFormed)
			return;

		_barCount++;

		var price = candle.ClosePrice;
		var target = (decimal)TargetPosition;

		// Rebalance: ensure position matches target
		if (_barCount % RebalancePeriod == 0)
		{
			var current = Position;
			var diff = target - current;

			if (diff > 0)
				BuyMarket(Math.Abs(diff));
			else if (diff < 0)
				SellMarket(Math.Abs(diff));
		}

		// Trend reversal exit and re-entry
		if (Position > 0 && price < smaValue)
		{
			SellMarket(Math.Abs(Position));
		}
		else if (Position < 0 && price > smaValue)
		{
			BuyMarket(Math.Abs(Position));
		}
		else if (Position == 0)
		{
			if (target > 0 && price > smaValue)
				BuyMarket(Math.Abs(target));
			else if (target < 0 && price < smaValue)
				SellMarket(Math.Abs(target));
			else if (target > 0)
				BuyMarket(Math.Abs(target));
			else if (target < 0)
				SellMarket(Math.Abs(target));
		}
	}
}