use chrono::DateTime;
use chrono::NaiveDate;
use chrono::Utc;
use num_decimal::Num;
use serde::Deserialize;
use serde::Deserializer;
use serde::Serialize;
use serde_urlencoded::to_string as to_query;
use crate::api::v2::de::ContentDeserializer;
use crate::api::v2::de::TaggedContentVisitor;
use crate::api::v2::order;
use crate::util::abs_num_from_str;
use crate::util::enum_slice_to_str;
use crate::Str;
fn datetime_from_date_str<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
where
D: Deserializer<'de>,
{
let date = NaiveDate::deserialize(deserializer)?;
Ok(DateTime::from_utc(date.and_hms_opt(0, 0, 0).unwrap(), Utc))
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum ActivityType {
#[serde(rename = "FILL")]
Fill,
#[serde(rename = "TRANS")]
Transaction,
#[serde(rename = "MISC")]
Miscellaneous,
#[serde(rename = "ACATC")]
AcatsInOutCash,
#[serde(rename = "ACATS")]
AcatsInOutSecurities,
#[serde(rename = "CSD")]
CashDeposit,
#[serde(rename = "CSW")]
CashWithdrawal,
#[serde(rename = "DIV")]
Dividend,
#[serde(rename = "DIVCGL")]
CapitalGainLongTerm,
#[serde(rename = "DIVCGS")]
CapitalGainShortTerm,
#[serde(rename = "DIVFEE")]
DividendFee,
#[serde(rename = "DIVFT")]
DividendAdjusted,
#[serde(rename = "DIVNRA")]
DividendAdjustedNraWithheld,
#[serde(rename = "DIVROC")]
DividendReturnOfCapital,
#[serde(rename = "DIVTW")]
DividendAdjustedTefraWithheld,
#[serde(rename = "DIVTXEX")]
DividendTaxExtempt,
#[serde(rename = "INT")]
Interest,
#[serde(rename = "INTNRA")]
InterestAdjustedNraWithheld,
#[serde(rename = "INTTW")]
InterestAdjustedTefraWithheld,
#[serde(rename = "JNL")]
JournalEntry,
#[serde(rename = "JNLC")]
JournalEntryCash,
#[serde(rename = "JNLS")]
JournalEntryStock,
#[serde(rename = "MA")]
Acquisition,
#[serde(rename = "NC")]
NameChange,
#[serde(rename = "OPASN")]
OptionAssignment,
#[serde(rename = "OPEXP")]
OptionExpiration,
#[serde(rename = "OPXRC")]
OptionExercise,
#[serde(rename = "PTC")]
PassThruCharge,
#[serde(rename = "PTR")]
PassThruRebate,
#[serde(rename = "FEE")]
Fee,
#[serde(rename = "REORG")]
Reorg,
#[serde(rename = "SC")]
SymbolChange,
#[serde(rename = "SPIN")]
StockSpinoff,
#[serde(rename = "SPLIT")]
StockSplit,
#[serde(other)]
Unknown,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
pub enum Side {
#[serde(rename = "buy")]
Buy,
#[serde(rename = "sell")]
Sell,
#[serde(rename = "sell_short")]
ShortSell,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[non_exhaustive]
pub struct TradeActivity {
#[serde(rename = "id")]
pub id: String,
#[serde(rename = "transaction_time")]
pub transaction_time: DateTime<Utc>,
#[serde(rename = "symbol")]
pub symbol: String,
#[serde(rename = "order_id")]
pub order_id: order::Id,
#[serde(rename = "side")]
pub side: Side,
#[serde(rename = "qty", deserialize_with = "abs_num_from_str")]
pub quantity: Num,
#[serde(rename = "cum_qty", deserialize_with = "abs_num_from_str")]
pub cumulative_quantity: Num,
#[serde(rename = "leaves_qty", deserialize_with = "abs_num_from_str")]
pub unfilled_quantity: Num,
#[serde(rename = "price")]
pub price: Num,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[non_exhaustive]
pub struct NonTradeActivityImpl<T> {
#[serde(rename = "id")]
pub id: String,
#[serde(rename = "activity_type")]
pub type_: T,
#[serde(rename = "date", deserialize_with = "datetime_from_date_str")]
pub date: DateTime<Utc>,
#[serde(rename = "net_amount")]
pub net_amount: Num,
#[serde(rename = "symbol")]
pub symbol: Option<String>,
#[serde(rename = "qty")]
pub quantity: Option<Num>,
#[serde(rename = "price")]
pub price: Option<Num>,
#[serde(rename = "per_share_amount")]
pub per_share_amount: Option<Num>,
#[serde(rename = "description")]
pub description: Option<String>,
}
impl<T> NonTradeActivityImpl<T> {
fn into_other<U>(self, activity_type: U) -> NonTradeActivityImpl<U> {
let Self {
id,
date,
net_amount,
symbol,
quantity,
price,
per_share_amount,
description,
..
} = self;
NonTradeActivityImpl::<U> {
id,
type_: activity_type,
date,
net_amount,
symbol,
quantity,
price,
per_share_amount,
description,
}
}
}
pub type NonTradeActivity = NonTradeActivityImpl<ActivityType>;
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Activity {
Trade(TradeActivity),
NonTrade(NonTradeActivity),
}
impl Activity {
#[inline]
pub fn id(&self) -> &str {
match self {
Activity::Trade(trade) => &trade.id,
Activity::NonTrade(non_trade) => &non_trade.id,
}
}
#[inline]
pub fn time(&self) -> &DateTime<Utc> {
match self {
Activity::Trade(trade) => &trade.transaction_time,
Activity::NonTrade(non_trade) => &non_trade.date,
}
}
#[allow(clippy::result_large_err)]
pub fn into_trade(self) -> Result<TradeActivity, Self> {
match self {
Activity::Trade(trade) => Ok(trade),
Activity::NonTrade(..) => Err(self),
}
}
#[allow(clippy::result_large_err)]
pub fn into_non_trade(self) -> Result<NonTradeActivity, Self> {
match self {
Activity::Trade(..) => Err(self),
Activity::NonTrade(non_trade) => Ok(non_trade),
}
}
}
impl<'de> Deserialize<'de> for Activity {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let tagged = match Deserializer::deserialize_any(
deserializer,
TaggedContentVisitor::<ActivityType>::new("activity_type"),
) {
Ok(val) => val,
Err(err) => return Err(err),
};
let content = ContentDeserializer::new(tagged.content);
match tagged.tag {
ActivityType::Fill => TradeActivity::deserialize(content).map(Activity::Trade),
activity_type => NonTradeActivityImpl::<Option<()>>::deserialize(content)
.map(|non_trade| non_trade.into_other::<ActivityType>(activity_type))
.map(Activity::NonTrade),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
pub enum Direction {
#[serde(rename = "desc")]
Descending,
#[serde(rename = "asc")]
Ascending,
}
impl Default for Direction {
#[inline]
fn default() -> Self {
Self::Descending
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)]
pub struct ActivityReq {
#[serde(rename = "activity_types", serialize_with = "enum_slice_to_str")]
pub types: Vec<ActivityType>,
#[serde(rename = "direction")]
pub direction: Direction,
#[serde(rename = "until")]
pub until: Option<DateTime<Utc>>,
#[serde(rename = "after")]
pub after: Option<DateTime<Utc>>,
#[serde(rename = "page_size")]
pub page_size: Option<usize>,
#[serde(rename = "page_token")]
pub page_token: Option<String>,
}
Endpoint! {
pub Get(ActivityReq),
Ok => Vec<Activity>, [
OK,
],
Err => GetError, []
#[inline]
fn path(_input: &Self::Input) -> Str {
"/v2/account/activities".into()
}
fn query(input: &Self::Input) -> Result<Option<Str>, Self::ConversionError> {
Ok(Some(to_query(input)?.into()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Duration;
use serde_json::from_str as from_json;
use test_log::test;
use uuid::Uuid;
use crate::api_info::ApiInfo;
use crate::Client;
#[test]
fn parse_reference_trade_activity() {
let response = r#"{
"activity_type": "FILL",
"cum_qty": "1.5",
"id": "20190524113406977::8efc7b9a-8b2b-4000-9955-d36e7db0df74",
"leaves_qty": "0",
"price": "1.63",
"qty": "1",
"side": "buy",
"symbol": "LPCN",
"transaction_time": "2019-05-24T15:34:06.977Z",
"order_id": "904837e3-3b76-47ec-b432-046db621571b",
"type": "fill"
}"#;
let trade = from_json::<Activity>(response)
.unwrap()
.into_trade()
.unwrap();
let id = order::Id(Uuid::parse_str("904837e3-3b76-47ec-b432-046db621571b").unwrap());
assert_eq!(trade.symbol, "LPCN");
assert_eq!(trade.order_id, id);
assert_eq!(trade.side, Side::Buy);
assert_eq!(trade.quantity, Num::from(1));
assert_eq!(trade.cumulative_quantity, Num::new(3, 2));
assert_eq!(trade.unfilled_quantity, Num::from(0));
assert_eq!(trade.price, Num::new(163, 100));
}
#[test]
fn parse_reference_non_trade_activity() {
let response = r#"{
"activity_type": "DIV",
"id": "20190801011955195::5f596936-6f23-4cef-bdf1-3806aae57dbf",
"date": "2019-08-01",
"net_amount": "1.02",
"symbol": "T",
"per_share_amount": "0.51"
}"#;
let non_trade = from_json::<Activity>(response)
.unwrap()
.into_non_trade()
.unwrap();
assert_eq!(non_trade.type_, ActivityType::Dividend);
assert_eq!(
non_trade.date.naive_utc().date(),
NaiveDate::from_ymd_opt(2019, 8, 1).unwrap()
);
assert_eq!(non_trade.symbol, Some("T".into()));
assert_eq!(non_trade.per_share_amount, Some(Num::new(51, 100)));
}
#[test]
fn parse_dividend() {
let response = r#"{
"id":"20200626000000000::e3163618-f82b-4568-af54-b30404484224",
"activity_type":"DIV",
"date":"2020-01-01",
"net_amount":"21.97",
"description":"DIV",
"symbol":"SPY",
"qty":"201.9617035750071243",
"per_share_amount":"0.108783"
}"#;
let non_trade = from_json::<Activity>(response)
.unwrap()
.into_non_trade()
.unwrap();
assert_eq!(non_trade.type_, ActivityType::Dividend);
assert_eq!(
non_trade.date.naive_utc().date(),
NaiveDate::from_ymd_opt(2020, 1, 1).unwrap()
);
assert_eq!(non_trade.symbol, Some("SPY".into()));
assert_eq!(
non_trade.quantity,
Some(Num::new(2019617035750071243u64, 10000000000000000u64))
);
assert_eq!(non_trade.per_share_amount, Some(Num::new(108783, 1000000)));
}
#[test(tokio::test)]
async fn retrieve_some_activities() {
let api_info = ApiInfo::from_env().unwrap();
let client = Client::new(api_info);
let request = ActivityReq {
types: vec![
ActivityType::Fill,
ActivityType::Transaction,
ActivityType::Dividend,
],
..Default::default()
};
let activities = client.issue::<Get>(&request).await.unwrap();
assert!(!activities.is_empty());
for activity in activities {
match activity {
Activity::Trade(..) => (),
Activity::NonTrade(non_trade) => {
assert!(
non_trade.type_ == ActivityType::Transaction
|| non_trade.type_ == ActivityType::Dividend
);
},
}
}
}
#[test(tokio::test)]
async fn retrieve_trade_activities() {
let api_info = ApiInfo::from_env().unwrap();
let client = Client::new(api_info);
let request = ActivityReq {
types: vec![ActivityType::Fill],
..Default::default()
};
let activities = client.issue::<Get>(&request).await.unwrap();
assert!(!activities.is_empty());
for activity in activities {
match activity {
Activity::Trade(..) => (),
Activity::NonTrade(non_trade) => {
panic!("received unexpected non-trade variant {non_trade:?}")
},
}
}
}
#[test(tokio::test)]
async fn retrieve_all_activities() {
let api_info = ApiInfo::from_env().unwrap();
let client = Client::new(api_info);
let request = ActivityReq {
direction: Direction::Ascending,
..Default::default()
};
let activities = client.issue::<Get>(&request).await.unwrap();
assert!(!activities.is_empty());
let mut iter = activities.iter();
let mut time = iter.next().unwrap().time();
for activity in iter {
assert!(time <= activity.time());
time = activity.time();
}
}
#[test(tokio::test)]
async fn page_activities() {
let api_info = ApiInfo::from_env().unwrap();
let client = Client::new(api_info);
let mut request = ActivityReq {
page_size: Some(1),
..Default::default()
};
let activities = client.issue::<Get>(&request).await.unwrap();
assert_eq!(activities.len(), 1);
let newest_activity = &activities[0];
request.page_token = Some(newest_activity.id().to_string());
let activities = client.issue::<Get>(&request).await.unwrap();
assert_eq!(activities.len(), 1);
let next_activity = &activities[0];
assert!(newest_activity.time() >= next_activity.time());
assert_ne!(newest_activity.id(), next_activity.id());
}
#[test(tokio::test)]
async fn retrieve_after() {
let api_info = ApiInfo::from_env().unwrap();
let client = Client::new(api_info);
let mut request = ActivityReq {
direction: Direction::Ascending,
page_size: Some(1),
..Default::default()
};
let activities = client.issue::<Get>(&request).await.unwrap();
assert_eq!(activities.len(), 1);
let time = activities[0].time();
request.after = Some(*time + Duration::microseconds(1));
let activities = client.issue::<Get>(&request).await.unwrap();
assert_eq!(activities.len(), 1);
assert!(activities[0].time() > time);
}
#[test(tokio::test)]
async fn retrieve_until() {
let api_info = ApiInfo::from_env().unwrap();
let client = Client::new(api_info);
let mut request = ActivityReq {
direction: Direction::Ascending,
page_size: Some(2),
..Default::default()
};
let activities = client.issue::<Get>(&request).await.unwrap();
assert_eq!(activities.len(), 2);
let time = activities[1].time();
request.until = Some(*time - Duration::microseconds(1));
let activities = client.issue::<Get>(&request).await.unwrap();
assert!(activities.len() <= 2);
assert!(activities[0].time() < time);
}
}