moving engine to rust

This commit is contained in:
Boki 2025-07-03 20:10:33 -04:00
parent d14380d740
commit 16ac28a565
16 changed files with 1598 additions and 3 deletions

View file

@ -13,6 +13,7 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0"
anyhow = "1.0"
uuid = { version = "1.0", features = ["v4", "serde"] }
# Data structures
dashmap = "5.5"

View file

@ -11,6 +11,13 @@ const nativeBinding = require(join(__dirname, 'index.node'));
export const {
TradingEngine,
BacktestEngine,
TechnicalIndicators,
IncrementalSMA,
IncrementalEMA,
IncrementalRSI,
RiskAnalyzer,
OrderbookAnalyzer,
MarketData,
MarketUpdate,
Order,

Binary file not shown.

View file

@ -0,0 +1,206 @@
use napi::bindgen_prelude::*;
use napi::{threadsafe_function::ThreadsafeFunction, JsObject};
use napi_derive::napi;
use std::sync::Arc;
use parking_lot::Mutex;
use crate::backtest::{
BacktestEngine as RustBacktestEngine,
BacktestConfig,
Strategy, Signal,
strategy::{TypeScriptStrategy, StrategyCall, StrategyResponse},
};
use crate::{TradingMode, MarketUpdate};
use chrono::{DateTime, Utc};
#[napi]
pub struct BacktestEngine {
inner: Arc<Mutex<Option<RustBacktestEngine>>>,
strategies: Arc<Mutex<Vec<Arc<Mutex<TypeScriptStrategy>>>>>,
}
#[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))),
strategies: Arc::new(Mutex::new(Vec::new())),
})
}
#[napi]
pub fn add_typescript_strategy(
&mut self,
name: String,
id: String,
parameters: napi::JsObject,
callback: napi::JsFunction,
) -> Result<()> {
// Convert JsObject to serde_json::Value
let params = serde_json::Value::Object(serde_json::Map::new());
let mut strategy = TypeScriptStrategy::new(name, id, params);
// Create a thread-safe callback wrapper
let tsfn: ThreadsafeFunction<String> = callback
.create_threadsafe_function(0, |ctx| {
ctx.env.create_string_from_std(ctx.value)
.map(|v| vec![v])
})?;
// Set the callback that will call back into TypeScript
let tsfn_clone = tsfn.clone();
strategy.callback = Some(Box::new(move |call| {
let call_json = serde_json::to_string(&call).unwrap_or_default();
// For now, return empty response - proper implementation needed
let response = "{}".to_string();
serde_json::from_str(&response)
.unwrap_or_else(|_| crate::backtest::strategy::StrategyResponse { signals: vec![] })
}));
let strategy_arc = Arc::new(Mutex::new(strategy));
self.strategies.lock().push(strategy_arc.clone());
// Add to engine
if let Some(engine) = self.inner.lock().as_mut() {
engine.add_strategy(Box::new(StrategyWrapper(strategy_arc)));
}
Ok(())
}
#[napi]
pub fn run(&mut self) -> Result<String> {
let mut engine = self.inner.lock().take()
.ok_or_else(|| Error::from_reason("Engine already consumed"))?;
// 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| Error::from_reason(e))?;
// 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<()> {
// Convert JS objects to MarketData
let market_data: Vec<MarketUpdate> = data.into_iter()
.filter_map(|obj| parse_market_data(obj).ok())
.collect();
// In real implementation, this would load into the market data source
Ok(())
}
}
// Wrapper to make TypeScriptStrategy implement Strategy trait
struct StrategyWrapper(Arc<Mutex<TypeScriptStrategy>>);
impl Strategy for StrategyWrapper {
fn on_market_data(&mut self, data: &MarketUpdate) -> Vec<Signal> {
self.0.lock().on_market_data(data)
}
fn on_fill(&mut self, symbol: &str, quantity: f64, price: f64, side: &str) {
self.0.lock().on_fill(symbol, quantity, price, side)
}
fn get_name(&self) -> &str {
// This is a hack - in production, store name separately
"typescript_strategy"
}
fn get_parameters(&self) -> serde_json::Value {
self.0.lock().parameters.clone()
}
}
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")?;
Ok(BacktestConfig {
name,
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 {
return Err(Error::from_reason("Unsupported market data type"));
};
Ok(crate::MarketUpdate {
symbol,
timestamp: DateTime::<Utc>::from_timestamp(timestamp / 1000, 0)
.ok_or_else(|| Error::from_reason("Invalid timestamp"))?,
data,
})
}
// Error handling for threadsafe functions
struct ErrorStrategy;
impl From<napi::Error> for ErrorStrategy {
fn from(e: napi::Error) -> Self {
ErrorStrategy
}
}

View file

@ -1,8 +1,10 @@
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};

