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 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