chore: initialize lhbfx project and documentation
This commit is contained in:
272
frontend/src/composables/useDashboardData.ts
Normal file
272
frontend/src/composables/useDashboardData.ts
Normal file
@ -0,0 +1,272 @@
|
||||
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) {
|
||||
selectedTraderId.value = traderId
|
||||
traderDetail.value = await api<TraderDetail>(`/api/traders/${traderId}`)
|
||||
}
|
||||
|
||||
async function selectStock(stockCode: string) {
|
||||
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]) {
|
||||
await selectTrader(traderResult[0].id)
|
||||
}
|
||||
|
||||
const preferredStockCode = watchlist.value[0]?.stock_code ?? warningResult[0]?.stock_code
|
||||
if (preferredStockCode) {
|
||||
selectedWarningCode.value = preferredStockCode
|
||||
await selectStock(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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user