Metadata-Version: 2.4
Name: chidoribt
Version: 0.3.1
Summary: Multi-asset unified portfolio backtesting framework
License: MIT
Project-URL: Repository, https://github.com/MioQuant/chidoribt
Project-URL: Bug Tracker, https://github.com/MioQuant/chidoribt/issues
Keywords: backtesting,finance,trading,portfolio,quantitative
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Office/Business :: Financial :: Investment
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.23.0
Requires-Dist: pandas>=1.5.0
Provides-Extra: numba
Requires-Dist: numba>=0.56; extra == "numba"
Requires-Dist: scipy>=1.10; extra == "numba"
Provides-Extra: postgres
Requires-Dist: psycopg2-binary>=2.9; extra == "postgres"
Provides-Extra: tsdb
Requires-Dist: mini-tsdb>=0.1.0; extra == "tsdb"
Provides-Extra: benchmark
Requires-Dist: numba>=0.56; extra == "benchmark"
Requires-Dist: scipy>=1.10; extra == "benchmark"
Requires-Dist: psycopg2-binary>=2.9; extra == "benchmark"
Requires-Dist: python-dotenv>=1.0; extra == "benchmark"
Provides-Extra: full
Requires-Dist: numba>=0.56; extra == "full"
Requires-Dist: scipy>=1.10; extra == "full"
Requires-Dist: mini-tsdb>=0.1.0; extra == "full"
Requires-Dist: psycopg2-binary>=2.9; extra == "full"
Requires-Dist: python-dotenv>=1.0; extra == "full"
Dynamic: license-file

# ChidoriBT ⚡

**超高速多資產回測框架 / High-Performance Multi-Asset Backtesting Framework**

