Skip to main content

nautilus_databento/
common.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//! Common functions to support Databento adapter operations.
17
18use std::{fmt::Debug, fs, path::Path, sync::LazyLock};
19
20use databento::{dbn, historical::DateTimeRange};
21use indexmap::IndexMap;
22use nautilus_core::{UnixNanos, string::secret::REDACTED};
23use nautilus_model::{
24    enums::BarAggregation,
25    identifiers::{ClientId, Venue},
26};
27use time::OffsetDateTime;
28use ustr::Ustr;
29use zeroize::ZeroizeOnDrop;
30
31use crate::types::{DatabentoPublisher, PublisherId};
32
33/// Venue identifier string.
34pub const DATABENTO: &str = "DATABENTO";
35
36/// Static venue instance.
37pub static DATABENTO_VENUE: LazyLock<Venue> = LazyLock::new(|| Venue::new(Ustr::from(DATABENTO)));
38
39/// Static client ID instance.
40pub static DATABENTO_CLIENT_ID: LazyLock<ClientId> =
41    LazyLock::new(|| ClientId::new(Ustr::from(DATABENTO)));
42
43pub const ALL_SYMBOLS: &str = "ALL_SYMBOLS";
44
45/// Loads Databento publishers from a JSON file.
46///
47/// # Errors
48///
49/// Returns an error if the file cannot be read or parsed as JSON.
50pub fn load_publishers(filepath: impl AsRef<Path>) -> anyhow::Result<Vec<DatabentoPublisher>> {
51    let file_content = fs::read_to_string(filepath)?;
52    Ok(serde_json::from_str(&file_content)?)
53}
54
55/// Builds the publisher-to-venue map used by Databento decoders.
56#[must_use]
57pub fn build_publisher_venue_map(
58    publishers: &[DatabentoPublisher],
59) -> IndexMap<PublisherId, Venue> {
60    publishers
61        .iter()
62        .map(|p| (p.publisher_id, Venue::from(p.venue.as_str())))
63        .collect()
64}
65
66/// API credentials required for Databento API requests.
67#[derive(Clone, ZeroizeOnDrop)]
68pub struct Credential {
69    api_key: Box<[u8]>,
70}
71
72impl Debug for Credential {
73    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74        f.debug_struct(stringify!(Credential))
75            .field("api_key", &REDACTED)
76            .finish()
77    }
78}
79
80impl Credential {
81    /// Creates a new [`Credential`] instance from the API key.
82    #[must_use]
83    pub fn new(api_key: impl Into<String>) -> Self {
84        let api_key_bytes = api_key.into().into_bytes();
85
86        Self {
87            api_key: api_key_bytes.into_boxed_slice(),
88        }
89    }
90
91    /// Returns the API key associated with this credential.
92    ///
93    /// The API key is always valid UTF-8 because it is created from a `String`.
94    #[allow(
95        clippy::missing_panics_doc,
96        reason = "the API key is created from a UTF-8 String"
97    )]
98    #[must_use]
99    pub fn api_key(&self) -> &str {
100        std::str::from_utf8(&self.api_key).expect("API key is valid UTF-8")
101    }
102
103    /// Returns a masked version of the API key for logging purposes.
104    ///
105    /// Shows first 4 and last 4 characters with ellipsis in between.
106    /// For keys shorter than 8 characters, shows asterisks only.
107    #[must_use]
108    pub fn api_key_masked(&self) -> String {
109        nautilus_core::string::secret::mask_api_key(self.api_key())
110    }
111}
112
113/// # Errors
114///
115/// Returns an error if converting `start` or `end` to `OffsetDateTime` fails.
116pub fn get_date_time_range(start: UnixNanos, end: UnixNanos) -> anyhow::Result<DateTimeRange> {
117    Ok(DateTimeRange::from((
118        OffsetDateTime::from_unix_timestamp_nanos(i128::from(start.as_u64()))?,
119        OffsetDateTime::from_unix_timestamp_nanos(i128::from(end.as_u64()))?,
120    )))
121}
122
123/// Returns the Databento OHLCV schema for a bar aggregation.
124///
125/// # Errors
126///
127/// Returns an error if Databento has no OHLCV schema for `aggregation`.
128pub fn ohlcv_schema_from_aggregation(aggregation: BarAggregation) -> anyhow::Result<dbn::Schema> {
129    match aggregation {
130        BarAggregation::Second => Ok(dbn::Schema::Ohlcv1S),
131        BarAggregation::Minute => Ok(dbn::Schema::Ohlcv1M),
132        BarAggregation::Hour => Ok(dbn::Schema::Ohlcv1H),
133        BarAggregation::Day => Ok(dbn::Schema::Ohlcv1D),
134        _ => anyhow::bail!("Unsupported bar aggregation for Databento OHLCV: {aggregation:?}"),
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use rstest::*;
141
142    use super::*;
143
144    #[rstest]
145    #[case(
146        UnixNanos::default(),
147        UnixNanos::default(),
148        "DateTimeRange { start: 1970-01-01 0:00:00.0 +00:00:00, end: 1970-01-01 0:00:00.0 +00:00:00 }"
149    )]
150    #[case(UnixNanos::default(), 1_000_000_000.into(), "DateTimeRange { start: 1970-01-01 0:00:00.0 +00:00:00, end: 1970-01-01 0:00:01.0 +00:00:00 }")]
151    fn test_get_date_time_range(
152        #[case] start: UnixNanos,
153        #[case] end: UnixNanos,
154        #[case] range_str: &str,
155    ) {
156        let range = get_date_time_range(start, end).unwrap();
157        assert_eq!(format!("{range:?}"), range_str);
158    }
159
160    #[rstest]
161    fn test_credential_api_key_masked_short() {
162        let credential = Credential::new("short");
163        assert_eq!(credential.api_key_masked(), "*****");
164    }
165
166    #[rstest]
167    fn test_credential_api_key_masked_long() {
168        let credential = Credential::new("abcdefghijklmnop");
169        assert_eq!(credential.api_key_masked(), "abcd...mnop");
170    }
171
172    #[rstest]
173    fn test_credential_debug_redaction() {
174        let credential = Credential::new("test_api_key");
175        let debug_str = format!("{credential:?}");
176        assert!(debug_str.contains(REDACTED));
177        assert!(!debug_str.contains("test_api_key"));
178    }
179}