Files
lhbfx/frontend/src/composables/useDashboardData.ts

289 lines
8.7 KiB
TypeScript
Raw Normal View History

import { computed, ref, shallowRef } from 'vue'
import type {
ActionItem,
ActionsResponse,
PipelineStatus,
StockDetail,
Summary,
TraderDetail,
TraderListItem,
WarningItem,
WatchlistItem,
} from '../types'
import { numberFromText } from '../utils/format'
async function api<T>(path: string, init: RequestInit = {}): Promise<T> {
const headers = new Headers(init.headers)
if (init.body && !headers.has('Content-Type')) {
headers.set('Content-Type', 'application/json')
}
const response = await fetch(path, {
...init,
headers,
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
return response.json() as Promise<T>
}
export function useDashboardData() {
const status = ref<PipelineStatus | null>(null)
const summary = ref<Summary | null>(null)
const warnings = ref<WarningItem[]>([])
const traders = ref<TraderListItem[]>([])
const traderDetail = ref<TraderDetail | null>(null)
const stockDetail = ref<StockDetail | null>(null)
const actions = ref<ActionItem[]>([])
const watchlist = ref<WatchlistItem[]>([])
const selectedWarningCode = shallowRef('')
const selectedTraderId = shallowRef<number | null>(null)
const selectedStockCode = shallowRef('')
const selectedDateFrom = shallowRef('')
const selectedDateTo = shallowRef('')
const selectedTraderFilter = shallowRef('all')
const selectedActionFilter = shallowRef<'all' | 'buy' | 'sell' | 'net_buy' | 'net_sell'>('all')
const isBooting = shallowRef(true)
const errorMessage = shallowRef('')
const activeWarning = computed(() => {
return warnings.value.find((item) => item.stock_code === selectedWarningCode.value) ?? null
})
const availableTradeDates = computed(() => {
return (status.value?.recent_trade_days ?? [])
.map((item) => item.trade_date)
.filter((item): item is string => Boolean(item))
})
const filteredActions = computed(() => {
return actions.value.filter((item) => {
if (selectedTraderFilter.value !== 'all' && item.trader_name !== selectedTraderFilter.value) {
return false
}
if (selectedActionFilter.value !== 'all' && item.action_side !== selectedActionFilter.value) {
return false
}
return true
})
})
const watchlistMap = computed(() => {
return new Map(watchlist.value.map((item) => [item.stock_code, item]))
})
const watchedActionRows = computed(() => {
return filteredActions.value.filter((item) => watchlistMap.value.has(item.stock_code))
})
const candidateActionRows = computed(() => {
const unique = new Map<string, ActionItem>()
for (const item of filteredActions.value) {
if (watchlistMap.value.has(item.stock_code)) continue
if (!unique.has(item.stock_code)) {
unique.set(item.stock_code, item)
}
}
return [...unique.values()]
})
const watchlistMetrics = computed(() => {
const rows = watchedActionRows.value
const uniqueCodes = new Set(rows.map((item) => item.stock_code))
const buyTotal = rows.reduce((sum, item) => sum + (numberFromText(item.buy_amount_wan) ?? 0), 0)
const sellTotal = rows.reduce((sum, item) => sum + (numberFromText(item.sell_amount_wan) ?? 0), 0)
const netPositiveCount = new Set(
rows
.filter((item) => (numberFromText(item.net_amount_wan) ?? 0) > 0)
.map((item) => item.stock_code),
).size
const warningCount = new Set(
warnings.value
.filter((item) => watchlistMap.value.has(item.stock_code))
.map((item) => item.stock_code),
).size
return {
watchCount: watchlist.value.length,
activeWatchCount: uniqueCodes.size,
buyTotalWan: buyTotal,
sellTotalWan: sellTotal,
netPositiveCount,
warningCount,
}
})
const watchlistStocksForDisplay = computed(() => {
return watchlist.value.map((item) => {
const warning = warnings.value.find((warningItem) => warningItem.stock_code === item.stock_code) ?? null
return {
...item,
hasWarning: Boolean(warning),
warning,
}
})
})
function isWatched(stockCode: string): boolean {
return watchlistMap.value.has(stockCode)
}
async function loadWatchlist() {
watchlist.value = await api<WatchlistItem[]>('/api/watchlist')
}
async function addToWatchlist(item: Pick<WatchlistItem, 'stock_code' | 'stock_name' | 'source_trade_date' | 'source_trader_name'>) {
if (watchlistMap.value.has(item.stock_code)) return
await api<WatchlistItem>('/api/watchlist', {
method: 'POST',
body: JSON.stringify(item),
})
await loadWatchlist()
}
async function removeFromWatchlist(stockCode: string) {
if (!watchlistMap.value.has(stockCode)) return
await api<{ ok: boolean; stock_code: string }>(`/api/watchlist/${encodeURIComponent(stockCode)}`, {
method: 'DELETE',
})
await loadWatchlist()
}
async function loadActions() {
const params = new URLSearchParams()
if (selectedDateFrom.value && selectedDateTo.value) {
params.set('date_from', selectedDateFrom.value)
params.set('date_to', selectedDateTo.value)
}
if (selectedTraderFilter.value !== 'all') params.set('trader_name', selectedTraderFilter.value)
params.set('limit', '300')
const response = await api<ActionsResponse>(`/api/actions?${params.toString()}`)
actions.value = response.actions
if (!selectedDateFrom.value && response.date_from) {
selectedDateFrom.value = response.date_from
}
if (!selectedDateTo.value && response.date_to) {
selectedDateTo.value = response.date_to
}
}
async function selectTrader(traderId: number) {
if (traderDetail.value?.trader.id === traderId) {
selectedTraderId.value = traderId
return
}
selectedTraderId.value = traderId
traderDetail.value = await api<TraderDetail>(`/api/traders/${traderId}`)
}
async function selectStock(stockCode: string) {
if (stockDetail.value?.stock.stock_code === stockCode) {
selectedStockCode.value = stockCode
return
}
selectedStockCode.value = stockCode
stockDetail.value = await api<StockDetail>(`/api/stocks/${encodeURIComponent(stockCode)}`)
}
async function selectWarning(item: WarningItem) {
selectedWarningCode.value = item.stock_code
await selectStock(item.stock_code)
}
async function selectTradeDateRange(dateFrom: string, dateTo: string) {
selectedDateFrom.value = dateFrom
selectedDateTo.value = dateTo
await loadActions()
}
async function initialize() {
isBooting.value = true
errorMessage.value = ''
try {
const [statusResult, summaryResult, warningResult, traderResult, watchlistResult] = await Promise.all([
api<PipelineStatus>('/api/pipeline/status'),
api<Summary>('/api/summary'),
api<WarningItem[]>('/api/warnings?limit=40'),
api<TraderListItem[]>('/api/traders'),
api<WatchlistItem[]>('/api/watchlist'),
])
status.value = statusResult
summary.value = summaryResult
warnings.value = warningResult
traders.value = traderResult
watchlist.value = watchlistResult
const latestTradeDate = statusResult.recent_trade_days[0]?.trade_date ?? ''
selectedDateFrom.value = latestTradeDate
selectedDateTo.value = latestTradeDate
await loadActions()
if (traderResult[0]) {
selectedTraderId.value = traderResult[0].id
}
const watchlistCodeSet = new Set(watchlistResult.map((item) => item.stock_code))
const preferredWarningCode =
warningResult.find((item) => watchlistCodeSet.has(item.stock_code))?.stock_code ??
warningResult[0]?.stock_code ??
''
const preferredStockCode = watchlist.value[0]?.stock_code ?? preferredWarningCode
if (preferredWarningCode) {
selectedWarningCode.value = preferredWarningCode
}
if (preferredStockCode) {
selectedStockCode.value = preferredStockCode
}
} catch (error) {
errorMessage.value = String(error instanceof Error ? error.message : error)
} finally {
isBooting.value = false
}
}
return {
status,
summary,
warnings,
traders,
traderDetail,
stockDetail,
actions,
watchlist,
selectedWarningCode,
selectedTraderId,
selectedStockCode,
selectedDateFrom,
selectedDateTo,
selectedTraderFilter,
selectedActionFilter,
activeWarning,
availableTradeDates,
filteredActions,
watchedActionRows,
candidateActionRows,
watchlistMetrics,
watchlistStocksForDisplay,
isBooting,
errorMessage,
initialize,
loadActions,
loadWatchlist,
selectTrader,
selectStock,
selectWarning,
selectTradeDateRange,
isWatched,
addToWatchlist,
removeFromWatchlist,
}
}