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
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
//! Compile and runtime tests for docs/library/dataframe.md
//!
//! Requires the `dataframe` feature flag:
//!   cargo test --test doc_dataframe --features dataframe
//!   cargo test --test doc_dataframe --features dataframe -- --ignored   (network tests)
//!
//! Skipped sections (require additional Polars features not enabled by `dataframe`):
//!   - "CSV Export"     — requires polars/csv feature
//!   - "Parquet Export" — requires polars/parquet feature
//!   - "JSON Export"    — requires polars/json feature
//!   - "Rolling Windows" — RollingOptionsFixedWindow not in polars/lazy
//!
//! Polars operations using the older Series/Column direct arithmetic API (noted in the
//! doc as "older API style") are expressed via the equivalent lazy API.

#![cfg(feature = "dataframe")]

use finance_query::{Interval, TimeRange};

// ---------------------------------------------------------------------------
// Network tests — Chart Data (dataframe.md "Chart Data" section)
// ---------------------------------------------------------------------------

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

    // From dataframe.md "Chart Data" section
    let ticker = Ticker::new("AAPL").await.unwrap();
    let chart = ticker
        .chart(Interval::OneDay, TimeRange::OneMonth)
        .await
        .unwrap();

    // Convert to DataFrame
    let df = chart.to_dataframe().unwrap();
    println!("{}", df);

    // Verify documented columns exist
    assert!(df.column("timestamp").is_ok());
    assert!(df.column("open").is_ok());
    assert!(df.column("high").is_ok());
    assert!(df.column("low").is_ok());
    assert!(df.column("close").is_ok());
    assert!(df.column("volume").is_ok());
    assert!(df.height() > 0);
}

// ---------------------------------------------------------------------------
// Network tests — Quote Data (dataframe.md "Quote Data" section)
// ---------------------------------------------------------------------------

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

    // From dataframe.md "Quote Data" section
    let ticker = Ticker::new("NVDA").await.unwrap();
    let quote = ticker.quote().await.unwrap();

    // Convert to single-row DataFrame
    let df = quote.to_dataframe().unwrap();
    println!("{}", df);

    assert_eq!(df.height(), 1);
}

// ---------------------------------------------------------------------------
// Network tests — Corporate Events (dataframe.md "Corporate Events" section)
// ---------------------------------------------------------------------------

#[tokio::test]
#[ignore = "requires network access"]
async fn test_corporate_events_to_dataframe() {
    use finance_query::{CapitalGain, Dividend, Split, Ticker, TimeRange};

    // From dataframe.md "Corporate Events" section
    let ticker = Ticker::new("AAPL").await.unwrap();

    // Dividends
    let dividends = ticker.dividends(TimeRange::OneYear).await.unwrap();
    let div_df = Dividend::vec_to_dataframe(&dividends).unwrap();
    // Columns: timestamp, amount
    assert!(div_df.column("timestamp").is_ok());
    assert!(div_df.column("amount").is_ok());

    // Splits
    let splits = ticker.splits(TimeRange::Max).await.unwrap();
    let split_df = Split::vec_to_dataframe(&splits).unwrap();
    // Columns: timestamp, ratio
    assert!(split_df.column("timestamp").is_ok());
    assert!(split_df.column("ratio").is_ok());

    // Capital gains
    let gains = ticker.capital_gains(TimeRange::FiveYears).await.unwrap();
    let gains_df = CapitalGain::vec_to_dataframe(&gains).unwrap();
    // Columns: timestamp, amount
    assert!(gains_df.column("timestamp").is_ok());
    assert!(gains_df.column("amount").is_ok());
}

// ---------------------------------------------------------------------------
// Network tests — Screener Results (dataframe.md "Screener Results" section)
// ---------------------------------------------------------------------------

#[tokio::test]
#[ignore = "requires network access"]
async fn test_screener_to_dataframe() {
    use finance_query::{Screener, finance};

    // From dataframe.md "Screener Results" section
    let gainers = finance::screener(Screener::DayGainers, 50).await.unwrap();

    // Convert to DataFrame
    let df = gainers.to_dataframe().unwrap();
    println!("{}", df);

    assert!(df.height() > 0);
}

// ---------------------------------------------------------------------------
// Network tests — Indicators (dataframe.md "Indicators" section)
// Requires both `dataframe` and `indicators` features.
// ---------------------------------------------------------------------------

#[cfg(feature = "indicators")]
#[tokio::test]
#[ignore = "requires network access"]
async fn test_indicators_to_dataframe() {
    use finance_query::Ticker;

    // From dataframe.md "Indicators" section
    let ticker = Ticker::new("TSLA").await.unwrap();
    let indicators = ticker
        .indicators(Interval::OneDay, TimeRange::ThreeMonths)
        .await
        .unwrap();

    // Convert to single-row DataFrame with all 52 indicators
    let df = indicators.to_dataframe().unwrap();

    // Access specific indicators
    println!("RSI(14): {:?}", df.column("rsi_14").unwrap());
    // Note: macd is Option<MacdData> (nested struct), skipped by ToDataFrame derive.
    // Use scalar indicators instead:
    println!("ADX(14): {:?}", df.column("adx_14").unwrap());
}

// ---------------------------------------------------------------------------
// Network tests — Filtering (dataframe.md "Filtering Data" section)
// ---------------------------------------------------------------------------

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

    // From dataframe.md "Filtering Data" section
    let ticker = Ticker::new("AAPL").await.unwrap();
    let chart = ticker
        .chart(Interval::OneDay, TimeRange::SixMonths)
        .await
        .unwrap();
    let df = chart.to_dataframe().unwrap();

    // For filtering with current Polars API, see Polars documentation
    // Older API example (may need updates):
    // let high_volume = df.filter(&df.column("volume")?.gt(50_000_000)?)?;
    println!("Total days: {}", df.height());

    assert!(df.height() > 0);
}

// ---------------------------------------------------------------------------
// Network tests — Computing Statistics (dataframe.md "Computing Statistics" section)
// ---------------------------------------------------------------------------

#[tokio::test(flavor = "multi_thread")]
#[ignore = "requires network access"]
async fn test_polars_statistics() {
    use finance_query::Ticker;
    use polars::prelude::*;

    // From dataframe.md "Computing Statistics" section
    let ticker = Ticker::new("AAPL").await.unwrap();
    let chart = ticker
        .chart(Interval::OneDay, TimeRange::SixMonths)
        .await
        .unwrap();
    let df = chart.to_dataframe().unwrap();

    // Calculate average closing price, max high, min low via lazy API
    // (Polars 0.53 + lazy feature: Column::mean/max/min require lazy for f64 extraction)
    let stats = df
        .clone()
        .lazy()
        .select([
            col("close").mean().alias("avg_close"),
            col("high").max().alias("max_high"),
            col("low").min().alias("min_low"),
        ])
        .collect()
        .unwrap();

    let avg_close: f64 = stats
        .column("avg_close")
        .unwrap()
        .f64()
        .unwrap()
        .get(0)
        .unwrap();
    let max_high: f64 = stats
        .column("max_high")
        .unwrap()
        .f64()
        .unwrap()
        .get(0)
        .unwrap();
    let min_low: f64 = stats
        .column("min_low")
        .unwrap()
        .f64()
        .unwrap()
        .get(0)
        .unwrap();
    println!("Average close: ${:.2}", avg_close);
    println!("Range: ${:.2} - ${:.2}", min_low, max_high);

    assert!(avg_close > 0.0);
    assert!(max_high >= min_low);
}

// ---------------------------------------------------------------------------
// Network tests — Adding Calculated Columns (dataframe.md "Adding Calculated Columns")
// Doc uses outdated Series arithmetic; expressed via equivalent lazy API.
// ---------------------------------------------------------------------------

#[tokio::test(flavor = "multi_thread")]
#[ignore = "requires network access"]
async fn test_polars_calculated_columns() {
    use finance_query::Ticker;
    use polars::prelude::*;

    // From dataframe.md "Adding Calculated Columns" section
    let ticker = Ticker::new("AAPL").await.unwrap();
    let chart = ticker
        .chart(Interval::OneDay, TimeRange::OneMonth)
        .await
        .unwrap();
    let df = chart.to_dataframe().unwrap();

    // Doc shows (older API):
    //   let close = df.column("close")?;
    //   let prev_close = close.shift(1);
    //   let daily_return = ((close - &prev_close) / &prev_close) * lit(100.0);
    //   df.with_column(daily_return.alias("daily_return_pct"))?;
    //
    // Near-exact equivalent using lazy API (Polars 0.53):
    let df = df
        .lazy()
        .with_column(
            ((col("close") - col("close").shift(lit(1))) / col("close").shift(lit(1)) * lit(100.0))
                .alias("daily_return_pct"),
        )
        .collect()
        .unwrap();

    assert!(df.column("daily_return_pct").is_ok());
}

