1use std::collections::HashMap;
2
3use chrono::{DateTime, FixedOffset, Utc};
4use thiserror::Error;
5
6use crate::unified_data::{OrderRequest, OrderSide, OrderType, Position};
7
8#[derive(Debug, Clone)]
10pub struct RiskConfig {
11 pub max_position_size_pct: f64,
12 pub stop_loss_pct: f64,
13 pub take_profit_pct: f64,
14}
15
16impl Default for RiskConfig {
17 fn default() -> Self {
18 Self {
19 max_position_size_pct: 0.1,
20 stop_loss_pct: 0.05,
21 take_profit_pct: 0.1,
22 }
23 }
24}
25
26#[derive(Debug, Error, Clone)]
28pub enum RiskError {
29 #[error("position size exceeds configured limit: {message}")]
31 PositionSizeExceeded { message: String },
32 #[error("trading is halted by the emergency stop toggle")]
34 TradingHalted,
35}
36
37pub type Result<T> = std::result::Result<T, RiskError>;
39
40#[derive(Debug, Clone)]
42pub struct RiskOrder {
43 pub parent_order_id: String,
45 pub symbol: String,
47 pub side: OrderSide,
49 pub order_type: OrderType,
51 pub quantity: f64,
53 pub trigger_price: f64,
55 pub is_stop_loss: bool,
57 pub is_take_profit: bool,
59 pub created_at: DateTime<FixedOffset>,
61}
62
63impl RiskOrder {
64 fn new(
65 parent_order_id: &str,
66 symbol: &str,
67 side: OrderSide,
68 quantity: f64,
69 trigger_price: f64,
70 is_stop_loss: bool,
71 is_take_profit: bool,
72 ) -> Self {
73 Self {
74 parent_order_id: parent_order_id.to_string(),
75 symbol: symbol.to_string(),
76 side,
77 order_type: OrderType::Market,
78 quantity,
79 trigger_price,
80 is_stop_loss,
81 is_take_profit,
82 created_at: Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap()),
83 }
84 }
85}
86
87#[derive(Debug, Clone)]
89pub struct RiskManager {
90 config: RiskConfig,
91 portfolio_value: f64,
92 stop_losses: Vec<RiskOrder>,
93 take_profits: Vec<RiskOrder>,
94 emergency_stop: bool,
95}
96
97impl RiskManager {
98 pub fn new(config: RiskConfig, portfolio_value: f64) -> Self {
100 Self {
101 config,
102 portfolio_value,
103 stop_losses: Vec::new(),
104 take_profits: Vec::new(),
105 emergency_stop: false,
106 }
107 }
108
109 pub fn config(&self) -> &RiskConfig {
111 &self.config
112 }
113
114 pub fn update_portfolio_value(
118 &mut self,
119 new_value: f64,
120 _realized_pnl_delta: f64,
121 ) -> Result<()> {
122 self.portfolio_value = new_value.max(0.0);
123 Ok(())
124 }
125
126 pub fn validate_order(
128 &self,
129 order: &OrderRequest,
130 _positions: &HashMap<String, Position>,
131 ) -> Result<()> {
132 if self.emergency_stop {
133 return Err(RiskError::TradingHalted);
134 }
135
136 if let Some(price) = order.price {
137 let notional = price * order.quantity.abs();
138 let max_notional = self.config.max_position_size_pct * self.portfolio_value;
139 if max_notional > 0.0 && notional > max_notional {
140 return Err(RiskError::PositionSizeExceeded {
141 message: format!(
142 "order notional {:.2} exceeds {:.2} ({:.2}% of portfolio)",
143 notional,
144 max_notional,
145 self.config.max_position_size_pct * 100.0,
146 ),
147 });
148 }
149 }
150
151 Ok(())
152 }
153
154 pub fn generate_stop_loss(&self, position: &Position, order_id: &str) -> Option<RiskOrder> {
156 if position.size == 0.0 || self.config.stop_loss_pct <= 0.0 {
157 return None;
158 }
159
160 let trigger_price = if position.size > 0.0 {
161 position.entry_price * (1.0 - self.config.stop_loss_pct)
162 } else {
163 position.entry_price * (1.0 + self.config.stop_loss_pct)
164 };
165
166 let side = if position.size > 0.0 {
167 OrderSide::Sell
168 } else {
169 OrderSide::Buy
170 };
171
172 Some(RiskOrder::new(
173 order_id,
174 &position.symbol,
175 side,
176 position.size.abs(),
177 trigger_price,
178 true,
179 false,
180 ))
181 }
182
183 pub fn generate_take_profit(&self, position: &Position, order_id: &str) -> Option<RiskOrder> {
185 if position.size == 0.0 || self.config.take_profit_pct <= 0.0 {
186 return None;
187 }
188
189 let trigger_price = if position.size > 0.0 {
190 position.entry_price * (1.0 + self.config.take_profit_pct)
191 } else {
192 position.entry_price * (1.0 - self.config.take_profit_pct)
193 };
194
195 let side = if position.size > 0.0 {
196 OrderSide::Sell
197 } else {
198 OrderSide::Buy
199 };
200
201 Some(RiskOrder::new(
202 order_id,
203 &position.symbol,
204 side,
205 position.size.abs(),
206 trigger_price,
207 false,
208 true,
209 ))
210 }
211
212 pub fn register_stop_loss(&mut self, order: RiskOrder) {
214 self.stop_losses.push(order);
215 }
216
217 pub fn register_take_profit(&mut self, order: RiskOrder) {
219 self.take_profits.push(order);
220 }
221
222 pub fn check_risk_orders(&mut self, current_prices: &HashMap<String, f64>) -> Vec<RiskOrder> {
224 fn should_trigger(order: &RiskOrder, price: f64) -> bool {
225 if order.is_stop_loss {
226 match order.side {
227 OrderSide::Sell => price <= order.trigger_price,
228 OrderSide::Buy => price >= order.trigger_price,
229 }
230 } else if order.is_take_profit {
231 match order.side {
232 OrderSide::Sell => price >= order.trigger_price,
233 OrderSide::Buy => price <= order.trigger_price,
234 }
235 } else {
236 false
237 }
238 }
239
240 let mut triggered = Vec::new();
241
242 self.stop_losses.retain(|order| {
243 if let Some(price) = current_prices.get(&order.symbol) {
244 if should_trigger(order, *price) {
245 triggered.push(order.clone());
246 return false;
247 }
248 }
249 true
250 });
251
252 self.take_profits.retain(|order| {
253 if let Some(price) = current_prices.get(&order.symbol) {
254 if should_trigger(order, *price) {
255 triggered.push(order.clone());
256 return false;
257 }
258 }
259 true
260 });
261
262 triggered
263 }
264
265 pub fn activate_emergency_stop(&mut self) {
267 self.emergency_stop = true;
268 }
269
270 pub fn deactivate_emergency_stop(&mut self) {
272 self.emergency_stop = false;
273 }
274
275 pub fn should_stop_trading(&self) -> bool {
277 self.emergency_stop
278 }
279}