cfquant 0.2.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.
Files changed (90) hide show
  1. LTtx/__init__.py +2 -0
  2. LTtx/tx/Config.txt +8 -0
  3. LTtx/tx/LTtx_server.py +1177 -0
  4. LTtx/tx/__init__.py +5 -0
  5. LTtx/tx/tx.py +1935 -0
  6. cfquant/__init__.py +26 -0
  7. cfquant/account_routing.py +119 -0
  8. cfquant/channels.py +76 -0
  9. cfquant/cli.py +266 -0
  10. cfquant/client.py +365 -0
  11. cfquant/config.py +86 -0
  12. cfquant/hub.py +304 -0
  13. cfquant/logging_i18n.py +348 -0
  14. cfquant/normal_bridge.py +676 -0
  15. cfquant/pipe_bridge.py +249 -0
  16. cfquant/pipe_client.py +223 -0
  17. cfquant/pipe_hub.py +602 -0
  18. cfquant/pipe_transport.py +493 -0
  19. cfquant/protocol.py +238 -0
  20. cfquant/qmt_bridge.py +1081 -0
  21. cfquant/runtime_report.py +334 -0
  22. cfquant/tx.py +1898 -0
  23. cfquant/tx_trade_bridge.py +1905 -0
  24. cfquant/version.py +5 -0
  25. cfquant/xtconstant.py +262 -0
  26. cfquant/xtdata.py +462 -0
  27. cfquant/xttrader.py +955 -0
  28. cfquant/xttype.py +586 -0
  29. cfquant-0.2.0.dist-info/LICENSE +21 -0
  30. cfquant-0.2.0.dist-info/METADATA +144 -0
  31. cfquant-0.2.0.dist-info/RECORD +90 -0
  32. cfquant-0.2.0.dist-info/WHEEL +5 -0
  33. cfquant-0.2.0.dist-info/entry_points.txt +4 -0
  34. cfquant-0.2.0.dist-info/top_level.txt +6 -0
  35. cfquant_pipe_hub.py +45 -0
  36. cfquant_web_server.py +12137 -0
  37. qmt_scripts/CFQUANT.py +507 -0
  38. qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +830 -0
  39. qmt_scripts/CFQUANT_LITE.py +4928 -0
  40. qmt_scripts/CFQUANT_TRADE_LOWLAT.py +382 -0
  41. qmt_scripts/__init__.py +1 -0
  42. qmt_scripts//321/205/320/240/320/234/321/210/342/224/244/320/266/321/205/320/237/342/225/226/321/207/320/233/320/274/321/207/320/273/320/233/321/205/342/225/225/320/222/321/205/320/254/342/225/221/CFQUANT_CTYPE_ALL_LOWLAT_SH.py +866 -0
  43. qmt_scripts//321/205/320/240/320/234/321/210/342/224/244/320/266/321/205/320/237/342/225/226/321/207/320/233/320/274/321/207/320/273/320/233/321/205/342/225/225/320/222/321/205/320/254/342/225/221/CFQUANT_CTYPE_ALL_LOWLAT_SZ.py +866 -0
  44. qmt_scripts//321/205/320/240/320/234/321/210/342/224/244/320/266/321/205/320/237/342/225/226/321/207/320/233/320/274/321/207/320/273/320/233/321/205/342/225/225/320/222/321/205/320/254/342/225/221/CFQUANT_LITE_SH.py +5020 -0
  45. qmt_scripts//321/205/320/240/320/234/321/210/342/224/244/320/266/321/205/320/237/342/225/226/321/207/320/233/320/274/321/207/320/273/320/233/321/205/342/225/225/320/222/321/205/320/254/342/225/221/CFQUANT_LITE_SZ.py +5020 -0
  46. qmt_scripts//321/205/320/240/320/234/321/210/342/224/244/320/266/321/205/320/237/342/225/226/321/207/320/233/320/274/321/207/320/273/320/233/321/205/342/225/225/320/222/321/205/320/254/342/225/221/CFQUANT_TRADE_LOWLAT_SH.py +436 -0
  47. qmt_scripts//321/205/320/240/320/234/321/210/342/224/244/320/266/321/205/320/237/342/225/226/321/207/320/233/320/274/321/207/320/273/320/233/321/205/342/225/225/320/222/321/205/320/254/342/225/221/CFQUANT_TRADE_LOWLAT_SZ.py +436 -0
  48. qmt_scripts//321/205/320/240/320/234/321/210/342/224/244/320/266/321/205/320/237/342/225/226/321/207/320/233/320/274/321/207/320/273/320/233/321/205/342/225/225/320/222/321/205/320/254/342/225/221/readme.md +95 -0
  49. web_dashboard/__init__.py +1 -0
  50. web_dashboard/app.js +9847 -0
  51. web_dashboard/avatars/amber-pulse.svg +15 -0
  52. web_dashboard/avatars/copper-grid.svg +15 -0
  53. web_dashboard/avatars/market-blue.svg +15 -0
  54. web_dashboard/avatars/rose-circuit.svg +15 -0
  55. web_dashboard/avatars/signal-green.svg +15 -0
  56. web_dashboard/avatars/slate-wave.svg +15 -0
  57. web_dashboard/avatars/teal-orbit.svg +16 -0
  58. web_dashboard/avatars/violet-node.svg +15 -0
  59. web_dashboard/favicon.svg +5 -0
  60. web_dashboard/image/qmt_deploy/image-10.png +0 -0
  61. web_dashboard/image/qmt_deploy/image-11.png +0 -0
  62. web_dashboard/image/qmt_deploy/image-12.png +0 -0
  63. web_dashboard/image/qmt_deploy/image-13.png +0 -0
  64. web_dashboard/image/qmt_deploy/image-14.png +0 -0
  65. web_dashboard/image/qmt_deploy/image-15.png +0 -0
  66. web_dashboard/image/qmt_deploy/image-16.png +0 -0
  67. web_dashboard/image/qmt_deploy/image-17.png +0 -0
  68. web_dashboard/image/qmt_deploy/image-18.png +0 -0
  69. web_dashboard/image/qmt_deploy/image-19.png +0 -0
  70. web_dashboard/image/qmt_deploy/image-20.png +0 -0
  71. web_dashboard/image/qmt_deploy/image-21.png +0 -0
  72. web_dashboard/image/qmt_deploy/image-4.png +0 -0
  73. web_dashboard/image/qmt_deploy/image-5.png +0 -0
  74. web_dashboard/image/qmt_deploy/image-6.png +0 -0
  75. web_dashboard/image/qmt_deploy/image-7.png +0 -0
  76. web_dashboard/image/qmt_deploy/image-8.png +0 -0
  77. web_dashboard/image/qmt_deploy/image-9.png +0 -0
  78. web_dashboard/image//321/211/320/220/320/252/321/207/320/244/320/270/321/206/320/270/320/261/321/205/342/225/235/320/237/321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/image-1.png +0 -0
  79. web_dashboard/image//321/211/320/220/320/252/321/207/320/244/320/270/321/206/320/270/320/261/321/205/342/225/235/320/237/321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/image-2.png +0 -0
  80. web_dashboard/image//321/211/320/220/320/252/321/207/320/244/320/270/321/206/320/270/320/261/321/205/342/225/235/320/237/321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/image-3.png +0 -0
  81. web_dashboard/image//321/211/320/220/320/252/321/207/320/244/320/270/321/206/320/270/320/261/321/205/342/225/235/320/237/321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/image.png +0 -0
  82. web_dashboard/image//321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/1783479325179.png +0 -0
  83. web_dashboard/image//321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/1783479745262.png +0 -0
  84. web_dashboard/image//321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/1783480178700.png +0 -0
  85. web_dashboard/image//321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/1783480428759.png +0 -0
  86. web_dashboard/image//321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/1783489807217.png +0 -0
  87. web_dashboard/image//321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/1783490503491.png +0 -0
  88. web_dashboard/index.html +2317 -0
  89. web_dashboard/styles.css +7817 -0
  90. web_dashboard/vendor/mermaid.min.js +2029 -0