// ---------------------------------------------------------------------------
// Network tests — Time-based Operations (dataframe.md "Time-based Operations")
// Doc uses outdated Column::gt_eq(); expressed via lazy filter.
// ---------------------------------------------------------------------------

#[tokio::test(flavor = "multi_thread")]
#[ignore = "requires network access"]
async fn test_polars_time_based_operations() {
    use chrono::DateTime;
    use finance_query::Ticker;
    use polars::prelude::*;

    // From dataframe.md "Time-based Operations" section
    let ticker = Ticker::new("AAPL").await.unwrap();
    let chart = ticker
        .chart(Interval::OneDay, TimeRange::OneYear)
        .await
        .unwrap();
    let df = chart.to_dataframe().unwrap();

    // Convert timestamp to datetime
    let dates: Vec<_> = df
        .column("timestamp")
        .unwrap()
        .i64()
        .unwrap()
        .into_iter()
        .map(|ts| ts.map(|t| DateTime::from_timestamp(t, 0).unwrap()))
        .collect();
    assert!(!dates.is_empty());

    // Filter by date range (doc shows older API: df.column("timestamp")?.gt_eq(start_ts)?)
    // Near-exact equivalent using lazy filter:
    let start_ts = 1704067200i64; // 2024-01-01
    let df_filtered = df
        .lazy()
        .filter(col("timestamp").gt_eq(lit(start_ts)))
        .collect()
        .unwrap();

    println!("Filtered rows: {}", df_filtered.height());
}

// ---------------------------------------------------------------------------
// Network tests — Sorting and Ranking (dataframe.md "Sorting and Ranking" section)
// ---------------------------------------------------------------------------

#[tokio::test]
#[ignore = "requires network access"]
async fn test_polars_sorting_and_ranking() {
    use finance_query::{Screener, finance};
    use polars::prelude::*;

    // From dataframe.md "Sorting and Ranking" section
    let gainers = finance::screener(Screener::DayGainers, 100).await.unwrap();

    let mut df = gainers.to_dataframe().unwrap();

    // Sort by market cap descending
    df = df
        .sort(
            ["market_cap"],
            SortMultipleOptions::default().with_order_descending(true),
        )
        .unwrap();

    // Get top 10
    let top_10 = df.head(Some(10));
    println!("{}", top_10);

    assert!(top_10.height() <= 10);
}

// ---------------------------------------------------------------------------
// Network tests — Aggregations (dataframe.md "Aggregations" section)
// Doc's select-then-group_by pattern adapted to correct group_by-then-agg.
// ---------------------------------------------------------------------------

#[tokio::test(flavor = "multi_thread")]
#[ignore = "requires network access"]
async fn test_polars_aggregations() {
    use finance_query::Ticker;
    use polars::prelude::*;

    // From dataframe.md "Aggregations" section
    let ticker = Ticker::new("AAPL").await.unwrap();
    let chart = ticker
        .chart(Interval::OneDay, TimeRange::OneYear)
        .await
        .unwrap();
    let df = chart.to_dataframe().unwrap();

    // Group by month and aggregate (near-exact: corrected select→group_by ordering)
    let monthly = df
        .lazy()
        .with_column((col("timestamp") / lit(86400i64 * 30i64)).alias("month"))
        .group_by([col("month")])
        .agg([
            col("close").mean().alias("avg_close"),
            col("volume").sum().alias("total_volume"),
            col("high").max().alias("max_high"),
            col("low").min().alias("min_low"),
        ])
        .collect()
        .unwrap();

    println!("{}", monthly);
    assert!(monthly.height() > 0);
}

