added initial py analytics / rust core / ts orchestrator services

This commit is contained in:
Boki 2025-07-01 11:16:25 -04:00
parent 680b5fd2ae
commit c862ed496b
62 changed files with 13459 additions and 0 deletions

View file

@ -0,0 +1,591 @@
import { logger } from '@stock-bot/logger';
import * as stats from 'simple-statistics';
export interface Trade {
entryTime: Date;
exitTime: Date;
symbol: string;
side: 'long' | 'short';
entryPrice: number;
exitPrice: number;
quantity: number;
commission: number;
pnl: number;
returnPct: number;
holdingPeriod: number; // in minutes
mae: number; // Maximum Adverse Excursion
mfe: number; // Maximum Favorable Excursion
}
export interface PerformanceMetrics {
// Return metrics
totalReturn: number;
annualizedReturn: number;
cagr: number; // Compound Annual Growth Rate
// Risk metrics
volatility: number;
downVolatility: number;
maxDrawdown: number;
maxDrawdownDuration: number; // days
var95: number; // Value at Risk 95%
cvar95: number; // Conditional VaR 95%
// Risk-adjusted returns
sharpeRatio: number;
sortinoRatio: number;
calmarRatio: number;
informationRatio: number;
// Trade statistics
totalTrades: number;
winRate: number;
avgWin: number;
avgLoss: number;
avgWinLoss: number;
profitFactor: number;
expectancy: number;
payoffRatio: number;
// Trade analysis
avgHoldingPeriod: number;
avgTradesPerDay: number;
maxConsecutiveWins: number;
maxConsecutiveLosses: number;
largestWin: number;
largestLoss: number;
// Statistical measures
skewness: number;
kurtosis: number;
tailRatio: number;
// Kelly criterion
kellyFraction: number;
optimalLeverage: number;
}
export interface DrawdownAnalysis {
maxDrawdown: number;
maxDrawdownDuration: number;
currentDrawdown: number;
drawdownPeriods: Array<{
start: Date;
end: Date;
depth: number;
duration: number;
recovery: number;
}>;
underwaterCurve: Array<{ date: Date; drawdown: number }>;
}
export interface FactorAttribution {
alpha: number;
beta: number;
correlation: number;
treynorRatio: number;
trackingError: number;
upCapture: number;
downCapture: number;
}
export class PerformanceAnalyzer {
private equityCurve: Array<{ date: Date; value: number }> = [];
private trades: Trade[] = [];
private dailyReturns: number[] = [];
private benchmarkReturns?: number[];
constructor(private initialCapital: number = 100000) {}
addEquityPoint(date: Date, value: number): void {
this.equityCurve.push({ date, value });
this.calculateDailyReturns();
}
addTrade(trade: Trade): void {
this.trades.push(trade);
}
setBenchmark(returns: number[]): void {
this.benchmarkReturns = returns;
}
analyze(): PerformanceMetrics {
if (this.equityCurve.length < 2) {
return this.getEmptyMetrics();
}
// Calculate returns
const totalReturn = this.calculateTotalReturn();
const annualizedReturn = this.calculateAnnualizedReturn();
const cagr = this.calculateCAGR();
// Risk metrics
const volatility = this.calculateVolatility();
const downVolatility = this.calculateDownsideVolatility();
const drawdownAnalysis = this.analyzeDrawdowns();
const { var95, cvar95 } = this.calculateVaR();
// Risk-adjusted returns
const sharpeRatio = this.calculateSharpeRatio(annualizedReturn, volatility);
const sortinoRatio = this.calculateSortinoRatio(annualizedReturn, downVolatility);
const calmarRatio = annualizedReturn / Math.abs(drawdownAnalysis.maxDrawdown);
const informationRatio = this.calculateInformationRatio();
// Trade statistics
const tradeStats = this.analyzeTradeStatistics();
// Statistical measures
const { skewness, kurtosis } = this.calculateDistributionMetrics();
const tailRatio = this.calculateTailRatio();
// Kelly criterion
const { kellyFraction, optimalLeverage } = this.calculateKellyCriterion(tradeStats);
return {
totalReturn,
annualizedReturn,
cagr,
volatility,
downVolatility,
maxDrawdown: drawdownAnalysis.maxDrawdown,
maxDrawdownDuration: drawdownAnalysis.maxDrawdownDuration,
var95,
cvar95,
sharpeRatio,
sortinoRatio,
calmarRatio,
informationRatio,
...tradeStats,
skewness,
kurtosis,
tailRatio,
kellyFraction,
optimalLeverage
};
}
analyzeDrawdowns(): DrawdownAnalysis {
const drawdowns: number[] = [];
const underwaterCurve: Array<{ date: Date; drawdown: number }> = [];
let peak = this.equityCurve[0].value;
let maxDrawdown = 0;
let currentDrawdownStart: Date | null = null;
let drawdownPeriods: DrawdownAnalysis['drawdownPeriods'] = [];
for (let i = 0; i < this.equityCurve.length; i++) {
const point = this.equityCurve[i];
if (point.value > peak) {
// New peak - end current drawdown if any
if (currentDrawdownStart) {
const period = {
start: currentDrawdownStart,
end: point.date,
depth: maxDrawdown,
duration: this.daysBetween(currentDrawdownStart, point.date),
recovery: i
};
drawdownPeriods.push(period);
currentDrawdownStart = null;
}
peak = point.value;
}
const drawdown = (point.value - peak) / peak;
drawdowns.push(drawdown);
underwaterCurve.push({ date: point.date, drawdown });
if (drawdown < 0 && !currentDrawdownStart) {
currentDrawdownStart = point.date;
}
if (drawdown < maxDrawdown) {
maxDrawdown = drawdown;
}
}
// Handle ongoing drawdown
const currentDrawdown = drawdowns[drawdowns.length - 1];
// Calculate max drawdown duration
const maxDrawdownDuration = Math.max(
...drawdownPeriods.map(p => p.duration),
currentDrawdownStart ? this.daysBetween(currentDrawdownStart, new Date()) : 0
);
return {
maxDrawdown: Math.abs(maxDrawdown),
maxDrawdownDuration,
currentDrawdown: Math.abs(currentDrawdown),
drawdownPeriods,
underwaterCurve
};
}
calculateFactorAttribution(benchmarkReturns: number[]): FactorAttribution {
if (this.dailyReturns.length !== benchmarkReturns.length) {
throw new Error('Returns and benchmark must have same length');
}
// Calculate beta using linear regression
const regression = stats.linearRegression(
this.dailyReturns.map((r, i) => [benchmarkReturns[i], r])
);
const beta = regression.m;
const alpha = regression.b * 252; // Annualized
// Correlation
const correlation = stats.sampleCorrelation(this.dailyReturns, benchmarkReturns);
// Treynor ratio
const excessReturn = this.calculateAnnualizedReturn() - 0.02; // Assume 2% risk-free
const treynorRatio = beta !== 0 ? excessReturn / beta : 0;
// Tracking error
const returnDiffs = this.dailyReturns.map((r, i) => r - benchmarkReturns[i]);
const trackingError = stats.standardDeviation(returnDiffs) * Math.sqrt(252);
// Up/down capture
const upDays = benchmarkReturns
.map((r, i) => r > 0 ? { bench: r, port: this.dailyReturns[i] } : null)
.filter(d => d !== null) as Array<{ bench: number; port: number }>;
const downDays = benchmarkReturns
.map((r, i) => r < 0 ? { bench: r, port: this.dailyReturns[i] } : null)
.filter(d => d !== null) as Array<{ bench: number; port: number }>;
const upCapture = upDays.length > 0 ?
stats.mean(upDays.map(d => d.port)) / stats.mean(upDays.map(d => d.bench)) : 0;
const downCapture = downDays.length > 0 ?
stats.mean(downDays.map(d => d.port)) / stats.mean(downDays.map(d => d.bench)) : 0;
return {
alpha,
beta,
correlation,
treynorRatio,
trackingError,
upCapture,
downCapture
};
}
private calculateDailyReturns(): void {
this.dailyReturns = [];
for (let i = 1; i < this.equityCurve.length; i++) {
const prevValue = this.equityCurve[i - 1].value;
const currValue = this.equityCurve[i].value;
this.dailyReturns.push((currValue - prevValue) / prevValue);
}
}
private calculateTotalReturn(): number {
const finalValue = this.equityCurve[this.equityCurve.length - 1].value;
return ((finalValue - this.initialCapital) / this.initialCapital) * 100;
}
private calculateAnnualizedReturn(): number {
const totalReturn = this.calculateTotalReturn() / 100;
const years = this.getYears();
return (Math.pow(1 + totalReturn, 1 / years) - 1) * 100;
}
private calculateCAGR(): number {
const finalValue = this.equityCurve[this.equityCurve.length - 1].value;
const years = this.getYears();
return (Math.pow(finalValue / this.initialCapital, 1 / years) - 1) * 100;
}
private calculateVolatility(): number {
if (this.dailyReturns.length === 0) return 0;
return stats.standardDeviation(this.dailyReturns) * Math.sqrt(252) * 100;
}
private calculateDownsideVolatility(): number {
const negativeReturns = this.dailyReturns.filter(r => r < 0);
if (negativeReturns.length === 0) return 0;
return stats.standardDeviation(negativeReturns) * Math.sqrt(252) * 100;
}
private calculateVaR(): { var95: number; cvar95: number } {
if (this.dailyReturns.length === 0) return { var95: 0, cvar95: 0 };
const sortedReturns = [...this.dailyReturns].sort((a, b) => a - b);
const index95 = Math.floor(sortedReturns.length * 0.05);
const var95 = Math.abs(sortedReturns[index95]) * 100;
const cvar95 = Math.abs(stats.mean(sortedReturns.slice(0, index95))) * 100;
return { var95, cvar95 };
}
private calculateSharpeRatio(annualReturn: number, volatility: number, riskFree: number = 2): number {
if (volatility === 0) return 0;
return (annualReturn - riskFree) / volatility;
}
private calculateSortinoRatio(annualReturn: number, downVolatility: number, riskFree: number = 2): number {
if (downVolatility === 0) return 0;
return (annualReturn - riskFree) / downVolatility;
}
private calculateInformationRatio(): number {
if (!this.benchmarkReturns || this.benchmarkReturns.length !== this.dailyReturns.length) {
return 0;
}
const excessReturns = this.dailyReturns.map((r, i) => r - this.benchmarkReturns![i]);
const trackingError = stats.standardDeviation(excessReturns);
if (trackingError === 0) return 0;
const avgExcessReturn = stats.mean(excessReturns);
return (avgExcessReturn * 252) / (trackingError * Math.sqrt(252));
}
private analyzeTradeStatistics(): Partial<PerformanceMetrics> {
if (this.trades.length === 0) {
return {
totalTrades: 0,
winRate: 0,
avgWin: 0,
avgLoss: 0,
avgWinLoss: 0,
profitFactor: 0,
expectancy: 0,
payoffRatio: 0,
avgHoldingPeriod: 0,
avgTradesPerDay: 0,
maxConsecutiveWins: 0,
maxConsecutiveLosses: 0,
largestWin: 0,
largestLoss: 0
};
}
const wins = this.trades.filter(t => t.pnl > 0);
const losses = this.trades.filter(t => t.pnl < 0);
const totalWins = wins.reduce((sum, t) => sum + t.pnl, 0);
const totalLosses = Math.abs(losses.reduce((sum, t) => sum + t.pnl, 0));
const avgWin = wins.length > 0 ? totalWins / wins.length : 0;
const avgLoss = losses.length > 0 ? totalLosses / losses.length : 0;
const winRate = (wins.length / this.trades.length) * 100;
const profitFactor = totalLosses > 0 ? totalWins / totalLosses : totalWins > 0 ? Infinity : 0;
const expectancy = (winRate / 100 * avgWin) - ((100 - winRate) / 100 * avgLoss);
const payoffRatio = avgLoss > 0 ? avgWin / avgLoss : 0;
// Holding period analysis
const holdingPeriods = this.trades.map(t => t.holdingPeriod);
const avgHoldingPeriod = stats.mean(holdingPeriods);
// Trades per day
const tradingDays = this.getTradingDays();
const avgTradesPerDay = tradingDays > 0 ? this.trades.length / tradingDays : 0;
// Consecutive wins/losses
const { maxConsecutiveWins, maxConsecutiveLosses } = this.calculateConsecutiveStats();
// Largest win/loss
const largestWin = Math.max(...this.trades.map(t => t.pnl), 0);
const largestLoss = Math.abs(Math.min(...this.trades.map(t => t.pnl), 0));
return {
totalTrades: this.trades.length,
winRate,
avgWin,
avgLoss,
avgWinLoss: avgWin - avgLoss,
profitFactor,
expectancy,
payoffRatio,
avgHoldingPeriod,
avgTradesPerDay,
maxConsecutiveWins,
maxConsecutiveLosses,
largestWin,
largestLoss
};
}
private calculateDistributionMetrics(): { skewness: number; kurtosis: number } {
if (this.dailyReturns.length < 4) {
return { skewness: 0, kurtosis: 0 };
}
const mean = stats.mean(this.dailyReturns);
const std = stats.standardDeviation(this.dailyReturns);
if (std === 0) {
return { skewness: 0, kurtosis: 0 };
}
const n = this.dailyReturns.length;
// Skewness
const skewSum = this.dailyReturns.reduce((sum, r) => sum + Math.pow((r - mean) / std, 3), 0);
const skewness = (n / ((n - 1) * (n - 2))) * skewSum;
// Kurtosis
const kurtSum = this.dailyReturns.reduce((sum, r) => sum + Math.pow((r - mean) / std, 4), 0);
const kurtosis = (n * (n + 1) / ((n - 1) * (n - 2) * (n - 3))) * kurtSum -
(3 * (n - 1) * (n - 1)) / ((n - 2) * (n - 3));
return { skewness, kurtosis };
}
private calculateTailRatio(): number {
if (this.dailyReturns.length < 20) return 0;
const sorted = [...this.dailyReturns].sort((a, b) => b - a);
const percentile95 = sorted[Math.floor(sorted.length * 0.05)];
const percentile5 = sorted[Math.floor(sorted.length * 0.95)];
return Math.abs(percentile5) > 0 ? percentile95 / Math.abs(percentile5) : 0;
}
private calculateKellyCriterion(tradeStats: Partial<PerformanceMetrics>):
{ kellyFraction: number; optimalLeverage: number } {
const winRate = (tradeStats.winRate || 0) / 100;
const payoffRatio = tradeStats.payoffRatio || 0;
if (payoffRatio === 0) {
return { kellyFraction: 0, optimalLeverage: 1 };
}
// Kelly formula: f = p - q/b
// where p = win probability, q = loss probability, b = payoff ratio
const kellyFraction = winRate - (1 - winRate) / payoffRatio;
// Conservative Kelly (25% of full Kelly)
const conservativeKelly = Math.max(0, Math.min(0.25, kellyFraction * 0.25));
// Optimal leverage based on Sharpe ratio
const sharpe = this.calculateSharpeRatio(
this.calculateAnnualizedReturn(),
this.calculateVolatility()
);
const optimalLeverage = Math.max(1, Math.min(3, sharpe / 2));
return {
kellyFraction: conservativeKelly,
optimalLeverage
};
}
private calculateConsecutiveStats(): { maxConsecutiveWins: number; maxConsecutiveLosses: number } {
let maxWins = 0, maxLosses = 0;
let currentWins = 0, currentLosses = 0;
for (const trade of this.trades) {
if (trade.pnl > 0) {
currentWins++;
currentLosses = 0;
maxWins = Math.max(maxWins, currentWins);
} else if (trade.pnl < 0) {
currentLosses++;
currentWins = 0;
maxLosses = Math.max(maxLosses, currentLosses);
}
}
return { maxConsecutiveWins: maxWins, maxConsecutiveLosses: maxLosses };
}
private getYears(): number {
if (this.equityCurve.length < 2) return 1;
const start = this.equityCurve[0].date;
const end = this.equityCurve[this.equityCurve.length - 1].date;
return this.daysBetween(start, end) / 365;
}
private getTradingDays(): number {
if (this.equityCurve.length < 2) return 0;
const start = this.equityCurve[0].date;
const end = this.equityCurve[this.equityCurve.length - 1].date;
return this.daysBetween(start, end) * (252 / 365); // Approximate trading days
}
private daysBetween(start: Date, end: Date): number {
return (end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24);
}
private getEmptyMetrics(): PerformanceMetrics {
return {
totalReturn: 0,
annualizedReturn: 0,
cagr: 0,
volatility: 0,
downVolatility: 0,
maxDrawdown: 0,
maxDrawdownDuration: 0,
var95: 0,
cvar95: 0,
sharpeRatio: 0,
sortinoRatio: 0,
calmarRatio: 0,
informationRatio: 0,
totalTrades: 0,
winRate: 0,
avgWin: 0,
avgLoss: 0,
avgWinLoss: 0,
profitFactor: 0,
expectancy: 0,
payoffRatio: 0,
avgHoldingPeriod: 0,
avgTradesPerDay: 0,
maxConsecutiveWins: 0,
maxConsecutiveLosses: 0,
largestWin: 0,
largestLoss: 0,
skewness: 0,
kurtosis: 0,
tailRatio: 0,
kellyFraction: 0,
optimalLeverage: 1
};
}
exportReport(): string {
const metrics = this.analyze();
const drawdowns = this.analyzeDrawdowns();
return `
# Performance Report
## Summary Statistics
- Total Return: ${metrics.totalReturn.toFixed(2)}%
- Annualized Return: ${metrics.annualizedReturn.toFixed(2)}%
- CAGR: ${metrics.cagr.toFixed(2)}%
- Volatility: ${metrics.volatility.toFixed(2)}%
- Max Drawdown: ${metrics.maxDrawdown.toFixed(2)}%
- Sharpe Ratio: ${metrics.sharpeRatio.toFixed(2)}
- Sortino Ratio: ${metrics.sortinoRatio.toFixed(2)}
## Trade Analysis
- Total Trades: ${metrics.totalTrades}
- Win Rate: ${metrics.winRate.toFixed(1)}%
- Profit Factor: ${metrics.profitFactor.toFixed(2)}
- Average Win: $${metrics.avgWin.toFixed(2)}
- Average Loss: $${metrics.avgLoss.toFixed(2)}
- Expectancy: $${metrics.expectancy.toFixed(2)}
## Risk Metrics
- VaR (95%): ${metrics.var95.toFixed(2)}%
- CVaR (95%): ${metrics.cvar95.toFixed(2)}%
- Downside Volatility: ${metrics.downVolatility.toFixed(2)}%
- Tail Ratio: ${metrics.tailRatio.toFixed(2)}
- Skewness: ${metrics.skewness.toFixed(2)}
- Kurtosis: ${metrics.kurtosis.toFixed(2)}
## Optimal Position Sizing
- Kelly Fraction: ${(metrics.kellyFraction * 100).toFixed(1)}%
- Optimal Leverage: ${metrics.optimalLeverage.toFixed(1)}x
`;
}
}

View file

