Skip to content

Commit

Permalink
Merge pull request #307 from rsheftel/dev
Browse files Browse the repository at this point in the history
v4.3.2
  • Loading branch information
rsheftel authored Dec 9, 2023
2 parents 810e29b + 201feab commit 3a43b5b
Show file tree
Hide file tree
Showing 77 changed files with 12,941 additions and 9,824 deletions.
1 change: 1 addition & 0 deletions .git-blame-ignore-revs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
fd632acb99b20dc7c8ed2ab03e35fccdd3deed70
9 changes: 3 additions & 6 deletions .github/config_new_release.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@

new_version: '4.3.0'
new_version: '4.3.2'

change_log: |
- Fixed for pandas 2.0 so all tests pass PR #282
- Move exchange_calendar*.py files to pandas_market_calendar/exchange_calendars/ PR #284
- Move holidays_*.py to pandas_market_calendar/holidays/ PR #284
- Major cleanup including unused imports PR #284
- Reformat all code using Black and make black a standard PR #290
- Add XNSE as a name for BSE calendar # 277
release_body: |
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/releaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ jobs:
- name: build package
run: python -m build

- uses: pypa/gh-action-pypi-publish@release/v1
with:
user: __token__
Expand Down
7 changes: 5 additions & 2 deletions .github/workflows/test_runner.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,12 @@ jobs:
- name: install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest coveralls
pip install pytest coveralls black===23.7.0
pip install .
- name: ensure black format
run: python -m black --check pandas_market_calendars tests

- name: run tests
run: |
coverage run --source=pandas_market_calendars -m pytest tests
Expand Down
12 changes: 12 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/psf/black
rev: 23.7.0
hooks:
- id: black
# It is recommended to specify the latest version of Python
# supported by your project here, or alternatively use
# pre-commit's default_language_version, see
# https://pre-commit.com/#top_level-default_language_version
language_version: python3.8
12 changes: 12 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@ If you would like to see a new feature in `pandas_market_calendars`, it is proba

### Pull requests

Please format all code using black.

If you create an editable install with the dev extras you can get a pre-commit hook set-up.

```
python -m venv .venv
. .venv/bin/activate
python -m pip install --upgrade pip
pip install -e .[dev]
pre-commit install
```

If you would like to fix something in `pandas_market_calendars` - improvements to documentation, bug fixes, feature implementations, etc - pull requests are welcome!

All pull requests should be opened against the `dev` branch and should include a clear and concise summary of the changes you made.
Expand Down
1 change: 1 addition & 0 deletions docs/calendars.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Exchange TASE TASEExchangeCalendar gabglus
Exchange HKEX HKEXExchangeCalendar Yes 1dot75cm
Exchange ASX ASXExchangeCalendar pulledlamb
Exchange BSE BSEExchangeCalendar rakesh1988
Exchange IEX IEXExchangeCalendar Yes carterjfulcher
========= ====== ===================== ============ ==========

Futures Calendars
Expand Down
8 changes: 8 additions & 0 deletions docs/change_log.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ Change Log

Updates
-------
4.3.2 (12/09/2023)
~~~~~~~~~~~~~~~~~~
- Reformat all code using Black and make black a standard PR #290
- Add XNSE as a name for BSE calendar
- Update holidays for BSE # 277
- Update holidays for CN # 305
- Add IEX to list of exchanges

4.3.1 (09/06/2023)
~~~~~~~~~~~~~~~~~~
- Fixed broken build PR #292
Expand Down
17 changes: 9 additions & 8 deletions pandas_market_calendars/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,21 @@

from .calendar_registry import get_calendar, get_calendar_names
from .calendar_utils import convert_freq, date_range, merge_schedules

# TODO: is the below needed? Can I replace all the imports on the calendars with ".market_calendar"
from .market_calendar import MarketCalendar

# if running in development there may not be a package
try:
__version__ = metadata.version('pandas_market_calendars')
__version__ = metadata.version("pandas_market_calendars")
except metadata.PackageNotFoundError:
__version__ = 'development'
__version__ = "development"

