Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 134 additions & 0 deletions Algorithm.CSharp/SchedulingConvenienceRegressionAlgorithm.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
* 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 QuantConnect.Interfaces;

namespace QuantConnect.Algorithm.CSharp
{
/// <summary>
/// Regression algorithm asserting the scheduling convenience APIs: an every-N-days date rule,
/// a time rule with an IANA time zone id and scheduling with a default midnight time rule
/// </summary>
public class SchedulingConvenienceRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition
{
private readonly List<DateTime> _everyThreeDaysTimes = new();
private readonly List<DateTime> _defaultTimeRuleTimes = new();
private readonly List<DateTime> _londonTimes = new();

/// <summary>
/// Initialise the data and resolution required, as well as the cash and start-end dates for your algorithm. All algorithms must initialized.
/// </summary>
public override void Initialize()
{
SetStartDate(2013, 10, 07);
SetEndDate(2013, 10, 11);

// every 3 days, anchored at the start of the schedule
Schedule.On(DateRules.Every(TimeSpan.FromDays(3)), TimeRules.At(12, 0), () => _everyThreeDaysTimes.Add(Time));

// no time rule: defaults to midnight in the algorithm time zone
Schedule.On(DateRules.EveryDay(), () => _defaultTimeRuleTimes.Add(Time));

// IANA time zone id: 15:00 London (BST in October 2013) is 10:00 New York (EDT)
Schedule.On(DateRules.EveryDay(), TimeRules.At(15, 0, "Europe/London"), () => _londonTimes.Add(Time));
}

public override void OnEndOfAlgorithm()
{
// the schedule starts the day before the algorithm start, so the rule anchors at Oct 6 and
// fires Oct 6 (before the start, skipped), Oct 9 and Oct 12 (after the end)
AssertScheduledTimes(_everyThreeDaysTimes, new List<DateTime> { new(2013, 10, 09, 12, 0, 0) }, "every 3 days");

// fires at midnight every day, including the algorithm start time itself
AssertScheduledTimes(_defaultTimeRuleTimes,
Enumerable.Range(7, 5).Select(day => new DateTime(2013, 10, day)).ToList(), "default midnight");

AssertScheduledTimes(_londonTimes,
Enumerable.Range(7, 5).Select(day => new DateTime(2013, 10, day, 10, 0, 0)).ToList(), "London 15:00");
}

private static void AssertScheduledTimes(List<DateTime> actual, List<DateTime> expected, string name)
{
if (!actual.SequenceEqual(expected))
{
throw new RegressionTestException($"Unexpected '{name}' event times: expected [{string.Join(", ", expected)}] " +
$"but got [{string.Join(", ", actual)}]");
}
}

/// <summary>
/// This is used by the regression test system to indicate if the open source Lean repository has the required data to run this algorithm.
/// </summary>
public bool CanRunLocally { get; } = true;

/// <summary>
/// This is used by the regression test system to indicate which languages this algorithm is written in.
/// </summary>
public List<Language> Languages { get; } = new() { Language.CSharp, Language.Python };

/// <summary>
/// Data Points count of all timeslices of algorithm
/// </summary>
public long DataPoints => 42;

/// <summary>
/// Data Points count of the algorithm history
/// </summary>
public int AlgorithmHistoryDataPoints => 0;

/// <summary>
/// Final status of the algorithm
/// </summary>
public AlgorithmStatus AlgorithmStatus => AlgorithmStatus.Completed;

/// <summary>
/// This is used by the regression test system to indicate what the expected statistics are from running the algorithm
/// </summary>
public Dictionary<string, string> ExpectedStatistics => new Dictionary<string, string>
{
{"Total Orders", "0"},
{"Average Win", "0%"},
{"Average Loss", "0%"},
{"Compounding Annual Return", "0%"},
{"Drawdown", "0%"},
{"Expectancy", "0"},
{"Start Equity", "100000"},
{"End Equity", "100000"},
{"Net Profit", "0%"},
{"Sharpe Ratio", "0"},
{"Sortino Ratio", "0"},
{"Probabilistic Sharpe Ratio", "0%"},
{"Loss Rate", "0%"},
{"Win Rate", "0%"},
{"Profit-Loss Ratio", "0"},
{"Alpha", "0"},
{"Beta", "0"},
{"Annual Standard Deviation", "0"},
{"Annual Variance", "0"},
{"Information Ratio", "-8.91"},
{"Tracking Error", "0.223"},
{"Treynor Ratio", "0"},
{"Total Fees", "$0.00"},
{"Estimated Strategy Capacity", "$0"},
{"Lowest Capacity Asset", ""},
{"Portfolio Turnover", "0%"},
{"Drawdown Recovery", "0"},
{"OrderListHash", "d41d8cd98f00b204e9800998ecf8427e"}
};
}
}
62 changes: 62 additions & 0 deletions Algorithm.Python/SchedulingConvenienceRegressionAlgorithm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# 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.

