use crate::{
Underlying,
asset::Asset,
instrument::{
data_venue::DataVenue,
kind::{
InstrumentKind, cfd::CfdContract, future::FutureContract, option::OptionContract,
perpetual::PerpetualContract,
},
market_data::{MarketDataInstrument, kind::MarketDataInstrumentKind},
name::{InstrumentNameExchange, InstrumentNameInternal},
quote::InstrumentQuoteAsset,
spec::{InstrumentSpec, InstrumentSpecQuantity, OrderQuantityUnits},
},
};
use derive_more::{Constructor, Display};
use serde::{Deserialize, Serialize};
use std::fmt::Formatter;
pub mod kind;
pub mod data_venue;
pub mod name;
pub mod spec;
pub mod market_data;
pub mod quote;
#[derive(
Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Display,
)]
pub struct InstrumentId(pub u64);
#[derive(
Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Constructor,
)]
pub struct InstrumentIndex(pub usize);
impl InstrumentIndex {
pub fn index(&self) -> usize {
self.0
}
}
impl std::fmt::Display for InstrumentIndex {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "InstrumentIndex({})", self.0)
}
}
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)]
pub struct Instrument<ExchangeKey, AssetKey> {
pub exchange: ExchangeKey,
pub name_internal: InstrumentNameInternal,
pub name_exchange: InstrumentNameExchange,
pub underlying: Underlying<AssetKey>,
pub quote: InstrumentQuoteAsset,
#[serde(alias = "instrument_kind")]
pub kind: InstrumentKind<AssetKey>,
pub spec: Option<InstrumentSpec<AssetKey>>,
#[serde(default = "Option::default")]
pub data_venue: Option<DataVenue<ExchangeKey>>,
}
impl<ExchangeKey, AssetKey> Instrument<ExchangeKey, AssetKey> {
pub fn new<NameInternal, NameExchange>(
exchange: ExchangeKey,
name_internal: NameInternal,
name_exchange: NameExchange,
underlying: Underlying<AssetKey>,
quote: InstrumentQuoteAsset,
kind: InstrumentKind<AssetKey>,
spec: Option<InstrumentSpec<AssetKey>>,
) -> Self
where
NameInternal: Into<InstrumentNameInternal>,
NameExchange: Into<InstrumentNameExchange>,
{
Self {
exchange,
name_internal: name_internal.into(),
name_exchange: name_exchange.into(),
quote,
underlying,
kind,
spec,
data_venue: None,
}
}
pub fn spot<NameInternal, NameExchange>(
exchange: ExchangeKey,
name_internal: NameInternal,
name_exchange: NameExchange,
underlying: Underlying<AssetKey>,
spec: Option<InstrumentSpec<AssetKey>>,
) -> Self
where
NameInternal: Into<InstrumentNameInternal>,
NameExchange: Into<InstrumentNameExchange>,
{
Self {
exchange,
name_internal: name_internal.into(),
name_exchange: name_exchange.into(),
quote: InstrumentQuoteAsset::UnderlyingQuote,
underlying,
kind: InstrumentKind::Spot,
spec,
data_venue: None,
}
}
pub fn with_data_venue(mut self, data_venue: DataVenue<ExchangeKey>) -> Self {
self.data_venue = Some(data_venue);
self
}
pub fn data_exchange(&self) -> &ExchangeKey {
self.data_venue
.as_ref()
.map_or(&self.exchange, |data_venue| &data_venue.exchange)
}
pub fn data_name_exchange(&self) -> &InstrumentNameExchange {
self.data_venue
.as_ref()
.and_then(|data_venue| data_venue.name_exchange.as_ref())
.unwrap_or(&self.name_exchange)
}
pub fn map_exchange_key<FnFindExchange, NewExchangeKey>(
self,
find_exchange: FnFindExchange,
) -> Instrument<NewExchangeKey, AssetKey>
where
FnFindExchange: Fn(&ExchangeKey) -> NewExchangeKey,
{
let Instrument {
exchange,
name_internal,
name_exchange,
underlying,
quote,
kind,
spec,
data_venue,
} = self;
let data_venue =
data_venue.map(|data_venue| data_venue.map_exchange_key(|key| find_exchange(key)));
Instrument {
exchange: find_exchange(&exchange),
name_internal,
name_exchange,
underlying,
quote,
kind,
spec,
data_venue,
}
}
pub fn map_asset_key_with_lookup<FnFindAsset, NewAssetKey, Error>(
self,
find_asset: FnFindAsset,
) -> Result<Instrument<ExchangeKey, NewAssetKey>, Error>
where
FnFindAsset: Fn(&AssetKey) -> Result<NewAssetKey, Error>,
{
let Instrument {
exchange,
name_internal,
name_exchange,
underlying,
quote,
kind,
spec,
data_venue,
} = self;
let base_new_key = find_asset(&underlying.base)?;
let quote_new_key = find_asset(&underlying.quote)?;
let kind = match kind {
InstrumentKind::Spot => InstrumentKind::Spot,
InstrumentKind::Perpetual(contract) => InstrumentKind::Perpetual(PerpetualContract {
contract_size: contract.contract_size,
settlement_asset: find_asset(&contract.settlement_asset)?,
}),
InstrumentKind::Future(contract) => InstrumentKind::Future(FutureContract {
contract_size: contract.contract_size,
settlement_asset: find_asset(&contract.settlement_asset)?,
expiry: contract.expiry,
}),
InstrumentKind::Option(contract) => InstrumentKind::Option(OptionContract {
contract_size: contract.contract_size,
settlement_asset: find_asset(&contract.settlement_asset)?,
kind: contract.kind,
exercise: contract.exercise,
expiry: contract.expiry,
strike: contract.strike,
}),
InstrumentKind::Cfd(contract) => InstrumentKind::Cfd(CfdContract {
contract_size: contract.contract_size,
settlement_asset: find_asset(&contract.settlement_asset)?,
}),
};
let spec = match spec {
Some(spec) => {
let InstrumentSpec {
price,
quantity:
InstrumentSpecQuantity {
unit,
min,
increment,
},
notional,
} = spec;
let unit = match unit {
OrderQuantityUnits::Asset(asset) => {
OrderQuantityUnits::Asset(find_asset(&asset)?)
}
OrderQuantityUnits::Contract => OrderQuantityUnits::Contract,
OrderQuantityUnits::Quote => OrderQuantityUnits::Quote,
};
Some(InstrumentSpec {
price,
quantity: InstrumentSpecQuantity {
unit,
min,
increment,
},
notional,
})
}
None => None,
};
Ok(Instrument {
exchange,
name_internal,
name_exchange,
underlying: Underlying::new(base_new_key, quote_new_key),
quote,
kind,
spec,
data_venue,
})
}
}
impl<ExchangeKey> From<&Instrument<ExchangeKey, Asset>> for MarketDataInstrument {
fn from(value: &Instrument<ExchangeKey, Asset>) -> Self {
Self {
base: value.underlying.base.name_internal.clone(),
quote: value.underlying.quote.name_internal.clone(),
kind: MarketDataInstrumentKind::from(&value.kind),
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)] mod tests {
use super::*;
use crate::{
exchange::ExchangeId,
instrument::spec::{InstrumentSpecNotional, InstrumentSpecPrice},
};
use rust_decimal::Decimal;
fn instrument(data_venue: Option<DataVenue<ExchangeId>>) -> Instrument<ExchangeId, Asset> {
let instrument = Instrument::spot(
ExchangeId::AlpacaBroker,
"alpaca_broker-aapl",
"AAPL",
Underlying::new(
Asset::new_from_exchange("aapl"),
Asset::new_from_exchange("usd"),
),
None,
);
match data_venue {
Some(data_venue) => instrument.with_data_venue(data_venue),
None => instrument,
}
}
#[test]
fn data_venue_sorts_last_so_it_renumbers_no_existing_collection() {
let no_spec_with_data_venue =
instrument(Some(DataVenue::new_same_name(ExchangeId::LseEquities)));
let mut spec_without_data_venue = instrument(None);
spec_without_data_venue.spec = Some(InstrumentSpec::new(
InstrumentSpecPrice::new(Decimal::ONE, Decimal::ONE),
InstrumentSpecQuantity::new(OrderQuantityUnits::Quote, Decimal::ONE, Decimal::ONE),
InstrumentSpecNotional::new(Decimal::ONE),
));
assert!(
no_spec_with_data_venue < spec_without_data_venue,
"an earlier field must decide the order, whatever `data_venue` says"
);
assert!(instrument(None) < no_spec_with_data_venue);
}
#[test]
fn a_single_venue_instrument_reports_its_execution_venue_as_its_data_venue() {
let instrument = instrument(None);
assert_eq!(instrument.data_exchange(), &ExchangeId::AlpacaBroker);
assert_eq!(
instrument.data_name_exchange(),
&InstrumentNameExchange::new("AAPL")
);
}
#[test]
fn a_data_venue_without_a_symbol_overrides_the_venue_but_keeps_the_execution_symbol() {
let instrument = instrument(Some(DataVenue::new_same_name(ExchangeId::LseEquities)));
assert_eq!(instrument.data_exchange(), &ExchangeId::LseEquities);
assert_eq!(
instrument.data_name_exchange(),
&InstrumentNameExchange::new("AAPL")
);
assert_eq!(instrument.exchange, ExchangeId::AlpacaBroker);
assert_eq!(
instrument.name_exchange,
InstrumentNameExchange::new("AAPL")
);
}
#[test]
fn a_data_venue_symbol_overrides_the_execution_symbol() {
let instrument = instrument(Some(DataVenue::new(
ExchangeId::LseEquities,
Some(InstrumentNameExchange::new("BP.L")),
)));
assert_eq!(instrument.data_exchange(), &ExchangeId::LseEquities);
assert_eq!(
instrument.data_name_exchange(),
&InstrumentNameExchange::new("BP.L")
);
assert_eq!(
instrument.name_exchange,
InstrumentNameExchange::new("AAPL")
);
}
#[test]
fn map_exchange_key_maps_the_data_venue_too() {
let instrument = instrument(Some(DataVenue::new(
ExchangeId::LseEquities,
Some(InstrumentNameExchange::new("BP.L")),
)));
let mapped = instrument.map_exchange_key(|exchange| exchange.as_str().to_owned());
assert_eq!(mapped.exchange, "alpaca_broker");
assert_eq!(
mapped
.data_venue
.as_ref()
.map(|venue| venue.exchange.as_str()),
Some("lse_equities")
);
assert_eq!(
mapped.data_name_exchange(),
&InstrumentNameExchange::new("BP.L")
);
}
#[test]
fn an_instrument_without_a_data_venue_deserialises_from_a_config_that_predates_the_field() {
let json = r#"{
"exchange": "binance_spot",
"name_internal": "binance_spot-btc_usdt",
"name_exchange": "BTCUSDT",
"underlying": {
"base": { "name_internal": "btc", "name_exchange": "BTC" },
"quote": { "name_internal": "usdt", "name_exchange": "USDT" }
},
"quote": "underlying_quote",
"kind": "spot",
"spec": null
}"#;
let instrument =
serde_json::from_str::<Instrument<ExchangeId, Asset>>(json).expect("must deserialise");
assert_eq!(instrument.data_venue, None);
assert_eq!(instrument.data_exchange(), &ExchangeId::BinanceSpot);
}
#[test]
fn a_data_venue_round_trips_through_serde() {
let instrument = instrument(Some(DataVenue::new(
ExchangeId::LseEquities,
Some(InstrumentNameExchange::new("BP.L")),
)));
let json = serde_json::to_string(&instrument).expect("must serialise");
let round_tripped =
serde_json::from_str::<Instrument<ExchangeId, Asset>>(&json).expect("must deserialise");
assert_eq!(round_tripped, instrument);
}
}