# dyfinance

[![CI](https://github.com/qooh0/dyfinance/actions/workflows/ci.yml/badge.svg)](https://github.com/qooh0/dyfinance/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/dyfinance)](https://pypi.org/project/dyfinance/)
[![Python](https://img.shields.io/pypi/pyversions/dyfinance)](https://pypi.org/project/dyfinance/)
[![License](https://img.shields.io/github/license/qooh0/dyfinance)](LICENSE)

Drop-in replacement for [yfinance](https://github.com/ranaroussi/yfinance) with DuckDB OHLCV caching. Generated by Claude.

```python
# Change just this one line
import dyfinance as yf

ticker = yf.Ticker("AAPL")
df = ticker.history(period="1mo")  # cached transparently
```

## Why DuckDB?

dyfinance uses [DuckDB](https://duckdb.org/) as its cache backend:

- **Single file, zero config** -- like SQLite, DuckDB is an embedded database with no server process; your entire cache lives in one `~/.dyfinance/cache.duckdb` file
- **Columnar storage** -- well-suited for analytical queries across many tickers and long date ranges
- **Native pandas integration** -- DuckDB reads and writes DataFrames without serialization overhead
- **Your cache is a database** -- you can query the cache directly with SQL for custom analysis beyond what yfinance offers:

```python
import duckdb

conn = duckdb.connect("~/.dyfinance/cache.duckdb")
# Your own analysis — no yfinance API limits
conn.sql("""
    SELECT ticker, interval,
           MIN(ts) AS first_date, MAX(ts) AS last_date,
           COUNT(*) AS rows
    FROM ohlcv
    GROUP BY ticker, interval
""").show()
```

## Features

- **yfinance-compatible** -- same API signatures, defaults, and output structures
- **Transparent DuckDB caching** -- daily+ OHLCV data is cached locally; redundant fetches are eliminated
- **Full fallback** -- any attribute or method not overridden by dyfinance is delegated to yfinance
- **Spot-check validation** -- stale cache is automatically verified against fresh data and corrected if needed

## Installation

```bash
pip install dyfinance
```

Requires Python >= 3.10.

## Quick Start

```python
import dyfinance as yf

# Single ticker
aapl = yf.Ticker("AAPL")
hist = aapl.history(period="6mo")           # first call fetches, subsequent calls use cache
info = aapl.info                            # delegated to yfinance

# Multiple tickers
data = yf.download(["AAPL", "MSFT"], period="1mo")

# All yfinance features work as-is
aapl.dividends
aapl.financials
aapl.options
```

## Cached Intervals

| Cached (DuckDB)                    | Passthrough (yfinance direct)                 |
|------------------------------------|-----------------------------------------------|
| `1d`, `5d`, `1wk`, `1mo`, `3mo`   | `1m`, `2m`, `5m`, `15m`, `30m`, `60m`, `90m`, `1h` |

## Known Limitations

- **ISIN codes are not supported** in the cached path. yfinance's `download()` auto-converts ISIN codes (e.g. `US0378331005`) to ticker symbols, but dyfinance skips this conversion for cacheable intervals. Use ticker symbols directly (e.g. `AAPL` instead of `US0378331005`).

## Configuration

| Environment Variable       | Default                        | Description                   |
|----------------------------|--------------------------------|-------------------------------|
| `DYFINANCE_DB_PATH`         | `~/.dyfinance/cache.duckdb`     | Cache database file path      |
| `DYFINANCE_STALE_HOURS`     | `6`                            | Hours before spot-check fires |

## Cache Management

```python
import dyfinance as yf

# Clear cache for a specific ticker
yf.Ticker("AAPL").clear_cache()        # all intervals
yf.Ticker("AAPL").clear_cache("1d")    # daily only
```

## Benchmark

Cache-read performance compared against yfinance's built-in SQLite cache.
Both libraries serve data from their respective local caches — no network I/O is involved.

### Ticker.history() — single ticker (20 iterations, median)

| Ticker | Period | yfinance (SQLite) | dyfinance (DuckDB) | Ratio |
|--------|--------|------------------:|-------------------:|------:|
| AAPL   | 1mo    |          12.0 ms  |          20.8 ms   | 1.7x  |
| AAPL   | 6mo    |          13.6 ms  |          24.1 ms   | 1.8x  |
| AAPL   | 1y     |          15.3 ms  |          25.9 ms   | 1.7x  |
| MSFT   | 1mo    |          12.2 ms  |          21.8 ms   | 1.8x  |
| MSFT   | 1y     |          15.4 ms  |          25.8 ms   | 1.7x  |
| GOOGL  | 1mo    |          12.5 ms  |          21.9 ms   | 1.8x  |
| GOOGL  | 1y     |          14.7 ms  |          26.0 ms   | 1.8x  |

**For single-ticker reads, yfinance is ~1.7-1.8x faster.** SQLite (row-oriented) outperforms DuckDB (columnar) for small, per-ticker queries. This is expected — DuckDB's columnar engine is designed for analytical workloads, not single-row lookups.

### download() — multi-ticker batch (10 iterations, median)

| Tickers | yfinance (SQLite) | dyfinance (DuckDB) | Winner |
|--------:|------------------:|-------------------:|-------:|
|       5 |         161.2 ms  |          10.8 ms   | **dyfinance 15x faster** |
|      10 |         192.3 ms  |          16.8 ms   | **dyfinance 11x faster** |
|      20 |         252.3 ms  |          32.0 ms   | **dyfinance 8x faster**  |
|      30 |         309.9 ms  |          44.6 ms   | **dyfinance 7x faster**  |
|      50 |         485.1 ms  |          74.7 ms   | **dyfinance 6x faster**  |

**For multi-ticker downloads, dyfinance is 6-15x faster.** DuckDB reads all tickers in a single batch query, avoiding the per-ticker overhead of creating yfinance Ticker objects.

Reproduce with:

```bash
python benchmarks/bench_cache.py           # single-ticker
python benchmarks/bench_multi_symbol.py    # multi-ticker
```

## Development

```bash
pip install -e ".[dev]"

make test        # offline tests
make test-live   # live yfinance comparison tests (network required)
make lint        # ruff check
make typecheck   # mypy strict
make clean       # remove caches and build artifacts
```

## License

Apache-2.0
