fix: cycle-aware scoring thresholds for diminishing returns
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
This commit is contained in:
+35
-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"
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user