From 177fca7ae8fb320f44673c04d90a6a514b2a2464 Mon Sep 17 00:00:00 2001 From: Marquis03 <1282317554@qq.com> Date: Thu, 20 Aug 2026 10:35:52 +0800 Subject: [PATCH 1/2] fix(stocksdk): normalize realtime change percentage --- backend/app/plugins/stocksdk/provider.py | 11 ++++++++++- backend/tests/test_stocksdk_provider.py | 8 +++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/backend/app/plugins/stocksdk/provider.py b/backend/app/plugins/stocksdk/provider.py index c3481a4..97edeb0 100644 --- a/backend/app/plugins/stocksdk/provider.py +++ b/backend/app/plugins/stocksdk/provider.py @@ -203,7 +203,16 @@ class StockSDKProvider: except bridge.StockSDKBridgeError as e: logger.warning("stock-sdk realtime 拉取失败: %s", e) return [] - return result.get("rows") or [] + rows = result.get("rows") or [] + normalized: list[dict] = [] + for row in rows: + item = dict(row) + # stock-sdk 的 changePercent 是百分数值(-1.15 = -1.15%); + # provider 入口契约统一使用小数制(-0.0115 = -1.15%)。 + if item.get("change_pct") is not None: + item["change_pct"] = float(item["change_pct"]) / 100 + normalized.append(item) + return normalized # ---- instruments (标的维表) ---- def get_instruments(self, asset_type: str = "stock") -> list[dict]: diff --git a/backend/tests/test_stocksdk_provider.py b/backend/tests/test_stocksdk_provider.py index d6ddc57..b6a2b4d 100644 --- a/backend/tests/test_stocksdk_provider.py +++ b/backend/tests/test_stocksdk_provider.py @@ -80,12 +80,14 @@ def test_get_minute_datetime_is_beijing_wall_clock(monkeypatch): assert df["symbol"][0] == "600519.SH" -def test_get_realtime_passthrough(monkeypatch): +def test_get_realtime_normalizes_change_pct_to_decimal(monkeypatch): rows = [{"symbol": "600519.SH", "name": "贵州茅台", "last_price": 1200.0, - "prev_close": 1194.0, "open": 1186.0, "high": 1203.0, "low": 1180.0, "volume": 16325}] + "prev_close": 1194.0, "open": 1186.0, "high": 1203.0, "low": 1180.0, + "volume": 16325, "change_pct": -1.15}] _patch_run_job(monkeypatch, {"realtime": {"ok": True, "op": "realtime", "rows": rows}}) out = StockSDKProvider().get_realtime() - assert out == rows + assert abs(out[0]["change_pct"] - (-0.0115)) < 1e-12 + assert rows[0]["change_pct"] == -1.15 required = {"symbol", "last_price", "prev_close", "open", "high", "low", "volume"} assert required <= set(out[0].keys()) From 250595c925eeae13298b9933e340ba602a0d8ac4 Mon Sep 17 00:00:00 2001 From: Marquis03 <1282317554@qq.com> Date: Thu, 20 Aug 2026 10:48:54 +0800 Subject: [PATCH 2/2] fix(stocksdk): normalize realtime amount and timestamp --- backend/app/plugins/stocksdk/bridge.mjs | 1 + backend/app/plugins/stocksdk/provider.py | 3 +++ backend/tests/test_stocksdk_provider.py | 34 +++++++++++++++++++++--- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/backend/app/plugins/stocksdk/bridge.mjs b/backend/app/plugins/stocksdk/bridge.mjs index 586199f..84d1f22 100644 --- a/backend/app/plugins/stocksdk/bridge.mjs +++ b/backend/app/plugins/stocksdk/bridge.mjs @@ -213,6 +213,7 @@ async function opRealtime(sdk, job) { volume: q.volume, amount: q.amount, change_pct: q.changePercent, + timestamp: q.timestamp, }) } return rows diff --git a/backend/app/plugins/stocksdk/provider.py b/backend/app/plugins/stocksdk/provider.py index 97edeb0..c42d51c 100644 --- a/backend/app/plugins/stocksdk/provider.py +++ b/backend/app/plugins/stocksdk/provider.py @@ -211,6 +211,9 @@ class StockSDKProvider: # provider 入口契约统一使用小数制(-0.0115 = -1.15%)。 if item.get("change_pct") is not None: item["change_pct"] = float(item["change_pct"]) / 100 + # stock-sdk 全量实时行情的 amount 单位为万元;内部日K统一使用元。 + if item.get("amount") is not None: + item["amount"] = float(item["amount"]) * 10_000 normalized.append(item) return normalized diff --git a/backend/tests/test_stocksdk_provider.py b/backend/tests/test_stocksdk_provider.py index b6a2b4d..39e98c2 100644 --- a/backend/tests/test_stocksdk_provider.py +++ b/backend/tests/test_stocksdk_provider.py @@ -80,14 +80,18 @@ def test_get_minute_datetime_is_beijing_wall_clock(monkeypatch): assert df["symbol"][0] == "600519.SH" -def test_get_realtime_normalizes_change_pct_to_decimal(monkeypatch): +def test_get_realtime_normalizes_units_without_mutating_bridge_rows(monkeypatch): rows = [{"symbol": "600519.SH", "name": "贵州茅台", "last_price": 1200.0, "prev_close": 1194.0, "open": 1186.0, "high": 1203.0, "low": 1180.0, - "volume": 16325, "change_pct": -1.15}] + "volume": 16325, "amount": 159095, "change_pct": -1.15, + "timestamp": 1787193740000}] _patch_run_job(monkeypatch, {"realtime": {"ok": True, "op": "realtime", "rows": rows}}) out = StockSDKProvider().get_realtime() assert abs(out[0]["change_pct"] - (-0.0115)) < 1e-12 + assert out[0]["amount"] == 1_590_950_000 + assert out[0]["timestamp"] == 1787193740000 assert rows[0]["change_pct"] == -1.15 + assert rows[0]["amount"] == 159095 required = {"symbol", "last_price", "prev_close", "open", "high", "low", "volume"} assert required <= set(out[0].keys()) @@ -143,7 +147,7 @@ def test_bridge_uses_utf8_error_tolerant_subprocess(monkeypatch): assert kwargs["errors"] == "replace" -def test_bridge_mjs_resolves_local_stock_sdk_on_windows_path(tmp_path): +def test_bridge_mjs_resolves_local_sdk_and_maps_realtime_timestamp(tmp_path): if shutil.which("node") is None: raise AssertionError("node is required for stock-sdk bridge path regression test") @@ -157,7 +161,18 @@ def test_bridge_mjs_resolves_local_stock_sdk_on_windows_path(tmp_path): encoding="utf-8", ) (pkg_dir / "index.js").write_text( - "export class StockSDK { static version = 'fake-local' }\n", + """export class StockSDK { + static version = 'fake-local' + constructor() { + this.batch = { cn: async () => [{ + code: '600519', marketId: '1', name: '贵州茅台', price: 1200, + prevClose: 1194, open: 1186, high: 1203, low: 1180, + volume: 16325, amount: 159095, changePercent: 0.5, + timestamp: 1787193740000 + }] } + } +} +""", encoding="utf-8", ) @@ -173,6 +188,17 @@ def test_bridge_mjs_resolves_local_stock_sdk_on_windows_path(tmp_path): assert proc.returncode == 0 result = json.loads(proc.stdout) assert result == {"ok": True, "op": "ping", "version": "fake-local"} + realtime_proc = subprocess.run( + ["node", str(bridge_path)], + input=json.dumps({"op": "realtime"}), + capture_output=True, + text=True, + encoding="utf-8", + timeout=20, + ) + assert realtime_proc.returncode == 0 + row = json.loads(realtime_proc.stdout)["rows"][0] + assert row["timestamp"] == 1787193740000 def test_plugin_discovered_in_loader():