cfquant/pipe_bridge.py ADDED
@@ -0,0 +1,249 @@
1
+ # -*- coding: utf-8 -*-
2
+ import queue
3
+ import threading
4
+ import time
5
+
6
+ from .normal_bridge import NormalQmtBridge
7
+ from .pipe_transport import DEFAULT_PIPE_NAME, PipeTxClient
8
+ from .tx_trade_bridge import TxTradeBridge
9
+
10
+
11
+ class PipeNormalQmtBridge(NormalQmtBridge):
12
+ def __init__(
13
+ self,
14
+ context,
15
+ pipe_name=None,
16
+ request_channel="cfquant.normal.request",
17
+ request_channels=None,
18
+ callback_event_channel="cfquant.callback.event",
19
+ bridge_id="default",
20
+ account_id="",
21
+ show=True,
22
+ globals_dict=None,
23
+ schedule_timer=True,
24
+ pump_max_count=20,
25
+ pump_max_ms=0,
26
+ dispatch_on_qmt_thread=False,
27
+ connect_timeout_ms=3000,
28
+ ):
29
+ super(PipeNormalQmtBridge, self).__init__(
30
+ context,
31
+ ip="127.0.0.1",
32
+ port=0,
33
+ token="",
34
+ request_channel=request_channel,
35
+ callback_event_channel=callback_event_channel,
36
+ bridge_id=bridge_id,
37
+ account_id=account_id,
38
+ show=show,
39
+ globals_dict=globals_dict,
40
+ schedule_timer=schedule_timer,
41
+ pump_max_count=pump_max_count,
42
+ pump_max_ms=pump_max_ms,
43
+ dispatch_on_qmt_thread=dispatch_on_qmt_thread,
44
+ )
45
+ self.pipe_name = pipe_name or DEFAULT_PIPE_NAME
46
+ self.request_channels = request_channels or [request_channel]
47
+ self.connect_timeout_ms = int(connect_timeout_ms)
48
+
49
+ def start(self):
50
+ if self.running:
51
+ return self
52
+ self.running = True
53
+ if not self.started_at:
54
+ self.started_at = time.time()
55
+ self.tx = PipeTxClient(
56
+ pipe_name=self.pipe_name,
57
+ request_channel=self.request_channel,
58
+ request_channels=self.request_channels,
59
+ bridge_id=self.bridge_id,
60
+ endpoint_name="normal",
61
+ show=self.show,
62
+ connect_timeout_ms=self.connect_timeout_ms,
63
+ ).start()
64
+ self.tx.start_txg(self.request_channel)
65
+ self.recv_thread = threading.Thread(target=self._recv_loop)
66
+ self.recv_thread.daemon = True
67
+ self.recv_thread.start()
68
+ self._log(
69
+ "pipe normal bridge started pipe=%s request_channel=%s"
70
+ % (self.pipe_name, self.request_channel)
71
+ )
72
+ self._publish_runtime_report("start")
73
+ return self
74
+
75
+ def close(self):
76
+ self.running = False
77
+ self.worker_event.set()
78
+ if self.context is not None and self.schedule_key:
79
+ try:
80
+ self.context.cancel_schedule_run(self.schedule_key)
81
+ except Exception:
82
+ pass
83
+ tx = self.tx
84
+ self.tx = None
85
+ if tx is not None:
86
+ try:
87
+ tx.close()
88
+ except Exception:
89
+ pass
90
+ self._log("pipe normal bridge stopped")
91
+
92
+ def _status_extra(self):
93
+ data = super(PipeNormalQmtBridge, self)._status_extra()
94
+ data.update({
95
+ "transport": "pipe",
96
+ "pipe_name": self.pipe_name,
97
+ "pipe_request_channels": list(self.request_channels),
98
+ "pipe_connected": self.tx is not None and self.tx._get_conn() is not None,
99
+ })
100
+ return data
101
+
102
+
103
+ class PipeTradeBridge(TxTradeBridge):
104
+ def __init__(
105
+ self,
106
+ context,
107
+ pipe_name=None,
108
+ request_channel="cfquant.trade.request",
109
+ bridge_id="default",
110
+ account_id="",
111
+ show=True,
112
+ globals_dict=None,
113
+ connect_timeout_ms=3000,
114
+ ):
115
+ super(PipeTradeBridge, self).__init__(
116
+ context,
117
+ ip="127.0.0.1",
118
+ port=0,
119
+ token="",
120
+ request_channel=request_channel,
121
+ bridge_id=bridge_id,
122
+ account_id=account_id,
123
+ show=show,
124
+ globals_dict=globals_dict,
125
+ )
126
+ self.pipe_name = pipe_name or DEFAULT_PIPE_NAME
127
+ self.connect_timeout_ms = int(connect_timeout_ms)
128
+
129
+ def start(self):
130
+ if self.running:
131
+ return self
132
+ self.running = True
133
+ if not self.started_at:
134
+ self.started_at = time.time()
135
+ self.tx = PipeTxClient(
136
+ pipe_name=self.pipe_name,
137
+ request_channel=self.request_channel,
138
+ bridge_id=self.bridge_id,
139
+ endpoint_name="trade",
140
+ show=self.show,
141
+ connect_timeout_ms=self.connect_timeout_ms,
142
+ ).start()
143
+ self.tx.start_txg(self.request_channel)
144
+ self._log(
145
+ "pipe trade bridge started pipe=%s request_channel=%s"
146
+ % (self.pipe_name, self.request_channel)
147
+ )
148
+ self._publish_runtime_report("start")
149
+ return self
150
+
151
+ def close(self):
152
+ self.running = False
153
+ tx = self.tx
154
+ self.tx = None
155
+ if tx is not None:
156
+ try:
157
+ tx.close()
158
+ except Exception:
159
+ pass
160
+ self._log("pipe trade bridge stopped")
161
+
162
+ def poll(self, max_messages=100, timeout=0):
163
+ self.start()
164
+ count = 0
165
+ while self.running and count < max_messages:
166
+ try:
167
+ raw = self.tx.Q.get(timeout=timeout if count == 0 else 0)
168
+ except queue.Empty:
169
+ break
170
+ except Exception:
171
+ break
172
+ if raw is None:
173
+ break
174
+ self._handle_raw(raw)
175
+ count += 1
176
+ return count
177
+
178
+ def _status_extra(self):
179
+ return {
180
+ "transport": "pipe",
181
+ "pipe_name": self.pipe_name,
182
+ "pipe_connected": self.tx is not None and self.tx._get_conn() is not None,
183
+ }
184
+
185
+
186
+ def start_pipe_normal_bridge(
187
+ context,
188
+ pipe_name=None,
189
+ request_channel="cfquant.normal.request",
190
+ request_channels=None,
191
+ callback_event_channel="cfquant.callback.event",
192
+ bridge_id="default",
193
+ account_id="",
194
+ show=True,
195
+ schedule_timer=True,
196
+ pump_max_count=20,
197
+ pump_max_ms=0,
198
+ dispatch_on_qmt_thread=False,
199
+ connect_timeout_ms=3000,
200
+ ):
201
+ import sys
202
+
203
+ try:
204
+ globals_dict = sys._getframe(1).f_globals
205
+ except Exception:
206
+ globals_dict = {}
207
+ return PipeNormalQmtBridge(
208
+ context,
209
+ pipe_name=pipe_name,
210
+ request_channel=request_channel,
211
+ request_channels=request_channels,
212
+ callback_event_channel=callback_event_channel,
213
+ bridge_id=bridge_id,
214
+ account_id=account_id,
215
+ show=show,
216
+ globals_dict=globals_dict,
217
+ schedule_timer=schedule_timer,
218
+ pump_max_count=pump_max_count,
219
+ pump_max_ms=pump_max_ms,
220
+ dispatch_on_qmt_thread=dispatch_on_qmt_thread,
221
+ connect_timeout_ms=connect_timeout_ms,
222
+ ).start()
223
+
224
+
225
+ def start_pipe_trade_bridge(
226
+ context,
227
+ pipe_name=None,
228
+ request_channel="cfquant.trade.request",
229
+ bridge_id="default",
230
+ account_id="",
231
+ show=True,
232
+ connect_timeout_ms=3000,
233
+ ):
234
+ import sys
235
+
236
+ try:
237
+ globals_dict = sys._getframe(1).f_globals
238
+ except Exception:
239
+ globals_dict = {}
240
+ return PipeTradeBridge(
241
+ context,
242
+ pipe_name=pipe_name,
243
+ request_channel=request_channel,
244
+ bridge_id=bridge_id,
245
+ account_id=account_id,
246
+ show=show,
247
+ globals_dict=globals_dict,
248
+ connect_timeout_ms=connect_timeout_ms,
249
+ ).start()
cfquant/pipe_client.py ADDED
@@ -0,0 +1,223 @@
1
+ # -*- coding: utf-8 -*-
2
+ import queue
3
+ import threading
4
+
5
+ from .config import get_config
6
+ from .pipe_transport import (
7
+ DEFAULT_PIPE_NAME,
8
+ connect_pipe,
9
+ dumps_pipe_message,
10
+ loads_pipe_message,
11
+ normalize_pipe_name,
12
+ )
13
+ from .protocol import decode_value, dumps_message, loads_message, new_id, pack_request
14
+
15
+
16
+ class PipeRpcClient(object):
17
+ """
18
+ External Python RPC client over the cfquant named-pipe hub.
19
+ """
20
+
21
+ def __init__(self, pipe_name=None, request_channel=None, timeout=None, client_id=None, connect_timeout_ms=None):
22
+ cfg = get_config()
23
+ self.pipe_name = normalize_pipe_name(pipe_name or cfg.get("pipe_name") or DEFAULT_PIPE_NAME)
24
+ self.request_channel = request_channel or cfg["request_channel"]
25
+ self.timeout = float(timeout or cfg["timeout"])
26
+ self.client_id = client_id or cfg.get("client_id") or new_id("pipe_client")
27
+ self.reply_channel = self.client_id
28
+ self.connect_timeout_ms = int(connect_timeout_ms or cfg.get("pipe_connect_timeout_ms") or 3000)
29
+ self._rx_conn = None
30
+ self._tx_conn = None
31
+ self._pending = {}
32
+ self._callbacks = {}
33
+ self._lock = threading.RLock()
34
+ self._pending_lock = threading.RLock()
35
+ self._started = False
36
+ self._recv_thread = None
37
+
38
+ def start(self):
39
+ with self._lock:
40
+ if self._started and self._recv_thread is not None and self._recv_thread.is_alive():
41
+ return
42
+ if self._started:
43
+ self._started = False
44
+ self._close_conns_locked()
45
+ self._rx_conn = connect_pipe(self.pipe_name, timeout_ms=self.connect_timeout_ms)
46
+ self._rx_conn.write_frame(dumps_pipe_message({
47
+ "type": "hello",
48
+ "role": "api_rx",
49
+ "client_id": self.client_id,
50
+ }))
51
+ self._tx_conn = connect_pipe(self.pipe_name, timeout_ms=self.connect_timeout_ms)
52
+ self._tx_conn.write_frame(dumps_pipe_message({
53
+ "type": "hello",
54
+ "role": "api_tx",
55
+ "client_id": self.client_id,
56
+ }))
57
+ self._started = True
58
+ self._recv_thread = threading.Thread(target=self._recv_loop, args=(self._rx_conn,))
59
+ self._recv_thread.daemon = True
60
+ self._recv_thread.start()
61
+
62
+ def close(self):
63
+ with self._lock:
64
+ self._started = False
65
+ self._close_conns_locked()
66
+ self._fail_pending("cfquant pipe client closed")
67
+
68
+ def request(self, action, params=None, timeout=None, request_channel=None):
69
+ self.start()
70
+ effective_timeout = float(timeout or self.timeout)
71
+ request_id = new_id("req")
72
+ q = queue.Queue(maxsize=1)
73
+ with self._pending_lock:
74
+ self._pending[request_id] = q
75
+ raw = pack_request(
76
+ action,
77
+ params=params or {},
78
+ reply_channel=self.reply_channel,
79
+ client_id=self.client_id,
80
+ request_id=request_id,
81
+ timeout=effective_timeout,
82
+ )
83
+ try:
84
+ self._send_request(raw, request_channel or self.request_channel)
85
+ except Exception:
86
+ with self._pending_lock:
87
+ self._pending.pop(request_id, None)
88
+ self.close()
89
+ raise
90
+ try:
91
+ msg = q.get(timeout=effective_timeout)
92
+ except queue.Empty:
93
+ with self._pending_lock:
94
+ self._pending.pop(request_id, None)
95
+ self.close()
96
+ from .client import CfquantTimeout
97
+
98
+ raise CfquantTimeout("cfquant pipe request timeout: %s" % action)
99
+ if not msg.get("ok"):
100
+ err = msg.get("error") or {}
101
+ from .client import CfquantError
102
+
103
+ raise CfquantError(err.get("message") or str(err))
104
+ return decode_value(msg.get("result"))
105
+
106
+ def publish_event(self, channel, payload):
107
+ self.start()
108
+ self._send_request(dumps_message(payload), channel)
109
+
110
+ def add_callback(self, event, callback):
111
+ if callback is None:
112
+ return
113
+ self._callbacks.setdefault(event, []).append(callback)
114
+
115
+ def remove_callback(self, event, callback):
116
+ callbacks = self._callbacks.get(event) or []
117
+ if callback in callbacks:
118
+ callbacks.remove(callback)
119
+
120
+ def _send_request(self, payload, request_channel):
121
+ conn = self._tx_conn
122
+ if conn is None:
123
+ from .client import CfquantError
124
+
125
+ raise CfquantError("cfquant pipe client not started")
126
+ conn.write_frame(dumps_pipe_message({
127
+ "type": "request",
128
+ "role": "api_tx",
129
+ "client_id": self.client_id,
130
+ "request_channel": request_channel,
131
+ "payload": payload,
132
+ }))
133
+
134
+ def _recv_loop(self, expected_conn):
135
+ disconnect_message = "cfquant pipe connection closed"
136
+ while True:
137
+ try:
138
+ with self._lock:
139
+ if not self._started or self._rx_conn is not expected_conn:
140
+ return
141
+ conn = expected_conn
142
+ if conn is None:
143
+ disconnect_message = "cfquant pipe receive connection missing"
144
+ break
145
+ raw = conn.read_frame()
146
+ if raw is None:
147
+ disconnect_message = "cfquant pipe receive connection closed"
148
+ break
149
+ envelope = loads_pipe_message(raw)
150
+ payload = envelope.get("payload") if envelope else raw
151
+ msg = loads_message(payload)
152
+ if not msg:
153
+ continue
154
+ msg_type = msg.get("type")
155
+ if msg_type == "response":
156
+ with self._pending_lock:
157
+ q = self._pending.pop(msg.get("id"), None)
158
+ if q:
159
+ q.put(msg)
160
+ elif msg_type == "event":
161
+ self._dispatch_event(msg)
162
+ except Exception as e:
163
+ disconnect_message = "cfquant pipe receive failed: %s" % e
164
+ break
165
+ self._mark_disconnected(disconnect_message, expected_conn=expected_conn)
166
+
167
+ def _mark_disconnected(self, message, expected_conn=None):
168
+ with self._lock:
169
+ if expected_conn is not None and self._rx_conn is not expected_conn:
170
+ return
171
+ if not self._started and self._rx_conn is None and self._tx_conn is None:
172
+ return
173
+ self._started = False
174
+ self._close_conns_locked()
175
+ self._fail_pending(message)
176
+
177
+ def _close_conns_locked(self):
178
+ conns = [self._rx_conn, self._tx_conn]
179
+ self._rx_conn = None
180
+ self._tx_conn = None
181
+ for conn in conns:
182
+ if conn is None:
183
+ continue
184
+ try:
185
+ conn.close()
186
+ except Exception:
187
+ pass
188
+
189
+ def _fail_pending(self, message):
190
+ with self._pending_lock:
191
+ pending = list(self._pending.values())
192
+ self._pending.clear()
193
+ for q in pending:
194
+ try:
195
+ q.put_nowait({"ok": False, "error": {"message": message}})
196
+ except Exception:
197
+ pass
198
+
199
+ def _dispatch_event(self, msg):
200
+ event = msg.get("event")
201
+ data = decode_value(msg.get("data"))
202
+ full_msg = dict(msg)
203
+ full_msg["data"] = data
204
+ for callback in list(self._callbacks.get("__event__", [])):
205
+ try:
206
+ callback(full_msg)
207
+ except Exception:
208
+ pass
209
+ for callback in list(self._callbacks.get(event, [])):
210
+ try:
211
+ callback(data)
212
+ except Exception:
213
+ pass
214
+ if event and event.startswith("quote:"):
215
+ quote_msg = dict(msg)
216
+ quote_msg["data"] = data
217
+ if quote_msg.get("subscription_id") is not None and quote_msg.get("subscribe_id") is None:
218
+ quote_msg["subscribe_id"] = quote_msg.get("subscription_id")
219
+ for callback in list(self._callbacks.get("quote", [])):
220
+ try:
221
+ callback(quote_msg)
222
+ except Exception:
223
+ pass