finance-query 2.5.1

A Rust library for querying financial data
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
//! Compile and runtime tests for docs/library/edgar.md
//!
//! Run compile tests:
//!   cargo test --test doc_edgar
//! Run network tests:
//!   cargo test --test doc_edgar -- --ignored
//!
//! Known doc/source discrepancies corrected in tests:
//!   - edgar::search takes 6 params (query, forms, start_date, end_date, from, size);
//!     docs show only 4 (missing `from` and `size`).
//!   - EdgarSearchSource::display_names is Vec<String>, not Option<Vec<String>>;
//!     docs incorrectly show `if let Some(names) = &source.display_names`.
//!   - Complete example's `hits.total.value` skips two Option layers;
//!     corrected to use Option chaining.

use std::time::Duration;

// ---------------------------------------------------------------------------
// Compile-time — initialization API (edgar.md "Initialization" section)
// ---------------------------------------------------------------------------

#[test]
fn test_init_compiles() {
    use finance_query::edgar;

    // From edgar.md "Basic Initialization" section
    // Returns Err if already initialized — safe to ignore in tests.
    let _ = edgar::init("user@example.com");
}

#[test]
fn test_init_with_config_compiles() {
    use finance_query::edgar;

    // From edgar.md "Advanced Configuration" section
    // Returns Err if already initialized — safe to ignore in tests.
    let _ = edgar::init_with_config(
        "user@example.com",
        "my-financial-app",      // Optional: default is "finance-query"
        Duration::from_secs(60), // Optional: default is 30 seconds
    );
}

// ---------------------------------------------------------------------------
// Network tests — Rate Limiting (edgar.md "Rate Limiting" section)
// ---------------------------------------------------------------------------

#[tokio::test]
#[ignore = "requires network access"]
async fn test_resolve_cik_rate_limiting() {
    use finance_query::edgar;

    let _ = edgar::init("user@example.com");

    // From edgar.md "Rate Limiting" section
    // These requests are automatically rate-limited
    let cik1 = edgar::resolve_cik("AAPL").await.unwrap();
    let cik2 = edgar::resolve_cik("MSFT").await.unwrap();
    let cik3 = edgar::resolve_cik("GOOGL").await.unwrap();
    // Executed at max 10 req/sec automatically

    assert!(cik1 > 0);
    assert!(cik2 > 0);
    assert!(cik3 > 0);
}

// ---------------------------------------------------------------------------
// Network tests — CIK Resolution (edgar.md "Ticker to CIK Resolution" section)
// ---------------------------------------------------------------------------

#[tokio::test]
#[ignore = "requires network access"]
async fn test_resolve_cik() {
    use finance_query::edgar;

    let _ = edgar::init("user@example.com");

    // From edgar.md "Ticker to CIK Resolution" section

    // Resolve ticker to CIK (cached after first fetch)
    let cik = edgar::resolve_cik("AAPL").await.unwrap();
    println!("Apple CIK: {}", cik); // Output: Apple CIK: 320193
    assert_eq!(cik, 320193);

    // Subsequent lookups use the cache (no network request)
    let cik2 = edgar::resolve_cik("AAPL").await.unwrap(); // Instant
    assert_eq!(cik, cik2);

    // Case-insensitive lookup
    let cik3 = edgar::resolve_cik("aapl").await.unwrap(); // Also works
    assert_eq!(cik, cik3);
}

// ---------------------------------------------------------------------------
// Network tests — Filing History (edgar.md "Filing History" section)
// ---------------------------------------------------------------------------

#[tokio::test]
#[ignore = "requires network access"]
async fn test_submissions_filing_history() {
    use finance_query::edgar;

    let _ = edgar::init("user@example.com");

    // From edgar.md "Filing History (Submissions)" section

    // Get CIK first
    let cik = edgar::resolve_cik("AAPL").await.unwrap();

    // Fetch filing history
    let submissions = edgar::submissions(cik).await.unwrap();

    // Company information
    if let Some(name) = &submissions.name {
        println!("Company: {}", name);
    }
    if let Some(cik_str) = &submissions.cik {
        println!("CIK: {}", cik_str);
    }
    if let Some(sic) = &submissions.sic {
        println!("SIC: {}", sic);
    }
    if let Some(fiscal_year_end) = &submissions.fiscal_year_end {
        println!("Fiscal Year End: {}", fiscal_year_end);
    }

    // Recent filings
    if let Some(filings) = &submissions.filings
        && let Some(recent) = &filings.recent
    {
        for i in 0..5.min(recent.accession_number.len()) {
            let form = &recent.form[i];
            let date = &recent.filing_date[i];
            let accession = &recent.accession_number[i];

            println!("{} filed on {}: {}", form, date, accession);
        }
        assert!(!recent.accession_number.is_empty());
    }

    assert!(submissions.name.is_some());
}

