Metadata-Version: 2.4
Name: chidoribt
Version: 0.3.0
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"
Provides-Extra: tsdb
Requires-Dist: mini-tsdb>=0.1.0; extra == "tsdb"
Provides-Extra: postgres
Requires-Dist: psycopg2-binary>=2.9; extra == "postgres"
Provides-Extra: full
Requires-Dist: numba>=0.56; extra == "full"
Requires-Dist: mini-tsdb>=0.1.0; extra == "full"
Requires-Dist: psycopg2-binary>=2.9; 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×**（2.6 ms vs 58 ms）；單次 **full** 含指標快 **1.5×**（119 ms vs 177 ms）
- **省**：單次 fast 記憶體 **2.0 MB** vs vectorbt **6.6 MB**；單次 full **2.0 MB** vs **9.7 MB**；批量掃描僅需 **1/26**（7.5 MB vs 194.7 MB，240 組合）
- **彈性**：事件驅動模式可在每個時步嵌入任意 ML 模型推論，vectorbt 向量化架構無法支援此設計

---

## 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 |
| 單次 fast 記憶體³ | **2.0 MB** | 6.6 MB |
| 單次 full 記憶體³ | **2.0 MB** | 9.7 MB |
| 參數掃描記憶體（240 組合） | **7.5 MB** | 194.7 MB（serial）|
| ML 模型動態插入 | ✅ 事件驅動模式 | ❌ |
| 整數股數（台股張數） | ✅ | ❌ |
| 完整訂單簿（fills/orders） | ✅ | ❌ |
| 內建多核並行掃描 | ✅ ParamScanner | ❌（需自行 multiprocessing）|
| 內建技術指標數量 | 基本 | 60+ |

> ¹ fast：`run_single_fast`（batch_scan P=1）vs vectorbt 只讀 `final_value`；100 股 × 726 日、2023–2025。  
> ² full：`run_single_full`（direct kernel + 交易重建）vs vectorbt 同等四項指標；同規模。  
> ³ tracemalloc 主進程峰值；vectorbt full 因額外讀取 `trades` / `sharpe` 而高於 fast（6.6 → 9.7 MB）。  
> 公平性說明見 [docs/benchmark_paths.md](docs/benchmark_paths.md#單次-fast-vs-full-公平性說明)（本 repo 內文檔）。

---

## 效能基準（Benchmark）

> 測試規模：**100 支台股 × 726 個交易日**（2023–2025）  
> 策略：子母線型態（ChildParentStrategy）+ 止損 10% / 止盈 15%  
> 計時範圍：含訊號計算，不含 DB 載入與 PanelData 建構

### 單次 fast vs full（1 組參數，100 股 × 726 日，2023–2025）

> 共同前置：相同訊號、**對稱手續費 0.1425%**（與 vectorbt `fees=` 對齊，見下方台股費率說明）、共用現金池；計時含訊號計算，**不含** DB 載入與 PanelData 建構；JIT 暖機不計入。

**fast（僅淨值，參數篩選場景）**

```bash
python run_speed_benchmark.py --single-compare-fast --stocks 100 \
  --start 2023-01-01 --end 2025-12-31 \
  --hold-days 13 --stop-loss 0.10 --take-profit 0.15 --single-trade-size 50000
```

| 路徑 | 實作 | 均值 | 記憶體 |
|------|------|:----:|:------:|
| **Path S** | `run_single_fast`（`batch_scan_fixed` P=1） | **2.6 ms** | 2.0 MB |
| **Path I** | `from_signals` + 只讀 `final_value` | 58 ms | 6.6 MB |

**full（含 sharpe / n_trades，策略分析場景）**

```bash
python run_speed_benchmark.py --single-compare --stocks 100 \
  --start 2023-01-01 --end 2025-12-31 \
  --hold-days 13 --stop-loss 0.10 --take-profit 0.15 --single-trade-size 50000
```

| 路徑 | 實作 | 均值 | 記憶體 |
|------|------|:----:|:------:|
| **Path S** | `run_single_full`（direct kernel + 交易重建 + metrics） | **119 ms** | 2.0 MB |
| **Path I** | `from_signals` + sharpe / trades / total_return | 177 ms | 9.7 MB |

**為何 fast 差 22×，full 只差 1.5×？**

| 面向 | fast | full |
|------|------|------|
| ChidoriBT 額外工作 | 無（kernel 直接出淨值） | 交易重建 + Sharpe 計算（**+46× 開銷**） |
| vectorbt 額外工作 | 仍跑完整 `from_signals` | 再讀 `trades.count` / `sharpe_ratio`（**+3× 開銷**） |
| 結論 | 淨值篩選是 ChidoriBT 主場 | 要完整指標時差距縮小 |

> fast 的 22× **不代表**「單次完整回測快 22×」——兩邊在 fast 下**輸出欄位相同**（僅 `final_equity`），但 ChidoriBT 可跳過事後分析，vectorbt 仍須跑完整模擬。詳見 [公平性說明](docs/benchmark_paths.md#單次-fast-vs-full-公平性說明)。

**結果一致性（hold=13，兩模式 final_equity 相同）**

| 指標 | ChidoriBT | vectorbt | 備註 |
|------|:---------:|:--------:|------|
| final_equity | 1,057,009 | 1,023,305 | 見下方說明 |
| sharpe（full） | 0.225 | 0.154 | full 模式輸出 |
| n_trades（full） | 704 | 731 | 成交規則不同 |

**`final_equity` 差異 +3.3% 說明：** ChidoriBT 使用整數股數（`int(50_000 / price)`），vectorbt 使用浮點連續量。整數截斷使每筆買進略少花費、累積 704 筆後淨值高出約 3.3%，屬預期行為，不代表回測錯誤。

### 批量掃描（`--scan`，多組參數）

`--scan-output` 控制批量掃描輸出層級（與單次 full 不同）：

| 模式 | 回傳內容 | 用途 |
|------|---------|------|
| `fast`（預設） | 僅 `final_equity` | 240 組合極速篩選 |
| `full` | 含 sharpe / n_trades 等 | 批量掃描的完整指標 |

**批量掃描時差距顯著**，詳見下方「參數掃描 vs vectorbt」。

### 參數掃描 vs vectorbt（240 組合，100 股 × 726 日）

> 執行：`python run_speed_benchmark.py --scan --stocks 100 --scan-hold-days "3,5,7,10,15" --scan-stop-loss "0.03,0.05,0.08,0.10" --scan-take-profit "0.08,0.12,0.15,0.20" --scan-trade-size "20000,30000,50000"`

| 引擎 | 均值（240 組合） | 吞吐量 | 峰值記憶體 | vs ChidoriBT |
|------|:-----------:|:------:|:----------:|:------------:|
| **ChidoriBT** ParamScanner | **23.3 ms** | **10,284 組合/s** | **7.5 MB** | — |
| vectorbt serial（單核） | 14.55 s | 16 組合/s | 194.7 MB | 624× 慢 |
| vectorbt multiprocessing（12 核）¹ | 989.7 ms | 242 組合/s | — ² | 42× 慢 |

> ¹ Pool 預熱（含模組載入）不計入計時，與 ChidoriBT 排除 JIT 暖機的做法一致。
> ² 各 worker 記憶體未納入追蹤；主進程僅傳輸資料（1.4 MB）。

**為何 vectorbt multiprocessing 仍慢 42×？**
ChidoriBT 批量掃描在單進程內以共享記憶體並行計算，所有組合共用同一份資料，無跨進程傳輸開銷。vectorbt 每個組合需透過 IPC 傳送資料、在 worker 內重建物件再回傳結果，這些固定開銷無法透過增加 worker 數消除。

---

ChidoriBT is a high-performance Python backtesting library for multi-asset portfolio strategies. It supports vectorized (NumPy/Numba) execution, flexible data ingestion (DataFrame, PostgreSQL, Mini-TSDB), and produces detailed trade/equity analytics.

---

## Requirements

- **Python** ≥ 3.9, < 3.13
- **numpy** ≥ 1.23、**pandas** ≥ 1.5（core，無額外依賴）
- **numba** ≥ 0.56（選用，建議安裝以啟用 JIT 加速）

---

## Installation

**Core（DataFrame 傳入，無額外依賴）：**

```bash
pip install chidoribt
```

**含 Numba JIT 加速（建議）：**

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

**含 PostgreSQL 資料載入：**

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

**含 Mini-TSDB 時序資料庫：**

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

**全功能安裝：**

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

---

## 快速開始

### 方式一：直接傳入 DataFrame（OHLCV 格式，與原版 backtesting.py 欄位相容）

```python
import pandas as pd
from chidoribt import ChidoriBT

# 單資產 OHLCV DataFrame（欄位：Open, High, Low, Close, Volume）
ohlcv_df = pd.read_csv("AAPL.csv", index_col="date", parse_dates=True)

bt = ChidoriBT(data=ohlcv_df, cash=100_000, commission=0.001)
result = bt.run()

print(result["metrics"])
# {'total_return': 0.42, 'sharpe': 1.35, 'max_drawdown': -0.18, ...}
```

### 方式二：多資產 stacked DataFrame

```python
from chidoribt import ChidoriBT
from chidoribt.core import PanelData

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

bt = ChidoriBT(data=panel, strategy=MyStrategy, cash=100_000)
result = bt.run()
equity_curve = result["equity_curve"]   # pd.Series，index 為日期
trades_df    = result["trades"]         # pd.DataFrame，每筆交易記錄
```

### 方式三：Mini-TSDB 載入（需 `pip install "chidoribt[tsdb]"`）

```python
from chidoribt.data import StockTSDBLoader
from chidoribt import ChidoriBT
from chidoribt.core import PanelData

loader = StockTSDBLoader("./data/tsdb")
raw_df = loader.load(start_date="2023-01-01", end_date="2025-12-31")

bt = ChidoriBT(
    data=PanelData.from_dataframe(raw_df),
    strategy=MyStrategy,
    cash=100_000,
)
result = bt.run()
```

### 方式四：PostgreSQL 載入（需 `pip install "chidoribt[postgres]"`）

```python
from chidoribt.data import StockPostgresLoader
from chidoribt import ChidoriBT
from chidoribt.core import PanelData

loader = StockPostgresLoader()  # 讀取環境變數 DB_HOST / DB_PORT / DB_NAME
raw_df = loader.load(start_date="2023-01-01", end_date="2025-12-31")

bt = ChidoriBT(
    data=PanelData.from_dataframe(raw_df),
    strategy=MyStrategy,
    cash=100_000,
)
result = bt.run()
```

---

## 自訂策略

```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} 代表當日進場訊號；
        未包含在回傳 dict 中的股票視為無訊號。
        """
        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
```

---

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

`fast=False` 事件驅動模式允許在每個時步動態呼叫任意 Python 物件，包括 ML 模型推論。vectorbt 的向量化架構無法支援此設計。

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

class MLStrategy(MultiAssetStrategy):
    """在每個時步動態呼叫 ML 模型推論（vectorbt 無法支援此模式）"""

    def __init__(self, model, params=None):
        super().__init__(params or {})
        self.model = model   # 任何 sklearn / xgboost / lightgbm 模型

    def init(self):
        self.window = 20     # 特徵窗口

    def next(self, timestamp: pd.Timestamp, panel: PanelData, t: int) -> dict[str, bool]:
        if t < self.window:
            return {}

        # 即時特徵計算（僅使用 t 之前的資料，無 look-ahead bias）
        window_close = panel.close[t - self.window:t, :]  # shape: (window, N)
        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,  # 近 5 日動能
        ])  # shape: (N, 3)

        probs = self.model.predict_proba(X)[:, 1]   # 上漲機率

        return {
            panel.stock_ids[i]: True
            for i, p in enumerate(probs) if p > 0.6
        }


