py-ib-controller 0.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ibcontroller/__init__.py +0 -0
- ibcontroller/actions.py +273 -0
- ibcontroller/agent_client.py +466 -0
- ibcontroller/app_dirs.py +44 -0
- ibcontroller/cli.py +211 -0
- ibcontroller/config.py +410 -0
- ibcontroller/control_loop.py +636 -0
- ibcontroller/data/builtin_settings.toml +37 -0
- ibcontroller/data/ibcontroller.toml.example +178 -0
- ibcontroller/data/ibkr_settings.toml.example +135 -0
- ibcontroller/data/labels.json +81 -0
- ibcontroller/data/labels.json.example +44 -0
- ibcontroller/diagnostics.py +119 -0
- ibcontroller/dispatch.py +288 -0
- ibcontroller/ibcontroller-agent.jar +0 -0
- ibcontroller/labels.py +242 -0
- ibcontroller/launcher.py +909 -0
- ibcontroller/logging_setup.py +277 -0
- ibcontroller/login.py +578 -0
- ibcontroller/main.py +156 -0
- ibcontroller/py.typed +0 -0
- ibcontroller/recognisers.py +448 -0
- ibcontroller/schedule.py +157 -0
- ibcontroller/settings.py +554 -0
- py_ib_controller-0.0.0.dist-info/METADATA +490 -0
- py_ib_controller-0.0.0.dist-info/RECORD +30 -0
- py_ib_controller-0.0.0.dist-info/WHEEL +5 -0
- py_ib_controller-0.0.0.dist-info/entry_points.txt +2 -0
- py_ib_controller-0.0.0.dist-info/licenses/LICENSE.txt +176 -0
- py_ib_controller-0.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# ibcontroller configuration file -- flat, no [section] headers.
|
|
2
|
+
#
|
|
3
|
+
# Precedence (highest to lowest):
|
|
4
|
+
# 1. Environment variables (override everything)
|
|
5
|
+
# 2. This config file
|
|
6
|
+
# 3. Built-in defaults
|
|
7
|
+
#
|
|
8
|
+
# Credentials are environment-variable-only and must never appear in this file --
|
|
9
|
+
# ibcontroller.config.load_config() refuses to start if it finds one here. They use a
|
|
10
|
+
# single pair regardless of trading_mode below (the mode says which account to
|
|
11
|
+
# authenticate as; it does not select different credential variables):
|
|
12
|
+
# IBC_USERID / IBC_PASSWORD
|
|
13
|
+
# Each also accepts a _FILE-suffixed variant (e.g. IBC_PASSWORD_FILE=/run/secrets/pass)
|
|
14
|
+
# that reads the value from a file instead -- Docker/Compose secrets, not the env var itself.
|
|
15
|
+
#
|
|
16
|
+
# A .env file also works, via `ibcontroller run --dotenv <path>` -- it only fills in
|
|
17
|
+
# environment variables that aren't already set (python-dotenv, override=False), so it's a
|
|
18
|
+
# convenience for populating the environment layer above, not a fourth layer of its own.
|
|
19
|
+
#
|
|
20
|
+
# This file is copied here by `ibcontroller init` (or automatically by `ibcontroller run`
|
|
21
|
+
# the first time it's missing) with every key commented out below -- as shipped, it
|
|
22
|
+
# changes nothing versus built-in defaults. Only IBC_USERID/IBC_PASSWORD
|
|
23
|
+
# are actually required to run -- everything else here has a safe default or is
|
|
24
|
+
# auto-detected from the real install.
|
|
25
|
+
#
|
|
26
|
+
# IMPORTANT: this schema is closed. Every key here must already be a real field in
|
|
27
|
+
# ibcontroller/config.py -- an unknown key is rejected at startup (a clean error
|
|
28
|
+
# message + non-zero exit, not silently ignored, not a crash-with-traceback). You
|
|
29
|
+
# cannot add a new TWS/Gateway-settings toggle by just adding a key here; that
|
|
30
|
+
# always needs a config.py code change first (a new Config field), plus adding it
|
|
31
|
+
# to settings.py's own allow-list if it's meant to drive a declarative setting via
|
|
32
|
+
# "value_from_config" -- see ibkr_settings.toml.example.
|
|
33
|
+
|
|
34
|
+
# Socket/instance name (env: IBC_INSTANCE). Optional: when unset,
|
|
35
|
+
# defaults to "{program}-{trading_mode}" (e.g. "gateway-paper").
|
|
36
|
+
# instance = "dev"
|
|
37
|
+
|
|
38
|
+
# "gateway" or "tws" (env: IBC_PROGRAM)
|
|
39
|
+
# Default "gateway"
|
|
40
|
+
# program = "gateway"
|
|
41
|
+
|
|
42
|
+
# Installed TWS/Gateway version, e.g. "10.50" (env: IBC_TWS_VERSION). Omit this
|
|
43
|
+
# key (the default) to auto-detect it from tws_path -- if more than one version is
|
|
44
|
+
# installed, the greatest is picked (optionally narrowed by tws_channel below); set this
|
|
45
|
+
# explicitly to pin one instead.
|
|
46
|
+
# tws_version = "10.50"
|
|
47
|
+
|
|
48
|
+
# Which update channel to prefer when auto-detecting tws_version -- "stable" or "latest"
|
|
49
|
+
# (env: IBC_TWS_CHANNEL), as labeled by the install itself
|
|
50
|
+
# (.install4j/i4jparams.conf). Ignored when tws_version is set explicitly.
|
|
51
|
+
# Default "stable"
|
|
52
|
+
# tws_channel = "stable"
|
|
53
|
+
|
|
54
|
+
# Override the install path launcher.py would otherwise infer (env: IBC_TWS_PATH)
|
|
55
|
+
# tws_path = "~/Jts"
|
|
56
|
+
# tws_path = ""
|
|
57
|
+
|
|
58
|
+
# Where TWS/Gateway itself stores its settings, deliberately NOT the same as tws_path
|
|
59
|
+
# (the install location). None defaults, {instance}={program}-{trading_mode}
|
|
60
|
+
# ie, ~/Jts/gateway-live
|
|
61
|
+
# tws_settings_path =
|
|
62
|
+
|
|
63
|
+
# TWS/Gateway is "live" or "paper" -- defaults to "paper" (the safe, non-monetary mode)
|
|
64
|
+
# when unset (env: IBC_TRADING_MODE)
|
|
65
|
+
# Default "paper"
|
|
66
|
+
# trading_mode = "paper"
|
|
67
|
+
|
|
68
|
+
# Which ibkr_settings.toml to apply at startup (settings.py's own
|
|
69
|
+
# apply_settings_from_file) -- see ibkr_settings.toml.example. Omit this key (the
|
|
70
|
+
# default) to apply nothing declaratively.
|
|
71
|
+
# (env: IBC_SETTINGS_FILE)
|
|
72
|
+
# settings_file = "~/.config/ibkr_settings.toml"
|
|
73
|
+
|
|
74
|
+
# Where ibcontroller's own log and trace files live (env: IBC_LOG_DIR).
|
|
75
|
+
# Optional: when unset, defaults to the platformdirs user-log dir (or
|
|
76
|
+
# $IBC_APP_DIR/log in docker mode). Every instance's files (log, trace,
|
|
77
|
+
# gateway stdout) land flat in this one directory.
|
|
78
|
+
# log_dir = "~/.local/share/ibcontroller"
|
|
79
|
+
# log_dir = ""
|
|
80
|
+
|
|
81
|
+
# Where ibcontroller's own log and Gateway/TWS's own console output go: "std"
|
|
82
|
+
# (console only) or "file" (only, under log_dir) -- exclusive, not both. Doesn't
|
|
83
|
+
# affect the wire trace above, which stays file-only regardless of this setting.
|
|
84
|
+
# (env: IBC_LOG_SINK)
|
|
85
|
+
# Default "std"
|
|
86
|
+
# log_sink = "std"
|
|
87
|
+
|
|
88
|
+
# Diagnostics -- off by default. When enabled, logs a component structure dump
|
|
89
|
+
# (the same primitive settings.py/recognisers.py use to find controls) for each
|
|
90
|
+
# matching window event, to ibcontroller-{instance}.log at INFO. Built-in
|
|
91
|
+
# replacement for poking the agent socket directly with socat/nc.
|
|
92
|
+
# diagnostic_scope: which windows qualify -- "known" (recognised by a built-in or
|
|
93
|
+
# declarative recogniser), "unknown", or "all" (env: IBC_DIAGNOSTIC_SCOPE)
|
|
94
|
+
# Default "known"
|
|
95
|
+
# diagnostic_scope = "known"
|
|
96
|
+
# diagnostic_when: which events trigger a dump -- "open", "openclose", or "never"
|
|
97
|
+
# (off) (env: IBC_DIAGNOSTIC_WHEN)
|
|
98
|
+
# Default "never"
|
|
99
|
+
# diagnostic_when = "open"
|
|
100
|
+
|
|
101
|
+
# IBC: ReadOnlyLogin -- TWS only (IBC disables it on Gateway); NOT YET WIRED to any real
|
|
102
|
+
# behavior in ibcontroller (loaded, but nothing acts on it yet -- see TODO.md).
|
|
103
|
+
# Default false
|
|
104
|
+
# read_only_login = false
|
|
105
|
+
|
|
106
|
+
# IBC: ReadOnlyApi -- true/false sets it
|
|
107
|
+
# (env: IBC_READ_ONLY_API)
|
|
108
|
+
# Default "false"
|
|
109
|
+
# read_only_api = false
|
|
110
|
+
|
|
111
|
+
# IBC: ExistingSessionDetectedAction -- how ibcontroller itself reacts to Gateway's
|
|
112
|
+
# "existing session detected" dialog: manual/primary/primaryoverride/secondary
|
|
113
|
+
# (env: IBC_EXISTING_SESSION_ACTION)
|
|
114
|
+
# Default "manual"
|
|
115
|
+
# existing_session_action = "manual"
|
|
116
|
+
|
|
117
|
+
# IBC: AcceptIncomingConnectionAction -- how ibcontroller reacts to TWS/Gateway's
|
|
118
|
+
# "Accept incoming connection" dialog, shown when an API client connects.
|
|
119
|
+
# If set to 'accept', ibcontroller automatically accepts incoming API connection
|
|
120
|
+
# dialogs. If set to 'reject', ibcontroller automatically rejects incoming API
|
|
121
|
+
# connection dialogs. If set to 'manual', the user must decide whether to accept
|
|
122
|
+
# or reject incoming API connection dialogs. The default is 'manual'.
|
|
123
|
+
# (env: IBC_ACCEPT_INCOMING_CONNECTIONS)
|
|
124
|
+
# accept_incoming_connections = "manual"
|
|
125
|
+
|
|
126
|
+
# --- Login/2FA timeout and retry settings -- ibcontroller's own policy, not written to
|
|
127
|
+
# --- Gateway/TWS itself (though mfa_timeout mirrors IBKR's
|
|
128
|
+
# --- real external 2FA window).
|
|
129
|
+
# IBC: LoginDialogDisplayTimeout, default 60
|
|
130
|
+
# login_dialog_display_timeout = 60.0
|
|
131
|
+
# IBC: SecondFactorAuthenticationTimeout, default 180
|
|
132
|
+
# mfa_timeout = 180.0
|
|
133
|
+
# IBC: ReloginAfterSecondFactorAuthenticationTimeout, default false
|
|
134
|
+
# relogin_after_mfa_timeout = false
|
|
135
|
+
# IBC: SecondFactorAuthenticationExitInterval, default 60
|
|
136
|
+
# mfa_exit_interval = 60.0
|
|
137
|
+
|
|
138
|
+
# IBC: AutoRestartTime -- same name, same "hh:mm AM/PM" format (e.g. "08:00 AM"), a
|
|
139
|
+
# single space between the two parts. Drives settings.py's "auto_restart_time" action
|
|
140
|
+
# (see ibkr_settings.toml.example) -- omit this key (the default) to leave Gateway's
|
|
141
|
+
# existing Auto Restart setting unchanged (env: IBC_AUTO_RESTART_TIME)
|
|
142
|
+
# Default None - not set
|
|
143
|
+
# auto_restart_time = "08:00 AM"
|
|
144
|
+
|
|
145
|
+
# IBC: AutoLogoffTime -- same "hh:mm AM/PM" format, same Lock and Exit control as
|
|
146
|
+
# auto_restart_time above (a shared radio-button pair: "Auto restart"/"Auto logoff").
|
|
147
|
+
# If you set BOTH keys, auto_restart_time wins (builtin_settings.toml applies it
|
|
148
|
+
# last, matching IBC's own precedence) -- set only one of the two in practice.
|
|
149
|
+
# Omit this key (the default) to leave the existing setting unchanged
|
|
150
|
+
# (env: IBC_AUTO_LOGOFF_TIME)
|
|
151
|
+
# Default None - not set
|
|
152
|
+
# auto_logoff_time = "08:00 AM"
|
|
153
|
+
|
|
154
|
+
# --- Applies to TWS and Gateway alike (ported from IBC's IbcTws.java, the shared
|
|
155
|
+
# --- base class both programs use). Both are ibcontroller's own scheduled actions,
|
|
156
|
+
# --- not GUI settings written to TWS/Gateway -- see control_loop.py. If both are
|
|
157
|
+
# --- set, whichever occurs first wins.
|
|
158
|
+
# IBC: ColdRestartTime -- "HH:MM", 24-hour, local time. Every Sunday at this time,
|
|
159
|
+
# ibcontroller closes the instance tidily and relaunches with a full fresh login
|
|
160
|
+
# (via ibcontroller's own launch_instance, not IBC's native-launcher File > Restart),
|
|
161
|
+
# forcing the weekly reauth IBKR requires around Sunday 01:00 US/Eastern token
|
|
162
|
+
# invalidation (env: IBC_COLD_RESTART_TIME)
|
|
163
|
+
# Default None - not set
|
|
164
|
+
# cold_restart_time = "07:05"
|
|
165
|
+
|
|
166
|
+
# IBC: ClosedownAt -- "HH:MM" (every day) or "<Weekday> HH:MM" (one day a week,
|
|
167
|
+
# Weekday a full English name, Monday..Sunday). At this time, ibcontroller closes
|
|
168
|
+
# the instance tidily with no relaunch -- you're responsible for restarting it
|
|
169
|
+
# yourself (env: IBC_CLOSEDOWN_AT)
|
|
170
|
+
# Default None - not set
|
|
171
|
+
# closedown_at = "22:00"
|
|
172
|
+
# closedown_at = "Friday 22:00"
|
|
173
|
+
|
|
174
|
+
# JVM heap for TWS/Gateway at launch, e.g. "1024m"/"4g". Overrides the -Xmx
|
|
175
|
+
# line in the installed .vmoptions file (ibgateway.vmoptions/tws.vmoptions);
|
|
176
|
+
# bare size, no -Xmx prefix (env: IBC_JAVA_HEAP_SIZE)
|
|
177
|
+
# Default None - leave the installed file's value untouched
|
|
178
|
+
# java_heap_size = "2g"
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Example ibkr_settings.toml -- declarative Gateway/TWS Global Configuration settings,
|
|
2
|
+
# applied once at instance startup by settings.py's apply_settings_from_file, layered
|
|
3
|
+
# on top of the bundled built-in entries (data/builtin_settings.toml -- read_only_api/
|
|
4
|
+
# auto_restart_time, applied from ibcontroller.toml/env alone, no file needed).
|
|
5
|
+
#
|
|
6
|
+
#
|
|
7
|
+
# --- Overriding a built-in entry ------------------------------------------------------
|
|
8
|
+
# If a future TWS/Gateway release moves a built-in control (a different tree_path or
|
|
9
|
+
# label), add an entry here with the SAME value_from_config as the built-in you're
|
|
10
|
+
# replacing -- settings.py's merge rule matches on that field and uses your entry
|
|
11
|
+
# instead of the bundled one, rather than applying both. Example (commented out --
|
|
12
|
+
# read_only_api's real navigation hasn't actually changed; this just shows the shape):
|
|
13
|
+
#
|
|
14
|
+
# [[settings]]
|
|
15
|
+
# tree_path = "API/Settings/Read-Only" # a hypothetical new location
|
|
16
|
+
# action = "toggle"
|
|
17
|
+
# label = "Read-Only API Access" # a hypothetical new label
|
|
18
|
+
# value_from_config = "read_only_api" # same key as the built-in -- this replaces it
|
|
19
|
+
#
|
|
20
|
+
# Each [[settings]] entry:
|
|
21
|
+
# tree_path -- Global Configuration's own JTree path, e.g. "API/Settings"
|
|
22
|
+
# action -- "toggle" (a checkbox/radio), "type_text", "type_text_near_label"
|
|
23
|
+
# (a field with no accessible name, found by position -- see
|
|
24
|
+
# field_index below), or "auto_restart_time" (a composite
|
|
25
|
+
# action, see its own section below -- takes no label/label_ref
|
|
26
|
+
# at all).
|
|
27
|
+
# label / label_ref -- the widget's accessible label, either literal ("label") or a
|
|
28
|
+
# name resolved through labels.json's settings.controls map
|
|
29
|
+
# ("label_ref") -- use label_ref for anything already named
|
|
30
|
+
# there (version-proofed against IBKR renaming it), label for
|
|
31
|
+
# anything not worth naming yet. Not used by "auto_restart_time".
|
|
32
|
+
# value / value_from_config -- the wanted value, either literal ("value") or an
|
|
33
|
+
# attribute name looked up on the already-loaded Config at
|
|
34
|
+
# apply time ("value_from_config") -- this is how
|
|
35
|
+
# Config.read_only_api flows down to the real Gateway
|
|
36
|
+
# setting without a second, redundant declaration. A value
|
|
37
|
+
# that resolves to None (either source) skips the entry
|
|
38
|
+
# entirely -- "leave unchanged", matching IBC's own "" ==
|
|
39
|
+
# leave-unchanged semantics.
|
|
40
|
+
#
|
|
41
|
+
# IMPORTANT: value_from_config only works for the fixed,
|
|
42
|
+
# closed set of Config fields settings.py already knows about
|
|
43
|
+
# (today: read_only_api, auto_restart_time) -- the override
|
|
44
|
+
# case above is the only reason to use it here. Config's own
|
|
45
|
+
# schema is closed (an unknown key in ibcontroller.toml itself
|
|
46
|
+
# fails at startup -- see ibcontroller.toml.example), so
|
|
47
|
+
# value_from_config can never be pointed at a new, made-up
|
|
48
|
+
# field just by editing this file. Naming anything else here
|
|
49
|
+
# logs a warning at load time and is silently skipped forever
|
|
50
|
+
# at apply time -- never fatal, but never applies either. For
|
|
51
|
+
# any setting that isn't one of the two above (like the
|
|
52
|
+
# api_precautions_* entries below), use a literal "value".
|
|
53
|
+
# field_index -- only used by "type_text_near_label"; which text field (by
|
|
54
|
+
# position, 0-based) within the container below the matched
|
|
55
|
+
# label. Defaults to 0.
|
|
56
|
+
#
|
|
57
|
+
# Known gap, not urgent (2026-09-06): some Global Configuration screens use dropdown
|
|
58
|
+
# (JComboBox) controls -- not supported by any action here yet. Nothing declared below
|
|
59
|
+
# needs one; revisit if a real deployment does.
|
|
60
|
+
|
|
61
|
+
# --- API precautions (API/Precautions) ------------------------------------------------
|
|
62
|
+
# Live-checked against a real paper Gateway 10.50 account, 2026-09-06 -- NOT identical
|
|
63
|
+
# to IBC's own ConfigureApiPrecautionsTask.java list (see labels.py's own docstring for
|
|
64
|
+
# the full finding). Two real corrections from that live check:
|
|
65
|
+
# - api_precautions_bypass_us_stocks_market_data_in_shares is NOT a real control on
|
|
66
|
+
# this account/build at all (raises ElementNotFoundError if declared) -- dropped
|
|
67
|
+
# here. Likely account/market-data-entitlement gated, not a stale label; a
|
|
68
|
+
# different account could plausibly have it.
|
|
69
|
+
# - api_precautions_bypass_route_marketable_to_bbo exists on the real panel but isn't
|
|
70
|
+
# in IBC's own list -- added below, set to false (disabled) by default per the
|
|
71
|
+
# user's own instruction, everything else enabled.
|
|
72
|
+
# All 8 entries below are live-confirmed: each one actually landed (selected=True/False
|
|
73
|
+
# read back via a real dump) before Apply was clicked to commit them.
|
|
74
|
+
|
|
75
|
+
# [[settings]]
|
|
76
|
+
# tree_path = "API/Precautions"
|
|
77
|
+
# action = "toggle"
|
|
78
|
+
# label_ref = "api_precautions_bypass_us_stocks_market_data_in_shares"
|
|
79
|
+
# value = true
|
|
80
|
+
# Commented out, not dropped from labels.json: confirmed live 2026-09-06 that this
|
|
81
|
+
# control does not exist on this paper account/Gateway 10.50 build at all.
|
|
82
|
+
|
|
83
|
+
[[settings]]
|
|
84
|
+
tree_path = "API/Precautions"
|
|
85
|
+
action = "toggle"
|
|
86
|
+
label_ref = "api_precautions_bypass_order_precautions"
|
|
87
|
+
value = true
|
|
88
|
+
|
|
89
|
+
[[settings]]
|
|
90
|
+
tree_path = "API/Precautions"
|
|
91
|
+
action = "toggle"
|
|
92
|
+
label_ref = "api_precautions_bypass_bond_warning"
|
|
93
|
+
value = true
|
|
94
|
+
|
|
95
|
+
[[settings]]
|
|
96
|
+
tree_path = "API/Precautions"
|
|
97
|
+
action = "toggle"
|
|
98
|
+
label_ref = "api_precautions_bypass_negative_yield_to_worst"
|
|
99
|
+
value = true
|
|
100
|
+
|
|
101
|
+
[[settings]]
|
|
102
|
+
tree_path = "API/Precautions"
|
|
103
|
+
action = "toggle"
|
|
104
|
+
label_ref = "api_precautions_bypass_called_bond_warning"
|
|
105
|
+
value = true
|
|
106
|
+
|
|
107
|
+
[[settings]]
|
|
108
|
+
tree_path = "API/Precautions"
|
|
109
|
+
action = "toggle"
|
|
110
|
+
label_ref = "api_precautions_bypass_same_action_pair_trade"
|
|
111
|
+
value = true
|
|
112
|
+
|
|
113
|
+
[[settings]]
|
|
114
|
+
tree_path = "API/Precautions"
|
|
115
|
+
action = "toggle"
|
|
116
|
+
label_ref = "api_precautions_bypass_price_based_volatility"
|
|
117
|
+
value = true
|
|
118
|
+
|
|
119
|
+
[[settings]]
|
|
120
|
+
tree_path = "API/Precautions"
|
|
121
|
+
action = "toggle"
|
|
122
|
+
label_ref = "api_precautions_bypass_redirect_order_warning"
|
|
123
|
+
value = true
|
|
124
|
+
|
|
125
|
+
[[settings]]
|
|
126
|
+
tree_path = "API/Precautions"
|
|
127
|
+
action = "toggle"
|
|
128
|
+
label_ref = "api_precautions_bypass_no_overfill_protection"
|
|
129
|
+
value = true
|
|
130
|
+
|
|
131
|
+
[[settings]]
|
|
132
|
+
tree_path = "API/Precautions"
|
|
133
|
+
action = "toggle"
|
|
134
|
+
label_ref = "api_precautions_bypass_route_marketable_to_bbo"
|
|
135
|
+
value = false
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"login": {
|
|
3
|
+
"gateway_titles": ["IBKR Gateway", "IB Gateway", "Interactive Brokers Gateway"],
|
|
4
|
+
"tws_titles": ["New Login", "Login"],
|
|
5
|
+
"login_buttons": ["Log In", "Paper Log In", "Login"],
|
|
6
|
+
"api_type_ib_api": "IB API",
|
|
7
|
+
"api_type_fix": "FIX CTCI",
|
|
8
|
+
"trading_mode_live": "Live Trading",
|
|
9
|
+
"trading_mode_paper": "Paper Trading",
|
|
10
|
+
"username_field": "Username",
|
|
11
|
+
"password_field": "Password",
|
|
12
|
+
"starting_application_title": "Starting application...",
|
|
13
|
+
"tws_main_window_menu_item": "File/Lock Application"
|
|
14
|
+
},
|
|
15
|
+
"mfa": {
|
|
16
|
+
"title": "Second Factor Authentication"
|
|
17
|
+
},
|
|
18
|
+
"existing_session": {
|
|
19
|
+
"title": "Existing session detected",
|
|
20
|
+
"continue_buttons": ["OK", "Continue Login", "Reconnect This Session"],
|
|
21
|
+
"cancel_buttons": ["Cancel", "Exit Application"]
|
|
22
|
+
},
|
|
23
|
+
"accept_incoming_connection": {
|
|
24
|
+
"title": "Accept incoming connection",
|
|
25
|
+
"accept_buttons": ["OK", "Yes"],
|
|
26
|
+
"reject_buttons": ["No"]
|
|
27
|
+
},
|
|
28
|
+
"login_failed": {
|
|
29
|
+
"title": "Login failed",
|
|
30
|
+
"dismiss_button": "OK"
|
|
31
|
+
},
|
|
32
|
+
"dismiss_rules": [
|
|
33
|
+
{
|
|
34
|
+
"name": "non_brokerage_account",
|
|
35
|
+
"match_text": "This is not a brokerage account",
|
|
36
|
+
"click": "I understand and accept"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"name": "auto_restart_confirmation",
|
|
40
|
+
"match_text": "trading platform restart automatically",
|
|
41
|
+
"click": "OK"
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
"too_many_failed_login_attempts": {
|
|
45
|
+
"message_prefix": "Too many failed login attempts",
|
|
46
|
+
"dismiss_button": "OK"
|
|
47
|
+
},
|
|
48
|
+
"shutdown": {
|
|
49
|
+
"gateway_menu_path": "File/Close",
|
|
50
|
+
"tws_menu_path": "File/Exit"
|
|
51
|
+
},
|
|
52
|
+
"settings": {
|
|
53
|
+
"gateway_menu_path": "Configure/Settings",
|
|
54
|
+
"tws_menu_path": "File/Global Configuration...",
|
|
55
|
+
"tws_menu_path_classic": "Edit/Global Configuration...",
|
|
56
|
+
"dialog_title_marker": "Configuration",
|
|
57
|
+
"apply_button": "Apply",
|
|
58
|
+
"cancel_button": "Cancel",
|
|
59
|
+
"ok_button": "OK",
|
|
60
|
+
"splash_title_marker": "Starting application...",
|
|
61
|
+
"controls": {
|
|
62
|
+
"read_only_api": "Read-Only API",
|
|
63
|
+
"api_precautions_bypass_order_precautions": "Bypass Order Precautions for API Orders",
|
|
64
|
+
"api_precautions_bypass_bond_warning": "Bypass Bond warning for API Orders.",
|
|
65
|
+
"api_precautions_bypass_negative_yield_to_worst": "Bypass negative yield to worst confirmation for API Orders.",
|
|
66
|
+
"api_precautions_bypass_called_bond_warning": "Bypass Called Bond warning for API Orders",
|
|
67
|
+
"api_precautions_bypass_same_action_pair_trade": "Bypass \"same action pair trade\" warning for API orders.",
|
|
68
|
+
"api_precautions_bypass_price_based_volatility": "Bypass price-based volatility risk warning for API Orders.",
|
|
69
|
+
"api_precautions_bypass_us_stocks_market_data_in_shares": "Bypass US Stocks market data in shares warning for API Orders",
|
|
70
|
+
"api_precautions_bypass_redirect_order_warning": "Bypass Redirect Order warning for Stock API Orders",
|
|
71
|
+
"api_precautions_bypass_no_overfill_protection": "Bypass No Overfill Protection precaution for destinations where implied natively.",
|
|
72
|
+
"api_precautions_bypass_route_marketable_to_bbo": "Bypass Route Marketable to BBO warning for API orders.",
|
|
73
|
+
"auto_logoff_time_label": "Set Auto Log Off Time (HH:MM)",
|
|
74
|
+
"auto_restart_time_label": "Set Auto Restart Time (HH:MM)",
|
|
75
|
+
"auto_logoff_radio": "Auto logoff",
|
|
76
|
+
"auto_restart_radio": "Auto restart",
|
|
77
|
+
"am_radio": "AM",
|
|
78
|
+
"pm_radio": "PM"
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_readme": [
|
|
3
|
+
"Example override for {config_dir}/labels.json -- ibcontroller init/run copy this",
|
|
4
|
+
"file as labels.json.example, NOT as labels.json: labels.load_labels() applies",
|
|
5
|
+
"{config_dir}/labels.json unconditionally the moment it exists, so activating an",
|
|
6
|
+
"override is a deliberate rename, never automatic.",
|
|
7
|
+
"",
|
|
8
|
+
"labels.py's own _deep_merge overrides one field at a time -- you only need to name",
|
|
9
|
+
"the label that actually changed, not repeat every sibling field or every other",
|
|
10
|
+
"domain untouched. This file is trimmed down to two illustrative examples, not a",
|
|
11
|
+
"full copy of the bundled data/labels.json:",
|
|
12
|
+
"",
|
|
13
|
+
" - login.gateway_titles: shows how to add a title variant IBKR renamed/localized",
|
|
14
|
+
" (the override REPLACES the whole list, so include every title you still want,",
|
|
15
|
+
" not just the new one).",
|
|
16
|
+
" - dismiss_rules: this one is a list, not a nested object, so an override replaces",
|
|
17
|
+
" the whole list rather than merging entry by entry -- copy the bundled entries",
|
|
18
|
+
" here (as done below) before adding your own, or you'll lose the built-in ones.",
|
|
19
|
+
"",
|
|
20
|
+
"Remove this _readme key before using the file for real -- it is not a real Labels",
|
|
21
|
+
"field and is harmless where cattrs ignores unknown keys, but it's not meant to ship.",
|
|
22
|
+
"See labels.py's own module docstring and DismissRule's docstring for the full",
|
|
23
|
+
"mechanism."
|
|
24
|
+
],
|
|
25
|
+
"login": {
|
|
26
|
+
"gateway_titles": [
|
|
27
|
+
"IBKR Gateway",
|
|
28
|
+
"IB Gateway",
|
|
29
|
+
"Interactive Brokers Gateway"
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
"dismiss_rules": [
|
|
33
|
+
{
|
|
34
|
+
"name": "non_brokerage_account",
|
|
35
|
+
"match_text": "This is not a brokerage account",
|
|
36
|
+
"click": "I understand and accept"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"name": "auto_restart_confirmation",
|
|
40
|
+
"match_text": "trading platform restart automatically",
|
|
41
|
+
"click": "OK"
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Built-in replacement for poking the agent socket by hand (`socat`/`nc`)
|
|
2
|
+
during layer-by-layer development: `watch_for_diagnostics` runs for the
|
|
3
|
+
agent's whole lifetime, dumping component structure for windows matching
|
|
4
|
+
`Config.diagnostic_scope`/`diagnostic_when` and handing the formatted text
|
|
5
|
+
to a `Sink`.
|
|
6
|
+
|
|
7
|
+
Deliberately split into capture (`format_dump`, built on the existing
|
|
8
|
+
`actions.dump`), trigger (`watch_for_diagnostics`, modeled directly on
|
|
9
|
+
`recognisers.watch_for_unprompted_windows`), and sink (a plain callable) --
|
|
10
|
+
the log sink below (`log_sink`) is the only one shipped now, but an
|
|
11
|
+
interactive `diagnose` CLI or a control-API handler can pass a different
|
|
12
|
+
sink later without touching capture or trigger logic.
|
|
13
|
+
|
|
14
|
+
Off by default (`Config.diagnostic_when` defaults to `NEVER`)."""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import asyncio
|
|
19
|
+
import logging
|
|
20
|
+
from collections.abc import Callable
|
|
21
|
+
|
|
22
|
+
from ibcontroller.actions import dump
|
|
23
|
+
from ibcontroller.agent_client import Component, WindowEvent, WindowGoneError
|
|
24
|
+
from ibcontroller.config import DiagnosticScope, DiagnosticWhen
|
|
25
|
+
from ibcontroller.dispatch import Dispatcher
|
|
26
|
+
from ibcontroller.recognisers import RecognizerRegistry
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
Sink = Callable[[WindowEvent, str], None]
|
|
31
|
+
"""Receives the triggering event and the formatted dump text (or a
|
|
32
|
+
one-line fallback, see `watch_for_diagnostics`). `log_sink` is the only
|
|
33
|
+
implementation shipped now."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def log_sink(event: WindowEvent, text: str) -> None:
|
|
37
|
+
logger.info("diagnostic dump (%s):\n%s", event.kind, text)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def format_dump(event: WindowEvent, components: list[Component]) -> str:
|
|
41
|
+
"""One line per component: class, name/accessible_name/text (when
|
|
42
|
+
present -- `dump` already omits `text` for credential fields, nothing
|
|
43
|
+
extra to redact here), enabled/visible."""
|
|
44
|
+
lines = [
|
|
45
|
+
f"{event.kind} {event.window.class_} {event.window.title!r} "
|
|
46
|
+
f"window_id={event.window.window_id}"
|
|
47
|
+
]
|
|
48
|
+
for component in components:
|
|
49
|
+
parts = [component.class_]
|
|
50
|
+
if component.name:
|
|
51
|
+
parts.append(f"name={component.name!r}")
|
|
52
|
+
if component.accessible_name:
|
|
53
|
+
parts.append(f"accessible_name={component.accessible_name!r}")
|
|
54
|
+
if component.text:
|
|
55
|
+
parts.append(f"text={component.text!r}")
|
|
56
|
+
parts.append(f"enabled={component.enabled}")
|
|
57
|
+
parts.append(f"visible={component.visible}")
|
|
58
|
+
lines.append(" " + " ".join(parts))
|
|
59
|
+
return "\n".join(lines)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
async def watch_for_diagnostics(
|
|
63
|
+
registry: RecognizerRegistry,
|
|
64
|
+
dispatcher: Dispatcher,
|
|
65
|
+
scope: DiagnosticScope,
|
|
66
|
+
when: DiagnosticWhen,
|
|
67
|
+
sink: Sink = log_sink,
|
|
68
|
+
) -> None:
|
|
69
|
+
"""Runs for the agent's whole lifetime (meant to be started via
|
|
70
|
+
`asyncio.ensure_future`, like `watch_for_unprompted_windows`; never
|
|
71
|
+
returns on its own). A no-op if `when` is `NEVER`, so scheduling this
|
|
72
|
+
unconditionally is safe.
|
|
73
|
+
|
|
74
|
+
`scope` is checked against `registry.recognises()` -- the same
|
|
75
|
+
recognisers `watch_for_unprompted_windows` dispatches through, not a
|
|
76
|
+
separate classification. A `window_closed` event can't be dumped (the
|
|
77
|
+
window is already gone by the time this task reacts to the push over
|
|
78
|
+
the socket -- no in-process synchronous hook the way IBC's own
|
|
79
|
+
same-JVM listener has); `sink` still gets called, with a one-line
|
|
80
|
+
fallback noting the window's identity instead of its structure."""
|
|
81
|
+
if when is DiagnosticWhen.NEVER:
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
wanted_kinds = (
|
|
85
|
+
{"window_opened"}
|
|
86
|
+
if when is DiagnosticWhen.OPEN
|
|
87
|
+
else {"window_opened", "window_closed"}
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
queue: asyncio.Queue[WindowEvent] = asyncio.Queue()
|
|
91
|
+
|
|
92
|
+
def on_event(event: WindowEvent) -> None:
|
|
93
|
+
if event.kind in wanted_kinds:
|
|
94
|
+
queue.put_nowait(event)
|
|
95
|
+
|
|
96
|
+
dispatcher.window_events.connect(on_event)
|
|
97
|
+
try:
|
|
98
|
+
while True:
|
|
99
|
+
event = await queue.get()
|
|
100
|
+
try:
|
|
101
|
+
components = await dump(
|
|
102
|
+
dispatcher,
|
|
103
|
+
window=event.window.title,
|
|
104
|
+
window_id=event.window.window_id,
|
|
105
|
+
)
|
|
106
|
+
except WindowGoneError:
|
|
107
|
+
sink(
|
|
108
|
+
event,
|
|
109
|
+
f"{event.kind} {event.window.class_} {event.window.title!r} "
|
|
110
|
+
"(window already gone, no structure)",
|
|
111
|
+
)
|
|
112
|
+
continue
|
|
113
|
+
if scope is not DiagnosticScope.ALL:
|
|
114
|
+
known = registry.recognises(event, components)
|
|
115
|
+
if (scope is DiagnosticScope.KNOWN) != known:
|
|
116
|
+
continue
|
|
117
|
+
sink(event, format_dump(event, components))
|
|
118
|
+
finally:
|
|
119
|
+
dispatcher.window_events.disconnect(on_event)
|