py-ib-controller 0.0.0__tar.gz → 0.1.2__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 (77) hide show
  1. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/.github/workflows/ci.yml +7 -2
  2. py_ib_controller-0.1.2/.github/workflows/publish.yml +148 -0
  3. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/PKG-INFO +27 -13
  4. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/README.md +26 -12
  5. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/docs/development.md +8 -1
  6. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/ibcontroller/cli.py +1 -1
  7. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/ibcontroller/config.py +23 -4
  8. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/ibcontroller/ibcontroller-agent.jar +0 -0
  9. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/ibcontroller/launcher.py +24 -64
  10. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/py_ib_controller.egg-info/PKG-INFO +27 -13
  11. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/py_ib_controller.egg-info/SOURCES.txt +2 -0
  12. py_ib_controller-0.1.2/py_ib_controller.egg-info/scm_file_list.json +69 -0
  13. py_ib_controller-0.1.2/py_ib_controller.egg-info/scm_version.json +8 -0
  14. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/pyproject.toml +2 -0
  15. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/tests/test_config.py +38 -0
  16. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/tests/test_launcher.py +74 -69
  17. py_ib_controller-0.0.0/.github/workflows/publish.yml +0 -79
  18. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/.github/ISSUE_TEMPLATE.md +0 -0
  19. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/.github/dependabot.yml +0 -0
  20. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/.gitignore +0 -0
  21. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/.pre-commit-config.yaml +0 -0
  22. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/.sdkmanrc +0 -0
  23. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/CONTRIBUTING.md +0 -0
  24. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/LICENSE.txt +0 -0
  25. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/Makefile +0 -0
  26. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/agent/src/main/java/ibcontroller/agent/AgentMain.java +0 -0
  27. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/agent/src/main/java/ibcontroller/agent/ComponentLookup.java +0 -0
  28. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/agent/src/main/java/ibcontroller/agent/EventBridge.java +0 -0
  29. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/agent/src/main/java/ibcontroller/agent/Protocol.java +0 -0
  30. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/agent/src/main/java/ibcontroller/agent/Security.java +0 -0
  31. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/agent/src/main/java/ibcontroller/agent/WriteOps.java +0 -0
  32. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/docs/Architecture.svg +0 -0
  33. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/docs/Configuration.md +0 -0
  34. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/ibcontroller/__init__.py +0 -0
  35. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/ibcontroller/actions.py +0 -0
  36. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/ibcontroller/agent_client.py +0 -0
  37. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/ibcontroller/app_dirs.py +0 -0
  38. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/ibcontroller/control_loop.py +0 -0
  39. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/ibcontroller/data/builtin_settings.toml +0 -0
  40. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/ibcontroller/data/ibcontroller.toml.example +0 -0
  41. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/ibcontroller/data/ibkr_settings.toml.example +0 -0
  42. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/ibcontroller/data/labels.json +0 -0
  43. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/ibcontroller/data/labels.json.example +0 -0
  44. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/ibcontroller/diagnostics.py +0 -0
  45. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/ibcontroller/dispatch.py +0 -0
  46. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/ibcontroller/labels.py +0 -0
  47. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/ibcontroller/logging_setup.py +0 -0
  48. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/ibcontroller/login.py +0 -0
  49. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/ibcontroller/main.py +0 -0
  50. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/ibcontroller/py.typed +0 -0
  51. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/ibcontroller/recognisers.py +0 -0
  52. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/ibcontroller/schedule.py +0 -0
  53. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/ibcontroller/settings.py +0 -0
  54. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/ibcontroller.env +0 -0
  55. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/py_ib_controller.egg-info/dependency_links.txt +0 -0
  56. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/py_ib_controller.egg-info/entry_points.txt +0 -0
  57. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/py_ib_controller.egg-info/requires.txt +0 -0
  58. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/py_ib_controller.egg-info/top_level.txt +0 -0
  59. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/setup.cfg +0 -0
  60. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/tests/__init__.py +0 -0
  61. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/tests/conftest.py +0 -0
  62. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/tests/fakes.py +0 -0
  63. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/tests/test_actions.py +0 -0
  64. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/tests/test_agent_client.py +0 -0
  65. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/tests/test_app_dirs.py +0 -0
  66. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/tests/test_cli.py +0 -0
  67. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/tests/test_control_loop.py +0 -0
  68. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/tests/test_diagnostics.py +0 -0
  69. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/tests/test_dispatch.py +0 -0
  70. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/tests/test_labels.py +0 -0
  71. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/tests/test_logging_setup.py +0 -0
  72. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/tests/test_login.py +0 -0
  73. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/tests/test_main.py +0 -0
  74. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/tests/test_recognisers.py +0 -0
  75. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/tests/test_schedule.py +0 -0
  76. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/tests/test_settings.py +0 -0
  77. {py_ib_controller-0.0.0 → py_ib_controller-0.1.2}/tests/test_trace_integration.py +0 -0
