nautilus_model/data/
funding.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 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")
35)]
36pub struct FundingRateUpdate {
37    /// The instrument ID for the funding rate.
38    pub instrument_id: InstrumentId,
39    /// The current funding rate.
40    pub rate: Decimal,
41    /// UNIX timestamp (nanoseconds) for the next funding time.
42    pub next_funding_ns: Option<UnixNanos>,
43    /// UNIX timestamp (nanoseconds) when the funding rate event occurred.
44    pub ts_event: UnixNanos,
45    /// UNIX timestamp (nanoseconds) when the instance was created.
46    pub ts_init: UnixNanos,
47}
48
49impl PartialEq for FundingRateUpdate {
50    fn eq(&self, other: &Self) -> bool {
51        self.instrument_id == other.instrument_id
52            && self.rate == other.rate
53            && self.next_funding_ns == other.next_funding_ns
54    }
55}
56
57impl Hash for FundingRateUpdate {
58    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
59        // Hash only the fields used in PartialEq to maintain consistency
60        self.instrument_id.hash(state);
61        self.rate.hash(state);
62        self.next_funding_ns.hash(state);
63    }
64}
65
66impl FundingRateUpdate {
67    /// Creates a new [`FundingRateUpdate`] instance.
68    #[must_use]
69    pub fn new(
70        instrument_id: InstrumentId,
71        rate: Decimal,
72        next_funding_ns: Option<UnixNanos>,
73        ts_event: UnixNanos,
74        ts_init: UnixNanos,
75    ) -> Self {
76        Self {
77            instrument_id,
78            rate,
79            next_funding_ns,
80            ts_event,
81            ts_init,
82        }
83    }
84
85    /// Returns the metadata for the type, for use with serialization formats.
86    #[must_use]
87    pub fn get_metadata(instrument_id: &InstrumentId) -> HashMap<String, String> {
88        let mut metadata = HashMap::new();
89        metadata.insert("instrument_id".to_string(), instrument_id.to_string());
90        metadata
91    }
92
93    /// Returns the field map for the type, for use with Arrow schemas.
94    #[must_use]
95    pub fn get_fields() -> IndexMap<String, String> {
96        let mut metadata = IndexMap::new();
97        metadata.insert("rate".to_string(), "Decimal128".to_string());
98        metadata.insert("next_funding_ns".to_string(), "UInt64".to_string());
99        metadata.insert("ts_event".to_string(), "UInt64".to_string());
100        metadata.insert("ts_init".to_string(), "UInt64".to_string());
101        metadata
102    }
103}
104
105impl Display for FundingRateUpdate {
106    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107        write!(
108            f,
109            "{},{},{:?},{},{}",
110            self.instrument_id,
111            self.rate,
112            self.next_funding_ns.map(|ts| ts.as_u64()),
113            self.ts_event,
114            self.ts_init
115        )
116    }
117}
118
119impl Serializable for FundingRateUpdate {}
120
121impl HasTsInit for FundingRateUpdate {
122    fn ts_init(&self) -> UnixNanos {
123        self.ts_init
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use std::{
130        collections::hash_map::DefaultHasher,
131        hash::{Hash, Hasher},
132        str::FromStr,
133    };
134
135    use nautilus_core::serialization::{
136        Serializable,
137        msgpack::{FromMsgPack, ToMsgPack},
138    };
139    use rstest::{fixture, rstest};
140    use serde_json;
141
142    use super::*;
143
144    #[fixture]
145    fn instrument_id() -> InstrumentId {
146        InstrumentId::from("BTCUSDT-PERP.BINANCE")
147    }
148
149    #[rstest]
150    fn test_funding_rate_update_new(instrument_id: InstrumentId) {
151        let rate = Decimal::from_str("0.0001").unwrap();
152        let ts_event = UnixNanos::from(1);
153        let ts_init = UnixNanos::from(2);
154
155        let funding_rate = FundingRateUpdate::new(instrument_id, rate, None, ts_event, ts_init);
156
157        assert_eq!(funding_rate.instrument_id, instrument_id);
158        assert_eq!(funding_rate.rate, rate);
159        assert_eq!(funding_rate.next_funding_ns, None);
160        assert_eq!(funding_rate.ts_event, ts_event);
161        assert_eq!(funding_rate.ts_init, ts_init);
162    }
163
164    #[rstest]
165    fn test_funding_rate_update_new_with_optional_fields(instrument_id: InstrumentId) {
166        let rate = Decimal::from_str("0.0001").unwrap();
167        let next_funding_ns = Some(UnixNanos::from(1000));
168        let ts_event = UnixNanos::from(1);
169        let ts_init = UnixNanos::from(2);
170
171        let funding_rate =
172            FundingRateUpdate::new(instrument_id, rate, next_funding_ns, ts_event, ts_init);
173
174        assert_eq!(funding_rate.instrument_id, instrument_id);
175        assert_eq!(funding_rate.rate, rate);
176        assert_eq!(funding_rate.next_funding_ns, next_funding_ns);
177        assert_eq!(funding_rate.ts_event, ts_event);
178        assert_eq!(funding_rate.ts_init, ts_init);
179    }
180
181    #[rstest]
182    fn test_funding_rate_update_display(instrument_id: InstrumentId) {
183        let rate = Decimal::from_str("0.0001").unwrap();
184        let next_funding_ns = Some(UnixNanos::from(1000));
185        let ts_event = UnixNanos::from(1);
186        let ts_init = UnixNanos::from(2);
187
188        let funding_rate =
189            FundingRateUpdate::new(instrument_id, rate, next_funding_ns, ts_event, ts_init);
190
191        assert_eq!(
192            format!("{funding_rate}"),
193            "BTCUSDT-PERP.BINANCE,0.0001,Some(1000),1,2"
194        );
195    }
196
197    #[rstest]
198    fn test_funding_rate_update_get_ts_init(instrument_id: InstrumentId) {
199        let rate = Decimal::from_str("0.0001").unwrap();
200        let ts_event = UnixNanos::from(1);
201        let ts_init = UnixNanos::from(2);
202
203        let funding_rate = FundingRateUpdate::new(instrument_id, rate, None, ts_event, ts_init);
204
205        assert_eq!(funding_rate.ts_init(), ts_init);
206    }
207
208    #[rstest]
209    fn test_funding_rate_update_eq_hash(instrument_id: InstrumentId) {
210        let rate = Decimal::from_str("0.0001").unwrap();
211        let ts_event = UnixNanos::from(1);
212        let ts_init = UnixNanos::from(2);
213
214        let funding_rate1 = FundingRateUpdate::new(instrument_id, rate, None, ts_event, ts_init);
215        let funding_rate2 = FundingRateUpdate::new(instrument_id, rate, None, ts_event, ts_init);
216        let funding_rate3 = FundingRateUpdate::new(
217            instrument_id,
218            Decimal::from_str("0.0002").unwrap(),
219            None,
220            ts_event,
221            ts_init,
222        );
223
224        assert_eq!(funding_rate1, funding_rate2);
225        assert_ne!(funding_rate1, funding_rate3);
226
227        // Test Hash implementation
228        let mut hasher1 = DefaultHasher::new();
229        let mut hasher2 = DefaultHasher::new();
230        funding_rate1.hash(&mut hasher1);
231        funding_rate2.hash(&mut hasher2);
232        assert_eq!(hasher1.finish(), hasher2.finish());
233    }
234
235    #[rstest]
236    fn test_funding_rate_update_json_serialization(instrument_id: InstrumentId) {
237        let rate = Decimal::from_str("0.0001").unwrap();
238        let next_funding_ns = Some(UnixNanos::from(1000));
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, next_funding_ns, ts_event, ts_init);
244
245        let serialized = funding_rate.to_json_bytes().unwrap();
246        let deserialized = FundingRateUpdate::from_json_bytes(&serialized).unwrap();
247
248        assert_eq!(funding_rate, deserialized);
249    }
250
251    #[rstest]
252    fn test_funding_rate_update_msgpack_serialization(instrument_id: InstrumentId) {
253        let rate = Decimal::from_str("0.0001").unwrap();
254        let next_funding_ns = Some(UnixNanos::from(1000));
255        let ts_event = UnixNanos::from(1);
256        let ts_init = UnixNanos::from(2);
257
258        let funding_rate =
259            FundingRateUpdate::new(instrument_id, rate, next_funding_ns, ts_event, ts_init);
260
261        let serialized = funding_rate.to_msgpack_bytes().unwrap();
262        let deserialized = FundingRateUpdate::from_msgpack_bytes(&serialized).unwrap();
263
264        assert_eq!(funding_rate, deserialized);
265    }
266
267    #[rstest]
268    fn test_funding_rate_update_serde_json(instrument_id: InstrumentId) {
269        let rate = Decimal::from_str("0.0001").unwrap();
270        let next_funding_ns = Some(UnixNanos::from(1000));
271        let ts_event = UnixNanos::from(1);
272        let ts_init = UnixNanos::from(2);
273
274        let funding_rate =
275            FundingRateUpdate::new(instrument_id, rate, next_funding_ns, ts_event, ts_init);
276
277        let json_str = serde_json::to_string(&funding_rate).unwrap();
278        let deserialized: FundingRateUpdate = serde_json::from_str(&json_str).unwrap();
279
280        assert_eq!(funding_rate, deserialized);
281    }
282}