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
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
36 changes: 28 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,39 @@ 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 securityExchangeHours = new SecurityExchangeHours(
entry.DataTimeZone,
entry.ExchangeHours.Holidays,
entry.ExchangeHours.MarketHours.ToDictionary(),
entry.ExchangeHours.EarlyCloses,
entry.ExchangeHours.LateOpens);
var hours = securityExchangeHours.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.Reset();
}
}
}
234 changes: 234 additions & 0 deletions Tests/Engine/RealTime/LiveTradingRealTimeHandlerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@
using QuantConnect.Lean.Engine.Results;
using QuantConnect.Lean.Engine.RealTime;
using QuantConnect.Tests.Engine.DataFeeds;
using System.Linq;
using QuantConnect.Securities;
using System.Collections.Generic;
using QuantConnect.Data.Market;
using QuantConnect.Lean.Engine.TransactionHandlers;
using Moq;
using QuantConnect.Brokerages.Backtesting;
using static QuantConnect.Tests.Engine.BrokerageTransactionHandlerTests.BrokerageTransactionHandlerTests;
using QuantConnect.Orders;
using System.Reflection;
using QuantConnect.Lean.Engine.HistoricalData;

namespace QuantConnect.Tests.Engine.RealTime
{
Expand Down Expand Up @@ -63,6 +74,80 @@ public void ThreadSafety()
realTimeHandler.Exit();
}

[NonParallelizable]
[TestCaseSource(typeof(ExchangeHoursDataClass), nameof(ExchangeHoursDataClass.TestCases))]
public void RefreshesMarketHoursCorrectly(SecurityExchangeHours securityExchangeHours, MarketHoursSegment expectedSegment)
{
Security security;
var algorithm = new AlgorithmStub();
security = algorithm.AddEquity("SPY");

var realTimeHandler = new TestLiveTradingRealTimeHandler();
realTimeHandler.Setup(algorithm,
new AlgorithmNodePacket(PacketType.AlgorithmNode),
new BacktestingResultHandler(),
null,
new TestTimeLimitManager());

var time = new DateTime(2023, 5, 30).Date;
var entry = new MarketHoursDatabase.Entry(TimeZones.NewYork, securityExchangeHours);
var key = new SecurityDatabaseKey(Market.USA, null, SecurityType.Equity);
var mhdb = new MarketHoursDatabase(new Dictionary<SecurityDatabaseKey, MarketHoursDatabase.Entry>() { { key, entry} });
realTimeHandler.SetMarketHoursDatabase(mhdb);
realTimeHandler.TestRefreshMarketHoursToday(security, time, expectedSegment);
}

[Test]
public void ResetMarketHoursCorrectly()
{
var algorithm = new TestAlgorithm { HistoryProvider = new FakeHistoryProvider() };
algorithm.SubscriptionManager.SetDataManager(new DataManagerStub(algorithm));
algorithm.SetCash(100000);
algorithm.SetStartDate(2023, 5, 30);
algorithm.SetEndDate(2023, 5, 30);
var security = algorithm.AddEquity("SPY");
security.Exchange = new SecurityExchange(SecurityExchangeHours.AlwaysOpen(TimeZones.NewYork));
var symbol = security.Symbol;
algorithm.SetFinishedWarmingUp();

var handleOptionNotification = typeof(BrokerageTransactionHandler).GetMethod("HandleOptionNotification", BindingFlags.NonPublic | BindingFlags.Instance);

var transactionHandler = new TestBrokerageTransactionHandler();
var broker = new BacktestingBrokerage(algorithm);
transactionHandler.Initialize(algorithm, broker, new BacktestingResultHandler());

// Creates a market order
security.SetMarketPrice(new TradeBar(new DateTime(2023, 5, 30), symbol, 280m, 280m, 280m, 280m, 100));

var orderRequest = new SubmitOrderRequest(OrderType.Market, security.Type, security.Symbol, 1, 0, 0, new DateTime(2023, 5, 30), "TestTag1");

var orderProcessorMock = new Mock<IOrderProcessor>();
orderProcessorMock.Setup(m => m.GetOrderTicket(It.IsAny<int>())).Returns(new OrderTicket(algorithm.Transactions, orderRequest));
algorithm.Transactions.SetOrderProcessor(orderProcessorMock.Object);
var orderTicket = transactionHandler.Process(orderRequest);
transactionHandler.HandleOrderRequest(orderRequest);
Assert.IsTrue(orderTicket.Status == OrderStatus.Submitted);
broker.Scan();
Assert.IsTrue(orderTicket.Status == OrderStatus.Filled);

var realTimeHandler = new TestLiveTradingRealTimeHandlerReset();
realTimeHandler.Setup(algorithm,
new AlgorithmNodePacket(PacketType.AlgorithmNode),
new BacktestingResultHandler(),
null,
new TestTimeLimitManager());
realTimeHandler.AddRefreshHoursScheduledEvent();

orderRequest = new SubmitOrderRequest(OrderType.Market, security.Type, security.Symbol, 1, 0, 0, new DateTime(2023, 5, 30), "TestTag2");
orderRequest.SetOrderId(2);
orderTicket = transactionHandler.Process(orderRequest);
transactionHandler.HandleOrderRequest(orderRequest);
Assert.IsTrue(orderTicket.Status == OrderStatus.Submitted);
broker.Scan();
Assert.IsTrue(orderTicket.Status != OrderStatus.Filled);
realTimeHandler.Exit();
}

