Develop #28
@@ -16,7 +16,7 @@ export function PatternCaseImage({ score }: PatternCaseImageProps) {
|
|||||||
patternCaseImageById[matchedCase.id as keyof typeof patternCaseImageById]
|
patternCaseImageById[matchedCase.id as keyof typeof patternCaseImageById]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section aria-labelledby="best-pattern-case" className="mt-4">
|
<section aria-labelledby="best-pattern-case" className="mt-2">
|
||||||
<div className="mb-2 flex items-baseline justify-between gap-3">
|
<div className="mb-2 flex items-baseline justify-between gap-3">
|
||||||
<h3 className="text-xs font-medium" id="best-pattern-case">
|
<h3 className="text-xs font-medium" id="best-pattern-case">
|
||||||
最接近案例:{matchedCase.name}
|
最接近案例:{matchedCase.name}
|
||||||
@@ -32,7 +32,7 @@ export function PatternCaseImage({ score }: PatternCaseImageProps) {
|
|||||||
) : (
|
) : (
|
||||||
<img
|
<img
|
||||||
alt={`${matchedCase.name} B1 参考案例图`}
|
alt={`${matchedCase.name} B1 参考案例图`}
|
||||||
className="aspect-2/1 w-full rounded-md border border-border/60 object-contain"
|
className="h-40 w-full rounded-md border border-border/60 object-contain"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
onError={() => setFailedSource(source)}
|
onError={() => setFailedSource(source)}
|
||||||
src={source}
|
src={source}
|
||||||
|
|||||||
@@ -6,6 +6,13 @@ interface PatternScoreProps {
|
|||||||
score: SelectionPatternScore | null
|
score: SelectionPatternScore | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const breakdownLabels = {
|
||||||
|
trend_structure: "趋势",
|
||||||
|
kdj_state: "KDJ",
|
||||||
|
volume_pattern: "量能",
|
||||||
|
price_shape: "价格形态",
|
||||||
|
} as const
|
||||||
|
|
||||||
export function PatternScoreSummary({ score }: PatternScoreProps) {
|
export function PatternScoreSummary({ score }: PatternScoreProps) {
|
||||||
if (!score) return <span className="text-muted-foreground">未评分</span>
|
if (!score) return <span className="text-muted-foreground">未评分</span>
|
||||||
if (score.status === "failed") {
|
if (score.status === "failed") {
|
||||||
@@ -44,9 +51,20 @@ export function PatternScoreDetails({ score }: PatternScoreProps) {
|
|||||||
<p className="text-xl font-semibold tabular-nums">
|
<p className="text-xl font-semibold tabular-nums">
|
||||||
{formatScore(score.value)} 分
|
{formatScore(score.value)} 分
|
||||||
</p>
|
</p>
|
||||||
<Badge variant={score.status === "matched" ? "default" : "outline"}>
|
{score.breakdown ? (
|
||||||
{score.status === "matched" ? "已匹配" : "低于匹配阈值"}
|
<dl className="grid grid-cols-2 gap-1.5 text-xs">
|
||||||
</Badge>
|
{Object.entries(breakdownLabels).map(([key, label]) => (
|
||||||
|
<div key={key} className="rounded-md border border-border/60 p-1.5">
|
||||||
|
<dt className="text-muted-foreground">{label}</dt>
|
||||||
|
<dd className="mt-0.5 font-semibold tabular-nums">
|
||||||
|
{formatScore(
|
||||||
|
score.breakdown?.[key as keyof typeof breakdownLabels],
|
||||||
|
)}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</dl>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ describe("selection chart option", () => {
|
|||||||
})
|
})
|
||||||
expect(dataZoom[1].xAxisIndex).toEqual([0, 1, 2])
|
expect(dataZoom[1].xAxisIndex).toEqual([0, 1, 2])
|
||||||
expect(legends[0]?.data).toEqual(["知行白线", "知行黄线"])
|
expect(legends[0]?.data).toEqual(["知行白线", "知行黄线"])
|
||||||
|
expect(legends[1]?.data).toEqual(["J"])
|
||||||
expect(series.find((item) => item.name === "日线")).toMatchObject({
|
expect(series.find((item) => item.name === "日线")).toMatchObject({
|
||||||
xAxisIndex: 0,
|
xAxisIndex: 0,
|
||||||
yAxisIndex: 0,
|
yAxisIndex: 0,
|
||||||
@@ -62,7 +63,16 @@ describe("selection chart option", () => {
|
|||||||
xAxisIndex: 1,
|
xAxisIndex: 1,
|
||||||
yAxisIndex: 1,
|
yAxisIndex: 1,
|
||||||
})
|
})
|
||||||
expect(series.filter((item) => item.xAxisIndex === 2)).toHaveLength(3)
|
expect(series.filter((item) => item.xAxisIndex === 2)).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
data: points.map((item) => item.j),
|
||||||
|
name: "J",
|
||||||
|
xAxisIndex: 2,
|
||||||
|
yAxisIndex: 2,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
expect(series.find((item) => item.name === "K")).toBeUndefined()
|
||||||
|
expect(series.find((item) => item.name === "D")).toBeUndefined()
|
||||||
expect(series[0].data[0]).toEqual([10, 10.5, 9, 11])
|
expect(series[0].data[0]).toEqual([10, 10.5, 9, 11])
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -91,7 +101,7 @@ describe("selection chart option", () => {
|
|||||||
expect(candle).toHaveLength(4)
|
expect(candle).toHaveLength(4)
|
||||||
expect(candle.every(Number.isNaN)).toBe(true)
|
expect(candle.every(Number.isNaN)).toBe(true)
|
||||||
expect(volume?.data[0]).toMatchObject({ value: null })
|
expect(volume?.data[0]).toMatchObject({ value: null })
|
||||||
expect(lines).toHaveLength(5)
|
expect(lines).toHaveLength(3)
|
||||||
for (const indicator of lines) {
|
for (const indicator of lines) {
|
||||||
expect(indicator.data).toEqual([null])
|
expect(indicator.data).toEqual([null])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export type SelectionChartOption = ComposeOption<
|
|||||||
/**
|
/**
|
||||||
* Build the three-grid financial chart configuration from aligned API points.
|
* Build the three-grid financial chart configuration from aligned API points.
|
||||||
*
|
*
|
||||||
* @param points Ascending qfq OHLCV, KDJ, and Zhixing-line points from the API.
|
* @param points Ascending qfq OHLCV, J, and Zhixing-line points from the API.
|
||||||
* @returns One ECharts option with synchronized axes and an exact 120-point window.
|
* @returns One ECharts option with synchronized axes and an exact 120-point window.
|
||||||
*/
|
*/
|
||||||
export function buildSelectionChartOption(
|
export function buildSelectionChartOption(
|
||||||
@@ -75,7 +75,7 @@ export function buildSelectionChartOption(
|
|||||||
top: 8,
|
top: 8,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
data: ["K", "D", "J"],
|
data: ["J"],
|
||||||
right: 20,
|
right: 20,
|
||||||
top: "73%",
|
top: "73%",
|
||||||
},
|
},
|
||||||
@@ -126,25 +126,25 @@ export function buildSelectionChartOption(
|
|||||||
xAxisIndex: 1,
|
xAxisIndex: 1,
|
||||||
yAxisIndex: 1,
|
yAxisIndex: 1,
|
||||||
},
|
},
|
||||||
...(["k", "d", "j"] as const).map((key, index): LineSeriesOption => ({
|
{
|
||||||
connectNulls: false,
|
connectNulls: false,
|
||||||
data: points.map((point) => point[key]),
|
data: points.map((point) => point.j),
|
||||||
name: key.toUpperCase(),
|
lineStyle: {
|
||||||
|
color: "#9333ea",
|
||||||
|
width: 1.5,
|
||||||
|
},
|
||||||
|
name: "J",
|
||||||
showSymbol: false,
|
showSymbol: false,
|
||||||
smooth: false,
|
smooth: false,
|
||||||
type: "line",
|
type: "line",
|
||||||
xAxisIndex: 2,
|
xAxisIndex: 2,
|
||||||
yAxisIndex: 2,
|
yAxisIndex: 2,
|
||||||
lineStyle: {
|
|
||||||
color: ["#2563eb", "#d97706", "#9333ea"][index],
|
|
||||||
width: 1.5,
|
|
||||||
},
|
},
|
||||||
})),
|
|
||||||
],
|
],
|
||||||
title: [
|
title: [
|
||||||
{ left: 12, text: "日线", textStyle: { fontSize: 12 }, top: 8 },
|
{ left: 12, text: "日线", textStyle: { fontSize: 12 }, top: 8 },
|
||||||
{ left: 72, text: "成交量", textStyle: { fontSize: 12 }, top: "54%" },
|
{ left: 72, text: "成交量", textStyle: { fontSize: 12 }, top: "54%" },
|
||||||
{ left: 72, text: "KDJ", textStyle: { fontSize: 12 }, top: "72%" },
|
{ left: 72, text: "J", textStyle: { fontSize: 12 }, top: "72%" },
|
||||||
],
|
],
|
||||||
tooltip: {
|
tooltip: {
|
||||||
axisPointer: { type: "cross" },
|
axisPointer: { type: "cross" },
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ describe("SelectionChart", () => {
|
|||||||
|
|
||||||
expect(
|
expect(
|
||||||
screen.getByRole("img", {
|
screen.getByRole("img", {
|
||||||
name: "药明康德 日线、知行黄白线、成交量和 KDJ 图表",
|
name: "药明康德 日线、知行黄白线、成交量和 J 线图表",
|
||||||
}),
|
}),
|
||||||
).toBeInTheDocument()
|
).toBeInTheDocument()
|
||||||
expect(chartMocks.init).toHaveBeenCalledTimes(1)
|
expect(chartMocks.init).toHaveBeenCalledTimes(1)
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export function SelectionChart({ chart, className }: SelectionChartProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
aria-label={`${chart.name} 日线、知行黄白线、成交量和 KDJ 图表`}
|
aria-label={`${chart.name} 日线、知行黄白线、成交量和 J 线图表`}
|
||||||
className={cn("h-full min-h-70 w-full", className)}
|
className={cn("h-full min-h-70 w-full", className)}
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
role="img"
|
role="img"
|
||||||
|
|||||||
@@ -212,7 +212,10 @@ describe("SelectionResultsPage", () => {
|
|||||||
expect(
|
expect(
|
||||||
within(detailPanel).getByText("最接近案例:华纳药厂"),
|
within(detailPanel).getByText("最接近案例:华纳药厂"),
|
||||||
).toBeInTheDocument()
|
).toBeInTheDocument()
|
||||||
expect(within(detailPanel).queryByText("价格形态")).not.toBeInTheDocument()
|
expect(within(detailPanel).getByText("价格形态")).toBeInTheDocument()
|
||||||
|
expect(within(detailPanel).getByText("趋势")).toBeInTheDocument()
|
||||||
|
expect(within(detailPanel).getByText("量能")).toBeInTheDocument()
|
||||||
|
expect(within(detailPanel).queryByText("已匹配")).not.toBeInTheDocument()
|
||||||
expect(within(detailPanel).getByText("J 值")).toBeInTheDocument()
|
expect(within(detailPanel).getByText("J 值")).toBeInTheDocument()
|
||||||
expect(within(detailPanel).getByText("RSI")).toBeInTheDocument()
|
expect(within(detailPanel).getByText("RSI")).toBeInTheDocument()
|
||||||
expect(within(detailPanel).getByText("日涨跌幅")).toBeInTheDocument()
|
expect(within(detailPanel).getByText("日涨跌幅")).toBeInTheDocument()
|
||||||
@@ -239,7 +242,7 @@ describe("SelectionResultsPage", () => {
|
|||||||
score: null,
|
score: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
expected: "低于匹配阈值",
|
expected: "低于阈值",
|
||||||
score: {
|
score: {
|
||||||
breakdown: {
|
breakdown: {
|
||||||
kdj_state: 42,
|
kdj_state: 42,
|
||||||
|
|||||||
Reference in New Issue
Block a user