Compare commits
50
Commits
ea8c2092a6
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51c321ccba | ||
|
|
67d016fe25 | ||
|
|
082bcfbbbc | ||
|
|
b0243adf61 | ||
|
|
cfc27a38de | ||
|
|
eefff47070 | ||
|
|
f89fd3bb4e | ||
|
|
8d2d351bbb | ||
|
|
525d3ba634 | ||
|
|
49872447ab | ||
|
|
2a13b4ef46 | ||
|
|
66d7e4329e | ||
|
|
3869ced1ff | ||
|
|
7223144b13 | ||
|
|
f77cc955db | ||
|
|
96fdf5a88e | ||
|
|
d2ac5749dc | ||
|
|
7211ed1723 | ||
|
|
84464151be | ||
|
|
ca69495071 | ||
|
|
41c62e1af2 | ||
|
|
3158d7479d | ||
|
|
50f3f76830 | ||
|
|
56f669ac0f | ||
|
|
06f5620cb3 | ||
|
|
5679d99ece | ||
|
|
3d6397da21 | ||
|
|
f023f7e999 | ||
|
|
453df222d6 | ||
|
|
47b2ce1476 | ||
|
|
db45b14882 | ||
|
|
bbd7d493b7 | ||
|
|
f5bed410ae | ||
|
|
1e8c6ba75e | ||
|
|
e484331196 | ||
|
|
b9cf04aed7 | ||
|
|
2b7abecdcb | ||
|
|
502b47842e | ||
|
|
368878a482 | ||
|
|
294c665106 | ||
|
|
4975e1647d | ||
|
|
8f94c60034 | ||
|
|
b024a38040 | ||
|
|
b018296a51 | ||
|
|
5b80eac207 | ||
|
|
16ce8823f3 | ||
|
|
f9b8af828a | ||
|
|
36eda3eb3e | ||
|
|
588c4a8fa4 | ||
|
|
3e96a320b9 |
+6
-3
@@ -1,5 +1,8 @@
|
|||||||
bitcoin_*.png
|
|
||||||
bitcoin_*.txt
|
|
||||||
/output
|
/output
|
||||||
|
__pycache__/
|
||||||
|
.pytest_cache/
|
||||||
|
.ruff_cache/
|
||||||
|
.venv/
|
||||||
|
result
|
||||||
scratch.py
|
scratch.py
|
||||||
/.private
|
.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,178 @@
|
|||||||
|
|
||||||
**Don't take this seriously. It's all in good fun.**
|
**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
|
## The 2024 edition
|
||||||
a Bitcoin price model, using data from [Investing.com](https://www.investing.com/crypto/bitcoin/historical-data).
|
|
||||||
|
|
||||||
The model is still a mess: lots of redundant code as we went through various
|
In November 2024 I asked Claude (3.6 Sonnet, via copy and paste in Claude Web)
|
||||||
methods for projecting future prices; the plots' colors don't render correctly.
|
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.
|
||||||
|
- `powerlaw`: log price grows linearly in log time since genesis, so growth
|
||||||
|
keeps slowing. Fitted walk-forward; the exponent has stayed between 5.4 and
|
||||||
|
6.0 in every fit since 2014.
|
||||||
|
|
||||||
|
### Findings so far (development data)
|
||||||
|
|
||||||
|
- `cycle` loses to the random walk at every horizon, and so does every setting
|
||||||
|
tried (recency half-life 0.25-2 cycles, smoothing bandwidth 15-60 days).
|
||||||
|
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.
|
||||||
|
- `powerlaw` models exactly that, and it is the first model to beat the random
|
||||||
|
walk with some confidence: +53% and +63% skill at 3 and 4 years, with
|
||||||
|
unbiased outcomes (mean PIT 0.51). Only 3-4 independent windows back that
|
||||||
|
up, the functional form is famous *because* it fits Bitcoin's history, and
|
||||||
|
the holdout hasn't been run yet.
|
||||||
|
- Its intervals are too wide at long horizons (the 80% interval held every
|
||||||
|
3-year outcome), because it treats deviations from the trend as permanent.
|
||||||
|
|
||||||
|
### A/B tests of the 2024 ideas
|
||||||
|
|
||||||
|
`just ab` runs the experiments in `btcmodel/experiments.py`: ideas salvaged
|
||||||
|
from the old branches (catalogued in [docs/2024-ideas.md](docs/2024-ideas.md)),
|
||||||
|
each a control plus variants that change one component. Hypotheses and the
|
||||||
|
verdict rule were written down before anything ran. With ~100 comparisons,
|
||||||
|
expect a few flukes either way.
|
||||||
|
|
||||||
|
| Experiment | Idea | Verdict |
|
||||||
|
|---|---|---|
|
||||||
|
| shrink-cycle | scale the cycle drift by 0.25/0.5/0.75 | better, all three: it fixes the level crudely |
|
||||||
|
| diminishing-returns | power-law trend, optionally reverting to it, or the cycle shape rescaled to it | better (both power-law variants); cycle shape on the power law inconclusive |
|
||||||
|
| vol-window | EWMA blends, shorter or longer windows | worse: the plain 365-day window wins |
|
||||||
|
| vol-reversion | volatility reverting to a long-run level or falling trend | worse |
|
||||||
|
| cycle-vol | volatility by cycle position | inconclusive (no effect) |
|
||||||
|
| tails | Student-t, or empirical horizon-level shape | Student-t worse; empirical +5% at 1 month only |
|
||||||
|
| cycle-phase | align cycles by fraction elapsed, not days | inconclusive |
|
||||||
|
|
||||||
|
Round 2 tested the power law against two refinements (the verdict rule was
|
||||||
|
unchanged, and the holdout candidates were fixed before it ran):
|
||||||
|
|
||||||
|
| Experiment | Idea | Verdict |
|
||||||
|
|---|---|---|
|
||||||
|
| powerlaw-ou | deviations from the trend revert, so uncertainty levels off; optionally plus trend-parameter uncertainty | inconclusive: +21% to +45% at 2-4 years (intervals above zero), but -0.3% and -1% at 1 month fail the "never negative" clause. Bands too narrow without parameter uncertainty, too wide with it |
|
||||||
|
| cycle-on-powerlaw | the cycle's timing, rescaled to the power-law level | inconclusive: +18% at 2 years, slightly negative at short horizons and 4 years |
|
||||||
|
|
||||||
|
### Holdout (run once, 2026-09-24)
|
||||||
|
|
||||||
|
Scored on outcomes after 2024-11-26 for the four candidates fixed in advance
|
||||||
|
(`random_walk`, `drift_rw`, `cycle`, `powerlaw`). The holdout is ~22 months
|
||||||
|
long, so at 2-4 years it is essentially one outcome period seen from several
|
||||||
|
origins (1.4-1.9 windows), and the bootstrap intervals there mean little.
|
||||||
|
|
||||||
|
| Skill vs random walk | 1mo | 3mo | 6mo | 1y | 2y | 3y | 4y |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| drift_rw | -0% | +1% | +5% | +14% | +31% | +53% | -65% |
|
||||||
|
| cycle | -14% | -8% | -25% | -104% | -46% | -21% | -179% |
|
||||||
|
| powerlaw | -1% | -7% | -5% | +3% | +45% | +58% | +15% |
|
||||||
|
|
||||||
|
Consistent with development: nothing beats the random walk inside a year,
|
||||||
|
`cycle` fails badly, and `powerlaw` is the best long-horizon forecast, unbiased
|
||||||
|
at 2-3 years (mean PIT 0.53-0.56) but with intervals too wide (its 80% interval
|
||||||
|
held every 2- and 3-year outcome). The holdout is now spent for these models;
|
||||||
|
a new model needs new data to be tested honestly.
|
||||||
|
|
||||||
|
### Forward test (from 2026-09-23)
|
||||||
|
|
||||||
|
New data is the only honest test left, so forecasts are now recorded before
|
||||||
|
their outcomes exist. `just snapshot` writes every tracked model's forecast
|
||||||
|
from the latest price to `data/forecasts/<date>.csv`. It refuses if the price
|
||||||
|
data is more than two days old or the date already has a snapshot, and the
|
||||||
|
files are committed so version control dates them. `just forward` scores
|
||||||
|
every recorded forecast whose target date has passed, the same way the
|
||||||
|
backtest does.
|
||||||
|
|
||||||
|
Tracked: the four registered models plus `powerlaw_ou`, `powerlaw_ou_param`
|
||||||
|
and `cycle_on_powerlaw`, which development data couldn't settle. Snapshots
|
||||||
|
store the forecasts themselves, so if a model's definition changes it gets a
|
||||||
|
new name. The routine is `just weekly` (update, snapshot, score), then commit
|
||||||
|
the new snapshot. The first 1-month outcomes arrive 2026-10-23; 1-year results
|
||||||
|
need until late 2027, and 4-year ones until 2030.
|
||||||
|
|
||||||
|
## 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 ab # run A/B experiments -> output/ab/
|
||||||
|
just weekly # new prices, a forward-test snapshot, and its scores so far
|
||||||
|
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
|
||||||
|
experiments.py A/B tests: hypothesis, control, variants, verdict rule
|
||||||
|
forward.py forward test: record forecasts, score them as outcomes arrive
|
||||||
|
models/ drift, volatility and shape components; register models 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,190 @@
|
|||||||
|
"""
|
||||||
|
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
|
||||||
|
python -m btcmodel ab [NAME ...] run A/B experiments on development data
|
||||||
|
python -m btcmodel snapshot record tracked models' forecasts for the forward test
|
||||||
|
python -m btcmodel forward score recorded forecasts whose targets have passed
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from . import data, evaluate, experiments, forward, 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")
|
||||||
|
ab = commands.add_parser("ab", help="run A/B experiments on development data")
|
||||||
|
ab.add_argument("names", nargs="*", help="experiments to run (default: all)")
|
||||||
|
commands.add_parser("snapshot", help="record forecasts for the forward test")
|
||||||
|
commands.add_parser("forward", help="score recorded forecasts")
|
||||||
|
args = parser.parse_args()
|
||||||
|
if args.command == "ab" and (unknown := set(args.names) - set(experiments.EXPERIMENTS)):
|
||||||
|
parser.error(f"unknown experiments: {', '.join(sorted(unknown))}")
|
||||||
|
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)
|
||||||
|
elif args.command == "ab":
|
||||||
|
run_ab(args.names or list(experiments.EXPERIMENTS), args.output)
|
||||||
|
elif args.command == "snapshot":
|
||||||
|
run_snapshot()
|
||||||
|
elif args.command == "forward":
|
||||||
|
run_forward(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_ab(names: list[str], output: Path) -> None:
|
||||||
|
prices = data.load_prices(until=data.DEV_CUTOFF)
|
||||||
|
verdicts = []
|
||||||
|
for name in names:
|
||||||
|
experiment = experiments.EXPERIMENTS[name]
|
||||||
|
scores, summary = experiments.run(experiment, prices)
|
||||||
|
out = output / "ab" / name
|
||||||
|
out.mkdir(parents=True, exist_ok=True)
|
||||||
|
control = experiment.control.name
|
||||||
|
report = (
|
||||||
|
f"{name}: {experiment.hypothesis}\n(from {experiment.source})\n\n"
|
||||||
|
+ evaluate.format_summary(summary, baseline=control)
|
||||||
|
)
|
||||||
|
print(report + "\n")
|
||||||
|
(out / "report.txt").write_text(report + "\n")
|
||||||
|
summary.to_csv(out / "summary.csv", index=False)
|
||||||
|
plots.skill_chart(summary, out / "skill.png", f"{name}: skill vs {control}", control)
|
||||||
|
plots.calibration_chart(summary, out / "calibration.png", f"{name}: interval coverage")
|
||||||
|
for variant in experiment.variants:
|
||||||
|
v = summary[summary.model == variant.name].set_index("horizon")
|
||||||
|
verdicts.append(
|
||||||
|
{
|
||||||
|
"experiment": name,
|
||||||
|
"variant": variant.name,
|
||||||
|
"control": control,
|
||||||
|
**{evaluate.horizon_label(h): f"{s:+.0%}" for h, s in v["skill"].items()},
|
||||||
|
"verdict": experiments.verdict(v),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
table = pd.DataFrame(verdicts).to_string(index=False)
|
||||||
|
print(table)
|
||||||
|
(output / "ab").mkdir(parents=True, exist_ok=True)
|
||||||
|
(output / "ab" / "verdicts.txt").write_text(table + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
def run_snapshot() -> None:
|
||||||
|
prices = data.load_prices()
|
||||||
|
path = forward.snapshot(prices)
|
||||||
|
recorded = [(m, f) for m, f in forward.load_snapshots() if f.origin == prices.index[-1]]
|
||||||
|
medians = pd.DataFrame(
|
||||||
|
{m: [plots.price_formatter(p) for p in np.exp(f.quantile(0.5))] for m, f in recorded},
|
||||||
|
index=[evaluate.horizon_label(h) for h in evaluate.HORIZONS],
|
||||||
|
).T
|
||||||
|
print(
|
||||||
|
f"recorded {len(recorded)} models' forecasts from {prices.index[-1]:%Y-%m-%d} in {path}\n"
|
||||||
|
)
|
||||||
|
print("medians:\n" + medians.to_string())
|
||||||
|
|
||||||
|
|
||||||
|
def run_forward(output: Path) -> None:
|
||||||
|
prices = data.load_prices()
|
||||||
|
scores = forward.score_snapshots(prices)
|
||||||
|
due = forward.next_due(prices)
|
||||||
|
if scores.empty:
|
||||||
|
when = f"; the first is due {due:%Y-%m-%d}" if due is not None else ""
|
||||||
|
print(f"no recorded forecast has reached its target date yet{when}")
|
||||||
|
return
|
||||||
|
out = output / "forward"
|
||||||
|
out.mkdir(parents=True, exist_ok=True)
|
||||||
|
summary = evaluate.summarize(scores, step_days=forward.SNAPSHOT_STEP_DAYS)
|
||||||
|
report = (
|
||||||
|
f"forward test: {scores.origin.nunique()} snapshots from {scores.origin.min():%Y-%m-%d}, "
|
||||||
|
f"outcomes through {prices.index[-1]:%Y-%m-%d}\n\n" + evaluate.format_summary(summary)
|
||||||
|
)
|
||||||
|
if due is not None:
|
||||||
|
report += f"\n\nnext outcome due {due:%Y-%m-%d}"
|
||||||
|
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", "forward test: skill by horizon")
|
||||||
|
plots.calibration_chart(summary, out / "calibration.png", "forward test: 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,133 @@
|
|||||||
|
"""
|
||||||
|
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 Forecast, 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:
|
||||||
|
rows.append(score(model.name, model.forecast(history, horizons[scored]), outcome))
|
||||||
|
return pd.concat(rows, ignore_index=True)
|
||||||
|
|
||||||
|
|
||||||
|
def score(model: str, f: Forecast, outcome: np.ndarray) -> pd.DataFrame:
|
||||||
|
"""Score one forecast against the observed log prices at its horizons."""
|
||||||
|
row = {
|
||||||
|
"model": model,
|
||||||
|
"origin": f.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)
|
||||||
|
return pd.DataFrame(row)
|
||||||
|
|
||||||
|
|
||||||
|
def summarize(
|
||||||
|
scores: pd.DataFrame,
|
||||||
|
baseline: str = BASELINE,
|
||||||
|
step_days: int = ORIGIN_STEP_DAYS,
|
||||||
|
n_boot: int = 2000,
|
||||||
|
seed: int = 0,
|
||||||
|
) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
Per model and horizon: mean CRPS, skill relative to `baseline`, and coverage.
|
||||||
|
|
||||||
|
Skill is 1 - CRPS / baseline CRPS (positive = better than the baseline),
|
||||||
|
with a 90% block bootstrap interval over origins spaced `step_days` apart. 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 / 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, baseline: str = BASELINE) -> 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),
|
||||||
|
f"skill vs {baseline} [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,169 @@
|
|||||||
|
"""
|
||||||
|
A/B tests of individual ideas, mostly salvaged from the 2024 model's branches.
|
||||||
|
|
||||||
|
Each experiment states its hypothesis and pairs a control with variants that
|
||||||
|
change one component. Experiments are written down before they are run, and
|
||||||
|
every result is reported, including the failures; with a few dozen
|
||||||
|
comparisons over a handful of independent windows, some "wins" will be luck.
|
||||||
|
Verdicts use one rule, fixed in advance (see `verdict`), and anything that
|
||||||
|
passes still has to hold up on the holdout.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from .evaluate import backtest, summarize
|
||||||
|
from .models import MODELS
|
||||||
|
from .models.base import Composite
|
||||||
|
from .models.drift import (
|
||||||
|
CycleDrift,
|
||||||
|
PowerLawDrift,
|
||||||
|
PowerLawScaledCycleDrift,
|
||||||
|
ShrunkDrift,
|
||||||
|
TrailingMeanDrift,
|
||||||
|
)
|
||||||
|
from .models.shape import Empirical, StudentT
|
||||||
|
from .models.volatility import CycleVol, EwmaVol, ReversionVol, TrailingVol, TrendReversionVol
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Experiment:
|
||||||
|
name: str
|
||||||
|
hypothesis: str
|
||||||
|
source: str # where in the old history the idea came from
|
||||||
|
control: Composite
|
||||||
|
variants: tuple[Composite, ...]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def models(self) -> list[Composite]:
|
||||||
|
return [self.control, *self.variants]
|
||||||
|
|
||||||
|
|
||||||
|
def run(experiment: Experiment, prices: pd.DataFrame) -> tuple[pd.DataFrame, pd.DataFrame]:
|
||||||
|
scores = backtest(experiment.models, prices)
|
||||||
|
return scores, summarize(scores, baseline=experiment.control.name)
|
||||||
|
|
||||||
|
|
||||||
|
def verdict(variant_summary: pd.DataFrame) -> str:
|
||||||
|
"""
|
||||||
|
Decide from skill vs the control, per horizon, with 90% intervals:
|
||||||
|
|
||||||
|
- "worse" if the interval is entirely below zero at any horizon;
|
||||||
|
- "better" if the interval is entirely above zero at two or more horizons
|
||||||
|
and the point estimate is non-negative at every horizon;
|
||||||
|
- "inconclusive" otherwise.
|
||||||
|
"""
|
||||||
|
if (variant_summary["skill_hi"] < 0).any():
|
||||||
|
return "worse"
|
||||||
|
if (variant_summary["skill_lo"] > 0).sum() >= 2 and (variant_summary["skill"] >= 0).all():
|
||||||
|
return "better"
|
||||||
|
return "inconclusive"
|
||||||
|
|
||||||
|
|
||||||
|
# Idea labels (D1, V2, ...) refer to docs/2024-ideas.md.
|
||||||
|
CYCLE = MODELS["cycle"]
|
||||||
|
DRIFT_RW = MODELS["drift_rw"]
|
||||||
|
POWERLAW = MODELS["powerlaw"]
|
||||||
|
# Volatility and shape experiments use drift_rw as the control: the zero-drift
|
||||||
|
# random walk is biased low at long horizons, so anything that merely widened
|
||||||
|
# its intervals would look like an improvement.
|
||||||
|
_VOL = dict(drift=TrailingMeanDrift())
|
||||||
|
|
||||||
|
EXPERIMENTS: dict[str, Experiment] = {
|
||||||
|
e.name: e
|
||||||
|
for e in (
|
||||||
|
Experiment(
|
||||||
|
"shrink-cycle",
|
||||||
|
"the cycle drift is overfit; pulling it toward zero improves it",
|
||||||
|
"D2: damping constants throughout the 2024 model",
|
||||||
|
CYCLE,
|
||||||
|
tuple(
|
||||||
|
Composite(f"cycle_x{f}", ShrunkDrift(CycleDrift(), f), TrailingVol())
|
||||||
|
for f in (0.25, 0.5, 0.75)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Experiment(
|
||||||
|
"diminishing-returns",
|
||||||
|
"each cycle grows less than the last; a power-law trend captures that",
|
||||||
|
"D3: initial commit, old/backtests-trend-enhancement-1",
|
||||||
|
DRIFT_RW,
|
||||||
|
(
|
||||||
|
Composite("powerlaw", PowerLawDrift(), TrailingVol()),
|
||||||
|
Composite("powerlaw_revert", PowerLawDrift(revert=True), TrailingVol()),
|
||||||
|
Composite("cycle_on_powerlaw", PowerLawScaledCycleDrift(), TrailingVol()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Experiment(
|
||||||
|
"vol-window",
|
||||||
|
"recent volatility predicts future volatility better than a flat 365-day window",
|
||||||
|
"V1: 'Add improved vol calculation'",
|
||||||
|
DRIFT_RW,
|
||||||
|
(
|
||||||
|
Composite("ewma_blend", volatility=EwmaVol(), **_VOL),
|
||||||
|
Composite("ewma_90", volatility=EwmaVol(spans=(90,), weights=(1.0,)), **_VOL),
|
||||||
|
Composite("trailing_730", volatility=TrailingVol(730), **_VOL),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Experiment(
|
||||||
|
"vol-reversion",
|
||||||
|
"volatility shocks fade toward a long-run level, which itself is falling",
|
||||||
|
"V2, V4, C2: adaptive windows, market maturity, horizon multipliers",
|
||||||
|
DRIFT_RW,
|
||||||
|
(
|
||||||
|
Composite("revert_level", volatility=ReversionVol(), **_VOL),
|
||||||
|
Composite("revert_trend", volatility=ReversionVol(long_run="trend"), **_VOL),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Experiment(
|
||||||
|
"cycle-vol",
|
||||||
|
"volatility depends on position in the halving cycle",
|
||||||
|
"V3: old/backtest-vol-cycle",
|
||||||
|
DRIFT_RW,
|
||||||
|
(Composite("cycle_vol", volatility=CycleVol(), **_VOL),),
|
||||||
|
),
|
||||||
|
Experiment(
|
||||||
|
"tails",
|
||||||
|
"log returns are heavier-tailed than normal, even at long horizons",
|
||||||
|
"S1: skewed innovations in tuning-1",
|
||||||
|
DRIFT_RW,
|
||||||
|
(
|
||||||
|
Composite("student_t4", volatility=TrailingVol(), shape=StudentT(4), **_VOL),
|
||||||
|
Composite("empirical", volatility=TrailingVol(), shape=Empirical(), **_VOL),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Experiment(
|
||||||
|
"cycle-phase",
|
||||||
|
"cycles align better by fraction elapsed than by days since the halving",
|
||||||
|
"D1: the 2024 model stretched every cycle to 1460 days",
|
||||||
|
CYCLE,
|
||||||
|
(Composite("cycle_fraction", CycleDrift(phase="fraction"), TrailingVol()),),
|
||||||
|
),
|
||||||
|
# Round 2. Before running these, the holdout candidates were fixed as:
|
||||||
|
# the three original models, powerlaw, and any variant below that is
|
||||||
|
# "better" than its control.
|
||||||
|
Experiment(
|
||||||
|
"powerlaw-ou",
|
||||||
|
"deviations from the power-law trend fade, so long-horizon uncertainty is bounded",
|
||||||
|
"follow-up to diminishing-returns: powerlaw_revert beat drift_rw, but its bands"
|
||||||
|
" were too wide",
|
||||||
|
POWERLAW,
|
||||||
|
(
|
||||||
|
Composite("powerlaw_ou", PowerLawDrift(revert=True), TrendReversionVol()),
|
||||||
|
Composite(
|
||||||
|
"powerlaw_ou_param",
|
||||||
|
PowerLawDrift(revert=True),
|
||||||
|
TrendReversionVol(parameter_uncertainty=True),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Experiment(
|
||||||
|
"cycle-on-powerlaw",
|
||||||
|
"with the level set by the power law, the cycle's timing adds information",
|
||||||
|
"D1 on D3. This pair was already compared informally on the same data after"
|
||||||
|
" round 1, so only the holdout can really settle it",
|
||||||
|
POWERLAW,
|
||||||
|
(Composite("cycle_on_powerlaw", PowerLawScaledCycleDrift(), TrailingVol()),),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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,104 @@
|
|||||||
|
"""
|
||||||
|
Forward test: forecasts recorded before their outcomes exist.
|
||||||
|
|
||||||
|
`snapshot` stores every tracked model's forecast from the latest price in
|
||||||
|
data/forecasts/<origin>.csv. The files are committed, so version control shows
|
||||||
|
each one was written before its outcome was known. `score_snapshots` grades
|
||||||
|
every recorded forecast whose target date has passed, exactly as the backtest
|
||||||
|
does.
|
||||||
|
|
||||||
|
Snapshots store the forecast quantiles themselves, so model code can change
|
||||||
|
without rewriting the record. If a model's definition changes, give it a new
|
||||||
|
name rather than reusing the old one.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import datetime as dt
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from .data import DATA_DIR
|
||||||
|
from .evaluate import HORIZONS, score
|
||||||
|
from .experiments import EXPERIMENTS
|
||||||
|
from .forecast import LEVELS, Forecast
|
||||||
|
from .models import MODELS
|
||||||
|
|
||||||
|
FORECASTS_DIR = DATA_DIR / "forecasts"
|
||||||
|
# A snapshot must be made from fresh data, not reconstructed after the fact.
|
||||||
|
MAX_DATA_AGE_DAYS = 2
|
||||||
|
# Intended cadence (weekly), used to size bootstrap blocks when scoring.
|
||||||
|
SNAPSHOT_STEP_DAYS = 7
|
||||||
|
|
||||||
|
_VARIANTS = {m.name: m for e in EXPERIMENTS.values() for m in e.models}
|
||||||
|
# The registered models, plus candidates that could only be settled on new data.
|
||||||
|
TRACKED = [
|
||||||
|
*MODELS.values(),
|
||||||
|
*(_VARIANTS[n] for n in ("powerlaw_ou", "powerlaw_ou_param", "cycle_on_powerlaw")),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Named by percentile: p0.5, p1.5, ..., p99.5 (the grid has no exact median).
|
||||||
|
_QUANTILE_COLUMNS = [f"p{100 * level:.1f}" for level in LEVELS]
|
||||||
|
|
||||||
|
|
||||||
|
def snapshot(
|
||||||
|
prices: pd.DataFrame,
|
||||||
|
models=TRACKED,
|
||||||
|
directory: Path = FORECASTS_DIR,
|
||||||
|
today: pd.Timestamp | None = None,
|
||||||
|
) -> Path:
|
||||||
|
"""Record each model's forecast from the last price in `prices`."""
|
||||||
|
origin = prices.index[-1]
|
||||||
|
if today is None:
|
||||||
|
today = pd.Timestamp(dt.datetime.now(dt.UTC).date())
|
||||||
|
if (today - origin).days > MAX_DATA_AGE_DAYS:
|
||||||
|
raise ValueError(
|
||||||
|
f"latest price is from {origin:%Y-%m-%d}; update the data first "
|
||||||
|
"(forecasts must be recorded in real time)"
|
||||||
|
)
|
||||||
|
path = directory / f"{origin:%Y-%m-%d}.csv"
|
||||||
|
if path.exists():
|
||||||
|
raise FileExistsError(f"{path} already exists")
|
||||||
|
|
||||||
|
frames = []
|
||||||
|
for model in models:
|
||||||
|
f = model.forecast(prices, HORIZONS)
|
||||||
|
frame = pd.DataFrame(f.log_quantiles, columns=_QUANTILE_COLUMNS)
|
||||||
|
frame.insert(0, "model", model.name)
|
||||||
|
frame.insert(1, "horizon", f.horizons)
|
||||||
|
frame.insert(2, "target", f.dates.strftime("%Y-%m-%d"))
|
||||||
|
frames.append(frame)
|
||||||
|
directory.mkdir(parents=True, exist_ok=True)
|
||||||
|
pd.concat(frames).to_csv(path, index=False, float_format="%.6f")
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def load_snapshots(directory: Path = FORECASTS_DIR) -> list[tuple[str, Forecast]]:
|
||||||
|
"""Every recorded forecast, as (model name, Forecast)."""
|
||||||
|
forecasts = []
|
||||||
|
for path in sorted(directory.glob("*.csv")):
|
||||||
|
origin = pd.Timestamp(path.stem)
|
||||||
|
for model, g in pd.read_csv(path).groupby("model", sort=False):
|
||||||
|
quantiles = g[_QUANTILE_COLUMNS].to_numpy()
|
||||||
|
forecasts.append((model, Forecast(origin, g["horizon"].to_numpy(), quantiles)))
|
||||||
|
return forecasts
|
||||||
|
|
||||||
|
|
||||||
|
def score_snapshots(prices: pd.DataFrame, directory: Path = FORECASTS_DIR) -> pd.DataFrame:
|
||||||
|
"""Score every recorded forecast horizon whose target date is in `prices`."""
|
||||||
|
log_close = np.log(prices["close"])
|
||||||
|
rows = []
|
||||||
|
for model, f in load_snapshots(directory):
|
||||||
|
due = f.dates <= prices.index[-1]
|
||||||
|
if not due.any():
|
||||||
|
continue
|
||||||
|
due_forecast = Forecast(f.origin, f.horizons[due], f.log_quantiles[due])
|
||||||
|
outcome = log_close.loc[due_forecast.dates].to_numpy()
|
||||||
|
rows.append(score(model, due_forecast, outcome))
|
||||||
|
return pd.concat(rows, ignore_index=True) if rows else pd.DataFrame()
|
||||||
|
|
||||||
|
|
||||||
|
def next_due(prices: pd.DataFrame, directory: Path = FORECASTS_DIR) -> pd.Timestamp | None:
|
||||||
|
"""The earliest target date not yet observed, if any."""
|
||||||
|
pending = [d for _, f in load_snapshots(directory) for d in f.dates if d > prices.index[-1]]
|
||||||
|
return min(pending) if pending else None
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
"""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)
|
||||||
|
|
||||||
|
|
||||||
|
def cycle_fraction(dates) -> tuple[np.ndarray, np.ndarray]:
|
||||||
|
"""For each date, return (cycle index, fraction of that cycle elapsed, in [0, 1))."""
|
||||||
|
index, days = cycle_position(dates)
|
||||||
|
lengths = (CYCLE_STARTS[index + 1] - CYCLE_STARTS[index]).days
|
||||||
|
return index, days / np.asarray(lengths)
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
"""
|
||||||
|
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.
|
||||||
|
Most models are a Composite of a drift and a volatility component.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .base import Composite
|
||||||
|
from .drift import CycleDrift, PowerLawDrift, TrailingMeanDrift, ZeroDrift
|
||||||
|
from .volatility import TrailingVol
|
||||||
|
|
||||||
|
BASELINE = "random_walk"
|
||||||
|
|
||||||
|
# Order is fixed: it sets each model's colour in every chart.
|
||||||
|
MODELS = {
|
||||||
|
m.name: m
|
||||||
|
for m in (
|
||||||
|
# "It stays about here, give or take."
|
||||||
|
Composite(BASELINE, ZeroDrift(), TrailingVol()),
|
||||||
|
# "It keeps doing what it did last cycle."
|
||||||
|
Composite("drift_rw", TrailingMeanDrift(), TrailingVol()),
|
||||||
|
# The 2024 model, distilled.
|
||||||
|
Composite("cycle", CycleDrift(), TrailingVol()),
|
||||||
|
# "Growth keeps slowing, like it always has." Passed the diminishing-returns A/B.
|
||||||
|
Composite("powerlaw", PowerLawDrift(), TrailingVol()),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
"""
|
||||||
|
Models built from parts.
|
||||||
|
|
||||||
|
Most ideas about Bitcoin prices say something about either the expected return
|
||||||
|
(drift) or the size of the uncertainty (volatility). A Composite pairs one of
|
||||||
|
each, so an A/B test can swap exactly one part and hold the other fixed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Protocol
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from ..forecast import Forecast
|
||||||
|
from .shape import Normal
|
||||||
|
|
||||||
|
|
||||||
|
class Drift(Protocol):
|
||||||
|
def expected_log_return(self, history: pd.DataFrame, horizons: np.ndarray) -> np.ndarray:
|
||||||
|
"""Expected cumulative log return from the origin to each horizon."""
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class Volatility(Protocol):
|
||||||
|
def sd(self, history: pd.DataFrame, horizons: np.ndarray) -> np.ndarray:
|
||||||
|
"""Standard deviation of the cumulative log return at each horizon."""
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class Shape(Protocol):
|
||||||
|
def standard_quantiles(self, history: pd.DataFrame, horizons: np.ndarray) -> np.ndarray:
|
||||||
|
"""Quantiles at LEVELS of a mean-0, sd-1 distribution, shape (H, N_LEVELS)."""
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Composite:
|
||||||
|
"""Log price: mean from `drift`, spread from `volatility`, normal unless `shape` says."""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
drift: Drift
|
||||||
|
volatility: Volatility
|
||||||
|
shape: Shape = Normal()
|
||||||
|
|
||||||
|
def forecast(self, history: pd.DataFrame, horizons: np.ndarray) -> Forecast:
|
||||||
|
mean = np.log(history["close"].iloc[-1]) + self.drift.expected_log_return(history, horizons)
|
||||||
|
sd = self.volatility.sd(history, horizons)
|
||||||
|
z = self.shape.standard_quantiles(history, horizons)
|
||||||
|
return Forecast(history.index[-1], horizons, mean[:, None] + sd[:, None] * z)
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
"""Expected-return components."""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from ..data import log_returns
|
||||||
|
from ..halving import GENESIS, cycle_fraction, cycle_position
|
||||||
|
|
||||||
|
# Longer than any cycle so far (the longest, cycle 0, is 1425 days).
|
||||||
|
MAX_CYCLE_DAYS = 1500
|
||||||
|
# Length every cycle is stretched to when phase="fraction".
|
||||||
|
NOMINAL_CYCLE_DAYS = 1440
|
||||||
|
|
||||||
|
|
||||||
|
def mean_by_cycle_day(values, day, weight, bandwidth_days, prior_days) -> np.ndarray:
|
||||||
|
"""
|
||||||
|
Weighted mean of `values` around each day of the cycle, shape (MAX_CYCLE_DAYS,).
|
||||||
|
|
||||||
|
Neighbouring days are pooled with a Gaussian kernel. Where the data is thin,
|
||||||
|
the mean shrinks toward the overall weighted mean, as if `prior_days` extra
|
||||||
|
observations sat at that value.
|
||||||
|
"""
|
||||||
|
sum_wv = np.bincount(day, weights=weight * values, minlength=MAX_CYCLE_DAYS)
|
||||||
|
sum_w = np.bincount(day, weights=weight, minlength=MAX_CYCLE_DAYS)
|
||||||
|
|
||||||
|
# Peak-1 kernel, so smoothed weights count (weighted) days of data.
|
||||||
|
half_width = int(np.ceil(4 * bandwidth_days))
|
||||||
|
offsets = np.arange(-half_width, half_width + 1)
|
||||||
|
kernel = np.exp(-0.5 * (offsets / bandwidth_days) ** 2)
|
||||||
|
smooth_wv = np.convolve(sum_wv, kernel, mode="same")
|
||||||
|
smooth_w = np.convolve(sum_w, kernel, mode="same")
|
||||||
|
|
||||||
|
overall = sum_wv.sum() / sum_w.sum()
|
||||||
|
return (smooth_wv + prior_days * overall) / (smooth_w + prior_days)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ZeroDrift:
|
||||||
|
"""No expected change in log price."""
|
||||||
|
|
||||||
|
def expected_log_return(self, history: pd.DataFrame, horizons: np.ndarray) -> np.ndarray:
|
||||||
|
return np.zeros(len(horizons))
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class TrailingMeanDrift:
|
||||||
|
"""Mean daily log return over the trailing `window` days, extrapolated."""
|
||||||
|
|
||||||
|
window: int = 1460
|
||||||
|
|
||||||
|
def expected_log_return(self, history: pd.DataFrame, horizons: np.ndarray) -> np.ndarray:
|
||||||
|
return log_returns(history).iloc[-self.window :].mean() * horizons
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class CycleDrift:
|
||||||
|
"""
|
||||||
|
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 mean (see
|
||||||
|
`mean_by_cycle_day`).
|
||||||
|
|
||||||
|
`phase="days"` aligns cycles by days since the halving; `phase="fraction"`
|
||||||
|
stretches every cycle to NOMINAL_CYCLE_DAYS, as the 2024 model did.
|
||||||
|
"""
|
||||||
|
|
||||||
|
bandwidth_days: float = 30.0
|
||||||
|
recency_half_life: float = 1.0
|
||||||
|
prior_days: float = 10.0
|
||||||
|
phase: str = "days"
|
||||||
|
|
||||||
|
def position(self, dates) -> tuple[np.ndarray, np.ndarray]:
|
||||||
|
"""(cycle index, cycle day) under this model's phase convention."""
|
||||||
|
if self.phase == "days":
|
||||||
|
return cycle_position(dates)
|
||||||
|
if self.phase == "fraction":
|
||||||
|
index, fraction = cycle_fraction(dates)
|
||||||
|
return index, np.round(fraction * NOMINAL_CYCLE_DAYS).astype(int)
|
||||||
|
raise ValueError(f"unknown phase {self.phase!r}")
|
||||||
|
|
||||||
|
def 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 = self.position(returns.index)
|
||||||
|
current_cycle = self.position(history.index[-1:])[0][0]
|
||||||
|
return mean_by_cycle_day(
|
||||||
|
returns.to_numpy(),
|
||||||
|
day,
|
||||||
|
weight=0.5 ** ((current_cycle - cycle) / self.recency_half_life),
|
||||||
|
bandwidth_days=self.bandwidth_days,
|
||||||
|
prior_days=self.prior_days,
|
||||||
|
)
|
||||||
|
|
||||||
|
def expected_log_return(self, history: pd.DataFrame, horizons: np.ndarray) -> np.ndarray:
|
||||||
|
drift = self.by_cycle_day(history)
|
||||||
|
future = history.index[-1] + pd.to_timedelta(np.arange(1, horizons.max() + 1), unit="D")
|
||||||
|
_, future_day = self.position(future)
|
||||||
|
return np.cumsum(drift[future_day])[horizons - 1]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ShrunkDrift:
|
||||||
|
"""Another drift scaled by `factor`: 0 is no drift, 1 is the original."""
|
||||||
|
|
||||||
|
inner: CycleDrift | TrailingMeanDrift
|
||||||
|
factor: float
|
||||||
|
|
||||||
|
def expected_log_return(self, history: pd.DataFrame, horizons: np.ndarray) -> np.ndarray:
|
||||||
|
return self.factor * self.inner.expected_log_return(history, horizons)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PowerLawDrift:
|
||||||
|
"""
|
||||||
|
Log price grows linearly in log time since genesis: ln P = a + b ln t.
|
||||||
|
|
||||||
|
Growth therefore slows like b/t, which is the diminishing returns the
|
||||||
|
cycle-by-cycle numbers show. The line is fitted by least squares to the
|
||||||
|
history. With `revert=True` the gap between price and line also closes,
|
||||||
|
at the rate of an AR(1) fitted to the daily residuals.
|
||||||
|
"""
|
||||||
|
|
||||||
|
revert: bool = False
|
||||||
|
|
||||||
|
def fit(self, history: pd.DataFrame) -> tuple[float, float, float]:
|
||||||
|
"""Return (intercept, slope, daily AR(1) coefficient of the residuals)."""
|
||||||
|
log_t = np.log((history.index - GENESIS).days.to_numpy())
|
||||||
|
log_p = np.log(history["close"].to_numpy())
|
||||||
|
slope, intercept = np.polyfit(log_t, log_p, 1)
|
||||||
|
resid = log_p - (intercept + slope * log_t)
|
||||||
|
phi = resid[1:] @ resid[:-1] / (resid[:-1] @ resid[:-1])
|
||||||
|
return intercept, slope, phi
|
||||||
|
|
||||||
|
def expected_log_return(self, history: pd.DataFrame, horizons: np.ndarray) -> np.ndarray:
|
||||||
|
intercept, slope, phi = self.fit(history)
|
||||||
|
t0 = (history.index[-1] - GENESIS).days
|
||||||
|
trend = slope * np.log((t0 + horizons) / t0)
|
||||||
|
if not self.revert:
|
||||||
|
return trend
|
||||||
|
gap = np.log(history["close"].iloc[-1]) - (intercept + slope * np.log(t0))
|
||||||
|
return trend + (phi**horizons - 1) * gap
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PowerLawScaledCycleDrift:
|
||||||
|
"""
|
||||||
|
The cycle shape, rescaled so its average over a cycle equals the power-law
|
||||||
|
trend's growth rate over the next cycle: keep the timing, fix the level.
|
||||||
|
"""
|
||||||
|
|
||||||
|
cycle: CycleDrift = CycleDrift()
|
||||||
|
|
||||||
|
def expected_log_return(self, history: pd.DataFrame, horizons: np.ndarray) -> np.ndarray:
|
||||||
|
shape = self.cycle.by_cycle_day(history)
|
||||||
|
cycle_mean = shape[:NOMINAL_CYCLE_DAYS].mean()
|
||||||
|
level = PowerLawDrift().expected_log_return(history, np.array([NOMINAL_CYCLE_DAYS]))[0]
|
||||||
|
level /= NOMINAL_CYCLE_DAYS
|
||||||
|
if cycle_mean <= 1e-6:
|
||||||
|
return level * horizons
|
||||||
|
future = history.index[-1] + pd.to_timedelta(np.arange(1, horizons.max() + 1), unit="D")
|
||||||
|
_, future_day = self.cycle.position(future)
|
||||||
|
return np.cumsum(shape[future_day] * level / cycle_mean)[horizons - 1]
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
"""Distribution shapes: standardised quantiles (mean 0, sd 1) at each horizon."""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
from scipy.stats import norm, t
|
||||||
|
|
||||||
|
from ..forecast import LEVELS
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Normal:
|
||||||
|
def standard_quantiles(self, history: pd.DataFrame, horizons: np.ndarray) -> np.ndarray:
|
||||||
|
return np.broadcast_to(norm.ppf(LEVELS), (len(horizons), len(LEVELS)))
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class StudentT:
|
||||||
|
"""Student's t with `df` degrees of freedom, rescaled to unit variance."""
|
||||||
|
|
||||||
|
df: float = 4.0
|
||||||
|
|
||||||
|
def standard_quantiles(self, history: pd.DataFrame, horizons: np.ndarray) -> np.ndarray:
|
||||||
|
q = t.ppf(LEVELS, self.df) / np.sqrt(self.df / (self.df - 2))
|
||||||
|
return np.broadcast_to(q, (len(horizons), len(LEVELS)))
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Empirical:
|
||||||
|
"""
|
||||||
|
Filtered historical simulation at the horizon level: the shape of past
|
||||||
|
h-day log returns, each divided by the trailing volatility at its start.
|
||||||
|
|
||||||
|
Overlapping h-day returns are far from independent, so a horizon falls back
|
||||||
|
to normal unless the history spans at least `min_windows` of them.
|
||||||
|
"""
|
||||||
|
|
||||||
|
vol_window: int = 365
|
||||||
|
min_windows: int = 3
|
||||||
|
|
||||||
|
def standard_quantiles(self, history: pd.DataFrame, horizons: np.ndarray) -> np.ndarray:
|
||||||
|
log_price = np.log(history["close"])
|
||||||
|
sigma = log_price.diff().rolling(self.vol_window).std()
|
||||||
|
out = np.empty((len(horizons), len(LEVELS)))
|
||||||
|
for i, h in enumerate(horizons):
|
||||||
|
z = ((log_price.shift(-h) - log_price) / (sigma * np.sqrt(h))).dropna()
|
||||||
|
if len(z) < self.min_windows * h:
|
||||||
|
out[i] = norm.ppf(LEVELS)
|
||||||
|
else:
|
||||||
|
out[i] = np.quantile((z - z.mean()) / z.std(), LEVELS)
|
||||||
|
return out
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
"""Uncertainty components."""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from ..data import log_returns
|
||||||
|
from ..halving import GENESIS, cycle_position
|
||||||
|
from .drift import PowerLawDrift, mean_by_cycle_day
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class TrailingVol:
|
||||||
|
"""Standard deviation of daily log returns over the trailing `window` days, scaled by √h."""
|
||||||
|
|
||||||
|
window: int = 365
|
||||||
|
|
||||||
|
def sd(self, history: pd.DataFrame, horizons: np.ndarray) -> np.ndarray:
|
||||||
|
return log_returns(history).iloc[-self.window :].std() * np.sqrt(horizons)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class EwmaVol:
|
||||||
|
"""Weighted blend of exponentially weighted standard deviations, scaled by √h."""
|
||||||
|
|
||||||
|
spans: tuple[int, ...] = (30, 90, 180)
|
||||||
|
weights: tuple[float, ...] = (0.2, 0.5, 0.3)
|
||||||
|
|
||||||
|
def sd(self, history: pd.DataFrame, horizons: np.ndarray) -> np.ndarray:
|
||||||
|
returns = log_returns(history)
|
||||||
|
sigma = sum(
|
||||||
|
w * returns.ewm(span=s).std().iloc[-1]
|
||||||
|
for s, w in zip(self.spans, self.weights, strict=True)
|
||||||
|
)
|
||||||
|
return sigma * np.sqrt(horizons)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ReversionVol:
|
||||||
|
"""
|
||||||
|
Volatility starts at its current (short EWMA) level and decays toward a
|
||||||
|
long-run level with a `half_life_days` half-life.
|
||||||
|
|
||||||
|
`long_run="level"` uses the trailing `long_window` standard deviation;
|
||||||
|
`long_run="trend"` extrapolates a log-linear trend in 90-day realised
|
||||||
|
volatility over the same window, since volatility has fallen for years.
|
||||||
|
"""
|
||||||
|
|
||||||
|
now_span: int = 30
|
||||||
|
half_life_days: float = 90.0
|
||||||
|
long_run: str = "level"
|
||||||
|
long_window: int = 1460
|
||||||
|
|
||||||
|
def sd(self, history: pd.DataFrame, horizons: np.ndarray) -> np.ndarray:
|
||||||
|
returns = log_returns(history)
|
||||||
|
now = returns.ewm(span=self.now_span).std().iloc[-1]
|
||||||
|
days = np.arange(1, horizons.max() + 1)
|
||||||
|
trailing = returns.iloc[-self.long_window :]
|
||||||
|
if self.long_run == "level":
|
||||||
|
long_run = np.full(len(days), trailing.std())
|
||||||
|
elif self.long_run == "trend":
|
||||||
|
# Non-overlapping 90-day windows, anchored at the origin.
|
||||||
|
realised = trailing.rolling(90).std().iloc[::-90].dropna()
|
||||||
|
age = (realised.index - history.index[-1]).days.to_numpy()
|
||||||
|
slope, intercept = np.polyfit(age, np.log(realised.to_numpy()), 1)
|
||||||
|
long_run = np.exp(intercept + slope * days)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"unknown long_run {self.long_run!r}")
|
||||||
|
phi = 0.5 ** (1 / self.half_life_days)
|
||||||
|
daily_var = long_run**2 + (now**2 - long_run**2) * phi**days
|
||||||
|
return np.sqrt(np.cumsum(daily_var)[horizons - 1])
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class CycleVol:
|
||||||
|
"""
|
||||||
|
Trailing volatility, modulated by how volatile each day of the halving
|
||||||
|
cycle has been relative to the rest of the cycle.
|
||||||
|
|
||||||
|
The per-day ratio is estimated like CycleDrift's drift (kernel-smoothed,
|
||||||
|
recency-weighted), then pulled `shrink` of the way back toward 1. The
|
||||||
|
trailing estimate is first divided by the ratio it was measured under, so
|
||||||
|
the cycle effect isn't counted twice.
|
||||||
|
"""
|
||||||
|
|
||||||
|
window: int = 365
|
||||||
|
bandwidth_days: float = 30.0
|
||||||
|
recency_half_life: float = 1.0
|
||||||
|
shrink: float = 0.5
|
||||||
|
|
||||||
|
def sd(self, history: pd.DataFrame, horizons: np.ndarray) -> np.ndarray:
|
||||||
|
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)
|
||||||
|
squared = returns.to_numpy() ** 2
|
||||||
|
variance = mean_by_cycle_day(squared, day, weight, self.bandwidth_days, prior_days=10.0)
|
||||||
|
overall = (weight * squared).sum() / weight.sum()
|
||||||
|
ratio = 1 + (1 - self.shrink) * (np.sqrt(variance / overall) - 1)
|
||||||
|
|
||||||
|
past_ratio = ratio[day[-self.window :]]
|
||||||
|
base = returns.iloc[-self.window :].std() / np.sqrt(np.mean(past_ratio**2))
|
||||||
|
future = history.index[-1] + pd.to_timedelta(np.arange(1, horizons.max() + 1), unit="D")
|
||||||
|
_, future_day = cycle_position(future)
|
||||||
|
return base * np.sqrt(np.cumsum(ratio[future_day] ** 2)[horizons - 1])
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class TrendReversionVol:
|
||||||
|
"""
|
||||||
|
Uncertainty for a price that reverts to the power-law trend.
|
||||||
|
|
||||||
|
Deviations from the trend follow a daily AR(1) with coefficient φ (fitted
|
||||||
|
by PowerLawDrift), so their variance levels off: after h days it is
|
||||||
|
σ²(1 − φ^2h) / (1 − φ²), with σ the trailing `window`-day volatility.
|
||||||
|
|
||||||
|
With `parameter_uncertainty`, the uncertainty of the fitted trend line is
|
||||||
|
added. The residuals are so autocorrelated that ~5000 days carry the
|
||||||
|
information of only n(1 − φ)/(1 + φ) independent points, and the
|
||||||
|
coefficient covariance is inflated to match.
|
||||||
|
"""
|
||||||
|
|
||||||
|
window: int = 365
|
||||||
|
parameter_uncertainty: bool = False
|
||||||
|
|
||||||
|
def sd(self, history: pd.DataFrame, horizons: np.ndarray) -> np.ndarray:
|
||||||
|
intercept, slope, phi = PowerLawDrift().fit(history)
|
||||||
|
phi = min(phi, 0.9999)
|
||||||
|
sigma = log_returns(history).iloc[-self.window :].std()
|
||||||
|
variance = sigma**2 * (1 - phi ** (2 * horizons)) / (1 - phi**2)
|
||||||
|
if self.parameter_uncertainty:
|
||||||
|
variance = variance + self._trend_variance(history, horizons, intercept, slope, phi)
|
||||||
|
return np.sqrt(variance)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _trend_variance(history, horizons, intercept, slope, phi) -> np.ndarray:
|
||||||
|
t = (history.index - GENESIS).days.to_numpy()
|
||||||
|
x = np.column_stack([np.ones(len(t)), np.log(t)])
|
||||||
|
resid = np.log(history["close"].to_numpy()) - x @ [intercept, slope]
|
||||||
|
n_eff = len(t) * (1 - phi) / (1 + phi)
|
||||||
|
cov = resid.var() * np.linalg.inv(x.T @ x) * len(t) / n_eff
|
||||||
|
# The forecast mean is a(1 − φ^h) + b(ln t_h − φ^h ln t_0) + φ^h ln P_0.
|
||||||
|
decay = phi**horizons
|
||||||
|
g = np.column_stack([1 - decay, np.log(t[-1] + horizons) - decay * np.log(t[-1])])
|
||||||
|
return np.einsum("hi,ij,hj->h", g, cov, g)
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
"""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_colors(names) -> dict[str, str]:
|
||||||
|
"""Registered models keep their MODELS slot; others follow in order of appearance."""
|
||||||
|
names = list(dict.fromkeys(names))
|
||||||
|
if all(n in MODELS for n in names):
|
||||||
|
return {n: SERIES[list(MODELS).index(n)] for n in names}
|
||||||
|
return {n: SERIES[i % len(SERIES)] for i, n in enumerate(names)}
|
||||||
|
|
||||||
|
|
||||||
|
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)]
|
||||||
|
colors = model_colors(forecasts)
|
||||||
|
for ax, (name, f) in zip(axes, forecasts.items(), strict=True):
|
||||||
|
color = colors[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, baseline: str = BASELINE) -> None:
|
||||||
|
"""CRPS skill vs `baseline`, by horizon, with bootstrap intervals."""
|
||||||
|
horizons = sorted(summary["horizon"].unique())
|
||||||
|
x = np.arange(len(horizons))
|
||||||
|
colors = model_colors(summary["model"])
|
||||||
|
fig, ax = plt.subplots(figsize=(8, 4.5))
|
||||||
|
ax.axhline(0, color=colors[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 = colors[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(f"CRPS skill vs {baseline} (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))
|
||||||
|
colors = model_colors(summary["model"])
|
||||||
|
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=colors[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
|
||||||
|
@@ -0,0 +1,50 @@
|
|||||||
|
model,horizon,target,p0.5,p1.5,p2.5,p3.5,p4.5,p5.5,p6.5,p7.5,p8.5,p9.5,p10.5,p11.5,p12.5,p13.5,p14.5,p15.5,p16.5,p17.5,p18.5,p19.5,p20.5,p21.5,p22.5,p23.5,p24.5,p25.5,p26.5,p27.5,p28.5,p29.5,p30.5,p31.5,p32.5,p33.5,p34.5,p35.5,p36.5,p37.5,p38.5,p39.5,p40.5,p41.5,p42.5,p43.5,p44.5,p45.5,p46.5,p47.5,p48.5,p49.5,p50.5,p51.5,p52.5,p53.5,p54.5,p55.5,p56.5,p57.5,p58.5,p59.5,p60.5,p61.5,p62.5,p63.5,p64.5,p65.5,p66.5,p67.5,p68.5,p69.5,p70.5,p71.5,p72.5,p73.5,p74.5,p75.5,p76.5,p77.5,p78.5,p79.5,p80.5,p81.5,p82.5,p83.5,p84.5,p85.5,p86.5,p87.5,p88.5,p89.5,p90.5,p91.5,p92.5,p93.5,p94.5,p95.5,p96.5,p97.5,p98.5,p99.5
|
||||||
|
random_walk,30,2026-10-23,11.007321,11.060207,11.087595,11.106893,11.122080,11.134750,11.145711,11.155431,11.164207,11.172239,11.179670,11.186606,11.193124,11.199288,11.205145,11.210737,11.216095,11.221247,11.226215,11.231019,11.235676,11.240199,11.244602,11.248895,11.253088,11.257190,11.261209,11.265151,11.269023,11.272831,11.276580,11.280275,11.283920,11.287520,11.291077,11.294596,11.298080,11.301533,11.304956,11.308354,11.311727,11.315081,11.318415,11.321734,11.325038,11.328332,11.331616,11.334892,11.338164,11.341432,11.344699,11.347968,11.351239,11.354516,11.357800,11.361093,11.364398,11.367716,11.371051,11.374404,11.377778,11.381175,11.384599,11.388051,11.391535,11.395054,11.398612,11.402211,11.405856,11.409551,11.413300,11.417108,11.420980,11.424923,11.428941,11.433044,11.437237,11.441530,11.445932,11.450456,11.455112,11.459916,11.464884,11.470036,11.475394,11.480986,11.486844,11.493007,11.499526,11.506461,11.513892,11.521925,11.530701,11.540420,11.551381,11.564051,11.579238,11.598536,11.625925,11.678811
|
||||||
|
random_walk,91,2026-12-23,10.758317,10.850425,10.898127,10.931737,10.958187,10.980254,10.999344,11.016272,11.031556,11.045546,11.058489,11.070568,11.081920,11.092655,11.102857,11.112596,11.121928,11.130901,11.139554,11.147921,11.156030,11.163908,11.171576,11.179053,11.186356,11.193500,11.200500,11.207366,11.214110,11.220742,11.227272,11.233707,11.240055,11.246324,11.252520,11.258649,11.264717,11.270730,11.276692,11.282609,11.288486,11.294325,11.300133,11.305913,11.311669,11.317404,11.323124,11.328830,11.334528,11.340220,11.345911,11.351603,11.357301,11.363008,11.368727,11.374463,11.380218,11.385998,11.391806,11.397646,11.403522,11.409439,11.415401,11.421414,11.427482,11.433611,11.439807,11.446076,11.452424,11.458859,11.465389,11.472021,11.478766,11.485632,11.492631,11.499775,11.507078,11.514555,11.522223,11.530101,11.538211,11.546578,11.555230,11.564203,11.573535,11.583274,11.593476,11.604211,11.615564,11.627642,11.640585,11.654575,11.669859,11.686788,11.705878,11.727944,11.754395,11.788005,11.835706,11.927815
|
||||||
|
random_walk,182,2027-03-24,10.516106,10.646367,10.713827,10.761359,10.798765,10.829972,10.856969,10.880910,10.902525,10.922309,10.940613,10.957695,10.973750,10.988932,11.003360,11.017133,11.030330,11.043019,11.055256,11.067089,11.078558,11.089699,11.100543,11.111117,11.121445,11.131548,11.141447,11.151157,11.160695,11.170074,11.179309,11.188409,11.197387,11.206252,11.215015,11.223683,11.232264,11.240768,11.249200,11.257568,11.265878,11.274137,11.282350,11.290524,11.298664,11.306775,11.314863,11.322934,11.330992,11.339042,11.347089,11.355140,11.363197,11.371268,11.379356,11.387468,11.395608,11.403781,11.411995,11.420253,11.428564,11.436931,11.445364,11.453867,11.462449,11.471117,11.479879,11.488744,11.497722,11.506823,11.516057,11.525436,11.534974,11.544685,11.554583,11.564687,11.575015,11.585589,11.596433,11.607573,11.619042,11.630875,11.643112,11.655801,11.668999,11.682771,11.697200,11.712381,11.728436,11.745518,11.763822,11.783606,11.805222,11.829162,11.856159,11.887366,11.924773,11.972304,12.039765,12.170026
|
||||||
|
random_walk,365,2027-09-23,10.171962,10.356432,10.451966,10.519279,10.572251,10.616446,10.654678,10.688581,10.719192,10.747210,10.773131,10.797321,10.820058,10.841557,10.861990,10.881494,10.900184,10.918154,10.935483,10.952240,10.968482,10.984259,10.999615,11.014590,11.029216,11.043524,11.057542,11.071293,11.084800,11.098083,11.111160,11.124048,11.136762,11.149317,11.161726,11.174001,11.186154,11.198196,11.210137,11.221987,11.233756,11.245451,11.257083,11.268658,11.280186,11.291673,11.303127,11.314556,11.325967,11.337367,11.348764,11.360164,11.371575,11.383004,11.394459,11.405946,11.417473,11.429049,11.440680,11.452376,11.464144,11.475994,11.487935,11.499977,11.512130,11.524406,11.536814,11.549369,11.562083,11.574971,11.588048,11.601331,11.614838,11.628589,11.642607,11.656915,11.671541,11.686516,11.701872,11.717650,11.733892,11.750648,11.767978,11.785947,11.804637,11.824142,11.844574,11.866073,11.888810,11.913000,11.938922,11.966939,11.997550,12.031454,12.069686,12.113880,12.166853,12.234165,12.329699,12.514169
|
||||||
|
random_walk,730,2028-09-22,9.686875,9.947755,10.082861,10.178055,10.252970,10.315470,10.369538,10.417485,10.460775,10.500398,10.537056,10.571267,10.603421,10.633826,10.662721,10.690305,10.716736,10.742149,10.766657,10.790354,10.813324,10.835636,10.857354,10.878531,10.899215,10.919450,10.939274,10.958722,10.977824,10.996608,11.015102,11.033328,11.051309,11.069064,11.086612,11.103972,11.121159,11.138189,11.155076,11.171835,11.188478,11.205018,11.221468,11.237838,11.254140,11.270385,11.286584,11.302747,11.318885,11.335007,11.351124,11.367247,11.383384,11.399548,11.415746,11.431992,11.448294,11.464664,11.481113,11.497653,11.514296,11.531055,11.547942,11.564972,11.582159,11.599519,11.617068,11.634823,11.652803,11.671029,11.689523,11.708308,11.727410,11.746857,11.766681,11.786916,11.807601,11.828778,11.850495,11.872808,11.895777,11.919474,11.943982,11.969395,11.995827,12.023410,12.052306,12.082710,12.114865,12.149075,12.185733,12.225356,12.268646,12.316593,12.370662,12.433162,12.508076,12.603271,12.738376,12.999256
|
||||||
|
random_walk,1095,2029-09-22,9.314655,9.634166,9.799636,9.916225,10.007976,10.084523,10.150743,10.209465,10.262485,10.311013,10.355910,10.397809,10.437190,10.474427,10.509818,10.543600,10.575972,10.607097,10.637112,10.666135,10.694267,10.721594,10.748192,10.774129,10.799462,10.824245,10.848524,10.872342,10.895737,10.918744,10.941394,10.963716,10.985738,11.007483,11.028976,11.050237,11.071287,11.092144,11.112827,11.133352,11.153735,11.173993,11.194139,11.214188,11.234154,11.254050,11.273890,11.293685,11.313450,11.333196,11.352935,11.372681,11.392446,11.412242,11.432081,11.451977,11.471943,11.491992,11.512139,11.532396,11.552780,11.573305,11.593987,11.614845,11.635894,11.657155,11.678648,11.700394,11.722415,11.744738,11.767388,11.790394,11.813789,11.837607,11.861886,11.886669,11.912002,11.937939,11.964537,11.991864,12.019996,12.049019,12.079035,12.110160,12.142531,12.176314,12.211704,12.248941,12.288323,12.330222,12.375119,12.423647,12.476666,12.535389,12.601609,12.678155,12.769907,12.886495,13.051965,13.371476
|
||||||
|
random_walk,1460,2030-09-22,9.000859,9.369798,9.560867,9.695492,9.801437,9.889825,9.966290,10.034097,10.095318,10.151353,10.203196,10.251577,10.297051,10.340049,10.380914,10.419922,10.457302,10.493242,10.527901,10.561414,10.593898,10.625452,10.656165,10.686114,10.715366,10.743983,10.772019,10.799521,10.826535,10.853101,10.879255,10.905030,10.930459,10.955568,10.980386,11.004936,11.029242,11.053326,11.077209,11.100909,11.124446,11.147837,11.171100,11.194251,11.217305,11.240279,11.263188,11.286046,11.308868,11.331669,11.354462,11.377263,11.400085,11.422943,11.445852,11.468826,11.491881,11.515031,11.538294,11.561686,11.585222,11.608923,11.632805,11.656889,11.681195,11.705745,11.730563,11.755673,11.781101,11.806876,11.833030,11.859596,11.886610,11.914113,11.942148,11.970765,12.000017,12.029966,12.060679,12.092234,12.124717,12.158231,12.192890,12.228829,12.266209,12.305218,12.346083,12.389081,12.434554,12.482935,12.534778,12.590813,12.652034,12.719842,12.796306,12.884694,12.990640,13.125265,13.316333,13.685272
|
||||||
|
drift_rw,30,2026-10-23,11.038036,11.090922,11.118311,11.137609,11.152795,11.165466,11.176426,11.186146,11.194922,11.202954,11.210386,11.217321,11.223839,11.230003,11.235861,11.241453,11.246811,11.251963,11.256931,11.261735,11.266391,11.270914,11.275317,11.279610,11.283803,11.287905,11.291924,11.295866,11.299739,11.303547,11.307296,11.310991,11.314636,11.318235,11.321793,11.325312,11.328796,11.332248,11.335672,11.339069,11.342443,11.345796,11.349131,11.352449,11.355754,11.359047,11.362331,11.365608,11.368879,11.372147,11.375415,11.378683,11.381955,11.385231,11.388515,11.391808,11.395113,11.398432,11.401766,11.405119,11.408493,11.411890,11.415314,11.418766,11.422250,11.425770,11.429327,11.432926,11.436571,11.440266,11.444015,11.447823,11.451696,11.455638,11.459657,11.463759,11.467952,11.472245,11.476648,11.481171,11.485827,11.490631,11.495600,11.500751,11.506110,11.511701,11.517559,11.523723,11.530241,11.537176,11.544608,11.552640,11.561416,11.571136,11.582097,11.594767,11.609953,11.629251,11.656640,11.709526
|
||||||
|
drift_rw,91,2026-12-23,10.851487,10.943595,10.991297,11.024907,11.051357,11.073424,11.092514,11.109442,11.124726,11.138716,11.151659,11.163738,11.175090,11.185825,11.196027,11.205766,11.215098,11.224071,11.232724,11.241091,11.249200,11.257078,11.264746,11.272223,11.279526,11.286670,11.293670,11.300536,11.307280,11.313912,11.320442,11.326877,11.333225,11.339494,11.345690,11.351819,11.357887,11.363900,11.369863,11.375779,11.381656,11.387495,11.393303,11.399083,11.404839,11.410574,11.416294,11.422000,11.427698,11.433390,11.439081,11.444773,11.450471,11.456178,11.461897,11.467633,11.473388,11.479168,11.484976,11.490816,11.496692,11.502609,11.508571,11.514584,11.520652,11.526781,11.532977,11.539246,11.545594,11.552029,11.558559,11.565191,11.571936,11.578802,11.585801,11.592945,11.600248,11.607725,11.615393,11.623271,11.631381,11.639748,11.648400,11.657373,11.666705,11.676444,11.686646,11.697381,11.708734,11.720812,11.733755,11.747745,11.763029,11.779958,11.799048,11.821115,11.847565,11.881175,11.928876,12.020985
|
||||||
|
drift_rw,182,2027-03-24,10.702446,10.832707,10.900167,10.947699,10.985105,11.016312,11.043309,11.067250,11.088865,11.108649,11.126953,11.144035,11.160091,11.175272,11.189700,11.203473,11.216670,11.229359,11.241596,11.253429,11.264898,11.276039,11.286883,11.297457,11.307785,11.317888,11.327787,11.337497,11.347035,11.356415,11.365649,11.374749,11.383727,11.392593,11.401355,11.410023,11.418605,11.427108,11.435540,11.443908,11.452218,11.460477,11.468690,11.476864,11.485004,11.493115,11.501203,11.509274,11.517332,11.525382,11.533430,11.541480,11.549538,11.557608,11.565696,11.573808,11.581948,11.590121,11.598335,11.606594,11.614904,11.623272,11.631704,11.640207,11.648789,11.657457,11.666219,11.675084,11.684062,11.693163,11.702397,11.711776,11.721314,11.731025,11.740923,11.751027,11.761355,11.771929,11.782773,11.793914,11.805383,11.817215,11.829452,11.842141,11.855339,11.869112,11.883540,11.898721,11.914776,11.931858,11.950162,11.969946,11.991562,12.015502,12.042499,12.073707,12.111113,12.158645,12.226105,12.356366
|
||||||
|
drift_rw,365,2027-09-23,10.545666,10.730136,10.825670,10.892983,10.945955,10.990150,11.028382,11.062285,11.092896,11.120914,11.146835,11.171025,11.193762,11.215261,11.235694,11.255198,11.273888,11.291858,11.309187,11.325944,11.342186,11.357963,11.373319,11.388294,11.402920,11.417228,11.431246,11.444997,11.458504,11.471787,11.484864,11.497752,11.510466,11.523021,11.535430,11.547705,11.559858,11.571900,11.583841,11.595691,11.607460,11.619155,11.630787,11.642362,11.653890,11.665377,11.676831,11.688260,11.699671,11.711071,11.722468,11.733868,11.745279,11.756708,11.768163,11.779650,11.791177,11.802753,11.814384,11.826080,11.837848,11.849698,11.861639,11.873681,11.885834,11.898110,11.910518,11.923073,11.935787,11.948675,11.961752,11.975035,11.988542,12.002293,12.016311,12.030619,12.045245,12.060220,12.075576,12.091354,12.107596,12.124352,12.141682,12.159651,12.178341,12.197846,12.218278,12.239777,12.262514,12.286704,12.312626,12.340643,12.371254,12.405158,12.443390,12.487584,12.540557,12.607869,12.703403,12.887873
|
||||||
|
drift_rw,730,2028-09-22,10.434283,10.695163,10.830269,10.925463,11.000378,11.062878,11.116946,11.164893,11.208183,11.247806,11.284464,11.318675,11.350829,11.381234,11.410129,11.437713,11.464144,11.489557,11.514065,11.537762,11.560732,11.583044,11.604762,11.625939,11.646623,11.666858,11.686682,11.706130,11.725232,11.744016,11.762510,11.780736,11.798717,11.816472,11.834020,11.851380,11.868567,11.885597,11.902484,11.919243,11.935886,11.952426,11.968876,11.985246,12.001548,12.017793,12.033992,12.050155,12.066293,12.082415,12.098532,12.114655,12.130792,12.146956,12.163154,12.179400,12.195702,12.212072,12.228521,12.245061,12.261704,12.278463,12.295350,12.312380,12.329567,12.346927,12.364476,12.382231,12.400211,12.418437,12.436931,12.455716,12.474818,12.494265,12.514089,12.534324,12.555009,12.576186,12.597903,12.620216,12.643185,12.666882,12.691390,12.716803,12.743235,12.770818,12.799714,12.830118,12.862273,12.896483,12.933141,12.972764,13.016054,13.064001,13.118070,13.180570,13.255484,13.350679,13.485784,13.746664
|
||||||
|
drift_rw,1095,2029-09-22,10.435767,10.755278,10.920748,11.037337,11.129088,11.205635,11.271855,11.330577,11.383597,11.432125,11.477022,11.518921,11.558302,11.595539,11.630930,11.664712,11.697084,11.728209,11.758224,11.787247,11.815379,11.842706,11.869304,11.895241,11.920574,11.945357,11.969636,11.993454,12.016849,12.039856,12.062506,12.084828,12.106850,12.128595,12.150088,12.171349,12.192399,12.213256,12.233939,12.254464,12.274847,12.295105,12.315251,12.335300,12.355266,12.375162,12.395002,12.414797,12.434562,12.454308,12.474048,12.493793,12.513558,12.533354,12.553193,12.573089,12.593055,12.613104,12.633251,12.653508,12.673892,12.694417,12.715099,12.735957,12.757006,12.778268,12.799760,12.821506,12.843527,12.865850,12.888500,12.911506,12.934901,12.958719,12.982998,13.007781,13.033114,13.059051,13.085649,13.112976,13.141108,13.170131,13.200147,13.231272,13.263643,13.297426,13.332816,13.370053,13.409435,13.451334,13.496231,13.544759,13.597778,13.656501,13.722721,13.799267,13.891019,14.007607,14.173077,14.492588
|
||||||
|
drift_rw,1460,2030-09-22,10.495675,10.864614,11.055683,11.190308,11.296253,11.384641,11.461106,11.528913,11.590134,11.646169,11.698012,11.746393,11.791867,11.834865,11.875730,11.914738,11.952118,11.988058,12.022717,12.056230,12.088714,12.120268,12.150981,12.180930,12.210182,12.238799,12.266835,12.294337,12.321351,12.347917,12.374071,12.399846,12.425275,12.450384,12.475202,12.499752,12.524058,12.548142,12.572025,12.595725,12.619262,12.642653,12.665916,12.689067,12.712121,12.735095,12.758004,12.780862,12.803684,12.826485,12.849278,12.872079,12.894901,12.917759,12.940668,12.963642,12.986697,13.009847,13.033110,13.056502,13.080038,13.103739,13.127621,13.151705,13.176011,13.200561,13.225379,13.250489,13.275917,13.301692,13.327846,13.354412,13.381426,13.408929,13.436964,13.465581,13.494833,13.524782,13.555495,13.587050,13.619533,13.653047,13.687706,13.723645,13.761025,13.800034,13.840899,13.883897,13.929370,13.977751,14.029594,14.085629,14.146850,14.214658,14.291122,14.379510,14.485456,14.620081,14.811149,15.180088
|
||||||
|
cycle,30,2026-10-23,11.017695,11.070581,11.097969,11.117267,11.132454,11.145124,11.156085,11.165805,11.174581,11.182613,11.190045,11.196980,11.203498,11.209662,11.215520,11.221111,11.226469,11.231621,11.236590,11.241393,11.246050,11.250573,11.254976,11.259269,11.263462,11.267564,11.271583,11.275525,11.279397,11.283206,11.286955,11.290649,11.294294,11.297894,11.301451,11.304970,11.308455,11.311907,11.315330,11.318728,11.322102,11.325455,11.328789,11.332108,11.335413,11.338706,11.341990,11.345266,11.348538,11.351806,11.355073,11.358342,11.361613,11.364890,11.368174,11.371467,11.374772,11.378090,11.381425,11.384778,11.388152,11.391549,11.394973,11.398425,11.401909,11.405428,11.408986,11.412585,11.416230,11.419925,11.423674,11.427482,11.431354,11.435297,11.439316,11.443418,11.447611,11.451904,11.456306,11.460830,11.465486,11.470290,11.475258,11.480410,11.485768,11.491360,11.497218,11.503381,11.509900,11.516835,11.524266,11.532299,11.541075,11.550794,11.561755,11.574425,11.589612,11.608910,11.636299,11.689185
|
||||||
|
cycle,91,2026-12-23,10.823841,10.915949,10.963651,10.997261,11.023711,11.045778,11.064868,11.081796,11.097081,11.111070,11.124013,11.136092,11.147445,11.158179,11.168382,11.178120,11.187452,11.196425,11.205078,11.213445,11.221555,11.229432,11.237100,11.244577,11.251880,11.259025,11.266024,11.272890,11.279634,11.286267,11.292796,11.299231,11.305580,11.311848,11.318044,11.324173,11.330242,11.336254,11.342217,11.348134,11.354010,11.359850,11.365657,11.371437,11.377193,11.382929,11.388648,11.394355,11.400052,11.405745,11.411435,11.417127,11.422825,11.428532,11.434251,11.439987,11.445743,11.451522,11.457330,11.463170,11.469046,11.474963,11.480925,11.486938,11.493006,11.499135,11.505331,11.511600,11.517949,11.524384,11.530913,11.537545,11.544290,11.551156,11.558155,11.565300,11.572603,11.580080,11.587747,11.595625,11.603735,11.612102,11.620755,11.629727,11.639059,11.648798,11.659000,11.669735,11.681088,11.693167,11.706110,11.720099,11.735383,11.752312,11.771402,11.793469,11.819919,11.853529,11.901230,11.993339
|
||||||
|
cycle,182,2027-03-24,10.898356,11.028617,11.096077,11.143609,11.181015,11.212222,11.239219,11.263160,11.284775,11.304559,11.322863,11.339945,11.356000,11.371182,11.385610,11.399383,11.412580,11.425269,11.437506,11.449339,11.460808,11.471949,11.482793,11.493366,11.503695,11.513798,11.523697,11.533407,11.542945,11.552324,11.561558,11.570659,11.579637,11.588502,11.597265,11.605933,11.614514,11.623018,11.631450,11.639818,11.648128,11.656386,11.664600,11.672774,11.680914,11.689025,11.697113,11.705184,11.713242,11.721292,11.729339,11.737390,11.745447,11.753518,11.761606,11.769718,11.777857,11.786031,11.794245,11.802503,11.810814,11.819181,11.827614,11.836117,11.844699,11.853366,11.862129,11.870994,11.879972,11.889073,11.898307,11.907686,11.917224,11.926934,11.936833,11.946937,11.957265,11.967839,11.978682,11.989823,12.001292,12.013125,12.025362,12.038051,12.051249,12.065021,12.079450,12.094631,12.110686,12.127768,12.146072,12.165856,12.187472,12.211412,12.238409,12.269616,12.307022,12.354554,12.422015,12.552276
|
||||||
|
cycle,365,2027-09-23,10.691812,10.876281,10.971816,11.039128,11.092101,11.136295,11.174527,11.208431,11.239041,11.267059,11.292980,11.317171,11.339908,11.361407,11.381839,11.401343,11.420033,11.438003,11.455333,11.472089,11.488331,11.504108,11.519465,11.534439,11.549066,11.563374,11.577392,11.591143,11.604650,11.617933,11.631010,11.643898,11.656612,11.669167,11.681575,11.693850,11.706004,11.718045,11.729987,11.741837,11.753605,11.765301,11.776932,11.788508,11.800035,11.811522,11.822976,11.834405,11.845817,11.857217,11.868614,11.880014,11.891425,11.902854,11.914308,11.925795,11.937323,11.948898,11.960529,11.972225,11.983994,11.995844,12.007785,12.019827,12.031980,12.044255,12.056664,12.069219,12.081933,12.094821,12.107898,12.121180,12.134687,12.148439,12.162456,12.176765,12.191391,12.206365,12.221722,12.237499,12.253741,12.270498,12.287827,12.305797,12.324487,12.343991,12.364424,12.385923,12.408660,12.432850,12.458771,12.486789,12.517400,12.551303,12.589535,12.633729,12.686702,12.754015,12.849549,13.034019
|
||||||
|
cycle,730,2028-09-22,11.127521,11.388401,11.523506,11.618701,11.693615,11.756115,11.810184,11.858131,11.901421,11.941044,11.977702,12.011912,12.044067,12.074471,12.103367,12.130950,12.157382,12.182795,12.207303,12.231000,12.253970,12.276282,12.298000,12.319176,12.339861,12.360096,12.379920,12.399367,12.418469,12.437254,12.455748,12.473974,12.491954,12.509710,12.527258,12.544618,12.561805,12.578835,12.595722,12.612481,12.629124,12.645664,12.662113,12.678483,12.694785,12.711031,12.727230,12.743393,12.759530,12.775653,12.791770,12.807893,12.824030,12.840193,12.856392,12.872637,12.888940,12.905310,12.921759,12.938299,12.954942,12.971701,12.988588,13.005618,13.022805,13.040165,13.057713,13.075469,13.093449,13.111675,13.130169,13.148954,13.168055,13.187503,13.207327,13.227562,13.248246,13.269423,13.291141,13.313453,13.336423,13.360120,13.384628,13.410041,13.436472,13.464056,13.492952,13.523356,13.555510,13.589721,13.626379,13.666002,13.709292,13.757239,13.811307,13.873807,13.948722,14.043917,14.179022,14.439902
|
||||||
|
cycle,1095,2029-09-22,11.767059,12.086569,12.252039,12.368628,12.460380,12.536926,12.603146,12.661869,12.714888,12.763416,12.808313,12.850212,12.889594,12.926831,12.962221,12.996003,13.028375,13.059500,13.089515,13.118539,13.146670,13.173997,13.200596,13.226532,13.251866,13.276648,13.300928,13.324746,13.348141,13.371147,13.393797,13.416119,13.438141,13.459887,13.481379,13.502640,13.523690,13.544547,13.565230,13.585755,13.606139,13.626396,13.646542,13.666591,13.686557,13.706454,13.726293,13.746089,13.765853,13.785599,13.805339,13.825085,13.844849,13.864645,13.884484,13.904381,13.924347,13.944396,13.964542,13.984799,14.005183,14.025708,14.046391,14.067248,14.088298,14.109559,14.131051,14.152797,14.174819,14.197141,14.219791,14.242797,14.266192,14.290010,14.314290,14.339072,14.364406,14.390342,14.416941,14.444268,14.472399,14.501423,14.531438,14.562563,14.594935,14.628717,14.664107,14.701345,14.740726,14.782625,14.827522,14.876050,14.929069,14.987792,15.054012,15.130558,15.222310,15.338899,15.504369,15.823880
|
||||||
|
cycle,1460,2030-09-22,11.007316,11.376255,11.567323,11.701948,11.807894,11.896282,11.972747,12.040554,12.101775,12.157810,12.209653,12.258034,12.303507,12.346505,12.387370,12.426379,12.463759,12.499699,12.534357,12.567871,12.600354,12.631909,12.662622,12.692571,12.721823,12.750440,12.778475,12.805978,12.832992,12.859558,12.885712,12.911487,12.936916,12.962025,12.986843,13.011393,13.035699,13.059783,13.083665,13.107366,13.130903,13.154294,13.177557,13.200707,13.223762,13.246736,13.269645,13.292503,13.315325,13.338126,13.360919,13.383720,13.406542,13.429400,13.452309,13.475283,13.498337,13.521488,13.544751,13.568142,13.591679,13.615379,13.639262,13.663346,13.687652,13.712202,13.737020,13.762129,13.787558,13.813333,13.839487,13.866053,13.893067,13.920570,13.948605,13.977222,14.006474,14.036423,14.067136,14.098691,14.131174,14.164687,14.199346,14.235286,14.272666,14.311675,14.352539,14.395537,14.441011,14.489392,14.541235,14.597270,14.658491,14.726298,14.802763,14.891151,14.997096,15.131722,15.322790,15.691729
|
||||||
|
powerlaw,30,2026-10-23,11.032747,11.085633,11.113022,11.132320,11.147507,11.160177,11.171138,11.180857,11.189633,11.197666,11.205097,11.212032,11.218551,11.224714,11.230572,11.236164,11.241522,11.246674,11.251642,11.256446,11.261102,11.265626,11.270028,11.274321,11.278514,11.282617,11.286635,11.290578,11.294450,11.298258,11.302007,11.305702,11.309347,11.312946,11.316504,11.320023,11.323507,11.326959,11.330383,11.333780,11.337154,11.340507,11.343842,11.347160,11.350465,11.353758,11.357042,11.360319,11.363590,11.366859,11.370126,11.373394,11.376666,11.379942,11.383226,11.386520,11.389824,11.393143,11.396477,11.399831,11.403204,11.406602,11.410025,11.413478,11.416962,11.420481,11.424038,11.427638,11.431283,11.434978,11.438727,11.442535,11.446407,11.450349,11.454368,11.458470,11.462663,11.466956,11.471359,11.475882,11.480539,11.485343,11.490311,11.495463,11.500821,11.506413,11.512270,11.518434,11.524952,11.531888,11.539319,11.547351,11.556127,11.565847,11.576808,11.589478,11.604665,11.623963,11.651351,11.704237
|
||||||
|
powerlaw,91,2026-12-23,10.835084,10.927193,10.974894,11.008505,11.034955,11.057021,11.076111,11.093040,11.108324,11.122314,11.135257,11.147335,11.158688,11.169423,11.179625,11.189364,11.198696,11.207669,11.216322,11.224688,11.232798,11.240676,11.248344,11.255821,11.263124,11.270268,11.277267,11.284134,11.290878,11.297510,11.304040,11.310475,11.316823,11.323092,11.329288,11.335417,11.341485,11.347498,11.353460,11.359377,11.365253,11.371093,11.376901,11.382681,11.388436,11.394172,11.399891,11.405598,11.411296,11.416988,11.422679,11.428371,11.434069,11.439775,11.445495,11.451230,11.456986,11.462766,11.468574,11.474413,11.480290,11.486207,11.492169,11.498182,11.504250,11.510379,11.516575,11.522844,11.529192,11.535627,11.542157,11.548789,11.555533,11.562400,11.569399,11.576543,11.583846,11.591323,11.598991,11.606869,11.614979,11.623345,11.631998,11.640971,11.650303,11.660042,11.670244,11.680979,11.692332,11.704410,11.717353,11.731343,11.746627,11.763556,11.782645,11.804712,11.831162,11.864772,11.912474,12.004582
|
||||||
|
powerlaw,182,2027-03-24,10.668584,10.798845,10.866305,10.913837,10.951243,10.982450,11.009448,11.033388,11.055003,11.074788,11.093092,11.110174,11.126229,11.141410,11.155838,11.169611,11.182809,11.195498,11.207735,11.219567,11.231036,11.242177,11.253021,11.263595,11.273923,11.284027,11.293925,11.303636,11.313173,11.322553,11.331787,11.340888,11.349866,11.358731,11.367493,11.376161,11.384743,11.393246,11.401678,11.410046,11.418356,11.426615,11.434828,11.443002,11.451142,11.459254,11.467342,11.475412,11.483470,11.491520,11.499568,11.507618,11.515676,11.523746,11.531835,11.539946,11.548086,11.556260,11.564473,11.572732,11.581042,11.589410,11.597842,11.606345,11.614927,11.623595,11.632357,11.641223,11.650201,11.659301,11.668535,11.677915,11.687453,11.697163,11.707061,11.717165,11.727493,11.738067,11.748911,11.760052,11.771521,11.783353,11.795590,11.808280,11.821477,11.835250,11.849678,11.864859,11.880915,11.897996,11.916300,11.936085,11.957700,11.981641,12.008638,12.039845,12.077251,12.124783,12.192243,12.322504
|
||||||
|
powerlaw,365,2027-09-23,10.473609,10.658078,10.753612,10.820925,10.873898,10.918092,10.956324,10.990228,11.020838,11.048856,11.074777,11.098968,11.121704,11.143203,11.163636,11.183140,11.201830,11.219800,11.237129,11.253886,11.270128,11.285905,11.301262,11.316236,11.330862,11.345171,11.359188,11.372940,11.386447,11.399730,11.412807,11.425694,11.438409,11.450963,11.463372,11.475647,11.487800,11.499842,11.511783,11.523634,11.535402,11.547098,11.558729,11.570304,11.581832,11.593319,11.604773,11.616202,11.627613,11.639014,11.650410,11.661811,11.673222,11.684651,11.696105,11.707592,11.719119,11.730695,11.742326,11.754022,11.765790,11.777641,11.789582,11.801624,11.813777,11.826052,11.838461,11.851015,11.863730,11.876617,11.889694,11.902977,11.916484,11.930236,11.944253,11.958562,11.973188,11.988162,12.003519,12.019296,12.035538,12.052294,12.069624,12.087594,12.106284,12.125788,12.146220,12.167719,12.190456,12.214647,12.240568,12.268586,12.299196,12.333100,12.371332,12.415526,12.468499,12.535812,12.631346,12.815815
|
||||||
|
powerlaw,730,2028-09-22,10.274476,10.535355,10.670461,10.765655,10.840570,10.903070,10.957138,11.005085,11.048375,11.087998,11.124657,11.158867,11.191022,11.221426,11.250322,11.277905,11.304337,11.329750,11.354257,11.377955,11.400924,11.423237,11.444954,11.466131,11.486816,11.507051,11.526875,11.546322,11.565424,11.584209,11.602702,11.620928,11.638909,11.656664,11.674213,11.691572,11.708759,11.725789,11.742677,11.759435,11.776078,11.792619,11.809068,11.825438,11.841740,11.857985,11.874184,11.890347,11.906485,11.922607,11.938725,11.954847,11.970985,11.987148,12.003347,12.019592,12.035894,12.052264,12.068713,12.085254,12.101897,12.118655,12.135543,12.152573,12.169760,12.187119,12.204668,12.222423,12.240404,12.258630,12.277123,12.295908,12.315010,12.334457,12.354281,12.374516,12.395201,12.416378,12.438095,12.460408,12.483377,12.507075,12.531582,12.556996,12.583427,12.611010,12.639906,12.670310,12.702465,12.736675,12.773334,12.812957,12.856247,12.904194,12.958262,13.020762,13.095677,13.190871,13.325977,13.586856
|
||||||
|
powerlaw,1095,2029-09-22,10.174070,10.493580,10.659050,10.775639,10.867391,10.943937,11.010157,11.068880,11.121899,11.170427,11.215324,11.257223,11.296605,11.333842,11.369232,11.403014,11.435386,11.466511,11.496526,11.525550,11.553681,11.581008,11.607607,11.633543,11.658877,11.683659,11.707939,11.731757,11.755152,11.778158,11.800808,11.823130,11.845152,11.866898,11.888390,11.909651,11.930701,11.951558,11.972241,11.992766,12.013150,12.033407,12.053553,12.073602,12.093568,12.113465,12.133304,12.153100,12.172864,12.192610,12.212350,12.232096,12.251860,12.271656,12.291495,12.311392,12.331358,12.351407,12.371553,12.391810,12.412194,12.432719,12.453402,12.474259,12.495309,12.516570,12.538062,12.559808,12.581830,12.604152,12.626802,12.649808,12.673203,12.697021,12.721301,12.746083,12.771417,12.797353,12.823952,12.851279,12.879410,12.908434,12.938449,12.969574,13.001946,13.035728,13.071118,13.108356,13.147737,13.189636,13.234533,13.283061,13.336080,13.394803,13.461023,13.537569,13.629321,13.745910,13.911380,14.230891
|
||||||
|
powerlaw,1460,2030-09-22,10.119280,10.488219,10.679288,10.813913,10.919858,11.008247,11.084711,11.152518,11.213739,11.269775,11.321617,11.369998,11.415472,11.458470,11.499335,11.538343,11.575723,11.611663,11.646322,11.679835,11.712319,11.743873,11.774586,11.804535,11.833788,11.862404,11.890440,11.917942,11.944956,11.971522,11.997676,12.023452,12.048880,12.073990,12.098807,12.123357,12.147664,12.171747,12.195630,12.219330,12.242867,12.266258,12.289521,12.312672,12.335727,12.358701,12.381609,12.404467,12.427290,12.450090,12.472884,12.495684,12.518506,12.541364,12.564273,12.587247,12.610302,12.633453,12.656715,12.680107,12.703644,12.727344,12.751226,12.775310,12.799616,12.824167,12.848984,12.874094,12.899522,12.925298,12.951452,12.978017,13.005031,13.032534,13.060569,13.089186,13.118438,13.148387,13.179100,13.210655,13.243139,13.276652,13.311311,13.347251,13.384630,13.423639,13.464504,13.507502,13.552976,13.601356,13.653199,13.709234,13.770456,13.838263,13.914727,14.003115,14.109061,14.243686,14.434754,14.803694
|
||||||
|
powerlaw_ou,30,2026-10-23,11.087983,11.138510,11.164677,11.183114,11.197623,11.209728,11.220199,11.229486,11.237870,11.245544,11.252644,11.259270,11.265497,11.271386,11.276982,11.282325,11.287444,11.292366,11.297112,11.301702,11.306150,11.310472,11.314678,11.318780,11.322786,11.326705,11.330544,11.334311,11.338010,11.341648,11.345230,11.348760,11.352243,11.355681,11.359080,11.362442,11.365771,11.369069,11.372340,11.375586,11.378809,11.382013,11.385198,11.388369,11.391526,11.394673,11.397810,11.400940,11.404066,11.407188,11.410310,11.413433,11.416558,11.419688,11.422826,11.425972,11.429129,11.432300,11.435486,11.438689,11.441913,11.445158,11.448429,11.451727,11.455056,11.458418,11.461817,11.465256,11.468738,11.472268,11.475850,11.479488,11.483188,11.486954,11.490794,11.494713,11.498719,11.502820,11.507027,11.511348,11.515797,11.520386,11.525133,11.530055,11.535174,11.540516,11.546113,11.552001,11.558229,11.564855,11.571954,11.579629,11.588013,11.597299,11.607771,11.619876,11.634385,11.652822,11.678989,11.729515
|
||||||
|
powerlaw_ou,91,2026-12-23,11.021042,11.101360,11.142955,11.172263,11.195327,11.214569,11.231216,11.245977,11.259305,11.271504,11.282790,11.293322,11.303222,11.312583,11.321479,11.329971,11.338108,11.345932,11.353478,11.360773,11.367845,11.374715,11.381401,11.387921,11.394289,11.400519,11.406622,11.412609,11.418490,11.424273,11.429967,11.435578,11.441114,11.446581,11.451983,11.457328,11.462619,11.467862,11.473061,11.478221,11.483345,11.488437,11.493501,11.498541,11.503560,11.508562,11.513549,11.518525,11.523494,11.528457,11.533419,11.538383,11.543351,11.548327,11.553315,11.558316,11.563335,11.568375,11.573439,11.578532,11.583656,11.588815,11.594014,11.599257,11.604549,11.609893,11.615296,11.620762,11.626298,11.631909,11.637603,11.643386,11.649267,11.655255,11.661358,11.667588,11.673956,11.680476,11.687162,11.694031,11.701103,11.708399,11.715944,11.723768,11.731906,11.740398,11.749294,11.758655,11.768554,11.779087,11.790373,11.802572,11.815899,11.830661,11.847307,11.866549,11.889613,11.918921,11.960516,12.040834
|
||||||
|
powerlaw_ou,182,2027-03-24,11.053127,11.153396,11.205323,11.241911,11.270704,11.294726,11.315507,11.333935,11.350574,11.365803,11.379892,11.393041,11.405400,11.417086,11.428192,11.438793,11.448952,11.458720,11.468139,11.477247,11.486075,11.494651,11.502998,11.511138,11.519088,11.526865,11.534484,11.541959,11.549301,11.556521,11.563628,11.570634,11.577544,11.584369,11.591113,11.597786,11.604391,11.610937,11.617427,11.623869,11.630265,11.636622,11.642945,11.649237,11.655502,11.661746,11.667972,11.674184,11.680387,11.686583,11.692778,11.698975,11.705177,11.711390,11.717616,11.723859,11.730125,11.736417,11.742739,11.749096,11.755493,11.761934,11.768425,11.774970,11.781576,11.788248,11.794993,11.801817,11.808728,11.815733,11.822841,11.830061,11.837403,11.844877,11.852497,11.860274,11.868224,11.876363,11.884710,11.893286,11.902114,11.911222,11.920642,11.930409,11.940568,11.951170,11.962276,11.973962,11.986320,11.999469,12.013559,12.028788,12.045426,12.063854,12.084635,12.108657,12.137451,12.174038,12.225966,12.326235
|
||||||
|
powerlaw_ou,365,2027-09-23,11.218358,11.333244,11.392742,11.434664,11.467655,11.495179,11.518990,11.540104,11.559169,11.576618,11.592761,11.607827,11.621987,11.635377,11.648102,11.660249,11.671889,11.683080,11.693873,11.704309,11.714424,11.724250,11.733814,11.743140,11.752249,11.761160,11.769891,11.778455,11.786867,11.795139,11.803284,11.811310,11.819228,11.827047,11.834775,11.842420,11.849989,11.857489,11.864926,11.872306,11.879635,11.886919,11.894163,11.901372,11.908551,11.915705,11.922839,11.929957,11.937064,11.944164,11.951261,11.958361,11.965468,11.972586,11.979720,11.986874,11.994053,12.001262,12.008506,12.015790,12.023119,12.030499,12.037936,12.045436,12.053005,12.060650,12.068378,12.076197,12.084115,12.092141,12.100286,12.108558,12.116970,12.125534,12.134265,12.143176,12.152285,12.161611,12.171175,12.181001,12.191116,12.201552,12.212345,12.223536,12.235176,12.247323,12.260048,12.273438,12.287598,12.302664,12.318807,12.336257,12.355321,12.376435,12.400246,12.427770,12.460761,12.502683,12.562181,12.677067
|
||||||
|
powerlaw_ou,730,2028-09-22,11.564081,11.684393,11.746700,11.790602,11.825151,11.853974,11.878909,11.901021,11.920985,11.939259,11.956165,11.971942,11.986771,12.000792,12.014119,12.026839,12.039029,12.050749,12.062051,12.072980,12.083573,12.093863,12.103879,12.113645,12.123184,12.132516,12.141658,12.150627,12.159436,12.168099,12.176628,12.185034,12.193326,12.201514,12.209607,12.217613,12.225539,12.233393,12.241181,12.248910,12.256585,12.264213,12.271799,12.279349,12.286867,12.294359,12.301829,12.309284,12.316726,12.324161,12.331594,12.339029,12.346472,12.353926,12.361396,12.368888,12.376406,12.383956,12.391542,12.399170,12.406845,12.414574,12.422362,12.430216,12.438142,12.446148,12.454241,12.462429,12.470721,12.479127,12.487656,12.496319,12.505128,12.514097,12.523239,12.532571,12.542110,12.551877,12.561892,12.572182,12.582775,12.593704,12.605006,12.616726,12.628916,12.641637,12.654963,12.668984,12.683814,12.699591,12.716497,12.734770,12.754734,12.776846,12.801781,12.830605,12.865154,12.909055,12.971363,13.091674
|
||||||
|
powerlaw_ou,1095,2029-09-22,11.861940,11.982763,12.045336,12.089424,12.124120,12.153066,12.178107,12.200313,12.220362,12.238713,12.255691,12.271535,12.286427,12.300508,12.313891,12.326666,12.338908,12.350677,12.362028,12.373003,12.383641,12.393975,12.404033,12.413841,12.423421,12.432792,12.441973,12.450980,12.459827,12.468527,12.477092,12.485533,12.493861,12.502084,12.510211,12.518251,12.526211,12.534098,12.541919,12.549681,12.557389,12.565049,12.572668,12.580249,12.587799,12.595323,12.602826,12.610311,12.617785,12.625252,12.632717,12.640184,12.647658,12.655143,12.662646,12.670169,12.677720,12.685301,12.692919,12.700580,12.708288,12.716049,12.723871,12.731758,12.739718,12.747758,12.755885,12.764108,12.772436,12.780877,12.789442,12.798142,12.806989,12.815995,12.825177,12.834548,12.844128,12.853936,12.863994,12.874328,12.884966,12.895941,12.907291,12.919061,12.931303,12.944078,12.957460,12.971542,12.986434,13.002278,13.019256,13.037607,13.057656,13.079862,13.104903,13.133849,13.168545,13.212633,13.275206,13.396029
|
||||||
|
powerlaw_ou,1460,2030-09-22,12.129741,12.250614,12.313212,12.357318,12.392028,12.420986,12.446038,12.468253,12.488310,12.506668,12.523653,12.539504,12.554402,12.568489,12.581877,12.594657,12.606904,12.618678,12.630033,12.641013,12.651655,12.661993,12.672056,12.681868,12.691451,12.700827,12.710012,12.719022,12.727873,12.736576,12.745145,12.753589,12.761920,12.770147,12.778277,12.786321,12.794284,12.802174,12.809999,12.817763,12.825475,12.833138,12.840759,12.848344,12.855897,12.863424,12.870930,12.878418,12.885895,12.893365,12.900833,12.908303,12.915780,12.923269,12.930774,12.938301,12.945854,12.953439,12.961060,12.968724,12.976435,12.984200,12.992024,12.999914,13.007878,13.015921,13.024052,13.032278,13.040609,13.049054,13.057622,13.066326,13.075176,13.084187,13.093372,13.102747,13.112331,13.122143,13.132205,13.142543,13.153185,13.164165,13.175520,13.187294,13.199541,13.212321,13.225709,13.239796,13.254694,13.270545,13.287530,13.305888,13.325946,13.348161,13.373212,13.402170,13.436880,13.480986,13.543584,13.664457
|
||||||
|
powerlaw_ou_param,30,2026-10-23,11.075724,11.128181,11.155348,11.174490,11.189554,11.202121,11.212993,11.222634,11.231339,11.239306,11.246678,11.253557,11.260022,11.266136,11.271946,11.277493,11.282807,11.287917,11.292845,11.297610,11.302229,11.306716,11.311083,11.315341,11.319500,11.323569,11.327555,11.331466,11.335307,11.339084,11.342803,11.346467,11.350083,11.353653,11.357182,11.360672,11.364128,11.367553,11.370948,11.374318,11.377665,11.380991,11.384298,11.387590,11.390868,11.394135,11.397392,11.400642,11.403887,11.407129,11.410370,11.413612,11.416856,11.420107,11.423364,11.426630,11.429908,11.433200,11.436508,11.439834,11.443180,11.446550,11.449946,11.453370,11.456826,11.460317,11.463845,11.467415,11.471031,11.474696,11.478415,11.482192,11.486033,11.489943,11.493929,11.497998,11.502157,11.506416,11.510783,11.515269,11.519888,11.524653,11.529581,11.534691,11.540006,11.545552,11.551363,11.557476,11.563942,11.570821,11.578192,11.586159,11.594864,11.604505,11.615377,11.627945,11.643009,11.662150,11.689317,11.741774
|
||||||
|
powerlaw_ou_param,91,2026-12-23,10.963897,11.053216,11.099473,11.132065,11.157714,11.179113,11.197625,11.214041,11.228862,11.242428,11.254979,11.266692,11.277701,11.288111,11.298004,11.307448,11.316497,11.325198,11.333589,11.341703,11.349567,11.357206,11.364642,11.371892,11.378974,11.385902,11.392689,11.399348,11.405888,11.412319,11.418651,11.424891,11.431047,11.437126,11.443134,11.449078,11.454962,11.460793,11.466575,11.472313,11.478011,11.483674,11.489306,11.494911,11.500492,11.506054,11.511600,11.517134,11.522659,11.528179,11.533697,11.539217,11.544742,11.550276,11.555822,11.561384,11.566966,11.572571,11.578203,11.583866,11.589564,11.595301,11.601083,11.606914,11.612798,11.618742,11.624750,11.630829,11.636985,11.643226,11.649557,11.655989,11.662529,11.669187,11.675974,11.682902,11.689984,11.697235,11.704670,11.712310,11.720174,11.728287,11.736678,11.745379,11.754429,11.763873,11.773766,11.784176,11.795185,11.806897,11.819448,11.833014,11.847836,11.864252,11.882764,11.904162,11.929811,11.962404,12.008661,12.097980
|
||||||
|
powerlaw_ou_param,182,2027-03-24,10.917993,11.039547,11.102499,11.146854,11.181760,11.210881,11.236074,11.258414,11.278585,11.297047,11.314127,11.330067,11.345050,11.359216,11.372680,11.385532,11.397848,11.409689,11.421108,11.432150,11.442852,11.453248,11.463367,11.473235,11.482872,11.492301,11.501538,11.510599,11.519499,11.528252,11.536869,11.545361,11.553739,11.562012,11.570189,11.578277,11.586285,11.594220,11.602089,11.609897,11.617652,11.625359,11.633023,11.640651,11.648246,11.655816,11.663364,11.670895,11.678414,11.685926,11.693436,11.700948,11.708467,11.715998,11.723546,11.731115,11.738711,11.746338,11.754003,11.761710,11.769464,11.777273,11.785141,11.793076,11.801084,11.809173,11.817350,11.825623,11.834000,11.842493,11.851110,11.859862,11.868763,11.877824,11.887061,11.896489,11.906127,11.915994,11.926113,11.936510,11.947212,11.958254,11.969673,11.981514,11.993829,12.006682,12.020145,12.034312,12.049294,12.065234,12.082315,12.100777,12.120947,12.143288,12.168481,12.197602,12.232508,12.276863,12.339814,12.461369
|
||||||
|
powerlaw_ou_param,365,2027-09-23,10.943348,11.101554,11.183486,11.241215,11.286645,11.324547,11.357336,11.386412,11.412665,11.436693,11.458924,11.479670,11.499170,11.517608,11.535131,11.551858,11.567887,11.583299,11.598161,11.612532,11.626461,11.639992,11.653162,11.666004,11.678548,11.690819,11.702841,11.714635,11.726219,11.737610,11.748825,11.759878,11.770782,11.781549,11.792191,11.802719,11.813142,11.823469,11.833710,11.843873,11.853966,11.863996,11.873972,11.883899,11.893785,11.903637,11.913460,11.923262,11.933048,11.942825,11.952600,11.962377,11.972163,11.981965,11.991788,12.001640,12.011526,12.021453,12.031429,12.041459,12.051552,12.061715,12.071956,12.082283,12.092706,12.103234,12.113876,12.124643,12.135547,12.146600,12.157815,12.169206,12.180790,12.192584,12.204606,12.216877,12.229421,12.242263,12.255433,12.268964,12.282893,12.297264,12.312126,12.327538,12.343567,12.360294,12.377817,12.396255,12.415755,12.436501,12.458732,12.482760,12.509013,12.538089,12.570878,12.608780,12.654210,12.711939,12.793871,12.952077
|
||||||
|
powerlaw_ou_param,730,2028-09-22,11.120857,11.310984,11.409448,11.478825,11.533423,11.578972,11.618377,11.653320,11.684870,11.713747,11.740463,11.765396,11.788830,11.810988,11.832047,11.852150,11.871413,11.889934,11.907795,11.925065,11.941805,11.958066,11.973894,11.989328,12.004402,12.019150,12.033597,12.047770,12.061692,12.075382,12.088860,12.102143,12.115247,12.128187,12.140976,12.153628,12.166154,12.178565,12.190872,12.203086,12.215215,12.227270,12.239258,12.251188,12.263069,12.274908,12.286714,12.298494,12.310255,12.322004,12.333751,12.345501,12.357262,12.369041,12.380847,12.392686,12.404567,12.416498,12.428486,12.440540,12.452669,12.464883,12.477190,12.489602,12.502127,12.514779,12.527568,12.540508,12.553612,12.566895,12.580373,12.594064,12.607985,12.622158,12.636606,12.651353,12.666428,12.681861,12.697689,12.713950,12.730690,12.747961,12.765821,12.784342,12.803606,12.823708,12.844767,12.866926,12.890360,12.915292,12.942008,12.970885,13.002435,13.037378,13.076783,13.122332,13.176930,13.246307,13.344771,13.534898
|
||||||
|
powerlaw_ou_param,1095,2029-09-22,11.337474,11.540910,11.646266,11.720499,11.778919,11.827657,11.869820,11.907209,11.940967,11.971865,12.000452,12.027129,12.052204,12.075913,12.098446,12.119956,12.140568,12.160385,12.179496,12.197976,12.215887,12.233287,12.250222,12.266736,12.282866,12.298646,12.314105,12.329270,12.344166,12.358814,12.373236,12.387448,12.401470,12.415315,12.429000,12.442537,12.455940,12.469220,12.482389,12.495457,12.508436,12.521334,12.534161,12.546927,12.559639,12.572307,12.584939,12.597543,12.610128,12.622700,12.635269,12.647841,12.660425,12.673029,12.685661,12.698330,12.711042,12.723808,12.736635,12.749533,12.762512,12.775580,12.788749,12.802029,12.815432,12.828969,12.842653,12.856499,12.870520,12.884733,12.899155,12.913803,12.928699,12.943864,12.959323,12.975103,12.991233,13.007747,13.024682,13.042081,13.059993,13.078473,13.097584,13.117401,13.138013,13.159522,13.182056,13.205765,13.230840,13.257517,13.286104,13.317002,13.350760,13.388149,13.430312,13.479050,13.537469,13.611703,13.717059,13.920495
|
||||||
|
powerlaw_ou_param,1460,2030-09-22,11.556555,11.767714,11.877071,11.954123,12.014760,12.065348,12.109112,12.147921,12.182960,12.215032,12.244703,12.272394,12.298420,12.323030,12.346419,12.368745,12.390139,12.410709,12.430546,12.449727,12.468318,12.486378,12.503957,12.521098,12.537840,12.554219,12.570265,12.586006,12.601467,12.616672,12.631641,12.646393,12.660947,12.675318,12.689522,12.703573,12.717485,12.731269,12.744938,12.758503,12.771974,12.785362,12.798676,12.811926,12.825121,12.838270,12.851382,12.864465,12.877527,12.890576,12.903622,12.916672,12.929734,12.942816,12.955928,12.969077,12.982272,12.995522,13.008837,13.022225,13.035696,13.049260,13.062929,13.076714,13.090625,13.104676,13.118880,13.133252,13.147805,13.162558,13.177527,13.192731,13.208193,13.223934,13.239979,13.256358,13.273100,13.290241,13.307820,13.325880,13.344472,13.363653,13.383489,13.404059,13.425453,13.447780,13.471168,13.495778,13.521804,13.549495,13.579167,13.611238,13.646278,13.685086,13.728850,13.779439,13.840076,13.917127,14.026484,14.237643
|
||||||
|
cycle_on_powerlaw,30,2026-10-23,11.013061,11.065946,11.093335,11.112633,11.127820,11.140490,11.151451,11.161171,11.169946,11.177979,11.185410,11.192345,11.198864,11.205027,11.210885,11.216477,11.221835,11.226987,11.231955,11.236759,11.241416,11.245939,11.250341,11.254634,11.258828,11.262930,11.266948,11.270891,11.274763,11.278571,11.282320,11.286015,11.289660,11.293259,11.296817,11.300336,11.303820,11.307273,11.310696,11.314093,11.317467,11.320820,11.324155,11.327473,11.330778,11.334071,11.337355,11.340632,11.343903,11.347172,11.350439,11.353707,11.356979,11.360256,11.363539,11.366833,11.370137,11.373456,11.376791,11.380144,11.383518,11.386915,11.390338,11.393791,11.397275,11.400794,11.404351,11.407951,11.411596,11.415291,11.419040,11.422848,11.426720,11.430662,11.434681,11.438783,11.442977,11.447270,11.451672,11.456195,11.460852,11.465656,11.470624,11.475776,11.481134,11.486726,11.492583,11.498747,11.505265,11.512201,11.519632,11.527664,11.536440,11.546160,11.557121,11.569791,11.584978,11.604276,11.631665,11.684550
|
||||||
|
cycle_on_powerlaw,91,2026-12-23,10.794570,10.886678,10.934380,10.967990,10.994440,11.016507,11.035597,11.052525,11.067809,11.081799,11.094742,11.106821,11.118173,11.128908,11.139110,11.148849,11.158181,11.167154,11.175807,11.184174,11.192283,11.200161,11.207829,11.215306,11.222609,11.229753,11.236753,11.243619,11.250363,11.256996,11.263525,11.269960,11.276308,11.282577,11.288773,11.294902,11.300970,11.306983,11.312946,11.318863,11.324739,11.330579,11.336386,11.342166,11.347922,11.353657,11.359377,11.365083,11.370781,11.376474,11.382164,11.387856,11.393554,11.399261,11.404980,11.410716,11.416472,11.422251,11.428059,11.433899,11.439775,11.445692,11.451654,11.457667,11.463735,11.469864,11.476060,11.482329,11.488677,11.495113,11.501642,11.508274,11.515019,11.521885,11.528884,11.536028,11.543332,11.550808,11.558476,11.566354,11.574464,11.582831,11.591484,11.600456,11.609788,11.619527,11.629729,11.640464,11.651817,11.663896,11.676838,11.690828,11.706112,11.723041,11.742131,11.764198,11.790648,11.824258,11.871959,11.964068
|
||||||
|
cycle_on_powerlaw,182,2027-03-24,10.727596,10.857857,10.925317,10.972849,11.010255,11.041463,11.068460,11.092400,11.114016,11.133800,11.152104,11.169186,11.185241,11.200422,11.214850,11.228623,11.241821,11.254510,11.266747,11.278579,11.290048,11.301189,11.312033,11.322607,11.332935,11.343039,11.352937,11.362648,11.372186,11.381565,11.390799,11.399900,11.408878,11.417743,11.426505,11.435173,11.443755,11.452258,11.460690,11.469058,11.477368,11.485627,11.493840,11.502014,11.510154,11.518266,11.526354,11.534424,11.542482,11.550532,11.558580,11.566630,11.574688,11.582758,11.590847,11.598958,11.607098,11.615272,11.623485,11.631744,11.640054,11.648422,11.656854,11.665357,11.673939,11.682607,11.691369,11.700235,11.709213,11.718313,11.727547,11.736927,11.746465,11.756175,11.766073,11.776177,11.786505,11.797079,11.807923,11.819064,11.830533,11.842366,11.854602,11.867292,11.880489,11.894262,11.908690,11.923871,11.939927,11.957009,11.975313,11.995097,12.016712,12.040653,12.067650,12.098857,12.136263,12.183795,12.251255,12.381516
|
||||||
|
cycle_on_powerlaw,365,2027-09-23,10.459584,10.644053,10.739588,10.806900,10.859873,10.904067,10.942299,10.976203,11.006813,11.034831,11.060752,11.084943,11.107680,11.129179,11.149611,11.169115,11.187805,11.205775,11.223105,11.239861,11.256103,11.271880,11.287237,11.302211,11.316837,11.331146,11.345164,11.358915,11.372422,11.385705,11.398782,11.411669,11.424384,11.436938,11.449347,11.461622,11.473775,11.485817,11.497759,11.509609,11.521377,11.533073,11.544704,11.556280,11.567807,11.579294,11.590748,11.602177,11.613588,11.624989,11.636385,11.647786,11.659197,11.670626,11.682080,11.693567,11.705095,11.716670,11.728301,11.739997,11.751765,11.763616,11.775557,11.787599,11.799752,11.812027,11.824436,11.836991,11.849705,11.862592,11.875669,11.888952,11.902459,11.916211,11.930228,11.944537,11.959163,11.974137,11.989494,12.005271,12.021513,12.038270,12.055599,12.073569,12.092259,12.111763,12.132196,12.153695,12.176431,12.200622,12.226543,12.254561,12.285171,12.319075,12.357307,12.401501,12.454474,12.521787,12.617321,12.801790
|
||||||
|
cycle_on_powerlaw,730,2028-09-22,10.483953,10.744833,10.879939,10.975133,11.050048,11.112548,11.166616,11.214563,11.257853,11.297476,11.334134,11.368345,11.400499,11.430904,11.459799,11.487383,11.513814,11.539227,11.563735,11.587432,11.610402,11.632714,11.654432,11.675609,11.696293,11.716528,11.736352,11.755800,11.774902,11.793686,11.812180,11.830406,11.848387,11.866142,11.883690,11.901050,11.918237,11.935267,11.952154,11.968913,11.985556,12.002096,12.018545,12.034916,12.051218,12.067463,12.083662,12.099825,12.115962,12.132085,12.148202,12.164325,12.180462,12.196626,12.212824,12.229070,12.245372,12.261742,12.278191,12.294731,12.311374,12.328133,12.345020,12.362050,12.379237,12.396597,12.414146,12.431901,12.449881,12.468107,12.486601,12.505386,12.524488,12.543935,12.563759,12.583994,12.604679,12.625856,12.647573,12.669885,12.692855,12.716552,12.741060,12.766473,12.792905,12.820488,12.849384,12.879788,12.911943,12.946153,12.982811,13.022434,13.065724,13.113671,13.167740,13.230240,13.305154,13.400349,13.535454,13.796334
|
||||||
|
cycle_on_powerlaw,1095,2029-09-22,10.671517,10.991027,11.156497,11.273086,11.364838,11.441384,11.507604,11.566327,11.619346,11.667874,11.712771,11.754670,11.794052,11.831289,11.866679,11.900461,11.932833,11.963958,11.993973,12.022997,12.051128,12.078456,12.105054,12.130990,12.156324,12.181107,12.205386,12.229204,12.252599,12.275605,12.298255,12.320578,12.342599,12.364345,12.385837,12.407098,12.428148,12.449005,12.469688,12.490213,12.510597,12.530854,12.551000,12.571050,12.591015,12.610912,12.630751,12.650547,12.670311,12.690057,12.709797,12.729543,12.749307,12.769103,12.788943,12.808839,12.828805,12.848854,12.869000,12.889257,12.909641,12.930166,12.950849,12.971706,12.992756,13.014017,13.035510,13.057255,13.079277,13.101599,13.124249,13.147256,13.170650,13.194468,13.218748,13.243531,13.268864,13.294800,13.321399,13.348726,13.376857,13.405881,13.435896,13.467021,13.499393,13.533175,13.568565,13.605803,13.645184,13.687083,13.731980,13.780508,13.833527,13.892250,13.958470,14.035017,14.126768,14.243357,14.408827,14.728338
|
||||||
|
cycle_on_powerlaw,1460,2030-09-22,10.110988,10.479927,10.670995,10.805621,10.911566,10.999954,11.076419,11.144226,11.205447,11.261482,11.313325,11.361706,11.407180,11.450178,11.491042,11.530051,11.567431,11.603371,11.638030,11.671543,11.704026,11.735581,11.766294,11.796243,11.825495,11.854112,11.882147,11.909650,11.936664,11.963230,11.989384,12.015159,12.040588,12.065697,12.090515,12.115065,12.139371,12.163455,12.187337,12.211038,12.234575,12.257966,12.281229,12.304380,12.327434,12.350408,12.373317,12.396175,12.418997,12.441798,12.464591,12.487392,12.510214,12.533072,12.555981,12.578955,12.602010,12.625160,12.648423,12.671814,12.695351,12.719052,12.742934,12.767018,12.791324,12.815874,12.840692,12.865801,12.891230,12.917005,12.943159,12.969725,12.996739,13.024242,13.052277,13.080894,13.110146,13.140095,13.170808,13.202363,13.234846,13.268360,13.303018,13.338958,13.376338,13.415347,13.456212,13.499210,13.544683,13.593064,13.644907,13.700942,13.762163,13.829970,13.906435,13.994823,14.100768,14.235394,14.426462,14.795401
|
||||||
|
@@ -1,27 +1,5 @@
|
|||||||
"Date","Price","Open","High","Low","Vol.","Change %"
|
"Date","Price","Open","High","Low","Vol.","Change %"
|
||||||
"12/19/2024","97,466.1","100,190.5","102,778.8","95,672.2","200.57K","-2.73%"
|
"11/27/2024","92,699.5","91,975.5","93,024.5","91,795.5","125.81K","0.78%"
|
||||||
"12/18/2024","100,197.8","106,140.7","106,477.6","100,049.3","187.49K","-5.60%"
|
|
||||||
"12/17/2024","106,138.9","106,053.4","108,244.9","105,350.6","149.37K","0.08%"
|
|
||||||
"12/16/2024","106,057.6","104,479.2","107,767.6","103,351.7","232.66K","1.55%"
|
|
||||||
"12/15/2024","104,443.0","101,417.9","105,120.9","101,234.9","133.99K","2.98%"
|
|
||||||
"12/14/2024","101,417.7","101,423.7","102,633.0","100,626.3","105.31K","-0.01%"
|
|
||||||
"12/13/2024","101,426.2","100,008.3","101,891.2","99,214.2","162.40K","1.42%"
|
|
||||||
"12/12/2024","100,009.9","101,126.3","102,495.3","99,334.5","188.96K","-1.10%"
|
|
||||||
"12/11/2024","101,126.2","96,603.2","101,877.1","95,689.5","250.02K","4.69%"
|
|
||||||
"12/10/2024","96,600.3","97,311.5","98,237.8","94,304.5","271.45K","-0.78%"
|
|
||||||
"12/09/2024","97,359.4","101,129.7","101,198.6","94,395.8","321.01K","-3.72%"
|
|
||||||
"12/08/2024","101,115.8","99,837.0","101,339.9","98,713.9","128.93K","1.28%"
|
|
||||||
"12/07/2024","99,837.0","99,757.2","100,441.8","98,969.0","118.44K","0.08%"
|
|
||||||
"12/06/2024","99,760.7","96,968.9","101,908.5","96,452.0","213.49K","2.88%"
|
|
||||||
"12/05/2024","96,970.8","98,624.8","103,719.4","92,510.4","245.79K","-1.69%"
|
|
||||||
"12/04/2024","98,635.8","95,900.1","99,030.7","94,625.3","113.77K","2.85%"
|
|
||||||
"12/03/2024","95,898.7","95,843.9","96,245.0","93,646.1","95.08K","0.05%"
|
|
||||||
"12/02/2024","95,846.6","97,199.3","98,082.0","94,510.6","95.80K","-1.39%"
|
|
||||||
"12/01/2024","97,201.4","96,404.7","97,813.9","95,721.8","42.90K","0.83%"
|
|
||||||
"11/30/2024","96,405.7","97,438.5","97,461.9","96,119.6","36.92K","-1.06%"
|
|
||||||
"11/29/2024","97,438.0","95,645.6","98,599.8","95,405.7","76.54K","1.87%"
|
|
||||||
"11/28/2024","95,645.5","95,881.4","96,549.0","94,648.8","68.17K","-0.25%"
|
|
||||||
"11/27/2024","95,886.6","91,975.5","97,219.8","91,795.5","106.39K","4.25%"
|
|
||||||
"11/26/2024","91,977.6","93,046.5","94,978.8","90,817.2","131.88K","-1.17%"
|
"11/26/2024","91,977.6","93,046.5","94,978.8","90,817.2","131.88K","-1.17%"
|
||||||
"11/25/2024","93,064.4","97,948.8","98,872.4","92,875.3","124.98K","-4.97%"
|
"11/25/2024","93,064.4","97,948.8","98,872.4","92,875.3","124.98K","-4.97%"
|
||||||
"11/24/2024","97,935.2","97,696.4","98,552.6","95,765.4","77.04K","0.24%"
|
"11/24/2024","97,935.2","97,696.4","98,552.6","95,765.4","77.04K","0.24%"
|
||||||
|
|
Before Width: | Height: | Size: 783 KiB After Width: | Height: | Size: 783 KiB |
@@ -0,0 +1,65 @@
|
|||||||
|
# Ideas in the 2024 model's history
|
||||||
|
|
||||||
|
A catalogue of the modeling ideas in the old `model.py` and its branches
|
||||||
|
(September 2026). Labels are referenced from `btcmodel/experiments.py`.
|
||||||
|
"Fudge" means the constant or rule existed to hit backtest coverage/MAPE
|
||||||
|
targets rather than to express a hypothesis.
|
||||||
|
|
||||||
|
## Drift
|
||||||
|
|
||||||
|
- **D1. Mean return by halving-cycle position.** The core idea throughout
|
||||||
|
(initial commit, `Switch to simpler log-based projection`, `Implement trend
|
||||||
|
smoothing`, `tuning-b`). Smoothing went Savitzky-Golay → 60-day centred mean
|
||||||
|
→ Gaussian kernel with recency weights. The old code stretched every cycle
|
||||||
|
to 1460 days (phase = fraction of the halving interval). Bugs: simple
|
||||||
|
returns compounded as log returns (+~25%/yr bias) in the initial commit;
|
||||||
|
genesis date off by a year; `tuning-3` regressed log price on row index
|
||||||
|
across cycles. The "blend ratio of 0 is best?" commit was blending the
|
||||||
|
simple- and log-return versions of the same quantity.
|
||||||
|
- **D2. Drift damping.** Constants everywhere (×0.6–0.9, asymmetric, era- and
|
||||||
|
cycle-position-keyed, a 3%/day cap, the fundamentals ×0.65–0.75). Fudge as
|
||||||
|
implemented; the hypothesis underneath (the cycle drift is overfit) is real.
|
||||||
|
- **D3. Diminishing returns.** Initial commit decayed drift by 0.9^(t/365);
|
||||||
|
`old/backtests-trend-enhancement-1` regressed per-cycle returns on cycle
|
||||||
|
number, but added that on top of the cycle drift (double counting) and
|
||||||
|
clipped paths.
|
||||||
|
- **D4. "Skew".** `loc += sign(μ)·0.087σ`, tuned so 68% coverage hit 68.1%.
|
||||||
|
A fudge; its honest cousin is momentum.
|
||||||
|
- **D5. Stock-to-flow.** A 30% blend of the daily % change in S2F into the
|
||||||
|
drift. Broken: mismatched units, and S2F "halvings" on the wrong dates
|
||||||
|
created a large fake post-halving drift.
|
||||||
|
|
||||||
|
## Volatility
|
||||||
|
|
||||||
|
- **V1. EWMA blend.** 30/90/180-day spans, weights .5/.3/.2 then .2/.5/.3,
|
||||||
|
times a 1.2 fudge.
|
||||||
|
- **V2. Adaptive windows / regime weights.** Short/long vol ratio rescales
|
||||||
|
window lengths and blend weights; over-parameterised, and after the
|
||||||
|
fundamentals rewrite the adaptive windows were computed but unused.
|
||||||
|
- **V3. Cycle-position volatility.** `old/backtest-vol-cycle`.
|
||||||
|
- **V4. Market maturity.** Volume growth, inverse vol, autocorrelation and a
|
||||||
|
post-futures dummy, min-max normalised in-sample. Removed as "complexity
|
||||||
|
without clear benefit".
|
||||||
|
- **V5. "Fundamentals".** Supply growth, volume/supply and "depth" combined
|
||||||
|
and clipped to [0.65, 0.75]: effectively a constant, grid-searched to match
|
||||||
|
the era constants it replaced.
|
||||||
|
- **V6. Market conditions.** Vol ratio, MA50−MA200 trend strength (≈30× too
|
||||||
|
large from a units bug) and drawdown widening uncertainty.
|
||||||
|
|
||||||
|
## Calibration (all fudges)
|
||||||
|
|
||||||
|
- **C1. Era scaling keyed to the backtest's training start date**, with stacked
|
||||||
|
factors (net ≈0.45× vol) and hand-picked era boundaries.
|
||||||
|
- **C2. Horizon uncertainty multipliers**, caps and floors on daily vol.
|
||||||
|
- **C3. Changing the quantile levels**: a "95%" band read off the ~81%
|
||||||
|
quantiles past a year.
|
||||||
|
|
||||||
|
## Shape
|
||||||
|
|
||||||
|
- **S1. Skewed innovations**, with skew set per era (`tuning-1`).
|
||||||
|
|
||||||
|
## Also
|
||||||
|
|
||||||
|
Plumbing (backtest framework, multiprocessing, output handling, cycle and
|
||||||
|
CDPR plots). The old NOTES.md described "machine learning" and "macro
|
||||||
|
indicator" experiments; no such code ever existed.
|
||||||
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,49 @@
|
|||||||
run *name:
|
# List recipes
|
||||||
python3 ./model.py -n "{{ name }}"
|
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:
|
fmt:
|
||||||
black ./*.py
|
ruff format .
|
||||||
|
ruff check --fix --select I .
|
||||||
|
nix fmt flake.nix
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
ruff check ./model.py
|
ruff check .
|
||||||
|
ruff format --check .
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf output
|
rm -rf output
|
||||||
|
|
||||||
|
# Run A/B experiments on development data (default: all)
|
||||||
|
ab *names:
|
||||||
|
python -m btcmodel ab {{ names }}
|
||||||
|
|
||||||
|
# Record tracked models' forecasts for the forward test (commit the new file)
|
||||||
|
snapshot:
|
||||||
|
python -m btcmodel snapshot
|
||||||
|
|
||||||
|
# Score recorded forecasts whose target dates have passed
|
||||||
|
forward:
|
||||||
|
python -m btcmodel forward
|
||||||
|
|
||||||
|
# The weekly routine: new prices, a new snapshot, and scores so far
|
||||||
|
weekly: update snapshot forward
|
||||||
|
|||||||
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]
|
[project]
|
||||||
name = "bitcoin-model"
|
name = "btcmodel"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
description = ""
|
description = "Probabilistic Bitcoin price model"
|
||||||
authors = ["Anthropic Claude <[email protected]>", "Sam Fredrickson <[email protected]>"]
|
authors = [
|
||||||
|
{ name = "Anthropic Claude", email = "[email protected]" },
|
||||||
|
{ name = "Sam Fredrickson", email = "[email protected]" },
|
||||||
|
]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.13"
|
||||||
|
dependencies = ["numpy>=2", "pandas>=3", "scipy>=1.14", "matplotlib>=3.9"]
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[dependency-groups]
|
||||||
python = "^3.13"
|
dev = ["pytest", "ruff"]
|
||||||
pandas = "^2.2.3"
|
|
||||||
matplotlib = "^3.9.2"
|
|
||||||
seaborn = "^0.13.2"
|
|
||||||
scipy = "^1.14.1"
|
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
line-length = 100
|
||||||
|
|
||||||
[build-system]
|
[tool.ruff.lint]
|
||||||
requires = ["poetry-core"]
|
select = ["E", "F", "I", "B", "UP"]
|
||||||
build-backend = "poetry.core.masonry.api"
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = ["tests"]
|
||||||
|
pythonpath = ["."]
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from btcmodel.experiments import EXPERIMENTS
|
||||||
|
from btcmodel.halving import GENESIS
|
||||||
|
from btcmodel.models.drift import CycleDrift, PowerLawDrift, ShrunkDrift
|
||||||
|
from btcmodel.models.shape import StudentT
|
||||||
|
from btcmodel.models.volatility import ReversionVol, TrailingVol, TrendReversionVol
|
||||||
|
|
||||||
|
from .test_models import synthetic_prices
|
||||||
|
|
||||||
|
ALL_VARIANTS = {m.name: m for e in EXPERIMENTS.values() for m in e.models}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("name", list(ALL_VARIANTS))
|
||||||
|
def test_experiment_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 = ALL_VARIANTS[name].forecast(prices, horizons)
|
||||||
|
assert np.isfinite(f.log_quantiles).all()
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
def test_shrunk_drift_scales_linearly():
|
||||||
|
prices = synthetic_prices(lambda day: np.where(day < 700, 0.002, -0.001))
|
||||||
|
horizons = np.array([100, 1000])
|
||||||
|
full = CycleDrift().expected_log_return(prices, horizons)
|
||||||
|
np.testing.assert_allclose(
|
||||||
|
ShrunkDrift(CycleDrift(), 0.5).expected_log_return(prices, horizons), full / 2
|
||||||
|
)
|
||||||
|
np.testing.assert_allclose(
|
||||||
|
ShrunkDrift(CycleDrift(), 0.0).expected_log_return(prices, horizons), 0
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_power_law_recovers_its_exponent():
|
||||||
|
dates = pd.date_range("2011-01-01", "2024-11-26", freq="D", name="date")
|
||||||
|
t = (dates - GENESIS).days.to_numpy()
|
||||||
|
prices = pd.DataFrame({"close": 1e-17 * t**5.8}, index=dates)
|
||||||
|
_, slope, _ = PowerLawDrift().fit(prices)
|
||||||
|
assert slope == pytest.approx(5.8, rel=1e-6)
|
||||||
|
expected = 5.8 * np.log((t[-1] + 365) / t[-1])
|
||||||
|
assert PowerLawDrift().expected_log_return(prices, np.array([365]))[0] == pytest.approx(
|
||||||
|
expected
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_reversion_vol_matches_trailing_when_already_at_long_run():
|
||||||
|
rng = np.random.default_rng(0)
|
||||||
|
dates = pd.date_range("2011-01-01", "2024-11-26", freq="D", name="date")
|
||||||
|
prices = pd.DataFrame(
|
||||||
|
{"close": 100 * np.exp(np.cumsum(0.03 * rng.standard_normal(len(dates))))}, index=dates
|
||||||
|
)
|
||||||
|
horizons = np.array([30, 365, 1460])
|
||||||
|
reverting = ReversionVol(now_span=1460, long_window=len(dates)).sd(prices, horizons)
|
||||||
|
flat = TrailingVol(window=len(dates)).sd(prices, horizons)
|
||||||
|
np.testing.assert_allclose(reverting, flat, rtol=0.05)
|
||||||
|
|
||||||
|
|
||||||
|
def test_student_t_shape_has_unit_variance_and_fatter_tails():
|
||||||
|
q = StudentT(4).standard_quantiles(None, np.array([1]))[0]
|
||||||
|
assert q[-1] > 2.576 # beyond the normal 99.5% quantile
|
||||||
|
assert np.interp(0.8413, np.linspace(0.005, 0.995, 100), q) < 1.0 # thinner shoulders
|
||||||
|
|
||||||
|
|
||||||
|
def test_trend_reversion_vol_levels_off():
|
||||||
|
# A power-law trend plus AR(1) deviations with a ~70-day half-life.
|
||||||
|
rng = np.random.default_rng(0)
|
||||||
|
dates = pd.date_range("2011-01-01", "2024-11-26", freq="D", name="date")
|
||||||
|
t = (dates - GENESIS).days.to_numpy()
|
||||||
|
gap = np.zeros(len(t))
|
||||||
|
for i in range(1, len(t)):
|
||||||
|
gap[i] = 0.99 * gap[i - 1] + 0.03 * rng.standard_normal()
|
||||||
|
prices = pd.DataFrame({"close": np.exp(-40 + 5.8 * np.log(t) + gap)}, index=dates)
|
||||||
|
|
||||||
|
horizons = np.array([30, 365, 1460, 5000])
|
||||||
|
plain = TrendReversionVol().sd(prices, horizons)
|
||||||
|
with_params = TrendReversionVol(parameter_uncertainty=True).sd(prices, horizons)
|
||||||
|
assert np.all(np.diff(plain) >= 0)
|
||||||
|
stationary = 0.03 / np.sqrt(1 - 0.99**2)
|
||||||
|
assert plain[-1] == pytest.approx(stationary, rel=0.15)
|
||||||
|
assert np.all(with_params >= plain)
|
||||||
@@ -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,57 @@
|
|||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from btcmodel import forward
|
||||||
|
from btcmodel.evaluate import HORIZONS, score
|
||||||
|
from btcmodel.models import MODELS
|
||||||
|
|
||||||
|
from .test_models import synthetic_prices
|
||||||
|
|
||||||
|
MODELS_UNDER_TEST = [MODELS["random_walk"], MODELS["powerlaw"]]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def prices():
|
||||||
|
return synthetic_prices(lambda day: 0.001 + 0 * day, end="2025-03-31", noise=0.03)
|
||||||
|
|
||||||
|
|
||||||
|
def test_snapshot_records_every_model_and_horizon(prices, tmp_path):
|
||||||
|
history = prices.loc[:"2024-11-26"]
|
||||||
|
path = forward.snapshot(history, MODELS_UNDER_TEST, tmp_path, pd.Timestamp("2024-11-27"))
|
||||||
|
recorded = pd.read_csv(path)
|
||||||
|
assert path.name == "2024-11-26.csv"
|
||||||
|
assert len(recorded) == len(MODELS_UNDER_TEST) * len(HORIZONS)
|
||||||
|
assert recorded.target.iloc[0] == "2024-12-26"
|
||||||
|
|
||||||
|
|
||||||
|
def test_snapshot_refuses_stale_data_and_duplicates(prices, tmp_path):
|
||||||
|
history = prices.loc[:"2024-11-26"]
|
||||||
|
with pytest.raises(ValueError, match="real time"):
|
||||||
|
forward.snapshot(history, MODELS_UNDER_TEST, tmp_path, pd.Timestamp("2024-12-10"))
|
||||||
|
forward.snapshot(history, MODELS_UNDER_TEST, tmp_path, pd.Timestamp("2024-11-27"))
|
||||||
|
with pytest.raises(FileExistsError):
|
||||||
|
forward.snapshot(history, MODELS_UNDER_TEST, tmp_path, pd.Timestamp("2024-11-27"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_scores_only_due_horizons_and_match_the_backtest(prices, tmp_path):
|
||||||
|
history = prices.loc[:"2024-11-26"]
|
||||||
|
forward.snapshot(history, MODELS_UNDER_TEST, tmp_path, pd.Timestamp("2024-11-27"))
|
||||||
|
|
||||||
|
assert forward.score_snapshots(prices.loc[:"2024-12-20"], tmp_path).empty
|
||||||
|
assert forward.next_due(prices.loc[:"2024-12-20"], tmp_path) == pd.Timestamp("2024-12-26")
|
||||||
|
|
||||||
|
scores = forward.score_snapshots(prices, tmp_path)
|
||||||
|
assert sorted(scores.horizon.unique()) == [30, 91]
|
||||||
|
|
||||||
|
model = MODELS["powerlaw"]
|
||||||
|
horizons = np.array([30, 91])
|
||||||
|
direct = score(
|
||||||
|
"powerlaw",
|
||||||
|
model.forecast(history, horizons),
|
||||||
|
np.log(
|
||||||
|
prices.close.loc[history.index[-1] + pd.to_timedelta(horizons, unit="D")]
|
||||||
|
).to_numpy(),
|
||||||
|
)
|
||||||
|
recorded = scores[scores.model == "powerlaw"].sort_values("horizon")
|
||||||
|
np.testing.assert_allclose(recorded.crps, direct.crps, atol=1e-5)
|
||||||
@@ -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, CycleDrift
|
||||||
|
|
||||||
|
|
||||||
|
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 = CycleDrift(prior_days=0).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 = CycleDrift(recency_half_life=0.25).by_cycle_day(prices)
|
||||||
|
equal = CycleDrift(recency_half_life=1e9).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