finlab-sentinel 0.1.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- finlab_sentinel-0.1.5/.github/workflows/ci.yml +77 -0
- finlab_sentinel-0.1.5/.github/workflows/release.yml +47 -0
- finlab_sentinel-0.1.5/.gitignore +146 -0
- finlab_sentinel-0.1.5/PKG-INFO +272 -0
- finlab_sentinel-0.1.5/README.md +233 -0
- finlab_sentinel-0.1.5/pyproject.toml +118 -0
- finlab_sentinel-0.1.5/sentinel.toml.example +76 -0
- finlab_sentinel-0.1.5/src/finlab_sentinel/__init__.py +23 -0
- finlab_sentinel-0.1.5/src/finlab_sentinel/cli/__init__.py +5 -0
- finlab_sentinel-0.1.5/src/finlab_sentinel/cli/main.py +485 -0
- finlab_sentinel-0.1.5/src/finlab_sentinel/comparison/__init__.py +27 -0
- finlab_sentinel-0.1.5/src/finlab_sentinel/comparison/differ.py +524 -0
- finlab_sentinel-0.1.5/src/finlab_sentinel/comparison/hasher.py +172 -0
- finlab_sentinel-0.1.5/src/finlab_sentinel/comparison/policies.py +223 -0
- finlab_sentinel-0.1.5/src/finlab_sentinel/comparison/report.py +259 -0
- finlab_sentinel-0.1.5/src/finlab_sentinel/config/__init__.py +25 -0
- finlab_sentinel-0.1.5/src/finlab_sentinel/config/loader.py +222 -0
- finlab_sentinel-0.1.5/src/finlab_sentinel/config/schema.py +130 -0
- finlab_sentinel-0.1.5/src/finlab_sentinel/core/__init__.py +9 -0
- finlab_sentinel-0.1.5/src/finlab_sentinel/core/hooks.py +195 -0
- finlab_sentinel-0.1.5/src/finlab_sentinel/core/interceptor.py +287 -0
- finlab_sentinel-0.1.5/src/finlab_sentinel/core/patcher.py +151 -0
- finlab_sentinel-0.1.5/src/finlab_sentinel/core/registry.py +69 -0
- finlab_sentinel-0.1.5/src/finlab_sentinel/exceptions.py +43 -0
- finlab_sentinel-0.1.5/src/finlab_sentinel/handlers/__init__.py +17 -0
- finlab_sentinel-0.1.5/src/finlab_sentinel/handlers/base.py +43 -0
- finlab_sentinel-0.1.5/src/finlab_sentinel/handlers/callback.py +110 -0
- finlab_sentinel-0.1.5/src/finlab_sentinel/handlers/exception.py +39 -0
- finlab_sentinel-0.1.5/src/finlab_sentinel/handlers/warning.py +92 -0
- finlab_sentinel-0.1.5/src/finlab_sentinel/py.typed +0 -0
- finlab_sentinel-0.1.5/src/finlab_sentinel/storage/__init__.py +10 -0
- finlab_sentinel-0.1.5/src/finlab_sentinel/storage/backend.py +186 -0
- finlab_sentinel-0.1.5/src/finlab_sentinel/storage/cleanup.py +83 -0
- finlab_sentinel-0.1.5/src/finlab_sentinel/storage/index.py +338 -0
- finlab_sentinel-0.1.5/src/finlab_sentinel/storage/parquet.py +317 -0
- finlab_sentinel-0.1.5/tests/__init__.py +1 -0
- finlab_sentinel-0.1.5/tests/conftest.py +98 -0
- finlab_sentinel-0.1.5/tests/unit/__init__.py +1 -0
- finlab_sentinel-0.1.5/tests/unit/test_cleanup.py +171 -0
- finlab_sentinel-0.1.5/tests/unit/test_cli.py +382 -0
- finlab_sentinel-0.1.5/tests/unit/test_config.py +158 -0
- finlab_sentinel-0.1.5/tests/unit/test_differ.py +207 -0
- finlab_sentinel-0.1.5/tests/unit/test_handlers.py +173 -0
- finlab_sentinel-0.1.5/tests/unit/test_hasher.py +100 -0
- finlab_sentinel-0.1.5/tests/unit/test_hooks.py +235 -0
- finlab_sentinel-0.1.5/tests/unit/test_interceptor.py +418 -0
- finlab_sentinel-0.1.5/tests/unit/test_loader.py +202 -0
- finlab_sentinel-0.1.5/tests/unit/test_patcher.py +199 -0
- finlab_sentinel-0.1.5/tests/unit/test_policies.py +265 -0
- finlab_sentinel-0.1.5/tests/unit/test_report.py +227 -0
- finlab_sentinel-0.1.5/tests/unit/test_storage.py +260 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main, develop]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
concurrency:
|
|
10
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
11
|
+
cancel-in-progress: true
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
lint:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Set up uv
|
|
20
|
+
uses: astral-sh/setup-uv@v3
|
|
21
|
+
with:
|
|
22
|
+
version: "latest"
|
|
23
|
+
|
|
24
|
+
- name: Set up Python
|
|
25
|
+
run: uv python install 3.12
|
|
26
|
+
|
|
27
|
+
- name: Install dependencies
|
|
28
|
+
run: uv sync --extra dev
|
|
29
|
+
|
|
30
|
+
- name: Run ruff linter
|
|
31
|
+
run: uv run ruff check src/ tests/
|
|
32
|
+
|
|
33
|
+
- name: Run ruff formatter check
|
|
34
|
+
run: uv run ruff format --check src/ tests/
|
|
35
|
+
|
|
36
|
+
- name: Run mypy type checker
|
|
37
|
+
run: uv run mypy src/
|
|
38
|
+
|
|
39
|
+
test:
|
|
40
|
+
runs-on: ${{ matrix.os }}
|
|
41
|
+
strategy:
|
|
42
|
+
fail-fast: false
|
|
43
|
+
matrix:
|
|
44
|
+
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
45
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
46
|
+
|
|
47
|
+
steps:
|
|
48
|
+
- uses: actions/checkout@v4
|
|
49
|
+
|
|
50
|
+
- name: Set up uv
|
|
51
|
+
uses: astral-sh/setup-uv@v3
|
|
52
|
+
with:
|
|
53
|
+
version: "latest"
|
|
54
|
+
|
|
55
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
56
|
+
run: uv python install ${{ matrix.python-version }}
|
|
57
|
+
|
|
58
|
+
- name: Install dependencies
|
|
59
|
+
run: uv sync --extra dev
|
|
60
|
+
|
|
61
|
+
- name: Run tests with coverage
|
|
62
|
+
run: uv run pytest tests/ -v --cov=src/finlab_sentinel --cov-report=xml --cov-report=html --cov-fail-under=80 --junitxml=junit/test-results-${{ matrix.python-version }}.xml
|
|
63
|
+
|
|
64
|
+
- name: Upload coverage reports to Codecov
|
|
65
|
+
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'
|
|
66
|
+
uses: codecov/codecov-action@v5
|
|
67
|
+
with:
|
|
68
|
+
token: ${{ secrets.CODECOV_TOKEN }}
|
|
69
|
+
|
|
70
|
+
- name: Upload test results
|
|
71
|
+
uses: actions/upload-artifact@v4
|
|
72
|
+
if: always()
|
|
73
|
+
with:
|
|
74
|
+
name: test-results-${{ matrix.os }}-${{ matrix.python-version }}
|
|
75
|
+
path: |
|
|
76
|
+
junit/
|
|
77
|
+
htmlcov/
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch: # Manual trigger only
|
|
5
|
+
|
|
6
|
+
jobs:
|
|
7
|
+
publish:
|
|
8
|
+
name: Build and Publish to PyPI
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
environment: pypi-publish # Requires environment approval
|
|
11
|
+
steps:
|
|
12
|
+
- name: Check out code
|
|
13
|
+
uses: actions/checkout@v4
|
|
14
|
+
|
|
15
|
+
- name: Install uv
|
|
16
|
+
uses: astral-sh/setup-uv@v3
|
|
17
|
+
with:
|
|
18
|
+
version: "latest"
|
|
19
|
+
|
|
20
|
+
- name: Set up Python
|
|
21
|
+
run: uv python install 3.12
|
|
22
|
+
|
|
23
|
+
- name: Install dependencies
|
|
24
|
+
run: uv sync --extra dev
|
|
25
|
+
|
|
26
|
+
- name: Verify project configuration
|
|
27
|
+
run: |
|
|
28
|
+
uv run python -c "import sys; sys.path.insert(0, 'src'); import finlab_sentinel; print(f'Package version: {finlab_sentinel.__version__}')"
|
|
29
|
+
|
|
30
|
+
- name: Build package
|
|
31
|
+
run: uv build
|
|
32
|
+
|
|
33
|
+
- name: Check package contents
|
|
34
|
+
run: uv run twine check dist/*
|
|
35
|
+
|
|
36
|
+
- name: Publish to PyPI
|
|
37
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
38
|
+
with:
|
|
39
|
+
password: ${{ secrets.PYPI_API_TOKEN }}
|
|
40
|
+
verbose: true
|
|
41
|
+
|
|
42
|
+
- name: Upload build artifacts
|
|
43
|
+
uses: actions/upload-artifact@v4
|
|
44
|
+
if: always()
|
|
45
|
+
with:
|
|
46
|
+
name: dist
|
|
47
|
+
path: dist/
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
*.manifest
|
|
31
|
+
*.spec
|
|
32
|
+
|
|
33
|
+
# Installer logs
|
|
34
|
+
pip-log.txt
|
|
35
|
+
pip-delete-this-directory.txt
|
|
36
|
+
|
|
37
|
+
# Unit test / coverage reports
|
|
38
|
+
htmlcov/
|
|
39
|
+
.tox/
|
|
40
|
+
.nox/
|
|
41
|
+
.coverage
|
|
42
|
+
.coverage.*
|
|
43
|
+
.cache
|
|
44
|
+
nosetests.xml
|
|
45
|
+
coverage.xml
|
|
46
|
+
*.cover
|
|
47
|
+
*.py,cover
|
|
48
|
+
.hypothesis/
|
|
49
|
+
.pytest_cache/
|
|
50
|
+
junit/
|
|
51
|
+
|
|
52
|
+
# Translations
|
|
53
|
+
*.mo
|
|
54
|
+
*.pot
|
|
55
|
+
|
|
56
|
+
# Django stuff:
|
|
57
|
+
*.log
|
|
58
|
+
local_settings.py
|
|
59
|
+
db.sqlite3
|
|
60
|
+
db.sqlite3-journal
|
|
61
|
+
|
|
62
|
+
# Flask stuff:
|
|
63
|
+
instance/
|
|
64
|
+
.webassets-cache
|
|
65
|
+
|
|
66
|
+
# Scrapy stuff:
|
|
67
|
+
.scrapy
|
|
68
|
+
|
|
69
|
+
# Sphinx documentation
|
|
70
|
+
docs/_build/
|
|
71
|
+
|
|
72
|
+
# PyBuilder
|
|
73
|
+
.pybuilder/
|
|
74
|
+
target/
|
|
75
|
+
|
|
76
|
+
# Jupyter Notebook
|
|
77
|
+
.ipynb_checkpoints
|
|
78
|
+
|
|
79
|
+
# IPython
|
|
80
|
+
profile_default/
|
|
81
|
+
ipython_config.py
|
|
82
|
+
|
|
83
|
+
# pyenv
|
|
84
|
+
.python-version
|
|
85
|
+
|
|
86
|
+
# pipenv
|
|
87
|
+
Pipfile.lock
|
|
88
|
+
|
|
89
|
+
# PEP 582
|
|
90
|
+
__pypackages__/
|
|
91
|
+
|
|
92
|
+
# Celery stuff
|
|
93
|
+
celerybeat-schedule
|
|
94
|
+
celerybeat.pid
|
|
95
|
+
|
|
96
|
+
# SageMath parsed files
|
|
97
|
+
*.sage.py
|
|
98
|
+
|
|
99
|
+
# Environments
|
|
100
|
+
.env
|
|
101
|
+
.venv
|
|
102
|
+
env/
|
|
103
|
+
venv/
|
|
104
|
+
ENV/
|
|
105
|
+
env.bak/
|
|
106
|
+
venv.bak/
|
|
107
|
+
|
|
108
|
+
# Spyder project settings
|
|
109
|
+
.spyderproject
|
|
110
|
+
.spyproject
|
|
111
|
+
|
|
112
|
+
# Rope project settings
|
|
113
|
+
.ropeproject
|
|
114
|
+
|
|
115
|
+
# mkdocs documentation
|
|
116
|
+
/site
|
|
117
|
+
|
|
118
|
+
# mypy
|
|
119
|
+
.mypy_cache/
|
|
120
|
+
.dmypy.json
|
|
121
|
+
dmypy.json
|
|
122
|
+
|
|
123
|
+
# Pyre type checker
|
|
124
|
+
.pyre/
|
|
125
|
+
|
|
126
|
+
# pytype static type analyzer
|
|
127
|
+
.pytype/
|
|
128
|
+
|
|
129
|
+
# Cython debug symbols
|
|
130
|
+
cython_debug/
|
|
131
|
+
|
|
132
|
+
# IDE
|
|
133
|
+
.idea/
|
|
134
|
+
.vscode/
|
|
135
|
+
*.swp
|
|
136
|
+
*.swo
|
|
137
|
+
*~
|
|
138
|
+
|
|
139
|
+
# Local config
|
|
140
|
+
sentinel.toml
|
|
141
|
+
|
|
142
|
+
# uv
|
|
143
|
+
uv.lock
|
|
144
|
+
|
|
145
|
+
# finlab-sentinel specific
|
|
146
|
+
.finlab-sentinel/
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: finlab-sentinel
|
|
3
|
+
Version: 0.1.5
|
|
4
|
+
Summary: Defensive monitoring layer for finlab data.get API - detect unexpected data changes
|
|
5
|
+
Project-URL: Homepage, https://github.com/iapcal/finlab-sentinel
|
|
6
|
+
Project-URL: Repository, https://github.com/iapcal/finlab-sentinel
|
|
7
|
+
Project-URL: Issues, https://github.com/iapcal/finlab-sentinel/issues
|
|
8
|
+
Author-email: iapcal <chiyimin2018@gmail.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: backtesting,data-validation,finlab,monitoring,taiwan-stock
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: pandas>=2.0.0
|
|
24
|
+
Requires-Dist: pyarrow>=14.0.0
|
|
25
|
+
Requires-Dist: pydantic-settings>=2.0.0
|
|
26
|
+
Requires-Dist: pydantic>=2.0.0
|
|
27
|
+
Requires-Dist: rich>=13.0.0
|
|
28
|
+
Requires-Dist: typer>=0.12.0
|
|
29
|
+
Requires-Dist: xxhash>=3.0.0
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: mypy>=1.10.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: pandas-stubs>=2.0.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
|
|
34
|
+
Requires-Dist: pytest-mock>=3.0.0; extra == 'dev'
|
|
35
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
36
|
+
Requires-Dist: ruff>=0.4.0; extra == 'dev'
|
|
37
|
+
Requires-Dist: twine>=5.0.0; extra == 'dev'
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
|
|
40
|
+
# finlab-sentinel
|
|
41
|
+
|
|
42
|
+

|
|
43
|
+

|
|
44
|
+

|
|
45
|
+

|
|
46
|
+
[](LICENSE)
|
|
47
|
+
[](https://github.com/iapcal/finlab-sentinel/actions/workflows/ci.yml)
|
|
48
|
+
[](https://codecov.io/gh/iapcal/finlab-sentinel)
|
|
49
|
+
|
|
50
|
+
**finlab-sentinel** 是 [finlab](https://github.com/finlab-python/finlab) 套件的防禦層,用於監控 `data.get` API 的資料變化,防止未預期的資料異動影響回測或選股結果。
|
|
51
|
+
|
|
52
|
+
## 功能特色
|
|
53
|
+
|
|
54
|
+
- **自動比對**: 每次 `data.get` 時自動比對歷史資料
|
|
55
|
+
- **滾動備份**: 保留 7 天(可配置)的備份資料
|
|
56
|
+
- **智慧檢測**:
|
|
57
|
+
- 數值容差比對(可配置 rtol/atol)
|
|
58
|
+
- dtype 變更檢測
|
|
59
|
+
- NA 類型差異檢測(pd.NA vs np.nan vs None)
|
|
60
|
+
- **彈性政策**:
|
|
61
|
+
- `append_only`: 只允許新增,不允許刪除或修改歷史
|
|
62
|
+
- `threshold`: 允許小幅度變更(如 10% 以內)
|
|
63
|
+
- 黑名單配置:指定可修改歷史的資料集
|
|
64
|
+
- **可配置行為**:
|
|
65
|
+
- 拋出例外(預設)
|
|
66
|
+
- 警告並使用快取
|
|
67
|
+
- 警告並使用新資料
|
|
68
|
+
- **Preprocess Hook**: 比對前預處理(如四捨五入),支援萬用字元模式
|
|
69
|
+
- **通知機制**: 支援自訂 callback(如 LINE、email 通知)
|
|
70
|
+
- **CLI 工具**: 管理備份、查看差異、接受新資料
|
|
71
|
+
|
|
72
|
+
## 安裝
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pip install finlab-sentinel
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
或使用 uv:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
uv add finlab-sentinel
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## 快速開始
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
import finlab_sentinel
|
|
88
|
+
|
|
89
|
+
# 啟用 sentinel
|
|
90
|
+
finlab_sentinel.enable()
|
|
91
|
+
|
|
92
|
+
# 正常使用 finlab
|
|
93
|
+
from finlab import data
|
|
94
|
+
close = data.get('price:收盤價') # 自動備份並比對
|
|
95
|
+
|
|
96
|
+
# 如果資料異常,會根據配置拋出例外或警告
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## 配置
|
|
100
|
+
|
|
101
|
+
建立 `sentinel.toml` 檔案:
|
|
102
|
+
|
|
103
|
+
```toml
|
|
104
|
+
[storage]
|
|
105
|
+
path = "~/.finlab-sentinel/"
|
|
106
|
+
retention_days = 7
|
|
107
|
+
|
|
108
|
+
[comparison]
|
|
109
|
+
rtol = 1e-5
|
|
110
|
+
change_threshold = 0.10
|
|
111
|
+
|
|
112
|
+
[comparison.policies]
|
|
113
|
+
default_mode = "append_only"
|
|
114
|
+
history_modifiable = ["fundamental_features:某些財報資料"]
|
|
115
|
+
# 允許 NA→有值 的轉換(例如:預估資料後來補上)
|
|
116
|
+
allow_na_to_value = ["price:收盤價"]
|
|
117
|
+
|
|
118
|
+
[anomaly]
|
|
119
|
+
behavior = "raise" # raise | warn_return_cached | warn_return_new
|
|
120
|
+
save_reports = true
|
|
121
|
+
|
|
122
|
+
# 可選:設定通知 callback
|
|
123
|
+
# callback = "myproject.notifications:send_line"
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## CLI 使用
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
# 列出所有備份
|
|
130
|
+
sentinel list
|
|
131
|
+
|
|
132
|
+
# 清理過期備份
|
|
133
|
+
sentinel cleanup --days 14
|
|
134
|
+
|
|
135
|
+
# 查看資料差異
|
|
136
|
+
sentinel diff "price:收盤價"
|
|
137
|
+
|
|
138
|
+
# 接受新資料作為基準
|
|
139
|
+
sentinel accept "price:收盤價" --reason "確認資料修正"
|
|
140
|
+
|
|
141
|
+
# 匯出備份
|
|
142
|
+
sentinel export "price:收盤價" -o ./backup.parquet
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## 處理資料異常
|
|
146
|
+
|
|
147
|
+
當檢測到資料異常時:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
from finlab_sentinel import DataAnomalyError
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
close = data.get('price:收盤價')
|
|
154
|
+
except DataAnomalyError as e:
|
|
155
|
+
print(f"資料異常: {e.report.summary}")
|
|
156
|
+
# 檢查報告詳情
|
|
157
|
+
print(f"變動比例: {e.report.comparison_result.change_ratio:.1%}")
|
|
158
|
+
|
|
159
|
+
# 如果確認要接受新資料
|
|
160
|
+
from finlab_sentinel.core.interceptor import accept_current_data
|
|
161
|
+
accept_current_data('price:收盤價', reason="確認資料修正")
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Preprocess Hook
|
|
165
|
+
|
|
166
|
+
Preprocess hook 讓你可以在比對前先對資料做預處理,例如四捨五入、排序欄位等。這在處理預期的浮點數精度差異時特別有用。
|
|
167
|
+
|
|
168
|
+
**注意**: 預處理只用於比對,回傳給使用者的永遠是原始資料。
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
import finlab_sentinel
|
|
172
|
+
|
|
173
|
+
# 註冊特定 dataset 的 preprocess hook
|
|
174
|
+
finlab_sentinel.register_preprocess_hook(
|
|
175
|
+
"price:收盤價",
|
|
176
|
+
lambda df: df.round(2) # 四捨五入到小數第二位
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# 支援萬用字元模式
|
|
180
|
+
finlab_sentinel.register_preprocess_hook(
|
|
181
|
+
"price:*", # 符合所有 price: 開頭的 dataset
|
|
182
|
+
lambda df: df.round(2)
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# 也支援 ? 萬用字元(符合單一字元)
|
|
186
|
+
finlab_sentinel.register_preprocess_hook(
|
|
187
|
+
"price:?",
|
|
188
|
+
lambda df: df.round(2)
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
finlab_sentinel.enable()
|
|
192
|
+
|
|
193
|
+
# 使用 finlab
|
|
194
|
+
from finlab import data
|
|
195
|
+
close = data.get('price:收盤價') # 比對時會先 round(2),但回傳原始資料
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### 進階用法
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
import finlab_sentinel
|
|
202
|
+
|
|
203
|
+
# 自訂預處理函式
|
|
204
|
+
def normalize_for_comparison(df):
|
|
205
|
+
"""標準化 DataFrame 以忽略預期的差異"""
|
|
206
|
+
df = df.copy()
|
|
207
|
+
# 四捨五入數值欄位
|
|
208
|
+
numeric_cols = df.select_dtypes(include=['float64', 'float32']).columns
|
|
209
|
+
df[numeric_cols] = df[numeric_cols].round(4)
|
|
210
|
+
# 排序欄位(忽略欄位順序差異)
|
|
211
|
+
df = df[sorted(df.columns)]
|
|
212
|
+
return df
|
|
213
|
+
|
|
214
|
+
finlab_sentinel.register_preprocess_hook("fundamental_features:*", normalize_for_comparison)
|
|
215
|
+
|
|
216
|
+
# 取消註冊
|
|
217
|
+
finlab_sentinel.unregister_preprocess_hook("price:收盤價")
|
|
218
|
+
|
|
219
|
+
# 清除所有 hooks
|
|
220
|
+
finlab_sentinel.clear_preprocess_hooks()
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### 優先順序
|
|
224
|
+
|
|
225
|
+
當多個 pattern 都符合時,精確匹配優先於萬用字元匹配:
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
finlab_sentinel.register_preprocess_hook("price:*", lambda df: df.round(1))
|
|
229
|
+
finlab_sentinel.register_preprocess_hook("price:收盤價", lambda df: df.round(2))
|
|
230
|
+
|
|
231
|
+
# "price:收盤價" 會使用 round(2)(精確匹配)
|
|
232
|
+
# "price:開盤價" 會使用 round(1)(萬用字元匹配)
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## 自訂通知
|
|
236
|
+
|
|
237
|
+
```python
|
|
238
|
+
def send_line_notification(report):
|
|
239
|
+
"""當檢測到異常時發送 LINE 通知"""
|
|
240
|
+
import requests
|
|
241
|
+
requests.post(
|
|
242
|
+
"https://notify-api.line.me/api/notify",
|
|
243
|
+
headers={"Authorization": f"Bearer {LINE_TOKEN}"},
|
|
244
|
+
data={"message": f"finlab 資料異常: {report.summary}"}
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
# 在 sentinel.toml 中設定
|
|
248
|
+
# [anomaly]
|
|
249
|
+
# callback = "myproject.notifications:send_line_notification"
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## 開發
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
# Clone 專案
|
|
256
|
+
git clone https://github.com/yourusername/finlab-sentinel
|
|
257
|
+
cd finlab-sentinel
|
|
258
|
+
|
|
259
|
+
# 使用 uv 安裝開發依賴
|
|
260
|
+
uv sync --dev
|
|
261
|
+
|
|
262
|
+
# 執行測試
|
|
263
|
+
uv run pytest
|
|
264
|
+
|
|
265
|
+
# 執行 lint
|
|
266
|
+
uv run ruff check src/ tests/
|
|
267
|
+
uv run mypy src/
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## License
|
|
271
|
+
|
|
272
|
+
MIT License
|