use crate::order::{
id::StrategyId,
request::{OrderRequestCancel, OrderRequestOpen, RequestCancel, RequestOpen},
state::UnindexedOrderState,
};
use chrono::{DateTime, Utc};
use derive_more::{Constructor, Display};
use id::ClientOrderId;
use rust_decimal::Decimal;
use rustrade_instrument::{
Side,
asset::{AssetIndex, name::AssetNameExchange},
exchange::{ExchangeId, ExchangeIndex},
instrument::{InstrumentIndex, name::InstrumentNameExchange},
};
use serde::{Deserialize, Serialize};
use state::{
ActiveOrderState, Cancelled, Expired, InactiveOrderState, Open, OpenInFlight, OrderState,
};
pub mod id;
pub mod state;
pub mod request;
pub mod bracket;
pub type UnindexedOrder = Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState>;
pub type UnindexedOrderKey = OrderKey<ExchangeId, InstrumentNameExchange>;
pub type UnindexedInactiveOrder =
Order<ExchangeId, InstrumentNameExchange, state::UnindexedInactiveOrderState>;
pub type UnindexedOrderSnapshot = Order<
ExchangeId,
InstrumentNameExchange,
OrderState<AssetNameExchange, InstrumentNameExchange>,
>;
pub type OrderSnapshot<
ExchangeKey = ExchangeIndex,
AssetKey = AssetIndex,
InstrumentKey = InstrumentIndex,
> = Order<ExchangeKey, InstrumentKey, OrderState<AssetKey, InstrumentKey>>;
#[derive(
Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Constructor,
)]
pub struct OrderEvent<State, ExchangeKey = ExchangeIndex, InstrumentKey = InstrumentIndex> {
pub key: OrderKey<ExchangeKey, InstrumentKey>,
pub state: State,
}
#[derive(
Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Constructor,
)]
pub struct OrderKey<ExchangeKey = ExchangeIndex, InstrumentKey = InstrumentIndex> {
pub exchange: ExchangeKey,
pub instrument: InstrumentKey,
pub strategy: StrategyId,
pub cid: ClientOrderId,
}
impl<ExchangeKey, InstrumentKey> OrderKey<ExchangeKey, &InstrumentKey>
where
InstrumentKey: Clone,
{
pub fn into_owned_instrument(self) -> OrderKey<ExchangeKey, InstrumentKey> {
let Self {
exchange,
instrument,
strategy,
cid,
} = self;
OrderKey {
exchange,
instrument: instrument.clone(),
strategy,
cid,
}
}
}
impl<State, ExchangeKey, InstrumentKey> OrderEvent<State, ExchangeKey, &InstrumentKey>
where
InstrumentKey: Clone,
{
pub fn into_owned_instrument(self) -> OrderEvent<State, ExchangeKey, InstrumentKey> {
let Self { key, state } = self;
OrderEvent {
key: key.into_owned_instrument(),
state,
}
}
}
#[derive(
Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Constructor,
)]
pub struct Order<ExchangeKey = ExchangeIndex, InstrumentKey = InstrumentIndex, State = OrderState> {
pub key: OrderKey<ExchangeKey, InstrumentKey>,
pub side: Side,
pub price: Option<Decimal>,
pub quantity: Decimal,
pub kind: OrderKind,
pub time_in_force: TimeInForce,
pub state: State,
}
impl<ExchangeKey, InstrumentKey, State> Order<ExchangeKey, InstrumentKey, State> {
pub fn map_state<NewState>(
self,
f: impl FnOnce(State) -> NewState,
) -> Order<ExchangeKey, InstrumentKey, NewState> {
let Order {
key,
side,
price,
quantity,
kind,
time_in_force,
state,
} = self;
Order {
key,
side,
price,
quantity,
kind,
time_in_force,
state: f(state),
}
}
}
impl<ExchangeKey, AssetKey, InstrumentKey>
Order<ExchangeKey, InstrumentKey, OrderState<AssetKey, InstrumentKey>>
{
pub fn to_active(&self) -> Option<Order<ExchangeKey, InstrumentKey, ActiveOrderState>>
where
ExchangeKey: Clone,
InstrumentKey: Clone,
{
let OrderState::Active(state) = &self.state else {
return None;
};
Some(Order {
key: self.key.clone(),
side: self.side,
price: self.price,
quantity: self.quantity,
kind: self.kind,
time_in_force: self.time_in_force,
state: state.clone(),
})
}
pub fn to_inactive(
&self,
) -> Option<Order<ExchangeKey, InstrumentKey, InactiveOrderState<AssetKey, InstrumentKey>>>
where
ExchangeKey: Clone,
AssetKey: Clone,
InstrumentKey: Clone,
{
let OrderState::Inactive(state) = &self.state else {
return None;
};
Some(Order {
key: self.key.clone(),
side: self.side,
price: self.price,
quantity: self.quantity,
kind: self.kind,
time_in_force: self.time_in_force,
state: state.clone(),
})
}
}
impl<ExchangeKey, InstrumentKey> Order<ExchangeKey, InstrumentKey, ActiveOrderState>
where
ExchangeKey: Clone,
InstrumentKey: Clone,
{
pub fn to_request_cancel(&self) -> Option<OrderRequestCancel<ExchangeKey, InstrumentKey>> {
let Order { key, state, .. } = self;
let request_cancel = match state {
ActiveOrderState::OpenInFlight(_) => RequestCancel { id: None },
ActiveOrderState::Open(open) => RequestCancel {
id: Some(open.id.clone()),
},
_ => return None,
};
Some(OrderRequestCancel {
key: key.clone(),
state: request_cancel,
})
}
}
#[derive(
Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Display,
)]
pub enum TrailingOffsetType {
Absolute,
Percentage,
BasisPoints,
}
#[derive(
Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Display,
)]
#[non_exhaustive]
pub enum OrderKind {
Market,
Limit,
#[display("Stop({trigger_price})")]
Stop {
trigger_price: Decimal,
},
#[display("StopLimit({trigger_price})")]
StopLimit {
trigger_price: Decimal,
},
#[display("TakeProfit({trigger_price})")]
TakeProfit {
trigger_price: Decimal,
},
#[display("TakeProfitLimit({trigger_price})")]
TakeProfitLimit {
trigger_price: Decimal,
},
#[display("TrailingStop({offset}, {offset_type}, activation: {activation_price:?})")]
TrailingStop {
offset: Decimal,
offset_type: TrailingOffsetType,
activation_price: Option<Decimal>,
},
#[display("TrailingStopLimit({offset}, {offset_type}, {stop_price}, {limit_offset})")]
TrailingStopLimit {
offset: Decimal,
offset_type: TrailingOffsetType,
stop_price: Decimal,
limit_offset: Decimal,
},
#[display("TrailingStopFixedLimit({offset}, {offset_type}, activation: {activation_price:?})")]
TrailingStopFixedLimit {
offset: Decimal,
offset_type: TrailingOffsetType,
activation_price: Option<Decimal>,
},
#[display("ConditionalUnresolved({family}, trigger: {trigger_price:?})")]
ConditionalUnresolved {
family: ConditionalFamily,
trigger_price: Option<Decimal>,
},
}
impl OrderKind {
pub(crate) fn is_conditional_unresolved(&self) -> bool {
matches!(self, Self::ConditionalUnresolved { .. })
}
}
#[derive(
Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Display,
)]
#[non_exhaustive]
pub enum ConditionalFamily {
Stop,
StopLimit,
TakeProfit,
TakeProfitLimit,
}
impl ConditionalFamily {
pub fn has_limit_price(self) -> bool {
matches!(self, Self::StopLimit | Self::TakeProfitLimit)
}
}
#[derive(
Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Display,
)]
pub enum TimeInForce {
GoodUntilCancelled {
post_only: bool,
},
GoodUntilEndOfDay,
FillOrKill,
ImmediateOrCancel,
#[display("GoodTillDate({expiry})")]
GoodTillDate {
expiry: DateTime<Utc>,
},
AtOpen,
AtClose,
}
impl<ExchangeKey, InstrumentKey> Order<ExchangeKey, InstrumentKey, ActiveOrderState>
where
ExchangeKey: Clone,
InstrumentKey: Clone,
{
pub fn open_in_flight(
request: &OrderRequestOpen<ExchangeKey, InstrumentKey>,
time_sent: DateTime<Utc>,
) -> Self {
let OrderRequestOpen {
key,
state:
RequestOpen {
side,
price,
quantity,
kind,
time_in_force,
position_id: _,
reduce_only: _, market: _, margin_effect: _, },
} = request;
Self {
key: key.clone(),
side: *side,
price: *price,
quantity: *quantity,
kind: *kind,
time_in_force: *time_in_force,
state: ActiveOrderState::OpenInFlight(OpenInFlight::new(time_sent)),
}
}
}
impl<ExchangeKey, InstrumentKey> From<Order<ExchangeKey, InstrumentKey, Open>>
for Order<ExchangeKey, InstrumentKey, ActiveOrderState>
{
fn from(value: Order<ExchangeKey, InstrumentKey, Open>) -> Self {
value.map_state(ActiveOrderState::Open)
}
}
impl<ExchangeKey, AssetKey, InstrumentKey> From<Order<ExchangeKey, InstrumentKey, Open>>
for Order<ExchangeKey, InstrumentKey, OrderState<AssetKey, InstrumentKey>>
{
fn from(value: Order<ExchangeKey, InstrumentKey, Open>) -> Self {
value.map_state(|state| OrderState::Active(ActiveOrderState::Open(state)))
}
}
impl<ExchangeKey, AssetKey, InstrumentKey> From<Order<ExchangeKey, InstrumentKey, Cancelled>>
for Order<ExchangeKey, InstrumentKey, OrderState<AssetKey, InstrumentKey>>
{
fn from(value: Order<ExchangeKey, InstrumentKey, Cancelled>) -> Self {
value.map_state(|state| OrderState::Inactive(InactiveOrderState::Cancelled(state)))
}
}
impl<ExchangeKey, AssetKey, InstrumentKey> From<Order<ExchangeKey, InstrumentKey, Expired>>
for Order<ExchangeKey, InstrumentKey, OrderState<AssetKey, InstrumentKey>>
{
fn from(value: Order<ExchangeKey, InstrumentKey, Expired>) -> Self {
value.map_state(|state| OrderState::Inactive(InactiveOrderState::Expired(state)))
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)] mod tests {
use super::*;
use rust_decimal_macros::dec;
#[test]
fn order_kind_display_take_profit() {
let tp = OrderKind::TakeProfit {
trigger_price: dec!(100.50),
};
assert_eq!(tp.to_string(), "TakeProfit(100.50)");
}
#[test]
fn order_kind_display_take_profit_limit() {
let tpl = OrderKind::TakeProfitLimit {
trigger_price: dec!(200.25),
};
assert_eq!(tpl.to_string(), "TakeProfitLimit(200.25)");
}
#[test]
fn order_kind_serde_roundtrip_take_profit() {
let original = OrderKind::TakeProfit {
trigger_price: dec!(150.00),
};
let json = serde_json::to_string(&original).unwrap();
let deserialized: OrderKind = serde_json::from_str(&json).unwrap();
assert_eq!(original, deserialized);
}
#[test]
fn order_kind_serde_roundtrip_take_profit_limit() {
let original = OrderKind::TakeProfitLimit {
trigger_price: dec!(250.75),
};
let json = serde_json::to_string(&original).unwrap();
let deserialized: OrderKind = serde_json::from_str(&json).unwrap();
assert_eq!(original, deserialized);
}
#[test]
fn trailing_kinds_round_trip_and_a_stored_trailing_stop_loads() {
for activation_price in [None, Some(dec!(105))] {
for original in [
OrderKind::TrailingStop {
offset: dec!(1),
offset_type: TrailingOffsetType::Percentage,
activation_price,
},
OrderKind::TrailingStopFixedLimit {
offset: dec!(25),
offset_type: TrailingOffsetType::BasisPoints,
activation_price,
},
] {
let json = serde_json::to_string(&original).unwrap();
let deserialized: OrderKind = serde_json::from_str(&json).unwrap();
assert_eq!(original, deserialized);
}
}
let stored: OrderKind =
serde_json::from_str(r#"{"TrailingStop":{"offset":"1","offset_type":"Percentage"}}"#)
.unwrap();
assert_eq!(
stored,
OrderKind::TrailingStop {
offset: dec!(1),
offset_type: TrailingOffsetType::Percentage,
activation_price: None,
}
);
assert_eq!(
OrderKind::TrailingStop {
offset: dec!(1),
offset_type: TrailingOffsetType::Percentage,
activation_price: Some(dec!(105)),
}
.to_string(),
"TrailingStop(1, Percentage, activation: Some(105))"
);
}
}