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};
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 std::hash::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////////////////////////////////////////////////////////////////////////////////
128// Tests
129////////////////////////////////////////////////////////////////////////////////
130
131#[cfg(test)]
132mod tests {
133    use std::{
134        collections::hash_map::DefaultHasher,
135        hash::{Hash, Hasher},
136        str::FromStr,
137    };
138
139    use nautilus_core::serialization::Serializable;
140    use rstest::{fixture, rstest};
141    use serde_json;
142
143    use super::*;
144
145    #[fixture]
146    fn instrument_id() -> InstrumentId {
147        InstrumentId::from("BTCUSDT-PERP.BINANCE")
148    }
149
150    #[rstest]
151    fn test_funding_rate_update_new(instrument_id: InstrumentId) {
152        let rate = Decimal::from_str("0.0001").unwrap();
153        let ts_event = UnixNanos::from(1);
154        let ts_init = UnixNanos::from(2);
155
156        let funding_rate = FundingRateUpdate::new(instrument_id, rate, None, ts_event, ts_init);
157
158        assert_eq!(funding_rate.instrument_id, instrument_id);
159        assert_eq!(funding_rate.rate, rate);
160        assert_eq!(funding_rate.next_funding_ns, None);
161        assert_eq!(funding_rate.ts_event, ts_event);
162        assert_eq!(funding_rate.ts_init, ts_init);
163    }
164
165    #[rstest]
166    fn test_funding_rate_update_new_with_optional_fields(instrument_id: InstrumentId) {
167        let rate = Decimal::from_str("0.0001").unwrap();
168        let next_funding_ns = Some(UnixNanos::from(1000));
169        let ts_event = UnixNanos::from(1);
170        let ts_init = UnixNanos::from(2);
171
172        let funding_rate =
173            FundingRateUpdate::new(instrument_id, rate, next_funding_ns, ts_event, ts_init);
174
175        assert_eq!(funding_rate.instrument_id, instrument_id);
176        assert_eq!(funding_rate.rate, rate);
177        assert_eq!(funding_rate.next_funding_ns, next_funding_ns);
178        assert_eq!(funding_rate.ts_event, ts_event);
179        assert_eq!(funding_rate.ts_init, ts_init);
180    }
181
182    #[rstest]
183    fn test_funding_rate_update_display(instrument_id: InstrumentId) {
184        let rate = Decimal::from_str("0.0001").unwrap();
185        let next_funding_ns = Some(UnixNanos::from(1000));
186        let ts_event = UnixNanos::from(1);
187        let ts_init = UnixNanos::from(2);
188
189        let funding_rate =
190            FundingRateUpdate::new(instrument_id, rate, next_funding_ns, ts_event, ts_init);
191
192        assert_eq!(
193            format!("{funding_rate}"),
194            "BTCUSDT-PERP.BINANCE,0.0001,Some(1000),1,2"
195        );
196    }
197
198    #[rstest]
199    fn test_funding_rate_update_get_ts_init(instrument_id: InstrumentId) {
200        let rate = Decimal::from_str("0.0001").unwrap();
201        let ts_event = UnixNanos::from(1);
202        let ts_init = UnixNanos::from(2);
203
204        let funding_rate = FundingRateUpdate::new(instrument_id, rate, None, ts_event, ts_init);
205
206        assert_eq!(funding_rate.ts_init(), ts_init);
207    }
208
209    #[rstest]
210    fn test_funding_rate_update_eq_hash(instrument_id: InstrumentId) {
211        let rate = Decimal::from_str("0.0001").unwrap();
212        let ts_event = UnixNanos::from(1);
213        let ts_init = UnixNanos::from(2);
214
215        let funding_rate1 = FundingRateUpdate::new(instrument_id, rate, None, ts_event, ts_init);
216        let funding_rate2 = FundingRateUpdate::new(instrument_id, rate, None, ts_event, ts_init);
217        let funding_rate3 = FundingRateUpdate::new(
218            instrument_id,
219            Decimal::from_str("0.0002").unwrap(),
220            None,
221            ts_event,
222            ts_init,
223        );
224
225        assert_eq!(funding_rate1, funding_rate2);
226        assert_ne!(funding_rate1, funding_rate3);
227
228        // Test Hash implementation
229        let mut hasher1 = DefaultHasher::new();
230        let mut hasher2 = DefaultHasher::new();
231        funding_rate1.hash(&mut hasher1);
232        funding_rate2.hash(&mut hasher2);
233        assert_eq!(hasher1.finish(), hasher2.finish());
234    }
235
236    #[rstest]
237    fn test_funding_rate_update_json_serialization(instrument_id: InstrumentId) {
238        let rate = Decimal::from_str("0.0001").unwrap();
239        let next_funding_ns = Some(UnixNanos::from(1000));
240        let ts_event = UnixNanos::from(1);
241        let ts_init = UnixNanos::from(2);
242
243        let funding_rate =
244            FundingRateUpdate::new(instrument_id, rate, next_funding_ns, ts_event, ts_init);
245
246        let serialized = funding_rate.to_json_bytes().unwrap();
247        let deserialized = FundingRateUpdate::from_json_bytes(&serialized).unwrap();
248
249        assert_eq!(funding_rate, deserialized);
250    }
251
252    #[rstest]
253    fn test_funding_rate_update_msgpack_serialization(instrument_id: InstrumentId) {
254        let rate = Decimal::from_str("0.0001").unwrap();
255        let next_funding_ns = Some(UnixNanos::from(1000));
256        let ts_event = UnixNanos::from(1);
257        let ts_init = UnixNanos::from(2);
258
259        let funding_rate =
260            FundingRateUpdate::new(instrument_id, rate, next_funding_ns, ts_event, ts_init);
261
262        let serialized = funding_rate.to_msgpack_bytes().unwrap();
263        let deserialized = FundingRateUpdate::from_msgpack_bytes(&serialized).unwrap();
264
265        assert_eq!(funding_rate, deserialized);
266    }
267
268    #[rstest]
269    fn test_funding_rate_update_serde_json(instrument_id: InstrumentId) {
270        let rate = Decimal::from_str("0.0001").unwrap();
271        let next_funding_ns = Some(UnixNanos::from(1000));
272        let ts_event = UnixNanos::from(1);
273        let ts_init = UnixNanos::from(2);
274
275        let funding_rate =
276            FundingRateUpdate::new(instrument_id, rate, next_funding_ns, ts_event, ts_init);
277
278        let json_str = serde_json::to_string(&funding_rate).unwrap();
279        let deserialized: FundingRateUpdate = serde_json::from_str(&json_str).unwrap();
280
281        assert_eq!(funding_rate, deserialized);
282    }
283}