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.
@@ -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)")