use crate::order::{Order, OrderEvent, OrderKey, TimeInForce};
use derive_more::Constructor;
use rust_decimal::Decimal;
use rustrade_instrument::{Side, exchange::ExchangeId, instrument::name::InstrumentNameExchange};
use serde::{Deserialize, Serialize};
use super::{id::StrategyId, state::UnindexedOrderState};
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Constructor)]
pub struct RequestOpenBracket {
pub side: Side,
pub quantity: Decimal,
pub entry_price: Decimal,
pub take_profit_price: Decimal,
pub stop_loss_price: Decimal,
pub stop_loss_limit_price: Option<Decimal>,
pub time_in_force: TimeInForce,
}
pub type BracketOrderRequest<ExchangeKey = ExchangeId, InstrumentKey = InstrumentNameExchange> =
OrderEvent<RequestOpenBracket, ExchangeKey, InstrumentKey>;
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct BracketOrderResult {
pub parent: Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState>,
pub take_profit: Option<Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState>>,
pub stop_loss: Option<Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState>>,
}
impl BracketOrderResult {
pub fn with_all_legs(
parent: Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState>,
take_profit: Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState>,
stop_loss: Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState>,
) -> Self {
Self {
parent,
take_profit: Some(take_profit),
stop_loss: Some(stop_loss),
}
}
pub fn parent_only(
parent: Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState>,
) -> Self {
Self {
parent,
take_profit: None,
stop_loss: None,
}
}
pub fn has_all_legs(&self) -> bool {
self.take_profit.is_some() && self.stop_loss.is_some()
}
pub fn is_failed(&self) -> bool {
self.parent.state.is_failed()
}
}
#[derive(Debug, Clone)]
#[must_use = "builder does nothing unless .build() or .try_build() is called"]
pub struct BracketOrderRequestBuilder<
ExchangeKey = ExchangeId,
InstrumentKey = InstrumentNameExchange,
> {
key: OrderKey<ExchangeKey, InstrumentKey>,
side: Option<Side>,
quantity: Option<Decimal>,
entry_price: Option<Decimal>,
take_profit_price: Option<Decimal>,
stop_loss_price: Option<Decimal>,
stop_loss_limit_price: Option<Decimal>,
time_in_force: TimeInForce,
}
impl<ExchangeKey, InstrumentKey> BracketOrderRequestBuilder<ExchangeKey, InstrumentKey>
where
ExchangeKey: Clone,
InstrumentKey: Clone,
{
pub fn new(
exchange: ExchangeKey,
instrument: InstrumentKey,
strategy: StrategyId,
cid: super::id::ClientOrderId,
) -> Self {
Self {
key: OrderKey::new(exchange, instrument, strategy, cid),
side: None,
quantity: None,
entry_price: None,
take_profit_price: None,
stop_loss_price: None,
stop_loss_limit_price: None,
time_in_force: TimeInForce::GoodUntilCancelled { post_only: false },
}
}
pub fn side(mut self, side: Side) -> Self {
self.side = Some(side);
self
}
pub fn quantity(mut self, quantity: Decimal) -> Self {
self.quantity = Some(quantity);
self
}
pub fn entry_price(mut self, price: Decimal) -> Self {
self.entry_price = Some(price);
self
}
pub fn take_profit_price(mut self, price: Decimal) -> Self {
self.take_profit_price = Some(price);
self
}
pub fn stop_loss_price(mut self, price: Decimal) -> Self {
self.stop_loss_price = Some(price);
self
}
pub fn stop_loss_limit_price(mut self, price: Decimal) -> Self {
self.stop_loss_limit_price = Some(price);
self
}
pub fn time_in_force(mut self, tif: TimeInForce) -> Self {
self.time_in_force = tif;
self
}
#[track_caller]
#[allow(clippy::expect_used)] pub fn build(self) -> BracketOrderRequest<ExchangeKey, InstrumentKey> {
BracketOrderRequest {
key: self.key,
state: RequestOpenBracket {
side: self.side.expect("side is required"),
quantity: self.quantity.expect("quantity is required"),
entry_price: self.entry_price.expect("entry_price is required"),
take_profit_price: self
.take_profit_price
.expect("take_profit_price is required"),
stop_loss_price: self.stop_loss_price.expect("stop_loss_price is required"),
stop_loss_limit_price: self.stop_loss_limit_price,
time_in_force: self.time_in_force,
},
}
}
pub fn try_build(self) -> Option<BracketOrderRequest<ExchangeKey, InstrumentKey>> {
Some(BracketOrderRequest {
key: self.key,
state: RequestOpenBracket {
side: self.side?,
quantity: self.quantity?,
entry_price: self.entry_price?,
take_profit_price: self.take_profit_price?,
stop_loss_price: self.stop_loss_price?,
stop_loss_limit_price: self.stop_loss_limit_price,
time_in_force: self.time_in_force,
},
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::order::id::ClientOrderId;
use rust_decimal_macros::dec;
#[test]
fn test_request_open_bracket_new() {
let req = RequestOpenBracket::new(
Side::Buy,
dec!(100),
dec!(150.00),
dec!(160.00),
dec!(145.00),
None,
TimeInForce::GoodUntilCancelled { post_only: false },
);
assert_eq!(req.side, Side::Buy);
assert_eq!(req.quantity, dec!(100));
assert_eq!(req.entry_price, dec!(150.00));
assert_eq!(req.take_profit_price, dec!(160.00));
assert_eq!(req.stop_loss_price, dec!(145.00));
assert!(req.stop_loss_limit_price.is_none());
}
#[test]
fn test_request_open_bracket_with_stop_limit() {
let req = RequestOpenBracket::new(
Side::Buy,
dec!(100),
dec!(150.00),
dec!(160.00),
dec!(145.00),
Some(dec!(144.00)),
TimeInForce::GoodUntilEndOfDay,
);
assert_eq!(req.stop_loss_limit_price, Some(dec!(144.00)));
}
#[test]
fn test_bracket_order_request_builder() {
let instrument = InstrumentNameExchange::from("AAPL");
let request = BracketOrderRequestBuilder::new(
ExchangeId::AlpacaBroker,
instrument.clone(),
StrategyId::new("test"),
ClientOrderId::new("bracket-001"),
)
.side(Side::Buy)
.quantity(dec!(10))
.entry_price(dec!(150.00))
.take_profit_price(dec!(160.00))
.stop_loss_price(dec!(145.00))
.build();
assert_eq!(request.key.exchange, ExchangeId::AlpacaBroker);
assert_eq!(request.key.instrument, instrument);
assert_eq!(request.state.side, Side::Buy);
assert_eq!(request.state.quantity, dec!(10));
}
#[test]
fn test_bracket_order_request_builder_try_build_missing_field() {
let instrument = InstrumentNameExchange::from("AAPL");
let result = BracketOrderRequestBuilder::new(
ExchangeId::AlpacaBroker,
instrument,
StrategyId::new("test"),
ClientOrderId::new("bracket-001"),
)
.side(Side::Buy)
.quantity(dec!(10))
.take_profit_price(dec!(160.00))
.stop_loss_price(dec!(145.00))
.try_build();
assert!(result.is_none());
}
}