use crate::instrument::{
kind::{
cfd::CfdContract, future::FutureContract, option::OptionContract,
perpetual::PerpetualContract,
},
market_data::kind::{
MarketDataFutureContract, MarketDataInstrumentKind, MarketDataOptionContract,
},
};
use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
pub mod perpetual;
pub mod future;
pub mod option;
pub mod cfd;
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum InstrumentKind<AssetKey> {
Spot,
Perpetual(PerpetualContract<AssetKey>),
Future(FutureContract<AssetKey>),
Option(OptionContract<AssetKey>),
Cfd(CfdContract<AssetKey>),
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum InstrumentKindDiscriminant {
Spot,
Perpetual,
Future,
Option,
Cfd,
}
impl Display for InstrumentKindDiscriminant {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
InstrumentKindDiscriminant::Spot => "spot",
InstrumentKindDiscriminant::Perpetual => "perpetual",
InstrumentKindDiscriminant::Future => "future",
InstrumentKindDiscriminant::Option => "option",
InstrumentKindDiscriminant::Cfd => "cfd",
})
}
}
impl<AssetKey> InstrumentKind<AssetKey> {
pub fn discriminant(&self) -> InstrumentKindDiscriminant {
match self {
InstrumentKind::Spot => InstrumentKindDiscriminant::Spot,
InstrumentKind::Perpetual(_) => InstrumentKindDiscriminant::Perpetual,
InstrumentKind::Future(_) => InstrumentKindDiscriminant::Future,
InstrumentKind::Option(_) => InstrumentKindDiscriminant::Option,
InstrumentKind::Cfd(_) => InstrumentKindDiscriminant::Cfd,
}
}
pub fn contract_size(&self) -> Decimal {
match self {
InstrumentKind::Spot => Decimal::ONE,
InstrumentKind::Perpetual(kind) => kind.contract_size,
InstrumentKind::Future(kind) => kind.contract_size,
InstrumentKind::Option(kind) => kind.contract_size,
InstrumentKind::Cfd(kind) => kind.contract_size,
}
}
pub fn expiry(&self) -> Option<DateTime<Utc>> {
match self {
InstrumentKind::Spot | InstrumentKind::Perpetual(_) | InstrumentKind::Cfd(_) => None,
InstrumentKind::Future(kind) => Some(kind.expiry),
InstrumentKind::Option(kind) => Some(kind.expiry),
}
}
pub fn settlement_asset(&self) -> Option<&AssetKey> {
match self {
InstrumentKind::Spot => None,
InstrumentKind::Perpetual(kind) => Some(&kind.settlement_asset),
InstrumentKind::Future(kind) => Some(&kind.settlement_asset),
InstrumentKind::Option(kind) => Some(&kind.settlement_asset),
InstrumentKind::Cfd(kind) => Some(&kind.settlement_asset),
}
}
pub fn is_split_eligible(&self) -> bool {
match self {
InstrumentKind::Spot => true,
InstrumentKind::Perpetual(_)
| InstrumentKind::Future(_)
| InstrumentKind::Option(_)
| InstrumentKind::Cfd(_) => false,
}
}
pub fn eq_market_data_instrument_kind(&self, other: &MarketDataInstrumentKind) -> bool {
match self {
Self::Spot => matches!(other, MarketDataInstrumentKind::Spot),
Self::Perpetual(_) => matches!(other, MarketDataInstrumentKind::Perpetual),
Self::Cfd(_) => matches!(other, MarketDataInstrumentKind::Cfd),
Self::Future(contract) => matches!(
other,
MarketDataInstrumentKind::Future(other_contract)
if contract.expiry == other_contract.expiry
),
Self::Option(contract) => matches!(
other,
MarketDataInstrumentKind::Option(other_contract)
if contract.kind == other_contract.kind
&& contract.exercise == other_contract.exercise
&& contract.expiry == other_contract.expiry
&& contract.strike == other_contract.strike
),
}
}
}
impl<AssetKey> From<InstrumentKind<AssetKey>> for MarketDataInstrumentKind {
fn from(value: InstrumentKind<AssetKey>) -> Self {
match value {
InstrumentKind::Spot => MarketDataInstrumentKind::Spot,
InstrumentKind::Perpetual(_) => MarketDataInstrumentKind::Perpetual,
InstrumentKind::Cfd(_) => MarketDataInstrumentKind::Cfd,
InstrumentKind::Future(contract) => {
MarketDataInstrumentKind::Future(MarketDataFutureContract {
expiry: contract.expiry,
})
}
InstrumentKind::Option(contract) => {
MarketDataInstrumentKind::Option(MarketDataOptionContract {
kind: contract.kind,
exercise: contract.exercise,
expiry: contract.expiry,
strike: contract.strike,
})
}
}
}
}
impl<AssetKey> From<&InstrumentKind<AssetKey>> for MarketDataInstrumentKind {
fn from(value: &InstrumentKind<AssetKey>) -> Self {
match value {
InstrumentKind::Spot => MarketDataInstrumentKind::Spot,
InstrumentKind::Perpetual(_) => MarketDataInstrumentKind::Perpetual,
InstrumentKind::Cfd(_) => MarketDataInstrumentKind::Cfd,
InstrumentKind::Future(contract) => {
MarketDataInstrumentKind::Future(MarketDataFutureContract {
expiry: contract.expiry,
})
}
InstrumentKind::Option(contract) => {
MarketDataInstrumentKind::Option(MarketDataOptionContract {
kind: contract.kind,
exercise: contract.exercise,
expiry: contract.expiry,
strike: contract.strike,
})
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::instrument::kind::option::{OptionExercise, OptionKind};
use chrono::TimeZone;
fn expiry(day: u32) -> DateTime<Utc> {
#[allow(clippy::unwrap_used)] Utc.with_ymd_and_hms(2026, 7, day, 0, 0, 0).unwrap()
}
fn spot() -> InstrumentKind<&'static str> {
InstrumentKind::Spot
}
fn cfd() -> InstrumentKind<&'static str> {
InstrumentKind::Cfd(CfdContract {
contract_size: Decimal::ONE,
settlement_asset: "usd",
})
}
fn perpetual() -> InstrumentKind<&'static str> {
InstrumentKind::Perpetual(PerpetualContract {
contract_size: Decimal::ONE,
settlement_asset: "usdt",
})
}
fn future(day: u32) -> InstrumentKind<&'static str> {
InstrumentKind::Future(FutureContract {
contract_size: Decimal::ONE,
settlement_asset: "usdt",
expiry: expiry(day),
})
}
fn option(day: u32, strike: Decimal) -> InstrumentKind<&'static str> {
InstrumentKind::Option(OptionContract {
contract_size: Decimal::ONE,
settlement_asset: "usdt",
kind: OptionKind::Call,
exercise: OptionExercise::European,
expiry: expiry(day),
strike,
})
}
#[test]
fn test_eq_market_data_instrument_kind_matches_own_twin() {
for kind in [
spot(),
cfd(),
perpetual(),
future(1),
option(1, Decimal::ONE),
] {
let twin = MarketDataInstrumentKind::from(&kind);
assert!(
kind.eq_market_data_instrument_kind(&twin),
"{kind:?} does not match the twin it converts into"
);
}
}
#[test]
fn test_eq_market_data_instrument_kind_rejects_other_variants() {
let kinds = [
spot(),
cfd(),
perpetual(),
future(1),
option(1, Decimal::ONE),
];
for kind in &kinds {
for other in &kinds {
if std::mem::discriminant(kind) == std::mem::discriminant(other) {
continue;
}
let twin = MarketDataInstrumentKind::from(other);
assert!(
!kind.eq_market_data_instrument_kind(&twin),
"{kind:?} incorrectly matched {twin:?}"
);
}
}
}
#[test]
fn test_eq_market_data_instrument_kind_compares_contract_fields() {
assert!(
!future(1).eq_market_data_instrument_kind(&MarketDataInstrumentKind::from(&future(2)))
);
assert!(!option(1, Decimal::ONE).eq_market_data_instrument_kind(
&MarketDataInstrumentKind::from(&option(2, Decimal::ONE))
));
assert!(!option(1, Decimal::ONE).eq_market_data_instrument_kind(
&MarketDataInstrumentKind::from(&option(1, Decimal::TWO))
));
}
#[test]
fn test_is_split_eligible_admits_only_the_deliverable_equity() {
assert!(spot().is_split_eligible());
for kind in [cfd(), perpetual(), future(1), option(1, Decimal::ONE)] {
assert!(
!kind.is_split_eligible(),
"{kind:?} must not be a corporate-action split target"
);
}
}
#[test]
fn test_cfd_accessors() {
let contract_size = Decimal::from(25);
let kind = InstrumentKind::Cfd(CfdContract {
contract_size,
settlement_asset: "gbp",
});
assert_eq!(kind.contract_size(), contract_size);
assert_eq!(kind.settlement_asset(), Some(&"gbp"));
assert_eq!(kind.expiry(), None);
}
#[test]
fn test_cfd_serde_roundtrip() {
let kind = InstrumentKind::Cfd(CfdContract {
contract_size: Decimal::ONE,
settlement_asset: "usd",
});
#[allow(clippy::unwrap_used)] let json = serde_json::to_string(&kind).unwrap();
assert!(json.contains("\"cfd\""), "{json}");
#[allow(clippy::unwrap_used)] let actual = serde_json::from_str::<InstrumentKind<&str>>(&json).unwrap();
assert_eq!(actual, kind);
}
#[test]
fn test_discriminant_names_each_variant() {
assert_eq!(spot().discriminant(), InstrumentKindDiscriminant::Spot);
assert_eq!(
perpetual().discriminant(),
InstrumentKindDiscriminant::Perpetual
);
assert_eq!(future(1).discriminant(), InstrumentKindDiscriminant::Future);
assert_eq!(
option(1, Decimal::ONE).discriminant(),
InstrumentKindDiscriminant::Option
);
assert_eq!(cfd().discriminant(), InstrumentKindDiscriminant::Cfd);
}
}