use std::fmt::{Debug, Display};
use nautilus_core::{UUID4, UnixNanos};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use crate::{
enums::PositionSideSpecified,
identifiers::{AccountId, InstrumentId, PositionId},
types::Quantity,
};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type")]
#[cfg_attr(
feature = "python",
pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
)]
#[cfg_attr(
feature = "python",
pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
)]
pub struct PositionStatusReport {
pub account_id: AccountId,
pub instrument_id: InstrumentId,
pub position_side: PositionSideSpecified,
pub quantity: Quantity,
pub signed_decimal_qty: Decimal,
pub report_id: UUID4,
pub ts_last: UnixNanos,
pub ts_init: UnixNanos,
pub venue_position_id: Option<PositionId>,
pub avg_px_open: Option<Decimal>,
}
impl PositionStatusReport {
#[allow(clippy::too_many_arguments)]
#[must_use]
pub fn new(
account_id: AccountId,
instrument_id: InstrumentId,
position_side: PositionSideSpecified,
quantity: Quantity,
ts_last: UnixNanos,
ts_init: UnixNanos,
report_id: Option<UUID4>,
venue_position_id: Option<PositionId>,
avg_px_open: Option<Decimal>,
) -> Self {
let signed_decimal_qty = match position_side {
PositionSideSpecified::Long => quantity.as_decimal(),
PositionSideSpecified::Short => -quantity.as_decimal(),
PositionSideSpecified::Flat => Decimal::ZERO,
};
Self {
account_id,
instrument_id,
position_side,
quantity,
signed_decimal_qty,
report_id: report_id.unwrap_or_default(),
ts_last,
ts_init,
venue_position_id,
avg_px_open,
}
}
#[must_use]
pub const fn has_venue_position_id(&self) -> bool {
self.venue_position_id.is_some()
}
#[must_use]
pub fn is_flat(&self) -> bool {
self.position_side == PositionSideSpecified::Flat
}
#[must_use]
pub fn is_long(&self) -> bool {
self.position_side == PositionSideSpecified::Long
}
#[must_use]
pub fn is_short(&self) -> bool {
self.position_side == PositionSideSpecified::Short
}
}
impl Display for PositionStatusReport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"PositionStatusReport(account={}, instrument={}, side={}, qty={}, venue_pos_id={:?}, avg_px_open={:?}, ts_last={}, ts_init={})",
self.account_id,
self.instrument_id,
self.position_side,
self.signed_decimal_qty,
self.venue_position_id,
self.avg_px_open,
self.ts_last,
self.ts_init
)
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use nautilus_core::UnixNanos;
use rstest::*;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use super::*;
use crate::{
identifiers::{AccountId, InstrumentId, PositionId},
types::Quantity,
};
fn test_position_status_report_long() -> PositionStatusReport {
PositionStatusReport::new(
AccountId::from("SIM-001"),
InstrumentId::from("AUDUSD.SIM"),
PositionSideSpecified::Long,
Quantity::from("100"),
UnixNanos::from(1_000_000_000),
UnixNanos::from(2_000_000_000),
None, Some(PositionId::from("P-001")), None, )
}
fn test_position_status_report_short() -> PositionStatusReport {
PositionStatusReport::new(
AccountId::from("SIM-001"),
InstrumentId::from("AUDUSD.SIM"),
PositionSideSpecified::Short,
Quantity::from("50"),
UnixNanos::from(1_000_000_000),
UnixNanos::from(2_000_000_000),
None,
None,
None,
)
}
fn test_position_status_report_flat() -> PositionStatusReport {
PositionStatusReport::new(
AccountId::from("SIM-001"),
InstrumentId::from("AUDUSD.SIM"),
PositionSideSpecified::Flat,
Quantity::from("0"),
UnixNanos::from(1_000_000_000),
UnixNanos::from(2_000_000_000),
None,
None,
None,
)
}
#[rstest]
fn test_position_status_report_new_long() {
let report = test_position_status_report_long();
assert_eq!(report.account_id, AccountId::from("SIM-001"));
assert_eq!(report.instrument_id, InstrumentId::from("AUDUSD.SIM"));
assert_eq!(report.position_side, PositionSideSpecified::Long);
assert_eq!(report.quantity, Quantity::from("100"));
assert_eq!(report.signed_decimal_qty, dec!(100));
assert_eq!(report.venue_position_id, Some(PositionId::from("P-001")));
assert_eq!(report.ts_last, UnixNanos::from(1_000_000_000));
assert_eq!(report.ts_init, UnixNanos::from(2_000_000_000));
}
#[rstest]
fn test_position_status_report_new_short() {
let report = test_position_status_report_short();
assert_eq!(report.position_side, PositionSideSpecified::Short);
assert_eq!(report.quantity, Quantity::from("50"));
assert_eq!(report.signed_decimal_qty, dec!(-50));
assert_eq!(report.venue_position_id, None);
}
#[rstest]
fn test_position_status_report_new_flat() {
let report = test_position_status_report_flat();
assert_eq!(report.position_side, PositionSideSpecified::Flat);
assert_eq!(report.quantity, Quantity::from("0"));
assert_eq!(report.signed_decimal_qty, Decimal::ZERO);
}
#[rstest]
fn test_position_status_report_with_generated_report_id() {
let report = PositionStatusReport::new(
AccountId::from("SIM-001"),
InstrumentId::from("AUDUSD.SIM"),
PositionSideSpecified::Long,
Quantity::from("100"),
UnixNanos::from(1_000_000_000),
UnixNanos::from(2_000_000_000),
None, None,
None,
);
assert_ne!(
report.report_id.to_string(),
"00000000-0000-0000-0000-000000000000"
);
}
#[rstest]
fn test_has_venue_position_id() {
let mut report = test_position_status_report_long();
assert!(report.has_venue_position_id());
report.venue_position_id = None;
assert!(!report.has_venue_position_id());
}
#[rstest]
fn test_is_flat() {
let long_report = test_position_status_report_long();
let short_report = test_position_status_report_short();
let flat_report = test_position_status_report_flat();
let no_position_report = PositionStatusReport::new(
AccountId::from("SIM-001"),
InstrumentId::from("AUDUSD.SIM"),
PositionSideSpecified::Flat,
Quantity::from("0"),
UnixNanos::from(1_000_000_000),
UnixNanos::from(2_000_000_000),
None,
None,
None,
);
assert!(!long_report.is_flat());
assert!(!short_report.is_flat());
assert!(flat_report.is_flat());
assert!(no_position_report.is_flat());
}
#[rstest]
fn test_is_long() {
let long_report = test_position_status_report_long();
let short_report = test_position_status_report_short();
let flat_report = test_position_status_report_flat();
assert!(long_report.is_long());
assert!(!short_report.is_long());
assert!(!flat_report.is_long());
}
#[rstest]
fn test_is_short() {
let long_report = test_position_status_report_long();
let short_report = test_position_status_report_short();
let flat_report = test_position_status_report_flat();
assert!(!long_report.is_short());
assert!(short_report.is_short());
assert!(!flat_report.is_short());
}
#[rstest]
fn test_display() {
let report = test_position_status_report_long();
let display_str = format!("{report}");
assert!(display_str.contains("PositionStatusReport"));
assert!(display_str.contains("SIM-001"));
assert!(display_str.contains("AUDUSD.SIM"));
assert!(display_str.contains("LONG"));
assert!(display_str.contains("100"));
assert!(display_str.contains("P-001"));
assert!(display_str.contains("avg_px_open=None"));
}
#[rstest]
fn test_clone_and_equality() {
let report1 = test_position_status_report_long();
let report2 = report1.clone();
assert_eq!(report1, report2);
}
#[rstest]
fn test_serialization_roundtrip() {
let original = test_position_status_report_long();
let json = serde_json::to_string(&original).unwrap();
let deserialized: PositionStatusReport = serde_json::from_str(&json).unwrap();
assert_eq!(original, deserialized);
}
#[rstest]
fn test_signed_decimal_qty_calculation() {
let long_100 = PositionStatusReport::new(
AccountId::from("SIM-001"),
InstrumentId::from("AUDUSD.SIM"),
PositionSideSpecified::Long,
Quantity::from("100.5"),
UnixNanos::from(1_000_000_000),
UnixNanos::from(2_000_000_000),
None,
None,
None,
);
let short_200 = PositionStatusReport::new(
AccountId::from("SIM-001"),
InstrumentId::from("AUDUSD.SIM"),
PositionSideSpecified::Short,
Quantity::from("200.75"),
UnixNanos::from(1_000_000_000),
UnixNanos::from(2_000_000_000),
None,
None,
None,
);
assert_eq!(long_100.signed_decimal_qty, dec!(100.5));
assert_eq!(short_200.signed_decimal_qty, dec!(-200.75));
}
#[rstest]
fn test_different_position_sides_not_equal() {
let long_report = test_position_status_report_long();
let short_report = PositionStatusReport::new(
AccountId::from("SIM-001"),
InstrumentId::from("AUDUSD.SIM"),
PositionSideSpecified::Short,
Quantity::from("100"), UnixNanos::from(1_000_000_000),
UnixNanos::from(2_000_000_000),
None, Some(PositionId::from("P-001")), None, );
assert_ne!(long_report, short_report);
assert_ne!(
long_report.signed_decimal_qty,
short_report.signed_decimal_qty
);
}
#[rstest]
fn test_with_avg_px_open() {
let report = PositionStatusReport::new(
AccountId::from("SIM-001"),
InstrumentId::from("AUDUSD.SIM"),
PositionSideSpecified::Long,
Quantity::from("100"),
UnixNanos::from(1_000_000_000),
UnixNanos::from(2_000_000_000),
None,
Some(PositionId::from("P-001")),
Some(Decimal::from_str("1.23456").unwrap()),
);
assert_eq!(
report.avg_px_open,
Some(rust_decimal::Decimal::from_str("1.23456").unwrap())
);
assert!(format!("{report}").contains("avg_px_open=Some(1.23456)"));
}
#[rstest]
fn test_avg_px_open_none_default() {
let report = PositionStatusReport::new(
AccountId::from("SIM-001"),
InstrumentId::from("AUDUSD.SIM"),
PositionSideSpecified::Long,
Quantity::from("100"),
UnixNanos::from(1_000_000_000),
UnixNanos::from(2_000_000_000),
None,
None,
None, );
assert_eq!(report.avg_px_open, None);
}
#[rstest]
fn test_avg_px_open_with_different_sides() {
let long_with_price = PositionStatusReport::new(
AccountId::from("SIM-001"),
InstrumentId::from("AUDUSD.SIM"),
PositionSideSpecified::Long,
Quantity::from("100"),
UnixNanos::from(1_000_000_000),
UnixNanos::from(2_000_000_000),
None,
None,
Some(Decimal::from_str("1.50000").unwrap()),
);
let short_with_price = PositionStatusReport::new(
AccountId::from("SIM-001"),
InstrumentId::from("AUDUSD.SIM"),
PositionSideSpecified::Short,
Quantity::from("100"),
UnixNanos::from(1_000_000_000),
UnixNanos::from(2_000_000_000),
None,
None,
Some(Decimal::from_str("1.60000").unwrap()),
);
assert_eq!(
long_with_price.avg_px_open,
Some(rust_decimal::Decimal::from_str("1.50000").unwrap())
);
assert_eq!(
short_with_price.avg_px_open,
Some(rust_decimal::Decimal::from_str("1.60000").unwrap())
);
}
#[rstest]
fn test_avg_px_open_serialization() {
let report = PositionStatusReport::new(
AccountId::from("SIM-001"),
InstrumentId::from("AUDUSD.SIM"),
PositionSideSpecified::Long,
Quantity::from("100"),
UnixNanos::from(1_000_000_000),
UnixNanos::from(2_000_000_000),
None,
None,
Some(Decimal::from_str("1.99999").unwrap()),
);
let json = serde_json::to_string(&report).unwrap();
let deserialized: PositionStatusReport = serde_json::from_str(&json).unwrap();
assert_eq!(report.avg_px_open, deserialized.avg_px_open);
}
}