📝

Draft article

This draft is visible to admins and superusers only. Sign in with an authorized account.

← 기사 목록으로
August 25, 2026
5분 소요

수익률 방향 예측을 위한 XGBoost: 클래스 불균형과 의사결정 임계값

수익률 방향 예측을 위한 XGBoost: 클래스 불균형과 의사결정 임계값
#machine-learning
#xgboost
#gradient-boosting
#quant
#prediction

다음 한 시간의 수익률이 +0.5%를 초과하는지 예측하도록 분류기를 학습시킨다고 해서 균형 잡힌 이진 문제를 만든 것은 아니다. 조용한 암호화폐 시장 국면에서는 70--80%의 바가 이 임계값 아래에 있으므로, 모델은 양성을 단 한 번도 예측하지 않고도 75%의 정확도를 얻을 수 있다. 여기서 정확도는 쓸모가 없다. 아무도 의도적으로 선택하지 않은 기본 0.5 의사결정 경계도 마찬가지다. predict()가 우연히 사용하는 값일 뿐이다.

표준적인 대응은 세 가지다. scale_pos_weight로 손실을 재가중하거나, 손실을 초점 손실로 바꾸거나, 손실은 그대로 둔 채 사후에 의사결정 임계값을 옮기는 방법이다. 보통 서로 바꿔 쓸 수 있는 방법처럼 소개되지만, 실제로는 그렇지 않다. 세 방법은 서로 다른 정밀도/재현율 절충을 만들며, 더 중요한 점은 비용 차감 후 PnL도 다르게 만든다는 것이다. 트레이딩에서는 재현율보다 정밀도가 훨씬 중요하기 때문이다. 거래를 놓치면 비용이 없지만, 잘못된 거래에는 스프레드와 수수료가 든다.

이 글에서는 시간 단위 BTC 및 ETH 데이터에 동일하게 purge를 적용한 walk-forward 폴드를 사용해 세 방법을 모두 측정한다. 이어서 여기서 따라 나오는 두 가지 주제를 다룬다. min_child_weight가 행 개수가 아니라 Hessian 임계값인 이유(손실을 재가중하면 "적절한" 값이 바뀌는 이유이기도 하다), 그리고 XGBoost, LightGBM, CatBoost가 실제로 어떻게 다른지다.

방법 정밀도 재현율 거래 수 비용 차감 후 PnL 보정 Sharpe
기준선(보정 없음, thr=0.5)
scale_pos_weight = n_neg/n_pos
초점 손실(γ=2, α=0.25)
임계값 최적화(최소 정밀도 0.55)

모든 행에서 폴드, 특성, 비용 모델을 동일하게 사용한다. Sharpe는 시도한 구성 수를 고려해 보정 Sharpe와 다중 검정 방식으로 보정한다. 비용은 슬리피지 및 비용 모델을 따른다.

수익률 예측에서 클래스 불균형 다루기

균형 잡힌 수익률 예측 신호장

방법 1: scale_pos_weight

가장 단순한 방법이다. scale_pos_weight = n_negative / n_positive로 설정한다.

n_pos = y_train.sum()
n_neg = len(y_train) - n_pos
scale_pos_weight = n_neg / n_pos  # e.g., 3.0 if 75% negative

model = xgb.XGBClassifier(
    scale_pos_weight=scale_pos_weight,
    ...
)

이렇게 하면 양성 클래스 샘플의 그래디언트가 확대되어, 양성 예시를 잘못 분류하는 것이 음성 예시를 잘못 분류하는 것보다 kk배 더 나쁘다고 모델에 알려준다.

놓치기 쉬운 부작용이 있다. Hessian도 함께 확대된다는 점이다. min_child_weight는 리프의 Hessian 합에 대한 임계값이므로(아래 참조), 손실을 재가중하면 트리가 얼마나 공격적으로 분할되는지가 조용히 바뀐다. scale_pos_weight=1에서 조정한 min_child_weightscale_pos_weight=3에서도 같은 의미를 갖지 않는다. 두 값을 순차적으로가 아니라 함께 다시 조정해야 한다.

두 번째 부작용은 출력 확률이 더 이상 보정되지 않는다는 점이다. predict_proba는 실제 확률과 단조 관계인 값을 반환하지만 그 값이 실제 확률과 같지는 않다. 따라서 이 숫자를 기반으로 한 하위 포지션 사이징은 잘못된다.

방법 2: 초점 손실

초점 손실은 클래스와 관계없이 쉬운 예시의 가중치를 낮추고, 어렵고 모호한 샘플에 학습을 집중시킨다. +0.5% 바와 +0.4% 바 사이의 경계가 대부분 노이즈인 수익률 예측에는 매력적인 관점이다. 다만 라벨이 이처럼 시끄러울 때 "모호한 사례에 집중하라"와 "학습할 수 없는 사례에 집중하라"는 같은 지시가 된다. 그래서 가정하는 대신 측정해야 한다.

FL(pt)=αt(1pt)γlog(pt)\text{FL}(p_t) = -\alpha_t (1 - p_t)^{\gamma} \log(p_t)

여기서 ptp_t는 실제 클래스에 대한 예측 확률이고, αt\alpha_t는 클래스 가중치를 균형화하며, γ\gamma(일반적으로 1--3)는 쉬운 예시의 가중치를 얼마나 강하게 낮출지 제어한다.

def focal_loss_objective(y_true, y_pred, gamma=2.0, alpha=0.25):
    """
    Custom focal loss for XGBoost. Returns gradient and hessian.
    """
    p = 1.0 / (1.0 + np.exp(-y_pred))  # sigmoid

    g1 = alpha * y_true * (1 - p)**gamma * (gamma * p * np.log(p + 1e-9) + p - 1)
    g2 = (1 - alpha) * (1 - y_true) * p**gamma * (
        -gamma * (1 - p) * np.log(1 - p + 1e-9) - p
    )
    grad = -(g1 + g2)

    hess = np.maximum(grad * (1 - grad), 1e-6)

    return grad, hess


model = xgb.XGBClassifier(objective=focal_loss_objective, ...)

사용자 정의 목적 함수에는 그래디언트와 양의 Hessian이 있어야 리프 가중치 공식이 하강 단계가 된다. 초점 손실의 정확한 2차 도함수는 모든 곳에서 양수가 아니므로 구현에서는 대리 근사를 사용한다. 하지만 스케일 인자가 다른 대리 근사는 단계 크기를 바꾸고, Hessian 합을 통해 min_child_weight가 수행하는 가지치기의 의미도 바꾼다. 실제로 비용이 발생하는지는 비교표만이 보여줄 수 있다.

방법 3: 임계값 최적화

손실은 그대로 두고 보정된 모델을 학습한 뒤 검증 세트에서 의사결정 임계값을 옮긴다.

from sklearn.metrics import precision_recall_curve

def optimize_threshold(y_true, y_proba, min_precision=0.55):
    """
    Find the threshold maximizing F1 subject to a minimum precision.
    """
    precisions, recalls, thresholds = precision_recall_curve(y_true, y_proba)
    f1_scores = 2 * (precisions * recalls) / (precisions + recalls + 1e-9)

    valid = precisions[:-1] >= min_precision
    if not valid.any():
        return 0.5  # fallback

    best_idx = np.argmax(f1_scores[:-1] * valid)
    return thresholds[best_idx]

트레이딩에서 중요한 것은 정밀도 하한이다. F1만 최대화하면 정밀도를 재현율과 맞바꾸게 된다. PnL의 관점에서는 스프레드를 매번 지불해야 하는 한계 거래가 늘어난다는 뜻이다. 정밀도를 제약하고 그 안에서 재현율을 최대화하는 방식은 "거래는 줄이되, 거래할 때는 맞히자"에 해당한다.

이를 정직하게 유지하려면 두 가지 규칙을 지켜야 한다. 훈련 구간보다 시간순으로 뒤에 있는 검증 데이터에서 임계값을 학습하고, 폴드마다 다시 학습하는 것이다. 전체 표본에서 한 번 고른 임계값은 룩어헤드 편향 분류에 정리된 유형의 미래 정보 누수다. 폴드별 재학습은 무료 진단도 제공한다. 최적 임계값이 폴드마다 크게 뛰면 보정이 불안정하고 임계값이 노이즈에 맞춰지고 있다는 뜻이다.

min_child_weight가 행 개수가 아니라 Hessian 합인 이유

곡률이 뒷받침하는 트리 분할

그래디언트 부스팅은 가산 앙상블을 만든다. tt 단계에서 정규화된 목적 함수를 최소화하는 트리 ftf_t를 추가하며, XGBoost는 이를 2차 Taylor 전개로 근사한다.

L(t)i=1n[gift(xi)+12hift2(xi)]+Ω(ft)\mathcal{L}^{(t)} \approx \sum_{i=1}^{n} \Bigl[ g_i f_t(\mathbf{x}_i) + \frac{1}{2} h_i f_t^2(\mathbf{x}_i) \Bigr] + \Omega(f_t)

여기서 gi=l/y^i(t1)g_i = \partial l / \partial \hat{y}_i^{(t-1)}, hi=2l/(y^i(t1))2h_i = \partial^2 l / \partial (\hat{y}_i^{(t-1)})^2이고,

Ω(f)=γT+12λj=1Twj2\Omega(f) = \gamma T + \frac{1}{2}\lambda \sum_{j=1}^{T} w_j^2

TT는 리프 수, wjw_j는 리프 가중치, γ\gamma는 리프당 페널티, λ\lambda는 L2 항이다. 최적의 리프 가중치를 풀면 wj=ijgi/(ijhi+λ)w_j^* = -\sum_{i \in j} g_i / (\sum_{i \in j} h_i + \lambda)가 된다.

Hessian 합은 분모에 들어간다. 핵심은 바로 여기에 있다. min_child_weight는 리프의 행 수가 아니라 그 에 임계값을 둔다. Log loss에서는 hi=pi(1pi)h_i = p_i(1 - p_i)p=0.5p = 0.5에서 가장 크고, 예측이 확신에 가까워질수록 0으로 줄어든다. 따라서 확신 있게 분류된 바가 가득한 리프는 Hessian 합이 작아져 가지치기되지만, 실제로 모호한 바 몇 개를 담은 리프는 살아남을 수 있다.

노이즈가 많은 금융 라벨에서는 이것이 원하는 동작이며, 다음과 같은 실무적 결과를 설명한다.

  • min_child_weight를 높이면 소수의 불확실한 관측치에 의존하는 리프를 가지친다. 바로 노이즈를 학습했을 가능성이 가장 큰 관측치들이다. 이는 min_child_samples처럼 행 수를 세는 것보다 정밀한 도구다.
  • 손실의 스케일을 바꾸는 모든 요소는 Hessian의 스케일도 바꾼다. scale_pos_weight, 사용자 정의 목적 함수, 샘플 가중치는 입력한 숫자가 그대로여도 유효한 min_child_weight를 바꾼다.
  • 부스팅이 진행되고 예측이 더 선명해지면 Hessian은 전체적으로 작아진다. 따라서 고정된 min_child_weight는 뒤쪽 라운드에서 더 공격적으로 가지친다. 이것은 내장된 annealing 효과이며, 낮은 학습률과 많은 트리가 적은 수의 큰 단계와 다르게 작동하는 이유다.

LightGBM의 min_child_samples는 행 수를 세는 값으로, 이름만 비슷할 뿐 실제로는 다른 파라미터다. Hessian에 대응하는 파라미터는 min_sum_hessian_in_leaf다. 이름을 맞춰 두 라이브러리 사이에서 설정을 옮기는 것은 모델을 무심코 바꾸는 흔한 방법이다.

XGBoost vs. LightGBM vs. CatBoost: 엔지니어링 차이

세 가지 그래디언트 부스팅 아키텍처

세 라이브러리 모두 그래디언트 부스팅 결정 트리를 구현한다. 차이는 트리를 구성하는 방식에 있으며, 실제로는 학습 시간과 샘플 외 점수에 나타난다.

XGBoost는 **레벨 단위(너비 우선)**로 성장한다. 같은 깊이의 모든 리프를 더 깊이 들어가기 전에 분할한다. 그 결과 균형 잡힌 트리가 만들어지고, max_depth가 의미 있는 복잡도 제어 수단이 되며 튜닝 결과도 더 예측 가능하다. 반면 더 줄일 손실이 거의 남지 않은 리프까지 분할하느라 작업을 낭비한다.

LightGBM리프 단위로 성장한다. 리프가 어디에 있든 손실을 가장 크게 줄이는 리프를 분할한다. 더 적은 분할로 같은 학습 손실에 도달하지만 트리가 깊고 불균형해지므로, max_depth는 적절한 제어 손잡이가 아니며 num_leaves가 그 역할을 한다. 속도를 높이는 두 가지 추가 기법도 있다.

  • GOSS(그래디언트 기반 단측 샘플링)는 그래디언트가 큰 인스턴스를 모두 유지하고 작은 그래디언트 인스턴스는 무작위로 서브샘플링한 뒤, 생존한 샘플의 가중치를 높여 그래디언트 추정치를 불편으로 유지한다. 금융 데이터에서 그래디언트가 큰 인스턴스는 모델이 현재 틀리고 있는 바이며, 라벨 노이즈도 바로 그곳에 있다. 따라서 GOSS는 노이즈가 가장 심한 곳에 샘플링을 집중한다. 당연히 가정하지 말고 일반 서브샘플링과 비교해 볼 가치가 있다.
  • EFB(Exclusive Feature Bundling)는 상호 배타적인 희소 특성을 하나의 bin-space 특성으로 묶는다. 원-핫 인코딩에는 효과적이지만, 기술 특성 행렬의 대부분을 차지하는 밀집 연속 특성에는 거의 쓸모가 없다.

CatBoost대칭(oblivious) 트리로 성장한다. 같은 깊이의 모든 노드가 동일한 분할 조건을 사용한다. 강력한 정규화 효과가 있고 추론도 매우 빠르다. 트리가 인덱스 조회로 바뀌기 때문이다. 대신 트리 하나당 표현력은 낮아진다. 두 가지 특징적인 메커니즘은 다음과 같다.

  • 순서 기반 부스팅. 표준 부스팅은 해당 샘플로 학습한 모델을 사용해 그 샘플의 잔차를 계산하므로 잔차가 편향된다. 이를 "prediction shift"라고 한다. CatBoost는 무작위 순열에서 해당 샘플보다 앞선 샘플만으로 학습한 모델을 사용해 각 샘플의 잔차를 추정한다. 이 구조는 시계열 관점에 자연스럽게 맞으며, 데이터가 적을 때 특히 중요하다.
  • 순서 기반 타깃 통계는 범주형 인코딩에 사용된다. 앞선 샘플만으로 타깃 통계를 계산하므로 순진한 평균 인코딩이 만드는 타깃 누수를 피한다. 특성 집합에 거래소, 자산 등급, 시장 국면 라벨이 있다면 이것이 CatBoost를 사용하는 정직한 이유다.
속성 XGBoost LightGBM CatBoost
트리 성장 레벨 단위 리프 단위 대칭(oblivious)
복잡도 제어 max_depth num_leaves depth
리프 크기 제한 min_child_weight(Hessian) min_child_samples(행) min_data_in_leaf(행)
범주형 특성 수동 인코딩 기본 지원 네이티브, 순서 기반 TS
정규화 L1/L2 + gamma L1/L2 + num_leaves L2 + 무작위 강도
사용자 정의 손실 유연함 유연함 다소 제한적
학습 시간, 이 데이터셋
표본 외 log loss, 동일 폴드

정성적 행은 라이브러리의 사실을 보여준다. 마지막 두 행만이 "무엇을 써야 하는가"에 답하며, 반드시 자신의 데이터로 실행해 얻어야 한다. 잘 튜닝된 구현 사이의 차이는 대개 데이터셋의 형태가 좌우할 만큼 작다.

라이브러리를 바꾸는 일은 대부분 이름을 바꾸는 작업이다. 아래처럼 파라미터 차이만 나열한 단일 학습 함수면 충분하다.

import xgboost as xgb

def train_xgb_model(X_train, y_train, X_val, y_val, class_weight_ratio=1.0):
    """Train XGBoost classifier for return direction prediction."""
    model = xgb.XGBClassifier(
        n_estimators=2000,
        max_depth=5,
        learning_rate=0.01,
        subsample=0.7,
        colsample_bytree=0.7,
        min_child_weight=10,   # Hessian sum, not row count
        gamma=1.0,
        reg_alpha=0.1,
        reg_lambda=1.0,
        scale_pos_weight=class_weight_ratio,
        objective='binary:logistic',
        eval_metric='logloss',
        tree_method='hist',
        random_state=42,
        early_stopping_rounds=50,
    )
    model.fit(X_train, y_train, eval_set=[(X_val, y_val)], verbose=False)
    return model
개념 XGBoost LightGBM CatBoost
트리 수 n_estimators n_estimators iterations
L2 페널티 reg_lambda reg_lambda l2_leaf_reg
열 샘플링 colsample_bytree colsample_bytree rsm
클래스 불균형 scale_pos_weight scale_pos_weight auto_class_weights='Balanced'
조기 중단 early_stopping_rounds lgb.early_stopping() callback early_stopping_rounds

폴드 전체의 SHAP 중요도로 알파 쇠퇴 감지하기

폴드가 진행될수록 약해지는 특성 기여도

이 블로그에서는 이미 그래디언트 부스팅 모델의 SHAP을 다뤘다. TreeExplainer, 요약 플롯, 그리고 이를 읽는 방법이다. 아직 다루지 않은 것은 SHAP를 시간축으로 사용하는 방법이다. walk-forward 폴드마다 설명기를 하나씩 두고, 시간에 따른 각 특성의 평균 절대 기여도를 추적한다.

def shap_over_time(models, test_sets, feature_names) -> pd.DataFrame:
    """
    Track SHAP-based feature importance across walk-forward folds.
    Rows are folds, columns are features.
    """
    importance_over_time = []
    for fold_idx, (model, X_test) in enumerate(zip(models, test_sets)):
        explainer = shap.TreeExplainer(model)
        shap_values = explainer.shap_values(X_test)
        mean_abs_shap = np.abs(shap_values).mean(axis=0)
        importance_over_time.append(
            pd.Series(mean_abs_shap, index=feature_names, name=fold_idx)
        )
    return pd.DataFrame(importance_over_time)

출력은 폴드 × 특성 행렬이며, 여기에는 구별할 수 있는 세 가지 형태가 나타난다.

  1. 단조 감소 — 특성의 우위가 쇠퇴하고 있다. 제거 후보이거나, 시장 구조에 무엇이 바뀌었는지 조사할 후보가 된다.
  2. 추세 없는 높은 변동성 — 일부 국면에서 모델이 붙잡는 노이즈 특성이다. 이는 플래토 분석이 파라미터에 대해 정량화하는 신호를 특성에 적용한 것과 같다.
  3. 시장 국면 전환에 따른 계단식 변화 — 특정 폴드에서 중요도가 떨어진 뒤 계속 낮게 유지된다. 대개 alpha 쇠퇴가 아니라 거래소, 상장, 수수료 구조의 사건으로 추적된다.

주의할 점은 모델의 전체적인 확신도가 변할 때 평균 절대 SHAP를 폴드 간에 비교할 수 없다는 것이다. 모든 곳에서 0.5에 가깝게 예측하는 모델은 모든 특성의 기여도를 동시에 작게 만든다. 비교하기 전에 각 폴드의 중요도를 합이 1이 되도록 정규화하라. 그러면 확신도의 변화가 아니라 상대적 중요도의 변화를 읽을 수 있다.

트리가 유리한 이유: 간단히

비선형 시장 지형의 의사결정 트리

Grinsztajn, Oyallon, Varoquaux(NeurIPS 2022)는 45개의 표 형식 데이터셋에서 트리 앙상블과 딥러닝을 비교하고, 트리에 유리한 세 가지 구조적 특성을 분리해 냈다. 세 가지 모두 금융 데이터를 설명한다.

  1. 불규칙한 타깃 함수. 수익률은 매끄럽지 않다. 불연속, 국면 변화, 임계값 효과가 있다. 축에 맞춘 분할은 매끄러운 표면을 근사하지 않고도 이를 포착한다.
  2. 정보가 없는 특성. 알파 파이프라인은 수백 개의 후보를 만들지만 대부분은 노이즈다. 트리는 분할마다 선택하는 반면, 신경망은 모든 입력에 용량을 분산해 노이즈에 파라미터를 쓴다.
  3. 비회전 불변성. 거래량은 변동성과 서로 바꿀 수 없다. 신경망은 기본적으로 회전 불변이어서 특성의 선형 결합을 원래 특성과 동등하게 취급한다. 서로 다른 의미를 가진 특성에는 명백히 잘못된 가정이다.

이 절충의 실무적 측면인 데이터 크기, 지연 시간, 특성 공학 비용, 해석 가능성, 국면 적응에 대해서는 머신러닝을 이용한 스프레드 모델링에 이미 전체 의사결정 표가 있다.

특성 공학

시장 신호를 특성으로 변환하기

import pandas as pd
import numpy as np

def build_features(df: pd.DataFrame) -> pd.DataFrame:
    """
    Build trading features from OHLCV data.

    Expects columns: open, high, low, close, volume, timestamp
    """
    feat = pd.DataFrame(index=df.index)

    feat['return_1'] = df['close'].pct_change(1)
    feat['return_5'] = df['close'].pct_change(5)
    feat['return_15'] = df['close'].pct_change(15)
    feat['return_60'] = df['close'].pct_change(60)

    log_ret = np.log(df['close'] / df['close'].shift(1))
    feat['volatility_20'] = log_ret.rolling(20).std()
    feat['volatility_60'] = log_ret.rolling(60).std()
    feat['vol_ratio'] = feat['volatility_20'] / feat['volatility_60']

    feat['parkinson_vol'] = np.sqrt(
        (1 / (4 * np.log(2)))
        * (np.log(df['high'] / df['low']) ** 2).rolling(20).mean()
    )

    feat['volume_sma_ratio'] = df['volume'] / df['volume'].rolling(20).mean()
    feat['volume_std_20'] = df['volume'].rolling(20).std()
    feat['obv'] = (np.sign(df['close'].diff()) * df['volume']).cumsum()
    feat['obv_slope'] = feat['obv'].diff(5) / feat['obv'].shift(5)

    feat['high_low_range'] = (df['high'] - df['low']) / df['close']
    feat['close_position'] = (df['close'] - df['low']) / (df['high'] - df['low'])
    feat['gap'] = df['open'] / df['close'].shift(1) - 1

    delta = df['close'].diff()
    gain = delta.clip(lower=0).rolling(14).mean()
    loss = (-delta.clip(upper=0)).rolling(14).mean()
    feat['rsi_14'] = 100 - 100 / (1 + gain / loss)

    ema_12 = df['close'].ewm(span=12).mean()
    ema_26 = df['close'].ewm(span=26).mean()
    feat['macd'] = (ema_12 - ema_26) / df['close']
    feat['macd_signal'] = feat['macd'].ewm(span=9).mean()
    feat['macd_hist'] = feat['macd'] - feat['macd_signal']

    for window in [10, 20, 50]:
        sma = df['close'].rolling(window).mean()
        feat[f'distance_sma_{window}'] = (df['close'] - sma) / sma
        std = df['close'].rolling(window).std()
        feat[f'bb_position_{window}'] = (df['close'] - sma) / (2 * std)

    return feat

위 특성은 모두 설계상 인과적이다. rolling 및 expanding 연산만 사용하고 전체 표본 통계는 사용하지 않는다. 의도적인 선택이다. 전체 시계열 z-score는 이런 파이프라인에서 가장 흔한 누수이며, 보고된 Sharpe에 미치는 영향은 룩어헤드 편향 분류에서 측정했다.

금융 데이터에서 XGBoost의 하이퍼파라미터 범위

부스팅 표면을 둘러싼 하이퍼파라미터 궤도

목표는 표본 내 성능의 극대화가 아니라 표본 외 안정성의 극대화다. 정규화를 먼저, 복잡도는 그다음에 다룬다.

파라미터 일반적인 범위 목적
max_depth 3--7 상호작용 차수를 제한한다. 더 깊은 트리는 고차 상호작용을 모델링하지만 더 빠르게 과적합한다. 4에서 시작한다.
min_child_weight 5--100 리프의 최소 Hessian 합이다. scale_pos_weight나 목적 함수를 바꿀 때마다 다시 조정한다.
learning_rate 0.005--0.05 수축률이다. 낮은 값은 더 많은 트리를 필요로 하지만 일반화가 더 잘 된다.
subsample 0.5--0.8 트리별 행 샘플링이다. 무작위성을 더해 과적합을 줄인다.
colsample_bytree 0.5--0.8 트리별 열 샘플링이다. 상관된 특성이 많을 때 특히 중요하다.
gamma 0.5--5.0 분할에 필요한 최소 손실 감소량이다. 가지치기 임계값으로 작동한다.
reg_alpha(L1) 0.01--1.0 리프 가중치에 적용하는 L1이다. 희소성을 유도한다.
reg_lambda(L2) 0.1--10.0 리프 가중치에 적용하는 L2다. 리프 값이 커지는 것을 막는다.

검색 절차 자체인 TPE, study 지속성, 그리고 베이지안 검색이 좌표 하강보다 나은 이유는 Optuna와 좌표 하강을 참고하라. 여기서 사용하는 시도 횟수는 이후 Sharpe 보정에 반드시 반영해야 한다.

이 글에서 의도적으로 다른 글에 남겨 둔 것

연구 프런티어의 관문

그래디언트 부스팅 모델을 둘러싼 장치는 이미 측정 결과와 함께 설명되어 있다.

여기서 제시한 세 가지 추가 함정은 결과가 아니라 가설이다. 상장된 자산만 학습해 생기는 생존자 편향, 베타 잔차 수익률이 더 잘 작동할 수 있는 원시 수익률 타깃의 비정상성, 그리고 SHAP-폴드 진단을 자연스러운 드리프트 모니터로 활용하는 재학습 주기다.

결론

해결된 부스팅 의사결정 지형

수익률 방향 분류는 불균형 문제이고, 의사결정 임계값은 아무도 의도적으로 고르지 않았다. 세 가지 표준 해결책은 서로 바꿔 쓸 수 없다. scale_pos_weight는 한 줄이면 되지만 확률을 보정되지 않은 상태로 만들고 min_child_weight를 조용히 이동시킨다. 초점 손실에는 대부분의 구현이 근사하는 Hessian이 필요하다. 임계값 최적화는 모델을 그대로 둔 채 트레이딩에 중요한 제약인 정밀도를 제자리로 가져오지만, 훈련 이후의 데이터에서 폴드마다 임계값을 다시 학습할 때만 그렇다.

어느 방법이 이기는지는 데이터, 임계값, 비용 모델에 관한 실증적 질문이다. 위 표는 한 데이터셋에 대한 답이다. 자신의 데이터에서 실행해 보는 비용은 추측하며 튜닝하는 데 드는 시간보다 적다.


추가 읽을거리:

blog.disclaimer

Authors

Eugen Soloviov
Eugen Soloviov

Trading-systems engineer

Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.

Newsletter

시장에서 앞서 나가세요

뉴스레터를 구독하여 독점적인 AI 트레이딩 통찰력, 시장 분석 및 플랫폼 업데이트를 받아보세요.

귀하의 개인정보를 존중합니다. 언제든지 구독을 취소할 수 있습니다.