Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix bug in LiveTradingRealTimeHandler #7696

Merged
15 changes: 14 additions & 1 deletion Common/Securities/MarketHoursDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ public class MarketHoursDatabase
private static MarketHoursDatabase _alwaysOpenMarketHoursDatabase;
private static readonly object DataFolderMarketHoursDatabaseLock = new object();

private readonly Dictionary<SecurityDatabaseKey, Entry> _entries;
private Dictionary<SecurityDatabaseKey, Entry> _entries;
private readonly Dictionary<SecurityDatabaseKey, Entry> _customEntries = new();

/// <summary>
/// Gets all the exchange hours held by this provider
Expand Down Expand Up @@ -116,6 +117,17 @@ public static void Reset()
}
}

/// <summary>
/// Reload entries dictionary from MHDB file and merge them with previous custom ones
/// </summary>
public void ReloadEntries()
{
Reset();
var fileEntries = FromDataFolder()._entries.Where(x => !_customEntries.ContainsKey(x.Key));
var newEntries = fileEntries.Concat(_customEntries).ToDictionary();
_entries = newEntries;
}

/// <summary>
/// Gets the instance of the <see cref="MarketHoursDatabase"/> class produced by reading in the market hours
/// data found in /Data/market-hours/
Expand Down Expand Up @@ -167,6 +179,7 @@ public virtual Entry SetEntry(string market, string symbol, SecurityType securit
var key = new SecurityDatabaseKey(market, symbol, securityType);
var entry = new Entry(dataTimeZone, exchangeHours);
_entries[key] = entry;
_customEntries[key] = entry;
return entry;
}

Expand Down
5 changes: 5 additions & 0 deletions Common/Securities/SecurityExchangeHours.cs
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,11 @@ private void SetMarketHoursForDay(DayOfWeek dayOfWeek, out LocalMarketHours loca
/// <param name="localDateTime">The local date time to retrieve market hours for</param>
public LocalMarketHours GetMarketHours(DateTime localDateTime)
{
if (_holidays.Contains(localDateTime.Ticks))
{
return new LocalMarketHours(localDateTime.DayOfWeek);
}

LocalMarketHours marketHours;
switch (localDateTime.DayOfWeek)
{
Expand Down
3 changes: 0 additions & 3 deletions Data/market-hours/market-hours-database.json
Original file line number Diff line number Diff line change
Expand Up @@ -1170,7 +1170,6 @@
"9/7/2020",
"9/6/2021",
"9/5/2022",
"9/4/2023",
"9/2/2024",
"9/11/2001",
"9/12/2001",
Expand Down Expand Up @@ -1476,7 +1475,6 @@
"9/7/2020",
"9/6/2021",
"9/5/2022",
"9/4/2023",
"9/2/2024",
"9/11/2001",
"9/12/2001",
Expand Down Expand Up @@ -1782,7 +1780,6 @@
"9/7/2020",
"9/6/2021",
"9/5/2022",
"9/4/2023",
"9/2/2024",
"9/11/2001",
"9/12/2001",
Expand Down
30 changes: 22 additions & 8 deletions Engine/RealTime/LiveTradingRealTimeHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public class LiveTradingRealTimeHandler : BacktestingRealTimeHandler
{
private Thread _realTimeThread;
private CancellationTokenSource _cancellationTokenSource = new();
private static MarketHoursDatabase _marketHoursDatabase = MarketHoursDatabase.FromDataFolder();
protected MarketHoursDatabase MarketHoursDatabase = MarketHoursDatabase.FromDataFolder();

/// <summary>
/// Boolean flag indicating thread state.
Expand Down Expand Up @@ -113,18 +113,20 @@ private void Run()
}

/// <summary>
/// Refresh the Today variable holding the market hours information
/// Refresh the market hours for each security in the given date
/// </summary>
private void RefreshMarketHoursToday(DateTime date)
/// <remarks>Each time this method is called, the MarketHoursDatabase is reset</remarks>
protected void RefreshMarketHoursToday(DateTime date)
{
date = date.Date;
ResetMarketHoursDatabase();

// update market hours for each security
foreach (var kvp in Algorithm.Securities)
{
var security = kvp.Value;

var marketHours = MarketToday(date, security.Symbol);
var marketHours = GetMarketHours(date, security.Symbol);
security.Exchange.SetMarketHours(marketHours, date.DayOfWeek);
var localMarketHours = security.Exchange.Hours.MarketHours[date.DayOfWeek];
Log.Trace($"LiveTradingRealTimeHandler.RefreshMarketHoursToday({security.Type}): Market hours set: Symbol: {security.Symbol} {localMarketHours} ({security.Exchange.Hours.TimeZone})");
Expand Down Expand Up @@ -175,21 +177,33 @@ public override void Exit()
}

/// <summary>
/// Get the calendar open hours for the date.
/// Get the market hours for the given symbol and date
/// </summary>
private IEnumerable<MarketHoursSegment> MarketToday(DateTime time, Symbol symbol)
protected virtual IEnumerable<MarketHoursSegment> GetMarketHours(DateTime time, Symbol symbol)
{
if (Config.GetBool("force-exchange-always-open"))
{
yield return MarketHoursSegment.OpenAllDay();
yield break;
}

var hours = _marketHoursDatabase.GetExchangeHours(symbol.ID.Market, symbol, symbol.ID.SecurityType);
foreach (var segment in hours.MarketHours[time.DayOfWeek].Segments)
var entry = MarketHoursDatabase.GetEntry(symbol.ID.Market, symbol, symbol.ID.SecurityType);
var hours = entry.ExchangeHours.GetMarketHours(time);

foreach (var segment in hours.Segments)
{
yield return segment;
}
}

/// <summary>
/// Resets the market hours database, forcing a reload when reused.
/// Called in tests where multiple algorithms are run sequentially,
/// and we need to guarantee that every test starts with the same environment.
/// </summary>
protected virtual void ResetMarketHoursDatabase()
{
MarketHoursDatabase.ReloadEntries();
}
}
}
16 changes: 16 additions & 0 deletions Tests/Common/Securities/MarketHoursDatabaseTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,22 @@ public void CustomEntriesStoredAndFetched()
Assert.AreSame(entry, fetchedEntry);
}

[TestCase("UWU", SecurityType.Base)]
[TestCase("SPX", SecurityType.Index)]
public void CustomEntriesAreNotLostWhenReset(string ticker, SecurityType securityType)
{
var database = MarketHoursDatabase.FromDataFolder();
var hours = SecurityExchangeHours.AlwaysOpen(TimeZones.Chicago);
var entry = database.SetEntry(Market.USA, ticker, securityType, hours);

MarketHoursDatabase.Entry returnedEntry;
Assert.IsTrue(database.TryGetEntry(Market.USA, ticker, securityType, out returnedEntry));
Assert.AreEqual(returnedEntry, entry);
Assert.DoesNotThrow(() => database.ReloadEntries());
Assert.IsTrue(database.TryGetEntry(Market.USA, ticker, securityType, out returnedEntry));
Assert.AreEqual(returnedEntry, entry);
}

private static MarketHoursDatabase GetMarketHoursDatabase(string file)
{
return MarketHoursDatabase.FromFile(file);
Expand Down
Loading
Loading