messy work. backtests / mock-data
This commit is contained in:
parent
4e4a048988
commit
fa70ada2bb
51 changed files with 2576 additions and 887 deletions
|
|
@ -222,6 +222,21 @@
|
||||||
"origins": ["http://localhost:3000", "http://localhost:4200"],
|
"origins": ["http://localhost:3000", "http://localhost:4200"],
|
||||||
"credentials": true
|
"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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,21 @@
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
import { ConfigManager, createAppConfig } from '@stock-bot/config';
|
import { ConfigManager, createAppConfig } from '@stock-bot/config';
|
||||||
import { getLogger } from '@stock-bot/logger';
|
import { getLogger } from '@stock-bot/logger';
|
||||||
import { stockAppSchema, type StockAppConfig } from './schemas';
|
import { stockAppSchema, type StockAppConfig } from './schemas';
|
||||||
|
|
||||||
let configInstance: ConfigManager<StockAppConfig> | null = null;
|
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
|
* Initialize the stock application configuration
|
||||||
* @param serviceName - Optional service name to override port configuration
|
* @param serviceName - Optional service name to override port configuration
|
||||||
*/
|
*/
|
||||||
export function initializeStockConfig(
|
export function initializeStockConfig(
|
||||||
serviceName?: 'dataIngestion' | 'dataPipeline' | 'webApi'
|
serviceName?: 'dataIngestion' | 'dataPipeline' | 'webApi' | 'orchestrator'
|
||||||
): StockAppConfig {
|
): StockAppConfig {
|
||||||
try {
|
try {
|
||||||
if (!configInstance) {
|
if (!configInstance) {
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,27 @@ export const stockAppSchema = baseAppSchema.extend({
|
||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
.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(),
|
.optional(),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,251 +1,21 @@
|
||||||
const { existsSync, readFileSync } = require('fs')
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
const { existsSync } = require('fs')
|
||||||
const { join } = require('path')
|
const { join } = require('path')
|
||||||
|
|
||||||
const { platform, arch } = process
|
|
||||||
|
|
||||||
let nativeBinding = null
|
let nativeBinding = null
|
||||||
let localFileExisted = false
|
|
||||||
let loadError = null
|
|
||||||
|
|
||||||
function isMusl() {
|
// Try to load the native binding
|
||||||
// For Node 10
|
try {
|
||||||
if (!process.report || typeof process.report.getReport !== 'function') {
|
if (existsSync(join(__dirname, 'index.node'))) {
|
||||||
try {
|
nativeBinding = require('./index.node')
|
||||||
const lddPath = require('child_process').execSync('which ldd 2>/dev/null', { encoding: 'utf8' })
|
|
||||||
return readFileSync(lddPath, 'utf8').includes('musl')
|
|
||||||
} catch (e) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
const { glibcVersionRuntime } = process.report.getReport().header
|
throw new Error('index.node not found')
|
||||||
return !glibcVersionRuntime
|
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(`Failed to load native binding: ${e.message}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (platform) {
|
// Export all bindings
|
||||||
case 'android':
|
module.exports = nativeBinding
|
||||||
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
|
|
||||||
29
apps/stock/core/index.mjs
Normal file
29
apps/stock/core/index.mjs
Normal 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.
|
|
@ -1,11 +1,20 @@
|
||||||
{
|
{
|
||||||
"name": "@stock-bot/core",
|
"name": "@stock-bot/core",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"main": "index.js",
|
"type": "module",
|
||||||
|
"main": "index.mjs",
|
||||||
"types": "index.d.ts",
|
"types": "index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./index.mjs",
|
||||||
|
"require": "./index.js",
|
||||||
|
"types": "./index.d.ts"
|
||||||
|
}
|
||||||
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"index.d.ts",
|
"index.d.ts",
|
||||||
"index.js",
|
"index.js",
|
||||||
|
"index.mjs",
|
||||||
"index.node"
|
"index.node"
|
||||||
],
|
],
|
||||||
"napi": {
|
"napi": {
|
||||||
|
|
|
||||||
|
|
@ -224,9 +224,8 @@ impl TradingEngine {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[napi]
|
#[napi]
|
||||||
pub fn set_microstructure(&self, _symbol: String, microstructure_json: String) -> Result<()> {
|
pub fn set_microstructure(&self, symbol: String, microstructure_js: JsObject) -> Result<()> {
|
||||||
let _microstructure: MarketMicrostructure = serde_json::from_str(µstructure_json)
|
let microstructure = parse_microstructure(microstructure_js)?;
|
||||||
.map_err(|e| Error::from_reason(format!("Failed to parse microstructure: {}", e)))?;
|
|
||||||
|
|
||||||
let _core = self.core.lock();
|
let _core = self.core.lock();
|
||||||
// Store microstructure for use in fill simulation
|
// Store microstructure for use in fill simulation
|
||||||
|
|
@ -236,10 +235,43 @@ impl TradingEngine {
|
||||||
|
|
||||||
#[napi]
|
#[napi]
|
||||||
pub fn load_historical_data(&self, data_json: String) -> Result<()> {
|
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)))?;
|
.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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -324,3 +356,19 @@ fn parse_risk_limits(limits_js: JsObject) -> Result<RiskLimits> {
|
||||||
max_symbol_exposure: limits_js.get_named_property("maxSymbolExposure")?,
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ use crate::{MarketDataSource, MarketUpdate};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
use super::mock_data_generator::MockDataGenerator;
|
||||||
|
|
||||||
// Historical data source for backtesting
|
// Historical data source for backtesting
|
||||||
pub struct HistoricalDataSource {
|
pub struct HistoricalDataSource {
|
||||||
|
|
@ -24,6 +25,19 @@ impl HistoricalDataSource {
|
||||||
queue.extend(data);
|
queue.extend(data);
|
||||||
*self.current_position.lock() = 0;
|
*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]
|
#[async_trait::async_trait]
|
||||||
|
|
|
||||||
229
apps/stock/core/src/core/mock_data_generator.rs
Normal file
229
apps/stock/core/src/core/mock_data_generator.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ pub mod time_providers;
|
||||||
pub mod market_data_sources;
|
pub mod market_data_sources;
|
||||||
pub mod execution_handlers;
|
pub mod execution_handlers;
|
||||||
pub mod market_microstructure;
|
pub mod market_microstructure;
|
||||||
|
pub mod mock_data_generator;
|
||||||
|
|
||||||
use crate::{MarketDataSource, ExecutionHandler, TimeProvider, TradingMode};
|
use crate::{MarketDataSource, ExecutionHandler, TimeProvider, TradingMode};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,13 +19,15 @@
|
||||||
"@stock-bot/questdb": "*",
|
"@stock-bot/questdb": "*",
|
||||||
"@stock-bot/queue": "*",
|
"@stock-bot/queue": "*",
|
||||||
"@stock-bot/shutdown": "*",
|
"@stock-bot/shutdown": "*",
|
||||||
|
"@stock-bot/stock-config": "*",
|
||||||
"@stock-bot/utils": "*",
|
"@stock-bot/utils": "*",
|
||||||
"hono": "^4.0.0",
|
"hono": "^4.0.0",
|
||||||
"socket.io": "^4.7.2",
|
"socket.io": "^4.7.2",
|
||||||
"socket.io-client": "^4.7.2",
|
"socket.io-client": "^4.7.2",
|
||||||
"zod": "^3.22.0",
|
"zod": "^3.22.0",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
"axios": "^1.6.0"
|
"axios": "^1.6.0",
|
||||||
|
"simple-statistics": "^7.8.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.0.0",
|
"@types/node": "^20.0.0",
|
||||||
|
|
|
||||||
|
|
@ -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';
|
import * as stats from 'simple-statistics';
|
||||||
|
|
||||||
export interface Trade {
|
export interface Trade {
|
||||||
|
|
@ -168,6 +170,19 @@ export class PerformanceAnalyzer {
|
||||||
analyzeDrawdowns(): DrawdownAnalysis {
|
analyzeDrawdowns(): DrawdownAnalysis {
|
||||||
const drawdowns: number[] = [];
|
const drawdowns: number[] = [];
|
||||||
const underwaterCurve: Array<{ date: Date; drawdown: 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 peak = this.equityCurve[0].value;
|
||||||
let maxDrawdown = 0;
|
let maxDrawdown = 0;
|
||||||
let currentDrawdownStart: Date | null = null;
|
let currentDrawdownStart: Date | null = null;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import { Hono } from 'hono';
|
import { Hono } from 'hono';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { logger } from '@stock-bot/logger';
|
import { IServiceContainer } from '@stock-bot/di';
|
||||||
import { AnalyticsService } from '../../services/AnalyticsService';
|
import { AnalyticsService } from '../../services/AnalyticsService';
|
||||||
import { container } from '../../container';
|
|
||||||
|
|
||||||
const DateRangeSchema = z.object({
|
const DateRangeSchema = z.object({
|
||||||
startDate: z.string().datetime(),
|
startDate: z.string().datetime(),
|
||||||
|
|
@ -20,9 +19,9 @@ const OptimizationRequestSchema = z.object({
|
||||||
}).optional()
|
}).optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export function createAnalyticsRoutes(): Hono {
|
export function createAnalyticsRoutes(container: IServiceContainer): Hono {
|
||||||
const app = new Hono();
|
const app = new Hono();
|
||||||
const analyticsService = container.get('AnalyticsService') as AnalyticsService;
|
const analyticsService = container.custom?.AnalyticsService as AnalyticsService;
|
||||||
|
|
||||||
// Get performance metrics
|
// Get performance metrics
|
||||||
app.get('/performance/:portfolioId', async (c) => {
|
app.get('/performance/:portfolioId', async (c) => {
|
||||||
|
|
@ -50,7 +49,7 @@ export function createAnalyticsRoutes(): Hono {
|
||||||
}, 400);
|
}, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.error('Error getting performance metrics:', error);
|
container.logger.error('Error getting performance metrics:', error);
|
||||||
return c.json({
|
return c.json({
|
||||||
error: error instanceof Error ? error.message : 'Failed to get performance metrics'
|
error: error instanceof Error ? error.message : 'Failed to get performance metrics'
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
@ -77,7 +76,7 @@ export function createAnalyticsRoutes(): Hono {
|
||||||
}, 400);
|
}, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.error('Error optimizing portfolio:', error);
|
container.logger.error('Error optimizing portfolio:', error);
|
||||||
return c.json({
|
return c.json({
|
||||||
error: error instanceof Error ? error.message : 'Failed to optimize portfolio'
|
error: error instanceof Error ? error.message : 'Failed to optimize portfolio'
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
@ -92,7 +91,7 @@ export function createAnalyticsRoutes(): Hono {
|
||||||
|
|
||||||
return c.json(metrics);
|
return c.json(metrics);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error getting risk metrics:', error);
|
container.logger.error('Error getting risk metrics:', error);
|
||||||
return c.json({
|
return c.json({
|
||||||
error: error instanceof Error ? error.message : 'Failed to get risk metrics'
|
error: error instanceof Error ? error.message : 'Failed to get risk metrics'
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
@ -109,7 +108,7 @@ export function createAnalyticsRoutes(): Hono {
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString()
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error detecting market regime:', error);
|
container.logger.error('Error detecting market regime:', error);
|
||||||
return c.json({
|
return c.json({
|
||||||
error: error instanceof Error ? error.message : 'Failed to detect market regime'
|
error: error instanceof Error ? error.message : 'Failed to detect market regime'
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
@ -138,7 +137,7 @@ export function createAnalyticsRoutes(): Hono {
|
||||||
}, 400);
|
}, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.error('Error calculating correlation:', error);
|
container.logger.error('Error calculating correlation:', error);
|
||||||
return c.json({
|
return c.json({
|
||||||
error: error instanceof Error ? error.message : 'Failed to calculate correlation'
|
error: error instanceof Error ? error.message : 'Failed to calculate correlation'
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
@ -169,7 +168,7 @@ export function createAnalyticsRoutes(): Hono {
|
||||||
}, 400);
|
}, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.error('Error making prediction:', error);
|
container.logger.error('Error making prediction:', error);
|
||||||
return c.json({
|
return c.json({
|
||||||
error: error instanceof Error ? error.message : 'Failed to make prediction'
|
error: error instanceof Error ? error.message : 'Failed to make prediction'
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,48 @@
|
||||||
import { Hono } from 'hono';
|
import { Hono } from 'hono';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { logger } from '@stock-bot/logger';
|
import { IServiceContainer } from '@stock-bot/di';
|
||||||
import { BacktestConfigSchema } from '../../types';
|
import { BacktestConfigSchema } from '../../types';
|
||||||
import { BacktestEngine } from '../../backtest/BacktestEngine';
|
import { BacktestEngine } from '../../backtest/BacktestEngine';
|
||||||
import { ModeManager } from '../../core/ModeManager';
|
import { ModeManager } from '../../core/ModeManager';
|
||||||
import { container } from '../../container';
|
|
||||||
|
|
||||||
const BacktestIdSchema = z.object({
|
const BacktestIdSchema = z.object({
|
||||||
backtestId: z.string()
|
backtestId: z.string()
|
||||||
});
|
});
|
||||||
|
|
||||||
export function createBacktestRoutes(): Hono {
|
export function createBacktestRoutes(container: IServiceContainer): Hono {
|
||||||
const app = new Hono();
|
const app = new Hono();
|
||||||
const backtestEngine = container.get('BacktestEngine') as BacktestEngine;
|
const backtestEngine = container.custom?.BacktestEngine as BacktestEngine;
|
||||||
const modeManager = container.get('ModeManager') as ModeManager;
|
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) => {
|
app.post('/run', async (c) => {
|
||||||
try {
|
try {
|
||||||
const body = await c.req.json();
|
const body = await c.req.json();
|
||||||
|
|
@ -36,7 +63,7 @@ export function createBacktestRoutes(): Hono {
|
||||||
}, 400);
|
}, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.error('Error running backtest:', error);
|
container.logger.error('Error running backtest:', error);
|
||||||
return c.json({
|
return c.json({
|
||||||
error: error instanceof Error ? error.message : 'Failed to run backtest'
|
error: error instanceof Error ? error.message : 'Failed to run backtest'
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
@ -53,7 +80,7 @@ export function createBacktestRoutes(): Hono {
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString()
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error stopping backtest:', error);
|
container.logger.error('Error stopping backtest:', error);
|
||||||
return c.json({
|
return c.json({
|
||||||
error: error instanceof Error ? error.message : 'Failed to stop backtest'
|
error: error instanceof Error ? error.message : 'Failed to stop backtest'
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
@ -72,7 +99,7 @@ export function createBacktestRoutes(): Hono {
|
||||||
currentTime: new Date().toISOString()
|
currentTime: new Date().toISOString()
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error getting backtest progress:', error);
|
container.logger.error('Error getting backtest progress:', error);
|
||||||
return c.json({
|
return c.json({
|
||||||
error: error instanceof Error ? error.message : 'Failed to get progress'
|
error: error instanceof Error ? error.message : 'Failed to get progress'
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,16 @@
|
||||||
import { Hono } from 'hono';
|
import { Hono } from 'hono';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { logger } from '@stock-bot/logger';
|
import { IServiceContainer } from '@stock-bot/di';
|
||||||
import { OrderRequestSchema } from '../../types';
|
import { OrderRequestSchema } from '../../types';
|
||||||
import { ExecutionService } from '../../services/ExecutionService';
|
import { ExecutionService } from '../../services/ExecutionService';
|
||||||
import { container } from '../../container';
|
|
||||||
|
|
||||||
const OrderIdSchema = z.object({
|
const OrderIdSchema = z.object({
|
||||||
orderId: z.string()
|
orderId: z.string()
|
||||||
});
|
});
|
||||||
|
|
||||||
export function createOrderRoutes(): Hono {
|
export function createOrderRoutes(container: IServiceContainer): Hono {
|
||||||
const app = new Hono();
|
const app = new Hono();
|
||||||
const executionService = container.get('ExecutionService') as ExecutionService;
|
const executionService = container.custom?.ExecutionService as ExecutionService;
|
||||||
|
|
||||||
// Submit new order
|
// Submit new order
|
||||||
app.post('/', async (c) => {
|
app.post('/', async (c) => {
|
||||||
|
|
@ -30,7 +29,7 @@ export function createOrderRoutes(): Hono {
|
||||||
}, 400);
|
}, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.error('Error submitting order:', error);
|
container.logger.error('Error submitting order:', error);
|
||||||
return c.json({
|
return c.json({
|
||||||
error: error instanceof Error ? error.message : 'Failed to submit order'
|
error: error instanceof Error ? error.message : 'Failed to submit order'
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
@ -50,7 +49,7 @@ export function createOrderRoutes(): Hono {
|
||||||
return c.json({ error: 'Order not found or already filled' }, 404);
|
return c.json({ error: 'Order not found or already filled' }, 404);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error cancelling order:', error);
|
container.logger.error('Error cancelling order:', error);
|
||||||
return c.json({
|
return c.json({
|
||||||
error: error instanceof Error ? error.message : 'Failed to cancel order'
|
error: error instanceof Error ? error.message : 'Failed to cancel order'
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
@ -70,7 +69,7 @@ export function createOrderRoutes(): Hono {
|
||||||
return c.json({ error: 'Order not found' }, 404);
|
return c.json({ error: 'Order not found' }, 404);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error getting order status:', error);
|
container.logger.error('Error getting order status:', error);
|
||||||
return c.json({
|
return c.json({
|
||||||
error: error instanceof Error ? error.message : 'Failed to get order status'
|
error: error instanceof Error ? error.message : 'Failed to get order status'
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
@ -101,7 +100,7 @@ export function createOrderRoutes(): Hono {
|
||||||
}, 400);
|
}, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.error('Error submitting batch orders:', error);
|
container.logger.error('Error submitting batch orders:', error);
|
||||||
return c.json({
|
return c.json({
|
||||||
error: error instanceof Error ? error.message : 'Failed to submit batch orders'
|
error: error instanceof Error ? error.message : 'Failed to submit batch orders'
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,15 @@
|
||||||
import { Hono } from 'hono';
|
import { Hono } from 'hono';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { logger } from '@stock-bot/logger';
|
import { IServiceContainer } from '@stock-bot/di';
|
||||||
import { ModeManager } from '../../core/ModeManager';
|
import { ModeManager } from '../../core/ModeManager';
|
||||||
import { container } from '../../container';
|
|
||||||
|
|
||||||
const SymbolSchema = z.object({
|
const SymbolSchema = z.object({
|
||||||
symbol: z.string()
|
symbol: z.string()
|
||||||
});
|
});
|
||||||
|
|
||||||
export function createPositionRoutes(): Hono {
|
export function createPositionRoutes(container: IServiceContainer): Hono {
|
||||||
const app = new Hono();
|
const app = new Hono();
|
||||||
const modeManager = container.get('ModeManager') as ModeManager;
|
const modeManager = container.custom?.ModeManager as ModeManager;
|
||||||
|
|
||||||
// Get all positions
|
// Get all positions
|
||||||
app.get('/', async (c) => {
|
app.get('/', async (c) => {
|
||||||
|
|
@ -23,7 +22,7 @@ export function createPositionRoutes(): Hono {
|
||||||
positions
|
positions
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error getting positions:', error);
|
container.logger.error('Error getting positions:', error);
|
||||||
return c.json({
|
return c.json({
|
||||||
error: error instanceof Error ? error.message : 'Failed to get positions'
|
error: error instanceof Error ? error.message : 'Failed to get positions'
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
@ -41,7 +40,7 @@ export function createPositionRoutes(): Hono {
|
||||||
positions
|
positions
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error getting open positions:', error);
|
container.logger.error('Error getting open positions:', error);
|
||||||
return c.json({
|
return c.json({
|
||||||
error: error instanceof Error ? error.message : 'Failed to get open positions'
|
error: error instanceof Error ? error.message : 'Failed to get open positions'
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
@ -69,7 +68,7 @@ export function createPositionRoutes(): Hono {
|
||||||
}, 404);
|
}, 404);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error getting position:', error);
|
container.logger.error('Error getting position:', error);
|
||||||
return c.json({
|
return c.json({
|
||||||
error: error instanceof Error ? error.message : 'Failed to get position'
|
error: error instanceof Error ? error.message : 'Failed to get position'
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
@ -92,7 +91,7 @@ export function createPositionRoutes(): Hono {
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString()
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error getting P&L:', error);
|
container.logger.error('Error getting P&L:', error);
|
||||||
return c.json({
|
return c.json({
|
||||||
error: error instanceof Error ? error.message : 'Failed to get P&L'
|
error: error instanceof Error ? error.message : 'Failed to get P&L'
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
@ -111,7 +110,7 @@ export function createPositionRoutes(): Hono {
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString()
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error getting risk metrics:', error);
|
container.logger.error('Error getting risk metrics:', error);
|
||||||
return c.json({
|
return c.json({
|
||||||
error: error instanceof Error ? error.message : 'Failed to get risk metrics'
|
error: error instanceof Error ? error.message : 'Failed to get risk metrics'
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import { Server as SocketIOServer, Socket } from 'socket.io';
|
import { Server as SocketIOServer, Socket } from 'socket.io';
|
||||||
import { logger } from '@stock-bot/logger';
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { IServiceContainer } from '@stock-bot/di';
|
||||||
import { MarketDataService } from '../../services/MarketDataService';
|
import { MarketDataService } from '../../services/MarketDataService';
|
||||||
import { ExecutionService } from '../../services/ExecutionService';
|
import { ExecutionService } from '../../services/ExecutionService';
|
||||||
import { ModeManager } from '../../core/ModeManager';
|
import { ModeManager } from '../../core/ModeManager';
|
||||||
import { Container } from '@stock-bot/di';
|
|
||||||
|
|
||||||
const SubscribeSchema = z.object({
|
const SubscribeSchema = z.object({
|
||||||
symbols: z.array(z.string()),
|
symbols: z.array(z.string()),
|
||||||
|
|
@ -15,16 +14,16 @@ const UnsubscribeSchema = z.object({
|
||||||
symbols: z.array(z.string())
|
symbols: z.array(z.string())
|
||||||
});
|
});
|
||||||
|
|
||||||
export function setupWebSocketHandlers(io: SocketIOServer, container: Container): void {
|
export function setupWebSocketHandlers(io: SocketIOServer, container: IServiceContainer): void {
|
||||||
const marketDataService = container.get('MarketDataService') as MarketDataService;
|
const marketDataService = container.custom?.MarketDataService as MarketDataService;
|
||||||
const executionService = container.get('ExecutionService') as ExecutionService;
|
const executionService = container.custom?.ExecutionService as ExecutionService;
|
||||||
const modeManager = container.get('ModeManager') as ModeManager;
|
const modeManager = container.custom?.ModeManager as ModeManager;
|
||||||
|
|
||||||
// Track client subscriptions
|
// Track client subscriptions
|
||||||
const clientSubscriptions = new Map<string, Set<string>>();
|
const clientSubscriptions = new Map<string, Set<string>>();
|
||||||
|
|
||||||
io.on('connection', (socket: Socket) => {
|
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());
|
clientSubscriptions.set(socket.id, new Set());
|
||||||
|
|
||||||
// Send initial connection info
|
// Send initial connection info
|
||||||
|
|
@ -44,13 +43,13 @@ export function setupWebSocketHandlers(io: SocketIOServer, container: Container)
|
||||||
subscriptions.add(symbol);
|
subscriptions.add(symbol);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(`Client ${socket.id} subscribed to: ${symbols.join(', ')}`);
|
container.logger.debug(`Client ${socket.id} subscribed to: ${symbols.join(', ')}`);
|
||||||
|
|
||||||
if (callback) {
|
if (callback) {
|
||||||
callback({ success: true, symbols });
|
callback({ success: true, symbols });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Subscription error:', error);
|
container.logger.error('Subscription error:', error);
|
||||||
if (callback) {
|
if (callback) {
|
||||||
callback({
|
callback({
|
||||||
success: false,
|
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) {
|
if (callback) {
|
||||||
callback({ success: true, symbols });
|
callback({ success: true, symbols });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Unsubscribe error:', error);
|
container.logger.error('Unsubscribe error:', error);
|
||||||
if (callback) {
|
if (callback) {
|
||||||
callback({
|
callback({
|
||||||
success: false,
|
success: false,
|
||||||
|
|
@ -107,7 +106,7 @@ export function setupWebSocketHandlers(io: SocketIOServer, container: Container)
|
||||||
callback({ success: true, result });
|
callback({ success: true, result });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Order submission error:', error);
|
container.logger.error('Order submission error:', error);
|
||||||
if (callback) {
|
if (callback) {
|
||||||
callback({
|
callback({
|
||||||
success: false,
|
success: false,
|
||||||
|
|
@ -127,7 +126,7 @@ export function setupWebSocketHandlers(io: SocketIOServer, container: Container)
|
||||||
callback({ success: true, positions });
|
callback({ success: true, positions });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error getting positions:', error);
|
container.logger.error('Error getting positions:', error);
|
||||||
if (callback) {
|
if (callback) {
|
||||||
callback({
|
callback({
|
||||||
success: false,
|
success: false,
|
||||||
|
|
@ -139,7 +138,7 @@ export function setupWebSocketHandlers(io: SocketIOServer, container: Container)
|
||||||
|
|
||||||
// Handle disconnection
|
// Handle disconnection
|
||||||
socket.on('disconnect', async () => {
|
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
|
// Unsubscribe from all symbols for this client
|
||||||
const subscriptions = clientSubscriptions.get(socket.id);
|
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');
|
||||||
}
|
}
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
import { logger } from '@stock-bot/logger';
|
|
||||||
import { EventEmitter } from 'events';
|
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 { StorageService } from '../services/StorageService';
|
||||||
import { StrategyManager } from '../strategies/StrategyManager';
|
import { StrategyManager } from '../strategies/StrategyManager';
|
||||||
import { TradingEngine } from '../../core';
|
import { BacktestConfigSchema, MarketData, MarketMicrostructure, PerformanceMetrics } from '../types';
|
||||||
import { DataManager } from '../data/DataManager';
|
|
||||||
import { MarketSimulator } from './MarketSimulator';
|
import { MarketSimulator } from './MarketSimulator';
|
||||||
import { PerformanceAnalyzer } from '../analytics/PerformanceAnalyzer';
|
|
||||||
|
|
||||||
interface BacktestEvent {
|
interface BacktestEvent {
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
|
|
@ -35,12 +34,16 @@ export class BacktestEngine extends EventEmitter {
|
||||||
private marketSimulator: MarketSimulator;
|
private marketSimulator: MarketSimulator;
|
||||||
private performanceAnalyzer: PerformanceAnalyzer;
|
private performanceAnalyzer: PerformanceAnalyzer;
|
||||||
private microstructures: Map<string, MarketMicrostructure> = new Map();
|
private microstructures: Map<string, MarketMicrostructure> = new Map();
|
||||||
|
private container: IServiceContainer;
|
||||||
|
private initialCapital: number = 100000;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
container: IServiceContainer,
|
||||||
private storageService: StorageService,
|
private storageService: StorageService,
|
||||||
private strategyManager: StrategyManager
|
private strategyManager: StrategyManager
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
this.container = container;
|
||||||
this.dataManager = new DataManager(storageService);
|
this.dataManager = new DataManager(storageService);
|
||||||
this.marketSimulator = new MarketSimulator({
|
this.marketSimulator = new MarketSimulator({
|
||||||
useHistoricalSpreads: true,
|
useHistoricalSpreads: true,
|
||||||
|
|
@ -55,11 +58,24 @@ export class BacktestEngine extends EventEmitter {
|
||||||
// Validate config
|
// Validate config
|
||||||
const validatedConfig = BacktestConfigSchema.parse(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
|
// Reset state
|
||||||
this.reset();
|
this.reset();
|
||||||
this.isRunning = true;
|
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
|
// Generate backtest ID
|
||||||
const backtestId = `backtest_${Date.now()}`;
|
const backtestId = `backtest_${Date.now()}`;
|
||||||
|
|
@ -84,7 +100,7 @@ export class BacktestEngine extends EventEmitter {
|
||||||
});
|
});
|
||||||
marketData.sort((a, b) => a.data.timestamp - b.data.timestamp);
|
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
|
// Initialize strategies
|
||||||
await this.strategyManager.initializeStrategies(validatedConfig.strategies || []);
|
await this.strategyManager.initializeStrategies(validatedConfig.strategies || []);
|
||||||
|
|
@ -110,17 +126,18 @@ export class BacktestEngine extends EventEmitter {
|
||||||
equityCurve: this.equityCurve,
|
equityCurve: this.equityCurve,
|
||||||
drawdown: this.calculateDrawdown(),
|
drawdown: this.calculateDrawdown(),
|
||||||
dailyReturns: this.calculateDailyReturns(),
|
dailyReturns: this.calculateDailyReturns(),
|
||||||
finalPositions
|
finalPositions,
|
||||||
|
ohlcData: this.getOHLCData(marketData, validatedConfig.symbols)
|
||||||
};
|
};
|
||||||
|
|
||||||
await this.storeResults(result);
|
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;
|
return result;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Backtest failed:', error);
|
this.container.logger.error('Backtest failed:', error);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
this.isRunning = false;
|
this.isRunning = false;
|
||||||
|
|
@ -133,6 +150,7 @@ export class BacktestEngine extends EventEmitter {
|
||||||
const startDate = new Date(config.startDate);
|
const startDate = new Date(config.startDate);
|
||||||
const endDate = new Date(config.endDate);
|
const endDate = new Date(config.endDate);
|
||||||
|
|
||||||
|
try {
|
||||||
for (const symbol of config.symbols) {
|
for (const symbol of config.symbols) {
|
||||||
const bars = await this.storageService.getHistoricalBars(
|
const bars = await this.storageService.getHistoricalBars(
|
||||||
symbol,
|
symbol,
|
||||||
|
|
@ -141,6 +159,30 @@ export class BacktestEngine extends EventEmitter {
|
||||||
config.dataFrequency
|
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
|
// Convert to MarketData format
|
||||||
bars.forEach(bar => {
|
bars.forEach(bar => {
|
||||||
data.push({
|
data.push({
|
||||||
|
|
@ -158,6 +200,16 @@ export class BacktestEngine extends EventEmitter {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
} 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
|
// Sort by timestamp
|
||||||
data.sort((a, b) => {
|
data.sort((a, b) => {
|
||||||
|
|
@ -169,6 +221,48 @@ export class BacktestEngine extends EventEmitter {
|
||||||
return data;
|
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 {
|
private populateEventQueue(marketData: MarketData[]): void {
|
||||||
// Convert market data to events
|
// Convert market data to events
|
||||||
marketData.forEach(data => {
|
marketData.forEach(data => {
|
||||||
|
|
@ -234,7 +328,7 @@ export class BacktestEngine extends EventEmitter {
|
||||||
|
|
||||||
private async processMarketData(data: MarketData): Promise<void> {
|
private async processMarketData(data: MarketData): Promise<void> {
|
||||||
const tradingEngine = this.strategyManager.getTradingEngine();
|
const tradingEngine = this.strategyManager.getTradingEngine();
|
||||||
if (!tradingEngine) return;
|
if (!tradingEngine) {return;}
|
||||||
|
|
||||||
// Process through market simulator for realistic orderbook
|
// Process through market simulator for realistic orderbook
|
||||||
const orderbook = this.marketSimulator.processMarketData(data);
|
const orderbook = this.marketSimulator.processMarketData(data);
|
||||||
|
|
@ -300,7 +394,7 @@ export class BacktestEngine extends EventEmitter {
|
||||||
// Track performance
|
// Track performance
|
||||||
this.performanceAnalyzer.addEquityPoint(
|
this.performanceAnalyzer.addEquityPoint(
|
||||||
new Date(this.currentTime),
|
new Date(this.currentTime),
|
||||||
this.getPortfolioValue()
|
await this.getPortfolioValue()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -321,12 +415,7 @@ export class BacktestEngine extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateEquityCurve(): Promise<void> {
|
private async updateEquityCurve(): Promise<void> {
|
||||||
const tradingEngine = this.strategyManager.getTradingEngine();
|
const totalEquity = await this.getPortfolioValue();
|
||||||
if (!tradingEngine) return;
|
|
||||||
|
|
||||||
// Get current P&L
|
|
||||||
const [realized, unrealized] = tradingEngine.getTotalPnl();
|
|
||||||
const totalEquity = 100000 + realized + unrealized; // Assuming 100k starting capital
|
|
||||||
|
|
||||||
this.equityCurve.push({
|
this.equityCurve.push({
|
||||||
timestamp: this.currentTime,
|
timestamp: this.currentTime,
|
||||||
|
|
@ -334,6 +423,17 @@ export class BacktestEngine extends EventEmitter {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
private calculatePerformance(): PerformanceMetrics {
|
||||||
// Use sophisticated performance analyzer
|
// Use sophisticated performance analyzer
|
||||||
this.trades.forEach(trade => {
|
this.trades.forEach(trade => {
|
||||||
|
|
@ -366,49 +466,6 @@ export class BacktestEngine extends EventEmitter {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
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 }[] {
|
private calculateDrawdown(): { timestamp: number; value: number }[] {
|
||||||
const drawdowns: { timestamp: number; value: number }[] = [];
|
const drawdowns: { timestamp: number; value: number }[] = [];
|
||||||
let peak = this.equityCurve[0]?.value || 0;
|
let peak = this.equityCurve[0]?.value || 0;
|
||||||
|
|
@ -451,7 +508,7 @@ export class BacktestEngine extends EventEmitter {
|
||||||
|
|
||||||
private async getFinalPositions(): Promise<any[]> {
|
private async getFinalPositions(): Promise<any[]> {
|
||||||
const tradingEngine = this.strategyManager.getTradingEngine();
|
const tradingEngine = this.strategyManager.getTradingEngine();
|
||||||
if (!tradingEngine) return [];
|
if (!tradingEngine) {return [];}
|
||||||
|
|
||||||
const positions = JSON.parse(tradingEngine.getOpenPositions());
|
const positions = JSON.parse(tradingEngine.getOpenPositions());
|
||||||
return positions;
|
return positions;
|
||||||
|
|
@ -465,7 +522,7 @@ export class BacktestEngine extends EventEmitter {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Could also store detailed results in a separate table or file
|
// 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 {
|
private reset(): void {
|
||||||
|
|
@ -521,7 +578,7 @@ export class BacktestEngine extends EventEmitter {
|
||||||
|
|
||||||
private getPortfolioValue(): number {
|
private getPortfolioValue(): number {
|
||||||
const tradingEngine = this.strategyManager.getTradingEngine();
|
const tradingEngine = this.strategyManager.getTradingEngine();
|
||||||
if (!tradingEngine) return 100000; // Default initial capital
|
if (!tradingEngine) {return 100000;} // Default initial capital
|
||||||
|
|
||||||
const [realized, unrealized] = tradingEngine.getTotalPnl();
|
const [realized, unrealized] = tradingEngine.getTotalPnl();
|
||||||
return 100000 + realized + unrealized;
|
return 100000 + realized + unrealized;
|
||||||
|
|
@ -529,7 +586,7 @@ export class BacktestEngine extends EventEmitter {
|
||||||
|
|
||||||
async stopBacktest(): Promise<void> {
|
async stopBacktest(): Promise<void> {
|
||||||
this.isRunning = false;
|
this.isRunning = false;
|
||||||
logger.info('Backtest stop requested');
|
this.container.logger.info('Backtest stop requested');
|
||||||
}
|
}
|
||||||
|
|
||||||
async exportResults(format: 'json' | 'csv' | 'html' = 'json'): Promise<string> {
|
async exportResults(format: 'json' | 'csv' | 'html' = 'json'): Promise<string> {
|
||||||
|
|
@ -631,4 +688,25 @@ export class BacktestEngine extends EventEmitter {
|
||||||
</html>
|
</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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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 { MarketData, Quote, Trade, Bar, OrderBookSnapshot, PriceLevel } from '../types';
|
||||||
import { MarketMicrostructure } from '../types/MarketMicrostructure';
|
import { MarketMicrostructure } from '../types/MarketMicrostructure';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { Container } from '@stock-bot/di';
|
import { IServiceContainer } from '@stock-bot/di';
|
||||||
import { logger } from '@stock-bot/logger';
|
|
||||||
import { ModeManager } from './core/ModeManager';
|
import { ModeManager } from './core/ModeManager';
|
||||||
import { MarketDataService } from './services/MarketDataService';
|
import { MarketDataService } from './services/MarketDataService';
|
||||||
import { ExecutionService } from './services/ExecutionService';
|
import { ExecutionService } from './services/ExecutionService';
|
||||||
|
|
@ -9,39 +8,94 @@ import { StrategyManager } from './strategies/StrategyManager';
|
||||||
import { BacktestEngine } from './backtest/BacktestEngine';
|
import { BacktestEngine } from './backtest/BacktestEngine';
|
||||||
import { PaperTradingManager } from './paper/PaperTradingManager';
|
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: {}
|
||||||
|
};
|
||||||
|
|
||||||
// Register core services
|
// Create storage service first as it's needed by other services
|
||||||
container.singleton('Logger', () => logger);
|
const storageService = new StorageService(
|
||||||
|
services,
|
||||||
|
services.mongodb!,
|
||||||
|
services.postgres!,
|
||||||
|
services.questdb || null
|
||||||
|
);
|
||||||
|
|
||||||
container.singleton('ModeManager', () => new ModeManager(
|
// Create other services
|
||||||
container.get('MarketDataService'),
|
const marketDataService = new MarketDataService(services);
|
||||||
container.get('ExecutionService'),
|
const executionService = new ExecutionService(services, storageService);
|
||||||
container.get('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
|
||||||
|
);
|
||||||
|
|
||||||
container.singleton('MarketDataService', () => new MarketDataService());
|
// Store custom services
|
||||||
|
services.custom = {
|
||||||
|
StorageService: storageService,
|
||||||
|
MarketDataService: marketDataService,
|
||||||
|
ExecutionService: executionService,
|
||||||
|
AnalyticsService: analyticsService,
|
||||||
|
StrategyManager: strategyManager,
|
||||||
|
BacktestEngine: backtestEngine,
|
||||||
|
PaperTradingManager: paperTradingManager,
|
||||||
|
ModeManager: modeManager
|
||||||
|
};
|
||||||
|
|
||||||
container.singleton('ExecutionService', () => new ExecutionService(
|
// Register services in the Awilix container for resolution
|
||||||
container.get('ModeManager')
|
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 }
|
||||||
|
});
|
||||||
|
|
||||||
container.singleton('AnalyticsService', () => new AnalyticsService());
|
// Update the serviceContainer to include our custom services
|
||||||
|
const serviceContainer = container.cradle.serviceContainer;
|
||||||
|
if (serviceContainer && serviceContainer.custom) {
|
||||||
|
Object.assign(serviceContainer.custom, services.custom);
|
||||||
|
}
|
||||||
|
|
||||||
container.singleton('StorageService', () => new StorageService());
|
// Setup event listeners after all services are registered
|
||||||
|
strategyManager.setupEventListeners();
|
||||||
|
|
||||||
container.singleton('StrategyManager', () => new StrategyManager(
|
// Initialize mode manager with default paper trading mode
|
||||||
container.get('ModeManager'),
|
await modeManager.initializeMode({
|
||||||
container.get('MarketDataService'),
|
mode: 'paper',
|
||||||
container.get('ExecutionService')
|
startingCapital: 100000
|
||||||
));
|
});
|
||||||
|
}
|
||||||
|
|
||||||
container.singleton('BacktestEngine', () => new BacktestEngine(
|
// For backward compatibility, export a container getter
|
||||||
container.get('StorageService'),
|
export function getContainer(): IServiceContainer {
|
||||||
container.get('StrategyManager')
|
throw new Error('Container should be accessed through ServiceApplication. Update your code to use dependency injection.');
|
||||||
));
|
}
|
||||||
|
|
||||||
container.singleton('PaperTradingManager', () => new PaperTradingManager(
|
|
||||||
container.get('ExecutionService')
|
|
||||||
));
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { logger } from '@stock-bot/logger';
|
import { TradingEngine } from '@stock-bot/core';
|
||||||
import { TradingEngine } from '../../core';
|
import { IServiceContainer } from '@stock-bot/di';
|
||||||
import { TradingMode, ModeConfig, BacktestConfigSchema, PaperConfigSchema, LiveConfigSchema } from '../types';
|
import { TradingMode, ModeConfig, BacktestConfigSchema, PaperConfigSchema, LiveConfigSchema } from '../types';
|
||||||
import { MarketDataService } from '../services/MarketDataService';
|
import { MarketDataService } from '../services/MarketDataService';
|
||||||
import { ExecutionService } from '../services/ExecutionService';
|
import { ExecutionService } from '../services/ExecutionService';
|
||||||
|
|
@ -11,13 +11,16 @@ export class ModeManager extends EventEmitter {
|
||||||
private config: ModeConfig | null = null;
|
private config: ModeConfig | null = null;
|
||||||
private tradingEngine: TradingEngine | null = null;
|
private tradingEngine: TradingEngine | null = null;
|
||||||
private isInitialized = false;
|
private isInitialized = false;
|
||||||
|
private container: IServiceContainer;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
container: IServiceContainer,
|
||||||
private marketDataService: MarketDataService,
|
private marketDataService: MarketDataService,
|
||||||
private executionService: ExecutionService,
|
private executionService: ExecutionService,
|
||||||
private storageService: StorageService
|
private storageService: StorageService
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
this.container = container;
|
||||||
}
|
}
|
||||||
|
|
||||||
async initializeMode(config: ModeConfig): Promise<void> {
|
async initializeMode(config: ModeConfig): Promise<void> {
|
||||||
|
|
@ -52,7 +55,7 @@ export class ModeManager extends EventEmitter {
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
this.emit('modeChanged', config);
|
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 {
|
private createEngineConfig(config: ModeConfig): any {
|
||||||
|
|
@ -127,7 +130,7 @@ export class ModeManager extends EventEmitter {
|
||||||
async transitionMode(fromMode: TradingMode, toMode: TradingMode, config: ModeConfig): Promise<void> {
|
async transitionMode(fromMode: TradingMode, toMode: TradingMode, config: ModeConfig): Promise<void> {
|
||||||
if (fromMode === 'paper' && toMode === 'live') {
|
if (fromMode === 'paper' && toMode === 'live') {
|
||||||
// Special handling for paper to live transition
|
// 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
|
// 1. Get current paper positions
|
||||||
const paperPositions = await this.tradingEngine!.getOpenPositions();
|
const paperPositions = await this.tradingEngine!.getOpenPositions();
|
||||||
|
|
@ -136,7 +139,7 @@ export class ModeManager extends EventEmitter {
|
||||||
await this.initializeMode(config);
|
await this.initializeMode(config);
|
||||||
|
|
||||||
// 3. Reconcile positions (this would be handled by a reconciliation service)
|
// 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 {
|
} else {
|
||||||
// Standard mode switch
|
// Standard mode switch
|
||||||
await this.initializeMode(config);
|
await this.initializeMode(config);
|
||||||
|
|
@ -146,7 +149,7 @@ export class ModeManager extends EventEmitter {
|
||||||
async shutdown(): Promise<void> {
|
async shutdown(): Promise<void> {
|
||||||
if (!this.isInitialized) return;
|
if (!this.isInitialized) return;
|
||||||
|
|
||||||
logger.info(`Shutting down ${this.mode} mode...`);
|
this.container.logger.info(`Shutting down ${this.mode} mode...`);
|
||||||
|
|
||||||
// Shutdown services
|
// Shutdown services
|
||||||
await this.marketDataService.shutdown();
|
await this.marketDataService.shutdown();
|
||||||
|
|
|
||||||
|
|
@ -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 { StorageService } from '../services/StorageService';
|
||||||
import { MarketData, Bar } from '../types';
|
import { MarketData, Bar } from '../types';
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
|
|
@ -62,13 +64,19 @@ export class DataManager extends EventEmitter {
|
||||||
for (const symbol of symbols) {
|
for (const symbol of symbols) {
|
||||||
try {
|
try {
|
||||||
// Load raw data
|
// Load raw data
|
||||||
const data = await this.storageService.getHistoricalBars(
|
let data = await this.storageService.getHistoricalBars(
|
||||||
symbol,
|
symbol,
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
resolution
|
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
|
// Apply corporate actions
|
||||||
const adjustedData = await this.applyCorporateActions(symbol, data, startDate, endDate);
|
const adjustedData = await this.applyCorporateActions(symbol, data, startDate, endDate);
|
||||||
|
|
||||||
|
|
@ -432,4 +440,62 @@ export class DataManager extends EventEmitter {
|
||||||
this.aggregatedCache.clear();
|
this.aggregatedCache.clear();
|
||||||
this.dataQualityIssues = [];
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 { Hono } from 'hono';
|
||||||
import { cors } from 'hono/cors';
|
import { cors } from 'hono/cors';
|
||||||
import { Server as SocketIOServer } from 'socket.io';
|
import { createRoutes } from './routes/create-routes';
|
||||||
import { createServer } from 'http';
|
import { createContainer } from './simple-container';
|
||||||
import { logger } from '@stock-bot/logger';
|
|
||||||
import { ModeManager } from './core/ModeManager';
|
|
||||||
import { createOrderRoutes } from './api/rest/orders';
|
|
||||||
import { createPositionRoutes } from './api/rest/positions';
|
|
||||||
import { createAnalyticsRoutes } from './api/rest/analytics';
|
|
||||||
import { createBacktestRoutes } from './api/rest/backtest';
|
|
||||||
import { setupWebSocketHandlers } from './api/websocket';
|
|
||||||
import { container } from './container';
|
|
||||||
|
|
||||||
const PORT = process.env.PORT || 3002;
|
// 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() {
|
async function main() {
|
||||||
|
let server: any; // Declare server in outer scope for shutdown
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Initialize container with all services using configuration
|
||||||
|
const services = await createContainer(config);
|
||||||
|
|
||||||
// Initialize Hono app
|
// Initialize Hono app
|
||||||
const app = new Hono();
|
const app = new Hono();
|
||||||
|
|
||||||
// Middleware
|
// CORS middleware - use config for origins
|
||||||
app.use('*', cors());
|
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) => {
|
app.use('*', async (c, next) => {
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
await next();
|
await next();
|
||||||
const ms = Date.now() - start;
|
const ms = Date.now() - start;
|
||||||
logger.debug(`${c.req.method} ${c.req.url} - ${ms}ms`);
|
services.logger.debug(`${c.req.method} ${c.req.url} - ${ms}ms`);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Health check
|
// Create and mount routes (without Socket.IO for now)
|
||||||
app.get('/health', (c) => {
|
const routes = createRoutes(services);
|
||||||
const modeManager = container.get('ModeManager');
|
app.route('/', routes);
|
||||||
return c.json({
|
|
||||||
status: 'healthy',
|
// Start Bun server
|
||||||
mode: modeManager.getCurrentMode(),
|
try {
|
||||||
timestamp: new Date().toISOString()
|
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 });
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mount routes
|
services.logger.info(`Orchestrator service started on port ${server.port}`);
|
||||||
app.route('/api/orders', createOrderRoutes());
|
services.logger.info(`Server hostname: ${server.hostname}`);
|
||||||
app.route('/api/positions', createPositionRoutes());
|
services.logger.info(`Server URL: http://${server.hostname}:${server.port}`);
|
||||||
app.route('/api/analytics', createAnalyticsRoutes());
|
|
||||||
app.route('/api/backtest', createBacktestRoutes());
|
|
||||||
|
|
||||||
// Create HTTP server and Socket.IO
|
// Test that server is actually listening
|
||||||
const server = createServer(app.fetch);
|
setTimeout(async () => {
|
||||||
const io = new SocketIOServer(server, {
|
try {
|
||||||
cors: {
|
const testResponse = await fetch(`http://localhost:${server.port}/health`);
|
||||||
origin: '*',
|
services.logger.info(`Server self-test: ${testResponse.status} ${testResponse.statusText}`);
|
||||||
methods: ['GET', 'POST']
|
} 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',
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Setup WebSocket handlers
|
// Note: Socket.IO with Bun requires a different setup
|
||||||
setupWebSocketHandlers(io, container);
|
// For now, we'll disable Socket.IO to avoid the CORS error
|
||||||
|
if (serviceConfig.enableWebSocket) {
|
||||||
// Initialize mode manager
|
services.logger.info('WebSocket support is enabled but Socket.IO integration with Bun requires additional setup');
|
||||||
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
|
// Graceful shutdown
|
||||||
process.on('SIGINT', async () => {
|
process.on('SIGINT', async () => {
|
||||||
logger.info('Shutting down trading orchestrator...');
|
services.logger.info('Orchestrator service shutting down...');
|
||||||
|
|
||||||
|
// Cleanup any active trading sessions
|
||||||
|
const modeManager = services.custom?.ModeManager;
|
||||||
|
if (modeManager) {
|
||||||
await modeManager.shutdown();
|
await modeManager.shutdown();
|
||||||
server.close();
|
}
|
||||||
|
|
||||||
|
if (server) {
|
||||||
|
server.stop();
|
||||||
|
}
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to start orchestrator service:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch((error) => {
|
// Start the service
|
||||||
logger.error('Failed to start trading orchestrator:', error);
|
main().catch(error => {
|
||||||
|
logger.error('Unhandled error:', error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import { logger } from '@stock-bot/logger';
|
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
|
import { IServiceContainer } from '@stock-bot/di';
|
||||||
import { OrderRequest, Position } from '../types';
|
import { OrderRequest, Position } from '../types';
|
||||||
|
import { StorageService } from '../services/StorageService';
|
||||||
|
import { MarketDataService } from '../services/MarketDataService';
|
||||||
import { ExecutionService } from '../services/ExecutionService';
|
import { ExecutionService } from '../services/ExecutionService';
|
||||||
|
|
||||||
interface VirtualAccount {
|
interface VirtualAccount {
|
||||||
|
|
@ -49,12 +51,17 @@ export class PaperTradingManager extends EventEmitter {
|
||||||
private marketPrices = new Map<string, { bid: number; ask: number }>();
|
private marketPrices = new Map<string, { bid: number; ask: number }>();
|
||||||
private readonly COMMISSION_RATE = 0.001; // 0.1%
|
private readonly COMMISSION_RATE = 0.001; // 0.1%
|
||||||
private readonly MARGIN_REQUIREMENT = 0.25; // 25% margin requirement
|
private readonly MARGIN_REQUIREMENT = 0.25; // 25% margin requirement
|
||||||
|
private container: IServiceContainer;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
container: IServiceContainer,
|
||||||
|
private storageService: StorageService,
|
||||||
|
private marketDataService: MarketDataService,
|
||||||
private executionService: ExecutionService,
|
private executionService: ExecutionService,
|
||||||
initialBalance: number = 100000
|
initialBalance: number = 100000
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
this.container = container;
|
||||||
|
|
||||||
this.account = {
|
this.account = {
|
||||||
balance: initialBalance,
|
balance: initialBalance,
|
||||||
|
|
@ -362,6 +369,6 @@ export class PaperTradingManager extends EventEmitter {
|
||||||
marginUsed: 0
|
marginUsed: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
logger.info('Paper trading account reset');
|
this.container.logger.info('Paper trading account reset');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
52
apps/stock/orchestrator/src/routes/create-routes.ts
Normal file
52
apps/stock/orchestrator/src/routes/create-routes.ts
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { logger } from '@stock-bot/logger';
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import { IServiceContainer } from '@stock-bot/di';
|
||||||
import { PerformanceMetrics, RiskMetrics } from '../types';
|
import { PerformanceMetrics, RiskMetrics } from '../types';
|
||||||
|
import { StorageService } from './StorageService';
|
||||||
|
|
||||||
interface OptimizationParams {
|
interface OptimizationParams {
|
||||||
returns: number[][];
|
returns: number[][];
|
||||||
|
|
@ -24,8 +25,12 @@ export class AnalyticsService {
|
||||||
private analyticsUrl: string;
|
private analyticsUrl: string;
|
||||||
private cache = new Map<string, { data: any; timestamp: number }>();
|
private cache = new Map<string, { data: any; timestamp: number }>();
|
||||||
private readonly CACHE_TTL_MS = 60000; // 1 minute cache
|
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';
|
this.analyticsUrl = process.env.ANALYTICS_SERVICE_URL || 'http://localhost:3003';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -50,7 +55,7 @@ export class AnalyticsService {
|
||||||
this.setCache(cacheKey, metrics);
|
this.setCache(cacheKey, metrics);
|
||||||
return metrics;
|
return metrics;
|
||||||
} catch (error) {
|
} 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 default metrics if analytics service is unavailable
|
||||||
return this.getDefaultPerformanceMetrics();
|
return this.getDefaultPerformanceMetrics();
|
||||||
}
|
}
|
||||||
|
|
@ -61,7 +66,7 @@ export class AnalyticsService {
|
||||||
const response = await axios.post(`${this.analyticsUrl}/optimize/portfolio`, params);
|
const response = await axios.post(`${this.analyticsUrl}/optimize/portfolio`, params);
|
||||||
return response.data as PortfolioWeights;
|
return response.data as PortfolioWeights;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error optimizing portfolio:', error);
|
this.container.logger.error('Error optimizing portfolio:', error);
|
||||||
// Return equal weights as fallback
|
// Return equal weights as fallback
|
||||||
return this.getEqualWeights(params.returns[0].length);
|
return this.getEqualWeights(params.returns[0].length);
|
||||||
}
|
}
|
||||||
|
|
@ -78,7 +83,7 @@ export class AnalyticsService {
|
||||||
this.setCache(cacheKey, metrics);
|
this.setCache(cacheKey, metrics);
|
||||||
return metrics;
|
return metrics;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error fetching risk metrics:', error);
|
this.container.logger.error('Error fetching risk metrics:', error);
|
||||||
return this.getDefaultRiskMetrics();
|
return this.getDefaultRiskMetrics();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -94,7 +99,7 @@ export class AnalyticsService {
|
||||||
this.setCache(cacheKey, regime, 300000); // Cache for 5 minutes
|
this.setCache(cacheKey, regime, 300000); // Cache for 5 minutes
|
||||||
return regime;
|
return regime;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error detecting market regime:', error);
|
this.container.logger.error('Error detecting market regime:', error);
|
||||||
return 'normal'; // Default regime
|
return 'normal'; // Default regime
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -104,7 +109,7 @@ export class AnalyticsService {
|
||||||
const response = await axios.post(`${this.analyticsUrl}/analytics/correlation`, { symbols });
|
const response = await axios.post(`${this.analyticsUrl}/analytics/correlation`, { symbols });
|
||||||
return response.data.matrix as number[][];
|
return response.data.matrix as number[][];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error calculating correlation matrix:', error);
|
this.container.logger.error('Error calculating correlation matrix:', error);
|
||||||
// Return identity matrix as fallback
|
// Return identity matrix as fallback
|
||||||
return this.getIdentityMatrix(symbols.length);
|
return this.getIdentityMatrix(symbols.length);
|
||||||
}
|
}
|
||||||
|
|
@ -115,7 +120,7 @@ export class AnalyticsService {
|
||||||
const response = await axios.get(`${this.analyticsUrl}/analytics/backtest/${backtestId}`);
|
const response = await axios.get(`${this.analyticsUrl}/analytics/backtest/${backtestId}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error running backtest analysis:', error);
|
this.container.logger.error('Error running backtest analysis:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -128,7 +133,7 @@ export class AnalyticsService {
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error getting model prediction:', error);
|
this.container.logger.error('Error getting model prediction:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import { logger } from '@stock-bot/logger';
|
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import { IServiceContainer } from '@stock-bot/di';
|
||||||
import { ModeConfig, OrderRequest, OrderRequestSchema } from '../types';
|
import { ModeConfig, OrderRequest, OrderRequestSchema } from '../types';
|
||||||
import { TradingEngine } from '../../core';
|
import { TradingEngine } from '@stock-bot/core';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import { StorageService } from './StorageService';
|
||||||
|
|
||||||
interface ExecutionReport {
|
interface ExecutionReport {
|
||||||
orderId: string;
|
orderId: string;
|
||||||
|
|
@ -29,9 +30,13 @@ export class ExecutionService extends EventEmitter {
|
||||||
private tradingEngine: TradingEngine | null = null;
|
private tradingEngine: TradingEngine | null = null;
|
||||||
private brokerClient: any = null; // Would be specific broker API client
|
private brokerClient: any = null; // Would be specific broker API client
|
||||||
private pendingOrders = new Map<string, OrderRequest>();
|
private pendingOrders = new Map<string, OrderRequest>();
|
||||||
|
private container: IServiceContainer;
|
||||||
|
private storageService: StorageService;
|
||||||
|
|
||||||
constructor(private modeManager: any) {
|
constructor(container: IServiceContainer, storageService: StorageService) {
|
||||||
super();
|
super();
|
||||||
|
this.container = container;
|
||||||
|
this.storageService = storageService;
|
||||||
}
|
}
|
||||||
|
|
||||||
async initialize(config: ModeConfig, tradingEngine: TradingEngine): Promise<void> {
|
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> {
|
private async initializeBroker(broker: string, accountId: string): Promise<void> {
|
||||||
// In real implementation, would initialize specific broker API
|
// In real implementation, would initialize specific broker API
|
||||||
// For example: Alpaca, Interactive Brokers, etc.
|
// 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> {
|
async submitOrder(orderRequest: OrderRequest): Promise<ExecutionReport> {
|
||||||
|
|
@ -97,7 +102,7 @@ export class ExecutionService extends EventEmitter {
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error submitting order:', error);
|
this.container.logger.error('Error submitting order:', error);
|
||||||
return this.createRejectionReport(
|
return this.createRejectionReport(
|
||||||
orderId,
|
orderId,
|
||||||
clientOrderId,
|
clientOrderId,
|
||||||
|
|
@ -171,7 +176,7 @@ export class ExecutionService extends EventEmitter {
|
||||||
): Promise<ExecutionReport> {
|
): Promise<ExecutionReport> {
|
||||||
// In real implementation, would submit to actual broker
|
// In real implementation, would submit to actual broker
|
||||||
// This is a placeholder
|
// This is a placeholder
|
||||||
logger.info(`Submitting order ${orderId} to broker`);
|
this.container.logger.info(`Submitting order ${orderId} to broker`);
|
||||||
|
|
||||||
// Simulate broker response
|
// Simulate broker response
|
||||||
return {
|
return {
|
||||||
|
|
@ -189,7 +194,7 @@ export class ExecutionService extends EventEmitter {
|
||||||
async cancelOrder(orderId: string): Promise<boolean> {
|
async cancelOrder(orderId: string): Promise<boolean> {
|
||||||
const order = this.pendingOrders.get(orderId);
|
const order = this.pendingOrders.get(orderId);
|
||||||
if (!order) {
|
if (!order) {
|
||||||
logger.warn(`Order ${orderId} not found`);
|
this.container.logger.warn(`Order ${orderId} not found`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -222,7 +227,7 @@ export class ExecutionService extends EventEmitter {
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error cancelling order ${orderId}:`, error);
|
this.container.logger.error(`Error cancelling order ${orderId}:`, error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -286,7 +291,7 @@ export class ExecutionService extends EventEmitter {
|
||||||
async routeOrderToExchange(order: OrderRequest, exchange: string): Promise<void> {
|
async routeOrderToExchange(order: OrderRequest, exchange: string): Promise<void> {
|
||||||
// This would route orders to specific exchanges in live mode
|
// This would route orders to specific exchanges in live mode
|
||||||
// For now, just a placeholder
|
// 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> {
|
async getOrderStatus(orderId: string): Promise<ExecutionReport | null> {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { logger } from '@stock-bot/logger';
|
|
||||||
import { io, Socket } from 'socket.io-client';
|
import { io, Socket } from 'socket.io-client';
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
|
import { IServiceContainer } from '@stock-bot/di';
|
||||||
import { ModeConfig, MarketData, QuoteSchema, TradeSchema, BarSchema } from '../types';
|
import { ModeConfig, MarketData, QuoteSchema, TradeSchema, BarSchema } from '../types';
|
||||||
import { QuestDBClient } from '@stock-bot/questdb';
|
import { QuestDBClient } from '@stock-bot/questdb';
|
||||||
|
|
||||||
|
|
@ -13,6 +13,12 @@ export class MarketDataService extends EventEmitter {
|
||||||
private batchTimer: NodeJS.Timeout | null = null;
|
private batchTimer: NodeJS.Timeout | null = null;
|
||||||
private readonly BATCH_SIZE = 100;
|
private readonly BATCH_SIZE = 100;
|
||||||
private readonly BATCH_INTERVAL_MS = 50;
|
private readonly BATCH_INTERVAL_MS = 50;
|
||||||
|
private container: IServiceContainer;
|
||||||
|
|
||||||
|
constructor(container: IServiceContainer) {
|
||||||
|
super();
|
||||||
|
this.container = container;
|
||||||
|
}
|
||||||
|
|
||||||
async initialize(config: ModeConfig): Promise<void> {
|
async initialize(config: ModeConfig): Promise<void> {
|
||||||
this.mode = config.mode;
|
this.mode = config.mode;
|
||||||
|
|
@ -41,7 +47,7 @@ export class MarketDataService extends EventEmitter {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.dataIngestionSocket.on('connect', () => {
|
this.dataIngestionSocket.on('connect', () => {
|
||||||
logger.info('Connected to data-ingestion service');
|
this.container.logger.info('Connected to data-ingestion service');
|
||||||
// Re-subscribe to symbols
|
// Re-subscribe to symbols
|
||||||
this.subscriptions.forEach(symbol => {
|
this.subscriptions.forEach(symbol => {
|
||||||
this.dataIngestionSocket!.emit('subscribe', { symbol });
|
this.dataIngestionSocket!.emit('subscribe', { symbol });
|
||||||
|
|
@ -49,7 +55,7 @@ export class MarketDataService extends EventEmitter {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.dataIngestionSocket.on('disconnect', () => {
|
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) => {
|
this.dataIngestionSocket.on('marketData', (data: any) => {
|
||||||
|
|
@ -57,7 +63,7 @@ export class MarketDataService extends EventEmitter {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.dataIngestionSocket.on('error', (error: any) => {
|
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 });
|
this.dataIngestionSocket.emit('subscribe', { symbol });
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(`Subscribed to ${symbol}`);
|
this.container.logger.debug(`Subscribed to ${symbol}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async unsubscribeFromSymbol(symbol: string): Promise<void> {
|
async unsubscribeFromSymbol(symbol: string): Promise<void> {
|
||||||
|
|
@ -78,7 +84,7 @@ export class MarketDataService extends EventEmitter {
|
||||||
this.dataIngestionSocket.emit('unsubscribe', { symbol });
|
this.dataIngestionSocket.emit('unsubscribe', { symbol });
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(`Unsubscribed from ${symbol}`);
|
this.container.logger.debug(`Unsubscribed from ${symbol}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleMarketData(data: any): void {
|
private handleMarketData(data: any): void {
|
||||||
|
|
@ -118,7 +124,7 @@ export class MarketDataService extends EventEmitter {
|
||||||
});
|
});
|
||||||
marketData = { type: 'bar', data: bar };
|
marketData = { type: 'bar', data: bar };
|
||||||
} else {
|
} else {
|
||||||
logger.warn('Unknown market data format:', data);
|
this.container.logger.warn('Unknown market data format:', data);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -134,7 +140,7 @@ export class MarketDataService extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} 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
|
// Close QuestDB connection
|
||||||
if (this.questdbClient) {
|
if (this.questdbClient) {
|
||||||
await this.questdbClient.close();
|
await this.questdbClient.disconnect();
|
||||||
this.questdbClient = null;
|
this.questdbClient = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,31 @@
|
||||||
import { logger } from '@stock-bot/logger';
|
import { IServiceContainer } from '@stock-bot/di';
|
||||||
import { QuestDBClient } from '@stock-bot/questdb';
|
import { QuestDBClient } from '@stock-bot/questdb';
|
||||||
import { PostgresClient } from '@stock-bot/postgres';
|
import { PostgresClient } from '@stock-bot/postgres';
|
||||||
|
import { MongoDBClient } from '@stock-bot/mongodb';
|
||||||
import { ModeConfig, MarketData, Position } from '../types';
|
import { ModeConfig, MarketData, Position } from '../types';
|
||||||
|
|
||||||
export class StorageService {
|
export class StorageService {
|
||||||
private questdb: QuestDBClient | null = null;
|
private questdb: QuestDBClient | null = null;
|
||||||
private postgres: PostgresClient | null = null;
|
private postgres: PostgresClient | null = null;
|
||||||
|
private mongodb: MongoDBClient | null = null;
|
||||||
private mode: 'backtest' | 'paper' | 'live' = 'paper';
|
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> {
|
async initialize(config: ModeConfig): Promise<void> {
|
||||||
this.mode = config.mode;
|
this.mode = config.mode;
|
||||||
|
// Clients are already injected via DI
|
||||||
// Initialize QuestDB for time-series data
|
|
||||||
this.questdb = new QuestDBClient({
|
|
||||||
host: process.env.QUESTDB_HOST || 'localhost',
|
|
||||||
port: parseInt(process.env.QUESTDB_PORT || '9000'),
|
|
||||||
database: process.env.QUESTDB_DATABASE || 'trading'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize PostgreSQL for relational data
|
|
||||||
this.postgres = new PostgresClient({
|
|
||||||
host: process.env.POSTGRES_HOST || 'localhost',
|
|
||||||
port: parseInt(process.env.POSTGRES_PORT || '5432'),
|
|
||||||
database: process.env.POSTGRES_DATABASE || 'trading',
|
|
||||||
user: process.env.POSTGRES_USER || 'postgres',
|
|
||||||
password: process.env.POSTGRES_PASSWORD || 'postgres'
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.createTables();
|
await this.createTables();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -281,12 +280,12 @@ export class StorageService {
|
||||||
|
|
||||||
async shutdown(): Promise<void> {
|
async shutdown(): Promise<void> {
|
||||||
if (this.questdb) {
|
if (this.questdb) {
|
||||||
await this.questdb.close();
|
await this.questdb.disconnect();
|
||||||
this.questdb = null;
|
this.questdb = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.postgres) {
|
if (this.postgres) {
|
||||||
await this.postgres.close();
|
await this.postgres.disconnect();
|
||||||
this.postgres = null;
|
this.postgres = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
137
apps/stock/orchestrator/src/simple-container.ts
Normal file
137
apps/stock/orchestrator/src/simple-container.ts
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import { EventEmitter } from 'events';
|
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 { MarketData, StrategyConfig, OrderRequest } from '../types';
|
||||||
import { ModeManager } from '../core/ModeManager';
|
import { ModeManager } from '../core/ModeManager';
|
||||||
import { ExecutionService } from '../services/ExecutionService';
|
import { ExecutionService } from '../services/ExecutionService';
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,41 @@
|
||||||
import { logger } from '@stock-bot/logger';
|
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
|
import { IServiceContainer } from '@stock-bot/di';
|
||||||
import { MarketData, StrategyConfig, OrderRequest } from '../types';
|
import { MarketData, StrategyConfig, OrderRequest } from '../types';
|
||||||
import { BaseStrategy } from './BaseStrategy';
|
import { BaseStrategy } from './BaseStrategy';
|
||||||
import { ModeManager } from '../core/ModeManager';
|
import { TradingEngine } from '@stock-bot/core';
|
||||||
import { MarketDataService } from '../services/MarketDataService';
|
|
||||||
import { ExecutionService } from '../services/ExecutionService';
|
|
||||||
import { TradingEngine } from '../../core';
|
|
||||||
|
|
||||||
export class StrategyManager extends EventEmitter {
|
export class StrategyManager extends EventEmitter {
|
||||||
private strategies = new Map<string, BaseStrategy>();
|
private strategies = new Map<string, BaseStrategy>();
|
||||||
private activeStrategies = new Set<string>();
|
private activeStrategies = new Set<string>();
|
||||||
private tradingEngine: TradingEngine | null = null;
|
private tradingEngine: TradingEngine | null = null;
|
||||||
|
private container: IServiceContainer;
|
||||||
|
|
||||||
constructor(
|
constructor(container: IServiceContainer) {
|
||||||
private modeManager: ModeManager,
|
|
||||||
private marketDataService: MarketDataService,
|
|
||||||
private executionService: ExecutionService
|
|
||||||
) {
|
|
||||||
super();
|
super();
|
||||||
this.setupEventListeners();
|
this.container = container;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupEventListeners(): void {
|
|
||||||
// Listen for market data
|
// Listen for market data
|
||||||
this.marketDataService.on('marketData', (data: MarketData) => {
|
marketDataService.on('marketData', (data: MarketData) => {
|
||||||
this.handleMarketData(data);
|
this.handleMarketData(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Listen for market data batches (more efficient)
|
// Listen for market data batches (more efficient)
|
||||||
this.marketDataService.on('marketDataBatch', (batch: MarketData[]) => {
|
marketDataService.on('marketDataBatch', (batch: MarketData[]) => {
|
||||||
this.handleMarketDataBatch(batch);
|
this.handleMarketDataBatch(batch);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Listen for fills
|
// Listen for fills
|
||||||
this.executionService.on('fill', (fill: any) => {
|
executionService.on('fill', (fill: any) => {
|
||||||
this.handleFill(fill);
|
this.handleFill(fill);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -47,7 +49,10 @@ export class StrategyManager extends EventEmitter {
|
||||||
this.activeStrategies.clear();
|
this.activeStrategies.clear();
|
||||||
|
|
||||||
// Get trading engine from mode manager
|
// 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
|
// Initialize new strategies
|
||||||
for (const config of configs) {
|
for (const config of configs) {
|
||||||
|
|
@ -59,9 +64,9 @@ export class StrategyManager extends EventEmitter {
|
||||||
await this.enableStrategy(config.id);
|
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) {
|
} 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
|
// For now, create a base strategy instance
|
||||||
const strategy = new BaseStrategy(
|
const strategy = new BaseStrategy(
|
||||||
config,
|
config,
|
||||||
this.modeManager,
|
this.container.custom?.ModeManager,
|
||||||
this.executionService
|
this.container.custom?.ExecutionService
|
||||||
);
|
);
|
||||||
|
|
||||||
// Set up strategy event handlers
|
// Set up strategy event handlers
|
||||||
|
|
@ -80,12 +85,10 @@ export class StrategyManager extends EventEmitter {
|
||||||
this.handleStrategySignal(config.id, signal);
|
this.handleStrategySignal(config.id, signal);
|
||||||
});
|
});
|
||||||
|
|
||||||
strategy.on('order', (order: OrderRequest) => {
|
strategy.on('error', (error: Error) => {
|
||||||
this.handleStrategyOrder(config.id, order);
|
this.container.logger.error(`Strategy ${config.id} error:`, error);
|
||||||
});
|
});
|
||||||
|
|
||||||
await strategy.initialize();
|
|
||||||
|
|
||||||
return strategy;
|
return strategy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -95,9 +98,9 @@ export class StrategyManager extends EventEmitter {
|
||||||
throw new Error(`Strategy ${strategyId} not found`);
|
throw new Error(`Strategy ${strategyId} not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await strategy.start();
|
await strategy.initialize();
|
||||||
this.activeStrategies.add(strategyId);
|
this.activeStrategies.add(strategyId);
|
||||||
logger.info(`Enabled strategy: ${strategyId}`);
|
this.container.logger.info(`Enabled strategy: ${strategyId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async disableStrategy(strategyId: string): Promise<void> {
|
async disableStrategy(strategyId: string): Promise<void> {
|
||||||
|
|
@ -106,118 +109,79 @@ export class StrategyManager extends EventEmitter {
|
||||||
throw new Error(`Strategy ${strategyId} not found`);
|
throw new Error(`Strategy ${strategyId} not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await strategy.stop();
|
await strategy.shutdown();
|
||||||
this.activeStrategies.delete(strategyId);
|
this.activeStrategies.delete(strategyId);
|
||||||
logger.info(`Disabled strategy: ${strategyId}`);
|
this.container.logger.info(`Disabled strategy: ${strategyId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleMarketData(data: MarketData): Promise<void> {
|
private async handleMarketData(data: MarketData): Promise<void> {
|
||||||
// Forward to active strategies
|
// Forward to all active strategies
|
||||||
for (const strategyId of this.activeStrategies) {
|
for (const strategyId of this.activeStrategies) {
|
||||||
const strategy = this.strategies.get(strategyId);
|
const strategy = this.strategies.get(strategyId);
|
||||||
if (strategy && strategy.isInterestedInSymbol(data.data.symbol)) {
|
if (strategy) {
|
||||||
try {
|
try {
|
||||||
await strategy.onMarketData(data);
|
await strategy.onMarketData(data);
|
||||||
} catch (error) {
|
} 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> {
|
private async handleMarketDataBatch(batch: MarketData[]): Promise<void> {
|
||||||
// Group by symbol for efficiency
|
// Process batch more efficiently
|
||||||
const bySymbol = new Map<string, MarketData[]>();
|
|
||||||
|
|
||||||
for (const data of batch) {
|
|
||||||
const symbol = data.data.symbol;
|
|
||||||
if (!bySymbol.has(symbol)) {
|
|
||||||
bySymbol.set(symbol, []);
|
|
||||||
}
|
|
||||||
bySymbol.get(symbol)!.push(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Forward to strategies
|
|
||||||
for (const strategyId of this.activeStrategies) {
|
for (const strategyId of this.activeStrategies) {
|
||||||
const strategy = this.strategies.get(strategyId);
|
const strategy = this.strategies.get(strategyId);
|
||||||
if (!strategy) continue;
|
if (strategy) {
|
||||||
|
|
||||||
const relevantData: MarketData[] = [];
|
|
||||||
for (const [symbol, data] of bySymbol) {
|
|
||||||
if (strategy.isInterestedInSymbol(symbol)) {
|
|
||||||
relevantData.push(...data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (relevantData.length > 0) {
|
|
||||||
try {
|
try {
|
||||||
await strategy.onMarketDataBatch(relevantData);
|
await strategy.onMarketDataBatch(batch);
|
||||||
} catch (error) {
|
} 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> {
|
private async handleFill(fill: any): Promise<void> {
|
||||||
// Notify relevant strategies about fills
|
// Forward fill to the strategy that created the order
|
||||||
for (const strategyId of this.activeStrategies) {
|
for (const [strategyId, strategy] of this.strategies) {
|
||||||
const strategy = this.strategies.get(strategyId);
|
if (strategy.hasOrder(fill.orderId)) {
|
||||||
if (strategy && strategy.hasPosition(fill.symbol)) {
|
|
||||||
try {
|
try {
|
||||||
await strategy.onFill(fill);
|
await strategy.onFill(fill);
|
||||||
} catch (error) {
|
} 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> {
|
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
|
// Convert signal to order request
|
||||||
this.emit('strategySignal', {
|
const orderRequest: OrderRequest = {
|
||||||
strategyId,
|
symbol: signal.symbol,
|
||||||
signal,
|
quantity: signal.quantity,
|
||||||
timestamp: Date.now()
|
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
|
// Submit order through execution service
|
||||||
const result = await this.executionService.submitOrder(order);
|
const executionService = this.container.custom?.ExecutionService;
|
||||||
|
if (executionService) {
|
||||||
// Notify strategy of order result
|
try {
|
||||||
const strategy = this.strategies.get(strategyId);
|
const result = await executionService.submitOrder(orderRequest);
|
||||||
if (strategy) {
|
this.container.logger.info(`Order submitted for strategy ${strategyId}:`, result);
|
||||||
await strategy.onOrderUpdate(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Emit for monitoring
|
|
||||||
this.emit('strategyOrder', {
|
|
||||||
strategyId,
|
|
||||||
order,
|
|
||||||
result,
|
|
||||||
timestamp: Date.now()
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Failed to submit order from strategy ${strategyId}:`, error);
|
this.container.logger.error(`Failed to submit order for strategy ${strategyId}:`, error);
|
||||||
|
|
||||||
// Notify strategy of failure
|
|
||||||
const strategy = this.strategies.get(strategyId);
|
|
||||||
if (strategy) {
|
|
||||||
await strategy.onOrderError(order, error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async onMarketData(data: MarketData): Promise<void> {
|
async onMarketData(data: MarketData): Promise<void> {
|
||||||
// Called by backtest engine
|
|
||||||
await this.handleMarketData(data);
|
await this.handleMarketData(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -225,52 +189,25 @@ export class StrategyManager extends EventEmitter {
|
||||||
return this.tradingEngine;
|
return this.tradingEngine;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getActiveStrategies(): string[] {
|
||||||
|
return Array.from(this.activeStrategies);
|
||||||
|
}
|
||||||
|
|
||||||
getStrategy(strategyId: string): BaseStrategy | undefined {
|
getStrategy(strategyId: string): BaseStrategy | undefined {
|
||||||
return this.strategies.get(strategyId);
|
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> {
|
async shutdown(): Promise<void> {
|
||||||
logger.info('Shutting down strategy manager...');
|
this.container.logger.info('Shutting down strategy manager...');
|
||||||
|
|
||||||
// Disable all strategies
|
// Disable all strategies
|
||||||
for (const strategyId of this.activeStrategies) {
|
for (const strategyId of this.activeStrategies) {
|
||||||
await this.disableStrategy(strategyId);
|
await this.disableStrategy(strategyId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shutdown all strategies
|
// Clear all strategies
|
||||||
for (const [id, strategy] of this.strategies) {
|
|
||||||
await strategy.shutdown();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.strategies.clear();
|
this.strategies.clear();
|
||||||
this.activeStrategies.clear();
|
this.activeStrategies.clear();
|
||||||
this.removeAllListeners();
|
this.tradingEngine = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import { BaseStrategy, Signal } from '../BaseStrategy';
|
import { BaseStrategy, Signal } from '../BaseStrategy';
|
||||||
import { MarketData } from '../../types';
|
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';
|
import * as tf from '@tensorflow/tfjs-node';
|
||||||
|
|
||||||
interface MLModelConfig {
|
interface MLModelConfig {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import { BaseStrategy, Signal } from '../BaseStrategy';
|
import { BaseStrategy, Signal } from '../BaseStrategy';
|
||||||
import { MarketData } from '../../types';
|
import { MarketData } from '../../types';
|
||||||
import { logger } from '@stock-bot/logger';
|
import { getLogger } from '@stock-bot/logger';
|
||||||
|
|
||||||
|
const logger = getLogger('MeanReversionStrategy');
|
||||||
|
|
||||||
interface MeanReversionIndicators {
|
interface MeanReversionIndicators {
|
||||||
sma20: number;
|
sma20: number;
|
||||||
|
|
|
||||||
|
|
@ -162,4 +162,4 @@ export const RiskMetricsSchema = z.object({
|
||||||
export type RiskMetrics = z.infer<typeof RiskMetricsSchema>;
|
export type RiskMetrics = z.infer<typeof RiskMetricsSchema>;
|
||||||
|
|
||||||
// Re-export specialized types
|
// Re-export specialized types
|
||||||
export { MarketMicrostructure, PriceLevel, OrderBookSnapshot } from './types/MarketMicrostructure';
|
export type { MarketMicrostructure, PriceLevel, OrderBookSnapshot } from './types/MarketMicrostructure';
|
||||||
|
|
@ -31,7 +31,7 @@ const app = new ServiceApplication(
|
||||||
enableHandlers: false, // Web API doesn't use handlers
|
enableHandlers: false, // Web API doesn't use handlers
|
||||||
enableScheduledJobs: false, // Web API doesn't use scheduled jobs
|
enableScheduledJobs: false, // Web API doesn't use scheduled jobs
|
||||||
corsConfig: {
|
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'],
|
allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
||||||
allowHeaders: ['Content-Type', 'Authorization'],
|
allowHeaders: ['Content-Type', 'Authorization'],
|
||||||
credentials: true,
|
credentials: true,
|
||||||
|
|
|
||||||
97
apps/stock/web-api/src/routes/backtest.routes.ts
Normal file
97
apps/stock/web-api/src/routes/backtest.routes.ts
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
import { Hono } from 'hono';
|
||||||
|
import type { IServiceContainer } from '@stock-bot/handlers';
|
||||||
|
import { BacktestService } from '../services/backtest.service';
|
||||||
|
import { getLogger } from '@stock-bot/logger';
|
||||||
|
|
||||||
|
const logger = getLogger('backtest-routes');
|
||||||
|
|
||||||
|
export function createBacktestRoutes(container: IServiceContainer) {
|
||||||
|
const backtestRoutes = new Hono();
|
||||||
|
const backtestService = new BacktestService();
|
||||||
|
|
||||||
|
// Create a new backtest
|
||||||
|
backtestRoutes.post('/api/backtests', async (c) => {
|
||||||
|
try {
|
||||||
|
const body = await c.req.json();
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if (!body.strategy || !body.symbols || !body.startDate || !body.endDate) {
|
||||||
|
return c.json({
|
||||||
|
error: 'Missing required fields: strategy, symbols, startDate, endDate'
|
||||||
|
}, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const backtest = await backtestService.createBacktest({
|
||||||
|
...body,
|
||||||
|
initialCapital: body.initialCapital || 100000,
|
||||||
|
});
|
||||||
|
|
||||||
|
return c.json(backtest, 201);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to create backtest', { error });
|
||||||
|
return c.json({ error: 'Failed to create backtest' }, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get backtest status
|
||||||
|
backtestRoutes.get('/api/backtests/:id', async (c) => {
|
||||||
|
try {
|
||||||
|
const id = c.req.param('id');
|
||||||
|
const backtest = await backtestService.getBacktest(id);
|
||||||
|
|
||||||
|
if (!backtest) {
|
||||||
|
return c.json({ error: 'Backtest not found' }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json(backtest);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to get backtest', { error });
|
||||||
|
return c.json({ error: 'Failed to get backtest' }, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get backtest results
|
||||||
|
backtestRoutes.get('/api/backtests/:id/results', async (c) => {
|
||||||
|
try {
|
||||||
|
const id = c.req.param('id');
|
||||||
|
const results = await backtestService.getBacktestResults(id);
|
||||||
|
|
||||||
|
if (!results) {
|
||||||
|
return c.json({ error: 'Results not found' }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json(results);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to get results', { error });
|
||||||
|
return c.json({ error: 'Failed to get results' }, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// List all backtests
|
||||||
|
backtestRoutes.get('/api/backtests', async (c) => {
|
||||||
|
try {
|
||||||
|
const limit = Number(c.req.query('limit')) || 50;
|
||||||
|
const offset = Number(c.req.query('offset')) || 0;
|
||||||
|
|
||||||
|
const backtests = await backtestService.listBacktests({ limit, offset });
|
||||||
|
return c.json(backtests);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to list backtests', { error });
|
||||||
|
return c.json({ error: 'Failed to list backtests' }, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cancel a backtest
|
||||||
|
backtestRoutes.post('/api/backtests/:id/cancel', async (c) => {
|
||||||
|
try {
|
||||||
|
const id = c.req.param('id');
|
||||||
|
await backtestService.cancelBacktest(id);
|
||||||
|
return c.json({ message: 'Backtest cancelled' });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to cancel backtest', { error });
|
||||||
|
return c.json({ error: 'Failed to cancel backtest' }, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return backtestRoutes;
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,7 @@ import { createExchangeRoutes } from './exchange.routes';
|
||||||
import { createHealthRoutes } from './health.routes';
|
import { createHealthRoutes } from './health.routes';
|
||||||
import { createMonitoringRoutes } from './monitoring.routes';
|
import { createMonitoringRoutes } from './monitoring.routes';
|
||||||
import { createPipelineRoutes } from './pipeline.routes';
|
import { createPipelineRoutes } from './pipeline.routes';
|
||||||
|
import { createBacktestRoutes } from './backtest.routes';
|
||||||
|
|
||||||
export function createRoutes(container: IServiceContainer): Hono {
|
export function createRoutes(container: IServiceContainer): Hono {
|
||||||
const app = new Hono();
|
const app = new Hono();
|
||||||
|
|
@ -18,12 +19,14 @@ export function createRoutes(container: IServiceContainer): Hono {
|
||||||
const exchangeRoutes = createExchangeRoutes(container);
|
const exchangeRoutes = createExchangeRoutes(container);
|
||||||
const monitoringRoutes = createMonitoringRoutes(container);
|
const monitoringRoutes = createMonitoringRoutes(container);
|
||||||
const pipelineRoutes = createPipelineRoutes(container);
|
const pipelineRoutes = createPipelineRoutes(container);
|
||||||
|
const backtestRoutes = createBacktestRoutes(container);
|
||||||
|
|
||||||
// Mount routes
|
// Mount routes
|
||||||
app.route('/health', healthRoutes);
|
app.route('/health', healthRoutes);
|
||||||
app.route('/api/exchanges', exchangeRoutes);
|
app.route('/api/exchanges', exchangeRoutes);
|
||||||
app.route('/api/system/monitoring', monitoringRoutes);
|
app.route('/api/system/monitoring', monitoringRoutes);
|
||||||
app.route('/api/pipeline', pipelineRoutes);
|
app.route('/api/pipeline', pipelineRoutes);
|
||||||
|
app.route('/', backtestRoutes); // Mounted at root since routes already have /api prefix
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
174
apps/stock/web-api/src/services/backtest.service.ts
Normal file
174
apps/stock/web-api/src/services/backtest.service.ts
Normal file
|
|
@ -0,0 +1,174 @@
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import { getLogger } from '@stock-bot/logger';
|
||||||
|
|
||||||
|
const logger = getLogger('backtest-service');
|
||||||
|
|
||||||
|
// Use environment variable or default
|
||||||
|
const ORCHESTRATOR_URL = process.env.ORCHESTRATOR_URL || 'http://localhost:2004';
|
||||||
|
|
||||||
|
export interface BacktestRequest {
|
||||||
|
strategy: string;
|
||||||
|
symbols: string[];
|
||||||
|
startDate: string;
|
||||||
|
endDate: string;
|
||||||
|
initialCapital: number;
|
||||||
|
config?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BacktestJob {
|
||||||
|
id: string;
|
||||||
|
status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
|
||||||
|
strategy: string;
|
||||||
|
symbols: string[];
|
||||||
|
startDate: Date;
|
||||||
|
endDate: Date;
|
||||||
|
initialCapital: number;
|
||||||
|
config: Record<string, any>;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In-memory storage for demo (replace with database)
|
||||||
|
const backtestStore = new Map<string, BacktestJob>();
|
||||||
|
const backtestResults = new Map<string, any>();
|
||||||
|
|
||||||
|
export class BacktestService {
|
||||||
|
async createBacktest(request: BacktestRequest): Promise<BacktestJob> {
|
||||||
|
const backtestId = uuidv4();
|
||||||
|
|
||||||
|
// Store in memory (replace with database)
|
||||||
|
const backtest: BacktestJob = {
|
||||||
|
id: backtestId,
|
||||||
|
status: 'pending',
|
||||||
|
strategy: request.strategy,
|
||||||
|
symbols: request.symbols,
|
||||||
|
startDate: new Date(request.startDate),
|
||||||
|
endDate: new Date(request.endDate),
|
||||||
|
initialCapital: request.initialCapital,
|
||||||
|
config: request.config || {},
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
backtestStore.set(backtestId, backtest);
|
||||||
|
|
||||||
|
// Call orchestrator to run backtest
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${ORCHESTRATOR_URL}/api/backtest/run`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
mode: 'backtest',
|
||||||
|
startDate: new Date(request.startDate).toISOString(),
|
||||||
|
endDate: new Date(request.endDate).toISOString(),
|
||||||
|
symbols: request.symbols,
|
||||||
|
initialCapital: request.initialCapital,
|
||||||
|
dataFrequency: '1d', // Default to daily
|
||||||
|
speed: 'max', // Default speed
|
||||||
|
fillModel: {
|
||||||
|
slippage: 'realistic',
|
||||||
|
marketImpact: true,
|
||||||
|
partialFills: true
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Orchestrator returned ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
// Store result when available
|
||||||
|
if (result.performance) {
|
||||||
|
// Backtest completed immediately
|
||||||
|
backtest.status = 'completed';
|
||||||
|
backtestStore.set(backtestId, backtest);
|
||||||
|
backtestResults.set(backtestId, result);
|
||||||
|
} else {
|
||||||
|
// Update status to running if not completed
|
||||||
|
backtest.status = 'running';
|
||||||
|
backtestStore.set(backtestId, backtest);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('Backtest started in orchestrator', { backtestId, result });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to start backtest in orchestrator', { backtestId, error });
|
||||||
|
backtest.status = 'failed';
|
||||||
|
backtest.error = error.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return backtest;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBacktest(id: string): Promise<BacktestJob | null> {
|
||||||
|
return backtestStore.get(id) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBacktestResults(id: string): Promise<any> {
|
||||||
|
const results = backtestResults.get(id);
|
||||||
|
if (!results) return null;
|
||||||
|
|
||||||
|
// Transform orchestrator response to frontend expected format
|
||||||
|
return {
|
||||||
|
backtestId: results.id || id,
|
||||||
|
metrics: {
|
||||||
|
totalReturn: results.performance?.totalReturn || 0,
|
||||||
|
sharpeRatio: results.performance?.sharpeRatio || 0,
|
||||||
|
maxDrawdown: results.performance?.maxDrawdown || 0,
|
||||||
|
winRate: results.performance?.winRate || 0,
|
||||||
|
totalTrades: results.performance?.totalTrades || 0,
|
||||||
|
profitFactor: results.performance?.profitFactor
|
||||||
|
},
|
||||||
|
equity: results.equityCurve?.map((point: any) => ({
|
||||||
|
date: new Date(point.timestamp).toISOString(),
|
||||||
|
value: point.value
|
||||||
|
})) || [],
|
||||||
|
trades: results.trades?.map((trade: any) => ({
|
||||||
|
symbol: trade.symbol,
|
||||||
|
entryDate: new Date(trade.entryTime).toISOString(),
|
||||||
|
exitDate: new Date(trade.exitTime).toISOString(),
|
||||||
|
entryPrice: trade.entryPrice,
|
||||||
|
exitPrice: trade.exitPrice,
|
||||||
|
quantity: trade.quantity,
|
||||||
|
pnl: trade.pnl
|
||||||
|
})) || [],
|
||||||
|
ohlcData: results.ohlcData || {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async listBacktests(params: { limit: number; offset: number }): Promise<BacktestJob[]> {
|
||||||
|
const all = Array.from(backtestStore.values());
|
||||||
|
return all
|
||||||
|
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
|
||||||
|
.slice(params.offset, params.offset + params.limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateBacktestStatus(id: string, status: BacktestJob['status'], error?: string): Promise<void> {
|
||||||
|
const backtest = backtestStore.get(id);
|
||||||
|
if (backtest) {
|
||||||
|
backtest.status = status;
|
||||||
|
backtest.updatedAt = new Date();
|
||||||
|
if (error) {
|
||||||
|
backtest.error = error;
|
||||||
|
}
|
||||||
|
backtestStore.set(id, backtest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async cancelBacktest(id: string): Promise<void> {
|
||||||
|
await this.updateBacktestStatus(id, 'cancelled');
|
||||||
|
|
||||||
|
// Call orchestrator to stop backtest
|
||||||
|
try {
|
||||||
|
await fetch(`${ORCHESTRATOR_URL}/api/backtest/stop`, {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to stop backtest in orchestrator', { backtestId: id, error });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
215
apps/stock/web-app/src/components/charts/Chart.tsx
Normal file
215
apps/stock/web-app/src/components/charts/Chart.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
2
apps/stock/web-app/src/components/charts/index.ts
Normal file
2
apps/stock/web-app/src/components/charts/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { Chart } from './Chart';
|
||||||
|
export type { ChartProps, ChartData } from './Chart';
|
||||||
119
apps/stock/web-app/src/features/backtest/BacktestListPage.tsx
Normal file
119
apps/stock/web-app/src/features/backtest/BacktestListPage.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,24 +1,115 @@
|
||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { BacktestConfiguration } from './components/BacktestConfiguration';
|
import { BacktestConfiguration } from './components/BacktestConfiguration';
|
||||||
import { BacktestResults } from './components/BacktestResults';
|
|
||||||
import { BacktestControls } from './components/BacktestControls';
|
import { BacktestControls } from './components/BacktestControls';
|
||||||
|
import { BacktestResults } from './components/BacktestResults';
|
||||||
import { useBacktest } from './hooks/useBacktest';
|
import { useBacktest } from './hooks/useBacktest';
|
||||||
|
import type { BacktestConfig, BacktestResult as LocalBacktestResult } from './types/backtest.types';
|
||||||
|
|
||||||
export function BacktestPage() {
|
export function BacktestPage() {
|
||||||
const {
|
const {
|
||||||
config,
|
backtest,
|
||||||
status,
|
|
||||||
results,
|
results,
|
||||||
currentTime,
|
|
||||||
error,
|
|
||||||
isLoading,
|
isLoading,
|
||||||
handleConfigSubmit,
|
isPolling,
|
||||||
handleStart,
|
error,
|
||||||
handlePause,
|
createBacktest,
|
||||||
handleResume,
|
cancelBacktest,
|
||||||
handleStop,
|
reset,
|
||||||
handleStep,
|
|
||||||
} = useBacktest();
|
} = 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 (
|
return (
|
||||||
<div className="flex flex-col h-full space-y-6">
|
<div className="flex flex-col h-full space-y-6">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
|
|
@ -38,7 +129,7 @@ export function BacktestPage() {
|
||||||
<div className="lg:col-span-1 space-y-4">
|
<div className="lg:col-span-1 space-y-4">
|
||||||
<BacktestConfiguration
|
<BacktestConfiguration
|
||||||
onSubmit={handleConfigSubmit}
|
onSubmit={handleConfigSubmit}
|
||||||
disabled={status === 'running' || isLoading}
|
disabled={status === 'running' || isLoading || isPolling}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{config && (
|
{config && (
|
||||||
|
|
@ -59,7 +150,7 @@ export function BacktestPage() {
|
||||||
<div className="lg:col-span-2">
|
<div className="lg:col-span-2">
|
||||||
<BacktestResults
|
<BacktestResults
|
||||||
status={status}
|
status={status}
|
||||||
results={results}
|
results={adaptedResults}
|
||||||
currentTime={currentTime}
|
currentTime={currentTime}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -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 { MetricsCard } from './MetricsCard';
|
||||||
import { PositionsTable } from './PositionsTable';
|
import { PositionsTable } from './PositionsTable';
|
||||||
import { TradeLog } from './TradeLog';
|
import { TradeLog } from './TradeLog';
|
||||||
|
import { Chart } from '../../../components/charts';
|
||||||
|
import { useState, useMemo } from 'react';
|
||||||
|
|
||||||
interface BacktestResultsProps {
|
interface BacktestResultsProps {
|
||||||
status: BacktestStatus;
|
status: BacktestStatus;
|
||||||
|
|
@ -10,6 +13,11 @@ interface BacktestResultsProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BacktestResults({ status, results, currentTime }: 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') {
|
if (status === 'idle') {
|
||||||
return (
|
return (
|
||||||
<div className="bg-surface-secondary p-8 rounded-lg border border-border h-full flex items-center justify-center">
|
<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"
|
title="Total Trades"
|
||||||
value={results.metrics.totalTrades.toString()}
|
value={results.metrics.totalTrades.toString()}
|
||||||
/>
|
/>
|
||||||
|
{results.metrics.profitFactor && (
|
||||||
<MetricsCard
|
<MetricsCard
|
||||||
title="Profitable Trades"
|
title="Profit Factor"
|
||||||
value={results.metrics.profitableTrades.toString()}
|
value={results.metrics.profitFactor.toFixed(2)}
|
||||||
|
trend={results.metrics.profitFactor >= 1 ? 'up' : 'down'}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Performance Chart Placeholder */}
|
{/* Performance Chart */}
|
||||||
<div className="bg-surface-secondary p-4 rounded-lg border border-border">
|
<div className="bg-surface-secondary p-4 rounded-lg border border-border">
|
||||||
<h3 className="text-base font-medium text-text-primary mb-4">
|
<h3 className="text-base font-medium text-text-primary mb-4">
|
||||||
Portfolio Performance
|
Portfolio Performance
|
||||||
</h3>
|
</h3>
|
||||||
<div className="h-64 bg-background rounded border border-border flex items-center justify-center">
|
{(() => {
|
||||||
|
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">
|
<p className="text-sm text-text-muted">
|
||||||
Performance chart will be displayed here (requires recharts)
|
No data available
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})()}
|
||||||
</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 */}
|
{/* Trade Log */}
|
||||||
{results.trades.length > 0 && (
|
{results.trades && results.trades.length > 0 && (
|
||||||
<div className="bg-surface-secondary p-4 rounded-lg border border-border">
|
<div className="bg-surface-secondary p-4 rounded-lg border border-border">
|
||||||
<h3 className="text-base font-medium text-text-primary mb-4">
|
<h3 className="text-base font-medium text-text-primary mb-4">
|
||||||
Trade History
|
Trade History
|
||||||
</h3>
|
</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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,169 +1,175 @@
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { BacktestService } from '../services/backtestService';
|
import type { BacktestJob, BacktestRequest, BacktestResult } from '../services/backtestApi';
|
||||||
import type { BacktestConfig, BacktestResult, BacktestStatus } from '../types';
|
import { backtestApi, } from '../services/backtestApi';
|
||||||
|
|
||||||
export function useBacktest() {
|
interface UseBacktestReturn {
|
||||||
const [backtestId, setBacktestId] = useState<string | null>(null);
|
// State
|
||||||
const [config, setConfig] = useState<BacktestConfig | null>(null);
|
backtest: BacktestJob | null;
|
||||||
const [status, setStatus] = useState<BacktestStatus>('idle');
|
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 [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 [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isPolling, setIsPolling] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const cleanupRef = useRef<(() => void) | null>(null);
|
const pollingIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
const handleConfigSubmit = useCallback(async (newConfig: BacktestConfig) => {
|
// Poll for status updates
|
||||||
|
const pollStatus = useCallback(async (backtestId: string) => {
|
||||||
try {
|
try {
|
||||||
|
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);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
// Create backtest
|
|
||||||
const { id } = await BacktestService.createBacktest(newConfig);
|
|
||||||
|
|
||||||
setBacktestId(id);
|
|
||||||
setConfig(newConfig);
|
|
||||||
setStatus('configured');
|
|
||||||
setResults(null);
|
setResults(null);
|
||||||
setCurrentTime(null);
|
|
||||||
setProgress(0);
|
try {
|
||||||
|
const newBacktest = await backtestApi.createBacktest(request);
|
||||||
|
setBacktest(newBacktest);
|
||||||
|
|
||||||
|
// Start polling for updates
|
||||||
|
setIsPolling(true);
|
||||||
|
pollingIntervalRef.current = setInterval(() => {
|
||||||
|
pollStatus(newBacktest.id);
|
||||||
|
}, 2000); // Poll every 2 seconds
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'Failed to create backtest');
|
setError(err instanceof Error ? err.message : 'Failed to create backtest');
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
}, [pollStatus]);
|
||||||
|
|
||||||
|
// Cancel running backtest
|
||||||
|
const cancelBacktest = useCallback(async () => {
|
||||||
|
if (!backtest || backtest.status !== 'running') return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await backtestApi.cancelBacktest(backtest.id);
|
||||||
|
setBacktest({ ...backtest, status: 'cancelled' });
|
||||||
|
setIsPolling(false);
|
||||||
|
|
||||||
|
// Clear polling interval
|
||||||
|
if (pollingIntervalRef.current) {
|
||||||
|
clearInterval(pollingIntervalRef.current);
|
||||||
|
pollingIntervalRef.current = null;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to cancel backtest');
|
||||||
|
}
|
||||||
|
}, [backtest]);
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleStart = useCallback(async () => {
|
|
||||||
if (!backtestId) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
setIsLoading(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : 'Failed to stop backtest');
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}, [backtestId]);
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}, [backtestId]);
|
|
||||||
|
|
||||||
// Cleanup on unmount
|
// Cleanup on unmount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
if (cleanupRef.current) {
|
if (pollingIntervalRef.current) {
|
||||||
cleanupRef.current();
|
clearInterval(pollingIntervalRef.current);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
backtestId,
|
backtest,
|
||||||
config,
|
|
||||||
status,
|
|
||||||
results,
|
results,
|
||||||
currentTime,
|
|
||||||
progress,
|
|
||||||
error,
|
|
||||||
isLoading,
|
isLoading,
|
||||||
handleConfigSubmit,
|
isPolling,
|
||||||
handleStart,
|
error,
|
||||||
handlePause,
|
createBacktest,
|
||||||
handleResume,
|
cancelBacktest,
|
||||||
handleStop,
|
reset,
|
||||||
handleStep,
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
119
apps/stock/web-app/src/features/backtest/services/backtestApi.ts
Normal file
119
apps/stock/web-app/src/features/backtest/services/backtestApi.ts
Normal 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}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
119
bun.lock
119
bun.lock
|
|
@ -31,6 +31,7 @@
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"supertest": "^6.3.4",
|
"supertest": "^6.3.4",
|
||||||
"ts-unused-exports": "^11.0.1",
|
"ts-unused-exports": "^11.0.1",
|
||||||
|
"tsx": "^4.20.3",
|
||||||
"turbo": "^2.5.4",
|
"turbo": "^2.5.4",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"yup": "^1.6.1",
|
"yup": "^1.6.1",
|
||||||
|
|
@ -122,9 +123,11 @@
|
||||||
"@stock-bot/questdb": "*",
|
"@stock-bot/questdb": "*",
|
||||||
"@stock-bot/queue": "*",
|
"@stock-bot/queue": "*",
|
||||||
"@stock-bot/shutdown": "*",
|
"@stock-bot/shutdown": "*",
|
||||||
|
"@stock-bot/stock-config": "*",
|
||||||
"@stock-bot/utils": "*",
|
"@stock-bot/utils": "*",
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
"hono": "^4.0.0",
|
"hono": "^4.0.0",
|
||||||
|
"simple-statistics": "^7.8.3",
|
||||||
"socket.io": "^4.7.2",
|
"socket.io": "^4.7.2",
|
||||||
"socket.io-client": "^4.7.2",
|
"socket.io-client": "^4.7.2",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
|
|
@ -570,49 +573,55 @@
|
||||||
|
|
||||||
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="],
|
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="],
|
||||||
|
|
||||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
|
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA=="],
|
||||||
|
|
||||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="],
|
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.5", "", { "os": "android", "cpu": "arm" }, "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA=="],
|
||||||
|
|
||||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="],
|
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.5", "", { "os": "android", "cpu": "arm64" }, "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg=="],
|
||||||
|
|
||||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="],
|
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.5", "", { "os": "android", "cpu": "x64" }, "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw=="],
|
||||||
|
|
||||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="],
|
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ=="],
|
||||||
|
|
||||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="],
|
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ=="],
|
||||||
|
|
||||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="],
|
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw=="],
|
||||||
|
|
||||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="],
|
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw=="],
|
||||||
|
|
||||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="],
|
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.5", "", { "os": "linux", "cpu": "arm" }, "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw=="],
|
||||||
|
|
||||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="],
|
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg=="],
|
||||||
|
|
||||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="],
|
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA=="],
|
||||||
|
|
||||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="],
|
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg=="],
|
||||||
|
|
||||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="],
|
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg=="],
|
||||||
|
|
||||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="],
|
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ=="],
|
||||||
|
|
||||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="],
|
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA=="],
|
||||||
|
|
||||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="],
|
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ=="],
|
||||||
|
|
||||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="],
|
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.5", "", { "os": "linux", "cpu": "x64" }, "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw=="],
|
||||||
|
|
||||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="],
|
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.5", "", { "os": "none", "cpu": "arm64" }, "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw=="],
|
||||||
|
|
||||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="],
|
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.5", "", { "os": "none", "cpu": "x64" }, "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ=="],
|
||||||
|
|
||||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="],
|
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.5", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw=="],
|
||||||
|
|
||||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="],
|
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg=="],
|
||||||
|
|
||||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="],
|
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.5", "", { "os": "win32", "cpu": "x64" }, "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g=="],
|
||||||
|
|
||||||
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="],
|
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="],
|
||||||
|
|
||||||
|
|
@ -1400,7 +1409,7 @@
|
||||||
|
|
||||||
"es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="],
|
"es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="],
|
||||||
|
|
||||||
"esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
|
"esbuild": ["esbuild@0.25.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.5", "@esbuild/android-arm": "0.25.5", "@esbuild/android-arm64": "0.25.5", "@esbuild/android-x64": "0.25.5", "@esbuild/darwin-arm64": "0.25.5", "@esbuild/darwin-x64": "0.25.5", "@esbuild/freebsd-arm64": "0.25.5", "@esbuild/freebsd-x64": "0.25.5", "@esbuild/linux-arm": "0.25.5", "@esbuild/linux-arm64": "0.25.5", "@esbuild/linux-ia32": "0.25.5", "@esbuild/linux-loong64": "0.25.5", "@esbuild/linux-mips64el": "0.25.5", "@esbuild/linux-ppc64": "0.25.5", "@esbuild/linux-riscv64": "0.25.5", "@esbuild/linux-s390x": "0.25.5", "@esbuild/linux-x64": "0.25.5", "@esbuild/netbsd-arm64": "0.25.5", "@esbuild/netbsd-x64": "0.25.5", "@esbuild/openbsd-arm64": "0.25.5", "@esbuild/openbsd-x64": "0.25.5", "@esbuild/sunos-x64": "0.25.5", "@esbuild/win32-arm64": "0.25.5", "@esbuild/win32-ia32": "0.25.5", "@esbuild/win32-x64": "0.25.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ=="],
|
||||||
|
|
||||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||||
|
|
||||||
|
|
@ -1540,7 +1549,7 @@
|
||||||
|
|
||||||
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
|
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
|
||||||
|
|
||||||
"fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
|
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||||
|
|
||||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||||
|
|
||||||
|
|
@ -1564,6 +1573,8 @@
|
||||||
|
|
||||||
"get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="],
|
"get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="],
|
||||||
|
|
||||||
|
"get-tsconfig": ["get-tsconfig@4.10.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ=="],
|
||||||
|
|
||||||
"get-uri": ["get-uri@6.0.4", "", { "dependencies": { "basic-ftp": "^5.0.2", "data-uri-to-buffer": "^6.0.2", "debug": "^4.3.4" } }, "sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ=="],
|
"get-uri": ["get-uri@6.0.4", "", { "dependencies": { "basic-ftp": "^5.0.2", "data-uri-to-buffer": "^6.0.2", "debug": "^4.3.4" } }, "sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ=="],
|
||||||
|
|
||||||
"github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="],
|
"github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="],
|
||||||
|
|
@ -2180,6 +2191,8 @@
|
||||||
|
|
||||||
"resolve-mongodb-srv": ["resolve-mongodb-srv@1.1.5", "", { "dependencies": { "whatwg-url": "^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0" }, "bin": { "resolve-mongodb-srv": "bin/resolve-mongodb-srv.js" } }, "sha512-flu1XTSLDJHvTnWu2aJh2w9jgGPcNYJn2obMkuzXiyWSz0MLXu9IRCjvirJ4zRoCPHJJPt3uLQVNJTrzFRWd1w=="],
|
"resolve-mongodb-srv": ["resolve-mongodb-srv@1.1.5", "", { "dependencies": { "whatwg-url": "^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0" }, "bin": { "resolve-mongodb-srv": "bin/resolve-mongodb-srv.js" } }, "sha512-flu1XTSLDJHvTnWu2aJh2w9jgGPcNYJn2obMkuzXiyWSz0MLXu9IRCjvirJ4zRoCPHJJPt3uLQVNJTrzFRWd1w=="],
|
||||||
|
|
||||||
|
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||||
|
|
||||||
"responselike": ["responselike@3.0.0", "", { "dependencies": { "lowercase-keys": "^3.0.0" } }, "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg=="],
|
"responselike": ["responselike@3.0.0", "", { "dependencies": { "lowercase-keys": "^3.0.0" } }, "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg=="],
|
||||||
|
|
||||||
"ret": ["ret@0.1.15", "", {}, "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg=="],
|
"ret": ["ret@0.1.15", "", {}, "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg=="],
|
||||||
|
|
@ -2250,6 +2263,8 @@
|
||||||
|
|
||||||
"simple-oauth2": ["simple-oauth2@5.1.0", "", { "dependencies": { "@hapi/hoek": "^11.0.4", "@hapi/wreck": "^18.0.0", "debug": "^4.3.4", "joi": "^17.6.4" } }, "sha512-gWDa38Ccm4MwlG5U7AlcJxPv3lvr80dU7ARJWrGdgvOKyzSj1gr3GBPN1rABTedAYvC/LsGYoFuFxwDBPtGEbw=="],
|
"simple-oauth2": ["simple-oauth2@5.1.0", "", { "dependencies": { "@hapi/hoek": "^11.0.4", "@hapi/wreck": "^18.0.0", "debug": "^4.3.4", "joi": "^17.6.4" } }, "sha512-gWDa38Ccm4MwlG5U7AlcJxPv3lvr80dU7ARJWrGdgvOKyzSj1gr3GBPN1rABTedAYvC/LsGYoFuFxwDBPtGEbw=="],
|
||||||
|
|
||||||
|
"simple-statistics": ["simple-statistics@7.8.8", "", {}, "sha512-CUtP0+uZbcbsFpqEyvNDYjJCl+612fNgjT8GaVuvMG7tBuJg8gXGpsP5M7X658zy0IcepWOZ6nPBu1Qb9ezA1w=="],
|
||||||
|
|
||||||
"slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
|
"slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
|
||||||
|
|
||||||
"slice-ansi": ["slice-ansi@4.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ=="],
|
"slice-ansi": ["slice-ansi@4.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ=="],
|
||||||
|
|
@ -2386,6 +2401,8 @@
|
||||||
|
|
||||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"tsx": ["tsx@4.20.3", "", { "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ=="],
|
||||||
|
|
||||||
"tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
|
"tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
|
||||||
|
|
||||||
"turbo": ["turbo@2.5.4", "", { "optionalDependencies": { "turbo-darwin-64": "2.5.4", "turbo-darwin-arm64": "2.5.4", "turbo-linux-64": "2.5.4", "turbo-linux-arm64": "2.5.4", "turbo-windows-64": "2.5.4", "turbo-windows-arm64": "2.5.4" }, "bin": { "turbo": "bin/turbo" } }, "sha512-kc8ZibdRcuWUG1pbYSBFWqmIjynlD8Lp7IB6U3vIzvOv9VG+6Sp8bzyeBWE3Oi8XV5KsQrznyRTBPvrf99E4mA=="],
|
"turbo": ["turbo@2.5.4", "", { "optionalDependencies": { "turbo-darwin-64": "2.5.4", "turbo-darwin-arm64": "2.5.4", "turbo-linux-64": "2.5.4", "turbo-linux-arm64": "2.5.4", "turbo-windows-64": "2.5.4", "turbo-windows-arm64": "2.5.4" }, "bin": { "turbo": "bin/turbo" } }, "sha512-kc8ZibdRcuWUG1pbYSBFWqmIjynlD8Lp7IB6U3vIzvOv9VG+6Sp8bzyeBWE3Oi8XV5KsQrznyRTBPvrf99E4mA=="],
|
||||||
|
|
@ -2596,6 +2613,8 @@
|
||||||
|
|
||||||
"bun-types/@types/node": ["@types/node@22.15.32", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-3jigKqgSjsH6gYZv2nEsqdXfZqIFGAV36XYYjf9KGZ3PSG+IhLecqPnI310RvjutyMwifE2hhhNEklOUrvx/wA=="],
|
"bun-types/@types/node": ["@types/node@22.15.32", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-3jigKqgSjsH6gYZv2nEsqdXfZqIFGAV36XYYjf9KGZ3PSG+IhLecqPnI310RvjutyMwifE2hhhNEklOUrvx/wA=="],
|
||||||
|
|
||||||
|
"chokidar/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
|
||||||
|
|
||||||
"chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
"chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||||
|
|
||||||
"cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
"cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||||
|
|
@ -2700,6 +2719,8 @@
|
||||||
|
|
||||||
"pkg-dir/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
|
"pkg-dir/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
|
||||||
|
|
||||||
|
"playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
|
||||||
|
|
||||||
"prebuild-install/tar-fs": ["tar-fs@2.1.3", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg=="],
|
"prebuild-install/tar-fs": ["tar-fs@2.1.3", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg=="],
|
||||||
|
|
||||||
"prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
"prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
||||||
|
|
@ -2720,6 +2741,8 @@
|
||||||
|
|
||||||
"rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
"rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
||||||
|
|
||||||
|
"rollup/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
|
||||||
|
|
||||||
"run-applescript/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="],
|
"run-applescript/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="],
|
||||||
|
|
||||||
"send/mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
|
"send/mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
|
||||||
|
|
@ -2742,6 +2765,10 @@
|
||||||
|
|
||||||
"type-is/mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
|
"type-is/mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
|
||||||
|
|
||||||
|
"vite/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
|
||||||
|
|
||||||
|
"vite/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
|
||||||
|
|
||||||
"win-export-certificate-and-key/node-addon-api": ["node-addon-api@3.2.1", "", {}, "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A=="],
|
"win-export-certificate-and-key/node-addon-api": ["node-addon-api@3.2.1", "", {}, "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A=="],
|
||||||
|
|
||||||
"wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
|
"wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
|
||||||
|
|
@ -2992,6 +3019,50 @@
|
||||||
|
|
||||||
"type-is/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
"type-is/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||||
|
|
||||||
|
"vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
|
||||||
|
|
||||||
|
"vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="],
|
||||||
|
|
||||||
|
"vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="],
|
||||||
|
|
||||||
|
"vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="],
|
||||||
|
|
||||||
|
"vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="],
|
||||||
|
|
||||||
|
"vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="],
|
||||||
|
|
||||||
|
"vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="],
|
||||||
|
|
||||||
|
"vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="],
|
||||||
|
|
||||||
|
"vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="],
|
||||||
|
|
||||||
|
"vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="],
|
||||||
|
|
||||||
|
"vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="],
|
||||||
|
|
||||||
|
"vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="],
|
||||||
|
|
||||||
|
"vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="],
|
||||||
|
|
||||||
|
"vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="],
|
||||||
|
|
||||||
|
"vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="],
|
||||||
|
|
||||||
|
"vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="],
|
||||||
|
|
||||||
|
"vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="],
|
||||||
|
|
||||||
|
"vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="],
|
||||||
|
|
||||||
|
"vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="],
|
||||||
|
|
||||||
|
"vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="],
|
||||||
|
|
||||||
|
"vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="],
|
||||||
|
|
||||||
|
"vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="],
|
||||||
|
|
||||||
"wrap-ansi/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
"wrap-ansi/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
||||||
|
|
||||||
"wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
|
"wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"name": "@stock-bot/di",
|
"name": "@stock-bot/di",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"main": "./src/index.ts",
|
"main": "./dist/index.js",
|
||||||
"types": "./src/index.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"clean": "rm -rf dist",
|
"clean": "rm -rf dist",
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,7 @@
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"supertest": "^6.3.4",
|
"supertest": "^6.3.4",
|
||||||
"ts-unused-exports": "^11.0.1",
|
"ts-unused-exports": "^11.0.1",
|
||||||
|
"tsx": "^4.20.3",
|
||||||
"turbo": "^2.5.4",
|
"turbo": "^2.5.4",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"yup": "^1.6.1"
|
"yup": "^1.6.1"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue