import os import json from typing import Optional from fastapi import FastAPI from fastapi.responses import JSONResponse from fastapi.middleware.cors import CORSMiddleware import requests from apscheduler.schedulers.background import BackgroundScheduler from pydantic import BaseModel from .db import init_db, save_articles, get_latest_articles from .scraper import fetch_market_news, fetch_major_indices, fetch_overseas_news app = FastAPI() # CORS 설정 (프론트엔드 접근 허용) app.add_middleware( CORSMiddleware, allow_origins=["*"], # 운영 시에는 구체적인 도메인으로 제한하는 것이 좋음 allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) scheduler = BackgroundScheduler(timezone=os.getenv("TZ", "Asia/Seoul")) # Windows AI Server URL (NAS .env에서 설정) WINDOWS_AI_SERVER_URL = os.getenv("WINDOWS_AI_SERVER_URL", "http://192.168.0.5:8000") @app.on_event("startup") def on_startup(): init_db() # 매일 아침 8시 뉴스 스크랩 (NAS 자체 수행) scheduler.add_job(run_scraping_job, "cron", hour="8", minute="0") # 앱 시작 시에도 한 번 실행 (데이터 없으면) if not get_latest_articles(1): run_scraping_job() scheduler.start() def run_scraping_job(): print("[StockLab] Starting news scraping...") # 1. 국내 articles_kr = fetch_market_news() count_kr = save_articles(articles_kr) # 2. 해외 (임시 차단) # articles_world = fetch_overseas_news() # count_world = save_articles(articles_world) count_world = 0 print(f"[StockLab] Saved {count_kr} domestic, {count_world} overseas articles.") @app.get("/health") def health(): return {"ok": True} @app.get("/api/stock/news") def get_news(limit: int = 20, category: str = None): """최신 주식 뉴스 조회 (category: 'domestic' | 'overseas')""" return get_latest_articles(limit, category) @app.get("/api/stock/indices") def get_indices(): """주요 지표(KOSPI 등) 실시간 크롤링 조회""" return fetch_major_indices() @app.post("/api/stock/scrap") def trigger_scrap(): """수동 스크랩 트리거""" run_scraping_job() return {"ok": True} # --- Trading API (Windows Proxy) --- @app.get("/api/trade/balance") def get_balance(): """계좌 잔고 조회 (Windows AI Server Proxy)""" print(f"[Proxy] Requesting Balance from {WINDOWS_AI_SERVER_URL}...") try: resp = requests.get(f"{WINDOWS_AI_SERVER_URL}/trade/balance", timeout=5) if resp.status_code != 200: print(f"[ProxyError] Balance Error: {resp.status_code} {resp.text}") return JSONResponse(status_code=resp.status_code, content=resp.json()) print("[Proxy] Balance Success") return resp.json() except Exception as e: print(f"[ProxyError] Connection Failed: {e}") return JSONResponse( status_code=500, content={"error": "Connection Failed", "detail": str(e), "target": WINDOWS_AI_SERVER_URL} ) class OrderRequest(BaseModel): ticker: str # 종목 코드 (예: "005930") action: str # "BUY" or "SELL" quantity: int # 주문 수량 price: int = 0 # 0이면 시장가 reason: Optional[str] = "Manual Order" # 주문 사유 (AI 기록용) @app.post("/api/trade/order") def order_stock(req: OrderRequest): """주식 매수/매도 주문 (Windows AI Server Proxy)""" print(f"[Proxy] Order Request: {req.dict()} to {WINDOWS_AI_SERVER_URL}...") try: resp = requests.post(f"{WINDOWS_AI_SERVER_URL}/trade/order", json=req.dict(), timeout=10) if resp.status_code != 200: print(f"[ProxyError] Order Error: {resp.status_code} {resp.text}") return JSONResponse(status_code=resp.status_code, content=resp.json()) return resp.json() except Exception as e: print(f"[ProxyError] Order Connection Failed: {e}") return JSONResponse( status_code=500, content={"error": "Connection Failed", "detail": str(e), "target": WINDOWS_AI_SERVER_URL} ) @app.get("/api/version") def version(): return {"version": os.getenv("APP_VERSION", "dev")}