xtquant-share 1.1.2__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.
- xqshare/__init__.py +38 -0
- xqshare/auth.py +466 -0
- xqshare/client.py +682 -0
- xqshare/server.py +868 -0
- xqshare/tools/__init__.py +7 -0
- xqshare/tools/common.py +457 -0
- xqshare/tools/xtdata.py +98 -0
- xqshare/tools/xttrader.py +122 -0
- xtquant_share-1.1.2.dist-info/METADATA +756 -0
- xtquant_share-1.1.2.dist-info/RECORD +14 -0
- xtquant_share-1.1.2.dist-info/WHEEL +5 -0
- xtquant_share-1.1.2.dist-info/entry_points.txt +4 -0
- xtquant_share-1.1.2.dist-info/licenses/LICENSE +21 -0
- xtquant_share-1.1.2.dist-info/top_level.txt +1 -0
xqshare/__init__.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""
|
|
2
|
+
XtQuant Share (xqshare) - Transparent remote proxy for xtquant library
|
|
3
|
+
|
|
4
|
+
Allows using xtquant on macOS/Linux by proxying calls to a Windows server.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
__version__ = "1.1.1"
|
|
8
|
+
__author__ = "Jason Hu"
|
|
9
|
+
|
|
10
|
+
from .client import (
|
|
11
|
+
XtQuantRemote,
|
|
12
|
+
connect,
|
|
13
|
+
disconnect,
|
|
14
|
+
get_client,
|
|
15
|
+
xtdata,
|
|
16
|
+
xttrader,
|
|
17
|
+
xttype,
|
|
18
|
+
xtview,
|
|
19
|
+
datadir,
|
|
20
|
+
ConnectionError,
|
|
21
|
+
AuthenticationError,
|
|
22
|
+
CallbackError,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"XtQuantRemote",
|
|
27
|
+
"connect",
|
|
28
|
+
"disconnect",
|
|
29
|
+
"get_client",
|
|
30
|
+
"xtdata",
|
|
31
|
+
"xttrader",
|
|
32
|
+
"xttype",
|
|
33
|
+
"xtview",
|
|
34
|
+
"datadir",
|
|
35
|
+
"ConnectionError",
|
|
36
|
+
"AuthenticationError",
|
|
37
|
+
"CallbackError",
|
|
38
|
+
]
|
xqshare/auth.py
ADDED
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
"""
|
|
2
|
+
xqshare 权限模块
|
|
3
|
+
|
|
4
|
+
实现多级账号权限控制,支持免费到企业级的 5 级账号体系。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import yaml
|
|
9
|
+
from typing import Dict, List, Optional, Set
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from enum import Enum
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Permission(Enum):
|
|
16
|
+
"""权限枚举"""
|
|
17
|
+
BASIC = "basic" # 基础信息
|
|
18
|
+
DAILY = "daily" # 日线数据
|
|
19
|
+
MINUTE = "minute" # 分钟线数据
|
|
20
|
+
TICK = "tick" # 实时行情
|
|
21
|
+
TRADE_QUERY = "trade_query" # 交易查询权限(持仓、资产、委托等)
|
|
22
|
+
TRADE_ORDER = "trade_order" # 完整交易权限(查询+下单+撤单)
|
|
23
|
+
CALLBACK = "callback" # 回调功能
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AccountLevel(Enum):
|
|
27
|
+
"""账号等级枚举"""
|
|
28
|
+
FREE = "free"
|
|
29
|
+
PLUS = "plus"
|
|
30
|
+
STANDARD = "standard"
|
|
31
|
+
PREMIUM = "premium"
|
|
32
|
+
ENTERPRISE = "enterprise"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# 账号等级对应的权限组合
|
|
36
|
+
LEVEL_PERMISSIONS: Dict[AccountLevel, Set[Permission]] = {
|
|
37
|
+
AccountLevel.FREE: {Permission.BASIC, Permission.DAILY},
|
|
38
|
+
AccountLevel.PLUS: {Permission.BASIC, Permission.DAILY, Permission.MINUTE},
|
|
39
|
+
AccountLevel.STANDARD: {Permission.BASIC, Permission.DAILY, Permission.MINUTE, Permission.TICK, Permission.CALLBACK},
|
|
40
|
+
AccountLevel.PREMIUM: {Permission.BASIC, Permission.DAILY, Permission.MINUTE, Permission.TICK, Permission.TRADE_QUERY, Permission.CALLBACK},
|
|
41
|
+
AccountLevel.ENTERPRISE: {Permission.BASIC, Permission.DAILY, Permission.MINUTE, Permission.TICK, Permission.TRADE_QUERY, Permission.TRADE_ORDER, Permission.CALLBACK},
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# API 与权限的映射关系
|
|
45
|
+
# 格式: "module.method": Permission 或 "method": Permission
|
|
46
|
+
API_PERMISSIONS: Dict[str, Permission] = {
|
|
47
|
+
# ==================== basic 权限 ====================
|
|
48
|
+
"xtdata.get_stock_list_in_sector": Permission.BASIC,
|
|
49
|
+
"xtdata.get_index_list": Permission.BASIC,
|
|
50
|
+
"xtdata.get_instrument_detail": Permission.BASIC,
|
|
51
|
+
"xtdata.get_divid_factors": Permission.BASIC,
|
|
52
|
+
"xtdata.get_sector_list": Permission.BASIC,
|
|
53
|
+
"get_all_stocks": Permission.BASIC,
|
|
54
|
+
"get_index_list": Permission.BASIC,
|
|
55
|
+
|
|
56
|
+
# ==================== daily 权限 ====================
|
|
57
|
+
# get_market_data 和 get_market_data_ex 的 period 参数决定权限
|
|
58
|
+
# 这里设置基础权限,实际检查时会根据 period 参数动态判断
|
|
59
|
+
"xtdata.get_market_data": Permission.DAILY,
|
|
60
|
+
"xtdata.get_market_data_ex": Permission.DAILY,
|
|
61
|
+
"xtdata.get_full_kline": Permission.DAILY,
|
|
62
|
+
"xtdata.download_history_data": Permission.DAILY,
|
|
63
|
+
"xtdata.download_history_data2": Permission.DAILY,
|
|
64
|
+
"download_history_data2": Permission.DAILY,
|
|
65
|
+
|
|
66
|
+
# ==================== minute 权限 ====================
|
|
67
|
+
"xtdata.get_financial_data": Permission.MINUTE,
|
|
68
|
+
"xtdata.download_financial_data": Permission.MINUTE,
|
|
69
|
+
"xtdata.download_financial_data2": Permission.MINUTE,
|
|
70
|
+
|
|
71
|
+
# ==================== tick 权限 ====================
|
|
72
|
+
"xtdata.get_full_tick": Permission.TICK,
|
|
73
|
+
"xtdata.subscribe_quote": Permission.TICK,
|
|
74
|
+
|
|
75
|
+
# ==================== trade 权限 ====================
|
|
76
|
+
# 精确匹配:查询方法需要 TRADE_QUERY
|
|
77
|
+
"create_trader": Permission.TRADE_QUERY,
|
|
78
|
+
"create_xttrader": Permission.TRADE_QUERY,
|
|
79
|
+
# 生命周期方法
|
|
80
|
+
"xttrader.start": Permission.TRADE_QUERY,
|
|
81
|
+
"xttrader.stop": Permission.TRADE_QUERY,
|
|
82
|
+
"xttrader.connect": Permission.TRADE_QUERY,
|
|
83
|
+
# 注册和订阅
|
|
84
|
+
"xttrader.register_callback": Permission.TRADE_QUERY,
|
|
85
|
+
"xttrader.subscribe": Permission.TRADE_QUERY,
|
|
86
|
+
"xttrader.unsubscribe": Permission.TRADE_QUERY,
|
|
87
|
+
"xttrader.query_account_infos": Permission.TRADE_QUERY,
|
|
88
|
+
"xttrader.query_account_status": Permission.TRADE_QUERY,
|
|
89
|
+
"xttrader.query_stock_asset": Permission.TRADE_QUERY,
|
|
90
|
+
"xttrader.query_stock_order": Permission.TRADE_QUERY,
|
|
91
|
+
"xttrader.query_stock_orders": Permission.TRADE_QUERY,
|
|
92
|
+
"xttrader.query_stock_trades": Permission.TRADE_QUERY,
|
|
93
|
+
"xttrader.query_stock_position": Permission.TRADE_QUERY,
|
|
94
|
+
"xttrader.query_stock_positions": Permission.TRADE_QUERY,
|
|
95
|
+
"xttrader.query_credit_detail": Permission.TRADE_QUERY,
|
|
96
|
+
"xttrader.query_stk_compacts": Permission.TRADE_QUERY,
|
|
97
|
+
"xttrader.query_credit_subjects": Permission.TRADE_QUERY,
|
|
98
|
+
"xttrader.query_credit_slo_code": Permission.TRADE_QUERY,
|
|
99
|
+
"xttrader.query_credit_assure": Permission.TRADE_QUERY,
|
|
100
|
+
"xttrader.query_new_purchase_limit": Permission.TRADE_QUERY,
|
|
101
|
+
# 异步查询方法(带回调)
|
|
102
|
+
"xttrader.query_account_infos_async": Permission.TRADE_QUERY,
|
|
103
|
+
"xttrader.query_account_status_async": Permission.TRADE_QUERY,
|
|
104
|
+
"xttrader.query_stock_asset_async": Permission.TRADE_QUERY,
|
|
105
|
+
"xttrader.query_stock_orders_async": Permission.TRADE_QUERY,
|
|
106
|
+
"xttrader.query_stock_trades_async": Permission.TRADE_QUERY,
|
|
107
|
+
"xttrader.query_stock_positions_async": Permission.TRADE_QUERY,
|
|
108
|
+
"xttrader.query_credit_detail_async": Permission.TRADE_QUERY,
|
|
109
|
+
"xttrader.query_stk_compacts_async": Permission.TRADE_QUERY,
|
|
110
|
+
"xttrader.query_credit_subjects_async": Permission.TRADE_QUERY,
|
|
111
|
+
"xttrader.query_credit_slo_code_async": Permission.TRADE_QUERY,
|
|
112
|
+
"xttrader.query_credit_assure_async": Permission.TRADE_QUERY,
|
|
113
|
+
"xttrader.query_new_purchase_limit_async": Permission.TRADE_QUERY,
|
|
114
|
+
# 通配符:其他 xttrader 方法(下单/撤单等)需要 TRADE_ORDER
|
|
115
|
+
"xttrader.*": Permission.TRADE_ORDER,
|
|
116
|
+
|
|
117
|
+
# ==================== callback 权限 ====================
|
|
118
|
+
# 订阅类 API(包含 callback 参数)
|
|
119
|
+
"xtdata.subscribe_whole_quote": Permission.CALLBACK,
|
|
120
|
+
"xtdata.subscribe_full_tick": Permission.CALLBACK,
|
|
121
|
+
"xtdata.subscribe_quote": Permission.CALLBACK,
|
|
122
|
+
# 异步测试回调
|
|
123
|
+
"test_async_callback": Permission.CALLBACK,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
# 分钟/小时线周期列表(不含1m,1m属于tick权限)
|
|
127
|
+
MINUTE_PERIODS = {"5m", "15m", "30m", "1h"}
|
|
128
|
+
# 日线及以上周期列表
|
|
129
|
+
DAILY_PERIODS = {"1d", "1w", "1mon", "1q", "1hy", "1y"}
|
|
130
|
+
# Tick 周期(包含分笔和1分钟线)
|
|
131
|
+
TICK_PERIODS = {"tick", "1m"}
|
|
132
|
+
|
|
133
|
+
# 默认客户端配置(当配置文件不存在时使用)
|
|
134
|
+
DEFAULT_CLIENT_ID = "client-standard"
|
|
135
|
+
DEFAULT_CLIENT_SECRET = "xqshare-default-secret"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class PermissionError(Exception):
|
|
139
|
+
"""权限错误"""
|
|
140
|
+
def __init__(self, permission: Permission, method: str, level: AccountLevel):
|
|
141
|
+
self.permission = permission
|
|
142
|
+
self.method = method
|
|
143
|
+
self.level = level
|
|
144
|
+
super().__init__(
|
|
145
|
+
f"权限不足: 方法 '{method}' 需要 '{permission.value}' 权限,"
|
|
146
|
+
f"当前账号等级 '{level.value}' 无此权限"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@dataclass
|
|
151
|
+
class ClientConfig:
|
|
152
|
+
"""客户端配置"""
|
|
153
|
+
secret: str
|
|
154
|
+
level: AccountLevel = AccountLevel.FREE
|
|
155
|
+
|
|
156
|
+
@classmethod
|
|
157
|
+
def from_dict(cls, data: dict) -> "ClientConfig":
|
|
158
|
+
"""从字典创建配置"""
|
|
159
|
+
level_str = data.get("level", "free")
|
|
160
|
+
try:
|
|
161
|
+
level = AccountLevel(level_str.lower())
|
|
162
|
+
except ValueError:
|
|
163
|
+
level = AccountLevel.FREE
|
|
164
|
+
return cls(
|
|
165
|
+
secret=data.get("secret", ""),
|
|
166
|
+
level=level
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class PermissionChecker:
|
|
171
|
+
"""权限检查器"""
|
|
172
|
+
|
|
173
|
+
def __init__(self, config_path: Optional[str] = None):
|
|
174
|
+
"""
|
|
175
|
+
初始化权限检查器
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
config_path: 配置文件路径,默认为 ./clients.yaml(当前工作目录)
|
|
179
|
+
"""
|
|
180
|
+
if config_path is None:
|
|
181
|
+
config_path = os.path.join(os.getcwd(), "clients.yaml")
|
|
182
|
+
self.config_path = config_path
|
|
183
|
+
self._clients: Dict[str, ClientConfig] = {}
|
|
184
|
+
self._use_default_client = False # 标记是否使用默认客户端
|
|
185
|
+
# 配置文件热更新相关属性
|
|
186
|
+
self._last_mtime: float = 0 # 文件最后修改时间
|
|
187
|
+
self._last_check_time: float = 0 # 上次检查时间
|
|
188
|
+
self._check_interval: int = 300 # 检查间隔 5 分钟
|
|
189
|
+
self._load_config()
|
|
190
|
+
|
|
191
|
+
def _load_config(self) -> None:
|
|
192
|
+
"""加载配置文件"""
|
|
193
|
+
config_file = Path(self.config_path)
|
|
194
|
+
|
|
195
|
+
if not config_file.exists():
|
|
196
|
+
# 配置文件不存在,使用默认客户端
|
|
197
|
+
self._create_default_client()
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
# 记录文件修改时间
|
|
201
|
+
self._last_mtime = config_file.stat().st_mtime
|
|
202
|
+
|
|
203
|
+
try:
|
|
204
|
+
with open(config_file, "r", encoding="utf-8") as f:
|
|
205
|
+
data = yaml.safe_load(f)
|
|
206
|
+
|
|
207
|
+
if data and "clients" in data:
|
|
208
|
+
for client_id, client_data in data["clients"].items():
|
|
209
|
+
if isinstance(client_data, dict):
|
|
210
|
+
self._clients[client_id] = ClientConfig.from_dict(client_data)
|
|
211
|
+
|
|
212
|
+
# 如果配置为空,使用默认客户端
|
|
213
|
+
if not self._clients:
|
|
214
|
+
self._create_default_client()
|
|
215
|
+
else:
|
|
216
|
+
import logging
|
|
217
|
+
logging.info(f"[配置加载] 有效账号数量: {len(self._clients)}")
|
|
218
|
+
except Exception as e:
|
|
219
|
+
# 配置加载失败,使用默认客户端
|
|
220
|
+
import logging
|
|
221
|
+
logging.warning(f"加载客户端配置失败: {e},使用默认客户端")
|
|
222
|
+
self._create_default_client()
|
|
223
|
+
|
|
224
|
+
def _create_default_client(self) -> None:
|
|
225
|
+
"""创建默认客户端配置"""
|
|
226
|
+
self._use_default_client = True
|
|
227
|
+
self._clients[DEFAULT_CLIENT_ID] = ClientConfig(
|
|
228
|
+
secret=DEFAULT_CLIENT_SECRET,
|
|
229
|
+
level=AccountLevel.STANDARD
|
|
230
|
+
)
|
|
231
|
+
import logging
|
|
232
|
+
logging.warning(f"[配置加载] 未找到 clients.yaml,启用默认账号: {DEFAULT_CLIENT_ID} (level=standard)")
|
|
233
|
+
|
|
234
|
+
def check_and_reload_if_changed(self) -> bool:
|
|
235
|
+
"""
|
|
236
|
+
检查配置文件是否变更,如果变更则重新加载
|
|
237
|
+
|
|
238
|
+
每 5 分钟检查一次文件修改时间,如果检测到变更则重新加载配置。
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
bool: 是否重新加载了配置
|
|
242
|
+
"""
|
|
243
|
+
import time
|
|
244
|
+
current_time = time.time()
|
|
245
|
+
|
|
246
|
+
# 检查间隔未到,跳过
|
|
247
|
+
if current_time - self._last_check_time < self._check_interval:
|
|
248
|
+
return False
|
|
249
|
+
|
|
250
|
+
self._last_check_time = current_time
|
|
251
|
+
|
|
252
|
+
config_file = Path(self.config_path)
|
|
253
|
+
if not config_file.exists():
|
|
254
|
+
return False
|
|
255
|
+
|
|
256
|
+
try:
|
|
257
|
+
current_mtime = config_file.stat().st_mtime
|
|
258
|
+
if current_mtime != self._last_mtime:
|
|
259
|
+
import logging
|
|
260
|
+
logging.info(f"检测到配置文件变更,重新加载: {self.config_path}")
|
|
261
|
+
# 重置状态
|
|
262
|
+
self._clients = {}
|
|
263
|
+
self._use_default_client = False
|
|
264
|
+
# 重新加载
|
|
265
|
+
self._load_config()
|
|
266
|
+
return True
|
|
267
|
+
except Exception as e:
|
|
268
|
+
import logging
|
|
269
|
+
logging.warning(f"检查配置文件变更失败: {e}")
|
|
270
|
+
|
|
271
|
+
return False
|
|
272
|
+
|
|
273
|
+
def get_client_config(self, client_id: str) -> Optional[ClientConfig]:
|
|
274
|
+
"""
|
|
275
|
+
获取客户端配置
|
|
276
|
+
|
|
277
|
+
优先级:
|
|
278
|
+
1. 配置文件中的配置
|
|
279
|
+
2. 环境变量中的配置
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
client_id: 客户端 ID
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
客户端配置,如果未找到返回 None
|
|
286
|
+
"""
|
|
287
|
+
# 优先使用配置文件
|
|
288
|
+
if client_id in self._clients:
|
|
289
|
+
return self._clients[client_id]
|
|
290
|
+
|
|
291
|
+
# 尝试从环境变量获取
|
|
292
|
+
secret = os.environ.get(f"XQSHARE_CLIENT_{client_id}")
|
|
293
|
+
if secret:
|
|
294
|
+
return ClientConfig(secret=secret, level=AccountLevel.FREE)
|
|
295
|
+
|
|
296
|
+
return None
|
|
297
|
+
|
|
298
|
+
def verify_secret(self, client_id: str, client_secret: str) -> tuple[bool, Optional[AccountLevel]]:
|
|
299
|
+
"""
|
|
300
|
+
验证客户端密钥
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
client_id: 客户端 ID
|
|
304
|
+
client_secret: 客户端密钥
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
(验证结果, 账号等级)
|
|
308
|
+
"""
|
|
309
|
+
config = self.get_client_config(client_id)
|
|
310
|
+
|
|
311
|
+
if config is None:
|
|
312
|
+
# 未在配置中找到
|
|
313
|
+
if self._use_default_client:
|
|
314
|
+
# 使用默认客户端模式,拒绝未知客户端
|
|
315
|
+
return False, None
|
|
316
|
+
# 非默认模式,尝试使用环境变量中的默认密钥
|
|
317
|
+
default_secret = os.environ.get("XQSHARE_CLIENT_SECRET", "default-secret")
|
|
318
|
+
if client_secret == default_secret:
|
|
319
|
+
return True, AccountLevel.FREE
|
|
320
|
+
return False, None
|
|
321
|
+
|
|
322
|
+
if config.secret == client_secret:
|
|
323
|
+
return True, config.level
|
|
324
|
+
|
|
325
|
+
return False, None
|
|
326
|
+
|
|
327
|
+
def has_permission(self, level: AccountLevel, permission: Permission) -> bool:
|
|
328
|
+
"""
|
|
329
|
+
检查账号等级是否有指定权限
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
level: 账号等级
|
|
333
|
+
permission: 需要的权限
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
是否有权限
|
|
337
|
+
"""
|
|
338
|
+
return permission in LEVEL_PERMISSIONS.get(level, set())
|
|
339
|
+
|
|
340
|
+
def check_api_permission(
|
|
341
|
+
self,
|
|
342
|
+
level: AccountLevel,
|
|
343
|
+
method: str,
|
|
344
|
+
args: tuple = (),
|
|
345
|
+
kwargs: dict = None
|
|
346
|
+
) -> Optional[PermissionError]:
|
|
347
|
+
"""
|
|
348
|
+
检查 API 调用权限
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
level: 账号等级
|
|
352
|
+
method: API 方法名(可以是 "module.method" 或 "method")
|
|
353
|
+
args: 位置参数
|
|
354
|
+
kwargs: 关键字参数
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
如果权限不足返回 PermissionError,否则返回 None
|
|
358
|
+
"""
|
|
359
|
+
if kwargs is None:
|
|
360
|
+
kwargs = {}
|
|
361
|
+
|
|
362
|
+
# 获取需要的权限
|
|
363
|
+
required_permission = self._get_required_permission(method, args, kwargs)
|
|
364
|
+
|
|
365
|
+
if required_permission is None:
|
|
366
|
+
# 未配置权限的方法
|
|
367
|
+
# STANDARD 及以上级别允许,FREE/PLUS 级别拒绝
|
|
368
|
+
if level in (AccountLevel.FREE, AccountLevel.PLUS):
|
|
369
|
+
return PermissionError(Permission.BASIC, method, level)
|
|
370
|
+
return None
|
|
371
|
+
|
|
372
|
+
# 检查权限
|
|
373
|
+
if not self.has_permission(level, required_permission):
|
|
374
|
+
return PermissionError(required_permission, method, level)
|
|
375
|
+
|
|
376
|
+
return None
|
|
377
|
+
|
|
378
|
+
def _get_required_permission(
|
|
379
|
+
self,
|
|
380
|
+
method: str,
|
|
381
|
+
args: tuple,
|
|
382
|
+
kwargs: dict
|
|
383
|
+
) -> Optional[Permission]:
|
|
384
|
+
"""
|
|
385
|
+
获取 API 调用需要的权限
|
|
386
|
+
|
|
387
|
+
对于 get_market_data 和 get_market_data_ex,
|
|
388
|
+
根据 period 参数动态判断权限
|
|
389
|
+
"""
|
|
390
|
+
# 1. 精确匹配
|
|
391
|
+
if method in API_PERMISSIONS:
|
|
392
|
+
permission = API_PERMISSIONS[method]
|
|
393
|
+
|
|
394
|
+
# 特殊处理:日线/分钟线/tick 数据根据 period 判断
|
|
395
|
+
if method in ("xtdata.get_market_data", "xtdata.get_market_data_ex",
|
|
396
|
+
"xtdata.get_full_kline",
|
|
397
|
+
"xtdata.download_history_data", "xtdata.download_history_data2",
|
|
398
|
+
"download_history_data2"):
|
|
399
|
+
period = self._extract_period(args, kwargs)
|
|
400
|
+
if period in MINUTE_PERIODS:
|
|
401
|
+
return Permission.MINUTE
|
|
402
|
+
elif period in TICK_PERIODS:
|
|
403
|
+
return Permission.TICK
|
|
404
|
+
elif period in DAILY_PERIODS:
|
|
405
|
+
return Permission.DAILY
|
|
406
|
+
# 其他周期默认为日线权限
|
|
407
|
+
|
|
408
|
+
return permission
|
|
409
|
+
|
|
410
|
+
# 2. 通配符匹配(支持 "module.*" 格式)
|
|
411
|
+
for pattern, permission in API_PERMISSIONS.items():
|
|
412
|
+
if "*" in pattern:
|
|
413
|
+
# 将通配符模式转换为前缀匹配
|
|
414
|
+
prefix = pattern.rstrip("*")
|
|
415
|
+
if method.startswith(prefix):
|
|
416
|
+
return permission
|
|
417
|
+
|
|
418
|
+
return None
|
|
419
|
+
|
|
420
|
+
def _extract_period(self, args: tuple, kwargs: dict) -> str:
|
|
421
|
+
"""
|
|
422
|
+
从参数中提取 period 值
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
args: 位置参数
|
|
426
|
+
kwargs: 关键字参数
|
|
427
|
+
|
|
428
|
+
Returns:
|
|
429
|
+
period 值,默认 "1d"
|
|
430
|
+
"""
|
|
431
|
+
# 优先从 kwargs 获取
|
|
432
|
+
if "period" in kwargs:
|
|
433
|
+
return kwargs["period"]
|
|
434
|
+
|
|
435
|
+
# 常见 API 的 period 参数位置:
|
|
436
|
+
# get_market_data(stock_list, period='1d', ...)
|
|
437
|
+
# download_history_data(stock_code, period='1d', ...)
|
|
438
|
+
# download_history_data2(stock_list, period='1d', ...)
|
|
439
|
+
|
|
440
|
+
# 对于大多数 API,period 是第二个位置参数(索引 1)
|
|
441
|
+
if len(args) >= 2 and isinstance(args[1], str):
|
|
442
|
+
return args[1]
|
|
443
|
+
|
|
444
|
+
# download_history_data 的 period 是第一个位置参数(索引 0)之后的
|
|
445
|
+
if len(args) >= 2 and isinstance(args[0], str) and isinstance(args[1], str):
|
|
446
|
+
return args[1]
|
|
447
|
+
|
|
448
|
+
return "1d"
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
# 全局权限检查器实例
|
|
452
|
+
_permission_checker: Optional[PermissionChecker] = None
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def get_permission_checker(config_path: Optional[str] = None) -> PermissionChecker:
|
|
456
|
+
"""获取全局权限检查器实例"""
|
|
457
|
+
global _permission_checker
|
|
458
|
+
if _permission_checker is None or config_path is not None:
|
|
459
|
+
_permission_checker = PermissionChecker(config_path)
|
|
460
|
+
return _permission_checker
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def reset_permission_checker():
|
|
464
|
+
"""重置权限检查器(主要用于测试)"""
|
|
465
|
+
global _permission_checker
|
|
466
|
+
_permission_checker = None
|