@ -0,0 +1,180 @@
import { Hono } from 'hono';
import { z } from 'zod';
import { logger } from '@stock-bot/logger';
import { AnalyticsService } from '../../services/AnalyticsService';
import { container } from '../../container';
const DateRangeSchema = z.object({
startDate: z.string().datetime(),
endDate: z.string().datetime()
});
const OptimizationRequestSchema = z.object({
symbols: z.array(z.string()),
returns: z.array(z.array(z.number())),
constraints: z.object({
minWeight: z.number().optional(),
maxWeight: z.number().optional(),
targetReturn: z.number().optional(),
maxRisk: z.number().optional()
}).optional()
});
export function createAnalyticsRoutes(): Hono {
const app = new Hono();
const analyticsService = container.get('AnalyticsService') as AnalyticsService;
// Get performance metrics
app.get('/performance/:portfolioId', async (c) => {
try {
const portfolioId = c.req.param('portfolioId');
const query = c.req.query();
const { startDate, endDate } = DateRangeSchema.parse({
startDate: query.start_date,
endDate: query.end_date
});
const metrics = await analyticsService.getPerformanceMetrics(
portfolioId,
new Date(startDate),
new Date(endDate)
);
return c.json(metrics);
} catch (error) {
if (error instanceof z.ZodError) {
return c.json({
error: 'Invalid date range',
details: error.errors
}, 400);
}
logger.error('Error getting performance metrics:', error);
return c.json({
error: error instanceof Error ? error.message : 'Failed to get performance metrics'
}, 500);
}
});
// Portfolio optimization
app.post('/optimize', async (c) => {
try {
const body = await c.req.json();
const request = OptimizationRequestSchema.parse(body);
const result = await analyticsService.optimizePortfolio({
returns: request.returns,
constraints: request.constraints
});
return c.json(result);
} catch (error) {
if (error instanceof z.ZodError) {
return c.json({
error: 'Invalid optimization request',
details: error.errors
}, 400);
}
logger.error('Error optimizing portfolio:', error);
return c.json({
error: error instanceof Error ? error.message : 'Failed to optimize portfolio'
}, 500);
}
});
// Get risk metrics
app.get('/risk/:portfolioId', async (c) => {
try {
const portfolioId = c.req.param('portfolioId');
const metrics = await analyticsService.getRiskMetrics(portfolioId);
return c.json(metrics);
} catch (error) {
logger.error('Error getting risk metrics:', error);
return c.json({
error: error instanceof Error ? error.message : 'Failed to get risk metrics'
}, 500);
}
});
// Market regime detection
app.get('/regime', async (c) => {
try {
const regime = await analyticsService.detectMarketRegime();
return c.json({
regime,
timestamp: new Date().toISOString()
});
} catch (error) {
logger.error('Error detecting market regime:', error);
return c.json({
error: error instanceof Error ? error.message : 'Failed to detect market regime'
}, 500);
}
});
// Calculate correlation matrix
app.post('/correlation', async (c) => {
try {
const body = await c.req.json();
const { symbols } = z.object({
symbols: z.array(z.string()).min(2)
}).parse(body);
const matrix = await analyticsService.calculateCorrelationMatrix(symbols);
return c.json({
symbols,
matrix
});
} catch (error) {
if (error instanceof z.ZodError) {
return c.json({
error: 'Invalid correlation request',
details: error.errors
}, 400);
}
logger.error('Error calculating correlation:', error);
return c.json({
error: error instanceof Error ? error.message : 'Failed to calculate correlation'
}, 500);
}
});
// ML model prediction
app.post('/predict', async (c) => {
try {
const body = await c.req.json();
const { modelId, features } = z.object({
modelId: z.string(),
features: z.record(z.number())
}).parse(body);
const prediction = await analyticsService.predictWithModel(modelId, features);
if (prediction) {
return c.json(prediction);
} else {
return c.json({ error: 'Model not found or prediction failed' }, 404);
}
} catch (error) {
if (error instanceof z.ZodError) {
return c.json({
error: 'Invalid prediction request',
details: error.errors
}, 400);
}
logger.error('Error making prediction:', error);
return c.json({
error: error instanceof Error ? error.message : 'Failed to make prediction'
}, 500);
}
});
return app;
}

View file

@ -0,0 +1,162 @@
import { Hono } from 'hono';
import { z } from 'zod';
import { logger } from '@stock-bot/logger';
import { BacktestConfigSchema } from '../../types';
import { BacktestEngine } from '../../backtest/BacktestEngine';
import { ModeManager } from '../../core/ModeManager';
import { container } from '../../container';
const BacktestIdSchema = z.object({
backtestId: z.string()
});
export function createBacktestRoutes(): Hono {
const app = new Hono();
const backtestEngine = container.get('BacktestEngine') as BacktestEngine;
const modeManager = container.get('ModeManager') as ModeManager;
// Run new backtest
app.post('/run', async (c) => {
try {
const body = await c.req.json();
const config = BacktestConfigSchema.parse(body);
// Initialize backtest mode
await modeManager.initializeMode(config);
// Run backtest
const result = await backtestEngine.runBacktest(config);
return c.json(result, 201);
} catch (error) {
if (error instanceof z.ZodError) {
return c.json({
error: 'Invalid backtest configuration',
details: error.errors
}, 400);
}
logger.error('Error running backtest:', error);
return c.json({
error: error instanceof Error ? error.message : 'Failed to run backtest'
}, 500);
}
});
// Stop running backtest
app.post('/stop', async (c) => {
try {
await backtestEngine.stopBacktest();
return c.json({
message: 'Backtest stop requested',
timestamp: new Date().toISOString()
});
} catch (error) {
logger.error('Error stopping backtest:', error);
return c.json({
error: error instanceof Error ? error.message : 'Failed to stop backtest'
}, 500);
}
});
// Get backtest progress
app.get('/progress', async (c) => {
try {
// In real implementation, would track progress
return c.json({
status: 'running',
progress: 0.5,
processed: 10000,
total: 20000,
currentTime: new Date().toISOString()
});
} catch (error) {
logger.error('Error getting backtest progress:', error);
return c.json({
error: error instanceof Error ? error.message : 'Failed to get progress'
}, 500);
}
});
// Stream backtest events (Server-Sent Events)
app.get('/stream', async (c) => {
c.header('Content-Type', 'text/event-stream');
c.header('Cache-Control', 'no-cache');
c.header('Connection', 'keep-alive');
const stream = new ReadableStream({
start(controller) {
// Listen for backtest events
const onProgress = (data: any) => {
controller.enqueue(`data: ${JSON.stringify(data)}\n\n`);
};
const onComplete = (data: any) => {
controller.enqueue(`data: ${JSON.stringify({ event: 'complete', data })}\n\n`);
controller.close();
};
backtestEngine.on('progress', onProgress);
backtestEngine.on('complete', onComplete);
// Cleanup on close
c.req.raw.signal.addEventListener('abort', () => {
backtestEngine.off('progress', onProgress);
backtestEngine.off('complete', onComplete);
controller.close();
});
}
});
return new Response(stream);
});
// Validate backtest configuration
app.post('/validate', async (c) => {
try {
const body = await c.req.json();
const config = BacktestConfigSchema.parse(body);
// Additional validation logic
const validation = {
valid: true,
warnings: [] as string[],
estimatedDuration: 0
};
// Check data availability
const startDate = new Date(config.startDate);
const endDate = new Date(config.endDate);
const days = (endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24);
if (days > 365) {
validation.warnings.push('Large date range may take significant time to process');
}
if (config.symbols.length > 100) {
validation.warnings.push('Large number of symbols may impact performance');
}
// Estimate duration (simplified)
validation.estimatedDuration = days * config.symbols.length * 0.1; // seconds
return c.json(validation);
} catch (error) {
if (error instanceof z.ZodError) {
return c.json({
valid: false,
error: 'Invalid configuration',
details: error.errors
}, 400);
}
return c.json({
valid: false,
error: error instanceof Error ? error.message : 'Validation failed'
}, 500);
}
});
return app;
}

View file

@ -0,0 +1,112 @@
import { Hono } from 'hono';
import { z } from 'zod';
import { logger } from '@stock-bot/logger';
import { OrderRequestSchema } from '../../types';
import { ExecutionService } from '../../services/ExecutionService';
import { container } from '../../container';
const OrderIdSchema = z.object({
orderId: z.string()
});
export function createOrderRoutes(): Hono {
const app = new Hono();
const executionService = container.get('ExecutionService') as ExecutionService;
// Submit new order
app.post('/', async (c) => {
try {
const body = await c.req.json();
const orderRequest = OrderRequestSchema.parse(body);
const result = await executionService.submitOrder(orderRequest);
return c.json(result, 201);
} catch (error) {
if (error instanceof z.ZodError) {
return c.json({
error: 'Invalid order request',
details: error.errors
}, 400);
}
logger.error('Error submitting order:', error);
return c.json({
error: error instanceof Error ? error.message : 'Failed to submit order'
}, 500);
}
});
// Cancel order
app.delete('/:orderId', async (c) => {
try {
const { orderId } = OrderIdSchema.parse(c.req.param());
const success = await executionService.cancelOrder(orderId);
if (success) {
return c.json({ message: 'Order cancelled successfully' });
} else {
return c.json({ error: 'Order not found or already filled' }, 404);
}
} catch (error) {
logger.error('Error cancelling order:', error);
return c.json({
error: error instanceof Error ? error.message : 'Failed to cancel order'
}, 500);
}
});
// Get order status
app.get('/:orderId', async (c) => {
try {
const { orderId } = OrderIdSchema.parse(c.req.param());
const status = await executionService.getOrderStatus(orderId);
if (status) {
return c.json(status);
} else {
return c.json({ error: 'Order not found' }, 404);
}
} catch (error) {
logger.error('Error getting order status:', error);
return c.json({
error: error instanceof Error ? error.message : 'Failed to get order status'
}, 500);
}
});
// Batch order submission
app.post('/batch', async (c) => {
try {
const body = await c.req.json();
const orders = z.array(OrderRequestSchema).parse(body);
const results = await Promise.allSettled(
orders.map(order => executionService.submitOrder(order))
);
const response = results.map((result, index) => ({
order: orders[index],
result: result.status === 'fulfilled' ? result.value : { error: result.reason }
}));
return c.json(response, 201);
} catch (error) {
if (error instanceof z.ZodError) {
return c.json({
error: 'Invalid batch order request',
details: error.errors
}, 400);
}
logger.error('Error submitting batch orders:', error);
return c.json({
error: error instanceof Error ? error.message : 'Failed to submit batch orders'
}, 500);
}
});
return app;
}

View file

@ -0,0 +1,122 @@
import { Hono } from 'hono';
import { z } from 'zod';
import { logger } from '@stock-bot/logger';
import { ModeManager } from '../../core/ModeManager';
import { container } from '../../container';
const SymbolSchema = z.object({
symbol: z.string()
});
export function createPositionRoutes(): Hono {
const app = new Hono();
const modeManager = container.get('ModeManager') as ModeManager;
// Get all positions
app.get('/', async (c) => {
try {
const tradingEngine = modeManager.getTradingEngine();
const positions = JSON.parse(tradingEngine.getAllPositions());
return c.json({
mode: modeManager.getCurrentMode(),
positions
});
} catch (error) {
logger.error('Error getting positions:', error);
return c.json({
error: error instanceof Error ? error.message : 'Failed to get positions'
}, 500);
}
});
// Get open positions only
app.get('/open', async (c) => {
try {
const tradingEngine = modeManager.getTradingEngine();
const positions = JSON.parse(tradingEngine.getOpenPositions());
return c.json({
mode: modeManager.getCurrentMode(),
positions
});
} catch (error) {
logger.error('Error getting open positions:', error);
return c.json({
error: error instanceof Error ? error.message : 'Failed to get open positions'
}, 500);
}
});
// Get position for specific symbol
app.get('/:symbol', async (c) => {
try {
const { symbol } = SymbolSchema.parse(c.req.param());
const tradingEngine = modeManager.getTradingEngine();
const positionJson = tradingEngine.getPosition(symbol);
const position = positionJson ? JSON.parse(positionJson) : null;
if (position) {
return c.json({
mode: modeManager.getCurrentMode(),
position
});
} else {
return c.json({
error: 'Position not found',
symbol
}, 404);
}
} catch (error) {
logger.error('Error getting position:', error);
return c.json({
error: error instanceof Error ? error.message : 'Failed to get position'
}, 500);
}
});
// Get P&L summary
app.get('/pnl/summary', async (c) => {
try {
const tradingEngine = modeManager.getTradingEngine();
const [realizedPnl, unrealizedPnl] = tradingEngine.getTotalPnl();
return c.json({
mode: modeManager.getCurrentMode(),
pnl: {
realized: realizedPnl,
unrealized: unrealizedPnl,
total: realizedPnl + unrealizedPnl
},
timestamp: new Date().toISOString()
});
} catch (error) {
logger.error('Error getting P&L:', error);
return c.json({
error: error instanceof Error ? error.message : 'Failed to get P&L'
}, 500);
}
});
// Get risk metrics
app.get('/risk/metrics', async (c) => {
try {
const tradingEngine = modeManager.getTradingEngine();
const metrics = JSON.parse(tradingEngine.getRiskMetrics());
return c.json({
mode: modeManager.getCurrentMode(),
risk: metrics,
timestamp: new Date().toISOString()
});
} catch (error) {
logger.error('Error getting risk metrics:', error);
return c.json({
error: error instanceof Error ? error.message : 'Failed to get risk metrics'
}, 500);
}
});
return app;
}

View file

@ -0,0 +1,195 @@
import { Server as SocketIOServer, Socket } from 'socket.io';
import { logger } from '@stock-bot/logger';
import { z } from 'zod';
import { MarketDataService } from '../../services/MarketDataService';
import { ExecutionService } from '../../services/ExecutionService';
import { ModeManager } from '../../core/ModeManager';
import { Container } from '@stock-bot/di';
const SubscribeSchema = z.object({
symbols: z.array(z.string()),
dataTypes: z.array(z.enum(['quote', 'trade', 'bar'])).optional()
});
const UnsubscribeSchema = z.object({
symbols: z.array(z.string())
});
export function setupWebSocketHandlers(io: SocketIOServer, container: Container): void {
const marketDataService = container.get('MarketDataService') as MarketDataService;
const executionService = container.get('ExecutionService') as ExecutionService;
const modeManager = container.get('ModeManager') as ModeManager;
// Track client subscriptions
const clientSubscriptions = new Map<string, Set<string>>();
io.on('connection', (socket: Socket) => {
logger.info(`WebSocket client connected: ${socket.id}`);
clientSubscriptions.set(socket.id, new Set());
// Send initial connection info
socket.emit('connected', {
mode: modeManager.getCurrentMode(),
timestamp: new Date().toISOString()
});
// Handle market data subscriptions
socket.on('subscribe', async (data: any, callback?: Function) => {
try {
const { symbols, dataTypes } = SubscribeSchema.parse(data);
const subscriptions = clientSubscriptions.get(socket.id)!;
for (const symbol of symbols) {
await marketDataService.subscribeToSymbol(symbol);
subscriptions.add(symbol);
}
logger.debug(`Client ${socket.id} subscribed to: ${symbols.join(', ')}`);
if (callback) {
callback({ success: true, symbols });
}
} catch (error) {
logger.error('Subscription error:', error);
if (callback) {
callback({
success: false,
error: error instanceof Error ? error.message : 'Subscription failed'
});
}
}
});
// Handle unsubscribe
socket.on('unsubscribe', async (data: any, callback?: Function) => {
try {
const { symbols } = UnsubscribeSchema.parse(data);
const subscriptions = clientSubscriptions.get(socket.id)!;
for (const symbol of symbols) {
subscriptions.delete(symbol);
// Check if any other clients are subscribed
let othersSubscribed = false;
for (const [clientId, subs] of clientSubscriptions) {
if (clientId !== socket.id && subs.has(symbol)) {
othersSubscribed = true;
break;
}
}
if (!othersSubscribed) {
await marketDataService.unsubscribeFromSymbol(symbol);
}
}
logger.debug(`Client ${socket.id} unsubscribed from: ${symbols.join(', ')}`);
if (callback) {
callback({ success: true, symbols });
}
} catch (error) {
logger.error('Unsubscribe error:', error);
if (callback) {
callback({
success: false,
error: error instanceof Error ? error.message : 'Unsubscribe failed'
});
}
}
});
// Handle order submission via WebSocket
socket.on('submitOrder', async (order: any, callback?: Function) => {
try {
const result = await executionService.submitOrder(order);
if (callback) {
callback({ success: true, result });
}
} catch (error) {
logger.error('Order submission error:', error);
if (callback) {
callback({
success: false,
error: error instanceof Error ? error.message : 'Order submission failed'
});
}
}
});
// Handle position queries
socket.on('getPositions', async (callback?: Function) => {
try {
const tradingEngine = modeManager.getTradingEngine();
const positions = JSON.parse(tradingEngine.getAllPositions());
if (callback) {
callback({ success: true, positions });
}
} catch (error) {
logger.error('Error getting positions:', error);
if (callback) {
callback({
success: false,
error: error instanceof Error ? error.message : 'Failed to get positions'
});
}
}
});
// Handle disconnection
socket.on('disconnect', async () => {
logger.info(`WebSocket client disconnected: ${socket.id}`);
// Unsubscribe from all symbols for this client
const subscriptions = clientSubscriptions.get(socket.id);
if (subscriptions) {
for (const symbol of subscriptions) {
// Check if any other clients are subscribed
let othersSubscribed = false;
for (const [clientId, subs] of clientSubscriptions) {
if (clientId !== socket.id && subs.has(symbol)) {
othersSubscribed = true;
break;
}
}
if (!othersSubscribed) {
await marketDataService.unsubscribeFromSymbol(symbol);
}
}
}
clientSubscriptions.delete(socket.id);
});
});
// Forward market data to subscribed clients
marketDataService.on('marketData', (data: any) => {
for (const [clientId, subscriptions] of clientSubscriptions) {
if (subscriptions.has(data.data.symbol)) {
io.to(clientId).emit('marketData', data);
}
}
});
// Forward order updates to all clients
executionService.on('orderUpdate', (update: any) => {
io.emit('orderUpdate', update);
});
// Forward fills to all clients
executionService.on('fill', (fill: any) => {
io.emit('fill', fill);
});
// Mode change notifications
modeManager.on('modeChanged', (config: any) => {
io.emit('modeChanged', {
mode: config.mode,
timestamp: new Date().toISOString()
});
});
logger.info('WebSocket handlers initialized');
}

View file

