diff --git a/docs/tutorials/trading_index.ipynb b/docs/tutorials/trading_index.ipynb
index 8c2c8495..861e6f08 100644
--- a/docs/tutorials/trading_index.ipynb
+++ b/docs/tutorials/trading_index.ipynb
@@ -4,13 +4,8 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "# `trading_index`"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
+ "# `trading_index`\n",
+ "\n",
"This tutorial shows all the ins and outs of the calendars' `trading_index` method. (For other calendar methods see the [calendar_methods.ipynb](./calendar_methods.ipynb) tutorial.)\n",
"\n",
"The [What does it do?](#What-does-it-do?) section explains basic usage. The following sections then explore the method's arguments and options:\n",
@@ -20,13 +15,14 @@
"* [`closed`](#closed)\n",
"* [`force_close`, `force_break_close` and `force`](#force_close,-force_break_close-and-force)\n",
"* [`ignore_breaks`](#ignore_breaks)\n",
+ "* [`start` and `end` as times](#start-and-end-as-times)\n",
"\n",
"The final section covers [overlapping indices](#Overlapping-indices)."
]
},
{
"cell_type": "code",
- "execution_count": 2,
+ "execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
@@ -35,12 +31,13 @@
"import pandas as pd\n",
"\n",
"hkg = xcals.get_calendar(\"XHKG\") # Hong Kong Stock Exchange\n",
- "start, end = \"2021-12-23\", \"2021-12-28\""
+ "start = \"2021-12-23\"\n",
+ "end = \"2021-12-28\""
]
},
{
"cell_type": "code",
- "execution_count": 3,
+ "execution_count": 2,
"metadata": {},
"outputs": [
{
@@ -72,43 +69,43 @@
" \n",
"
\n",
" \n",
- " 2021-12-23 00:00:00+00:00 | \n",
- " 2021-12-23 01:30:00 | \n",
- " 2021-12-23 04:00:00 | \n",
- " 2021-12-23 05:00:00 | \n",
- " 2021-12-23 08:00:00 | \n",
+ " 2021-12-23 | \n",
+ " 2021-12-23 01:30:00+00:00 | \n",
+ " 2021-12-23 04:00:00+00:00 | \n",
+ " 2021-12-23 05:00:00+00:00 | \n",
+ " 2021-12-23 08:00:00+00:00 | \n",
"
\n",
" \n",
- " 2021-12-24 00:00:00+00:00 | \n",
- " 2021-12-24 01:30:00 | \n",
+ " 2021-12-24 | \n",
+ " 2021-12-24 01:30:00+00:00 | \n",
" NaT | \n",
" NaT | \n",
- " 2021-12-24 04:00:00 | \n",
+ " 2021-12-24 04:00:00+00:00 | \n",
"
\n",
" \n",
- " 2021-12-28 00:00:00+00:00 | \n",
- " 2021-12-28 01:30:00 | \n",
- " 2021-12-28 04:00:00 | \n",
- " 2021-12-28 05:00:00 | \n",
- " 2021-12-28 08:00:00 | \n",
+ " 2021-12-28 | \n",
+ " 2021-12-28 01:30:00+00:00 | \n",
+ " 2021-12-28 04:00:00+00:00 | \n",
+ " 2021-12-28 05:00:00+00:00 | \n",
+ " 2021-12-28 08:00:00+00:00 | \n",
"
\n",
" \n",
"\n",
""
],
"text/plain": [
- " market_open break_start \\\n",
- "2021-12-23 00:00:00+00:00 2021-12-23 01:30:00 2021-12-23 04:00:00 \n",
- "2021-12-24 00:00:00+00:00 2021-12-24 01:30:00 NaT \n",
- "2021-12-28 00:00:00+00:00 2021-12-28 01:30:00 2021-12-28 04:00:00 \n",
- "\n",
- " break_end market_close \n",
- "2021-12-23 00:00:00+00:00 2021-12-23 05:00:00 2021-12-23 08:00:00 \n",
- "2021-12-24 00:00:00+00:00 NaT 2021-12-24 04:00:00 \n",
- "2021-12-28 00:00:00+00:00 2021-12-28 05:00:00 2021-12-28 08:00:00 "
+ " market_open break_start \\\n",
+ "2021-12-23 2021-12-23 01:30:00+00:00 2021-12-23 04:00:00+00:00 \n",
+ "2021-12-24 2021-12-24 01:30:00+00:00 NaT \n",
+ "2021-12-28 2021-12-28 01:30:00+00:00 2021-12-28 04:00:00+00:00 \n",
+ "\n",
+ " break_end market_close \n",
+ "2021-12-23 2021-12-23 05:00:00+00:00 2021-12-23 08:00:00+00:00 \n",
+ "2021-12-24 NaT 2021-12-24 04:00:00+00:00 \n",
+ "2021-12-28 2021-12-28 05:00:00+00:00 2021-12-28 08:00:00+00:00 "
]
},
- "execution_count": 3,
+ "execution_count": 2,
"metadata": {},
"output_type": "execute_result"
}
@@ -135,7 +132,7 @@
},
{
"cell_type": "code",
- "execution_count": 4,
+ "execution_count": 3,
"metadata": {},
"outputs": [
{
@@ -158,7 +155,7 @@
" dtype='datetime64[ns, UTC]', freq=None)"
]
},
- "execution_count": 4,
+ "execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
@@ -176,7 +173,7 @@
},
{
"cell_type": "code",
- "execution_count": 5,
+ "execution_count": 4,
"metadata": {},
"outputs": [
{
@@ -191,7 +188,7 @@
" dtype='datetime64[ns, UTC]', freq=None)"
]
},
- "execution_count": 5,
+ "execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
@@ -204,14 +201,48 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "### `intervals`"
+ "Or between times, if `start` and `end` are passed as times (as opposed to dates)."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "DatetimeIndex(['2021-12-23 02:30:00+00:00', '2021-12-23 03:00:00+00:00',\n",
+ " '2021-12-23 03:30:00+00:00', '2021-12-23 05:00:00+00:00',\n",
+ " '2021-12-23 05:30:00+00:00', '2021-12-23 06:00:00+00:00',\n",
+ " '2021-12-23 06:30:00+00:00', '2021-12-23 07:00:00+00:00',\n",
+ " '2021-12-23 07:30:00+00:00', '2021-12-24 01:30:00+00:00',\n",
+ " '2021-12-24 02:00:00+00:00', '2021-12-24 02:30:00+00:00',\n",
+ " '2021-12-24 03:00:00+00:00', '2021-12-24 03:30:00+00:00',\n",
+ " '2021-12-28 01:30:00+00:00', '2021-12-28 02:00:00+00:00',\n",
+ " '2021-12-28 02:30:00+00:00', '2021-12-28 03:00:00+00:00',\n",
+ " '2021-12-28 03:30:00+00:00', '2021-12-28 05:00:00+00:00',\n",
+ " '2021-12-28 05:30:00+00:00', '2021-12-28 06:00:00+00:00',\n",
+ " '2021-12-28 06:30:00+00:00', '2021-12-28 07:00:00+00:00'],\n",
+ " dtype='datetime64[ns, UTC]', freq=None)"
+ ]
+ },
+ "execution_count": 5,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "start_min = \"2021-12-23 02:30\"\n",
+ "end_min = \"2021-12-28 07:00\"\n",
+ "hkg.trading_index(start=start_min, end=end_min, period=\"30min\", intervals=False)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "By passing `intervals` as False the index can be output as a `DatetimeIndex`, as shown above. Although by default the index is output as an `IntervalIndex`."
+ "Note that `start` can be passed as a time and `end` as a date, or vice-versa."
]
},
{
@@ -222,9 +253,12 @@
{
"data": {
"text/plain": [
- "IntervalIndex([[2021-12-23 01:30:00, 2021-12-23 02:00:00), [2021-12-23 02:00:00, 2021-12-23 02:30:00), [2021-12-23 02:30:00, 2021-12-23 03:00:00), [2021-12-23 03:00:00, 2021-12-23 03:30:00), [2021-12-23 03:30:00, 2021-12-23 04:00:00) ... [2021-12-23 05:30:00, 2021-12-23 06:00:00), [2021-12-23 06:00:00, 2021-12-23 06:30:00), [2021-12-23 06:30:00, 2021-12-23 07:00:00), [2021-12-23 07:00:00, 2021-12-23 07:30:00), [2021-12-23 07:30:00, 2021-12-23 08:00:00)],\n",
- " closed='left',\n",
- " dtype='interval[datetime64[ns, UTC]]')"
+ "DatetimeIndex(['2021-12-23 02:30:00+00:00', '2021-12-23 03:00:00+00:00',\n",
+ " '2021-12-23 03:30:00+00:00', '2021-12-23 05:00:00+00:00',\n",
+ " '2021-12-23 05:30:00+00:00', '2021-12-23 06:00:00+00:00',\n",
+ " '2021-12-23 06:30:00+00:00', '2021-12-23 07:00:00+00:00',\n",
+ " '2021-12-23 07:30:00+00:00'],\n",
+ " dtype='datetime64[ns, UTC]', freq=None)"
]
},
"execution_count": 6,
@@ -232,6 +266,42 @@
"output_type": "execute_result"
}
],
+ "source": [
+ "hkg.trading_index(start=start_min, end=start, period=\"30min\", intervals=False)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "See the '[`start` and `end` as times](#start-and-end-as-times)' section for further notes on how the first and last indices are defined when `start` and/or `end` are passed as times."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### `intervals`\n",
+ "\n",
+ "By passing `intervals` as False the index can be output as a `DatetimeIndex`, as shown above. Although by default the index is output as an `IntervalIndex`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "IntervalIndex([[2021-12-23 01:30:00, 2021-12-23 02:00:00), [2021-12-23 02:00:00, 2021-12-23 02:30:00), [2021-12-23 02:30:00, 2021-12-23 03:00:00), [2021-12-23 03:00:00, 2021-12-23 03:30:00), [2021-12-23 03:30:00, 2021-12-23 04:00:00) ... [2021-12-23 05:30:00, 2021-12-23 06:00:00), [2021-12-23 06:00:00, 2021-12-23 06:30:00), [2021-12-23 06:30:00, 2021-12-23 07:00:00), [2021-12-23 07:00:00, 2021-12-23 07:30:00), [2021-12-23 07:30:00, 2021-12-23 08:00:00)], dtype='interval[datetime64[ns, UTC], left]')"
+ ]
+ },
+ "execution_count": 7,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
"source": [
"index = hkg.trading_index(start, start, \"30min\")\n",
"index"
@@ -246,7 +316,7 @@
},
{
"cell_type": "code",
- "execution_count": 7,
+ "execution_count": 8,
"metadata": {},
"outputs": [
{
@@ -369,7 +439,7 @@
"[2021-12-23 07:30:00, 2021-12-23 08:00:00) 2021-12-23 08:00:00+00:00 "
]
},
- "execution_count": 7,
+ "execution_count": 8,
"metadata": {},
"output_type": "execute_result"
}
@@ -403,7 +473,7 @@
},
{
"cell_type": "code",
- "execution_count": 8,
+ "execution_count": 9,
"metadata": {},
"outputs": [
{
@@ -412,7 +482,7 @@
"Interval('2021-12-23 01:30:00', '2021-12-23 02:00:00', closed='left')"
]
},
- "execution_count": 8,
+ "execution_count": 9,
"metadata": {},
"output_type": "execute_result"
}
@@ -425,12 +495,12 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "The length of each period of the example above is 30 minutes."
+ "The length of each period for the example above is 30 minutes."
]
},
{
"cell_type": "code",
- "execution_count": 9,
+ "execution_count": 10,
"metadata": {},
"outputs": [
{
@@ -443,7 +513,7 @@
" dtype='timedelta64[ns]', freq=None)"
]
},
- "execution_count": 9,
+ "execution_count": 10,
"metadata": {},
"output_type": "execute_result"
}
@@ -461,7 +531,7 @@
},
{
"cell_type": "code",
- "execution_count": 10,
+ "execution_count": 11,
"metadata": {},
"outputs": [],
"source": [
@@ -479,12 +549,12 @@
"\n",
"indexes = [ hkg.trading_index(start, start, period) for period in periods ]\n",
"for i, index in enumerate(indexes):\n",
- " pd.testing.assert_index_equal(index, indexes[i])"
+ " pd.testing.assert_index_equal(index, indexes[0])"
]
},
{
"cell_type": "code",
- "execution_count": 11,
+ "execution_count": 12,
"metadata": {},
"outputs": [
{
@@ -572,7 +642,7 @@
"[2021-12-23 07:00:00, 2021-12-23 08:00:00) 2021-12-23 08:00:00+00:00 "
]
},
- "execution_count": 11,
+ "execution_count": 12,
"metadata": {},
"output_type": "execute_result"
}
@@ -590,18 +660,16 @@
},
{
"cell_type": "code",
- "execution_count": 12,
+ "execution_count": 13,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
- "DatetimeIndex(['2021-12-23 00:00:00+00:00', '2021-12-24 00:00:00+00:00',\n",
- " '2021-12-28 00:00:00+00:00'],\n",
- " dtype='datetime64[ns, UTC]', freq='C')"
+ "DatetimeIndex(['2021-12-23', '2021-12-24', '2021-12-28'], dtype='datetime64[ns]', freq='C')"
]
},
- "execution_count": 12,
+ "execution_count": 13,
"metadata": {},
"output_type": "execute_result"
}
@@ -619,18 +687,16 @@
},
{
"cell_type": "code",
- "execution_count": 13,
+ "execution_count": 14,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
- "IntervalIndex([[2021-12-23 01:30:00, 2021-12-23 01:30:00.001000), [2021-12-23 01:30:00.001000, 2021-12-23 01:30:00.002000), [2021-12-23 01:30:00.002000, 2021-12-23 01:30:00.003000), [2021-12-23 01:30:00.003000, 2021-12-23 01:30:00.004000), [2021-12-23 01:30:00.004000, 2021-12-23 01:30:00.005000) ... [2021-12-23 07:59:59.995000, 2021-12-23 07:59:59.996000), [2021-12-23 07:59:59.996000, 2021-12-23 07:59:59.997000), [2021-12-23 07:59:59.997000, 2021-12-23 07:59:59.998000), [2021-12-23 07:59:59.998000, 2021-12-23 07:59:59.999000), [2021-12-23 07:59:59.999000, 2021-12-23 08:00:00)],\n",
- " closed='left',\n",
- " dtype='interval[datetime64[ns, UTC]]')"
+ "IntervalIndex([[2021-12-23 01:30:00, 2021-12-23 01:30:00.001000), [2021-12-23 01:30:00.001000, 2021-12-23 01:30:00.002000), [2021-12-23 01:30:00.002000, 2021-12-23 01:30:00.003000), [2021-12-23 01:30:00.003000, 2021-12-23 01:30:00.004000), [2021-12-23 01:30:00.004000, 2021-12-23 01:30:00.005000) ... [2021-12-23 07:59:59.995000, 2021-12-23 07:59:59.996000), [2021-12-23 07:59:59.996000, 2021-12-23 07:59:59.997000), [2021-12-23 07:59:59.997000, 2021-12-23 07:59:59.998000), [2021-12-23 07:59:59.998000, 2021-12-23 07:59:59.999000), [2021-12-23 07:59:59.999000, 2021-12-23 08:00:00)], dtype='interval[datetime64[ns, UTC], left]', length=19800000)"
]
},
- "execution_count": 13,
+ "execution_count": 14,
"metadata": {},
"output_type": "execute_result"
}
@@ -649,7 +715,7 @@
},
{
"cell_type": "code",
- "execution_count": 14,
+ "execution_count": 15,
"metadata": {},
"outputs": [
{
@@ -658,7 +724,7 @@
"19800000"
]
},
- "execution_count": 14,
+ "execution_count": 15,
"metadata": {},
"output_type": "execute_result"
}
@@ -676,7 +742,7 @@
},
{
"cell_type": "code",
- "execution_count": 15,
+ "execution_count": 16,
"metadata": {},
"outputs": [],
"source": [
@@ -685,7 +751,7 @@
},
{
"cell_type": "code",
- "execution_count": 16,
+ "execution_count": 17,
"metadata": {},
"outputs": [
{
@@ -698,7 +764,7 @@
" dtype='timedelta64[ns]', freq=None)"
]
},
- "execution_count": 16,
+ "execution_count": 17,
"metadata": {},
"output_type": "execute_result"
}
@@ -716,7 +782,7 @@
},
{
"cell_type": "code",
- "execution_count": 17,
+ "execution_count": 18,
"metadata": {},
"outputs": [
{
@@ -731,7 +797,7 @@
" dtype='datetime64[ns, UTC]', freq=None)"
]
},
- "execution_count": 17,
+ "execution_count": 18,
"metadata": {},
"output_type": "execute_result"
}
@@ -742,7 +808,7 @@
},
{
"cell_type": "code",
- "execution_count": 18,
+ "execution_count": 19,
"metadata": {},
"outputs": [
{
@@ -774,25 +840,25 @@
" \n",
" \n",
" \n",
- " 2021-12-23 00:00:00+00:00 | \n",
- " 2021-12-23 01:30:00 | \n",
- " 2021-12-23 04:00:00 | \n",
- " 2021-12-23 05:00:00 | \n",
- " 2021-12-23 08:00:00 | \n",
+ " 2021-12-23 | \n",
+ " 2021-12-23 01:30:00+00:00 | \n",
+ " 2021-12-23 04:00:00+00:00 | \n",
+ " 2021-12-23 05:00:00+00:00 | \n",
+ " 2021-12-23 08:00:00+00:00 | \n",
"
\n",
" \n",
"\n",
""
],
"text/plain": [
- " market_open break_start \\\n",
- "2021-12-23 00:00:00+00:00 2021-12-23 01:30:00 2021-12-23 04:00:00 \n",
+ " market_open break_start \\\n",
+ "2021-12-23 2021-12-23 01:30:00+00:00 2021-12-23 04:00:00+00:00 \n",
"\n",
- " break_end market_close \n",
- "2021-12-23 00:00:00+00:00 2021-12-23 05:00:00 2021-12-23 08:00:00 "
+ " break_end market_close \n",
+ "2021-12-23 2021-12-23 05:00:00+00:00 2021-12-23 08:00:00+00:00 "
]
},
- "execution_count": 18,
+ "execution_count": 19,
"metadata": {},
"output_type": "execute_result"
}
@@ -806,9 +872,9 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "Hong Kong has a lunch break. The trading index covers the morning subsession starting with the open, and then the afternoon subsession starting with the break-end. The hour and a half represents the difference between the last indice of the morning subsession and the first indice of the afternoon subsession. (See [`ignore_breaks`](#ignore_breaks) to treat session as continuous.)\n",
+ "Hong Kong has a lunch break. The trading index covers the morning subsession starting with the open, and then the afternoon subsession starting with the break-end. The hour and a half represents the difference between the last indice of the morning subsession and the first indice of the afternoon subsession. (See [`ignore_breaks`](#ignore_breaks) for how to treat sessions as continuous.)\n",
"\n",
- "Ok, fine, but why hasn't the index included indices for 04:00 or 08:00? Because, it's `closed` on the \"left\" (by default)..."
+ "Ok, fine, but why hasn't the index included indices for 04:00 or 08:00? That's because it's `closed` on the \"left\" (by default)..."
]
},
{
@@ -822,7 +888,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "The `closed` parameter has a different effect depending on whether the index is being output as an `IntervalIndex` (default) or `DatetimeIndex` (i.e. if `intervals=False`).\n",
+ "The `closed` parameter has a different effect depending on whether the index is being output as an `IntervalIndex` (default) or `DatetimeIndex` (i.e. `intervals=False`).\n",
"\n",
"If the index is being output as an `IntervalIndex` then `closed` can take either \"left\" (default) or \"right\". This value is simply passed through to the `closed` parameter of the `IntervalIndex` (to define the side on which all the intervals should be closed). In this case `closed` has no other effect.\n",
"\n",
@@ -837,7 +903,7 @@
},
{
"cell_type": "code",
- "execution_count": 19,
+ "execution_count": 20,
"metadata": {},
"outputs": [
{
@@ -852,7 +918,7 @@
" dtype='datetime64[ns, UTC]', freq=None)"
]
},
- "execution_count": 19,
+ "execution_count": 20,
"metadata": {},
"output_type": "execute_result"
}
@@ -872,7 +938,7 @@
},
{
"cell_type": "code",
- "execution_count": 20,
+ "execution_count": 21,
"metadata": {},
"outputs": [
{
@@ -888,7 +954,7 @@
" dtype='datetime64[ns, UTC]', freq=None)"
]
},
- "execution_count": 20,
+ "execution_count": 21,
"metadata": {},
"output_type": "execute_result"
}
@@ -906,7 +972,7 @@
},
{
"cell_type": "code",
- "execution_count": 21,
+ "execution_count": 22,
"metadata": {},
"outputs": [
{
@@ -920,7 +986,7 @@
" dtype='datetime64[ns, UTC]', freq=None)"
]
},
- "execution_count": 21,
+ "execution_count": 22,
"metadata": {},
"output_type": "execute_result"
}
@@ -945,7 +1011,7 @@
},
{
"cell_type": "code",
- "execution_count": 22,
+ "execution_count": 23,
"metadata": {},
"outputs": [
{
@@ -1026,7 +1092,7 @@
"[2021-12-23 07:40:00, 2021-12-23 09:00:00) 2021-12-23 09:00:00+00:00 "
]
},
- "execution_count": 22,
+ "execution_count": 23,
"metadata": {},
"output_type": "execute_result"
}
@@ -1045,7 +1111,7 @@
},
{
"cell_type": "code",
- "execution_count": 23,
+ "execution_count": 24,
"metadata": {},
"outputs": [
{
@@ -1077,25 +1143,25 @@
" \n",
" \n",
" \n",
- " 2021-12-23 00:00:00+00:00 | \n",
- " 2021-12-23 01:30:00 | \n",
- " 2021-12-23 04:00:00 | \n",
- " 2021-12-23 05:00:00 | \n",
- " 2021-12-23 08:00:00 | \n",
+ " 2021-12-23 | \n",
+ " 2021-12-23 01:30:00+00:00 | \n",
+ " 2021-12-23 04:00:00+00:00 | \n",
+ " 2021-12-23 05:00:00+00:00 | \n",
+ " 2021-12-23 08:00:00+00:00 | \n",
"
\n",
" \n",
"\n",
""
],
"text/plain": [
- " market_open break_start \\\n",
- "2021-12-23 00:00:00+00:00 2021-12-23 01:30:00 2021-12-23 04:00:00 \n",
+ " market_open break_start \\\n",
+ "2021-12-23 2021-12-23 01:30:00+00:00 2021-12-23 04:00:00+00:00 \n",
"\n",
- " break_end market_close \n",
- "2021-12-23 00:00:00+00:00 2021-12-23 05:00:00 2021-12-23 08:00:00 "
+ " break_end market_close \n",
+ "2021-12-23 2021-12-23 05:00:00+00:00 2021-12-23 08:00:00+00:00 "
]
},
- "execution_count": 23,
+ "execution_count": 24,
"metadata": {},
"output_type": "execute_result"
}
@@ -1107,7 +1173,7 @@
},
{
"cell_type": "code",
- "execution_count": 24,
+ "execution_count": 25,
"metadata": {},
"outputs": [
{
@@ -1116,7 +1182,7 @@
"Interval('2021-12-23 02:50:00', '2021-12-23 04:10:00', closed='left')"
]
},
- "execution_count": 24,
+ "execution_count": 25,
"metadata": {},
"output_type": "execute_result"
}
@@ -1140,7 +1206,7 @@
},
{
"cell_type": "code",
- "execution_count": 25,
+ "execution_count": 26,
"metadata": {},
"outputs": [
{
@@ -1221,7 +1287,7 @@
"[2021-12-23 07:40:00, 2021-12-23 08:00:00) 2021-12-23 08:00:00+00:00 "
]
},
- "execution_count": 25,
+ "execution_count": 26,
"metadata": {},
"output_type": "execute_result"
}
@@ -1242,7 +1308,7 @@
},
{
"cell_type": "code",
- "execution_count": 26,
+ "execution_count": 27,
"metadata": {},
"outputs": [
{
@@ -1323,7 +1389,7 @@
"[2021-12-23 07:40:00, 2021-12-23 09:00:00) 2021-12-23 09:00:00+00:00 "
]
},
- "execution_count": 26,
+ "execution_count": 27,
"metadata": {},
"output_type": "execute_result"
}
@@ -1342,7 +1408,7 @@
},
{
"cell_type": "code",
- "execution_count": 27,
+ "execution_count": 28,
"metadata": {},
"outputs": [
{
@@ -1423,7 +1489,7 @@
"[2021-12-23 07:40:00, 2021-12-23 08:00:00) 2021-12-23 08:00:00+00:00 "
]
},
- "execution_count": 27,
+ "execution_count": 28,
"metadata": {},
"output_type": "execute_result"
}
@@ -1442,7 +1508,7 @@
},
{
"cell_type": "code",
- "execution_count": 28,
+ "execution_count": 29,
"metadata": {},
"outputs": [
{
@@ -1453,7 +1519,7 @@
" dtype='timedelta64[ns]', freq=None)"
]
},
- "execution_count": 28,
+ "execution_count": 29,
"metadata": {},
"output_type": "execute_result"
}
@@ -1466,13 +1532,8 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "**If having indices that represent only trading periods is more important to you than having them all relfect the same period length, then forcing the close is the way to go.**"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
+ "**If having indices that represent only trading periods is more important to you than having them all relfect the same period length, then forcing the close is the way to go.**\n",
+ "\n",
"The force close options can also be employed when creating `DatetimeIndex`.\n",
"\n",
"When `closed` is either \"right\" or \"both\" the effect will be similar to that for the `IntervalIndex`, in that if the (sub)sessions' last indices otherwise fall later than the close they will be curtailed to the close."
@@ -1480,7 +1541,7 @@
},
{
"cell_type": "code",
- "execution_count": 29,
+ "execution_count": 30,
"metadata": {},
"outputs": [
{
@@ -1492,7 +1553,7 @@
" dtype='datetime64[ns, UTC]', freq=None)"
]
},
- "execution_count": 29,
+ "execution_count": 30,
"metadata": {},
"output_type": "execute_result"
}
@@ -1506,47 +1567,42 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "When `closed` is either \"left\" or \"neither\" the effect of forcing the close simply adds the corresponding close to the index. This might be thought of as adding the right side of the (sub)session's last implied period, albeit curtailed to the close. \n",
- "\n",
- "Recalling the question of why 04:00 and 08:00 aren't included when doing this."
+ "If `closed` is either \"left\" or \"neither\" then `force` has no effect as there cannot be an indice to the right of the close to force."
]
},
{
"cell_type": "code",
- "execution_count": 30,
+ "execution_count": 31,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
- "DatetimeIndex(['2021-12-23 01:30:00+00:00', '2021-12-23 02:00:00+00:00',\n",
- " '2021-12-23 02:30:00+00:00', '2021-12-23 03:00:00+00:00',\n",
- " '2021-12-23 03:30:00+00:00', '2021-12-23 05:00:00+00:00',\n",
- " '2021-12-23 05:30:00+00:00', '2021-12-23 06:00:00+00:00',\n",
- " '2021-12-23 06:30:00+00:00', '2021-12-23 07:00:00+00:00',\n",
- " '2021-12-23 07:30:00+00:00'],\n",
+ "DatetimeIndex(['2021-12-23 01:30:00+00:00', '2021-12-23 02:50:00+00:00',\n",
+ " '2021-12-23 05:00:00+00:00', '2021-12-23 06:20:00+00:00',\n",
+ " '2021-12-23 07:40:00+00:00'],\n",
" dtype='datetime64[ns, UTC]', freq=None)"
]
},
- "execution_count": 30,
+ "execution_count": 31,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
- "hkg.trading_index(start, start, \"30min\", intervals=False)"
+ "hkg.trading_index(start, start, \"80T\", closed=\"left\", force=True, intervals=False)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "Forcing the closes offers a way of including them if that's what's required."
+ "Recalling the question of why 04:00 and 08:00 aren't included when doing this..."
]
},
{
"cell_type": "code",
- "execution_count": 31,
+ "execution_count": 32,
"metadata": {},
"outputs": [
{
@@ -1554,49 +1610,67 @@
"text/plain": [
"DatetimeIndex(['2021-12-23 01:30:00+00:00', '2021-12-23 02:00:00+00:00',\n",
" '2021-12-23 02:30:00+00:00', '2021-12-23 03:00:00+00:00',\n",
- " '2021-12-23 03:30:00+00:00', '2021-12-23 04:00:00+00:00',\n",
- " '2021-12-23 05:00:00+00:00', '2021-12-23 05:30:00+00:00',\n",
- " '2021-12-23 06:00:00+00:00', '2021-12-23 06:30:00+00:00',\n",
- " '2021-12-23 07:00:00+00:00', '2021-12-23 07:30:00+00:00',\n",
- " '2021-12-23 08:00:00+00:00'],\n",
+ " '2021-12-23 03:30:00+00:00', '2021-12-23 05:00:00+00:00',\n",
+ " '2021-12-23 05:30:00+00:00', '2021-12-23 06:00:00+00:00',\n",
+ " '2021-12-23 06:30:00+00:00', '2021-12-23 07:00:00+00:00',\n",
+ " '2021-12-23 07:30:00+00:00'],\n",
" dtype='datetime64[ns, UTC]', freq=None)"
]
},
- "execution_count": 31,
+ "execution_count": 32,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
- "hkg.trading_index(start, start, \"30min\", force=True, intervals=False)"
+ "hkg.trading_index(start, start, \"30min\", intervals=False)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "Worth nothing that if all closes are forced then the same index will be returned by passing `closed` as:\n",
- "- \"left\" or \"both\".\n",
- "- \"right\" or \"neither\"."
+ "Passing `closed` as \"both\" and `force` as `True` will always include the closes."
]
},
{
- "cell_type": "markdown",
+ "cell_type": "code",
+ "execution_count": 33,
"metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "DatetimeIndex(['2021-12-23 01:30:00+00:00', '2021-12-23 02:30:00+00:00',\n",
+ " '2021-12-23 03:30:00+00:00', '2021-12-23 04:00:00+00:00',\n",
+ " '2021-12-23 05:00:00+00:00', '2021-12-23 06:00:00+00:00',\n",
+ " '2021-12-23 07:00:00+00:00', '2021-12-23 08:00:00+00:00'],\n",
+ " dtype='datetime64[ns, UTC]', freq=None)"
+ ]
+ },
+ "execution_count": 33,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
"source": [
- "### `ignore_breaks`"
+ "hkg.trading_index(start, start, \"1H\", closed=\"both\", force=True, intervals=False)"
]
},
{
"cell_type": "markdown",
- "metadata": {},
+ "metadata": {
+ "tags": []
+ },
"source": [
- "`ignore_breaks` provides for ignoring any session breaks and instead treating every session as if it were continuous. The following shows how by default `ignore_breaks` is False such that non-trading indices are not introduced within breaks."
+ "### `ignore_breaks`\n",
+ "\n",
+ "`ignore_breaks` provides for ignoring any session breaks and instead treating every session as if it were continuous. The following shows how by default `ignore_breaks` is `False` such that non-trading indices are not introduced within breaks."
]
},
{
"cell_type": "code",
- "execution_count": 32,
+ "execution_count": 34,
"metadata": {},
"outputs": [
{
@@ -1628,25 +1702,25 @@
" \n",
" \n",
" \n",
- " 2021-12-23 00:00:00+00:00 | \n",
- " 2021-12-23 01:30:00 | \n",
- " 2021-12-23 04:00:00 | \n",
- " 2021-12-23 05:00:00 | \n",
- " 2021-12-23 08:00:00 | \n",
+ " 2021-12-23 | \n",
+ " 2021-12-23 01:30:00+00:00 | \n",
+ " 2021-12-23 04:00:00+00:00 | \n",
+ " 2021-12-23 05:00:00+00:00 | \n",
+ " 2021-12-23 08:00:00+00:00 | \n",
"
\n",
" \n",
"\n",
""
],
"text/plain": [
- " market_open break_start \\\n",
- "2021-12-23 00:00:00+00:00 2021-12-23 01:30:00 2021-12-23 04:00:00 \n",
+ " market_open break_start \\\n",
+ "2021-12-23 2021-12-23 01:30:00+00:00 2021-12-23 04:00:00+00:00 \n",
"\n",
- " break_end market_close \n",
- "2021-12-23 00:00:00+00:00 2021-12-23 05:00:00 2021-12-23 08:00:00 "
+ " break_end market_close \n",
+ "2021-12-23 2021-12-23 05:00:00+00:00 2021-12-23 08:00:00+00:00 "
]
},
- "execution_count": 32,
+ "execution_count": 34,
"metadata": {},
"output_type": "execute_result"
}
@@ -1658,7 +1732,7 @@
},
{
"cell_type": "code",
- "execution_count": 33,
+ "execution_count": 35,
"metadata": {},
"outputs": [
{
@@ -1781,7 +1855,7 @@
"[2021-12-23 07:30:00, 2021-12-23 08:00:00) 2021-12-23 08:00:00+00:00 "
]
},
- "execution_count": 33,
+ "execution_count": 35,
"metadata": {},
"output_type": "execute_result"
}
@@ -1795,12 +1869,14 @@
"cell_type": "markdown",
"metadata": {},
"source": [
+ "Notice that above there are no indices between the 04:00 close of the morning session and the 05:00 open of the afternoon session.\n",
+ "\n",
"Passing `ignore_breaks` as True will include indices through any break."
]
},
{
"cell_type": "code",
- "execution_count": 34,
+ "execution_count": 36,
"metadata": {},
"outputs": [
{
@@ -1937,7 +2013,7 @@
"[2021-12-23 07:30:00, 2021-12-23 08:00:00) 2021-12-23 08:00:00+00:00 "
]
},
- "execution_count": 34,
+ "execution_count": 36,
"metadata": {},
"output_type": "execute_result"
}
@@ -1956,7 +2032,7 @@
},
{
"cell_type": "code",
- "execution_count": 35,
+ "execution_count": 37,
"metadata": {},
"outputs": [
{
@@ -2051,7 +2127,7 @@
"[2021-12-23 07:30:00, 2021-12-23 08:30:00) 2021-12-23 08:30:00+00:00 "
]
},
- "execution_count": 35,
+ "execution_count": 37,
"metadata": {},
"output_type": "execute_result"
}
@@ -2065,30 +2141,420 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "### `Overlapping indices`"
+ "### `start` and `end` as times"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "Now, if you've been playing with `trading_index` as you've worked through this tutorial and you've managed to get this far without raising an error, then you just haven't been trying hard enough..."
+ "When `start` and `end` are passed as times, how the first and last indices are defined depends on whether the index is returned as an `IntervalIndex` or a `DatetimeIndex`.\n",
+ "\n",
+ "When returning an `IntervalIndex` (`intervals=True`):\n",
+ "* The first indice will be:\n",
+ " * if `start` coincides with the left side of an indice, then that indice.\n",
+ " * otherwise the nearest indice to `start` with a left side that is later than `start`.\n",
+ "\n",
+ "* The last indice will be:\n",
+ " * if `end` coincides with the right side of an indice, then that indice.\n",
+ " * otherwise the nearest indice to `end` with a right side that is earlier than `end`.\n",
+ "\n",
+ "In the following example the `start` and `end` times coincide with, respectively, the left and right side of indices."
]
},
{
"cell_type": "code",
- "execution_count": 36,
+ "execution_count": 38,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "start_min = pd.Timestamp(\"2021-12-23 03:00\")\n",
+ "end_min = pd.Timestamp(\"2021-12-23 07:00\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 39,
"metadata": {},
"outputs": [
{
"data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " | \n",
+ " left_side | \n",
+ " right_side | \n",
+ "
\n",
+ " \n",
+ " IntervalIndex | \n",
+ " | \n",
+ " | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " [2021-12-23 03:00:00, 2021-12-23 03:30:00) | \n",
+ " 2021-12-23 03:00:00+00:00 | \n",
+ " 2021-12-23 03:30:00+00:00 | \n",
+ "
\n",
+ " \n",
+ " [2021-12-23 03:30:00, 2021-12-23 04:00:00) | \n",
+ " 2021-12-23 03:30:00+00:00 | \n",
+ " 2021-12-23 04:00:00+00:00 | \n",
+ "
\n",
+ " \n",
+ " [2021-12-23 05:00:00, 2021-12-23 05:30:00) | \n",
+ " 2021-12-23 05:00:00+00:00 | \n",
+ " 2021-12-23 05:30:00+00:00 | \n",
+ "
\n",
+ " \n",
+ " [2021-12-23 05:30:00, 2021-12-23 06:00:00) | \n",
+ " 2021-12-23 05:30:00+00:00 | \n",
+ " 2021-12-23 06:00:00+00:00 | \n",
+ "
\n",
+ " \n",
+ " [2021-12-23 06:00:00, 2021-12-23 06:30:00) | \n",
+ " 2021-12-23 06:00:00+00:00 | \n",
+ " 2021-12-23 06:30:00+00:00 | \n",
+ "
\n",
+ " \n",
+ " [2021-12-23 06:30:00, 2021-12-23 07:00:00) | \n",
+ " 2021-12-23 06:30:00+00:00 | \n",
+ " 2021-12-23 07:00:00+00:00 | \n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
"
+ ],
"text/plain": [
- "IntervalIndex([[2021-12-23 01:30:00, 2021-12-23 03:15:00), [2021-12-23 03:15:00, 2021-12-23 05:00:00), [2021-12-23 05:00:00, 2021-12-23 06:45:00), [2021-12-23 06:45:00, 2021-12-23 08:30:00)],\n",
- " closed='left',\n",
- " dtype='interval[datetime64[ns, UTC]]')"
+ " left_side \\\n",
+ "IntervalIndex \n",
+ "[2021-12-23 03:00:00, 2021-12-23 03:30:00) 2021-12-23 03:00:00+00:00 \n",
+ "[2021-12-23 03:30:00, 2021-12-23 04:00:00) 2021-12-23 03:30:00+00:00 \n",
+ "[2021-12-23 05:00:00, 2021-12-23 05:30:00) 2021-12-23 05:00:00+00:00 \n",
+ "[2021-12-23 05:30:00, 2021-12-23 06:00:00) 2021-12-23 05:30:00+00:00 \n",
+ "[2021-12-23 06:00:00, 2021-12-23 06:30:00) 2021-12-23 06:00:00+00:00 \n",
+ "[2021-12-23 06:30:00, 2021-12-23 07:00:00) 2021-12-23 06:30:00+00:00 \n",
+ "\n",
+ " right_side \n",
+ "IntervalIndex \n",
+ "[2021-12-23 03:00:00, 2021-12-23 03:30:00) 2021-12-23 03:30:00+00:00 \n",
+ "[2021-12-23 03:30:00, 2021-12-23 04:00:00) 2021-12-23 04:00:00+00:00 \n",
+ "[2021-12-23 05:00:00, 2021-12-23 05:30:00) 2021-12-23 05:30:00+00:00 \n",
+ "[2021-12-23 05:30:00, 2021-12-23 06:00:00) 2021-12-23 06:00:00+00:00 \n",
+ "[2021-12-23 06:00:00, 2021-12-23 06:30:00) 2021-12-23 06:30:00+00:00 \n",
+ "[2021-12-23 06:30:00, 2021-12-23 07:00:00) 2021-12-23 07:00:00+00:00 "
]
},
- "execution_count": 36,
+ "execution_count": 39,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "index = hkg.trading_index(start_min, end_min, \"30T\", intervals=True)\n",
+ "show_as_df(index)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Note the effect of moving `start` forwards and `end` backwards by one minute."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 40,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " | \n",
+ " left_side | \n",
+ " right_side | \n",
+ "
\n",
+ " \n",
+ " IntervalIndex | \n",
+ " | \n",
+ " | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " [2021-12-23 03:30:00, 2021-12-23 04:00:00) | \n",
+ " 2021-12-23 03:30:00+00:00 | \n",
+ " 2021-12-23 04:00:00+00:00 | \n",
+ "
\n",
+ " \n",
+ " [2021-12-23 05:00:00, 2021-12-23 05:30:00) | \n",
+ " 2021-12-23 05:00:00+00:00 | \n",
+ " 2021-12-23 05:30:00+00:00 | \n",
+ "
\n",
+ " \n",
+ " [2021-12-23 05:30:00, 2021-12-23 06:00:00) | \n",
+ " 2021-12-23 05:30:00+00:00 | \n",
+ " 2021-12-23 06:00:00+00:00 | \n",
+ "
\n",
+ " \n",
+ " [2021-12-23 06:00:00, 2021-12-23 06:30:00) | \n",
+ " 2021-12-23 06:00:00+00:00 | \n",
+ " 2021-12-23 06:30:00+00:00 | \n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
"
+ ],
+ "text/plain": [
+ " left_side \\\n",
+ "IntervalIndex \n",
+ "[2021-12-23 03:30:00, 2021-12-23 04:00:00) 2021-12-23 03:30:00+00:00 \n",
+ "[2021-12-23 05:00:00, 2021-12-23 05:30:00) 2021-12-23 05:00:00+00:00 \n",
+ "[2021-12-23 05:30:00, 2021-12-23 06:00:00) 2021-12-23 05:30:00+00:00 \n",
+ "[2021-12-23 06:00:00, 2021-12-23 06:30:00) 2021-12-23 06:00:00+00:00 \n",
+ "\n",
+ " right_side \n",
+ "IntervalIndex \n",
+ "[2021-12-23 03:30:00, 2021-12-23 04:00:00) 2021-12-23 04:00:00+00:00 \n",
+ "[2021-12-23 05:00:00, 2021-12-23 05:30:00) 2021-12-23 05:30:00+00:00 \n",
+ "[2021-12-23 05:30:00, 2021-12-23 06:00:00) 2021-12-23 06:00:00+00:00 \n",
+ "[2021-12-23 06:00:00, 2021-12-23 06:30:00) 2021-12-23 06:30:00+00:00 "
+ ]
+ },
+ "execution_count": 40,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "one_min = pd.Timedelta(1, \"T\")\n",
+ "index = hkg.trading_index(start_min + one_min, end_min - one_min, \"30T\", intervals=True)\n",
+ "show_as_df(index)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "In short, when returning an `IntervalIndex`:\n",
+ "* the first indice will never include a period that falls before `start`.\n",
+ "* the last indice will never include a period that falls after `end`.\n",
+ "\n",
+ "The same is not true for when a `DatetimeIndex` is returned. In this case the first and last indices are defined without consideration to the periods that the indices may represent.\n",
+ "\n",
+ "When returning an `DatetimeIndex`:\n",
+ "* The first indice will be either `start`, if start is an indice, or otherwise the nearest indice that follows `start`. \n",
+ "* The last indice will be either `end`, if end is an indice, or otherwise the nearest indice that preceeds `end`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 41,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "start_min=Timestamp('2021-12-23 03:00:00')\tend_min=Timestamp('2021-12-23 07:00:00')\n"
+ ]
+ },
+ {
+ "data": {
+ "text/plain": [
+ "DatetimeIndex(['2021-12-23 03:00:00+00:00', '2021-12-23 03:30:00+00:00',\n",
+ " '2021-12-23 05:00:00+00:00', '2021-12-23 05:30:00+00:00',\n",
+ " '2021-12-23 06:00:00+00:00', '2021-12-23 06:30:00+00:00',\n",
+ " '2021-12-23 07:00:00+00:00'],\n",
+ " dtype='datetime64[ns, UTC]', freq=None)"
+ ]
+ },
+ "execution_count": 41,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "print(f\"{start_min=}\\t{end_min=}\") # for reference\n",
+ "hkg.trading_index(start_min, end_min, \"30T\", closed=\"left\", intervals=False)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Note the different interpretation of what `end` represents. Here the `end` indice is included even though it represents a period (07:00 - 07:30) that falls after `end_min`. If intervals=True then the analogous indice is excluded."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 42,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " | \n",
+ " left_side | \n",
+ " right_side | \n",
+ "
\n",
+ " \n",
+ " IntervalIndex | \n",
+ " | \n",
+ " | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " [2021-12-23 03:00:00, 2021-12-23 03:30:00) | \n",
+ " 2021-12-23 03:00:00+00:00 | \n",
+ " 2021-12-23 03:30:00+00:00 | \n",
+ "
\n",
+ " \n",
+ " [2021-12-23 03:30:00, 2021-12-23 04:00:00) | \n",
+ " 2021-12-23 03:30:00+00:00 | \n",
+ " 2021-12-23 04:00:00+00:00 | \n",
+ "
\n",
+ " \n",
+ " [2021-12-23 05:00:00, 2021-12-23 05:30:00) | \n",
+ " 2021-12-23 05:00:00+00:00 | \n",
+ " 2021-12-23 05:30:00+00:00 | \n",
+ "
\n",
+ " \n",
+ " [2021-12-23 05:30:00, 2021-12-23 06:00:00) | \n",
+ " 2021-12-23 05:30:00+00:00 | \n",
+ " 2021-12-23 06:00:00+00:00 | \n",
+ "
\n",
+ " \n",
+ " [2021-12-23 06:00:00, 2021-12-23 06:30:00) | \n",
+ " 2021-12-23 06:00:00+00:00 | \n",
+ " 2021-12-23 06:30:00+00:00 | \n",
+ "
\n",
+ " \n",
+ " [2021-12-23 06:30:00, 2021-12-23 07:00:00) | \n",
+ " 2021-12-23 06:30:00+00:00 | \n",
+ " 2021-12-23 07:00:00+00:00 | \n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
"
+ ],
+ "text/plain": [
+ " left_side \\\n",
+ "IntervalIndex \n",
+ "[2021-12-23 03:00:00, 2021-12-23 03:30:00) 2021-12-23 03:00:00+00:00 \n",
+ "[2021-12-23 03:30:00, 2021-12-23 04:00:00) 2021-12-23 03:30:00+00:00 \n",
+ "[2021-12-23 05:00:00, 2021-12-23 05:30:00) 2021-12-23 05:00:00+00:00 \n",
+ "[2021-12-23 05:30:00, 2021-12-23 06:00:00) 2021-12-23 05:30:00+00:00 \n",
+ "[2021-12-23 06:00:00, 2021-12-23 06:30:00) 2021-12-23 06:00:00+00:00 \n",
+ "[2021-12-23 06:30:00, 2021-12-23 07:00:00) 2021-12-23 06:30:00+00:00 \n",
+ "\n",
+ " right_side \n",
+ "IntervalIndex \n",
+ "[2021-12-23 03:00:00, 2021-12-23 03:30:00) 2021-12-23 03:30:00+00:00 \n",
+ "[2021-12-23 03:30:00, 2021-12-23 04:00:00) 2021-12-23 04:00:00+00:00 \n",
+ "[2021-12-23 05:00:00, 2021-12-23 05:30:00) 2021-12-23 05:30:00+00:00 \n",
+ "[2021-12-23 05:30:00, 2021-12-23 06:00:00) 2021-12-23 06:00:00+00:00 \n",
+ "[2021-12-23 06:00:00, 2021-12-23 06:30:00) 2021-12-23 06:30:00+00:00 \n",
+ "[2021-12-23 06:30:00, 2021-12-23 07:00:00) 2021-12-23 07:00:00+00:00 "
+ ]
+ },
+ "execution_count": 42,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "index = hkg.trading_index(start_min, end_min, \"30T\", closed=\"left\", intervals=True)\n",
+ "show_as_df(index)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Final point on passing `start` and/or `end` as times:\n",
+ "\n",
+ "**If the period is one day then `start` and/or `end` cannot be passed as times.**"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### `Overlapping indices`"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Now, if you've been playing with `trading_index` as you've worked through this tutorial and you've managed to get this far without raising an error, then you just haven't been trying hard enough..."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 43,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "IntervalIndex([[2021-12-23 01:30:00, 2021-12-23 03:15:00), [2021-12-23 03:15:00, 2021-12-23 05:00:00), [2021-12-23 05:00:00, 2021-12-23 06:45:00), [2021-12-23 06:45:00, 2021-12-23 08:30:00)], dtype='interval[datetime64[ns, UTC], left]')"
+ ]
+ },
+ "execution_count": 43,
"metadata": {},
"output_type": "execute_result"
}
@@ -2111,18 +2577,17 @@
"metadata": {},
"outputs": [],
"source": [
- "period_106 = hkg.trading_index(start, start, \"106T\")\n",
- "# Run call for full traceback"
+ "period_106 = hkg.trading_index(start, start, \"106T\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "```python\n",
+ "```\n",
"---------------------------------------------------------------------------\n",
"IntervalsOverlapError Traceback (most recent call last)\n",
- "~\\AppData\\Local\\Temp/ipykernel_16260/3133521167.py in \n",
+ "Input In [44], in ()\n",
"----> 1 period_106 = hkg.trading_index(start, start, \"106T\")\n",
"\n",
"IntervalsOverlapError: Unable to create trading index as intervals would overlap. This can occur when the frequency is longer than a break or the gap between one session's close and the next session's open. To shorten intervals that would otherwise overlap either pass `curtail_overlaps` as True or pass `force_close` and/or `force_break_close` as True.\n",
@@ -2138,7 +2603,7 @@
},
{
"cell_type": "code",
- "execution_count": 38,
+ "execution_count": 45,
"metadata": {},
"outputs": [
{
@@ -2212,7 +2677,7 @@
"[2021-12-23 06:45:00, 2021-12-23 08:30:00) 2021-12-23 08:30:00+00:00 "
]
},
- "execution_count": 38,
+ "execution_count": 45,
"metadata": {},
"output_type": "execute_result"
}
@@ -2225,12 +2690,12 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "See how the right side of the second indice (the last indice of the morning subsession) has bumped right up against the left side of the third indice (the first indice of the afternoon subsession). The indices don't overlap because all the intervals are all `closed` on the \"left\" (by default), so the exact instance '2021-12-23 05:00:00' is present in the third indice..."
+ "See how the right side of the second indice (the last indice of the morning subsession) has bumped right up against the left side of the third indice (the first indice of the afternoon subsession). The indices don't overlap because all the intervals are all `closed` on the \"left\" (by default), so the exact timestamp '2021-12-23 05:00:00' is present in the third indice..."
]
},
{
"cell_type": "code",
- "execution_count": 39,
+ "execution_count": 46,
"metadata": {},
"outputs": [
{
@@ -2239,7 +2704,7 @@
"True"
]
},
- "execution_count": 39,
+ "execution_count": 46,
"metadata": {},
"output_type": "execute_result"
}
@@ -2257,7 +2722,7 @@
},
{
"cell_type": "code",
- "execution_count": 40,
+ "execution_count": 47,
"metadata": {},
"outputs": [
{
@@ -2266,7 +2731,7 @@
"False"
]
},
- "execution_count": 40,
+ "execution_count": 47,
"metadata": {},
"output_type": "execute_result"
}
@@ -2284,7 +2749,7 @@
},
{
"cell_type": "code",
- "execution_count": 41,
+ "execution_count": 48,
"metadata": {},
"outputs": [
{
@@ -2358,7 +2823,7 @@
"[2021-12-23 06:46:00, 2021-12-23 08:32:00) 2021-12-23 08:32:00+00:00 "
]
},
- "execution_count": 41,
+ "execution_count": 48,
"metadata": {},
"output_type": "execute_result"
}
@@ -2376,7 +2841,7 @@
},
{
"cell_type": "code",
- "execution_count": 42,
+ "execution_count": 49,
"metadata": {},
"outputs": [
{
@@ -2450,7 +2915,7 @@
"[2021-12-23 06:51:00, 2021-12-23 08:42:00) 2021-12-23 08:42:00+00:00 "
]
},
- "execution_count": 42,
+ "execution_count": 49,
"metadata": {},
"output_type": "execute_result"
}
@@ -2469,7 +2934,7 @@
},
{
"cell_type": "code",
- "execution_count": 43,
+ "execution_count": 50,
"metadata": {},
"outputs": [
{
@@ -2480,7 +2945,7 @@
" dtype='timedelta64[ns]', freq=None)"
]
},
- "execution_count": 43,
+ "execution_count": 50,
"metadata": {},
"output_type": "execute_result"
}
@@ -2500,7 +2965,7 @@
},
{
"cell_type": "code",
- "execution_count": 44,
+ "execution_count": 51,
"metadata": {},
"outputs": [
{
@@ -2567,7 +3032,7 @@
"[2021-12-23 07:30:00, 2021-12-23 10:00:00) 2021-12-23 10:00:00+00:00 "
]
},
- "execution_count": 44,
+ "execution_count": 51,
"metadata": {},
"output_type": "execute_result"
}
@@ -2587,7 +3052,7 @@
},
{
"cell_type": "code",
- "execution_count": 45,
+ "execution_count": 52,
"metadata": {},
"outputs": [
{
@@ -2619,43 +3084,43 @@
" \n",
" \n",
" \n",
- " 2021-12-01 00:00:00+00:00 | \n",
- " 2021-11-30 23:00:00 | \n",
+ " 2021-12-01 | \n",
+ " 2021-11-30 23:00:00+00:00 | \n",
" NaT | \n",
" NaT | \n",
- " 2021-12-01 23:00:00 | \n",
+ " 2021-12-01 23:00:00+00:00 | \n",
" \n",
" \n",
- " 2021-12-02 00:00:00+00:00 | \n",
- " 2021-12-01 23:00:00 | \n",
+ " 2021-12-02 | \n",
+ " 2021-12-01 23:00:00+00:00 | \n",
" NaT | \n",
" NaT | \n",
- " 2021-12-02 23:00:00 | \n",
+ " 2021-12-02 23:00:00+00:00 | \n",
" \n",
" \n",
- " 2021-12-03 00:00:00+00:00 | \n",
- " 2021-12-02 23:00:00 | \n",
+ " 2021-12-03 | \n",
+ " 2021-12-02 23:00:00+00:00 | \n",
" NaT | \n",
" NaT | \n",
- " 2021-12-03 23:00:00 | \n",
+ " 2021-12-03 23:00:00+00:00 | \n",
" \n",
" \n",
"\n",
""
],
"text/plain": [
- " market_open break_start break_end \\\n",
- "2021-12-01 00:00:00+00:00 2021-11-30 23:00:00 NaT NaT \n",
- "2021-12-02 00:00:00+00:00 2021-12-01 23:00:00 NaT NaT \n",
- "2021-12-03 00:00:00+00:00 2021-12-02 23:00:00 NaT NaT \n",
- "\n",
- " market_close \n",
- "2021-12-01 00:00:00+00:00 2021-12-01 23:00:00 \n",
- "2021-12-02 00:00:00+00:00 2021-12-02 23:00:00 \n",
- "2021-12-03 00:00:00+00:00 2021-12-03 23:00:00 "
+ " market_open break_start break_end \\\n",
+ "2021-12-01 2021-11-30 23:00:00+00:00 NaT NaT \n",
+ "2021-12-02 2021-12-01 23:00:00+00:00 NaT NaT \n",
+ "2021-12-03 2021-12-02 23:00:00+00:00 NaT NaT \n",
+ "\n",
+ " market_close \n",
+ "2021-12-01 2021-12-01 23:00:00+00:00 \n",
+ "2021-12-02 2021-12-02 23:00:00+00:00 \n",
+ "2021-12-03 2021-12-03 23:00:00+00:00 "
]
},
- "execution_count": 45,
+ "execution_count": 52,
"metadata": {},
"output_type": "execute_result"
}
@@ -2677,7 +3142,7 @@
},
{
"cell_type": "code",
- "execution_count": 46,
+ "execution_count": 53,
"metadata": {},
"outputs": [
{
@@ -2786,7 +3251,7 @@
"[2021-12-03 15:00:00, 2021-12-03 23:00:00) 2021-12-03 23:00:00+00:00 "
]
},
- "execution_count": 46,
+ "execution_count": 53,
"metadata": {},
"output_type": "execute_result"
}
@@ -2808,18 +3273,17 @@
"metadata": {},
"outputs": [],
"source": [
- "cmes.trading_index(start24, end24, \"7T\")\n",
- "# Run cell for full traceback"
+ "cmes.trading_index(start24, end24, \"7T\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "```python\n",
+ "```\n",
"---------------------------------------------------------------------------\n",
"IntervalsOverlapError Traceback (most recent call last)\n",
- "~\\AppData\\Local\\Temp/ipykernel_16260/821017084.py in \n",
+ "Input In [54], in ()\n",
"----> 1 cmes.trading_index(start24, end24, \"7T\")\n",
"\n",
"IntervalsOverlapError: Unable to create trading index as intervals would overlap. This can occur when the frequency is longer than a break or the gap between one session's close and the next session's open. To shorten intervals that would otherwise overlap either pass `curtail_overlaps` as True or pass `force_close` and/or `force_break_close` as True.\n",
@@ -2828,7 +3292,7 @@
},
{
"cell_type": "code",
- "execution_count": 48,
+ "execution_count": 55,
"metadata": {},
"outputs": [
{
@@ -2954,7 +3418,7 @@
"[618 rows x 2 columns]"
]
},
- "execution_count": 48,
+ "execution_count": 55,
"metadata": {},
"output_type": "execute_result"
}
@@ -2967,7 +3431,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "Overlapping indices are also a consideration for trading indexes output as `DatetimeIndex`, although only if the index is being `closed` on either the \"right\" side or \"both\" (it's not possible for a `DatetimeIndex` `closed` on the \"left\" side or \"neither\" to have overlapping indices as the right side is not defined, and hence cannot overlap the left side of the following indice).\n",
+ "Overlapping indices are also a consideration for trading indexes output as `DatetimeIndex`, although only if the index is being `closed` on either the \"right\" side or \"both\" sides (it's not possible for a `DatetimeIndex` `closed` on the \"left\" side or \"neither\" side to have overlapping indices as the right side is not defined, and hence cannot overlap the left side of the following indice).\n",
"\n",
"If the index is being `closed` on the \"right\" then the periods represented by the indices will overlap whenever they would overlap for the equivalent `IntervalIndex`. Only difference is that an `IndicesOverlapError` is raised rather than an `IntervalsOverlapError`."
]
@@ -2985,10 +3449,10 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "```python\n",
+ "```\n",
"---------------------------------------------------------------------------\n",
"IndicesOverlapError Traceback (most recent call last)\n",
- "~\\AppData\\Local\\Temp/ipykernel_16260/3132062204.py in \n",
+ "Input In [56], in ()\n",
"----> 1 hkg.trading_index(start, start, \"106T\", closed=\"right\", intervals=False)\n",
"\n",
"IndicesOverlapError: Unable to create trading index as an indice would fall to the right of (later than) the subsequent indice. This can occur when the frequency is longer than a break or the frequency is longer than the gap between one session's close and the next session's open. Consider passing `closed` as `left` or passing `force_close` and/or `force_break_close` as True.\n",
@@ -3004,7 +3468,7 @@
},
{
"cell_type": "code",
- "execution_count": 50,
+ "execution_count": 57,
"metadata": {},
"outputs": [
{
@@ -3015,7 +3479,7 @@
" dtype='datetime64[ns, UTC]', freq=None)"
]
},
- "execution_count": 50,
+ "execution_count": 57,
"metadata": {},
"output_type": "execute_result"
}
@@ -3044,10 +3508,10 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "```python\n",
+ "```\n",
"---------------------------------------------------------------------------\n",
"IndicesOverlapError Traceback (most recent call last)\n",
- "~\\AppData\\Local\\Temp/ipykernel_16260/377205800.py in \n",
+ "Input In [58], in ()\n",
"----> 1 hkg.trading_index(start, start, \"105T\", closed=\"both\", intervals=False)\n",
"\n",
"IndicesOverlapError: Unable to create trading index as an indice would fall to the right of (later than) the subsequent indice. This can occur when the frequency is longer than a break or the frequency is longer than the gap between one session's close and the next session's open. Consider passing `closed` as `left` or passing `force_close` and/or `force_break_close` as True.\n",
@@ -3066,9 +3530,9 @@
],
"metadata": {
"kernelspec": {
- "display_name": "xcals 3.7",
+ "display_name": "Python38 xcals",
"language": "python",
- "name": "xcals"
+ "name": "py38_xcals"
},
"language_info": {
"codemirror_mode": {
@@ -3080,7 +3544,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.7.0"
+ "version": "3.8.10"
},
"widgets": {
"application/vnd.jupyter.widget-state+json": {
diff --git a/exchange_calendars/calendar_helpers.py b/exchange_calendars/calendar_helpers.py
index 01aa647d..b8252b85 100644
--- a/exchange_calendars/calendar_helpers.py
+++ b/exchange_calendars/calendar_helpers.py
@@ -114,6 +114,42 @@ def one_minute_later(arr: np.ndarray) -> np.ndarray:
return arr
+def is_date(ts: pd.Timestamp) -> bool:
+ """Query if a timestamp represents a date (as opposed to a time).
+
+ `ts` considered to represent a date if tz-naive and has time component
+ as 00:00.
+
+ Parameters
+ ----------
+ ts
+ Timestamp to query.
+
+ Returns
+ -------
+ bool
+ Boolean indicating if `ts` represents a date.
+ """
+ return ts.tz is None and ts == ts.normalize()
+
+
+def to_utc(ts: pd.Timestamp) -> pd.Timestamp:
+ """Return a copy of a given timestamp with timezone set to UTC.
+
+ If `ts` is tz-aware will convert `ts` to UTC.
+ If `ts` is tz-naive will localize as UTC.
+
+ Parameters
+ ----------
+ ts
+ Timestamp to return a copy of with timezone set to UTC.
+ """
+ try:
+ return ts.tz_convert(pytz.UTC)
+ except TypeError:
+ return ts.tz_localize(pytz.UTC)
+
+
def parse_timestamp(
timestamp: Date | Minute,
param_name: str = "minute",
@@ -217,6 +253,43 @@ def parse_timestamp(
return ts
+def parse_date_or_minute(
+ ts: Date | Minute,
+ param_name: str,
+ calendar: ExchangeCalendar,
+) -> tuple[pd.Timestamp, bool]:
+ """Parse input that can be interpreted as a Date or a Minute.
+
+ Parameters
+ ----------
+ ts
+ Input that can be interpreted as a Date or a Minute. Must be valid
+ input to pd.Timestamp.
+
+ param_name
+ Name of a parameter that was to receive a Date or a Minute.
+
+ calendar
+ ExchangeCalendar against which to evaluate out-of-bounds
+ timestamps.
+
+ Returns
+ -------
+ tuple[pd.Timestamp, bool]:
+ [0] Parsed input to a pd.Timestamp.
+ [1] Boolean indicating if input was interpreted as a Minute (True)
+ or a Date (False).
+ """
+ ts = parse_timestamp(ts, param_name, calendar, raise_oob=False, utc=False)
+ is_time = not is_date(ts)
+ ts = to_utc(ts) if is_time else ts
+ if is_time and calendar._minute_oob(ts):
+ raise errors.MinuteOutOfBounds(calendar, ts, param_name)
+ elif not is_time and calendar._date_oob(ts):
+ raise errors.DateOutOfBounds(calendar, ts, param_name)
+ return ts, is_time
+
+
def parse_trading_minute(
calendar: ExchangeCalendar, minute: TradingMinute, param_name: str = "minute"
) -> pd.Timestamp:
@@ -374,10 +447,11 @@ class _TradingIndex:
def __init__(
self,
calendar: ExchangeCalendar,
- start: Date,
- end: Date,
+ start_: Date | Minute,
+ end_: Date | Minute,
period: pd.Timedelta,
- closed: str, # Literal["left", "right", "both", "neither"] when min python 3.8
+ # TODO Literal["left", "right", "both", "neither"] when min python 3.8...
+ closed: str,
force_close: bool,
force_break_close: bool,
curtail_overlaps: bool,
@@ -388,6 +462,16 @@ def __init__(
self.force_close = force_close
self.curtail_overlaps = curtail_overlaps
+ # parse `start_` and `end_`
+ start_, self.start_as_time = parse_date_or_minute(start_, "start", calendar)
+ end_, self.end_as_time = parse_date_or_minute(end_, "end", calendar)
+ self.start_, self.end_ = start_, end_
+ # define start and end as sessions
+ start = (
+ calendar.minute_to_session(start_, "next") if self.start_as_time else start_
+ )
+ end = calendar.minute_to_session(end_, "previous") if self.end_as_time else end_
+
# get session bound values over requested range
slice_start = calendar.sessions.searchsorted(start)
slice_end = calendar.sessions.searchsorted(end, side="right")
@@ -483,7 +567,7 @@ def _create_index_for_sessions(
if not on_freq:
num_indices -= 1 # add the close later
else:
- on_freq = False
+ on_freq = True
if self.closed == "both":
num_indices += 1
@@ -554,13 +638,31 @@ def _trading_index(self) -> np.ndarray:
return index
+ def curtail_for_times(
+ self, index: pd.DatetimeIndex | pd.IntervalIndex
+ ) -> pd.DatetimeIndex | pd.IntervalIndex:
+ """Curtail start and end of trading index.
+
+ Curtails any unwanted rows from the start and end of `index` if
+ class received `start_` and/or `end_` as times.
+ """
+ intervals = isinstance(index, pd.IntervalIndex)
+ bv: np.ndarray | None = None
+ if self.start_as_time:
+ bv = index.left >= self.start_ if intervals else index >= self.start_
+ if self.end_as_time:
+ bv_end = index.right <= self.end_ if intervals else index <= self.end_
+ bv = bv & bv_end if bv is not None else bv_end
+ return index if bv is None else index[bv]
+
def trading_index(self) -> pd.DatetimeIndex:
"""Create trading index as a DatetimeIndex."""
self.verify_non_overlapping()
index = self._trading_index()
if self.has_break:
index.sort()
- return pd.DatetimeIndex(index, tz="UTC")
+ index = pd.DatetimeIndex(index, tz="UTC")
+ return self.curtail_for_times(index)
@contextlib.contextmanager
def _override_defaults(self, **kwargs):
@@ -599,4 +701,5 @@ def trading_index_intervals(self) -> pd.IntervalIndex:
left = pd.DatetimeIndex(left, tz="UTC")
right = pd.DatetimeIndex(right, tz="UTC")
- return pd.IntervalIndex.from_arrays(left, right, self.closed)
+ index = pd.IntervalIndex.from_arrays(left, right, self.closed)
+ return self.curtail_for_times(index)
diff --git a/exchange_calendars/exchange_calendar.py b/exchange_calendars/exchange_calendar.py
index f16568f2..cd7f0d96 100644
--- a/exchange_calendars/exchange_calendar.py
+++ b/exchange_calendars/exchange_calendar.py
@@ -2278,11 +2278,12 @@ def sessions_minutes_count(
def trading_index(
self,
- start: Date,
- end: Date,
+ start: Date | Minute,
+ end: Date | Minute,
period: pd.Timedelta | str,
intervals: bool = True,
- closed: str = "left", # when move to min 3.8 Literal["left", "right", "both", "neither"]
+ # TODO Literal["left", "right", "both", "neither"] when min python 3.8...
+ closed: str = "left",
force_close: bool = False,
force_break_close: bool = False,
force: bool | None = None,
@@ -2309,12 +2310,55 @@ def trading_index(
Parameters
----------
start
- Start of session range over which to create index. Must be
- timezone naive.
+ Timestamp representing start of index.
+
+ If `start` is passed as a date then the first indice will be:
+ if `start` is a session, then the first indice of that
+ session (i.e. the left side of the first indice will be
+ the session open).
+ otherwise, the first indice of the nearest session
+ following `start`.
+
+ If `start` is passed as a minute then the first indice will be:
+ if `start` coincides with (the left side of*) an indice,
+ then that indice.
+ otherwise the nearest indice to `start` (with a left side*)
+ that is later than `start`.
+ * if `intervals` is True (default)
+
+ `start` will be interpreted as a date if it is timezone-naive
+ and does not have a time component (or any time component is
+ 00:00). Otherwise `start` will be interpreted as a time.
+
+ If `period` is one day ("1d") then `start` must be passed as
+ a date. The first indice will be either `start`, if `start` is
+ a session, or otherwise the nearest session following `start`.
end
- End of session range over which to create index. Must be
- timezone naive.
+ Timestamp representing end of index.
+
+ If `end` is passed as a date then the last indice will be:
+ if `end` is a session, then the last indice of that
+ session (i.e. either the right side of the final indice
+ will be the session close or the final indice will
+ contain the session close).
+ otherwise, the last indice of the nearest session
+ preceeding `end`.
+
+ If `end` is passed as a minute then the last indice will be:
+ if `end` coincides with (the right side of*) an indice,
+ then that indice.
+ otherwise the nearest indice to `end` (with a right side*)
+ that is earlier than `end`.
+ * if `intervals` is True (default)
+
+ `end` will be interpreted as a date if it is timezone-naive
+ and does not have a time component (or any time component is
+ 00:00). Otherwise `start` will be interpreted as a time.
+
+ If `period` is one day ("1d") then `end` must be passed as
+ a date. The last indice will be either `end`, if `end` is
+ a session, or otherwise the nearest session prceeding `end`.
period
If `intervals` is True, the length of each interval. If
@@ -2465,8 +2509,6 @@ def trading_index(
variation of which is employed within the underlying _TradingIndex
class).
"""
- start, end = self._parse_start_end_dates(start, end, parse)
-
if not isinstance(period, pd.Timedelta):
try:
period = pd.Timedelta(period)
@@ -2487,6 +2529,7 @@ def trading_index(
raise ValueError(msg)
if period == pd.Timedelta(1, "D"):
+ start, end = self._parse_start_end_dates(start, end, parse)
return self.sessions_in_range(start, end)
if intervals and closed in ["both", "neither"]:
diff --git a/setup.cfg b/setup.cfg
index 9c2cfc06..bcbf93ca 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -3,7 +3,7 @@ description-file = README.md
license_file = LICENSE
[tool:pytest]
-addopts = -v --doctest-modules --durations=15 --capture=no
+addopts = -v --doctest-modules --durations=15
testpaths =
tests
diff --git a/tests/test_calendar_helpers.py b/tests/test_calendar_helpers.py
index 77b53869..c32d96db 100644
--- a/tests/test_calendar_helpers.py
+++ b/tests/test_calendar_helpers.py
@@ -10,6 +10,7 @@
import numpy as np
import pandas as pd
import pytest
+import pytz
from hypothesis import assume, given, settings
from hypothesis import strategies as st
from pandas.testing import assert_index_equal
@@ -24,10 +25,44 @@
@pytest.fixture(scope="class")
-def one_minute() -> abc.Iterator[pd.Timedelta]:
+def one_min() -> abc.Iterator[pd.Timedelta]:
yield pd.Timedelta(1, "T")
+def test_is_date(one_min):
+ f = m.is_date
+ T = pd.Timestamp
+
+ assert f(T("2021-11-02"))
+ assert f(T("2021-11-02 00:00"))
+ assert f(T("2021-11-02 00:00:00.0000000"))
+ assert not f(T("2021-11-02 00:00:00.000001"))
+ assert not f(T("2021-11-01 23:59:00.999999"))
+ assert not f(T("2021-11-02 12:00"))
+
+ minutes = [
+ T("2021-11-02", tz=pytz.UTC),
+ T("2021-11-02", tz="US/Eastern"),
+ T("2021-11-02", tz=pytz.UTC).tz_convert("US/Eastern"),
+ ]
+ for minute in minutes:
+ assert not f(minute)
+ assert not f(minute + one_min)
+
+
+def test_is_utc():
+ f = m.to_utc
+ T = pd.Timestamp
+
+ expected = T("2021-11-02", tz="UTC")
+ assert f(T("2021-11-02", tz="UTC")) == expected
+ assert f(T("2021-11-02")) == expected
+
+ expected = T("2021-11-02 13:33", tz="UTC")
+ assert f(T("2021-11-02 13:33")) == expected
+ assert f(T("2021-11-02 09:33", tz="US/Eastern")) == expected
+
+
@pytest.fixture
def one_day() -> abc.Iterator[pd.DateOffset]:
yield pd.DateOffset(days=1)
@@ -108,13 +143,13 @@ def trading_minute() -> abc.Iterator[str]:
@pytest.fixture
-def minute_too_early(calendar, one_minute) -> abc.Iterator[pd.Timestamp]:
- yield calendar.first_minute - one_minute
+def minute_too_early(calendar, one_min) -> abc.Iterator[pd.Timestamp]:
+ yield calendar.first_minute - one_min
@pytest.fixture
-def minute_too_late(calendar, one_minute) -> abc.Iterator[pd.Timestamp]:
- yield calendar.last_minute + one_minute
+def minute_too_late(calendar, one_min) -> abc.Iterator[pd.Timestamp]:
+ yield calendar.last_minute + one_min
@pytest.fixture
@@ -205,6 +240,65 @@ def test_parse_timestamp_error_oob(
assert rtrn == minute_too_late
+def test_parse_date_or_minute_for_minute(
+ calendar, param_name, minute, minute_mult, date
+):
+ """Tests `parse_date_or_minute` for input that represents a Minute."""
+
+ def f(ts: pd.Timestamp) -> tuple[pd.Timestamp, bool]:
+ return m.parse_date_or_minute(ts, param_name, calendar)
+
+ assert f(minute_mult) == (pd.Timestamp(minute, tz=pytz.UTC), True)
+ # verify that midnight with tz as UTC intepreted as minute, not date.
+ assert f(pd.Timestamp(date, tz=pytz.UTC)) == (pd.Timestamp(date, tz=pytz.UTC), True)
+
+
+def test_parse_date_or_minute_for_date(calendar, param_name, date, date_mult):
+ """Tests `parse_date_or_minute` for input that represents a Minute."""
+ f = m.parse_date_or_minute
+ assert f(date_mult, param_name, calendar) == (pd.Timestamp(date), False)
+
+
+def test_parse_date_or_minute_oob(
+ calendar,
+ param_name,
+ date_too_early,
+ date_too_late,
+ minute_too_early,
+ minute_too_late,
+):
+ """Tests `parse_date_or_minute` for out-of-bounds input.
+
+ Tests as if an extension of parse_timestamp, i.e. only tests added
+ functionality.
+ """
+
+ def f(ts: pd.Timestamp) -> tuple[pd.Timestamp, bool]:
+ return m.parse_date_or_minute(ts, param_name, calendar)
+
+ # Verify raises errors for out-of-bounds ts
+ first_min = calendar.first_minute
+ last_min = calendar.last_minute
+ first_date = calendar.first_session
+ last_date = calendar.last_session
+ # verify returns at calendar's minute bounds
+ assert f(first_min) == (first_min, True)
+ assert f(last_min) == (last_min, True)
+ # verify raises error other side of calendar's minute bounds
+ with pytest.raises(errors.MinuteOutOfBounds):
+ f(minute_too_early)
+ with pytest.raises(errors.MinuteOutOfBounds):
+ f(minute_too_late)
+ # verify returns at calendar's session bounds
+ assert f(first_date) == (first_date, False)
+ assert f(last_date) == (last_date, False)
+ # verify raises error other side of calendar's session bounds
+ with pytest.raises(errors.DateOutOfBounds):
+ f(date_too_early)
+ with pytest.raises(errors.DateOutOfBounds):
+ f(date_too_late)
+
+
def test_parse_date(date_mult, param_name):
date = date_mult
dt = m.parse_date(date, param_name, raise_oob=False)
@@ -296,6 +390,7 @@ class TestTradingIndex:
Also includes:
- concrete tests to verify overlap handling.
+ - conceret test to verify passing start and/or end as a time.
- parsing tests for ExchangeCalendar.trading_index.
NOTE: `_TradingIndex` is also tested via
@@ -473,7 +568,7 @@ def bounds(start: pd.Series, end: pd.Series, force: bool):
As for `start` albeit indicating end times.
"""
lower_bounds = start if closed_left else start + period
- if force:
+ if force and closed_right:
if (lower_bounds > end).any():
# period longer than session/subsession duration
lower_bounds[lower_bounds > end] = end
@@ -543,7 +638,7 @@ def test_indices_fuzz(
calendars_with_answers,
force_close: bool,
force_break_close: bool,
- one_minute,
+ one_min,
):
"""Fuzz for unexpected errors and options behaviour.
@@ -566,7 +661,7 @@ def test_indices_fuzz(
closed = data.draw(st.sampled_from(closed_options))
closed_right = closed in ["right", "both"]
- max_period = pd.Timedelta(1, "D") - one_minute
+ max_period = pd.Timedelta(1, "D") - one_min
params_allow_overlap = closed_right and not (force_break_close and force_close)
if params_allow_overlap:
@@ -580,12 +675,12 @@ def test_indices_fuzz(
# guard against "neither" returning empty. Tested for under seprate test.
if closed == "neither":
- if has_break and not force_break_close:
- am_length = (ans.break_starts[slc] - ans.opens[slc]).min() - one_minute
- pm_length = (ans.closes[slc] - ans.break_ends[slc]).min() - one_minute
+ if has_break:
+ am_length = (ans.break_starts[slc] - ans.opens[slc]).min() - one_min
+ pm_length = (ans.closes[slc] - ans.break_ends[slc]).min() - one_min
max_period = min(max_period, am_length, pm_length)
- elif not force_close:
- min_length = (ans.closes[slc] - ans.opens[slc]).min() - one_minute
+ else:
+ min_length = (ans.closes[slc] - ans.opens[slc]).min() - one_min
max_period = min(max_period, min_length)
period = data.draw(self.st_periods(maximum=max_period))
@@ -647,7 +742,7 @@ def test_intervals_fuzz(
calendars_with_answers,
force_break_close: bool,
curtail: bool,
- one_minute,
+ one_min,
):
"""Fuzz for unexpected errors and options behaviour.
@@ -666,7 +761,7 @@ def test_intervals_fuzz(
force_close = data.draw(st.booleans())
closed = data.draw(st.sampled_from(["left", "right"]))
- max_period = pd.Timedelta(1, "D") - one_minute
+ max_period = pd.Timedelta(1, "D") - one_min
params_allow_overlap = not curtail and not (force_break_close and force_close)
if params_allow_overlap:
@@ -794,9 +889,7 @@ def test_daily_fuzz(
@pytest.mark.parametrize("name", ["XHKG", "24/7", "CMES"])
@given(data=st.data(), closed=st.sampled_from(["right", "both"]))
@settings(deadline=None)
- def test_overlap_error_fuzz(
- self, data, name, calendars, answers, closed, one_minute
- ):
+ def test_overlap_error_fuzz(self, data, name, calendars, answers, closed, one_min):
"""Fuzz for expected IndicesOverlapError.
NB. Test should exclude calendars, such as "XLON", for which
@@ -820,7 +913,7 @@ def test_overlap_error_fuzz(
else:
min_period = (ans.opens.shift(-1)[slc] - ans.closes[slc]).min()
- period = data.draw(self.st_periods(minimum=max(one_minute, min_period)))
+ period = data.draw(self.st_periods(minimum=max(one_min, min_period)))
# assume overlaps (i.e. reject test parameters if does not overlap)
op = operator.ge if closed == "both" else operator.gt
@@ -1014,9 +1107,304 @@ def test_ignore_breaks(self, cal_start_end):
rtrn = cal_amended.trading_index(**kwargs, ignore_breaks=False)
assert_index_equal(rtrn, index_true)
+ def test_start_end_times(self, one_min, calendars):
+ """Test effect of passing start and/or end as a time.
+
+ Tests passing start / end as combinations of dates and/or times.
+
+ Tests by comparing return with subset of return for start and end
+ as sessions.
+
+ Tests return with `intervals` as True (IntervalIndex) and False
+ (DatetimeIndex). With `intervals` as False test for all `closed`
+ options.
+ """
+ cal = calendars["XHKG"]
+ one_min = one_min
+
+ # Define a start session and end session as sessions of standard length
+ start_s = pd.Timestamp("2021-12-06")
+ end_s = pd.Timestamp("2021-12-20")
+
+ # assert of standard length
+ standard_length = pd.Timedelta(hours=6, minutes=30)
+ start_s_open, start_s_close = cal.session_open_close(start_s)
+ assert start_s_close - start_s_open == standard_length
+ end_s_open, end_s_close = cal.session_open_close(end_s)
+ assert end_s_close - end_s_open == standard_length
+
+ f = cal.trading_index
+
+ # Note: when intervals = False the return will include the indice coinciding
+ # with `start` and `end` even if the period that indice represents falls,
+ # respectively, before `start` or after `end`
+
+ def assertions(
+ starts: list[
+ tuple[
+ pd.Timestamp,
+ int | None,
+ int | None,
+ int | None,
+ int | None,
+ int | None,
+ ]
+ ],
+ ends: list[
+ tuple[
+ pd.Timestamp,
+ int | None,
+ int | None,
+ int | None,
+ int | None,
+ int | None,
+ ]
+ ],
+ period: pd.Timedelta | str,
+ force: bool,
+ ignore_breaks: bool,
+ curtail_overlaps: bool = False,
+ ):
+ """Assert returns slice of return for sessions.
+
+ Parameters
+ ----------
+ starts: list of tuple (see method signature) of:
+ [0] value to pass as start.
+ Other items define the start of slice of subset when start is [0] and:
+ [1] intervals is True.
+ [2] intervals is False and 'closed' is "left".
+ [3] intervals is False and 'closed' is "right".
+ [4] intervals is False and 'closed' is "both".
+ [5] intervals is False and 'closed' is "neither".
+
+ ends: list of tuple (see method signature) of:
+ [0] value to pass as end.
+ Other items define the end of slice of subset when end is [0] and:
+ [1] intervals is True.
+ [2] intervals is False and 'closed' is "left".
+ [3] intervals is False and 'closed' is "right".
+ [4] intervals is False and 'closed' is "both".
+ [5] intervals is False and 'closed' is "neither".
+
+ All other parameters will be passed thorugh to `trading_index`.
+ """
+ args_dates = (start_s, end_s, period)
+ kwargs = dict(
+ force=force,
+ ignore_breaks=ignore_breaks,
+ curtail_overlaps=curtail_overlaps,
+ )
+ for (start, slc_start, ssl, ssr, ssb, ssn), (
+ end,
+ slc_end,
+ sel,
+ ser,
+ seb,
+ sen,
+ ) in itertools.product(starts, ends):
+ args = (start, end, period)
+
+ # verify for intervals index
+ intervals = True
+ index_dates = f(*args_dates, intervals, **kwargs)
+ rtrn = f(*args, intervals=intervals, **kwargs)
+ assert_index_equal(rtrn, index_dates[slc_start:slc_end])
+
+ # verify for datetime index
+ intervals = False
+ closed = "left"
+ rtrn = f(*args, intervals=intervals, closed=closed, **kwargs)
+ index_dates = f(*args_dates, intervals, closed=closed, **kwargs)
+ assert_index_equal(rtrn, index_dates[ssl:sel])
+
+ closed = "right"
+ rtrn = f(*args, intervals=intervals, closed=closed, **kwargs)
+ index_dates = f(*args_dates, intervals, closed=closed, **kwargs)
+ assert_index_equal(rtrn, index_dates[ssr:ser])
+
+ closed = "both"
+ rtrn = f(*args, intervals=intervals, closed=closed, **kwargs)
+ index_dates = f(*args_dates, intervals, closed=closed, **kwargs)
+ assert_index_equal(rtrn, index_dates[ssb:seb])
+
+ closed = "neither"
+ rtrn = f(*args, intervals=intervals, closed=closed, **kwargs)
+ index_dates = f(*args_dates, intervals, closed=closed, **kwargs)
+ assert_index_equal(rtrn, index_dates[ssn:sen])
+
+ force, ignore_breaks = False, True
+
+ period = pd.Timedelta(1, "T")
+ delta = period * 22
+
+ starts = [
+ (start_s, None, None, None, None, None),
+ (start_s_open, None, None, None, None, None),
+ (start_s_open + delta, 22, 22, 21, 22, 21),
+ (start_s_open + delta - one_min, 21, 21, 20, 21, 20),
+ (start_s_open + delta + one_min, 23, 23, 22, 23, 22),
+ ]
+ ends = [
+ (end_s, None, None, None, None, None),
+ (end_s_close, None, None, None, None, None),
+ (end_s_close - delta, -22, -21, -22, -22, -21),
+ (end_s_close - delta + one_min, -21, -20, -21, -21, -20),
+ (end_s_close - delta - one_min, -23, -22, -23, -23, -22),
+ ]
+
+ assertions(starts, ends, period, force, ignore_breaks)
+
+ period = pd.Timedelta(5, "T")
+ delta = period * 2
+
+ starts = [
+ (start_s, None, None, None, None, None),
+ (start_s_open, None, None, None, None, None),
+ (start_s_open + delta, 2, 2, 1, 2, 1),
+ (start_s_open + delta - one_min, 2, 2, 1, 2, 1),
+ (start_s_open + delta + one_min, 3, 3, 2, 3, 2),
+ ]
+ ends = [
+ (end_s, None, None, None, None, None),
+ (end_s_close, None, None, None, None, None),
+ (end_s_close - delta, -2, -1, -2, -2, -1),
+ (end_s_close - delta + one_min, -2, -1, -2, -2, -1),
+ (end_s_close - delta - one_min, -3, -2, -3, -3, -2),
+ ]
+
+ assertions(starts, ends, period, force, ignore_breaks)
+
+ period = pd.Timedelta(1, "H")
+ end_s_open = cal.session_open(end_s)
+
+ # ignoring breaks...
+ # assert assumption that end unaligned by 30mins
+ assert (end_s_close - end_s_open) % period == pd.Timedelta(30, "T")
+
+ end_s_aligned_post_close = end_s_close + pd.Timedelta(30, "T")
+ end_s_break_end = cal.session_break_end(end_s)
+ # assert assumption that pm session 3H duration
+ assert end_s_close - end_s_break_end == pd.Timedelta(3, "H")
+
+ starts = [
+ (start_s, None, None, None, None, None),
+ (start_s_open, None, None, None, None, None),
+ (start_s_open + period, 1, 1, None, 1, None),
+ (start_s_open + period - one_min, 1, 1, None, 1, None),
+ (start_s_open + period + one_min, 2, 2, 1, 2, 1),
+ ]
+ ends = [
+ (end_s, None, None, None, None, None),
+ (end_s_aligned_post_close, None, None, None, None, None),
+ (end_s_aligned_post_close + one_min, None, None, None, None, None),
+ (end_s_aligned_post_close - one_min, -1, None, -1, -1, None),
+ (end_s_close, -1, None, -1, -1, None),
+ (end_s_aligned_post_close - period + one_min, -1, None, -1, -1, None),
+ (end_s_aligned_post_close - period, -1, None, -1, -1, None),
+ (end_s_aligned_post_close - period - one_min, -2, -1, -2, -2, -1),
+ (end_s_break_end, -4, -3, -4, -4, -3),
+ (end_s_break_end + pd.Timedelta(30, "T"), -3, -2, -3, -3, -2),
+ (end_s_break_end + pd.Timedelta(29, "T"), -4, -3, -4, -4, -3),
+ (end_s_break_end - pd.Timedelta(30, "T"), -4, -3, -4, -4, -3),
+ (end_s_break_end - pd.Timedelta(31, "T"), -5, -4, -5, -5, -4),
+ ]
+
+ assertions(starts, ends, period, force, ignore_breaks)
+
+ # verify effect of force
+ starts = [
+ (start_s, None, None, None, None, None),
+ (start_s_open, None, None, None, None, None),
+ (start_s_open + period, 1, 1, None, 1, None),
+ (start_s_open + period - one_min, 1, 1, None, 1, None),
+ (start_s_open + period + one_min, 2, 2, 1, 2, 1),
+ ]
+ ends = [
+ (end_s, None, None, None, None, None),
+ (end_s_aligned_post_close, None, None, None, None, None),
+ (end_s_close, None, None, None, None, None),
+ (end_s_close - one_min, -1, None, -1, -1, None),
+ (end_s_close - pd.Timedelta(30, "T"), -1, None, -1, -1, None),
+ (end_s_close - pd.Timedelta(31, "T"), -2, -1, -2, -2, -1),
+ # break end as before...
+ (end_s_break_end, -4, -3, -4, -4, -3),
+ (end_s_break_end + pd.Timedelta(30, "T"), -3, -2, -3, -3, -2),
+ (end_s_break_end + pd.Timedelta(29, "T"), -4, -3, -4, -4, -3),
+ (end_s_break_end - pd.Timedelta(30, "T"), -4, -3, -4, -4, -3),
+ (end_s_break_end - pd.Timedelta(31, "T"), -5, -4, -5, -5, -4),
+ ]
+
+ force = True
+ assertions(starts, ends, period, force, ignore_breaks)
+
+ # ACKNOWLEDGING BREAKS
+
+ end_s_break_start = cal.session_break_start(end_s)
+ # assert assumption that break start unaligned by 30mins
+ assert (end_s_break_start - end_s_open) % period == pd.Timedelta(30, "T")
+
+ starts = [
+ (start_s, None, None, None, None, None),
+ (start_s_open, None, None, None, None, None),
+ (start_s_open + period, 1, 1, None, 1, None),
+ (start_s_open + period - one_min, 1, 1, None, 1, None),
+ (start_s_open + period + one_min, 2, 2, 1, 2, 1),
+ ]
+ ends = [
+ (end_s, None, None, None, None, None),
+ (end_s_aligned_post_close, None, None, None, None, None),
+ (end_s_close, None, None, None, None, None),
+ (end_s_close - one_min, -1, None, -1, -1, None),
+ (end_s_close - period, -1, None, -1, -1, None),
+ (end_s_close - period - one_min, -2, -1, -2, -2, -1),
+ (end_s_break_end, -3, -2, -3, -3, -2),
+ (end_s_break_end + one_min, -3, -2, -3, -3, -2),
+ (end_s_break_end - one_min, -3, -3, -3, -4, -2),
+ (end_s_break_start, -4, -3, -4, -5, -2),
+ (end_s_break_start + pd.Timedelta(30, "T"), -3, -3, -3, -4, -2),
+ (end_s_break_start + pd.Timedelta(29, "T"), -4, -3, -4, -5, -2),
+ (end_s_break_start - pd.Timedelta(30, "T"), -4, -3, -4, -5, -2),
+ (end_s_break_start - pd.Timedelta(31, "T"), -5, -4, -5, -6, -3),
+ ]
+
+ force, ignore_breaks = False, False
+ # expected = expected_index(interval, force, ignore_breaks)
+ assertions(starts, ends, period, force, ignore_breaks)
+
+ # verifying effect of force when acknowledging breaks
+
+ starts = [
+ (start_s, None, None, None, None, None),
+ (start_s_open, None, None, None, None, None),
+ (start_s_open + period, 1, 1, None, 1, None),
+ (start_s_open + period - one_min, 1, 1, None, 1, None),
+ (start_s_open + period + one_min, 2, 2, 1, 2, 1),
+ ]
+ ends = [
+ (end_s, None, None, None, None, None),
+ (end_s_aligned_post_close, None, None, None, None, None),
+ # end_s_close and end_s_break_end as before
+ (end_s_close, None, None, None, None, None),
+ (end_s_close - one_min, -1, None, -1, -1, None),
+ (end_s_close - period, -1, None, -1, -1, None),
+ (end_s_close - period - one_min, -2, -1, -2, -2, -1),
+ (end_s_break_end, -3, -2, -3, -3, -2),
+ (end_s_break_end + one_min, -3, -2, -3, -3, -2),
+ (end_s_break_end - one_min, -3, -3, -3, -4, -2),
+ # end_s_break_start affected by force
+ (end_s_break_start, -3, -3, -3, -4, -2),
+ (end_s_break_start - one_min, -4, -3, -4, -5, -2),
+ (end_s_break_start - pd.Timedelta(30, "T"), -4, -3, -4, -5, -2),
+ (end_s_break_start - pd.Timedelta(31, "T"), -5, -4, -5, -6, -3),
+ ]
+
+ force, ignore_breaks = True, False
+ assertions(starts, ends, period, force, ignore_breaks)
+
# PARSING TESTS
- def test_parsing_errors(self, cal_start_end):
+ def test_parsing_errors(self, cal_start_end, one_min, one_day):
cal, start, end = cal_start_end
error_msg = (
"`period` cannot be greater than one day although received as"
@@ -1036,3 +1424,15 @@ def test_parsing_errors(self, cal_start_end):
cal.trading_index(
start, end, "20T", intervals=True, closed="both", parse=False
)
+
+ # Verify raises error if period "1D" and start or end not passed as a date.
+ start = pd.Timestamp("2018-05-01", tz=pytz.UTC)
+ end = pd.Timestamp("2018-05-31")
+ with pytest.raises(ValueError, match="a Date must be timezone naive"):
+ cal.trading_index(start, end, "1D")
+
+ start = pd.Timestamp("2018-05-01 00:01")
+ with pytest.raises(
+ ValueError, match="a Date must have a time component of 00:00"
+ ):
+ cal.trading_index(start, end, "1D")
diff --git a/tests/test_exchange_calendar.py b/tests/test_exchange_calendar.py
index fe6d3b10..591cab6a 100644
--- a/tests/test_exchange_calendar.py
+++ b/tests/test_exchange_calendar.py
@@ -3678,7 +3678,7 @@ def test_trading_index(self, calendars, answers):
Assumes default value (False) for each of `force_close`,
`force_break_close` and `curtail_overlaps`. See test class
`test_calendar_helpers.TestTradingIndex` for more comprehensive
- fuzz testing of select calendars (and parsing testing).
+ testing (including fuzz tests and parsing tests).
"""
cal, ans = calendars["left"], answers["left"]
| | | |