GitHub で見る

CCFp通貨強弱戦略

概要

この戦略はクラシックなMetaTrader CCFpエキスパートアドバイザーをStockSharpの高レベルAPIにポートします。7つのUSDベースの主要通貨ペア(EURUSD、GBPUSD、AUDUSD、NZDUSD、USDCAD、USDCHF、USDJPY)の速い・遅い単純移動平均の比率を使用して、8つの主要通貨(USD、EUR、GBP、CHF、JPY、AUD、CAD、NZD)の相対的な強さスコアを計算します。2つの通貨強度の差が設定可能なしきい値を上回ると、戦略は弱い通貨に対して強い通貨を表現する成行ポジションを開設します。

実装は推奨された高レベルアーキテクチャに従っています:各インストゥルメントには独自のローソク足サブスクリプションがあり、インジケーターはBindを通じてバインドされ、注文管理は成行注文でRegisterOrderを使用します。実行された注文のコメントは元の(TOPDOWN)フォーマットを再利用して、MQLバージョンと同じ簿記スタイルを維持します。

必要なインストゥルメント

戦略パラメーターに以下の証券を添付します:

  • EURUSD
  • GBPUSD
  • AUDUSD
  • NZDUSD
  • USDCAD
  • USDCHF
  • USDJPY

7つのペアはすべてCandle Typeパラメーターを通じて設定された同じ時間軸を共有する必要があります。

パラメーター

パラメーター 説明
Fast MA 強度計算に使用される速い移動平均期間。
Slow MA 強度計算に使用される遅い移動平均期間。
Strength Step 新しいシグナルをトリガーするために超えなければならない2つの通貨間の最小差。
Close Opposite 有効にすると、戦略は新しい注文を送る前に反対のポジションをクローズします。
Candle Type インジケーターによって処理されるローソク足シリーズ。
基本Volume 標準のStrategy.Volumeプロパティから取得され、送信された各成行注文に使用されます。

取引ロジック

  1. 7つのUSD主要ペアのそれぞれは独自の単純移動平均ペア(速い・遅い)でサブスクライブされます。
  2. 完了したローソク足が届くたびに、戦略は遅い・速い平均の比率をオリジナルのCCFpインジケーターによって生成されるのと同じ合成強度値に変換します。
  3. 7つのペアすべてが更新された後、8つの通貨強度スコアが再計算されます。
  4. 「top」通貨と「down」通貨の差がStrength Stepレベルを上方に超え、top通貨が上昇しdown通貨が下落している場合、機会が検出されます。
  5. 戦略は強い通貨へのロングエクスポージャーと弱い通貨へのショートエクスポージャーを表現する成行注文を開設します:
    • USDが強い通貨の場合、相手ペアに1つの注文のみが置かれます(例:short EURUSD)。
    • USDが弱い通貨の場合、戦略は強い通貨が基軸通貨であるペアを買います(例:long EURUSD)。
    • 両方の通貨が非USDの場合、戦略は2つの注文を送ります:top通貨のUSDに対するロングとdown通貨のUSDに対するショート。
  6. Close Oppositeが有効で、ターゲットペアにまだ反対のポジションが開いている場合、戦略は新しいトレードに入る前にまず成行クローズ注文を送ります。

リスク管理

  • 戦略は明示的なストップロスまたはテイクプロフィット注文を添付しません;リスク管理はClose Oppositeフラグと手動ポートフォリオ管理ツールによって処理されます。
  • エントリーサイズはVolumeプロパティによって制御されます。口座サイズと各セグメントの希望するエクスポージャーに応じて設定します。

オリジナルMQL実装との違い

  • 通貨強度計算は単一の時間軸でStockSharpのSimpleMovingAverageインジケーターを使用します。MQLインジケーターからのマルチタイムフレーム係数スタッキングは、Fast MAとSlow MA期間を調整することでエミュレートできます。
  • 保護ストップは自動的にトレーリングされません;代わりに、戦略はエントリー/エグジットロジックの再現に集中し、高度なリスク管理をStockSharpのポートフォリオ層に任せます。
  • 注文ルーティングはMetaTraderトレードオブジェクトの代わりに高レベルのRegisterOrderヘルパーとStockSharpのセキュリティ参照を使用します。
using System;
using System.Collections.Generic;
using System.Linq;

using Ecng.Common;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// CCFp-style relative currency strength across the seven USD majors.
/// </summary>
public class CcfpCurrencyStrengthStrategy : Strategy
{
	private static readonly string[] _currencies = ["USD", "EUR", "GBP", "CHF", "JPY", "AUD", "CAD", "NZD"];

