bizonquant-mcp 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,178 @@
1
+ # ==============================================================================
2
+ # 1. Environment & Packages
3
+ # ==============================================================================
4
+ .venv/
5
+ env/
6
+ venv/
7
+ ENV/
8
+ .python-version
9
+ node_modules/
10
+ .npm/
11
+ *.local
12
+ /__pycache__/
13
+ # ==============================================================================
14
+ # 2. Caches, Build & OS
15
+ # ==============================================================================
16
+ __pycache__/
17
+ *.py[cod]
18
+ *$py.class
19
+ .pytest_cache/
20
+ .mypy_cache/
21
+ .ruff_cache/
22
+ .eslintcache
23
+ .parcel-cache
24
+ .vite/
25
+ .npm-global/
26
+ .DS_Store
27
+ Thumbs.db
28
+ *.swp
29
+ *.swo
30
+ *.bak
31
+ *.bak_*
32
+ .project
33
+ .pydevproject
34
+ .settings/
35
+ build/
36
+ dist/
37
+ out/
38
+ electron/dist/
39
+ electron/out/
40
+ .deploy_tmp/
41
+ .deploy_fix/
42
+ *.docker-build-lock
43
+ .docker/
44
+ .qwen
45
+ # ==============================================================================
46
+ # 3. AI, IDE & Local Tools
47
+ # ==============================================================================
48
+ .vscode/
49
+ .idea/
50
+ .trae/
51
+ .codex/
52
+ .codex_logs/
53
+ .copilot/
54
+ .gemini/
55
+ .geminiignore
56
+ .hintrc
57
+ .traeignore
58
+ .antigravityignore
59
+ .workbuddy/
60
+ .claude/
61
+ .omo/
62
+ .playwright-mcp/
63
+ .codegraph/
64
+
65
+ # ==============================================================================
66
+ # 4. Data, ML & Results
67
+ # ==============================================================================
68
+ logs/
69
+ *.log
70
+ # Databases
71
+ *.duckdb
72
+ *.duckdb.tmp/
73
+ *.sqlite
74
+ *.sqlite3
75
+ *.db
76
+ *.db-journal
77
+ *.db-wal
78
+ *.db-shm
79
+
80
+ # Qlib & Large Data Files
81
+ db/qlib_data/**/*.bin
82
+ db/qlib_data/**/*.data
83
+ !db/qlib_data/
84
+ !db/qlib_data/**/
85
+ !db/qlib_data/**/*.txt
86
+ db/qlib_bin/
87
+ db/qlib_csv/
88
+ db/*.tar.gz
89
+ db.tar.gz
90
+ *.bin
91
+ *.parquet
92
+ # 测试夹具是代码的一部分,必须入库——否则干净克隆上读取测试直接失败
93
+ !electron/electron/services/__tests__/fixtures/*.parquet
94
+ *.csv
95
+ !backend/services/trade/data/corporate_actions.csv
96
+ !backend/tests/**/*.csv
97
+ !data/examples/**/*.csv
98
+ data/backtest_results/
99
+ userdata/
100
+
101
+ # Test Coverage Artifacts
102
+ .coverage
103
+ coverage.xml
104
+ htmlcov/
105
+
106
+ # Runtime user data
107
+ user_pools_local/
108
+ data/user_strategies/
109
+
110
+ # Temp files
111
+ tmp/
112
+ temp/
113
+ .temp/
114
+ run/*.pid
115
+
116
+ # ==============================================================================
117
+ # 5. Secrets & Config
118
+ # ==============================================================================
119
+ .env
120
+ .env.local
121
+ .env.*.local
122
+ !.env.example
123
+ *.pem
124
+ *.key
125
+ .ssh/
126
+ /db/*.sql
127
+ /redis
128
+ /data/ai_ide/tmp_run
129
+ /electron/icon.iconset
130
+ # ==============================================================================
131
+ # 5. Marketing Files
132
+ # ==============================================================================
133
+ marketing/
134
+
135
+ # ==============================================================================
136
+ # 6. Repository Specific Exceptions
137
+ # ==============================================================================
138
+ !.github/
139
+ redis/data/appendonlydir/appendonly.aof.2.incr.aof
140
+ # Windows specific build artifacts
141
+ *.exe
142
+ *.msi
143
+ electron/public/fonts/AlibabaPuHuiTi-Bold.ttf
144
+ electron/public/fonts/AlibabaPuHuiTi-Regular.ttf
145
+ data/ai_ide/config.json
146
+ db/diagram/postgresql_schema.dot
147
+ db/backups/*.gz
148
+ !db/feature_snapshots/*.json
149
+ website/download-config.json
150
+
151
+ # Ignore db subdirectories
152
+ /db/backups/
153
+ /db/custom/
154
+ /db/feature_snapshots/
155
+ /db/qlib_data/
156
+ /models/users/
157
+ data/user_10000001
158
+ db/csmar
159
+ /website/
160
+ /db/
161
+ /sync_to_gitee.sh
162
+
163
+ # 运行时产物(同步暂存/审计报告/临时检查输出),不入库
164
+ /audit/
165
+ /data/sync_staging/
166
+ data/backups/
167
+ data/uploads/
168
+
169
+ # 生产 PG 备份产物(scripts/ops/db/pg_backup.sh),不入库
170
+ /backups/
171
+ *.sql.gz
172
+
173
+ # 外部 agent skills(npx skills add 可重装的依赖,不是源码;软链在 .claude/ 下,已被第 60 行覆盖)
174
+ /.agents/
175
+
176
+ # Orca 工作树与本地工具产物,不入库
177
+ /.orca/
178
+ /.reasonix/
@@ -0,0 +1,84 @@
1
+ Metadata-Version: 2.5
2
+ Name: bizonquant-mcp
3
+ Version: 0.1.0
4
+ Summary: BizonQuant MCP server —— 把量化能力面(行情/多因子选股/回测)开放给外部 agent
5
+ License: MIT
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: httpx>=0.27
8
+ Requires-Dist: mcp>=1.9
9
+ Description-Content-Type: text/markdown
10
+
11
+ # bizonquant-mcp
12
+
13
+ BizonQuant 量化平台的 MCP server(客户端形态):跑在你本机,把平台能力面开放给外部 agent(Claude Code / Cursor / Codex / 自写 agent),**服务器零常驻进程、不开前端**。
14
+
15
+ ## 能力(首期 15 工具)
16
+
17
+ | 组 | 工具 | 说明 |
18
+ |---|---|---|
19
+ | 锚点 | `get_me_tool` | 当前用户;凭证探针 |
20
+ | 锚点 | `get_trading_day_info_tool` | 交易日判断 + 上/下一交易日 |
21
+ | 锚点 | `get_data_coverage_tool` | 数据覆盖/因子数/股票池——回测日期边界来源 |
22
+ | 行情 | `search_stocks_tool` | 模糊名 → 规范代码 |
23
+ | 行情 | `get_daily_klines_tool` | K 线 + 区间摘要(涨跌/高低/MA20/MA60/站上均线) |
24
+ | 选股 | `list_factors_tool` | 因子目录 + 取值语义(防条件翻译错) |
25
+ | 选股 | `screen_stocks_tool` | 即时截面选股,附因子值 |
26
+ | 选股 | `list_presets_tool` / `save_preset_tool` | 方案列表 / 存方案 |
27
+ | 回测 | `submit_backtest_tool` | 提交多因子回测(异步) |
28
+ | 回测 | `wait_backtest_tool` | 有界等待(≤50s),done:false 续调 |
29
+ | 回测 | `get_backtest_status_tool` / `get_backtest_result_tool` / `list_backtests_tool` | 状态 / 结果(曲线抽样)/ 历史 |
30
+ | 总览 | `list_my_strategies_tool` | 全部策略与运行状态 |
31
+
32
+ ## 安装与运行
33
+
34
+ ```bash
35
+ # 从仓库根
36
+ cd mcp-server && uv sync
37
+ uv run bizonquant-mcp init # 录入 key 并自检(写入 ~/.config/bizonquant-mcp/config.json,0600)
38
+ uv run bizonquant-mcp doctor # 探链路:health / token / whoami / 数据面
39
+ ```
40
+
41
+ ## 接入 Claude Code(stdio)
42
+
43
+ ```jsonc
44
+ // ~/.claude.json 的 mcpServers
45
+ "bizonquant": {
46
+ "command": "uv",
47
+ "args": ["--directory", "<仓库路径>/mcp-server", "run", "bizonquant-mcp"],
48
+ "env": {
49
+ "BIZONQUANT_ACCESS_KEY": "qm_live_xxx",
50
+ "BIZONQUANT_SECRET_KEY": "sk_yyy",
51
+ "BIZONQUANT_BASE_URL": "https://bizon.naiyu.xyz:8443"
52
+ }
53
+ }
54
+ ```
55
+
56
+ 也支持单变量 `BIZONQUANT_API_KEY=access_key.secret_key`。
57
+
58
+ ## 拿凭证(三选一)
59
+
60
+ 1. **桌面端**:API 密钥页建 key,permissions 勾 `mcp.read`(只读)或 `mcp.*`(含 save_preset / submit_backtest 写点)
61
+ 2. **过渡 curl**:登录(注意 device_type)→ `POST /api/v1/api-keys`(`{"name":"mcp","permissions":["mcp.*"]}`,secret 只显示一次)
62
+ 3. 桌面端「Agent 接入」UI(待建,阶段 2)
63
+
64
+ ## 客户端纪律(内置,无需配置)
65
+
66
+ - 默认拒绝 `http://` 明文网关(`BIZONQUANT_ALLOW_INSECURE=1` 可解锁内网场景)
67
+ - token 只走 Authorization 头;401 自动换发一次,仍 401 即停(key 撤销后不打风暴)
68
+ - UA 带 `bizonquant-mcp/{version}`,服务端可审计
69
+
70
+ ## 安全模型
71
+
72
+ - 凭证是 API Key(access_key + bcrypt secret),不是账号密码——泄漏面仅限该 key 的能力面
73
+ - 服务端对 agent token 施加 **scope 硬闸**:GET 全放,非 GET 只放 5 条写白名单(选股/存方案/回测提交),`/admin` 与 `/api-keys` 一律 403
74
+ - 交易/资金类端点不在能力面内;删 key / 停用 / 轮换 secret 即时全链失效
75
+ - `revoke` 命令 = 同 key 再换发一次(互踢规则下旧会话全灭);永久吊销请在桌面端删 key
76
+
77
+ ## 开发
78
+
79
+ ```bash
80
+ uv run pytest -q # 全量测试(含与网关 scope 闸、TS 工具表的 (method,path) 对拍)
81
+ uv run bizonquant-mcp --version
82
+ ```
83
+
84
+ 对拍闸:`tests/test_conformance.py` 断言本包工具的 (method, path) ⊆ 网关 `AGENT_WRITE_ALLOWLIST` ∪ GET——网关收窄或工具新增写路径而网关未放行时,测试立即红。
@@ -0,0 +1,74 @@
1
+ # bizonquant-mcp
2
+
3
+ BizonQuant 量化平台的 MCP server(客户端形态):跑在你本机,把平台能力面开放给外部 agent(Claude Code / Cursor / Codex / 自写 agent),**服务器零常驻进程、不开前端**。
4
+
5
+ ## 能力(首期 15 工具)
6
+
7
+ | 组 | 工具 | 说明 |
8
+ |---|---|---|
9
+ | 锚点 | `get_me_tool` | 当前用户;凭证探针 |
10
+ | 锚点 | `get_trading_day_info_tool` | 交易日判断 + 上/下一交易日 |
11
+ | 锚点 | `get_data_coverage_tool` | 数据覆盖/因子数/股票池——回测日期边界来源 |
12
+ | 行情 | `search_stocks_tool` | 模糊名 → 规范代码 |
13
+ | 行情 | `get_daily_klines_tool` | K 线 + 区间摘要(涨跌/高低/MA20/MA60/站上均线) |
14
+ | 选股 | `list_factors_tool` | 因子目录 + 取值语义(防条件翻译错) |
15
+ | 选股 | `screen_stocks_tool` | 即时截面选股,附因子值 |
16
+ | 选股 | `list_presets_tool` / `save_preset_tool` | 方案列表 / 存方案 |
17
+ | 回测 | `submit_backtest_tool` | 提交多因子回测(异步) |
18
+ | 回测 | `wait_backtest_tool` | 有界等待(≤50s),done:false 续调 |
19
+ | 回测 | `get_backtest_status_tool` / `get_backtest_result_tool` / `list_backtests_tool` | 状态 / 结果(曲线抽样)/ 历史 |
20
+ | 总览 | `list_my_strategies_tool` | 全部策略与运行状态 |
21
+
22
+ ## 安装与运行
23
+
24
+ ```bash
25
+ # 从仓库根
26
+ cd mcp-server && uv sync
27
+ uv run bizonquant-mcp init # 录入 key 并自检(写入 ~/.config/bizonquant-mcp/config.json,0600)
28
+ uv run bizonquant-mcp doctor # 探链路:health / token / whoami / 数据面
29
+ ```
30
+
31
+ ## 接入 Claude Code(stdio)
32
+
33
+ ```jsonc
34
+ // ~/.claude.json 的 mcpServers
35
+ "bizonquant": {
36
+ "command": "uv",
37
+ "args": ["--directory", "<仓库路径>/mcp-server", "run", "bizonquant-mcp"],
38
+ "env": {
39
+ "BIZONQUANT_ACCESS_KEY": "qm_live_xxx",
40
+ "BIZONQUANT_SECRET_KEY": "sk_yyy",
41
+ "BIZONQUANT_BASE_URL": "https://bizon.naiyu.xyz:8443"
42
+ }
43
+ }
44
+ ```
45
+
46
+ 也支持单变量 `BIZONQUANT_API_KEY=access_key.secret_key`。
47
+
48
+ ## 拿凭证(三选一)
49
+
50
+ 1. **桌面端**:API 密钥页建 key,permissions 勾 `mcp.read`(只读)或 `mcp.*`(含 save_preset / submit_backtest 写点)
51
+ 2. **过渡 curl**:登录(注意 device_type)→ `POST /api/v1/api-keys`(`{"name":"mcp","permissions":["mcp.*"]}`,secret 只显示一次)
52
+ 3. 桌面端「Agent 接入」UI(待建,阶段 2)
53
+
54
+ ## 客户端纪律(内置,无需配置)
55
+
56
+ - 默认拒绝 `http://` 明文网关(`BIZONQUANT_ALLOW_INSECURE=1` 可解锁内网场景)
57
+ - token 只走 Authorization 头;401 自动换发一次,仍 401 即停(key 撤销后不打风暴)
58
+ - UA 带 `bizonquant-mcp/{version}`,服务端可审计
59
+
60
+ ## 安全模型
61
+
62
+ - 凭证是 API Key(access_key + bcrypt secret),不是账号密码——泄漏面仅限该 key 的能力面
63
+ - 服务端对 agent token 施加 **scope 硬闸**:GET 全放,非 GET 只放 5 条写白名单(选股/存方案/回测提交),`/admin` 与 `/api-keys` 一律 403
64
+ - 交易/资金类端点不在能力面内;删 key / 停用 / 轮换 secret 即时全链失效
65
+ - `revoke` 命令 = 同 key 再换发一次(互踢规则下旧会话全灭);永久吊销请在桌面端删 key
66
+
67
+ ## 开发
68
+
69
+ ```bash
70
+ uv run pytest -q # 全量测试(含与网关 scope 闸、TS 工具表的 (method,path) 对拍)
71
+ uv run bizonquant-mcp --version
72
+ ```
73
+
74
+ 对拍闸:`tests/test_conformance.py` 断言本包工具的 (method, path) ⊆ 网关 `AGENT_WRITE_ALLOWLIST` ∪ GET——网关收窄或工具新增写路径而网关未放行时,测试立即红。
@@ -0,0 +1,32 @@
1
+ [project]
2
+ name = "bizonquant-mcp"
3
+ version = "0.1.0"
4
+ description = "BizonQuant MCP server —— 把量化能力面(行情/多因子选股/回测)开放给外部 agent"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ license = { text = "MIT" }
8
+ dependencies = [
9
+ "mcp>=1.9",
10
+ "httpx>=0.27",
11
+ ]
12
+
13
+ [project.scripts]
14
+ bizonquant-mcp = "bizonquant_mcp.cli:main"
15
+
16
+ [build-system]
17
+ requires = ["hatchling"]
18
+ build-backend = "hatchling.build"
19
+
20
+ [tool.hatch.build.targets.wheel]
21
+ packages = ["src/bizonquant_mcp"]
22
+
23
+ [dependency-groups]
24
+ dev = [
25
+ "pytest>=8",
26
+ "pytest-asyncio>=0.24",
27
+ "respx>=0.22",
28
+ ]
29
+
30
+ [tool.pytest.ini_options]
31
+ asyncio_mode = "auto"
32
+ testpaths = ["tests"]
@@ -0,0 +1,19 @@
1
+ """BizonQuant MCP server:把平台能力面(行情/多因子选股/回测)开放给外部 agent。
2
+
3
+ 客户端形态(调研报告 C 方案):本包是 stdio MCP server,跑在用户本机,
4
+ 所有工具都是对公网网关 REST 的薄封装——服务器零常驻进程。
5
+ """
6
+
7
+ from bizonquant_mcp.gateway import (
8
+ BizonQuantError,
9
+ CredentialError,
10
+ GatewayClient,
11
+ )
12
+ from bizonquant_mcp.version import __version__
13
+
14
+ __all__ = [
15
+ "BizonQuantError",
16
+ "CredentialError",
17
+ "GatewayClient",
18
+ "__version__",
19
+ ]
@@ -0,0 +1,5 @@
1
+ """python -m bizonquant_mcp → stdio MCP server。"""
2
+
3
+ from bizonquant_mcp.server import main
4
+
5
+ main()
@@ -0,0 +1,168 @@
1
+ """CLI:init / doctor / revoke——从零到可用全程不开前端。
2
+
3
+ init 交互式录入 access_key/secret_key/base_url,写配置文件(0600),
4
+ 并跑一遍 doctor 验证
5
+ doctor 探链路:health → 换发 token → whoami → 数据覆盖摘要
6
+ revoke 用同 key 再换发一次:互踢规则下同 key 旧会话全部失效
7
+ (key 本体的删除/停用需在桌面端或 API 管理)
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import json
14
+ import os
15
+ import stat
16
+ import sys
17
+ from pathlib import Path
18
+
19
+ from bizonquant_mcp import __version__
20
+ from bizonquant_mcp.gateway import DEFAULT_BASE_URL, BizonQuantError, GatewayClient
21
+
22
+ CONFIG_PATH = (
23
+ Path(os.environ.get("BIZONQUANT_CONFIG", ""))
24
+ or Path.home() / ".config" / "bizonquant-mcp" / "config.json"
25
+ )
26
+
27
+
28
+ def _load_or_prompt(access: str | None, secret: str | None, base_url: str | None) -> tuple[str, str, str]:
29
+ env_access = os.environ.get("BIZONQUANT_ACCESS_KEY")
30
+ env_secret = os.environ.get("BIZONQUANT_SECRET_KEY")
31
+ access = access or env_access
32
+ secret = secret or env_secret
33
+ base_url = base_url or os.environ.get("BIZONQUANT_BASE_URL") or DEFAULT_BASE_URL
34
+
35
+ if not (access and secret) and CONFIG_PATH.exists():
36
+ cfg = json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
37
+ access = access or cfg.get("access_key")
38
+ secret = secret or cfg.get("secret_key")
39
+ base_url = base_url or cfg.get("base_url") or DEFAULT_BASE_URL
40
+
41
+ if not (access and secret):
42
+ if not sys.stdin.isatty():
43
+ raise BizonQuantError("非交互环境且未提供凭证(--access-key/--secret-key 或环境变量)")
44
+ print("BizonQuant MCP 凭证初始化")
45
+ print("(access_key/secret_key 在桌面端「API 密钥」页创建,permissions 勾选 mcp.read / mcp.write)")
46
+ access = access or input("access_key: ").strip()
47
+ secret = secret or input("secret_key: ").strip()
48
+ custom = input(f"base_url [{DEFAULT_BASE_URL}]: ").strip()
49
+ base_url = custom or base_url
50
+ return access, secret, base_url
51
+
52
+
53
+ def cmd_init(args: argparse.Namespace) -> int:
54
+ access, secret, base_url = _load_or_prompt(args.access_key, args.secret_key, args.base_url)
55
+ CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
56
+ CONFIG_PATH.write_text(
57
+ json.dumps(
58
+ {"access_key": access, "secret_key": secret, "base_url": base_url},
59
+ ensure_ascii=False,
60
+ indent=2,
61
+ ),
62
+ encoding="utf-8",
63
+ )
64
+ CONFIG_PATH.chmod(stat.S_IRUSR | stat.S_IWUSR) # 0600:secret 落盘仅本人可读
65
+ print(f"配置已写入 {CONFIG_PATH}")
66
+ return cmd_doctor(args)
67
+
68
+
69
+ def cmd_doctor(args: argparse.Namespace) -> int:
70
+ access, secret, base_url = _load_or_prompt(args.access_key, args.secret_key, args.base_url)
71
+ gw = GatewayClient(access_key=access, secret_key=secret, base_url=base_url)
72
+ ok = True
73
+
74
+ async def _run() -> None:
75
+ nonlocal ok
76
+
77
+ try:
78
+ resp = await gw._http.get("/health")
79
+ print(f"1) 网关可达:{base_url} → HTTP {resp.status_code}")
80
+ except Exception as exc: # noqa: BLE001 - doctor 要把任何异常转成人话
81
+ print(f"1) 网关不可达:{exc}")
82
+ ok = False
83
+ return
84
+ try:
85
+ await gw._fetch_token()
86
+ print("2) agent-token 换发:成功(scope=agent)")
87
+ except Exception as exc: # noqa: BLE001
88
+ print(f"2) agent-token 换发失败:{exc}")
89
+ ok = False
90
+ return
91
+ try:
92
+ me = await gw.get("/api/v1/users/me")
93
+ user = me
94
+ if isinstance(me, dict):
95
+ user = me.get("user") or me.get("data", {}).get("user") or me.get("data") or me
96
+ print(
97
+ f"3) whoami:{user.get('username', '?')}(tenant={user.get('tenant_id', '?')})"
98
+ if isinstance(user, dict)
99
+ else f"3) whoami:{me}"
100
+ )
101
+ except Exception as exc: # noqa: BLE001
102
+ print(f"3) whoami 失败:{exc}")
103
+ ok = False
104
+ try:
105
+ cov = await gw.get("/api/v1/selection/multi-factor/metadata")
106
+ keys = sorted(cov.keys()) if isinstance(cov, dict) else type(cov).__name__
107
+ print(f"4) 数据面 metadata:{keys}")
108
+ except Exception as exc: # noqa: BLE001
109
+ print(f"4) 数据面不可用:{exc}")
110
+
111
+ import asyncio
112
+
113
+ asyncio.run(_run())
114
+ print("doctor: PASS" if ok else "doctor: FAIL")
115
+ return 0 if ok else 1
116
+
117
+
118
+ def cmd_revoke(args: argparse.Namespace) -> int:
119
+ access, secret, base_url = _load_or_prompt(args.access_key, args.secret_key, args.base_url)
120
+ gw = GatewayClient(access_key=access, secret_key=secret, base_url=base_url)
121
+
122
+ async def _run() -> None:
123
+ # 同 key 再换发一次 → 互踢规则下该 key 的全部旧会话立即失效
124
+ await gw._fetch_token()
125
+ print("已换发新会话:同 key 的旧 agent token 全部 401。")
126
+ print("如需永久吊销,请在桌面端删除该 API Key。")
127
+
128
+ import asyncio
129
+
130
+ asyncio.run(_run())
131
+ return 0
132
+
133
+
134
+ def main() -> None:
135
+ if len(sys.argv) == 1:
136
+ # 无参数(console script 默认)= 启动 MCP stdio server
137
+ from bizonquant_mcp.server import main as server_main
138
+
139
+ server_main()
140
+ return
141
+
142
+ parser = argparse.ArgumentParser(prog="bizonquant-mcp", description=__doc__)
143
+ parser.add_argument("--version", action="version", version=f"bizonquant-mcp {__version__}")
144
+ sub = parser.add_subparsers(dest="cmd", required=True)
145
+
146
+ def _common(p: argparse.ArgumentParser) -> None:
147
+ p.add_argument("--access-key", help="覆盖配置/环境变量")
148
+ p.add_argument("--secret-key", help="覆盖配置/环境变量")
149
+ p.add_argument("--base-url", help="覆盖配置/环境变量")
150
+
151
+ p_init = sub.add_parser("init", help="录入凭证写配置文件并自检")
152
+ _common(p_init)
153
+ p_init.set_defaults(func=cmd_init)
154
+
155
+ p_doc = sub.add_parser("doctor", help="探链路:health/token/whoami/数据面")
156
+ _common(p_doc)
157
+ p_doc.set_defaults(func=cmd_doctor)
158
+
159
+ p_rev = sub.add_parser("revoke", help="踢掉同 key 全部旧会话")
160
+ _common(p_rev)
161
+ p_rev.set_defaults(func=cmd_revoke)
162
+
163
+ args = parser.parse_args()
164
+ try:
165
+ sys.exit(args.func(args))
166
+ except BizonQuantError as exc:
167
+ print(f"错误:{exc}", file=sys.stderr)
168
+ sys.exit(2)
@@ -0,0 +1,38 @@
1
+ """工具公共件:响应解包与输出裁剪(移植 QuantBot TS 工具语义)。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from typing import Any
7
+
8
+
9
+ def unwrap(data: Any) -> Any:
10
+ """网关两种响应包装:{code,message,data:{...}} 与顶层平铺——统一取载荷。"""
11
+ if isinstance(data, dict) and "data" in data and isinstance(data["data"], dict):
12
+ inner = data["data"]
13
+ # 防误伤:business payload 里也可能有 data 键(如 klines bars),
14
+ # 仅当外层像标准包装(有 code/message 或唯一 data 键)时才解包
15
+ if "code" in data or "message" in data or set(data) == {"data"}:
16
+ return inner
17
+ return data
18
+
19
+
20
+ def trim_curve(points: list[Any], max_points: int = 120) -> list[Any]:
21
+ """曲线抽样:≤max_points 原样返回,超过等距抽 max_points 点(保首尾)。"""
22
+ if len(points) <= max_points:
23
+ return points
24
+ step = (len(points) - 1) / (max_points - 1)
25
+ return [points[round(i * step)] for i in range(max_points)]
26
+
27
+
28
+ def sma(values: list[float], window: int) -> float | None:
29
+ """简单均线;样本不足返回 None。"""
30
+ if len(values) < window:
31
+ return None
32
+ return sum(values[-window:]) / window
33
+
34
+
35
+ def safe_round(value: float | None, digits: int = 4) -> float | None:
36
+ if value is None or (isinstance(value, float) and math.isnan(value)):
37
+ return None
38
+ return round(value, digits)