use std::collections::HashMap;
use chrono::{DateTime, NaiveDate, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::trading::Position;
use crate::types::serde_util::comma_separated;
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct EodPositions {
#[serde(default)]
pub asof: Option<NaiveDate>,
#[serde(
default,
deserialize_with = "crate::types::serde_util::null_as_default"
)]
pub positions: HashMap<String, Vec<Position>>,
#[serde(default)]
pub next_page_token: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct AggregatePosition {
#[serde(default)]
pub symbol: Option<String>,
#[serde(default)]
pub cusip: Option<String>,
#[serde(default)]
pub asset_type: Option<String>,
#[serde(default)]
pub num_accounts: Option<i64>,
#[serde(default, with = "crate::types::option_decimal")]
pub long_qty: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub long_market_value: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub short_qty: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub short_market_value: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub closing_price: Option<Decimal>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CashInterestDetail {
#[serde(default)]
pub account_id: Option<Uuid>,
#[serde(default)]
pub date: Option<NaiveDate>,
#[serde(default)]
pub currency: Option<crate::types::SupportedCurrencies>,
#[serde(default, with = "crate::types::option_decimal")]
pub cash_balance: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub account_accrued_interest: Option<Decimal>,
#[serde(default)]
pub account_rate_bps: Option<i64>,
#[serde(default, with = "crate::types::option_decimal")]
pub correspondent_fee: Option<Decimal>,
#[serde(default)]
pub correspondent_rate_bps: Option<i64>,
#[serde(default)]
pub apr_tier_id: Option<Uuid>,
#[serde(default)]
pub apr_tier_name: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CashInterestReport {
#[serde(
default,
deserialize_with = "crate::types::serde_util::null_as_default"
)]
pub interest: Vec<CashInterestDetail>,
#[serde(default)]
pub next_page_token: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct AprTierDetails {
#[serde(default)]
pub as_of: Option<NaiveDate>,
#[serde(default)]
pub total_accounts: Option<i64>,
#[serde(default, with = "crate::types::option_decimal")]
pub total_balance: Option<Decimal>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct AprTier {
#[serde(default)]
pub id: Option<Uuid>,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub currency: Option<crate::types::SupportedCurrencies>,
#[serde(default)]
pub account_rate_bps: Option<i64>,
#[serde(default)]
pub correspondent_rate_bps: Option<i64>,
#[serde(default)]
pub is_default: Option<bool>,
#[serde(default)]
pub details: Option<AprTierDetails>,
#[serde(default)]
pub created_at: Option<DateTime<Utc>>,
#[serde(default)]
pub updated_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct AprTiers {
#[serde(
default,
deserialize_with = "crate::types::serde_util::null_as_default"
)]
pub apr_tiers: Vec<AprTier>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct GetEodPositionsRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub account_id: Option<Uuid>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub asset: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub asof: Option<NaiveDate>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub page_token: Option<String>,
}
impl GetEodPositionsRequest {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn asof(mut self, asof: NaiveDate) -> Self {
self.asof = Some(asof);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct GetAggregatePositionsRequest {
pub date: NaiveDate,
#[serde(
default,
skip_serializing_if = "Option::is_none",
serialize_with = "comma_separated"
)]
pub symbols: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub firm_accounts: Option<bool>,
}
impl GetAggregatePositionsRequest {
#[must_use]
pub fn new(date: NaiveDate) -> Self {
Self {
date,
symbols: None,
firm_accounts: None,
}
}
#[must_use]
pub fn symbols(mut self, symbols: Vec<String>) -> Self {
self.symbols = Some(symbols);
self
}
#[must_use]
pub fn firm_accounts(mut self, include: bool) -> Self {
self.firm_accounts = Some(include);
self
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct GetCashInterestRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub account_id: Option<Uuid>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub date: Option<NaiveDate>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub after: Option<NaiveDate>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub before: Option<NaiveDate>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub direction: Option<crate::types::Sort>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub page_size: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub page_token: Option<String>,
}
impl GetCashInterestRequest {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn account_id(mut self, account_id: Uuid) -> Self {
self.account_id = Some(account_id);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn eod_positions_are_keyed_by_account_and_read_as_trading_positions() {
let report: EodPositions = serde_json::from_value(serde_json::json!({
"asof": "2026-01-02",
"next_page_token": null,
"positions": {
"550e8400-e29b-41d4-a716-446655440000": [{
"asset_id": "550e8400-e29b-41d4-a716-446655440001",
"symbol": "AAPL",
"exchange": "NASDAQ",
"asset_class": "us_equity",
"avg_entry_price": "180.00",
"qty": "10",
"qty_available": "10",
"side": "long",
"market_value": "1850.00",
"cost_basis": "1800.00",
"unrealized_pl": "50.00",
"unrealized_plpc": "0.0277",
"unrealized_intraday_pl": "5.00",
"unrealized_intraday_plpc": "0.0027",
"current_price": "185.00",
"lastday_price": "184.50",
"change_today": "0.0027",
}],
},
}))
.unwrap();
assert_eq!(report.positions.len(), 1);
assert_eq!(
report.positions["550e8400-e29b-41d4-a716-446655440000"][0].symbol,
"AAPL"
);
}
#[test]
fn the_aggregate_report_requires_a_date() {
let request = GetAggregatePositionsRequest::new("2026-01-02".parse().unwrap());
let json = serde_json::to_value(&request).unwrap();
assert_eq!(json["date"], "2026-01-02");
}
}