1use std::collections::HashMap;
8use chrono::{DateTime, FixedOffset, Utc};
9use thiserror::Error;
10use tracing::{info, warn, error};
11
12use crate::trading_mode::{RiskConfig};
13use crate::unified_data::{Position, OrderRequest, OrderSide, OrderType};
14
15#[derive(Debug, Error)]
17pub enum RiskError {
18 #[error("Position size exceeds limit: {message}")]
20 PositionSizeExceeded {
21 message: String,
22 },
23
24 #[error("Daily loss limit reached: {current_loss_pct}% exceeds {max_loss_pct}%")]
26 DailyLossLimitReached {
27 current_loss_pct: f64,
28 max_loss_pct: f64,
29 },
30
31 #[error("Leverage limit exceeded: {current_leverage}x exceeds {max_leverage}x")]
33 LeverageLimitExceeded {
34 current_leverage: f64,
35 max_leverage: f64,
36 },
37
38 #[error("Insufficient margin: {required_margin} exceeds {available_margin}")]
40 InsufficientMargin {
41 required_margin: f64,
42 available_margin: f64,
43 },
44
45 #[error("Portfolio concentration limit exceeded: {asset_class} at {concentration_pct}% exceeds {max_concentration_pct}%")]
47 ConcentrationLimitExceeded {
48 asset_class: String,
49 concentration_pct: f64,
50 max_concentration_pct: f64,
51 },
52
53 #[error("Position correlation limit exceeded: {symbol1} and {symbol2} correlation {correlation} exceeds {max_correlation}")]
55 CorrelationLimitExceeded {
56 symbol1: String,
57 symbol2: String,
58 correlation: f64,
59 max_correlation: f64,
60 },
61
62 #[error("Portfolio volatility limit exceeded: {current_volatility_pct}% exceeds {max_volatility_pct}%")]
64 VolatilityLimitExceeded {
65 current_volatility_pct: f64,
66 max_volatility_pct: f64,
67 },
68
69 #[error("Drawdown limit exceeded: {current_drawdown_pct}% exceeds {max_drawdown_pct}%")]
71 DrawdownLimitExceeded {
72 current_drawdown_pct: f64,
73 max_drawdown_pct: f64,
74 },
75
76 #[error("Risk management error: {0}")]
78 General(String),
79}
80
81pub type Result<T> = std::result::Result<T, RiskError>;
83
84#[derive(Debug, Clone)]
86pub struct RiskOrder {
87 pub parent_order_id: String,
89
90 pub symbol: String,
92
93 pub side: OrderSide,
95
96 pub order_type: OrderType,
98
99 pub quantity: f64,
101
102 pub trigger_price: f64,
104
105 pub is_stop_loss: bool,
107
108 pub is_take_profit: bool,
110}
111
112#[derive(Debug, Clone)]
114struct DailyRiskTracker {
115 date: chrono::NaiveDate,
117
118 starting_value: f64,
120
121 current_value: f64,
123
124 realized_pnl: f64,
126
127 unrealized_pnl: f64,
129
130 max_drawdown: f64,
132
133 highest_value: f64,
135}
136
137impl DailyRiskTracker {
138 fn new(portfolio_value: f64) -> Self {
140 Self {
141 date: Utc::now().date_naive(),
142 starting_value: portfolio_value,
143 current_value: portfolio_value,
144 realized_pnl: 0.0,
145 unrealized_pnl: 0.0,
146 max_drawdown: 0.0,
147 highest_value: portfolio_value,
148 }
149 }
150
151 fn update(&mut self, portfolio_value: f64, realized_pnl_delta: f64) {
153 self.current_value = portfolio_value;
154 self.realized_pnl += realized_pnl_delta;
155 self.unrealized_pnl = portfolio_value - self.starting_value - self.realized_pnl;
156
157 if portfolio_value > self.highest_value {
159 self.highest_value = portfolio_value;
160 }
161
162 let current_drawdown = (self.highest_value - portfolio_value) / self.highest_value;
164 if current_drawdown > self.max_drawdown {
165 self.max_drawdown = current_drawdown;
166 }
167 }
168
169 fn is_daily_loss_limit_reached(&self, max_daily_loss_pct: f64) -> bool {
171 let daily_loss_pct = (self.starting_value - self.current_value) / self.starting_value * 100.0;
172 daily_loss_pct >= max_daily_loss_pct
173 }
174
175 fn daily_loss_pct(&self) -> f64 {
177 (self.starting_value - self.current_value) / self.starting_value * 100.0
178 }
179
180 fn reset(&mut self, portfolio_value: f64) {
182 self.date = Utc::now().date_naive();
183 self.starting_value = portfolio_value;
184 self.current_value = portfolio_value;
185 self.realized_pnl = 0.0;
186 self.unrealized_pnl = 0.0;
187 self.max_drawdown = 0.0;
188 self.highest_value = portfolio_value;
189 }
190}
191
192#[derive(Debug, Clone, PartialEq, Eq, Hash)]
194pub enum AssetClass {
195 Crypto,
197
198 Stablecoin,
200
201 Defi,
203
204 Layer1,
206
207 Layer2,
209
210 Meme,
212
213 NFT,
215
216 Gaming,
218
219 Other,
221}
222
223#[derive(Debug, Clone)]
225pub struct VolatilityData {
226 pub symbol: String,
228
229 pub daily_volatility: f64,
231
232 pub weekly_volatility: f64,
234
235 pub monthly_volatility: f64,
237
238 pub price_history: Vec<f64>,
240
241 pub last_update: DateTime<FixedOffset>,
243}
244
245#[derive(Debug, Clone)]
247pub struct CorrelationData {
248 pub symbol1: String,
250
251 pub symbol2: String,
253
254 pub correlation: f64,
256
257 pub last_update: DateTime<FixedOffset>,
259}
260
261#[derive(Debug, Clone)]
263pub struct PortfolioMetrics {
264 pub value: f64,
266
267 pub volatility: f64,
269
270 pub max_drawdown: f64,
272
273 pub var_95: f64,
275
276 pub var_99: f64,
278
279 pub concentration: HashMap<AssetClass, f64>,
281}
282
283#[derive(Debug)]
285pub struct RiskManager {
286 config: RiskConfig,
288
289 portfolio_value: f64,
291
292 available_margin: f64,
294
295 daily_tracker: DailyRiskTracker,
297
298 stop_loss_orders: HashMap<String, RiskOrder>,
300
301 take_profit_orders: HashMap<String, RiskOrder>,
303
304 emergency_stop: bool,
306
307 asset_classes: HashMap<String, AssetClass>,
309
310 volatility_data: HashMap<String, VolatilityData>,
312
313 correlation_data: HashMap<(String, String), CorrelationData>,
315
316 portfolio_metrics: PortfolioMetrics,
318
319 historical_portfolio_values: Vec<(DateTime<FixedOffset>, f64)>,
321}
322
323impl RiskManager {
324 pub fn new(config: RiskConfig, initial_portfolio_value: f64) -> Self {
326 let available_margin = initial_portfolio_value;
327 let now = Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap());
328
329 Self {
330 config,
331 portfolio_value: initial_portfolio_value,
332 available_margin,
333 daily_tracker: DailyRiskTracker::new(initial_portfolio_value),
334 stop_loss_orders: HashMap::new(),
335 take_profit_orders: HashMap::new(),
336 emergency_stop: false,
337 asset_classes: Self::default_asset_classes(),
338 volatility_data: HashMap::new(),
339 correlation_data: HashMap::new(),
340 portfolio_metrics: PortfolioMetrics {
341 value: initial_portfolio_value,
342 volatility: 0.0,
343 max_drawdown: 0.0,
344 var_95: 0.0,
345 var_99: 0.0,
346 concentration: HashMap::new(),
347 },
348 historical_portfolio_values: vec![(now, initial_portfolio_value)],
349 }
350 }
351
352 pub fn default(initial_portfolio_value: f64) -> Self {
354 Self::new(RiskConfig::default(), initial_portfolio_value)
355 }
356
357 fn default_asset_classes() -> HashMap<String, AssetClass> {
359 let mut map = HashMap::new();
360
361 map.insert("BTC".to_string(), AssetClass::Crypto);
363 map.insert("ETH".to_string(), AssetClass::Crypto);
364 map.insert("BNB".to_string(), AssetClass::Crypto);
365 map.insert("SOL".to_string(), AssetClass::Crypto);
366 map.insert("XRP".to_string(), AssetClass::Crypto);
367 map.insert("ADA".to_string(), AssetClass::Crypto);
368 map.insert("AVAX".to_string(), AssetClass::Crypto);
369
370 map.insert("USDT".to_string(), AssetClass::Stablecoin);
372 map.insert("USDC".to_string(), AssetClass::Stablecoin);
373 map.insert("DAI".to_string(), AssetClass::Stablecoin);
374 map.insert("BUSD".to_string(), AssetClass::Stablecoin);
375
376 map.insert("UNI".to_string(), AssetClass::Defi);
378 map.insert("AAVE".to_string(), AssetClass::Defi);
379 map.insert("MKR".to_string(), AssetClass::Defi);
380 map.insert("COMP".to_string(), AssetClass::Defi);
381 map.insert("SNX".to_string(), AssetClass::Defi);
382 map.insert("SUSHI".to_string(), AssetClass::Defi);
383
384 map.insert("DOT".to_string(), AssetClass::Layer1);
386 map.insert("ATOM".to_string(), AssetClass::Layer1);
387 map.insert("NEAR".to_string(), AssetClass::Layer1);
388 map.insert("ALGO".to_string(), AssetClass::Layer1);
389
390 map.insert("MATIC".to_string(), AssetClass::Layer2);
392 map.insert("LRC".to_string(), AssetClass::Layer2);
393 map.insert("OMG".to_string(), AssetClass::Layer2);
394 map.insert("IMX".to_string(), AssetClass::Layer2);
395
396 map.insert("DOGE".to_string(), AssetClass::Meme);
398 map.insert("SHIB".to_string(), AssetClass::Meme);
399 map.insert("PEPE".to_string(), AssetClass::Meme);
400
401 map.insert("APE".to_string(), AssetClass::NFT);
403 map.insert("SAND".to_string(), AssetClass::NFT);
404 map.insert("MANA".to_string(), AssetClass::NFT);
405
406 map.insert("AXS".to_string(), AssetClass::Gaming);
408 map.insert("ENJ".to_string(), AssetClass::Gaming);
409 map.insert("GALA".to_string(), AssetClass::Gaming);
410
411 map
412 }
413
414 pub fn config(&self) -> &RiskConfig {
416 &self.config
417 }
418
419 pub fn update_config(&mut self, config: RiskConfig) {
421 info!("Updating risk configuration");
422 self.config = config;
423 }
424
425 pub fn update_portfolio_value(&mut self, new_value: f64, realized_pnl_delta: f64) -> Result<()> {
427 let current_date = Utc::now().date_naive();
429 if current_date != self.daily_tracker.date {
430 info!("New trading day, resetting daily risk tracker");
431 self.daily_tracker.reset(new_value);
432 } else {
433 self.daily_tracker.update(new_value, realized_pnl_delta);
435
436 if self.daily_tracker.is_daily_loss_limit_reached(self.config.max_daily_loss_pct) {
438 let daily_loss_pct = self.daily_tracker.daily_loss_pct();
439 warn!("Daily loss limit reached: {:.2}% exceeds {:.2}%",
440 daily_loss_pct, self.config.max_daily_loss_pct);
441
442 self.emergency_stop = true;
444
445 return Err(RiskError::DailyLossLimitReached {
446 current_loss_pct: daily_loss_pct,
447 max_loss_pct: self.config.max_daily_loss_pct,
448 });
449 }
450 }
451
452 self.portfolio_value = new_value;
454 self.available_margin = new_value; Ok(())
457 }
458
459 pub fn validate_order(&self, order: &OrderRequest, current_positions: &HashMap<String, Position>) -> Result<()> {
461 if self.emergency_stop {
463 return Err(RiskError::General("Emergency stop is active".to_string()));
464 }
465
466 if self.daily_tracker.is_daily_loss_limit_reached(self.config.max_daily_loss_pct) {
468 let daily_loss_pct = self.daily_tracker.daily_loss_pct();
469 return Err(RiskError::DailyLossLimitReached {
470 current_loss_pct: daily_loss_pct,
471 max_loss_pct: self.config.max_daily_loss_pct,
472 });
473 }
474
475 if self.portfolio_metrics.max_drawdown > self.config.max_drawdown_pct {
477 return Err(RiskError::DrawdownLimitExceeded {
478 current_drawdown_pct: self.portfolio_metrics.max_drawdown * 100.0,
479 max_drawdown_pct: self.config.max_drawdown_pct * 100.0,
480 });
481 }
482
483 let order_value = match order.price {
485 Some(price) => order.quantity * price,
486 None => {
487 if let Some(position) = current_positions.get(&order.symbol) {
491 order.quantity * position.current_price
492 } else {
493 return Err(RiskError::General(
495 "Cannot validate market order without price information".to_string()
496 ));
497 }
498 }
499 };
500
501 let max_position_value = self.portfolio_value * self.config.max_position_size_pct;
503
504 let mut new_position_value = order_value;
506 if let Some(position) = current_positions.get(&order.symbol) {
507 new_position_value = match order.side {
509 OrderSide::Buy => position.size.abs() * position.current_price + order_value,
510 OrderSide::Sell => {
511 if order.quantity <= position.size {
512 (position.size - order.quantity).abs() * position.current_price
514 } else {
515 (order.quantity - position.size).abs() * position.current_price
517 }
518 }
519 };
520 }
521
522 if let Some(volatility_data) = self.volatility_data.get(&order.symbol) {
524 let volatility_adjusted_max_size = self.calculate_volatility_adjusted_position_size(
525 &order.symbol,
526 max_position_value
527 );
528
529 if new_position_value > volatility_adjusted_max_size {
530 return Err(RiskError::PositionSizeExceeded {
531 message: format!(
532 "Position value ${:.2} exceeds volatility-adjusted limit ${:.2}",
533 new_position_value, volatility_adjusted_max_size
534 ),
535 });
536 }
537 } else if new_position_value > max_position_value {
538 return Err(RiskError::PositionSizeExceeded {
539 message: format!(
540 "Position value ${:.2} exceeds limit ${:.2} ({:.2}% of portfolio)",
541 new_position_value, max_position_value, self.config.max_position_size_pct * 100.0
542 ),
543 });
544 }
545
546 let total_position_value = current_positions.values()
548 .map(|p| p.size.abs() * p.current_price)
549 .sum::<f64>() + order_value;
550
551 let current_leverage = total_position_value / self.portfolio_value;
552 if current_leverage > self.config.max_leverage {
553 return Err(RiskError::LeverageLimitExceeded {
554 current_leverage,
555 max_leverage: self.config.max_leverage,
556 });
557 }
558
559 let required_margin = total_position_value / self.config.max_leverage;
561 if required_margin > self.available_margin {
562 return Err(RiskError::InsufficientMargin {
563 required_margin,
564 available_margin: self.available_margin,
565 });
566 }
567
568 if let Some(asset_class) = self.get_asset_class(&order.symbol) {
570 let new_concentration = self.calculate_concentration_after_order(
571 current_positions,
572 order,
573 asset_class
574 );
575
576 if new_concentration > self.config.max_concentration_pct {
577 return Err(RiskError::ConcentrationLimitExceeded {
578 asset_class: format!("{:?}", asset_class),
579 concentration_pct: new_concentration * 100.0,
580 max_concentration_pct: self.config.max_concentration_pct * 100.0,
581 });
582 }
583 }
584
585 if let Err(e) = self.validate_correlation_limits(current_positions, order) {
587 return Err(e);
588 }
589
590 if let Err(e) = self.validate_portfolio_volatility(current_positions, order) {
592 return Err(e);
593 }
594
595 Ok(())
596 }
597
598 pub fn generate_stop_loss(&self, position: &Position, parent_order_id: &str) -> Option<RiskOrder> {
600 if position.size == 0.0 {
601 return None;
602 }
603
604 let stop_loss_price = if position.size > 0.0 {
606 position.entry_price * (1.0 - self.config.stop_loss_pct)
608 } else {
609 position.entry_price * (1.0 + self.config.stop_loss_pct)
611 };
612
613 Some(RiskOrder {
615 parent_order_id: parent_order_id.to_string(),
616 symbol: position.symbol.clone(),
617 side: if position.size > 0.0 { OrderSide::Sell } else { OrderSide::Buy },
618 order_type: OrderType::StopMarket,
619 quantity: position.size.abs(),
620 trigger_price: stop_loss_price,
621 is_stop_loss: true,
622 is_take_profit: false,
623 })
624 }
625
626 pub fn generate_take_profit(&self, position: &Position, parent_order_id: &str) -> Option<RiskOrder> {
628 if position.size == 0.0 {
629 return None;
630 }
631
632 let take_profit_price = if position.size > 0.0 {
634 position.entry_price * (1.0 + self.config.take_profit_pct)
636 } else {
637 position.entry_price * (1.0 - self.config.take_profit_pct)
639 };
640
641 Some(RiskOrder {
643 parent_order_id: parent_order_id.to_string(),
644 symbol: position.symbol.clone(),
645 side: if position.size > 0.0 { OrderSide::Sell } else { OrderSide::Buy },
646 order_type: OrderType::TakeProfitMarket,
647 quantity: position.size.abs(),
648 trigger_price: take_profit_price,
649 is_stop_loss: false,
650 is_take_profit: true,
651 })
652 }
653
654 pub fn register_stop_loss(&mut self, order: RiskOrder) {
656 self.stop_loss_orders.insert(order.parent_order_id.clone(), order);
657 }
658
659 pub fn register_take_profit(&mut self, order: RiskOrder) {
661 self.take_profit_orders.insert(order.parent_order_id.clone(), order);
662 }
663
664 pub fn check_risk_orders(&mut self, current_prices: &HashMap<String, f64>) -> Vec<RiskOrder> {
666 let mut triggered_orders = Vec::new();
667
668 let mut triggered_stop_loss_ids = Vec::new();
670 for (id, order) in &self.stop_loss_orders {
671 if let Some(¤t_price) = current_prices.get(&order.symbol) {
672 let should_trigger = match order.side {
673 OrderSide::Sell => current_price <= order.trigger_price,
674 OrderSide::Buy => current_price >= order.trigger_price,
675 };
676
677 if should_trigger {
678 triggered_orders.push(order.clone());
679 triggered_stop_loss_ids.push(id.clone());
680 }
681 }
682 }
683
684 for id in triggered_stop_loss_ids {
686 self.stop_loss_orders.remove(&id);
687 }
688
689 let mut triggered_take_profit_ids = Vec::new();
691 for (id, order) in &self.take_profit_orders {
692 if let Some(¤t_price) = current_prices.get(&order.symbol) {
693 let should_trigger = match order.side {
694 OrderSide::Sell => current_price >= order.trigger_price,
695 OrderSide::Buy => current_price <= order.trigger_price,
696 };
697
698 if should_trigger {
699 triggered_orders.push(order.clone());
700 triggered_take_profit_ids.push(id.clone());
701 }
702 }
703 }
704
705 for id in triggered_take_profit_ids {
707 self.take_profit_orders.remove(&id);
708 }
709
710 triggered_orders
711 }
712
713 pub fn should_stop_trading(&self) -> bool {
715 self.emergency_stop ||
716 self.daily_tracker.is_daily_loss_limit_reached(self.config.max_daily_loss_pct)
717 }
718
719 pub fn activate_emergency_stop(&mut self) {
721 warn!("Emergency stop activated");
722 self.emergency_stop = true;
723 }
724
725 pub fn deactivate_emergency_stop(&mut self) {
727 info!("Emergency stop deactivated");
728 self.emergency_stop = false;
729 }
730
731 pub fn daily_risk_metrics(&self) -> (f64, f64, f64) {
733 (
734 self.daily_tracker.daily_loss_pct(),
735 self.daily_tracker.max_drawdown * 100.0,
736 (self.daily_tracker.realized_pnl / self.daily_tracker.starting_value) * 100.0
737 )
738 }
739
740 pub fn calculate_required_margin(&self, position_value: f64) -> f64 {
742 position_value / self.config.max_leverage
743 }
744
745 pub fn available_margin(&self) -> f64 {
747 self.available_margin
748 }
749
750 pub fn update_available_margin(&mut self, margin: f64) {
752 self.available_margin = margin;
753 }
754
755 pub fn calculate_volatility_adjusted_position_size(&self, symbol: &str, base_max_size: f64) -> f64 {
757 if let Some(volatility_data) = self.volatility_data.get(symbol) {
758 let volatility_factor = 1.0 / (1.0 + volatility_data.daily_volatility / 100.0);
761 base_max_size * volatility_factor
762 } else {
763 base_max_size
765 }
766 }
767
768 pub fn get_asset_class(&self, symbol: &str) -> Option<&AssetClass> {
770 self.asset_classes.get(symbol)
771 }
772
773 pub fn calculate_concentration_after_order(
775 &self,
776 current_positions: &HashMap<String, Position>,
777 order: &OrderRequest,
778 asset_class: &AssetClass,
779 ) -> f64 {
780 let mut total_value = 0.0;
781 let mut asset_class_value = 0.0;
782
783 for position in current_positions.values() {
785 let position_value = position.size.abs() * position.current_price;
786 total_value += position_value;
787
788 if let Some(pos_asset_class) = self.asset_classes.get(&position.symbol) {
789 if pos_asset_class == asset_class {
790 asset_class_value += position_value;
791 }
792 }
793 }
794
795 if let Some(order_asset_class) = self.asset_classes.get(&order.symbol) {
797 if order_asset_class == asset_class {
798 let order_value = match order.price {
799 Some(price) => order.quantity * price,
800 None => {
801 if let Some(position) = current_positions.get(&order.symbol) {
803 order.quantity * position.current_price
804 } else {
805 0.0 }
807 }
808 };
809 asset_class_value += order_value;
810 }
811 }
812
813 let order_value = match order.price {
815 Some(price) => order.quantity * price,
816 None => {
817 if let Some(position) = current_positions.get(&order.symbol) {
818 order.quantity * position.current_price
819 } else {
820 0.0
821 }
822 }
823 };
824 total_value += order_value;
825
826 if total_value > 0.0 {
827 asset_class_value / total_value
828 } else {
829 0.0
830 }
831 }
832
833 pub fn validate_correlation_limits(
835 &self,
836 current_positions: &HashMap<String, Position>,
837 order: &OrderRequest,
838 ) -> Result<()> {
839 for position in current_positions.values() {
841 if position.symbol != order.symbol {
842 let key1 = (position.symbol.clone(), order.symbol.clone());
844 let key2 = (order.symbol.clone(), position.symbol.clone());
845
846 if let Some(correlation_data) = self.correlation_data.get(&key1)
847 .or_else(|| self.correlation_data.get(&key2)) {
848
849 if correlation_data.correlation.abs() > self.config.max_correlation_pct {
851 let position_direction = if position.size > 0.0 { 1.0 } else { -1.0 };
852 let order_direction = match order.side {
853 OrderSide::Buy => 1.0,
854 OrderSide::Sell => -1.0,
855 };
856
857 if position_direction * order_direction > 0.0 {
859 return Err(RiskError::CorrelationLimitExceeded {
860 symbol1: position.symbol.clone(),
861 symbol2: order.symbol.clone(),
862 correlation: correlation_data.correlation,
863 max_correlation: self.config.max_correlation_pct,
864 });
865 }
866 }
867 }
868 }
869 }
870
871 Ok(())
872 }
873
874 pub fn validate_portfolio_volatility(
876 &self,
877 current_positions: &HashMap<String, Position>,
878 order: &OrderRequest,
879 ) -> Result<()> {
880 let mut portfolio_variance = 0.0;
882 let mut total_value = 0.0;
883
884 for position in current_positions.values() {
886 if let Some(volatility_data) = self.volatility_data.get(&position.symbol) {
887 let position_value = position.size.abs() * position.current_price;
888 let weight = position_value / self.portfolio_value;
889 portfolio_variance += (weight * volatility_data.daily_volatility / 100.0).powi(2);
890 total_value += position_value;
891 }
892 }
893
894 if let Some(volatility_data) = self.volatility_data.get(&order.symbol) {
896 let order_value = match order.price {
897 Some(price) => order.quantity * price,
898 None => {
899 if let Some(position) = current_positions.get(&order.symbol) {
900 order.quantity * position.current_price
901 } else {
902 return Ok(()); }
904 }
905 };
906
907 let new_total_value = total_value + order_value;
908 let weight = order_value / new_total_value;
909 portfolio_variance += (weight * volatility_data.daily_volatility / 100.0).powi(2);
910 }
911
912 let portfolio_volatility = portfolio_variance.sqrt() * 100.0; if portfolio_volatility > self.config.max_portfolio_volatility_pct {
915 return Err(RiskError::VolatilityLimitExceeded {
916 current_volatility_pct: portfolio_volatility,
917 max_volatility_pct: self.config.max_portfolio_volatility_pct,
918 });
919 }
920
921 Ok(())
922 }
923}
924
925#[cfg(test)]
926mod tests {
927 use super::*;
928 use chrono::TimeZone;
929
930 fn create_test_position(symbol: &str, size: f64, entry_price: f64, current_price: f64) -> Position {
931 Position {
932 symbol: symbol.to_string(),
933 size,
934 entry_price,
935 current_price,
936 unrealized_pnl: (current_price - entry_price) * size,
937 realized_pnl: 0.0,
938 funding_pnl: 0.0,
939 timestamp: Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap()),
940 }
941 }
942
943 fn create_test_order(symbol: &str, side: OrderSide, quantity: f64, price: Option<f64>) -> OrderRequest {
944 OrderRequest {
945 symbol: symbol.to_string(),
946 side,
947 order_type: OrderType::Limit,
948 quantity,
949 price,
950 reduce_only: false,
951 time_in_force: crate::trading_mode_impl::TimeInForce::GoodTillCancel,
952 }
953 }
954
955 #[test]
956 fn test_position_size_validation() {
957 let config = RiskConfig {
958 max_position_size_pct: 0.1, max_daily_loss_pct: 0.02, stop_loss_pct: 0.05, take_profit_pct: 0.1, max_leverage: 3.0, };
964
965 let portfolio_value = 10000.0;
966 let mut risk_manager = RiskManager::new(config, portfolio_value);
967
968 let mut positions = HashMap::new();
969
970 let order = create_test_order("BTC", OrderSide::Buy, 0.1, Some(9000.0));
972 assert!(risk_manager.validate_order(&order, &positions).is_ok());
974
975 let order = create_test_order("BTC", OrderSide::Buy, 0.2, Some(9000.0));
977 assert!(risk_manager.validate_order(&order, &positions).is_err());
979
980 positions.insert(
982 "BTC".to_string(),
983 create_test_position("BTC", 0.05, 8000.0, 9000.0)
984 );
985
986 let order = create_test_order("BTC", OrderSide::Buy, 0.05, Some(9000.0));
988 assert!(risk_manager.validate_order(&order, &positions).is_ok());
992
993 let order = create_test_order("BTC", OrderSide::Buy, 0.07, Some(9000.0));
995 assert!(risk_manager.validate_order(&order, &positions).is_err());
999 }
1000
1001 #[test]
1002 fn test_leverage_validation() {
1003 let config = RiskConfig {
1004 max_position_size_pct: 0.5, max_daily_loss_pct: 0.02, stop_loss_pct: 0.05, take_profit_pct: 0.1, max_leverage: 2.0, };
1010
1011 let portfolio_value = 10000.0;
1012 let mut risk_manager = RiskManager::new(config, portfolio_value);
1013
1014 let mut positions = HashMap::new();
1015 positions.insert(
1016 "ETH".to_string(),
1017 create_test_position("ETH", 2.0, 1500.0, 1600.0)
1018 );
1019 let order = create_test_order("BTC", OrderSide::Buy, 0.1, Some(9000.0));
1023 assert!(risk_manager.validate_order(&order, &positions).is_ok());
1027
1028 let order = create_test_order("BTC", OrderSide::Buy, 2.0, Some(9000.0));
1030 assert!(risk_manager.validate_order(&order, &positions).is_err());
1034 }
1035
1036 #[test]
1037 fn test_daily_loss_limit() {
1038 let config = RiskConfig {
1039 max_position_size_pct: 0.1, max_daily_loss_pct: 2.0, stop_loss_pct: 0.05, take_profit_pct: 0.1, max_leverage: 3.0, };
1045
1046 let portfolio_value = 10000.0;
1047 let mut risk_manager = RiskManager::new(config, portfolio_value);
1048
1049 assert!(risk_manager.update_portfolio_value(9900.0, -100.0).is_ok());
1051
1052 let (daily_loss_pct, _, _) = risk_manager.daily_risk_metrics();
1054 assert_eq!(daily_loss_pct, 1.0);
1055
1056 assert!(risk_manager.update_portfolio_value(9700.0, -200.0).is_err());
1058
1059 assert!(risk_manager.should_stop_trading());
1061 }
1062
1063 #[test]
1064 fn test_stop_loss_generation() {
1065 let config = RiskConfig {
1066 max_position_size_pct: 0.1, max_daily_loss_pct: 2.0, stop_loss_pct: 0.05, take_profit_pct: 0.1, max_leverage: 3.0, };
1072
1073 let portfolio_value = 10000.0;
1074 let risk_manager = RiskManager::new(config, portfolio_value);
1075
1076 let long_position = create_test_position("BTC", 0.1, 10000.0, 10000.0);
1078 let stop_loss = risk_manager.generate_stop_loss(&long_position, "order1").unwrap();
1079
1080 assert_eq!(stop_loss.symbol, "BTC");
1081 assert!(matches!(stop_loss.side, OrderSide::Sell));
1082 assert!(matches!(stop_loss.order_type, OrderType::StopMarket));
1083 assert_eq!(stop_loss.quantity, 0.1);
1084 assert_eq!(stop_loss.trigger_price, 9500.0); let short_position = create_test_position("BTC", -0.1, 10000.0, 10000.0);
1088 let stop_loss = risk_manager.generate_stop_loss(&short_position, "order2").unwrap();
1089
1090 assert_eq!(stop_loss.symbol, "BTC");
1091 assert!(matches!(stop_loss.side, OrderSide::Buy));
1092 assert!(matches!(stop_loss.order_type, OrderType::StopMarket));
1093 assert_eq!(stop_loss.quantity, 0.1);
1094 assert_eq!(stop_loss.trigger_price, 10500.0); }
1096
1097 #[test]
1098 fn test_take_profit_generation() {
1099 let config = RiskConfig {
1100 max_position_size_pct: 0.1, max_daily_loss_pct: 2.0, stop_loss_pct: 0.05, take_profit_pct: 0.1, max_leverage: 3.0, };
1106
1107 let portfolio_value = 10000.0;
1108 let risk_manager = RiskManager::new(config, portfolio_value);
1109
1110 let long_position = create_test_position("BTC", 0.1, 10000.0, 10000.0);
1112 let take_profit = risk_manager.generate_take_profit(&long_position, "order1").unwrap();
1113
1114 assert_eq!(take_profit.symbol, "BTC");
1115 assert!(matches!(take_profit.side, OrderSide::Sell));
1116 assert!(matches!(take_profit.order_type, OrderType::TakeProfitMarket));
1117 assert_eq!(take_profit.quantity, 0.1);
1118 assert_eq!(take_profit.trigger_price, 11000.0); let short_position = create_test_position("BTC", -0.1, 10000.0, 10000.0);
1122 let take_profit = risk_manager.generate_take_profit(&short_position, "order2").unwrap();
1123
1124 assert_eq!(take_profit.symbol, "BTC");
1125 assert!(matches!(take_profit.side, OrderSide::Buy));
1126 assert!(matches!(take_profit.order_type, OrderType::TakeProfitMarket));
1127 assert_eq!(take_profit.quantity, 0.1);
1128 assert_eq!(take_profit.trigger_price, 9000.0); }
1130
1131 #[test]
1132 fn test_risk_orders_triggering() {
1133 let config = RiskConfig {
1134 max_position_size_pct: 0.1,
1135 max_daily_loss_pct: 2.0,
1136 stop_loss_pct: 0.05,
1137 take_profit_pct: 0.1,
1138 max_leverage: 3.0,
1139 };
1140
1141 let portfolio_value = 10000.0;
1142 let mut risk_manager = RiskManager::new(config, portfolio_value);
1143
1144 let long_position = create_test_position("BTC", 0.1, 10000.0, 10000.0);
1146 let stop_loss = risk_manager.generate_stop_loss(&long_position, "order1").unwrap();
1147 risk_manager.register_stop_loss(stop_loss);
1148
1149 let take_profit = risk_manager.generate_take_profit(&long_position, "order1").unwrap();
1151 risk_manager.register_take_profit(take_profit);
1152
1153 let mut current_prices = HashMap::new();
1155 current_prices.insert("BTC".to_string(), 10000.0);
1156 let triggered = risk_manager.check_risk_orders(¤t_prices);
1157 assert_eq!(triggered.len(), 0);
1158
1159 current_prices.insert("BTC".to_string(), 9400.0); let triggered = risk_manager.check_risk_orders(¤t_prices);
1162 assert_eq!(triggered.len(), 1);
1163 assert!(triggered[0].is_stop_loss);
1164
1165 let long_position = create_test_position("BTC", 0.1, 10000.0, 10000.0);
1167 let stop_loss = risk_manager.generate_stop_loss(&long_position, "order2").unwrap();
1168 risk_manager.register_stop_loss(stop_loss);
1169 let take_profit = risk_manager.generate_take_profit(&long_position, "order2").unwrap();
1170 risk_manager.register_take_profit(take_profit);
1171
1172 current_prices.insert("BTC".to_string(), 11100.0); let triggered = risk_manager.check_risk_orders(¤t_prices);
1175 assert_eq!(triggered.len(), 1);
1176 assert!(triggered[0].is_take_profit);
1177 }
1178
1179 #[test]
1180 fn test_emergency_stop() {
1181 let config = RiskConfig::default();
1182 let portfolio_value = 10000.0;
1183 let mut risk_manager = RiskManager::new(config, portfolio_value);
1184
1185 assert!(!risk_manager.should_stop_trading());
1187
1188 risk_manager.activate_emergency_stop();
1190 assert!(risk_manager.should_stop_trading());
1191
1192 let positions = HashMap::new();
1194 let order = create_test_order("BTC", OrderSide::Buy, 0.1, Some(10000.0));
1195 assert!(risk_manager.validate_order(&order, &positions).is_err());
1196
1197 risk_manager.deactivate_emergency_stop();
1199 assert!(!risk_manager.should_stop_trading());
1200
1201 assert!(risk_manager.validate_order(&order, &positions).is_ok());
1203 }
1204 pub fn get_asset_class(&self, symbol: &str) -> Option<&AssetClass> {
1206 self.asset_classes.get(symbol)
1207 }
1208
1209 pub fn set_asset_class(&mut self, symbol: String, asset_class: AssetClass) {
1211 self.asset_classes.insert(symbol, asset_class);
1212 }
1213
1214 fn calculate_concentration_after_order(
1216 &self,
1217 current_positions: &HashMap<String, Position>,
1218 order: &OrderRequest,
1219 asset_class: &AssetClass
1220 ) -> f64 {
1221 let mut asset_class_values = HashMap::new();
1223 let mut total_position_value = 0.0;
1224
1225 for (symbol, position) in current_positions {
1227 let position_value = position.size.abs() * position.current_price;
1228 total_position_value += position_value;
1229
1230 if let Some(class) = self.get_asset_class(symbol) {
1231 *asset_class_values.entry(class).or_insert(0.0) += position_value;
1232 }
1233 }
1234
1235 let order_value = match order.price {
1237 Some(price) => order.quantity * price,
1238 None => {
1239 if let Some(position) = current_positions.get(&order.symbol) {
1240 order.quantity * position.current_price
1241 } else {
1242 0.0
1244 }
1245 }
1246 };
1247
1248 total_position_value += order_value;
1250
1251 *asset_class_values.entry(asset_class).or_insert(0.0) += order_value;
1253
1254 if total_position_value > 0.0 {
1256 asset_class_values.get(asset_class).unwrap_or(&0.0) / total_position_value
1257 } else {
1258 0.0
1259 }
1260 }
1261
1262 pub fn update_volatility_data(&mut self, symbol: String, price_history: Vec<f64>) {
1264 if price_history.len() < 30 {
1265 warn!("Insufficient price history for volatility calculation for {}", symbol);
1266 return;
1267 }
1268
1269 let mut daily_returns = Vec::with_capacity(price_history.len() - 1);
1271 for i in 1..price_history.len() {
1272 let daily_return = (price_history[i] - price_history[i-1]) / price_history[i-1];
1273 daily_returns.push(daily_return);
1274 }
1275
1276 let mean_return = daily_returns.iter().sum::<f64>() / daily_returns.len() as f64;
1278 let variance = daily_returns.iter()
1279 .map(|r| (r - mean_return).powi(2))
1280 .sum::<f64>() / daily_returns.len() as f64;
1281 let daily_volatility = variance.sqrt() * 100.0; let weekly_volatility = daily_volatility * (5.0_f64).sqrt();
1285
1286 let monthly_volatility = daily_volatility * (21.0_f64).sqrt();
1288
1289 self.volatility_data.insert(symbol.clone(), VolatilityData {
1291 symbol,
1292 daily_volatility,
1293 weekly_volatility,
1294 monthly_volatility,
1295 price_history,
1296 last_update: Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap()),
1297 });
1298
1299 self.update_portfolio_metrics();
1301 }
1302
1303 fn calculate_volatility_adjusted_position_size(&self, symbol: &str, base_position_size: f64) -> f64 {
1305 if let Some(volatility_data) = self.volatility_data.get(symbol) {
1306 let volatility_factor = 1.0 - (self.config.volatility_sizing_factor *
1309 (volatility_data.daily_volatility / 100.0));
1310
1311 let adjusted_factor = volatility_factor.max(0.1).min(1.0);
1313
1314 base_position_size * adjusted_factor
1315 } else {
1316 base_position_size
1318 }
1319 }
1320
1321 pub fn update_correlation_data(&mut self, symbol1: String, symbol2: String, price_history1: &[f64], price_history2: &[f64]) {
1323 if price_history1.len() < 30 || price_history2.len() < 30 || price_history1.len() != price_history2.len() {
1324 warn!("Insufficient or mismatched price history for correlation calculation");
1325 return;
1326 }
1327
1328 let mut returns1 = Vec::with_capacity(price_history1.len() - 1);
1330 let mut returns2 = Vec::with_capacity(price_history2.len() - 1);
1331
1332 for i in 1..price_history1.len() {
1333 let return1 = (price_history1[i] - price_history1[i-1]) / price_history1[i-1];
1334 let return2 = (price_history2[i] - price_history2[i-1]) / price_history2[i-1];
1335 returns1.push(return1);
1336 returns2.push(return2);
1337 }
1338
1339 let mean1 = returns1.iter().sum::<f64>() / returns1.len() as f64;
1341 let mean2 = returns2.iter().sum::<f64>() / returns2.len() as f64;
1342
1343 let mut numerator = 0.0;
1344 let mut denom1 = 0.0;
1345 let mut denom2 = 0.0;
1346
1347 for i in 0..returns1.len() {
1348 let diff1 = returns1[i] - mean1;
1349 let diff2 = returns2[i] - mean2;
1350 numerator += diff1 * diff2;
1351 denom1 += diff1 * diff1;
1352 denom2 += diff2 * diff2;
1353 }
1354
1355 let correlation = if denom1 > 0.0 && denom2 > 0.0 {
1356 numerator / (denom1.sqrt() * denom2.sqrt())
1357 } else {
1358 0.0
1359 };
1360
1361 let correlation_data = CorrelationData {
1363 symbol1: symbol1.clone(),
1364 symbol2: symbol2.clone(),
1365 correlation,
1366 last_update: Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap()),
1367 };
1368
1369 self.correlation_data.insert((symbol1.clone(), symbol2.clone()), correlation_data.clone());
1370 self.correlation_data.insert((symbol2, symbol1), CorrelationData {
1371 symbol1: correlation_data.symbol2,
1372 symbol2: correlation_data.symbol1,
1373 correlation: correlation_data.correlation,
1374 last_update: correlation_data.last_update,
1375 });
1376 }
1377
1378 pub fn get_correlation(&self, symbol1: &str, symbol2: &str) -> Option<f64> {
1380 self.correlation_data.get(&(symbol1.to_string(), symbol2.to_string()))
1381 .map(|data| data.correlation)
1382 }
1383
1384 fn validate_correlation_limits(&self, current_positions: &HashMap<String, Position>, order: &OrderRequest) -> Result<()> {
1386 if self.correlation_data.is_empty() || current_positions.is_empty() {
1388 return Ok(());
1389 }
1390
1391 for (symbol, position) in current_positions {
1393 if symbol == &order.symbol || position.size == 0.0 {
1395 continue;
1396 }
1397
1398 if let Some(correlation) = self.get_correlation(&order.symbol, symbol) {
1400 let same_direction = (order.side == OrderSide::Buy && position.size > 0.0) ||
1402 (order.side == OrderSide::Sell && position.size < 0.0);
1403
1404 if same_direction && correlation.abs() > self.config.max_position_correlation {
1405 return Err(RiskError::CorrelationLimitExceeded {
1406 symbol1: order.symbol.clone(),
1407 symbol2: symbol.clone(),
1408 correlation,
1409 max_correlation: self.config.max_position_correlation,
1410 });
1411 }
1412 }
1413 }
1414
1415 Ok(())
1416 }
1417
1418 pub fn update_portfolio_metrics(&mut self) {
1420 if !self.volatility_data.is_empty() {
1422 self.calculate_portfolio_volatility();
1423 }
1424
1425 self.calculate_drawdown();
1427
1428 self.calculate_value_at_risk();
1430 }
1431
1432 fn calculate_portfolio_volatility(&mut self) {
1434 let total_value = self.portfolio_value;
1439 if total_value <= 0.0 {
1440 self.portfolio_metrics.volatility = 0.0;
1441 return;
1442 }
1443
1444 let mut weighted_volatility = 0.0;
1446 let mut total_weighted_value = 0.0;
1447
1448 for (symbol, volatility_data) in &self.volatility_data {
1449 let weight = 1.0 / self.volatility_data.len() as f64;
1452 weighted_volatility += volatility_data.daily_volatility * weight;
1453 total_weighted_value += weight;
1454 }
1455
1456 if total_weighted_value > 0.0 {
1458 self.portfolio_metrics.volatility = weighted_volatility / total_weighted_value;
1459 } else {
1460 self.portfolio_metrics.volatility = 0.0;
1461 }
1462 }
1463
1464 fn calculate_drawdown(&mut self) {
1466 if self.historical_portfolio_values.is_empty() {
1467 self.portfolio_metrics.max_drawdown = 0.0;
1468 return;
1469 }
1470
1471 let mut peak_value = self.historical_portfolio_values[0].1;
1473 let mut max_drawdown = 0.0;
1474
1475 for &(_, value) in &self.historical_portfolio_values {
1476 if value > peak_value {
1477 peak_value = value;
1478 }
1479
1480 let drawdown = if peak_value > 0.0 {
1481 (peak_value - value) / peak_value
1482 } else {
1483 0.0
1484 };
1485
1486 if drawdown > max_drawdown {
1487 max_drawdown = drawdown;
1488 }
1489 }
1490
1491 self.portfolio_metrics.max_drawdown = max_drawdown;
1492 }
1493
1494 fn calculate_value_at_risk(&mut self) {
1496 if self.historical_portfolio_values.len() < 30 {
1500 self.portfolio_metrics.var_95 = 0.0;
1501 self.portfolio_metrics.var_99 = 0.0;
1502 return;
1503 }
1504
1505 let mut daily_returns = Vec::with_capacity(self.historical_portfolio_values.len() - 1);
1507 for i in 1..self.historical_portfolio_values.len() {
1508 let prev_value = self.historical_portfolio_values[i-1].1;
1509 let curr_value = self.historical_portfolio_values[i].1;
1510
1511 if prev_value > 0.0 {
1512 let daily_return = (curr_value - prev_value) / prev_value;
1513 daily_returns.push(daily_return);
1514 }
1515 }
1516
1517 daily_returns.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1519
1520 let index_95 = (daily_returns.len() as f64 * 0.05).floor() as usize;
1522 if index_95 < daily_returns.len() {
1523 self.portfolio_metrics.var_95 = -daily_returns[index_95] * self.portfolio_value;
1524 }
1525
1526 let index_99 = (daily_returns.len() as f64 * 0.01).floor() as usize;
1528 if index_99 < daily_returns.len() {
1529 self.portfolio_metrics.var_99 = -daily_returns[index_99] * self.portfolio_value;
1530 }
1531 }
1532
1533 fn validate_portfolio_volatility(&self, current_positions: &HashMap<String, Position>, order: &OrderRequest) -> Result<()> {
1535 if self.volatility_data.is_empty() {
1537 return Ok(());
1538 }
1539
1540 if let Some(volatility_data) = self.volatility_data.get(&order.symbol) {
1544 let order_value = match order.price {
1546 Some(price) => order.quantity * price,
1547 None => {
1548 if let Some(position) = current_positions.get(&order.symbol) {
1549 order.quantity * position.current_price
1550 } else {
1551 0.0
1552 }
1553 }
1554 };
1555
1556 let position_weight = order_value / self.portfolio_value;
1557
1558 if position_weight > 0.1 && volatility_data.daily_volatility > self.config.max_portfolio_volatility_pct {
1560 return Err(RiskError::VolatilityLimitExceeded {
1561 current_volatility_pct: volatility_data.daily_volatility,
1562 max_volatility_pct: self.config.max_portfolio_volatility_pct,
1563 });
1564 }
1565
1566 if self.portfolio_metrics.volatility > self.config.max_portfolio_volatility_pct * 0.9 {
1568 if volatility_data.daily_volatility > self.portfolio_metrics.volatility {
1570 return Err(RiskError::VolatilityLimitExceeded {
1571 current_volatility_pct: self.portfolio_metrics.volatility,
1572 max_volatility_pct: self.config.max_portfolio_volatility_pct,
1573 });
1574 }
1575 }
1576 }
1577
1578 Ok(())
1579 }
1580
1581 pub fn update_portfolio_value_with_history(&mut self, new_value: f64, realized_pnl_delta: f64) -> Result<()> {
1583 let result = self.update_portfolio_value(new_value, realized_pnl_delta);
1585
1586 let now = Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap());
1588 self.historical_portfolio_values.push((now, new_value));
1589
1590 if self.historical_portfolio_values.len() > 1000 {
1592 self.historical_portfolio_values.remove(0);
1593 }
1594
1595 self.update_portfolio_metrics();
1597
1598 if self.portfolio_metrics.max_drawdown > self.config.max_drawdown_pct {
1600 warn!(
1601 "Maximum drawdown limit reached: {:.2}% exceeds {:.2}%",
1602 self.portfolio_metrics.max_drawdown * 100.0,
1603 self.config.max_drawdown_pct * 100.0
1604 );
1605
1606 self.activate_emergency_stop();
1608
1609 return Err(RiskError::DrawdownLimitExceeded {
1610 current_drawdown_pct: self.portfolio_metrics.max_drawdown * 100.0,
1611 max_drawdown_pct: self.config.max_drawdown_pct * 100.0,
1612 });
1613 }
1614
1615 result
1616 }
1617
1618 pub fn get_portfolio_metrics(&self) -> &PortfolioMetrics {
1620 &self.portfolio_metrics
1621 }
1622
1623 pub fn get_volatility_data(&self, symbol: &str) -> Option<&VolatilityData> {
1625 self.volatility_data.get(symbol)
1626 }
1627
1628 pub fn get_all_volatility_data(&self) -> &HashMap<String, VolatilityData> {
1630 &self.volatility_data
1631 }
1632
1633 pub fn get_all_correlation_data(&self) -> &HashMap<(String, String), CorrelationData> {
1635 &self.correlation_data
1636 }
1637
1638 pub fn calculate_volatility_adjusted_position_size(&self, symbol: &str, base_size: f64) -> f64 {
1640 if let Some(volatility_data) = self.volatility_data.get(symbol) {
1641 let volatility_factor = 1.0 / (1.0 + volatility_data.daily_volatility);
1643 base_size * volatility_factor
1644 } else {
1645 base_size
1646 }
1647 }
1648
1649 pub fn get_asset_class(&self, _symbol: &str) -> Option<String> {
1651 Some("crypto".to_string())
1653 }
1654
1655 pub fn calculate_concentration_after_order(
1657 &self,
1658 current_positions: &HashMap<String, Position>,
1659 order: &OrderRequest,
1660 asset_class: String
1661 ) -> f64 {
1662 let current_class_value: f64 = current_positions.values()
1664 .filter(|p| self.get_asset_class(&p.symbol).as_deref() == Some(&asset_class))
1665 .map(|p| p.size.abs() * p.current_price)
1666 .sum();
1667
1668 let order_value = if self.get_asset_class(&order.symbol).as_deref() == Some(&asset_class) {
1670 order.quantity.abs() * order.price.unwrap_or(0.0)
1671 } else {
1672 0.0
1673 };
1674
1675 let total_class_value = current_class_value + order_value;
1676
1677 if self.portfolio_value > 0.0 {
1679 total_class_value / self.portfolio_value
1680 } else {
1681 0.0
1682 }
1683 }
1684
1685 pub fn validate_correlation_limits(
1687 &self,
1688 current_positions: &HashMap<String, Position>,
1689 order: &OrderRequest
1690 ) -> Result<()> {
1691 for position in current_positions.values() {
1693 if let Some(correlation_data) = self.correlation_data.get(&(position.symbol.clone(), order.symbol.clone())) {
1694 if correlation_data.correlation.abs() > 0.8 {
1695 return Err(RiskError::CorrelationLimitExceeded {
1696 symbol1: position.symbol.clone(),
1697 symbol2: order.symbol.clone(),
1698 correlation: correlation_data.correlation,
1699 max_correlation: 0.8,
1700 });
1701 }
1702 }
1703 }
1704 Ok(())
1705 }
1706
1707 pub fn validate_portfolio_volatility(
1709 &self,
1710 _current_positions: &HashMap<String, Position>,
1711 _order: &OrderRequest
1712 ) -> Result<()> {
1713 if self.portfolio_metrics.volatility > self.config.max_portfolio_volatility_pct {
1715 return Err(RiskError::VolatilityLimitExceeded {
1716 current_volatility_pct: self.portfolio_metrics.volatility,
1717 max_volatility_pct: self.config.max_portfolio_volatility_pct,
1718 });
1719 }
1720 Ok(())
1721 }
1722}