work on new engine

This commit is contained in:
Boki 2025-07-04 11:24:27 -04:00
parent 44476da13f
commit a1e5a21847
126 changed files with 3425 additions and 6695 deletions

View file

@ -0,0 +1,469 @@
use napi::bindgen_prelude::*;
use napi_derive::napi;
use std::sync::Arc;
use parking_lot::Mutex;
use crate::backtest::{
BacktestEngine as RustBacktestEngine,
BacktestConfig,
Strategy, Signal,
};
use crate::{TradingMode, MarketUpdate};
use chrono::{DateTime, Utc};
#[napi]
pub struct BacktestEngine {
inner: Arc<Mutex<Option<RustBacktestEngine>>>,
}
#[napi]
impl BacktestEngine {
#[napi(constructor)]
pub fn new(config: napi::JsObject, env: Env) -> Result<Self> {
let config = parse_backtest_config(config)?;
// Create mode
let mode = TradingMode::Backtest {
start_time: config.start_time,
end_time: config.end_time,
speed_multiplier: 0.0, // Max speed
};
// Create components
let time_provider = crate::core::create_time_provider(&mode);
let market_data_source = crate::core::create_market_data_source(&mode);
let execution_handler = crate::core::create_execution_handler(&mode);
let engine = RustBacktestEngine::new(
config,
mode,
time_provider,
market_data_source,
execution_handler,
);
Ok(Self {
inner: Arc::new(Mutex::new(Some(engine))),
})
}
#[napi]
pub fn add_typescript_strategy(
&mut self,
name: String,
id: String,
parameters: napi::JsObject,
_callback: napi::JsFunction,
) -> Result<()> {
eprintln!("Adding strategy: {}", name);
// For now, we'll add a native Rust SMA strategy
// In the future, we'll implement proper TypeScript callback support
let fast_period: usize = parameters.get_named_property::<f64>("fastPeriod")
.unwrap_or(5.0) as usize;
let slow_period: usize = parameters.get_named_property::<f64>("slowPeriod")
.unwrap_or(15.0) as usize;
if let Some(engine) = self.inner.lock().as_mut() {
engine.add_strategy(Box::new(SimpleSMAStrategy::new(
name.clone(),
id,
fast_period,
slow_period,
)));
eprintln!("Strategy '{}' added with fast={}, slow={}", name, fast_period, slow_period);
}
Ok(())
}
#[napi]
pub fn add_native_strategy(
&mut self,
strategy_type: String,
name: String,
id: String,
parameters: napi::JsObject,
) -> Result<()> {
eprintln!("Adding native Rust strategy: {} ({})", name, strategy_type);
if let Some(engine) = self.inner.lock().as_mut() {
match strategy_type.as_str() {
"sma_crossover" => {
let fast_period: usize = parameters.get_named_property::<f64>("fastPeriod")
.unwrap_or(5.0) as usize;
let slow_period: usize = parameters.get_named_property::<f64>("slowPeriod")
.unwrap_or(15.0) as usize;
engine.add_strategy(Box::new(SimpleSMAStrategy::new(
name.clone(),
id,
fast_period,
slow_period,
)));
}
"mean_reversion" => {
let lookback_period: usize = parameters.get_named_property::<f64>("lookbackPeriod")
.unwrap_or(20.0) as usize;
let entry_threshold: f64 = parameters.get_named_property::<f64>("entryThreshold")
.unwrap_or(2.0);
let position_size: f64 = parameters.get_named_property::<f64>("positionSize")
.unwrap_or(100.0);
engine.add_strategy(Box::new(crate::strategies::MeanReversionFixedStrategy::new(
name.clone(),
id,
lookback_period,
entry_threshold,
position_size,
)));
}
"momentum" => {
let lookback_period: usize = parameters.get_named_property::<f64>("lookbackPeriod")
.unwrap_or(14.0) as usize;
let momentum_threshold: f64 = parameters.get_named_property::<f64>("momentumThreshold")
.unwrap_or(5.0);
let position_size: f64 = parameters.get_named_property::<f64>("positionSize")
.unwrap_or(100.0);
engine.add_strategy(Box::new(crate::strategies::MomentumStrategy::new(
name.clone(),
id,
lookback_period,
momentum_threshold,
position_size,
)));
}
"pairs_trading" => {
let pair_a: String = parameters.get_named_property::<String>("pairA")?;
let pair_b: String = parameters.get_named_property::<String>("pairB")?;
let lookback_period: usize = parameters.get_named_property::<f64>("lookbackPeriod")
.unwrap_or(20.0) as usize;
let entry_threshold: f64 = parameters.get_named_property::<f64>("entryThreshold")
.unwrap_or(2.0);
let position_size: f64 = parameters.get_named_property::<f64>("positionSize")
.unwrap_or(100.0);
engine.add_strategy(Box::new(crate::strategies::PairsTradingStrategy::new(
name.clone(),
id,
pair_a,
pair_b,
lookback_period,
entry_threshold,
position_size,
)));
}
_ => {
return Err(Error::from_reason(format!("Unknown strategy type: {}", strategy_type)));
}
}
eprintln!("Native strategy '{}' added successfully", name);
}
Ok(())
}
#[napi]
pub fn run(&mut self) -> Result<String> {
eprintln!("=== BACKTEST RUN START ===");
let mut engine = self.inner.lock().take()
.ok_or_else(|| Error::from_reason("Engine already consumed"))?;
// Config and strategies are private, skip detailed logging
// Run the backtest synchronously for now
let runtime = tokio::runtime::Runtime::new()
.map_err(|e| Error::from_reason(e.to_string()))?;
let result = runtime.block_on(engine.run())
.map_err(|e| {
eprintln!("ERROR: Backtest engine failed: {}", e);
Error::from_reason(e)
})?;
eprintln!("=== BACKTEST RUN COMPLETE ===");
eprintln!("Total trades: {}", result.trades.len());
eprintln!("Equity points: {}", result.equity.len());
// Return result as JSON
serde_json::to_string(&result)
.map_err(|e| Error::from_reason(e.to_string()))
}
#[napi]
pub fn load_market_data(&self, data: Vec<napi::JsObject>) -> Result<()> {
eprintln!("load_market_data called with {} items", data.len());
// Convert JS objects to MarketData
let market_data: Vec<MarketUpdate> = data.into_iter()
.filter_map(|obj| parse_market_data(obj).ok())
.collect();
eprintln!("Parsed {} valid market data items", market_data.len());
// Load data into the historical data source
if let Some(engine) = self.inner.lock().as_ref() {
// Access the market data source through the engine
let mut data_source = engine.market_data_source.write();
if let Some(historical_source) = data_source.as_any_mut()
.downcast_mut::<crate::core::market_data_sources::HistoricalDataSource>() {
eprintln!("Loading data into HistoricalDataSource");
historical_source.load_data(market_data);
eprintln!("Data loaded successfully");
} else {
eprintln!("ERROR: Could not downcast to HistoricalDataSource");
}
} else {
eprintln!("ERROR: Engine not found");
}
Ok(())
}
}
fn parse_backtest_config(obj: napi::JsObject) -> Result<BacktestConfig> {
let name: String = obj.get_named_property("name")?;
let symbols: Vec<String> = obj.get_named_property("symbols")?;
let start_date: String = obj.get_named_property("startDate")?;
let end_date: String = obj.get_named_property("endDate")?;
let initial_capital: f64 = obj.get_named_property("initialCapital")?;
let commission: f64 = obj.get_named_property("commission")?;
let slippage: f64 = obj.get_named_property("slippage")?;
let data_frequency: String = obj.get_named_property("dataFrequency")?;
let strategy: Option<String> = obj.get_named_property("strategy").ok();
Ok(BacktestConfig {
name,
strategy,
symbols,
start_time: DateTime::parse_from_rfc3339(&start_date)
.map_err(|e| Error::from_reason(e.to_string()))?
.with_timezone(&Utc),
end_time: DateTime::parse_from_rfc3339(&end_date)
.map_err(|e| Error::from_reason(e.to_string()))?
.with_timezone(&Utc),
initial_capital,
commission,
slippage,
data_frequency,
})
}
fn parse_market_data(obj: napi::JsObject) -> Result<crate::MarketUpdate> {
let symbol: String = obj.get_named_property("symbol")?;
let timestamp: i64 = obj.get_named_property("timestamp")?;
let data_type: String = obj.get_named_property("type")?;
let data = if data_type == "bar" {
crate::MarketDataType::Bar(crate::Bar {
open: obj.get_named_property("open")?,
high: obj.get_named_property("high")?,
low: obj.get_named_property("low")?,
close: obj.get_named_property("close")?,
volume: obj.get_named_property("volume")?,
vwap: obj.get_named_property("vwap").ok(),
})
} else {
eprintln!("Unsupported market data type: {}", data_type);
return Err(Error::from_reason("Unsupported market data type"));
};
// First few items
static mut COUNT: usize = 0;
unsafe {
if COUNT < 3 {
eprintln!("Parsed market data: symbol={}, timestamp={}, close={}",
symbol, timestamp,
if let crate::MarketDataType::Bar(ref bar) = data { bar.close } else { 0.0 });
COUNT += 1;
}
}
Ok(crate::MarketUpdate {
symbol,
timestamp: DateTime::<Utc>::from_timestamp(timestamp / 1000, 0)
.ok_or_else(|| Error::from_reason("Invalid timestamp"))?,
data,
})
}
// Simple SMA Strategy for testing
struct SimpleSMAStrategy {
name: String,
id: String,
fast_period: usize,
slow_period: usize,
price_history: std::collections::HashMap<String, Vec<f64>>,
positions: std::collections::HashMap<String, f64>,
}
impl SimpleSMAStrategy {
fn new(name: String, id: String, fast_period: usize, slow_period: usize) -> Self {
eprintln!("Creating SimpleSMAStrategy: name={}, fast={}, slow={}", name, fast_period, slow_period);
Self {
name,
id,
fast_period,
slow_period,
price_history: std::collections::HashMap::new(),
positions: std::collections::HashMap::new(),
}
}
}
impl Strategy for SimpleSMAStrategy {
fn on_market_data(&mut self, data: &MarketUpdate) -> Vec<Signal> {
// Count calls
static mut CALL_COUNT: usize = 0;
unsafe {
CALL_COUNT += 1;
if CALL_COUNT % 100 == 1 {
eprintln!("SimpleSMAStrategy.on_market_data called {} times", CALL_COUNT);
}
}
let mut signals = Vec::new();
// Check if it's bar data
if let crate::MarketDataType::Bar(bar) = &data.data {
let symbol = &data.symbol;
let price = bar.close;
// Update price history
let history = self.price_history.entry(symbol.clone()).or_insert_with(Vec::new);
history.push(price);
// Debug: Log first few prices
if history.len() <= 3 {
eprintln!("Price history for {}: {:?}", symbol, history);
} else if history.len() == 10 || history.len() == 15 {
eprintln!("Price history length for {}: {} bars", symbol, history.len());
}
// Keep only necessary history (need one extra for previous SMA calculation)
if history.len() > self.slow_period + 1 {
history.remove(0);
}
// Need enough data
if history.len() >= self.slow_period {
// Debug when we first have enough data
if history.len() == self.slow_period {
eprintln!("Now have enough data for {}: {} bars", symbol, history.len());
}
// Calculate SMAs
let fast_sma = history[history.len() - self.fast_period..].iter().sum::<f64>() / self.fast_period as f64;
let slow_sma = history.iter().sum::<f64>() / history.len() as f64;
// Debug: Log SMAs periodically
if history.len() % 10 == 0 || (history.len() > self.slow_period && history.len() < self.slow_period + 5) {
eprintln!("SMAs for {}: fast={:.2}, slow={:.2}, price={:.2}, history_len={}",
symbol, fast_sma, slow_sma, price, history.len());
// Also log if they're close to crossing
let diff = (fast_sma - slow_sma).abs();
let pct_diff = diff / slow_sma * 100.0;
if pct_diff < 1.0 {
eprintln!(" -> SMAs are close! Difference: {:.4} ({:.2}%)", diff, pct_diff);
}
}
// Previous SMAs (if we have enough history)
if history.len() > self.slow_period {
// Debug: First time checking for crossovers
if history.len() == self.slow_period + 1 {
eprintln!("Starting crossover checks for {}", symbol);
}
let prev_history = &history[..history.len() - 1];
let prev_fast_sma = prev_history[prev_history.len() - self.fast_period..].iter().sum::<f64>() / self.fast_period as f64;
let prev_slow_sma = prev_history.iter().sum::<f64>() / prev_history.len() as f64;
let current_position = self.positions.get(symbol).copied().unwrap_or(0.0);
// Golden cross - buy signal
if prev_fast_sma <= prev_slow_sma && fast_sma > slow_sma && current_position <= 0.0 {
signals.push(Signal {
symbol: symbol.clone(),
signal_type: crate::backtest::SignalType::Buy,
strength: 1.0,
quantity: Some(100.0), // Fixed quantity for testing
reason: Some("Golden cross".to_string()),
metadata: None,
});
self.positions.insert(symbol.clone(), 1.0);
eprintln!("Generated BUY signal for {} at price {}", symbol, price);
}
// Death cross - sell signal
else if prev_fast_sma >= prev_slow_sma && fast_sma < slow_sma && current_position >= 0.0 {
signals.push(Signal {
symbol: symbol.clone(),
signal_type: crate::backtest::SignalType::Sell,
strength: 1.0,
quantity: Some(100.0), // Fixed quantity for testing
reason: Some("Death cross".to_string()),
metadata: None,
});
self.positions.insert(symbol.clone(), -1.0);
eprintln!("Generated SELL signal for {} at price {}", symbol, price);
}
}
} else {
// Debug: Log when we don't have enough data
if history.len() == 1 || history.len() == 10 || history.len() == 20 {
eprintln!("Not enough data for {}: {} bars (need {})", symbol, history.len(), self.slow_period);
}
}
}
signals
}
fn on_fill(&mut self, symbol: &str, quantity: f64, price: f64, side: &str) {
eprintln!("🔸 SMA Strategy - Fill received: {} {} @ ${:.2} - {}", quantity, symbol, price, side);
let current_pos = self.positions.get(symbol).copied().unwrap_or(0.0);
let new_pos = if side == "buy" { current_pos + quantity } else { current_pos - quantity };
eprintln!(" Position change: {} -> {}", current_pos, new_pos);
if new_pos.abs() < 0.0001 {
self.positions.remove(symbol);
eprintln!(" Position closed");
} else {
self.positions.insert(symbol.to_string(), new_pos);
}
}
fn get_name(&self) -> &str {
&self.name
}
fn get_parameters(&self) -> serde_json::Value {
serde_json::json!({
"fast_period": self.fast_period,
"slow_period": self.slow_period
})
}
}
// Error handling for threadsafe functions
struct ErrorStrategy;
impl From<napi::Error> for ErrorStrategy {
fn from(_e: napi::Error) -> Self {
ErrorStrategy
}
}
// Helper to convert NAPI parameters to JSON
fn napi_params_to_json(obj: napi::JsObject) -> Result<serde_json::Value> {
// For now, just extract the common parameters
let fast_period = obj.get_named_property::<f64>("fastPeriod").unwrap_or(5.0);
let slow_period = obj.get_named_property::<f64>("slowPeriod").unwrap_or(15.0);
Ok(serde_json::json!({
"fastPeriod": fast_period,
"slowPeriod": slow_period
}))
}

