learning_ai_invt_trdg/backend/src/utils/cryptoMarketData.ts

108 lines
3.5 KiB
TypeScript

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<Candle[]> {
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;
}