@@ -5,6 +5,11 @@ on:
5
5
  branches: [master]
6
6
  pull_request:
7
7
  workflow_dispatch:
8
+ workflow_call:
9
+
10
+ env:
11
+ JAVA_VERSION: "17"
12
+ JAVA_DISTRIBUTION: zulu
8
13
 
9
14
  jobs:
10
15
  check-build-test:
@@ -29,8 +34,8 @@ jobs:
29
34
  - name: Set up JDK
30
35
  uses: actions/setup-java@v6
31
36
  with:
32
- distribution: zulu
33
- java-version: "17"
37
+ distribution: ${{ env.JAVA_DISTRIBUTION }}
38
+ java-version: ${{ env.JAVA_VERSION }}
34
39
 
35
40
  - name: Build Java agent jar
36
41
  run: make build
@@ -0,0 +1,148 @@
1
+ name: Publish
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ env:
8
+ PYTHON_VERSION: "3.12"
9
+ JAVA_VERSION: "17"
10
+ JAVA_DISTRIBUTION: zulu
11
+
12
+ jobs:
13
+ check:
14
+ uses: ./.github/workflows/ci.yml
15
+
16
+ build:
17
+ needs: check
18
+ runs-on: ubuntu-latest
19
+ steps:
20
+ - name: Checkout full history
21
+ uses: actions/checkout@v7
22
+ with:
23
+ # setuptools-scm derives the package version from git tags
24
+ # (pyproject.toml: dynamic = ["version"]); a shallow clone
25
+ # would silently produce a fallback/dev version.
26
+ fetch-depth: 0
27
+
28
+ - name: Install uv and Python
29
+ uses: astral-sh/setup-uv@v10.1.0
30
+ with:
31
+ python-version: ${{ env.PYTHON_VERSION }}
32
+
33
+ - name: Restore environment
34
+ run: uv sync --all-groups
35
+
36
+ - name: Set up JDK
37
+ uses: actions/setup-java@v6
38
+ with:
39
+ distribution: ${{ env.JAVA_DISTRIBUTION }}
40
+ java-version: ${{ env.JAVA_VERSION }}
41
+
42
+ - name: Build jar and Python distribution
43
+ run: make dist
44
+
45
+ - name: Store the distribution packages
46
+ uses: actions/upload-artifact@v7
47
+ with:
48
+ name: python-package-distributions
49
+ path: dist/
50
+
51
+ publish-to-pypi:
52
+ needs: build
53
+ runs-on: ubuntu-latest
54
+ environment: pypi
55
+ permissions:
56
+ contents: read
57
+ id-token: write # trusted publishing to PyPI -- no API token in secrets
58
+ steps:
59
+ - name: Download all the dists
60
+ uses: actions/download-artifact@v8
61
+ with:
62
+ name: python-package-distributions
63
+ path: dist/
64
+
65
+ - name: Publish to PyPI
66
+ uses: pypa/gh-action-pypi-publish@release/v1
67
+
68
+ github-release:
69
+ name: Sign distributions with Sigstore, upload to GitHub Release
70
+ needs: publish-to-pypi
71
+ runs-on: ubuntu-latest
72
+ permissions:
73
+ contents: write # mandatory for making GitHub Releases
74
+ id-token: write # mandatory for sigstore
75
+ steps:
76
+ - name: Checkout full history
77
+ uses: actions/checkout@v7
78
+ with:
79
+ # git-cliff needs the full commit/tag log to build the changelog
80
+ fetch-depth: 0
81
+
82
+ - name: Download all the dists
83
+ uses: actions/download-artifact@v8
84
+ with:
85
+ name: python-package-distributions
86
+ path: dist/
87
+
88
+ - name: Generate checksums
89
+ run: sha256sum dist/* > dist/SHA256SUMS
90
+
91
+ - name: Sign the dists with Sigstore
92
+ uses: sigstore/gh-action-sigstore-python@v3.5.0
93
+ with:
94
+ inputs: >-
95
+ ./dist/*.tar.gz ./dist/*.whl
96
+
97
+ - name: Generate changelog
98
+ uses: orhun/git-cliff-action@v4
99
+ with:
100
+ # no cliff.toml -- git-cliff's built-in default config already
101
+ # groups Conventional Commits (feat/fix/docs/...) correctly
102
+ args: --latest --tag ${{ github.ref_name }} --strip header
103
+ output: CHANGELOG_release.md
104
+
105
+ - name: Assemble release notes
106
+ env:
107
+ TAG: ${{ github.ref_name }}
108
+ REPO: ${{ github.repository }}
109
+ run: |
110
+ PREV_TAG=$(git describe --tags --abbrev=0 "$TAG^" 2>/dev/null || true)
111
+ {
112
+ cat CHANGELOG_release.md
113
+ echo
114
+ echo "## Installation"
115
+ echo
116
+ echo " pip install py-ib-controller==${TAG#v}"
117
+ echo
118
+ echo "## Verifying release artifacts"
119
+ echo
120
+ echo "Assets are signed with [Sigstore](https://www.sigstore.dev/). To verify:"
121
+ echo
122
+ echo " pip install sigstore"
123
+ echo " sigstore verify identity dist/*.whl dist/*.tar.gz \\"
124
+ echo " --cert-identity https://github.com/$REPO/.github/workflows/publish.yml@refs/tags/$TAG \\"
125
+ echo " --cert-oidc-issuer https://token.actions.githubusercontent.com"
126
+ echo
127
+ echo "\`SHA256SUMS\` is attached for checksum verification."
128
+ if [ -n "$PREV_TAG" ]; then
129
+ echo
130
+ echo "**Full Changelog**: https://github.com/$REPO/compare/$PREV_TAG...$TAG"
131
+ fi
132
+ } > notes.md
133
+
134
+ - name: Create GitHub Release
135
+ env:
136
+ GITHUB_TOKEN: ${{ github.token }}
137
+ run: >-
138
+ gh release create '${{ github.ref_name }}'
139
+ --repo '${{ github.repository }}' --notes-file notes.md
140
+
141
+ - name: Upload artifacts and signatures to GitHub Release
142
+ env:
143
+ GITHUB_TOKEN: ${{ github.token }}
144
+ # dist/ contains the built packages, SHA256SUMS, plus the
145
+ # sigstore-produced signatures and certificates.
146
+ run: >-
147
+ gh release upload '${{ github.ref_name }}' dist/**
148
+ --repo '${{ github.repository }}'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: py-ib-controller
3
- Version: 0.0.0
3
+ Version: 0.1.2
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
@@ -28,18 +28,36 @@ Heavily inspired by [IBC](https://github.com/IbcAlpha/IBC) project, now archived
28
28
 
29
29
  ## What it does?
30
30
 
31
- - Finds ibgateway/TWS installation, JRE and launchs it.
31
+ - Finds ibgateway/TWS installation, JRE and launch it.
32
32
  - Automates user/password entry, waits for MFA.
33
- - Automates ibgatewa/TWS settings.
33
+ - Automates ibgateway/TWS settings.
34
34
  - Manages pop-ups and automatically accept.
35
-
36
35
  - Manages ibgateway/TWS restarts.
37
36
 
38
37
  It provides a "**declarative**" engine, so settings and pop-ups can be "declared" by configuration entries.
39
38
 
40
39
  ## How to use it
41
40
 
41
+ To use `ibcontroller` the recommended way is to install with uv/pipx or pip:
42
+
42
43
  ```bash
44
+ # install with uv/pipx
45
+ uv tool install py-ib-controller
46
+
47
+ # with pip
48
+ uv pip install py-ib-controller
49
+ ```
50
+
51
+ Give it a spin, `ib-controller run --dotenv=.env-paper --trading-mode=paper --program=gateway --tws-channel latest`
52
+
53
+ https://github.com/user-attachments/assets/b97f7180-29d9-4e09-b32f-8be838dff0e2
54
+
55
+ To build from source, this is the short version, for a detailed guide please check the [development guide](./docs/development.md).
56
+
57
+ ```bash
58
+ # 0. Fetch the code
59
+ git clone https://github.com/gnzsnz/ibcontroller.git
60
+ cd ibcontroller
43
61
  # 1. Build the Java agent jar and install the package (editable, for a source checkout)
44
62
  make dist
45
63
  uv sync
@@ -47,11 +65,7 @@ uv sync
47
65
  # 2. Scaffold a starter config directory (platformdirs default, or $IBC_APP_DIR)
48
66
  uv run ibcontroller init
49
67
 
50
- # 3. Export IBC_USERID / IBC_PASSWORD (see Credentials below) -- the
51
- # only settings actually required. Everything else in the base ibcontroller.toml
52
- # is commented out and already safe to run as-is: trading_mode defaults to "paper",
53
- # and tws_version is auto-detected from standard tws_path.
54
-
68
+ # 3. Export IBC_USERID / IBC_PASSWORD or create an .env file (see Credentials below).
55
69
  # 4. Run it -- Ctrl-C for a graceful shutdown
56
70
  uv run ibcontroller run
57
71
  ```
@@ -61,8 +75,6 @@ uv run ibcontroller run
61
75
  run directly on a fresh install -- it will still fail with a clear error naming whichever
62
76
  setting is actually missing (in practice, just the two credential env vars).
63
77
 
64
- `ibcontroller version` prints the installed package version.
65
-
66
78
  ### CLI parameters
67
79
 
68
80
  You can use the following cli parameters:
@@ -138,7 +150,9 @@ environment variables, with precedence `defaults < TOML file < .env < environmen
138
150
  can be loaded from a `.env` file. Credentials are environment-variable-only and must
139
151
  never appear in the config file.
140
152
 
141
- A sample file with all settings is provided as an [example](./ibcontroller.env).
153
+ A sample file with all environment settings is provided as an [example](./ibcontroller.env).
154
+ In the same way a sample `.toml` file with all config is
155
+ [available](./ibcontroller/data/ibcontroller.toml.example).
142
156
 
143
157
  `ibcontroller init` (or the first `ibcontroller run`) copies starter templates into the
144
158
  config directory: `ibcontroller.toml` (every key commented out -- safe to run as-is,
@@ -263,7 +277,7 @@ malformed value is logged and skipped rather than aborting the other one.
263
277
 
264
278
  - `cold_restart_time` -- `"HH:MM"`, 24-hour, local time. Every **Sunday** at this time,
265
279
  ibcontroller closes the instance tidily and relaunches with a full fresh login forcing the weekly
266
- reauth IBKR requires around Sunday 01:00 US/Eastern token invalidation.
280
+ re-auth IBKR requires around Sunday 01:00 US/Eastern token invalidation.
267
281
  - `closedown_at` -- `"HH:MM"` (every day) or `"<Weekday> HH:MM"` (one day a week,
268
282
  `Weekday` a full English name, `Monday`..`Sunday`). Closes the instance tidily at that
269
283
  time, with no relaunch -- you're responsible for restarting it yourself.
@@ -6,18 +6,36 @@ Heavily inspired by [IBC](https://github.com/IbcAlpha/IBC) project, now archived
6
6
 
7
7
  ## What it does?
8
8
 
9
- - Finds ibgateway/TWS installation, JRE and launchs it.
9
+ - Finds ibgateway/TWS installation, JRE and launch it.
10
10
  - Automates user/password entry, waits for MFA.
11
- - Automates ibgatewa/TWS settings.
11
+ - Automates ibgateway/TWS settings.
12
12
  - Manages pop-ups and automatically accept.
13
-
14
13
  - Manages ibgateway/TWS restarts.
15
14
 
16
15
  It provides a "**declarative**" engine, so settings and pop-ups can be "declared" by configuration entries.
17
16
 
18
17
  ## How to use it
19
18
 
19
+ To use `ibcontroller` the recommended way is to install with uv/pipx or pip:
20
+
20
21
  ```bash
22
+ # install with uv/pipx
23
+ uv tool install py-ib-controller
24
+
25
+ # with pip
26
+ uv pip install py-ib-controller
27
+ ```
28
+
29
+ Give it a spin, `ib-controller run --dotenv=.env-paper --trading-mode=paper --program=gateway --tws-channel latest`
30
+
31
+ https://github.com/user-attachments/assets/b97f7180-29d9-4e09-b32f-8be838dff0e2
32
+
33
+ To build from source, this is the short version, for a detailed guide please check the [development guide](./docs/development.md).
34
+
35
+ ```bash
36
+ # 0. Fetch the code
37
+ git clone https://github.com/gnzsnz/ibcontroller.git
38
+ cd ibcontroller
21
39
  # 1. Build the Java agent jar and install the package (editable, for a source checkout)
22
40
  make dist
23
41
  uv sync
@@ -25,11 +43,7 @@ uv sync
25
43
  # 2. Scaffold a starter config directory (platformdirs default, or $IBC_APP_DIR)
26
44
  uv run ibcontroller init
27
45
 
28
- # 3. Export IBC_USERID / IBC_PASSWORD (see Credentials below) -- the
29
- # only settings actually required. Everything else in the base ibcontroller.toml
30
- # is commented out and already safe to run as-is: trading_mode defaults to "paper",
31
- # and tws_version is auto-detected from standard tws_path.
32
-
46
+ # 3. Export IBC_USERID / IBC_PASSWORD or create an .env file (see Credentials below).
33
47
  # 4. Run it -- Ctrl-C for a graceful shutdown
34
48
  uv run ibcontroller run
35
49
  ```
@@ -39,8 +53,6 @@ uv run ibcontroller run
39
53
  run directly on a fresh install -- it will still fail with a clear error naming whichever
40
54
  setting is actually missing (in practice, just the two credential env vars).
41
55
 
42
- `ibcontroller version` prints the installed package version.
43
-
44
56
  ### CLI parameters
45
57
 
46
58
  You can use the following cli parameters:
@@ -116,7 +128,9 @@ environment variables, with precedence `defaults < TOML file < .env < environmen
116
128
  can be loaded from a `.env` file. Credentials are environment-variable-only and must
117
129
  never appear in the config file.
118
130
 
119
- A sample file with all settings is provided as an [example](./ibcontroller.env).
131
+ A sample file with all environment settings is provided as an [example](./ibcontroller.env).
132
+ In the same way a sample `.toml` file with all config is
133
+ [available](./ibcontroller/data/ibcontroller.toml.example).
120
134
 
121
135
  `ibcontroller init` (or the first `ibcontroller run`) copies starter templates into the
122
136
  config directory: `ibcontroller.toml` (every key commented out -- safe to run as-is,
@@ -241,7 +255,7 @@ malformed value is logged and skipped rather than aborting the other one.
241
255
 
242
256
  - `cold_restart_time` -- `"HH:MM"`, 24-hour, local time. Every **Sunday** at this time,
243
257
  ibcontroller closes the instance tidily and relaunches with a full fresh login forcing the weekly
244
- reauth IBKR requires around Sunday 01:00 US/Eastern token invalidation.
258
+ re-auth IBKR requires around Sunday 01:00 US/Eastern token invalidation.
245
259
  - `closedown_at` -- `"HH:MM"` (every day) or `"<Weekday> HH:MM"` (one day a week,
246
260
  `Weekday` a full English name, `Monday`..`Sunday`). Closes the instance tidily at that
247
261
  time, with no relaunch -- you're responsible for restarting it yourself.
@@ -87,7 +87,7 @@ as it can be: `javac` + `jar`, wired through a small `Makefile`.
87
87
 
88
88
  ## TWS / IBKR Gateway itself
89
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.
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
91
 
92
92
  After the first run, `ibcontroller` renames the gateway/TWS script. This is required to manage restarts.
93
93
 
@@ -123,3 +123,10 @@ Day-to-day cycle: `make build` after any agent-side change, `uv run pytest` for
123
123
  source .venv/bin/activate
124
124
  ibcontroller run --dotenv=.env-paper --trading-mode=paper --program=gateway
125
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
+ ![ibcontroller schematic architecture](Architecture.svg)
@@ -198,7 +198,7 @@ def run( # noqa: PLR0913, PLR0917 -- one typer.Option per Config field, not
198
198
  def version() -> None:
199
199
  """Print the installed ibcontroller version."""
200
200
  try:
201
- typer.echo(_pkg_version("ibcontroller"))
201
+ typer.echo(_pkg_version("py-ib-controller"))
202
202
  except PackageNotFoundError:
203
203
  typer.echo("unknown (not installed)")
204
204
 
@@ -131,6 +131,19 @@ def _log_level_converter(raw: object) -> int:
131
131
  return _LOG_LEVELS[name]
132
132
 
133
133
 
134
+ def _expand_user(raw: object) -> str:
135
+ """Expands `~` regardless of source (factory default, TOML file, env var, CLI) --
136
+ without this, a bare `Path(...)` downstream treats a literal `~` as a relative path
137
+ component and creates a `~` directory instead of expanding to the home directory
138
+ (gitea #57). Single point of truth for every path-typed Config field, so no
139
+ consumption site needs its own `.expanduser()` call."""
140
+ return str(Path(str(raw)).expanduser())
141
+
142
+
143
+ def _optional_path_converter(raw: object) -> str | None:
144
+ return None if raw is None else _expand_user(raw)
145
+
146
+
134
147
  class TradingMode(StrEnum):
135
148
  LIVE = "live"
136
149
  PAPER = "paper"
@@ -208,7 +221,9 @@ class Config:
208
221
  # The IBKR settings file to load (if any) -- see `ibkr_settings.toml.example`.
209
222
  # Defaults to `None` (inert) until a user points `[settings] file` at a renamed
210
223
  # copy of the example file. See module docstring for why this is deliberate.
211
- settings_file: str | None = None
224
+ settings_file: str | None = ts.option(
225
+ default=None, converter=_optional_path_converter
226
+ )
212
227
  # instance default: f"{program}-{trading_mode.value}".
213
228
  instance: str = "{program}-{trading_mode}"
214
229
  program: str = "gateway" # "gateway" or "tws"
@@ -220,11 +235,13 @@ class Config:
220
235
  # installs (not a strict enum). Defaults to "stable", the safer pick when several
221
236
  # channels are installed side by side.
222
237
  tws_channel: str = "stable"
223
- tws_path: str | None = None
238
+ tws_path: str | None = ts.option(default=None, converter=_optional_path_converter)
224
239
  # Where TWS/Gateway itself stores its settings (IBC's own TWS_SETTINGS_PATH) --
225
240
  # deliberately NOT the same as tws_path (the install location). None defaults,
226
241
  # per instance, in launcher.py's resolve_tws_settings_path.
227
- tws_settings_path: str | None = None
242
+ tws_settings_path: str | None = ts.option(
243
+ default=None, converter=_optional_path_converter
244
+ )
228
245
  # IBC-key-compatible settings (file or env, never a secret).
229
246
  trading_mode: TradingMode = TradingMode.PAPER
230
247
  # IBC: ReadOnlyLogin -- loaded but not yet wired to any behavior.
@@ -280,7 +297,9 @@ class Config:
280
297
  # (DictLoader), so the config file's `log_dir` and the IBC_LOG_DIR env var
281
298
  # genuinely override it. See load_config for the loader order.
282
299
  trace_enabled: bool = False
283
- log_dir: str = ts.option(factory=lambda: str(resolve_app_dirs()[1]))
300
+ log_dir: str = ts.option(
301
+ factory=lambda: str(resolve_app_dirs()[1]), converter=_expand_user
302
+ )
284
303
  # Logging level for ibcontroller's own log file (not the raw wire trace).
285
304
  # Was previously unannotated (`log_level = logging.INFO`), which meant attrs
286
305
  # never turned it into a real field at all -- not configurable, silently fixed
@@ -16,7 +16,6 @@ import asyncio
16
16
  import functools
17
17
  import logging
18
18
  import platform
19
- import plistlib
20
19
  import re
21
20
  from dataclasses import dataclass
22
21
  from pathlib import Path
@@ -95,7 +94,7 @@ def _os_name() -> str:
95
94
 
96
95
  def _resolve_tws_path(config: Config, os_name: str) -> Path:
97
96
  if config.tws_path:
98
- return Path(config.tws_path).expanduser()
97
+ return Path(config.tws_path)
99
98
  return Path.home() / ("Applications" if os_name == "macos" else "Jts")
100
99
 
101
100
 
@@ -113,7 +112,7 @@ def resolve_tws_settings_path(config: Config) -> Path:
113
112
  running live and paper concurrently is this project's normal use case,
114
113
  not an edge case to opt into safety for."""
115
114
  if config.tws_settings_path:
116
- return Path(config.tws_settings_path).expanduser()
115
+ return Path(config.tws_settings_path)
117
116
  return Path.home() / "Jts" / config.instance
118
117
 
119
118
 
@@ -311,11 +310,10 @@ def _version_sort_key(version: str) -> tuple[int, ...]:
311
310
 
312
311
  def _read_i4j_variable(install4j_dir: Path, name: str) -> str | None:
313
312
  """A plain install4j `<variable name="{name}" value="..." />` entry --
314
- distinct from `_read_jxbrowser_key`'s own regex, which pulls a `-D` flag
315
- out of the much larger `javaOptions` variable's own value blob (two
316
- different shapes in the same `i4jparams.conf` file). Used for `channel`,
317
- which each installed version carries its own real value for, not a fixed
318
- label."""
313
+ distinct from `_read_i4j_vmoptions`'s own handling of the much larger
314
+ `javaOptions` variable's own value blob (two different shapes in the same
315
+ `i4jparams.conf` file). Used for `channel`, which each installed version
316
+ carries its own real value for, not a fixed label."""
319
317
  conf = install4j_dir / "i4jparams.conf"
320
318
  if not conf.is_file():
321
319
  return None
@@ -441,48 +439,17 @@ def _read_vmoptions_file(path: Path) -> list[str]:
441
439
  return options
442
440
 
443
441
 
444
- def _read_jxbrowser_key(install4j_dir: Path) -> str | None:
445
- """Passkey (WebAuthn) 2FA renders in an embedded browser (JxBrowser),
446
- which needs `-DjxBrowserKey=<license key>` to initialise at all. Read
447
- dynamically since the key is version-specific."""
448
- conf = install4j_dir / "i4jparams.conf"
449
- if not conf.is_file():
450
- return None
451
- match = re.search(r"DjxBrowserKey=([^\"\s]+)", conf.read_text())
452
- return match.group(1) if match else None
453
-
454
-
455
- def _read_macos_vmoptions(app_bundle: Path) -> list[str]:
456
- """`--add-opens`/`--add-exports` (JPMS strong encapsulation) live in the
457
- `.app` bundle's `Info.plist` (install4j's `JavaVM`/`VMOptionArray`), not
458
- in `ibgateway.vmoptions`. Read live from the installed app, not
459
- hardcoded, since it can change per version.
460
-
461
- Takes the `.app` bundle's own path directly (not `program_path`): the
462
- bundle may already have been renamed by `_prevent_native_restart`, so
463
- re-deriving `f"{program_path.name}.app"` here would silently miss it on
464
- every relaunch after the first."""
465
- app_plist = app_bundle / "Contents" / "Info.plist"
466
- if not app_plist.is_file():
467
- return []
468
- with app_plist.open("rb") as f:
469
- data = plistlib.load(f)
470
- raw_options = data.get("JavaVM", {}).get("VMOptionArray", [])
471
- return [opt for opt in raw_options if "${" not in opt and not opt.startswith("-D")]
472
-
473
-
474
- def _read_linux_vmoptions(install4j_dir: Path) -> list[str]:
475
- """`--add-opens`/`--add-exports` (JPMS strong encapsulation) live in
476
- install4j's own `javaOptions` variable inside `i4jparams.conf` on Linux,
477
- not in `tws.vmoptions`/`ibgateway.vmoptions`. Mirrors
478
- `_read_macos_vmoptions`'s role for `Info.plist` on macOS, just a
479
- different source file for the same install4j mechanism.
480
-
481
- Unlike the macOS version, `-D` tokens are kept here rather than filtered:
482
- on Linux this same variable is also the only place some installs carry
483
- real `-D` fixes (e.g. Gateway's `-Djdk.xml.elementAttributeLimit`) and a
484
- `-DjxBrowserKey`. A harmless duplicate of `_read_jxbrowser_key`'s own
485
- value when both fire -- the JVM just takes the last one."""
442
+ def _read_i4j_vmoptions(install4j_dir: Path) -> list[str]:
443
+ """`--add-opens`/`--add-exports` (JPMS strong encapsulation), the real
444
+ XML-parser `-D` fixes (e.g. Gateway's `-Djdk.xml.elementAttributeLimit`),
445
+ and `-DjxBrowserKey` all live in install4j's own `javaOptions` variable
446
+ inside `i4jparams.conf`, not in `tws.vmoptions`/`ibgateway.vmoptions` --
447
+ true on both Linux and macOS (checked directly against IBC's own
448
+ `ibcstart.sh`, which reads this same variable, from this same
449
+ `{program_path}/.install4j` location, on both platforms uniformly, and
450
+ never touches macOS's `Info.plist` at all). `-D` tokens are kept here
451
+ rather than filtered -- this is the only place some installs carry them,
452
+ so filtering would silently drop real options."""
486
453
  raw = _read_i4j_variable(install4j_dir, "javaOptions")
487
454
  if not raw:
488
455
  return []
@@ -501,9 +468,7 @@ def _prevent_native_restart(program_path: Path) -> Path:
501
468
  it back.
502
469
 
503
470
  Idempotent: a second call after the rename already happened is a no-op,
504
- returning the same renamed path. Returns the `.app` bundle's current real
505
- path either way, since `_read_macos_vmoptions` needs to read
506
- `Info.plist` from wherever it actually lives now, renamed or not.
471
+ returning the same renamed path.
507
472
 
508
473
  macOS only, called only under `os_name == "macos"` in `build_launch_plan`
509
474
  -- see `_prevent_native_restart_linux` for the Linux equivalent, called
@@ -538,10 +503,10 @@ def _prevent_native_restart_linux(program_path: Path, script_name: str) -> Path:
538
503
 
539
504
  Idempotent, matching `_prevent_native_restart`: a second call after the
540
505
  rename already happened is a no-op, returning the same renamed path.
541
- Unlike the macOS version, the return value doesn't need to be read back by
542
- a vmoptions reader -- `_read_linux_vmoptions` reads from
543
- `.install4j/i4jparams.conf`, which doesn't move when this script is
544
- renamed -- so callers only need this for its side effect."""
506
+ The return value doesn't need to be read back by a vmoptions reader --
507
+ `_read_i4j_vmoptions` reads from `.install4j/i4jparams.conf`, which
508
+ doesn't move when this script is renamed -- so callers only need this
509
+ for its side effect."""
545
510
  original = program_path / script_name
546
511
  renamed = program_path / f"{script_name}-1"
547
512
  if renamed.exists():
@@ -624,17 +589,12 @@ def build_launch_plan(
624
589
  channel = _read_i4j_variable(install4j_dir, "channel") or config.tws_channel
625
590
  vm_options.append(f"-Dchannel={channel}")
626
591
 
627
- jxbrowser_key = _read_jxbrowser_key(install4j_dir)
628
- if jxbrowser_key:
629
- vm_options.append(f"-DjxBrowserKey={jxbrowser_key}")
630
-
631
592
  if os_name == "macos":
632
- app_bundle = _prevent_native_restart(program_path)
633
- vm_options.extend(_read_macos_vmoptions(app_bundle))
593
+ _prevent_native_restart(program_path)
634
594
  else:
635
595
  script_name = "ibgateway" if resolved_program == "gateway" else "tws"
636
596
  _prevent_native_restart_linux(program_path, script_name)
637
- vm_options.extend(_read_linux_vmoptions(install4j_dir))
597
+ vm_options.extend(_read_i4j_vmoptions(install4j_dir))
638
598
 
639
599
  # Agent-side logging (java.util.logging, AgentMain.configureLogging, 2026-09-08):
640
600
  # its own per-instance file under the same shared log dir Python uses, so the
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: py-ib-controller
3
- Version: 0.0.0
3
+ Version: 0.1.2
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
@@ -28,18 +28,36 @@ Heavily inspired by [IBC](https://github.com/IbcAlpha/IBC) project, now archived
28
28
 
29
29
  ## What it does?
30
30
 
31
- - Finds ibgateway/TWS installation, JRE and launchs it.
31
+ - Finds ibgateway/TWS installation, JRE and launch it.
32
32
  - Automates user/password entry, waits for MFA.
33
- - Automates ibgatewa/TWS settings.
33
+ - Automates ibgateway/TWS settings.
34
34
  - Manages pop-ups and automatically accept.
35
-
36
35
  - Manages ibgateway/TWS restarts.
37
36
 
38
37
  It provides a "**declarative**" engine, so settings and pop-ups can be "declared" by configuration entries.
39
38
 
40
39
  ## How to use it
41
40
 
41
+ To use `ibcontroller` the recommended way is to install with uv/pipx or pip:
42
+
42
43
  ```bash
44
+ # install with uv/pipx
45
+ uv tool install py-ib-controller
46
+
47
+ # with pip
48
+ uv pip install py-ib-controller
49
+ ```
50
+
51
+ Give it a spin, `ib-controller run --dotenv=.env-paper --trading-mode=paper --program=gateway --tws-channel latest`
52
+
53
+ https://github.com/user-attachments/assets/b97f7180-29d9-4e09-b32f-8be838dff0e2
54
+
55
+ To build from source, this is the short version, for a detailed guide please check the [development guide](./docs/development.md).
56
+
57
+ ```bash
58
+ # 0. Fetch the code
59
+ git clone https://github.com/gnzsnz/ibcontroller.git
60
+ cd ibcontroller
43
61
  # 1. Build the Java agent jar and install the package (editable, for a source checkout)
44
62
  make dist
45
63
  uv sync
@@ -47,11 +65,7 @@ uv sync
47
65
  # 2. Scaffold a starter config directory (platformdirs default, or $IBC_APP_DIR)
48
66
  uv run ibcontroller init
49
67
 
50
- # 3. Export IBC_USERID / IBC_PASSWORD (see Credentials below) -- the
51
- # only settings actually required. Everything else in the base ibcontroller.toml
52
- # is commented out and already safe to run as-is: trading_mode defaults to "paper",
53
- # and tws_version is auto-detected from standard tws_path.
54
-
68
+ # 3. Export IBC_USERID / IBC_PASSWORD or create an .env file (see Credentials below).
55
69
  # 4. Run it -- Ctrl-C for a graceful shutdown
56
70
  uv run ibcontroller run
57
71
  ```
@@ -61,8 +75,6 @@ uv run ibcontroller run
61
75
  run directly on a fresh install -- it will still fail with a clear error naming whichever
62
76
  setting is actually missing (in practice, just the two credential env vars).
63
77
 
64
- `ibcontroller version` prints the installed package version.
65
-
66
78
  ### CLI parameters
67
79
 
68
80
  You can use the following cli parameters:
@@ -138,7 +150,9 @@ environment variables, with precedence `defaults < TOML file < .env < environmen
138
150
  can be loaded from a `.env` file. Credentials are environment-variable-only and must
139
151
  never appear in the config file.
140
152
 
141
- A sample file with all settings is provided as an [example](./ibcontroller.env).
153
+ A sample file with all environment settings is provided as an [example](./ibcontroller.env).
154
+ In the same way a sample `.toml` file with all config is
155
+ [available](./ibcontroller/data/ibcontroller.toml.example).
142
156
 
143
157
  `ibcontroller init` (or the first `ibcontroller run`) copies starter templates into the
144
158
  config directory: `ibcontroller.toml` (every key commented out -- safe to run as-is,
@@ -263,7 +277,7 @@ malformed value is logged and skipped rather than aborting the other one.
263
277
 
264
278
  - `cold_restart_time` -- `"HH:MM"`, 24-hour, local time. Every **Sunday** at this time,
265
279
  ibcontroller closes the instance tidily and relaunches with a full fresh login forcing the weekly
266
- reauth IBKR requires around Sunday 01:00 US/Eastern token invalidation.
280
+ re-auth IBKR requires around Sunday 01:00 US/Eastern token invalidation.
267
281
  - `closedown_at` -- `"HH:MM"` (every day) or `"<Weekday> HH:MM"` (one day a week,
268
282
  `Weekday` a full English name, `Monday`..`Sunday`). Closes the instance tidily at that
269
283
  time, with no relaunch -- you're responsible for restarting it yourself.