finval 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.
- finval-0.1.0/.gitignore +56 -0
- finval-0.1.0/LICENSE +21 -0
- finval-0.1.0/PKG-INFO +174 -0
- finval-0.1.0/README.md +137 -0
- finval-0.1.0/examples/quickstart.py +88 -0
- finval-0.1.0/pyproject.toml +71 -0
- finval-0.1.0/src/finval/__init__.py +29 -0
- finval-0.1.0/src/finval/baselines/__init__.py +32 -0
- finval-0.1.0/src/finval/baselines/gaussian.py +64 -0
- finval-0.1.0/src/finval/baselines/historical.py +80 -0
- finval-0.1.0/src/finval/core/__init__.py +11 -0
- finval-0.1.0/src/finval/core/bootstrap.py +116 -0
- finval-0.1.0/src/finval/core/result.py +201 -0
- finval-0.1.0/src/finval/core/thresholds.py +227 -0
- finval-0.1.0/src/finval/metrics/__init__.py +62 -0
- finval-0.1.0/src/finval/metrics/calibration.py +314 -0
- finval-0.1.0/src/finval/metrics/dependence.py +416 -0
- finval-0.1.0/src/finval/metrics/distribution.py +328 -0
- finval-0.1.0/src/finval/metrics/paths.py +106 -0
- finval-0.1.0/src/finval/metrics/temporal.py +354 -0
- finval-0.1.0/src/finval/validate.py +339 -0
- finval-0.1.0/tests/__init__.py +0 -0
- finval-0.1.0/tests/conftest.py +40 -0
- finval-0.1.0/tests/test_baselines.py +82 -0
- finval-0.1.0/tests/test_calibration.py +96 -0
- finval-0.1.0/tests/test_dependence.py +93 -0
- finval-0.1.0/tests/test_distribution.py +110 -0
- finval-0.1.0/tests/test_paths.py +32 -0
- finval-0.1.0/tests/test_temporal.py +78 -0
- finval-0.1.0/tests/test_validate.py +87 -0
finval-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
lib/
|
|
14
|
+
lib64/
|
|
15
|
+
parts/
|
|
16
|
+
sdist/
|
|
17
|
+
var/
|
|
18
|
+
wheels/
|
|
19
|
+
*.egg-info/
|
|
20
|
+
.installed.cfg
|
|
21
|
+
*.egg
|
|
22
|
+
MANIFEST
|
|
23
|
+
|
|
24
|
+
# Virtual environments
|
|
25
|
+
.venv/
|
|
26
|
+
venv/
|
|
27
|
+
env/
|
|
28
|
+
ENV/
|
|
29
|
+
|
|
30
|
+
# Testing
|
|
31
|
+
.pytest_cache/
|
|
32
|
+
.coverage
|
|
33
|
+
htmlcov/
|
|
34
|
+
.tox/
|
|
35
|
+
.hypothesis/
|
|
36
|
+
|
|
37
|
+
# Type checking
|
|
38
|
+
.mypy_cache/
|
|
39
|
+
.dmypy.json
|
|
40
|
+
dmypy.json
|
|
41
|
+
.ruff_cache/
|
|
42
|
+
|
|
43
|
+
# IDE
|
|
44
|
+
.vscode/
|
|
45
|
+
.idea/
|
|
46
|
+
*.swp
|
|
47
|
+
*.swo
|
|
48
|
+
*~
|
|
49
|
+
.DS_Store
|
|
50
|
+
|
|
51
|
+
# Jupyter
|
|
52
|
+
.ipynb_checkpoints/
|
|
53
|
+
|
|
54
|
+
# Build artifacts
|
|
55
|
+
*.log
|
|
56
|
+
.cache/
|
finval-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sablier
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
finval-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: finval
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Rigorous validation for synthetic financial time series
|
|
5
|
+
Project-URL: Homepage, https://github.com/sablier-it/finval
|
|
6
|
+
Project-URL: Repository, https://github.com/sablier-it/finval
|
|
7
|
+
Project-URL: Issues, https://github.com/sablier-it/finval/issues
|
|
8
|
+
Author-email: Sablier <hello@sablier.it>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: finance,generative models,stylized facts,synthetic data,time series,validation
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: numpy>=1.24
|
|
24
|
+
Requires-Dist: scipy>=1.11
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: matplotlib>=3.7; extra == 'dev'
|
|
27
|
+
Requires-Dist: mypy>=1.8; extra == 'dev'
|
|
28
|
+
Requires-Dist: pandas>=2.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest-cov>=4.1; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest>=7.4; extra == 'dev'
|
|
31
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
32
|
+
Provides-Extra: pandas
|
|
33
|
+
Requires-Dist: pandas>=2.0; extra == 'pandas'
|
|
34
|
+
Provides-Extra: plot
|
|
35
|
+
Requires-Dist: matplotlib>=3.7; extra == 'plot'
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
# finval
|
|
39
|
+
|
|
40
|
+
**Rigorous validation for synthetic financial time series.**
|
|
41
|
+
|
|
42
|
+
`finval` is a Python library for assessing the quality of synthetic
|
|
43
|
+
market data against real data. It was built because no existing library
|
|
44
|
+
covers the financial stylized facts that matter: fat tails, volatility
|
|
45
|
+
clustering, leverage effect, crash co-movement, and probabilistic
|
|
46
|
+
forecast calibration.
|
|
47
|
+
|
|
48
|
+
`finval` is the scoring backend behind
|
|
49
|
+
**[FinBench](https://github.com/sablier-it/finbench)**, the public
|
|
50
|
+
leaderboard for multivariate financial time-series generation.
|
|
51
|
+
|
|
52
|
+
> ⚠️ **Beta.** The library is in active use (57 tests, FinBench
|
|
53
|
+
> production scoring) but the public API may still evolve in minor
|
|
54
|
+
> versions. Pin to `finval==0.1.x` if you need API stability.
|
|
55
|
+
|
|
56
|
+
## Why finval?
|
|
57
|
+
|
|
58
|
+
General-purpose synthetic data libraries (`sdmetrics`, `synthcity`,
|
|
59
|
+
`tsgm`) treat time series as generic sequences. They don't know what
|
|
60
|
+
"leverage effect" is, don't check PIT uniformity, and don't compute tail
|
|
61
|
+
dependence coefficients. For financial applications — risk management,
|
|
62
|
+
backtesting, derivatives — you need a suite that tests the things that
|
|
63
|
+
actually matter for market data.
|
|
64
|
+
|
|
65
|
+
`finval` ships **17 metric functions** producing **20 numeric scores**
|
|
66
|
+
across 5 categories (the calibration `coverage_50 / 90 / 95` come from
|
|
67
|
+
a single function), each with thresholds calibrated against real
|
|
68
|
+
financial data and justified by the statistical literature.
|
|
69
|
+
|
|
70
|
+
## Installation
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
pip install finval
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Quickstart
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
import numpy as np
|
|
80
|
+
import finval
|
|
81
|
+
|
|
82
|
+
# 2D data: (n_samples, n_features) returns
|
|
83
|
+
real = np.random.randn(1000, 3) * 0.01
|
|
84
|
+
synthetic = np.random.randn(1000, 3) * 0.01
|
|
85
|
+
|
|
86
|
+
report = finval.validate(synthetic, real)
|
|
87
|
+
print(report.summary())
|
|
88
|
+
print(f"Overall quality: {report.overall_quality}")
|
|
89
|
+
print(f"Pass rate: {report.pass_rate:.0%}")
|
|
90
|
+
|
|
91
|
+
# 3D data: (n_paths, horizon, n_features) for path-level validation
|
|
92
|
+
real_paths = np.random.randn(100, 60, 3) * 0.01
|
|
93
|
+
syn_paths = np.random.randn(100, 60, 3) * 0.01
|
|
94
|
+
|
|
95
|
+
report = finval.validate_paths(syn_paths, real_paths)
|
|
96
|
+
print(report.summary())
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Metrics
|
|
100
|
+
|
|
101
|
+
### Distribution (15% of overall score)
|
|
102
|
+
- **marginal_ks** — Kolmogorov-Smirnov test on each feature's marginal
|
|
103
|
+
- **energy_distance** — multivariate distribution difference
|
|
104
|
+
- **tail_quantiles** — 1st/5th/95th/99th percentile comparison (robust alternative to kurtosis)
|
|
105
|
+
- **tail_heaviness** — excess kurtosis error (diagnostic only)
|
|
106
|
+
|
|
107
|
+
### Dependence (25%)
|
|
108
|
+
- **pearson_corr** — linear correlation matrix error
|
|
109
|
+
- **spearman_corr** — rank correlation matrix error
|
|
110
|
+
- **copula_distance** — Cramér-von Mises distance between empirical copulas
|
|
111
|
+
- **tail_dependence_upper** — rally co-movement (λ_U)
|
|
112
|
+
- **tail_dependence_lower** — crash co-movement (λ_L)
|
|
113
|
+
- **correlation_breakdown** — stress vs calm regime correlation shift
|
|
114
|
+
|
|
115
|
+
### Temporal (20%)
|
|
116
|
+
- **acf_returns** — autocorrelation of returns (should be ~0)
|
|
117
|
+
- **volatility_clustering** — autocorrelation of squared returns
|
|
118
|
+
- **leverage_effect** — corr(r_t, |r_{t+k}|) (negative for equities)
|
|
119
|
+
- **cross_correlation** — contemporaneous cross-asset correlation
|
|
120
|
+
|
|
121
|
+
### Calibration (30%)
|
|
122
|
+
- **pit_uniformity** — KS test on probability integral transforms
|
|
123
|
+
- **crps** — continuous ranked probability score
|
|
124
|
+
- **coverage_50 / 90 / 95** — empirical vs nominal interval coverage
|
|
125
|
+
|
|
126
|
+
### Path-level (10%)
|
|
127
|
+
- **drawdown_distribution** — KS test on max drawdown distribution
|
|
128
|
+
|
|
129
|
+
## Baselines
|
|
130
|
+
|
|
131
|
+
Compare your model against simple reference generators to calibrate what
|
|
132
|
+
"good" means for your data:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from finval.baselines import gaussian_baseline, historical_bootstrap, block_bootstrap
|
|
136
|
+
|
|
137
|
+
# Gaussian: matches mean+cov, no temporal structure
|
|
138
|
+
gauss = gaussian_baseline(real, n_samples=1000)
|
|
139
|
+
|
|
140
|
+
# i.i.d. bootstrap: matches joint distribution exactly, zero temporal
|
|
141
|
+
boot = historical_bootstrap(real, n_samples=1000)
|
|
142
|
+
|
|
143
|
+
# Block bootstrap: preserves short-range temporal structure
|
|
144
|
+
blocks = block_bootstrap(real, n_paths=100, path_length=60, block_size=20)
|
|
145
|
+
|
|
146
|
+
# Validate each
|
|
147
|
+
for name, syn in [("gaussian", gauss), ("iid", boot)]:
|
|
148
|
+
r = finval.validate(syn, real)
|
|
149
|
+
print(f"{name}: {r.overall_quality} ({r.overall_score:.0%})")
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Design principles
|
|
153
|
+
|
|
154
|
+
1. **Reliable over comprehensive.** Each metric is chosen because it's
|
|
155
|
+
robust and informative, not because it's impressive.
|
|
156
|
+
|
|
157
|
+
2. **Mean over max for pairwise metrics.** Max over n(n-1)/2 feature
|
|
158
|
+
pairs is dominated by sampling noise. `finval` uses mean error, which
|
|
159
|
+
is harder to fool and more stable run-to-run.
|
|
160
|
+
|
|
161
|
+
3. **Lower is always better.** Every metric is normalized so that zero
|
|
162
|
+
is perfect and higher is worse. No flipped signs to remember.
|
|
163
|
+
|
|
164
|
+
4. **Financial stylized facts first.** Leverage effect, vol clustering,
|
|
165
|
+
fat tails, crash co-movement — these aren't optional for financial
|
|
166
|
+
data.
|
|
167
|
+
|
|
168
|
+
5. **Proper scoring rules.** CRPS and PIT uniformity are proper scoring
|
|
169
|
+
rules, not just rank-order checks. Your model is evaluated against
|
|
170
|
+
the ground truth the statistics literature actually endorses.
|
|
171
|
+
|
|
172
|
+
## License
|
|
173
|
+
|
|
174
|
+
MIT
|
finval-0.1.0/README.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# finval
|
|
2
|
+
|
|
3
|
+
**Rigorous validation for synthetic financial time series.**
|
|
4
|
+
|
|
5
|
+
`finval` is a Python library for assessing the quality of synthetic
|
|
6
|
+
market data against real data. It was built because no existing library
|
|
7
|
+
covers the financial stylized facts that matter: fat tails, volatility
|
|
8
|
+
clustering, leverage effect, crash co-movement, and probabilistic
|
|
9
|
+
forecast calibration.
|
|
10
|
+
|
|
11
|
+
`finval` is the scoring backend behind
|
|
12
|
+
**[FinBench](https://github.com/sablier-it/finbench)**, the public
|
|
13
|
+
leaderboard for multivariate financial time-series generation.
|
|
14
|
+
|
|
15
|
+
> ⚠️ **Beta.** The library is in active use (57 tests, FinBench
|
|
16
|
+
> production scoring) but the public API may still evolve in minor
|
|
17
|
+
> versions. Pin to `finval==0.1.x` if you need API stability.
|
|
18
|
+
|
|
19
|
+
## Why finval?
|
|
20
|
+
|
|
21
|
+
General-purpose synthetic data libraries (`sdmetrics`, `synthcity`,
|
|
22
|
+
`tsgm`) treat time series as generic sequences. They don't know what
|
|
23
|
+
"leverage effect" is, don't check PIT uniformity, and don't compute tail
|
|
24
|
+
dependence coefficients. For financial applications — risk management,
|
|
25
|
+
backtesting, derivatives — you need a suite that tests the things that
|
|
26
|
+
actually matter for market data.
|
|
27
|
+
|
|
28
|
+
`finval` ships **17 metric functions** producing **20 numeric scores**
|
|
29
|
+
across 5 categories (the calibration `coverage_50 / 90 / 95` come from
|
|
30
|
+
a single function), each with thresholds calibrated against real
|
|
31
|
+
financial data and justified by the statistical literature.
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install finval
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quickstart
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
import numpy as np
|
|
43
|
+
import finval
|
|
44
|
+
|
|
45
|
+
# 2D data: (n_samples, n_features) returns
|
|
46
|
+
real = np.random.randn(1000, 3) * 0.01
|
|
47
|
+
synthetic = np.random.randn(1000, 3) * 0.01
|
|
48
|
+
|
|
49
|
+
report = finval.validate(synthetic, real)
|
|
50
|
+
print(report.summary())
|
|
51
|
+
print(f"Overall quality: {report.overall_quality}")
|
|
52
|
+
print(f"Pass rate: {report.pass_rate:.0%}")
|
|
53
|
+
|
|
54
|
+
# 3D data: (n_paths, horizon, n_features) for path-level validation
|
|
55
|
+
real_paths = np.random.randn(100, 60, 3) * 0.01
|
|
56
|
+
syn_paths = np.random.randn(100, 60, 3) * 0.01
|
|
57
|
+
|
|
58
|
+
report = finval.validate_paths(syn_paths, real_paths)
|
|
59
|
+
print(report.summary())
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Metrics
|
|
63
|
+
|
|
64
|
+
### Distribution (15% of overall score)
|
|
65
|
+
- **marginal_ks** — Kolmogorov-Smirnov test on each feature's marginal
|
|
66
|
+
- **energy_distance** — multivariate distribution difference
|
|
67
|
+
- **tail_quantiles** — 1st/5th/95th/99th percentile comparison (robust alternative to kurtosis)
|
|
68
|
+
- **tail_heaviness** — excess kurtosis error (diagnostic only)
|
|
69
|
+
|
|
70
|
+
### Dependence (25%)
|
|
71
|
+
- **pearson_corr** — linear correlation matrix error
|
|
72
|
+
- **spearman_corr** — rank correlation matrix error
|
|
73
|
+
- **copula_distance** — Cramér-von Mises distance between empirical copulas
|
|
74
|
+
- **tail_dependence_upper** — rally co-movement (λ_U)
|
|
75
|
+
- **tail_dependence_lower** — crash co-movement (λ_L)
|
|
76
|
+
- **correlation_breakdown** — stress vs calm regime correlation shift
|
|
77
|
+
|
|
78
|
+
### Temporal (20%)
|
|
79
|
+
- **acf_returns** — autocorrelation of returns (should be ~0)
|
|
80
|
+
- **volatility_clustering** — autocorrelation of squared returns
|
|
81
|
+
- **leverage_effect** — corr(r_t, |r_{t+k}|) (negative for equities)
|
|
82
|
+
- **cross_correlation** — contemporaneous cross-asset correlation
|
|
83
|
+
|
|
84
|
+
### Calibration (30%)
|
|
85
|
+
- **pit_uniformity** — KS test on probability integral transforms
|
|
86
|
+
- **crps** — continuous ranked probability score
|
|
87
|
+
- **coverage_50 / 90 / 95** — empirical vs nominal interval coverage
|
|
88
|
+
|
|
89
|
+
### Path-level (10%)
|
|
90
|
+
- **drawdown_distribution** — KS test on max drawdown distribution
|
|
91
|
+
|
|
92
|
+
## Baselines
|
|
93
|
+
|
|
94
|
+
Compare your model against simple reference generators to calibrate what
|
|
95
|
+
"good" means for your data:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from finval.baselines import gaussian_baseline, historical_bootstrap, block_bootstrap
|
|
99
|
+
|
|
100
|
+
# Gaussian: matches mean+cov, no temporal structure
|
|
101
|
+
gauss = gaussian_baseline(real, n_samples=1000)
|
|
102
|
+
|
|
103
|
+
# i.i.d. bootstrap: matches joint distribution exactly, zero temporal
|
|
104
|
+
boot = historical_bootstrap(real, n_samples=1000)
|
|
105
|
+
|
|
106
|
+
# Block bootstrap: preserves short-range temporal structure
|
|
107
|
+
blocks = block_bootstrap(real, n_paths=100, path_length=60, block_size=20)
|
|
108
|
+
|
|
109
|
+
# Validate each
|
|
110
|
+
for name, syn in [("gaussian", gauss), ("iid", boot)]:
|
|
111
|
+
r = finval.validate(syn, real)
|
|
112
|
+
print(f"{name}: {r.overall_quality} ({r.overall_score:.0%})")
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Design principles
|
|
116
|
+
|
|
117
|
+
1. **Reliable over comprehensive.** Each metric is chosen because it's
|
|
118
|
+
robust and informative, not because it's impressive.
|
|
119
|
+
|
|
120
|
+
2. **Mean over max for pairwise metrics.** Max over n(n-1)/2 feature
|
|
121
|
+
pairs is dominated by sampling noise. `finval` uses mean error, which
|
|
122
|
+
is harder to fool and more stable run-to-run.
|
|
123
|
+
|
|
124
|
+
3. **Lower is always better.** Every metric is normalized so that zero
|
|
125
|
+
is perfect and higher is worse. No flipped signs to remember.
|
|
126
|
+
|
|
127
|
+
4. **Financial stylized facts first.** Leverage effect, vol clustering,
|
|
128
|
+
fat tails, crash co-movement — these aren't optional for financial
|
|
129
|
+
data.
|
|
130
|
+
|
|
131
|
+
5. **Proper scoring rules.** CRPS and PIT uniformity are proper scoring
|
|
132
|
+
rules, not just rank-order checks. Your model is evaluated against
|
|
133
|
+
the ground truth the statistics literature actually endorses.
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
MIT
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""finval quickstart example.
|
|
2
|
+
|
|
3
|
+
Demonstrates the three main use cases:
|
|
4
|
+
|
|
5
|
+
1. Flat 2D validation (distribution + dependence)
|
|
6
|
+
2. Path-level 3D validation (adds temporal + path metrics)
|
|
7
|
+
3. Baseline comparison (gaussian vs bootstrap vs your model)
|
|
8
|
+
|
|
9
|
+
Run from the finval repo root:
|
|
10
|
+
|
|
11
|
+
python examples/quickstart.py
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
import finval
|
|
17
|
+
from finval.baselines import (
|
|
18
|
+
block_bootstrap,
|
|
19
|
+
gaussian_baseline,
|
|
20
|
+
historical_bootstrap,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def build_real_returns(n: int = 2000, seed: int = 0) -> np.ndarray:
|
|
25
|
+
"""Synthesize realistic-ish multi-asset returns with fat tails,
|
|
26
|
+
cross-asset correlation, and a crash regime."""
|
|
27
|
+
rng = np.random.default_rng(seed)
|
|
28
|
+
x = rng.standard_t(df=5, size=(n, 3)) * 0.01
|
|
29
|
+
# Induce correlation: feature 1 is mostly driven by feature 0
|
|
30
|
+
x[:, 1] = 0.7 * x[:, 0] + 0.3 * x[:, 1]
|
|
31
|
+
# Inject a few crash days where all assets move together
|
|
32
|
+
crash_days = rng.choice(n, size=n // 100, replace=False)
|
|
33
|
+
x[crash_days] -= 0.04
|
|
34
|
+
return x
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def main() -> None:
|
|
38
|
+
print("=" * 72)
|
|
39
|
+
print("finval quickstart")
|
|
40
|
+
print("=" * 72)
|
|
41
|
+
|
|
42
|
+
# ----------------------------------------------------------------------
|
|
43
|
+
# 1. Flat validation
|
|
44
|
+
# ----------------------------------------------------------------------
|
|
45
|
+
real = build_real_returns(n=2000, seed=0)
|
|
46
|
+
synthetic_good = build_real_returns(n=2000, seed=1) # same distribution, new seed
|
|
47
|
+
|
|
48
|
+
print("\n1) Flat validation (matched synthetic):")
|
|
49
|
+
report = finval.validate(synthetic_good, real, feature_names=["asset_a", "asset_b", "asset_c"])
|
|
50
|
+
print(report.summary())
|
|
51
|
+
|
|
52
|
+
# ----------------------------------------------------------------------
|
|
53
|
+
# 2. Baseline comparison
|
|
54
|
+
# ----------------------------------------------------------------------
|
|
55
|
+
print("\n2) Baseline comparison:")
|
|
56
|
+
for name, syn in [
|
|
57
|
+
("gaussian_iid", gaussian_baseline(real, n_samples=2000, seed=2)),
|
|
58
|
+
("iid_bootstrap", historical_bootstrap(real, n_samples=2000, seed=3)),
|
|
59
|
+
("your_model", synthetic_good),
|
|
60
|
+
]:
|
|
61
|
+
r = finval.validate(syn, real)
|
|
62
|
+
print(f" {name:20s} {r.overall_quality:10s} {r.overall_score:5.0%}")
|
|
63
|
+
|
|
64
|
+
# ----------------------------------------------------------------------
|
|
65
|
+
# 3. Path-level validation
|
|
66
|
+
# ----------------------------------------------------------------------
|
|
67
|
+
# Build return paths from the flat series by slicing non-overlapping windows
|
|
68
|
+
real_paths = real[: 60 * 30].reshape(30, 60, 3)
|
|
69
|
+
syn_paths = synthetic_good[: 60 * 30].reshape(30, 60, 3)
|
|
70
|
+
|
|
71
|
+
print("\n3) Path-level validation:")
|
|
72
|
+
report = finval.validate_paths(syn_paths, real_paths)
|
|
73
|
+
print(report.summary())
|
|
74
|
+
|
|
75
|
+
# ----------------------------------------------------------------------
|
|
76
|
+
# 4. Block bootstrap as a strong baseline for path metrics
|
|
77
|
+
# ----------------------------------------------------------------------
|
|
78
|
+
print("\n4) Block bootstrap beats gaussian on path metrics:")
|
|
79
|
+
block_paths = block_bootstrap(real, n_paths=30, path_length=60, block_size=20, seed=4)
|
|
80
|
+
gauss_paths = gaussian_baseline(real, n_paths=30, path_length=60, seed=5)
|
|
81
|
+
|
|
82
|
+
for name, paths in [("gaussian_paths", gauss_paths), ("block_bootstrap", block_paths)]:
|
|
83
|
+
r = finval.validate_paths(paths, real_paths)
|
|
84
|
+
print(f" {name:20s} {r.overall_quality:10s} {r.overall_score:5.0%}")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
if __name__ == "__main__":
|
|
88
|
+
main()
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "finval"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Rigorous validation for synthetic financial time series"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { text = "MIT" }
|
|
7
|
+
requires-python = ">=3.10"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Sablier", email = "hello@sablier.it" },
|
|
10
|
+
]
|
|
11
|
+
keywords = [
|
|
12
|
+
"finance",
|
|
13
|
+
"synthetic data",
|
|
14
|
+
"time series",
|
|
15
|
+
"validation",
|
|
16
|
+
"generative models",
|
|
17
|
+
"stylized facts",
|
|
18
|
+
]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 3 - Alpha",
|
|
21
|
+
"Intended Audience :: Financial and Insurance Industry",
|
|
22
|
+
"Intended Audience :: Science/Research",
|
|
23
|
+
"License :: OSI Approved :: MIT License",
|
|
24
|
+
"Programming Language :: Python :: 3",
|
|
25
|
+
"Programming Language :: Python :: 3.10",
|
|
26
|
+
"Programming Language :: Python :: 3.11",
|
|
27
|
+
"Programming Language :: Python :: 3.12",
|
|
28
|
+
"Topic :: Scientific/Engineering :: Mathematics",
|
|
29
|
+
"Topic :: Office/Business :: Financial :: Investment",
|
|
30
|
+
]
|
|
31
|
+
dependencies = [
|
|
32
|
+
"numpy>=1.24",
|
|
33
|
+
"scipy>=1.11",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[project.optional-dependencies]
|
|
37
|
+
pandas = ["pandas>=2.0"]
|
|
38
|
+
plot = ["matplotlib>=3.7"]
|
|
39
|
+
dev = [
|
|
40
|
+
"pytest>=7.4",
|
|
41
|
+
"pytest-cov>=4.1",
|
|
42
|
+
"ruff>=0.4",
|
|
43
|
+
"mypy>=1.8",
|
|
44
|
+
"pandas>=2.0",
|
|
45
|
+
"matplotlib>=3.7",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
[project.urls]
|
|
49
|
+
Homepage = "https://github.com/sablier-it/finval"
|
|
50
|
+
Repository = "https://github.com/sablier-it/finval"
|
|
51
|
+
Issues = "https://github.com/sablier-it/finval/issues"
|
|
52
|
+
|
|
53
|
+
[build-system]
|
|
54
|
+
requires = ["hatchling"]
|
|
55
|
+
build-backend = "hatchling.build"
|
|
56
|
+
|
|
57
|
+
[tool.hatch.build.targets.wheel]
|
|
58
|
+
packages = ["src/finval"]
|
|
59
|
+
|
|
60
|
+
[tool.ruff]
|
|
61
|
+
line-length = 100
|
|
62
|
+
target-version = "py310"
|
|
63
|
+
|
|
64
|
+
[tool.ruff.lint]
|
|
65
|
+
select = ["E", "F", "W", "I", "N", "UP", "B", "SIM", "RUF"]
|
|
66
|
+
ignore = ["E501"] # line length handled by formatter
|
|
67
|
+
|
|
68
|
+
[tool.pytest.ini_options]
|
|
69
|
+
testpaths = ["tests"]
|
|
70
|
+
python_files = ["test_*.py"]
|
|
71
|
+
addopts = "-v --tb=short"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""finval — rigorous validation for synthetic financial time series.
|
|
2
|
+
|
|
3
|
+
Quick start:
|
|
4
|
+
|
|
5
|
+
import finval
|
|
6
|
+
|
|
7
|
+
# synthetic and real are (n_samples, n_features) arrays of returns
|
|
8
|
+
report = finval.validate(synthetic, real)
|
|
9
|
+
print(report.summary())
|
|
10
|
+
report.to_dict()
|
|
11
|
+
|
|
12
|
+
For path-level validation with drawdowns and calibration:
|
|
13
|
+
|
|
14
|
+
# paths are (n_paths, horizon, n_features) arrays
|
|
15
|
+
report = finval.validate_paths(synthetic_paths, real_returns)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from finval.core.result import MetricResult, ValidationReport
|
|
19
|
+
from finval.validate import validate, validate_paths
|
|
20
|
+
|
|
21
|
+
__version__ = "0.1.0"
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"MetricResult",
|
|
25
|
+
"ValidationReport",
|
|
26
|
+
"validate",
|
|
27
|
+
"validate_paths",
|
|
28
|
+
"__version__",
|
|
29
|
+
]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Baseline generators for benchmarking synthetic financial data.
|
|
2
|
+
|
|
3
|
+
A baseline is a simple generator that produces synthetic data from real
|
|
4
|
+
data. Baselines are not meant to be good models — they're meant to be
|
|
5
|
+
reference points so users can answer "is my fancy model actually better
|
|
6
|
+
than X?" where X is a trivially simple generator.
|
|
7
|
+
|
|
8
|
+
Three baselines ship with finval:
|
|
9
|
+
|
|
10
|
+
- `gaussian`: Multivariate Gaussian with the empirical mean and covariance
|
|
11
|
+
of the real data. No temporal structure, no tails, no vol clustering.
|
|
12
|
+
This is the minimum bar any generative model should clear.
|
|
13
|
+
|
|
14
|
+
- `historical_bootstrap`: Random sampling (with replacement) from real
|
|
15
|
+
returns. Reproduces marginals and joint distribution exactly in
|
|
16
|
+
expectation, but destroys all temporal structure (no ACF, no leverage).
|
|
17
|
+
|
|
18
|
+
- `block_bootstrap`: Moving-block bootstrap preserves local dependence.
|
|
19
|
+
This is the strongest simple baseline — it reproduces short-range
|
|
20
|
+
temporal structure and most stylized facts at the cost of exact
|
|
21
|
+
marginal duplication. A generative model that beats block bootstrap
|
|
22
|
+
on all metrics is genuinely doing something new.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from finval.baselines.gaussian import gaussian_baseline
|
|
26
|
+
from finval.baselines.historical import block_bootstrap, historical_bootstrap
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"gaussian_baseline",
|
|
30
|
+
"historical_bootstrap",
|
|
31
|
+
"block_bootstrap",
|
|
32
|
+
]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Multivariate Gaussian baseline.
|
|
2
|
+
|
|
3
|
+
Fits a multivariate normal to the real returns and samples from it.
|
|
4
|
+
This is the naive "Gaussian i.i.d." benchmark. A model that can't beat
|
|
5
|
+
this on temporal metrics is not capturing any time structure.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def gaussian_baseline(
|
|
14
|
+
real: np.ndarray,
|
|
15
|
+
n_samples: int | None = None,
|
|
16
|
+
n_paths: int | None = None,
|
|
17
|
+
path_length: int | None = None,
|
|
18
|
+
seed: int = 42,
|
|
19
|
+
) -> np.ndarray:
|
|
20
|
+
"""Generate samples from a multivariate Gaussian fit to real returns.
|
|
21
|
+
|
|
22
|
+
Two output modes:
|
|
23
|
+
|
|
24
|
+
1. Flat mode (n_samples given): returns shape (n_samples, n_features),
|
|
25
|
+
matching the format expected by distribution / dependence metrics.
|
|
26
|
+
|
|
27
|
+
2. Path mode (n_paths and path_length given): returns shape
|
|
28
|
+
(n_paths, path_length, n_features), matching the format expected
|
|
29
|
+
by temporal and path metrics. Each row within a path is an
|
|
30
|
+
independent draw — there is no temporal structure.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
real: (n_obs, n_features) real return series used to fit mean+cov.
|
|
34
|
+
n_samples: Number of flat samples to return.
|
|
35
|
+
n_paths: Number of paths to return (requires path_length).
|
|
36
|
+
path_length: Length of each path.
|
|
37
|
+
seed: RNG seed.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
numpy array of shape (n_samples, n_features) or (n_paths, path_length, n_features).
|
|
41
|
+
"""
|
|
42
|
+
real = np.asarray(real)
|
|
43
|
+
if real.ndim != 2:
|
|
44
|
+
raise ValueError(f"real must be 2D, got shape {real.shape}")
|
|
45
|
+
|
|
46
|
+
mean = np.nanmean(real, axis=0)
|
|
47
|
+
# Handle NaN by dropping rows before covariance
|
|
48
|
+
clean = real[~np.any(np.isnan(real), axis=1)]
|
|
49
|
+
if len(clean) < 2:
|
|
50
|
+
raise ValueError("need at least 2 clean rows to fit covariance")
|
|
51
|
+
cov = np.cov(clean, rowvar=False)
|
|
52
|
+
# Regularize to ensure positive-definite
|
|
53
|
+
d = cov.shape[0]
|
|
54
|
+
cov = cov + 1e-10 * np.eye(d)
|
|
55
|
+
|
|
56
|
+
rng = np.random.default_rng(seed)
|
|
57
|
+
|
|
58
|
+
if n_samples is not None:
|
|
59
|
+
return rng.multivariate_normal(mean, cov, size=n_samples)
|
|
60
|
+
|
|
61
|
+
if n_paths is not None and path_length is not None:
|
|
62
|
+
return rng.multivariate_normal(mean, cov, size=(n_paths, path_length))
|
|
63
|
+
|
|
64
|
+
raise ValueError("must specify either n_samples or (n_paths and path_length)")
|