nautilus_model/events/position/
adjusted.rs1use 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#[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")
39)]
40pub struct PositionAdjusted {
41 pub trader_id: TraderId,
43 pub strategy_id: StrategyId,
45 pub instrument_id: InstrumentId,
47 pub position_id: PositionId,
49 pub account_id: AccountId,
51 pub adjustment_type: PositionAdjustmentType,
53 pub quantity_change: Option<Decimal>,
55 pub pnl_change: Option<Money>,
57 pub reason: Option<Ustr>,
59 pub event_id: UUID4,
61 pub ts_event: UnixNanos,
63 pub ts_init: UnixNanos,
65}
66
67impl PositionAdjusted {
68 #[allow(clippy::too_many_arguments)]
70 pub fn new(
71 trader_id: TraderId,
72 strategy_id: StrategyId,
73 instrument_id: InstrumentId,
74 position_id: PositionId,
75 account_id: AccountId,
76 adjustment_type: PositionAdjustmentType,
77 quantity_change: Option<Decimal>,
78 pnl_change: Option<Money>,
79 reason: Option<Ustr>,
80 event_id: UUID4,
81 ts_event: UnixNanos,
82 ts_init: UnixNanos,
83 ) -> Self {
84 Self {
85 trader_id,
86 strategy_id,
87 instrument_id,
88 position_id,
89 account_id,
90 adjustment_type,
91 quantity_change,
92 pnl_change,
93 reason,
94 event_id,
95 ts_event,
96 ts_init,
97 }
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use std::str::FromStr;
104
105 use nautilus_core::UnixNanos;
106 use rstest::*;
107
108 use super::*;
109 use crate::{
110 enums::PositionAdjustmentType,
111 identifiers::{AccountId, InstrumentId, PositionId, StrategyId, TraderId},
112 types::{Currency, Money},
113 };
114
115 fn create_test_commission_adjustment() -> PositionAdjusted {
116 PositionAdjusted::new(
117 TraderId::from("TRADER-001"),
118 StrategyId::from("EMA-CROSS"),
119 InstrumentId::from("BTCUSDT.BINANCE"),
120 PositionId::from("P-001"),
121 AccountId::from("BINANCE-001"),
122 PositionAdjustmentType::Commission,
123 Some(Decimal::from_str("-0.001").unwrap()),
124 None,
125 Some(Ustr::from("O-123")),
126 Default::default(),
127 UnixNanos::from(1_000_000_000),
128 UnixNanos::from(2_000_000_000),
129 )
130 }
131
132 fn create_test_funding_adjustment() -> PositionAdjusted {
133 PositionAdjusted::new(
134 TraderId::from("TRADER-001"),
135 StrategyId::from("EMA-CROSS"),
136 InstrumentId::from("BTCUSD-PERP.BINANCE"),
137 PositionId::from("P-002"),
138 AccountId::from("BINANCE-001"),
139 PositionAdjustmentType::Funding,
140 None,
141 Some(Money::new(-5.50, Currency::USD())),
142 Some(Ustr::from("funding_2024_01_15_08:00")),
143 Default::default(),
144 UnixNanos::from(1_000_000_000),
145 UnixNanos::from(2_000_000_000),
146 )
147 }
148
149 #[rstest]
150 fn test_position_adjustment_commission_new() {
151 let adjustment = create_test_commission_adjustment();
152
153 assert_eq!(adjustment.trader_id, TraderId::from("TRADER-001"));
154 assert_eq!(adjustment.strategy_id, StrategyId::from("EMA-CROSS"));
155 assert_eq!(
156 adjustment.instrument_id,
157 InstrumentId::from("BTCUSDT.BINANCE")
158 );
159 assert_eq!(adjustment.position_id, PositionId::from("P-001"));
160 assert_eq!(adjustment.account_id, AccountId::from("BINANCE-001"));
161 assert_eq!(
162 adjustment.adjustment_type,
163 PositionAdjustmentType::Commission
164 );
165 assert_eq!(
166 adjustment.quantity_change,
167 Some(Decimal::from_str("-0.001").unwrap())
168 );
169 assert_eq!(adjustment.pnl_change, None);
170 assert_eq!(adjustment.reason, Some(Ustr::from("O-123")));
171 assert_eq!(adjustment.ts_event, UnixNanos::from(1_000_000_000));
172 assert_eq!(adjustment.ts_init, UnixNanos::from(2_000_000_000));
173 }
174
175 #[rstest]
176 fn test_position_adjustment_funding_new() {
177 let adjustment = create_test_funding_adjustment();
178
179 assert_eq!(adjustment.trader_id, TraderId::from("TRADER-001"));
180 assert_eq!(adjustment.strategy_id, StrategyId::from("EMA-CROSS"));
181 assert_eq!(
182 adjustment.instrument_id,
183 InstrumentId::from("BTCUSD-PERP.BINANCE")
184 );
185 assert_eq!(adjustment.position_id, PositionId::from("P-002"));
186 assert_eq!(adjustment.account_id, AccountId::from("BINANCE-001"));
187 assert_eq!(adjustment.adjustment_type, PositionAdjustmentType::Funding);
188 assert_eq!(adjustment.quantity_change, None);
189 assert_eq!(
190 adjustment.pnl_change,
191 Some(Money::new(-5.50, Currency::USD()))
192 );
193 assert_eq!(
194 adjustment.reason,
195 Some(Ustr::from("funding_2024_01_15_08:00"))
196 );
197 assert_eq!(adjustment.ts_event, UnixNanos::from(1_000_000_000));
198 assert_eq!(adjustment.ts_init, UnixNanos::from(2_000_000_000));
199 }
200
201 #[rstest]
202 fn test_position_adjustment_clone() {
203 let adjustment1 = create_test_commission_adjustment();
204 let adjustment2 = adjustment1;
205
206 assert_eq!(adjustment1, adjustment2);
207 }
208
209 #[rstest]
210 fn test_position_adjustment_debug() {
211 let adjustment = create_test_commission_adjustment();
212 let debug_str = format!("{adjustment:?}");
213
214 assert!(debug_str.contains("PositionAdjusted"));
215 assert!(debug_str.contains("TRADER-001"));
216 assert!(debug_str.contains("EMA-CROSS"));
217 assert!(debug_str.contains("BTCUSDT.BINANCE"));
218 assert!(debug_str.contains("P-001"));
219 assert!(debug_str.contains("Commission"));
220 }
221
222 #[rstest]
223 fn test_position_adjustment_partial_eq() {
224 let adjustment1 = create_test_commission_adjustment();
225 let mut adjustment2 = create_test_commission_adjustment();
226 adjustment2.event_id = adjustment1.event_id;
227
228 let mut adjustment3 = create_test_commission_adjustment();
229 adjustment3.event_id = adjustment1.event_id;
230 adjustment3.quantity_change = Some(Decimal::from_str("-0.002").unwrap());
231
232 assert_eq!(adjustment1, adjustment2);
233 assert_ne!(adjustment1, adjustment3);
234 }
235
236 #[rstest]
237 fn test_position_adjustment_different_types() {
238 let commission = create_test_commission_adjustment();
239 let funding = create_test_funding_adjustment();
240
241 assert_eq!(
242 commission.adjustment_type,
243 PositionAdjustmentType::Commission
244 );
245 assert_eq!(funding.adjustment_type, PositionAdjustmentType::Funding);
246 assert_ne!(commission.adjustment_type, funding.adjustment_type);
247 }
248
249 #[rstest]
250 fn test_position_adjustment_timestamps() {
251 let adjustment = create_test_commission_adjustment();
252
253 assert_eq!(adjustment.ts_event, UnixNanos::from(1_000_000_000));
254 assert_eq!(adjustment.ts_init, UnixNanos::from(2_000_000_000));
255 assert!(adjustment.ts_event < adjustment.ts_init);
256 }
257
258 #[rstest]
259 fn test_position_adjustment_serialization() {
260 let original = create_test_commission_adjustment();
261
262 let json = serde_json::to_string(&original).unwrap();
263 let deserialized: PositionAdjusted = serde_json::from_str(&json).unwrap();
264
265 assert_eq!(original, deserialized);
266 }
267}