messy work. backtests / mock-data

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

View file

@ -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
}
}
}
}

View file

@ -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<StockAppConfig> | 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) {

View file

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

View file

@ -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
// Export all bindings
module.exports = nativeBinding

29
apps/stock/core/index.mjs Normal file
View file

@ -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;

Binary file not shown.

View file

@ -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": {

View file

@ -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(&microstructure_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<MarketUpdate> = serde_json::from_str(&data_json)
let data: Vec<MarketUpdate> = 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::<crate::core::market_data_sources::HistoricalDataSource>() {
historical_source.load_data(data);
}
}
Ok(())
}
#[napi]
pub fn generate_mock_data(&self, symbol: String, start_time: i64, end_time: i64, seed: Option<u32>) -> 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::<crate::core::market_data_sources::HistoricalDataSource>() {
let start_dt = DateTime::<Utc>::from_timestamp_millis(start_time)
.ok_or_else(|| Error::from_reason("Invalid start time"))?;
let end_dt = DateTime::<Utc>::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<RiskLimits> {
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<MarketMicrostructure> {
let intraday_volume_profile: Vec<f64> = 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,
})
}

View file

@ -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<Utc>,
end_time: DateTime<Utc>,
seed: Option<u64>
) {
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]

View file

@ -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<Utc>,
end_time: DateTime<Utc>,
interval_ms: i64,
) -> Vec<MarketUpdate> {
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<Utc>,
end_time: DateTime<Utc>,
trades_per_minute: u32,
) -> Vec<MarketUpdate> {
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<Utc>,
end_time: DateTime<Utc>,
timeframe: &str,
) -> Vec<MarketUpdate> {
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<Utc>,
end_time: DateTime<Utc>,
) -> Vec<MarketUpdate> {
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
}
}

View file

@ -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};

View file

@ -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",

View file

@ -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;

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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<string, Set<string>>();
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');
}

View file

