feat: enhance Jenkins pipeline with Discord notifications and model hot-reload functionality
- Added a new stage to the Jenkins pipeline to notify Discord when a build starts, succeeds, or fails, improving communication during the CI/CD process. - Implemented model hot-reload functionality in the MLFilter class, allowing automatic reloading of models when file changes are detected, enhancing responsiveness to updates. - Updated deployment scripts to provide clearer messaging regarding model loading and container status, improving user experience and debugging capabilities.
This commit is contained in:
28
Jenkinsfile
vendored
28
Jenkinsfile
vendored
@@ -7,9 +7,24 @@ pipeline {
|
|||||||
IMAGE_TAG = "${env.BUILD_NUMBER}"
|
IMAGE_TAG = "${env.BUILD_NUMBER}"
|
||||||
FULL_IMAGE = "${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}"
|
FULL_IMAGE = "${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}"
|
||||||
LATEST_IMAGE = "${REGISTRY}/${IMAGE_NAME}:latest"
|
LATEST_IMAGE = "${REGISTRY}/${IMAGE_NAME}:latest"
|
||||||
|
|
||||||
|
// 젠킨스 자격 증명에 저장해둔 디스코드 웹훅 주소를 불러옵니다.
|
||||||
|
DISCORD_WEBHOOK = credentials('discord-webhook')
|
||||||
}
|
}
|
||||||
|
|
||||||
stages {
|
stages {
|
||||||
|
// 빌드가 시작되자마자 알림을 보냅니다.
|
||||||
|
stage('Notify Build Start') {
|
||||||
|
steps {
|
||||||
|
sh """
|
||||||
|
curl -H "Content-Type: application/json" \
|
||||||
|
-X POST \
|
||||||
|
-d '{"content": "🚀 **[빌드 시작]** `cointrader` (Build #${env.BUILD_NUMBER}) 배포 파이프라인 가동"}' \
|
||||||
|
${DISCORD_WEBHOOK}
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
stage('Git Clone from Gitea') {
|
stage('Git Clone from Gitea') {
|
||||||
steps {
|
steps {
|
||||||
git branch: 'main',
|
git branch: 'main',
|
||||||
@@ -55,12 +70,25 @@ pipeline {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 파이프라인 결과에 따른 디스코드 알림
|
||||||
post {
|
post {
|
||||||
success {
|
success {
|
||||||
echo "Build #${env.BUILD_NUMBER} 성공: ${FULL_IMAGE} → 운영 LXC(10.1.10.24) 배포 완료"
|
echo "Build #${env.BUILD_NUMBER} 성공: ${FULL_IMAGE} → 운영 LXC(10.1.10.24) 배포 완료"
|
||||||
|
sh """
|
||||||
|
curl -H "Content-Type: application/json" \
|
||||||
|
-X POST \
|
||||||
|
-d '{"content": "✅ **[배포 성공]** `cointrader` (Build #${env.BUILD_NUMBER}) 운영 서버(10.1.10.24) 배포 완료!\\n- 📦 이미지: `${FULL_IMAGE}`"}' \
|
||||||
|
${DISCORD_WEBHOOK}
|
||||||
|
"""
|
||||||
}
|
}
|
||||||
failure {
|
failure {
|
||||||
echo "Build #${env.BUILD_NUMBER} 실패"
|
echo "Build #${env.BUILD_NUMBER} 실패"
|
||||||
|
sh """
|
||||||
|
curl -H "Content-Type: application/json" \
|
||||||
|
-X POST \
|
||||||
|
-d '{"content": "❌ **[배포 실패]** `cointrader` (Build #${env.BUILD_NUMBER}) 파이프라인 에러 발생. 젠킨스 로그를 확인해 주세요!"}' \
|
||||||
|
${DISCORD_WEBHOOK}
|
||||||
|
"""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -16,7 +16,6 @@ LOCAL_LOG="models/training_log.json"
|
|||||||
# ── 백엔드별 파일 목록 설정 ──────────────────────────────────────────────────
|
# ── 백엔드별 파일 목록 설정 ──────────────────────────────────────────────────
|
||||||
# mlx: ONNX 파일만 전송 (Linux 서버는 onnxruntime으로 추론)
|
# mlx: ONNX 파일만 전송 (Linux 서버는 onnxruntime으로 추론)
|
||||||
# lgbm: pkl 파일 전송
|
# lgbm: pkl 파일 전송
|
||||||
RELOAD_CMD="from src.ml_filter import MLFilter; f=MLFilter(); f.reload_model(); print('리로드 완료')"
|
|
||||||
if [ "$BACKEND" = "mlx" ]; then
|
if [ "$BACKEND" = "mlx" ]; then
|
||||||
LOCAL_FILES=("models/mlx_filter.weights.onnx")
|
LOCAL_FILES=("models/mlx_filter.weights.onnx")
|
||||||
else
|
else
|
||||||
@@ -68,11 +67,12 @@ fi
|
|||||||
echo "=== 전송 완료 ==="
|
echo "=== 전송 완료 ==="
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# ── 핫리로드 ─────────────────────────────────────────────────────────────────
|
# ── 핫리로드 안내 ────────────────────────────────────────────────────────────
|
||||||
echo "=== 핫리로드 시도 ==="
|
# 봇이 캔들마다 모델 파일 mtime을 감지해 자동 리로드한다.
|
||||||
|
# 컨테이너가 실행 중이면 다음 캔들(최대 1분) 안에 자동 적용된다.
|
||||||
|
echo "=== 모델 전송 완료 — 봇이 다음 캔들에서 자동 리로드합니다 ==="
|
||||||
if ssh "${LXC_HOST}" "docker inspect -f '{{.State.Running}}' cointrader 2>/dev/null | grep -q true"; then
|
if ssh "${LXC_HOST}" "docker inspect -f '{{.State.Running}}' cointrader 2>/dev/null | grep -q true"; then
|
||||||
ssh "${LXC_HOST}" "docker exec cointrader python -c \"${RELOAD_CMD}\""
|
echo " 컨테이너 실행 중: 다음 캔들 마감 시 자동 핫리로드 예정"
|
||||||
echo "=== 핫리로드 완료 ==="
|
|
||||||
else
|
else
|
||||||
echo " cointrader 컨테이너가 실행 중이 아닙니다. 건너뜁니다."
|
echo " cointrader 컨테이너가 실행 중이 아닙니다."
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ class TradingBot:
|
|||||||
logger.info("기존 포지션 없음 - 신규 진입 대기")
|
logger.info("기존 포지션 없음 - 신규 진입 대기")
|
||||||
|
|
||||||
async def process_candle(self, df, btc_df=None, eth_df=None):
|
async def process_candle(self, df, btc_df=None, eth_df=None):
|
||||||
|
self.ml_filter.check_and_reload()
|
||||||
|
|
||||||
if not self.risk.is_trading_allowed():
|
if not self.risk.is_trading_allowed():
|
||||||
logger.warning("리스크 한도 초과 - 거래 중단")
|
logger.warning("리스크 한도 초과 - 거래 중단")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -10,12 +10,22 @@ ONNX_MODEL_PATH = Path("models/mlx_filter.weights.onnx")
|
|||||||
LGBM_MODEL_PATH = Path("models/lgbm_filter.pkl")
|
LGBM_MODEL_PATH = Path("models/lgbm_filter.pkl")
|
||||||
|
|
||||||
|
|
||||||
|
def _mtime(path: Path) -> float:
|
||||||
|
"""파일이 없으면 0.0 반환."""
|
||||||
|
try:
|
||||||
|
return path.stat().st_mtime
|
||||||
|
except FileNotFoundError:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
class MLFilter:
|
class MLFilter:
|
||||||
"""
|
"""
|
||||||
ML 필터. ONNX(MLX 신경망) 우선 로드, 없으면 LightGBM으로 폴백한다.
|
ML 필터. ONNX(MLX 신경망) 우선 로드, 없으면 LightGBM으로 폴백한다.
|
||||||
둘 다 없으면 항상 진입을 허용한다.
|
둘 다 없으면 항상 진입을 허용한다.
|
||||||
|
|
||||||
우선순위: ONNX > LightGBM > 폴백(항상 허용)
|
우선순위: ONNX > LightGBM > 폴백(항상 허용)
|
||||||
|
|
||||||
|
check_and_reload()를 주기적으로 호출하면 모델 파일 변경 시 자동 리로드된다.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -29,6 +39,8 @@ class MLFilter:
|
|||||||
self._threshold = threshold
|
self._threshold = threshold
|
||||||
self._onnx_session = None
|
self._onnx_session = None
|
||||||
self._lgbm_model = None
|
self._lgbm_model = None
|
||||||
|
self._loaded_onnx_mtime: float = 0.0
|
||||||
|
self._loaded_lgbm_mtime: float = 0.0
|
||||||
self._try_load()
|
self._try_load()
|
||||||
|
|
||||||
def _try_load(self):
|
def _try_load(self):
|
||||||
@@ -41,7 +53,12 @@ class MLFilter:
|
|||||||
providers=["CPUExecutionProvider"],
|
providers=["CPUExecutionProvider"],
|
||||||
)
|
)
|
||||||
self._lgbm_model = None
|
self._lgbm_model = None
|
||||||
logger.info(f"ML 필터 ONNX 모델 로드 완료: {self._onnx_path}")
|
self._loaded_onnx_mtime = _mtime(self._onnx_path)
|
||||||
|
self._loaded_lgbm_mtime = 0.0
|
||||||
|
logger.info(
|
||||||
|
f"ML 필터 로드: ONNX ({self._onnx_path}) "
|
||||||
|
f"| 임계값={self._threshold}"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"ONNX 모델 로드 실패: {e}")
|
logger.warning(f"ONNX 모델 로드 실패: {e}")
|
||||||
@@ -51,14 +68,51 @@ class MLFilter:
|
|||||||
if self._lgbm_path.exists():
|
if self._lgbm_path.exists():
|
||||||
try:
|
try:
|
||||||
self._lgbm_model = joblib.load(self._lgbm_path)
|
self._lgbm_model = joblib.load(self._lgbm_path)
|
||||||
logger.info(f"ML 필터 LightGBM 모델 로드 완료: {self._lgbm_path}")
|
self._loaded_lgbm_mtime = _mtime(self._lgbm_path)
|
||||||
|
self._loaded_onnx_mtime = 0.0
|
||||||
|
logger.info(
|
||||||
|
f"ML 필터 로드: LightGBM ({self._lgbm_path}) "
|
||||||
|
f"| 임계값={self._threshold}"
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"LightGBM 모델 로드 실패: {e}")
|
logger.warning(f"LightGBM 모델 로드 실패: {e}")
|
||||||
self._lgbm_model = None
|
self._lgbm_model = None
|
||||||
|
else:
|
||||||
|
logger.warning("ML 필터: 모델 파일 없음 → 모든 신호 허용 (폴백)")
|
||||||
|
|
||||||
def is_model_loaded(self) -> bool:
|
def is_model_loaded(self) -> bool:
|
||||||
return self._onnx_session is not None or self._lgbm_model is not None
|
return self._onnx_session is not None or self._lgbm_model is not None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def active_backend(self) -> str:
|
||||||
|
if self._onnx_session is not None:
|
||||||
|
return "ONNX"
|
||||||
|
if self._lgbm_model is not None:
|
||||||
|
return "LightGBM"
|
||||||
|
return "폴백(없음)"
|
||||||
|
|
||||||
|
def check_and_reload(self) -> bool:
|
||||||
|
"""
|
||||||
|
모델 파일의 mtime을 확인해 변경됐으면 리로드한다.
|
||||||
|
실제로 리로드가 일어났으면 True 반환.
|
||||||
|
"""
|
||||||
|
onnx_changed = _mtime(self._onnx_path) != self._loaded_onnx_mtime
|
||||||
|
lgbm_changed = _mtime(self._lgbm_path) != self._loaded_lgbm_mtime
|
||||||
|
|
||||||
|
if onnx_changed or lgbm_changed:
|
||||||
|
changed_files = []
|
||||||
|
if onnx_changed:
|
||||||
|
changed_files.append(str(self._onnx_path))
|
||||||
|
if lgbm_changed:
|
||||||
|
changed_files.append(str(self._lgbm_path))
|
||||||
|
logger.info(f"ML 필터: 모델 파일 변경 감지 → 리로드 ({', '.join(changed_files)})")
|
||||||
|
self._onnx_session = None
|
||||||
|
self._lgbm_model = None
|
||||||
|
self._try_load()
|
||||||
|
logger.info(f"ML 필터 핫리로드 완료: 백엔드={self.active_backend}")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def should_enter(self, features: pd.Series) -> bool:
|
def should_enter(self, features: pd.Series) -> bool:
|
||||||
"""
|
"""
|
||||||
확률 >= threshold 이면 True (진입 허용).
|
확률 >= threshold 이면 True (진입 허용).
|
||||||
@@ -74,15 +128,21 @@ class MLFilter:
|
|||||||
else:
|
else:
|
||||||
X = features.to_frame().T
|
X = features.to_frame().T
|
||||||
proba = float(self._lgbm_model.predict_proba(X)[0][1])
|
proba = float(self._lgbm_model.predict_proba(X)[0][1])
|
||||||
logger.debug(f"ML 필터 확률: {proba:.3f} (임계값: {self._threshold})")
|
logger.debug(
|
||||||
|
f"ML 필터 [{self.active_backend}] 확률: {proba:.3f} "
|
||||||
|
f"(임계값: {self._threshold})"
|
||||||
|
)
|
||||||
return bool(proba >= self._threshold)
|
return bool(proba >= self._threshold)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"ML 필터 예측 오류 (폴백 허용): {e}")
|
logger.warning(f"ML 필터 예측 오류 (폴백 허용): {e}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def reload_model(self):
|
def reload_model(self):
|
||||||
"""재학습 후 모델을 핫 리로드한다."""
|
"""외부에서 강제 리로드할 때 사용 (하위 호환)."""
|
||||||
|
prev_backend = self.active_backend
|
||||||
self._onnx_session = None
|
self._onnx_session = None
|
||||||
self._lgbm_model = None
|
self._lgbm_model = None
|
||||||
self._try_load()
|
self._try_load()
|
||||||
logger.info("ML 필터 모델 리로드 완료")
|
logger.info(
|
||||||
|
f"ML 필터 강제 리로드 완료: {prev_backend} → {self.active_backend}"
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user