diff --git a/apps/stock/config/config/default.json b/apps/stock/config/config/default.json index 0ebc0f0..6f127f8 100644 --- a/apps/stock/config/config/default.json +++ b/apps/stock/config/config/default.json @@ -222,6 +222,21 @@ "origins": ["http://localhost:3000", "http://localhost:4200"], "credentials": true } + }, + "orchestrator": { + "port": 2004, + "defaultMode": "paper", + "paperTradingCapital": 100000, + "enableWebSocket": true, + "backtesting": { + "maxConcurrent": 5, + "defaultSpeed": "max", + "dataResolutions": ["1m", "5m", "15m", "1h", "1d"] + }, + "strategies": { + "maxActive": 10, + "watchdogInterval": 5000 + } } } } diff --git a/apps/stock/config/src/config-instance.ts b/apps/stock/config/src/config-instance.ts index b956d79..fb80626 100644 --- a/apps/stock/config/src/config-instance.ts +++ b/apps/stock/config/src/config-instance.ts @@ -1,16 +1,21 @@ import * as path from 'path'; +import { fileURLToPath } from 'url'; import { ConfigManager, createAppConfig } from '@stock-bot/config'; import { getLogger } from '@stock-bot/logger'; import { stockAppSchema, type StockAppConfig } from './schemas'; let configInstance: ConfigManager | null = null; +// ESM-compatible __dirname +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + /** * Initialize the stock application configuration * @param serviceName - Optional service name to override port configuration */ export function initializeStockConfig( - serviceName?: 'dataIngestion' | 'dataPipeline' | 'webApi' + serviceName?: 'dataIngestion' | 'dataPipeline' | 'webApi' | 'orchestrator' ): StockAppConfig { try { if (!configInstance) { diff --git a/apps/stock/config/src/schemas/stock-app.schema.ts b/apps/stock/config/src/schemas/stock-app.schema.ts index a06b960..9b7f6ad 100644 --- a/apps/stock/config/src/schemas/stock-app.schema.ts +++ b/apps/stock/config/src/schemas/stock-app.schema.ts @@ -89,6 +89,27 @@ export const stockAppSchema = baseAppSchema.extend({ .optional(), }) .optional(), + orchestrator: z + .object({ + port: z.number().default(3002), + defaultMode: z.enum(['backtest', 'paper', 'live']).default('paper'), + paperTradingCapital: z.number().default(100000), + enableWebSocket: z.boolean().default(true), + backtesting: z + .object({ + maxConcurrent: z.number().default(5), + defaultSpeed: z.string().default('max'), + dataResolutions: z.array(z.string()).default(['1m', '5m', '15m', '1h', '1d']), + }) + .optional(), + strategies: z + .object({ + maxActive: z.number().default(10), + defaultTimeout: z.number().default(30000), + }) + .optional(), + }) + .optional(), }) .optional(), }); diff --git a/apps/stock/core/index.js b/apps/stock/core/index.js index f1bae1d..aab76ce 100644 --- a/apps/stock/core/index.js +++ b/apps/stock/core/index.js @@ -1,251 +1,21 @@ -const { existsSync, readFileSync } = require('fs') +/* tslint:disable */ +/* eslint-disable */ + +const { existsSync } = require('fs') const { join } = require('path') -const { platform, arch } = process - let nativeBinding = null -let localFileExisted = false -let loadError = null -function isMusl() { - // For Node 10 - if (!process.report || typeof process.report.getReport !== 'function') { - try { - const lddPath = require('child_process').execSync('which ldd 2>/dev/null', { encoding: 'utf8' }) - return readFileSync(lddPath, 'utf8').includes('musl') - } catch (e) { - return true - } +// Try to load the native binding +try { + if (existsSync(join(__dirname, 'index.node'))) { + nativeBinding = require('./index.node') } else { - const { glibcVersionRuntime } = process.report.getReport().header - return !glibcVersionRuntime + throw new Error('index.node not found') } +} catch (e) { + throw new Error(`Failed to load native binding: ${e.message}`) } -switch (platform) { - case 'android': - switch (arch) { - case 'arm64': - localFileExisted = existsSync(join(__dirname, 'core.android-arm64.node')) - try { - if (localFileExisted) { - nativeBinding = require('./core.android-arm64.node') - } else { - nativeBinding = require('@stock-bot/core-android-arm64') - } - } catch (e) { - loadError = e - } - break - case 'arm': - localFileExisted = existsSync(join(__dirname, 'core.android-arm-eabi.node')) - try { - if (localFileExisted) { - nativeBinding = require('./core.android-arm-eabi.node') - } else { - nativeBinding = require('@stock-bot/core-android-arm-eabi') - } - } catch (e) { - loadError = e - } - break - default: - throw new Error(`Unsupported architecture on Android ${arch}`) - } - break - case 'win32': - switch (arch) { - case 'x64': - localFileExisted = existsSync( - join(__dirname, 'core.win32-x64-msvc.node') - ) - try { - if (localFileExisted) { - nativeBinding = require('./core.win32-x64-msvc.node') - } else { - nativeBinding = require('@stock-bot/core-win32-x64-msvc') - } - } catch (e) { - loadError = e - } - break - case 'ia32': - localFileExisted = existsSync( - join(__dirname, 'core.win32-ia32-msvc.node') - ) - try { - if (localFileExisted) { - nativeBinding = require('./core.win32-ia32-msvc.node') - } else { - nativeBinding = require('@stock-bot/core-win32-ia32-msvc') - } - } catch (e) { - loadError = e - } - break - case 'arm64': - localFileExisted = existsSync( - join(__dirname, 'core.win32-arm64-msvc.node') - ) - try { - if (localFileExisted) { - nativeBinding = require('./core.win32-arm64-msvc.node') - } else { - nativeBinding = require('@stock-bot/core-win32-arm64-msvc') - } - } catch (e) { - loadError = e - } - break - default: - throw new Error(`Unsupported architecture on Windows: ${arch}`) - } - break - case 'darwin': - localFileExisted = existsSync(join(__dirname, 'core.darwin-universal.node')) - try { - if (localFileExisted) { - nativeBinding = require('./core.darwin-universal.node') - } else { - nativeBinding = require('@stock-bot/core-darwin-universal') - } - break - } catch {} - switch (arch) { - case 'x64': - localFileExisted = existsSync(join(__dirname, 'core.darwin-x64.node')) - try { - if (localFileExisted) { - nativeBinding = require('./core.darwin-x64.node') - } else { - nativeBinding = require('@stock-bot/core-darwin-x64') - } - } catch (e) { - loadError = e - } - break - case 'arm64': - localFileExisted = existsSync( - join(__dirname, 'core.darwin-arm64.node') - ) - try { - if (localFileExisted) { - nativeBinding = require('./core.darwin-arm64.node') - } else { - nativeBinding = require('@stock-bot/core-darwin-arm64') - } - } catch (e) { - loadError = e - } - break - default: - throw new Error(`Unsupported architecture on macOS: ${arch}`) - } - break - case 'freebsd': - if (arch !== 'x64') { - throw new Error(`Unsupported architecture on FreeBSD: ${arch}`) - } - localFileExisted = existsSync(join(__dirname, 'core.freebsd-x64.node')) - try { - if (localFileExisted) { - nativeBinding = require('./core.freebsd-x64.node') - } else { - nativeBinding = require('@stock-bot/core-freebsd-x64') - } - } catch (e) { - loadError = e - } - break - case 'linux': - switch (arch) { - case 'x64': - if (isMusl()) { - localFileExisted = existsSync( - join(__dirname, 'core.linux-x64-musl.node') - ) - try { - if (localFileExisted) { - nativeBinding = require('./core.linux-x64-musl.node') - } else { - nativeBinding = require('@stock-bot/core-linux-x64-musl') - } - } catch (e) { - loadError = e - } - } else { - localFileExisted = existsSync( - join(__dirname, 'core.linux-x64-gnu.node') - ) - try { - if (localFileExisted) { - nativeBinding = require('./core.linux-x64-gnu.node') - } else { - nativeBinding = require('@stock-bot/core-linux-x64-gnu') - } - } catch (e) { - loadError = e - } - } - break - case 'arm64': - if (isMusl()) { - localFileExisted = existsSync( - join(__dirname, 'core.linux-arm64-musl.node') - ) - try { - if (localFileExisted) { - nativeBinding = require('./core.linux-arm64-musl.node') - } else { - nativeBinding = require('@stock-bot/core-linux-arm64-musl') - } - } catch (e) { - loadError = e - } - } else { - localFileExisted = existsSync( - join(__dirname, 'core.linux-arm64-gnu.node') - ) - try { - if (localFileExisted) { - nativeBinding = require('./core.linux-arm64-gnu.node') - } else { - nativeBinding = require('@stock-bot/core-linux-arm64-gnu') - } - } catch (e) { - loadError = e - } - } - break - case 'arm': - localFileExisted = existsSync( - join(__dirname, 'core.linux-arm-gnueabihf.node') - ) - try { - if (localFileExisted) { - nativeBinding = require('./core.linux-arm-gnueabihf.node') - } else { - nativeBinding = require('@stock-bot/core-linux-arm-gnueabihf') - } - } catch (e) { - loadError = e - } - break - default: - throw new Error(`Unsupported architecture on Linux: ${arch}`) - } - break - default: - throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`) -} - -if (!nativeBinding) { - if (loadError) { - throw loadError - } - throw new Error(`Failed to load native binding`) -} - -const { TradingEngine } = nativeBinding - -module.exports.TradingEngine = TradingEngine \ No newline at end of file +// Export all bindings +module.exports = nativeBinding \ No newline at end of file diff --git a/apps/stock/core/index.mjs b/apps/stock/core/index.mjs new file mode 100644 index 0000000..e7df1a1 --- /dev/null +++ b/apps/stock/core/index.mjs @@ -0,0 +1,29 @@ +// ESM wrapper for the native module +import { createRequire } from 'module'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; + +const require = createRequire(import.meta.url); +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const nativeBinding = require(join(__dirname, 'index.node')); + +export const { + TradingEngine, + MarketData, + MarketUpdate, + Order, + Fill, + Position, + RiskLimits, + RiskMetrics, + ExecutionResult, + OrderBookLevel, + OrderBookSnapshot, + MarketMicrostructure, + PositionUpdate, + RiskCheckResult +} = nativeBinding; + +export default nativeBinding; \ No newline at end of file diff --git a/apps/stock/core/index.node b/apps/stock/core/index.node index 7b0e51a..81251f9 100755 Binary files a/apps/stock/core/index.node and b/apps/stock/core/index.node differ diff --git a/apps/stock/core/package.json b/apps/stock/core/package.json index bd77a1e..6840ca2 100644 --- a/apps/stock/core/package.json +++ b/apps/stock/core/package.json @@ -1,11 +1,20 @@ { "name": "@stock-bot/core", "version": "1.0.0", - "main": "index.js", + "type": "module", + "main": "index.mjs", "types": "index.d.ts", + "exports": { + ".": { + "import": "./index.mjs", + "require": "./index.js", + "types": "./index.d.ts" + } + }, "files": [ "index.d.ts", "index.js", + "index.mjs", "index.node" ], "napi": { diff --git a/apps/stock/core/src/api/mod.rs b/apps/stock/core/src/api/mod.rs index 4ea637c..d01710d 100644 --- a/apps/stock/core/src/api/mod.rs +++ b/apps/stock/core/src/api/mod.rs @@ -224,9 +224,8 @@ impl TradingEngine { } #[napi] - pub fn set_microstructure(&self, _symbol: String, microstructure_json: String) -> Result<()> { - let _microstructure: MarketMicrostructure = serde_json::from_str(µstructure_json) - .map_err(|e| Error::from_reason(format!("Failed to parse microstructure: {}", e)))?; + pub fn set_microstructure(&self, symbol: String, microstructure_js: JsObject) -> Result<()> { + let microstructure = parse_microstructure(microstructure_js)?; let _core = self.core.lock(); // Store microstructure for use in fill simulation @@ -236,10 +235,43 @@ impl TradingEngine { #[napi] pub fn load_historical_data(&self, data_json: String) -> Result<()> { - let _data: Vec = serde_json::from_str(&data_json) + let data: Vec = serde_json::from_str(&data_json) .map_err(|e| Error::from_reason(format!("Failed to parse data: {}", e)))?; - // In real implementation, would load into historical data source + let core = self.core.lock(); + + // Downcast to HistoricalDataSource if in backtest mode + if let TradingMode::Backtest { .. } = core.get_mode() { + let mut data_source = core.market_data_source.write(); + if let Some(historical_source) = data_source.as_any_mut().downcast_mut::() { + historical_source.load_data(data); + } + } + + Ok(()) + } + + #[napi] + pub fn generate_mock_data(&self, symbol: String, start_time: i64, end_time: i64, seed: Option) -> Result<()> { + let core = self.core.lock(); + + // Only available in backtest mode + if let TradingMode::Backtest { .. } = core.get_mode() { + let mut data_source = core.market_data_source.write(); + if let Some(historical_source) = data_source.as_any_mut().downcast_mut::() { + let start_dt = DateTime::::from_timestamp_millis(start_time) + .ok_or_else(|| Error::from_reason("Invalid start time"))?; + let end_dt = DateTime::::from_timestamp_millis(end_time) + .ok_or_else(|| Error::from_reason("Invalid end time"))?; + + historical_source.generate_mock_data(symbol, start_dt, end_dt, seed.map(|s| s as u64)); + } else { + return Err(Error::from_reason("Failed to access historical data source")); + } + } else { + return Err(Error::from_reason("Mock data generation only available in backtest mode")); + } + Ok(()) } } @@ -323,4 +355,20 @@ fn parse_risk_limits(limits_js: JsObject) -> Result { max_gross_exposure: limits_js.get_named_property("maxGrossExposure")?, max_symbol_exposure: limits_js.get_named_property("maxSymbolExposure")?, }) +} + +fn parse_microstructure(microstructure_js: JsObject) -> Result { + let intraday_volume_profile: Vec = microstructure_js.get_named_property("intradayVolumeProfile") + .unwrap_or_else(|_| vec![1.0/24.0; 24]); + + Ok(MarketMicrostructure { + symbol: microstructure_js.get_named_property("symbol")?, + avg_spread_bps: microstructure_js.get_named_property("avgSpreadBps")?, + daily_volume: microstructure_js.get_named_property("dailyVolume")?, + avg_trade_size: microstructure_js.get_named_property("avgTradeSize")?, + volatility: microstructure_js.get_named_property("volatility")?, + tick_size: microstructure_js.get_named_property("tickSize")?, + lot_size: microstructure_js.get_named_property("lotSize")?, + intraday_volume_profile, + }) } \ No newline at end of file diff --git a/apps/stock/core/src/core/market_data_sources.rs b/apps/stock/core/src/core/market_data_sources.rs index ca05f4a..fc8f645 100644 --- a/apps/stock/core/src/core/market_data_sources.rs +++ b/apps/stock/core/src/core/market_data_sources.rs @@ -2,6 +2,7 @@ use crate::{MarketDataSource, MarketUpdate}; use chrono::{DateTime, Utc}; use parking_lot::Mutex; use std::collections::VecDeque; +use super::mock_data_generator::MockDataGenerator; // Historical data source for backtesting pub struct HistoricalDataSource { @@ -24,6 +25,19 @@ impl HistoricalDataSource { queue.extend(data); *self.current_position.lock() = 0; } + + // Generate mock data for testing + pub fn generate_mock_data( + &self, + symbol: String, + start_time: DateTime, + end_time: DateTime, + seed: Option + ) { + let mut generator = MockDataGenerator::new(seed.unwrap_or(42)); + let data = generator.generate_mixed_data(symbol, start_time, end_time); + self.load_data(data); + } } #[async_trait::async_trait] diff --git a/apps/stock/core/src/core/mock_data_generator.rs b/apps/stock/core/src/core/mock_data_generator.rs new file mode 100644 index 0000000..4584016 --- /dev/null +++ b/apps/stock/core/src/core/mock_data_generator.rs @@ -0,0 +1,229 @@ +use crate::{MarketUpdate, MarketDataType, Quote, Trade, Bar, Side}; +use chrono::{DateTime, Utc, Duration}; +use rand::{Rng, SeedableRng}; +use rand::rngs::StdRng; +use rand_distr::{Normal, Distribution}; + +pub struct MockDataGenerator { + rng: StdRng, + base_price: f64, + volatility: f64, + spread_bps: f64, + volume_mean: f64, + volume_std: f64, +} + +impl MockDataGenerator { + pub fn new(seed: u64) -> Self { + Self { + rng: StdRng::seed_from_u64(seed), + base_price: 100.0, + volatility: 0.02, // 2% daily volatility + spread_bps: 5.0, // 5 basis points spread + volume_mean: 1_000_000.0, + volume_std: 200_000.0, + } + } + + pub fn with_params(seed: u64, base_price: f64, volatility: f64, spread_bps: f64) -> Self { + Self { + rng: StdRng::seed_from_u64(seed), + base_price, + volatility, + spread_bps, + volume_mean: 1_000_000.0, + volume_std: 200_000.0, + } + } + + pub fn generate_quotes( + &mut self, + symbol: String, + start_time: DateTime, + end_time: DateTime, + interval_ms: i64, + ) -> Vec { + let mut updates = Vec::new(); + let mut current_time = start_time; + let mut price = self.base_price; + + let price_dist = Normal::new(0.0, self.volatility).unwrap(); + let volume_dist = Normal::new(self.volume_mean, self.volume_std).unwrap(); + + while current_time <= end_time { + // Generate price movement + let return_pct = price_dist.sample(&mut self.rng) / 100.0; + price *= 1.0 + return_pct; + price = price.max(0.01); // Ensure positive price + + // Calculate bid/ask + let half_spread = price * self.spread_bps / 20000.0; + let bid = price - half_spread; + let ask = price + half_spread; + + // Generate volume + let volume = volume_dist.sample(&mut self.rng).max(0.0) as u32; + + updates.push(MarketUpdate { + symbol: symbol.clone(), + timestamp: current_time, + data: MarketDataType::Quote(Quote { + bid, + ask, + bid_size: (volume / 10) as f64, + ask_size: (volume / 10) as f64, + }), + }); + + current_time = current_time + Duration::milliseconds(interval_ms); + } + + updates + } + + pub fn generate_trades( + &mut self, + symbol: String, + start_time: DateTime, + end_time: DateTime, + trades_per_minute: u32, + ) -> Vec { + let mut updates = Vec::new(); + let mut current_time = start_time; + let mut price = self.base_price; + + let price_dist = Normal::new(0.0, self.volatility / 100.0).unwrap(); + let volume_dist = Normal::new(100.0, 50.0).unwrap(); + + let interval_ms = 60_000 / trades_per_minute as i64; + + while current_time <= end_time { + // Generate price movement + let return_pct = price_dist.sample(&mut self.rng); + price *= 1.0 + return_pct; + price = price.max(0.01); + + // Generate trade size + let raw_size: f64 = volume_dist.sample(&mut self.rng); + let size = raw_size.max(1.0) as u32; + + // Random buy/sell + let is_buy = self.rng.gen_bool(0.5); + + updates.push(MarketUpdate { + symbol: symbol.clone(), + timestamp: current_time, + data: MarketDataType::Trade(Trade { + price, + size: size as f64, + side: if is_buy { Side::Buy } else { Side::Sell }, + }), + }); + + current_time = current_time + Duration::milliseconds(interval_ms); + } + + updates + } + + pub fn generate_bars( + &mut self, + symbol: String, + start_time: DateTime, + end_time: DateTime, + timeframe: &str, + ) -> Vec { + let mut updates = Vec::new(); + let mut current_time = start_time; + let mut price = self.base_price; + + let interval = match timeframe { + "1m" => Duration::minutes(1), + "5m" => Duration::minutes(5), + "15m" => Duration::minutes(15), + "1h" => Duration::hours(1), + "1d" => Duration::days(1), + _ => Duration::minutes(1), + }; + + let price_dist = Normal::new(0.0, self.volatility).unwrap(); + let volume_dist = Normal::new(self.volume_mean, self.volume_std).unwrap(); + + while current_time <= end_time { + // Generate OHLC + let open = price; + let mut high = open; + let mut low = open; + + // Simulate intrabar movements + for _ in 0..4 { + let move_pct = price_dist.sample(&mut self.rng) / 100.0; + price *= 1.0 + move_pct; + price = price.max(0.01); + high = high.max(price); + low = low.min(price); + } + + let close = price; + let volume = volume_dist.sample(&mut self.rng).max(0.0) as u64; + + updates.push(MarketUpdate { + symbol: symbol.clone(), + timestamp: current_time, + data: MarketDataType::Bar(Bar { + open, + high, + low, + close, + volume: volume as f64, + vwap: Some((open + high + low + close) / 4.0), + }), + }); + + current_time = current_time + interval; + } + + updates + } + + pub fn generate_mixed_data( + &mut self, + symbol: String, + start_time: DateTime, + end_time: DateTime, + ) -> Vec { + let mut all_updates = Vec::new(); + + // Generate quotes every 100ms + let quotes = self.generate_quotes( + symbol.clone(), + start_time, + end_time, + 100 + ); + all_updates.extend(quotes); + + // Generate trades + let trades = self.generate_trades( + symbol.clone(), + start_time, + end_time, + 20 // 20 trades per minute + ); + all_updates.extend(trades); + + // Generate 1-minute bars + let bars = self.generate_bars( + symbol, + start_time, + end_time, + "1m" + ); + all_updates.extend(bars); + + // Sort by timestamp + all_updates.sort_by_key(|update| update.timestamp); + + all_updates + } +} \ No newline at end of file diff --git a/apps/stock/core/src/core/mod.rs b/apps/stock/core/src/core/mod.rs index c01b0bf..84eb333 100644 --- a/apps/stock/core/src/core/mod.rs +++ b/apps/stock/core/src/core/mod.rs @@ -2,6 +2,7 @@ pub mod time_providers; pub mod market_data_sources; pub mod execution_handlers; pub mod market_microstructure; +pub mod mock_data_generator; use crate::{MarketDataSource, ExecutionHandler, TimeProvider, TradingMode}; diff --git a/apps/stock/orchestrator/package.json b/apps/stock/orchestrator/package.json index 2e4a495..d550613 100644 --- a/apps/stock/orchestrator/package.json +++ b/apps/stock/orchestrator/package.json @@ -19,13 +19,15 @@ "@stock-bot/questdb": "*", "@stock-bot/queue": "*", "@stock-bot/shutdown": "*", + "@stock-bot/stock-config": "*", "@stock-bot/utils": "*", "hono": "^4.0.0", "socket.io": "^4.7.2", "socket.io-client": "^4.7.2", "zod": "^3.22.0", "uuid": "^9.0.0", - "axios": "^1.6.0" + "axios": "^1.6.0", + "simple-statistics": "^7.8.3" }, "devDependencies": { "@types/node": "^20.0.0", diff --git a/apps/stock/orchestrator/src/analytics/PerformanceAnalyzer.ts b/apps/stock/orchestrator/src/analytics/PerformanceAnalyzer.ts index c282020..f268d37 100644 --- a/apps/stock/orchestrator/src/analytics/PerformanceAnalyzer.ts +++ b/apps/stock/orchestrator/src/analytics/PerformanceAnalyzer.ts @@ -1,4 +1,6 @@ -import { logger } from '@stock-bot/logger'; +import { getLogger } from '@stock-bot/logger'; + +const logger = getLogger('PerformanceAnalyzer'); import * as stats from 'simple-statistics'; export interface Trade { @@ -168,6 +170,19 @@ export class PerformanceAnalyzer { analyzeDrawdowns(): DrawdownAnalysis { const drawdowns: number[] = []; const underwaterCurve: Array<{ date: Date; drawdown: number }> = []; + + // Handle empty equity curve + if (this.equityCurve.length === 0) { + return { + maxDrawdown: 0, + averageDrawdown: 0, + maxDrawdownDuration: 0, + underwaterTime: 0, + drawdownPeriods: [], + currentDrawdown: 0 + }; + } + let peak = this.equityCurve[0].value; let maxDrawdown = 0; let currentDrawdownStart: Date | null = null; diff --git a/apps/stock/orchestrator/src/api/rest/analytics.ts b/apps/stock/orchestrator/src/api/rest/analytics.ts index 17c1561..a06da60 100644 --- a/apps/stock/orchestrator/src/api/rest/analytics.ts +++ b/apps/stock/orchestrator/src/api/rest/analytics.ts @@ -1,8 +1,7 @@ import { Hono } from 'hono'; import { z } from 'zod'; -import { logger } from '@stock-bot/logger'; +import { IServiceContainer } from '@stock-bot/di'; import { AnalyticsService } from '../../services/AnalyticsService'; -import { container } from '../../container'; const DateRangeSchema = z.object({ startDate: z.string().datetime(), @@ -20,9 +19,9 @@ const OptimizationRequestSchema = z.object({ }).optional() }); -export function createAnalyticsRoutes(): Hono { +export function createAnalyticsRoutes(container: IServiceContainer): Hono { const app = new Hono(); - const analyticsService = container.get('AnalyticsService') as AnalyticsService; + const analyticsService = container.custom?.AnalyticsService as AnalyticsService; // Get performance metrics app.get('/performance/:portfolioId', async (c) => { @@ -50,7 +49,7 @@ export function createAnalyticsRoutes(): Hono { }, 400); } - logger.error('Error getting performance metrics:', error); + container.logger.error('Error getting performance metrics:', error); return c.json({ error: error instanceof Error ? error.message : 'Failed to get performance metrics' }, 500); @@ -77,7 +76,7 @@ export function createAnalyticsRoutes(): Hono { }, 400); } - logger.error('Error optimizing portfolio:', error); + container.logger.error('Error optimizing portfolio:', error); return c.json({ error: error instanceof Error ? error.message : 'Failed to optimize portfolio' }, 500); @@ -92,7 +91,7 @@ export function createAnalyticsRoutes(): Hono { return c.json(metrics); } catch (error) { - logger.error('Error getting risk metrics:', error); + container.logger.error('Error getting risk metrics:', error); return c.json({ error: error instanceof Error ? error.message : 'Failed to get risk metrics' }, 500); @@ -109,7 +108,7 @@ export function createAnalyticsRoutes(): Hono { timestamp: new Date().toISOString() }); } catch (error) { - logger.error('Error detecting market regime:', error); + container.logger.error('Error detecting market regime:', error); return c.json({ error: error instanceof Error ? error.message : 'Failed to detect market regime' }, 500); @@ -138,7 +137,7 @@ export function createAnalyticsRoutes(): Hono { }, 400); } - logger.error('Error calculating correlation:', error); + container.logger.error('Error calculating correlation:', error); return c.json({ error: error instanceof Error ? error.message : 'Failed to calculate correlation' }, 500); @@ -169,7 +168,7 @@ export function createAnalyticsRoutes(): Hono { }, 400); } - logger.error('Error making prediction:', error); + container.logger.error('Error making prediction:', error); return c.json({ error: error instanceof Error ? error.message : 'Failed to make prediction' }, 500); diff --git a/apps/stock/orchestrator/src/api/rest/backtest.ts b/apps/stock/orchestrator/src/api/rest/backtest.ts index 7e5e290..303634c 100644 --- a/apps/stock/orchestrator/src/api/rest/backtest.ts +++ b/apps/stock/orchestrator/src/api/rest/backtest.ts @@ -1,21 +1,48 @@ import { Hono } from 'hono'; import { z } from 'zod'; -import { logger } from '@stock-bot/logger'; +import { IServiceContainer } from '@stock-bot/di'; 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 { +export function createBacktestRoutes(container: IServiceContainer): Hono { const app = new Hono(); - const backtestEngine = container.get('BacktestEngine') as BacktestEngine; - const modeManager = container.get('ModeManager') as ModeManager; + const backtestEngine = container.custom?.BacktestEngine as BacktestEngine; + const modeManager = container.custom?.ModeManager as ModeManager; - // Run new backtest + // Default POST to / is the same as /run for backward compatibility + app.post('/', 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); + } + + container.logger.error('Error running backtest:', error); + return c.json({ + error: error instanceof Error ? error.message : 'Failed to run backtest' + }, 500); + } + }); + + // Run new backtest (same as above but at /run) app.post('/run', async (c) => { try { const body = await c.req.json(); @@ -36,7 +63,7 @@ export function createBacktestRoutes(): Hono { }, 400); } - logger.error('Error running backtest:', error); + container.logger.error('Error running backtest:', error); return c.json({ error: error instanceof Error ? error.message : 'Failed to run backtest' }, 500); @@ -53,7 +80,7 @@ export function createBacktestRoutes(): Hono { timestamp: new Date().toISOString() }); } catch (error) { - logger.error('Error stopping backtest:', error); + container.logger.error('Error stopping backtest:', error); return c.json({ error: error instanceof Error ? error.message : 'Failed to stop backtest' }, 500); @@ -72,7 +99,7 @@ export function createBacktestRoutes(): Hono { currentTime: new Date().toISOString() }); } catch (error) { - logger.error('Error getting backtest progress:', error); + container.logger.error('Error getting backtest progress:', error); return c.json({ error: error instanceof Error ? error.message : 'Failed to get progress' }, 500); diff --git a/apps/stock/orchestrator/src/api/rest/orders.ts b/apps/stock/orchestrator/src/api/rest/orders.ts index 847ea08..43a3da3 100644 --- a/apps/stock/orchestrator/src/api/rest/orders.ts +++ b/apps/stock/orchestrator/src/api/rest/orders.ts @@ -1,17 +1,16 @@ import { Hono } from 'hono'; import { z } from 'zod'; -import { logger } from '@stock-bot/logger'; +import { IServiceContainer } from '@stock-bot/di'; import { OrderRequestSchema } from '../../types'; import { ExecutionService } from '../../services/ExecutionService'; -import { container } from '../../container'; const OrderIdSchema = z.object({ orderId: z.string() }); -export function createOrderRoutes(): Hono { +export function createOrderRoutes(container: IServiceContainer): Hono { const app = new Hono(); - const executionService = container.get('ExecutionService') as ExecutionService; + const executionService = container.custom?.ExecutionService as ExecutionService; // Submit new order app.post('/', async (c) => { @@ -30,7 +29,7 @@ export function createOrderRoutes(): Hono { }, 400); } - logger.error('Error submitting order:', error); + container.logger.error('Error submitting order:', error); return c.json({ error: error instanceof Error ? error.message : 'Failed to submit order' }, 500); @@ -50,7 +49,7 @@ export function createOrderRoutes(): Hono { return c.json({ error: 'Order not found or already filled' }, 404); } } catch (error) { - logger.error('Error cancelling order:', error); + container.logger.error('Error cancelling order:', error); return c.json({ error: error instanceof Error ? error.message : 'Failed to cancel order' }, 500); @@ -70,7 +69,7 @@ export function createOrderRoutes(): Hono { return c.json({ error: 'Order not found' }, 404); } } catch (error) { - logger.error('Error getting order status:', error); + container.logger.error('Error getting order status:', error); return c.json({ error: error instanceof Error ? error.message : 'Failed to get order status' }, 500); @@ -101,7 +100,7 @@ export function createOrderRoutes(): Hono { }, 400); } - logger.error('Error submitting batch orders:', error); + container.logger.error('Error submitting batch orders:', error); return c.json({ error: error instanceof Error ? error.message : 'Failed to submit batch orders' }, 500); diff --git a/apps/stock/orchestrator/src/api/rest/positions.ts b/apps/stock/orchestrator/src/api/rest/positions.ts index 1697965..aef9226 100644 --- a/apps/stock/orchestrator/src/api/rest/positions.ts +++ b/apps/stock/orchestrator/src/api/rest/positions.ts @@ -1,16 +1,15 @@ import { Hono } from 'hono'; import { z } from 'zod'; -import { logger } from '@stock-bot/logger'; +import { IServiceContainer } from '@stock-bot/di'; import { ModeManager } from '../../core/ModeManager'; -import { container } from '../../container'; const SymbolSchema = z.object({ symbol: z.string() }); -export function createPositionRoutes(): Hono { +export function createPositionRoutes(container: IServiceContainer): Hono { const app = new Hono(); - const modeManager = container.get('ModeManager') as ModeManager; + const modeManager = container.custom?.ModeManager as ModeManager; // Get all positions app.get('/', async (c) => { @@ -23,7 +22,7 @@ export function createPositionRoutes(): Hono { positions }); } catch (error) { - logger.error('Error getting positions:', error); + container.logger.error('Error getting positions:', error); return c.json({ error: error instanceof Error ? error.message : 'Failed to get positions' }, 500); @@ -41,7 +40,7 @@ export function createPositionRoutes(): Hono { positions }); } catch (error) { - logger.error('Error getting open positions:', error); + container.logger.error('Error getting open positions:', error); return c.json({ error: error instanceof Error ? error.message : 'Failed to get open positions' }, 500); @@ -69,7 +68,7 @@ export function createPositionRoutes(): Hono { }, 404); } } catch (error) { - logger.error('Error getting position:', error); + container.logger.error('Error getting position:', error); return c.json({ error: error instanceof Error ? error.message : 'Failed to get position' }, 500); @@ -92,7 +91,7 @@ export function createPositionRoutes(): Hono { timestamp: new Date().toISOString() }); } catch (error) { - logger.error('Error getting P&L:', error); + container.logger.error('Error getting P&L:', error); return c.json({ error: error instanceof Error ? error.message : 'Failed to get P&L' }, 500); @@ -111,7 +110,7 @@ export function createPositionRoutes(): Hono { timestamp: new Date().toISOString() }); } catch (error) { - logger.error('Error getting risk metrics:', error); + container.logger.error('Error getting risk metrics:', error); return c.json({ error: error instanceof Error ? error.message : 'Failed to get risk metrics' }, 500); diff --git a/apps/stock/orchestrator/src/api/websocket/index.ts b/apps/stock/orchestrator/src/api/websocket/index.ts index a95ca5c..96ee224 100644 --- a/apps/stock/orchestrator/src/api/websocket/index.ts +++ b/apps/stock/orchestrator/src/api/websocket/index.ts @@ -1,10 +1,9 @@ import { Server as SocketIOServer, Socket } from 'socket.io'; -import { logger } from '@stock-bot/logger'; import { z } from 'zod'; +import { IServiceContainer } from '@stock-bot/di'; 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()), @@ -15,16 +14,16 @@ 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; +export function setupWebSocketHandlers(io: SocketIOServer, container: IServiceContainer): void { + const marketDataService = container.custom?.MarketDataService as MarketDataService; + const executionService = container.custom?.ExecutionService as ExecutionService; + const modeManager = container.custom?.ModeManager as ModeManager; // Track client subscriptions const clientSubscriptions = new Map>(); io.on('connection', (socket: Socket) => { - logger.info(`WebSocket client connected: ${socket.id}`); + container.logger.info(`WebSocket client connected: ${socket.id}`); clientSubscriptions.set(socket.id, new Set()); // Send initial connection info @@ -44,13 +43,13 @@ export function setupWebSocketHandlers(io: SocketIOServer, container: Container) subscriptions.add(symbol); } - logger.debug(`Client ${socket.id} subscribed to: ${symbols.join(', ')}`); + container.logger.debug(`Client ${socket.id} subscribed to: ${symbols.join(', ')}`); if (callback) { callback({ success: true, symbols }); } } catch (error) { - logger.error('Subscription error:', error); + container.logger.error('Subscription error:', error); if (callback) { callback({ success: false, @@ -83,13 +82,13 @@ export function setupWebSocketHandlers(io: SocketIOServer, container: Container) } } - logger.debug(`Client ${socket.id} unsubscribed from: ${symbols.join(', ')}`); + container.logger.debug(`Client ${socket.id} unsubscribed from: ${symbols.join(', ')}`); if (callback) { callback({ success: true, symbols }); } } catch (error) { - logger.error('Unsubscribe error:', error); + container.logger.error('Unsubscribe error:', error); if (callback) { callback({ success: false, @@ -107,7 +106,7 @@ export function setupWebSocketHandlers(io: SocketIOServer, container: Container) callback({ success: true, result }); } } catch (error) { - logger.error('Order submission error:', error); + container.logger.error('Order submission error:', error); if (callback) { callback({ success: false, @@ -127,7 +126,7 @@ export function setupWebSocketHandlers(io: SocketIOServer, container: Container) callback({ success: true, positions }); } } catch (error) { - logger.error('Error getting positions:', error); + container.logger.error('Error getting positions:', error); if (callback) { callback({ success: false, @@ -139,7 +138,7 @@ export function setupWebSocketHandlers(io: SocketIOServer, container: Container) // Handle disconnection socket.on('disconnect', async () => { - logger.info(`WebSocket client disconnected: ${socket.id}`); + container.logger.info(`WebSocket client disconnected: ${socket.id}`); // Unsubscribe from all symbols for this client const subscriptions = clientSubscriptions.get(socket.id); @@ -191,5 +190,5 @@ export function setupWebSocketHandlers(io: SocketIOServer, container: Container) }); }); - logger.info('WebSocket handlers initialized'); + container.logger.info('WebSocket handlers initialized'); } \ No newline at end of file diff --git a/apps/stock/orchestrator/src/backtest/BacktestEngine.ts b/apps/stock/orchestrator/src/backtest/BacktestEngine.ts index 718423c..48f9a22 100644 --- a/apps/stock/orchestrator/src/backtest/BacktestEngine.ts +++ b/apps/stock/orchestrator/src/backtest/BacktestEngine.ts @@ -1,12 +1,11 @@ -import { logger } from '@stock-bot/logger'; import { EventEmitter } from 'events'; -import { MarketData, BacktestConfigSchema, PerformanceMetrics, MarketMicrostructure } from '../types'; +import { IServiceContainer } from '@stock-bot/di'; +import { PerformanceAnalyzer } from '../analytics/PerformanceAnalyzer'; +import { DataManager } from '../data/DataManager'; import { StorageService } from '../services/StorageService'; import { StrategyManager } from '../strategies/StrategyManager'; -import { TradingEngine } from '../../core'; -import { DataManager } from '../data/DataManager'; +import { BacktestConfigSchema, MarketData, MarketMicrostructure, PerformanceMetrics } from '../types'; import { MarketSimulator } from './MarketSimulator'; -import { PerformanceAnalyzer } from '../analytics/PerformanceAnalyzer'; interface BacktestEvent { timestamp: number; @@ -35,12 +34,16 @@ export class BacktestEngine extends EventEmitter { private marketSimulator: MarketSimulator; private performanceAnalyzer: PerformanceAnalyzer; private microstructures: Map = new Map(); + private container: IServiceContainer; + private initialCapital: number = 100000; constructor( + container: IServiceContainer, private storageService: StorageService, private strategyManager: StrategyManager ) { super(); + this.container = container; this.dataManager = new DataManager(storageService); this.marketSimulator = new MarketSimulator({ useHistoricalSpreads: true, @@ -55,11 +58,24 @@ export class BacktestEngine extends EventEmitter { // Validate config const validatedConfig = BacktestConfigSchema.parse(config); - logger.info(`Starting backtest from ${validatedConfig.startDate} to ${validatedConfig.endDate}`); + this.container.logger.info(`Starting backtest from ${validatedConfig.startDate} to ${validatedConfig.endDate}`); // Reset state this.reset(); this.isRunning = true; + this.initialCapital = validatedConfig.initialCapital; + + // Initialize equity curve with starting capital + this.equityCurve.push({ + timestamp: new Date(validatedConfig.startDate).getTime(), + value: this.initialCapital + }); + + // Initialize performance analyzer with starting capital + this.performanceAnalyzer.addEquityPoint( + new Date(validatedConfig.startDate), + this.initialCapital + ); // Generate backtest ID const backtestId = `backtest_${Date.now()}`; @@ -84,7 +100,7 @@ export class BacktestEngine extends EventEmitter { }); marketData.sort((a, b) => a.data.timestamp - b.data.timestamp); - logger.info(`Loaded ${marketData.length} market data points`); + this.container.logger.info(`Loaded ${marketData.length} market data points`); // Initialize strategies await this.strategyManager.initializeStrategies(validatedConfig.strategies || []); @@ -110,17 +126,18 @@ export class BacktestEngine extends EventEmitter { equityCurve: this.equityCurve, drawdown: this.calculateDrawdown(), dailyReturns: this.calculateDailyReturns(), - finalPositions + finalPositions, + ohlcData: this.getOHLCData(marketData, validatedConfig.symbols) }; await this.storeResults(result); - logger.info(`Backtest completed: ${performance.totalTrades} trades, ${performance.totalReturn}% return`); + this.container.logger.info(`Backtest completed: ${performance.totalTrades} trades, ${performance.totalReturn}% return`); return result; } catch (error) { - logger.error('Backtest failed:', error); + this.container.logger.error('Backtest failed:', error); throw error; } finally { this.isRunning = false; @@ -133,30 +150,65 @@ export class BacktestEngine extends EventEmitter { 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() + try { + for (const symbol of config.symbols) { + const bars = await this.storageService.getHistoricalBars( + symbol, + startDate, + endDate, + config.dataFrequency + ); + + // If no data found, use mock data + if (!bars || bars.length === 0) { + this.container.logger.warn(`No historical data found for ${symbol}, using mock data`); + + // Tell the Rust core to generate mock data + const tradingEngine = this.strategyManager.getTradingEngine(); + if (tradingEngine && tradingEngine.generateMockData) { + await tradingEngine.generateMockData( + symbol, + startDate.getTime(), + endDate.getTime(), + 42 // seed for reproducibility + ); + + // For now, we'll generate mock data on the TypeScript side + // as the Rust integration needs more work + const mockData = this.generateMockData(symbol, startDate, endDate); + data.push(...mockData); + } else { + // Fallback to TypeScript mock data generation + const mockData = this.generateMockData(symbol, startDate, endDate); + data.push(...mockData); } - }); - }); + } else { + // 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() + } + }); + }); + } + } + } catch (error) { + this.container.logger.warn('Error loading historical data, using mock data:', error); + + // Generate mock data for all symbols + for (const symbol of config.symbols) { + const mockData = this.generateMockData(symbol, startDate, endDate); + data.push(...mockData); + } } // Sort by timestamp @@ -168,6 +220,48 @@ export class BacktestEngine extends EventEmitter { return data; } + + private generateMockData(symbol: string, startDate: Date, endDate: Date): MarketData[] { + const data: MarketData[] = []; + const startTime = startDate.getTime(); + const endTime = endDate.getTime(); + const interval = 24 * 60 * 60 * 1000; // 1 day in milliseconds + + let price = 100; // Base price + let currentTime = startTime; + + while (currentTime <= endTime) { + // Generate random price movement + const changePercent = (Math.random() - 0.5) * 0.04; // +/- 2% daily + price = price * (1 + changePercent); + + // Generate OHLC + const open = price; + const high = price * (1 + Math.random() * 0.02); + const low = price * (1 - Math.random() * 0.02); + const close = price * (1 + (Math.random() - 0.5) * 0.01); + const volume = Math.random() * 1000000 + 500000; + + data.push({ + type: 'bar', + data: { + symbol, + open, + high, + low, + close, + volume, + vwap: (open + high + low + close) / 4, + timestamp: currentTime + } + }); + + currentTime += interval; + price = close; // Next bar opens at previous close + } + + return data; + } private populateEventQueue(marketData: MarketData[]): void { // Convert market data to events @@ -234,7 +328,7 @@ export class BacktestEngine extends EventEmitter { private async processMarketData(data: MarketData): Promise { const tradingEngine = this.strategyManager.getTradingEngine(); - if (!tradingEngine) return; + if (!tradingEngine) {return;} // Process through market simulator for realistic orderbook const orderbook = this.marketSimulator.processMarketData(data); @@ -300,7 +394,7 @@ export class BacktestEngine extends EventEmitter { // Track performance this.performanceAnalyzer.addEquityPoint( new Date(this.currentTime), - this.getPortfolioValue() + await this.getPortfolioValue() ); } @@ -321,18 +415,24 @@ export class BacktestEngine extends EventEmitter { } private async updateEquityCurve(): Promise { - 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 + const totalEquity = await this.getPortfolioValue(); this.equityCurve.push({ timestamp: this.currentTime, value: totalEquity }); } + + private async getPortfolioValue(): Promise { + const tradingEngine = this.strategyManager.getTradingEngine(); + if (!tradingEngine) { + return this.initialCapital; + } + + // Get current P&L + const [realized, unrealized] = tradingEngine.getTotalPnl(); + return this.initialCapital + realized + unrealized; + } private calculatePerformance(): PerformanceMetrics { // Use sophisticated performance analyzer @@ -365,49 +465,6 @@ export class BacktestEngine extends EventEmitter { 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 }[] = []; @@ -451,7 +508,7 @@ export class BacktestEngine extends EventEmitter { private async getFinalPositions(): Promise { const tradingEngine = this.strategyManager.getTradingEngine(); - if (!tradingEngine) return []; + if (!tradingEngine) {return [];} const positions = JSON.parse(tradingEngine.getOpenPositions()); return positions; @@ -465,7 +522,7 @@ export class BacktestEngine extends EventEmitter { ); // Could also store detailed results in a separate table or file - logger.debug(`Backtest results stored with ID: ${result.id}`); + this.container.logger.debug(`Backtest results stored with ID: ${result.id}`); } private reset(): void { @@ -521,7 +578,7 @@ export class BacktestEngine extends EventEmitter { private getPortfolioValue(): number { const tradingEngine = this.strategyManager.getTradingEngine(); - if (!tradingEngine) return 100000; // Default initial capital + if (!tradingEngine) {return 100000;} // Default initial capital const [realized, unrealized] = tradingEngine.getTotalPnl(); return 100000 + realized + unrealized; @@ -529,7 +586,7 @@ export class BacktestEngine extends EventEmitter { async stopBacktest(): Promise { this.isRunning = false; - logger.info('Backtest stop requested'); + this.container.logger.info('Backtest stop requested'); } async exportResults(format: 'json' | 'csv' | 'html' = 'json'): Promise { @@ -631,4 +688,25 @@ export class BacktestEngine extends EventEmitter { `; } + + private getOHLCData(marketData: MarketData[], symbols: string[]): Record { + const ohlcData: Record = {}; + + symbols.forEach(symbol => { + const symbolData = marketData + .filter(d => d.type === 'bar' && d.data.symbol === symbol) + .map(d => ({ + time: Math.floor(d.data.timestamp / 1000), // Convert to seconds for lightweight-charts + open: d.data.open, + high: d.data.high, + low: d.data.low, + close: d.data.close, + volume: d.data.volume + })); + + ohlcData[symbol] = symbolData; + }); + + return ohlcData; + } } \ No newline at end of file diff --git a/apps/stock/orchestrator/src/backtest/MarketSimulator.ts b/apps/stock/orchestrator/src/backtest/MarketSimulator.ts index 2ef5bb2..9fe9ecb 100644 --- a/apps/stock/orchestrator/src/backtest/MarketSimulator.ts +++ b/apps/stock/orchestrator/src/backtest/MarketSimulator.ts @@ -1,4 +1,6 @@ -import { logger } from '@stock-bot/logger'; +import { getLogger } from '@stock-bot/logger'; + +const logger = getLogger('MarketSimulator'); import { MarketData, Quote, Trade, Bar, OrderBookSnapshot, PriceLevel } from '../types'; import { MarketMicrostructure } from '../types/MarketMicrostructure'; diff --git a/apps/stock/orchestrator/src/container.ts b/apps/stock/orchestrator/src/container.ts index da2f2b3..4633de8 100644 --- a/apps/stock/orchestrator/src/container.ts +++ b/apps/stock/orchestrator/src/container.ts @@ -1,5 +1,4 @@ -import { Container } from '@stock-bot/di'; -import { logger } from '@stock-bot/logger'; +import { IServiceContainer } from '@stock-bot/di'; import { ModeManager } from './core/ModeManager'; import { MarketDataService } from './services/MarketDataService'; import { ExecutionService } from './services/ExecutionService'; @@ -9,39 +8,94 @@ 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 orchestrator-specific services in the DI container + */ +export async function registerOrchestratorServices(container: any): Promise { + // Create a service adapter that provides the expected interface + const services: IServiceContainer = { + logger: container.cradle.logger, + cache: container.cradle.cache, + globalCache: container.cradle.globalCache, + mongodb: container.cradle.mongoClient, + postgres: container.cradle.postgresClient, + questdb: container.cradle.questdbClient, + browser: container.cradle.browser, + proxy: container.cradle.proxyManager, + queueManager: container.cradle.queueManager, + queue: undefined, // Will be set if needed + custom: {} + }; + + // Create storage service first as it's needed by other services + const storageService = new StorageService( + services, + services.mongodb!, + services.postgres!, + services.questdb || null + ); + + // Create other services + const marketDataService = new MarketDataService(services); + const executionService = new ExecutionService(services, storageService); + const analyticsService = new AnalyticsService(services, storageService); + const strategyManager = new StrategyManager(services); + const backtestEngine = new BacktestEngine(services, storageService, strategyManager); + const paperTradingManager = new PaperTradingManager( + services, + storageService, + marketDataService, + executionService + ); + const modeManager = new ModeManager( + services, + marketDataService, + executionService, + storageService + ); + + // Store custom services + services.custom = { + StorageService: storageService, + MarketDataService: marketDataService, + ExecutionService: executionService, + AnalyticsService: analyticsService, + StrategyManager: strategyManager, + BacktestEngine: backtestEngine, + PaperTradingManager: paperTradingManager, + ModeManager: modeManager + }; + + // Register services in the Awilix container for resolution + container.register({ + StorageService: { value: storageService }, + MarketDataService: { value: marketDataService }, + ExecutionService: { value: executionService }, + AnalyticsService: { value: analyticsService }, + StrategyManager: { value: strategyManager }, + BacktestEngine: { value: backtestEngine }, + PaperTradingManager: { value: paperTradingManager }, + ModeManager: { value: modeManager }, + orchestratorServices: { value: services } + }); + + // Update the serviceContainer to include our custom services + const serviceContainer = container.cradle.serviceContainer; + if (serviceContainer && serviceContainer.custom) { + Object.assign(serviceContainer.custom, services.custom); + } + + // Setup event listeners after all services are registered + strategyManager.setupEventListeners(); + + // Initialize mode manager with default paper trading mode + await modeManager.initializeMode({ + mode: 'paper', + startingCapital: 100000 + }); +} -// 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') -)); \ No newline at end of file +// For backward compatibility, export a container getter +export function getContainer(): IServiceContainer { + throw new Error('Container should be accessed through ServiceApplication. Update your code to use dependency injection.'); +} \ No newline at end of file diff --git a/apps/stock/orchestrator/src/core/ModeManager.ts b/apps/stock/orchestrator/src/core/ModeManager.ts index b539e36..bd57a07 100644 --- a/apps/stock/orchestrator/src/core/ModeManager.ts +++ b/apps/stock/orchestrator/src/core/ModeManager.ts @@ -1,5 +1,5 @@ -import { logger } from '@stock-bot/logger'; -import { TradingEngine } from '../../core'; +import { TradingEngine } from '@stock-bot/core'; +import { IServiceContainer } from '@stock-bot/di'; import { TradingMode, ModeConfig, BacktestConfigSchema, PaperConfigSchema, LiveConfigSchema } from '../types'; import { MarketDataService } from '../services/MarketDataService'; import { ExecutionService } from '../services/ExecutionService'; @@ -11,13 +11,16 @@ export class ModeManager extends EventEmitter { private config: ModeConfig | null = null; private tradingEngine: TradingEngine | null = null; private isInitialized = false; + private container: IServiceContainer; constructor( + container: IServiceContainer, private marketDataService: MarketDataService, private executionService: ExecutionService, private storageService: StorageService ) { super(); + this.container = container; } async initializeMode(config: ModeConfig): Promise { @@ -52,7 +55,7 @@ export class ModeManager extends EventEmitter { this.isInitialized = true; this.emit('modeChanged', config); - logger.info(`Trading mode initialized: ${config.mode}`); + this.container.logger.info(`Trading mode initialized: ${config.mode}`); } private createEngineConfig(config: ModeConfig): any { @@ -127,7 +130,7 @@ export class ModeManager extends EventEmitter { async transitionMode(fromMode: TradingMode, toMode: TradingMode, config: ModeConfig): Promise { if (fromMode === 'paper' && toMode === 'live') { // Special handling for paper to live transition - logger.info('Transitioning from paper to live trading...'); + this.container.logger.info('Transitioning from paper to live trading...'); // 1. Get current paper positions const paperPositions = await this.tradingEngine!.getOpenPositions(); @@ -136,7 +139,7 @@ export class ModeManager extends EventEmitter { await this.initializeMode(config); // 3. Reconcile positions (this would be handled by a reconciliation service) - logger.info(`Paper positions to reconcile: ${paperPositions}`); + this.container.logger.info(`Paper positions to reconcile: ${paperPositions}`); } else { // Standard mode switch await this.initializeMode(config); @@ -146,7 +149,7 @@ export class ModeManager extends EventEmitter { async shutdown(): Promise { if (!this.isInitialized) return; - logger.info(`Shutting down ${this.mode} mode...`); + this.container.logger.info(`Shutting down ${this.mode} mode...`); // Shutdown services await this.marketDataService.shutdown(); diff --git a/apps/stock/orchestrator/src/data/DataManager.ts b/apps/stock/orchestrator/src/data/DataManager.ts index fbf7f55..2387432 100644 --- a/apps/stock/orchestrator/src/data/DataManager.ts +++ b/apps/stock/orchestrator/src/data/DataManager.ts @@ -1,4 +1,6 @@ -import { logger } from '@stock-bot/logger'; +import { getLogger } from '@stock-bot/logger'; + +const logger = getLogger('DataManager'); import { StorageService } from '../services/StorageService'; import { MarketData, Bar } from '../types'; import { EventEmitter } from 'events'; @@ -62,13 +64,19 @@ export class DataManager extends EventEmitter { for (const symbol of symbols) { try { // Load raw data - const data = await this.storageService.getHistoricalBars( + let data = await this.storageService.getHistoricalBars( symbol, startDate, endDate, resolution ); + // If no data found, generate mock data + if (!data || data.length === 0) { + logger.warn(`No historical data found for ${symbol}, generating mock data`); + data = this.generateMockBars(symbol, startDate, endDate, resolution); + } + // Apply corporate actions const adjustedData = await this.applyCorporateActions(symbol, data, startDate, endDate); @@ -432,4 +440,62 @@ export class DataManager extends EventEmitter { this.aggregatedCache.clear(); this.dataQualityIssues = []; } + + private generateMockBars( + symbol: string, + startDate: Date, + endDate: Date, + resolution: string + ): any[] { + const bars: any[] = []; + const resolutionMs = DataManager.RESOLUTIONS[resolution]?.milliseconds || 86400000; // Default to 1 day + + let currentTime = startDate.getTime(); + const endTime = endDate.getTime(); + + // Base price varies by symbol + let basePrice = 100; + if (symbol === 'AAPL') basePrice = 150; + else if (symbol === 'GOOGL') basePrice = 140; + else if (symbol === 'MSFT') basePrice = 380; + else if (symbol === 'TSLA') basePrice = 250; + + let price = basePrice; + + while (currentTime <= endTime) { + // Generate realistic intraday movement + const volatility = 0.02; // 2% daily volatility + const trend = 0.0001; // Slight upward trend + + // Random walk with trend + const changePercent = (Math.random() - 0.5) * volatility + trend; + price = price * (1 + changePercent); + + // Generate OHLC + const open = price; + const intraDayVolatility = volatility / 4; + const high = price * (1 + Math.random() * intraDayVolatility); + const low = price * (1 - Math.random() * intraDayVolatility); + const close = low + Math.random() * (high - low); + + // Volume with some randomness + const baseVolume = 10000000; // 10M shares + const volume = baseVolume * (0.5 + Math.random()); + + bars.push({ + timestamp: new Date(currentTime), + open: Number(open.toFixed(2)), + high: Number(high.toFixed(2)), + low: Number(low.toFixed(2)), + close: Number(close.toFixed(2)), + volume: Math.floor(volume), + vwap: Number(((high + low + close) / 3).toFixed(2)) + }); + + currentTime += resolutionMs; + price = close; // Next bar opens at previous close + } + + return bars; + } } \ No newline at end of file diff --git a/apps/stock/orchestrator/src/handlers/backtest/backtest.handler.ts b/apps/stock/orchestrator/src/handlers/backtest/backtest.handler.ts new file mode 100644 index 0000000..3f6de01 --- /dev/null +++ b/apps/stock/orchestrator/src/handlers/backtest/backtest.handler.ts @@ -0,0 +1,121 @@ +import { + BaseHandler, + Handler, + Operation, +} from '@stock-bot/handlers'; +import { getLogger } from '@stock-bot/logger'; +import type { OrchestratorServices } from '../../types'; + +const logger = getLogger('backtest-handler'); + +interface BacktestPayload { + backtestId: string; + strategy: string; + symbols: string[]; + startDate: string; + endDate: string; + initialCapital: number; + config?: Record; +} + +interface CancelBacktestPayload { + backtestId: string; +} + +@Handler('orchestrator') +export class BacktestHandler extends BaseHandler { + + @Operation('run-backtest') + async runBacktest(payload: BacktestPayload) { + const { backtestId, strategy, symbols, startDate, endDate, initialCapital, config } = payload; + + logger.info('Starting backtest', { backtestId, strategy, symbolCount: symbols.length }); + + try { + // Update status in web-api (via Redis or direct DB update) + await this.updateBacktestStatus(backtestId, 'running'); + + // TODO: Call Rust core via NAPI bindings + // For now, we'll simulate the backtest + const results = await this.simulateBacktest({ + strategy, + symbols, + startDate, + endDate, + initialCapital, + config, + }); + + // Store results + await this.storeBacktestResults(backtestId, results); + + // Update status to completed + await this.updateBacktestStatus(backtestId, 'completed'); + + logger.info('Backtest completed', { backtestId }); + + return { success: true, backtestId, results }; + } catch (error) { + logger.error('Backtest failed', { backtestId, error }); + + await this.updateBacktestStatus(backtestId, 'failed', error.message); + + throw error; + } + } + + @Operation('cancel-backtest') + async cancelBacktest(payload: CancelBacktestPayload) { + const { backtestId } = payload; + + logger.info('Cancelling backtest', { backtestId }); + + // TODO: Implement actual cancellation logic + // For now, just update the status + await this.updateBacktestStatus(backtestId, 'cancelled'); + + return { success: true, backtestId }; + } + + private async updateBacktestStatus(backtestId: string, status: string, error?: string) { + // TODO: Update in MongoDB or notify web-api + logger.info('Updating backtest status', { backtestId, status, error }); + } + + private async storeBacktestResults(backtestId: string, results: any) { + // TODO: Store in MongoDB + logger.info('Storing backtest results', { backtestId }); + } + + private async simulateBacktest(params: Omit) { + // Simulate some processing time + await new Promise(resolve => setTimeout(resolve, 2000)); + + // Return mock results + return { + metrics: { + totalReturn: 0.15, + sharpeRatio: 1.2, + maxDrawdown: -0.08, + winRate: 0.55, + totalTrades: 150, + profitFactor: 1.8, + }, + equity: [ + { date: params.startDate, value: params.initialCapital }, + { date: params.endDate, value: params.initialCapital * 1.15 }, + ], + trades: [ + { + symbol: params.symbols[0], + entryDate: params.startDate, + exitDate: params.endDate, + entryPrice: 100, + exitPrice: 115, + quantity: 100, + pnl: 1500, + }, + ], + }; + } +} \ No newline at end of file diff --git a/apps/stock/orchestrator/src/index.ts b/apps/stock/orchestrator/src/index.ts index 683111d..49599b3 100644 --- a/apps/stock/orchestrator/src/index.ts +++ b/apps/stock/orchestrator/src/index.ts @@ -1,83 +1,150 @@ +/** + * Stock Bot Orchestrator Service + * Coordinates between Rust core, data feeds, and analytics + */ + +import { getLogger } from '@stock-bot/logger'; +import { initializeStockConfig } from '@stock-bot/stock-config'; 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'; +import { createRoutes } from './routes/create-routes'; +import { createContainer } from './simple-container'; -const PORT = process.env.PORT || 3002; +// Initialize configuration with service-specific overrides +const config = initializeStockConfig('orchestrator'); +const logger = getLogger('orchestrator'); + +// Get service-specific config +const serviceConfig = config.services?.orchestrator || { + port: 2004, + defaultMode: 'paper', + paperTradingCapital: 100000, + enableWebSocket: true +}; + +const PORT = serviceConfig.port; + +// Log the configuration +logger.info('Service configuration:', { + port: PORT, + defaultMode: serviceConfig.defaultMode, + enableWebSocket: serviceConfig.enableWebSocket, + backtesting: serviceConfig.backtesting, + strategies: serviceConfig.strategies +}); async function main() { - // Initialize Hono app - const app = new Hono(); + let server: any; // Declare server in outer scope for shutdown - // 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() + try { + // Initialize container with all services using configuration + const services = await createContainer(config); + + // Initialize Hono app + const app = new Hono(); + + // CORS middleware - use config for origins + app.use('*', cors({ + origin: config.services?.webApi?.cors?.origins || ['http://localhost:4200', 'http://localhost:3000', 'http://localhost:5173', 'http://localhost:5174'], + allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], + allowHeaders: ['Content-Type', 'Authorization'], + credentials: config.services?.webApi?.cors?.credentials ?? true, + })); + + // Logging middleware + app.use('*', async (c, next) => { + const start = Date.now(); + await next(); + const ms = Date.now() - start; + services.logger.debug(`${c.req.method} ${c.req.url} - ${ms}ms`); }); - }); - // Mount routes - app.route('/api/orders', createOrderRoutes()); - app.route('/api/positions', createPositionRoutes()); - app.route('/api/analytics', createAnalyticsRoutes()); - app.route('/api/backtest', createBacktestRoutes()); + // Create and mount routes (without Socket.IO for now) + const routes = createRoutes(services); + app.route('/', routes); - // Create HTTP server and Socket.IO - const server = createServer(app.fetch); - const io = new SocketIOServer(server, { - cors: { - origin: '*', - methods: ['GET', 'POST'] + // Start Bun server + try { + server = Bun.serve({ + port: PORT, + hostname: '0.0.0.0', // Explicitly bind to all interfaces + fetch: async (req) => { + services.logger.debug(`Incoming request: ${req.method} ${req.url}`); + try { + const response = await app.fetch(req); + return response; + } catch (error) { + services.logger.error('Request handling error:', error); + return new Response('Internal Server Error', { status: 500 }); + } + }, + error: (error) => { + services.logger.error('Server error:', error); + return new Response('Internal Server Error', { status: 500 }); + }, + }); + + services.logger.info(`Orchestrator service started on port ${server.port}`); + services.logger.info(`Server hostname: ${server.hostname}`); + services.logger.info(`Server URL: http://${server.hostname}:${server.port}`); + + // Test that server is actually listening + setTimeout(async () => { + try { + const testResponse = await fetch(`http://localhost:${server.port}/health`); + services.logger.info(`Server self-test: ${testResponse.status} ${testResponse.statusText}`); + } catch (error) { + services.logger.error('Server self-test failed:', error); + } + }, 1000); + } catch (error) { + services.logger.error('Failed to start Bun server:', error); + throw error; + } + services.logger.info('Service metadata:', { + version: '1.0.0', + description: 'Trading System Orchestrator', + defaultMode: serviceConfig.defaultMode, + enableWebSocket: serviceConfig.enableWebSocket, + endpoints: { + health: '/health', + orders: '/api/orders', + positions: '/api/positions', + analytics: '/api/analytics', + backtest: '/api/backtest', + } + }); + + // Note: Socket.IO with Bun requires a different setup + // For now, we'll disable Socket.IO to avoid the CORS error + if (serviceConfig.enableWebSocket) { + services.logger.info('WebSocket support is enabled but Socket.IO integration with Bun requires additional setup'); } - }); - // Setup WebSocket handlers - setupWebSocketHandlers(io, container); + // Graceful shutdown + process.on('SIGINT', async () => { + services.logger.info('Orchestrator service shutting down...'); + + // Cleanup any active trading sessions + const modeManager = services.custom?.ModeManager; + if (modeManager) { + await modeManager.shutdown(); + } + + if (server) { + server.stop(); + } + process.exit(0); + }); - // 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); - }); + } catch (error) { + logger.error('Failed to start orchestrator service:', error); + process.exit(1); + } } -main().catch((error) => { - logger.error('Failed to start trading orchestrator:', error); +// Start the service +main().catch(error => { + logger.error('Unhandled error:', error); process.exit(1); }); \ No newline at end of file diff --git a/apps/stock/orchestrator/src/paper/PaperTradingManager.ts b/apps/stock/orchestrator/src/paper/PaperTradingManager.ts index 508c648..07b0c87 100644 --- a/apps/stock/orchestrator/src/paper/PaperTradingManager.ts +++ b/apps/stock/orchestrator/src/paper/PaperTradingManager.ts @@ -1,6 +1,8 @@ -import { logger } from '@stock-bot/logger'; import { EventEmitter } from 'events'; +import { IServiceContainer } from '@stock-bot/di'; import { OrderRequest, Position } from '../types'; +import { StorageService } from '../services/StorageService'; +import { MarketDataService } from '../services/MarketDataService'; import { ExecutionService } from '../services/ExecutionService'; interface VirtualAccount { @@ -49,12 +51,17 @@ export class PaperTradingManager extends EventEmitter { private marketPrices = new Map(); private readonly COMMISSION_RATE = 0.001; // 0.1% private readonly MARGIN_REQUIREMENT = 0.25; // 25% margin requirement + private container: IServiceContainer; constructor( + container: IServiceContainer, + private storageService: StorageService, + private marketDataService: MarketDataService, private executionService: ExecutionService, initialBalance: number = 100000 ) { super(); + this.container = container; this.account = { balance: initialBalance, @@ -362,6 +369,6 @@ export class PaperTradingManager extends EventEmitter { marginUsed: 0 }; - logger.info('Paper trading account reset'); + this.container.logger.info('Paper trading account reset'); } } \ No newline at end of file diff --git a/apps/stock/orchestrator/src/routes/create-routes.ts b/apps/stock/orchestrator/src/routes/create-routes.ts new file mode 100644 index 0000000..24daed8 --- /dev/null +++ b/apps/stock/orchestrator/src/routes/create-routes.ts @@ -0,0 +1,52 @@ +import { Hono } from 'hono'; +import { IServiceContainer } from '@stock-bot/di'; +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 { Server as SocketIOServer } from 'socket.io'; + +/** + * Create all routes for the orchestrator service + */ +export function createRoutes( + services: IServiceContainer, + io?: SocketIOServer +): Hono { + const app = new Hono(); + services.logger.info('Creating orchestrator routes'); + + // Health check with mode status + app.get('/health', (c) => { + const modeManager = services.custom?.ModeManager; + return c.json({ + status: 'healthy', + service: 'orchestrator', + mode: modeManager?.getCurrentMode() || 'unknown', + timestamp: new Date().toISOString() + }); + }); + + // Mount REST API routes + app.route('/api/orders', createOrderRoutes(services)); + app.route('/api/positions', createPositionRoutes(services)); + app.route('/api/analytics', createAnalyticsRoutes(services)); + app.route('/api/backtest', createBacktestRoutes(services)); + + // Setup WebSocket handlers if Socket.IO is provided + if (io) { + setupWebSocketHandlers(io, services); + services.logger.info('WebSocket handlers configured'); + } + + // Add request logging middleware + app.use('*', async (c, next) => { + const start = Date.now(); + await next(); + const ms = Date.now() - start; + services.logger.debug(`${c.req.method} ${c.req.url} - ${ms}ms`); + }); + + return app; +} \ No newline at end of file diff --git a/apps/stock/orchestrator/src/services/AnalyticsService.ts b/apps/stock/orchestrator/src/services/AnalyticsService.ts index f3698e5..cd67162 100644 --- a/apps/stock/orchestrator/src/services/AnalyticsService.ts +++ b/apps/stock/orchestrator/src/services/AnalyticsService.ts @@ -1,6 +1,7 @@ -import { logger } from '@stock-bot/logger'; import axios from 'axios'; +import { IServiceContainer } from '@stock-bot/di'; import { PerformanceMetrics, RiskMetrics } from '../types'; +import { StorageService } from './StorageService'; interface OptimizationParams { returns: number[][]; @@ -24,8 +25,12 @@ export class AnalyticsService { private analyticsUrl: string; private cache = new Map(); private readonly CACHE_TTL_MS = 60000; // 1 minute cache + private container: IServiceContainer; + private storageService: StorageService; - constructor() { + constructor(container: IServiceContainer, storageService: StorageService) { + this.container = container; + this.storageService = storageService; this.analyticsUrl = process.env.ANALYTICS_SERVICE_URL || 'http://localhost:3003'; } @@ -50,7 +55,7 @@ export class AnalyticsService { this.setCache(cacheKey, metrics); return metrics; } catch (error) { - logger.error('Error fetching performance metrics:', error); + this.container.logger.error('Error fetching performance metrics:', error); // Return default metrics if analytics service is unavailable return this.getDefaultPerformanceMetrics(); } @@ -61,7 +66,7 @@ export class AnalyticsService { const response = await axios.post(`${this.analyticsUrl}/optimize/portfolio`, params); return response.data as PortfolioWeights; } catch (error) { - logger.error('Error optimizing portfolio:', error); + this.container.logger.error('Error optimizing portfolio:', error); // Return equal weights as fallback return this.getEqualWeights(params.returns[0].length); } @@ -78,7 +83,7 @@ export class AnalyticsService { this.setCache(cacheKey, metrics); return metrics; } catch (error) { - logger.error('Error fetching risk metrics:', error); + this.container.logger.error('Error fetching risk metrics:', error); return this.getDefaultRiskMetrics(); } } @@ -94,7 +99,7 @@ export class AnalyticsService { this.setCache(cacheKey, regime, 300000); // Cache for 5 minutes return regime; } catch (error) { - logger.error('Error detecting market regime:', error); + this.container.logger.error('Error detecting market regime:', error); return 'normal'; // Default regime } } @@ -104,7 +109,7 @@ export class AnalyticsService { 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); + this.container.logger.error('Error calculating correlation matrix:', error); // Return identity matrix as fallback return this.getIdentityMatrix(symbols.length); } @@ -115,7 +120,7 @@ export class AnalyticsService { const response = await axios.get(`${this.analyticsUrl}/analytics/backtest/${backtestId}`); return response.data; } catch (error) { - logger.error('Error running backtest analysis:', error); + this.container.logger.error('Error running backtest analysis:', error); return null; } } @@ -128,7 +133,7 @@ export class AnalyticsService { }); return response.data; } catch (error) { - logger.error('Error getting model prediction:', error); + this.container.logger.error('Error getting model prediction:', error); return null; } } diff --git a/apps/stock/orchestrator/src/services/ExecutionService.ts b/apps/stock/orchestrator/src/services/ExecutionService.ts index aabebf2..1e09268 100644 --- a/apps/stock/orchestrator/src/services/ExecutionService.ts +++ b/apps/stock/orchestrator/src/services/ExecutionService.ts @@ -1,9 +1,10 @@ -import { logger } from '@stock-bot/logger'; import { EventEmitter } from 'events'; import { v4 as uuidv4 } from 'uuid'; +import { IServiceContainer } from '@stock-bot/di'; import { ModeConfig, OrderRequest, OrderRequestSchema } from '../types'; -import { TradingEngine } from '../../core'; +import { TradingEngine } from '@stock-bot/core'; import axios from 'axios'; +import { StorageService } from './StorageService'; interface ExecutionReport { orderId: string; @@ -29,9 +30,13 @@ export class ExecutionService extends EventEmitter { private tradingEngine: TradingEngine | null = null; private brokerClient: any = null; // Would be specific broker API client private pendingOrders = new Map(); + private container: IServiceContainer; + private storageService: StorageService; - constructor(private modeManager: any) { + constructor(container: IServiceContainer, storageService: StorageService) { super(); + this.container = container; + this.storageService = storageService; } async initialize(config: ModeConfig, tradingEngine: TradingEngine): Promise { @@ -47,7 +52,7 @@ export class ExecutionService extends EventEmitter { private async initializeBroker(broker: string, accountId: string): Promise { // In real implementation, would initialize specific broker API // For example: Alpaca, Interactive Brokers, etc. - logger.info(`Initializing ${broker} broker connection for account ${accountId}`); + this.container.logger.info(`Initializing ${broker} broker connection for account ${accountId}`); } async submitOrder(orderRequest: OrderRequest): Promise { @@ -97,7 +102,7 @@ export class ExecutionService extends EventEmitter { return result; } catch (error) { - logger.error('Error submitting order:', error); + this.container.logger.error('Error submitting order:', error); return this.createRejectionReport( orderId, clientOrderId, @@ -171,7 +176,7 @@ export class ExecutionService extends EventEmitter { ): Promise { // In real implementation, would submit to actual broker // This is a placeholder - logger.info(`Submitting order ${orderId} to broker`); + this.container.logger.info(`Submitting order ${orderId} to broker`); // Simulate broker response return { @@ -189,7 +194,7 @@ export class ExecutionService extends EventEmitter { async cancelOrder(orderId: string): Promise { const order = this.pendingOrders.get(orderId); if (!order) { - logger.warn(`Order ${orderId} not found`); + this.container.logger.warn(`Order ${orderId} not found`); return false; } @@ -222,7 +227,7 @@ export class ExecutionService extends EventEmitter { return true; } catch (error) { - logger.error(`Error cancelling order ${orderId}:`, error); + this.container.logger.error(`Error cancelling order ${orderId}:`, error); return false; } } @@ -286,7 +291,7 @@ export class ExecutionService extends EventEmitter { async routeOrderToExchange(order: OrderRequest, exchange: string): Promise { // This would route orders to specific exchanges in live mode // For now, just a placeholder - logger.info(`Routing order to ${exchange}:`, order); + this.container.logger.info(`Routing order to ${exchange}:`, order); } async getOrderStatus(orderId: string): Promise { diff --git a/apps/stock/orchestrator/src/services/MarketDataService.ts b/apps/stock/orchestrator/src/services/MarketDataService.ts index 7aa5727..a9767e6 100644 --- a/apps/stock/orchestrator/src/services/MarketDataService.ts +++ b/apps/stock/orchestrator/src/services/MarketDataService.ts @@ -1,6 +1,6 @@ -import { logger } from '@stock-bot/logger'; import { io, Socket } from 'socket.io-client'; import { EventEmitter } from 'events'; +import { IServiceContainer } from '@stock-bot/di'; import { ModeConfig, MarketData, QuoteSchema, TradeSchema, BarSchema } from '../types'; import { QuestDBClient } from '@stock-bot/questdb'; @@ -13,6 +13,12 @@ export class MarketDataService extends EventEmitter { private batchTimer: NodeJS.Timeout | null = null; private readonly BATCH_SIZE = 100; private readonly BATCH_INTERVAL_MS = 50; + private container: IServiceContainer; + + constructor(container: IServiceContainer) { + super(); + this.container = container; + } async initialize(config: ModeConfig): Promise { this.mode = config.mode; @@ -41,7 +47,7 @@ export class MarketDataService extends EventEmitter { }); this.dataIngestionSocket.on('connect', () => { - logger.info('Connected to data-ingestion service'); + this.container.logger.info('Connected to data-ingestion service'); // Re-subscribe to symbols this.subscriptions.forEach(symbol => { this.dataIngestionSocket!.emit('subscribe', { symbol }); @@ -49,7 +55,7 @@ export class MarketDataService extends EventEmitter { }); this.dataIngestionSocket.on('disconnect', () => { - logger.warn('Disconnected from data-ingestion service'); + this.container.logger.warn('Disconnected from data-ingestion service'); }); this.dataIngestionSocket.on('marketData', (data: any) => { @@ -57,7 +63,7 @@ export class MarketDataService extends EventEmitter { }); this.dataIngestionSocket.on('error', (error: any) => { - logger.error('Data ingestion socket error:', error); + this.container.logger.error('Data ingestion socket error:', error); }); } @@ -68,7 +74,7 @@ export class MarketDataService extends EventEmitter { this.dataIngestionSocket.emit('subscribe', { symbol }); } - logger.debug(`Subscribed to ${symbol}`); + this.container.logger.debug(`Subscribed to ${symbol}`); } async unsubscribeFromSymbol(symbol: string): Promise { @@ -78,7 +84,7 @@ export class MarketDataService extends EventEmitter { this.dataIngestionSocket.emit('unsubscribe', { symbol }); } - logger.debug(`Unsubscribed from ${symbol}`); + this.container.logger.debug(`Unsubscribed from ${symbol}`); } private handleMarketData(data: any): void { @@ -118,7 +124,7 @@ export class MarketDataService extends EventEmitter { }); marketData = { type: 'bar', data: bar }; } else { - logger.warn('Unknown market data format:', data); + this.container.logger.warn('Unknown market data format:', data); return; } @@ -134,7 +140,7 @@ export class MarketDataService extends EventEmitter { } } catch (error) { - logger.error('Error handling market data:', error); + this.container.logger.error('Error handling market data:', error); } } @@ -270,7 +276,7 @@ export class MarketDataService extends EventEmitter { // Close QuestDB connection if (this.questdbClient) { - await this.questdbClient.close(); + await this.questdbClient.disconnect(); this.questdbClient = null; } diff --git a/apps/stock/orchestrator/src/services/StorageService.ts b/apps/stock/orchestrator/src/services/StorageService.ts index 06f7fe1..0fd5269 100644 --- a/apps/stock/orchestrator/src/services/StorageService.ts +++ b/apps/stock/orchestrator/src/services/StorageService.ts @@ -1,32 +1,31 @@ -import { logger } from '@stock-bot/logger'; +import { IServiceContainer } from '@stock-bot/di'; import { QuestDBClient } from '@stock-bot/questdb'; import { PostgresClient } from '@stock-bot/postgres'; +import { MongoDBClient } from '@stock-bot/mongodb'; import { ModeConfig, MarketData, Position } from '../types'; export class StorageService { private questdb: QuestDBClient | null = null; private postgres: PostgresClient | null = null; + private mongodb: MongoDBClient | null = null; private mode: 'backtest' | 'paper' | 'live' = 'paper'; + private container: IServiceContainer; + + constructor( + container: IServiceContainer, + mongoClient: MongoDBClient, + postgresClient: PostgresClient, + questdbClient: QuestDBClient | null + ) { + this.container = container; + this.mongodb = mongoClient; + this.postgres = postgresClient; + this.questdb = questdbClient; + } async initialize(config: ModeConfig): Promise { 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' - }); - + // Clients are already injected via DI await this.createTables(); } @@ -281,12 +280,12 @@ export class StorageService { async shutdown(): Promise { if (this.questdb) { - await this.questdb.close(); + await this.questdb.disconnect(); this.questdb = null; } if (this.postgres) { - await this.postgres.close(); + await this.postgres.disconnect(); this.postgres = null; } } diff --git a/apps/stock/orchestrator/src/simple-container.ts b/apps/stock/orchestrator/src/simple-container.ts new file mode 100644 index 0000000..8adb3ed --- /dev/null +++ b/apps/stock/orchestrator/src/simple-container.ts @@ -0,0 +1,137 @@ +import { getLogger } from '@stock-bot/logger'; +import { MongoDBClient } from '@stock-bot/mongodb'; +import { PostgreSQLClient } from '@stock-bot/postgres'; +import { createCache } from '@stock-bot/cache'; +import { QueueManager, Queue } from '@stock-bot/queue'; +import { IServiceContainer } from '@stock-bot/handlers'; +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'; + +/** + * Creates a simplified service container without the DI framework + * All configuration comes from the config object + */ +export async function createContainer(config: any): Promise { + const logger = getLogger('orchestrator'); + + // Initialize database clients if enabled + let mongoClient: MongoDBClient | undefined; + if (config.database.mongodb.enabled) { + mongoClient = new MongoDBClient(config.database.mongodb, logger); + await mongoClient.connect(); + } + + let postgresClient: PostgreSQLClient | undefined; + if (config.database.postgres.enabled) { + postgresClient = new PostgreSQLClient({ + host: config.database.postgres.host, + port: config.database.postgres.port, + database: config.database.postgres.database, + username: config.database.postgres.user, + password: config.database.postgres.password, + poolSettings: { + min: 1, + max: config.database.postgres.poolSize || 20, + idleTimeoutMillis: config.database.postgres.idleTimeout || 10000 + } + }); + await postgresClient.connect(); + } + + // Initialize cache if enabled + let cache: any; + if (config.database.dragonfly.enabled) { + cache = createCache({ + redisConfig: { + host: config.database.dragonfly.host, + port: config.database.dragonfly.port, + db: config.database.dragonfly.db, + keyPrefix: config.database.dragonfly.keyPrefix + }, + ttl: 3600, + enableMetrics: true + }); + } + + // Initialize queue if enabled + let queue: Queue | undefined; + let queueManager: QueueManager | undefined; + if (config.queue.enabled) { + queue = new Queue('orchestrator', { + redis: { + host: config.queue.redis.host, + port: config.queue.redis.port, + db: config.queue.redis.db + } + }); + queueManager = new QueueManager(config, logger); + } + + // Create base services container + const services: IServiceContainer = { + logger, + cache, + mongodb: mongoClient, + postgres: postgresClient, + queue, + queueManager, + config, + custom: {} + }; + + // Create orchestrator services + const storageService = new StorageService( + services, + mongoClient!, + postgresClient!, + null // QuestDB not needed for now + ); + + const marketDataService = new MarketDataService(services); + const executionService = new ExecutionService(services, storageService); + const analyticsService = new AnalyticsService(services, storageService); + const strategyManager = new StrategyManager(services); + const backtestEngine = new BacktestEngine(services, storageService, strategyManager); + const paperTradingManager = new PaperTradingManager( + services, + storageService, + marketDataService, + executionService + ); + const modeManager = new ModeManager( + services, + marketDataService, + executionService, + storageService + ); + + // Store custom services + services.custom = { + StorageService: storageService, + MarketDataService: marketDataService, + ExecutionService: executionService, + AnalyticsService: analyticsService, + StrategyManager: strategyManager, + BacktestEngine: backtestEngine, + PaperTradingManager: paperTradingManager, + ModeManager: modeManager + }; + + // Setup event listeners after all services are registered + strategyManager.setupEventListeners(); + + // Initialize mode manager with configured default mode + const serviceConfig = config.services?.orchestrator || {}; + await modeManager.initializeMode({ + mode: serviceConfig.defaultMode || 'paper', + startingCapital: serviceConfig.paperTradingCapital || 100000 + }); + + return services; +} \ No newline at end of file diff --git a/apps/stock/orchestrator/src/strategies/BaseStrategy.ts b/apps/stock/orchestrator/src/strategies/BaseStrategy.ts index 2572d8a..8ff47b1 100644 --- a/apps/stock/orchestrator/src/strategies/BaseStrategy.ts +++ b/apps/stock/orchestrator/src/strategies/BaseStrategy.ts @@ -1,5 +1,7 @@ import { EventEmitter } from 'events'; -import { logger } from '@stock-bot/logger'; +import { getLogger } from '@stock-bot/logger'; + +const logger = getLogger('BaseStrategy'); import { MarketData, StrategyConfig, OrderRequest } from '../types'; import { ModeManager } from '../core/ModeManager'; import { ExecutionService } from '../services/ExecutionService'; diff --git a/apps/stock/orchestrator/src/strategies/StrategyManager.ts b/apps/stock/orchestrator/src/strategies/StrategyManager.ts index 13330df..a3bf425 100644 --- a/apps/stock/orchestrator/src/strategies/StrategyManager.ts +++ b/apps/stock/orchestrator/src/strategies/StrategyManager.ts @@ -1,39 +1,41 @@ -import { logger } from '@stock-bot/logger'; import { EventEmitter } from 'events'; +import { IServiceContainer } from '@stock-bot/di'; 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'; +import { TradingEngine } from '@stock-bot/core'; export class StrategyManager extends EventEmitter { private strategies = new Map(); private activeStrategies = new Set(); private tradingEngine: TradingEngine | null = null; + private container: IServiceContainer; - constructor( - private modeManager: ModeManager, - private marketDataService: MarketDataService, - private executionService: ExecutionService - ) { + constructor(container: IServiceContainer) { super(); - this.setupEventListeners(); + this.container = container; } - private setupEventListeners(): void { + setupEventListeners(): void { + const marketDataService = this.container.custom?.MarketDataService; + const executionService = this.container.custom?.ExecutionService; + + if (!marketDataService || !executionService) { + this.container.logger.error('Required services not found in container'); + return; + } + // Listen for market data - this.marketDataService.on('marketData', (data: MarketData) => { + marketDataService.on('marketData', (data: MarketData) => { this.handleMarketData(data); }); // Listen for market data batches (more efficient) - this.marketDataService.on('marketDataBatch', (batch: MarketData[]) => { + marketDataService.on('marketDataBatch', (batch: MarketData[]) => { this.handleMarketDataBatch(batch); }); // Listen for fills - this.executionService.on('fill', (fill: any) => { + executionService.on('fill', (fill: any) => { this.handleFill(fill); }); } @@ -47,7 +49,10 @@ export class StrategyManager extends EventEmitter { this.activeStrategies.clear(); // Get trading engine from mode manager - this.tradingEngine = this.modeManager.getTradingEngine(); + const modeManager = this.container.custom?.ModeManager; + if (modeManager) { + this.tradingEngine = modeManager.getTradingEngine(); + } // Initialize new strategies for (const config of configs) { @@ -59,9 +64,9 @@ export class StrategyManager extends EventEmitter { await this.enableStrategy(config.id); } - logger.info(`Initialized strategy: ${config.name} (${config.id})`); + this.container.logger.info(`Initialized strategy: ${config.name} (${config.id})`); } catch (error) { - logger.error(`Failed to initialize strategy ${config.name}:`, error); + this.container.logger.error(`Failed to initialize strategy ${config.name}:`, error); } } } @@ -71,8 +76,8 @@ export class StrategyManager extends EventEmitter { // For now, create a base strategy instance const strategy = new BaseStrategy( config, - this.modeManager, - this.executionService + this.container.custom?.ModeManager, + this.container.custom?.ExecutionService ); // Set up strategy event handlers @@ -80,12 +85,10 @@ export class StrategyManager extends EventEmitter { this.handleStrategySignal(config.id, signal); }); - strategy.on('order', (order: OrderRequest) => { - this.handleStrategyOrder(config.id, order); + strategy.on('error', (error: Error) => { + this.container.logger.error(`Strategy ${config.id} error:`, error); }); - await strategy.initialize(); - return strategy; } @@ -94,10 +97,10 @@ export class StrategyManager extends EventEmitter { if (!strategy) { throw new Error(`Strategy ${strategyId} not found`); } - - await strategy.start(); + + await strategy.initialize(); this.activeStrategies.add(strategyId); - logger.info(`Enabled strategy: ${strategyId}`); + this.container.logger.info(`Enabled strategy: ${strategyId}`); } async disableStrategy(strategyId: string): Promise { @@ -105,119 +108,80 @@ export class StrategyManager extends EventEmitter { if (!strategy) { throw new Error(`Strategy ${strategyId} not found`); } - - await strategy.stop(); + + await strategy.shutdown(); this.activeStrategies.delete(strategyId); - logger.info(`Disabled strategy: ${strategyId}`); + this.container.logger.info(`Disabled strategy: ${strategyId}`); } private async handleMarketData(data: MarketData): Promise { - // Forward to active strategies + // Forward to all active strategies for (const strategyId of this.activeStrategies) { const strategy = this.strategies.get(strategyId); - if (strategy && strategy.isInterestedInSymbol(data.data.symbol)) { + if (strategy) { try { await strategy.onMarketData(data); } catch (error) { - logger.error(`Strategy ${strategyId} error processing market data:`, error); + this.container.logger.error(`Error processing market data for strategy ${strategyId}:`, error); } } } } private async handleMarketDataBatch(batch: MarketData[]): Promise { - // Group by symbol for efficiency - const bySymbol = new Map(); - - 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 + // Process batch more efficiently 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) { + if (strategy) { try { - await strategy.onMarketDataBatch(relevantData); + await strategy.onMarketDataBatch(batch); } catch (error) { - logger.error(`Strategy ${strategyId} error processing batch:`, error); + this.container.logger.error(`Error processing market data batch for strategy ${strategyId}:`, error); } } } } private async handleFill(fill: any): Promise { - // Notify relevant strategies about fills - for (const strategyId of this.activeStrategies) { - const strategy = this.strategies.get(strategyId); - if (strategy && strategy.hasPosition(fill.symbol)) { + // Forward fill to the strategy that created the order + for (const [strategyId, strategy] of this.strategies) { + if (strategy.hasOrder(fill.orderId)) { try { await strategy.onFill(fill); } catch (error) { - logger.error(`Strategy ${strategyId} error processing fill:`, error); + this.container.logger.error(`Error processing fill for strategy ${strategyId}:`, error); } + break; } } } private async handleStrategySignal(strategyId: string, signal: any): Promise { - logger.debug(`Strategy ${strategyId} generated signal:`, signal); + this.container.logger.info(`Strategy ${strategyId} generated signal:`, signal); - // Emit for monitoring/logging - this.emit('strategySignal', { - strategyId, - signal, - timestamp: Date.now() - }); - } + // Convert signal to order request + const orderRequest: OrderRequest = { + symbol: signal.symbol, + quantity: signal.quantity, + side: signal.side, + type: signal.orderType || 'market', + timeInForce: signal.timeInForce || 'day', + strategyId + }; - private async handleStrategyOrder(strategyId: string, order: OrderRequest): Promise { - 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); + // Submit order through execution service + const executionService = this.container.custom?.ExecutionService; + if (executionService) { + try { + const result = await executionService.submitOrder(orderRequest); + this.container.logger.info(`Order submitted for strategy ${strategyId}:`, result); + } catch (error) { + this.container.logger.error(`Failed to submit order for strategy ${strategyId}:`, error); } } } async onMarketData(data: MarketData): Promise { - // Called by backtest engine await this.handleMarketData(data); } @@ -225,52 +189,25 @@ export class StrategyManager extends EventEmitter { return this.tradingEngine; } + getActiveStrategies(): string[] { + return Array.from(this.activeStrategies); + } + getStrategy(strategyId: string): BaseStrategy | undefined { return this.strategies.get(strategyId); } - getAllStrategies(): Map { - return new Map(this.strategies); - } - - getActiveStrategies(): Set { - return new Set(this.activeStrategies); - } - - async updateStrategyConfig(strategyId: string, updates: Partial): Promise { - 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 { - const strategy = this.strategies.get(strategyId); - if (!strategy) { - throw new Error(`Strategy ${strategyId} not found`); - } - - return strategy.getPerformance(); - } - async shutdown(): Promise { - logger.info('Shutting down strategy manager...'); + this.container.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(); - } - + // Clear all strategies this.strategies.clear(); this.activeStrategies.clear(); - this.removeAllListeners(); + this.tradingEngine = null; } } \ No newline at end of file diff --git a/apps/stock/orchestrator/src/strategies/examples/MLEnhancedStrategy.ts b/apps/stock/orchestrator/src/strategies/examples/MLEnhancedStrategy.ts index 9b37a9a..286b649 100644 --- a/apps/stock/orchestrator/src/strategies/examples/MLEnhancedStrategy.ts +++ b/apps/stock/orchestrator/src/strategies/examples/MLEnhancedStrategy.ts @@ -1,6 +1,8 @@ import { BaseStrategy, Signal } from '../BaseStrategy'; import { MarketData } from '../../types'; -import { logger } from '@stock-bot/logger'; +import { getLogger } from '@stock-bot/logger'; + +const logger = getLogger('MLEnhancedStrategy'); import * as tf from '@tensorflow/tfjs-node'; interface MLModelConfig { diff --git a/apps/stock/orchestrator/src/strategies/examples/MeanReversionStrategy.ts b/apps/stock/orchestrator/src/strategies/examples/MeanReversionStrategy.ts index 2f294d4..1901b3d 100644 --- a/apps/stock/orchestrator/src/strategies/examples/MeanReversionStrategy.ts +++ b/apps/stock/orchestrator/src/strategies/examples/MeanReversionStrategy.ts @@ -1,6 +1,8 @@ import { BaseStrategy, Signal } from '../BaseStrategy'; import { MarketData } from '../../types'; -import { logger } from '@stock-bot/logger'; +import { getLogger } from '@stock-bot/logger'; + +const logger = getLogger('MeanReversionStrategy'); interface MeanReversionIndicators { sma20: number; diff --git a/apps/stock/orchestrator/src/types.ts b/apps/stock/orchestrator/src/types.ts index 55a2094..1a0dc8b 100644 --- a/apps/stock/orchestrator/src/types.ts +++ b/apps/stock/orchestrator/src/types.ts @@ -162,4 +162,4 @@ export const RiskMetricsSchema = z.object({ export type RiskMetrics = z.infer; // Re-export specialized types -export { MarketMicrostructure, PriceLevel, OrderBookSnapshot } from './types/MarketMicrostructure'; \ No newline at end of file +export type { MarketMicrostructure, PriceLevel, OrderBookSnapshot } from './types/MarketMicrostructure'; \ No newline at end of file diff --git a/apps/stock/web-api/src/index.ts b/apps/stock/web-api/src/index.ts index d6ab51d..4e2461a 100644 --- a/apps/stock/web-api/src/index.ts +++ b/apps/stock/web-api/src/index.ts @@ -31,7 +31,7 @@ const app = new ServiceApplication( enableHandlers: false, // Web API doesn't use handlers enableScheduledJobs: false, // Web API doesn't use scheduled jobs corsConfig: { - origin: ['http://localhost:4200', 'http://localhost:3000', 'http://localhost:3002'], + origin: ['http://localhost:4200', 'http://localhost:3000', 'http://localhost:3002', 'http://localhost:5173', 'http://localhost:5174'], allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], allowHeaders: ['Content-Type', 'Authorization'], credentials: true, diff --git a/apps/stock/web-api/src/routes/backtest.routes.ts b/apps/stock/web-api/src/routes/backtest.routes.ts new file mode 100644 index 0000000..16b077e --- /dev/null +++ b/apps/stock/web-api/src/routes/backtest.routes.ts @@ -0,0 +1,97 @@ +import { Hono } from 'hono'; +import type { IServiceContainer } from '@stock-bot/handlers'; +import { BacktestService } from '../services/backtest.service'; +import { getLogger } from '@stock-bot/logger'; + +const logger = getLogger('backtest-routes'); + +export function createBacktestRoutes(container: IServiceContainer) { + const backtestRoutes = new Hono(); + const backtestService = new BacktestService(); + + // Create a new backtest + backtestRoutes.post('/api/backtests', async (c) => { + try { + const body = await c.req.json(); + + // Validate required fields + if (!body.strategy || !body.symbols || !body.startDate || !body.endDate) { + return c.json({ + error: 'Missing required fields: strategy, symbols, startDate, endDate' + }, 400); + } + + const backtest = await backtestService.createBacktest({ + ...body, + initialCapital: body.initialCapital || 100000, + }); + + return c.json(backtest, 201); + } catch (error) { + logger.error('Failed to create backtest', { error }); + return c.json({ error: 'Failed to create backtest' }, 500); + } + }); + + // Get backtest status + backtestRoutes.get('/api/backtests/:id', async (c) => { + try { + const id = c.req.param('id'); + const backtest = await backtestService.getBacktest(id); + + if (!backtest) { + return c.json({ error: 'Backtest not found' }, 404); + } + + return c.json(backtest); + } catch (error) { + logger.error('Failed to get backtest', { error }); + return c.json({ error: 'Failed to get backtest' }, 500); + } + }); + + // Get backtest results + backtestRoutes.get('/api/backtests/:id/results', async (c) => { + try { + const id = c.req.param('id'); + const results = await backtestService.getBacktestResults(id); + + if (!results) { + return c.json({ error: 'Results not found' }, 404); + } + + return c.json(results); + } catch (error) { + logger.error('Failed to get results', { error }); + return c.json({ error: 'Failed to get results' }, 500); + } + }); + + // List all backtests + backtestRoutes.get('/api/backtests', async (c) => { + try { + const limit = Number(c.req.query('limit')) || 50; + const offset = Number(c.req.query('offset')) || 0; + + const backtests = await backtestService.listBacktests({ limit, offset }); + return c.json(backtests); + } catch (error) { + logger.error('Failed to list backtests', { error }); + return c.json({ error: 'Failed to list backtests' }, 500); + } + }); + + // Cancel a backtest + backtestRoutes.post('/api/backtests/:id/cancel', async (c) => { + try { + const id = c.req.param('id'); + await backtestService.cancelBacktest(id); + return c.json({ message: 'Backtest cancelled' }); + } catch (error) { + logger.error('Failed to cancel backtest', { error }); + return c.json({ error: 'Failed to cancel backtest' }, 500); + } + }); + + return backtestRoutes; +} \ No newline at end of file diff --git a/apps/stock/web-api/src/routes/create-routes.ts b/apps/stock/web-api/src/routes/create-routes.ts index ab2876e..0e084b4 100644 --- a/apps/stock/web-api/src/routes/create-routes.ts +++ b/apps/stock/web-api/src/routes/create-routes.ts @@ -9,6 +9,7 @@ import { createExchangeRoutes } from './exchange.routes'; import { createHealthRoutes } from './health.routes'; import { createMonitoringRoutes } from './monitoring.routes'; import { createPipelineRoutes } from './pipeline.routes'; +import { createBacktestRoutes } from './backtest.routes'; export function createRoutes(container: IServiceContainer): Hono { const app = new Hono(); @@ -18,12 +19,14 @@ export function createRoutes(container: IServiceContainer): Hono { const exchangeRoutes = createExchangeRoutes(container); const monitoringRoutes = createMonitoringRoutes(container); const pipelineRoutes = createPipelineRoutes(container); + const backtestRoutes = createBacktestRoutes(container); // Mount routes app.route('/health', healthRoutes); app.route('/api/exchanges', exchangeRoutes); app.route('/api/system/monitoring', monitoringRoutes); app.route('/api/pipeline', pipelineRoutes); + app.route('/', backtestRoutes); // Mounted at root since routes already have /api prefix return app; } diff --git a/apps/stock/web-api/src/services/backtest.service.ts b/apps/stock/web-api/src/services/backtest.service.ts new file mode 100644 index 0000000..7313f2d --- /dev/null +++ b/apps/stock/web-api/src/services/backtest.service.ts @@ -0,0 +1,174 @@ +import { v4 as uuidv4 } from 'uuid'; +import { getLogger } from '@stock-bot/logger'; + +const logger = getLogger('backtest-service'); + +// Use environment variable or default +const ORCHESTRATOR_URL = process.env.ORCHESTRATOR_URL || 'http://localhost:2004'; + +export interface BacktestRequest { + strategy: string; + symbols: string[]; + startDate: string; + endDate: string; + initialCapital: number; + config?: Record; +} + +export interface BacktestJob { + id: string; + status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'; + strategy: string; + symbols: string[]; + startDate: Date; + endDate: Date; + initialCapital: number; + config: Record; + createdAt: Date; + updatedAt: Date; + error?: string; +} + +// In-memory storage for demo (replace with database) +const backtestStore = new Map(); +const backtestResults = new Map(); + +export class BacktestService { + async createBacktest(request: BacktestRequest): Promise { + const backtestId = uuidv4(); + + // Store in memory (replace with database) + const backtest: BacktestJob = { + id: backtestId, + status: 'pending', + strategy: request.strategy, + symbols: request.symbols, + startDate: new Date(request.startDate), + endDate: new Date(request.endDate), + initialCapital: request.initialCapital, + config: request.config || {}, + createdAt: new Date(), + updatedAt: new Date(), + }; + + backtestStore.set(backtestId, backtest); + + // Call orchestrator to run backtest + try { + const response = await fetch(`${ORCHESTRATOR_URL}/api/backtest/run`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + mode: 'backtest', + startDate: new Date(request.startDate).toISOString(), + endDate: new Date(request.endDate).toISOString(), + symbols: request.symbols, + initialCapital: request.initialCapital, + dataFrequency: '1d', // Default to daily + speed: 'max', // Default speed + fillModel: { + slippage: 'realistic', + marketImpact: true, + partialFills: true + } + }), + }); + + if (!response.ok) { + throw new Error(`Orchestrator returned ${response.status}`); + } + + const result = await response.json(); + + // Store result when available + if (result.performance) { + // Backtest completed immediately + backtest.status = 'completed'; + backtestStore.set(backtestId, backtest); + backtestResults.set(backtestId, result); + } else { + // Update status to running if not completed + backtest.status = 'running'; + backtestStore.set(backtestId, backtest); + } + + logger.info('Backtest started in orchestrator', { backtestId, result }); + } catch (error) { + logger.error('Failed to start backtest in orchestrator', { backtestId, error }); + backtest.status = 'failed'; + backtest.error = error.message; + } + + return backtest; + } + + async getBacktest(id: string): Promise { + return backtestStore.get(id) || null; + } + + async getBacktestResults(id: string): Promise { + const results = backtestResults.get(id); + if (!results) return null; + + // Transform orchestrator response to frontend expected format + return { + backtestId: results.id || id, + metrics: { + totalReturn: results.performance?.totalReturn || 0, + sharpeRatio: results.performance?.sharpeRatio || 0, + maxDrawdown: results.performance?.maxDrawdown || 0, + winRate: results.performance?.winRate || 0, + totalTrades: results.performance?.totalTrades || 0, + profitFactor: results.performance?.profitFactor + }, + equity: results.equityCurve?.map((point: any) => ({ + date: new Date(point.timestamp).toISOString(), + value: point.value + })) || [], + trades: results.trades?.map((trade: any) => ({ + symbol: trade.symbol, + entryDate: new Date(trade.entryTime).toISOString(), + exitDate: new Date(trade.exitTime).toISOString(), + entryPrice: trade.entryPrice, + exitPrice: trade.exitPrice, + quantity: trade.quantity, + pnl: trade.pnl + })) || [], + ohlcData: results.ohlcData || {} + }; + } + + async listBacktests(params: { limit: number; offset: number }): Promise { + const all = Array.from(backtestStore.values()); + return all + .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()) + .slice(params.offset, params.offset + params.limit); + } + + async updateBacktestStatus(id: string, status: BacktestJob['status'], error?: string): Promise { + const backtest = backtestStore.get(id); + if (backtest) { + backtest.status = status; + backtest.updatedAt = new Date(); + if (error) { + backtest.error = error; + } + backtestStore.set(id, backtest); + } + } + + async cancelBacktest(id: string): Promise { + await this.updateBacktestStatus(id, 'cancelled'); + + // Call orchestrator to stop backtest + try { + await fetch(`${ORCHESTRATOR_URL}/api/backtest/stop`, { + method: 'POST', + }); + } catch (error) { + logger.error('Failed to stop backtest in orchestrator', { backtestId: id, error }); + } + } +} \ No newline at end of file diff --git a/apps/stock/web-app/src/components/charts/Chart.tsx b/apps/stock/web-app/src/components/charts/Chart.tsx new file mode 100644 index 0000000..101cd57 --- /dev/null +++ b/apps/stock/web-app/src/components/charts/Chart.tsx @@ -0,0 +1,215 @@ +import * as LightweightCharts from 'lightweight-charts'; +import { useEffect, useRef, useState } from 'react'; + +export interface ChartData { + time: number; + open?: number; + high?: number; + low?: number; + close?: number; + value?: number; + volume?: number; +} + +export interface ChartProps { + data: ChartData[]; + height?: number; + type?: 'candlestick' | 'line' | 'area'; + showVolume?: boolean; + theme?: 'light' | 'dark'; + overlayData?: Array<{ + name: string; + data: Array<{ time: number; value: number }>; + color?: string; + lineWidth?: number; + }>; + className?: string; +} + +export function Chart({ + data, + height = 400, + type = 'candlestick', + showVolume = true, + theme = 'dark', + overlayData = [], + className = '', +}: ChartProps) { + const chartContainerRef = useRef(null); + const chartRef = useRef(null); + const mainSeriesRef = useRef | null>(null); + const volumeSeriesRef = useRef | null>(null); + const overlaySeriesRef = useRef>>(new Map()); + + // Debug logging + console.log('Chart - data received:', data); + console.log('Chart - data length:', data?.length); + console.log('Chart - data type:', Array.isArray(data) ? 'array' : typeof data); + console.log('Chart - first data point:', data?.[0]); + + useEffect(() => { + if (!chartContainerRef.current || !data || !data.length) { + console.log('Chart - early return:', { + hasContainer: !!chartContainerRef.current, + hasData: !!data, + dataLength: data?.length + }); + return; + } + + // Create chart + const chart = LightweightCharts.createChart(chartContainerRef.current, { + width: chartContainerRef.current.clientWidth, + height: height, + layout: { + background: { + type: LightweightCharts.ColorType.Solid, + color: theme === 'dark' ? '#0f0f0f' : '#ffffff' + }, + textColor: theme === 'dark' ? '#d1d5db' : '#374151', + }, + grid: { + vertLines: { + color: theme === 'dark' ? '#1f2937' : '#e5e7eb', + visible: true, + }, + horzLines: { + color: theme === 'dark' ? '#1f2937' : '#e5e7eb', + visible: true, + }, + }, + crosshair: { + mode: LightweightCharts.CrosshairMode.Normal, + }, + rightPriceScale: { + borderColor: theme === 'dark' ? '#1f2937' : '#e5e7eb', + autoScale: true, + }, + timeScale: { + borderColor: theme === 'dark' ? '#1f2937' : '#e5e7eb', + timeVisible: true, + secondsVisible: false, + }, + }); + + chartRef.current = chart; + + // Create main series + if (type === 'candlestick' && data[0].open !== undefined) { + mainSeriesRef.current = chart.addCandlestickSeries({ + upColor: '#10b981', + downColor: '#ef4444', + borderUpColor: '#10b981', + borderDownColor: '#ef4444', + wickUpColor: '#10b981', + wickDownColor: '#ef4444', + }); + mainSeriesRef.current.setData(data as LightweightCharts.CandlestickData[]); + } else if (type === 'line' || (type === 'candlestick' && data[0].value !== undefined)) { + mainSeriesRef.current = chart.addLineSeries({ + color: '#3b82f6', + lineWidth: 2, + }); + const lineData = data.map(d => ({ + time: d.time, + value: d.value ?? d.close ?? 0 + })); + mainSeriesRef.current.setData(lineData); + } else if (type === 'area') { + mainSeriesRef.current = chart.addAreaSeries({ + lineColor: '#3b82f6', + topColor: '#3b82f6', + bottomColor: 'rgba(59, 130, 246, 0.1)', + lineWidth: 2, + }); + const areaData = data.map(d => ({ + time: d.time, + value: d.value ?? d.close ?? 0 + })); + mainSeriesRef.current.setData(areaData); + } + + // Add volume if available + if (showVolume && data.some(d => d.volume)) { + volumeSeriesRef.current = chart.addHistogramSeries({ + color: '#3b82f680', + priceFormat: { + type: 'volume', + }, + priceScaleId: 'volume', + }); + + volumeSeriesRef.current.priceScale().applyOptions({ + scaleMargins: { + top: 0.8, + bottom: 0, + }, + }); + + const volumeData = data + .filter(d => d.volume !== undefined) + .map(d => ({ + time: d.time, + value: d.volume!, + color: d.close && d.open ? + (d.close >= d.open ? '#10b98140' : '#ef444440') : + '#3b82f640', + })); + volumeSeriesRef.current.setData(volumeData); + } + + // Add overlay series + overlaySeriesRef.current.clear(); + overlayData.forEach((overlay, index) => { + const series = chart.addLineSeries({ + color: overlay.color || ['#ff9800', '#4caf50', '#9c27b0', '#f44336'][index % 4], + lineWidth: overlay.lineWidth || 2, + title: overlay.name, + priceScaleId: index === 0 ? '' : `overlay-${index}`, // First overlay uses main scale + }); + + if (index > 0) { + series.priceScale().applyOptions({ + scaleMargins: { + top: 0.1, + bottom: 0.1, + }, + }); + } + + series.setData(overlay.data); + overlaySeriesRef.current.set(overlay.name, series); + }); + + // Fit content + chart.timeScale().fitContent(); + + // Handle resize + const handleResize = () => { + if (chartContainerRef.current && chart) { + chart.applyOptions({ + width: chartContainerRef.current.clientWidth, + }); + } + }; + + window.addEventListener('resize', handleResize); + + // Cleanup + return () => { + window.removeEventListener('resize', handleResize); + if (chart) { + chart.remove(); + } + }; + }, [data, height, type, showVolume, theme, overlayData]); + + return ( +
+
+
+ ); +} \ No newline at end of file diff --git a/apps/stock/web-app/src/components/charts/index.ts b/apps/stock/web-app/src/components/charts/index.ts new file mode 100644 index 0000000..69a7bff --- /dev/null +++ b/apps/stock/web-app/src/components/charts/index.ts @@ -0,0 +1,2 @@ +export { Chart } from './Chart'; +export type { ChartProps, ChartData } from './Chart'; \ No newline at end of file diff --git a/apps/stock/web-app/src/features/backtest/BacktestListPage.tsx b/apps/stock/web-app/src/features/backtest/BacktestListPage.tsx new file mode 100644 index 0000000..5a8a4e1 --- /dev/null +++ b/apps/stock/web-app/src/features/backtest/BacktestListPage.tsx @@ -0,0 +1,119 @@ +import { useEffect } from 'react'; +import { useBacktestList } from './hooks/useBacktest'; +import { Link } from 'react-router-dom'; + +export function BacktestListPage() { + const { backtests, isLoading, error, loadBacktests } = useBacktestList(); + + useEffect(() => { + loadBacktests(); + }, [loadBacktests]); + + const getStatusColor = (status: string) => { + switch (status) { + case 'completed': + return 'text-success'; + case 'running': + return 'text-primary-400'; + case 'failed': + return 'text-error'; + case 'cancelled': + return 'text-text-muted'; + default: + return 'text-text-secondary'; + } + }; + + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleString(); + }; + + return ( +
+
+
+

Backtest History

+

+ View and manage your backtest runs +

+
+ + New Backtest + +
+ + {error && ( +
+ {error} +
+ )} + + {isLoading ? ( +
+
Loading backtests...
+
+ ) : backtests.length === 0 ? ( +
+

No backtests found

+ + Create Your First Backtest + +
+ ) : ( +
+ + + + + + + + + + + + + + {backtests.map((backtest) => ( + + + + + + + + + + ))} + +
IDStrategySymbolsPeriodStatusCreatedActions
+ {backtest.id.slice(0, 8)}... + + {backtest.strategy} + + {backtest.symbols.join(', ')} + + {new Date(backtest.startDate).toLocaleDateString()} - {new Date(backtest.endDate).toLocaleDateString()} + + {backtest.status} + + {formatDate(backtest.createdAt)} + + + View + +
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/apps/stock/web-app/src/features/backtest/BacktestPage.tsx b/apps/stock/web-app/src/features/backtest/BacktestPage.tsx index 786fcf2..947e603 100644 --- a/apps/stock/web-app/src/features/backtest/BacktestPage.tsx +++ b/apps/stock/web-app/src/features/backtest/BacktestPage.tsx @@ -1,23 +1,114 @@ +import { useCallback, useEffect, useState } from 'react'; import { BacktestConfiguration } from './components/BacktestConfiguration'; -import { BacktestResults } from './components/BacktestResults'; import { BacktestControls } from './components/BacktestControls'; +import { BacktestResults } from './components/BacktestResults'; import { useBacktest } from './hooks/useBacktest'; +import type { BacktestConfig, BacktestResult as LocalBacktestResult } from './types/backtest.types'; export function BacktestPage() { const { - config, - status, + backtest, results, - currentTime, - error, isLoading, - handleConfigSubmit, - handleStart, - handlePause, - handleResume, - handleStop, - handleStep, + isPolling, + error, + createBacktest, + cancelBacktest, + reset, } = useBacktest(); + + // Local state to bridge between the API format and the existing UI components + const [config, setConfig] = useState(null); + const [adaptedResults, setAdaptedResults] = useState(null); + + // Adapt the backtest status from API format to local format + const status = backtest ? + (backtest.status === 'pending' ? 'configured' : + backtest.status === 'running' ? 'running' : + backtest.status === 'completed' ? 'completed' : + backtest.status === 'failed' ? 'error' : + backtest.status === 'cancelled' ? 'stopped' : 'idle') : 'idle'; + + // Current time is not available in the new API, so we'll estimate it based on progress + const currentTime = null; + + // Adapt the results when they come in + useEffect(() => { + if (results && config) { + setAdaptedResults({ + id: backtest?.id || '', + config, + metrics: { + totalReturn: results.metrics.totalReturn, + sharpeRatio: results.metrics.sharpeRatio, + maxDrawdown: results.metrics.maxDrawdown, + winRate: results.metrics.winRate, + totalTrades: results.metrics.totalTrades, + profitableTrades: Math.round(results.metrics.totalTrades * results.metrics.winRate / 100), + }, + positions: [], // Not provided by current API + trades: results.trades?.map(t => ({ + id: `${t.symbol}-${t.entryDate}`, + timestamp: t.exitDate, + symbol: t.symbol, + side: t.pnl > 0 ? 'buy' : 'sell', + quantity: t.quantity, + price: t.exitPrice, + commission: 0, + pnl: t.pnl, + })) || [], + performanceData: results.equity.map(e => ({ + timestamp: e.date, + portfolioValue: e.value, + pnl: 0, // Would need to calculate from equity curve + drawdown: 0, // Would need to calculate + })), + }); + } + }, [results, config, backtest]); + + const handleConfigSubmit = useCallback(async (newConfig: BacktestConfig) => { + setConfig(newConfig); + setAdaptedResults(null); + + // Convert local config to API format + await createBacktest({ + strategy: newConfig.strategy, + symbols: newConfig.symbols, + startDate: newConfig.startDate.toISOString().split('T')[0], + endDate: newConfig.endDate.toISOString().split('T')[0], + initialCapital: newConfig.initialCapital, + config: { + commission: newConfig.commission, + slippage: newConfig.slippage, + speedMultiplier: newConfig.speedMultiplier, + }, + }); + }, [createBacktest]); + + const handleStart = useCallback(() => { + // Backtest starts automatically after creation in the new API + // Nothing to do here + }, []); + + const handlePause = useCallback(() => { + // Pause not supported in current API + console.warn('Pause not supported in current API'); + }, []); + + const handleResume = useCallback(() => { + // Resume not supported in current API + console.warn('Resume not supported in current API'); + }, []); + + const handleStop = useCallback(async () => { + await cancelBacktest(); + }, [cancelBacktest]); + + const handleStep = useCallback(() => { + // Step not supported in current API + console.warn('Step not supported in current API'); + }, []); return (
@@ -38,7 +129,7 @@ export function BacktestPage() {
{config && ( @@ -59,7 +150,7 @@ export function BacktestPage() {
diff --git a/apps/stock/web-app/src/features/backtest/components/BacktestResults.tsx b/apps/stock/web-app/src/features/backtest/components/BacktestResults.tsx index cdb3729..fe16c3e 100644 --- a/apps/stock/web-app/src/features/backtest/components/BacktestResults.tsx +++ b/apps/stock/web-app/src/features/backtest/components/BacktestResults.tsx @@ -1,7 +1,10 @@ -import type { BacktestResult, BacktestStatus } from '../types'; +import type { BacktestStatus } from '../types'; +import type { BacktestResult } from '../services/backtestApi'; import { MetricsCard } from './MetricsCard'; import { PositionsTable } from './PositionsTable'; import { TradeLog } from './TradeLog'; +import { Chart } from '../../../components/charts'; +import { useState, useMemo } from 'react'; interface BacktestResultsProps { status: BacktestStatus; @@ -10,6 +13,11 @@ interface BacktestResultsProps { } export function BacktestResults({ status, results, currentTime }: BacktestResultsProps) { + // Debug logging + console.log('BacktestResults - results:', results); + console.log('BacktestResults - ohlcData keys:', results?.ohlcData ? Object.keys(results.ohlcData) : 'No ohlcData'); + console.log('BacktestResults - first symbol data:', results?.ohlcData && Object.keys(results.ohlcData).length > 0 ? results.ohlcData[Object.keys(results.ohlcData)[0]] : 'No data'); + console.log('BacktestResults - equity data:', results?.equity); if (status === 'idle') { return (
@@ -99,41 +107,98 @@ export function BacktestResults({ status, results, currentTime }: BacktestResult title="Total Trades" value={results.metrics.totalTrades.toString()} /> - + {results.metrics.profitFactor && ( + = 1 ? 'up' : 'down'} + /> + )}
- {/* Performance Chart Placeholder */} + {/* Performance Chart */}

Portfolio Performance

-
-

- Performance chart will be displayed here (requires recharts) -

-
+ {(() => { + const hasOhlcData = results.ohlcData && Object.keys(results.ohlcData).length > 0; + const hasEquityData = results.equity && results.equity.length > 0; + + console.log('Chart section - hasOhlcData:', hasOhlcData); + console.log('Chart section - hasEquityData:', hasEquityData); + + if (hasOhlcData) { + const firstSymbol = Object.keys(results.ohlcData)[0]; + const ohlcData = results.ohlcData[firstSymbol]; + console.log('Chart section - using OHLC data for symbol:', firstSymbol); + console.log('Chart section - OHLC data points:', ohlcData?.length); + + return ( + ({ + time: Math.floor(new Date(point.date).getTime() / 1000), + value: point.value + })), + color: '#10b981', + lineWidth: 3 + } + ] : []} + className="rounded" + /> + ); + } else if (hasEquityData) { + console.log('Chart section - using equity data only'); + return ( + ({ + time: Math.floor(new Date(point.date).getTime() / 1000), + value: point.value + }))} + height={400} + type="area" + showVolume={false} + theme="dark" + className="rounded" + /> + ); + } else { + console.log('Chart section - showing no data message'); + return ( +
+

+ No data available +

+
+ ); + } + })()}
- {/* Positions Table */} - {results.positions.length > 0 && ( -
-

- Current Positions -

- -
- )} - {/* Trade Log */} - {results.trades.length > 0 && ( + {results.trades && results.trades.length > 0 && (

Trade History

- + ({ + id: crypto.randomUUID(), + timestamp: trade.entryDate, + symbol: trade.symbol, + side: 'buy' as const, + quantity: trade.quantity, + price: trade.entryPrice, + commission: 0, + pnl: trade.pnl + }))} />
)}
diff --git a/apps/stock/web-app/src/features/backtest/hooks/useBacktest.ts b/apps/stock/web-app/src/features/backtest/hooks/useBacktest.ts index f567447..922e6ce 100644 --- a/apps/stock/web-app/src/features/backtest/hooks/useBacktest.ts +++ b/apps/stock/web-app/src/features/backtest/hooks/useBacktest.ts @@ -1,169 +1,175 @@ import { useCallback, useEffect, useRef, useState } from 'react'; -import { BacktestService } from '../services/backtestService'; -import type { BacktestConfig, BacktestResult, BacktestStatus } from '../types'; +import type { BacktestJob, BacktestRequest, BacktestResult } from '../services/backtestApi'; +import { backtestApi, } from '../services/backtestApi'; -export function useBacktest() { - const [backtestId, setBacktestId] = useState(null); - const [config, setConfig] = useState(null); - const [status, setStatus] = useState('idle'); +interface UseBacktestReturn { + // State + backtest: BacktestJob | null; + results: BacktestResult | null; + isLoading: boolean; + isPolling: boolean; + error: string | null; + + // Actions + createBacktest: (request: BacktestRequest) => Promise; + cancelBacktest: () => Promise; + reset: () => void; +} + +export function useBacktest(): UseBacktestReturn { + const [backtest, setBacktest] = useState(null); const [results, setResults] = useState(null); - const [currentTime, setCurrentTime] = useState(null); - const [progress, setProgress] = useState(0); - const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(false); + const [isPolling, setIsPolling] = useState(false); + const [error, setError] = useState(null); + + const pollingIntervalRef = useRef(null); - const cleanupRef = useRef<(() => void) | null>(null); - - const handleConfigSubmit = useCallback(async (newConfig: BacktestConfig) => { + // Poll for status updates + const pollStatus = useCallback(async (backtestId: string) => { try { - setIsLoading(true); - setError(null); + const updatedBacktest = await backtestApi.getBacktest(backtestId); + setBacktest(updatedBacktest); + + if (updatedBacktest.status === 'completed') { + // Fetch results + const backtestResults = await backtestApi.getBacktestResults(backtestId); + setResults(backtestResults); + setIsPolling(false); + + // Clear polling interval + if (pollingIntervalRef.current) { + clearInterval(pollingIntervalRef.current); + pollingIntervalRef.current = null; + } + } else if (updatedBacktest.status === 'failed' || updatedBacktest.status === 'cancelled') { + setIsPolling(false); + + // Clear polling interval + if (pollingIntervalRef.current) { + clearInterval(pollingIntervalRef.current); + pollingIntervalRef.current = null; + } + + if (updatedBacktest.status === 'failed' && updatedBacktest.error) { + setError(updatedBacktest.error); + } + } + } catch (err) { + console.error('Error polling backtest status:', err); + // Don't stop polling on transient errors + } + }, []); + + // Create a new backtest + const createBacktest = useCallback(async (request: BacktestRequest) => { + setIsLoading(true); + setError(null); + setResults(null); + + try { + const newBacktest = await backtestApi.createBacktest(request); + setBacktest(newBacktest); - // Create backtest - const { id } = await BacktestService.createBacktest(newConfig); + // Start polling for updates + setIsPolling(true); + pollingIntervalRef.current = setInterval(() => { + pollStatus(newBacktest.id); + }, 2000); // Poll every 2 seconds - setBacktestId(id); - setConfig(newConfig); - setStatus('configured'); - setResults(null); - setCurrentTime(null); - setProgress(0); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to create backtest'); } finally { setIsLoading(false); } - }, []); + }, [pollStatus]); - const handleStart = useCallback(async () => { - if (!backtestId) return; + // Cancel running backtest + const cancelBacktest = useCallback(async () => { + if (!backtest || backtest.status !== 'running') return; try { - setIsLoading(true); - setError(null); + await backtestApi.cancelBacktest(backtest.id); + setBacktest({ ...backtest, status: 'cancelled' }); + setIsPolling(false); - await BacktestService.startBacktest(backtestId); - setStatus('running'); - - // Start polling for updates - cleanupRef.current = await BacktestService.pollBacktestUpdates( - backtestId, - (newStatus, newProgress, newTime) => { - setStatus(newStatus); - if (newProgress !== undefined) setProgress(newProgress); - if (newTime !== undefined) setCurrentTime(newTime); - - // Fetch full results when completed - if (newStatus === 'completed') { - BacktestService.getBacktestResults(backtestId).then(setResults); - } - } - ); - } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to start backtest'); - } finally { - setIsLoading(false); - } - }, [backtestId]); - - const handlePause = useCallback(async () => { - if (!backtestId) return; - - try { - setIsLoading(true); - setError(null); - - await BacktestService.pauseBacktest(backtestId); - setStatus('paused'); - } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to pause backtest'); - } finally { - setIsLoading(false); - } - }, [backtestId]); - - const handleResume = useCallback(async () => { - if (!backtestId) return; - - try { - setIsLoading(true); - setError(null); - - await BacktestService.resumeBacktest(backtestId); - setStatus('running'); - } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to resume backtest'); - } finally { - setIsLoading(false); - } - }, [backtestId]); - - const handleStop = useCallback(async () => { - if (!backtestId) return; - - try { - setIsLoading(true); - setError(null); - - await BacktestService.stopBacktest(backtestId); - setStatus('stopped'); - - // Stop polling - if (cleanupRef.current) { - cleanupRef.current(); - cleanupRef.current = null; + // Clear polling interval + if (pollingIntervalRef.current) { + clearInterval(pollingIntervalRef.current); + pollingIntervalRef.current = null; } } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to stop backtest'); - } finally { - setIsLoading(false); + setError(err instanceof Error ? err.message : 'Failed to cancel backtest'); } - }, [backtestId]); + }, [backtest]); - const handleStep = useCallback(async () => { - if (!backtestId) return; - - try { - setIsLoading(true); - setError(null); - - await BacktestService.stepBacktest(backtestId); - - // Get updated status - const statusUpdate = await BacktestService.getBacktestStatus(backtestId); - setStatus(statusUpdate.status); - if (statusUpdate.progress !== undefined) setProgress(statusUpdate.progress); - if (statusUpdate.currentTime !== undefined) setCurrentTime(statusUpdate.currentTime); - } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to step backtest'); - } finally { - setIsLoading(false); + // Reset state + const reset = useCallback(() => { + setBacktest(null); + setResults(null); + setError(null); + setIsLoading(false); + setIsPolling(false); + + // Clear polling interval + if (pollingIntervalRef.current) { + clearInterval(pollingIntervalRef.current); + pollingIntervalRef.current = null; } - }, [backtestId]); + }, []); // Cleanup on unmount useEffect(() => { return () => { - if (cleanupRef.current) { - cleanupRef.current(); + if (pollingIntervalRef.current) { + clearInterval(pollingIntervalRef.current); } }; }, []); return { - backtestId, - config, - status, + backtest, results, - currentTime, - progress, - error, isLoading, - handleConfigSubmit, - handleStart, - handlePause, - handleResume, - handleStop, - handleStep, + isPolling, + error, + createBacktest, + cancelBacktest, + reset, + }; +} + +// Separate hook for listing backtests +interface UseBacktestListReturn { + backtests: BacktestJob[]; + isLoading: boolean; + error: string | null; + loadBacktests: (limit?: number, offset?: number) => Promise; +} + +export function useBacktestList(): UseBacktestListReturn { + const [backtests, setBacktests] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const loadBacktests = useCallback(async (limit = 50, offset = 0) => { + setIsLoading(true); + setError(null); + + try { + const list = await backtestApi.listBacktests(limit, offset); + setBacktests(list); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to load backtests'); + } finally { + setIsLoading(false); + } + }, []); + + return { + backtests, + isLoading, + error, + loadBacktests, }; } \ No newline at end of file diff --git a/apps/stock/web-app/src/features/backtest/services/backtestApi.ts b/apps/stock/web-app/src/features/backtest/services/backtestApi.ts new file mode 100644 index 0000000..a1ad43c --- /dev/null +++ b/apps/stock/web-app/src/features/backtest/services/backtestApi.ts @@ -0,0 +1,119 @@ +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:2003'; + +export interface BacktestRequest { + strategy: string; + symbols: string[]; + startDate: string; + endDate: string; + initialCapital: number; + config?: Record; +} + +export interface BacktestJob { + id: string; + status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'; + strategy: string; + symbols: string[]; + startDate: string; + endDate: string; + initialCapital: number; + config: Record; + createdAt: string; + updatedAt: string; + error?: string; +} + +export interface BacktestResult { + backtestId: string; + metrics: { + totalReturn: number; + sharpeRatio: number; + maxDrawdown: number; + winRate: number; + totalTrades: number; + profitFactor?: number; + }; + equity: Array<{ date: string; value: number }>; + trades?: Array<{ + symbol: string; + entryDate: string; + exitDate: string; + entryPrice: number; + exitPrice: number; + quantity: number; + pnl: number; + }>; + ohlcData?: Record>; +} + +export const backtestApi = { + // Create a new backtest + async createBacktest(request: BacktestRequest): Promise { + const response = await fetch(`${API_BASE_URL}/api/backtests`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(request), + }); + + if (!response.ok) { + throw new Error(`Failed to create backtest: ${response.statusText}`); + } + + return response.json(); + }, + + // Get backtest status + async getBacktest(id: string): Promise { + const response = await fetch(`${API_BASE_URL}/api/backtests/${id}`); + + if (!response.ok) { + throw new Error(`Failed to get backtest: ${response.statusText}`); + } + + return response.json(); + }, + + // Get backtest results + async getBacktestResults(id: string): Promise { + const response = await fetch(`${API_BASE_URL}/api/backtests/${id}/results`); + + if (!response.ok) { + throw new Error(`Failed to get results: ${response.statusText}`); + } + + return response.json(); + }, + + // List all backtests + async listBacktests(limit = 50, offset = 0): Promise { + const response = await fetch( + `${API_BASE_URL}/api/backtests?limit=${limit}&offset=${offset}` + ); + + if (!response.ok) { + throw new Error(`Failed to list backtests: ${response.statusText}`); + } + + return response.json(); + }, + + // Cancel a running backtest + async cancelBacktest(id: string): Promise { + const response = await fetch(`${API_BASE_URL}/api/backtests/${id}/cancel`, { + method: 'POST', + }); + + if (!response.ok) { + throw new Error(`Failed to cancel backtest: ${response.statusText}`); + } + }, +}; \ No newline at end of file diff --git a/bun.lock b/bun.lock index fef587d..2ec5985 100644 --- a/bun.lock +++ b/bun.lock @@ -31,6 +31,7 @@ "prettier": "^3.5.3", "supertest": "^6.3.4", "ts-unused-exports": "^11.0.1", + "tsx": "^4.20.3", "turbo": "^2.5.4", "typescript": "^5.8.3", "yup": "^1.6.1", @@ -122,9 +123,11 @@ "@stock-bot/questdb": "*", "@stock-bot/queue": "*", "@stock-bot/shutdown": "*", + "@stock-bot/stock-config": "*", "@stock-bot/utils": "*", "axios": "^1.6.0", "hono": "^4.0.0", + "simple-statistics": "^7.8.3", "socket.io": "^4.7.2", "socket.io-client": "^4.7.2", "uuid": "^9.0.0", @@ -570,49 +573,55 @@ "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="], - "@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA=="], - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.5", "", { "os": "android", "cpu": "arm" }, "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA=="], - "@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="], + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.5", "", { "os": "android", "cpu": "arm64" }, "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg=="], - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="], + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.5", "", { "os": "android", "cpu": "x64" }, "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw=="], - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="], + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ=="], - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="], + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ=="], - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="], + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw=="], - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="], + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw=="], - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="], + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.5", "", { "os": "linux", "cpu": "arm" }, "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw=="], - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="], + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg=="], - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="], + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA=="], - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="], + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg=="], - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="], + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg=="], - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="], + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ=="], - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="], + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA=="], - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="], + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ=="], - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="], + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.5", "", { "os": "linux", "cpu": "x64" }, "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw=="], - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="], + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.5", "", { "os": "none", "cpu": "arm64" }, "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw=="], - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="], + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.5", "", { "os": "none", "cpu": "x64" }, "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ=="], - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="], + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.5", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw=="], - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="], + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg=="], - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.5", "", { "os": "win32", "cpu": "x64" }, "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g=="], "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="], @@ -1400,7 +1409,7 @@ "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], - "esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], + "esbuild": ["esbuild@0.25.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.5", "@esbuild/android-arm": "0.25.5", "@esbuild/android-arm64": "0.25.5", "@esbuild/android-x64": "0.25.5", "@esbuild/darwin-arm64": "0.25.5", "@esbuild/darwin-x64": "0.25.5", "@esbuild/freebsd-arm64": "0.25.5", "@esbuild/freebsd-x64": "0.25.5", "@esbuild/linux-arm": "0.25.5", "@esbuild/linux-arm64": "0.25.5", "@esbuild/linux-ia32": "0.25.5", "@esbuild/linux-loong64": "0.25.5", "@esbuild/linux-mips64el": "0.25.5", "@esbuild/linux-ppc64": "0.25.5", "@esbuild/linux-riscv64": "0.25.5", "@esbuild/linux-s390x": "0.25.5", "@esbuild/linux-x64": "0.25.5", "@esbuild/netbsd-arm64": "0.25.5", "@esbuild/netbsd-x64": "0.25.5", "@esbuild/openbsd-arm64": "0.25.5", "@esbuild/openbsd-x64": "0.25.5", "@esbuild/sunos-x64": "0.25.5", "@esbuild/win32-arm64": "0.25.5", "@esbuild/win32-ia32": "0.25.5", "@esbuild/win32-x64": "0.25.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], @@ -1540,7 +1549,7 @@ "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], - "fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], @@ -1564,6 +1573,8 @@ "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], + "get-tsconfig": ["get-tsconfig@4.10.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ=="], + "get-uri": ["get-uri@6.0.4", "", { "dependencies": { "basic-ftp": "^5.0.2", "data-uri-to-buffer": "^6.0.2", "debug": "^4.3.4" } }, "sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ=="], "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], @@ -2180,6 +2191,8 @@ "resolve-mongodb-srv": ["resolve-mongodb-srv@1.1.5", "", { "dependencies": { "whatwg-url": "^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0" }, "bin": { "resolve-mongodb-srv": "bin/resolve-mongodb-srv.js" } }, "sha512-flu1XTSLDJHvTnWu2aJh2w9jgGPcNYJn2obMkuzXiyWSz0MLXu9IRCjvirJ4zRoCPHJJPt3uLQVNJTrzFRWd1w=="], + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + "responselike": ["responselike@3.0.0", "", { "dependencies": { "lowercase-keys": "^3.0.0" } }, "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg=="], "ret": ["ret@0.1.15", "", {}, "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg=="], @@ -2250,6 +2263,8 @@ "simple-oauth2": ["simple-oauth2@5.1.0", "", { "dependencies": { "@hapi/hoek": "^11.0.4", "@hapi/wreck": "^18.0.0", "debug": "^4.3.4", "joi": "^17.6.4" } }, "sha512-gWDa38Ccm4MwlG5U7AlcJxPv3lvr80dU7ARJWrGdgvOKyzSj1gr3GBPN1rABTedAYvC/LsGYoFuFxwDBPtGEbw=="], + "simple-statistics": ["simple-statistics@7.8.8", "", {}, "sha512-CUtP0+uZbcbsFpqEyvNDYjJCl+612fNgjT8GaVuvMG7tBuJg8gXGpsP5M7X658zy0IcepWOZ6nPBu1Qb9ezA1w=="], + "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], "slice-ansi": ["slice-ansi@4.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ=="], @@ -2386,6 +2401,8 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "tsx": ["tsx@4.20.3", "", { "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ=="], + "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], "turbo": ["turbo@2.5.4", "", { "optionalDependencies": { "turbo-darwin-64": "2.5.4", "turbo-darwin-arm64": "2.5.4", "turbo-linux-64": "2.5.4", "turbo-linux-arm64": "2.5.4", "turbo-windows-64": "2.5.4", "turbo-windows-arm64": "2.5.4" }, "bin": { "turbo": "bin/turbo" } }, "sha512-kc8ZibdRcuWUG1pbYSBFWqmIjynlD8Lp7IB6U3vIzvOv9VG+6Sp8bzyeBWE3Oi8XV5KsQrznyRTBPvrf99E4mA=="], @@ -2596,6 +2613,8 @@ "bun-types/@types/node": ["@types/node@22.15.32", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-3jigKqgSjsH6gYZv2nEsqdXfZqIFGAV36XYYjf9KGZ3PSG+IhLecqPnI310RvjutyMwifE2hhhNEklOUrvx/wA=="], + "chokidar/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], @@ -2700,6 +2719,8 @@ "pkg-dir/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + "playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + "prebuild-install/tar-fs": ["tar-fs@2.1.3", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg=="], "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], @@ -2720,6 +2741,8 @@ "rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "rollup/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + "run-applescript/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], "send/mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="], @@ -2742,6 +2765,10 @@ "type-is/mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="], + "vite/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], + + "vite/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + "win-export-certificate-and-key/node-addon-api": ["node-addon-api@3.2.1", "", {}, "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A=="], "wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], @@ -2992,6 +3019,50 @@ "type-is/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], + + "vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], + + "vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="], + + "vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="], + + "vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="], + + "vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="], + + "vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="], + + "vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="], + + "vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="], + + "vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="], + + "vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="], + + "vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="], + + "vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="], + + "vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="], + + "vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="], + + "vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="], + + "vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="], + + "vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="], + + "vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="], + + "vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="], + + "vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="], + + "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], + "wrap-ansi/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], diff --git a/libs/core/di/package.json b/libs/core/di/package.json index ef1580a..293bdad 100644 --- a/libs/core/di/package.json +++ b/libs/core/di/package.json @@ -1,8 +1,8 @@ { "name": "@stock-bot/di", "version": "1.0.0", - "main": "./src/index.ts", - "types": "./src/index.ts", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", "scripts": { "build": "tsc", "clean": "rm -rf dist", diff --git a/package.json b/package.json index 134e721..1798985 100644 --- a/package.json +++ b/package.json @@ -95,6 +95,7 @@ "prettier": "^3.5.3", "supertest": "^6.3.4", "ts-unused-exports": "^11.0.1", + "tsx": "^4.20.3", "turbo": "^2.5.4", "typescript": "^5.8.3", "yup": "^1.6.1"