py-ib-controller 0.1.2__tar.gz → 0.1.4__tar.gz
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.
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/.github/workflows/ci.yml +1 -1
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/.github/workflows/publish.yml +4 -3
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/PKG-INFO +51 -5
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/README.md +50 -4
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/agent/src/main/java/ibcontroller/agent/AgentMain.java +16 -1
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/docs/Configuration.md +21 -0
- py_ib_controller-0.1.4/docs/development.md +252 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller/cli.py +78 -9
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller/config.py +89 -9
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller/control_loop.py +120 -29
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller/data/ibcontroller.toml.example +21 -5
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller/data/ibkr_settings.toml.example +3 -23
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller/data/labels.json +14 -1
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller/ibcontroller-agent.jar +0 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller/labels.py +44 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller/launcher.py +139 -33
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller/logging_setup.py +76 -9
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller/login.py +31 -7
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller/recognisers.py +155 -16
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller.env +14 -4
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/py_ib_controller.egg-info/PKG-INFO +51 -5
- py_ib_controller-0.1.4/py_ib_controller.egg-info/scm_version.json +8 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/tests/test_cli.py +116 -2
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/tests/test_config.py +141 -3
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/tests/test_control_loop.py +280 -8
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/tests/test_labels.py +9 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/tests/test_launcher.py +175 -3
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/tests/test_logging_setup.py +43 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/tests/test_login.py +87 -24
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/tests/test_recognisers.py +155 -4
- py_ib_controller-0.1.2/docs/development.md +0 -132
- py_ib_controller-0.1.2/py_ib_controller.egg-info/scm_version.json +0 -8
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/.github/ISSUE_TEMPLATE.md +0 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/.github/dependabot.yml +0 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/.gitignore +0 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/.pre-commit-config.yaml +0 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/.sdkmanrc +0 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/CONTRIBUTING.md +0 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/LICENSE.txt +0 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/Makefile +0 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/agent/src/main/java/ibcontroller/agent/ComponentLookup.java +0 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/agent/src/main/java/ibcontroller/agent/EventBridge.java +0 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/agent/src/main/java/ibcontroller/agent/Protocol.java +0 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/agent/src/main/java/ibcontroller/agent/Security.java +0 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/agent/src/main/java/ibcontroller/agent/WriteOps.java +0 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/docs/Architecture.svg +0 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller/__init__.py +0 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller/actions.py +0 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller/agent_client.py +0 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller/app_dirs.py +0 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller/data/builtin_settings.toml +0 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller/data/labels.json.example +0 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller/diagnostics.py +0 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller/dispatch.py +0 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller/main.py +0 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller/py.typed +0 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller/schedule.py +0 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/ibcontroller/settings.py +0 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/py_ib_controller.egg-info/SOURCES.txt +0 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/py_ib_controller.egg-info/dependency_links.txt +0 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/py_ib_controller.egg-info/entry_points.txt +0 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/py_ib_controller.egg-info/requires.txt +0 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/py_ib_controller.egg-info/scm_file_list.json +0 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/py_ib_controller.egg-info/top_level.txt +0 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/pyproject.toml +0 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/setup.cfg +0 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/tests/__init__.py +0 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/tests/conftest.py +0 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/tests/fakes.py +0 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/tests/test_actions.py +0 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/tests/test_agent_client.py +0 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/tests/test_app_dirs.py +0 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/tests/test_diagnostics.py +0 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/tests/test_dispatch.py +0 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/tests/test_main.py +0 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/tests/test_schedule.py +0 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/tests/test_settings.py +0 -0
- {py_ib_controller-0.1.2 → py_ib_controller-0.1.4}/tests/test_trace_integration.py +0 -0
|
@@ -26,7 +26,7 @@ jobs:
|
|
|
26
26
|
fetch-depth: 0
|
|
27
27
|
|
|
28
28
|
- name: Install uv and Python
|
|
29
|
-
uses: astral-sh/setup-uv@v10.
|
|
29
|
+
uses: astral-sh/setup-uv@v10.2.0
|
|
30
30
|
with:
|
|
31
31
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
32
32
|
|
|
@@ -95,21 +95,22 @@ jobs:
|
|
|
95
95
|
./dist/*.tar.gz ./dist/*.whl
|
|
96
96
|
|
|
97
97
|
- name: Generate changelog
|
|
98
|
+
id: git-cliff
|
|
98
99
|
uses: orhun/git-cliff-action@v4
|
|
99
100
|
with:
|
|
100
101
|
# no cliff.toml -- git-cliff's built-in default config already
|
|
101
102
|
# groups Conventional Commits (feat/fix/docs/...) correctly
|
|
102
103
|
args: --latest --tag ${{ github.ref_name }} --strip header
|
|
103
|
-
output: CHANGELOG_release.md
|
|
104
104
|
|
|
105
105
|
- name: Assemble release notes
|
|
106
106
|
env:
|
|
107
107
|
TAG: ${{ github.ref_name }}
|
|
108
108
|
REPO: ${{ github.repository }}
|
|
109
|
+
CHANGELOG: ${{ steps.git-cliff.outputs.content }}
|
|
109
110
|
run: |
|
|
110
111
|
PREV_TAG=$(git describe --tags --abbrev=0 "$TAG^" 2>/dev/null || true)
|
|
111
112
|
{
|
|
112
|
-
|
|
113
|
+
echo "$CHANGELOG"
|
|
113
114
|
echo
|
|
114
115
|
echo "## Installation"
|
|
115
116
|
echo
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: py-ib-controller
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Login/launch controller for TWS and IBKR Gateway, via an in-process Java agent
|
|
5
5
|
Author-email: gnzsnz <8376642+gnzsnz@users.noreply.github.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -52,7 +52,9 @@ Give it a spin, `ib-controller run --dotenv=.env-paper --trading-mode=paper --pr
|
|
|
52
52
|
|
|
53
53
|
https://github.com/user-attachments/assets/b97f7180-29d9-4e09-b32f-8be838dff0e2
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
You can check the full list of [cli parameters](#cli-parameters) or [config keys](#configuration).
|
|
56
|
+
|
|
57
|
+
To it build from source:
|
|
56
58
|
|
|
57
59
|
```bash
|
|
58
60
|
# 0. Fetch the code
|
|
@@ -70,9 +72,11 @@ uv run ibcontroller init
|
|
|
70
72
|
uv run ibcontroller run
|
|
71
73
|
```
|
|
72
74
|
|
|
75
|
+
For a detailed build guide please check the [development guide](./docs/development.md).
|
|
76
|
+
|
|
73
77
|
`ibcontroller run` scaffolds the config directory automatically the first time
|
|
74
78
|
`ibcontroller.toml` is missing (same effect as running `init` first), so it can also be
|
|
75
|
-
run directly on a fresh install
|
|
79
|
+
run directly on a fresh install. It will still fail with a clear error naming whichever
|
|
76
80
|
setting is actually missing (in practice, just the two credential env vars).
|
|
77
81
|
|
|
78
82
|
### CLI parameters
|
|
@@ -179,6 +183,11 @@ Each also accepts a `_FILE`-suffixed variant (`IBC_USERID_FILE`,
|
|
|
179
183
|
`IBC_PASSWORD_FILE`) that reads the value from a file instead —
|
|
180
184
|
Docker/Compose secrets, so a value never has to sit in the process environment.
|
|
181
185
|
|
|
186
|
+
The startup `IBC_*` environment dump masks any variable whose name contains
|
|
187
|
+
`PASSWORD`, `USERID`, `SECRET`, `TOKEN`, `TOTP` or `KEY` (e.g.
|
|
188
|
+
`IBC_PASSWORD_PAPER`, set by other tooling), except `_FILE` variants, which
|
|
189
|
+
only hold a path.
|
|
190
|
+
|
|
182
191
|
### File location
|
|
183
192
|
|
|
184
193
|
When environment variable `IBC_APP_DIR` is not set, then
|
|
@@ -224,6 +233,8 @@ allowing `None`. `Config`'s schema is closed — an unknown key in
|
|
|
224
233
|
| `log_dir` | `IBC_LOG_DIR` | Where ibcontroller's own log file lives; always resolved at startup | platform default |
|
|
225
234
|
| `log_level` | `IBC_LOG_LEVEL` | `debug`/`info`/`warning`/`error` | `info` |
|
|
226
235
|
| `log_sink` | `IBC_LOG_SINK` | `"std"` (console only) or `"file"` (only, under `log_dir`) — exclusive, not both. Covers `ibcontroller-{instance}.log` and `gateway-{instance}.log` only; the Java agent log and the wire trace are always file, unaffected — see "Logging" below | `"std"` |
|
|
236
|
+
| `log_max_bytes` | `IBC_LOG_MAX_BYTES` | Rotate log/trace files at this size in bytes; `0` disables rotation. Only respected when `log_sink="file"` | `10485760` (10 MiB) |
|
|
237
|
+
| `log_backup_count` | `IBC_LOG_BACKUP_COUNT` | Number of rotated backups to keep. Only respected when `log_sink="file"` | `5` |
|
|
227
238
|
| `diagnostic_scope` | `IBC_DIAGNOSTIC_SCOPE` | `"known"`/`"unknown"`/`"all"` — which windows get a structure dump logged (see `docs/Configuration.md`'s "Diagnostics") | `"known"` |
|
|
228
239
|
| `diagnostic_when` | `IBC_DIAGNOSTIC_WHEN` | `"open"`/`"openclose"`/`"never"` — when to log a structure dump | `"never"` |
|
|
229
240
|
| (env only) | `IBC_APP_DIR`, `--app-dir` | Override file locations (config/log/run) for container mode; `--app-dir`/`init`/`run` set the env var for the invocation, same effect |
|
|
@@ -241,8 +252,10 @@ TWS/ibgateway settings
|
|
|
241
252
|
| `mfa_timeout` | `IBC_MFA_TIMEOUT` | IB's 2FA timeout in seconds | `180.0` |
|
|
242
253
|
| `relogin_after_mfa_timeout` | `IBC_RELOGIN_AFTER_MFA_TIMEOUT` | Restart login if 2FA times out | `false` |
|
|
243
254
|
| `mfa_exit_interval` | `IBC_MFA_EXIT_INTERVAL` | Bounds the post-2FA wait when relogin enabled | `60.0` |
|
|
255
|
+
| `mfa_timeout_action` | `IBC_MFA_TIMEOUT_ACTION` | `exit` (exit 75, see "Exit codes" above) or `restart` (relaunch with a fresh login) when the 2FA push goes unanswered until `mfa_timeout` or `mfa_exit_interval` fires | `"exit"` |
|
|
244
256
|
| `auto_restart_time` | `IBC_AUTO_RESTART_TIME` | `"hh:mm AM/PM"` daily auto-restart time. Applied to Gateway/TWS automatically -- see "Declarative configuration" below | `None` |
|
|
245
257
|
| `auto_logoff_time` | `IBC_AUTO_LOGOFF_TIME` | `"hh:mm AM/PM"` daily auto-logoff time; same "Lock and Exit" radio-button pair as `auto_restart_time` -- if both are set, `auto_restart_time` wins | `None` |
|
|
258
|
+
| `time_zone` | `IBC_TIME_ZONE` | IANA zone name, e.g. `"Europe/Zurich"`, written to `jts.ini`'s `TimeZone`. Auto-detected from `TZ`, then `/etc/localtime` -- no setup needed in the common case; never overwrites an existing `TimeZone` line | auto-detected |
|
|
246
259
|
| `cold_restart_time` | `IBC_COLD_RESTART_TIME` | TWS and Gateway. `"HH:MM"` 24-hour local time; every Sunday, ibcontroller closes the instance tidily and relaunches with a full fresh login, forcing IBKR's weekly Sunday 01:00 US/Eastern token-invalidation reauth -- not a GUI setting, see "Scheduled shutdown" below | `None` |
|
|
247
260
|
| `closedown_at` | `IBC_CLOSEDOWN_AT` | TWS and Gateway. `"HH:MM"` (daily) or `"<Weekday> HH:MM"` (weekly); closes the instance tidily at that time, no relaunch -- not a GUI setting, see "Scheduled shutdown" below | `None` |
|
|
248
261
|
|
|
@@ -288,7 +301,7 @@ US/Eastern invalidation window if that's the intent.
|
|
|
288
301
|
|
|
289
302
|
## Declarative configuration
|
|
290
303
|
|
|
291
|
-
Beyond `ibcontroller.toml`,
|
|
304
|
+
Beyond `ibcontroller.toml`, two things let you shape ibcontroller's behavior without
|
|
292
305
|
writing any Python: **`labels.json`** (window/button text, plus simple pop-up dismissal
|
|
293
306
|
rules) and **`ibkr_settings.toml`** (Global Configuration settings applied to Gateway/TWS
|
|
294
307
|
at startup). Each is covered below.
|
|
@@ -409,10 +422,17 @@ scrollback.
|
|
|
409
422
|
| --- | --- | --- | --- |
|
|
410
423
|
| `ibcontroller-{instance}.log` | ibcontroller app control flow | `log_sink=file` (else console) | `log_level` (default `info`) |
|
|
411
424
|
| `gateway-{instance}.log` | TWS/Gateway own stdout/stderr | `log_sink=file` (else console) | TWS/Gateway's own |
|
|
412
|
-
| `ibcontroller-java-agent-{instance}.log` | Java agent (in-process JUL) | always file, not affected by `log_sink` | `log_level` (see mapping below) |
|
|
425
|
+
| `ibcontroller-java-agent-{instance}.log` (see rotation note below) | Java agent (in-process JUL) | always file, not affected by `log_sink` | `log_level` (see mapping below) |
|
|
413
426
|
| `cmd-{instance}.jsonl` | raw wire: commands sent + results, NDJSON | `trace_enabled`; always file, not affected by `log_sink` | always (DEBUG emit, gated separately) |
|
|
414
427
|
| `events-{instance}.jsonl` | raw wire: every event message received, NDJSON | `trace_enabled`; always file, not affected by `log_sink` | always (DEBUG emit, gated separately) |
|
|
415
428
|
|
|
429
|
+
**Rotation** (`IBC_LOG_MAX_BYTES`/`IBC_LOG_BACKUP_COUNT`, default 10 MiB x 5
|
|
430
|
+
backups, **on by default**) applies to every file above in file mode. Set
|
|
431
|
+
`IBC_LOG_MAX_BYTES=0` to disable it (unbounded files). Python's files keep
|
|
432
|
+
their usual name active, with `.1`/`.2`/... as old backups. The Java agent log
|
|
433
|
+
is the opposite: once rotation is on, the active file is
|
|
434
|
+
`ibcontroller-java-agent-{instance}.log.0`, not the unsuffixed name.
|
|
435
|
+
|
|
416
436
|
### Log levels
|
|
417
437
|
|
|
418
438
|
`IBC_LOG_LEVEL` (`debug`/`info`/`warning`/`error`, default `info`) controls
|
|
@@ -484,6 +504,32 @@ or both.
|
|
|
484
504
|
instances (e.g. `paper` and `live`) can run in parallel and each tail its own files
|
|
485
505
|
without interleaving, even if copied or globbed out of their directory.
|
|
486
506
|
|
|
507
|
+
## Exit codes
|
|
508
|
+
|
|
509
|
+
`ibcontroller run` exits with a [BSD sysexits](https://man.freebsd.org/cgi/man.cgi?query=sysexits)
|
|
510
|
+
code, grouped by what a supervisor (a container's `restart:` policy, systemd's
|
|
511
|
+
`Restart=`) should do about it. The specific cause is always in the logs and in the
|
|
512
|
+
`stopped: <CAUSE>` line.
|
|
513
|
+
|
|
514
|
+
| Code | Meaning | Supervisor action |
|
|
515
|
+
| --- | --- | --- |
|
|
516
|
+
| `0` | Intentional stop (Ctrl-C, `closedown_at`, Gateway/TWS closed normally, e.g. File>Close) | Do not restart |
|
|
517
|
+
| `1` | Unhandled exception (a bug) | Restart, report |
|
|
518
|
+
| `2` | CLI usage error | Fix the command |
|
|
519
|
+
| `78` (`EX_CONFIG`) | Deployment error (bad config, install not found) | Do not retry |
|
|
520
|
+
| `77` (`EX_NOPERM`) | Credentials rejected | Do not retry (lockout risk) |
|
|
521
|
+
| `75` (`EX_TEMPFAIL`) | 2FA not approved in time, `mfa_timeout_action=exit` | Retry per policy |
|
|
522
|
+
| `69` (`EX_UNAVAILABLE`) | Transient runtime failure (process crashed or killed, connection lost, JVM exited before becoming ready, login timed out with no 2FA dialog) | Retry |
|
|
523
|
+
|
|
524
|
+
`77`/`78` work with systemd's `RestartPreventExitStatus=`.
|
|
525
|
+
|
|
526
|
+
Some login-blocking dialogs IB reports as a server-side condition -- not a credential
|
|
527
|
+
rejection -- never reach this table at all: a "Login failed"/"Login Error" dialog, or a
|
|
528
|
+
Gateway "Connection to server failed" reason other than a known credential/account one
|
|
529
|
+
(e.g. a stale restart token), make ibcontroller relaunch in-process with a full fresh
|
|
530
|
+
login, unbounded, the same way a scheduled `cold_restart_time` does -- the container
|
|
531
|
+
itself never exits or restarts for these.
|
|
532
|
+
|
|
487
533
|
## License
|
|
488
534
|
|
|
489
535
|
```text
|
|
@@ -30,7 +30,9 @@ Give it a spin, `ib-controller run --dotenv=.env-paper --trading-mode=paper --pr
|
|
|
30
30
|
|
|
31
31
|
https://github.com/user-attachments/assets/b97f7180-29d9-4e09-b32f-8be838dff0e2
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
You can check the full list of [cli parameters](#cli-parameters) or [config keys](#configuration).
|
|
34
|
+
|
|
35
|
+
To it build from source:
|
|
34
36
|
|
|
35
37
|
```bash
|
|
36
38
|
# 0. Fetch the code
|
|
@@ -48,9 +50,11 @@ uv run ibcontroller init
|
|
|
48
50
|
uv run ibcontroller run
|
|
49
51
|
```
|
|
50
52
|
|
|
53
|
+
For a detailed build guide please check the [development guide](./docs/development.md).
|
|
54
|
+
|
|
51
55
|
`ibcontroller run` scaffolds the config directory automatically the first time
|
|
52
56
|
`ibcontroller.toml` is missing (same effect as running `init` first), so it can also be
|
|
53
|
-
run directly on a fresh install
|
|
57
|
+
run directly on a fresh install. It will still fail with a clear error naming whichever
|
|
54
58
|
setting is actually missing (in practice, just the two credential env vars).
|
|
55
59
|
|
|
56
60
|
### CLI parameters
|
|
@@ -157,6 +161,11 @@ Each also accepts a `_FILE`-suffixed variant (`IBC_USERID_FILE`,
|
|
|
157
161
|
`IBC_PASSWORD_FILE`) that reads the value from a file instead —
|
|
158
162
|
Docker/Compose secrets, so a value never has to sit in the process environment.
|
|
159
163
|
|
|
164
|
+
The startup `IBC_*` environment dump masks any variable whose name contains
|
|
165
|
+
`PASSWORD`, `USERID`, `SECRET`, `TOKEN`, `TOTP` or `KEY` (e.g.
|
|
166
|
+
`IBC_PASSWORD_PAPER`, set by other tooling), except `_FILE` variants, which
|
|
167
|
+
only hold a path.
|
|
168
|
+
|
|
160
169
|
### File location
|
|
161
170
|
|
|
162
171
|
When environment variable `IBC_APP_DIR` is not set, then
|
|
@@ -202,6 +211,8 @@ allowing `None`. `Config`'s schema is closed — an unknown key in
|
|
|
202
211
|
| `log_dir` | `IBC_LOG_DIR` | Where ibcontroller's own log file lives; always resolved at startup | platform default |
|
|
203
212
|
| `log_level` | `IBC_LOG_LEVEL` | `debug`/`info`/`warning`/`error` | `info` |
|
|
204
213
|
| `log_sink` | `IBC_LOG_SINK` | `"std"` (console only) or `"file"` (only, under `log_dir`) — exclusive, not both. Covers `ibcontroller-{instance}.log` and `gateway-{instance}.log` only; the Java agent log and the wire trace are always file, unaffected — see "Logging" below | `"std"` |
|
|
214
|
+
| `log_max_bytes` | `IBC_LOG_MAX_BYTES` | Rotate log/trace files at this size in bytes; `0` disables rotation. Only respected when `log_sink="file"` | `10485760` (10 MiB) |
|
|
215
|
+
| `log_backup_count` | `IBC_LOG_BACKUP_COUNT` | Number of rotated backups to keep. Only respected when `log_sink="file"` | `5` |
|
|
205
216
|
| `diagnostic_scope` | `IBC_DIAGNOSTIC_SCOPE` | `"known"`/`"unknown"`/`"all"` — which windows get a structure dump logged (see `docs/Configuration.md`'s "Diagnostics") | `"known"` |
|
|
206
217
|
| `diagnostic_when` | `IBC_DIAGNOSTIC_WHEN` | `"open"`/`"openclose"`/`"never"` — when to log a structure dump | `"never"` |
|
|
207
218
|
| (env only) | `IBC_APP_DIR`, `--app-dir` | Override file locations (config/log/run) for container mode; `--app-dir`/`init`/`run` set the env var for the invocation, same effect |
|
|
@@ -219,8 +230,10 @@ TWS/ibgateway settings
|
|
|
219
230
|
| `mfa_timeout` | `IBC_MFA_TIMEOUT` | IB's 2FA timeout in seconds | `180.0` |
|
|
220
231
|
| `relogin_after_mfa_timeout` | `IBC_RELOGIN_AFTER_MFA_TIMEOUT` | Restart login if 2FA times out | `false` |
|
|
221
232
|
| `mfa_exit_interval` | `IBC_MFA_EXIT_INTERVAL` | Bounds the post-2FA wait when relogin enabled | `60.0` |
|
|
233
|
+
| `mfa_timeout_action` | `IBC_MFA_TIMEOUT_ACTION` | `exit` (exit 75, see "Exit codes" above) or `restart` (relaunch with a fresh login) when the 2FA push goes unanswered until `mfa_timeout` or `mfa_exit_interval` fires | `"exit"` |
|
|
222
234
|
| `auto_restart_time` | `IBC_AUTO_RESTART_TIME` | `"hh:mm AM/PM"` daily auto-restart time. Applied to Gateway/TWS automatically -- see "Declarative configuration" below | `None` |
|
|
223
235
|
| `auto_logoff_time` | `IBC_AUTO_LOGOFF_TIME` | `"hh:mm AM/PM"` daily auto-logoff time; same "Lock and Exit" radio-button pair as `auto_restart_time` -- if both are set, `auto_restart_time` wins | `None` |
|
|
236
|
+
| `time_zone` | `IBC_TIME_ZONE` | IANA zone name, e.g. `"Europe/Zurich"`, written to `jts.ini`'s `TimeZone`. Auto-detected from `TZ`, then `/etc/localtime` -- no setup needed in the common case; never overwrites an existing `TimeZone` line | auto-detected |
|
|
224
237
|
| `cold_restart_time` | `IBC_COLD_RESTART_TIME` | TWS and Gateway. `"HH:MM"` 24-hour local time; every Sunday, ibcontroller closes the instance tidily and relaunches with a full fresh login, forcing IBKR's weekly Sunday 01:00 US/Eastern token-invalidation reauth -- not a GUI setting, see "Scheduled shutdown" below | `None` |
|
|
225
238
|
| `closedown_at` | `IBC_CLOSEDOWN_AT` | TWS and Gateway. `"HH:MM"` (daily) or `"<Weekday> HH:MM"` (weekly); closes the instance tidily at that time, no relaunch -- not a GUI setting, see "Scheduled shutdown" below | `None` |
|
|
226
239
|
|
|
@@ -266,7 +279,7 @@ US/Eastern invalidation window if that's the intent.
|
|
|
266
279
|
|
|
267
280
|
## Declarative configuration
|
|
268
281
|
|
|
269
|
-
Beyond `ibcontroller.toml`,
|
|
282
|
+
Beyond `ibcontroller.toml`, two things let you shape ibcontroller's behavior without
|
|
270
283
|
writing any Python: **`labels.json`** (window/button text, plus simple pop-up dismissal
|
|
271
284
|
rules) and **`ibkr_settings.toml`** (Global Configuration settings applied to Gateway/TWS
|
|
272
285
|
at startup). Each is covered below.
|
|
@@ -387,10 +400,17 @@ scrollback.
|
|
|
387
400
|
| --- | --- | --- | --- |
|
|
388
401
|
| `ibcontroller-{instance}.log` | ibcontroller app control flow | `log_sink=file` (else console) | `log_level` (default `info`) |
|
|
389
402
|
| `gateway-{instance}.log` | TWS/Gateway own stdout/stderr | `log_sink=file` (else console) | TWS/Gateway's own |
|
|
390
|
-
| `ibcontroller-java-agent-{instance}.log` | Java agent (in-process JUL) | always file, not affected by `log_sink` | `log_level` (see mapping below) |
|
|
403
|
+
| `ibcontroller-java-agent-{instance}.log` (see rotation note below) | Java agent (in-process JUL) | always file, not affected by `log_sink` | `log_level` (see mapping below) |
|
|
391
404
|
| `cmd-{instance}.jsonl` | raw wire: commands sent + results, NDJSON | `trace_enabled`; always file, not affected by `log_sink` | always (DEBUG emit, gated separately) |
|
|
392
405
|
| `events-{instance}.jsonl` | raw wire: every event message received, NDJSON | `trace_enabled`; always file, not affected by `log_sink` | always (DEBUG emit, gated separately) |
|
|
393
406
|
|
|
407
|
+
**Rotation** (`IBC_LOG_MAX_BYTES`/`IBC_LOG_BACKUP_COUNT`, default 10 MiB x 5
|
|
408
|
+
backups, **on by default**) applies to every file above in file mode. Set
|
|
409
|
+
`IBC_LOG_MAX_BYTES=0` to disable it (unbounded files). Python's files keep
|
|
410
|
+
their usual name active, with `.1`/`.2`/... as old backups. The Java agent log
|
|
411
|
+
is the opposite: once rotation is on, the active file is
|
|
412
|
+
`ibcontroller-java-agent-{instance}.log.0`, not the unsuffixed name.
|
|
413
|
+
|
|
394
414
|
### Log levels
|
|
395
415
|
|
|
396
416
|
`IBC_LOG_LEVEL` (`debug`/`info`/`warning`/`error`, default `info`) controls
|
|
@@ -462,6 +482,32 @@ or both.
|
|
|
462
482
|
instances (e.g. `paper` and `live`) can run in parallel and each tail its own files
|
|
463
483
|
without interleaving, even if copied or globbed out of their directory.
|
|
464
484
|
|
|
485
|
+
## Exit codes
|
|
486
|
+
|
|
487
|
+
`ibcontroller run` exits with a [BSD sysexits](https://man.freebsd.org/cgi/man.cgi?query=sysexits)
|
|
488
|
+
code, grouped by what a supervisor (a container's `restart:` policy, systemd's
|
|
489
|
+
`Restart=`) should do about it. The specific cause is always in the logs and in the
|
|
490
|
+
`stopped: <CAUSE>` line.
|
|
491
|
+
|
|
492
|
+
| Code | Meaning | Supervisor action |
|
|
493
|
+
| --- | --- | --- |
|
|
494
|
+
| `0` | Intentional stop (Ctrl-C, `closedown_at`, Gateway/TWS closed normally, e.g. File>Close) | Do not restart |
|
|
495
|
+
| `1` | Unhandled exception (a bug) | Restart, report |
|
|
496
|
+
| `2` | CLI usage error | Fix the command |
|
|
497
|
+
| `78` (`EX_CONFIG`) | Deployment error (bad config, install not found) | Do not retry |
|
|
498
|
+
| `77` (`EX_NOPERM`) | Credentials rejected | Do not retry (lockout risk) |
|
|
499
|
+
| `75` (`EX_TEMPFAIL`) | 2FA not approved in time, `mfa_timeout_action=exit` | Retry per policy |
|
|
500
|
+
| `69` (`EX_UNAVAILABLE`) | Transient runtime failure (process crashed or killed, connection lost, JVM exited before becoming ready, login timed out with no 2FA dialog) | Retry |
|
|
501
|
+
|
|
502
|
+
`77`/`78` work with systemd's `RestartPreventExitStatus=`.
|
|
503
|
+
|
|
504
|
+
Some login-blocking dialogs IB reports as a server-side condition -- not a credential
|
|
505
|
+
rejection -- never reach this table at all: a "Login failed"/"Login Error" dialog, or a
|
|
506
|
+
Gateway "Connection to server failed" reason other than a known credential/account one
|
|
507
|
+
(e.g. a stale restart token), make ibcontroller relaunch in-process with a full fresh
|
|
508
|
+
login, unbounded, the same way a scheduled `cold_restart_time` does -- the container
|
|
509
|
+
itself never exits or restarts for these.
|
|
510
|
+
|
|
465
511
|
## License
|
|
466
512
|
|
|
467
513
|
```text
|
|
@@ -133,6 +133,14 @@ public final class AgentMain {
|
|
|
133
133
|
* Python and read here -- the agent never invents paths or levels itself. When
|
|
134
134
|
* {@code ibcontroller.logfile} is absent (e.g. {@code make run}), logs go to
|
|
135
135
|
* stderr only, same as before this mechanism existed.
|
|
136
|
+
*
|
|
137
|
+
* <p>Rotation (gitea #32): {@code -Dibcontroller.logfile.maxbytes=<N>} and
|
|
138
|
+
* {@code -Dibcontroller.logfile.backupcount=<N>} mirror Python's own
|
|
139
|
+
* {@code log_max_bytes}/{@code log_backup_count}. {@code FileHandler}'s own
|
|
140
|
+
* fd can't be rotated externally (the JVM keeps writing to a renamed/unlinked
|
|
141
|
+
* inode), so this uses its 4-arg constructor, which does native generation
|
|
142
|
+
* rotation on the JVM's own thread. {@code maxbytes=0} (the default if the
|
|
143
|
+
* property is absent) keeps the old unbounded, append-only file.
|
|
136
144
|
*/
|
|
137
145
|
private static void configureLogging() throws Exception {
|
|
138
146
|
Logger root = Logger.getLogger("");
|
|
@@ -145,7 +153,14 @@ public final class AgentMain {
|
|
|
145
153
|
|
|
146
154
|
String logFile = System.getProperty("ibcontroller.logfile");
|
|
147
155
|
if (logFile != null && !logFile.isEmpty()) {
|
|
148
|
-
|
|
156
|
+
int maxBytes = Integer.parseInt(
|
|
157
|
+
System.getProperty("ibcontroller.logfile.maxbytes", "0"));
|
|
158
|
+
int backupCount = Integer.parseInt(
|
|
159
|
+
System.getProperty("ibcontroller.logfile.backupcount", "0"));
|
|
160
|
+
FileHandler fileHandler =
|
|
161
|
+
maxBytes > 0
|
|
162
|
+
? new FileHandler(logFile, maxBytes, Math.max(backupCount, 1), true)
|
|
163
|
+
: new FileHandler(logFile, true);
|
|
149
164
|
fileHandler.setFormatter(new SimpleFormatter());
|
|
150
165
|
root.addHandler(fileHandler);
|
|
151
166
|
} else {
|
|
@@ -24,6 +24,10 @@ accepts a `_FILE`-suffixed variant that reads the value from a file instead
|
|
|
24
24
|
(Docker/Compose secrets): `IBC_USERID` / `IBC_PASSWORD`,
|
|
25
25
|
`IBC_USERID_FILE` / `IBC_PASSWORD_FILE`.
|
|
26
26
|
|
|
27
|
+
The startup `IBC_*` environment dump masks any variable whose name contains
|
|
28
|
+
`PASSWORD`, `USERID`, `SECRET`, `TOKEN`, `TOTP` or `KEY` (e.g.
|
|
29
|
+
`IBC_PASSWORD_PAPER`), except `_FILE` variants, which only hold a path.
|
|
30
|
+
|
|
27
31
|
## IBController specific settings
|
|
28
32
|
|
|
29
33
|
Settings that drive the behaviour of `ibcontroller` itself.
|
|
@@ -60,6 +64,7 @@ Settings related to TWS/ibgateway.
|
|
|
60
64
|
| `existing_session_action` | `IBC_EXISTING_SESSION_ACTION` | What to do when an existing session is detected -- see below | `"manual"` |
|
|
61
65
|
| `auto_restart_time` | `IBC_AUTO_RESTART_TIME` | Daily auto-restart time, `"hh:mm AM/PM"` -- see below | `None` (unchanged) |
|
|
62
66
|
| `auto_logoff_time` | `IBC_AUTO_LOGOFF_TIME` | Daily auto-logoff time, `"hh:mm AM/PM"` -- see below | `None` (unchanged) |
|
|
67
|
+
| `time_zone` | `IBC_TIME_ZONE` | IANA zone name, e.g. `"Europe/Zurich"`, written to `jts.ini`'s `TimeZone` -- see below | auto-detected |
|
|
63
68
|
|
|
64
69
|
### Accept Incoming Connection
|
|
65
70
|
|
|
@@ -129,6 +134,21 @@ Time above, just selecting "Auto logoff" instead of "Auto restart". Omit this
|
|
|
129
134
|
key (the default) to leave the existing setting unchanged. If both keys are
|
|
130
135
|
set, Auto Restart Time wins -- set only one of the two in practice.
|
|
131
136
|
|
|
137
|
+
### Time Zone
|
|
138
|
+
|
|
139
|
+
**key:** `time_zone`
|
|
140
|
+
**Environment variable:** `IBC_TIME_ZONE`
|
|
141
|
+
|
|
142
|
+
IANA zone name, e.g. `"Europe/Zurich"`, written to `jts.ini`'s `TimeZone` key
|
|
143
|
+
(under `[Logon]`) before every launch. No setup needed in the common case:
|
|
144
|
+
auto-detected from the container's own `TZ` environment variable first, then
|
|
145
|
+
from `/etc/localtime`'s zoneinfo symlink target; set `IBC_TIME_ZONE`
|
|
146
|
+
explicitly only when the desired TWS/Gateway session time zone should differ
|
|
147
|
+
from the host/container's own. Never overwrites a `TimeZone` line already
|
|
148
|
+
present in `jts.ini` -- a value found there was set deliberately, either by
|
|
149
|
+
you or by an earlier run of ibcontroller itself. Leaves the key untouched if
|
|
150
|
+
nothing resolves.
|
|
151
|
+
|
|
132
152
|
### Cold Restart Time
|
|
133
153
|
|
|
134
154
|
**key:** `cold_restart_time`
|
|
@@ -169,6 +189,7 @@ TWS/Gateway itself.
|
|
|
169
189
|
| `mfa_timeout` | `IBC_MFA_TIMEOUT` | The real 2FA budget in seconds, mirroring IBKR's own external limit | `180.0` |
|
|
170
190
|
| `relogin_after_mfa_timeout` | `IBC_RELOGIN_AFTER_MFA_TIMEOUT` | Restart the login attempt if 2FA times out (instead of giving up) | `false` |
|
|
171
191
|
| `mfa_exit_interval` | `IBC_MFA_EXIT_INTERVAL` | Bounds the wait after a timed-out 2FA when relogin is enabled | `60.0` |
|
|
192
|
+
| `mfa_timeout_action` | `IBC_MFA_TIMEOUT_ACTION` | What to do when the 2FA push goes unanswered until `mfa_timeout` or `mfa_exit_interval` expires: `exit` (exit 75) or `restart` (relaunch with a fresh login) | `exit` |
|
|
172
193
|
|
|
173
194
|
## Diagnostics
|
|
174
195
|
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# Development environment
|
|
2
|
+
|
|
3
|
+
How to get `ibcontroller` itself, and the Java agent it drives, running locally.
|
|
4
|
+
|
|
5
|
+
## Target platform
|
|
6
|
+
|
|
7
|
+
The supported platforms are macOS and Linux.
|
|
8
|
+
|
|
9
|
+
## Python toolchain
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh # if uv isn't already installed
|
|
13
|
+
cd ibcontroller
|
|
14
|
+
uv sync # installs the dev dependency group too
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Requires Python 3.12+ (pinned in `.python-version`). Day-to-day commands:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
uv run pytest # test suite (pytest-asyncio, pytest-aiohttp already in the dev group)
|
|
21
|
+
uv run ruff check . # lint
|
|
22
|
+
uv run ruff format . # format
|
|
23
|
+
uv run pyrefly check # type check
|
|
24
|
+
uv run bandit -c pyproject.toml -r ibcontroller/ # security lint
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## pre-commit
|
|
28
|
+
|
|
29
|
+
`.pre-commit-config.yaml` (project root) runs ruff (lint + format), bandit, pyrefly, and basic
|
|
30
|
+
hygiene hooks (trailing whitespace, end-of-file, yaml/toml checks, large-file check) on every
|
|
31
|
+
commit — the same checks as the `uv run` commands above, wired to run automatically instead of by
|
|
32
|
+
hand.
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
uv run pre-commit install # one-time per clone: installs the git hook
|
|
36
|
+
uv run pre-commit run --all-files # run every hook against the whole tree, not just staged files
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
CI runs the same config, so a clean local `pre-commit run --all-files` is a reliable predictor of
|
|
40
|
+
CI passing.
|
|
41
|
+
|
|
42
|
+
## Java toolchain
|
|
43
|
+
|
|
44
|
+
The agent needs to build against, and run compatibly with, the JRE TWS/Gateway bundles.
|
|
45
|
+
|
|
46
|
+
**Local dev JDK, via SDKMAN:** a real Azul account/API token is needed for the exact SA-tier build
|
|
47
|
+
above; without one, the closest public match is Zulu's CA (Certified Availability) tier at the
|
|
48
|
+
same JDK version. SDKMAN manages that install and pins the version per project:
|
|
49
|
+
|
|
50
|
+
One-time install (adds SDKMAN's init to your shell rc automatically):
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
curl -s "https://get.sdkman.io" | bash
|
|
54
|
+
# install
|
|
55
|
+
sdk install java 17.0.15-zulu
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The pinned Java version lives in `.sdkmanrc` at the project root (checked in):
|
|
59
|
+
|
|
60
|
+
```env
|
|
61
|
+
java=17.0.15-zulu
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Activate it per clone — SDKMAN then sets `JAVA_HOME` for the current shell:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
sdk env use # reads .sdkmanrc; installs the pinned JDK on first use
|
|
68
|
+
java -version
|
|
69
|
+
openjdk version "17.0.15" 2025-04-15 LTS
|
|
70
|
+
OpenJDK Runtime Environment Zulu17.58+21-CA (build 17.0.15+6-LTS)
|
|
71
|
+
OpenJDK 64-Bit Server VM Zulu17.58+21-CA (build 17.0.15+6-LTS, mixed mode, sharing)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Enable auto-env (`sdkman_auto_env=true` in `~/.sdkman/etc/config`) to have `JAVA_HOME` set
|
|
75
|
+
automatically when entering the project directory, instead of running `sdk env use` in every new
|
|
76
|
+
terminal. Either way, `JAVA_HOME` is the contract: the `Makefile` reads it (`JDK ?= $(JAVA_HOME)`),
|
|
77
|
+
and CI's `actions/setup-java` (zulu 17) sets the same variable, so local and CI build with the
|
|
78
|
+
same pin.
|
|
79
|
+
|
|
80
|
+
This JDK is for *compiling* only — its own version doesn't need to match whatever Gateway/TWS
|
|
81
|
+
happens to bundle locally . The `Makefile`'s `--release 17` tells `javac` to target Java 17
|
|
82
|
+
bytecode/API regardless of which JDK compiles it, so the output runs correctly on whatever
|
|
83
|
+
JRE TWS/gateway is using.
|
|
84
|
+
|
|
85
|
+
The agent is small, so keep the build as plain
|
|
86
|
+
as it can be: `javac` + `jar`, wired through a small `Makefile`.
|
|
87
|
+
|
|
88
|
+
## TWS / IBKR Gateway itself
|
|
89
|
+
|
|
90
|
+
Download from IBKR directly [gateway](https://www.interactivebrokers.com/en/trading/ibgateway-latest.php) or [TWS](https://www.interactivebrokers.com/en/trading/download-tws.php). Then install.
|
|
91
|
+
|
|
92
|
+
After the first run, `ibcontroller` renames the gateway/TWS script. This is required to manage restarts.
|
|
93
|
+
|
|
94
|
+
## Building and attaching the agent
|
|
95
|
+
|
|
96
|
+
Java agent file structure:
|
|
97
|
+
|
|
98
|
+
```text
|
|
99
|
+
agent/src/main/java/ibcontroller/agent/
|
|
100
|
+
AgentMain.java — entry point: both sockets, EventBridge installed before Gateway launches
|
|
101
|
+
Protocol.java — command accept loop, hand-rolled JSON codec, full command dispatch
|
|
102
|
+
ComponentLookup.java — read-only Swing tree walk, dump/get_text
|
|
103
|
+
WriteOps.java — EDT-dispatched writes, set_text/set_checkbox/click
|
|
104
|
+
EventBridge.java — AWTEventListener install + NDJSON event forwarding
|
|
105
|
+
Security.java — password redaction, the credential-field boundary
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Build using`Makefile`:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
make build # build/ibcontroller-agent.jar, then copies it into ibcontroller/ (for uv/pyproject packaging)
|
|
112
|
+
make dist # build the jar, then uv build the Python package (jar is required package-data, see Makefile)
|
|
113
|
+
make clean # rm -rf build dist
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Day-to-day cycle: `make build` after any agent-side change, `uv run pytest` for the Python side,
|
|
117
|
+
`pre-commit run --all-files` before pushing.
|
|
118
|
+
|
|
119
|
+
## Running ibcontroller
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
# from the git repo
|
|
123
|
+
source .venv/bin/activate
|
|
124
|
+
ibcontroller run --dotenv=.env-paper --trading-mode=paper --program=gateway
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Architecture
|
|
128
|
+
|
|
129
|
+
`ibcontroller` follows a layered architecture with separation of concerns between layers.
|
|
130
|
+
Any contribution must follow this separation of concerns.
|
|
131
|
+
|
|
132
|
+

|
|
133
|
+
|
|
134
|
+
### Rules for every layer
|
|
135
|
+
|
|
136
|
+
- A layer talks only to the layer directly below it. The exceptions are listed per layer.
|
|
137
|
+
- L3's schema types (`WindowEvent`, `WindowInfo`, `Component`, …) and exceptions are the shared
|
|
138
|
+
vocabulary of every layer above L3. Changing them is a cross-layer change.
|
|
139
|
+
- Everything runs on one asyncio loop. No blocking call on the loop: file and process work goes
|
|
140
|
+
through `anyio.to_thread` or the logging listener threads.
|
|
141
|
+
- One object graph per running instance (`Dispatcher`, `LoginManager`, recogniser registry, sockets,
|
|
142
|
+
settings directory). No module-level state shared between instances.
|
|
143
|
+
- Every user-facing string (window titles, button and menu labels) comes from `labels.json` through
|
|
144
|
+
`labels.py`. Never a Python literal.
|
|
145
|
+
- Credentials stay `Secret` until L3 builds the request line. Nothing logs or traces a credential
|
|
146
|
+
value.
|
|
147
|
+
|
|
148
|
+
### L1 Process (`launcher.py`)
|
|
149
|
+
|
|
150
|
+
- **Owns:** TWS/Gateway install discovery (macOS and Linux, channel filtering), JRE selection, the
|
|
151
|
+
JVM command line, the per-instance settings directory and socket paths, native-restart
|
|
152
|
+
prevention, spawning the JVM with the agent, draining its stdout, and readiness (`ping`).
|
|
153
|
+
- **Provides:** `LaunchedInstance`: the process, a started `Dispatcher` (L4), socket paths,
|
|
154
|
+
settings directory.
|
|
155
|
+
- **Rules:** one launch per call. Relaunch policy belongs to L6. L1 may construct L3 and L4 objects
|
|
156
|
+
("spawn and attach").
|
|
157
|
+
- **Known deviation:** `clean_shutdown` lives here and uses L5-1. It belongs to Management (L5).
|
|
158
|
+
|
|
159
|
+
### L2 Java agent (`agent/`)
|
|
160
|
+
|
|
161
|
+
- **Owns:** everything inside the TWS/Gateway JVM: starting the entry point, the AWT window
|
|
162
|
+
listener and window-ID registry, the command socket (request/response, one client at a time),
|
|
163
|
+
the event socket (`hello`, `snapshot`, `window_opened`/`window_closed`, `keepalive`, `overflow`),
|
|
164
|
+
running every UI read and write on the EDT, and credential redaction (`dump` redacts, `get_text`
|
|
165
|
+
refuses).
|
|
166
|
+
- **Provides:** the wire protocol to L3.
|
|
167
|
+
- **Rules:** the agent reports and executes; it makes no login, dialog or lifecycle decisions. Any
|
|
168
|
+
protocol change updates L3 in the same change.
|
|
169
|
+
|
|
170
|
+
### L3 Agent client (`agent_client.py`)
|
|
171
|
+
|
|
172
|
+
- **Owns:** the two socket connections, JSON line framing, the typed message schemas
|
|
173
|
+
(`attrs`/`cattrs`), and the typed exceptions mapping L2's error codes.
|
|
174
|
+
- **Provides:** one typed method per agent command; an async iterator over event messages.
|
|
175
|
+
- **Rules:** no scheduling, no fan-out, no imports from other `ibcontroller` modules. `set_text` and
|
|
176
|
+
`set_text_near_label` are the only places a `Secret` is unwrapped.
|
|
177
|
+
|
|
178
|
+
### L4 Dispatcher (`dispatch.py`)
|
|
179
|
+
|
|
180
|
+
- **Owns:** one `Dispatcher` per instance: both L3 connections, command serialization (one FIFO
|
|
181
|
+
worker, one command on the wire at a time), event fan-out, and the optional wire trace.
|
|
182
|
+
- **Provides:** `send_command`, `window_events`, `tasks` (for L6 to detect a lost connection),
|
|
183
|
+
`start`/`stop`.
|
|
184
|
+
- **Rules:** never interprets commands or events beyond routing them.
|
|
185
|
+
|
|
186
|
+
### L5-1 Actions (`actions.py`, `labels.py`)
|
|
187
|
+
|
|
188
|
+
- **Owns:** the action vocabulary (`click`, `type_text`, `toggle`, `navigate_menu`, `expand_tree`,
|
|
189
|
+
`dump`, `wait_for_event`, …) and the label data.
|
|
190
|
+
- **Rules:** the only L5 code that calls `send_command`. L3 exceptions propagate unchanged. Actions
|
|
191
|
+
take a `window_id` wherever the agent supports one.
|
|
192
|
+
|
|
193
|
+
### L5 Domains
|
|
194
|
+
|
|
195
|
+
| Domain | Code | Role |
|
|
196
|
+
| ---------- | ------------------------------------------------------------- | ----------------------------------------------------------------------------- |
|
|
197
|
+
| Login | `login.py`, login-outcome recognisers in `recognisers.py` | Fill credentials, detect success, MFA and failure, report the outcome. |
|
|
198
|
+
| Dialogs | `RecognizerRegistry`, `watch_for_unprompted_windows` | Recognise and handle windows nobody is waiting for. |
|
|
199
|
+
| Settings | `settings.py`, `builtin_settings.toml`, `ibkr_settings.toml` | Apply declared Global Configuration settings. |
|
|
200
|
+
| Scheduling | `schedule.py` | Wall-clock maths for cold restart and close-down. No I/O. |
|
|
201
|
+
| Diagnostic | `diagnostics.py` | Dump window structure for development and support. |
|
|
202
|
+
| Management | not built | Graceful shutdown, restart and other lifecycle operations on the UI. |
|
|
203
|
+
|
|
204
|
+
- **Rules:**
|
|
205
|
+
- Act on the UI only through L5-1.
|
|
206
|
+
- Report outcomes to L6 as return values or typed exceptions. L6 decides relaunch, exit or
|
|
207
|
+
continue.
|
|
208
|
+
- Domains do not call each other; L6 wires them together.
|
|
209
|
+
- A recogniser acts only on the window it matched (`window_id`). The dialogs registry never acts
|
|
210
|
+
on a window with a visible credential field.
|
|
211
|
+
- **Known deviations:**
|
|
212
|
+
- `recognisers.py` and `diagnostics.py` subscribe to `window_events` directly instead of through
|
|
213
|
+
L5-1.
|
|
214
|
+
- The registry construction and the settings sequence live in `control_loop.py`.
|
|
215
|
+
- Management's pieces live in L1 (`clean_shutdown`) and L6 (scheduled shutdown).
|
|
216
|
+
|
|
217
|
+
### L6 Control loop (`control_loop.py`)
|
|
218
|
+
|
|
219
|
+
- **Owns:** one instance's lifecycle, one cycle per JVM: launch → login (with the dialogs watcher) →
|
|
220
|
+
settings → ready → shutdown. Races every phase against process exit and connection loss,
|
|
221
|
+
classifies why the cycle ended (`ShutdownCause`), applies the relaunch policy, and owns every
|
|
222
|
+
task started during a cycle.
|
|
223
|
+
- **Provides:** `run_control_loop`, cancelled to request a graceful stop. `cli.py` maps its result
|
|
224
|
+
and exceptions to exit codes.
|
|
225
|
+
- **Rules:** the only layer that decides relaunch or stop. Cancellation is the only stop request.
|
|
226
|
+
|
|
227
|
+
### L7 Control API (planned, not built)
|
|
228
|
+
|
|
229
|
+
The contract below is provisional: it fixes the boundaries, not the interface.
|
|
230
|
+
|
|
231
|
+
- **Purpose:** let something outside the process observe a running instance and request lifecycle
|
|
232
|
+
operations on it: an operator, a supervisor, a container health check.
|
|
233
|
+
- **Owns:** the external interface: transport, access control, request validation, response
|
|
234
|
+
format, and mapping each request to an L6 or Management operation.
|
|
235
|
+
- **Consumes:** L6, for lifecycle requests and instance state (startup state, login state, last
|
|
236
|
+
`ShutdownCause`), and Management (L5) operations, through L6.
|
|
237
|
+
- **Rules:**
|
|
238
|
+
- Never reaches L4, L3 or L2 directly, and never drives the UI through L5-1 itself. A UI
|
|
239
|
+
operation is a Management operation, so serialization, window scoping and logging stay in L5.
|
|
240
|
+
- Never stops, kills or relaunches the process itself. It asks L6, which stays the only owner of
|
|
241
|
+
the cycle.
|
|
242
|
+
- Requests are intents, not blocking calls: a long operation is accepted, and its progress is
|
|
243
|
+
observable through state.
|
|
244
|
+
- Scoped to one instance, like every other layer.
|
|
245
|
+
- Local-only by default. Remote access needs explicit configuration, like IBC's `BindAddress` and
|
|
246
|
+
`ControlFrom`.
|
|
247
|
+
- Never returns credentials. Diagnostic output follows the same redaction as `dump`.
|
|
248
|
+
- **Candidate operations, not committed:** status; stop; restart and cold restart; IBC
|
|
249
|
+
`CommandServer` parity (`STOP`, `EXIT`, `RESTART`, `ENABLEAPI`, `RECONNECTDATA`,
|
|
250
|
+
`RECONNECTACCOUNT`); a diagnostic dump.
|
|
251
|
+
- **Open questions:** transport and access control; which IBC commands to support; whether
|
|
252
|
+
user-authored rules and plugins attach at L7 or at L5.
|