insight-proxy-client 0.0.1__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.
- insight_proxy_client-0.0.1/PKG-INFO +91 -0
- insight_proxy_client-0.0.1/README.md +76 -0
- insight_proxy_client-0.0.1/pyproject.toml +27 -0
- insight_proxy_client-0.0.1/setup.cfg +4 -0
- insight_proxy_client-0.0.1/src/insight_proxy/__init__.py +4 -0
- insight_proxy_client-0.0.1/src/insight_proxy/client.py +183 -0
- insight_proxy_client-0.0.1/src/insight_proxy/com/__init__.py +1 -0
- insight_proxy_client-0.0.1/src/insight_proxy/com/insight/__init__.py +9 -0
- insight_proxy_client-0.0.1/src/insight_proxy/com/insight/_contract.py +2 -0
- insight_proxy_client-0.0.1/src/insight_proxy/com/insight/_session.py +179 -0
- insight_proxy_client-0.0.1/src/insight_proxy/com/insight/market_service.py +43 -0
- insight_proxy_client-0.0.1/src/insight_proxy/com/insight/playback.py +13 -0
- insight_proxy_client-0.0.1/src/insight_proxy/com/insight/query.py +325 -0
- insight_proxy_client-0.0.1/src/insight_proxy/com/insight/subscribe.py +55 -0
- insight_proxy_client-0.0.1/src/insight_proxy/protocol.py +58 -0
- insight_proxy_client-0.0.1/src/insight_proxy/typed_codec.py +160 -0
- insight_proxy_client-0.0.1/src/insight_proxy_client.egg-info/PKG-INFO +91 -0
- insight_proxy_client-0.0.1/src/insight_proxy_client.egg-info/SOURCES.txt +19 -0
- insight_proxy_client-0.0.1/src/insight_proxy_client.egg-info/dependency_links.txt +1 -0
- insight_proxy_client-0.0.1/src/insight_proxy_client.egg-info/requires.txt +2 -0
- insight_proxy_client-0.0.1/src/insight_proxy_client.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: insight-proxy-client
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Python client for insight_proxy with Insight 6.1.2 compatible data APIs
|
|
5
|
+
Classifier: Programming Language :: Python :: 3
|
|
6
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
7
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: pandas<3,>=1.5
|
|
14
|
+
Requires-Dist: numpy<3,>=1.23
|
|
15
|
+
|
|
16
|
+
# insight-proxy-client
|
|
17
|
+
|
|
18
|
+
Python client for the **insight_proxy** TCP service. Data APIs target **Insight 6.1.2**;
|
|
19
|
+
the import namespace is `insight_proxy.com.insight`.
|
|
20
|
+
This is an independent adapter, not the official Insight SDK.
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```shell
|
|
25
|
+
python -m pip install insight-proxy-client==0.0.1
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Supports Python 3.9–3.12. pandas and NumPy are installed as dependencies.
|
|
29
|
+
The client does not require the vendor SDK or native DLLs. A separately deployed
|
|
30
|
+
insight_proxy server with Insight 6.1.2 and valid data access is required.
|
|
31
|
+
This package does not include the server executable or account credentials.
|
|
32
|
+
|
|
33
|
+
## Quick start
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from datetime import datetime
|
|
37
|
+
from insight_proxy.com.insight import Client
|
|
38
|
+
from insight_proxy.com.insight.market_service import market_service
|
|
39
|
+
|
|
40
|
+
class Market(market_service):
|
|
41
|
+
def on_subscribe_tick(self, result):
|
|
42
|
+
print(result)
|
|
43
|
+
|
|
44
|
+
def on_query_response(self, result):
|
|
45
|
+
print(result)
|
|
46
|
+
|
|
47
|
+
# Both Client() and Client(Market()) are supported.
|
|
48
|
+
# Defaults: host="127.0.0.1", port=8765, token="123456".
|
|
49
|
+
# The token must match the server configuration.
|
|
50
|
+
with Client(Market()) as client:
|
|
51
|
+
frame = client.query.get_kline(
|
|
52
|
+
"601688.SH",
|
|
53
|
+
[datetime(2026, 10, 19), datetime(2026, 10, 26, 23, 59, 59)],
|
|
54
|
+
frequency="daily", fq="none",
|
|
55
|
+
)
|
|
56
|
+
print(frame)
|
|
57
|
+
client.subscribe.subscribe_tick_by_id(["601688.SH"], mode="coverage")
|
|
58
|
+
input("Press Enter to close the subscription session.\n")
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Construction does not open a connection. Use `client.connect()` / `client.close()`
|
|
62
|
+
or a `with` block. Override connection settings using keyword arguments:
|
|
63
|
+
`Client(Market(), host="127.0.0.1", port=8765, token="your-server-token")`.
|
|
64
|
+
Example dates are fixed historical values; select dates and instruments available
|
|
65
|
+
to your server account.
|
|
66
|
+
|
|
67
|
+
## Migrating SDK data calls
|
|
68
|
+
|
|
69
|
+
Replace `insight_python.com.insight` imports with `insight_proxy.com.insight`.
|
|
70
|
+
Replace SDK login and initialization with the proxy connection:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from insight_proxy.com.insight import connect, close, query, subscribe, playback
|
|
74
|
+
|
|
75
|
+
connect() # or connect(Market(), host="...", port=8765, token="...")
|
|
76
|
+
try:
|
|
77
|
+
result = query.get_kline() # retains the SDK's missing-argument response
|
|
78
|
+
finally:
|
|
79
|
+
close()
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The compatibility layer exposes 80 query functions, 11 data subscription functions,
|
|
83
|
+
local `subscribe.sync()`, two playback functions and the `market_service` callbacks.
|
|
84
|
+
It preserves Insight 6.1.2 data function signatures, DataFrame/Series values,
|
|
85
|
+
column ordering, indexes, dtypes, datetime values, tuples and callback names.
|
|
86
|
+
SDK login/logging helpers and native gateway types are outside this client API.
|
|
87
|
+
One compatibility session exclusively owns the server's native SDK login.
|
|
88
|
+
Native SDK permissions and availability still determine which requests succeed.
|
|
89
|
+
|
|
90
|
+
Do not install the older combined `insight_proxy` distribution alongside this
|
|
91
|
+
client package in the same environment: they provide the same import namespace.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# insight-proxy-client
|
|
2
|
+
|
|
3
|
+
Python client for the **insight_proxy** TCP service. Data APIs target **Insight 6.1.2**;
|
|
4
|
+
the import namespace is `insight_proxy.com.insight`.
|
|
5
|
+
This is an independent adapter, not the official Insight SDK.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```shell
|
|
10
|
+
python -m pip install insight-proxy-client==0.0.1
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Supports Python 3.9–3.12. pandas and NumPy are installed as dependencies.
|
|
14
|
+
The client does not require the vendor SDK or native DLLs. A separately deployed
|
|
15
|
+
insight_proxy server with Insight 6.1.2 and valid data access is required.
|
|
16
|
+
This package does not include the server executable or account credentials.
|
|
17
|
+
|
|
18
|
+
## Quick start
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from datetime import datetime
|
|
22
|
+
from insight_proxy.com.insight import Client
|
|
23
|
+
from insight_proxy.com.insight.market_service import market_service
|
|
24
|
+
|
|
25
|
+
class Market(market_service):
|
|
26
|
+
def on_subscribe_tick(self, result):
|
|
27
|
+
print(result)
|
|
28
|
+
|
|
29
|
+
def on_query_response(self, result):
|
|
30
|
+
print(result)
|
|
31
|
+
|
|
32
|
+
# Both Client() and Client(Market()) are supported.
|
|
33
|
+
# Defaults: host="127.0.0.1", port=8765, token="123456".
|
|
34
|
+
# The token must match the server configuration.
|
|
35
|
+
with Client(Market()) as client:
|
|
36
|
+
frame = client.query.get_kline(
|
|
37
|
+
"601688.SH",
|
|
38
|
+
[datetime(2026, 10, 19), datetime(2026, 10, 26, 23, 59, 59)],
|
|
39
|
+
frequency="daily", fq="none",
|
|
40
|
+
)
|
|
41
|
+
print(frame)
|
|
42
|
+
client.subscribe.subscribe_tick_by_id(["601688.SH"], mode="coverage")
|
|
43
|
+
input("Press Enter to close the subscription session.\n")
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Construction does not open a connection. Use `client.connect()` / `client.close()`
|
|
47
|
+
or a `with` block. Override connection settings using keyword arguments:
|
|
48
|
+
`Client(Market(), host="127.0.0.1", port=8765, token="your-server-token")`.
|
|
49
|
+
Example dates are fixed historical values; select dates and instruments available
|
|
50
|
+
to your server account.
|
|
51
|
+
|
|
52
|
+
## Migrating SDK data calls
|
|
53
|
+
|
|
54
|
+
Replace `insight_python.com.insight` imports with `insight_proxy.com.insight`.
|
|
55
|
+
Replace SDK login and initialization with the proxy connection:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from insight_proxy.com.insight import connect, close, query, subscribe, playback
|
|
59
|
+
|
|
60
|
+
connect() # or connect(Market(), host="...", port=8765, token="...")
|
|
61
|
+
try:
|
|
62
|
+
result = query.get_kline() # retains the SDK's missing-argument response
|
|
63
|
+
finally:
|
|
64
|
+
close()
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The compatibility layer exposes 80 query functions, 11 data subscription functions,
|
|
68
|
+
local `subscribe.sync()`, two playback functions and the `market_service` callbacks.
|
|
69
|
+
It preserves Insight 6.1.2 data function signatures, DataFrame/Series values,
|
|
70
|
+
column ordering, indexes, dtypes, datetime values, tuples and callback names.
|
|
71
|
+
SDK login/logging helpers and native gateway types are outside this client API.
|
|
72
|
+
One compatibility session exclusively owns the server's native SDK login.
|
|
73
|
+
Native SDK permissions and availability still determine which requests succeed.
|
|
74
|
+
|
|
75
|
+
Do not install the older combined `insight_proxy` distribution alongside this
|
|
76
|
+
client package in the same environment: they provide the same import namespace.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "insight-proxy-client"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "Python client for insight_proxy with Insight 6.1.2 compatible data APIs"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
dependencies = ["pandas>=1.5,<3", "numpy>=1.23,<3"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Programming Language :: Python :: 3.9",
|
|
15
|
+
"Programming Language :: Python :: 3.10",
|
|
16
|
+
"Programming Language :: Python :: 3.11",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[tool.setuptools.packages.find]
|
|
22
|
+
where = ["src"]
|
|
23
|
+
include = ["insight_proxy*"]
|
|
24
|
+
|
|
25
|
+
[tool.setuptools]
|
|
26
|
+
package-dir = {"" = "src"}
|
|
27
|
+
include-package-data = false
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Thread-safe request/response multiplexing with a bounded event queue."""
|
|
2
|
+
import itertools
|
|
3
|
+
import queue
|
|
4
|
+
import socket
|
|
5
|
+
import ssl
|
|
6
|
+
import threading
|
|
7
|
+
|
|
8
|
+
from .protocol import frame_size, pack, unpack, restore_dates
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BridgeError(RuntimeError):
|
|
12
|
+
def __init__(self, code, message):
|
|
13
|
+
self.code = code
|
|
14
|
+
super().__init__(f"{code}: {message}")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class InsightClient:
|
|
18
|
+
def __init__(self, host="127.0.0.1", port=8765, *, token="", timeout=30,
|
|
19
|
+
event_queue_size=10000, ssl_context=None):
|
|
20
|
+
self.host, self.port = host, port
|
|
21
|
+
self.token, self.timeout = token, timeout
|
|
22
|
+
self.ssl_context = ssl_context
|
|
23
|
+
self._sock = None
|
|
24
|
+
self._reader = None
|
|
25
|
+
self._send_lock = threading.Lock()
|
|
26
|
+
self._lock = threading.Lock()
|
|
27
|
+
self._ids = itertools.count(1)
|
|
28
|
+
self._pending = {}
|
|
29
|
+
self._events = queue.Queue(maxsize=event_queue_size)
|
|
30
|
+
self._failure = None
|
|
31
|
+
|
|
32
|
+
def connect(self):
|
|
33
|
+
if self._sock is not None:
|
|
34
|
+
raise RuntimeError("Already connected; create a new client for reconnect")
|
|
35
|
+
sock = socket.create_connection((self.host, self.port), self.timeout)
|
|
36
|
+
try:
|
|
37
|
+
if self.ssl_context:
|
|
38
|
+
sock = self.ssl_context.wrap_socket(sock, server_hostname=self.host)
|
|
39
|
+
sock.settimeout(None)
|
|
40
|
+
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
|
41
|
+
self._sock = sock
|
|
42
|
+
self._reader = threading.Thread(target=self._read_loop, daemon=True)
|
|
43
|
+
self._reader.start()
|
|
44
|
+
self.request("hello", {"version": 1, "token": self.token})
|
|
45
|
+
return self
|
|
46
|
+
except BaseException:
|
|
47
|
+
sock.close()
|
|
48
|
+
self.close()
|
|
49
|
+
raise
|
|
50
|
+
|
|
51
|
+
def _exact(self, size):
|
|
52
|
+
chunks = bytearray()
|
|
53
|
+
while len(chunks) < size:
|
|
54
|
+
chunk = self._sock.recv(size - len(chunks))
|
|
55
|
+
if not chunk:
|
|
56
|
+
raise ConnectionError("Bridge disconnected; reconnect and resubscribe")
|
|
57
|
+
chunks.extend(chunk)
|
|
58
|
+
return bytes(chunks)
|
|
59
|
+
|
|
60
|
+
def _fail(self, error):
|
|
61
|
+
with self._lock:
|
|
62
|
+
if self._failure is None:
|
|
63
|
+
self._failure = error
|
|
64
|
+
pending, self._pending = self._pending, {}
|
|
65
|
+
for waiter in pending.values():
|
|
66
|
+
waiter.put(self._failure)
|
|
67
|
+
if self._sock:
|
|
68
|
+
try:
|
|
69
|
+
self._sock.shutdown(socket.SHUT_RDWR)
|
|
70
|
+
except OSError:
|
|
71
|
+
pass
|
|
72
|
+
self._sock.close()
|
|
73
|
+
|
|
74
|
+
def _read_loop(self):
|
|
75
|
+
try:
|
|
76
|
+
while True:
|
|
77
|
+
message = restore_dates(unpack(self._exact(frame_size(self._exact(4)))))
|
|
78
|
+
if message.get("type") == "response":
|
|
79
|
+
with self._lock:
|
|
80
|
+
waiter = self._pending.pop(message.get("id"), None)
|
|
81
|
+
if waiter:
|
|
82
|
+
waiter.put(message)
|
|
83
|
+
elif message.get("type") == "event":
|
|
84
|
+
try:
|
|
85
|
+
self._events.put_nowait(message)
|
|
86
|
+
except queue.Full:
|
|
87
|
+
raise BridgeError("slow_consumer", "Client event queue full; connection closed to signal a data gap")
|
|
88
|
+
else:
|
|
89
|
+
raise ValueError("Unknown server message")
|
|
90
|
+
except Exception as exc:
|
|
91
|
+
self._fail(exc)
|
|
92
|
+
|
|
93
|
+
def request(self, method, params=None, *, timeout=None):
|
|
94
|
+
request_id = next(self._ids)
|
|
95
|
+
waiter = queue.Queue()
|
|
96
|
+
frame = pack({"id": request_id, "method": method, "params": params or {}})
|
|
97
|
+
with self._lock:
|
|
98
|
+
if self._failure:
|
|
99
|
+
raise self._failure
|
|
100
|
+
if self._sock is None:
|
|
101
|
+
raise ConnectionError("Call connect() first")
|
|
102
|
+
self._pending[request_id] = waiter
|
|
103
|
+
try:
|
|
104
|
+
with self._send_lock:
|
|
105
|
+
self._sock.sendall(frame)
|
|
106
|
+
try:
|
|
107
|
+
reply = waiter.get(timeout=self.timeout if timeout is None else timeout)
|
|
108
|
+
except queue.Empty:
|
|
109
|
+
raise TimeoutError(f"{method} timed out; upstream execution may still finish") from None
|
|
110
|
+
if isinstance(reply, Exception):
|
|
111
|
+
raise reply
|
|
112
|
+
if not reply.get("ok"):
|
|
113
|
+
error = reply["error"]
|
|
114
|
+
raise BridgeError(error["code"], error["message"])
|
|
115
|
+
return reply.get("result")
|
|
116
|
+
finally:
|
|
117
|
+
with self._lock:
|
|
118
|
+
self._pending.pop(request_id, None)
|
|
119
|
+
|
|
120
|
+
def ping(self):
|
|
121
|
+
return self.request("ping")
|
|
122
|
+
|
|
123
|
+
def status(self):
|
|
124
|
+
return self.request("status")
|
|
125
|
+
|
|
126
|
+
def methods(self):
|
|
127
|
+
return self.request("methods")
|
|
128
|
+
|
|
129
|
+
def query(self, name, **params):
|
|
130
|
+
"""Returns {columns, records, dtypes}; datetime parameters are encoded automatically."""
|
|
131
|
+
return self.request("query", {"name": name, "kwargs": params})
|
|
132
|
+
|
|
133
|
+
def snapshot(self, symbols, datatype):
|
|
134
|
+
return self.query("get_gdp_recent_snap", htsc_code=symbols, data_type=datatype)
|
|
135
|
+
|
|
136
|
+
def static(self, symbols):
|
|
137
|
+
return self.query("get_gdp_static_by_id", htsc_code=symbols)
|
|
138
|
+
|
|
139
|
+
def subscribe(self, datatype, symbols=None):
|
|
140
|
+
"""symbols=None subscribes the entire datatype. Returns submitted, not confirmed."""
|
|
141
|
+
return self.request("subscribe", {"datatype": datatype, "symbols": symbols})
|
|
142
|
+
|
|
143
|
+
def unsubscribe(self, datatype, symbols=None):
|
|
144
|
+
return self.request("unsubscribe", {"datatype": datatype, "symbols": symbols})
|
|
145
|
+
|
|
146
|
+
def subscribe_tick(self, symbols):
|
|
147
|
+
"""Legacy SDK stock/futures/ETF tick subscription with explicit codes."""
|
|
148
|
+
return self.request("subscribe_tick", {"symbols": symbols})
|
|
149
|
+
|
|
150
|
+
def unsubscribe_tick(self, symbols):
|
|
151
|
+
return self.request("unsubscribe_tick", {"symbols": symbols})
|
|
152
|
+
|
|
153
|
+
def unsubscribe_all(self):
|
|
154
|
+
return self.request("unsubscribe_all")
|
|
155
|
+
|
|
156
|
+
def next_event(self, timeout=None):
|
|
157
|
+
# Check failure first: do not silently serve buffered stale data after a gap.
|
|
158
|
+
import time
|
|
159
|
+
deadline = None if timeout is None else time.monotonic() + timeout
|
|
160
|
+
while True:
|
|
161
|
+
if self._failure:
|
|
162
|
+
raise self._failure
|
|
163
|
+
wait = 0.2 if deadline is None else max(0, min(0.2, deadline - time.monotonic()))
|
|
164
|
+
try:
|
|
165
|
+
return self._events.get(timeout=wait)
|
|
166
|
+
except queue.Empty:
|
|
167
|
+
if deadline is not None and time.monotonic() >= deadline:
|
|
168
|
+
raise TimeoutError("No event received") from None
|
|
169
|
+
|
|
170
|
+
def events(self):
|
|
171
|
+
while True:
|
|
172
|
+
yield self.next_event()
|
|
173
|
+
|
|
174
|
+
def close(self):
|
|
175
|
+
self._fail(ConnectionError("Client closed"))
|
|
176
|
+
if self._reader and threading.current_thread() is not self._reader:
|
|
177
|
+
self._reader.join(timeout=2)
|
|
178
|
+
|
|
179
|
+
def __enter__(self):
|
|
180
|
+
return self.connect()
|
|
181
|
+
|
|
182
|
+
def __exit__(self, *_):
|
|
183
|
+
self.close()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Insight-compatible module namespace."""
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Insight 6.1.2 data API over TCP, for Python 3.9-3.12.
|
|
2
|
+
|
|
3
|
+
Change SDK imports to this package and replace login/initialization with connect().
|
|
4
|
+
An SDK-compatible connection exclusively owns the server's native login session.
|
|
5
|
+
"""
|
|
6
|
+
from ._session import Client, connect, close, SDKError
|
|
7
|
+
from . import query, subscribe, playback, market_service
|
|
8
|
+
|
|
9
|
+
__all__ = ['Client', 'connect', 'close', 'SDKError', 'query', 'subscribe', 'playback', 'market_service']
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"""Pinned public API contract, generated from SDK 6.1.2."""
|
|
2
|
+
CONTRACT = {'sdk_version': '6.1.2', 'modules': {'query': {'query_fin_info': '(query_type, params)', 'get_kline': "(htsc_code=None, time=None, frequency='daily', fq='pre')", 'get_derived': '(htsc_code=None, trading_day=None, type=None)', 'get_trade_distribution': '(htsc_code=None, trading_day=None)', 'get_chip_distribution': '(htsc_code=None, trading_day=None)', 'get_money_flow': '(htsc_code=None, trading_day=None)', 'get_change_summary': '(market=None, trading_day=None)', 'get_billboard': '(type=None, market=None)', 'get_stock_info': '(htsc_code=None, listing_date=None, listing_state=None)', 'get_all_stocks_info': '(listing_date=None, exchange=None, listing_state=None)', 'get_trading_days': '(exchange=None, trading_day=None, count=None)', 'get_industries': '(classified=None)', 'get_industry': "(htsc_code=None, classified='sw')", 'get_industry_stocks': "(industry_code=None, classified='sw')", 'get_new_share': '(htsc_code=None, book_start_date_online=None, listing_date=None)', 'get_daily_basic': '(htsc_code=None, trading_day=None)', 'get_stock_valuation': '(htsc_code=None, trading_day=None)', 'get_bond_info': '(htsc_code=None, secu_category_code=None, listing_date=None, issue_start_date=None, end_date=None)', 'get_all_bonds': '(exchange=None, secu_category_code=None, listing_date=None, issue_start_date=None, end_date=None)', 'get_repo_price': '(htsc_code=None, exchange=None, trading_day=None)', 'get_new_con_bond': '(htsc_code=None, exchange=None, book_start_date_online=None, listing_date=None, issue_date=None, convert_code=None)', 'get_bond_price': '(htsc_code=None, exchange=None, trading_day=None)', 'get_con_bond_redemption': '(htsc_code=None, exchange=None, register_date=None)', 'get_con_bond_2_shares_change': '(htsc_code=None, exchange=None, pub_date=None, convert_code=None)', 'get_con_bond_2_shares': '(htsc_code=None, pub_date=None, exer_begin_date=None, exer_end_date=None, convert_code=None, exchange=None)', 'get_income_statement': '(htsc_code=None, end_date=None, period=None)', 'get_balance_sheet': '(htsc_code=None, end_date=None, period=None)', 'get_cashflow_statement': '(htsc_code=None, end_date=None, period=None)', 'get_fin_indicator': '(htsc_code=None, end_date=None, period=None)', 'get_newest_stock_value': '(htsc_code=None, exchange=None)', 'get_company_info': '(htsc_code=None, name=None)', 'get_shareholder_num': '(htsc_code=None, name=None, end_date=None)', 'get_additional_share': '(htsc_code=None, listing_date=None)', 'get_allotment_share': '(htsc_code=None, ini_pub_date=None, is_allot_half_year=None)', 'get_capital_structure': '(htsc_code=None, end_date=None)', 'get_dividend': '(htsc_code=None, right_reg_date=None, ex_divi_date=None, divi_pay_date=None)', 'get_north_bound': '(htsc_code=None, trading_day=None)', 'get_margin_target': '(htsc_code=None, exchange=None)', 'get_margin_summary': '(htsc_code=None, trading_day=None)', 'get_margin_detail': '(exchange=None, trading_day=None)', 'get_shareholders_top10': '(htsc_code=None, change_date=None)', 'get_shareholders_floating_top10': '(htsc_code=None, change_date=None)', 'get_tick': '(htsc_code=None, security_type=None, trading_day=None)', 'get_adj_factor': '(htsc_code=None, begin_date=None)', 'get_locked_shares': '(htsc_code=None, listing_date=None)', 'get_frozen_shares': '(htsc_code=None, freezing_start_date=None)', 'get_hk_industry': '(htsc_code=None)', 'get_hk_industry_stocks': '(industry_code=None)', 'get_hk_daily_basic': '(htsc_code=None, trading_day=None)', 'get_hk_stock_valuation': '(htsc_code=None, trading_day=None)', 'get_hk_stock_basic_info': '(htsc_code=None, listing_date=None, listing_state=None)', 'get_hk_dividend': '(htsc_code=None, ex_divi_date=None)', 'get_hk_index_mapping': '(month=None)', 'get_main_product_info': '(htsc_code=None, product_code=None, product_level=None)', 'get_htsc_margin_target': '()', 'get_htsc_margin_by_id': '(htsc_code=None, data_type=None)', 'get_htsc_margin_by_type': '(data_type=None, security_type=None, is_async=False)', 'get_index_info': '(htsc_code=None, trading_day=None)', 'get_all_index': '(exchange=None, trading_day=None)', 'get_index_component': '(htsc_code=None, name=None, stock_code=None, trading_day=None)', 'get_index_component_pro': '(htsc_code=None, name=None, stock_code=None, trading_day=None)', 'get_factors': '(htsc_code=None, factor_name=None, trading_day=None)', 'get_fund_info': '(htsc_code=None, trading_day=None)', 'get_fund_target': '(htsc_code=None, exchange=None, end_date=None)', 'get_etf_component': '(htsc_code=None, pub_date=None, trading_day=None)', 'get_public_fund_portfolio': '(htsc_code=None, name=None, exchange=None, end_date=None)', 'get_etf_redemption': '(htsc_code=None, exchange=None, trading_day=None)', 'get_basic_info': '(htsc_code=None)', 'get_all_basic_info': '(security_type=None, exchange=None, today=True)', 'get_news': '(htsc_code=None, event_ids=None, pub_date=None)', 'find_edb_index': '(name=None)', 'edb': '(indexes=None, pub_date=None)', 'get_rpt_basicinfo_ht': '(id=None, report_code=None, time=None, title=None, language=None, is_valid=None)', 'get_rpt_stk_ht': '(report_code=None)', 'get_rpt_industry_ht': '(report_code=None)', 'get_rpt_author_ht': '(report_code=None)', 'get_rpt_annex_ht': '(report_code=None)', 'get_rpt_stkpredict_ht': '(report_code=None)', 'get_etf_info': '(query)', 'query_mdcontant': '(security_idsource_and_types, security_id_list)'}, 'subscribe': {'subscribe_tick_by_id': "(htsc_code, mode='coverage')", 'subscribe_tick_by_type': "(query=None, mode='coverage')", 'subscribe_kline_by_id': "(htsc_code=None, frequency=None, mode='coverage')", 'subscribe_kline_by_type': "(query=None, frequency=None, mode='coverage')", 'subscribe_trans_and_order_by_id': "(htsc_code=None, mode='coverage')", 'subscribe_trans_and_order_by_type': "(query=None, mode='coverage')", 'subscribe_htsc_margin_by_id': "(htsc_code=None, data_type=None, mode='coverage')", 'subscribe_htsc_margin_by_type': "(security_type=None, data_type=None, mode='coverage')", 'subscribe_derived': "(type=None, htsc_code=None, exchange=None, frequency='1min', mode='coverage')", 'subscribe_news_by_type': "(query=None, mode='coverage')", 'subscribe_news_by_id': "(htsc_code=None, mode='coverage')", 'sync': '()'}, 'playback': {'playback_tick': "(htsc_code=None, replay_time=None, fq='pre', timeout=600)", 'playback_trans_and_order': "(htsc_code=None, replay_time=None, fq='pre', timeout=600)"}}, 'sha256': {'query': '17976b193d25e5a05e8e3492b9f04ad54a93ddd1a700a30212b721cd09e3b4a7', 'subscribe': 'b7920962f688aac4cb8bedd8c5a60c80846a729b347eafc94719c5faba9913b0', 'playback': 'fdd2d88a9a67f564eae0c6a048916a4c25c0e1ca2bcd38ec03d24a3a5e2e2ddf'}, 'callbacks': ['on_subscribe_tick', 'on_subscribe_kline', 'on_subscribe_trans_and_order', 'on_subscribe_derived', 'on_subscribe_htsc_margin', 'on_subscribe_news', 'on_playback_tick', 'on_playback_trans_and_order', 'onPlaybackStatus', 'onPlaybackResponse', 'onPlaybackControlResponse', 'on_query_response', 'on_query_margin_response']}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import builtins
|
|
2
|
+
import functools
|
|
3
|
+
import importlib
|
|
4
|
+
import inspect
|
|
5
|
+
import logging
|
|
6
|
+
import queue
|
|
7
|
+
import threading
|
|
8
|
+
|
|
9
|
+
from ...client import InsightClient, BridgeError
|
|
10
|
+
from ...typed_codec import encode, decode
|
|
11
|
+
from ._contract import CONTRACT
|
|
12
|
+
from .market_service import market_service
|
|
13
|
+
|
|
14
|
+
LOG = logging.getLogger('insight_proxy.compat')
|
|
15
|
+
_default = None
|
|
16
|
+
_default_lock = threading.RLock()
|
|
17
|
+
_EXCEPTIONS = {name: getattr(builtins, name) for name in (
|
|
18
|
+
'ValueError', 'TypeError', 'KeyError', 'IndexError', 'RuntimeError', 'AttributeError',
|
|
19
|
+
'NotImplementedError', 'AssertionError', 'ZeroDivisionError', 'OverflowError',
|
|
20
|
+
'NameError', 'UnboundLocalError', 'TimeoutError', 'OSError', 'ConnectionError',
|
|
21
|
+
'PermissionError', 'FileNotFoundError', 'ImportError', 'ModuleNotFoundError')}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SDKError(RuntimeError):
|
|
25
|
+
"""A vendor exception whose class is not part of the safe builtin allowlist."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class _Namespace:
|
|
29
|
+
def __init__(self, client, module):
|
|
30
|
+
self._client, self._module = client, module
|
|
31
|
+
|
|
32
|
+
def __dir__(self):
|
|
33
|
+
return list(CONTRACT['modules'][self._module])
|
|
34
|
+
|
|
35
|
+
def __getattr__(self, name):
|
|
36
|
+
if name not in CONTRACT['modules'][self._module]:
|
|
37
|
+
raise AttributeError(name)
|
|
38
|
+
template = getattr(importlib.import_module('.' + self._module, __package__), name)
|
|
39
|
+
if name == 'sync':
|
|
40
|
+
return template
|
|
41
|
+
signature = inspect.signature(template)
|
|
42
|
+
|
|
43
|
+
@functools.wraps(template)
|
|
44
|
+
def invoke(*args, **kwargs):
|
|
45
|
+
bound = signature.bind(*args, **kwargs)
|
|
46
|
+
bound.apply_defaults()
|
|
47
|
+
return self._client._call(self._module, name, dict(bound.arguments))
|
|
48
|
+
return invoke
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Client:
|
|
52
|
+
"""Explicit session alternative to module-level connect/query/subscribe.
|
|
53
|
+
|
|
54
|
+
Callback exceptions are logged and placed in errors. Transport failures are
|
|
55
|
+
also placed there and close the session; neither is silently swallowed.
|
|
56
|
+
on_event receives bridge status/ack/error events, outside the SDK callback API.
|
|
57
|
+
"""
|
|
58
|
+
def __init__(self, marketservice=None, *, host='127.0.0.1', port=8765, token='123456',
|
|
59
|
+
timeout=30, ssl_context=None, event_queue_size=10000, on_event=None):
|
|
60
|
+
self.transport = InsightClient(host, port, token=token, timeout=timeout,
|
|
61
|
+
ssl_context=ssl_context, event_queue_size=event_queue_size)
|
|
62
|
+
self.marketservice = market_service() if marketservice is None else marketservice
|
|
63
|
+
self.on_event = on_event
|
|
64
|
+
self.errors = queue.Queue(maxsize=100)
|
|
65
|
+
self.query = _Namespace(self, 'query')
|
|
66
|
+
self.subscribe = _Namespace(self, 'subscribe')
|
|
67
|
+
self.playback = _Namespace(self, 'playback')
|
|
68
|
+
self._closed = threading.Event()
|
|
69
|
+
self._thread = None
|
|
70
|
+
self._connected = False
|
|
71
|
+
self.capabilities = None
|
|
72
|
+
|
|
73
|
+
def connect(self):
|
|
74
|
+
# Fail early with an actionable dependency message, before claiming a login.
|
|
75
|
+
try:
|
|
76
|
+
import pandas # noqa: F401
|
|
77
|
+
except ImportError as exc:
|
|
78
|
+
raise ImportError('Install insight-proxy-client (pandas >=1.5,<3)') from exc
|
|
79
|
+
try:
|
|
80
|
+
self.transport.connect()
|
|
81
|
+
self.capabilities = self.transport.request('sdk_open', {'sdk_version': '6.1.2', 'codec': 1})
|
|
82
|
+
if self.capabilities.get('modules') != CONTRACT['modules']:
|
|
83
|
+
raise BridgeError('incompatible_sdk', 'Server API contract differs from Insight 6.1.2')
|
|
84
|
+
self._connected = True
|
|
85
|
+
self._thread = threading.Thread(target=self._callbacks, name='insight-callbacks', daemon=True)
|
|
86
|
+
self._thread.start()
|
|
87
|
+
return self
|
|
88
|
+
except BaseException:
|
|
89
|
+
self.close()
|
|
90
|
+
raise
|
|
91
|
+
|
|
92
|
+
def _call(self, module, name, kwargs):
|
|
93
|
+
if not self._connected or self._closed.is_set():
|
|
94
|
+
raise ConnectionError('Call connect() first; create a new Client after close()')
|
|
95
|
+
timeout = self.transport.timeout
|
|
96
|
+
# Native playback is blocking and has its own public timeout argument.
|
|
97
|
+
if module == 'playback' and isinstance(kwargs.get('timeout'), (int, float)):
|
|
98
|
+
timeout = max(timeout, kwargs['timeout'] + 15)
|
|
99
|
+
result = self.transport.request('sdk_call', {'module': module, 'name': name,
|
|
100
|
+
'kwargs': encode(kwargs)}, timeout=timeout)
|
|
101
|
+
if 'exception' in result:
|
|
102
|
+
error = result['exception']
|
|
103
|
+
cls = _EXCEPTIONS.get(error['type'], SDKError)
|
|
104
|
+
raise cls(*decode(error['args']))
|
|
105
|
+
return decode(result['value'])
|
|
106
|
+
|
|
107
|
+
def _record_error(self, error):
|
|
108
|
+
try:
|
|
109
|
+
self.errors.put_nowait(error)
|
|
110
|
+
except queue.Full:
|
|
111
|
+
self.close()
|
|
112
|
+
|
|
113
|
+
def _callbacks(self):
|
|
114
|
+
while not self._closed.is_set():
|
|
115
|
+
try:
|
|
116
|
+
event = self.transport.next_event(timeout=0.2)
|
|
117
|
+
except TimeoutError:
|
|
118
|
+
continue
|
|
119
|
+
except Exception as exc:
|
|
120
|
+
if not self._closed.is_set():
|
|
121
|
+
self._record_error(exc)
|
|
122
|
+
LOG.error('Insight connection ended: %s', exc)
|
|
123
|
+
self.close()
|
|
124
|
+
break
|
|
125
|
+
try:
|
|
126
|
+
if event['event'] == 'sdk_callback':
|
|
127
|
+
name = event['data']['name']
|
|
128
|
+
if name not in CONTRACT['callbacks']:
|
|
129
|
+
raise ValueError('Unknown SDK callback: ' + str(name))
|
|
130
|
+
callback = getattr(self.marketservice, name, None)
|
|
131
|
+
if callback is not None:
|
|
132
|
+
callback(decode(event['data']['value']))
|
|
133
|
+
elif event['event'] == 'sdk_callback_error':
|
|
134
|
+
raise SDKError(event['data']['message'])
|
|
135
|
+
elif self.on_event:
|
|
136
|
+
self.on_event(event)
|
|
137
|
+
except Exception as exc:
|
|
138
|
+
self._record_error(exc)
|
|
139
|
+
LOG.exception('Insight callback failed')
|
|
140
|
+
|
|
141
|
+
def close(self):
|
|
142
|
+
self._closed.set()
|
|
143
|
+
self._connected = False
|
|
144
|
+
self.transport.close()
|
|
145
|
+
if self._thread and threading.current_thread() is not self._thread:
|
|
146
|
+
self._thread.join(timeout=2)
|
|
147
|
+
# The server resets the native login before another compatibility lease.
|
|
148
|
+
|
|
149
|
+
def __enter__(self):
|
|
150
|
+
return self.connect()
|
|
151
|
+
|
|
152
|
+
def __exit__(self, *_):
|
|
153
|
+
self.close()
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def connect(marketservice=None, **connection):
|
|
157
|
+
"""Initialize module-level APIs; credentials remain in the server config."""
|
|
158
|
+
global _default
|
|
159
|
+
with _default_lock:
|
|
160
|
+
if _default is not None and not _default._closed.is_set():
|
|
161
|
+
raise RuntimeError('A default connection is already active; call close() first')
|
|
162
|
+
_default = Client(marketservice, **connection).connect()
|
|
163
|
+
return _default
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def close():
|
|
167
|
+
global _default
|
|
168
|
+
with _default_lock:
|
|
169
|
+
if _default is not None:
|
|
170
|
+
_default.close()
|
|
171
|
+
_default = None
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def call(module, name, kwargs):
|
|
175
|
+
with _default_lock:
|
|
176
|
+
client = _default
|
|
177
|
+
if client is None:
|
|
178
|
+
raise ConnectionError('Call insight_proxy.com.insight.connect() first')
|
|
179
|
+
return client._call(module, name, kwargs)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Override these callbacks exactly as with Insight 6.1.2."""
|
|
2
|
+
class market_service:
|
|
3
|
+
def __init__(self):
|
|
4
|
+
pass
|
|
5
|
+
|
|
6
|
+
def on_subscribe_tick(self, result):
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
def on_subscribe_kline(self, result):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
def on_subscribe_trans_and_order(self, result):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
def on_subscribe_derived(self, result):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
def on_subscribe_htsc_margin(self, result):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
def on_subscribe_news(self, result):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
def on_playback_tick(self, result):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
def on_playback_trans_and_order(self, result):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
def onPlaybackStatus(self, status):
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
def onPlaybackResponse(self, response):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
def onPlaybackControlResponse(self, response):
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
def on_query_response(self, result):
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
def on_query_margin_response(self, result):
|
|
43
|
+
pass
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Insight 6.1.2 signatures. Generated by tools/generate-compat.py."""
|
|
2
|
+
from ._session import call as _call
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def playback_tick(htsc_code=None, replay_time=None, fq='pre', timeout=600):
|
|
6
|
+
"""Socket equivalent of Insight 6.1.2 playback.playback_tick."""
|
|
7
|
+
return _call('playback', 'playback_tick', locals())
|
|
8
|
+
|
|
9
|
+
def playback_trans_and_order(htsc_code=None, replay_time=None, fq='pre', timeout=600):
|
|
10
|
+
"""Socket equivalent of Insight 6.1.2 playback.playback_trans_and_order."""
|
|
11
|
+
return _call('playback', 'playback_trans_and_order', locals())
|
|
12
|
+
|
|
13
|
+
__all__ = ['playback_tick', 'playback_trans_and_order']
|