use std::collections::HashMap;
use std::fmt::{Display, Formatter};
use crate::core::instrument::Instrument;
use crate::marketdata::{
AccountInfo, InstrumentId, MarketDataService, MarketDataSync, Quote, QuoteResolution,
};
use crate::param::{AccountGroupId, AccountId, Price};
use super::market_order_pricer::WithSlippage;
const MAX_SLIPPAGE_BPS: u16 = 10_000;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SpotFundsConfigError {
SlippageOutOfRange {
bps: u16,
},
}
impl Display for SpotFundsConfigError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::SlippageOutOfRange { bps } => {
write!(
f,
"slippage {bps} bps is out of range (must be <= 10 000 bps)"
)
}
}
}
}
impl std::error::Error for SpotFundsConfigError {}
fn check_slippage_bps(bps: u16) -> Result<(), SpotFundsConfigError> {
if bps > MAX_SLIPPAGE_BPS {
return Err(SpotFundsConfigError::SlippageOutOfRange { bps });
}
Ok(())
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum SpotFundsPriceError {
QuoteUnavailable,
CalculationFailed,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum SpotFundsPricingSource {
#[default]
Mark,
BookTop,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct SpotFundsOverride {
pub slippage_bps: Option<u16>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SpotFundsOverrideTarget {
Instrument(InstrumentId),
InstrumentAccount(InstrumentId, AccountId),
InstrumentAccountGroup(InstrumentId, AccountGroupId),
}
#[derive(Clone, Debug)]
pub struct SpotFundsSettings {
account_overrides: HashMap<(InstrumentId, AccountId), WithSlippage>,
account_group_overrides: HashMap<(InstrumentId, AccountGroupId), WithSlippage>,
instrument_overrides: HashMap<InstrumentId, WithSlippage>,
global_pricer: WithSlippage,
pricing_source: SpotFundsPricingSource,
}
impl SpotFundsSettings {
pub fn new<Overrides>(
slippage_bps: u16,
pricing_source: SpotFundsPricingSource,
overrides: Overrides,
) -> Result<Self, SpotFundsConfigError>
where
Overrides: IntoIterator<Item = (SpotFundsOverrideTarget, SpotFundsOverride)>,
{
check_slippage_bps(slippage_bps)?;
let global_pricer = WithSlippage::new(slippage_bps);
let mut account_overrides = HashMap::new();
let mut account_group_overrides = HashMap::new();
let mut instrument_overrides = HashMap::new();
for (target, ovr) in overrides {
let Some(bps) = ovr.slippage_bps else {
continue;
};
check_slippage_bps(bps)?;
let pricer = WithSlippage::new(bps);
match target {
SpotFundsOverrideTarget::Instrument(instrument_id) => {
instrument_overrides.insert(instrument_id, pricer);
}
SpotFundsOverrideTarget::InstrumentAccount(instrument_id, account_id) => {
account_overrides.insert((instrument_id, account_id), pricer);
}
SpotFundsOverrideTarget::InstrumentAccountGroup(
instrument_id,
account_group_id,
) => {
account_group_overrides.insert((instrument_id, account_group_id), pricer);
}
}
}
Ok(Self {
account_overrides,
account_group_overrides,
instrument_overrides,
global_pricer,
pricing_source,
})
}
pub fn set_global_slippage_bps(
&mut self,
slippage_bps: u16,
) -> Result<(), SpotFundsConfigError> {
check_slippage_bps(slippage_bps)?;
self.global_pricer = WithSlippage::new(slippage_bps);
Ok(())
}
pub fn set_pricing_source(&mut self, pricing_source: SpotFundsPricingSource) {
self.pricing_source = pricing_source;
}
pub fn set_override(
&mut self,
target: SpotFundsOverrideTarget,
ovr: SpotFundsOverride,
) -> Result<(), SpotFundsConfigError> {
if let Some(bps) = ovr.slippage_bps {
check_slippage_bps(bps)?;
}
let pricer = ovr.slippage_bps.map(WithSlippage::new);
match target {
SpotFundsOverrideTarget::Instrument(instrument_id) => {
set_or_clear(&mut self.instrument_overrides, instrument_id, pricer);
}
SpotFundsOverrideTarget::InstrumentAccount(instrument_id, account_id) => {
set_or_clear(
&mut self.account_overrides,
(instrument_id, account_id),
pricer,
);
}
SpotFundsOverrideTarget::InstrumentAccountGroup(instrument_id, account_group_id) => {
set_or_clear(
&mut self.account_group_overrides,
(instrument_id, account_group_id),
pricer,
);
}
}
Ok(())
}
fn pricer_for(
&self,
instrument_id: InstrumentId,
account_id: AccountId,
account_info: &impl AccountInfo,
) -> &WithSlippage {
if let Some(p) = self.account_overrides.get(&(instrument_id, account_id)) {
return p;
}
if let Some(account_group_id) = account_info.group() {
if let Some(p) = self
.account_group_overrides
.get(&(instrument_id, account_group_id))
{
return p;
}
}
if let Some(p) = self.instrument_overrides.get(&instrument_id) {
return p;
}
&self.global_pricer
}
fn pricing_base_for_buy(&self, quote: &Quote) -> Option<Price> {
match self.pricing_source {
SpotFundsPricingSource::Mark => quote.mark,
SpotFundsPricingSource::BookTop => quote.ask,
}
}
fn pricing_base_for_sell(&self, quote: &Quote) -> Option<Price> {
match self.pricing_source {
SpotFundsPricingSource::Mark => quote.mark,
SpotFundsPricingSource::BookTop => quote.bid,
}
}
pub(super) fn effective_buy_price(
&self,
quote: &Quote,
instrument_id: InstrumentId,
account_id: AccountId,
account_info: &impl AccountInfo,
) -> Result<Price, SpotFundsPriceError> {
let base = self
.pricing_base_for_buy(quote)
.ok_or(SpotFundsPriceError::QuoteUnavailable)?;
self.pricer_for(instrument_id, account_id, account_info)
.effective_buy_price(base)
.map_err(|_| SpotFundsPriceError::CalculationFailed)
}
pub(super) fn effective_sell_price(
&self,
quote: &Quote,
instrument_id: InstrumentId,
account_id: AccountId,
account_info: &impl AccountInfo,
) -> Result<Price, SpotFundsPriceError> {
let base = self
.pricing_base_for_sell(quote)
.ok_or(SpotFundsPriceError::QuoteUnavailable)?;
self.pricer_for(instrument_id, account_id, account_info)
.effective_sell_price(base)
.map_err(|_| SpotFundsPriceError::CalculationFailed)
}
}
fn set_or_clear<K, V>(map: &mut HashMap<K, V>, key: K, value: Option<V>)
where
K: std::hash::Hash + Eq,
{
match value {
Some(v) => {
map.insert(key, v);
}
None => {
map.remove(&key);
}
}
}
pub struct SpotFundsMarketData<Sync: MarketDataSync> {
pub(super) market_data: Sync::Shared<MarketDataService<Sync>>,
}
impl<Sync: MarketDataSync> SpotFundsMarketData<Sync> {
pub fn new(market_data: Sync::Shared<MarketDataService<Sync>>) -> Self {
Self { market_data }
}
pub(super) fn quote(
&self,
instrument_id: InstrumentId,
account_id: AccountId,
account_info: &impl AccountInfo,
) -> Option<Quote> {
self.market_data
.get(
instrument_id,
account_id,
account_info,
QuoteResolution::AccountThenGroupThenDefault,
)
.ok()
}
pub(super) fn resolve(&self, instrument: &Instrument) -> Option<InstrumentId> {
self.market_data.resolve(instrument)
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::*;
use crate::marketdata::{MarketDataBuilder, Quote, QuoteTtl};
use crate::param::{Asset, Price};
use crate::FullSync;
fn px(s: &str) -> Price {
Price::from_str(s).expect("valid price")
}
fn asset(s: &str) -> Asset {
Asset::new(s).expect("valid asset")
}
fn account(n: u64) -> AccountId {
AccountId::from_u64(n)
}
fn group(n: u32) -> AccountGroupId {
AccountGroupId::from_u32(n).expect("valid account group id")
}
fn service_with_mark_100() -> (Arc<MarketDataService<FullSync>>, InstrumentId) {
let svc = MarketDataBuilder::<FullSync>::new(QuoteTtl::Infinite).build();
let id = svc
.register(Instrument::new(asset("AAPL"), asset("USD")))
.expect("register must succeed");
svc.push(id, Quote::new().with_mark(px("100")))
.expect("push must succeed");
(svc, id)
}
fn buy_price_of(
svc: &Arc<MarketDataService<FullSync>>,
settings: &SpotFundsSettings,
id: InstrumentId,
account_id: AccountId,
account_info: &impl AccountInfo,
) -> Result<Price, SpotFundsPriceError> {
let md = SpotFundsMarketData::<FullSync>::new(Arc::clone(svc));
let quote = md
.quote(id, account_id, account_info)
.ok_or(SpotFundsPriceError::QuoteUnavailable)?;
settings.effective_buy_price("e, id, account_id, account_info)
}
fn sell_price_of(
svc: &Arc<MarketDataService<FullSync>>,
settings: &SpotFundsSettings,
id: InstrumentId,
account_id: AccountId,
account_info: &impl AccountInfo,
) -> Result<Price, SpotFundsPriceError> {
let md = SpotFundsMarketData::<FullSync>::new(Arc::clone(svc));
let quote = md
.quote(id, account_id, account_info)
.ok_or(SpotFundsPriceError::QuoteUnavailable)?;
settings.effective_sell_price("e, id, account_id, account_info)
}
fn buy_price<Overrides>(
slippage_bps: u16,
overrides: Overrides,
account_id: AccountId,
account_info: &impl AccountInfo,
) -> Result<Price, SpotFundsPriceError>
where
Overrides: IntoIterator<Item = (SpotFundsOverrideTarget, SpotFundsOverride)>,
{
let (svc, id) = service_with_mark_100();
let settings =
SpotFundsSettings::new(slippage_bps, SpotFundsPricingSource::Mark, overrides)
.expect("settings must build");
buy_price_of(&svc, &settings, id, account_id, account_info)
}
#[test]
fn account_override_wins_over_group_instrument_and_global() {
let (svc, id) = service_with_mark_100();
let acc = account(7);
let grp = group(3);
let overrides = [
(
SpotFundsOverrideTarget::Instrument(id),
SpotFundsOverride {
slippage_bps: Some(1000),
},
),
(
SpotFundsOverrideTarget::InstrumentAccountGroup(id, grp),
SpotFundsOverride {
slippage_bps: Some(2000),
},
),
(
SpotFundsOverrideTarget::InstrumentAccount(id, acc),
SpotFundsOverride {
slippage_bps: Some(3000),
},
),
];
let settings = SpotFundsSettings::new(0, SpotFundsPricingSource::Mark, overrides)
.expect("settings must build");
assert_eq!(
buy_price_of(&svc, &settings, id, acc, &Some(grp)),
Ok(px("130"))
);
}
#[test]
fn group_override_used_when_no_account_override_matches() {
let acc = account(7);
let grp = group(3);
let (svc, id) = service_with_mark_100();
let overrides = [
(
SpotFundsOverrideTarget::Instrument(id),
SpotFundsOverride {
slippage_bps: Some(1000),
},
),
(
SpotFundsOverrideTarget::InstrumentAccountGroup(id, grp),
SpotFundsOverride {
slippage_bps: Some(2000),
},
),
];
let settings = SpotFundsSettings::new(0, SpotFundsPricingSource::Mark, overrides)
.expect("settings must build");
assert_eq!(
buy_price_of(&svc, &settings, id, acc, &Some(grp)),
Ok(px("120"))
);
}
#[test]
fn instrument_default_used_when_neither_account_nor_group_matches() {
let acc = account(7);
let (svc, id) = service_with_mark_100();
let settings = SpotFundsSettings::new(
0,
SpotFundsPricingSource::Mark,
[(
SpotFundsOverrideTarget::Instrument(id),
SpotFundsOverride {
slippage_bps: Some(1000),
},
)],
)
.expect("settings must build");
assert_eq!(buy_price_of(&svc, &settings, id, acc, &None), Ok(px("110")));
assert_eq!(
buy_price_of(&svc, &settings, id, acc, &Some(group(9))),
Ok(px("110"))
);
}
#[test]
fn global_used_when_nothing_matches() {
let acc = account(7);
assert_eq!(
buy_price(1000, std::iter::empty(), acc, &None),
Ok(px("110"))
);
}
#[test]
fn none_slippage_override_entry_is_treated_as_absent() {
let acc = account(7);
let grp = group(3);
let (svc, id) = service_with_mark_100();
let overrides = [
(
SpotFundsOverrideTarget::InstrumentAccount(id, acc),
SpotFundsOverride { slippage_bps: None },
),
(
SpotFundsOverrideTarget::InstrumentAccountGroup(id, grp),
SpotFundsOverride { slippage_bps: None },
),
(
SpotFundsOverrideTarget::Instrument(id),
SpotFundsOverride {
slippage_bps: Some(1000),
},
),
];
let settings = SpotFundsSettings::new(0, SpotFundsPricingSource::Mark, overrides)
.expect("settings must build");
assert_eq!(
buy_price_of(&svc, &settings, id, acc, &Some(grp)),
Ok(px("110"))
);
}
#[test]
fn out_of_range_account_override_returns_slippage_out_of_range() {
let (_svc, id) = service_with_mark_100();
let result = SpotFundsSettings::new(
0,
SpotFundsPricingSource::Mark,
[(
SpotFundsOverrideTarget::InstrumentAccount(id, account(7)),
SpotFundsOverride {
slippage_bps: Some(10_001),
},
)],
);
assert_eq!(
result.err(),
Some(SpotFundsConfigError::SlippageOutOfRange { bps: 10_001 })
);
}
#[test]
fn out_of_range_group_override_returns_slippage_out_of_range() {
let (_svc, id) = service_with_mark_100();
let result = SpotFundsSettings::new(
0,
SpotFundsPricingSource::Mark,
[(
SpotFundsOverrideTarget::InstrumentAccountGroup(id, group(3)),
SpotFundsOverride {
slippage_bps: Some(10_001),
},
)],
);
assert_eq!(
result.err(),
Some(SpotFundsConfigError::SlippageOutOfRange { bps: 10_001 })
);
}
fn sell_price<Overrides>(
slippage_bps: u16,
overrides: Overrides,
account_id: AccountId,
account_info: &impl AccountInfo,
) -> Result<Price, SpotFundsPriceError>
where
Overrides: IntoIterator<Item = (SpotFundsOverrideTarget, SpotFundsOverride)>,
{
let (svc, id) = service_with_mark_100();
let settings =
SpotFundsSettings::new(slippage_bps, SpotFundsPricingSource::Mark, overrides)
.expect("settings must build");
sell_price_of(&svc, &settings, id, account_id, account_info)
}
#[test]
fn sell_account_override_wins_over_group_instrument_and_global() {
let (svc, id) = service_with_mark_100();
let acc = account(7);
let grp = group(3);
let overrides = [
(
SpotFundsOverrideTarget::Instrument(id),
SpotFundsOverride {
slippage_bps: Some(1000),
},
),
(
SpotFundsOverrideTarget::InstrumentAccountGroup(id, grp),
SpotFundsOverride {
slippage_bps: Some(2000),
},
),
(
SpotFundsOverrideTarget::InstrumentAccount(id, acc),
SpotFundsOverride {
slippage_bps: Some(3000),
},
),
];
let settings = SpotFundsSettings::new(0, SpotFundsPricingSource::Mark, overrides)
.expect("settings must build");
assert_eq!(
sell_price_of(&svc, &settings, id, acc, &Some(grp)),
Ok(px("70"))
);
}
#[test]
fn sell_group_override_used_when_no_account_override_matches() {
let acc = account(7);
let grp = group(3);
let (svc, id) = service_with_mark_100();
let overrides = [
(
SpotFundsOverrideTarget::Instrument(id),
SpotFundsOverride {
slippage_bps: Some(1000),
},
),
(
SpotFundsOverrideTarget::InstrumentAccountGroup(id, grp),
SpotFundsOverride {
slippage_bps: Some(2000),
},
),
];
let settings = SpotFundsSettings::new(0, SpotFundsPricingSource::Mark, overrides)
.expect("settings must build");
assert_eq!(
sell_price_of(&svc, &settings, id, acc, &Some(grp)),
Ok(px("80"))
);
}
#[test]
fn sell_instrument_default_used_when_neither_account_nor_group_matches() {
let acc = account(7);
let (svc, id) = service_with_mark_100();
let settings = SpotFundsSettings::new(
0,
SpotFundsPricingSource::Mark,
[(
SpotFundsOverrideTarget::Instrument(id),
SpotFundsOverride {
slippage_bps: Some(1000),
},
)],
)
.expect("settings must build");
assert_eq!(sell_price_of(&svc, &settings, id, acc, &None), Ok(px("90")));
assert_eq!(
sell_price_of(&svc, &settings, id, acc, &Some(group(9))),
Ok(px("90"))
);
}
#[test]
fn sell_global_used_when_nothing_matches() {
let acc = account(7);
assert_eq!(
sell_price(1000, std::iter::empty(), acc, &None),
Ok(px("90"))
);
}
}