feat: Crypto.com-Client und Backfill-Script
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
21
src/server/market/cryptocom.test.ts
Normal file
21
src/server/market/cryptocom.test.ts
Normal file
@@ -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([]);
|
||||||
|
});
|
||||||
35
src/server/market/cryptocom.ts
Normal file
35
src/server/market/cryptocom.ts
Normal file
@@ -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<Candle[]> {
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
34
src/server/scripts/backfill.ts
Normal file
34
src/server/scripts/backfill.ts
Normal file
@@ -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();
|
||||||
Reference in New Issue
Block a user