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.
- LTtx/__init__.py +2 -0
- LTtx/tx/Config.txt +8 -0
- LTtx/tx/LTtx_server.py +1177 -0
- LTtx/tx/__init__.py +5 -0
- LTtx/tx/tx.py +1935 -0
- cfquant/__init__.py +26 -0
- cfquant/account_routing.py +119 -0
- cfquant/channels.py +76 -0
- cfquant/cli.py +266 -0
- cfquant/client.py +365 -0
- cfquant/config.py +86 -0
- cfquant/hub.py +304 -0
- cfquant/logging_i18n.py +348 -0
- cfquant/normal_bridge.py +676 -0
- cfquant/pipe_bridge.py +249 -0
- cfquant/pipe_client.py +223 -0
- cfquant/pipe_hub.py +602 -0
- cfquant/pipe_transport.py +493 -0
- cfquant/protocol.py +238 -0
- cfquant/qmt_bridge.py +1081 -0
- cfquant/runtime_report.py +334 -0
- cfquant/tx.py +1898 -0
- cfquant/tx_trade_bridge.py +1905 -0
- cfquant/version.py +5 -0
- cfquant/xtconstant.py +262 -0
- cfquant/xtdata.py +462 -0
- cfquant/xttrader.py +955 -0
- cfquant/xttype.py +586 -0
- cfquant-0.2.0.dist-info/LICENSE +21 -0
- cfquant-0.2.0.dist-info/METADATA +144 -0
- cfquant-0.2.0.dist-info/RECORD +90 -0
- cfquant-0.2.0.dist-info/WHEEL +5 -0
- cfquant-0.2.0.dist-info/entry_points.txt +4 -0
- cfquant-0.2.0.dist-info/top_level.txt +6 -0
- cfquant_pipe_hub.py +45 -0
- cfquant_web_server.py +12137 -0
- qmt_scripts/CFQUANT.py +507 -0
- qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +830 -0
- qmt_scripts/CFQUANT_LITE.py +4928 -0
- qmt_scripts/CFQUANT_TRADE_LOWLAT.py +382 -0
- qmt_scripts/__init__.py +1 -0
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- web_dashboard/__init__.py +1 -0
- web_dashboard/app.js +9847 -0
- web_dashboard/avatars/amber-pulse.svg +15 -0
- web_dashboard/avatars/copper-grid.svg +15 -0
- web_dashboard/avatars/market-blue.svg +15 -0
- web_dashboard/avatars/rose-circuit.svg +15 -0
- web_dashboard/avatars/signal-green.svg +15 -0
- web_dashboard/avatars/slate-wave.svg +15 -0
- web_dashboard/avatars/teal-orbit.svg +16 -0
- web_dashboard/avatars/violet-node.svg +15 -0
- web_dashboard/favicon.svg +5 -0
- web_dashboard/image/qmt_deploy/image-10.png +0 -0
- web_dashboard/image/qmt_deploy/image-11.png +0 -0
- web_dashboard/image/qmt_deploy/image-12.png +0 -0
- web_dashboard/image/qmt_deploy/image-13.png +0 -0
- web_dashboard/image/qmt_deploy/image-14.png +0 -0
- web_dashboard/image/qmt_deploy/image-15.png +0 -0
- web_dashboard/image/qmt_deploy/image-16.png +0 -0
- web_dashboard/image/qmt_deploy/image-17.png +0 -0
- web_dashboard/image/qmt_deploy/image-18.png +0 -0
- web_dashboard/image/qmt_deploy/image-19.png +0 -0
- web_dashboard/image/qmt_deploy/image-20.png +0 -0
- web_dashboard/image/qmt_deploy/image-21.png +0 -0
- web_dashboard/image/qmt_deploy/image-4.png +0 -0
- web_dashboard/image/qmt_deploy/image-5.png +0 -0
- web_dashboard/image/qmt_deploy/image-6.png +0 -0
- web_dashboard/image/qmt_deploy/image-7.png +0 -0
- web_dashboard/image/qmt_deploy/image-8.png +0 -0
- web_dashboard/image/qmt_deploy/image-9.png +0 -0
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- web_dashboard/index.html +2317 -0
- web_dashboard/styles.css +7817 -0
- 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
|