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::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.core.nautilus_pyo3.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("rate".to_string(), "Decimal128".to_string());
108        metadata.insert("interval".to_string(), "UInt16".to_string());
109        metadata.insert("next_funding_ns".to_string(), "UInt64".to_string());
110        metadata.insert("ts_event".to_string(), "UInt64".to_string());
111        metadata.insert("ts_init".to_string(), "UInt64".to_string());
112        metadata
113    }
114}
115
116impl Display for FundingRateUpdate {
117    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118        write!(
119            f,
120            "{},{},{:?},{:?},{},{}",
121            self.instrument_id,
122            self.rate,
123            self.interval,
124            self.next_funding_ns.map(|ts| ts.as_u64()),
125            self.ts_event,
126            self.ts_init
127        )
128    }
129}
130
131impl Serializable for FundingRateUpdate {}
132
133impl HasTsInit for FundingRateUpdate {
134    fn ts_init(&self) -> UnixNanos {
135        self.ts_init
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use std::{
142        collections::hash_map::DefaultHasher,
143        hash::{Hash, Hasher},
144        str::FromStr,
145    };
146
147    use nautilus_core::serialization::{
148        Serializable,
149        msgpack::{FromMsgPack, ToMsgPack},
150    };
151    use rstest::{fixture, rstest};
152    use serde_json;
153
154    use super::*;
155
156    #[fixture]
157    fn instrument_id() -> InstrumentId {
158        InstrumentId::from("BTCUSDT-PERP.BINANCE")
159    }
160
161    #[rstest]
162    fn test_funding_rate_update_new(instrument_id: InstrumentId) {
163        let rate = Decimal::from_str("0.0001").unwrap();
164        let ts_event = UnixNanos::from(1);
165        let ts_init = UnixNanos::from(2);
166
167        let funding_rate =
168            FundingRateUpdate::new(instrument_id, rate, None, None, ts_event, ts_init);
169
170        assert_eq!(funding_rate.instrument_id, instrument_id);
171        assert_eq!(funding_rate.rate, rate);
172        assert_eq!(funding_rate.interval, None);
173        assert_eq!(funding_rate.next_funding_ns, None);
174        assert_eq!(funding_rate.ts_event, ts_event);
175        assert_eq!(funding_rate.ts_init, ts_init);
176    }
177
178    #[rstest]
179    fn test_funding_rate_update_new_with_optional_fields(instrument_id: InstrumentId) {
180        let rate = Decimal::from_str("0.0001").unwrap();
181        let interval = Some(60);
182        let next_funding_ns = Some(UnixNanos::from(1000));
183        let ts_event = UnixNanos::from(1);
184        let ts_init = UnixNanos::from(2);
185
186        let funding_rate = FundingRateUpdate::new(
187            instrument_id,
188            rate,
189            interval,
190            next_funding_ns,
191            ts_event,
192            ts_init,
193        );
194
195        assert_eq!(funding_rate.instrument_id, instrument_id);
196        assert_eq!(funding_rate.rate, rate);
197        assert_eq!(funding_rate.interval, interval);
198        assert_eq!(funding_rate.next_funding_ns, next_funding_ns);
199        assert_eq!(funding_rate.ts_event, ts_event);
200        assert_eq!(funding_rate.ts_init, ts_init);
201    }
202
203    #[rstest]
204    fn test_funding_rate_update_display(instrument_id: InstrumentId) {
205        let rate = Decimal::from_str("0.0001").unwrap();
206        let interval = Some(60);
207        let next_funding_ns = Some(UnixNanos::from(1000));
208        let ts_event = UnixNanos::from(1);
209        let ts_init = UnixNanos::from(2);
210
211        let funding_rate = FundingRateUpdate::new(
212            instrument_id,
213            rate,
214            interval,
215            next_funding_ns,
216            ts_event,
217            ts_init,
218        );
219
220        assert_eq!(
221            format!("{funding_rate}"),
222            "BTCUSDT-PERP.BINANCE,0.0001,Some(60),Some(1000),1,2"
223        );
224    }
225
226    #[rstest]
227    fn test_funding_rate_update_get_ts_init(instrument_id: InstrumentId) {
228        let rate = Decimal::from_str("0.0001").unwrap();
229        let ts_event = UnixNanos::from(1);
230        let ts_init = UnixNanos::from(2);
231
232        let funding_rate =
233            FundingRateUpdate::new(instrument_id, rate, None, None, ts_event, ts_init);
234
235        assert_eq!(funding_rate.ts_init(), ts_init);
236    }
237
238    #[rstest]
239    fn test_funding_rate_update_eq_hash(instrument_id: InstrumentId) {
240        let rate = Decimal::from_str("0.0001").unwrap();
241        let ts_event = UnixNanos::from(1);
242        let ts_init = UnixNanos::from(2);
243
244        let funding_rate1 =
245            FundingRateUpdate::new(instrument_id, rate, None, None, ts_event, ts_init);
246        let funding_rate2 =
247            FundingRateUpdate::new(instrument_id, rate, None, None, ts_event, ts_init);
248        let funding_rate3 = FundingRateUpdate::new(
249            instrument_id,
250            Decimal::from_str("0.0002").unwrap(),
251            None,
252            None,
253            ts_event,
254            ts_init,
255        );
256
257        assert_eq!(funding_rate1, funding_rate2);
258        assert_ne!(funding_rate1, funding_rate3);
259
260        // Test Hash implementation
261        let mut hasher1 = DefaultHasher::new();
262        let mut hasher2 = DefaultHasher::new();
263        funding_rate1.hash(&mut hasher1);
264        funding_rate2.hash(&mut hasher2);
265        assert_eq!(hasher1.finish(), hasher2.finish());
266    }
267
268    #[rstest]
269    fn test_funding_rate_update_json_serialization(instrument_id: InstrumentId) {
270        let rate = Decimal::from_str("0.0001").unwrap();
271        let interval = Some(60);
272        let next_funding_ns = Some(UnixNanos::from(1000));
273        let ts_event = UnixNanos::from(1);
274        let ts_init = UnixNanos::from(2);
275
276        let funding_rate = FundingRateUpdate::new(
277            instrument_id,
278            rate,
279            interval,
280            next_funding_ns,
281            ts_event,
282            ts_init,
283        );
284
285        let serialized = funding_rate.to_json_bytes().unwrap();
286        let deserialized = FundingRateUpdate::from_json_bytes(&serialized).unwrap();
287
288        assert_eq!(funding_rate, deserialized);
289    }
290
291    #[rstest]
292    fn test_funding_rate_update_msgpack_serialization(instrument_id: InstrumentId) {
293        let rate = Decimal::from_str("0.0001").unwrap();
294        let interval = Some(60);
295        let next_funding_ns = Some(UnixNanos::from(1000));
296        let ts_event = UnixNanos::from(1);
297        let ts_init = UnixNanos::from(2);
298
299        let funding_rate = FundingRateUpdate::new(
300            instrument_id,
301            rate,
302            interval,
303            next_funding_ns,
304            ts_event,
305            ts_init,
306        );
307
308        let serialized = funding_rate.to_msgpack_bytes().unwrap();
309        let deserialized = FundingRateUpdate::from_msgpack_bytes(&serialized).unwrap();
310
311        assert_eq!(funding_rate, deserialized);
312    }
313
314    #[rstest]
315    fn test_funding_rate_update_serde_json(instrument_id: InstrumentId) {
316        let rate = Decimal::from_str("0.0001").unwrap();
317        let interval = Some(60);
318        let next_funding_ns = Some(UnixNanos::from(1000));
319        let ts_event = UnixNanos::from(1);
320        let ts_init = UnixNanos::from(2);
321
322        let funding_rate = FundingRateUpdate::new(
323            instrument_id,
324            rate,
325            interval,
326            next_funding_ns,
327            ts_event,
328            ts_init,
329        );
330
331        let json_str = serde_json::to_string(&funding_rate).unwrap();
332        let deserialized: FundingRateUpdate = serde_json::from_str(&json_str).unwrap();
333
334        assert_eq!(funding_rate, deserialized);
335    }
336}