mirror of
https://github.com/shy3130/tick-stock-panel.git
synced 2026-11-02 10:26:13 +00:00
fix(dragon-tiger): thscode 为 null 的退市/B股标的归一化, 修复龙虎榜崩溃
用户反馈: 展开复盘页龙虎榜 (2026-10-29 榜单 72 条) 前端崩溃。 根因: fuyao 对退市整理期股/B股返回 thscode=null (ticker 有值) — 今天 首次上榜的萃华退(002731)/宁通信B(200468)/动力B股(900920) 3 条 null thscode 被后端原样透传, 违反前端 DragonTigerStockItem.thscode: string 契约 (下游按 string 使用, 收到 null 即崩)。 - dragon_tiger 服务新增归一化 (新拉取 + 历史缓存读回共用): thscode 缺失时由 ticker 推导市场后缀 (6xx/900→SH, 0xx/3xx/200→SZ, 4xx/8xx→BJ), 两者都不可用则丢弃并告警; 游资席位嵌套 rows 同样处理 - 前端: 类型放宽为 string | null (如实反映数据源现实), 榜单卡片统一走 _dtCode/_dtLabel 兜底 (旧缓存/异常数据源也不再崩), 无代码行不触发 onOpenStock - 实测真实数据: 72 条归一后 0 条 null; 三条退市/B股 → 002731.SZ / 200468.SZ / 900920.SH; 席位嵌套 rows 同样干净 验证: 后端新增 6 条归一化用例 (ticker 推导/不可识别丢弃/嵌套 rows/ 缓存读回/端到端); 前端新增龙虎榜卡片回归 2 条 (展开 72 条不崩 + 点击 null 行以 ticker 兜底回调); 后端全量 3546 passed; 前端 49 文件 348 用例 + build 通过; ruff 零新增。 Signed-off-by: shy3130 <shy3130@qq.com>
This commit is contained in:
@@ -88,20 +88,109 @@ def _boards_of(provider, board_type: str, iso: str | None) -> dict:
|
||||
"trade_date": data.get("trade_date"),
|
||||
"stock_count": data.get("stock_count"),
|
||||
"count": data.get("count"),
|
||||
"stock_items": data.get("stock_items") or [],
|
||||
"hot_money_items": data.get("hot_money_items") or [],
|
||||
"stock_items": normalize_stock_items(data.get("stock_items") or []),
|
||||
"hot_money_items": normalize_hot_money_items(data.get("hot_money_items") or []),
|
||||
}
|
||||
|
||||
|
||||
_TICKER_RE = re.compile(r"^(\d{6})(?:\.[A-Za-z]{2})?$")
|
||||
|
||||
|
||||
def derive_thscode(ticker: object) -> str | None:
|
||||
"""由 ticker 推导带市场后缀的 thscode (如 002731 → 002731.SZ)。
|
||||
|
||||
fuyao 对退市整理期股 / B 股返回的 thscode 可能为 null (ticker 有值),
|
||||
不能把 null 透传给前端 (各消费方按 string 契约使用)。规则:
|
||||
6xxxxx / 900xxx (沪B) → .SH
|
||||
0xxxxx / 3xxxxx / 200xxx(深B) → .SZ
|
||||
4xxxxx / 8xxxxx (北交所) → .BJ
|
||||
"""
|
||||
if not isinstance(ticker, str):
|
||||
return None
|
||||
raw = ticker.strip()
|
||||
m = _TICKER_RE.match(raw)
|
||||
if not m:
|
||||
return None
|
||||
if "." in raw:
|
||||
return raw
|
||||
code = m.group(1)
|
||||
if code.startswith("900") or code[0] == "6":
|
||||
return f"{code}.SH"
|
||||
if code.startswith("200") or code[0] in ("0", "3"):
|
||||
return f"{code}.SZ"
|
||||
if code[0] in ("4", "8"):
|
||||
return f"{code}.BJ"
|
||||
return None
|
||||
|
||||
|
||||
def normalize_stock_items(items: object) -> list[dict]:
|
||||
"""榜单个股行规范化: thscode 缺失时由 ticker 推导, 两者都不可用则丢弃并告警。"""
|
||||
if not isinstance(items, list):
|
||||
return []
|
||||
out: list[dict] = []
|
||||
dropped: list[str] = []
|
||||
for item in items:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
code = item.get("thscode")
|
||||
if not isinstance(code, str) or not code.strip():
|
||||
derived = derive_thscode(item.get("ticker"))
|
||||
if derived is None:
|
||||
dropped.append(str(item.get("name") or item.get("ticker") or "?"))
|
||||
continue
|
||||
item = {**item, "thscode": derived}
|
||||
out.append(item)
|
||||
if dropped:
|
||||
logger.warning(
|
||||
"龙虎榜 %d 条标的缺少可识别代码 (thscode/ticker 均不可用), 已跳过: %s",
|
||||
len(dropped), dropped[:5])
|
||||
return out
|
||||
|
||||
|
||||
def normalize_hot_money_items(items: object) -> list[dict]:
|
||||
"""游资席位行: 席位本身无代码, 但关联股票 rows 必须带可用 thscode。"""
|
||||
if not isinstance(items, list):
|
||||
return []
|
||||
out: list[dict] = []
|
||||
for seat in items:
|
||||
if not isinstance(seat, dict):
|
||||
continue
|
||||
rows = seat.get("rows")
|
||||
if isinstance(rows, list):
|
||||
seat = {**seat, "rows": normalize_stock_items(rows)}
|
||||
out.append(seat)
|
||||
return out
|
||||
|
||||
|
||||
def normalize_payload(payload: dict) -> dict:
|
||||
"""规范化整个龙虎榜容器 (新拉取与历史缓存读回共用)。"""
|
||||
for board in ("all", "org"):
|
||||
b = payload.get(board)
|
||||
if isinstance(b, dict):
|
||||
payload[board] = {
|
||||
**b,
|
||||
"stock_items": normalize_stock_items(b.get("stock_items") or []),
|
||||
}
|
||||
hot = payload.get("hot_money")
|
||||
if isinstance(hot, dict):
|
||||
payload["hot_money"] = {
|
||||
**hot,
|
||||
"hot_money_items": normalize_hot_money_items(hot.get("hot_money_items") or []),
|
||||
}
|
||||
return payload
|
||||
|
||||
|
||||
def _cache_path(data_dir: Path, d: date_cls) -> Path:
|
||||
return data_dir / "dragon_tiger" / f"date={d.isoformat()}.json"
|
||||
|
||||
|
||||
def _load_cache(path: Path) -> dict | None:
|
||||
try:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError):
|
||||
return None
|
||||
# 旧版本写入的缓存可能已含 thscode=null (fuyao 退市/B股数据), 读回时同样规范化
|
||||
return normalize_payload(data) if isinstance(data, dict) else None
|
||||
|
||||
|
||||
def _store_cache(path: Path, payload: dict) -> None:
|
||||
|
||||
@@ -155,3 +155,91 @@ def test_build_recap_context_contains_summary(data_dir, monkeypatch):
|
||||
def test_build_recap_context_empty_without_source(data_dir, monkeypatch):
|
||||
monkeypatch.setattr(dt, "_provider", lambda: None)
|
||||
assert dt.build_recap_context(data_dir) == ""
|
||||
|
||||
|
||||
# ---- thscode 归一化 (2026-10-29 用户反馈: 退市整理期股/B股 thscode=null 进前端) ----
|
||||
|
||||
def test_derive_thscode_market_suffix():
|
||||
assert dt.derive_thscode("002731") == "002731.SZ"
|
||||
assert dt.derive_thscode("300750") == "300750.SZ"
|
||||
assert dt.derive_thscode("600519") == "600519.SH"
|
||||
assert dt.derive_thscode("688111") == "688111.SH"
|
||||
assert dt.derive_thscode("830799") == "830799.BJ"
|
||||
assert dt.derive_thscode("900920") == "900920.SH" # 沪B
|
||||
assert dt.derive_thscode("200468") == "200468.SZ" # 深B
|
||||
assert dt.derive_thscode("600519.SH") == "600519.SH" # 已带后缀原样返回
|
||||
assert dt.derive_thscode(None) is None
|
||||
assert dt.derive_thscode("ABCDEF") is None
|
||||
|
||||
|
||||
def test_normalize_stock_items_fills_null_thscode_from_ticker():
|
||||
items = [
|
||||
{"thscode": None, "ticker": "002731", "name": "萃华退", "net_value": -525558.3},
|
||||
{"thscode": None, "ticker": "200468", "name": "宁通信B"},
|
||||
{"thscode": "600519.SH", "ticker": "600519", "name": "贵州茅台"},
|
||||
]
|
||||
out = dt.normalize_stock_items(items)
|
||||
assert [i["thscode"] for i in out] == ["002731.SZ", "200468.SZ", "600519.SH"]
|
||||
# 其它字段原样保留
|
||||
assert out[0]["name"] == "萃华退" and out[0]["net_value"] == -525558.3
|
||||
|
||||
|
||||
def test_normalize_stock_items_drops_unidentifiable():
|
||||
out = dt.normalize_stock_items([
|
||||
{"thscode": None, "ticker": None, "name": "无代码"},
|
||||
{"thscode": " ", "ticker": "nope", "name": "坏ticker"},
|
||||
"junk",
|
||||
])
|
||||
assert out == []
|
||||
|
||||
|
||||
def test_boards_of_normalizes_nested_seat_rows():
|
||||
class _NullProvider:
|
||||
def dragon_tiger(self, board_type, date):
|
||||
return {
|
||||
"trade_date": date,
|
||||
"stock_items": [{"thscode": None, "ticker": "900920", "name": "动力B股"}],
|
||||
"hot_money_items": [{"name": "T王", "buying": 1.0,
|
||||
"rows": [{"thscode": None, "ticker": "002731", "name": "萃华退"}]}],
|
||||
}
|
||||
|
||||
got = dt._boards_of(_NullProvider(), "all", "2026-10-29")
|
||||
assert got["stock_items"][0]["thscode"] == "900920.SH"
|
||||
assert got["hot_money_items"][0]["rows"][0]["thscode"] == "002731.SZ"
|
||||
|
||||
|
||||
def test_load_cache_normalizes_legacy_null_thscode(tmp_path):
|
||||
"""旧版本写入的历史缓存含 null thscode → 读回即归一, 不把脏数据透给前端。"""
|
||||
path = tmp_path / "date=2026-10-29.json"
|
||||
path.write_text(json.dumps({
|
||||
"state": "ok",
|
||||
"trade_date": "2026-10-29",
|
||||
"all": {"stock_items": [{"thscode": None, "ticker": "002731", "name": "萃华退"}]},
|
||||
"org": {"stock_items": [{"thscode": "600519.SH"}]},
|
||||
"hot_money": {"hot_money_items": [
|
||||
{"name": "T王", "rows": [{"thscode": None, "ticker": "200468"}]},
|
||||
]},
|
||||
}, ensure_ascii=False), encoding="utf-8")
|
||||
|
||||
payload = dt._load_cache(path)
|
||||
assert payload["all"]["stock_items"][0]["thscode"] == "002731.SZ"
|
||||
assert payload["org"]["stock_items"][0]["thscode"] == "600519.SH"
|
||||
assert payload["hot_money"]["hot_money_items"][0]["rows"][0]["thscode"] == "200468.SZ"
|
||||
|
||||
|
||||
def test_get_dragon_tiger_normalizes_provider_null_thscode(data_dir, monkeypatch):
|
||||
class _NullProvider:
|
||||
def dragon_tiger(self, board_type, date):
|
||||
return {
|
||||
"trade_date": "2026-08-28",
|
||||
"count": 1,
|
||||
"stock_count": 1,
|
||||
"stock_items": [{"thscode": None, "ticker": "002731", "name": "萃华退",
|
||||
"net_value": -525558.3}],
|
||||
"hot_money_items": [],
|
||||
}
|
||||
|
||||
_use_provider(monkeypatch, _NullProvider())
|
||||
payload = dt.get_dragon_tiger(data_dir, date(2026, 8, 28))
|
||||
assert payload["state"] == "ok"
|
||||
assert payload["all"]["stock_items"][0]["thscode"] == "002731.SZ"
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
// @vitest-environment jsdom
|
||||
// 回归: fuyao 龙虎榜对退市整理期股/B股返回 thscode=null (ticker 有值),
|
||||
// 复盘页龙虎榜卡片渲染/点击该行不得崩溃 (用户反馈: 展开龙虎榜即崩)。
|
||||
// 真实数据样本 (2026-10-29 榜单): 萃华退 002731 / 宁通信B 200468 / 动力B股 900920。
|
||||
import { act } from 'react'
|
||||
import { createRoot, type Root } from 'react-dom/client'
|
||||
import { MemoryRouter } from 'react-router-dom'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
|
||||
import { DragonTigerCard } from './extension'
|
||||
import type { DragonTigerStockItem } from '@/lib/api'
|
||||
|
||||
const _items: DragonTigerStockItem[] = [
|
||||
{ thscode: null, ticker: '002731', name: '萃华退', change: -0.107143, net_value: -525558.3, net_rate: -0.0131, buy_value: 4489000, sell_value: 5014558.3, range_days: 1 },
|
||||
{ thscode: null, ticker: '200468', name: '宁通信B', change: -0.08011, net_value: -54180.43, net_rate: -0.0102, buy_value: 2617000, sell_value: 2671180.43, range_days: 1 },
|
||||
{ thscode: null, ticker: '900920', name: '动力B股', change: 0.099602, net_value: 447119.44, net_rate: 0.0521, buy_value: 12030000, sell_value: 11582880.56, range_days: 1 },
|
||||
{ thscode: '600519.SH', ticker: '600519', name: '贵州茅台', change: 0.012, net_value: 93000000, net_rate: 0.04, buy_value: 200000000, sell_value: 107000000, range_days: 1 },
|
||||
]
|
||||
|
||||
const _payload = {
|
||||
state: 'ok' as const,
|
||||
trade_date: '2026-10-29',
|
||||
all: { trade_date: '2026-10-29', stock_count: _items.length, count: _items.length, stock_items: _items },
|
||||
org: { trade_date: '2026-10-29', stock_count: 1, count: 1, stock_items: [_items[3]] },
|
||||
hot_money: { trade_date: '2026-10-29', count: 1, hot_money_items: [
|
||||
{ name: 'T王', buying: 5000000, rows: [_items[0], _items[1]] },
|
||||
] },
|
||||
}
|
||||
|
||||
vi.mock('@/lib/api', () => ({
|
||||
api: { dragonTiger: vi.fn(async () => _payload) },
|
||||
}))
|
||||
|
||||
let host: HTMLDivElement
|
||||
let root: Root
|
||||
let client: QueryClient
|
||||
|
||||
beforeEach(() => {
|
||||
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })
|
||||
host = document.createElement('div')
|
||||
document.body.append(host)
|
||||
root = createRoot(host)
|
||||
client = new QueryClient({ defaultOptions: { queries: { retry: false } } })
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await act(async () => root.unmount())
|
||||
client.clear()
|
||||
host.remove()
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
async function renderCard(onOpenStock = vi.fn()) {
|
||||
const errors: unknown[] = []
|
||||
const spy = vi.spyOn(console, 'error').mockImplementation((...args) => { errors.push(args) })
|
||||
await act(async () => root.render(
|
||||
<MemoryRouter>
|
||||
<QueryClientProvider client={client}>
|
||||
<DragonTigerCard date="2026-10-29" onOpenStock={onOpenStock} />
|
||||
</QueryClientProvider>
|
||||
</MemoryRouter>,
|
||||
))
|
||||
await act(async () => { await new Promise(r => setTimeout(r, 80)) })
|
||||
spy.mockRestore()
|
||||
return { errors, onOpenStock }
|
||||
}
|
||||
|
||||
async function expand() {
|
||||
const btn = [...host.querySelectorAll('button')].find(b => b.textContent?.includes('查看三榜明细'))
|
||||
expect(btn).toBeDefined()
|
||||
await act(async () => { btn!.click() })
|
||||
await act(async () => { await new Promise(r => setTimeout(r, 60)) })
|
||||
}
|
||||
|
||||
it('含 thscode=null 的标的: 展开三榜明细不崩溃, 以名称/ticker 兜底展示', async () => {
|
||||
const { errors } = await renderCard()
|
||||
expect(errors.some(e => String(e).includes('TypeError'))).toBe(false)
|
||||
|
||||
await expand()
|
||||
expect(errors.some(e => String(e).includes('TypeError'))).toBe(false)
|
||||
// 三条 null thscode 标的都以名称 + ticker 展示
|
||||
expect(host.textContent).toContain('萃华退')
|
||||
expect(host.textContent).toContain('宁通信B')
|
||||
expect(host.textContent).toContain('动力B股')
|
||||
expect(host.textContent).toContain('002731')
|
||||
})
|
||||
|
||||
it('点击 thscode=null 的行用 ticker 兜底回调, 不崩溃', async () => {
|
||||
const { errors, onOpenStock } = await renderCard()
|
||||
await expand()
|
||||
|
||||
const row = [...host.querySelectorAll('button')].find(b => b.textContent?.includes('萃华退'))
|
||||
expect(row).toBeDefined()
|
||||
await act(async () => { row!.click() })
|
||||
|
||||
expect(errors.some(e => String(e).includes('TypeError'))).toBe(false)
|
||||
expect(onOpenStock).toHaveBeenCalledWith('002731')
|
||||
})
|
||||
@@ -1072,6 +1072,18 @@ function _dtSorted(items: DragonTigerStockItem[], key: 'net_value' | 'org_net_va
|
||||
return asc ? s.reverse() : s
|
||||
}
|
||||
|
||||
/** 榜单标的代码: 后端已归一非空, 此处兜底旧缓存/异常数据源的 null thscode。 */
|
||||
function _dtCode(i: DragonTigerStockItem): string {
|
||||
const code = i.thscode
|
||||
if (typeof code === 'string' && code.trim()) return code
|
||||
return i.ticker ?? ''
|
||||
}
|
||||
|
||||
/** 榜单标的展示名: 名称优先, 无名称时退代码/ticker。 */
|
||||
function _dtLabel(i: DragonTigerStockItem): string {
|
||||
return i.name ?? (_dtCode(i) || '—')
|
||||
}
|
||||
|
||||
/** 排名序号色: 前三用暖色系奖牌感, 其余弱化 */
|
||||
function _rankCls(idx: number): string {
|
||||
if (idx === 0) return 'bg-amber-500/20 text-amber-400'
|
||||
@@ -1086,20 +1098,22 @@ function _DtPill({ item, idx, value, onOpenStock }: {
|
||||
value: number | null | undefined
|
||||
onOpenStock: (s: string) => void
|
||||
}) {
|
||||
const code = _dtCode(item)
|
||||
const label = _dtLabel(item)
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onOpenStock(item.thscode)}
|
||||
onClick={() => code && onOpenStock(code)}
|
||||
className="group inline-flex items-center gap-1.5 rounded-full border border-border/60 bg-elevated/40 py-0.5 pl-0.5 pr-2.5 transition-all hover:border-accent/40 hover:bg-elevated/70"
|
||||
title={`查看 ${item.name ?? item.thscode} · 净买 ${fmtVolume(value ?? null)}`}
|
||||
title={`查看 ${label} · 净买 ${fmtVolume(value ?? null)}`}
|
||||
>
|
||||
<span className={cn('grid h-[18px] w-[18px] min-w-[18px] place-items-center rounded-full font-mono text-[9px] font-bold leading-none', _rankCls(idx))}>
|
||||
{idx + 1}
|
||||
</span>
|
||||
<span className="text-[11px] text-foreground/90 transition-colors group-hover:text-accent">
|
||||
{item.name ?? item.thscode}
|
||||
{label}
|
||||
</span>
|
||||
{(() => { const b = boardTag(item.thscode); return b && (
|
||||
{(() => { const b = boardTag(code); return b && (
|
||||
<span className={`inline-flex items-center rounded border px-1 text-[8px] font-bold leading-tight ${b.color}`}>
|
||||
{b.label}
|
||||
</span>
|
||||
@@ -1129,7 +1143,7 @@ function _DtSummaryRow({ label, items, pick, onOpenStock }: {
|
||||
)}
|
||||
</span>
|
||||
{items.map((i, idx) => (
|
||||
<_DtPill key={`${i.thscode}-${idx}`} item={i} idx={idx} value={pick(i)} onOpenStock={onOpenStock} />
|
||||
<_DtPill key={`${_dtCode(i)}-${idx}`} item={i} idx={idx} value={pick(i)} onOpenStock={onOpenStock} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
@@ -1202,13 +1216,15 @@ function _DtStockTable({ items, tab, onOpenStock }: {
|
||||
{sorted.map((i, idx) => {
|
||||
const barPct = Math.max(2, Math.min(100, (Math.abs(i.net_value ?? 0) / maxAbs) * 100))
|
||||
const positive = (i.net_value ?? 0) >= 0
|
||||
const code = _dtCode(i)
|
||||
const label = _dtLabel(i)
|
||||
return (
|
||||
<button
|
||||
key={`${i.thscode}-${i.range_days ?? 1}`}
|
||||
key={`${code}-${i.range_days ?? 1}`}
|
||||
type="button"
|
||||
onClick={() => onOpenStock(i.thscode)}
|
||||
onClick={() => code && onOpenStock(code)}
|
||||
className="flex w-full items-center gap-2 border-t border-border/30 px-2.5 py-2 text-left text-[11px] transition-colors hover:bg-accent/[0.05]"
|
||||
title={`查看 ${i.name ?? i.thscode} 详情`}
|
||||
title={`查看 ${label} 详情`}
|
||||
>
|
||||
<span className={cn('w-5 shrink-0 text-center font-mono text-[9px] tabular-nums', idx < 3 ? 'text-amber-400/90' : 'text-muted/50')}>
|
||||
{idx + 1}
|
||||
@@ -1217,9 +1233,9 @@ function _DtStockTable({ items, tab, onOpenStock }: {
|
||||
{fmtPct(i.change ?? null, 1)}
|
||||
</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="text-foreground">{i.name ?? '—'}</span>
|
||||
<span className="ml-1.5 font-mono text-[9px] text-muted">{i.ticker ?? i.thscode}</span>
|
||||
{(() => { const b = boardTag(i.thscode); return b && (
|
||||
<span className="text-foreground">{label}</span>
|
||||
<span className="ml-1.5 font-mono text-[9px] text-muted">{i.ticker ?? code}</span>
|
||||
{(() => { const b = boardTag(code); return b && (
|
||||
<span className={`ml-1 inline-flex items-center rounded border px-1 text-[8px] font-bold leading-tight ${b.color}`}>
|
||||
{b.label}
|
||||
</span>
|
||||
@@ -1304,16 +1320,18 @@ function _DtSeatList({ seats, onOpenStock }: {
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1 flex min-w-0 flex-wrap items-center gap-1.5">
|
||||
{(s.rows ?? []).map(r => (
|
||||
{(s.rows ?? []).map((r, ri) => {
|
||||
const code = _dtCode(r)
|
||||
return (
|
||||
<button
|
||||
key={`${s.name}-${r.thscode}`}
|
||||
key={`${s.name}-${code}-${ri}`}
|
||||
type="button"
|
||||
onClick={() => onOpenStock(r.thscode)}
|
||||
onClick={() => code && onOpenStock(code)}
|
||||
className="inline-flex items-baseline gap-1.5 rounded-full border border-border/60 bg-surface/60 px-2 py-0.5 text-[10px] transition-all hover:border-accent/40 hover:text-accent"
|
||||
title={`查看 ${r.name ?? r.thscode} 详情`}
|
||||
title={`查看 ${_dtLabel(r)} 详情`}
|
||||
>
|
||||
<span>{r.name ?? r.thscode}</span>
|
||||
{(() => { const b = boardTag(r.thscode); return b && (
|
||||
<span>{_dtLabel(r)}</span>
|
||||
{(() => { const b = boardTag(code); return b && (
|
||||
<span className={`inline-flex items-center self-center rounded border px-1 text-[8px] font-bold leading-tight ${b.color}`}>
|
||||
{b.label}
|
||||
</span>
|
||||
@@ -1322,7 +1340,8 @@ function _DtSeatList({ seats, onOpenStock }: {
|
||||
{fmtVolume(r.hot_money_item_net_value ?? r.net_value ?? null)}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1333,7 +1352,7 @@ function _DtSeatList({ seats, onOpenStock }: {
|
||||
|
||||
/** 追高风险阈值: 60日回测高开≥5%子集当日开盘买 -1.97% (温和高开才是名单 alpha 来源) */
|
||||
|
||||
function DragonTigerCard({ date, onOpenStock }: {
|
||||
export function DragonTigerCard({ date, onOpenStock }: {
|
||||
date?: string
|
||||
onOpenStock: (symbol: string) => void
|
||||
}) {
|
||||
|
||||
@@ -749,7 +749,8 @@ export interface AiReviewReport {
|
||||
|
||||
// ===== 龙虎榜 (fuyao 专有, 复盘页) =====
|
||||
export interface DragonTigerStockItem {
|
||||
thscode: string
|
||||
/** 后端已归一 (缺失时由 ticker 推导); 旧缓存/异常数据源可能为 null, 消费方需兜底 */
|
||||
thscode: string | null
|
||||
ticker?: string | null
|
||||
name?: string | null
|
||||
change?: number | null // 当日涨跌幅 (小数制)
|
||||
|
||||
Reference in New Issue
Block a user