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)]
105mod tests {
106 use std::str::FromStr;
107
108 use nautilus_core::UnixNanos;
109 use rstest::*;
110
111 use super::*;
112 use crate::{
113 enums::PositionAdjustmentType,
114 identifiers::{AccountId, InstrumentId, PositionId, StrategyId, TraderId},
115 types::{Currency, Money},
116 };
117
118 fn create_test_commission_adjustment() -> PositionAdjusted {
119 PositionAdjusted::new(
120 TraderId::from("TRADER-001"),
121 StrategyId::from("EMA-CROSS"),
122 InstrumentId::from("BTCUSDT.BINANCE"),
123 PositionId::from("P-001"),
124 AccountId::from("BINANCE-001"),
125 PositionAdjustmentType::Commission,
126 Some(Decimal::from_str("-0.001").unwrap()),
127 None,
128 Some(Ustr::from("O-123")),
129 Default::default(),
130 UnixNanos::from(1_000_000_000),
131 UnixNanos::from(2_000_000_000),
132 )
133 }
134
135 fn create_test_funding_adjustment() -> PositionAdjusted {
136 PositionAdjusted::new(
137 TraderId::from("TRADER-001"),
138 StrategyId::from("EMA-CROSS"),
139 InstrumentId::from("BTCUSD-PERP.BINANCE"),
140 PositionId::from("P-002"),
141 AccountId::from("BINANCE-001"),
142 PositionAdjustmentType::Funding,
143 None,
144 Some(Money::new(-5.50, Currency::USD())),
145 Some(Ustr::from("funding_2024_01_15_08:00")),
146 Default::default(),
147 UnixNanos::from(1_000_000_000),
148 UnixNanos::from(2_000_000_000),
149 )
150 }
151
152 #[rstest]
153 fn test_position_adjustment_commission_new() {
154 let adjustment = create_test_commission_adjustment();
155
156 assert_eq!(adjustment.trader_id, TraderId::from("TRADER-001"));
157 assert_eq!(adjustment.strategy_id, StrategyId::from("EMA-CROSS"));
158 assert_eq!(
159 adjustment.instrument_id,
160 InstrumentId::from("BTCUSDT.BINANCE")
161 );
162 assert_eq!(adjustment.position_id, PositionId::from("P-001"));
163 assert_eq!(adjustment.account_id, AccountId::from("BINANCE-001"));
164 assert_eq!(
165 adjustment.adjustment_type,
166 PositionAdjustmentType::Commission
167 );
168 assert_eq!(
169 adjustment.quantity_change,
170 Some(Decimal::from_str("-0.001").unwrap())
171 );
172 assert_eq!(adjustment.pnl_change, None);
173 assert_eq!(adjustment.reason, Some(Ustr::from("O-123")));
174 assert_eq!(adjustment.ts_event, UnixNanos::from(1_000_000_000));
175 assert_eq!(adjustment.ts_init, UnixNanos::from(2_000_000_000));
176 }
177
178 #[rstest]
179 fn test_position_adjustment_funding_new() {
180 let adjustment = create_test_funding_adjustment();
181
182 assert_eq!(adjustment.trader_id, TraderId::from("TRADER-001"));
183 assert_eq!(adjustment.strategy_id, StrategyId::from("EMA-CROSS"));
184 assert_eq!(
185 adjustment.instrument_id,
186 InstrumentId::from("BTCUSD-PERP.BINANCE")
187 );
188 assert_eq!(adjustment.position_id, PositionId::from("P-002"));
189 assert_eq!(adjustment.account_id, AccountId::from("BINANCE-001"));
190 assert_eq!(adjustment.adjustment_type, PositionAdjustmentType::Funding);
191 assert_eq!(adjustment.quantity_change, None);
192 assert_eq!(
193 adjustment.pnl_change,
194 Some(Money::new(-5.50, Currency::USD()))
195 );
196 assert_eq!(
197 adjustment.reason,
198 Some(Ustr::from("funding_2024_01_15_08:00"))
199 );
200 assert_eq!(adjustment.ts_event, UnixNanos::from(1_000_000_000));
201 assert_eq!(adjustment.ts_init, UnixNanos::from(2_000_000_000));
202 }
203
204 #[rstest]
205 fn test_position_adjustment_clone() {
206 let adjustment1 = create_test_commission_adjustment();
207 let adjustment2 = adjustment1;
208
209 assert_eq!(adjustment1, adjustment2);
210 }
211
212 #[rstest]
213 fn test_position_adjustment_debug() {
214 let adjustment = create_test_commission_adjustment();
215 let debug_str = format!("{adjustment:?}");
216
217 assert!(debug_str.contains("PositionAdjusted"));
218 assert!(debug_str.contains("TRADER-001"));
219 assert!(debug_str.contains("EMA-CROSS"));
220 assert!(debug_str.contains("BTCUSDT.BINANCE"));
221 assert!(debug_str.contains("P-001"));
222 assert!(debug_str.contains("Commission"));
223 }
224
225 #[rstest]
226 fn test_position_adjustment_partial_eq() {
227 let adjustment1 = create_test_commission_adjustment();
228 let mut adjustment2 = create_test_commission_adjustment();
229 adjustment2.event_id = adjustment1.event_id;
230
231 let mut adjustment3 = create_test_commission_adjustment();
232 adjustment3.event_id = adjustment1.event_id;
233 adjustment3.quantity_change = Some(Decimal::from_str("-0.002").unwrap());
234
235 assert_eq!(adjustment1, adjustment2);
236 assert_ne!(adjustment1, adjustment3);
237 }
238
239 #[rstest]
240 fn test_position_adjustment_different_types() {
241 let commission = create_test_commission_adjustment();
242 let funding = create_test_funding_adjustment();
243
244 assert_eq!(
245 commission.adjustment_type,
246 PositionAdjustmentType::Commission
247 );
248 assert_eq!(funding.adjustment_type, PositionAdjustmentType::Funding);
249 assert_ne!(commission.adjustment_type, funding.adjustment_type);
250 }
251
252 #[rstest]
253 fn test_position_adjustment_timestamps() {
254 let adjustment = create_test_commission_adjustment();
255
256 assert_eq!(adjustment.ts_event, UnixNanos::from(1_000_000_000));
257 assert_eq!(adjustment.ts_init, UnixNanos::from(2_000_000_000));
258 assert!(adjustment.ts_event < adjustment.ts_init);
259 }
260
261 #[rstest]
262 fn test_position_adjustment_serialization() {
263 let original = create_test_commission_adjustment();
264
265 let json = serde_json::to_string(&original).unwrap();
266 let deserialized: PositionAdjusted = serde_json::from_str(&json).unwrap();
267
268 assert_eq!(original, deserialized);
269 }
270}