use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)]
pub struct OptionContract<AssetKey> {
pub contract_size: Decimal,
pub settlement_asset: AssetKey,
pub kind: OptionKind,
pub exercise: OptionExercise,
#[serde(default)]
pub settlement: OptionSettlement,
#[serde(with = "chrono::serde::ts_milliseconds")]
pub expiry: DateTime<Utc>,
pub strike: Decimal,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum OptionKind {
#[serde(alias = "CALL", alias = "Call")]
Call,
#[serde(alias = "PUT", alias = "Put")]
Put,
}
impl Display for OptionKind {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
OptionKind::Call => "call",
OptionKind::Put => "put",
}
)
}
}
#[derive(
Debug, Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize,
)]
#[serde(rename_all = "lowercase")]
pub enum OptionSettlement {
#[default]
#[serde(alias = "PHYSICAL", alias = "Physical")]
Physical,
#[serde(alias = "CASH", alias = "Cash")]
Cash,
}
impl Display for OptionSettlement {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
OptionSettlement::Physical => "physical",
OptionSettlement::Cash => "cash",
}
)
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum OptionExercise {
#[serde(alias = "AMERICAN", alias = "American")]
American,
#[serde(alias = "BERMUDAN", alias = "Bermudan")]
Bermudan,
#[serde(alias = "EUROPEAN", alias = "European")]
European,
}
impl Display for OptionExercise {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
OptionExercise::American => "american",
OptionExercise::Bermudan => "bermudan",
OptionExercise::European => "european",
}
)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)] mod tests {
use super::*;
use rust_decimal_macros::dec;
fn contract(settlement: OptionSettlement) -> OptionContract<String> {
OptionContract {
contract_size: dec!(100),
settlement_asset: "usd".to_string(),
kind: OptionKind::Call,
exercise: OptionExercise::American,
settlement,
expiry: "2026-11-20T21:00:00Z".parse().unwrap(),
strike: dec!(700),
}
}
#[test]
fn a_contract_serialised_without_settlement_loads_as_physical() {
let json = r#"{
"contract_size": "100",
"settlement_asset": "usd",
"kind": "call",
"exercise": "american",
"expiry": 1795208400000,
"strike": "700"
}"#;
let contract: OptionContract<String> = serde_json::from_str(json).unwrap();
assert_eq!(contract.settlement, OptionSettlement::Physical);
}
#[test]
fn settlement_round_trips_in_lowercase() {
for settlement in [OptionSettlement::Physical, OptionSettlement::Cash] {
let json = serde_json::to_string(&contract(settlement)).unwrap();
assert!(
json.contains(&format!(r#""settlement":"{settlement}""#)),
"{json}"
);
let back: OptionContract<String> = serde_json::from_str(&json).unwrap();
assert_eq!(back.settlement, settlement);
}
}
#[test]
fn settlement_accepts_capitalised_aliases() {
for (raw, expected) in [
(r#""PHYSICAL""#, OptionSettlement::Physical),
(r#""Physical""#, OptionSettlement::Physical),
(r#""CASH""#, OptionSettlement::Cash),
(r#""Cash""#, OptionSettlement::Cash),
] {
assert_eq!(
serde_json::from_str::<OptionSettlement>(raw).unwrap(),
expected
);
}
}
#[test]
fn contracts_differing_only_in_settlement_are_different_instruments() {
assert_ne!(
contract(OptionSettlement::Physical),
contract(OptionSettlement::Cash)
);
}
}