From 8f834a18903f09705b91d74b1777b2389a184382 Mon Sep 17 00:00:00 2001 From: 21in7 Date: Sun, 1 Mar 2026 18:30:01 +0900 Subject: [PATCH] feat: implement training and deployment pipeline for LightGBM model on Mac to LXC - Added comprehensive plans for training a LightGBM model on M4 Mac Mini and deploying it to an LXC container. - Created scripts for model training, deployment, and a full pipeline execution. - Enhanced model transfer with error handling and logging for better tracking. - Introduced profiling for training time analysis and dataset generation optimization. Made-with: Cursor --- .../2026-03-01-m4-accelerated-training.md | 635 ++++++++++++++++++ .../2026-03-01-train-on-mac-deploy-to-lxc.md | 317 +++++++++ models/training_log.json | 8 + scripts/deploy_model.sh | 59 ++ scripts/train_and_deploy.sh | 34 + scripts/train_model.py | 2 +- 6 files changed, 1054 insertions(+), 1 deletion(-) create mode 100644 docs/plans/2026-03-01-m4-accelerated-training.md create mode 100644 docs/plans/2026-03-01-train-on-mac-deploy-to-lxc.md create mode 100644 models/training_log.json create mode 100755 scripts/deploy_model.sh create mode 100755 scripts/train_and_deploy.sh diff --git a/docs/plans/2026-03-01-m4-accelerated-training.md b/docs/plans/2026-03-01-m4-accelerated-training.md new file mode 100644 index 0000000..18dd16e --- /dev/null +++ b/docs/plans/2026-03-01-m4-accelerated-training.md @@ -0,0 +1,635 @@ +# M4 Mac Mini 가속 학습 구현 계획 + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** M4 맥미니의 GPU(Metal/MPS)를 활용해 모델 학습 속도를 높이고, Neural Engine 활용 가능 여부를 검토한다. + +**Architecture:** 현재 LightGBM CPU 학습 파이프라인을 유지하면서, 데이터셋 생성 단계(병렬 CPU 연산)와 LightGBM 학습 단계를 각각 최적화한다. LightGBM은 Apple Silicon GPU를 공식 지원하지 않으므로, (1) MetalGBM 실험적 대체, (2) PyTorch MPS 기반 신경망 필터 추가, (3) 현재 CPU 파이프라인 최적화 세 가지 경로를 단계별로 시도한다. + +**Tech Stack:** Python 3.13, LightGBM 4.6, MetalGBM(실험), PyTorch(MPS), Apple MLX, scikit-learn 1.8 + +--- + +## 배경 및 제약사항 분석 + +### M4 맥미니 하드웨어 구조 +- **CPU**: 10코어 (P코어 4 + E코어 6) +- **GPU**: 10코어 통합 GPU (Metal 지원) +- **Neural Engine (NPU)**: 38 TOPS — 행렬 연산 특화, Apple 전용 API로만 접근 가능 +- **통합 메모리**: CPU/GPU/NPU가 동일 메모리 공유 → 데이터 복사 오버헤드 없음 + +### 현재 학습 파이프라인 병목 분석 +``` +[1단계] 데이터셋 생성: multiprocessing.Pool → CPU 병렬 + - _process_index(): 각 캔들에서 Indicators 계산 + 피처 추출 + - 약 129,000개 인덱스 처리 (90일 × 1440분) + - 현재 병목: Python GIL 우회는 됐지만 pickle 직렬화 오버헤드 큼 + +[2단계] LightGBM 학습: CPU 전용 + - n_estimators=300, 샘플 수 ~수천 개 + - 실제 학습 시간은 짧음 (수초~수십초) + - GPU 가속 효과 미미할 가능성 높음 +``` + +### 각 가속 방법의 현실적 평가 + +| 방법 | 효과 | 난이도 | 권장 여부 | +|------|------|--------|-----------| +| Neural Engine 직접 사용 | ❌ 불가 (Apple 내부 전용) | - | 불가 | +| LightGBM GPU (Metal) | ❌ 공식 미지원 | 높음 | 비권장 | +| MetalGBM | ⚠️ 실험적 (2025.11 신생) | 중간 | 실험 가능 | +| PyTorch MPS 신경망 | ✅ 가능, 소규모 모델은 CPU보다 느릴 수 있음 | 중간 | 조건부 권장 | +| Apple MLX 신경망 | ✅ Apple Silicon 최적화 | 중간 | 권장 | +| CPU 파이프라인 최적화 | ✅ 즉각 효과 | 낮음 | **최우선 권장** | + +> **핵심 결론**: 현재 학습 샘플 수(수천 개)와 피처 수(13개)에서는 LightGBM 자체 학습 시간이 매우 짧다. 실제 병목은 **데이터셋 생성(1단계)** 이며, 이를 먼저 최적화하는 것이 가장 효과적이다. GPU/NPU 가속은 신경망 모델로 전환 시 의미가 있다. + +--- + +## Task 1: 현재 학습 시간 프로파일링 + +**Files:** +- Create: `scripts/profile_training.py` + +**Step 1: 프로파일링 스크립트 작성** + +```python +""" +학습 파이프라인 각 단계의 소요 시간을 측정한다. +사용법: python scripts/profile_training.py --data data/xrpusdt_1m.parquet +""" +import sys +from pathlib import Path +sys.path.insert(0, str(Path(__file__).parent.parent)) + +import time +import argparse +import pandas as pd +from scripts.train_model import generate_dataset, _cgroup_cpu_count + +def profile(data_path: str): + print(f"데이터 로드: {data_path}") + df = pd.read_parquet(data_path) + print(f"캔들 수: {len(df)}") + + workers = max(1, _cgroup_cpu_count() - 1) + print(f"사용 코어: {workers}") + + t0 = time.perf_counter() + dataset = generate_dataset(df) + t1 = time.perf_counter() + print(f"\n[결과] 데이터셋 생성: {t1-t0:.1f}초, 샘플 {len(dataset)}개") + + import lightgbm as lgb + from sklearn.model_selection import train_test_split + from src.ml_features import FEATURE_COLS + X = dataset[FEATURE_COLS] + y = dataset["label"] + X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=42) + + model = lgb.LGBMClassifier( + n_estimators=300, learning_rate=0.05, num_leaves=31, + min_child_samples=20, subsample=0.8, colsample_bytree=0.8, + class_weight="balanced", random_state=42, verbose=-1, + ) + t2 = time.perf_counter() + model.fit(X_train, y_train) + t3 = time.perf_counter() + print(f"[결과] LightGBM 학습: {t3-t2:.1f}초") + print(f"[결과] 전체: {t3-t0:.1f}초") + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--data", default="data/xrpusdt_1m.parquet") + args = parser.parse_args() + profile(args.data) +``` + +**Step 2: 프로파일링 실행** + +```bash +python scripts/profile_training.py --data data/xrpusdt_1m.parquet +``` + +예상 출력: +``` +[결과] 데이터셋 생성: XX.X초, 샘플 XXXX개 +[결과] LightGBM 학습: X.X초 +[결과] 전체: XX.X초 +``` + +→ 데이터셋 생성이 전체의 90% 이상을 차지하면 Task 2로 진행 +→ LightGBM 학습이 병목이면 Task 4(MetalGBM)로 진행 + +**Step 3: 커밋** + +```bash +git add scripts/profile_training.py +git commit -m "feat: add training pipeline profiler" +``` + +--- + +## Task 2: 데이터셋 생성 최적화 (CPU, 즉각 효과) + +현재 `multiprocessing.Pool`의 pickle 직렬화 오버헤드를 줄이고, numpy 벡터화로 대체한다. + +**Files:** +- Modify: `scripts/train_model.py` +- Create: `tests/test_train_model_perf.py` + +**Step 1: 실패 테스트 작성** + +```python +# tests/test_train_model_perf.py +import time +import pandas as pd +import pytest +from scripts.train_model import generate_dataset + +@pytest.fixture +def sample_df(): + return pd.read_parquet("data/xrpusdt_1m.parquet").iloc[:5000] + +def test_dataset_generation_speed(sample_df): + """5000개 캔들에서 데이터셋 생성이 30초 이내여야 한다.""" + t0 = time.perf_counter() + dataset = generate_dataset(sample_df) + elapsed = time.perf_counter() - t0 + assert elapsed < 30.0, f"너무 느림: {elapsed:.1f}초" + assert len(dataset) > 0 +``` + +**Step 2: 테스트 실행 (실패 확인)** + +```bash +pytest tests/test_train_model_perf.py -v +``` + +**Step 3: `train_model.py`에 `n_jobs` 자동 감지 개선 및 chunksize 튜닝** + +`scripts/train_model.py`의 `generate_dataset` 함수에서: + +```python +# 기존 +workers = n_jobs or max(1, _cgroup_cpu_count() - 1) +chunk = max(1, len(task_args) // (workers * 10)) + +# 변경: M4의 P코어/E코어 혼합을 고려해 worker 수를 P코어 수로 제한 +# M4 mini: 4 P코어 + 6 E코어 = 10코어. 실제 병렬 처리는 P코어 기준이 효율적 +workers = n_jobs or min(max(1, _cgroup_cpu_count() - 1), 8) +# chunksize를 크게 잡아 IPC 오버헤드 감소 +chunk = max(100, len(task_args) // workers) +``` + +`scripts/train_model.py`의 `generate_dataset` 함수 내 두 줄을 수정: + +```python +workers = n_jobs or min(max(1, _cgroup_cpu_count() - 1), 8) +# ... +chunk = max(100, len(task_args) // workers) +``` + +**Step 4: 테스트 재실행 (통과 확인)** + +```bash +pytest tests/test_train_model_perf.py -v +``` + +**Step 5: 커밋** + +```bash +git add scripts/train_model.py tests/test_train_model_perf.py +git commit -m "perf: tune multiprocessing chunksize for M4 P-core efficiency" +``` + +--- + +## Task 3: Apple MLX 기반 신경망 필터 실험 (GPU/Neural Engine 활용) + +LightGBM을 대체하거나 앙상블할 수 있는 MLX 기반 경량 신경망을 구현한다. MLX는 Apple Silicon의 통합 GPU와 Neural Engine을 자동으로 활용한다. + +**Files:** +- Create: `src/mlx_filter.py` +- Create: `scripts/train_mlx_model.py` +- Create: `tests/test_mlx_filter.py` + +**Step 1: MLX 설치 확인 및 설치** + +```bash +# venv 활성화 후 +pip install mlx +python -c "import mlx.core as mx; print('MLX device:', mx.default_device())" +``` + +예상 출력: `MLX device: Device(gpu, 0)` (GPU 자동 사용) + +**Step 2: requirements.txt에 mlx 추가** + +`requirements.txt`에 다음 줄 추가: +``` +mlx>=0.22.0 +``` + +**Step 3: 실패 테스트 작성** + +```python +# tests/test_mlx_filter.py +import pytest +import numpy as np + +def test_mlx_available(): + """MLX가 설치되어 GPU 디바이스를 사용할 수 있어야 한다.""" + import mlx.core as mx + device = mx.default_device() + assert device is not None + +def test_mlx_filter_predict_shape(): + """MLXFilter가 (N,) 형태의 확률값을 반환해야 한다.""" + from src.mlx_filter import MLXFilter + import pandas as pd + X = pd.DataFrame({ + "rsi": [50.0], "macd_hist": [0.1], "bb_pct": [0.5], + "ema_align": [1.0], "stoch_k": [50.0], "stoch_d": [50.0], + "atr_pct": [0.01], "vol_ratio": [1.0], + "ret_1": [0.001], "ret_3": [0.002], "ret_5": [0.003], + "signal_strength": [3.0], "side": [1.0], + }) + model = MLXFilter(input_dim=13, hidden_dim=64) + proba = model.predict_proba(X) + assert proba.shape == (1,) + assert 0.0 <= proba[0] <= 1.0 +``` + +**Step 4: 테스트 실행 (실패 확인)** + +```bash +pytest tests/test_mlx_filter.py -v +``` + +**Step 5: MLXFilter 구현** + +```python +# src/mlx_filter.py +""" +Apple MLX 기반 경량 신경망 필터. +M4의 통합 GPU와 Neural Engine을 자동으로 활용한다. +""" +import numpy as np +import pandas as pd +import mlx.core as mx +import mlx.nn as nn +import mlx.optimizers as optim +from pathlib import Path + +from src.ml_features import FEATURE_COLS + + +class _Net(nn.Module): + """2층 MLP 분류기.""" + def __init__(self, input_dim: int, hidden_dim: int): + super().__init__() + self.fc1 = nn.Linear(input_dim, hidden_dim) + self.fc2 = nn.Linear(hidden_dim, hidden_dim // 2) + self.fc3 = nn.Linear(hidden_dim // 2, 1) + self.dropout = nn.Dropout(p=0.2) + + def __call__(self, x: mx.array) -> mx.array: + x = nn.relu(self.fc1(x)) + x = self.dropout(x) + x = nn.relu(self.fc2(x)) + return self.fc3(x).squeeze(-1) + + +class MLXFilter: + """scikit-learn 호환 인터페이스를 제공하는 MLX 신경망 필터.""" + + def __init__(self, input_dim: int = 13, hidden_dim: int = 64, + lr: float = 1e-3, epochs: int = 50, batch_size: int = 256): + self.input_dim = input_dim + self.hidden_dim = hidden_dim + self.lr = lr + self.epochs = epochs + self.batch_size = batch_size + self.model = _Net(input_dim, hidden_dim) + self._trained = False + + def fit(self, X: pd.DataFrame, y: pd.Series) -> "MLXFilter": + X_np = X[FEATURE_COLS].values.astype(np.float32) + y_np = y.values.astype(np.float32) + + # 정규화 파라미터 저장 + self._mean = X_np.mean(axis=0) + self._std = X_np.std(axis=0) + 1e-8 + X_np = (X_np - self._mean) / self._std + + optimizer = optim.Adam(learning_rate=self.lr) + + def loss_fn(model, x, y): + logits = model(x) + return nn.losses.binary_cross_entropy(logits, y, with_logits=True).mean() + + loss_and_grad = nn.value_and_grad(self.model, loss_fn) + + n = len(X_np) + for epoch in range(self.epochs): + idx = np.random.permutation(n) + epoch_loss = 0.0 + steps = 0 + for start in range(0, n, self.batch_size): + batch_idx = idx[start:start + self.batch_size] + x_batch = mx.array(X_np[batch_idx]) + y_batch = mx.array(y_np[batch_idx]) + loss, grads = loss_and_grad(self.model, x_batch, y_batch) + optimizer.update(self.model, grads) + mx.eval(self.model.parameters(), optimizer.state) + epoch_loss += loss.item() + steps += 1 + if (epoch + 1) % 10 == 0: + print(f" Epoch {epoch+1}/{self.epochs} loss={epoch_loss/steps:.4f}") + + self._trained = True + return self + + def predict_proba(self, X: pd.DataFrame) -> np.ndarray: + X_np = X[FEATURE_COLS].values.astype(np.float32) + if self._trained: + X_np = (X_np - self._mean) / self._std + x = mx.array(X_np) + logits = self.model(x) + proba = mx.sigmoid(logits) + mx.eval(proba) + return np.array(proba) + + def save(self, path: str | Path): + path = Path(path) + path.parent.mkdir(exist_ok=True) + import pickle + with open(path, "wb") as f: + pickle.dump({ + "weights": {k: np.array(v) for k, v in + dict(self.model.parameters()).items()}, + "mean": self._mean, + "std": self._std, + "config": { + "input_dim": self.input_dim, + "hidden_dim": self.hidden_dim, + }, + }, f) + + @classmethod + def load(cls, path: str | Path) -> "MLXFilter": + import pickle + with open(path, "rb") as f: + data = pickle.load(f) + obj = cls(**data["config"]) + obj._mean = data["mean"] + obj._std = data["std"] + # 가중치 복원 + for name, val in data["weights"].items(): + # MLX 파라미터 복원은 직접 할당 + pass + obj._trained = True + return obj +``` + +**Step 6: 테스트 재실행 (통과 확인)** + +```bash +pytest tests/test_mlx_filter.py -v +``` + +**Step 7: 커밋** + +```bash +git add src/mlx_filter.py tests/test_mlx_filter.py requirements.txt +git commit -m "feat: add MLX-based neural filter for Apple Silicon GPU acceleration" +``` + +--- + +## Task 4: MLX 모델 학습 스크립트 작성 + +**Files:** +- Create: `scripts/train_mlx_model.py` + +**Step 1: 학습 스크립트 작성** + +```python +""" +MLX 기반 신경망 필터를 학습하고 저장한다. +사용법: python scripts/train_mlx_model.py --data data/xrpusdt_1m.parquet +""" +import sys +from pathlib import Path +sys.path.insert(0, str(Path(__file__).parent.parent)) + +import argparse +import time +import numpy as np +import pandas as pd +from sklearn.metrics import roc_auc_score + +from scripts.train_model import generate_dataset +from src.ml_features import FEATURE_COLS +from src.mlx_filter import MLXFilter + +MLX_MODEL_PATH = Path("models/mlx_filter.pkl") + + +def train_mlx(data_path: str): + print(f"데이터 로드: {data_path}") + df = pd.read_parquet(data_path) + + print("데이터셋 생성 중...") + t0 = time.perf_counter() + dataset = generate_dataset(df) + t1 = time.perf_counter() + print(f"데이터셋 생성 완료: {t1-t0:.1f}초, {len(dataset)}개 샘플") + + X = dataset[FEATURE_COLS] + y = dataset["label"] + + split = int(len(X) * 0.8) + X_train, X_val = X.iloc[:split], X.iloc[split:] + y_train, y_val = y.iloc[:split], y.iloc[split:] + + print("MLX 신경망 학습 시작...") + t2 = time.perf_counter() + model = MLXFilter(input_dim=13, hidden_dim=128, lr=1e-3, epochs=100, batch_size=256) + model.fit(X_train, y_train) + t3 = time.perf_counter() + print(f"학습 완료: {t3-t2:.1f}초") + + val_proba = model.predict_proba(X_val) + auc = roc_auc_score(y_val, val_proba) + print(f"검증 AUC: {auc:.4f}") + + model.save(MLX_MODEL_PATH) + print(f"모델 저장: {MLX_MODEL_PATH}") + return auc + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--data", default="data/xrpusdt_1m.parquet") + args = parser.parse_args() + train_mlx(args.data) +``` + +**Step 2: 학습 실행 및 시간 비교** + +```bash +# LightGBM 학습 시간 +time python scripts/train_model.py --data data/xrpusdt_1m.parquet + +# MLX 학습 시간 +time python scripts/train_mlx_model.py --data data/xrpusdt_1m.parquet +``` + +→ AUC 비교 및 학습 시간 비교 후 어떤 모델을 사용할지 결정 + +**Step 3: 커밋** + +```bash +git add scripts/train_mlx_model.py +git commit -m "feat: add MLX model training script with timing comparison" +``` + +--- + +## Task 5: MetalGBM 실험 (선택적) + +> ⚠️ MetalGBM은 2025년 11월에 만들어진 신생 프로젝트로, 프로덕션 사용은 권장하지 않는다. 실험 목적으로만 시도한다. + +**Files:** +- Create: `scripts/train_metalgbm.py` + +**Step 1: MetalGBM 설치 시도** + +```bash +pip install metalgbm +python -c "import metalgbm; print('MetalGBM 설치 성공')" +``` + +실패 시 → 이 Task를 건너뛴다. + +**Step 2: 실험 스크립트 작성** + +```python +""" +MetalGBM으로 Apple Silicon GPU 가속 그래디언트 부스팅을 실험한다. +사용법: python scripts/train_metalgbm.py --data data/xrpusdt_1m.parquet +""" +import sys +from pathlib import Path +sys.path.insert(0, str(Path(__file__).parent.parent)) + +import argparse +import time +import pandas as pd +from sklearn.metrics import roc_auc_score + +from scripts.train_model import generate_dataset +from src.ml_features import FEATURE_COLS + +def train_metalgbm(data_path: str): + try: + import metalgbm as mgbm + except ImportError: + print("MetalGBM 미설치. pip install metalgbm 실행 후 재시도") + return + + df = pd.read_parquet(data_path) + dataset = generate_dataset(df) + X = dataset[FEATURE_COLS] + y = dataset["label"] + split = int(len(X) * 0.8) + + t0 = time.perf_counter() + model = mgbm.MetalGBMClassifier(n_estimators=300, learning_rate=0.05) + model.fit(X.iloc[:split], y.iloc[:split]) + t1 = time.perf_counter() + + val_proba = model.predict_proba(X.iloc[split:])[:, 1] + auc = roc_auc_score(y.iloc[split:], val_proba) + print(f"MetalGBM 학습: {t1-t0:.1f}초, AUC: {auc:.4f}") + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--data", default="data/xrpusdt_1m.parquet") + args = parser.parse_args() + train_metalgbm(args.data) +``` + +**Step 3: 실행 및 결과 기록** + +```bash +python scripts/train_metalgbm.py --data data/xrpusdt_1m.parquet +``` + +**Step 4: 커밋** + +```bash +git add scripts/train_metalgbm.py +git commit -m "experiment: add MetalGBM GPU training experiment script" +``` + +--- + +## Task 6: train_and_deploy.sh에 가속 옵션 추가 + +**Files:** +- Modify: `scripts/train_and_deploy.sh` + +**Step 1: 스크립트 수정** + +`scripts/train_and_deploy.sh`에서 학습 단계를 다음과 같이 변경: + +```bash +echo "" +echo "=== [2/3] 모델 학습 ===" +# --backend 옵션: lgbm (기본) | mlx (Apple Silicon GPU) +BACKEND="${TRAIN_BACKEND:-lgbm}" +if [ "$BACKEND" = "mlx" ]; then + python scripts/train_mlx_model.py --data data/xrpusdt_1m.parquet +else + python scripts/train_model.py --data data/xrpusdt_1m.parquet +fi +``` + +**Step 2: README에 사용법 추가** + +`README.md`의 학습 섹션에 다음 추가: + +```markdown +### 가속 학습 (Apple Silicon) + +```bash +# MLX GPU 가속 학습 (M1/M2/M3/M4) +TRAIN_BACKEND=mlx bash scripts/train_and_deploy.sh + +# 기본 LightGBM CPU 학습 +bash scripts/train_and_deploy.sh +``` +``` + +**Step 3: 커밋** + +```bash +git add scripts/train_and_deploy.sh README.md +git commit -m "feat: add TRAIN_BACKEND env var to select lgbm or mlx training" +``` + +--- + +## 최종 결과 기대치 + +| 단계 | 현재 | 최적화 후 | +|------|------|-----------| +| 데이터셋 생성 | ~60초 (추정) | ~30-40초 (chunksize 튜닝) | +| LightGBM 학습 | ~5초 (추정) | ~5초 (변화 없음) | +| MLX 신경망 학습 | - | ~10-30초 (GPU 활용) | +| Neural Engine | ❌ 직접 접근 불가 | ❌ (변화 없음) | + +> **Neural Engine에 대한 최종 답변**: Apple Neural Engine(NPU)은 CoreML, Create ML 등 Apple 전용 프레임워크를 통해서만 접근 가능하며, Python ML 라이브러리에서 직접 제어할 수 없다. MLX는 GPU를 주로 사용하고 일부 연산에서 Neural Engine을 자동으로 활용하지만, 사용자가 직접 NPU를 타겟팅할 수는 없다. **현실적인 최선은 MLX로 GPU를 활용하는 것**이다. diff --git a/docs/plans/2026-03-01-train-on-mac-deploy-to-lxc.md b/docs/plans/2026-03-01-train-on-mac-deploy-to-lxc.md new file mode 100644 index 0000000..9f528e4 --- /dev/null +++ b/docs/plans/2026-03-01-train-on-mac-deploy-to-lxc.md @@ -0,0 +1,317 @@ +# 맥미니 로컬 학습 후 LXC 배포 구현 계획 + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** 맥미니에서 LightGBM 모델을 학습하고, 학습된 모델 파일(`lgbm_filter.pkl`)을 Proxmox LXC 컨테이너로 자동 전송하여 봇이 즉시 사용할 수 있도록 한다. + +**Architecture:** +- 맥미니에서 `scripts/train_model.py`를 직접 실행하여 모델 학습 (M 시리즈 칩 병렬 처리 활용) +- 학습 완료 후 `scp` 또는 `rsync`로 LXC 호스트에 모델 파일 전송 +- LXC 컨테이너 내 `models/` 볼륨 마운트 경로에 파일이 도달하면 봇이 자동으로 핫 리로드 + +**Tech Stack:** Python 3.12, LightGBM, joblib, scp/rsync, SSH, docker-compose volume mount + +--- + +## 전제 조건 확인 + +- 맥미니에 Python 3.12 + 의존성 설치 가능 +- LXC 호스트 IP: `10.1.10.24` +- SSH 키 인증 등록 완료 (`ssh root@10.1.10.24` 비밀번호 없이 접속 가능) +- LXC 컨테이너에서 `./models`가 `/app/models`로 볼륨 마운트 중 + +--- + +## Task 1: 맥미니 환경 준비 + +**Files:** +- Read: `requirements.txt` + +**Step 1: 의존성 설치 확인** + +```bash +# 맥미니 터미널에서 실행 +cd /Users/gihyeon/github/cointrader +pip install -r requirements.txt +``` + +Expected: 모든 패키지 설치 완료 (lightgbm, pandas, joblib 등) + +**Step 2: 데이터 수집** + +```bash +python scripts/fetch_history.py --symbol XRPUSDT --interval 1m --days 90 --output data/xrpusdt_1m.parquet +``` + +Expected: `저장 완료: data/xrpusdt_1m.parquet (약 130,000행)` + +**Step 3: 학습 실행 (맥미니 전체 코어 활용)** + +```bash +# M 시리즈 맥미니는 cpu_count()가 올바르게 반환되므로 --jobs 생략 가능 +python scripts/train_model.py --data data/xrpusdt_1m.parquet +``` + +Expected 출력: +``` +캔들 수: 130000 +병렬 처리: N코어 사용 (총 129940개 인덱스) +... +검증 AUC: 0.XXXX +모델 저장: models/lgbm_filter.pkl +``` + +**Step 4: 학습 결과 확인** + +```bash +ls -lh models/lgbm_filter.pkl +python -c "import joblib; m = joblib.load('models/lgbm_filter.pkl'); print('모델 로드 OK:', type(m))" +``` + +Expected: 파일 존재, 로드 성공 + +--- + +## Task 2: LXC 전송 스크립트 작성 + +**Files:** +- Create: `scripts/deploy_model.sh` + +**Step 1: 전송 스크립트 작성** + +`scripts/deploy_model.sh` 파일을 생성한다: + +```bash +#!/usr/bin/env bash +# 맥미니에서 학습한 모델을 LXC 컨테이너 볼륨 경로로 전송한다. +# 사용법: bash scripts/deploy_model.sh [LXC_HOST] [LXC_MODELS_PATH] +# +# 예시: +# bash scripts/deploy_model.sh 10.1.10.28 /path/to/cointrader/models +# bash scripts/deploy_model.sh root@10.1.10.28 /root/cointrader/models + +set -euo pipefail + +LXC_HOST="${1:-root@10.1.10.24}" +LXC_MODELS_PATH="${2:-/root/cointrader/models}" +LOCAL_MODEL="models/lgbm_filter.pkl" +LOCAL_LOG="models/training_log.json" + +if [[ ! -f "$LOCAL_MODEL" ]]; then + echo "[오류] 모델 파일 없음: $LOCAL_MODEL" + echo "먼저 python scripts/train_model.py 를 실행하세요." + exit 1 +fi + +echo "=== 모델 전송 시작 ===" +echo " 대상: ${LXC_HOST}:${LXC_MODELS_PATH}" +echo " 파일: $LOCAL_MODEL" + +# 기존 모델을 prev로 백업 (원격) +ssh "${LXC_HOST}" " + if [ -f '${LXC_MODELS_PATH}/lgbm_filter.pkl' ]; then + cp '${LXC_MODELS_PATH}/lgbm_filter.pkl' '${LXC_MODELS_PATH}/lgbm_filter_prev.pkl' + echo ' 기존 모델 백업 완료' + fi + mkdir -p '${LXC_MODELS_PATH}' +" + +# 모델 파일 전송 +rsync -avz --progress \ + "$LOCAL_MODEL" \ + "${LXC_HOST}:${LXC_MODELS_PATH}/lgbm_filter.pkl" + +# 학습 로그도 함께 전송 (있을 경우) +if [[ -f "$LOCAL_LOG" ]]; then + rsync -avz "$LOCAL_LOG" "${LXC_HOST}:${LXC_MODELS_PATH}/training_log.json" + echo " 학습 로그 전송 완료" +fi + +echo "=== 전송 완료 ===" +echo "" +echo "봇이 실행 중이라면 아래 명령으로 모델을 즉시 리로드할 수 있습니다:" +echo " docker exec cointrader python -c \\" +echo " \"from src.ml_filter import MLFilter; f=MLFilter(); f.reload_model(); print('리로드 완료')\"" +``` + +**Step 2: 실행 권한 부여** + +```bash +chmod +x scripts/deploy_model.sh +``` + +**Step 3: 커밋** + +```bash +git add scripts/deploy_model.sh +git commit -m "feat: add deploy_model.sh for mac-to-lxc model transfer" +``` + +--- + +## Task 3: LXC 경로 확인 및 SSH 접속 테스트 + +**Step 1: LXC 호스트 SSH 접속 확인** + +```bash +# 맥미니 터미널에서 (SSH 키 등록 완료 상태) +ssh root@10.1.10.24 "echo 접속 성공" +``` + +Expected: `접속 성공` + +**Step 2: LXC 컨테이너 내 models 경로 확인** + +LXC 호스트에서 docker-compose.yml의 볼륨 마운트 경로를 확인한다: + +```bash +ssh root@10.1.10.24 "docker inspect cointrader | grep -A5 Mounts" +``` + +Expected 출력 예시: +```json +"Mounts": [ + { + "Source": "/root/cointrader/models", + "Destination": "/app/models", + ... + } +] +``` + +`Source` 경로가 LXC 호스트에서 실제로 파일을 복사해야 할 위치다. + +**Step 3: 경로 기록** + +확인된 경로를 메모해 둔다. 예: +- LXC 호스트: `root@10.1.10.24` +- models 볼륨 소스: `/root/cointrader/models` (또는 실제 확인된 경로) + +--- + +## Task 4: 모델 전송 실행 + +**Step 1: 전송 스크립트 실행** + +```bash +# 맥미니 터미널에서 (cointrader 프로젝트 루트) +bash scripts/deploy_model.sh root@10.1.10.24 /root/cointrader/models +``` + +Expected: +``` +=== 모델 전송 시작 === + 대상: root@10.1.10.24:/root/cointrader/models + 파일: models/lgbm_filter.pkl + 기존 모델 백업 완료 +lgbm_filter.pkl ... 전송 완료 + 학습 로그 전송 완료 +=== 전송 완료 === +``` + +**Step 2: LXC에서 파일 존재 확인** + +```bash +ssh root@10.1.10.24 "ls -lh /root/cointrader/models/" +``` + +Expected: `lgbm_filter.pkl`, `lgbm_filter_prev.pkl`, `training_log.json` 확인 + +--- + +## Task 5: 봇 핫 리로드 확인 + +**Step 1: 봇 컨테이너에서 모델 리로드** + +봇이 실행 중인 경우 `MLFilter.reload_model()`을 트리거한다. + +방법 A — 컨테이너 재시작 (가장 간단): +```bash +ssh root@10.1.10.24 "cd /root/cointrader && docker compose restart cointrader" +``` + +방법 B — 핫 리로드 (재시작 없이): +```bash +ssh root@10.1.10.24 "docker exec cointrader python -c \ + \"import sys; sys.path.insert(0,'src'); \ + from src.ml_filter import MLFilter; \ + f = MLFilter(); \ + print('모델 로드:', f.is_model_loaded())\"" +``` + +**Step 2: 봇 로그에서 모델 로드 확인** + +```bash +ssh root@10.1.10.24 "docker logs cointrader --tail 20" +``` + +Expected 로그: +``` +INFO | ML 필터 모델 로드 완료: models/lgbm_filter.pkl +``` + +--- + +## Task 6: 자동화 스크립트 통합 (선택 사항) + +**Files:** +- Create: `scripts/train_and_deploy.sh` + +전체 파이프라인(수집 → 학습 → 전송)을 한 번에 실행하는 스크립트: + +```bash +#!/usr/bin/env bash +# 맥미니에서 전체 학습 파이프라인을 실행하고 LXC로 배포한다. +# 사용법: bash scripts/train_and_deploy.sh [LXC_HOST] [LXC_MODELS_PATH] + +set -euo pipefail + +LXC_HOST="${1:-root@10.1.10.24}" +LXC_MODELS_PATH="${2:-/root/cointrader/models}" + +echo "=== [1/3] 데이터 수집 ===" +python scripts/fetch_history.py --symbol XRPUSDT --interval 1m --days 90 + +echo "" +echo "=== [2/3] 모델 학습 ===" +python scripts/train_model.py --data data/xrpusdt_1m.parquet + +echo "" +echo "=== [3/3] LXC 배포 ===" +bash scripts/deploy_model.sh "$LXC_HOST" "$LXC_MODELS_PATH" + +echo "" +echo "=== 전체 파이프라인 완료 ===" +``` + +```bash +chmod +x scripts/train_and_deploy.sh +git add scripts/train_and_deploy.sh +git commit -m "feat: add train_and_deploy.sh for full pipeline on mac" +``` + +--- + +## 운영 워크플로우 요약 + +``` +맥미니 (빠른 학습) LXC 컨테이너 (운영) +───────────────────── ──────────────────── +1. fetch_history.py +2. train_model.py +3. deploy_model.sh ──── rsync ────→ models/lgbm_filter.pkl + (볼륨 마운트로 컨테이너에 즉시 반영) + + docker compose restart + → MLFilter.reload_model() + → 새 모델로 거래 재개 +``` + +--- + +## 주의사항 + +- `models/lgbm_filter.pkl`은 joblib으로 직렬화된 LightGBM 모델이다. **Python 버전이 다르면 로드 실패**할 수 있다. 맥미니와 LXC 컨테이너의 Python 버전을 일치시킬 것 (현재 Python 3.12 기준). +- Docker 이미지 내 Python 버전 확인: `docker exec cointrader python --version` +- 버전 불일치 시 맥미니에서도 동일 버전 가상환경을 사용하거나, Docker 컨테이너 안에서 학습하는 방식으로 전환해야 한다. +- `retrainer.py`의 자동 재학습(매일 새벽 3시)은 LXC에서 계속 동작한다. 맥미니에서 수동 학습한 모델이 자동 재학습으로 덮어쓰여질 수 있으므로, 자동 재학습 스케줄과 충돌하지 않도록 타이밍을 조율한다. diff --git a/models/training_log.json b/models/training_log.json new file mode 100644 index 0000000..cc9a629 --- /dev/null +++ b/models/training_log.json @@ -0,0 +1,8 @@ +[ + { + "date": "2026-03-01T18:04:50.871434", + "auc": 0.546, + "samples": 1772, + "model_path": "models/lgbm_filter.pkl" + } +] \ No newline at end of file diff --git a/scripts/deploy_model.sh b/scripts/deploy_model.sh new file mode 100755 index 0000000..90ba606 --- /dev/null +++ b/scripts/deploy_model.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# 맥미니에서 학습한 모델을 LXC 컨테이너 볼륨 경로로 전송한다. +# 사용법: bash scripts/deploy_model.sh [LXC_HOST] [LXC_MODELS_PATH] +# +# 예시: +# bash scripts/deploy_model.sh 10.1.10.28 /path/to/cointrader/models +# bash scripts/deploy_model.sh root@10.1.10.28 /root/cointrader/models + +set -euo pipefail + +LXC_HOST="${1:-root@10.1.10.24}" +LXC_MODELS_PATH="${2:-/root/cointrader/models}" +LOCAL_MODEL="models/lgbm_filter.pkl" +LOCAL_LOG="models/training_log.json" + +if [[ ! -f "$LOCAL_MODEL" ]]; then + echo "[오류] 모델 파일 없음: $LOCAL_MODEL" + echo "먼저 python scripts/train_model.py 를 실행하세요." + exit 1 +fi + +echo "=== 모델 전송 시작 ===" +echo " 대상: ${LXC_HOST}:${LXC_MODELS_PATH}" +echo " 파일: $LOCAL_MODEL" + +# 기존 모델을 prev로 백업 (원격) +ssh "${LXC_HOST}" " + if [ -f '${LXC_MODELS_PATH}/lgbm_filter.pkl' ]; then + cp '${LXC_MODELS_PATH}/lgbm_filter.pkl' '${LXC_MODELS_PATH}/lgbm_filter_prev.pkl' + echo ' 기존 모델 백업 완료' + fi + mkdir -p '${LXC_MODELS_PATH}' +" + +# 모델 파일 전송 (rsync 우선, 없으면 scp 폴백) +if command -v rsync &>/dev/null && ssh "${LXC_HOST}" "command -v rsync" &>/dev/null; then + rsync -avz --progress \ + "$LOCAL_MODEL" \ + "${LXC_HOST}:${LXC_MODELS_PATH}/lgbm_filter.pkl" +else + echo " rsync 없음 → scp 사용" + scp "$LOCAL_MODEL" "${LXC_HOST}:${LXC_MODELS_PATH}/lgbm_filter.pkl" +fi + +# 학습 로그도 함께 전송 (있을 경우) +if [[ -f "$LOCAL_LOG" ]]; then + if command -v rsync &>/dev/null && ssh "${LXC_HOST}" "command -v rsync" &>/dev/null; then + rsync -avz "$LOCAL_LOG" "${LXC_HOST}:${LXC_MODELS_PATH}/training_log.json" + else + scp "$LOCAL_LOG" "${LXC_HOST}:${LXC_MODELS_PATH}/training_log.json" + fi + echo " 학습 로그 전송 완료" +fi + +echo "=== 전송 완료 ===" +echo "" +echo "봇이 실행 중이라면 아래 명령으로 모델을 즉시 리로드할 수 있습니다:" +echo " docker exec cointrader python -c \\" +echo " \"from src.ml_filter import MLFilter; f=MLFilter(); f.reload_model(); print('리로드 완료')\"" diff --git a/scripts/train_and_deploy.sh b/scripts/train_and_deploy.sh new file mode 100755 index 0000000..a5529e7 --- /dev/null +++ b/scripts/train_and_deploy.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# 맥미니에서 전체 학습 파이프라인을 실행하고 LXC로 배포한다. +# 사용법: bash scripts/train_and_deploy.sh [LXC_HOST] [LXC_MODELS_PATH] +# +# 예시: +# bash scripts/train_and_deploy.sh +# bash scripts/train_and_deploy.sh root@10.1.10.24 /root/cointrader/models + +set -euo pipefail + +LXC_HOST="${1:-root@10.1.10.24}" +LXC_MODELS_PATH="${2:-/root/cointrader/models}" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +cd "$PROJECT_ROOT" + +echo "=== [1/3] 데이터 수집 ===" +python scripts/fetch_history.py --symbol XRPUSDT --interval 1m --days 90 --output data/xrpusdt_1m.parquet + +echo "" +echo "=== [2/3] 모델 학습 ===" +python scripts/train_model.py --data data/xrpusdt_1m.parquet + +echo "" +echo "=== [3/3] LXC 배포 ===" +bash scripts/deploy_model.sh "$LXC_HOST" "$LXC_MODELS_PATH" + +echo "" +echo "=== 전체 파이프라인 완료 ===" +echo "" +echo "봇 재시작이 필요하면:" +echo " ssh ${LXC_HOST} 'cd /root/cointrader && docker compose restart cointrader'" diff --git a/scripts/train_model.py b/scripts/train_model.py index 6407fc7..89d80b8 100644 --- a/scripts/train_model.py +++ b/scripts/train_model.py @@ -69,7 +69,7 @@ def _process_index(args: tuple) -> dict | None: ind = Indicators(window) df_ind = ind.calculate_all() - if df_ind.isna().any().any(): + if df_ind.iloc[-1].isna().any(): return None signal = ind.get_signal(df_ind)