private class TestTimeLimitManager : IIsolatorLimitResultProvider
{
public IsolatorLimitResult IsWithinLimit()
Expand All @@ -78,5 +163,154 @@ public bool TryRequestAdditionalTime(int minutes)
throw new NotImplementedException();
}
}

public class TestLiveTradingRealTimeHandler: LiveTradingRealTimeHandler
{
private static AutoResetEvent OnSecurityUpdated = new AutoResetEvent(false);
public void SetMarketHoursDatabase(MarketHoursDatabase marketHoursDatabase)
{
MarketHoursDatabase = marketHoursDatabase;
}

public void TestRefreshMarketHoursToday(Security security, DateTime time, MarketHoursSegment expectedSegment)
{
OnSecurityUpdated.Reset();
RefreshMarketHoursToday(time);
OnSecurityUpdated.WaitOne();
AssertMarketHours(security, time, expectedSegment);
}

protected override IEnumerable<MarketHoursSegment> GetMarketHours(DateTime time, Symbol symbol)
{
var results = base.GetMarketHours(time, symbol);
OnSecurityUpdated.Set();
return results;
}

public void AssertMarketHours(Security security, DateTime time, MarketHoursSegment expectedSegment)
{
var marketHours = security.Exchange.Hours.MarketHours[time.DayOfWeek];
var segment = marketHours.Segments.SingleOrDefault();

if (segment == null)
{
Assert.AreEqual(expectedSegment, segment);
}
else
{
Assert.AreEqual(expectedSegment.Start, segment.Start);
Assert.AreEqual(expectedSegment.End, segment.End);
for (var hour = segment.Start; hour < segment.End; hour = hour.Add(TimeSpan.FromHours(1)))
{
Assert.IsTrue(marketHours.IsOpen(hour, false));
}
Assert.AreEqual(expectedSegment.End, security.Exchange.Hours.GetNextMarketClose(time.Date, false).TimeOfDay);
Assert.AreEqual(expectedSegment.Start, security.Exchange.Hours.GetNextMarketOpen(time.Date, false).TimeOfDay);
}

Exit();
}
}

public class TestLiveTradingRealTimeHandlerReset : LiveTradingRealTimeHandler
{
private static AutoResetEvent OnSecurityUpdated = new AutoResetEvent(false);

public void AddRefreshHoursScheduledEvent()
{
Add(new ScheduledEvent("RefreshHours", new[] { new DateTime(2023, 6, 29) }, (name, triggerTime) =>
{
// refresh market hours from api every day
RefreshMarketHoursToday((new DateTime(2023, 5, 30)).Date);
}));
OnSecurityUpdated.Reset();
SetTime(DateTime.UtcNow);
OnSecurityUpdated.WaitOne();
Exit();
}

protected override IEnumerable<MarketHoursSegment> GetMarketHours(DateTime time, Symbol symbol)
{
var results = base.GetMarketHours(time, symbol);
OnSecurityUpdated.Set();
return results;
}

protected override void ResetMarketHoursDatabase()
{
var entry = new MarketHoursDatabase.Entry(TimeZones.NewYork, ExchangeHoursDataClass.CreateExchangeHoursWithHolidays());
var key = new SecurityDatabaseKey(Market.USA, null, SecurityType.Equity);
var mhdb = new MarketHoursDatabase(new Dictionary<SecurityDatabaseKey, MarketHoursDatabase.Entry>() { { key, entry } });
MarketHoursDatabase = mhdb;
}
}

