Integrate unused indicators into technical scoring; fix indicator dropdown
Deploy / lint (push) Successful in 5s
Deploy / test (push) Successful in 28s
Deploy / deploy (push) Successful in 22s

- Technical dimension now uses all directional indicators:
  0.30*ADX + 0.20*EMA + 0.20*RSI + 0.15*EMA_Cross (bullish=80 /
  neutral=50 / bearish=20) + 0.10*Volume_Profile (POC proximity)
  + 0.05*Pivot_Points (structure confluence); weights re-normalize
  when data is insufficient, as before
- ATR stays out of scoring (volatility input for scanner stops,
  not a directional signal)
- IndicatorSelector uses the shared Select so the option list is
  dark instead of the native white popup
- Update technical scoring tests for the six-component breakdown

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 17:17:07 +02:00
parent 9c6a0a72fa
commit d139dd0390
3 changed files with 96 additions and 27 deletions
+25 -11
View File
@@ -44,8 +44,9 @@ async def test_returns_none_tuple_when_no_records(db_session):
@pytest.mark.asyncio
async def test_returns_breakdown_with_all_sub_scores(db_session):
"""With enough data, returns score and breakdown with ADX, EMA, RSI sub-scores."""
records = _make_ohlcv_records(50)
"""With enough data, returns score and breakdown with all six sub-scores."""
# 60 bars: enough for every indicator incl. EMA Cross (needs 51)
records = _make_ohlcv_records(60)
with patch(
"app.services.price_service.query_ohlcv",
@@ -66,17 +67,27 @@ async def test_returns_breakdown_with_all_sub_scores(db_session):
assert "ADX" in names
assert "EMA" in names
assert "RSI" in names
assert "EMA_Cross" in names
assert "Volume_Profile" in names
assert "Pivot_Points" in names
# Verify weights
# Verify weights sum to 1 and match the formula
weight_map = {s["name"]: s["weight"] for s in breakdown["sub_scores"]}
assert weight_map["ADX"] == 0.4
assert weight_map["EMA"] == 0.3
assert weight_map["RSI"] == 0.3
assert weight_map["ADX"] == 0.30
assert weight_map["EMA"] == 0.20
assert weight_map["RSI"] == 0.20
assert weight_map["EMA_Cross"] == 0.15
assert weight_map["Volume_Profile"] == 0.10
assert weight_map["Pivot_Points"] == 0.05
assert sum(weight_map.values()) == pytest.approx(1.0)
# Verify raw_value is present and numeric
# Verify raw_value is present (numeric except EMA_Cross's signal string)
for sub in breakdown["sub_scores"]:
assert sub["raw_value"] is not None
assert isinstance(sub["raw_value"], (int, float))
if sub["name"] == "EMA_Cross":
assert sub["raw_value"] in ("bullish", "neutral", "bearish")
else:
assert isinstance(sub["raw_value"], (int, float))
assert sub["description"]
assert breakdown["unavailable"] == []
@@ -112,8 +123,8 @@ async def test_partial_sub_scores_with_insufficient_data(db_session):
@pytest.mark.asyncio
async def test_all_sub_scores_unavailable(db_session):
"""With very few bars (not enough for any indicator), returns None score with breakdown."""
# 5 bars: not enough for any indicator
records = _make_ohlcv_records(5)
# 4 bars: not enough for any indicator (Pivot Points needs 5+)
records = _make_ohlcv_records(4)
with patch(
"app.services.price_service.query_ohlcv",
@@ -125,12 +136,15 @@ async def test_all_sub_scores_unavailable(db_session):
assert score is None
assert breakdown is not None
assert breakdown["sub_scores"] == []
assert len(breakdown["unavailable"]) == 3
assert len(breakdown["unavailable"]) == 6
unavail_names = [u["name"] for u in breakdown["unavailable"]]
assert "ADX" in unavail_names
assert "EMA" in unavail_names
assert "RSI" in unavail_names
assert "EMA_Cross" in unavail_names
assert "Volume_Profile" in unavail_names
assert "Pivot_Points" in unavail_names
@pytest.mark.asyncio