Auf GitHub ansehen

CCFp-Währungsstärke-Strategie

Übersicht

Diese Strategie portiert den klassischen MetaTrader CCFp-Expertenberater in die High-Level-API von StockSharp. Sie berechnet eine relative Stärke-Punktzahl für die acht wichtigsten Währungen (USD, EUR, GBP, CHF, JPY, AUD, CAD, NZD) anhand von Verhältnissen zwischen schnellen und langsamen einfachen gleitenden Durchschnitten auf den sieben USD-basierten Hauptpaaren (EURUSD, GBPUSD, AUDUSD, NZDUSD, USDCAD, USDCHF, USDJPY). Wenn die Differenz zwischen zwei Währungsstärken einen konfigurierbaren Schwellenwert überschreitet, öffnet die Strategie Marktpositionen, die die stärkere Währung gegen die schwächere ausdrücken.

Die Implementierung folgt der empfohlenen High-Level-Architektur: Jedes Instrument hat sein eigenes Kerzenabonnement, Indikatoren werden über Bind gebunden, und die Auftragsverwaltung verwendet RegisterOrder mit Marktaufträgen. Kommentare zu ausgeführten Aufträgen verwenden das ursprüngliche (TOPDOWN)-Format, um denselben Buchführungsstil wie die MQL-Version beizubehalten.

Erforderliche Instrumente

Folgende Wertpapiere an die Strategieparameter anhängen:

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

Alle sieben Paare müssen denselben Zeitrahmen teilen, der über den Parameter Candle Type eingestellt wird.

Parameter

Parameter Beschreibung
Fast MA Schnelle Moving-Average-Periode für die Stärkeberechnung.
Slow MA Langsame Moving-Average-Periode für die Stärkeberechnung.
Strength Step Minimale Differenz zwischen zwei Währungen, die überschritten werden muss, um ein neues Signal auszulösen.
Close Opposite Bei Aktivierung schließt die Strategie gegenläufige Positionen vor dem Senden einer neuen Order.
Candle Type Von den Indikatoren verarbeitete Kerzenserie.
Basis-Volume Wird von der Standard-Strategy.Volume-Eigenschaft übernommen und für jeden gesendeten Marktauftrag verwendet.

Handelslogik

  1. Jedes der sieben USD-Hauptpaare wird mit seinem eigenen Paar einfacher gleitender Durchschnitte (schnell und langsam) abonniert.
  2. Jedes Mal, wenn eine abgeschlossene Kerze eintrifft, konvertiert die Strategie das Verhältnis der langsamen und schnellen Durchschnitte in dieselben synthetischen Stärkewerte, die vom ursprünglichen CCFp-Indikator erzeugt werden.
  3. Nachdem alle sieben Paare aktualisiert wurden, werden die acht Währungsstärke-Punktzahlen neu berechnet.
  4. Wenn die Differenz zwischen einer "Top"- und einer "Down"-Währung das Strength Step-Niveau aufwärts kreuzt, während die Top-Währung steigt und die Down-Währung fällt, wird eine Gelegenheit erkannt.
  5. Die Strategie öffnet Marktaufträge, die Long-Exposition zur starken Währung und Short-Exposition zur schwachen Währung ausdrücken:
    • Wenn USD die starke Währung ist, wird nur ein Auftrag auf dem Gegenpaar platziert (zum Beispiel Short EURUSD).
    • Wenn USD die schwache Währung ist, kauft die Strategie das Paar, wo die starke Währung die Basis ist (zum Beispiel Long EURUSD).
    • Wenn beide Währungen Nicht-USD sind, sendet die Strategie zwei Aufträge: Long die Top-Währung gegen USD und Short die Down-Währung gegen USD.
  6. Wenn Close Opposite aktiviert ist und eine gegenläufige Position auf einem Zielpaar noch offen ist, sendet die Strategie zuerst einen schließenden Marktauftrag, bevor sie in einen neuen Trade einsteigt.

Risikomanagement

  • Die Strategie fügt keine expliziten Stop-Loss- oder Take-Profit-Aufträge hinzu; die Risikokontrolle wird durch das Close Opposite-Flag zusammen mit manuellen Portfolio-Verwaltungstools gehandhabt.
  • Die Einstiegsgröße wird durch die Volume-Eigenschaft kontrolliert. Entsprechend der Kontogröße und des gewünschten Exposures pro Segment konfigurieren.

Unterschiede zur ursprünglichen MQL-Implementierung

  • Die Währungsstärkeberechnung verwendet StockSharp-SimpleMovingAverage-Indikatoren auf einem einzigen Zeitrahmen. Multi-Zeitrahmen-Koeffizientenstapelung aus dem MQL-Indikator kann durch Anpassen der Fast MA- und Slow MA-Perioden emuliert werden.
  • Schützende Stops werden nicht automatisch nachgezogen; stattdessen konzentriert sich die Strategie auf die Reproduktion der Einstiegs-/Ausstiegslogik und überlässt die erweiterte Risikokontrolle der Portfolio-Ebene von StockSharp.
  • Die Auftragsweiterleitung verwendet den High-Level-RegisterOrder-Helfer und StockSharp-Sicherheitsreferenzen anstelle von MetaTrader-Handelsobjekten.
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; }
	}
}