Skip to main content

finlab-sentinel

Python versions Windows Linux macOS License: MIT CI coverage

finlab-sentinel 是 finlab 套件的防禦層,用於監控 data.get API 的資料變化,防止未預期的資料異動影響回測或選股結果。

功能特色

  • 自動比對: 每次 data.get 時自動比對歷史資料
  • 滾動備份: 保留 7 天(可配置)的備份資料
  • 智慧檢測:
    • 數值容差比對(可配置 rtol/atol)
    • dtype 變更檢測
    • NA 類型差異檢測(pd.NA vs np.nan vs None)
  • 彈性政策:
    • append_only: 只允許新增,不允許刪除或修改歷史
    • threshold: 允許小幅度變更(如 10% 以內)
    • 黑名單配置:指定可修改歷史的資料集
  • 可配置行為:
    • 拋出例外(預設)
    • 警告並使用快取
    • 警告並使用新資料
  • Preprocess Hook: 比對前預處理(如四捨五入),支援萬用字元模式
  • 通知機制: 支援自訂 callback(如 LINE、email 通知)
  • CLI 工具: 管理備份、查看差異、接受新資料
  • 時間旅行: 回到歷史時間點取得當時的備份資料
  • 永久 Patch: accept 新資料時自動保存舊 baseline 快照,不受備份清理影響,隨時可查閱、匯出或還原(撤銷 accept)

安裝

pip install finlab-sentinel

或使用 uv:

uv add finlab-sentinel

快速開始

import finlab_sentinel

# 啟用 sentinel
finlab_sentinel.enable()

# 正常使用 finlab
from finlab import data
close = data.get('price:收盤價')  # 自動備份並比對

# 如果資料異常,會根據配置拋出例外或警告

配置

建立 sentinel.toml 檔案:

[storage]
path = "~/.finlab-sentinel/"
retention_days = 7

[comparison]
rtol = 1e-5
change_threshold = 0.10

[comparison.policies]
default_mode = "append_only"
history_modifiable = ["fundamental_features:某些財報資料"]
# 允許 NA→有值 的轉換(例如:預估資料後來補上)
allow_na_to_value = ["price:收盤價"]

[anomaly]
behavior = "raise"  # raise | warn_return_cached | warn_return_new
save_reports = true

# 可選:設定通知 callback
# callback = "myproject.notifications:send_line"

CLI 使用

# 列出所有備份
sentinel list

# 清理過期備份
sentinel cleanup --days 14

# 查看資料差異
sentinel diff "price:收盤價"

# 接受新資料作為基準(會印出保存舊 baseline 的 patch id)
sentinel accept "price:收盤價" --reason "確認資料修正"

# 一定要能撤銷:無法建立 patch 時不接受、不做任何變更;--json 輸出單行 JSON
sentinel accept "price:收盤價" --require-patch --json

# 匯出備份
sentinel export "price:收盤價" -o ./backup.parquet

# 管理永久 patch(accept 時自動產生)
sentinel patch list
sentinel patch show <patch_id>
sentinel patch export <patch_id> -o ./old_data.parquet
sentinel patch delete <patch_id>
sentinel patch restore <patch_id>   # 撤銷 accept:還原 patch 保存的舊 baseline

處理資料異常

當檢測到資料異常時:

from finlab_sentinel import DataAnomalyError

try:
    close = data.get('price:收盤價')
except DataAnomalyError as e:
    print(f"資料異常: {e.report.summary}")
    # 檢查報告詳情
    print(f"變動比例: {e.report.comparison_result.change_ratio:.1%}")

    # 如果確認要接受新資料
    from finlab_sentinel.core.interceptor import accept_current_data
    accept_current_data('price:收盤價', reason="確認資料修正")

需要知道 accept 產生的 patch(例如之後要撤銷)時,改用 accept_dataset:

import finlab_sentinel as fs

result = fs.accept_dataset("price:收盤價", reason="確認資料修正", require_patch=True)
result.patch_id    # 保存舊 baseline 的 patch;fs.restore_patch(result.patch_id) 即撤銷
result.to_dict()   # 可序列化成 JSON 的結果摘要

