py-ib-controller 0.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ibcontroller/__init__.py +0 -0
- ibcontroller/actions.py +273 -0
- ibcontroller/agent_client.py +466 -0
- ibcontroller/app_dirs.py +44 -0
- ibcontroller/cli.py +211 -0
- ibcontroller/config.py +410 -0
- ibcontroller/control_loop.py +636 -0
- ibcontroller/data/builtin_settings.toml +37 -0
- ibcontroller/data/ibcontroller.toml.example +178 -0
- ibcontroller/data/ibkr_settings.toml.example +135 -0
- ibcontroller/data/labels.json +81 -0
- ibcontroller/data/labels.json.example +44 -0
- ibcontroller/diagnostics.py +119 -0
- ibcontroller/dispatch.py +288 -0
- ibcontroller/ibcontroller-agent.jar +0 -0
- ibcontroller/labels.py +242 -0
- ibcontroller/launcher.py +909 -0
- ibcontroller/logging_setup.py +277 -0
- ibcontroller/login.py +578 -0
- ibcontroller/main.py +156 -0
- ibcontroller/py.typed +0 -0
- ibcontroller/recognisers.py +448 -0
- ibcontroller/schedule.py +157 -0
- ibcontroller/settings.py +554 -0
- py_ib_controller-0.0.0.dist-info/METADATA +490 -0
- py_ib_controller-0.0.0.dist-info/RECORD +30 -0
- py_ib_controller-0.0.0.dist-info/WHEEL +5 -0
- py_ib_controller-0.0.0.dist-info/entry_points.txt +2 -0
- py_ib_controller-0.0.0.dist-info/licenses/LICENSE.txt +176 -0
- py_ib_controller-0.0.0.dist-info/top_level.txt +1 -0
ibcontroller/schedule.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""schedule.py -- pure wall-clock scheduling math for the self-scheduled
|
|
2
|
+
shutdown actions (`Config.cold_restart_time`/`closedown_at`), consumed by
|
|
3
|
+
`control_loop.py`. No I/O, no asyncio -- same split discipline as
|
|
4
|
+
`launcher.build_launch_plan` vs `launch_instance`: testable against a
|
|
5
|
+
synthetic `datetime`, no live Gateway/TWS needed.
|
|
6
|
+
|
|
7
|
+
Both settings come from IBC's own `config.ini` (`ColdRestartTime`/
|
|
8
|
+
`ClosedownAt`) but, unlike `auto_restart_time`/`auto_logoff_time`, are not GUI
|
|
9
|
+
settings at all -- IBC implements them itself as a self-scheduled tidy
|
|
10
|
+
close-down (`IbcTws.java`'s own `startShutdownTimerIfRequired`/
|
|
11
|
+
`getColdRestartTime`/`getShutdownTime`), which this module ports the
|
|
12
|
+
scheduling math for. Applies to TWS and Gateway alike: `IbcTws.java` is IBC's
|
|
13
|
+
shared base class for both programs, and `IbcGateway.java`'s near-empty (38
|
|
14
|
+
lines: `setupDefaultEnvironment` + `IbcTws.load()`) body is exactly *why*
|
|
15
|
+
Gateway inherits the whole shutdown timer, not proof it's TWS-only.
|
|
16
|
+
`control_loop.py` owns actually acting on the result: ending the READY wait
|
|
17
|
+
so the existing `clean_shutdown` call performs the real menu-based close
|
|
18
|
+
(`File > Exit` on TWS, `File > Close` on Gateway), and -- for a cold restart
|
|
19
|
+
-- forcing a fresh relaunch via `launch_instance` with no restart hash (this
|
|
20
|
+
is ibcontroller's own supervisor-driven relaunch, not IBC's native-launcher
|
|
21
|
+
`File > Restart`/`COLDRESTART`-flag mechanism)."""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import logging
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from datetime import datetime, time, timedelta
|
|
28
|
+
from enum import Enum, auto
|
|
29
|
+
|
|
30
|
+
from ibcontroller.config import Config
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
_WEEKDAY_NAMES = [
|
|
35
|
+
"Monday",
|
|
36
|
+
"Tuesday",
|
|
37
|
+
"Wednesday",
|
|
38
|
+
"Thursday",
|
|
39
|
+
"Friday",
|
|
40
|
+
"Saturday",
|
|
41
|
+
"Sunday",
|
|
42
|
+
]
|
|
43
|
+
# Index matches datetime.weekday() (Monday=0..Sunday=6) directly.
|
|
44
|
+
_WEEKDAY_INDEX = {name: i for i, name in enumerate(_WEEKDAY_NAMES)}
|
|
45
|
+
_COLD_RESTART_WEEKDAY = _WEEKDAY_INDEX["Sunday"]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ScheduleError(Exception):
|
|
49
|
+
"""A malformed `cold_restart_time`/`closedown_at` value."""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ScheduledAction(Enum):
|
|
53
|
+
COLD_RESTART = auto()
|
|
54
|
+
TIDY_CLOSEDOWN = auto()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class ScheduledShutdown:
|
|
59
|
+
at: datetime
|
|
60
|
+
action: ScheduledAction
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _parse_hhmm(value: str, *, setting_name: str) -> time:
|
|
64
|
+
"""Strict 24-hour `"HH:MM"` -- mirrors IBC's own
|
|
65
|
+
`SimpleDateFormat("HH:mm")` strictness (unlike `auto_restart_time`/
|
|
66
|
+
`auto_logoff_time`'s 12-hour `"hh:mm AM/PM"`, these two settings are
|
|
67
|
+
always 24-hour in IBC's own `config.ini`)."""
|
|
68
|
+
try:
|
|
69
|
+
parsed = datetime.strptime(value.strip(), "%H:%M")
|
|
70
|
+
except ValueError:
|
|
71
|
+
raise ScheduleError(
|
|
72
|
+
f'{setting_name} setting must be 24-hour "HH:MM", got {value!r}'
|
|
73
|
+
) from None
|
|
74
|
+
return parsed.time()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def parse_closedown_at(value: str) -> tuple[int | None, time]:
|
|
78
|
+
"""Parses `"HH:MM"` (every day) or `"<Weekday> HH:MM"` (one day a week --
|
|
79
|
+
`Weekday` a full English name, `Monday`..`Sunday`, matched case-
|
|
80
|
+
insensitively). Deliberately NOT locale-dependent (`%A`/`strptime`) --
|
|
81
|
+
IBC's own `config.ini` comment flags exactly this class of bug for
|
|
82
|
+
non-Latin-1 locales (Unicode-escaping a day name); a fixed English table
|
|
83
|
+
sidesteps it entirely rather than porting the same fragility. Returns
|
|
84
|
+
`(weekday index matching datetime.weekday(), time)`, weekday `None` for
|
|
85
|
+
"every day"."""
|
|
86
|
+
parts = value.strip().rsplit(" ", 1)
|
|
87
|
+
if len(parts) == 2: # noqa: PLR2004
|
|
88
|
+
day_name = parts[0].strip().title()
|
|
89
|
+
if day_name not in _WEEKDAY_INDEX:
|
|
90
|
+
raise ScheduleError(
|
|
91
|
+
'closedown_at setting must be "HH:MM" or "<Weekday> HH:MM" '
|
|
92
|
+
f"(Weekday one of {', '.join(_WEEKDAY_NAMES)}), got {value!r}"
|
|
93
|
+
)
|
|
94
|
+
return _WEEKDAY_INDEX[day_name], _parse_hhmm(
|
|
95
|
+
parts[1], setting_name="closedown_at"
|
|
96
|
+
)
|
|
97
|
+
return None, _parse_hhmm(value, setting_name="closedown_at")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _next_occurrence(target: time, weekday: int | None, now: datetime) -> datetime:
|
|
101
|
+
"""Next wall-clock instant strictly after `now` matching `target`/
|
|
102
|
+
`weekday` (`weekday=None` -- every day). Wraps to the next matching day
|
|
103
|
+
if today's slot has already passed (or is exactly `now`)."""
|
|
104
|
+
candidate = now.replace(
|
|
105
|
+
hour=target.hour, minute=target.minute, second=0, microsecond=0
|
|
106
|
+
)
|
|
107
|
+
if weekday is None:
|
|
108
|
+
if candidate <= now:
|
|
109
|
+
candidate += timedelta(days=1)
|
|
110
|
+
return candidate
|
|
111
|
+
candidate += timedelta(days=(weekday - now.weekday()) % 7)
|
|
112
|
+
if candidate <= now:
|
|
113
|
+
candidate += timedelta(days=7)
|
|
114
|
+
return candidate
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def next_scheduled_shutdown(config: Config, now: datetime) -> ScheduledShutdown | None:
|
|
118
|
+
"""Earliest of `Config.cold_restart_time` (every Sunday) and
|
|
119
|
+
`Config.closedown_at` (daily or weekly) -- applies to TWS and Gateway
|
|
120
|
+
alike. A malformed value is logged and that one field is skipped
|
|
121
|
+
(matching `settings.py`'s own per-entry error isolation) rather than
|
|
122
|
+
aborting the other field or raising out of this function. Returns `None`
|
|
123
|
+
if nothing applies."""
|
|
124
|
+
candidates: list[ScheduledShutdown] = []
|
|
125
|
+
if config.cold_restart_time:
|
|
126
|
+
try:
|
|
127
|
+
target = _parse_hhmm(
|
|
128
|
+
config.cold_restart_time, setting_name="cold_restart_time"
|
|
129
|
+
)
|
|
130
|
+
except ScheduleError:
|
|
131
|
+
logger.exception("IBController > invalid cold_restart_time -- ignoring")
|
|
132
|
+
else:
|
|
133
|
+
candidates.append(
|
|
134
|
+
ScheduledShutdown(
|
|
135
|
+
at=_next_occurrence(target, _COLD_RESTART_WEEKDAY, now),
|
|
136
|
+
action=ScheduledAction.COLD_RESTART,
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
if config.closedown_at:
|
|
140
|
+
try:
|
|
141
|
+
weekday, target = parse_closedown_at(config.closedown_at)
|
|
142
|
+
except ScheduleError:
|
|
143
|
+
logger.exception("IBController > invalid closedown_at -- ignoring")
|
|
144
|
+
else:
|
|
145
|
+
candidates.append(
|
|
146
|
+
ScheduledShutdown(
|
|
147
|
+
at=_next_occurrence(target, weekday, now),
|
|
148
|
+
action=ScheduledAction.TIDY_CLOSEDOWN,
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
if not candidates:
|
|
152
|
+
return None
|
|
153
|
+
# Ties break toward cold restart, matching IBC's own
|
|
154
|
+
# `coldRestartTime.before(shutdownTime)` check in
|
|
155
|
+
# startShutdownTimerIfRequired.
|
|
156
|
+
candidates.sort(key=lambda c: (c.at, c.action is not ScheduledAction.COLD_RESTART))
|
|
157
|
+
return candidates[0]
|