DataFrame Support#
abstract · Cargo Docs
Finance Query provides optional Polars DataFrame conversion for data analysis workflows.
warning · Feature Flag Required
DataFrame support requires the dataframe feature flag. Add it to your Cargo.toml:
[dependencies]
finance-query = { version = "3", features = ["dataframe"] }
polars = "0.53"Overview#
The dataframe feature enables .to_dataframe() methods on many response types, converting them into Polars DataFrames for powerful data manipulation and analysis.
Supported Types:
- Charts -
Chart,Candle - Quotes -
Quote, market summary quotes, trending quotes - Corporate Events -
Dividend,Split,CapitalGain - Screeners - Screener results
- Search & Lookup - Search results, lookup results
- Options - Options contracts, options chains
- Recommendations - Recommended symbols
- Sector & Industry - Company lists, ETFs, performance data
- News - News articles
- Indicators - Technical indicators summary
- Market Data - Exchanges, currencies, market hours
The Polars conversion path is deliberately outside the measured performance gate — DataFrame construction cost is dominated by Polars itself, not this crate.
Basic Usage#
Chart Data#
Convert historical OHLCV data to DataFrame:
use finance_query::{Interval, Ticker, TimeRange};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let ticker = Ticker::new("AAPL").await?;
let chart = ticker.chart(Interval::OneDay, TimeRange::OneMonth).await?;
// Convert to DataFrame
let df = chart.to_dataframe()?;
println!("{}", df);
Ok(())
}
recorded outputcargo soothfast docs capture
shape: (23, 7) ┌────────────┬────────────┬────────────┬────────────┬────────────┬──────────┬────────────┐ │ timestamp ┆ open ┆ high ┆ low ┆ close ┆ volume ┆ adj_close │ │ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │ │ i64 ┆ f64 ┆ f64 ┆ f64 ┆ f64 ┆ i64 ┆ f64 │ ╞════════════╪════════════╪════════════╪════════════╪════════════╪══════════╪════════════╡ │ 1783517400 ┆ 311.910004 ┆ 314.820007 ┆ 307.049988 ┆ 313.390015 ┆ 41323500 ┆ 313.390015 │ │ 1783603800 ┆ 310.51001 ┆ 316.529999 ┆ 308.160004 ┆ 316.220001 ┆ 48124500 ┆ 316.220001 │ │ 1783690200 ┆ 314.720001 ┆ 316.910004 ┆ 312.170013 ┆ 315.320007 ┆ 34132300 ┆ 315.320007 │ │ 1783949400 ┆ 317.019989 ┆ 323.450012 ┆ 315.779999 ┆ 317.309998 ┆ 43257800 ┆ 317.309998 │ │ 1784035800 ┆ 313.76001 ┆ 316.190002 ┆ 311.910004 ┆ 314.859985 ┆ 36336800 ┆ 314.859985 │ │ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … │ │ 1785763800 ┆ 309.579987 ┆ 311.799988 ┆ 302.559998 ┆ 303.420013 ┆ 75052000 ┆ 303.420013 │ │ 1785850200 ┆ 302.730011 ┆ 310.420013 ┆ 301.320007 ┆ 309.380005 ┆ 68001000 ┆ 309.380005 │ │ 1785936600 ┆ 309.359985 ┆ 311.709991 ┆ 305.670013 ┆ 311.0 ┆ 49438800 ┆ 311.0 │ │ 1786023000 ┆ 314.339996 ┆ 316.290009 ┆ 309.230011 ┆ 312.410004 ┆ 46139900 ┆ 312.410004 │ │ 1786109400 ┆ 311.450012 ┆ 314.809998 ┆ 310.73999 ┆ 313.329987 ┆ 34407100 ┆ 313.329987 │ └────────────┴────────────┴────────────┴────────────┴────────────┴──────────┴────────────┘
Chart DataFrame Columns:
timestamp(i64) - Unix timestampopen(f64) - Opening pricehigh(f64) - High pricelow(f64) - Low priceclose(f64) - Closing pricevolume(i64) - Trading volumeadj_close(Option) - Adjusted close price (accounts for splits/dividends)
Quote Data#
Single quote to DataFrame:
use finance_query::{Ticker, format::Both};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let ticker = Ticker::new("NVDA").await?;
let quote = ticker.quote::<Both>().await?;
// Convert to single-row DataFrame
let df = quote.to_dataframe()?;
println!("{}", df);
Ok(())
}
recorded outputcargo soothfast docs capture
shape: (1, 154) ┌────────┬──────────┬────────────┬────────────┬───┬────────────┬───────────┬───────────┬───────────┐ │ symbol ┆ logo_url ┆ company_lo ┆ short_name ┆ … ┆ most_recen ┆ price_hin ┆ tradeable ┆ financial │ │ --- ┆ --- ┆ go_url ┆ --- ┆ ┆ t_quarter ┆ t ┆ --- ┆ _currency │ │ str ┆ str ┆ --- ┆ str ┆ ┆ --- ┆ --- ┆ bool ┆ --- │ │ ┆ ┆ str ┆ ┆ ┆ i64 ┆ i64 ┆ ┆ str │ ╞════════╪══════════╪════════════╪════════════╪═══╪════════════╪═══════════╪═══════════╪═══════════╡ │ NVDA ┆ null ┆ null ┆ NVIDIA Cor ┆ … ┆ 1777161600 ┆ 2 ┆ false ┆ USD │ │ ┆ ┆ ┆ poration ┆ ┆ ┆ ┆ ┆ │ └────────┴──────────┴────────────┴────────────┴───┴────────────┴───────────┴───────────┴───────────┘
Quote DataFrame includes 30+ columns like:
symbol,short_name,exchangeregular_market_price,regular_market_change,regular_market_change_percentmarket_cap,volume,average_volumefifty_two_week_high,fifty_two_week_lowpe_ratio,eps,dividend_yield- And many more...
Corporate Events#
Convert dividends, splits, or capital gains to DataFrame:
use finance_query::{CapitalGain, Dividend, Split, Ticker, TimeRange};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let ticker = Ticker::new("AAPL").await?;
// Dividends
let dividends = ticker.dividends(TimeRange::OneYear).await?;
let div_df = Dividend::vec_to_dataframe(÷nds)?;
// Columns: timestamp, amount
println!("dividends: {:?}", div_df.shape());
// Splits
let splits = ticker.splits(TimeRange::Max).await?;
let split_df = Split::vec_to_dataframe(&splits)?;
// Columns: timestamp, ratio
println!("splits: {:?}", split_df.shape());
// Capital gains (AAPL is a stock, not a fund, so this is typically empty)
let gains = ticker.capital_gains(TimeRange::FiveYears).await?;
let gains_df = CapitalGain::vec_to_dataframe(&gains)?;
// Columns: timestamp, amount
println!("capital gains: {:?}", gains_df.shape());
Ok(())
}
recorded outputcargo soothfast docs capture
dividends: (4, 2) splits: (5, 4) capital gains: (0, 2)
Screener Results#
Convert screener results to DataFrame for analysis:
use finance_query::{Screener, finance};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let gainers = finance::screener(Screener::DayGainers, 50).await?;
// Convert to DataFrame
let df = gainers.to_dataframe()?;
println!("{}", df);
Ok(())
}
recorded outputcargo soothfast docs capture
shape: (50, 59) ┌────────┬────────────┬────────────┬────────────┬───┬───────────┬───────────┬───────────┬──────────┐ │ symbol ┆ short_name ┆ long_name ┆ display_na ┆ … ┆ earnings_ ┆ earnings_ ┆ earnings_ ┆ currency │ │ --- ┆ --- ┆ --- ┆ me ┆ ┆ timestamp ┆ timestamp ┆ timestamp ┆ --- │ │ str ┆ str ┆ str ┆ --- ┆ ┆ --- ┆ _start ┆ _end ┆ str │ │ ┆ ┆ ┆ str ┆ ┆ i64 ┆ --- ┆ --- ┆ │ │ ┆ ┆ ┆ ┆ ┆ ┆ i64 ┆ i64 ┆ │ ╞════════╪════════════╪════════════╪════════════╪═══╪═══════════╪═══════════╪═══════════╪══════════╡ │ TEAM ┆ Atlassian ┆ Atlassian ┆ Atlassian ┆ … ┆ 178604640 ┆ 179330400 ┆ 179330400 ┆ USD │ │ ┆ Corporatio ┆ Corporatio ┆ ┆ ┆ 0 ┆ 0 ┆ 0 ┆ │ │ ┆ n ┆ n ┆ ┆ ┆ ┆ ┆ ┆ │ │ DOCS ┆ Doximity, ┆ Doximity, ┆ Doximity ┆ … ┆ 178604640 ┆ 179390880 ┆ 179390880 ┆ USD │ │ ┆ Inc. ┆ Inc. ┆ ┆ ┆ 0 ┆ 0 ┆ 0 ┆ │ │ FIGS ┆ FIGS, Inc. ┆ FIGS, Inc. ┆ FIGS ┆ … ┆ 178604640 ┆ 179390880 ┆ 179390880 ┆ USD │ │ ┆ ┆ ┆ ┆ ┆ 0 ┆ 0 ┆ 0 ┆ │ │ TWLO ┆ Twilio ┆ Twilio ┆ Twilio ┆ … ┆ 178604640 ┆ 179330400 ┆ 179330400 ┆ USD │ │ ┆ Inc. ┆ Inc. ┆ ┆ ┆ 0 ┆ 0 ┆ 0 ┆ │ │ BTG ┆ B2Gold ┆ B2Gold ┆ B2Gold ┆ … ┆ 178604640 ┆ 179382240 ┆ 179382240 ┆ USD │ │ ┆ Corp ┆ Corp. ┆ ┆ ┆ 0 ┆ 0 ┆ 0 ┆ │ │ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … │ │ PLSE ┆ Pulse Bios ┆ Pulse Bios ┆ Pulse Bios ┆ … ┆ 178604640 ┆ 179390880 ┆ 179390880 ┆ USD │ │ ┆ ciences, ┆ ciences, ┆ ciences ┆ ┆ 0 ┆ 0 ┆ 0 ┆ │ │ ┆ Inc ┆ Inc. ┆ ┆ ┆ ┆ ┆ ┆ │ │ WGS ┆ GeneDx ┆ GeneDx ┆ GeneDx ┆ … ┆ 178578720 ┆ 179313120 ┆ 179313120 ┆ USD │ │ ┆ Holdings ┆ Holdings ┆ ┆ ┆ 0 ┆ 0 ┆ 0 ┆ │ │ ┆ Corp. ┆ Corp. ┆ ┆ ┆ ┆ ┆ ┆ │ │ ARIS ┆ Aris ┆ Aris ┆ Aris ┆ … ┆ 178535520 ┆ 179321760 ┆ 179321760 ┆ USD │ │ ┆ Mining Cor ┆ Mining Cor ┆ Mining ┆ ┆ 0 ┆ 0 ┆ 0 ┆ │ │ ┆ poration ┆ poration ┆ ┆ ┆ ┆ ┆ ┆ │ │ SSRM ┆ SSR Mining ┆ SSR Mining ┆ SSR Mining ┆ … ┆ 178587360 ┆ 179373600 ┆ 179373600 ┆ USD │ │ ┆ Inc. ┆ Inc. ┆ ┆ ┆ 0 ┆ 0 ┆ 0 ┆ │ │ ERO ┆ Ero Copper ┆ Ero Copper ┆ Ero Copper ┆ … ┆ 178596000 ┆ 179373600 ┆ 179373600 ┆ USD │ │ ┆ Corp. ┆ Corp. ┆ ┆ ┆ 0 ┆ 0 ┆ 0 ┆ │ └────────┴────────────┴────────────┴────────────┴───┴───────────┴───────────┴───────────┴──────────┘
Indicators#
note · Feature Flag Required
The indicators() method requires the indicators feature flag:
finance-query = { version = "3", features = ["dataframe", "indicators"] }Convert technical indicators to DataFrame:
use finance_query::{Interval, Ticker, TimeRange};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let ticker = Ticker::new("TSLA").await?;
let indicators = ticker
.indicators(Interval::OneDay, TimeRange::ThreeMonths)
.await?;
// Convert to single-row DataFrame with all 52 indicators
let df = indicators.to_dataframe()?;
// Access specific scalar indicators. Nested indicators (e.g. `macd`,
// an Option<MacdData>) are skipped by the derive — read them off the struct.
println!("RSI(14): {:?}", df.column("rsi_14")?);
println!("ADX(14): {:?}", df.column("adx_14")?);
Ok(())
}
recorded outputcargo soothfast docs capture
RSI(14): Scalar(ScalarColumn { name: "rsi_14", scalar: Scalar { dtype: Float64, value: Float64(47.64980926070213) }, length: 1, materialized: OnceLock(shape: (1,)
Series: 'rsi_14' [f64]
[
47.649809
]) })
ADX(14): Scalar(ScalarColumn { name: "adx_14", scalar: Scalar { dtype: Float64, value: Float64(30.896739718155665) }, length: 1, materialized: OnceLock(shape: (1,)
Series: 'adx_14' [f64]
[
30.89674
]) })Working with Polars#
The examples below use the Polars 0.53 lazy API (finance-query's dataframe
feature enables polars/lazy). For the full expression reference, see the
Polars Documentation.
Filtering Data#
use finance_query::{Interval, Ticker, TimeRange};
use polars::prelude::*;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let ticker = Ticker::new("AAPL").await?;
let chart = ticker.chart(Interval::OneDay, TimeRange::SixMonths).await?;
let df = chart.to_dataframe()?;
// Keep only high-volume days
let high_volume = df
.clone()
.lazy()
.filter(col("volume").gt(lit(50_000_000i64)))
.collect()?;
println!(
"Total days: {}, high-volume days: {}",
df.height(),
high_volume.height()
);
Ok(())
}
recorded outputcargo soothfast docs capture
Total days: 125, high-volume days: 42
Computing Statistics#
use finance_query::{Interval, Ticker, TimeRange};
use polars::prelude::*;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let ticker = Ticker::new("AAPL").await?;
let chart = ticker.chart(Interval::OneDay, TimeRange::SixMonths).await?;
let df = chart.to_dataframe()?;
// Average close, max high, min low in one pass
let stats = df
.lazy()
.select([
col("close").mean().alias("avg_close"),
col("high").max().alias("max_high"),
col("low").min().alias("min_low"),
])
.collect()?;
let avg_close: f64 = stats.column("avg_close")?.f64()?.get(0).unwrap();
let max_high: f64 = stats.column("max_high")?.f64()?.get(0).unwrap();
let min_low: f64 = stats.column("min_low")?.f64()?.get(0).unwrap();
println!("Average close: ${:.2}", avg_close);
println!("Range: ${:.2} - ${:.2}", min_low, max_high);
Ok(())
}
recorded outputcargo soothfast docs capture
Average close: $285.81 Range: $245.51 - $344.57
Adding Calculated Columns#
use finance_query::{Interval, Ticker, TimeRange};
use polars::prelude::*;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let ticker = Ticker::new("AAPL").await?;
let chart = ticker.chart(Interval::OneDay, TimeRange::OneMonth).await?;
let df = chart.to_dataframe()?;
// Add daily return column
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()?;
println!("{:?}", df.column("daily_return_pct")?);
Ok(())
}
recorded outputcargo soothfast docs capture
Series(SeriesColumn { inner: shape: (23,)
Series: 'daily_return_pct' [f64]
[
null
0.903024
-0.28461
0.631102
-0.772119
…
-1.777213
1.964271
0.523626
0.453377
0.294479
], materialized_at: None })Time-based Operations#
use chrono::DateTime;
use finance_query::{Interval, Ticker, TimeRange};
use polars::prelude::*;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let ticker = Ticker::new("AAPL").await?;
let chart = ticker.chart(Interval::OneDay, TimeRange::OneYear).await?;
let df = chart.to_dataframe()?;
// Convert timestamp to datetime
let dates: Vec<_> = df
.column("timestamp")?
.i64()?
.into_iter()
.map(|ts| ts.map(|t| DateTime::from_timestamp(t, 0).unwrap()))
.collect();
println!("{} rows", dates.len());
// Filter by date range
let start_ts = 1704067200i64; // 2024-01-01
let df_filtered = df
.lazy()
.filter(col("timestamp").gt_eq(lit(start_ts)))
.collect()?;
println!("Rows since 2024-01-01: {}", df_filtered.height());
Ok(())
}
recorded outputcargo soothfast docs capture
251 rows Rows since 2024-01-01: 251
Sorting and Ranking#
use finance_query::{Screener, finance};
use polars::prelude::*;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let gainers = finance::screener(Screener::DayGainers, 100).await?;
let mut df = gainers.to_dataframe()?;
// Sort by market cap descending
df = df.sort(
["market_cap"],
SortMultipleOptions::default().with_order_descending(true),
)?;
// Get top 10
let top_10 = df.head(Some(10));
println!("{}", top_10);
Ok(())
}
recorded outputcargo soothfast docs capture
shape: (10, 59) ┌────────┬────────────┬────────────┬────────────┬───┬───────────┬───────────┬───────────┬──────────┐ │ symbol ┆ short_name ┆ long_name ┆ display_na ┆ … ┆ earnings_ ┆ earnings_ ┆ earnings_ ┆ currency │ │ --- ┆ --- ┆ --- ┆ me ┆ ┆ timestamp ┆ timestamp ┆ timestamp ┆ --- │ │ str ┆ str ┆ str ┆ --- ┆ ┆ --- ┆ _start ┆ _end ┆ str │ │ ┆ ┆ ┆ str ┆ ┆ i64 ┆ --- ┆ --- ┆ │ │ ┆ ┆ ┆ ┆ ┆ ┆ i64 ┆ i64 ┆ │ ╞════════╪════════════╪════════════╪════════════╪═══╪═══════════╪═══════════╪═══════════╪══════════╡ │ SPCX ┆ Space Expl ┆ Space Expl ┆ Space Expl ┆ … ┆ 178587360 ┆ 179373600 ┆ 179373600 ┆ USD │ │ ┆ oration ┆ oration ┆ oration ┆ ┆ 0 ┆ 0 ┆ 0 ┆ │ │ ┆ Technologi ┆ Technologi ┆ ┆ ┆ ┆ ┆ ┆ │ │ ┆ es… ┆ es… ┆ ┆ ┆ ┆ ┆ ┆ │ │ PLTR ┆ Palantir ┆ Palantir ┆ Palantir ┆ … ┆ 178578720 ┆ 179364960 ┆ 179364960 ┆ USD │ │ ┆ Technologi ┆ Technologi ┆ ┆ ┆ 0 ┆ 0 ┆ 0 ┆ │ │ ┆ es Inc. ┆ es Inc. ┆ ┆ ┆ ┆ ┆ ┆ │ │ ABNB ┆ Airbnb, ┆ Airbnb, ┆ Airbnb ┆ … ┆ 178604640 ┆ 179390880 ┆ 179390880 ┆ USD │ │ ┆ Inc. ┆ Inc. ┆ ┆ ┆ 0 ┆ 0 ┆ 0 ┆ │ │ COHR ┆ Coherent ┆ Coherent ┆ Coherent ┆ … ┆ 178656480 ┆ 178656480 ┆ 178656480 ┆ USD │ │ ┆ Corp. ┆ Corp. ┆ ┆ ┆ 0 ┆ 0 ┆ 0 ┆ │ │ HONA ┆ Honeywell ┆ Honeywell ┆ Honeywell ┆ … ┆ 178596000 ┆ 179382240 ┆ 179382240 ┆ USD │ │ ┆ Aerospace ┆ Aerospace ┆ Aerospace ┆ ┆ 0 ┆ 0 ┆ 0 ┆ │ │ ┆ Inc. ┆ Inc. ┆ ┆ ┆ ┆ ┆ ┆ │ │ RKLB ┆ Rocket Lab ┆ Rocket Lab ┆ Rocket Lab ┆ … ┆ 178639200 ┆ 178639200 ┆ 178639200 ┆ USD │ │ ┆ Corporatio ┆ Corporatio ┆ ┆ ┆ 0 ┆ 0 ┆ 0 ┆ │ │ ┆ n ┆ n ┆ ┆ ┆ ┆ ┆ ┆ │ │ AU ┆ AngloGold ┆ AngloGold ┆ AngloGold ┆ … ┆ 178550100 ┆ 178550100 ┆ 178550100 ┆ USD │ │ ┆ Ashanti ┆ Ashanti ┆ Ashanti ┆ ┆ 0 ┆ 0 ┆ 0 ┆ │ │ ┆ PLC ┆ plc ┆ ┆ ┆ ┆ ┆ ┆ │ │ CRDO ┆ Credo ┆ Credo ┆ Credo ┆ … ┆ 178034400 ┆ 178837920 ┆ 178837920 ┆ USD │ │ ┆ Technology ┆ Technology ┆ Technology ┆ ┆ 0 ┆ 0 ┆ 0 ┆ │ │ ┆ Group ┆ Group ┆ Group ┆ ┆ ┆ ┆ ┆ │ │ ┆ Holding… ┆ Holding… ┆ Holding ┆ ┆ ┆ ┆ ┆ │ │ NTRA ┆ Natera, ┆ Natera, ┆ Natera ┆ … ┆ 178604640 ┆ 179390880 ┆ 179390880 ┆ USD │ │ ┆ Inc. ┆ Inc. ┆ ┆ ┆ 0 ┆ 0 ┆ 0 ┆ │ │ AXON ┆ Axon Enter ┆ Axon Enter ┆ Axon ┆ … ┆ 178596000 ┆ 179373600 ┆ 179373600 ┆ USD │ │ ┆ prise, ┆ prise, ┆ Enterprise ┆ ┆ 0 ┆ 0 ┆ 0 ┆ │ │ ┆ Inc. ┆ Inc. ┆ ┆ ┆ ┆ ┆ ┆ │ └────────┴────────────┴────────────┴────────────┴───┴───────────┴───────────┴───────────┴──────────┘
Aggregations#
use finance_query::{Interval, Ticker, TimeRange};
use polars::prelude::*;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let ticker = Ticker::new("AAPL").await?;
let chart = ticker.chart(Interval::OneDay, TimeRange::OneYear).await?;
let df = chart.to_dataframe()?;
// Group by (approximate) month and aggregate
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()?;
println!("{}", monthly);
Ok(())
}
recorded outputcargo soothfast docs capture
shape: (14, 5) ┌───────┬────────────┬──────────────┬────────────┬────────────┐ │ month ┆ avg_close ┆ total_volume ┆ max_high ┆ min_low │ │ --- ┆ --- ┆ --- ┆ --- ┆ --- │ │ i64 ┆ f64 ┆ i64 ┆ f64 ┆ f64 │ ╞═══════╪════════════╪══════════════╪════════════╪════════════╡ │ 679 ┆ 261.201363 ┆ 1086109600 ┆ 277.320007 ┆ 244.0 │ │ 688 ┆ 322.733637 ┆ 1216397200 ┆ 344.570007 ┆ 300.0 │ │ 676 ┆ 229.350006 ┆ 113854000 ┆ 231.0 ┆ 219.25 │ │ 677 ┆ 231.592499 ┆ 943741000 ┆ 241.320007 ┆ 223.779999 │ │ 684 ┆ 253.480499 ┆ 789735900 ┆ 262.480011 ┆ 245.509995 │ │ … ┆ … ┆ … ┆ … ┆ … │ │ 689 ┆ 312.246663 ┆ 129985800 ┆ 316.290009 ┆ 305.670013 │ │ 686 ┆ 303.452858 ┆ 1001455700 ┆ 316.940002 ┆ 285.779999 │ │ 682 ┆ 259.249999 ┆ 1186011100 ┆ 279.5 ┆ 243.419998 │ │ 685 ┆ 268.947273 ┆ 1041091400 ┆ 288.029999 ┆ 245.699997 │ │ 678 ┆ 247.65909 ┆ 1257230200 ┆ 259.23999 ┆ 225.949997 │ └───────┴────────────┴──────────────┴────────────┴────────────┘
Multiple Symbols#
Combine data from multiple symbols:
use finance_query::{Interval, Ticker, TimeRange};
use polars::prelude::*;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let aapl = Ticker::new("AAPL").await?;
let msft = Ticker::new("MSFT").await?;
let nvda = Ticker::new("NVDA").await?;
let aapl_chart = aapl.chart(Interval::OneDay, TimeRange::OneMonth).await?;
let msft_chart = msft.chart(Interval::OneDay, TimeRange::OneMonth).await?;
let nvda_chart = nvda.chart(Interval::OneDay, TimeRange::OneMonth).await?;
// Convert to DataFrames
let mut aapl_df = aapl_chart.to_dataframe()?;
let mut msft_df = msft_chart.to_dataframe()?;
let mut nvda_df = nvda_chart.to_dataframe()?;
// Add symbol column to each
aapl_df.with_column(Series::new("symbol".into(), vec!["AAPL"; aapl_df.height()]).into())?;
msft_df.with_column(Series::new("symbol".into(), vec!["MSFT"; msft_df.height()]).into())?;
nvda_df.with_column(Series::new("symbol".into(), vec!["NVDA"; nvda_df.height()]).into())?;
// Combine into single DataFrame
let combined = concat(
[aapl_df.lazy(), msft_df.lazy(), nvda_df.lazy()],
UnionArgs::default(),
)?
.collect()?;
println!("Combined data: {} rows", combined.height());
Ok(())
}
recorded outputcargo soothfast docs capture
Combined data: 69 rows
Exporting Data#
The dataframe feature enables Polars with only its lazy feature. File
writers live behind Polars' own feature flags, so exporting requires adding
polars to your Cargo.toml with the matching features (csv, parquet,
json):
polars = { version = "0.53", features = ["lazy", "csv", "parquet", "json"] }CSV Export#
use polars::prelude::*;
use std::fs::File;
let mut df = chart.to_dataframe()?;
// Write to CSV (requires the polars `csv` feature)
let mut file = File::create("aapl_prices.csv")?;
CsvWriter::new(&mut file)
.include_header(true)
.finish(&mut df)?;
Parquet Export#
use polars::prelude::*;
use std::fs::File;
let mut df = chart.to_dataframe()?;
// Write to Parquet (requires the polars `parquet` feature)
let file = File::create("aapl_prices.parquet")?;
ParquetWriter::new(file)
.finish(&mut df)?;
JSON Export#
use polars::prelude::*;
use std::fs::File;
let mut df = chart.to_dataframe()?;
// Write to JSON (requires the polars `json` feature)
let mut file = File::create("aapl_prices.json")?;
JsonWriter::new(&mut file)
.finish(&mut df)?;
Advanced Patterns#
Rolling Windows#
Rolling aggregations require the Polars rolling_window feature in addition
to lazy:
use polars::prelude::*;
let df = chart.to_dataframe()?;
// Calculate 20-day moving average (requires the polars `rolling_window` feature)
let ma20 = df.lazy()
.select([
col("timestamp"),
col("close"),
col("close")
.rolling_mean(RollingOptionsFixedWindow::default().window_size(20))
.alias("ma_20"),
])
.collect()?;
println!("{}", ma20);
Joining DataFrames#
Requires the polars-ops Polars feature (enabled by default alongside lazy in finance-query's own dataframe feature).
use finance_query::{Dividend, Interval, Ticker, TimeRange};
use polars::prelude::*;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let aapl = Ticker::new("AAPL").await?;
let aapl_chart = aapl.chart(Interval::OneDay, TimeRange::OneMonth).await?;
let aapl_divs = aapl.dividends(TimeRange::OneMonth).await?;
let price_df = aapl_chart.to_dataframe()?;
let div_df = Dividend::vec_to_dataframe(&aapl_divs)?;
let joined = price_df.left_join(&div_df, ["timestamp"], ["timestamp"])?;
println!("joined shape: {:?}", joined.shape());
Ok(())
}
recorded outputcargo soothfast docs capture
joined shape: (23, 8)
Custom Analysis#
use finance_query::{Interval, Ticker, TimeRange};
use polars::prelude::*;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let ticker = Ticker::new("AAPL").await?;
let chart = ticker.chart(Interval::OneDay, TimeRange::OneMonth).await?;
let df = chart.to_dataframe()?;
// Calculate daily price range as percentage
let range_pct = df
.lazy()
.select([
col("timestamp"),
((col("high") - col("low")) / col("close") * lit(100.0)).alias("range_pct"),
])
.collect()?;
// Find days with highest volatility
let volatile_days = range_pct
.sort(
["range_pct"],
SortMultipleOptions::default().with_order_descending(true),
)?
.head(Some(10));
println!("Most volatile days:\n{}", volatile_days);
Ok(())
}
recorded outputcargo soothfast docs capture
Most volatile days: shape: (10, 2) ┌────────────┬───────────┐ │ timestamp ┆ range_pct │ │ --- ┆ --- │ │ i64 ┆ f64 │ ╞════════════╪═══════════╡ │ 1784899800 ┆ 3.828599 │ │ 1784122200 ┆ 3.483971 │ │ 1785504600 ┆ 3.460556 │ │ 1784554200 ┆ 3.071129 │ │ 1785763800 ┆ 3.04528 │ │ 1785850200 ┆ 2.941369 │ │ 1783603800 ┆ 2.64689 │ │ 1783517400 ┆ 2.479345 │ │ 1783949400 ┆ 2.417199 │ │ 1784208600 ┆ 2.367516 │ └────────────┴───────────┘
Type Conversions#
Vec to DataFrame#
Many types support converting Vec<T> to DataFrame:
use finance_query::{Dividend, SearchOptions, Ticker, TimeRange, finance};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Vec of dividends to DataFrame
let ticker = Ticker::new("AAPL").await?;
let dividends = ticker.dividends(TimeRange::FiveYears).await?;
let df = Dividend::vec_to_dataframe(÷nds)?;
println!("{} dividend rows", df.height());
// SearchQuotes wrapper has to_dataframe() method
let results = finance::search("tech", &SearchOptions::default()).await?;
let df = results.quotes.to_dataframe()?;
println!("{} rows", df.height());
Ok(())
}
recorded outputcargo soothfast docs capture
19 dividend rows 7 rows
The conversion itself needs no network. Response types are #[non_exhaustive]
and cannot be constructed literally, but any serde-compatible source works —
this example runs as a real test on a fixture value:
use finance_query::Dividend;
let dividends: Vec<Dividend> = serde_json::from_str(
r#"[{"timestamp": 1704067200, "amount": 0.24},
{"timestamp": 1711929600, "amount": 0.25}]"#,
)
.unwrap();
let df = Dividend::vec_to_dataframe(÷nds).unwrap();
assert_eq!(df.height(), 2);
assert!(df.column("timestamp").is_ok());
assert!(df.column("amount").is_ok());
println!("rows = {}", df.height());
println!("columns = {:?}", df.get_column_names());
recorded outputcargo soothfast docs capture
rows = 2 columns = ["timestamp", "amount"]
Single Item to DataFrame#
Individual structs create single-row DataFrames:
use finance_query::{Ticker, format::Both};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let ticker = Ticker::new("AAPL").await?;
let quote = ticker.quote::<Both>().await?;
let df = quote.to_dataframe()?; // 1 row, 30+ columns
println!("{} columns", df.width());
Ok(())
}
recorded outputcargo soothfast docs capture
154 columns
Error Handling#
DataFrame conversion can fail due to Polars errors:
use finance_query::{Interval, Ticker, TimeRange};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let ticker = Ticker::new("AAPL").await?;
let chart = ticker.chart(Interval::OneDay, TimeRange::OneMonth).await?;
match chart.to_dataframe() {
Ok(df) => {
println!("DataFrame created: {} rows", df.height());
}
Err(e) => {
eprintln!("DataFrame conversion error: {}", e);
}
}
Ok(())
}
recorded outputcargo soothfast docs capture
DataFrame created: 23 rows
Best Practices#
tip · Combine with Ticker Caching
Ticker instances cache data automatically. Fetch once, convert to DataFrame multiple times without additional API calls:
use finance_query::{Interval, Ticker, TimeRange};
use polars::prelude::*;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let ticker = Ticker::new("AAPL").await?;
let chart = ticker.chart(Interval::OneDay, TimeRange::OneMonth).await?;
// Convert to DataFrame for analysis
let df = chart.to_dataframe()?;
// Reuse the same chart data for different analyses
let high_volume = df
.clone()
.lazy()
.filter(col("volume").gt(lit(50_000_000i64)))
.collect()?;
let recent = df.tail(Some(5));
// No additional API calls - data is cached in the Ticker
println!("{} high-volume days, recent:\n{}", high_volume.height(), recent);
Ok(())
}
Next Steps#
- Ticker API - Methods that return DataFrame-compatible types
- Technical Indicators - Convert indicator results to DataFrames for analysis
- Backtesting - Analyze backtest results in DataFrames
- Finance Module - Market-wide data with DataFrame support
- Polars Documentation - Complete Polars guide