// ---------------------------------------------------------------------------
// Network tests — Multiple Symbols (dataframe.md "Multiple Symbols" section)
// Doc passes DataFrame slice to concat(); adapted to lazy concat for Polars 0.53.
// ---------------------------------------------------------------------------

#[tokio::test(flavor = "multi_thread")]
#[ignore = "requires network access"]
async fn test_multiple_symbols_concat() {
    use finance_query::Ticker;
    use polars::prelude::*;

    // From dataframe.md "Multiple Symbols" section
    let aapl = Ticker::new("AAPL").await.unwrap();
    let msft = Ticker::new("MSFT").await.unwrap();
    let nvda = Ticker::new("NVDA").await.unwrap();

    let aapl_chart = aapl
        .chart(Interval::OneDay, TimeRange::OneMonth)
        .await
        .unwrap();
    let msft_chart = msft
        .chart(Interval::OneDay, TimeRange::OneMonth)
        .await
        .unwrap();
    let nvda_chart = nvda
        .chart(Interval::OneDay, TimeRange::OneMonth)
        .await
        .unwrap();

    // Convert to DataFrames
    let mut aapl_df = aapl_chart.to_dataframe().unwrap();
    let mut msft_df = msft_chart.to_dataframe().unwrap();
    let mut nvda_df = nvda_chart.to_dataframe().unwrap();

    // Add symbol column to each (Series::new requires .into() → Column in Polars 0.53)
    aapl_df
        .with_column(Series::new("symbol".into(), vec!["AAPL"; aapl_df.height()]).into())
        .unwrap();
    msft_df
        .with_column(Series::new("symbol".into(), vec!["MSFT"; msft_df.height()]).into())
        .unwrap();
    nvda_df
        .with_column(Series::new("symbol".into(), vec!["NVDA"; nvda_df.height()]).into())
        .unwrap();

    // Combine into single DataFrame (doc shows concat(&[dfs], UnionArgs::default());
    // adapted for Polars 0.53 lazy concat)
    let combined = concat(
        [aapl_df.lazy(), msft_df.lazy(), nvda_df.lazy()],
        UnionArgs::default(),
    )
    .unwrap()
    .collect()
    .unwrap();

    println!("Combined data: {} rows", combined.height());
    assert!(combined.height() > 0);
}

// ---------------------------------------------------------------------------
// Network tests — Joining DataFrames (dataframe.md "Joining DataFrames" section)
// Doc notes the join API may require additional trait imports; tests DataFrame
// creation only — the commented join syntax is preserved.
// ---------------------------------------------------------------------------

#[tokio::test]
#[ignore = "requires network access"]
async fn test_polars_joining_dataframes() {
    use finance_query::{Dividend, Ticker, TimeRange};

    // From dataframe.md "Joining DataFrames" section
    let ticker = Ticker::new("AAPL").await.unwrap();
    let aapl_chart = ticker
        .chart(Interval::OneDay, TimeRange::OneMonth)
        .await
        .unwrap();
    let aapl_divs = ticker.dividends(TimeRange::OneMonth).await.unwrap();

    let price_df = aapl_chart.to_dataframe().unwrap();
    let div_df = Dividend::vec_to_dataframe(&aapl_divs).unwrap();

    // Note: left_join API may require trait imports in newer Polars versions
    // Example: use polars_ops::frame::join::DataFrameJoinOps;
    // let joined = price_df.left_join(&div_df, ["timestamp"], ["timestamp"])?;
    assert!(price_df.column("timestamp").is_ok());
    assert!(div_df.column("timestamp").is_ok());
}

// ---------------------------------------------------------------------------
// Network tests — Custom Analysis (dataframe.md "Custom Analysis" section)
// ---------------------------------------------------------------------------

