import { supabase } from './supabase.js' import { getAllDecisions, setAllDecisions, clearAllDecisions } from './dataCache.js' import { writeRawDecisions, clearAllStores, readAllRawDecisions } from './idb.js' import { filterRowsToNyseTradingDays, countNonTradingDaysBetweenForAsset, countTradingDaysBetweenForAsset } from './marketCalendar.js' import { computeBuyHoldEquity, computeStrategyEquity, calculateMetricsFromSeries, computeWinRate } from './perf.js' import { STRATEGIES } from './strategies.js' /** * 全局数据服务 * 负责从 Supabase 加载数据、缓存管理和数据计算 */ class DataService { constructor() { this.loading = false this.loaded = false this.agents = [] this.tableRows = [] this.lastUpdated = null this.dateBounds = { min: null, max: null } this.listeners = new Set() // Filter options cache this.useDbFilterCache = this._getDefaultUseDbFilterCache() this.nameOptions = [] this.assetOptions = [] this.modelOptions = [] this.strategyOptions = [] } _getDefaultUseDbFilterCache() { try { if (localStorage.getItem('disableDbFilterCache') === '1') return false } catch (_) {} const envFlag = ((import.meta.env.VITE_USE_DB_FILTER_CACHE || '') + '').toLowerCase() return envFlag === '1' || envFlag === 'true' || envFlag === 'yes' || envFlag === 'on' } /** * 订阅数据变化 */ subscribe(callback) { this.listeners.add(callback) return () => this.listeners.delete(callback) } /** * 通知所有订阅者 */ _notify() { this.listeners.forEach(callback => { try { callback({ loading: this.loading, loaded: this.loaded, agents: this.agents, tableRows: this.tableRows, lastUpdated: this.lastUpdated, dateBounds: this.dateBounds, nameOptions: this.nameOptions, assetOptions: this.assetOptions, modelOptions: this.modelOptions, strategyOptions: this.strategyOptions }) } catch (e) { console.error('[DataService] Error notifying listener:', e) } }) } /** * 从数据库加载筛选选项 */ async loadFilterOptionsFromDb() { if (!this.useDbFilterCache) return null try { const { data, error } = await supabase .from('filter_options') .select('names, assets, models, strategies, updated_at') .eq('id', 'latest') .maybeSingle() if (error || !data) { const msg = (error && error.message) ? String(error.message).toLowerCase() : '' const code = (error && error.code) ? String(error.code) : '' const isNotFound = msg.includes('not found') || msg.includes('does not exist') || code === '42P01' || code === 'PGRST116' if (isNotFound) { this.useDbFilterCache = false try { localStorage.setItem('disableDbFilterCache', '1') } catch (_) {} } return null } return { names: data.names || [], assets: data.assets || [], models: data.models || [], strategies: data.strategies || [] } } catch (e) { const msg = e && e.message ? String(e.message).toLowerCase() : '' if (msg.includes('not found') || msg.includes('does not exist')) { this.useDbFilterCache = false try { localStorage.setItem('disableDbFilterCache', '1') } catch (_) {} } return null } } /** * 保存筛选选项到数据库 */ async saveFilterOptionsToDb(names, assets, models, strategies) { if (!this.useDbFilterCache) return try { await supabase .from('filter_options') .upsert({ id: 'latest', names, assets, models, strategies, updated_at: new Date().toISOString() }, { onConflict: 'id' }) } catch (e) { const msg = e && e.message ? String(e.message).toLowerCase() : '' if (msg.includes('not found') || msg.includes('does not exist')) { this.useDbFilterCache = false try { localStorage.setItem('disableDbFilterCache', '1') } catch (_) {} } } } /** * 从 Supabase 拉取所有数据 */ async _fetchAllFromRemote() { const pageSize = 1000 let from = 0 const all = [] while (true) { const to = from + pageSize - 1 const { data, error } = await supabase .from('trading_decisions') .select('id, agent_name, asset, model, date, price, recommended_action, news_count, sentiment, created_at, updated_at') .order('updated_at', { ascending: false }) .range(from, to) if (error) { console.error('[DataService] Error fetching data:', error) break } all.push(...(data || [])) if (!data || data.length < pageSize) break from += pageSize } return all } /** * 计算单个分组的指标 */ async _computeAgentMetrics(row, allDecisions) { try { // 构建完整时间序列 const seq = allDecisions .filter(r => r.agent_name === row.agent_name && r.asset === row.asset && r.model === row.model) .sort((a, b) => (a.date > b.date ? 1 : -1)) const isCrypto = row.asset === 'BTC' || row.asset === 'ETH' const seqFiltered = isCrypto ? seq : (await filterRowsToNyseTradingDays(seq)) // 计算日期范围 const dates = seqFiltered.map(s => s.date).filter(Boolean).sort() const start_date = dates[0] || '-' const end_date = dates[dates.length - 1] || '-' let closed_days = 0 if (dates.length > 1) { closed_days = await countNonTradingDaysBetweenForAsset(row.asset, start_date, end_date) } const trading_days = await countTradingDaysBetweenForAsset(row.asset, start_date, end_date) // 为每个策略计算指标 const results = [] for (const s of STRATEGIES) { const st = computeStrategyEquity(seqFiltered, 100000, s.fee, s.strategy, s.tradingMode) const stNoFee = computeStrategyEquity(seqFiltered, 100000, 0, s.strategy, s.tradingMode) const metrics = calculateMetricsFromSeries(st, isCrypto ? 'crypto' : 'stock') const metricsNoFee = calculateMetricsFromSeries(stNoFee, isCrypto ? 'crypto' : 'stock') const { winRate, trades } = computeWinRate(seqFiltered, s.strategy, s.tradingMode) const bhSeries = computeBuyHoldEquity(seqFiltered, 100000) const buy_hold_return = bhSeries.length ? (bhSeries[bhSeries.length - 1] - bhSeries[0]) / bhSeries[0] : 0 results.push({ decision_ids: seqFiltered.map(r => r.id).filter(Boolean), agent_name: row.agent_name, asset: row.asset, model: row.model, strategy: s.id, strategy_label: s.label, balance: st.length ? st[st.length - 1] : 100000, return_with_fees: metrics.total_return / 100, return_no_fees: metricsNoFee.total_return / 100, buy_hold_return, sharpe_ratio: metrics.sharpe_ratio, win_rate: winRate, trades, start_date, end_date, closed_days, trading_days, fee: s.fee, tradingMode: s.tradingMode, series: seqFiltered.map(r => ({ id: r.id, date: r.date, price: r.price, recommended_action: r.recommended_action })), key: `${row.agent_name}|${row.asset}|${row.model}|${s.id}` }) } return results } catch (e) { console.error('[DataService] Error computing metrics:', e) return [] } } /** * 加载数据(主方法) */ async load(forceRefresh = false) { if (this.loading) { console.log('[DataService] Already loading, skipping...') return } this.loading = true this._notify() try { // 1. 尝试从数据库加载筛选选项 await this.loadFilterOptionsFromDb() // 2. 获取所有决策数据 let all = null if (forceRefresh) { // 强制刷新:清除所有缓存,从远程拉取 await clearAllStores() clearAllDecisions() all = await this._fetchAllFromRemote() setAllDecisions(all) await writeRawDecisions(all) } else { // 正常加载:先尝试内存缓存 all = getAllDecisions() if (!all) { // 再尝试 IndexedDB 缓存 const cached = await readAllRawDecisions() if (cached && cached.length) { all = cached setAllDecisions(all) } } if (!all) { // 最后从远程拉取 all = await this._fetchAllFromRemote() setAllDecisions(all) await writeRawDecisions(all) } } // 3. 计算全局日期范围 const allDates = all.map(r => r && r.date).filter(Boolean).sort() this.dateBounds = { min: allDates.length ? new Date(allDates[0]) : null, max: allDates.length ? new Date(allDates[allDates.length - 1]) : null } // 4. 按 agent_name|asset|model 分组去重 const keyToRow = new Map() for (const row of all) { const key = `${row.agent_name}|${row.asset}|${row.model}` if (!keyToRow.has(key)) { keyToRow.set(key, row) } } // 5. 计算每个分组的指标 const agents = [] for (const row of keyToRow.values()) { const metrics = await this._computeAgentMetrics(row, all) agents.push(...metrics) } this.agents = agents // 6. 转换为表格行 this.tableRows = agents.map(a => ({ agent_name: a.agent_name, asset: a.asset, model: a.model, strategy: a.strategy, strategy_label: a.strategy_label, balance: a.balance, ret_with_fees: a.return_with_fees, ret_no_fees: a.return_no_fees, buy_hold: a.buy_hold_return, vs_bh_with_fees: a.return_with_fees - a.buy_hold_return, sharpe: a.sharpe_ratio, trading_days: a.trading_days, win_rate: a.win_rate, trades: a.trades, start_date: a.start_date, end_date: a.end_date, closed_date: a.closed_days, decision_ids: a.decision_ids, fee: a.fee, tradingMode: a.tradingMode, series: a.series, key: a.key })) // 7. 生成筛选选项 this.nameOptions = Array.from(new Set(agents.map(a => a.agent_name))) .map(v => ({ label: v, value: v })) this.assetOptions = Array.from(new Set(agents.map(a => a.asset))) .map(v => ({ label: v, value: v })) this.modelOptions = Array.from(new Set(agents.map(a => a.model))) .map(v => ({ label: v, value: v })) this.strategyOptions = STRATEGIES.map(s => ({ label: s.label, value: s.id })) // 8. 保存筛选选项到数据库 await this.saveFilterOptionsToDb( this.nameOptions.map(o => o.value), this.assetOptions.map(o => o.value), this.modelOptions.map(o => o.value), this.strategyOptions.map(o => o.value) ) this.lastUpdated = Date.now() this.loaded = true console.log('[DataService] Data loaded successfully:', { agents: this.agents.length, tableRows: this.tableRows.length, names: this.nameOptions.length, assets: this.assetOptions.length }) } catch (e) { console.error('[DataService] Error loading data:', e) } finally { this.loading = false this._notify() } } /** * 强制刷新数据 */ async forceRefresh() { return this.load(true) } /** * 获取当前状态 */ getState() { return { loading: this.loading, loaded: this.loaded, agents: this.agents, tableRows: this.tableRows, lastUpdated: this.lastUpdated, dateBounds: this.dateBounds, nameOptions: this.nameOptions, assetOptions: this.assetOptions, modelOptions: this.modelOptions, strategyOptions: this.strategyOptions } } } // 导出单例 export const dataService = new DataService()