adding backtest table / pages
This commit is contained in:
parent
38a6e73ad5
commit
a876f3c35b
19 changed files with 1058 additions and 69 deletions
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
70
apps/stock/web-api/src/migrations/migration-runner.ts
Normal file
70
apps/stock/web-api/src/migrations/migration-runner.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue