fx_risk/
engine.rs

1//! Risk engine implementation
2
3use crate::limits::RiskLimits;
4use dashmap::DashMap;
5use fx_utils::{OrderId, Quantity, Result, Side};
6use std::sync::Arc;
7
8/// Current position for an instrument
9#[derive(Debug, Clone)]
10pub struct Position {
11    pub instrument: String,
12    pub quantity: i64, // Positive for long, negative for short
13}
14
15/// Risk engine for pre-trade validation
16pub struct RiskEngine {
17    limits: Arc<RiskLimits>,
18    positions: DashMap<String, Position>,
19    open_orders: DashMap<OrderId, Quantity>,
20}
21
22impl RiskEngine {
23    pub fn new(limits: RiskLimits) -> Self {
24        Self {
25            limits: Arc::new(limits),
26            positions: DashMap::new(),
27            open_orders: DashMap::new(),
28        }
29    }
30
31    pub fn check_order(
32        &self,
33        instrument: &str,
34        side: Side,
35        quantity: Quantity,
36        order_id: OrderId,
37    ) -> Result<()> {
38        // Check order size limit
39        if quantity.0 > self.limits.max_order_size.0 {
40            return Err(fx_utils::Error::InvalidInput(format!(
41                "Order size {} exceeds limit {}",
42                quantity.0, self.limits.max_order_size.0
43            )));
44        }
45
46        // Check open orders limit
47        if self.open_orders.len() >= self.limits.max_open_orders {
48            return Err(fx_utils::Error::InvalidInput(
49                "Maximum open orders limit reached".to_string(),
50            ));
51        }
52
53        // Check position limit
54        let current_position = self
55            .positions
56            .get(instrument)
57            .map(|p| p.quantity)
58            .unwrap_or(0);
59
60        let new_position = match side {
61            Side::Buy => current_position + quantity.0 as i64,
62            Side::Sell => current_position - quantity.0 as i64,
63        };
64
65        #[allow(clippy::cast_abs_to_unsigned)]
66        #[allow(clippy::cast_abs_to_unsigned)]
67        if new_position.abs() as u64 > self.limits.max_position_size.0 {
68            return Err(fx_utils::Error::InvalidInput(format!(
69                "Position limit would be exceeded: {}",
70                new_position
71            )));
72        }
73
74        // Register open order
75        self.open_orders.insert(order_id, quantity);
76
77        Ok(())
78    }
79
80    pub fn update_position(&self, instrument: &str, side: Side, quantity: Quantity) {
81        let mut position = self
82            .positions
83            .entry(instrument.to_string())
84            .or_insert_with(|| Position {
85                instrument: instrument.to_string(),
86                quantity: 0,
87            });
88
89        match side {
90            Side::Buy => position.quantity += quantity.0 as i64,
91            Side::Sell => position.quantity -= quantity.0 as i64,
92        }
93    }
94
95    pub fn remove_order(&self, order_id: OrderId) {
96        self.open_orders.remove(&order_id);
97    }
98
99    /// Get current position for an instrument
100    pub fn get_position(&self, instrument: &str) -> i64 {
101        self.positions
102            .get(instrument)
103            .map(|p| p.quantity)
104            .unwrap_or(0)
105    }
106
107    /// Get all positions (for exposure calculation)
108    pub fn positions(&self) -> &DashMap<String, Position> {
109        &self.positions
110    }
111
112    /// Get open orders (for exposure calculation)
113    pub fn open_orders(&self) -> &DashMap<OrderId, Quantity> {
114        &self.open_orders
115    }
116
117    /// Get risk limits
118    pub fn limits(&self) -> &Arc<RiskLimits> {
119        &self.limits
120    }
121}