📝

Draft article

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

← กลับไปยังบทความ
August 25, 2026
อ่าน 5 นาที

XGBoost for Return Direction: Class Imbalance and Decision Thresholds

XGBoost for Return Direction: Class Imbalance and Decision Thresholds
#machine-learning
#xgboost
#gradient-boosting
#quant
#prediction

หากฝึกตัวจำแนกเพื่อทำนายว่าผลตอบแทนของชั่วโมงถัดไปเกิน +0.5% หรือไม่ นั่นไม่ใช่ปัญหาไบนารีที่สมดุล ในระบอบคริปโตที่เงียบ 70--80% ของแท่งอยู่ต่ำกว่าเกณฑ์ โมเดลจึงได้ความแม่นยำ 75% เพียงไม่ทำนายผลบวกเลย ความแม่นยำไม่มีประโยชน์ที่นี่ เช่นเดียวกับขอบเขตตัดสินใจเริ่มต้น 0.5 ที่ไม่มีใครเลือกโดยเจตนา — มันเป็นเพียงค่าที่ predict() ใช้

มีการตอบสนองมาตรฐานสามประการ: ปรับน้ำหนักการสูญเสียด้วย scale_pos_weight แทนที่การสูญเสียด้วยการสูญเสียโฟกัส หรือปล่อยการสูญเสียไว้ตามลำพังและย้ายเกณฑ์การตัดสินใจตามความเป็นจริง พวกเขามักจะนำเสนอว่าใช้แทนกันได้ พวกเขาไม่ได้ พวกเขาสร้างการแลกเปลี่ยนที่แม่นยำ/การเรียกคืนที่แตกต่างกัน และที่สำคัญกว่านั้นคือ PnL หลังต้นทุนที่แตกต่างกัน เนื่องจากการซื้อขายให้ความสำคัญกับความแม่นยำมากกว่าการเรียกคืน — การซื้อขายที่พลาดไปไม่มีค่าใช้จ่ายใดๆ การซื้อขายที่ไม่ดีมีค่าใช้จ่ายส่วนต่างบวกค่าธรรมเนียม

บทความนี้จะวัดทั้งสามข้อมูลบนข้อมูล BTC และ ETH รายชั่วโมงแบบ Walk-forward ที่ถูกล้างข้อมูลเดียวกัน จากนั้นจะครอบคลุมสองสิ่งที่ตามมา: เหตุใด min_child_weight จึงเป็นเกณฑ์ Hessian แทนที่จะเป็นจำนวนแถว (ซึ่งเป็นสาเหตุที่ค่า "ถูกต้อง" เปลี่ยนไปเมื่อคุณชั่งน้ำหนักการสูญเสียใหม่) และจุดที่ XGBoost, LightGBM และ CatBoost แตกต่างกันจริง ๆ

วิธีการ ความแม่นยำ จำ การค้า PnL หลังต้นทุน กิ่วชาร์ป
เส้นฐาน (ไม่มีการแก้ไข thr=0.5)
scale_pos_weight = n_neg/n_pos
การสูญเสียโฟกัส (γ=2, α=0.25)
การเพิ่มประสิทธิภาพเกณฑ์ (ความแม่นยำขั้นต่ำ 0.55)

พับเท่ากัน คุณลักษณะเดียวกัน รูปแบบต้นทุนเดียวกันสำหรับทุกแถว Sharpe ถูกกิ่วตามจำนวนการกำหนดค่าที่พยายามต่อ Sharpe ที่ deflate และการทดสอบหลายครั้ง ต้นทุนเป็นไปตาม แบบจำลองการเลื่อนไหลและต้นทุน

การจัดการความไม่สมดุลของคลาสในการทำนายผลตอบแทน

ช่องสัญญาณการทำนายผลตอบแทนที่สมดุล

วิธีที่ 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 จะแย่กว่าการจัดประเภทตัวอย่างเชิงลบผิดครั้ง