__all__ = [
'MarketCalendar',
'get_calendar',
'get_calendar_names',
'merge_schedules',
'date_range',
'convert_freq'
"MarketCalendar",
"get_calendar",
"get_calendar_names",
"merge_schedules",
"date_range",
"convert_freq",
]
14 changes: 9 additions & 5 deletions pandas_market_calendars/calendar_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
from .calendars.bmf import BMFExchangeCalendar
from .calendars.bse import BSEExchangeCalendar
from .calendars.cboe import CFEExchangeCalendar
from .calendars.cme import \
CMEEquityExchangeCalendar, \
CMEBondExchangeCalendar
from .calendars.cme import CMEEquityExchangeCalendar, CMEBondExchangeCalendar
from .calendars.cme_globex_base import CMEGlobexBaseExchangeCalendar
from .calendars.cme_globex_agriculture import CMEGlobexAgricultureExchangeCalendar
from .calendars.cme_globex_crypto import CMEGlobexCryptoExchangeCalendar
from .calendars.cme_globex_energy_and_metals import CMEGlobexEnergyAndMetalsExchangeCalendar
from .calendars.cme_globex_energy_and_metals import (
CMEGlobexEnergyAndMetalsExchangeCalendar,
)
from .calendars.cme_globex_equities import CMEGlobexEquitiesExchangeCalendar
from .calendars.cme_globex_fx import CMEGlobexFXExchangeCalendar
from .calendars.cme_globex_fixed_income import CMEGlobexFixedIncomeCalendar
Expand All @@ -21,7 +21,11 @@
from .calendars.lse import LSEExchangeCalendar
from .calendars.nyse import NYSEExchangeCalendar
from .calendars.ose import OSEExchangeCalendar
from .calendars.sifma import SIFMAUSExchangeCalendar, SIFMAUKExchangeCalendar, SIFMAJPExchangeCalendar
from .calendars.sifma import (
SIFMAUSExchangeCalendar,
SIFMAUKExchangeCalendar,
SIFMAJPExchangeCalendar,
)
from .calendars.six import SIXExchangeCalendar
from .calendars.sse import SSEExchangeCalendar
from .calendars.tase import TASEExchangeCalendar
Expand Down
128 changes: 82 additions & 46 deletions pandas_market_calendars/calendar_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
import pandas as pd
import numpy as np

def merge_schedules(schedules, how='outer'):

def merge_schedules(schedules, how="outer"):
"""
Given a list of schedules will return a merged schedule. The merge method (how) will either return the superset
of any datetime when any schedule is open (outer) or only the datetime where all markets are open (inner)
Expand All @@ -22,21 +23,31 @@ def merge_schedules(schedules, how='outer'):
"""
all_cols = [x.columns for x in schedules]
all_cols = list(itertools.chain(*all_cols))
if ('break_start' in all_cols) or ('break_end' in all_cols):
warnings.warn('Merge schedules will drop the break_start and break_end from result.')
if ("break_start" in all_cols) or ("break_end" in all_cols):
warnings.warn(
"Merge schedules will drop the break_start and break_end from result."
)

result = schedules[0]
for schedule in schedules[1:]:
result = result.merge(schedule, how=how, right_index=True, left_index=True)
if how == 'outer':
result['market_open'] = result.apply(lambda x: min(x.market_open_x, x.market_open_y), axis=1)
result['market_close'] = result.apply(lambda x: max(x.market_close_x, x.market_close_y), axis=1)
elif how == 'inner':
result['market_open'] = result.apply(lambda x: max(x.market_open_x, x.market_open_y), axis=1)
result['market_close'] = result.apply(lambda x: min(x.market_close_x, x.market_close_y), axis=1)
if how == "outer":
result["market_open"] = result.apply(
lambda x: min(x.market_open_x, x.market_open_y), axis=1
)
result["market_close"] = result.apply(
lambda x: max(x.market_close_x, x.market_close_y), axis=1
)
elif how == "inner":
result["market_open"] = result.apply(
lambda x: max(x.market_open_x, x.market_open_y), axis=1
)
result["market_close"] = result.apply(
lambda x: min(x.market_close_x, x.market_close_y), axis=1
)
else:
raise ValueError('how argument must be "inner" or "outer"')
result = result[['market_open', 'market_close']]
result = result[["market_open", "market_close"]]
return result


Expand All @@ -50,6 +61,7 @@ def convert_freq(index, frequency):
"""
return pd.DataFrame(index=index).asfreq(frequency).index


class _date_range:
"""
This is a callable class that should be used by calling the already initiated instance: `date_range`.
Expand Down Expand Up @@ -91,7 +103,7 @@ class _date_range:
"""

def __init__(self, schedule = None, frequency= None, closed='right', force_close=True):
def __init__(self, schedule=None, frequency=None, closed="right", force_close=True):
if not closed in ("left", "right", "both", None):
raise ValueError("closed must be 'left', 'right', 'both' or None.")
elif not force_close in (True, False, None):
Expand All @@ -100,24 +112,32 @@ def __init__(self, schedule = None, frequency= None, closed='right', force_close
self.closed = closed
self.force_close = force_close
self.has_breaks = False
if frequency is None: self.frequency = None
if frequency is None:
self.frequency = None
else:
self.frequency = pd.Timedelta(frequency)
if self.frequency > pd.Timedelta("1D"):
raise ValueError('Frequency must be 1D or higher frequency.')
raise ValueError("Frequency must be 1D or higher frequency.")

elif schedule.market_close.lt(schedule.market_open).any():
raise ValueError("Schedule contains rows where market_close < market_open,"
" please correct the schedule")
raise ValueError(
"Schedule contains rows where market_close < market_open,"
" please correct the schedule"
)

if "break_start" in schedule:
if not all([
schedule.market_open.le(schedule.break_start).all(),
schedule.break_start.le(schedule.break_end).all(),
schedule.break_end.le(schedule.market_close).all()]):
raise ValueError("Not all rows match the condition: "
"market_open <= break_start <= break_end <= market_close, "
"please correct the schedule")
if not all(
[
schedule.market_open.le(schedule.break_start).all(),
schedule.break_start.le(schedule.break_end).all(),
schedule.break_end.le(schedule.market_close).all(),
]
):
raise ValueError(
"Not all rows match the condition: "
"market_open <= break_start <= break_end <= market_close, "
"please correct the schedule"
)
self.has_breaks = True

def _check_overlap(self, schedule):
Expand All @@ -131,9 +151,11 @@ def _check_overlap(self, schedule):
end_times = schedule.start + num_bars * self.frequency

if end_times.gt(schedule.start.shift(-1)).any():
raise ValueError(f"The chosen frequency will lead to overlaps in the calculated index. "
f"Either choose a higher frequency or avoid setting force_close to None "
f"when setting closed to 'right', 'both' or None.")
raise ValueError(
f"The chosen frequency will lead to overlaps in the calculated index. "
f"Either choose a higher frequency or avoid setting force_close to None "
f"when setting closed to 'right', 'both' or None."
)

def _check_disappearing_session(self, schedule):
"""checks if requested frequency and schedule would lead to lost trading sessions.
Expand All @@ -142,12 +164,13 @@ def _check_disappearing_session(self, schedule):
:param schedule: pd.DataFrame with first column: 'start' and second column: 'end'
:raises UserWarning:"""
if self.force_close is False and self.closed == "right":

if (schedule.end- schedule.start).lt(self.frequency).any():
warnings.warn("An interval of the chosen frequency is larger than some of the trading sessions, "
"while closed== 'right' and force_close is False. This will make those trading sessions "
"disappear. Use a higher frequency or change the values of closed/force_close, to "
"keep this from happening.")
if (schedule.end - schedule.start).lt(self.frequency).any():
warnings.warn(
"An interval of the chosen frequency is larger than some of the trading sessions, "
"while closed== 'right' and force_close is False. This will make those trading sessions "
"disappear. Use a higher frequency or change the values of closed/force_close, to "
"keep this from happening."
)

def _calc_num_bars(self, schedule):
"""calculate the number of timestamps needed for each trading session.
Expand All @@ -156,25 +179,30 @@ def _calc_num_bars(self, schedule):
:return: pd.Series of float64"""
return np.ceil((schedule.end - schedule.start) / self.frequency)


def _calc_time_series(self, schedule):
"""Method used by date_range to calculate the trading index.
:param schedule: pd.DataFrame with first column: 'start' and second column: 'end'
:return: pd.Series of datetime64[ns, UTC]"""
:param schedule: pd.DataFrame with first column: 'start' and second column: 'end'
:return: pd.Series of datetime64[ns, UTC]"""
num_bars = self._calc_num_bars(schedule)