View file

@ -0,0 +1,424 @@
use std::collections::HashMap;
use std::sync::Arc;
use parking_lot::RwLock;
use chrono::{DateTime, Utc};
use serde::{Serialize, Deserialize};
use crate::{
TradingMode, MarketDataSource, ExecutionHandler, TimeProvider,
MarketUpdate, MarketDataType, Order, Fill, Side,
positions::PositionTracker,
risk::RiskEngine,
orderbook::OrderBookManager,
};
use super::{
BacktestConfig, BacktestState, EventQueue, BacktestEvent, EventType,
Strategy, Signal, SignalType, BacktestResult,
};
pub struct BacktestEngine {
config: BacktestConfig,
state: Arc<RwLock<BacktestState>>,
event_queue: Arc<RwLock<EventQueue>>,
strategies: Arc<RwLock<Vec<Box<dyn Strategy>>>>,
// Core components
position_tracker: Arc<PositionTracker>,
risk_engine: Arc<RiskEngine>,
orderbook_manager: Arc<OrderBookManager>,
time_provider: Arc<Box<dyn TimeProvider>>,
market_data_source: Arc<RwLock<Box<dyn MarketDataSource>>>,
execution_handler: Arc<RwLock<Box<dyn ExecutionHandler>>>,
// Metrics
total_trades: usize,
profitable_trades: usize,
total_pnl: f64,
}
impl BacktestEngine {
pub fn new(
config: BacktestConfig,
mode: TradingMode,
time_provider: Box<dyn TimeProvider>,
market_data_source: Box<dyn MarketDataSource>,
execution_handler: Box<dyn ExecutionHandler>,
) -> Self {
let state = Arc::new(RwLock::new(
BacktestState::new(config.initial_capital, config.start_time)
));
Self {
config,
state,
event_queue: Arc::new(RwLock::new(EventQueue::new())),
strategies: Arc::new(RwLock::new(Vec::new())),
position_tracker: Arc::new(PositionTracker::new()),
risk_engine: Arc::new(RiskEngine::new()),
orderbook_manager: Arc::new(OrderBookManager::new()),
time_provider: Arc::new(time_provider),
market_data_source: Arc::new(RwLock::new(market_data_source)),
execution_handler: Arc::new(RwLock::new(execution_handler)),
total_trades: 0,
profitable_trades: 0,
total_pnl: 0.0,
}
}
pub fn add_strategy(&mut self, strategy: Box<dyn Strategy>) {
self.strategies.write().push(strategy);
}
pub async fn run(&mut self) -> Result<BacktestResult, String> {
// Initialize start time
if let Some(simulated_time) = self.time_provider.as_any()
.downcast_ref::<crate::core::time_providers::SimulatedTime>()
{
simulated_time.advance_to(self.config.start_time);
}
// Load market data
self.load_market_data().await?;
// Main event loop
while !self.event_queue.read().is_empty() ||
self.time_provider.now() < self.config.end_time
{
// Get next batch of events
let current_time = self.time_provider.now();
let events = self.event_queue.write().pop_until(current_time);
for event in events {
self.process_event(event).await?;
}
// Update portfolio value
self.update_portfolio_value();
// Check if we should advance time
if self.event_queue.read().is_empty() {
// Advance to next data point or end time
if let Some(next_time) = self.get_next_event_time() {
if next_time < self.config.end_time {
self.advance_time(next_time);
} else {
break;
}
} else {
break;
}
}
}
// Generate results
Ok(self.generate_results())
}
async fn load_market_data(&mut self) -> Result<(), String> {
let mut data_source = self.market_data_source.write();
// Seek to start time
data_source.seek_to_time(self.config.start_time)?;
// Load all data into event queue
while let Some(update) = data_source.get_next_update().await {
if update.timestamp > self.config.end_time {
break;
}
let event = BacktestEvent::market_data(update.timestamp, update);
self.event_queue.write().push(event);
}
Ok(())
}
async fn process_event(&mut self, event: BacktestEvent) -> Result<(), String> {
match event.event_type {
EventType::MarketData(data) => {
self.process_market_data(data).await?;
}
EventType::OrderSubmitted(order) => {
self.process_order_submission(order).await?;
}
EventType::OrderFilled(fill) => {
// Fills are already processed when orders are executed
// This event is just for recording
self.state.write().record_fill(fill);
}
EventType::OrderCancelled(order_id) => {
self.process_order_cancellation(&order_id)?;
}
EventType::TimeUpdate(time) => {
self.advance_time(time);
}
}
Ok(())
}
async fn process_market_data(&mut self, data: MarketUpdate) -> Result<(), String> {
// Update orderbook if it's quote data
match &data.data {
MarketDataType::Quote(quote) => {
// For now, skip orderbook updates
// self.orderbook_manager.update_quote(&data.symbol, quote.bid, quote.ask);
}
MarketDataType::Trade(trade) => {
// For now, skip orderbook updates
// self.orderbook_manager.update_last_trade(&data.symbol, trade.price, trade.size);
}
_ => {}
}
// Convert to simpler MarketData for strategies
let market_data = self.convert_to_market_data(&data);
// Send to strategies
let mut all_signals = Vec::new();
{
let mut strategies = self.strategies.write();
for strategy in strategies.iter_mut() {
let signals = strategy.on_market_data(&market_data);
all_signals.extend(signals);
}
}
// Process signals
for signal in all_signals {
self.process_signal(signal).await?;
}
// Check pending orders for fills
self.check_pending_orders(&data).await?;
Ok(())
}
fn convert_to_market_data(&self, update: &MarketUpdate) -> MarketUpdate {
// MarketData is a type alias for MarketUpdate
update.clone()
}
async fn process_signal(&mut self, signal: Signal) -> Result<(), String> {
// Only process strong signals
if signal.strength.abs() < 0.7 {
return Ok(());
}
// Convert signal to order
let order = self.signal_to_order(signal)?;
// Submit order
self.process_order_submission(order).await
}
fn signal_to_order(&self, signal: Signal) -> Result<Order, String> {
let quantity = signal.quantity.unwrap_or_else(|| {
// Calculate position size based on portfolio
self.calculate_position_size(&signal.symbol, signal.strength)
});
let side = match signal.signal_type {
SignalType::Buy => Side::Buy,
SignalType::Sell => Side::Sell,
SignalType::Close => {
// Determine side based on current position
let position = self.position_tracker.get_position(&signal.symbol);
if position.as_ref().map(|p| p.quantity > 0.0).unwrap_or(false) {
Side::Sell
} else {
Side::Buy
}
}
};
Ok(crate::Order {
id: format!("order_{}", uuid::Uuid::new_v4()),
symbol: signal.symbol,
side,
quantity,
order_type: crate::OrderType::Market,
time_in_force: crate::TimeInForce::Day,
})
}
async fn process_order_submission(&mut self, order: Order) -> Result<(), String> {
// Risk checks
// Get current position for the symbol
let current_position = self.position_tracker
.get_position(&order.symbol)
.map(|p| p.quantity);
let risk_check = self.risk_engine.check_order(&order, current_position);
if !risk_check.passed {
return Err(format!("Risk check failed: {:?}", risk_check.violations));
}
// Add to pending orders
self.state.write().add_pending_order(order.clone());
// For market orders in backtesting, fill immediately
if matches!(order.order_type, crate::OrderType::Market) {
self.check_order_fill(&order).await?;
}
Ok(())
}
async fn check_pending_orders(&mut self, market_data: &MarketUpdate) -> Result<(), String> {
let orders_to_check: Vec<Order> = {
let state = self.state.read();
state.pending_orders.values()
.filter(|o| o.symbol == market_data.symbol)
.cloned()
.collect()
};
for order in orders_to_check {
self.check_order_fill(&order).await?;
}
Ok(())
}
async fn check_order_fill(&mut self, order: &Order) -> Result<(), String> {
// Get current market price
// For now, use a simple fill model with last known price
// In a real backtest, this would use orderbook data
let base_price = 100.0; // TODO: Get from market data
// Apply slippage
let fill_price = match order.side {
crate::Side::Buy => base_price * (1.0 + self.config.slippage),
crate::Side::Sell => base_price * (1.0 - self.config.slippage),
};
// Create fill
let fill = crate::Fill {
timestamp: self.time_provider.now(),
price: fill_price,
quantity: order.quantity,
commission: order.quantity * fill_price * self.config.commission,
};
// Process the fill
self.process_fill(&order, fill).await
}
async fn process_fill(&mut self, order: &crate::Order, fill: crate::Fill) -> Result<(), String> {
// Remove from pending orders
self.state.write().remove_pending_order(&order.id);
// Update positions
let update = self.position_tracker.process_fill(
&order.symbol,
&fill,
order.side,
);
// Record the fill
self.state.write().record_fill(fill.clone());
// Update cash
let cash_change = match order.side {
crate::Side::Buy => -(fill.quantity * fill.price + fill.commission),
crate::Side::Sell => fill.quantity * fill.price - fill.commission,
};
self.state.write().cash += cash_change;
// Notify strategies
{
let mut strategies = self.strategies.write();
for strategy in strategies.iter_mut() {
strategy.on_fill(&order.symbol, fill.quantity, fill.price,
&format!("{:?}", order.side));
}
}
// Update metrics
self.total_trades += 1;
if update.resulting_position.realized_pnl > 0.0 {
self.profitable_trades += 1;
}
self.total_pnl = update.resulting_position.realized_pnl;
Ok(())
}
fn process_order_cancellation(&mut self, order_id: &str) -> Result<(), String> {
self.state.write().remove_pending_order(order_id);
Ok(())
}
fn advance_time(&mut self, time: DateTime<Utc>) {
if let Some(simulated_time) = self.time_provider.as_any()
.downcast_ref::<crate::core::time_providers::SimulatedTime>()
{
simulated_time.advance_to(time);
}
self.state.write().current_time = time;
}
fn update_portfolio_value(&mut self) {
let positions = self.position_tracker.get_all_positions();
let mut portfolio_value = self.state.read().cash;
for position in positions {
// For now, use a simple market value calculation
let market_value = position.quantity * 100.0; // TODO: Get actual price
portfolio_value += market_value;
}
self.state.write().update_portfolio_value(portfolio_value);
}
fn calculate_position_size(&self, symbol: &str, signal_strength: f64) -> f64 {
let portfolio_value = self.state.read().portfolio_value;
let allocation = 0.1; // 10% per position
let position_value = portfolio_value * allocation * signal_strength.abs();
let price = 100.0; // TODO: Get actual price from market data
(position_value / price).floor()
}
fn get_next_event_time(&self) -> Option<DateTime<Utc>> {
// In a real implementation, this would look at the next market data point
None
}
fn generate_results(&self) -> BacktestResult {
let state = self.state.read();
let (realized_pnl, unrealized_pnl) = self.position_tracker.get_total_pnl();
let total_pnl = realized_pnl + unrealized_pnl;
let total_return = (total_pnl / self.config.initial_capital) * 100.0;
BacktestResult {
config: self.config.clone(),
metrics: super::BacktestMetrics {
total_return,
total_trades: self.total_trades,
profitable_trades: self.profitable_trades,
win_rate: if self.total_trades > 0 {
(self.profitable_trades as f64 / self.total_trades as f64) * 100.0
} else { 0.0 },
profit_factor: 0.0, // TODO: Calculate properly
sharpe_ratio: 0.0, // TODO: Calculate properly
max_drawdown: 0.0, // TODO: Calculate properly
total_pnl,
avg_win: 0.0, // TODO: Calculate properly
avg_loss: 0.0, // TODO: Calculate properly
},
equity_curve: state.equity_curve.clone(),
trades: state.completed_trades.clone(),
final_positions: self.position_tracker.get_all_positions()
.into_iter()
.map(|p| (p.symbol.clone(), p))
.collect(),
}
}
}
// Add uuid dependency
use uuid::Uuid;

