xtquant-rpc-client 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_client-0.1.0/PKG-INFO +30 -0
- xtquant_rpc_client-0.1.0/README.md +18 -0
- xtquant_rpc_client-0.1.0/pyproject.toml +20 -0
- xtquant_rpc_client-0.1.0/src/xtquant_rpc/__init__.py +1 -0
- xtquant_rpc_client-0.1.0/src/xtquant_rpc/v1/__init__.py +1 -0
- xtquant_rpc_client-0.1.0/src/xtquant_rpc/v1/service_pb2.py +64 -0
- xtquant_rpc_client-0.1.0/src/xtquant_rpc/v1/service_pb2_grpc.py +97 -0
- xtquant_rpc_client-0.1.0/src/xtquant_rpc_client/__init__.py +11 -0
- xtquant_rpc_client-0.1.0/src/xtquant_rpc_client/client.py +80 -0
- xtquant_rpc_client-0.1.0/src/xtquant_rpc_client/defaults.py +25 -0
- xtquant_rpc_client-0.1.0/src/xtquant_rpc_client/exceptions.py +25 -0
- xtquant_rpc_client-0.1.0/src/xtquant_rpc_client/serialization.py +150 -0
- xtquant_rpc_client-0.1.0/src/xtquant_rpc_client/xtdata.py +32 -0
- xtquant_rpc_client-0.1.0/src/xtquant_rpc_client/xtdata_methods.py +81 -0
- xtquant_rpc_client-0.1.0/tests/conftest.py +13 -0
- xtquant_rpc_client-0.1.0/tests/test_serialization.py +52 -0
- xtquant_rpc_client-0.1.0/tests/test_xtdata_wrapper.py +30 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: xtquant-rpc-client
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Cross-platform gRPC client for remotely hosted 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
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# xtquant-rpc-client
|
|
14
|
+
|
|
15
|
+
Cross-platform Python client for remotely hosted `xtquant` domains.
|
|
16
|
+
|
|
17
|
+
Current scope:
|
|
18
|
+
|
|
19
|
+
- gRPC transport over protobuf
|
|
20
|
+
- token-based authentication
|
|
21
|
+
- `xtdata` synchronous read interface wrappers
|
|
22
|
+
|
|
23
|
+
Typical usage:
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from xtquant_rpc_client import configure_default_client, xtdata
|
|
27
|
+
|
|
28
|
+
configure_default_client(host="127.0.0.1", port=50051, token="your-token")
|
|
29
|
+
dates = xtdata.get_trading_dates("SH", count=5)
|
|
30
|
+
```
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# xtquant-rpc-client
|
|
2
|
+
|
|
3
|
+
Cross-platform Python client for remotely hosted `xtquant` domains.
|
|
4
|
+
|
|
5
|
+
Current scope:
|
|
6
|
+
|
|
7
|
+
- gRPC transport over protobuf
|
|
8
|
+
- token-based authentication
|
|
9
|
+
- `xtdata` synchronous read interface wrappers
|
|
10
|
+
|
|
11
|
+
Typical usage:
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from xtquant_rpc_client import configure_default_client, xtdata
|
|
15
|
+
|
|
16
|
+
configure_default_client(host="127.0.0.1", port=50051, token="your-token")
|
|
17
|
+
dates = xtdata.get_trading_dates("SH", count=5)
|
|
18
|
+
```
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27.0"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "xtquant-rpc-client"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Cross-platform gRPC client for remotely hosted 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
|
+
]
|
|
18
|
+
|
|
19
|
+
[tool.hatch.build.targets.wheel]
|
|
20
|
+
packages = ["src/xtquant_rpc_client", "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,80 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from threading import Lock
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import grpc
|
|
8
|
+
|
|
9
|
+
from xtquant_rpc.v1 import service_pb2, service_pb2_grpc
|
|
10
|
+
|
|
11
|
+
from .exceptions import (
|
|
12
|
+
AuthenticationError,
|
|
13
|
+
DomainNotSupportedError,
|
|
14
|
+
MethodNotSupportedError,
|
|
15
|
+
RemoteInvocationError,
|
|
16
|
+
)
|
|
17
|
+
from .serialization import from_proto_value, to_proto_value
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(slots=True)
|
|
21
|
+
class ConnectionConfig:
|
|
22
|
+
host: str
|
|
23
|
+
port: int
|
|
24
|
+
token: str
|
|
25
|
+
timeout: float = 30.0
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class XtquantRpcClient:
|
|
29
|
+
def __init__(self, host: str, port: int, token: str, timeout: float = 30.0) -> None:
|
|
30
|
+
self.config = ConnectionConfig(host=host, port=port, token=token, timeout=timeout)
|
|
31
|
+
self._lock = Lock()
|
|
32
|
+
self._channel: grpc.Channel | None = None
|
|
33
|
+
self._stub: service_pb2_grpc.XtquantRpcServiceStub | None = None
|
|
34
|
+
|
|
35
|
+
def _ensure_stub(self) -> service_pb2_grpc.XtquantRpcServiceStub:
|
|
36
|
+
if self._stub is None:
|
|
37
|
+
with self._lock:
|
|
38
|
+
if self._stub is None:
|
|
39
|
+
target = f"{self.config.host}:{self.config.port}"
|
|
40
|
+
self._channel = grpc.insecure_channel(target)
|
|
41
|
+
self._stub = service_pb2_grpc.XtquantRpcServiceStub(self._channel)
|
|
42
|
+
return self._stub
|
|
43
|
+
|
|
44
|
+
def close(self) -> None:
|
|
45
|
+
if self._channel is not None:
|
|
46
|
+
self._channel.close()
|
|
47
|
+
self._channel = None
|
|
48
|
+
self._stub = None
|
|
49
|
+
|
|
50
|
+
def invoke(self, domain: str, method: str, *args: Any, **kwargs: Any) -> Any:
|
|
51
|
+
request = service_pb2.InvokeRequest(
|
|
52
|
+
domain=domain,
|
|
53
|
+
method=method,
|
|
54
|
+
request_id="",
|
|
55
|
+
auth_token=self.config.token,
|
|
56
|
+
)
|
|
57
|
+
request.args.extend(to_proto_value(arg) for arg in args)
|
|
58
|
+
for key, value in kwargs.items():
|
|
59
|
+
request.kwargs[key].CopyFrom(to_proto_value(value))
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
response = self._ensure_stub().Invoke(request, timeout=self.config.timeout)
|
|
63
|
+
except grpc.RpcError as exc:
|
|
64
|
+
if exc.code() == grpc.StatusCode.UNAUTHENTICATED:
|
|
65
|
+
raise AuthenticationError(exc.details() or "authentication failed") from exc
|
|
66
|
+
raise RemoteInvocationError(exc.code().name, exc.details() or "gRPC transport error") from exc
|
|
67
|
+
|
|
68
|
+
if response.ok:
|
|
69
|
+
return from_proto_value(response.result)
|
|
70
|
+
|
|
71
|
+
error_type = response.error_type or "RemoteInvocationError"
|
|
72
|
+
message = response.error_message or "remote invocation failed"
|
|
73
|
+
traceback_text = response.traceback
|
|
74
|
+
if error_type == "DomainNotSupportedError":
|
|
75
|
+
raise DomainNotSupportedError(message)
|
|
76
|
+
if error_type == "MethodNotSupportedError":
|
|
77
|
+
raise MethodNotSupportedError(message)
|
|
78
|
+
if error_type == "AuthenticationError":
|
|
79
|
+
raise AuthenticationError(message)
|
|
80
|
+
raise RemoteInvocationError(error_type, message, traceback_text)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from .client import XtquantRpcClient
|
|
6
|
+
|
|
7
|
+
_default_client: XtquantRpcClient | None = None
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def configure_default_client(host: str, port: int, token: str, timeout: float = 30.0) -> XtquantRpcClient:
|
|
11
|
+
global _default_client
|
|
12
|
+
if _default_client is not None:
|
|
13
|
+
_default_client.close()
|
|
14
|
+
_default_client = XtquantRpcClient(host=host, port=port, token=token, timeout=timeout)
|
|
15
|
+
return _default_client
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_default_client() -> XtquantRpcClient:
|
|
19
|
+
if _default_client is None:
|
|
20
|
+
raise RuntimeError("Default xtquant RPC client is not configured. Call configure_default_client(...) first.")
|
|
21
|
+
return _default_client
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def invoke(domain: str, method: str, *args: Any, **kwargs: Any) -> Any:
|
|
25
|
+
return get_default_client().invoke(domain, method, *args, **kwargs)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
class XtquantRpcError(Exception):
|
|
2
|
+
"""Base exception raised by the xtquant RPC client."""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class AuthenticationError(XtquantRpcError):
|
|
6
|
+
"""Raised when the server rejects the provided token."""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DomainNotSupportedError(XtquantRpcError):
|
|
10
|
+
"""Raised when the requested xtquant domain is not registered on the server."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MethodNotSupportedError(XtquantRpcError):
|
|
14
|
+
"""Raised when a domain does not expose the requested method."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class RemoteInvocationError(XtquantRpcError):
|
|
18
|
+
"""Raised when the remote domain execution failed."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, error_type: str, error_message: str, traceback_text: str = "") -> None:
|
|
21
|
+
self.error_type = error_type
|
|
22
|
+
self.error_message = error_message
|
|
23
|
+
self.traceback_text = traceback_text
|
|
24
|
+
message = f"{error_type}: {error_message}" if error_type else error_message
|
|
25
|
+
super().__init__(message)
|
|
@@ -0,0 +1,150 @@
|
|
|
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
|
+
column_name = frame.columns[0]
|
|
25
|
+
series = frame.iloc[:, 0]
|
|
26
|
+
if name:
|
|
27
|
+
series.name = name
|
|
28
|
+
elif column_name is not None:
|
|
29
|
+
series.name = column_name
|
|
30
|
+
return series
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _dataframe_to_arrow_bytes(frame: pd.DataFrame) -> bytes:
|
|
34
|
+
table = pa.Table.from_pandas(frame, preserve_index=True)
|
|
35
|
+
sink = pa.BufferOutputStream()
|
|
36
|
+
with pa.ipc.new_stream(sink, table.schema) as writer:
|
|
37
|
+
writer.write_table(table)
|
|
38
|
+
return sink.getvalue().to_pybytes()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _dataframe_from_arrow_bytes(payload: bytes) -> pd.DataFrame:
|
|
42
|
+
table = pa.ipc.open_stream(payload).read_all()
|
|
43
|
+
return table.to_pandas()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def to_proto_value(value: Any) -> service_pb2.Value:
|
|
47
|
+
proto = service_pb2.Value()
|
|
48
|
+
if value is None:
|
|
49
|
+
proto.null_value.SetInParent()
|
|
50
|
+
return proto
|
|
51
|
+
if isinstance(value, bool):
|
|
52
|
+
proto.bool_value = value
|
|
53
|
+
return proto
|
|
54
|
+
if isinstance(value, np.bool_):
|
|
55
|
+
proto.bool_value = bool(value)
|
|
56
|
+
return proto
|
|
57
|
+
if isinstance(value, int) and not isinstance(value, bool):
|
|
58
|
+
if value >= 0:
|
|
59
|
+
proto.uint_value = value
|
|
60
|
+
else:
|
|
61
|
+
proto.int_value = value
|
|
62
|
+
return proto
|
|
63
|
+
if isinstance(value, np.integer):
|
|
64
|
+
int_value = int(value)
|
|
65
|
+
if int_value >= 0:
|
|
66
|
+
proto.uint_value = int_value
|
|
67
|
+
else:
|
|
68
|
+
proto.int_value = int_value
|
|
69
|
+
return proto
|
|
70
|
+
if isinstance(value, float):
|
|
71
|
+
proto.float_value = value
|
|
72
|
+
return proto
|
|
73
|
+
if isinstance(value, np.floating):
|
|
74
|
+
proto.float_value = float(value)
|
|
75
|
+
return proto
|
|
76
|
+
if isinstance(value, str):
|
|
77
|
+
proto.string_value = value
|
|
78
|
+
return proto
|
|
79
|
+
if isinstance(value, bytes):
|
|
80
|
+
proto.bytes_value = value
|
|
81
|
+
return proto
|
|
82
|
+
if isinstance(value, datetime):
|
|
83
|
+
proto.datetime_value.iso_datetime = value.isoformat()
|
|
84
|
+
return proto
|
|
85
|
+
if isinstance(value, date):
|
|
86
|
+
proto.date_value.iso_date = value.isoformat()
|
|
87
|
+
return proto
|
|
88
|
+
if isinstance(value, np.ndarray):
|
|
89
|
+
proto.ndarray_value.shape.extend(int(dim) for dim in value.shape)
|
|
90
|
+
proto.ndarray_value.dtype = value.dtype.str
|
|
91
|
+
proto.ndarray_value.data = value.tobytes(order="C")
|
|
92
|
+
return proto
|
|
93
|
+
if isinstance(value, pd.DataFrame):
|
|
94
|
+
proto.dataframe_value.arrow_ipc = _dataframe_to_arrow_bytes(value)
|
|
95
|
+
return proto
|
|
96
|
+
if isinstance(value, pd.Series):
|
|
97
|
+
proto.series_value.arrow_ipc = _series_to_arrow_bytes(value)
|
|
98
|
+
proto.series_value.name = "" if value.name is None else str(value.name)
|
|
99
|
+
return proto
|
|
100
|
+
if isinstance(value, tuple):
|
|
101
|
+
value = list(value)
|
|
102
|
+
if isinstance(value, list):
|
|
103
|
+
proto.list_value.items.extend(to_proto_value(item) for item in value)
|
|
104
|
+
return proto
|
|
105
|
+
if isinstance(value, dict):
|
|
106
|
+
for key, item in value.items():
|
|
107
|
+
entry = proto.map_value.entries.add()
|
|
108
|
+
entry.key.CopyFrom(to_proto_value(key))
|
|
109
|
+
entry.value.CopyFrom(to_proto_value(item))
|
|
110
|
+
return proto
|
|
111
|
+
raise TypeError(f"Unsupported value type for protobuf serialization: {type(value)!r}")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def from_proto_value(value: service_pb2.Value) -> Any:
|
|
115
|
+
kind = value.WhichOneof("kind")
|
|
116
|
+
if kind == "null_value" or kind is None:
|
|
117
|
+
return None
|
|
118
|
+
if kind == "bool_value":
|
|
119
|
+
return value.bool_value
|
|
120
|
+
if kind == "int_value":
|
|
121
|
+
return value.int_value
|
|
122
|
+
if kind == "uint_value":
|
|
123
|
+
return value.uint_value
|
|
124
|
+
if kind == "float_value":
|
|
125
|
+
return value.float_value
|
|
126
|
+
if kind == "string_value":
|
|
127
|
+
return value.string_value
|
|
128
|
+
if kind == "bytes_value":
|
|
129
|
+
return value.bytes_value
|
|
130
|
+
if kind == "date_value":
|
|
131
|
+
return date.fromisoformat(value.date_value.iso_date)
|
|
132
|
+
if kind == "datetime_value":
|
|
133
|
+
return datetime.fromisoformat(value.datetime_value.iso_datetime)
|
|
134
|
+
if kind == "list_value":
|
|
135
|
+
return [from_proto_value(item) for item in value.list_value.items]
|
|
136
|
+
if kind == "map_value":
|
|
137
|
+
result = {}
|
|
138
|
+
for entry in value.map_value.entries:
|
|
139
|
+
result[from_proto_value(entry.key)] = from_proto_value(entry.value)
|
|
140
|
+
return result
|
|
141
|
+
if kind == "ndarray_value":
|
|
142
|
+
dtype = np.dtype(value.ndarray_value.dtype)
|
|
143
|
+
data = np.frombuffer(value.ndarray_value.data, dtype=dtype)
|
|
144
|
+
return data.reshape(tuple(value.ndarray_value.shape))
|
|
145
|
+
if kind == "dataframe_value":
|
|
146
|
+
return _dataframe_from_arrow_bytes(value.dataframe_value.arrow_ipc)
|
|
147
|
+
if kind == "series_value":
|
|
148
|
+
name = value.series_value.name or None
|
|
149
|
+
return _series_from_arrow_bytes(value.series_value.arrow_ipc, name)
|
|
150
|
+
raise TypeError(f"Unsupported protobuf value kind: {kind!r}")
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from .defaults import invoke
|
|
6
|
+
from .xtdata_methods import SUPPORTED_METHOD_SIGNATURES
|
|
7
|
+
|
|
8
|
+
__all__ = sorted(SUPPORTED_METHOD_SIGNATURES)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _invoke_method(method: str, local_values: dict[str, Any]) -> Any:
|
|
12
|
+
kwargs = dict(local_values)
|
|
13
|
+
extra_kwargs = kwargs.pop("kwargs", {})
|
|
14
|
+
kwargs.update(extra_kwargs)
|
|
15
|
+
return invoke("xtdata", method, **kwargs)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _make_wrapper(method: str, signature_text: str):
|
|
19
|
+
namespace: dict[str, Any] = {}
|
|
20
|
+
source = (
|
|
21
|
+
f"def {method}{signature_text}:\n"
|
|
22
|
+
f" return _invoke_method('{method}', locals())\n"
|
|
23
|
+
)
|
|
24
|
+
exec(source, {"_invoke_method": _invoke_method}, namespace)
|
|
25
|
+
func = namespace[method]
|
|
26
|
+
func.__module__ = __name__
|
|
27
|
+
func.__doc__ = f"Remote xtdata proxy for `{method}`."
|
|
28
|
+
return func
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
for _method_name, _signature_text in SUPPORTED_METHOD_SIGNATURES.items():
|
|
32
|
+
globals()[_method_name] = _make_wrapper(_method_name, _signature_text)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
SUPPORTED_METHOD_SIGNATURES = {
|
|
2
|
+
"get_data_dir": "()",
|
|
3
|
+
"get_field_list": "(metaid)",
|
|
4
|
+
"get_stock_list_in_sector": "(sector_name, real_timetag=-1)",
|
|
5
|
+
"get_index_weight": "(index_code)",
|
|
6
|
+
"get_financial_data": "(stock_list, table_list=[], start_time='', end_time='', report_type='report_time')",
|
|
7
|
+
"get_financial_data_ori": "(stock_list, table_list=[], start_time='', end_time='', report_type='report_time')",
|
|
8
|
+
"get_market_data_ori": "(field_list=[], stock_list=[], period='1d', start_time='', end_time='', count=-1, dividend_type='none', fill_data=True, enable_read_from_server=True, data_dir=None)",
|
|
9
|
+
"get_market_data": "(field_list=[], stock_list=[], period='1d', start_time='', end_time='', count=-1, dividend_type='none', fill_data=True)",
|
|
10
|
+
"get_market_data_ex_ori": "(field_list=[], stock_list=[], period='1d', start_time='', end_time='', count=-1, dividend_type='none', fill_data=True, enable_read_from_server=True, data_dir=None)",
|
|
11
|
+
"get_market_data_ex": "(field_list=[], stock_list=[], period='1d', start_time='', end_time='', count=-1, dividend_type='none', fill_data=True)",
|
|
12
|
+
"get_local_data": "(field_list=[], stock_list=[], period='1d', start_time='', end_time='', count=-1, dividend_type='none', fill_data=True, data_dir=None)",
|
|
13
|
+
"get_l2_quote": "(field_list=[], stock_code='', start_time='', end_time='', count=-1)",
|
|
14
|
+
"get_l2_order": "(field_list=[], stock_code='', start_time='', end_time='', count=-1)",
|
|
15
|
+
"get_l2_transaction": "(field_list=[], stock_code='', start_time='', end_time='', count=-1)",
|
|
16
|
+
"get_divid_factors": "(stock_code, start_time='', end_time='')",
|
|
17
|
+
"getDividFactors": "(stock_code, date)",
|
|
18
|
+
"get_main_contract": "(code_market, start_time='', end_time='')",
|
|
19
|
+
"get_sec_main_contract": "(code_market, start_time='', end_time='')",
|
|
20
|
+
"datetime_to_timetag": "(datetime, format='%Y%m%d%H%M%S')",
|
|
21
|
+
"timetag_to_datetime": "(timetag, format)",
|
|
22
|
+
"timetagToDateTime": "(timetag, format)",
|
|
23
|
+
"get_trading_dates": "(market, start_time='', end_time='', count=-1)",
|
|
24
|
+
"get_full_tick": "(code_list)",
|
|
25
|
+
"get_l2thousand_queue": "(stock_code, gear_num=None, price=None)",
|
|
26
|
+
"get_transactioncount": "(code_list)",
|
|
27
|
+
"get_fullspeed_orderbook": "(code_list)",
|
|
28
|
+
"get_sector_list": "()",
|
|
29
|
+
"get_sector_info": "(sector_name='')",
|
|
30
|
+
"get_instrument_detail": "(stock_code, iscomplete=False)",
|
|
31
|
+
"get_instrument_detail_list": "(stock_list, iscomplete=False)",
|
|
32
|
+
"download_index_weight": "()",
|
|
33
|
+
"download_history_contracts": "(incrementally=True)",
|
|
34
|
+
"download_history_data": "(stock_code, period, start_time='', end_time='', incrementally=None)",
|
|
35
|
+
"download_financial_data": "(stock_list, table_list=[], start_time='', end_time='', incrementally=None)",
|
|
36
|
+
"get_instrument_type": "(stock_code, variety_list=None)",
|
|
37
|
+
"download_sector_data": "()",
|
|
38
|
+
"download_holiday_data": "(incrementally=True)",
|
|
39
|
+
"get_holidays": "()",
|
|
40
|
+
"get_market_last_trade_date": "(market)",
|
|
41
|
+
"get_trading_calendar": "(market, start_time='', end_time='')",
|
|
42
|
+
"is_stock_type": "(stock, tag)",
|
|
43
|
+
"download_cb_data": "()",
|
|
44
|
+
"get_cb_info": "(stockcode)",
|
|
45
|
+
"get_option_detail_data": "(optioncode)",
|
|
46
|
+
"get_option_undl_data": "(undl_code_ref)",
|
|
47
|
+
"get_option_list": "(undl_code, dedate, opttype='', isavailavle=False)",
|
|
48
|
+
"get_his_option_list": "(undl_code, dedate)",
|
|
49
|
+
"get_his_option_list_batch": "(undl_code, start_time='', end_time='')",
|
|
50
|
+
"get_ipo_info": "(start_time='', end_time='')",
|
|
51
|
+
"get_markets": "()",
|
|
52
|
+
"get_wp_market_list": "()",
|
|
53
|
+
"get_his_st_data": "(stock_code)",
|
|
54
|
+
"get_period_list": "()",
|
|
55
|
+
"get_formulas": "()",
|
|
56
|
+
"get_quote_server_config": "()",
|
|
57
|
+
"get_quote_server_status": "()",
|
|
58
|
+
"get_etf_info": "()",
|
|
59
|
+
"download_etf_info": "()",
|
|
60
|
+
"download_his_st_data": "()",
|
|
61
|
+
"get_hk_broker_dict": "()",
|
|
62
|
+
"get_broker_queue_data": "(stock_list=[], start_time='', end_time='', count=-1, show_broker_name=False)",
|
|
63
|
+
"get_full_kline": "(field_list=[], stock_list=[], period='1m', start_time='', end_time='', count=1, dividend_type='none', fill_data=True)",
|
|
64
|
+
"download_tabular_data": "(stock_list, period, start_time='', end_time='', incrementally=None, download_type='validationbypage', source='')",
|
|
65
|
+
"get_trading_contract_list": "(stockcode, date=None)",
|
|
66
|
+
"get_trading_period": "(stock_code)",
|
|
67
|
+
"get_kline_trading_period": "(stock_code)",
|
|
68
|
+
"get_all_trading_periods": "()",
|
|
69
|
+
"get_all_kline_trading_periods": "()",
|
|
70
|
+
"get_authorized_market_list": "()",
|
|
71
|
+
"compute_coming_trading_calendar": "(market, start_time='', end_time='')",
|
|
72
|
+
"get_tabular_formula": "(codes, fields, period, start_time, end_time, count=-1, dividend_type='none', **kwargs)",
|
|
73
|
+
"bnd_get_conversion_price": "(stock_code, start_time='', end_time='')",
|
|
74
|
+
"bnd_get_call_info": "(stock_code, start_time='', end_time='')",
|
|
75
|
+
"bnd_get_put_info": "(stock_code, start_time='', end_time='')",
|
|
76
|
+
"bnd_get_amount_change": "(stock_code, start_time='', end_time='')",
|
|
77
|
+
"get_tabular_data": "(field_list=[], stock_list=[], period='1d', start_time='', end_time='', count=-1, dividend_type='none', fill_data=True)",
|
|
78
|
+
"get_order_rank": "(code, order_time, order_type, order_price, order_volume, order_left_volume)",
|
|
79
|
+
"get_current_connect_sub_info": "()",
|
|
80
|
+
"get_all_sub_info": "()",
|
|
81
|
+
}
|
|
@@ -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,52 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import date, datetime
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
from xtquant_rpc_client.serialization import from_proto_value, to_proto_value
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_round_trip_nested_python_values() -> None:
|
|
12
|
+
value = {
|
|
13
|
+
"none": None,
|
|
14
|
+
"bool": True,
|
|
15
|
+
"int": -7,
|
|
16
|
+
"uint": 9,
|
|
17
|
+
"float": 1.25,
|
|
18
|
+
"str": "abc",
|
|
19
|
+
"bytes": b"xyz",
|
|
20
|
+
"date": date(2026, 3, 21),
|
|
21
|
+
"datetime": datetime(2026, 3, 21, 9, 30, 15),
|
|
22
|
+
"list": [1, "two", False],
|
|
23
|
+
99: {"nested": "ok"},
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
restored = from_proto_value(to_proto_value(value))
|
|
27
|
+
|
|
28
|
+
assert restored["none"] is None
|
|
29
|
+
assert restored["bool"] is True
|
|
30
|
+
assert restored["int"] == -7
|
|
31
|
+
assert restored["uint"] == 9
|
|
32
|
+
assert restored["float"] == 1.25
|
|
33
|
+
assert restored["str"] == "abc"
|
|
34
|
+
assert restored["bytes"] == b"xyz"
|
|
35
|
+
assert restored["date"] == date(2026, 3, 21)
|
|
36
|
+
assert restored["datetime"] == datetime(2026, 3, 21, 9, 30, 15)
|
|
37
|
+
assert restored["list"] == [1, "two", False]
|
|
38
|
+
assert restored[99] == {"nested": "ok"}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_round_trip_dataframe_series_and_ndarray() -> None:
|
|
42
|
+
frame = pd.DataFrame({"symbol": ["000001.SZ", "600000.SH"], "close": [12.3, 9.8]})
|
|
43
|
+
series = pd.Series([1, 2, 3], name="weights", index=["a", "b", "c"])
|
|
44
|
+
array = np.arange(6, dtype=np.int64).reshape(2, 3)
|
|
45
|
+
|
|
46
|
+
restored_frame = from_proto_value(to_proto_value(frame))
|
|
47
|
+
restored_series = from_proto_value(to_proto_value(series))
|
|
48
|
+
restored_array = from_proto_value(to_proto_value(array))
|
|
49
|
+
|
|
50
|
+
pd.testing.assert_frame_equal(restored_frame, frame)
|
|
51
|
+
pd.testing.assert_series_equal(restored_series, series)
|
|
52
|
+
assert np.array_equal(restored_array, array)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
|
|
5
|
+
from xtquant_rpc_client import xtdata
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_wrapper_signature_is_exposed() -> None:
|
|
9
|
+
signature = inspect.signature(xtdata.get_market_data)
|
|
10
|
+
assert "field_list" in signature.parameters
|
|
11
|
+
assert signature.parameters["period"].default == "1d"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_wrapper_dispatches_via_default_invoke(monkeypatch) -> None:
|
|
15
|
+
captured: dict[str, object] = {}
|
|
16
|
+
|
|
17
|
+
def fake_invoke(domain, method, **kwargs):
|
|
18
|
+
captured["domain"] = domain
|
|
19
|
+
captured["method"] = method
|
|
20
|
+
captured["kwargs"] = kwargs
|
|
21
|
+
return "ok"
|
|
22
|
+
|
|
23
|
+
monkeypatch.setattr("xtquant_rpc_client.xtdata.invoke", fake_invoke)
|
|
24
|
+
|
|
25
|
+
result = xtdata.get_stock_list_in_sector("沪深300", real_timetag=123)
|
|
26
|
+
|
|
27
|
+
assert result == "ok"
|
|
28
|
+
assert captured["domain"] == "xtdata"
|
|
29
|
+
assert captured["method"] == "get_stock_list_in_sector"
|
|
30
|
+
assert captured["kwargs"] == {"sector_name": "沪深300", "real_timetag": 123}
|