feat(selection): 集成 B1 FastDTW 图形评分

This commit is contained in:
yuxuanhui
2026-08-31 16:14:16 +08:00
parent 86762c0d9a
commit 6ce291e242
51 changed files with 2917 additions and 41 deletions
@@ -7,7 +7,10 @@ import {
} from "@testing-library/react"
import { beforeEach, describe, expect, it, vi } from "vitest"
import type { SelectionResults } from "../api/selection.types"
import type {
SelectionPatternScore,
SelectionResults,
} from "../api/selection.types"
import { SelectionResultsPage } from "./selection-results-page"
@@ -26,7 +29,13 @@ vi.mock("@/features/selection/api/selection.query", () => ({
vi.mock("@tanstack/react-router", () => ({
useNavigate: () => routerNavigate,
useSearch: () => ({ page: 1, pageSize: 5, search: "", category: "all" }),
useSearch: () => ({
page: 1,
pageSize: 5,
search: "",
category: "all",
sort: "code",
}),
}))
const selectedResult: SelectionResults = {
@@ -50,6 +59,24 @@ const selectedResult: SelectionResults = {
{
close: 10.5,
name: "平安银行",
score: {
breakdown: {
kdj_state: 83,
price_shape: 90.1,
trend_structure: 71.2,
volume_pattern: 88,
},
case: {
breakout_date: "2025-05-12",
id: "case_001",
name: "华纳药厂",
},
reason: null,
status: "matched",
threshold: 60,
value: 86.4,
version: "zhixing_b1_pattern_fastdtw_v1",
},
signals: [
{
category: "zhixing_b1_original_b1",
@@ -117,6 +144,9 @@ describe("SelectionResultsPage", () => {
expect(
within(table).getByRole("columnheader", { name: "J 值" }),
).toBeInTheDocument()
expect(
within(table).getByRole("columnheader", { name: "图形评分" }),
).toBeInTheDocument()
expect(
within(table).queryByRole("columnheader", { name: "关键详情" }),
).not.toBeInTheDocument()
@@ -125,15 +155,75 @@ describe("SelectionResultsPage", () => {
expect(within(table).getByText("12")).toBeInTheDocument()
expect(within(table).getByText("13")).toBeInTheDocument()
expect(within(table).getByText("平安银行")).toBeInTheDocument()
expect(within(table).getByText("86.40 分")).toBeInTheDocument()
expect(within(table).getByText("华纳药厂")).toBeInTheDocument()
const detailPanel = screen.getByRole("complementary", {
name: "当前股票详情",
})
expect(within(detailPanel).getByText("关键指标")).toBeInTheDocument()
expect(within(detailPanel).getByText("图形相似度评分")).toBeInTheDocument()
expect(
within(detailPanel).getByText("最佳案例:华纳药厂"),
).toBeInTheDocument()
expect(within(detailPanel).getByText("价格形态")).toBeInTheDocument()
expect(within(detailPanel).getAllByText("J 值")).toHaveLength(2)
expect(within(detailPanel).getByText("RSI")).toBeInTheDocument()
})
it.each<{
expected: string
score: SelectionPatternScore | null
}>([
{
expected: "本次运行未执行图形评分。",
score: null,
},
{
expected: "未匹配到 60.00 分以上案例。",
score: {
breakdown: null,
case: null,
reason: "未匹配到评分阈值以上案例",
status: "below_threshold",
threshold: 60,
value: null,
version: "zhixing_b1_pattern_fastdtw_v1",
},
},
{
expected: "评分暂不可用",
score: {
breakdown: null,
case: null,
reason: "FastDTW unavailable",
status: "failed",
threshold: null,
value: null,
version: null,
},
},
])(
"keeps selection signals visible when score state changes",
({ expected, score }) => {
useSelectionResults.mockReturnValue({
data: {
...selectedResult,
stocks: [{ ...selectedResult.stocks[0], score }],
},
isError: false,
isPending: false,
})
render(<SelectionResultsPage />)
expect(screen.getAllByText(expected).length).toBeGreaterThan(0)
const table = screen.getByRole("table")
expect(within(table).getByText("原始 B1")).toBeInTheDocument()
expect(within(table).getByText("12")).toBeInTheDocument()
},
)
it("opens execution status details in a drawer and restores trigger focus", async () => {
useSelectionResults.mockReturnValue({
data: {
@@ -328,6 +418,42 @@ describe("SelectionResultsPage", () => {
})
})
it("requests database-backed score sorting and resets the page", async () => {
render(<SelectionResultsPage />)
const sortTrigger = screen.getByRole("combobox", {
name: "排序命中股票",
})
fireEvent.click(sortTrigger)
const option = await screen.findByRole("option", { name: "评分从高到低" })
fireEvent.pointerDown(option, { pointerType: "mouse" })
fireEvent.click(option)
const lastCall = routerNavigate.mock.calls.at(-1)
const searchUpdate = lastCall?.[0].search as (previous: {
category: string
page: number
pageSize: number
search: string
sort: string
}) => Record<string, unknown>
expect(
searchUpdate({
category: "all",
page: 3,
pageSize: 5,
search: "",
sort: "code",
}),
).toEqual({
category: "all",
page: 1,
pageSize: 5,
search: "",
sort: "score_desc",
})
})
it("filters signals, updates the detail panel, and expands mobile details", () => {
const extraStock = {
...selectedResult.stocks[0],
@@ -536,6 +662,7 @@ describe("SelectionResultsPage", () => {
expect(useSelectionRun).toHaveBeenCalledWith("run-1", {
page: 1,
pageSize: 5,
sort: "code",
})
})