# 使用方式
from sklearn.ensemble import GradientBoostingClassifier

model = GradientBoostingClassifier().fit(X_train, y_train)

bt = ChidoriBT(
    data=panel,
    strategy=MLStrategy(model=model),
    cash=1_000_000,
    commission=0.001425,   # 買進手續費 0.1425%（見下方台股費率說明）
    fast=False,            # ← 必須使用事件驅動模式以支援動態推論
)
result = bt.run()
print(result["metrics"])
```

### 台股費率說明

台股實際費用結構：

| 方向 | 費用 |
|------|------|
| 買進 | 手續費 **0.1425%** |
| 賣出 | 手續費 **0.1425%** + 證交稅 **0.3%**（一般股票） |

目前 `commission=0.001425` 為**單一對稱費率**（買賣同率），與 vectorbt benchmark 的 `fees=0.001425` 對齊，**尚未模擬賣出證交稅 0.3%**。因此回測結果會比實盤略為樂觀。若需更貼近實盤，請在策略層自行扣除賣出稅負，或等待框架支援非對稱費率 `(buy_commission, sell_commission)`。

---

## 參數掃描（ParamScanner）

批量掃描時所有參數組合共享同一份 price matrix，無需逐組重建 DataFrame。

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

panel = PanelData.from_dataframe(stacked_df)
strat = ChildParentStrategy({"hold_days": 5})
strat.set_panel(panel)

scanner = ParamScanner(panel, strat._entry_signals, mode="fixed")

# fast：僅跑 batch kernel，不計 Sharpe / MDD
# 回傳 pd.DataFrame，欄位：
#   hold_days, stop_loss, take_profit, trade_size, final_equity,
#   total_return（由淨值推算）, sharpe=0, max_drawdown=0
df_fast = scanner.scan_fast(
    hold_days=[3, 5, 7], stop_loss=[0.05, 0.08], take_profit=[0.10, 0.15],
    trade_size=[30_000, 50_000],
)

# full：含 equity curve 與完整指標（與 vectorbt 主要指標對齊）
# 回傳 pd.DataFrame，欄位：
#   上述 + sharpe, max_drawdown, equity_curve（每列一條 pd.Series）
#   注意：n_trades 需由 run_single_full() 或交易重建另行取得
df_full = scanner.scan_full(
    hold_days=[3, 5, 7], stop_loss=[0.05, 0.08], take_profit=[0.10, 0.15],
    trade_size=[30_000, 50_000],
)
print(df_full[["hold_days", "final_equity", "total_return", "sharpe"]].head())

# 等權再平衡批量掃描（無 trade_size 維度）
scanner_reb = ParamScanner(panel, strat._entry_signals, mode="rebalance")
df_reb = scanner_reb.scan_fast(
    hold_days=[5, 10], stop_loss=[0.05], take_profit=[0.10],
)
```

