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バージョンと同じ簿記スタイルを維持します。
必要なインストゥルメント
戦略パラメーターに以下の証券を添付します:
EURUSDGBPUSDAUDUSDNZDUSDUSDCADUSDCHFUSDJPY
7つのペアはすべてCandle Typeパラメーターを通じて設定された同じ時間軸を共有する必要があります。
パラメーター
| パラメーター | 説明 |
|---|---|
Fast MA |
強度計算に使用される速い移動平均期間。 |
Slow MA |
強度計算に使用される遅い移動平均期間。 |
Strength Step |
新しいシグナルをトリガーするために超えなければならない2つの通貨間の最小差。 |
Close Opposite |
有効にすると、戦略は新しい注文を送る前に反対のポジションをクローズします。 |
Candle Type |
インジケーターによって処理されるローソク足シリーズ。 |
基本Volume |
標準のStrategy.Volumeプロパティから取得され、送信された各成行注文に使用されます。 |
取引ロジック
- 7つのUSD主要ペアのそれぞれは独自の単純移動平均ペア(速い・遅い)でサブスクライブされます。
- 完了したローソク足が届くたびに、戦略は遅い・速い平均の比率をオリジナルのCCFpインジケーターによって生成されるのと同じ合成強度値に変換します。
- 7つのペアすべてが更新された後、8つの通貨強度スコアが再計算されます。
- 「top」通貨と「down」通貨の差が
Strength Stepレベルを上方に超え、top通貨が上昇しdown通貨が下落している場合、機会が検出されます。 - 戦略は強い通貨へのロングエクスポージャーと弱い通貨へのショートエクスポージャーを表現する成行注文を開設します:
- USDが強い通貨の場合、相手ペアに1つの注文のみが置かれます(例:short
EURUSD)。 - USDが弱い通貨の場合、戦略は強い通貨が基軸通貨であるペアを買います(例:long
EURUSD)。 - 両方の通貨が非USDの場合、戦略は2つの注文を送ります:top通貨のUSDに対するロングとdown通貨のUSDに対するショート。
- USDが強い通貨の場合、相手ペアに1つの注文のみが置かれます(例:short
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; }
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
clr.AddReference("StockSharp.BusinessEntities")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates, Sides, OrderTypes
from StockSharp.Algo.Indicators import SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
from StockSharp.BusinessEntities import Security, Order, EntitiesExtensions
CURRENCIES = ["USD", "EUR", "GBP", "CHF", "JPY", "AUD", "CAD", "NZD"]
class ccfp_currency_strength_strategy(Strategy):
def __init__(self):
super(ccfp_currency_strength_strategy, self).__init__()
self._ids = {
"EURUSD": self.Param("EURUSD", "EURUSD").SetDisplay("EURUSD", "EUR/USD security id.", "Securities"),
"GBPUSD": self.Param("GBPUSD", "GBPUSD").SetDisplay("GBPUSD", "GBP/USD security id.", "Securities"),
"AUDUSD": self.Param("AUDUSD", "AUDUSD").SetDisplay("AUDUSD", "AUD/USD security id.", "Securities"),
"NZDUSD": self.Param("NZDUSD", "NZDUSD").SetDisplay("NZDUSD", "NZD/USD security id.", "Securities"),
"USDCAD": self.Param("USDCAD", "USDCAD").SetDisplay("USDCAD", "USD/CAD security id.", "Securities"),
"USDCHF": self.Param("USDCHF", "USDCHF").SetDisplay("USDCHF", "USD/CHF security id.", "Securities"),
"USDJPY": self.Param("USDJPY", "USDJPY").SetDisplay("USDJPY", "USD/JPY security id.", "Securities"),
}
self._fast_ma = self.Param("FastMa", 5).SetGreaterThanZero().SetDisplay("Fast MA", "Fast SMA period.", "Indicators")
self._slow_ma = self.Param("SlowMa", 20).SetGreaterThanZero().SetDisplay("Slow MA", "Slow SMA period.", "Indicators")
self._strength_step = self.Param("StrengthStep", 0.001).SetGreaterThanZero().SetDisplay("Strength Step", "Minimum top/down strength spread.", "Signal")
self._close_opposite = self.Param("CloseOpposite", True).SetDisplay("Close Opposite", "Close opposite exposure before entering.", "Trading")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))).SetDisplay("Candle Type", "Common timeframe.", "General")
self._pairs = {}
self._previous_strengths = {}
self._last_evaluation_time = None
def _definitions(self):
return [
("EURUSD", str(self._ids["EURUSD"].Value), "EUR", "USD"),
("GBPUSD", str(self._ids["GBPUSD"].Value), "GBP", "USD"),
("AUDUSD", str(self._ids["AUDUSD"].Value), "AUD", "USD"),
("NZDUSD", str(self._ids["NZDUSD"].Value), "NZD", "USD"),
("USDCAD", str(self._ids["USDCAD"].Value), "USD", "CAD"),
("USDCHF", str(self._ids["USDCHF"].Value), "USD", "CHF"),
("USDJPY", str(self._ids["USDJPY"].Value), "USD", "JPY"),
]
def _security(self, sec_id):
sec = EntitiesExtensions.LookupById(self, sec_id)
if sec is None:
sec = Security()
sec.Id = sec_id
return sec
def GetWorkingSecurities(self):
return [(self._security(sec_id), self._candle_type.Value) for _, sec_id, _, _ in self._definitions()]
def OnReseted(self):
super(ccfp_currency_strength_strategy, self).OnReseted()
self._pairs = {}
self._previous_strengths = {}
self._last_evaluation_time = None
def OnStarted2(self, time):
super(ccfp_currency_strength_strategy, self).OnStarted2(time)
for name, sec_id, base, quote in self._definitions():
security = self._security(sec_id)
fast = SimpleMovingAverage()
fast.Length = int(self._fast_ma.Value)
slow = SimpleMovingAverage()
slow.Length = int(self._slow_ma.Value)
state = {"security": security, "base": base, "quote": quote, "ratio": 0.0, "ready": False, "time": None}
self._pairs[name] = state
def make_handler(pair_state, fast_indicator, slow_indicator):
def handler(candle, fast_value, slow_value):
if candle.State != CandleStates.Finished or not fast_indicator.IsFormed or not slow_indicator.IsFormed:
return
fast_v = float(fast_value)
slow_v = float(slow_value)
if fast_v == 0 or slow_v == 0:
return
pair_state["ratio"] = fast_v / slow_v
pair_state["ready"] = True
pair_state["time"] = candle.OpenTime
if not all(p["ready"] for p in self._pairs.values()):
return
times = {p["time"] for p in self._pairs.values()}
if len(times) != 1:
return
current_time = next(iter(times))
if current_time is None or current_time == self._last_evaluation_time:
return
self._last_evaluation_time = current_time
self._evaluate()
return handler
self.SubscribeCandles(self._candle_type.Value, security=security).Bind(
fast, slow, make_handler(state, fast, slow)).Start()
def _evaluate(self):
# Fast/slow ratio of each currency against USD; a cross ratio is the quotient of two of them.
ratios = {"USD": 1.0}
for pair in self._pairs.values():
if pair["quote"] == "USD":
ratios[pair["base"]] = pair["ratio"]
else:
ratios[pair["quote"]] = 1.0 / pair["ratio"]
strengths = {}
for currency in CURRENCIES:
strength = 0.0
for other in CURRENCIES:
if other != currency:
strength += ratios[currency] / ratios[other] - 1.0
strengths[currency] = strength
top = max(strengths, key=strengths.get)
down = min(strengths, key=strengths.get)
spread = strengths[top] - strengths[down]
if len(self._previous_strengths) == len(strengths):
previous_spread = self._previous_strengths[top] - self._previous_strengths[down]
if (previous_spread < float(self._strength_step.Value) <= spread and
strengths[top] > self._previous_strengths[top] and
strengths[down] < self._previous_strengths[down]):
self._trade_spread(top, down)
self._previous_strengths = dict(strengths)
def _trade_spread(self, top, down):
if top == "USD":
self._trade_against_usd(down, False)
elif down == "USD":
self._trade_against_usd(top, True)
else:
self._trade_against_usd(top, True)
self._trade_against_usd(down, False)
def _trade_against_usd(self, currency, want_currency_long):
pair = None
for candidate in self._pairs.values():
if ((candidate["base"] == currency and candidate["quote"] == "USD") or
(candidate["base"] == "USD" and candidate["quote"] == currency)):
pair = candidate
break
if pair is None:
return
currency_is_base = pair["base"] == currency
buy_pair = want_currency_long == currency_is_base
self._submit(pair["security"], Sides.Buy if buy_pair else Sides.Sell)
def _submit(self, security, side):
position_value = self.GetPositionValue(security, self.Portfolio)
position = float(position_value) if position_value is not None else 0.0
if (side == Sides.Buy and position > 0) or (side == Sides.Sell and position < 0):
return
if position != 0 and bool(self._close_opposite.Value):
self._register_market(security, side, abs(position))
self._register_market(security, side, float(self.Volume))
def _register_market(self, security, side, volume):
order = Order()
order.Security = security
order.Portfolio = self.Portfolio
order.Type = OrderTypes.Market
order.Side = side
order.Volume = volume
order.Comment = "(TOPDOWN)"
self.RegisterOrder(order)
def CreateClone(self):
return ccfp_currency_strength_strategy()