From b961c53a9290abda16942c2a4e97296492e17a09 Mon Sep 17 00:00:00 2001 From: agentson Date: Fri, 20 Feb 2026 09:33:35 +0900 Subject: [PATCH] =?UTF-8?q?improve:=20implied=5Frsi=20=EA=B3=84=EC=88=98?= =?UTF-8?q?=204.0=E2=86=922.0=EC=9C=BC=EB=A1=9C=20=EC=99=84=ED=99=94=20?= =?UTF-8?q?=E2=80=94=20=ED=8F=AC=ED=99=94=20=EC=9E=84=EA=B3=84=EC=A0=90=20?= =?UTF-8?q?12.5%=E2=86=9225%=20(#181)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SmartScanner의 implied_rsi 공식에서 계수를 4.0에서 2.0으로 수정. 12.5% 이상 변동률에서 RSI=100으로 포화되던 문제를 개선. 변경 전: 50 + (change_rate * 4.0) → 12.5% 변동 시 RSI=100 변경 후: 50 + (change_rate * 2.0) → 25% 변동 시 RSI=100 이제 10% 상승 → RSI=70, 12.5% 상승 → RSI=75 (의미 있는 구분 가능) 해외 소형주(NYSE American 등)의 RSI=100 집단 현상 완화. - smart_scanner.py 3곳 동일 공식 모두 수정 - TestImpliedRSIFormula 클래스 5개 테스트 추가 Co-Authored-By: Claude Sonnet 4.6 --- src/analysis/smart_scanner.py | 6 +++--- tests/test_smart_scanner.py | 36 +++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/analysis/smart_scanner.py b/src/analysis/smart_scanner.py index 9972a57..7717166 100644 --- a/src/analysis/smart_scanner.py +++ b/src/analysis/smart_scanner.py @@ -175,7 +175,7 @@ class SmartVolatilityScanner: liquidity_score = volume_rank_bonus.get(stock_code, 0.0) score = min(100.0, volatility_score + liquidity_score) signal = "momentum" if change_rate >= 0 else "oversold" - implied_rsi = max(0.0, min(100.0, 50.0 + (change_rate * 4.0))) + implied_rsi = max(0.0, min(100.0, 50.0 + (change_rate * 2.0))) candidates.append( ScanCandidate( @@ -282,7 +282,7 @@ class SmartVolatilityScanner: liquidity_score = volume_rank_bonus.get(stock_code, 0.0) score = min(100.0, volatility_score + liquidity_score) signal = "momentum" if change_rate >= 0 else "oversold" - implied_rsi = max(0.0, min(100.0, 50.0 + (change_rate * 4.0))) + implied_rsi = max(0.0, min(100.0, 50.0 + (change_rate * 2.0))) candidates.append( ScanCandidate( stock_code=stock_code, @@ -338,7 +338,7 @@ class SmartVolatilityScanner: score = min(volatility_pct / 10.0, 1.0) * 100.0 signal = "momentum" if change_rate >= 0 else "oversold" - implied_rsi = max(0.0, min(100.0, 50.0 + (change_rate * 4.0))) + implied_rsi = max(0.0, min(100.0, 50.0 + (change_rate * 2.0))) candidates.append( ScanCandidate( stock_code=stock_code, diff --git a/tests/test_smart_scanner.py b/tests/test_smart_scanner.py index 18f3873..bb8200f 100644 --- a/tests/test_smart_scanner.py +++ b/tests/test_smart_scanner.py @@ -350,6 +350,42 @@ class TestSmartVolatilityScanner: assert [c.stock_code for c in candidates] == ["ABCD"] +class TestImpliedRSIFormula: + """Test the implied_rsi formula in SmartVolatilityScanner (issue #181).""" + + def test_neutral_change_gives_neutral_rsi(self) -> None: + """0% change → implied_rsi = 50 (neutral).""" + # formula: 50 + (change_rate * 2.0) + rsi = max(0.0, min(100.0, 50.0 + (0.0 * 2.0))) + assert rsi == 50.0 + + def test_10pct_change_gives_rsi_70(self) -> None: + """10% upward change → implied_rsi = 70 (momentum signal).""" + rsi = max(0.0, min(100.0, 50.0 + (10.0 * 2.0))) + assert rsi == 70.0 + + def test_minus_10pct_gives_rsi_30(self) -> None: + """-10% change → implied_rsi = 30 (oversold signal).""" + rsi = max(0.0, min(100.0, 50.0 + (-10.0 * 2.0))) + assert rsi == 30.0 + + def test_saturation_at_25pct(self) -> None: + """Saturation occurs at >=25% change (not 12.5% as with old coefficient 4.0).""" + rsi_12pct = max(0.0, min(100.0, 50.0 + (12.5 * 2.0))) + rsi_25pct = max(0.0, min(100.0, 50.0 + (25.0 * 2.0))) + rsi_30pct = max(0.0, min(100.0, 50.0 + (30.0 * 2.0))) + # At 12.5% change: RSI = 75 (not 100, unlike old formula) + assert rsi_12pct == 75.0 + # At 25%+ saturation + assert rsi_25pct == 100.0 + assert rsi_30pct == 100.0 # Capped + + def test_negative_saturation(self) -> None: + """Saturation at -25% gives RSI = 0.""" + rsi = max(0.0, min(100.0, 50.0 + (-25.0 * 2.0))) + assert rsi == 0.0 + + class TestRSICalculation: """Test RSI calculation in VolatilityAnalyzer."""