# Tasks ## 1. Add quality score helper function - [x] 1.1 Create `_compute_quality_score(rr, strength, distance, entry_price, *, w_rr=0.35, w_strength=0.35, w_proximity=0.30, rr_cap=10.0) -> float` function in `app/services/rr_scanner_service.py` that computes a weighted sum of normalized R:R, normalized strength, and normalized proximity - [x] 1.2 Implement normalization: `norm_rr = min(rr / rr_cap, 1.0)`, `norm_strength = strength / 100.0`, `norm_proximity = 1.0 - min(distance / entry_price, 1.0)` - [x] 1.3 Return `w_rr * norm_rr + w_strength * norm_strength + w_proximity * norm_proximity` ## 2. Replace long setup selection logic - [x] 2.1 In `scan_ticker`, replace the long setup loop that tracks `best_rr` / `best_target` with a loop that computes `quality_score` for each candidate via `_compute_quality_score` and tracks `best_quality` / `best_candidate_rr` / `best_candidate_target` - [x] 2.2 Keep the `rr >= rr_threshold` filter — only candidates meeting the threshold are scored - [x] 2.3 Store the selected candidate's actual R:R ratio (not the quality score) in `TradeSetup.rr_ratio` ## 3. Replace short setup selection logic - [x] 3.1 Apply the same quality-score selection change to the short setup loop, mirroring the long setup changes - [x] 3.2 Ensure distance is computed as `entry_price - lv.price_level` for short candidates ## 4. Write unit tests for `_compute_quality_score` - [x] 4.1 Create `tests/unit/test_rr_scanner_quality_score.py` with tests for known inputs verifying the formula output - [x] 4.2 Test edge cases: strength=0, strength=100, distance=0, rr at cap, rr above cap - [x] 4.3 Test that all normalized components stay in 0–1 range ## 5. Write exploratory bug-condition tests (run on unfixed code to confirm bug) - [x] 5.1 [PBT-exploration] Create `tests/unit/test_rr_scanner_bug_exploration.py` with a property test that generates multiple S/R levels with varying strengths and distances, calls `scan_ticker`, and asserts the selected target is NOT always the most distant level — expected to FAIL on unfixed code, confirming the bug ## 6. Write fix-checking tests - [x] 6.1 [PBT-fix] Create `tests/unit/test_rr_scanner_fix_check.py` with a property test that generates multiple candidate S/R levels meeting the R:R threshold, calls `scan_ticker` on fixed code, and asserts the selected target has the highest quality score among all candidates ## 7. Write preservation tests - [x] 7.1 [PBT-preservation] Create `tests/unit/test_rr_scanner_preservation.py` with a property test that generates zero-candidate and single-candidate scenarios and asserts the fixed function produces the same output as the original (no setup for zero candidates, same setup for single candidate) - [x] 7.2 Add unit test verifying that when no S/R levels exist, no setup is produced (unchanged) - [x] 7.3 Add unit test verifying that when only one candidate meets threshold, it is selected (unchanged) - [x] 7.4 Add unit test verifying `get_trade_setups` sorting is unchanged (R:R desc, composite desc) ## 8. Integration test - [x] 8.1 Add integration test in `tests/unit/test_rr_scanner_integration.py` that mocks DB with multiple S/R levels of varying quality, runs `scan_ticker`, and verifies the full flow: quality-based selection, correct TradeSetup fields, database persistence