messy work. backtests / mock-data

This commit is contained in:
Boki 2025-07-03 08:37:23 -04:00
parent 4e4a048988
commit fa70ada2bb
51 changed files with 2576 additions and 887 deletions

View file

@ -31,7 +31,7 @@ const app = new ServiceApplication(
enableHandlers: false, // Web API doesn't use handlers
enableScheduledJobs: false, // Web API doesn't use scheduled jobs
corsConfig: {
origin: ['http://localhost:4200', 'http://localhost:3000', 'http://localhost:3002'],
origin: ['http://localhost:4200', 'http://localhost:3000', 'http://localhost:3002', 'http://localhost:5173', 'http://localhost:5174'],
allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowHeaders: ['Content-Type', 'Authorization'],
credentials: true,

View file

@ -0,0 +1,97 @@
import { Hono } from 'hono';
import type { IServiceContainer } from '@stock-bot/handlers';
import { BacktestService } from '../services/backtest.service';
import { getLogger } from '@stock-bot/logger';
const logger = getLogger('backtest-routes');
export function createBacktestRoutes(container: IServiceContainer) {
const backtestRoutes = new Hono();
const backtestService = new BacktestService();
// Create a new backtest
backtestRoutes.post('/api/backtests', async (c) => {
try {
const body = await c.req.json();
// Validate required fields
if (!body.strategy || !body.symbols || !body.startDate || !body.endDate) {
return c.json({
error: 'Missing required fields: strategy, symbols, startDate, endDate'
}, 400);
}
const backtest = await backtestService.createBacktest({
...body,
initialCapital: body.initialCapital || 100000,
});
return c.json(backtest, 201);
} catch (error) {
logger.error('Failed to create backtest', { error });
return c.json({ error: 'Failed to create backtest' }, 500);
}
});
// Get backtest status
backtestRoutes.get('/api/backtests/:id', async (c) => {
try {
const id = c.req.param('id');
const backtest = await backtestService.getBacktest(id);
if (!backtest) {
return c.json({ error: 'Backtest not found' }, 404);
}
return c.json(backtest);
} catch (error) {
logger.error('Failed to get backtest', { error });
return c.json({ error: 'Failed to get backtest' }, 500);
}
});
// Get backtest results
backtestRoutes.get('/api/backtests/:id/results', async (c) => {
try {
const id = c.req.param('id');
const results = await backtestService.getBacktestResults(id);
if (!results) {
return c.json({ error: 'Results not found' }, 404);
}
return c.json(results);
} catch (error) {
logger.error('Failed to get results', { error });
return c.json({ error: 'Failed to get results' }, 500);
}
});
// List all backtests
backtestRoutes.get('/api/backtests', async (c) => {
try {
const limit = Number(c.req.query('limit')) || 50;
const offset = Number(c.req.query('offset')) || 0;
const backtests = await backtestService.listBacktests({ limit, offset });
return c.json(backtests);
} catch (error) {
logger.error('Failed to list backtests', { error });
return c.json({ error: 'Failed to list backtests' }, 500);
}
});
// Cancel a backtest
backtestRoutes.post('/api/backtests/:id/cancel', async (c) => {
try {
const id = c.req.param('id');
await backtestService.cancelBacktest(id);
return c.json({ message: 'Backtest cancelled' });
} catch (error) {
logger.error('Failed to cancel backtest', { error });
return c.json({ error: 'Failed to cancel backtest' }, 500);
}
});
return backtestRoutes;
}

View file

@ -9,6 +9,7 @@ import { createExchangeRoutes } from './exchange.routes';
import { createHealthRoutes } from './health.routes';
import { createMonitoringRoutes } from './monitoring.routes';
import { createPipelineRoutes } from './pipeline.routes';
import { createBacktestRoutes } from './backtest.routes';
export function createRoutes(container: IServiceContainer): Hono {
const app = new Hono();
@ -18,12 +19,14 @@ export function createRoutes(container: IServiceContainer): Hono {
const exchangeRoutes = createExchangeRoutes(container);
const monitoringRoutes = createMonitoringRoutes(container);
const pipelineRoutes = createPipelineRoutes(container);
const backtestRoutes = createBacktestRoutes(container);
// Mount routes
app.route('/health', healthRoutes);
app.route('/api/exchanges', exchangeRoutes);
app.route('/api/system/monitoring', monitoringRoutes);
app.route('/api/pipeline', pipelineRoutes);
app.route('/', backtestRoutes); // Mounted at root since routes already have /api prefix
return app;
}

View file

@ -0,0 +1,174 @@
import { v4 as uuidv4 } from 'uuid';
import { getLogger } from '@stock-bot/logger';
const logger = getLogger('backtest-service');
// Use environment variable or default
const ORCHESTRATOR_URL = process.env.ORCHESTRATOR_URL || 'http://localhost:2004';
export interface BacktestRequest {
strategy: string;
symbols: string[];
startDate: string;
endDate: string;
initialCapital: number;
config?: Record<string, any>;
}
export interface BacktestJob {
id: string;
status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
strategy: string;
symbols: string[];
startDate: Date;
endDate: Date;
initialCapital: number;
config: Record<string, any>;
createdAt: Date;
updatedAt: Date;
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 {
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(),
};
backtestStore.set(backtestId, backtest);
// Call orchestrator to run backtest
try {
const response = await fetch(`${ORCHESTRATOR_URL}/api/backtest/run`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
mode: 'backtest',
startDate: new Date(request.startDate).toISOString(),
endDate: new Date(request.endDate).toISOString(),
symbols: request.symbols,
initialCapital: request.initialCapital,
dataFrequency: '1d', // Default to daily
speed: 'max', // Default speed
fillModel: {
slippage: 'realistic',
marketImpact: true,
partialFills: true
}
}),
});
if (!response.ok) {
throw new Error(`Orchestrator returned ${response.status}`);
}
const result = await response.json();
// Store result when available
if (result.performance) {
// Backtest completed immediately
backtest.status = 'completed';
backtestStore.set(backtestId, backtest);
backtestResults.set(backtestId, result);
} else {
// Update status to running if not completed
backtest.status = 'running';
backtestStore.set(backtestId, backtest);
}
logger.info('Backtest started in orchestrator', { backtestId, result });
} catch (error) {
logger.error('Failed to start backtest in orchestrator', { backtestId, error });
backtest.status = 'failed';
backtest.error = error.message;
}
return backtest;
}
async getBacktest(id: string): Promise<BacktestJob | null> {
return backtestStore.get(id) || null;
}
async getBacktestResults(id: string): Promise<any> {
const results = backtestResults.get(id);
if (!results) return null;
// Transform orchestrator response to frontend expected format
return {
backtestId: results.id || id,
metrics: {
totalReturn: results.performance?.totalReturn || 0,
sharpeRatio: results.performance?.sharpeRatio || 0,
maxDrawdown: results.performance?.maxDrawdown || 0,
winRate: results.performance?.winRate || 0,
totalTrades: results.performance?.totalTrades || 0,
profitFactor: results.performance?.profitFactor
},
equity: results.equityCurve?.map((point: any) => ({
date: new Date(point.timestamp).toISOString(),
value: point.value
})) || [],
trades: results.trades?.map((trade: any) => ({
symbol: trade.symbol,
entryDate: new Date(trade.entryTime).toISOString(),
exitDate: new Date(trade.exitTime).toISOString(),
entryPrice: trade.entryPrice,
exitPrice: trade.exitPrice,
quantity: trade.quantity,
pnl: trade.pnl
})) || [],
ohlcData: results.ohlcData || {}
};
}
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);
}
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);
}
}
async cancelBacktest(id: string): Promise<void> {
await this.updateBacktestStatus(id, 'cancelled');
// Call orchestrator to stop backtest
try {
await fetch(`${ORCHESTRATOR_URL}/api/backtest/stop`, {
method: 'POST',
});
} catch (error) {
logger.error('Failed to stop backtest in orchestrator', { backtestId: id, error });
}
}
}