예제 소스
실시간 XGBoost와 DQN 결합 구조: 거래 AI를 검증하는 법 · 파일 59개
main_llm_db_rl_viewer.py
"""
DB 캔들 → **캡쳐된 차트 PNG** 를 ``PngToStatusPolicy`` 가 보고 매수/매도/유지를
결정 → 모의 포트폴리오로 백테스트 + REINFORCE 온라인 학습.
이전 버전은 손수 만든 17차원 상태 벡터로 학습했지만 LLM 의 액션 정확도가
떨어져, **차트 이미지 자체** 를 입력으로 쓰는 정책으로 교체했습니다.
구버전은 ``main_llm_db_rl_viewer_legacy.py`` 에 백업되어 있습니다.
설정은 argparse 없이 다음 파일만 수정합니다.
- config/llm_rl_backend.py 백엔드·모델 경로
- config/llm_rl_model_download.py HF 스냅샷 다운로드 여부
- config/llm_rl_train.py 학습·뷰어·LABELS_CSV_PATH·PngToStatus 옵션
· 액션 CSV 모방학습 체크포인트 쓰려면 ``USE_PNG_STATUS_ACTION_SUPERVISED = True`` 및
``train_png_policy_from_action_csv.py`` 로 ``PNG_STATUS_ACTION_SUPERVISED_CKPT`` 생성
· 학습 맞춤 확인: ``PNG_STATUS_SHOW_VS_ACTION_CSV_EVAL`` + ``PNG_STATUS_EVAL_ACTION_CSV_PATH``
실행 (프로젝트 루트에서):
python train_png_policy_from_action_csv.py # 선택: ACTION CSV → PNG 정책 학습
python main_llm_db_rl_viewer.py
"""
from __future__ import annotations
import json
import logging.config
import warnings
from pathlib import Path
import numpy as np
import pandas as pd
warnings.filterwarnings("ignore")
np.seterr(divide="ignore", invalid="ignore")
_cfg = Path(__file__).resolve().parent / "config" / "logging.json"
if _cfg.exists():
with open(_cfg, "r", encoding="utf-8") as f:
logging.config.dictConfig(json.load(f))
import logging
logger = logging.getLogger(__name__)
print(
"[main_llm_db_rl_viewer] 무거운 의존성(torch, Tk, transformers 등) 로드 중… "
"최초 30~90초는 터미널에 아무것도 안 나올 수 있습니다.",
flush=True,
)
def _ensure_tkinter() -> None:
"""Homebrew python@3.12 등은 기본적으로 _tkinter 가 빠져 있을 수 있음."""
import sys
try:
import tkinter # noqa: F401
except ImportError as e:
mj, mn = sys.version_info.major, sys.version_info.minor
ver = f"{mj}.{mn}"
pybin = f"python{mj}.{mn}"
raise SystemExit(
"Tkinter(_tkinter)를 불러올 수 없습니다. 이 뷰어는 Tk GUI가 필요합니다.\n\n"
"[macOS + Homebrew 예 — Python 버전에 맞출 것]\n"
f" brew install python-tk@{ver}\n"
f" $(brew --prefix python-tk@{ver})/bin/{pybin} -m venv .venv\n"
" source .venv/bin/activate && pip install -r requirements.txt\n\n"
"또는 python.org 공식 macOS 설치본(Tk 포함)으로 새 venv 를 만드세요.\n"
f"(현재 인터프리터: {sys.executable})\n\n"
f"원인: {e}"
) from e
_ensure_tkinter()
from config import llm_rl_backend as be # noqa: E402
from config import llm_rl_model_download as dl # noqa: E402
from config import llm_rl_train as tr # noqa: E402
from lib.rl_data_labels import merge_extreme_labels_from_csv # noqa: E402
from lib.rl_policy_teacher import load_policy_markdown # noqa: E402
from lib.png_status_vision_viewer import PngStatusVisionBacktestViewer # noqa: E402
from Market import Market # noqa: E402
import main_llm_db_test_ollama as ollama_bt # noqa: E402
from PngToStatus import ( # noqa: E402
DEFAULT_INPUT_H,
DEFAULT_INPUT_W,
load_png_status_policy,
)
from viewer import ( # noqa: E402
DEFAULT_MLX_MODEL_DIR,
DEFAULT_TORCH_MODEL_DIR,
download_hf_snapshot,
make_local_vision_callable,
make_ollama_vision_callable,
resolve_model_path,
)
def _maybe_download_hf() -> None:
if not dl.ENABLE_SNAPSHOT:
return
models_dir = Path(be.MODELS_DIR).resolve()
models_dir.mkdir(parents=True, exist_ok=True)
logger.info("HF 스냅샷 다운로드: %s", dl.REPO_ID)
download_hf_snapshot(dl.REPO_ID.strip(), models_dir)
def _build_vision_callable():
if be.BACKEND == "ollama":
vision = make_ollama_vision_callable(
host=str(be.OLLAMA_HOST).rstrip("/"),
model=str(be.OLLAMA_MODEL).strip(),
)
v_retry = max(1, int(ollama_bt.OLLAMA_RETRY))
v_backoff = float(ollama_bt.OLLAMA_RETRY_BACKOFF_SEC)
status_lbl = f"ollama · {be.OLLAMA_MODEL}"
return vision, v_retry, v_backoff, status_lbl
models_dir = Path(be.MODELS_DIR).resolve()
folder = be.TORCH_MODEL_FOLDER if be.BACKEND == "torch" else be.MLX_MODEL_FOLDER
mp = resolve_model_path(models_dir, folder.strip())
if not mp.exists():
raise SystemExit(
f"모델 경로 없음: {mp}\n"
" config/llm_rl_model_download.py 에서 ENABLE_SNAPSHOT = True 로 받거나 폴더를 채우세요."
)
vision = make_local_vision_callable(
be.BACKEND,
str(mp),
max_new_tokens_torch=int(be.MAX_NEW_TOKENS_TORCH),
max_tokens_mlx=int(be.MAX_TOKENS_MLX),
)
import os
v_retry = int(os.environ.get("LOCAL_LLM_RETRY", "2"))
v_backoff = float(os.environ.get("LOCAL_LLM_RETRY_BACKOFF_SEC", "1.0"))
status_lbl = f"{be.BACKEND} · {mp.name}"
return vision, v_retry, v_backoff, status_lbl
def _load_action_csv_aligned_for_eval(path: Path, n: int) -> np.ndarray | None:
"""뷰어 df 행 수 n 에 맞게 tail 슬라이스한 정수 액션 배열."""
if not path.is_file():
logger.warning("액션 CSV 없음 — CSV 대비 학습 확인 패널 생략: %s", path)
return None
s = pd.read_csv(path, usecols=["action"]).squeeze("columns")
arr = s.astype(np.float64).round().astype(np.int64).to_numpy()
if len(arr) < n:
logger.warning(
"액션 CSV 행(%s)이 df(%s)보다 짧아 CSV 비교를 끕니다.",
len(arr),
n,
)
return None
if len(arr) > n:
logger.info("액션 CSV tail %s행만 사용 (df와 길이 맞춤)", n)
arr = arr[-n:]
return arr
def main() -> None:
print("[main_llm_db_rl_viewer] main() 시작", flush=True)
_maybe_download_hf()
vision, v_retry, v_backoff, status_lbl = _build_vision_callable()
logger.info("Market.get_data() …")
market = Market()
df = market.get_data()
if df is None or len(df) < tr.MIN_BARS + tr.BAR_STEP:
raise SystemExit("데이터가 부족합니다.")
n_full = len(df)
cap = getattr(tr, "RL_MAX_BARS", None)
bar_offset = 0
if cap is not None and n_full > int(cap):
bar_offset = n_full - int(cap)
df = df.iloc[-int(cap):].reset_index(drop=True)
logger.info(
"RL_MAX_BARS=%s 적용: tail %s봉 (전체 %s봉, bar_offset=%s)",
cap,
len(df),
n_full,
bar_offset,
)
df = merge_extreme_labels_from_csv(df, tr.LABELS_CSV_PATH, source_bar_offset=bar_offset)
eval_action_labels: np.ndarray | None = None
eval_action_csv_path: str | None = None
if bool(getattr(tr, "********", False)):
csv_path = Path(
getattr(
tr,
"PNG_STATUS_EVAL_ACTION_CSV_PATH",
getattr(tr, "ACTION_CSV_FOR_SUPERVISED_TRAIN", "cache/ACTION_repaired.CSV"),
)
)
eval_action_labels = _load_action_csv_aligned_for_eval(csv_path, len(df))
if eval_action_labels is not None:
eval_action_csv_path = str(csv_path)
logger.info("액션 CSV 비교 표시: %s", csv_path)
policy_doc_path = getattr(tr, "POLICY_DOC_PATH", None)
vision_policy_doc = (
load_policy_markdown(policy_doc_path)
if getattr(tr, "********", False)
else ""
)
# PngToStatus 정책 로드 (없으면 신규 생성)
if bool(getattr(tr, "********", False)):
pts_ckpt = getattr(
tr,
"********",
"cache/png_status_policy_action_repaired.pt",
)
logger.info("액션 CSV 모방학습 체크포인트 사용: %s", pts_ckpt)
if bool(getattr(tr, "ONLINE_TRAIN", False)):
logger.warning(
"ONLINE_TRAIN=True 이면 뷰어가 같은 경로에 정책을 덮어쓸 수 있습니다. "
"모방 가중치만 보려면 llm_rl_train.py 에서 ONLINE_TRAIN=False 권장."
)
else:
pts_ckpt = getattr(tr, "PNG_STATUS_CHECKPOINT_PATH", "cache/png_status_policy.pt")
pts_h = int(getattr(tr, "PNG_STATUS_INPUT_H", DEFAULT_INPUT_H))
pts_w = int(getattr(tr, "PNG_STATUS_INPUT_W", DEFAULT_INPUT_W))
pts_hidden = int(getattr(tr, "PNG_STATUS_HIDDEN", 256))
png_policy = load_png_status_policy(
pts_ckpt, h=pts_h, w=pts_w, hidden=pts_hidden
)
logger.info(
"PngToStatusPolicy 준비: ckpt=%s · 입력 %dx%d · hidden=%d",
pts_ckpt, pts_h, pts_w, pts_hidden,
)
port = ollama_bt.PaperLlmPortfolio(float(tr.INITIAL_KRW))
port.min_trade_bars = max(0, int(tr.MIN_TRADE_BARS))
app = PngStatusVisionBacktestViewer(
df=df,
portfolio=port,
vision_call=vision,
png_policy=png_policy,
online_train=bool(getattr(tr, "ONLINE_TRAIN", False)),
online_sample_action=bool(getattr(tr, "ONLINE_SAMPLE_ACTION", True)),
online_max_episodes=getattr(tr, "ONLINE_MAX_EPISODES", None),
online_save_every_ep=int(getattr(tr, "ONLINE_SAVE_EVERY_EP", 1)),
online_clear_text_each_ep=bool(getattr(tr, "ONLINE_CLEAR_TEXT_EACH_EP", False)),
checkpoint_path=str(pts_ckpt),
train_lr=float(getattr(tr, "PNG_STATUS_LR", tr.LEARNING_RATE)),
train_weight_decay=float(tr.WEIGHT_DECAY),
reward_win_weight=float(tr.REWARD_WIN_RATE_WEIGHT),
reward_profit_scale=float(tr.REWARD_PROFIT_SCALE),
initial_krw=float(tr.INITIAL_KRW),
min_trade_bars=int(tr.MIN_TRADE_BARS),
min_bars=max(50, tr.MIN_BARS),
bar_step=max(1, tr.BAR_STEP),
dry_run=tr.DRY_RUN,
auto_interval_sec=tr.AUTO_INTERVAL_SEC,
chart_max_width=tr.CHART_MAX_WIDTH,
vision_retries=max(1, int(be.VISION_RETRIES)),
vision_backoff_sec=float(be.VISION_BACKOFF_SEC),
window_title=(
getattr(tr, "PNG_STATUS_WINDOW_TITLE", "PNG→Status RL 차트 학습 뷰어 (캡쳐 이미지 기반)")
+ (
" · [액션모방 정책]"
if bool(getattr(tr, "********", False))
else ""
)
),
status_backend_label=status_lbl,
teacher_kl_coef=float(getattr(tr, "RL_TEACHER_KL_COEF", 0.0)),
teacher_temperature=float(getattr(tr, "RL_TEACHER_TEMPERATURE", 1.0)),
llm_align_weight=float(getattr(tr, "RL_LLM_ALIGN_WEIGHT", 0.0)),
vision_policy_doc=vision_policy_doc or None,
policy_doc_path_for_meta=str(policy_doc_path) if policy_doc_path else None,
save_train_meta_with_checkpoint=bool(
getattr(tr, "********", False)
),
eval_action_labels=eval_action_labels,
eval_action_csv_path=eval_action_csv_path,
)
if bool(getattr(tr, "ONLINE_TRAIN", False)) and bool(
getattr(tr, "ONLINE_AUTOSTART", False)
):
print(
"[main_llm_db_rl_viewer] 자동 스텝 시작 — torch 백엔드면 첫 LLM 호출에서 "
"VLM 로딩으로 수 분간 ‘처리 중…’만 보일 수 있습니다.",
flush=True,
)
app.auto_mode.set(True)
app.btn_stop.configure(state="normal")
app._start_step()
app.mainloop()
if __name__ == "__main__":
main()