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
new file mode 100644
index 00000000..6d5cb241
--- /dev/null
+++ b/More/FxMacroDataCalendar/Program.cs
@@ -0,0 +1,284 @@
+//==============================================================================
+// 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.Net;
+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 sealed class MacroBlackoutSample : Algorithm
+ {
+ private const string AssetName = "$SPX";
+ private const string CalendarCurrency = "USD";
+ private const int FastTrendDays = 50;
+ private const int SlowTrendDays = 200;
+ 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 ?? DefaultStartDate;
+ EndDate = EndDate ?? TimeZoneInfo.ConvertTime(DateTimeOffset.UtcNow, _marketTimeZone)
+ .Date
+ .AddHours(16);
+ WarmupPeriod = TimeSpan.FromDays(365);
+
+ _blackoutDates = new HashSet(
+ _calendarClient.TopTierBlackoutDates(CalendarCurrency, this, _marketTimeZone));
+
+ 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))
+ targetWeight = 0.0;
+
+ 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 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
+ {
+ 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,
+ Algorithm parentAlgorithm,
+ TimeZoneInfo marketTimeZone)
+ {
+ if (parentAlgorithm.StartDate == null || parentAlgorithm.EndDate == null)
+ throw new InvalidOperationException("Set StartDate and EndDate before loading FXMacroData calendar events.");
+
+ 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
+ .Select(item => LocalEventDate(item, marketTimeZone))
+ .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 ?? 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;
+ }
+
+ 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,
+ CultureInfo.InvariantCulture,
+ DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
+ out var utcTimestamp))
+ {
+ return TimeZoneInfo.ConvertTime(utcTimestamp, marketTimeZone).Date;
+ }
+
+ if (!string.IsNullOrWhiteSpace(item.Date)
+ && DateTime.TryParse(
+ item.Date,
+ CultureInfo.InvariantCulture,
+ DateTimeStyles.AssumeLocal,
+ out var date))
+ {
+ return date.Date;
+ }
+
+ return DateTime.MinValue;
+ }
+ }
+
+ internal sealed class CalendarResponse
+ {
+ [JsonPropertyName("data")]
+ 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")]
+ public string Name { get; set; } = string.Empty;
+
+ [JsonPropertyName("date")]
+ public string Date { get; set; } = string.Empty;
+
+ [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; }
+
+ [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