Ver no GitHub

Estratégia de Força de Divisas CCFp

Visão geral

Esta estratégia porta o clássico consultor especialista CCFp do MetaTrader para a API de alto nível do StockSharp. Ela calcula uma pontuação de força relativa para as oito principais divisas (USD, EUR, GBP, CHF, JPY, AUD, CAD, NZD) usando razões entre médias móviles simples rápidas e lentas nos sete pares principais baseados em USD (EURUSD, GBPUSD, AUDUSD, NZDUSD, USDCAD, USDCHF, USDJPY). Quando a diferença entre duas forças de divisas ultrapassa um limiar configurável, a estratégia abre posições de mercado que expressam a divisa mais forte contra a mais fraca.

A implementação segue a arquitetura de alto nível recomendada: cada instrumento tem sua própria assinatura de candles, os indicadores são vinculados via Bind, e o gerenciamento de ordens usa RegisterOrder com ordens de mercado. Os comentários nas ordens executadas reutilizam o formato (TOPDOWN) original para manter o mesmo estilo de contabilidade que a versão MQL.

Instrumentos necessários

Anexar os seguintes valores aos parâmetros da estratégia:

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

Todos os sete pares devem compartilhar o mesmo período definido através do parâmetro Candle Type.

Parâmetros

Parâmetro Descrição
Fast MA Período de média móvel rápida usado no cálculo de força.
Slow MA Período de média móvel lenta usado no cálculo de força.
Strength Step Diferença mínima entre duas divisas que deve ser excedida para acionar um novo sinal.
Close Opposite Se habilitado, a estratégia fecha posições opostas antes de enviar uma nova ordem.
Candle Type Série de candles processada pelos indicadores.
Volume base Tomado da propriedade padrão Strategy.Volume e usado para cada ordem de mercado enviada.

Lógica de trading

  1. Cada um dos sete pares principais USD é assinado com seu próprio par de médias móviles simples (rápida e lenta).
  2. Cada vez que um candle concluído chega, a estratégia converte a razão das médias lenta e rápida nos mesmos valores de força sintéticos produzidos pelo indicador CCFp original.
  3. Depois de todos os sete pares serem atualizados, as oito pontuações de força de divisas são recalculadas.
  4. Quando a diferença entre uma divisa "top" e uma "down" cruza para cima o nível Strength Step, enquanto a divisa top está subindo e a divisa down está caindo, uma oportunidade é detectada.
  5. A estratégia abre ordens de mercado que expressam exposição comprada à divisa forte e exposição vendida à divisa fraca:
    • Se USD é a divisa forte, apenas uma ordem é colocada no par contraparte (por exemplo, short EURUSD).
    • Se USD é a divisa fraca, a estratégia compra o par onde a divisa forte é a base (por exemplo, long EURUSD).
    • Quando ambas as divisas são não-USD, a estratégia envia duas ordens: long a divisa top contra USD e short a divisa down contra USD.
  6. Se Close Opposite está habilitado e uma posição oposta ainda está aberta em um par alvo, a estratégia envia primeiro uma ordem de mercado de fechamento antes de entrar em um novo trade.

Gestão de risco

  • A estratégia não anexa ordens explícitas de stop-loss ou take-profit; o controle de risco é tratado pelo flag Close Opposite junto com ferramentas de gerenciamento de portfólio manual.
  • O tamanho de entrada é controlado pela propriedade Volume. Configurá-la de acordo com o tamanho da conta e a exposição desejada por segmento.

Diferenças em relação à implementação MQL original

  • O cálculo de força de divisas usa indicadores SimpleMovingAverage do StockSharp em um único período. O empilhamento de coeficientes de múltiplos períodos do indicador MQL pode ser emulado ajustando os períodos Fast MA e Slow MA.
  • Os stops protetores não são trailing automaticamente; em vez disso, a estratégia se concentra em reproduzir a lógica de entrada/saída e deixa o controle avançado de risco para a camada de portfólio do StockSharp.
  • O roteamento de ordens usa o auxiliar de alto nível RegisterOrder e as referências de segurança do StockSharp em vez de objetos de trade do MetaTrader.
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; }
	}
}