2026-08-09 09:34:46 +08:00
|
|
|
import {
|
2026-09-01 19:27:41 +08:00
|
|
|
useInfiniteQuery,
|
2026-08-09 09:34:46 +08:00
|
|
|
useMutation,
|
|
|
|
|
useQuery,
|
|
|
|
|
useQueryClient,
|
2026-09-01 19:27:41 +08:00
|
|
|
type InfiniteData,
|
2026-08-09 09:34:46 +08:00
|
|
|
type QueryClient,
|
|
|
|
|
} from "@tanstack/react-query"
|
|
|
|
|
import { useEffect } from "react"
|
|
|
|
|
|
|
|
|
|
import {
|
2026-09-01 13:37:19 +08:00
|
|
|
getSelectionChart,
|
2026-08-09 09:34:46 +08:00
|
|
|
getSelectionResults,
|
|
|
|
|
getSelectionRun,
|
|
|
|
|
triggerSelectionRun,
|
|
|
|
|
} from "./selection.api"
|
2026-09-01 19:27:41 +08:00
|
|
|
import {
|
|
|
|
|
selectionResultPageSize,
|
|
|
|
|
type SelectionResults,
|
|
|
|
|
type SelectionResultsQuery,
|
|
|
|
|
type SelectionRunRequest,
|
|
|
|
|
type SelectionStrategy,
|
2026-08-10 11:09:23 +08:00
|
|
|
} from "./selection.types"
|
2026-08-09 09:34:46 +08:00
|
|
|
|
2026-09-01 19:27:41 +08:00
|
|
|
export type SelectionResultsListQuery = Omit<SelectionResultsQuery, "page">
|
|
|
|
|
|
|
|
|
|
const defaultListQuery: SelectionResultsListQuery = {
|
|
|
|
|
pageSize: selectionResultPageSize,
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-01 13:37:19 +08:00
|
|
|
export const selectionChartQueryKey = (
|
|
|
|
|
tsCode?: string,
|
|
|
|
|
targetTradeDate?: string,
|
2026-09-05 13:41:25 +08:00
|
|
|
strategy?: SelectionStrategy,
|
2026-09-01 13:37:19 +08:00
|
|
|
) =>
|
2026-09-05 13:41:25 +08:00
|
|
|
[
|
|
|
|
|
"selection",
|
|
|
|
|
"chart",
|
|
|
|
|
tsCode ?? "none",
|
|
|
|
|
targetTradeDate ?? "none",
|
|
|
|
|
strategy ?? "none",
|
|
|
|
|
] as const
|
2026-09-01 13:37:19 +08:00
|
|
|
|
2026-08-09 09:34:46 +08:00
|
|
|
export const selectionResultsQueryKey = (
|
|
|
|
|
strategy: SelectionStrategy,
|
|
|
|
|
targetTradeDate?: string,
|
2026-09-01 19:27:41 +08:00
|
|
|
query: SelectionResultsListQuery = defaultListQuery,
|
2026-08-10 11:09:23 +08:00
|
|
|
) =>
|
|
|
|
|
[
|
|
|
|
|
"selection",
|
|
|
|
|
"results",
|
|
|
|
|
strategy,
|
|
|
|
|
targetTradeDate,
|
|
|
|
|
query.pageSize,
|
|
|
|
|
query.search ?? "",
|
|
|
|
|
query.category ?? "all",
|
2026-08-31 16:14:16 +08:00
|
|
|
query.sort ?? "code",
|
2026-08-10 11:09:23 +08:00
|
|
|
] as const
|
2026-08-09 09:34:46 +08:00
|
|
|
|
2026-08-10 11:09:23 +08:00
|
|
|
export const selectionRunQueryKey = (
|
|
|
|
|
runId: string,
|
2026-09-01 19:27:41 +08:00
|
|
|
query: SelectionResultsListQuery = defaultListQuery,
|
2026-08-10 11:09:23 +08:00
|
|
|
) =>
|
|
|
|
|
[
|
|
|
|
|
"selection",
|
|
|
|
|
"runs",
|
|
|
|
|
runId,
|
|
|
|
|
query.pageSize,
|
|
|
|
|
query.search ?? "",
|
|
|
|
|
query.category ?? "all",
|
2026-08-31 16:14:16 +08:00
|
|
|
query.sort ?? "code",
|
2026-08-10 11:09:23 +08:00
|
|
|
] as const
|
2026-08-09 09:34:46 +08:00
|
|
|
|
2026-09-01 19:27:41 +08:00
|
|
|
export function flattenSelectionResults(
|
|
|
|
|
data: InfiniteData<SelectionResults> | undefined,
|
|
|
|
|
): SelectionResults | undefined {
|
|
|
|
|
const firstPage = data?.pages[0]
|
|
|
|
|
if (!firstPage) return undefined
|
|
|
|
|
return {
|
|
|
|
|
...firstPage,
|
|
|
|
|
stocks: data.pages.flatMap((page) => page.stocks),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-09 09:34:46 +08:00
|
|
|
export function useSelectionResults(
|
|
|
|
|
strategy: SelectionStrategy,
|
|
|
|
|
targetTradeDate?: string,
|
2026-09-01 19:27:41 +08:00
|
|
|
query: SelectionResultsListQuery = defaultListQuery,
|
2026-08-09 09:34:46 +08:00
|
|
|
) {
|
2026-09-01 19:27:41 +08:00
|
|
|
return useInfiniteQuery<
|
|
|
|
|
SelectionResults,
|
|
|
|
|
Error,
|
|
|
|
|
InfiniteData<SelectionResults>,
|
|
|
|
|
ReturnType<typeof selectionResultsQueryKey>,
|
|
|
|
|
number
|
|
|
|
|
>({
|
|
|
|
|
getNextPageParam: nextSelectionPageParam,
|
|
|
|
|
initialPageParam: 1,
|
|
|
|
|
queryFn: ({ pageParam, signal }) =>
|
|
|
|
|
getSelectionResults(
|
|
|
|
|
strategy,
|
|
|
|
|
targetTradeDate,
|
|
|
|
|
{ ...query, page: pageParam },
|
|
|
|
|
signal,
|
|
|
|
|
),
|
2026-08-10 11:09:23 +08:00
|
|
|
queryKey: selectionResultsQueryKey(strategy, targetTradeDate, query),
|
2026-08-09 09:34:46 +08:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-05 13:41:25 +08:00
|
|
|
export function useSelectionChart(
|
|
|
|
|
tsCode?: string,
|
|
|
|
|
targetTradeDate?: string,
|
|
|
|
|
strategy?: SelectionStrategy,
|
|
|
|
|
) {
|
2026-09-01 13:37:19 +08:00
|
|
|
return useQuery({
|
|
|
|
|
enabled: Boolean(tsCode && targetTradeDate),
|
|
|
|
|
queryFn: ({ signal }) =>
|
2026-09-05 13:41:25 +08:00
|
|
|
getSelectionChart(tsCode ?? "", targetTradeDate ?? "", strategy, signal),
|
|
|
|
|
queryKey: selectionChartQueryKey(tsCode, targetTradeDate, strategy),
|
2026-09-01 13:37:19 +08:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-10 11:09:23 +08:00
|
|
|
export function useSelectionRun(
|
|
|
|
|
runId: string | null,
|
2026-09-01 19:27:41 +08:00
|
|
|
resultQuery: SelectionResultsListQuery = defaultListQuery,
|
2026-08-10 11:09:23 +08:00
|
|
|
) {
|
2026-08-09 09:34:46 +08:00
|
|
|
const queryClient = useQueryClient()
|
2026-09-01 19:27:41 +08:00
|
|
|
const query = useInfiniteQuery<
|
|
|
|
|
SelectionResults,
|
|
|
|
|
Error,
|
|
|
|
|
InfiniteData<SelectionResults>,
|
|
|
|
|
ReturnType<typeof selectionRunQueryKey>,
|
|
|
|
|
number
|
|
|
|
|
>({
|
2026-08-09 09:34:46 +08:00
|
|
|
enabled: Boolean(runId),
|
2026-09-01 19:27:41 +08:00
|
|
|
getNextPageParam: nextSelectionPageParam,
|
|
|
|
|
initialPageParam: 1,
|
|
|
|
|
queryFn: ({ pageParam, signal }) =>
|
|
|
|
|
getSelectionRun(runId ?? "", { ...resultQuery, page: pageParam }, signal),
|
2026-08-10 11:09:23 +08:00
|
|
|
queryKey: selectionRunQueryKey(runId ?? "none", resultQuery),
|
2026-09-01 19:27:41 +08:00
|
|
|
refetchInterval: (currentQuery) =>
|
|
|
|
|
currentQuery.state.data?.pages[0]?.status === "running" ? 1500 : false,
|
2026-08-09 09:34:46 +08:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-09-01 19:27:41 +08:00
|
|
|
const result = query.data?.pages[0]
|
2026-08-09 09:34:46 +08:00
|
|
|
if (!runId || !result || result.status === "running") return
|
|
|
|
|
|
|
|
|
|
void invalidateSelectionResults(queryClient, result.strategy)
|
|
|
|
|
}, [query.data, queryClient, runId])
|
|
|
|
|
|
|
|
|
|
return query
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useTriggerSelectionRun() {
|
|
|
|
|
const queryClient = useQueryClient()
|
|
|
|
|
|
|
|
|
|
return useMutation({
|
|
|
|
|
mutationFn: ({
|
|
|
|
|
request,
|
|
|
|
|
signal,
|
|
|
|
|
}: {
|
|
|
|
|
request: SelectionRunRequest
|
|
|
|
|
signal?: AbortSignal
|
|
|
|
|
}) => triggerSelectionRun(request, signal),
|
|
|
|
|
onSuccess: (_accepted, { request }) => {
|
|
|
|
|
void invalidateSelectionResults(queryClient, request.strategy)
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function invalidateSelectionResults(
|
|
|
|
|
queryClient: QueryClient,
|
|
|
|
|
strategy: SelectionStrategy,
|
|
|
|
|
) {
|
|
|
|
|
return queryClient.invalidateQueries({
|
|
|
|
|
queryKey: ["selection", "results", strategy],
|
|
|
|
|
})
|
|
|
|
|
}
|
2026-09-01 19:27:41 +08:00
|
|
|
|
|
|
|
|
function nextSelectionPageParam(
|
|
|
|
|
lastPage: SelectionResults,
|
|
|
|
|
pages: SelectionResults[],
|
|
|
|
|
lastPageParam: number,
|
|
|
|
|
) {
|
|
|
|
|
const loadedStockCount = pages.reduce(
|
|
|
|
|
(count, page) => count + page.stocks.length,
|
|
|
|
|
0,
|
|
|
|
|
)
|
|
|
|
|
const total = lastPage.stocks_total ?? lastPage.selected_stock_count
|
|
|
|
|
if (
|
|
|
|
|
(lastPage.status !== "success" && lastPage.status !== "partial_success") ||
|
|
|
|
|
lastPage.stocks.length === 0 ||
|
|
|
|
|
lastPage.page !== lastPageParam ||
|
|
|
|
|
loadedStockCount >= total
|
|
|
|
|
) {
|
|
|
|
|
return undefined
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const nextPageParam = lastPageParam + 1
|
|
|
|
|
return Number.isSafeInteger(nextPageParam) ? nextPageParam : undefined
|
|
|
|
|
}
|