[![PyPI version](https://badge.fury.io/py/chidoribt.svg)](https://pypi.org/project/chidoribt/)
[![Python 3.9+](https://img.shields.io/badge/python-3.9%2B-blue.svg)](https://pypi.org/project/chidoribt/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Numba](https://img.shields.io/badge/accelerated%20by-Numba-orange.svg)](https://numba.pydata.org/)

<p align="center">
  <img src="https://tse2.mm.bing.net/th/id/OIP.tzQLNs50GB5WLS5qufp1gAAAAA?cb=thfc1falcon&rs=1&pid=ImgDetMain&o=7&rm=3" alt="Chidori — 千鳥" width="200"/>
</p>

---

## 專案名稱由來

> **千鳥（Chidori）** — 精準、迅速，如閃電貫穿黑暗。

「千鳥」是忍術中以**極致速度**與**精準集中**著稱的雷遁之術。ChidoriBT 的設計哲學正是如此：

- **快**：批量參數掃描（240 組合）比 vectorbt serial 快 **624×**；單次 fast 淨值篩選快 **22×**
- **省**：批量掃描記憶體僅需 vectorbt 的 **1/26**（7.5 MB vs 194.7 MB，240 組合）
- **彈性**：事件驅動模式可在每個時步嵌入任意 ML 模型推論

---

## ChidoriBT vs vectorbt

| 功能 | **ChidoriBT** | vectorbt（free） |
|------|:-------------:|:----------------:|
| 單次 fast（1 組，僅淨值） | **2.6 ms** | 58 ms |
| 單次 full（1 組，含 sharpe/trades） | **119 ms** | 177 ms |
| 批量掃描 fast（240 組合） | **23.3 ms** | 14.55 s |
| 參數掃描（240 組合，12 核 MP） | **23.3 ms** | 989.7 ms |
| 參數掃描記憶體（240 組合） | **7.5 MB** | 194.7 MB |
| ML 模型動態插入 | ✅ | ❌ |
| 整數股數（台股張數） | ✅ | ❌ |
| 內建多核並行掃描 | ✅ ParamScanner | ❌ |

> 測試規模：100 支台股 × 726 交易日（2023–2025），子母線型態策略。公平性說明見 [docs/benchmark_paths.md](docs/benchmark_paths.md)。

---

## 安裝

**Python** ≥ 3.9，< 3.13

```bash
pip install "chidoribt[benchmark]"
```

本機開發：

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

含 Numba JIT、scipy、PostgreSQL 載入與 `.env` 支援。若僅用自備 DataFrame、不需 DB，也可 `pip install chidoribt` 後自行安裝 `numba`、`scipy`。

---

## 快速開始：參數掃描

典型流程：**載入面板 → JIT 暖機 → 掃描參數**。repo 內 `backtest/` 提供可直接執行的範例。

### 環境設定

在專案根目錄建立 `.env`（PostgreSQL 載入時需要）：

```env
DB_HOST=localhost
DB_PORT=5433
DB_NAME=your_db
DB_USER=your_user
DB_PASSWORD=your_password
```

### 240 組合批量掃描（`backtest/test_scan.py`）

```bash
cd backtest
python test_scan.py
```

對應程式碼：

```python
from chidoribt.benchmark import (
    DEFAULT_SCAN_GRID,
    bench_param_scan,
    load_panel_from_db,
    print_bench_report,
    print_numba_info,
    warmup_numba_kernels,
)

STOCKS = 100
REPEATS = 3
INITIAL_CASH = 1_000_000
MAX_POSITIONS = 20

print_numba_info()                              # 確認 Numba 並行執行緒數
panel, meta = load_panel_from_db(stocks=STOCKS) # 從 DB 載入，或改用 PanelData.from_dataframe(df)
warmup_numba_kernels(panel)                     # 一次性 JIT 暖機

timer, summary = bench_param_scan(
    panel=panel, n=REPEATS,
    initial_cash=INITIAL_CASH, max_positions=MAX_POSITIONS,
    **DEFAULT_SCAN_GRID,                        # 240 組合：hold×sl×tp×trade_size
)

print_bench_report(timer, extra={
    "n_combos": summary["n_combos"],
    "best_final_equity": f"{summary['best_final_equity']:,.0f}",
    "throughput": f"{summary['throughput_combos_per_s']:,.0f} 組合/s",
})

df = summary["dataframe"]
print(df.nlargest(5, "final_equity"))
```

`DEFAULT_SCAN_GRID` 預設網格：

| 維度 | 值 |
|------|-----|
| hold_days | 3, 5, 7, 10, 15 |
| stop_loss | 0.03, 0.05, 0.08, 0.10 |
| take_profit | 0.08, 0.12, 0.15, 0.20 |
| trade_size | 20,000, 30,000, 50,000 |

### 單組參數快速篩選（`backtest/test.py`）

```bash
python test.py
```

對應程式碼：

```python
from chidoribt.benchmark import (
    bench_single_fast,
    load_panel_from_db,
    print_bench_report,
    print_numba_info,
    warmup_numba_kernels,
)

print_numba_info()
panel, meta = load_panel_from_db(stocks=100)
warmup_numba_kernels(panel, stop_loss=0.05, take_profit=0.10)

timer, summary = bench_single_fast(
    panel=panel, n=3,
    hold_days=5, stop_loss_pct=0.05, take_profit_pct=0.10, trade_size=50_000,
    initial_cash=1_000_000, max_positions=20,
)

print_bench_report(timer, extra={"final_equity": f"{summary['final_equity']:,.0f}"})
```

### 自備 DataFrame（不需 DB）

```python
from chidoribt import PanelData, ParamScanner
from chidoribt.benchmark import print_numba_info, warmup_numba_kernels
from chidoribt.strategies.stock_patterns import ChildParentStrategy

# stacked DataFrame 欄位：stock_id, date, Open, High, Low, Close, Volume
print_numba_info()
panel = PanelData.from_dataframe(stacked_df)
warmup_numba_kernels(panel)

strat = ChildParentStrategy({"hold_days": 5})
strat.set_panel(panel)
scanner = ParamScanner(panel, strat._entry_signals, mode="fixed")

df = scanner.scan_fast(
    hold_days=[3, 5, 7, 10, 15],
    stop_loss=[0.03, 0.05, 0.08, 0.10],
    take_profit=[0.08, 0.12, 0.15, 0.20],
    trade_size=[20_000, 30_000, 50_000],
)
```

### scan_fast vs scan_full

| 模式 | 方法 | 回傳 |
|------|------|------|
| fast | `scan_fast()` / `run_single_fast()` | 僅 `final_equity`（極速篩選） |
| full | `scan_full()` / `run_single_full()` | 含 sharpe、equity_curve 等 |

> **Numba 並行**：若環境變數 `NUMBA_NUM_THREADS=1`（IDE 常見），`import chidoribt.benchmark` 會自動改為 CPU 核心數。

---

## 自訂策略

```python
import pandas as pd
from chidoribt.strategies.base import MultiAssetStrategy
from chidoribt.core import PanelData

class MyStrategy(MultiAssetStrategy):
    def init(self):
        pass

    def next(self, timestamp: pd.Timestamp, panel: PanelData, t: int) -> dict[str, bool]:
        """每個交易日回傳 {stock_id: True/False} 進場訊號。"""
        signals = {}
        for i, stock_id in enumerate(panel.stock_ids):
            close = panel.close[t, i]
            ma20  = panel.close[max(0, t-20):t, i].mean() if t >= 20 else close
            if close > ma20:
                signals[stock_id] = True
        return signals
```

搭配 `ChidoriBT` 執行完整回測：

```python
from chidoribt import ChidoriBT

bt = ChidoriBT(data=panel, strategy=MyStrategy, cash=1_000_000, commission=0.001425)
result = bt.run()
print(result["metrics"])
```

---

## 執行模式

| 模式 | 參數 | 說明 |
|------|------|------|
| JIT 加速（預設） | `fast=True` | Numba JIT，最快 |
| 事件驅動 | `fast=False` | 含完整 fills/orders；**支援 ML 插入** |
| 等權再平衡 | `mode="rebalance"` | 每日等權分配；`ParamScanner(mode="rebalance")` 可批量掃描 |

```python
result = bt.run(mode="portfolio", fast=True)   # 預設
result = bt.run(mode="portfolio", fast=False)  # audit log + ML
result = bt.run(mode="rebalance", fast=True)   # 等權再平衡
```

**回傳欄位：** `equity_curve`、`trades`、`fills`（fast=False）、`metrics`（total_return、sharpe、max_drawdown、win_rate、n_trades）

---

## 台股費率說明

| 方向 | 費用 |
|------|------|
| 買進 | 手續費 **0.1425%** |
| 賣出 | 手續費 **0.1425%** + 證交稅 **0.3%** |

目前 `commission=0.001425` 為對稱費率，與 vectorbt benchmark 對齊，**尚未模擬賣出證交稅**。

---

## 套件架構

```
chidoribt/
├── benchmark.py             # Numba 設定、DB 載入、掃描基準工具
├── engine.py                # 主引擎
├── core/
│   ├── panel.py             # PanelData（T×N NumPy 面板）
│   ├── numba_kernels.py     # @njit kernel
│   └── scanner.py           # ParamScanner（批量 prange 掃描）
├── strategies/              # MultiAssetStrategy + 範例策略
├── analytics/               # 績效指標
└── data/                    # PostgreSQL / Mini-TSDB 載入
```

---

## ML 模型整合（`fast=False`）

事件驅動模式允許在每個時步動態呼叫 ML 模型推論，vectorbt 向量化架構無法支援。

```python
import numpy as np
from chidoribt import ChidoriBT
from chidoribt.strategies.base import MultiAssetStrategy
from chidoribt.core import PanelData

class MLStrategy(MultiAssetStrategy):
    def __init__(self, model, params=None):
        super().__init__(params or {})
        self.model = model

    def init(self):
        self.window = 20

    def next(self, timestamp, panel: PanelData, t: int) -> dict[str, bool]:
        if t < self.window:
            return {}
        window_close = panel.close[t - self.window:t, :]
        returns = np.diff(window_close, axis=0) / (window_close[:-1] + 1e-9)
        X = np.hstack([
            returns.mean(axis=0, keepdims=True).T,
            returns.std(axis=0, keepdims=True).T,
            (returns[-5:].sum(axis=0, keepdims=True)).T,
        ])
        probs = self.model.predict_proba(X)[:, 1]
        return {panel.stock_ids[i]: True for i, p in enumerate(probs) if p > 0.6}

bt = ChidoriBT(
    data=panel,
    strategy=MLStrategy(model=trained_model),
    cash=1_000_000,
    commission=0.001425,
    fast=False,   # 必須使用事件驅動模式
)
result = bt.run()
```

---
