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

8
drizzle.config.ts Normal file
View File

@@ -0,0 +1,8 @@
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
dialect: 'postgresql',
schema: './src/server/db/schema.ts',
out: './drizzle',
dbCredentials: { url: process.env.DATABASE_URL! },
});

View File

@@ -0,0 +1,18 @@
CREATE TABLE "backtest_runs" (
"id" serial PRIMARY KEY NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"kind" text NOT NULL,
"config" jsonb NOT NULL,
"result" jsonb NOT NULL
);
--> statement-breakpoint
CREATE TABLE "candles" (
"pair" varchar(16) NOT NULL,
"ts" timestamp with time zone NOT NULL,
"open" double precision NOT NULL,
"high" double precision NOT NULL,
"low" double precision NOT NULL,
"close" double precision NOT NULL,
"volume" double precision NOT NULL,
CONSTRAINT "candles_pair_ts_pk" PRIMARY KEY("pair","ts")
);

View File

@@ -0,0 +1,126 @@
{
"id": "00b411bc-669e-4667-881c-c9161fa42bb0",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.backtest_runs": {
"name": "backtest_runs",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"kind": {
"name": "kind",
"type": "text",
"primaryKey": false,
"notNull": true
},
"config": {
"name": "config",
"type": "jsonb",
"primaryKey": false,
"notNull": true
},
"result": {
"name": "result",
"type": "jsonb",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.candles": {
"name": "candles",
"schema": "",
"columns": {
"pair": {
"name": "pair",
"type": "varchar(16)",
"primaryKey": false,
"notNull": true
},
"ts": {
"name": "ts",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
},
"open": {
"name": "open",
"type": "double precision",
"primaryKey": false,
"notNull": true
},
"high": {
"name": "high",
"type": "double precision",
"primaryKey": false,
"notNull": true
},
"low": {
"name": "low",
"type": "double precision",
"primaryKey": false,
"notNull": true
},
"close": {
"name": "close",
"type": "double precision",
"primaryKey": false,
"notNull": true
},
"volume": {
"name": "volume",
"type": "double precision",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"candles_pair_ts_pk": {
"name": "candles_pair_ts_pk",
"columns": [
"pair",
"ts"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1781038570957,
"tag": "0000_nifty_brood",
"breakpoints": true
}
]
}

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) };
}