use std::ops::Deref;
use std::ops::Not;
use chrono::DateTime;
use chrono::Utc;
use http::Method;
use http_endpoint::Bytes;
use num_decimal::Num;
use serde::de::IntoDeserializer;
use serde::Deserialize;
use serde::Deserializer;
use serde::Serialize;
use serde_json::from_slice as from_json;
use serde_json::to_vec as to_json;
use serde_urlencoded::to_string as to_query;
use uuid::Uuid;
use crate::api::v2::asset;
use crate::util::vec_from_str;
use crate::Str;
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct Id(pub Uuid);
impl Deref for Id {
type Target = Uuid;
#[inline]
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[non_exhaustive]
pub enum Status {
#[serde(rename = "new")]
New,
#[serde(rename = "replaced")]
Replaced,
#[serde(rename = "partially_filled")]
PartiallyFilled,
#[serde(rename = "filled")]
Filled,
#[serde(rename = "done_for_day")]
DoneForDay,
#[serde(rename = "canceled")]
Canceled,
#[serde(rename = "expired")]
Expired,
#[serde(rename = "accepted")]
Accepted,
#[serde(rename = "pending_new")]
PendingNew,
#[serde(rename = "accepted_for_bidding")]
AcceptedForBidding,
#[serde(rename = "pending_cancel")]
PendingCancel,
#[serde(rename = "pending_replace")]
PendingReplace,
#[serde(rename = "stopped")]
Stopped,
#[serde(rename = "rejected")]
Rejected,
#[serde(rename = "suspended")]
Suspended,
#[serde(rename = "calculated")]
Calculated,
#[serde(rename = "held")]
Held,
#[doc(hidden)]
#[serde(other, rename(serialize = "unknown"))]
Unknown,
}
impl Status {
#[inline]
pub fn is_terminal(self) -> bool {
matches!(
self,
Self::Replaced | Self::Filled | Self::Canceled | Self::Expired | Self::Rejected
)
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum Side {
#[serde(rename = "buy")]
Buy,
#[serde(rename = "sell")]
Sell,
}
impl Not for Side {
type Output = Self;
#[inline]
fn not(self) -> Self::Output {
match self {
Self::Buy => Self::Sell,
Self::Sell => Self::Buy,
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[non_exhaustive]
pub enum Class {
#[serde(rename = "simple")]
Simple,
#[serde(rename = "bracket")]
Bracket,
#[serde(rename = "oco")]
OneCancelsOther,
#[serde(rename = "oto")]
OneTriggersOther,
}
impl Default for Class {
#[inline]
fn default() -> Self {
Self::Simple
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[non_exhaustive]
pub enum Type {
#[serde(rename = "market")]
Market,
#[serde(rename = "limit")]
Limit,
#[serde(rename = "stop")]
Stop,
#[serde(rename = "stop_limit")]
StopLimit,
#[serde(rename = "trailing_stop")]
TrailingStop,
}
impl Default for Type {
#[inline]
fn default() -> Self {
Self::Market
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[non_exhaustive]
pub enum TimeInForce {
#[serde(rename = "day")]
Day,
#[serde(rename = "fok")]
FillOrKill,
#[serde(rename = "ioc")]
ImmediateOrCancel,
#[serde(rename = "gtc")]
UntilCanceled,
#[serde(rename = "opg")]
UntilMarketOpen,
#[serde(rename = "cls")]
UntilMarketClose,
}
impl Default for TimeInForce {
#[inline]
fn default() -> Self {
Self::Day
}
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename = "take_profit")]
struct TakeProfitSerde {
#[serde(rename = "limit_price")]
limit_price: Num,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(from = "TakeProfitSerde", into = "TakeProfitSerde")]
#[non_exhaustive]
pub enum TakeProfit {
Limit(Num),
}
impl From<TakeProfitSerde> for TakeProfit {
fn from(other: TakeProfitSerde) -> Self {
Self::Limit(other.limit_price)
}
}
impl From<TakeProfit> for TakeProfitSerde {
fn from(other: TakeProfit) -> Self {
match other {
TakeProfit::Limit(limit_price) => Self { limit_price },
}
}
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename = "stop_loss")]
struct StopLossSerde {
#[serde(rename = "stop_price")]
stop_price: Num,
#[serde(rename = "limit_price", skip_serializing_if = "Option::is_none")]
limit_price: Option<Num>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(from = "StopLossSerde", into = "StopLossSerde")]
#[non_exhaustive]
pub enum StopLoss {
Stop(Num),
StopLimit(Num, Num),
}
impl From<StopLossSerde> for StopLoss {
fn from(other: StopLossSerde) -> Self {
if let Some(limit_price) = other.limit_price {
Self::StopLimit(other.stop_price, limit_price)
} else {
Self::Stop(other.stop_price)
}
}
}
impl From<StopLoss> for StopLossSerde {
fn from(other: StopLoss) -> Self {
match other {
StopLoss::Stop(stop_price) => Self {
stop_price,
limit_price: None,
},
StopLoss::StopLimit(stop_price, limit_price) => Self {
stop_price,
limit_price: Some(limit_price),
},
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(untagged)]
pub enum Amount {
Quantity {
#[serde(rename = "qty")]
quantity: Num,
},
Notional {
#[serde(rename = "notional")]
notional: Num,
},
}
impl Amount {
#[inline]
pub fn quantity(amount: impl Into<Num>) -> Self {
Self::Quantity {
quantity: amount.into(),
}
}
#[inline]
pub fn notional(amount: impl Into<Num>) -> Self {
Self::Notional {
notional: amount.into(),
}
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct CreateReqInit {
pub class: Class,
pub type_: Type,
pub time_in_force: TimeInForce,
pub limit_price: Option<Num>,
pub stop_price: Option<Num>,
pub trail_price: Option<Num>,
pub trail_percent: Option<Num>,
pub take_profit: Option<TakeProfit>,
pub stop_loss: Option<StopLoss>,
pub extended_hours: bool,
pub client_order_id: Option<String>,
#[doc(hidden)]
pub _non_exhaustive: (),
}
impl CreateReqInit {
pub fn init<S>(self, symbol: S, side: Side, amount: Amount) -> CreateReq
where
S: Into<String>,
{
CreateReq {
symbol: asset::Symbol::Sym(symbol.into()),
amount,
side,
class: self.class,
type_: self.type_,
time_in_force: self.time_in_force,
limit_price: self.limit_price,
stop_price: self.stop_price,
take_profit: self.take_profit,
stop_loss: self.stop_loss,
extended_hours: self.extended_hours,
client_order_id: self.client_order_id,
trail_price: self.trail_price,
trail_percent: self.trail_percent,
_non_exhaustive: (),
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct CreateReq {
#[serde(rename = "symbol")]
pub symbol: asset::Symbol,
#[serde(flatten)]
pub amount: Amount,
#[serde(rename = "side")]
pub side: Side,
#[serde(rename = "order_class")]
pub class: Class,
#[serde(rename = "type")]
pub type_: Type,
#[serde(rename = "time_in_force")]
pub time_in_force: TimeInForce,
#[serde(rename = "limit_price")]
pub limit_price: Option<Num>,
#[serde(rename = "stop_price")]
pub stop_price: Option<Num>,
#[serde(rename = "trail_price")]
pub trail_price: Option<Num>,
#[serde(rename = "trail_percent")]
pub trail_percent: Option<Num>,
#[serde(rename = "take_profit")]
pub take_profit: Option<TakeProfit>,
#[serde(rename = "stop_loss")]
pub stop_loss: Option<StopLoss>,
#[serde(rename = "extended_hours")]
pub extended_hours: bool,
#[serde(rename = "client_order_id")]
pub client_order_id: Option<String>,
#[doc(hidden)]
#[serde(skip)]
pub _non_exhaustive: (),
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub struct ChangeReq {
#[serde(rename = "qty")]
pub quantity: Option<Num>,
#[serde(rename = "time_in_force")]
pub time_in_force: Option<TimeInForce>,
#[serde(rename = "limit_price")]
pub limit_price: Option<Num>,
#[serde(rename = "stop_price")]
pub stop_price: Option<Num>,
#[serde(rename = "trail")]
pub trail: Option<Num>,
#[serde(rename = "client_order_id")]
pub client_order_id: Option<String>,
#[doc(hidden)]
#[serde(skip)]
pub _non_exhaustive: (),
}
fn empty_to_default<'de, D>(deserializer: D) -> Result<Class, D::Error>
where
D: Deserializer<'de>,
{
let class = <&str>::deserialize(deserializer)?;
if class.is_empty() {
Ok(Class::default())
} else {
Class::deserialize(class.into_deserializer())
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct Order {
#[serde(rename = "id")]
pub id: Id,
#[serde(rename = "client_order_id")]
pub client_order_id: String,
#[serde(rename = "status")]
pub status: Status,
#[serde(rename = "created_at")]
pub created_at: DateTime<Utc>,
#[serde(rename = "updated_at")]
pub updated_at: Option<DateTime<Utc>>,
#[serde(rename = "submitted_at")]
pub submitted_at: Option<DateTime<Utc>>,
#[serde(rename = "filled_at")]
pub filled_at: Option<DateTime<Utc>>,
#[serde(rename = "expired_at")]
pub expired_at: Option<DateTime<Utc>>,
#[serde(rename = "canceled_at")]
pub canceled_at: Option<DateTime<Utc>>,
#[serde(rename = "asset_class")]
pub asset_class: asset::Class,
#[serde(rename = "asset_id")]
pub asset_id: asset::Id,
#[serde(rename = "symbol")]
pub symbol: String,
#[serde(flatten)]
pub amount: Amount,
#[serde(rename = "filled_qty")]
pub filled_quantity: Num,
#[serde(rename = "type")]
pub type_: Type,
#[serde(rename = "order_class", deserialize_with = "empty_to_default")]
pub class: Class,
#[serde(rename = "side")]
pub side: Side,
#[serde(rename = "time_in_force")]
pub time_in_force: TimeInForce,
#[serde(rename = "limit_price")]
pub limit_price: Option<Num>,
#[serde(rename = "stop_price")]
pub stop_price: Option<Num>,
#[serde(rename = "trail_price")]
pub trail_price: Option<Num>,
#[serde(rename = "trail_percent")]
pub trail_percent: Option<Num>,
#[serde(rename = "filled_avg_price")]
pub average_fill_price: Option<Num>,
#[serde(rename = "extended_hours")]
pub extended_hours: bool,
#[serde(rename = "legs", deserialize_with = "vec_from_str")]
pub legs: Vec<Order>,
#[doc(hidden)]
#[serde(skip)]
pub _non_exhaustive: (),
}
Endpoint! {
pub Get(Id),
Ok => Order, [
OK,
],
Err => GetError, [
NOT_FOUND => NotFound,
]
fn path(input: &Self::Input) -> Str {
format!("/v2/orders/{}", input.as_simple()).into()
}
}
Endpoint! {
pub GetByClientId(String),
Ok => Order, [
OK,
],
Err => GetByClientIdError, [
NOT_FOUND => NotFound,
]
#[inline]
fn path(_input: &Self::Input) -> Str {
"/v2/orders:by_client_order_id".into()
}
fn query(input: &Self::Input) -> Result<Option<Str>, Self::ConversionError> {
#[derive(Serialize)]
struct ClientOrderId<'s> {
#[serde(rename = "client_order_id")]
order_id: &'s str,
}
let order_id = ClientOrderId {
order_id: input,
};
Ok(Some(to_query(order_id)?.into()))
}
}
Endpoint! {
pub Create(CreateReq),
Ok => Order, [
OK,
],
Err => CreateError, [
UNPROCESSABLE_ENTITY => InvalidInput,
]
#[inline]
fn method() -> Method {
Method::POST
}
#[inline]
fn path(_input: &Self::Input) -> Str {
"/v2/orders".into()
}
fn body(input: &Self::Input) -> Result<Option<Bytes>, Self::ConversionError> {
let json = to_json(input)?;
let bytes = Bytes::from(json);
Ok(Some(bytes))
}
}
Endpoint! {
pub Change((Id, ChangeReq)),
Ok => Order, [
OK,
],
Err => ChangeError, [
NOT_FOUND => NotFound,
UNPROCESSABLE_ENTITY => InvalidInput,
]
#[inline]
fn method() -> Method {
Method::PATCH
}
fn path(input: &Self::Input) -> Str {
let (id, _) = input;
format!("/v2/orders/{}", id.as_simple()).into()
}
fn body(input: &Self::Input) -> Result<Option<Bytes>, Self::ConversionError> {
let (_, request) = input;
let json = to_json(request)?;
let bytes = Bytes::from(json);
Ok(Some(bytes))
}
}
EndpointNoParse! {
pub Delete(Id),
Ok => (), [
NO_CONTENT,
],
Err => DeleteError, [
NOT_FOUND => NotFound,
UNPROCESSABLE_ENTITY => NotCancelable,
]
#[inline]
fn method() -> Method {
Method::DELETE
}
fn path(input: &Self::Input) -> Str {
format!("/v2/orders/{}", input.as_simple()).into()
}
#[inline]
fn parse(body: &[u8]) -> Result<Self::Output, Self::ConversionError> {
debug_assert_eq!(body, b"");
Ok(())
}
fn parse_err(body: &[u8]) -> Result<Self::ApiError, Vec<u8>> {
from_json::<Self::ApiError>(body).map_err(|_| body.to_vec())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr as _;
use futures::TryFutureExt;
use serde_json::from_slice as from_json;
use test_log::test;
use uuid::Uuid;
use crate::api::v2::asset;
use crate::api::v2::asset::Exchange;
use crate::api::v2::asset::Symbol;
use crate::api::v2::order_util::order_aapl;
use crate::api_info::ApiInfo;
use crate::Client;
use crate::RequestError;
#[test]
fn emit_side() {
assert_eq!(to_json(&Side::Buy).unwrap(), br#""buy""#);
assert_eq!(to_json(&Side::Sell).unwrap(), br#""sell""#);
}
#[test]
fn negate_side() {
assert_eq!(!Side::Buy, Side::Sell);
assert_eq!(!Side::Sell, Side::Buy);
}
#[test]
fn emit_type() {
assert_eq!(to_json(&Type::Market).unwrap(), br#""market""#);
assert_eq!(to_json(&Type::Limit).unwrap(), br#""limit""#);
assert_eq!(to_json(&Type::Stop).unwrap(), br#""stop""#);
}
#[test]
fn serialize_deserialize_legs() {
let take_profit = TakeProfit::Limit(Num::new(3, 2));
let json = to_json(&take_profit).unwrap();
assert_eq!(json, br#"{"limit_price":"1.5"}"#);
assert_eq!(from_json::<TakeProfit>(&json).unwrap(), take_profit);
let stop_loss = StopLoss::Stop(Num::from(42));
let json = to_json(&stop_loss).unwrap();
assert_eq!(json, br#"{"stop_price":"42"}"#);
assert_eq!(from_json::<StopLoss>(&json).unwrap(), stop_loss);
let stop_loss = StopLoss::StopLimit(Num::from(13), Num::from(96));
let json = to_json(&stop_loss).unwrap();
let expected = br#"{"stop_price":"13","limit_price":"96"}"#;
assert_eq!(json, &expected[..]);
assert_eq!(from_json::<StopLoss>(&json).unwrap(), stop_loss);
}
#[test]
fn parse_quantity_amount() {
let serialized = br#"{
"qty": "15"
}"#;
let amount = from_json::<Amount>(serialized).unwrap();
assert_eq!(amount, Amount::quantity(15));
}
#[test]
fn parse_notional_amount() {
let serialized = br#"{
"notional": "15.12"
}"#;
let amount = from_json::<Amount>(serialized).unwrap();
assert_eq!(amount, Amount::notional(Num::from_str("15.12").unwrap()));
}
#[test]
fn deserialize_serialize_reference_order() {
let json = br#"{
"id": "904837e3-3b76-47ec-b432-046db621571b",
"client_order_id": "904837e3-3b76-47ec-b432-046db621571b",
"created_at": "2018-10-05T05:48:59Z",
"updated_at": "2018-10-05T05:48:59Z",
"submitted_at": "2018-10-05T05:48:59Z",
"filled_at": "2018-10-05T05:48:59Z",
"expired_at": "2018-10-05T05:48:59Z",
"canceled_at": "2018-10-05T05:48:59Z",
"failed_at": "2018-10-05T05:48:59Z",
"asset_id": "904837e3-3b76-47ec-b432-046db621571b",
"symbol": "AAPL",
"asset_class": "us_equity",
"qty": "15",
"filled_qty": "0",
"type": "market",
"order_class": "oto",
"side": "buy",
"time_in_force": "day",
"limit_price": "107.00",
"stop_price": "106.00",
"filled_avg_price": "106.25",
"status": "accepted",
"extended_hours": false,
"legs": null
}"#;
let id = Id(Uuid::parse_str("904837e3-3b76-47ec-b432-046db621571b").unwrap());
let order = from_json::<Order>(&to_json(&from_json::<Order>(json).unwrap()).unwrap()).unwrap();
assert_eq!(order.id, id);
assert_eq!(
order.created_at,
DateTime::parse_from_rfc3339("2018-10-05T05:48:59Z").unwrap()
);
assert_eq!(order.symbol, "AAPL");
assert_eq!(order.amount, Amount::quantity(15));
assert_eq!(order.type_, Type::Market);
assert_eq!(order.class, Class::OneTriggersOther);
assert_eq!(order.time_in_force, TimeInForce::Day);
assert_eq!(order.limit_price, Some(Num::from(107)));
assert_eq!(order.stop_price, Some(Num::from(106)));
assert_eq!(order.average_fill_price, Some(Num::new(10625, 100)));
}
#[test]
fn deserialize_order_with_empty_order_class() {
let json = br#"{
"id": "904837e3-3b76-47ec-b432-046db621571b",
"client_order_id": "904837e3-3b76-47ec-b432-046db621571b",
"created_at": "2018-10-05T05:48:59Z",
"updated_at": "2018-10-05T05:48:59Z",
"submitted_at": "2018-10-05T05:48:59Z",
"filled_at": "2018-10-05T05:48:59Z",
"expired_at": "2018-10-05T05:48:59Z",
"canceled_at": "2018-10-05T05:48:59Z",
"failed_at": "2018-10-05T05:48:59Z",
"asset_id": "904837e3-3b76-47ec-b432-046db621571b",
"symbol": "AAPL",
"asset_class": "us_equity",
"qty": "15",
"filled_qty": "0",
"type": "market",
"order_class": "",
"side": "buy",
"time_in_force": "day",
"limit_price": "107.00",
"stop_price": "106.00",
"filled_avg_price": "106.25",
"status": "accepted",
"extended_hours": false,
"legs": null
}"#;
let order = from_json::<Order>(json).unwrap();
assert_eq!(order.class, Class::Simple);
}
#[test]
fn serialize_deserialize_order_request() {
let request = CreateReqInit {
type_: Type::TrailingStop,
trail_price: Some(Num::from(50)),
..Default::default()
}
.init("SPY", Side::Buy, Amount::quantity(1));
let json = to_json(&request).unwrap();
assert_eq!(from_json::<CreateReq>(&json).unwrap(), request);
}
#[test]
fn serialize_deserialize_change_request() {
let request = ChangeReq {
quantity: Some(Num::from(37)),
time_in_force: Some(TimeInForce::UntilCanceled),
trail: Some(Num::from(42)),
..Default::default()
};
let json = to_json(&request).unwrap();
assert_eq!(from_json::<ChangeReq>(&json).unwrap(), request);
}
#[test(tokio::test)]
async fn submit_limit_order() {
async fn test(extended_hours: bool) -> Result<(), RequestError<CreateError>> {
let mut request = CreateReqInit {
type_: Type::Limit,
limit_price: Some(Num::from(1)),
extended_hours,
..Default::default()
}
.init("SPY", Side::Buy, Amount::quantity(1));
request.symbol =
Symbol::SymExchgCls("SPY".to_string(), Exchange::Arca, asset::Class::UsEquity);
let api_info = ApiInfo::from_env().unwrap();
let client = Client::new(api_info);
let order = client.issue::<Create>(&request).await?;
client.issue::<Delete>(&order.id).await.unwrap();
assert_eq!(order.symbol, "SPY");
assert_eq!(order.amount, Amount::quantity(1));
assert_eq!(order.side, Side::Buy);
assert_eq!(order.type_, Type::Limit);
assert_eq!(order.class, Class::default());
assert_eq!(order.time_in_force, TimeInForce::Day);
assert_eq!(order.limit_price, Some(Num::from(1)));
assert_eq!(order.stop_price, None);
assert_eq!(order.extended_hours, extended_hours);
Ok(())
}
test(false).await.unwrap();
let result = test(true).await;
match result {
Ok(()) | Err(RequestError::Endpoint(CreateError::NotPermitted(..))) => (),
err => panic!("unexpected error: {err:?}"),
};
}
#[test(tokio::test)]
async fn submit_trailing_stop_price_order() {
let request = CreateReqInit {
type_: Type::TrailingStop,
trail_price: Some(Num::from(50)),
..Default::default()
}
.init("SPY", Side::Buy, Amount::quantity(1));
let api_info = ApiInfo::from_env().unwrap();
let client = Client::new(api_info);
let order = client.issue::<Create>(&request).await.unwrap();
client.issue::<Delete>(&order.id).await.unwrap();
assert_eq!(order.symbol, "SPY");
assert_eq!(order.amount, Amount::quantity(1));
assert_eq!(order.side, Side::Buy);
assert_eq!(order.type_, Type::TrailingStop);
assert_eq!(order.time_in_force, TimeInForce::Day);
assert_eq!(order.limit_price, None);
assert_eq!(order.trail_price, Some(Num::from(50)));
assert_eq!(order.trail_percent, None);
}
#[test(tokio::test)]
async fn submit_trailing_stop_percent_order() {
let request = CreateReqInit {
type_: Type::TrailingStop,
trail_percent: Some(Num::from(10)),
..Default::default()
}
.init("SPY", Side::Buy, Amount::quantity(1));
let api_info = ApiInfo::from_env().unwrap();
let client = Client::new(api_info);
let order = client.issue::<Create>(&request).await.unwrap();
client.issue::<Delete>(&order.id).await.unwrap();
assert_eq!(order.symbol, "SPY");
assert_eq!(order.amount, Amount::quantity(1));
assert_eq!(order.side, Side::Buy);
assert_eq!(order.type_, Type::TrailingStop);
assert_eq!(order.time_in_force, TimeInForce::Day);
assert_eq!(order.limit_price, None);
assert_eq!(order.trail_price, None);
assert_eq!(order.trail_percent, Some(Num::from(10)));
}
#[test(tokio::test)]
async fn submit_bracket_order() {
let request = CreateReqInit {
class: Class::Bracket,
type_: Type::Limit,
limit_price: Some(Num::from(2)),
take_profit: Some(TakeProfit::Limit(Num::from(3))),
stop_loss: Some(StopLoss::Stop(Num::from(1))),
..Default::default()
}
.init("SPY", Side::Buy, Amount::quantity(1));
let api_info = ApiInfo::from_env().unwrap();
let client = Client::new(api_info);
let order = client.issue::<Create>(&request).await.unwrap();
client.issue::<Delete>(&order.id).await.unwrap();
for leg in &order.legs {
client.issue::<Delete>(&leg.id).await.unwrap();
}
assert_eq!(order.symbol, "SPY");
assert_eq!(order.amount, Amount::quantity(1));
assert_eq!(order.side, Side::Buy);
assert_eq!(order.type_, Type::Limit);
assert_eq!(order.class, Class::Bracket);
assert_eq!(order.time_in_force, TimeInForce::Day);
assert_eq!(order.limit_price, Some(Num::from(2)));
assert_eq!(order.stop_price, None);
assert!(!order.extended_hours);
assert_eq!(order.legs.len(), 2);
assert_eq!(order.legs[0].status, Status::Held);
assert_eq!(order.legs[1].status, Status::Held);
}
#[test(tokio::test)]
async fn submit_one_triggers_other_order() {
let request = CreateReqInit {
class: Class::OneTriggersOther,
type_: Type::Limit,
limit_price: Some(Num::from(2)),
stop_loss: Some(StopLoss::Stop(Num::from(1))),
..Default::default()
}
.init("SPY", Side::Buy, Amount::quantity(1));
let api_info = ApiInfo::from_env().unwrap();
let client = Client::new(api_info);
let order = client.issue::<Create>(&request).await.unwrap();
client.issue::<Delete>(&order.id).await.unwrap();
for leg in &order.legs {
client.issue::<Delete>(&leg.id).await.unwrap();
}
assert_eq!(order.symbol, "SPY");
assert_eq!(order.amount, Amount::quantity(1));
assert_eq!(order.side, Side::Buy);
assert_eq!(order.type_, Type::Limit);
assert_eq!(order.class, Class::OneTriggersOther);
assert_eq!(order.time_in_force, TimeInForce::Day);
assert_eq!(order.limit_price, Some(Num::from(2)));
assert_eq!(order.stop_price, None);
assert!(!order.extended_hours);
assert_eq!(order.legs.len(), 1);
assert_eq!(order.legs[0].status, Status::Held);
}
#[test(tokio::test)]
async fn submit_other_order_types() {
async fn test(time_in_force: TimeInForce) {
let api_info = ApiInfo::from_env().unwrap();
let client = Client::new(api_info);
let request = CreateReqInit {
type_: Type::Limit,
class: Class::Simple,
time_in_force,
limit_price: Some(Num::from(1)),
..Default::default()
}
.init("AAPL", Side::Buy, Amount::quantity(1));
match client.issue::<Create>(&request).await {
Ok(order) => {
client.issue::<Delete>(&order.id).await.unwrap();
assert_eq!(order.time_in_force, time_in_force);
},
Err(RequestError::Endpoint(CreateError::NotPermitted(..))) => (),
Err(err) => panic!("Received unexpected error: {err:?}"),
}
}
test(TimeInForce::FillOrKill).await;
test(TimeInForce::ImmediateOrCancel).await;
test(TimeInForce::UntilMarketOpen).await;
test(TimeInForce::UntilMarketClose).await;
}
#[test(tokio::test)]
async fn submit_unsatisfiable_order() {
let api_info = ApiInfo::from_env().unwrap();
let client = Client::new(api_info);
let request = CreateReqInit {
type_: Type::Limit,
limit_price: Some(Num::from(1000)),
..Default::default()
}
.init("AAPL", Side::Buy, Amount::quantity(100_000));
let result = client.issue::<Create>(&request).await;
let err = result.unwrap_err();
match err {
RequestError::Endpoint(CreateError::NotPermitted(..)) => (),
_ => panic!("Received unexpected error: {err:?}"),
};
}
#[test(tokio::test)]
async fn submit_unsatisfiable_notional_order() {
let request =
CreateReqInit::default().init("SPY", Side::Buy, Amount::notional(Num::from(10_000_000)));
let api_info = ApiInfo::from_env().unwrap();
let client = Client::new(api_info);
let result = client.issue::<Create>(&request).await;
let err = result.unwrap_err();
match err {
RequestError::Endpoint(CreateError::NotPermitted(..)) => (),
_ => panic!("Received unexpected error: {err:?}"),
};
}
#[test(tokio::test)]
async fn submit_unsatisfiable_fractional_order() {
let qty = Num::from(1_000_000) + Num::new(1, 2);
let request = CreateReqInit::default().init("SPY", Side::Buy, Amount::quantity(qty));
let api_info = ApiInfo::from_env().unwrap();
let client = Client::new(api_info);
let result = client.issue::<Create>(&request).await;
let err = result.unwrap_err();
match err {
RequestError::Endpoint(CreateError::NotPermitted(..)) => (),
_ => panic!("Received unexpected error: {err:?}"),
};
}
#[test(tokio::test)]
async fn cancel_invalid_order() {
let id = Id(Uuid::parse_str("00000000-0000-0000-0000-000000000000").unwrap());
let api_info = ApiInfo::from_env().unwrap();
let client = Client::new(api_info);
let result = client.issue::<Delete>(&id).await;
let err = result.unwrap_err();
match err {
RequestError::Endpoint(DeleteError::NotFound(..)) => (),
_ => panic!("Received unexpected error: {err:?}"),
};
}
#[test(tokio::test)]
async fn retrieve_order_by_id() {
let api_info = ApiInfo::from_env().unwrap();
let client = Client::new(api_info);
let submitted = order_aapl(&client).await.unwrap();
let result = client.issue::<Get>(&submitted.id).await;
client.issue::<Delete>(&submitted.id).await.unwrap();
let gotten = result.unwrap();
assert_eq!(submitted.id, gotten.id);
assert_eq!(submitted.asset_class, gotten.asset_class);
assert_eq!(submitted.asset_id, gotten.asset_id);
assert_eq!(submitted.symbol, gotten.symbol);
assert_eq!(submitted.amount, gotten.amount);
assert_eq!(submitted.type_, gotten.type_);
assert_eq!(submitted.side, gotten.side);
assert_eq!(submitted.time_in_force, gotten.time_in_force);
}
#[test(tokio::test)]
async fn retrieve_non_existent_order() {
let id = Id(Uuid::parse_str("00000000-0000-0000-0000-000000000000").unwrap());
let api_info = ApiInfo::from_env().unwrap();
let client = Client::new(api_info);
let result = client.issue::<Get>(&id).await;
let err = result.unwrap_err();
match err {
RequestError::Endpoint(GetError::NotFound(..)) => (),
_ => panic!("Received unexpected error: {err:?}"),
};
}
#[test(tokio::test)]
async fn extended_hours_market_order() {
let request = CreateReqInit {
extended_hours: true,
..Default::default()
}
.init("SPY", Side::Buy, Amount::quantity(1));
let api_info = ApiInfo::from_env().unwrap();
let client = Client::new(api_info);
let result = client.issue::<Create>(&request).await;
let err = result.unwrap_err();
match err {
RequestError::Endpoint(CreateError::InvalidInput(..)) => (),
_ => panic!("Received unexpected error: {err:?}"),
};
}
#[test(tokio::test)]
async fn change_order() {
let request = CreateReqInit {
type_: Type::Limit,
limit_price: Some(Num::from(1)),
..Default::default()
}
.init("AAPL", Side::Buy, Amount::quantity(1));
let api_info = ApiInfo::from_env().unwrap();
let client = Client::new(api_info);
let order = client.issue::<Create>(&request).await.unwrap();
let request = ChangeReq {
quantity: Some(Num::from(2)),
time_in_force: Some(TimeInForce::UntilCanceled),
limit_price: Some(Num::from(2)),
..Default::default()
};
let result = client.issue::<Change>(&(order.id, request)).await;
let id = if let Ok(replaced) = &result {
replaced.id
} else {
order.id
};
client.issue::<Delete>(&id).await.unwrap();
match result {
Ok(order) => {
assert_eq!(order.amount, Amount::quantity(2));
assert_eq!(order.time_in_force, TimeInForce::UntilCanceled);
assert_eq!(order.limit_price, Some(Num::from(2)));
assert_eq!(order.stop_price, None);
},
Err(RequestError::Endpoint(ChangeError::InvalidInput(..))) => {
},
e => panic!("received unexpected error: {e:?}"),
}
}
#[test(tokio::test)]
async fn change_trail_stop_order() {
let request = CreateReqInit {
type_: Type::TrailingStop,
trail_price: Some(Num::from(20)),
..Default::default()
}
.init("SPY", Side::Buy, Amount::quantity(1));
let api_info = ApiInfo::from_env().unwrap();
let client = Client::new(api_info);
let order = client.issue::<Create>(&request).await.unwrap();
assert_eq!(order.trail_price, Some(Num::from(20)));
let request = ChangeReq {
trail: Some(Num::from(30)),
..Default::default()
};
let result = client.issue::<Change>(&(order.id, request)).await;
let id = if let Ok(replaced) = &result {
replaced.id
} else {
order.id
};
client.issue::<Delete>(&id).await.unwrap();
match result {
Ok(order) => {
assert_eq!(order.trail_price, Some(Num::from(30)));
},
Err(RequestError::Endpoint(ChangeError::InvalidInput(..))) => (),
e => panic!("received unexpected error: {e:?}"),
}
}
#[test(tokio::test)]
async fn submit_with_client_order_id() {
let client_order_id = Uuid::new_v4().as_simple().to_string();
let request = CreateReqInit {
type_: Type::Limit,
limit_price: Some(Num::from(1)),
client_order_id: Some(client_order_id.clone()),
..Default::default()
}
.init("SPY", Side::Buy, Amount::quantity(1));
let api_info = ApiInfo::from_env().unwrap();
let client = Client::new(api_info);
let (issued, retrieved) = client
.issue::<Create>(&request)
.and_then(|order| async {
let retrieved = client.issue::<GetByClientId>(&client_order_id).await;
client.issue::<Delete>(&order.id).await.unwrap();
Ok((order, retrieved.unwrap()))
})
.await
.unwrap();
assert_eq!(issued.client_order_id, client_order_id);
assert_eq!(retrieved.client_order_id, client_order_id);
assert_eq!(retrieved.id, issued.id);
let err = client.issue::<Create>(&request).await.unwrap_err();
match err {
RequestError::Endpoint(CreateError::InvalidInput(..)) => (),
_ => panic!("Received unexpected error: {err:?}"),
};
}
#[test(tokio::test)]
async fn change_client_order_id() {
let request = CreateReqInit {
type_: Type::Limit,
limit_price: Some(Num::from(1)),
..Default::default()
}
.init("SPY", Side::Buy, Amount::quantity(1));
let api_info = ApiInfo::from_env().unwrap();
let client = Client::new(api_info);
let order = client.issue::<Create>(&request).await.unwrap();
let client_order_id = Uuid::new_v4().as_simple().to_string();
let request = ChangeReq {
client_order_id: Some(client_order_id.clone()),
..Default::default()
};
let change_result = client.issue::<Change>(&(order.id, request)).await;
let id = if let Ok(replaced) = &change_result {
replaced.id
} else {
order.id
};
let get_result = client.issue::<GetByClientId>(&client_order_id).await;
let () = client.issue::<Delete>(&id).await.unwrap();
match change_result {
Ok(..) => {
let order = get_result.unwrap();
assert_eq!(order.symbol, "SPY");
assert_eq!(order.type_, Type::Limit);
assert_eq!(order.limit_price, Some(Num::from(1)));
},
Err(RequestError::Endpoint(ChangeError::InvalidInput(..))) => (),
e => panic!("received unexpected error: {e:?}"),
}
}
}