M1 │ 스캐너 최소 거래량 100만→10만주 │ kosdaq_scanner.py │
Some checks failed
CI/CD Pipeline / test-backend (push) Has been cancelled
CI/CD Pipeline / test-frontend (push) Has been cancelled
CI/CD Pipeline / build-and-push (push) Has been cancelled

├─────┼─────────────────────────────────────────────────────┼────────────────────────────────┤
  │ M2  │ ADX 히스테리시스 18/22 (플리핑 방지)                │ mean_reversion_strategy.py     │
  ├─────┼─────────────────────────────────────────────────────┼────────────────────────────────┤
  │ M3  │ 분할매도 최소 수익 1.0%→0.5%                        │ mean_reversion_strategy.py     │
  ├─────┼─────────────────────────────────────────────────────┼────────────────────────────────┤
  │ M4  │ ORB 갭 필터 4% + 레인지 폭 0.3% 최소                │ day_trading_strategy.py        │
  ├─────┼─────────────────────────────────────────────────────┼────────────────────────────────┤
  │ M5  │ 쿨다운 스트릭 부분매도 시 미변경                    │ daytrading/position_manager.py │
  ├─────┼─────────────────────────────────────────────────────┼────────────────────────────────┤
  │ M6  │ MDD 강제매도 loss_tracker 기록 제거 (허위 ban 방지) │ mdd_manager.py                 │
  ├─────┼─────────────────────────────────────────────────────┼────────────────────────────────┤
  │ M7  │ RSI Wilder 스무딩으로 통일                          │ mean_reversion_strategy.py
This commit is contained in:
2026-04-03 11:41:52 +09:00
parent eda5d76af9
commit 9d096ddd57
22 changed files with 299 additions and 132 deletions

View File

