use std::fmt::Write as _;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use sha1::Digest as _;
use super::types::response::{OrderBookSummaryResponse, OrderSummary};
use super::types::{Amount, AmountInner, OrderType, Side, TickSize};
use crate::Result;
use crate::error::Error;
pub const USDC_DECIMALS: u32 = 6;
pub(crate) fn walk_levels<F: Fn(&OrderSummary) -> Decimal>(
levels: &[OrderSummary],
target: Decimal,
accumulate: F,
order_type: &OrderType,
) -> Option<Decimal> {
if levels.is_empty() {
return None;
}
let mut total = Decimal::ZERO;
for level in levels.iter().rev() {
total += accumulate(level);
if total >= target {
return Some(level.price);
}
}
if *order_type == OrderType::FOK {
return None;
}
Some(levels[0].price)
}
pub fn calculate_market_price(
orderbook: &OrderBookSummaryResponse,
side: Side,
amount: Amount,
order_type: &OrderType,
) -> Result<Decimal> {
let (levels, acc): (&[OrderSummary], fn(&OrderSummary) -> Decimal) = match (side, amount.0) {
(Side::Buy, AmountInner::Usdc(_)) => (&orderbook.asks, |l| l.size * l.price),
(Side::Buy, AmountInner::Shares(_)) => (&orderbook.asks, |l| l.size),
(Side::Sell, AmountInner::Shares(_)) => (&orderbook.bids, |l| l.size),
(Side::Sell, AmountInner::Usdc(_)) => {
return Err(Error::validation(
"SELL orders must specify their amount in shares, not USDC",
));
}
(Side::Unknown, _) => return Err(Error::validation(format!("Invalid side: {side}"))),
};
walk_levels(levels, amount.as_inner(), acc, order_type).ok_or_else(|| {
Error::validation(format!(
"Insufficient liquidity to fill {} on {side:?}",
amount.as_inner()
))
})
}
#[must_use]
pub fn orderbook_summary_hash(orderbook: &OrderBookSummaryResponse) -> String {
let mut json = String::with_capacity(512);
json.push('{');
let _ = write!(json, "\"market\":\"{}\"", orderbook.market);
let asset_id_json = serde_json::to_string(&orderbook.asset_id).unwrap_or_default();
let _ = write!(json, ",\"asset_id\":{asset_id_json}");
let _ = write!(
json,
",\"timestamp\":\"{}\"",
orderbook.timestamp.timestamp_millis()
);
json.push_str(",\"hash\":\"\"");
json.push_str(",\"bids\":[");
for (i, o) in orderbook.bids.iter().enumerate() {
if i > 0 {
json.push(',');
}
let _ = write!(
json,
"{{\"price\":\"{}\",\"size\":\"{}\"}}",
o.price, o.size
);
}
json.push(']');
json.push_str(",\"asks\":[");
for (i, o) in orderbook.asks.iter().enumerate() {
if i > 0 {
json.push(',');
}
let _ = write!(
json,
"{{\"price\":\"{}\",\"size\":\"{}\"}}",
o.price, o.size
);
}
json.push(']');
let _ = write!(json, ",\"min_order_size\":\"{}\"", orderbook.min_order_size);
let _ = write!(
json,
",\"tick_size\":\"{}\"",
Decimal::from(orderbook.tick_size)
);
let _ = write!(json, ",\"neg_risk\":{}", orderbook.neg_risk);
let last = orderbook.last_trade_price.unwrap_or(Decimal::ZERO);
let _ = write!(json, ",\"last_trade_price\":\"{last}\"");
json.push('}');
let mut hasher = sha1::Sha1::new();
hasher.update(json.as_bytes());
let result = hasher.finalize();
format!("{result:x}")
}
pub fn adjust_market_buy_amount(
amount: Decimal,
user_usdc_balance: Decimal,
price: Decimal,
fee_rate: Decimal,
fee_exponent: Decimal,
builder_taker_fee_rate: Decimal,
) -> Result<Decimal> {
let base = price * (Decimal::ONE - price);
let base_f64: f64 = base.try_into().unwrap_or(0.0);
let exp_f64: f64 = fee_exponent.try_into().unwrap_or(0.0);
let platform_fee_rate =
fee_rate * Decimal::try_from(base_f64.powf(exp_f64)).unwrap_or(Decimal::ZERO);
let platform_fee = amount / price * platform_fee_rate;
let total_cost = amount + platform_fee + amount * builder_taker_fee_rate;
let raw = if user_usdc_balance <= total_cost {
let divisor = Decimal::ONE + platform_fee_rate / price + builder_taker_fee_rate;
user_usdc_balance / divisor
} else {
amount
};
let adjusted = raw.trunc_with_scale(USDC_DECIMALS);
if adjusted.is_zero() {
return Err(Error::validation(format!(
"user_usdc_balance {user_usdc_balance} too small to cover fees at price {price}; \
fee-adjusted amount truncated to zero"
)));
}
Ok(adjusted)
}
#[must_use]
pub fn price_valid(price: Decimal, tick_size: TickSize) -> bool {
let ts = Decimal::from(tick_size);
price >= ts && price <= dec!(1) - ts
}
#[cfg(test)]
mod tests {
use chrono::{DateTime, Utc};
use rust_decimal_macros::dec;
use super::*;
use crate::types::{B256, U256};
fn make_orderbook(
bids: Vec<OrderSummary>,
asks: Vec<OrderSummary>,
) -> OrderBookSummaryResponse {
OrderBookSummaryResponse::builder()
.market(B256::ZERO)
.asset_id(U256::ZERO)
.timestamp(Utc::now())
.bids(bids)
.asks(asks)
.min_order_size(dec!(0.01))
.neg_risk(false)
.tick_size(TickSize::Hundredth)
.build()
}
fn order(price: Decimal, size: Decimal) -> OrderSummary {
OrderSummary::builder().price(price).size(size).build()
}
#[test]
fn calculate_market_price_buy_usdc_sufficient_liquidity() {
let ob = make_orderbook(
vec![],
vec![
order(dec!(0.52), dec!(100)),
order(dec!(0.51), dec!(100)),
order(dec!(0.50), dec!(100)),
],
);
let amt = Amount::usdc(dec!(80)).unwrap();
assert_eq!(
calculate_market_price(&ob, Side::Buy, amt, &OrderType::FOK).unwrap(),
dec!(0.51),
);
}
#[test]
fn calculate_market_price_buy_shares_sufficient_liquidity() {
let ob = make_orderbook(
vec![],
vec![
order(dec!(0.52), dec!(100)),
order(dec!(0.51), dec!(100)),
order(dec!(0.50), dec!(100)),
],
);
let amt = Amount::shares(dec!(150)).unwrap();
assert_eq!(
calculate_market_price(&ob, Side::Buy, amt, &OrderType::FOK).unwrap(),
dec!(0.51),
);
}
#[test]
fn calculate_market_price_buy_insufficient_fok() {
let ob = make_orderbook(vec![], vec![order(dec!(0.50), dec!(10))]);
let amt = Amount::usdc(dec!(100)).unwrap();
calculate_market_price(&ob, Side::Buy, amt, &OrderType::FOK).unwrap_err();
}
#[test]
fn calculate_market_price_buy_insufficient_fak() {
let ob = make_orderbook(
vec![],
vec![order(dec!(0.60), dec!(5)), order(dec!(0.50), dec!(10))],
);
let amt = Amount::usdc(dec!(1000)).unwrap();
assert_eq!(
calculate_market_price(&ob, Side::Buy, amt, &OrderType::FAK).unwrap(),
dec!(0.60),
);
}
#[test]
fn calculate_market_price_sell_shares() {
let ob = make_orderbook(
vec![
order(dec!(0.48), dec!(100)),
order(dec!(0.49), dec!(100)),
order(dec!(0.50), dec!(100)),
],
vec![],
);
let amt = Amount::shares(dec!(150)).unwrap();
assert_eq!(
calculate_market_price(&ob, Side::Sell, amt, &OrderType::FOK).unwrap(),
dec!(0.49),
);
}
#[test]
fn calculate_market_price_sell_usdc_is_rejected() {
let ob = make_orderbook(
vec![order(dec!(0.49), dec!(100))],
vec![order(dec!(0.51), dec!(100))],
);
let amt = Amount::usdc(dec!(10)).unwrap();
calculate_market_price(&ob, Side::Sell, amt, &OrderType::FOK).unwrap_err();
}
#[test]
fn calculate_market_price_empty_orderbook() {
let ob = make_orderbook(vec![], vec![]);
let amt = Amount::usdc(dec!(100)).unwrap();
calculate_market_price(&ob, Side::Buy, amt, &OrderType::FOK).unwrap_err();
}
#[test]
fn calculate_market_price_unknown_side_errors() {
let ob = make_orderbook(
vec![order(dec!(0.49), dec!(100))],
vec![order(dec!(0.51), dec!(100))],
);
let amt = Amount::usdc(dec!(10)).unwrap();
calculate_market_price(&ob, Side::Unknown, amt, &OrderType::FOK).unwrap_err();
}
#[test]
fn price_valid_within_bounds() {
assert!(price_valid(dec!(0.5), TickSize::Hundredth));
assert!(price_valid(dec!(0.01), TickSize::Hundredth));
assert!(price_valid(dec!(0.99), TickSize::Hundredth));
}
#[test]
fn price_valid_at_boundaries() {
assert!(price_valid(dec!(0.1), TickSize::Tenth));
assert!(price_valid(dec!(0.9), TickSize::Tenth));
}
#[test]
fn price_valid_out_of_bounds() {
assert!(!price_valid(dec!(0.0), TickSize::Hundredth));
assert!(!price_valid(dec!(1.0), TickSize::Hundredth));
assert!(!price_valid(dec!(0.005), TickSize::Hundredth));
assert!(!price_valid(dec!(0.995), TickSize::Hundredth));
}
#[test]
fn price_valid_all_tick_sizes() {
assert!(price_valid(dec!(0.5), TickSize::Tenth));
assert!(price_valid(dec!(0.5), TickSize::Hundredth));
assert!(price_valid(dec!(0.5), TickSize::Thousandth));
assert!(price_valid(dec!(0.5), TickSize::TenThousandth));
}
#[test]
fn orderbook_hash_deterministic() {
let ts = DateTime::from_timestamp_millis(1_700_000_000_000).expect("valid ts");
let ob = OrderBookSummaryResponse::builder()
.market(B256::ZERO)
.asset_id(U256::ZERO)
.timestamp(ts)
.bids(vec![order(dec!(0.49), dec!(50))])
.asks(vec![order(dec!(0.51), dec!(25))])
.min_order_size(dec!(0.01))
.neg_risk(false)
.tick_size(TickSize::Hundredth)
.build();
let hash = orderbook_summary_hash(&ob);
assert_eq!(hash.len(), 40);
assert!(hash.chars().all(|c| c.is_ascii_hexdigit()));
assert_eq!(hash, orderbook_summary_hash(&ob));
}
#[test]
fn orderbook_hash_differs_for_different_inputs() {
let ts = DateTime::from_timestamp_millis(1_700_000_000_000).expect("valid ts");
let ob1 = OrderBookSummaryResponse::builder()
.market(B256::ZERO)
.asset_id(U256::from(1_u64))
.timestamp(ts)
.min_order_size(dec!(0.01))
.neg_risk(false)
.tick_size(TickSize::Hundredth)
.build();
let ob2 = OrderBookSummaryResponse::builder()
.market(B256::ZERO)
.asset_id(U256::from(2_u64))
.timestamp(ts)
.min_order_size(dec!(0.01))
.neg_risk(false)
.tick_size(TickSize::Hundredth)
.build();
assert_ne!(orderbook_summary_hash(&ob1), orderbook_summary_hash(&ob2));
}
#[test]
fn adjust_market_buy_no_adjustment_when_balance_sufficient() {
let result = adjust_market_buy_amount(
dec!(100),
dec!(1000),
dec!(0.5),
dec!(0.02),
dec!(1),
dec!(0),
)
.unwrap();
assert_eq!(result, dec!(100));
}
#[test]
fn adjust_market_buy_adjusts_when_balance_insufficient() {
let result = adjust_market_buy_amount(
dec!(100),
dec!(100),
dec!(0.5),
dec!(0.02),
dec!(1),
dec!(0),
)
.unwrap();
assert!(result < dec!(100));
assert!(result > dec!(0));
}
#[test]
fn adjust_market_buy_with_builder_fee() {
let result = adjust_market_buy_amount(
dec!(100),
dec!(100),
dec!(0.5),
dec!(0),
dec!(1),
dec!(0.005),
)
.unwrap();
let expected = (dec!(100) / dec!(1.005)).trunc_with_scale(USDC_DECIMALS);
assert_eq!(result, expected);
}
#[test]
fn adjust_market_buy_errors_when_balance_truncates_to_zero() {
let err = adjust_market_buy_amount(
dec!(100), dec!(0.0000001), dec!(0.5),
dec!(0.02),
dec!(1),
dec!(0.005),
)
.unwrap_err();
assert!(err.to_string().contains("truncated to zero"));
}
fn calc_platform_fee(amount: Decimal, price: Decimal, rate: Decimal, exponent: u32) -> Decimal {
let base = price * (Decimal::ONE - price);
let base_f64 = f64::try_from(base).unwrap_or(0.0);
let rate_factor = rate
* Decimal::try_from(base_f64.powi(i32::try_from(exponent).unwrap_or(0)))
.unwrap_or(Decimal::ZERO);
(amount / price) * rate_factor
}
fn calc_builder_fee(amount: Decimal, rate: Decimal) -> Decimal {
amount * rate
}
fn close_to(actual: Decimal, expected: Decimal, tol: Decimal) {
let diff = (actual - expected).abs();
assert!(
diff <= tol,
"|{actual} − {expected}| = {diff} exceeds tolerance {tol}"
);
}
#[test]
fn platform_fee_0_25_exp_2_at_midprice() {
close_to(
calc_platform_fee(dec!(100) * dec!(0.5), dec!(0.5), dec!(0.25), 2),
dec!(1.5625),
dec!(0.000001),
);
}
#[test]
fn platform_fee_0_25_exp_2_symmetric_prices() {
let cases = [
(dec!(0.3), dec!(0.7), dec!(1.1025)),
(dec!(0.1), dec!(0.9), dec!(0.2025)),
(dec!(0.05), dec!(0.95), dec!(0.05640625)),
(dec!(0.01), dec!(0.99), dec!(0.00245025)),
];
for (p_low, p_high, expected) in cases {
close_to(
calc_platform_fee(dec!(100) * p_low, p_low, dec!(0.25), 2),
expected,
dec!(0.000001),
);
close_to(
calc_platform_fee(dec!(100) * p_high, p_high, dec!(0.25), 2),
expected,
dec!(0.000001),
);
}
}
#[test]
fn platform_fee_0_25_exp_2_fractional_contracts() {
close_to(
calc_platform_fee(dec!(125.5) * dec!(0.5), dec!(0.5), dec!(0.25), 2),
dec!(1.9609375),
dec!(0.000001),
);
}
#[test]
fn builder_fee_1_pct() {
close_to(
calc_builder_fee(dec!(100) * dec!(0.5), dec!(0.01)),
dec!(0.5),
dec!(0.000001),
);
}
#[test]
fn builder_fee_5_pct() {
close_to(
calc_builder_fee(dec!(200) * dec!(0.75), dec!(0.05)),
dec!(7.5),
dec!(0.000001),
);
}
#[test]
fn combined_platform_and_builder_fee() {
let amount_usd = dec!(100) * dec!(0.5);
let platform = calc_platform_fee(amount_usd, dec!(0.5), dec!(0.25), 2);
let builder = calc_builder_fee(amount_usd, dec!(0.01));
close_to(platform, dec!(1.5625), dec!(0.000001));
close_to(builder, dec!(0.5), dec!(0.000001));
close_to(platform + builder, dec!(2.0625), dec!(0.000001));
}
#[test]
fn adjust_buy_balance_strictly_greater_returns_amount_unchanged() {
let amount = dec!(50);
let price = dec!(0.5);
let fee = calc_platform_fee(amount, price, dec!(0.25), 2);
let balance = amount + fee + dec!(1); let result =
adjust_market_buy_amount(amount, balance, price, dec!(0.25), dec!(2), dec!(0)).unwrap();
assert_eq!(result, amount);
}
#[test]
fn adjust_buy_balance_equal_to_total_cost_matches_divide_path() {
let amount = dec!(50);
let price = dec!(0.5);
let fee = calc_platform_fee(amount, price, dec!(0.25), 2);
let total_cost = amount + fee;
let result =
adjust_market_buy_amount(amount, total_cost, price, dec!(0.25), dec!(2), dec!(0))
.unwrap();
close_to(result, amount, dec!(0.000001));
}
#[test]
fn adjust_buy_conserves_notional_platform_only() {
let amount = dec!(50);
let price = dec!(0.5);
let adjusted =
adjust_market_buy_amount(amount, amount, price, dec!(0.25), dec!(2), dec!(0)).unwrap();
let fee = calc_platform_fee(adjusted, price, dec!(0.25), 2);
close_to(adjusted + fee, amount, dec!(0.000001));
assert!(adjusted < amount);
}
#[test]
fn adjust_buy_conserves_notional_builder_only() {
let amount = dec!(50);
let price = dec!(0.5);
let builder_rate = dec!(0.01);
let adjusted =
adjust_market_buy_amount(amount, amount, price, dec!(0), dec!(0), builder_rate)
.unwrap();
let fee = calc_builder_fee(adjusted, builder_rate);
close_to(adjusted + fee, amount, dec!(0.000001));
}
#[test]
fn adjust_buy_conserves_notional_platform_and_builder() {
let amount = dec!(50);
let price = dec!(0.5);
let builder_rate = dec!(0.01);
let adjusted =
adjust_market_buy_amount(amount, amount, price, dec!(0.25), dec!(2), builder_rate)
.unwrap();
let platform = calc_platform_fee(adjusted, price, dec!(0.25), 2);
let builder = calc_builder_fee(adjusted, builder_rate);
close_to(adjusted + platform + builder, amount, dec!(0.000001));
}
#[test]
fn adjust_buy_conserves_notional_at_price_0_3() {
let amount = dec!(30);
let price = dec!(0.3);
let builder_rate = dec!(0.02);
let adjusted =
adjust_market_buy_amount(amount, amount, price, dec!(0.25), dec!(2), builder_rate)
.unwrap();
let platform = calc_platform_fee(adjusted, price, dec!(0.25), 2);
let builder = calc_builder_fee(adjusted, builder_rate);
close_to(adjusted + platform + builder, amount, dec!(0.000001));
}
#[test]
fn production_fee_sports_v2() {
close_to(
calc_platform_fee(dec!(100), dec!(0.5), dec!(0.03), 1),
dec!(1.5),
dec!(0.000001),
);
close_to(
calc_platform_fee(dec!(100), dec!(0.3), dec!(0.03), 1),
dec!(2.1),
dec!(0.000001),
);
close_to(
calc_platform_fee(dec!(100), dec!(0.7), dec!(0.03), 1),
dec!(0.9),
dec!(0.000001),
);
}
#[test]
fn production_fee_politics_family() {
close_to(
calc_platform_fee(dec!(100), dec!(0.5), dec!(0.04), 1),
dec!(2.0),
dec!(0.000001),
);
close_to(
calc_platform_fee(dec!(100), dec!(0.3), dec!(0.04), 1),
dec!(2.8),
dec!(0.000001),
);
close_to(
calc_platform_fee(dec!(100), dec!(0.7), dec!(0.04), 1),
dec!(1.2),
dec!(0.000001),
);
}
#[test]
fn production_fee_culture_family() {
close_to(
calc_platform_fee(dec!(100), dec!(0.5), dec!(0.05), 1),
dec!(2.5),
dec!(0.000001),
);
close_to(
calc_platform_fee(dec!(100), dec!(0.3), dec!(0.05), 1),
dec!(3.5),
dec!(0.000001),
);
close_to(
calc_platform_fee(dec!(100), dec!(0.7), dec!(0.05), 1),
dec!(1.5),
dec!(0.000001),
);
}
#[test]
fn production_fee_crypto_v2() {
close_to(
calc_platform_fee(dec!(100), dec!(0.5), dec!(0.072), 1),
dec!(3.6),
dec!(0.000001),
);
close_to(
calc_platform_fee(dec!(100), dec!(0.3), dec!(0.072), 1),
dec!(5.04),
dec!(0.000001),
);
close_to(
calc_platform_fee(dec!(100), dec!(0.7), dec!(0.072), 1),
dec!(2.16),
dec!(0.000001),
);
}
#[test]
fn production_adjust_buy_conserves_notional_across_all_tiers() {
let amount = dec!(100);
let tiers: [(&str, Decimal, u32); 4] = [
("sports_v2", dec!(0.03), 1),
("politics_family", dec!(0.04), 1),
("culture_family", dec!(0.05), 1),
("crypto_v2", dec!(0.072), 1),
];
let prices = [dec!(0.3), dec!(0.5), dec!(0.7)];
for (name, rate, exponent) in tiers {
for price in prices {
let adjusted = adjust_market_buy_amount(
amount,
amount,
price,
rate,
Decimal::from(exponent),
dec!(0),
)
.unwrap_or_else(|e| {
panic!("adjust failed for {name} @ price={price}: {e}");
});
let fee = calc_platform_fee(adjusted, price, rate, exponent);
let diff = (adjusted + fee - amount).abs();
assert!(
diff <= dec!(0.0001),
"tier={name} price={price}: adjusted ({adjusted}) + fee ({fee}) = {} vs \
amount {amount}, diff {diff}",
adjusted + fee,
);
}
}
}
}