@ -0,0 +1,634 @@
import { logger } from '@stock-bot/logger';
import { EventEmitter } from 'events';
import { MarketData, BacktestConfigSchema, PerformanceMetrics, MarketMicrostructure } from '../types';
import { StorageService } from '../services/StorageService';
import { StrategyManager } from '../strategies/StrategyManager';
import { TradingEngine } from '../../core';
import { DataManager } from '../data/DataManager';
import { MarketSimulator } from './MarketSimulator';
import { PerformanceAnalyzer } from '../analytics/PerformanceAnalyzer';
interface BacktestEvent {
timestamp: number;
type: 'market_data' | 'strategy_signal' | 'order_fill';
data: any;
}
interface BacktestResult {
id: string;
config: any;
performance: PerformanceMetrics;
trades: any[];
equityCurve: { timestamp: number; value: number }[];
drawdown: { timestamp: number; value: number }[];
dailyReturns: number[];
finalPositions: any[];
}
export class BacktestEngine extends EventEmitter {
private eventQueue: BacktestEvent[] = [];
private currentTime: number = 0;
private equityCurve: { timestamp: number; value: number }[] = [];
private trades: any[] = [];
private isRunning = false;
private dataManager: DataManager;
private marketSimulator: MarketSimulator;
private performanceAnalyzer: PerformanceAnalyzer;
private microstructures: Map<string, MarketMicrostructure> = new Map();
constructor(
private storageService: StorageService,
private strategyManager: StrategyManager
) {
super();
this.dataManager = new DataManager(storageService);
this.marketSimulator = new MarketSimulator({
useHistoricalSpreads: true,
modelHiddenLiquidity: true,
includeDarkPools: true,
latencyMs: 1
});
this.performanceAnalyzer = new PerformanceAnalyzer();
}
async runBacktest(config: any): Promise<BacktestResult> {
// Validate config
const validatedConfig = BacktestConfigSchema.parse(config);
logger.info(`Starting backtest from ${validatedConfig.startDate} to ${validatedConfig.endDate}`);
// Reset state
this.reset();
this.isRunning = true;
// Generate backtest ID
const backtestId = `backtest_${Date.now()}`;
try {
// Load historical data with multi-resolution support
const dataMap = await this.dataManager.loadHistoricalData(
validatedConfig.symbols,
new Date(validatedConfig.startDate),
new Date(validatedConfig.endDate),
validatedConfig.dataFrequency,
true // Include extended hours
);
// Load market microstructure for each symbol
await this.loadMarketMicrostructure(validatedConfig.symbols);
// Convert to flat array and sort by time
const marketData: MarketData[] = [];
dataMap.forEach((data, symbol) => {
marketData.push(...data);
});
marketData.sort((a, b) => a.data.timestamp - b.data.timestamp);
logger.info(`Loaded ${marketData.length} market data points`);
// Initialize strategies
await this.strategyManager.initializeStrategies(validatedConfig.strategies || []);
// Convert market data to events
this.populateEventQueue(marketData);
// Main backtest loop
await this.processEvents();
// Calculate final metrics
const performance = this.calculatePerformance();
// Get final positions
const finalPositions = await this.getFinalPositions();
// Store results
const result: BacktestResult = {
id: backtestId,
config: validatedConfig,
performance,
trades: this.trades,
equityCurve: this.equityCurve,
drawdown: this.calculateDrawdown(),
dailyReturns: this.calculateDailyReturns(),
finalPositions
};
await this.storeResults(result);
logger.info(`Backtest completed: ${performance.totalTrades} trades, ${performance.totalReturn}% return`);
return result;
} catch (error) {
logger.error('Backtest failed:', error);
throw error;
} finally {
this.isRunning = false;
this.reset();
}
}
private async loadHistoricalData(config: any): Promise<MarketData[]> {
const data: MarketData[] = [];
const startDate = new Date(config.startDate);
const endDate = new Date(config.endDate);
for (const symbol of config.symbols) {
const bars = await this.storageService.getHistoricalBars(
symbol,
startDate,
endDate,
config.dataFrequency
);
// Convert to MarketData format
bars.forEach(bar => {
data.push({
type: 'bar',
data: {
symbol,
open: bar.open,
high: bar.high,
low: bar.low,
close: bar.close,
volume: bar.volume,
vwap: bar.vwap,
timestamp: new Date(bar.timestamp).getTime()
}
});
});
}
// Sort by timestamp
data.sort((a, b) => {
const timeA = a.data.timestamp;
const timeB = b.data.timestamp;
return timeA - timeB;
});
return data;
}
private populateEventQueue(marketData: MarketData[]): void {
// Convert market data to events
marketData.forEach(data => {
this.eventQueue.push({
timestamp: data.data.timestamp,
type: 'market_data',
data
});
});
// Sort by timestamp (should already be sorted)
this.eventQueue.sort((a, b) => a.timestamp - b.timestamp);
}
private async processEvents(): Promise<void> {
const tradingEngine = this.strategyManager.getTradingEngine();
let lastEquityUpdate = 0;
const equityUpdateInterval = 60000; // Update equity every minute
while (this.eventQueue.length > 0 && this.isRunning) {
const event = this.eventQueue.shift()!;
// Advance time
this.currentTime = event.timestamp;
if (tradingEngine) {
await tradingEngine.advanceTime(this.currentTime);
}
// Process event based on type
switch (event.type) {
case 'market_data':
await this.processMarketData(event.data);
break;
case 'strategy_signal':
await this.processStrategySignal(event.data);
break;
case 'order_fill':
await this.processFill(event.data);
break;
}
// Update equity curve periodically
if (this.currentTime - lastEquityUpdate > equityUpdateInterval) {
await this.updateEquityCurve();
lastEquityUpdate = this.currentTime;
}
// Emit progress
if (this.eventQueue.length % 1000 === 0) {
this.emit('progress', {
processed: this.trades.length,
remaining: this.eventQueue.length,
currentTime: new Date(this.currentTime)
});
}
}
// Final equity update
await this.updateEquityCurve();
}
private async processMarketData(data: MarketData): Promise<void> {
const tradingEngine = this.strategyManager.getTradingEngine();
if (!tradingEngine) return;
// Process through market simulator for realistic orderbook
const orderbook = this.marketSimulator.processMarketData(data);
if (orderbook) {
// Update trading engine with simulated orderbook
if (orderbook.bids.length > 0 && orderbook.asks.length > 0) {
tradingEngine.updateQuote(
orderbook.symbol,
orderbook.bids[0].price,
orderbook.asks[0].price,
orderbook.bids[0].size,
orderbook.asks[0].size
);
}
// Set microstructure in trading core for realistic fills
const microstructure = this.microstructures.get(orderbook.symbol);
if (microstructure && tradingEngine.setMicrostructure) {
tradingEngine.setMicrostructure(orderbook.symbol, microstructure);
}
} else {
// Fallback to simple processing
switch (data.type) {
case 'quote':
tradingEngine.updateQuote(
data.data.symbol,
data.data.bid,
data.data.ask,
data.data.bidSize,
data.data.askSize
);
break;
case 'trade':
tradingEngine.updateTrade(
data.data.symbol,
data.data.price,
data.data.size,
data.data.side
);
break;
case 'bar':
const spread = data.data.high - data.data.low;
const spreadBps = (spread / data.data.close) * 10000;
const halfSpread = data.data.close * Math.min(spreadBps, 10) / 20000;
tradingEngine.updateQuote(
data.data.symbol,
data.data.close - halfSpread,
data.data.close + halfSpread,
data.data.volume / 100,
data.data.volume / 100
);
break;
}
}
// Let strategies process the data
await this.strategyManager.onMarketData(data);
// Track performance
this.performanceAnalyzer.addEquityPoint(
new Date(this.currentTime),
this.getPortfolioValue()
);
}
private async processStrategySignal(signal: any): Promise<void> {
// Strategy signals are handled by strategy manager
// This is here for future extensions
}
private async processFill(fill: any): Promise<void> {
// Record trade
this.trades.push({
...fill,
backtestTime: this.currentTime
});
// Store in database
await this.storageService.storeFill(fill);
}
private async updateEquityCurve(): Promise<void> {
const tradingEngine = this.strategyManager.getTradingEngine();
if (!tradingEngine) return;
// Get current P&L
const [realized, unrealized] = tradingEngine.getTotalPnl();
const totalEquity = 100000 + realized + unrealized; // Assuming 100k starting capital
this.equityCurve.push({
timestamp: this.currentTime,
value: totalEquity
});
}
private calculatePerformance(): PerformanceMetrics {
// Use sophisticated performance analyzer
this.trades.forEach(trade => {
this.performanceAnalyzer.addTrade({
entryTime: new Date(trade.entryTime),
exitTime: new Date(trade.exitTime || this.currentTime),
symbol: trade.symbol,
side: trade.side,
entryPrice: trade.entryPrice,
exitPrice: trade.exitPrice || trade.currentPrice,
quantity: trade.quantity,
commission: trade.commission || 0,
pnl: trade.pnl || 0,
returnPct: trade.returnPct || 0,
holdingPeriod: trade.holdingPeriod || 0,
mae: trade.mae || 0,
mfe: trade.mfe || 0
});
});
const metrics = this.performanceAnalyzer.analyze();
// Add drawdown analysis
const drawdownAnalysis = this.performanceAnalyzer.analyzeDrawdowns();
return {
...metrics,
maxDrawdown: drawdownAnalysis.maxDrawdown,
maxDrawdownDuration: drawdownAnalysis.maxDrawdownDuration
};
}
const initialEquity = this.equityCurve[0].value;
const finalEquity = this.equityCurve[this.equityCurve.length - 1].value;
const totalReturn = ((finalEquity - initialEquity) / initialEquity) * 100;
// Calculate daily returns
const dailyReturns = this.calculateDailyReturns();
// Sharpe ratio (assuming 0% risk-free rate)
const avgReturn = dailyReturns.reduce((a, b) => a + b, 0) / dailyReturns.length;
const stdDev = Math.sqrt(
dailyReturns.reduce((sum, r) => sum + Math.pow(r - avgReturn, 2), 0) / dailyReturns.length
);
const sharpeRatio = stdDev > 0 ? (avgReturn / stdDev) * Math.sqrt(252) : 0; // Annualized
// Win rate and profit factor
const winningTrades = this.trades.filter(t => t.pnl > 0);
const losingTrades = this.trades.filter(t => t.pnl < 0);
const winRate = this.trades.length > 0 ? (winningTrades.length / this.trades.length) * 100 : 0;
const totalWins = winningTrades.reduce((sum, t) => sum + t.pnl, 0);
const totalLosses = Math.abs(losingTrades.reduce((sum, t) => sum + t.pnl, 0));
const profitFactor = totalLosses > 0 ? totalWins / totalLosses : totalWins > 0 ? Infinity : 0;
const avgWin = winningTrades.length > 0 ? totalWins / winningTrades.length : 0;
const avgLoss = losingTrades.length > 0 ? totalLosses / losingTrades.length : 0;
// Max drawdown
const drawdowns = this.calculateDrawdown();
const maxDrawdown = Math.min(...drawdowns.map(d => d.value));
return {
totalReturn,
sharpeRatio,
sortinoRatio: sharpeRatio * 0.8, // Simplified for now
maxDrawdown: Math.abs(maxDrawdown),
winRate,
profitFactor,
avgWin,
avgLoss,
totalTrades: this.trades.length
};
}
private calculateDrawdown(): { timestamp: number; value: number }[] {
const drawdowns: { timestamp: number; value: number }[] = [];
let peak = this.equityCurve[0]?.value || 0;
for (const point of this.equityCurve) {
if (point.value > peak) {
peak = point.value;
}
const drawdown = ((point.value - peak) / peak) * 100;
drawdowns.push({
timestamp: point.timestamp,
value: drawdown
});
}
return drawdowns;
}
private calculateDailyReturns(): number[] {
const dailyReturns: number[] = [];
const dailyEquity = new Map<string, number>();
// Group equity by day
for (const point of this.equityCurve) {
const date = new Date(point.timestamp).toDateString();
dailyEquity.set(date, point.value);
}
// Calculate returns
const dates = Array.from(dailyEquity.keys()).sort();
for (let i = 1; i < dates.length; i++) {
const prevValue = dailyEquity.get(dates[i - 1])!;
const currValue = dailyEquity.get(dates[i])!;
const dailyReturn = ((currValue - prevValue) / prevValue) * 100;
dailyReturns.push(dailyReturn);
}
return dailyReturns;
}
private async getFinalPositions(): Promise<any[]> {
const tradingEngine = this.strategyManager.getTradingEngine();
if (!tradingEngine) return [];
const positions = JSON.parse(tradingEngine.getOpenPositions());
return positions;
}
private async storeResults(result: BacktestResult): Promise<void> {
// Store performance metrics
await this.storageService.storeStrategyPerformance(
result.id,
result.performance
);
// Could also store detailed results in a separate table or file
logger.debug(`Backtest results stored with ID: ${result.id}`);
}
private reset(): void {
this.eventQueue = [];
this.currentTime = 0;
this.equityCurve = [];
this.trades = [];
this.marketSimulator.reset();
}
private async loadMarketMicrostructure(symbols: string[]): Promise<void> {
// In real implementation, would load from database
// For now, create reasonable defaults based on symbol characteristics
for (const symbol of symbols) {
const microstructure: MarketMicrostructure = {
symbol,
avgSpreadBps: 2 + Math.random() * 3, // 2-5 bps
dailyVolume: 10_000_000 * (1 + Math.random() * 9), // 10-100M shares
avgTradeSize: 100 + Math.random() * 400, // 100-500 shares
volatility: 0.15 + Math.random() * 0.25, // 15-40% annual vol
tickSize: 0.01,
lotSize: 1,
intradayVolumeProfile: this.generateIntradayProfile()
};
this.microstructures.set(symbol, microstructure);
this.marketSimulator.setMicrostructure(symbol, microstructure);
}
}
private generateIntradayProfile(): number[] {
// U-shaped intraday volume pattern
const profile = new Array(24).fill(0);
const tradingHours = [9, 10, 11, 12, 13, 14, 15, 16]; // 9:30 AM to 4:00 PM
tradingHours.forEach((hour, idx) => {
if (idx === 0 || idx === tradingHours.length - 1) {
// High volume at open and close
profile[hour] = 0.2;
} else if (idx === 1 || idx === tradingHours.length - 2) {
// Moderate volume
profile[hour] = 0.15;
} else {
// Lower midday volume
profile[hour] = 0.1;
}
});
// Normalize
const sum = profile.reduce((a, b) => a + b, 0);
return profile.map(v => v / sum);
}
private getPortfolioValue(): number {
const tradingEngine = this.strategyManager.getTradingEngine();
if (!tradingEngine) return 100000; // Default initial capital
const [realized, unrealized] = tradingEngine.getTotalPnl();
return 100000 + realized + unrealized;
}
async stopBacktest(): Promise<void> {
this.isRunning = false;
logger.info('Backtest stop requested');
}
async exportResults(format: 'json' | 'csv' | 'html' = 'json'): Promise<string> {
const result = {
summary: this.calculatePerformance(),
trades: this.trades,
equityCurve: this.equityCurve,
drawdowns: this.calculateDrawdown(),
dataQuality: this.dataManager.getDataQualityReport(),
performanceReport: this.performanceAnalyzer.exportReport()
};
switch (format) {
case 'json':
return JSON.stringify(result, null, 2);
case 'csv':
// Convert to CSV format
return this.convertToCSV(result);
case 'html':
// Generate HTML report
return this.generateHTMLReport(result);
default:
return JSON.stringify(result);
}
}
private convertToCSV(result: any): string {
// Simple CSV conversion for trades
const headers = ['Date', 'Symbol', 'Side', 'Entry', 'Exit', 'Quantity', 'PnL', 'Return%'];
const rows = result.trades.map(t => [
new Date(t.entryTime).toISOString(),
t.symbol,
t.side,
t.entryPrice,
t.exitPrice,
t.quantity,
t.pnl,
t.returnPct
]);
return [headers, ...rows].map(row => row.join(',')).join('\n');
}
private generateHTMLReport(result: any): string {
return `
<!DOCTYPE html>
<html>
<head>
<title>Backtest Report</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #f2f2f2; }
.metric { margin: 10px 0; }
.positive { color: green; }
.negative { color: red; }
</style>
</head>
<body>
<h1>Backtest Performance Report</h1>
<h2>Summary Statistics</h2>
<div class="metric">Total Return: <span class="${result.summary.totalReturn >= 0 ? 'positive' : 'negative'}">${result.summary.totalReturn.toFixed(2)}%</span></div>
<div class="metric">Sharpe Ratio: ${result.summary.sharpeRatio.toFixed(2)}</div>
<div class="metric">Max Drawdown: <span class="negative">${result.summary.maxDrawdown.toFixed(2)}%</span></div>
<div class="metric">Win Rate: ${result.summary.winRate.toFixed(1)}%</div>
<div class="metric">Total Trades: ${result.summary.totalTrades}</div>
<h2>Detailed Performance Metrics</h2>
<pre>${result.performanceReport}</pre>
<h2>Trade History</h2>
<table>
<tr>
<th>Date</th>
<th>Symbol</th>
<th>Side</th>
<th>Entry Price</th>
<th>Exit Price</th>
<th>Quantity</th>
<th>P&L</th>
<th>Return %</th>
</tr>
${result.trades.map(t => `
<tr>
<td>${new Date(t.entryTime).toLocaleDateString()}</td>
<td>${t.symbol}</td>
<td>${t.side}</td>
<td>$${t.entryPrice.toFixed(2)}</td>
<td>$${t.exitPrice.toFixed(2)}</td>
<td>${t.quantity}</td>
<td class="${t.pnl >= 0 ? 'positive' : 'negative'}">$${t.pnl.toFixed(2)}</td>
<td class="${t.returnPct >= 0 ? 'positive' : 'negative'}">${t.returnPct.toFixed(2)}%</td>
</tr>
`).join('')}
</table>
</body>
</html>
`;
}
}

View file

@ -0,0 +1,385 @@
import { logger } from '@stock-bot/logger';
import { MarketData, Quote, Trade, Bar, OrderBookSnapshot, PriceLevel } from '../types';
import { MarketMicrostructure } from '../types/MarketMicrostructure';
export interface SimulationConfig {
useHistoricalSpreads: boolean;
modelHiddenLiquidity: boolean;
includeDarkPools: boolean;
latencyMs: number;
rebateRate: number;
takeFeeRate: number;
}
export interface LiquidityProfile {
visibleLiquidity: number;
hiddenLiquidity: number;
darkPoolLiquidity: number;
totalLiquidity: number;
}
export class MarketSimulator {
private orderBooks: Map<string, OrderBookSnapshot> = new Map();
private microstructures: Map<string, MarketMicrostructure> = new Map();
private liquidityProfiles: Map<string, LiquidityProfile> = new Map();
private lastTrades: Map<string, Trade> = new Map();
private config: SimulationConfig;
constructor(config: Partial<SimulationConfig> = {}) {
this.config = {
useHistoricalSpreads: true,
modelHiddenLiquidity: true,
includeDarkPools: true,
latencyMs: 1,
rebateRate: -0.0002, // 2 bps rebate for providing liquidity
takeFeeRate: 0.0003, // 3 bps fee for taking liquidity
...config
};
}
setMicrostructure(symbol: string, microstructure: MarketMicrostructure): void {
this.microstructures.set(symbol, microstructure);
this.updateLiquidityProfile(symbol);
}
processMarketData(data: MarketData): OrderBookSnapshot | null {
const { symbol } = this.getSymbolFromData(data);
switch (data.type) {
case 'quote':
return this.updateFromQuote(symbol, data.data);
case 'trade':
return this.updateFromTrade(symbol, data.data);
case 'bar':
return this.reconstructFromBar(symbol, data.data);
default:
return null;
}
}
private updateFromQuote(symbol: string, quote: Quote): OrderBookSnapshot {
let orderbook = this.orderBooks.get(symbol);
const microstructure = this.microstructures.get(symbol);
if (!orderbook || !microstructure) {
// Create new orderbook
orderbook = this.createOrderBook(symbol, quote, microstructure);
} else {
// Update existing orderbook
orderbook = this.updateOrderBook(orderbook, quote, microstructure);
}
this.orderBooks.set(symbol, orderbook);
return orderbook;
}
private updateFromTrade(symbol: string, trade: Trade): OrderBookSnapshot | null {
const orderbook = this.orderBooks.get(symbol);
if (!orderbook) return null;
// Update last trade
this.lastTrades.set(symbol, trade);
// Adjust orderbook based on trade
// Large trades likely consumed liquidity
const impactFactor = Math.min(trade.size / 1000, 0.1); // Max 10% impact
if (trade.side === 'buy') {
// Buy trade consumed ask liquidity
orderbook.asks = orderbook.asks.map((level, i) => ({
...level,
size: level.size * (1 - impactFactor * Math.exp(-i * 0.5))
}));
} else {
// Sell trade consumed bid liquidity
orderbook.bids = orderbook.bids.map((level, i) => ({
...level,
size: level.size * (1 - impactFactor * Math.exp(-i * 0.5))
}));
}
return orderbook;
}
private reconstructFromBar(symbol: string, bar: Bar): OrderBookSnapshot {
const microstructure = this.microstructures.get(symbol) || this.createDefaultMicrostructure(symbol);
// Estimate spread from high-low range
const hlSpread = (bar.high - bar.low) / bar.close;
const estimatedSpreadBps = Math.max(
microstructure.avgSpreadBps,
hlSpread * 10000 * 0.1 // 10% of HL range as spread estimate
);
// Create synthetic quote
const midPrice = bar.vwap || (bar.high + bar.low + bar.close) / 3;
const halfSpread = midPrice * estimatedSpreadBps / 20000;
const quote: Quote = {
bid: midPrice - halfSpread,
ask: midPrice + halfSpread,
bidSize: bar.volume / 100, // Rough estimate
askSize: bar.volume / 100
};
return this.createOrderBook(symbol, quote, microstructure);
}
private createOrderBook(
symbol: string,
topQuote: Quote,
microstructure?: MarketMicrostructure
): OrderBookSnapshot {
const micro = microstructure || this.createDefaultMicrostructure(symbol);
const levels = 10;
const bids: PriceLevel[] = [];
const asks: PriceLevel[] = [];
// Model order book depth
for (let i = 0; i < levels; i++) {
const depthFactor = Math.exp(-i * 0.3); // Exponential decay
const spreadMultiplier = 1 + i * 0.1; // Wider spread at deeper levels
// Hidden liquidity modeling
const hiddenRatio = this.config.modelHiddenLiquidity ?
1 + Math.random() * 2 : // 1-3x visible size hidden
1;
// Bid levels
const bidPrice = topQuote.bid - (i * micro.tickSize);
const bidSize = topQuote.bidSize * depthFactor * (0.8 + Math.random() * 0.4);
bids.push({
price: bidPrice,
size: Math.round(bidSize / micro.lotSize) * micro.lotSize,
orderCount: Math.max(1, Math.floor(bidSize / 100)),
hiddenSize: this.config.modelHiddenLiquidity ? bidSize * (hiddenRatio - 1) : undefined
});
// Ask levels
const askPrice = topQuote.ask + (i * micro.tickSize);
const askSize = topQuote.askSize * depthFactor * (0.8 + Math.random() * 0.4);
asks.push({
price: askPrice,
size: Math.round(askSize / micro.lotSize) * micro.lotSize,
orderCount: Math.max(1, Math.floor(askSize / 100)),
hiddenSize: this.config.modelHiddenLiquidity ? askSize * (hiddenRatio - 1) : undefined
});
}
return {
symbol,
timestamp: new Date(),
bids,
asks,
lastTrade: this.lastTrades.get(symbol)
};
}
private updateOrderBook(
current: OrderBookSnapshot,
quote: Quote,
microstructure?: MarketMicrostructure
): OrderBookSnapshot {
const micro = microstructure || this.createDefaultMicrostructure(current.symbol);
// Update top of book
if (current.bids.length > 0) {
current.bids[0].price = quote.bid;
current.bids[0].size = quote.bidSize;
}
if (current.asks.length > 0) {
current.asks[0].price = quote.ask;
current.asks[0].size = quote.askSize;
}
// Adjust deeper levels based on spread changes
const oldSpread = current.asks[0].price - current.bids[0].price;
const newSpread = quote.ask - quote.bid;
const spreadRatio = newSpread / oldSpread;
// Update deeper levels
for (let i = 1; i < current.bids.length; i++) {
// Adjust sizes based on top of book changes
const sizeRatio = quote.bidSize / (current.bids[0].size || quote.bidSize);
current.bids[i].size *= sizeRatio * (0.9 + Math.random() * 0.2);
// Adjust prices to maintain relative spacing
const spacing = (current.bids[i-1].price - current.bids[i].price) * spreadRatio;
current.bids[i].price = current.bids[i-1].price - spacing;
}
for (let i = 1; i < current.asks.length; i++) {
const sizeRatio = quote.askSize / (current.asks[0].size || quote.askSize);
current.asks[i].size *= sizeRatio * (0.9 + Math.random() * 0.2);
const spacing = (current.asks[i].price - current.asks[i-1].price) * spreadRatio;
current.asks[i].price = current.asks[i-1].price + spacing;
}
return current;
}
simulateMarketImpact(
symbol: string,
side: 'buy' | 'sell',
orderSize: number,
orderType: 'market' | 'limit',
limitPrice?: number
): {
fills: Array<{ price: number; size: number; venue: string }>;
totalCost: number;
avgPrice: number;
marketImpact: number;
fees: number;
} {
const orderbook = this.orderBooks.get(symbol);
const microstructure = this.microstructures.get(symbol);
const liquidityProfile = this.liquidityProfiles.get(symbol);
if (!orderbook) {
throw new Error(`No orderbook available for ${symbol}`);
}
const fills: Array<{ price: number; size: number; venue: string }> = [];
let remainingSize = orderSize;
let totalCost = 0;
let fees = 0;
// Get relevant price levels
const levels = side === 'buy' ? orderbook.asks : orderbook.bids;
const multiplier = side === 'buy' ? 1 : -1;
// Simulate walking the book
for (const level of levels) {
if (remainingSize <= 0) break;
// Check limit price constraint
if (limitPrice !== undefined) {
if (side === 'buy' && level.price > limitPrice) break;
if (side === 'sell' && level.price < limitPrice) break;
}
// Calculate available liquidity including hidden
let availableSize = level.size;
if (level.hiddenSize && this.config.modelHiddenLiquidity) {
availableSize += level.hiddenSize * Math.random(); // Hidden liquidity probabilistic
}
const fillSize = Math.min(remainingSize, availableSize);
// Simulate latency - price might move
if (this.config.latencyMs > 0 && orderType === 'market') {
const priceMovement = microstructure ?
(Math.random() - 0.5) * microstructure.avgSpreadBps / 10000 * level.price :
0;
level.price += priceMovement * multiplier;
}
fills.push({
price: level.price,
size: fillSize,
venue: 'primary'
});
totalCost += fillSize * level.price;
remainingSize -= fillSize;
// Calculate fees
if (orderType === 'market') {
fees += fillSize * level.price * this.config.takeFeeRate;
} else {
// Limit orders that provide liquidity get rebate
fees += fillSize * level.price * this.config.rebateRate;
}
}
// Dark pool execution for remaining size
if (remainingSize > 0 && this.config.includeDarkPools && liquidityProfile) {
const darkPoolPct = liquidityProfile.darkPoolLiquidity / liquidityProfile.totalLiquidity;
const darkPoolSize = remainingSize * darkPoolPct * Math.random();
if (darkPoolSize > 0) {
const midPrice = (orderbook.bids[0].price + orderbook.asks[0].price) / 2;
fills.push({
price: midPrice,
size: darkPoolSize,
venue: 'dark'
});
totalCost += darkPoolSize * midPrice;
remainingSize -= darkPoolSize;
// Dark pools typically have lower fees
fees += darkPoolSize * midPrice * 0.0001;
}
}
// Calculate results
const filledSize = orderSize - remainingSize;
const avgPrice = filledSize > 0 ? totalCost / filledSize : 0;
// Calculate market impact
const initialMid = (orderbook.bids[0].price + orderbook.asks[0].price) / 2;
const marketImpact = filledSize > 0 ?
Math.abs(avgPrice - initialMid) / initialMid * 10000 : // in bps
0;
return {
fills,
totalCost,
avgPrice,
marketImpact,
fees
};
}
private updateLiquidityProfile(symbol: string): void {
const microstructure = this.microstructures.get(symbol);
if (!microstructure) return;
// Estimate liquidity distribution
const visiblePct = 0.3; // 30% visible
const hiddenPct = 0.5; // 50% hidden
const darkPct = 0.2; // 20% dark
const totalDailyLiquidity = microstructure.dailyVolume;
this.liquidityProfiles.set(symbol, {
visibleLiquidity: totalDailyLiquidity * visiblePct,
hiddenLiquidity: totalDailyLiquidity * hiddenPct,
darkPoolLiquidity: totalDailyLiquidity * darkPct,
totalLiquidity: totalDailyLiquidity
});
}
private createDefaultMicrostructure(symbol: string): MarketMicrostructure {
return {
symbol,
avgSpreadBps: 5,
dailyVolume: 1000000,
avgTradeSize: 100,
volatility: 0.02,
tickSize: 0.01,
lotSize: 1,
intradayVolumeProfile: new Array(24).fill(1/24)
};
}
private getSymbolFromData(data: MarketData): { symbol: string } {
return { symbol: data.data.symbol };
}
getOrderBook(symbol: string): OrderBookSnapshot | undefined {
return this.orderBooks.get(symbol);
}
getLiquidityProfile(symbol: string): LiquidityProfile | undefined {
return this.liquidityProfiles.get(symbol);
}
reset(): void {
this.orderBooks.clear();
this.lastTrades.clear();
}
}

View file

@ -0,0 +1,47 @@
import { Container } from '@stock-bot/di';
import { logger } from '@stock-bot/logger';
import { ModeManager } from './core/ModeManager';
import { MarketDataService } from './services/MarketDataService';
import { ExecutionService } from './services/ExecutionService';
import { AnalyticsService } from './services/AnalyticsService';
import { StorageService } from './services/StorageService';
import { StrategyManager } from './strategies/StrategyManager';
import { BacktestEngine } from './backtest/BacktestEngine';
import { PaperTradingManager } from './paper/PaperTradingManager';
// Create and configure the DI container
export const container = new Container();
// Register core services
container.singleton('Logger', () => logger);
container.singleton('ModeManager', () => new ModeManager(
container.get('MarketDataService'),
container.get('ExecutionService'),
container.get('StorageService')
));
container.singleton('MarketDataService', () => new MarketDataService());
container.singleton('ExecutionService', () => new ExecutionService(
container.get('ModeManager')
));
container.singleton('AnalyticsService', () => new AnalyticsService());
container.singleton('StorageService', () => new StorageService());
container.singleton('StrategyManager', () => new StrategyManager(
container.get('ModeManager'),
container.get('MarketDataService'),
container.get('ExecutionService')
));
container.singleton('BacktestEngine', () => new BacktestEngine(
container.get('StorageService'),
container.get('StrategyManager')
));
container.singleton('PaperTradingManager', () => new PaperTradingManager(
container.get('ExecutionService')
));

View file

@ -0,0 +1,162 @@
import { logger } from '@stock-bot/logger';
import { TradingEngine } from '../../core';
import { TradingMode, ModeConfig, BacktestConfigSchema, PaperConfigSchema, LiveConfigSchema } from '../types';
import { MarketDataService } from '../services/MarketDataService';
import { ExecutionService } from '../services/ExecutionService';
import { StorageService } from '../services/StorageService';
import { EventEmitter } from 'events';
export class ModeManager extends EventEmitter {
private mode: TradingMode = 'paper';
private config: ModeConfig | null = null;
private tradingEngine: TradingEngine | null = null;
private isInitialized = false;
constructor(
private marketDataService: MarketDataService,
private executionService: ExecutionService,
private storageService: StorageService
) {
super();
}
async initializeMode(config: ModeConfig): Promise<void> {
// Validate config based on mode
switch (config.mode) {
case 'backtest':
BacktestConfigSchema.parse(config);
break;
case 'paper':
PaperConfigSchema.parse(config);
break;
case 'live':
LiveConfigSchema.parse(config);
break;
}
// Shutdown current mode if initialized
if (this.isInitialized) {
await this.shutdown();
}
this.mode = config.mode;
this.config = config;
// Create Rust trading engine with appropriate config
const engineConfig = this.createEngineConfig(config);
this.tradingEngine = new TradingEngine(config.mode, engineConfig);
// Initialize services for the mode
await this.initializeServices(config);
this.isInitialized = true;
this.emit('modeChanged', config);
logger.info(`Trading mode initialized: ${config.mode}`);
}
private createEngineConfig(config: ModeConfig): any {
switch (config.mode) {
case 'backtest':
return {
startTime: new Date(config.startDate).getTime(),
endTime: new Date(config.endDate).getTime(),
speedMultiplier: this.getSpeedMultiplier(config.speed)
};
case 'paper':
return {
startingCapital: config.startingCapital
};
case 'live':
return {
broker: config.broker,
accountId: config.accountId
};
}
}
private getSpeedMultiplier(speed: string): number {
switch (speed) {
case 'max': return 0;
case 'realtime': return 1;
case '2x': return 2;
case '5x': return 5;
case '10x': return 10;
default: return 0;
}
}
private async initializeServices(config: ModeConfig): Promise<void> {
// Configure market data service
await this.marketDataService.initialize(config);
// Configure execution service
await this.executionService.initialize(config, this.tradingEngine!);
// Configure storage
await this.storageService.initialize(config);
}
getCurrentMode(): TradingMode {
return this.mode;
}
getConfig(): ModeConfig | null {
return this.config;
}
getTradingEngine(): TradingEngine {
if (!this.tradingEngine) {
throw new Error('Trading engine not initialized');
}
return this.tradingEngine;
}
isBacktestMode(): boolean {
return this.mode === 'backtest';
}
isPaperMode(): boolean {
return this.mode === 'paper';
}
isLiveMode(): boolean {
return this.mode === 'live';
}
async transitionMode(fromMode: TradingMode, toMode: TradingMode, config: ModeConfig): Promise<void> {
if (fromMode === 'paper' && toMode === 'live') {
// Special handling for paper to live transition
logger.info('Transitioning from paper to live trading...');
// 1. Get current paper positions
const paperPositions = await this.tradingEngine!.getOpenPositions();
// 2. Initialize new mode
await this.initializeMode(config);
// 3. Reconcile positions (this would be handled by a reconciliation service)
logger.info(`Paper positions to reconcile: ${paperPositions}`);
} else {
// Standard mode switch
await this.initializeMode(config);
}
}
async shutdown(): Promise<void> {
if (!this.isInitialized) return;
logger.info(`Shutting down ${this.mode} mode...`);
// Shutdown services
await this.marketDataService.shutdown();
await this.executionService.shutdown();
await this.storageService.shutdown();
// Cleanup trading engine
this.tradingEngine = null;
this.isInitialized = false;
this.emit('shutdown');
}
}

View file

@ -0,0 +1,435 @@
import { logger } from '@stock-bot/logger';
import { StorageService } from '../services/StorageService';
import { MarketData, Bar } from '../types';
import { EventEmitter } from 'events';
export interface DataResolution {
interval: string;
milliseconds: number;
}
export interface CorporateAction {
symbol: string;
date: Date;
type: 'split' | 'dividend' | 'spinoff';
factor?: number;
amount?: number;
newSymbol?: string;
}
export interface DataQualityIssue {
timestamp: Date;
symbol: string;
issue: string;
severity: 'warning' | 'error';
details?: any;
}
export class DataManager extends EventEmitter {
private static RESOLUTIONS: Record<string, DataResolution> = {
'tick': { interval: 'tick', milliseconds: 0 },
'1s': { interval: '1s', milliseconds: 1000 },
'5s': { interval: '5s', milliseconds: 5000 },
'10s': { interval: '10s', milliseconds: 10000 },
'30s': { interval: '30s', milliseconds: 30000 },
'1m': { interval: '1m', milliseconds: 60000 },
'5m': { interval: '5m', milliseconds: 300000 },
'15m': { interval: '15m', milliseconds: 900000 },
'30m': { interval: '30m', milliseconds: 1800000 },
'1h': { interval: '1h', milliseconds: 3600000 },
'4h': { interval: '4h', milliseconds: 14400000 },
'1d': { interval: '1d', milliseconds: 86400000 },
};
private dataCache: Map<string, MarketData[]> = new Map();
private aggregatedCache: Map<string, Map<string, Bar[]>> = new Map();
private corporateActions: Map<string, CorporateAction[]> = new Map();
private dataQualityIssues: DataQualityIssue[] = [];
constructor(private storageService: StorageService) {
super();
}
async loadHistoricalData(
symbols: string[],
startDate: Date,
endDate: Date,
resolution: string = '1m',
includeExtendedHours: boolean = false
): Promise<Map<string, MarketData[]>> {
const result = new Map<string, MarketData[]>();
for (const symbol of symbols) {
try {
// Load raw data
const data = await this.storageService.getHistoricalBars(
symbol,
startDate,
endDate,
resolution
);
// Apply corporate actions
const adjustedData = await this.applyCorporateActions(symbol, data, startDate, endDate);
// Quality checks
const cleanedData = this.performQualityChecks(symbol, adjustedData);
// Convert to MarketData format
const marketData = this.convertToMarketData(symbol, cleanedData);
result.set(symbol, marketData);
this.dataCache.set(`${symbol}:${resolution}`, marketData);
logger.info(`Loaded ${marketData.length} bars for ${symbol} at ${resolution} resolution`);
} catch (error) {
logger.error(`Failed to load data for ${symbol}:`, error);
this.emit('dataError', { symbol, error });
}
}
return result;
}
async applyCorporateActions(
symbol: string,
data: any[],
startDate: Date,
endDate: Date
): Promise<any[]> {
// Load corporate actions for the period
const actions = await this.loadCorporateActions(symbol, startDate, endDate);
if (actions.length === 0) return data;
// Sort actions by date (newest first)
actions.sort((a, b) => b.date.getTime() - a.date.getTime());
// Apply adjustments
return data.map(bar => {
const barDate = new Date(bar.timestamp);
let adjustedBar = { ...bar };
for (const action of actions) {
if (barDate < action.date) {
switch (action.type) {
case 'split':
if (action.factor) {
adjustedBar.open /= action.factor;
adjustedBar.high /= action.factor;
adjustedBar.low /= action.factor;
adjustedBar.close /= action.factor;
adjustedBar.volume *= action.factor;
}
break;
case 'dividend':
if (action.amount) {
// Adjust for dividends (simplified)
const adjustment = 1 - (action.amount / adjustedBar.close);
adjustedBar.open *= adjustment;
adjustedBar.high *= adjustment;
adjustedBar.low *= adjustment;
adjustedBar.close *= adjustment;
}
break;
}
}
}
return adjustedBar;
});
}
performQualityChecks(symbol: string, data: any[]): any[] {
const cleaned: any[] = [];
for (let i = 0; i < data.length; i++) {
const bar = data[i];
const prevBar = i > 0 ? data[i - 1] : null;
const issues: string[] = [];
// Check for missing data
if (!bar.open || !bar.high || !bar.low || !bar.close || bar.volume === undefined) {
issues.push('Missing OHLCV data');
}
// Check for invalid prices
if (bar.low > bar.high) {
issues.push('Low > High');
}
if (bar.open > bar.high || bar.open < bar.low) {
issues.push('Open outside High/Low range');
}
if (bar.close > bar.high || bar.close < bar.low) {
issues.push('Close outside High/Low range');
}
// Check for zero or negative prices
if (bar.open <= 0 || bar.high <= 0 || bar.low <= 0 || bar.close <= 0) {
issues.push('Zero or negative prices');
}
// Check for extreme price movements (>20% in one bar)
if (prevBar) {
const priceChange = Math.abs((bar.close - prevBar.close) / prevBar.close);
if (priceChange > 0.2) {
issues.push(`Extreme price movement: ${(priceChange * 100).toFixed(1)}%`);
}
}
// Check for volume spikes (>10x average)
if (i >= 20) {
const avgVolume = data.slice(i - 20, i)
.reduce((sum, b) => sum + b.volume, 0) / 20;
if (bar.volume > avgVolume * 10) {
issues.push('Volume spike detected');
}
}
// Handle issues
if (issues.length > 0) {
const severity = issues.some(issue =>
issue.includes('Missing') || issue.includes('Zero')
) ? 'error' : 'warning';
this.dataQualityIssues.push({
timestamp: new Date(bar.timestamp),
symbol,
issue: issues.join(', '),
severity,
details: bar
});
// For errors, try to interpolate or skip
if (severity === 'error') {
if (prevBar && i < data.length - 1) {
// Interpolate from surrounding bars
const nextBar = data[i + 1];
cleaned.push({
...bar,
open: (prevBar.close + nextBar.open) / 2,
high: Math.max(prevBar.high, nextBar.high) * 0.9,
low: Math.min(prevBar.low, nextBar.low) * 1.1,
close: (prevBar.close + nextBar.close) / 2,
volume: (prevBar.volume + nextBar.volume) / 2,
interpolated: true
});
}
// Skip if we can't interpolate
continue;
}
}
cleaned.push(bar);
}
return cleaned;
}
aggregateData(
data: MarketData[],
fromResolution: string,
toResolution: string
): Bar[] {
const fromMs = DataManager.RESOLUTIONS[fromResolution]?.milliseconds;
const toMs = DataManager.RESOLUTIONS[toResolution]?.milliseconds;
if (!fromMs || !toMs || fromMs >= toMs) {
throw new Error(`Cannot aggregate from ${fromResolution} to ${toResolution}`);
}
const bars: Bar[] = [];
let currentBar: Partial<Bar> | null = null;
let barStartTime = 0;
for (const item of data) {
if (item.type !== 'bar') continue;
const bar = item.data;
const timestamp = bar.timestamp;
const alignedTime = Math.floor(timestamp / toMs) * toMs;
if (!currentBar || alignedTime > barStartTime) {
// Finalize previous bar
if (currentBar && currentBar.open !== undefined) {
bars.push(currentBar as Bar);
}
// Start new bar
currentBar = {
timestamp: alignedTime,
open: bar.open,
high: bar.high,
low: bar.low,
close: bar.close,
volume: bar.volume,
vwap: bar.vwap
};
barStartTime = alignedTime;
} else {
// Update current bar
currentBar.high = Math.max(currentBar.high!, bar.high);
currentBar.low = Math.min(currentBar.low!, bar.low);
currentBar.close = bar.close;
currentBar.volume! += bar.volume;
// Recalculate VWAP if available
if (bar.vwap && currentBar.vwap) {
const totalValue = (currentBar.vwap * (currentBar.volume! - bar.volume)) +
(bar.vwap * bar.volume);
currentBar.vwap = totalValue / currentBar.volume!;
}
}
}
// Add final bar
if (currentBar && currentBar.open !== undefined) {
bars.push(currentBar as Bar);
}
return bars;
}
downsampleData(
data: MarketData[],
targetPoints: number
): MarketData[] {
if (data.length <= targetPoints) return data;
// Use LTTB (Largest Triangle Three Buckets) algorithm
const downsampled: MarketData[] = [];
const bucketSize = (data.length - 2) / (targetPoints - 2);
// Always include first point
downsampled.push(data[0]);
for (let i = 0; i < targetPoints - 2; i++) {
const bucketStart = Math.floor((i) * bucketSize) + 1;
const bucketEnd = Math.floor((i + 1) * bucketSize) + 1;
// Find point with maximum area in bucket
let maxArea = -1;
let maxAreaPoint = 0;
const prevPoint = downsampled[downsampled.length - 1];
const prevTime = prevPoint.data.timestamp;
const prevPrice = this.getPrice(prevPoint);
// Calculate average of next bucket for area calculation
let nextBucketStart = Math.floor((i + 1) * bucketSize) + 1;
let nextBucketEnd = Math.floor((i + 2) * bucketSize) + 1;
if (nextBucketEnd >= data.length) {
nextBucketEnd = data.length - 1;
}
let avgTime = 0;
let avgPrice = 0;
for (let j = nextBucketStart; j < nextBucketEnd; j++) {
avgTime += data[j].data.timestamp;
avgPrice += this.getPrice(data[j]);
}
avgTime /= (nextBucketEnd - nextBucketStart);
avgPrice /= (nextBucketEnd - nextBucketStart);
// Find point with max area
for (let j = bucketStart; j < bucketEnd && j < data.length; j++) {
const time = data[j].data.timestamp;
const price = this.getPrice(data[j]);
// Calculate triangle area
const area = Math.abs(
(prevTime - avgTime) * (price - prevPrice) -
(prevTime - time) * (avgPrice - prevPrice)
);
if (area > maxArea) {
maxArea = area;
maxAreaPoint = j;
}
}
downsampled.push(data[maxAreaPoint]);
}
// Always include last point
downsampled.push(data[data.length - 1]);
return downsampled;
}
private getPrice(data: MarketData): number {
switch (data.type) {
case 'bar':
return data.data.close;
case 'trade':
return data.data.price;
case 'quote':
return (data.data.bid + data.data.ask) / 2;
default:
return 0;
}
}
private convertToMarketData(symbol: string, bars: any[]): MarketData[] {
return bars.map(bar => ({
type: 'bar' as const,
data: {
symbol,
open: bar.open,
high: bar.high,
low: bar.low,
close: bar.close,
volume: bar.volume,
vwap: bar.vwap,
timestamp: new Date(bar.timestamp).getTime(),
interpolated: bar.interpolated
}
}));
}
private async loadCorporateActions(
symbol: string,
startDate: Date,
endDate: Date
): Promise<CorporateAction[]> {
// Check cache first
const cached = this.corporateActions.get(symbol);
if (cached) {
return cached.filter(action =>
action.date >= startDate && action.date <= endDate
);
}
// In real implementation, load from database
// For now, return empty array
return [];
}
getDataQualityReport(): {
totalIssues: number;
bySymbol: Record<string, number>;
bySeverity: Record<string, number>;
issues: DataQualityIssue[];
} {
const bySymbol: Record<string, number> = {};
const bySeverity: Record<string, number> = { warning: 0, error: 0 };
for (const issue of this.dataQualityIssues) {
bySymbol[issue.symbol] = (bySymbol[issue.symbol] || 0) + 1;
bySeverity[issue.severity]++;
}
return {
totalIssues: this.dataQualityIssues.length,
bySymbol,
bySeverity,
issues: this.dataQualityIssues
};
}
clearCache(): void {
this.dataCache.clear();
this.aggregatedCache.clear();
this.dataQualityIssues = [];
}
}

View file

@ -0,0 +1,83 @@
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { Server as SocketIOServer } from 'socket.io';
import { createServer } from 'http';
import { logger } from '@stock-bot/logger';
import { ModeManager } from './core/ModeManager';
import { createOrderRoutes } from './api/rest/orders';
import { createPositionRoutes } from './api/rest/positions';
import { createAnalyticsRoutes } from './api/rest/analytics';
import { createBacktestRoutes } from './api/rest/backtest';
import { setupWebSocketHandlers } from './api/websocket';
import { container } from './container';
const PORT = process.env.PORT || 3002;
async function main() {
// Initialize Hono app
const app = new Hono();
// Middleware
app.use('*', cors());
app.use('*', async (c, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
logger.debug(`${c.req.method} ${c.req.url} - ${ms}ms`);
});
// Health check
app.get('/health', (c) => {
const modeManager = container.get('ModeManager');
return c.json({
status: 'healthy',
mode: modeManager.getCurrentMode(),
timestamp: new Date().toISOString()
});
});
// Mount routes
app.route('/api/orders', createOrderRoutes());
app.route('/api/positions', createPositionRoutes());
app.route('/api/analytics', createAnalyticsRoutes());
app.route('/api/backtest', createBacktestRoutes());
// Create HTTP server and Socket.IO
const server = createServer(app.fetch);
const io = new SocketIOServer(server, {
cors: {
origin: '*',
methods: ['GET', 'POST']
}
});
// Setup WebSocket handlers
setupWebSocketHandlers(io, container);
// Initialize mode manager
const modeManager = container.get('ModeManager') as ModeManager;
// Default to paper trading mode
await modeManager.initializeMode({
mode: 'paper',
startingCapital: 100000
});
// Start server
server.listen(PORT, () => {
logger.info(`Trading orchestrator running on port ${PORT}`);
});
// Graceful shutdown
process.on('SIGINT', async () => {
logger.info('Shutting down trading orchestrator...');
await modeManager.shutdown();
server.close();
process.exit(0);
});
}
main().catch((error) => {
logger.error('Failed to start trading orchestrator:', error);
process.exit(1);
});

View file

@ -0,0 +1,367 @@
import { logger } from '@stock-bot/logger';
import { EventEmitter } from 'events';
import { OrderRequest, Position } from '../types';
import { ExecutionService } from '../services/ExecutionService';
interface VirtualAccount {
balance: number;
buyingPower: number;
positions: Map<string, VirtualPosition>;
orders: Map<string, VirtualOrder>;
trades: VirtualTrade[];
equity: number;
marginUsed: number;
}
interface VirtualPosition {
symbol: string;
quantity: number;
averagePrice: number;
marketValue: number;
unrealizedPnl: number;
realizedPnl: number;
}
interface VirtualOrder {
id: string;
symbol: string;
side: 'buy' | 'sell';
quantity: number;
orderType: string;
limitPrice?: number;
status: string;
submittedAt: Date;
}
interface VirtualTrade {
orderId: string;
symbol: string;
side: 'buy' | 'sell';
quantity: number;
price: number;
commission: number;
timestamp: Date;
pnl?: number;
}
export class PaperTradingManager extends EventEmitter {
private account: VirtualAccount;
private marketPrices = new Map<string, { bid: number; ask: number }>();
private readonly COMMISSION_RATE = 0.001; // 0.1%
private readonly MARGIN_REQUIREMENT = 0.25; // 25% margin requirement
constructor(
private executionService: ExecutionService,
initialBalance: number = 100000
) {
super();
this.account = {
balance: initialBalance,
buyingPower: initialBalance * (1 / this.MARGIN_REQUIREMENT),
positions: new Map(),
orders: new Map(),
trades: [],
equity: initialBalance,
marginUsed: 0
};
this.setupEventListeners();
}
private setupEventListeners(): void {
// Listen for market data updates to track prices
// In real implementation, would connect to market data service
}
updateMarketPrice(symbol: string, bid: number, ask: number): void {
this.marketPrices.set(symbol, { bid, ask });
// Update position values
const position = this.account.positions.get(symbol);
if (position) {
const midPrice = (bid + ask) / 2;
position.marketValue = position.quantity * midPrice;
position.unrealizedPnl = position.quantity * (midPrice - position.averagePrice);
}
// Update account equity
this.updateAccountEquity();
}
async executeOrder(order: OrderRequest): Promise<any> {
// Validate order
const validation = this.validateOrder(order);
if (!validation.valid) {
return {
status: 'rejected',
reason: validation.reason
};
}
// Check buying power
const requiredCapital = this.calculateRequiredCapital(order);
if (requiredCapital > this.account.buyingPower) {
return {
status: 'rejected',
reason: 'Insufficient buying power'
};
}
// Create virtual order
const virtualOrder: VirtualOrder = {
id: `paper_${Date.now()}`,
symbol: order.symbol,
side: order.side,
quantity: order.quantity,
orderType: order.orderType,
limitPrice: order.limitPrice,
status: 'pending',
submittedAt: new Date()
};
this.account.orders.set(virtualOrder.id, virtualOrder);
// Simulate order execution based on type
if (order.orderType === 'market') {
await this.executeMarketOrder(virtualOrder);
} else if (order.orderType === 'limit') {
// Limit orders would be checked periodically
virtualOrder.status = 'accepted';
}
return {
orderId: virtualOrder.id,
status: virtualOrder.status
};
}
private async executeMarketOrder(order: VirtualOrder): Promise<void> {
const marketPrice = this.marketPrices.get(order.symbol);
if (!marketPrice) {
order.status = 'rejected';
this.emit('orderUpdate', {
orderId: order.id,
status: 'rejected',
reason: 'No market data available'
});
return;
}
// Simulate realistic fill with slippage
const fillPrice = order.side === 'buy'
? marketPrice.ask * (1 + this.getSlippage(order.quantity))
: marketPrice.bid * (1 - this.getSlippage(order.quantity));
const commission = fillPrice * order.quantity * this.COMMISSION_RATE;
// Create trade
const trade: VirtualTrade = {
orderId: order.id,
symbol: order.symbol,
side: order.side,
quantity: order.quantity,
price: fillPrice,
commission,
timestamp: new Date()
};
// Update position
this.updatePosition(trade);
// Update account
const totalCost = (fillPrice * order.quantity) + commission;
if (order.side === 'buy') {
this.account.balance -= totalCost;
} else {
this.account.balance += (fillPrice * order.quantity) - commission;
}
// Record trade
this.account.trades.push(trade);
order.status = 'filled';
// Update buying power and margin
this.updateBuyingPower();
// Emit events
this.emit('fill', {
orderId: order.id,
symbol: order.symbol,
side: order.side,
quantity: order.quantity,
price: fillPrice,
commission,
timestamp: new Date()
});
this.emit('orderUpdate', {
orderId: order.id,
status: 'filled'
});
}
private updatePosition(trade: VirtualTrade): void {
const position = this.account.positions.get(trade.symbol) || {
symbol: trade.symbol,
quantity: 0,
averagePrice: 0,
marketValue: 0,
unrealizedPnl: 0,
realizedPnl: 0
};
const oldQuantity = position.quantity;
const oldAvgPrice = position.averagePrice;
if (trade.side === 'buy') {
// Adding to position
const newQuantity = oldQuantity + trade.quantity;
position.averagePrice = oldQuantity >= 0
? ((oldQuantity * oldAvgPrice) + (trade.quantity * trade.price)) / newQuantity
: trade.price;
position.quantity = newQuantity;
} else {
// Reducing position
const newQuantity = oldQuantity - trade.quantity;
if (oldQuantity > 0) {
// Realize P&L on closed portion
const realizedPnl = trade.quantity * (trade.price - oldAvgPrice) - trade.commission;
position.realizedPnl += realizedPnl;
trade.pnl = realizedPnl;
}
position.quantity = newQuantity;
if (Math.abs(newQuantity) < 0.0001) {
// Position closed
this.account.positions.delete(trade.symbol);
return;
}
}
this.account.positions.set(trade.symbol, position);
}
private validateOrder(order: OrderRequest): { valid: boolean; reason?: string } {
if (order.quantity <= 0) {
return { valid: false, reason: 'Invalid quantity' };
}
if (order.orderType === 'limit' && !order.limitPrice) {
return { valid: false, reason: 'Limit price required for limit orders' };
}
return { valid: true };
}
private calculateRequiredCapital(order: OrderRequest): number {
const marketPrice = this.marketPrices.get(order.symbol);
if (!marketPrice) return Infinity;
const price = order.side === 'buy' ? marketPrice.ask : marketPrice.bid;
const notional = price * order.quantity;
const commission = notional * this.COMMISSION_RATE;
const marginRequired = notional * this.MARGIN_REQUIREMENT;
return order.side === 'buy' ? marginRequired + commission : commission;
}
private updateBuyingPower(): void {
let totalMarginUsed = 0;
for (const position of this.account.positions.values()) {
totalMarginUsed += Math.abs(position.marketValue) * this.MARGIN_REQUIREMENT;
}
this.account.marginUsed = totalMarginUsed;
this.account.buyingPower = (this.account.equity - totalMarginUsed) / this.MARGIN_REQUIREMENT;
}
private updateAccountEquity(): void {
let totalUnrealizedPnl = 0;
for (const position of this.account.positions.values()) {
totalUnrealizedPnl += position.unrealizedPnl;
}
this.account.equity = this.account.balance + totalUnrealizedPnl;
}
private getSlippage(quantity: number): number {
// Simple slippage model - increases with order size
const baseSlippage = 0.0001; // 1 basis point
const sizeImpact = quantity / 10000; // Impact increases with size
return baseSlippage + (sizeImpact * 0.0001);
}
checkLimitOrders(): void {
// Called periodically to check if limit orders can be filled
for (const [orderId, order] of this.account.orders) {
if (order.status !== 'accepted' || order.orderType !== 'limit') continue;
const marketPrice = this.marketPrices.get(order.symbol);
if (!marketPrice) continue;
const canFill = order.side === 'buy'
? marketPrice.ask <= order.limitPrice!
: marketPrice.bid >= order.limitPrice!;
if (canFill) {
this.executeMarketOrder(order);
}
}
}
getAccount(): VirtualAccount {
return { ...this.account };
}
getPosition(symbol: string): VirtualPosition | undefined {
return this.account.positions.get(symbol);
}
getAllPositions(): VirtualPosition[] {
return Array.from(this.account.positions.values());
}
getPerformanceMetrics(): any {
const totalTrades = this.account.trades.length;
const winningTrades = this.account.trades.filter(t => t.pnl && t.pnl > 0);
const losingTrades = this.account.trades.filter(t => t.pnl && t.pnl < 0);
const totalPnl = this.account.trades.reduce((sum, t) => sum + (t.pnl || 0), 0);
const totalCommission = this.account.trades.reduce((sum, t) => sum + t.commission, 0);
return {
totalTrades,
winningTrades: winningTrades.length,
losingTrades: losingTrades.length,
winRate: totalTrades > 0 ? (winningTrades.length / totalTrades) * 100 : 0,
totalPnl,
totalCommission,
netPnl: totalPnl - totalCommission,
currentEquity: this.account.equity,
currentPositions: this.account.positions.size
};
}
reset(): void {
const initialBalance = this.account.balance +
Array.from(this.account.positions.values())
.reduce((sum, p) => sum + p.marketValue, 0);
this.account = {
balance: initialBalance,
buyingPower: initialBalance * (1 / this.MARGIN_REQUIREMENT),
positions: new Map(),
orders: new Map(),
trades: [],
equity: initialBalance,
marginUsed: 0
};
logger.info('Paper trading account reset');
}
}

View file

@ -0,0 +1,209 @@
import { logger } from '@stock-bot/logger';
import axios from 'axios';
import { PerformanceMetrics, RiskMetrics } from '../types';
interface OptimizationParams {
returns: number[][];
constraints?: {
minWeight?: number;
maxWeight?: number;
targetReturn?: number;
maxRisk?: number;
};
}
interface PortfolioWeights {
symbols: string[];
weights: number[];
expectedReturn: number;
expectedRisk: number;
sharpeRatio: number;
}
export class AnalyticsService {
private analyticsUrl: string;
private cache = new Map<string, { data: any; timestamp: number }>();
private readonly CACHE_TTL_MS = 60000; // 1 minute cache
constructor() {
this.analyticsUrl = process.env.ANALYTICS_SERVICE_URL || 'http://localhost:3003';
}
async getPerformanceMetrics(
portfolioId: string,
startDate: Date,
endDate: Date
): Promise<PerformanceMetrics> {
const cacheKey = `perf_${portfolioId}_${startDate.toISOString()}_${endDate.toISOString()}`;
const cached = this.getFromCache(cacheKey);
if (cached) return cached;
try {
const response = await axios.get(`${this.analyticsUrl}/analytics/performance/${portfolioId}`, {
params: {
start_date: startDate.toISOString(),
end_date: endDate.toISOString()
}
});
const metrics = response.data as PerformanceMetrics;
this.setCache(cacheKey, metrics);
return metrics;
} catch (error) {
logger.error('Error fetching performance metrics:', error);
// Return default metrics if analytics service is unavailable
return this.getDefaultPerformanceMetrics();
}
}
async optimizePortfolio(params: OptimizationParams): Promise<PortfolioWeights> {
try {
const response = await axios.post(`${this.analyticsUrl}/optimize/portfolio`, params);
return response.data as PortfolioWeights;
} catch (error) {
logger.error('Error optimizing portfolio:', error);
// Return equal weights as fallback
return this.getEqualWeights(params.returns[0].length);
}
}
async getRiskMetrics(portfolioId: string): Promise<RiskMetrics> {
const cacheKey = `risk_${portfolioId}`;
const cached = this.getFromCache(cacheKey);
if (cached) return cached;
try {
const response = await axios.get(`${this.analyticsUrl}/analytics/risk/${portfolioId}`);
const metrics = response.data as RiskMetrics;
this.setCache(cacheKey, metrics);
return metrics;
} catch (error) {
logger.error('Error fetching risk metrics:', error);
return this.getDefaultRiskMetrics();
}
}
async detectMarketRegime(): Promise<string> {
const cacheKey = 'market_regime';
const cached = this.getFromCache(cacheKey);
if (cached) return cached;
try {
const response = await axios.get(`${this.analyticsUrl}/analytics/regime`);
const regime = response.data.regime as string;
this.setCache(cacheKey, regime, 300000); // Cache for 5 minutes
return regime;
} catch (error) {
logger.error('Error detecting market regime:', error);
return 'normal'; // Default regime
}
}
async calculateCorrelationMatrix(symbols: string[]): Promise<number[][]> {
try {
const response = await axios.post(`${this.analyticsUrl}/analytics/correlation`, { symbols });
return response.data.matrix as number[][];
} catch (error) {
logger.error('Error calculating correlation matrix:', error);
// Return identity matrix as fallback
return this.getIdentityMatrix(symbols.length);
}
}
async runBacktestAnalysis(backtestId: string): Promise<any> {
try {
const response = await axios.get(`${this.analyticsUrl}/analytics/backtest/${backtestId}`);
return response.data;
} catch (error) {
logger.error('Error running backtest analysis:', error);
return null;
}
}
async predictWithModel(modelId: string, features: Record<string, number>): Promise<any> {
try {
const response = await axios.post(`${this.analyticsUrl}/models/predict`, {
model_id: modelId,
features
});
return response.data;
} catch (error) {
logger.error('Error getting model prediction:', error);
return null;
}
}
// Cache management
private getFromCache(key: string): any | null {
const cached = this.cache.get(key);
if (!cached) return null;
const now = Date.now();
if (now - cached.timestamp > this.CACHE_TTL_MS) {
this.cache.delete(key);
return null;
}
return cached.data;
}
private setCache(key: string, data: any, ttl?: number): void {
this.cache.set(key, {
data,
timestamp: Date.now()
});
// Auto-cleanup after TTL
setTimeout(() => {
this.cache.delete(key);
}, ttl || this.CACHE_TTL_MS);
}
// Fallback methods when analytics service is unavailable
private getDefaultPerformanceMetrics(): PerformanceMetrics {
return {
totalReturn: 0,
sharpeRatio: 0,
sortinoRatio: 0,
maxDrawdown: 0,
winRate: 0,
profitFactor: 0,
avgWin: 0,
avgLoss: 0,
totalTrades: 0
};
}
private getDefaultRiskMetrics(): RiskMetrics {
return {
currentExposure: 0,
dailyPnl: 0,
positionCount: 0,
grossExposure: 0,
var95: 0,
cvar95: 0
};
}
private getEqualWeights(n: number): PortfolioWeights {
const weight = 1 / n;
return {
symbols: Array(n).fill('').map((_, i) => `Asset${i + 1}`),
weights: Array(n).fill(weight),
expectedReturn: 0,
expectedRisk: 0,
sharpeRatio: 0
};
}
private getIdentityMatrix(n: number): number[][] {
const matrix: number[][] = [];
for (let i = 0; i < n; i++) {
matrix[i] = [];
for (let j = 0; j < n; j++) {
matrix[i][j] = i === j ? 1 : 0;
}
}
return matrix;
}
}

View file

@ -0,0 +1,312 @@
import { logger } from '@stock-bot/logger';
import { EventEmitter } from 'events';
import { v4 as uuidv4 } from 'uuid';
import { ModeConfig, OrderRequest, OrderRequestSchema } from '../types';
import { TradingEngine } from '../../core';
import axios from 'axios';
interface ExecutionReport {
orderId: string;
clientOrderId: string;
symbol: string;
side: 'buy' | 'sell';
quantity: number;
status: 'pending' | 'accepted' | 'partiallyFilled' | 'filled' | 'cancelled' | 'rejected';
fills: Fill[];
rejectionReason?: string;
timestamp: number;
}
interface Fill {
price: number;
quantity: number;
commission: number;
timestamp: number;
}
export class ExecutionService extends EventEmitter {
private mode: 'backtest' | 'paper' | 'live' = 'paper';
private tradingEngine: TradingEngine | null = null;
private brokerClient: any = null; // Would be specific broker API client
private pendingOrders = new Map<string, OrderRequest>();
constructor(private modeManager: any) {
super();
}
async initialize(config: ModeConfig, tradingEngine: TradingEngine): Promise<void> {
this.mode = config.mode;
this.tradingEngine = tradingEngine;
if (config.mode === 'live') {
// Initialize broker connection
await this.initializeBroker(config.broker, config.accountId);
}
}
private async initializeBroker(broker: string, accountId: string): Promise<void> {
// In real implementation, would initialize specific broker API
// For example: Alpaca, Interactive Brokers, etc.
logger.info(`Initializing ${broker} broker connection for account ${accountId}`);
}
async submitOrder(orderRequest: OrderRequest): Promise<ExecutionReport> {
// Validate order request
const validatedOrder = OrderRequestSchema.parse(orderRequest);
// Generate order ID
const orderId = uuidv4();
const clientOrderId = validatedOrder.clientOrderId || orderId;
// Store pending order
this.pendingOrders.set(orderId, validatedOrder);
try {
// Check risk before submitting
const riskResult = await this.checkRisk(validatedOrder);
if (!riskResult.passed) {
return this.createRejectionReport(
orderId,
clientOrderId,
validatedOrder,
`Risk check failed: ${riskResult.violations.join(', ')}`
);
}
// Submit based on mode
let result: ExecutionReport;
switch (this.mode) {
case 'backtest':
case 'paper':
result = await this.submitToSimulation(orderId, clientOrderId, validatedOrder);
break;
case 'live':
result = await this.submitToBroker(orderId, clientOrderId, validatedOrder);
break;
}
// Emit order event
this.emit('orderUpdate', result);
// If filled, update positions
if (result.fills.length > 0) {
await this.processFills(result);
}
return result;
} catch (error) {
logger.error('Error submitting order:', error);
return this.createRejectionReport(
orderId,
clientOrderId,
validatedOrder,
error instanceof Error ? error.message : 'Unknown error'
);
}
}
private async checkRisk(order: OrderRequest): Promise<any> {
if (!this.tradingEngine) {
throw new Error('Trading engine not initialized');
}
// Convert to engine format
const engineOrder = {
id: uuidv4(),
symbol: order.symbol,
side: order.side,
quantity: order.quantity,
orderType: order.orderType,
limitPrice: order.limitPrice,
timeInForce: order.timeInForce
};
const result = this.tradingEngine.checkRisk(engineOrder);
return JSON.parse(result);
}
private async submitToSimulation(
orderId: string,
clientOrderId: string,
order: OrderRequest
): Promise<ExecutionReport> {
if (!this.tradingEngine) {
throw new Error('Trading engine not initialized');
}
// Convert to engine format
const engineOrder = {
id: orderId,
symbol: order.symbol,
side: order.side,
quantity: order.quantity,
orderType: order.orderType,
limitPrice: order.limitPrice,
timeInForce: order.timeInForce
};
// Submit to engine
const result = await this.tradingEngine.submitOrder(engineOrder);
const engineResult = JSON.parse(result);
// Convert back to our format
return {
orderId,
clientOrderId,
symbol: order.symbol,
side: order.side,
quantity: order.quantity,
status: this.mapEngineStatus(engineResult.status),
fills: engineResult.fills || [],
timestamp: Date.now()
};
}
private async submitToBroker(
orderId: string,
clientOrderId: string,
order: OrderRequest
): Promise<ExecutionReport> {
// In real implementation, would submit to actual broker
// This is a placeholder
logger.info(`Submitting order ${orderId} to broker`);
// Simulate broker response
return {
orderId,
clientOrderId,
symbol: order.symbol,
side: order.side,
quantity: order.quantity,
status: 'pending',
fills: [],
timestamp: Date.now()
};
}
async cancelOrder(orderId: string): Promise<boolean> {
const order = this.pendingOrders.get(orderId);
if (!order) {
logger.warn(`Order ${orderId} not found`);
return false;
}
try {
switch (this.mode) {
case 'backtest':
case 'paper':
// Cancel in simulation
if (this.tradingEngine) {
await this.tradingEngine.cancelOrder(orderId);
}
break;
case 'live':
// Cancel with broker
if (this.brokerClient) {
await this.brokerClient.cancelOrder(orderId);
}
break;
}
this.pendingOrders.delete(orderId);
// Emit cancellation event
this.emit('orderUpdate', {
orderId,
status: 'cancelled',
timestamp: Date.now()
});
return true;
} catch (error) {
logger.error(`Error cancelling order ${orderId}:`, error);
return false;
}
}
private async processFills(executionReport: ExecutionReport): Promise<void> {
if (!this.tradingEngine) return;
for (const fill of executionReport.fills) {
// Update position in engine
const result = this.tradingEngine.processFill(
executionReport.symbol,
fill.price,
fill.quantity,
executionReport.side,
fill.commission
);
// Emit fill event
this.emit('fill', {
orderId: executionReport.orderId,
symbol: executionReport.symbol,
side: executionReport.side,
...fill,
positionUpdate: JSON.parse(result)
});
}
}
private createRejectionReport(
orderId: string,
clientOrderId: string,
order: OrderRequest,
reason: string
): ExecutionReport {
return {
orderId,
clientOrderId,
symbol: order.symbol,
side: order.side,
quantity: order.quantity,
status: 'rejected',
fills: [],
rejectionReason: reason,
timestamp: Date.now()
};
}
private mapEngineStatus(engineStatus: string): ExecutionReport['status'] {
const statusMap: Record<string, ExecutionReport['status']> = {
'Pending': 'pending',
'Accepted': 'accepted',
'PartiallyFilled': 'partiallyFilled',
'Filled': 'filled',
'Cancelled': 'cancelled',
'Rejected': 'rejected'
};
return statusMap[engineStatus] || 'rejected';
}
async routeOrderToExchange(order: OrderRequest, exchange: string): Promise<void> {
// This would route orders to specific exchanges in live mode
// For now, just a placeholder
logger.info(`Routing order to ${exchange}:`, order);
}
async getOrderStatus(orderId: string): Promise<ExecutionReport | null> {
// In real implementation, would query broker or internal state
return null;
}
async shutdown(): Promise<void> {
// Cancel all pending orders
for (const orderId of this.pendingOrders.keys()) {
await this.cancelOrder(orderId);
}
// Disconnect from broker
if (this.brokerClient) {
// await this.brokerClient.disconnect();
this.brokerClient = null;
}
this.tradingEngine = null;
this.removeAllListeners();
}
}

View file

@ -0,0 +1,280 @@
import { logger } from '@stock-bot/logger';
import { io, Socket } from 'socket.io-client';
import { EventEmitter } from 'events';
import { ModeConfig, MarketData, QuoteSchema, TradeSchema, BarSchema } from '../types';
import { QuestDBClient } from '@stock-bot/questdb';
export class MarketDataService extends EventEmitter {
private mode: 'backtest' | 'paper' | 'live' = 'paper';
private dataIngestionSocket: Socket | null = null;
private questdbClient: QuestDBClient | null = null;
private subscriptions = new Set<string>();
private batchBuffer: MarketData[] = [];
private batchTimer: NodeJS.Timeout | null = null;
private readonly BATCH_SIZE = 100;
private readonly BATCH_INTERVAL_MS = 50;
async initialize(config: ModeConfig): Promise<void> {
this.mode = config.mode;
if (config.mode === 'backtest') {
// Initialize QuestDB client for historical data
this.questdbClient = new QuestDBClient({
host: process.env.QUESTDB_HOST || 'localhost',
port: parseInt(process.env.QUESTDB_PORT || '9000'),
database: process.env.QUESTDB_DATABASE || 'trading'
});
} else {
// Connect to data-ingestion service for real-time data
await this.connectToDataIngestion();
}
}
private async connectToDataIngestion(): Promise<void> {
const dataIngestionUrl = process.env.DATA_INGESTION_URL || 'http://localhost:3001';
this.dataIngestionSocket = io(dataIngestionUrl, {
transports: ['websocket'],
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 1000
});
this.dataIngestionSocket.on('connect', () => {
logger.info('Connected to data-ingestion service');
// Re-subscribe to symbols
this.subscriptions.forEach(symbol => {
this.dataIngestionSocket!.emit('subscribe', { symbol });
});
});
this.dataIngestionSocket.on('disconnect', () => {
logger.warn('Disconnected from data-ingestion service');
});
this.dataIngestionSocket.on('marketData', (data: any) => {
this.handleMarketData(data);
});
this.dataIngestionSocket.on('error', (error: any) => {
logger.error('Data ingestion socket error:', error);
});
}
async subscribeToSymbol(symbol: string): Promise<void> {
this.subscriptions.add(symbol);
if (this.mode !== 'backtest' && this.dataIngestionSocket?.connected) {
this.dataIngestionSocket.emit('subscribe', { symbol });
}
logger.debug(`Subscribed to ${symbol}`);
}
async unsubscribeFromSymbol(symbol: string): Promise<void> {
this.subscriptions.delete(symbol);
if (this.mode !== 'backtest' && this.dataIngestionSocket?.connected) {
this.dataIngestionSocket.emit('unsubscribe', { symbol });
}
logger.debug(`Unsubscribed from ${symbol}`);
}
private handleMarketData(data: any): void {
try {
// Validate and transform data
let marketData: MarketData;
if (data.bid !== undefined && data.ask !== undefined) {
const quote = QuoteSchema.parse({
symbol: data.symbol,
bid: data.bid,
ask: data.ask,
bidSize: data.bidSize || data.bid_size || 0,
askSize: data.askSize || data.ask_size || 0,
timestamp: data.timestamp || Date.now()
});
marketData = { type: 'quote', data: quote };
} else if (data.price !== undefined && data.size !== undefined) {
const trade = TradeSchema.parse({
symbol: data.symbol,
price: data.price,
size: data.size,
side: data.side || 'buy',
timestamp: data.timestamp || Date.now()
});
marketData = { type: 'trade', data: trade };
} else if (data.open !== undefined && data.close !== undefined) {
const bar = BarSchema.parse({
symbol: data.symbol,
open: data.open,
high: data.high,
low: data.low,
close: data.close,
volume: data.volume,
vwap: data.vwap,
timestamp: data.timestamp || Date.now()
});
marketData = { type: 'bar', data: bar };
} else {
logger.warn('Unknown market data format:', data);
return;
}
// Add to batch buffer
this.batchBuffer.push(marketData);
// Process batch if size threshold reached
if (this.batchBuffer.length >= this.BATCH_SIZE) {
this.processBatch();
} else if (!this.batchTimer) {
// Set timer for time-based batching
this.batchTimer = setTimeout(() => this.processBatch(), this.BATCH_INTERVAL_MS);
}
} catch (error) {
logger.error('Error handling market data:', error);
}
}
private processBatch(): void {
if (this.batchBuffer.length === 0) return;
// Clear timer
if (this.batchTimer) {
clearTimeout(this.batchTimer);
this.batchTimer = null;
}
// Emit batch
const batch = [...this.batchBuffer];
this.batchBuffer = [];
this.emit('marketDataBatch', batch);
// Also emit individual events for strategies that need them
batch.forEach(data => {
this.emit('marketData', data);
});
}
async loadHistoricalData(
symbols: string[],
startTime: Date,
endTime: Date,
interval: string = '1m'
): Promise<MarketData[]> {
if (!this.questdbClient) {
throw new Error('QuestDB client not initialized');
}
const data: MarketData[] = [];
for (const symbol of symbols) {
// Query for bars
const bars = await this.questdbClient.query(`
SELECT
timestamp,
open,
high,
low,
close,
volume,
vwap
FROM bars_${interval}
WHERE symbol = '${symbol}'
AND timestamp >= '${startTime.toISOString()}'
AND timestamp < '${endTime.toISOString()}'
ORDER BY timestamp
`);
// Convert to MarketData format
bars.forEach((row: any) => {
data.push({
type: 'bar',
data: {
symbol,
open: row.open,
high: row.high,
low: row.low,
close: row.close,
volume: row.volume,
vwap: row.vwap,
timestamp: new Date(row.timestamp).getTime()
}
});
});
// Also query for trades if needed for more granular simulation
if (interval === '1m' || interval === 'tick') {
const trades = await this.questdbClient.query(`
SELECT
timestamp,
price,
size,
side
FROM trades
WHERE symbol = '${symbol}'
AND timestamp >= '${startTime.toISOString()}'
AND timestamp < '${endTime.toISOString()}'
ORDER BY timestamp
`);
trades.forEach((row: any) => {
data.push({
type: 'trade',
data: {
symbol,
price: row.price,
size: row.size,
side: row.side,
timestamp: new Date(row.timestamp).getTime()
}
});
});
}
}
// Sort all data by timestamp
data.sort((a, b) => {
const timeA = a.type === 'bar' ? a.data.timestamp :
a.type === 'trade' ? a.data.timestamp :
a.data.timestamp;
const timeB = b.type === 'bar' ? b.data.timestamp :
b.type === 'trade' ? b.data.timestamp :
b.data.timestamp;
return timeA - timeB;
});
return data;
}
async shutdown(): Promise<void> {
// Clear batch timer
if (this.batchTimer) {
clearTimeout(this.batchTimer);
this.batchTimer = null;
}
// Process any remaining data
if (this.batchBuffer.length > 0) {
this.processBatch();
}
// Disconnect from data ingestion
if (this.dataIngestionSocket) {
this.dataIngestionSocket.disconnect();
this.dataIngestionSocket = null;
}
// Close QuestDB connection
if (this.questdbClient) {
await this.questdbClient.close();
this.questdbClient = null;
}
this.subscriptions.clear();
this.removeAllListeners();
}
}

View file

@ -0,0 +1,293 @@
import { logger } from '@stock-bot/logger';
import { QuestDBClient } from '@stock-bot/questdb';
import { PostgresClient } from '@stock-bot/postgres';
import { ModeConfig, MarketData, Position } from '../types';
export class StorageService {
private questdb: QuestDBClient | null = null;
private postgres: PostgresClient | null = null;
private mode: 'backtest' | 'paper' | 'live' = 'paper';
async initialize(config: ModeConfig): Promise<void> {
this.mode = config.mode;
// Initialize QuestDB for time-series data
this.questdb = new QuestDBClient({
host: process.env.QUESTDB_HOST || 'localhost',
port: parseInt(process.env.QUESTDB_PORT || '9000'),
database: process.env.QUESTDB_DATABASE || 'trading'
});
// Initialize PostgreSQL for relational data
this.postgres = new PostgresClient({
host: process.env.POSTGRES_HOST || 'localhost',
port: parseInt(process.env.POSTGRES_PORT || '5432'),
database: process.env.POSTGRES_DATABASE || 'trading',
user: process.env.POSTGRES_USER || 'postgres',
password: process.env.POSTGRES_PASSWORD || 'postgres'
});
await this.createTables();
}
private async createTables(): Promise<void> {
// Create tables if they don't exist
if (this.postgres) {
// Orders table
await this.postgres.query(`
CREATE TABLE IF NOT EXISTS orders (
id UUID PRIMARY KEY,
client_order_id VARCHAR(255),
symbol VARCHAR(50) NOT NULL,
side VARCHAR(10) NOT NULL,
quantity DECIMAL(20, 8) NOT NULL,
order_type VARCHAR(20) NOT NULL,
limit_price DECIMAL(20, 8),
stop_price DECIMAL(20, 8),
time_in_force VARCHAR(10) NOT NULL,
status VARCHAR(20) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
mode VARCHAR(10) NOT NULL
)
`);
// Fills table
await this.postgres.query(`
CREATE TABLE IF NOT EXISTS fills (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
order_id UUID NOT NULL REFERENCES orders(id),
symbol VARCHAR(50) NOT NULL,
price DECIMAL(20, 8) NOT NULL,
quantity DECIMAL(20, 8) NOT NULL,
commission DECIMAL(20, 8) NOT NULL,
side VARCHAR(10) NOT NULL,
timestamp TIMESTAMP NOT NULL,
mode VARCHAR(10) NOT NULL
)
`);
// Positions table
await this.postgres.query(`
CREATE TABLE IF NOT EXISTS positions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
symbol VARCHAR(50) NOT NULL,
quantity DECIMAL(20, 8) NOT NULL,
average_price DECIMAL(20, 8) NOT NULL,
realized_pnl DECIMAL(20, 8) NOT NULL DEFAULT 0,
unrealized_pnl DECIMAL(20, 8) NOT NULL DEFAULT 0,
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
mode VARCHAR(10) NOT NULL,
UNIQUE(symbol, mode)
)
`);
// Strategy performance table
await this.postgres.query(`
CREATE TABLE IF NOT EXISTS strategy_performance (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
strategy_id VARCHAR(255) NOT NULL,
timestamp TIMESTAMP NOT NULL,
total_return DECIMAL(20, 8),
sharpe_ratio DECIMAL(20, 8),
max_drawdown DECIMAL(20, 8),
win_rate DECIMAL(20, 8),
total_trades INTEGER,
mode VARCHAR(10) NOT NULL
)
`);
}
}
async storeMarketData(data: MarketData[]): Promise<void> {
if (!this.questdb) return;
for (const item of data) {
try {
switch (item.type) {
case 'quote':
await this.questdb.insert('quotes', {
symbol: item.data.symbol,
bid: item.data.bid,
ask: item.data.ask,
bid_size: item.data.bidSize,
ask_size: item.data.askSize,
timestamp: new Date(item.data.timestamp)
});
break;
case 'trade':
await this.questdb.insert('trades', {
symbol: item.data.symbol,
price: item.data.price,
size: item.data.size,
side: item.data.side,
timestamp: new Date(item.data.timestamp)
});
break;
case 'bar':
const interval = '1m'; // Would be determined from context
await this.questdb.insert(`bars_${interval}`, {
symbol: item.data.symbol,
open: item.data.open,
high: item.data.high,
low: item.data.low,
close: item.data.close,
volume: item.data.volume,
vwap: item.data.vwap || null,
timestamp: new Date(item.data.timestamp)
});
break;
}
} catch (error) {
logger.error('Error storing market data:', error);
}
}
}
async storeOrder(order: any): Promise<void> {
if (!this.postgres) return;
await this.postgres.query(`
INSERT INTO orders (
id, client_order_id, symbol, side, quantity,
order_type, limit_price, stop_price, time_in_force,
status, mode
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
`, [
order.id,
order.clientOrderId,
order.symbol,
order.side,
order.quantity,
order.orderType,
order.limitPrice || null,
order.stopPrice || null,
order.timeInForce,
order.status,
this.mode
]);
}
async updateOrderStatus(orderId: string, status: string): Promise<void> {
if (!this.postgres) return;
await this.postgres.query(`
UPDATE orders
SET status = $1, updated_at = NOW()
WHERE id = $2
`, [status, orderId]);
}
async storeFill(fill: any): Promise<void> {
if (!this.postgres) return;
await this.postgres.query(`
INSERT INTO fills (
order_id, symbol, price, quantity, commission, side, timestamp, mode
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
`, [
fill.orderId,
fill.symbol,
fill.price,
fill.quantity,
fill.commission,
fill.side,
new Date(fill.timestamp),
this.mode
]);
}
async updatePosition(position: Position): Promise<void> {
if (!this.postgres) return;
await this.postgres.query(`
INSERT INTO positions (
symbol, quantity, average_price, realized_pnl, unrealized_pnl, mode
) VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (symbol, mode) DO UPDATE SET
quantity = $2,
average_price = $3,
realized_pnl = $4,
unrealized_pnl = $5,
updated_at = NOW()
`, [
position.symbol,
position.quantity,
position.averagePrice,
position.realizedPnl,
position.unrealizedPnl,
this.mode
]);
}
async getPositions(): Promise<Position[]> {
if (!this.postgres) return [];
const result = await this.postgres.query(`
SELECT * FROM positions WHERE mode = $1
`, [this.mode]);
return result.rows.map((row: any) => ({
symbol: row.symbol,
quantity: parseFloat(row.quantity),
averagePrice: parseFloat(row.average_price),
realizedPnl: parseFloat(row.realized_pnl),
unrealizedPnl: parseFloat(row.unrealized_pnl),
totalCost: parseFloat(row.quantity) * parseFloat(row.average_price),
lastUpdate: row.updated_at
}));
}
async storeStrategyPerformance(strategyId: string, metrics: any): Promise<void> {
if (!this.postgres) return;
await this.postgres.query(`
INSERT INTO strategy_performance (
strategy_id, timestamp, total_return, sharpe_ratio,
max_drawdown, win_rate, total_trades, mode
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
`, [
strategyId,
new Date(),
metrics.totalReturn,
metrics.sharpeRatio,
metrics.maxDrawdown,
metrics.winRate,
metrics.totalTrades,
this.mode
]);
}
async getHistoricalBars(
symbol: string,
startTime: Date,
endTime: Date,
interval: string = '1m'
): Promise<any[]> {
if (!this.questdb) return [];
const result = await this.questdb.query(`
SELECT * FROM bars_${interval}
WHERE symbol = '${symbol}'
AND timestamp >= '${startTime.toISOString()}'
AND timestamp < '${endTime.toISOString()}'
ORDER BY timestamp
`);
return result;
}
async shutdown(): Promise<void> {
if (this.questdb) {
await this.questdb.close();
this.questdb = null;
}
if (this.postgres) {
await this.postgres.close();
this.postgres = null;
}
}
}

View file

@ -0,0 +1,255 @@
import { EventEmitter } from 'events';
import { logger } from '@stock-bot/logger';
import { MarketData, StrategyConfig, OrderRequest } from '../types';
import { ModeManager } from '../core/ModeManager';
import { ExecutionService } from '../services/ExecutionService';
export interface Signal {
type: 'buy' | 'sell' | 'close';
symbol: string;
strength: number; // -1 to 1
reason?: string;
metadata?: Record<string, any>;
}
export abstract class BaseStrategy extends EventEmitter {
protected config: StrategyConfig;
protected isActive = false;
protected positions = new Map<string, number>();
protected pendingOrders = new Map<string, OrderRequest>();
protected performance = {
trades: 0,
wins: 0,
losses: 0,
totalPnl: 0,
maxDrawdown: 0,
currentDrawdown: 0,
peakEquity: 0
};
constructor(
config: StrategyConfig,
protected modeManager: ModeManager,
protected executionService: ExecutionService
) {
super();
this.config = config;
}
async initialize(): Promise<void> {
logger.info(`Initializing strategy: ${this.config.name}`);
// Subscribe to symbols
for (const symbol of this.config.symbols) {
// Note: In real implementation, would subscribe through market data service
logger.debug(`Strategy ${this.config.id} subscribed to ${symbol}`);
}
}
async start(): Promise<void> {
this.isActive = true;
logger.info(`Started strategy: ${this.config.name}`);
this.onStart();
}
async stop(): Promise<void> {
this.isActive = false;
// Cancel pending orders
for (const [orderId, order] of this.pendingOrders) {
await this.executionService.cancelOrder(orderId);
}
this.pendingOrders.clear();
logger.info(`Stopped strategy: ${this.config.name}`);
this.onStop();
}
async shutdown(): Promise<void> {
await this.stop();
this.removeAllListeners();
logger.info(`Shutdown strategy: ${this.config.name}`);
}
// Market data handling
async onMarketData(data: MarketData): Promise<void> {
if (!this.isActive) return;
try {
// Update any indicators or state
this.updateIndicators(data);
// Generate signals
const signal = await this.generateSignal(data);
if (signal) {
this.emit('signal', signal);
// Convert signal to order if strong enough
const order = await this.signalToOrder(signal);
if (order) {
this.emit('order', order);
}
}
} catch (error) {
logger.error(`Strategy ${this.config.id} error:`, error);
}
}
async onMarketDataBatch(batch: MarketData[]): Promise<void> {
// Default implementation processes individually
// Strategies can override for more efficient batch processing
for (const data of batch) {
await this.onMarketData(data);
}
}
// Order and fill handling
async onOrderUpdate(update: any): Promise<void> {
logger.debug(`Strategy ${this.config.id} order update:`, update);
if (update.status === 'filled') {
// Remove from pending
this.pendingOrders.delete(update.orderId);
// Update position tracking
const fill = update.fills[0]; // Assuming single fill for simplicity
if (fill) {
const currentPos = this.positions.get(update.symbol) || 0;
const newPos = update.side === 'buy'
? currentPos + fill.quantity
: currentPos - fill.quantity;
if (Math.abs(newPos) < 0.0001) {
this.positions.delete(update.symbol);
} else {
this.positions.set(update.symbol, newPos);
}
}
} else if (update.status === 'rejected' || update.status === 'cancelled') {
this.pendingOrders.delete(update.orderId);
}
}
async onOrderError(order: OrderRequest, error: any): Promise<void> {
logger.error(`Strategy ${this.config.id} order error:`, error);
// Strategies can override to handle errors
}
async onFill(fill: any): Promise<void> {
// Update performance metrics
this.performance.trades++;
if (fill.pnl > 0) {
this.performance.wins++;
} else if (fill.pnl < 0) {
this.performance.losses++;
}
this.performance.totalPnl += fill.pnl;
// Update drawdown
const currentEquity = this.getEquity();
if (currentEquity > this.performance.peakEquity) {
this.performance.peakEquity = currentEquity;
this.performance.currentDrawdown = 0;
} else {
this.performance.currentDrawdown = (this.performance.peakEquity - currentEquity) / this.performance.peakEquity;
this.performance.maxDrawdown = Math.max(this.performance.maxDrawdown, this.performance.currentDrawdown);
}
}
// Configuration
async updateConfig(updates: Partial<StrategyConfig>): Promise<void> {
this.config = { ...this.config, ...updates };
logger.info(`Updated config for strategy ${this.config.id}`);
// Strategies can override to handle specific config changes
this.onConfigUpdate(updates);
}
// Helper methods
isInterestedInSymbol(symbol: string): boolean {
return this.config.symbols.includes(symbol);
}
hasPosition(symbol: string): boolean {
return this.positions.has(symbol) && Math.abs(this.positions.get(symbol)!) > 0.0001;
}
getPosition(symbol: string): number {
return this.positions.get(symbol) || 0;
}
getPerformance(): any {
const winRate = this.performance.trades > 0
? (this.performance.wins / this.performance.trades) * 100
: 0;
return {
...this.performance,
winRate,
averagePnl: this.performance.trades > 0
? this.performance.totalPnl / this.performance.trades
: 0
};
}
protected getEquity(): number {
// Simplified - in reality would calculate based on positions and market values
return 100000 + this.performance.totalPnl; // Assuming 100k starting capital
}
protected async signalToOrder(signal: Signal): Promise<OrderRequest | null> {
// Only act on strong signals
if (Math.abs(signal.strength) < 0.7) return null;
// Check if we already have a position
const currentPosition = this.getPosition(signal.symbol);
// Simple logic - can be overridden by specific strategies
if (signal.type === 'buy' && currentPosition <= 0) {
return {
symbol: signal.symbol,
side: 'buy',
quantity: this.calculatePositionSize(signal),
orderType: 'market',
timeInForce: 'DAY'
};
} else if (signal.type === 'sell' && currentPosition >= 0) {
return {
symbol: signal.symbol,
side: 'sell',
quantity: this.calculatePositionSize(signal),
orderType: 'market',
timeInForce: 'DAY'
};
} else if (signal.type === 'close' && currentPosition !== 0) {
return {
symbol: signal.symbol,
side: currentPosition > 0 ? 'sell' : 'buy',
quantity: Math.abs(currentPosition),
orderType: 'market',
timeInForce: 'DAY'
};
}
return null;
}
protected calculatePositionSize(signal: Signal): number {
// Simple fixed size - strategies should override with proper position sizing
const baseSize = 100; // 100 shares
const allocation = this.config.allocation || 1.0;
return Math.floor(baseSize * allocation * Math.abs(signal.strength));
}
// Abstract methods that strategies must implement
protected abstract updateIndicators(data: MarketData): void;
protected abstract generateSignal(data: MarketData): Promise<Signal | null>;
// Optional hooks for strategies to override
protected onStart(): void {}
protected onStop(): void {}
protected onConfigUpdate(updates: Partial<StrategyConfig>): void {}
}

View file

@ -0,0 +1,276 @@
import { logger } from '@stock-bot/logger';
import { EventEmitter } from 'events';
import { MarketData, StrategyConfig, OrderRequest } from '../types';
import { BaseStrategy } from './BaseStrategy';
import { ModeManager } from '../core/ModeManager';
import { MarketDataService } from '../services/MarketDataService';
import { ExecutionService } from '../services/ExecutionService';
import { TradingEngine } from '../../core';
export class StrategyManager extends EventEmitter {
private strategies = new Map<string, BaseStrategy>();
private activeStrategies = new Set<string>();
private tradingEngine: TradingEngine | null = null;
constructor(
private modeManager: ModeManager,
private marketDataService: MarketDataService,
private executionService: ExecutionService
) {
super();
this.setupEventListeners();
}
private setupEventListeners(): void {
// Listen for market data
this.marketDataService.on('marketData', (data: MarketData) => {
this.handleMarketData(data);
});
// Listen for market data batches (more efficient)
this.marketDataService.on('marketDataBatch', (batch: MarketData[]) => {
this.handleMarketDataBatch(batch);
});
// Listen for fills
this.executionService.on('fill', (fill: any) => {
this.handleFill(fill);
});
}
async initializeStrategies(configs: StrategyConfig[]): Promise<void> {
// Clear existing strategies
for (const [id, strategy] of this.strategies) {
await strategy.shutdown();
}
this.strategies.clear();
this.activeStrategies.clear();
// Get trading engine from mode manager
this.tradingEngine = this.modeManager.getTradingEngine();
// Initialize new strategies
for (const config of configs) {
try {
const strategy = await this.createStrategy(config);
this.strategies.set(config.id, strategy);
if (config.enabled) {
await this.enableStrategy(config.id);
}
logger.info(`Initialized strategy: ${config.name} (${config.id})`);
} catch (error) {
logger.error(`Failed to initialize strategy ${config.name}:`, error);
}
}
}
private async createStrategy(config: StrategyConfig): Promise<BaseStrategy> {
// In a real system, this would dynamically load strategy classes
// For now, create a base strategy instance
const strategy = new BaseStrategy(
config,
this.modeManager,
this.executionService
);
// Set up strategy event handlers
strategy.on('signal', (signal: any) => {
this.handleStrategySignal(config.id, signal);
});
strategy.on('order', (order: OrderRequest) => {
this.handleStrategyOrder(config.id, order);
});
await strategy.initialize();
return strategy;
}
async enableStrategy(strategyId: string): Promise<void> {
const strategy = this.strategies.get(strategyId);
if (!strategy) {
throw new Error(`Strategy ${strategyId} not found`);
}
await strategy.start();
this.activeStrategies.add(strategyId);
logger.info(`Enabled strategy: ${strategyId}`);
}
async disableStrategy(strategyId: string): Promise<void> {
const strategy = this.strategies.get(strategyId);
if (!strategy) {
throw new Error(`Strategy ${strategyId} not found`);
}
await strategy.stop();
this.activeStrategies.delete(strategyId);
logger.info(`Disabled strategy: ${strategyId}`);
}
private async handleMarketData(data: MarketData): Promise<void> {
// Forward to active strategies
for (const strategyId of this.activeStrategies) {
const strategy = this.strategies.get(strategyId);
if (strategy && strategy.isInterestedInSymbol(data.data.symbol)) {
try {
await strategy.onMarketData(data);
} catch (error) {
logger.error(`Strategy ${strategyId} error processing market data:`, error);
}
}
}
}
private async handleMarketDataBatch(batch: MarketData[]): Promise<void> {
// Group by symbol for efficiency
const bySymbol = new Map<string, MarketData[]>();
for (const data of batch) {
const symbol = data.data.symbol;
if (!bySymbol.has(symbol)) {
bySymbol.set(symbol, []);
}
bySymbol.get(symbol)!.push(data);
}
// Forward to strategies
for (const strategyId of this.activeStrategies) {
const strategy = this.strategies.get(strategyId);
if (!strategy) continue;
const relevantData: MarketData[] = [];
for (const [symbol, data] of bySymbol) {
if (strategy.isInterestedInSymbol(symbol)) {
relevantData.push(...data);
}
}
if (relevantData.length > 0) {
try {
await strategy.onMarketDataBatch(relevantData);
} catch (error) {
logger.error(`Strategy ${strategyId} error processing batch:`, error);
}
}
}
}
private async handleFill(fill: any): Promise<void> {
// Notify relevant strategies about fills
for (const strategyId of this.activeStrategies) {
const strategy = this.strategies.get(strategyId);
if (strategy && strategy.hasPosition(fill.symbol)) {
try {
await strategy.onFill(fill);
} catch (error) {
logger.error(`Strategy ${strategyId} error processing fill:`, error);
}
}
}
}
private async handleStrategySignal(strategyId: string, signal: any): Promise<void> {
logger.debug(`Strategy ${strategyId} generated signal:`, signal);
// Emit for monitoring/logging
this.emit('strategySignal', {
strategyId,
signal,
timestamp: Date.now()
});
}
private async handleStrategyOrder(strategyId: string, order: OrderRequest): Promise<void> {
logger.info(`Strategy ${strategyId} placing order:`, order);
try {
// Submit order through execution service
const result = await this.executionService.submitOrder(order);
// Notify strategy of order result
const strategy = this.strategies.get(strategyId);
if (strategy) {
await strategy.onOrderUpdate(result);
}
// Emit for monitoring
this.emit('strategyOrder', {
strategyId,
order,
result,
timestamp: Date.now()
});
} catch (error) {
logger.error(`Failed to submit order from strategy ${strategyId}:`, error);
// Notify strategy of failure
const strategy = this.strategies.get(strategyId);
if (strategy) {
await strategy.onOrderError(order, error);
}
}
}
async onMarketData(data: MarketData): Promise<void> {
// Called by backtest engine
await this.handleMarketData(data);
}
getTradingEngine(): TradingEngine | null {
return this.tradingEngine;
}
getStrategy(strategyId: string): BaseStrategy | undefined {
return this.strategies.get(strategyId);
}
getAllStrategies(): Map<string, BaseStrategy> {
return new Map(this.strategies);
}
getActiveStrategies(): Set<string> {
return new Set(this.activeStrategies);
}
async updateStrategyConfig(strategyId: string, updates: Partial<StrategyConfig>): Promise<void> {
const strategy = this.strategies.get(strategyId);
if (!strategy) {
throw new Error(`Strategy ${strategyId} not found`);
}
await strategy.updateConfig(updates);
logger.info(`Updated configuration for strategy ${strategyId}`);
}
async getStrategyPerformance(strategyId: string): Promise<any> {
const strategy = this.strategies.get(strategyId);
if (!strategy) {
throw new Error(`Strategy ${strategyId} not found`);
}
return strategy.getPerformance();
}
async shutdown(): Promise<void> {
logger.info('Shutting down strategy manager...');
// Disable all strategies
for (const strategyId of this.activeStrategies) {
await this.disableStrategy(strategyId);
}
// Shutdown all strategies
for (const [id, strategy] of this.strategies) {
await strategy.shutdown();
}
this.strategies.clear();
this.activeStrategies.clear();
this.removeAllListeners();
}
}

View file

@ -0,0 +1,414 @@
import { BaseStrategy, Signal } from '../BaseStrategy';
import { MarketData } from '../../types';
import { logger } from '@stock-bot/logger';
import * as tf from '@tensorflow/tfjs-node';
interface MLModelConfig {
modelPath?: string;
features: string[];
lookbackPeriod: number;
updateFrequency: number; // How often to retrain in minutes
minTrainingSize: number;
}
export class MLEnhancedStrategy extends BaseStrategy {
private model: tf.LayersModel | null = null;
private featureBuffer: Map<string, number[][]> = new Map();
private predictions: Map<string, number> = new Map();
private lastUpdate: number = 0;
private trainingData: { features: number[][]; labels: number[] } = { features: [], labels: [] };
// Feature extractors
private indicators: Map<string, any> = new Map();
// ML Configuration
private mlConfig: MLModelConfig = {
features: [
'returns_20', 'returns_50', 'volatility_20', 'rsi_14',
'volume_ratio', 'price_position', 'macd_signal'
],
lookbackPeriod: 50,
updateFrequency: 1440, // Daily
minTrainingSize: 1000
};
protected async onStart(): Promise<void> {
logger.info('ML Enhanced Strategy starting...');
// Try to load existing model
if (this.mlConfig.modelPath) {
try {
this.model = await tf.loadLayersModel(`file://${this.mlConfig.modelPath}`);
logger.info('Loaded existing ML model');
} catch (error) {
logger.warn('Could not load model, will train new one');
}
}
// Initialize feature buffers for each symbol
this.config.symbols.forEach(symbol => {
this.featureBuffer.set(symbol, []);
this.indicators.set(symbol, {
prices: [],
volumes: [],
returns: [],
sma20: 0,
sma50: 0,
rsi: 50,
macd: 0,
signal: 0
});
});
}
protected updateIndicators(data: MarketData): void {
if (data.type !== 'bar') return;
const symbol = data.data.symbol;
const indicators = this.indicators.get(symbol);
if (!indicators) return;
// Update price and volume history
indicators.prices.push(data.data.close);
indicators.volumes.push(data.data.volume);
if (indicators.prices.length > 200) {
indicators.prices.shift();
indicators.volumes.shift();
}
// Calculate returns
if (indicators.prices.length >= 2) {
const ret = (data.data.close - indicators.prices[indicators.prices.length - 2]) /
indicators.prices[indicators.prices.length - 2];
indicators.returns.push(ret);
if (indicators.returns.length > 50) {
indicators.returns.shift();
}
}
// Update technical indicators
if (indicators.prices.length >= 20) {
indicators.sma20 = this.calculateSMA(indicators.prices, 20);
indicators.volatility20 = this.calculateVolatility(indicators.returns, 20);
}
if (indicators.prices.length >= 50) {
indicators.sma50 = this.calculateSMA(indicators.prices, 50);
}
if (indicators.prices.length >= 14) {
indicators.rsi = this.calculateRSI(indicators.prices, 14);
}
// Extract features
const features = this.extractFeatures(symbol, data);
if (features) {
const buffer = this.featureBuffer.get(symbol)!;
buffer.push(features);
if (buffer.length > this.mlConfig.lookbackPeriod) {
buffer.shift();
}
// Make prediction if we have enough data
if (buffer.length === this.mlConfig.lookbackPeriod && this.model) {
this.makePrediction(symbol, buffer);
}
}
// Check if we should update the model
const now = Date.now();
if (now - this.lastUpdate > this.mlConfig.updateFrequency * 60 * 1000) {
this.updateModel();
this.lastUpdate = now;
}
}
protected async generateSignal(data: MarketData): Promise<Signal | null> {
if (data.type !== 'bar') return null;
const symbol = data.data.symbol;
const prediction = this.predictions.get(symbol);
if (!prediction || Math.abs(prediction) < 0.01) {
return null; // No strong signal
}
const position = this.getPosition(symbol);
const indicators = this.indicators.get(symbol);
// Risk management checks
const volatility = indicators?.volatility20 || 0.02;
const maxPositionRisk = 0.02; // 2% max risk per position
const positionSize = this.calculatePositionSize(volatility, maxPositionRisk);
// Generate signals based on ML predictions
if (prediction > 0.02 && position <= 0) {
// Strong bullish prediction
return {
type: 'buy',
symbol,
strength: Math.min(prediction * 50, 1), // Scale prediction to 0-1
reason: `ML prediction: ${(prediction * 100).toFixed(2)}% expected return`,
metadata: {
prediction,
confidence: this.calculateConfidence(symbol),
features: this.getLatestFeatures(symbol)
}
};
} else if (prediction < -0.02 && position >= 0) {
// Strong bearish prediction
return {
type: 'sell',
symbol,
strength: Math.min(Math.abs(prediction) * 50, 1),
reason: `ML prediction: ${(prediction * 100).toFixed(2)}% expected return`,
metadata: {
prediction,
confidence: this.calculateConfidence(symbol),
features: this.getLatestFeatures(symbol)
}
};
} else if (position !== 0 && Math.sign(position) !== Math.sign(prediction)) {
// Exit if prediction reverses
return {
type: 'close',
symbol,
strength: 1,
reason: 'ML prediction reversed',
metadata: { prediction }
};
}
return null;
}
private extractFeatures(symbol: string, data: MarketData): number[] | null {
const indicators = this.indicators.get(symbol);
if (!indicators || indicators.prices.length < 50) return null;
const features: number[] = [];
// Price returns
const currentPrice = indicators.prices[indicators.prices.length - 1];
features.push((currentPrice / indicators.prices[indicators.prices.length - 20] - 1)); // 20-day return
features.push((currentPrice / indicators.prices[indicators.prices.length - 50] - 1)); // 50-day return
// Volatility
features.push(indicators.volatility20 || 0);
// RSI
features.push((indicators.rsi - 50) / 50); // Normalize to -1 to 1
// Volume ratio
const avgVolume = indicators.volumes.slice(-20).reduce((a, b) => a + b, 0) / 20;
features.push(data.data.volume / avgVolume - 1);
// Price position in daily range
const pricePosition = (data.data.close - data.data.low) / (data.data.high - data.data.low);
features.push(pricePosition * 2 - 1); // Normalize to -1 to 1
// MACD signal
if (indicators.macd && indicators.signal) {
features.push((indicators.macd - indicators.signal) / currentPrice);
} else {
features.push(0);
}
// Store for training
if (indicators.returns.length >= 21) {
const futureReturn = indicators.returns[indicators.returns.length - 1];
this.trainingData.features.push([...features]);
this.trainingData.labels.push(futureReturn);
// Limit training data size
if (this.trainingData.features.length > 10000) {
this.trainingData.features.shift();
this.trainingData.labels.shift();
}
}
return features;
}
private async makePrediction(symbol: string, featureBuffer: number[][]): Promise<void> {
if (!this.model) return;
try {
// Prepare input tensor
const input = tf.tensor3d([featureBuffer]);
// Make prediction
const prediction = await this.model.predict(input) as tf.Tensor;
const value = (await prediction.data())[0];
this.predictions.set(symbol, value);
// Cleanup tensors
input.dispose();
prediction.dispose();
} catch (error) {
logger.error('ML prediction error:', error);
}
}
private async updateModel(): Promise<void> {
if (this.trainingData.features.length < this.mlConfig.minTrainingSize) {
logger.info('Not enough training data yet');
return;
}
logger.info('Updating ML model...');
try {
// Create or update model
if (!this.model) {
this.model = this.createModel();
}
// Prepare training data
const features = tf.tensor2d(this.trainingData.features);
const labels = tf.tensor1d(this.trainingData.labels);
// Train model
await this.model.fit(features, labels, {
epochs: 50,
batchSize: 32,
validationSplit: 0.2,
shuffle: true,
callbacks: {
onEpochEnd: (epoch, logs) => {
if (epoch % 10 === 0) {
logger.debug(`Epoch ${epoch}: loss = ${logs?.loss.toFixed(4)}`);
}
}
}
});
logger.info('Model updated successfully');
// Cleanup tensors
features.dispose();
labels.dispose();
// Save model if path provided
if (this.mlConfig.modelPath) {
await this.model.save(`file://${this.mlConfig.modelPath}`);
}
} catch (error) {
logger.error('Model update error:', error);
}
}
private createModel(): tf.LayersModel {
const model = tf.sequential({
layers: [
// LSTM layer for sequence processing
tf.layers.lstm({
units: 64,
returnSequences: true,
inputShape: [this.mlConfig.lookbackPeriod, this.mlConfig.features.length]
}),
tf.layers.dropout({ rate: 0.2 }),
// Second LSTM layer
tf.layers.lstm({
units: 32,
returnSequences: false
}),
tf.layers.dropout({ rate: 0.2 }),
// Dense layers
tf.layers.dense({
units: 16,
activation: 'relu'
}),
tf.layers.dropout({ rate: 0.1 }),
// Output layer
tf.layers.dense({
units: 1,
activation: 'tanh' // Output between -1 and 1
})
]
});
model.compile({
optimizer: tf.train.adam(0.001),
loss: 'meanSquaredError',
metrics: ['mae']
});
return model;
}
private calculateConfidence(symbol: string): number {
// Simple confidence based on prediction history accuracy
// In practice, would track actual vs predicted returns
const prediction = this.predictions.get(symbol) || 0;
return Math.min(Math.abs(prediction) * 10, 1);
}
private getLatestFeatures(symbol: string): Record<string, number> {
const buffer = this.featureBuffer.get(symbol);
if (!buffer || buffer.length === 0) return {};
const latest = buffer[buffer.length - 1];
return {
returns_20: latest[0],
returns_50: latest[1],
volatility_20: latest[2],
rsi_normalized: latest[3],
volume_ratio: latest[4],
price_position: latest[5],
macd_signal: latest[6]
};
}
private calculateVolatility(returns: number[], period: number): number {
if (returns.length < period) return 0;
const recentReturns = returns.slice(-period);
const mean = recentReturns.reduce((a, b) => a + b, 0) / period;
const variance = recentReturns.reduce((sum, r) => sum + Math.pow(r - mean, 2), 0) / period;
return Math.sqrt(variance * 252); // Annualized
}
private calculatePositionSize(volatility: number, maxRisk: number): number {
// Kelly-inspired position sizing with volatility adjustment
const targetVolatility = 0.15; // 15% annual target
const volAdjustment = targetVolatility / volatility;
return Math.min(volAdjustment, 2.0); // Max 2x leverage
}
protected onStop(): void {
logger.info('ML Enhanced Strategy stopped');
// Save final model if configured
if (this.model && this.mlConfig.modelPath) {
this.model.save(`file://${this.mlConfig.modelPath}`)
.then(() => logger.info('Model saved'))
.catch(err => logger.error('Failed to save model:', err));
}
// Cleanup
this.featureBuffer.clear();
this.predictions.clear();
this.indicators.clear();
if (this.model) {
this.model.dispose();
}
}
protected onConfigUpdate(updates: any): void {
logger.info('ML Enhanced Strategy config updated:', updates);
if (updates.mlConfig) {
this.mlConfig = { ...this.mlConfig, ...updates.mlConfig };
}
}
}

View file

@ -0,0 +1,192 @@
import { BaseStrategy, Signal } from '../BaseStrategy';
import { MarketData } from '../../types';
import { logger } from '@stock-bot/logger';
interface MeanReversionIndicators {
sma20: number;
sma50: number;
stdDev: number;
zScore: number;
rsi: number;
}
export class MeanReversionStrategy extends BaseStrategy {
private priceHistory = new Map<string, number[]>();
private indicators = new Map<string, MeanReversionIndicators>();
// Strategy parameters
private readonly LOOKBACK_PERIOD = 20;
private readonly Z_SCORE_ENTRY = 2.0;
private readonly Z_SCORE_EXIT = 0.5;
private readonly RSI_OVERSOLD = 30;
private readonly RSI_OVERBOUGHT = 70;
private readonly MIN_VOLUME = 1000000; // $1M daily volume
protected updateIndicators(data: MarketData): void {
if (data.type !== 'bar') return;
const symbol = data.data.symbol;
const price = data.data.close;
// Update price history
if (!this.priceHistory.has(symbol)) {
this.priceHistory.set(symbol, []);
}
const history = this.priceHistory.get(symbol)!;
history.push(price);
// Keep only needed history
if (history.length > this.LOOKBACK_PERIOD * 3) {
history.shift();
}
// Calculate indicators if we have enough data
if (history.length >= this.LOOKBACK_PERIOD) {
const indicators = this.calculateIndicators(history);
this.indicators.set(symbol, indicators);
}
}
private calculateIndicators(prices: number[]): MeanReversionIndicators {
const len = prices.length;
// Calculate SMAs
const sma20 = this.calculateSMA(prices, 20);
const sma50 = len >= 50 ? this.calculateSMA(prices, 50) : sma20;
// Calculate standard deviation
const stdDev = this.calculateStdDev(prices.slice(-20), sma20);
// Calculate Z-score
const currentPrice = prices[len - 1];
const zScore = stdDev > 0 ? (currentPrice - sma20) / stdDev : 0;
// Calculate RSI
const rsi = this.calculateRSI(prices, 14);
return { sma20, sma50, stdDev, zScore, rsi };
}
protected async generateSignal(data: MarketData): Promise<Signal | null> {
if (data.type !== 'bar') return null;
const symbol = data.data.symbol;
const indicators = this.indicators.get(symbol);
if (!indicators) return null;
// Check volume filter
if (data.data.volume * data.data.close < this.MIN_VOLUME) {
return null;
}
const position = this.getPosition(symbol);
const { zScore, rsi, sma20, sma50 } = indicators;
// Entry signals
if (position === 0) {
// Long entry: Oversold conditions
if (zScore < -this.Z_SCORE_ENTRY && rsi < this.RSI_OVERSOLD && sma20 > sma50) {
return {
type: 'buy',
symbol,
strength: Math.min(Math.abs(zScore) / 3, 1),
reason: `Mean reversion long: Z-score ${zScore.toFixed(2)}, RSI ${rsi.toFixed(0)}`,
metadata: { indicators }
};
}
// Short entry: Overbought conditions
if (zScore > this.Z_SCORE_ENTRY && rsi > this.RSI_OVERBOUGHT && sma20 < sma50) {
return {
type: 'sell',
symbol,
strength: Math.min(Math.abs(zScore) / 3, 1),
reason: `Mean reversion short: Z-score ${zScore.toFixed(2)}, RSI ${rsi.toFixed(0)}`,
metadata: { indicators }
};
}
}
// Exit signals
if (position > 0) {
// Exit long: Price reverted to mean or stop loss
if (zScore > -this.Z_SCORE_EXIT || zScore > this.Z_SCORE_ENTRY) {
return {
type: 'close',
symbol,
strength: 1,
reason: `Exit long: Z-score ${zScore.toFixed(2)}`,
metadata: { indicators }
};
}
} else if (position < 0) {
// Exit short: Price reverted to mean or stop loss
if (zScore < this.Z_SCORE_EXIT || zScore < -this.Z_SCORE_ENTRY) {
return {
type: 'close',
symbol,
strength: 1,
reason: `Exit short: Z-score ${zScore.toFixed(2)}`,
metadata: { indicators }
};
}
}
return null;
}
private calculateSMA(prices: number[], period: number): number {
const relevantPrices = prices.slice(-period);
return relevantPrices.reduce((sum, p) => sum + p, 0) / relevantPrices.length;
}
private calculateStdDev(prices: number[], mean: number): number {
const squaredDiffs = prices.map(p => Math.pow(p - mean, 2));
const variance = squaredDiffs.reduce((sum, d) => sum + d, 0) / prices.length;
return Math.sqrt(variance);
}
private calculateRSI(prices: number[], period: number = 14): number {
if (prices.length < period + 1) return 50;
let gains = 0;
let losses = 0;
// Calculate initial average gain/loss
for (let i = 1; i <= period; i++) {
const change = prices[prices.length - i] - prices[prices.length - i - 1];
if (change > 0) {
gains += change;
} else {
losses += Math.abs(change);
}
}
const avgGain = gains / period;
const avgLoss = losses / period;
if (avgLoss === 0) return 100;
const rs = avgGain / avgLoss;
const rsi = 100 - (100 / (1 + rs));
return rsi;
}
protected onStart(): void {
logger.info(`Mean Reversion Strategy started with symbols: ${this.config.symbols.join(', ')}`);
}
protected onStop(): void {
logger.info('Mean Reversion Strategy stopped');
// Clear indicators
this.priceHistory.clear();
this.indicators.clear();
}
protected onConfigUpdate(updates: any): void {
logger.info('Mean Reversion Strategy config updated:', updates);
}
}

View file

@ -0,0 +1,165 @@
import { z } from 'zod';
// Trading modes
export const TradingModeSchema = z.enum(['backtest', 'paper', 'live']);
export type TradingMode = z.infer<typeof TradingModeSchema>;
// Mode configurations
export const BacktestConfigSchema = z.object({
mode: z.literal('backtest'),
startDate: z.string().datetime(),
endDate: z.string().datetime(),
symbols: z.array(z.string()),
initialCapital: z.number().positive(),
dataFrequency: z.enum(['1m', '5m', '15m', '1h', '1d']),
fillModel: z.object({
slippage: z.enum(['zero', 'conservative', 'realistic', 'aggressive']),
marketImpact: z.boolean(),
partialFills: z.boolean()
}).optional(),
speed: z.enum(['max', 'realtime', '2x', '5x', '10x']).default('max')
});
export const PaperConfigSchema = z.object({
mode: z.literal('paper'),
startingCapital: z.number().positive(),
fillModel: z.object({
useRealOrderBook: z.boolean().default(true),
addLatency: z.number().min(0).default(100)
}).optional()
});
export const LiveConfigSchema = z.object({
mode: z.literal('live'),
broker: z.string(),
accountId: z.string(),
accountType: z.enum(['cash', 'margin']),
riskLimits: z.object({
maxPositionSize: z.number().positive(),
maxDailyLoss: z.number().positive(),
maxOrderSize: z.number().positive(),
maxGrossExposure: z.number().positive(),
maxSymbolExposure: z.number().positive()
})
});
export const ModeConfigSchema = z.discriminatedUnion('mode', [
BacktestConfigSchema,
PaperConfigSchema,
LiveConfigSchema
]);
export type ModeConfig = z.infer<typeof ModeConfigSchema>;
// Market data types
export const QuoteSchema = z.object({
symbol: z.string(),
bid: z.number(),
ask: z.number(),
bidSize: z.number(),
askSize: z.number(),
timestamp: z.number()
});
export const TradeSchema = z.object({
symbol: z.string(),
price: z.number(),
size: z.number(),
side: z.enum(['buy', 'sell']),
timestamp: z.number()
});
export const BarSchema = z.object({
symbol: z.string(),
open: z.number(),
high: z.number(),
low: z.number(),
close: z.number(),
volume: z.number(),
vwap: z.number().optional(),
timestamp: z.number()
});
export const MarketDataSchema = z.discriminatedUnion('type', [
z.object({ type: z.literal('quote'), data: QuoteSchema }),
z.object({ type: z.literal('trade'), data: TradeSchema }),
z.object({ type: z.literal('bar'), data: BarSchema })
]);
export type MarketData = z.infer<typeof MarketDataSchema>;
export type Quote = z.infer<typeof QuoteSchema>;
export type Trade = z.infer<typeof TradeSchema>;
export type Bar = z.infer<typeof BarSchema>;
// Order types
export const OrderSideSchema = z.enum(['buy', 'sell']);
export const OrderTypeSchema = z.enum(['market', 'limit', 'stop', 'stop_limit']);
export const TimeInForceSchema = z.enum(['DAY', 'GTC', 'IOC', 'FOK']);
export const OrderRequestSchema = z.object({
symbol: z.string(),
side: OrderSideSchema,
quantity: z.number().positive(),
orderType: OrderTypeSchema,
limitPrice: z.number().positive().optional(),
stopPrice: z.number().positive().optional(),
timeInForce: TimeInForceSchema.default('DAY'),
clientOrderId: z.string().optional()
});
export type OrderRequest = z.infer<typeof OrderRequestSchema>;
// Position types
export const PositionSchema = z.object({
symbol: z.string(),
quantity: z.number(),
averagePrice: z.number(),
realizedPnl: z.number(),
unrealizedPnl: z.number(),
totalCost: z.number(),
lastUpdate: z.string().datetime()
});
export type Position = z.infer<typeof PositionSchema>;
// Strategy types
export const StrategyConfigSchema = z.object({
id: z.string(),
name: z.string(),
enabled: z.boolean(),
parameters: z.record(z.any()),
symbols: z.array(z.string()),
allocation: z.number().min(0).max(1)
});
export type StrategyConfig = z.infer<typeof StrategyConfigSchema>;
// Analytics types
export const PerformanceMetricsSchema = z.object({
totalReturn: z.number(),
sharpeRatio: z.number(),
sortinoRatio: z.number(),
maxDrawdown: z.number(),
winRate: z.number(),
profitFactor: z.number(),
avgWin: z.number(),
avgLoss: z.number(),
totalTrades: z.number()
});
export type PerformanceMetrics = z.infer<typeof PerformanceMetricsSchema>;
// Risk types
export const RiskMetricsSchema = z.object({
currentExposure: z.number(),
dailyPnl: z.number(),
positionCount: z.number(),
grossExposure: z.number(),
var95: z.number().optional(),
cvar95: z.number().optional()
});
export type RiskMetrics = z.infer<typeof RiskMetricsSchema>;
// Re-export specialized types
export { MarketMicrostructure, PriceLevel, OrderBookSnapshot } from './types/MarketMicrostructure';

View file

@ -0,0 +1,29 @@
export interface MarketMicrostructure {
symbol: string;
avgSpreadBps: number;
dailyVolume: number;
avgTradeSize: number;
volatility: number;
tickSize: number;
lotSize: number;
intradayVolumeProfile: number[]; // 24 hourly buckets as percentages
}
export interface PriceLevel {
price: number;
size: number;
orderCount?: number;
hiddenSize?: number; // For modeling iceberg orders
}
export interface OrderBookSnapshot {
symbol: string;
timestamp: Date;
bids: PriceLevel[];
asks: PriceLevel[];
lastTrade?: {
price: number;
size: number;
side: 'buy' | 'sell';
};
}