@@ -38,6 +38,9 @@ from app.services.strategy_config_service import strategy_cfg
logger = logging.getLogger(__name__)
# M2: ADX hysteresis state — 직전 regime을 기억하여 ADX 경계선에서 잦은 전환 방지
_previous_regime: Optional["MarketRegime"] = None
class MarketRegime(Enum):
BULL = "bull"
@@ -163,11 +166,10 @@ def detect_market_regime(index_closes: List[float],
return MarketRegime.BEAR # 데이터 부족 시 보수적
# CLAUDE.md 6.1: ADX < 20 → SIDEWAYS (진입 차단)
# S3 fix: ADX 값은 먼저 계산하되, slope도 함께 확인 후 판정
_adx_value = None
if highs is not None and lows is not None:
adx = calculate_adx(highs, lows, index_closes)
if adx is not None and adx < settings.MEAN_REVERSION_ADX_SIDEWAYS:
logger.info("시장 국면: SIDEWAYS (ADX=%.1f < %.1f) — CLAUDE.md 6.1", adx, settings.MEAN_REVERSION_ADX_SIDEWAYS)
return MarketRegime.SIDEWAYS
_adx_value = calculate_adx(highs, lows, index_closes)
ma200_current = sum(index_closes[-settings.MEAN_REVERSION_MA200_PERIOD:]) / settings.MEAN_REVERSION_MA200_PERIOD
# [#10] 기울기 비교 기간을 5일→20일로 확대하여 SIDEWAYS 과다판정 방지
@@ -182,28 +184,64 @@ def detect_market_regime(index_closes: List[float],
# CLAUDE.md 6.1: 기울기 ≈ 0 → SIDEWAYS
slope_ratio = abs(ma200_current - ma200_prev) / ma200_prev if ma200_prev > 0 else 0
# S3 fix: SIDEWAYS 판정은 ADX < threshold AND slope ≈ 0 모두 충족 시에만.
# ADX < threshold이더라도 slope가 명확하면 BULL/BEAR로 분류한다.
# M2: ADX hysteresis — enter SIDEWAYS at ADX < 18, exit SIDEWAYS at ADX > 22
global _previous_regime
_ADX_ENTER_SIDEWAYS = settings.MEAN_REVERSION_ADX_SIDEWAYS - 2.0 # 18
_ADX_EXIT_SIDEWAYS = settings.MEAN_REVERSION_ADX_SIDEWAYS + 2.0 # 22
if _adx_value is not None:
_was_sideways = (_previous_regime == MarketRegime.SIDEWAYS)
# hysteresis: 이미 SIDEWAYS면 ADX > 22 돌파해야 탈출, 아니면 ADX < 18 이하여야 진입
_adx_triggers_sideways = (
(_was_sideways and _adx_value < _ADX_EXIT_SIDEWAYS) or
(not _was_sideways and _adx_value < _ADX_ENTER_SIDEWAYS)
)
if _adx_triggers_sideways:
if slope_ratio < settings.MEAN_REVERSION_SLOPE_THRESHOLD:
logger.info("시장 국면: SIDEWAYS (ADX=%.1f, hysteresis=[enter<%.0f,exit>%.0f] AND slope≈0, ratio=%.5f) — CLAUDE.md 6.1",
_adx_value, _ADX_ENTER_SIDEWAYS, _ADX_EXIT_SIDEWAYS, slope_ratio)
_previous_regime = MarketRegime.SIDEWAYS
return MarketRegime.SIDEWAYS
else:
logger.info("시장 국면: ADX=%.1f이나 slope 명확(ratio=%.5f) — BULL/BEAR로 분류", _adx_value, slope_ratio)
if slope_ratio < settings.MEAN_REVERSION_SLOPE_THRESHOLD:
logger.info("시장 국면: SIDEWAYS (MA200 기울기 ≈ 0, ratio=%.5f) — CLAUDE.md 6.1", slope_ratio)
_previous_regime = MarketRegime.SIDEWAYS
return MarketRegime.SIDEWAYS
is_bull = index_closes[-1] > ma200_current and slope_positive
logger.info("시장 국면: %s (MA200 slope_ratio=%.5f, 20일 기준)", "BULL" if is_bull else "BEAR", slope_ratio)
return MarketRegime.BULL if is_bull else MarketRegime.BEAR
result = MarketRegime.BULL if is_bull else MarketRegime.BEAR
logger.info("시장 국면: %s (MA200 slope_ratio=%.5f, 20일 기준)", result.value.upper(), slope_ratio)
_previous_regime = result
return result
def calculate_rsi(closes: List[float], period: int = 14) -> Optional[float]:
"""RSI(Relative Strength Index) 계산"""
"""RSI(Relative Strength Index) 계산 — Wilder 평활법
feature_engineering.compute_rsi()와 동일한 Wilder smoothing 사용:
- 첫 period개: 단순 평균(seed)
- 이후: avg = (prev_avg * (period-1) + current) / period
"""
if len(closes) < period + 1:
return None
gains, losses = [], []
for i in range(-period, 0):
diff = closes[i] - closes[i - 1]
gains.append(max(diff, 0))
losses.append(max(-diff, 0))
# 모든 price diff 계산
diffs = [closes[i] - closes[i - 1] for i in range(1, len(closes))]
# Seed: 첫 period개의 단순 평균
avg_gain = sum(max(d, 0) for d in diffs[:period]) / period
avg_loss = sum(max(-d, 0) for d in diffs[:period]) / period
# Wilder smoothing: 나머지 구간
for i in range(period, len(diffs)):
avg_gain = (avg_gain * (period - 1) + max(diffs[i], 0)) / period
avg_loss = (avg_loss * (period - 1) + max(-diffs[i], 0)) / period
avg_gain = sum(gains) / period
avg_loss = sum(losses) / period
if avg_loss == 0:
return 100.0
rs = avg_gain / avg_loss
@@ -301,6 +339,8 @@ def check_buy_signal(
if not _adp_bb.fallback_used:
# Normal 기준 0.30 대비 비율로 스케일링
_adaptive_bb_scale = _adp_bb.bb_pct_b / 0.30 if _adp_bb.bb_pct_b > 0 else 1.0
# S1 fix: BB 중심선(pct_b=0.5) 위 진입 방지 — 스케일 상한 1.3
_adaptive_bb_scale = min(_adaptive_bb_scale, 1.3)
except Exception:
pass
@@ -462,7 +502,7 @@ def check_exit_signal(
min_profit_price = entry_price + 2.0 * atr # CLAUDE.md 6.3: 2×ATR
_profit_pct = ((close - entry_price) / entry_price * 100) if entry_price > 0 else 0
# 최소 수익률 가드: 수수료(~0.4%)를 커버하지 못하면 익절하지 않음
_MIN_PROFIT_FOR_EXIT = 1.0 # 최소 1% 수익이어야 익절 (수수료+세금 0.4% 커버 + 알파)
_MIN_PROFIT_FOR_EXIT = 0.5 # 최소 0.5% 수익이어야 익절 (수수료+세금 ~0.3-0.4% 커버)
if remaining_pct > threshold and _profit_pct >= _MIN_PROFIT_FOR_EXIT:
rsi_exit = rsi >= _adaptive_rsi_partial
bb_exit = close > bb_middle