use std::ops::Not;
use http::Method;
use num_decimal::Num;
use serde::Deserialize;
use serde::Serialize;
use crate::api::v2::asset;
use crate::api::v2::order;
use crate::util::abs_num_from_str;
use crate::Str;
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum Side {
#[serde(rename = "long")]
Long,
#[serde(rename = "short")]
Short,
}
impl Not for Side {
type Output = Self;
#[inline]
fn not(self) -> Self::Output {
match self {
Self::Long => Self::Short,
Self::Short => Self::Long,
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct Position {
#[serde(rename = "asset_id")]
pub asset_id: asset::Id,
#[serde(rename = "symbol")]
pub symbol: String,
#[serde(rename = "exchange")]
pub exchange: asset::Exchange,
#[serde(rename = "asset_class")]
pub asset_class: asset::Class,
#[serde(rename = "avg_entry_price")]
pub average_entry_price: Num,
#[serde(rename = "qty", deserialize_with = "abs_num_from_str")]
pub quantity: Num,
#[serde(rename = "qty_available")]
pub quantity_available: Num,
#[serde(rename = "side")]
pub side: Side,
#[serde(rename = "market_value")]
pub market_value: Option<Num>,
#[serde(rename = "cost_basis")]
pub cost_basis: Num,
#[serde(rename = "unrealized_pl")]
pub unrealized_gain_total: Option<Num>,
#[serde(rename = "unrealized_plpc")]
pub unrealized_gain_total_percent: Option<Num>,
#[serde(rename = "unrealized_intraday_pl")]
pub unrealized_gain_today: Option<Num>,
#[serde(rename = "unrealized_intraday_plpc")]
pub unrealized_gain_today_percent: Option<Num>,
#[serde(rename = "current_price")]
pub current_price: Option<Num>,
#[serde(rename = "lastday_price")]
pub last_day_price: Option<Num>,
#[serde(rename = "change_today")]
pub change_today: Option<Num>,
#[doc(hidden)]
#[serde(skip)]
pub _non_exhaustive: (),
}
Endpoint! {
pub Get(asset::Symbol),
Ok => Position, [
OK,
],
Err => GetError, [
NOT_FOUND => NotFound,
]
#[inline]
fn path(input: &Self::Input) -> Str {
format!("/v2/positions/{input}").into()
}
}
Endpoint! {
pub Delete(asset::Symbol),
Ok => order::Order, [
OK,
],
Err => DeleteError, [
NOT_FOUND => NotFound,
]
#[inline]
fn method() -> Method {
Method::DELETE
}
#[inline]
fn path(input: &Self::Input) -> Str {
format!("/v2/positions/{input}").into()
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::from_str as from_json;
use serde_json::to_string as to_json;
use test_log::test;
use crate::api_info::ApiInfo;
use crate::Client;
use crate::RequestError;
#[test]
fn negate_side() {
assert_eq!(!Side::Long, Side::Short);
assert_eq!(!Side::Short, Side::Long);
}
#[test]
fn deserialize_serialize_reference_position() {
let json = r#"{
"asset_id": "904837e3-3b76-47ec-b432-046db621571b",
"symbol": "AAPL",
"exchange": "NASDAQ",
"asset_class": "us_equity",
"avg_entry_price": "100.0",
"qty": "5",
"qty_available": "3",
"side": "long",
"market_value": "600.0",
"cost_basis": "500.0",
"unrealized_pl": "100.0",
"unrealized_plpc": "0.20",
"unrealized_intraday_pl": "10.0",
"unrealized_intraday_plpc": "0.0084",
"current_price": "120.0",
"lastday_price": "119.0",
"change_today": "0.0084"
}"#;
let pos =
from_json::<Position>(&to_json(&from_json::<Position>(json).unwrap()).unwrap()).unwrap();
assert_eq!(pos.symbol, "AAPL");
assert_eq!(pos.exchange, asset::Exchange::Nasdaq);
assert_eq!(pos.asset_class, asset::Class::UsEquity);
assert_eq!(pos.average_entry_price, Num::from(100));
assert_eq!(pos.quantity, Num::from(5));
assert_eq!(pos.quantity_available, Num::from(3));
assert_eq!(pos.side, Side::Long);
assert_eq!(pos.market_value, Some(Num::from(600)));
assert_eq!(pos.cost_basis, Num::from(500));
assert_eq!(pos.unrealized_gain_total, Some(Num::from(100)));
assert_eq!(pos.unrealized_gain_total_percent, Some(Num::new(20, 100)));
assert_eq!(pos.unrealized_gain_today, Some(Num::from(10)));
assert_eq!(pos.unrealized_gain_today_percent, Some(Num::new(84, 10000)));
assert_eq!(pos.current_price, Some(Num::from(120)));
assert_eq!(pos.last_day_price, Some(Num::from(119)));
assert_eq!(pos.change_today, Some(Num::new(84, 10000)));
}
#[test]
fn parse_fractional_position() {
let response = r#"{
"asset_id": "904837e3-3b76-47ec-b432-046db621571b",
"symbol": "AAPL",
"exchange": "NASDAQ",
"asset_class": "us_equity",
"avg_entry_price": "100.0",
"qty": "0.5",
"qty_available": "0.5",
"side": "long",
"market_value": "600.0",
"cost_basis": "500.0",
"unrealized_pl": "100.0",
"unrealized_plpc": "0.20",
"unrealized_intraday_pl": "10.0",
"unrealized_intraday_plpc": "0.0084",
"current_price": "120.0",
"lastday_price": "119.0",
"change_today": "0.0084"
}"#;
let pos = from_json::<Position>(response).unwrap();
assert_eq!(pos.symbol, "AAPL");
assert_eq!(pos.exchange, asset::Exchange::Nasdaq);
assert_eq!(pos.asset_class, asset::Class::UsEquity);
assert_eq!(pos.average_entry_price, Num::from(100));
assert_eq!(pos.quantity, Num::new(1, 2));
assert_eq!(pos.quantity_available, Num::new(1, 2));
assert_eq!(pos.side, Side::Long);
assert_eq!(pos.market_value, Some(Num::from(600)));
assert_eq!(pos.cost_basis, Num::from(500));
assert_eq!(pos.unrealized_gain_total, Some(Num::from(100)));
assert_eq!(pos.unrealized_gain_total_percent, Some(Num::new(20, 100)));
assert_eq!(pos.unrealized_gain_today, Some(Num::from(10)));
assert_eq!(pos.unrealized_gain_today_percent, Some(Num::new(84, 10000)));
assert_eq!(pos.current_price, Some(Num::from(120)));
assert_eq!(pos.last_day_price, Some(Num::from(119)));
assert_eq!(pos.change_today, Some(Num::new(84, 10000)));
}
#[test]
fn parse_short_position() {
let response = r#"{
"asset_id":"d704f4fd-c735-44f8-a7fa-7a50fef08fe4",
"symbol":"XLK",
"exchange":"ARCA",
"asset_class":"us_equity",
"qty":"-24",
"qty_available": "-24",
"avg_entry_price":"82.69",
"side":"short",
"market_value":"-2011.44",
"cost_basis":"-1984.56",
"unrealized_pl":"-26.88",
"unrealized_plpc":"-0.0135445640343451",
"unrealized_intraday_pl":"-26.88",
"unrealized_intraday_plpc":"-0.0135445640343451",
"current_price":"83.81",
"lastday_price":"88.91",
"change_today":"-0.0573613766730402"
}"#;
let pos = from_json::<Position>(response).unwrap();
assert_eq!(pos.symbol, "XLK");
assert_eq!(pos.quantity, Num::from(24));
assert_eq!(pos.quantity_available, Num::from(-24));
}
#[test(tokio::test)]
async fn retrieve_position() {
let api_info = ApiInfo::from_env().unwrap();
let client = Client::new(api_info);
let symbol = asset::Symbol::Sym("SPY".to_string());
let result = client.issue::<Get>(&symbol).await;
match result {
Ok(pos) => {
assert_eq!(pos.symbol, "SPY");
assert_eq!(pos.asset_class, asset::Class::UsEquity);
},
Err(err) => match err {
RequestError::Endpoint(GetError::NotFound(..)) => (),
_ => panic!("Received unexpected error: {err:?}"),
},
}
}
#[test(tokio::test)]
async fn delete_non_existent_position() {
let api_info = ApiInfo::from_env().unwrap();
let client = Client::new(api_info);
for symbol in ["TSLA", "SPY", "XLK"] {
let symbol = asset::Symbol::Sym(symbol.to_string());
if client.issue::<Get>(&symbol).await.is_ok() {
continue
}
let result = client.issue::<Delete>(&symbol).await;
match result {
Err(RequestError::Endpoint(DeleteError::NotFound(..))) => (),
_ => panic!("Received unexpected result: {:?}", result),
}
}
}
}