from AlgorithmImports import *

### <summary>
### Regression algorithm asserting the scheduling convenience APIs: an every-N-days date rule,
### a time rule with an IANA time zone id and scheduling with a default midnight time rule
### </summary>
class SchedulingConvenienceRegressionAlgorithm(QCAlgorithm):
def initialize(self):
'''Initialise the data and resolution required, as well as the cash and start-end dates for your algorithm. All algorithms must initialized.'''
self.set_start_date(2013, 10, 7)
self.set_end_date(2013, 10, 11)

self._every_three_days_times = []
self._default_time_rule_times = []
self._london_times = []

# every 3 days, anchored at the start of the schedule
self.schedule.on(self.date_rules.every(timedelta(days=3)), self.time_rules.at(12, 0),
lambda: self._every_three_days_times.append(self.time))

# no time rule: defaults to midnight in the algorithm time zone
self.schedule.on(self.date_rules.every_day(), lambda: self._default_time_rule_times.append(self.time))

# IANA time zone id: 15:00 London (BST in October 2013) is 10:00 New York (EDT)
self.schedule.on(self.date_rules.every_day(), self.time_rules.at(15, 0, "Europe/London"),
lambda: self._london_times.append(self.time))

# date/time rule properties are tolerant to being called as if they were methods, returning the rule itself
for rule, expected_name in [(self.time_rules.midnight(), "Midnight"), (self.time_rules.noon(), "Noon"),
(self.time_rules.now(), "Now"), (self.date_rules.today(), "TodayOnly"),
(self.date_rules.tomorrow(), "TomorrowOnly")]:
if rule.name != expected_name:
raise RegressionTestException(f"Unexpected rule name: expected {expected_name} but got {rule.name}")

def on_end_of_algorithm(self):
# the schedule starts the day before the algorithm start, so the rule anchors at Oct 6 and
# fires Oct 6 (before the start, skipped), Oct 9 and Oct 12 (after the end)
self._assert_scheduled_times(self._every_three_days_times, [datetime(2013, 10, 9, 12, 0, 0)], "every 3 days")

# fires at midnight every day, including the algorithm start time itself
self._assert_scheduled_times(self._default_time_rule_times, [datetime(2013, 10, day) for day in range(7, 12)],
"default midnight")

self._assert_scheduled_times(self._london_times, [datetime(2013, 10, day, 10, 0, 0) for day in range(7, 12)],
"London 15:00")

def _assert_scheduled_times(self, actual, expected, name):
if actual != expected:
raise RegressionTestException(f"Unexpected '{name}' event times: expected {expected} but got {actual}")
18 changes: 18 additions & 0 deletions Common/Scheduling/DateRules.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,24 @@ public IDateRule Every(params DayOfWeek[] days)
return new FuncDateRule(string.Join(",", days), (start, end) => Time.EachDay(start, end).Where(date => hash.Contains(date.DayOfWeek)));
}

/// <summary>
/// Specifies an event should fire every <paramref name="interval"/> days, anchored at the start of the schedule.
/// In Python, accepts a timedelta, e.g. 'self.date_rules.every(timedelta(days=5))'
/// </summary>
/// <param name="interval">The interval between event dates; must be a whole number of days of at least 1</param>
/// <returns>A date rule that fires every N days</returns>
public IDateRule Every(TimeSpan interval)
{
var days = (int)interval.TotalDays;
if (days < 1 || interval != TimeSpan.FromDays(days))
{
throw new ArgumentException("DateRules.Every(): interval must be a whole number of days of at least 1." +
$" For intraday schedules use TimeRules.Every() instead. Value provided: {interval}");
}
return new FuncDateRule($"Every {days.ToStringInvariant()} days",
(start, end) => Time.EachDay(start, end).Where((date, index) => index % days == 0));
}

/// <summary>
/// Specifies an event should fire every day
/// </summary>
Expand Down
13 changes: 13 additions & 0 deletions Common/Scheduling/FuncDateRule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,5 +70,18 @@ public IEnumerable<DateTime> GetDates(DateTime start, DateTime end)
{
return _getDatesFunction(start, end);
}

/// <summary>
/// Returns this same rule instance. Makes rule instances tolerant to being called in Python:
/// convenience properties like <see cref="DateRules.Today"/> are indistinguishable from sibling
/// methods like <see cref="DateRules.EveryDay()"/> there, so algorithms frequently write
/// 'self.date_rules.today()', which used to fail with "'FuncDateRule' object is not callable"
/// </summary>
/// <remarks>Python.NET wires any public '__call__' method to the Python call operator</remarks>
/// <returns>This same date rule instance</returns>
public IDateRule __call__()
{
return this;
}
}
}
13 changes: 13 additions & 0 deletions Common/Scheduling/FuncTimeRule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,5 +70,18 @@ public IEnumerable<DateTime> CreateUtcEventTimes(IEnumerable<DateTime> dates)
{
return _createUtcEventTimesFunction(dates);
}

/// <summary>
/// Returns this same rule instance. Makes rule instances tolerant to being called in Python:
/// convenience properties like <see cref="TimeRules.Midnight"/> are indistinguishable from sibling
/// methods like <see cref="TimeRules.At(int, int, int)"/> there, so algorithms frequently write
/// 'self.time_rules.midnight()', which used to fail with "'FuncTimeRule' object is not callable"
/// </summary>
/// <remarks>Python.NET wires any public '__call__' method to the Python call operator</remarks>
/// <returns>This same time rule instance</returns>
public ITimeRule __call__()
{
return this;
}
}
}
50 changes: 50 additions & 0 deletions Common/Scheduling/ScheduleManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,56 @@ public void Remove(ScheduledEvent scheduledEvent)
}
}

/// <summary>
/// Schedules the callback to run on the dates given by the date rule, at midnight in the algorithm's
/// time zone (<see cref="TimeRules.Midnight"/>), mirroring universe selection schedules that
/// only take a date rule
/// </summary>
/// <param name="dateRule">Specifies what dates the event should run</param>
/// <param name="callback">The callback to be invoked</param>
public ScheduledEvent On(IDateRule dateRule, Action callback)
{
return On(dateRule, TimeRules.Midnight, callback);
}

/// <summary>
/// Schedules the callback to run on the dates given by the date rule, at midnight in the algorithm's
/// time zone (<see cref="TimeRules.Midnight"/>), mirroring universe selection schedules that
/// only take a date rule
/// </summary>
/// <param name="dateRule">Specifies what dates the event should run</param>
/// <param name="callback">The callback to be invoked</param>
public ScheduledEvent On(IDateRule dateRule, PyObject callback)
{
// Guard the missing-callback mistake: On(dateRule, timeRule) would otherwise schedule the time
// rule itself as the callback, silently doing nothing now that time rules define __call__
if (callback.TryConvert(out ITimeRule _))
{
throw new ArgumentException("ScheduleManager.On(): missing callback. Use On(dateRule, timeRule, callback), " +
"or On(dateRule, callback) to default the time rule to midnight.");
}
using (Py.GIL())
{
if (!callback.IsCallable())
{
throw new ArgumentException($"ScheduleManager.On(): the provided callback is not callable: {callback.Repr()}");
}
}
return On(dateRule, TimeRules.Midnight, callback);
}

/// <summary>
/// Schedules the callback to run on the dates given by the date rule, at midnight in the algorithm's
/// time zone (<see cref="TimeRules.Midnight"/>), mirroring universe selection schedules that
/// only take a date rule
/// </summary>
/// <param name="dateRule">Specifies what dates the event should run</param>
/// <param name="callback">The callback to be invoked</param>
public ScheduledEvent On(IDateRule dateRule, Action<string, DateTime> callback)
{
return On(dateRule, TimeRules.Midnight, callback);
}

/// <summary>
/// Schedules the callback to run using the specified date and time rules
/// </summary>
Expand Down
55 changes: 55 additions & 0 deletions Common/Scheduling/TimeRules.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

using System;
using NodaTime;
using NodaTime.TimeZones;
using System.Linq;
using QuantConnect.Interfaces;
using QuantConnect.Securities;
Expand Down Expand Up @@ -121,6 +122,42 @@ public ITimeRule At(int hour, int minute, int second, DateTimeZone timeZone)
return At(new TimeSpan(hour, minute, second), timeZone);
}

/// <summary>
/// Specifies an event should fire at the specified time of day in the specified time zone
/// </summary>
/// <param name="hour">The hour</param>
/// <param name="minute">The minute</param>
/// <param name="timeZone">The time zone the event time is represented in, as an IANA time zone id, e.g. "Europe/London"</param>
/// <returns>A time rule that fires at the specified time in the algorithm's time zone</returns>
public ITimeRule At(int hour, int minute, string timeZone)
{
return At(new TimeSpan(hour, minute, 0), ParseTimeZone(timeZone));
}

/// <summary>
/// Specifies an event should fire at the specified time of day in the specified time zone
/// </summary>
/// <param name="hour">The hour</param>
/// <param name="minute">The minute</param>
/// <param name="second">The second</param>
/// <param name="timeZone">The time zone the event time is represented in, as an IANA time zone id, e.g. "Europe/London"</param>
/// <returns>A time rule that fires at the specified time in the algorithm's time zone</returns>
public ITimeRule At(int hour, int minute, int second, string timeZone)
{
return At(new TimeSpan(hour, minute, second), ParseTimeZone(timeZone));
}

/// <summary>
/// Specifies an event should fire at the specified time of day in the specified time zone
/// </summary>
/// <param name="timeOfDay">The time of day in the algorithm's time zone the event should fire</param>
/// <param name="timeZone">The time zone the event time is represented in, as an IANA time zone id, e.g. "Europe/London"</param>
/// <returns>A time rule that fires at the specified time in the algorithm's time zone</returns>
public ITimeRule At(TimeSpan timeOfDay, string timeZone)
{
return At(timeOfDay, ParseTimeZone(timeZone));
}

/// <summary>
/// Specifies an event should fire at the specified time of day in the specified time zone
/// </summary>
Expand Down Expand Up @@ -387,6 +424,24 @@ where exchangeHours.IsDateOpen(date, extendedMarketClose)
return new FuncTimeRule(name, applicator);
}

/// <summary>
/// Helper method to resolve an IANA/tzdb time zone id, e.g. "Europe/London", into a <see cref="DateTimeZone"/>
/// </summary>
/// <param name="timeZone">The IANA time zone id</param>
/// <returns>The <see cref="DateTimeZone"/> instance for the given id</returns>
private static DateTimeZone ParseTimeZone(string timeZone)
{
try
{
return DateTimeZoneProviders.Tzdb[timeZone];
}
catch (DateTimeZoneNotFoundException)
{
throw new ArgumentException($"TimeRules.At(): TimeZone with id '{timeZone}' was not found. " +
"For a complete list of time zones please visit: http://en.wikipedia.org/wiki/List_of_tz_database_time_zones");
}
}

/// <summary>
/// For each provided date will yield all the time intervals based on the supplied time span
/// </summary>
Expand Down
Loading
Loading