Skip to main content

nautilus_model/events/position/
adjusted.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use nautilus_core::{UUID4, UnixNanos};
17use rust_decimal::Decimal;
18use serde::{Deserialize, Serialize};
19use ustr::Ustr;
20
21use crate::{
22    enums::PositionAdjustmentType,
23    identifiers::{AccountId, InstrumentId, PositionId, StrategyId, TraderId},
24    types::Money,
25};
26
27/// Represents an adjustment to a position's quantity or realized PnL.
28///
29/// This event is used to track changes to positions that occur outside of normal
30/// order fills, such as:
31/// - Commission adjustments that affect the actual quantity held (e.g., crypto spot commissions)
32/// - Funding payments that affect realized PnL (e.g., perpetual futures funding)
33#[repr(C)]
34#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)]
35#[serde(tag = "type")]
36#[cfg_attr(
37    feature = "python",
38    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
39)]
40#[cfg_attr(
41    feature = "python",
42    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
43)]
44pub struct PositionAdjusted {
45    /// The trader ID associated with the event.
46    pub trader_id: TraderId,
47    /// The strategy ID associated with the event.
48    pub strategy_id: StrategyId,
49    /// The instrument ID associated with the event.
50    pub instrument_id: InstrumentId,
51    /// The position ID associated with the event.
52    pub position_id: PositionId,
53    /// The account ID associated with the event.
54    pub account_id: AccountId,
55    /// The type of adjustment.
56    pub adjustment_type: PositionAdjustmentType,
57    /// The quantity change (if applicable). Positive increases quantity, negative decreases.
58    pub quantity_change: Option<Decimal>,
59    /// The PnL change (if applicable). Can be positive or negative.
60    pub pnl_change: Option<Money>,
61    /// Optional reason or reference for the adjustment (e.g., order ID, funding period).
62    pub reason: Option<Ustr>,
63    /// The unique identifier for the event.
64    pub event_id: UUID4,
65    /// UNIX timestamp (nanoseconds) when the event occurred.
66    pub ts_event: UnixNanos,
67    /// UNIX timestamp (nanoseconds) when the event was initialized.
68    pub ts_init: UnixNanos,
69}
70
71impl PositionAdjusted {
72    /// Creates a new [`PositionAdjusted`] instance.
73    #[expect(clippy::too_many_arguments)]
74    #[must_use]
75    pub fn new(
76        trader_id: TraderId,
77        strategy_id: StrategyId,
78        instrument_id: InstrumentId,
79        position_id: PositionId,
80        account_id: AccountId,
81        adjustment_type: PositionAdjustmentType,
82        quantity_change: Option<Decimal>,
83        pnl_change: Option<Money>,
84        reason: Option<Ustr>,
85        event_id: UUID4,
86        ts_event: UnixNanos,
87        ts_init: UnixNanos,
88    ) -> Self {
89        Self {
90            trader_id,
91            strategy_id,
92            instrument_id,
93            position_id,
94            account_id,
95            adjustment_type,
96            quantity_change,
97            pnl_change,
98            reason,
99            event_id,
100            ts_event,
101            ts_init,
102        }
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use std::str::FromStr;
109
110    use nautilus_core::UnixNanos;
111    use rstest::*;
112
113    use super::*;
114    use crate::{
115        enums::PositionAdjustmentType,
116        identifiers::{AccountId, InstrumentId, PositionId, StrategyId, TraderId},
117        types::{Currency, Money},
118    };
119
120    fn create_test_commission_adjustment() -> PositionAdjusted {
121        PositionAdjusted::new(
122            TraderId::from("TRADER-001"),
123            StrategyId::from("EMA-CROSS"),
124            InstrumentId::from("BTCUSDT.BINANCE"),
125            PositionId::from("P-001"),
126            AccountId::from("BINANCE-001"),
127            PositionAdjustmentType::Commission,
128            Some(Decimal::from_str("-0.001").unwrap()),
129            None,
130            Some(Ustr::from("O-123")),
131            UUID4::default(),
132            UnixNanos::from(1_000_000_000),
133            UnixNanos::from(2_000_000_000),
134        )
135    }
136
137    fn create_test_funding_adjustment() -> PositionAdjusted {
138        PositionAdjusted::new(
139            TraderId::from("TRADER-001"),
140            StrategyId::from("EMA-CROSS"),
141            InstrumentId::from("BTCUSD-PERP.BINANCE"),
142            PositionId::from("P-002"),
143            AccountId::from("BINANCE-001"),
144            PositionAdjustmentType::Funding,
145            None,
146            Some(Money::new(-5.50, Currency::USD())),
147            Some(Ustr::from("funding_2024_01_15_08:00")),
148            UUID4::default(),
149            UnixNanos::from(1_000_000_000),
150            UnixNanos::from(2_000_000_000),
151        )
152    }
153
154    #[rstest]
155    fn test_position_adjustment_different_types() {
156        let commission = create_test_commission_adjustment();
157        let funding = create_test_funding_adjustment();
158
159        assert_eq!(
160            commission.adjustment_type,
161            PositionAdjustmentType::Commission
162        );
163        assert_eq!(funding.adjustment_type, PositionAdjustmentType::Funding);
164        assert_ne!(commission.adjustment_type, funding.adjustment_type);
165    }
166
167    #[rstest]
168    fn test_position_adjustment_serialization() {
169        let original = create_test_commission_adjustment();
170
171        let json = serde_json::to_string(&original).unwrap();
172        let deserialized: PositionAdjusted = serde_json::from_str(&json).unwrap();
173
174        assert_eq!(original, deserialized);
175    }
176}