Summary
Add a finance-query-python crate that compiles the Rust library to a native Python extension via PyO3 and maturin, published to PyPI as finance-query-py. This replaces the legacy v1/ Python implementation with zero-overhead Rust performance, full async support through a bundled tokio runtime, and first-class pandas/numpy integration.
Problem Statement
The v1/ directory contains a legacy Python implementation that duplicates logic now maintained in Rust. Python is the dominant language in quantitative finance and data science — yfinance has 15 M+ monthly downloads. The Rust library has a richer API (backtesting, 42 indicators, streaming, risk analytics, EDGAR, FRED, options, screeners) than any comparable Python package, but it is inaccessible to Python users. PyO3 lets us expose the Rust library directly with no reimplementation cost, no data copies across a process boundary, and the full performance of the native library.
Proposed Solution
New crate: finance-query-python
finance-query-python/
├── Cargo.toml
├── pyproject.toml # maturin build backend
├── src/
│ ├── lib.rs # #[pymodule] root
│ ├── ticker.rs # PyTicker wrapper
│ ├── tickers.rs # PyTickers wrapper
│ ├── finance.rs # top-level functions (search, screener, etc.)
│ ├── models/ # #[pyclass] wrappers for response types
│ │ ├── quote.rs
│ │ ├── chart.rs
│ │ ├── options.rs
│ │ └── ...
│ └── constants.rs # Interval, TimeRange, Frequency, etc. as Python enums
├── finance_query/
│ ├── __init__.py # re-exports + runtime bootstrap
│ ├── py.typed # PEP 561 marker
│ └── _finance_query.pyi # type stubs (generated + hand-polished)
└── tests/
└── test_ticker.py
Cargo.toml excerpt:
[package]
name = "finance-query-python"
version = "2.5.1"
[lib]
name = "_finance_query"
crate-type = ["cdylib"]
[dependencies]
finance-query = { path = "..", features = ["indicators", "risk", "fred", "crypto", "rss", "backtesting"] }
pyo3 = { version = "0.23", features = ["extension-module", "abi3-py39"] }
pyo3-async-runtimes = { version = "0.23", features = ["tokio-runtime"] }
tokio = { version = "1", features = ["full"] }
Async bridging strategy
Python callers use await natively. Every Rust async fn is exposed as a Python coroutine via pyo3_async_runtimes::tokio::future_into_py. A single shared tokio runtime is initialised once on module import and reused for the lifetime of the process — no asyncio.run() overhead per call.
# Works with asyncio, trio (via anyio), and Jupyter
import asyncio
from finance_query import Ticker
async def main():
ticker = await Ticker.new("AAPL")
quote = await ticker.quote()
print(quote.current_price)
asyncio.run(main())
PyTicker wrapper
#[pyclass(name = "Ticker")]
pub struct PyTicker {
inner: finance_query::Ticker,
}
#[pymethods]
impl PyTicker {
#[staticmethod]
fn new(py: Python<'_>, symbol: &str) -> PyResult<Bound<'_, PyAny>> {
let symbol = symbol.to_owned();
pyo3_async_runtimes::tokio::future_into_py(py, async move {
let ticker = finance_query::Ticker::new(&symbol).await.map_err(into_py_err)?;
Ok(PyTicker { inner: ticker })
})
}
fn quote<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
let inner = self.inner.clone();
pyo3_async_runtimes::tokio::future_into_py(py, async move {
let quote = inner.quote().await.map_err(into_py_err)?;
Ok(PyQuote::from(quote))
})
}
fn chart<'py>(&self, py: Python<'py>, interval: PyInterval, range: PyTimeRange)
-> PyResult<Bound<'py, PyAny>> { ... }
// Builder pattern exposed as class methods
#[staticmethod]
fn builder(symbol: &str) -> PyTickerBuilder { ... }
}
Model types
All #[pyclass] model types:
- Expose fields as
#[getter] properties — no dict copies
- Implement
__repr__ and __str__ via the Rust Debug/Display impls
- Implement
__eq__ where the Rust type is PartialEq
- Provide
to_dict() → PyDict for interop with pandas/json/dataclasses
Vec<T> fields become Python list; Option<T> becomes T | None
HashMap<K, V> fields become dict
Pandas integration
When pandas is installed, model types with list-of-numeric data get a to_dataframe() method:
chart = await ticker.chart(Interval.ONE_DAY, TimeRange.ONE_YEAR)
df = chart.to_dataframe()
# Returns pd.DataFrame with columns: open, high, low, close, volume, date
# Uses zero-copy Arrow IPC via arro3/polars → pandas when the dataframe feature is enabled
# Falls back to dict-of-lists construction otherwise
The Candle, OptionContract, FinancialStatement and Spark types all gain to_dataframe().
Constants as Python enums
from finance_query import Interval, TimeRange, Frequency, StatementType, Region
chart = await ticker.chart(Interval.ONE_DAY, TimeRange.ONE_YEAR)
stmt = await ticker.financials(StatementType.INCOME, Frequency.QUARTERLY)
Rust enums map to Python enum.Enum subclasses via pyo3's #[pyclass] on unit variants.
Top-level finance functions
from finance_query import finance
results = await finance.search("Apple")
trending = await finance.trending(Region.US)
screener = await finance.screener(Screener.DAY_GAINERS, count=25)
fng = await finance.fear_and_greed()
Streaming
from finance_query.streaming import PriceStream
async with PriceStream.subscribe(["AAPL", "NVDA"]) as stream:
async for update in stream:
print(update.symbol, update.price)
Exposed via pyo3_async_runtimes::tokio and Python's AsyncIterator protocol.
Feature flag mapping
| Python extra |
Rust features |
Unlocks |
finance-query-py (base) |
yahoo |
Ticker, Tickers, finance module, EDGAR, streaming |
finance-query-py[indicators] |
indicators |
42 technical indicators |
finance-query-py[backtesting] |
backtesting |
Strategy backtesting engine |
finance-query-py[risk] |
risk |
VaR, Sharpe/Sortino/Calmar, beta, drawdown |
finance-query-py[fred] |
fred |
FRED macro series + Treasury yields |
finance-query-py[crypto] |
crypto |
CoinGecko market data |
finance-query-py[rss] |
rss |
RSS/Atom feed aggregation |
finance-query-py[all] |
all above |
Everything |
Pre-built wheels on PyPI eliminate the need for users to have a Rust toolchain.
Type stubs
A _finance_query.pyi stub file is checked into the repo and updated with each release. Combined with py.typed, this gives IDEs (VS Code, PyCharm) full autocomplete and type checking via mypy/pyright.
class Ticker:
@staticmethod
async def new(symbol: str) -> Ticker: ...
async def quote(self) -> Quote: ...
async def chart(self, interval: Interval, range: TimeRange) -> Chart: ...
async def financials(
self, statement: StatementType, frequency: Frequency
) -> FinancialStatement: ...
async def options(self, date: str | None = None) -> Options: ...
async def news(self) -> list[News]: ...
async def recommendations(self, limit: int = 5) -> Recommendation: ...
# ... all other Ticker methods
class Chart:
candles: list[Candle]
meta: ChartMeta
def to_dataframe(self) -> pd.DataFrame: ...
Build and distribution
# finance-query-python/pyproject.toml
[build-system]
requires = ["maturin>=1.7"]
build-backend = "maturin"
[project]
name = "finance-query-py"
requires-python = ">=3.9"
dependencies = []
[project.optional-dependencies]
pandas = ["pandas>=2.0"]
all = ["pandas>=2.0"]
[tool.maturin]
features = ["pyo3/extension-module"]
python-source = "finance_query"
module-name = "finance_query._finance_query"
CI builds wheels for:
manylinux_2_28 x86_64, aarch64 (via maturin-action)
- macOS 12+ x86_64 and arm64
- Windows x86_64
- CPython 3.9, 3.10, 3.11, 3.12, 3.13 (abi3 wheels cover all)
Usage examples
Quant workflow:
import asyncio
import pandas as pd
from finance_query import Ticker, Interval, TimeRange, Frequency, StatementType
async def analyse(symbol: str):
ticker = await Ticker.new(symbol)
# All quote modules in one request, cached
quote = await ticker.quote()
print(f"{quote.display_name}: ${quote.current_price}")
# Chart → pandas in two lines
chart = await ticker.chart(Interval.ONE_DAY, TimeRange.ONE_YEAR)
df = chart.to_dataframe()
df["sma50"] = df["close"].rolling(50).mean()
# Financials
income = await ticker.financials(StatementType.INCOME, Frequency.QUARTERLY)
return df
df = asyncio.run(analyse("AAPL"))
Batch processing:
from finance_query import Tickers, Interval, TimeRange
async def batch():
tickers = await Tickers.new(["AAPL", "MSFT", "NVDA", "TSLA"])
quotes = await tickers.quotes()
# quotes.data: dict[str, Quote], quotes.errors: dict[str, str]
charts = await tickers.charts(Interval.ONE_DAY, TimeRange.SIX_MONTHS)
dfs = {sym: r.to_dataframe() for sym, r in charts.data.items()}
Backtesting (with [backtesting] extra):
from finance_query import Ticker, Interval, TimeRange
from finance_query.backtesting import SmaCrossover, BacktestConfig
async def backtest():
ticker = await Ticker.new("AAPL")
strategy = SmaCrossover(fast=20, slow=50)
config = BacktestConfig(initial_capital=10_000, commission=0.001)
result = await ticker.backtest(strategy, Interval.ONE_DAY, TimeRange.FIVE_YEARS, config)
print(f"Total return: {result.total_return:.1%}, Sharpe: {result.sharpe_ratio:.2f}")
Use Cases
- Quant researchers and data scientists who live in Jupyter notebooks but want Rust-speed data fetching
- yfinance users seeking a drop-in that won't break when Yahoo Finance changes its scraping surface
- ML pipelines that need reliable OHLCV + fundamentals + news + options data in pandas form
- Backtesting workflows that currently call the Rust CLI or HTTP server from Python scripts
Alternatives Considered
| Approach |
Why rejected |
Maintain v1/ Python rewrite |
Already out of date, duplicates all Rust logic, no indicators/backtesting/EDGAR |
HTTP server + requests wrapper |
Extra process, latency, no type safety, requires server deployment |
subprocess calling the fq CLI |
Shell overhead, no streaming, painful for batch use |
cffi / raw C bindings |
Far more boilerplate than PyO3, no async bridging story |
Implementation Phases
Phase 1 — Core (MVP for PyPI alpha):
Ticker + Tickers with all quote/chart/news/options/financials methods
finance.* top-level functions
- All constants as Python enums
Chart.to_dataframe() + Quote.to_dict()
- Type stubs +
py.typed
- manylinux + macOS + Windows wheels in CI
Phase 2 — Feature extras:
indicators, risk, fred, crypto, rss, backtesting extras
to_dataframe() on OptionContract, FinancialStatement, Spark
- Streaming
PriceStream async iterator
Phase 3 — Ecosystem integration:
- Zero-copy Arrow/polars → pandas via
arro3 when dataframe feature is enabled
__dataframe__ protocol (PEP 680) for framework-agnostic DataFrames
- Jupyter display hooks (
_repr_html_) on model types
- Async context manager for
Ticker to scope the tokio runtime lifetime
Additional Context
v1/ can be archived (moved to v1/legacy/ or removed) once the PyO3 bindings reach feature parity
- PyPI package name
finance-query-py is distinct from finance_query (the Rust crate's Python module name) to avoid collision
- maturin supports workspace Cargo.toml; the new crate slots cleanly into the existing workspace resolver = "2" setup
abi3-py39 produces a single wheel per platform that works on Python 3.9 through 3.13+ without rebuilding
Priority
Summary
Add a
finance-query-pythoncrate that compiles the Rust library to a native Python extension via PyO3 and maturin, published to PyPI asfinance-query-py. This replaces the legacyv1/Python implementation with zero-overhead Rust performance, full async support through a bundled tokio runtime, and first-class pandas/numpy integration.Problem Statement
The
v1/directory contains a legacy Python implementation that duplicates logic now maintained in Rust. Python is the dominant language in quantitative finance and data science — yfinance has 15 M+ monthly downloads. The Rust library has a richer API (backtesting, 42 indicators, streaming, risk analytics, EDGAR, FRED, options, screeners) than any comparable Python package, but it is inaccessible to Python users. PyO3 lets us expose the Rust library directly with no reimplementation cost, no data copies across a process boundary, and the full performance of the native library.Proposed Solution
New crate:
finance-query-pythonCargo.tomlexcerpt:Async bridging strategy
Python callers use
awaitnatively. Every Rustasync fnis exposed as a Python coroutine viapyo3_async_runtimes::tokio::future_into_py. A single shared tokio runtime is initialised once on module import and reused for the lifetime of the process — noasyncio.run()overhead per call.PyTicker wrapper
Model types
All
#[pyclass]model types:#[getter]properties — no dict copies__repr__and__str__via the RustDebug/Displayimpls__eq__where the Rust type isPartialEqto_dict()→PyDictfor interop with pandas/json/dataclassesVec<T>fields become Pythonlist;Option<T>becomesT | NoneHashMap<K, V>fields becomedictPandas integration
When pandas is installed, model types with list-of-numeric data get a
to_dataframe()method:The
Candle,OptionContract,FinancialStatementandSparktypes all gainto_dataframe().Constants as Python enums
Rust enums map to Python
enum.Enumsubclasses viapyo3's#[pyclass]on unit variants.Top-level finance functions
Streaming
Exposed via
pyo3_async_runtimes::tokioand Python'sAsyncIteratorprotocol.Feature flag mapping
finance-query-py(base)yahoofinance-query-py[indicators]indicatorsfinance-query-py[backtesting]backtestingfinance-query-py[risk]riskfinance-query-py[fred]fredfinance-query-py[crypto]cryptofinance-query-py[rss]rssfinance-query-py[all]Pre-built wheels on PyPI eliminate the need for users to have a Rust toolchain.
Type stubs
A
_finance_query.pyistub file is checked into the repo and updated with each release. Combined withpy.typed, this gives IDEs (VS Code, PyCharm) full autocomplete and type checking via mypy/pyright.Build and distribution
CI builds wheels for:
manylinux_2_28x86_64, aarch64 (viamaturin-action)Usage examples
Quant workflow:
Batch processing:
Backtesting (with
[backtesting]extra):Use Cases
Alternatives Considered
v1/Python rewriterequestswrappersubprocesscalling thefqCLIcffi/ raw C bindingsImplementation Phases
Phase 1 — Core (MVP for PyPI alpha):
Ticker+Tickerswith all quote/chart/news/options/financials methodsfinance.*top-level functionsChart.to_dataframe()+Quote.to_dict()py.typedPhase 2 — Feature extras:
indicators,risk,fred,crypto,rss,backtestingextrasto_dataframe()onOptionContract,FinancialStatement,SparkPriceStreamasync iteratorPhase 3 — Ecosystem integration:
arro3whendataframefeature is enabled__dataframe__protocol (PEP 680) for framework-agnostic DataFrames_repr_html_) on model typesTickerto scope the tokio runtime lifetimeAdditional Context
v1/can be archived (moved tov1/legacy/or removed) once the PyO3 bindings reach feature parityfinance-query-pyis distinct fromfinance_query(the Rust crate's Python module name) to avoid collisionabi3-py39produces a single wheel per platform that works on Python 3.9 through 3.13+ without rebuildingPriority