py-ib-controller 0.1.3__tar.gz → 0.1.5__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.
Files changed (96) hide show
  1. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/.github/workflows/ci.yml +5 -2
  2. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/.github/workflows/publish.yml +1 -1
  3. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/Makefile +15 -0
  4. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/PKG-INFO +141 -18
  5. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/README.md +140 -17
  6. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/agent/src/main/java/ibcontroller/agent/AgentMain.java +78 -66
  7. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/agent/src/main/java/ibcontroller/agent/ComponentLookup.java +37 -83
  8. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/agent/src/main/java/ibcontroller/agent/EventBridge.java +74 -41
  9. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/agent/src/main/java/ibcontroller/agent/Protocol.java +25 -12
  10. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/agent/src/main/java/ibcontroller/agent/Security.java +3 -4
  11. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/agent/src/main/java/ibcontroller/agent/WriteOps.java +51 -99
  12. py_ib_controller-0.1.5/agent/src/test/java/ibcontroller/agent/AgentTests.java +243 -0
  13. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/docs/Configuration.md +82 -5
  14. py_ib_controller-0.1.5/docs/development.md +469 -0
  15. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/ibcontroller/actions.py +97 -81
  16. py_ib_controller-0.1.5/ibcontroller/agent_client.py +605 -0
  17. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/ibcontroller/cli.py +55 -22
  18. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/ibcontroller/config.py +132 -34
  19. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/ibcontroller/control_loop.py +331 -220
  20. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/ibcontroller/data/builtin_settings.toml +1 -1
  21. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/ibcontroller/data/ibcontroller.toml.example +64 -18
  22. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/ibcontroller/data/ibkr_settings.toml.example +30 -20
  23. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/ibcontroller/data/labels.json +65 -4
  24. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/ibcontroller/data/labels.json.example +10 -11
  25. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/ibcontroller/diagnostics.py +53 -18
  26. py_ib_controller-0.1.5/ibcontroller/dispatch.py +391 -0
  27. py_ib_controller-0.1.5/ibcontroller/errors.py +33 -0
  28. py_ib_controller-0.1.5/ibcontroller/ibcontroller-agent.jar +0 -0
  29. py_ib_controller-0.1.5/ibcontroller/labels.py +437 -0
  30. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/ibcontroller/launcher.py +393 -219
  31. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/ibcontroller/logging_setup.py +86 -22
  32. py_ib_controller-0.1.5/ibcontroller/login.py +660 -0
  33. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/ibcontroller/main.py +22 -13
  34. py_ib_controller-0.1.5/ibcontroller/management.py +124 -0
  35. py_ib_controller-0.1.5/ibcontroller/recognisers.py +789 -0
  36. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/ibcontroller/schedule.py +2 -1
  37. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/ibcontroller/settings.py +158 -95
  38. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/ibcontroller.env +33 -11
  39. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/py_ib_controller.egg-info/PKG-INFO +141 -18
  40. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/py_ib_controller.egg-info/SOURCES.txt +7 -0
  41. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/py_ib_controller.egg-info/scm_file_list.json +7 -0
  42. py_ib_controller-0.1.5/py_ib_controller.egg-info/scm_version.json +8 -0
  43. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/pyproject.toml +1 -0
  44. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/tests/conftest.py +12 -4
  45. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/tests/fakes.py +42 -0
  46. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/tests/test_actions.py +214 -196
  47. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/tests/test_agent_client.py +456 -33
  48. py_ib_controller-0.1.5/tests/test_agent_jar.py +136 -0
  49. py_ib_controller-0.1.5/tests/test_cli.py +351 -0
  50. py_ib_controller-0.1.5/tests/test_command_wire.py +162 -0
  51. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/tests/test_config.py +170 -19
  52. py_ib_controller-0.1.5/tests/test_control_loop.py +1172 -0
  53. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/tests/test_diagnostics.py +101 -4
  54. py_ib_controller-0.1.5/tests/test_dispatch.py +710 -0
  55. py_ib_controller-0.1.5/tests/test_errors.py +73 -0
  56. py_ib_controller-0.1.5/tests/test_labels.py +240 -0
  57. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/tests/test_launcher.py +676 -309
  58. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/tests/test_logging_setup.py +46 -3
  59. py_ib_controller-0.1.5/tests/test_login.py +1858 -0
  60. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/tests/test_main.py +7 -8
  61. py_ib_controller-0.1.5/tests/test_management.py +306 -0
  62. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/tests/test_recognisers.py +719 -32
  63. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/tests/test_schedule.py +5 -3
  64. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/tests/test_settings.py +505 -60
  65. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/tests/test_trace_integration.py +12 -11
  66. py_ib_controller-0.1.3/docs/development.md +0 -132
  67. py_ib_controller-0.1.3/ibcontroller/agent_client.py +0 -466
  68. py_ib_controller-0.1.3/ibcontroller/dispatch.py +0 -288
  69. py_ib_controller-0.1.3/ibcontroller/ibcontroller-agent.jar +0 -0
  70. py_ib_controller-0.1.3/ibcontroller/labels.py +0 -242
  71. py_ib_controller-0.1.3/ibcontroller/login.py +0 -578
  72. py_ib_controller-0.1.3/ibcontroller/recognisers.py +0 -448
  73. py_ib_controller-0.1.3/py_ib_controller.egg-info/scm_version.json +0 -8
  74. py_ib_controller-0.1.3/tests/test_cli.py +0 -173
  75. py_ib_controller-0.1.3/tests/test_control_loop.py +0 -688
  76. py_ib_controller-0.1.3/tests/test_dispatch.py +0 -360
  77. py_ib_controller-0.1.3/tests/test_labels.py +0 -85
  78. py_ib_controller-0.1.3/tests/test_login.py +0 -1263
  79. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/.github/ISSUE_TEMPLATE.md +0 -0
  80. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/.github/dependabot.yml +0 -0
  81. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/.gitignore +0 -0
  82. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/.pre-commit-config.yaml +0 -0
  83. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/.sdkmanrc +0 -0
  84. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/CONTRIBUTING.md +0 -0
  85. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/LICENSE.txt +0 -0
  86. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/docs/Architecture.svg +0 -0
  87. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/ibcontroller/__init__.py +0 -0
  88. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/ibcontroller/app_dirs.py +0 -0
  89. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/ibcontroller/py.typed +0 -0
  90. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/py_ib_controller.egg-info/dependency_links.txt +0 -0
  91. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/py_ib_controller.egg-info/entry_points.txt +0 -0
  92. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/py_ib_controller.egg-info/requires.txt +0 -0
  93. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/py_ib_controller.egg-info/top_level.txt +0 -0
  94. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/setup.cfg +0 -0
  95. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/tests/__init__.py +0 -0
  96. {py_ib_controller-0.1.3 → py_ib_controller-0.1.5}/tests/test_app_dirs.py +0 -0
