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.
@@ -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,6 @@
1
+ class DomainNotSupportedError(Exception):
2
+ """Raised when a requested xtquant domain is not registered."""
3
+
4
+
5
+ class MethodNotSupportedError(Exception):
6
+ """Raised when a requested xtquant method is not allowed."""
@@ -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", [], {})