diff --git a/backend/app/services/quote_service.py b/backend/app/services/quote_service.py index 688f153..0e80c87 100644 --- a/backend/app/services/quote_service.py +++ b/backend/app/services/quote_service.py @@ -818,7 +818,12 @@ class QuoteService: ] if c in df.columns] if not keep or "symbol" not in keep: return pl.DataFrame() - return df.select(keep) + out = df.select(keep) + # 实时 API 的 turnover_rate 入口契约为小数制(0.05 = 5%). + # enriched 内部统一存百分数值(5 = 5%), 后续页面/筛选直接展示和比较。 + if "turnover_rate" in out.columns: + out = out.with_columns((pl.col("turnover_rate").cast(pl.Float64, strict=False) * 100).alias("turnover_rate")) + return out @staticmethod def _build_index_quotes(records: list[dict]) -> pl.DataFrame: diff --git a/backend/app/strategy/builtin/high_turnover_surge.py b/backend/app/strategy/builtin/high_turnover_surge.py index 807de60..710d396 100644 --- a/backend/app/strategy/builtin/high_turnover_surge.py +++ b/backend/app/strategy/builtin/high_turnover_surge.py @@ -30,7 +30,7 @@ ALERTS = [] def filter(df: pl.DataFrame, params: dict) -> pl.Expr: - min_to = params.get("min_turnover", 5.0) / 100.0 + min_to = params.get("min_turnover", 5.0) min_chg = params.get("min_change", 3.0) / 100.0 expr = pl.col("symbol").is_not_null() | pl.col("symbol").is_null() if params.get("use_turnover_filter", True): diff --git a/backend/tests/test_high_turnover_strategy.py b/backend/tests/test_high_turnover_strategy.py new file mode 100644 index 0000000..c330cfb --- /dev/null +++ b/backend/tests/test_high_turnover_strategy.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +import polars as pl + +from app.strategy.builtin import high_turnover_surge + + +def test_high_turnover_surge_uses_percent_value_turnover_rate(): + df = pl.DataFrame({ + "symbol": ["low", "hit"], + "turnover_rate": [4.9, 5.1], + "change_pct": [0.04, 0.04], + }) + + expr = high_turnover_surge.filter(df, {"min_turnover": 5.0, "min_change": 3.0}) + out = df.filter(expr) + + assert out["symbol"].to_list() == ["hit"] diff --git a/backend/tests/test_realtime_turnover_rate.py b/backend/tests/test_realtime_turnover_rate.py new file mode 100644 index 0000000..c4ad75a --- /dev/null +++ b/backend/tests/test_realtime_turnover_rate.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +import polars as pl +import pytest + +from app.indicators import pipeline +from app.services.quote_service import QuoteService + + +def _today_rows(turnover_rate: float | None = None) -> pl.DataFrame: + row = { + "symbol": "600000.SH", + "open": 10.0, + "high": 10.0, + "low": 10.0, + "close": 10.0, + "raw_close": 10.0, + "raw_high": 10.0, + "volume": 8000.0, + } + if turnover_rate is not None: + row["turnover_rate"] = turnover_rate + return pl.DataFrame([row]) + + +def _instruments() -> pl.DataFrame: + return pl.DataFrame({ + "symbol": ["600000.SH"], + "name": ["Test"], + "float_shares": [100_000_000.0], + }) + + +def test_quote_extra_normalizes_realtime_turnover_fraction_to_percent_value(): + out = QuoteService._build_quote_extra([{"symbol": "600000.SH", "turnover_rate": 0.008}]) + + assert out["turnover_rate"][0] == pytest.approx(0.8) + + +def test_realtime_turnover_rate_uses_api_value_directly_after_entry_normalization(): + out = pipeline._compute_limit_signals_today(_today_rows(0.8), _instruments()) + + assert out["turnover_rate"][0] == pytest.approx(0.8) + + +def test_realtime_turnover_rate_falls_back_to_float_shares_when_missing(): + out = pipeline._compute_limit_signals_today(_today_rows(), _instruments()) + + assert out["turnover_rate"][0] == pytest.approx(0.8) diff --git a/docs/custom-data-source.md b/docs/custom-data-source.md index 1d9692a..d91a52a 100644 --- a/docs/custom-data-source.md +++ b/docs/custom-data-source.md @@ -253,7 +253,7 @@ cp docs/examples/custom-data-source/mock_source.yaml data/data_sources/mock_sour change_pct = 涨跌幅 (小数, 0.0366 = 3.66%) change_amount = 涨跌额 amplitude = 振幅 - turnover_rate = 换手率 + turnover_rate = 换手率 (小数, 0.05 = 5%; 若上游返回 5 表示 5%, 配置 transforms: turnover_rate: "value / 100") 分钟K (minute): symbol = 股票代码 diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index be56704..8141be7 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -415,8 +415,7 @@ function StockList({ title, rows, mode, onStockClick }: { ) : mode === 'active' ? ( <> - {/* overview 的 turnover_rate 为小数制, 需 ×100 转百分数显示 */} -
{fmtPrice(r.turnover_rate != null ? r.turnover_rate * 100 : null, 1)}%
+
{fmtPrice(r.turnover_rate, 1)}%
{fmtStockPct(r.change_pct)}
) : ( diff --git a/frontend/src/pages/settings/DataSourceEditor.tsx b/frontend/src/pages/settings/DataSourceEditor.tsx index 474b17a..3c24d0e 100644 --- a/frontend/src/pages/settings/DataSourceEditor.tsx +++ b/frontend/src/pages/settings/DataSourceEditor.tsx @@ -45,7 +45,7 @@ const FIELD_LABELS: Record = { change_pct: '涨跌幅 (小数 0.0366=3.66%)', change_amount: '涨跌额', amplitude: '振幅 (小数)', - turnover_rate: '换手率 (小数)', + turnover_rate: '换手率 (小数 0.05=5%)', timestamp: '时间戳', session: '交易时段', }