diff --git a/.gitignore b/.gitignore index 52d5b6df9..9e784d263 100644 --- a/.gitignore +++ b/.gitignore @@ -149,3 +149,6 @@ cython_debug/ # Ignore dotenv/direnv files .env* + +# Ignore notebooks +*.ipynb diff --git a/pyproject.toml b/pyproject.toml index 4e45dcd7b..5903f8503 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ dependencies = [ "pydantic>=2.10.2,<3", "annotated-types>=0.7.0,<0.8", "polyfactory>=2.18.1,<3", + "exchange-calendars>=4.8", ] description = "ThetaGang is an IBKR bot for getting money" license = "AGPL-3.0-only" diff --git a/thetagang.toml b/thetagang.toml index f7a8b8f4d..394ea7de6 100644 --- a/thetagang.toml +++ b/thetagang.toml @@ -616,3 +616,41 @@ sell_threshold = 10000 # ["noTakeLiq", "1"], ] strategy = "Vwap" + +[exchange_hours] +# ThetaGang can check whether the market is open before running. This is useful +# to avoid placing orders when the market is closed. We can also (for example) +# avoid running too early (i.e., immediately after market open) to avoid the +# initial volatility, or too late (i.e., immediately before market close) to +# avoid the closing volatility. + +# The exchange to check for market hours. XNYS is the NYSE, but you can use any +# of the ISO exchange codes from +# https://github.com/gerrymanoim/exchange_calendars?tab=readme-ov-file#Calendars. +# +# For example, you'd use "XTKS" for the Tokyo Stock Exchange, "XLON" for the +# London Stock Exchange, or "XHKG" for the Hong Kong Stock Exchange. +exchange = "XNYS" + +# If the market is closed, we can either exit immediately, wait until it's +# time to begin, or continue as normal. Specify "exit" to exit, "wait" to wait, +# and "continue" to continue. +# +# Setting this to "continue" is equivalent to disabling this feature. +action_when_closed = "exit" + +# The maximum amount of time, in seconds, to wait for the market to open if you +# set `exchange_hours.action_when_closed = "wait"`. For example, a value of 3600 +# means ThetaGang will only wait for the market to open if it's within 1 hour of +# the current time. +max_wait_until_open = 3600 + +# The amount of time, in seconds, after the market open, to wait before running +# ThetaGang. For example, if this is set to 1800, ThetaGang will consider the +# market closed until 30 minutes after the open. +delay_after_open = 1800 + +# The amount of time, in seconds, before the market close, to stop running +# ThetaGang. For example, if set to 1800, ThetaGang will consider the market +# closed 30 minutes prior to the actual close. +delay_before_close = 1800 diff --git a/thetagang/config.py b/thetagang/config.py index e86a343e4..16ad109db 100644 --- a/thetagang/config.py +++ b/thetagang/config.py @@ -1,4 +1,5 @@ import math +from enum import Enum from typing import Any, Dict, List, Literal, Optional, Tuple from pydantic import BaseModel, Field, model_validator @@ -469,11 +470,34 @@ class Puts(BaseModel): no_trading: Optional[bool] = None +class ActionWhenClosedEnum(str, Enum): + wait = "wait" + exit = "exit" + continue_ = "continue" + + +class ExchangeHoursConfig(BaseModel, DisplayMixin): + exchange: str = Field(default="XNYS") + action_when_closed: ActionWhenClosedEnum = Field(default=ActionWhenClosedEnum.exit) + delay_after_open: int = Field(default=1800, ge=0) + delay_before_close: int = Field(default=1800, ge=0) + max_wait_until_open: int = Field(default=3600, ge=0) + + def add_to_table(self, table: Table, section: str = "") -> None: + table.add_row("[spring_green1]Exchange hours") + table.add_row("", "Exchange", "=", self.exchange) + table.add_row("", "Action when closed", "=", self.action_when_closed) + table.add_row("", "Delay after open", "=", f"{self.delay_after_open}s") + table.add_row("", "Delay before close", "=", f"{self.delay_before_close}s") + table.add_row("", "Max wait until open", "=", f"{self.max_wait_until_open}s") + + class Config(BaseModel, DisplayMixin): account: AccountConfig option_chains: OptionChainsConfig roll_when: RollWhenConfig target: TargetConfig + exchange_hours: ExchangeHoursConfig = Field(default_factory=ExchangeHoursConfig) orders: OrdersConfig = Field(default_factory=OrdersConfig) ib_async: IBAsyncConfig = Field(default_factory=IBAsyncConfig) @@ -636,6 +660,7 @@ def display(self, config_path: str) -> None: # Add all component tables self.account.add_to_table(config_table) + self.exchange_hours.add_to_table(config_table) if self.constants: self.constants.add_to_table(config_table) self.orders.add_to_table(config_table) diff --git a/thetagang/exchange_hours.py b/thetagang/exchange_hours.py new file mode 100644 index 000000000..4f0042ffd --- /dev/null +++ b/thetagang/exchange_hours.py @@ -0,0 +1,77 @@ +import time +from datetime import datetime, timezone + +import exchange_calendars as xcals +import pandas as pd + +from thetagang import log +from thetagang.config import ExchangeHoursConfig + + +def determine_action(config: ExchangeHoursConfig, now: datetime) -> str: + if config.action_when_closed == "continue": + return "continue" + + calendar = xcals.get_calendar(config.exchange) + today = now.date() + + if calendar.is_session(today): # type: ignore + open = calendar.session_open(today) # type: ignore + close = calendar.session_close(today) # type: ignore + + start = open + pd.Timedelta(seconds=config.delay_after_open) + end = close - pd.Timedelta(seconds=config.delay_before_close) + + log.info(f"Exchange hours open={open}, close={close}, start={start}, end={end}") + + if start <= now <= end: + # Exchange is open + return "continue" + elif config.action_when_closed == "exit": + log.info("Exchange is closed") + return "exit" + elif config.action_when_closed == "wait": + log.info("Exchange is closed") + return "wait" + elif config.action_when_closed == "wait": + return "wait" + + log.info("Exchange is closed") + return "exit" + + +def waited_for_open(config: ExchangeHoursConfig, now: datetime) -> bool: + calendar = xcals.get_calendar(config.exchange) + today = now.date() + + next_session = calendar.date_to_session(today, direction="next") # type: ignore + + open = calendar.session_open(next_session) # type: ignore + start = open + pd.Timedelta(seconds=config.delay_after_open) + + seconds_until_start = (start - now).total_seconds() + + if seconds_until_start < config.max_wait_until_open: + log.info( + f"Waiting for exchange to open, start={start} seconds_until_start={seconds_until_start}" + ) + time.sleep(seconds_until_start) + return True + else: + log.info( + f"Max wait time exceeded, exiting (seconds_until_start={seconds_until_start}, max_wait_until_open={config.max_wait_until_open})" + ) + + return False + + +def need_to_exit(config: ExchangeHoursConfig) -> bool: + now = datetime.now(tz=timezone.utc) + action = determine_action(config, now) + if action == "exit": + return True + if action == "wait": + return not waited_for_open(config, now) + + # action is "continue" + return False diff --git a/thetagang/test_exchange_hours.py b/thetagang/test_exchange_hours.py new file mode 100644 index 000000000..394459751 --- /dev/null +++ b/thetagang/test_exchange_hours.py @@ -0,0 +1,104 @@ +from datetime import datetime, timezone +from unittest.mock import patch + +from thetagang.config import ActionWhenClosedEnum, ExchangeHoursConfig +from thetagang.exchange_hours import determine_action, waited_for_open + + +def test_determine_action_continue_when_closed(): + config = ExchangeHoursConfig( + exchange="XNYS", + delay_after_open=0, + delay_before_close=0, + action_when_closed=ActionWhenClosedEnum.continue_, + ) + now = datetime(2025, 1, 21, 12, 0, tzinfo=timezone.utc) + + result = determine_action(config, now) + assert result == "continue" + + +def test_determine_action_in_open_window(): + config = ExchangeHoursConfig( + exchange="XNYS", + delay_after_open=60, + delay_before_close=60, + action_when_closed=ActionWhenClosedEnum.continue_, + ) + now = datetime(2025, 1, 21, 15, 0, tzinfo=timezone.utc) + + result = determine_action(config, now) + assert result == "continue" + + +def test_determine_action_after_close(): + config = ExchangeHoursConfig( + exchange="XNYS", + delay_after_open=60, + delay_before_close=60, + action_when_closed=ActionWhenClosedEnum.exit, + ) + now = datetime(2025, 1, 21, 21, 0, tzinfo=timezone.utc) + + result = determine_action(config, now) + assert result == "exit" + + +def test_determine_action_session_closed_wait(): + config = ExchangeHoursConfig( + exchange="XNYS", + delay_after_open=60, + delay_before_close=60, + action_when_closed=ActionWhenClosedEnum.wait, + ) + now = datetime(2025, 1, 21, 14, 29, tzinfo=timezone.utc) + + result = determine_action(config, now) + assert result == "wait" + + +@patch("thetagang.exchange_hours.time.sleep") +def test_waited_for_open_under_max(mock_sleep): + config = ExchangeHoursConfig( + exchange="XNYS", + delay_after_open=60, + delay_before_close=60, + action_when_closed=ActionWhenClosedEnum.wait, + max_wait_until_open=600, + ) + now = datetime(2025, 1, 21, 14, 29, tzinfo=timezone.utc) + + assert waited_for_open(config, now) is True + mock_sleep.assert_called_once() + + +@patch("thetagang.exchange_hours.time.sleep") +def test_waited_for_open_exceeds_max(mock_sleep): + config = ExchangeHoursConfig( + exchange="XNYS", + delay_after_open=60, + delay_before_close=60, + action_when_closed=ActionWhenClosedEnum.wait, + max_wait_until_open=30, + ) + now = datetime(2025, 1, 21, 14, 0, tzinfo=timezone.utc) + + assert waited_for_open(config, now) is False + mock_sleep.assert_not_called() + + +@patch("thetagang.exchange_hours.time.sleep") +def test_waited_for_open_negative_difference(mock_sleep): + config = ExchangeHoursConfig( + exchange="XNYS", + delay_after_open=60, + delay_before_close=60, + action_when_closed=ActionWhenClosedEnum.wait, + max_wait_until_open=300, + ) + # 'now' is already after the start time + now = datetime(2025, 1, 21, 15, 0, tzinfo=timezone.utc) + + # seconds_until_start will be negative, but code checks if it's < max_wait_until_open + assert waited_for_open(config, now) is True + mock_sleep.assert_called_once() diff --git a/thetagang/thetagang.py b/thetagang/thetagang.py index bd477fc20..45989bd19 100755 --- a/thetagang/thetagang.py +++ b/thetagang/thetagang.py @@ -6,6 +6,7 @@ from thetagang import log from thetagang.config import Config, normalize_config +from thetagang.exchange_hours import need_to_exit from thetagang.portfolio_manager import PortfolioManager util.patchAsyncio() @@ -24,6 +25,10 @@ def start(config_path: str, without_ibc: bool = False, dry_run: bool = False) -> if config.ib_async.logfile: util.logToFile(config.ib_async.logfile) + # Check if exchange is open before continuing + if need_to_exit(config.exchange_hours): + return + async def onConnected() -> None: log.info(f"Connected to IB Gateway, serverVersion={ib.client.serverVersion()}") await portfolio_manager.manage() diff --git a/uv.lock b/uv.lock index d3d899088..ccf097484 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,10 @@ version = 1 requires-python = ">=3.10, <3.13" +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version < '3.11'", +] [[package]] name = "annotated-types" @@ -88,6 +93,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, ] +[[package]] +name = "exchange-calendars" +version = "4.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "korean-lunar-calendar" }, + { name = "numpy" }, + { name = "pandas" }, + { name = "pyluach" }, + { name = "toolz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/78/1802680e9ea575712411a8aa4bd2979c0cea510a007816fe88dbdc72199b/exchange_calendars-4.8.tar.gz", hash = "sha256:70d1f2dce712fb193aa56f67df17a292221a4c24589a0077fac3f41aaf5e0a29", size = 3854982 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/f4/c4762d034eecf246d1f836dcb9d51e126c5f008833ebbb987905fa03bac6/exchange_calendars-4.8-py3-none-any.whl", hash = "sha256:98990e1419e6379c22c19e8dff31ab0abf59a0841c24d21953f731b50b4125f9", size = 197757 }, +] + [[package]] name = "faker" version = "33.3.1" @@ -141,6 +163,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, ] +[[package]] +name = "korean-lunar-calendar" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/93/a0bd2bd53ab19330e83ecc5652b7774ae86fd2fee19bc05ad220cf9db08b/korean_lunar_calendar-0.3.1.tar.gz", hash = "sha256:eb2c485124a061016926bdea6d89efdf9b9fdbf16db55895b6cf1e5bec17b857", size = 9877 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/96/30f3fe51b336bb6da4714f4fdad7bbdce8f13af79af2eb75e22908f3f9f4/korean_lunar_calendar-0.3.1-py3-none-any.whl", hash = "sha256:392757135c492c4f42a604e6038042953c35c6f449dda5f27e3f86a7f9c943e5", size = 9033 }, +] + [[package]] name = "markdown-it-py" version = "3.0.0" @@ -240,6 +271,41 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, ] +[[package]] +name = "pandas" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/70/c853aec59839bceed032d52010ff5f1b8d87dc3114b762e4ba2727661a3b/pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5", size = 12580827 }, + { url = "https://files.pythonhosted.org/packages/99/f2/c4527768739ffa4469b2b4fff05aa3768a478aed89a2f271a79a40eee984/pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348", size = 11303897 }, + { url = "https://files.pythonhosted.org/packages/ed/12/86c1747ea27989d7a4064f806ce2bae2c6d575b950be087837bdfcabacc9/pandas-2.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d9c45366def9a3dd85a6454c0e7908f2b3b8e9c138f5dc38fed7ce720d8453ed", size = 66480908 }, + { url = "https://files.pythonhosted.org/packages/44/50/7db2cd5e6373ae796f0ddad3675268c8d59fb6076e66f0c339d61cea886b/pandas-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86976a1c5b25ae3f8ccae3a5306e443569ee3c3faf444dfd0f41cda24667ad57", size = 13064210 }, + { url = "https://files.pythonhosted.org/packages/61/61/a89015a6d5536cb0d6c3ba02cebed51a95538cf83472975275e28ebf7d0c/pandas-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8661b0238a69d7aafe156b7fa86c44b881387509653fdf857bebc5e4008ad42", size = 16754292 }, + { url = "https://files.pythonhosted.org/packages/ce/0d/4cc7b69ce37fac07645a94e1d4b0880b15999494372c1523508511b09e40/pandas-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37e0aced3e8f539eccf2e099f65cdb9c8aa85109b0be6e93e2baff94264bdc6f", size = 14416379 }, + { url = "https://files.pythonhosted.org/packages/31/9e/6ebb433de864a6cd45716af52a4d7a8c3c9aaf3a98368e61db9e69e69a9c/pandas-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:56534ce0746a58afaf7942ba4863e0ef81c9c50d3f0ae93e9497d6a41a057645", size = 11598471 }, + { url = "https://files.pythonhosted.org/packages/a8/44/d9502bf0ed197ba9bf1103c9867d5904ddcaf869e52329787fc54ed70cc8/pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039", size = 12602222 }, + { url = "https://files.pythonhosted.org/packages/52/11/9eac327a38834f162b8250aab32a6781339c69afe7574368fffe46387edf/pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd", size = 11321274 }, + { url = "https://files.pythonhosted.org/packages/45/fb/c4beeb084718598ba19aa9f5abbc8aed8b42f90930da861fcb1acdb54c3a/pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698", size = 15579836 }, + { url = "https://files.pythonhosted.org/packages/cd/5f/4dba1d39bb9c38d574a9a22548c540177f78ea47b32f99c0ff2ec499fac5/pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc", size = 13058505 }, + { url = "https://files.pythonhosted.org/packages/b9/57/708135b90391995361636634df1f1130d03ba456e95bcf576fada459115a/pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3", size = 16744420 }, + { url = "https://files.pythonhosted.org/packages/86/4a/03ed6b7ee323cf30404265c284cee9c65c56a212e0a08d9ee06984ba2240/pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32", size = 14440457 }, + { url = "https://files.pythonhosted.org/packages/ed/8c/87ddf1fcb55d11f9f847e3c69bb1c6f8e46e2f40ab1a2d2abadb2401b007/pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5", size = 11617166 }, + { url = "https://files.pythonhosted.org/packages/17/a3/fb2734118db0af37ea7433f57f722c0a56687e14b14690edff0cdb4b7e58/pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9", size = 12529893 }, + { url = "https://files.pythonhosted.org/packages/e1/0c/ad295fd74bfac85358fd579e271cded3ac969de81f62dd0142c426b9da91/pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4", size = 11363475 }, + { url = "https://files.pythonhosted.org/packages/c6/2a/4bba3f03f7d07207481fed47f5b35f556c7441acddc368ec43d6643c5777/pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3", size = 15188645 }, + { url = "https://files.pythonhosted.org/packages/38/f8/d8fddee9ed0d0c0f4a2132c1dfcf0e3e53265055da8df952a53e7eaf178c/pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319", size = 12739445 }, + { url = "https://files.pythonhosted.org/packages/20/e8/45a05d9c39d2cea61ab175dbe6a2de1d05b679e8de2011da4ee190d7e748/pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8", size = 16359235 }, + { url = "https://files.pythonhosted.org/packages/1d/99/617d07a6a5e429ff90c90da64d428516605a1ec7d7bea494235e1c3882de/pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a", size = 14056756 }, + { url = "https://files.pythonhosted.org/packages/29/d4/1244ab8edf173a10fd601f7e13b9566c1b525c4f365d6bee918e68381889/pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13", size = 11504248 }, +] + [[package]] name = "platformdirs" version = "4.3.6" @@ -371,6 +437,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, ] +[[package]] +name = "pyluach" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/fc/4567068be2c78ac09339201f4a1adfd9e95f6f873f6f37f6fafb5648363e/pyluach-2.2.0.tar.gz", hash = "sha256:9063a25387cd7624276fd0656508bada08aa8a6f22e8db352844cd858e69012b", size = 26198 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/83/2e585d06d49e0320050b3d7d8ae0dfbd1459e976ff9f4b4d8bcca983d474/pyluach-2.2.0-py3-none-any.whl", hash = "sha256:d1eb49d6292087e9290f4661ae01b60c8c933704ec8c9cef82673b349ff96adf", size = 25037 }, +] + [[package]] name = "pytest" version = "8.3.4" @@ -421,6 +496,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1b/b4/afd75551a3b910abd1d922dbd45e49e5deeb4d47dc50209ce489ba9844dd/pytimeparse-1.1.8-py2.py3-none-any.whl", hash = "sha256:04b7be6cc8bd9f5647a6325444926c3ac34ee6bc7e69da4367ba282f076036bd", size = 9969 }, ] +[[package]] +name = "pytz" +version = "2024.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/31/3c70bf7603cc2dca0f19bdc53b4537a797747a58875b552c8c413d963a3f/pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a", size = 319692 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/c3/005fcca25ce078d2cc29fd559379817424e94885510568bc1bc53d7d5846/pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725", size = 508002 }, +] + [[package]] name = "pyyaml" version = "6.0.2" @@ -521,6 +605,7 @@ dependencies = [ { name = "annotated-types" }, { name = "click" }, { name = "click-log" }, + { name = "exchange-calendars" }, { name = "ib-async" }, { name = "more-itertools" }, { name = "numpy" }, @@ -546,6 +631,7 @@ requires-dist = [ { name = "annotated-types", specifier = ">=0.7.0,<0.8" }, { name = "click", specifier = ">=8.1.3,<9" }, { name = "click-log", specifier = ">=0.4.0,<0.5" }, + { name = "exchange-calendars", specifier = ">=4.8" }, { name = "ib-async", specifier = ">=1.0.3,<2" }, { name = "more-itertools", specifier = ">=9.1,<11.0" }, { name = "numpy", specifier = ">=1.26,<3.0" }, @@ -604,6 +690,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, ] +[[package]] +name = "toolz" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/0b/d80dfa675bf592f636d1ea0b835eab4ec8df6e9415d8cfd766df54456123/toolz-1.0.0.tar.gz", hash = "sha256:2c86e3d9a04798ac556793bced838816296a2f085017664e4995cb40a1047a02", size = 66790 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/98/eb27cc78ad3af8e302c9d8ff4977f5026676e130d28dd7578132a457170c/toolz-1.0.0-py3-none-any.whl", hash = "sha256:292c8f1c4e7516bf9086f8850935c799a874039c8bcf959d47b600e4c44a6236", size = 56383 }, +] + [[package]] name = "typing-extensions" version = "4.12.2" @@ -613,6 +708,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, ] +[[package]] +name = "tzdata" +version = "2024.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/34/943888654477a574a86a98e9896bae89c7aa15078ec29f490fef2f1e5384/tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc", size = 193282 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/ab/7e5f53c3b9d14972843a647d8d7a853969a58aecc7559cb3267302c94774/tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd", size = 346586 }, +] + [[package]] name = "virtualenv" version = "20.28.1"