diff --git a/src/server/market/cryptocom.test.ts b/src/server/market/cryptocom.test.ts new file mode 100644 index 0000000..a6da516 --- /dev/null +++ b/src/server/market/cryptocom.test.ts @@ -0,0 +1,21 @@ +import { expect, test } from 'bun:test'; +import { parseCandlestickResponse } from './cryptocom'; + +test('parst Crypto.com-Candlestick-Response', () => { + const json = { + result: { + data: [ + { t: 900000, o: '1.0', h: '1.2', l: '0.9', c: '1.1', v: '1000' }, + { t: 1800000, o: '1.1', h: '1.3', l: '1.0', c: '1.2', v: '500' }, + ], + }, + }; + const out = parseCandlestickResponse(json); + expect(out).toHaveLength(2); + expect(out[0]).toEqual({ ts: 900000, open: 1, high: 1.2, low: 0.9, close: 1.1, volume: 1000 }); +}); + +test('leere/fehlende Daten → leeres Array', () => { + expect(parseCandlestickResponse({})).toEqual([]); + expect(parseCandlestickResponse({ result: {} })).toEqual([]); +}); diff --git a/src/server/market/cryptocom.ts b/src/server/market/cryptocom.ts new file mode 100644 index 0000000..909fb23 --- /dev/null +++ b/src/server/market/cryptocom.ts @@ -0,0 +1,35 @@ +import type { Candle, Pair } from '../types'; + +const BASE = 'https://api.crypto.com/exchange/v1'; + +export function parseCandlestickResponse(json: any): Candle[] { + const data = json?.result?.data; + if (!Array.isArray(data)) return []; + return data.map((d: any) => ({ + ts: Number(d.t), + open: Number(d.o), + high: Number(d.h), + low: Number(d.l), + close: Number(d.c), + volume: Number(d.v), + })); +} + +export async function fetchCandles(pair: Pair, timeframe: string, count: number, endTs?: number): Promise { + const url = new URL(`${BASE}/public/get-candlestick`); + url.searchParams.set('instrument_name', pair); + url.searchParams.set('timeframe', timeframe); + url.searchParams.set('count', String(count)); + if (endTs !== undefined) url.searchParams.set('end_ts', String(endTs)); + + for (let attempt = 1; ; attempt++) { + try { + const res = await fetch(url); + if (!res.ok) throw new Error(`HTTP ${res.status} für ${pair}`); + return parseCandlestickResponse(await res.json()); + } catch (err) { + if (attempt >= 3) throw err; + await Bun.sleep(1000 * 2 ** (attempt - 1)); + } + } +} diff --git a/src/server/scripts/backfill.ts b/src/server/scripts/backfill.ts new file mode 100644 index 0000000..8b19048 --- /dev/null +++ b/src/server/scripts/backfill.ts @@ -0,0 +1,34 @@ +import { PAIRS } from '../types'; +import { fetchCandles } from '../market/cryptocom'; +import { insertCandles, getCoverage } from '../market/candle-store'; +import { sql } from '../db/client'; + +const M15 = 15 * 60 * 1000; +const TARGET_MONTHS = 14; +const since = Date.now() - TARGET_MONTHS * 30 * 24 * 60 * 60 * 1000; + +for (const pair of PAIRS) { + let endTs: number | undefined = undefined; + let total = 0; + for (;;) { + const batch = await fetchCandles(pair, '15m', 300, endTs); + if (batch.length === 0) break; + + // Sanity: 15-min-Raster + for (const c of batch) { + if (c.ts % M15 !== 0) throw new Error(`${pair}: Timestamp ${c.ts} nicht im 15m-Raster — Konvention prüfen!`); + } + // Noch laufende Candle verwerfen + const closed = batch.filter((c) => c.ts + M15 <= Date.now()); + await insertCandles(pair, closed); + total += closed.length; + + const oldest = Math.min(...batch.map((c) => c.ts)); + if (oldest <= since) break; + endTs = oldest - 1; + await Bun.sleep(200); // Rate-Limit-Schonung + } + const cov = await getCoverage(pair); + console.log(`${pair}: +${total} Candles, Coverage ${cov.from?.toISOString()} → ${cov.to?.toISOString()} (${cov.count})`); +} +await sql.end();