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:
BizzleBot 2026-03-21 22:35:13 +00:00
parent 6bfbd30e3d
commit 5538f666c5
2 changed files with 52 additions and 32 deletions

View File

@ -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,

View File

@ -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"