Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
573884a1c2 | ||
|
|
de2cd512cd | ||
|
|
8fca6181d5 | ||
|
|
4647c596b3 | ||
|
|
f1d38f9abb | ||
|
|
fb590105ce | ||
|
|
85e0a6839f | ||
|
|
ececd65a22 | ||
|
|
5538f666c5 | ||
|
|
6bfbd30e3d | ||
|
|
6398c6c8f4 | ||
|
|
22fc7fc6cd | ||
|
|
28b5240a81 | ||
|
|
e385765fda | ||
|
|
0ddb4ab01b |
@@ -1,160 +1,222 @@
|
||||
# BTC ML Trading Strategy Optimizer
|
||||
# Bitcoin Accumulation Zone Monitor
|
||||
|
||||
An automated optimization loop that trains ML models on BTC/USDT data, backtests trading strategies, and uses an LLM to iteratively improve the configuration.
|
||||
> Bitcoin on-chain metrics dashboard with classic equal-weight scoring, ML-optimized scoring, historical backtesting, and click-to-select metric context for long-term BTC accumulation decisions.
|
||||
|
||||

|
||||
|
||||
## What It Does
|
||||
|
||||
Monitors Bitcoin accumulation conditions using 16 scored market/on-chain indicators plus optional informational cycle metrics. Each scored metric receives a 0-10 score and rolls into a 0-100 accumulation score.
|
||||
|
||||
The dashboard now supports two scoring modes:
|
||||
|
||||
- **Classic** — transparent equal-weight scoring across every active metric.
|
||||
- **ML** — feature-importance weights trained against historical 365-day forward returns, with displayed per-metric weights and point contributions.
|
||||
|
||||
Historical backtests show score-vs-price behavior, score bracket performance, major signal events, and current-score context. Metric cards are clickable: selecting a metric overlays its historical series on the score chart and shows comparable historical periods with forward returns.
|
||||
|
||||
## Screenshots
|
||||
|
||||
### Main Dashboard — ML mode + metric context
|
||||

|
||||
*Live BTC price, Classic/ML scoring toggle, 16 active scored metrics, ML weights/contributions, metric sparklines, and click-to-select historical context.*
|
||||
|
||||
### Historical Backtest
|
||||

|
||||
*Current signal percentile, comparable historical periods by cycle, score-vs-BTC chart, bracket performance, and major signal events.*
|
||||
|
||||
### Settings
|
||||

