fix: 매수 차단·백테스트 왜곡 유발한 버그 5종 수정
수개월간 매수 0건 + 백테스트가 손실로 보이던 근본 원인들을 수정. - ADX 발산(mean_reversion_strategy, run_backtest): Wilder 평활에 /period 누락으로 ADX가 100 초과 발산 → 실서비스 SIDEWAYS 게이트(ADX<20) 영구 무력화. 공식 복원(표준 Wilder와 대수적으로 동일 검증). - 백테스트 워밍업(run_backtest): 국면판정 220봉을 start_date부터 세서 14개월 미만 백테스트의 ~90%가 강제 BEAR → 가짜 손실. start_date 340일 전부터 프리로드하고 성과는 sim_start_date부터만 집계. CAGR 분모도 워밍업 제외한 실제 거래일로 수정. → 최근1년 -0.33%→+6.95%, 가산성 회복. - ML HOLD 게이트(trade_executor): 신뢰도 무관 매수 차단 → 데이터 부족으로 ML이 실패(conf=0)한 HOLD까지 모든 매수를 막던 것을 conf>=0.6 조건부로. - 빈 Redis 추천 캐시(swing/scheduler): 빈 캐시를 '히트'로 처리해 DB 폴백을 건너뛰던 것을 recommendations_by_rule 비면 DB 폴백하도록. - alembic 로깅(env.py, main.py): lifespan의 alembic upgrade가 fileConfig로 앱 로거를 꺼 startup 이후 로그 전멸 → 스킵(스키마는 create_all 담당). 검증: code-verifier 통과(실전 경로 CONFIRMED 버그 없음), walk-forward 6/9구간 양(+)·과적합 아님(조건부 견고). 서바이버십 편향은 별도 이슈로 검증 진행 중. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HBmzf1HDnvFHLraJ8eaaKJ
This commit is contained in:
@@ -424,8 +424,13 @@ def calculate_adx(highs: List[float], lows: List[float], closes: List[float], pe
|
||||
|
||||
if len(dxs) < period:
|
||||
return None
|
||||
adx_s = _wilder_smooth(dxs)
|
||||
return round(adx_s[-1], 2)
|
||||
# [FIX] ADX는 합산형 _wilder_smooth(초기값=sum)가 아니라 평균형 Wilder 평활을
|
||||
# 써야 한다. 기존엔 DX(0~100)에 sum-accumulation을 적용해 ADX가 100을 넘어
|
||||
# 발산했고, SIDEWAYS 게이트(ADX<20)가 사실상 무력화됐다. mean_reversion_strategy와 동일 수정.
|
||||
adx_val = sum(dxs[:period]) / period # 초기값 = 평균
|
||||
for dx_v in dxs[period:]:
|
||||
adx_val = adx_val - adx_val / period + dx_v / period
|
||||
return round(adx_val, 2)
|
||||
|
||||
|
||||
def is_reversal_candle(opens: List[float], highs: List[float],
|
||||
@@ -992,6 +997,7 @@ def run_simulation(
|
||||
all_dates: List[date],
|
||||
initial_capital: float,
|
||||
universe: Dict[str, Tuple[str, str]],
|
||||
sim_start_date: Optional[date] = None,
|
||||
) -> Tuple[BacktestMetrics, List[Trade], List[Dict], List[Dict]]:
|
||||
"""핵심 백테스트 시뮬레이션 -- 일봉 단위로 매매 결정 실행.
|
||||
|
||||
@@ -1071,6 +1077,13 @@ def run_simulation(
|
||||
mode_params = ADAPTIVE_MODES[adaptive_mode]
|
||||
regime_update_counter = 0
|
||||
|
||||
# [FIX] 워밍업 구간(sim_start_date 이전): 국면·지표 누적만 하고
|
||||
# 매매·평가·성과기록은 건너뛴다. 이렇게 해야 국면판정 220봉 워밍업이
|
||||
# sim_start_date 시점에 이미 완료되어, 짧은 기간 백테스트도 유효해지고
|
||||
# 구간 분할 시 거래수·수익이 가산적으로 일치한다.
|
||||
if sim_start_date is not None and current_date < sim_start_date:
|
||||
continue
|
||||
|
||||
# 현재 평가 자산
|
||||
position_value = 0.0
|
||||
for code, pos in positions.items():
|
||||
@@ -1377,7 +1390,12 @@ def run_simulation(
|
||||
"unrealized_pnl_pct": round((last_close / pos.entry_price - 1) * 100, 2),
|
||||
})
|
||||
|
||||
metrics = compute_metrics(equity_values, all_trades, initial_capital, len(all_dates))
|
||||
# [FIX] CAGR 연율화 분모는 워밍업(sim_start_date 이전)을 제외한 실제 거래일 수를
|
||||
# 써야 한다. len(all_dates)는 워밍업 340일을 포함해 years를 부풀려 CAGR을 과소평가했다.
|
||||
_sim_trading_days = sum(
|
||||
1 for _d in all_dates if sim_start_date is None or _d >= sim_start_date
|
||||
)
|
||||
metrics = compute_metrics(equity_values, all_trades, initial_capital, _sim_trading_days)
|
||||
|
||||
return metrics, all_trades, equity_curve, drawdown_curve, open_positions, mode_changes
|
||||
|
||||
@@ -1607,8 +1625,15 @@ def main():
|
||||
t0 = time.time()
|
||||
|
||||
# 1. 데이터 수집
|
||||
# [FIX] 국면판정(MA200+기울기=220거래일) 및 지표 워밍업을 위해 start_date보다
|
||||
# 약 340일(≈230거래일) 앞선 데이터를 추가 로드한다. 실제 매매·성과 집계는
|
||||
# run_simulation의 sim_start_date=start_date 부터만 이뤄진다(워밍업 아티팩트 제거).
|
||||
from datetime import datetime as _dt
|
||||
from datetime import timedelta as _td
|
||||
_sim_start = _dt.strptime(start_date, "%Y-%m-%d").date()
|
||||
_warmup_start = (_sim_start - _td(days=340)).strftime("%Y-%m-%d")
|
||||
bars_by_code, index_bars, all_dates = load_data_fdr(
|
||||
BACKTEST_UNIVERSE, start_date, end_date,
|
||||
BACKTEST_UNIVERSE, _warmup_start, end_date,
|
||||
)
|
||||
|
||||
if not bars_by_code:
|
||||
@@ -1625,6 +1650,7 @@ def main():
|
||||
print(f"\n 시뮬레이션 시작...")
|
||||
metrics, all_trades, equity_curve, drawdown_curve, open_positions, mode_changes = run_simulation(
|
||||
aligned, index_bars, all_dates, initial_capital, BACKTEST_UNIVERSE,
|
||||
sim_start_date=_sim_start,
|
||||
)
|
||||
|
||||
elapsed = time.time() - t0
|
||||
|
||||
Reference in New Issue
Block a user