messy work. backtests / mock-data

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

View file

@ -1,251 +1,21 @@
const { existsSync, readFileSync } = require('fs')
/* tslint:disable */
/* eslint-disable */
const { existsSync } = require('fs')
const { join } = require('path')
const { platform, arch } = process
let nativeBinding = null
let localFileExisted = false
let loadError = null
function isMusl() {
// For Node 10
if (!process.report || typeof process.report.getReport !== 'function') {
try {
const lddPath = require('child_process').execSync('which ldd 2>/dev/null', { encoding: 'utf8' })
return readFileSync(lddPath, 'utf8').includes('musl')
} catch (e) {
return true
}
// Try to load the native binding
try {
if (existsSync(join(__dirname, 'index.node'))) {
nativeBinding = require('./index.node')
} else {
const { glibcVersionRuntime } = process.report.getReport().header
return !glibcVersionRuntime
throw new Error('index.node not found')
}
} catch (e) {
throw new Error(`Failed to load native binding: ${e.message}`)
}
switch (platform) {
case 'android':
switch (arch) {
case 'arm64':
localFileExisted = existsSync(join(__dirname, 'core.android-arm64.node'))
try {
if (localFileExisted) {
nativeBinding = require('./core.android-arm64.node')
} else {
nativeBinding = require('@stock-bot/core-android-arm64')
}
} catch (e) {
loadError = e
}
break
case 'arm':
localFileExisted = existsSync(join(__dirname, 'core.android-arm-eabi.node'))
try {
if (localFileExisted) {
nativeBinding = require('./core.android-arm-eabi.node')
} else {
nativeBinding = require('@stock-bot/core-android-arm-eabi')
}
} catch (e) {
loadError = e
}
break
default:
throw new Error(`Unsupported architecture on Android ${arch}`)
}
break
case 'win32':
switch (arch) {
case 'x64':
localFileExisted = existsSync(
join(__dirname, 'core.win32-x64-msvc.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./core.win32-x64-msvc.node')
} else {
nativeBinding = require('@stock-bot/core-win32-x64-msvc')
}
} catch (e) {
loadError = e
}
break
case 'ia32':
localFileExisted = existsSync(
join(__dirname, 'core.win32-ia32-msvc.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./core.win32-ia32-msvc.node')
} else {
nativeBinding = require('@stock-bot/core-win32-ia32-msvc')
}
} catch (e) {
loadError = e
}
break
case 'arm64':
localFileExisted = existsSync(
join(__dirname, 'core.win32-arm64-msvc.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./core.win32-arm64-msvc.node')
} else {
nativeBinding = require('@stock-bot/core-win32-arm64-msvc')
}
} catch (e) {
loadError = e
}
break
default:
throw new Error(`Unsupported architecture on Windows: ${arch}`)
}
break
case 'darwin':
localFileExisted = existsSync(join(__dirname, 'core.darwin-universal.node'))
try {
if (localFileExisted) {
nativeBinding = require('./core.darwin-universal.node')
} else {
nativeBinding = require('@stock-bot/core-darwin-universal')
}
break
} catch {}
switch (arch) {
case 'x64':
localFileExisted = existsSync(join(__dirname, 'core.darwin-x64.node'))
try {
if (localFileExisted) {
nativeBinding = require('./core.darwin-x64.node')
} else {
nativeBinding = require('@stock-bot/core-darwin-x64')
}
} catch (e) {
loadError = e
}
break
case 'arm64':
localFileExisted = existsSync(
join(__dirname, 'core.darwin-arm64.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./core.darwin-arm64.node')
} else {
nativeBinding = require('@stock-bot/core-darwin-arm64')
}
} catch (e) {
loadError = e
}
break
default:
throw new Error(`Unsupported architecture on macOS: ${arch}`)
}
break
case 'freebsd':
if (arch !== 'x64') {
throw new Error(`Unsupported architecture on FreeBSD: ${arch}`)
}
localFileExisted = existsSync(join(__dirname, 'core.freebsd-x64.node'))
try {
if (localFileExisted) {
nativeBinding = require('./core.freebsd-x64.node')
} else {
nativeBinding = require('@stock-bot/core-freebsd-x64')
}
} catch (e) {
loadError = e
}
break
case 'linux':
switch (arch) {
case 'x64':
if (isMusl()) {
localFileExisted = existsSync(
join(__dirname, 'core.linux-x64-musl.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./core.linux-x64-musl.node')
} else {
nativeBinding = require('@stock-bot/core-linux-x64-musl')
}
} catch (e) {
loadError = e
}
} else {
localFileExisted = existsSync(
join(__dirname, 'core.linux-x64-gnu.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./core.linux-x64-gnu.node')
} else {
nativeBinding = require('@stock-bot/core-linux-x64-gnu')
}
} catch (e) {
loadError = e
}
}
break
case 'arm64':
if (isMusl()) {
localFileExisted = existsSync(
join(__dirname, 'core.linux-arm64-musl.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./core.linux-arm64-musl.node')
} else {
nativeBinding = require('@stock-bot/core-linux-arm64-musl')
}
} catch (e) {
loadError = e
}
} else {
localFileExisted = existsSync(
join(__dirname, 'core.linux-arm64-gnu.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./core.linux-arm64-gnu.node')
} else {
nativeBinding = require('@stock-bot/core-linux-arm64-gnu')
}
} catch (e) {
loadError = e
}
}
break
case 'arm':
localFileExisted = existsSync(
join(__dirname, 'core.linux-arm-gnueabihf.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./core.linux-arm-gnueabihf.node')
} else {
nativeBinding = require('@stock-bot/core-linux-arm-gnueabihf')
}
} catch (e) {
loadError = e
}
break
default:
throw new Error(`Unsupported architecture on Linux: ${arch}`)
}
break
default:
throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`)
}
if (!nativeBinding) {
if (loadError) {
throw loadError
}
throw new Error(`Failed to load native binding`)
}
const { TradingEngine } = nativeBinding
module.exports.TradingEngine = TradingEngine
// Export all bindings
module.exports = nativeBinding

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

@ -0,0 +1,29 @@
// ESM wrapper for the native module
import { createRequire } from 'module';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
const require = createRequire(import.meta.url);
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const nativeBinding = require(join(__dirname, 'index.node'));
export const {
TradingEngine,
MarketData,
MarketUpdate,
Order,
Fill,
Position,
RiskLimits,
RiskMetrics,
ExecutionResult,
OrderBookLevel,
OrderBookSnapshot,
MarketMicrostructure,
PositionUpdate,
RiskCheckResult
} = nativeBinding;
export default nativeBinding;

Binary file not shown.

View file

@ -1,11 +1,20 @@
{
"name": "@stock-bot/core",
"version": "1.0.0",
"main": "index.js",
"type": "module",
"main": "index.mjs",
"types": "index.d.ts",
"exports": {
".": {
"import": "./index.mjs",
"require": "./index.js",
"types": "./index.d.ts"
}
},
"files": [
"index.d.ts",
"index.js",
"index.mjs",
"index.node"
],
"napi": {

View file

@ -224,9 +224,8 @@ impl TradingEngine {
}
#[napi]
pub fn set_microstructure(&self, _symbol: String, microstructure_json: String) -> Result<()> {
let _microstructure: MarketMicrostructure = serde_json::from_str(&microstructure_json)
.map_err(|e| Error::from_reason(format!("Failed to parse microstructure: {}", e)))?;
pub fn set_microstructure(&self, symbol: String, microstructure_js: JsObject) -> Result<()> {
let microstructure = parse_microstructure(microstructure_js)?;
let _core = self.core.lock();
// Store microstructure for use in fill simulation
@ -236,10 +235,43 @@ impl TradingEngine {
#[napi]
pub fn load_historical_data(&self, data_json: String) -> Result<()> {
let _data: Vec<MarketUpdate> = serde_json::from_str(&data_json)
let data: Vec<MarketUpdate> = serde_json::from_str(&data_json)
.map_err(|e| Error::from_reason(format!("Failed to parse data: {}", e)))?;
// In real implementation, would load into historical data source
let core = self.core.lock();
// Downcast to HistoricalDataSource if in backtest mode
if let TradingMode::Backtest { .. } = core.get_mode() {
let mut data_source = core.market_data_source.write();
if let Some(historical_source) = data_source.as_any_mut().downcast_mut::<crate::core::market_data_sources::HistoricalDataSource>() {
historical_source.load_data(data);
}
}
Ok(())
}
#[napi]
pub fn generate_mock_data(&self, symbol: String, start_time: i64, end_time: i64, seed: Option<u32>) -> Result<()> {
let core = self.core.lock();
// Only available in backtest mode
if let TradingMode::Backtest { .. } = core.get_mode() {
let mut data_source = core.market_data_source.write();
if let Some(historical_source) = data_source.as_any_mut().downcast_mut::<crate::core::market_data_sources::HistoricalDataSource>() {
let start_dt = DateTime::<Utc>::from_timestamp_millis(start_time)
.ok_or_else(|| Error::from_reason("Invalid start time"))?;
let end_dt = DateTime::<Utc>::from_timestamp_millis(end_time)
.ok_or_else(|| Error::from_reason("Invalid end time"))?;
historical_source.generate_mock_data(symbol, start_dt, end_dt, seed.map(|s| s as u64));
} else {
return Err(Error::from_reason("Failed to access historical data source"));
}
} else {
return Err(Error::from_reason("Mock data generation only available in backtest mode"));
}
Ok(())
}
}
@ -323,4 +355,20 @@ fn parse_risk_limits(limits_js: JsObject) -> Result<RiskLimits> {
max_gross_exposure: limits_js.get_named_property("maxGrossExposure")?,
max_symbol_exposure: limits_js.get_named_property("maxSymbolExposure")?,
})
}
fn parse_microstructure(microstructure_js: JsObject) -> Result<MarketMicrostructure> {
let intraday_volume_profile: Vec<f64> = microstructure_js.get_named_property("intradayVolumeProfile")
.unwrap_or_else(|_| vec![1.0/24.0; 24]);
Ok(MarketMicrostructure {
symbol: microstructure_js.get_named_property("symbol")?,
avg_spread_bps: microstructure_js.get_named_property("avgSpreadBps")?,
daily_volume: microstructure_js.get_named_property("dailyVolume")?,
avg_trade_size: microstructure_js.get_named_property("avgTradeSize")?,
volatility: microstructure_js.get_named_property("volatility")?,
tick_size: microstructure_js.get_named_property("tickSize")?,
lot_size: microstructure_js.get_named_property("lotSize")?,
intraday_volume_profile,
})
}

View file

@ -2,6 +2,7 @@ use crate::{MarketDataSource, MarketUpdate};
use chrono::{DateTime, Utc};
use parking_lot::Mutex;
use std::collections::VecDeque;
use super::mock_data_generator::MockDataGenerator;
// Historical data source for backtesting
pub struct HistoricalDataSource {
@ -24,6 +25,19 @@ impl HistoricalDataSource {
queue.extend(data);
*self.current_position.lock() = 0;
}
// Generate mock data for testing
pub fn generate_mock_data(
&self,
symbol: String,
start_time: DateTime<Utc>,
end_time: DateTime<Utc>,
seed: Option<u64>
) {
let mut generator = MockDataGenerator::new(seed.unwrap_or(42));
let data = generator.generate_mixed_data(symbol, start_time, end_time);
self.load_data(data);
}
}
#[async_trait::async_trait]

View file

@ -0,0 +1,229 @@
use crate::{MarketUpdate, MarketDataType, Quote, Trade, Bar, Side};
use chrono::{DateTime, Utc, Duration};
use rand::{Rng, SeedableRng};
use rand::rngs::StdRng;
use rand_distr::{Normal, Distribution};
pub struct MockDataGenerator {
rng: StdRng,
base_price: f64,
volatility: f64,
spread_bps: f64,
volume_mean: f64,
volume_std: f64,
}
impl MockDataGenerator {
pub fn new(seed: u64) -> Self {
Self {
rng: StdRng::seed_from_u64(seed),
base_price: 100.0,
volatility: 0.02, // 2% daily volatility
spread_bps: 5.0, // 5 basis points spread
volume_mean: 1_000_000.0,
volume_std: 200_000.0,
}
}
pub fn with_params(seed: u64, base_price: f64, volatility: f64, spread_bps: f64) -> Self {
Self {
rng: StdRng::seed_from_u64(seed),
base_price,
volatility,
spread_bps,
volume_mean: 1_000_000.0,
volume_std: 200_000.0,
}
}
pub fn generate_quotes(
&mut self,
symbol: String,
start_time: DateTime<Utc>,
end_time: DateTime<Utc>,
interval_ms: i64,
) -> Vec<MarketUpdate> {
let mut updates = Vec::new();
let mut current_time = start_time;
let mut price = self.base_price;
let price_dist = Normal::new(0.0, self.volatility).unwrap();
let volume_dist = Normal::new(self.volume_mean, self.volume_std).unwrap();
while current_time <= end_time {
// Generate price movement
let return_pct = price_dist.sample(&mut self.rng) / 100.0;
price *= 1.0 + return_pct;
price = price.max(0.01); // Ensure positive price
// Calculate bid/ask
let half_spread = price * self.spread_bps / 20000.0;
let bid = price - half_spread;
let ask = price + half_spread;
// Generate volume
let volume = volume_dist.sample(&mut self.rng).max(0.0) as u32;
updates.push(MarketUpdate {
symbol: symbol.clone(),
timestamp: current_time,
data: MarketDataType::Quote(Quote {
bid,
ask,
bid_size: (volume / 10) as f64,
ask_size: (volume / 10) as f64,
}),
});
current_time = current_time + Duration::milliseconds(interval_ms);
}
updates
}
pub fn generate_trades(
&mut self,
symbol: String,
start_time: DateTime<Utc>,
end_time: DateTime<Utc>,
trades_per_minute: u32,
) -> Vec<MarketUpdate> {
let mut updates = Vec::new();
let mut current_time = start_time;
let mut price = self.base_price;
let price_dist = Normal::new(0.0, self.volatility / 100.0).unwrap();
let volume_dist = Normal::new(100.0, 50.0).unwrap();
let interval_ms = 60_000 / trades_per_minute as i64;
while current_time <= end_time {
// Generate price movement
let return_pct = price_dist.sample(&mut self.rng);
price *= 1.0 + return_pct;
price = price.max(0.01);
// Generate trade size
let raw_size: f64 = volume_dist.sample(&mut self.rng);
let size = raw_size.max(1.0) as u32;
// Random buy/sell
let is_buy = self.rng.gen_bool(0.5);
updates.push(MarketUpdate {
symbol: symbol.clone(),
timestamp: current_time,
data: MarketDataType::Trade(Trade {
price,
size: size as f64,
side: if is_buy { Side::Buy } else { Side::Sell },
}),
});
current_time = current_time + Duration::milliseconds(interval_ms);
}
updates
}
pub fn generate_bars(
&mut self,
symbol: String,
start_time: DateTime<Utc>,
end_time: DateTime<Utc>,
timeframe: &str,
) -> Vec<MarketUpdate> {
let mut updates = Vec::new();
let mut current_time = start_time;
let mut price = self.base_price;
let interval = match timeframe {
"1m" => Duration::minutes(1),
"5m" => Duration::minutes(5),
"15m" => Duration::minutes(15),
"1h" => Duration::hours(1),
"1d" => Duration::days(1),
_ => Duration::minutes(1),
};
let price_dist = Normal::new(0.0, self.volatility).unwrap();
let volume_dist = Normal::new(self.volume_mean, self.volume_std).unwrap();
while current_time <= end_time {
// Generate OHLC
let open = price;
let mut high = open;
let mut low = open;
// Simulate intrabar movements
for _ in 0..4 {
let move_pct = price_dist.sample(&mut self.rng) / 100.0;
price *= 1.0 + move_pct;
price = price.max(0.01);
high = high.max(price);
low = low.min(price);
}
let close = price;
let volume = volume_dist.sample(&mut self.rng).max(0.0) as u64;
updates.push(MarketUpdate {
symbol: symbol.clone(),
timestamp: current_time,
data: MarketDataType::Bar(Bar {
open,
high,
low,
close,
volume: volume as f64,
vwap: Some((open + high + low + close) / 4.0),
}),
});
current_time = current_time + interval;
}
updates
}
pub fn generate_mixed_data(
&mut self,
symbol: String,
start_time: DateTime<Utc>,
end_time: DateTime<Utc>,
) -> Vec<MarketUpdate> {
let mut all_updates = Vec::new();
// Generate quotes every 100ms
let quotes = self.generate_quotes(
symbol.clone(),
start_time,
end_time,
100
);
all_updates.extend(quotes);
// Generate trades
let trades = self.generate_trades(
symbol.clone(),
start_time,
end_time,
20 // 20 trades per minute
);
all_updates.extend(trades);
// Generate 1-minute bars
let bars = self.generate_bars(
symbol,
start_time,
end_time,
"1m"
);
all_updates.extend(bars);
// Sort by timestamp
all_updates.sort_by_key(|update| update.timestamp);
all_updates
}
}

View file

@ -2,6 +2,7 @@ pub mod time_providers;
pub mod market_data_sources;
pub mod execution_handlers;
pub mod market_microstructure;
pub mod mock_data_generator;
use crate::{MarketDataSource, ExecutionHandler, TimeProvider, TradingMode};