M1 │ 스캐너 최소 거래량 100만→10만주 │ kosdaq_scanner.py │
├─────┼─────────────────────────────────────────────────────┼────────────────────────────────┤ │ 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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user