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