@@ -9,6 +9,7 @@ test -x "$script_file" || true
99grep -Fq ' container_name="${IB_GATEWAY_CONTAINER_NAME:-ib-gateway}"' " $script_file "
1010grep -Fq ' ready_timeout_seconds="${IB_GATEWAY_READY_TIMEOUT_SECONDS:-240}"' " $script_file "
1111grep -Fq ' ready_stability_seconds="${IB_GATEWAY_READY_STABILITY_SECONDS:-35}"' " $script_file "
12+ grep -Fq ' order_access_timeout_seconds="${IB_GATEWAY_ORDER_ACCESS_TIMEOUT_SECONDS:-4}"' " $script_file "
1213grep -Fq ' gateway_port=4002' " $script_file "
1314grep -Fq ' gateway_port=4001' " $script_file "
1415grep -Fq " docker inspect --format '{{.State.Running}}'" " $script_file "
@@ -20,8 +21,134 @@ grep -Fq 'confirm_stable_ready()' "$script_file"
2021grep -Fq ' confirming stability' " $script_file "
2122grep -Fq ' from ib_insync import IB' " $script_file "
2223grep -Fq ' readonly=True' " $script_file "
24+ grep -Fq ' read_only_api = os.environ.get("READ_ONLY_API", "").strip().lower()' " $script_file "
25+ grep -Fq ' require_order_access = read_only_api == "no"' " $script_file "
26+ grep -Fq ' ib.RaiseRequestErrors = True' " $script_file "
27+ grep -Fq ' ib.RequestTimeout = order_access_timeout_seconds' " $script_file "
28+ grep -Fq ' request_order_data("open orders request", ib.reqOpenOrders)' " $script_file "
29+ if grep -Fq ' ib.reqCompletedOrders(' " $script_file " ; then
30+ echo " Gateway readiness must not depend on completed-order history" >&2
31+ exit 1
32+ fi
33+ grep -Fq ' if client_id <= 0:' " $script_file "
34+ grep -Fq ' IB API writable healthcheck ready' " $script_file "
35+ grep -Fq ' account_count={len(accounts)}' " $script_file "
36+ if grep -Fq " accounts={','.join(accounts)}" " $script_file " ; then
37+ echo " Gateway healthcheck must not log account identifiers" >&2
38+ exit 1
39+ fi
2340grep -Fq ' IB API ib_insync healthcheck ready' " $script_file "
2441grep -Fq ' b"API\0" + struct.pack(">I", len(b"v157..176")) + b"v157..176"' " $script_file "
2542grep -Fq ' has_next_valid_id and has_managed_accounts' " $script_file "
2643grep -Fq ' IB API handshake readiness' " $script_file "
2744grep -Fq ' docker logs --tail 120 "${container_name}"' " $script_file "
45+
46+ tmp_dir=" $( mktemp -d) "
47+ trap ' rm -rf "$tmp_dir"' EXIT
48+ awk '
49+ /^[[:space:]]*python3 <<' \' ' PY' \' ' $/ { capture = 1; next }
50+ capture && /^PY$/ { exit }
51+ capture { print }
52+ ' " $script_file " > " $tmp_dir /check_api.py"
53+ cat > " $tmp_dir /ib_insync.py" << 'PY '
54+ import os
55+
56+
57+ class Event:
58+ def __init__(self):
59+ self.handlers = []
60+
61+ def __iadd__(self, handler):
62+ self.handlers.append(handler)
63+ return self
64+
65+ def __isub__(self, handler):
66+ self.handlers.remove(handler)
67+ return self
68+
69+ def emit(self, *args):
70+ for handler in tuple(self.handlers):
71+ handler(*args)
72+
73+
74+ class Client:
75+ @staticmethod
76+ def serverVersion():
77+ return 176
78+
79+
80+ class IB:
81+ RaiseRequestErrors = False
82+ RequestTimeout = 0
83+
84+ def __init__(self):
85+ self.client = Client()
86+ self.errorEvent = Event()
87+ self.connected = False
88+
89+ def connect(self, *_args, **kwargs):
90+ assert kwargs["clientId"] > 0
91+ assert kwargs["readonly"] is True
92+ self.connected = True
93+
94+ @staticmethod
95+ def managedAccounts():
96+ return ["U_TEST"]
97+
98+ def reqOpenOrders(self):
99+ assert self.RaiseRequestErrors is True
100+ assert self.RequestTimeout == 1
101+ if os.environ.get("FAKE_IB_READ_ONLY") == "1":
102+ self.errorEvent.emit(-1, 321, "API is in Read-Only mode", None)
103+ raise TimeoutError("open orders timed out")
104+ if os.environ.get("FAKE_IB_UNRELATED_321") == "1":
105+ self.errorEvent.emit(-1, 321, "Generic validation error", None)
106+ if os.environ.get("FAKE_IB_FAIL_ON_ORDER_ACCESS") == "1":
107+ raise AssertionError("order access probe must not run")
108+ return []
109+
110+ def reqCompletedOrders(self, *, apiOnly):
111+ assert apiOnly is True
112+ return []
113+
114+ def isConnected(self):
115+ return self.connected
116+
117+ def disconnect(self):
118+ self.connected = False
119+ PY
120+
121+ healthcheck_env=(
122+ IB_GATEWAY_HEALTHCHECK_PORT=4001
123+ IB_GATEWAY_HEALTHCHECK_CLIENT_ID=9001
124+ IB_GATEWAY_HEALTHCHECK_TIMEOUT_SECONDS=2
125+ IB_GATEWAY_ORDER_ACCESS_TIMEOUT_SECONDS=1
126+ PYTHONPATH=" $tmp_dir "
127+ )
128+
129+ env " ${healthcheck_env[@]} " READ_ONLY_API=no \
130+ python3 " $tmp_dir /check_api.py" > " $tmp_dir /writable.out"
131+ grep -Fq ' IB API writable healthcheck ready' " $tmp_dir /writable.out"
132+ grep -Fq ' account_count=1' " $tmp_dir /writable.out"
133+
134+ if env " ${healthcheck_env[@]} " READ_ONLY_API=no FAKE_IB_READ_ONLY=1 \
135+ python3 " $tmp_dir /check_api.py" > " $tmp_dir /read-only.out" 2>&1 ; then
136+ echo " Read-Only Gateway unexpectedly passed writable healthcheck" >&2
137+ exit 1
138+ fi
139+ grep -Fq ' Gateway is in Read-Only mode' " $tmp_dir /read-only.out"
140+
141+ env " ${healthcheck_env[@]} " READ_ONLY_API=no FAKE_IB_UNRELATED_321=1 \
142+ python3 " $tmp_dir /check_api.py" > " $tmp_dir /unrelated-321.out"
143+ grep -Fq ' IB API writable healthcheck ready' " $tmp_dir /unrelated-321.out"
144+
145+ env " ${healthcheck_env[@]} " READ_ONLY_API=yes FAKE_IB_FAIL_ON_ORDER_ACCESS=1 \
146+ python3 " $tmp_dir /check_api.py" > " $tmp_dir /read-only-expected.out"
147+ grep -Fq ' writable=false' " $tmp_dir /read-only-expected.out"
148+
149+ if env " ${healthcheck_env[@]} " IB_GATEWAY_HEALTHCHECK_CLIENT_ID=0 READ_ONLY_API=no \
150+ python3 " $tmp_dir /check_api.py" > " $tmp_dir /client-zero.out" 2>&1 ; then
151+ echo " clientId=0 unexpectedly passed Gateway healthcheck" >&2
152+ exit 1
153+ fi
154+ grep -Fq ' client ID must be greater than zero' " $tmp_dir /client-zero.out"
0 commit comments