panda-data 0.1.0__py3-none-any.whl
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.
- panda_data/__init__.py +68 -0
- panda_data/client.py +372 -0
- panda_data/config/__init__.py +177 -0
- panda_data/core/__init__.py +2 -0
- panda_data/core/service.py +49 -0
- panda_data/exceptions.py +140 -0
- panda_data/readers/__init__.py +1 -0
- panda_data/readers/financial_and_factors_reader.py +852 -0
- panda_data/readers/future_reader.py +206 -0
- panda_data/readers/init_token.py +121 -0
- panda_data/readers/market_reader.py +512 -0
- panda_data/readers/market_reference_reader.py +1455 -0
- panda_data/readers/trading_tools_reader.py +242 -0
- panda_data/test.py +32 -0
- panda_data/transport/__init__.py +6 -0
- panda_data/transport/http.py +1155 -0
- panda_data/utils/common_utils.py +49 -0
- panda_data/utils/param_check_utils.py +1409 -0
- panda_data-0.1.0.dist-info/METADATA +438 -0
- panda_data-0.1.0.dist-info/RECORD +22 -0
- panda_data-0.1.0.dist-info/WHEEL +5 -0
- panda_data-0.1.0.dist-info/top_level.txt +1 -0
panda_data/__init__.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""panda_data SDK public interface."""
|
|
2
|
+
|
|
3
|
+
from .client import get_client, get_factory
|
|
4
|
+
from .readers import (
|
|
5
|
+
financial_and_factors_reader,
|
|
6
|
+
future_reader,
|
|
7
|
+
market_reader,
|
|
8
|
+
market_reference_reader,
|
|
9
|
+
trading_tools_reader,
|
|
10
|
+
init_token
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
init_token = init_token.init_token
|
|
14
|
+
|
|
15
|
+
# market
|
|
16
|
+
get_market_data = market_reader.get_market_data
|
|
17
|
+
get_market_min_data = market_reader.get_market_min_data
|
|
18
|
+
get_future_tick = market_reader.get_future_tick
|
|
19
|
+
get_hk_daily = market_reader.get_hk_daily
|
|
20
|
+
get_us_daily = market_reader.get_us_daily
|
|
21
|
+
get_hk_transaction = market_reader.get_hk_transaction
|
|
22
|
+
|
|
23
|
+
# market_reference
|
|
24
|
+
get_stock_detail = market_reference_reader.get_stock_detail
|
|
25
|
+
get_index_detail = market_reference_reader.get_index_detail
|
|
26
|
+
get_concept_list = market_reference_reader.get_concept_list
|
|
27
|
+
get_concept_constituents = market_reference_reader.get_concept_constituents
|
|
28
|
+
get_industry_detail = market_reference_reader.get_industry_detail
|
|
29
|
+
get_industry_constituents = market_reference_reader.get_industry_constituents
|
|
30
|
+
get_stock_industry = market_reference_reader.get_stock_industry
|
|
31
|
+
get_index_indicator = market_reference_reader.get_index_indicator
|
|
32
|
+
get_index_weights = market_reference_reader.get_index_weights
|
|
33
|
+
get_lhb_list = market_reference_reader.get_lhb_list
|
|
34
|
+
get_lhb_detail = market_reference_reader.get_lhb_detail
|
|
35
|
+
get_repurchase = market_reference_reader.get_repurchase
|
|
36
|
+
get_margin = market_reference_reader.get_margin
|
|
37
|
+
get_hsgt_hold = market_reference_reader.get_hsgt_hold
|
|
38
|
+
get_investor_activity = market_reference_reader.get_investor_activity
|
|
39
|
+
get_restricted_list = market_reference_reader.get_restricted_list
|
|
40
|
+
get_holder_count = market_reference_reader.get_holder_count
|
|
41
|
+
get_top_holders = market_reference_reader.get_top_holders
|
|
42
|
+
get_block_trade = market_reference_reader.get_block_trade
|
|
43
|
+
get_share_float = market_reference_reader.get_share_float
|
|
44
|
+
|
|
45
|
+
# financial_and_factors
|
|
46
|
+
get_fina_forecast = financial_and_factors_reader.get_fina_forecast
|
|
47
|
+
get_fina_performance = financial_and_factors_reader.get_fina_performance
|
|
48
|
+
get_fina_reports = financial_and_factors_reader.get_fina_reports
|
|
49
|
+
get_financial_statement = financial_and_factors_reader.get_financial_statement
|
|
50
|
+
get_financial_statement_daily = financial_and_factors_reader.get_financial_statement_daily
|
|
51
|
+
get_audit_opinion = financial_and_factors_reader.get_audit_opinion
|
|
52
|
+
get_factor = financial_and_factors_reader.get_factor
|
|
53
|
+
get_adj_factor = financial_and_factors_reader.get_adj_factor
|
|
54
|
+
|
|
55
|
+
# future
|
|
56
|
+
get_future_detail = future_reader.get_future_detail
|
|
57
|
+
get_future_market_post = future_reader.get_future_market_post
|
|
58
|
+
get_future_dominant = future_reader.get_future_dominant
|
|
59
|
+
|
|
60
|
+
# trading_tools
|
|
61
|
+
get_trade_cal = trading_tools_reader.get_trade_cal
|
|
62
|
+
get_prev_trade_date = trading_tools_reader.get_prev_trade_date
|
|
63
|
+
get_last_trade_date = trading_tools_reader.get_last_trade_date
|
|
64
|
+
get_stock_status_change = trading_tools_reader.get_stock_status_change
|
|
65
|
+
get_trade_list = trading_tools_reader.get_trade_list
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
__all__ = [name for name in globals().keys() if name.startswith('get_')] + ['init_token', 'get_client', 'get_factory']
|
panda_data/client.py
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from threading import Lock
|
|
6
|
+
from typing import Any, Dict, Iterable, Optional, Type, TypeVar
|
|
7
|
+
from urllib.parse import urlparse
|
|
8
|
+
|
|
9
|
+
import pandas as pd
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# 模块级函数,用于多进程处理(必须定义在类外部)
|
|
13
|
+
def _build_dataframe_chunk(chunk_data):
|
|
14
|
+
"""在子进程中构建 DataFrame(模块级函数,可被多进程序列化)"""
|
|
15
|
+
return pd.DataFrame(chunk_data)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from panda_data.config import get_config
|
|
19
|
+
from panda_data.exceptions import ServiceError
|
|
20
|
+
from panda_data.transport.http import (
|
|
21
|
+
HTTPClient,
|
|
22
|
+
HTTPClientConfig,
|
|
23
|
+
)
|
|
24
|
+
from panda_data.utils.common_utils import find_project_root
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class ClientConfig:
|
|
29
|
+
base_url: str # HTTP base URL, 例如: "http://localhost:8080"
|
|
30
|
+
username: str = field(default="")
|
|
31
|
+
password: str = field(default="")
|
|
32
|
+
timeout: float = 30.0
|
|
33
|
+
retry: int = 3
|
|
34
|
+
verify_ssl: bool = True
|
|
35
|
+
proxy_type: Optional[str] = None
|
|
36
|
+
proxy_host: Optional[str] = None
|
|
37
|
+
proxy_port: Optional[int] = None
|
|
38
|
+
proxy_username: Optional[str] = None
|
|
39
|
+
proxy_password: Optional[str] = None
|
|
40
|
+
use_gzip: bool = False
|
|
41
|
+
data_field: Iterable[str] | None = field(default_factory=lambda: ("data",))
|
|
42
|
+
abnormal_endpoint: str = field(default="/abnormal")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _extract_nested(data: Any, path: Optional[Iterable[str]]) -> Any:
|
|
46
|
+
"""Utility to extract a nested value from a dictionary-like payload."""
|
|
47
|
+
if data is None or path is None:
|
|
48
|
+
return data
|
|
49
|
+
|
|
50
|
+
# 如果 data 是列表,说明可能是流式响应直接返回的数据列表
|
|
51
|
+
# 这种情况下,直接返回列表,不进行嵌套提取
|
|
52
|
+
if isinstance(data, list):
|
|
53
|
+
return data
|
|
54
|
+
|
|
55
|
+
# 确保 current 是字典类型
|
|
56
|
+
if not isinstance(data, dict):
|
|
57
|
+
# 如果不是字典也不是列表,可能是其他类型,直接返回
|
|
58
|
+
return data
|
|
59
|
+
|
|
60
|
+
current = data
|
|
61
|
+
# 检查错误码(只有当 current 是字典时才检查)
|
|
62
|
+
if isinstance(current, dict):
|
|
63
|
+
code = current.get("code")
|
|
64
|
+
if code is not None and code != 200 and code != '200':
|
|
65
|
+
raise ServiceError(f"服务返回错误:[错误码 {code} :{current.get('message', '未知错误')}]")
|
|
66
|
+
|
|
67
|
+
for key in path:
|
|
68
|
+
if not isinstance(current, dict):
|
|
69
|
+
return None
|
|
70
|
+
current = current.get(key)
|
|
71
|
+
if current is None:
|
|
72
|
+
return None
|
|
73
|
+
return current
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class PandaServiceClient:
|
|
77
|
+
"""Client responsible for communicating with the Java service via HTTP."""
|
|
78
|
+
|
|
79
|
+
def __init__(self, config: ClientConfig) -> None:
|
|
80
|
+
self._config = config
|
|
81
|
+
http_config = HTTPClientConfig(
|
|
82
|
+
base_url=config.base_url,
|
|
83
|
+
username=config.username,
|
|
84
|
+
password=config.password,
|
|
85
|
+
timeout=config.timeout,
|
|
86
|
+
max_retries=config.retry,
|
|
87
|
+
verify_ssl=config.verify_ssl,
|
|
88
|
+
proxy_type=config.proxy_type,
|
|
89
|
+
proxy_host=config.proxy_host,
|
|
90
|
+
proxy_port=config.proxy_port,
|
|
91
|
+
proxy_username=config.proxy_username,
|
|
92
|
+
proxy_password=config.proxy_password,
|
|
93
|
+
use_gzip=config.use_gzip,
|
|
94
|
+
)
|
|
95
|
+
self._http_client = HTTPClient(http_config)
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def config(self) -> ClientConfig:
|
|
99
|
+
return self._config
|
|
100
|
+
|
|
101
|
+
def request(
|
|
102
|
+
self,
|
|
103
|
+
*,
|
|
104
|
+
endpoint: str,
|
|
105
|
+
payload: Optional[Dict[str, Any]] = None,
|
|
106
|
+
method: str = "POST",
|
|
107
|
+
params: Optional[Dict[str, Any]] = None,
|
|
108
|
+
headers: Optional[Dict[str, str]] = None,
|
|
109
|
+
data_path: Optional[Iterable[str]] = None,
|
|
110
|
+
) -> Any:
|
|
111
|
+
result = self._http_client.request(
|
|
112
|
+
method=method,
|
|
113
|
+
endpoint=endpoint,
|
|
114
|
+
payload=payload,
|
|
115
|
+
params=params,
|
|
116
|
+
headers=headers,
|
|
117
|
+
)
|
|
118
|
+
data = _extract_nested(result, data_path or self._config.data_field)
|
|
119
|
+
return data
|
|
120
|
+
|
|
121
|
+
def fetch_dataframe(
|
|
122
|
+
self,
|
|
123
|
+
*,
|
|
124
|
+
endpoint: str,
|
|
125
|
+
payload: Optional[Dict[str, Any]] = None,
|
|
126
|
+
method: str = "POST",
|
|
127
|
+
params: Optional[Dict[str, Any]] = None,
|
|
128
|
+
headers: Optional[Dict[str, str]] = None,
|
|
129
|
+
data_path: Optional[Iterable[str]] = None,
|
|
130
|
+
) -> pd.DataFrame:
|
|
131
|
+
data = self.request(
|
|
132
|
+
endpoint=endpoint,
|
|
133
|
+
payload=payload,
|
|
134
|
+
method=method,
|
|
135
|
+
params=params,
|
|
136
|
+
headers=headers,
|
|
137
|
+
data_path=data_path,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
if data is None:
|
|
141
|
+
return pd.DataFrame()
|
|
142
|
+
|
|
143
|
+
# 检查是否直接是 DataFrame(Parquet 返回的情况,经过 _extract_nested 后)
|
|
144
|
+
if isinstance(data, pd.DataFrame):
|
|
145
|
+
return data
|
|
146
|
+
|
|
147
|
+
# 检查是否是 Parquet 格式返回的 DataFrame(特殊标记)
|
|
148
|
+
if isinstance(data, dict) and "__dataframe__" in data:
|
|
149
|
+
return data["__dataframe__"]
|
|
150
|
+
|
|
151
|
+
if isinstance(data, dict):
|
|
152
|
+
# 如果数据是字典,检查是否有 'data' 字段
|
|
153
|
+
if 'data' in data:
|
|
154
|
+
# 检查 data 字段是否是 DataFrame(Parquet 返回的情况)
|
|
155
|
+
if isinstance(data['data'], pd.DataFrame):
|
|
156
|
+
return data['data']
|
|
157
|
+
elif isinstance(data['data'], list):
|
|
158
|
+
# 如果 data 字段是列表,直接使用它
|
|
159
|
+
data_list = data['data']
|
|
160
|
+
else:
|
|
161
|
+
# 其他类型,转换为列表
|
|
162
|
+
data_list = [data['data']]
|
|
163
|
+
else:
|
|
164
|
+
# 否则将整个字典作为一行
|
|
165
|
+
data_list = [data]
|
|
166
|
+
elif isinstance(data, (list, tuple)):
|
|
167
|
+
data_list = data
|
|
168
|
+
else:
|
|
169
|
+
raise ServiceError(
|
|
170
|
+
f"Unexpected payload format returned by the service. Type: {type(data)}, Value: {str(data)[:200]}")
|
|
171
|
+
|
|
172
|
+
# 对于大数据量,使用分块处理以避免内存峰值和提升性能
|
|
173
|
+
if len(data_list) > 100000: # 超过 10 万条记录时使用分块处理
|
|
174
|
+
total_rows = len(data_list)
|
|
175
|
+
chunk_size = 100000 # 增大块大小到 10 万条,减少合并次数
|
|
176
|
+
|
|
177
|
+
# 对于超大数据量,使用优化的单进程分块处理
|
|
178
|
+
# 注意:多进程的数据序列化开销很大,对于 Python 对象列表,单进程可能更快
|
|
179
|
+
# 如果未来需要多进程,建议使用共享内存或 Arrow 格式
|
|
180
|
+
chunks = []
|
|
181
|
+
for i in range(0, total_rows, chunk_size):
|
|
182
|
+
chunk_end = min(i + chunk_size, total_rows)
|
|
183
|
+
chunk = data_list[i:chunk_end]
|
|
184
|
+
df_chunk = pd.DataFrame(chunk)
|
|
185
|
+
chunks.append(df_chunk)
|
|
186
|
+
if chunks:
|
|
187
|
+
df = pd.concat(chunks, ignore_index=True)
|
|
188
|
+
del chunks
|
|
189
|
+
import gc
|
|
190
|
+
gc.collect()
|
|
191
|
+
else:
|
|
192
|
+
df = pd.DataFrame()
|
|
193
|
+
else:
|
|
194
|
+
df = pd.DataFrame(data_list)
|
|
195
|
+
return df
|
|
196
|
+
|
|
197
|
+
def close(self) -> None:
|
|
198
|
+
self._http_client.close()
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
ReaderT = TypeVar("ReaderT")
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class ClientFactory:
|
|
205
|
+
"""Factory responsible for creating readers bound to a shared client instance."""
|
|
206
|
+
|
|
207
|
+
def __init__(self, client: PandaServiceClient) -> None:
|
|
208
|
+
self._client = client
|
|
209
|
+
self._instances: Dict[Type[Any], Any] = {}
|
|
210
|
+
self._lock = Lock()
|
|
211
|
+
|
|
212
|
+
@property
|
|
213
|
+
def client(self) -> PandaServiceClient:
|
|
214
|
+
return self._client
|
|
215
|
+
|
|
216
|
+
def get_reader(self, reader_cls: Type[ReaderT]) -> ReaderT:
|
|
217
|
+
with self._lock:
|
|
218
|
+
if reader_cls not in self._instances:
|
|
219
|
+
self._instances[reader_cls] = reader_cls(self._client)
|
|
220
|
+
return self._instances[reader_cls]
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
_factory: Optional[ClientFactory] = None
|
|
224
|
+
_factory_lock = Lock()
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _parse_address(address: str) -> tuple:
|
|
228
|
+
"""解析地址字符串为(host, port)"""
|
|
229
|
+
# 支持格式: "host:port", "tcp://host:port", "host"
|
|
230
|
+
if "://" in address:
|
|
231
|
+
parsed = urlparse(address)
|
|
232
|
+
host = parsed.hostname or parsed.path.split(":")[0]
|
|
233
|
+
port = parsed.port or (int(parsed.path.split(":")[-1]) if ":" in parsed.path else 8080)
|
|
234
|
+
elif ":" in address:
|
|
235
|
+
parts = address.rsplit(":", 1)
|
|
236
|
+
host = parts[0]
|
|
237
|
+
port = int(parts[1])
|
|
238
|
+
else:
|
|
239
|
+
host = address
|
|
240
|
+
port = 8080 # 默认端口
|
|
241
|
+
|
|
242
|
+
if not host:
|
|
243
|
+
raise ServiceError(f"Invalid address format: {address}")
|
|
244
|
+
|
|
245
|
+
return host, port
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def init(
|
|
249
|
+
*,
|
|
250
|
+
username: Optional[str] = None,
|
|
251
|
+
password: Optional[str] = None,
|
|
252
|
+
base_url: Optional[str] = None, # HTTP base URL, 例如: "http://localhost:8080"
|
|
253
|
+
address: Optional[str] = None, # 兼容旧接口,格式: "host:port" 或 "http://host:port"
|
|
254
|
+
timeout: Optional[float] = None,
|
|
255
|
+
max_retries: Optional[int] = None,
|
|
256
|
+
verify_ssl: Optional[bool] = None,
|
|
257
|
+
proxy_type: Optional[str] = None,
|
|
258
|
+
proxy_host: Optional[str] = None,
|
|
259
|
+
proxy_port: Optional[int] = None,
|
|
260
|
+
proxy_username: Optional[str] = None,
|
|
261
|
+
proxy_password: Optional[str] = None,
|
|
262
|
+
use_gzip: Optional[bool] = None,
|
|
263
|
+
data_field: Optional[Iterable[str]] = None,
|
|
264
|
+
abnormal_endpoint: Optional[str] = None,
|
|
265
|
+
) -> PandaServiceClient:
|
|
266
|
+
"""
|
|
267
|
+
初始化客户端。需先调用 init_token() 登录后使用。
|
|
268
|
+
"""
|
|
269
|
+
global _factory
|
|
270
|
+
service_config = get_config()
|
|
271
|
+
|
|
272
|
+
if not username:
|
|
273
|
+
username = service_config.get("DEFAULT_USERNAME", "")
|
|
274
|
+
if not password:
|
|
275
|
+
password = service_config.get("DEFAULT_PASSWORD", "")
|
|
276
|
+
|
|
277
|
+
# 解析 base_url
|
|
278
|
+
if base_url:
|
|
279
|
+
http_base_url = base_url
|
|
280
|
+
elif address:
|
|
281
|
+
# 兼容旧的 address 格式
|
|
282
|
+
if address.startswith("http://") or address.startswith("https://"):
|
|
283
|
+
http_base_url = address
|
|
284
|
+
else:
|
|
285
|
+
# 格式: "host:port" 或 "tcp://host:port"
|
|
286
|
+
parsed_host, parsed_port = _parse_address(address)
|
|
287
|
+
http_base_url = f"http://{parsed_host}:{parsed_port}"
|
|
288
|
+
else:
|
|
289
|
+
# 从配置中获取
|
|
290
|
+
http_base_url = (
|
|
291
|
+
service_config.get("HTTP_SERVICE_BASE_URL")
|
|
292
|
+
or service_config.get("JAVA_SERVICE_BASE_URL")
|
|
293
|
+
)
|
|
294
|
+
if http_base_url:
|
|
295
|
+
# 如果配置中没有协议,添加 http://
|
|
296
|
+
if not http_base_url.startswith(("http://", "https://")):
|
|
297
|
+
parsed_host, parsed_port = _parse_address(http_base_url)
|
|
298
|
+
http_base_url = f"http://{parsed_host}:{parsed_port}"
|
|
299
|
+
else:
|
|
300
|
+
http_base_url = "http://localhost:8080"
|
|
301
|
+
|
|
302
|
+
config_kwargs: Dict[str, Any] = {
|
|
303
|
+
"base_url": http_base_url + "/pandaData",
|
|
304
|
+
"username": username,
|
|
305
|
+
"password": password,
|
|
306
|
+
"timeout": timeout or float(
|
|
307
|
+
service_config.get("HTTP_TIMEOUT") or service_config.get("JAVA_SERVICE_TIMEOUT", 30.0)),
|
|
308
|
+
"retry": max_retries
|
|
309
|
+
if max_retries is not None
|
|
310
|
+
else int(service_config.get("HTTP_MAX_RETRIES") or service_config.get("JAVA_SERVICE_MAX_RETRIES", 3)),
|
|
311
|
+
"verify_ssl": (
|
|
312
|
+
verify_ssl
|
|
313
|
+
if verify_ssl is not None
|
|
314
|
+
else service_config.get("HTTP_VERIFY_SSL",
|
|
315
|
+
service_config.get("JAVA_SERVICE_VERIFY_SSL", "true")).lower() in {"true", "1",
|
|
316
|
+
"yes"}
|
|
317
|
+
),
|
|
318
|
+
"proxy_type": proxy_type or service_config.get("HTTP_PROXY_TYPE"),
|
|
319
|
+
"proxy_host": proxy_host or service_config.get("HTTP_PROXY_HOST"),
|
|
320
|
+
"proxy_port": (
|
|
321
|
+
proxy_port
|
|
322
|
+
if proxy_port is not None
|
|
323
|
+
else (
|
|
324
|
+
int(service_config["HTTP_PROXY_PORT"])
|
|
325
|
+
if service_config.get("HTTP_PROXY_PORT")
|
|
326
|
+
else None
|
|
327
|
+
)
|
|
328
|
+
),
|
|
329
|
+
"proxy_username": proxy_username or service_config.get("HTTP_PROXY_USERNAME"),
|
|
330
|
+
"proxy_password": proxy_password or service_config.get("HTTP_PROXY_PASSWORD"),
|
|
331
|
+
"use_gzip": (
|
|
332
|
+
use_gzip
|
|
333
|
+
if use_gzip is not None
|
|
334
|
+
else service_config.get("HTTP_USE_GZIP", "false").lower() in {"true", "1", "yes"}
|
|
335
|
+
),
|
|
336
|
+
"data_field": data_field
|
|
337
|
+
if data_field is not None
|
|
338
|
+
else _parse_data_field(service_config.get("HTTP_DATA_FIELD") or service_config.get("JAVA_SERVICE_DATA_FIELD")),
|
|
339
|
+
"abnormal_endpoint": abnormal_endpoint
|
|
340
|
+
or service_config.get("HTTP_ABNORMAL_ENDPOINT")
|
|
341
|
+
or service_config.get("JAVA_SERVICE_ABNORMAL_ENDPOINT", "/abnormal"),
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
client_config = ClientConfig(**config_kwargs)
|
|
345
|
+
client = PandaServiceClient(client_config)
|
|
346
|
+
|
|
347
|
+
with _factory_lock:
|
|
348
|
+
_factory = ClientFactory(client)
|
|
349
|
+
|
|
350
|
+
return client
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def get_factory(base_url: Optional[str] = None) -> ClientFactory:
|
|
354
|
+
if _factory is None:
|
|
355
|
+
# 要求必须先调用 init() 或 init_token() 才能使用
|
|
356
|
+
# 不再自动从token文件读取配置,确保安全性
|
|
357
|
+
from panda_data.exceptions import ClientNotInitializedError
|
|
358
|
+
raise ClientNotInitializedError(
|
|
359
|
+
"客户端未初始化,请先调用 panda_data.init_token() 进行登录!"
|
|
360
|
+
)
|
|
361
|
+
return _factory
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def get_client(base_url: Optional[str] = None) -> PandaServiceClient:
|
|
365
|
+
return get_factory(base_url).client
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _parse_data_field(raw: Optional[str]) -> Optional[Iterable[str]]:
|
|
369
|
+
if not raw:
|
|
370
|
+
return None
|
|
371
|
+
parts = [segment.strip() for segment in raw.split(".") if segment.strip()]
|
|
372
|
+
return tuple(parts) if parts else None
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""
|
|
2
|
+
配置模块,用于加载和管理配置信息
|
|
3
|
+
支持从配置文件和环境变量导入,环境变量优先级更高
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# 获取logger
|
|
10
|
+
try:
|
|
11
|
+
from panda_data.logger.config import logger
|
|
12
|
+
except ImportError:
|
|
13
|
+
# 如果无法导入logger,创建一个基本的logger
|
|
14
|
+
logging.basicConfig(level=logging.INFO)
|
|
15
|
+
logger = logging.getLogger("config")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# 初始化配置变量config = None
|
|
19
|
+
|
|
20
|
+
def load_config():
|
|
21
|
+
"""加载配置文件,并从环境变量更新配置"""
|
|
22
|
+
global config
|
|
23
|
+
|
|
24
|
+
config = {}
|
|
25
|
+
|
|
26
|
+
# ========== 日志配置 ==========
|
|
27
|
+
config["LOG_LEVEL"] = os.getenv("LOG_LEVEL", "DEBUG")
|
|
28
|
+
config["log_file"] = os.getenv("LOG_FILE", "logs/panda_data.log")
|
|
29
|
+
config["log_rotation"] = os.getenv("LOG_ROTATION", "1 MB")
|
|
30
|
+
config["LOG_PATH"] = os.getenv("LOG_PATH", "~/log")
|
|
31
|
+
|
|
32
|
+
# ========== HTTP 通用配置(client / init_token 等使用) ==========
|
|
33
|
+
config["DEFAULT_USERNAME"] = os.getenv("DEFAULT_USERNAME", "")
|
|
34
|
+
config["DEFAULT_PASSWORD"] = os.getenv("DEFAULT_PASSWORD", "")
|
|
35
|
+
|
|
36
|
+
config["HTTP_SERVICE_BASE_URL"] = os.getenv("HTTP_SERVICE_BASE_URL")
|
|
37
|
+
config["HTTP_TIMEOUT"] = os.getenv("HTTP_TIMEOUT", "300")
|
|
38
|
+
config["HTTP_MAX_RETRIES"] = os.getenv("HTTP_MAX_RETRIES", "3")
|
|
39
|
+
config["HTTP_VERIFY_SSL"] = os.getenv("HTTP_VERIFY_SSL", "true")
|
|
40
|
+
config["HTTP_PROXY_TYPE"] = os.getenv("HTTP_PROXY_TYPE") # "http" or "https"
|
|
41
|
+
config["HTTP_PROXY_HOST"] = os.getenv("HTTP_PROXY_HOST")
|
|
42
|
+
config["HTTP_PROXY_PORT"] = os.getenv("HTTP_PROXY_PORT")
|
|
43
|
+
config["HTTP_PROXY_USERNAME"] = os.getenv("HTTP_PROXY_USERNAME")
|
|
44
|
+
config["HTTP_PROXY_PASSWORD"] = os.getenv("HTTP_PROXY_PASSWORD")
|
|
45
|
+
config["HTTP_USE_GZIP"] = os.getenv("HTTP_USE_GZIP", "false")
|
|
46
|
+
config["HTTP_DATA_FIELD"] = os.getenv("HTTP_DATA_FIELD", "data")
|
|
47
|
+
config["HTTP_ABNORMAL_ENDPOINT"] = os.getenv("HTTP_ABNORMAL_ENDPOINT", "/abnormal")
|
|
48
|
+
|
|
49
|
+
# Legacy HTTP service config (for backward compatibility)
|
|
50
|
+
config["JAVA_SERVICE_BASE_URL"] = os.getenv("JAVA_SERVICE_BASE_URL", "http://pandadata.pandaai.online")
|
|
51
|
+
config["JAVA_SERVICE_TIMEOUT"] = os.getenv("JAVA_SERVICE_TIMEOUT", "15")
|
|
52
|
+
config["JAVA_SERVICE_MAX_POOL_SIZE"] = os.getenv("JAVA_SERVICE_MAX_POOL_SIZE", "10")
|
|
53
|
+
config["JAVA_SERVICE_MAX_RETRIES"] = os.getenv("JAVA_SERVICE_MAX_RETRIES", "3")
|
|
54
|
+
config["JAVA_SERVICE_BACKOFF_FACTOR"] = os.getenv("JAVA_SERVICE_BACKOFF_FACTOR", "0.3")
|
|
55
|
+
config["JAVA_SERVICE_VERIFY_SSL"] = os.getenv("JAVA_SERVICE_VERIFY_SSL", "true")
|
|
56
|
+
config["JAVA_SERVICE_DATA_FIELD"] = os.getenv("JAVA_SERVICE_DATA_FIELD", "data")
|
|
57
|
+
config["JAVA_SERVICE_ABNORMAL_ENDPOINT"] = os.getenv("JAVA_SERVICE_ABNORMAL_ENDPOINT", "/abnormal")
|
|
58
|
+
|
|
59
|
+
# ========== init_token(登录) ==========
|
|
60
|
+
config["JAVA_SERVICE_USER_ENDPOINT"] = os.getenv("JAVA_SERVICE_USER_ENDPOINT", "/dataUser")
|
|
61
|
+
config["JAVA_SERVICE_USER_PATH_LOGIN"] = os.getenv("JAVA_SERVICE_USER_PATH_LOGIN", "/login")
|
|
62
|
+
|
|
63
|
+
# ========== market_reader(行情数据) ==========
|
|
64
|
+
config["JAVA_SERVICE_KLINE_ENDPOINT"] = os.getenv("JAVA_SERVICE_KLINE_ENDPOINT", "/multi")
|
|
65
|
+
config["JAVA_SERVICE_KLINE_PATH_GET_MARKET_DATA"] = os.getenv("JAVA_SERVICE_KLINE_PATH_GET_MARKET_DATA", "/getMultiMarketData")
|
|
66
|
+
config["JAVA_SERVICE_KLINE_PATH_GET_MARKET_MIN_DATA"] = os.getenv("JAVA_SERVICE_KLINE_PATH_GET_MARKET_MIN_DATA", "/getMultiMarketMinData")
|
|
67
|
+
config["JAVA_SERVICE_TICK_PATH_FUTURE_TICK"] = os.getenv("JAVA_SERVICE_TICK_PATH_FUTURE_TICK", "/getFutureTickData")
|
|
68
|
+
|
|
69
|
+
# ========== market_reference_reader(市场参考) ==========
|
|
70
|
+
# KLINE - 股票详情
|
|
71
|
+
config["JAVA_SERVICE_KLINE_PATH_GET_STOCK_DETAIL"] = os.getenv("JAVA_SERVICE_KLINE_PATH_GET_STOCK_DETAIL", "/getStockDetail")
|
|
72
|
+
|
|
73
|
+
# INDEX - 指数
|
|
74
|
+
config["JAVA_SERVICE_INDEX_ENDPOINT"] = os.getenv("JAVA_SERVICE_INDEX_ENDPOINT", "/index")
|
|
75
|
+
config["JAVA_SERVICE_INDEX_PATH_GET_SYMBOL"] = os.getenv("JAVA_SERVICE_INDEX_PATH_GET_SYMBOL", "/getIndexSymbolData")
|
|
76
|
+
config["JAVA_SERVICE_INDEX_PATH_GET_INDICATOR"] = os.getenv("JAVA_SERVICE_INDEX_PATH_GET_INDICATOR", "/getIndexIndicatorData")
|
|
77
|
+
config["JAVA_SERVICE_INDEX_PATH_GET_WEIGHTS"] = os.getenv("JAVA_SERVICE_INDEX_PATH_GET_WEIGHTS", "/getIndexWeightsData")
|
|
78
|
+
|
|
79
|
+
# CONCEPT - 概念
|
|
80
|
+
config["JAVA_SERVICE_CONCEPT_ENDPOINT"] = os.getenv("JAVA_SERVICE_CONCEPT_ENDPOINT", "/concept")
|
|
81
|
+
config["JAVA_SERVICE_CONCEPT_PATH_GET_CONCEPT_LIST"] = os.getenv("JAVA_SERVICE_CONCEPT_PATH_GET_CONCEPT_LIST", "/getConceptData")
|
|
82
|
+
config["JAVA_SERVICE_CONCEPT_PATH_GET_CONCEPT_STOCK"] = os.getenv("JAVA_SERVICE_CONCEPT_PATH_GET_CONCEPT_STOCK", "/getConceptStockData")
|
|
83
|
+
|
|
84
|
+
# INDUSTRY - 行业
|
|
85
|
+
config["JAVA_SERVICE_INDUSTRY_ENDPOINT"] = os.getenv("JAVA_SERVICE_INDUSTRY_ENDPOINT", "/industry")
|
|
86
|
+
config["JAVA_SERVICE_INDUSTRY_PATH_GET_STOCK"] = os.getenv("JAVA_SERVICE_INDUSTRY_PATH_GET_STOCK", "/getIndustryStockData")
|
|
87
|
+
config["JAVA_SERVICE_INDUSTRY_PATH_GET_LIST"] = os.getenv("JAVA_SERVICE_INDUSTRY_PATH_GET_LIST", "/getIndustryList")
|
|
88
|
+
config["JAVA_SERVICE_INDUSTRY_PATH_GET_STOCK_INDUSTRY"] = os.getenv("JAVA_SERVICE_INDUSTRY_PATH_GET_STOCK_INDUSTRY", "/getStockIndustry")
|
|
89
|
+
|
|
90
|
+
# ABNORMAL - 龙虎榜
|
|
91
|
+
config["JAVA_SERVICE_ABNORMAL_PATH_GET_ABNORMAL_DATA"] = os.getenv("JAVA_SERVICE_ABNORMAL_PATH_GET_ABNORMAL_DATA", "/getAbnormalData")
|
|
92
|
+
config["JAVA_SERVICE_ABNORMAL_PATH_GET_ABNORMAL_DETAIL"] = os.getenv(
|
|
93
|
+
"JAVA_SERVICE_ABNORMAL_PATH_GET_ABNORMAL_DETAIL", "/getAbnormalDetailData")
|
|
94
|
+
|
|
95
|
+
# BUYBACK - 回购
|
|
96
|
+
config["JAVA_SERVICE_BUY_BACK_ENDPOINT"] = os.getenv("JAVA_SERVICE_BUY_BACK_ENDPOINT", "/buyback")
|
|
97
|
+
config["JAVA_SERVICE_BUY_BACK_PATH_GET_BUY_BACK_DATA"] = os.getenv(
|
|
98
|
+
"JAVA_SERVICE_BUY_BACK_PATH_GET_BUY_BACK_DATA", "/getBuyBackData")
|
|
99
|
+
|
|
100
|
+
# SECURITIES_MARGIN / STOCK_CONNECT / STOCK - 两融、北向、资金等
|
|
101
|
+
config["JAVA_SERVICE_SECURITIES_MARGIN_ENDPOINT"] = os.getenv("JAVA_SERVICE_SECURITIES_MARGIN_ENDPOINT", "/stock")
|
|
102
|
+
config["JAVA_SERVICE_SECURITIES_MARGIN_PATH_GET_MARGIN"] = os.getenv("JAVA_SERVICE_SECURITIES_MARGIN_PATH_GET_MARGIN", "/getSecuritiesMarginData")
|
|
103
|
+
|
|
104
|
+
config["JAVA_SERVICE_STOCK_CONNECT_ENDPOINT"] = os.getenv("JAVA_SERVICE_STOCK_CONNECT_ENDPOINT", "/stock")
|
|
105
|
+
config["JAVA_SERVICE_STOCK_CONNECT_PATH_GET_STOCK_CONNECT_DATA"] = os.getenv("JAVA_SERVICE_STOCK_CONNECT_PATH_GET_STOCK_CONNECT_DATA", "/getStockConnectData")
|
|
106
|
+
|
|
107
|
+
config["JAVA_SERVICE_STOCK_ENDPOINT"] = os.getenv("JAVA_SERVICE_STOCK_ENDPOINT", "/stock")
|
|
108
|
+
config["JAVA_SERVICE_STOCK_PATH_GET_FLOW"] = os.getenv("JAVA_SERVICE_STOCK_PATH_GET_FLOW", "/getStockFlowData")
|
|
109
|
+
config["JAVA_SERVICE_STOCK_PATH_GET_INVESTOR_ACTIVITIES"] = os.getenv("JAVA_SERVICE_STOCK_PATH_GET_INVESTOR_ACTIVITIES", "/getStockInvestorData")
|
|
110
|
+
config["JAVA_SERVICE_STOCK_PATH_GET_RESTRICTED_DETAILS"] = os.getenv("JAVA_SERVICE_STOCK_PATH_GET_RESTRICTED_DETAILS", "/getStockRestrictedData")
|
|
111
|
+
config["JAVA_SERVICE_STOCK_PATH_GET_HOLDER_NUMBER"] = os.getenv("JAVA_SERVICE_STOCK_PATH_GET_HOLDER_NUMBER", "/getStockHolderNumberData")
|
|
112
|
+
config["JAVA_SERVICE_STOCK_PATH_GET_MAIN_SHAREHOLDER"] = os.getenv("JAVA_SERVICE_STOCK_PATH_GET_MAIN_SHAREHOLDER", "/getStockMainHolderData")
|
|
113
|
+
config["JAVA_SERVICE_STOCK_PATH_GET_BLOCK_TRADE"] = os.getenv("JAVA_SERVICE_STOCK_PATH_GET_BLOCK_TRADE", "/getStockBlockTradeData")
|
|
114
|
+
config["JAVA_SERVICE_STOCK_PATH_GET_SHARES"] = os.getenv("JAVA_SERVICE_STOCK_PATH_GET_SHARES", "/getStockShareData")
|
|
115
|
+
|
|
116
|
+
# ========== financial_and_factors_reader(财务与因子) ==========
|
|
117
|
+
# FINANCIAL - 财务
|
|
118
|
+
config["JAVA_SERVICE_FINANCIAL_ENDPOINT"] = os.getenv("JAVA_SERVICE_FINANCIAL_ENDPOINT", "/financial")
|
|
119
|
+
config["JAVA_SERVICE_FINANCIAL_PATH_GET_FORECAST"] = os.getenv("JAVA_SERVICE_FINANCIAL_PATH_GET_FORECAST", "/getFinancialForecastData")
|
|
120
|
+
config["JAVA_SERVICE_FINANCIAL_PATH_GET_PERFORMANCE"] = os.getenv("JAVA_SERVICE_FINANCIAL_PATH_GET_PERFORMANCE", "/getFinancialPerformanceData")
|
|
121
|
+
config["JAVA_SERVICE_FINANCIAL_PATH_GET_EX"] = os.getenv("JAVA_SERVICE_FINANCIAL_PATH_GET_EX", "/getFinancialExData")
|
|
122
|
+
config["JAVA_SERVICE_FINANCIAL_PATH_GET_STATEMENT"] = os.getenv("JAVA_SERVICE_FINANCIAL_PATH_GET_STATEMENT", "/getFinancialStatementData")
|
|
123
|
+
config["JAVA_SERVICE_FINANCIAL_PATH_GET_STATEMENT_DAILY"] = os.getenv("JAVA_SERVICE_FINANCIAL_PATH_GET_STATEMENT_DAILY", "/getFinancialStatementDailyData")
|
|
124
|
+
|
|
125
|
+
# STOCK - 审计、复权因子
|
|
126
|
+
config["JAVA_SERVICE_STOCK_PATH_GET_AUDIT_OPINION"] = os.getenv("JAVA_SERVICE_STOCK_PATH_GET_AUDIT_OPINION", "/getStockAuditData")
|
|
127
|
+
config["JAVA_SERVICE_STOCK_PATH_GET_FACTOR_RESTORED"] = os.getenv("JAVA_SERVICE_STOCK_PATH_GET_FACTOR_RESTORED", "/getFactorRestoredData")
|
|
128
|
+
|
|
129
|
+
# KLINE - 因子
|
|
130
|
+
config["JAVA_SERVICE_KLINE_PATH_GET_FACTOR"] = os.getenv("JAVA_SERVICE_KLINE_PATH_GET_FACTOR", "/getFactor")
|
|
131
|
+
|
|
132
|
+
# ========== future_reader(期货) ==========
|
|
133
|
+
config["JAVA_SERVICE_FUTURE_ENDPOINT"] = os.getenv("JAVA_SERVICE_FUTURE_ENDPOINT", "/future")
|
|
134
|
+
config["JAVA_SERVICE_FUTURE_PATH_GET_FUTURE_LIST"] = os.getenv("JAVA_SERVICE_FUTURE_PATH_GET_FUTURE_LIST", "/getFutureList")
|
|
135
|
+
config["JAVA_SERVICE_FUTURE_PATH_GET_FACTOR_POST"] = os.getenv("JAVA_SERVICE_FUTURE_PATH_GET_FACTOR_POST", "/getFutureMarketPostData")
|
|
136
|
+
config["JAVA_SERVICE_FUTURE_PATH_FUTURE_DOMINANT"] = os.getenv("JAVA_SERVICE_FUTURE_PATH_FUTURE_DOMINANT", "/getFutureDominantData")
|
|
137
|
+
|
|
138
|
+
# ========== trading_tools_reader(交易工具) ==========
|
|
139
|
+
config["JAVA_SERVICE_CALENDAR_ENDPOINT"] = os.getenv("JAVA_SERVICE_CALENDAR_ENDPOINT", "/tradeCalendar")
|
|
140
|
+
config["JAVA_SERVICE_CALENDAR_PATH_GET_TRADE_CALENDAR_DATA"] = os.getenv("JAVA_SERVICE_CALENDAR_PATH_GET_TRADE_CALENDAR_DATA", "/getTradeCalendarData")
|
|
141
|
+
config["JAVA_SERVICE_CALENDAR_PATH_GET_PREVIOUS_NTH_TRADING_DAY"] = os.getenv("JAVA_SERVICE_CALENDAR_PATH_GET_PREVIOUS_NTH_TRADING_DAY", "/getPreviousNthTradingDay")
|
|
142
|
+
config["JAVA_SERVICE_CALENDAR_PATH_GET_LATEST_TRADING_DAY"] = os.getenv("JAVA_SERVICE_CALENDAR_PATH_GET_LATEST_TRADING_DAY", "/getLatestTradingDay")
|
|
143
|
+
|
|
144
|
+
config["JAVA_SERVICE_STOCK_PATH_GET_SPECIAL_TREATMENT"] = os.getenv("JAVA_SERVICE_STOCK_PATH_GET_SPECIAL_TREATMENT", "/getStockSpecialTreatmentData")
|
|
145
|
+
config["JAVA_SERVICE_STOCK_PATH_GET_TRADING_STOCK_LIST"] = os.getenv("JAVA_SERVICE_STOCK_PATH_GET_TRADING_STOCK_LIST", "/getTradingStockList")
|
|
146
|
+
|
|
147
|
+
# 打包前可加入 panda_data_local_config 模块覆盖配置
|
|
148
|
+
try:
|
|
149
|
+
import panda_data_local_config as _local
|
|
150
|
+
if getattr(_local, "JAVA_SERVICE_BASE_URL", None):
|
|
151
|
+
config["JAVA_SERVICE_BASE_URL"] = _local.JAVA_SERVICE_BASE_URL
|
|
152
|
+
except ImportError:
|
|
153
|
+
pass
|
|
154
|
+
|
|
155
|
+
return config
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def get_config():
|
|
159
|
+
"""
|
|
160
|
+
获取配置对象,如果配置未加载则先加载配置
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
dict: 配置信息字典
|
|
164
|
+
"""
|
|
165
|
+
global config
|
|
166
|
+
if config is None:
|
|
167
|
+
config = load_config()
|
|
168
|
+
return config
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# 初始加载配置
|
|
172
|
+
try:
|
|
173
|
+
config = load_config()
|
|
174
|
+
# logger.info(f"初始化配置成功: {config}") # 已禁用初始化配置日志
|
|
175
|
+
except Exception as e:
|
|
176
|
+
logger.error(f"初始化配置失败: {str(e)}")
|
|
177
|
+
# 不在初始化时抛出异常,留到实际使用时再处理
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Iterable, Optional
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
from panda_data.client import get_client
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def request_service(
|
|
11
|
+
endpoint: str,
|
|
12
|
+
base_url: Optional[str] = None,
|
|
13
|
+
*,
|
|
14
|
+
payload: Optional[Dict[str, Any]] = None,
|
|
15
|
+
method: str = "POST",
|
|
16
|
+
params: Optional[Dict[str, Any]] = None,
|
|
17
|
+
headers: Optional[Dict[str, str]] = None,
|
|
18
|
+
data_path: Optional[Iterable[str]] = None,
|
|
19
|
+
) -> Any:
|
|
20
|
+
client = get_client(base_url)
|
|
21
|
+
return client.request(
|
|
22
|
+
endpoint=endpoint,
|
|
23
|
+
payload=payload,
|
|
24
|
+
method=method,
|
|
25
|
+
params=params,
|
|
26
|
+
headers=headers,
|
|
27
|
+
data_path=data_path,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def fetch_dataframe(
|
|
32
|
+
endpoint: str,
|
|
33
|
+
*,
|
|
34
|
+
payload: Optional[Dict[str, Any]] = None,
|
|
35
|
+
method: str = "POST",
|
|
36
|
+
params: Optional[Dict[str, Any]] = None,
|
|
37
|
+
headers: Optional[Dict[str, str]] = None,
|
|
38
|
+
data_path: Optional[Iterable[str]] = None,
|
|
39
|
+
) -> pd.DataFrame:
|
|
40
|
+
client = get_client()
|
|
41
|
+
return client.fetch_dataframe(
|
|
42
|
+
endpoint=endpoint,
|
|
43
|
+
payload=payload,
|
|
44
|
+
method=method,
|
|
45
|
+
params=params,
|
|
46
|
+
headers=headers,
|
|
47
|
+
data_path=data_path,
|
|
48
|
+
)
|
|
49
|
+
|