finished initial backtest / engine
This commit is contained in:
parent
55b4ca78c9
commit
c106a719e8
18 changed files with 1571 additions and 180 deletions
Binary file not shown.
|
|
@ -159,6 +159,20 @@ impl TradingEngine {
|
|||
|
||||
#[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,
|
||||
|
|
@ -175,7 +189,7 @@ impl TradingEngine {
|
|||
commission,
|
||||
};
|
||||
|
||||
let update = core.position_tracker.process_fill(&symbol, &fill, side);
|
||||
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);
|
||||
|
|
@ -212,12 +226,18 @@ impl TradingEngine {
|
|||
|
||||
// Backtest-specific methods
|
||||
#[napi]
|
||||
pub fn advance_time(&self, _to_timestamp: i64) -> Result<()> {
|
||||
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(())
|
||||
// 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"))
|
||||
}
|
||||
|
|
@ -274,6 +294,39 @@ impl TradingEngine {
|
|||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ pub mod api;
|
|||
pub mod analytics;
|
||||
|
||||
// Re-export commonly used types
|
||||
pub use positions::{Position, PositionUpdate};
|
||||
pub use positions::{Position, PositionUpdate, TradeRecord, ClosedTrade};
|
||||
pub use risk::{RiskLimits, RiskCheckResult, RiskMetrics};
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ use crate::{Fill, Side};
|
|||
use chrono::{DateTime, Utc};
|
||||
use dashmap::DashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use parking_lot::RwLock;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Position {
|
||||
|
|
@ -21,17 +23,159 @@ pub struct PositionUpdate {
|
|||
pub resulting_position: Position,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TradeRecord {
|
||||
pub id: String,
|
||||
pub symbol: String,
|
||||
pub side: Side,
|
||||
pub quantity: f64,
|
||||
pub price: f64,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
pub commission: f64,
|
||||
pub order_id: Option<String>,
|
||||
pub strategy_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ClosedTrade {
|
||||
pub id: String,
|
||||
pub symbol: String,
|
||||
pub entry_time: DateTime<Utc>,
|
||||
pub exit_time: DateTime<Utc>,
|
||||
pub entry_price: f64,
|
||||
pub exit_price: f64,
|
||||
pub quantity: f64,
|
||||
pub side: Side, // Side of the opening trade
|
||||
pub pnl: f64,
|
||||
pub pnl_percent: f64,
|
||||
pub commission: f64,
|
||||
pub duration_ms: i64,
|
||||
pub entry_fill_id: String,
|
||||
pub exit_fill_id: String,
|
||||
}
|
||||
|
||||
pub struct PositionTracker {
|
||||
positions: DashMap<String, Position>,
|
||||
trade_history: Arc<RwLock<Vec<TradeRecord>>>,
|
||||
closed_trades: Arc<RwLock<Vec<ClosedTrade>>>,
|
||||
open_trades: DashMap<String, Vec<TradeRecord>>, // Track open trades by symbol
|
||||
next_trade_id: Arc<RwLock<u64>>,
|
||||
}
|
||||
|
||||
impl PositionTracker {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
positions: DashMap::new(),
|
||||
trade_history: Arc::new(RwLock::new(Vec::new())),
|
||||
closed_trades: Arc::new(RwLock::new(Vec::new())),
|
||||
open_trades: DashMap::new(),
|
||||
next_trade_id: Arc::new(RwLock::new(1)),
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_trade_id(&self) -> String {
|
||||
let mut id = self.next_trade_id.write();
|
||||
let current_id = *id;
|
||||
*id += 1;
|
||||
format!("T{:08}", current_id)
|
||||
}
|
||||
|
||||
pub fn process_fill_with_tracking(
|
||||
&self,
|
||||
symbol: &str,
|
||||
fill: &Fill,
|
||||
side: Side,
|
||||
order_id: Option<String>,
|
||||
strategy_id: Option<String>
|
||||
) -> PositionUpdate {
|
||||
// First process the fill normally
|
||||
let update = self.process_fill(symbol, fill, side);
|
||||
|
||||
// Create trade record
|
||||
let trade_record = TradeRecord {
|
||||
id: self.generate_trade_id(),
|
||||
symbol: symbol.to_string(),
|
||||
side,
|
||||
quantity: fill.quantity,
|
||||
price: fill.price,
|
||||
timestamp: fill.timestamp,
|
||||
commission: fill.commission,
|
||||
order_id,
|
||||
strategy_id,
|
||||
};
|
||||
|
||||
// Add to trade history
|
||||
self.trade_history.write().push(trade_record.clone());
|
||||
|
||||
// Handle trade matching for closed trades
|
||||
match side {
|
||||
Side::Buy => {
|
||||
// For buy orders, just add to open trades
|
||||
self.open_trades.entry(symbol.to_string())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(trade_record);
|
||||
}
|
||||
Side::Sell => {
|
||||
// For sell orders, try to match with open buy trades
|
||||
if let Some(mut open_trades) = self.open_trades.get_mut(symbol) {
|
||||
let mut remaining_quantity = fill.quantity;
|
||||
let mut trades_to_remove = Vec::new();
|
||||
|
||||
// FIFO matching
|
||||
for (idx, open_trade) in open_trades.iter_mut().enumerate() {
|
||||
if open_trade.side == Side::Buy && remaining_quantity > 0.0 {
|
||||
let close_quantity = remaining_quantity.min(open_trade.quantity);
|
||||
|
||||
// Create closed trade record
|
||||
let closed_trade = ClosedTrade {
|
||||
id: format!("CT{}", self.generate_trade_id()),
|
||||
symbol: symbol.to_string(),
|
||||
entry_time: open_trade.timestamp,
|
||||
exit_time: fill.timestamp,
|
||||
entry_price: open_trade.price,
|
||||
exit_price: fill.price,
|
||||
quantity: close_quantity,
|
||||
side: Side::Buy, // Opening side
|
||||
pnl: close_quantity * (fill.price - open_trade.price) - (open_trade.commission + fill.commission * close_quantity / fill.quantity),
|
||||
pnl_percent: ((fill.price - open_trade.price) / open_trade.price) * 100.0,
|
||||
commission: open_trade.commission + fill.commission * close_quantity / fill.quantity,
|
||||
duration_ms: (fill.timestamp - open_trade.timestamp).num_milliseconds(),
|
||||
entry_fill_id: open_trade.id.clone(),
|
||||
exit_fill_id: trade_record.id.clone(),
|
||||
};
|
||||
|
||||
self.closed_trades.write().push(closed_trade);
|
||||
|
||||
// Update quantities
|
||||
remaining_quantity -= close_quantity;
|
||||
open_trade.quantity -= close_quantity;
|
||||
|
||||
if open_trade.quantity <= 0.0 {
|
||||
trades_to_remove.push(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove fully closed trades
|
||||
for idx in trades_to_remove.into_iter().rev() {
|
||||
open_trades.remove(idx);
|
||||
}
|
||||
|
||||
// If we still have quantity left, it's a short position
|
||||
if remaining_quantity > 0.0 {
|
||||
let short_trade = TradeRecord {
|
||||
quantity: remaining_quantity,
|
||||
..trade_record.clone()
|
||||
};
|
||||
open_trades.push(short_trade);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update
|
||||
}
|
||||
|
||||
pub fn process_fill(&self, symbol: &str, fill: &Fill, side: Side) -> PositionUpdate {
|
||||
let mut entry = self.positions.entry(symbol.to_string()).or_insert_with(|| {
|
||||
Position {
|
||||
|
|
@ -162,5 +306,33 @@ impl PositionTracker {
|
|||
|
||||
pub fn reset(&self) {
|
||||
self.positions.clear();
|
||||
self.trade_history.write().clear();
|
||||
self.closed_trades.write().clear();
|
||||
self.open_trades.clear();
|
||||
*self.next_trade_id.write() = 1;
|
||||
}
|
||||
|
||||
pub fn get_trade_history(&self) -> Vec<TradeRecord> {
|
||||
self.trade_history.read().clone()
|
||||
}
|
||||
|
||||
pub fn get_closed_trades(&self) -> Vec<ClosedTrade> {
|
||||
self.closed_trades.read().clone()
|
||||
}
|
||||
|
||||
pub fn get_open_trades(&self) -> Vec<TradeRecord> {
|
||||
let mut all_open_trades = Vec::new();
|
||||
for entry in self.open_trades.iter() {
|
||||
all_open_trades.extend(entry.value().clone());
|
||||
}
|
||||
all_open_trades
|
||||
}
|
||||
|
||||
pub fn get_trade_count(&self) -> usize {
|
||||
self.trade_history.read().len()
|
||||
}
|
||||
|
||||
pub fn get_closed_trade_count(&self) -> usize {
|
||||
self.closed_trades.read().len()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue