Files
cointrader/docs/plans/2026-03-03-adx-ml-feature-migration-plan.md
2026-03-03 21:18:22 +09:00

6.5 KiB

ADX ML 피처 마이그레이션 구현 계획

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: ADX 하드필터를 제거하고 ADX를 24번째 ML 피처로 추가하여, 횡보장 판단을 ML 모델에 위임한다. ADX 값을 항상 로그에 남겨 대시보드 끊김도 해소한다.

Architecture: indicators.py에서 ADX < 25 early-return 삭제, ml_features.py에 ADX 피처 추가 (23 → 24개), dataset_builder.py에서 ADX 하드필터 삭제 + ADX 피처 추출 추가. 기존 모델과 호환 안 되므로 재학습 필수.

Tech Stack: LightGBM, pandas-ta, pytest


Task 1: ML 피처 테스트 업데이트 (24개 피처)

Files:

  • Modify: tests/test_ml_features.py:52-54

Step 1: Update the test

test_feature_cols_has_23_items를 24개로 변경:

def test_feature_cols_has_24_items():
    from src.ml_features import FEATURE_COLS
    assert len(FEATURE_COLS) == 24

test_build_features_with_btc_eth_has_21_features의 assert도 변경:

def test_build_features_with_btc_eth_has_24_features():
    xrp_df = _make_df(10, base_price=1.0)
    btc_df = _make_df(10, base_price=50000.0)
    eth_df = _make_df(10, base_price=3000.0)
    features = build_features(xrp_df, "LONG", btc_df=btc_df, eth_df=eth_df)
    assert len(features) == 24

test_build_features_without_btc_eth_has_13_features도 변경:

def test_build_features_without_btc_eth_has_16_features():
    xrp_df = _make_df(10, base_price=1.0)
    features = build_features(xrp_df, "LONG")
    assert len(features) == 16

_make_df"adx": [20.0] * n 컬럼 추가.

Step 2: Run test to verify it fails

Run: pytest tests/test_ml_features.py::test_feature_cols_has_24_items -v Expected: FAIL — 현재 23개


Task 2: FEATURE_COLS에 ADX 추가 + build_features() 수정

Files:

  • Modify: src/ml_features.py:4-14 (FEATURE_COLS), src/ml_features.py:98-112 (base dict)

Step 3: Add ADX to FEATURE_COLS

FEATURE_COLS = [
    "rsi", "macd_hist", "bb_pct", "ema_align",
    "stoch_k", "stoch_d", "atr_pct", "vol_ratio",
    "ret_1", "ret_3", "ret_5", "signal_strength", "side",
    "btc_ret_1", "btc_ret_3", "btc_ret_5",
    "eth_ret_1", "eth_ret_3", "eth_ret_5",
    "xrp_btc_rs", "xrp_eth_rs",
    "oi_change", "funding_rate",
    "adx",
]

Step 4: Add ADX extraction in build_features()

base dict 생성 부분 (line 112 이후)에 추가:

    base["adx"] = float(last.get("adx", 0))

docstring의 "23개 피처"를 "24개 피처"로 변경.

Step 5: Run tests to verify they pass

Run: pytest tests/test_ml_features.py -v Expected: ALL PASS

Step 6: Commit

git add src/ml_features.py tests/test_ml_features.py
git commit -m "feat: add ADX as 24th ML feature"

Task 3: indicators.py ADX 하드필터 제거 + 항상 로깅

Files:

  • Modify: src/indicators.py:63-67

Step 7: Replace ADX hard filter with always-log

get_signal() 메서드에서 기존 ADX 필터 코드:

        # ADX 횡보장 필터: ADX < 25이면 추세 부재로 판단하여 진입 차단
        adx = last.get("adx", None)
        if adx is not None and not pd.isna(adx) and adx < 25:
            logger.debug(f"ADX 필터: {adx:.1f} < 25 — HOLD")
            return "HOLD"

를 다음으로 교체:

        # ADX 로깅 (ML 피처로 위임, 하드필터 제거)
        adx = last.get("adx", None)
        if adx is not None and not pd.isna(adx):
            logger.debug(f"ADX: {adx:.1f}")

Step 8: Run ADX-related tests

Run: pytest tests/test_indicators.py -k "adx" -v Expected: test_adx_column_exists PASS, test_adx_nan_falls_through PASS, test_adx_filter_blocks_low_adx FAIL (필터 제거됨)


Task 4: ADX 필터 테스트 업데이트

Files:

  • Modify: tests/test_indicators.py:57-71

Step 9: Replace block test with pass-through test

test_adx_filter_blocks_low_adx를 제거하고 새 테스트로 교체:

def test_adx_low_does_not_block_signal(sample_df):
    """ADX < 25여도 시그널이 차단되지 않는다 (ML에 위임)."""
    ind = Indicators(sample_df)
    df = ind.calculate_all()
    # 강한 LONG 신호가 나오도록 지표 조작
    df.loc[df.index[-1], "rsi"] = 20
    df.loc[df.index[-2], "macd"] = -1
    df.loc[df.index[-2], "macd_signal"] = 0
    df.loc[df.index[-1], "macd"] = 1
    df.loc[df.index[-1], "macd_signal"] = 0
    df.loc[df.index[-1], "volume"] = df.loc[df.index[-1], "vol_ma20"] * 2
    df["adx"] = 15.0
    signal = ind.get_signal(df)
    # ADX 낮아도 지표 조건 충족 시 LONG 반환 (ML이 최종 판단)
    assert signal == "LONG"

Step 10: Run all indicator tests

Run: pytest tests/test_indicators.py -v Expected: ALL PASS

Step 11: Commit

git add src/indicators.py tests/test_indicators.py
git commit -m "feat: remove ADX hard filter, delegate to ML"

Task 5: dataset_builder.py ADX 하드필터 제거 + ADX 피처 추가

Files:

  • Modify: src/dataset_builder.py:119-123 (ADX 필터 삭제), src/dataset_builder.py:215-230 (ADX 피처 추가)

Step 12: Remove ADX hard filter in _calc_signals()

_calc_signals() 함수에서 다음 코드 삭제 (lines 119-123):

    # ADX 횡보장 필터: ADX < 25이면 추세 부재로 판단하여 진입 차단
    if "adx" in d.columns:
        adx = d["adx"].values
        low_adx = (~np.isnan(adx)) & (adx < 25)
        signal_arr[low_adx] = "HOLD"

Step 13: Add ADX feature to _calc_features_vectorized()

_calc_features_vectorized() 함수의 result DataFrame 생성 부분에 "adx" 추가:

    # ADX (ML 피처로 제공 — rolling z-score 정규화)
    adx_raw = d["adx"].values.astype(np.float64) if "adx" in d.columns else np.zeros(len(d), dtype=np.float64)
    adx_z = _rolling_zscore(adx_raw)

result DataFrame에 "adx": adx_z, 추가 (side 다음에).

Step 14: Run full test suite

Run: pytest tests/ -v --tb=short Expected: ALL PASS

Step 15: Commit

git add src/dataset_builder.py
git commit -m "feat: remove ADX hard filter from dataset builder, add ADX as ML feature"

Task 6: 전체 테스트 + 최종 검증

Step 16: Run full test suite

Run: bash scripts/run_tests.sh Expected: ALL PASS

Step 17: Final commit if needed

주의: 기존 모델(23 피처)은 24 피처 입력과 호환 안 됨. 배포 전 반드시 bash scripts/train_and_deploy.sh 실행하여 재학습 필요.