public class ExchangeHoursDataClass
{
private static LocalMarketHours _sunday = new LocalMarketHours(DayOfWeek.Sunday, new TimeSpan(9, 30, 0), new TimeSpan(16, 0, 0));
private static LocalMarketHours _monday = new LocalMarketHours(DayOfWeek.Monday, new TimeSpan(9, 30, 0), new TimeSpan(16, 0, 0));
private static LocalMarketHours _tuesday = new LocalMarketHours(DayOfWeek.Tuesday, new TimeSpan(9, 30, 0), new TimeSpan(16, 0, 0));
private static LocalMarketHours _wednesday = new LocalMarketHours(DayOfWeek.Wednesday, new TimeSpan(9, 30, 0), new TimeSpan(16, 0, 0));
private static LocalMarketHours _thursday = new LocalMarketHours(DayOfWeek.Thursday, new TimeSpan(9, 30, 0), new TimeSpan(16, 0, 0));
private static LocalMarketHours _friday = new LocalMarketHours(DayOfWeek.Friday, new TimeSpan(9, 30, 0), new TimeSpan(16, 0, 0));
private static LocalMarketHours _saturday = new LocalMarketHours(DayOfWeek.Saturday, new TimeSpan(9, 30, 0), new TimeSpan(16, 0, 0));

public static IEnumerable<TestCaseData> TestCases
{
get
{
yield return new TestCaseData(CreateExchangeHoursWithEarlyCloseAndLateOpen(), new MarketHoursSegment(MarketHoursState.Market,new TimeSpan(10, 0, 0), new TimeSpan(13, 0, 0)));
yield return new TestCaseData(CreateExchangeHoursWithEarlyClose(), new MarketHoursSegment(MarketHoursState.Market, new TimeSpan(9, 30, 0), new TimeSpan(13, 0, 0)));
yield return new TestCaseData(CreateExchangeHoursWithLateOpen(), new MarketHoursSegment(MarketHoursState.Market, new TimeSpan(10, 0, 0), new TimeSpan(16, 0, 0)));
yield return new TestCaseData(CreateExchangeHoursWithHolidays(), null);
}
}

private static SecurityExchangeHours CreateExchangeHoursWithEarlyCloseAndLateOpen()
{
var earlyCloses = new Dictionary<DateTime, TimeSpan> { { new DateTime(2023, 5, 30).Date, new TimeSpan(13, 0, 0) } };
var lateOpens = new Dictionary<DateTime, TimeSpan>() { { new DateTime(2023, 5, 30).Date, new TimeSpan(10, 0, 0) } };
var exchangeHours = new SecurityExchangeHours(TimeZones.NewYork, new List<DateTime>(), new[]
{
_sunday, _monday, _tuesday, _wednesday, _thursday, _friday, _saturday
}.ToDictionary(x => x.DayOfWeek), earlyCloses, lateOpens);
return exchangeHours;
}

private static SecurityExchangeHours CreateExchangeHoursWithEarlyClose()
{
var earlyCloses = new Dictionary<DateTime, TimeSpan> { { new DateTime(2023, 5, 30).Date, new TimeSpan(13, 0, 0) } };
var lateOpens = new Dictionary<DateTime, TimeSpan>();
var exchangeHours = new SecurityExchangeHours(TimeZones.NewYork, new List<DateTime>(), new[]
{
_sunday, _monday, _tuesday, _wednesday, _thursday, _friday, _saturday
}.ToDictionary(x => x.DayOfWeek), earlyCloses, lateOpens);
return exchangeHours;
}

private static SecurityExchangeHours CreateExchangeHoursWithLateOpen()
{
var earlyCloses = new Dictionary<DateTime, TimeSpan>();
var lateOpens = new Dictionary<DateTime, TimeSpan>() { { new DateTime(2023, 5, 30).Date, new TimeSpan(10, 0, 0) } };
var exchangeHours = new SecurityExchangeHours(TimeZones.NewYork, new List<DateTime>(), new[]
{
_sunday, _monday, _tuesday, _wednesday, _thursday, _friday, _saturday
}.ToDictionary(x => x.DayOfWeek), earlyCloses, lateOpens);
return exchangeHours;
}

public static SecurityExchangeHours CreateExchangeHoursWithHolidays()
{
var earlyCloses = new Dictionary<DateTime, TimeSpan>();
var lateOpens = new Dictionary<DateTime, TimeSpan>();
var holidays = new List<DateTime>() { new DateTime(2023, 5, 30).Date };
var exchangeHours = new SecurityExchangeHours(TimeZones.NewYork, holidays, new[]
{
_sunday, _monday, _tuesday, _wednesday, _thursday, _friday, _saturday
}.ToDictionary(x => x.DayOfWeek), earlyCloses, lateOpens);
return exchangeHours;
}

}
}
}
Loading