fix: validate scraper metric semantics

This commit is contained in:
Hermes Agent
2026-07-26 23:15:37 +00:00
parent b06cabf3aa
commit 6655bcfa5a
4 changed files with 67 additions and 12 deletions
+2 -1
View File
@@ -530,8 +530,9 @@ def score_all(metrics):
vdd = metrics.get("vdd_multiple", {})
vdd_score, vdd_desc = score_momentum_pct(vdd.get("value"))
results.append({
"name": "VDD Multiple",
"name": "VDD 30-Period Momentum",
"key": "vdd_multiple",
"transform": "30_period_return",
"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,
+9 -2
View File
@@ -114,8 +114,15 @@ def collect_onchain_history(progress_cb=None):
for metric_key, trace_name in cfg["traces"].items():
if trace_name is None:
# Grab first trace with numeric data
for candidate in traces:
if metric_key == "lth_supply":
from scrapers.lookintobitcoin import _find_lth_supply_trace
candidates = [_find_lth_supply_trace(traces)]
else:
candidates = traces
# Grab the first validated trace with numeric data.
for candidate in candidates:
if not candidate:
continue
y = candidate.get("y", [])
if y and any(v is not None for v in y[-10:]):
dates, values = _extract_series(candidate)
+28 -9
View File
@@ -115,6 +115,30 @@ def _find_trace(traces, name):
return None
def _trace_signal_is_active(trace):
"""Return true only when the signal trace is active at its latest point."""
if not trace:
return False
values = trace.get("y", [])
if not values:
return False
latest = values[-1]
try:
return latest is not None and float(latest) != 0
except (TypeError, ValueError):
return bool(latest)
def _find_lth_supply_trace(traces):
"""Select an explicitly named LTH supply series and never a price fallback."""
for trace in traces or []:
name = str(trace.get("name", "")).lower()
is_lth = "long-term holder" in name or "long term holder" in name or "lth" in name
if is_lth and "supply" in name and "price" not in name:
return trace
return None
def _get_latest_value(trace):
"""Get the most recent non-null y value from a trace."""
if not trace:
@@ -210,21 +234,16 @@ def scrape_all():
],
"value": None,
}
# Try to detect buy signal from trace names/colors
# A named signal trace is not itself proof that the signal is active.
for t in traces:
name = t.get("name", "").lower()
if "buy" in name or "signal" in name:
if ("buy" in name or "signal" in name) and _trace_signal_is_active(t):
results[metric_key]["buy_signal"] = True
break
elif metric_key == "lth_supply":
# Get main supply trace
t = traces[0] if traces else None
for candidate in traces:
name = candidate.get("name", "").lower()
if "supply" in name or "lth" in name:
t = candidate
break
# Require an explicitly named LTH supply trace; price is not supply.
t = _find_lth_supply_trace(traces)
recent = _get_recent_values(t, 60)
# Determine trend: compare recent avg to older avg
trend = None
+28
View File
@@ -0,0 +1,28 @@
from scrapers import lookintobitcoin
from scoring import engine
def test_hash_ribbon_signal_requires_a_current_truthy_marker():
named_but_inactive = {"name": "Buy Signal", "y": [1, None, None]}
active = {"name": "Buy Signal", "y": [None, 0, 1]}
assert lookintobitcoin._trace_signal_is_active(named_but_inactive) is False
assert lookintobitcoin._trace_signal_is_active(active) is True
def test_lth_supply_trace_selection_never_falls_back_to_price():
traces = [
{"name": "BTC Price", "y": [60000, 61000]},
{"name": "Long-Term Holder Supply", "y": [14_000_000, 14_100_000]},
]
assert lookintobitcoin._find_lth_supply_trace(traces)["name"] == "Long-Term Holder Supply"
assert lookintobitcoin._find_lth_supply_trace(traces[:1]) is None
def test_vdd_derived_return_is_labeled_as_momentum_not_raw_multiple():
result = engine.score_all({"vdd_multiple": {"value": 0.12}})
vdd = next(metric for metric in result["metrics"] if metric["key"] == "vdd_multiple")
assert vdd["name"] == "VDD 30-Period Momentum"
assert vdd["transform"] == "30_period_return"