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