Skip to main content

nautilus_coinapi/http/
query.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//! Typed CoinAPI query parameters.
17
18use std::num::NonZeroUsize;
19
20use jiff::{RoundMode, Timestamp, TimestampRound, Unit};
21use serde::{Deserialize, Serialize, Serializer};
22
23use super::error::Error;
24use crate::common::enums::OhlcvPeriod;
25
26/// Largest documented historical page size.
27pub const HISTORY_LIMIT_MAX: u32 = 100_000;
28
29/// An explicit historical time window and record limit.
30///
31/// This represents one provider page without local filtering. OHLCV responses can include
32/// periods outside these bounds; range methods filter them locally.
33#[derive(Debug, Clone, Serialize)]
34pub struct HistoryQuery {
35    #[serde(serialize_with = "serialize_time")]
36    pub time_start: Timestamp,
37    #[serde(serialize_with = "serialize_time")]
38    pub time_end: Timestamp,
39    pub limit: u32,
40}
41
42impl HistoryQuery {
43    pub(super) fn validate(&self) -> Result<(), Error> {
44        if provider_time(self.time_start)? >= provider_time(self.time_end)? {
45            return Err(Error::Validation(
46                "time_start must precede time_end at CoinAPI's 100 ns resolution".into(),
47            ));
48        }
49
50        if !(1..=HISTORY_LIMIT_MAX).contains(&self.limit) {
51            return Err(Error::Validation(format!(
52                "limit must be in [1, {HISTORY_LIMIT_MAX}]"
53            )));
54        }
55
56        Ok(())
57    }
58}
59
60// CoinAPI accepts only zero, three, or seven fractional digits, so bounds always carry seven
61fn serialize_time<S: Serializer>(time: &Timestamp, serializer: S) -> Result<S::Ok, S::Error> {
62    let time = provider_time(*time).map_err(serde::ser::Error::custom)?;
63    serializer.collect_str(&format_args!("{time:.7}"))
64}
65
66// CoinAPI records sit on the 100 ns grid, so rounding both bounds up to it selects the same
67// records as the exact [time_start, time_end) and loses nothing at seven digits.
68pub(super) fn provider_time(time: Timestamp) -> Result<Timestamp, Error> {
69    time.round(
70        TimestampRound::new()
71            .smallest(Unit::Nanosecond)
72            .increment(100)
73            .mode(RoundMode::Ceil),
74    )
75    .map_err(|e| Error::Validation(format!("timestamp out of range: {e}")))
76}
77
78/// A historical time window `[time_start, time_end)` fetched across as many pages as it needs.
79///
80/// Without a limit, the window and the client credit budget bound the records returned.
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub struct HistoryRange {
83    pub time_start: Timestamp,
84    pub time_end: Timestamp,
85    pub limit: Option<NonZeroUsize>,
86}
87
88/// Filters for exchange discovery.
89#[derive(Debug, Clone, Default, Serialize)]
90pub struct ExchangesQuery {
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub filter_exchange_id: Option<String>,
93}
94
95/// Filters for exchange-scoped active symbol discovery.
96#[derive(Debug, Clone, Default, Serialize, Deserialize)]
97#[serde(deny_unknown_fields)]
98pub struct SymbolsQuery {
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub filter_symbol_id: Option<String>,
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub filter_asset_id: Option<String>,
103}
104
105/// Maximum depth requested for a single current order book; the provider default when absent.
106#[derive(Debug, Clone, Serialize)]
107pub struct BookQuery {
108    #[serde(skip_serializing_if = "Option::is_none")]
109    pub limit_levels: Option<u32>,
110}
111
112/// Historical trades, optionally including the exchange trade identifier.
113#[derive(Debug, Clone, Serialize)]
114pub struct TradesQuery {
115    #[serde(flatten)]
116    pub range: HistoryQuery,
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub include_id: Option<bool>,
119}
120
121/// Historical OHLCV for a provider period identifier.
122#[derive(Debug, Clone, Serialize)]
123pub struct OhlcvQuery {
124    pub period_id: OhlcvPeriod,
125    #[serde(flatten)]
126    pub range: HistoryQuery,
127}