feat: DB-Schema (candles, backtest_runs), Migration, CandleStore

This commit is contained in:
2026-06-09 20:56:24 +00:00
parent f318446ebf
commit 27a10dc794
9 changed files with 243 additions and 0 deletions

7
src/server/config.ts Normal file
View File

@@ -0,0 +1,7 @@
import { z } from 'zod';
const Env = z.object({
DATABASE_URL: z.string().url(),
});
export const env = Env.parse(process.env);

7
src/server/db/client.ts Normal file
View File

@@ -0,0 +1,7 @@
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import { env } from '../config';
import * as schema from './schema';
export const sql = postgres(env.DATABASE_URL, { max: 5 });
export const db = drizzle(sql, { schema });

6
src/server/db/migrate.ts Normal file
View File

@@ -0,0 +1,6 @@
import { migrate } from 'drizzle-orm/postgres-js/migrator';
import { db, sql } from './client';
await migrate(db, { migrationsFolder: './drizzle' });
console.log('Migrations angewendet.');
await sql.end();

23
src/server/db/schema.ts Normal file
View File

@@ -0,0 +1,23 @@
import { doublePrecision, jsonb, pgTable, primaryKey, serial, text, timestamp, varchar } from 'drizzle-orm/pg-core';
export const candles = pgTable(
'candles',
{
pair: varchar('pair', { length: 16 }).notNull(),
ts: timestamp('ts', { withTimezone: true }).notNull(),
open: doublePrecision('open').notNull(),
high: doublePrecision('high').notNull(),
low: doublePrecision('low').notNull(),
close: doublePrecision('close').notNull(),
volume: doublePrecision('volume').notNull(),
},
(t) => [primaryKey({ columns: [t.pair, t.ts] })],
);
export const backtestRuns = pgTable('backtest_runs', {
id: serial('id').primaryKey(),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
kind: text('kind').notNull(), // 'single' | 'walkforward'
config: jsonb('config').notNull(),
result: jsonb('result').notNull(),
});

View File

@@ -0,0 +1,35 @@
import { and, asc, count, eq, gte, lt, max, min } from 'drizzle-orm';
import { db } from '../db/client';
import { candles } from '../db/schema';
import type { Candle, Pair } from '../types';
export async function insertCandles(pair: Pair, items: Candle[]): Promise<void> {
for (let i = 0; i < items.length; i += 1000) {
const chunk = items.slice(i, i + 1000).map((c) => ({
pair,
ts: new Date(c.ts),
open: c.open,
high: c.high,
low: c.low,
close: c.close,
volume: c.volume,
}));
await db.insert(candles).values(chunk).onConflictDoNothing();
}
}
export async function getCandles(pair: Pair, from?: number, to?: number): Promise<Candle[]> {
const conds = [eq(candles.pair, pair)];
if (from !== undefined) conds.push(gte(candles.ts, new Date(from)));
if (to !== undefined) conds.push(lt(candles.ts, new Date(to)));
const rows = await db.select().from(candles).where(and(...conds)).orderBy(asc(candles.ts));
return rows.map((r) => ({ ts: r.ts.getTime(), open: r.open, high: r.high, low: r.low, close: r.close, volume: r.volume }));
}
export async function getCoverage(pair: Pair): Promise<{ from: Date | null; to: Date | null; count: number }> {
const [row] = await db
.select({ from: min(candles.ts), to: max(candles.ts), count: count() })
.from(candles)
.where(eq(candles.pair, pair));
return { from: row.from, to: row.to, count: Number(row.count) };
}