1use crate::portfolio::PortfolioBalance;
8use crate::rsm::engine_deps::{engine_positions_to_portfolio_balance, EngineDeps, EnginePosition};
9use crate::rsm::portfolio_margin::account_builder::{build_account_from_balance, expiry_to_years};
10use hypercall_engine::contract_key;
11use hypercall_engine::order_index::EngineOrderIndex;
12use hypercall_types::api_models::Order as ApiOrder;
13
14pub type EnginePositionMap = HashMap<(WalletAddress, String), EnginePosition>;
16use crate::rsm::margin_mode::MarginMode;
17use crate::rsm::margin_service::SpanMarginService;
18use crate::shared::order_types::{get_timestamp_millis, ParsedSymbol};
19use crate::standard_margin::StandardMarginService;
20use crate::types::{Account, MarginDetails, OptionContract, Position};
21use crate::vol_oracle::SharedVolOracle;
22use hypercall_engine::margin_admission::{
23 decide_portfolio_margin, decide_standard_margin, MarginAdmissionDecision,
24 PortfolioMarginAdmissionInput, StandardMarginAdmissionInput,
25};
26use hypercall_types::OrderInfo;
27use hypercall_types::{to_contract_units_decimal, to_human_readable_decimal, Side, WalletAddress};
28use rust_decimal::prelude::ToPrimitive;
29use rust_decimal::Decimal;
30use rust_decimal_macros::dec;
31use std::collections::HashMap;
32use tracing::{debug, info, warn};
33
34pub struct MarginManager {
38 pub span_margin_service: SpanMarginService,
40 pub standard_margin_service: StandardMarginService,
42}
43
44impl MarginManager {
45 pub fn new(config: &crate::types::Config) -> Self {
47 Self::new_with_vol_oracle(config, None)
48 }
49
50 pub fn new_with_vol_oracle(
51 config: &crate::types::Config,
52 vol_oracle: Option<SharedVolOracle>,
53 ) -> Self {
54 Self {
55 span_margin_service: match vol_oracle {
56 Some(oracle) => SpanMarginService::new_with_vol_oracle(config.clone(), oracle),
57 None => SpanMarginService::new_fail_closed(config.clone()),
58 },
59 standard_margin_service: StandardMarginService::new(),
60 }
61 }
62
63 pub fn check_margin_for_order(
71 &self,
72 deps: &EngineDeps,
73 engine_positions: &EnginePositionMap,
74 balance_ledger: &HashMap<WalletAddress, Decimal>,
75 wallet: &WalletAddress,
76 order_info: &OrderInfo,
77 order_index: &EngineOrderIndex,
78 ) -> Result<(), String> {
79 let margin_mode = self.get_margin_mode(deps, wallet)?;
80
81 debug!(
82 "Unified margin check: Starting for wallet {} on symbol {} (mode: {:?})",
83 wallet, order_info.symbol, margin_mode
84 );
85
86 match margin_mode {
87 MarginMode::Portfolio => self.check_margin_portfolio(
88 deps,
89 engine_positions,
90 balance_ledger,
91 wallet,
92 order_info,
93 order_index,
94 ),
95 MarginMode::Standard => self.check_margin_standard(
96 deps,
97 engine_positions,
98 balance_ledger,
99 wallet,
100 order_info,
101 order_index,
102 ),
103 }
104 }
105
106 pub fn check_margin_for_orders(
116 &self,
117 deps: &EngineDeps,
118 engine_positions: &EnginePositionMap,
119 balance_ledger: &HashMap<WalletAddress, Decimal>,
120 wallet: &WalletAddress,
121 proposed_orders: &[OrderInfo],
122 order_index: &EngineOrderIndex,
123 ) -> Result<(), String> {
124 self.check_margin_for_orders_with_standard_short_bypass(
125 deps,
126 engine_positions,
127 balance_ledger,
128 wallet,
129 proposed_orders,
130 order_index,
131 false,
132 )
133 }
134
135 pub fn check_margin_for_quote_provider_orders(
141 &self,
142 deps: &EngineDeps,
143 engine_positions: &EnginePositionMap,
144 balance_ledger: &HashMap<WalletAddress, Decimal>,
145 wallet: &WalletAddress,
146 proposed_orders: &[OrderInfo],
147 order_index: &EngineOrderIndex,
148 ) -> Result<(), String> {
149 self.check_margin_for_orders_with_standard_short_bypass(
150 deps,
151 engine_positions,
152 balance_ledger,
153 wallet,
154 proposed_orders,
155 order_index,
156 true,
157 )
158 }
159
160 fn check_margin_for_orders_with_standard_short_bypass(
161 &self,
162 deps: &EngineDeps,
163 engine_positions: &EnginePositionMap,
164 balance_ledger: &HashMap<WalletAddress, Decimal>,
165 wallet: &WalletAddress,
166 proposed_orders: &[OrderInfo],
167 order_index: &EngineOrderIndex,
168 allow_standard_short_bypass: bool,
169 ) -> Result<(), String> {
170 if proposed_orders.is_empty() {
171 return Ok(());
172 }
173
174 let margin_mode = self.get_margin_mode(deps, wallet)?;
175
176 debug!(
177 "Unified multi-order margin check: Starting for wallet {} ({} legs, mode: {:?})",
178 wallet,
179 proposed_orders.len(),
180 margin_mode
181 );
182
183 if matches!(margin_mode, MarginMode::Standard) {
184 return self.check_margin_standard_for_orders(
185 deps,
186 engine_positions,
187 balance_ledger,
188 wallet,
189 proposed_orders,
190 order_index,
191 allow_standard_short_bypass,
192 );
193 }
194
195 let hypothetical_account = self.build_hypothetical_account_for_orders(
196 deps,
197 engine_positions,
198 balance_ledger,
199 wallet,
200 proposed_orders,
201 order_index,
202 )?;
203
204 let margin_result = self.run_margin_on_account_with_deps(&hypothetical_account, deps)?;
205
206 match margin_result {
207 None => {
208 info!(
209 "Multi-order margin check: PASSED for wallet {} ({} proposed legs) — no margin details returned",
210 wallet,
211 proposed_orders.len()
212 );
213 Ok(())
214 }
215 Some(details) => {
216 let available_collateral = details.equity;
217 let margin_required = details.initial_margin_required;
218 let excess_margin = available_collateral - margin_required;
219
220 debug!(
221 "Multi-order margin check: wallet={}, legs={}, margin_required={:.2}, available={:.2}, excess={:.2}",
222 wallet,
223 proposed_orders.len(),
224 margin_required,
225 available_collateral,
226 excess_margin
227 );
228
229 if excess_margin < Decimal::ZERO {
230 let error_msg = format!(
231 "Insufficient margin across {} legs: required={:.2}, available={:.2}, shortfall={:.2}",
232 proposed_orders.len(),
233 margin_required,
234 available_collateral,
235 -excess_margin
236 );
237 warn!(
238 "Multi-order margin check: FAILED for wallet {} - {}",
239 wallet, error_msg
240 );
241 Err(error_msg)
242 } else {
243 info!(
244 "Multi-order margin check: PASSED for wallet {} ({} legs) - excess_margin={:.2}",
245 wallet,
246 proposed_orders.len(),
247 excess_margin
248 );
249 Ok(())
250 }
251 }
252 }
253 }
254
255 pub fn get_margin_mode(
257 &self,
258 deps: &EngineDeps,
259 wallet: &WalletAddress,
260 ) -> Result<MarginMode, String> {
261 if let Some(margin_mode) = deps.wallet_margin_modes.get(wallet).copied() {
262 return Ok(margin_mode);
263 }
264
265 let Some(tier_cache) = deps.tier_cache.as_ref() else {
266 return Ok(MarginMode::Standard);
267 };
268
269 tier_cache
270 .get_margin_mode_sync(wallet)
271 .map_err(|error| error.to_string())
272 }
273
274 pub fn get_risk_account(
278 &self,
279 deps: &EngineDeps,
280 engine_positions: &EnginePositionMap,
281 balance_ledger: &HashMap<WalletAddress, Decimal>,
282 wallet: &WalletAddress,
283 ) -> Result<Account, String> {
284 let mut balance =
285 engine_positions_to_portfolio_balance(engine_positions, wallet, &deps.reference_prices);
286
287 let wallet_lower = wallet.to_string().to_lowercase();
290 for ((account, coin), perp) in &deps.perp_positions {
291 if account == &wallet_lower {
292 let symbol = format!("{}-PERP", coin);
293 if !balance.positions.contains_key(&symbol) && perp.size != 0.0 {
294 use crate::portfolio::PositionData;
295 let entry_price = perp.entry_price.ok_or_else(|| {
296 format!(
297 "Missing HyperCore entry price for {} {} - cannot build risk account",
298 wallet, symbol
299 )
300 })?;
301 balance.positions.insert(
302 symbol.clone(),
303 PositionData {
304 symbol,
305 amount: Decimal::try_from(perp.size).map_err(|_| {
306 format!(
307 "Invalid HyperCore perp size for {} {}: {}",
308 wallet, coin, perp.size
309 )
310 })?,
311 entry_price: Decimal::try_from(entry_price).map_err(|_| {
312 format!(
313 "Invalid HyperCore entry price for {} {}: {}",
314 wallet, coin, entry_price
315 )
316 })?,
317 margin_posted: dec!(0),
318 realized_pnl: dec!(0),
319 unrealized_pnl: Decimal::try_from(perp.unrealized_pnl).map_err(
320 |_| {
321 format!(
322 "Invalid HyperCore unrealized PnL for {} {}: {}",
323 wallet, coin, perp.unrealized_pnl
324 )
325 },
326 )?,
327 },
328 );
329 }
330 }
331 }
332
333 let spot_prices = self.build_spot_prices_for_balance(deps, &balance);
334 let mut account = build_account_from_balance(wallet, &balance, Some(*wallet), &spot_prices)
335 .map_err(|e| format!("Cannot build risk account for {}: {}", wallet, e))?;
336
337 let margin_mode = self.get_margin_mode(deps, wallet).ok();
340 let (cash, using_hypercore_equity) = if margin_mode == Some(MarginMode::Portfolio) {
341 if let Some(&hc_equity) = deps.hypercore_account_equity.get(wallet) {
342 (hc_equity, true)
343 } else {
344 (
345 balance_ledger.get(wallet).copied().unwrap_or(Decimal::ZERO),
346 false,
347 )
348 }
349 } else {
350 (
351 balance_ledger.get(wallet).copied().unwrap_or(Decimal::ZERO),
352 false,
353 )
354 };
355
356 if using_hypercore_equity {
360 for position in account.portfolio.values_mut() {
361 position.perp_unrealized_pnl = Decimal::ZERO;
362 }
363 }
364
365 account.cash = cash
366 .to_f64()
367 .ok_or_else(|| format!("Engine balance ledger {} not representable as f64", cash))?;
368 tracing::trace!(
369 wallet = %wallet,
370 cash = %cash,
371 using_hypercore_equity = using_hypercore_equity,
372 balance_wallet_count = balance_ledger.len(),
373 engine_position_count = engine_positions.len(),
374 "Built risk account cash"
375 );
376
377 Ok(account)
378 }
379
380 pub fn build_account_for_risk(
382 &self,
383 deps: &EngineDeps,
384 engine_positions: &EnginePositionMap,
385 _balance_ledger: &HashMap<WalletAddress, Decimal>,
386 wallet: &WalletAddress,
387 ) -> Result<Account, String> {
388 let balance =
389 engine_positions_to_portfolio_balance(engine_positions, wallet, &deps.reference_prices);
390
391 let spot_prices = self.build_spot_prices_for_balance(deps, &balance);
392
393 build_account_from_balance(wallet, &balance, Some(*wallet), &spot_prices)
394 .map_err(|e| format!("Cannot build risk account for {}: {}", wallet, e))
395 }
396
397 pub fn get_span_margin_for_wallet(
399 &self,
400 deps: &EngineDeps,
401 engine_positions: &EnginePositionMap,
402 balance_ledger: &HashMap<WalletAddress, Decimal>,
403 wallet: &WalletAddress,
404 ) -> Result<Option<MarginDetails>, String> {
405 let account = self.get_risk_account(deps, engine_positions, balance_ledger, wallet)?;
406 self.run_margin_on_account(&account)
407 }
408
409 fn build_spot_prices_for_balance(
413 &self,
414 deps: &EngineDeps,
415 balance: &PortfolioBalance,
416 ) -> HashMap<String, f64> {
417 let mut spot_prices = HashMap::new();
418
419 for symbol in balance.positions.keys() {
420 let underlying = if symbol.ends_with("-PERP") {
421 symbol.trim_end_matches("-PERP").to_string()
422 } else if let Ok(parsed) = ParsedSymbol::from_symbol(symbol) {
423 parsed.underlying
424 } else {
425 continue;
426 };
427 if spot_prices.contains_key(&underlying) {
428 continue;
429 }
430 if let Some(&price) = deps.reference_prices.get(&underlying) {
431 spot_prices.insert(underlying, price);
432 }
433 }
434
435 spot_prices
436 }
437
438 pub fn get_spot_price_for_margin(
440 &self,
441 deps: &EngineDeps,
442 underlying: &str,
443 ) -> Result<f64, String> {
444 if let Some(&price) = deps.reference_prices.get(underlying) {
445 return Ok(price);
446 }
447 Err(format!(
448 "No spot price available for {} - no PriceUpdate command received yet",
449 underlying
450 ))
451 }
452
453 pub fn fetch_open_orders(
455 &self,
456 order_index: &EngineOrderIndex,
457 wallet: &WalletAddress,
458 ) -> Vec<ApiOrder> {
459 let summaries = order_index.get_all_orders_for_wallet(wallet);
460
461 let all_orders: Vec<ApiOrder> = summaries
462 .into_iter()
463 .map(|s| {
464 let timestamp = get_timestamp_millis();
465 ApiOrder {
466 order_id: s.order_id as i64,
467 wallet_address: *wallet,
468 symbol: s.symbol.clone(),
469 side: format!("{:?}", s.side),
470 price: s.price,
471 size: s.remaining_size,
472 tif: "gtc".to_string(),
473 status: Some("open".to_string()),
474 created_at: timestamp as i64,
475 updated_at: None,
476 filled_size: Some(s.original_size - s.remaining_size),
477 mmp_enabled: s.mmp_enabled,
478 }
479 })
480 .collect();
481
482 debug!(
483 "Fetched {} open orders for wallet {}",
484 all_orders.len(),
485 wallet
486 );
487
488 all_orders
489 }
490
491 pub fn create_simulated_order(
493 &self,
494 order_info: &OrderInfo,
495 wallet: &WalletAddress,
496 ) -> ApiOrder {
497 let timestamp = get_timestamp_millis();
498 ApiOrder {
499 order_id: 0,
500 wallet_address: *wallet,
501 symbol: order_info.symbol.clone(),
502 side: format!("{:?}", order_info.side),
503 price: order_info.price,
504 size: to_human_readable_decimal(&order_info.symbol, order_info.size),
505 tif: format!("{:?}", order_info.tif).to_lowercase(),
506 status: Some("simulated".to_string()),
507 created_at: timestamp as i64,
508 updated_at: None,
509 filled_size: Some(dec!(0)),
510 mmp_enabled: order_info.mmp_enabled,
511 }
512 }
513
514 fn check_margin_portfolio(
518 &self,
519 deps: &EngineDeps,
520 engine_positions: &EnginePositionMap,
521 balance_ledger: &HashMap<WalletAddress, Decimal>,
522 wallet: &WalletAddress,
523 order_info: &OrderInfo,
524 order_index: &EngineOrderIndex,
525 ) -> Result<(), String> {
526 let is_reduce_only = order_info.reduce_only == Some(true);
527 let hypothetical_account = self.build_hypothetical_account_for_order(
528 deps,
529 engine_positions,
530 balance_ledger,
531 wallet,
532 order_info,
533 order_index,
534 )?;
535
536 let margin_result = self.run_margin_on_account_with_deps(&hypothetical_account, deps)?;
537
538 match margin_result {
539 None => {
540 info!(
541 "Portfolio margin check: PASSED for wallet {} - no margin details returned",
542 wallet
543 );
544 Ok(())
545 }
546 Some(details) => {
547 let available_collateral = details.equity;
548 let margin_required = details.initial_margin_required;
549 let expected_initial_margin =
550 details.scanning_risk.max(details.option_floor) + details.gamma_overlay;
551
552 debug_assert!(
553 (details.initial_margin_required - expected_initial_margin).abs()
554 < dec!(0.000000001),
555 "IM ({}) should equal max(scanning_risk={}, option_floor={}) + gamma_overlay={}",
556 details.initial_margin_required,
557 details.scanning_risk,
558 details.option_floor,
559 details.gamma_overlay
560 );
561 debug!(
562 "Portfolio margin check: equity={}, cash={}, mtm={}",
563 details.equity, hypothetical_account.cash, details.net_option_value
564 );
565
566 let excess_margin = available_collateral - margin_required;
567
568 debug!(
569 "Portfolio margin check: wallet={}, margin_required={:.2}, available={:.2}, excess={:.2}",
570 wallet, margin_required, available_collateral, excess_margin
571 );
572
573 let decision = decide_portfolio_margin(PortfolioMarginAdmissionInput {
574 is_reduce_only,
575 available_collateral,
576 margin_required,
577 settlement_context: None,
578 });
579
580 if matches!(decision, MarginAdmissionDecision::Accepted) && is_reduce_only {
581 info!(
582 "Portfolio margin check: PASSED for wallet {} - aggregate reduce-only order allowed",
583 wallet
584 );
585 return Ok(());
586 }
587
588 if let MarginAdmissionDecision::Rejected(error_msg) = decision {
589 warn!(
590 "Portfolio margin check: FAILED for wallet {} - {}",
591 wallet, error_msg
592 );
593 Err(error_msg)
594 } else {
595 info!(
596 "Portfolio margin check: PASSED for wallet {} - excess_margin={:.2}",
597 wallet, excess_margin
598 );
599 Ok(())
600 }
601 }
602 }
603 }
604
605 fn check_margin_standard(
609 &self,
610 deps: &EngineDeps,
611 engine_positions: &EnginePositionMap,
612 balance_ledger: &HashMap<WalletAddress, Decimal>,
613 wallet: &WalletAddress,
614 order_info: &OrderInfo,
615 order_index: &EngineOrderIndex,
616 ) -> Result<(), String> {
617 if !deps.config.allow_standard_margin_shorts {
618 self.reject_standard_net_short_order(
619 engine_positions,
620 wallet,
621 order_info,
622 order_index,
623 )?;
624 }
625
626 let mut account =
627 match crate::standard_margin::StandardAccountBuilder::build_from_engine_state(
628 wallet,
629 balance_ledger,
630 engine_positions,
631 &deps.reference_prices,
632 ) {
633 Ok(a) => a,
634 Err(e) => {
635 debug!(
636 "StandardAccountBuilder::build_from_engine_state failed for {}: {}, falling back to portfolio margin",
637 wallet, e
638 );
639 return self.check_margin_portfolio(
640 deps,
641 engine_positions,
642 balance_ledger,
643 wallet,
644 order_info,
645 order_index,
646 );
647 }
648 };
649
650 let wallet_lower = wallet.to_string().to_lowercase();
651 for ((account_addr, coin), perp) in &deps.perp_positions {
652 if account_addr != &wallet_lower || perp.size == 0.0 {
653 continue;
654 }
655 let symbol = format!("{}-PERP", coin);
656 if account.perp_positions.iter().any(|p| p.symbol == symbol) {
657 continue;
658 }
659 let entry_price = perp.entry_price.ok_or_else(|| {
660 format!(
661 "Missing HyperCore entry price for {} {} - cannot check standard margin",
662 wallet, symbol
663 )
664 })?;
665 let size = Decimal::try_from(perp.size).map_err(|_| {
666 format!(
667 "Invalid HyperCore perp size for {} {}: {}",
668 wallet, coin, perp.size
669 )
670 })?;
671 let entry_price_decimal = Decimal::try_from(entry_price).map_err(|_| {
672 format!(
673 "Invalid HyperCore entry price for {} {}: {}",
674 wallet, coin, entry_price
675 )
676 })?;
677 let mark_price = Decimal::try_from(entry_price + perp.unrealized_pnl / perp.size)
678 .map_err(|_| {
679 format!(
680 "Invalid HyperCore mark price for {} {} from entry={} pnl={} size={}",
681 wallet, coin, entry_price, perp.unrealized_pnl, perp.size
682 )
683 })?;
684 account
685 .perp_positions
686 .push(hypercall_margin::standard::PerpPosition {
687 symbol,
688 underlying: coin.clone(),
689 size,
690 mark_price,
691 entry_price: entry_price_decimal,
692 });
693 }
694
695 let is_option = ParsedSymbol::from_symbol(&order_info.symbol).is_ok();
696 let order_quantity = to_human_readable_decimal(&order_info.symbol, order_info.size);
697 let is_risk_increasing = self.standard_margin_service.is_risk_increasing(
698 &account,
699 &order_info.symbol,
700 matches!(order_info.side, Side::Buy),
701 order_quantity,
702 is_option,
703 );
704 let is_reduce_only = order_info.reduce_only == Some(true);
705 let requested_premium = if is_option && matches!(order_info.side, Side::Buy) {
706 order_info.price * order_quantity
707 } else {
708 dec!(0)
709 };
710 let proposed_premium = if is_reduce_only {
711 dec!(0)
712 } else if matches!(order_info.side, Side::Buy) {
713 let short_qty = engine_positions
716 .get(&(*wallet, order_info.symbol.clone()))
717 .map(|p| {
718 if p.quantity < dec!(0) {
719 -p.quantity
720 } else {
721 dec!(0)
722 }
723 })
724 .unwrap_or(dec!(0));
725 let opening_qty = (order_quantity - short_qty).max(dec!(0));
726 order_info.price * opening_qty
727 } else {
728 requested_premium
729 };
730
731 let existing_reserved_premium =
732 order_index.calculate_open_buy_premium(wallet, engine_positions);
733
734 let total_reserved_premium = existing_reserved_premium + proposed_premium;
735
736 let mut account_with_existing_open_orders = account.clone();
737 account_with_existing_open_orders.usdc_balance -= existing_reserved_premium;
738
739 self.add_open_sell_orders_to_standard_account(
740 deps,
741 &mut account_with_existing_open_orders,
742 wallet,
743 order_index,
744 )?;
745
746 let base_result = self.standard_margin_service.compute_margin(&account);
747 let existing_open_orders_result = self
748 .standard_margin_service
749 .compute_margin(&account_with_existing_open_orders);
750 let current_open_orders_im =
751 (existing_open_orders_result.position_im - base_result.position_im).max(dec!(0));
752
753 let mut post_trade_account = account_with_existing_open_orders.clone();
754 post_trade_account.usdc_balance -= proposed_premium;
755
756 if is_option && matches!(order_info.side, Side::Sell) {
757 if let Ok(parsed) = ParsedSymbol::from_symbol(&order_info.symbol) {
758 let qty = to_human_readable_decimal(&order_info.symbol, order_info.size);
759 let spot_price_f64 = self
760 .get_spot_price_for_margin(deps, &parsed.underlying)
761 .map_err(|e| {
762 format!(
763 "Cannot check margin for sell order on {}: {}",
764 order_info.symbol, e
765 )
766 })?;
767 let spot_price = Decimal::from_f64_retain(spot_price_f64).ok_or_else(|| {
768 format!(
769 "Invalid spot price for {}: {} - cannot convert to Decimal",
770 parsed.underlying, spot_price_f64
771 )
772 })?;
773 let expiry_ts = expiry_date_to_timestamp(&parsed.underlying, parsed.expiry) as i64;
774 if expiry_ts <= 0 {
775 return Err(format!(
776 "invalid expiry in order symbol {} - cannot compute standard margin safely",
777 order_info.symbol
778 ));
779 }
780
781 let hypothetical_position = hypercall_margin::standard::OptionPosition {
782 symbol: order_info.symbol.clone(),
783 underlying: parsed.underlying.clone(),
784 expiry_ts,
785 strike: parsed.strike,
786 is_call: matches!(parsed.option_type, crate::types::OptionType::Call),
787 size: -qty,
788 mark_price: order_info.price,
789 entry_price: order_info.price,
790 spot_price,
791 };
792 post_trade_account
793 .option_positions
794 .push(hypothetical_position);
795 }
796 }
797
798 if order_info.is_perp {
799 let underlying = order_info
800 .underlying
801 .clone()
802 .unwrap_or_else(|| order_info.symbol.replace("-PERP", ""));
803
804 let size = to_human_readable_decimal(&order_info.symbol, order_info.size);
805 let signed_size = if matches!(order_info.side, Side::Buy) {
806 size
807 } else {
808 -size
809 };
810
811 let hypothetical_perp = hypercall_margin::standard::PerpPosition {
812 symbol: order_info.symbol.clone(),
813 underlying,
814 size: signed_size,
815 mark_price: order_info.price,
816 entry_price: order_info.price,
817 };
818 post_trade_account.perp_positions.push(hypothetical_perp);
819 }
820
821 debug!(
822 "Standard margin check: wallet={}, existing_reserved={:.2}, requested_premium={:.2}, reserved_proposed_premium={:.2}, adjusted_balance={:.2}, option_positions={}, perp_positions={}, reduce_only={}, risk_increasing={}",
823 wallet,
824 existing_reserved_premium,
825 requested_premium,
826 proposed_premium,
827 post_trade_account.usdc_balance,
828 post_trade_account.option_positions.len(),
829 post_trade_account.perp_positions.len(),
830 is_reduce_only,
831 is_risk_increasing
832 );
833
834 let hypothetical_result = self
835 .standard_margin_service
836 .compute_margin(&post_trade_account);
837
838 let post_accept_open_orders_im =
839 (hypothetical_result.position_im - base_result.position_im).max(dec!(0));
840 let incremental_open_orders_im = post_accept_open_orders_im - current_open_orders_im;
841
842 let result = hypothetical_result;
843
844 debug!(
845 "Standard margin check: wallet={}, equity={}, position_im={}, current_open_orders_im={}, incremental_open_orders_im={}, post_accept_open_orders_im={}, total_im={}, reduce_only={}, risk_increasing={}",
846 wallet,
847 result.equity,
848 base_result.position_im,
849 current_open_orders_im,
850 incremental_open_orders_im,
851 post_accept_open_orders_im,
852 result.position_im,
853 is_reduce_only,
854 is_risk_increasing
855 );
856
857 let is_closing_position = !is_risk_increasing
858 && account
859 .option_positions
860 .iter()
861 .any(|p| p.symbol == order_info.symbol && p.size.abs() > dec!(0))
862 || !is_risk_increasing
863 && account
864 .perp_positions
865 .iter()
866 .any(|p| p.symbol == order_info.symbol && p.size.abs() > dec!(0));
867
868 let is_premium_debiting_buy = is_option && matches!(order_info.side, Side::Buy);
869 let decision = decide_standard_margin(StandardMarginAdmissionInput {
870 is_reduce_only,
871 is_closing_position,
872 is_premium_debiting_buy,
873 equity: result.equity,
874 post_balance: post_trade_account.usdc_balance,
875 total_reserved_premium,
876 initial_margin: result.initial_margin,
877 position_im: base_result.position_im,
878 current_open_orders_im,
879 incremental_open_orders_im,
880 post_accept_open_orders_im,
881 });
882
883 if matches!(decision, MarginAdmissionDecision::Accepted)
884 && (is_reduce_only || is_closing_position)
885 {
886 info!(
887 "Standard margin check: PASSED for wallet {} - {} (equity={:.2})",
888 wallet,
889 if is_reduce_only {
890 "reduce-only flag"
891 } else {
892 "closing position"
893 },
894 result.equity
895 );
896 return Ok(());
897 }
898
899 if let MarginAdmissionDecision::Rejected(error_msg) = decision {
900 warn!(
901 "Standard margin check: FAILED for wallet {} - {}",
902 wallet, error_msg
903 );
904 return Err(error_msg);
905 }
906
907 info!(
908 "Standard margin check: PASSED for wallet {} - initial_margin={:.2}",
909 wallet, result.initial_margin
910 );
911 Ok(())
912 }
913
914 fn check_margin_standard_for_orders(
915 &self,
916 deps: &EngineDeps,
917 engine_positions: &EnginePositionMap,
918 balance_ledger: &HashMap<WalletAddress, Decimal>,
919 wallet: &WalletAddress,
920 proposed_orders: &[OrderInfo],
921 order_index: &EngineOrderIndex,
922 allow_standard_short_bypass: bool,
923 ) -> Result<(), String> {
924 if !allow_standard_short_bypass && !deps.config.allow_standard_margin_shorts {
925 self.reject_standard_net_short_orders(
926 engine_positions,
927 wallet,
928 proposed_orders,
929 order_index,
930 )?;
931 }
932
933 let mut simulated_positions = engine_positions.clone();
934 let mut simulated_cash = balance_ledger.clone();
935 let mut wallet_cash = simulated_cash.get(wallet).copied().unwrap_or(Decimal::ZERO);
936 let mut signed_premium = dec!(0);
937
938 for order_info in proposed_orders {
939 let order_quantity = to_human_readable_decimal(&order_info.symbol, order_info.size);
940 if order_quantity <= dec!(0) {
941 return Err(format!(
942 "Invalid RFQ margin quantity {} for {}",
943 order_quantity, order_info.symbol
944 ));
945 }
946
947 let is_option = ParsedSymbol::from_symbol(&order_info.symbol).is_ok();
948 let signed_size = if matches!(order_info.side, Side::Buy) {
949 order_quantity
950 } else {
951 -order_quantity
952 };
953
954 crate::rsm::engine_deps::apply_fill_to_positions(
955 &mut simulated_positions,
956 *wallet,
957 order_info.symbol.clone(),
958 signed_size,
959 order_info.price,
960 );
961
962 if is_option {
963 let premium = order_info.price * order_quantity;
964 if matches!(order_info.side, Side::Buy) {
965 wallet_cash -= premium;
966 signed_premium -= premium;
967 } else {
968 wallet_cash += premium;
969 signed_premium += premium;
970 }
971 }
972 }
973 simulated_cash.insert(*wallet, wallet_cash);
974
975 let account = crate::standard_margin::StandardAccountBuilder::build_from_engine_state(
976 wallet,
977 &simulated_cash,
978 &simulated_positions,
979 &deps.reference_prices,
980 )
981 .map_err(|e| {
982 format!(
983 "Cannot build standard margin account after RFQ fills for {}: {}",
984 wallet, e
985 )
986 })?;
987
988 let existing_reserved_premium =
989 order_index.calculate_open_buy_premium(wallet, &simulated_positions);
990 let mut account_with_open_orders = account.clone();
991 account_with_open_orders.usdc_balance -= existing_reserved_premium;
992 self.add_open_sell_orders_to_standard_account(
993 deps,
994 &mut account_with_open_orders,
995 wallet,
996 order_index,
997 )?;
998
999 let result = self
1000 .standard_margin_service
1001 .compute_margin(&account_with_open_orders);
1002
1003 debug!(
1004 "Standard multi-order margin check: wallet={}, legs={}, signed_premium={:.2}, reserved_open_buy_premium={:.2}, post_cash={:.2}, equity={:.2}, position_im={:.2}, initial_margin={:.2}",
1005 wallet,
1006 proposed_orders.len(),
1007 signed_premium,
1008 existing_reserved_premium,
1009 account_with_open_orders.usdc_balance,
1010 result.equity,
1011 result.position_im,
1012 result.initial_margin
1013 );
1014
1015 if result.equity < dec!(0) {
1016 let error_msg = format!(
1017 "Insufficient funds (Standard): post-trade equity is negative. \
1018 equity={:.2}, signed_premium={:.2}, shortfall={:.2}",
1019 result.equity, signed_premium, -result.equity
1020 );
1021 warn!(
1022 "Standard multi-order margin check: FAILED for wallet {} - {}",
1023 wallet, error_msg
1024 );
1025 return Err(error_msg);
1026 }
1027
1028 if result.initial_margin < dec!(0) {
1029 let error_msg = format!(
1030 "Insufficient margin (Standard): equity={:.2}, position_im={:.2}, reserved_open_buy_premium={:.2}, shortfall={:.2}",
1031 result.equity,
1032 result.position_im,
1033 existing_reserved_premium,
1034 -result.initial_margin
1035 );
1036 warn!(
1037 "Standard multi-order margin check: FAILED for wallet {} - {}",
1038 wallet, error_msg
1039 );
1040 Err(error_msg)
1041 } else {
1042 info!(
1043 "Standard multi-order margin check: PASSED for wallet {} - initial_margin={:.2}",
1044 wallet, result.initial_margin
1045 );
1046 Ok(())
1047 }
1048 }
1049
1050 fn reject_standard_net_short_order(
1051 &self,
1052 engine_positions: &EnginePositionMap,
1053 wallet: &WalletAddress,
1054 order_info: &OrderInfo,
1055 order_index: &EngineOrderIndex,
1056 ) -> Result<(), String> {
1057 self.reject_standard_net_short_orders(
1058 engine_positions,
1059 wallet,
1060 std::slice::from_ref(order_info),
1061 order_index,
1062 )
1063 }
1064
1065 fn reject_standard_net_short_orders(
1066 &self,
1067 engine_positions: &EnginePositionMap,
1068 wallet: &WalletAddress,
1069 proposed_orders: &[OrderInfo],
1070 order_index: &EngineOrderIndex,
1071 ) -> Result<(), String> {
1072 let mut proposed_net_by_contract: HashMap<String, Decimal> = HashMap::new();
1073
1074 for order_info in proposed_orders {
1075 let Some(key) = contract_key(&order_info.symbol) else {
1076 continue;
1077 };
1078 if order_info.is_perp {
1079 continue;
1080 }
1081
1082 let quantity = to_human_readable_decimal(&order_info.symbol, order_info.size);
1083 if quantity <= dec!(0) {
1084 return Err(format!(
1085 "Invalid Standard margin option quantity {} for {}",
1086 quantity, order_info.symbol
1087 ));
1088 }
1089
1090 let signed_quantity = if matches!(order_info.side, Side::Buy) {
1091 quantity
1092 } else {
1093 -quantity
1094 };
1095 *proposed_net_by_contract.entry(key).or_insert(dec!(0)) += signed_quantity;
1096 }
1097
1098 for (contract, proposed_net) in proposed_net_by_contract {
1099 let current_position = engine_positions
1100 .iter()
1101 .filter(|((position_wallet, position_symbol), _)| {
1102 position_wallet == wallet
1103 && contract_key(position_symbol).as_deref() == Some(contract.as_str())
1104 })
1105 .map(|(_, position)| position.quantity)
1106 .sum::<Decimal>();
1107 let open_sell_quantity = order_index.get_open_sells_for_contract(wallet, &contract);
1108 let pre_order_available_long = current_position - open_sell_quantity;
1109 let post_order_available_long = pre_order_available_long + proposed_net;
1110
1111 if post_order_available_long < dec!(0)
1112 && post_order_available_long < pre_order_available_long
1113 {
1114 return Err(format!(
1115 "Standard margin accounts cannot increase net short options. symbol={}, current_position={}, proposed_net={}, open_sell_orders={}, shortfall={}",
1116 contract,
1117 current_position,
1118 proposed_net,
1119 open_sell_quantity,
1120 -post_order_available_long
1121 ));
1122 }
1123 }
1124
1125 Ok(())
1126 }
1127
1128 fn add_open_sell_orders_to_standard_account(
1129 &self,
1130 deps: &EngineDeps,
1131 account: &mut hypercall_margin::standard::StandardAccount,
1132 wallet: &WalletAddress,
1133 order_index: &EngineOrderIndex,
1134 ) -> Result<(), String> {
1135 let open_sell_positions = order_index.get_open_sell_option_positions(wallet);
1136 for mut pos in open_sell_positions {
1137 if pos.position.spot_price == dec!(0) {
1138 let spot_f64 = self
1139 .get_spot_price_for_margin(deps, &pos.position.underlying)
1140 .map_err(|e| {
1141 format!(
1142 "Cannot check margin for open sell order on {}: {}",
1143 pos.position.symbol, e
1144 )
1145 })?;
1146 pos.position.spot_price = Decimal::from_f64_retain(spot_f64).ok_or_else(|| {
1147 format!(
1148 "Invalid spot price for {}: {} - cannot convert to Decimal",
1149 pos.position.underlying, spot_f64
1150 )
1151 })?;
1152 }
1153 account.option_positions.push(pos.position);
1154 }
1155 Ok(())
1156 }
1157
1158 fn build_hypothetical_account_for_orders(
1166 &self,
1167 deps: &EngineDeps,
1168 engine_positions: &EnginePositionMap,
1169 balance_ledger: &HashMap<WalletAddress, Decimal>,
1170 wallet: &WalletAddress,
1171 proposed_orders: &[OrderInfo],
1172 order_index: &EngineOrderIndex,
1173 ) -> Result<Account, String> {
1174 let mut account = self.get_risk_account(deps, engine_positions, balance_ledger, wallet)?;
1175
1176 let open_orders = self.fetch_open_orders(order_index, wallet);
1177 let mut all_orders = open_orders;
1178 for order_info in proposed_orders {
1179 all_orders.push(self.create_simulated_order(order_info, wallet));
1180 }
1181
1182 let mut spot_prices: HashMap<String, f64> = HashMap::new();
1183 for order in &all_orders {
1184 let underlying = if let Ok(parsed) = ParsedSymbol::from_symbol(&order.symbol) {
1185 parsed.underlying.clone()
1186 } else {
1187 perp_underlying(&order.symbol)
1188 .ok_or_else(|| {
1189 format!(
1190 "Unsupported non-option order symbol {} in hypothetical margin simulation",
1191 order.symbol
1192 )
1193 })?
1194 .to_string()
1195 };
1196
1197 if !spot_prices.contains_key(&underlying) {
1198 let price = self.get_spot_price_for_margin(deps, &underlying)?;
1199 spot_prices.insert(underlying.clone(), price);
1200 }
1201 }
1202
1203 debug!(
1204 "build_hypothetical_account_for_orders: wallet={}, executed_positions={}, \
1205 open_orders+proposed={}, proposed_legs={}, spot_prices={:?}",
1206 wallet,
1207 account.portfolio.len(),
1208 all_orders.len(),
1209 proposed_orders.len(),
1210 spot_prices
1211 );
1212
1213 for order in &all_orders {
1214 self.apply_order_to_account(deps, &mut account, order, &spot_prices)?;
1215 }
1216
1217 Ok(account)
1218 }
1219
1220 fn build_hypothetical_account_for_order(
1222 &self,
1223 deps: &EngineDeps,
1224 engine_positions: &EnginePositionMap,
1225 balance_ledger: &HashMap<WalletAddress, Decimal>,
1226 wallet: &WalletAddress,
1227 order_info: &OrderInfo,
1228 order_index: &EngineOrderIndex,
1229 ) -> Result<Account, String> {
1230 let mut account = self.get_risk_account(deps, engine_positions, balance_ledger, wallet)?;
1231
1232 let open_orders = self.fetch_open_orders(order_index, wallet);
1233 let simulated_order = self.create_simulated_order(order_info, wallet);
1234
1235 let mut all_orders = open_orders;
1236 all_orders.push(simulated_order);
1237
1238 let mut spot_prices: HashMap<String, f64> = HashMap::new();
1239 for order in &all_orders {
1240 let underlying = if let Ok(parsed) = ParsedSymbol::from_symbol(&order.symbol) {
1241 parsed.underlying.clone()
1242 } else {
1243 perp_underlying(&order.symbol)
1244 .ok_or_else(|| {
1245 format!(
1246 "Unsupported non-option order symbol {} in hypothetical margin simulation",
1247 order.symbol
1248 )
1249 })?
1250 .to_string()
1251 };
1252
1253 if !spot_prices.contains_key(&underlying) {
1254 let price = self.get_spot_price_for_margin(deps, &underlying)?;
1255 spot_prices.insert(underlying.clone(), price);
1256 }
1257 }
1258
1259 debug!(
1260 "build_hypothetical_account: wallet={}, executed_positions={}, open_orders={}, spot_prices={:?}",
1261 wallet,
1262 account.portfolio.len(),
1263 all_orders.len(),
1264 spot_prices
1265 );
1266
1267 for order in &all_orders {
1268 self.apply_order_to_account(deps, &mut account, order, &spot_prices)?;
1269 }
1270
1271 debug!(
1272 "build_hypothetical_account: final portfolio has {} positions, cash={}",
1273 account.portfolio.len(),
1274 account.cash
1275 );
1276
1277 for (underlying, position) in &account.portfolio {
1278 debug!(
1279 "build_hypothetical_account: underlying={}, spot={}, delta={}, options={}",
1280 underlying,
1281 position.spot,
1282 position.delta,
1283 position.options.len()
1284 );
1285 for option in &position.options {
1286 debug!(
1287 " option: type={:?}, strike={}, expiry={:.4}, qty={}, entry={}",
1288 option.option_type,
1289 option.strike,
1290 option.expiry,
1291 option.quantity,
1292 option.entry_price
1293 );
1294 }
1295 }
1296
1297 Ok(account)
1298 }
1299
1300 fn apply_order_to_account(
1302 &self,
1303 deps: &EngineDeps,
1304 account: &mut Account,
1305 order: &ApiOrder,
1306 spot_prices: &HashMap<String, f64>,
1307 ) -> Result<(), String> {
1308 let remaining_size = order.size - order.filled_size.unwrap_or(dec!(0));
1309 if remaining_size <= dec!(0) {
1310 return Ok(());
1311 }
1312 let remaining_size_u64 = to_contract_units_decimal(&order.symbol, remaining_size);
1313
1314 if let Ok(parsed) = ParsedSymbol::from_symbol(&order.symbol) {
1315 let spot_price = spot_prices
1316 .get(&parsed.underlying)
1317 .copied()
1318 .or_else(|| {
1319 account
1320 .portfolio
1321 .get(&parsed.underlying)
1322 .and_then(|position| position.spot.to_f64())
1323 })
1324 .or_else(|| deps.reference_prices.get(&parsed.underlying).copied());
1325 let spot_price = match spot_price {
1326 Some(spot_price) => spot_price,
1327 None => self
1328 .get_spot_price_for_margin(deps, &parsed.underlying)
1329 .map_err(|error| {
1330 format!(
1331 "Missing spot price for underlying {} (symbol: {}): {}",
1332 parsed.underlying, order.symbol, error
1333 )
1334 })?,
1335 };
1336
1337 let position = account
1338 .portfolio
1339 .entry(parsed.underlying.clone())
1340 .or_insert_with(|| Position {
1341 spot: Decimal::from_f64_retain(spot_price).unwrap_or_else(|| {
1342 panic!(
1343 "STATE_CORRUPTION: validated option spot price {} for {} is not representable as Decimal",
1344 spot_price, parsed.underlying
1345 )
1346 }),
1347 delta: Decimal::ZERO,
1348 perp_unrealized_pnl: Decimal::ZERO,
1349 options: Vec::new(),
1350 });
1351
1352 let size_in_units_decimal =
1353 to_human_readable_decimal(&order.symbol, remaining_size_u64);
1354
1355 let quantity_change = match order.side.as_str() {
1356 "Buy" => size_in_units_decimal,
1357 "Sell" => -size_in_units_decimal,
1358 _ => {
1359 panic!(
1360 "STATE_CORRUPTION: Unknown order side '{}' for order on symbol {}. \
1361 Invalid order state - margin calculation cannot proceed safely. Restart required.",
1362 order.side, order.symbol
1363 );
1364 }
1365 };
1366
1367 let expiry_years = expiry_to_years(&parsed.underlying, parsed.expiry);
1368 let strike_f64 = parsed.strike.to_f64().ok_or_else(|| {
1369 format!(
1370 "invalid strike in order symbol {} - cannot compute margin safely",
1371 order.symbol
1372 )
1373 })?;
1374 let expiry_ts = expiry_date_to_timestamp(&parsed.underlying, parsed.expiry) as i64;
1375 if expiry_ts <= 0 {
1376 return Err(format!(
1377 "invalid expiry in order symbol {} - cannot compute margin safely",
1378 order.symbol
1379 ));
1380 }
1381 let theoretical_price = crate::rsm::black_scholes::black_scholes_with_moments(
1382 &parsed.option_type,
1383 spot_price,
1384 strike_f64,
1385 expiry_years,
1386 0.05,
1387 0.50,
1388 0.0,
1389 0.0,
1390 );
1391
1392 position.options.push(OptionContract {
1393 option_type: parsed.option_type.clone(),
1394 strike: parsed.strike,
1395 expiry_ts,
1396 expiry: Decimal::from_f64_retain(expiry_years).unwrap_or_else(|| {
1397 panic!(
1398 "STATE_CORRUPTION: validated option expiry {} for {} is not representable as Decimal",
1399 expiry_years, order.symbol
1400 )
1401 }),
1402 quantity: quantity_change,
1403 entry_price: Decimal::from_f64_retain(theoretical_price).unwrap_or_else(|| {
1404 panic!(
1405 "STATE_CORRUPTION: theoretical option price {} for {} is not representable as Decimal",
1406 theoretical_price, order.symbol
1407 )
1408 }),
1409 });
1410 } else {
1411 let underlying = perp_underlying(&order.symbol)
1412 .ok_or_else(|| {
1413 format!(
1414 "Unsupported non-option order symbol {} in hypothetical margin simulation",
1415 order.symbol
1416 )
1417 })?
1418 .to_string();
1419 let spot_price = spot_prices
1420 .get(&underlying)
1421 .copied()
1422 .or_else(|| {
1423 account
1424 .portfolio
1425 .get(&underlying)
1426 .and_then(|position| position.spot.to_f64())
1427 })
1428 .or_else(|| deps.reference_prices.get(&underlying).copied());
1429 let spot_price = match spot_price {
1430 Some(spot_price) => spot_price,
1431 None => self
1432 .get_spot_price_for_margin(deps, &underlying)
1433 .map_err(|error| {
1434 format!(
1435 "Missing spot price for perp underlying {} (symbol: {}): {}",
1436 underlying, order.symbol, error
1437 )
1438 })?,
1439 };
1440
1441 let position = account
1442 .portfolio
1443 .entry(underlying.clone())
1444 .or_insert_with(|| Position {
1445 spot: Decimal::from_f64_retain(spot_price).unwrap_or_else(|| {
1446 panic!(
1447 "STATE_CORRUPTION: validated perp spot price {} for {} is not representable as Decimal",
1448 spot_price, underlying
1449 )
1450 }),
1451 delta: Decimal::ZERO,
1452 perp_unrealized_pnl: Decimal::ZERO,
1453 options: Vec::new(),
1454 });
1455
1456 let size_in_units_decimal =
1457 to_human_readable_decimal(&order.symbol, remaining_size_u64);
1458 let delta_change = match order.side.as_str() {
1459 "Buy" => size_in_units_decimal,
1460 "Sell" => -size_in_units_decimal,
1461 _ => {
1462 panic!(
1463 "STATE_CORRUPTION: Unknown order side '{}' for perp order on symbol {}. \
1464 Invalid order state - margin calculation cannot proceed safely. Restart required.",
1465 order.side, order.symbol
1466 );
1467 }
1468 };
1469
1470 position.delta += delta_change;
1471 }
1472
1473 Ok(())
1474 }
1475
1476 pub fn run_margin_on_account(
1478 &self,
1479 account: &Account,
1480 ) -> Result<Option<MarginDetails>, String> {
1481 self.run_margin_on_account_at(account, chrono::Utc::now().timestamp())
1482 }
1483
1484 pub fn run_margin_on_account_with_deps(
1485 &self,
1486 account: &Account,
1487 deps: &EngineDeps,
1488 ) -> Result<Option<MarginDetails>, String> {
1489 self.run_margin_on_account_at(account, deps.margin_timestamp_s)
1490 }
1491
1492 fn run_margin_on_account_at(
1493 &self,
1494 account: &Account,
1495 now_ts: i64,
1496 ) -> Result<Option<MarginDetails>, String> {
1497 let margin_details = self
1498 .span_margin_service
1499 .compute_margin_for_account_at(account, now_ts)
1500 .map_err(|e| e.to_string())?;
1501
1502 debug!(
1503 "run_margin_on_account: account={}, equity={:.2}, IM={:.2}, scanning_risk={:.2}",
1504 account.id,
1505 margin_details.equity,
1506 margin_details.initial_margin_required,
1507 margin_details.scanning_risk
1508 );
1509
1510 Ok(Some(margin_details))
1511 }
1512
1513 pub fn check_tier_restrictions(
1515 &self,
1516 deps: &EngineDeps,
1517 engine_positions: &EnginePositionMap,
1518 balance_ledger: &HashMap<WalletAddress, Decimal>,
1519 wallet: &WalletAddress,
1520 order_info: &OrderInfo,
1521 order_index: &EngineOrderIndex,
1522 ) -> Result<(), String> {
1523 if order_info.is_perp {
1524 return Ok(());
1525 }
1526
1527 let tier_owned = deps
1528 .wallet_tiers
1529 .get(wallet)
1530 .cloned()
1531 .unwrap_or_else(|| "tier2".to_string());
1532 let tier = tier_owned.as_str();
1533
1534 if tier == "tier2" || tier == "market_maker" {
1535 return Ok(());
1536 }
1537
1538 match order_info.side {
1539 Side::Buy => Ok(()),
1540 Side::Sell => {
1541 let parsed = ParsedSymbol::from_symbol(&order_info.symbol)
1542 .map_err(|e| format!("Failed to parse symbol: {}", e))?;
1543
1544 let account =
1545 self.get_risk_account(deps, engine_positions, balance_ledger, wallet)?;
1546 let position = account.portfolio.get(&parsed.underlying);
1547
1548 let mut filled_long_position = dec!(0);
1549 if let Some(pos) = position {
1550 let target_expiry_ts = hypercall_types::expiry_date_to_timestamp(
1551 &parsed.underlying,
1552 parsed.expiry,
1553 ) as i64;
1554 if target_expiry_ts <= 0 {
1555 return Err(format!(
1556 "Invalid expiry in order symbol: {}",
1557 order_info.symbol
1558 ));
1559 }
1560 for option_contract in &pos.options {
1561 if option_contract.option_type == parsed.option_type
1562 && (option_contract.strike - parsed.strike).abs() < dec!(0.01)
1563 && option_contract.expiry_ts == target_expiry_ts
1564 && option_contract.quantity > Decimal::ZERO
1565 {
1566 filled_long_position += option_contract.quantity;
1567 }
1568 }
1569 }
1570
1571 hypercall_engine::admission::validate_tier_sell_restriction(
1572 tier,
1573 wallet,
1574 order_info,
1575 filled_long_position,
1576 order_index,
1577 )
1578 }
1579 }
1580 }
1581}
1582
1583use crate::shared::order_types::perp_underlying;
1584
1585pub fn expiry_date_to_timestamp(underlying: &str, expiry: u64) -> u64 {
1590 u64::try_from(hypercall_types::expiry_date_to_timestamp(
1591 underlying, expiry,
1592 ))
1593 .unwrap_or(0)
1594}