mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 15:34:16 +08:00
perf(kline): update detail chart with latest row
This commit is contained in:
@@ -44,8 +44,6 @@ interface Props {
|
||||
onPriceDoubleClick?: (price: number, currentPrice: number) => void
|
||||
/** 扩展数据列参数(逗号分隔 config_id.field_name),透传给 klineDaily 接口 */
|
||||
extColumns?: string
|
||||
/** 日K自动刷新间隔(ms)。undefined = 不轮询(默认)。个股对话框实时刷新时传入, 盘中今日蜡烛随之更新 */
|
||||
refetchIntervalMs?: number
|
||||
}
|
||||
|
||||
function isValidRow(r: any): boolean {
|
||||
@@ -121,7 +119,6 @@ export function StockDailyKChart({
|
||||
onDateClick,
|
||||
onPriceDoubleClick,
|
||||
extColumns,
|
||||
refetchIntervalMs,
|
||||
}: Props) {
|
||||
const [activeIndicators, setActiveIndicators] = useState<string[]>(['vol'])
|
||||
const [showMarkers, setShowMarkers] = useState(true)
|
||||
@@ -131,7 +128,7 @@ export function StockDailyKChart({
|
||||
const dateRange = externalDateRange ?? getDefaultRange()
|
||||
|
||||
// 查询配置统一来自 klineDailyQueryOptions, 与 StockPanel 信息条/邻近预取共享同一 cache key (只发一次请求)
|
||||
const kline = useQuery({ ...klineDailyQueryOptions(symbol, dateRange, extColumns), enabled: !!symbol, refetchInterval: refetchIntervalMs })
|
||||
const kline = useQuery({ ...klineDailyQueryOptions(symbol, dateRange, extColumns), enabled: !!symbol })
|
||||
|
||||
const rows = useMemo(() => toOHLC(kline.data?.rows ?? []), [kline.data?.rows])
|
||||
const stockInfo = kline.data?.stock_info
|
||||
|
||||
@@ -40,7 +40,7 @@ export function StockIntradayChart({
|
||||
// 避免读到分钟增量落盘的上一轮本地分区; 历史日期后端自行忽略 live。
|
||||
...klineMinuteQueryOptions(symbol, date ?? undefined, refetchIntervalMs != null),
|
||||
enabled: !!symbol && !!date,
|
||||
refetchInterval: refetchIntervalMs,
|
||||
refetchInterval: query => query.state.data?.source === 'none' ? false : refetchIntervalMs,
|
||||
})
|
||||
|
||||
const fetchMinute = useMutation({
|
||||
|
||||
@@ -215,7 +215,6 @@ export function StockPanel({
|
||||
onPriceDoubleClick={onPriceDoubleClick}
|
||||
visibleBars={showIntraday ? 40 : 60}
|
||||
extColumns={extColumns}
|
||||
refetchIntervalMs={refetchIntervalMs}
|
||||
/>
|
||||
|
||||
{showIntraday && selectedDate && !intradayDismissed && (
|
||||
|
||||
+19
-7
@@ -234,6 +234,20 @@ export interface KlineRow {
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface KlineDailyResponse {
|
||||
symbol: string
|
||||
name?: string
|
||||
stock_info?: { name?: string; total_shares?: number; float_shares?: number; ext?: Record<string, unknown> }
|
||||
rows: KlineRow[]
|
||||
source?: string
|
||||
}
|
||||
|
||||
export interface KlineDailyLatestResponse {
|
||||
symbol: string
|
||||
row: KlineRow | null
|
||||
source: 'live' | 'none'
|
||||
}
|
||||
|
||||
// ===== Watchlist =====
|
||||
export interface WatchlistEntry {
|
||||
symbol: string
|
||||
@@ -2082,18 +2096,16 @@ export const api = {
|
||||
request<CapabilitiesResponse>('/api/capabilities/redetect', { method: 'POST' }),
|
||||
|
||||
klineDaily: (symbol: string, days = 120, dateRange?: { start: string; end: string }, extColumns?: string) =>
|
||||
request<{
|
||||
symbol: string
|
||||
name?: string
|
||||
stock_info?: { name?: string; total_shares?: number; float_shares?: number; ext?: Record<string, unknown> }
|
||||
rows: KlineRow[]
|
||||
source?: string
|
||||
}>(
|
||||
request<KlineDailyResponse>(
|
||||
(dateRange
|
||||
? `/api/kline/daily?symbol=${encodeURIComponent(symbol)}&start_date=${dateRange.start}&end_date=${dateRange.end}`
|
||||
: `/api/kline/daily?symbol=${encodeURIComponent(symbol)}&days=${days}`)
|
||||
+ (extColumns ? `&ext_columns=${encodeURIComponent(extColumns)}` : ''),
|
||||
),
|
||||
klineDailyLatest: (symbol: string) =>
|
||||
request<KlineDailyLatestResponse>(
|
||||
`/api/kline/daily/latest?symbol=${encodeURIComponent(symbol)}`,
|
||||
),
|
||||
klineDailyBatch: (symbols: string[], days = 12) =>
|
||||
request<{ data: Record<string, KlineRow[]> }>('/api/kline/daily-batch', {
|
||||
method: 'POST',
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* placeholderData 内置"仅同 symbol 占位"守卫: 改日期范围/扩展字段时旧数据可暂显(不闪),
|
||||
* 切股时不透传上一只股票的数据(不误显示)。
|
||||
*/
|
||||
import { api } from '@/lib/api'
|
||||
import { api, type KlineDailyLatestResponse, type KlineDailyResponse } from '@/lib/api'
|
||||
import { QK } from '@/lib/queryKeys'
|
||||
|
||||
/** 分时 tab 多日分时默认周期 (StockPanel 预取与弹窗存储回退共用, 避免魔数两处漂移) */
|
||||
@@ -29,6 +29,34 @@ export function klineDailyQueryOptions(
|
||||
}
|
||||
}
|
||||
|
||||
export function klineDailyLatestQueryOptions(symbol: string) {
|
||||
return {
|
||||
queryKey: QK.klineLatest(symbol),
|
||||
queryFn: () => api.klineDailyLatest(symbol),
|
||||
}
|
||||
}
|
||||
|
||||
export function mergeLatestKlineRow(
|
||||
current: KlineDailyResponse | undefined,
|
||||
latest: KlineDailyLatestResponse,
|
||||
): KlineDailyResponse | undefined {
|
||||
if (!current || !latest.row || current.symbol !== latest.symbol) return current
|
||||
|
||||
const latestDate = String(latest.row.date).slice(0, 10)
|
||||
const last = current.rows.at(-1)
|
||||
if (!last) return { ...current, rows: [{ ...latest.row, date: latestDate }] }
|
||||
|
||||
const lastDate = String(last.date).slice(0, 10)
|
||||
if (latestDate < lastDate) return current
|
||||
if (latestDate === lastDate) {
|
||||
return {
|
||||
...current,
|
||||
rows: [...current.rows.slice(0, -1), { ...last, ...latest.row, date: latestDate }],
|
||||
}
|
||||
}
|
||||
return { ...current, rows: [...current.rows, { ...latest.row, date: latestDate }] }
|
||||
}
|
||||
|
||||
/**
|
||||
* 单日分时查询配置 — 与 klineDailyQueryOptions 同风格的单源 options (date 为空 = 最新日内)。
|
||||
*
|
||||
|
||||
@@ -79,6 +79,7 @@ export const QK = {
|
||||
// Kline
|
||||
kline: (symbol: string, start: string, end: string, extColumns?: string) =>
|
||||
['kline', symbol, start, end, extColumns ?? ''] as const,
|
||||
klineLatest: (symbol: string) => ['kline-latest', symbol] as const,
|
||||
stockLevels: (symbol: string, days?: number) => ['stock-levels', symbol, days ?? 120] as const,
|
||||
klineMinute: (symbol: string, date: string) =>
|
||||
['kline-minute', symbol, date] as const,
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useEffect, useRef, useCallback, useSyncExternalStore } from 'react'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { SSE_INVALIDATE_PREFIXES, QK } from './queryKeys'
|
||||
import { klineDailyLatestQueryOptions, mergeLatestKlineRow } from './kline'
|
||||
import { getQueryConfig } from './useQueryConfig'
|
||||
import { toast } from '@/components/Toast'
|
||||
import { pushAlertToasts } from '@/components/AlertToast'
|
||||
import { feedReviewEvent } from './reviewStore'
|
||||
import type { StrategyAlertEvent } from './api'
|
||||
import type { KlineDailyResponse, StrategyAlertEvent } from './api'
|
||||
|
||||
// ===== 全局 SSE 连接状态 (模块级 store, 仿 AlertToast.tsx 模式) =====
|
||||
// 实时行情 SSE 断开时 UI 无感知 → 会漏掉策略告警。这里暴露连接状态,
|
||||
@@ -47,8 +48,8 @@ export function useQuoteStreamStatus(): QuoteStreamStatus {
|
||||
}
|
||||
|
||||
// ===== 焦点股票注册表 (个股对话框用) =====
|
||||
// 个股对话框打开时注册当前 symbol, SSE quotes_updated 推送时精准 invalidate
|
||||
// 该 symbol 的日K查询 (['kline', symbol]), 让日K最后一根蜡烛随实时价变化。
|
||||
// 个股对话框打开时注册当前 symbol, SSE quotes_updated 推送时只取当日最新行,
|
||||
// 再原位更新该 symbol 的日K查询缓存,避免重复下载整段历史。
|
||||
// 不加进 SSE_INVALIDATE_PREFIXES 全局列表 —— 避免回测弹窗等也每秒重拉。
|
||||
let _focusSymbol: string | null = null
|
||||
|
||||
@@ -161,10 +162,25 @@ export function useQuoteStream(
|
||||
),
|
||||
})
|
||||
}
|
||||
// 焦点股票日K精准刷新: 个股对话框打开时, 日K最后一根蜡烛随实时价变化。
|
||||
// 后端 _maybe_inject_live_candle 只读内存缓存, 不调 TickFlow, 秒级重拉零额外成本。
|
||||
// 焦点股票日K增量刷新: 只取内存中的当日行并合并缓存尾部。
|
||||
if (_focusSymbol) {
|
||||
qc.invalidateQueries({ queryKey: ['kline', _focusSymbol] })
|
||||
const symbol = _focusSymbol
|
||||
void qc.fetchQuery({ ...klineDailyLatestQueryOptions(symbol), staleTime: 0 })
|
||||
.then((latest) => {
|
||||
if (_focusSymbol !== symbol || !latest.row) return
|
||||
const latestDate = String(latest.row.date).slice(0, 10)
|
||||
const queries = qc.getQueryCache().findAll({ queryKey: ['kline', symbol] })
|
||||
for (const query of queries) {
|
||||
const start = String(query.queryKey[2] ?? '')
|
||||
const end = String(query.queryKey[3] ?? '')
|
||||
if (latestDate < start || latestDate > end) continue
|
||||
qc.setQueryData<KlineDailyResponse>(
|
||||
query.queryKey,
|
||||
current => mergeLatestKlineRow(current, latest),
|
||||
)
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user