use crate::order::id::{ClientOrderId, OrderId, StrategyId};
use chrono::{DateTime, Utc};
use derive_more::{Constructor, From};
use rust_decimal::Decimal;
use rustrade_instrument::{
Side,
asset::QuoteAsset,
instrument::{Instrument, kind::InstrumentKind, round_toward_zero_to_step},
};
use serde::{Deserialize, Serialize};
use smol_str::SmolStr;
use std::fmt::{Display, Formatter};
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, From)]
pub struct TradeId<T = SmolStr>(pub T);
impl<T> Display for TradeId<T>
where
T: Display,
{
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
Display::fmt(&self.0, f)
}
}
impl TradeId {
pub fn new<S: AsRef<str>>(id: S) -> Self {
Self(SmolStr::new(id))
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)]
pub struct Trade<AssetKey, InstrumentKey> {
pub id: TradeId,
pub order_id: OrderId,
pub instrument: InstrumentKey,
pub strategy: StrategyId,
pub time_exchange: DateTime<Utc>,
pub side: Side,
pub price: Decimal,
pub quantity: Decimal,
#[serde(default)]
pub order_filled_quantity: Option<Decimal>,
pub fees: AssetFees<AssetKey>,
#[serde(default)]
pub origin: TradeOrigin,
#[serde(default)]
pub position_side: Option<Side>,
#[serde(default)]
pub client_order_id: Option<ClientOrderId>,
}
impl<AssetKey, InstrumentKey> Trade<AssetKey, InstrumentKey> {
#[allow(clippy::too_many_arguments)] pub fn new(
id: TradeId,
order_id: OrderId,
instrument: InstrumentKey,
strategy: StrategyId,
time_exchange: DateTime<Utc>,
side: Side,
price: Decimal,
quantity: Decimal,
order_filled_quantity: Option<Decimal>,
fees: AssetFees<AssetKey>,
) -> Self {
Self {
id,
order_id,
instrument,
strategy,
time_exchange,
side,
price,
quantity,
order_filled_quantity,
fees,
origin: TradeOrigin::Order,
position_side: None,
client_order_id: None,
}
}
#[must_use]
pub fn with_origin(self, origin: TradeOrigin) -> Self {
Self { origin, ..self }
}
#[must_use]
pub fn with_position_side(self, position_side: Side) -> Self {
Self {
position_side: Some(position_side),
..self
}
}
#[must_use]
pub fn with_client_order_id(self, client_order_id: ClientOrderId) -> Self {
Self {
client_order_id: Some(client_order_id),
..self
}
}
pub fn value_quote(&self) -> Decimal {
self.price * self.quantity.abs()
}
pub fn holding_move<ExchangeKey>(
&self,
instrument: &Instrument<ExchangeKey, AssetKey>,
held: Decimal,
reduce_only: bool,
) -> Option<HoldingMove>
where
AssetKey: PartialEq,
{
let base_fee = matches!(instrument.kind, InstrumentKind::Spot)
&& self.fees.asset == instrument.underlying.base
&& !self.fees.fees.is_zero();
let netted = signed(self.side, self.quantity.abs())
- if base_fee {
self.fees.fees
} else {
Decimal::ZERO
};
let (netted_side, _) = unsigned(netted, self.side);
if reduce_only && let Some(moved) = reduced_by(held, netted) {
let (side, quantity) = unsigned(moved, netted_side);
return Some(HoldingMove {
side,
quantity,
dust: Decimal::ZERO,
excess: netted - moved,
});
}
if !base_fee {
return None;
}
let holds = held + netted;
let rounded = instrument
.quantity_step()
.map_or(holds, |step| round_toward_zero_to_step(holds, step));
let (side, quantity) = unsigned(rounded - held, netted_side);
Some(HoldingMove {
side,
quantity,
dust: holds - rounded,
excess: Decimal::ZERO,
})
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Deserialize, Serialize)]
pub struct HoldingMove {
pub side: Side,
pub quantity: Decimal,
pub dust: Decimal,
pub excess: Decimal,
}
fn signed(side: Side, quantity: Decimal) -> Decimal {
match side {
Side::Buy => quantity,
Side::Sell => -quantity,
}
}
fn unsigned(moved: Decimal, at_zero: Side) -> (Side, Decimal) {
let side = match moved.cmp(&Decimal::ZERO) {
std::cmp::Ordering::Greater => Side::Buy,
std::cmp::Ordering::Less => Side::Sell,
std::cmp::Ordering::Equal => at_zero,
};
(side, moved.abs())
}
fn reduced_by(held: Decimal, moved: Decimal) -> Option<Decimal> {
if held.cmp(&Decimal::ZERO) != moved.cmp(&Decimal::ZERO).reverse() {
return (!moved.is_zero()).then_some(Decimal::ZERO);
}
(moved.abs() > held.abs()).then_some(-held)
}
#[non_exhaustive]
#[must_use = "`resume` says whether the span was read to its end"]
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
pub struct TradesRead<AssetKey, InstrumentKey> {
pub trades: Vec<Trade<AssetKey, InstrumentKey>>,
pub resume: Option<DateTime<Utc>>,
pub held_since: Option<DateTime<Utc>>,
}
impl<AssetKey, InstrumentKey> TradesRead<AssetKey, InstrumentKey> {
pub fn new(trades: Vec<Trade<AssetKey, InstrumentKey>>, resume: Option<DateTime<Utc>>) -> Self {
Self {
trades,
resume,
held_since: None,
}
}
pub fn complete(trades: Vec<Trade<AssetKey, InstrumentKey>>) -> Self {
Self::new(trades, None)
}
pub fn with_held_since(mut self, held_since: Option<DateTime<Utc>>) -> Self {
self.held_since = held_since;
self
}
}
impl<AssetKey, InstrumentKey> Display for Trade<AssetKey, InstrumentKey>
where
AssetKey: Display,
InstrumentKey: Display,
{
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{{ instrument: {}, side: {}, price: {}, quantity: {}, time: {}",
self.instrument, self.side, self.price, self.quantity, self.time_exchange
)?;
if self.origin != TradeOrigin::Order {
write!(f, ", origin: {}", self.origin)?;
}
write!(f, " }}")
}
}
#[non_exhaustive]
#[derive(
Debug, Copy, Clone, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize,
)]
pub enum TradeOrigin {
#[default]
Order,
Liquidation,
Adl,
Assignment,
Exercise,
Expiry,
External,
}
impl TradeOrigin {
pub fn is_forced(self) -> bool {
matches!(self, Self::Liquidation | Self::Adl | Self::Assignment)
}
}
impl Display for TradeOrigin {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Order => "order",
Self::Liquidation => "liquidation",
Self::Adl => "adl",
Self::Assignment => "assignment",
Self::Exercise => "exercise",
Self::Expiry => "expiry",
Self::External => "external",
})
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Constructor)]
pub struct TradeAmendment<AssetKey, InstrumentKey> {
pub instrument: InstrumentKey,
pub order_id: OrderId,
pub time_exchange: DateTime<Utc>,
pub original: Option<TradeId>,
pub kind: TradeAmendmentKind<AssetKey, InstrumentKey>,
}
#[non_exhaustive]
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
pub enum TradeAmendmentKind<AssetKey, InstrumentKey> {
Busted {
quantity: Option<Decimal>,
},
Corrected {
replacement: Trade<AssetKey, InstrumentKey>,
},
CorrectedUnresolved {
id: Option<TradeId>,
price: Option<Decimal>,
quantity: Option<Decimal>,
},
}
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)]
pub struct AssetFees<AssetKey> {
pub asset: AssetKey,
pub fees: Decimal,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fees_quote: Option<Decimal>,
}
impl<AssetKey> AssetFees<AssetKey> {
pub fn new(asset: AssetKey, fees: Decimal, fees_quote: Option<Decimal>) -> Self {
Self {
asset,
fees,
fees_quote,
}
}
}
impl AssetFees<QuoteAsset> {
pub fn quote_fees(fees: Decimal) -> Self {
Self {
asset: QuoteAsset,
fees,
fees_quote: Some(fees),
}
}
}
impl Default for AssetFees<QuoteAsset> {
fn default() -> Self {
Self {
asset: QuoteAsset,
fees: Decimal::ZERO,
fees_quote: Some(Decimal::ZERO),
}
}
}
impl<AssetKey> Default for AssetFees<Option<AssetKey>> {
fn default() -> Self {
Self {
asset: None,
fees: Decimal::ZERO,
fees_quote: None,
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)] mod tests {
use super::*;
use rust_decimal_macros::dec;
use rustrade_instrument::{
asset::{Asset, name::AssetNameExchange},
exchange::ExchangeId,
instrument::{
kind::perpetual::PerpetualContract,
name::InstrumentNameExchange,
spec::{
InstrumentSpec, InstrumentSpecNotional, InstrumentSpecPrice,
InstrumentSpecQuantity, OrderQuantityUnits,
},
},
test_utils,
};
fn trade() -> Trade<AssetNameExchange, InstrumentNameExchange> {
Trade::new(
TradeId::new("t-1"),
OrderId::new("o-1"),
InstrumentNameExchange::new("btc_usdt"),
StrategyId::unknown(),
DateTime::<Utc>::MIN_UTC,
Side::Sell,
Decimal::ONE,
Decimal::TWO,
None,
AssetFees::new(AssetNameExchange::from("usdt"), Decimal::ZERO, None),
)
}
#[test]
fn a_trade_is_of_an_order_unless_given_another_origin() {
assert_eq!(trade().origin, TradeOrigin::Order);
assert_eq!(
trade().with_origin(TradeOrigin::Adl).origin,
TradeOrigin::Adl
);
}
#[test]
fn a_trade_without_an_origin_deserialises_as_an_orders() {
let mut json = serde_json::to_value(trade().with_origin(TradeOrigin::External)).unwrap();
assert_eq!(json["origin"], "External");
json.as_object_mut().unwrap().remove("origin");
let read: Trade<AssetNameExchange, InstrumentNameExchange> =
serde_json::from_value(json).unwrap();
assert_eq!(read, trade());
}
#[test]
fn display_names_the_origin_only_when_it_is_not_an_orders() {
assert!(!trade().to_string().contains("origin"));
assert!(
trade()
.with_origin(TradeOrigin::Liquidation)
.to_string()
.ends_with(", origin: liquidation }")
);
}
fn btc_usdt(step: Option<Decimal>) -> Instrument<ExchangeId, Asset> {
let mut instrument = test_utils::instrument(ExchangeId::BinanceSpot, "btc", "usdt");
instrument.spec = step.map(|step| {
InstrumentSpec::new(
InstrumentSpecPrice::new(dec!(0.01), dec!(0.01)),
InstrumentSpecQuantity::new(
OrderQuantityUnits::Asset(test_utils::asset("btc")),
step,
step,
),
InstrumentSpecNotional::new(Decimal::ZERO),
)
});
instrument
}
fn fill(side: Side, quantity: Decimal, fee_asset: &str, fee: Decimal) -> Trade<Asset, ()> {
Trade::new(
TradeId::new("t-1"),
OrderId::new("o-1"),
(),
StrategyId::unknown(),
DateTime::<Utc>::MIN_UTC,
side,
dec!(100),
quantity,
None,
AssetFees::new(test_utils::asset(fee_asset), fee, None),
)
}
fn moved(side: Side, quantity: Decimal, dust: Decimal) -> Option<HoldingMove> {
Some(HoldingMove {
side,
quantity,
dust,
excess: Decimal::ZERO,
})
}
fn cut(side: Side, quantity: Decimal, excess: Decimal) -> Option<HoldingMove> {
Some(HoldingMove {
side,
quantity,
dust: Decimal::ZERO,
excess,
})
}
#[test]
fn holding_move_is_none_unless_a_spot_fill_pays_a_base_fee() {
let mut perpetual = btc_usdt(None);
perpetual.kind = InstrumentKind::Perpetual(PerpetualContract {
contract_size: Decimal::ONE,
settlement_asset: test_utils::asset("btc"),
});
let cases = [
(
"quote fee",
btc_usdt(None),
fill(Side::Buy, dec!(1), "usdt", dec!(0.1)),
),
(
"third-asset fee",
btc_usdt(None),
fill(Side::Buy, dec!(1), "bnb", dec!(0.001)),
),
(
"zero base fee",
btc_usdt(None),
fill(Side::Buy, dec!(1), "btc", Decimal::ZERO),
),
(
"derivative",
perpetual,
fill(Side::Buy, dec!(1), "btc", dec!(0.001)),
),
];
for (case, instrument, fill) in cases {
assert_eq!(
fill.holding_move(&instrument, Decimal::ZERO, false),
None,
"{case}"
);
}
}
#[test]
fn holding_move_nets_a_base_fee_on_either_side() {
let spot = btc_usdt(None);
let cases = [
(
"buy holds less",
fill(Side::Buy, dec!(1), "btc", dec!(0.001)),
moved(Side::Buy, dec!(0.999), Decimal::ZERO),
),
(
"sell delivers more",
fill(Side::Sell, dec!(1), "btc", dec!(0.001)),
moved(Side::Sell, dec!(1.001), Decimal::ZERO),
),
(
"rebate adds",
fill(Side::Buy, dec!(1), "btc", dec!(-0.001)),
moved(Side::Buy, dec!(1.001), Decimal::ZERO),
),
(
"fee above a buy sells",
fill(Side::Buy, dec!(0.001), "btc", dec!(0.003)),
moved(Side::Sell, dec!(0.002), Decimal::ZERO),
),
(
"fee equal to a buy moves nothing",
fill(Side::Buy, dec!(0.001), "btc", dec!(0.001)),
moved(Side::Buy, Decimal::ZERO, Decimal::ZERO),
),
];
for (case, fill, expected) in cases {
assert_eq!(fill.holding_move(&spot, dec!(2), false), expected, "{case}");
}
}
#[test]
fn holding_move_rounds_the_position_toward_zero_to_the_step() {
let spot = btc_usdt(Some(dec!(0.01)));
let cases = [
(
"buy from flat",
Decimal::ZERO,
fill(Side::Buy, dec!(1), "btc", dec!(0.001)),
moved(Side::Buy, dec!(0.99), dec!(0.009)),
),
(
"sell from a long",
dec!(2),
fill(Side::Sell, dec!(1), "btc", dec!(0.001)),
moved(Side::Sell, dec!(1.01), dec!(0.009)),
),
(
"buy back a short",
dec!(-1),
fill(Side::Buy, dec!(1), "btc", dec!(0.001)),
moved(Side::Buy, dec!(1), dec!(-0.001)),
),
(
"sell opening a short",
Decimal::ZERO,
fill(Side::Sell, dec!(1), "btc", dec!(0.001)),
moved(Side::Sell, dec!(1), dec!(-0.001)),
),
(
"held off the step",
dec!(0.005),
fill(Side::Buy, dec!(1), "btc", dec!(0.001)),
moved(Side::Buy, dec!(0.995), dec!(0.004)),
),
(
"lands on the step",
dec!(0.001),
fill(Side::Buy, dec!(1), "btc", dec!(0.001)),
moved(Side::Buy, dec!(0.999), Decimal::ZERO),
),
];
for (case, held, fill, expected) in cases {
assert_eq!(fill.holding_move(&spot, held, false), expected, "{case}");
}
}
#[test]
fn holding_move_rounded_to_nothing_keeps_the_netted_side() {
let spot = btc_usdt(Some(dec!(1)));
let cases = [
(
"buy",
dec!(1),
fill(Side::Buy, dec!(0.5), "btc", dec!(0.001)),
moved(Side::Buy, Decimal::ZERO, dec!(0.499)),
),
(
"fee reverses the buy",
dec!(-1),
fill(Side::Buy, dec!(0.001), "btc", dec!(0.003)),
moved(Side::Sell, Decimal::ZERO, dec!(-0.002)),
),
];
for (case, held, fill, expected) in cases {
assert_eq!(fill.holding_move(&spot, held, false), expected, "{case}");
}
}
#[test]
fn holding_move_cuts_a_reduce_only_move_at_the_position() {
let spot = btc_usdt(Some(dec!(0.01)));
let cases = [
(
"buy past a short",
dec!(-10),
fill(Side::Buy, dec!(10.5), "btc", dec!(0.01)),
cut(Side::Buy, dec!(10), dec!(0.49)),
),
(
"sell past a long",
dec!(1),
fill(Side::Sell, dec!(1.5), "usdt", dec!(0.1)),
cut(Side::Sell, dec!(1), dec!(-0.5)),
),
(
"buy from flat",
Decimal::ZERO,
fill(Side::Buy, dec!(1), "usdt", dec!(0.1)),
cut(Side::Buy, Decimal::ZERO, dec!(1)),
),
(
"buy onto a long",
dec!(1),
fill(Side::Buy, dec!(1), "btc", dec!(0.001)),
cut(Side::Buy, Decimal::ZERO, dec!(0.999)),
),
(
"fee reverses a buy onto a short",
dec!(-1),
fill(Side::Buy, dec!(0.001), "btc", dec!(0.003)),
cut(Side::Sell, Decimal::ZERO, dec!(-0.002)),
),
];
for (case, held, fill, expected) in cases {
assert_eq!(fill.holding_move(&spot, held, true), expected, "{case}");
}
}
#[test]
fn holding_move_leaves_a_reduce_only_move_the_position_takes() {
let spot = btc_usdt(Some(dec!(0.01)));
let cases = [
(
"partial, base fee",
dec!(-2),
fill(Side::Buy, dec!(1), "btc", dec!(0.001)),
moved(Side::Buy, dec!(1), dec!(-0.001)),
),
(
"to flat exactly, base fee",
dec!(-0.999),
fill(Side::Buy, dec!(1), "btc", dec!(0.001)),
moved(Side::Buy, dec!(0.999), Decimal::ZERO),
),
(
"partial, rounded",
dec!(2),
fill(Side::Sell, dec!(1), "btc", dec!(0.001)),
moved(Side::Sell, dec!(1.01), dec!(0.009)),
),
(
"to flat exactly, quote fee",
dec!(1),
fill(Side::Sell, dec!(1), "usdt", dec!(0.1)),
None,
),
(
"fee equal to a buy moves nothing",
dec!(-1),
fill(Side::Buy, dec!(0.001), "btc", dec!(0.001)),
moved(Side::Buy, Decimal::ZERO, Decimal::ZERO),
),
(
"fee equal to a buy, held off the step",
dec!(-1.005),
fill(Side::Buy, dec!(0.001), "btc", dec!(0.001)),
moved(Side::Buy, dec!(0.005), dec!(-0.005)),
),
];
for (case, held, fill, expected) in cases {
assert_eq!(fill.holding_move(&spot, held, true), expected, "{case}");
}
}
#[test]
fn holding_move_cuts_a_reduce_only_derivative_fill() {
let mut perpetual = btc_usdt(None);
perpetual.kind = InstrumentKind::Perpetual(PerpetualContract {
contract_size: Decimal::ONE,
settlement_asset: test_utils::asset("btc"),
});
let fill = fill(Side::Buy, dec!(3), "btc", dec!(0.001));
assert_eq!(
fill.holding_move(&perpetual, dec!(-2), true),
cut(Side::Buy, dec!(2), dec!(1))
);
assert_eq!(fill.holding_move(&perpetual, dec!(-3), true), None);
assert_eq!(fill.holding_move(&perpetual, dec!(-2), false), None);
}
#[test]
fn only_liquidation_adl_and_assignment_are_forced() {
let forced: Vec<_> = [
TradeOrigin::Order,
TradeOrigin::Liquidation,
TradeOrigin::Adl,
TradeOrigin::Assignment,
TradeOrigin::Exercise,
TradeOrigin::Expiry,
TradeOrigin::External,
]
.into_iter()
.filter(|origin| origin.is_forced())
.collect();
assert_eq!(
forced,
[
TradeOrigin::Liquidation,
TradeOrigin::Adl,
TradeOrigin::Assignment
]
);
}
}