accept_dataset 失敗時拋出 AcceptError(找不到 baseline、取不到目前資料、require_patch=True 卻無法建立 patch、或 accept 途中 baseline 被其他程序更新),baseline 保持不變。require_patch=True 時,只要 baseline 有任何改變(content hash 或原始資料,即使 preprocess hook 讓 hash 相同、或 [accept] create_patch = false)都會建立 patch;資料完全相同時不需要 patch,patch_id 為 None。

Preprocess Hook

Preprocess hook 讓你可以在比對前先對資料做預處理,例如四捨五入、排序欄位等。這在處理預期的浮點數精度差異時特別有用。

注意: 預處理只用於比對,回傳給使用者的永遠是原始資料。

import finlab_sentinel

# 註冊特定 dataset 的 preprocess hook
finlab_sentinel.register_preprocess_hook(
    "price:收盤價",
    lambda df: df.round(2)  # 四捨五入到小數第二位
)

# 支援萬用字元模式
finlab_sentinel.register_preprocess_hook(
    "price:*",  # 符合所有 price: 開頭的 dataset
    lambda df: df.round(2)
)

# 也支援 ? 萬用字元(符合單一字元)
finlab_sentinel.register_preprocess_hook(
    "price:?",
    lambda df: df.round(2)
)

finlab_sentinel.enable()

# 使用 finlab
from finlab import data
close = data.get('price:收盤價')  # 比對時會先 round(2),但回傳原始資料

進階用法

import finlab_sentinel

# 自訂預處理函式
def normalize_for_comparison(df):
    """標準化 DataFrame 以忽略預期的差異"""
    df = df.copy()
    # 四捨五入數值欄位
    numeric_cols = df.select_dtypes(include=['float64', 'float32']).columns
    df[numeric_cols] = df[numeric_cols].round(4)
    # 排序欄位(忽略欄位順序差異)
    df = df[sorted(df.columns)]
    return df

finlab_sentinel.register_preprocess_hook("fundamental_features:*", normalize_for_comparison)

# 取消註冊
finlab_sentinel.unregister_preprocess_hook("price:收盤價")

# 清除所有 hooks
finlab_sentinel.clear_preprocess_hooks()

優先順序

當多個 pattern 都符合時,精確匹配優先於萬用字元匹配:

finlab_sentinel.register_preprocess_hook("price:*", lambda df: df.round(1))
finlab_sentinel.register_preprocess_hook("price:收盤價", lambda df: df.round(2))

# "price:收盤價" 會使用 round(2)(精確匹配)
# "price:開盤價" 會使用 round(1)(萬用字元匹配)

時間旅行 (Time Travel)

時間旅行功能讓你可以回到過去的某個時間點,取得當時備份的資料。這對於重現歷史回測結果、調查資料異動,或驗證策略在特定時間點的表現非常有用。

基本用法

import finlab_sentinel as fs
from datetime import datetime

# 啟用 sentinel
fs.enable()

# 設定時間旅行到指定時間點
fs.set_time_travel(datetime(2024, 1, 5, 14, 30))

# 現在 data.get() 會回傳該時間點的備份資料
from finlab import data
close = data.get("price:收盤價")  # 回傳 2024-01-05 14:30 的備份資料

# 結束時間旅行,回到正常模式
fs.exit_time_travel()

查詢狀態

# 查詢目前時間旅行狀態
status = fs.get_time_travel_status()
print(status)
# {'enabled': True, 'target_time': '2024-01-05T14:30:00'}

錯誤處理

from finlab_sentinel import NoHistoricalDataError

try:
    fs.set_time_travel(datetime(2020, 1, 1))  # 很久以前
    close = data.get("price:收盤價")
except NoHistoricalDataError as e:
    print(f"找不到該時間點的備份資料: {e}")
finally:
    fs.exit_time_travel()