# ---> calculate the desired timeseries:
if self.closed == "left":
opens = schedule.start.repeat(num_bars) # keep as is
time_series = (opens.groupby(opens.index).cumcount()) * self.frequency + opens
opens = schedule.start.repeat(num_bars) # keep as is
time_series = (
opens.groupby(opens.index).cumcount()
) * self.frequency + opens
elif self.closed == "right":
opens = schedule.start.repeat(num_bars) # dont add row but shift up
time_series = (opens.groupby(opens.index).cumcount()+ 1) * self.frequency + opens
opens = schedule.start.repeat(num_bars) # dont add row but shift up
time_series = (
opens.groupby(opens.index).cumcount() + 1
) * self.frequency + opens
else:
num_bars += 1
opens = schedule.start.repeat(num_bars) # add row but dont shift up
time_series = (opens.groupby(opens.index).cumcount()) * self.frequency + opens
opens = schedule.start.repeat(num_bars) # add row but dont shift up
time_series = (
opens.groupby(opens.index).cumcount()
) * self.frequency + opens

if not self.force_close is None:
time_series = time_series[time_series.le(schedule.end.repeat(num_bars))]
Expand All @@ -183,8 +211,7 @@ def _calc_time_series(self, schedule):

return time_series


def __call__(self, schedule, frequency, closed='right', force_close=True, **kwargs):
def __call__(self, schedule, frequency, closed="right", force_close=True, **kwargs):
"""
See class docstring for more information.
Expand All @@ -205,15 +232,23 @@ def __call__(self, schedule, frequency, closed='right', force_close=True, **kwar
self.__init__(schedule, frequency, closed, force_close)
if self.has_breaks:
# rearrange the schedule, to make every row one session
before = schedule[["market_open", "break_start"]].set_index(schedule["market_open"])
after = schedule[["break_end", "market_close"]].set_index(schedule["break_end"])
before = schedule[["market_open", "break_start"]].set_index(
schedule["market_open"]
)
after = schedule[["break_end", "market_close"]].set_index(
schedule["break_end"]
)
before.columns = after.columns = ["start", "end"]
schedule = pd.concat([before, after]).sort_index()

else:
schedule = schedule.rename(columns= {"market_open": "start", "market_close": "end"})
schedule = schedule.rename(
columns={"market_open": "start", "market_close": "end"}
)

schedule = schedule[schedule.start.ne(schedule.end)] # drop the 'no-trading sessions'
schedule = schedule[
schedule.start.ne(schedule.end)
] # drop the 'no-trading sessions'
self._check_overlap(schedule)
self._check_disappearing_session(schedule)

Expand All @@ -222,4 +257,5 @@ def __call__(self, schedule, frequency, closed='right', force_close=True, **kwar
time_series.name = None
return pd.DatetimeIndex(time_series.drop_duplicates())


date_range = _date_range()
Loading

0 comments on commit 3a43b5b

Please sign in to comment.