ผลข้างเคียงนั้นพลาดได้ง่าย: มันยังปรับขนาด Hessians ด้วย เนื่องจาก min_child_weight เป็นเกณฑ์สำหรับ ผลรวมของ Hessians ในใบไม้ (ดูด้านล่าง) การชั่งน้ำหนักการสูญเสียใหม่จะเปลี่ยนความรุนแรงของการแยกต้นไม้อย่างรุนแรง min_child_weight ที่ปรับที่ scale_pos_weight=1 จะไม่มีความหมายเหมือนกันใน scale_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, ...)

objective แบบกำหนดเองต้องมี gradient และ Hessian ที่เป็นบวก เพื่อให้สูตรน้ำหนักใบไม้เป็นขั้นตอนลงจริง อนุพันธ์อันดับสองที่แน่นอนของ focal loss ไม่เป็นบวกทุกจุด จึงมีการใช้ค่าประมาณ — แต่ค่าประมาณที่ต่างกันด้วยตัวคูณสเกลจะเปลี่ยนขนาดขั้น และผ่านผลรวม Hessian ก็เปลี่ยนสิ่งที่ min_child_weight ตัดทิ้ง ตารางเปรียบเทียบเท่านั้นที่จะบอกว่าต้นทุนในทางปฏิบัติมีหรือไม่

วิธีที่ 3: การปรับเกณฑ์ให้เหมาะสม

ปล่อย loss ไว้ ฝึกโมเดลที่ผ่าน calibration แล้วเลื่อนเกณฑ์ตัดสินใจบนชุด validation:

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]

ขีดล่างของ precision คือสิ่งสำคัญต่อการเทรด การเพิ่ม F1 สูงสุดเพียงอย่างเดียวแลก precision กับ recall ซึ่งในแง่ PnL หมายถึงการเทรดชายขอบมากขึ้นและจ่าย spread ทุกครั้ง การกำหนด precision แล้วเพิ่ม recall ภายใต้ข้อจำกัดแปลเป็น "เทรดน้อยลง แต่ถูกต้องเมื่อเทรด"

มีกฎสองข้อเพื่อรักษาความซื่อสัตย์: fit เกณฑ์บนข้อมูล validation ที่อยู่หลังหน้าต่างฝึกตามลำดับเวลา และ fit ใหม่ต่อพับ เกณฑ์ที่เลือกครั้งเดียวจากตัวอย่างทั้งหมดคือการรั่วไหลแบบ look-ahead ตามที่จัดหมวดไว้ใน อนุกรมวิธานของ look-ahead bias การ fit ใหม่ต่อพับยังเป็นตัววินิจฉัยฟรี — หากเกณฑ์ที่เหมาะสมกระโดดไปมาระหว่างพับ calibration ไม่เสถียรและเกณฑ์กำลัง fit สัญญาณรบกวน

ทำไม min_child_weight จึงเป็นผลรวม Hessian ไม่ใช่จำนวนแถว

Curvature supported tree split

Gradient boosting สร้าง ensemble แบบบวก ที่ขั้น tt จะเพิ่มต้นไม้ ftf_t ซึ่งลด objective ที่มี regularization โดย XGBoost ประมาณค่าด้วย Taylor expansion อันดับสอง:

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 และลดลงสู่ศูนย์เมื่อ prediction เข้าใกล้ความแน่นอน ดังนั้นใบไม้ที่เต็มไปด้วยแท่งซึ่งจำแนกอย่างมั่นใจจะมีผลรวม Hessian เล็กและถูก prune ขณะที่ใบไม้ที่มีแท่งคลุมเครือจริงเพียงไม่กี่แท่งอาจอยู่รอด

สำหรับ label การเงินที่มีสัญญาณรบกวน นี่คือพฤติกรรมที่ต้องการ และอธิบายผลในทางปฏิบัติ:

  • การเพิ่ม min_child_weight จะ prune ใบไม้ที่อาศัย observation ไม่แน่นอน จำนวนน้อย — ซึ่งเป็นกลุ่มที่มีโอกาส fit noise มากที่สุด นี่เป็นเครื่องมือที่คมกว่าการนับแถวแบบ min_child_samples
  • สิ่งใดก็ตามที่ rescale loss จะ rescale Hessian ด้วย scale_pos_weight, objective แบบกำหนดเอง และ sample weight ล้วนเปลี่ยน min_child_weight ที่มีผล แม้ตัวเลขที่พิมพ์จะไม่เปลี่ยน
  • เมื่อ boosting ดำเนินต่อและ prediction คมขึ้น Hessian จะหดตัวทั่วทั้งโมเดล ดังนั้น min_child_weight คงที่จะ prune รุนแรงขึ้นในรอบหลัง นี่คือผล annealing ในตัว และเป็นเหตุที่ learning rate ต่ำกับต้นไม้จำนวนมากทำงานต่างจากขั้นใหญ่จำนวนน้อย

min_child_samples ของ LightGBM คือจำนวนแถว จึงเป็นพารามิเตอร์ที่แตกต่างจริงแม้ชื่อคล้ายกัน ส่วนตัวเทียบเท่า Hessian คือ min_sum_hessian_in_leaf การย้าย configuration ระหว่างสองไลบรารีด้วยการจับคู่ชื่อเป็นวิธีที่ทำให้เปลี่ยนโมเดลโดยไม่ตั้งใจได้บ่อย

XGBoost เทียบกับ LightGBM และ CatBoost: ความแตกต่างทางวิศวกรรม

Three gradient boosting architectures

ทั้งสามไลบรารีใช้ gradient boosted decision tree ความแตกต่างอยู่ที่การสร้างต้นไม้ และสิ่งนี้เองที่ปรากฏในเวลาฝึกและคะแนนนอกตัวอย่าง

XGBoost เติบโตแบบ level-wise (breadth-first): แบ่งใบไม้ทั้งหมดที่ความลึกหนึ่งก่อนลงลึกกว่า ทำให้ต้นไม้สมดุล ทำให้ max_depth เป็นตัวควบคุมความซับซ้อนที่มีความหมาย และปรับแต่งได้คาดเดาง่ายกว่า แต่ยังเสียงานกับการแบ่งใบไม้ที่เหลือ loss ให้ลดได้น้อย

LightGBM เติบโตแบบ leaf-wise: แบ่งใบไม้ที่ให้การลด loss มากที่สุดไม่ว่ามันอยู่ที่ใด การแบ่งน้อยครั้งก็ได้ training loss เท่าเดิม แต่ต้นไม้จะลึกและไม่สมดุล ดังนั้น max_depth ไม่ใช่ปุ่มควบคุมที่เหมาะ — num_leaves ต่างหาก มีเคล็ดลับอีกสองอย่างที่ทำให้เร็ว:

  • GOSS (Gradient-based One-Side Sampling) เก็บ instance ที่มี gradient สูงทั้งหมดและสุ่มตัวอย่าง instance ที่มี gradient ต่ำ โดยเพิ่มน้ำหนักผู้รอดชีวิตเพื่อให้ค่า gradient ไม่ลำเอียง ในข้อมูลการเงิน instance ที่ gradient สูงคือแท่งที่โมเดลผิดอยู่ — และเป็นจุดที่ noise ของ label อยู่ด้วย ดังนั้น GOSS จึงสุ่มตัวอย่างตรงบริเวณที่ noise แย่ที่สุด ควรตรวจสอบเทียบกับ subsampling ธรรมดาแทนการสันนิษฐาน
  • EFB (Exclusive Feature Bundling) บรรจุฟีเจอร์ sparse ที่ไม่เกิดร่วมกันลงในฟีเจอร์เดียวใน bin-space มีประสิทธิภาพกับ one-hot encoding แต่แทบไม่มีประโยชน์กับฟีเจอร์ต่อเนื่องหนาแน่น ซึ่งเป็นส่วนใหญ่ของเมทริกซ์ฟีเจอร์เทคนิค

