Skip to content

Commit 4d47498

Browse files
committed
fix: deduplicate market search and improve MCP error contracts
1 parent 9b89a77 commit 4d47498

10 files changed

Lines changed: 329 additions & 75 deletions

File tree

‎backend_api_python/app/data/market_symbols_seed.py‎

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,22 @@
1414
logger = get_logger(__name__)
1515

1616

17+
_SEARCHABLE_SYMBOLS = """(
18+
SELECT market, symbol, name, sort_order, is_active, is_hot
19+
FROM (
20+
SELECT market, symbol, name, sort_order, is_active,
21+
MAX(is_hot) OVER (PARTITION BY market, UPPER(symbol)) AS is_hot,
22+
ROW_NUMBER() OVER (
23+
PARTITION BY market, UPPER(symbol)
24+
ORDER BY sort_order DESC, id ASC
25+
) AS occurrence
26+
FROM qd_market_symbols
27+
WHERE is_active = 1
28+
) ranked
29+
WHERE occurrence = 1
30+
)"""
31+
32+
1733
def _get_db_connection():
1834
"""Get database connection, returns None if not available."""
1935
try:
@@ -42,8 +58,8 @@ def get_hot_symbols(market: str, limit: int = 10) -> List[Dict]:
4258
with _get_db_connection() as db:
4359
cur = db.cursor()
4460
cur.execute(
45-
"""
46-
SELECT market, symbol, name FROM qd_market_symbols
61+
f"""
62+
SELECT market, symbol, name FROM {_SEARCHABLE_SYMBOLS} s
4763
WHERE market = ? AND is_active = 1 AND is_hot = 1
4864
ORDER BY sort_order DESC
4965
LIMIT ?
@@ -82,8 +98,8 @@ def search_symbols(market: str, keyword: str, limit: int = 20) -> List[Dict]:
8298
with _get_db_connection() as db:
8399
cur = db.cursor()
84100
cur.execute(
85-
"""
86-
SELECT market, symbol, name FROM qd_market_symbols
101+
f"""
102+
SELECT market, symbol, name FROM {_SEARCHABLE_SYMBOLS} s
87103
WHERE market = ? AND is_active = 1 AND UPPER(symbol) = UPPER(?)
88104
LIMIT ?
89105
""",
@@ -98,8 +114,8 @@ def search_symbols(market: str, keyword: str, limit: int = 20) -> List[Dict]:
98114
]
99115

100116
cur.execute(
101-
"""
102-
SELECT market, symbol, name FROM qd_market_symbols s
117+
f"""
118+
SELECT market, symbol, name FROM {_SEARCHABLE_SYMBOLS} s
103119
WHERE market = ? AND is_active = 1
104120
AND (
105121
UPPER(symbol) LIKE UPPER(?)
@@ -168,8 +184,8 @@ def _search_symbols_without_aliases(market: str, keyword: str, limit: int) -> Li
168184
with _get_db_connection() as db:
169185
cur = db.cursor()
170186
cur.execute(
171-
"""
172-
SELECT market, symbol, name FROM qd_market_symbols
187+
f"""
188+
SELECT market, symbol, name FROM {_SEARCHABLE_SYMBOLS} s
173189
WHERE market = ? AND is_active = 1
174190
AND (UPPER(symbol) LIKE UPPER(?) OR UPPER(name) LIKE UPPER(?))
175191
ORDER BY
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# 插件协议与搜索修复 — 2026-09-11
2+
3+
## 完成的修复
4+
5+
- MCP 0.5.1 在协议注册边界识别 HTTP 错误、业务错误、验证失败和未确认操作,返回 `CallToolResult(isError=true)`,保留脱敏后的内容与结构化错误。正常空账户不被误标为失败,已有内部工具函数调用方式不变。
6+
- 全部 58 个上游工具增加四类风险注解;加上连接器共 60 个,实际注册后 60 个均有注解。写操作保守标记为非幂等,避免旧后端缺少防重时调用方自动重试;注解不代替权限与确认检查。
7+
- 搜索目录按 market + 大写 symbol 合并,再排序和分页;同名不同市场保留,优先保留排序值高的记录。精确匹配、模糊/别名匹配、旧表回退和热门列表均覆盖,热门标记取同组任一活跃记录。没有删除或修改目录数据库记录。
8+
9+
## 验证
10+
11+
- MCP:38 项测试通过。
12+
- 后端搜索及相关回归:37 项测试通过,包括 5 项新增 SQL 执行测试。
13+
- 插件连接器与离线安装:26 项通过;组合首次安装/连接轮换用例 45.8 秒。
14+
- 合计 101 项自动化测试通过。没有真实下单、启动策略或发送通知。
15+
- 新插件原始启动入口连接现有线上服务:不存在的策略、语法错误、危险指标导入、Alpaca 账户错误,全部返回 `isError=true`;健康检查仍正常。
16+
- 用修复后的搜索函数在本机 backend 容器内执行只读 PostgreSQL 查询,NVDA 返回一条。这是独立进程验证,没有修改运行中的服务代码。
17+
18+
## 插件交付
19+
20+
- MCP 包:0.5.1,wheel 已构建,未上传 PyPI。
21+
- 本地插件已重装为 `0.1.0+codex.20260911082228`。
22+
- 离线运行包 SHA-256:`8688b279add72cf8a8bda2a8b78e35d3713063e915c4d38b9e58fab83f96b5eb`。
23+
- 安装目录与经过测试的运行包 manifest 一致。最后一次重装仅同步了文档,运行包内容未变。
24+
- 可分发 ZIP:`outputs/plugin-fixes-20260911/quantdinger-plugin-mcp-0.5.1.zip`(相对于工作区根目录),SHA-256:`635a50a5ec9c2b324de7e8aaa9b31b5b8dc6e3f0bd0d078dad221ca3b3df2baf`。
25+
- Codex 新任务才能可靠加载更新的工具。没有提交到公开插件市场。
26+
27+
## Alpaca 环境错误的解释
28+
29+
已观察的账户 #983 为 `exchange_id=alpaca`、`environment=demo`。`UNSUPPORTED_TRADING_ENVIRONMENT` 出自 QuantDinger 自己的 `live_trading/factory.py` 环境校验,并非 Alpaca 返回的鉴权错误。
30+
31+
旧版本的允许列表没有 Alpaca,因而回退到只允许 live;demo 在创建券商客户端前就被拒绝。对两个现有容器的校验函数做无网络验证:
32+
33+
| 容器 | 输入 | 结果 |
34+
|---|---|---|
35+
| 新 backend(3fdc2709cd72) | alpaca / demo / both | 接受 |
36+
| 旧 trading-worker(a5e2fe718223) | alpaca / demo / both | UNSUPPORTED_TRADING_ENVIRONMENT |
37+
38+
本地现有修复允许 Alpaca live/demo,并将模拟账户路由到 paper API。本轮没有更改账户环境或密钥,也没有把 paper 账户切为 live。
39+
40+
线上 `ai.quantdinger.com` 在 08:20 UTC 仍返回这一错误,因此不能把本机 backend 的新版本视为线上已更新。账户快照由 backend 处理,实际交易由 trading-worker 处理,两者都需要确认实际加载的代码版本。仅重启原容器不会换镜像。
41+
42+
这次诊断确证了环境校验拒绝的原因;不代表已验证密钥在 Alpaca 上有效,也不代表完整持仓/挂单读取已经通过。当前账户快照仍有通用加密货币适配回退路径,环境修复部署后还应专门验证 Alpaca 股票持仓及挂单映射。
43+
44+
## 仍需部署
45+
46+
后端搜索修复在本地源码中,运行中的 Docker 服务与线上服务没有在本轮被重建。更新后端镜像后才能使服务端去重生效。Alpaca 需对齐线上 backend 与 trading-worker 的修复版本,然后重新读取账户快照。本轮没有改动交易 worker 状态。
47+
48+
测试日志与真实调用结果位于工作区 `outputs/plugin-fixes-20260911/`。
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"""Execute search SQL against duplicate catalog rows before pagination."""
2+
from contextlib import contextmanager
3+
import sqlite3
4+
5+
import pytest
6+
7+
from app.data import market_symbols_seed as symbols
8+
9+
10+
@pytest.fixture
11+
def catalog(monkeypatch):
12+
db = sqlite3.connect(':memory:')
13+
db.row_factory = lambda cursor, row: dict(zip([x[0] for x in cursor.description], row))
14+
db.executescript('''
15+
CREATE TABLE qd_market_symbols (
16+
id INTEGER, market TEXT, symbol TEXT, name TEXT,
17+
sort_order INTEGER, is_active INTEGER, is_hot INTEGER
18+
);
19+
CREATE TABLE qd_market_symbol_aliases (
20+
market TEXT, symbol TEXT, alias TEXT, is_active INTEGER
21+
);
22+
INSERT INTO qd_market_symbols VALUES
23+
(1, 'USStock', 'NVDA', 'NVIDIA', 100, 1, 0),
24+
(2, 'USStock', 'nvda', 'Duplicate NVIDIA', 90, 1, 1),
25+
(3, 'USStock', 'NVDL', 'NVIDIA ETF', 80, 1, 1),
26+
(4, 'USStock', 'NVDX', 'NVIDIA inactive', 200, 0, 1),
27+
(5, 'HKStock', 'NVDA', 'Different market', 100, 1, 1);
28+
INSERT INTO qd_market_symbol_aliases VALUES
29+
('USStock', 'NVDA', 'GPU', 1),
30+
('USStock', 'NVDL', 'GPU', 1);
31+
''')
32+
33+
@contextmanager
34+
def connect():
35+
yield db
36+
37+
monkeypatch.setattr(symbols, '_get_db_connection', connect)
38+
yield db
39+
db.close()
40+
41+
42+
def test_exact_match_collapses_case_variants_and_preserves_market(catalog):
43+
assert symbols.search_symbols('USStock', 'nvda') == [
44+
{'market': 'USStock', 'symbol': 'NVDA', 'name': 'NVIDIA'}
45+
]
46+
assert symbols.search_symbols('HKStock', 'NVDA')[0]['name'] == 'Different market'
47+
48+
49+
@pytest.mark.parametrize('keyword', ['NVD', 'NVIDIA', 'GPU'])
50+
def test_limit_applies_after_deduplication(catalog, keyword):
51+
rows = symbols.search_symbols('USStock', keyword, limit=2)
52+
assert [x['symbol'] for x in rows] == ['NVDA', 'NVDL']
53+
54+
55+
def test_hot_symbols_and_missing_alias_table_still_deduplicate(catalog):
56+
assert [x['symbol'] for x in symbols.get_hot_symbols('USStock', limit=2)] == ['NVDA', 'NVDL']
57+
catalog.execute('DROP TABLE qd_market_symbol_aliases')
58+
assert [x['symbol'] for x in symbols.search_symbols('USStock', 'NVD', limit=2)] == ['NVDA', 'NVDL']

‎mcp_server/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ The MCP server is a thin, tenant-scoped wrapper over `/api/agent/v1`. It exposes
55
## Install and run
66

77
```bash
8-
pip install "quantdinger-mcp==0.5.0"
8+
pip install "quantdinger-mcp==0.6.0"
99
export QUANTDINGER_BASE_URL=http://localhost:8888
1010
export QUANTDINGER_AGENT_TOKEN=qd_agent_xxx
1111
quantdinger-mcp

‎mcp_server/RELEASE.md‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
# Release quantdinger-mcp to PyPI
22

3-
## This release: 0.5.0
3+
## This release: 0.6.0
4+
5+
Gateway HTTP errors, application errors, validation failures, and confirmation denials now return MCP `isError=true` while preserving redacted structured details. All 58 tools declare read-only, destructive, idempotent, and open-world hints. Write hints remain conservatively non-idempotent so clients cannot infer that retries against older gateways are safe. Confirmation and server-side permission checks remain required.
6+
7+
## Previous release: 0.5.0
48

59
This release expands MCP to complete research, strategy authoring, backtest, broker observation, notification, and safety-gated trading workflows. It adds atomic idempotency, distributed quotas, job cancellation, notional caps, and an emergency stop.
610

@@ -29,10 +33,10 @@ py -3.13 -m build
2933
# 5. Upload (you run this -- needs your PyPI token)
3034
$env:TWINE_USERNAME = "__token__"
3135
$env:TWINE_PASSWORD = "pypi-Ag..." # your API token
32-
py -3.13 -m twine upload dist/quantdinger_mcp-0.5.0*
36+
py -3.13 -m twine upload dist/quantdinger_mcp-0.6.0*
3337
3438
# 6. Verify
35-
pip install --upgrade "quantdinger-mcp==0.5.0"
39+
pip install --upgrade "quantdinger-mcp==0.6.0"
3640
quantdinger-mcp
3741
```
3842

@@ -44,11 +48,11 @@ pip install -e ".[dev]"
4448
python -m pytest tests/ -q
4549
rm -rf dist build src/*.egg-info
4650
python -m build
47-
TWINE_USERNAME=__token__ TWINE_PASSWORD=pypi-... python -m twine upload dist/quantdinger_mcp-0.5.0*
51+
TWINE_USERNAME=__token__ TWINE_PASSWORD=pypi-... python -m twine upload dist/quantdinger_mcp-0.6.0*
4852
```
4953

5054
## Notes
5155

52-
- Upload **only** the `0.5.0` files from `dist/` -- do not upload older versions again.
56+
- Upload **only** the `0.6.0` files from `dist/` -- do not upload older versions again.
5357
- PyPI token: Account settings -> API tokens -> scope `quantdinger-mcp` or entire account.
5458
- After publish: restart Cursor MCP or `pip install --upgrade quantdinger-mcp`.

‎mcp_server/pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "quantdinger-mcp"
7-
version = "0.5.0"
7+
version = "0.6.0"
88
description = "Full-surface Model Context Protocol server for QuantDinger research, strategy authoring, backtests, observations, notifications, and safety-gated trading."
99
readme = "README.md"
1010
requires-python = ">=3.10"
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
"""QuantDinger MCP server package."""
2-
__version__ = "0.5.0"
2+
__version__ = "0.6.0"

0 commit comments

Comments
 (0)