예제 소스
실시간 XGBoost와 DQN 결합 구조: 거래 AI를 검증하는 법 · 파일 59개
run_lora_until.py
"""
지정 시각까지 차트 LoRA 를 이어서 학습하고, 중간마다 모의매매를 돌린다.
모의매매는 ``evaluate_qwen_chart_lora.py`` 의 PaperAccount (Account.py 규칙) 를 쓰고
실제 업비트 주문은 넣지 않는다. 시작봉은 300 이상 PNG 중 무작위로 고른다.
예:
python run_lora_until.py --until 2026-10-22T20:00:00 --train-minutes 120 --eval-seconds 7200
"""
from __future__ import annotations
import argparse
import json
import subprocess
import sys
import time
from datetime import datetime, timedelta
from pathlib import Path
from evaluate_qwen_chart_lora import adapter_fingerprint
ADAPTER = Path("cache/qwen_chart_lora")
PAPER_ROOT = Path("cache/qwen_chart_lora/paper")
PAPER_LOG = Path("cache/qwen_chart_lora/paper_hourly.jsonl")
DEFAULT_TRAIN_MINUTES = 120
DEFAULT_EVAL_SECONDS = 7200
LOAD_BUFFER = timedelta(minutes=12)
def _parse_until(value: str | None) -> datetime | None:
if not value or value in {"forever", "inf", "infinite"}:
return None
return datetime.fromisoformat(value)
def _run(command: list[str]) -> subprocess.CompletedProcess[str]:
print("+", " ".join(command), flush=True)
return subprocess.run(command, check=False, text=True)
def _read_latest_result(root: Path) -> dict:
results = sorted(root.glob("*/result.json"), key=lambda p: p.stat().st_mtime)
if not results:
raise FileNotFoundError(f"모의매매 결과 없음: {root}")
return json.loads(results[-1].read_text(encoding="utf-8"))
def _append_log(payload: dict) -> None:
PAPER_LOG.parent.mkdir(parents=True, exist_ok=True)
with PAPER_LOG.open("a", encoding="utf-8") as fh:
fh.write(json.dumps(payload, ensure_ascii=False) + "\n")
def _past_returns() -> list[float]:
if not PAPER_LOG.is_file():
return []
values: list[float] = []
for line in PAPER_LOG.read_text(encoding="utf-8").splitlines():
if not line.strip():
continue
row = json.loads(line)
result = row.get("result") or {}
if "liquidation_return_pct" in result:
values.append(float(result["liquidation_return_pct"]))
return values
def _trend_note(current_ret: float) -> str:
past = _past_returns()
if not past:
return "첫 모의매매 · 비교 기준 없음"
last = past[-1]
avg = sum(past) / len(past)
better_than_last = "상승" if current_ret > last else ("하락" if current_ret < last else "동일")
better_than_avg = "평균 이상" if current_ret >= avg else "평균 이하"
return (
f"직전 {last:+.3f}% → 이번 {current_ret:+.3f}% ({better_than_last} {current_ret - last:+.3f}%p) · "
f"누적 평균 {avg:+.3f}% ({better_than_avg}, n={len(past)})"
)
def _summarize(result: dict) -> str:
ret = float(result.get("liquidation_return_pct", 0.0))
loss_krw = float(result.get("initial_krw", 0.0)) * ret / 100.0
counts = result.get("executed_counts") or {}
return (
f"시작봉 {result.get('start_bar')}~{result.get('last_evaluated_bar')} · "
f"처리 {result.get('processed')}봉 · "
f"청산 수익률 {ret:+.3f}% ({loss_krw:+,.0f}원) · "
f"거래 {result.get('completed_trades')}회 · "
f"승률 {float(result.get('win_rate_pct', 0.0)):.1f}% · "
f"체결 매수 {counts.get('매수', 0)} · 체결 매도 {counts.get('매도', 0)}"
)
def _train_until(python: str, until: datetime, args: argparse.Namespace) -> bool:
result = _run(
[
python,
"-u",
"train_qwen_chart_lora.py",
"--steps",
"100000",
"--save-every",
str(args.save_every),
"--model",
str(args.model),
"--out",
str(args.adapter),
"--csv",
str(args.csv),
"--chart-dir",
str(args.chart_dir),
"--until",
until.isoformat(timespec="seconds"),
]
)
if result.returncode != 0:
print(f"[WARN] 학습 종료 코드 {result.returncode}", flush=True)
return False
return True
def _paper_eval(python: str, args: argparse.Namespace) -> dict:
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
out_root = PAPER_ROOT / stamp
out_root.mkdir(parents=True, exist_ok=True)
_run(
[
python,
"-u",
"evaluate_qwen_chart_lora.py",
"--model",
str(args.model),
"--adapter",
str(args.adapter),
"--csv",
str(args.csv),
"--chart-dir",
str(args.chart_dir),
"--output-root",
str(out_root),
"--start-bar",
"300",
"--random-start",
"--max-seconds",
str(args.eval_seconds),
"--initial-krw",
str(args.initial_krw),
"--force",
]
)
result = _read_latest_result(out_root)
fingerprint = adapter_fingerprint(args.adapter)
ret = float(result.get("liquidation_return_pct", 0.0))
trend = _trend_note(ret)
payload = {
"logged_at": datetime.now().isoformat(timespec="seconds"),
"adapter_sha256": fingerprint,
"output_root": str(out_root.resolve()),
"summary": _summarize(result),
"trend": trend,
"result": result,
}
_append_log(payload)
print(f"[모의매매] {payload['summary']}", flush=True)
print(f"[수익률추이] {trend}", flush=True)
return result
def loop(args: argparse.Namespace) -> None:
deadline = _parse_until(args.until)
python = sys.executable
train_chunk = timedelta(minutes=int(args.train_minutes))
eval_td = timedelta(seconds=int(args.eval_seconds))
cycle = 0
while True:
now = datetime.now()
if deadline is not None:
remaining = deadline - now
if remaining.total_seconds() < 90:
print(f"[STOP] 마감 시각 {deadline.isoformat(timespec='seconds')}", flush=True)
break
can_eval = remaining >= eval_td + LOAD_BUFFER
if can_eval and remaining >= train_chunk + eval_td + timedelta(minutes=2):
train_end = min(now + train_chunk, deadline - eval_td)
elif can_eval:
train_end = deadline - eval_td
else:
train_end = deadline
else:
can_eval = True
train_end = now + train_chunk
cycle += 1
print(
f"[CYCLE {cycle}] 학습 {now.strftime('%m-%d %H:%M')}→{train_end.strftime('%m-%d %H:%M')}"
+ (f" · 이어서 {int(args.eval_seconds) // 60}분 모의매매" if can_eval else " · 모의매매 없이 학습만"),
flush=True,
)
if train_end > now:
ok = _train_until(python, train_end, args)
if not ok:
print("[WARN] 학습 실패, 60초 뒤 다시 시도", flush=True)
time.sleep(60)
continue
if can_eval and (deadline is None or datetime.now() < deadline):
try:
_paper_eval(python, args)
except Exception as exc:
print(f"[WARN] 모의매매 실패: {exc}", flush=True)
time.sleep(60)
def main() -> None:
parser = argparse.ArgumentParser(description="지정 시각까지 LoRA 학습 + 주기 모의매매")
parser.add_argument(
"--until",
default="forever",
help="로컬 ISO 시각. forever 이면 마감 없이 반복",
)
parser.add_argument("--model", type=Path, default=Path("models/Qwen2.5-VL-3B-Instruct"))
parser.add_argument("--adapter", type=Path, default=ADAPTER)
parser.add_argument("--csv", type=Path, default=Path("cache/market_extreme_labels.csv"))
parser.add_argument("--chart-dir", type=Path, default=Path("chart"))
parser.add_argument("--save-every", type=int, default=50)
parser.add_argument("--train-minutes", type=int, default=DEFAULT_TRAIN_MINUTES)
parser.add_argument("--eval-seconds", type=int, default=DEFAULT_EVAL_SECONDS)
parser.add_argument("--initial-krw", type=float, default=5_000_000.0)
args = parser.parse_args()
loop(args)
if __name__ == "__main__":
main()