[잔고 관리] - _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>
211 lines
6.1 KiB
Python
211 lines
6.1 KiB
Python
import os
|
|
import signal
|
|
import threading
|
|
import uvicorn
|
|
import multiprocessing
|
|
from fastapi import FastAPI, Request
|
|
from pydantic import BaseModel
|
|
from contextlib import asynccontextmanager
|
|
|
|
from modules.config import Config
|
|
from modules.services.ollama import OllamaManager
|
|
from modules.services.kis import KISClient
|
|
from modules.services.news import NewsCollector
|
|
from modules.services.telegram import TelegramMessenger
|
|
from modules.bot import AutoTradingBot
|
|
from modules.utils.process_tracker import ProcessTracker, ProcessWatchdog
|
|
from modules.services.telegram_bot.runner import run_telegram_bot_standalone
|
|
|
|
# 전역 객체
|
|
messenger = TelegramMessenger()
|
|
ai_agent = None
|
|
kis_client = None
|
|
news_collector = None
|
|
watchdog = None
|
|
|
|
|
|
def run_trading_bot(ipc_lock, command_queue, shutdown_event, eod_event=None):
|
|
"""트레이딩 봇 실행 래퍼"""
|
|
ProcessTracker.register("Trading Bot Main")
|
|
bot = AutoTradingBot(ipc_lock=ipc_lock, command_queue=command_queue,
|
|
shutdown_event=shutdown_event, eod_event=eod_event)
|
|
bot.loop()
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
global ai_agent, kis_client, news_collector, watchdog
|
|
|
|
# 1. 설정 검증
|
|
Config.validate()
|
|
|
|
# 2. 좀비 프로세스 정리
|
|
try:
|
|
ProcessTracker.check_and_kill_zombies()
|
|
ProcessTracker.clear()
|
|
ProcessTracker.register("Main Server (Uvicorn Worker)")
|
|
except Exception:
|
|
pass
|
|
|
|
# 3. 전역 객체 초기화
|
|
ai_agent = OllamaManager()
|
|
kis_client = KISClient()
|
|
news_collector = NewsCollector()
|
|
|
|
# 4. 공유 리소스 생성
|
|
ipc_lock = multiprocessing.Lock()
|
|
command_queue = multiprocessing.Queue()
|
|
shutdown_event = multiprocessing.Event()
|
|
eod_event = multiprocessing.Event() # [v3.1] EOD 셧다운 시그널
|
|
|
|
print("[Server] Starting AI Trading Bot & Telegram Bot...")
|
|
|
|
# 5. 자식 프로세스 생성
|
|
bot_args = (ipc_lock, command_queue, shutdown_event, eod_event)
|
|
telegram_args = (ipc_lock, command_queue, shutdown_event)
|
|
|
|
bot_process = multiprocessing.Process(
|
|
target=run_trading_bot, args=bot_args)
|
|
bot_process.start()
|
|
|
|
telegram_process = multiprocessing.Process(
|
|
target=run_telegram_bot_standalone, args=telegram_args)
|
|
telegram_process.start()
|
|
|
|
# 6. Watchdog 시작
|
|
watchdog = ProcessWatchdog(shutdown_event=shutdown_event)
|
|
watchdog.watch("Trading Bot", bot_process, run_trading_bot, bot_args)
|
|
watchdog.watch("Telegram Bot", telegram_process,
|
|
run_telegram_bot_standalone, telegram_args)
|
|
watchdog.start()
|
|
|
|
messenger.send_message("[Server Started] Windows AI Server Online.")
|
|
|
|
# [v3.1] EOD 모니터 스레드: 봇이 EOD 시그널을 보내면 서버 프로세스 자동 종료
|
|
_server_pid = os.getpid()
|
|
|
|
def _eod_monitor():
|
|
"""eod_event 감지 시 SIGTERM으로 uvicorn 우아하게 종료"""
|
|
while not shutdown_event.is_set():
|
|
if eod_event.is_set():
|
|
print("[Server] EOD 시그널 수신 — 서버 종료 중 (15초 후)...")
|
|
import time as _time
|
|
_time.sleep(15) # 자식 프로세스 정리 시간
|
|
print(f"[Server] SIGTERM → PID {_server_pid}")
|
|
os.kill(_server_pid, signal.SIGTERM)
|
|
return
|
|
import time as _time
|
|
_time.sleep(5)
|
|
|
|
_eod_thread = threading.Thread(target=_eod_monitor, daemon=True, name="EODMonitor")
|
|
_eod_thread.start()
|
|
|
|
yield
|
|
|
|
# [Shutdown]
|
|
print("[Server] Shutting down...")
|
|
shutdown_event.set()
|
|
|
|
if watchdog:
|
|
watchdog.stop()
|
|
|
|
# 자식 프로세스 종료
|
|
for name in ["Trading Bot", "Telegram Bot"]:
|
|
proc = watchdog.get_process(name) if watchdog else None
|
|
if proc and proc.is_alive():
|
|
print(f" - Stopping {name}...")
|
|
proc.join(timeout=5)
|
|
if proc.is_alive():
|
|
proc.terminate()
|
|
proc.join(timeout=3)
|
|
|
|
# SharedMemory 정리
|
|
try:
|
|
from multiprocessing.shared_memory import SharedMemory
|
|
shm = SharedMemory(name=Config.SHM_NAME, create=False)
|
|
shm.close()
|
|
shm.unlink()
|
|
except Exception:
|
|
pass
|
|
|
|
messenger.send_message("[Server Stopped] Server Shutting Down.")
|
|
|
|
|
|
app = FastAPI(title="Windows AI Stock Server", lifespan=lifespan)
|
|
|
|
|
|
@app.middleware("http")
|
|
async def log_requests(request: Request, call_next):
|
|
print(f"[HTTP] {request.method} {request.url}")
|
|
response = await call_next(request)
|
|
return response
|
|
|
|
|
|
class ManualOrderRequest(BaseModel):
|
|
ticker: str
|
|
action: str
|
|
quantity: int
|
|
|
|
|
|
@app.get("/")
|
|
def index():
|
|
vram = 0
|
|
if ai_agent:
|
|
vram = ai_agent.check_vram()
|
|
return {
|
|
"status": "online",
|
|
"gpu_vram": round(vram, 2),
|
|
"service": "Windows AI Server (Refactored)"
|
|
}
|
|
|
|
|
|
@app.get("/trade/balance")
|
|
@app.get("/api/trade/balance")
|
|
async def get_balance():
|
|
if not kis_client:
|
|
return {"error": "Server not initialized"}
|
|
return kis_client.get_balance()
|
|
|
|
|
|
@app.post("/trade/order")
|
|
@app.post("/api/trade/order")
|
|
async def manual_order(req: ManualOrderRequest):
|
|
ticker = req.ticker
|
|
qty = req.quantity
|
|
action = req.action.upper()
|
|
|
|
result = "No Action"
|
|
if action == "BUY":
|
|
result = kis_client.buy_stock(ticker, qty)
|
|
elif action == "SELL":
|
|
result = kis_client.sell_stock(ticker, qty)
|
|
|
|
return {"status": "executed", "kis_result": result}
|
|
|
|
|
|
@app.post("/analyze/portfolio")
|
|
@app.post("/api/analyze/portfolio")
|
|
async def analyze_portfolio():
|
|
balance = kis_client.get_balance()
|
|
news = news_collector.get_market_news()
|
|
|
|
prompt = f"""
|
|
Analyze this portfolio with recent news:
|
|
Portfolio: {balance}
|
|
News: {news}
|
|
Response in Korean.
|
|
"""
|
|
analysis = ai_agent.request_inference(prompt)
|
|
return {"analysis": analysis}
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# 서버 시작 시 좀비 프로세스 정리
|
|
try:
|
|
ProcessTracker.check_and_kill_zombies()
|
|
except Exception:
|
|
pass
|
|
|
|
print("[Server] Starting Windows AI Server...")
|
|
uvicorn.run("main_server:app", host="0.0.0.0", port=8000, reload=False)
|