在 GitHub 上查看

CCFp 货币强弱策略

概述

该策略把经典的 MetaTrader CCFp 专家顾问迁移到 StockSharp 的高级 API。它使用七个与美元相关的主要货币对(EURUSD、GBPUSD、AUDUSD、NZDUSD、USDCAD、USDCHF、USDJPY)的快慢简单移动平均线之比,计算八种主要货币(USD、EUR、GBP、CHF、JPY、AUD、CAD、NZD)的相对强弱。当两种货币的强弱差值向上突破设定阈值时,策略会建立多空组合,买入更强的货币、卖出更弱的货币。

实现遵循高层框架的最佳实践:每个品种都有单独的蜡烛订阅,指标通过 Bind 连接,订单通过 RegisterOrder 以市价形式提交。成交评论沿用原策略的 (TOPDOWN) 格式,方便对比历史记录。

必需的交易品种

启动前请在参数中指定以下证券:

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

所有品种应使用同一时间框架,并通过参数 Candle Type 统一设置。

参数说明

参数 含义
Fast MA 快速移动平均线周期。
Slow MA 慢速移动平均线周期。
Strength Step 触发新信号所需的最小强弱差值。
Close Opposite 若为 true,先平掉反向仓位,再开新仓。
Candle Type 指标使用的蜡烛类型与时间框架。
基础 Volume 使用 Strategy.Volume 作为每笔市价单的数量。

交易流程

  1. 对七个美元货币对分别订阅蜡烛并计算一组快慢简单移动平均线。
  2. 每当收到完成的蜡烛,就把快慢均线的比例转换为与原 CCFp 指标相同的货币强弱值。
  3. 当所有货币对更新完毕后,重新计算八种货币的强弱分数。
  4. 如果“强势”货币与“弱势”货币之间的差值向上突破 Strength Step,且强势货币继续走强、弱势货币继续走弱,则视为有效信号。
  5. 策略根据信号提交市价单:
    • 若 USD 是强势货币,仅在对应货币对上做反向交易(例如做空 EURUSD)。
    • 若 USD 是弱势货币,则买入以强势货币为基准的货币对(例如做多 EURUSD)。
    • 若两种货币都不是 USD,则同时做多强势货币对 USD、做空弱势货币对 USD。
  6. 如果 Close Opposite 启用且目标品种存在反向持仓,会先发送市价单平仓,然后再建仓。

风险控制

  • 策略不会自动设置止损或止盈;风险管理依赖 Close Opposite 选项和外部的资金管理工具。
  • 建仓数量由 Volume 属性控制,请根据账户规模和承受能力自行设定。

与原版 MQL 策略的差异

  • 货币强弱计算使用单一时间框架上的 SimpleMovingAverage 指标。通过调整 Fast MA 与 Slow MA 可以模拟原指标的多时间框架叠加效果。
  • 没有实现自动拖动止损,重点还原的是入场/出场逻辑,高级风控留给 StockSharp 投资组合层处理。
  • 订单通过 RegisterOrder 提交,使用 StockSharp 的 Security 对象,而不是 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; }
	}
}