813 lines
32 KiB
C#
813 lines
32 KiB
C#
/*
|
|
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
|
|
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*
|
|
*/
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using NodaTime;
|
|
using NUnit.Framework;
|
|
using Python.Runtime;
|
|
using QuantConnect.Data;
|
|
using QuantConnect.Data.Market;
|
|
using QuantConnect.Scheduling;
|
|
using QuantConnect.Securities;
|
|
|
|
namespace QuantConnect.Tests.Common.Scheduling
|
|
{
|
|
[TestFixture, Parallelizable(ParallelScope.All)]
|
|
public class TimeRulesTests
|
|
{
|
|
private static DateTime _utcNow = new DateTime(2021, 07, 27, 1, 10, 10, 500);
|
|
|
|
[Test]
|
|
public void AtSpecificTimeFromUtc()
|
|
{
|
|
var rules = GetTimeRules(TimeZones.Utc);
|
|
var rule = rules.At(TimeSpan.FromHours(12));
|
|
var times = rule.CreateUtcEventTimes(new[] { new DateTime(2000, 01, 01) });
|
|
|
|
int count = 0;
|
|
foreach (var time in times)
|
|
{
|
|
count++;
|
|
Assert.AreEqual(TimeSpan.FromHours(12), time.TimeOfDay);
|
|
}
|
|
Assert.AreEqual(1, count);
|
|
}
|
|
|
|
[Test]
|
|
public void AtSpecificTimeFromNonUtc()
|
|
{
|
|
var rules = GetTimeRules(TimeZones.NewYork);
|
|
var rule = rules.At(TimeSpan.FromHours(12));
|
|
var times = rule.CreateUtcEventTimes(new[] { new DateTime(2000, 01, 01) });
|
|
|
|
int count = 0;
|
|
foreach (var time in times)
|
|
{
|
|
count++;
|
|
Assert.AreEqual(TimeSpan.FromHours(12 + 5), time.TimeOfDay);
|
|
}
|
|
Assert.AreEqual(1, count);
|
|
}
|
|
|
|
[Test]
|
|
public void RegularMarketOpenNoDelta()
|
|
{
|
|
var rules = GetTimeRules(TimeZones.Utc);
|
|
var rule = rules.AfterMarketOpen(Symbols.SPY);
|
|
var times = rule.CreateUtcEventTimes(new[] { new DateTime(2000, 01, 03) });
|
|
|
|
int count = 0;
|
|
foreach (var time in times)
|
|
{
|
|
count++;
|
|
Assert.AreEqual(TimeSpan.FromHours(9.5 + 5), time.TimeOfDay);
|
|
}
|
|
Assert.AreEqual(1, count);
|
|
}
|
|
|
|
[Test]
|
|
public void RegularMarketOpenWithDelta()
|
|
{
|
|
var rules = GetTimeRules(TimeZones.Utc);
|
|
var rule = rules.AfterMarketOpen(Symbols.SPY, 30);
|
|
var times = rule.CreateUtcEventTimes(new[] { new DateTime(2000, 01, 03) });
|
|
|
|
int count = 0;
|
|
foreach (var time in times)
|
|
{
|
|
count++;
|
|
Assert.AreEqual(TimeSpan.FromHours(9.5 + 5 + .5), time.TimeOfDay);
|
|
}
|
|
Assert.AreEqual(1, count);
|
|
}
|
|
|
|
[Test]
|
|
public void ExtendedMarketOpenNoDelta()
|
|
{
|
|
var rules = GetTimeRules(TimeZones.Utc);
|
|
var rule = rules.AfterMarketOpen(Symbols.SPY, 0, true);
|
|
var times = rule.CreateUtcEventTimes(new[] { new DateTime(2000, 01, 03) });
|
|
|
|
int count = 0;
|
|
foreach (var time in times)
|
|
{
|
|
count++;
|
|
Assert.AreEqual(TimeSpan.FromHours(4 + 5), time.TimeOfDay);
|
|
}
|
|
Assert.AreEqual(1, count);
|
|
}
|
|
|
|
[Test]
|
|
public void RegularMarketOpenNoDeltaForContinuousSchedules()
|
|
{
|
|
var rules = GetFutureTimeRules(TimeZones.Utc);
|
|
var rule = rules.AfterMarketOpen(Symbols.ES_Future_Chain, 0);
|
|
var times = rule.CreateUtcEventTimes(new[] {
|
|
new DateTime(2022, 01, 03),
|
|
new DateTime(2022, 01, 04),
|
|
new DateTime(2022, 01, 05),
|
|
new DateTime(2022, 01, 06),
|
|
new DateTime(2022, 01, 07),
|
|
new DateTime(2022, 01, 10)
|
|
});
|
|
|
|
var expectedMarketOpenDates = new[] {
|
|
new DateTime(2022, 01, 03, 14, 30, 0),
|
|
new DateTime(2022, 01, 04, 14, 30, 0),
|
|
new DateTime(2022, 01, 05, 14, 30, 0),
|
|
new DateTime(2022, 01, 06, 14, 30, 0),
|
|
new DateTime(2022, 01, 07, 14, 30, 0),
|
|
new DateTime(2022, 01, 10, 14, 30, 0)
|
|
};
|
|
int count = 0;
|
|
foreach (var time in times)
|
|
{
|
|
Assert.AreEqual(expectedMarketOpenDates[count], time);
|
|
count++;
|
|
}
|
|
Assert.AreEqual(6, count);
|
|
}
|
|
|
|
[Test]
|
|
public void ExtendedMarketOpenNoDeltaForContinuousSchedules()
|
|
{
|
|
var rules = GetFutureTimeRules(TimeZones.Utc, true);
|
|
var rule = rules.AfterMarketOpen(Symbols.ES_Future_Chain, 0, true);
|
|
var times = rule.CreateUtcEventTimes(new[] {
|
|
new DateTime(2022, 01, 01),
|
|
new DateTime(2022, 01, 02),
|
|
new DateTime(2022, 01, 03),
|
|
new DateTime(2022, 01, 04),
|
|
new DateTime(2022, 01, 05),
|
|
new DateTime(2022, 01, 06),
|
|
new DateTime(2022, 01, 07),
|
|
new DateTime(2022, 01, 08),
|
|
new DateTime(2022, 01, 09)
|
|
});
|
|
|
|
var expectedMarketOpenDates = new[] {
|
|
new DateTime(2022, 01, 02, 23, 0, 0), // sunday 6pm
|
|
// market is open for the whole day, so goes from midnight to midnight
|
|
new DateTime(2022, 01, 03, 5, 0, 0),
|
|
new DateTime(2022, 01, 04, 5, 0, 0),
|
|
new DateTime(2022, 01, 05, 5, 0, 0),
|
|
new DateTime(2022, 01, 06, 5, 0, 0),
|
|
new DateTime(2022, 01, 07, 5, 0, 0),
|
|
new DateTime(2022, 01, 09, 23, 0, 0) // sunday 6pm
|
|
};
|
|
int count = 0;
|
|
foreach (var time in times)
|
|
{
|
|
Assert.AreEqual(expectedMarketOpenDates[count], time);
|
|
count++;
|
|
}
|
|
Assert.AreEqual(7, count);
|
|
}
|
|
|
|
[TestCase(true, 9 - 0.5)]
|
|
[TestCase(false, 14.5 - 0.5)]
|
|
public void BeforeMarketOpenWithDelta(bool extendedMarketHours, double expectedHour)
|
|
{
|
|
var rules = GetTimeRules(TimeZones.Utc);
|
|
var rule = rules.BeforeMarketOpen(Symbols.SPY, 30, extendedMarketHours);
|
|
var times = rule.CreateUtcEventTimes(new[] { new DateTime(2000, 01, 03) });
|
|
var type = extendedMarketHours ? "ExtendedMarketOpen" : "MarketOpen";
|
|
Assert.AreEqual($"{Symbols.SPY}: 30 min before {type}", rule.Name);
|
|
|
|
int count = 0;
|
|
foreach (var time in times)
|
|
{
|
|
count++;
|
|
Assert.AreEqual(TimeSpan.FromHours(expectedHour), time.TimeOfDay);
|
|
}
|
|
Assert.AreEqual(1, count);
|
|
}
|
|
|
|
[Test]
|
|
public void ExtendedMarketCloseNoDeltaForContinuousSchedules()
|
|
{
|
|
var rules = GetTimeRules(TimeZones.Utc);
|
|
var rule = rules.BeforeMarketClose(Symbols.SPY, 0, true);
|
|
var times = rule.CreateUtcEventTimes(new[] {
|
|
new DateTime(2022, 01, 01),
|
|
new DateTime(2022, 01, 02),
|
|
new DateTime(2022, 01, 03),
|
|
new DateTime(2022, 01, 04),
|
|
new DateTime(2022, 01, 05),
|
|
new DateTime(2022, 01, 06),
|
|
new DateTime(2022, 01, 07),
|
|
new DateTime(2022, 01, 08),
|
|
new DateTime(2022, 01, 09)
|
|
});
|
|
|
|
var expectedMarketOpenDates = new[] {
|
|
new DateTime(2022, 01, 04, 01, 00, 00),
|
|
new DateTime(2022, 01, 05, 01, 00, 00),
|
|
new DateTime(2022, 01, 06, 01, 00, 00),
|
|
new DateTime(2022, 01, 07, 01, 00, 00),
|
|
new DateTime(2022, 01, 08, 01, 00, 00)
|
|
};
|
|
int count = 0;
|
|
foreach (var time in times)
|
|
{
|
|
Assert.AreEqual(expectedMarketOpenDates[count], time);
|
|
count++;
|
|
}
|
|
Assert.AreEqual(5, count);
|
|
}
|
|
|
|
[Test]
|
|
public void ExtendedMarketOpenWithDelta()
|
|
{
|
|
var rules = GetTimeRules(TimeZones.Utc);
|
|
var rule = rules.AfterMarketOpen(Symbols.SPY, 30, true);
|
|
var times = rule.CreateUtcEventTimes(new[] { new DateTime(2000, 01, 03) });
|
|
|
|
int count = 0;
|
|
foreach (var time in times)
|
|
{
|
|
count++;
|
|
Assert.AreEqual(TimeSpan.FromHours(4 + 5 + .5), time.TimeOfDay);
|
|
}
|
|
Assert.AreEqual(1, count);
|
|
}
|
|
|
|
[TestCase(true)]
|
|
[TestCase(false)]
|
|
public void MultipleMarketOpen(bool extendedMarketHours)
|
|
{
|
|
var symbol = Symbols.CreateFutureSymbol("HSI", new DateTime(2025, 01, 27));
|
|
var rules = GetTimeRules(TimeZones.Utc);
|
|
var rule = rules.AfterMarketOpen(symbol, extendedMarketOpen: extendedMarketHours);
|
|
var times = rule.CreateUtcEventTimes(new[] { new DateTime(2000, 01, 04) });
|
|
|
|
int count = 0;
|
|
foreach (var time in times)
|
|
{
|
|
count++;
|
|
if (extendedMarketHours)
|
|
{
|
|
Assert.AreEqual(TimeSpan.FromHours(24 - 8), time.TimeOfDay);
|
|
}
|
|
else
|
|
{
|
|
Assert.AreEqual(TimeSpan.FromHours(9.25 - 8), time.TimeOfDay);
|
|
}
|
|
}
|
|
Assert.AreEqual(1, count);
|
|
}
|
|
|
|
[TestCase(true)]
|
|
[TestCase(false)]
|
|
public void MultipleMarketClosure(bool extendedMarketHours)
|
|
{
|
|
var symbol = Symbols.CreateFutureSymbol("HSI", new DateTime(2025, 01, 27));
|
|
var rules = GetTimeRules(TimeZones.Utc);
|
|
var rule = rules.BeforeMarketClose(symbol, extendedMarketClose: extendedMarketHours);
|
|
var times = rule.CreateUtcEventTimes(new[] { new DateTime(2000, 01, 03) });
|
|
|
|
int count = 0;
|
|
foreach (var time in times)
|
|
{
|
|
count++;
|
|
if (extendedMarketHours)
|
|
{
|
|
Assert.AreEqual(TimeSpan.FromHours(24 - 8), time.TimeOfDay);
|
|
}
|
|
else
|
|
{
|
|
Assert.AreEqual(TimeSpan.FromHours(16.5 - 8), time.TimeOfDay);
|
|
}
|
|
}
|
|
Assert.AreEqual(1, count);
|
|
}
|
|
|
|
[Test]
|
|
public void RegularMarketCloseNoDelta()
|
|
{
|
|
var rules = GetTimeRules(TimeZones.Utc);
|
|
var rule = rules.BeforeMarketClose(Symbols.SPY);
|
|
var times = rule.CreateUtcEventTimes(new[] { new DateTime(2000, 01, 03) });
|
|
|
|
int count = 0;
|
|
foreach (var time in times)
|
|
{
|
|
count++;
|
|
Assert.AreEqual(TimeSpan.FromHours(16 + 5), time.TimeOfDay);
|
|
}
|
|
Assert.AreEqual(1, count);
|
|
}
|
|
|
|
[Test]
|
|
public void RegularMarketCloseWithDelta()
|
|
{
|
|
var rules = GetTimeRules(TimeZones.Utc);
|
|
var rule = rules.BeforeMarketClose(Symbols.SPY, 30);
|
|
var times = rule.CreateUtcEventTimes(new[] { new DateTime(2000, 01, 03) });
|
|
|
|
int count = 0;
|
|
foreach (var time in times)
|
|
{
|
|
count++;
|
|
Assert.AreEqual(TimeSpan.FromHours(16 + 5 - .5), time.TimeOfDay);
|
|
}
|
|
Assert.AreEqual(1, count);
|
|
}
|
|
|
|
[Test]
|
|
public void ExtendedMarketCloseNoDelta()
|
|
{
|
|
var rules = GetTimeRules(TimeZones.Utc);
|
|
var rule = rules.BeforeMarketClose(Symbols.SPY, 0, true);
|
|
var times = rule.CreateUtcEventTimes(new[] { new DateTime(2000, 01, 03) });
|
|
|
|
int count = 0;
|
|
foreach (var time in times)
|
|
{
|
|
count++;
|
|
Assert.AreEqual(TimeSpan.FromHours((20 + 5) % 24), time.TimeOfDay);
|
|
}
|
|
Assert.AreEqual(1, count);
|
|
}
|
|
|
|
[Test]
|
|
public void ExtendedMarketCloseWithDelta()
|
|
{
|
|
var rules = GetTimeRules(TimeZones.Utc);
|
|
var rule = rules.BeforeMarketClose(Symbols.SPY, 30, true);
|
|
var times = rule.CreateUtcEventTimes(new[] { new DateTime(2000, 01, 03) });
|
|
|
|
int count = 0;
|
|
foreach (var time in times)
|
|
{
|
|
count++;
|
|
Assert.AreEqual(TimeSpan.FromHours((20 + 5 - .5) % 24), time.TimeOfDay);
|
|
}
|
|
Assert.AreEqual(1, count);
|
|
}
|
|
|
|
[TestCase(true, (21 + 4 + .5) % 24)]
|
|
[TestCase(false, (21 + .5) % 24)]
|
|
public void AfterMarketCloseWithDelta(bool extendedMarketHours, double expectedHour)
|
|
{
|
|
var rules = GetTimeRules(TimeZones.Utc);
|
|
var rule = rules.AfterMarketClose(Symbols.SPY, 30, extendedMarketHours);
|
|
var times = rule.CreateUtcEventTimes(new[] { new DateTime(2000, 01, 03) });
|
|
var type = extendedMarketHours ? "ExtendedMarketClose" : "MarketClose";
|
|
Assert.AreEqual($"{Symbols.SPY}: 30 min after {type}", rule.Name);
|
|
|
|
int count = 0;
|
|
foreach (var time in times)
|
|
{
|
|
count++;
|
|
Assert.AreEqual(TimeSpan.FromHours(expectedHour), time.TimeOfDay);
|
|
}
|
|
Assert.AreEqual(1, count);
|
|
}
|
|
|
|
[Test]
|
|
public void NoSymbolMarketOpenDefaultsToSpyWhenNoSecurities()
|
|
{
|
|
var rules = GetEmptyTimeRules(TimeZones.Utc);
|
|
var rule = rules.AfterMarketOpen(30);
|
|
var times = rule.CreateUtcEventTimes([new DateTime(2000, 01, 03)]);
|
|
|
|
int count = 0;
|
|
foreach (var time in times)
|
|
{
|
|
count++;
|
|
Assert.AreEqual(TimeSpan.FromHours(9.5 + 5 + .5), time.TimeOfDay);
|
|
}
|
|
Assert.AreEqual(1, count);
|
|
}
|
|
|
|
[Test]
|
|
public void NoSymbolMarketCloseDefaultsToSpyWhenNoSecurities()
|
|
{
|
|
var rules = GetEmptyTimeRules(TimeZones.Utc);
|
|
var rule = rules.BeforeMarketClose(30);
|
|
var times = rule.CreateUtcEventTimes([new DateTime(2000, 01, 03)]);
|
|
|
|
int count = 0;
|
|
foreach (var time in times)
|
|
{
|
|
count++;
|
|
Assert.AreEqual(TimeSpan.FromHours(16 + 5 - .5), time.TimeOfDay);
|
|
}
|
|
Assert.AreEqual(1, count);
|
|
}
|
|
|
|
[Test]
|
|
public void NoSymbolMarketOpenIgnoresAlwaysOpenAndUsesSpy()
|
|
{
|
|
// Only an always-open (24/7) security is subscribed: it should be skipped and SPY used as fallback.
|
|
var rules = GetEmptyTimeRules(TimeZones.Utc);
|
|
AddAlwaysOpenSecurity(rules);
|
|
|
|
var rule = rules.AfterMarketOpen();
|
|
var times = rule.CreateUtcEventTimes([new DateTime(2000, 01, 03)]);
|
|
|
|
int count = 0;
|
|
foreach (var time in times)
|
|
{
|
|
count++;
|
|
Assert.AreEqual(TimeSpan.FromHours(9.5 + 5), time.TimeOfDay);
|
|
}
|
|
Assert.AreEqual(1, count);
|
|
}
|
|
|
|
[Test]
|
|
public void NoSymbolMarketOpenWithEquityAndExtendedFuturePicksFuture()
|
|
{
|
|
// SPY extended opens at 4:00 NY = 9:00 UTC; ES Future extended opens at 0:00 NY = 5:00 UTC. ES wins.
|
|
var rules = GetEmptyTimeRules(TimeZones.Utc);
|
|
AddSecurity(rules, Symbols.SPY);
|
|
AddSecurity(rules, Symbols.ES_Future_Chain, extendedMarketHours: true);
|
|
|
|
var rule = rules.AfterMarketOpen(0, extendedMarketOpen: true);
|
|
var times = rule.CreateUtcEventTimes([new DateTime(2022, 01, 03)]).ToList();
|
|
|
|
Assert.AreEqual(1, times.Count);
|
|
Assert.AreEqual(new DateTime(2022, 01, 03, 5, 0, 0), times[0]);
|
|
}
|
|
|
|
[Test]
|
|
public void NoSymbolMarketCloseWithEquityAndExtendedFuturePicksFuture()
|
|
{
|
|
// SPY extended closes at 20:00 NY = 01:00 UTC next day; ES Future extended closes at 24:00 NY = 5:00 UTC next day. ES wins.
|
|
var rules = GetEmptyTimeRules(TimeZones.Utc);
|
|
AddSecurity(rules, Symbols.SPY);
|
|
AddSecurity(rules, Symbols.ES_Future_Chain, extendedMarketHours: true);
|
|
|
|
var rule = rules.BeforeMarketClose(0, extendedMarketClose: true);
|
|
var times = rule.CreateUtcEventTimes([new DateTime(2022, 01, 03)]).ToList();
|
|
|
|
Assert.AreEqual(1, times.Count);
|
|
Assert.AreEqual(new DateTime(2022, 01, 04, 5, 0, 0), times[0]);
|
|
}
|
|
|
|
[Test]
|
|
public void NoSymbolMarketOpenWithEquityAndForexPicksForex()
|
|
{
|
|
// EURUSD (Oanda) on Mon Jan 3, 2022 first daily open is 0:00 NY = 5:00 UTC; SPY regular opens 9:30 NY = 14:30 UTC. Forex wins.
|
|
var rules = GetEmptyTimeRules(TimeZones.Utc);
|
|
AddSecurity(rules, Symbols.SPY);
|
|
AddSecurity(rules, Symbols.EURUSD);
|
|
|
|
var rule = rules.AfterMarketOpen();
|
|
var times = rule.CreateUtcEventTimes([new DateTime(2022, 01, 03)]).ToList();
|
|
|
|
Assert.AreEqual(1, times.Count);
|
|
Assert.AreEqual(new DateTime(2022, 01, 03, 5, 0, 0), times[0]);
|
|
}
|
|
|
|
[Test]
|
|
public void NoSymbolMarketCloseWithEquityAndForexPicksForex()
|
|
{
|
|
// EURUSD last daily close on Mon Jan 3, 2022 is 24:00 NY = 5:00 UTC next day; SPY regular closes 16:00 NY = 21:00 UTC. Forex wins.
|
|
var rules = GetEmptyTimeRules(TimeZones.Utc);
|
|
AddSecurity(rules, Symbols.SPY);
|
|
AddSecurity(rules, Symbols.EURUSD);
|
|
|
|
var rule = rules.BeforeMarketClose();
|
|
var times = rule.CreateUtcEventTimes([new DateTime(2022, 01, 03)]).ToList();
|
|
|
|
Assert.AreEqual(1, times.Count);
|
|
Assert.AreEqual(new DateTime(2022, 01, 04, 5, 0, 0), times[0]);
|
|
}
|
|
|
|
[Test]
|
|
public void NoSymbolMarketOpenWithEquityAndCfdPicksCfd()
|
|
{
|
|
// XAUUSD (Oanda CFD) Mon first daily open is 0:00 NY = 5:00 UTC; SPY regular opens 14:30 UTC. CFD wins.
|
|
var rules = GetEmptyTimeRules(TimeZones.Utc);
|
|
AddSecurity(rules, Symbols.SPY);
|
|
AddSecurity(rules, Symbols.XAUUSD);
|
|
|
|
var rule = rules.AfterMarketOpen();
|
|
var times = rule.CreateUtcEventTimes([new DateTime(2022, 01, 03)]).ToList();
|
|
|
|
Assert.AreEqual(1, times.Count);
|
|
Assert.AreEqual(new DateTime(2022, 01, 03, 5, 0, 0), times[0]);
|
|
}
|
|
|
|
[Test]
|
|
public void NoSymbolMarketCloseWithEquityAndCfdPicksCfd()
|
|
{
|
|
// XAUUSD last daily close on Mon is 24:00 NY = 5:00 UTC next day; SPY regular closes 21:00 UTC. CFD wins.
|
|
var rules = GetEmptyTimeRules(TimeZones.Utc);
|
|
AddSecurity(rules, Symbols.SPY);
|
|
AddSecurity(rules, Symbols.XAUUSD);
|
|
|
|
var rule = rules.BeforeMarketClose();
|
|
var times = rule.CreateUtcEventTimes([new DateTime(2022, 01, 03)]).ToList();
|
|
|
|
Assert.AreEqual(1, times.Count);
|
|
Assert.AreEqual(new DateTime(2022, 01, 04, 5, 0, 0), times[0]);
|
|
}
|
|
|
|
[Test]
|
|
public void NoSymbolMarketOpenWithEquityAndRegularFutureMatchesEquity()
|
|
{
|
|
// SPY and ES regular sessions both open at 9:30 NY = 14:30 UTC on Mon Jan 3, 2022.
|
|
var rules = GetEmptyTimeRules(TimeZones.Utc);
|
|
AddSecurity(rules, Symbols.SPY);
|
|
AddSecurity(rules, Symbols.ES_Future_Chain);
|
|
|
|
var rule = rules.AfterMarketOpen();
|
|
var times = rule.CreateUtcEventTimes([new DateTime(2022, 01, 03)]).ToList();
|
|
|
|
Assert.AreEqual(1, times.Count);
|
|
Assert.AreEqual(new DateTime(2022, 01, 03, 14, 30, 0), times[0]);
|
|
}
|
|
|
|
[Test]
|
|
public void NoSymbolMarketCloseWithEquityAndRegularFuturePicksFuture()
|
|
{
|
|
// SPY regular closes 16:00 NY = 21:00 UTC; ES regular closes 17:00 NY = 22:00 UTC. ES wins.
|
|
var rules = GetEmptyTimeRules(TimeZones.Utc);
|
|
AddSecurity(rules, Symbols.SPY);
|
|
AddSecurity(rules, Symbols.ES_Future_Chain);
|
|
|
|
var rule = rules.BeforeMarketClose();
|
|
var times = rule.CreateUtcEventTimes([new DateTime(2022, 01, 03)]).ToList();
|
|
|
|
Assert.AreEqual(1, times.Count);
|
|
Assert.AreEqual(new DateTime(2022, 01, 03, 22, 0, 0), times[0]);
|
|
}
|
|
|
|
[Test]
|
|
public void NoSymbolMarketOpenWithEquityAndEquityOptionMatches()
|
|
{
|
|
// SPY equity and SPY options share regular hours: 9:30 NY = 14:30 UTC.
|
|
var rules = GetEmptyTimeRules(TimeZones.Utc);
|
|
AddSecurity(rules, Symbols.SPY);
|
|
AddSecurity(rules, Symbols.SPY_Option_Chain);
|
|
|
|
var rule = rules.AfterMarketOpen();
|
|
var times = rule.CreateUtcEventTimes([new DateTime(2022, 01, 03)]).ToList();
|
|
|
|
Assert.AreEqual(1, times.Count);
|
|
Assert.AreEqual(new DateTime(2022, 01, 03, 14, 30, 0), times[0]);
|
|
}
|
|
|
|
[Test]
|
|
public void NoSymbolMarketCloseWithEquityAndEquityOptionMatches()
|
|
{
|
|
// SPY equity and SPY options share regular close: 16:00 NY = 21:00 UTC.
|
|
var rules = GetEmptyTimeRules(TimeZones.Utc);
|
|
AddSecurity(rules, Symbols.SPY);
|
|
AddSecurity(rules, Symbols.SPY_Option_Chain);
|
|
|
|
var rule = rules.BeforeMarketClose();
|
|
var times = rule.CreateUtcEventTimes([new DateTime(2022, 01, 03)]).ToList();
|
|
|
|
Assert.AreEqual(1, times.Count);
|
|
Assert.AreEqual(new DateTime(2022, 01, 03, 21, 0, 0), times[0]);
|
|
}
|
|
|
|
[Test]
|
|
public void NoSymbolMarketOpenWithEquityAndIndexOptionMatches()
|
|
{
|
|
// SPX index option regular opens 8:30 CT = 14:30 UTC, same as SPY equity 14:30 UTC.
|
|
var rules = GetEmptyTimeRules(TimeZones.Utc);
|
|
AddSecurity(rules, Symbols.SPY);
|
|
AddSecurity(rules, Symbol.CreateCanonicalOption(Symbols.SPX));
|
|
|
|
var rule = rules.AfterMarketOpen();
|
|
var times = rule.CreateUtcEventTimes([new DateTime(2022, 01, 03)]).ToList();
|
|
|
|
Assert.AreEqual(1, times.Count);
|
|
Assert.AreEqual(new DateTime(2022, 01, 03, 14, 30, 0), times[0]);
|
|
}
|
|
|
|
[Test]
|
|
public void GetMarketOpenCloseExchangeHoursDedupesOptionContracts()
|
|
{
|
|
var rules = GetEmptyTimeRules(TimeZones.Utc);
|
|
var expiry = new DateTime(2024, 01, 19);
|
|
for (var strike = 100; strike < 110; strike++)
|
|
{
|
|
var contract = Symbol.CreateOption(Symbols.SPY, Market.USA, OptionStyle.American,
|
|
OptionRight.Call, strike, expiry);
|
|
AddSecurity(rules, contract);
|
|
}
|
|
|
|
var method = typeof(BaseScheduleRules).GetMethod("GetMarketOpenCloseExchangeHours",
|
|
BindingFlags.NonPublic | BindingFlags.Instance);
|
|
var hours = ((IEnumerable<SecurityExchangeHours>)method.Invoke(rules, null)).ToList();
|
|
|
|
// Expected 2: SPY equity hours (pre-seeded fallback) + SPY option hours (10 contracts deduped to one).
|
|
Assert.AreEqual(2, hours.Count);
|
|
}
|
|
|
|
[Test]
|
|
public void NoSymbolMarketCloseWithEquityAndIndexOptionPicksIndexOption()
|
|
{
|
|
// SPY regular closes 16:00 NY = 21:00 UTC; SPX index option closes 15:15 CT = 21:15 UTC. SPX wins.
|
|
var rules = GetEmptyTimeRules(TimeZones.Utc);
|
|
AddSecurity(rules, Symbols.SPY);
|
|
AddSecurity(rules, Symbol.CreateCanonicalOption(Symbols.SPX));
|
|
|
|
var rule = rules.BeforeMarketClose();
|
|
var times = rule.CreateUtcEventTimes([new DateTime(2022, 01, 03)]).ToList();
|
|
|
|
Assert.AreEqual(1, times.Count);
|
|
Assert.AreEqual(new DateTime(2022, 01, 03, 21, 15, 0), times[0]);
|
|
}
|
|
|
|
[TestCase(0)]
|
|
[TestCase(-1)]
|
|
public void EveryValidatesTimeSpan(int timeSpanMinutes)
|
|
{
|
|
var rules = GetTimeRules(TimeZones.Utc);
|
|
Assert.Throws<ArgumentException>(() => rules.Every(TimeSpan.FromMinutes(timeSpanMinutes)));
|
|
}
|
|
|
|
[Test]
|
|
public void Every()
|
|
{
|
|
var rules = GetTimeRules(TimeZones.Utc);
|
|
var every = rules.Every(TimeSpan.FromMilliseconds(500));
|
|
var previous = new DateTime(2019, 6, 28);
|
|
var dateTimes = every.CreateUtcEventTimes(new[] { previous });
|
|
foreach (var dateTime in dateTimes)
|
|
{
|
|
if (previous != dateTime)
|
|
{
|
|
Assert.Fail("Unexpected Every DateTime");
|
|
}
|
|
previous = previous.AddMilliseconds(500);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void SetTimeZone()
|
|
{
|
|
var rules = GetTimeRules(TimeZones.NewYork);
|
|
var nowNewYork = rules.Now.CreateUtcEventTimes(new [] { _utcNow.Date }).Single();
|
|
|
|
rules.SetDefaultTimeZone(TimeZones.Utc);
|
|
|
|
var nowUtc = rules.Now.CreateUtcEventTimes(new [] { _utcNow.Date }).Single();
|
|
|
|
Assert.AreEqual(_utcNow, nowUtc);
|
|
Assert.AreEqual(nowUtc, nowNewYork);
|
|
}
|
|
|
|
[Test]
|
|
public void SetFuncTimeRuleInPythonWorksAsExpected()
|
|
{
|
|
using (Py.GIL())
|
|
{
|
|
var pythonModule = PyModule.FromString("testModule", @"
|
|
from AlgorithmImports import *
|
|
|
|
def CustomTimeRule(dates):
|
|
return [dates[0] + timedelta(days=1)]
|
|
");
|
|
dynamic pythonCustomTimeRule = pythonModule.GetAttr("CustomTimeRule");
|
|
var funcTimeRule = new FuncTimeRule("PythonFuncTimeRule", pythonCustomTimeRule);
|
|
Assert.AreEqual("PythonFuncTimeRule", funcTimeRule.Name);
|
|
Assert.AreEqual(new DateTime(2023, 1, 2, 0, 0, 0), funcTimeRule.CreateUtcEventTimes(new List<DateTime>() { new DateTime(2023, 1, 1) }).First());
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void SetFuncTimeRuleInPythonWorksAsExpectedWithCSharpFunc()
|
|
{
|
|
using (Py.GIL())
|
|
{
|
|
var pythonModule = PyModule.FromString("testModule", @"
|
|
from AlgorithmImports import *
|
|
|
|
def GetFuncTimeRule(csharpFunc):
|
|
return FuncTimeRule(""CSharp"", csharpFunc)
|
|
");
|
|
dynamic getFuncTimeRule = pythonModule.GetAttr("GetFuncTimeRule");
|
|
Func<IEnumerable<DateTime>, IEnumerable<DateTime>> csharpFunc = (dates) => { return new List<DateTime>() { new DateTime(2001, 3, 18) }; };
|
|
var funcTimeRule = getFuncTimeRule(csharpFunc);
|
|
Assert.AreEqual("CSharp", (funcTimeRule.Name as PyObject).GetAndDispose<string>());
|
|
Assert.AreEqual(new DateTime(2001, 3, 18),
|
|
(funcTimeRule.CreateUtcEventTimes(new List<DateTime>() { new DateTime(2023, 1, 1) }) as PyObject).GetAndDispose<List<DateTime>>().First());
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void SetFuncTimeRuleInPythonFailsWhenInvalidTimeRule()
|
|
{
|
|
using (Py.GIL())
|
|
{
|
|
var pythonModule = PyModule.FromString("testModule", @"
|
|
from AlgorithmImports import *
|
|
|
|
wrongCustomTimeRule = ""hello""
|
|
");
|
|
dynamic pythonCustomTimeRule = pythonModule.GetAttr("wrongCustomTimeRule");
|
|
Assert.Throws<ArgumentException>(() => new FuncTimeRule("PythonFuncTimeRule", pythonCustomTimeRule));
|
|
}
|
|
}
|
|
|
|
private static TimeRules GetEmptyTimeRules(DateTimeZone dateTimeZone)
|
|
{
|
|
var timeKeeper = new TimeKeeper(_utcNow, new List<DateTimeZone>());
|
|
var manager = new SecurityManager(timeKeeper);
|
|
var mhdb = MarketHoursDatabase.FromDataFolder();
|
|
return new TimeRules(null, manager, dateTimeZone, mhdb);
|
|
}
|
|
|
|
private static SecurityManager GetSecurityManager(TimeRules rules)
|
|
{
|
|
var prop = typeof(BaseScheduleRules).GetProperty("Securities", BindingFlags.NonPublic | BindingFlags.Instance);
|
|
return (SecurityManager)prop.GetValue(rules);
|
|
}
|
|
|
|
private static void AddSecurity(TimeRules rules, Symbol symbol, bool extendedMarketHours = false)
|
|
{
|
|
var manager = GetSecurityManager(rules);
|
|
var mhdb = MarketHoursDatabase.FromDataFolder();
|
|
var entry = mhdb.GetEntry(symbol.ID.Market, symbol, symbol.SecurityType);
|
|
var config = new SubscriptionDataConfig(typeof(TradeBar), symbol, Resolution.Daily,
|
|
entry.DataTimeZone, entry.ExchangeHours.TimeZone, true, extendedMarketHours, false);
|
|
manager.Add(symbol, new Security(
|
|
entry.ExchangeHours,
|
|
config,
|
|
new Cash(Currencies.USD, 0, 1m),
|
|
SymbolProperties.GetDefault(Currencies.USD),
|
|
ErrorCurrencyConverter.Instance,
|
|
RegisteredSecurityDataTypesProvider.Null,
|
|
new SecurityCache()));
|
|
}
|
|
|
|
private static void AddAlwaysOpenSecurity(TimeRules rules)
|
|
{
|
|
AddSecurity(rules, Symbols.BTCUSD);
|
|
}
|
|
|
|
private static TimeRules GetTimeRules(DateTimeZone dateTimeZone)
|
|
{
|
|
var timeKeeper = new TimeKeeper(_utcNow, new List<DateTimeZone>());
|
|
var manager = new SecurityManager(timeKeeper);
|
|
var mhdb = MarketHoursDatabase.FromDataFolder();
|
|
var marketHourDbEntry = mhdb.GetEntry(Market.USA, (string)null, SecurityType.Equity);
|
|
var securityExchangeHours = marketHourDbEntry.ExchangeHours;
|
|
var config = new SubscriptionDataConfig(typeof(TradeBar), Symbols.SPY, Resolution.Daily, marketHourDbEntry.DataTimeZone, securityExchangeHours.TimeZone, true, false, false);
|
|
manager.Add(
|
|
Symbols.SPY,
|
|
new Security(
|
|
securityExchangeHours,
|
|
config,
|
|
new Cash(Currencies.USD, 0, 1m),
|
|
SymbolProperties.GetDefault(Currencies.USD),
|
|
ErrorCurrencyConverter.Instance,
|
|
RegisteredSecurityDataTypesProvider.Null,
|
|
new SecurityCache()
|
|
)
|
|
);
|
|
var rules = new TimeRules(null, manager, dateTimeZone, mhdb);
|
|
return rules;
|
|
}
|
|
|
|
private static TimeRules GetFutureTimeRules(DateTimeZone dateTimeZone, bool extendedMarket = false)
|
|
{
|
|
var timeKeeper = new TimeKeeper(_utcNow, new List<DateTimeZone>());
|
|
var manager = new SecurityManager(timeKeeper);
|
|
var mhdb = MarketHoursDatabase.FromDataFolder();
|
|
var marketHourDbEntry = mhdb.GetEntry(Market.CME, "ES", SecurityType.Future);
|
|
var securityExchangeHours = marketHourDbEntry.ExchangeHours;
|
|
var config = new SubscriptionDataConfig(typeof(TradeBar), Symbols.ES_Future_Chain, Resolution.Daily, marketHourDbEntry.DataTimeZone,
|
|
securityExchangeHours.TimeZone, true, extendedMarket, false);
|
|
manager.Add(
|
|
Symbols.ES_Future_Chain,
|
|
new Security(
|
|
securityExchangeHours,
|
|
config,
|
|
new Cash(Currencies.USD, 0, 1m),
|
|
SymbolProperties.GetDefault(Currencies.USD),
|
|
ErrorCurrencyConverter.Instance,
|
|
RegisteredSecurityDataTypesProvider.Null,
|
|
new SecurityCache()
|
|
)
|
|
);
|
|
var rules = new TimeRules(null, manager, dateTimeZone, mhdb);
|
|
return rules;
|
|
}
|
|
}
|
|
}
|