work on new engine
This commit is contained in:
parent
44476da13f
commit
a1e5a21847
126 changed files with 3425 additions and 6695 deletions
469
apps/stock/engine/src/api/backtest.rs
Normal file
469
apps/stock/engine/src/api/backtest.rs
Normal 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
|
||||
}))
|
||||
}
|
||||
243
apps/stock/engine/src/api/indicators.rs
Normal file
243
apps/stock/engine/src/api/indicators.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
435
apps/stock/engine/src/api/mod.rs
Normal file
435
apps/stock/engine/src/api/mod.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
166
apps/stock/engine/src/api/risk.rs
Normal file
166
apps/stock/engine/src/api/risk.rs
Normal 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(
|
||||
¶ms,
|
||||
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())
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue