quantvolt 0.1.0__tar.gz → 0.2.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.
Files changed (121) hide show
  1. {quantvolt-0.1.0 → quantvolt-0.2.0}/Cargo.lock +1 -1
  2. {quantvolt-0.1.0 → quantvolt-0.2.0}/Cargo.toml +1 -2
  3. {quantvolt-0.1.0 → quantvolt-0.2.0}/PKG-INFO +30 -4
  4. {quantvolt-0.1.0 → quantvolt-0.2.0}/README.md +26 -2
  5. {quantvolt-0.1.0 → quantvolt-0.2.0}/pyproject.toml +23 -5
  6. {quantvolt-0.1.0 → quantvolt-0.2.0}/rust/src/paths.rs +2 -2
  7. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/__init__.py +73 -1
  8. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/assets/dispatch_sdp.py +8 -2
  9. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/assets/long_dated.py +16 -17
  10. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/cli.py +45 -4
  11. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/data/datasets.py +2 -1
  12. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/data/netztransparenz.py +4 -12
  13. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/hedging/ppa_nomination.py +4 -12
  14. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/models/__init__.py +72 -0
  15. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/models/commodity.py +18 -3
  16. quantvolt-0.2.0/src/quantvolt/models/instruments.py +514 -0
  17. quantvolt-0.2.0/src/quantvolt/models/ppa.py +89 -0
  18. quantvolt-0.2.0/src/quantvolt/models/ppa_terms.py +444 -0
  19. quantvolt-0.2.0/src/quantvolt/models/units.py +224 -0
  20. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/numerics/__init__.py +4 -0
  21. quantvolt-0.2.0/src/quantvolt/numerics/_degenerate.py +75 -0
  22. quantvolt-0.2.0/src/quantvolt/numerics/bachelier.py +316 -0
  23. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/numerics/black76.py +6 -18
  24. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/portfolio/model.py +31 -3
  25. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/portfolio/settlement.py +2 -6
  26. quantvolt-0.2.0/src/quantvolt/portfolio/valuation.py +641 -0
  27. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/pricing/__init__.py +26 -0
  28. quantvolt-0.2.0/src/quantvolt/pricing/bachelier.py +105 -0
  29. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/pricing/power_hedge.py +1 -3
  30. quantvolt-0.2.0/src/quantvolt/pricing/ppa.py +855 -0
  31. quantvolt-0.2.0/src/quantvolt/pricing/ppa_valuation.py +372 -0
  32. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/risk/cashflow_metrics.py +1 -3
  33. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/risk/scenarios.py +2 -1
  34. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/testing.py +5 -1
  35. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/workflow/modeling.py +8 -8
  36. quantvolt-0.1.0/src/quantvolt/models/instruments.py +0 -221
  37. quantvolt-0.1.0/src/quantvolt/models/ppa.py +0 -54
  38. quantvolt-0.1.0/src/quantvolt/portfolio/valuation.py +0 -231
  39. quantvolt-0.1.0/src/quantvolt/pricing/ppa.py +0 -377
  40. {quantvolt-0.1.0 → quantvolt-0.2.0}/.gitignore +0 -0
  41. {quantvolt-0.1.0 → quantvolt-0.2.0}/LICENSE +0 -0
  42. {quantvolt-0.1.0 → quantvolt-0.2.0}/rust/src/asian_mc.rs +0 -0
  43. {quantvolt-0.1.0 → quantvolt-0.2.0}/rust/src/lib.rs +0 -0
  44. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/_core.pyi +0 -0
  45. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/_validation.py +0 -0
  46. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/assets/__init__.py +0 -0
  47. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/assets/_tolerance.py +0 -0
  48. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/assets/dispatch_approx.py +0 -0
  49. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/assets/dispatch_deterministic.py +0 -0
  50. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/assets/plant.py +0 -0
  51. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/assets/storage.py +0 -0
  52. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/curvemodels/__init__.py +0 -0
  53. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/curvemodels/multifactor.py +0 -0
  54. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/curvemodels/schwartz_smith.py +0 -0
  55. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/curves/__init__.py +0 -0
  56. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/curves/arbitrage.py +0 -0
  57. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/curves/builder.py +0 -0
  58. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/data/__init__.py +0 -0
  59. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/data/base.py +0 -0
  60. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/data/commercial.py +0 -0
  61. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/data/dataset_catalog.json +0 -0
  62. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/data/entsoe.py +0 -0
  63. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/data/entsog.py +0 -0
  64. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/data/open_meteo.py +0 -0
  65. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/data/smard.py +0 -0
  66. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/exceptions.py +0 -0
  67. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/hedging/__init__.py +0 -0
  68. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/hedging/_conditioning.py +0 -0
  69. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/hedging/hybrid.py +0 -0
  70. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/hedging/mean_variance.py +0 -0
  71. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/hedging/ppa_walk_forward.py +0 -0
  72. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/hedging/variance_min.py +0 -0
  73. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/market/__init__.py +0 -0
  74. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/market/outages.py +0 -0
  75. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/market/transmission.py +0 -0
  76. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/market/weather.py +0 -0
  77. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/models/curve.py +0 -0
  78. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/models/discount_curve.py +0 -0
  79. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/models/greeks.py +0 -0
  80. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/models/interval.py +0 -0
  81. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/models/power_hedge.py +0 -0
  82. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/models/schedule.py +0 -0
  83. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/models/vol_surface.py +0 -0
  84. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/numerics/_normal.py +0 -0
  85. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/numerics/daycount.py +0 -0
  86. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/numerics/exotic.py +0 -0
  87. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/numerics/interpolation.py +0 -0
  88. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/numerics/monte_carlo.py +0 -0
  89. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/numerics/risk_adjustment.py +0 -0
  90. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/numerics/rootfind.py +0 -0
  91. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/numerics/spread_models.py +0 -0
  92. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/portfolio/__init__.py +0 -0
  93. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/pricing/_dates.py +0 -0
  94. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/pricing/exotic.py +0 -0
  95. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/pricing/futures.py +0 -0
  96. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/pricing/implied_vol.py +0 -0
  97. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/pricing/mark_to_market.py +0 -0
  98. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/pricing/spread_option.py +0 -0
  99. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/pricing/spreads.py +0 -0
  100. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/pricing/swap.py +0 -0
  101. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/pricing/tolling.py +0 -0
  102. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/pricing/transmission_right.py +0 -0
  103. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/pricing/vanilla.py +0 -0
  104. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/py.typed +0 -0
  105. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/risk/__init__.py +0 -0
  106. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/risk/_levels.py +0 -0
  107. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/risk/aggregation.py +0 -0
  108. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/risk/cfar.py +0 -0
  109. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/risk/covariance.py +0 -0
  110. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/risk/credit_var.py +0 -0
  111. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/risk/engine.py +0 -0
  112. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/risk/mc_var.py +0 -0
  113. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/risk/parametric_var.py +0 -0
  114. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/stats/__init__.py +0 -0
  115. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/stats/correlation.py +0 -0
  116. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/stats/descriptive.py +0 -0
  117. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/stats/mean_reversion.py +0 -0
  118. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/stats/normality.py +0 -0
  119. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/stats/stationarity.py +0 -0
  120. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/workflow/__init__.py +0 -0
  121. {quantvolt-0.1.0 → quantvolt-0.2.0}/src/quantvolt/workflow/criteria.py +0 -0
@@ -237,7 +237,7 @@ dependencies = [
237
237
 
238
238
  [[package]]
239
239
  name = "quantvolt-core"
240
- version = "0.1.0"
240
+ version = "0.2.0"
241
241
  dependencies = [
242
242
  "ndarray 0.17.2",
243
243
  "numpy",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "quantvolt-core"
3
- version = "0.1.0"
3
+ version = "0.2.0"
4
4
  edition = "2021"
5
5
  publish = false
6
6
  readme = "README.md"
@@ -27,4 +27,3 @@ rand_distr = "0.6"
27
27
  # reduction in CI wheel-build time.
28
28
  [profile.release]
29
29
  lto = "thin"
30
-
@@ -1,11 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quantvolt
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Intended Audience :: Financial and Insurance Industry
6
6
  Classifier: Intended Audience :: Science/Research
7
7
  Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3.11
8
9
  Classifier: Programming Language :: Python :: 3.12
10
+ Classifier: Programming Language :: Python :: 3.13
9
11
  Classifier: Programming Language :: Rust
10
12
  Classifier: Topic :: Office/Business :: Financial :: Investment
11
13
  Classifier: Topic :: Scientific/Engineering :: Mathematics
@@ -28,7 +30,7 @@ Summary: Quantitative analysis of European power and energy markets
28
30
  Keywords: energy,power,gas,carbon,quant,derivatives,risk-management,pricing,monte-carlo,forward-curve
29
31
  Author-email: Nima Bahrami <nima@skillmix.nl>
30
32
  License-Expression: MIT
31
- Requires-Python: >=3.12
33
+ Requires-Python: >=3.11
32
34
  Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
33
35
  Project-URL: Documentation, https://nimabahrami.github.io/quantvolt/
34
36
  Project-URL: Homepage, https://nimabahrami.github.io/quantvolt/
@@ -51,9 +53,24 @@ Covered markets: EEX Phelix DE/AT power, EPEX SPOT power (DE, FR, NL, BE, GB), T
51
53
  natural gas (ICE Endex), and EUA carbon allowances — extensible by the caller without editing
52
54
  library source. See [docs/european-markets.md](docs/european-markets.md).
53
55
 
56
+ ## Why QuantVolt?
57
+
58
+ A typical desk assembles a curve spreadsheet, a standalone options pricer, a storage/dispatch
59
+ optimizer and a bolted-on VaR tool, each with its own units, sign conventions and delivery-period
60
+ identity — hand-reconciling between them is slow and a recurring source of quiet errors. QuantVolt
61
+ instead moves the same immutable `ForwardCurve` / `DeliveryPeriod` / `MarketData` / `Greeks` value
62
+ objects through curve construction, pricing, portfolio valuation, risk, realized settlement and
63
+ physical-asset valuation, with determinism, eager validation and serializability enforced
64
+ structurally rather than left to convention. It is equally explicit about scope: a PPA stays
65
+ `unpriced` until you register a forward-looking pricer, storage/dispatch results are brought into
66
+ a portfolio explicitly, no market data ships with the package, and approximations (Kirk,
67
+ Turnbull-Wakeman, `bang_bang`) are labelled, never hidden. See
68
+ [the "Why QuantVolt?" page](https://nimabahrami.github.io/quantvolt/#/guide/why) and the
69
+ [external validation evidence](docs/validation.md) for the checkable version of these claims.
70
+
54
71
  ## Install
55
72
 
56
- Requires Python 3.12+.
73
+ Requires Python 3.11+.
57
74
 
58
75
  ```bash
59
76
  uv add quantvolt # or: pip install quantvolt
@@ -295,7 +312,7 @@ specialised names stay on their sub-package. Full reference with a runnable exam
295
312
  | `quantvolt.stats` | Descriptive statistics, normality tests, ADF/KPSS stationarity with Samuelson-effect detection, correlation, Ornstein-Uhlenbeck mean reversion |
296
313
  | `quantvolt.market` | Transmission cost, temperature/degree-day utilities, generation outage / reliability KPIs |
297
314
  | `quantvolt.workflow` | 7-step model-selection workflow for structured products |
298
- | `quantvolt.portfolio` | `Portfolio`/`Position` book assembly, forward-looking `value_portfolio`, and realized interval `settle_energy_portfolio` aggregation |
315
+ | `quantvolt.portfolio` | `Portfolio`/`Position` book assembly; `value_portfolio` natively prices futures, forwards, swaps, transmission/pipeline rights, vanilla options, spread options and tolling agreements via the caller-extensible `DEFAULT_PRICERS` registry (PPAs and realized hedges are valued via a caller-registered pricer, e.g. `make_ppa_pricer` — deliberately not a default); realized interval `settle_energy_portfolio` aggregation is kept structurally separate from that forward-looking NPV |
299
316
  | `quantvolt.data` | Optional (`quantvolt[data]`): provider adapters behind a `DataSource` protocol — the only package that performs I/O |
300
317
  | `quantvolt.testing` | `assert_input_unchanged` — shipped test utility for the no-mutation invariant |
301
318
  | `quantvolt.exceptions` | The `EnergyQuantError` hierarchy (`ValidationError`, `ArbitrageError`, `MissingTenorError`, …) |
@@ -317,11 +334,20 @@ positions. See [docs/risk-and-assets.md](docs/risk-and-assets.md).
317
334
 
318
335
  ## Documentation
319
336
 
337
+ - [Why QuantVolt?](https://nimabahrami.github.io/quantvolt/#/guide/why) — the integration problem,
338
+ the shared computational model, the extensible pricer registry, and an honest "what QuantVolt is
339
+ not" section
320
340
  - [docs/api.md](docs/api.md) — module-by-module API reference with runnable examples
321
341
  - [docs/risk-and-assets.md](docs/risk-and-assets.md) — the VaR family, the physical-vs-risk-neutral
322
342
  drift rule, incomplete-market hedging, dispatch & storage, and the long-dated / liquidity caveats
323
343
  - [docs/european-markets.md](docs/european-markets.md) — market coverage, power-vs-gas
324
344
  statistics, negative prices, Samuelson effect, carbon costs, data-source policy
345
+ - [docs/validation.md](docs/validation.md) — external validation evidence: QuantVolt's pricers
346
+ checked against QuantLib 1.43 golden fixtures, with honest caveats and regeneration instructions
347
+ - Three complete, runnable end-to-end tutorials (site, each backed by a `site/examples/
348
+ verify_tutorial_*.py` script): [spark spread to hedge](https://nimabahrami.github.io/quantvolt/#/guide/tutorial-spark),
349
+ [renewable PPA to CFaR](https://nimabahrami.github.io/quantvolt/#/guide/tutorial-ppa), and
350
+ [storage intrinsic to hedge](https://nimabahrami.github.io/quantvolt/#/guide/tutorial-storage)
325
351
  - `.kiro/steering/` — product, tech, structure, and coding-style standards
326
352
  - `.kiro/specs/power-energy-quant-analysis/` — requirements, design, and task breakdown
327
353
  (the library is spec-driven; the design document is the source of truth for the maths)
@@ -14,9 +14,24 @@ Covered markets: EEX Phelix DE/AT power, EPEX SPOT power (DE, FR, NL, BE, GB), T
14
14
  natural gas (ICE Endex), and EUA carbon allowances — extensible by the caller without editing
15
15
  library source. See [docs/european-markets.md](docs/european-markets.md).
16
16
 
17
+ ## Why QuantVolt?
18
+
19
+ A typical desk assembles a curve spreadsheet, a standalone options pricer, a storage/dispatch
20
+ optimizer and a bolted-on VaR tool, each with its own units, sign conventions and delivery-period
21
+ identity — hand-reconciling between them is slow and a recurring source of quiet errors. QuantVolt
22
+ instead moves the same immutable `ForwardCurve` / `DeliveryPeriod` / `MarketData` / `Greeks` value
23
+ objects through curve construction, pricing, portfolio valuation, risk, realized settlement and
24
+ physical-asset valuation, with determinism, eager validation and serializability enforced
25
+ structurally rather than left to convention. It is equally explicit about scope: a PPA stays
26
+ `unpriced` until you register a forward-looking pricer, storage/dispatch results are brought into
27
+ a portfolio explicitly, no market data ships with the package, and approximations (Kirk,
28
+ Turnbull-Wakeman, `bang_bang`) are labelled, never hidden. See
29
+ [the "Why QuantVolt?" page](https://nimabahrami.github.io/quantvolt/#/guide/why) and the
30
+ [external validation evidence](docs/validation.md) for the checkable version of these claims.
31
+
17
32
  ## Install
18
33
 
19
- Requires Python 3.12+.
34
+ Requires Python 3.11+.
20
35
 
21
36
  ```bash
22
37
  uv add quantvolt # or: pip install quantvolt
@@ -258,7 +273,7 @@ specialised names stay on their sub-package. Full reference with a runnable exam
258
273
  | `quantvolt.stats` | Descriptive statistics, normality tests, ADF/KPSS stationarity with Samuelson-effect detection, correlation, Ornstein-Uhlenbeck mean reversion |
259
274
  | `quantvolt.market` | Transmission cost, temperature/degree-day utilities, generation outage / reliability KPIs |
260
275
  | `quantvolt.workflow` | 7-step model-selection workflow for structured products |
261
- | `quantvolt.portfolio` | `Portfolio`/`Position` book assembly, forward-looking `value_portfolio`, and realized interval `settle_energy_portfolio` aggregation |
276
+ | `quantvolt.portfolio` | `Portfolio`/`Position` book assembly; `value_portfolio` natively prices futures, forwards, swaps, transmission/pipeline rights, vanilla options, spread options and tolling agreements via the caller-extensible `DEFAULT_PRICERS` registry (PPAs and realized hedges are valued via a caller-registered pricer, e.g. `make_ppa_pricer` — deliberately not a default); realized interval `settle_energy_portfolio` aggregation is kept structurally separate from that forward-looking NPV |
262
277
  | `quantvolt.data` | Optional (`quantvolt[data]`): provider adapters behind a `DataSource` protocol — the only package that performs I/O |
263
278
  | `quantvolt.testing` | `assert_input_unchanged` — shipped test utility for the no-mutation invariant |
264
279
  | `quantvolt.exceptions` | The `EnergyQuantError` hierarchy (`ValidationError`, `ArbitrageError`, `MissingTenorError`, …) |
@@ -280,11 +295,20 @@ positions. See [docs/risk-and-assets.md](docs/risk-and-assets.md).
280
295
 
281
296
  ## Documentation
282
297
 
298
+ - [Why QuantVolt?](https://nimabahrami.github.io/quantvolt/#/guide/why) — the integration problem,
299
+ the shared computational model, the extensible pricer registry, and an honest "what QuantVolt is
300
+ not" section
283
301
  - [docs/api.md](docs/api.md) — module-by-module API reference with runnable examples
284
302
  - [docs/risk-and-assets.md](docs/risk-and-assets.md) — the VaR family, the physical-vs-risk-neutral
285
303
  drift rule, incomplete-market hedging, dispatch & storage, and the long-dated / liquidity caveats
286
304
  - [docs/european-markets.md](docs/european-markets.md) — market coverage, power-vs-gas
287
305
  statistics, negative prices, Samuelson effect, carbon costs, data-source policy
306
+ - [docs/validation.md](docs/validation.md) — external validation evidence: QuantVolt's pricers
307
+ checked against QuantLib 1.43 golden fixtures, with honest caveats and regeneration instructions
308
+ - Three complete, runnable end-to-end tutorials (site, each backed by a `site/examples/
309
+ verify_tutorial_*.py` script): [spark spread to hedge](https://nimabahrami.github.io/quantvolt/#/guide/tutorial-spark),
310
+ [renewable PPA to CFaR](https://nimabahrami.github.io/quantvolt/#/guide/tutorial-ppa), and
311
+ [storage intrinsic to hedge](https://nimabahrami.github.io/quantvolt/#/guide/tutorial-storage)
288
312
  - `.kiro/steering/` — product, tech, structure, and coding-style standards
289
313
  - `.kiro/specs/power-energy-quant-analysis/` — requirements, design, and task breakdown
290
314
  (the library is spec-driven; the design document is the source of truth for the maths)
@@ -4,10 +4,10 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "quantvolt"
7
- version = "0.1.0"
7
+ version = "0.2.0"
8
8
  description = "Quantitative analysis of European power and energy markets"
9
9
  readme = "README.md"
10
- requires-python = ">=3.12"
10
+ requires-python = ">=3.11"
11
11
  license = "MIT"
12
12
  license-files = ["LICENSE"]
13
13
  authors = [{ name = "Nima Bahrami", email = "nima@skillmix.nl" }]
@@ -28,7 +28,9 @@ classifiers = [
28
28
  "Intended Audience :: Financial and Insurance Industry",
29
29
  "Intended Audience :: Science/Research",
30
30
  "Programming Language :: Python :: 3",
31
+ "Programming Language :: Python :: 3.11",
31
32
  "Programming Language :: Python :: 3.12",
33
+ "Programming Language :: Python :: 3.13",
32
34
  "Programming Language :: Rust",
33
35
  "Topic :: Office/Business :: Financial :: Investment",
34
36
  "Topic :: Scientific/Engineering :: Mathematics",
@@ -61,12 +63,20 @@ dev = [
61
63
  "maturin>=1.7,<2.0",
62
64
  ]
63
65
 
66
+ # PEP 735 dependency group for OFFLINE external-validation fixture generation only.
67
+ # NOT a [project.optional-dependencies] extra and NOT a core/runtime dependency: the
68
+ # analytics core never imports QuantLib, CI never installs this group, and the generator
69
+ # scripts under scripts/ (wheel-excluded above) are the only consumers. See
70
+ # .kiro/specs/external-validation/ and docs/validation.md for the regeneration workflow.
71
+ [dependency-groups]
72
+ validation = ["QuantLib>=1.34"]
73
+
64
74
  [tool.maturin]
65
75
  python-source = "src"
66
76
  module-name = "quantvolt._core"
67
- # abi3-py312: build a single stable-ABI wheel that works on CPython 3.12+
77
+ # abi3-py311: build a single stable-ABI wheel that works on CPython 3.11+
68
78
  # (one compile per platform instead of one per Python version).
69
- features = ["pyo3/extension-module", "pyo3/abi3-py312"]
79
+ features = ["pyo3/extension-module", "pyo3/abi3-py311"]
70
80
  # Keep the sdist to the buildable package only. The docs site, rendered docs,
71
81
  # research data, helper scripts, and the logo are not part of the installable
72
82
  # library and would only bloat the distribution.
@@ -90,12 +100,20 @@ exclude = [
90
100
  [tool.ruff]
91
101
  line-length = 100
92
102
  src = ["src", "tests"]
103
+ target-version = "py311"
93
104
 
94
105
  [tool.ruff.lint]
95
106
  select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
96
107
 
108
+ [tool.ruff.lint.per-file-ignores]
109
+ # Authored documentation text (AUTHORED_DOCS) carries long human-readable lines;
110
+ # RUF001 flags the en-dashes used deliberately in that prose.
111
+ "site/build_api.py" = ["E501", "RUF001"]
112
+ # One-off internal refactoring tool, not shipped code.
113
+ "site/tools/refactor_reference_design.py" = ["E501"]
114
+
97
115
  [tool.mypy]
98
- python_version = "3.12"
116
+ python_version = "3.11"
99
117
  strict = true
100
118
  files = ["src/quantvolt"]
101
119
 
@@ -490,11 +490,11 @@ pub fn simulate_correlated_forwards_term_core(
490
490
  if antithetic {
491
491
  write_record(&mut data, p_minus, 0, rec, dim, &minus);
492
492
  }
493
- for step in 0..steps {
493
+ for (step, factor) in factors.iter().enumerate().take(steps) {
494
494
  for e in &mut eps {
495
495
  *e = rng.sample::<f64, _>(StandardNormal);
496
496
  }
497
- matvec_lower(&factors[step], &eps, dim, &mut incr);
497
+ matvec_lower(factor, &eps, dim, &mut incr);
498
498
  let offset = step * dim;
499
499
  for d in 0..dim {
500
500
  if active_steps[offset + d] {
@@ -22,6 +22,7 @@ from .assets import (
22
22
  PhysicalFactorMapping,
23
23
  PlantModel,
24
24
  StorageModel,
25
+ ValuationSource,
25
26
  bang_bang,
26
27
  dispatch_deterministic,
27
28
  dispatch_value,
@@ -70,7 +71,12 @@ from .hedging import (
70
71
  )
71
72
  from .models import (
72
73
  BUILT_IN_COMMODITIES,
74
+ CachedAssetValuation,
75
+ CapFloorStripContract,
76
+ CapFloorType,
77
+ ChangeInLawAllocation,
73
78
  CommodityConfig,
79
+ CurtailmentTreatment,
74
80
  CurveNode,
75
81
  DeliveryPeriod,
76
82
  DeliverySchedule,
@@ -81,24 +87,43 @@ from .models import (
81
87
  Granularity,
82
88
  Greeks,
83
89
  Hub,
90
+ IndexationStep,
84
91
  InstrumentPriceRecord,
85
92
  Moneyness,
93
+ NegativePriceClause,
94
+ NegativePriceTreatment,
95
+ OptionSide,
96
+ OptionType,
86
97
  PipelineRight,
87
98
  PlantConfig,
88
99
  PowerDeliveryInterval,
89
100
  PowerHedgeContract,
90
101
  PowerHedgePosition,
91
102
  PowerHedgeType,
103
+ PpaAvailabilityGuarantee,
92
104
  PpaContract,
105
+ PpaContractMetadata,
106
+ PpaCreditSupportType,
107
+ PpaPriceTerms,
108
+ PpaReconciliationPeriod,
109
+ PpaReconciliationTerms,
93
110
  PpaSettlementType,
111
+ PpaTerms,
112
+ PpaToleranceBand,
94
113
  PpaVolumeBasis,
114
+ PpaVolumeTerms,
115
+ PriceUnit,
95
116
  RiskType,
96
117
  SettlementType,
118
+ SpreadOptionContract,
97
119
  SwapContract,
120
+ TollingAgreement,
98
121
  TransmissionRight,
99
122
  TransportDirection,
123
+ VanillaOptionContract,
100
124
  VolatilitySurface,
101
125
  VolatilityTenor,
126
+ convert_price,
102
127
  )
103
128
  from .portfolio import (
104
129
  Instrument,
@@ -114,6 +139,8 @@ from .portfolio import (
114
139
  )
115
140
  from .pricing import (
116
141
  AsianOptionRequest,
142
+ BachelierOptionRequest,
143
+ BachelierOptionResult,
117
144
  BarrierOptionRequest,
118
145
  CapFloorRequest,
119
146
  CapFloorResult,
@@ -129,6 +156,11 @@ from .pricing import (
129
156
  PowerHedgeSettlement,
130
157
  PpaDataColumns,
131
158
  PpaIntervalSettlement,
159
+ PpaPeriodValuation,
160
+ PpaPeriodVolume,
161
+ PpaReconciliationColumns,
162
+ PpaValuationResult,
163
+ PpaVolumeProfile,
132
164
  SpreadOptionRequest,
133
165
  SpreadOptionResult,
134
166
  SwapPricingResult,
@@ -147,19 +179,23 @@ from .pricing import (
147
179
  futures_delta,
148
180
  implied_heat_rate,
149
181
  implied_vol,
182
+ make_ppa_pricer,
150
183
  mark_to_market,
151
184
  power_cap_payoff,
152
185
  power_floor_payoff,
153
186
  price_asian,
187
+ price_bachelier_option,
154
188
  price_barrier,
155
189
  price_cap_floor,
156
190
  price_futures,
157
191
  price_lookback,
192
+ price_ppa,
158
193
  price_spark_spread_option,
159
194
  price_spread_option,
160
195
  price_swap,
161
196
  price_tolling_agreement,
162
197
  price_vanilla_option,
198
+ reconcile_ppa_ledger,
163
199
  settle_power_hedge_interval,
164
200
  settle_power_hedges_frame,
165
201
  settle_ppa_frame,
@@ -188,7 +224,7 @@ from .risk import (
188
224
  parametric_var,
189
225
  )
190
226
 
191
- __version__ = "0.1.0"
227
+ __version__ = "0.2.0"
192
228
 
193
229
  __all__ = [
194
230
  "BUILT_IN_COMMODITIES",
@@ -197,13 +233,20 @@ __all__ = [
197
233
  "ArbitrageWarning",
198
234
  "AsianOptionRequest",
199
235
  "AuthenticationError",
236
+ "BachelierOptionRequest",
237
+ "BachelierOptionResult",
200
238
  "BangBangHedgeWarning",
201
239
  "BarrierOptionRequest",
240
+ "CachedAssetValuation",
202
241
  "CapFloorRequest",
203
242
  "CapFloorResult",
243
+ "CapFloorStripContract",
244
+ "CapFloorType",
204
245
  "CashflowStrategyComparison",
205
246
  "CashflowStrategyMetrics",
247
+ "ChangeInLawAllocation",
206
248
  "CommodityConfig",
249
+ "CurtailmentTreatment",
207
250
  "CurveBuildResult",
208
251
  "CurveBuilder",
209
252
  "CurveNode",
@@ -229,6 +272,7 @@ __all__ = [
229
272
  "Greeks",
230
273
  "Hub",
231
274
  "ImpliedVolResult",
275
+ "IndexationStep",
232
276
  "Instrument",
233
277
  "InstrumentPriceRecord",
234
278
  "InsufficientDataError",
@@ -243,8 +287,12 @@ __all__ = [
243
287
  "MtMResult",
244
288
  "MultifactorForwardModel",
245
289
  "NativeExtensionError",
290
+ "NegativePriceClause",
291
+ "NegativePriceTreatment",
246
292
  "NoPricingDataError",
247
293
  "NumericalError",
294
+ "OptionSide",
295
+ "OptionType",
248
296
  "PhysicalFactorMapping",
249
297
  "PipelineRight",
250
298
  "PlantConfig",
@@ -259,16 +307,31 @@ __all__ = [
259
307
  "PowerHedgePosition",
260
308
  "PowerHedgeSettlement",
261
309
  "PowerHedgeType",
310
+ "PpaAvailabilityGuarantee",
262
311
  "PpaContract",
312
+ "PpaContractMetadata",
313
+ "PpaCreditSupportType",
263
314
  "PpaDataColumns",
264
315
  "PpaIntervalSettlement",
265
316
  "PpaNominationCandidate",
266
317
  "PpaNominationColumns",
267
318
  "PpaNominationFit",
268
319
  "PpaNominationObjective",
320
+ "PpaPeriodValuation",
321
+ "PpaPeriodVolume",
322
+ "PpaPriceTerms",
323
+ "PpaReconciliationColumns",
324
+ "PpaReconciliationPeriod",
325
+ "PpaReconciliationTerms",
269
326
  "PpaSettlementType",
327
+ "PpaTerms",
328
+ "PpaToleranceBand",
329
+ "PpaValuationResult",
270
330
  "PpaVolumeBasis",
331
+ "PpaVolumeProfile",
332
+ "PpaVolumeTerms",
271
333
  "PpaWalkForwardResult",
334
+ "PriceUnit",
272
335
  "PricedPosition",
273
336
  "RateLimitError",
274
337
  "RiskEngine",
@@ -281,16 +344,20 @@ __all__ = [
281
344
  "SchwartzSmithParams",
282
345
  "SettledPortfolioPosition",
283
346
  "SettlementType",
347
+ "SpreadOptionContract",
284
348
  "SpreadOptionRequest",
285
349
  "SpreadOptionResult",
286
350
  "StorageModel",
287
351
  "SwapContract",
288
352
  "SwapPricingResult",
353
+ "TollingAgreement",
289
354
  "TollingResult",
290
355
  "TransmissionRight",
291
356
  "TransportDirection",
292
357
  "TransportRightResult",
293
358
  "ValidationError",
359
+ "ValuationSource",
360
+ "VanillaOptionContract",
294
361
  "VanillaOptionRequest",
295
362
  "VanillaOptionResult",
296
363
  "VolatilitySurface",
@@ -307,6 +374,7 @@ __all__ = [
307
374
  "classify_moneyness",
308
375
  "clean_spread",
309
376
  "compare_cashflow_strategies",
377
+ "convert_price",
310
378
  "crack_spread",
311
379
  "credit_var",
312
380
  "dark_spread",
@@ -321,21 +389,25 @@ __all__ = [
321
389
  "implied_heat_rate",
322
390
  "implied_vol",
323
391
  "linear_cross_hedge",
392
+ "make_ppa_pricer",
324
393
  "mark_to_market",
325
394
  "monte_carlo_var",
326
395
  "parametric_var",
327
396
  "power_cap_payoff",
328
397
  "power_floor_payoff",
329
398
  "price_asian",
399
+ "price_bachelier_option",
330
400
  "price_barrier",
331
401
  "price_cap_floor",
332
402
  "price_futures",
333
403
  "price_lookback",
404
+ "price_ppa",
334
405
  "price_spark_spread_option",
335
406
  "price_spread_option",
336
407
  "price_swap",
337
408
  "price_tolling_agreement",
338
409
  "price_vanilla_option",
410
+ "reconcile_ppa_ledger",
339
411
  "settle_energy_portfolio",
340
412
  "settle_power_hedge_interval",
341
413
  "settle_power_hedges_frame",
@@ -771,7 +771,13 @@ def _dispatch_lsm(
771
771
  conditions.append(float(np.linalg.cond(basis)))
772
772
  predicted = basis @ coeffs
773
773
  continuation = {s: predicted[:, j] for j, s in enumerate(feasible_next)}
774
- coefficients = {s: coeffs[:, j].copy() for j, s in enumerate(feasible_next)}
774
+ # .astype(np.float64) (not .copy()): numpy's lstsq stub types its first
775
+ # return value as floating[Any] on some numpy releases (narrower Vector =
776
+ # NDArray[float64] elsewhere) -- this pins the static (and, since the data
777
+ # is already float64, identical-at-runtime) dtype across numpy versions.
778
+ coefficients = {
779
+ s: coeffs[:, j].astype(np.float64) for j, s in enumerate(feasible_next)
780
+ }
775
781
  else:
776
782
  # A min-run-locked online state whose derated capacity has collapsed
777
783
  # below every feasible level on *some* paths realizes -inf there (a
@@ -799,7 +805,7 @@ def _dispatch_lsm(
799
805
  step_conditions.append(float(np.linalg.cond(design)))
800
806
  predicted_s = basis @ coeffs_s
801
807
  continuation[state] = np.where(finite, predicted_s, -np.inf)
802
- coefficients[state] = coeffs_s.copy()
808
+ coefficients[state] = coeffs_s.astype(np.float64)
803
809
  conditions.append(max(step_conditions, default=1.0))
804
810
  policy_coefficients.append(coefficients)
805
811
 
@@ -18,11 +18,15 @@ long-dated risk is never understated (Req 23):
18
18
 
19
19
  Tagging semantics
20
20
  -----------------
21
- :class:`ValuationSource` is the single vocabulary of provenance tags. Its two
22
- values, ``"forward"`` and ``"projected"``, are exactly the strings a caller should
23
- propagate onto a :class:`~quantvolt.portfolio.model.Position`'s ``tags`` so that
24
- downstream risk code can tell the regimes apart. :class:`BenchmarkResult` carries
25
- the tag prominently in its ``source`` field.
21
+ :class:`ValuationSource` (``models/instruments.py``) is the single vocabulary of
22
+ provenance tags. This module produces two of its values, ``"forward"`` and
23
+ ``"projected"`` -- exactly the strings a caller should propagate onto a
24
+ :class:`~quantvolt.portfolio.model.Position`'s ``tags`` so that downstream risk code
25
+ can tell the regimes apart. :class:`BenchmarkResult` carries the tag prominently in
26
+ its ``source`` field. A third value, ``"simulated"``, is produced elsewhere (the
27
+ portfolio-native-pricers spec's ``CachedAssetValuation`` wrapper, Req 19) for a
28
+ precomputed LSMC/dispatch cache -- a third regime this module does not itself
29
+ produce, tagged the same way for the same reason (Property-66 pattern).
26
30
 
27
31
  Intended wiring (this module does not touch ``risk/``)
28
32
  ------------------------------------------------------
@@ -62,10 +66,10 @@ from __future__ import annotations
62
66
 
63
67
  from collections.abc import Callable
64
68
  from dataclasses import dataclass
65
- from enum import StrEnum
66
69
 
67
70
  from ..exceptions import MissingTenorError, ValidationError
68
71
  from ..models.curve import ForwardCurve
72
+ from ..models.instruments import ValuationSource as ValuationSource # explicit re-export
69
73
  from ..models.schedule import DeliveryPeriod
70
74
  from ..numerics.risk_adjustment import PriceOfRiskKind
71
75
  from ..portfolio.model import PricedPosition
@@ -73,17 +77,12 @@ from ..portfolio.model import PricedPosition
73
77
  # A pure projection of a period to its model spot expectation (documented, never mutated).
74
78
  SpotModel = Callable[[DeliveryPeriod], float]
75
79
 
76
-
77
- class ValuationSource(StrEnum):
78
- """Provenance of a long-dated valuation; the tag that separates the two regimes.
79
-
80
- The string values double as the ``Position.tags`` markers a caller propagates so
81
- that :func:`var_applicability_guard` (and any risk code) can tell a projected
82
- value apart from a forward-based one.
83
- """
84
-
85
- FORWARD = "forward" # liquid forward curve covers the period (Req 23.1)
86
- PROJECTED = "projected" # projected from a spot model + corporate premium (Req 23.2)
80
+ # ``ValuationSource`` is now defined in ``models/instruments.py`` (the portfolio-native-pricers
81
+ # spec, Req 19, reuses it for a THIRD regime -- a precomputed LSMC/dispatch cache -- and
82
+ # ``models/instruments.py`` is the leaf-ish module every instrument-bearing type can import
83
+ # without a cycle back through ``portfolio/model.py``, which this module already depends on).
84
+ # Imported here (not re-defined) so there remains exactly one provenance vocabulary; every
85
+ # existing ``from quantvolt.assets.long_dated import ValuationSource`` import keeps working.
87
86
 
88
87
 
89
88
  @dataclass(frozen=True, slots=True)
@@ -6,11 +6,52 @@ import argparse
6
6
  import json
7
7
  from dataclasses import asdict
8
8
  from importlib import import_module
9
+ from pathlib import Path
10
+ from typing import Any, Protocol, cast
9
11
 
10
12
 
11
- def _datasets():
13
+ class _DatasetRecordLike(Protocol):
14
+ """Structural stand-in for ``quantvolt.data.datasets.DatasetRecord``.
15
+
16
+ The core (including this CLI entry point) must never statically import
17
+ ``quantvolt.data`` (Req 12.1; enforced by
18
+ ``tests/unit/test_data_isolation.py::test_no_core_source_file_imports_the_data_layer``),
19
+ so this protocol names only the attributes the CLI itself reads rather than importing
20
+ the real dataclass.
21
+ """
22
+
23
+ dataset_id: str
24
+ version: str
25
+ filename: str
26
+ size: int
27
+ sha256: str
28
+ format: str
29
+ license: str
30
+ source: str
31
+ url: str
32
+
33
+
34
+ class _DatasetsModule(Protocol):
35
+ """Structural type for the ``quantvolt.data.datasets`` submodule.
36
+
37
+ ``quantvolt.data.datasets`` is imported lazily by name (rather than with a normal
38
+ ``import`` statement) so that CLI subcommands other than ``data`` never trigger the
39
+ optional ``quantvolt[data]`` extra's dependency (``httpx``, pulled in transitively via
40
+ ``quantvolt.data.__init__``). This protocol lets mypy still check the call sites below
41
+ against the submodule's real signatures without a static import of the data layer.
42
+ """
43
+
44
+ def list_datasets(self) -> tuple[_DatasetRecordLike, ...]: ...
45
+ def info(self, dataset_id: str) -> _DatasetRecordLike: ...
46
+ def fetch(self, dataset_id: str, *, force: bool = ..., offline: bool = ...) -> Path: ...
47
+ def verify(self, dataset_id: str) -> bool: ...
48
+ def path(self, dataset_id: str) -> Path: ...
49
+ def remove(self, dataset_id: str) -> bool: ...
50
+
51
+
52
+ def _datasets() -> _DatasetsModule:
12
53
  """Load the optional data layer only after the CLI is invoked."""
13
- return import_module("quantvolt.data.datasets")
54
+ return cast("_DatasetsModule", import_module("quantvolt.data.datasets"))
14
55
 
15
56
 
16
57
  def _parser() -> argparse.ArgumentParser:
@@ -36,14 +77,14 @@ def main(argv: list[str] | None = None) -> int:
36
77
  if args.operation == "list":
37
78
  records = datasets.list_datasets()
38
79
  if args.as_json:
39
- print(json.dumps([asdict(record) for record in records], indent=2))
80
+ print(json.dumps([asdict(cast(Any, record)) for record in records], indent=2))
40
81
  else:
41
82
  for record in records:
42
83
  print(f"{record.dataset_id:38} {record.version:8} {record.size:>10} bytes")
43
84
  return 0
44
85
  dataset_id = args.dataset_id
45
86
  if args.operation == "info":
46
- print(json.dumps(asdict(datasets.info(dataset_id)), indent=2))
87
+ print(json.dumps(asdict(cast(Any, datasets.info(dataset_id))), indent=2))
47
88
  elif args.operation == "fetch":
48
89
  print(datasets.fetch(dataset_id, force=args.force, offline=args.offline))
49
90
  elif args.operation == "verify":
@@ -38,7 +38,8 @@ class DatasetRecord:
38
38
 
39
39
  def _catalog() -> dict[str, Any]:
40
40
  resource = files("quantvolt.data").joinpath(_CATALOG_RESOURCE)
41
- return json.loads(resource.read_text(encoding="utf-8"))
41
+ catalog: dict[str, Any] = json.loads(resource.read_text(encoding="utf-8"))
42
+ return catalog
42
43
 
43
44
 
44
45
  def _record(dataset_id: str) -> DatasetRecord: