fix(realtime): unify turnover rate units (#93)

This commit is contained in:
wshy
2026-07-09 19:14:50 +08:00
committed by GitHub
parent 6bb6eadb12
commit 6076074ac0
7 changed files with 77 additions and 6 deletions
+6 -1
View File
@@ -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:
@@ -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):
@@ -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"]
@@ -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)
+1 -1
View File
@@ -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 = 股票代码
+1 -2
View File
@@ -415,8 +415,7 @@ function StockList({ title, rows, mode, onStockClick }: {
</>
) : mode === 'active' ? (
<>
{/* overview 的 turnover_rate 为小数制, 需 ×100 转百分数显示 */}
<div className="font-mono text-[11px] text-accent">{fmtPrice(r.turnover_rate != null ? r.turnover_rate * 100 : null, 1)}%</div>
<div className="font-mono text-[11px] text-accent">{fmtPrice(r.turnover_rate, 1)}%</div>
<div className={`font-mono text-[9px] ${pctClass(r.change_pct)}`}>{fmtStockPct(r.change_pct)}</div>
</>
) : (
@@ -45,7 +45,7 @@ const FIELD_LABELS: Record<string, string> = {
change_pct: '涨跌幅 (小数 0.0366=3.66%)',
change_amount: '涨跌额',
amplitude: '振幅 (小数)',
turnover_rate: '换手率 (小数)',
turnover_rate: '换手率 (小数 0.05=5%)',
timestamp: '时间戳',
session: '交易时段',
}