feat(selection): 集成 B1 FastDTW 图形评分
This commit is contained in:
@@ -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",
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user