feat(selection): implement gold brick resonance strategy with evaluation and logging enhancements
This commit is contained in:
@@ -77,7 +77,7 @@ export const routePresentation: Record<string, RoutePresentation> = {
|
||||
"/selection": {
|
||||
id: "selection",
|
||||
breadcrumb: "研究工作台",
|
||||
title: "知行 B1 执行结果",
|
||||
title: "选股策略执行结果",
|
||||
},
|
||||
"/sector-radar": {
|
||||
id: "sector-radar",
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user