use crate::{
error::OrderError,
margin::MarginEffect,
market::MarketSnapshot,
order::{
OrderEvent, OrderKind, TimeInForce,
id::{PositionId, VenueOrderId},
state::Cancelled,
},
};
use derive_more::Constructor;
use rust_decimal::Decimal;
use rustrade_instrument::{
Side,
asset::{AssetIndex, name::AssetNameExchange},
exchange::{ExchangeId, ExchangeIndex},
instrument::{InstrumentIndex, name::InstrumentNameExchange},
};
use serde::{Deserialize, Deserializer, Serialize};
use std::fmt;
pub type OrderRequestOpen<ExchangeKey = ExchangeIndex, InstrumentKey = InstrumentIndex> =
OrderEvent<RequestOpen, ExchangeKey, InstrumentKey>;
pub type OrderRequestCancel<ExchangeKey = ExchangeIndex, InstrumentKey = InstrumentIndex> =
OrderEvent<RequestCancel, ExchangeKey, InstrumentKey>;
pub type OrderResponseCancel<
ExchangeKey = ExchangeIndex,
AssetKey = AssetIndex,
InstrumentKey = InstrumentIndex,
> = OrderEvent<Result<Cancelled, OrderError<AssetKey, InstrumentKey>>, ExchangeKey, InstrumentKey>;
pub type UnindexedOrderResponseCancel =
OrderResponseCancel<ExchangeId, AssetNameExchange, InstrumentNameExchange>;
#[non_exhaustive]
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)]
pub struct RequestOpen {
pub side: Side,
pub price: Option<Decimal>,
pub quantity: Decimal,
pub kind: OrderKind,
pub time_in_force: TimeInForce,
#[serde(default)]
pub position_id: Option<PositionId>,
#[serde(default)]
pub reduce_only: ReduceOnly,
#[serde(default)]
pub market: Option<MarketSnapshot>,
#[serde(default)]
pub margin_effect: Option<MarginEffect>,
}
impl RequestOpen {
pub fn new(
side: Side,
price: Option<Decimal>,
quantity: Decimal,
kind: OrderKind,
time_in_force: TimeInForce,
) -> Self {
Self {
side,
price,
quantity,
kind,
time_in_force,
position_id: None,
reduce_only: ReduceOnly::No,
market: None,
margin_effect: None,
}
}
#[must_use]
pub fn with_position_id(self, position_id: PositionId) -> Self {
Self {
position_id: Some(position_id),
..self
}
}
#[must_use]
pub fn with_reduce_only(self, reduce_only: ReduceOnly) -> Self {
Self {
reduce_only,
..self
}
}
#[must_use]
pub fn with_market(self, market: MarketSnapshot) -> Self {
Self {
market: Some(market),
..self
}
}
#[must_use]
pub fn with_margin_effect(self, margin_effect: MarginEffect) -> Self {
Self {
margin_effect: Some(margin_effect),
..self
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub enum ReduceOnly {
#[default]
No,
Position,
PositionAndFreeBase,
}
impl ReduceOnly {
pub const fn is_reduce_only(self) -> bool {
!matches!(self, Self::No)
}
}
#[derive(Deserialize, Serialize)]
#[serde(rename = "ReduceOnly", rename_all = "snake_case")]
enum ReduceOnlyVariant {
No,
Position,
PositionAndFreeBase,
}
impl From<ReduceOnly> for ReduceOnlyVariant {
fn from(reduce_only: ReduceOnly) -> Self {
match reduce_only {
ReduceOnly::No => Self::No,
ReduceOnly::Position => Self::Position,
ReduceOnly::PositionAndFreeBase => Self::PositionAndFreeBase,
}
}
}
impl From<ReduceOnlyVariant> for ReduceOnly {
fn from(variant: ReduceOnlyVariant) -> Self {
match variant {
ReduceOnlyVariant::No => Self::No,
ReduceOnlyVariant::Position => Self::Position,
ReduceOnlyVariant::PositionAndFreeBase => Self::PositionAndFreeBase,
}
}
}
impl Serialize for ReduceOnly {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
ReduceOnlyVariant::from(*self).serialize(serializer)
}
}
impl<'de> Deserialize<'de> for ReduceOnly {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct ReduceOnlyVisitor;
impl serde::de::Visitor<'_> for ReduceOnlyVisitor {
type Value = ReduceOnly;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(r#""no", "position", "position_and_free_base", or a bool"#)
}
fn visit_bool<E: serde::de::Error>(self, reduce_only: bool) -> Result<Self::Value, E> {
Ok(if reduce_only {
ReduceOnly::Position
} else {
ReduceOnly::No
})
}
fn visit_str<E: serde::de::Error>(self, name: &str) -> Result<Self::Value, E> {
match name {
"no" => Ok(ReduceOnly::No),
"position" => Ok(ReduceOnly::Position),
"position_and_free_base" => Ok(ReduceOnly::PositionAndFreeBase),
_ => Err(E::unknown_variant(
name,
&["no", "position", "position_and_free_base"],
)),
}
}
}
if deserializer.is_human_readable() {
deserializer.deserialize_any(ReduceOnlyVisitor)
} else {
ReduceOnlyVariant::deserialize(deserializer).map(Self::from)
}
}
}
#[derive(
Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Default, Deserialize, Serialize, Constructor,
)]
pub struct RequestCancel {
pub id: Option<VenueOrderId>,
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
fn limit() -> RequestOpen {
RequestOpen::new(
Side::Sell,
Some(dec!(101)),
dec!(2),
OrderKind::Limit,
TimeInForce::GoodUntilCancelled { post_only: false },
)
}
#[test]
fn new_sets_the_given_fields_and_defaults_the_rest() {
let request = limit();
assert_eq!(request.side, Side::Sell);
assert_eq!(request.price, Some(dec!(101)));
assert_eq!(request.quantity, dec!(2));
assert_eq!(request.kind, OrderKind::Limit);
assert_eq!(
request.time_in_force,
TimeInForce::GoodUntilCancelled { post_only: false }
);
assert_eq!(request.position_id, None);
assert_eq!(request.reduce_only, ReduceOnly::No);
assert_eq!(request.market, None);
assert_eq!(request.margin_effect, None);
}
#[test]
#[allow(clippy::unwrap_used)] fn a_request_serialised_without_a_margin_effect_still_loads() {
let mut json = serde_json::to_value(limit()).unwrap();
json.as_object_mut().unwrap().remove("margin_effect");
let request: RequestOpen = serde_json::from_value(json).unwrap();
assert_eq!(request.margin_effect, None);
}
#[test]
#[allow(clippy::unwrap_used)] fn reduce_only_round_trips_by_name_and_loads_a_flag() {
for (reduce_only, name) in [
(ReduceOnly::No, "no"),
(ReduceOnly::Position, "position"),
(ReduceOnly::PositionAndFreeBase, "position_and_free_base"),
] {
let json = serde_json::to_value(limit().with_reduce_only(reduce_only)).unwrap();
assert_eq!(json["reduce_only"], name);
let loaded: RequestOpen = serde_json::from_value(json).unwrap();
assert_eq!(loaded.reduce_only, reduce_only);
}
for (flag, reduce_only) in [(false, ReduceOnly::No), (true, ReduceOnly::Position)] {
let mut json = serde_json::to_value(limit()).unwrap();
json["reduce_only"] = flag.into();
let loaded: RequestOpen = serde_json::from_value(json).unwrap();
assert_eq!(loaded.reduce_only, reduce_only);
}
let mut json = serde_json::to_value(limit()).unwrap();
json.as_object_mut().unwrap().remove("reduce_only");
let loaded: RequestOpen = serde_json::from_value(json).unwrap();
assert_eq!(loaded.reduce_only, ReduceOnly::No);
let mut json = serde_json::to_value(limit()).unwrap();
json["reduce_only"] = "sometimes".into();
let error = serde_json::from_value::<RequestOpen>(json).unwrap_err();
assert!(error.to_string().contains("sometimes"), "{error}");
let mut json = serde_json::to_value(limit()).unwrap();
json["reduce_only"] = 1.into();
let error = serde_json::from_value::<RequestOpen>(json).unwrap_err();
assert!(
error
.to_string()
.contains(r#""no", "position", "position_and_free_base", or a bool"#),
"{error}"
);
}
struct NotHumanReadable<D>(D);
impl<'de, D: Deserializer<'de>> Deserializer<'de> for NotHumanReadable<D> {
type Error = D::Error;
fn deserialize_any<V: serde::de::Visitor<'de>>(
self,
_: V,
) -> Result<V::Value, Self::Error> {
Err(serde::de::Error::custom(
"a binary format cannot deserialize_any",
))
}
fn deserialize_enum<V: serde::de::Visitor<'de>>(
self,
name: &'static str,
variants: &'static [&'static str],
visitor: V,
) -> Result<V::Value, Self::Error> {
self.0.deserialize_enum(name, variants, visitor)
}
fn is_human_readable(&self) -> bool {
false
}
serde::forward_to_deserialize_any! {
bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string bytes byte_buf
option unit unit_struct newtype_struct seq tuple tuple_struct map struct identifier
ignored_any
}
}
#[test]
#[allow(clippy::unwrap_used)] fn a_binary_format_reads_reduce_only_as_a_variant() {
for reduce_only in [
ReduceOnly::No,
ReduceOnly::Position,
ReduceOnly::PositionAndFreeBase,
] {
let written = serde_json::to_value(reduce_only).unwrap();
assert_eq!(
ReduceOnly::deserialize(NotHumanReadable(&written)).unwrap(),
reduce_only
);
}
assert!(ReduceOnly::deserialize(NotHumanReadable(&serde_json::Value::Bool(true))).is_err());
}
#[test]
#[allow(clippy::unwrap_used)] fn a_binary_format_reads_reduce_only_by_its_variant_index() {
use serde::de::value::{Error, U32Deserializer};
let read =
|index| ReduceOnly::deserialize(NotHumanReadable(U32Deserializer::<Error>::new(index)));
assert_eq!(read(0).unwrap(), ReduceOnly::No);
assert_eq!(read(1).unwrap(), ReduceOnly::Position);
assert_eq!(read(2).unwrap(), ReduceOnly::PositionAndFreeBase);
assert!(read(3).is_err());
}
#[test]
fn only_no_is_not_reduce_only() {
assert!(!ReduceOnly::No.is_reduce_only());
assert!(ReduceOnly::Position.is_reduce_only());
assert!(ReduceOnly::PositionAndFreeBase.is_reduce_only());
}
#[test]
fn with_methods_set_only_their_own_field() {
let set = RequestOpen {
position_id: Some(PositionId::new("a")),
reduce_only: ReduceOnly::Position,
market: Some(MarketSnapshot::from_last_price(Some(dec!(100)))),
margin_effect: Some(MarginEffect::Borrow),
..limit()
};
assert_eq!(
set.clone().with_position_id(PositionId::new("b")),
RequestOpen {
position_id: Some(PositionId::new("b")),
..set.clone()
}
);
assert_eq!(
set.clone()
.with_reduce_only(ReduceOnly::PositionAndFreeBase),
RequestOpen {
reduce_only: ReduceOnly::PositionAndFreeBase,
..set.clone()
}
);
let market = MarketSnapshot::from_last_price(Some(dec!(200)));
assert_eq!(
set.clone().with_market(market),
RequestOpen {
market: Some(market),
..set.clone()
}
);
assert_eq!(
set.clone().with_margin_effect(MarginEffect::Repay),
RequestOpen {
margin_effect: Some(MarginEffect::Repay),
..set
}
);
assert_eq!(
limit().with_position_id(PositionId::NETTING).position_id,
Some(PositionId::NETTING)
);
assert_eq!(
limit().with_reduce_only(ReduceOnly::Position).reduce_only,
ReduceOnly::Position
);
assert_eq!(limit().with_market(market).market, Some(market));
assert_eq!(
limit()
.with_margin_effect(MarginEffect::Repay)
.margin_effect,
Some(MarginEffect::Repay)
);
}
}