use super::order::{OrderId, PriceEffect, Symbol};
use crate::accounts::AccountNumber;
use crate::api::quote_streaming::DxFeedSymbol;
use crate::types::instrument::InstrumentType;
use chrono::{DateTime, FixedOffset, NaiveDate};
use pretty_simple_display::{DebugPretty, DisplaySimple};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use std::fmt::Display;
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
pub enum QuantityDirection {
Long,
Short,
Zero,
}
impl Display for QuantityDirection {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
QuantityDirection::Long => write!(f, "Long"),
QuantityDirection::Short => write!(f, "Short"),
QuantityDirection::Zero => write!(f, "Zero"),
}
}
}
#[derive(DebugPretty, DisplaySimple, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct FullPosition {
pub account_number: AccountNumber,
pub symbol: Symbol,
pub instrument_type: InstrumentType,
pub underlying_symbol: Symbol,
#[serde(with = "rust_decimal::serde::arbitrary_precision")]
pub quantity: Decimal,
pub quantity_direction: QuantityDirection,
#[serde(with = "rust_decimal::serde::arbitrary_precision")]
pub close_price: Decimal,
#[serde(with = "rust_decimal::serde::arbitrary_precision")]
pub average_open_price: Decimal,
#[serde(with = "rust_decimal::serde::arbitrary_precision")]
pub average_yearly_market_close_price: Decimal,
#[serde(with = "rust_decimal::serde::arbitrary_precision")]
pub average_daily_market_close_price: Decimal,
#[serde(with = "crate::types::wire::decimal")]
pub multiplier: Decimal,
pub cost_effect: PriceEffect,
pub is_suppressed: bool,
pub is_frozen: bool,
#[serde(with = "rust_decimal::serde::arbitrary_precision")]
pub restricted_quantity: Decimal,
#[serde(with = "rust_decimal::serde::arbitrary_precision")]
pub realized_day_gain: Decimal,
pub realized_day_gain_effect: String,
#[serde(with = "crate::types::wire::date")]
pub realized_day_gain_date: NaiveDate,
#[serde(with = "rust_decimal::serde::arbitrary_precision")]
pub realized_today: Decimal,
pub realized_today_effect: String,
#[serde(with = "crate::types::wire::date")]
pub realized_today_date: NaiveDate,
#[serde(with = "crate::types::wire::datetime")]
pub created_at: DateTime<FixedOffset>,
#[serde(with = "crate::types::wire::datetime")]
pub updated_at: DateTime<FixedOffset>,
#[serde(default, with = "crate::types::wire::decimal_option")]
pub mark: Option<Decimal>,
#[serde(default, with = "crate::types::wire::decimal_option")]
pub mark_price: Option<Decimal>,
#[serde(default)]
pub deliverable_type: Option<String>,
#[serde(default, with = "crate::types::wire::datetime_option")]
pub expires_at: Option<DateTime<FixedOffset>>,
#[serde(default, with = "crate::types::wire::decimal_option")]
pub face_value: Option<Decimal>,
#[serde(default, with = "crate::types::wire::decimal_option")]
pub fixing_price: Option<Decimal>,
#[serde(default, with = "crate::types::wire::decimal_option")]
pub par_size: Option<Decimal>,
#[serde(default)]
pub order_id: Option<OrderId>,
#[serde(default)]
pub streamer_symbol: Option<String>,
#[serde(default)]
pub update_type: Option<String>,
}
#[derive(DebugPretty, DisplaySimple, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct BriefPosition {
pub account_number: AccountNumber,
pub symbol: Symbol,
pub instrument_type: InstrumentType,
pub underlying_symbol: Symbol,
#[serde(with = "rust_decimal::serde::arbitrary_precision")]
pub quantity: Decimal,
pub quantity_direction: QuantityDirection,
#[serde(with = "rust_decimal::serde::arbitrary_precision")]
pub close_price: Decimal,
#[serde(with = "rust_decimal::serde::arbitrary_precision")]
pub average_open_price: Decimal,
#[serde(with = "crate::types::wire::decimal")]
pub multiplier: Decimal,
pub cost_effect: PriceEffect,
pub is_suppressed: bool,
pub is_frozen: bool,
#[serde(with = "crate::types::wire::decimal")]
pub restricted_quantity: Decimal,
#[serde(with = "rust_decimal::serde::arbitrary_precision")]
pub realized_day_gain: Decimal,
#[serde(with = "rust_decimal::serde::arbitrary_precision")]
pub realized_today: Decimal,
#[serde(with = "crate::types::wire::datetime")]
pub created_at: DateTime<FixedOffset>,
#[serde(default)]
pub streamer_symbol: Option<DxFeedSymbol>,
#[serde(default, with = "crate::types::wire::decimal_option")]
pub average_daily_market_close_price: Option<Decimal>,
#[serde(default, with = "crate::types::wire::decimal_option")]
pub average_yearly_market_close_price: Option<Decimal>,
#[serde(default, with = "crate::types::wire::decimal_option")]
pub mark: Option<Decimal>,
#[serde(default, with = "crate::types::wire::decimal_option")]
pub mark_price: Option<Decimal>,
#[serde(default, with = "crate::types::wire::decimal_option")]
pub face_value: Option<Decimal>,
#[serde(default, with = "crate::types::wire::decimal_option")]
pub fixing_price: Option<Decimal>,
#[serde(default, with = "crate::types::wire::decimal_option")]
pub par_size: Option<Decimal>,
#[serde(default)]
pub deliverable_type: Option<String>,
#[serde(default)]
pub update_type: Option<String>,
#[serde(default)]
pub order_id: Option<OrderId>,
#[serde(default, with = "crate::types::wire::datetime_option")]
pub expires_at: Option<DateTime<FixedOffset>>,
#[serde(default)]
pub realized_day_gain_effect: Option<PriceEffect>,
#[serde(default, with = "crate::types::wire::date_option")]
pub realized_day_gain_date: Option<NaiveDate>,
#[serde(default)]
pub realized_today_effect: Option<PriceEffect>,
#[serde(default, with = "crate::types::wire::date_option")]
pub realized_today_date: Option<NaiveDate>,
#[serde(with = "crate::types::wire::datetime")]
pub updated_at: DateTime<FixedOffset>,
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal::Decimal;
use std::str::FromStr;
#[test]
fn test_quantity_direction_display() {
assert_eq!(format!("{}", QuantityDirection::Long), "Long");
assert_eq!(format!("{}", QuantityDirection::Short), "Short");
assert_eq!(format!("{}", QuantityDirection::Zero), "Zero");
}
#[test]
fn test_quantity_direction_serialization() {
let long = QuantityDirection::Long;
let serialized = serde_json::to_string(&long).unwrap();
assert_eq!(serialized, "\"Long\"");
let short = QuantityDirection::Short;
let serialized = serde_json::to_string(&short).unwrap();
assert_eq!(serialized, "\"Short\"");
let zero = QuantityDirection::Zero;
let serialized = serde_json::to_string(&zero).unwrap();
assert_eq!(serialized, "\"Zero\"");
}
#[test]
fn test_quantity_direction_deserialization() {
let long: QuantityDirection = serde_json::from_str("\"Long\"").unwrap();
matches!(long, QuantityDirection::Long);
let short: QuantityDirection = serde_json::from_str("\"Short\"").unwrap();
matches!(short, QuantityDirection::Short);
let zero: QuantityDirection = serde_json::from_str("\"Zero\"").unwrap();
matches!(zero, QuantityDirection::Zero);
}
#[test]
fn test_quantity_direction_clone_and_copy() {
let original = QuantityDirection::Long;
let cloned = original;
let copied = original;
matches!(cloned, QuantityDirection::Long);
matches!(copied, QuantityDirection::Long);
}
#[test]
fn test_quantity_direction_debug() {
let long = QuantityDirection::Long;
let debug_str = format!("{:?}", long);
assert_eq!(debug_str, "Long");
}
#[test]
fn test_full_position_debug() {
let json = r#"{
"account-number": "TEST123",
"symbol": "AAPL",
"instrument-type": "Equity",
"underlying-symbol": "AAPL",
"quantity": "100",
"quantity-direction": "Long",
"close-price": "150.50",
"average-open-price": "145.00",
"average-yearly-market-close-price": "140.00",
"average-daily-market-close-price": "149.00",
"multiplier": 1.0,
"cost-effect": "Debit",
"is-suppressed": false,
"is-frozen": false,
"restricted-quantity": "0",
"realized-day-gain": "550.00",
"realized-day-gain-effect": "Credit",
"realized-day-gain-date": "2024-01-01",
"realized-today": "550.00",
"realized-today-effect": "Credit",
"realized-today-date": "2024-01-01",
"created-at": "2024-01-01T10:00:00Z",
"updated-at": "2024-01-01T16:00:00Z"
}"#;
let position: Result<FullPosition, _> = serde_json::from_str(json);
assert!(position.is_ok());
let position = position.unwrap();
assert_eq!(position.account_number.0, "TEST123");
assert_eq!(position.symbol.0, "AAPL");
assert_eq!(position.quantity, Decimal::from_str("100").unwrap());
matches!(position.quantity_direction, QuantityDirection::Long);
matches!(position.instrument_type, InstrumentType::Equity);
}
#[test]
fn test_brief_position_debug() {
let json = r#"{
"account-number": "BRIEF123",
"symbol": "MSFT",
"instrument-type": "Equity",
"underlying-symbol": "MSFT",
"quantity": "50",
"quantity-direction": "Short",
"close-price": "300.00",
"average-open-price": "295.00",
"multiplier": 1.0,
"cost-effect": "Credit",
"is-suppressed": true,
"is-frozen": false,
"restricted-quantity": 10.0,
"realized-day-gain": "-250.00",
"realized-today": "-250.00",
"created-at": "2024-01-01T09:00:00Z",
"updated-at": "2024-01-01T15:30:00Z"
}"#;
let position: Result<BriefPosition, _> = serde_json::from_str(json);
assert!(position.is_ok());
let position = position.unwrap();
assert_eq!(position.account_number.0, "BRIEF123");
assert_eq!(position.symbol.0, "MSFT");
assert_eq!(position.quantity, Decimal::from_str("50").unwrap());
matches!(position.quantity_direction, QuantityDirection::Short);
assert!(position.is_suppressed);
assert!(!position.is_frozen);
}
#[test]
fn test_position_with_zero_quantity() {
let json = r#"{
"account-number": "ZERO123",
"symbol": "TSLA",
"instrument-type": "Equity",
"underlying-symbol": "TSLA",
"quantity": "0",
"quantity-direction": "Zero",
"close-price": "200.00",
"average-open-price": "200.00",
"multiplier": 1.0,
"cost-effect": "None",
"is-suppressed": false,
"is-frozen": false,
"restricted-quantity": 0.0,
"realized-day-gain": "0.00",
"realized-today": "0.00",
"created-at": "2024-01-01T12:00:00Z",
"updated-at": "2024-01-01T12:00:00Z"
}"#;
let position: Result<BriefPosition, _> = serde_json::from_str(json);
assert!(position.is_ok());
let position = position.unwrap();
matches!(position.quantity_direction, QuantityDirection::Zero);
assert_eq!(position.quantity, Decimal::ZERO);
matches!(position.cost_effect, PriceEffect::None);
}
}