feat: documentation update
- Completed export workflow implementation (query builders, CSV/XLSX serializers, streaming API endpoints, UI modals, automated tests). - Added export modal UI and client script to trigger downloads directly from dashboard. - Documented import/export field mapping and usage guidelines in FR-008. - Updated installation guide with export environment variables, dependencies, and CLI/CI usage instructions.
This commit is contained in:
184
specifications/financial_metrics.md
Normal file
184
specifications/financial_metrics.md
Normal file
@@ -0,0 +1,184 @@
|
||||
# Financial Metrics Specification
|
||||
|
||||
## 1. Purpose
|
||||
|
||||
Define the standard methodology CalMiner uses to compute discounted-cash-flow
|
||||
profitability metrics, including Net Present Value (NPV), Internal Rate of Return
|
||||
(IRR), and Payback Period. These calculations underpin scenario evaluation,
|
||||
reporting, and investment decision support within the platform.
|
||||
|
||||
## 2. Scope
|
||||
|
||||
- Applies to scenario-level profitability analysis using cash flows stored in
|
||||
the application database.
|
||||
- Covers deterministic cash-flow evaluation; stochastic extensions (e.g., Monte
|
||||
Carlo) may overlay these metrics but should reference this specification.
|
||||
- Documents the assumptions implemented in `services/financial.py` and the
|
||||
related pytest coverage in `tests/test_financial.py`.
|
||||
|
||||
## 3. Inputs and Definitions
|
||||
|
||||
| Symbol | Name | Description | Units / Domain | Notes |
|
||||
| -------- | ----------------------- | ------------------------------------------- | ----------------- | --------------------------------------------------------- |
|
||||
| $CF_t$ | Cash flow at period $t$ | Currency amount (positive or negative) | Scenario currency | Negative values typically represent investments/outflows. |
|
||||
| $t$ | Period index | Fractional period (0 = anchor) | Real number | Derived from explicit index or calendar date. |
|
||||
| $r$ | Discount rate | Decimal representation of the annual rate | $r > -1$ | Scenario configuration provides default rate. |
|
||||
| $m$ | Compounds per year | Compounding frequency | Positive integer | Defaults to 1 (annual). |
|
||||
| $RV$ | Residual value | Terminal value realised after final period | Scenario currency | Optional. |
|
||||
| $t_{RV}$ | Residual periods | Timing of residual value relative to anchor | Real number | Defaults to last period + 1. |
|
||||
|
||||
### Period Anchoring and Timing
|
||||
|
||||
Cash flows can be supplied with either:
|
||||
|
||||
1. `period_index` — explicit integers/floats overriding all other timing.
|
||||
2. `date` — calendar dates. The earliest dated flow anchors the timeline and
|
||||
subsequent dates convert the day difference into fractional periods using a
|
||||
365-day year divided by `compounds_per_year`.
|
||||
3. Neither — flows default to sequential periods based on input order.
|
||||
|
||||
This aligns with `normalize_cash_flows` in `services/financial.py`, ensuring all
|
||||
calculations receive `(amount, periods)` tuples.
|
||||
|
||||
## 4. Net Present Value (NPV)
|
||||
|
||||
### Formula
|
||||
|
||||
For a set of cash flows $CF_t$ with discount rate $r$ and compounding frequency
|
||||
$m$:
|
||||
|
||||
$$
|
||||
\text{NPV} = \sum_{t=0}^{n} \frac{CF_t}{\left(1 + \frac{r}{m}\right)^{t}} +
|
||||
\begin{cases}
|
||||
\dfrac{RV}{\left(1 + \frac{r}{m}\right)^{t_{RV}}} & \text{if residual value present} \\
|
||||
0 & \text{otherwise}
|
||||
\end{cases}
|
||||
$$
|
||||
|
||||
### Implementation Notes
|
||||
|
||||
- `discount_factor` computes $(1 + r/m)^{-t}`; NPV iterates over the normalised
|
||||
flows and sums `amount \* factor`.
|
||||
- Residual values default to one period after the final cash flow when
|
||||
`residual_periods` is omitted.
|
||||
- Empty cash-flow sequences return 0 unless a residual value is supplied.
|
||||
|
||||
## 5. Internal Rate of Return (IRR)
|
||||
|
||||
### Definition
|
||||
|
||||
IRR is the discount rate $r$ for which NPV equals zero:
|
||||
|
||||
$$
|
||||
0 = \sum_{t=0}^{n} \frac{CF_t}{\left(1 + \frac{r}{m}\right)^{t}}
|
||||
$$
|
||||
|
||||
### Solver Behaviour
|
||||
|
||||
- Newton–Raphson iteration starts from `guess` (default 10%).
|
||||
- Derivative instability or non-finite values trigger a fallback to a bracketed
|
||||
bisection search between:
|
||||
- Lower bound: $-0.99 \times m$
|
||||
- Upper bound: $10.0$ (doubles until the root is bracketed or attempts exceed 12)
|
||||
- Raises `ConvergenceError` when no sign change is found or the bisection fails
|
||||
within the iteration budget (`max_iterations` default 100; bisection uses
|
||||
double this limit).
|
||||
- Validates that the cash-flow series includes at least one negative and one
|
||||
positive value; otherwise IRR is undefined and a `ValueError` is raised.
|
||||
|
||||
### Caveats
|
||||
|
||||
- Multiple sign changes may yield multiple IRRs. The solver returns the root it
|
||||
finds within the configured bounds; scenarios must interpret the result in
|
||||
context.
|
||||
- Rates less than `-1 * m` imply nonphysical periodic rates and are excluded.
|
||||
|
||||
## 6. Payback Period
|
||||
|
||||
### Definition
|
||||
|
||||
The payback period is the earliest period $t$ where cumulative cash flows become
|
||||
non-negative. With fractional interpolation (default behaviour), the period is
|
||||
calculated as:
|
||||
|
||||
$$
|
||||
\text{Payback} = t_{prev} + \left(\frac{-\text{Cumulative}_{prev}}{CF_t}\right)
|
||||
\times (t - t_{prev})
|
||||
$$
|
||||
|
||||
where $t_{prev}$ is the previous period with negative cumulative cash flow.
|
||||
|
||||
### Implementation Notes
|
||||
|
||||
- Cash flows are sorted by period to ensure chronological accumulation.
|
||||
- When `allow_fractional` is `False`, the function returns the first period with
|
||||
non-negative cumulative total without interpolation.
|
||||
- `PaybackNotReachedError` is raised if the cumulative total never becomes
|
||||
non-negative.
|
||||
|
||||
## 7. Examples
|
||||
|
||||
### Example 1: Baseline Project
|
||||
|
||||
- Initial investment: $-1,000,000$ at period 0.
|
||||
- Annual inflows: 300k, 320k, 340k, 360k, 450k (periods 1-5).
|
||||
- Discount rate: 8% annual, `compounds_per_year = 1`.
|
||||
|
||||
| Period | Cash Flow (currency) |
|
||||
| ------ | -------------------- |
|
||||
| 0 | -1,000,000 |
|
||||
| 1 | 300,000 |
|
||||
| 2 | 320,000 |
|
||||
| 3 | 340,000 |
|
||||
| 4 | 360,000 |
|
||||
| 5 | 450,000 |
|
||||
|
||||
- `net_present_value` ≈ 205,759
|
||||
- `internal_rate_of_return` ≈ 0.158
|
||||
- `payback_period` ≈ 4.13 periods
|
||||
|
||||
### Example 2: Residual Value with Irregular Timing
|
||||
|
||||
- Investment: -500,000 on 2024-01-01
|
||||
- Cash inflows on irregular dates (2024-07-01: 180k, 2025-01-01: 200k,
|
||||
2025-11-01: 260k)
|
||||
- Residual value 150k realised two years after final inflow
|
||||
- Discount rate: 10%, `compounds_per_year = 4`
|
||||
|
||||
NPV discounts each cash flow by converting day deltas to quarterly periods. The
|
||||
residual is discounted at `t_{RV} = last_period + 2` (because the override is
|
||||
supplied).
|
||||
|
||||
## 8. Testing Strategy
|
||||
|
||||
`tests/test_financial.py` exercises:
|
||||
|
||||
- `normalize_cash_flows` with date-based, index-based, and sequential cash-flow
|
||||
inputs.
|
||||
- NPV calculations with and without residual values, including discount-rate
|
||||
sensitivity checks.
|
||||
- IRR convergence success cases, invalid inputs, and non-converging scenarios.
|
||||
- Payback period exact, fractional, and never-payback cases.
|
||||
|
||||
Developers extending the financial metrics should add regression tests covering
|
||||
new assumptions or solver behaviour.
|
||||
|
||||
## 9. Integration Notes
|
||||
|
||||
- Scenario evaluation services should pass cash flows as `CashFlow` instances to
|
||||
reuse the shared normalisation logic.
|
||||
- UI and reporting layers should display rates as percentages but supply them as
|
||||
decimals to the service layer.
|
||||
- Future Monte Carlo or sensitivity analyses can reuse the same helpers to
|
||||
evaluate each simulated cash-flow path.
|
||||
|
||||
## 10. References
|
||||
|
||||
- Internal implementation: `calminer/services/financial.py`
|
||||
- Tests: `calminer/tests/test_financial.py`
|
||||
- Related specification: `calminer-docs/specifications/price_calculation.md`
|
||||
- Architecture context: `calminer-docs/architecture/08_concepts/02_data_model.md`
|
||||
|
||||
```}
|
||||
|
||||
```
|
||||
141
specifications/monte_carlo_simulation.md
Normal file
141
specifications/monte_carlo_simulation.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# Monte Carlo Simulation Specification
|
||||
|
||||
## 1. Purpose
|
||||
|
||||
Define the configuration, inputs, and outputs for CalMiner's Monte Carlo
|
||||
simulation engine used to evaluate project scenarios with stochastic cash-flow
|
||||
assumptions. The engine augments deterministic profitability metrics by
|
||||
sampling cash-flow distributions and aggregating resulting Net Present Value
|
||||
(NPV), Internal Rate of Return (IRR), and Payback Period statistics.
|
||||
|
||||
## 2. Scope
|
||||
|
||||
- Applies to scenario-level profitability analysis executed via
|
||||
`services/simulation.py`.
|
||||
- Covers configuration dataclasses (`SimulationConfig`, `CashFlowSpec`,
|
||||
`DistributionSpec`) and supported distribution families.
|
||||
- Outlines expectations for downstream reporting and visualization modules that
|
||||
consume simulation results.
|
||||
|
||||
## 3. Inputs
|
||||
|
||||
### 3.1 Cash Flow Specifications
|
||||
|
||||
Each Monte Carlo run receives an ordered collection of `CashFlowSpec` entries.
|
||||
Each spec pairs a deterministic `CashFlow` (amount, period index/date) with an
|
||||
optional `DistributionSpec`. When no distribution is provided the deterministic
|
||||
value is used for every iteration.
|
||||
|
||||
### 3.2 Simulation Configuration
|
||||
|
||||
`SimulationConfig` controls execution:
|
||||
|
||||
| Field | Description |
|
||||
| ------------------------------------- | ----------------------------------------------------------------- |
|
||||
| `iterations` | Number of Monte Carlo iterations (must be > 0). |
|
||||
| `discount_rate` | Annual discount rate (decimal) passed to NPV helper. |
|
||||
| `seed` | Optional RNG seed to ensure reproducible sampling. |
|
||||
| `metrics` | Tuple of requested metrics (`npv`, `irr`, `payback`). |
|
||||
| `percentiles` | Percentile cutoffs (0–100) computed for each metric. |
|
||||
| `compounds_per_year` | Compounding frequency reused by financial helpers. |
|
||||
| `return_samples` | When `True`, raw metric samples are returned alongside summaries. |
|
||||
| `residual_value` / `residual_periods` | Optional residual cash flow inputs reused by NPV. |
|
||||
|
||||
### 3.3 Context Metadata
|
||||
|
||||
Optional dictionaries provide dynamic parameters when sourcing distribution
|
||||
means or other values:
|
||||
|
||||
- `scenario_context`: scenario-specific values (e.g., salvage mean, cost
|
||||
overrides).
|
||||
- `metadata`: shared configuration (e.g., global commodity price expectations).
|
||||
|
||||
## 4. Distributions
|
||||
|
||||
`DistributionSpec` defines stochastic behaviour:
|
||||
|
||||
| Property | Description |
|
||||
| ------------ | ------------------------------------------------------------------------------- |
|
||||
| `type` | `normal`, `lognormal`, `triangular`, or `discrete`. |
|
||||
| `parameters` | Mapping of required parameters per distribution family. |
|
||||
| `source` | How base parameters are sourced: `static`, `scenario_field`, or `metadata_key`. |
|
||||
| `source_key` | Identifier used for non-static sources. |
|
||||
|
||||
### 4.1 Parameter Validation
|
||||
|
||||
- `normal`: requires non-negative `std_dev`; defaults `mean` to baseline cash
|
||||
flow amount when omitted.
|
||||
- `lognormal`: requires `mean` (mu in log space) and non-negative `sigma`.
|
||||
- `triangular`: requires `min`, `mode`, `max` with constraint `min <= mode <= max`.
|
||||
- `discrete`: requires paired `values`/`probabilities` sequences; probabilities
|
||||
must be non-negative and sum to 1.0.
|
||||
|
||||
Invalid definitions raise `DistributionConfigError` before sampling.
|
||||
|
||||
## 5. Algorithm Overview
|
||||
|
||||
1. Seed a NumPy `Generator` (`default_rng(seed)`) unless a generator instance is
|
||||
supplied.
|
||||
2. For each iteration:
|
||||
- Realise cash flows by sampling distributions or using deterministic
|
||||
values.
|
||||
- Compute requested metrics using shared helpers from
|
||||
`services/financial.py`:
|
||||
- NPV via `net_present_value` (respecting `residual_value` inputs).
|
||||
- IRR via `internal_rate_of_return`; non-converging or invalid trajectories
|
||||
return `NaN` and increment `failed_runs`.
|
||||
- Payback via `payback_period`; scenarios failing to hit non-negative
|
||||
cumulative cash flow record `NaN`.
|
||||
3. Aggregate results into per-metric arrays; calculate summary statistics:
|
||||
mean, sample standard deviation, min/max, and configured percentiles using
|
||||
`numpy.percentile`.
|
||||
4. Assemble `SimulationResult` containing summary descriptors and optional raw
|
||||
samples when `return_samples` is enabled.
|
||||
|
||||
## 6. Outputs
|
||||
|
||||
`SimulationResult` includes:
|
||||
|
||||
- `iterations`: total iteration count executed.
|
||||
- `summaries`: mapping of `SimulationMetric` to `MetricSummary` objects with:
|
||||
- `mean`, `std_dev`, `minimum`, `maximum`.
|
||||
- `percentiles`: mapping of configured percentile cutoffs to values.
|
||||
- `sample_size`: number of successful (non-NaN) samples.
|
||||
- `failed_runs`: count of iterations producing `NaN` for the metric.
|
||||
- `samples`: optional mapping of metric to raw `numpy.ndarray` of samples when
|
||||
detailed analysis is required downstream.
|
||||
|
||||
## 7. Error Handling
|
||||
|
||||
- Invalid configuration or missing context raises `DistributionConfigError`.
|
||||
- Zero iterations or invalid percentile ranges raise `ValueError`.
|
||||
- Financial helper exceptions (`ConvergenceError`, `PaybackNotReachedError`)
|
||||
are captured per iteration and converted to `NaN` samples to preserve
|
||||
aggregate results while flagging failure counts.
|
||||
|
||||
## 8. Usage Guidance
|
||||
|
||||
- Scenario services should construct `CashFlowSpec` instances from persisted
|
||||
financial inputs and optional uncertainty definitions stored alongside the
|
||||
scenario.
|
||||
- Reporting routes can request raw samples when producing histogram or violin
|
||||
plots; otherwise rely on `MetricSummary` statistics for tabular output.
|
||||
- Visualizations implementing FR-005 should leverage percentile outputs to
|
||||
render fan charts or confidence intervals.
|
||||
- When integrating with scheduling workflows, persist the deterministic seed to
|
||||
ensure repeated runs remain comparable.
|
||||
|
||||
## 9. Testing
|
||||
|
||||
`tests/test_simulation.py` covers deterministic parity with financial helpers,
|
||||
seed reproducibility, context parameter sourcing, failure accounting for metrics
|
||||
that cannot be computed, error handling for misconfigured distributions, and
|
||||
sample-return functionality. Additional regression cases should accompany new
|
||||
metrics or distribution families.
|
||||
|
||||
## 10. References
|
||||
|
||||
- Implementation: `calminer/services/simulation.py`
|
||||
- Financial helpers: `calminer/services/financial.py`
|
||||
- Tests: `calminer/tests/test_simulation.py`
|
||||
- Related specification: `calminer-docs/specifications/financial_metrics.md`
|
||||
@@ -1,4 +1,141 @@
|
||||
# Variables for Price Calculation
|
||||
# Product Price Calculation Specification
|
||||
|
||||
## 1. Purpose
|
||||
|
||||
Provide a detailed reference for calculating product sale prices across supported metals, including adjustments for ore grade, recovery rate, moisture, and impurities. This document extends the initial variable list with explicit formula definitions, validation rules, and example workflows.
|
||||
|
||||
## 2. Scope
|
||||
|
||||
- Applies to primary commodity outputs (e.g., copper, gold, lithium) cited in scenario models.
|
||||
- Supports integration into scenario profitability pipelines and exportable reporting.
|
||||
- Covers penalties/credits based on quality metrics (water content, impurity assays) and market adjustments.
|
||||
|
||||
## 3. Inputs & Parameters
|
||||
|
||||
| Symbol | Name | Description | Units / Domain | Validation |
|
||||
| ------------- | ----------------------- | ---------------------------------------------------- | ---------------------------- | ----------------------- |
|
||||
| $M$ | Metal | Commodity identifier (e.g., `copper`, `gold`) | Enum `MetalType` | Required |
|
||||
| $Q_{ore}$ | Ore tonnage processed | Total ore mass entering processing | metric tonnes | $Q_{ore} > 0$ |
|
||||
| $G$ | Head grade | Percentage of target metal in ore (mass fraction) | % (0–100) | $0 < G \leq 100$ |
|
||||
| $R$ | Recovery rate | Plant recovery efficiency | % (0–100) | $0 < R \leq 100$ |
|
||||
| $T$ | Treatment charge | Base processing fee negotiated with smelter/refiner | currency / tonne concentrate | $T \geq 0$ |
|
||||
| $S$ | Smelting charge | Additional fee tied to concentrate handling | currency / tonne concentrate | $S \geq 0$ |
|
||||
| $M_{moist}$ | Moisture content | Percentage water in concentrate | % (0–100) | $0 \leq M_{moist} < 40$ |
|
||||
| $M_{imp}^{i}$ | Impurity content | For impurity _i_ (e.g., As, Pb, Zn) measure mass ppm | ppm | $0 \leq M_{imp}^{i}$ |
|
||||
| $F_{moist}$ | Moisture penalty factor | Currency impact per excess moisture percentage | currency / % | Optional |
|
||||
| $F_{imp}^{i}$ | Impurity penalty factor | Currency impact per ppm over threshold | currency / ppm | Optional |
|
||||
| $Adj_{prem}$ | Premiums/credits | Adders (e.g., gold credits in copper concentrate) | currency | Optional |
|
||||
| $FX$ | FX rate | Convert pricing currency to scenario currency | currency conversion rate | $FX > 0$ |
|
||||
|
||||
## 4. Derived Values
|
||||
|
||||
1. **Metal content** (payable basis):
|
||||
$$ Q*{metal} = Q*{ore} \times \frac{G}{100} \times \frac{R}{100} $$
|
||||
|
||||
2. **Payable mass after deductions** (if contractual payable is <100%): Introduce payable percentage $K_M$ (default 100%).
|
||||
$$ Q*{pay} = Q*{metal} \times \frac{K_M}{100} $$
|
||||
|
||||
3. **Gross revenue** in reference currency:
|
||||
$$ Rev*{gross}^{ref} = Q*{pay} \times P\_{ref} $$
|
||||
|
||||
4. **Treatment and smelting charges**:
|
||||
$$ Charges = T + S $$
|
||||
|
||||
5. **Moisture penalty** (if moisture exceeds threshold $M_{moist}^{thr}$):
|
||||
$$ Pen*{moist} = \max(0, M*{moist} - M*{moist}^{thr}) \times F*{moist} $$
|
||||
|
||||
6. **Impurity penalty** (sum over impurities with thresholds $M_{imp}^{i,thr}$):
|
||||
$$ Pen*{imp} = \sum_i \max(0, M*{imp}^{i} - M*{imp}^{i,thr}) \times F*{imp}^{i} $$
|
||||
|
||||
7. **Net revenue before premiums**:
|
||||
$$ Rev*{net}^{ref} = Rev*{gross}^{ref} - Charges - Pen*{moist} - Pen*{imp} $$
|
||||
|
||||
8. **Apply premiums/credits**:
|
||||
$$ Rev*{adj}^{ref} = Rev*{net}^{ref} + Adj\_{prem} $$
|
||||
|
||||
9. **Convert to scenario currency**:
|
||||
$$ Rev*{adj} = Rev*{adj}^{ref} \times FX $$
|
||||
|
||||
## 5. Workflow Examples
|
||||
|
||||
### 5.1 Copper Concentrate
|
||||
|
||||
- Ore feed $Q_{ore} = 100,000$ t, head grade $G = 1.2\%$, recovery $R = 90\%$.
|
||||
- Reference price $P_{ref} = \$8,500$/t, payable percentage $K_M = 96\%$.
|
||||
- Treatment and smelting charges $T+S = \$100/t$ concentrate equivalent.
|
||||
- Moisture threshold 8%, actual 10%, penalty factor $F_{moist} = \$3,000$ per %.
|
||||
- Arsenic impurity: threshold 0 ppm (premium for As-free), actual 100 ppm, penalty factor $F_{imp}^{As} = \$2$ per ppm.
|
||||
|
||||
Calculations:
|
||||
|
||||
1. $Q_{metal} = 100,000 \times 0.012 \times 0.9 = 1,080$ t.
|
||||
2. $Q_{pay} = 1,080 \times 0.96 = 1,036.8$ t.
|
||||
3. $Rev_{gross}^{ref} = 1,036.8 \times 8,500 = \$8,812,800$.
|
||||
4. $Pen_{moist} = (10 - 8) \times 3,000 = \$6,000$.
|
||||
5. $Pen_{imp} = (100 - 0) \times 2 = \$200$.
|
||||
6. $Rev_{net}^{ref} = 8,812,800 - 100,000 - 6,000 - 200 = \$8,706,600$.
|
||||
7. Assume premiums $Adj_{prem} = \$50,000$, FX = 1.0 → final $Rev_{adj} = \$8,756,600$.
|
||||
|
||||
### 5.2 Gold Doré
|
||||
|
||||
- Ore feed 50,000 t, head grade 2.5 g/t (convert to % mass: 0.00025%), recovery 92%.
|
||||
- Reference price $P_{ref} = \$1,900$/oz, convert to per tonne of metal ($1\ \text{oz} = 0.0311035 \text{kg}$).
|
||||
- Treat $Q_{metal}$ in kilograms or troy ounces as implementation convenience.
|
||||
- No moisture/impurity penalties; add refining charge 1.5% of revenue.
|
||||
|
||||
Implementation note: Provide conversion helpers for precious metals (grams per tonne to ounces, etc.).
|
||||
|
||||
## 6. Validation & Error Handling
|
||||
|
||||
- Ensure required inputs are provided; raise descriptive validation errors per scenario when data missing.
|
||||
- Bound checks for percentage inputs; clamp or reject values outside (0,100].
|
||||
- Penalty factors default to zero if not configured; log warnings if impurities exceed supported list.
|
||||
- FX rate defaults to 1.0 when scenario currency matches reference currency; enforce positive values.
|
||||
|
||||
## 7. Data Sources & Configuration
|
||||
|
||||
- Reference prices sourced from market data integration (e.g., LME, LBMA). Provide placeholder configuration until connectors exist.
|
||||
- Penalty thresholds and factors configurable per smelter contract; persisted in the `pricing_settings` tables (`pricing_settings`, `pricing_metal_settings`, `pricing_impurity_settings`).
|
||||
- The default configuration lives in the `pricing_settings` row with slug `default`. `services.bootstrap.bootstrap_pricing_settings` runs during FastAPI startup (and via `scripts/initial_data.py`) to ensure this row exists and mirrors the configured baseline metadata.
|
||||
- Runtime consumers obtain metadata through the FastAPI dependency `dependencies.get_pricing_metadata`, which loads the persisted defaults with impurity overrides. If the requested slug is missing, the dependency reseeds the table from `Settings.pricing_metadata()` before returning a fresh `PricingMetadata` instance.
|
||||
- Deployment environments can still influence the initial bootstrap by setting the following environment variables **before the database is seeded**. They are only read when creating or reseeding the `default` record:
|
||||
|
||||
| Environment Variable | Default | Bootstrap Usage |
|
||||
| ------------------------------------------- | ------- | ------------------------------------------------------------------------------- |
|
||||
| `CALMINER_PRICING_DEFAULT_PAYABLE_PCT` | `100.0` | Initial payable percentage stored in the default pricing settings row. |
|
||||
| `CALMINER_PRICING_DEFAULT_CURRENCY` | `USD` | Initial currency recorded in the persisted metadata (can be set to `null`). |
|
||||
| `CALMINER_PRICING_MOISTURE_THRESHOLD_PCT` | `8.0` | Initial moisture threshold applied when seeding the database. |
|
||||
| `CALMINER_PRICING_MOISTURE_PENALTY_PER_PCT` | `0.0` | Initial moisture penalty factor captured in the persisted configuration record. |
|
||||
|
||||
Operators should update the database record (or project-specific overrides) after bootstrap to align with active smelter contracts. Subsequent application restarts reuse the stored values without re-reading environment variables.
|
||||
|
||||
## 8. Output Schema
|
||||
|
||||
Define structured result for integration:
|
||||
|
||||
```json
|
||||
{
|
||||
"metal": "copper",
|
||||
"ore_tonnage": 100000,
|
||||
"head_grade_pct": 1.2,
|
||||
"recovery_pct": 90,
|
||||
"payable_metal_tonnes": 1036.8,
|
||||
"reference_price": 8500,
|
||||
"gross_revenue": 8812800,
|
||||
"moisture_penalty": 6000,
|
||||
"impurity_penalty": 200,
|
||||
"treatment_smelt_charges": 100000,
|
||||
"premiums": 50000,
|
||||
"net_revenue": 8756600,
|
||||
"currency": "USD"
|
||||
}
|
||||
```
|
||||
|
||||
## 9. Dependencies & Next Steps
|
||||
|
||||
- Align with FR-006 (performance monitoring) to collect calculation metrics.
|
||||
- Coordinate with reporting requirements to expose inputs/outputs in exports.
|
||||
- Next implementation steps: build pricing service module, integrate with scenario evaluation, and author unit tests using example data above.# Variables for Price Calculation
|
||||
|
||||
## Variables
|
||||
|
||||
|
||||
77
specifications/pricing_settings_data_model.md
Normal file
77
specifications/pricing_settings_data_model.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# Pricing Settings Data Model
|
||||
|
||||
## Objective
|
||||
|
||||
Persist pricing configuration values that currently live in environment variables so projects can own default payable percentages, currency, and penalty factors without redeploying the application.
|
||||
|
||||
## Core Entity: `pricing_settings`
|
||||
|
||||
Holds the defaults that are injected into `PricingMetadata` when evaluating scenarios.
|
||||
|
||||
| Column | Type | Nullable | Default | Notes |
|
||||
| -------------------------- | ----------------------- | -------- | -------- | -------------------------------------------------------------------------- |
|
||||
| `id` | Integer | No | Identity | Primary key |
|
||||
| `name` | String(128) | No | — | Human readable label displayed in the UI |
|
||||
| `slug` | String(64) | No | — | Unique code used for programmatic lookup (e.g. `default`, `contract-a`) |
|
||||
| `description` | Text | Yes | `NULL` | Optional descriptive text |
|
||||
| `default_currency` | String(3) | Yes | `NULL` | Normalised ISO-4217 code; fallback when scenario currency is absent |
|
||||
| `default_payable_pct` | Numeric(5,2) | No | `100.00` | Default payable percentage applied when not supplied with an input |
|
||||
| `moisture_threshold_pct` | Numeric(5,2) | No | `8.00` | Percentage moisture threshold before penalties apply |
|
||||
| `moisture_penalty_per_pct` | Numeric(14,4) | No | `0.0000` | Currency amount deducted per percentage point above the moisture threshold |
|
||||
| `metadata` | JSON | Yes | `NULL` | Future extension bucket (e.g. FX assumptions) |
|
||||
| `created_at` | DateTime(timezone=True) | No | `now()` | Creation timestamp |
|
||||
| `updated_at` | DateTime(timezone=True) | No | `now()` | Auto updated timestamp |
|
||||
|
||||
## Child Entity: `pricing_metal_settings`
|
||||
|
||||
Stores overrides that apply to specific commodities (payable percentage or alternate thresholds).
|
||||
|
||||
| Column | Type | Nullable | Default | Notes |
|
||||
| -------------------------- | ----------------------- | -------- | -------- | ------------------------------------------------------------------------------ |
|
||||
| `id` | Integer | No | Identity | Primary key |
|
||||
| `pricing_settings_id` | Integer (FK) | No | — | References `pricing_settings.id` with cascade delete |
|
||||
| `metal_code` | String(32) | No | — | Normalised commodity identifier (e.g. `copper`, `gold`) |
|
||||
| `payable_pct` | Numeric(5,2) | Yes | `NULL` | Contractual payable percentage for this metal; overrides parent value when set |
|
||||
| `moisture_threshold_pct` | Numeric(5,2) | Yes | `NULL` | Optional metal specific moisture threshold |
|
||||
| `moisture_penalty_per_pct` | Numeric(14,4) | Yes | `NULL` | Optional metal specific penalty factor |
|
||||
| `data` | JSON | Yes | `NULL` | Additional metal settings (credits, payable deductions) |
|
||||
| `created_at` | DateTime(timezone=True) | No | `now()` | Creation timestamp |
|
||||
| `updated_at` | DateTime(timezone=True) | No | `now()` | Auto updated timestamp |
|
||||
|
||||
`metal_code` should have a unique constraint together with `pricing_settings_id` to prevent duplication.
|
||||
|
||||
## Child Entity: `pricing_impurity_settings`
|
||||
|
||||
Represents impurity penalty factors and thresholds that are injected into `PricingMetadata.impurity_thresholds` and `PricingMetadata.impurity_penalty_per_ppm`.
|
||||
|
||||
| Column | Type | Nullable | Default | Notes |
|
||||
| --------------------- | ----------------------- | -------- | -------- | ---------------------------------------------------- |
|
||||
| `id` | Integer | No | Identity | Primary key |
|
||||
| `pricing_settings_id` | Integer (FK) | No | — | References `pricing_settings.id` with cascade delete |
|
||||
| `impurity_code` | String(32) | No | — | Identifier such as `As`, `Pb`, `Zn` |
|
||||
| `threshold_ppm` | Numeric(14,4) | No | `0.0000` | Contractual impurity allowance |
|
||||
| `penalty_per_ppm` | Numeric(14,4) | No | `0.0000` | Currency penalty applied per ppm above the threshold |
|
||||
| `notes` | Text | Yes | `NULL` | Optional narrative about the contract rule |
|
||||
| `created_at` | DateTime(timezone=True) | No | `now()` | Creation timestamp |
|
||||
| `updated_at` | DateTime(timezone=True) | No | `now()` | Auto updated timestamp |
|
||||
|
||||
Add a unique constraint on `(pricing_settings_id, impurity_code)`.
|
||||
|
||||
## Mapping to `PricingMetadata`
|
||||
|
||||
- `default_currency`, `default_payable_pct`, `moisture_threshold_pct`, and `moisture_penalty_per_pct` map directly to the dataclass fields.
|
||||
- `pricing_metal_settings` rows provide per-metal overrides. During load, prefer metal-specific values when present, falling back to the parent record. These values hydrate a composed `PricingMetadata` or supplementary structure passed to the evaluator.
|
||||
- `pricing_impurity_settings` rows populate `PricingMetadata.impurity_thresholds` and `PricingMetadata.impurity_penalty_per_ppm` dictionaries.
|
||||
|
||||
## Usage Notes
|
||||
|
||||
- Global defaults can be represented by a `pricing_settings` row referenced by new projects. Future migrations will add `projects.pricing_settings_id` to point at the desired configuration.
|
||||
- The `metadata` JSON column gives room to store additional contract attributes (e.g. minimum lot penalties, premium formulas) without immediate schema churn.
|
||||
- Numeric precision follows existing financial models (two decimal places for percentages, four for monetary/ppm penalties) to align with current tests.
|
||||
|
||||
## Bootstrap & Runtime Loading
|
||||
|
||||
- `services.bootstrap.bootstrap_pricing_settings` ensures a baseline record (slug `default`) exists during FastAPI startup and when running `scripts/initial_data.py`. Initial values come from `config.settings.Settings.pricing_metadata()`, allowing operators to shape the first record via environment variables.
|
||||
- When projects lack an explicit configuration, the bootstrap associates them with the default record through `UnitOfWork.set_project_pricing_settings`, guaranteeing every project has pricing metadata.
|
||||
- At request time, `dependencies.get_pricing_metadata` loads the persisted defaults using `UnitOfWork.get_pricing_metadata(include_children=True)`. If the slug is missing, it reseeds the default record from the bootstrap metadata before returning a `PricingMetadata` instance.
|
||||
- Callers therefore observe a consistent fallback chain: project-specific settings → default database record → freshly seeded defaults derived from environment values.
|
||||
Reference in New Issue
Block a user