View file

@ -0,0 +1,243 @@
use napi_derive::napi;
use napi::{bindgen_prelude::*};
use serde_json;
use crate::indicators::{
SMA, EMA, RSI, MACD, BollingerBands, Stochastic, ATR,
Indicator, IncrementalIndicator
};
/// Convert JS array to Vec<f64>
fn js_array_to_vec(arr: Vec<f64>) -> Vec<f64> {
arr
}
#[napi]
pub struct TechnicalIndicators {}
#[napi]
impl TechnicalIndicators {
#[napi(constructor)]
pub fn new() -> Self {
Self {}
}
/// Calculate Simple Moving Average
#[napi]
pub fn calculate_sma(&self, values: Vec<f64>, period: u32) -> Result<Vec<f64>> {
match SMA::calculate_series(&values, period as usize) {
Ok(result) => Ok(result),
Err(e) => Err(Error::from_reason(e.to_string())),
}
}
/// Calculate Exponential Moving Average
#[napi]
pub fn calculate_ema(&self, values: Vec<f64>, period: u32) -> Result<Vec<f64>> {
match EMA::calculate_series(&values, period as usize) {
Ok(result) => Ok(result),
Err(e) => Err(Error::from_reason(e.to_string())),
}
}
/// Calculate Relative Strength Index
#[napi]
pub fn calculate_rsi(&self, values: Vec<f64>, period: u32) -> Result<Vec<f64>> {
match RSI::calculate_series(&values, period as usize) {
Ok(result) => Ok(result),
Err(e) => Err(Error::from_reason(e.to_string())),
}
}
/// Calculate MACD - returns JSON string
#[napi]
pub fn calculate_macd(
&self,
values: Vec<f64>,
fast_period: u32,
slow_period: u32,
signal_period: u32
) -> Result<String> {
match MACD::calculate_series(&values, fast_period as usize, slow_period as usize, signal_period as usize) {
Ok((macd, signal, histogram)) => {
let result = serde_json::json!({
"macd": macd,
"signal": signal,
"histogram": histogram
});
Ok(result.to_string())
}
Err(e) => Err(Error::from_reason(e.to_string())),
}
}
/// Calculate Bollinger Bands - returns JSON string
#[napi]
pub fn calculate_bollinger_bands(
&self,
values: Vec<f64>,
period: u32,
std_dev: f64
) -> Result<String> {
match BollingerBands::calculate_series(&values, period as usize, std_dev) {
Ok((middle, upper, lower)) => {
let result = serde_json::json!({
"middle": middle,
"upper": upper,
"lower": lower
});
Ok(result.to_string())
}
Err(e) => Err(Error::from_reason(e.to_string())),
}
}
/// Calculate Stochastic Oscillator - returns JSON string
#[napi]
pub fn calculate_stochastic(
&self,
high: Vec<f64>,
low: Vec<f64>,
close: Vec<f64>,
k_period: u32,
d_period: u32,
smooth_k: u32
) -> Result<String> {
match Stochastic::calculate_series(
&high,
&low,
&close,
k_period as usize,
d_period as usize,
smooth_k as usize
) {
Ok((k, d)) => {
let result = serde_json::json!({
"k": k,
"d": d
});
Ok(result.to_string())
}
Err(e) => Err(Error::from_reason(e.to_string())),
}
}
/// Calculate Average True Range
#[napi]
pub fn calculate_atr(
&self,
high: Vec<f64>,
low: Vec<f64>,
close: Vec<f64>,
period: u32
) -> Result<Vec<f64>> {
match ATR::calculate_series(&high, &low, &close, period as usize) {
Ok(result) => Ok(result),
Err(e) => Err(Error::from_reason(e.to_string())),
}
}
}
/// Incremental indicator calculator for streaming data
#[napi]
pub struct IncrementalSMA {
indicator: SMA,
}
#[napi]
impl IncrementalSMA {
#[napi(constructor)]
pub fn new(period: u32) -> Result<Self> {
match SMA::new(period as usize) {
Ok(indicator) => Ok(Self { indicator }),
Err(e) => Err(Error::from_reason(e.to_string())),
}
}
#[napi]
pub fn update(&mut self, value: f64) -> Result<Option<f64>> {
match self.indicator.update(value) {
Ok(result) => Ok(result),
Err(e) => Err(Error::from_reason(e.to_string())),
}
}
#[napi]
pub fn current(&self) -> Option<f64> {
self.indicator.current()
}
#[napi]
pub fn reset(&mut self) {
self.indicator.reset();
}
#[napi]
pub fn is_ready(&self) -> bool {
self.indicator.is_ready()
}
}
/// Incremental EMA calculator
#[napi]
pub struct IncrementalEMA {
indicator: EMA,
}
#[napi]
impl IncrementalEMA {
#[napi(constructor)]
pub fn new(period: u32) -> Result<Self> {
match EMA::new(period as usize) {
Ok(indicator) => Ok(Self { indicator }),
Err(e) => Err(Error::from_reason(e.to_string())),
}
}
#[napi]
pub fn update(&mut self, value: f64) -> Result<Option<f64>> {
match self.indicator.update(value) {
Ok(result) => Ok(result),
Err(e) => Err(Error::from_reason(e.to_string())),
}
}
#[napi]
pub fn current(&self) -> Option<f64> {
self.indicator.current()
}
#[napi]
pub fn reset(&mut self) {
self.indicator.reset();
}
}
/// Incremental RSI calculator
#[napi]
pub struct IncrementalRSI {
indicator: RSI,
}
#[napi]
impl IncrementalRSI {
#[napi(constructor)]
pub fn new(period: u32) -> Result<Self> {
match RSI::new(period as usize) {
Ok(indicator) => Ok(Self { indicator }),
Err(e) => Err(Error::from_reason(e.to_string())),
}
}
#[napi]
pub fn update(&mut self, value: f64) -> Result<Option<f64>> {
match self.indicator.update(value) {
Ok(result) => Ok(result),
Err(e) => Err(Error::from_reason(e.to_string())),
}
}
#[napi]
pub fn current(&self) -> Option<f64> {
self.indicator.current()
}
}

View file

@ -0,0 +1,435 @@
mod indicators;
mod risk;
mod backtest;
pub use indicators::{TechnicalIndicators, IncrementalSMA, IncrementalEMA, IncrementalRSI};
pub use risk::{RiskAnalyzer, OrderbookAnalyzer};
pub use backtest::BacktestEngine;
use napi_derive::napi;
use napi::{bindgen_prelude::*, JsObject};
use crate::{
TradingCore, TradingMode, Order, OrderType, TimeInForce, Side,
MarketUpdate, Quote, Trade,
MarketMicrostructure,
core::{create_market_data_source, create_execution_handler, create_time_provider},
};
use crate::risk::RiskLimits;
use std::sync::Arc;
use parking_lot::Mutex;
use chrono::{DateTime, Utc};
#[napi]
pub struct TradingEngine {
core: Arc<Mutex<TradingCore>>,
}
#[napi]
impl TradingEngine {
#[napi(constructor)]
pub fn new(mode: String, config: JsObject) -> Result<Self> {
let mode = parse_mode(&mode, config)?;
let market_data_source = create_market_data_source(&mode);
let execution_handler = create_execution_handler(&mode);
let time_provider = create_time_provider(&mode);
let core = TradingCore::new(mode, market_data_source, execution_handler, time_provider);
Ok(Self {
core: Arc::new(Mutex::new(core)),
})
}
#[napi]
pub fn get_mode(&self) -> String {
let core = self.core.lock();
match core.get_mode() {
TradingMode::Backtest { .. } => "backtest".to_string(),
TradingMode::Paper { .. } => "paper".to_string(),
TradingMode::Live { .. } => "live".to_string(),
}
}
#[napi]
pub fn get_current_time(&self) -> i64 {
let core = self.core.lock();
core.get_time().timestamp_millis()
}
#[napi]
pub fn submit_order(&self, order_js: JsObject) -> Result<String> {
let order = parse_order(order_js)?;
// For now, return a mock result - in real implementation would queue the order
let result = crate::ExecutionResult {
order_id: order.id.clone(),
status: crate::OrderStatus::Accepted,
fills: vec![],
};
Ok(serde_json::to_string(&result).unwrap())
}
#[napi]
pub fn check_risk(&self, order_js: JsObject) -> Result<String> {
let order = parse_order(order_js)?;
let core = self.core.lock();
// Get current position for the symbol
let position = core.position_tracker.get_position(&order.symbol);
let current_quantity = position.map(|p| p.quantity);
let result = core.risk_engine.check_order(&order, current_quantity);
Ok(serde_json::to_string(&result).unwrap())
}
#[napi]
pub fn update_quote(&self, symbol: String, bid: f64, ask: f64, bid_size: f64, ask_size: f64) -> Result<()> {
let quote = Quote { bid, ask, bid_size, ask_size };
let core = self.core.lock();
let timestamp = core.get_time();
core.orderbooks.update_quote(&symbol, quote, timestamp);
// Update unrealized P&L
let mid_price = (bid + ask) / 2.0;
core.position_tracker.update_unrealized_pnl(&symbol, mid_price);
Ok(())
}
#[napi]
pub fn update_trade(&self, symbol: String, price: f64, size: f64, side: String) -> Result<()> {
let side = match side.as_str() {
"buy" | "Buy" => Side::Buy,
"sell" | "Sell" => Side::Sell,
_ => return Err(Error::from_reason("Invalid side")),
};
let trade = Trade { price, size, side };
let core = self.core.lock();
let timestamp = core.get_time();
core.orderbooks.update_trade(&symbol, trade, timestamp);
Ok(())
}
#[napi]
pub fn get_orderbook_snapshot(&self, symbol: String, depth: u32) -> Result<String> {
let core = self.core.lock();
let snapshot = core.orderbooks.get_snapshot(&symbol, depth as usize)
.ok_or_else(|| Error::from_reason("Symbol not found"))?;
Ok(serde_json::to_string(&snapshot).unwrap())
}
#[napi]
pub fn get_best_bid_ask(&self, symbol: String) -> Result<Vec<f64>> {
let core = self.core.lock();
let (bid, ask) = core.orderbooks.get_best_bid_ask(&symbol)
.ok_or_else(|| Error::from_reason("Symbol not found"))?;
Ok(vec![bid, ask])
}
#[napi]
pub fn get_position(&self, symbol: String) -> Result<Option<String>> {
let core = self.core.lock();
let position = core.position_tracker.get_position(&symbol);
Ok(position.map(|p| serde_json::to_string(&p).unwrap()))
}
#[napi]
pub fn get_all_positions(&self) -> Result<String> {
let core = self.core.lock();
let positions = core.position_tracker.get_all_positions();
Ok(serde_json::to_string(&positions).unwrap())
}
#[napi]
pub fn get_open_positions(&self) -> Result<String> {
let core = self.core.lock();
let positions = core.position_tracker.get_open_positions();
Ok(serde_json::to_string(&positions).unwrap())
}
#[napi]
pub fn get_total_pnl(&self) -> Result<Vec<f64>> {
let core = self.core.lock();
let (realized, unrealized) = core.position_tracker.get_total_pnl();
Ok(vec![realized, unrealized])
}
#[napi]
pub fn process_fill(&self, symbol: String, price: f64, quantity: f64, side: String, commission: f64) -> Result<String> {
self.process_fill_with_metadata(symbol, price, quantity, side, commission, None, None)
}
#[napi]
pub fn process_fill_with_metadata(
&self,
symbol: String,
price: f64,
quantity: f64,
side: String,
commission: f64,
order_id: Option<String>,
strategy_id: Option<String>
) -> Result<String> {
let side = match side.as_str() {
"buy" | "Buy" => Side::Buy,
"sell" | "Sell" => Side::Sell,
_ => return Err(Error::from_reason("Invalid side")),
};
let core = self.core.lock();
let timestamp = core.get_time();
let fill = crate::Fill {
timestamp,
price,
quantity,
commission,
};
let update = core.position_tracker.process_fill_with_tracking(&symbol, &fill, side, order_id, strategy_id);
// Update risk engine with new position
core.risk_engine.update_position(&symbol, update.resulting_position.quantity);
// Update daily P&L
if update.resulting_position.realized_pnl != 0.0 {
core.risk_engine.update_daily_pnl(update.resulting_position.realized_pnl);
}
Ok(serde_json::to_string(&update).unwrap())
}
#[napi]
pub fn update_risk_limits(&self, limits_js: JsObject) -> Result<()> {
let limits = parse_risk_limits(limits_js)?;
let core = self.core.lock();
core.risk_engine.update_limits(limits);
Ok(())
}
#[napi]
pub fn reset_daily_metrics(&self) -> Result<()> {
let core = self.core.lock();
core.risk_engine.reset_daily_metrics();
Ok(())
}
#[napi]
pub fn get_risk_metrics(&self) -> Result<String> {
let core = self.core.lock();
let metrics = core.risk_engine.get_risk_metrics();
Ok(serde_json::to_string(&metrics).unwrap())
}
// Backtest-specific methods
#[napi]
pub fn advance_time(&self, to_timestamp: i64) -> Result<()> {
let core = self.core.lock();
if let TradingMode::Backtest { .. } = core.get_mode() {
// Downcast time provider to SimulatedTime and advance it
if let Some(simulated_time) = core.time_provider.as_any().downcast_ref::<crate::core::time_providers::SimulatedTime>() {
let new_time = DateTime::<Utc>::from_timestamp_millis(to_timestamp)
.ok_or_else(|| Error::from_reason("Invalid timestamp"))?;
simulated_time.advance_to(new_time);
Ok(())
} else {
Err(Error::from_reason("Failed to access simulated time provider"))
}
} else {
Err(Error::from_reason("Can only advance time in backtest mode"))
}
}
#[napi]
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
// In real implementation, would pass to execution handler
Ok(())
}
#[napi]
pub fn load_historical_data(&self, data_json: String) -> Result<()> {
let data: Vec<MarketUpdate> = serde_json::from_str(&data_json)
.map_err(|e| Error::from_reason(format!("Failed to parse data: {}", e)))?;
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(())
}
#[napi]
pub fn get_trade_history(&self) -> Result<String> {
let core = self.core.lock();
let trades = core.position_tracker.get_trade_history();
Ok(serde_json::to_string(&trades).unwrap())
}
#[napi]
pub fn get_closed_trades(&self) -> Result<String> {
let core = self.core.lock();
let trades = core.position_tracker.get_closed_trades();
Ok(serde_json::to_string(&trades).unwrap())
}
#[napi]
pub fn get_open_trades(&self) -> Result<String> {
let core = self.core.lock();
let trades = core.position_tracker.get_open_trades();
Ok(serde_json::to_string(&trades).unwrap())
}
#[napi]
pub fn get_trade_count(&self) -> Result<u32> {
let core = self.core.lock();
Ok(core.position_tracker.get_trade_count() as u32)
}
#[napi]
pub fn get_closed_trade_count(&self) -> Result<u32> {
let core = self.core.lock();
Ok(core.position_tracker.get_closed_trade_count() as u32)
}
}
// Helper functions to parse JavaScript objects
fn parse_mode(mode_str: &str, config: JsObject) -> Result<TradingMode> {
match mode_str {
"backtest" => {
let start_time: i64 = config.get_named_property("startTime")?;
let end_time: i64 = config.get_named_property("endTime")?;
let speed_multiplier: f64 = config.get_named_property("speedMultiplier")
.unwrap_or(1.0);
Ok(TradingMode::Backtest {
start_time: DateTime::<Utc>::from_timestamp_millis(start_time)
.ok_or_else(|| Error::from_reason("Invalid start time"))?,
end_time: DateTime::<Utc>::from_timestamp_millis(end_time)
.ok_or_else(|| Error::from_reason("Invalid end time"))?,
speed_multiplier,
})
}
"paper" => {
let starting_capital: f64 = config.get_named_property("startingCapital")?;
Ok(TradingMode::Paper { starting_capital })
}
"live" => {
let broker: String = config.get_named_property("broker")?;
let account_id: String = config.get_named_property("accountId")?;
Ok(TradingMode::Live { broker, account_id })
}
_ => Err(Error::from_reason("Invalid mode")),
}
}
fn parse_order(order_js: JsObject) -> Result<Order> {
let id: String = order_js.get_named_property("id")?;
let symbol: String = order_js.get_named_property("symbol")?;
let side_str: String = order_js.get_named_property("side")?;
let side = match side_str.as_str() {
"buy" | "Buy" => Side::Buy,
"sell" | "Sell" => Side::Sell,
_ => return Err(Error::from_reason("Invalid side")),
};
let quantity: f64 = order_js.get_named_property("quantity")?;
let order_type_str: String = order_js.get_named_property("orderType")?;
let order_type = match order_type_str.as_str() {
"market" => OrderType::Market,
"limit" => {
let price: f64 = order_js.get_named_property("limitPrice")?;
OrderType::Limit { price }
}
_ => return Err(Error::from_reason("Invalid order type")),
};
let time_in_force_str: String = order_js.get_named_property("timeInForce")
.unwrap_or_else(|_| "DAY".to_string());
let time_in_force = match time_in_force_str.as_str() {
"DAY" => TimeInForce::Day,
"GTC" => TimeInForce::GTC,
"IOC" => TimeInForce::IOC,
"FOK" => TimeInForce::FOK,
_ => TimeInForce::Day,
};
Ok(Order {
id,
symbol,
side,
quantity,
order_type,
time_in_force,
})
}
fn parse_risk_limits(limits_js: JsObject) -> Result<RiskLimits> {
Ok(RiskLimits {
max_position_size: limits_js.get_named_property("maxPositionSize")?,
max_order_size: limits_js.get_named_property("maxOrderSize")?,
max_daily_loss: limits_js.get_named_property("maxDailyLoss")?,
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

@ -0,0 +1,166 @@
use napi_derive::napi;
use napi::{bindgen_prelude::*};
use crate::risk::{BetSizer, BetSizingParameters, MarketRegime, RiskModel};
use crate::orderbook::{OrderBookAnalytics, LiquidityProfile};
use crate::positions::Position;
use std::collections::HashMap;
#[napi]
pub struct RiskAnalyzer {
risk_model: RiskModel,
bet_sizer: BetSizer,
}
#[napi]
impl RiskAnalyzer {
#[napi(constructor)]
pub fn new(capital: f64, base_risk_per_trade: f64, lookback_period: u32) -> Self {
Self {
risk_model: RiskModel::new(lookback_period as usize),
bet_sizer: BetSizer::new(capital, base_risk_per_trade),
}
}
#[napi]
pub fn update_returns(&mut self, symbol: String, returns: Vec<f64>) -> Result<()> {
self.risk_model.update_returns(&symbol, returns);
Ok(())
}
#[napi]
pub fn calculate_portfolio_risk(&self, positions_json: String, prices_json: String) -> Result<String> {
// Parse positions
let positions_data: Vec<(String, f64, f64)> = serde_json::from_str(&positions_json)
.map_err(|e| Error::from_reason(format!("Failed to parse positions: {}", e)))?;
let mut positions = HashMap::new();
for (symbol, quantity, avg_price) in positions_data {
positions.insert(symbol.clone(), Position {
symbol,
quantity,
average_price: avg_price,
realized_pnl: 0.0,
unrealized_pnl: 0.0,
total_cost: quantity * avg_price,
last_update: chrono::Utc::now(),
});
}
// Parse prices
let prices: HashMap<String, f64> = serde_json::from_str(&prices_json)
.map_err(|e| Error::from_reason(format!("Failed to parse prices: {}", e)))?;
// Calculate risk
match self.risk_model.calculate_portfolio_risk(&positions, &prices) {
Ok(risk) => Ok(serde_json::to_string(&risk).unwrap()),
Err(e) => Err(Error::from_reason(e)),
}
}
#[napi]
pub fn calculate_position_size(
&self,
signal_strength: f64,
signal_confidence: f64,
volatility: f64,
liquidity_score: f64,
current_drawdown: f64,
price: f64,
stop_loss: Option<f64>,
market_regime: String,
) -> Result<String> {
let regime = match market_regime.as_str() {
"trending" => MarketRegime::Trending,
"range_bound" => MarketRegime::RangeBound,
"high_volatility" => MarketRegime::HighVolatility,
"low_volatility" => MarketRegime::LowVolatility,
_ => MarketRegime::Transitioning,
};
let params = BetSizingParameters {
signal_strength,
signal_confidence,
market_regime: regime,
volatility,
liquidity_score,
correlation_exposure: 0.0, // Would be calculated from portfolio
current_drawdown,
};
let position_size = self.bet_sizer.calculate_position_size(
&params,
price,
stop_loss,
None, // Historical performance
None, // Orderbook analytics
None, // Liquidity profile
);
Ok(serde_json::to_string(&position_size).unwrap())
}
#[napi]
pub fn calculate_optimal_stop_loss(
&self,
entry_price: f64,
volatility: f64,
support_levels: Vec<f64>,
atr: Option<f64>,
is_long: bool,
) -> f64 {
self.bet_sizer.calculate_optimal_stop_loss(
entry_price,
volatility,
&support_levels,
atr,
is_long,
)
}
}
#[napi]
pub struct OrderbookAnalyzer {}
#[napi]
impl OrderbookAnalyzer {
#[napi(constructor)]
pub fn new() -> Self {
Self {}
}
#[napi]
pub fn analyze_orderbook(&self, snapshot_json: String) -> Result<String> {
let snapshot: crate::OrderBookSnapshot = serde_json::from_str(&snapshot_json)
.map_err(|e| Error::from_reason(format!("Failed to parse snapshot: {}", e)))?;
match OrderBookAnalytics::calculate(&snapshot) {
Some(analytics) => Ok(serde_json::to_string(&analytics).unwrap()),
None => Err(Error::from_reason("Failed to calculate analytics")),
}
}
#[napi]
pub fn calculate_liquidity_profile(&self, snapshot_json: String) -> Result<String> {
let snapshot: crate::OrderBookSnapshot = serde_json::from_str(&snapshot_json)
.map_err(|e| Error::from_reason(format!("Failed to parse snapshot: {}", e)))?;
let profile = LiquidityProfile::from_snapshot(&snapshot);
Ok(serde_json::to_string(&profile).unwrap())
}
#[napi]
pub fn calculate_market_impact(
&self,
snapshot_json: String,
order_size_usd: f64,
is_buy: bool,
) -> Result<String> {
let snapshot: crate::OrderBookSnapshot = serde_json::from_str(&snapshot_json)
.map_err(|e| Error::from_reason(format!("Failed to parse snapshot: {}", e)))?;
let profile = LiquidityProfile::from_snapshot(&snapshot);
let impact = profile.calculate_market_impact(order_size_usd, is_buy);
Ok(serde_json::to_string(&impact).unwrap())
}
}