// ---------------------------------------------------------------------------
// Network tests — Company Facts (edgar.md "Company Facts (XBRL Data)" section)
// ---------------------------------------------------------------------------

#[tokio::test]
#[ignore = "requires network access"]
async fn test_company_facts_xbrl() {
    use finance_query::edgar;

    let _ = edgar::init("user@example.com");

    // From edgar.md "Company Facts (XBRL Data)" section

    // Get CIK
    let cik = edgar::resolve_cik("AAPL").await.unwrap();

    // Fetch company facts
    let facts = edgar::company_facts(cik).await.unwrap();

    // Access financial data by taxonomy and concept
    if let Some(us_gaap) = facts.facts.get("us-gaap") {
        // Revenue data (FactsByTaxonomy is a tuple struct, access with .0)
        if let Some(revenue) = us_gaap.0.get("Revenues") {
            if let Some(label) = &revenue.label {
                println!("Revenue concept: {}", label);
            }
            if let Some(description) = &revenue.description {
                println!("Description: {}", description);
            }

            // Access data points by unit (e.g., USD)
            if let Some(usd_data) = revenue.units.get("USD") {
                for point in usd_data.iter().take(5) {
                    if let (Some(fy), Some(val)) = (point.fy, point.val) {
                        println!("FY {}: ${}", fy, val);
                    }
                }
            }
        }

        // Assets data
        if let Some(assets) = us_gaap.0.get("Assets")
            && let Some(usd_data) = assets.units.get("USD")
        {
            for point in usd_data.iter().take(5) {
                if let (Some(fy), Some(val)) = (point.fy, point.val) {
                    println!("FY {}: ${}", fy, val);
                }
            }
        }
    }

    assert!(!facts.facts.is_empty());
}

// ---------------------------------------------------------------------------
// Network tests — Basic Search (edgar.md "Full-Text Search" section)
// ---------------------------------------------------------------------------

#[tokio::test]
#[ignore = "requires network access"]
async fn test_search_basic() {
    use finance_query::edgar;

    let _ = edgar::init("user@example.com");

    // From edgar.md "Full-Text Search" section
    //
    // Note: edgar::search takes 6 params (query, forms, start_date, end_date, from, size).
    // Doc shows 4 params — `from` and `size` are missing from the docs.
    let results = edgar::search(
        "artificial intelligence",
        None, // No form filter
        None, // No start date
        None, // No end date
        None, // from (offset)
        None, // size (limit)
    )
    .await
    .unwrap();

    // Display results
    if let Some(hits) = &results.hits {
        if let Some(total) = &hits.total
            && let Some(value) = total.value
        {
            println!("Total hits: {}", value);
        }

        for hit in &hits.hits {
            if let Some(source) = &hit._source {
                let form = source.form.as_deref().unwrap_or("Unknown");
                let file_date = source.file_date.as_deref().unwrap_or("Unknown");

                println!("{} filed on {}", form, file_date);

                // Note: display_names is Vec<String>, not Option<Vec<String>>.
                // Doc shows `if let Some(names)` which won't compile; corrected here:
                if !source.display_names.is_empty() {
                    println!("  Companies: {:?}", source.display_names);
                }
            }
        }
    }

    assert!(results.hits.is_some());
}

// ---------------------------------------------------------------------------
// Network tests — Filtered Search (edgar.md "Filtered Search" section)
// ---------------------------------------------------------------------------

#[tokio::test]
#[ignore = "requires network access"]
async fn test_search_filtered() {
    use finance_query::edgar;

    let _ = edgar::init("user@example.com");

    // From edgar.md "Filtered Search" section
    // Search for 10-K filings only
    let results = edgar::search(
        "machine learning",
        Some(&["10-K"]),    // Only 10-K forms
        Some("2024-01-01"), // From Jan 1, 2024
        Some("2024-12-31"), // To Dec 31, 2024
        None,
        None,
    )
    .await
    .unwrap();

    assert!(results.hits.is_some());
}

// ---------------------------------------------------------------------------
// Network tests — Common Form Filters (edgar.md "Common Form Filters" section)
// ---------------------------------------------------------------------------

#[tokio::test]
#[ignore = "requires network access"]
async fn test_search_common_form_filters() {
    use finance_query::edgar;

    let _ = edgar::init("user@example.com");

    // From edgar.md "Common Form Filters" section

    // Annual reports
    edgar::search("query", Some(&["10-K"]), None, None, None, None)
        .await
        .unwrap();

    // Quarterly reports
    edgar::search("query", Some(&["10-Q"]), None, None, None, None)
        .await
        .unwrap();

    // Current events
    edgar::search("query", Some(&["8-K"]), None, None, None, None)
        .await
        .unwrap();

    // Multiple form types
    edgar::search(
        "query",
        Some(&["10-K", "10-Q", "8-K"]),
        None,
        None,
        None,
        None,
    )
    .await
    .unwrap();
}

// ---------------------------------------------------------------------------
// Network tests — Complete Example (edgar.md "Complete Example" section)
// ---------------------------------------------------------------------------

#[tokio::test]
#[ignore = "requires network access"]
async fn test_complete_example() {
    use finance_query::edgar;

    // From edgar.md "Complete Example" section
    let ticker = "AAPL";

    // Create EDGAR client
    let _ = edgar::init("user@example.com");

    // Step 1: Resolve ticker to CIK
    println!("Resolving {} to CIK...", ticker);
    let cik = edgar::resolve_cik(ticker).await.unwrap();
    println!("CIK: {}\n", cik);

    // Step 2: Get filing history
    println!("Fetching filing history...");
    let submissions = edgar::submissions(cik).await.unwrap();
    if let Some(name) = &submissions.name {
        println!("Company: {}", name);
    }
    if let Some(sic_description) = &submissions.sic_description {
        println!("Industry: {}", sic_description);
    }

    // Show recent 10-K and 10-Q filings
    if let Some(filings) = &submissions.filings
        && let Some(recent) = &filings.recent
    {
        println!("\nRecent filings:");
        for i in 0..10.min(recent.form.len()) {
            let form = &recent.form[i];
            if form == "10-K" || form == "10-Q" {
                let date = &recent.filing_date[i];
                println!("  {} filed on {}", form, date);
            }
        }
    }

    // Step 3: Get company facts (XBRL data)
    println!("\nFetching XBRL financial data...");
    let facts = edgar::company_facts(cik).await.unwrap();

    if let Some(us_gaap) = facts.facts.get("us-gaap") {
        // Show revenue trend (FactsByTaxonomy is a tuple struct, access with .0)
        if let Some(revenue) = us_gaap.0.get("Revenues")
            && let Some(usd) = revenue.units.get("USD")
        {
            println!("\nRevenue Trend:");
            for point in usd.iter().take(5) {
                if let (Some(fy), Some(val)) = (point.fy, point.val) {
                    println!("  FY {}: ${:>15}", fy, val);
                }
            }
        }

        // Show assets
        if let Some(assets) = us_gaap.0.get("Assets")
            && let Some(usd) = assets.units.get("USD")
        {
            println!("\nAssets:");
            for point in usd.iter().take(3) {
                if let (Some(fy), Some(val)) = (point.fy, point.val) {
                    println!("  FY {}: ${:>15}", fy, val);
                }
            }
        }
    }

    // Step 4: Search for AI mentions in recent filings
    println!("\nSearching for 'artificial intelligence' mentions...");
    let search_results = edgar::search(
        "artificial intelligence",
        Some(&["10-K", "10-Q"]),
        Some("2024-01-01"),
        None,
        None,
        None,
    )
    .await
    .unwrap();
    // Note: doc shows `hits.total.value` directly, but total is Option<EdgarSearchTotal>
    // and value is Option<u64>. Corrected to use Option chaining:
    if let Some(hits) = &search_results.hits {
        let count = hits.total.as_ref().and_then(|t| t.value).unwrap_or(0);
        println!("Found {} mentions", count);
    }

    assert!(cik > 0);
    assert!(facts.facts.contains_key("us-gaap"));
}