	private readonly StrategyParam<string> _eurusd;
	private readonly StrategyParam<string> _gbpusd;
	private readonly StrategyParam<string> _audusd;
	private readonly StrategyParam<string> _nzdusd;
	private readonly StrategyParam<string> _usdcad;
	private readonly StrategyParam<string> _usdchf;
	private readonly StrategyParam<string> _usdjpy;
	private readonly StrategyParam<int> _fastMa;
	private readonly StrategyParam<int> _slowMa;
	private readonly StrategyParam<decimal> _strengthStep;
	private readonly StrategyParam<bool> _closeOpposite;
	private readonly StrategyParam<DataType> _candleType;

	private readonly Dictionary<string, PairState> _pairs = new(StringComparer.OrdinalIgnoreCase);
	private readonly Dictionary<string, decimal> _previousStrengths = new(StringComparer.OrdinalIgnoreCase);
	private DateTimeOffset? _lastEvaluationTime;

	public string EURUSD { get => _eurusd.Value; set => _eurusd.Value = value; }
	public string GBPUSD { get => _gbpusd.Value; set => _gbpusd.Value = value; }
	public string AUDUSD { get => _audusd.Value; set => _audusd.Value = value; }
	public string NZDUSD { get => _nzdusd.Value; set => _nzdusd.Value = value; }
	public string USDCAD { get => _usdcad.Value; set => _usdcad.Value = value; }
	public string USDCHF { get => _usdchf.Value; set => _usdchf.Value = value; }
	public string USDJPY { get => _usdjpy.Value; set => _usdjpy.Value = value; }
	public int FastMa { get => _fastMa.Value; set => _fastMa.Value = value; }
	public int SlowMa { get => _slowMa.Value; set => _slowMa.Value = value; }
	public decimal StrengthStep { get => _strengthStep.Value; set => _strengthStep.Value = value; }
	public bool CloseOpposite { get => _closeOpposite.Value; set => _closeOpposite.Value = value; }
	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }

	public CcfpCurrencyStrengthStrategy()
	{
		_eurusd = Param(nameof(EURUSD), "EURUSD").SetDisplay("EURUSD", "EUR/USD security id.", "Securities");
		_gbpusd = Param(nameof(GBPUSD), "GBPUSD").SetDisplay("GBPUSD", "GBP/USD security id.", "Securities");
		_audusd = Param(nameof(AUDUSD), "AUDUSD").SetDisplay("AUDUSD", "AUD/USD security id.", "Securities");
		_nzdusd = Param(nameof(NZDUSD), "NZDUSD").SetDisplay("NZDUSD", "NZD/USD security id.", "Securities");
		_usdcad = Param(nameof(USDCAD), "USDCAD").SetDisplay("USDCAD", "USD/CAD security id.", "Securities");
		_usdchf = Param(nameof(USDCHF), "USDCHF").SetDisplay("USDCHF", "USD/CHF security id.", "Securities");
		_usdjpy = Param(nameof(USDJPY), "USDJPY").SetDisplay("USDJPY", "USD/JPY security id.", "Securities");
		_fastMa = Param(nameof(FastMa), 5).SetGreaterThanZero().SetDisplay("Fast MA", "Fast SMA period.", "Indicators");
		_slowMa = Param(nameof(SlowMa), 20).SetGreaterThanZero().SetDisplay("Slow MA", "Slow SMA period.", "Indicators");
		_strengthStep = Param(nameof(StrengthStep), 0.001m).SetGreaterThanZero().SetDisplay("Strength Step", "Minimum top/down strength spread.", "Signal");
		_closeOpposite = Param(nameof(CloseOpposite), true).SetDisplay("Close Opposite", "Close opposite exposure before entering.", "Trading");
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame()).SetDisplay("Candle Type", "Common timeframe.", "General");
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		foreach (var (_, id, _, _) in PairDefinitions())
			yield return (Resolve(id), CandleType);
	}

	protected override void OnReseted()
	{
		base.OnReseted();
		_pairs.Clear();
		_previousStrengths.Clear();
		_lastEvaluationTime = null;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		foreach (var (name, id, baseCurrency, quoteCurrency) in PairDefinitions())
		{
			var security = Resolve(id);
			var fast = new SimpleMovingAverage { Length = FastMa };
			var slow = new SimpleMovingAverage { Length = SlowMa };
			var state = new PairState(name, security, baseCurrency, quoteCurrency);
			_pairs[name] = state;

			SubscribeCandles(CandleType, security: security)
				.Bind(fast, slow, (candle, fastValue, slowValue) =>
				{
					if (candle.State != CandleStates.Finished || !fast.IsFormed || !slow.IsFormed || fastValue == 0m || slowValue == 0m)
						return;

					state.Ratio = fastValue / slowValue;
					state.HasValue = true;
					state.LastTime = candle.OpenTime;

					if (!_pairs.Values.All(p => p.HasValue))
						return;

					var times = _pairs.Values.Select(p => p.LastTime).Distinct().ToArray();
					if (times.Length != 1 || times[0] is null || times[0] == _lastEvaluationTime)
						return;

					_lastEvaluationTime = times[0];
					Evaluate();
				})
				.Start();
		}
	}

	private void Evaluate()
	{
		// Fast/slow ratio of each currency against USD; a cross ratio is the quotient of two of them.
		var ratios = new Dictionary<string, decimal>(StringComparer.OrdinalIgnoreCase)
		{
			["USD"] = 1m,
		};

		foreach (var pair in _pairs.Values)
		{
			if (pair.QuoteCurrency.EqualsIgnoreCase("USD"))
				ratios[pair.BaseCurrency] = pair.Ratio;
			else
				ratios[pair.QuoteCurrency] = 1m / pair.Ratio;
		}

		var strengths = new Dictionary<string, decimal>(StringComparer.OrdinalIgnoreCase);

		foreach (var currency in _currencies)
		{
			var strength = 0m;

			foreach (var other in _currencies)
			{
				if (!other.EqualsIgnoreCase(currency))
					strength += ratios[currency] / ratios[other] - 1m;
			}

			strengths[currency] = strength;
		}

		var top = strengths.MaxBy(p => p.Value);
		var down = strengths.MinBy(p => p.Value);
		var spread = top.Value - down.Value;

		if (_previousStrengths.Count == strengths.Count)
		{
			var previousSpread = _previousStrengths[top.Key] - _previousStrengths[down.Key];
			var topRising = top.Value > _previousStrengths[top.Key];
			var downFalling = down.Value < _previousStrengths[down.Key];

			if (previousSpread < StrengthStep && spread >= StrengthStep && topRising && downFalling)
				TradeCurrencySpread(top.Key, down.Key);
		}

		_previousStrengths.Clear();
		foreach (var pair in strengths)
			_previousStrengths[pair.Key] = pair.Value;
	}

	private void TradeCurrencySpread(string top, string down)
	{
		if (top.EqualsIgnoreCase("USD"))
			TradeAgainstUsd(down, wantCurrencyLong: false);
		else if (down.EqualsIgnoreCase("USD"))
			TradeAgainstUsd(top, wantCurrencyLong: true);
		else
		{
			TradeAgainstUsd(top, wantCurrencyLong: true);
			TradeAgainstUsd(down, wantCurrencyLong: false);
		}
	}

	private void TradeAgainstUsd(string currency, bool wantCurrencyLong)
	{
		var pair = _pairs.Values.FirstOrDefault(p =>
			(p.BaseCurrency.EqualsIgnoreCase(currency) && p.QuoteCurrency.EqualsIgnoreCase("USD")) ||
			(p.BaseCurrency.EqualsIgnoreCase("USD") && p.QuoteCurrency.EqualsIgnoreCase(currency)));

		if (pair is null)
			return;

		var currencyIsBase = pair.BaseCurrency.EqualsIgnoreCase(currency);
		var buyPair = wantCurrencyLong == currencyIsBase;
		Submit(pair.Security, buyPair ? Sides.Buy : Sides.Sell);
	}

	private void Submit(Security security, Sides side)
	{
		var position = GetPositionValue(security, Portfolio) ?? 0m;

		if (side == Sides.Buy ? position > 0m : position < 0m)
			return;

		if (position != 0m && CloseOpposite)
			RegisterMarket(security, side, Math.Abs(position));

		RegisterMarket(security, side, Volume);
	}

	private void RegisterMarket(Security security, Sides side, decimal volume)
	{
		RegisterOrder(new Order
		{
			Security = security,
			Portfolio = Portfolio,
			Type = OrderTypes.Market,
			Side = side,
			Volume = volume,
			Comment = "(TOPDOWN)",
		});
	}

	private Security Resolve(string id)
		=> this.LookupById(id) ?? new Security { Id = id };

	private IEnumerable<(string Name, string Id, string Base, string Quote)> PairDefinitions()
	{
		yield return ("EURUSD", EURUSD, "EUR", "USD");
		yield return ("GBPUSD", GBPUSD, "GBP", "USD");
		yield return ("AUDUSD", AUDUSD, "AUD", "USD");
		yield return ("NZDUSD", NZDUSD, "NZD", "USD");
		yield return ("USDCAD", USDCAD, "USD", "CAD");
		yield return ("USDCHF", USDCHF, "USD", "CHF");
		yield return ("USDJPY", USDJPY, "USD", "JPY");
	}

	private sealed class PairState(string name, Security security, string baseCurrency, string quoteCurrency)
	{
		public string Name { get; } = name;
		public Security Security { get; } = security;
		public string BaseCurrency { get; } = baseCurrency;
		public string QuoteCurrency { get; } = quoteCurrency;
		public decimal Ratio { get; set; }
		public bool HasValue { get; set; }
		public DateTimeOffset? LastTime { get; set; }
	}
}