use nautilus_core::{
UUID4,
nanos::{DurationNanos, UnixNanos},
};
use crate::{
enums::{OrderSide, PositionSide},
events::OrderFilled,
identifiers::{AccountId, ClientOrderId, InstrumentId, PositionId, StrategyId, TraderId},
position::Position,
types::{Currency, Money, Price, Quantity},
};
#[repr(C)]
#[derive(Clone, PartialEq, Debug)]
#[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 PositionClosed {
pub trader_id: TraderId,
pub strategy_id: StrategyId,
pub instrument_id: InstrumentId,
pub position_id: PositionId,
pub account_id: AccountId,
pub opening_order_id: ClientOrderId,
pub closing_order_id: Option<ClientOrderId>,
pub entry: OrderSide,
pub side: PositionSide,
pub signed_qty: f64,
pub quantity: Quantity,
pub peak_quantity: Quantity,
pub last_qty: Quantity,
pub last_px: Price,
pub currency: Currency,
pub avg_px_open: f64,
pub avg_px_close: Option<f64>,
pub realized_return: f64,
pub realized_pnl: Option<Money>,
pub unrealized_pnl: Money,
pub duration: DurationNanos,
pub event_id: UUID4,
pub ts_opened: UnixNanos,
pub ts_closed: Option<UnixNanos>,
pub ts_event: UnixNanos,
pub ts_init: UnixNanos,
}
impl PositionClosed {
pub fn create(
position: &Position,
fill: &OrderFilled,
event_id: UUID4,
ts_init: UnixNanos,
) -> Self {
Self {
trader_id: position.trader_id,
strategy_id: position.strategy_id,
instrument_id: position.instrument_id,
position_id: position.id,
account_id: position.account_id,
opening_order_id: position.opening_order_id,
closing_order_id: position.closing_order_id,
entry: position.entry,
side: position.side,
signed_qty: position.signed_qty,
quantity: position.quantity,
peak_quantity: position.peak_qty,
last_qty: fill.last_qty,
last_px: fill.last_px,
currency: position.quote_currency,
avg_px_open: position.avg_px_open,
avg_px_close: position.avg_px_close,
realized_return: position.realized_return,
realized_pnl: position.realized_pnl,
unrealized_pnl: Money::new(0.0, position.quote_currency),
duration: position.duration_ns,
event_id,
ts_opened: position.ts_opened,
ts_closed: position.ts_closed,
ts_event: fill.ts_event,
ts_init,
}
}
}
#[cfg(test)]
mod tests {
use nautilus_core::UnixNanos;
use rstest::*;
use super::*;
use crate::{
enums::{LiquiditySide, OrderSide, OrderType, PositionSide},
events::OrderFilled,
identifiers::{
AccountId, ClientOrderId, InstrumentId, PositionId, StrategyId, TradeId, TraderId,
VenueOrderId,
},
instruments::{InstrumentAny, stubs::audusd_sim},
position::Position,
types::{Currency, Money, Price, Quantity},
};
fn create_test_position_closed() -> PositionClosed {
PositionClosed {
trader_id: TraderId::from("TRADER-001"),
strategy_id: StrategyId::from("EMA-CROSS"),
instrument_id: InstrumentId::from("EURUSD.SIM"),
position_id: PositionId::from("P-001"),
account_id: AccountId::from("SIM-001"),
opening_order_id: ClientOrderId::from("O-19700101-000000-001-001-1"),
closing_order_id: Some(ClientOrderId::from("O-19700101-000000-001-001-2")),
entry: OrderSide::Buy,
side: PositionSide::Flat,
signed_qty: 0.0,
quantity: Quantity::from("0"),
peak_quantity: Quantity::from("150"),
last_qty: Quantity::from("150"),
last_px: Price::from("1.0600"),
currency: Currency::USD(),
avg_px_open: 1.0525,
avg_px_close: Some(1.0600),
realized_return: 0.0071,
realized_pnl: Some(Money::new(112.50, Currency::USD())),
unrealized_pnl: Money::new(0.0, Currency::USD()),
duration: 3_600_000_000_000, event_id: UUID4::default(),
ts_opened: UnixNanos::from(1_000_000_000),
ts_closed: Some(UnixNanos::from(4_600_000_000)),
ts_event: UnixNanos::from(4_600_000_000),
ts_init: UnixNanos::from(5_000_000_000),
}
}
fn create_test_order_filled() -> OrderFilled {
OrderFilled::new(
TraderId::from("TRADER-001"),
StrategyId::from("EMA-CROSS"),
InstrumentId::from("EURUSD.SIM"),
ClientOrderId::from("O-19700101-000000-001-001-2"),
VenueOrderId::from("2"),
AccountId::from("SIM-001"),
TradeId::from("T-002"),
OrderSide::Sell,
OrderType::Market,
Quantity::from("150"),
Price::from("1.0600"),
Currency::USD(),
LiquiditySide::Taker,
UUID4::default(),
UnixNanos::from(4_600_000_000),
UnixNanos::from(5_000_000_000),
false,
Some(PositionId::from("P-001")),
Some(Money::new(2.5, Currency::USD())),
)
}
#[rstest]
fn test_position_closed_create() {
let instrument = audusd_sim();
let initial_fill = OrderFilled::new(
TraderId::from("TRADER-001"),
StrategyId::from("EMA-CROSS"),
InstrumentId::from("AUD/USD.SIM"),
ClientOrderId::from("O-19700101-000000-001-001-1"),
VenueOrderId::from("1"),
AccountId::from("SIM-001"),
TradeId::from("T-001"),
OrderSide::Buy,
OrderType::Market,
Quantity::from("100"),
Price::from("0.8000"),
Currency::USD(),
LiquiditySide::Taker,
UUID4::default(),
UnixNanos::from(1_000_000_000),
UnixNanos::from(2_000_000_000),
false,
Some(PositionId::from("P-001")),
Some(Money::new(2.0, Currency::USD())),
);
let position = Position::new(&InstrumentAny::CurrencyPair(instrument), initial_fill);
let closing_fill = create_test_order_filled();
let event_id = UUID4::default();
let ts_init = UnixNanos::from(6_000_000_000);
let position_closed = PositionClosed::create(&position, &closing_fill, event_id, ts_init);
assert_eq!(position_closed.trader_id, position.trader_id);
assert_eq!(position_closed.strategy_id, position.strategy_id);
assert_eq!(position_closed.instrument_id, position.instrument_id);
assert_eq!(position_closed.position_id, position.id);
assert_eq!(position_closed.account_id, position.account_id);
assert_eq!(position_closed.opening_order_id, position.opening_order_id);
assert_eq!(position_closed.closing_order_id, position.closing_order_id);
assert_eq!(position_closed.entry, position.entry);
assert_eq!(position_closed.side, position.side);
assert_eq!(position_closed.signed_qty, position.signed_qty);
assert_eq!(position_closed.quantity, position.quantity);
assert_eq!(position_closed.peak_quantity, position.peak_qty);
assert_eq!(position_closed.last_qty, closing_fill.last_qty);
assert_eq!(position_closed.last_px, closing_fill.last_px);
assert_eq!(position_closed.currency, position.quote_currency);
assert_eq!(position_closed.avg_px_open, position.avg_px_open);
assert_eq!(position_closed.avg_px_close, position.avg_px_close);
assert_eq!(position_closed.realized_return, position.realized_return);
assert_eq!(position_closed.realized_pnl, position.realized_pnl);
assert_eq!(
position_closed.unrealized_pnl,
Money::new(0.0, position.quote_currency)
);
assert_eq!(position_closed.duration, position.duration_ns);
assert_eq!(position_closed.event_id, event_id);
assert_eq!(position_closed.ts_opened, position.ts_opened);
assert_eq!(position_closed.ts_closed, position.ts_closed);
assert_eq!(position_closed.ts_event, closing_fill.ts_event);
assert_eq!(position_closed.ts_init, ts_init);
}
#[rstest]
fn test_position_closed_flat_position() {
let position_closed = create_test_position_closed();
assert_eq!(position_closed.side, PositionSide::Flat);
assert_eq!(position_closed.signed_qty, 0.0);
assert_eq!(position_closed.quantity, Quantity::from("0"));
assert_eq!(
position_closed.unrealized_pnl,
Money::new(0.0, Currency::USD())
);
}
#[rstest]
fn test_position_closed_loss_scenario() {
let mut position_closed = create_test_position_closed();
position_closed.avg_px_close = Some(1.0400); position_closed.realized_return = -0.0119;
position_closed.realized_pnl = Some(Money::new(-187.50, Currency::USD()));
assert_eq!(position_closed.avg_px_close, Some(1.0400));
assert!(position_closed.realized_return < 0.0);
assert_eq!(
position_closed.realized_pnl,
Some(Money::new(-187.50, Currency::USD()))
);
}
}