From 27a10dc7943d6b828546646ac97d65c320fb5d78 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 9 Jun 2026 20:56:24 +0000 Subject: [PATCH] feat: DB-Schema (candles, backtest_runs), Migration, CandleStore --- drizzle.config.ts | 8 ++ drizzle/0000_nifty_brood.sql | 18 +++++ drizzle/meta/0000_snapshot.json | 126 ++++++++++++++++++++++++++++++ drizzle/meta/_journal.json | 13 +++ src/server/config.ts | 7 ++ src/server/db/client.ts | 7 ++ src/server/db/migrate.ts | 6 ++ src/server/db/schema.ts | 23 ++++++ src/server/market/candle-store.ts | 35 +++++++++ 9 files changed, 243 insertions(+) create mode 100644 drizzle.config.ts create mode 100644 drizzle/0000_nifty_brood.sql create mode 100644 drizzle/meta/0000_snapshot.json create mode 100644 drizzle/meta/_journal.json create mode 100644 src/server/config.ts create mode 100644 src/server/db/client.ts create mode 100644 src/server/db/migrate.ts create mode 100644 src/server/db/schema.ts create mode 100644 src/server/market/candle-store.ts diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..696f472 --- /dev/null +++ b/drizzle.config.ts @@ -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! }, +}); diff --git a/drizzle/0000_nifty_brood.sql b/drizzle/0000_nifty_brood.sql new file mode 100644 index 0000000..4e87a08 --- /dev/null +++ b/drizzle/0000_nifty_brood.sql @@ -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") +); diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..ad04ed8 --- /dev/null +++ b/drizzle/meta/0000_snapshot.json @@ -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": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json new file mode 100644 index 0000000..aefe02e --- /dev/null +++ b/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1781038570957, + "tag": "0000_nifty_brood", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/src/server/config.ts b/src/server/config.ts new file mode 100644 index 0000000..f009788 --- /dev/null +++ b/src/server/config.ts @@ -0,0 +1,7 @@ +import { z } from 'zod'; + +const Env = z.object({ + DATABASE_URL: z.string().url(), +}); + +export const env = Env.parse(process.env); diff --git a/src/server/db/client.ts b/src/server/db/client.ts new file mode 100644 index 0000000..8a79555 --- /dev/null +++ b/src/server/db/client.ts @@ -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 }); diff --git a/src/server/db/migrate.ts b/src/server/db/migrate.ts new file mode 100644 index 0000000..cbc90a9 --- /dev/null +++ b/src/server/db/migrate.ts @@ -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(); diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts new file mode 100644 index 0000000..b534cde --- /dev/null +++ b/src/server/db/schema.ts @@ -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(), +}); diff --git a/src/server/market/candle-store.ts b/src/server/market/candle-store.ts new file mode 100644 index 0000000..2981132 --- /dev/null +++ b/src/server/market/candle-store.ts @@ -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 { + 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 { + 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) }; +}