326 lines
No EOL
11 KiB
Rust
326 lines
No EOL
11 KiB
Rust
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> {
|
|
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(&symbol, &fill, side);
|
|
|
|
// 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() {
|
|
// In real implementation, would downcast and advance time
|
|
// For now, return success in backtest mode
|
|
Ok(())
|
|
} else {
|
|
Err(Error::from_reason("Can only advance time in backtest mode"))
|
|
}
|
|
}
|
|
|
|
#[napi]
|
|
pub fn set_microstructure(&self, _symbol: String, microstructure_json: String) -> Result<()> {
|
|
let _microstructure: MarketMicrostructure = serde_json::from_str(µstructure_json)
|
|
.map_err(|e| Error::from_reason(format!("Failed to parse microstructure: {}", e)))?;
|
|
|
|
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)))?;
|
|
|
|
// In real implementation, would load into historical data source
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
// 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")?,
|
|
})
|
|
} |