fa45d8aa5f
- health_checklist.json: 192.168.1.122→node122
- ocr_client.py: docstring IP→node122
- docs/market-data-requirements.md: IP→node122
- 所有API调用通过ProxyHandler({})绕过系统代理
Privoxy对node122:18003返回500,直连正常
348 lines
11 KiB
Python
348 lines
11 KiB
Python
# https://github.com/pandas-dev/pandas/blob/master/pandas/tseries/offsets.py
|
|
# https://github.com/pandas-dev/pandas/blob/master/pandas/_libs/tslibs/offsets.pyx
|
|
|
|
import contextlib
|
|
from datetime import date, datetime, timedelta
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
import toolz
|
|
from dateutil.easter import EASTER_ORTHODOX, easter
|
|
from pandas._libs.tslibs.conversion import localize_pydatetime
|
|
from pandas._libs.tslibs.offsets import apply_wraps
|
|
from pandas.tseries.offsets import BaseOffset, CustomBusinessDay
|
|
|
|
|
|
class CompositeCustomBusinessDay(CustomBusinessDay):
|
|
_prefix = "C"
|
|
_attributes = (
|
|
"n",
|
|
"normalize",
|
|
"weekmask",
|
|
"holidays",
|
|
"calendar",
|
|
"offset",
|
|
"business_days",
|
|
)
|
|
|
|
def __init__(
|
|
self,
|
|
n=1,
|
|
normalize=False,
|
|
weekmask="Mon Tue Wed Thu Fri",
|
|
holidays=None,
|
|
calendar=None,
|
|
offset=timedelta(0),
|
|
business_days=None,
|
|
):
|
|
CustomBusinessDay.__init__(
|
|
self, n, normalize, weekmask, holidays, calendar, offset
|
|
)
|
|
self.business_days = business_days
|
|
|
|
def __setstate__(self, state):
|
|
self.business_days = state.pop("business_days")
|
|
CustomBusinessDay.__setstate__(self, state)
|
|
|
|
@property
|
|
def business_days(self):
|
|
"""
|
|
Returns list of tuples of (start_date, end_date, custom_business_day)
|
|
which overrides default behavior for the given interval, which starts
|
|
from start_date to end_date, inclusive in both sides.
|
|
"""
|
|
return tuple(self._business_days)
|
|
|
|
@business_days.setter
|
|
def business_days(self, business_days):
|
|
self._business_days = []
|
|
self._business_days_index = pd.IntervalIndex([], closed="both")
|
|
self._business_days_all_index = pd.IntervalIndex([], closed="both")
|
|
if business_days is not None:
|
|
self._business_days = business_days
|
|
business_days_intervals = []
|
|
for start_date, end_date, _ in self._business_days:
|
|
if start_date is None:
|
|
start_date = pd.Timestamp.min # noqa: PLW2901
|
|
else:
|
|
start_date = pd.Timestamp(start_date) # noqa: PLW2901
|
|
if end_date is None:
|
|
end_date = pd.Timestamp.max # noqa: PLW2901
|
|
else:
|
|
end_date = pd.Timestamp(end_date) # noqa: PLW2901
|
|
interval = pd.Interval(start_date, end_date, closed="both")
|
|
business_days_intervals.append(interval)
|
|
self._business_days_index = pd.IntervalIndex(
|
|
business_days_intervals, closed="both"
|
|
)
|
|
business_days_all_intervals = []
|
|
right = self._business_days_index[0]
|
|
if pd.Timestamp.min < right.left:
|
|
interval = pd.Interval(
|
|
pd.Timestamp.min, right.left - pd.Timedelta(1, unit="D")
|
|
)
|
|
business_days_all_intervals.append(interval)
|
|
business_days_all_intervals.append(right)
|
|
for left, right in toolz.sliding_window(
|
|
2, toolz.concatv(self._business_days_index)
|
|
):
|
|
if pd.Timestamp(
|
|
(right.left - left.right).days, unit="D"
|
|
) > pd.Timestamp(1, unit="D"):
|
|
interval = pd.Interval(
|
|
left.right + pd.Timedelta(1, unit="D"),
|
|
right.left - pd.Timedelta(1, unit="D"),
|
|
)
|
|
business_days_all_intervals.append(interval)
|
|
business_days_all_intervals.append(right)
|
|
left = self._business_days_index[-1]
|
|
if left.right < pd.Timestamp.max:
|
|
interval = pd.Interval(
|
|
left.right + pd.Timedelta(1, unit="D"), pd.Timestamp.max
|
|
)
|
|
business_days_all_intervals.append(interval)
|
|
self._business_days_all_index = pd.IntervalIndex(
|
|
business_days_all_intervals, closed="both"
|
|
)
|
|
|
|
def _as_custom_business_day(self):
|
|
return CustomBusinessDay(
|
|
self.n,
|
|
self.normalize,
|
|
self.weekmask,
|
|
self.holidays,
|
|
self.calendar,
|
|
self.offset,
|
|
)
|
|
|
|
def _custom_business_day_for(
|
|
self, other, n=None, is_edge=False, with_interval=False
|
|
):
|
|
loc = self._business_days_all_index.get_loc(other)
|
|
if is_edge and n is not None:
|
|
loc += np.sign(n)
|
|
interval = self._business_days_all_index[loc]
|
|
try:
|
|
loc = self._business_days_index.get_loc(interval.left)
|
|
except KeyError:
|
|
bday = self._as_custom_business_day()
|
|
else:
|
|
bday = self._business_days[loc][-1]
|
|
if n is not None:
|
|
bday = bday.base * n
|
|
if with_interval:
|
|
return bday, interval
|
|
return bday
|
|
|
|
def _moved(self, from_date, to_date, bday):
|
|
return np.busday_count(
|
|
np.datetime64(from_date.date()),
|
|
np.datetime64(to_date.date()),
|
|
busdaycal=bday.calendar,
|
|
)
|
|
|
|
@apply_wraps
|
|
def _apply(self, other):
|
|
if isinstance(other, datetime):
|
|
moved = 0
|
|
remaining = self.n - moved
|
|
bday, interval = self._custom_business_day_for(
|
|
other, remaining, with_interval=True
|
|
)
|
|
result = bday + other
|
|
while not interval.left <= result <= interval.right:
|
|
previous_other = other
|
|
if result < interval.left:
|
|
other = interval.left
|
|
elif result > interval.right:
|
|
other = interval.right
|
|
else:
|
|
raise RuntimeError("Should not reach here")
|
|
moved += self._moved(previous_other, other, bday)
|
|
remaining = self.n - moved
|
|
if remaining == 0:
|
|
break
|
|
bday, interval = self._custom_business_day_for(
|
|
other, remaining, is_edge=True, with_interval=True
|
|
)
|
|
result = bday._apply(other) # noqa: SLF001
|
|
return result
|
|
return super().apply(other)
|
|
|
|
# backwards compat
|
|
apply = _apply
|
|
|
|
def is_on_offset(self, dt):
|
|
if self.normalize and not _is_normalized(dt):
|
|
return False
|
|
day64 = _to_dt64D(dt)
|
|
bday = self._custom_business_day_for(day64)
|
|
return np.is_busday(day64, busdaycal=bday.calendar)
|
|
|
|
|
|
def _is_normalized(dt):
|
|
if dt.hour != 0 or dt.minute != 0 or dt.second != 0 or dt.microsecond != 0:
|
|
# Regardless of whether dt is datetime vs Timestamp
|
|
return False
|
|
if isinstance(dt, pd.Timestamp):
|
|
return dt.nanosecond == 0
|
|
return True
|
|
|
|
|
|
def _to_dt64D(dt): # noqa: N802
|
|
# Currently
|
|
# > np.datetime64(dt.datetime(2013,5,1),dtype='datetime64[D]')
|
|
# numpy.datetime64('2013-05-01T02:00:00.000000+0200')
|
|
# Thus astype is needed to cast datetime to datetime64[D]
|
|
if getattr(dt, "tzinfo", None) is not None:
|
|
# Get the nanosecond timestamp,
|
|
# equiv `Timestamp(dt).value` or `dt.timestamp() * 10**9`
|
|
dt = pd.Timestamp(dt).value
|
|
dt = np.int64(dt).astype("datetime64[ns]")
|
|
else:
|
|
dt = np.datetime64(dt)
|
|
if dt.dtype.name != "datetime64[D]":
|
|
dt = dt.astype("datetime64[D]")
|
|
return dt
|
|
|
|
|
|
def _get_calendar(weekmask, holidays, calendar):
|
|
"""
|
|
Generate busdaycalendar
|
|
"""
|
|
if isinstance(calendar, np.busdaycalendar):
|
|
if not holidays:
|
|
holidays = tuple(calendar.holidays)
|
|
elif not isinstance(holidays, tuple):
|
|
holidays = tuple(holidays)
|
|
else:
|
|
# Trust that calendar.holidays and holidays are
|
|
# consistent
|
|
pass
|
|
# Update weekmask if applicable (added)
|
|
calendar = np.busdaycalendar(weekmask, holidays)
|
|
return calendar, holidays
|
|
|
|
if holidays is None:
|
|
holidays = []
|
|
# Handle non list holidays also (added)
|
|
if isinstance(holidays, pd.DatetimeIndex):
|
|
holidays = holidays.tolist()
|
|
with contextlib.suppress(AttributeError):
|
|
holidays = holidays + calendar.holidays().tolist()
|
|
holidays = [_to_dt64D(dt) for dt in holidays]
|
|
holidays = tuple(sorted(holidays))
|
|
|
|
kwargs = {"weekmask": weekmask}
|
|
if holidays:
|
|
kwargs["holidays"] = holidays
|
|
|
|
busdaycalendar = np.busdaycalendar(**kwargs)
|
|
return busdaycalendar, holidays
|
|
|
|
|
|
class MultipleWeekmaskCustomBusinessDay(CompositeCustomBusinessDay):
|
|
_prefix = "C"
|
|
_attributes = (
|
|
"n",
|
|
"normalize",
|
|
"weekmask",
|
|
"holidays",
|
|
"calendar",
|
|
"offset",
|
|
"business_days",
|
|
"weekmasks",
|
|
)
|
|
|
|
def __init__(
|
|
self,
|
|
n=1,
|
|
normalize=False,
|
|
weekmask="Mon Tue Wed Thu Fri",
|
|
holidays=None,
|
|
calendar=None,
|
|
offset=timedelta(0),
|
|
business_days=None,
|
|
weekmasks=None,
|
|
):
|
|
self._weekmasks = weekmasks
|
|
if business_days is None and weekmasks is not None:
|
|
calendars = [
|
|
_get_calendar(weekmask=weekmask, holidays=holidays, calendar=calendar)
|
|
for _start_date, _end_date, weekmask in weekmasks
|
|
]
|
|
business_days = [
|
|
(
|
|
start_date,
|
|
end_date,
|
|
CustomBusinessDay(
|
|
n, normalize, weekmask, holidays, calendar, offset
|
|
),
|
|
)
|
|
for (start_date, end_date, weekmask), (calendar, holidays) in zip(
|
|
weekmasks, calendars, strict=False
|
|
)
|
|
]
|
|
CompositeCustomBusinessDay.__init__(
|
|
self, n, normalize, weekmask, holidays, calendar, offset, business_days
|
|
)
|
|
|
|
def __setstate__(self, state):
|
|
self.weekmasks = state.pop("weekmasks")
|
|
CompositeCustomBusinessDay.__setstate__(self, state)
|
|
|
|
@property
|
|
def weekmasks(self):
|
|
return tuple(self._weekmasks)
|
|
|
|
@weekmasks.setter
|
|
def weekmasks(self, weekmasks):
|
|
self._weekmasks = weekmasks
|
|
|
|
|
|
class SingleConstructorOffset(BaseOffset):
|
|
def __reduce__(self): ...
|
|
|
|
|
|
class OrthodoxEaster(SingleConstructorOffset):
|
|
"""
|
|
DateOffset for the Orthodox Easter holiday.
|
|
"""
|
|
|
|
def __setstate__(self, state):
|
|
self.n = state.pop("n")
|
|
self.normalize = state.pop("normalize")
|
|
|
|
@apply_wraps
|
|
def _apply(self, other: datetime) -> datetime:
|
|
current_easter = easter(other.year, method=EASTER_ORTHODOX)
|
|
current_easter = datetime(
|
|
current_easter.year, current_easter.month, current_easter.day
|
|
)
|
|
current_easter = localize_pydatetime(current_easter, other.tzinfo)
|
|
|
|
n = self.n
|
|
if n >= 0 and other < current_easter:
|
|
n -= 1
|
|
elif n < 0 and other > current_easter:
|
|
n += 1
|
|
|
|
new = easter(other.year + n, method=EASTER_ORTHODOX)
|
|
return datetime(
|
|
new.year,
|
|
new.month,
|
|
new.day,
|
|
other.hour,
|
|
other.minute,
|
|
other.second,
|
|
other.microsecond,
|
|
)
|
|
|
|
def is_on_offset(self, dt: datetime) -> bool:
|
|
if self.normalize and not _is_normalized(dt):
|
|
return False
|
|
|
|
return date(dt.year, dt.month, dt.day) == easter(
|
|
dt.year, method=EASTER_ORTHODOX
|
|
)
|