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.
@@ -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,11 @@
1
+ from .client import XtquantRpcClient
2
+ from .defaults import configure_default_client, get_default_client, invoke
3
+ from . import xtdata
4
+
5
+ __all__ = [
6
+ "XtquantRpcClient",
7
+ "configure_default_client",
8
+ "get_default_client",
9
+ "invoke",
10
+ "xtdata",
11
+ ]
@@ -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}