1use dbn::{Compression, SType};
4use serde::Deserialize;
5use time::{Date, OffsetDateTime};
6use tracing::instrument;
7
8use crate::{
9 deserialize::deserialize_date_time,
10 historical::{handle_zstd_jsonl_response, AddToForm},
11 reference::{AdjustmentStatus, Country, Currency, End, Event, Frequency, SecurityType, Start},
12 DateTimeLike, Symbols,
13};
14
15#[derive(Debug)]
17pub struct AdjustmentFactorsClient<'a> {
18 pub(crate) inner: &'a mut super::Client,
19}
20
21impl AdjustmentFactorsClient<'_> {
22 #[instrument(name = "adjustment_factors.get_range")]
28 pub async fn get_range(
29 &mut self,
30 params: &GetRangeParams,
31 ) -> crate::Result<Vec<AdjustmentFactor>> {
32 let form = vec![
33 ("stype_in", params.stype_in.to_string()),
34 ("symbols", params.symbols.to_api_string()),
35 ("compression", Compression::Zstd.to_string()),
36 ]
37 .add_to_form(&Start(params.start))
38 .add_to_form(&End(params.end))
39 .add_to_form(¶ms.countries)
40 .add_to_form(¶ms.security_types);
41
42 let resp = self
43 .inner
44 .post("adjustment_factors.get_range")?
45 .form(&form)
46 .send()
47 .await?;
48 let mut adjustment_factors: Vec<AdjustmentFactor> =
49 handle_zstd_jsonl_response(resp).await?;
50 adjustment_factors.sort_by_key(|a| a.ex_date);
51 Ok(adjustment_factors)
52 }
53}
54
55#[derive(Debug, Clone, bon::Builder, PartialEq, Eq)]
58pub struct GetRangeParams {
59 #[builder(with = |dt: impl DateTimeLike| dt.to_date_time())]
61 pub start: OffsetDateTime,
62 #[builder(with = |dt: impl DateTimeLike| dt.to_date_time())]
66 pub end: Option<OffsetDateTime>,
67 #[builder(into)]
69 pub symbols: Symbols,
70 #[builder(default = SType::RawSymbol)]
73 pub stype_in: SType,
74 #[builder(default, into)]
77 pub countries: Vec<Country>,
78 #[builder(default, into)]
81 pub security_types: Vec<SecurityType>,
82}
83
84#[derive(Debug, Clone, PartialEq, Deserialize)]
86pub struct AdjustmentFactor {
87 pub security_id: String,
92 pub event_id: String,
94 pub event: Event,
96
97 pub issuer_name: String,
102 pub security_type: SecurityType,
104 pub primary_exchange: Option<String>,
106 pub exchange: Option<String>,
110 pub operating_mic: String,
112
113 pub symbol: Option<String>,
118 pub nasdaq_symbol: Option<String>,
120 pub local_code: Option<String>,
123 pub local_code_resulting: Option<String>,
125 pub isin: Option<String>,
127 pub isin_resulting: Option<String>,
129 pub us_code: Option<String>,
131
132 pub status: AdjustmentStatus,
134 pub ex_date: Date,
136 pub factor: f64,
138 pub close: Option<f64>,
140 pub currency: Option<String>,
142 pub sentiment: f64,
145 pub reason: u32,
147 pub gross_dividend: Option<f64>,
150 pub dividend_currency: Option<Currency>,
152 pub frequency: Option<Frequency>,
154 pub option: u32,
158 pub detail: String,
160 #[serde(deserialize_with = "deserialize_date_time")]
162 pub ts_created: OffsetDateTime,
163}
164
165#[cfg(test)]
166mod tests {
167 use std::io;
168
169 use reqwest::StatusCode;
170 use time::macros::{date, datetime};
171 use wiremock::{
172 matchers::{basic_auth, method, path},
173 Mock, MockServer, ResponseTemplate,
174 };
175
176 use super::*;
177 use crate::{
178 body_contains,
179 historical::{test_infra::API_KEY, API_VERSION},
180 reference::test_infra::client,
181 };
182
183 #[tokio::test]
184 async fn test_get_range() {
185 let _ = tracing_subscriber::FmtSubscriber::builder()
186 .with_test_writer()
187 .try_init();
188 let start = datetime!(2023- 10 - 10 00:00 UTC);
189
190 let bytes = zstd::encode_all(
191 io::Cursor::new(concat!(
192 r#"{"security_id": "S-1318698","#,
193 r#""event_id": "E-3287361-DIV","#,
194 r#""event": "DIV","#,
195 r#""issuer_name": "VanEck ETF Trust","#,
196 r#""security_type": "ETF","#,
197 r#""primary_exchange": "USBATS","#,
198 r#""exchange": "USBATS","#,
199 r#""operating_mic": "BATS","#,
200 r#""symbol": "HYD","#,
201 r#""nasdaq_symbol": "HYD","#,
202 r#""local_code": "HYD","#,
203 r#""local_code_resulting": null,"#,
204 r#""isin": "US92189H4092","#,
205 r#""isin_resulting": null,"#,
206 r#""us_code": "92189H409","#,
207 r#""status": "A","#,
208 r#""ex_date": "2024-05-01","#,
209 r#""factor": 0.995833170541121,"#,
210 r#""close": 51.19,"#,
211 r#""currency": "USD","#,
212 r#""sentiment": 0.998241844110178,"#,
213 r#""reason": 17,"#,
214 r#""gross_dividend": 0.2133,"#,
215 r#""dividend_currency": "USD","#,
216 r#""frequency": "MNT","#,
217 r#""option": 1,"#,
218 r#""detail": "INT Dividend (cash) of USD0.2133/ETF","#,
219 r#""ts_created": "1970-01-01T00:00:00.000000000Z"}
220"#,
221 )),
222 0,
223 )
224 .unwrap();
225
226 let mock_server = MockServer::start().await;
227 Mock::given(method("POST"))
228 .and(basic_auth(API_KEY, ""))
229 .and(path(format!(
230 "/v{API_VERSION}/adjustment_factors.get_range"
231 )))
232 .and(body_contains(
233 "start",
234 start.unix_timestamp_nanos().to_string(),
235 ))
236 .and(body_contains("stype_in", "raw_symbol"))
237 .and(body_contains("symbols", "MSFT"))
238 .and(body_contains("security_types", "EQS"))
239 .respond_with(ResponseTemplate::new(StatusCode::OK.as_u16()).set_body_bytes(bytes))
240 .mount(&mock_server)
241 .await;
242
243 let mut client = client(&mock_server);
244 let res = client
245 .adjustment_factors()
246 .get_range(
247 &GetRangeParams::builder()
248 .start(start)
249 .security_types([SecurityType::Eqs])
250 .countries([Country::Us])
251 .symbols("MSFT")
252 .build(),
253 )
254 .await
255 .unwrap();
256 assert_eq!(res.len(), 1);
257 let res = &res[0];
258 assert_eq!(res.event, Event::Div);
259 assert_eq!(res.security_type, SecurityType::Etf);
260 assert_eq!(res.status, AdjustmentStatus::Apply);
261 assert_eq!(res.ex_date, date!(2024 - 05 - 01));
262 assert_eq!(res.dividend_currency, Some(Currency::Usd));
263 assert_eq!(res.frequency, Some(Frequency::Monthly));
264 }
265}