Rewrite as a probabilistic model with walk-forward evaluation.

Replace the 2024 model (model.py, ~2000 lines) with the btcmodel package, the
baseline for future work:

- Forecasts are quantiles of log price at each horizon, scored with CRPS in a
  walk-forward backtest (origins every 30 days from 2014, horizons 1 month to
  4 years). Skill is relative to a zero-drift random walk, with circular
  block-bootstrap intervals and a count of independent windows.
- Development data stops at 2024-11-26, the last day the 2024 model saw.
  Later outcomes are a holdout, scored only by `backtest --holdout`.
- Models: random_walk, drift_rw, and cycle (the 2024 model's cycle-position
  drift, now kernel-smoothed and recency-weighted). On development data
  nothing beats the random walk with confidence; cycle loses at every horizon.
- Prices: the Investing.com archive moves to data/ (cut at 2024-11-26; its
  last row was intraday) and is extended with Coinbase daily closes by
  `update`.

Also: Nix flake dev shell (Python 3.13, pandas 3), ruff in place of black,
pytest suite, and a rewritten README. NOTES.md is removed as inaccurate, and
poetry is dropped.
This commit is contained in:
sam
2026-09-24 02:19:02 -07:00
parent eefff47070
commit cfc27a38de
24 changed files with 1767 additions and 3124 deletions
+6 -2
View File
@@ -1,4 +1,8 @@
bitcoin_*.png
bitcoin_*.txt
/output
__pycache__/
.pytest_cache/
.ruff_cache/
.venv/
result
scratch.py
.DS_Store
-361
View File
@@ -1,361 +0,0 @@
# Bitcoin Price Model Documentation
## Model Overview
A probabilistic price projection model combining log returns analysis, cycle awareness, and Monte Carlo simulation. The model generates projected price ranges with confidence intervals, balancing short-term market dynamics with long-term cyclical patterns.
## Core Design Principles
### 1. Return Analysis
- Uses log returns for better handling of exponential growth
- Combines multiple timeframes for volatility estimation
- Implements adaptive window sizing based on market conditions
- Handles volatility clustering through regime-aware adjustments
### 2. Cycle Integration
- Recognizes Bitcoin's ~4 year (1460 day) halving cycle
- Maps historical returns to cycle positions (0-1 scale)
- Adjusts expectations based on position in cycle
- Handles transitions between cycles with uncertainty scaling
### 3. Market Era Recognition
Three distinct eras with specific characteristics:
- Early (2013-2017): Higher base volatility, conservative trends
- Transition (2017-2020): Futures market introduction period
- Mature (2020+): Institutional participation, reduced base volatility
### 4. Uncertainty Estimation
- Generates both point estimates and confidence intervals
- Adapts uncertainty based on market conditions
- Uses asymmetric volatility response
- Implements dynamic confidence interval calibration
## Architecture
### Key Components
1. **Trend Analysis (`analyze_trends`)**
- Calculates cycle-position-specific returns
- Applies position-aware smoothing
- Handles cycle boundaries
2. **Volatility Estimation (`calculate_volatility`)**
- Adaptive window sizing
- Multi-timeframe integration
- Era-specific scaling
- Regime detection and response
3. **Price Projection (`project_prices`)**
- Monte Carlo simulation engine
- Dynamic uncertainty scaling
- Confidence interval calculation
- Trend integration
4. **Projection Adjustment (`get_projection_adjustments`)**
- Time-varying uncertainty scaling
- Market condition response
- Cycle position awareness
- Minimum uncertainty bounds
# Model Performance & Validation
## Performance Characteristics
### Normal Market Conditions
- MAPE: 30-40% typical
- 95% CI Coverage: ~95%
- 68% CI Coverage: ~73%
- Best performance in mature market periods (2020+)
- Most reliable for 3-6 month horizons
### Stress Periods
- MAPE: 30-60%
- 95% CI Coverage: ~95%
- 68% CI Coverage: ~76%
- Wider but well-calibrated confidence intervals
- Maintains reliability through increased uncertainty
### Key Strengths
1. Consistent confidence interval coverage
2. Rapid adaptation to volatility changes
3. Robust handling of cycle transitions
4. Well-calibrated uncertainty estimates
### Known Limitations
1. Higher error during market structure changes
2. Increased uncertainty in early cycle periods
3. Limited incorporation of external factors
4. May underestimate extreme events
## Validation Framework
### Backtest Configuration
- Minimum training period: 8 years
- Validation period: 2 years
- Rolling window approach
- Separate evaluation of normal/stress periods
### Key Test Periods
1. **Cycle Transitions**
- Pre/post halving periods
- Historical halvings (2016, 2020, 2024)
- Cycle peak/trough transitions
2. **Market Structure Changes**
- Futures introduction (2017)
- Institution adoption (2020-2021)
- Major market events (e.g., COVID crash)
3. **Recent History**
- 2021 bull market
- 2022 drawdown
- 2024 recovery
### Validation Metrics
1. **Accuracy Measures**
- MAPE (Mean Absolute Percentage Error)
- RMSE (Root Mean Square Error)
- Maximum deviation
2. **Calibration Measures**
- Confidence interval coverage
- Uncertainty estimation accuracy
- Regime transition handling
3. **Stability Measures**
- Parameter sensitivity
- Training period dependence
- Regime change response
# Technical Implementation
## Core Functions
### Volatility Calculation
```python
def calculate_volatility(df, short_window=30, medium_window=90, long_window=180):
"""
Adaptive volatility calculation combining multiple timeframes.
Features:
- Dynamic window sizing based on market conditions
- Era-specific scaling factors
- Regime-aware adjustments
- Robust error handling and fallbacks
"""
```
Key parameters:
- `short_window`: Fast response (default 30 days)
- `medium_window`: Primary estimate (default 90 days)
- `long_window`: Stability baseline (default 180 days)
Adaptive features:
- Windows shrink in high volatility periods
- Expand during low volatility
- Minimum size constraints for stability
- Weighted combination based on regime
### Cycle Position
```python
def get_cycle_position(date, halving_dates):
"""
Calculate position in halving cycle (0 to 1).
0 = halving event
1 = just before next halving
"""
```
Position calculation:
- Linear interpolation between halvings
- Special handling for pre-first-halving
- Extension mechanism for future cycles
- Built-in boundary condition handling
### Price Projection
```python
def project_prices(df, days_forward=365, simulations=1000,
confidence_levels=[0.95, 0.68]):
"""
Generate price projections with confidence intervals.
Core simulation parameters:
- Number of paths: 1000
- Confidence levels: 95% and 68%
- Dynamic uncertainty scaling
"""
```
## Data Requirements
### Input Data
Minimum fields:
- Date
- Close price
- Trading volume (optional)
- High/Low (optional)
Format requirements:
- Daily data preferred
- Sorted chronologically
- No missing dates
- Prices > 0
### Training Data
Minimum requirements:
- 2 years for basic operation
- 8 years recommended
- Must include at least one cycle transition
- Should span multiple market regimes
## Error Handling
### Data Validation
- Missing value detection and interpolation
- Outlier identification
- Zero/negative price handling
- Volume anomaly detection
### Runtime Guards
- Minimum data length checks
- Window size validation
- Numerical stability checks
- Regime transition handling
### Fallback Mechanisms
1. Simple volatility calculation
2. Default uncertainty estimates
3. Conservative parameter sets
4. Standard cycle assumption
## Memory and Performance
### Optimization Features
- Efficient numpy operations
- Vectorized calculations where possible
- Smart data windowing
- Caching of intermediate results
### Resource Usage
Typical requirements for 10-year dataset:
- Memory: ~100MB
- CPU: ~2-5 seconds per projection
- Storage: Negligible
### Parallelization
- Multiprocessing support for backtests
- Independent path simulation
- Multiple period analysis
- Backtest parallelization
# Development History & Evolution
## Major Versions
### Version 1.0 (Initial Implementation)
- Basic log return analysis
- Fixed volatility windows
- Simple cycle position calculation
- Base Monte Carlo simulation
### Version 2.0 (Market Structure)
- Added era-based adjustments
- Improved cycle handling
- Multiple timeframe volatility
- Enhanced Monte Carlo engine
### Version 3.0 (Current)
- Adaptive volatility windows
- Dynamic uncertainty scaling
- Improved regime detection
- Enhanced confidence interval calibration
## Key Improvements
### Volatility Estimation
1. **Fixed → Adaptive Windows**
- Initial: Fixed 30/90/180 day windows
- Current: Dynamic sizing based on regime
- Result: Better regime transition handling
2. **Uncertainty Calibration**
- Initial: Fixed scaling factors
- Current: Market-aware dynamic scaling
- Result: More reliable confidence intervals
3. **Era Recognition**
- Initial: Single model for all periods
- Current: Era-specific adjustments
- Result: Better handling of market evolution
### Simulation Engine
1. **Path Generation**
- Initial: Basic random walks
- Current: Regime-aware path simulation
- Result: More realistic price trajectories
2. **Confidence Intervals**
- Initial: Fixed width
- Current: Dynamic, asymmetric intervals
- Result: Better calibrated uncertainty
## Failed Experiments
### 1. Complex Regime Detection
- Attempted multiple indicator fusion
- Added excessive complexity
- Reduced model stability
- Reverted to simpler approach
### 2. Machine Learning Integration
- Tested neural network components
- Reduced interpretability
- Inconsistent improvements
- Kept traditional statistical approach
### 3. External Factor Integration
- Tried incorporating macro indicators
- Added noise to projections
- Complicated parameter estimation
- Maintained focus on price dynamics
## Recent Improvements (2024)
### Adaptive Volatility Windows
- Implementation: Dynamic window sizing
- Purpose: Better regime handling
- Results:
- Improved 95% CI coverage to ~95%
- Better stress period handling
- More reliable uncertainty estimates
### Performance Metrics
Normal Periods:
- MAPE: 39.9%
- RMSE: $12,007
- 95% CI Coverage: 95.9%
- 68% CI Coverage: 72.5%
Stress Periods:
- MAPE: 32.8%
- RMSE: $12,794
- 95% CI Coverage: 95.2%
- 68% CI Coverage: 76.1%
## Future Directions
### Short Term
1. Fine-tune adaptive parameters
2. Improve transition period handling
3. Enhanced backtest framework
4. Additional regime indicators
### Medium Term
1. Cycle strength indicators
2. Volume analysis integration
3. Improved documentation
4. Performance optimization
### Long Term
1. Real-time adaptation framework
2. Advanced regime detection
3. Market microstructure integration
4. External API integration
+100 -10
View File
@@ -6,18 +6,108 @@
**Don't take this seriously. It's all in good fun.**
I decided to have fun and ask Anthropic's Claude AI (3.6 Sonnet) to help build
a Bitcoin price model, using data from [Investing.com](https://www.investing.com/crypto/bitcoin/historical-data).
## The 2024 edition
The model is still a mess: lots of redundant code as we went through various
methods for projecting future prices; the plots' colors don't render correctly.
In November 2024 I asked Claude (3.6 Sonnet, via copy and paste in Claude Web)
to help build a Bitcoin price model. After a lot of branching it produced ~2000
lines of cycle analysis, "market fundamentals", era adjustments and Monte Carlo,
and forecasts like this one (as of 2024-11-14):
I feel like I'm dangerous enough to know what to ask for out of a model, but
not knowledgeable enough evaluate whether what Claude produced actually makes
any sense. (Stats class in college was a long time ago...)
![moooooon](docs/2024-forecast.png)
## tl;dr show me the projection!
In September 2026 a newer Claude scored the forecasts it made on 2024-11-27
against what actually happened:
As of writing (2024-11-14), here is what the model generates:
| | 2024 forecast (median) | Actual |
|---|---|---|
| Cycle top | 2025-10-21, $199K | 2025-10-06, $124.7K |
| 2026-06-30 | $133K (95% floor $65K) | $58.5K |
| 2026-09-23 | $124K | $84.4K |
![moooooon](./moneyshot.png)
It called the timing of the top within 15 days, 19 months ahead, but put the
level ~60% too high. "The price stays at $92.7K" was a better forecast (18%
mean error against 68%). Its "95%" intervals were really ~81% intervals past a
year out, thanks to a fudge factor that narrowed them at long horizons.
That code lives in the jj/git history. This is the rewrite.
## How it works now
A forecast is a probability distribution of the price at each horizon, not a
line. Every model produces quantiles of log price, and every model is scored
the same way:
- **Walk-forward.** Every 30 days from 2014 on, each model sees only the data up
to that day and forecasts 1 month to 4 years ahead.
- **CRPS.** Each forecast is scored against what happened with the continuous
ranked probability score, in log-price units (0.1 ≈ "typically 10% off"). It
rewards being sharp and being calibrated at once, and can't be gamed by
narrowing or widening intervals.
- **Baselines.** Skill is reported relative to a zero-drift random walk, with a
90% block-bootstrap interval. Nearby forecasts overlap heavily, so the report
also shows `windows`: the number of genuinely independent outcomes.
- **Holdout.** Development only sees data up to 2024-11-26, the last day the
2024 model saw. Everything after it is held out, and is scored only by
`just holdout`, once per round of model changes. (Caveat: we already know
roughly what happened in 2025-26, so it isn't perfectly blind.)
### Models
- `random_walk`: zero drift; "it stays about here, give or take".
- `drift_rw`: drift equal to the last four years' average; "it keeps doing what
it did last cycle".
- `cycle`: the 2024 model's one real idea. Expected return depends on the day of
the halving cycle, estimated from past cycles, with recent cycles weighted
more.
### Findings so far (development data)
- Nothing beats the random walk with any confidence at any horizon. At 2-4
years there are only 3-5 independent outcomes in the whole history.
- `drift_rw` leads at 2-4 years (+13-18% skill, but the intervals span zero).
- `cycle` loses to both at every horizon, and so does every setting tried
(recency half-life 0.25-2 cycles, smoothing bandwidth 15-60 days). The cycle
*shape* costs accuracy. The level is the problem: each cycle has grown less
than the last (log return 4.0, 2.6, 2.0, i.e. roughly ×55, ×13, ×7), so any
average of past cycles overshoots.
That points at diminishing returns as the structure worth modelling, e.g. a
power-law trend, which is next.
## Usage
With Nix:
```sh
nix develop
just update # fetch new daily prices from Coinbase
just backtest # score models on development data -> output/backtest/
just forecast # forecast from the latest price -> output/forecast/
just test
just holdout # score on held-out outcomes; sparingly
```
Without Nix, any Python 3.13 with numpy, pandas 3, scipy and matplotlib works:
`python -m btcmodel --help`.
## Layout
```
btcmodel/
data.py price loading (Investing.com archive + Coinbase), dev cutoff
halving.py halving calendar, position in cycle
forecast.py Forecast (quantiles of log price), CRPS, PIT
evaluate.py walk-forward backtest and summary
models/ one file per model family; register new ones in __init__.py
plots.py fan chart, skill and calibration charts
```
A model is any object with a `name` and
`forecast(history, horizons) -> Forecast`. `history` is a date-indexed frame
holding everything up to the forecast origin and nothing after it. Other data
sources (hash rate, on-chain metrics, macro series) can join it as extra
columns. Anything published with a lag must be shifted to the date it was
actually available, or the backtest will quietly cheat.
Price data: [Investing.com](https://www.investing.com/crypto/bitcoin/historical-data)
through 2024-11-26, then Coinbase Exchange daily closes (UTC).
View File
+100
View File
@@ -0,0 +1,100 @@
"""
Command line entry point.
python -m btcmodel update fetch new daily prices from Coinbase
python -m btcmodel backtest score models on development data
python -m btcmodel backtest --holdout score models on outcomes after DEV_CUTOFF
python -m btcmodel forecast forecast from the latest price
"""
import argparse
from pathlib import Path
import numpy as np
import pandas as pd
from . import data, evaluate, plots
from .models import MODELS
FORECAST_REPORT_HORIZONS = (182, 365, 730, 1095, 1460)
FORECAST_REPORT_LEVELS = (0.05, 0.25, 0.5, 0.75, 0.95)
def main() -> None:
parser = argparse.ArgumentParser(prog="btcmodel", description="Bitcoin price model")
parser.add_argument("-o", "--output", type=Path, default=Path("output"))
parser.add_argument("-m", "--models", nargs="+", choices=list(MODELS), default=list(MODELS))
commands = parser.add_subparsers(dest="command", required=True)
commands.add_parser("update", help="fetch new daily prices")
backtest = commands.add_parser("backtest", help="walk-forward evaluation")
backtest.add_argument(
"--holdout",
action="store_true",
help=f"score outcomes after {data.DEV_CUTOFF:%Y-%m-%d} (don't use while developing)",
)
commands.add_parser("forecast", help="forecast from the latest price")
args = parser.parse_args()
models = [MODELS[name] for name in args.models]
if args.command == "update":
added = data.update_coinbase()
print(f"added {added} days; latest {data.load_prices().index[-1]:%Y-%m-%d}")
elif args.command == "backtest":
run_backtest(models, args.output, args.holdout)
elif args.command == "forecast":
run_forecast(models, args.output)
def run_backtest(models, output: Path, holdout: bool) -> None:
if holdout:
name, prices = "holdout", data.load_prices()
scores = evaluate.backtest(models, prices, score_after=data.DEV_CUTOFF)
else:
name, prices = "backtest", data.load_prices(until=data.DEV_CUTOFF)
scores = evaluate.backtest(models, prices)
out = output / name
out.mkdir(parents=True, exist_ok=True)
summary = evaluate.summarize(scores)
report = (
f"{name}: origins every {evaluate.ORIGIN_STEP_DAYS} days from "
f"{evaluate.FIRST_ORIGIN:%Y-%m-%d}, outcomes through {prices.index[-1]:%Y-%m-%d}\n\n"
+ evaluate.format_summary(summary)
)
print(report)
(out / "report.txt").write_text(report + "\n")
scores.to_csv(out / "scores.csv", index=False)
summary.to_csv(out / "summary.csv", index=False)
plots.skill_chart(summary, out / "skill.png", f"{name}: skill by horizon")
plots.calibration_chart(summary, out / "calibration.png", f"{name}: interval coverage")
print(f"\nwrote {out}/")
def run_forecast(models, output: Path) -> None:
prices = data.load_prices()
horizons = np.arange(1, max(FORECAST_REPORT_HORIZONS) + 1)
forecasts = {m.name: m.forecast(prices, horizons) for m in models}
out = output / "forecast"
out.mkdir(parents=True, exist_ok=True)
rows = []
for name, f in forecasts.items():
for h in FORECAST_REPORT_HORIZONS:
row = {"model": name, "date": f.dates[h - 1].date(), "horizon": h}
for level in FORECAST_REPORT_LEVELS:
row[f"p{level * 100:02.0f}"] = np.exp(f.quantile(level)[h - 1])
rows.append(row)
table = pd.DataFrame(rows)
table.to_csv(out / "forecast.csv", index=False)
shown = table.copy()
for column in shown.columns[3:]:
shown[column] = shown[column].map(plots.price_formatter)
print(f"from {prices.index[-1]:%Y-%m-%d} at {plots.price_formatter(prices.close.iloc[-1])}\n")
print(shown.to_string(index=False))
plots.fan_chart(prices, forecasts, out / "fan.png")
print(f"\nwrote {out}/")
if __name__ == "__main__":
main()
+117
View File
@@ -0,0 +1,117 @@
"""Daily BTC-USD closing prices.
Two sources, stitched at ARCHIVE_END:
- data/investing.csv: the original Investing.com download (2010-07-18 onward).
Its last row (2024-11-27) was an intraday snapshot, so it is cut a day early.
- data/coinbase.csv: Coinbase Exchange daily candles (UTC days), appended by
`python -m btcmodel update`.
Everything up to ARCHIVE_END is development data. Everything after it is the
holdout: outcomes nobody had seen while the 2024 model was being built, and which
model development here must not look at (see evaluate.py).
"""
import datetime as dt
import json
import urllib.request
from pathlib import Path
import numpy as np
import pandas as pd
DATA_DIR = Path(__file__).resolve().parent.parent / "data"
ARCHIVE_CSV = DATA_DIR / "investing.csv"
COINBASE_CSV = DATA_DIR / "coinbase.csv"
ARCHIVE_END = pd.Timestamp("2024-11-26")
DEV_CUTOFF = ARCHIVE_END
# 2010 has four distinct prices and no change on 89% of days; it is noise.
DATA_START = pd.Timestamp("2011-01-01")
COINBASE_URL = "https://api.exchange.coinbase.com/products/BTC-USD/candles"
COINBASE_MAX_CANDLES = 300
def load_prices(
until: pd.Timestamp | str | None = None, start: pd.Timestamp | str = DATA_START
) -> pd.DataFrame:
"""
Load daily closes as a frame indexed by date, with a single `close` column.
Models receive a prefix of this frame, so additional data sources can be
joined in as extra columns later without changing the model interface.
"""
archive = _read_archive()
parts = [archive[archive.index <= ARCHIVE_END]]
if COINBASE_CSV.exists():
coinbase = pd.read_csv(COINBASE_CSV, index_col="date", parse_dates=["date"])
parts.append(coinbase.loc[coinbase.index > ARCHIVE_END, ["close"]])
df = pd.concat(parts).sort_index()
df = df[df.index >= pd.Timestamp(start)]
if until is not None:
df = df[df.index <= pd.Timestamp(until)]
expected = pd.date_range(df.index[0], df.index[-1], freq="D")
missing = expected.difference(df.index)
if len(missing):
raise ValueError(f"{len(missing)} missing days, first {missing[0].date()}")
if not (df["close"] > 0).all():
raise ValueError("non-positive closing price")
return df
def log_returns(df: pd.DataFrame) -> pd.Series:
"""Daily log returns of the close, without the leading NaN."""
return np.log(df["close"]).diff().iloc[1:]
def _read_archive() -> pd.DataFrame:
raw = pd.read_csv(ARCHIVE_CSV, encoding="utf-8-sig", thousands=",")
dates = pd.to_datetime(raw["Date"], format="%m/%d/%Y")
return pd.DataFrame({"close": raw["Price"].astype(float).values}, index=dates.rename("date"))
def update_coinbase() -> int:
"""Append completed daily candles since the last stored day. Returns rows added."""
if COINBASE_CSV.exists():
existing = pd.read_csv(COINBASE_CSV, index_col="date", parse_dates=["date"])
first = existing.index.max() + pd.Timedelta(days=1)
else:
existing = None
first = ARCHIVE_END + pd.Timedelta(days=1)
# Today's candle is still forming; only take finished UTC days.
last = pd.Timestamp(dt.datetime.now(dt.UTC).date()) - pd.Timedelta(days=1)
rows = []
chunk_start = first
while chunk_start <= last:
chunk_end = min(chunk_start + pd.Timedelta(days=COINBASE_MAX_CANDLES - 1), last)
rows.extend(_fetch_candles(chunk_start, chunk_end))
chunk_start = chunk_end + pd.Timedelta(days=1)
if not rows:
return 0
new = pd.DataFrame(rows, columns=["date", "close"]).set_index("date").sort_index()
new = new[(new.index >= first) & (new.index <= last)]
combined = new if existing is None else pd.concat([existing, new])
combined = combined[~combined.index.duplicated(keep="last")].sort_index()
combined.to_csv(COINBASE_CSV, date_format="%Y-%m-%d")
return len(new)
def _fetch_candles(start: pd.Timestamp, end: pd.Timestamp) -> list[tuple[pd.Timestamp, float]]:
url = (
f"{COINBASE_URL}?granularity=86400"
f"&start={start:%Y-%m-%d}T00:00:00Z&end={end:%Y-%m-%d}T00:00:00Z"
)
# Coinbase rejects requests without a User-Agent.
request = urllib.request.Request(url, headers={"User-Agent": "btcmodel"})
with urllib.request.urlopen(request, timeout=30) as response:
candles = json.load(response)
# Each candle is [time, low, high, open, close, volume].
return [
(pd.Timestamp(dt.datetime.fromtimestamp(c[0], dt.UTC).date()), float(c[4])) for c in candles
]
+123
View File
@@ -0,0 +1,123 @@
"""
Walk-forward evaluation.
From each origin (every ORIGIN_STEP_DAYS from FIRST_ORIGIN), each model sees the
data up to that day only and forecasts every horizon. Each forecast whose target
date has been observed is scored against what happened.
Development runs load data only up to DEV_CUTOFF, so outcomes after it cannot
influence model design. The holdout run scores only targets after DEV_CUTOFF.
"""
import numpy as np
import pandas as pd
from .forecast import crps, pit
from .models import BASELINE
HORIZONS = np.array([30, 91, 182, 365, 730, 1095, 1460])
FIRST_ORIGIN = pd.Timestamp("2014-01-01")
ORIGIN_STEP_DAYS = 30
COVERAGES = (0.5, 0.8, 0.95)
def backtest(
models, data: pd.DataFrame, horizons=HORIZONS, score_after: pd.Timestamp | None = None
) -> pd.DataFrame:
"""One row per (model, origin, horizon) with an observed outcome."""
log_close = np.log(data["close"])
last = data.index[-1]
rows = []
for origin in pd.date_range(FIRST_ORIGIN, last, freq=f"{ORIGIN_STEP_DAYS}D"):
targets = origin + pd.to_timedelta(horizons, unit="D")
scored = targets <= last
if score_after is not None:
scored &= targets > score_after
if not scored.any():
continue
history = data.loc[:origin]
outcome = log_close.loc[targets[scored]].to_numpy()
for model in models:
f = model.forecast(history, horizons[scored])
row = {
"model": model.name,
"origin": origin,
"horizon": f.horizons,
"outcome": outcome,
"median": f.quantile(0.5),
"crps": crps(f.log_quantiles, outcome),
"pit": pit(f.log_quantiles, outcome),
}
for c in COVERAGES:
lo, hi = f.interval(c)
row[f"in{c:.0%}"] = (lo <= outcome) & (outcome <= hi)
rows.append(pd.DataFrame(row))
return pd.concat(rows, ignore_index=True)
def summarize(scores: pd.DataFrame, n_boot: int = 2000, seed: int = 0) -> pd.DataFrame:
"""
Per model and horizon: mean CRPS, skill relative to the baseline, and coverage.
Skill is 1 - CRPS / baseline CRPS (positive = better than the baseline),
with a 90% moving-block bootstrap interval over origins. Forecasts from
nearby origins overlap heavily, so `windows` (the span covered divided by
the horizon) is the honest count of independent outcomes. Treat intervals
with fewer than ~5 windows as optimistic.
"""
rng = np.random.default_rng(seed)
rows = []
for horizon, at_h in scores.groupby("horizon"):
base = at_h[at_h.model == BASELINE].set_index("origin")["crps"].sort_index()
span = (base.index[-1] - base.index[0]).days + horizon
block = max(1, min(int(np.ceil(horizon / ORIGIN_STEP_DAYS)), len(base) // 2))
boot_index = _block_bootstrap_indices(len(base), block, n_boot, rng)
for model, g in at_h.groupby("model", sort=False):
m = g.set_index("origin")["crps"].reindex(base.index).to_numpy()
boot = 1 - m[boot_index].mean(axis=1) / base.to_numpy()[boot_index].mean(axis=1)
rows.append(
{
"model": model,
"horizon": horizon,
"forecasts": len(g),
"windows": span / horizon,
"crps": g["crps"].mean(),
"skill": 1 - m.mean() / base.mean(),
"skill_lo": np.quantile(boot, 0.05),
"skill_hi": np.quantile(boot, 0.95),
**{f"cov{c:.0%}": g[f"in{c:.0%}"].mean() for c in COVERAGES},
"mean_pit": g["pit"].mean(),
}
)
return pd.DataFrame(rows)
def _block_bootstrap_indices(n, block, n_boot, rng) -> np.ndarray:
"""Circular block bootstrap, so the first and last origins aren't under-sampled."""
n_blocks = int(np.ceil(n / block))
starts = rng.integers(0, n, size=(n_boot, n_blocks))
return ((starts[:, :, None] + np.arange(block)) % n).reshape(n_boot, -1)[:, :n]
def format_summary(summary: pd.DataFrame) -> str:
table = pd.DataFrame(
{
"model": summary["model"],
"horizon": summary["horizon"].map(horizon_label),
"windows": summary["windows"].map("{:.1f}".format),
"crps": summary["crps"].map("{:.3f}".format),
"skill vs rw [90%]": [
f"{s:+.0%} [{lo:+.0%}, {hi:+.0%}]"
for s, lo, hi in zip(summary.skill, summary.skill_lo, summary.skill_hi, strict=True)
],
**{f"in {c:.0%}": summary[f"cov{c:.0%}"].map("{:.0%}".format) for c in COVERAGES},
"mean pit": summary["mean_pit"].map("{:.2f}".format),
}
)
return table.to_string(index=False)
def horizon_label(days: int) -> str:
if days < 365:
return f"{round(days / 30.4)}mo"
return f"{round(days / 365)}y"
+76
View File
@@ -0,0 +1,76 @@
"""
The one output format every model produces, and how it is scored.
A forecast is a set of quantiles of the natural-log price at each horizon.
Quantiles work for any model (closed-form, simulated, bootstrapped, mixtures)
and make scoring simple. Working in log price makes errors relative: a CRPS of
0.1 is roughly "typically 10% off", in 2013 or in 2026.
"""
from dataclasses import dataclass
import numpy as np
import pandas as pd
from scipy.stats import norm
N_LEVELS = 100
# Midpoints of 100 equal-probability bins: 0.005, 0.015, ..., 0.995.
LEVELS = (np.arange(N_LEVELS) + 0.5) / N_LEVELS
@dataclass(frozen=True)
class Forecast:
origin: pd.Timestamp
horizons: np.ndarray # days after origin, shape (H,)
log_quantiles: np.ndarray # log price at LEVELS, shape (H, N_LEVELS), rows nondecreasing
@classmethod
def normal(cls, origin, horizons, mean, sd) -> "Forecast":
"""Normal distribution in log price (i.e. lognormal price) at each horizon."""
mean = np.broadcast_to(np.asarray(mean, dtype=float), np.shape(horizons))
sd = np.broadcast_to(np.asarray(sd, dtype=float), np.shape(horizons))
q = mean[:, None] + sd[:, None] * norm.ppf(LEVELS)[None, :]
return cls(pd.Timestamp(origin), np.asarray(horizons), q)
@classmethod
def from_samples(cls, origin, horizons, samples) -> "Forecast":
"""Empirical quantiles of simulated log prices, shape (n_samples, H)."""
q = np.quantile(np.asarray(samples), LEVELS, axis=0).T
return cls(pd.Timestamp(origin), np.asarray(horizons), q)
@property
def dates(self) -> pd.DatetimeIndex:
return self.origin + pd.to_timedelta(self.horizons, unit="D")
def quantile(self, level: float) -> np.ndarray:
"""Log price at an arbitrary level, interpolated between grid levels."""
return np.array([np.interp(level, LEVELS, row) for row in self.log_quantiles])
def interval(self, coverage: float) -> tuple[np.ndarray, np.ndarray]:
"""Central interval in log price holding `coverage` probability."""
tail = (1 - coverage) / 2
return self.quantile(tail), self.quantile(1 - tail)
def crps(log_quantiles: np.ndarray, outcome: np.ndarray) -> np.ndarray:
"""
Continuous ranked probability score, from quantiles, in log-price units.
CRPS is twice the pinball loss integrated over all quantile levels; the
quantile grid gives the integral directly. It rewards sharpness and
calibration together and has no free parameters to game. Lower is better.
"""
outcome = np.asarray(outcome, dtype=float)
u = outcome[..., None] - log_quantiles
pinball = u * (LEVELS - (u < 0))
return 2 * pinball.mean(axis=-1)
def pit(log_quantiles: np.ndarray, outcome: np.ndarray) -> np.ndarray:
"""Probability integral transform: forecast CDF evaluated at the outcome."""
return np.array(
[
np.interp(y, q, LEVELS, left=0.0, right=1.0)
for q, y in zip(np.atleast_2d(log_quantiles), np.atleast_1d(outcome), strict=True)
]
)
+34
View File
@@ -0,0 +1,34 @@
"""Halving calendar and position within the halving cycle."""
import numpy as np
import pandas as pd
GENESIS = pd.Timestamp("2009-01-03")
# Block heights 210k, 420k, 630k, 840k (UTC dates).
HALVINGS = pd.DatetimeIndex(["2012-11-28", "2016-07-09", "2020-05-11", "2024-04-20"])
# Later halvings are projected at the length of the last cycle. Block times drift
# by weeks per cycle, which is noise at the resolution this is used.
_LAST_CYCLE = HALVINGS[-1] - HALVINGS[-2]
_PROJECTED = pd.DatetimeIndex([HALVINGS[-1] + k * _LAST_CYCLE for k in range(1, 6)])
# Genesis starts cycle 0.
CYCLE_STARTS = pd.DatetimeIndex([GENESIS]).append(HALVINGS).append(_PROJECTED)
def cycle_position(dates) -> tuple[np.ndarray, np.ndarray]:
"""
For each date, return (cycle index, days since that cycle began).
Cycle 0 runs from genesis to the first halving; a halving day is day 0 of
the cycle it starts.
"""
dates = pd.DatetimeIndex(dates)
if (dates < GENESIS).any():
raise ValueError("date before genesis")
if (dates >= CYCLE_STARTS[-1]).any():
raise ValueError("date beyond projected halvings")
index = CYCLE_STARTS.searchsorted(dates, side="right") - 1
days = (dates - CYCLE_STARTS[index]).days
return np.asarray(index), np.asarray(days)
+15
View File
@@ -0,0 +1,15 @@
"""
Candidate models.
A model is any object with a `name` and a
`forecast(history: pd.DataFrame, horizons: np.ndarray) -> Forecast` method.
`history` holds every row up to and including the forecast origin and nothing
after it; the harness guarantees that, so models can use all of it freely.
"""
from .baselines import DriftRandomWalk, RandomWalk
from .cycle import CycleModel
# Order is fixed: it sets each model's colour in every chart.
MODELS = {m.name: m for m in (RandomWalk(), DriftRandomWalk(), CycleModel())}
BASELINE = RandomWalk.name
+47
View File
@@ -0,0 +1,47 @@
"""Reference forecasts every other model has to beat."""
from dataclasses import dataclass
from typing import ClassVar
import numpy as np
import pandas as pd
from ..data import log_returns
from ..forecast import Forecast
@dataclass(frozen=True)
class RandomWalk:
"""
Zero-drift random walk in log price: "it stays about here, give or take".
Volatility is the trailing standard deviation of daily log returns.
"""
name: ClassVar[str] = "random_walk"
vol_window: int = 365
def forecast(self, history: pd.DataFrame, horizons: np.ndarray) -> Forecast:
sigma = log_returns(history).iloc[-self.vol_window :].std()
mean = np.log(history["close"].iloc[-1])
return Forecast.normal(history.index[-1], horizons, mean, sigma * np.sqrt(horizons))
@dataclass(frozen=True)
class DriftRandomWalk:
"""
Random walk whose drift is the mean daily log return over the trailing
`drift_window` days (one halving cycle by default): "it keeps doing what it
did last cycle".
"""
name: ClassVar[str] = "drift_rw"
drift_window: int = 1460
vol_window: int = 365
def forecast(self, history: pd.DataFrame, horizons: np.ndarray) -> Forecast:
returns = log_returns(history)
mu = returns.iloc[-self.drift_window :].mean()
sigma = returns.iloc[-self.vol_window :].std()
mean = np.log(history["close"].iloc[-1]) + mu * horizons
return Forecast.normal(history.index[-1], horizons, mean, sigma * np.sqrt(horizons))
+73
View File
@@ -0,0 +1,73 @@
"""The 2024 model, distilled."""
from dataclasses import dataclass
from typing import ClassVar
import numpy as np
import pandas as pd
from ..data import log_returns
from ..forecast import Forecast
from ..halving import cycle_position
# Longer than any cycle so far (the longest, cycle 0, is 1425 days).
MAX_CYCLE_DAYS = 1500
@dataclass(frozen=True)
class CycleModel:
"""
Expected return depends on how many days it has been since the last halving.
The drift for day d of the cycle is a weighted mean of the daily log returns
observed around day d of every past cycle. Of the 2024 model's ~2000 lines,
this idea did all the work. It differs from that model in two ways:
- Neighbouring cycle days are pooled with a Gaussian kernel. The 2024 model
averaged each day separately and then took a rolling mean.
- Past cycles are down-weighted, halving each `recency_half_life` cycles, so
the 10-100x cycles of 2011-2017 don't set the level. The 2024 model
averaged all cycles equally, then scaled by ~0.7; it overshot the
2025 peak by ~60%. (This is the idea on the old `tuning-b` branch.)
Where the data is thin, the drift shrinks toward the overall weighted mean,
as if `prior_days` extra observations sat at that value. Noise is a
constant-volatility random walk, so the distribution is closed-form.
"""
name: ClassVar[str] = "cycle"
bandwidth_days: float = 30.0
recency_half_life: float = 1.0
prior_days: float = 10.0
vol_window: int = 365
def drift_by_cycle_day(self, history: pd.DataFrame) -> np.ndarray:
"""Expected daily log return for each day of the cycle, shape (MAX_CYCLE_DAYS,)."""
returns = log_returns(history)
cycle, day = cycle_position(returns.index)
current_cycle = cycle_position(history.index[-1:])[0][0]
weight = 0.5 ** ((current_cycle - cycle) / self.recency_half_life)
sum_wr = np.bincount(day, weights=weight * returns.values, minlength=MAX_CYCLE_DAYS)
sum_w = np.bincount(day, weights=weight, minlength=MAX_CYCLE_DAYS)
# Peak-1 kernel, so smoothed weights count (recency-weighted) days of data.
half_width = int(np.ceil(4 * self.bandwidth_days))
offsets = np.arange(-half_width, half_width + 1)
kernel = np.exp(-0.5 * (offsets / self.bandwidth_days) ** 2)
smooth_wr = np.convolve(sum_wr, kernel, mode="same")
smooth_w = np.convolve(sum_w, kernel, mode="same")
overall = sum_wr.sum() / sum_w.sum()
return (smooth_wr + self.prior_days * overall) / (smooth_w + self.prior_days)
def forecast(self, history: pd.DataFrame, horizons: np.ndarray) -> Forecast:
drift = self.drift_by_cycle_day(history)
origin = history.index[-1]
future = origin + pd.to_timedelta(np.arange(1, horizons.max() + 1), unit="D")
_, future_day = cycle_position(future)
cumulative = np.cumsum(drift[future_day])
sigma = log_returns(history).iloc[-self.vol_window :].std()
mean = np.log(history["close"].iloc[-1]) + cumulative[horizons - 1]
return Forecast.normal(origin, horizons, mean, sigma * np.sqrt(horizons))
+172
View File
@@ -0,0 +1,172 @@
"""Charts. One colour per model, fixed by its position in MODELS."""
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt # noqa: E402
import numpy as np # noqa: E402
import pandas as pd # noqa: E402
from matplotlib.ticker import FuncFormatter, LogLocator, PercentFormatter # noqa: E402
from .evaluate import COVERAGES, horizon_label # noqa: E402
from .forecast import Forecast # noqa: E402
from .halving import CYCLE_STARTS # noqa: E402
from .models import BASELINE, MODELS # noqa: E402
SURFACE = "#fcfcfb"
INK = "#0b0b0b"
INK_SECONDARY = "#52514e"
MUTED = "#898781"
GRID = "#e1e0d9"
AXIS = "#c3c2b7"
SERIES = ["#2a78d6", "#eb6834", "#1baf7a", "#eda100", "#e87ba4", "#008300", "#4a3aa7", "#e34948"]
plt.rcParams.update(
{
"figure.facecolor": SURFACE,
"axes.facecolor": SURFACE,
"savefig.facecolor": SURFACE,
"axes.edgecolor": AXIS,
"axes.linewidth": 0.8,
"axes.labelcolor": INK_SECONDARY,
"axes.titlecolor": INK,
"axes.titlesize": 11,
"axes.titleweight": "semibold",
"axes.titlelocation": "left",
"axes.spines.top": False,
"axes.spines.right": False,
"axes.grid": True,
"grid.color": GRID,
"grid.linewidth": 0.8,
"xtick.color": MUTED,
"ytick.color": MUTED,
"xtick.labelcolor": INK_SECONDARY,
"ytick.labelcolor": INK_SECONDARY,
"legend.frameon": False,
"legend.labelcolor": INK_SECONDARY,
"lines.linewidth": 2,
"lines.solid_capstyle": "round",
"lines.solid_joinstyle": "round",
"font.family": "sans-serif",
"font.size": 9,
}
)
def model_color(name: str) -> str:
return SERIES[list(MODELS).index(name) % len(SERIES)]
def price_formatter(x, _=None) -> str:
for scale, suffix in ((1e9, "B"), (1e6, "M"), (1e3, "K")):
if x >= scale:
return f"${x / scale:.3g}{suffix}"
return f"${x:.3g}"
def fan_chart(history: pd.DataFrame, forecasts: dict[str, Forecast], path: Path) -> None:
"""History plus 50/80/95% intervals, one panel per model on shared axes."""
fig, axes = plt.subplots(
len(forecasts), 1, figsize=(10, 3.2 * len(forecasts)), sharex=True, sharey=True
)
axes = np.atleast_1d(axes)
shown = history[history.index >= history.index[-1] - pd.Timedelta(days=6 * 365)]
for ax, (name, f) in zip(axes, forecasts.items(), strict=True):
color = model_color(name)
for c in sorted(COVERAGES, reverse=True):
lo, hi = f.interval(c)
ax.fill_between(f.dates, np.exp(lo), np.exp(hi), color=color, alpha=0.1, lw=0)
median = np.exp(f.quantile(0.5))
ax.plot(shown.index, shown["close"], color=INK, lw=1.2)
ax.plot(f.dates, median, color=color)
ax.annotate(
f"median {price_formatter(median[-1])}",
(f.dates[-1], median[-1]),
xytext=(6, 0),
textcoords="offset points",
va="center",
color=INK_SECONDARY,
)
for start in CYCLE_STARTS:
if shown.index[0] <= start <= f.dates[-1]:
ax.axvline(start, color=AXIS, lw=0.8, zorder=0)
ax.set_yscale("log")
ax.yaxis.set_major_locator(LogLocator(base=10, subs=(1, 2, 5)))
ax.yaxis.set_major_formatter(FuncFormatter(price_formatter))
ax.yaxis.set_minor_formatter(FuncFormatter(lambda *_: ""))
ax.set_title(f"{name}: forecast from {f.origin:%Y-%m-%d}")
axes[0].fill_between([], [], color=MUTED, alpha=0.3, label="50% interval")
axes[0].fill_between([], [], color=MUTED, alpha=0.2, label="80% interval")
axes[0].fill_between([], [], color=MUTED, alpha=0.1, label="95% interval")
axes[0].legend(loc="upper left")
axes[-1].set_xlabel("vertical lines: halvings (future ones projected)", color=MUTED)
fig.tight_layout()
fig.savefig(path, dpi=150)
plt.close(fig)
def skill_chart(summary: pd.DataFrame, path: Path, title: str) -> None:
"""CRPS skill vs the random walk, by horizon, with bootstrap intervals."""
horizons = sorted(summary["horizon"].unique())
x = np.arange(len(horizons))
fig, ax = plt.subplots(figsize=(8, 4.5))
ax.axhline(0, color=model_color(BASELINE), lw=2, label=BASELINE)
for name, g in summary[summary.model != BASELINE].groupby("model", sort=False):
g = g.set_index("horizon").reindex(horizons)
color = model_color(name)
ax.fill_between(x, g.skill_lo, g.skill_hi, color=color, alpha=0.1, lw=0)
ax.plot(x, g.skill, color=color, marker="o", ms=6, mec=SURFACE, mew=2, label=name)
ax.annotate(
name,
(x[-1], g.skill.iloc[-1]),
xytext=(8, 0),
textcoords="offset points",
va="center",
color=INK_SECONDARY,
)
ax.set_xticks(x, [horizon_label(h) for h in horizons])
ax.set_xlabel("forecast horizon")
ax.set_ylabel("CRPS skill vs random walk (higher is better)")
ax.yaxis.set_major_formatter(PercentFormatter(1.0, decimals=0))
ax.set_title(title)
ax.legend(loc="lower left")
fig.tight_layout()
fig.savefig(path, dpi=150)
plt.close(fig)
def calibration_chart(summary: pd.DataFrame, path: Path, title: str) -> None:
"""How often each nominal interval contained the outcome, by horizon."""
horizons = sorted(summary["horizon"].unique())
x = np.arange(len(horizons))
fig, axes = plt.subplots(1, len(COVERAGES), figsize=(12, 4), sharey=True)
for ax, c in zip(axes, COVERAGES, strict=True):
ax.axhline(c, color=INK_SECONDARY, lw=1)
ax.annotate(
"target", (x[-1], c), xytext=(0, 4), textcoords="offset points", ha="right", color=MUTED
)
for name, g in summary.groupby("model", sort=False):
g = g.set_index("horizon").reindex(horizons)
ax.plot(
x,
g[f"cov{c:.0%}"],
color=model_color(name),
marker="o",
ms=6,
mec=SURFACE,
mew=2,
label=name,
)
ax.set_xticks(x, [horizon_label(h) for h in horizons])
ax.set_title(f"{c:.0%} interval")
ax.set_ylim(0, 1.02)
ax.yaxis.set_major_formatter(PercentFormatter(1.0, decimals=0))
axes[0].set_ylabel("share of outcomes inside")
axes[-1].legend(loc="lower left")
fig.suptitle(title, x=0.01, ha="left", color=INK, fontsize=11, fontweight="semibold")
fig.tight_layout()
fig.savefig(path, dpi=150)
plt.close(fig)
+667
View File
@@ -0,0 +1,667 @@
date,close
2024-11-27,95951.19
2024-11-28,95665.53
2024-11-29,97490.56
2024-11-30,96465.42
2024-12-01,97263.18
2024-12-02,95862.89
2024-12-03,95924.52
2024-12-04,98746.24
2024-12-05,97044.23
2024-12-06,99891.35
2024-12-07,99929.32
2024-12-08,101174.99
2024-12-09,97324.81
2024-12-10,96660.76
2024-12-11,101202.11
2024-12-12,100030.47
2024-12-13,101428.75
2024-12-14,101399.99
2024-12-15,104447.76
2024-12-16,106099.81
2024-12-17,106136.99
2024-12-18,100150.73
2024-12-19,97372.21
2024-12-20,97765.0
2024-12-21,97230.08
2024-12-22,95087.75
2024-12-23,94764.56
2024-12-24,98594.47
2024-12-25,99346.28
2024-12-26,95669.49
2024-12-27,94171.89
2024-12-28,95130.82
2024-12-29,93563.35
2024-12-30,92620.71
2024-12-31,93354.22
2025-01-01,94383.59
2025-01-02,96903.19
2025-01-03,98136.51
2025-01-04,98209.85
2025-01-05,98345.33
2025-01-06,102279.41
2025-01-07,96941.98
2025-01-08,95036.63
2025-01-09,92547.44
2025-01-10,94701.18
2025-01-11,94565.02
2025-01-12,94509.62
2025-01-13,94506.45
2025-01-14,96534.96
2025-01-15,100510.23
2025-01-16,99981.78
2025-01-17,104107.0
2025-01-18,104435.0
2025-01-19,101211.13
2025-01-20,102145.43
2025-01-21,106159.26
2025-01-22,103667.11
2025-01-23,103926.36
2025-01-24,104850.27
2025-01-25,104733.56
2025-01-26,102563.0
2025-01-27,102062.42
2025-01-28,101290.0
2025-01-29,103747.25
2025-01-30,104742.64
2025-01-31,102411.26
2025-02-01,100623.85
2025-02-02,97676.52
2025-02-03,101460.2
2025-02-04,97795.05
2025-02-05,96638.33
2025-02-06,96564.62
2025-02-07,96537.08
2025-02-08,96476.25
2025-02-09,96475.82
2025-02-10,97444.41
2025-02-11,95774.08
2025-02-12,97862.53
2025-02-13,96625.29
2025-02-14,97509.03
2025-02-15,97596.94
2025-02-16,96119.88
2025-02-17,95781.8
2025-02-18,95607.4
2025-02-19,96632.03
2025-02-20,98347.2
2025-02-21,96157.03
2025-02-22,96582.12
2025-02-23,96265.98
2025-02-24,91510.82
2025-02-25,88583.74
2025-02-26,84111.78
2025-02-27,84625.19
2025-02-28,84297.73
2025-03-01,86018.76
2025-03-02,94265.48
2025-03-03,86161.1
2025-03-04,87249.96
2025-03-05,90603.73
2025-03-06,89921.85
2025-03-07,86756.98
2025-03-08,86206.69
2025-03-09,80699.17
2025-03-10,78544.71
2025-03-11,82914.51
2025-03-12,83659.43
2025-03-13,81073.43
2025-03-14,83980.49
2025-03-15,84351.46
2025-03-16,82562.57
2025-03-17,84011.4
2025-03-18,82698.76
2025-03-19,86877.96
2025-03-20,84183.42
2025-03-21,84061.96
2025-03-22,83852.03
2025-03-23,86092.94
2025-03-24,87523.62
2025-03-25,87427.88
2025-03-26,86926.01
2025-03-27,87217.48
2025-03-28,84381.8
2025-03-29,82616.83
2025-03-30,82379.98
2025-03-31,82534.32
2025-04-01,85170.37
2025-04-02,82490.08
2025-04-03,83174.33
2025-04-04,83860.16
2025-04-05,83498.25
2025-04-06,78370.75
2025-04-07,79140.01
2025-04-08,76251.64
2025-04-09,82594.75
2025-04-10,79552.34
2025-04-11,83380.47
2025-04-12,85271.01
2025-04-13,83733.07
2025-04-14,84590.36
2025-04-15,83629.78
2025-04-16,84028.72
2025-04-17,84961.97
2025-04-18,84466.47
2025-04-19,85074.99
2025-04-20,85177.34
2025-04-21,87522.07
2025-04-22,93489.1
2025-04-23,93740.92
2025-04-24,94021.96
2025-04-25,94708.79
2025-04-26,94666.64
2025-04-27,93780.57
2025-04-28,95042.57
2025-04-29,94271.09
2025-04-30,94182.54
2025-05-01,96524.98
2025-05-02,96929.81
2025-05-03,95861.33
2025-05-04,94272.55
2025-05-05,94733.99
2025-05-06,96839.17
2025-05-07,97058.32
2025-05-08,103253.49
2025-05-09,102986.96
2025-05-10,104821.19
2025-05-11,104137.62
2025-05-12,102800.21
2025-05-13,104131.06
2025-05-14,103545.38
2025-05-15,103786.43
2025-05-16,103500.03
2025-05-17,103161.07
2025-05-18,106473.46
2025-05-19,105616.18
2025-05-20,106883.24
2025-05-21,109699.54
2025-05-22,111722.53
2025-05-23,107329.61
2025-05-24,107794.01
2025-05-25,109048.68
2025-05-26,109464.32
2025-05-27,108978.46
2025-05-28,107819.29
2025-05-29,105572.58
2025-05-30,104033.26
2025-05-31,104645.87
2025-06-01,105697.94
2025-06-02,105904.94
2025-06-03,105447.82
2025-06-04,104753.38
2025-06-05,101570.2
2025-06-06,104397.99
2025-06-07,105619.02
2025-06-08,105784.4
2025-06-09,110301.15
2025-06-10,110300.24
2025-06-11,108669.37
2025-06-12,105721.05
2025-06-13,106118.7
2025-06-14,105465.42
2025-06-15,105599.25
2025-06-16,106853.38
2025-06-17,104590.44
2025-06-18,104915.6
2025-06-19,104671.9
2025-06-20,103317.8
2025-06-21,102160.03
2025-06-22,100996.87
2025-06-23,105419.39
2025-06-24,106141.0
2025-06-25,107400.22
2025-06-26,107029.64
2025-06-27,107113.38
2025-06-28,107352.2
2025-06-29,108386.44
2025-06-30,107173.21
2025-07-01,105711.78
2025-07-02,108888.32
2025-07-03,109628.83
2025-07-04,108028.6
2025-07-05,108246.65
2025-07-06,109217.98
2025-07-07,108269.84
2025-07-08,108958.04
2025-07-09,111282.85
2025-07-10,116031.3
2025-07-11,117582.16
2025-07-12,117472.65
2025-07-13,119130.81
2025-07-14,119863.71
2025-07-15,117781.64
2025-07-16,118681.68
2025-07-17,119273.69
2025-07-18,118023.05
2025-07-19,117914.17
2025-07-20,117312.7
2025-07-21,117428.79
2025-07-22,120026.42
2025-07-23,118810.44
2025-07-24,118397.04
2025-07-25,117633.27
2025-07-26,117969.63
2025-07-27,119465.52
2025-07-28,118070.59
2025-07-29,117933.39
2025-07-30,117830.15
2025-07-31,115761.13
2025-08-01,113248.73
2025-08-02,112542.74
2025-08-03,114215.73
2025-08-04,115051.85
2025-08-05,114112.95
2025-08-06,115028.83
2025-08-07,117515.49
2025-08-08,116683.79
2025-08-09,116492.51
2025-08-10,119309.37
2025-08-11,118701.84
2025-08-12,120113.18
2025-08-13,123365.63
2025-08-14,118389.79
2025-08-15,117436.96
2025-08-16,117455.68
2025-08-17,117488.6
2025-08-18,116286.76
2025-08-19,112856.19
2025-08-20,114276.0
2025-08-21,112480.3
2025-08-22,116908.68
2025-08-23,115383.87
2025-08-24,113478.0
2025-08-25,110127.74
2025-08-26,111788.01
2025-08-27,111253.21
2025-08-28,112574.85
2025-08-29,108378.32
2025-08-30,108827.93
2025-08-31,108247.95
2025-09-01,109240.55
2025-09-02,111247.94
2025-09-03,111756.41
2025-09-04,110720.79
2025-09-05,110670.02
2025-09-06,110212.6
2025-09-07,111129.61
2025-09-08,112072.57
2025-09-09,111549.32
2025-09-10,113983.97
2025-09-11,115540.0
2025-09-12,116106.03
2025-09-13,115968.35
2025-09-14,115314.13
2025-09-15,115381.08
2025-09-16,116832.56
2025-09-17,116484.4
2025-09-18,117117.99
2025-09-19,115690.55
2025-09-20,115752.4
2025-09-21,115282.27
2025-09-22,112736.59
2025-09-23,112017.21
2025-09-24,113348.17
2025-09-25,109035.72
2025-09-26,109697.29
2025-09-27,109681.16
2025-09-28,112197.67
2025-09-29,114365.07
2025-09-30,114067.71
2025-10-01,118659.97
2025-10-02,120621.32
2025-10-03,122318.4
2025-10-04,122458.56
2025-10-05,123520.79
2025-10-06,124720.09
2025-10-07,121393.95
2025-10-08,123343.25
2025-10-09,121714.51
2025-10-10,112980.28
2025-10-11,110768.89
2025-10-12,115067.98
2025-10-13,115274.03
2025-10-14,113068.0
2025-10-15,110804.12
2025-10-16,108198.0
2025-10-17,106463.3
2025-10-18,107208.91
2025-10-19,108676.78
2025-10-20,110568.06
2025-10-21,108362.27
2025-10-22,107585.98
2025-10-23,110116.03
2025-10-24,111042.13
2025-10-25,111666.21
2025-10-26,114548.09
2025-10-27,114087.06
2025-10-28,112906.75
2025-10-29,110032.13
2025-10-30,108308.18
2025-10-31,109555.27
2025-11-01,110052.25
2025-11-02,110536.01
2025-11-03,106557.98
2025-11-04,101468.15
2025-11-05,103869.0
2025-11-06,101290.5
2025-11-07,103284.27
2025-11-08,102285.57
2025-11-09,104694.95
2025-11-10,105979.38
2025-11-11,103009.35
2025-11-12,101639.01
2025-11-13,99614.54
2025-11-14,94503.96
2025-11-15,95544.94
2025-11-16,94183.97
2025-11-17,92100.6
2025-11-18,92914.46
2025-11-19,91461.6
2025-11-20,86536.88
2025-11-21,85068.88
2025-11-22,84684.0
2025-11-23,86808.28
2025-11-24,88266.2
2025-11-25,87325.01
2025-11-26,90468.84
2025-11-27,91316.7
2025-11-28,90902.7
2025-11-29,90828.06
2025-11-30,90369.51
2025-12-01,86282.36
2025-12-02,91308.05
2025-12-03,93453.84
2025-12-04,92093.54
2025-12-05,89345.29
2025-12-06,89267.97
2025-12-07,90402.29
2025-12-08,90653.86
2025-12-09,92690.0
2025-12-10,92035.99
2025-12-11,92542.0
2025-12-12,90278.0
2025-12-13,90255.32
2025-12-14,88171.61
2025-12-15,86417.5
2025-12-16,87854.01
2025-12-17,86209.77
2025-12-18,85460.02
2025-12-19,88092.65
2025-12-20,88324.0
2025-12-21,88639.95
2025-12-22,88556.77
2025-12-23,87429.32
2025-12-24,87609.03
2025-12-25,87171.68
2025-12-26,87296.01
2025-12-27,87808.0
2025-12-28,87877.29
2025-12-29,87110.78
2025-12-30,88397.5
2025-12-31,87497.94
2026-01-01,88738.34
2026-01-02,89957.99
2026-01-03,90598.02
2026-01-04,91494.0
2026-01-05,93870.06
2026-01-06,93722.01
2026-01-07,91279.73
2026-01-08,91026.0
2026-01-09,90515.45
2026-01-10,90385.54
2026-01-11,90872.01
2026-01-12,91188.09
2026-01-13,95384.23
2026-01-14,96955.16
2026-01-15,95587.65
2026-01-16,95504.0
2026-01-17,95109.99
2026-01-18,93633.53
2026-01-19,92559.66
2026-01-20,88310.6
2026-01-21,89363.71
2026-01-22,89463.5
2026-01-23,89474.93
2026-01-24,89082.17
2026-01-25,86561.94
2026-01-26,88250.01
2026-01-27,89116.96
2026-01-28,89162.4
2026-01-29,84513.2
2026-01-30,84110.99
2026-01-31,78648.0
2026-02-01,76895.53
2026-02-02,78666.85
2026-02-03,75661.49
2026-02-04,72998.0
2026-02-05,62791.0
2026-02-06,70527.7
2026-02-07,69244.74
2026-02-08,70279.33
2026-02-09,70101.99
2026-02-10,68803.65
2026-02-11,67024.93
2026-02-12,66208.0
2026-02-13,68812.0
2026-02-14,69786.96
2026-02-15,68792.0
2026-02-16,68858.13
2026-02-17,67466.7
2026-02-18,66419.79
2026-02-19,66972.5
2026-02-20,67984.42
2026-02-21,67955.78
2026-02-22,67617.98
2026-02-23,64625.78
2026-02-24,64068.33
2026-02-25,67992.68
2026-02-26,67490.51
2026-02-27,65859.94
2026-02-28,66967.85
2026-03-01,65769.0
2026-03-02,68832.01
2026-03-03,68335.99
2026-03-04,72683.26
2026-03-05,70887.6
2026-03-06,68108.24
2026-03-07,67259.62
2026-03-08,65970.56
2026-03-09,68432.17
2026-03-10,69960.83
2026-03-11,70208.0
2026-03-12,70531.56
2026-03-13,70944.33
2026-03-14,71232.02
2026-03-15,72830.01
2026-03-16,74886.47
2026-03-17,73934.11
2026-03-18,71245.02
2026-03-19,69918.3
2026-03-20,70497.01
2026-03-21,68912.02
2026-03-22,67844.94
2026-03-23,70874.23
2026-03-24,70533.49
2026-03-25,71301.53
2026-03-26,68769.02
2026-03-27,66353.34
2026-03-28,66320.15
2026-03-29,65956.94
2026-03-30,66737.18
2026-03-31,68221.85
2026-04-01,68112.35
2026-04-02,66894.48
2026-04-03,66959.99
2026-04-04,67291.73
2026-04-05,69005.0
2026-04-06,68853.61
2026-04-07,71910.2
2026-04-08,71085.99
2026-04-09,71798.01
2026-04-10,72997.88
2026-04-11,73086.0
2026-04-12,70755.35
2026-04-13,74446.0
2026-04-14,74179.04
2026-04-15,74836.31
2026-04-16,75163.09
2026-04-17,77098.01
2026-04-18,75736.82
2026-04-19,73823.14
2026-04-20,75864.86
2026-04-21,76348.58
2026-04-22,78208.52
2026-04-23,78281.32
2026-04-24,77461.8
2026-04-25,77650.72
2026-04-26,78673.83
2026-04-27,77364.42
2026-04-28,76317.99
2026-04-29,75752.88
2026-04-30,76305.78
2026-05-01,78234.05
2026-05-02,78682.31
2026-05-03,78558.89
2026-05-04,79852.37
2026-05-05,80907.72
2026-05-06,81438.1
2026-05-07,80005.38
2026-05-08,80188.82
2026-05-09,80655.96
2026-05-10,82199.99
2026-05-11,81727.06
2026-05-12,80484.72
2026-05-13,79288.26
2026-05-14,81079.39
2026-05-15,79058.51
2026-05-16,78105.45
2026-05-17,77407.59
2026-05-18,76943.98
2026-05-19,76767.48
2026-05-20,77475.99
2026-05-21,77547.62
2026-05-22,75443.91
2026-05-23,76650.0
2026-05-24,76975.99
2026-05-25,77249.07
2026-05-26,75826.1
2026-05-27,74315.98
2026-05-28,73512.56
2026-05-29,73366.77
2026-05-30,73770.69
2026-05-31,73575.17
2026-06-01,71314.42
2026-06-02,66658.35
2026-06-03,64040.0
2026-06-04,63806.02
2026-06-05,61032.0
2026-06-06,60850.48
2026-06-07,63302.73
2026-06-08,63062.98
2026-06-09,61685.8
2026-06-10,61449.46
2026-06-11,63563.42
2026-06-12,63538.43
2026-06-13,64429.99
2026-06-14,65706.62
2026-06-15,66276.8
2026-06-16,65616.64
2026-06-17,64446.04
2026-06-18,62879.01
2026-06-19,63475.2
2026-06-20,64222.99
2026-06-21,63235.63
2026-06-22,63950.61
2026-06-23,62644.86
2026-06-24,60983.13
2026-06-25,59703.65
2026-06-26,60002.53
2026-06-27,59934.84
2026-06-28,59474.01
2026-06-29,60162.73
2026-06-30,58523.93
2026-07-01,59961.45
2026-07-02,61484.02
2026-07-03,62520.22
2026-07-04,63086.45
2026-07-05,63580.44
2026-07-06,64001.84
2026-07-07,63323.27
2026-07-08,62237.7
2026-07-09,63175.25
2026-07-10,64128.49
2026-07-11,63773.04
2026-07-12,63740.32
2026-07-13,62264.94
2026-07-14,64988.64
2026-07-15,64716.63
2026-07-16,63770.6
2026-07-17,63894.0
2026-07-18,64796.36
2026-07-19,64681.78
2026-07-20,65213.05
2026-07-21,66516.18
2026-07-22,66086.61
2026-07-23,65051.23
2026-07-24,64083.32
2026-07-25,64295.61
2026-07-26,65341.07
2026-07-27,63694.43
2026-07-28,63847.12
2026-07-29,63896.14
2026-07-30,64721.9
2026-07-31,62825.9
2026-08-01,62764.2
2026-08-02,63499.49
2026-08-03,63466.51
2026-08-04,64050.51
2026-08-05,64603.03
2026-08-06,64267.3
2026-08-07,64891.61
2026-08-08,64908.72
2026-08-09,64848.69
2026-08-10,63911.88
2026-08-11,63531.75
2026-08-12,63411.72
2026-08-13,63425.35
2026-08-14,62975.19
2026-08-15,63018.75
2026-08-16,62836.66
2026-08-17,64484.18
2026-08-18,64681.33
2026-08-19,69300.01
2026-08-20,73011.87
2026-08-21,78325.54
2026-08-22,77054.44
2026-08-23,77729.36
2026-08-24,78981.59
2026-08-25,78526.8
2026-08-26,79026.18
2026-08-27,80275.34
2026-08-28,77839.19
2026-08-29,78233.93
2026-08-30,77665.14
2026-08-31,78562.74
2026-09-01,77398.69
2026-09-02,77307.36
2026-09-03,81263.99
2026-09-04,79675.12
2026-09-05,79831.57
2026-09-06,80339.13
2026-09-07,79091.97
2026-09-08,78447.11
2026-09-09,78283.98
2026-09-10,76536.55
2026-09-11,77208.55
2026-09-12,77262.85
2026-09-13,76799.85
2026-09-14,78175.01
2026-09-15,75584.17
2026-09-16,76144.99
2026-09-17,76348.74
2026-09-18,80875.04
2026-09-19,81233.91
2026-09-20,81159.64
2026-09-21,86594.94
2026-09-22,86198.05
2026-09-23,84378.31
1 date close
2 2024-11-27 95951.19
3 2024-11-28 95665.53
4 2024-11-29 97490.56
5 2024-11-30 96465.42
6 2024-12-01 97263.18
7 2024-12-02 95862.89
8 2024-12-03 95924.52
9 2024-12-04 98746.24
10 2024-12-05 97044.23
11 2024-12-06 99891.35
12 2024-12-07 99929.32
13 2024-12-08 101174.99
14 2024-12-09 97324.81
15 2024-12-10 96660.76
16 2024-12-11 101202.11
17 2024-12-12 100030.47
18 2024-12-13 101428.75
19 2024-12-14 101399.99
20 2024-12-15 104447.76
21 2024-12-16 106099.81
22 2024-12-17 106136.99
23 2024-12-18 100150.73
24 2024-12-19 97372.21
25 2024-12-20 97765.0
26 2024-12-21 97230.08
27 2024-12-22 95087.75
28 2024-12-23 94764.56
29 2024-12-24 98594.47
30 2024-12-25 99346.28
31 2024-12-26 95669.49
32 2024-12-27 94171.89
33 2024-12-28 95130.82
34 2024-12-29 93563.35
35 2024-12-30 92620.71
36 2024-12-31 93354.22
37 2025-01-01 94383.59
38 2025-01-02 96903.19
39 2025-01-03 98136.51
40 2025-01-04 98209.85
41 2025-01-05 98345.33
42 2025-01-06 102279.41
43 2025-01-07 96941.98
44 2025-01-08 95036.63
45 2025-01-09 92547.44
46 2025-01-10 94701.18
47 2025-01-11 94565.02
48 2025-01-12 94509.62
49 2025-01-13 94506.45
50 2025-01-14 96534.96
51 2025-01-15 100510.23
52 2025-01-16 99981.78
53 2025-01-17 104107.0
54 2025-01-18 104435.0
55 2025-01-19 101211.13
56 2025-01-20 102145.43
57 2025-01-21 106159.26
58 2025-01-22 103667.11
59 2025-01-23 103926.36
60 2025-01-24 104850.27
61 2025-01-25 104733.56
62 2025-01-26 102563.0
63 2025-01-27 102062.42
64 2025-01-28 101290.0
65 2025-01-29 103747.25
66 2025-01-30 104742.64
67 2025-01-31 102411.26
68 2025-02-01 100623.85
69 2025-02-02 97676.52
70 2025-02-03 101460.2
71 2025-02-04 97795.05
72 2025-02-05 96638.33
73 2025-02-06 96564.62
74 2025-02-07 96537.08
75 2025-02-08 96476.25
76 2025-02-09 96475.82
77 2025-02-10 97444.41
78 2025-02-11 95774.08
79 2025-02-12 97862.53
80 2025-02-13 96625.29
81 2025-02-14 97509.03
82 2025-02-15 97596.94
83 2025-02-16 96119.88
84 2025-02-17 95781.8
85 2025-02-18 95607.4
86 2025-02-19 96632.03
87 2025-02-20 98347.2
88 2025-02-21 96157.03
89 2025-02-22 96582.12
90 2025-02-23 96265.98
91 2025-02-24 91510.82
92 2025-02-25 88583.74
93 2025-02-26 84111.78
94 2025-02-27 84625.19
95 2025-02-28 84297.73
96 2025-03-01 86018.76
97 2025-03-02 94265.48
98 2025-03-03 86161.1
99 2025-03-04 87249.96
100 2025-03-05 90603.73
101 2025-03-06 89921.85
102 2025-03-07 86756.98
103 2025-03-08 86206.69
104 2025-03-09 80699.17
105 2025-03-10 78544.71
106 2025-03-11 82914.51
107 2025-03-12 83659.43
108 2025-03-13 81073.43
109 2025-03-14 83980.49
110 2025-03-15 84351.46
111 2025-03-16 82562.57
112 2025-03-17 84011.4
113 2025-03-18 82698.76
114 2025-03-19 86877.96
115 2025-03-20 84183.42
116 2025-03-21 84061.96
117 2025-03-22 83852.03
118 2025-03-23 86092.94
119 2025-03-24 87523.62
120 2025-03-25 87427.88
121 2025-03-26 86926.01
122 2025-03-27 87217.48
123 2025-03-28 84381.8
124 2025-03-29 82616.83
125 2025-03-30 82379.98
126 2025-03-31 82534.32
127 2025-04-01 85170.37
128 2025-04-02 82490.08
129 2025-04-03 83174.33
130 2025-04-04 83860.16
131 2025-04-05 83498.25
132 2025-04-06 78370.75
133 2025-04-07 79140.01
134 2025-04-08 76251.64
135 2025-04-09 82594.75
136 2025-04-10 79552.34
137 2025-04-11 83380.47
138 2025-04-12 85271.01
139 2025-04-13 83733.07
140 2025-04-14 84590.36
141 2025-04-15 83629.78
142 2025-04-16 84028.72
143 2025-04-17 84961.97
144 2025-04-18 84466.47
145 2025-04-19 85074.99
146 2025-04-20 85177.34
147 2025-04-21 87522.07
148 2025-04-22 93489.1
149 2025-04-23 93740.92
150 2025-04-24 94021.96
151 2025-04-25 94708.79
152 2025-04-26 94666.64
153 2025-04-27 93780.57
154 2025-04-28 95042.57
155 2025-04-29 94271.09
156 2025-04-30 94182.54
157 2025-05-01 96524.98
158 2025-05-02 96929.81
159 2025-05-03 95861.33
160 2025-05-04 94272.55
161 2025-05-05 94733.99
162 2025-05-06 96839.17
163 2025-05-07 97058.32
164 2025-05-08 103253.49
165 2025-05-09 102986.96
166 2025-05-10 104821.19
167 2025-05-11 104137.62
168 2025-05-12 102800.21
169 2025-05-13 104131.06
170 2025-05-14 103545.38
171 2025-05-15 103786.43
172 2025-05-16 103500.03
173 2025-05-17 103161.07
174 2025-05-18 106473.46
175 2025-05-19 105616.18
176 2025-05-20 106883.24
177 2025-05-21 109699.54
178 2025-05-22 111722.53
179 2025-05-23 107329.61
180 2025-05-24 107794.01
181 2025-05-25 109048.68
182 2025-05-26 109464.32
183 2025-05-27 108978.46
184 2025-05-28 107819.29
185 2025-05-29 105572.58
186 2025-05-30 104033.26
187 2025-05-31 104645.87
188 2025-06-01 105697.94
189 2025-06-02 105904.94
190 2025-06-03 105447.82
191 2025-06-04 104753.38
192 2025-06-05 101570.2
193 2025-06-06 104397.99
194 2025-06-07 105619.02
195 2025-06-08 105784.4
196 2025-06-09 110301.15
197 2025-06-10 110300.24
198 2025-06-11 108669.37
199 2025-06-12 105721.05
200 2025-06-13 106118.7
201 2025-06-14 105465.42
202 2025-06-15 105599.25
203 2025-06-16 106853.38
204 2025-06-17 104590.44
205 2025-06-18 104915.6
206 2025-06-19 104671.9
207 2025-06-20 103317.8
208 2025-06-21 102160.03
209 2025-06-22 100996.87
210 2025-06-23 105419.39
211 2025-06-24 106141.0
212 2025-06-25 107400.22
213 2025-06-26 107029.64
214 2025-06-27 107113.38
215 2025-06-28 107352.2
216 2025-06-29 108386.44
217 2025-06-30 107173.21
218 2025-07-01 105711.78
219 2025-07-02 108888.32
220 2025-07-03 109628.83
221 2025-07-04 108028.6
222 2025-07-05 108246.65
223 2025-07-06 109217.98
224 2025-07-07 108269.84
225 2025-07-08 108958.04
226 2025-07-09 111282.85
227 2025-07-10 116031.3
228 2025-07-11 117582.16
229 2025-07-12 117472.65
230 2025-07-13 119130.81
231 2025-07-14 119863.71
232 2025-07-15 117781.64
233 2025-07-16 118681.68
234 2025-07-17 119273.69
235 2025-07-18 118023.05
236 2025-07-19 117914.17
237 2025-07-20 117312.7
238 2025-07-21 117428.79
239 2025-07-22 120026.42
240 2025-07-23 118810.44
241 2025-07-24 118397.04
242 2025-07-25 117633.27
243 2025-07-26 117969.63
244 2025-07-27 119465.52
245 2025-07-28 118070.59
246 2025-07-29 117933.39
247 2025-07-30 117830.15
248 2025-07-31 115761.13
249 2025-08-01 113248.73
250 2025-08-02 112542.74
251 2025-08-03 114215.73
252 2025-08-04 115051.85
253 2025-08-05 114112.95
254 2025-08-06 115028.83
255 2025-08-07 117515.49
256 2025-08-08 116683.79
257 2025-08-09 116492.51
258 2025-08-10 119309.37
259 2025-08-11 118701.84
260 2025-08-12 120113.18
261 2025-08-13 123365.63
262 2025-08-14 118389.79
263 2025-08-15 117436.96
264 2025-08-16 117455.68
265 2025-08-17 117488.6
266 2025-08-18 116286.76
267 2025-08-19 112856.19
268 2025-08-20 114276.0
269 2025-08-21 112480.3
270 2025-08-22 116908.68
271 2025-08-23 115383.87
272 2025-08-24 113478.0
273 2025-08-25 110127.74
274 2025-08-26 111788.01
275 2025-08-27 111253.21
276 2025-08-28 112574.85
277 2025-08-29 108378.32
278 2025-08-30 108827.93
279 2025-08-31 108247.95
280 2025-09-01 109240.55
281 2025-09-02 111247.94
282 2025-09-03 111756.41
283 2025-09-04 110720.79
284 2025-09-05 110670.02
285 2025-09-06 110212.6
286 2025-09-07 111129.61
287 2025-09-08 112072.57
288 2025-09-09 111549.32
289 2025-09-10 113983.97
290 2025-09-11 115540.0
291 2025-09-12 116106.03
292 2025-09-13 115968.35
293 2025-09-14 115314.13
294 2025-09-15 115381.08
295 2025-09-16 116832.56
296 2025-09-17 116484.4
297 2025-09-18 117117.99
298 2025-09-19 115690.55
299 2025-09-20 115752.4
300 2025-09-21 115282.27
301 2025-09-22 112736.59
302 2025-09-23 112017.21
303 2025-09-24 113348.17
304 2025-09-25 109035.72
305 2025-09-26 109697.29
306 2025-09-27 109681.16
307 2025-09-28 112197.67
308 2025-09-29 114365.07
309 2025-09-30 114067.71
310 2025-10-01 118659.97
311 2025-10-02 120621.32
312 2025-10-03 122318.4
313 2025-10-04 122458.56
314 2025-10-05 123520.79
315 2025-10-06 124720.09
316 2025-10-07 121393.95
317 2025-10-08 123343.25
318 2025-10-09 121714.51
319 2025-10-10 112980.28
320 2025-10-11 110768.89
321 2025-10-12 115067.98
322 2025-10-13 115274.03
323 2025-10-14 113068.0
324 2025-10-15 110804.12
325 2025-10-16 108198.0
326 2025-10-17 106463.3
327 2025-10-18 107208.91
328 2025-10-19 108676.78
329 2025-10-20 110568.06
330 2025-10-21 108362.27
331 2025-10-22 107585.98
332 2025-10-23 110116.03
333 2025-10-24 111042.13
334 2025-10-25 111666.21
335 2025-10-26 114548.09
336 2025-10-27 114087.06
337 2025-10-28 112906.75
338 2025-10-29 110032.13
339 2025-10-30 108308.18
340 2025-10-31 109555.27
341 2025-11-01 110052.25
342 2025-11-02 110536.01
343 2025-11-03 106557.98
344 2025-11-04 101468.15
345 2025-11-05 103869.0
346 2025-11-06 101290.5
347 2025-11-07 103284.27
348 2025-11-08 102285.57
349 2025-11-09 104694.95
350 2025-11-10 105979.38
351 2025-11-11 103009.35
352 2025-11-12 101639.01
353 2025-11-13 99614.54
354 2025-11-14 94503.96
355 2025-11-15 95544.94
356 2025-11-16 94183.97
357 2025-11-17 92100.6
358 2025-11-18 92914.46
359 2025-11-19 91461.6
360 2025-11-20 86536.88
361 2025-11-21 85068.88
362 2025-11-22 84684.0
363 2025-11-23 86808.28
364 2025-11-24 88266.2
365 2025-11-25 87325.01
366 2025-11-26 90468.84
367 2025-11-27 91316.7
368 2025-11-28 90902.7
369 2025-11-29 90828.06
370 2025-11-30 90369.51
371 2025-12-01 86282.36
372 2025-12-02 91308.05
373 2025-12-03 93453.84
374 2025-12-04 92093.54
375 2025-12-05 89345.29
376 2025-12-06 89267.97
377 2025-12-07 90402.29
378 2025-12-08 90653.86
379 2025-12-09 92690.0
380 2025-12-10 92035.99
381 2025-12-11 92542.0
382 2025-12-12 90278.0
383 2025-12-13 90255.32
384 2025-12-14 88171.61
385 2025-12-15 86417.5
386 2025-12-16 87854.01
387 2025-12-17 86209.77
388 2025-12-18 85460.02
389 2025-12-19 88092.65
390 2025-12-20 88324.0
391 2025-12-21 88639.95
392 2025-12-22 88556.77
393 2025-12-23 87429.32
394 2025-12-24 87609.03
395 2025-12-25 87171.68
396 2025-12-26 87296.01
397 2025-12-27 87808.0
398 2025-12-28 87877.29
399 2025-12-29 87110.78
400 2025-12-30 88397.5
401 2025-12-31 87497.94
402 2026-01-01 88738.34
403 2026-01-02 89957.99
404 2026-01-03 90598.02
405 2026-01-04 91494.0
406 2026-01-05 93870.06
407 2026-01-06 93722.01
408 2026-01-07 91279.73
409 2026-01-08 91026.0
410 2026-01-09 90515.45
411 2026-01-10 90385.54
412 2026-01-11 90872.01
413 2026-01-12 91188.09
414 2026-01-13 95384.23
415 2026-01-14 96955.16
416 2026-01-15 95587.65
417 2026-01-16 95504.0
418 2026-01-17 95109.99
419 2026-01-18 93633.53
420 2026-01-19 92559.66
421 2026-01-20 88310.6
422 2026-01-21 89363.71
423 2026-01-22 89463.5
424 2026-01-23 89474.93
425 2026-01-24 89082.17
426 2026-01-25 86561.94
427 2026-01-26 88250.01
428 2026-01-27 89116.96
429 2026-01-28 89162.4
430 2026-01-29 84513.2
431 2026-01-30 84110.99
432 2026-01-31 78648.0
433 2026-02-01 76895.53
434 2026-02-02 78666.85
435 2026-02-03 75661.49
436 2026-02-04 72998.0
437 2026-02-05 62791.0
438 2026-02-06 70527.7
439 2026-02-07 69244.74
440 2026-02-08 70279.33
441 2026-02-09 70101.99
442 2026-02-10 68803.65
443 2026-02-11 67024.93
444 2026-02-12 66208.0
445 2026-02-13 68812.0
446 2026-02-14 69786.96
447 2026-02-15 68792.0
448 2026-02-16 68858.13
449 2026-02-17 67466.7
450 2026-02-18 66419.79
451 2026-02-19 66972.5
452 2026-02-20 67984.42
453 2026-02-21 67955.78
454 2026-02-22 67617.98
455 2026-02-23 64625.78
456 2026-02-24 64068.33
457 2026-02-25 67992.68
458 2026-02-26 67490.51
459 2026-02-27 65859.94
460 2026-02-28 66967.85
461 2026-03-01 65769.0
462 2026-03-02 68832.01
463 2026-03-03 68335.99
464 2026-03-04 72683.26
465 2026-03-05 70887.6
466 2026-03-06 68108.24
467 2026-03-07 67259.62
468 2026-03-08 65970.56
469 2026-03-09 68432.17
470 2026-03-10 69960.83
471 2026-03-11 70208.0
472 2026-03-12 70531.56
473 2026-03-13 70944.33
474 2026-03-14 71232.02
475 2026-03-15 72830.01
476 2026-03-16 74886.47
477 2026-03-17 73934.11
478 2026-03-18 71245.02
479 2026-03-19 69918.3
480 2026-03-20 70497.01
481 2026-03-21 68912.02
482 2026-03-22 67844.94
483 2026-03-23 70874.23
484 2026-03-24 70533.49
485 2026-03-25 71301.53
486 2026-03-26 68769.02
487 2026-03-27 66353.34
488 2026-03-28 66320.15
489 2026-03-29 65956.94
490 2026-03-30 66737.18
491 2026-03-31 68221.85
492 2026-04-01 68112.35
493 2026-04-02 66894.48
494 2026-04-03 66959.99
495 2026-04-04 67291.73
496 2026-04-05 69005.0
497 2026-04-06 68853.61
498 2026-04-07 71910.2
499 2026-04-08 71085.99
500 2026-04-09 71798.01
501 2026-04-10 72997.88
502 2026-04-11 73086.0
503 2026-04-12 70755.35
504 2026-04-13 74446.0
505 2026-04-14 74179.04
506 2026-04-15 74836.31
507 2026-04-16 75163.09
508 2026-04-17 77098.01
509 2026-04-18 75736.82
510 2026-04-19 73823.14
511 2026-04-20 75864.86
512 2026-04-21 76348.58
513 2026-04-22 78208.52
514 2026-04-23 78281.32
515 2026-04-24 77461.8
516 2026-04-25 77650.72
517 2026-04-26 78673.83
518 2026-04-27 77364.42
519 2026-04-28 76317.99
520 2026-04-29 75752.88
521 2026-04-30 76305.78
522 2026-05-01 78234.05
523 2026-05-02 78682.31
524 2026-05-03 78558.89
525 2026-05-04 79852.37
526 2026-05-05 80907.72
527 2026-05-06 81438.1
528 2026-05-07 80005.38
529 2026-05-08 80188.82
530 2026-05-09 80655.96
531 2026-05-10 82199.99
532 2026-05-11 81727.06
533 2026-05-12 80484.72
534 2026-05-13 79288.26
535 2026-05-14 81079.39
536 2026-05-15 79058.51
537 2026-05-16 78105.45
538 2026-05-17 77407.59
539 2026-05-18 76943.98
540 2026-05-19 76767.48
541 2026-05-20 77475.99
542 2026-05-21 77547.62
543 2026-05-22 75443.91
544 2026-05-23 76650.0
545 2026-05-24 76975.99
546 2026-05-25 77249.07
547 2026-05-26 75826.1
548 2026-05-27 74315.98
549 2026-05-28 73512.56
550 2026-05-29 73366.77
551 2026-05-30 73770.69
552 2026-05-31 73575.17
553 2026-06-01 71314.42
554 2026-06-02 66658.35
555 2026-06-03 64040.0
556 2026-06-04 63806.02
557 2026-06-05 61032.0
558 2026-06-06 60850.48
559 2026-06-07 63302.73
560 2026-06-08 63062.98
561 2026-06-09 61685.8
562 2026-06-10 61449.46
563 2026-06-11 63563.42
564 2026-06-12 63538.43
565 2026-06-13 64429.99
566 2026-06-14 65706.62
567 2026-06-15 66276.8
568 2026-06-16 65616.64
569 2026-06-17 64446.04
570 2026-06-18 62879.01
571 2026-06-19 63475.2
572 2026-06-20 64222.99
573 2026-06-21 63235.63
574 2026-06-22 63950.61
575 2026-06-23 62644.86
576 2026-06-24 60983.13
577 2026-06-25 59703.65
578 2026-06-26 60002.53
579 2026-06-27 59934.84
580 2026-06-28 59474.01
581 2026-06-29 60162.73
582 2026-06-30 58523.93
583 2026-07-01 59961.45
584 2026-07-02 61484.02
585 2026-07-03 62520.22
586 2026-07-04 63086.45
587 2026-07-05 63580.44
588 2026-07-06 64001.84
589 2026-07-07 63323.27
590 2026-07-08 62237.7
591 2026-07-09 63175.25
592 2026-07-10 64128.49
593 2026-07-11 63773.04
594 2026-07-12 63740.32
595 2026-07-13 62264.94
596 2026-07-14 64988.64
597 2026-07-15 64716.63
598 2026-07-16 63770.6
599 2026-07-17 63894.0
600 2026-07-18 64796.36
601 2026-07-19 64681.78
602 2026-07-20 65213.05
603 2026-07-21 66516.18
604 2026-07-22 66086.61
605 2026-07-23 65051.23
606 2026-07-24 64083.32
607 2026-07-25 64295.61
608 2026-07-26 65341.07
609 2026-07-27 63694.43
610 2026-07-28 63847.12
611 2026-07-29 63896.14
612 2026-07-30 64721.9
613 2026-07-31 62825.9
614 2026-08-01 62764.2
615 2026-08-02 63499.49
616 2026-08-03 63466.51
617 2026-08-04 64050.51
618 2026-08-05 64603.03
619 2026-08-06 64267.3
620 2026-08-07 64891.61
621 2026-08-08 64908.72
622 2026-08-09 64848.69
623 2026-08-10 63911.88
624 2026-08-11 63531.75
625 2026-08-12 63411.72
626 2026-08-13 63425.35
627 2026-08-14 62975.19
628 2026-08-15 63018.75
629 2026-08-16 62836.66
630 2026-08-17 64484.18
631 2026-08-18 64681.33
632 2026-08-19 69300.01
633 2026-08-20 73011.87
634 2026-08-21 78325.54
635 2026-08-22 77054.44
636 2026-08-23 77729.36
637 2026-08-24 78981.59
638 2026-08-25 78526.8
639 2026-08-26 79026.18
640 2026-08-27 80275.34
641 2026-08-28 77839.19
642 2026-08-29 78233.93
643 2026-08-30 77665.14
644 2026-08-31 78562.74
645 2026-09-01 77398.69
646 2026-09-02 77307.36
647 2026-09-03 81263.99
648 2026-09-04 79675.12
649 2026-09-05 79831.57
650 2026-09-06 80339.13
651 2026-09-07 79091.97
652 2026-09-08 78447.11
653 2026-09-09 78283.98
654 2026-09-10 76536.55
655 2026-09-11 77208.55
656 2026-09-12 77262.85
657 2026-09-13 76799.85
658 2026-09-14 78175.01
659 2026-09-15 75584.17
660 2026-09-16 76144.99
661 2026-09-17 76348.74
662 2026-09-18 80875.04
663 2026-09-19 81233.91
664 2026-09-20 81159.64
665 2026-09-21 86594.94
666 2026-09-22 86198.05
667 2026-09-23 84378.31
View File

Before

Width:  |  Height:  |  Size: 783 KiB

After

Width:  |  Height:  |  Size: 783 KiB

Generated
+27
View File
@@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1790178388,
"narHash": "sha256-kK3t7gwoz4Nx8RF46cs1Xz/skKNcK7y3KYP+gGqX6W8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "00455b0a3690d3f5dc61e9aef4277dc86235b73f",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}
+36
View File
@@ -0,0 +1,36 @@
{
description = "Bitcoin price model";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
outputs =
{ self, nixpkgs }:
let
systems = [
"aarch64-darwin"
"x86_64-darwin"
"aarch64-linux"
"x86_64-linux"
];
forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f nixpkgs.legacyPackages.${system});
in
{
devShells = forAllSystems (pkgs: {
default = pkgs.mkShell {
packages = [
(pkgs.python313.withPackages (ps: [
ps.numpy
ps.pandas
ps.scipy
ps.matplotlib
ps.pytest
]))
pkgs.ruff
pkgs.just
];
};
});
formatter = forAllSystems (pkgs: pkgs.nixfmt);
};
}
+27 -4
View File
@@ -1,11 +1,34 @@
run *name:
python3 ./model.py -n "{{ name }}"
# List recipes
default:
@just --list
# Fetch new daily prices from Coinbase
update:
python -m btcmodel update
# Score models on development data (outcomes up to the dev cutoff)
backtest *args:
python -m btcmodel {{ args }} backtest
# Score models on held-out outcomes; run sparingly, never while tuning
holdout *args:
python -m btcmodel {{ args }} backtest --holdout
# Forecast from the latest price
forecast *args:
python -m btcmodel {{ args }} forecast
test:
pytest -q
fmt:
black ./*.py
ruff format .
ruff check --fix --select I .
nix fmt flake.nix
lint:
ruff check ./model.py
ruff check .
ruff format --check .
clean:
rm -rf output
-1999
View File
File diff suppressed because it is too large Load Diff
Generated
-734
View File
@@ -1,734 +0,0 @@
# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand.
[[package]]
name = "contourpy"
version = "1.3.1"
description = "Python library for calculating contours of 2D quadrilateral grids"
optional = false
python-versions = ">=3.10"
files = [
{file = "contourpy-1.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a045f341a77b77e1c5de31e74e966537bba9f3c4099b35bf4c2e3939dd54cdab"},
{file = "contourpy-1.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:500360b77259914f7805af7462e41f9cb7ca92ad38e9f94d6c8641b089338124"},
{file = "contourpy-1.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2f926efda994cdf3c8d3fdb40b9962f86edbc4457e739277b961eced3d0b4c1"},
{file = "contourpy-1.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:adce39d67c0edf383647a3a007de0a45fd1b08dedaa5318404f1a73059c2512b"},
{file = "contourpy-1.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abbb49fb7dac584e5abc6636b7b2a7227111c4f771005853e7d25176daaf8453"},
{file = "contourpy-1.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0cffcbede75c059f535725c1680dfb17b6ba8753f0c74b14e6a9c68c29d7ea3"},
{file = "contourpy-1.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ab29962927945d89d9b293eabd0d59aea28d887d4f3be6c22deaefbb938a7277"},
{file = "contourpy-1.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:974d8145f8ca354498005b5b981165b74a195abfae9a8129df3e56771961d595"},
{file = "contourpy-1.3.1-cp310-cp310-win32.whl", hash = "sha256:ac4578ac281983f63b400f7fe6c101bedc10651650eef012be1ccffcbacf3697"},
{file = "contourpy-1.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:174e758c66bbc1c8576992cec9599ce8b6672b741b5d336b5c74e35ac382b18e"},
{file = "contourpy-1.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3e8b974d8db2c5610fb4e76307e265de0edb655ae8169e8b21f41807ccbeec4b"},
{file = "contourpy-1.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:20914c8c973f41456337652a6eeca26d2148aa96dd7ac323b74516988bea89fc"},
{file = "contourpy-1.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19d40d37c1c3a4961b4619dd9d77b12124a453cc3d02bb31a07d58ef684d3d86"},
{file = "contourpy-1.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:113231fe3825ebf6f15eaa8bc1f5b0ddc19d42b733345eae0934cb291beb88b6"},
{file = "contourpy-1.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4dbbc03a40f916a8420e420d63e96a1258d3d1b58cbdfd8d1f07b49fcbd38e85"},
{file = "contourpy-1.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a04ecd68acbd77fa2d39723ceca4c3197cb2969633836ced1bea14e219d077c"},
{file = "contourpy-1.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c414fc1ed8ee1dbd5da626cf3710c6013d3d27456651d156711fa24f24bd1291"},
{file = "contourpy-1.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:31c1b55c1f34f80557d3830d3dd93ba722ce7e33a0b472cba0ec3b6535684d8f"},
{file = "contourpy-1.3.1-cp311-cp311-win32.whl", hash = "sha256:f611e628ef06670df83fce17805c344710ca5cde01edfdc72751311da8585375"},
{file = "contourpy-1.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:b2bdca22a27e35f16794cf585832e542123296b4687f9fd96822db6bae17bfc9"},
{file = "contourpy-1.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0ffa84be8e0bd33410b17189f7164c3589c229ce5db85798076a3fa136d0e509"},
{file = "contourpy-1.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805617228ba7e2cbbfb6c503858e626ab528ac2a32a04a2fe88ffaf6b02c32bc"},
{file = "contourpy-1.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade08d343436a94e633db932e7e8407fe7de8083967962b46bdfc1b0ced39454"},
{file = "contourpy-1.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47734d7073fb4590b4a40122b35917cd77be5722d80683b249dac1de266aac80"},
{file = "contourpy-1.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2ba94a401342fc0f8b948e57d977557fbf4d515f03c67682dd5c6191cb2d16ec"},
{file = "contourpy-1.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efa874e87e4a647fd2e4f514d5e91c7d493697127beb95e77d2f7561f6905bd9"},
{file = "contourpy-1.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1bf98051f1045b15c87868dbaea84f92408337d4f81d0e449ee41920ea121d3b"},
{file = "contourpy-1.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:61332c87493b00091423e747ea78200659dc09bdf7fd69edd5e98cef5d3e9a8d"},
{file = "contourpy-1.3.1-cp312-cp312-win32.whl", hash = "sha256:e914a8cb05ce5c809dd0fe350cfbb4e881bde5e2a38dc04e3afe1b3e58bd158e"},
{file = "contourpy-1.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:08d9d449a61cf53033612cb368f3a1b26cd7835d9b8cd326647efe43bca7568d"},
{file = "contourpy-1.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a761d9ccfc5e2ecd1bf05534eda382aa14c3e4f9205ba5b1684ecfe400716ef2"},
{file = "contourpy-1.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:523a8ee12edfa36f6d2a49407f705a6ef4c5098de4f498619787e272de93f2d5"},
{file = "contourpy-1.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece6df05e2c41bd46776fbc712e0996f7c94e0d0543af1656956d150c4ca7c81"},
{file = "contourpy-1.3.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:573abb30e0e05bf31ed067d2f82500ecfdaec15627a59d63ea2d95714790f5c2"},
{file = "contourpy-1.3.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9fa36448e6a3a1a9a2ba23c02012c43ed88905ec80163f2ffe2421c7192a5d7"},
{file = "contourpy-1.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ea9924d28fc5586bf0b42d15f590b10c224117e74409dd7a0be3b62b74a501c"},
{file = "contourpy-1.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b75aa69cb4d6f137b36f7eb2ace9280cfb60c55dc5f61c731fdf6f037f958a3"},
{file = "contourpy-1.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:041b640d4ec01922083645a94bb3b2e777e6b626788f4095cf21abbe266413c1"},
{file = "contourpy-1.3.1-cp313-cp313-win32.whl", hash = "sha256:36987a15e8ace5f58d4d5da9dca82d498c2bbb28dff6e5d04fbfcc35a9cb3a82"},
{file = "contourpy-1.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:a7895f46d47671fa7ceec40f31fae721da51ad34bdca0bee83e38870b1f47ffd"},
{file = "contourpy-1.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9ddeb796389dadcd884c7eb07bd14ef12408aaae358f0e2ae24114d797eede30"},
{file = "contourpy-1.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:19c1555a6801c2f084c7ddc1c6e11f02eb6a6016ca1318dd5452ba3f613a1751"},
{file = "contourpy-1.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:841ad858cff65c2c04bf93875e384ccb82b654574a6d7f30453a04f04af71342"},
{file = "contourpy-1.3.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4318af1c925fb9a4fb190559ef3eec206845f63e80fb603d47f2d6d67683901c"},
{file = "contourpy-1.3.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:14c102b0eab282427b662cb590f2e9340a9d91a1c297f48729431f2dcd16e14f"},
{file = "contourpy-1.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05e806338bfeaa006acbdeba0ad681a10be63b26e1b17317bfac3c5d98f36cda"},
{file = "contourpy-1.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4d76d5993a34ef3df5181ba3c92fabb93f1eaa5729504fb03423fcd9f3177242"},
{file = "contourpy-1.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:89785bb2a1980c1bd87f0cb1517a71cde374776a5f150936b82580ae6ead44a1"},
{file = "contourpy-1.3.1-cp313-cp313t-win32.whl", hash = "sha256:8eb96e79b9f3dcadbad2a3891672f81cdcab7f95b27f28f1c67d75f045b6b4f1"},
{file = "contourpy-1.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:287ccc248c9e0d0566934e7d606201abd74761b5703d804ff3df8935f523d546"},
{file = "contourpy-1.3.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b457d6430833cee8e4b8e9b6f07aa1c161e5e0d52e118dc102c8f9bd7dd060d6"},
{file = "contourpy-1.3.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb76c1a154b83991a3cbbf0dfeb26ec2833ad56f95540b442c73950af2013750"},
{file = "contourpy-1.3.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:44a29502ca9c7b5ba389e620d44f2fbe792b1fb5734e8b931ad307071ec58c53"},
{file = "contourpy-1.3.1.tar.gz", hash = "sha256:dfd97abd83335045a913e3bcc4a09c0ceadbe66580cf573fe961f4a825efa699"},
]
[package.dependencies]
numpy = ">=1.23"
[package.extras]
bokeh = ["bokeh", "selenium"]
docs = ["furo", "sphinx (>=7.2)", "sphinx-copybutton"]
mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.11.1)", "types-Pillow"]
test = ["Pillow", "contourpy[test-no-images]", "matplotlib"]
test-no-images = ["pytest", "pytest-cov", "pytest-rerunfailures", "pytest-xdist", "wurlitzer"]
[[package]]
name = "cycler"
version = "0.12.1"
description = "Composable style cycles"
optional = false
python-versions = ">=3.8"
files = [
{file = "cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30"},
{file = "cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c"},
]
[package.extras]
docs = ["ipython", "matplotlib", "numpydoc", "sphinx"]
tests = ["pytest", "pytest-cov", "pytest-xdist"]
[[package]]
name = "fonttools"
version = "4.55.0"
description = "Tools to manipulate font files"
optional = false
python-versions = ">=3.8"
files = [
{file = "fonttools-4.55.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:51c029d4c0608a21a3d3d169dfc3fb776fde38f00b35ca11fdab63ba10a16f61"},
{file = "fonttools-4.55.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bca35b4e411362feab28e576ea10f11268b1aeed883b9f22ed05675b1e06ac69"},
{file = "fonttools-4.55.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ce4ba6981e10f7e0ccff6348e9775ce25ffadbee70c9fd1a3737e3e9f5fa74f"},
{file = "fonttools-4.55.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31d00f9852a6051dac23294a4cf2df80ced85d1d173a61ba90a3d8f5abc63c60"},
{file = "fonttools-4.55.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e198e494ca6e11f254bac37a680473a311a88cd40e58f9cc4dc4911dfb686ec6"},
{file = "fonttools-4.55.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7208856f61770895e79732e1dcbe49d77bd5783adf73ae35f87fcc267df9db81"},
{file = "fonttools-4.55.0-cp310-cp310-win32.whl", hash = "sha256:e7e6a352ff9e46e8ef8a3b1fe2c4478f8a553e1b5a479f2e899f9dc5f2055880"},
{file = "fonttools-4.55.0-cp310-cp310-win_amd64.whl", hash = "sha256:636caaeefe586d7c84b5ee0734c1a5ab2dae619dc21c5cf336f304ddb8f6001b"},
{file = "fonttools-4.55.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fa34aa175c91477485c44ddfbb51827d470011e558dfd5c7309eb31bef19ec51"},
{file = "fonttools-4.55.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:37dbb3fdc2ef7302d3199fb12468481cbebaee849e4b04bc55b77c24e3c49189"},
{file = "fonttools-4.55.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5263d8e7ef3c0ae87fbce7f3ec2f546dc898d44a337e95695af2cd5ea21a967"},
{file = "fonttools-4.55.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f307f6b5bf9e86891213b293e538d292cd1677e06d9faaa4bf9c086ad5f132f6"},
{file = "fonttools-4.55.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f0a4b52238e7b54f998d6a56b46a2c56b59c74d4f8a6747fb9d4042190f37cd3"},
{file = "fonttools-4.55.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3e569711464f777a5d4ef522e781dc33f8095ab5efd7548958b36079a9f2f88c"},
{file = "fonttools-4.55.0-cp311-cp311-win32.whl", hash = "sha256:2b3ab90ec0f7b76c983950ac601b58949f47aca14c3f21eed858b38d7ec42b05"},
{file = "fonttools-4.55.0-cp311-cp311-win_amd64.whl", hash = "sha256:aa046f6a63bb2ad521004b2769095d4c9480c02c1efa7d7796b37826508980b6"},
{file = "fonttools-4.55.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:838d2d8870f84fc785528a692e724f2379d5abd3fc9dad4d32f91cf99b41e4a7"},
{file = "fonttools-4.55.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f46b863d74bab7bb0d395f3b68d3f52a03444964e67ce5c43ce43a75efce9246"},
{file = "fonttools-4.55.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33b52a9cfe4e658e21b1f669f7309b4067910321757fec53802ca8f6eae96a5a"},
{file = "fonttools-4.55.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:732a9a63d6ea4a81b1b25a1f2e5e143761b40c2e1b79bb2b68e4893f45139a40"},
{file = "fonttools-4.55.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7dd91ac3fcb4c491bb4763b820bcab6c41c784111c24172616f02f4bc227c17d"},
{file = "fonttools-4.55.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1f0e115281a32ff532118aa851ef497a1b7cda617f4621c1cdf81ace3e36fb0c"},
{file = "fonttools-4.55.0-cp312-cp312-win32.whl", hash = "sha256:6c99b5205844f48a05cb58d4a8110a44d3038c67ed1d79eb733c4953c628b0f6"},
{file = "fonttools-4.55.0-cp312-cp312-win_amd64.whl", hash = "sha256:f8c8c76037d05652510ae45be1cd8fb5dd2fd9afec92a25374ac82255993d57c"},
{file = "fonttools-4.55.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8118dc571921dc9e4b288d9cb423ceaf886d195a2e5329cc427df82bba872cd9"},
{file = "fonttools-4.55.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01124f2ca6c29fad4132d930da69158d3f49b2350e4a779e1efbe0e82bd63f6c"},
{file = "fonttools-4.55.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81ffd58d2691f11f7c8438796e9f21c374828805d33e83ff4b76e4635633674c"},
{file = "fonttools-4.55.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5435e5f1eb893c35c2bc2b9cd3c9596b0fcb0a59e7a14121562986dd4c47b8dd"},
{file = "fonttools-4.55.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d12081729280c39d001edd0f4f06d696014c26e6e9a0a55488fabc37c28945e4"},
{file = "fonttools-4.55.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a7ad1f1b98ab6cb927ab924a38a8649f1ffd7525c75fe5b594f5dab17af70e18"},
{file = "fonttools-4.55.0-cp313-cp313-win32.whl", hash = "sha256:abe62987c37630dca69a104266277216de1023cf570c1643bb3a19a9509e7a1b"},
{file = "fonttools-4.55.0-cp313-cp313-win_amd64.whl", hash = "sha256:2863555ba90b573e4201feaf87a7e71ca3b97c05aa4d63548a4b69ea16c9e998"},
{file = "fonttools-4.55.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:00f7cf55ad58a57ba421b6a40945b85ac7cc73094fb4949c41171d3619a3a47e"},
{file = "fonttools-4.55.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f27526042efd6f67bfb0cc2f1610fa20364396f8b1fc5edb9f45bb815fb090b2"},
{file = "fonttools-4.55.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e67974326af6a8879dc2a4ec63ab2910a1c1a9680ccd63e4a690950fceddbe"},
{file = "fonttools-4.55.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61dc0a13451143c5e987dec5254d9d428f3c2789a549a7cf4f815b63b310c1cc"},
{file = "fonttools-4.55.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:b2e526b325a903868c62155a6a7e24df53f6ce4c5c3160214d8fe1be2c41b478"},
{file = "fonttools-4.55.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:b7ef9068a1297714e6fefe5932c33b058aa1d45a2b8be32a4c6dee602ae22b5c"},
{file = "fonttools-4.55.0-cp38-cp38-win32.whl", hash = "sha256:55718e8071be35dff098976bc249fc243b58efa263768c611be17fe55975d40a"},
{file = "fonttools-4.55.0-cp38-cp38-win_amd64.whl", hash = "sha256:553bd4f8cc327f310c20158e345e8174c8eed49937fb047a8bda51daf2c353c8"},
{file = "fonttools-4.55.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3f901cef813f7c318b77d1c5c14cf7403bae5cb977cede023e22ba4316f0a8f6"},
{file = "fonttools-4.55.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8c9679fc0dd7e8a5351d321d8d29a498255e69387590a86b596a45659a39eb0d"},
{file = "fonttools-4.55.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd2820a8b632f3307ebb0bf57948511c2208e34a4939cf978333bc0a3f11f838"},
{file = "fonttools-4.55.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23bbbb49bec613a32ed1b43df0f2b172313cee690c2509f1af8fdedcf0a17438"},
{file = "fonttools-4.55.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a656652e1f5d55b9728937a7e7d509b73d23109cddd4e89ee4f49bde03b736c6"},
{file = "fonttools-4.55.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f50a1f455902208486fbca47ce33054208a4e437b38da49d6721ce2fef732fcf"},
{file = "fonttools-4.55.0-cp39-cp39-win32.whl", hash = "sha256:161d1ac54c73d82a3cded44202d0218ab007fde8cf194a23d3dd83f7177a2f03"},
{file = "fonttools-4.55.0-cp39-cp39-win_amd64.whl", hash = "sha256:ca7fd6987c68414fece41c96836e945e1f320cda56fc96ffdc16e54a44ec57a2"},
{file = "fonttools-4.55.0-py3-none-any.whl", hash = "sha256:12db5888cd4dd3fcc9f0ee60c6edd3c7e1fd44b7dd0f31381ea03df68f8a153f"},
{file = "fonttools-4.55.0.tar.gz", hash = "sha256:7636acc6ab733572d5e7eec922b254ead611f1cdad17be3f0be7418e8bfaca71"},
]
[package.extras]
all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "pycairo", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.1.0)", "xattr", "zopfli (>=0.1.4)"]
graphite = ["lz4 (>=1.7.4.2)"]
interpolatable = ["munkres", "pycairo", "scipy"]
lxml = ["lxml (>=4.0)"]
pathops = ["skia-pathops (>=0.5.0)"]
plot = ["matplotlib"]
repacker = ["uharfbuzz (>=0.23.0)"]
symfont = ["sympy"]
type1 = ["xattr"]
ufo = ["fs (>=2.2.0,<3)"]
unicode = ["unicodedata2 (>=15.1.0)"]
woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"]
[[package]]
name = "kiwisolver"
version = "1.4.7"
description = "A fast implementation of the Cassowary constraint solver"
optional = false
python-versions = ">=3.8"
files = [
{file = "kiwisolver-1.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8a9c83f75223d5e48b0bc9cb1bf2776cf01563e00ade8775ffe13b0b6e1af3a6"},
{file = "kiwisolver-1.4.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:58370b1ffbd35407444d57057b57da5d6549d2d854fa30249771775c63b5fe17"},
{file = "kiwisolver-1.4.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aa0abdf853e09aff551db11fce173e2177d00786c688203f52c87ad7fcd91ef9"},
{file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8d53103597a252fb3ab8b5845af04c7a26d5e7ea8122303dd7a021176a87e8b9"},
{file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:88f17c5ffa8e9462fb79f62746428dd57b46eb931698e42e990ad63103f35e6c"},
{file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a9ca9c710d598fd75ee5de59d5bda2684d9db36a9f50b6125eaea3969c2599"},
{file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f4d742cb7af1c28303a51b7a27aaee540e71bb8e24f68c736f6f2ffc82f2bf05"},
{file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e28c7fea2196bf4c2f8d46a0415c77a1c480cc0724722f23d7410ffe9842c407"},
{file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e968b84db54f9d42046cf154e02911e39c0435c9801681e3fc9ce8a3c4130278"},
{file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0c18ec74c0472de033e1bebb2911c3c310eef5649133dd0bedf2a169a1b269e5"},
{file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8f0ea6da6d393d8b2e187e6a5e3fb81f5862010a40c3945e2c6d12ae45cfb2ad"},
{file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:f106407dda69ae456dd1227966bf445b157ccc80ba0dff3802bb63f30b74e895"},
{file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:84ec80df401cfee1457063732d90022f93951944b5b58975d34ab56bb150dfb3"},
{file = "kiwisolver-1.4.7-cp310-cp310-win32.whl", hash = "sha256:71bb308552200fb2c195e35ef05de12f0c878c07fc91c270eb3d6e41698c3bcc"},
{file = "kiwisolver-1.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:44756f9fd339de0fb6ee4f8c1696cfd19b2422e0d70b4cefc1cc7f1f64045a8c"},
{file = "kiwisolver-1.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:78a42513018c41c2ffd262eb676442315cbfe3c44eed82385c2ed043bc63210a"},
{file = "kiwisolver-1.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d2b0e12a42fb4e72d509fc994713d099cbb15ebf1103545e8a45f14da2dfca54"},
{file = "kiwisolver-1.4.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2a8781ac3edc42ea4b90bc23e7d37b665d89423818e26eb6df90698aa2287c95"},
{file = "kiwisolver-1.4.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:46707a10836894b559e04b0fd143e343945c97fd170d69a2d26d640b4e297935"},
{file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef97b8df011141c9b0f6caf23b29379f87dd13183c978a30a3c546d2c47314cb"},
{file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ab58c12a2cd0fc769089e6d38466c46d7f76aced0a1f54c77652446733d2d02"},
{file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:803b8e1459341c1bb56d1c5c010406d5edec8a0713a0945851290a7930679b51"},
{file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9a9e8a507420fe35992ee9ecb302dab68550dedc0da9e2880dd88071c5fb052"},
{file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18077b53dc3bb490e330669a99920c5e6a496889ae8c63b58fbc57c3d7f33a18"},
{file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6af936f79086a89b3680a280c47ea90b4df7047b5bdf3aa5c524bbedddb9e545"},
{file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3abc5b19d24af4b77d1598a585b8a719beb8569a71568b66f4ebe1fb0449460b"},
{file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:933d4de052939d90afbe6e9d5273ae05fb836cc86c15b686edd4b3560cc0ee36"},
{file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:65e720d2ab2b53f1f72fb5da5fb477455905ce2c88aaa671ff0a447c2c80e8e3"},
{file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3bf1ed55088f214ba6427484c59553123fdd9b218a42bbc8c6496d6754b1e523"},
{file = "kiwisolver-1.4.7-cp311-cp311-win32.whl", hash = "sha256:4c00336b9dd5ad96d0a558fd18a8b6f711b7449acce4c157e7343ba92dd0cf3d"},
{file = "kiwisolver-1.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:929e294c1ac1e9f615c62a4e4313ca1823ba37326c164ec720a803287c4c499b"},
{file = "kiwisolver-1.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:e33e8fbd440c917106b237ef1a2f1449dfbb9b6f6e1ce17c94cd6a1e0d438376"},
{file = "kiwisolver-1.4.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:5360cc32706dab3931f738d3079652d20982511f7c0ac5711483e6eab08efff2"},
{file = "kiwisolver-1.4.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942216596dc64ddb25adb215c3c783215b23626f8d84e8eff8d6d45c3f29f75a"},
{file = "kiwisolver-1.4.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:48b571ecd8bae15702e4f22d3ff6a0f13e54d3d00cd25216d5e7f658242065ee"},
{file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad42ba922c67c5f219097b28fae965e10045ddf145d2928bfac2eb2e17673640"},
{file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:612a10bdae23404a72941a0fc8fa2660c6ea1217c4ce0dbcab8a8f6543ea9e7f"},
{file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e838bba3a3bac0fe06d849d29772eb1afb9745a59710762e4ba3f4cb8424483"},
{file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:22f499f6157236c19f4bbbd472fa55b063db77a16cd74d49afe28992dff8c258"},
{file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693902d433cf585133699972b6d7c42a8b9f8f826ebcaf0132ff55200afc599e"},
{file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4e77f2126c3e0b0d055f44513ed349038ac180371ed9b52fe96a32aa071a5107"},
{file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:657a05857bda581c3656bfc3b20e353c232e9193eb167766ad2dc58b56504948"},
{file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4bfa75a048c056a411f9705856abfc872558e33c055d80af6a380e3658766038"},
{file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:34ea1de54beef1c104422d210c47c7d2a4999bdecf42c7b5718fbe59a4cac383"},
{file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:90da3b5f694b85231cf93586dad5e90e2d71b9428f9aad96952c99055582f520"},
{file = "kiwisolver-1.4.7-cp312-cp312-win32.whl", hash = "sha256:18e0cca3e008e17fe9b164b55735a325140a5a35faad8de92dd80265cd5eb80b"},
{file = "kiwisolver-1.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:58cb20602b18f86f83a5c87d3ee1c766a79c0d452f8def86d925e6c60fbf7bfb"},
{file = "kiwisolver-1.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:f5a8b53bdc0b3961f8b6125e198617c40aeed638b387913bf1ce78afb1b0be2a"},
{file = "kiwisolver-1.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2e6039dcbe79a8e0f044f1c39db1986a1b8071051efba3ee4d74f5b365f5226e"},
{file = "kiwisolver-1.4.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a1ecf0ac1c518487d9d23b1cd7139a6a65bc460cd101ab01f1be82ecf09794b6"},
{file = "kiwisolver-1.4.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ab9ccab2b5bd5702ab0803676a580fffa2aa178c2badc5557a84cc943fcf750"},
{file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f816dd2277f8d63d79f9c8473a79fe54047bc0467754962840782c575522224d"},
{file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf8bcc23ceb5a1b624572a1623b9f79d2c3b337c8c455405ef231933a10da379"},
{file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dea0bf229319828467d7fca8c7c189780aa9ff679c94539eed7532ebe33ed37c"},
{file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c06a4c7cf15ec739ce0e5971b26c93638730090add60e183530d70848ebdd34"},
{file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:913983ad2deb14e66d83c28b632fd35ba2b825031f2fa4ca29675e665dfecbe1"},
{file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5337ec7809bcd0f424c6b705ecf97941c46279cf5ed92311782c7c9c2026f07f"},
{file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4c26ed10c4f6fa6ddb329a5120ba3b6db349ca192ae211e882970bfc9d91420b"},
{file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c619b101e6de2222c1fcb0531e1b17bbffbe54294bfba43ea0d411d428618c27"},
{file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:073a36c8273647592ea332e816e75ef8da5c303236ec0167196793eb1e34657a"},
{file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3ce6b2b0231bda412463e152fc18335ba32faf4e8c23a754ad50ffa70e4091ee"},
{file = "kiwisolver-1.4.7-cp313-cp313-win32.whl", hash = "sha256:f4c9aee212bc89d4e13f58be11a56cc8036cabad119259d12ace14b34476fd07"},
{file = "kiwisolver-1.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:8a3ec5aa8e38fc4c8af308917ce12c536f1c88452ce554027e55b22cbbfbff76"},
{file = "kiwisolver-1.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:76c8094ac20ec259471ac53e774623eb62e6e1f56cd8690c67ce6ce4fcb05650"},
{file = "kiwisolver-1.4.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5d5abf8f8ec1f4e22882273c423e16cae834c36856cac348cfbfa68e01c40f3a"},
{file = "kiwisolver-1.4.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:aeb3531b196ef6f11776c21674dba836aeea9d5bd1cf630f869e3d90b16cfade"},
{file = "kiwisolver-1.4.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b7d755065e4e866a8086c9bdada157133ff466476a2ad7861828e17b6026e22c"},
{file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08471d4d86cbaec61f86b217dd938a83d85e03785f51121e791a6e6689a3be95"},
{file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7bbfcb7165ce3d54a3dfbe731e470f65739c4c1f85bb1018ee912bae139e263b"},
{file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d34eb8494bea691a1a450141ebb5385e4b69d38bb8403b5146ad279f4b30fa3"},
{file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9242795d174daa40105c1d86aba618e8eab7bf96ba8c3ee614da8302a9f95503"},
{file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a0f64a48bb81af7450e641e3fe0b0394d7381e342805479178b3d335d60ca7cf"},
{file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8e045731a5416357638d1700927529e2b8ab304811671f665b225f8bf8d8f933"},
{file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:4322872d5772cae7369f8351da1edf255a604ea7087fe295411397d0cfd9655e"},
{file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:e1631290ee9271dffe3062d2634c3ecac02c83890ada077d225e081aca8aab89"},
{file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:edcfc407e4eb17e037bca59be0e85a2031a2ac87e4fed26d3e9df88b4165f92d"},
{file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:4d05d81ecb47d11e7f8932bd8b61b720bf0b41199358f3f5e36d38e28f0532c5"},
{file = "kiwisolver-1.4.7-cp38-cp38-win32.whl", hash = "sha256:b38ac83d5f04b15e515fd86f312479d950d05ce2368d5413d46c088dda7de90a"},
{file = "kiwisolver-1.4.7-cp38-cp38-win_amd64.whl", hash = "sha256:d83db7cde68459fc803052a55ace60bea2bae361fc3b7a6d5da07e11954e4b09"},
{file = "kiwisolver-1.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3f9362ecfca44c863569d3d3c033dbe8ba452ff8eed6f6b5806382741a1334bd"},
{file = "kiwisolver-1.4.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e8df2eb9b2bac43ef8b082e06f750350fbbaf2887534a5be97f6cf07b19d9583"},
{file = "kiwisolver-1.4.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f32d6edbc638cde7652bd690c3e728b25332acbadd7cad670cc4a02558d9c417"},
{file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e2e6c39bd7b9372b0be21456caab138e8e69cc0fc1190a9dfa92bd45a1e6e904"},
{file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dda56c24d869b1193fcc763f1284b9126550eaf84b88bbc7256e15028f19188a"},
{file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79849239c39b5e1fd906556c474d9b0439ea6792b637511f3fe3a41158d89ca8"},
{file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5e3bc157fed2a4c02ec468de4ecd12a6e22818d4f09cde2c31ee3226ffbefab2"},
{file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3da53da805b71e41053dc670f9a820d1157aae77b6b944e08024d17bcd51ef88"},
{file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8705f17dfeb43139a692298cb6637ee2e59c0194538153e83e9ee0c75c2eddde"},
{file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:82a5c2f4b87c26bb1a0ef3d16b5c4753434633b83d365cc0ddf2770c93829e3c"},
{file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce8be0466f4c0d585cdb6c1e2ed07232221df101a4c6f28821d2aa754ca2d9e2"},
{file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:409afdfe1e2e90e6ee7fc896f3df9a7fec8e793e58bfa0d052c8a82f99c37abb"},
{file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5b9c3f4ee0b9a439d2415012bd1b1cc2df59e4d6a9939f4d669241d30b414327"},
{file = "kiwisolver-1.4.7-cp39-cp39-win32.whl", hash = "sha256:a79ae34384df2b615eefca647a2873842ac3b596418032bef9a7283675962644"},
{file = "kiwisolver-1.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:cf0438b42121a66a3a667de17e779330fc0f20b0d97d59d2f2121e182b0505e4"},
{file = "kiwisolver-1.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:764202cc7e70f767dab49e8df52c7455e8de0df5d858fa801a11aa0d882ccf3f"},
{file = "kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:94252291e3fe68001b1dd747b4c0b3be12582839b95ad4d1b641924d68fd4643"},
{file = "kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b7dfa3b546da08a9f622bb6becdb14b3e24aaa30adba66749d38f3cc7ea9706"},
{file = "kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd3de6481f4ed8b734da5df134cd5a6a64fe32124fe83dde1e5b5f29fe30b1e6"},
{file = "kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a91b5f9f1205845d488c928e8570dcb62b893372f63b8b6e98b863ebd2368ff2"},
{file = "kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40fa14dbd66b8b8f470d5fc79c089a66185619d31645f9b0773b88b19f7223c4"},
{file = "kiwisolver-1.4.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:eb542fe7933aa09d8d8f9d9097ef37532a7df6497819d16efe4359890a2f417a"},
{file = "kiwisolver-1.4.7-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bfa1acfa0c54932d5607e19a2c24646fb4c1ae2694437789129cf099789a3b00"},
{file = "kiwisolver-1.4.7-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:eee3ea935c3d227d49b4eb85660ff631556841f6e567f0f7bda972df6c2c9935"},
{file = "kiwisolver-1.4.7-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f3160309af4396e0ed04db259c3ccbfdc3621b5559b5453075e5de555e1f3a1b"},
{file = "kiwisolver-1.4.7-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a17f6a29cf8935e587cc8a4dbfc8368c55edc645283db0ce9801016f83526c2d"},
{file = "kiwisolver-1.4.7-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10849fb2c1ecbfae45a693c070e0320a91b35dd4bcf58172c023b994283a124d"},
{file = "kiwisolver-1.4.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:ac542bf38a8a4be2dc6b15248d36315ccc65f0743f7b1a76688ffb6b5129a5c2"},
{file = "kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8b01aac285f91ca889c800042c35ad3b239e704b150cfd3382adfc9dcc780e39"},
{file = "kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:48be928f59a1f5c8207154f935334d374e79f2b5d212826307d072595ad76a2e"},
{file = "kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f37cfe618a117e50d8c240555331160d73d0411422b59b5ee217843d7b693608"},
{file = "kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:599b5c873c63a1f6ed7eead644a8a380cfbdf5db91dcb6f85707aaab213b1674"},
{file = "kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:801fa7802e5cfabe3ab0c81a34c323a319b097dfb5004be950482d882f3d7225"},
{file = "kiwisolver-1.4.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0c6c43471bc764fad4bc99c5c2d6d16a676b1abf844ca7c8702bdae92df01ee0"},
{file = "kiwisolver-1.4.7.tar.gz", hash = "sha256:9893ff81bd7107f7b685d3017cc6583daadb4fc26e4a888350df530e41980a60"},
]
[[package]]
name = "matplotlib"
version = "3.9.2"
description = "Python plotting package"
optional = false
python-versions = ">=3.9"
files = [
{file = "matplotlib-3.9.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:9d78bbc0cbc891ad55b4f39a48c22182e9bdaea7fc0e5dbd364f49f729ca1bbb"},
{file = "matplotlib-3.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c375cc72229614632c87355366bdf2570c2dac01ac66b8ad048d2dabadf2d0d4"},
{file = "matplotlib-3.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d94ff717eb2bd0b58fe66380bd8b14ac35f48a98e7c6765117fe67fb7684e64"},
{file = "matplotlib-3.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab68d50c06938ef28681073327795c5db99bb4666214d2d5f880ed11aeaded66"},
{file = "matplotlib-3.9.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:65aacf95b62272d568044531e41de26285d54aec8cb859031f511f84bd8b495a"},
{file = "matplotlib-3.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:3fd595f34aa8a55b7fc8bf9ebea8aa665a84c82d275190a61118d33fbc82ccae"},
{file = "matplotlib-3.9.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d8dd059447824eec055e829258ab092b56bb0579fc3164fa09c64f3acd478772"},
{file = "matplotlib-3.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c797dac8bb9c7a3fd3382b16fe8f215b4cf0f22adccea36f1545a6d7be310b41"},
{file = "matplotlib-3.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d719465db13267bcef19ea8954a971db03b9f48b4647e3860e4bc8e6ed86610f"},
{file = "matplotlib-3.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8912ef7c2362f7193b5819d17dae8629b34a95c58603d781329712ada83f9447"},
{file = "matplotlib-3.9.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7741f26a58a240f43bee74965c4882b6c93df3e7eb3de160126d8c8f53a6ae6e"},
{file = "matplotlib-3.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:ae82a14dab96fbfad7965403c643cafe6515e386de723e498cf3eeb1e0b70cc7"},
{file = "matplotlib-3.9.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ac43031375a65c3196bee99f6001e7fa5bdfb00ddf43379d3c0609bdca042df9"},
{file = "matplotlib-3.9.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:be0fc24a5e4531ae4d8e858a1a548c1fe33b176bb13eff7f9d0d38ce5112a27d"},
{file = "matplotlib-3.9.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf81de2926c2db243c9b2cbc3917619a0fc85796c6ba4e58f541df814bbf83c7"},
{file = "matplotlib-3.9.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6ee45bc4245533111ced13f1f2cace1e7f89d1c793390392a80c139d6cf0e6c"},
{file = "matplotlib-3.9.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:306c8dfc73239f0e72ac50e5a9cf19cc4e8e331dd0c54f5e69ca8758550f1e1e"},
{file = "matplotlib-3.9.2-cp312-cp312-win_amd64.whl", hash = "sha256:5413401594cfaff0052f9d8b1aafc6d305b4bd7c4331dccd18f561ff7e1d3bd3"},
{file = "matplotlib-3.9.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:18128cc08f0d3cfff10b76baa2f296fc28c4607368a8402de61bb3f2eb33c7d9"},
{file = "matplotlib-3.9.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4876d7d40219e8ae8bb70f9263bcbe5714415acfdf781086601211335e24f8aa"},
{file = "matplotlib-3.9.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d9f07a80deab4bb0b82858a9e9ad53d1382fd122be8cde11080f4e7dfedb38b"},
{file = "matplotlib-3.9.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7c0410f181a531ec4e93bbc27692f2c71a15c2da16766f5ba9761e7ae518413"},
{file = "matplotlib-3.9.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:909645cce2dc28b735674ce0931a4ac94e12f5b13f6bb0b5a5e65e7cea2c192b"},
{file = "matplotlib-3.9.2-cp313-cp313-win_amd64.whl", hash = "sha256:f32c7410c7f246838a77d6d1eff0c0f87f3cb0e7c4247aebea71a6d5a68cab49"},
{file = "matplotlib-3.9.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:37e51dd1c2db16ede9cfd7b5cabdfc818b2c6397c83f8b10e0e797501c963a03"},
{file = "matplotlib-3.9.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b82c5045cebcecd8496a4d694d43f9cc84aeeb49fe2133e036b207abe73f4d30"},
{file = "matplotlib-3.9.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f053c40f94bc51bc03832a41b4f153d83f2062d88c72b5e79997072594e97e51"},
{file = "matplotlib-3.9.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbe196377a8248972f5cede786d4c5508ed5f5ca4a1e09b44bda889958b33f8c"},
{file = "matplotlib-3.9.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5816b1e1fe8c192cbc013f8f3e3368ac56fbecf02fb41b8f8559303f24c5015e"},
{file = "matplotlib-3.9.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:cef2a73d06601437be399908cf13aee74e86932a5ccc6ccdf173408ebc5f6bb2"},
{file = "matplotlib-3.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e0830e188029c14e891fadd99702fd90d317df294c3298aad682739c5533721a"},
{file = "matplotlib-3.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ba9c1299c920964e8d3857ba27173b4dbb51ca4bab47ffc2c2ba0eb5e2cbc5"},
{file = "matplotlib-3.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1cd93b91ab47a3616b4d3c42b52f8363b88ca021e340804c6ab2536344fad9ca"},
{file = "matplotlib-3.9.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6d1ce5ed2aefcdce11904fc5bbea7d9c21fff3d5f543841edf3dea84451a09ea"},
{file = "matplotlib-3.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:b2696efdc08648536efd4e1601b5fd491fd47f4db97a5fbfd175549a7365c1b2"},
{file = "matplotlib-3.9.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d52a3b618cb1cbb769ce2ee1dcdb333c3ab6e823944e9a2d36e37253815f9556"},
{file = "matplotlib-3.9.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:039082812cacd6c6bec8e17a9c1e6baca230d4116d522e81e1f63a74d01d2e21"},
{file = "matplotlib-3.9.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6758baae2ed64f2331d4fd19be38b7b4eae3ecec210049a26b6a4f3ae1c85dcc"},
{file = "matplotlib-3.9.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:050598c2b29e0b9832cde72bcf97627bf00262adbc4a54e2b856426bb2ef0697"},
{file = "matplotlib-3.9.2.tar.gz", hash = "sha256:96ab43906269ca64a6366934106fa01534454a69e471b7bf3d79083981aaab92"},
]
[package.dependencies]
contourpy = ">=1.0.1"
cycler = ">=0.10"
fonttools = ">=4.22.0"
kiwisolver = ">=1.3.1"
numpy = ">=1.23"
packaging = ">=20.0"
pillow = ">=8"
pyparsing = ">=2.3.1"
python-dateutil = ">=2.7"
[package.extras]
dev = ["meson-python (>=0.13.1)", "numpy (>=1.25)", "pybind11 (>=2.6)", "setuptools (>=64)", "setuptools_scm (>=7)"]
[[package]]
name = "numpy"
version = "2.1.3"
description = "Fundamental package for array computing in Python"
optional = false
python-versions = ">=3.10"
files = [
{file = "numpy-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c894b4305373b9c5576d7a12b473702afdf48ce5369c074ba304cc5ad8730dff"},
{file = "numpy-2.1.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b47fbb433d3260adcd51eb54f92a2ffbc90a4595f8970ee00e064c644ac788f5"},
{file = "numpy-2.1.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:825656d0743699c529c5943554d223c021ff0494ff1442152ce887ef4f7561a1"},
{file = "numpy-2.1.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:6a4825252fcc430a182ac4dee5a505053d262c807f8a924603d411f6718b88fd"},
{file = "numpy-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e711e02f49e176a01d0349d82cb5f05ba4db7d5e7e0defd026328e5cfb3226d3"},
{file = "numpy-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78574ac2d1a4a02421f25da9559850d59457bac82f2b8d7a44fe83a64f770098"},
{file = "numpy-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c7662f0e3673fe4e832fe07b65c50342ea27d989f92c80355658c7f888fcc83c"},
{file = "numpy-2.1.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fa2d1337dc61c8dc417fbccf20f6d1e139896a30721b7f1e832b2bb6ef4eb6c4"},
{file = "numpy-2.1.3-cp310-cp310-win32.whl", hash = "sha256:72dcc4a35a8515d83e76b58fdf8113a5c969ccd505c8a946759b24e3182d1f23"},
{file = "numpy-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:ecc76a9ba2911d8d37ac01de72834d8849e55473457558e12995f4cd53e778e0"},
{file = "numpy-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4d1167c53b93f1f5d8a139a742b3c6f4d429b54e74e6b57d0eff40045187b15d"},
{file = "numpy-2.1.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c80e4a09b3d95b4e1cac08643f1152fa71a0a821a2d4277334c88d54b2219a41"},
{file = "numpy-2.1.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:576a1c1d25e9e02ed7fa5477f30a127fe56debd53b8d2c89d5578f9857d03ca9"},
{file = "numpy-2.1.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:973faafebaae4c0aaa1a1ca1ce02434554d67e628b8d805e61f874b84e136b09"},
{file = "numpy-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:762479be47a4863e261a840e8e01608d124ee1361e48b96916f38b119cfda04a"},
{file = "numpy-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc6f24b3d1ecc1eebfbf5d6051faa49af40b03be1aaa781ebdadcbc090b4539b"},
{file = "numpy-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:17ee83a1f4fef3c94d16dc1802b998668b5419362c8a4f4e8a491de1b41cc3ee"},
{file = "numpy-2.1.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15cb89f39fa6d0bdfb600ea24b250e5f1a3df23f901f51c8debaa6a5d122b2f0"},
{file = "numpy-2.1.3-cp311-cp311-win32.whl", hash = "sha256:d9beb777a78c331580705326d2367488d5bc473b49a9bc3036c154832520aca9"},
{file = "numpy-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:d89dd2b6da69c4fff5e39c28a382199ddedc3a5be5390115608345dec660b9e2"},
{file = "numpy-2.1.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f55ba01150f52b1027829b50d70ef1dafd9821ea82905b63936668403c3b471e"},
{file = "numpy-2.1.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13138eadd4f4da03074851a698ffa7e405f41a0845a6b1ad135b81596e4e9958"},
{file = "numpy-2.1.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a6b46587b14b888e95e4a24d7b13ae91fa22386c199ee7b418f449032b2fa3b8"},
{file = "numpy-2.1.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:0fa14563cc46422e99daef53d725d0c326e99e468a9320a240affffe87852564"},
{file = "numpy-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8637dcd2caa676e475503d1f8fdb327bc495554e10838019651b76d17b98e512"},
{file = "numpy-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2312b2aa89e1f43ecea6da6ea9a810d06aae08321609d8dc0d0eda6d946a541b"},
{file = "numpy-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a38c19106902bb19351b83802531fea19dee18e5b37b36454f27f11ff956f7fc"},
{file = "numpy-2.1.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:02135ade8b8a84011cbb67dc44e07c58f28575cf9ecf8ab304e51c05528c19f0"},
{file = "numpy-2.1.3-cp312-cp312-win32.whl", hash = "sha256:e6988e90fcf617da2b5c78902fe8e668361b43b4fe26dbf2d7b0f8034d4cafb9"},
{file = "numpy-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:0d30c543f02e84e92c4b1f415b7c6b5326cbe45ee7882b6b77db7195fb971e3a"},
{file = "numpy-2.1.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96fe52fcdb9345b7cd82ecd34547fca4321f7656d500eca497eb7ea5a926692f"},
{file = "numpy-2.1.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f653490b33e9c3a4c1c01d41bc2aef08f9475af51146e4a7710c450cf9761598"},
{file = "numpy-2.1.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:dc258a761a16daa791081d026f0ed4399b582712e6fc887a95af09df10c5ca57"},
{file = "numpy-2.1.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:016d0f6f5e77b0f0d45d77387ffa4bb89816b57c835580c3ce8e099ef830befe"},
{file = "numpy-2.1.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c181ba05ce8299c7aa3125c27b9c2167bca4a4445b7ce73d5febc411ca692e43"},
{file = "numpy-2.1.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5641516794ca9e5f8a4d17bb45446998c6554704d888f86df9b200e66bdcce56"},
{file = "numpy-2.1.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ea4dedd6e394a9c180b33c2c872b92f7ce0f8e7ad93e9585312b0c5a04777a4a"},
{file = "numpy-2.1.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0df3635b9c8ef48bd3be5f862cf71b0a4716fa0e702155c45067c6b711ddcef"},
{file = "numpy-2.1.3-cp313-cp313-win32.whl", hash = "sha256:50ca6aba6e163363f132b5c101ba078b8cbd3fa92c7865fd7d4d62d9779ac29f"},
{file = "numpy-2.1.3-cp313-cp313-win_amd64.whl", hash = "sha256:747641635d3d44bcb380d950679462fae44f54b131be347d5ec2bce47d3df9ed"},
{file = "numpy-2.1.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:996bb9399059c5b82f76b53ff8bb686069c05acc94656bb259b1d63d04a9506f"},
{file = "numpy-2.1.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:45966d859916ad02b779706bb43b954281db43e185015df6eb3323120188f9e4"},
{file = "numpy-2.1.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:baed7e8d7481bfe0874b566850cb0b85243e982388b7b23348c6db2ee2b2ae8e"},
{file = "numpy-2.1.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a9f7f672a3388133335589cfca93ed468509cb7b93ba3105fce780d04a6576a0"},
{file = "numpy-2.1.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7aac50327da5d208db2eec22eb11e491e3fe13d22653dce51b0f4109101b408"},
{file = "numpy-2.1.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4394bc0dbd074b7f9b52024832d16e019decebf86caf909d94f6b3f77a8ee3b6"},
{file = "numpy-2.1.3-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:50d18c4358a0a8a53f12a8ba9d772ab2d460321e6a93d6064fc22443d189853f"},
{file = "numpy-2.1.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:14e253bd43fc6b37af4921b10f6add6925878a42a0c5fe83daee390bca80bc17"},
{file = "numpy-2.1.3-cp313-cp313t-win32.whl", hash = "sha256:08788d27a5fd867a663f6fc753fd7c3ad7e92747efc73c53bca2f19f8bc06f48"},
{file = "numpy-2.1.3-cp313-cp313t-win_amd64.whl", hash = "sha256:2564fbdf2b99b3f815f2107c1bbc93e2de8ee655a69c261363a1172a79a257d4"},
{file = "numpy-2.1.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4f2015dfe437dfebbfce7c85c7b53d81ba49e71ba7eadbf1df40c915af75979f"},
{file = "numpy-2.1.3-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:3522b0dfe983a575e6a9ab3a4a4dfe156c3e428468ff08ce582b9bb6bd1d71d4"},
{file = "numpy-2.1.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c006b607a865b07cd981ccb218a04fc86b600411d83d6fc261357f1c0966755d"},
{file = "numpy-2.1.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e14e26956e6f1696070788252dcdff11b4aca4c3e8bd166e0df1bb8f315a67cb"},
{file = "numpy-2.1.3.tar.gz", hash = "sha256:aa08e04e08aaf974d4458def539dece0d28146d866a39da5639596f4921fd761"},
]
[[package]]
name = "packaging"
version = "24.2"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.8"
files = [
{file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
{file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
]
[[package]]
name = "pandas"
version = "2.2.3"
description = "Powerful data structures for data analysis, time series, and statistics"
optional = false
python-versions = ">=3.9"
files = [
{file = "pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5"},
{file = "pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348"},
{file = "pandas-2.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d9c45366def9a3dd85a6454c0e7908f2b3b8e9c138f5dc38fed7ce720d8453ed"},
{file = "pandas-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86976a1c5b25ae3f8ccae3a5306e443569ee3c3faf444dfd0f41cda24667ad57"},
{file = "pandas-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8661b0238a69d7aafe156b7fa86c44b881387509653fdf857bebc5e4008ad42"},
{file = "pandas-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37e0aced3e8f539eccf2e099f65cdb9c8aa85109b0be6e93e2baff94264bdc6f"},
{file = "pandas-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:56534ce0746a58afaf7942ba4863e0ef81c9c50d3f0ae93e9497d6a41a057645"},
{file = "pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039"},
{file = "pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd"},
{file = "pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698"},
{file = "pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc"},
{file = "pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3"},
{file = "pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32"},
{file = "pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5"},
{file = "pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9"},
{file = "pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4"},
{file = "pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3"},
{file = "pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319"},
{file = "pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8"},
{file = "pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a"},
{file = "pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13"},
{file = "pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015"},
{file = "pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28"},
{file = "pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0"},
{file = "pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24"},
{file = "pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659"},
{file = "pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb"},
{file = "pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d"},
{file = "pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468"},
{file = "pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18"},
{file = "pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2"},
{file = "pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4"},
{file = "pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d"},
{file = "pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a"},
{file = "pandas-2.2.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc6b93f9b966093cb0fd62ff1a7e4c09e6d546ad7c1de191767baffc57628f39"},
{file = "pandas-2.2.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5dbca4c1acd72e8eeef4753eeca07de9b1db4f398669d5994086f788a5d7cc30"},
{file = "pandas-2.2.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8cd6d7cc958a3910f934ea8dbdf17b2364827bb4dafc38ce6eef6bb3d65ff09c"},
{file = "pandas-2.2.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99df71520d25fade9db7c1076ac94eb994f4d2673ef2aa2e86ee039b6746d20c"},
{file = "pandas-2.2.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:31d0ced62d4ea3e231a9f228366919a5ea0b07440d9d4dac345376fd8e1477ea"},
{file = "pandas-2.2.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7eee9e7cea6adf3e3d24e304ac6b8300646e2a5d1cd3a3c2abed9101b0846761"},
{file = "pandas-2.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:4850ba03528b6dd51d6c5d273c46f183f39a9baf3f0143e566b89450965b105e"},
{file = "pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667"},
]
[package.dependencies]
numpy = {version = ">=1.26.0", markers = "python_version >= \"3.12\""}
python-dateutil = ">=2.8.2"
pytz = ">=2020.1"
tzdata = ">=2022.7"
[package.extras]
all = ["PyQt5 (>=5.15.9)", "SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)", "beautifulsoup4 (>=4.11.2)", "bottleneck (>=1.3.6)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=2022.12.0)", "fsspec (>=2022.11.0)", "gcsfs (>=2022.11.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.9.2)", "matplotlib (>=3.6.3)", "numba (>=0.56.4)", "numexpr (>=2.8.4)", "odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "pandas-gbq (>=0.19.0)", "psycopg2 (>=2.9.6)", "pyarrow (>=10.0.1)", "pymysql (>=1.0.2)", "pyreadstat (>=1.2.0)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "qtpy (>=2.3.0)", "s3fs (>=2022.11.0)", "scipy (>=1.10.0)", "tables (>=3.8.0)", "tabulate (>=0.9.0)", "xarray (>=2022.12.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)", "zstandard (>=0.19.0)"]
aws = ["s3fs (>=2022.11.0)"]
clipboard = ["PyQt5 (>=5.15.9)", "qtpy (>=2.3.0)"]
compression = ["zstandard (>=0.19.0)"]
computation = ["scipy (>=1.10.0)", "xarray (>=2022.12.0)"]
consortium-standard = ["dataframe-api-compat (>=0.1.7)"]
excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)"]
feather = ["pyarrow (>=10.0.1)"]
fss = ["fsspec (>=2022.11.0)"]
gcp = ["gcsfs (>=2022.11.0)", "pandas-gbq (>=0.19.0)"]
hdf5 = ["tables (>=3.8.0)"]
html = ["beautifulsoup4 (>=4.11.2)", "html5lib (>=1.1)", "lxml (>=4.9.2)"]
mysql = ["SQLAlchemy (>=2.0.0)", "pymysql (>=1.0.2)"]
output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.9.0)"]
parquet = ["pyarrow (>=10.0.1)"]
performance = ["bottleneck (>=1.3.6)", "numba (>=0.56.4)", "numexpr (>=2.8.4)"]
plot = ["matplotlib (>=3.6.3)"]
postgresql = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "psycopg2 (>=2.9.6)"]
pyarrow = ["pyarrow (>=10.0.1)"]
spss = ["pyreadstat (>=1.2.0)"]
sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)"]
test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"]
xml = ["lxml (>=4.9.2)"]
[[package]]
name = "pillow"
version = "11.0.0"
description = "Python Imaging Library (Fork)"
optional = false
python-versions = ">=3.9"
files = [
{file = "pillow-11.0.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:6619654954dc4936fcff82db8eb6401d3159ec6be81e33c6000dfd76ae189947"},
{file = "pillow-11.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b3c5ac4bed7519088103d9450a1107f76308ecf91d6dabc8a33a2fcfb18d0fba"},
{file = "pillow-11.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a65149d8ada1055029fcb665452b2814fe7d7082fcb0c5bed6db851cb69b2086"},
{file = "pillow-11.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88a58d8ac0cc0e7f3a014509f0455248a76629ca9b604eca7dc5927cc593c5e9"},
{file = "pillow-11.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:c26845094b1af3c91852745ae78e3ea47abf3dbcd1cf962f16b9a5fbe3ee8488"},
{file = "pillow-11.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:1a61b54f87ab5786b8479f81c4b11f4d61702830354520837f8cc791ebba0f5f"},
{file = "pillow-11.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:674629ff60030d144b7bca2b8330225a9b11c482ed408813924619c6f302fdbb"},
{file = "pillow-11.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:598b4e238f13276e0008299bd2482003f48158e2b11826862b1eb2ad7c768b97"},
{file = "pillow-11.0.0-cp310-cp310-win32.whl", hash = "sha256:9a0f748eaa434a41fccf8e1ee7a3eed68af1b690e75328fd7a60af123c193b50"},
{file = "pillow-11.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:a5629742881bcbc1f42e840af185fd4d83a5edeb96475a575f4da50d6ede337c"},
{file = "pillow-11.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:ee217c198f2e41f184f3869f3e485557296d505b5195c513b2bfe0062dc537f1"},
{file = "pillow-11.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1c1d72714f429a521d8d2d018badc42414c3077eb187a59579f28e4270b4b0fc"},
{file = "pillow-11.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:499c3a1b0d6fc8213519e193796eb1a86a1be4b1877d678b30f83fd979811d1a"},
{file = "pillow-11.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8b2351c85d855293a299038e1f89db92a2f35e8d2f783489c6f0b2b5f3fe8a3"},
{file = "pillow-11.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f4dba50cfa56f910241eb7f883c20f1e7b1d8f7d91c750cd0b318bad443f4d5"},
{file = "pillow-11.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5ddbfd761ee00c12ee1be86c9c0683ecf5bb14c9772ddbd782085779a63dd55b"},
{file = "pillow-11.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:45c566eb10b8967d71bf1ab8e4a525e5a93519e29ea071459ce517f6b903d7fa"},
{file = "pillow-11.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b4fd7bd29610a83a8c9b564d457cf5bd92b4e11e79a4ee4716a63c959699b306"},
{file = "pillow-11.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cb929ca942d0ec4fac404cbf520ee6cac37bf35be479b970c4ffadf2b6a1cad9"},
{file = "pillow-11.0.0-cp311-cp311-win32.whl", hash = "sha256:006bcdd307cc47ba43e924099a038cbf9591062e6c50e570819743f5607404f5"},
{file = "pillow-11.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:52a2d8323a465f84faaba5236567d212c3668f2ab53e1c74c15583cf507a0291"},
{file = "pillow-11.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:16095692a253047fe3ec028e951fa4221a1f3ed3d80c397e83541a3037ff67c9"},
{file = "pillow-11.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2c0a187a92a1cb5ef2c8ed5412dd8d4334272617f532d4ad4de31e0495bd923"},
{file = "pillow-11.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:084a07ef0821cfe4858fe86652fffac8e187b6ae677e9906e192aafcc1b69903"},
{file = "pillow-11.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8069c5179902dcdce0be9bfc8235347fdbac249d23bd90514b7a47a72d9fecf4"},
{file = "pillow-11.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f02541ef64077f22bf4924f225c0fd1248c168f86e4b7abdedd87d6ebaceab0f"},
{file = "pillow-11.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:fcb4621042ac4b7865c179bb972ed0da0218a076dc1820ffc48b1d74c1e37fe9"},
{file = "pillow-11.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:00177a63030d612148e659b55ba99527803288cea7c75fb05766ab7981a8c1b7"},
{file = "pillow-11.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8853a3bf12afddfdf15f57c4b02d7ded92c7a75a5d7331d19f4f9572a89c17e6"},
{file = "pillow-11.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3107c66e43bda25359d5ef446f59c497de2b5ed4c7fdba0894f8d6cf3822dafc"},
{file = "pillow-11.0.0-cp312-cp312-win32.whl", hash = "sha256:86510e3f5eca0ab87429dd77fafc04693195eec7fd6a137c389c3eeb4cfb77c6"},
{file = "pillow-11.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:8ec4a89295cd6cd4d1058a5e6aec6bf51e0eaaf9714774e1bfac7cfc9051db47"},
{file = "pillow-11.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:27a7860107500d813fcd203b4ea19b04babe79448268403172782754870dac25"},
{file = "pillow-11.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bcd1fb5bb7b07f64c15618c89efcc2cfa3e95f0e3bcdbaf4642509de1942a699"},
{file = "pillow-11.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0e038b0745997c7dcaae350d35859c9715c71e92ffb7e0f4a8e8a16732150f38"},
{file = "pillow-11.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ae08bd8ffc41aebf578c2af2f9d8749d91f448b3bfd41d7d9ff573d74f2a6b2"},
{file = "pillow-11.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d69bfd8ec3219ae71bcde1f942b728903cad25fafe3100ba2258b973bd2bc1b2"},
{file = "pillow-11.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:61b887f9ddba63ddf62fd02a3ba7add935d053b6dd7d58998c630e6dbade8527"},
{file = "pillow-11.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:c6a660307ca9d4867caa8d9ca2c2658ab685de83792d1876274991adec7b93fa"},
{file = "pillow-11.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:73e3a0200cdda995c7e43dd47436c1548f87a30bb27fb871f352a22ab8dcf45f"},
{file = "pillow-11.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fba162b8872d30fea8c52b258a542c5dfd7b235fb5cb352240c8d63b414013eb"},
{file = "pillow-11.0.0-cp313-cp313-win32.whl", hash = "sha256:f1b82c27e89fffc6da125d5eb0ca6e68017faf5efc078128cfaa42cf5cb38798"},
{file = "pillow-11.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ba470552b48e5835f1d23ecb936bb7f71d206f9dfeee64245f30c3270b994de"},
{file = "pillow-11.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:846e193e103b41e984ac921b335df59195356ce3f71dcfd155aa79c603873b84"},
{file = "pillow-11.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4ad70c4214f67d7466bea6a08061eba35c01b1b89eaa098040a35272a8efb22b"},
{file = "pillow-11.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6ec0d5af64f2e3d64a165f490d96368bb5dea8b8f9ad04487f9ab60dc4bb6003"},
{file = "pillow-11.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c809a70e43c7977c4a42aefd62f0131823ebf7dd73556fa5d5950f5b354087e2"},
{file = "pillow-11.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:4b60c9520f7207aaf2e1d94de026682fc227806c6e1f55bba7606d1c94dd623a"},
{file = "pillow-11.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1e2688958a840c822279fda0086fec1fdab2f95bf2b717b66871c4ad9859d7e8"},
{file = "pillow-11.0.0-cp313-cp313t-win32.whl", hash = "sha256:607bbe123c74e272e381a8d1957083a9463401f7bd01287f50521ecb05a313f8"},
{file = "pillow-11.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c39ed17edea3bc69c743a8dd3e9853b7509625c2462532e62baa0732163a904"},
{file = "pillow-11.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:75acbbeb05b86bc53cbe7b7e6fe00fbcf82ad7c684b3ad82e3d711da9ba287d3"},
{file = "pillow-11.0.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2e46773dc9f35a1dd28bd6981332fd7f27bec001a918a72a79b4133cf5291dba"},
{file = "pillow-11.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2679d2258b7f1192b378e2893a8a0a0ca472234d4c2c0e6bdd3380e8dfa21b6a"},
{file = "pillow-11.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eda2616eb2313cbb3eebbe51f19362eb434b18e3bb599466a1ffa76a033fb916"},
{file = "pillow-11.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20ec184af98a121fb2da42642dea8a29ec80fc3efbaefb86d8fdd2606619045d"},
{file = "pillow-11.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:8594f42df584e5b4bb9281799698403f7af489fba84c34d53d1c4bfb71b7c4e7"},
{file = "pillow-11.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:c12b5ae868897c7338519c03049a806af85b9b8c237b7d675b8c5e089e4a618e"},
{file = "pillow-11.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:70fbbdacd1d271b77b7721fe3cdd2d537bbbd75d29e6300c672ec6bb38d9672f"},
{file = "pillow-11.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5178952973e588b3f1360868847334e9e3bf49d19e169bbbdfaf8398002419ae"},
{file = "pillow-11.0.0-cp39-cp39-win32.whl", hash = "sha256:8c676b587da5673d3c75bd67dd2a8cdfeb282ca38a30f37950511766b26858c4"},
{file = "pillow-11.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:94f3e1780abb45062287b4614a5bc0874519c86a777d4a7ad34978e86428b8dd"},
{file = "pillow-11.0.0-cp39-cp39-win_arm64.whl", hash = "sha256:290f2cc809f9da7d6d622550bbf4c1e57518212da51b6a30fe8e0a270a5b78bd"},
{file = "pillow-11.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1187739620f2b365de756ce086fdb3604573337cc28a0d3ac4a01ab6b2d2a6d2"},
{file = "pillow-11.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fbbcb7b57dc9c794843e3d1258c0fbf0f48656d46ffe9e09b63bbd6e8cd5d0a2"},
{file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d203af30149ae339ad1b4f710d9844ed8796e97fda23ffbc4cc472968a47d0b"},
{file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21a0d3b115009ebb8ac3d2ebec5c2982cc693da935f4ab7bb5c8ebe2f47d36f2"},
{file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:73853108f56df97baf2bb8b522f3578221e56f646ba345a372c78326710d3830"},
{file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e58876c91f97b0952eb766123bfef372792ab3f4e3e1f1a2267834c2ab131734"},
{file = "pillow-11.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:224aaa38177597bb179f3ec87eeefcce8e4f85e608025e9cfac60de237ba6316"},
{file = "pillow-11.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5bd2d3bdb846d757055910f0a59792d33b555800813c3b39ada1829c372ccb06"},
{file = "pillow-11.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:375b8dd15a1f5d2feafff536d47e22f69625c1aa92f12b339ec0b2ca40263273"},
{file = "pillow-11.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:daffdf51ee5db69a82dd127eabecce20729e21f7a3680cf7cbb23f0829189790"},
{file = "pillow-11.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7326a1787e3c7b0429659e0a944725e1b03eeaa10edd945a86dead1913383944"},
{file = "pillow-11.0.0.tar.gz", hash = "sha256:72bacbaf24ac003fea9bff9837d1eedb6088758d41e100c1552930151f677739"},
]
[package.extras]
docs = ["furo", "olefile", "sphinx (>=8.1)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"]
fpx = ["olefile"]
mic = ["olefile"]
tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"]
typing = ["typing-extensions"]
xmp = ["defusedxml"]
[[package]]
name = "pyparsing"
version = "3.2.0"
description = "pyparsing module - Classes and methods to define and execute parsing grammars"
optional = false
python-versions = ">=3.9"
files = [
{file = "pyparsing-3.2.0-py3-none-any.whl", hash = "sha256:93d9577b88da0bbea8cc8334ee8b918ed014968fd2ec383e868fb8afb1ccef84"},
{file = "pyparsing-3.2.0.tar.gz", hash = "sha256:cbf74e27246d595d9a74b186b810f6fbb86726dbf3b9532efb343f6d7294fe9c"},
]
[package.extras]
diagrams = ["jinja2", "railroad-diagrams"]
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
description = "Extensions to the standard Python datetime module"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
files = [
{file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
{file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
]
[package.dependencies]
six = ">=1.5"
[[package]]
name = "pytz"
version = "2024.2"
description = "World timezone definitions, modern and historical"
optional = false
python-versions = "*"
files = [
{file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"},
{file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"},
]
[[package]]
name = "scipy"
version = "1.14.1"
description = "Fundamental algorithms for scientific computing in Python"
optional = false
python-versions = ">=3.10"
files = [
{file = "scipy-1.14.1-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:b28d2ca4add7ac16ae8bb6632a3c86e4b9e4d52d3e34267f6e1b0c1f8d87e389"},
{file = "scipy-1.14.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:d0d2821003174de06b69e58cef2316a6622b60ee613121199cb2852a873f8cf3"},
{file = "scipy-1.14.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8bddf15838ba768bb5f5083c1ea012d64c9a444e16192762bd858f1e126196d0"},
{file = "scipy-1.14.1-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:97c5dddd5932bd2a1a31c927ba5e1463a53b87ca96b5c9bdf5dfd6096e27efc3"},
{file = "scipy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ff0a7e01e422c15739ecd64432743cf7aae2b03f3084288f399affcefe5222d"},
{file = "scipy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e32dced201274bf96899e6491d9ba3e9a5f6b336708656466ad0522d8528f69"},
{file = "scipy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8426251ad1e4ad903a4514712d2fa8fdd5382c978010d1c6f5f37ef286a713ad"},
{file = "scipy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:a49f6ed96f83966f576b33a44257d869756df6cf1ef4934f59dd58b25e0327e5"},
{file = "scipy-1.14.1-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:2da0469a4ef0ecd3693761acbdc20f2fdeafb69e6819cc081308cc978153c675"},
{file = "scipy-1.14.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:c0ee987efa6737242745f347835da2cc5bb9f1b42996a4d97d5c7ff7928cb6f2"},
{file = "scipy-1.14.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3a1b111fac6baec1c1d92f27e76511c9e7218f1695d61b59e05e0fe04dc59617"},
{file = "scipy-1.14.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8475230e55549ab3f207bff11ebfc91c805dc3463ef62eda3ccf593254524ce8"},
{file = "scipy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:278266012eb69f4a720827bdd2dc54b2271c97d84255b2faaa8f161a158c3b37"},
{file = "scipy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fef8c87f8abfb884dac04e97824b61299880c43f4ce675dd2cbeadd3c9b466d2"},
{file = "scipy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b05d43735bb2f07d689f56f7b474788a13ed8adc484a85aa65c0fd931cf9ccd2"},
{file = "scipy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:716e389b694c4bb564b4fc0c51bc84d381735e0d39d3f26ec1af2556ec6aad94"},
{file = "scipy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:631f07b3734d34aced009aaf6fedfd0eb3498a97e581c3b1e5f14a04164a456d"},
{file = "scipy-1.14.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:af29a935803cc707ab2ed7791c44288a682f9c8107bc00f0eccc4f92c08d6e07"},
{file = "scipy-1.14.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2843f2d527d9eebec9a43e6b406fb7266f3af25a751aa91d62ff416f54170bc5"},
{file = "scipy-1.14.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:eb58ca0abd96911932f688528977858681a59d61a7ce908ffd355957f7025cfc"},
{file = "scipy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30ac8812c1d2aab7131a79ba62933a2a76f582d5dbbc695192453dae67ad6310"},
{file = "scipy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f9ea80f2e65bdaa0b7627fb00cbeb2daf163caa015e59b7516395fe3bd1e066"},
{file = "scipy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:edaf02b82cd7639db00dbff629995ef185c8df4c3ffa71a5562a595765a06ce1"},
{file = "scipy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:2ff38e22128e6c03ff73b6bb0f85f897d2362f8c052e3b8ad00532198fbdae3f"},
{file = "scipy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1729560c906963fc8389f6aac023739ff3983e727b1a4d87696b7bf108316a79"},
{file = "scipy-1.14.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:4079b90df244709e675cdc8b93bfd8a395d59af40b72e339c2287c91860deb8e"},
{file = "scipy-1.14.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e0cf28db0f24a38b2a0ca33a85a54852586e43cf6fd876365c86e0657cfe7d73"},
{file = "scipy-1.14.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0c2f95de3b04e26f5f3ad5bb05e74ba7f68b837133a4492414b3afd79dfe540e"},
{file = "scipy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b99722ea48b7ea25e8e015e8341ae74624f72e5f21fc2abd45f3a93266de4c5d"},
{file = "scipy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5149e3fd2d686e42144a093b206aef01932a0059c2a33ddfa67f5f035bdfe13e"},
{file = "scipy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4f5a7c49323533f9103d4dacf4e4f07078f360743dec7f7596949149efeec06"},
{file = "scipy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:baff393942b550823bfce952bb62270ee17504d02a1801d7fd0719534dfb9c84"},
{file = "scipy-1.14.1.tar.gz", hash = "sha256:5a275584e726026a5699459aa72f828a610821006228e841b94275c4a7c08417"},
]
[package.dependencies]
numpy = ">=1.23.5,<2.3"
[package.extras]
dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy (==1.10.0)", "pycodestyle", "pydevtool", "rich-click", "ruff (>=0.0.292)", "types-psutil", "typing_extensions"]
doc = ["jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.13.1)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0,<=7.3.7)", "sphinx-design (>=0.4.0)"]
test = ["Cython", "array-api-strict (>=2.0)", "asv", "gmpy2", "hypothesis (>=6.30)", "meson", "mpmath", "ninja", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"]
[[package]]
name = "seaborn"
version = "0.13.2"
description = "Statistical data visualization"
optional = false
python-versions = ">=3.8"
files = [
{file = "seaborn-0.13.2-py3-none-any.whl", hash = "sha256:636f8336facf092165e27924f223d3c62ca560b1f2bb5dff7ab7fad265361987"},
{file = "seaborn-0.13.2.tar.gz", hash = "sha256:93e60a40988f4d65e9f4885df477e2fdaff6b73a9ded434c1ab356dd57eefff7"},
]
[package.dependencies]
matplotlib = ">=3.4,<3.6.1 || >3.6.1"
numpy = ">=1.20,<1.24.0 || >1.24.0"
pandas = ">=1.2"
[package.extras]
dev = ["flake8", "flit", "mypy", "pandas-stubs", "pre-commit", "pytest", "pytest-cov", "pytest-xdist"]
docs = ["ipykernel", "nbconvert", "numpydoc", "pydata_sphinx_theme (==0.10.0rc2)", "pyyaml", "sphinx (<6.0.0)", "sphinx-copybutton", "sphinx-design", "sphinx-issues"]
stats = ["scipy (>=1.7)", "statsmodels (>=0.12)"]
[[package]]
name = "six"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
files = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
[[package]]
name = "tzdata"
version = "2024.2"
description = "Provider of IANA time zone data"
optional = false
python-versions = ">=2"
files = [
{file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"},
{file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"},
]
[metadata]
lock-version = "2.0"
python-versions = "^3.13"
content-hash = "376b98659bcfe622090d838b23f142eaa76c6441ace6ca751db716db53d9af63"
+20 -14
View File
@@ -1,18 +1,24 @@
[tool.poetry]
name = "bitcoin-model"
version = "0.1.0"
description = ""
authors = ["Anthropic Claude <[email protected]>", "Sam Fredrickson <[email protected]>"]
[project]
name = "btcmodel"
version = "0.2.0"
description = "Probabilistic Bitcoin price model"
authors = [
{ name = "Anthropic Claude", email = "[email protected]" },
{ name = "Sam Fredrickson", email = "[email protected]" },
]
readme = "README.md"
requires-python = ">=3.13"
dependencies = ["numpy>=2", "pandas>=3", "scipy>=1.14", "matplotlib>=3.9"]
[tool.poetry.dependencies]
python = "^3.13"
pandas = "^2.2.3"
matplotlib = "^3.9.2"
seaborn = "^0.13.2"
scipy = "^1.14.1"
[dependency-groups]
dev = ["pytest", "ruff"]
[tool.ruff]
line-length = 100
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[tool.ruff.lint]
select = ["E", "F", "I", "B", "UP"]
[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["."]
+44
View File
@@ -0,0 +1,44 @@
import numpy as np
import pandas as pd
from scipy.stats import norm
from btcmodel.forecast import Forecast, crps, pit
def normal_crps(mu, sigma, y):
"""Closed-form CRPS of N(mu, sigma^2) at y."""
z = (y - mu) / sigma
return sigma * (z * (2 * norm.cdf(z) - 1) + 2 * norm.pdf(z) - 1 / np.sqrt(np.pi))
def test_crps_matches_closed_form_for_normal():
f = Forecast.normal("2020-01-01", np.array([1, 2, 3]), mean=[0.0, 1.0, 2.0], sd=[1.0, 0.5, 2.0])
y = np.array([0.3, -0.2, 5.0])
expected = normal_crps(np.array([0.0, 1.0, 2.0]), np.array([1.0, 0.5, 2.0]), y)
np.testing.assert_allclose(crps(f.log_quantiles, y), expected, rtol=0.02)
def test_crps_prefers_the_right_forecast():
y = np.array([0.0])
good = Forecast.normal("2020-01-01", np.array([1]), 0.0, 0.1)
biased = Forecast.normal("2020-01-01", np.array([1]), 0.5, 0.1)
vague = Forecast.normal("2020-01-01", np.array([1]), 0.0, 2.0)
assert crps(good.log_quantiles, y) < crps(biased.log_quantiles, y)
assert crps(good.log_quantiles, y) < crps(vague.log_quantiles, y)
def test_pit_and_intervals():
f = Forecast.normal("2020-01-01", np.array([1, 1, 1]), 0.0, 1.0)
np.testing.assert_allclose(
pit(f.log_quantiles, [0.0, 1.0, -10.0]), [0.5, 0.841, 0.0], atol=0.01
)
lo, hi = f.interval(0.95)
np.testing.assert_allclose(hi, 1.96, atol=0.01)
np.testing.assert_allclose(lo, -1.96, atol=0.01)
def test_from_samples_recovers_quantiles():
rng = np.random.default_rng(0)
f = Forecast.from_samples("2020-01-01", np.array([10]), rng.normal(0, 1, size=(200_000, 1)))
np.testing.assert_allclose(f.quantile(0.5), 0.0, atol=0.02)
assert f.dates[0] == pd.Timestamp("2020-01-11")
+83
View File
@@ -0,0 +1,83 @@
import numpy as np
import pandas as pd
import pytest
from btcmodel import data
from btcmodel.evaluate import backtest
from btcmodel.halving import HALVINGS, cycle_position
from btcmodel.models import MODELS, CycleModel
def synthetic_prices(daily_return, start="2011-01-01", end="2024-11-26", noise=0.0, seed=0):
"""Prices whose daily log return is `daily_return(cycle_day)` plus optional noise."""
dates = pd.date_range(start, end, freq="D", name="date")
_, day = cycle_position(dates)
r = daily_return(day) + noise * np.random.default_rng(seed).standard_normal(len(dates))
return pd.DataFrame({"close": 100 * np.exp(np.cumsum(r))}, index=dates)
def test_cycle_position():
index, day = cycle_position(pd.DatetimeIndex(["2009-01-03", "2012-11-27", *HALVINGS]))
assert list(index) == [0, 0, 1, 2, 3, 4]
assert list(day) == [0, 1424, 0, 0, 0, 0]
# A projected halving about four years after the last one starts cycle 5.
index, _ = cycle_position(pd.DatetimeIndex(["2028-06-01"]))
assert index[0] == 5
def test_cycle_model_recovers_a_cycle_shaped_drift():
# Up for the first half of each cycle, down in the second half.
def shape(day):
return np.where(day < 700, 0.002, -0.001)
prices = synthetic_prices(shape)
drift = CycleModel(prior_days=0).drift_by_cycle_day(prices)
assert drift[300] == pytest.approx(0.002, abs=2e-4)
assert drift[1100] == pytest.approx(-0.001, abs=2e-4)
def test_cycle_model_weights_recent_cycles_more():
# The same day of the cycle returns less in each later cycle.
prices = synthetic_prices(lambda day: np.zeros_like(day, dtype=float))
cycle, _ = cycle_position(prices.index)
r = 0.004 / 2.0**cycle
prices["close"] = 100 * np.exp(np.cumsum(r))
drift = CycleModel(recency_half_life=0.25).drift_by_cycle_day(prices)
equal = CycleModel(recency_half_life=1e9).drift_by_cycle_day(prices)
latest_complete = 0.004 / 2.0**3
assert abs(drift[900] - latest_complete) < abs(equal[900] - latest_complete)
@pytest.mark.parametrize("name", list(MODELS))
def test_models_produce_valid_forecasts(name):
prices = synthetic_prices(lambda day: 0.001 + 0 * day, noise=0.03)
horizons = np.array([1, 30, 365, 1460])
f = MODELS[name].forecast(prices, horizons)
assert f.log_quantiles.shape == (4, 100)
assert np.all(np.diff(f.log_quantiles, axis=1) >= 0)
lo, hi = f.interval(0.8)
assert np.all(np.diff(hi - lo) > 0), "uncertainty should grow with horizon"
def test_backtest_never_shows_models_the_future():
prices = synthetic_prices(lambda day: 0.001 + 0 * day, noise=0.03)
class Spy:
name = "random_walk"
def forecast(self, history, horizons):
origin = history.index[-1]
assert history.index.max() == origin
assert (origin + pd.Timedelta(days=int(horizons.min()))) > history.index.max()
return MODELS["random_walk"].forecast(history, horizons)
scores = backtest([Spy()], prices)
assert len(scores) > 0
targets = scores.origin + pd.to_timedelta(scores.horizon, unit="D")
assert targets.max() <= prices.index[-1]
def test_development_data_stops_at_cutoff():
prices = data.load_prices(until=data.DEV_CUTOFF)
assert prices.index[0] == data.DATA_START
assert prices.index[-1] == data.DEV_CUTOFF