Skip to content

Commit 5f29892

Browse files
committed
Refactor GetExchangeHours and add unit tests
1 parent 5b26b61 commit 5f29892

File tree

2 files changed

+66
-24
lines changed

2 files changed

+66
-24
lines changed

Common/Securities/SecurityExchangeHours.cs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ public HashSet<DateTime> BankHolidays
117117
/// </summary>
118118
public static SecurityExchangeHours AlwaysOpen(DateTimeZone timeZone)
119119
{
120-
var dayOfWeeks = Enum.GetValues(typeof (DayOfWeek)).OfType<DayOfWeek>();
120+
var dayOfWeeks = Enum.GetValues(typeof(DayOfWeek)).OfType<DayOfWeek>();
121121
return new SecurityExchangeHours(timeZone,
122122
Enumerable.Empty<DateTime>(),
123123
dayOfWeeks.Select(LocalMarketHours.OpenAllDay).ToDictionary(x => x.DayOfWeek),
@@ -282,7 +282,7 @@ public DateTime GetPreviousMarketOpen(DateTime localDateTime, bool extendedMarke
282282
for (int i = 0; i < 7; i++)
283283
{
284284
DateTime? potentialResult = null;
285-
foreach(var segment in marketHours.Segments.Reverse())
285+
foreach (var segment in marketHours.Segments.Reverse())
286286
{
287287
if ((time.Date + segment.Start <= localDateTime) &&
288288
(segment.State == MarketHoursState.Market || extendedMarketHours))
@@ -601,16 +601,26 @@ public LocalMarketHours GetMarketHours(DateTime localDateTime)
601601
// and add it before the segments previous to the lateOpenTime
602602
// Otherwise, just take the segments previous to the lateOpenTime
603603
List<MarketHoursSegment> segmentsLateOpen = null;
604+
var closeAllDay = false;
604605
if (hasLateOpen)
605606
{
606607
var index = 0;
607608
segmentsLateOpen = new List<MarketHoursSegment>();
608-
for(var i = 0; i < marketHoursSegments.Count; i++)
609+
for (var i = 0; i < marketHoursSegments.Count; i++)
609610
{
610611
var segment = marketHoursSegments[i];
612+
// If we're at the last segment and it ends before the late open time, the market is closed all day
613+
if (segment.End < lateOpenTime && i == marketHoursSegments.Count - 1)
614+
{
615+
closeAllDay = true;
616+
// Set index to the count to ensure TakeLast(0) later,
617+
// meaning no segments will be included after this point.
618+
index = marketHoursSegments.Count;
619+
break;
620+
}
611621
if (segment.Start <= lateOpenTime && lateOpenTime <= segment.End)
612622
{
613-
segmentsLateOpen.Add(new (segment.State, lateOpenTime, segment.End));
623+
segmentsLateOpen.Add(new(segment.State, lateOpenTime, segment.End));
614624
index = i + 1;
615625
break;
616626
}
@@ -627,7 +637,8 @@ public LocalMarketHours GetMarketHours(DateTime localDateTime)
627637

628638
// Since it could be the case we have a late open after an early close (the market resumes after the early close), we need to take
629639
// the segments before the early close and the segments after the late open and append them to obtain the expected market hours
630-
if (segmentsEarlyClose != null && hasLateOpen && earlyCloseTime <= lateOpenTime)
640+
// unless market is closed all day
641+
if (segmentsEarlyClose != null && hasLateOpen && earlyCloseTime <= lateOpenTime && !closeAllDay)
631642
{
632643
segmentsEarlyClose.AddRange(segmentsLateOpen);
633644
marketHoursSegments = segmentsEarlyClose;

Tests/Common/Securities/SecurityExchangeHoursTests.cs

Lines changed: 50 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -668,49 +668,47 @@ private static TestCaseData[] GetTestCases()
668668
new MarketHoursSegment(MarketHoursState.PreMarket, new TimeSpan(0, 0, 0), new TimeSpan(8, 30, 0)),
669669
new MarketHoursSegment(MarketHoursState.Market, new TimeSpan(8, 30, 0), new TimeSpan(12, 0, 0)))
670670
),
671-
// 2.2 Early close before market opens (should have no effect)
671+
// 2.2 Early close before market opens (should remove market segment)
672672
new TestCaseData(
673-
new DateTime(2020, 7, 3, 7, 0, 0),
673+
new DateTime(2020, 7, 3, 7, 0, 0), // Early close before open
674674
new DateTime(),
675675
new LocalMarketHours(DayOfWeek.Friday,
676676
new MarketHoursSegment(MarketHoursState.PreMarket, new TimeSpan(0, 0, 0), new TimeSpan(7, 0, 0)))
677677
),
678678
// 2.3 Early close after market closes (should have no effect)
679679
new TestCaseData(
680-
new DateTime(2020, 7, 3, 17, 0, 0),
680+
new DateTime(2020, 7, 3, 17, 0, 0), // Early close after regular close
681681
new DateTime(),
682682
new LocalMarketHours(DayOfWeek.Friday,
683683
new MarketHoursSegment(MarketHoursState.PreMarket, new TimeSpan(0, 0, 0), new TimeSpan(8, 30, 0)),
684684
new MarketHoursSegment(MarketHoursState.Market, new TimeSpan(8, 30, 0), new TimeSpan(16, 0, 0)))
685685
),
686686

687687
// 3. Late open only scenarios
688-
// 3.1 Late open during regular market hours
688+
// 3.1 Late open during regular market hours (should adjust market open)
689689
new TestCaseData(
690690
new DateTime(),
691691
new DateTime(2020, 7, 3, 10, 0, 0), // Late open at 10am
692692
new LocalMarketHours(DayOfWeek.Friday,
693693
new MarketHoursSegment(MarketHoursState.Market, new TimeSpan(10, 0, 0), new TimeSpan(16, 0, 0)))
694694
),
695-
// 3.2 Late open before market opens (should adjust pre-market)
695+
// 3.2 Late open before market opens (should delay premarket start)
696696
new TestCaseData(
697697
new DateTime(),
698-
new DateTime(2020, 7, 3, 7, 0, 0),
698+
new DateTime(2020, 7, 3, 7, 0, 0), // Late open before market
699699
new LocalMarketHours(DayOfWeek.Friday,
700700
new MarketHoursSegment(MarketHoursState.PreMarket, new TimeSpan(7, 0, 0), new TimeSpan(8, 30, 0)),
701701
new MarketHoursSegment(MarketHoursState.Market, new TimeSpan(8, 30, 0), new TimeSpan(16, 0, 0)))
702702
),
703-
// 3.3 Late open after market closes
703+
// 3.3 Late open after market close (market should be closed all day)
704704
new TestCaseData(
705705
new DateTime(),
706706
new DateTime(2020, 7, 3, 17, 0, 0), // Late open at 17
707-
new LocalMarketHours(DayOfWeek.Friday,
708-
new MarketHoursSegment(MarketHoursState.PreMarket, new TimeSpan(0, 0, 0), new TimeSpan(8, 30, 0)),
709-
new MarketHoursSegment(MarketHoursState.Market, new TimeSpan(8, 30, 0), new TimeSpan(16, 0, 0)))
707+
LocalMarketHours.ClosedAllDay(DayOfWeek.Friday)
710708
),
711709

712710
// 4. Both early close and late open scenarios
713-
// 4.1 Early close before late open (market closes early and reopens)
711+
// 4.1 Open <= Earlyclose <= Close and EarlyClose < LateOpen (market closes then reopens)
714712
new TestCaseData(
715713
new DateTime(2020, 7, 3, 12, 0, 0), // Close at noon
716714
new DateTime(2020, 7, 3, 13, 0, 0), // Reopen at 1pm
@@ -719,37 +717,70 @@ private static TestCaseData[] GetTestCases()
719717
new MarketHoursSegment(MarketHoursState.Market, new TimeSpan(8, 30, 0), new TimeSpan(12, 0, 0)),
720718
new MarketHoursSegment(MarketHoursState.Market, new TimeSpan(13, 0, 0), new TimeSpan(16, 0, 0)))
721719
),
722-
// 4.2 Early close after late open (only early close applies)
720+
// 4.2 Open <= Earlyclose <= Close and EarlyClose > LateOpen (only one market segment should exist)
723721
new TestCaseData(
724722
new DateTime(2020, 7, 3, 15, 0, 0), // Close at 3pm
725723
new DateTime(2020, 7, 3, 14, 0, 0), // Late open at 2pm
726724
new LocalMarketHours(DayOfWeek.Friday,
727725
new MarketHoursSegment(MarketHoursState.Market, new TimeSpan(14, 0, 0), new TimeSpan(15, 0, 0)))
728726
),
729-
// 4.3 Both outside market hours
727+
// 4.3 Open <= Earlyclose <= Close and LateOpen > Close (market closed all day)
728+
new TestCaseData(
729+
new DateTime(2020, 7, 3, 13, 0, 0),
730+
new DateTime(2020, 7, 3, 17, 0, 0),
731+
LocalMarketHours.ClosedAllDay(DayOfWeek.Friday)
732+
),
733+
// 4.4 Earlyclose <= Open and LateOpen > Close (market closed all day)
730734
new TestCaseData(
731-
new DateTime(2020, 7, 3, 7, 0, 0), // Before open
732-
new DateTime(2020, 7, 3, 17, 0, 0), // After close
735+
new DateTime(2020, 7, 3, 7, 0, 0),
736+
new DateTime(2020, 7, 3, 17, 0, 0),
737+
LocalMarketHours.ClosedAllDay(DayOfWeek.Friday)
738+
),
739+
// 4.5 Earlyclose <= Open and EarlyClose < LateOpen <= Close
740+
new TestCaseData(
741+
new DateTime(2020, 7, 3, 7, 0, 0),
742+
new DateTime(2020, 7, 3, 14, 0, 0),
733743
new LocalMarketHours(DayOfWeek.Friday,
734744
new MarketHoursSegment(MarketHoursState.PreMarket, new TimeSpan(0, 0, 0), new TimeSpan(7, 0, 0)),
735-
new MarketHoursSegment(MarketHoursState.PreMarket, new TimeSpan(0, 0, 0), new TimeSpan(8, 30, 0)),
736-
new MarketHoursSegment(MarketHoursState.Market, new TimeSpan(8, 30, 0), new TimeSpan(16, 0, 0)))
745+
new MarketHoursSegment(MarketHoursState.Market, new TimeSpan(14, 0, 0), new TimeSpan(16, 0, 0)))
746+
),
747+
// 4.6 LateOpen < Earlyclose <= Open
748+
new TestCaseData(
749+
new DateTime(2020, 7, 3, 7, 0, 0),
750+
new DateTime(2020, 7, 3, 6, 0, 0),
751+
new LocalMarketHours(DayOfWeek.Friday,
752+
new MarketHoursSegment(MarketHoursState.PreMarket, new TimeSpan(6, 0, 0), new TimeSpan(7, 0, 0)))
737753
),
738754

739755
// 5. Edge cases
740-
// 5.1 Early close exactly at market open
756+
// 5.1 Early close exactly at market open (no market segment)
741757
new TestCaseData(
742758
new DateTime(2020, 7, 3, 8, 30, 0),
743759
new DateTime(),
744760
new LocalMarketHours(DayOfWeek.Friday,
745761
new MarketHoursSegment(MarketHoursState.PreMarket, new TimeSpan(0, 0, 0), new TimeSpan(8, 30, 0)))
746762
),
747-
// 5.2 Late open exactly at market close
763+
// 5.2 Late open exactly at market close (market segment has zero duration)
748764
new TestCaseData(
749765
new DateTime(),
750766
new DateTime(2020, 7, 3, 16, 0, 0),
751767
new LocalMarketHours(DayOfWeek.Friday,
752768
new MarketHoursSegment(MarketHoursState.Market, new TimeSpan(16, 0, 0), new TimeSpan(16, 0, 0)))
769+
),
770+
// 5.3 Early close and late open at the same time (split into two segments with zero-duration overlap)
771+
new TestCaseData(
772+
new DateTime(2020, 7, 3, 13, 0, 0),
773+
new DateTime(2020, 7, 3, 13, 0, 0),
774+
new LocalMarketHours(DayOfWeek.Friday,
775+
new MarketHoursSegment(MarketHoursState.PreMarket, new TimeSpan(0, 0, 0), new TimeSpan(8, 30, 0)),
776+
new MarketHoursSegment(MarketHoursState.Market, new TimeSpan(8, 30, 0), new TimeSpan(13, 0, 0)),
777+
new MarketHoursSegment(MarketHoursState.Market, new TimeSpan(13, 0, 0), new TimeSpan(13, 0, 0)))
778+
),
779+
// 5.4 EarlyOpen > Close and LateOpen > Close (market closed all day)
780+
new TestCaseData(
781+
new DateTime(2020, 7, 3, 17, 0, 0),
782+
new DateTime(2020, 7, 3, 17, 0, 0),
783+
LocalMarketHours.ClosedAllDay(DayOfWeek.Friday)
753784
)
754785
};
755786
}

0 commit comments

Comments
 (0)