adding backtest table / pages

This commit is contained in:
Boki 2025-07-04 14:27:34 -04:00
parent 38a6e73ad5
commit a876f3c35b
19 changed files with 1058 additions and 69 deletions

View file

@ -71,6 +71,12 @@ async function createContainer(config: any) {
})
.build(); // This automatically initializes services
// Run database migrations
if (container.postgres) {
const { runMigrations } = await import('./migrations/migration-runner');
await runMigrations(container);
}
return container;
}

View file

@ -0,0 +1,70 @@
import { readFileSync, readdirSync } from 'fs';
import { join } from 'path';
import type { IServiceContainer } from '@stock-bot/handlers';
import { getLogger } from '@stock-bot/logger';
const logger = getLogger('migration-runner');
export async function runMigrations(container: IServiceContainer): Promise<void> {
logger.info('Migration runner called');
logger.info('Container type:', typeof container);
logger.info('Container postgres available:', !!container.postgres);
if (!container.postgres) {
logger.warn('PostgreSQL not available, skipping migrations');
logger.info('Container keys:', Object.keys(container));
return;
}
try {
// Create migrations table if it doesn't exist
await container.postgres.query(`
CREATE TABLE IF NOT EXISTS migrations (
id SERIAL PRIMARY KEY,
filename VARCHAR(255) NOT NULL UNIQUE,
executed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
)
`);
// Get list of migration files from database/postgres/init
const migrationsDir = join(process.cwd(), 'database', 'postgres', 'init');
logger.info('Looking for migrations in:', migrationsDir);
const files = readdirSync(migrationsDir)
.filter(f => f.endsWith('.sql') && f.startsWith('001_')) // Only run our backtest migration for now
.sort();
logger.info('Found migration files:', files);
for (const file of files) {
// Check if migration has already been run
const result = await container.postgres.query(
'SELECT 1 FROM migrations WHERE filename = $1',
[file]
);
if (result.rows.length === 0) {
logger.info(`Running migration: ${file}`);
// Read and execute migration
const sql = readFileSync(join(migrationsDir, file), 'utf8');
await container.postgres.query(sql);
// Record migration as executed
await container.postgres.query(
'INSERT INTO migrations (filename) VALUES ($1)',
[file]
);
logger.info(`Migration completed: ${file}`);
} else {
logger.debug(`Migration already executed: ${file}`);
}
}
logger.info('All migrations completed successfully');
} catch (error) {
logger.error('Migration failed', { error });
throw error;
}
}

View file

