# dyfinance 仕様書

## 概要

dyfinance は yfinance の **drop-in replacement** ライブラリである。
DuckDB による OHLCV キャッシュ層を透過的に挟むことで、同一データの再取得を回避し、ネットワーク負荷と応答時間を削減する。

```python
# 利用者はこの1行を変えるだけで dyfinance を使える
import dyfinance as yf
```

## 設計目標

1. **yfinance と同一の I/O** - すべての公開 API は yfinance と同じシグネチャ・デフォルト値・出力構造を持つ
2. **透過的キャッシュ** - 利用者がキャッシュの存在を意識する必要がない
3. **安全なキャッシュ** - raw データ (auto_adjust=False) で保存し、読み出し時に後処理を適用することで、オプションの組み合わせに依存しない
4. **yfinance の完全な fallback** - dyfinance が実装しない機能はすべて yfinance に透過的に委譲する

## yfinance 互換性

[docs/yfinance-compatibility.md](docs/yfinance-compatibility.md) を参照。

---

## アーキテクチャ

```
dyfinance (公開 API)
├── Ticker          … yfinance.Ticker のプロキシ。history() のみキャッシュ付きで上書き
├── Tickers         … yfinance.Tickers のプロキシ。download() 経由でキャッシュを利用
├── download()      … yfinance.download() の drop-in replacement
└── __getattr__     … 上記以外は yfinance に透過的に委譲

内部モジュール
├── _ticker.py      … Ticker クラス本体
├── _tickers.py     … Tickers クラス本体
├── _download.py    … download() 関数本体
├── _cache.py       … CacheEngine (DuckDB 操作)
├── _schema.py      … DuckDB テーブル定義
├── _constants.py   … 列名定数
└── _version.py     … バージョン
```

---

## コンポーネント仕様

### Ticker (`_ticker.py`)

yfinance.Ticker のプロキシクラス。

- `__init__(ticker, session=None)` で yfinance.Ticker を内部に保持
- `__getattr__` で history() 以外のすべてのプロパティ・メソッドを yfinance.Ticker に委譲
- `history()` のみキャッシュロジックを挟む

#### history() のフロー

```
history(period, interval, start, end, ...) が呼ばれる
    │
    ├─ interval がキャッシュ対象外 → yfinance.Ticker.history() に直接委譲
    │
    ├─ interval がキャッシュ対象 (1d, 5d, 1wk, 1mo, 3mo)
    │   ├─ period/start/end を UTC タイムスタンプに解決
    │   ├─ CacheEngine.get_cached_ohlcv() でキャッシュを確認
    │   ├─ missing_ranges があれば yfinance から取得し CacheEngine.store_ohlcv() で保存
    │   ├─ キャッシュから最終データを取得
    │   └─ _apply_post_processing() で後処理を適用
    │       ├─ auto_adjust: Adj Close / Close 比率で OHLC を調整、Adj Close 列を削除
    │       ├─ back_adjust: 累積比率で後方調整、Adj Close 列を削除
    │       ├─ rounding: 価格列を小数点以下2桁に丸め
    │       ├─ actions=False: Dividends / Stock Splits / Capital Gains 列を削除
    │       └─ keepna=False: 全列 0/NaN の行を除去
    └─ DataFrame を返す
```

#### キャッシュ保存ルール

- yfinance には **常に** `auto_adjust=False, actions=True, rounding=False, keepna=False` で問い合わせる
- raw データをそのまま DuckDB に保存する
- 利用者が指定したオプションは読み出し後の後処理で適用する

### Tickers (`_tickers.py`)

yfinance.Tickers のプロキシクラス。

- 内部に `{symbol: dyfinance.Ticker}` の辞書を保持
- `history()` は `download()` に委譲
- `download()` は `dyfinance.download()` を呼び出す
- `**kwargs` を受け付ける（yfinance.Tickers 互換）
- ティッカーの順序を保持し、dedup しない（yfinance.Tickers と同一の挙動）

### download() (`_download.py`)

yfinance.download() の drop-in replacement。

- キャッシュ対象 interval → 各ティッカーを `dyfinance.Ticker.history()` 経由で取得（キャッシュが効く）
- キャッシュ対象外 interval → `yfinance.download()` に直接委譲
- ティッカーの正規化: `sorted(set(...))` で dedup（yfinance.download と同一の挙動）
- `ignore_tz`, `group_by`, `multi_level_index` の処理は yfinance と同一

### CacheEngine (`_cache.py`)

DuckDB を使った OHLCV キャッシュエンジン。

- シングルトンパターン (`CacheEngine.default()`)
- デフォルト DB パス: `~/.dyfinance/cache.duckdb` (環境変数 `DYFINANCE_DB_PATH` で上書き可)
- `get_cached_ohlcv(ticker, interval, start, end)` → `(cached_df, missing_ranges)`
- `store_ohlcv(ticker, interval, df)` → キャッシュに保存・メタデータ更新
- `cache_meta` テーブルで各 (ticker, interval) のキャッシュ済み範囲を管理

#### キャッシュ検証（スポットチェック）

完全キャッシュヒット時（missing_ranges が空）、dyfinance はキャッシュの鮮度を自動検証する:

```
完全キャッシュヒット
  │
  ├─ fetched_at が STALE_THRESHOLD (デフォルト 6 時間) 以内 → そのまま返す
  │
  └─ fetched_at が古い
      ├─ yfinance から直近 5 行を軽量フェッチ
      ├─ キャッシュの Adj Close と比較
      │
      ├─ 一致 (相対誤差 ≤ 1e-4) → fetched_at を更新、キャッシュ返す
      │
      └─ 不一致
          ├─ 比率が均一 → 全キャッシュの Adj Close に比率補正を適用
          │               （yfinance の取得限界 ~2年 を超えるデータも保全）
          └─ 比率が不均一 → キャッシュ全破棄、要求レンジを yfinance から再取得
```

- `STALE_THRESHOLD`: 環境変数 `DYFINANCE_STALE_HOURS` で変更可能（デフォルト: `6`）
- スポットチェックは完全キャッシュヒット時のみ実行。部分ヒットは通常通り yfinance を呼ぶ

#### 手動キャッシュ無効化

```python
Ticker("AAPL").clear_cache()          # AAPL の全 interval を削除
Ticker("AAPL").clear_cache("1d")      # AAPL の日足キャッシュのみ削除
```

#### キャッシュ対象 interval

```python
CACHEABLE_INTERVALS = {"1d", "5d", "1wk", "1mo", "3mo"}
```

イントラデイ (`1m`, `2m`, `5m`, `15m`, `30m`, `60m`, `90m`, `1h`) はキャッシュ対象外。

### DuckDB スキーマ (`_schema.py`)

| テーブル | 主キー | 用途 |
|---------|--------|------|
| `ohlcv` | (ticker, interval, ts) | OHLCV + Adj Close + 配当・分割データ |
| `cache_meta` | (ticker, interval) | キャッシュ済み日付範囲の管理 |
| `schema_version` | - | スキーマバージョン管理 |

---

## 依存関係

| パッケージ | バージョン | 用途 |
|-----------|----------|------|
| yfinance | >=1.2, <2 | 株価データの取得元 |
| duckdb | >=1.0, <2 | ローカルキャッシュ DB |
| pandas | >=2.0 | DataFrame 操作 |

Python >= 3.12

---

## テスト戦略

| 層 | ファイル | マーカー | 内容 |
|----|---------|---------|------|
| Layer 1: Mock | `test_ticker_history.py`, `test_download.py` | (デフォルト) | yfinance をモックし、ロジックを単体テスト |
| Layer 2: Fixture | `test_compat.py` (TestFixture*) | (デフォルト, skip if missing) | 録画済み parquet で出力構造を検証 |
| Layer 3: Live | `test_compat.py` (TestLive*) | `@pytest.mark.network` | yfinance と dyfinance の出力をリアルタイム比較 |
| Proxy | `test_proxy.py` | (デフォルト) | モジュール fallback と属性委譲の検証 |
| Cache | `test_cache.py` | (デフォルト) | CacheEngine の単体テスト |
| Schema | `test_schema.py` | (デフォルト) | DuckDB スキーマの検証 |

### テスト実行

```bash
# CI (ネットワーク不要)
pytest tests/ -m "not network"

# ライブ互換性テスト (ネットワーク必要)
pytest tests/ -m "network"
```
