diff --git a/FxMacroDataClient.cs b/FxMacroDataClient.cs new file mode 100644 index 0000000..43c01fc --- /dev/null +++ b/FxMacroDataClient.cs @@ -0,0 +1,312 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + */ + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; +using QuantConnect.Configuration; + +namespace QuantConnect.DataSource +{ + /// + /// Raw JSON client for FXMacroData's public read/data endpoints. + /// + public sealed class FxMacroDataClient : IDisposable + { + public const string DefaultBaseUrl = "https://fxmacrodata.com/api/v1"; + + private readonly HttpClient _httpClient; + private readonly bool _ownsClient; + private readonly string _baseUrl; + private readonly string _apiKey; + + public FxMacroDataClient(HttpClient httpClient = null, string baseUrl = DefaultBaseUrl, string apiKey = null) + { + _httpClient = httpClient ?? new HttpClient(); + _ownsClient = httpClient == null; + _baseUrl = baseUrl.EndsWith("/") ? baseUrl.Substring(0, baseUrl.Length - 1) : baseUrl; + _apiKey = string.IsNullOrWhiteSpace(apiKey) ? Config.Get("fxmacrodata-api-key") : apiKey; + } + + public Task GetDataCatalogueAsync(string currency = "usd", bool includeCapabilities = false, + bool includeCoverage = false, string indicator = null) + { + return GetAsync($"data_catalogue/{currency.ToLowerInvariant()}", new Dictionary + { + ["include_capabilities"] = includeCapabilities, + ["include_coverage"] = includeCoverage, + ["indicator"] = indicator + }); + } + + public Task GetAnnouncementsAsync(string currency, string indicator, DateTime? startDate = null, + DateTime? endDate = null, int limit = 20, int offset = 0, string seriesMode = "raw", + string revisions = "latest", bool officialOnly = true) + { + return GetAsync($"announcements/{currency.ToLowerInvariant()}/{indicator}", new Dictionary + { + ["start_date"] = FormatDate(startDate), + ["end_date"] = FormatDate(endDate), + ["series_mode"] = seriesMode, + ["limit"] = limit, + ["offset"] = offset, + ["revisions"] = revisions, + ["official_only"] = officialOnly + }); + } + + public Task GetLatestAnnouncementsAsync(string currency = "usd") + { + return GetAsync($"announcements/{currency.ToLowerInvariant()}/latest"); + } + + public Task GetAnnouncementChangesAsync(string currencies = null, string indicators = null, + string since = null, int limit = 100, string payload = "compact") + { + return GetAsync("announcements/changes", new Dictionary + { + ["currencies"] = currencies, + ["indicators"] = indicators, + ["since"] = since, + ["limit"] = limit, + ["payload"] = payload + }); + } + + public Task GetPredictionsAsync(string currency, string indicator, DateTime? startDate = null, + DateTime? endDate = null, int limit = 20, int offset = 0, string predictionType = null, + string predictionSource = null) + { + return GetAsync($"predictions/{currency.ToLowerInvariant()}/{indicator}", new Dictionary + { + ["prediction_type"] = predictionType, + ["prediction_source"] = predictionSource, + ["start_date"] = FormatDate(startDate), + ["end_date"] = FormatDate(endDate), + ["limit"] = limit, + ["offset"] = offset + }); + } + + public Task GetReleaseCalendarAsync(string currency = "usd", string indicator = null, + DateTime? startDate = null, DateTime? endDate = null, string timezone = null) + { + return GetAsync($"calendar/{currency.ToLowerInvariant()}", new Dictionary + { + ["indicator"] = indicator, + ["start_date"] = FormatDate(startDate), + ["end_date"] = FormatDate(endDate), + ["timezone"] = timezone + }); + } + + public Task GetForexAsync(string baseCurrency, string quoteCurrency, DateTime? startDate = null, + DateTime? endDate = null, int limit = 20, int offset = 0, string indicators = null) + { + return GetAsync($"forex/{baseCurrency.ToLowerInvariant()}/{quoteCurrency.ToLowerInvariant()}", + new Dictionary + { + ["start_date"] = FormatDate(startDate), + ["end_date"] = FormatDate(endDate), + ["limit"] = limit, + ["offset"] = offset, + ["indicators"] = indicators + }); + } + + public Task GetCotAsync(string currency, DateTime? startDate = null, DateTime? endDate = null, + int limit = 20, int offset = 0) + { + return GetAsync($"cot/{currency.ToLowerInvariant()}", new Dictionary + { + ["start_date"] = FormatDate(startDate), + ["end_date"] = FormatDate(endDate), + ["limit"] = limit, + ["offset"] = offset + }); + } + + public Task GetCommodityAsync(string indicator, DateTime? startDate = null, DateTime? endDate = null, + int limit = 20, int offset = 0) + { + return GetAsync($"commodities/{indicator}", new Dictionary + { + ["start_date"] = FormatDate(startDate), + ["end_date"] = FormatDate(endDate), + ["limit"] = limit, + ["offset"] = offset + }); + } + + public Task GetLatestCommoditiesAsync() + { + return GetAsync("commodities/latest"); + } + + public Task GetCurvesAsync(string currency, string curveFamily = "government_nominal", + string metric = "spot", DateTime? date = null) + { + return GetAsync($"curves/{currency.ToLowerInvariant()}", new Dictionary + { + ["curve_family"] = curveFamily, + ["metric"] = metric, + ["date"] = FormatDate(date) + }); + } + + public Task GetCurveProxiesAsync(string currency, string curveFamily = "government_nominal", + DateTime? date = null) + { + return GetAsync($"curve_proxies/{currency.ToLowerInvariant()}", new Dictionary + { + ["curve_family"] = curveFamily, + ["date"] = FormatDate(date) + }); + } + + public Task GetForwardCurvesAsync(string currency, string curveFamily = "government_nominal", + string method = "derived_from_spot_nodes", DateTime? date = null) + { + return GetAsync($"forward_curves/{currency.ToLowerInvariant()}", new Dictionary + { + ["curve_family"] = curveFamily, + ["method"] = method, + ["date"] = FormatDate(date) + }); + } + + public Task GetRateDifferentialsAsync(string baseCurrency, string quoteCurrency, string measure = "auto", + DateTime? startDate = null, DateTime? endDate = null, int limit = 20, int offset = 0) + { + return GetAsync($"rate_differentials/{baseCurrency.ToLowerInvariant()}/{quoteCurrency.ToLowerInvariant()}", + new Dictionary + { + ["measure"] = measure, + ["start_date"] = FormatDate(startDate), + ["end_date"] = FormatDate(endDate), + ["limit"] = limit, + ["offset"] = offset + }); + } + + public Task GetForwardDifferentialsAsync(string baseCurrency, string quoteCurrency, + string curveFamily = "government_nominal", decimal startTenorYears = 2m, decimal endTenorYears = 5m, + DateTime? startDate = null, DateTime? endDate = null, int limit = 20, int offset = 0) + { + return GetAsync($"forward_differentials/{baseCurrency.ToLowerInvariant()}/{quoteCurrency.ToLowerInvariant()}", + new Dictionary + { + ["curve_family"] = curveFamily, + ["start_tenor_years"] = startTenorYears, + ["end_tenor_years"] = endTenorYears, + ["start_date"] = FormatDate(startDate), + ["end_date"] = FormatDate(endDate), + ["limit"] = limit, + ["offset"] = offset + }); + } + + public Task GetMarketSessionsAsync(string at = null) + { + return GetAsync("market_sessions", new Dictionary { ["at"] = at }); + } + + public Task GetRiskSentimentAsync(DateTime? startDate = null, DateTime? endDate = null, + int limit = 20, int offset = 0) + { + return GetAsync("risk_sentiment", new Dictionary + { + ["start_date"] = FormatDate(startDate), + ["end_date"] = FormatDate(endDate), + ["limit"] = limit, + ["offset"] = offset + }); + } + + public Task GetNewsAsync(string currency, int limit = 20, int offset = 0) + { + return GetAsync($"news/{currency.ToLowerInvariant()}", new Dictionary + { + ["limit"] = limit, + ["offset"] = offset + }); + } + + public Task GetPressReleasesAsync(string currency, int limit = 20, int offset = 0) + { + return GetAsync($"press-releases/{currency.ToLowerInvariant()}", new Dictionary + { + ["limit"] = limit, + ["offset"] = offset + }); + } + + public async Task GraphQlAsync(string query, object variables = null) + { + var payload = JsonConvert.SerializeObject(new + { + query, + variables = variables ?? new { } + }); + using var content = new StringContent(payload, Encoding.UTF8, "application/json"); + var response = await _httpClient.PostAsync($"{_baseUrl}/graphql", content).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsStringAsync().ConfigureAwait(false); + } + + public void Dispose() + { + if (_ownsClient) + { + _httpClient.Dispose(); + } + } + + private Task GetAsync(string path, IDictionary query = null) + { + return _httpClient.GetStringAsync(BuildUri(path, query)); + } + + private Uri BuildUri(string path, IDictionary query) + { + var parameters = new Dictionary(query ?? new Dictionary()); + if (!string.IsNullOrWhiteSpace(_apiKey)) + { + parameters["api_key"] = _apiKey; + } + var url = $"{_baseUrl}/{path.TrimStart('/')}"; + var queryString = string.Join("&", parameters + .Where(pair => pair.Value != null) + .Select(pair => $"{Uri.EscapeDataString(pair.Key)}={Uri.EscapeDataString(FormatValue(pair.Value))}")); + return new Uri(string.IsNullOrWhiteSpace(queryString) ? url : $"{url}?{queryString}"); + } + + private static string FormatDate(DateTime? value) + { + return value?.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); + } + + private static string FormatValue(object value) + { + return value switch + { + bool boolean => boolean ? "true" : "false", + DateTime dateTime => dateTime.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), + IFormattable formattable => formattable.ToString(null, CultureInfo.InvariantCulture), + _ => value.ToString() + }; + } + } +} + diff --git a/FxMacroDataReleaseCalendar.cs b/FxMacroDataReleaseCalendar.cs new file mode 100644 index 0000000..2036e80 --- /dev/null +++ b/FxMacroDataReleaseCalendar.cs @@ -0,0 +1,175 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using NodaTime; +using QuantConnect.Configuration; +using QuantConnect.Data; + +namespace QuantConnect.DataSource +{ + /// + /// FXMacroData economic release calendar data. Subscribe with a currency symbol + /// such as "USD", "EUR", or "JPY" to receive scheduled macro and central-bank + /// events for event-aware algorithms. + /// + public class FxMacroDataReleaseCalendar : BaseData + { + /// + /// Time when the release became available. + /// + public override DateTime EndTime { get; set; } + + public string Release { get; set; } + public string Name { get; set; } + public string Currency { get; set; } + public int? MarketTier { get; set; } + public bool TopTierForCurrency { get; set; } + public string Source { get; set; } + public string SourceUrl { get; set; } + + public override SubscriptionDataSource GetSource(SubscriptionDataConfig config, DateTime date, bool isLiveMode) + { + var currency = config.Symbol.Value.ToLowerInvariant(); + var source = $"https://fxmacrodata.com/api/v1/calendar/{currency}?limit=100"; + var apiKey = Config.Get("fxmacrodata-api-key"); + if (!string.IsNullOrWhiteSpace(apiKey)) + { + source += $"&api_key={Uri.EscapeDataString(apiKey)}"; + } + return new SubscriptionDataSource(source, SubscriptionTransportMedium.RemoteFile, FileFormat.UnfoldingCollection); + } + + public override BaseData Reader(SubscriptionDataConfig config, string line, DateTime date, bool isLiveMode) + { + var payload = JsonConvert.DeserializeObject(line); + var entries = (payload?.Data ?? new List()).Select(row => new FxMacroDataReleaseCalendar + { + Symbol = config.Symbol, + Time = row.Date.Date, + EndTime = ResolveEndTime(row), + Release = row.Release, + Name = row.Name, + Currency = row.Currency, + MarketTier = row.MarketTier, + TopTierForCurrency = row.TopTierForCurrency, + Source = row.Source, + SourceUrl = row.SourceUrl, + Value = row.MarketTier ?? 0 + }); + + return new BaseDataCollection(date, config.Symbol, entries); + } + + public override BaseData Clone() + { + return new FxMacroDataReleaseCalendar + { + Symbol = Symbol, + Time = Time, + EndTime = EndTime, + Release = Release, + Name = Name, + Currency = Currency, + MarketTier = MarketTier, + TopTierForCurrency = TopTierForCurrency, + Source = Source, + SourceUrl = SourceUrl, + Value = Value + }; + } + + public override bool RequiresMapping() + { + return false; + } + + public override bool IsSparseData() + { + return true; + } + + public override Resolution DefaultResolution() + { + return Resolution.Daily; + } + + public override List SupportedResolutions() + { + return DailyResolution; + } + + public override DateTimeZone DataTimeZone() + { + return DateTimeZone.Utc; + } + + private static DateTime ResolveEndTime(CalendarRow row) + { + if (row.AnnouncementDateTimeUtc.HasValue) + { + return DateTime.SpecifyKind(row.AnnouncementDateTimeUtc.Value, DateTimeKind.Utc); + } + if (row.AnnouncementUnix.HasValue) + { + return DateTimeOffset.FromUnixTimeSeconds(row.AnnouncementUnix.Value).UtcDateTime; + } + return DateTime.SpecifyKind(row.Date.Date, DateTimeKind.Utc); + } + + private sealed class CalendarPayload + { + [JsonProperty("data")] + public List Data { get; set; } = new List(); + } + + private sealed class CalendarRow + { + [JsonProperty("date")] + public DateTime Date { get; set; } + + [JsonProperty("release")] + public string Release { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("currency")] + public string Currency { get; set; } + + [JsonProperty("market_tier")] + public int? MarketTier { get; set; } + + [JsonProperty("top_tier_for_currency")] + public bool TopTierForCurrency { get; set; } + + [JsonProperty("announcement_datetime")] + public long? AnnouncementUnix { get; set; } + + [JsonProperty("announcement_datetime_utc")] + public DateTime? AnnouncementDateTimeUtc { get; set; } + + [JsonProperty("source")] + public string Source { get; set; } + + [JsonProperty("source_url")] + public string SourceUrl { get; set; } + } + } +} diff --git a/FxMacroDataReleaseCalendarAlgorithm.py b/FxMacroDataReleaseCalendarAlgorithm.py new file mode 100644 index 0000000..e852165 --- /dev/null +++ b/FxMacroDataReleaseCalendarAlgorithm.py @@ -0,0 +1,39 @@ +# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. +# Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from AlgorithmImports import * +from QuantConnect.DataSource import * + + +class FxMacroDataReleaseCalendarAlgorithm(QCAlgorithm): + def initialize(self): + self.set_start_date(2026, 1, 1) + self.set_end_date(2026, 12, 31) + self.set_cash(100000) + + self.spy = self.add_equity("SPY", Resolution.DAILY).symbol + self.usd_calendar = self.add_data( + FxMacroDataReleaseCalendar, "USD", Resolution.DAILY + ).symbol + + def on_data(self, data: Slice): + event = data.get(FxMacroDataReleaseCalendar, self.usd_calendar) + if event is None: + return + + self.debug( + f"{self.time.date()} {event.name} tier={event.market_tier} " + f"source={event.source}" + ) + if event.market_tier == 1: + self.set_holdings(self.spy, 0.25)