use crate::{
indexer::{
ClientId, ClobPairId, Height, OrderExecution, OrderFlags, OrderType, PerpetualMarket,
Price, Quantity, Subaccount,
},
node::Address,
};
use anyhow::{anyhow as err, Error};
use bigdecimal::{num_traits::cast::ToPrimitive, BigDecimal, One};
use chrono::{DateTime, Utc};
use derive_more::From;
pub use dydx_proto::dydxprotocol::clob::{
order::{Side as OrderSide, TimeInForce as OrderTimeInForce},
OrderId,
};
use dydx_proto::dydxprotocol::{
clob::{
msg_cancel_order,
order::{self, ConditionType},
BuilderCodeParameters, Order, TwapParameters,
},
subaccounts::SubaccountId,
};
pub const SHORT_TERM_ORDER_MAXIMUM_LIFETIME: u32 = 20;
pub const DEFAULT_RUST_CLIENT_METADATA: u32 = 4;
#[derive(From, Clone, Debug)]
pub enum OrderGoodUntil {
Block(Height),
Time(DateTime<Utc>),
}
impl TryFrom<OrderGoodUntil> for order::GoodTilOneof {
type Error = Error;
fn try_from(until: OrderGoodUntil) -> Result<Self, Error> {
match until {
OrderGoodUntil::Block(height) => Ok(Self::GoodTilBlock(height.0)),
OrderGoodUntil::Time(time) => Ok(Self::GoodTilBlockTime(time.timestamp().try_into()?)),
}
}
}
impl TryFrom<OrderGoodUntil> for msg_cancel_order::GoodTilOneof {
type Error = Error;
fn try_from(until: OrderGoodUntil) -> Result<Self, Error> {
match until {
OrderGoodUntil::Block(height) => Ok(Self::GoodTilBlock(height.0)),
OrderGoodUntil::Time(time) => Ok(Self::GoodTilBlockTime(time.timestamp().try_into()?)),
}
}
}
#[derive(Clone, Debug)]
pub struct OrderMarketParams {
pub atomic_resolution: i32,
pub clob_pair_id: ClobPairId,
pub oracle_price: Option<Price>,
pub quantum_conversion_exponent: i32,
pub step_base_quantums: u64,
pub subticks_per_tick: u32,
}
impl OrderMarketParams {
pub fn quantize_price(&self, price: impl Into<Price>) -> BigDecimal {
const QUOTE_QUANTUMS_ATOMIC_RESOLUTION: i32 = -6;
let scale = -(self.atomic_resolution
- self.quantum_conversion_exponent
- QUOTE_QUANTUMS_ATOMIC_RESOLUTION);
let factor = BigDecimal::new(One::one(), scale.into());
let raw_subticks = price.into().0 * factor;
let subticks_per_tick = BigDecimal::from(self.subticks_per_tick);
let quantums = quantize(&raw_subticks, &subticks_per_tick);
quantums.max(subticks_per_tick)
}
pub fn quantize_quantity(&self, quantity: impl Into<Quantity>) -> BigDecimal {
let factor = BigDecimal::new(One::one(), self.atomic_resolution.into());
let raw_quantums = quantity.into().0 * factor;
let step_base_quantums = BigDecimal::from(self.step_base_quantums);
let quantums = quantize(&raw_quantums, &step_base_quantums);
quantums.max(step_base_quantums)
}
pub fn dequantize_subticks(&self, subticks: impl Into<BigDecimal>) -> BigDecimal {
const QUOTE_QUANTUMS_ATOMIC_RESOLUTION: i32 = -6;
let scale = -(self.atomic_resolution
- self.quantum_conversion_exponent
- QUOTE_QUANTUMS_ATOMIC_RESOLUTION);
let factor = BigDecimal::new(One::one(), scale.into());
subticks.into() / factor
}
pub fn dequantize_quantums(&self, quantums: impl Into<BigDecimal>) -> BigDecimal {
let factor = BigDecimal::new(One::one(), self.atomic_resolution.into());
quantums.into() / factor
}
pub fn clob_pair_id(&self) -> &ClobPairId {
&self.clob_pair_id
}
}
fn quantize(value: &BigDecimal, fraction: &BigDecimal) -> BigDecimal {
(value / fraction).round(0) * fraction
}
impl From<PerpetualMarket> for OrderMarketParams {
fn from(market: PerpetualMarket) -> Self {
Self {
atomic_resolution: market.atomic_resolution,
clob_pair_id: market.clob_pair_id,
oracle_price: market.oracle_price,
quantum_conversion_exponent: market.quantum_conversion_exponent,
step_base_quantums: market.step_base_quantums,
subticks_per_tick: market.subticks_per_tick,
}
}
}
#[derive(Clone, Debug)]
pub struct OrderBuilder {
market_params: OrderMarketParams,
subaccount_id: SubaccountId,
flags: OrderFlags,
side: Option<OrderSide>,
ty: Option<OrderType>,
size: Option<Quantity>,
price: Option<Price>,
time_in_force: Option<OrderTimeInForce>,
reduce_only: Option<bool>,
until: Option<OrderGoodUntil>,
post_only: Option<bool>,
execution: Option<OrderExecution>,
trigger_price: Option<Price>,
slippage: BigDecimal,
builder_code_parameters: Option<BuilderCodeParameters>,
twap_parameters: Option<TwapParameters>,
order_router_address: Address,
}
impl OrderBuilder {
pub fn new(market_for: impl Into<OrderMarketParams>, subaccount: Subaccount) -> Self {
Self {
market_params: market_for.into(),
subaccount_id: subaccount.into(),
flags: OrderFlags::ShortTerm,
side: Some(OrderSide::Buy),
ty: Some(OrderType::Market),
size: None,
price: None,
time_in_force: None,
reduce_only: None,
until: None,
post_only: None,
execution: None,
trigger_price: None,
slippage: BigDecimal::new(5.into(), 2),
builder_code_parameters: None,
twap_parameters: None,
order_router_address: Address::default(),
}
}
pub fn market(mut self, side: impl Into<OrderSide>, size: impl Into<Quantity>) -> Self {
self.ty = Some(OrderType::Market);
self.side = Some(side.into());
self.size = Some(size.into());
self
}
pub fn limit(
mut self,
side: impl Into<OrderSide>,
price: impl Into<Price>,
size: impl Into<Quantity>,
) -> Self {
self.ty = Some(OrderType::Limit);
self.price = Some(price.into());
self.side = Some(side.into());
self.size = Some(size.into());
self
}
pub fn stop_limit(
mut self,
side: impl Into<OrderSide>,
price: impl Into<Price>,
trigger_price: impl Into<Price>,
size: impl Into<Quantity>,
) -> Self {
self.ty = Some(OrderType::StopLimit);
self.price = Some(price.into());
self.trigger_price = Some(trigger_price.into());
self.side = Some(side.into());
self.size = Some(size.into());
self.conditional()
}
pub fn stop_market(
mut self,
side: impl Into<OrderSide>,
trigger_price: impl Into<Price>,
size: impl Into<Quantity>,
) -> Self {
self.ty = Some(OrderType::StopMarket);
self.trigger_price = Some(trigger_price.into());
self.side = Some(side.into());
self.size = Some(size.into());
self.conditional()
}
pub fn take_profit_limit(
mut self,
side: impl Into<OrderSide>,
price: impl Into<Price>,
trigger_price: impl Into<Price>,
size: impl Into<Quantity>,
) -> Self {
self.ty = Some(OrderType::TakeProfit);
self.price = Some(price.into());
self.trigger_price = Some(trigger_price.into());
self.side = Some(side.into());
self.size = Some(size.into());
self.conditional()
}
pub fn take_profit_market(
mut self,
side: impl Into<OrderSide>,
trigger_price: impl Into<Price>,
size: impl Into<Quantity>,
) -> Self {
self.ty = Some(OrderType::TakeProfitMarket);
self.trigger_price = Some(trigger_price.into());
self.side = Some(side.into());
self.size = Some(size.into());
self.conditional()
}
pub fn long_term(mut self) -> Self {
self.flags = OrderFlags::LongTerm;
self
}
pub fn short_term(mut self) -> Self {
self.flags = OrderFlags::ShortTerm;
self
}
pub fn conditional(mut self) -> Self {
self.flags = OrderFlags::Conditional;
self
}
pub fn price(mut self, price: impl Into<Price>) -> Self {
self.price = Some(price.into());
self
}
pub fn size(mut self, size: impl Into<Quantity>) -> Self {
self.size = Some(size.into());
self
}
pub fn time_in_force(mut self, tif: impl Into<OrderTimeInForce>) -> Self {
self.time_in_force = Some(tif.into());
self
}
pub fn reduce_only(mut self, reduce: impl Into<bool>) -> Self {
self.reduce_only = Some(reduce.into());
self
}
pub fn until(mut self, gtof: impl Into<OrderGoodUntil>) -> Self {
self.until = Some(gtof.into());
self
}
pub fn execution(mut self, execution: impl Into<OrderExecution>) -> Self {
self.execution = Some(execution.into());
self
}
pub fn allowed_slippage(mut self, slippage_percent: impl Into<BigDecimal>) -> Self {
self.slippage = slippage_percent.into() / 100.0;
self
}
pub fn builder_code_parameters(
mut self,
builder_code_parameters: impl Into<BuilderCodeParameters>,
) -> Self {
self.builder_code_parameters = Some(builder_code_parameters.into());
self
}
pub fn twap_parameters(mut self, twap_parameters: impl Into<TwapParameters>) -> Self {
self.twap_parameters = Some(twap_parameters.into());
self
}
pub fn order_router_address(mut self, order_router_address: impl Into<Address>) -> Self {
self.order_router_address = order_router_address.into();
self
}
pub fn update_market(&mut self, market_for: impl Into<OrderMarketParams>) {
self.market_params = market_for.into();
}
pub fn update_market_price(&mut self, price: impl Into<Price>) {
self.market_params.oracle_price = Some(price.into());
}
pub fn build(self, client_id: impl Into<ClientId>) -> Result<(OrderId, Order), Error> {
let side = self
.side
.ok_or_else(|| err!("Missing Order side (Buy/Sell)"))?;
let size = self
.size
.as_ref()
.ok_or_else(|| err!("Missing Order size"))?;
let ty = self.ty.as_ref().ok_or_else(|| err!("Missing Order type"))?;
let post_only = self.post_only.as_ref().unwrap_or(&false);
let execution = self.execution.as_ref().unwrap_or(&OrderExecution::Default);
let time_in_force = ty.time_in_force(
&self.time_in_force.unwrap_or(OrderTimeInForce::Unspecified),
*post_only,
execution,
)?;
let reduce_only = *self.reduce_only.as_ref().unwrap_or(&false);
let until = self
.until
.as_ref()
.ok_or_else(|| err!("Missing Order until (good-til-oneof)"))?;
let quantums = self
.market_params
.quantize_quantity(size.clone())
.to_u64()
.ok_or_else(|| err!("Failed converting BigDecimal size into u64"))?;
let conditional_order_trigger_subticks = match ty {
OrderType::StopLimit
| OrderType::StopMarket
| OrderType::TakeProfit
| OrderType::TakeProfitMarket => self
.market_params
.quantize_price(
self.trigger_price
.clone()
.ok_or_else(|| err!("Missing Order trigger price"))?,
)
.to_u64()
.ok_or_else(|| err!("Failed converting BigDecimal trigger-price into u64"))?,
_ => 0,
};
let clob_pair_id = self.market_params.clob_pair_id().0;
let order_id = OrderId {
subaccount_id: Some(self.subaccount_id.clone()),
client_id: client_id.into().0,
order_flags: self.flags.clone() as u32,
clob_pair_id,
};
let order = Order {
order_id: Some(order_id.clone()),
side: side.into(),
quantums,
subticks: self.calculate_subticks()?,
time_in_force: time_in_force.into(),
reduce_only,
client_metadata: DEFAULT_RUST_CLIENT_METADATA,
condition_type: ty.condition_type()?.into(),
conditional_order_trigger_subticks,
good_til_oneof: Some(until.clone().try_into()?),
builder_code_parameters: self.builder_code_parameters.clone(),
twap_parameters: self.twap_parameters,
order_router_address: self.order_router_address.to_string(),
};
Ok((order_id, order))
}
fn calculate_subticks(&self) -> Result<u64, Error> {
let ty = self.ty.as_ref().ok_or_else(|| err!("Missing Order type"))?;
let price = match ty {
OrderType::Market | OrderType::StopMarket | OrderType::TakeProfitMarket => {
if let Some(price) = self.price.clone() {
price
} else if let Some(oracle_price) = self.market_params.oracle_price.clone() {
let side = self
.side
.as_ref()
.ok_or_else(|| err!("Missing Order side"))?;
let one = <BigDecimal as One>::one();
match side {
OrderSide::Buy => oracle_price * (one + &self.slippage),
OrderSide::Sell => oracle_price * (one - &self.slippage),
_ => return Err(err!("Order side {side:?} not supported")),
}
} else {
return Err(err!("Failed to calculate Market order slippage price"));
}
}
_ => self
.price
.clone()
.ok_or_else(|| err!("Missing Order price"))?,
};
self.market_params
.quantize_price(price)
.to_u64()
.ok_or_else(|| err!("Failed converting BigDecimal price into u64"))
}
}
impl OrderType {
pub fn time_in_force(
&self,
time_in_force: &OrderTimeInForce,
post_only: bool,
execution: &OrderExecution,
) -> Result<OrderTimeInForce, Error> {
match self {
OrderType::Market => Ok(OrderTimeInForce::Ioc),
OrderType::Limit => {
if post_only {
Ok(OrderTimeInForce::PostOnly)
} else {
Ok(*time_in_force)
}
}
OrderType::StopLimit | OrderType::TakeProfit => match execution {
OrderExecution::Default => Ok(OrderTimeInForce::Unspecified),
OrderExecution::PostOnly => Ok(OrderTimeInForce::PostOnly),
OrderExecution::Fok => Ok(OrderTimeInForce::FillOrKill),
OrderExecution::Ioc => Ok(OrderTimeInForce::Ioc),
},
OrderType::StopMarket | OrderType::TakeProfitMarket => match execution {
OrderExecution::Default | OrderExecution::PostOnly => Err(err!(
"Execution value {execution:?} not supported for order type {self:?}"
)),
OrderExecution::Fok => Ok(OrderTimeInForce::FillOrKill),
OrderExecution::Ioc => Ok(OrderTimeInForce::Ioc),
},
_ => Err(err!(
"Invalid combination of order type, time in force, and execution"
)),
}
}
pub fn condition_type(&self) -> Result<ConditionType, Error> {
match self {
OrderType::Limit | OrderType::Market => Ok(ConditionType::Unspecified),
OrderType::StopLimit | OrderType::StopMarket => Ok(ConditionType::StopLoss),
OrderType::TakeProfit | OrderType::TakeProfitMarket => Ok(ConditionType::TakeProfit),
_ => Err(err!("Order type unsupported for condition type")),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::indexer::{ClobPairId, PerpetualMarketStatus, PerpetualMarketType, Ticker};
use std::str::FromStr;
fn sample_market_params() -> OrderMarketParams {
PerpetualMarket {
ticker: Ticker::from("BTC-USD"),
default_funding_rate_1h: Default::default(),
atomic_resolution: -10,
clob_pair_id: ClobPairId(0),
market_type: PerpetualMarketType::Cross,
quantum_conversion_exponent: -9,
step_base_quantums: 1_000_000,
subticks_per_tick: 100_000,
base_open_interest: Default::default(),
initial_margin_fraction: Default::default(),
maintenance_margin_fraction: Default::default(),
next_funding_rate: Default::default(),
open_interest: Default::default(),
open_interest_lower_cap: None,
open_interest_upper_cap: None,
oracle_price: Default::default(),
price_change_24h: Default::default(),
status: PerpetualMarketStatus::Active,
step_size: Default::default(),
tick_size: Default::default(),
trades_24h: 0,
volume_24h: Quantity(0.into()),
}
.into()
}
fn bigdecimal(val: &str) -> BigDecimal {
BigDecimal::from_str(val).expect("Failed converting str into BigDecimal")
}
#[test]
fn market_size_to_quantums() {
let market = sample_market_params();
let size = bigdecimal("0.01");
let quantums = market.quantize_quantity(size);
let expected = bigdecimal("100_000_000");
assert_eq!(quantums, expected);
}
#[test]
fn market_price_to_subticks() {
let market = sample_market_params();
let price = bigdecimal("50_000");
let subticks = market.quantize_price(price);
let expected = bigdecimal("5_000_000_000");
assert_eq!(subticks, expected);
}
#[test]
fn market_quantums_to_size() {
let market = sample_market_params();
let quantums = bigdecimal("100_000_000");
let size = market.dequantize_quantums(quantums);
let expected = bigdecimal("0.01");
assert_eq!(size, expected);
}
#[test]
fn market_subticks_to_price() {
let market = sample_market_params();
let subticks = bigdecimal("5_000_000_000");
let price = market.dequantize_subticks(subticks);
let expected = bigdecimal("50_000");
assert_eq!(price, expected);
}
}