@ -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<string, MarketMicrostructure> = 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<void> {
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<void> {
const tradingEngine = this.strategyManager.getTradingEngine();
if (!tradingEngine) return;
// Get current P&L
const [realized, unrealized] = tradingEngine.getTotalPnl();
const totalEquity = 100000 + realized + unrealized; // Assuming 100k starting capital
const totalEquity = await this.getPortfolioValue();
this.equityCurve.push({
timestamp: this.currentTime,
value: totalEquity
});
}
private async getPortfolioValue(): Promise<number> {
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<any[]> {
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<void> {
this.isRunning = false;
logger.info('Backtest stop requested');
this.container.logger.info('Backtest stop requested');
}
async exportResults(format: 'json' | 'csv' | 'html' = 'json'): Promise<string> {
@ -631,4 +688,25 @@ export class BacktestEngine extends EventEmitter {
</html>
`;
}
private getOHLCData(marketData: MarketData[], symbols: string[]): Record<string, any[]> {
const ohlcData: Record<string, any[]> = {};
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;
}
}

View file

@ -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';

View file

@ -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<void> {
// 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')
));
// 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.');
}

View file

@ -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<void> {
@ -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<void> {
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<void> {
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();

View file

@ -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;
}
}

View file

@ -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<string, any>;
}
interface CancelBacktestPayload {
backtestId: string;
}
@Handler('orchestrator')
export class BacktestHandler extends BaseHandler<OrchestratorServices> {
@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<BacktestPayload, 'backtestId'>) {
// 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,
},
],
};
}
}

View file

@ -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);
});

View file

@ -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<string, { bid: number; ask: number }>();
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');
}
}

View file

@ -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;
}

View file

@ -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<string, { data: any; timestamp: number }>();
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;
}
}

View file

@ -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<string, OrderRequest>();
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<void> {
@ -47,7 +52,7 @@ export class ExecutionService extends EventEmitter {
private async initializeBroker(broker: string, accountId: string): Promise<void> {
// In real implementation, would initialize specific broker API
// For example: Alpaca, Interactive Brokers, etc.
logger.info(`Initializing ${broker} broker connection for account ${accountId}`);
this.container.logger.info(`Initializing ${broker} broker connection for account ${accountId}`);
}
async submitOrder(orderRequest: OrderRequest): Promise<ExecutionReport> {
@ -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<ExecutionReport> {
// 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<boolean> {
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<void> {
// 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<ExecutionReport | null> {

View file

@ -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<void> {
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<void> {
@ -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;
}

View file

@ -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<void> {
this.mode = config.mode;
// Initialize QuestDB for time-series data
this.questdb = new QuestDBClient({
host: process.env.QUESTDB_HOST || 'localhost',
port: parseInt(process.env.QUESTDB_PORT || '9000'),
database: process.env.QUESTDB_DATABASE || 'trading'
});
// Initialize PostgreSQL for relational data
this.postgres = new PostgresClient({
host: process.env.POSTGRES_HOST || 'localhost',
port: parseInt(process.env.POSTGRES_PORT || '5432'),
database: process.env.POSTGRES_DATABASE || 'trading',
user: process.env.POSTGRES_USER || 'postgres',
password: process.env.POSTGRES_PASSWORD || 'postgres'
});
// Clients are already injected via DI
await this.createTables();
}
@ -281,12 +280,12 @@ export class StorageService {
async shutdown(): Promise<void> {
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;
}
}

View file

@ -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<any> {
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;
}

View file

@ -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';

View file

@ -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<string, BaseStrategy>();
private activeStrategies = new Set<string>();
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<void> {
@ -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<void> {
// 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<void> {
// Group by symbol for efficiency
const bySymbol = new Map<string, MarketData[]>();
for (const data of batch) {
const symbol = data.data.symbol;
if (!bySymbol.has(symbol)) {
bySymbol.set(symbol, []);
}
bySymbol.get(symbol)!.push(data);
}
// Forward to strategies
// 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<void> {
// 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<void> {
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<void> {
logger.info(`Strategy ${strategyId} placing order:`, order);
try {
// Submit order through execution service
const result = await this.executionService.submitOrder(order);
// Notify strategy of order result
const strategy = this.strategies.get(strategyId);
if (strategy) {
await strategy.onOrderUpdate(result);
}
// Emit for monitoring
this.emit('strategyOrder', {
strategyId,
order,
result,
timestamp: Date.now()
});
} catch (error) {
logger.error(`Failed to submit order from strategy ${strategyId}:`, error);
// Notify strategy of failure
const strategy = this.strategies.get(strategyId);
if (strategy) {
await strategy.onOrderError(order, error);
// 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<void> {
// 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<string, BaseStrategy> {
return new Map(this.strategies);
}
getActiveStrategies(): Set<string> {
return new Set(this.activeStrategies);
}
async updateStrategyConfig(strategyId: string, updates: Partial<StrategyConfig>): Promise<void> {
const strategy = this.strategies.get(strategyId);
if (!strategy) {
throw new Error(`Strategy ${strategyId} not found`);
}
await strategy.updateConfig(updates);
logger.info(`Updated configuration for strategy ${strategyId}`);
}
async getStrategyPerformance(strategyId: string): Promise<any> {
const strategy = this.strategies.get(strategyId);
if (!strategy) {
throw new Error(`Strategy ${strategyId} not found`);
}
return strategy.getPerformance();
}
async shutdown(): Promise<void> {
logger.info('Shutting down strategy manager...');
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;
}
}

View file

@ -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 {

View file

@ -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;

View file

@ -162,4 +162,4 @@ export const RiskMetricsSchema = z.object({
export type RiskMetrics = z.infer<typeof RiskMetricsSchema>;
// Re-export specialized types
export { MarketMicrostructure, PriceLevel, OrderBookSnapshot } from './types/MarketMicrostructure';
export type { MarketMicrostructure, PriceLevel, OrderBookSnapshot } from './types/MarketMicrostructure';

View file

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

View file

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

View file

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

View file

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

View file

@ -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<HTMLDivElement>(null);
const chartRef = useRef<LightweightCharts.IChartApi | null>(null);
const mainSeriesRef = useRef<LightweightCharts.ISeriesApi<any> | null>(null);
const volumeSeriesRef = useRef<LightweightCharts.ISeriesApi<any> | null>(null);
const overlaySeriesRef = useRef<Map<string, LightweightCharts.ISeriesApi<any>>>(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 (
<div className={`relative ${className}`}>
<div
ref={chartContainerRef}
style={{ width: '100%', height: `${height}px` }}
/>
</div>
);
}

View file

@ -0,0 +1,2 @@
export { Chart } from './Chart';
export type { ChartProps, ChartData } from './Chart';

View file

@ -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 (
<div className="flex flex-col h-full space-y-6">
<div className="flex-shrink-0 flex justify-between items-center">
<div>
<h1 className="text-lg font-bold text-text-primary mb-2">Backtest History</h1>
<p className="text-text-secondary text-sm">
View and manage your backtest runs
</p>
</div>
<Link
to="/backtests/new"
className="px-4 py-2 bg-primary-500 text-white rounded-md text-sm font-medium hover:bg-primary-600 transition-colors"
>
New Backtest
</Link>
</div>
{error && (
<div className="bg-error/10 border border-error text-error px-4 py-3 rounded-lg text-sm">
{error}
</div>
)}
{isLoading ? (
<div className="flex items-center justify-center h-64">
<div className="text-text-secondary">Loading backtests...</div>
</div>
) : backtests.length === 0 ? (
<div className="bg-surface-secondary p-8 rounded-lg border border-border text-center">
<p className="text-text-secondary mb-4">No backtests found</p>
<Link
to="/backtests/new"
className="inline-flex px-4 py-2 bg-primary-500 text-white rounded-md text-sm font-medium hover:bg-primary-600 transition-colors"
>
Create Your First Backtest
</Link>
</div>
) : (
<div className="bg-surface-secondary rounded-lg border border-border overflow-hidden">
<table className="w-full">
<thead className="bg-background border-b border-border">
<tr>
<th className="px-4 py-3 text-left text-sm font-medium text-text-secondary">ID</th>
<th className="px-4 py-3 text-left text-sm font-medium text-text-secondary">Strategy</th>
<th className="px-4 py-3 text-left text-sm font-medium text-text-secondary">Symbols</th>
<th className="px-4 py-3 text-left text-sm font-medium text-text-secondary">Period</th>
<th className="px-4 py-3 text-left text-sm font-medium text-text-secondary">Status</th>
<th className="px-4 py-3 text-left text-sm font-medium text-text-secondary">Created</th>
<th className="px-4 py-3 text-left text-sm font-medium text-text-secondary">Actions</th>
</tr>
</thead>
<tbody>
{backtests.map((backtest) => (
<tr key={backtest.id} className="border-b border-border hover:bg-background/50 transition-colors">
<td className="px-4 py-3 text-sm text-text-primary font-mono">
{backtest.id.slice(0, 8)}...
</td>
<td className="px-4 py-3 text-sm text-text-primary capitalize">
{backtest.strategy}
</td>
<td className="px-4 py-3 text-sm text-text-primary">
{backtest.symbols.join(', ')}
</td>
<td className="px-4 py-3 text-sm text-text-primary">
{new Date(backtest.startDate).toLocaleDateString()} - {new Date(backtest.endDate).toLocaleDateString()}
</td>
<td className={`px-4 py-3 text-sm font-medium capitalize ${getStatusColor(backtest.status)}`}>
{backtest.status}
</td>
<td className="px-4 py-3 text-sm text-text-secondary">
{formatDate(backtest.createdAt)}
</td>
<td className="px-4 py-3 text-sm">
<Link
to={`/backtests/${backtest.id}`}
className="text-primary-400 hover:text-primary-300 font-medium"
>
View
</Link>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
);
}

View file

@ -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<BacktestConfig | null>(null);
const [adaptedResults, setAdaptedResults] = useState<LocalBacktestResult | null>(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 (
<div className="flex flex-col h-full space-y-6">
@ -38,7 +129,7 @@ export function BacktestPage() {
<div className="lg:col-span-1 space-y-4">
<BacktestConfiguration
onSubmit={handleConfigSubmit}
disabled={status === 'running' || isLoading}
disabled={status === 'running' || isLoading || isPolling}
/>
{config && (
@ -59,7 +150,7 @@ export function BacktestPage() {
<div className="lg:col-span-2">
<BacktestResults
status={status}
results={results}
results={adaptedResults}
currentTime={currentTime}
/>
</div>

View file

@ -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 (
<div className="bg-surface-secondary p-8 rounded-lg border border-border h-full flex items-center justify-center">
@ -99,41 +107,98 @@ export function BacktestResults({ status, results, currentTime }: BacktestResult
title="Total Trades"
value={results.metrics.totalTrades.toString()}
/>
<MetricsCard
title="Profitable Trades"
value={results.metrics.profitableTrades.toString()}
/>
{results.metrics.profitFactor && (
<MetricsCard
title="Profit Factor"
value={results.metrics.profitFactor.toFixed(2)}
trend={results.metrics.profitFactor >= 1 ? 'up' : 'down'}
/>
)}
</div>
{/* Performance Chart Placeholder */}
{/* Performance Chart */}
<div className="bg-surface-secondary p-4 rounded-lg border border-border">
<h3 className="text-base font-medium text-text-primary mb-4">
Portfolio Performance
</h3>
<div className="h-64 bg-background rounded border border-border flex items-center justify-center">
<p className="text-sm text-text-muted">
Performance chart will be displayed here (requires recharts)
</p>
</div>
{(() => {
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 (
<Chart
data={ohlcData}
height={400}
type="candlestick"
showVolume={true}
theme="dark"
overlayData={hasEquityData ? [
{
name: 'Portfolio Value',
data: results.equity.map(point => ({
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 (
<Chart
data={results.equity.map(point => ({
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 (
<div className="h-96 bg-background rounded border border-border flex items-center justify-center">
<p className="text-sm text-text-muted">
No data available
</p>
</div>
);
}
})()}
</div>
{/* Positions Table */}
{results.positions.length > 0 && (
<div className="bg-surface-secondary p-4 rounded-lg border border-border">
<h3 className="text-base font-medium text-text-primary mb-4">
Current Positions
</h3>
<PositionsTable positions={results.positions} />
</div>
)}
{/* Trade Log */}
{results.trades.length > 0 && (
{results.trades && results.trades.length > 0 && (
<div className="bg-surface-secondary p-4 rounded-lg border border-border">
<h3 className="text-base font-medium text-text-primary mb-4">
Trade History
</h3>
<TradeLog trades={results.trades} />
<TradeLog trades={results.trades.map(trade => ({
id: crypto.randomUUID(),
timestamp: trade.entryDate,
symbol: trade.symbol,
side: 'buy' as const,
quantity: trade.quantity,
price: trade.entryPrice,
commission: 0,
pnl: trade.pnl
}))} />
</div>
)}
</div>

View file

@ -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<string | null>(null);
const [config, setConfig] = useState<BacktestConfig | null>(null);
const [status, setStatus] = useState<BacktestStatus>('idle');
interface UseBacktestReturn {
// State
backtest: BacktestJob | null;
results: BacktestResult | null;
isLoading: boolean;
isPolling: boolean;
error: string | null;
// Actions
createBacktest: (request: BacktestRequest) => Promise<void>;
cancelBacktest: () => Promise<void>;
reset: () => void;
}
export function useBacktest(): UseBacktestReturn {
const [backtest, setBacktest] = useState<BacktestJob | null>(null);
const [results, setResults] = useState<BacktestResult | null>(null);
const [currentTime, setCurrentTime] = useState<number | null>(null);
const [progress, setProgress] = useState<number>(0);
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [isPolling, setIsPolling] = useState(false);
const [error, setError] = useState<string | null>(null);
const pollingIntervalRef = useRef<NodeJS.Timeout | null>(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<void>;
}
export function useBacktestList(): UseBacktestListReturn {
const [backtests, setBacktests] = useState<BacktestJob[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(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,
};
}

View file

@ -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<string, any>;
}
export interface BacktestJob {
id: string;
status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
strategy: string;
symbols: string[];
startDate: string;
endDate: string;
initialCapital: number;
config: Record<string, any>;
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<string, Array<{
time: number;
open: number;
high: number;
low: number;
close: number;
volume?: number;
}>>;
}
export const backtestApi = {
// Create a new backtest
async createBacktest(request: BacktestRequest): Promise<BacktestJob> {
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<BacktestJob> {
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<BacktestResult> {
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<BacktestJob[]> {
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<void> {
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}`);
}
},
};