Skip to main content

alpaca_data/
client.rs

1use std::fmt;
2use std::sync::Arc;
3use std::time::Duration;
4
5use alpaca_core::{BaseUrl, Credentials, env};
6use alpaca_http::{
7    ConcurrencyLimit, HttpClient, HttpResponse, NoContent, RequestParts, RetryConfig,
8    StaticHeaderAuthenticator, TransportObserver,
9};
10use serde::de::DeserializeOwned;
11
12use crate::{
13    Error, corporate_actions::CorporateActionsClient, news::NewsClient, options::OptionsClient,
14    stocks::StocksClient,
15};
16
17pub const DATA_API_KEY_ENV: &str = "ALPACA_DATA_API_KEY";
18pub const DATA_SECRET_KEY_ENV: &str = "ALPACA_DATA_SECRET_KEY";
19const DEFAULT_DATA_BASE_URL: &str = "https://data.alpaca.markets";
20const APCA_API_KEY_HEADER: &str = "APCA-API-KEY-ID";
21const APCA_API_SECRET_HEADER: &str = "APCA-API-SECRET-KEY";
22const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
23const DEFAULT_MAX_IN_FLIGHT: usize = 50;
24const DEFAULT_POOL_MAX_IDLE_PER_HOST: usize = 50;
25const DEFAULT_POOL_IDLE_TIMEOUT: Duration = Duration::from_secs(90);
26const DEFAULT_TCP_KEEPALIVE: Duration = Duration::from_secs(60);
27
28#[derive(Clone)]
29pub struct Client {
30    pub(crate) inner: Arc<ClientInner>,
31}
32
33#[allow(dead_code)]
34pub(crate) struct ClientInner {
35    http: HttpClient,
36    auth: StaticHeaderAuthenticator,
37    base_url: BaseUrl,
38}
39
40#[derive(Clone, Default)]
41pub struct ClientBuilder {
42    api_key: Option<String>,
43    secret_key: Option<String>,
44    base_url: Option<BaseUrl>,
45    timeout: Option<Duration>,
46    observer: Option<Arc<dyn TransportObserver>>,
47    retry_config: RetryConfig,
48    max_in_flight: Option<usize>,
49}
50
51impl Client {
52    #[must_use]
53    pub fn builder() -> ClientBuilder {
54        ClientBuilder::default()
55    }
56
57    pub fn new(credentials: Credentials) -> Result<Self, Error> {
58        Self::builder().credentials(credentials).build()
59    }
60
61    pub fn from_env() -> Result<Self, Error> {
62        Self::builder().credentials_from_env()?.build()
63    }
64
65    #[must_use]
66    pub fn base_url(&self) -> &BaseUrl {
67        &self.inner.base_url
68    }
69
70    #[must_use]
71    pub fn stocks(&self) -> StocksClient {
72        StocksClient::new(self.inner.clone())
73    }
74
75    #[must_use]
76    pub fn options(&self) -> OptionsClient {
77        OptionsClient::new(self.inner.clone())
78    }
79
80    #[must_use]
81    pub fn news(&self) -> NewsClient {
82        NewsClient::new(self.inner.clone())
83    }
84
85    #[must_use]
86    pub fn corporate_actions(&self) -> CorporateActionsClient {
87        CorporateActionsClient::new(self.inner.clone())
88    }
89}
90
91impl fmt::Debug for Client {
92    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93        f.debug_struct("Client")
94            .field("base_url", self.base_url())
95            .field("http", &"HttpClient")
96            .field("auth", &"[REDACTED]")
97            .finish()
98    }
99}
100
101impl fmt::Debug for ClientInner {
102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103        f.debug_struct("ClientInner")
104            .field("base_url", &self.base_url)
105            .field("http", &"HttpClient")
106            .field("auth", &"[REDACTED]")
107            .finish()
108    }
109}
110
111impl fmt::Debug for ClientBuilder {
112    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113        f.debug_struct("ClientBuilder")
114            .field("api_key", &redacted_option(&self.api_key))
115            .field("secret_key", &redacted_option(&self.secret_key))
116            .field("base_url", &self.base_url)
117            .field("timeout", &self.timeout)
118            .field(
119                "observer",
120                &self.observer.as_ref().map(|_| "TransportObserver"),
121            )
122            .field("retry_config", &self.retry_config)
123            .field("max_in_flight", &self.max_in_flight)
124            .finish()
125    }
126}
127
128impl ClientBuilder {
129    #[must_use]
130    pub fn credentials(mut self, credentials: Credentials) -> Self {
131        self.api_key = Some(credentials.api_key().to_owned());
132        self.secret_key = Some(credentials.secret_key().to_owned());
133        self
134    }
135
136    #[must_use]
137    pub fn api_key(mut self, api_key: impl Into<String>) -> Self {
138        self.api_key = Some(api_key.into());
139        self
140    }
141
142    #[must_use]
143    pub fn secret_key(mut self, secret_key: impl Into<String>) -> Self {
144        self.secret_key = Some(secret_key.into());
145        self
146    }
147
148    #[must_use]
149    pub fn base_url(mut self, base_url: BaseUrl) -> Self {
150        self.base_url = Some(base_url);
151        self
152    }
153
154    pub fn base_url_str(mut self, base_url: impl AsRef<str>) -> Result<Self, Error> {
155        self.base_url = Some(BaseUrl::new(base_url.as_ref())?);
156        Ok(self)
157    }
158
159    pub fn credentials_from_env(self) -> Result<Self, Error> {
160        self.credentials_from_env_names(DATA_API_KEY_ENV, DATA_SECRET_KEY_ENV)
161    }
162
163    pub fn credentials_from_env_names(
164        mut self,
165        api_key_var: &str,
166        secret_key_var: &str,
167    ) -> Result<Self, Error> {
168        if let Some(credentials) = env::credentials_from_env_names(api_key_var, secret_key_var)? {
169            return Ok(self.credentials(credentials));
170        }
171
172        if let Some(credentials) = env::credentials_from_env()? {
173            self = self.credentials(credentials);
174        }
175
176        Ok(self)
177    }
178
179    #[must_use]
180    pub fn timeout(mut self, timeout: Duration) -> Self {
181        self.timeout = Some(timeout);
182        self
183    }
184
185    #[must_use]
186    pub fn observer(mut self, observer: Arc<dyn TransportObserver>) -> Self {
187        self.observer = Some(observer);
188        self
189    }
190
191    #[must_use]
192    pub fn retry_config(mut self, retry_config: RetryConfig) -> Self {
193        self.retry_config = retry_config;
194        self
195    }
196
197    #[must_use]
198    pub fn max_in_flight(mut self, max_in_flight: usize) -> Self {
199        self.max_in_flight = Some(max_in_flight);
200        self
201    }
202
203    pub fn build(self) -> Result<Client, Error> {
204        let credentials = match (self.api_key, self.secret_key) {
205            (Some(api_key), Some(secret_key)) => Credentials::new(api_key, secret_key)?,
206            (None, None) => return Err(Error::MissingCredentials),
207            _ => {
208                return Err(Error::InvalidConfiguration(
209                    "api_key and secret_key must be paired".to_owned(),
210                ));
211            }
212        };
213
214        let base_url = match self.base_url {
215            Some(base_url) => base_url,
216            None => BaseUrl::new(DEFAULT_DATA_BASE_URL)?,
217        };
218        let auth = StaticHeaderAuthenticator::from_pairs([
219            (APCA_API_KEY_HEADER, credentials.api_key()),
220            (APCA_API_SECRET_HEADER, credentials.secret_key()),
221        ])?;
222
223        let timeout = self.timeout.unwrap_or(DEFAULT_TIMEOUT);
224        let reqwest_client = Self::build_reqwest_client(timeout)?;
225
226        let mut http_builder = HttpClient::builder()
227            .retry_config(self.retry_config)
228            .reqwest_client(reqwest_client);
229        if let Some(observer) = self.observer {
230            http_builder = http_builder.observer(observer);
231        }
232        http_builder = http_builder.concurrency_limit(ConcurrencyLimit::new(Some(
233            self.max_in_flight.unwrap_or(DEFAULT_MAX_IN_FLIGHT),
234        )));
235
236        let http = http_builder.build()?;
237
238        Ok(Client {
239            inner: Arc::new(ClientInner {
240                http,
241                auth,
242                base_url,
243            }),
244        })
245    }
246
247    fn build_reqwest_client(timeout: Duration) -> Result<reqwest::Client, Error> {
248        reqwest::Client::builder()
249            .no_proxy()
250            .pool_max_idle_per_host(DEFAULT_POOL_MAX_IDLE_PER_HOST)
251            .pool_idle_timeout(DEFAULT_POOL_IDLE_TIMEOUT)
252            .tcp_keepalive(DEFAULT_TCP_KEEPALIVE)
253            .timeout(timeout)
254            .http1_only()
255            .build()
256            .map_err(|error| alpaca_http::Error::from_reqwest(error, None).into())
257    }
258}
259
260impl ClientInner {
261    #[allow(dead_code)]
262    pub(crate) async fn send_json<T>(&self, request: RequestParts) -> Result<HttpResponse<T>, Error>
263    where
264        T: DeserializeOwned,
265    {
266        self.http
267            .send_json(&self.base_url, request, Some(&self.auth))
268            .await
269            .map_err(Error::from)
270    }
271
272    #[allow(dead_code)]
273    pub(crate) async fn send_text(
274        &self,
275        request: RequestParts,
276    ) -> Result<HttpResponse<String>, Error> {
277        self.http
278            .send_text(&self.base_url, request, Some(&self.auth))
279            .await
280            .map_err(Error::from)
281    }
282
283    #[allow(dead_code)]
284    pub(crate) async fn send_no_content(
285        &self,
286        request: RequestParts,
287    ) -> Result<HttpResponse<NoContent>, Error> {
288        self.http
289            .send_no_content(&self.base_url, request, Some(&self.auth))
290            .await
291            .map_err(Error::from)
292    }
293}
294
295fn redacted_option(value: &Option<String>) -> &'static str {
296    match value {
297        Some(_) => "[REDACTED]",
298        None => "None",
299    }
300}