Compare commits
4 Commits
feature/is
...
feature/is
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b961c53a92 | ||
| 76a7ee7cdb | |||
| 17112b864a | |||
|
|
28bcc7acd7 |
@@ -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,
|
||||
|
||||
12
src/main.py
12
src/main.py
@@ -1358,10 +1358,18 @@ def _start_dashboard_server(settings: Settings) -> threading.Thread | None:
|
||||
if not settings.DASHBOARD_ENABLED:
|
||||
return None
|
||||
|
||||
# Validate dependencies before spawning the thread so startup failures are
|
||||
# reported synchronously (avoids the misleading "started" → "failed" log pair).
|
||||
try:
|
||||
import uvicorn # noqa: F401
|
||||
from src.dashboard import create_dashboard_app # noqa: F401
|
||||
except ImportError as exc:
|
||||
logger.warning("Dashboard server unavailable (missing dependency): %s", exc)
|
||||
return None
|
||||
|
||||
def _serve() -> None:
|
||||
try:
|
||||
import uvicorn
|
||||
|
||||
from src.dashboard import create_dashboard_app
|
||||
|
||||
app = create_dashboard_app(settings.DB_PATH)
|
||||
@@ -1372,7 +1380,7 @@ def _start_dashboard_server(settings: Settings) -> threading.Thread | None:
|
||||
log_level="info",
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning("Dashboard server failed to start: %s", exc)
|
||||
logger.warning("Dashboard server stopped unexpectedly: %s", exc)
|
||||
|
||||
thread = threading.Thread(
|
||||
target=_serve,
|
||||
|
||||
@@ -2194,6 +2194,29 @@ def test_start_dashboard_server_enabled_starts_thread() -> None:
|
||||
mock_thread.start.assert_called_once()
|
||||
|
||||
|
||||
def test_start_dashboard_server_returns_none_when_uvicorn_missing() -> None:
|
||||
"""Returns None (no thread) and logs a warning when uvicorn is not installed."""
|
||||
settings = Settings(
|
||||
KIS_APP_KEY="test_key",
|
||||
KIS_APP_SECRET="test_secret",
|
||||
KIS_ACCOUNT_NO="12345678-01",
|
||||
GEMINI_API_KEY="test_gemini_key",
|
||||
DASHBOARD_ENABLED=True,
|
||||
)
|
||||
import builtins
|
||||
real_import = builtins.__import__
|
||||
|
||||
def mock_import(name: str, *args: object, **kwargs: object) -> object:
|
||||
if name == "uvicorn":
|
||||
raise ImportError("No module named 'uvicorn'")
|
||||
return real_import(name, *args, **kwargs)
|
||||
|
||||
with patch("builtins.__import__", side_effect=mock_import):
|
||||
thread = _start_dashboard_server(settings)
|
||||
|
||||
assert thread is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# BUY cooldown tests (#179)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user