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};
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()
}
}
#[non_exhaustive]
#[must_use = "`resume` says whether the span was read to its end"]
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Constructor)]
pub struct TradesRead<AssetKey, InstrumentKey> {
pub trades: Vec<Trade<AssetKey, InstrumentKey>>,
pub resume: Option<DateTime<Utc>>,
}
impl<AssetKey, InstrumentKey> TradesRead<AssetKey, InstrumentKey> {
pub fn complete(trades: Vec<Trade<AssetKey, InstrumentKey>>) -> Self {
Self {
trades,
resume: None,
}
}
}
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 rustrade_instrument::{
asset::name::AssetNameExchange, instrument::name::InstrumentNameExchange,
};
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 }")
);
}
#[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
]
);
}
}