Skip to main content

nautilus_model/data/
funding.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
16//! Domain types representing funding rate data for perpetual swap instruments.
17
18use std::{collections::HashMap, fmt::Display, hash::Hash};
19
20use indexmap::IndexMap;
21use nautilus_core::{UnixNanos, serialization::Serializable};
22use rust_decimal::Decimal;
23use serde::{Deserialize, Serialize};
24
25use super::{ARROW_TIMESTAMP_NANOSECOND, HasTsInit};
26use crate::identifiers::InstrumentId;
27
28/// Represents a funding rate update for perpetual swap instruments.
29#[repr(C)]
30#[derive(Clone, Copy, Debug, Eq, Serialize, Deserialize)]
31#[serde(tag = "type")]
32#[cfg_attr(
33    feature = "python",
34    pyo3::pyclass(module = "nautilus_trader.model", from_py_object)
35)]
36#[cfg_attr(
37    feature = "python",
38    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
39)]
40pub struct FundingRateUpdate {
41    /// The instrument ID for the funding rate.
42    pub instrument_id: InstrumentId,
43    /// The current funding rate.
44    pub rate: Decimal,
45    /// Time interval (minutes) between funding payments.
46    pub interval: Option<u16>,
47    /// UNIX timestamp (nanoseconds) for the next funding time.
48    pub next_funding_ns: Option<UnixNanos>,
49    /// UNIX timestamp (nanoseconds) when the funding rate event occurred.
50    pub ts_event: UnixNanos,
51    /// UNIX timestamp (nanoseconds) when the instance was created.
52    pub ts_init: UnixNanos,
53}
54
55impl PartialEq for FundingRateUpdate {
56    fn eq(&self, other: &Self) -> bool {
57        self.instrument_id == other.instrument_id
58            && self.rate == other.rate
59            && self.interval == other.interval
60            && self.next_funding_ns == other.next_funding_ns
61    }
62}
63
64impl Hash for FundingRateUpdate {
65    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
66        // Hash only the fields used in PartialEq to maintain consistency
67        self.instrument_id.hash(state);
68        self.rate.hash(state);
69        self.interval.hash(state);
70        self.next_funding_ns.hash(state);
71    }
72}
73
74impl FundingRateUpdate {
75    /// Creates a new [`FundingRateUpdate`] instance.
76    #[must_use]
77    pub fn new(
78        instrument_id: InstrumentId,
79        rate: Decimal,
80        interval: Option<u16>,
81        next_funding_ns: Option<UnixNanos>,
82        ts_event: UnixNanos,
83        ts_init: UnixNanos,
84    ) -> Self {
85        Self {
86            instrument_id,
87            rate,
88            interval,
89            next_funding_ns,
90            ts_event,
91            ts_init,
92        }
93    }
94
95    /// Returns the metadata for the type, for use with serialization formats.
96    #[must_use]
97    pub fn get_metadata(instrument_id: &InstrumentId) -> HashMap<String, String> {
98        let mut metadata = HashMap::new();
99        metadata.insert("instrument_id".to_string(), instrument_id.to_string());
100        metadata
101    }
102
103    /// Returns the field map for the type, for use with Arrow schemas.
104    #[must_use]
105    pub fn get_fields() -> IndexMap<String, String> {
106        let mut metadata = IndexMap::new();
107        metadata.insert("instrument_id".to_string(), "Utf8".to_string());
108        metadata.insert("rate".to_string(), "Utf8".to_string());
109        metadata.insert("interval".to_string(), "UInt64".to_string());
110        metadata.insert(
111            "next_funding_ns".to_string(),
112            ARROW_TIMESTAMP_NANOSECOND.to_string(),
113        );
114        metadata.insert(
115            "ts_event".to_string(),
116            ARROW_TIMESTAMP_NANOSECOND.to_string(),
117        );
118        metadata.insert(
119            "ts_init".to_string(),
120            ARROW_TIMESTAMP_NANOSECOND.to_string(),
121        );
122        metadata
123    }
124}
125
126impl Display for FundingRateUpdate {
127    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128        write!(
129            f,
130            "{},{},{:?},{:?},{},{}",
131            self.instrument_id,
132            self.rate,
133            self.interval,
134            self.next_funding_ns.map(|ts| ts.as_u64()),
135            self.ts_event,
136            self.ts_init
137        )
138    }
139}
140
141impl Serializable for FundingRateUpdate {}
142
143impl HasTsInit for FundingRateUpdate {
144    fn ts_init(&self) -> UnixNanos {
145        self.ts_init
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use std::{
152        collections::hash_map::DefaultHasher,
153        hash::{Hash, Hasher},
154        str::FromStr,
155    };
156
157    use nautilus_core::serialization::{
158        Serializable,
159        msgpack::{FromMsgPack, ToMsgPack},
160    };
161    use rstest::{fixture, rstest};
162    use serde_json;
163
164    use super::*;
165
166    #[fixture]
167    fn instrument_id() -> InstrumentId {
168        InstrumentId::from("BTCUSDT-PERP.BINANCE")
169    }
170
171    #[rstest]
172    fn test_funding_rate_update_new(instrument_id: InstrumentId) {
173        let rate = Decimal::from_str("0.0001").unwrap();
174        let ts_event = UnixNanos::from(1);
175        let ts_init = UnixNanos::from(2);
176
177        let funding_rate =
178            FundingRateUpdate::new(instrument_id, rate, None, None, ts_event, ts_init);
179
180        assert_eq!(funding_rate.instrument_id, instrument_id);
181        assert_eq!(funding_rate.rate, rate);
182        assert_eq!(funding_rate.interval, None);
183        assert_eq!(funding_rate.next_funding_ns, None);
184        assert_eq!(funding_rate.ts_event, ts_event);
185        assert_eq!(funding_rate.ts_init, ts_init);
186    }
187
188    #[rstest]
189    fn test_funding_rate_update_new_with_optional_fields(instrument_id: InstrumentId) {
190        let rate = Decimal::from_str("0.0001").unwrap();
191        let interval = Some(60);
192        let next_funding_ns = Some(UnixNanos::from(1000));
193        let ts_event = UnixNanos::from(1);
194        let ts_init = UnixNanos::from(2);
195
196        let funding_rate = FundingRateUpdate::new(
197            instrument_id,
198            rate,
199            interval,
200            next_funding_ns,
201            ts_event,
202            ts_init,
203        );
204
205        assert_eq!(funding_rate.instrument_id, instrument_id);
206        assert_eq!(funding_rate.rate, rate);
207        assert_eq!(funding_rate.interval, interval);
208        assert_eq!(funding_rate.next_funding_ns, next_funding_ns);
209        assert_eq!(funding_rate.ts_event, ts_event);
210        assert_eq!(funding_rate.ts_init, ts_init);
211    }
212
213    #[rstest]
214    fn test_funding_rate_update_display(instrument_id: InstrumentId) {
215        let rate = Decimal::from_str("0.0001").unwrap();
216        let interval = Some(60);
217        let next_funding_ns = Some(UnixNanos::from(1000));
218        let ts_event = UnixNanos::from(1);
219        let ts_init = UnixNanos::from(2);
220
221        let funding_rate = FundingRateUpdate::new(
222            instrument_id,
223            rate,
224            interval,
225            next_funding_ns,
226            ts_event,
227            ts_init,
228        );
229
230        assert_eq!(
231            format!("{funding_rate}"),
232            "BTCUSDT-PERP.BINANCE,0.0001,Some(60),Some(1000),1,2"
233        );
234    }
235
236    #[rstest]
237    fn test_funding_rate_update_get_ts_init(instrument_id: InstrumentId) {
238        let rate = Decimal::from_str("0.0001").unwrap();
239        let ts_event = UnixNanos::from(1);
240        let ts_init = UnixNanos::from(2);
241
242        let funding_rate =
243            FundingRateUpdate::new(instrument_id, rate, None, None, ts_event, ts_init);
244
245        assert_eq!(funding_rate.ts_init(), ts_init);
246    }
247
248    #[rstest]
249    fn test_funding_rate_update_eq_hash(instrument_id: InstrumentId) {
250        let rate = Decimal::from_str("0.0001").unwrap();
251        let ts_event = UnixNanos::from(1);
252        let ts_init = UnixNanos::from(2);
253
254        let funding_rate1 =
255            FundingRateUpdate::new(instrument_id, rate, None, None, ts_event, ts_init);
256        let funding_rate2 =
257            FundingRateUpdate::new(instrument_id, rate, None, None, ts_event, ts_init);
258        let funding_rate3 = FundingRateUpdate::new(
259            instrument_id,
260            Decimal::from_str("0.0002").unwrap(),
261            None,
262            None,
263            ts_event,
264            ts_init,
265        );
266
267        assert_eq!(funding_rate1, funding_rate2);
268        assert_ne!(funding_rate1, funding_rate3);
269
270        // Test Hash implementation
271        let mut hasher1 = DefaultHasher::new();
272        let mut hasher2 = DefaultHasher::new();
273        funding_rate1.hash(&mut hasher1);
274        funding_rate2.hash(&mut hasher2);
275        assert_eq!(hasher1.finish(), hasher2.finish());
276    }
277
278    #[rstest]
279    fn test_funding_rate_update_json_serialization(instrument_id: InstrumentId) {
280        let rate = Decimal::from_str("0.0001").unwrap();
281        let interval = Some(60);
282        let next_funding_ns = Some(UnixNanos::from(1000));
283        let ts_event = UnixNanos::from(1);
284        let ts_init = UnixNanos::from(2);
285
286        let funding_rate = FundingRateUpdate::new(
287            instrument_id,
288            rate,
289            interval,
290            next_funding_ns,
291            ts_event,
292            ts_init,
293        );
294
295        let serialized = funding_rate.to_json_bytes().unwrap();
296        let deserialized = FundingRateUpdate::from_json_bytes(&serialized).unwrap();
297
298        assert_funding_rate_fields(&funding_rate, &deserialized);
299    }
300
301    #[rstest]
302    fn test_funding_rate_update_msgpack_serialization(instrument_id: InstrumentId) {
303        let rate = Decimal::from_str("0.0001").unwrap();
304        let interval = Some(60);
305        let next_funding_ns = Some(UnixNanos::from(1000));
306        let ts_event = UnixNanos::from(1);
307        let ts_init = UnixNanos::from(2);
308
309        let funding_rate = FundingRateUpdate::new(
310            instrument_id,
311            rate,
312            interval,
313            next_funding_ns,
314            ts_event,
315            ts_init,
316        );
317
318        let serialized = funding_rate.to_msgpack_bytes().unwrap();
319        let deserialized = FundingRateUpdate::from_msgpack_bytes(&serialized).unwrap();
320
321        assert_funding_rate_fields(&funding_rate, &deserialized);
322    }
323
324    #[rstest]
325    fn test_funding_rate_update_serde_json(instrument_id: InstrumentId) {
326        let rate = Decimal::from_str("0.0001").unwrap();
327        let interval = Some(60);
328        let next_funding_ns = Some(UnixNanos::from(1000));
329        let ts_event = UnixNanos::from(1);
330        let ts_init = UnixNanos::from(2);
331
332        let funding_rate = FundingRateUpdate::new(
333            instrument_id,
334            rate,
335            interval,
336            next_funding_ns,
337            ts_event,
338            ts_init,
339        );
340
341        let json_str = serde_json::to_string(&funding_rate).unwrap();
342        let deserialized: FundingRateUpdate = serde_json::from_str(&json_str).unwrap();
343
344        assert_funding_rate_fields(&funding_rate, &deserialized);
345    }
346
347    fn assert_funding_rate_fields(expected: &FundingRateUpdate, actual: &FundingRateUpdate) {
348        assert_eq!(expected.instrument_id, actual.instrument_id);
349        assert_eq!(expected.rate, actual.rate);
350        assert_eq!(expected.interval, actual.interval);
351        assert_eq!(expected.next_funding_ns, actual.next_funding_ns);
352        assert_eq!(expected.ts_event, actual.ts_event);
353        assert_eq!(expected.ts_init, actual.ts_init);
354    }
355}