messy work. backtests / mock-data
This commit is contained in:
parent
4e4a048988
commit
fa70ada2bb
51 changed files with 2576 additions and 887 deletions
|
|
@ -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,
|
||||
|
|
|
|||
97
apps/stock/web-api/src/routes/backtest.routes.ts
Normal file
97
apps/stock/web-api/src/routes/backtest.routes.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
174
apps/stock/web-api/src/services/backtest.service.ts
Normal file
174
apps/stock/web-api/src/services/backtest.service.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue