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,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();
}
}