xtquant-rpc-server 0.1.0__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.
- xtquant_rpc_server-0.1.0/PKG-INFO +28 -0
- xtquant_rpc_server-0.1.0/README.md +15 -0
- xtquant_rpc_server-0.1.0/pyproject.toml +24 -0
- xtquant_rpc_server-0.1.0/src/xtquant_rpc/__init__.py +1 -0
- xtquant_rpc_server-0.1.0/src/xtquant_rpc/v1/__init__.py +1 -0
- xtquant_rpc_server-0.1.0/src/xtquant_rpc/v1/service_pb2.py +64 -0
- xtquant_rpc_server-0.1.0/src/xtquant_rpc/v1/service_pb2_grpc.py +97 -0
- xtquant_rpc_server-0.1.0/src/xtquant_rpc_server/__init__.py +10 -0
- xtquant_rpc_server-0.1.0/src/xtquant_rpc_server/domain_registry.py +24 -0
- xtquant_rpc_server-0.1.0/src/xtquant_rpc_server/domains_xtdata.py +112 -0
- xtquant_rpc_server-0.1.0/src/xtquant_rpc_server/exceptions.py +6 -0
- xtquant_rpc_server-0.1.0/src/xtquant_rpc_server/interceptors.py +26 -0
- xtquant_rpc_server-0.1.0/src/xtquant_rpc_server/main.py +34 -0
- xtquant_rpc_server-0.1.0/src/xtquant_rpc_server/serialization.py +147 -0
- xtquant_rpc_server-0.1.0/src/xtquant_rpc_server/service.py +41 -0
- xtquant_rpc_server-0.1.0/tests/conftest.py +13 -0
- xtquant_rpc_server-0.1.0/tests/test_server_roundtrip.py +48 -0
- xtquant_rpc_server-0.1.0/tests/test_xtdata_domain.py +37 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: xtquant-rpc-server
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Windows gRPC host for xtquant domains.
|
|
5
|
+
Requires-Python: <3.14,>=3.11
|
|
6
|
+
Requires-Dist: grpcio>=1.78.0
|
|
7
|
+
Requires-Dist: numpy>=2.4.3
|
|
8
|
+
Requires-Dist: pandas>=3.0.1
|
|
9
|
+
Requires-Dist: protobuf>=6.33.6
|
|
10
|
+
Requires-Dist: pyarrow>=23.0.1
|
|
11
|
+
Requires-Dist: xtquant>=250516.1.1
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# xtquant-rpc-server
|
|
15
|
+
|
|
16
|
+
Windows gRPC host for `xtquant` capability domains.
|
|
17
|
+
|
|
18
|
+
Current scope:
|
|
19
|
+
|
|
20
|
+
- token-protected gRPC server
|
|
21
|
+
- `xtdata` domain registration
|
|
22
|
+
- explicit whitelist for synchronous read-oriented interfaces
|
|
23
|
+
|
|
24
|
+
Typical usage:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
xtquant-rpc-server --host 0.0.0.0 --port 50051 --token your-token
|
|
28
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# xtquant-rpc-server
|
|
2
|
+
|
|
3
|
+
Windows gRPC host for `xtquant` capability domains.
|
|
4
|
+
|
|
5
|
+
Current scope:
|
|
6
|
+
|
|
7
|
+
- token-protected gRPC server
|
|
8
|
+
- `xtdata` domain registration
|
|
9
|
+
- explicit whitelist for synchronous read-oriented interfaces
|
|
10
|
+
|
|
11
|
+
Typical usage:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
xtquant-rpc-server --host 0.0.0.0 --port 50051 --token your-token
|
|
15
|
+
```
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27.0"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "xtquant-rpc-server"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Windows gRPC host for xtquant domains."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11,<3.14"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"grpcio>=1.78.0",
|
|
13
|
+
"numpy>=2.4.3",
|
|
14
|
+
"pandas>=3.0.1",
|
|
15
|
+
"protobuf>=6.33.6",
|
|
16
|
+
"pyarrow>=23.0.1",
|
|
17
|
+
"xtquant>=250516.1.1",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
xtquant-rpc-server = "xtquant_rpc_server.main:main"
|
|
22
|
+
|
|
23
|
+
[tool.hatch.build.targets.wheel]
|
|
24
|
+
packages = ["src/xtquant_rpc_server", "src/xtquant_rpc"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Generated xtquant RPC protobuf modules."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""v1 protobuf definitions for xtquant RPC."""
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
3
|
+
# NO CHECKED-IN PROTOBUF GENCODE
|
|
4
|
+
# source: xtquant_rpc/v1/service.proto
|
|
5
|
+
# Protobuf Python Version: 6.31.1
|
|
6
|
+
"""Generated protocol buffer code."""
|
|
7
|
+
from google.protobuf import descriptor as _descriptor
|
|
8
|
+
from google.protobuf import descriptor_pool as _descriptor_pool
|
|
9
|
+
from google.protobuf import runtime_version as _runtime_version
|
|
10
|
+
from google.protobuf import symbol_database as _symbol_database
|
|
11
|
+
from google.protobuf.internal import builder as _builder
|
|
12
|
+
_runtime_version.ValidateProtobufRuntimeVersion(
|
|
13
|
+
_runtime_version.Domain.PUBLIC,
|
|
14
|
+
6,
|
|
15
|
+
31,
|
|
16
|
+
1,
|
|
17
|
+
'',
|
|
18
|
+
'xtquant_rpc/v1/service.proto'
|
|
19
|
+
)
|
|
20
|
+
# @@protoc_insertion_point(imports)
|
|
21
|
+
|
|
22
|
+
_sym_db = _symbol_database.Default()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1cxtquant_rpc/v1/service.proto\x12\x0extquant.rpc.v1\"\x0b\n\tNullValue\"\x1d\n\tDateValue\x12\x10\n\x08iso_date\x18\x01 \x01(\t\"%\n\rDateTimeValue\x12\x14\n\x0ciso_datetime\x18\x01 \x01(\t\"1\n\tListValue\x12$\n\x05items\x18\x01 \x03(\x0b\x32\x15.xtquant.rpc.v1.Value\"T\n\x08MapEntry\x12\"\n\x03key\x18\x01 \x01(\x0b\x32\x15.xtquant.rpc.v1.Value\x12$\n\x05value\x18\x02 \x01(\x0b\x32\x15.xtquant.rpc.v1.Value\"5\n\x08MapValue\x12)\n\x07\x65ntries\x18\x01 \x03(\x0b\x32\x18.xtquant.rpc.v1.MapEntry\":\n\x0cNdarrayValue\x12\r\n\x05shape\x18\x01 \x03(\x03\x12\r\n\x05\x64type\x18\x02 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x03 \x01(\x0c\"#\n\x0e\x44\x61taFrameValue\x12\x11\n\tarrow_ipc\x18\x01 \x01(\x0c\".\n\x0bSeriesValue\x12\x11\n\tarrow_ipc\x18\x01 \x01(\x0c\x12\x0c\n\x04name\x18\x02 \x01(\t\"\xb8\x04\n\x05Value\x12/\n\nnull_value\x18\x01 \x01(\x0b\x32\x19.xtquant.rpc.v1.NullValueH\x00\x12\x14\n\nbool_value\x18\x02 \x01(\x08H\x00\x12\x13\n\tint_value\x18\x03 \x01(\x03H\x00\x12\x14\n\nuint_value\x18\x04 \x01(\x04H\x00\x12\x15\n\x0b\x66loat_value\x18\x05 \x01(\x01H\x00\x12\x16\n\x0cstring_value\x18\x06 \x01(\tH\x00\x12\x15\n\x0b\x62ytes_value\x18\x07 \x01(\x0cH\x00\x12/\n\nlist_value\x18\x08 \x01(\x0b\x32\x19.xtquant.rpc.v1.ListValueH\x00\x12-\n\tmap_value\x18\t \x01(\x0b\x32\x18.xtquant.rpc.v1.MapValueH\x00\x12/\n\ndate_value\x18\n \x01(\x0b\x32\x19.xtquant.rpc.v1.DateValueH\x00\x12\x37\n\x0e\x64\x61tetime_value\x18\x0b \x01(\x0b\x32\x1d.xtquant.rpc.v1.DateTimeValueH\x00\x12\x35\n\rndarray_value\x18\x0c \x01(\x0b\x32\x1c.xtquant.rpc.v1.NdarrayValueH\x00\x12\x39\n\x0f\x64\x61taframe_value\x18\r \x01(\x0b\x32\x1e.xtquant.rpc.v1.DataFrameValueH\x00\x12\x33\n\x0cseries_value\x18\x0e \x01(\x0b\x32\x1b.xtquant.rpc.v1.SeriesValueH\x00\x42\x06\n\x04kind\"\xfd\x01\n\rInvokeRequest\x12\x0e\n\x06\x64omain\x18\x01 \x01(\t\x12\x0e\n\x06method\x18\x02 \x01(\t\x12#\n\x04\x61rgs\x18\x03 \x03(\x0b\x32\x15.xtquant.rpc.v1.Value\x12\x39\n\x06kwargs\x18\x04 \x03(\x0b\x32).xtquant.rpc.v1.InvokeRequest.KwargsEntry\x12\x12\n\nrequest_id\x18\x05 \x01(\t\x12\x12\n\nauth_token\x18\x06 \x01(\t\x1a\x44\n\x0bKwargsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12$\n\x05value\x18\x02 \x01(\x0b\x32\x15.xtquant.rpc.v1.Value:\x02\x38\x01\"\x81\x01\n\x0eInvokeResponse\x12\n\n\x02ok\x18\x01 \x01(\x08\x12%\n\x06result\x18\x02 \x01(\x0b\x32\x15.xtquant.rpc.v1.Value\x12\x12\n\nerror_type\x18\x03 \x01(\t\x12\x15\n\rerror_message\x18\x04 \x01(\t\x12\x11\n\ttraceback\x18\x05 \x01(\t2\\\n\x11XtquantRpcService\x12G\n\x06Invoke\x12\x1d.xtquant.rpc.v1.InvokeRequest\x1a\x1e.xtquant.rpc.v1.InvokeResponseb\x06proto3')
|
|
28
|
+
|
|
29
|
+
_globals = globals()
|
|
30
|
+
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
|
31
|
+
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'xtquant_rpc.v1.service_pb2', _globals)
|
|
32
|
+
if not _descriptor._USE_C_DESCRIPTORS:
|
|
33
|
+
DESCRIPTOR._loaded_options = None
|
|
34
|
+
_globals['_INVOKEREQUEST_KWARGSENTRY']._loaded_options = None
|
|
35
|
+
_globals['_INVOKEREQUEST_KWARGSENTRY']._serialized_options = b'8\001'
|
|
36
|
+
_globals['_NULLVALUE']._serialized_start=48
|
|
37
|
+
_globals['_NULLVALUE']._serialized_end=59
|
|
38
|
+
_globals['_DATEVALUE']._serialized_start=61
|
|
39
|
+
_globals['_DATEVALUE']._serialized_end=90
|
|
40
|
+
_globals['_DATETIMEVALUE']._serialized_start=92
|
|
41
|
+
_globals['_DATETIMEVALUE']._serialized_end=129
|
|
42
|
+
_globals['_LISTVALUE']._serialized_start=131
|
|
43
|
+
_globals['_LISTVALUE']._serialized_end=180
|
|
44
|
+
_globals['_MAPENTRY']._serialized_start=182
|
|
45
|
+
_globals['_MAPENTRY']._serialized_end=266
|
|
46
|
+
_globals['_MAPVALUE']._serialized_start=268
|
|
47
|
+
_globals['_MAPVALUE']._serialized_end=321
|
|
48
|
+
_globals['_NDARRAYVALUE']._serialized_start=323
|
|
49
|
+
_globals['_NDARRAYVALUE']._serialized_end=381
|
|
50
|
+
_globals['_DATAFRAMEVALUE']._serialized_start=383
|
|
51
|
+
_globals['_DATAFRAMEVALUE']._serialized_end=418
|
|
52
|
+
_globals['_SERIESVALUE']._serialized_start=420
|
|
53
|
+
_globals['_SERIESVALUE']._serialized_end=466
|
|
54
|
+
_globals['_VALUE']._serialized_start=469
|
|
55
|
+
_globals['_VALUE']._serialized_end=1037
|
|
56
|
+
_globals['_INVOKEREQUEST']._serialized_start=1040
|
|
57
|
+
_globals['_INVOKEREQUEST']._serialized_end=1293
|
|
58
|
+
_globals['_INVOKEREQUEST_KWARGSENTRY']._serialized_start=1225
|
|
59
|
+
_globals['_INVOKEREQUEST_KWARGSENTRY']._serialized_end=1293
|
|
60
|
+
_globals['_INVOKERESPONSE']._serialized_start=1296
|
|
61
|
+
_globals['_INVOKERESPONSE']._serialized_end=1425
|
|
62
|
+
_globals['_XTQUANTRPCSERVICE']._serialized_start=1427
|
|
63
|
+
_globals['_XTQUANTRPCSERVICE']._serialized_end=1519
|
|
64
|
+
# @@protoc_insertion_point(module_scope)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
|
|
2
|
+
"""Client and server classes corresponding to protobuf-defined services."""
|
|
3
|
+
import grpc
|
|
4
|
+
import warnings
|
|
5
|
+
|
|
6
|
+
from xtquant_rpc.v1 import service_pb2 as xtquant__rpc_dot_v1_dot_service__pb2
|
|
7
|
+
|
|
8
|
+
GRPC_GENERATED_VERSION = '1.78.0'
|
|
9
|
+
GRPC_VERSION = grpc.__version__
|
|
10
|
+
_version_not_supported = False
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
from grpc._utilities import first_version_is_lower
|
|
14
|
+
_version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
|
|
15
|
+
except ImportError:
|
|
16
|
+
_version_not_supported = True
|
|
17
|
+
|
|
18
|
+
if _version_not_supported:
|
|
19
|
+
raise RuntimeError(
|
|
20
|
+
f'The grpc package installed is at version {GRPC_VERSION},'
|
|
21
|
+
+ ' but the generated code in xtquant_rpc/v1/service_pb2_grpc.py depends on'
|
|
22
|
+
+ f' grpcio>={GRPC_GENERATED_VERSION}.'
|
|
23
|
+
+ f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
|
|
24
|
+
+ f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class XtquantRpcServiceStub(object):
|
|
29
|
+
"""Missing associated documentation comment in .proto file."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, channel):
|
|
32
|
+
"""Constructor.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
channel: A grpc.Channel.
|
|
36
|
+
"""
|
|
37
|
+
self.Invoke = channel.unary_unary(
|
|
38
|
+
'/xtquant.rpc.v1.XtquantRpcService/Invoke',
|
|
39
|
+
request_serializer=xtquant__rpc_dot_v1_dot_service__pb2.InvokeRequest.SerializeToString,
|
|
40
|
+
response_deserializer=xtquant__rpc_dot_v1_dot_service__pb2.InvokeResponse.FromString,
|
|
41
|
+
_registered_method=True)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class XtquantRpcServiceServicer(object):
|
|
45
|
+
"""Missing associated documentation comment in .proto file."""
|
|
46
|
+
|
|
47
|
+
def Invoke(self, request, context):
|
|
48
|
+
"""Missing associated documentation comment in .proto file."""
|
|
49
|
+
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
|
50
|
+
context.set_details('Method not implemented!')
|
|
51
|
+
raise NotImplementedError('Method not implemented!')
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def add_XtquantRpcServiceServicer_to_server(servicer, server):
|
|
55
|
+
rpc_method_handlers = {
|
|
56
|
+
'Invoke': grpc.unary_unary_rpc_method_handler(
|
|
57
|
+
servicer.Invoke,
|
|
58
|
+
request_deserializer=xtquant__rpc_dot_v1_dot_service__pb2.InvokeRequest.FromString,
|
|
59
|
+
response_serializer=xtquant__rpc_dot_v1_dot_service__pb2.InvokeResponse.SerializeToString,
|
|
60
|
+
),
|
|
61
|
+
}
|
|
62
|
+
generic_handler = grpc.method_handlers_generic_handler(
|
|
63
|
+
'xtquant.rpc.v1.XtquantRpcService', rpc_method_handlers)
|
|
64
|
+
server.add_generic_rpc_handlers((generic_handler,))
|
|
65
|
+
server.add_registered_method_handlers('xtquant.rpc.v1.XtquantRpcService', rpc_method_handlers)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# This class is part of an EXPERIMENTAL API.
|
|
69
|
+
class XtquantRpcService(object):
|
|
70
|
+
"""Missing associated documentation comment in .proto file."""
|
|
71
|
+
|
|
72
|
+
@staticmethod
|
|
73
|
+
def Invoke(request,
|
|
74
|
+
target,
|
|
75
|
+
options=(),
|
|
76
|
+
channel_credentials=None,
|
|
77
|
+
call_credentials=None,
|
|
78
|
+
insecure=False,
|
|
79
|
+
compression=None,
|
|
80
|
+
wait_for_ready=None,
|
|
81
|
+
timeout=None,
|
|
82
|
+
metadata=None):
|
|
83
|
+
return grpc.experimental.unary_unary(
|
|
84
|
+
request,
|
|
85
|
+
target,
|
|
86
|
+
'/xtquant.rpc.v1.XtquantRpcService/Invoke',
|
|
87
|
+
xtquant__rpc_dot_v1_dot_service__pb2.InvokeRequest.SerializeToString,
|
|
88
|
+
xtquant__rpc_dot_v1_dot_service__pb2.InvokeResponse.FromString,
|
|
89
|
+
options,
|
|
90
|
+
channel_credentials,
|
|
91
|
+
insecure,
|
|
92
|
+
call_credentials,
|
|
93
|
+
compression,
|
|
94
|
+
wait_for_ready,
|
|
95
|
+
timeout,
|
|
96
|
+
metadata,
|
|
97
|
+
_registered_method=True)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from .domain_registry import DomainRegistry
|
|
2
|
+
from .domains_xtdata import SUPPORTED_METHODS, XtdataDomainHandler
|
|
3
|
+
from .service import create_grpc_server
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"DomainRegistry",
|
|
7
|
+
"SUPPORTED_METHODS",
|
|
8
|
+
"XtdataDomainHandler",
|
|
9
|
+
"create_grpc_server",
|
|
10
|
+
]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Protocol
|
|
4
|
+
|
|
5
|
+
from .exceptions import DomainNotSupportedError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DomainHandler(Protocol):
|
|
9
|
+
def invoke(self, method: str, args: list[Any], kwargs: dict[str, Any]) -> Any:
|
|
10
|
+
...
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DomainRegistry:
|
|
14
|
+
def __init__(self) -> None:
|
|
15
|
+
self._handlers: dict[str, DomainHandler] = {}
|
|
16
|
+
|
|
17
|
+
def register(self, domain: str, handler: DomainHandler) -> None:
|
|
18
|
+
self._handlers[domain] = handler
|
|
19
|
+
|
|
20
|
+
def invoke(self, domain: str, method: str, args: list[Any], kwargs: dict[str, Any]) -> Any:
|
|
21
|
+
handler = self._handlers.get(domain)
|
|
22
|
+
if handler is None:
|
|
23
|
+
raise DomainNotSupportedError(f"Unsupported domain: {domain}")
|
|
24
|
+
return handler.invoke(method, args, kwargs)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from .exceptions import MethodNotSupportedError
|
|
6
|
+
|
|
7
|
+
SUPPORTED_METHODS = {
|
|
8
|
+
"get_data_dir",
|
|
9
|
+
"get_field_list",
|
|
10
|
+
"get_stock_list_in_sector",
|
|
11
|
+
"get_index_weight",
|
|
12
|
+
"get_financial_data",
|
|
13
|
+
"get_financial_data_ori",
|
|
14
|
+
"get_market_data_ori",
|
|
15
|
+
"get_market_data",
|
|
16
|
+
"get_market_data_ex_ori",
|
|
17
|
+
"get_market_data_ex",
|
|
18
|
+
"get_local_data",
|
|
19
|
+
"get_l2_quote",
|
|
20
|
+
"get_l2_order",
|
|
21
|
+
"get_l2_transaction",
|
|
22
|
+
"get_divid_factors",
|
|
23
|
+
"getDividFactors",
|
|
24
|
+
"get_main_contract",
|
|
25
|
+
"get_sec_main_contract",
|
|
26
|
+
"datetime_to_timetag",
|
|
27
|
+
"timetag_to_datetime",
|
|
28
|
+
"timetagToDateTime",
|
|
29
|
+
"get_trading_dates",
|
|
30
|
+
"get_full_tick",
|
|
31
|
+
"get_l2thousand_queue",
|
|
32
|
+
"get_transactioncount",
|
|
33
|
+
"get_fullspeed_orderbook",
|
|
34
|
+
"get_sector_list",
|
|
35
|
+
"get_sector_info",
|
|
36
|
+
"get_instrument_detail",
|
|
37
|
+
"get_instrument_detail_list",
|
|
38
|
+
"download_index_weight",
|
|
39
|
+
"download_history_contracts",
|
|
40
|
+
"download_history_data",
|
|
41
|
+
"download_financial_data",
|
|
42
|
+
"get_instrument_type",
|
|
43
|
+
"download_sector_data",
|
|
44
|
+
"download_holiday_data",
|
|
45
|
+
"get_holidays",
|
|
46
|
+
"get_market_last_trade_date",
|
|
47
|
+
"get_trading_calendar",
|
|
48
|
+
"is_stock_type",
|
|
49
|
+
"download_cb_data",
|
|
50
|
+
"get_cb_info",
|
|
51
|
+
"get_option_detail_data",
|
|
52
|
+
"get_option_undl_data",
|
|
53
|
+
"get_option_list",
|
|
54
|
+
"get_his_option_list",
|
|
55
|
+
"get_his_option_list_batch",
|
|
56
|
+
"get_ipo_info",
|
|
57
|
+
"get_markets",
|
|
58
|
+
"get_wp_market_list",
|
|
59
|
+
"get_his_st_data",
|
|
60
|
+
"get_period_list",
|
|
61
|
+
"get_formulas",
|
|
62
|
+
"get_quote_server_config",
|
|
63
|
+
"get_quote_server_status",
|
|
64
|
+
"get_etf_info",
|
|
65
|
+
"download_etf_info",
|
|
66
|
+
"download_his_st_data",
|
|
67
|
+
"get_hk_broker_dict",
|
|
68
|
+
"get_broker_queue_data",
|
|
69
|
+
"get_full_kline",
|
|
70
|
+
"download_tabular_data",
|
|
71
|
+
"get_trading_contract_list",
|
|
72
|
+
"get_trading_period",
|
|
73
|
+
"get_kline_trading_period",
|
|
74
|
+
"get_all_trading_periods",
|
|
75
|
+
"get_all_kline_trading_periods",
|
|
76
|
+
"get_authorized_market_list",
|
|
77
|
+
"compute_coming_trading_calendar",
|
|
78
|
+
"get_tabular_formula",
|
|
79
|
+
"bnd_get_conversion_price",
|
|
80
|
+
"bnd_get_call_info",
|
|
81
|
+
"bnd_get_put_info",
|
|
82
|
+
"bnd_get_amount_change",
|
|
83
|
+
"get_tabular_data",
|
|
84
|
+
"get_order_rank",
|
|
85
|
+
"get_current_connect_sub_info",
|
|
86
|
+
"get_all_sub_info",
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class XtdataDomainHandler:
|
|
91
|
+
def __init__(self, xtdata_module: Any | None = None, xtdata_ip: str = "", xtdata_port: int | None = None) -> None:
|
|
92
|
+
self._xtdata = xtdata_module
|
|
93
|
+
self._xtdata_ip = xtdata_ip
|
|
94
|
+
self._xtdata_port = xtdata_port
|
|
95
|
+
self._is_connected = False
|
|
96
|
+
|
|
97
|
+
def _load_xtdata(self):
|
|
98
|
+
if self._xtdata is None:
|
|
99
|
+
from xtquant import xtdata
|
|
100
|
+
|
|
101
|
+
self._xtdata = xtdata
|
|
102
|
+
if not self._is_connected and (self._xtdata_ip or self._xtdata_port is not None):
|
|
103
|
+
self._xtdata.connect(ip=self._xtdata_ip, port=self._xtdata_port)
|
|
104
|
+
self._is_connected = True
|
|
105
|
+
return self._xtdata
|
|
106
|
+
|
|
107
|
+
def invoke(self, method: str, args: list[Any], kwargs: dict[str, Any]) -> Any:
|
|
108
|
+
if method not in SUPPORTED_METHODS:
|
|
109
|
+
raise MethodNotSupportedError(f"Unsupported xtdata method: {method}")
|
|
110
|
+
xtdata = self._load_xtdata()
|
|
111
|
+
target = getattr(xtdata, method)
|
|
112
|
+
return target(*args, **kwargs)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Callable
|
|
4
|
+
|
|
5
|
+
import grpc
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AuthenticationInterceptor(grpc.ServerInterceptor):
|
|
9
|
+
def __init__(self, expected_token: str) -> None:
|
|
10
|
+
self._expected_token = expected_token
|
|
11
|
+
|
|
12
|
+
def intercept_service(self, continuation: Callable, handler_call_details: grpc.HandlerCallDetails):
|
|
13
|
+
handler = continuation(handler_call_details)
|
|
14
|
+
if handler is None or handler.unary_unary is None:
|
|
15
|
+
return handler
|
|
16
|
+
|
|
17
|
+
def unary_unary(request, context):
|
|
18
|
+
if request.auth_token != self._expected_token:
|
|
19
|
+
context.abort(grpc.StatusCode.UNAUTHENTICATED, "invalid xtquant RPC token")
|
|
20
|
+
return handler.unary_unary(request, context)
|
|
21
|
+
|
|
22
|
+
return grpc.unary_unary_rpc_method_handler(
|
|
23
|
+
unary_unary,
|
|
24
|
+
request_deserializer=handler.request_deserializer,
|
|
25
|
+
response_serializer=handler.response_serializer,
|
|
26
|
+
)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
from .domain_registry import DomainRegistry
|
|
6
|
+
from .domains_xtdata import XtdataDomainHandler
|
|
7
|
+
from .service import create_grpc_server
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
11
|
+
parser = argparse.ArgumentParser(description="Run the xtquant gRPC server.")
|
|
12
|
+
parser.add_argument("--host", default="0.0.0.0")
|
|
13
|
+
parser.add_argument("--port", type=int, default=50051)
|
|
14
|
+
parser.add_argument("--token", required=True)
|
|
15
|
+
parser.add_argument("--xtdata-ip", default="")
|
|
16
|
+
parser.add_argument("--xtdata-port", type=int, default=None)
|
|
17
|
+
return parser
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def main() -> None:
|
|
21
|
+
args = build_parser().parse_args()
|
|
22
|
+
registry = DomainRegistry()
|
|
23
|
+
registry.register(
|
|
24
|
+
"xtdata",
|
|
25
|
+
XtdataDomainHandler(
|
|
26
|
+
xtdata_ip=args.xtdata_ip,
|
|
27
|
+
xtdata_port=args.xtdata_port,
|
|
28
|
+
),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
server = create_grpc_server(registry=registry, token=args.token)
|
|
32
|
+
server.add_insecure_port(f"{args.host}:{args.port}")
|
|
33
|
+
server.start()
|
|
34
|
+
server.wait_for_termination()
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import date, datetime
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
import pyarrow as pa
|
|
9
|
+
|
|
10
|
+
from xtquant_rpc.v1 import service_pb2
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _series_to_arrow_bytes(series: pd.Series) -> bytes:
|
|
14
|
+
table = pa.Table.from_pandas(series.to_frame(name=series.name), preserve_index=True)
|
|
15
|
+
sink = pa.BufferOutputStream()
|
|
16
|
+
with pa.ipc.new_stream(sink, table.schema) as writer:
|
|
17
|
+
writer.write_table(table)
|
|
18
|
+
return sink.getvalue().to_pybytes()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _series_from_arrow_bytes(payload: bytes, name: str | None) -> pd.Series:
|
|
22
|
+
table = pa.ipc.open_stream(payload).read_all()
|
|
23
|
+
frame = table.to_pandas()
|
|
24
|
+
series = frame.iloc[:, 0]
|
|
25
|
+
if name:
|
|
26
|
+
series.name = name
|
|
27
|
+
return series
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _dataframe_to_arrow_bytes(frame: pd.DataFrame) -> bytes:
|
|
31
|
+
table = pa.Table.from_pandas(frame, preserve_index=True)
|
|
32
|
+
sink = pa.BufferOutputStream()
|
|
33
|
+
with pa.ipc.new_stream(sink, table.schema) as writer:
|
|
34
|
+
writer.write_table(table)
|
|
35
|
+
return sink.getvalue().to_pybytes()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _dataframe_from_arrow_bytes(payload: bytes) -> pd.DataFrame:
|
|
39
|
+
table = pa.ipc.open_stream(payload).read_all()
|
|
40
|
+
return table.to_pandas()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def to_proto_value(value: Any) -> service_pb2.Value:
|
|
44
|
+
proto = service_pb2.Value()
|
|
45
|
+
if value is None:
|
|
46
|
+
proto.null_value.SetInParent()
|
|
47
|
+
return proto
|
|
48
|
+
if isinstance(value, bool):
|
|
49
|
+
proto.bool_value = value
|
|
50
|
+
return proto
|
|
51
|
+
if isinstance(value, np.bool_):
|
|
52
|
+
proto.bool_value = bool(value)
|
|
53
|
+
return proto
|
|
54
|
+
if isinstance(value, int) and not isinstance(value, bool):
|
|
55
|
+
if value >= 0:
|
|
56
|
+
proto.uint_value = value
|
|
57
|
+
else:
|
|
58
|
+
proto.int_value = value
|
|
59
|
+
return proto
|
|
60
|
+
if isinstance(value, np.integer):
|
|
61
|
+
int_value = int(value)
|
|
62
|
+
if int_value >= 0:
|
|
63
|
+
proto.uint_value = int_value
|
|
64
|
+
else:
|
|
65
|
+
proto.int_value = int_value
|
|
66
|
+
return proto
|
|
67
|
+
if isinstance(value, float):
|
|
68
|
+
proto.float_value = value
|
|
69
|
+
return proto
|
|
70
|
+
if isinstance(value, np.floating):
|
|
71
|
+
proto.float_value = float(value)
|
|
72
|
+
return proto
|
|
73
|
+
if isinstance(value, str):
|
|
74
|
+
proto.string_value = value
|
|
75
|
+
return proto
|
|
76
|
+
if isinstance(value, bytes):
|
|
77
|
+
proto.bytes_value = value
|
|
78
|
+
return proto
|
|
79
|
+
if isinstance(value, datetime):
|
|
80
|
+
proto.datetime_value.iso_datetime = value.isoformat()
|
|
81
|
+
return proto
|
|
82
|
+
if isinstance(value, date):
|
|
83
|
+
proto.date_value.iso_date = value.isoformat()
|
|
84
|
+
return proto
|
|
85
|
+
if isinstance(value, np.ndarray):
|
|
86
|
+
proto.ndarray_value.shape.extend(int(dim) for dim in value.shape)
|
|
87
|
+
proto.ndarray_value.dtype = value.dtype.str
|
|
88
|
+
proto.ndarray_value.data = value.tobytes(order="C")
|
|
89
|
+
return proto
|
|
90
|
+
if isinstance(value, pd.DataFrame):
|
|
91
|
+
proto.dataframe_value.arrow_ipc = _dataframe_to_arrow_bytes(value)
|
|
92
|
+
return proto
|
|
93
|
+
if isinstance(value, pd.Series):
|
|
94
|
+
proto.series_value.arrow_ipc = _series_to_arrow_bytes(value)
|
|
95
|
+
proto.series_value.name = "" if value.name is None else str(value.name)
|
|
96
|
+
return proto
|
|
97
|
+
if isinstance(value, tuple):
|
|
98
|
+
value = list(value)
|
|
99
|
+
if isinstance(value, list):
|
|
100
|
+
proto.list_value.items.extend(to_proto_value(item) for item in value)
|
|
101
|
+
return proto
|
|
102
|
+
if isinstance(value, dict):
|
|
103
|
+
for key, item in value.items():
|
|
104
|
+
entry = proto.map_value.entries.add()
|
|
105
|
+
entry.key.CopyFrom(to_proto_value(key))
|
|
106
|
+
entry.value.CopyFrom(to_proto_value(item))
|
|
107
|
+
return proto
|
|
108
|
+
raise TypeError(f"Unsupported value type for protobuf serialization: {type(value)!r}")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def from_proto_value(value: service_pb2.Value) -> Any:
|
|
112
|
+
kind = value.WhichOneof("kind")
|
|
113
|
+
if kind == "null_value" or kind is None:
|
|
114
|
+
return None
|
|
115
|
+
if kind == "bool_value":
|
|
116
|
+
return value.bool_value
|
|
117
|
+
if kind == "int_value":
|
|
118
|
+
return value.int_value
|
|
119
|
+
if kind == "uint_value":
|
|
120
|
+
return value.uint_value
|
|
121
|
+
if kind == "float_value":
|
|
122
|
+
return value.float_value
|
|
123
|
+
if kind == "string_value":
|
|
124
|
+
return value.string_value
|
|
125
|
+
if kind == "bytes_value":
|
|
126
|
+
return value.bytes_value
|
|
127
|
+
if kind == "date_value":
|
|
128
|
+
return date.fromisoformat(value.date_value.iso_date)
|
|
129
|
+
if kind == "datetime_value":
|
|
130
|
+
return datetime.fromisoformat(value.datetime_value.iso_datetime)
|
|
131
|
+
if kind == "list_value":
|
|
132
|
+
return [from_proto_value(item) for item in value.list_value.items]
|
|
133
|
+
if kind == "map_value":
|
|
134
|
+
result = {}
|
|
135
|
+
for entry in value.map_value.entries:
|
|
136
|
+
result[from_proto_value(entry.key)] = from_proto_value(entry.value)
|
|
137
|
+
return result
|
|
138
|
+
if kind == "ndarray_value":
|
|
139
|
+
dtype = np.dtype(value.ndarray_value.dtype)
|
|
140
|
+
data = np.frombuffer(value.ndarray_value.data, dtype=dtype)
|
|
141
|
+
return data.reshape(tuple(value.ndarray_value.shape))
|
|
142
|
+
if kind == "dataframe_value":
|
|
143
|
+
return _dataframe_from_arrow_bytes(value.dataframe_value.arrow_ipc)
|
|
144
|
+
if kind == "series_value":
|
|
145
|
+
name = value.series_value.name or None
|
|
146
|
+
return _series_from_arrow_bytes(value.series_value.arrow_ipc, name)
|
|
147
|
+
raise TypeError(f"Unsupported protobuf value kind: {kind!r}")
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import traceback
|
|
4
|
+
from concurrent import futures
|
|
5
|
+
|
|
6
|
+
import grpc
|
|
7
|
+
|
|
8
|
+
from xtquant_rpc.v1 import service_pb2, service_pb2_grpc
|
|
9
|
+
|
|
10
|
+
from .domain_registry import DomainRegistry
|
|
11
|
+
from .interceptors import AuthenticationInterceptor
|
|
12
|
+
from .serialization import from_proto_value, to_proto_value
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RpcService(service_pb2_grpc.XtquantRpcServiceServicer):
|
|
16
|
+
def __init__(self, registry: DomainRegistry) -> None:
|
|
17
|
+
self._registry = registry
|
|
18
|
+
|
|
19
|
+
def Invoke(self, request: service_pb2.InvokeRequest, context: grpc.ServicerContext) -> service_pb2.InvokeResponse:
|
|
20
|
+
args = [from_proto_value(item) for item in request.args]
|
|
21
|
+
kwargs = {key: from_proto_value(value) for key, value in request.kwargs.items()}
|
|
22
|
+
try:
|
|
23
|
+
result = self._registry.invoke(request.domain, request.method, args, kwargs)
|
|
24
|
+
except Exception as exc:
|
|
25
|
+
return service_pb2.InvokeResponse(
|
|
26
|
+
ok=False,
|
|
27
|
+
error_type=type(exc).__name__,
|
|
28
|
+
error_message=str(exc),
|
|
29
|
+
traceback=traceback.format_exc(),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
return service_pb2.InvokeResponse(ok=True, result=to_proto_value(result))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def create_grpc_server(registry: DomainRegistry, token: str, max_workers: int = 16) -> grpc.Server:
|
|
36
|
+
server = grpc.server(
|
|
37
|
+
futures.ThreadPoolExecutor(max_workers=max_workers),
|
|
38
|
+
interceptors=[AuthenticationInterceptor(token)],
|
|
39
|
+
)
|
|
40
|
+
service_pb2_grpc.add_XtquantRpcServiceServicer_to_server(RpcService(registry), server)
|
|
41
|
+
return server
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
ROOT = Path(__file__).resolve().parents[3]
|
|
8
|
+
CLIENT_SRC = ROOT / "packages" / "xtquant-rpc-client" / "src"
|
|
9
|
+
SERVER_SRC = ROOT / "packages" / "xtquant-rpc-server" / "src"
|
|
10
|
+
|
|
11
|
+
for path in [str(CLIENT_SRC), str(SERVER_SRC)]:
|
|
12
|
+
if path not in sys.path:
|
|
13
|
+
sys.path.insert(0, path)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from xtquant_rpc_client import XtquantRpcClient
|
|
6
|
+
from xtquant_rpc_client.exceptions import AuthenticationError, DomainNotSupportedError
|
|
7
|
+
from xtquant_rpc_server.domain_registry import DomainRegistry
|
|
8
|
+
from xtquant_rpc_server.service import create_grpc_server
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class EchoDomain:
|
|
12
|
+
def invoke(self, method, args, kwargs):
|
|
13
|
+
if method == "mirror":
|
|
14
|
+
return {"args": args, "kwargs": kwargs}
|
|
15
|
+
raise RuntimeError("unexpected method")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_grpc_roundtrip_and_authentication() -> None:
|
|
19
|
+
registry = DomainRegistry()
|
|
20
|
+
registry.register("echo", EchoDomain())
|
|
21
|
+
server = create_grpc_server(registry, token="secret")
|
|
22
|
+
port = server.add_insecure_port("127.0.0.1:0")
|
|
23
|
+
server.start()
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
client = XtquantRpcClient(host="127.0.0.1", port=port, token="secret", timeout=5.0)
|
|
27
|
+
result = client.invoke("echo", "mirror", 1, 2, key="value")
|
|
28
|
+
assert result == {"args": [1, 2], "kwargs": {"key": "value"}}
|
|
29
|
+
|
|
30
|
+
bad_client = XtquantRpcClient(host="127.0.0.1", port=port, token="wrong", timeout=5.0)
|
|
31
|
+
with pytest.raises(AuthenticationError):
|
|
32
|
+
bad_client.invoke("echo", "mirror")
|
|
33
|
+
finally:
|
|
34
|
+
server.stop(grace=None)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_unknown_domain_maps_to_client_exception() -> None:
|
|
38
|
+
registry = DomainRegistry()
|
|
39
|
+
server = create_grpc_server(registry, token="secret")
|
|
40
|
+
port = server.add_insecure_port("127.0.0.1:0")
|
|
41
|
+
server.start()
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
client = XtquantRpcClient(host="127.0.0.1", port=port, token="secret", timeout=5.0)
|
|
45
|
+
with pytest.raises(DomainNotSupportedError):
|
|
46
|
+
client.invoke("missing", "mirror")
|
|
47
|
+
finally:
|
|
48
|
+
server.stop(grace=None)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from xtquant_rpc_server.domains_xtdata import XtdataDomainHandler
|
|
6
|
+
from xtquant_rpc_server.exceptions import MethodNotSupportedError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FakeXtdata:
|
|
10
|
+
def __init__(self) -> None:
|
|
11
|
+
self.calls = []
|
|
12
|
+
|
|
13
|
+
def connect(self, ip="", port=None):
|
|
14
|
+
self.calls.append(("connect", ip, port))
|
|
15
|
+
return True
|
|
16
|
+
|
|
17
|
+
def get_trading_dates(self, market, start_time="", end_time="", count=-1):
|
|
18
|
+
self.calls.append(("get_trading_dates", market, start_time, end_time, count))
|
|
19
|
+
return [20260320, 20260321]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_xtdata_handler_calls_allowed_method() -> None:
|
|
23
|
+
backend = FakeXtdata()
|
|
24
|
+
handler = XtdataDomainHandler(xtdata_module=backend, xtdata_ip="127.0.0.1", xtdata_port=58610)
|
|
25
|
+
|
|
26
|
+
result = handler.invoke("get_trading_dates", ["SH"], {"count": 2})
|
|
27
|
+
|
|
28
|
+
assert result == [20260320, 20260321]
|
|
29
|
+
assert backend.calls[0] == ("connect", "127.0.0.1", 58610)
|
|
30
|
+
assert backend.calls[1] == ("get_trading_dates", "SH", "", "", 2)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_xtdata_handler_rejects_unsupported_method() -> None:
|
|
34
|
+
handler = XtdataDomainHandler(xtdata_module=FakeXtdata())
|
|
35
|
+
|
|
36
|
+
with pytest.raises(MethodNotSupportedError):
|
|
37
|
+
handler.invoke("subscribe_quote", [], {})
|