import type { Candle } from '../connectors/types.js'; function normalizeCryptoProductId(symbol: string): string { const upper = String(symbol || '').trim().toUpperCase(); if (!upper) return upper; if (upper.includes('/')) return upper.replace('/', '-'); if (upper.endsWith('USDT')) return `${upper.slice(0, -4)}-USDT`; if (upper.endsWith('USD')) return `${upper.slice(0, -3)}-USD`; return upper; } function toCoinbaseGranularity(timeframe: string): number { const normalized = String(timeframe || '').trim().toLowerCase(); switch (normalized) { case '1m': case '1min': return 60; case '5m': case '5min': return 300; case '15m': case '15min': return 900; case '1h': case '1hour': return 3600; case '4h': case '4hour': return 3600; case '1d': case '1day': return 86400; case '1w': case '1week': return 86400; case '1month': return 86400; default: return 3600; } } function aggregateBars(candles: Candle[], factor: number): Candle[] { const aggregated: Candle[] = []; for (let i = 0; i < candles.length; i += factor) { const chunk = candles.slice(i, i + factor); if (!chunk.length) continue; aggregated.push({ timestamp: chunk[0].timestamp, open: chunk[0].open, high: Math.max(...chunk.map((c) => c.high)), low: Math.min(...chunk.map((c) => c.low)), close: chunk[chunk.length - 1].close, volume: chunk.reduce((sum, c) => sum + c.volume, 0), }); } return aggregated; } export async function fetchCoinbaseCryptoCandles(params: { symbol: string; timeframe: string; start?: string; end?: string; limit?: number; }): Promise { const productId = normalizeCryptoProductId(params.symbol); const granularity = toCoinbaseGranularity(params.timeframe); const query = new URLSearchParams({ granularity: String(granularity), }); if (params.start) query.set('start', params.start); if (params.end) query.set('end', params.end); const response = await fetch(`https://api.exchange.coinbase.com/products/${encodeURIComponent(productId)}/candles?${query.toString()}`); if (!response.ok) { const txt = await response.text().catch(() => ''); throw new Error(`Coinbase candles fetch failed: ${response.status} ${txt || response.statusText}`); } const payload = await response.json() as any[]; const candles: Candle[] = Array.isArray(payload) ? payload.map((row: any) => ({ timestamp: Number(row[0]) * 1000, low: Number(row[1]), high: Number(row[2]), open: Number(row[3]), close: Number(row[4]), volume: Number(row[5] || 0), })) : []; const sorted = candles .filter((c) => Number.isFinite(c.timestamp) && Number.isFinite(c.open) && Number.isFinite(c.high) && Number.isFinite(c.low) && Number.isFinite(c.close)) .sort((a, b) => a.timestamp - b.timestamp); const normalizedTimeframe = String(params.timeframe || '').trim().toLowerCase(); const aggregated = normalizedTimeframe === '4h' || normalizedTimeframe === '4hour' ? aggregateBars(sorted, 4) : sorted; if (params.limit && params.limit > 0 && aggregated.length > params.limit) { return aggregated.slice(-params.limit); } return aggregated; }