neural_trader/
portfolio.rs

1//! Portfolio management bindings for Node.js
2
3use napi::bindgen_prelude::*;
4use napi_derive::napi;
5use std::collections::HashMap;
6use std::sync::Arc;
7use tokio::sync::Mutex;
8
9/// Portfolio position
10#[napi(object)]
11#[derive(Clone)]
12pub struct Position {
13    pub symbol: String,
14    pub quantity: f64,
15    pub avg_cost: f64,
16    pub market_value: f64,
17    pub unrealized_pnl: f64,
18    pub realized_pnl: f64,
19}
20
21/// Portfolio optimization result
22#[napi(object)]
23pub struct PortfolioOptimization {
24    pub allocations: HashMap<String, f64>,
25    pub expected_return: f64,
26    pub risk: f64,
27    pub sharpe_ratio: f64,
28}
29
30/// Risk metrics
31#[napi(object)]
32pub struct RiskMetrics {
33    pub var_95: f64,          // Value at Risk (95% confidence)
34    pub cvar_95: f64,         // Conditional VaR
35    pub beta: f64,
36    pub sharpe_ratio: f64,
37    pub max_drawdown: f64,
38}
39
40/// Portfolio optimizer configuration
41#[napi(object)]
42pub struct OptimizerConfig {
43    pub risk_free_rate: f64,
44    pub max_position_size: Option<f64>,
45    pub min_position_size: Option<f64>,
46}
47
48/// Portfolio optimizer using modern portfolio theory
49#[napi]
50pub struct PortfolioOptimizer {
51    config: OptimizerConfig,
52}
53
54#[napi]
55impl PortfolioOptimizer {
56    /// Create a new portfolio optimizer
57    #[napi(constructor)]
58    pub fn new(config: OptimizerConfig) -> Self {
59        tracing::info!("Creating portfolio optimizer with risk-free rate: {}", config.risk_free_rate);
60        Self { config }
61    }
62
63    /// Optimize portfolio allocation
64    #[napi]
65    pub async fn optimize(
66        &self,
67        symbols: Vec<String>,
68        returns: Vec<f64>,
69        covariance: Vec<f64>,
70    ) -> Result<PortfolioOptimization> {
71        tracing::info!("Optimizing portfolio for {} symbols", symbols.len());
72
73        // In a real implementation:
74        // - Use quadratic programming to solve for optimal weights
75        // - Apply constraints (max/min position sizes)
76        // - Calculate efficient frontier
77
78        // Mock optimization result
79        let mut allocations = HashMap::new();
80        let equal_weight = 1.0 / symbols.len() as f64;
81
82        for symbol in symbols {
83            allocations.insert(symbol, equal_weight);
84        }
85
86        let avg_return = returns.iter().sum::<f64>() / returns.len() as f64;
87        let avg_risk = (covariance.iter().sum::<f64>() / covariance.len() as f64).sqrt();
88
89        Ok(PortfolioOptimization {
90            allocations,
91            expected_return: avg_return,
92            risk: avg_risk,
93            sharpe_ratio: (avg_return - self.config.risk_free_rate) / avg_risk,
94        })
95    }
96
97    /// Calculate risk metrics for given positions
98    #[napi]
99    pub fn calculate_risk(&self, positions: HashMap<String, f64>) -> Result<RiskMetrics> {
100        tracing::debug!("Calculating risk metrics for {} positions", positions.len());
101
102        // In a real implementation:
103        // - Calculate historical volatility
104        // - Compute VaR using historical simulation or Monte Carlo
105        // - Calculate portfolio beta
106
107        // Mock risk metrics
108        Ok(RiskMetrics {
109            var_95: 0.05,
110            cvar_95: 0.07,
111            beta: 1.2,
112            sharpe_ratio: 1.5,
113            max_drawdown: 0.15,
114        })
115    }
116}
117
118/// Portfolio manager for tracking positions
119#[napi]
120pub struct PortfolioManager {
121    positions: Arc<Mutex<HashMap<String, Position>>>,
122    cash: Arc<Mutex<f64>>,
123}
124
125#[napi]
126impl PortfolioManager {
127    /// Create a new portfolio manager
128    #[napi(constructor)]
129    pub fn new(initial_cash: f64) -> Self {
130        tracing::info!("Creating portfolio manager with initial cash: ${}", initial_cash);
131
132        Self {
133            positions: Arc::new(Mutex::new(HashMap::new())),
134            cash: Arc::new(Mutex::new(initial_cash)),
135        }
136    }
137
138    /// Get all positions
139    #[napi]
140    pub async fn get_positions(&self) -> Result<Vec<Position>> {
141        let positions = self.positions.lock().await;
142        Ok(positions.values().cloned().collect())
143    }
144
145    /// Get position for a specific symbol
146    #[napi]
147    pub async fn get_position(&self, symbol: String) -> Result<Option<Position>> {
148        let positions = self.positions.lock().await;
149        Ok(positions.get(&symbol).cloned())
150    }
151
152    /// Update position (called after trade execution)
153    #[napi]
154    pub async fn update_position(
155        &self,
156        symbol: String,
157        quantity: f64,
158        price: f64,
159    ) -> Result<Position> {
160        let mut positions = self.positions.lock().await;
161        let mut cash = self.cash.lock().await;
162
163        let position = positions.entry(symbol.clone()).or_insert(Position {
164            symbol: symbol.clone(),
165            quantity: 0.0,
166            avg_cost: 0.0,
167            market_value: 0.0,
168            unrealized_pnl: 0.0,
169            realized_pnl: 0.0,
170        });
171
172        // Update position
173        let old_quantity = position.quantity;
174        let old_avg_cost = position.avg_cost;
175
176        position.quantity += quantity;
177
178        if position.quantity != 0.0 {
179            position.avg_cost = ((old_quantity * old_avg_cost) + (quantity * price))
180                / position.quantity;
181        }
182
183        position.market_value = position.quantity * price;
184        position.unrealized_pnl = (price - position.avg_cost) * position.quantity;
185
186        // Update cash
187        *cash -= quantity * price;
188
189        tracing::info!(
190            "Updated position: {} shares of {} @ ${} (avg cost: ${})",
191            position.quantity, symbol, price, position.avg_cost
192        );
193
194        Ok(position.clone())
195    }
196
197    /// Get current cash balance
198    #[napi]
199    pub async fn get_cash(&self) -> Result<f64> {
200        Ok(*self.cash.lock().await)
201    }
202
203    /// Get total portfolio value
204    #[napi]
205    pub async fn get_total_value(&self) -> Result<f64> {
206        let positions = self.positions.lock().await;
207        let cash = *self.cash.lock().await;
208
209        let positions_value: f64 = positions.values()
210            .map(|p| p.market_value)
211            .sum();
212
213        Ok(cash + positions_value)
214    }
215
216    /// Get total unrealized P&L
217    #[napi]
218    pub async fn get_total_pnl(&self) -> Result<f64> {
219        let positions = self.positions.lock().await;
220        Ok(positions.values().map(|p| p.unrealized_pnl).sum())
221    }
222}