v3.1 과매수 방지, 앙상블 학습, KRX 캘린더 기반 장중 전용 운영 구현
[잔고 관리] - _today_buy_total 인스턴스 변수로 당일 누적 매수 추적 (KIS T+2 미차감 보완) - MAX_BUY_PER_CYCLE, MAX_DAILY_BUY_RATIO 설정 추가 - available_deposit = max_daily_buy - effective_today_buy 계산 [앙상블 & 포지션 사이징] - AdaptiveEnsemble 실제 연동 (하드코딩 가중치 제거) - Kelly Criterion Half-Kelly 포지션 비중 계산 - SignalWeights.normalize() Water-Filling 알고리즘으로 경계 위반 해결 - _accuracy_weighted() 크기 가중 정확도로 통일 - ensemble_weights.json → ensemble_history.json 통합 [LLM 클라이언트] - GeminiLLMClient 추가 (Gemini → Ollama 폴백 체인) - _class_last_call_ts 클래스 변수로 워커 재시작 후에도 스로틀 유지 - Ollama 미실행 조기 감지 및 명확한 오류 메시지 [KIS API] - 모든 requests.get/post에 timeout=Config.HTTP_TIMEOUT 적용 - get_balance()에 today_buy_amt 필드 추가 [장중 전용 운영] - KRXCalendar: exchange_calendars 기반, 2024~2026 공휴일 하드코딩 폴백 - EOD 셧다운: 15:35에 전체 상태 저장 후 서버 자동 종료 - Watchdog: .eod_date 마커로 EOD 후 재시작 차단 - daily_launcher.py: 매일 08:30 실행, 휴장일 감지 후 봇 미시작 - Windows 작업 스케줄러 WebAI_DailyLauncher 등록 [텔레그램 스킬 수정] - PYTHONIOENCODING=utf-8 서브프로세스 환경 설정 (cp949 이모지 오류 해결) - /regime: IPC macro_indices 파싱 구현, --json 모드 input() 블로킹 제거 - /weights: ensemble_history.json 형식 파싱 업데이트 - /model_health: glob 패턴 *_v3.pt 수정 - /postmortem: 거래 없을 때 빈 JSON 출력으로 Telegram 오류 해결 - /macro: price=0 시 prev_close 폴백 표시 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import pickle
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from collections import OrderedDict
|
||||
from sklearn.preprocessing import MinMaxScaler
|
||||
|
||||
@@ -164,15 +165,21 @@ def _build_feature_matrix(ohlcv_data):
|
||||
volume = np.array(ohlcv_data.get('volume', []), dtype=np.float64)
|
||||
|
||||
n = len(close)
|
||||
if len(open_) != n: open_ = close.copy()
|
||||
if len(high) != n: high = close.copy()
|
||||
if len(low) != n: low = close.copy()
|
||||
_degraded = []
|
||||
if len(open_) != n: open_ = close.copy(); _degraded.append('open')
|
||||
if len(high) != n: high = close.copy(); _degraded.append('high')
|
||||
if len(low) != n: low = close.copy(); _degraded.append('low')
|
||||
if _degraded:
|
||||
print(f"[LSTM] ⚠️ OHLCV 피처 불완전 ({', '.join(_degraded)} → close 대체). 예측 신뢰도 저하 가능")
|
||||
|
||||
# 거래량 정규화 (최대값 기준, 0이면 0)
|
||||
# 거래량 정규화 (20일 이동평균 대비 비율, max 기준보다 정보량이 높음)
|
||||
if len(volume) == n and volume.max() > 0:
|
||||
volume_norm = volume / (volume.max() + 1e-9)
|
||||
vol_series = pd.Series(volume)
|
||||
vol_ma20 = vol_series.rolling(20, min_periods=1).mean().values
|
||||
volume_norm = volume / (vol_ma20 + 1e-9)
|
||||
volume_norm = np.clip(volume_norm, 0.0, 5.0) / 5.0 # 0~5배 → 0~1 정규화
|
||||
else:
|
||||
volume_norm = np.zeros(n)
|
||||
volume_norm = np.full(n, 0.2) # 데이터 없으면 중립값
|
||||
|
||||
rsi = _compute_rsi(close, period=14)
|
||||
rsi_norm = rsi / 100.0 # 0~1 정규화
|
||||
@@ -375,8 +382,10 @@ class PricePredictor:
|
||||
change_rate = ((predicted_price - current_price) / current_price) * 100
|
||||
|
||||
cached_loss = self.training_status.get("loss", 0.5)
|
||||
# 캐시 신뢰도: 마지막 학습 loss 기반 동적 계산 (고정값 제거)
|
||||
cached_conf = min(0.70, 1.0 / (1.0 + (cached_loss * 200)))
|
||||
print(f"[AI] {ticker or '?'}: 쿨다운 중 → 캐시 예측 사용 "
|
||||
f"({predicted_price:.0f} / {change_rate:+.2f}%)")
|
||||
f"({predicted_price:.0f} / {change_rate:+.2f}% / conf={cached_conf:.2f})")
|
||||
return {
|
||||
"current": current_price,
|
||||
"predicted": float(predicted_price),
|
||||
@@ -384,7 +393,7 @@ class PricePredictor:
|
||||
"trend": trend,
|
||||
"loss": cached_loss,
|
||||
"val_loss": cached_loss,
|
||||
"confidence": 0.62,
|
||||
"confidence": round(cached_conf, 2),
|
||||
"epochs": 0,
|
||||
"device": str(self.device),
|
||||
"lr": self.optimizer.param_groups[0]['lr'],
|
||||
@@ -578,24 +587,28 @@ class PricePredictor:
|
||||
trend = "UP" if predicted_price > current_price else "DOWN"
|
||||
change_rate = ((predicted_price - current_price) / current_price) * 100
|
||||
|
||||
# 신뢰도 계산
|
||||
loss_confidence = 1.0 / (1.0 + (best_val_loss * 50))
|
||||
# ── 신뢰도 계산 (보수적 버전) ──────────────────────────────
|
||||
# val_loss 기반: 0.001→0.74, 0.003→0.62, 0.01→0.50 (이전보다 보수적)
|
||||
loss_confidence = 1.0 / (1.0 + (best_val_loss * 200))
|
||||
|
||||
# 오버피팅 페널티
|
||||
overfit_ratio = final_loss / (best_val_loss + 1e-9)
|
||||
if overfit_ratio < 0.5:
|
||||
overfit_penalty = 0.7
|
||||
elif overfit_ratio > 2.0:
|
||||
overfit_penalty = 0.8
|
||||
overfit_penalty = 0.65 # 심각한 언더피팅
|
||||
elif overfit_ratio > 2.5:
|
||||
overfit_penalty = 0.75 # 오버피팅
|
||||
else:
|
||||
overfit_penalty = 1.0
|
||||
|
||||
# 에포크 수 기반 수렴 판단
|
||||
epoch_factor = 1.0
|
||||
if actual_epochs < 10:
|
||||
epoch_factor = 0.6
|
||||
epoch_factor = 0.55 # 너무 이른 수렴 → 불신뢰
|
||||
elif actual_epochs >= max_epochs:
|
||||
epoch_factor = 0.8
|
||||
epoch_factor = 0.80 # 미수렴 → 부분 신뢰
|
||||
|
||||
confidence = min(0.95, loss_confidence * overfit_penalty * epoch_factor)
|
||||
# 최종 상한: 0.80 (이전 0.95보다 보수적 — LSTM 70% 가중치 남발 방지)
|
||||
confidence = min(0.80, loss_confidence * overfit_penalty * epoch_factor)
|
||||
|
||||
return {
|
||||
"current": current_price,
|
||||
|
||||
Reference in New Issue
Block a user