feat(selection): implement gold brick resonance strategy with evaluation and logging enhancements

This commit is contained in:
yuxuanhui
2026-09-05 10:22:08 +08:00
parent 9163590070
commit 79476252b1
20 changed files with 1060 additions and 97 deletions
@@ -1,13 +1,16 @@
export type SelectionStrategy = "zhixing_b1"
export const selectionStrategies = ["zhixing_b1", "gold_brick"] as const
export type SelectionStrategy = (typeof selectionStrategies)[number]
export type SelectionCategoryFilter =
"all" | "pullback" | "oversold" | "original"
"all" | "pullback" | "oversold" | "original" | "resonance"
export const selectionCategoryFilters = [
"all",
"pullback",
"oversold",
"original",
"resonance",
] as const
export type SelectionSort = "code" | "score_desc" | "score_asc"
@@ -15,7 +15,10 @@ import {
import type { SelectionResults } from "../api/selection.types"
import { IncompleteEvaluationTable } from "./incomplete-evaluation-table"
import { getSelectionRunStatusPresentation } from "./selection-presentation"
import {
getSelectionRunStatusPresentation,
getSelectionStrategyLabel,
} from "./selection-presentation"
interface ExecutionStatusDrawerProps {
finalFocus: RefObject<HTMLElement | null>
@@ -44,7 +47,9 @@ export function ExecutionStatusDrawer({
showCloseButton={false}
>
<DialogHeader className="shrink-0 gap-1 pr-10">
<DialogTitle className="text-lg">知行 B1 执行状态</DialogTitle>
<DialogTitle className="text-lg">
{getSelectionStrategyLabel(result.strategy)}执行状态
</DialogTitle>
<DialogDescription>
目标交易日:{tradeDate};当前状态:{statusLabel};未完成评估:
{result.failures.length} 条。
@@ -8,6 +8,7 @@ const failureStatusLabels: Record<string, string> = {
data_error: "数据读取失败",
insufficient_history: "历史数据不足",
missing_target_bar: "缺少目标交易日行情",
missing_turnover_rate: "缺少目标交易日换手率",
}
export function IncompleteEvaluationTable({
@@ -2,6 +2,7 @@ import type {
SelectionCategoryFilter,
SelectionRunStatus,
SelectionSignal,
SelectionStrategy,
SelectionStockResult,
} from "../api/selection.types"
@@ -20,6 +21,7 @@ export const selectionRunStatusPresentation: Record<
}
export const categoryLabels: Record<string, string> = {
gold_brick_resonance: "金砖共振",
zhixing_b1_extreme_volume: "超卖超缩量 B",
zhixing_b1_original_b1: "原始 B1",
zhixing_b1_oversold_turn: "超卖缩量拐头 B",
@@ -30,18 +32,35 @@ export const categoryLabels: Record<string, string> = {
}
const detailLabels: Record<string, string> = {
brick_chart: "砖型图",
brick_length: "砖柱变化",
daily_amplitude: "日振幅",
daily_change: "日涨跌幅",
j: "J 值",
macd: "MACD",
rsi: "RSI",
resonance_type: "共振条件",
signal: "信号",
sub_signal: "命中信号",
trend_white: "知行白线",
trend_yellow: "知行黄线",
turnover_rate: "换手率",
upper_shadow_strength: "上影线强度",
volume: "成交量",
x_momentum: "X 动能",
yellow_column: "黄柱",
}
const percentageDetailKeys = new Set(["daily_amplitude", "daily_change"])
const percentageDetailKeys = new Set([
"daily_amplitude",
"daily_change",
"turnover_rate",
])
const strategyLabels: Record<SelectionStrategy, string> = {
gold_brick: "金砖共振",
zhixing_b1: "知行 B1",
}
export type SignalCategoryFilter = SelectionCategoryFilter
@@ -53,8 +72,13 @@ export const signalCategoryOptions: ReadonlyArray<{
{ label: "回踩类", value: "pullback" },
{ label: "超卖类", value: "oversold" },
{ label: "原始 B1", value: "original" },
{ label: "金砖共振", value: "resonance" },
]
export function getSelectionStrategyLabel(strategy: SelectionStrategy) {
return strategyLabels[strategy]
}
export function getCategoryLabel(category: string) {
return categoryLabels[category] ?? category
}
@@ -151,6 +175,9 @@ export function matchesCategory(
}
export function categoryToneClass(category: string) {
if (category.includes("gold_brick")) {
return "border-warning/30 bg-warning/10 text-warning"
}
if (category.includes("pullback")) {
return "border-warning/30 bg-warning/10 text-warning"
}
@@ -7,7 +7,10 @@ import { Card } from "@/shared/ui/card"
import type { SelectionStockResult } from "../api/selection.types"
import { PatternScoreDetails } from "./pattern-score"
import { PatternCaseImage } from "./pattern-case-image"
import { getStockBasicMetrics } from "./selection-presentation"
import {
getSelectionStrategyLabel,
getStockBasicMetrics,
} from "./selection-presentation"
const SelectionChart = lazy(() =>
import("./selection-chart").then((module) => ({
@@ -49,7 +52,11 @@ export function SignalDetailPanel({ stock }: SignalDetailPanelProps) {
label: "目标交易日",
value: stock.target_trade_date,
},
{ key: "strategy", label: "策略", value: "知行 B1" },
{
key: "strategy",
label: "策略",
value: getSelectionStrategyLabel(stock.strategy),
},
]
return (
@@ -1,5 +1,5 @@
import { AlertTriangle, Play, RefreshCw, Search } from "lucide-react"
import { useSearch } from "@tanstack/react-router"
import { useNavigate, useSearch } from "@tanstack/react-router"
import { useRef, useState, type RefObject } from "react"
import { PageLayout } from "@/app/layout/page-layout"
@@ -11,8 +11,10 @@ import {
} from "@/features/selection/api/selection.query"
import {
selectionResultPageSize,
selectionStrategies,
type SelectionResults,
type SelectionResultsQuery,
type SelectionStrategy,
} from "@/features/selection/api/selection.types"
import { Button } from "@/shared/ui/button"
import {
@@ -43,20 +45,27 @@ import { Skeleton } from "@/shared/ui/skeleton"
import { ExecutionStatusDrawer } from "../components/execution-status-drawer"
import { ExecutionStatusTrigger } from "../components/execution-status-trigger"
import { getSelectionStrategyLabel } from "../components/selection-presentation"
import { SelectionResultsWorkbench } from "../components/selection-results-workbench"
const STRATEGY = "zhixing_b1" as const
const DEFAULT_STRATEGY: SelectionStrategy = "zhixing_b1"
const strategyOptions = selectionStrategies.map((value) => ({
label: getSelectionStrategyLabel(value),
value,
}))
export function SelectionResultsPage() {
// undefined follows the persisted result; null means the user cleared the picker.
const [targetTradeDate, setTargetTradeDate] = useState<
string | null | undefined
>(undefined)
const [strategy, setStrategy] = useState<SelectionStrategy>(DEFAULT_STRATEGY)
const [activeRunId, setActiveRunId] = useState<string | null>(null)
const [executionStatusDrawerOpen, setExecutionStatusDrawerOpen] =
useState(false)
const [rerunDialogOpen, setRerunDialogOpen] = useState(false)
const executionStatusTriggerRef = useRef<HTMLButtonElement>(null)
const navigate = useNavigate({ from: "/selection" })
const search = useSearch({ from: "/_workspace/selection" })
const resultQuery: Omit<SelectionResultsQuery, "page"> = {
pageSize: selectionResultPageSize,
@@ -66,7 +75,7 @@ export function SelectionResultsPage() {
}
const results = useSelectionResults(
STRATEGY,
strategy,
targetTradeDate || undefined,
resultQuery,
)
@@ -120,7 +129,7 @@ export function SelectionResultsPage() {
trigger.mutate(
{
request: {
strategy: STRATEGY,
strategy,
target_trade_date: selectedTargetTradeDate,
rerun,
},
@@ -136,6 +145,21 @@ export function SelectionResultsPage() {
trigger.reset()
}
function handleStrategyChange(value: string) {
const nextStrategy = selectionStrategies.find(
(candidate) => candidate === value,
)
if (!nextStrategy || nextStrategy === strategy) return
setStrategy(nextStrategy)
setExecutionStatusDrawerOpen(false)
setActiveRunId(null)
setRerunDialogOpen(false)
trigger.reset()
void navigate({
search: (previous) => ({ ...previous, category: "all" }),
})
}
return (
<>
<PageLayout
@@ -148,8 +172,10 @@ export function SelectionResultsPage() {
onDateChange={handleDateChange}
onExecute={handleExecute}
onOpenExecutionStatus={() => setExecutionStatusDrawerOpen(true)}
onStrategyChange={handleStrategyChange}
result={displayedResult}
selectedTargetTradeDate={selectedTargetTradeDate}
strategy={strategy}
triggerError={trigger.isError}
triggerRef={executionStatusTriggerRef}
/>
@@ -168,7 +194,7 @@ export function SelectionResultsPage() {
{!isRunning &&
!hasQueryError &&
displayedResult?.status === "no_data" ? (
<NoDataState />
<NoDataState strategy={strategy} />
) : null}
{!isRunning &&
!hasQueryError &&
@@ -196,8 +222,9 @@ export function SelectionResultsPage() {
<DialogHeader>
<DialogTitle>确认重新执行策略?</DialogTitle>
<DialogDescription>
重新执行前会清空 {selectedTargetTradeDate} 的知行 B1
旧结果,再重新计算当前有效股票池。 旧结果清空后无法恢复。
重新执行前会清空 {selectedTargetTradeDate} 的「
{getSelectionStrategyLabel(strategy)}
」旧结果,再重新计算当前有效股票池。旧结果清空后无法恢复。
</DialogDescription>
</DialogHeader>
<DialogFooter>
@@ -241,8 +268,10 @@ interface ExecutionToolbarProps {
onDateChange: (value: Date | undefined) => void
onExecute: () => void
onOpenExecutionStatus: () => void
onStrategyChange: (value: string) => void
result: SelectionResults | undefined
selectedTargetTradeDate: string
strategy: SelectionStrategy
triggerError: boolean
triggerRef: RefObject<HTMLButtonElement | null>
}
@@ -255,8 +284,10 @@ function ExecutionToolbar({
onDateChange,
onExecute,
onOpenExecutionStatus,
onStrategyChange,
result,
selectedTargetTradeDate,
strategy,
triggerError,
triggerRef,
}: ExecutionToolbarProps) {
@@ -275,9 +306,12 @@ function ExecutionToolbar({
<div className="space-y-1 text-xs font-medium">
<span className="block">策略</span>
<Select
disabled
items={[{ label: "知行 B1", value: STRATEGY }]}
value={STRATEGY}
disabled={isRunning}
items={strategyOptions}
onValueChange={(value) => {
if (typeof value === "string") onStrategyChange(value)
}}
value={strategy}
>
<SelectTrigger
aria-label="策略"
@@ -287,7 +321,11 @@ function ExecutionToolbar({
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value={STRATEGY}>知行 B1</SelectItem>
{strategyOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
@@ -418,7 +456,7 @@ function QueryError() {
)
}
function NoDataState() {
function NoDataState({ strategy }: { strategy: SelectionStrategy }) {
return (
<Card>
<CardHeader className="gap-1.5 p-4">
@@ -427,7 +465,8 @@ function NoDataState() {
暂无策略结果
</CardTitle>
<CardDescription>
请选择目标交易日并执行知行 B1,完成后这里会显示持久化结果。
请选择目标交易日并执行{getSelectionStrategyLabel(strategy)}
,完成后这里会显示持久化结果。
</CardDescription>
</CardHeader>
</Card>