|
||||
*LLM provider configuration for optional AI-powered signal commentary and local/cloud model selection.*
|
||||
|
||||
## Feature Highlights
|
||||
|
||||
- **16 scored metrics** from market sentiment, miner stress, valuation, holder behavior, network activity, and velocity signals.
|
||||
- **Classic vs ML scoring toggle** on the dashboard and backtest API.
|
||||
- **ML score explainability**: metric cards show learned weight and contribution in points.
|
||||
- **Leakage-resistant ML validation**: training uses purged time-series splits so 365-day forward-return labels do not overlap validation windows.
|
||||
- **Historical context panel**: compares the current composite score against historical periods and forward returns.
|
||||
- **Clickable metric cards**: select any metric to see percentile, similar historical levels, forward returns, example dates by market cycle, and highlighted chart periods.
|
||||
- **Score history chart** with BTC price overlay, range controls, and selected-metric overlay.
|
||||
- **Backtest dashboard** with current signal context, score bracket performance, and signal-crossing events.
|
||||
- **Quick vs full refresh**: quick refresh updates BTC price and Fear & Greed; full refresh re-scrapes on-chain sources.
|
||||
- **LLM settings UI** for Ollama, LM Studio, OpenAI, Anthropic, and OpenRouter.
|
||||
|
||||
## Metrics
|
||||
|
||||
| # | Metric | Source | Accumulation Signal |
|
||||
|---|--------|--------|-------------------|
|
||||
| 1 | Fear & Greed Index | alternative.me API | Extreme fear / capitulation sentiment |
|
||||
| 2 | Puell Multiple | LookIntoBitcoin | Miner revenue stress |
|
||||
| 3 | MVRV Z-Score | LookIntoBitcoin | Market near/below realized value |
|
||||
| 4 | Drawdown from ATH | Calculated from BTC price | Deep correction from cycle high |
|
||||
| 5 | Price vs 200W SMA | LookIntoBitcoin + BTC price | Price near/below long-term trend |
|
||||
| 6 | Reserve Risk | LookIntoBitcoin | High holder confidence relative to price |
|
||||
| 7 | RHODL Ratio | LookIntoBitcoin | Long-term holder dominance |
|
||||
| 8 | Net Unrealized Profit/Loss (NUPL) | LookIntoBitcoin | Capitulation / early recovery zones |
|
||||
| 9 | LTH Realized Price | LookIntoBitcoin | Price near long-term holder cost basis |
|
||||
| 10 | Hash Ribbons | LookIntoBitcoin | Miner capitulation/recovery signal |
|
||||
| 11 | SOPR | CheckOnChain | Spent outputs near loss / reset territory |
|
||||
| 12 | Sell-side Risk Ratio | CheckOnChain | Low realized profit/loss pressure |
|
||||
| 13 | Active Address Momentum | CheckOnChain | Network activity momentum extremes |
|
||||
| 14 | Transaction Count Momentum | CheckOnChain | Transaction activity momentum extremes |
|
||||
| 15 | NVT Price | CheckOnChain | Network-value valuation discount/premium |
|
||||
| 16 | VDD Multiple | CheckOnChain | Coin-days/velocity reset conditions |
|
||||
|
||||
Informational cards may also appear when data is available, such as **Long-Term Holder Supply** and **Pi Cycle Bottom**. These are displayed for context and are not included in the composite score.
|
||||
|
||||
## Score Interpretation
|
||||
|
||||
| Score | Assessment | Interpretation |
|
||||
|-------|-----------|----------------|
|
||||
| 80-100 | 🟢 Extreme Accumulation Zone | Broad capitulation/value conditions across active metrics |
|
||||
| 65-79 | 🟢 Strong Accumulation Zone | Historically attractive long-term entry territory |
|
||||
| 50-64 | 🟡 Moderate Opportunity | DCA-friendly, but not maximum-signal conditions |
|
||||
| 35-49 | 🟡 Neutral | Mixed signals; not compelling either direction |
|
||||
| 20-34 | 🔴 Caution — Overheated | Market conditions becoming less favorable |
|
||||
| 0-19 | 🔴 Extreme Caution | Historically poor accumulation setup |
|
||||
|
||||
Backtest tables provide actual historical forward-return statistics per score bracket, including 30d/90d/180d/1yr averages, win rate, max gain/loss, and average max drawdown.
|
||||
|
||||
## ML-Optimized Scoring
|
||||
|
||||
The ML mode uses a `GradientBoostingClassifier` trained on historical feature rows to predict whether a day was a good long-term buy based on 365-day forward return. Training features include:
|
||||
|
||||
- Classic metric scores.
|
||||
- Raw metric values.
|
||||
- 30-day metric deltas.
|
||||
- Interaction features such as MVRV × NUPL and Puell × Reserve Risk.
|
||||
- Cycle-position context such as days since ATH.
|
||||
|
||||
The resulting feature importances are aggregated back into transparent metric weights stored in `config/ml_weights.json`. The UI displays normalized weight and contribution for each active metric.
|
||||
|
||||
Validation uses purged expanding time-series splits: because each label uses a 365-day forward-return window, training rows whose label windows overlap validation are removed before scoring validation folds.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
| Component | Technology |
|
||||
|-----------|-----------|
|
||||
| Backend | Python 3.13 + FastAPI |
|
||||
| Frontend | Inline HTML/CSS/JS dark trading-terminal UI |
|
||||
| Charts | Chart.js |
|
||||
| Scraping | requests + Playwright-style browser scraping where needed |
|
||||
| Data APIs | alternative.me, CoinGecko, LookIntoBitcoin, CheckOnChain |
|
||||
| ML | NumPy + pandas + scikit-learn GradientBoostingClassifier |
|
||||
| Process Manager | pm2 or uvicorn |
|
||||
| Default Port | 3088 |
|
||||
|
||||
## How Data Is Collected
|
||||
|
||||
Data is collected from free/public sources and cached locally under `data/`.
|
||||
|
||||
- Fast live refreshes update BTC price, ATH/drawdown, 200D SMA/Mayer where possible, and Fear & Greed.
|
||||
- On-chain metrics are cached and reused because they update slowly.
|
||||
- Full refresh re-scrapes on-chain metrics from LookIntoBitcoin/CheckOnChain.
|
||||
- Historical backtest data lives in `data/history.json` and supports charting, backtests, and metric-context lookups.
|
||||
- Score history appends to `data/score_history.jsonl`.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
├── dashboard/
|
||||
│ └── server.py # FastAPI server + inline dashboard/backtest/settings UI
|
||||
├── scrapers/
|
||||
│ ├── lookintobitcoin.py # LookIntoBitcoin metric scraping
|
||||
│ ├── checkonchain.py # CheckOnChain metric scraping
|
||||
│ ├── history_collector.py # Full historical data collection
|
||||
│ ├── history_updater.py # Incremental historical updates
|
||||
│ ├── fear_greed.py # Fear & Greed Index API
|
||||
│ └── price.py # BTC price, ATH, drawdown, SMA helpers
|
||||
├── scoring/
|
||||
│ └── engine.py # Classic + ML-weighted scoring logic
|
||||
├── backtesting/
|
||||
│ └── engine.py # Historical backtest engine
|
||||
├── ml/
|
||||
│ └── optimizer.py # ML training, purged CV, weight export
|
||||
├── tests/
|
||||
│ ├── test_ml_optimizer_validation.py
|
||||
│ └── test_scoring_engine_ml.py
|
||||
├── data/
|
||||
│ ├── cache.json # Live metric cache
|
||||
│ ├── history.json # Historical metric/time-series data
|
||||
│ └── score_history.jsonl # Live score history
|
||||
├── config/
|
||||
│ ├── thresholds.json # Classic scoring thresholds
|
||||
│ ├── ml_weights.json # Learned ML metric weights
|
||||
│ └── llm_settings.json # Optional AI commentary provider config
|
||||
├── screenshots/ # README screenshots
|
||||
├── ARCHITECTURE.md
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Running
|
||||
|
||||
### Local / ad-hoc with uv
|
||||
|
||||
```bash
|
||||
cd /opt/data/btc-accumulation-monitor
|
||||
PYTHONPATH=. uv run \
|
||||
--with fastapi \
|
||||
--with uvicorn \
|
||||
--with requests \
|
||||
--with pandas \
|
||||
--with numpy \
|
||||
--with scikit-learn \
|
||||
python -m uvicorn dashboard.server:app --host 0.0.0.0 --port 3088
|
||||
```
|
||||
|
||||
### VPS-style install
|
||||
|
||||
```bash
|
||||
cd /opt/apps/btc-ml-optimizer
|
||||
python3 -m venv .venv
|
||||
. .venv/bin/activate
|
||||
pip install -r requirements_vps.txt pandas numpy scikit-learn
|
||||
python -m uvicorn dashboard.server:app --host 0.0.0.0 --port 3088
|
||||
```
|
||||
|
||||
### pm2
|
||||
|
||||
```bash
|
||||
pm2 start "python3 -m uvicorn dashboard.server:app --host 0.0.0.0 --port 3088" --name btc-ml-optimizer
|
||||
```
|
||||
|
||||
## First Run
|
||||
|
||||
1. Visit `http://localhost:3088` for the live dashboard.
|
||||
2. Use **Quick Refresh** for fast price/Fear & Greed updates.
|
||||
3. Use **Full Refresh** to re-scrape on-chain metrics.
|
||||
4. Visit `http://localhost:3088/backtest` to view historical score performance.
|
||||
5. If historical data is missing, use the backtest page's collection flow to populate `data/history.json`.
|
||||
|
||||
## Useful API Endpoints
|
||||
|
||||
| Endpoint | Description |
|
||||
|----------|-------------|
|
||||
| `GET /api/data?mode=classic` | Current metrics using equal-weight scoring |
|
||||
| `GET /api/data?mode=ml` | Current metrics using ML-optimized weights |
|
||||
| `GET /api/history` | Recent live score history |
|
||||
| `POST /api/refresh` | Quick refresh |
|
||||
| `POST /api/refresh?full=true` | Full on-chain refresh |
|
||||
| `GET /api/backtest?mode=classic` | Historical backtest with classic scoring |
|
||||
| `GET /api/backtest?mode=ml` | Historical backtest with ML scoring |
|
||||
| `GET /api/metric-context?metric=mvrv_zscore&mode=ml` | Similar historical levels and forward returns for one metric |
|
||||
| `GET /api/settings` | Safe LLM settings payload |
|
||||
|
||||
## Testing
|
||||
|
||||
Focused tests can be run with uv:
|
||||
|
||||
```bash
|
||||
cd /opt/data/btc-accumulation-monitor
|
||||
PYTHONPATH=. uv run --with pytest --with numpy --with scikit-learn --with pandas \
|
||||
pytest -q tests/test_ml_optimizer_validation.py tests/test_scoring_engine_ml.py
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Optimization Loop │
|
||||
│ │
|
||||
│ ┌──────────┐ ┌───────────────┐ ┌──────────────────────┐ │
|
||||
│ │ VPS │───>│ Windows PC │───>│ Mac Mini │ │
|
||||
│ │ (Orch.) │<───│ (GPU/ML) │ │ (LLM) │ │
|
||||
│ │ │<───────────────────────>│ │ │
|
||||
│ │ - Fetch │ │ - XGBoost │ │ - Ollama │ │
|
||||
│ │ data │ │ - LightGBM │ │ - qwen3.5:27b │ │
|
||||
│ │ - Coord │ │ - CatBoost │ │ - Analyze results │ │
|
||||
│ │ - Store │ │ - RTX 4070 Ti │ │ - Suggest changes │ │
|
||||
│ └──────────┘ └───────────────┘ └──────────────────────┘ │
|
||||
│ ▲ │ │
|
||||
│ └────────────────────────────────────────┘ │
|
||||
│ Modified config │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
See [ARCHITECTURE.md](ARCHITECTURE.md) for deeper implementation details on scoring, data collection, and backtesting.
|
||||
|
||||
### Machines (Tailscale)
|
||||
## License
|
||||
|
||||
| Machine | Role | Address | Key Resources |
|
||||
|------------|-------------|-------------------|---------------------|
|
||||
| VPS | Orchestrator | localhost | Coordination, data |
|
||||
| Windows PC | ML Engine | 100.76.218.38 | RTX 4070 Ti GPU |
|
||||
| Mac Mini | LLM | 100.100.242.21 | Ollama, qwen3.5:27b |
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
btc-ml-optimizer/
|
||||
├── orchestrator.py # Main loop — coordinates everything
|
||||
├── ml_engine/
|
||||
│ └── train_and_backtest.py # Self-contained ML script (runs on Windows)
|
||||
├── llm_client/
|
||||
│ └── analyzer.py # LLM strategy analyzer (calls Mac Mini)
|
||||
├── scripts/
|
||||
│ ├── fetch_data.py # BTC/USDT data fetcher (ccxt)
|
||||
│ └── setup_windows.sh # Install deps on Windows PC
|
||||
├── config/
|
||||
│ └── initial_config.json # Starting configuration
|
||||
├── data/ # OHLCV CSV files
|
||||
├── results/ # Iteration results + logs
|
||||
├── requirements_vps.txt # VPS Python dependencies
|
||||
└── requirements_windows.txt # Windows PC Python dependencies
|
||||
```
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. VPS (this machine)
|
||||
|
||||
```bash
|
||||
pip install -r requirements_vps.txt
|
||||
```
|
||||
|
||||
### 2. Windows PC
|
||||
|
||||
```bash
|
||||
# From VPS — installs all ML deps on Windows via SSH
|
||||
bash scripts/setup_windows.sh
|
||||
```
|
||||
|
||||
Or manually on Windows:
|
||||
```bash
|
||||
pip install -r requirements_windows.txt
|
||||
```
|
||||
|
||||
### 3. Mac Mini
|
||||
|
||||
Ensure Ollama is running with the qwen3.5:27b model:
|
||||
```bash
|
||||
ollama pull qwen3.5:27b
|
||||
ollama serve # should already be running
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Fetch Data
|
||||
|
||||
```bash
|
||||
python3 scripts/fetch_data.py
|
||||
```
|
||||
|
||||
Downloads 2 years of BTC/USDT 1h and 4h OHLCV data from Binance.
|
||||
|
||||
### Run the Optimizer
|
||||
|
||||
```bash
|
||||
python3 orchestrator.py
|
||||
```
|
||||
|
||||
The optimizer will:
|
||||
1. Ensure data is fetched
|
||||
2. Upload ML engine + data to Windows PC
|
||||
3. Train model and backtest on GPU
|
||||
4. Send results to LLM for analysis
|
||||
5. Apply LLM-suggested config changes
|
||||
6. Repeat until convergence (or 50 iterations)
|
||||
|
||||
### Run ML Engine Standalone (on Windows)
|
||||
|
||||
```bash
|
||||
python train_and_backtest.py --config config.json --data btc_4h.csv --output results.json
|
||||
```
|
||||
|
||||
## Configuration Reference
|
||||
|
||||
### `model_type`
|
||||
- `xgboost` — XGBoost with GPU (default, generally best)
|
||||
- `lightgbm` — LightGBM with GPU (faster training)
|
||||
- `catboost` — CatBoost with GPU (handles interactions well)
|
||||
- `ensemble` — Soft voting of all three
|
||||
|
||||
### `features`
|
||||
- `technical_indicators` — List of indicators to compute
|
||||
- `lookback_periods` — Windows for return/volatility features
|
||||
- `use_volume_features` — Include volume-derived features
|
||||
- `use_volatility_features` — Include volatility features
|
||||
- `use_candle_patterns` — Include candlestick pattern features
|
||||
- `use_lag_features` — Include lagged feature values
|
||||
- `lag_periods` — Specific lag periods to use
|
||||
|
||||
### `target`
|
||||
- `direction` — `"long"` or `"both"`
|
||||
- `horizon_candles` — Forward-looking prediction window
|
||||
- `threshold_pct` — Minimum % move to label as positive
|
||||
|
||||
### `hyperparameters`
|
||||
Standard gradient boosting params: `learning_rate`, `max_depth`, `n_estimators`, `subsample`, `colsample_bytree`, `min_child_weight`, `gamma`, `reg_alpha`, `reg_lambda`
|
||||
|
||||
### `strategy`
|
||||
- `entry_threshold` — Min probability to enter trade (0.5-0.8)
|
||||
- `stop_loss_pct` — Stop loss percentage
|
||||
- `take_profit_pct` — Take profit percentage
|
||||
- `trailing_stop_pct` — Trailing stop distance
|
||||
- `position_sizing` — `"confidence_scaled"` or `"fixed"`
|
||||
- `min_confidence_to_trade` — Absolute minimum confidence
|
||||
|
||||
### `training`
|
||||
- `walk_forward_windows` — Number of walk-forward splits (3-10)
|
||||
- `train_pct` / `validation_pct` / `test_pct` — Data split ratios
|
||||
|
||||
## Convergence Criteria
|
||||
|
||||
The optimizer stops when:
|
||||
- Sharpe ratio exceeds 3.0
|
||||
- Sharpe improvement < 1% over 5 consecutive iterations
|
||||
- Maximum 50 iterations reached
|
||||
|
||||
## Output
|
||||
|
||||
- `config/best_config.json` — Best configuration found
|
||||
- `results/iterations.jsonl` — Full log of every iteration
|
||||
- `results/results_iter_N.json` — Detailed results per iteration
|
||||
Private — not for public distribution.
|
||||
|
||||
+146
-45
@@ -25,45 +25,40 @@ BRACKETS = [
|
||||
(86, 100, "Extreme Accumulation"),
|
||||
]
|
||||
|
||||
# Scoring thresholds — replicated from scoring/engine.py for standalone use
|
||||
# Scoring thresholds — load from config/thresholds.json (single source of truth)
|
||||
import os as _os
|
||||
import json as _json
|
||||
|
||||
_THRESH_PATH = _os.path.join(_os.path.dirname(_os.path.dirname(_os.path.abspath(__file__))), "config", "thresholds.json")
|
||||
try:
|
||||
with open(_THRESH_PATH) as _f:
|
||||
_THRESH = _json.load(_f)
|
||||
except Exception:
|
||||
_THRESH = {}
|
||||
|
||||
METRIC_SCORERS = {
|
||||
"fear_greed": {
|
||||
"ranges": [[None, 10, 10], [10, 25, 7], [25, 45, 4], [45, 55, 2], [55, 75, 1], [75, None, 0]],
|
||||
},
|
||||
"puell_multiple": {
|
||||
"ranges": [[None, 0.3, 10], [0.3, 0.5, 8], [0.5, 0.8, 5], [0.8, 1.2, 3], [1.2, 2.0, 1], [2.0, None, 0]],
|
||||
},
|
||||
"mvrv_zscore": {
|
||||
"ranges": [[None, 0, 10], [0, 0.5, 8], [0.5, 1.5, 5], [1.5, 3, 2], [3, 5, 1], [5, None, 0]],
|
||||
},
|
||||
"reserve_risk": {
|
||||
"ranges": [[None, 0.002, 10], [0.002, 0.005, 7], [0.005, 0.01, 4], [0.01, 0.02, 2], [0.02, None, 0]],
|
||||
},
|
||||
"rhodl_ratio": {
|
||||
"ranges": [[None, 100, 10], [100, 500, 7], [500, 2000, 4], [2000, 10000, 1], [10000, None, 0]],
|
||||
},
|
||||
"nupl": {
|
||||
"ranges": [[None, 0, 10], [0, 0.25, 7], [0.25, 0.5, 4], [0.5, 0.75, 1], [0.75, None, 0]],
|
||||
},
|
||||
"fear_greed": {"ranges": _THRESH.get("fear_greed", {}).get("ranges", [[0, 15, 10], [15, 30, 8], [30, 45, 5], [45, 55, 3], [55, 75, 1], [75, None, 0]])},
|
||||
"puell_multiple": {"ranges": _THRESH.get("puell_multiple", {}).get("ranges", [[None, 0.4, 10], [0.4, 0.7, 8], [0.7, 1.0, 5], [1.0, 1.5, 3], [1.5, 2.0, 1], [2.0, None, 0]])},
|
||||
"mvrv_zscore": {"ranges": _THRESH.get("mvrv_zscore", {}).get("ranges", [[None, 0, 10], [0, 1.0, 8], [1.0, 2.0, 5], [2.0, 3.0, 3], [3.0, 5.0, 1], [5.0, None, 0]])},
|
||||
"reserve_risk": {"ranges": _THRESH.get("reserve_risk", {}).get("ranges", [[None, 0.002, 10], [0.002, 0.005, 7], [0.005, 0.01, 4], [0.01, 0.02, 2], [0.02, None, 0]])},
|
||||
"rhodl_ratio": {"ranges": _THRESH.get("rhodl_ratio", {}).get("ranges", [[None, 200, 10], [200, 1000, 7], [1000, 5000, 4], [5000, 20000, 1], [20000, None, 0]])},
|
||||
"nupl": {"ranges": _THRESH.get("nupl", {}).get("ranges", [[None, 0, 10], [0, 0.3, 8], [0.3, 0.5, 4], [0.5, 0.75, 1], [0.75, None, 0]])},
|
||||
}
|
||||
|
||||
# Ratio-based metrics: score based on price vs reference value
|
||||
RATIO_SCORERS = {
|
||||
"price_vs_200w_sma": {
|
||||
# pct_above ranges
|
||||
"ranges": [[None, 0, 10], [0, 20, 6], [20, 50, 3], [50, 100, 1], [100, None, 0]],
|
||||
"ranges": _THRESH.get("price_vs_200w_sma", {}).get("ranges", [[None, 0, 10], [0, 30, 7], [30, 60, 5], [60, 100, 2], [100, None, 0]]),
|
||||
"price_key": "btc_price",
|
||||
"ref_key": "200w_sma",
|
||||
},
|
||||
"lth_realized_price": {
|
||||
"ranges": [[None, 0, 10], [0, 20, 6], [20, 50, 3], [50, None, 1]],
|
||||
"ranges": _THRESH.get("lth_realized_price", {}).get("ranges", [[None, 0, 10], [0, 30, 7], [30, 80, 5], [80, 150, 3], [150, None, 1]]),
|
||||
"price_key": "btc_price",
|
||||
"ref_key": "lth_realized_price",
|
||||
},
|
||||
}
|
||||
|
||||
# Drawdown scoring
|
||||
DRAWDOWN_RANGES = [[70, None, 10], [50, 70, 8], [30, 50, 6], [20, 30, 4], [10, 20, 2], [None, 10, 0]]
|
||||
DRAWDOWN_RANGES = _THRESH.get("drawdown", {}).get("ranges", [[60, None, 10], [40, 60, 8], [25, 40, 6], [15, 25, 4], [5, 15, 2], [None, 5, 0]])
|
||||
|
||||
|
||||
def _score_range(value, ranges):
|
||||
@@ -126,8 +121,36 @@ def _compute_ath_series(price_lookup, dates):
|
||||
return drawdowns
|
||||
|
||||
|
||||
def score_day(date, index, drawdowns):
|
||||
"""Score a single day using all available metrics. Returns (composite_score, individual_scores, n_metrics)."""
|
||||
def _load_ml_weights():
|
||||
"""Load ML weights for ML-optimized scoring mode."""
|
||||
ml_path = _os.path.join(_os.path.dirname(_os.path.dirname(_os.path.abspath(__file__))), "config", "ml_weights.json")
|
||||
try:
|
||||
with open(ml_path) as f:
|
||||
data = _json.load(f)
|
||||
return data.get("weights", {})
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
# ML weight key mapping (backtest metric keys -> ML weight keys)
|
||||
_BT_ML_KEY_MAP = {
|
||||
"fear_greed": "fear_greed",
|
||||
"puell_multiple": "puell_multiple",
|
||||
"mvrv_zscore": "mvrv_zscore",
|
||||
"reserve_risk": "reserve_risk",
|
||||
"rhodl_ratio": "rhodl_ratio",
|
||||
"nupl": "nupl",
|
||||
"price_vs_200w_sma": "pct_above_200w_sma",
|
||||
"lth_realized_price": "pct_above_lth_rp",
|
||||
"drawdown": "drawdown",
|
||||
}
|
||||
|
||||
|
||||
def score_day(date, index, drawdowns, ml_weights=None):
|
||||
"""Score a single day using all available metrics. Returns (composite_score, details, n_metrics).
|
||||
|
||||
If ml_weights is provided, uses ML-optimized weighting instead of equal weights.
|
||||
details includes both "score" and "raw" (the actual metric value before scoring).
|
||||
"""
|
||||
scores = []
|
||||
details = {}
|
||||
|
||||
@@ -138,7 +161,7 @@ def score_day(date, index, drawdowns):
|
||||
s = _score_range(val, cfg["ranges"])
|
||||
if s is not None:
|
||||
scores.append(s)
|
||||
details[metric_key] = {"value": val, "score": s}
|
||||
details[metric_key] = {"value": val, "score": s, "raw": val}
|
||||
|
||||
# Ratio-based metrics (price vs reference)
|
||||
for metric_key, cfg in RATIO_SCORERS.items():
|
||||
@@ -155,7 +178,7 @@ def score_day(date, index, drawdowns):
|
||||
s = _score_range(pct_above, cfg["ranges"])
|
||||
if s is not None:
|
||||
scores.append(s)
|
||||
details[metric_key] = {"value": pct_above, "score": s}
|
||||
details[metric_key] = {"value": pct_above, "score": s, "raw": pct_above}
|
||||
|
||||
# Drawdown
|
||||
dd = drawdowns.get(date)
|
||||
@@ -163,11 +186,25 @@ def score_day(date, index, drawdowns):
|
||||
s = _score_range(dd, DRAWDOWN_RANGES)
|
||||
if s is not None:
|
||||
scores.append(s)
|
||||
details["drawdown"] = {"value": dd, "score": s}
|
||||
details["drawdown"] = {"value": dd, "score": s, "raw": dd}
|
||||
|
||||
if not scores:
|
||||
return None, details, 0
|
||||
|
||||
if ml_weights:
|
||||
# ML-weighted composite
|
||||
weighted_sum = 0.0
|
||||
weight_total = 0.0
|
||||
for metric_key, info in details.items():
|
||||
ml_key = _BT_ML_KEY_MAP.get(metric_key, metric_key)
|
||||
w = ml_weights.get(ml_key, 0.0)
|
||||
weighted_sum += info["score"] * w
|
||||
weight_total += w
|
||||
if weight_total > 0:
|
||||
composite = weighted_sum / weight_total * 10
|
||||
else:
|
||||
composite = sum(scores) / len(scores) * 10
|
||||
else:
|
||||
composite = sum(scores) / len(scores) * 10
|
||||
return round(composite, 1), details, len(scores)
|
||||
|
||||
@@ -213,9 +250,12 @@ def compute_max_drawdown_forward(price_lookup, date, window=90):
|
||||
return round(max_dd, 2) if max_dd > 0 else 0
|
||||
|
||||
|
||||
def run_backtest():
|
||||
"""Run the full backtest and return comprehensive results."""
|
||||
log.info("Loading historical data...")
|
||||
def run_backtest(ml_mode=False):
|
||||
"""Run the full backtest and return comprehensive results.
|
||||
|
||||
If ml_mode=True, uses ML-optimized metric weights instead of equal weights.
|
||||
"""
|
||||
log.info("Loading historical data... (ml_mode=%s)", ml_mode)
|
||||
if not os.path.exists(HISTORY_PATH):
|
||||
return {"error": "No historical data found. Run history collector first."}
|
||||
|
||||
@@ -245,19 +285,32 @@ def run_backtest():
|
||||
log.info("Computing forward returns...")
|
||||
fwd_returns = compute_forward_returns(price_lookup, all_dates)
|
||||
|
||||
# Load ML weights if in ML mode
|
||||
ml_weights = _load_ml_weights() if ml_mode else None
|
||||
if ml_mode and not ml_weights:
|
||||
log.warning("ML mode requested but no weights found — falling back to equal weights")
|
||||
ml_weights = None
|
||||
|
||||
# Score each day
|
||||
log.info("Scoring %d days...", len(all_dates))
|
||||
daily_scores = []
|
||||
for d in all_dates:
|
||||
composite, details, n_metrics = score_day(d, index, drawdowns)
|
||||
composite, details, n_metrics = score_day(d, index, drawdowns, ml_weights=ml_weights)
|
||||
if composite is not None and n_metrics >= 3: # Require at least 3 metrics
|
||||
price = price_lookup.get(d)
|
||||
# Collect raw metric values for per-metric historical exploration
|
||||
metric_values = {}
|
||||
for mk, info in details.items():
|
||||
raw = info.get("raw")
|
||||
if raw is not None:
|
||||
metric_values[mk] = round(raw, 6) if isinstance(raw, float) else raw
|
||||
entry = {
|
||||
"date": d,
|
||||
"score": composite,
|
||||
"n_metrics": n_metrics,
|
||||
"price": price,
|
||||
"forward_returns": fwd_returns.get(d, {}),
|
||||
"metric_values": metric_values,
|
||||
}
|
||||
daily_scores.append(entry)
|
||||
|
||||
@@ -361,21 +414,47 @@ def run_backtest():
|
||||
if abs(d["score"] - current_score) <= margin and d["forward_returns"]:
|
||||
comparable.append(d)
|
||||
|
||||
avg_1yr = None
|
||||
avg_returns = {}
|
||||
if comparable:
|
||||
yr_returns = [d["forward_returns"]["365d"] for d in comparable if "365d" in d["forward_returns"]]
|
||||
if yr_returns:
|
||||
avg_1yr = round(sum(yr_returns) / len(yr_returns), 2)
|
||||
for period in ["30d", "90d", "180d", "365d"]:
|
||||
vals = [d["forward_returns"][period] for d in comparable if period in d["forward_returns"]]
|
||||
if vals:
|
||||
avg_returns[period] = round(sum(vals) / len(vals), 2)
|
||||
avg_1yr = avg_returns.get("365d")
|
||||
|
||||
# Best comparable examples (most recent 5)
|
||||
# Best comparable examples — one per market cycle for diversity
|
||||
# Cycles: pre-2016, 2016-2017 bull, 2018-2019 bear, 2020-2021 bull, 2022-2023 bear, 2024+
|
||||
cycle_bins = [
|
||||
("pre-2016", "2010-01-01", "2015-12-31"),
|
||||
("2016-17 Bull", "2016-01-01", "2017-12-31"),
|
||||
("2018-19 Bear", "2018-01-01", "2019-12-31"),
|
||||
("2020-21 Bull", "2020-01-01", "2021-12-31"),
|
||||
("2022-23 Bear", "2022-01-01", "2023-12-31"),
|
||||
("2024+", "2024-01-01", "2099-12-31"),
|
||||
]
|
||||
examples = []
|
||||
for d in comparable[-5:]:
|
||||
used_cycles = set()
|
||||
# Sort comparable by closest score first, then pick one per cycle
|
||||
sorted_comp = sorted(comparable, key=lambda d: abs(d["score"] - current_score))
|
||||
for d in sorted_comp:
|
||||
cycle_label = None
|
||||
for label, start, end in cycle_bins:
|
||||
if start <= d["date"] <= end:
|
||||
cycle_label = label
|
||||
break
|
||||
if cycle_label and cycle_label not in used_cycles:
|
||||
used_cycles.add(cycle_label)
|
||||
examples.append({
|
||||
"date": d["date"],
|
||||
"score": d["score"],
|
||||
"price": d["price"],
|
||||
"forward_returns": d["forward_returns"],
|
||||
"cycle": cycle_label,
|
||||
})
|
||||
if len(examples) >= 6:
|
||||
break
|
||||
# Sort examples chronologically
|
||||
examples.sort(key=lambda d: d["date"])
|
||||
|
||||
current_context = {
|
||||
"current_score": current_score,
|
||||
@@ -383,20 +462,41 @@ def run_backtest():
|
||||
"percentile": percentile,
|
||||
"comparable_days": len(comparable),
|
||||
"avg_1yr_return": avg_1yr,
|
||||
"avg_30d_return": avg_returns.get("30d"),
|
||||
"avg_90d_return": avg_returns.get("90d"),
|
||||
"avg_180d_return": avg_returns.get("180d"),
|
||||
"examples": examples,
|
||||
}
|
||||
|
||||
# --- Build time series for charting ---
|
||||
# Downsample to weekly for chart efficiency
|
||||
# Smart downsampling: daily for last 2 years, weekly before that
|
||||
# Include per-metric values so the frontend can plot any metric.
|
||||
chart_data = []
|
||||
import datetime as _dt
|
||||
try:
|
||||
last_date = _dt.datetime.strptime(daily_scores[-1]["date"], "%Y-%m-%d")
|
||||
cutoff_date = (last_date - _dt.timedelta(days=730)).strftime("%Y-%m-%d")
|
||||
except Exception:
|
||||
cutoff_date = "2024-01-01"
|
||||
|
||||
# Collect all metric keys that were ever scored (for per-metric series)
|
||||
all_metric_keys = set()
|
||||
for d in daily_scores:
|
||||
all_metric_keys.update(d.get("metric_values", {}).keys())
|
||||
|
||||
for i, d in enumerate(daily_scores):
|
||||
# Include every 7th day + last day
|
||||
if i % 7 == 0 or i == len(daily_scores) - 1:
|
||||
chart_data.append({
|
||||
is_recent = d["date"] >= cutoff_date
|
||||
if is_recent or i % 7 == 0 or i == len(daily_scores) - 1:
|
||||
entry = {
|
||||
"date": d["date"],
|
||||
"score": d["score"],
|
||||
"price": d["price"],
|
||||
})
|
||||
}
|
||||
# Include per-metric values (raw metric value, not score)
|
||||
metric_vals = d.get("metric_values", {})
|
||||
if metric_vals:
|
||||
entry["metrics"] = metric_vals
|
||||
chart_data.append(entry)
|
||||
|
||||
result = {
|
||||
"date_range": {"start": daily_scores[0]["date"], "end": daily_scores[-1]["date"]},
|
||||
@@ -405,6 +505,7 @@ def run_backtest():
|
||||
"signal_events": signal_events,
|
||||
"current_context": current_context,
|
||||
"chart_data": chart_data,
|
||||
"ml_mode": ml_mode,
|
||||
"computed_at": datetime.utcnow().isoformat() + "Z",
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"provider": "ollama",
|
||||
"model": "qwen3.5:27b",
|
||||
"model": "gemma4:12b-mlx",
|
||||
"providers": {
|
||||
"ollama": {
|
||||
"base_url": "http://100.100.242.21:11434"
|
||||
"base_url": "http://100.79.255.5:11434"
|
||||
},
|
||||
"lmstudio": {
|
||||
"base_url": "http://100.100.242.21:1234"
|
||||
@@ -15,7 +15,7 @@
|
||||
"api_key": ""
|
||||
},
|
||||
"openrouter": {
|
||||
"api_key": ""
|
||||
"api_key": "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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
{
|
||||
"weights": {
|
||||
"pct_above_200w_sma": 0.5075,
|
||||
"drawdown": 0.1459,
|
||||
"pct_above_lth_rp": 0.1095,
|
||||
"rhodl_ratio": 0.089,
|
||||
"fear_greed": 0.0515,
|
||||
"reserve_risk": 0.046,
|
||||
"puell_multiple": 0.0255,
|
||||
"mvrv_zscore": 0.0182,
|
||||
"nupl": 0.0068
|
||||
},
|
||||
"feature_importances": {
|
||||
"raw_pct_above_200w_sma": 0.436377,
|
||||
"days_since_ath": 0.119405,
|
||||
"raw_pct_above_lth_rp": 0.109451,
|
||||
"raw_rhodl_ratio": 0.088999,
|
||||
"score_pct_above_200w_sma": 0.071148,
|
||||
"raw_fear_greed": 0.051475,
|
||||
"puell_x_reserve": 0.032886,
|
||||
"raw_drawdown": 0.026474,
|
||||
"raw_reserve_risk": 0.021707,
|
||||
"raw_mvrv_zscore": 0.012429,
|
||||
"raw_puell_multiple": 0.008599,
|
||||
"delta_30d_reserve_risk": 0.007865,
|
||||
"delta_30d_mvrv_zscore": 0.004263,
|
||||
"raw_nupl": 0.003271,
|
||||
"mvrv_x_nupl": 0.002979,
|
||||
"delta_30d_nupl": 0.002056,
|
||||
"delta_30d_puell_multiple": 0.000473,
|
||||
"score_fear_greed": 6.8e-05,
|
||||
"score_mvrv_zscore": 5.4e-05,
|
||||
"score_puell_multiple": 1e-05,
|
||||
"score_pct_above_lth_rp": 6e-06,
|
||||
"score_rhodl_ratio": 2e-06,
|
||||
"score_reserve_risk": 0.0,
|
||||
"score_nupl": 0.0,
|
||||
"score_drawdown": 0.0
|
||||
},
|
||||
"cv_results": {
|
||||
"mean_auc": 0.6164,
|
||||
"std_auc": 0.3317,
|
||||
"mean_f1": 0.6736,
|
||||
"mean_precision": 0.8015,
|
||||
"mean_recall": 0.7047
|
||||
},
|
||||
"training_info": {
|
||||
"n_samples": 2601,
|
||||
"n_positive": 1553,
|
||||
"positive_rate": 0.5971,
|
||||
"n_features": 25,
|
||||
"target_threshold": 30.0,
|
||||
"date_range": "2018-02-01 to 2025-03-21",
|
||||
"model": "GradientBoostingClassifier"
|
||||
},
|
||||
"comparison": {
|
||||
"equal_weight": [
|
||||
{
|
||||
"range": "0-20",
|
||||
"label": "Extreme Caution",
|
||||
"days": 295,
|
||||
"avg_365d": -5.94,
|
||||
"median_365d": -11.99,
|
||||
"win_rate_365d": 35.6
|
||||
},
|
||||
{
|
||||
"range": "21-40",
|
||||
"label": "Caution",
|
||||
"days": 587,
|
||||
"avg_365d": 23.84,
|
||||
"median_365d": -7.2,
|
||||
"win_rate_365d": 45.3
|
||||
},
|
||||
{
|
||||
"range": "41-55",
|
||||
"label": "Neutral",
|
||||
"days": 697,
|
||||
"avg_365d": 108.96,
|
||||
"median_365d": 75.92,
|
||||
"win_rate_365d": 70.4
|
||||
},
|
||||
{
|
||||
"range": "56-70",
|
||||
"label": "Moderate Opportunity",
|
||||
"days": 450,
|
||||
"avg_365d": 128.81,
|
||||
"median_365d": 109.03,
|
||||
"win_rate_365d": 96.4
|
||||
},
|
||||
{
|
||||
"range": "71-85",
|
||||
"label": "Strong Accumulation",
|
||||
"days": 275,
|
||||
"avg_365d": 175.76,
|
||||
"median_365d": 117.95,
|
||||
"win_rate_365d": 86.9
|
||||
},
|
||||
{
|
||||
"range": "86-100",
|
||||
"label": "Extreme Accumulation",
|
||||
"days": 247,
|
||||
"avg_365d": 115.5,
|
||||
"median_365d": 90.08,
|
||||
"win_rate_365d": 100.0
|
||||
}
|
||||
],
|
||||
"ml_weighted": [
|
||||
{
|
||||
"range": "0-20",
|
||||
"label": "Extreme Caution",
|
||||
"days": 577,
|
||||
"avg_365d": -6.17,
|
||||
"median_365d": -26.21,
|
||||
"win_rate_365d": 27.0
|
||||
},
|
||||
{
|
||||
"range": "21-40",
|
||||
"label": "Caution",
|
||||
"days": 855,
|
||||
"avg_365d": 77.5,
|
||||
"median_365d": 39.28,
|
||||
"win_rate_365d": 72.7
|
||||
},
|
||||
{
|
||||
"range": "41-55",
|
||||
"label": "Neutral",
|
||||
"days": 241,
|
||||
"avg_365d": 165.77,
|
||||
"median_365d": 124.05,
|
||||
"win_rate_365d": 92.5
|
||||
},
|
||||
{
|
||||
"range": "56-70",
|
||||
"label": "Moderate Opportunity",
|
||||
"days": 328,
|
||||
"avg_365d": 144.47,
|
||||
"median_365d": 124.27,
|
||||
"win_rate_365d": 89.6
|
||||
},
|
||||
{
|
||||
"range": "71-85",
|
||||
"label": "Strong Accumulation",
|
||||
"days": 201,
|
||||
"avg_365d": 210.2,
|
||||
"median_365d": 122.22,
|
||||
"win_rate_365d": 99.0
|
||||
},
|
||||
{
|
||||
"range": "86-100",
|
||||
"label": "Extreme Accumulation",
|
||||
"days": 287,
|
||||
"avg_365d": 113.92,
|
||||
"median_365d": 99.53,
|
||||
"win_rate_365d": 100.0
|
||||
}
|
||||
]
|
||||
},
|
||||
"trained_at": "2026-03-21T23:15:38.277703+00:00"
|
||||
}
|
||||
+16
-8
@@ -1,30 +1,38 @@
|
||||
{
|
||||
"_comment": "Cycle-aware thresholds — widened ranges to account for BTC maturing and diminishing cycle extremes",
|
||||
"fear_greed": {
|
||||
"ranges": [[0, 10, 10], [11, 25, 7], [26, 45, 4], [46, 55, 2], [56, 75, 1], [76, 100, 0]]
|
||||
"ranges": [[0, 15, 10], [15, 30, 8], [30, 45, 5], [45, 55, 3], [55, 75, 1], [75, 100, 0]]
|
||||
},
|
||||
"puell_multiple": {
|
||||
"ranges": [[null, 0.3, 10], [0.3, 0.5, 8], [0.5, 0.8, 5], [0.8, 1.2, 3], [1.2, 2.0, 1], [2.0, null, 0]]
|
||||
"_note": "Post-halving floors rising: 2016=0.15, 2020=0.3, 2024=0.5+",
|
||||
"ranges": [[null, 0.4, 10], [0.4, 0.7, 8], [0.7, 1.0, 5], [1.0, 1.5, 3], [1.5, 2.0, 1], [2.0, null, 0]]
|
||||
},
|
||||
"mvrv_zscore": {
|
||||
"ranges": [[null, 0, 10], [0, 0.5, 8], [0.5, 1.5, 5], [1.5, 3, 2], [3, 5, 1], [5, null, 0]]
|
||||
"_note": "Bottoms getting shallower: 2015=-0.6, 2018=-0.4, 2022=-0.3, next may be ~0",
|
||||
"ranges": [[null, 0, 10], [0, 1.0, 8], [1.0, 2.0, 5], [2.0, 3.0, 3], [3.0, 5.0, 1], [5.0, null, 0]]
|
||||
},
|
||||
"drawdown": {
|
||||
"ranges": [[70, null, 10], [50, 70, 8], [30, 50, 6], [20, 30, 4], [10, 20, 2], [null, 10, 0]]
|
||||
"_note": "Drawdowns compressing: 2014=86%, 2018=84%, 2022=77%, future may max at 50-60%",
|
||||
"ranges": [[60, null, 10], [40, 60, 8], [25, 40, 6], [15, 25, 4], [5, 15, 2], [null, 5, 0]]
|
||||
},
|
||||
"price_vs_200w_sma": {
|
||||
"ranges": [[null, 0, 10], [0, 20, 6], [20, 50, 3], [50, 100, 1], [100, null, 0]]
|
||||
"_note": "BTC spends more time above 200W SMA as it matures",
|
||||
"ranges": [[null, 0, 10], [0, 30, 7], [30, 60, 5], [60, 100, 2], [100, null, 0]]
|
||||
},
|
||||
"reserve_risk": {
|
||||
"ranges": [[null, 0.002, 10], [0.002, 0.005, 7], [0.005, 0.01, 4], [0.01, 0.02, 2], [0.02, null, 0]]
|
||||
},
|
||||
"rhodl_ratio": {
|
||||
"ranges": [[null, 100, 10], [100, 500, 7], [500, 2000, 4], [2000, 10000, 1], [10000, null, 0]]
|
||||
"_note": "RHODL baseline rising with institutional adoption",
|
||||
"ranges": [[null, 200, 10], [200, 1000, 7], [1000, 5000, 4], [5000, 20000, 1], [20000, null, 0]]
|
||||
},
|
||||
"nupl": {
|
||||
"ranges": [[null, 0, 10], [0, 0.25, 7], [0.25, 0.5, 4], [0.5, 0.75, 1], [0.75, null, 0]]
|
||||
"_note": "NUPL bottoms getting shallower as BTC matures",
|
||||
"ranges": [[null, 0, 10], [0, 0.3, 8], [0.3, 0.5, 4], [0.5, 0.75, 1], [0.75, null, 0]]
|
||||
},
|
||||
"lth_realized_price": {
|
||||
"ranges": [[null, 0, 10], [0, 20, 6], [20, 50, 3], [50, null, 1]]
|
||||
"_note": "Price stays further above LTH RP as BTC matures — 60% above is still a good entry in 2024+",
|
||||
"ranges": [[null, 0, 10], [0, 30, 7], [30, 80, 5], [80, 150, 3], [150, null, 1]]
|
||||
},
|
||||
"hash_ribbons": {
|
||||
"buy_signal": 10,
|
||||
|
||||
+906
-91
File diff suppressed because it is too large
Load Diff
@@ -2,3 +2,148 @@
|
||||
{"timestamp": "2026-03-20T22:30:13.547149+00:00", "composite_score": 51.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 11}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.910215736040605}, "price_vs_200w_sma": {"score": 3, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-20T22:46:34.952569+00:00", "composite_score": 51.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 11}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.931630710659896}, "price_vs_200w_sma": {"score": 3, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-20T22:51:27.724327+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 11}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.94907994923858}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-20T23:07:48.303808+00:00", "composite_score": 51.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 11}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.942734771573605}, "price_vs_200w_sma": {"score": 3, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-20T23:21:39.705718+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 11}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.07439720812183}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-20T23:27:15.835859+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 11}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.07122461928934}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-20T23:29:40.370530+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 11}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.099777918781726}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-20T23:32:26.885241+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 11}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.099777918781726}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-20T23:47:27.138815+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 11}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.07122461928934}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T00:02:27.412395+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.06408629441624}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T00:17:27.737482+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.025222081218274}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T00:32:28.011885+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.98953045685279}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T00:47:28.265430+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.006186548223354}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T01:02:28.558846+00:00", "composite_score": 51.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.930837563451774}, "price_vs_200w_sma": {"score": 3, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T01:17:28.812131+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.964149746192895}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T01:32:29.208821+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.029980964467}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T01:47:29.455146+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.07994923857868}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T02:02:29.737360+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.10057106598985}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T02:17:30.019509+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.07201776649746}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T02:32:30.343202+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.98477157360406}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T02:47:30.599161+00:00", "composite_score": 51.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.93797588832487}, "price_vs_200w_sma": {"score": 3, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T03:02:30.861751+00:00", "composite_score": 51.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.90704314720812}, "price_vs_200w_sma": {"score": 3, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T03:17:31.107047+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.94670050761421}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T03:32:31.337649+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.94670050761421}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T03:47:31.597224+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.98477157360406}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T04:02:31.879811+00:00", "composite_score": 51.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.93004441624365}, "price_vs_200w_sma": {"score": 3, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T04:17:32.138046+00:00", "composite_score": 51.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.89435279187817}, "price_vs_200w_sma": {"score": 3, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T04:32:33.906364+00:00", "composite_score": 51.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.900697969543145}, "price_vs_200w_sma": {"score": 3, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T04:47:34.155485+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.97921954314721}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T05:02:34.407649+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.975253807106604}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T05:17:34.633258+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.97049492385786}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T05:32:35.086231+00:00", "composite_score": 51.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.878489847715734}, "price_vs_200w_sma": {"score": 3, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T05:47:35.345297+00:00", "composite_score": 51.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.88880076142132}, "price_vs_200w_sma": {"score": 3, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T06:02:35.604338+00:00", "composite_score": 51.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.92607868020305}, "price_vs_200w_sma": {"score": 3, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T06:17:35.879813+00:00", "composite_score": 51.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.9173540609137}, "price_vs_200w_sma": {"score": 3, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T06:32:36.158808+00:00", "composite_score": 51.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.85231598984772}, "price_vs_200w_sma": {"score": 3, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T06:47:36.389767+00:00", "composite_score": 51.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.8880076142132}, "price_vs_200w_sma": {"score": 3, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T07:02:36.614994+00:00", "composite_score": 51.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.880869289340104}, "price_vs_200w_sma": {"score": 3, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T07:17:36.932975+00:00", "composite_score": 51.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.89435279187817}, "price_vs_200w_sma": {"score": 3, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T07:32:37.232230+00:00", "composite_score": 51.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.8118654822335}, "price_vs_200w_sma": {"score": 3, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T07:47:37.478904+00:00", "composite_score": 51.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.88959390862944}, "price_vs_200w_sma": {"score": 3, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T08:02:37.705904+00:00", "composite_score": 51.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.93242385786802}, "price_vs_200w_sma": {"score": 3, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T08:17:37.925968+00:00", "composite_score": 51.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.93797588832487}, "price_vs_200w_sma": {"score": 3, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T08:32:38.186918+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.078362944162436}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T08:47:38.410773+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.00697969543147}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T09:02:38.629878+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.00301395939086}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T09:17:38.850286+00:00", "composite_score": 51.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.933217005076145}, "price_vs_200w_sma": {"score": 3, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T09:32:39.127885+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.02046319796955}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T09:47:39.355904+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.983978426395936}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T10:02:39.589009+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.973667512690355}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T10:17:39.865976+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.986357868020306}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T10:32:40.432700+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.0188769035533}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T10:47:40.718041+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.02918781725889}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T11:02:40.931588+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.01253172588832}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T11:17:41.151257+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.037119289340104}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T11:32:41.449918+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.0117385786802}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T11:47:41.719647+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.99666878172589}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T12:02:41.950098+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.95542512690355}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T12:17:42.199534+00:00", "composite_score": 51.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.92528553299492}, "price_vs_200w_sma": {"score": 3, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T12:32:42.694007+00:00", "composite_score": 51.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.88721446700507}, "price_vs_200w_sma": {"score": 3, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T12:47:42.921963+00:00", "composite_score": 51.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.89197335025381}, "price_vs_200w_sma": {"score": 3, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T13:02:43.160452+00:00", "composite_score": 51.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.91656091370558}, "price_vs_200w_sma": {"score": 3, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T13:17:43.406206+00:00", "composite_score": 51.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.89355964467005}, "price_vs_200w_sma": {"score": 3, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T13:32:43.786751+00:00", "composite_score": 51.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.881662436548226}, "price_vs_200w_sma": {"score": 3, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T13:47:44.052238+00:00", "composite_score": 51.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.92052664974619}, "price_vs_200w_sma": {"score": 3, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T14:02:44.304809+00:00", "composite_score": 51.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.879282994923855}, "price_vs_200w_sma": {"score": 3, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T14:17:44.551776+00:00", "composite_score": 51.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.67465101522843}, "price_vs_200w_sma": {"score": 3, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T14:32:44.837983+00:00", "composite_score": 51.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.73968908629441}, "price_vs_200w_sma": {"score": 3, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T14:47:45.081254+00:00", "composite_score": 51.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.74841370558376}, "price_vs_200w_sma": {"score": 3, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T15:02:45.357457+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.99904822335025}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T15:17:45.594051+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.01332487309645}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T15:32:45.890870+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.964942893401016}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T15:47:46.142770+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 43.982392131979694}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T16:02:46.392729+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.00539340101523}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T16:17:46.656840+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.01570431472081}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T16:32:47.020533+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.13467639593909}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T16:47:47.262309+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.18067893401015}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T17:02:47.523039+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.25047588832488}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T17:17:47.803679+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.27982233502538}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T17:32:48.073161+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.192576142131976}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T17:47:48.330230+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.170368020304565}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T18:02:48.615315+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.23857868020305}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T18:17:48.885342+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.202887055837564}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T18:32:49.283593+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.16560913705584}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T18:47:49.583109+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.121192893401016}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T19:02:49.846889+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.16560913705584}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T19:17:50.119141+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.172747461928935}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T19:32:50.417600+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.15609137055838}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T19:47:50.704459+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.21795685279188}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T20:02:50.952361+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.17909263959391}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T20:17:51.183860+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.17909263959391}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T20:32:51.484992+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.121192893401016}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T20:47:51.763248+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.111675126903556}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T21:02:52.006943+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.138642131979694}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T21:17:52.292371+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.17433375634518}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T21:32:52.634501+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.21002538071066}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T21:47:52.933666+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.21637055837564}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T22:02:53.198903+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.234612944162436}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T22:04:28.799920+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.251269035533}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T22:17:53.423421+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.23857868020305}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T22:21:10.295734+00:00", "composite_score": 54.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 7, "value": 12}, "puell_multiple": {"score": 5, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 5, "value": 0.5211180167687892}, "drawdown": {"score": 6, "value": 44.23064720812182}, "price_vs_200w_sma": {"score": 6, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 7, "value": 0.22243290955405431}, "lth_realized_price": {"score": 1, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T22:35:13.975107+00:00", "composite_score": 71.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 10, "value": 12}, "puell_multiple": {"score": 8, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 8, "value": 0.5211180167687892}, "drawdown": {"score": 8, "value": 44.254441624365484}, "price_vs_200w_sma": {"score": 7, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 8, "value": 0.22243290955405431}, "lth_realized_price": {"score": 5, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T22:40:38.516771+00:00", "composite_score": 71.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 10, "value": 12}, "puell_multiple": {"score": 8, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 8, "value": 0.5211180167687892}, "drawdown": {"score": 8, "value": 44.20685279187817}, "price_vs_200w_sma": {"score": 7, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 8, "value": 0.22243290955405431}, "lth_realized_price": {"score": 5, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T22:41:18.262393+00:00", "composite_score": 71.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 10, "value": 12}, "puell_multiple": {"score": 8, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 8, "value": 0.5211180167687892}, "drawdown": {"score": 8, "value": 44.20685279187817}, "price_vs_200w_sma": {"score": 7, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 8, "value": 0.22243290955405431}, "lth_realized_price": {"score": 5, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T22:41:46.036660+00:00", "composite_score": 71.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 10, "value": 12}, "puell_multiple": {"score": 8, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 8, "value": 0.5211180167687892}, "drawdown": {"score": 8, "value": 44.21875}, "price_vs_200w_sma": {"score": 7, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 8, "value": 0.22243290955405431}, "lth_realized_price": {"score": 5, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T22:42:33.632103+00:00", "composite_score": 70.0, "scored_count": 9, "metrics": {"fear_greed": {"score": 10, "value": 12}, "puell_multiple": {"score": 8, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 8, "value": 0.5211180167687892}, "drawdown": {"score": null, "value": null}, "price_vs_200w_sma": {"score": 7, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 8, "value": 0.22243290955405431}, "lth_realized_price": {"score": 5, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T22:51:08.461576+00:00", "composite_score": 71.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 10, "value": 12}, "puell_multiple": {"score": 8, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 8, "value": 0.5211180167687892}, "drawdown": {"score": 8, "value": 44.24968274111675}, "price_vs_200w_sma": {"score": 7, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 8, "value": 0.22243290955405431}, "lth_realized_price": {"score": 5, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T22:53:45.530567+00:00", "composite_score": 71.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 10, "value": 12}, "puell_multiple": {"score": 8, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 8, "value": 0.5211180167687892}, "drawdown": {"score": 8, "value": 44.26713197969543}, "price_vs_200w_sma": {"score": 7, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 8, "value": 0.22243290955405431}, "lth_realized_price": {"score": 5, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T22:54:22.144542+00:00", "composite_score": 70.0, "scored_count": 9, "metrics": {"fear_greed": {"score": 10, "value": 12}, "puell_multiple": {"score": 8, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 8, "value": 0.5211180167687892}, "drawdown": {"score": null, "value": null}, "price_vs_200w_sma": {"score": 7, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 8, "value": 0.22243290955405431}, "lth_realized_price": {"score": 5, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T22:55:08.385540+00:00", "composite_score": 71.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 10, "value": 12}, "puell_multiple": {"score": 8, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 8, "value": 0.5211180167687892}, "drawdown": {"score": 8, "value": 44.26554568527919}, "price_vs_200w_sma": {"score": 7, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 8, "value": 0.22243290955405431}, "lth_realized_price": {"score": 5, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-03-21T22:55:33.933753+00:00", "composite_score": 71.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 10, "value": 12}, "puell_multiple": {"score": 8, "value": 0.6602699608966011}, "mvrv_zscore": {"score": 8, "value": 0.5211180167687892}, "drawdown": {"score": 8, "value": 44.26316624365482}, "price_vs_200w_sma": {"score": 7, "value": 58895.78086828114}, "reserve_risk": {"score": 10, "value": 0.0012985709697654493}, "rhodl_ratio": {"score": 4, "value": 1230.6243545314708}, "nupl": {"score": 8, "value": 0.22243290955405431}, "lth_realized_price": {"score": 5, "value": 43346.58756410873}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-06-27T18:10:22.517545+00:00", "composite_score": 63.3, "scored_count": 3, "metrics": {"fear_greed": {"score": 8, "value": 15}, "puell_multiple": {"score": null, "value": null}, "mvrv_zscore": {"score": null, "value": null}, "drawdown": {"score": 8, "value": 52.07407994923858}, "price_vs_200w_sma": {"score": null, "value": null}, "reserve_risk": {"score": null, "value": null}, "rhodl_ratio": {"score": null, "value": null}, "nupl": {"score": null, "value": null}, "lth_realized_price": {"score": null, "value": null}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-06-27T18:25:30.189079+00:00", "composite_score": 63.3, "scored_count": 3, "metrics": {"fear_greed": {"score": 8, "value": 15}, "puell_multiple": {"score": null, "value": null}, "mvrv_zscore": {"score": null, "value": null}, "drawdown": {"score": 8, "value": 52.098667512690355}, "price_vs_200w_sma": {"score": null, "value": null}, "reserve_risk": {"score": null, "value": null}, "rhodl_ratio": {"score": null, "value": null}, "nupl": {"score": null, "value": null}, "lth_realized_price": {"score": null, "value": null}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-06-27T18:26:00.556392+00:00", "composite_score": 63.3, "scored_count": 3, "metrics": {"fear_greed": {"score": 8, "value": 15}, "puell_multiple": {"score": null, "value": null}, "mvrv_zscore": {"score": null, "value": null}, "drawdown": {"score": 8, "value": 52.098667512690355}, "price_vs_200w_sma": {"score": null, "value": null}, "reserve_risk": {"score": null, "value": null}, "rhodl_ratio": {"score": null, "value": null}, "nupl": {"score": null, "value": null}, "lth_realized_price": {"score": null, "value": null}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-06-27T18:40:30.805472+00:00", "composite_score": 63.3, "scored_count": 3, "metrics": {"fear_greed": {"score": 8, "value": 15}, "puell_multiple": {"score": null, "value": null}, "mvrv_zscore": {"score": null, "value": null}, "drawdown": {"score": 8, "value": 52.04473350253808}, "price_vs_200w_sma": {"score": null, "value": null}, "reserve_risk": {"score": null, "value": null}, "rhodl_ratio": {"score": null, "value": null}, "nupl": {"score": null, "value": null}, "lth_realized_price": {"score": null, "value": null}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-06-27T18:55:31.544772+00:00", "composite_score": 63.3, "scored_count": 3, "metrics": {"fear_greed": {"score": 8, "value": 15}, "puell_multiple": {"score": null, "value": null}, "mvrv_zscore": {"score": null, "value": null}, "drawdown": {"score": 8, "value": 52.066148477157356}, "price_vs_200w_sma": {"score": null, "value": null}, "reserve_risk": {"score": null, "value": null}, "rhodl_ratio": {"score": null, "value": null}, "nupl": {"score": null, "value": null}, "lth_realized_price": {"score": null, "value": null}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-06-27T18:57:42.639217+00:00", "composite_score": 74.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 8, "value": 15}, "puell_multiple": {"score": 5, "value": 0.7044739567707577}, "mvrv_zscore": {"score": 8, "value": 0.22409759021503936}, "drawdown": {"score": 8, "value": 52.04790609137056}, "price_vs_200w_sma": {"score": 10, "value": 62284.65298428873}, "reserve_risk": {"score": 10, "value": 0.0010258831016337609}, "rhodl_ratio": {"score": 7, "value": 882.5868942025234}, "nupl": {"score": 8, "value": 0.11309045542914359}, "lth_realized_price": {"score": 7, "value": 49767.33015910989}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-06-27T19:12:44.307793+00:00", "composite_score": 74.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 8, "value": 15}, "puell_multiple": {"score": 5, "value": 0.7044739567707577}, "mvrv_zscore": {"score": 8, "value": 0.22409759021503936}, "drawdown": {"score": 8, "value": 52.02014593908629}, "price_vs_200w_sma": {"score": 10, "value": 62284.65298428873}, "reserve_risk": {"score": 10, "value": 0.0010258831016337609}, "rhodl_ratio": {"score": 7, "value": 882.5868942025234}, "nupl": {"score": 8, "value": 0.11309045542914359}, "lth_realized_price": {"score": 7, "value": 49767.33015910989}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-06-27T19:27:45.019177+00:00", "composite_score": 74.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 8, "value": 15}, "puell_multiple": {"score": 5, "value": 0.7044739567707577}, "mvrv_zscore": {"score": 8, "value": 0.22409759021503936}, "drawdown": {"score": 8, "value": 52.22874365482234}, "price_vs_200w_sma": {"score": 10, "value": 62284.65298428873}, "reserve_risk": {"score": 10, "value": 0.0010258831016337609}, "rhodl_ratio": {"score": 7, "value": 882.5868942025234}, "nupl": {"score": 8, "value": 0.11309045542914359}, "lth_realized_price": {"score": 7, "value": 49767.33015910989}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-06-27T19:42:45.679921+00:00", "composite_score": 74.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 8, "value": 15}, "puell_multiple": {"score": 5, "value": 0.7044739567707577}, "mvrv_zscore": {"score": 8, "value": 0.22409759021503936}, "drawdown": {"score": 8, "value": 52.35009517766498}, "price_vs_200w_sma": {"score": 10, "value": 62284.65298428873}, "reserve_risk": {"score": 10, "value": 0.0010258831016337609}, "rhodl_ratio": {"score": 7, "value": 882.5868942025234}, "nupl": {"score": 8, "value": 0.11309045542914359}, "lth_realized_price": {"score": 7, "value": 49767.33015910989}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-06-27T19:57:46.474768+00:00", "composite_score": 74.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 8, "value": 15}, "puell_multiple": {"score": 5, "value": 0.7044739567707577}, "mvrv_zscore": {"score": 8, "value": 0.22409759021503936}, "drawdown": {"score": 8, "value": 52.34850888324873}, "price_vs_200w_sma": {"score": 10, "value": 62284.65298428873}, "reserve_risk": {"score": 10, "value": 0.0010258831016337609}, "rhodl_ratio": {"score": 7, "value": 882.5868942025234}, "nupl": {"score": 8, "value": 0.11309045542914359}, "lth_realized_price": {"score": 7, "value": 49767.33015910989}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-06-27T20:12:47.228456+00:00", "composite_score": 74.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 8, "value": 15}, "puell_multiple": {"score": 5, "value": 0.7044739567707577}, "mvrv_zscore": {"score": 8, "value": 0.22409759021503936}, "drawdown": {"score": 8, "value": 52.2604695431472}, "price_vs_200w_sma": {"score": 10, "value": 62284.65298428873}, "reserve_risk": {"score": 10, "value": 0.0010258831016337609}, "rhodl_ratio": {"score": 7, "value": 882.5868942025234}, "nupl": {"score": 8, "value": 0.11309045542914359}, "lth_realized_price": {"score": 7, "value": 49767.33015910989}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-06-27T20:27:47.904578+00:00", "composite_score": 74.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 8, "value": 15}, "puell_multiple": {"score": 5, "value": 0.7044739567707577}, "mvrv_zscore": {"score": 8, "value": 0.22409759021503936}, "drawdown": {"score": 8, "value": 52.27633248730964}, "price_vs_200w_sma": {"score": 10, "value": 62284.65298428873}, "reserve_risk": {"score": 10, "value": 0.0010258831016337609}, "rhodl_ratio": {"score": 7, "value": 882.5868942025234}, "nupl": {"score": 8, "value": 0.11309045542914359}, "lth_realized_price": {"score": 7, "value": 49767.33015910989}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-06-27T20:42:48.631663+00:00", "composite_score": 74.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 8, "value": 15}, "puell_multiple": {"score": 5, "value": 0.7044739567707577}, "mvrv_zscore": {"score": 8, "value": 0.22409759021503936}, "drawdown": {"score": 8, "value": 52.26919416243655}, "price_vs_200w_sma": {"score": 10, "value": 62284.65298428873}, "reserve_risk": {"score": 10, "value": 0.0010258831016337609}, "rhodl_ratio": {"score": 7, "value": 882.5868942025234}, "nupl": {"score": 8, "value": 0.11309045542914359}, "lth_realized_price": {"score": 7, "value": 49767.33015910989}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-06-27T20:57:49.365132+00:00", "composite_score": 74.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 8, "value": 15}, "puell_multiple": {"score": 5, "value": 0.7044739567707577}, "mvrv_zscore": {"score": 8, "value": 0.22409759021503936}, "drawdown": {"score": 8, "value": 52.264435279187815}, "price_vs_200w_sma": {"score": 10, "value": 62284.65298428873}, "reserve_risk": {"score": 10, "value": 0.0010258831016337609}, "rhodl_ratio": {"score": 7, "value": 882.5868942025234}, "nupl": {"score": 8, "value": 0.11309045542914359}, "lth_realized_price": {"score": 7, "value": 49767.33015910989}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-06-27T21:12:50.093194+00:00", "composite_score": 74.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 8, "value": 15}, "puell_multiple": {"score": 5, "value": 0.7044739567707577}, "mvrv_zscore": {"score": 8, "value": 0.22409759021503936}, "drawdown": {"score": 8, "value": 52.202569796954315}, "price_vs_200w_sma": {"score": 10, "value": 62284.65298428873}, "reserve_risk": {"score": 10, "value": 0.0010258831016337609}, "rhodl_ratio": {"score": 7, "value": 882.5868942025234}, "nupl": {"score": 8, "value": 0.11309045542914359}, "lth_realized_price": {"score": 7, "value": 49767.33015910989}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-06-27T21:27:50.802146+00:00", "composite_score": 74.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 8, "value": 15}, "puell_multiple": {"score": 5, "value": 0.7044739567707577}, "mvrv_zscore": {"score": 8, "value": 0.22409759021503936}, "drawdown": {"score": 8, "value": 52.16132614213198}, "price_vs_200w_sma": {"score": 10, "value": 62284.65298428873}, "reserve_risk": {"score": 10, "value": 0.0010258831016337609}, "rhodl_ratio": {"score": 7, "value": 882.5868942025234}, "nupl": {"score": 8, "value": 0.11309045542914359}, "lth_realized_price": {"score": 7, "value": 49767.33015910989}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-06-27T21:42:51.460898+00:00", "composite_score": 74.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 8, "value": 15}, "puell_multiple": {"score": 5, "value": 0.7044739567707577}, "mvrv_zscore": {"score": 8, "value": 0.22409759021503936}, "drawdown": {"score": 8, "value": 52.24857233502538}, "price_vs_200w_sma": {"score": 10, "value": 62284.65298428873}, "reserve_risk": {"score": 10, "value": 0.0010258831016337609}, "rhodl_ratio": {"score": 7, "value": 882.5868942025234}, "nupl": {"score": 8, "value": 0.11309045542914359}, "lth_realized_price": {"score": 7, "value": 49767.33015910989}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-06-27T21:57:52.223283+00:00", "composite_score": 74.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 8, "value": 15}, "puell_multiple": {"score": 5, "value": 0.7044739567707577}, "mvrv_zscore": {"score": 8, "value": 0.22409759021503936}, "drawdown": {"score": 8, "value": 52.318369289340104}, "price_vs_200w_sma": {"score": 10, "value": 62284.65298428873}, "reserve_risk": {"score": 10, "value": 0.0010258831016337609}, "rhodl_ratio": {"score": 7, "value": 882.5868942025234}, "nupl": {"score": 8, "value": 0.11309045542914359}, "lth_realized_price": {"score": 7, "value": 49767.33015910989}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-06-27T22:12:52.898551+00:00", "composite_score": 74.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 8, "value": 15}, "puell_multiple": {"score": 5, "value": 0.7044739567707577}, "mvrv_zscore": {"score": 8, "value": 0.22409759021503936}, "drawdown": {"score": 8, "value": 52.37071700507614}, "price_vs_200w_sma": {"score": 10, "value": 62284.65298428873}, "reserve_risk": {"score": 10, "value": 0.0010258831016337609}, "rhodl_ratio": {"score": 7, "value": 882.5868942025234}, "nupl": {"score": 8, "value": 0.11309045542914359}, "lth_realized_price": {"score": 7, "value": 49767.33015910989}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-06-27T22:27:54.711182+00:00", "composite_score": 74.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 8, "value": 15}, "puell_multiple": {"score": 5, "value": 0.7044739567707577}, "mvrv_zscore": {"score": 8, "value": 0.22409759021503936}, "drawdown": {"score": 8, "value": 52.429409898477154}, "price_vs_200w_sma": {"score": 10, "value": 62284.65298428873}, "reserve_risk": {"score": 10, "value": 0.0010258831016337609}, "rhodl_ratio": {"score": 7, "value": 882.5868942025234}, "nupl": {"score": 8, "value": 0.11309045542914359}, "lth_realized_price": {"score": 7, "value": 49767.33015910989}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-06-27T22:42:55.510257+00:00", "composite_score": 74.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 8, "value": 15}, "puell_multiple": {"score": 5, "value": 0.7044739567707577}, "mvrv_zscore": {"score": 8, "value": 0.22409759021503936}, "drawdown": {"score": 8, "value": 52.31043781725888}, "price_vs_200w_sma": {"score": 10, "value": 62284.65298428873}, "reserve_risk": {"score": 10, "value": 0.0010258831016337609}, "rhodl_ratio": {"score": 7, "value": 882.5868942025234}, "nupl": {"score": 8, "value": 0.11309045542914359}, "lth_realized_price": {"score": 7, "value": 49767.33015910989}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-06-28T20:31:14.232026+00:00", "composite_score": 74.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 8, "value": 18}, "puell_multiple": {"score": 5, "value": 0.7044739567707577}, "mvrv_zscore": {"score": 8, "value": 0.22409759021503936}, "drawdown": {"score": 8, "value": 52.70066624365482}, "price_vs_200w_sma": {"score": 10, "value": 62284.65298428873}, "reserve_risk": {"score": 10, "value": 0.0010258831016337609}, "rhodl_ratio": {"score": 7, "value": 882.5868942025234}, "nupl": {"score": 8, "value": 0.11309045542914359}, "lth_realized_price": {"score": 7, "value": 49767.33015910989}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-06-28T20:35:38.591732+00:00", "composite_score": 74.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 8, "value": 18}, "puell_multiple": {"score": 5, "value": 0.7044739567707577}, "mvrv_zscore": {"score": 8, "value": 0.22409759021503936}, "drawdown": {"score": 8, "value": 52.73080583756345}, "price_vs_200w_sma": {"score": 10, "value": 62284.65298428873}, "reserve_risk": {"score": 10, "value": 0.0010258831016337609}, "rhodl_ratio": {"score": 7, "value": 882.5868942025234}, "nupl": {"score": 8, "value": 0.11309045542914359}, "lth_realized_price": {"score": 7, "value": 49767.33015910989}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-06-28T20:42:21.081121+00:00", "composite_score": 74.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 8, "value": 18}, "puell_multiple": {"score": 5, "value": 0.7044739567707577}, "mvrv_zscore": {"score": 8, "value": 0.22409759021503936}, "drawdown": {"score": 8, "value": 52.721288071065985}, "price_vs_200w_sma": {"score": 10, "value": 62284.65298428873}, "reserve_risk": {"score": 10, "value": 0.0010258831016337609}, "rhodl_ratio": {"score": 7, "value": 882.5868942025234}, "nupl": {"score": 8, "value": 0.11309045542914359}, "lth_realized_price": {"score": 7, "value": 49767.33015910989}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-06-28T20:57:21.749415+00:00", "composite_score": 74.0, "scored_count": 10, "metrics": {"fear_greed": {"score": 8, "value": 18}, "puell_multiple": {"score": 5, "value": 0.7044739567707577}, "mvrv_zscore": {"score": 8, "value": 0.22409759021503936}, "drawdown": {"score": 8, "value": 52.75301395939086}, "price_vs_200w_sma": {"score": 10, "value": 62284.65298428873}, "reserve_risk": {"score": 10, "value": 0.0010258831016337609}, "rhodl_ratio": {"score": 7, "value": 882.5868942025234}, "nupl": {"score": 8, "value": 0.11309045542914359}, "lth_realized_price": {"score": 7, "value": 49767.33015910989}, "hash_ribbons": {"score": 3, "value": null}}}
|
||||
{"timestamp": "2026-06-28T21:09:38.418891+00:00", "composite_score": 68.1, "scored_count": 16, "metrics": {"fear_greed": {"score": 8, "value": 18}, "puell_multiple": {"score": 5, "value": 0.7044739567707577}, "mvrv_zscore": {"score": 8, "value": 0.22409759021503936}, "drawdown": {"score": 8, "value": 52.73159898477158}, "price_vs_200w_sma": {"score": 10, "value": 62284.65298428873}, "reserve_risk": {"score": 10, "value": 0.0010258831016337609}, "rhodl_ratio": {"score": 7, "value": 882.5868942025234}, "nupl": {"score": 8, "value": 0.11309045542914359}, "lth_realized_price": {"score": 7, "value": 49767.33015910989}, "hash_ribbons": {"score": 3, "value": null}, "sopr": {"score": 8, "value": 0.990007085019496}, "sellside_risk": {"score": 10, "value": 0.000734882488382521}, "active_address_momentum": {"score": 4, "value": -0.09386733861382784}, "txcount_momentum": {"score": 4, "value": 0.04454611494295241}, "nvt_price": {"score": 5, "value": 54673.815447216126}, "vdd_multiple": {"score": 4, "value": -0.030495759573562122}}}
|
||||
{"timestamp": "2026-06-28T21:11:19.117377+00:00", "composite_score": 69.4, "scored_count": 16, "metrics": {"fear_greed": {"score": 8, "value": 18}, "puell_multiple": {"score": 5, "value": 0.7044739567707577}, "mvrv_zscore": {"score": 8, "value": 0.22409759021503936}, "drawdown": {"score": 8, "value": 52.723667512690355}, "price_vs_200w_sma": {"score": 10, "value": 62284.65298428873}, "reserve_risk": {"score": 10, "value": 0.0010258831016337609}, "rhodl_ratio": {"score": 7, "value": 882.5868942025234}, "nupl": {"score": 8, "value": 0.11309045542914359}, "lth_realized_price": {"score": 7, "value": 49767.33015910989}, "hash_ribbons": {"score": 3, "value": null}, "sopr": {"score": 8, "value": 0.990007085019496}, "sellside_risk": {"score": 10, "value": 0.000734882488382521}, "active_address_momentum": {"score": 4, "value": -0.09386733861382784}, "txcount_momentum": {"score": 6, "value": 0.04454611494295241}, "nvt_price": {"score": 5, "value": 54673.815447216126}, "vdd_multiple": {"score": 4, "value": -0.030495759573562122}}}
|
||||
{"timestamp": "2026-06-28T21:26:19.843356+00:00", "composite_score": 69.4, "scored_count": 16, "metrics": {"fear_greed": {"score": 8, "value": 18}, "puell_multiple": {"score": 5, "value": 0.7044739567707577}, "mvrv_zscore": {"score": 8, "value": 0.22409759021503936}, "drawdown": {"score": 8, "value": 52.723667512690355}, "price_vs_200w_sma": {"score": 10, "value": 62284.65298428873}, "reserve_risk": {"score": 10, "value": 0.0010258831016337609}, "rhodl_ratio": {"score": 7, "value": 882.5868942025234}, "nupl": {"score": 8, "value": 0.11309045542914359}, "lth_realized_price": {"score": 7, "value": 49767.33015910989}, "hash_ribbons": {"score": 3, "value": null}, "sopr": {"score": 8, "value": 0.990007085019496}, "sellside_risk": {"score": 10, "value": 0.000734882488382521}, "active_address_momentum": {"score": 4, "value": -0.09386733861382784}, "txcount_momentum": {"score": 6, "value": 0.04454611494295241}, "nvt_price": {"score": 5, "value": 54673.815447216126}, "vdd_multiple": {"score": 4, "value": -0.030495759573562122}}}
|
||||
{"timestamp": "2026-06-28T21:39:03.697500+00:00", "composite_score": 69.4, "scored_count": 16, "metrics": {"fear_greed": {"score": 8, "value": 18}, "puell_multiple": {"score": 5, "value": 0.7044739567707577}, "mvrv_zscore": {"score": 8, "value": 0.22409759021503936}, "drawdown": {"score": 8, "value": 52.68797588832488}, "price_vs_200w_sma": {"score": 10, "value": 62284.65298428873}, "reserve_risk": {"score": 10, "value": 0.0010258831016337609}, "rhodl_ratio": {"score": 7, "value": 882.5868942025234}, "nupl": {"score": 8, "value": 0.11309045542914359}, "lth_realized_price": {"score": 7, "value": 49767.33015910989}, "hash_ribbons": {"score": 3, "value": null}, "sopr": {"score": 8, "value": 0.990007085019496}, "sellside_risk": {"score": 10, "value": 0.000734882488382521}, "active_address_momentum": {"score": 4, "value": -0.09386733861382784}, "txcount_momentum": {"score": 6, "value": 0.04454611494295241}, "nvt_price": {"score": 5, "value": 54673.815447216126}, "vdd_multiple": {"score": 4, "value": -0.030495759573562122}}}
|
||||
{"timestamp": "2026-06-28T21:42:37.143098+00:00", "composite_score": 69.4, "scored_count": 16, "metrics": {"fear_greed": {"score": 8, "value": 18}, "puell_multiple": {"score": 5, "value": 0.7044739567707577}, "mvrv_zscore": {"score": 8, "value": 0.22409759021503936}, "drawdown": {"score": 8, "value": 52.71890862944163}, "price_vs_200w_sma": {"score": 10, "value": 62284.65298428873}, "reserve_risk": {"score": 10, "value": 0.0010258831016337609}, "rhodl_ratio": {"score": 7, "value": 882.5868942025234}, "nupl": {"score": 8, "value": 0.11309045542914359}, "lth_realized_price": {"score": 7, "value": 49767.33015910989}, "hash_ribbons": {"score": 3, "value": null}, "sopr": {"score": 8, "value": 0.990007085019496}, "sellside_risk": {"score": 10, "value": 0.000734882488382521}, "active_address_momentum": {"score": 4, "value": -0.09386733861382784}, "txcount_momentum": {"score": 6, "value": 0.04454611494295241}, "nvt_price": {"score": 5, "value": 54673.815447216126}, "vdd_multiple": {"score": 4, "value": -0.030495759573562122}}}
|
||||
{"timestamp": "2026-06-28T21:57:37.803751+00:00", "composite_score": 69.4, "scored_count": 16, "metrics": {"fear_greed": {"score": 8, "value": 18}, "puell_multiple": {"score": 5, "value": 0.7044739567707577}, "mvrv_zscore": {"score": 8, "value": 0.22409759021503936}, "drawdown": {"score": 8, "value": 52.96002538071066}, "price_vs_200w_sma": {"score": 10, "value": 62284.65298428873}, "reserve_risk": {"score": 10, "value": 0.0010258831016337609}, "rhodl_ratio": {"score": 7, "value": 882.5868942025234}, "nupl": {"score": 8, "value": 0.11309045542914359}, "lth_realized_price": {"score": 7, "value": 49767.33015910989}, "hash_ribbons": {"score": 3, "value": null}, "sopr": {"score": 8, "value": 0.990007085019496}, "sellside_risk": {"score": 10, "value": 0.000734882488382521}, "active_address_momentum": {"score": 4, "value": -0.09386733861382784}, "txcount_momentum": {"score": 6, "value": 0.04454611494295241}, "nvt_price": {"score": 5, "value": 54673.815447216126}, "vdd_multiple": {"score": 4, "value": -0.030495759573562122}}}
|
||||
{"timestamp": "2026-06-28T22:12:38.557670+00:00", "composite_score": 69.4, "scored_count": 16, "metrics": {"fear_greed": {"score": 8, "value": 18}, "puell_multiple": {"score": 5, "value": 0.7044739567707577}, "mvrv_zscore": {"score": 8, "value": 0.22409759021503936}, "drawdown": {"score": 8, "value": 52.72049492385786}, "price_vs_200w_sma": {"score": 10, "value": 62284.65298428873}, "reserve_risk": {"score": 10, "value": 0.0010258831016337609}, "rhodl_ratio": {"score": 7, "value": 882.5868942025234}, "nupl": {"score": 8, "value": 0.11309045542914359}, "lth_realized_price": {"score": 7, "value": 49767.33015910989}, "hash_ribbons": {"score": 3, "value": null}, "sopr": {"score": 8, "value": 0.990007085019496}, "sellside_risk": {"score": 10, "value": 0.000734882488382521}, "active_address_momentum": {"score": 4, "value": -0.09386733861382784}, "txcount_momentum": {"score": 6, "value": 0.04454611494295241}, "nvt_price": {"score": 5, "value": 54673.815447216126}, "vdd_multiple": {"score": 4, "value": -0.030495759573562122}}}
|
||||
{"timestamp": "2026-06-28T22:27:39.293792+00:00", "composite_score": 69.4, "scored_count": 16, "metrics": {"fear_greed": {"score": 8, "value": 18}, "puell_multiple": {"score": 5, "value": 0.7044739567707577}, "mvrv_zscore": {"score": 8, "value": 0.22409759021503936}, "drawdown": {"score": 8, "value": 52.94733502538072}, "price_vs_200w_sma": {"score": 10, "value": 62284.65298428873}, "reserve_risk": {"score": 10, "value": 0.0010258831016337609}, "rhodl_ratio": {"score": 7, "value": 882.5868942025234}, "nupl": {"score": 8, "value": 0.11309045542914359}, "lth_realized_price": {"score": 7, "value": 49767.33015910989}, "hash_ribbons": {"score": 3, "value": null}, "sopr": {"score": 8, "value": 0.990007085019496}, "sellside_risk": {"score": 10, "value": 0.000734882488382521}, "active_address_momentum": {"score": 4, "value": -0.09386733861382784}, "txcount_momentum": {"score": 6, "value": 0.04454611494295241}, "nvt_price": {"score": 5, "value": 54673.815447216126}, "vdd_multiple": {"score": 4, "value": -0.030495759573562122}}}
|
||||
{"timestamp": "2026-06-28T22:42:40.175733+00:00", "composite_score": 69.4, "scored_count": 16, "metrics": {"fear_greed": {"score": 8, "value": 18}, "puell_multiple": {"score": 5, "value": 0.7044739567707577}, "mvrv_zscore": {"score": 8, "value": 0.22409759021503936}, "drawdown": {"score": 8, "value": 53.05678934010152}, "price_vs_200w_sma": {"score": 10, "value": 62284.65298428873}, "reserve_risk": {"score": 10, "value": 0.0010258831016337609}, "rhodl_ratio": {"score": 7, "value": 882.5868942025234}, "nupl": {"score": 8, "value": 0.11309045542914359}, "lth_realized_price": {"score": 7, "value": 49767.33015910989}, "hash_ribbons": {"score": 3, "value": null}, "sopr": {"score": 8, "value": 0.990007085019496}, "sellside_risk": {"score": 10, "value": 0.000734882488382521}, "active_address_momentum": {"score": 4, "value": -0.09386733861382784}, "txcount_momentum": {"score": 6, "value": 0.04454611494295241}, "nvt_price": {"score": 5, "value": 54673.815447216126}, "vdd_multiple": {"score": 4, "value": -0.030495759573562122}}}
|
||||
{"timestamp": "2026-06-28T22:57:40.874889+00:00", "composite_score": 69.4, "scored_count": 16, "metrics": {"fear_greed": {"score": 8, "value": 18}, "puell_multiple": {"score": 5, "value": 0.7044739567707577}, "mvrv_zscore": {"score": 8, "value": 0.22409759021503936}, "drawdown": {"score": 8, "value": 53.22255710659899}, "price_vs_200w_sma": {"score": 10, "value": 62284.65298428873}, "reserve_risk": {"score": 10, "value": 0.0010258831016337609}, "rhodl_ratio": {"score": 7, "value": 882.5868942025234}, "nupl": {"score": 8, "value": 0.11309045542914359}, "lth_realized_price": {"score": 7, "value": 49767.33015910989}, "hash_ribbons": {"score": 3, "value": null}, "sopr": {"score": 8, "value": 0.990007085019496}, "sellside_risk": {"score": 10, "value": 0.000734882488382521}, "active_address_momentum": {"score": 4, "value": -0.09386733861382784}, "txcount_momentum": {"score": 6, "value": 0.04454611494295241}, "nvt_price": {"score": 5, "value": 54673.815447216126}, "vdd_multiple": {"score": 4, "value": -0.030495759573562122}}}
|
||||
{"timestamp": "2026-06-28T23:12:41.665346+00:00", "composite_score": 69.4, "scored_count": 16, "metrics": {"fear_greed": {"score": 8, "value": 18}, "puell_multiple": {"score": 5, "value": 0.7044739567707577}, "mvrv_zscore": {"score": 8, "value": 0.22409759021503936}, "drawdown": {"score": 8, "value": 53.02744289340101}, "price_vs_200w_sma": {"score": 10, "value": 62284.65298428873}, "reserve_risk": {"score": 10, "value": 0.0010258831016337609}, "rhodl_ratio": {"score": 7, "value": 882.5868942025234}, "nupl": {"score": 8, "value": 0.11309045542914359}, "lth_realized_price": {"score": 7, "value": 49767.33015910989}, "hash_ribbons": {"score": 3, "value": null}, "sopr": {"score": 8, "value": 0.990007085019496}, "sellside_risk": {"score": 10, "value": 0.000734882488382521}, "active_address_momentum": {"score": 4, "value": -0.09386733861382784}, "txcount_momentum": {"score": 6, "value": 0.04454611494295241}, "nvt_price": {"score": 5, "value": 54673.815447216126}, "vdd_multiple": {"score": 4, "value": -0.030495759573562122}}}
|
||||
{"timestamp": "2026-06-28T23:27:42.349311+00:00", "composite_score": 69.4, "scored_count": 16, "metrics": {"fear_greed": {"score": 8, "value": 18}, "puell_multiple": {"score": 5, "value": 0.7044739567707577}, "mvrv_zscore": {"score": 8, "value": 0.22409759021503936}, "drawdown": {"score": 8, "value": 52.95923223350254}, "price_vs_200w_sma": {"score": 10, "value": 62284.65298428873}, "reserve_risk": {"score": 10, "value": 0.0010258831016337609}, "rhodl_ratio": {"score": 7, "value": 882.5868942025234}, "nupl": {"score": 8, "value": 0.11309045542914359}, "lth_realized_price": {"score": 7, "value": 49767.33015910989}, "hash_ribbons": {"score": 3, "value": null}, "sopr": {"score": 8, "value": 0.990007085019496}, "sellside_risk": {"score": 10, "value": 0.000734882488382521}, "active_address_momentum": {"score": 4, "value": -0.09386733861382784}, "txcount_momentum": {"score": 6, "value": 0.04454611494295241}, "nvt_price": {"score": 5, "value": 54673.815447216126}, "vdd_multiple": {"score": 4, "value": -0.030495759573562122}}}
|
||||
|
||||
+656
@@ -0,0 +1,656 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
ML Optimizer for Bitcoin Accumulation Zone Scoring.
|
||||
|
||||
Trains a gradient boosted tree model on historical on-chain metrics to find
|
||||
optimal metric weights for identifying the best long-term buying opportunities.
|
||||
|
||||
Output: config/ml_weights.json with optimized weights and feature importances.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import numpy as np
|
||||
from sklearn.ensemble import GradientBoostingClassifier
|
||||
from sklearn.metrics import (
|
||||
classification_report,
|
||||
f1_score,
|
||||
precision_score,
|
||||
recall_score,
|
||||
roc_auc_score,
|
||||
)
|
||||
from sklearn.model_selection import TimeSeriesSplit
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
|
||||
)
|
||||
log = logging.getLogger("ml-optimizer")
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
HISTORY_PATH = os.path.join(BASE_DIR, "data", "history.json")
|
||||
OUTPUT_PATH = os.path.join(BASE_DIR, "config", "ml_weights.json")
|
||||
THRESHOLDS_PATH = os.path.join(BASE_DIR, "config", "thresholds.json")
|
||||
|
||||
# Date range: 2018-02-01 onward (when all 8 metrics + fear_greed available)
|
||||
START_DATE = "2018-02-01"
|
||||
# Training cutoff: need 1yr forward data for labels
|
||||
TRAIN_CUTOFF_DAYS = 365
|
||||
# Target: forward 365d return > 30% = "good time to buy"
|
||||
GOOD_BUY_THRESHOLD = 30.0
|
||||
# Validation embargo/purge horizon: labels use 365-day forward returns.
|
||||
LABEL_HORIZON_DAYS = 365
|
||||
VALIDATION_SPLITS = 5
|
||||
|
||||
# The 8 core metrics we score
|
||||
METRIC_KEYS = [
|
||||
"puell_multiple",
|
||||
"mvrv_zscore",
|
||||
"reserve_risk",
|
||||
"rhodl_ratio",
|
||||
"nupl",
|
||||
"fear_greed",
|
||||
]
|
||||
# Ratio-based metrics (derived from price vs reference)
|
||||
RATIO_METRICS = {
|
||||
"pct_above_200w_sma": {"price_key": "btc_price", "ref_key": "200w_sma"},
|
||||
"pct_above_lth_rp": {"price_key": "btc_price", "ref_key": "lth_realized_price"},
|
||||
}
|
||||
|
||||
|
||||
def load_history():
|
||||
"""Load historical data and build date-aligned lookup."""
|
||||
with open(HISTORY_PATH) as f:
|
||||
raw = json.load(f)
|
||||
|
||||
index = {}
|
||||
for key, data in raw.items():
|
||||
if not isinstance(data, dict) or "dates" not in data:
|
||||
continue
|
||||
lookup = {}
|
||||
for d, v in zip(data["dates"], data["values"]):
|
||||
if v is not None:
|
||||
lookup[d] = v
|
||||
index[key] = lookup
|
||||
return index
|
||||
|
||||
|
||||
def load_thresholds():
|
||||
"""Load scoring thresholds for converting raw values to 0-10 scores."""
|
||||
with open(THRESHOLDS_PATH) as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def score_range(value, ranges):
|
||||
"""Score a value using range-based thresholds (same logic as scoring/engine.py)."""
|
||||
if value is None:
|
||||
return None
|
||||
for low, high, score in ranges:
|
||||
low_ok = low is None or value >= low
|
||||
high_ok = high is None or value < high
|
||||
if low_ok and high_ok:
|
||||
return score
|
||||
return 0
|
||||
|
||||
|
||||
SCORE_KEYS = [
|
||||
"puell_multiple", "mvrv_zscore", "reserve_risk", "rhodl_ratio",
|
||||
"nupl", "fear_greed", "drawdown", "pct_above_200w_sma", "pct_above_lth_rp",
|
||||
]
|
||||
|
||||
SCORE_FEATURES = [f"score_{k}" for k in SCORE_KEYS]
|
||||
RAW_FEATURES = [
|
||||
"raw_puell_multiple", "raw_mvrv_zscore", "raw_reserve_risk",
|
||||
"raw_rhodl_ratio", "raw_nupl", "raw_fear_greed",
|
||||
"raw_pct_above_200w_sma", "raw_pct_above_lth_rp", "raw_drawdown",
|
||||
]
|
||||
DELTA_FEATURES = [
|
||||
"delta_30d_mvrv_zscore", "delta_30d_nupl",
|
||||
"delta_30d_puell_multiple", "delta_30d_reserve_risk",
|
||||
]
|
||||
INTERACTION_FEATURES = ["mvrv_x_nupl", "puell_x_reserve"]
|
||||
CYCLE_FEATURES = ["days_since_ath"]
|
||||
FEATURE_COLS = SCORE_FEATURES + RAW_FEATURES + DELTA_FEATURES + INTERACTION_FEATURES + CYCLE_FEATURES
|
||||
|
||||
BRACKETS = [
|
||||
(0, 20, "Extreme Caution"),
|
||||
(21, 40, "Caution"),
|
||||
(41, 55, "Neutral"),
|
||||
(56, 70, "Moderate Opportunity"),
|
||||
(71, 85, "Strong Accumulation"),
|
||||
(86, 100, "Extreme Accumulation"),
|
||||
]
|
||||
|
||||
|
||||
def _row_date(row):
|
||||
return datetime.strptime(row["date"], "%Y-%m-%d")
|
||||
|
||||
|
||||
def purged_time_series_splits(rows, n_splits=VALIDATION_SPLITS,
|
||||
label_horizon_days=LABEL_HORIZON_DAYS,
|
||||
embargo_days=0):
|
||||
"""Yield expanding-window splits with overlapping forward-label windows removed.
|
||||
|
||||
A row dated T with a 365-day forward-return label consumes information up to
|
||||
T+365. For validation beginning at V, any training row whose label window
|
||||
reaches V is removed. This keeps validation metrics out-of-sample for the
|
||||
forward-return label, not just for features.
|
||||
"""
|
||||
base_splitter = TimeSeriesSplit(n_splits=n_splits)
|
||||
row_dates = [_row_date(r) for r in rows]
|
||||
horizon = timedelta(days=label_horizon_days)
|
||||
embargo = timedelta(days=embargo_days)
|
||||
|
||||
for train_idx, val_idx in base_splitter.split(np.arange(len(rows))):
|
||||
val_start = row_dates[val_idx[0]]
|
||||
val_end = row_dates[val_idx[-1]]
|
||||
purged_train = []
|
||||
for idx in train_idx:
|
||||
label_end = row_dates[idx] + horizon
|
||||
before_validation_label_window = label_end <= val_start - embargo
|
||||
after_validation_embargo = row_dates[idx] > val_end + embargo
|
||||
if before_validation_label_window or after_validation_embargo:
|
||||
purged_train.append(idx)
|
||||
if purged_train:
|
||||
yield np.array(purged_train, dtype=int), np.array(val_idx, dtype=int)
|
||||
|
||||
|
||||
def _build_model():
|
||||
return GradientBoostingClassifier(
|
||||
n_estimators=300,
|
||||
learning_rate=0.05,
|
||||
max_depth=4,
|
||||
subsample=0.8,
|
||||
min_samples_leaf=20,
|
||||
random_state=42,
|
||||
)
|
||||
|
||||
|
||||
def derive_metric_weights(feature_cols, importances):
|
||||
"""Aggregate feature importances back to transparent score metric weights."""
|
||||
metric_names = list(SCORE_KEYS)
|
||||
feature_to_metric = {}
|
||||
for m in metric_names:
|
||||
feature_to_metric[f"score_{m}"] = m
|
||||
feature_to_metric[f"raw_{m}"] = m
|
||||
feature_to_metric["delta_30d_mvrv_zscore"] = "mvrv_zscore"
|
||||
feature_to_metric["delta_30d_nupl"] = "nupl"
|
||||
feature_to_metric["delta_30d_puell_multiple"] = "puell_multiple"
|
||||
feature_to_metric["delta_30d_reserve_risk"] = "reserve_risk"
|
||||
|
||||
metric_importances = {m: 0.0 for m in metric_names}
|
||||
for name, imp in zip(feature_cols, importances):
|
||||
if name in feature_to_metric:
|
||||
metric_importances[feature_to_metric[name]] += float(imp)
|
||||
elif name == "mvrv_x_nupl":
|
||||
metric_importances["mvrv_zscore"] += float(imp) / 2
|
||||
metric_importances["nupl"] += float(imp) / 2
|
||||
elif name == "puell_x_reserve":
|
||||
metric_importances["puell_multiple"] += float(imp) / 2
|
||||
metric_importances["reserve_risk"] += float(imp) / 2
|
||||
elif name == "days_since_ath":
|
||||
metric_importances["drawdown"] += float(imp)
|
||||
|
||||
total_imp = sum(metric_importances.values())
|
||||
if total_imp > 0:
|
||||
weights = {k: round(v / total_imp, 4) for k, v in metric_importances.items()}
|
||||
else:
|
||||
weights = {k: round(1 / len(metric_importances), 4) for k in metric_importances}
|
||||
return dict(sorted(weights.items(), key=lambda x: x[1], reverse=True))
|
||||
|
||||
|
||||
def build_dataset(index, thresholds):
|
||||
"""Build aligned training dataset: metric scores + forward returns."""
|
||||
# Get all dates from 2018-02-01 onward
|
||||
all_dates = set()
|
||||
for lookup in index.values():
|
||||
all_dates.update(lookup.keys())
|
||||
dates = sorted(d for d in all_dates if d >= START_DATE)
|
||||
|
||||
# Build price lookup for forward returns
|
||||
price_lookup = {}
|
||||
for pk in ["btc_price", "btc_price_sma", "btc_price_lth"]:
|
||||
if pk in index:
|
||||
for d, v in index[pk].items():
|
||||
if d not in price_lookup:
|
||||
price_lookup[d] = v
|
||||
|
||||
# Compute ATH series for drawdown
|
||||
all_dates_sorted = sorted(all_dates)
|
||||
ath = 0
|
||||
drawdowns = {}
|
||||
for d in all_dates_sorted:
|
||||
p = price_lookup.get(d)
|
||||
if p is None:
|
||||
continue
|
||||
if p > ath:
|
||||
ath = p
|
||||
if ath > 0:
|
||||
drawdowns[d] = ((ath - p) / ath) * 100
|
||||
|
||||
# Get threshold ranges for scoring raw values
|
||||
metric_ranges = {
|
||||
"puell_multiple": thresholds.get("puell_multiple", {}).get("ranges", []),
|
||||
"mvrv_zscore": thresholds.get("mvrv_zscore", {}).get("ranges", []),
|
||||
"reserve_risk": thresholds.get("reserve_risk", {}).get("ranges", []),
|
||||
"rhodl_ratio": thresholds.get("rhodl_ratio", {}).get("ranges", []),
|
||||
"nupl": thresholds.get("nupl", {}).get("ranges", []),
|
||||
"fear_greed": thresholds.get("fear_greed", {}).get("ranges", []),
|
||||
"drawdown": thresholds.get("drawdown", {}).get("ranges", []),
|
||||
"price_vs_200w_sma": thresholds.get("price_vs_200w_sma", {}).get("ranges", []),
|
||||
"lth_realized_price": thresholds.get("lth_realized_price", {}).get("ranges", []),
|
||||
}
|
||||
|
||||
log.info("Building dataset from %d dates (%s to %s)", len(dates), dates[0], dates[-1])
|
||||
|
||||
rows = []
|
||||
for d in dates:
|
||||
# Get raw metric values
|
||||
vals = {}
|
||||
skip = False
|
||||
for key in METRIC_KEYS:
|
||||
v = index.get(key, {}).get(d)
|
||||
if v is None:
|
||||
skip = True
|
||||
break
|
||||
vals[key] = v
|
||||
if skip:
|
||||
continue
|
||||
|
||||
# Compute ratio metrics
|
||||
price = price_lookup.get(d)
|
||||
sma_200w = index.get("200w_sma", {}).get(d)
|
||||
lth_rp = index.get("lth_realized_price", {}).get(d)
|
||||
|
||||
if price is None or sma_200w is None or lth_rp is None:
|
||||
continue
|
||||
if sma_200w == 0 or lth_rp == 0:
|
||||
continue
|
||||
|
||||
pct_200w = ((price - sma_200w) / sma_200w) * 100
|
||||
pct_lth = ((price - lth_rp) / lth_rp) * 100
|
||||
dd = drawdowns.get(d, 0)
|
||||
|
||||
vals["pct_above_200w_sma"] = pct_200w
|
||||
vals["pct_above_lth_rp"] = pct_lth
|
||||
vals["drawdown"] = dd
|
||||
|
||||
# Score each metric (0-10) using existing thresholds
|
||||
scores = {}
|
||||
scores["puell_multiple"] = score_range(vals["puell_multiple"], metric_ranges["puell_multiple"])
|
||||
scores["mvrv_zscore"] = score_range(vals["mvrv_zscore"], metric_ranges["mvrv_zscore"])
|
||||
scores["reserve_risk"] = score_range(vals["reserve_risk"], metric_ranges["reserve_risk"])
|
||||
scores["rhodl_ratio"] = score_range(vals["rhodl_ratio"], metric_ranges["rhodl_ratio"])
|
||||
scores["nupl"] = score_range(vals["nupl"], metric_ranges["nupl"])
|
||||
scores["fear_greed"] = score_range(vals["fear_greed"], metric_ranges["fear_greed"])
|
||||
scores["drawdown"] = score_range(dd, metric_ranges["drawdown"])
|
||||
scores["pct_above_200w_sma"] = score_range(pct_200w, metric_ranges["price_vs_200w_sma"])
|
||||
scores["pct_above_lth_rp"] = score_range(pct_lth, metric_ranges["lth_realized_price"])
|
||||
|
||||
if any(s is None for s in scores.values()):
|
||||
continue
|
||||
|
||||
# Forward returns
|
||||
dt = datetime.strptime(d, "%Y-%m-%d")
|
||||
fwd = {}
|
||||
for days in [30, 90, 180, 365]:
|
||||
future_d = (dt + timedelta(days=days)).strftime("%Y-%m-%d")
|
||||
fp = price_lookup.get(future_d)
|
||||
if fp is not None and price > 0:
|
||||
fwd[f"fwd_{days}d"] = ((fp - price) / price) * 100
|
||||
|
||||
# Compute rate-of-change features (30d deltas)
|
||||
deltas = {}
|
||||
d_30ago = (dt - timedelta(days=30)).strftime("%Y-%m-%d")
|
||||
for key in ["mvrv_zscore", "nupl", "puell_multiple", "reserve_risk"]:
|
||||
v_now = vals[key]
|
||||
v_prev = index.get(key, {}).get(d_30ago)
|
||||
if v_prev is not None and v_prev != 0:
|
||||
deltas[f"delta_30d_{key}"] = v_now - v_prev
|
||||
else:
|
||||
deltas[f"delta_30d_{key}"] = 0.0
|
||||
|
||||
# Interaction terms
|
||||
interactions = {
|
||||
"mvrv_x_nupl": vals["mvrv_zscore"] * vals["nupl"],
|
||||
"puell_x_reserve": vals["puell_multiple"] * vals["reserve_risk"],
|
||||
}
|
||||
|
||||
# Days since last ATH
|
||||
days_since_ath = 0
|
||||
for i in range(1, 2000):
|
||||
check_d = (dt - timedelta(days=i)).strftime("%Y-%m-%d")
|
||||
check_dd = drawdowns.get(check_d, 100)
|
||||
if check_dd < 0.1: # essentially at ATH
|
||||
days_since_ath = i
|
||||
break
|
||||
else:
|
||||
days_since_ath = 2000
|
||||
|
||||
row = {
|
||||
"date": d,
|
||||
"price": price,
|
||||
**{f"score_{k}": v for k, v in scores.items()},
|
||||
**{f"raw_{k}": v for k, v in vals.items()},
|
||||
**deltas,
|
||||
**interactions,
|
||||
"days_since_ath": days_since_ath,
|
||||
**fwd,
|
||||
}
|
||||
rows.append(row)
|
||||
|
||||
log.info("Built %d complete data rows", len(rows))
|
||||
return rows
|
||||
|
||||
|
||||
def train_model(rows):
|
||||
"""Train gradient boosted classifier to identify good buying opportunities."""
|
||||
# Filter to rows that have 365d forward return (for labeling)
|
||||
labeled = [r for r in rows if "fwd_365d" in r]
|
||||
log.info("Rows with 365d forward data: %d", len(labeled))
|
||||
|
||||
if len(labeled) < 100:
|
||||
log.error("Not enough labeled data. Need at least 100 rows, got %d", len(labeled))
|
||||
return None
|
||||
|
||||
# Create binary target: forward 365d return > threshold
|
||||
for r in labeled:
|
||||
r["target"] = 1 if r["fwd_365d"] > GOOD_BUY_THRESHOLD else 0
|
||||
|
||||
positive = sum(r["target"] for r in labeled)
|
||||
log.info("Target distribution: %d positive (%.1f%%), %d negative",
|
||||
positive, positive / len(labeled) * 100, len(labeled) - positive)
|
||||
|
||||
feature_cols = FEATURE_COLS
|
||||
|
||||
X = np.array([[r[f] for f in feature_cols] for r in labeled])
|
||||
y = np.array([r["target"] for r in labeled])
|
||||
|
||||
log.info("Feature matrix: %d samples x %d features", X.shape[0], X.shape[1])
|
||||
|
||||
# Purged time-series cross-validation. Standard TimeSeriesSplit is not
|
||||
# enough here because each label consumes the next 365 days of returns.
|
||||
cv_scores = []
|
||||
cv_f1 = []
|
||||
cv_precision = []
|
||||
cv_recall = []
|
||||
fold_results = []
|
||||
|
||||
splits = list(purged_time_series_splits(
|
||||
labeled,
|
||||
n_splits=VALIDATION_SPLITS,
|
||||
label_horizon_days=LABEL_HORIZON_DAYS,
|
||||
embargo_days=0,
|
||||
))
|
||||
if not splits:
|
||||
log.error("No viable purged validation splits. Need more history for %dd label horizon.",
|
||||
LABEL_HORIZON_DAYS)
|
||||
return None
|
||||
|
||||
for fold, (train_idx, val_idx) in enumerate(splits):
|
||||
X_train, X_val = X[train_idx], X[val_idx]
|
||||
y_train, y_val = y[train_idx], y[val_idx]
|
||||
|
||||
scaler = StandardScaler()
|
||||
X_train_s = scaler.fit_transform(X_train)
|
||||
X_val_s = scaler.transform(X_val)
|
||||
|
||||
model = _build_model()
|
||||
model.fit(X_train_s, y_train)
|
||||
|
||||
y_pred = model.predict(X_val_s)
|
||||
y_prob = model.predict_proba(X_val_s)[:, 1]
|
||||
|
||||
auc = roc_auc_score(y_val, y_prob) if len(np.unique(y_val)) > 1 else 0
|
||||
f1 = f1_score(y_val, y_pred, zero_division=0)
|
||||
prec = precision_score(y_val, y_pred, zero_division=0)
|
||||
rec = recall_score(y_val, y_pred, zero_division=0)
|
||||
|
||||
cv_scores.append(auc)
|
||||
cv_f1.append(f1)
|
||||
cv_precision.append(prec)
|
||||
cv_recall.append(rec)
|
||||
|
||||
fold_weights = derive_metric_weights(feature_cols, model.feature_importances_)
|
||||
fold_results.append({
|
||||
"fold": fold + 1,
|
||||
"train_idx": train_idx.tolist(),
|
||||
"val_idx": val_idx.tolist(),
|
||||
"weights": fold_weights,
|
||||
"metrics": {
|
||||
"auc": round(float(auc), 4),
|
||||
"f1": round(float(f1), 4),
|
||||
"precision": round(float(prec), 4),
|
||||
"recall": round(float(rec), 4),
|
||||
},
|
||||
"date_ranges": {
|
||||
"train": f"{labeled[train_idx[0]]['date']} to {labeled[train_idx[-1]]['date']}",
|
||||
"validation": f"{labeled[val_idx[0]]['date']} to {labeled[val_idx[-1]]['date']}",
|
||||
},
|
||||
"n_train": len(train_idx),
|
||||
"n_validation": len(val_idx),
|
||||
})
|
||||
|
||||
train_dates = fold_results[-1]["date_ranges"]["train"]
|
||||
val_dates = fold_results[-1]["date_ranges"]["validation"]
|
||||
log.info("Fold %d: Train %s | Val %s | AUC=%.3f F1=%.3f P=%.3f R=%.3f",
|
||||
fold + 1, train_dates, val_dates, auc, f1, prec, rec)
|
||||
|
||||
log.info("Purged CV Mean AUC: %.3f (+/- %.3f)", np.mean(cv_scores), np.std(cv_scores))
|
||||
log.info("Purged CV Mean F1: %.3f (+/- %.3f)", np.mean(cv_f1), np.std(cv_f1))
|
||||
|
||||
# Train final model on all labeled data
|
||||
log.info("Training final model on all %d labeled samples...", len(labeled))
|
||||
scaler = StandardScaler()
|
||||
X_scaled = scaler.fit_transform(X)
|
||||
|
||||
final_model = _build_model()
|
||||
final_model.fit(X_scaled, y)
|
||||
|
||||
# Feature importances
|
||||
importances = final_model.feature_importances_
|
||||
feat_imp = sorted(
|
||||
zip(feature_cols, importances),
|
||||
key=lambda x: x[1],
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
log.info("\nFeature Importance Ranking:")
|
||||
log.info("-" * 50)
|
||||
for name, imp in feat_imp:
|
||||
bar = "#" * int(imp * 200)
|
||||
log.info(" %-30s %.4f %s", name, imp, bar)
|
||||
|
||||
weights = derive_metric_weights(feature_cols, importances)
|
||||
|
||||
log.info("\nOptimal Metric Weights:")
|
||||
log.info("-" * 50)
|
||||
equal_weight = round(1 / len(weights), 4)
|
||||
for metric, w in weights.items():
|
||||
change = "+" if w > equal_weight else ""
|
||||
diff = (w - equal_weight) / equal_weight * 100
|
||||
log.info(" %-25s %.4f (%s%.0f%% vs equal)", metric, w, change, diff)
|
||||
|
||||
# Run comparison backtest: ML-weighted vs equal-weight
|
||||
log.info("\n" + "=" * 60)
|
||||
log.info("COMPARISON BACKTEST: ML-Weighted vs Equal-Weight")
|
||||
log.info("=" * 60)
|
||||
comparison = run_comparison(rows, weights)
|
||||
out_of_sample_comparison = run_out_of_sample_comparison(labeled, fold_results)
|
||||
|
||||
# Build output
|
||||
result = {
|
||||
"weights": weights,
|
||||
"feature_importances": {name: round(float(imp), 6) for name, imp in feat_imp},
|
||||
"cv_results": {
|
||||
"mean_auc": round(float(np.mean(cv_scores)), 4),
|
||||
"std_auc": round(float(np.std(cv_scores)), 4),
|
||||
"mean_f1": round(float(np.mean(cv_f1)), 4),
|
||||
"mean_precision": round(float(np.mean(cv_precision)), 4),
|
||||
"mean_recall": round(float(np.mean(cv_recall)), 4),
|
||||
"validation_method": "purged_expanding_window",
|
||||
"label_horizon_days": LABEL_HORIZON_DAYS,
|
||||
"folds": fold_results,
|
||||
},
|
||||
"training_info": {
|
||||
"n_samples": len(labeled),
|
||||
"n_positive": int(positive),
|
||||
"positive_rate": round(positive / len(labeled), 4),
|
||||
"n_features": len(feature_cols),
|
||||
"target_threshold": GOOD_BUY_THRESHOLD,
|
||||
"date_range": f"{labeled[0]['date']} to {labeled[-1]['date']}",
|
||||
"model": "GradientBoostingClassifier",
|
||||
},
|
||||
"comparison": comparison,
|
||||
"out_of_sample_comparison": out_of_sample_comparison,
|
||||
"trained_at": datetime.now(tz=__import__('datetime').timezone.utc).isoformat(),
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _composite_score(row, mode, ml_weights=None):
|
||||
scores = [row[f"score_{k}"] for k in SCORE_KEYS]
|
||||
if mode == "equal_weight" or not ml_weights:
|
||||
return sum(scores) / len(SCORE_KEYS) * 10
|
||||
equal_weight = 1.0 / len(SCORE_KEYS)
|
||||
weighted_sum = sum(row[f"score_{k}"] * ml_weights.get(k, equal_weight) for k in SCORE_KEYS)
|
||||
return weighted_sum * 10
|
||||
|
||||
|
||||
def _summarize_brackets(scored_rows, score_key):
|
||||
results = []
|
||||
for low, high, label in BRACKETS:
|
||||
days_in = [r for r in scored_rows if low <= r[score_key] <= high]
|
||||
if not days_in:
|
||||
results.append({
|
||||
"range": f"{low}-{high}", "label": label,
|
||||
"days": 0, "avg_365d": None,
|
||||
})
|
||||
continue
|
||||
returns_365 = [r["fwd_365d"] for r in days_in]
|
||||
returns_sorted = sorted(returns_365)
|
||||
win_rate = len([r for r in returns_365 if r > 0]) / len(returns_365) * 100
|
||||
results.append({
|
||||
"range": f"{low}-{high}",
|
||||
"label": label,
|
||||
"days": len(days_in),
|
||||
"avg_365d": round(sum(returns_365) / len(returns_365), 2),
|
||||
"median_365d": round(returns_sorted[len(returns_sorted) // 2], 2),
|
||||
"win_rate_365d": round(win_rate, 1),
|
||||
})
|
||||
return results
|
||||
|
||||
|
||||
def _log_comparison_table(results):
|
||||
log.info("\n%-18s | %-8s %-8s %-8s | %-8s %-8s %-8s",
|
||||
"Bracket", "EQ Avg", "EQ Med", "EQ Win%", "ML Avg", "ML Med", "ML Win%")
|
||||
log.info("-" * 80)
|
||||
for eq, ml in zip(results["equal_weight"], results["ml_weighted"]):
|
||||
eq_avg = f"{eq['avg_365d']:.1f}%" if eq["avg_365d"] is not None else "--"
|
||||
eq_med = f"{eq['median_365d']:.1f}%" if eq.get("median_365d") is not None else "--"
|
||||
eq_win = f"{eq['win_rate_365d']:.0f}%" if eq.get("win_rate_365d") is not None else "--"
|
||||
ml_avg = f"{ml['avg_365d']:.1f}%" if ml["avg_365d"] is not None else "--"
|
||||
ml_med = f"{ml['median_365d']:.1f}%" if ml.get("median_365d") is not None else "--"
|
||||
ml_win = f"{ml['win_rate_365d']:.0f}%" if ml.get("win_rate_365d") is not None else "--"
|
||||
log.info("%-18s | %-8s %-8s %-8s | %-8s %-8s %-8s",
|
||||
eq["label"], eq_avg, eq_med, eq_win, ml_avg, ml_med, ml_win)
|
||||
|
||||
|
||||
def run_comparison(rows, ml_weights):
|
||||
"""Compare final ML-weighted scoring vs equal-weight scoring across all labeled rows.
|
||||
|
||||
This is retained for backwards compatibility with existing output. It is an
|
||||
in-sample/full-history comparison; prefer out_of_sample_comparison for model
|
||||
selection decisions.
|
||||
"""
|
||||
scored_rows = [dict(r) for r in rows if "fwd_365d" in r]
|
||||
for r in scored_rows:
|
||||
r["composite_equal_weight"] = _composite_score(r, "equal_weight")
|
||||
r["composite_ml_weighted"] = _composite_score(r, "ml_weighted", ml_weights)
|
||||
|
||||
results = {
|
||||
"equal_weight": _summarize_brackets(scored_rows, "composite_equal_weight"),
|
||||
"ml_weighted": _summarize_brackets(scored_rows, "composite_ml_weighted"),
|
||||
}
|
||||
_log_comparison_table(results)
|
||||
return results
|
||||
|
||||
|
||||
def run_out_of_sample_comparison(rows, fold_results):
|
||||
"""Compare fold-specific ML weights on validation rows only."""
|
||||
validation_rows = []
|
||||
for fold in fold_results:
|
||||
weights = fold.get("weights", {})
|
||||
for idx in fold.get("val_idx", []):
|
||||
if idx >= len(rows) or "fwd_365d" not in rows[idx]:
|
||||
continue
|
||||
r = dict(rows[idx])
|
||||
r["fold"] = fold.get("fold")
|
||||
r["composite_equal_weight"] = _composite_score(r, "equal_weight")
|
||||
r["composite_ml_weighted"] = _composite_score(r, "ml_weighted", weights)
|
||||
validation_rows.append(r)
|
||||
|
||||
results = {
|
||||
"folds": len(fold_results),
|
||||
"validation_days": len(validation_rows),
|
||||
"equal_weight": _summarize_brackets(validation_rows, "composite_equal_weight"),
|
||||
"ml_weighted": _summarize_brackets(validation_rows, "composite_ml_weighted"),
|
||||
}
|
||||
return results
|
||||
|
||||
|
||||
def main():
|
||||
log.info("=" * 60)
|
||||
log.info("Bitcoin Accumulation Zone ML Optimizer")
|
||||
log.info("=" * 60)
|
||||
|
||||
if not os.path.exists(HISTORY_PATH):
|
||||
log.error("No historical data at %s. Run history collector first.", HISTORY_PATH)
|
||||
sys.exit(1)
|
||||
|
||||
# Load data
|
||||
log.info("Loading historical data...")
|
||||
index = load_history()
|
||||
thresholds = load_thresholds()
|
||||
|
||||
# Build dataset
|
||||
log.info("Building training dataset...")
|
||||
rows = build_dataset(index, thresholds)
|
||||
|
||||
# Train model
|
||||
log.info("Training ML model...")
|
||||
result = train_model(rows)
|
||||
|
||||
if result is None:
|
||||
log.error("Training failed.")
|
||||
sys.exit(1)
|
||||
|
||||
# Save weights
|
||||
with open(OUTPUT_PATH, "w") as f:
|
||||
json.dump(result, f, indent=2)
|
||||
log.info("\nSaved ML weights to %s", OUTPUT_PATH)
|
||||
|
||||
# Print summary
|
||||
log.info("\n" + "=" * 60)
|
||||
log.info("SUMMARY")
|
||||
log.info("=" * 60)
|
||||
log.info("Model: %s", result["training_info"]["model"])
|
||||
log.info("Samples: %d (%d positive)", result["training_info"]["n_samples"], result["training_info"]["n_positive"])
|
||||
log.info("CV AUC: %.3f (+/- %.3f)", result["cv_results"]["mean_auc"], result["cv_results"]["std_auc"])
|
||||
log.info("CV F1: %.3f", result["cv_results"]["mean_f1"])
|
||||
log.info("\nTop 5 Feature Importances:")
|
||||
for name, imp in list(result["feature_importances"].items())[:5]:
|
||||
log.info(" %-30s %.4f", name, imp)
|
||||
log.info("\nMetric Weights (ML-Optimized):")
|
||||
for metric, weight in result["weights"].items():
|
||||
log.info(" %-25s %.1f%%", metric, weight * 100)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+277
-23
@@ -75,7 +75,8 @@ def score_puell_multiple(value, thresholds=None):
|
||||
if value is None:
|
||||
return None, "No data"
|
||||
t = (thresholds or load_thresholds()).get("puell_multiple", {})
|
||||
ranges = t.get("ranges", [[None, 0.3, 10], [0.3, 0.5, 8], [0.5, 0.8, 5], [0.8, 1.2, 3], [1.2, 2.0, 1], [2.0, None, 0]])
|
||||
# Widened: post-halving Puell floors are rising (2016: 0.15, 2020: 0.3, 2024: 0.5+)
|
||||
ranges = t.get("ranges", [[None, 0.4, 10], [0.4, 0.7, 8], [0.7, 1.0, 5], [1.0, 1.5, 3], [1.5, 2.0, 1], [2.0, None, 0]])
|
||||
score = _score_range(value, ranges)
|
||||
|
||||
if value < 0.3:
|
||||
@@ -97,15 +98,17 @@ def score_mvrv_zscore(value, thresholds=None):
|
||||
if value is None:
|
||||
return None, "No data"
|
||||
t = (thresholds or load_thresholds()).get("mvrv_zscore", {})
|
||||
ranges = t.get("ranges", [[None, 0, 10], [0, 0.5, 8], [0.5, 1.5, 5], [1.5, 3, 2], [3, 5, 1], [5, None, 0]])
|
||||
# Widened ranges: BTC cycles compress — Z-Score bottoms are getting shallower
|
||||
# 2015 bottom: -0.6, 2018 bottom: -0.4, 2022 bottom: -0.3, next may be ~0
|
||||
ranges = t.get("ranges", [[None, 0, 10], [0, 1.0, 8], [1.0, 2.0, 5], [2.0, 3, 3], [3, 5, 1], [5, None, 0]])
|
||||
score = _score_range(value, ranges)
|
||||
|
||||
if value < 0:
|
||||
desc = "Below realized value — historically perfect buy zone"
|
||||
elif value < 0.5:
|
||||
desc = "Near realized value — strong accumulation"
|
||||
elif value < 1.5:
|
||||
desc = "Fair value range"
|
||||
elif value < 1.0:
|
||||
desc = "Near realized value — strong accumulation zone"
|
||||
elif value < 2.0:
|
||||
desc = "Fair value — decent entry territory"
|
||||
elif value < 3:
|
||||
desc = "Above fair value"
|
||||
elif value < 5:
|
||||
@@ -144,15 +147,16 @@ def score_price_vs_200w_sma(price, sma_200w, thresholds=None):
|
||||
return None, "No data"
|
||||
pct_above = ((price - sma_200w) / sma_200w) * 100
|
||||
t = (thresholds or load_thresholds()).get("price_vs_200w_sma", {})
|
||||
ranges = t.get("ranges", [[None, 0, 10], [0, 20, 6], [20, 50, 3], [50, 100, 1], [100, None, 0]])
|
||||
# Widened: BTC increasingly stays above 200W SMA as it matures
|
||||
ranges = t.get("ranges", [[None, 0, 10], [0, 30, 7], [30, 60, 5], [60, 100, 2], [100, None, 0]])
|
||||
score = _score_range(pct_above, ranges)
|
||||
|
||||
if pct_above < 0:
|
||||
desc = f"Below 200W SMA — historically rare buy zone"
|
||||
elif pct_above < 20:
|
||||
desc = f"{pct_above:.0f}% above 200W SMA — good value"
|
||||
elif pct_above < 50:
|
||||
desc = f"{pct_above:.0f}% above 200W SMA — moderate"
|
||||
elif pct_above < 30:
|
||||
desc = f"{pct_above:.0f}% above 200W SMA — strong value"
|
||||
elif pct_above < 60:
|
||||
desc = f"{pct_above:.0f}% above 200W SMA — fair value"
|
||||
elif pct_above < 100:
|
||||
desc = f"{pct_above:.0f}% above 200W SMA — extended"
|
||||
else:
|
||||
@@ -204,13 +208,15 @@ def score_nupl(value, thresholds=None):
|
||||
if value is None:
|
||||
return None, "No data"
|
||||
t = (thresholds or load_thresholds()).get("nupl", {})
|
||||
ranges = t.get("ranges", [[None, 0, 10], [0, 0.25, 7], [0.25, 0.5, 4], [0.5, 0.75, 1], [0.75, None, 0]])
|
||||
# Widened: NUPL bottoms getting shallower as BTC matures
|
||||
# 2015: -0.3, 2018: -0.28, 2022: -0.28, future may only dip to 0-0.1
|
||||
ranges = t.get("ranges", [[None, 0, 10], [0, 0.3, 8], [0.3, 0.5, 4], [0.5, 0.75, 1], [0.75, None, 0]])
|
||||
score = _score_range(value, ranges)
|
||||
|
||||
if value < 0:
|
||||
desc = "Capitulation — holders underwater"
|
||||
elif value < 0.25:
|
||||
desc = "Hope/Fear — early recovery"
|
||||
elif value < 0.3:
|
||||
desc = "Hope/Fear — early recovery, good accumulation"
|
||||
elif value < 0.5:
|
||||
desc = "Optimism — moderate profit taking"
|
||||
elif value < 0.75:
|
||||
@@ -226,14 +232,18 @@ def score_lth_realized_price(price, lth_rp, thresholds=None):
|
||||
return None, "No data"
|
||||
pct_above = ((price - lth_rp) / lth_rp) * 100
|
||||
t = (thresholds or load_thresholds()).get("lth_realized_price", {})
|
||||
ranges = t.get("ranges", [[None, 0, 10], [0, 20, 6], [20, 50, 3], [50, None, 1]])
|
||||
# Widened: as BTC matures, price spends more time above LTH RP
|
||||
# In 2024+, even "good" entries are 30-80% above LTH RP
|
||||
ranges = t.get("ranges", [[None, 0, 10], [0, 30, 7], [30, 80, 5], [80, 150, 3], [150, None, 1]])
|
||||
score = _score_range(pct_above, ranges)
|
||||
|
||||
if pct_above < 0:
|
||||
desc = f"Below LTH cost basis — LTHs underwater (extreme value)"
|
||||
elif pct_above < 20:
|
||||
desc = f"{pct_above:.0f}% above LTH cost basis — good value"
|
||||
elif pct_above < 50:
|
||||
elif pct_above < 30:
|
||||
desc = f"{pct_above:.0f}% above LTH cost basis — strong value"
|
||||
elif pct_above < 80:
|
||||
desc = f"{pct_above:.0f}% above LTH cost basis — fair value"
|
||||
elif pct_above < 150:
|
||||
desc = f"{pct_above:.0f}% above LTH cost basis — moderate"
|
||||
else:
|
||||
desc = f"{pct_above:.0f}% above LTH cost basis — extended"
|
||||
@@ -249,6 +259,65 @@ def score_hash_ribbons(data, thresholds=None):
|
||||
return 3, "Normal mining activity"
|
||||
|
||||
|
||||
def score_sopr(value, thresholds=None):
|
||||
if value is None:
|
||||
return None, "No data"
|
||||
if value < 0.98:
|
||||
return 10, "Deep loss realization — capitulation, strong accumulation"
|
||||
if value < 1.0:
|
||||
return 8, "Below breakeven — capitulation, good accumulation"
|
||||
if value < 1.02:
|
||||
return 5, "Near breakeven — neutral"
|
||||
if value < 1.05:
|
||||
return 2, "Moderate profit taking"
|
||||
return 0, "Elevated profit taking — caution"
|
||||
|
||||
|
||||
def score_sellside_risk(value, thresholds=None):
|
||||
if value is None:
|
||||
return None, "No data"
|
||||
if value < 0.001:
|
||||
return 10, "Very low sell-side risk — strong accumulation"
|
||||
if value < 0.002:
|
||||
return 8, "Low sell-side risk — good accumulation"
|
||||
if value < 0.005:
|
||||
return 5, "Moderate sell-side risk"
|
||||
if value < 0.01:
|
||||
return 2, "Elevated sell-side risk"
|
||||
return 0, "High sell-side risk"
|
||||
|
||||
|
||||
def score_momentum_pct(value):
|
||||
if value is None:
|
||||
return None, "No data"
|
||||
pct = value * 100
|
||||
if pct >= 20:
|
||||
return 10, f"Strong positive momentum (+{pct:.0f}%)"
|
||||
if pct >= 0:
|
||||
return 6, f"Mild positive momentum (+{pct:.0f}%)"
|
||||
if pct >= -10:
|
||||
return 4, f"Slightly negative momentum ({pct:.0f}%)"
|
||||
if pct >= -25:
|
||||
return 2, f"Weak momentum ({pct:.0f}%)"
|
||||
return 1, f"Strong negative momentum ({pct:.0f}%)"
|
||||
|
||||
|
||||
def score_nvt_price(nvt_price, spot_price):
|
||||
if nvt_price is None or spot_price is None or spot_price <= 0:
|
||||
return None, "No data"
|
||||
premium = (nvt_price - spot_price) / spot_price * 100
|
||||
if premium < -25:
|
||||
return 10, f"NVT price {abs(premium):.0f}% below spot — deep value"
|
||||
if premium < -10:
|
||||
return 8, f"NVT price {abs(premium):.0f}% below spot — undervalued"
|
||||
if premium < 10:
|
||||
relation = "below" if premium < 0 else "above"
|
||||
return 5, f"NVT price {abs(premium):.0f}% {relation} spot — fair value"
|
||||
if premium < 30:
|
||||
return 2, f"NVT price {premium:.0f}% above spot — extended"
|
||||
return 0, f"NVT price {premium:.0f}% above spot — overheated"
|
||||
|
||||
|
||||
def score_all(metrics):
|
||||
"""Score all metrics and return individual + composite scores."""
|
||||
thresholds = load_thresholds()
|
||||
@@ -389,6 +458,84 @@ def score_all(metrics):
|
||||
"recent": [],
|
||||
})
|
||||
|
||||
# SOPR
|
||||
sopr = metrics.get("sopr", {})
|
||||
sopr_score, sopr_desc = score_sopr(sopr.get("value"), thresholds)
|
||||
results.append({
|
||||
"name": "SOPR",
|
||||
"key": "sopr",
|
||||
"value": sopr.get("value"),
|
||||
"display_value": f"{sopr.get('value', 'N/A'):.4f}" if sopr.get("value") is not None else "N/A",
|
||||
"score": sopr_score,
|
||||
"description": sopr_desc,
|
||||
"recent": sopr.get("recent", []),
|
||||
})
|
||||
|
||||
# Sell-side Risk Ratio
|
||||
ssr = metrics.get("sellside_risk", {})
|
||||
ssr_score, ssr_desc = score_sellside_risk(ssr.get("value"), thresholds)
|
||||
results.append({
|
||||
"name": "Sell-side Risk Ratio",
|
||||
"key": "sellside_risk",
|
||||
"value": ssr.get("value"),
|
||||
"display_value": f"{ssr.get('value', 'N/A'):.6f}" if ssr.get("value") is not None else "N/A",
|
||||
"score": ssr_score,
|
||||
"description": ssr_desc,
|
||||
"recent": ssr.get("recent", []),
|
||||
})
|
||||
|
||||
# Active Address Momentum
|
||||
aam = metrics.get("active_address_momentum", {})
|
||||
aam_score, aam_desc = score_momentum_pct(aam.get("value"))
|
||||
results.append({
|
||||
"name": "Active Address Momentum",
|
||||
"key": "active_address_momentum",
|
||||
"value": aam.get("value"),
|
||||
"display_value": f"{aam.get('value') * 100:.1f}%" if aam.get("value") is not None else "N/A",
|
||||
"score": aam_score,
|
||||
"description": aam_desc,
|
||||
"recent": aam.get("recent", []),
|
||||
})
|
||||
|
||||
# Transaction Count Momentum
|
||||
txm = metrics.get("txcount_momentum", {})
|
||||
txm_score, txm_desc = score_momentum_pct(txm.get("value"))
|
||||
results.append({
|
||||
"name": "Transaction Count Momentum",
|
||||
"key": "txcount_momentum",
|
||||
"value": txm.get("value"),
|
||||
"display_value": f"{txm.get('value') * 100:.1f}%" if txm.get("value") is not None else "N/A",
|
||||
"score": txm_score,
|
||||
"description": txm_desc,
|
||||
"recent": txm.get("recent", []),
|
||||
})
|
||||
|
||||
# NVT Price
|
||||
nvt = metrics.get("nvt_price", {})
|
||||
nvt_score, nvt_desc = score_nvt_price(nvt.get("value"), current_price)
|
||||
results.append({
|
||||
"name": "NVT Price",
|
||||
"key": "nvt_price",
|
||||
"value": nvt.get("value"),
|
||||
"display_value": f"${nvt.get('value'):,.0f}" if nvt.get("value") is not None else "N/A",
|
||||
"score": nvt_score,
|
||||
"description": nvt_desc,
|
||||
"recent": nvt.get("recent", []),
|
||||
})
|
||||
|
||||
# VDD Multiple
|
||||
vdd = metrics.get("vdd_multiple", {})
|
||||
vdd_score, vdd_desc = score_momentum_pct(vdd.get("value"))
|
||||
results.append({
|
||||
"name": "VDD Multiple",
|
||||
"key": "vdd_multiple",
|
||||
"value": vdd.get("value"),
|
||||
"display_value": f"{vdd.get('value') * 100:.1f}%" if vdd.get("value") is not None else "N/A",
|
||||
"score": vdd_score,
|
||||
"description": vdd_desc,
|
||||
"recent": vdd.get("recent", []),
|
||||
})
|
||||
|
||||
# Compute composite
|
||||
valid_scores = [r["score"] for r in results if r["score"] is not None]
|
||||
if valid_scores:
|
||||
@@ -397,14 +544,16 @@ def score_all(metrics):
|
||||
else:
|
||||
composite = 0
|
||||
|
||||
# Assessment text
|
||||
if composite >= 71:
|
||||
# Assessment text — calibrated for cycle-aware scoring
|
||||
if composite >= 80:
|
||||
assessment = "EXTREME ACCUMULATION ZONE"
|
||||
elif composite >= 65:
|
||||
assessment = "STRONG ACCUMULATION ZONE"
|
||||
elif composite >= 51:
|
||||
elif composite >= 50:
|
||||
assessment = "MODERATE OPPORTUNITY"
|
||||
elif composite >= 31:
|
||||
elif composite >= 35:
|
||||
assessment = "NEUTRAL"
|
||||
elif composite >= 15:
|
||||
elif composite >= 20:
|
||||
assessment = "CAUTION — OVERHEATED"
|
||||
else:
|
||||
assessment = "EXTREME CAUTION"
|
||||
@@ -416,3 +565,108 @@ def score_all(metrics):
|
||||
"scored_count": len(valid_scores),
|
||||
"total_count": len(results),
|
||||
}
|
||||
|
||||
|
||||
# ── ML-Optimized Scoring ──────────────────────────────────────────────
|
||||
|
||||
ML_WEIGHTS_PATH = os.path.join(
|
||||
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
||||
"config",
|
||||
"ml_weights.json",
|
||||
)
|
||||
|
||||
# Maps scoring engine metric keys to ML weight keys
|
||||
_ML_KEY_MAP = {
|
||||
"fear_greed": "fear_greed",
|
||||
"puell_multiple": "puell_multiple",
|
||||
"mvrv_zscore": "mvrv_zscore",
|
||||
"drawdown": "drawdown",
|
||||
"price_vs_200w_sma": "pct_above_200w_sma",
|
||||
"reserve_risk": "reserve_risk",
|
||||
"rhodl_ratio": "rhodl_ratio",
|
||||
"nupl": "nupl",
|
||||
"lth_realized_price": "pct_above_lth_rp",
|
||||
}
|
||||
|
||||
|
||||
def load_ml_weights():
|
||||
"""Load ML-optimized weights from config."""
|
||||
try:
|
||||
with open(ML_WEIGHTS_PATH) as f:
|
||||
data = json.load(f)
|
||||
return data.get("weights", {})
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def score_all_ml(metrics):
|
||||
"""Score all metrics using ML-optimized weights.
|
||||
|
||||
Same output format as score_all() but uses learned weights
|
||||
instead of equal weighting. Each metric still shows its
|
||||
individual 0-10 score plus the ML weight applied to it.
|
||||
"""
|
||||
# Get classic scores first (reuses all individual scoring logic)
|
||||
classic = score_all(metrics)
|
||||
ml_weights = load_ml_weights()
|
||||
|
||||
if not ml_weights:
|
||||
# Fallback to classic if no ML weights available
|
||||
classic["ml_mode"] = False
|
||||
classic["ml_error"] = "ML weights not found — run ml/optimizer.py"
|
||||
return classic
|
||||
|
||||
results = classic["metrics"]
|
||||
|
||||
# Compute raw ML weights first, then normalize across only the currently
|
||||
# scored metrics. This keeps the dashboard's displayed per-metric weights and
|
||||
# contribution points consistent with the normalized composite score even
|
||||
# when optional metrics are missing or hash ribbons receives its fallback.
|
||||
weighted_metrics = []
|
||||
for m in results:
|
||||
if m["score"] is None:
|
||||
continue
|
||||
ml_key = _ML_KEY_MAP.get(m["key"])
|
||||
if ml_key is None:
|
||||
# Hash ribbons or unknown metric — use small default weight
|
||||
raw_weight = 0.01
|
||||
else:
|
||||
raw_weight = ml_weights.get(ml_key, 0.0)
|
||||
weighted_metrics.append((m, raw_weight))
|
||||
|
||||
weight_total = sum(raw_weight for _, raw_weight in weighted_metrics)
|
||||
if weight_total > 0:
|
||||
composite = sum(m["score"] * raw_weight for m, raw_weight in weighted_metrics) / weight_total * 10
|
||||
else:
|
||||
composite = 0
|
||||
|
||||
for m, raw_weight in weighted_metrics:
|
||||
effective_weight = raw_weight / weight_total if weight_total > 0 else 0.0
|
||||
m["ml_raw_weight"] = round(raw_weight, 4)
|
||||
m["ml_weight"] = round(effective_weight, 4)
|
||||
m["ml_contribution"] = round(m["score"] * effective_weight * 10, 2)
|
||||
|
||||
# Assessment text (same thresholds as classic)
|
||||
if composite >= 80:
|
||||
assessment = "EXTREME ACCUMULATION ZONE"
|
||||
elif composite >= 65:
|
||||
assessment = "STRONG ACCUMULATION ZONE"
|
||||
elif composite >= 50:
|
||||
assessment = "MODERATE OPPORTUNITY"
|
||||
elif composite >= 35:
|
||||
assessment = "NEUTRAL"
|
||||
elif composite >= 20:
|
||||
assessment = "CAUTION — OVERHEATED"
|
||||
else:
|
||||
assessment = "EXTREME CAUTION"
|
||||
|
||||
return {
|
||||
"metrics": results,
|
||||
"composite_score": round(composite, 1),
|
||||
"assessment": assessment,
|
||||
"scored_count": classic["scored_count"],
|
||||
"total_count": classic["total_count"],
|
||||
"ml_mode": True,
|
||||
"classic_score": classic["composite_score"],
|
||||
"ml_weight_total": round(weight_total, 4),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
"""Scraper for static CheckOnChain Plotly chart HTML pages."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import array
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from html import unescape
|
||||
|
||||
import requests
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
CHARTS = {
|
||||
"sopr": {
|
||||
"url": "https://charts.checkonchain.com/btconchain/realised/sopr/sopr_light.html",
|
||||
"traces": ["SOPR"],
|
||||
},
|
||||
"sellside_risk": {
|
||||
"url": "https://charts.checkonchain.com/btconchain/realised/sellsideriskratio_all/sellsideriskratio_all_light.html",
|
||||
"traces": ["Sell-side Risk Ratio", "Sellside Risk Ratio", "SSR"],
|
||||
},
|
||||
"active_address_momentum": {
|
||||
"url": "https://charts.checkonchain.com/btconchain/adoption/actaddress_momentum/actaddress_momentum_light.html",
|
||||
"traces": ["30DMA", "30 Day", "Active Address"],
|
||||
},
|
||||
"txcount_momentum": {
|
||||
"url": "https://charts.checkonchain.com/btconchain/adoption/txcount_momentum/txcount_momentum_light.html",
|
||||
"traces": ["30DMA", "30 Day", "Transaction"],
|
||||
},
|
||||
"nvt_price": {
|
||||
"url": "https://charts.checkonchain.com/btconchain/pricing/pricing_nvtprice/pricing_nvtprice_light.html",
|
||||
"traces": ["NVT Price", "NVT"],
|
||||
},
|
||||
"vdd_multiple": {
|
||||
"url": "https://charts.checkonchain.com/btconchain/lifespan/vddmultiple/vddmultiple_light.html",
|
||||
"traces": ["VDD Multiple", "Value Days Destroyed"],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _extract_plotly_traces(html_text: str):
|
||||
"""Extract first Plotly.newPlot trace array from a static Plotly HTML page."""
|
||||
marker = "Plotly.newPlot("
|
||||
start = html_text.find(marker)
|
||||
if start < 0:
|
||||
return []
|
||||
first_array = html_text.find("[", start)
|
||||
if first_array < 0:
|
||||
return []
|
||||
|
||||
depth = 0
|
||||
in_string = False
|
||||
escape = False
|
||||
quote = ""
|
||||
for idx in range(first_array, len(html_text)):
|
||||
ch = html_text[idx]
|
||||
if in_string:
|
||||
if escape:
|
||||
escape = False
|
||||
elif ch == "\\":
|
||||
escape = True
|
||||
elif ch == quote:
|
||||
in_string = False
|
||||
continue
|
||||
if ch in {'"', "'"}:
|
||||
in_string = True
|
||||
quote = ch
|
||||
elif ch == "[":
|
||||
depth += 1
|
||||
elif ch == "]":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
raw = html_text[first_array:idx + 1]
|
||||
return json.loads(raw)
|
||||
return []
|
||||
|
||||
|
||||
def scrape_chart(url: str, timeout=30):
|
||||
resp = requests.get(url, headers={"User-Agent": "Mozilla/5.0"}, timeout=timeout)
|
||||
resp.raise_for_status()
|
||||
return _extract_plotly_traces(unescape(resp.text))
|
||||
|
||||
|
||||
def _find_trace(traces, names):
|
||||
names = [n.lower() for n in names if n]
|
||||
# Prefer non-price traces with the requested terms.
|
||||
for trace in traces:
|
||||
trace_name = str(trace.get("name", "")).lower()
|
||||
if "price" in trace_name and not any("price" in n for n in names):
|
||||
continue
|
||||
if any(n in trace_name for n in names):
|
||||
return trace
|
||||
# Fallback: first numeric non-price trace.
|
||||
for trace in traces:
|
||||
trace_name = str(trace.get("name", "")).lower()
|
||||
if "price" in trace_name:
|
||||
continue
|
||||
y = trace.get("y") or []
|
||||
if any(v is not None for v in y[-30:]):
|
||||
return trace
|
||||
return None
|
||||
|
||||
|
||||
def _decode_plotly_array(values):
|
||||
"""Decode Plotly typed-array JSON ({dtype, bdata}) or return plain values."""
|
||||
if not isinstance(values, dict) or "bdata" not in values:
|
||||
return values or []
|
||||
|
||||
dtype = values.get("dtype")
|
||||
typecodes = {
|
||||
"f8": "d", "float64": "d",
|
||||
"f4": "f", "float32": "f",
|
||||
"i8": "q", "int64": "q",
|
||||
"i4": "i", "int32": "i",
|
||||
"u8": "Q", "uint64": "Q",
|
||||
"u4": "I", "uint32": "I",
|
||||
}
|
||||
typecode = typecodes.get(dtype)
|
||||
if not typecode:
|
||||
return []
|
||||
decoded = base64.b64decode(values["bdata"])
|
||||
arr = array.array(typecode)
|
||||
arr.frombytes(decoded)
|
||||
if values.get("byteorder") == "big":
|
||||
arr.byteswap()
|
||||
return arr.tolist()
|
||||
|
||||
|
||||
def _numeric_values(trace):
|
||||
values = []
|
||||
for value in _decode_plotly_array((trace or {}).get("y", [])):
|
||||
if value is None:
|
||||
continue
|
||||
try:
|
||||
values.append(float(value))
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
return values
|
||||
|
||||
|
||||
def _latest(values):
|
||||
return values[-1] if values else None
|
||||
|
||||
|
||||
def _momentum(values, window=30):
|
||||
if len(values) <= window or values[-window] == 0:
|
||||
return None
|
||||
return (values[-1] - values[-window]) / values[-window]
|
||||
|
||||
|
||||
def scrape_all():
|
||||
results = {}
|
||||
for key, cfg in CHARTS.items():
|
||||
log.info("Scraping CheckOnChain %s ...", key)
|
||||
try:
|
||||
traces = scrape_chart(cfg["url"])
|
||||
trace = _find_trace(traces, cfg.get("traces", []))
|
||||
values = _numeric_values(trace)
|
||||
value = _latest(values)
|
||||
if key in {"active_address_momentum", "txcount_momentum", "vdd_multiple"}:
|
||||
# The card value is momentum, while the sparkline shows the raw metric.
|
||||
value = _momentum(values)
|
||||
results[key] = {"value": value, "recent": values[-30:]}
|
||||
except Exception as exc:
|
||||
log.error("CheckOnChain scrape failed for %s: %s", key, exc)
|
||||
results[key] = {"value": None, "error": str(exc)}
|
||||
return results
|
||||
@@ -0,0 +1,112 @@
|
||||
"""Incremental history updater — appends new daily data to history.json from cache."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "data")
|
||||
HISTORY_PATH = os.path.join(DATA_DIR, "history.json")
|
||||
CACHE_PATH = os.path.join(DATA_DIR, "cache.json")
|
||||
|
||||
|
||||
def update_history():
|
||||
"""Append today's values from cache to history.json. Only adds NEW dates."""
|
||||
if not os.path.exists(HISTORY_PATH):
|
||||
log.warning("No history.json found — run full collection first")
|
||||
return False
|
||||
|
||||
if not os.path.exists(CACHE_PATH):
|
||||
log.warning("No cache.json found — run a scrape first")
|
||||
return False
|
||||
|
||||
with open(HISTORY_PATH) as f:
|
||||
history = json.load(f)
|
||||
with open(CACHE_PATH) as f:
|
||||
cache = json.load(f)
|
||||
|
||||
today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
||||
updated = False
|
||||
|
||||
# Map of cache keys to history keys and how to extract the value
|
||||
mappings = {
|
||||
"puell_multiple": {"history_key": "puell_multiple", "value_key": "value"},
|
||||
"mvrv_zscore": {"history_key": "mvrv_zscore", "value_key": "value"},
|
||||
"reserve_risk": {"history_key": "reserve_risk", "value_key": "value"},
|
||||
"rhodl_ratio": {"history_key": "rhodl_ratio", "value_key": "value"},
|
||||
"nupl": {"history_key": "nupl", "value_key": "value"},
|
||||
"200w_sma": {"history_key": "200w_sma", "value_key": "value"},
|
||||
"lth_realized_price": {"history_key": "lth_realized_price", "value_key": "value"},
|
||||
"lth_supply": {"history_key": "lth_supply", "value_key": "value"},
|
||||
}
|
||||
|
||||
for cache_key, mapping in mappings.items():
|
||||
hkey = mapping["history_key"]
|
||||
if hkey not in history:
|
||||
continue
|
||||
|
||||
h = history[hkey]
|
||||
dates = h.get("dates", [])
|
||||
values = h.get("values", [])
|
||||
|
||||
# Skip if today already in history
|
||||
if dates and dates[-1] >= today:
|
||||
continue
|
||||
|
||||
# Get value from cache
|
||||
cached = cache.get(cache_key, {})
|
||||
val = cached.get(mapping["value_key"])
|
||||
if val is not None:
|
||||
dates.append(today)
|
||||
values.append(val)
|
||||
h["dates"] = dates
|
||||
h["values"] = values
|
||||
updated = True
|
||||
log.info("Appended %s: %s = %s", hkey, today, val)
|
||||
|
||||
# Also update btc_price from cache
|
||||
price_data = cache.get("price", {})
|
||||
btc_price = price_data.get("price")
|
||||
if btc_price and "btc_price" in history:
|
||||
h = history["btc_price"]
|
||||
if h["dates"][-1] < today:
|
||||
h["dates"].append(today)
|
||||
h["values"].append(btc_price)
|
||||
updated = True
|
||||
|
||||
# BTC price for SMA chart
|
||||
if btc_price and "btc_price_sma" in history:
|
||||
h = history["btc_price_sma"]
|
||||
if h["dates"][-1] < today:
|
||||
h["dates"].append(today)
|
||||
h["values"].append(btc_price)
|
||||
updated = True
|
||||
|
||||
# BTC price for LTH chart
|
||||
if btc_price and "btc_price_lth" in history:
|
||||
h = history["btc_price_lth"]
|
||||
if h["dates"][-1] < today:
|
||||
h["dates"].append(today)
|
||||
h["values"].append(btc_price)
|
||||
updated = True
|
||||
|
||||
# Fear & Greed
|
||||
fg = cache.get("fear_greed", {})
|
||||
fg_val = fg.get("value")
|
||||
if fg_val is not None and "fear_greed" in history:
|
||||
h = history["fear_greed"]
|
||||
if h["dates"][-1] < today:
|
||||
h["dates"].append(today)
|
||||
h["values"].append(int(fg_val))
|
||||
updated = True
|
||||
|
||||
if updated:
|
||||
with open(HISTORY_PATH, "w") as f:
|
||||
json.dump(history, f)
|
||||
log.info("History updated with %s data", today)
|
||||
else:
|
||||
log.info("History already up to date (last date >= %s)", today)
|
||||
|
||||
return updated
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 492 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 298 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
@@ -0,0 +1,74 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from ml import optimizer
|
||||
|
||||
|
||||
def _row(date, returns=10.0, score_200w=10, score_drawdown=0):
|
||||
row = {
|
||||
"date": date,
|
||||
"price": 100.0,
|
||||
"fwd_365d": returns,
|
||||
"score_puell_multiple": 0,
|
||||
"score_mvrv_zscore": 0,
|
||||
"score_reserve_risk": 0,
|
||||
"score_rhodl_ratio": 0,
|
||||
"score_nupl": 0,
|
||||
"score_fear_greed": 0,
|
||||
"score_drawdown": score_drawdown,
|
||||
"score_pct_above_200w_sma": score_200w,
|
||||
"score_pct_above_lth_rp": 0,
|
||||
}
|
||||
return row
|
||||
|
||||
|
||||
def test_purged_time_series_splits_remove_overlapping_forward_label_windows():
|
||||
start = datetime(2020, 1, 1)
|
||||
rows = [_row((start + timedelta(days=i)).strftime("%Y-%m-%d")) for i in range(900)]
|
||||
|
||||
splits = list(
|
||||
optimizer.purged_time_series_splits(
|
||||
rows,
|
||||
n_splits=3,
|
||||
label_horizon_days=365,
|
||||
embargo_days=0,
|
||||
)
|
||||
)
|
||||
|
||||
assert splits, "expected at least one viable split"
|
||||
for train_idx, val_idx in splits:
|
||||
val_start = datetime.strptime(rows[val_idx[0]]["date"], "%Y-%m-%d")
|
||||
latest_allowed_train_date = val_start - timedelta(days=365)
|
||||
assert len(train_idx) > 0, "purging should keep non-overlapping expanding-window training rows"
|
||||
for idx in train_idx:
|
||||
train_date = datetime.strptime(rows[idx]["date"], "%Y-%m-%d")
|
||||
assert train_date <= latest_allowed_train_date
|
||||
|
||||
|
||||
def test_run_out_of_sample_comparison_scores_only_validation_rows_with_fold_weights():
|
||||
rows = [
|
||||
_row("2020-01-01", returns=-10, score_200w=0, score_drawdown=10),
|
||||
_row("2020-01-02", returns=-5, score_200w=0, score_drawdown=10),
|
||||
_row("2020-01-03", returns=100, score_200w=10, score_drawdown=10),
|
||||
_row("2020-01-04", returns=120, score_200w=10, score_drawdown=10),
|
||||
]
|
||||
fold_results = [
|
||||
{
|
||||
"fold": 1,
|
||||
"val_idx": [2, 3],
|
||||
"weights": {"pct_above_200w_sma": 1.0, "drawdown": 0.0},
|
||||
}
|
||||
]
|
||||
|
||||
comparison = optimizer.run_out_of_sample_comparison(rows, fold_results)
|
||||
|
||||
assert comparison["validation_days"] == 2
|
||||
assert comparison["folds"] == 1
|
||||
assert sum(bucket["days"] for bucket in comparison["ml_weighted"]) == 2
|
||||
assert sum(bucket["days"] for bucket in comparison["equal_weight"]) == 2
|
||||
|
||||
extreme_ml = next(bucket for bucket in comparison["ml_weighted"] if bucket["label"] == "Extreme Accumulation")
|
||||
assert extreme_ml["days"] == 2
|
||||
assert extreme_ml["avg_365d"] == 110.0
|
||||
|
||||
caution_equal = next(bucket for bucket in comparison["equal_weight"] if bucket["label"] == "Caution")
|
||||
assert caution_equal["days"] == 2
|
||||
@@ -0,0 +1,63 @@
|
||||
import math
|
||||
|
||||
from scoring import engine
|
||||
|
||||
|
||||
def _complete_metrics():
|
||||
return {
|
||||
"fear_greed": {"value": 10, "classification": "Extreme Fear"},
|
||||
"puell_multiple": {"value": 0.3},
|
||||
"mvrv_zscore": {"value": -0.1},
|
||||
"drawdown": {"value": 60.0, "ath": 250.0},
|
||||
"price": {"price": 100.0},
|
||||
"200w_sma": {"value": 120.0},
|
||||
"reserve_risk": {"value": 0.001},
|
||||
"rhodl_ratio": {"value": 50.0},
|
||||
"nupl": {"value": -0.1},
|
||||
"lth_realized_price": {"value": 120.0},
|
||||
"hash_ribbons": {"buy_signal": False},
|
||||
}
|
||||
|
||||
|
||||
def test_score_all_ml_normalizes_displayed_weights_and_contributions(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
engine,
|
||||
"load_ml_weights",
|
||||
lambda: {
|
||||
"fear_greed": 0.40,
|
||||
"puell_multiple": 0.20,
|
||||
"mvrv_zscore": 0.15,
|
||||
"drawdown": 0.10,
|
||||
"pct_above_200w_sma": 0.05,
|
||||
"reserve_risk": 0.04,
|
||||
"rhodl_ratio": 0.03,
|
||||
"nupl": 0.02,
|
||||
"pct_above_lth_rp": 0.01,
|
||||
},
|
||||
)
|
||||
|
||||
scored = engine.score_all_ml(_complete_metrics())
|
||||
|
||||
assert scored["ml_mode"] is True
|
||||
valid_metrics = [m for m in scored["metrics"] if m["score"] is not None]
|
||||
assert scored["ml_weight_total"] == 1.01 # trained weights + small hash-ribbons fallback
|
||||
assert math.isclose(sum(m["ml_weight"] for m in valid_metrics), 1.0, abs_tol=0.001)
|
||||
assert math.isclose(
|
||||
sum(m["ml_contribution"] for m in valid_metrics),
|
||||
scored["composite_score"],
|
||||
abs_tol=0.05,
|
||||
)
|
||||
|
||||
hash_ribbons = next(m for m in valid_metrics if m["key"] == "hash_ribbons")
|
||||
assert hash_ribbons["ml_raw_weight"] == 0.01
|
||||
assert hash_ribbons["ml_weight"] == round(0.01 / 1.01, 4)
|
||||
|
||||
|
||||
def test_score_all_ml_preserves_classic_fallback_when_weights_missing(monkeypatch):
|
||||
monkeypatch.setattr(engine, "load_ml_weights", lambda: {})
|
||||
|
||||
scored = engine.score_all_ml(_complete_metrics())
|
||||
|
||||
assert scored["ml_mode"] is False
|
||||
assert scored["ml_error"] == "ML weights not found — run ml/optimizer.py"
|
||||
assert "classic_score" not in scored
|
||||
Reference in New Issue
Block a user