View file

@ -0,0 +1,55 @@
use chrono::{DateTime, Utc};
use serde::{Serialize, Deserialize};
use crate::{MarketUpdate, Order, Fill};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum EventType {
MarketData(MarketUpdate),
OrderSubmitted(Order),
OrderFilled(Fill),
OrderCancelled(String), // order_id
TimeUpdate(DateTime<Utc>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BacktestEvent {
pub timestamp: DateTime<Utc>,
pub event_type: EventType,
}
impl BacktestEvent {
pub fn market_data(timestamp: DateTime<Utc>, data: MarketUpdate) -> Self {
Self {
timestamp,
event_type: EventType::MarketData(data),
}
}
pub fn order_submitted(timestamp: DateTime<Utc>, order: Order) -> Self {
Self {
timestamp,
event_type: EventType::OrderSubmitted(order),
}
}
pub fn order_filled(timestamp: DateTime<Utc>, fill: Fill) -> Self {
Self {
timestamp,
event_type: EventType::OrderFilled(fill),
}
}
pub fn order_cancelled(timestamp: DateTime<Utc>, order_id: String) -> Self {
Self {
timestamp,
event_type: EventType::OrderCancelled(order_id),
}
}
pub fn time_update(timestamp: DateTime<Utc>) -> Self {
Self {
timestamp,
event_type: EventType::TimeUpdate(timestamp),
}
}
}

View file

@ -0,0 +1,112 @@
use crate::{MarketUpdate, Order, Fill, TradingMode, MarketDataSource, ExecutionHandler, TimeProvider};
use crate::positions::PositionTracker;
use crate::risk::RiskEngine;
use crate::orderbook::OrderBookManager;
use chrono::{DateTime, Utc};
use std::collections::{BTreeMap, VecDeque};
use std::sync::Arc;
use parking_lot::RwLock;
use serde::{Serialize, Deserialize};
pub mod engine;
pub mod event;
pub mod strategy;
pub mod results;
pub use engine::BacktestEngine;
pub use event::{BacktestEvent, EventType};
pub use strategy::{Strategy, Signal, SignalType};
pub use results::{BacktestResult, BacktestMetrics};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BacktestConfig {
pub name: String,
pub symbols: Vec<String>,
pub start_time: DateTime<Utc>,
pub end_time: DateTime<Utc>,
pub initial_capital: f64,
pub commission: f64,
pub slippage: f64,
pub data_frequency: String,
}
#[derive(Debug, Clone)]
pub struct BacktestState {
pub current_time: DateTime<Utc>,
pub portfolio_value: f64,
pub cash: f64,
pub equity_curve: Vec<(DateTime<Utc>, f64)>,
pub pending_orders: BTreeMap<String, Order>,
pub completed_trades: Vec<Fill>,
}
impl BacktestState {
pub fn new(initial_capital: f64, start_time: DateTime<Utc>) -> Self {
Self {
current_time: start_time,
portfolio_value: initial_capital,
cash: initial_capital,
equity_curve: vec![(start_time, initial_capital)],
pending_orders: BTreeMap::new(),
completed_trades: Vec::new(),
}
}
pub fn update_portfolio_value(&mut self, value: f64) {
self.portfolio_value = value;
self.equity_curve.push((self.current_time, value));
}
pub fn add_pending_order(&mut self, order: Order) {
self.pending_orders.insert(order.id.clone(), order);
}
pub fn remove_pending_order(&mut self, order_id: &str) -> Option<Order> {
self.pending_orders.remove(order_id)
}
pub fn record_fill(&mut self, fill: Fill) {
self.completed_trades.push(fill);
}
}
// Event queue for deterministic replay
#[derive(Debug)]
pub struct EventQueue {
events: VecDeque<BacktestEvent>,
}
impl EventQueue {
pub fn new() -> Self {
Self {
events: VecDeque::new(),
}
}
pub fn push(&mut self, event: BacktestEvent) {
// Insert in time order
let pos = self.events.iter().position(|e| e.timestamp > event.timestamp)
.unwrap_or(self.events.len());
self.events.insert(pos, event);
}
pub fn pop_until(&mut self, timestamp: DateTime<Utc>) -> Vec<BacktestEvent> {
let mut events = Vec::new();
while let Some(event) = self.events.front() {
if event.timestamp <= timestamp {
events.push(self.events.pop_front().unwrap());
} else {
break;
}
}
events
}
pub fn is_empty(&self) -> bool {
self.events.is_empty()
}
pub fn len(&self) -> usize {
self.events.len()
}
}

View file

@ -0,0 +1,28 @@
use chrono::{DateTime, Utc};
use serde::{Serialize, Deserialize};
use std::collections::HashMap;
use crate::{Fill, Position};
use super::BacktestConfig;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BacktestMetrics {
pub total_return: f64,
pub total_trades: usize,
pub profitable_trades: usize,
pub win_rate: f64,
pub profit_factor: f64,
pub sharpe_ratio: f64,
pub max_drawdown: f64,
pub total_pnl: f64,
pub avg_win: f64,
pub avg_loss: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BacktestResult {
pub config: BacktestConfig,
pub metrics: BacktestMetrics,
pub equity_curve: Vec<(DateTime<Utc>, f64)>,
pub trades: Vec<Fill>,
pub final_positions: HashMap<String, Position>,
}

View file

@ -0,0 +1,100 @@
use chrono::{DateTime, Utc};
use serde::{Serialize, Deserialize};
use crate::MarketData;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SignalType {
Buy,
Sell,
Close,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Signal {
pub symbol: String,
pub signal_type: SignalType,
pub strength: f64, // -1.0 to 1.0
pub quantity: Option<f64>,
pub reason: Option<String>,
pub metadata: Option<serde_json::Value>,
}
// This trait will be implemented by Rust strategies
// TypeScript strategies will communicate through FFI
pub trait Strategy: Send + Sync {
fn on_market_data(&mut self, data: &MarketData) -> Vec<Signal>;
fn on_fill(&mut self, symbol: &str, quantity: f64, price: f64, side: &str);
fn get_name(&self) -> &str;
fn get_parameters(&self) -> serde_json::Value;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StrategyCall {
pub method: String,
pub data: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StrategyResponse {
pub signals: Vec<Signal>,
}
// Bridge for TypeScript strategies
// This will be used to wrap TypeScript strategies
pub struct TypeScriptStrategy {
pub name: String,
pub id: String,
pub parameters: serde_json::Value,
// Callback function will be injected from TypeScript
pub callback: Option<Box<dyn Fn(StrategyCall) -> StrategyResponse + Send + Sync>>,
}
impl TypeScriptStrategy {
pub fn new(name: String, id: String, parameters: serde_json::Value) -> Self {
Self {
name,
id,
parameters,
callback: None,
}
}
}
impl Strategy for TypeScriptStrategy {
fn on_market_data(&mut self, data: &MarketData) -> Vec<Signal> {
if let Some(callback) = &self.callback {
let call = StrategyCall {
method: "on_market_data".to_string(),
data: serde_json::to_value(data).unwrap_or_default(),
};
let response = callback(call);
response.signals
} else {
Vec::new()
}
}
fn on_fill(&mut self, symbol: &str, quantity: f64, price: f64, side: &str) {
if let Some(callback) = &self.callback {
let call = StrategyCall {
method: "on_fill".to_string(),
data: serde_json::json!({
"symbol": symbol,
"quantity": quantity,
"price": price,
"side": side
}),
};
callback(call);
}
}
fn get_name(&self) -> &str {
&self.name
}
fn get_parameters(&self) -> serde_json::Value {
self.parameters.clone()
}
}

View file

@ -7,11 +7,15 @@ pub mod positions;
pub mod api;
pub mod analytics;
pub mod indicators;
pub mod backtest;
// Re-export commonly used types
pub use positions::{Position, PositionUpdate, TradeRecord, ClosedTrade};
pub use risk::{RiskLimits, RiskCheckResult, RiskMetrics};
// Type alias for backtest compatibility
pub type MarketData = MarketUpdate;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::sync::Arc;