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.
- bizonquant_mcp-0.1.0/.gitignore +178 -0
- bizonquant_mcp-0.1.0/PKG-INFO +84 -0
- bizonquant_mcp-0.1.0/README.md +74 -0
- bizonquant_mcp-0.1.0/pyproject.toml +32 -0
- bizonquant_mcp-0.1.0/src/bizonquant_mcp/__init__.py +19 -0
- bizonquant_mcp-0.1.0/src/bizonquant_mcp/__main__.py +5 -0
- bizonquant_mcp-0.1.0/src/bizonquant_mcp/cli.py +168 -0
- bizonquant_mcp-0.1.0/src/bizonquant_mcp/common.py +38 -0
- bizonquant_mcp-0.1.0/src/bizonquant_mcp/gateway.py +166 -0
- bizonquant_mcp-0.1.0/src/bizonquant_mcp/server.py +71 -0
- bizonquant_mcp-0.1.0/src/bizonquant_mcp/tools/__init__.py +3 -0
- bizonquant_mcp-0.1.0/src/bizonquant_mcp/tools/backtest.py +203 -0
- bizonquant_mcp-0.1.0/src/bizonquant_mcp/tools/market.py +210 -0
- bizonquant_mcp-0.1.0/src/bizonquant_mcp/tools/screening.py +181 -0
- bizonquant_mcp-0.1.0/src/bizonquant_mcp/version.py +3 -0
- bizonquant_mcp-0.1.0/tests/test_conformance.py +111 -0
- bizonquant_mcp-0.1.0/tests/test_gateway.py +131 -0
- bizonquant_mcp-0.1.0/tests/test_tools.py +165 -0
- bizonquant_mcp-0.1.0/uv.lock +865 -0
|
@@ -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,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)
|