use crate::utils::ChainError;
use optionstratlib::chains::OptionData;
use optionstratlib::greeks::GreeksSnapshot;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GreekLevel {
None,
First,
All,
}
impl GreekLevel {
pub(crate) fn parse(raw: Option<&str>) -> Result<Self, ChainError> {
match raw {
None => Ok(Self::None),
Some(value) => match value.trim() {
"none" => Ok(Self::None),
"first" => Ok(Self::First),
"all" => Ok(Self::All),
other => Err(ChainError::Validation {
field: "greeks".to_string(),
reason: format!("unknown greek level '{other}'; expected none, first or all"),
}),
},
}
}
#[must_use]
#[inline]
pub(crate) fn wants_greeks(self) -> bool {
!matches!(self, Self::None)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, ToSchema)]
#[serde(deny_unknown_fields)]
pub struct FirstOrderGreeks {
pub theta: Option<f64>,
pub vega: Option<f64>,
pub rho: Option<f64>,
pub rho_d: Option<f64>,
}
impl From<&GreeksSnapshot> for FirstOrderGreeks {
fn from(snapshot: &GreeksSnapshot) -> Self {
Self {
theta: to_f64(snapshot.theta),
vega: to_f64(snapshot.vega),
rho: snapshot.rho.and_then(to_f64),
rho_d: snapshot.rho_d.and_then(to_f64),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, ToSchema)]
#[serde(deny_unknown_fields)]
pub struct FullGreeks {
pub delta: Option<f64>,
pub gamma: Option<f64>,
pub theta: Option<f64>,
pub vega: Option<f64>,
pub rho: Option<f64>,
pub rho_d: Option<f64>,
pub alpha: Option<f64>,
pub vanna: Option<f64>,
pub vomma: Option<f64>,
pub veta: Option<f64>,
pub charm: Option<f64>,
pub color: Option<f64>,
}
impl From<&GreeksSnapshot> for FullGreeks {
fn from(snapshot: &GreeksSnapshot) -> Self {
let GreeksSnapshot {
delta,
gamma,
theta,
vega,
rho,
rho_d,
alpha,
vanna,
vomma,
veta,
charm,
color,
} = snapshot;
Self {
delta: to_f64(*delta),
gamma: to_f64(*gamma),
theta: to_f64(*theta),
vega: to_f64(*vega),
rho: rho.and_then(to_f64),
rho_d: rho_d.and_then(to_f64),
alpha: alpha.and_then(to_f64),
vanna: to_f64(*vanna),
vomma: to_f64(*vomma),
veta: to_f64(*veta),
charm: to_f64(*charm),
color: to_f64(*color),
}
}
}
#[must_use]
#[inline]
fn to_f64(value: Decimal) -> Option<f64> {
use rust_decimal::prelude::ToPrimitive;
value.to_f64()
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, ToSchema)]
#[serde(untagged)]
pub enum GreeksResponse {
Full(FullGreeks),
FirstOrder(FirstOrderGreeks),
}
impl GreeksResponse {
#[must_use]
fn from_snapshot(snapshot: &GreeksSnapshot, level: GreekLevel) -> Option<Self> {
match level {
GreekLevel::None => None,
GreekLevel::First => Some(Self::FirstOrder(FirstOrderGreeks::from(snapshot))),
GreekLevel::All => Some(Self::Full(FullGreeks::from(snapshot))),
}
}
}
pub(crate) async fn admit_render<T, F>(level: GreekLevel, job: F) -> Result<T, ChainError>
where
F: FnOnce() -> Result<T, ChainError> + Send + 'static,
T: Send + 'static,
{
if !level.wants_greeks() {
return job();
}
crate::utils::admission::admit_blocking(job).await
}
pub(crate) async fn render_body<T, F>(level: GreekLevel, render: F) -> Result<Vec<u8>, ChainError>
where
F: FnOnce() -> T + Send + 'static,
T: serde::Serialize + Send + 'static,
{
admit_render(level, move || serialize_body(&render())).await
}
pub(crate) fn serialize_body<T: serde::Serialize>(value: &T) -> Result<Vec<u8>, ChainError> {
serde_json::to_vec(value)
.map_err(|error| ChainError::Internal(format!("failed to encode the response: {error}")))
}
#[must_use]
pub(crate) fn greeks_for(
data: &OptionData,
level: GreekLevel,
) -> (Option<GreeksResponse>, Option<GreeksResponse>) {
if !level.wants_greeks() {
return (None, None);
}
if data.greeks_call.is_some() || data.greeks_put.is_some() {
return (
data.greeks_call
.as_ref()
.and_then(|snapshot| GreeksResponse::from_snapshot(snapshot, level)),
data.greeks_put
.as_ref()
.and_then(|snapshot| GreeksResponse::from_snapshot(snapshot, level)),
);
}
let mut priced = data.clone();
priced.calculate_greeks();
(
priced
.greeks_call
.as_ref()
.and_then(|snapshot| GreeksResponse::from_snapshot(snapshot, level)),
priced
.greeks_put
.as_ref()
.and_then(|snapshot| GreeksResponse::from_snapshot(snapshot, level)),
)
}
#[cfg(test)]
mod tests {
use super::*;
use optionstratlib::ExpirationDate;
use optionstratlib::chains::chain::OptionChain;
use optionstratlib::chains::{OptionChainBuildParams, utils::OptionDataPriceParams};
use positive::{Positive, pos_or_panic};
use rust_decimal_macros::dec;
fn fixture_chain(prepopulated: bool) -> OptionChain {
let price_params = OptionDataPriceParams::new(
Some(Box::new(pos_or_panic!(100.0))),
Some(ExpirationDate::Days(pos_or_panic!(30.0))),
Some(dec!(0.04)),
Some(pos_or_panic!(0.015)),
Some("AAPL".to_string()),
);
let build_params = OptionChainBuildParams::new(
"AAPL".to_string(),
Some(Positive::ONE),
1,
Some(pos_or_panic!(5.0)),
dec!(-0.2),
dec!(0.5),
pos_or_panic!(0.02),
2,
price_params,
pos_or_panic!(0.2),
)
.with_greek_snapshots(prepopulated);
match OptionChain::build_chain(&build_params) {
Ok(chain) => chain,
Err(error) => panic!("the fixture chain must build: {error}"),
}
}
fn fixture_option(prepopulated: bool) -> optionstratlib::chains::OptionData {
let chain = fixture_chain(prepopulated);
match chain.iter().next() {
Some(data) => data.clone(),
None => panic!("the fixture chain must carry a strike"),
}
}
#[test]
fn test_greeks_for_returns_nothing_at_the_default_level() {
let data = fixture_option(false);
assert_eq!(greeks_for(&data, GreekLevel::None), (None, None));
}
#[test]
fn test_greeks_for_prices_an_option_that_carries_no_snapshots() {
let data = fixture_option(false);
assert!(
data.greeks_call.is_none() && data.greeks_put.is_none(),
"the fixture must start without snapshots, or this tests nothing"
);
let (call, put) = greeks_for(&data, GreekLevel::All);
assert!(matches!(call, Some(GreeksResponse::Full(_))));
assert!(matches!(put, Some(GreeksResponse::Full(_))));
assert!(data.greeks_call.is_none() && data.greeks_put.is_none());
}
#[test]
fn test_greeks_for_reads_snapshots_a_chain_already_carries() {
let prepopulated = fixture_option(true);
assert!(
prepopulated.greeks_call.is_some(),
"with_greek_snapshots must populate the call snapshot"
);
let (read_call, read_put) = greeks_for(&prepopulated, GreekLevel::All);
let (priced_call, priced_put) = greeks_for(&fixture_option(false), GreekLevel::All);
assert_eq!(read_call, priced_call, "reading must equal pricing");
assert_eq!(read_put, priced_put, "reading must equal pricing");
}
#[test]
fn test_greeks_for_projects_the_first_order_subset() {
let data = fixture_option(false);
let (first, _) = greeks_for(&data, GreekLevel::First);
let (all, _) = greeks_for(&data, GreekLevel::All);
match (first, all) {
(Some(GreeksResponse::FirstOrder(subset)), Some(GreeksResponse::Full(full))) => {
assert_eq!(subset.theta, full.theta);
assert_eq!(subset.vega, full.vega);
assert_eq!(subset.rho, full.rho);
assert_eq!(subset.rho_d, full.rho_d);
}
other => panic!("each level must yield its own variant, got {other:?}"),
}
}
#[test]
fn test_parse_absent_parameter_is_none() {
match GreekLevel::parse(None) {
Ok(level) => assert_eq!(level, GreekLevel::None),
Err(error) => panic!("an absent parameter must parse: {error}"),
}
}
#[test]
fn test_parse_accepts_the_three_documented_levels() {
for (raw, expected) in [
("none", GreekLevel::None),
("first", GreekLevel::First),
("all", GreekLevel::All),
] {
match GreekLevel::parse(Some(raw)) {
Ok(level) => assert_eq!(level, expected, "for {raw}"),
Err(error) => panic!("{raw} must parse: {error}"),
}
}
}
#[test]
fn test_parse_trims_surrounding_whitespace() {
match GreekLevel::parse(Some(" all ")) {
Ok(level) => assert_eq!(level, GreekLevel::All),
Err(error) => panic!("a padded value must parse: {error}"),
}
}
#[test]
fn test_parse_rejects_an_unknown_level_naming_the_field() {
match GreekLevel::parse(Some("second")) {
Ok(level) => panic!("an unknown level must be rejected, got {level:?}"),
Err(ChainError::Validation { field, reason }) => {
assert_eq!(field, "greeks");
assert!(
reason.contains("second"),
"the reason must quote the offending value, got {reason}"
);
}
Err(other) => panic!("expected a validation failure, got {other:?}"),
}
}
#[test]
fn test_parse_rejects_a_differently_cased_level() {
assert!(GreekLevel::parse(Some("ALL")).is_err());
assert!(GreekLevel::parse(Some("First")).is_err());
}
#[test]
fn test_wants_greeks_is_false_only_for_none() {
assert!(!GreekLevel::None.wants_greeks());
assert!(GreekLevel::First.wants_greeks());
assert!(GreekLevel::All.wants_greeks());
}
}