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()