1use napi::bindgen_prelude::*;
11use napi_derive::napi;
12
13#[napi(object)]
15pub struct RiskConfig {
16 pub confidence_level: f64, pub lookback_periods: u32, pub method: String, }
20
21impl Default for RiskConfig {
22 fn default() -> Self {
23 Self {
24 confidence_level: 0.95,
25 lookback_periods: 252, method: "historical".to_string(),
27 }
28 }
29}
30
31#[napi(object)]
33pub struct VaRResult {
34 pub var_amount: f64,
35 pub var_percentage: f64,
36 pub confidence_level: f64,
37 pub method: String,
38 pub portfolio_value: f64,
39}
40
41#[napi(object)]
43pub struct CVaRResult {
44 pub cvar_amount: f64,
45 pub cvar_percentage: f64,
46 pub var_amount: f64,
47 pub confidence_level: f64,
48}
49
50#[napi(object)]
52pub struct DrawdownMetrics {
53 pub max_drawdown: f64,
54 pub max_drawdown_duration: u32, pub current_drawdown: f64,
56 pub recovery_factor: f64,
57}
58
59#[napi(object)]
61pub struct KellyResult {
62 pub kelly_fraction: f64,
63 pub half_kelly: f64,
64 pub quarter_kelly: f64,
65 pub win_rate: f64,
66 pub avg_win: f64,
67 pub avg_loss: f64,
68}
69
70#[napi(object)]
72pub struct PositionSize {
73 pub shares: u32,
74 pub dollar_amount: f64,
75 pub percentage_of_portfolio: f64,
76 pub max_loss: f64,
77 pub reasoning: String,
78}
79
80#[napi]
82pub struct RiskManager {
83 config: RiskConfig,
84}
85
86#[napi]
87impl RiskManager {
88 #[napi(constructor)]
90 pub fn new(config: RiskConfig) -> Self {
91 tracing::info!(
92 "Creating risk manager with {} confidence, {} method",
93 config.confidence_level,
94 config.method
95 );
96
97 Self { config }
98 }
99
100 #[napi]
102 pub fn calculate_var(&self, returns: Vec<f64>, portfolio_value: f64) -> Result<VaRResult> {
103 if returns.is_empty() {
104 return Err(Error::from_reason("Returns data is empty"));
105 }
106
107 tracing::debug!("Calculating VaR for {} returns", returns.len());
108
109 let mut sorted_returns = returns.clone();
112 sorted_returns.sort_by(|a, b| a.partial_cmp(b).unwrap());
113
114 let index = ((1.0 - self.config.confidence_level) * sorted_returns.len() as f64) as usize;
115 let var_percentage = -sorted_returns[index.min(sorted_returns.len() - 1)];
116 let var_amount = var_percentage * portfolio_value;
117
118 Ok(VaRResult {
119 var_amount,
120 var_percentage,
121 confidence_level: self.config.confidence_level,
122 method: self.config.method.clone(),
123 portfolio_value,
124 })
125 }
126
127 #[napi]
129 pub fn calculate_cvar(&self, returns: Vec<f64>, portfolio_value: f64) -> Result<CVaRResult> {
130 if returns.is_empty() {
131 return Err(Error::from_reason("Returns data is empty"));
132 }
133
134 tracing::debug!("Calculating CVaR for {} returns", returns.len());
135
136 let var_result = self.calculate_var(returns.clone(), portfolio_value)?;
138
139 let mut sorted_returns = returns;
142 sorted_returns.sort_by(|a, b| a.partial_cmp(b).unwrap());
143
144 let var_threshold = -var_result.var_percentage;
145 let tail_returns: Vec<f64> = sorted_returns
146 .iter()
147 .filter(|&&r| r <= var_threshold)
148 .copied()
149 .collect();
150
151 let cvar_percentage = if !tail_returns.is_empty() {
152 -tail_returns.iter().sum::<f64>() / tail_returns.len() as f64
153 } else {
154 var_result.var_percentage
155 };
156
157 let cvar_amount = cvar_percentage * portfolio_value;
158
159 Ok(CVaRResult {
160 cvar_amount,
161 cvar_percentage,
162 var_amount: var_result.var_amount,
163 confidence_level: self.config.confidence_level,
164 })
165 }
166
167 #[napi]
169 pub fn calculate_kelly(
170 &self,
171 win_rate: f64,
172 avg_win: f64,
173 avg_loss: f64,
174 ) -> Result<KellyResult> {
175 if !(0.0..=1.0).contains(&win_rate) {
176 return Err(Error::from_reason("Win rate must be between 0 and 1"));
177 }
178
179 if avg_win <= 0.0 || avg_loss <= 0.0 {
180 return Err(Error::from_reason("Average win and loss must be positive"));
181 }
182
183 tracing::debug!(
184 "Calculating Kelly: win_rate={}, avg_win={}, avg_loss={}",
185 win_rate,
186 avg_win,
187 avg_loss
188 );
189
190 let b = avg_win / avg_loss;
193 let p = win_rate;
194 let q = 1.0 - win_rate;
195
196 let kelly_fraction = ((p * b) - q) / b;
197 let kelly_fraction = kelly_fraction.max(0.0).min(1.0); Ok(KellyResult {
200 kelly_fraction,
201 half_kelly: kelly_fraction / 2.0,
202 quarter_kelly: kelly_fraction / 4.0,
203 win_rate,
204 avg_win,
205 avg_loss,
206 })
207 }
208
209 #[napi]
211 pub fn calculate_drawdown(&self, equity_curve: Vec<f64>) -> Result<DrawdownMetrics> {
212 if equity_curve.is_empty() {
213 return Err(Error::from_reason("Equity curve is empty"));
214 }
215
216 tracing::debug!("Calculating drawdown for {} data points", equity_curve.len());
217
218 let mut max_drawdown = 0.0;
220 let mut max_drawdown_duration = 0u32;
221 let mut current_drawdown = 0.0;
222 let mut peak = equity_curve[0];
223 let mut current_duration = 0u32;
224
225 for &value in &equity_curve {
226 if value > peak {
227 peak = value;
228 current_duration = 0;
229 } else {
230 current_duration += 1;
231 let drawdown = (peak - value) / peak;
232 current_drawdown = drawdown;
233
234 if drawdown > max_drawdown {
235 max_drawdown = drawdown;
236 max_drawdown_duration = current_duration;
237 }
238 }
239 }
240
241 let recovery_factor = if max_drawdown > 0.0 {
242 let total_return = (equity_curve.last().unwrap() - equity_curve[0]) / equity_curve[0];
243 total_return / max_drawdown
244 } else {
245 0.0
246 };
247
248 Ok(DrawdownMetrics {
249 max_drawdown,
250 max_drawdown_duration,
251 current_drawdown,
252 recovery_factor,
253 })
254 }
255
256 #[napi]
258 pub fn calculate_position_size(
259 &self,
260 portfolio_value: f64,
261 price_per_share: f64,
262 risk_per_trade: f64, stop_loss_distance: f64, ) -> Result<PositionSize> {
265 if portfolio_value <= 0.0 {
266 return Err(Error::from_reason("Portfolio value must be positive"));
267 }
268
269 if price_per_share <= 0.0 {
270 return Err(Error::from_reason("Price per share must be positive"));
271 }
272
273 if risk_per_trade <= 0.0 || risk_per_trade > 1.0 {
274 return Err(Error::from_reason("Risk per trade must be between 0 and 1"));
275 }
276
277 tracing::debug!(
278 "Calculating position size: portfolio=${}, price=${}, risk={}%, stop=${}",
279 portfolio_value,
280 price_per_share,
281 risk_per_trade * 100.0,
282 stop_loss_distance
283 );
284
285 let max_risk_amount = portfolio_value * risk_per_trade;
287
288 let shares = if stop_loss_distance > 0.0 {
289 (max_risk_amount / stop_loss_distance).floor() as u32
290 } else {
291 (portfolio_value * risk_per_trade / price_per_share).floor() as u32
293 };
294
295 let dollar_amount = shares as f64 * price_per_share;
296 let percentage = dollar_amount / portfolio_value;
297
298 Ok(PositionSize {
299 shares,
300 dollar_amount,
301 percentage_of_portfolio: percentage,
302 max_loss: max_risk_amount,
303 reasoning: format!(
304 "Risking ${:.2} ({}%) on this trade with {} shares",
305 max_risk_amount,
306 risk_per_trade * 100.0,
307 shares
308 ),
309 })
310 }
311
312 #[napi]
314 pub fn validate_position(
315 &self,
316 position_size: f64,
317 portfolio_value: f64,
318 max_position_percentage: f64,
319 ) -> Result<bool> {
320 let position_percentage = position_size / portfolio_value;
321
322 if position_percentage > max_position_percentage {
323 return Err(Error::from_reason(format!(
324 "Position size ({:.2}%) exceeds maximum allowed ({:.2}%)",
325 position_percentage * 100.0,
326 max_position_percentage * 100.0
327 )));
328 }
329
330 Ok(true)
331 }
332}
333
334#[napi]
336pub fn calculate_sharpe_ratio(
337 returns: Vec<f64>,
338 risk_free_rate: f64,
339 annualization_factor: f64,
340) -> Result<f64> {
341 if returns.is_empty() {
342 return Err(Error::from_reason("Returns data is empty"));
343 }
344
345 let mean_return: f64 = returns.iter().sum::<f64>() / returns.len() as f64;
346 let variance: f64 = returns
347 .iter()
348 .map(|r| (r - mean_return).powi(2))
349 .sum::<f64>()
350 / returns.len() as f64;
351
352 let std_dev = variance.sqrt();
353
354 if std_dev == 0.0 {
355 return Ok(0.0);
356 }
357
358 let excess_return = mean_return - risk_free_rate;
359 let sharpe = (excess_return / std_dev) * annualization_factor.sqrt();
360
361 Ok(sharpe)
362}
363
364#[napi]
366pub fn calculate_sortino_ratio(
367 returns: Vec<f64>,
368 target_return: f64,
369 annualization_factor: f64,
370) -> Result<f64> {
371 if returns.is_empty() {
372 return Err(Error::from_reason("Returns data is empty"));
373 }
374
375 let mean_return: f64 = returns.iter().sum::<f64>() / returns.len() as f64;
376
377 let downside_returns: Vec<f64> = returns
379 .iter()
380 .filter(|&&r| r < target_return)
381 .copied()
382 .collect();
383
384 if downside_returns.is_empty() {
385 return Ok(f64::INFINITY);
386 }
387
388 let downside_variance: f64 = downside_returns
389 .iter()
390 .map(|r| (r - target_return).powi(2))
391 .sum::<f64>()
392 / downside_returns.len() as f64;
393
394 let downside_deviation = downside_variance.sqrt();
395
396 if downside_deviation == 0.0 {
397 return Ok(f64::INFINITY);
398 }
399
400 let excess_return = mean_return - target_return;
401 let sortino = (excess_return / downside_deviation) * annualization_factor.sqrt();
402
403 Ok(sortino)
404}
405
406#[napi]
408pub fn calculate_max_leverage(
409 _portfolio_value: f64,
410 volatility: f64,
411 max_volatility_target: f64,
412) -> Result<f64> {
413 if volatility <= 0.0 {
414 return Err(Error::from_reason("Volatility must be positive"));
415 }
416
417 if max_volatility_target <= 0.0 {
418 return Err(Error::from_reason("Max volatility target must be positive"));
419 }
420
421 let max_leverage = max_volatility_target / volatility;
422
423 Ok(max_leverage.min(3.0))
425}