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:
+6
-2
@@ -1,4 +1,8 @@
|
||||
bitcoin_*.png
|
||||
bitcoin_*.txt
|
||||
/output
|
||||
__pycache__/
|
||||
.pytest_cache/
|
||||
.ruff_cache/
|
||||
.venv/
|
||||
result
|
||||
scratch.py
|
||||
.DS_Store
|
||||
|
||||
@@ -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
|
||||
@@ -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...)
|
||||

|
||||
|
||||
## 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 |
|
||||
|
||||

|
||||
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).
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
]
|
||||
@@ -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"
|
||||
@@ -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)
|
||||
]
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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))
|
||||
@@ -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))
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
|
Before Width: | Height: | Size: 783 KiB After Width: | Height: | Size: 783 KiB |
Generated
+27
@@ -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
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Generated
-734
@@ -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
@@ -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 = ["."]
|
||||
|
||||
@@ -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")
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user