#[tokio::test(flavor = "multi_thread")]
#[ignore = "requires network access"]
async fn test_polars_custom_analysis() {
    use finance_query::{Screener, Ticker, finance};
    use polars::prelude::*;

    // From dataframe.md "Custom Analysis" section
    let ticker = Ticker::new("AAPL").await.unwrap();
    let chart = ticker
        .chart(Interval::OneDay, TimeRange::OneMonth)
        .await
        .unwrap();
    let df = chart.to_dataframe().unwrap();

    // Calculate daily price range as percentage
    let range_pct = df
        .clone()
        .lazy()
        .select([
            col("timestamp"),
            ((col("high") - col("low")) / col("close") * lit(100.0)).alias("range_pct"),
        ])
        .collect()
        .unwrap();

    // Find days with highest volatility
    let volatile_days = range_pct
        .sort(
            ["range_pct"],
            SortMultipleOptions::default().with_order_descending(true),
        )
        .unwrap()
        .head(Some(10));

    println!("Most volatile days:\n{}", volatile_days);
    assert!(volatile_days.height() <= 10);

    // Also cover the screener sort pattern from "Sorting and Ranking" with finance module
    let _ = finance::screener(Screener::DayGainers, 100).await.unwrap();
}

// ---------------------------------------------------------------------------
// Network tests — Vec to DataFrame (dataframe.md "Vec to DataFrame" section)
// ---------------------------------------------------------------------------

#[tokio::test]
#[ignore = "requires network access"]
async fn test_vec_to_dataframe() {
    use finance_query::{Dividend, SearchOptions, Ticker, TimeRange, finance};

    // From dataframe.md "Type Conversions — Vec to DataFrame" section

    // Vec of dividends to DataFrame
    let ticker = Ticker::new("AAPL").await.unwrap();
    let dividends = ticker.dividends(TimeRange::FiveYears).await.unwrap();
    let df = Dividend::vec_to_dataframe(&dividends).unwrap();
    assert!(df.column("timestamp").is_ok());

    // SearchQuotes wrapper has to_dataframe() method
    let results = finance::search("tech", &SearchOptions::default())
        .await
        .unwrap();
    let df = results.quotes.to_dataframe().unwrap();
    assert!(df.height() > 0);
}

// ---------------------------------------------------------------------------
// Network tests — Single Item to DataFrame (dataframe.md "Single Item to DataFrame")
// ---------------------------------------------------------------------------

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

    // From dataframe.md "Single Item to DataFrame" section
    let ticker = Ticker::new("AAPL").await.unwrap();
    let quote = ticker.quote().await.unwrap();
    let df = quote.to_dataframe().unwrap(); // 1 row, 30+ columns

    assert_eq!(df.height(), 1);
    assert!(df.width() >= 30);
}

// ---------------------------------------------------------------------------
// Network tests — Error Handling (dataframe.md "Error Handling" section)
// ---------------------------------------------------------------------------

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

    // From dataframe.md "Error Handling" section
    let ticker = Ticker::new("AAPL").await.unwrap();
    let chart = ticker
        .chart(Interval::OneDay, TimeRange::OneMonth)
        .await
        .unwrap();

    match chart.to_dataframe() {
        Ok(df) => {
            println!("DataFrame created: {} rows", df.height());
            assert!(df.height() > 0);
        }
        Err(e) => {
            eprintln!("DataFrame conversion error: {}", e);
            panic!("Expected successful conversion");
        }
    }
}

// ---------------------------------------------------------------------------
// Network tests — Best Practices (dataframe.md "Best Practices" section)
// df.filter() older API expressed via lazy; df.tail() is stable.
// ---------------------------------------------------------------------------

#[tokio::test(flavor = "multi_thread")]
#[ignore = "requires network access"]
async fn test_best_practices_cache_and_tail() {
    use finance_query::Ticker;
    use polars::prelude::*;

    // From dataframe.md "Best Practices — Combine with Ticker Caching" section
    let ticker = Ticker::new("AAPL").await.unwrap();
    let chart = ticker
        .chart(Interval::OneDay, TimeRange::OneMonth)
        .await
        .unwrap();

    // Convert to DataFrame for analysis
    let df = chart.to_dataframe().unwrap();

    // Reuse the same chart data for different analyses
    // Doc shows: df.filter(&df.column("volume")?.gt(50_000_000)?)?  (older API)
    // Near-exact equivalent using lazy filter:
    let high_volume = df
        .clone()
        .lazy()
        .filter(col("volume").gt(lit(50_000_000i64)))
        .collect()
        .unwrap();
    let recent = df.tail(Some(5));

    println!("High volume days: {}", high_volume.height());
    println!("Recent 5 days:\n{}", recent);

    // No additional API calls - data is cached in the Ticker
    assert!(recent.height() <= 5);
}