Files
MoFin/venv/lib/python3.12/site-packages/exchange_calendars/calendar_utils.py
T
知微 fa45d8aa5f fix: 小果地址统一node122(兼容LAN+EasyTier)
- 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,直连正常
2026-06-30 02:56:35 +08:00

587 lines
21 KiB
Python

from typing import Literal
from .calendar_helpers import parse_date, Date
from .always_open import AlwaysOpenCalendar
from .errors import CalendarNameCollision, CyclicCalendarAlias, InvalidCalendarName
from .exchange_calendar import ExchangeCalendar
from .exchange_calendar_aixk import AIXKExchangeCalendar
from .exchange_calendar_asex import ASEXExchangeCalendar
from .exchange_calendar_bvmf import BVMFExchangeCalendar
from .exchange_calendar_xcys import XCYSExchangeCalendar
from .exchange_calendar_cmes import CMESExchangeCalendar
from .exchange_calendar_iepa import IEPAExchangeCalendar
from .exchange_calendar_xams import XAMSExchangeCalendar
from .exchange_calendar_xasx import XASXExchangeCalendar
from .exchange_calendar_xbel import XBELExchangeCalendar
from .exchange_calendar_xbda import XBDAExchangeCalendar
from .exchange_calendar_xbkk import XBKKExchangeCalendar
from .exchange_calendar_xbog import XBOGExchangeCalendar
from .exchange_calendar_xbom import XBOMExchangeCalendar
from .exchange_calendar_xbru import XBRUExchangeCalendar
from .exchange_calendar_xbse import XBSEExchangeCalendar
from .exchange_calendar_xbud import XBUDExchangeCalendar
from .exchange_calendar_xbue import XBUEExchangeCalendar
from .exchange_calendar_xbra import XBRAExchangeCalendar
from .exchange_calendar_xcbf import XCBFExchangeCalendar
from .exchange_calendar_xcse import XCSEExchangeCalendar
from .exchange_calendar_xdub import XDUBExchangeCalendar
from .exchange_calendar_xdus import XDUSExchangeCalendar
from .exchange_calendar_xeee import XEEEExchangeCalendar
from .exchange_calendar_xetr import XETRExchangeCalendar
from .exchange_calendar_xeur import XEURExchangeCalendar
from .exchange_calendar_xfra import XFRAExchangeCalendar
from .exchange_calendar_xham import XHAMExchangeCalendar
from .exchange_calendar_xhel import XHELExchangeCalendar
from .exchange_calendar_xhkg import XHKGExchangeCalendar
from .exchange_calendar_xice import XICEExchangeCalendar
from .exchange_calendar_xidx import XIDXExchangeCalendar
from .exchange_calendar_xist import XISTExchangeCalendar
from .exchange_calendar_xjse import XJSEExchangeCalendar
from .exchange_calendar_xkar import XKARExchangeCalendar
from .exchange_calendar_xkls import XKLSExchangeCalendar
from .exchange_calendar_xkrx import XKRXExchangeCalendar
from .exchange_calendar_xlim import XLIMExchangeCalendar
from .exchange_calendar_xlis import XLISExchangeCalendar
from .exchange_calendar_xlit import XLITExchangeCalendar
from .exchange_calendar_xlju import XLJUExchangeCalendar
from .exchange_calendar_xlon import XLONExchangeCalendar
from .exchange_calendar_xlux import XLUXExchangeCalendar
from .exchange_calendar_xmad import XMADExchangeCalendar
from .exchange_calendar_xmex import XMEXExchangeCalendar
from .exchange_calendar_xmil import XMILExchangeCalendar
from .exchange_calendar_xmos import XMOSExchangeCalendar
from .exchange_calendar_xnys import XNYSExchangeCalendar
from .exchange_calendar_xnze import XNZEExchangeCalendar
from .exchange_calendar_xosl import XOSLExchangeCalendar
from .exchange_calendar_xpar import XPARExchangeCalendar
from .exchange_calendar_xphs import XPHSExchangeCalendar
from .exchange_calendar_xpra import XPRAExchangeCalendar
from .exchange_calendar_xris import XRISExchangeCalendar
from .exchange_calendar_xsau import XSAUExchangeCalendar
from .exchange_calendar_xses import XSESExchangeCalendar
from .exchange_calendar_xsgo import XSGOExchangeCalendar
from .exchange_calendar_xshg import XSHGExchangeCalendar
from .exchange_calendar_xstu import XSTUExchangeCalendar
from .exchange_calendar_xsto import XSTOExchangeCalendar
from .exchange_calendar_xswx import XSWXExchangeCalendar
from .exchange_calendar_xtae import XTAEExchangeCalendar
from .exchange_calendar_xtai import XTAIExchangeCalendar
from .exchange_calendar_xtal import XTALExchangeCalendar
from .exchange_calendar_xtks import XTKSExchangeCalendar
from .exchange_calendar_xtse import XTSEExchangeCalendar
from .exchange_calendar_xwar import XWARExchangeCalendar
from .exchange_calendar_xwbo import XWBOExchangeCalendar
from .exchange_calendar_xzag import XZAGExchangeCalendar
from .us_futures_calendar import QuantopianUSFuturesCalendar
from .weekday_calendar import WeekdayCalendar
_default_calendar_factories = {
# Exchange calendars.
"AIXK": AIXKExchangeCalendar,
"ASEX": ASEXExchangeCalendar,
"BVMF": BVMFExchangeCalendar,
"XCYS": XCYSExchangeCalendar,
"CMES": CMESExchangeCalendar,
"IEPA": IEPAExchangeCalendar,
"XAMS": XAMSExchangeCalendar,
"XASX": XASXExchangeCalendar,
"XBEL": XBELExchangeCalendar,
"XBDA": XBDAExchangeCalendar,
"XBKK": XBKKExchangeCalendar,
"XBOG": XBOGExchangeCalendar,
"XBOM": XBOMExchangeCalendar,
"XBRU": XBRUExchangeCalendar,
"XBSE": XBSEExchangeCalendar,
"XBUD": XBUDExchangeCalendar,
"XBUE": XBUEExchangeCalendar,
"XBRA": XBRAExchangeCalendar,
"XCBF": XCBFExchangeCalendar,
"XCSE": XCSEExchangeCalendar,
"XDUB": XDUBExchangeCalendar,
"XDUS": XDUSExchangeCalendar,
"XEEE": XEEEExchangeCalendar,
"XFRA": XFRAExchangeCalendar,
"XETR": XETRExchangeCalendar,
"XEUR": XEURExchangeCalendar,
"XHAM": XHAMExchangeCalendar,
"XHEL": XHELExchangeCalendar,
"XHKG": XHKGExchangeCalendar,
"XICE": XICEExchangeCalendar,
"XIDX": XIDXExchangeCalendar,
"XIST": XISTExchangeCalendar,
"XJSE": XJSEExchangeCalendar,
"XKAR": XKARExchangeCalendar,
"XKLS": XKLSExchangeCalendar,
"XKRX": XKRXExchangeCalendar,
"XLIM": XLIMExchangeCalendar,
"XLIS": XLISExchangeCalendar,
"XLIT": XLITExchangeCalendar,
"XLJU": XLJUExchangeCalendar,
"XLON": XLONExchangeCalendar,
"XLUX": XLUXExchangeCalendar,
"XMAD": XMADExchangeCalendar,
"XMEX": XMEXExchangeCalendar,
"XMIL": XMILExchangeCalendar,
"XMOS": XMOSExchangeCalendar,
"XNYS": XNYSExchangeCalendar,
"XNZE": XNZEExchangeCalendar,
"XOSL": XOSLExchangeCalendar,
"XPAR": XPARExchangeCalendar,
"XPHS": XPHSExchangeCalendar,
"XPRA": XPRAExchangeCalendar,
"XRIS": XRISExchangeCalendar,
"XSAU": XSAUExchangeCalendar,
"XSES": XSESExchangeCalendar,
"XSGO": XSGOExchangeCalendar,
"XSHG": XSHGExchangeCalendar,
"XSTO": XSTOExchangeCalendar,
"XSTU": XSTUExchangeCalendar,
"XSWX": XSWXExchangeCalendar,
"XTAE": XTAEExchangeCalendar,
"XTAI": XTAIExchangeCalendar,
"XTAL": XTALExchangeCalendar,
"XTKS": XTKSExchangeCalendar,
"XTSE": XTSEExchangeCalendar,
"XWAR": XWARExchangeCalendar,
"XWBO": XWBOExchangeCalendar,
"XZAG": XZAGExchangeCalendar,
# Miscellaneous calendars.
"us_futures": QuantopianUSFuturesCalendar,
"24/7": AlwaysOpenCalendar,
"24/5": WeekdayCalendar,
}
_default_calendar_aliases = {
"NYSE": "XNYS",
"NASDAQ": "XNYS",
"BATS": "XNYS",
"XNAS": "XNYS",
"ARCX": "XNYS",
"OOTC": "XNYS",
"XASE": "XNYS",
"XTSX": "XTSE",
"FWB": "XFRA",
"LSE": "XLON",
"TSX": "XTSE",
"BMF": "BVMF",
"CME": "CMES",
"CBOT": "CMES",
"COMEX": "CMES",
"NYMEX": "CMES",
"ICE": "IEPA",
"ICEUS": "IEPA",
"NYFE": "IEPA",
"CFE": "XCBF",
"JKT": "XIDX",
"SIX": "XSWX",
"JPX": "XTKS",
"ASX": "XASX",
"HKEX": "XHKG",
"OSE": "XOSL",
"BSE": "XBOM",
"SSE": "XSHG",
"TASE": "XTAE",
"BVB": "XBSE",
"LUXSE": "XLUX",
}
default_calendar_names = sorted(_default_calendar_factories.keys())
class ExchangeCalendarDispatcher:
"""
A class for dispatching and caching exchange calendars.
Methods of a global instance of this class can be accessed directly
from exchange_calendars, for example `exchange_calendars.get_calendar`.
Parameters
----------
calendars : dict[str -> ExchangeCalendar]
Initial set of calendars.
calendar_factories : dict[str -> function]
Factories for lazy calendar creation.
aliases : dict[str -> str]
Calendar name aliases.
"""
def __init__(self, calendars, calendar_factories, aliases):
self._calendars = calendars
self._calendar_factories = dict(calendar_factories)
self._aliases = dict(aliases)
# key: factory name, value: (calendar, dict of calendar kwargs)
self._factory_output_cache: dict[str, tuple[ExchangeCalendar, dict]] = {}
def _fabricate(self, name: str, **kwargs) -> ExchangeCalendar:
"""Fabricate calendar with `name` and `**kwargs`."""
try:
factory = self._calendar_factories[name]
except KeyError as e:
raise InvalidCalendarName(calendar_name=name) from e
calendar = factory(**kwargs)
self._factory_output_cache[name] = (calendar, kwargs)
return calendar
def _get_cached_factory_output(
self, name: str, **kwargs
) -> ExchangeCalendar | None:
"""Get calendar from factory output cache.
Return None if `name` not in cache or `name` in cache although
calendar got with kwargs other than `**kwargs`.
"""
calendar, calendar_kwargs = self._factory_output_cache.get(name, (None, None))
if calendar is not None and calendar_kwargs == kwargs:
return calendar
return None
def get_calendar(
self,
name: str,
start: Date | None = None,
end: Date | None = None,
side: Literal["left", "right", "both", "neither"] | None = None,
) -> ExchangeCalendar:
"""Get exchange calendar with a given name.
Parameters
----------
name
Name of the ExchangeCalendar to get, for example 'XNYS'.
The following arguments will be passed to the calendar factory.
These arguments can only be passed if `name` is registered as a
calendar factory (either by having been included to
`calendar_factories` passed to the dispatcher's constructor or
having been subsequently registered via the
`register_calendar_type` method).
start : default: as default for calendar factory
First calendar session will be `start`, if `start` is a
session, or first session after `start`. Cannot be earlier than
any date returned by the respective calendar class method
`bound_min`.
end : default: as default for calendar factory
Last calendar session will be `end`, if `end` is a session, or
last session before `end`. Cannot be later than any date
returned by the respective calendar class method `bound_max`.
side : default: as default for calendar factory
Define which of session open/close and break start/end should
be treated as a trading minute:
"left" - treat session open and break_start as trading minutes,
do not treat session close or break_end as trading minutes.
"right" - treat session close and break_end as trading minutes,
do not treat session open or break_start as tradng minutes.
"both" - treat all of session open, session close, break_start
and break_end as trading minutes.
"neither" - treat none of session open, session close,
break_start or break_end as trading minutes.
Returns
-------
ExchangeCalendar
Requested calendar.
Raises
------
InvalidCalendarName
If `name` does not represent a registered calendar.
ValueError
If `start`, `end` or `side` are received although `name` is a
registered calendar (as opposed to a calendar factory).
If `start` or `end` are received although do not parse as a
date that could represent a session.
"""
# will raise InvalidCalendarName if name not valid
name = self.resolve_alias(name)
kwargs = {
k: v
for k, v in zip(["start", "end", "side"], [start, end, side], strict=True)
if v is not None
}
if name in self._calendars:
if kwargs:
raise ValueError(
f"Receieved calendar arguments although {name} is registered"
f" as a specific instance of class"
f" {self._calendars[name].__class__}, not as a calendar factory."
)
return self._calendars[name]
if kwargs.get("start"):
kwargs["start"] = parse_date(kwargs["start"], "start", raise_oob=False)
else:
kwargs["start"] = None
if kwargs.get("end"):
kwargs["end"] = parse_date(kwargs["end"], "end", raise_oob=False)
else:
kwargs["end"] = None
cached = self._get_cached_factory_output(name, **kwargs)
return cached if cached is not None else self._fabricate(name, **kwargs)
def get_calendar_names(
self, include_aliases: bool = True, sort: bool = True
) -> list[str]:
"""Return all canoncial calendar names and, optionally, aliases.
Parameters
----------
include_aliases : default: True
True to include calendar aliases.
False to return only canonical calendar names.
sort : default: True
Return calendar names sorted alphabetically.
Returns
-------
list of str
List of canonical calendar names and, optionally, aliases.
See Also
--------
names_to_aliases : Mapping of cononcial names to aliases.
aliases_to_names : Mapping of aliases to canoncial names.
resolve_alias : Resolve single alias to a canonical name.
"""
keys = set(self._calendar_factories.keys()).union(set(self._calendars.keys()))
if include_aliases:
keys = keys.union(set(self._aliases.keys()))
names = list(keys)
if sort:
names.sort()
return names
def has_calendar(self, name):
"""
Do we have (or have the ability to make) a calendar with ``name``?
"""
return (
name in self._calendars
or name in self._calendar_factories
or name in self._aliases
)
def register_calendar(self, name, calendar, force=False):
"""
Registers a calendar for retrieval by the get_calendar method.
Parameters
----------
name: str
The key with which to register this calendar.
calendar: ExchangeCalendar
The calendar to be registered for retrieval.
force : bool, optional
If True, old calendars will be overwritten on a name collision.
If False, name collisions will raise an exception.
Default is False.
Raises
------
CalendarNameCollision
If a calendar is already registered with the given calendar's name.
"""
if force:
self.deregister_calendar(name)
if self.has_calendar(name):
raise CalendarNameCollision(calendar_name=name)
self._calendars[name] = calendar
def register_calendar_type(self, name, calendar_type, force=False):
"""
Registers a calendar by type.
This is useful for registering a new calendar to be lazily instantiated
at some future point in time.
Parameters
----------
name: str
The key with which to register this calendar.
calendar_type: type
The type of the calendar to register.
force : bool, optional
If True, old calendars will be overwritten on a name collision.
If False, name collisions will raise an exception.
Default is False.
Raises
------
CalendarNameCollision
If a calendar is already registered with the given calendar's name.
"""
if force:
self.deregister_calendar(name)
if self.has_calendar(name):
raise CalendarNameCollision(calendar_name=name)
self._calendar_factories[name] = calendar_type
def register_calendar_alias(self, alias, real_name, force=False):
"""
Register an alias for a calendar.
This is useful when multiple exchanges should share a calendar, or when
there are multiple ways to refer to the same exchange.
After calling ``register_alias('alias', 'real_name')``, subsequent
calls to ``get_calendar('alias')`` will return the same result as
``get_calendar('real_name')``.
Parameters
----------
alias : str
The name to be used to refer to a calendar.
real_name : str
The canonical name of the registered calendar.
force : bool, optional
If True, old calendars will be overwritten on a name collision.
If False, name collisions will raise an exception.
Default is False.
"""
if force:
self.deregister_calendar(alias)
if self.has_calendar(alias):
raise CalendarNameCollision(calendar_name=alias)
self._aliases[alias] = real_name
# Ensure that the new alias doesn't create a cycle, and back it out if
# we did.
try:
self.resolve_alias(alias)
except CyclicCalendarAlias:
del self._aliases[alias]
raise
def resolve_alias(self, name: str):
"""Resolve an alias to cononcial name of corresponding calendar.
A cononical name will resolve to itself.
Parameters
----------
name :
Alias or canoncial name corresponding to a calendar.
Returns
-------
canonical_name : str
Canonical name of calendar that would be created for `name`.
Raises
------
InvalidCalendarName
If `name` is not an alias or canonical name of any registered
calendar.
See Also
--------
aliases_to_names : Mapping of aliases to canoncial names.
names_to_aliases : Mapping of cononcial names to aliases.
"""
if name not in self.get_calendar_names(include_aliases=True, sort=False):
raise InvalidCalendarName(calendar_name=name)
seen = []
while name in self._aliases:
seen.append(name)
name = self._aliases[name]
# This is O(N ** 2), but if there's an alias chain longer than 2,
# something strange has happened.
if name in seen:
seen.append(name)
raise CyclicCalendarAlias(cycle=" -> ".join(repr(k) for k in seen))
return name
def aliases_to_names(self) -> dict[str, str]:
"""Return dictionary mapping aliases to canonical names.
Returns
-------
dict of {str, str}
Dictionary mapping aliases to canoncial name of corresponding
calendar.
See Also
--------
resolve_alias : Resolve single alias to a canonical name.
names_to_aliases : Mapping of cononcial names to aliases.
"""
return {alias: self.resolve_alias(alias) for alias in self._aliases}
def names_to_aliases(self) -> dict[str, list[str]]:
"""Return mapping of canonical calendar names to associated aliases.
Returns
-------
dict of {str, list of str}
Dictionary mapping canonical calendar names to any associated
aliases.
See Also
--------
aliases_to_names : Mapping of aliases to canoncial names.
"""
names = self.get_calendar_names(include_aliases=False)
dic = {name: [] for name in names}
for alias, name in self.aliases_to_names().items():
dic[name].append(alias)
return dic
def deregister_calendar(self, name):
"""
If a calendar is registered with the given name, it is de-registered.
Parameters
----------
name : str
The name of the calendar to be deregistered.
"""
self._calendars.pop(name, None)
self._calendar_factories.pop(name, None)
self._aliases.pop(name, None)
def clear_calendars(self):
"""
Deregisters all current registered calendars
"""
self._calendars.clear()
self._calendar_factories.clear()
self._aliases.clear()
# We maintain a global calendar dispatcher so that users can just do
# `register_calendar('my_calendar', calendar) and then use `get_calendar`
# without having to thread around a dispatcher.
global_calendar_dispatcher = ExchangeCalendarDispatcher(
calendars={},
calendar_factories=_default_calendar_factories,
aliases=_default_calendar_aliases,
)
get_calendar = global_calendar_dispatcher.get_calendar
get_calendar_names = global_calendar_dispatcher.get_calendar_names
clear_calendars = global_calendar_dispatcher.clear_calendars
deregister_calendar = global_calendar_dispatcher.deregister_calendar
register_calendar = global_calendar_dispatcher.register_calendar
register_calendar_type = global_calendar_dispatcher.register_calendar_type
register_calendar_alias = global_calendar_dispatcher.register_calendar_alias
resolve_alias = global_calendar_dispatcher.resolve_alias
aliases_to_names = global_calendar_dispatcher.aliases_to_names
names_to_aliases = global_calendar_dispatcher.names_to_aliases