CatBoost เติบโตเป็นต้นไม้ symmetric (oblivious): ทุก node ที่ความลึกเดียวกันใช้เงื่อนไขแบ่งเดียวกัน นี่เป็น regularizer ที่แข็งแรงและทำให้ inference เร็วมาก — ต้นไม้กลายเป็นการค้นดัชนี — แลกกับความสามารถในการแสดงออกต่อต้นไม้ กลไกเด่นสองอย่างคือ:

  • Ordered boosting boosting มาตรฐานคำนวณ residual ของตัวอย่างด้วยโมเดลที่ฝึกบนตัวอย่างนั้น ทำให้ residual มี bias — หรือ "prediction shift" CatBoost ประมาณ residual ของแต่ละตัวอย่างด้วยโมเดลที่ fit เฉพาะตัวอย่างก่อนหน้าใน permutation สุ่ม โครงสร้างนี้เหมาะกับการคิดแบบ time series และสำคัญที่สุดเมื่อข้อมูลมีจำกัด
  • Ordered target statistics สำหรับ categorical encoding คำนวณ target statistics จากตัวอย่างก่อนหน้าเท่านั้น จึงหลีกเลี่ยง target leakage ที่ mean-encoding แบบง่ายนำเข้ามา นี่คือเหตุผลที่ซื่อสัตย์ในการใช้ CatBoost เมื่อชุดฟีเจอร์มี label ของ exchange ระดับ asset หรือ regime
Property XGBoost LightGBM CatBoost
Tree growth Level-wise Leaf-wise Symmetric (oblivious)
Complexity knob max_depth num_leaves depth
Leaf-size guard min_child_weight (Hessian) min_child_samples (rows) min_data_in_leaf (rows)
Categorical features Manual encoding Basic support Native, ordered TS
Regularization L1/L2 + gamma L1/L2 + num_leaves L2 + random strength
Custom loss Flexible Flexible Somewhat limited
Train time, this dataset
OOS log loss, same folds

แถวเชิงคุณภาพเป็นข้อเท็จจริงของไลบรารี สองแถวสุดท้ายเท่านั้นที่ตอบว่า "ควรใช้ตัวไหน" และต้องมาจากการรันบนข้อมูลของคุณเอง — ความแตกต่างระหว่าง implementation ที่ปรับดีแล้วมักเล็กพอที่รูปร่างชุดข้อมูลจะเป็นตัวตัดสิน

การเปลี่ยนไลบรารีส่วนใหญ่เป็นเพียงการเปลี่ยนชื่อ ใช้ฟังก์ชันฝึกหนึ่งตัวและระบุพารามิเตอร์ที่แตกต่างแทนการทำซ้ำ:

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
Concept XGBoost LightGBM CatBoost
Tree count n_estimators n_estimators iterations
L2 penalty reg_lambda reg_lambda l2_leaf_reg
Column sampling colsample_bytree colsample_bytree rsm
Class imbalance scale_pos_weight scale_pos_weight auto_class_weights='Balanced'
Early stopping early_stopping_rounds lgb.early_stopping() callback early_stopping_rounds

ความสำคัญของ SHAP ข้ามพับในฐานะตัวตรวจจับการเสื่อมของอัลฟา

Feature attribution fading across folds

บล็อกนี้ครอบคลุม SHAP บนโมเดลการเพิ่มเกรเดียนต์ แล้ว — TreeExplainer กราฟสรุป และวิธีอ่านกราฟ สิ่งที่ยังไม่ได้กล่าวถึงคือการใช้ SHAP ตามเวลา: ใช้ตัวอธิบายหนึ่งตัวต่อพับ walk-forward และติดตามค่า attribution สัมบูรณ์เฉลี่ยของแต่ละฟีเจอร์เมื่อเวลาผ่านไป

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. ความแปรปรวนสูง ไม่มีแนวโน้ม — ฟีเจอร์เป็นสัญญาณรบกวนที่โมเดลเกาะในบางระบอบ นี่คือสัญญาณเดียวกับที่ การวิเคราะห์ plateau วัดสำหรับพารามิเตอร์ แต่นำมาใช้กับฟีเจอร์
  3. การเปลี่ยนขั้นเมื่อเปลี่ยนระบอบ — ความสำคัญลดลงที่พับหนึ่งและยังต่ำต่อไป มักติดตามได้ถึงเหตุการณ์ของตลาด การจดทะเบียน หรือโครงสร้างค่าธรรมเนียม มากกว่าการเสื่อมของอัลฟา