@@ -21,7 +21,7 @@ jobs:
21
21
  - uses: actions/checkout@v7
22
22
 
23
23
  - name: Install uv and Python
24
- uses: astral-sh/setup-uv@v10.1.0
24
+ uses: astral-sh/setup-uv@v10.2.0
25
25
  with:
26
26
  python-version: ${{ matrix.python-version }}
27
27
 
@@ -40,8 +40,11 @@ jobs:
40
40
  - name: Build Java agent jar
41
41
  run: make build
42
42
 
43
+ - name: Test Java agent
44
+ run: make test-java
45
+
43
46
  - name: Test
44
- run: uv run pytest
47
+ run: uv run pytest -n auto
45
48
 
46
49
  - name: Build
47
50
  run: uv build
@@ -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.1.0
29
+ uses: astral-sh/setup-uv@v10.2.0
30
30
  with:
31
31
  python-version: ${{ env.PYTHON_VERSION }}
32
32
 
@@ -19,6 +19,21 @@ build/ibcontroller-agent.jar: $(SRC)
19
19
  .PHONY: build
20
20
  build: build/ibcontroller-agent.jar
21
21
 
22
+ # Dependency-free Java unit tests (plain javac + java, same toolchain as the jar).
23
+ TEST_SRC := $(wildcard agent/src/test/java/ibcontroller/agent/*.java)
24
+
25
+ .PHONY: test-java
26
+ test-java: $(SRC) $(TEST_SRC)
27
+ mkdir -p build/test-classes
28
+ $(JAVAC) -d build/test-classes $(SRC) $(TEST_SRC)
29
+ $(JDK)/bin/java -Djava.awt.headless=true -cp build/test-classes ibcontroller.agent.AgentTests
30
+
31
+ # Everything: build the jar, run the Java unit tests, then the Python suite (which
32
+ # includes the tests that drive the built jar).
33
+ .PHONY: test
34
+ test: build test-java
35
+ uv run pytest -n auto
36
+
22
37
  # The Python package can't be built standalone -- `ibcontroller/ibcontroller-agent.jar`
23
38
  # must exist before `uv build` runs, since it's shipped via [tool.setuptools.package-data]
24
39
  # the same way ibcontroller/data/*.json is (see pyproject.toml). This target makes that
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: py-ib-controller
3
- Version: 0.1.3
3
+ Version: 0.1.5
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
- To build from source, this is the short version, for a detailed guide please check the [development guide](./docs/development.md).
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 -- it will still fail with a clear error naming whichever
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
@@ -121,7 +125,7 @@ And the `ibcontroller run` parameters:
121
125
  │ --trading-mode <live|paper> Override Config.trading_mode ('live'/'paper') for this invocation. │
122
126
  │ --tws-path <path> Override Config.tws_path (the TWS/Gateway install-path inference) for this invocation. │
123
127
  │ --tws-channel <str> Override Config.tws_channel ('stable'/'latest', filters install auto-detection) for this invocation. │
124
- │ --program <str> Override Config.program ('gateway'/'tws') for this invocation. │
128
+ │ --program <gateway|tws> Override Config.program ('gateway'/'tws') for this invocation. │
125
129
  │ --tws-settings-path <path> Override Config.tws_settings_path (where TWS/Gateway stores its own settings) for this invocation. │
126
130
  │ --instance <str> Override Config.instance (per-instance log/trace/socket names) for this invocation. Defaults to '{program}-{trading_mode}', so │
127
131
  │ --trading-mode alone is usually enough to keep paper/live instances apart. │
@@ -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
@@ -188,7 +197,7 @@ When environment variable `IBC_APP_DIR` is not set, then
188
197
  | --- | --- | --- |
189
198
  | `config_dir` | `~/.config/ibcontroller` | Linux |
190
199
  | `config_dir` | `~/Library/Application Support/ibcontroller` | macOS |
191
- | `log_dir` | `~/.local/share/ibcontroller` | Linux |
200
+ | `log_dir` | `~/.local/state/ibcontroller/log` | Linux |
192
201
  | `log_dir` | `~/Library/Logs/ibcontroller` | macOS |
193
202
  | `socket_dir` | `/run/user/<uid>/ibcontroller` | Linux |
194
203
  | `socket_dir` | `~/Library/Caches/TemporaryItems/ibcontroller` | macOS |
@@ -199,6 +208,13 @@ When `IBC_APP_DIR` is set then:
199
208
  - `log_dir`-> `IBC_APP_DIR/log`
200
209
  - `socket_dir` -> `IBC_APP_DIR/run`
201
210
 
211
+ Socket paths (`socket_dir/ibcontroller-agent-<instance>-events.sock`) must fit
212
+ the Unix socket limit: 102 bytes on macOS, 106 on Linux. On macOS the default
213
+ `socket_dir` leaves room for an instance name of about 15 characters with a
214
+ 5-letter user name (each extra user-name character costs one). Over the limit,
215
+ `ibcontroller run` stops with an error naming the path; use a shorter instance
216
+ name or set `IBC_APP_DIR` to a shorter directory.
217
+
202
218
  ### Settings
203
219
 
204
220
  `ibcontroller`ships with sensible defaults. **Only the two credential env vars below are actually mandatory**
@@ -214,7 +230,7 @@ allowing `None`. `Config`'s schema is closed — an unknown key in
214
230
  | TOML key | Env var or CLI flag | Description | Default |
215
231
  | --- | --- | --- | --- |
216
232
  | `instance` | `IBC_INSTANCE`, `--instance` | Instance name; separate log/trace files per instance | `"{program}-{trading_mode}"` (e.g. `"gateway-paper"`) |
217
- | `program` | `IBC_PROGRAM`, `--program` | `"gateway"` or `"tws"` | `"gateway"` |
233
+ | `program` | `IBC_PROGRAM`, `--program` | `"gateway"` or `"tws"` (any letter case); anything else is a config error | `"gateway"` |
218
234
  | `tws_version` | `IBC_TWS_VERSION` | Installed TWS/Gateway version, e.g. `"10.50"` | `None` — auto-detected (see below) |
219
235
  | `tws_channel` | `IBC_TWS_CHANNEL`, `--tws-channel` | `"stable"`/`"latest"`; narrows auto-detection to one update channel | `"stable"` |
220
236
  | `tws_path` | `IBC_TWS_PATH`, `--tws-path` | Override TWS/gateway install-path inference | `None` |
@@ -224,8 +240,11 @@ allowing `None`. `Config`'s schema is closed — an unknown key in
224
240
  | `log_dir` | `IBC_LOG_DIR` | Where ibcontroller's own log file lives; always resolved at startup | platform default |
225
241
  | `log_level` | `IBC_LOG_LEVEL` | `debug`/`info`/`warning`/`error` | `info` |
226
242
  | `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"` |
243
+ | `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) |
244
+ | `log_backup_count` | `IBC_LOG_BACKUP_COUNT` | Number of rotated backups to keep. Only respected when `log_sink="file"` | `5` |
227
245
  | `diagnostic_scope` | `IBC_DIAGNOSTIC_SCOPE` | `"known"`/`"unknown"`/`"all"` — which windows get a structure dump logged (see `docs/Configuration.md`'s "Diagnostics") | `"known"` |
228
246
  | `diagnostic_when` | `IBC_DIAGNOSTIC_WHEN` | `"open"`/`"openclose"`/`"never"` — when to log a structure dump | `"never"` |
247
+ | `diagnostic_settings_sweep` | `IBC_DIAGNOSTIC_SETTINGS_SWEEP` | Log a dump of each Global Configuration tree node in `labels.settings.sweep_paths` (see `docs/Configuration.md`'s "Diagnostics") | `false` |
229
248
  | (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 |
230
249
 
231
250
  TWS/ibgateway settings
@@ -233,16 +252,19 @@ TWS/ibgateway settings
233
252
  | TOML key | Env var or CLI flag | Description | Default |
234
253
  | --- | --- | --- | --- |
235
254
  | `trading_mode` | `IBC_TRADING_MODE`, `--trading-mode` | `"live"` or `"paper"`; which account to authenticate as | `"paper"` |
236
- | `read_only_login` | `IBC_READ_ONLY_LOGIN` | TWS-only; **not yet wired to any behavior** (loaded, unused — see TODO.md) | `false` |
255
+ | `read_only_login` | `IBC_READ_ONLY_LOGIN` | TWS-only (Gateway warns and logs in normally); clicks "Enter Read Only" on the 2FA frame instead of waiting for the push, no effect without 2FA (paper) | `false` |
237
256
  | `read_only_api` | `IBC_READ_ONLY_API` | `true`/`false` sets it; omit to leave unchanged. Applied to Gateway/TWS automatically -- see "Declarative configuration" below | `None` |
238
257
  | `accept_incoming_connections` | `IBC_ACCEPT_INCOMING_CONNECTIONS` | `manual`/`accept`/`reject` | `"manual"` |
239
258
  | `existing_session_action` | `IBC_EXISTING_SESSION_ACTION` | `manual`/`primary`/`primaryoverride`/`secondary` | `"manual"` |
240
259
  | `login_dialog_display_timeout` | `IBC_LOGIN_DIALOG_DISPLAY_TIMEOUT` | Seconds to wait for the login dialog | `60.0` |
241
- | `mfa_timeout` | `IBC_MFA_TIMEOUT` | IB's 2FA timeout in seconds | `180.0` |
242
- | `relogin_after_mfa_timeout` | `IBC_RELOGIN_AFTER_MFA_TIMEOUT` | Restart login if 2FA times out | `false` |
243
- | `mfa_exit_interval` | `IBC_MFA_EXIT_INTERVAL` | Bounds the post-2FA wait when relogin enabled | `60.0` |
260
+ | `mfa_timeout` | `IBC_MFA_TIMEOUT` | IB's 2FA limit in seconds; a dialog closing at or after it counts as timed out | `180.0` |
261
+ | `relogin_after_mfa_timeout` | `IBC_RELOGIN_AFTER_MFA_TIMEOUT` | Log in again after a timed-out push, and enforce `mfa_exit_interval` after an approved one | `false` |
262
+ | `mfa_exit_interval` | `IBC_MFA_EXIT_INTERVAL` | Post-approval wait for login to complete when relogin is on | `60.0` |
263
+ | `mfa_timeout_action` | `IBC_MFA_TIMEOUT_ACTION` | `exit` (exit 75, see "Exit codes" above) or `restart` (relaunch with a fresh login) when `mfa_exit_interval` expires after an approved push | `"exit"` |
264
+ | `restart_cap` | `IBC_RESTART_CAP` | Max consecutive relaunches that never reach READY (login frame timeout, transient login failure, logout, crash during login); then exit 70 | `3` |
244
265
  | `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
266
  | `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` |
267
+ | `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
268
  | `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
269
  | `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
270
 
@@ -288,7 +310,7 @@ US/Eastern invalidation window if that's the intent.
288
310
 
289
311
  ## Declarative configuration
290
312
 
291
- Beyond `ibcontroller.toml`, three things let you shape ibcontroller's behavior without
313
+ Beyond `ibcontroller.toml`, two things let you shape ibcontroller's behavior without
292
314
  writing any Python: **`labels.json`** (window/button text, plus simple pop-up dismissal
293
315
  rules) and **`ibkr_settings.toml`** (Global Configuration settings applied to Gateway/TWS
294
316
  at startup). Each is covered below.
@@ -300,10 +322,19 @@ more fields of the bundled default, field by field — you only need to include
300
322
  you're changing, not the whole file. Use it when a TWS/Gateway release renames a button
301
323
  or dialog title ibcontroller matches against.
302
324
 
325
+ A malformed file is rejected at startup (exit code 78) with a message naming the file and
326
+ the field: a string where a list is expected, an empty list of titles or buttons, invalid
327
+ JSON, or a `dismiss_rules` entry without `match_title`/`match_text`. A misspelled key is
328
+ logged as a warning and ignored, so check the log if an override seems to have no effect.
329
+
330
+ `generic.dismiss_buttons` lists the buttons tried, in order, to close a pop-up no specific
331
+ handler recognises (default `OK`, `Close`, `Dismiss`).
332
+
303
333
  The one part of `labels.json` that isn't just text is `dismiss_rules` — a list of
304
334
  "if a window matches, click this button" entries, checked on every window that opens.
305
335
  Each entry needs at least one of `match_title`/`match_text` (both, if given, must
306
- match); `click` is the button's label.
336
+ match); `click` is the button's label. `match_text` also matches message text with HTML
337
+ tags removed and entities decoded, so copy the phrase as it shows on screen.
307
338
 
308
339
  ```json
309
340
  {
@@ -317,13 +348,18 @@ match); `click` is the button's label.
317
348
  }
318
349
  ```
319
350
 
320
- `dismiss_rules` is a list, not a map — an override *replaces the whole list*, so if you
321
- want to keep the bundled entries (the non-brokerage-account warning, the auto-restart
322
- confirmation notice) alongside your own, copy them into your override file too rather
323
- than starting from an empty list. Good for any pop-up that's always handled the same
351
+ `dismiss_rules` entries are merged by `name`: an entry with a bundled rule's name replaces
352
+ that rule, an entry with a new name is added, and bundled rules (the non-brokerage-account
353
+ warning, the auto-restart confirmation notice) you don't name are kept. Removing a bundled
354
+ rule is not supported. Good for any pop-up that's always handled the same
324
355
  simple way; a dialog needing real logic (parsing a value out of its text, retrying,
325
356
  raising an error) isn't expressible this way and needs actual code.
326
357
 
358
+ Login-outcome dialogs ("Login failed", "Unrecognized Username or Password", the connection
359
+ and too-many-attempts warnings) and the "Shutdown progress" / "Restart in progress" windows
360
+ are always handled first. A `dismiss_rules` entry that matches one of them never runs, so a
361
+ credential rejection still exits with code 77 and a restart is still recognised as one.
362
+
327
363
  ### `ibkr_settings.toml` — Global Configuration settings
328
364
 
329
365
  Global Configuration settings (the "Configure" / "Settings" dialog inside Gateway/TWS)
@@ -338,7 +374,13 @@ are applied once, right after login, from two layers combined into one list:
338
374
  - **Your own `ibkr_settings.toml`** — anything else: any other Global Configuration
339
375
  checkbox or text field, activated by pointing `[settings] file = "..."` (or
340
376
  `IBC_SETTINGS_FILE`) at a real file (rename `ibkr_settings.toml.example` to
341
- start from the bundled worked examples, e.g. the `api_precautions_*` toggles).
377
+ start from the bundled worked examples: the `api_precautions_*` toggles, plus commented
378
+ entries for the API socket port, the master API client ID and "Send market data in lots
379
+ for US stocks" under `API/Settings`).
380
+
381
+ When no entry resolves to a value (the default), the dialog is not opened. Only the TWS Mosaic
382
+ layout is supported: on TWS Classic no setting is applied and the attempt waits for
383
+ `mfa_timeout` (default 180s) before failing. Gateway is not affected.
342
384
 
343
385
  An entry looks like this:
344
386
 
@@ -409,10 +451,17 @@ scrollback.
409
451
  | --- | --- | --- | --- |
410
452
  | `ibcontroller-{instance}.log` | ibcontroller app control flow | `log_sink=file` (else console) | `log_level` (default `info`) |
411
453
  | `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) |
454
+ | `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
455
  | `cmd-{instance}.jsonl` | raw wire: commands sent + results, NDJSON | `trace_enabled`; always file, not affected by `log_sink` | always (DEBUG emit, gated separately) |
414
456
  | `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
457
 
458
+ **Rotation** (`IBC_LOG_MAX_BYTES`/`IBC_LOG_BACKUP_COUNT`, default 10 MiB x 5
459
+ backups, **on by default**) applies to every file above in file mode. Set
460
+ `IBC_LOG_MAX_BYTES=0` to disable it (unbounded files). Python's files keep
461
+ their usual name active, with `.1`/`.2`/... as old backups. The Java agent log
462
+ is the opposite: once rotation is on, the active file is
463
+ `ibcontroller-java-agent-{instance}.log.0`, not the unsuffixed name.
464
+
416
465
  ### Log levels
417
466
 
418
467
  `IBC_LOG_LEVEL` (`debug`/`info`/`warning`/`error`, default `info`) controls
@@ -484,6 +533,80 @@ or both.
484
533
  instances (e.g. `paper` and `live`) can run in parallel and each tail its own files
485
534
  without interleaving, even if copied or globbed out of their directory.
486
535
 
536
+ ## Finding the TWS / Gateway installation
537
+
538
+ ibcontroller does not install anything: it launches the TWS or Gateway you already
539
+ installed with IBKR's own installer, using that install's jars and bundled JRE. It
540
+ resolves the installation in three steps.
541
+
542
+ 1. **Install root** (`tws_path`). Unless set, it defaults to:
543
+
544
+ | OS | Default root |
545
+ | --- | --- |
546
+ | macOS | `~/Applications` |
547
+ | Linux | `~/Jts` |
548
+
549
+ 2. **Version** (`tws_version`). A pinned `tws_version` is used as-is. Otherwise
550
+ ibcontroller scans the root, picks the greatest installed version and logs the
551
+ choice; `tws_channel` narrows the scan first. See "`tws_version` auto-detection"
552
+ above.
553
+
554
+ 3. **Install directory.** Version and `program` select the directory below the root:
555
+
556
+ | OS | Gateway | TWS |
557
+ | --- | --- | --- |
558
+ | macOS | `IB Gateway <version>/` | `Trader Workstation <version>/` |
559
+ | Linux | `ibgateway/<version>/` | `<version>/` |
560
+
561
+ A directory counts as an installation only if it has a `jars/` subfolder. The
562
+ classpath is built from those jars plus the install's `.install4j/i4jruntime.jar`,
563
+ and the JVM options come from its `ibgateway.vmoptions` / `tws.vmoptions`.
564
+
565
+ If nothing matches, ibcontroller exits with an error naming `tws_path` and
566
+ `tws_channel`. A `program = "tws"` request with no TWS install falls back to a
567
+ Gateway install; the reverse does not (details above).
568
+
569
+ Settings (`tws_settings_path`) are separate from the install root: each instance
570
+ defaults to its own `~/Jts/<instance>`, so `paper` and `live` do not share state.
571
+
572
+ ## Exit codes
573
+
574
+ `ibcontroller run` exits with a [BSD sysexits](https://man.freebsd.org/cgi/man.cgi?query=sysexits)
575
+ code, grouped by what a supervisor (a container's `restart:` policy, systemd's
576
+ `Restart=`) should do about it. The specific cause is always in the logs and in the
577
+ `stopped: <CAUSE>` line.
578
+
579
+ | Code | Meaning | Supervisor action |
580
+ | --- | --- | --- |
581
+ | `0` | Intentional stop (Ctrl-C, `closedown_at`, Gateway/TWS closed normally, e.g. File>Close, or this session yielded to another via `existing_session_action`: `stopped: SESSION_CEDED`) | Do not restart |
582
+ | `1` | Unhandled exception (a bug) | Restart, report |
583
+ | `2` | CLI usage error | Fix the command |
584
+ | `78` (`EX_CONFIG`) | Deployment error (bad config, install not found) | Do not retry |
585
+ | `77` (`EX_NOPERM`) | Credentials rejected | Do not retry (lockout risk) |
586
+ | `76` (`EX_PROTOCOL`) | A dialog `existing_session_action` rules out (Trading Login Handoff with `secondary`/`primaryoverride`) | Do not retry |
587
+ | `75` (`EX_TEMPFAIL`) | Login did not complete after 2FA, `mfa_timeout_action=exit` | Retry per policy |
588
+ | `70` (`EX_SOFTWARE`) | `restart_cap` consecutive relaunches never reached READY | Do not retry |
589
+ | `69` (`EX_UNAVAILABLE`) | Transient runtime failure (process crashed or killed, connection lost, JVM exited before becoming ready) | Retry |
590
+
591
+ `70`/`76`/`77`/`78` work with systemd's `RestartPreventExitStatus=`.
592
+
593
+ Some login-blocking dialogs IB reports as a server-side condition -- not a credential
594
+ rejection -- never reach this table at all: a "Login failed"/"Login Error"/"not currently available" dialog, or a
595
+ Gateway "Connection to server failed" reason other than a known credential/account one
596
+ (e.g. a stale restart token), make ibcontroller relaunch in-process with a full fresh
597
+ login, the same way a scheduled `cold_restart_time` does -- the container
598
+ itself does not exit for these, until `restart_cap` consecutive relaunches never reach READY
599
+ (exit 70).
600
+
601
+ ## Stopping under Docker
602
+
603
+ On SIGTERM (`docker stop`, Ctrl-C) a logged-in instance closes through File > Exit/Close so
604
+ TWS/Gateway can save its session and upload settings. Measured over 88 shutdowns: mean 1.3 s,
605
+ p99 4.3 s, max 4.9 s. A slow path (a dialog open, a stuck menu) can take longer: the menu retry
606
+ waits up to 5 s, the exit up to 8 s, then SIGTERM gets 5 s. Docker sends SIGKILL after 10 s by
607
+ default, which loses the graceful close. Set `stop_grace_period: 30s` (Compose) or
608
+ `docker stop -t 30` to keep it.
609
+
487
610
  ## License
488
611
 
489
612
  ```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
- To build from source, this is the short version, for a detailed guide please check the [development guide](./docs/development.md).
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 -- it will still fail with a clear error naming whichever
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
@@ -99,7 +103,7 @@ And the `ibcontroller run` parameters:
99
103
  │ --trading-mode <live|paper> Override Config.trading_mode ('live'/'paper') for this invocation. │
100
104
  │ --tws-path <path> Override Config.tws_path (the TWS/Gateway install-path inference) for this invocation. │
101
105
  │ --tws-channel <str> Override Config.tws_channel ('stable'/'latest', filters install auto-detection) for this invocation. │
102
- │ --program <str> Override Config.program ('gateway'/'tws') for this invocation. │
106
+ │ --program <gateway|tws> Override Config.program ('gateway'/'tws') for this invocation. │
103
107
  │ --tws-settings-path <path> Override Config.tws_settings_path (where TWS/Gateway stores its own settings) for this invocation. │
104
108
  │ --instance <str> Override Config.instance (per-instance log/trace/socket names) for this invocation. Defaults to '{program}-{trading_mode}', so │
105
109
  │ --trading-mode alone is usually enough to keep paper/live instances apart. │
@@ -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
@@ -166,7 +175,7 @@ When environment variable `IBC_APP_DIR` is not set, then
166
175
  | --- | --- | --- |
167
176
  | `config_dir` | `~/.config/ibcontroller` | Linux |
168
177
  | `config_dir` | `~/Library/Application Support/ibcontroller` | macOS |
169
- | `log_dir` | `~/.local/share/ibcontroller` | Linux |
178
+ | `log_dir` | `~/.local/state/ibcontroller/log` | Linux |
170
179
  | `log_dir` | `~/Library/Logs/ibcontroller` | macOS |
171
180
  | `socket_dir` | `/run/user/<uid>/ibcontroller` | Linux |
172
181
  | `socket_dir` | `~/Library/Caches/TemporaryItems/ibcontroller` | macOS |
@@ -177,6 +186,13 @@ When `IBC_APP_DIR` is set then:
177
186
  - `log_dir`-> `IBC_APP_DIR/log`
178
187
  - `socket_dir` -> `IBC_APP_DIR/run`
179
188
 
189
+ Socket paths (`socket_dir/ibcontroller-agent-<instance>-events.sock`) must fit
190
+ the Unix socket limit: 102 bytes on macOS, 106 on Linux. On macOS the default
191
+ `socket_dir` leaves room for an instance name of about 15 characters with a
192
+ 5-letter user name (each extra user-name character costs one). Over the limit,
193
+ `ibcontroller run` stops with an error naming the path; use a shorter instance
194
+ name or set `IBC_APP_DIR` to a shorter directory.
195
+
180
196
  ### Settings
181
197
 
182
198
  `ibcontroller`ships with sensible defaults. **Only the two credential env vars below are actually mandatory**
@@ -192,7 +208,7 @@ allowing `None`. `Config`'s schema is closed — an unknown key in
192
208
  | TOML key | Env var or CLI flag | Description | Default |
193
209
  | --- | --- | --- | --- |
194
210
  | `instance` | `IBC_INSTANCE`, `--instance` | Instance name; separate log/trace files per instance | `"{program}-{trading_mode}"` (e.g. `"gateway-paper"`) |
195
- | `program` | `IBC_PROGRAM`, `--program` | `"gateway"` or `"tws"` | `"gateway"` |
211
+ | `program` | `IBC_PROGRAM`, `--program` | `"gateway"` or `"tws"` (any letter case); anything else is a config error | `"gateway"` |
196
212
  | `tws_version` | `IBC_TWS_VERSION` | Installed TWS/Gateway version, e.g. `"10.50"` | `None` — auto-detected (see below) |
197
213
  | `tws_channel` | `IBC_TWS_CHANNEL`, `--tws-channel` | `"stable"`/`"latest"`; narrows auto-detection to one update channel | `"stable"` |
198
214
  | `tws_path` | `IBC_TWS_PATH`, `--tws-path` | Override TWS/gateway install-path inference | `None` |
@@ -202,8 +218,11 @@ allowing `None`. `Config`'s schema is closed — an unknown key in
202
218
  | `log_dir` | `IBC_LOG_DIR` | Where ibcontroller's own log file lives; always resolved at startup | platform default |
203
219
  | `log_level` | `IBC_LOG_LEVEL` | `debug`/`info`/`warning`/`error` | `info` |
204
220
  | `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"` |
221
+ | `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) |
222
+ | `log_backup_count` | `IBC_LOG_BACKUP_COUNT` | Number of rotated backups to keep. Only respected when `log_sink="file"` | `5` |
205
223
  | `diagnostic_scope` | `IBC_DIAGNOSTIC_SCOPE` | `"known"`/`"unknown"`/`"all"` — which windows get a structure dump logged (see `docs/Configuration.md`'s "Diagnostics") | `"known"` |
206
224
  | `diagnostic_when` | `IBC_DIAGNOSTIC_WHEN` | `"open"`/`"openclose"`/`"never"` — when to log a structure dump | `"never"` |
225
+ | `diagnostic_settings_sweep` | `IBC_DIAGNOSTIC_SETTINGS_SWEEP` | Log a dump of each Global Configuration tree node in `labels.settings.sweep_paths` (see `docs/Configuration.md`'s "Diagnostics") | `false` |
207
226
  | (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 |
208
227
 
209
228
  TWS/ibgateway settings
@@ -211,16 +230,19 @@ TWS/ibgateway settings
211
230
  | TOML key | Env var or CLI flag | Description | Default |
212
231
  | --- | --- | --- | --- |
213
232
  | `trading_mode` | `IBC_TRADING_MODE`, `--trading-mode` | `"live"` or `"paper"`; which account to authenticate as | `"paper"` |
214
- | `read_only_login` | `IBC_READ_ONLY_LOGIN` | TWS-only; **not yet wired to any behavior** (loaded, unused — see TODO.md) | `false` |
233
+ | `read_only_login` | `IBC_READ_ONLY_LOGIN` | TWS-only (Gateway warns and logs in normally); clicks "Enter Read Only" on the 2FA frame instead of waiting for the push, no effect without 2FA (paper) | `false` |
215
234
  | `read_only_api` | `IBC_READ_ONLY_API` | `true`/`false` sets it; omit to leave unchanged. Applied to Gateway/TWS automatically -- see "Declarative configuration" below | `None` |
216
235
  | `accept_incoming_connections` | `IBC_ACCEPT_INCOMING_CONNECTIONS` | `manual`/`accept`/`reject` | `"manual"` |
217
236
  | `existing_session_action` | `IBC_EXISTING_SESSION_ACTION` | `manual`/`primary`/`primaryoverride`/`secondary` | `"manual"` |
218
237
  | `login_dialog_display_timeout` | `IBC_LOGIN_DIALOG_DISPLAY_TIMEOUT` | Seconds to wait for the login dialog | `60.0` |
219
- | `mfa_timeout` | `IBC_MFA_TIMEOUT` | IB's 2FA timeout in seconds | `180.0` |
220
- | `relogin_after_mfa_timeout` | `IBC_RELOGIN_AFTER_MFA_TIMEOUT` | Restart login if 2FA times out | `false` |
221
- | `mfa_exit_interval` | `IBC_MFA_EXIT_INTERVAL` | Bounds the post-2FA wait when relogin enabled | `60.0` |
238
+ | `mfa_timeout` | `IBC_MFA_TIMEOUT` | IB's 2FA limit in seconds; a dialog closing at or after it counts as timed out | `180.0` |
239
+ | `relogin_after_mfa_timeout` | `IBC_RELOGIN_AFTER_MFA_TIMEOUT` | Log in again after a timed-out push, and enforce `mfa_exit_interval` after an approved one | `false` |
240
+ | `mfa_exit_interval` | `IBC_MFA_EXIT_INTERVAL` | Post-approval wait for login to complete when relogin is on | `60.0` |
241
+ | `mfa_timeout_action` | `IBC_MFA_TIMEOUT_ACTION` | `exit` (exit 75, see "Exit codes" above) or `restart` (relaunch with a fresh login) when `mfa_exit_interval` expires after an approved push | `"exit"` |
242
+ | `restart_cap` | `IBC_RESTART_CAP` | Max consecutive relaunches that never reach READY (login frame timeout, transient login failure, logout, crash during login); then exit 70 | `3` |
222
243
  | `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
244
  | `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` |
245
+ | `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
246
  | `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
247
  | `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
248
 
@@ -266,7 +288,7 @@ US/Eastern invalidation window if that's the intent.
266
288
 
267
289
  ## Declarative configuration
268
290
 
269
- Beyond `ibcontroller.toml`, three things let you shape ibcontroller's behavior without
291
+ Beyond `ibcontroller.toml`, two things let you shape ibcontroller's behavior without
270
292
  writing any Python: **`labels.json`** (window/button text, plus simple pop-up dismissal
271
293
  rules) and **`ibkr_settings.toml`** (Global Configuration settings applied to Gateway/TWS
272
294
  at startup). Each is covered below.
@@ -278,10 +300,19 @@ more fields of the bundled default, field by field — you only need to include
278
300
  you're changing, not the whole file. Use it when a TWS/Gateway release renames a button
279
301
  or dialog title ibcontroller matches against.
280
302
 
303
+ A malformed file is rejected at startup (exit code 78) with a message naming the file and
304
+ the field: a string where a list is expected, an empty list of titles or buttons, invalid
305
+ JSON, or a `dismiss_rules` entry without `match_title`/`match_text`. A misspelled key is
306
+ logged as a warning and ignored, so check the log if an override seems to have no effect.
307
+
308
+ `generic.dismiss_buttons` lists the buttons tried, in order, to close a pop-up no specific
309
+ handler recognises (default `OK`, `Close`, `Dismiss`).
310
+
281
311
  The one part of `labels.json` that isn't just text is `dismiss_rules` — a list of
282
312
  "if a window matches, click this button" entries, checked on every window that opens.
283
313
  Each entry needs at least one of `match_title`/`match_text` (both, if given, must
284
- match); `click` is the button's label.
314
+ match); `click` is the button's label. `match_text` also matches message text with HTML
315
+ tags removed and entities decoded, so copy the phrase as it shows on screen.
285
316
 
286
317
  ```json
287
318
  {
@@ -295,13 +326,18 @@ match); `click` is the button's label.
295
326
  }
296
327
  ```
297
328
 
298
- `dismiss_rules` is a list, not a map — an override *replaces the whole list*, so if you
299
- want to keep the bundled entries (the non-brokerage-account warning, the auto-restart
300
- confirmation notice) alongside your own, copy them into your override file too rather
301
- than starting from an empty list. Good for any pop-up that's always handled the same
329
+ `dismiss_rules` entries are merged by `name`: an entry with a bundled rule's name replaces
330
+ that rule, an entry with a new name is added, and bundled rules (the non-brokerage-account
331
+ warning, the auto-restart confirmation notice) you don't name are kept. Removing a bundled
332
+ rule is not supported. Good for any pop-up that's always handled the same
302
333
  simple way; a dialog needing real logic (parsing a value out of its text, retrying,
303
334
  raising an error) isn't expressible this way and needs actual code.
304
335
 
336
+ Login-outcome dialogs ("Login failed", "Unrecognized Username or Password", the connection
337
+ and too-many-attempts warnings) and the "Shutdown progress" / "Restart in progress" windows
338
+ are always handled first. A `dismiss_rules` entry that matches one of them never runs, so a
339
+ credential rejection still exits with code 77 and a restart is still recognised as one.
340
+
305
341
  ### `ibkr_settings.toml` — Global Configuration settings
306
342
 
307
343
  Global Configuration settings (the "Configure" / "Settings" dialog inside Gateway/TWS)
@@ -316,7 +352,13 @@ are applied once, right after login, from two layers combined into one list:
316
352
  - **Your own `ibkr_settings.toml`** — anything else: any other Global Configuration
317
353
  checkbox or text field, activated by pointing `[settings] file = "..."` (or
318
354
  `IBC_SETTINGS_FILE`) at a real file (rename `ibkr_settings.toml.example` to
319
- start from the bundled worked examples, e.g. the `api_precautions_*` toggles).
355
+ start from the bundled worked examples: the `api_precautions_*` toggles, plus commented
356
+ entries for the API socket port, the master API client ID and "Send market data in lots
357
+ for US stocks" under `API/Settings`).
358
+
359
+ When no entry resolves to a value (the default), the dialog is not opened. Only the TWS Mosaic
360
+ layout is supported: on TWS Classic no setting is applied and the attempt waits for
361
+ `mfa_timeout` (default 180s) before failing. Gateway is not affected.
320
362
 
321
363
  An entry looks like this:
322
364
 
@@ -387,10 +429,17 @@ scrollback.
387
429
  | --- | --- | --- | --- |
388
430
  | `ibcontroller-{instance}.log` | ibcontroller app control flow | `log_sink=file` (else console) | `log_level` (default `info`) |
389
431
  | `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) |
432
+ | `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
433
  | `cmd-{instance}.jsonl` | raw wire: commands sent + results, NDJSON | `trace_enabled`; always file, not affected by `log_sink` | always (DEBUG emit, gated separately) |
392
434
  | `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
435
 
436
+ **Rotation** (`IBC_LOG_MAX_BYTES`/`IBC_LOG_BACKUP_COUNT`, default 10 MiB x 5
437
+ backups, **on by default**) applies to every file above in file mode. Set
438
+ `IBC_LOG_MAX_BYTES=0` to disable it (unbounded files). Python's files keep
439
+ their usual name active, with `.1`/`.2`/... as old backups. The Java agent log
440
+ is the opposite: once rotation is on, the active file is
441
+ `ibcontroller-java-agent-{instance}.log.0`, not the unsuffixed name.
442
+
394
443
  ### Log levels
395
444
 
396
445
  `IBC_LOG_LEVEL` (`debug`/`info`/`warning`/`error`, default `info`) controls
@@ -462,6 +511,80 @@ or both.
462
511
  instances (e.g. `paper` and `live`) can run in parallel and each tail its own files
463
512
  without interleaving, even if copied or globbed out of their directory.
464
513
 
514
+ ## Finding the TWS / Gateway installation
515
+
516
+ ibcontroller does not install anything: it launches the TWS or Gateway you already
517
+ installed with IBKR's own installer, using that install's jars and bundled JRE. It
518
+ resolves the installation in three steps.
519
+
520
+ 1. **Install root** (`tws_path`). Unless set, it defaults to:
521
+
522
+ | OS | Default root |
523
+ | --- | --- |
524
+ | macOS | `~/Applications` |
525
+ | Linux | `~/Jts` |
526
+
527
+ 2. **Version** (`tws_version`). A pinned `tws_version` is used as-is. Otherwise
528
+ ibcontroller scans the root, picks the greatest installed version and logs the
529
+ choice; `tws_channel` narrows the scan first. See "`tws_version` auto-detection"
530
+ above.
531
+
532
+ 3. **Install directory.** Version and `program` select the directory below the root:
533
+
534
+ | OS | Gateway | TWS |
535
+ | --- | --- | --- |
536
+ | macOS | `IB Gateway <version>/` | `Trader Workstation <version>/` |
537
+ | Linux | `ibgateway/<version>/` | `<version>/` |
538
+
539
+ A directory counts as an installation only if it has a `jars/` subfolder. The
540
+ classpath is built from those jars plus the install's `.install4j/i4jruntime.jar`,
541
+ and the JVM options come from its `ibgateway.vmoptions` / `tws.vmoptions`.
542
+
543
+ If nothing matches, ibcontroller exits with an error naming `tws_path` and
544
+ `tws_channel`. A `program = "tws"` request with no TWS install falls back to a
545
+ Gateway install; the reverse does not (details above).
546
+
547
+ Settings (`tws_settings_path`) are separate from the install root: each instance
548
+ defaults to its own `~/Jts/<instance>`, so `paper` and `live` do not share state.
549
+
550
+ ## Exit codes
551
+
552
+ `ibcontroller run` exits with a [BSD sysexits](https://man.freebsd.org/cgi/man.cgi?query=sysexits)
553
+ code, grouped by what a supervisor (a container's `restart:` policy, systemd's
554
+ `Restart=`) should do about it. The specific cause is always in the logs and in the
555
+ `stopped: <CAUSE>` line.
556
+
557
+ | Code | Meaning | Supervisor action |
558
+ | --- | --- | --- |
559
+ | `0` | Intentional stop (Ctrl-C, `closedown_at`, Gateway/TWS closed normally, e.g. File>Close, or this session yielded to another via `existing_session_action`: `stopped: SESSION_CEDED`) | Do not restart |
560
+ | `1` | Unhandled exception (a bug) | Restart, report |
561
+ | `2` | CLI usage error | Fix the command |
562
+ | `78` (`EX_CONFIG`) | Deployment error (bad config, install not found) | Do not retry |
563
+ | `77` (`EX_NOPERM`) | Credentials rejected | Do not retry (lockout risk) |
564
+ | `76` (`EX_PROTOCOL`) | A dialog `existing_session_action` rules out (Trading Login Handoff with `secondary`/`primaryoverride`) | Do not retry |
565
+ | `75` (`EX_TEMPFAIL`) | Login did not complete after 2FA, `mfa_timeout_action=exit` | Retry per policy |
566
+ | `70` (`EX_SOFTWARE`) | `restart_cap` consecutive relaunches never reached READY | Do not retry |
567
+ | `69` (`EX_UNAVAILABLE`) | Transient runtime failure (process crashed or killed, connection lost, JVM exited before becoming ready) | Retry |
568
+
569
+ `70`/`76`/`77`/`78` work with systemd's `RestartPreventExitStatus=`.
570
+
571
+ Some login-blocking dialogs IB reports as a server-side condition -- not a credential
572
+ rejection -- never reach this table at all: a "Login failed"/"Login Error"/"not currently available" dialog, or a
573
+ Gateway "Connection to server failed" reason other than a known credential/account one
574
+ (e.g. a stale restart token), make ibcontroller relaunch in-process with a full fresh
575
+ login, the same way a scheduled `cold_restart_time` does -- the container
576
+ itself does not exit for these, until `restart_cap` consecutive relaunches never reach READY
577
+ (exit 70).
578
+
579
+ ## Stopping under Docker
580
+
581
+ On SIGTERM (`docker stop`, Ctrl-C) a logged-in instance closes through File > Exit/Close so
582
+ TWS/Gateway can save its session and upload settings. Measured over 88 shutdowns: mean 1.3 s,
583
+ p99 4.3 s, max 4.9 s. A slow path (a dialog open, a stuck menu) can take longer: the menu retry
584
+ waits up to 5 s, the exit up to 8 s, then SIGTERM gets 5 s. Docker sends SIGKILL after 10 s by
585
+ default, which loses the graceful close. Set `stop_grace_period: 30s` (Compose) or
586
+ `docker stop -t 30` to keep it.
587
+
465
588
  ## License
466
589
 
467
590
  ```text