feat: DB-Schema (candles, backtest_runs), Migration, CandleStore
This commit is contained in:
8
drizzle.config.ts
Normal file
8
drizzle.config.ts
Normal 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! },
|
||||||
|
});
|
||||||
18
drizzle/0000_nifty_brood.sql
Normal file
18
drizzle/0000_nifty_brood.sql
Normal 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")
|
||||||
|
);
|
||||||
126
drizzle/meta/0000_snapshot.json
Normal file
126
drizzle/meta/0000_snapshot.json
Normal 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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
drizzle/meta/_journal.json
Normal file
13
drizzle/meta/_journal.json
Normal 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
7
src/server/config.ts
Normal 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
7
src/server/db/client.ts
Normal 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
6
src/server/db/migrate.ts
Normal 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
23
src/server/db/schema.ts
Normal 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(),
|
||||||
|
});
|
||||||
35
src/server/market/candle-store.ts
Normal file
35
src/server/market/candle-store.ts
Normal 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) };
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user