[잔고 관리] - _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>
156 lines
5.5 KiB
Python
156 lines
5.5 KiB
Python
"""
|
|
daily_launcher.py — KRX 거래일 자동 런처
|
|
|
|
[동작 흐름]
|
|
1. 오늘이 KRX 거래일인지 확인
|
|
2. 휴장일이면: 텔레그램 알림 후 종료
|
|
3. 거래일이면: LSTM 워밍업 → main_server.py 시작
|
|
4. 봇은 15:35에 스스로 EOD 셧다운
|
|
|
|
[설치: Windows 작업 스케줄러]
|
|
트리거: 매일 08:30 (주말 포함 — 봇이 내부에서 휴장일 체크)
|
|
동작: python C:\\path\\to\\web-ai\\daily_launcher.py
|
|
시작 위치: C:\\path\\to\\web-ai
|
|
실행 계정: 현재 사용자 (로그인 여부 무관 실행 권장)
|
|
|
|
[수동 실행]
|
|
python daily_launcher.py
|
|
"""
|
|
import sys
|
|
import time
|
|
import subprocess
|
|
import datetime
|
|
import logging
|
|
from pathlib import Path
|
|
from zoneinfo import ZoneInfo
|
|
|
|
ROOT = Path(__file__).parent
|
|
LOG_FILE = ROOT / "daily_launcher.log"
|
|
KST = ZoneInfo("Asia/Seoul")
|
|
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format="%(asctime)s [Launcher] %(message)s",
|
|
datefmt="%Y-%m-%d %H:%M:%S",
|
|
handlers=[
|
|
logging.FileHandler(LOG_FILE, encoding="utf-8"),
|
|
logging.StreamHandler(sys.stdout),
|
|
],
|
|
)
|
|
log = logging.getLogger("daily_launcher")
|
|
|
|
|
|
def setup_path():
|
|
if str(ROOT) not in sys.path:
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
|
|
def send_notify(msg: str):
|
|
"""텔레그램 알림 발송 (실패해도 런처 계속 진행)"""
|
|
try:
|
|
from modules.services.telegram import TelegramMessenger
|
|
TelegramMessenger().send_message(msg)
|
|
except Exception as e:
|
|
log.warning(f"텔레그램 알림 실패: {e}")
|
|
|
|
|
|
def clear_eod_marker():
|
|
"""전일 EOD 마커 파일 삭제 (새 거래일 시작)"""
|
|
eod_file = ROOT / "data" / ".eod_date"
|
|
if not eod_file.exists():
|
|
return
|
|
try:
|
|
prev = datetime.date.fromisoformat(eod_file.read_text().strip())
|
|
today = datetime.datetime.now(KST).date()
|
|
if prev < today:
|
|
eod_file.unlink()
|
|
log.info(f"전일({prev}) EOD 마커 삭제 완료")
|
|
except Exception:
|
|
eod_file.unlink(missing_ok=True)
|
|
|
|
|
|
def wait_until_warmup_time(cal) -> None:
|
|
"""
|
|
워밍업 시작 시각까지 대기
|
|
- 장 시작 30분 전이면 즉시 워밍업
|
|
- 그보다 일찍 실행되면 '장 시작 30분 전'까지 대기
|
|
"""
|
|
secs = cal.seconds_to_open()
|
|
if secs <= 0:
|
|
log.info("이미 장 중 — 즉시 워밍업 시작")
|
|
return
|
|
|
|
warmup_start_secs = max(0, secs - 30 * 60) # 장 시작 30분 전
|
|
if warmup_start_secs > 0:
|
|
warmup_at = datetime.datetime.now(KST) + datetime.timedelta(seconds=warmup_start_secs)
|
|
log.info(f"워밍업 대기 중 ({warmup_start_secs/60:.0f}분 후 {warmup_at.strftime('%H:%M')} 시작)")
|
|
time.sleep(warmup_start_secs)
|
|
else:
|
|
log.info(f"장 시작 {secs/60:.0f}분 전 — 즉시 워밍업")
|
|
|
|
|
|
def run_warmup_and_server() -> int:
|
|
"""
|
|
warmup_and_restart.py 실행
|
|
- warmup: LSTM 사전학습
|
|
- 이후 main_server.py를 새 콘솔에서 자동 시작
|
|
"""
|
|
log.info("LSTM 워밍업 시작...")
|
|
result = subprocess.run(
|
|
[sys.executable, "warmup_and_restart.py"],
|
|
cwd=str(ROOT),
|
|
)
|
|
return result.returncode
|
|
|
|
|
|
def main():
|
|
setup_path()
|
|
|
|
from modules.utils.market_calendar import KRXCalendar
|
|
cal = KRXCalendar()
|
|
today = datetime.datetime.now(KST).date()
|
|
log.info(f"실행 날짜: {today} | 시장 상태: {cal.status_summary()}")
|
|
|
|
# ── 1. 휴장일 체크 ────────────────────────────────────────────────────────
|
|
if not cal.is_trading_day(today):
|
|
try:
|
|
nxt = cal.next_trading_open()
|
|
next_str = nxt.strftime("%m/%d(%a) %H:%M")
|
|
except Exception:
|
|
next_str = "미정"
|
|
|
|
msg = (
|
|
f"[자동매매] {today.strftime('%m/%d(%a)')} 휴장일\n"
|
|
f"다음 거래일: {next_str} KST 자동 시작"
|
|
)
|
|
log.info(f"휴장일 — 봇 시작 안 함 (다음: {next_str})")
|
|
send_notify(msg)
|
|
return
|
|
|
|
# ── 2. EOD 마커 초기화 ────────────────────────────────────────────────────
|
|
clear_eod_marker()
|
|
|
|
# ── 3. 워밍업 시각까지 대기 ───────────────────────────────────────────────
|
|
wait_until_warmup_time(cal)
|
|
|
|
# ── 4. 거래일 시작 알림 ───────────────────────────────────────────────────
|
|
log.info(f"거래일 확인 — 워밍업 및 봇 시작 ({datetime.datetime.now(KST).strftime('%H:%M')})")
|
|
send_notify(
|
|
f"[자동매매] {today.strftime('%m/%d(%a)')} 거래일 시작\n"
|
|
f"LSTM 워밍업 중..."
|
|
)
|
|
|
|
# ── 5. 워밍업 + 서버 시작 ─────────────────────────────────────────────────
|
|
rc = run_warmup_and_server()
|
|
if rc != 0:
|
|
log.error(f"워밍업 실패 (exit={rc}) — 수동 확인 필요")
|
|
send_notify(f"[Bot] 워밍업 실패! (exit={rc})\n수동으로 확인해 주세요.")
|
|
return
|
|
|
|
log.info("워밍업 완료. main_server.py가 백그라운드에서 실행 중.")
|
|
log.info("봇은 15:35에 스스로 EOD 셧다운합니다.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|