From 5538f666c5eb61c51c6153125e0435f9df5282a4 Mon Sep 17 00:00:00 2001 From: BizzleBot Date: Sat, 21 Mar 2026 22:35:13 +0000 Subject: [PATCH] fix: cycle-aware scoring thresholds for diminishing returns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PROBLEM: Fixed thresholds based on 2015-2018 extremes meant the score could barely reach 65 in the current cycle. MVRV Z-Score bottoms are getting shallower (-0.6 → -0.4 → -0.3), Puell floors are rising, NUPL extremes are compressing. A 'good buy' in 2024+ looks different than 2018. SOLUTION: Widened scoring ranges across all metrics: - MVRV Z-Score: 0-1.0 now scores 8/10 (was 0-0.5) - Puell Multiple: 0.4-0.7 scores 8/10 (was 0.3-0.5) - NUPL: 0-0.3 scores 8/10 (was 0-0.25) - LTH Realized Price: 0-30% above scores 7/10 (was 0-20%) - 200W SMA: 0-30% above scores 7/10 (was 0-20%) - Drawdown: 40-60% scores 8/10 (was 50-70%) - Fear & Greed: 0-15 scores 10/10 (was 0-10) - RHODL: 0-200 scores 10/10 (was 0-100) RESULT: - Today: 75/100 Strong Accumulation (was 56) - Nov 2022 bottom: 91/100 (still extreme) - 2024-2026 now has meaningful signal variation - Each threshold has a note explaining the cycle compression logic --- config/thresholds.json | 26 ++++++++++++------- scoring/engine.py | 58 +++++++++++++++++++++++++----------------- 2 files changed, 52 insertions(+), 32 deletions(-) diff --git a/config/thresholds.json b/config/thresholds.json index 6a923d6..6885f98 100644 --- a/config/thresholds.json +++ b/config/thresholds.json @@ -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, @@ -32,4 +40,4 @@ "normal": 3, "euphoria": 0 } -} \ No newline at end of file +} diff --git a/scoring/engine.py b/scoring/engine.py index 9627f02..eb3b9ae 100644 --- a/scoring/engine.py +++ b/scoring/engine.py @@ -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" @@ -397,14 +407,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"