From 6ef3cd577375ee5abd27fd0c055423634c5776e1 Mon Sep 17 00:00:00 2001 From: Robert Tidball Date: Wed, 8 Jul 2026 18:56:55 +1000 Subject: [PATCH 1/5] Add FXMacroData calendar sample --- More/FxMacroDataCalendar/Program.cs | 65 +++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 More/FxMacroDataCalendar/Program.cs diff --git a/More/FxMacroDataCalendar/Program.cs b/More/FxMacroDataCalendar/Program.cs new file mode 100644 index 00000000..0caa2fbe --- /dev/null +++ b/More/FxMacroDataCalendar/Program.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace FxMacroDataCalendar; + +internal static class Program +{ + private static void Main() + { + var events = FetchCalendar("USD", "2026-07-01", "2026-07-20"); + + Console.WriteLine("Top-tier USD macro blackout dates:"); + foreach (var item in events.Where(e => e.TopTierForCurrency || e.MarketTier == 1)) + Console.WriteLine($" {EventDate(item)}: {item.Name}"); + } + + private static IReadOnlyList FetchCalendar(string currency, string startDate, string endDate) + { + var url = "https://fxmacrodata.com/api/v1/calendar/" + + currency + + "?start_date=" + + Uri.EscapeDataString(startDate) + + "&end_date=" + + Uri.EscapeDataString(endDate); + + using var client = new HttpClient(); + var json = client.GetStringAsync(url).Result; + var payload = JsonSerializer.Deserialize(json); + return payload?.Data ?? Array.Empty(); + } + + private static string EventDate(CalendarEvent item) + { + var value = item.AnnouncementDatetimeUtc ?? item.Date ?? string.Empty; + return value.Length > 10 ? value.Substring(0, 10) : value; + } +} + +internal sealed class CalendarResponse +{ + [JsonPropertyName("data")] + public List Data { get; set; } = new(); +} + +internal sealed class CalendarEvent +{ + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("date")] + public string? Date { get; set; } + + [JsonPropertyName("announcement_datetime_utc")] + public string? AnnouncementDatetimeUtc { get; set; } + + [JsonPropertyName("market_tier")] + public int MarketTier { get; set; } + + [JsonPropertyName("top_tier_for_currency")] + public bool TopTierForCurrency { get; set; } +} From 090b1811e2b607a8523b98f5be31ad89a1b2a827 Mon Sep 17 00:00:00 2001 From: Robert Tidball Date: Fri, 10 Jul 2026 15:47:48 +1000 Subject: [PATCH 2/5] Add FXMacroData macro blackout sample strategy --- .../FxMacroDataCalendar.csproj | 13 ++ More/FxMacroDataCalendar/Program.cs | 201 ++++++++++++++---- 2 files changed, 173 insertions(+), 41 deletions(-) create mode 100644 More/FxMacroDataCalendar/FxMacroDataCalendar.csproj diff --git a/More/FxMacroDataCalendar/FxMacroDataCalendar.csproj b/More/FxMacroDataCalendar/FxMacroDataCalendar.csproj new file mode 100644 index 00000000..c13ab16f --- /dev/null +++ b/More/FxMacroDataCalendar/FxMacroDataCalendar.csproj @@ -0,0 +1,13 @@ + + + + Exe + net6.0-windows + x64 + + + + + + + diff --git a/More/FxMacroDataCalendar/Program.cs b/More/FxMacroDataCalendar/Program.cs index 0caa2fbe..e16a99a4 100644 --- a/More/FxMacroDataCalendar/Program.cs +++ b/More/FxMacroDataCalendar/Program.cs @@ -1,65 +1,184 @@ +//============================================================================== +// Project: TuringTrader, FXMacroData calendar sample +// Name: Program.cs +// Description: Sample strategy using FXMacroData macro-event blackout dates +//============================================================================== + +#region libraries using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Net.Http; using System.Text.Json; using System.Text.Json.Serialization; +using TuringTrader.SimulatorV2; +using TuringTrader.SimulatorV2.Indicators; +#endregion -namespace FxMacroDataCalendar; - -internal static class Program +namespace FxMacroDataCalendar { - private static void Main() + internal sealed class MacroBlackoutSample : Algorithm { - var events = FetchCalendar("USD", "2026-07-01", "2026-07-20"); + private const string AssetName = "$SPX"; + private const string CalendarCurrency = "USD"; + private const int FastTrendDays = 50; + private const int SlowTrendDays = 200; + private const int SampleLookbackYears = 10; + + private readonly ReleaseCalendarClient _calendarClient = new ReleaseCalendarClient(); + private HashSet _blackoutDates = new HashSet(); + + public override string Name => "FXMacroData Macro Blackout Sample"; + + public override void Run() + { + StartDate = StartDate ?? DateTime.Today.AddYears(-SampleLookbackYears); + EndDate = EndDate ?? DateTime.Today; + WarmupPeriod = TimeSpan.FromDays(365); + + _blackoutDates = new HashSet(_calendarClient.TopTierBlackoutDates(CalendarCurrency, this)); + + SimLoop(() => + { + var asset = Asset(AssetName); + var trendSignal = asset.Close.EMA(FastTrendDays)[0] > asset.Close.EMA(SlowTrendDays)[0]; + var targetWeight = trendSignal ? 1.0 : 0.0; + + if (IsMacroBlackout(SimDate) && asset.Position <= 0.0 && targetWeight > 0.0) + targetWeight = 0.0; - Console.WriteLine("Top-tier USD macro blackout dates:"); - foreach (var item in events.Where(e => e.TopTierForCurrency || e.MarketTier == 1)) - Console.WriteLine($" {EventDate(item)}: {item.Name}"); + if (Math.Abs(asset.Position - targetWeight) > 0.05) + asset.Allocate(targetWeight, OrderType.openNextBar); + + if (!IsOptimizing) + { + Plotter.SelectChart(Name, "Date"); + Plotter.SetX(SimDate); + Plotter.Plot(Name, NetAssetValue); + Plotter.Plot(AssetName, asset.Close[0]); + Plotter.Plot("Macro blackout", IsMacroBlackout(SimDate) ? 1.0 : 0.0); + } + }); + + if (!IsOptimizing) + { + Plotter.AddTargetAllocation(); + Plotter.AddHistoricalAllocations(); + Plotter.AddTradeLog(); + } + } + + private bool IsMacroBlackout(DateTime simDate) + { + return _blackoutDates.Contains(simDate.Date); + } } - private static IReadOnlyList FetchCalendar(string currency, string startDate, string endDate) + internal sealed class ReleaseCalendarClient { - var url = "https://fxmacrodata.com/api/v1/calendar/" - + currency - + "?start_date=" - + Uri.EscapeDataString(startDate) - + "&end_date=" - + Uri.EscapeDataString(endDate); - - using var client = new HttpClient(); - var json = client.GetStringAsync(url).Result; - var payload = JsonSerializer.Deserialize(json); - return payload?.Data ?? Array.Empty(); + private const string CalendarUrl = "https://fxmacrodata.com/api/v1/calendar/"; + private static readonly TimeSpan RequestTimeout = TimeSpan.FromSeconds(30); + + public IReadOnlyList TopTierBlackoutDates(string currency, Algorithm parentAlgorithm) + { + if (parentAlgorithm.StartDate == null || parentAlgorithm.EndDate == null) + throw new InvalidOperationException("Set StartDate and EndDate before loading FXMacroData calendar events."); + + return FetchCalendar(currency, (DateTime)parentAlgorithm.StartDate, (DateTime)parentAlgorithm.EndDate) + .Where(IsTopTier) + .Select(LocalEventDate) + .Where(date => date != DateTime.MinValue) + .Distinct() + .OrderBy(date => date) + .ToArray(); + } + + private static IReadOnlyList FetchCalendar(string currency, DateTime startDate, DateTime endDate) + { + var url = CalendarUrl + + Uri.EscapeDataString(currency.ToUpperInvariant()) + + "?start_date=" + + Uri.EscapeDataString(startDate.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)) + + "&end_date=" + + Uri.EscapeDataString(endDate.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)); + + using var client = new HttpClient { Timeout = RequestTimeout }; + var json = client.GetStringAsync(url).Result; + var payload = JsonSerializer.Deserialize(json); + return payload?.Data ?? Array.Empty(); + } + + private static bool IsTopTier(CalendarEvent item) + { + return item.TopTierForCurrency || item.MarketTier == 1; + } + + private static DateTime LocalEventDate(CalendarEvent item) + { + if (!string.IsNullOrWhiteSpace(item.AnnouncementDatetimeUtc) + && DateTimeOffset.TryParse( + item.AnnouncementDatetimeUtc, + CultureInfo.InvariantCulture, + DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, + out var utcTimestamp)) + { + return utcTimestamp.ToLocalTime().Date; + } + + if (!string.IsNullOrWhiteSpace(item.Date) + && DateTime.TryParse( + item.Date, + CultureInfo.InvariantCulture, + DateTimeStyles.AssumeLocal, + out var date)) + { + return date.Date; + } + + return DateTime.MinValue; + } } - private static string EventDate(CalendarEvent item) + internal sealed class CalendarResponse { - var value = item.AnnouncementDatetimeUtc ?? item.Date ?? string.Empty; - return value.Length > 10 ? value.Substring(0, 10) : value; + [JsonPropertyName("data")] + public List Data { get; set; } = new List(); } -} -internal sealed class CalendarResponse -{ - [JsonPropertyName("data")] - public List Data { get; set; } = new(); -} + internal sealed class CalendarEvent + { + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; -internal sealed class CalendarEvent -{ - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + [JsonPropertyName("date")] + public string Date { get; set; } = string.Empty; - [JsonPropertyName("date")] - public string? Date { get; set; } + [JsonPropertyName("announcement_datetime_utc")] + public string AnnouncementDatetimeUtc { get; set; } = string.Empty; - [JsonPropertyName("announcement_datetime_utc")] - public string? AnnouncementDatetimeUtc { get; set; } + [JsonPropertyName("market_tier")] + public int MarketTier { get; set; } - [JsonPropertyName("market_tier")] - public int MarketTier { get; set; } + [JsonPropertyName("top_tier_for_currency")] + public bool TopTierForCurrency { get; set; } + } - [JsonPropertyName("top_tier_for_currency")] - public bool TopTierForCurrency { get; set; } + internal static class Program + { + private static void Main() + { + var algo = new MacroBlackoutSample(); + algo.Run(); + + Console.WriteLine( + "{0}: {1} bars, {2} trades.", + algo.Name, + algo.EquityCurve.Count, + algo.Account.TradeLog.Count); + } + } } + +//============================================================================== +// end of file From 28ace22aa7e0c7f82c292db886cfd4866f21216c Mon Sep 17 00:00:00 2001 From: Robert Tidball Date: Sat, 11 Jul 2026 15:12:34 +1000 Subject: [PATCH 3/5] Refine FXMacroData calendar strategy sample --- More/FxMacroDataCalendar/Program.cs | 40 ++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/More/FxMacroDataCalendar/Program.cs b/More/FxMacroDataCalendar/Program.cs index e16a99a4..5a8728f7 100644 --- a/More/FxMacroDataCalendar/Program.cs +++ b/More/FxMacroDataCalendar/Program.cs @@ -24,20 +24,27 @@ internal sealed class MacroBlackoutSample : Algorithm private const string CalendarCurrency = "USD"; private const int FastTrendDays = 50; private const int SlowTrendDays = 200; - private const int SampleLookbackYears = 10; + private const string MarketTimeZoneId = "Eastern Standard Time"; + private static readonly DateTime DefaultStartDate = DateTimeOffset + .Parse("2007-01-01T16:00:00-05:00", CultureInfo.InvariantCulture) + .DateTime; private readonly ReleaseCalendarClient _calendarClient = new ReleaseCalendarClient(); + private readonly TimeZoneInfo _marketTimeZone = FindMarketTimeZone(); private HashSet _blackoutDates = new HashSet(); public override string Name => "FXMacroData Macro Blackout Sample"; public override void Run() { - StartDate = StartDate ?? DateTime.Today.AddYears(-SampleLookbackYears); - EndDate = EndDate ?? DateTime.Today; + StartDate = StartDate ?? DefaultStartDate; + EndDate = EndDate ?? TimeZoneInfo.ConvertTime(DateTimeOffset.UtcNow, _marketTimeZone) + .Date + .AddHours(16); WarmupPeriod = TimeSpan.FromDays(365); - _blackoutDates = new HashSet(_calendarClient.TopTierBlackoutDates(CalendarCurrency, this)); + _blackoutDates = new HashSet( + _calendarClient.TopTierBlackoutDates(CalendarCurrency, this, _marketTimeZone)); SimLoop(() => { @@ -45,7 +52,7 @@ public override void Run() var trendSignal = asset.Close.EMA(FastTrendDays)[0] > asset.Close.EMA(SlowTrendDays)[0]; var targetWeight = trendSignal ? 1.0 : 0.0; - if (IsMacroBlackout(SimDate) && asset.Position <= 0.0 && targetWeight > 0.0) + if (IsMacroBlackout(SimDate)) targetWeight = 0.0; if (Math.Abs(asset.Position - targetWeight) > 0.05) @@ -73,6 +80,18 @@ private bool IsMacroBlackout(DateTime simDate) { return _blackoutDates.Contains(simDate.Date); } + + private static TimeZoneInfo FindMarketTimeZone() + { + try + { + return TimeZoneInfo.FindSystemTimeZoneById(MarketTimeZoneId); + } + catch (Exception ex) when (ex is TimeZoneNotFoundException || ex is InvalidTimeZoneException) + { + return TimeZoneInfo.FindSystemTimeZoneById("America/New_York"); + } + } } internal sealed class ReleaseCalendarClient @@ -80,14 +99,17 @@ internal sealed class ReleaseCalendarClient private const string CalendarUrl = "https://fxmacrodata.com/api/v1/calendar/"; private static readonly TimeSpan RequestTimeout = TimeSpan.FromSeconds(30); - public IReadOnlyList TopTierBlackoutDates(string currency, Algorithm parentAlgorithm) + public IReadOnlyList TopTierBlackoutDates( + string currency, + Algorithm parentAlgorithm, + TimeZoneInfo marketTimeZone) { if (parentAlgorithm.StartDate == null || parentAlgorithm.EndDate == null) throw new InvalidOperationException("Set StartDate and EndDate before loading FXMacroData calendar events."); return FetchCalendar(currency, (DateTime)parentAlgorithm.StartDate, (DateTime)parentAlgorithm.EndDate) .Where(IsTopTier) - .Select(LocalEventDate) + .Select(item => LocalEventDate(item, marketTimeZone)) .Where(date => date != DateTime.MinValue) .Distinct() .OrderBy(date => date) @@ -114,7 +136,7 @@ private static bool IsTopTier(CalendarEvent item) return item.TopTierForCurrency || item.MarketTier == 1; } - private static DateTime LocalEventDate(CalendarEvent item) + private static DateTime LocalEventDate(CalendarEvent item, TimeZoneInfo marketTimeZone) { if (!string.IsNullOrWhiteSpace(item.AnnouncementDatetimeUtc) && DateTimeOffset.TryParse( @@ -123,7 +145,7 @@ private static DateTime LocalEventDate(CalendarEvent item) DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var utcTimestamp)) { - return utcTimestamp.ToLocalTime().Date; + return TimeZoneInfo.ConvertTime(utcTimestamp, marketTimeZone).Date; } if (!string.IsNullOrWhiteSpace(item.Date) From e5dd1b8e7dd733f628b3c52bee8992863856b16e Mon Sep 17 00:00:00 2001 From: Robert Tidball Date: Mon, 13 Jul 2026 16:16:31 +1000 Subject: [PATCH 4/5] fix: reject uncovered historical calendar ranges --- More/FxMacroDataCalendar/Program.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/More/FxMacroDataCalendar/Program.cs b/More/FxMacroDataCalendar/Program.cs index 5a8728f7..5ad1aec9 100644 --- a/More/FxMacroDataCalendar/Program.cs +++ b/More/FxMacroDataCalendar/Program.cs @@ -107,7 +107,13 @@ public IReadOnlyList TopTierBlackoutDates( if (parentAlgorithm.StartDate == null || parentAlgorithm.EndDate == null) throw new InvalidOperationException("Set StartDate and EndDate before loading FXMacroData calendar events."); - return FetchCalendar(currency, (DateTime)parentAlgorithm.StartDate, (DateTime)parentAlgorithm.EndDate) + var calendarEvents = FetchCalendar(currency, (DateTime)parentAlgorithm.StartDate, (DateTime)parentAlgorithm.EndDate); + if (calendarEvents.Count == 0 && parentAlgorithm.EndDate.Value.Date < DateTime.UtcNow.Date) + throw new InvalidOperationException( + "FXMacroData does not currently provide confirmed historical release timestamps for this range. " + + "Do not run a historical blackout backtest until the calendar has coverage for the requested period."); + + return calendarEvents .Where(IsTopTier) .Select(item => LocalEventDate(item, marketTimeZone)) .Where(date => date != DateTime.MinValue) @@ -128,7 +134,7 @@ private static IReadOnlyList FetchCalendar(string currency, DateT using var client = new HttpClient { Timeout = RequestTimeout }; var json = client.GetStringAsync(url).Result; var payload = JsonSerializer.Deserialize(json); - return payload?.Data ?? Array.Empty(); + return payload?.Data ?? new List(); } private static bool IsTopTier(CalendarEvent item) From ea3d9c57e2446d59735f593286e6bbda0315983b Mon Sep 17 00:00:00 2001 From: Robert Tidball Date: Mon, 13 Jul 2026 16:31:24 +1000 Subject: [PATCH 5/5] fix: load historical macro announcements --- More/FxMacroDataCalendar/Program.cs | 84 ++++++++++++++++++++++++++--- 1 file changed, 78 insertions(+), 6 deletions(-) diff --git a/More/FxMacroDataCalendar/Program.cs b/More/FxMacroDataCalendar/Program.cs index 5ad1aec9..6d5cb241 100644 --- a/More/FxMacroDataCalendar/Program.cs +++ b/More/FxMacroDataCalendar/Program.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Net; using System.Linq; using System.Net.Http; using System.Text.Json; @@ -96,8 +97,14 @@ private static TimeZoneInfo FindMarketTimeZone() internal sealed class ReleaseCalendarClient { + private const string AnnouncementsUrl = "https://fxmacrodata.com/api/v1/announcements/"; private const string CalendarUrl = "https://fxmacrodata.com/api/v1/calendar/"; private static readonly TimeSpan RequestTimeout = TimeSpan.FromSeconds(30); + private static readonly string[] HistoricalTopTierIndicators = + { + "employment", "non_farm_payrolls", "unemployment", "inflation", "inflation_mom", + "core_inflation", "core_inflation_mom", "pce", "core_pce", "gdp", "retail_sales", "policy_rate", + }; public IReadOnlyList TopTierBlackoutDates( string currency, @@ -107,14 +114,18 @@ public IReadOnlyList TopTierBlackoutDates( if (parentAlgorithm.StartDate == null || parentAlgorithm.EndDate == null) throw new InvalidOperationException("Set StartDate and EndDate before loading FXMacroData calendar events."); - var calendarEvents = FetchCalendar(currency, (DateTime)parentAlgorithm.StartDate, (DateTime)parentAlgorithm.EndDate); - if (calendarEvents.Count == 0 && parentAlgorithm.EndDate.Value.Date < DateTime.UtcNow.Date) - throw new InvalidOperationException( - "FXMacroData does not currently provide confirmed historical release timestamps for this range. " - + "Do not run a historical blackout backtest until the calendar has coverage for the requested period."); + var startDate = (DateTime)parentAlgorithm.StartDate; + var endDate = (DateTime)parentAlgorithm.EndDate; + var today = DateTime.UtcNow.Date; + var calendarEvents = new List(); + + if (startDate.Date < today) + calendarEvents.AddRange(FetchHistoricalAnnouncements(currency, startDate, endDate < today ? endDate : today)); + if (endDate.Date >= today) + calendarEvents.AddRange(FetchCalendar(currency, startDate > today ? startDate : today, endDate) + .Where(IsTopTier)); return calendarEvents - .Where(IsTopTier) .Select(item => LocalEventDate(item, marketTimeZone)) .Where(date => date != DateTime.MinValue) .Distinct() @@ -137,6 +148,46 @@ private static IReadOnlyList FetchCalendar(string currency, DateT return payload?.Data ?? new List(); } + private static IReadOnlyList FetchHistoricalAnnouncements( + string currency, + DateTime startDate, + DateTime endDate) + { + var events = new List(); + using var client = new HttpClient { Timeout = RequestTimeout }; + + foreach (var indicator in HistoricalTopTierIndicators) + { + var offset = 0; + while (true) + { + var url = AnnouncementsUrl + + Uri.EscapeDataString(currency.ToUpperInvariant()) + + "/" + + Uri.EscapeDataString(indicator) + + "?start_date=" + + Uri.EscapeDataString(startDate.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)) + + "&end_date=" + + Uri.EscapeDataString(endDate.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)) + + "&limit=100&offset=" + + offset.ToString(CultureInfo.InvariantCulture); + using var response = client.GetAsync(url).Result; + if (response.StatusCode == HttpStatusCode.NotFound) + break; + response.EnsureSuccessStatusCode(); + var payload = JsonSerializer.Deserialize(response.Content.ReadAsStringAsync().Result); + var rows = payload?.Data ?? new List(); + events.AddRange(rows); + + if (payload?.Pagination?.HasMore != true || rows.Count == 0) + break; + offset += rows.Count; + } + } + + return events; + } + private static bool IsTopTier(CalendarEvent item) { return item.TopTierForCurrency || item.MarketTier == 1; @@ -144,6 +195,9 @@ private static bool IsTopTier(CalendarEvent item) private static DateTime LocalEventDate(CalendarEvent item, TimeZoneInfo marketTimeZone) { + if (item.AnnouncementDatetime is long unixTimestamp) + return TimeZoneInfo.ConvertTime(DateTimeOffset.FromUnixTimeSeconds(unixTimestamp), marketTimeZone).Date; + if (!string.IsNullOrWhiteSpace(item.AnnouncementDatetimeUtc) && DateTimeOffset.TryParse( item.AnnouncementDatetimeUtc, @@ -174,6 +228,21 @@ internal sealed class CalendarResponse public List Data { get; set; } = new List(); } + internal sealed class AnnouncementResponse + { + [JsonPropertyName("data")] + public List Data { get; set; } = new List(); + + [JsonPropertyName("pagination")] + public Pagination Pagination { get; set; } = new Pagination(); + } + + internal sealed class Pagination + { + [JsonPropertyName("has_more")] + public bool HasMore { get; set; } + } + internal sealed class CalendarEvent { [JsonPropertyName("name")] @@ -185,6 +254,9 @@ internal sealed class CalendarEvent [JsonPropertyName("announcement_datetime_utc")] public string AnnouncementDatetimeUtc { get; set; } = string.Empty; + [JsonPropertyName("announcement_datetime")] + public long? AnnouncementDatetime { get; set; } + [JsonPropertyName("market_tier")] public int MarketTier { get; set; }