@ -7,7 +7,7 @@ const logger = getLogger('backtest-routes');
export function createBacktestRoutes(container: IServiceContainer) {
const backtestRoutes = new Hono();
const backtestService = new BacktestService();
const backtestService = new BacktestService(container);
// Create a new backtest
backtestRoutes.post('/api/backtests', async (c) => {

View file

@ -1,5 +1,6 @@
import { v4 as uuidv4 } from 'uuid';
import { getLogger } from '@stock-bot/logger';
import type { IServiceContainer } from '@stock-bot/handlers';
const logger = getLogger('backtest-service');
@ -29,29 +30,49 @@ export interface BacktestJob {
error?: string;
}
// In-memory storage for demo (replace with database)
const backtestStore = new Map<string, BacktestJob>();
const backtestResults = new Map<string, any>();
export class BacktestService {
private container: IServiceContainer;
constructor(container: IServiceContainer) {
this.container = container;
logger.info('BacktestService initialized', {
hasPostgres: !!container.postgres,
containerKeys: Object.keys(container)
});
}
async createBacktest(request: BacktestRequest): Promise<BacktestJob> {
const backtestId = uuidv4();
// Store in memory (replace with database)
const backtest: BacktestJob = {
id: backtestId,
status: 'pending',
strategy: request.strategy,
symbols: request.symbols,
startDate: new Date(request.startDate),
endDate: new Date(request.endDate),
initialCapital: request.initialCapital,
config: request.config || {},
createdAt: new Date(),
updatedAt: new Date(),
};
// Insert into PostgreSQL
const result = await this.container.postgres.query(
`INSERT INTO backtests
(id, name, strategy, symbols, start_date, end_date, initial_capital, config, status)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'pending')
RETURNING *`,
[
backtestId,
request.config?.name || `Backtest ${new Date().toISOString()}`,
request.strategy,
JSON.stringify(request.symbols),
request.startDate,
request.endDate,
request.initialCapital,
JSON.stringify(request.config || {})
]
);
backtestStore.set(backtestId, backtest);
const backtest: BacktestJob = {
id: result.rows[0].id,
status: result.rows[0].status,
strategy: result.rows[0].strategy,
symbols: result.rows[0].symbols,
startDate: result.rows[0].start_date,
endDate: result.rows[0].end_date,
initialCapital: parseFloat(result.rows[0].initial_capital),
config: result.rows[0].config,
createdAt: result.rows[0].created_at,
updatedAt: result.rows[0].updated_at,
};
// Call orchestrator to run backtest
try {
@ -87,10 +108,14 @@ export class BacktestService {
// Store result directly without transformation
if (result.status === 'completed') {
backtest.status = 'completed';
backtest.updatedAt = new Date();
backtestStore.set(backtestId, backtest);
backtestResults.set(backtestId, result);
// Update backtest status
await this.container.postgres.query(
'UPDATE backtests SET status = $1, updated_at = NOW() WHERE id = $2',
['completed', backtestId]
);
// Store results in backtest_results table
await this.saveBacktestResults(backtestId, result);
logger.info('Backtest completed', {
backtestId,
@ -99,14 +124,19 @@ export class BacktestService {
});
} else {
// Update status to running if not completed
backtest.status = 'running';
backtest.updatedAt = new Date();
backtestStore.set(backtestId, backtest);
await this.container.postgres.query(
'UPDATE backtests SET status = $1, updated_at = NOW() WHERE id = $2',
['running', backtestId]
);
}
logger.info('Backtest started in orchestrator', { backtestId, result });
} catch (error) {
logger.error('Failed to start backtest in orchestrator', { backtestId, error });
await this.container.postgres.query(
'UPDATE backtests SET status = $1, error = $2, updated_at = NOW() WHERE id = $3',
['failed', error.message, backtestId]
);
backtest.status = 'failed';
backtest.error = error.message;
}
@ -115,30 +145,113 @@ export class BacktestService {
}
async getBacktest(id: string): Promise<BacktestJob | null> {
return backtestStore.get(id) || null;
const result = await this.container.postgres.query(
'SELECT * FROM backtests WHERE id = $1',
[id]
);
if (result.rows.length === 0) {
return null;
}
const row = result.rows[0];
return {
id: row.id,
status: row.status,
strategy: row.strategy,
symbols: row.symbols,
startDate: row.start_date,
endDate: row.end_date,
initialCapital: parseFloat(row.initial_capital),
config: row.config,
createdAt: row.created_at,
updatedAt: row.updated_at,
error: row.error,
};
}
async getBacktestResults(id: string): Promise<any> {
// Return results directly without any transformation
return backtestResults.get(id) || null;
const result = await this.container.postgres.query(
`SELECT
br.*,
b.strategy,
b.symbols,
b.start_date,
b.end_date,
b.initial_capital,
b.config as backtest_config
FROM backtest_results br
JOIN backtests b ON b.id = br.backtest_id
WHERE br.backtest_id = $1`,
[id]
);
if (result.rows.length === 0) {
return null;
}
const row = result.rows[0];
// Reconstruct the result format expected by frontend
return {
backtestId: row.backtest_id,
status: 'completed',
completedAt: row.completed_at.toISOString(),
config: {
name: row.backtest_config?.name || 'Backtest',
strategy: row.strategy,
symbols: row.symbols,
startDate: row.start_date.toISOString(),
endDate: row.end_date.toISOString(),
initialCapital: parseFloat(row.initial_capital),
commission: row.backtest_config?.commission ?? 0.001,
slippage: row.backtest_config?.slippage ?? 0.0001,
dataFrequency: row.backtest_config?.dataFrequency || '1d',
},
metrics: row.metrics,
equity: row.equity_curve,
ohlcData: row.ohlc_data,
trades: row.trades,
positions: row.positions,
analytics: row.analytics,
executionTime: row.execution_time,
};
}
async listBacktests(params: { limit: number; offset: number }): Promise<BacktestJob[]> {
const all = Array.from(backtestStore.values());
return all
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
.slice(params.offset, params.offset + params.limit);
const result = await this.container.postgres.query(
`SELECT * FROM backtests
ORDER BY created_at DESC
LIMIT $1 OFFSET $2`,
[params.limit, params.offset]
);
return result.rows.map(row => ({
id: row.id,
status: row.status,
strategy: row.strategy,
symbols: row.symbols,
startDate: row.start_date,
endDate: row.end_date,
initialCapital: parseFloat(row.initial_capital),
config: row.config,
createdAt: row.created_at,
updatedAt: row.updated_at,
error: row.error,
}));
}
async updateBacktestStatus(id: string, status: BacktestJob['status'], error?: string): Promise<void> {
const backtest = backtestStore.get(id);
if (backtest) {
backtest.status = status;
backtest.updatedAt = new Date();
if (error) {
backtest.error = error;
}
backtestStore.set(id, backtest);
if (error) {
await this.container.postgres.query(
'UPDATE backtests SET status = $1, error = $2, updated_at = NOW() WHERE id = $3',
[status, error, id]
);
} else {
await this.container.postgres.query(
'UPDATE backtests SET status = $1, updated_at = NOW() WHERE id = $2',
[status, id]
);
}
}
@ -154,4 +267,28 @@ export class BacktestService {
logger.error('Failed to stop backtest in orchestrator', { backtestId: id, error });
}
}
private async saveBacktestResults(backtestId: string, result: any): Promise<void> {
try {
await this.container.postgres.query(
`INSERT INTO backtest_results
(backtest_id, completed_at, metrics, equity_curve, ohlc_data, trades, positions, analytics, execution_time)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
[
backtestId,
result.completedAt || new Date(),
JSON.stringify(result.metrics || {}),
JSON.stringify(result.equity || result.equityCurve || []),
JSON.stringify(result.ohlcData || {}),
JSON.stringify(result.trades || []),
JSON.stringify(result.positions || result.finalPositions || {}),
JSON.stringify(result.analytics || {}),
result.executionTime || 0
]
);
} catch (error) {
logger.error('Failed to save backtest results', { backtestId, error });
throw error;
}
}
}