1use std::collections::{HashMap, HashSet};
2
3use alpaca_core::float;
4use alpaca_time::clock;
5
6use crate::contract;
7use crate::error::{OptionError, OptionResult};
8use crate::numeric;
9use crate::types::{
10 ExecutionAction, ExecutionLeg, ExecutionQuoteRange, OptionPosition, OptionQuote,
11 OptionSnapshot, OrderSide, PositionIntent, PositionSide, QuotedLeg, RollRequest,
12 ScaledExecutionQuote, ScaledExecutionQuoteRange,
13};
14
15pub use crate::types::{
16 ExecutionLegInput, ExecutionSnapshot, Greeks, GreeksInput, RollLegSelection,
17};
18
19const CONTRACT_MULTIPLIER: f64 = 100.0;
20
21pub trait QuoteLike {
22 fn quote(&self) -> OptionQuote;
23}
24
25impl<T: QuoteLike + ?Sized> QuoteLike for &T {
26 fn quote(&self) -> OptionQuote {
27 (*self).quote()
28 }
29}
30
31impl QuoteLike for OptionQuote {
32 fn quote(&self) -> OptionQuote {
33 self.clone()
34 }
35}
36
37impl QuoteLike for OptionSnapshot {
38 fn quote(&self) -> OptionQuote {
39 self.quote.clone()
40 }
41}
42
43impl QuoteLike for OptionPosition {
44 fn quote(&self) -> OptionQuote {
45 self.snapshot_ref()
46 .map(|snapshot| snapshot.quote.clone())
47 .unwrap_or(OptionQuote {
48 bid: None,
49 ask: None,
50 mark: None,
51 last: None,
52 })
53 }
54}
55
56impl QuoteLike for QuotedLeg {
57 fn quote(&self) -> OptionQuote {
58 self.quote.clone()
59 }
60}
61
62pub trait QuoteRangeLike {
63 fn quote_range(&self) -> OptionResult<ExecutionQuoteRange>;
64}
65
66impl<T: QuoteRangeLike + ?Sized> QuoteRangeLike for &T {
67 fn quote_range(&self) -> OptionResult<ExecutionQuoteRange> {
68 (*self).quote_range()
69 }
70}
71
72impl QuoteRangeLike for [OptionPosition] {
73 fn quote_range(&self) -> OptionResult<ExecutionQuoteRange> {
74 range_from_positions(self)
75 }
76}
77
78impl QuoteRangeLike for [QuotedLeg] {
79 fn quote_range(&self) -> OptionResult<ExecutionQuoteRange> {
80 range_from_legs(self)
81 }
82}
83
84fn ensure_finite(name: &str, value: f64) -> OptionResult<()> {
85 if value.is_finite() {
86 Ok(())
87 } else {
88 Err(OptionError::new(
89 "invalid_execution_quote_input",
90 format!("{name} must be finite: {value}"),
91 ))
92 }
93}
94
95fn round_price(value: f64) -> OptionResult<f64> {
96 ensure_finite("quote value", value)?;
97 Ok(float::round(value, 2))
98}
99
100fn quote_value(value: Option<f64>, name: &str) -> OptionResult<f64> {
101 match value {
102 Some(number) => {
103 ensure_finite(name, number)?;
104 Ok(number)
105 }
106 None => Ok(0.0),
107 }
108}
109
110fn normalized_mark(quote: &OptionQuote) -> Option<f64> {
111 if let Some(mark) = quote.mark {
112 return Some(mark);
113 }
114
115 match (quote.bid, quote.ask) {
116 (Some(bid), Some(ask)) => Some(float::round((bid + ask) / 2.0, 12)),
117 (Some(bid), None) => Some(bid),
118 (None, Some(ask)) => Some(ask),
119 (None, None) => quote.last,
120 }
121}
122
123pub fn quote(source: &(impl QuoteLike + ?Sized)) -> OptionQuote {
124 let base = source.quote();
125 let mark = normalized_mark(&base);
126 OptionQuote {
127 bid: base.bid,
128 ask: base.ask,
129 mark,
130 last: base.last.or(mark),
131 }
132}
133
134pub fn limit_price(limit_price: Option<f64>) -> f64 {
135 match limit_price {
136 Some(value) if value.is_finite() => value,
137 _ => 0.0,
138 }
139}
140
141fn quote_bid_ask(option_quote: &OptionQuote) -> OptionResult<(f64, f64)> {
142 let normalized = quote(option_quote);
143 Ok((
144 quote_value(normalized.bid, "bid")?,
145 quote_value(normalized.ask, "ask")?,
146 ))
147}
148
149fn clamp_progress(progress: f64) -> OptionResult<f64> {
150 ensure_finite("progress", progress)?;
151 let normalized = if progress.abs() > 1.0 {
152 progress / 100.0
153 } else {
154 progress
155 };
156 Ok(normalized.clamp(0.0, 1.0))
157}
158
159fn normalized_filter_set(values: Option<&[String]>) -> HashSet<String> {
160 values
161 .into_iter()
162 .flatten()
163 .map(|value| value.trim().to_ascii_lowercase())
164 .filter(|value| !value.is_empty())
165 .collect()
166}
167
168fn derived_leg_type(position: &OptionPosition) -> String {
169 position.leg_type()
170}
171
172fn order_side_for_action(position_side: &PositionSide, action: &ExecutionAction) -> OrderSide {
173 match (position_side, action) {
174 (PositionSide::Long, ExecutionAction::Open) => OrderSide::Buy,
175 (PositionSide::Long, ExecutionAction::Close) => OrderSide::Sell,
176 (PositionSide::Short, ExecutionAction::Open) => OrderSide::Sell,
177 (PositionSide::Short, ExecutionAction::Close) => OrderSide::Buy,
178 }
179}
180
181fn position_intent_for(side: &OrderSide, action: &ExecutionAction) -> PositionIntent {
182 match (side, action) {
183 (OrderSide::Buy, ExecutionAction::Open) => PositionIntent::BuyToOpen,
184 (OrderSide::Sell, ExecutionAction::Open) => PositionIntent::SellToOpen,
185 (OrderSide::Buy, ExecutionAction::Close) => PositionIntent::BuyToClose,
186 (OrderSide::Sell, ExecutionAction::Close) => PositionIntent::SellToClose,
187 }
188}
189
190fn format_snapshot_number(value: Option<f64>, name: &str) -> OptionResult<String> {
191 let rounded = round_price(quote_value(value, name)?)?;
192 Ok(rounded.to_string())
193}
194
195fn execution_snapshot(
196 snapshot: Option<&OptionSnapshot>,
197) -> OptionResult<Option<ExecutionSnapshot>> {
198 Ok(snapshot.map(ExecutionSnapshot::from))
199}
200
201fn execution_leg(
202 symbol: String,
203 leg_type: String,
204 quantity: u32,
205 side: OrderSide,
206 action: &ExecutionAction,
207 snapshot: Option<ExecutionSnapshot>,
208) -> ExecutionLeg {
209 ExecutionLeg {
210 symbol,
211 ratio_qty: quantity.to_string(),
212 position_intent: position_intent_for(&side, action),
213 side,
214 leg_type,
215 snapshot,
216 }
217}
218
219fn normalize_leg_type(leg_type: &str) -> Option<String> {
220 let normalized = leg_type.trim().to_ascii_lowercase();
221 let canonical = match normalized.as_str() {
222 "longcall" | "longcall_low" | "longcall_high" | "bwb_longcall_low"
223 | "bwb_longcall_high" => "longcall",
224 "shortcall" | "bwb_shortcall" => "shortcall",
225 "longput" | "diagonal_longput" => "longput",
226 "shortput" | "diagonal_shortput" => "shortput",
227 _ => return None,
228 };
229 Some(canonical.to_string())
230}
231
232fn normalize_execution_side(side: &str) -> Option<OrderSide> {
233 OrderSide::from_str(side).ok()
234}
235
236fn normalize_position_intent(position_intent: &str) -> Option<PositionIntent> {
237 PositionIntent::from_str(position_intent).ok()
238}
239
240fn leg_side_from_type(leg_type: &str, action: &ExecutionAction) -> Option<OrderSide> {
241 let normalized = normalize_leg_type(leg_type)?;
242 let is_long = normalized.starts_with("long");
243 Some(match (is_long, action) {
244 (true, ExecutionAction::Open) => OrderSide::Buy,
245 (false, ExecutionAction::Open) => OrderSide::Sell,
246 (true, ExecutionAction::Close) => OrderSide::Sell,
247 (false, ExecutionAction::Close) => OrderSide::Buy,
248 })
249}
250
251pub fn leg_type(
252 symbol: &str,
253 side: &str,
254 position_intent: &str,
255 explicit_leg_type: Option<&str>,
256) -> Option<String> {
257 if let Some(explicit_leg_type) = explicit_leg_type.and_then(normalize_leg_type) {
258 return Some(explicit_leg_type);
259 }
260
261 let side = normalize_execution_side(side)?;
262 let position_intent = normalize_position_intent(position_intent)?;
263 let parsed = contract::parse_occ_symbol(symbol)?;
264 let is_close = position_intent.is_close();
265 let is_long = match side {
266 OrderSide::Buy => !is_close,
267 OrderSide::Sell => is_close,
268 };
269
270 Some(format!(
271 "{}{}",
272 if is_long { "long" } else { "short" },
273 parsed.option_right.as_str()
274 ))
275}
276
277fn normalize_roll_quantity(qty: Option<i64>) -> u32 {
278 match qty {
279 Some(value) if value.is_positive() => value.unsigned_abs() as u32,
280 _ => 1,
281 }
282}
283
284pub fn roll_request(
285 current_contract: &str,
286 target_contract: Option<&str>,
287 new_strike: Option<f64>,
288 new_expiration: Option<&str>,
289 leg_type: Option<&str>,
290 qty: Option<i64>,
291) -> Option<RollRequest> {
292 let current_contract = current_contract.trim();
293 if current_contract.is_empty() {
294 return None;
295 }
296
297 let leg_type = match leg_type.map(str::trim).filter(|value| !value.is_empty()) {
298 Some(value) => Some(normalize_leg_type(value)?),
299 None => None,
300 };
301
302 let (new_strike, new_expiration) = match target_contract
303 .map(str::trim)
304 .filter(|value| !value.is_empty())
305 {
306 Some(target_contract) => {
307 let parsed = contract::parse_occ_symbol(target_contract)?;
308 (Some(parsed.strike), parsed.expiration_date)
309 }
310 None => {
311 let new_strike = new_strike.filter(|value| value.is_finite())?;
312 let new_expiration = clock::parse_date(new_expiration?.trim()).ok()?;
313 (Some(new_strike), new_expiration)
314 }
315 };
316
317 Some(RollRequest {
318 current_contract: current_contract.to_string(),
319 leg_type,
320 qty: normalize_roll_quantity(qty),
321 new_strike,
322 new_expiration,
323 })
324}
325
326fn direct_quote(input: &ExecutionLegInput) -> OptionQuote {
327 let bid = input.bid;
328 let ask = input.ask;
329 let price = input.price;
330
331 if bid.is_none() && ask.is_none() && price.is_none() {
332 return OptionQuote {
333 bid: None,
334 ask: None,
335 mark: None,
336 last: None,
337 };
338 }
339
340 if bid.is_none() && ask.is_none() {
341 let price = price.unwrap_or(0.0);
342 if let Some(spread_percent) = input
343 .spread_percent
344 .filter(|value| value.is_finite() && *value > 0.0)
345 {
346 let spread = (price * spread_percent).max(0.0);
347 return quote(&OptionQuote {
348 bid: Some(price - spread / 2.0),
349 ask: Some(price + spread / 2.0),
350 mark: Some(price),
351 last: Some(price),
352 });
353 }
354
355 return quote(&OptionQuote {
356 bid: Some(price),
357 ask: Some(price),
358 mark: Some(price),
359 last: Some(price),
360 });
361 }
362
363 quote(&OptionQuote {
364 bid,
365 ask,
366 mark: price,
367 last: price,
368 })
369}
370
371fn direct_execution_snapshot(input: &ExecutionLegInput) -> OptionResult<Option<ExecutionSnapshot>> {
372 if let Some(snapshot) = &input.snapshot {
373 return Ok(Some(snapshot.clone()));
374 }
375
376 let normalized_quote = direct_quote(input);
377 if normalized_quote.bid.is_none()
378 && normalized_quote.ask.is_none()
379 && normalized_quote.mark.is_none()
380 {
381 return Ok(None);
382 }
383
384 Ok(Some(ExecutionSnapshot {
385 contract: input.contract.clone(),
386 timestamp: input.timestamp.clone().unwrap_or_default(),
387 bid: format_snapshot_number(normalized_quote.bid, "bid")?,
388 ask: format_snapshot_number(normalized_quote.ask, "ask")?,
389 price: format_snapshot_number(normalized_quote.mark.or(normalized_quote.last), "price")?,
390 greeks: input
391 .greeks
392 .as_ref()
393 .map(|greeks| Greeks {
394 delta: greeks.delta.unwrap_or(0.0),
395 gamma: greeks.gamma.unwrap_or(0.0),
396 vega: greeks.vega.unwrap_or(0.0),
397 theta: greeks.theta.unwrap_or(0.0),
398 rho: greeks.rho.unwrap_or(0.0),
399 })
400 .unwrap_or(Greeks {
401 delta: 0.0,
402 gamma: 0.0,
403 vega: 0.0,
404 theta: 0.0,
405 rho: 0.0,
406 }),
407 iv: input.iv.unwrap_or(0.0),
408 }))
409}
410
411pub fn leg(input: ExecutionLegInput) -> Option<ExecutionLeg> {
412 let contract_info = contract::parse_occ_symbol(&input.contract)?;
413 let leg_type = normalize_leg_type(&input.leg_type)?;
414 if !leg_type.ends_with(contract_info.option_right.as_str()) {
415 return None;
416 }
417 let side = leg_side_from_type(&leg_type, &input.action)?;
418
419 Some(execution_leg(
420 input.contract.clone(),
421 leg_type,
422 input.quantity.unwrap_or(1).max(1),
423 side,
424 &input.action,
425 direct_execution_snapshot(&input).ok()?,
426 ))
427}
428
429fn normalized_selection_quantity(quantity: Option<u32>, position_quantity: u32) -> u32 {
430 match quantity {
431 Some(value) if value > 0 => value.min(position_quantity.max(1)),
432 _ => position_quantity.max(1),
433 }
434}
435
436fn range_from_positions(positions: &[OptionPosition]) -> OptionResult<ExecutionQuoteRange> {
437 let mut best = 0.0;
438 let mut worst = 0.0;
439
440 for position in positions {
441 let Some(snapshot) = position.snapshot_ref() else {
442 continue;
443 };
444 let (bid, ask) = quote_bid_ask(&snapshot.quote)?;
445 let quantity = position.quantity() as f64;
446 match position.position_side() {
447 PositionSide::Long => {
448 best += bid * quantity;
449 worst += ask * quantity;
450 }
451 PositionSide::Short => {
452 best -= ask * quantity;
453 worst -= bid * quantity;
454 }
455 }
456 }
457
458 Ok(ExecutionQuoteRange {
459 best_price: round_price(best)?,
460 worst_price: round_price(worst)?,
461 })
462}
463
464fn range_from_legs(legs: &[QuotedLeg]) -> OptionResult<ExecutionQuoteRange> {
465 let mut best = 0.0;
466 let mut worst = 0.0;
467
468 for leg in legs {
469 let (bid, ask) = quote_bid_ask(&leg.quote)?;
470 let quantity = leg.ratio_quantity as f64;
471 match leg.order_side {
472 OrderSide::Buy => {
473 best += bid * quantity;
474 worst += ask * quantity;
475 }
476 OrderSide::Sell => {
477 best -= ask * quantity;
478 worst -= bid * quantity;
479 }
480 }
481 }
482
483 Ok(ExecutionQuoteRange {
484 best_price: round_price(best)?,
485 worst_price: round_price(worst)?,
486 })
487}
488
489pub fn order_legs(
490 positions: &[OptionPosition],
491 action: &str,
492 include_leg_types: Option<&[String]>,
493 exclude_leg_types: Option<&[String]>,
494) -> OptionResult<Vec<ExecutionLeg>> {
495 let action = ExecutionAction::from_str(action)?;
496 let include_leg_types = normalized_filter_set(include_leg_types);
497 let exclude_leg_types = normalized_filter_set(exclude_leg_types);
498 let mut legs = Vec::new();
499
500 for position in positions {
501 if position.quantity() == 0 {
502 continue;
503 }
504
505 let leg_type = derived_leg_type(position);
506 let normalized_leg_type = leg_type.to_ascii_lowercase();
507 if !include_leg_types.is_empty() && !include_leg_types.contains(&normalized_leg_type) {
508 continue;
509 }
510 if exclude_leg_types.contains(&normalized_leg_type) {
511 continue;
512 }
513
514 let side = order_side_for_action(&position.position_side(), &action);
515 legs.push(execution_leg(
516 position.occ_symbol().to_string(),
517 leg_type,
518 position.quantity(),
519 side,
520 &action,
521 execution_snapshot(position.snapshot_ref())?,
522 ));
523 }
524
525 Ok(legs)
526}
527
528pub fn roll_legs(
529 positions: &[OptionPosition],
530 snapshots: &HashMap<String, ExecutionSnapshot>,
531 selections: &[RollLegSelection],
532) -> OptionResult<Vec<ExecutionLeg>> {
533 let mut positions_by_leg_type = HashMap::new();
534 for position in positions {
535 positions_by_leg_type.insert(derived_leg_type(position).to_ascii_lowercase(), position);
536 }
537
538 let mut snapshots_by_leg_type = HashMap::new();
539 for (leg_type, snapshot) in snapshots {
540 snapshots_by_leg_type.insert(leg_type.trim().to_ascii_lowercase(), snapshot.clone());
541 }
542
543 let mut legs = Vec::new();
544 for selection in selections {
545 let normalized_leg_type = selection.leg_type.trim().to_ascii_lowercase();
546 let Some(position) = positions_by_leg_type.get(&normalized_leg_type) else {
547 continue;
548 };
549 let Some(snapshot) = snapshots_by_leg_type.get(&normalized_leg_type) else {
550 continue;
551 };
552
553 let quantity = normalized_selection_quantity(selection.quantity, position.quantity());
554 let close_side = order_side_for_action(&position.position_side(), &ExecutionAction::Close);
555 legs.push(execution_leg(
556 position.occ_symbol().to_string(),
557 normalized_leg_type.clone(),
558 quantity,
559 close_side,
560 &ExecutionAction::Close,
561 execution_snapshot(position.snapshot_ref())?,
562 ));
563
564 let open_side = order_side_for_action(&position.position_side(), &ExecutionAction::Open);
565 legs.push(execution_leg(
566 snapshot.contract.clone(),
567 normalized_leg_type,
568 quantity,
569 open_side,
570 &ExecutionAction::Open,
571 Some(snapshot.clone()),
572 ));
573 }
574
575 Ok(legs)
576}
577
578pub fn best_worst(
579 source: &(impl QuoteRangeLike + ?Sized),
580 structure_quantity: Option<u32>,
581) -> OptionResult<ScaledExecutionQuoteRange> {
582 let per_structure = source.quote_range()?;
583 scale_quote_range(
584 per_structure.best_price,
585 per_structure.worst_price,
586 structure_quantity.unwrap_or(1),
587 )
588}
589
590pub fn scale_quote(price: f64, structure_quantity: u32) -> OptionResult<ScaledExecutionQuote> {
591 ensure_finite("price", price)?;
592 let structure_quantity_f64 = structure_quantity as f64;
593 let normalized_price = round_price(price)?;
594 let total_price = round_price(normalized_price * structure_quantity_f64)?;
595
596 Ok(ScaledExecutionQuote {
597 structure_quantity,
598 price: normalized_price,
599 total_price,
600 total_dollars: round_price(total_price * CONTRACT_MULTIPLIER)?,
601 })
602}
603
604pub fn scale_quote_range(
605 best_price: f64,
606 worst_price: f64,
607 structure_quantity: u32,
608) -> OptionResult<ScaledExecutionQuoteRange> {
609 ensure_finite("best_price", best_price)?;
610 ensure_finite("worst_price", worst_price)?;
611 let structure_quantity_f64 = structure_quantity as f64;
612 let per_structure = ExecutionQuoteRange {
613 best_price: round_price(best_price)?,
614 worst_price: round_price(worst_price)?,
615 };
616 let per_order = ExecutionQuoteRange {
617 best_price: round_price(per_structure.best_price * structure_quantity_f64)?,
618 worst_price: round_price(per_structure.worst_price * structure_quantity_f64)?,
619 };
620 let dollars = ExecutionQuoteRange {
621 best_price: round_price(per_order.best_price * CONTRACT_MULTIPLIER)?,
622 worst_price: round_price(per_order.worst_price * CONTRACT_MULTIPLIER)?,
623 };
624
625 Ok(ScaledExecutionQuoteRange {
626 structure_quantity,
627 per_structure,
628 per_order,
629 dollars,
630 })
631}
632
633pub fn limit_quote_by_progress(
634 best_price: f64,
635 worst_price: f64,
636 progress: f64,
637) -> OptionResult<f64> {
638 ensure_finite("best_price", best_price)?;
639 ensure_finite("worst_price", worst_price)?;
640 let progress = clamp_progress(progress)?;
641 round_price(best_price + (worst_price - best_price) * progress)
642}
643
644pub fn progress_of_limit(best_price: f64, worst_price: f64, limit_price: f64) -> OptionResult<f64> {
645 ensure_finite("best_price", best_price)?;
646 ensure_finite("worst_price", worst_price)?;
647 ensure_finite("limit_price", limit_price)?;
648 if (worst_price - best_price).abs() < 1e-12 {
649 return Ok(0.5);
650 }
651 numeric::round(
652 ((limit_price - best_price) / (worst_price - best_price)).clamp(0.0, 1.0),
653 12,
654 )
655 .map_err(|_| {
656 OptionError::new(
657 "invalid_execution_quote_input",
658 "unable to normalize limit progress",
659 )
660 })
661}