使用情境

  • 重現歷史回測: 確保使用與當時相同的資料進行回測
  • 調查資料異動: 比對不同時間點的資料差異
  • 驗證策略表現: 在特定歷史時間點驗證選股策略

永久 Patch

當你 accept 新資料作為 baseline 時,sentinel 會自動建立一個永久 patch,保存 accept 前的舊 baseline 完整快照與 diff 摘要。Patch 存放在 <storage.path>/patches/,不受滾動備份的 retention 清理影響,之後可以隨時查閱、匯出舊資料,或還原成 baseline(見下方「還原 patch」)。

Patch ID 的格式為 <backup_key>__<時間>;同一秒內建立多個 patch 時,後建立者會加上 _2、_3 等後綴。

CLI 用法

# 列出所有 patch(可用 --dataset 過濾)
sentinel patch list

# 查看 patch 詳情(含 diff 摘要)
sentinel patch show "price__收盤價__2026-07-11T10-30-00"

# 匯出 accept 前的舊資料
sentinel patch export "price__收盤價__2026-07-11T10-30-00" -o ./old_data.parquet

# 刪除 patch(需確認,或加 --force)
sentinel patch delete "price__收盤價__2026-07-11T10-30-00"

# 還原 patch,撤銷當初的 accept(需確認,或加 --yes)
sentinel patch restore "price__收盤價__2026-07-11T10-30-00"

Python API

import finlab_sentinel as fs

# 列出 patch
patches = fs.list_patches("price:收盤價")
for p in patches:
    print(p.patch_id, p.created_at, p.diff_summary["summary_text"])

# 取回 accept 前的舊 baseline DataFrame
old_close = fs.load_patch_data(patches[0].patch_id)

還原 patch(撤銷 accept)

sentinel patch restore 以 patch 保存的舊 baseline 取代該資料源目前的 baseline,回到 accept 之前的狀態:資料與 content hash 都與 accept 前完全相同,因此下次 data.get 的比對行為也與 accept 前一致(例如當初觸發異常的新資料會再次觸發異常)。

# 先預覽:列出目前 baseline 與 patch 資料的 hash、時間、大小,不寫入任何東西
sentinel patch restore "price__收盤價__2026-07-11T10-30-00" --dry-run

# 還原(需確認,或加 --yes / -y(同 --force / -f)略過;--reason 記錄原因)
sentinel patch restore "price__收盤價__2026-07-11T10-30-00" --reason "撤銷誤判的 accept"

# 給程式呼叫:--json 輸出單行 JSON(需搭配 --yes 或 --dry-run,不會詢問)
sentinel patch restore "price__收盤價__2026-07-11T10-30-00" --yes --json

確認時只會取代預覽中看到的那個 baseline:若預覽之後 baseline 又被更新,還原會中止(回傳 1),不做任何變更。

import finlab_sentinel as fs

result = fs.restore_patch("price__收盤價__2026-07-11T10-30-00", reason="撤銷誤判的 accept")
result.changed       # baseline 是否被取代
result.new_patch_id  # 被取代的 baseline 另存成的新 patch;還原它即可撤銷這次還原
result.to_dict()     # 可序列化成 JSON 的結果摘要

# 只預覽(不寫入):preview.previous 是目前的 baseline,preview.patch 是要還原的 patch
preview = fs.restore_patch("price__收盤價__2026-07-11T10-30-00", dry_run=True)
# 確認後還原;預覽之後 baseline 若有變動則拋出 PatchRestoreError、不做任何變更
fs.restore_patch("price__收盤價__2026-07-11T10-30-00", expected_latest=preview.latest)
  • 還原本身也可以再還原:被取代的 baseline 會先另存成一個新 patch(reason 預設為 restore of <patch_id>,patch show 會顯示 Restored From),之後 restore 這個新 patch 就能回到還原前。
  • 來源 patch 會保留:還原不會消耗或刪除 patch,同一個 patch 可以重複使用;若 baseline 已與 patch 相同(hash 與資料皆一致)則不寫入任何東西,重複執行是安全的。
  • content hash 原樣還原、不重新計算:baseline 的 hash 是 data.get 攔截時對 preprocess hook 處理後的資料計算的,還原時直接沿用 patch 記錄的值,所以在沒有註冊 hook 的 CLI 中還原也正確。
  • Retention:還原後的 baseline 是還原當下建立的一般備份,retention 清理對待它的方式與剛存入的 baseline 相同;accept 與還原之前的備份仍留在歷史中(時間旅行查得到),照一般規則過期。
  • 不會讓資料源失去 baseline:新檔案完整寫入並 flush 到磁碟後才更新索引;任何一步失敗,還原會中止並拋出 PatchRestoreError(CLI 回傳 1),原本的 baseline 保持不變,也不留下半成品。
  • 不要在啟用 sentinel 的程序執行中還原(或 accept)同一個資料源:寫入 baseline 時一律檢查最新的 baseline 仍是當初讀到的那一個(compare-and-swap),所以兩邊都不會覆蓋對方——還原途中 baseline 被改動時還原會中止;data.get 比對到一半時 baseline 被還原,它不會存檔覆蓋(只記錄警告),下次 data.get 才以還原後的 baseline 比對。但那個執行中的程序已經拿到、並依舊 baseline 驗證過的資料不會因此改變,所以請等它結束再還原。
  • 資料源目前沒有 baseline 時,patch 資料直接成為 baseline(沒有東西需要另存);patch 不存在時拋出 PatchNotFoundError(CLI 回傳 1),不做任何變更。

關閉自動建立

在 sentinel.toml 中設定:

[accept]
create_patch = false

注意:若 accept 時新資料與現有 baseline 完全相同(hash 一致),不會產生 patch;patch 建立失敗(如磁碟已滿)不會阻擋 accept 本身,只會記錄錯誤日誌(accept_dataset 的 patch_error 會說明原因;要讓 accept 在這種情況下失敗,請用 --require-patch / require_patch=True)。無法計算 diff 時(例如 index 重複),patch 仍會建立,diff 摘要記為 diff unavailable。

自訂通知

def send_line_notification(report):
    """當檢測到異常時發送 LINE 通知"""
    import requests
    requests.post(
        "https://notify-api.line.me/api/notify",
        headers={"Authorization": f"Bearer {LINE_TOKEN}"},
        data={"message": f"finlab 資料異常: {report.summary}"}
    )

# 在 sentinel.toml 中設定
# [anomaly]
# callback = "myproject.notifications:send_line_notification"

開發

# Clone 專案
git clone https://github.com/yourusername/finlab-sentinel
cd finlab-sentinel

# 使用 uv 安裝開發依賴
uv sync --dev

# 執行測試
uv run pytest

# 執行 lint
uv run ruff check src/ tests/
uv run mypy src/

License

MIT License

Metadata

Release files for finlab-sentinel 0.1.11

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for finlab-sentinel 0.1.11
File Size Uploaded Provenance
finlab_sentinel-0.1.11.tar.gz 103.3 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for finlab-sentinel 0.1.11
File Interpreter ABI Platform
finlab_sentinel-0.1.11-py3-none-any.whl Python 3 none any Details

Total release size: 169.7 kB

Release files / finlab_sentinel-0.1.11.tar.gz

Download URL finlab_sentinel-0.1.11.tar.gz
Size 103.3 kB
Tags Source
SHA-256 checksum
How to use checksums
f041fe813e3346c18c410533ce3d26868f1b20d43c174647e418896f8f186092
BLAKE2b-256 checksum
How to use checksums
e0319c7bc173fb0c352f5d8ae0a2824f4921c159459ddc9b1039534c7cfc2023
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / finlab_sentinel-0.1.11-py3-none-any.whl

Download URL finlab_sentinel-0.1.11-py3-none-any.whl
Size 66.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
01056ccecd1dadabd824e7566c24b42f0c9764a718448172abbbba62f891da23
BLAKE2b-256 checksum
How to use checksums
665284b222eafae95e2dcf975251541dc69434049964971c7f70ead3388a1105
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Security for finlab-sentinel 0.1.11

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page