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)