กับดักคือค่า SHAP สัมบูรณ์เฉลี่ยเปรียบเทียบข้ามพับไม่ได้เมื่อความมั่นใจรวมของโมเดลเปลี่ยน: โมเดลที่ทำนายเข้าใกล้ 0.5 ทุกแห่งจะให้ attribution เล็กลงสำหรับทุกฟีเจอร์พร้อมกัน ทำให้ความสำคัญของแต่ละพับรวมกันเป็น 1 ก่อนเปรียบเทียบ เพื่ออ่านการเปลี่ยนแปลงของความสำคัญ สัมพัทธ์ แทนการเปลี่ยนแปลงของความมั่นใจ

ทำไมต้องใช้ต้นไม้โดยสรุป

Decision tree in nonlinear market landscape

Grinsztajn, Oyallon และ Varoquaux (NeurIPS 2022) ทดสอบกลุ่มต้นไม้เทียบกับ deep learning บนชุดข้อมูลตาราง 45 ชุด และแยกคุณสมบัติเชิงโครงสร้างสามข้อที่เอื้อต่อต้นไม้ — ทั้งสามข้ออธิบายข้อมูลการเงินได้:

  1. ฟังก์ชันเป้าหมายไม่สม่ำเสมอ ผลตอบแทนไม่ราบเรียบ มีจุดไม่ต่อเนื่อง การเปลี่ยนระบอบ และผลของเกณฑ์ การแบ่งตามแกนจับสิ่งเหล่านี้ได้โดยไม่ต้องประมาณพื้นผิวเรียบ
  2. ฟีเจอร์ที่ไม่มีข้อมูล สายงานอัลฟาสร้างตัวเลือกหลายร้อยตัวและส่วนใหญ่เป็นสัญญาณรบกวน ต้นไม้เลือกในแต่ละการแบ่ง ส่วนโครงข่ายประสาทกระจายความสามารถไปยังอินพุตทั้งหมดและใช้พารามิเตอร์กับสัญญาณรบกวน
  3. ไม่คงรูปต่อการหมุน ปริมาณไม่สามารถแทนที่ด้วยความผันผวนได้ โครงข่ายประสาทมีความคงรูปต่อการหมุนโดยปริยาย จึงถือว่าการผสมเชิงเส้นของฟีเจอร์เทียบเท่ากับฟีเจอร์เดิม — ซึ่งผิดสำหรับฟีเจอร์ที่มีความหมายเฉพาะต่างกัน

สำหรับด้านปฏิบัติของการแลกเปลี่ยนนี้ — ขนาดข้อมูล เวลาแฝง ความพยายามด้านวิศวกรรมฟีเจอร์ การตีความ และการปรับตามระบอบ — บล็อกมีตารางตัดสินใจฉบับเต็มใน การสร้างแบบจำลอง spread ด้วยแมชชีนเลิร์นนิง แล้ว

วิศวกรรมฟีเจอร์

Market signals transforming into features

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 ที่รายงานไว้ถูกวัดใน อนุกรมวิธานของ look-ahead bias

ช่วงไฮเปอร์พารามิเตอร์ของ XGBoost สำหรับข้อมูลการเงิน

Hyperparameter orbits around boosted surface

เป้าหมายไม่ใช่ประสิทธิภาพสูงสุดในตัวอย่าง แต่คือเสถียรภาพสูงสุดนอกตัวอย่าง การทำ regularization มาก่อน ความซับซ้อนมาทีหลัง:

Parameter Typical range Purpose
max_depth 3--7 จำกัดลำดับของปฏิสัมพันธ์ ต้นไม้ลึกจำลองปฏิสัมพันธ์ลำดับสูงแต่ overfit เร็วกว่า เริ่มที่ 4
min_child_weight 5--100 ผลรวม Hessian ขั้นต่ำในใบไม้ ปรับใหม่ทุกครั้งที่เปลี่ยน scale_pos_weight หรือ objective
learning_rate 0.005--0.05 การหดตัว ค่าต่ำต้องใช้ต้นไม้มากขึ้นแต่ generalize ได้ดีกว่า
subsample 0.5--0.8 สุ่มแถวต่อต้นไม้ เพิ่มความสุ่มและลด overfitting
colsample_bytree 0.5--0.8 สุ่มคอลัมน์ต่อต้นไม้ สำคัญเมื่อมีฟีเจอร์ที่สัมพันธ์กันมาก
gamma 0.5--5.0 การลด loss ขั้นต่ำสำหรับการแบ่ง ทำหน้าที่เป็นเกณฑ์ pruning
reg_alpha (L1) 0.01--1.0 L1 บนน้ำหนักใบไม้ ส่งเสริมความเบาบาง
reg_lambda (L2) 0.1--10.0 L2 บนน้ำหนักใบไม้ ป้องกันค่าใบไม้ขนาดใหญ่

สำหรับขั้นตอนการค้นหาเอง — TPE การคงอยู่ของ study และเหตุผลที่การค้นหาแบบเบย์ชนะ coordinate descent — ดู Optuna เทียบกับ coordinate descent จำนวน trial ที่ใช้ที่นั่นต้องนำไปใช้ในการลด Sharpe ภายหลังด้วย

สิ่งที่บทความนี้ตั้งใจฝากไว้ให้โพสต์อื่น

Research frontier gates

โครงสร้างรอบโมเดล gradient boosting มีการอธิบายพร้อมการวัดผลไว้แล้วที่นี่:

ยังมีข้อผิดพลาดอีกสามข้อที่ระบุไว้เป็นสมมติฐาน ไม่ใช่ผลลัพธ์: survivorship bias จากการฝึกเฉพาะสินทรัพย์ที่จดทะเบียน; ความไม่อยู่กับที่ของเป้าหมายผลตอบแทนดิบ ซึ่งผลตอบแทนที่เหลือจาก beta อาจทำได้ดีกว่า; และจังหวะการฝึกใหม่ ซึ่งการวินิจฉัย SHAP ข้ามพับเป็นตัวติดตาม drift ตามธรรมชาติ

บทสรุป

Resolved boosted decision landscape

การจำแนกทิศทางผลตอบแทนเป็นปัญหาที่คลาสไม่สมดุลและมีเกณฑ์ตัดสินใจที่ไม่มีใครเลือก การแก้มาตรฐานสามวิธีใช้แทนกันไม่ได้ scale_pos_weight ใช้เพียงบรรทัดเดียว แต่ทำให้ความน่าจะเป็นขาด calibration และเลื่อน min_child_weight อย่างเงียบๆ Focal loss ต้องใช้ Hessian ที่การใช้งานส่วนใหญ่ประมาณค่า ส่วนการปรับเกณฑ์ปล่อยโมเดลไว้และวางข้อจำกัดที่สำคัญต่อการเทรด — ความแม่นยำ — ไว้ในที่ที่ควรอยู่ แต่ต้องปรับเกณฑ์ใหม่ทุกพับด้วยข้อมูลที่เกิดหลังการฝึก

วิธีใดชนะเป็นคำถามเชิงประจักษ์เกี่ยวกับข้อมูล เกณฑ์ และโมเดลต้นทุนของคุณ ตารางด้านบนคือคำตอบสำหรับชุดข้อมูลหนึ่งชุด การรันกับข้อมูลของคุณมีต้นทุนน้อยกว่าเวลาปรับแต่งที่ใช้เดา


อ่านเพิ่มเติม:

ข้อจำกัดความรับผิดชอบ: ข้อมูลที่ให้ไว้ในบทความนี้มีไว้เพื่อการศึกษาและให้ข้อมูลเท่านั้น และไม่ถือเป็นคำแนะนำทางการเงิน การลงทุน หรือการเทรด การเทรดสกุลเงินดิจิทัลมีความเสี่ยงสูงที่จะขาดทุน

ผู้เขียน

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 เฉพาะ การวิเคราะห์ตลาด และการอัปเดตแพลตฟอร์ม

เราเคารพความเป็นส่วนตัวของคุณ ยกเลิกการสมัครได้ทุกเมื่อ