finlab-sentinel 0.1.0__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.0/.github/workflows/ci.yml +77 -0
- finlab_sentinel-0.1.0/.github/workflows/release.yml +47 -0
- finlab_sentinel-0.1.0/.gitignore +146 -0
- finlab_sentinel-0.1.0/PKG-INFO +198 -0
- finlab_sentinel-0.1.0/README.md +159 -0
- finlab_sentinel-0.1.0/pyproject.toml +118 -0
- finlab_sentinel-0.1.0/sentinel.toml.example +70 -0
- finlab_sentinel-0.1.0/src/finlab_sentinel/__init__.py +15 -0
- finlab_sentinel-0.1.0/src/finlab_sentinel/cli/__init__.py +5 -0
- finlab_sentinel-0.1.0/src/finlab_sentinel/cli/main.py +485 -0
- finlab_sentinel-0.1.0/src/finlab_sentinel/comparison/__init__.py +27 -0
- finlab_sentinel-0.1.0/src/finlab_sentinel/comparison/differ.py +335 -0
- finlab_sentinel-0.1.0/src/finlab_sentinel/comparison/hasher.py +172 -0
- finlab_sentinel-0.1.0/src/finlab_sentinel/comparison/policies.py +207 -0
- finlab_sentinel-0.1.0/src/finlab_sentinel/comparison/report.py +259 -0
- finlab_sentinel-0.1.0/src/finlab_sentinel/config/__init__.py +25 -0
- finlab_sentinel-0.1.0/src/finlab_sentinel/config/loader.py +222 -0
- finlab_sentinel-0.1.0/src/finlab_sentinel/config/schema.py +128 -0
- finlab_sentinel-0.1.0/src/finlab_sentinel/core/__init__.py +9 -0
- finlab_sentinel-0.1.0/src/finlab_sentinel/core/interceptor.py +275 -0
- finlab_sentinel-0.1.0/src/finlab_sentinel/core/patcher.py +151 -0
- finlab_sentinel-0.1.0/src/finlab_sentinel/core/registry.py +69 -0
- finlab_sentinel-0.1.0/src/finlab_sentinel/exceptions.py +43 -0
- finlab_sentinel-0.1.0/src/finlab_sentinel/handlers/__init__.py +17 -0
- finlab_sentinel-0.1.0/src/finlab_sentinel/handlers/base.py +43 -0
- finlab_sentinel-0.1.0/src/finlab_sentinel/handlers/callback.py +110 -0
- finlab_sentinel-0.1.0/src/finlab_sentinel/handlers/exception.py +39 -0
- finlab_sentinel-0.1.0/src/finlab_sentinel/handlers/warning.py +92 -0
- finlab_sentinel-0.1.0/src/finlab_sentinel/py.typed +0 -0
- finlab_sentinel-0.1.0/src/finlab_sentinel/storage/__init__.py +10 -0
- finlab_sentinel-0.1.0/src/finlab_sentinel/storage/backend.py +186 -0
- finlab_sentinel-0.1.0/src/finlab_sentinel/storage/cleanup.py +80 -0
- finlab_sentinel-0.1.0/src/finlab_sentinel/storage/index.py +296 -0
- finlab_sentinel-0.1.0/src/finlab_sentinel/storage/parquet.py +307 -0
- finlab_sentinel-0.1.0/tests/__init__.py +1 -0
- finlab_sentinel-0.1.0/tests/conftest.py +98 -0
- finlab_sentinel-0.1.0/tests/unit/__init__.py +1 -0
- finlab_sentinel-0.1.0/tests/unit/test_cleanup.py +100 -0
- finlab_sentinel-0.1.0/tests/unit/test_cli.py +382 -0
- finlab_sentinel-0.1.0/tests/unit/test_config.py +158 -0
- finlab_sentinel-0.1.0/tests/unit/test_differ.py +154 -0
- finlab_sentinel-0.1.0/tests/unit/test_handlers.py +173 -0
- finlab_sentinel-0.1.0/tests/unit/test_hasher.py +100 -0
- finlab_sentinel-0.1.0/tests/unit/test_interceptor.py +332 -0
- finlab_sentinel-0.1.0/tests/unit/test_loader.py +202 -0
- finlab_sentinel-0.1.0/tests/unit/test_patcher.py +199 -0
- finlab_sentinel-0.1.0/tests/unit/test_policies.py +185 -0
- finlab_sentinel-0.1.0/tests/unit/test_report.py +227 -0
- finlab_sentinel-0.1.0/tests/unit/test_storage.py +164 -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,198 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: finlab-sentinel
|
|
3
|
+
Version: 0.1.0
|
|
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
|
+
- **通知機制**: 支援自訂 callback(如 LINE、email 通知)
|
|
69
|
+
- **CLI 工具**: 管理備份、查看差異、接受新資料
|
|
70
|
+
|
|
71
|
+
## 安裝
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pip install finlab-sentinel
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
或使用 uv:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
uv add finlab-sentinel
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## 快速開始
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
import finlab_sentinel
|
|
87
|
+
|
|
88
|
+
# 啟用 sentinel
|
|
89
|
+
finlab_sentinel.enable()
|
|
90
|
+
|
|
91
|
+
# 正常使用 finlab
|
|
92
|
+
from finlab import data
|
|
93
|
+
close = data.get('price:收盤價') # 自動備份並比對
|
|
94
|
+
|
|
95
|
+
# 如果資料異常,會根據配置拋出例外或警告
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## 配置
|
|
99
|
+
|
|
100
|
+
建立 `sentinel.toml` 檔案:
|
|
101
|
+
|
|
102
|
+
```toml
|
|
103
|
+
[storage]
|
|
104
|
+
path = "~/.finlab-sentinel/"
|
|
105
|
+
retention_days = 7
|
|
106
|
+
|
|
107
|
+
[comparison]
|
|
108
|
+
rtol = 1e-5
|
|
109
|
+
change_threshold = 0.10
|
|
110
|
+
|
|
111
|
+
[comparison.policies]
|
|
112
|
+
default_mode = "append_only"
|
|
113
|
+
history_modifiable = ["fundamental_features:某些財報資料"]
|
|
114
|
+
|
|
115
|
+
[anomaly]
|
|
116
|
+
behavior = "raise" # raise | warn_return_cached | warn_return_new
|
|
117
|
+
save_reports = true
|
|
118
|
+
|
|
119
|
+
# 可選:設定通知 callback
|
|
120
|
+
# callback = "myproject.notifications:send_line"
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## CLI 使用
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
# 列出所有備份
|
|
127
|
+
sentinel list
|
|
128
|
+
|
|
129
|
+
# 清理過期備份
|
|
130
|
+
sentinel cleanup --days 14
|
|
131
|
+
|
|
132
|
+
# 查看資料差異
|
|
133
|
+
sentinel diff "price:收盤價"
|
|
134
|
+
|
|
135
|
+
# 接受新資料作為基準
|
|
136
|
+
sentinel accept "price:收盤價" --reason "確認資料修正"
|
|
137
|
+
|
|
138
|
+
# 匯出備份
|
|
139
|
+
sentinel export "price:收盤價" -o ./backup.parquet
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## 處理資料異常
|
|
143
|
+
|
|
144
|
+
當檢測到資料異常時:
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
from finlab_sentinel import DataAnomalyError
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
close = data.get('price:收盤價')
|
|
151
|
+
except DataAnomalyError as e:
|
|
152
|
+
print(f"資料異常: {e.report.summary}")
|
|
153
|
+
# 檢查報告詳情
|
|
154
|
+
print(f"變動比例: {e.report.comparison_result.change_ratio:.1%}")
|
|
155
|
+
|
|
156
|
+
# 如果確認要接受新資料
|
|
157
|
+
from finlab_sentinel.core.interceptor import accept_current_data
|
|
158
|
+
accept_current_data('price:收盤價', reason="確認資料修正")
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## 自訂通知
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
def send_line_notification(report):
|
|
165
|
+
"""當檢測到異常時發送 LINE 通知"""
|
|
166
|
+
import requests
|
|
167
|
+
requests.post(
|
|
168
|
+
"https://notify-api.line.me/api/notify",
|
|
169
|
+
headers={"Authorization": f"Bearer {LINE_TOKEN}"},
|
|
170
|
+
data={"message": f"finlab 資料異常: {report.summary}"}
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# 在 sentinel.toml 中設定
|
|
174
|
+
# [anomaly]
|
|
175
|
+
# callback = "myproject.notifications:send_line_notification"
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## 開發
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
# Clone 專案
|
|
182
|
+
git clone https://github.com/yourusername/finlab-sentinel
|
|
183
|
+
cd finlab-sentinel
|
|
184
|
+
|
|
185
|
+
# 使用 uv 安裝開發依賴
|
|
186
|
+
uv sync --dev
|
|
187
|
+
|
|
188
|
+
# 執行測試
|
|
189
|
+
uv run pytest
|
|
190
|
+
|
|
191
|
+
# 執行 lint
|
|
192
|
+
uv run ruff check src/ tests/
|
|
193
|
+
uv run mypy src/
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## License
|
|
197
|
+
|
|
198
|
+
MIT License
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# finlab-sentinel
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+

|
|
7
|
+
[](LICENSE)
|
|
8
|
+
[](https://github.com/iapcal/finlab-sentinel/actions/workflows/ci.yml)
|
|
9
|
+
[](https://codecov.io/gh/iapcal/finlab-sentinel)
|
|
10
|
+
|
|
11
|
+
**finlab-sentinel** 是 [finlab](https://github.com/finlab-python/finlab) 套件的防禦層,用於監控 `data.get` API 的資料變化,防止未預期的資料異動影響回測或選股結果。
|
|
12
|
+
|
|
13
|
+
## 功能特色
|
|
14
|
+
|
|
15
|
+
- **自動比對**: 每次 `data.get` 時自動比對歷史資料
|
|
16
|
+
- **滾動備份**: 保留 7 天(可配置)的備份資料
|
|
17
|
+
- **智慧檢測**:
|
|
18
|
+
- 數值容差比對(可配置 rtol/atol)
|
|
19
|
+
- dtype 變更檢測
|
|
20
|
+
- NA 類型差異檢測(pd.NA vs np.nan vs None)
|
|
21
|
+
- **彈性政策**:
|
|
22
|
+
- `append_only`: 只允許新增,不允許刪除或修改歷史
|
|
23
|
+
- `threshold`: 允許小幅度變更(如 10% 以內)
|
|
24
|
+
- 黑名單配置:指定可修改歷史的資料集
|
|
25
|
+
- **可配置行為**:
|
|
26
|
+
- 拋出例外(預設)
|
|
27
|
+
- 警告並使用快取
|
|
28
|
+
- 警告並使用新資料
|
|
29
|
+
- **通知機制**: 支援自訂 callback(如 LINE、email 通知)
|
|
30
|
+
- **CLI 工具**: 管理備份、查看差異、接受新資料
|
|
31
|
+
|
|
32
|
+
## 安裝
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install finlab-sentinel
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
或使用 uv:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
uv add finlab-sentinel
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 快速開始
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
import finlab_sentinel
|
|
48
|
+
|
|
49
|
+
# 啟用 sentinel
|
|
50
|
+
finlab_sentinel.enable()
|
|
51
|
+
|
|
52
|
+
# 正常使用 finlab
|
|
53
|
+
from finlab import data
|
|
54
|
+
close = data.get('price:收盤價') # 自動備份並比對
|
|
55
|
+
|
|
56
|
+
# 如果資料異常,會根據配置拋出例外或警告
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## 配置
|
|
60
|
+
|
|
61
|
+
建立 `sentinel.toml` 檔案:
|
|
62
|
+
|
|
63
|
+
```toml
|
|
64
|
+
[storage]
|
|
65
|
+
path = "~/.finlab-sentinel/"
|
|
66
|
+
retention_days = 7
|
|
67
|
+
|
|
68
|
+
[comparison]
|
|
69
|
+
rtol = 1e-5
|
|
70
|
+
change_threshold = 0.10
|
|
71
|
+
|
|
72
|
+
[comparison.policies]
|
|
73
|
+
default_mode = "append_only"
|
|
74
|
+
history_modifiable = ["fundamental_features:某些財報資料"]
|
|
75
|
+
|
|
76
|
+
[anomaly]
|
|
77
|
+
behavior = "raise" # raise | warn_return_cached | warn_return_new
|
|
78
|
+
save_reports = true
|
|
79
|
+
|
|
80
|
+
# 可選:設定通知 callback
|
|
81
|
+
# callback = "myproject.notifications:send_line"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## CLI 使用
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# 列出所有備份
|
|
88
|
+
sentinel list
|
|
89
|
+
|
|
90
|
+
# 清理過期備份
|
|
91
|
+
sentinel cleanup --days 14
|
|
92
|
+
|
|
93
|
+
# 查看資料差異
|
|
94
|
+
sentinel diff "price:收盤價"
|
|
95
|
+
|
|
96
|
+
# 接受新資料作為基準
|
|
97
|
+
sentinel accept "price:收盤價" --reason "確認資料修正"
|
|
98
|
+
|
|
99
|
+
# 匯出備份
|
|
100
|
+
sentinel export "price:收盤價" -o ./backup.parquet
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## 處理資料異常
|
|
104
|
+
|
|
105
|
+
當檢測到資料異常時:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from finlab_sentinel import DataAnomalyError
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
close = data.get('price:收盤價')
|
|
112
|
+
except DataAnomalyError as e:
|
|
113
|
+
print(f"資料異常: {e.report.summary}")
|
|
114
|
+
# 檢查報告詳情
|
|
115
|
+
print(f"變動比例: {e.report.comparison_result.change_ratio:.1%}")
|
|
116
|
+
|
|
117
|
+
# 如果確認要接受新資料
|
|
118
|
+
from finlab_sentinel.core.interceptor import accept_current_data
|
|
119
|
+
accept_current_data('price:收盤價', reason="確認資料修正")
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## 自訂通知
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
def send_line_notification(report):
|
|
126
|
+
"""當檢測到異常時發送 LINE 通知"""
|
|
127
|
+
import requests
|
|
128
|
+
requests.post(
|
|
129
|
+
"https://notify-api.line.me/api/notify",
|
|
130
|
+
headers={"Authorization": f"Bearer {LINE_TOKEN}"},
|
|
131
|
+
data={"message": f"finlab 資料異常: {report.summary}"}
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# 在 sentinel.toml 中設定
|
|
135
|
+
# [anomaly]
|
|
136
|
+
# callback = "myproject.notifications:send_line_notification"
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## 開發
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
# Clone 專案
|
|
143
|
+
git clone https://github.com/yourusername/finlab-sentinel
|
|
144
|
+
cd finlab-sentinel
|
|
145
|
+
|
|
146
|
+
# 使用 uv 安裝開發依賴
|
|
147
|
+
uv sync --dev
|
|
148
|
+
|
|
149
|
+
# 執行測試
|
|
150
|
+
uv run pytest
|
|
151
|
+
|
|
152
|
+
# 執行 lint
|
|
153
|
+
uv run ruff check src/ tests/
|
|
154
|
+
uv run mypy src/
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## License
|
|
158
|
+
|
|
159
|
+
MIT License
|