基準測試對照 vectorbt 時，請加上 `--scan-output full`：

```bash
python run_speed_benchmark.py --scan --stocks 100 --scan-output full \
  --scan-hold-days "13" --scan-stop-loss "0.10" \
  --scan-take-profit "0.15" --scan-trade-size "50000"
```

---

## 執行模式

| 模式 | `mode` 參數 | `fast` 參數 | 說明 |
|------|-------------|-------------|------|
| JIT 加速（預設） | `"portfolio"` | `True` | Numba JIT，最快；無中間物件 |
| 事件驅動 | `"portfolio"` | `False` | 純 Python 迴圈，含完整 fills/orders log；**支援 ML 插入** |
| 等權再平衡 | `"rebalance"` | — | 每日依等權分配再平衡 |

```python
# Numba JIT 加速（預設 fast=True）
result = bt.run(mode="portfolio", fast=True)

# 純 Python 路徑（含完整 audit log，適合嵌入 ML 模型）
result = bt.run(mode="portfolio", fast=False)
fills_df  = result["fills"]   # 每筆成交記錄
orders_df = result["orders"]  # 每筆下單記錄

# 等權再平衡：每日將淨值等分到當日有訊號的持倉
# 底層走 run_rebalance_loop；與 ParamScanner(mode="rebalance") 同一 kernel
result = bt.run(mode="rebalance", fast=True)
print(result["metrics"]["final_equity"])
```

`mode="rebalance"` 與 `ParamScanner(mode="rebalance")` 的關係：

| API | 用途 |
|-----|------|
| `bt.run(mode="rebalance")` | 單組策略、單次回測，輸出 equity / trades / metrics |
| `ParamScanner(..., mode="rebalance")` | 多組 `(hold, sl, tp)` 批量掃描，輸出 DataFrame |

---

## 回傳結果欄位

```python
result = bt.run()

result["equity_curve"]  # pd.Series — 每日資金曲線
result["trades"]        # pd.DataFrame — 每筆已平倉交易
result["fills"]         # pd.DataFrame — 每筆成交（fast=False 時）
result["metrics"]       # dict — 績效指標
```

**`metrics` 常用欄位：**

| 欄位 | 說明 |
|------|------|
| `total_return` | 總報酬率 |
| `sharpe` | 年化 Sharpe ratio |
| `max_drawdown` | 最大回撤 |
| `win_rate` | 勝率 |
| `n_trades` | 總交易筆數 |

---

## Mini-TSDB 資料遷移

若已有 PostgreSQL 歷史資料，可使用遷移腳本一次性搬移：

```bash
# 初次遷移
python migrate_pg_to_tsdb.py --tsdb-dir ./data/tsdb

# 清除舊資料後重新遷移
python migrate_pg_to_tsdb.py --tsdb-dir ./data/tsdb --clean

# 查看 TSDB 內容
python migrate_pg_to_tsdb.py --tsdb-dir ./data/tsdb --list
```

---

## 套件架構

```
chidoribt/
├── __init__.py              # ChidoriBT, PanelData, ParamScanner（公開 API）
├── engine.py                # 主引擎（fast=True / fast=False 路由）
├── core/
│   ├── panel.py             # PanelData（T×N NumPy 面板，C-contiguous）
│   ├── numba_kernels.py     # 所有 @njit kernel（核心加速層）
│   ├── scanner.py           # ParamScanner（批量 prange 掃描）
│   ├── portfolio.py         # UnifiedPortfolio（事件驅動回測）
│   └── trade_reconstructor.py
├── strategies/              # MultiAssetStrategy base + 範例策略
├── analytics/               # 績效指標計算
├── data/                    # 資料載入子套件
│   ├── _postgres.py         # StockPostgresLoader（psycopg2，optional）
│   └── _tsdb.py             # StockTSDBLoader（mini-tsdb，optional）
├── adapters/                # 內部 adapter 層
└── utils/                   # trade_logger 等工具
```

---
