"""미국 주식 매매 API 엔드포인트 - GET /us-trading/watchlist: 미국 주식 관심 종목 목록 - GET /us-trading/signals: 매매 신호 스캔 결과 - GET /us-trading/status: 미국장 상태 (개장/폐장/프리마켓) - POST /us-trading/execute: 미국 주식 매수/매도 실행 """ from fastapi import APIRouter, HTTPException from pydantic import BaseModel from typing import Optional, List import logging logger = logging.getLogger(__name__) router = APIRouter(prefix="/us-trading", tags=["US Trading"]) class UsOrderRequest(BaseModel): ticker: str order_type: str # "buy" or "sell" quantity: int price: Optional[float] = None # None = market order attempt (limit 근사) @router.get("/status") async def get_us_market_status(): """미국장 현재 상태""" from app.services.us_market_schedule import ( is_us_market_open, is_us_premarket, is_us_aftermarket, get_next_us_market_open ) return { "is_open": is_us_market_open(), "is_premarket": is_us_premarket(), "is_aftermarket": is_us_aftermarket(), "next_open": get_next_us_market_open().isoformat() if not is_us_market_open() else None, } @router.get("/watchlist") async def get_us_watchlist(): """미국 주식 관심 종목 목록""" from app.services.us_stock_strategy import get_us_watchlist tickers = get_us_watchlist() return {"tickers": tickers, "count": len(tickers)} @router.get("/signals") async def scan_us_signals(): """미국 주식 매매 신호 스캔""" from app.services.us_stock_strategy import scan_us_stocks from app.services.korea_investment_api import korea_investment_api try: results = await scan_us_stocks(korea_investment_api) return {"signals": results, "count": len(results)} except Exception as e: logger.error(f"US signal scan failed: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.post("/execute") async def execute_us_trade(req: UsOrderRequest): """미국 주식 매수/매도 실행""" from app.services.exchange_detector import detect_exchange from app.services.korea_investment_api import korea_investment_api exchange = detect_exchange(req.ticker) try: if req.order_type.lower() == "buy": if not req.price: # Get current price for limit order price_data = await korea_investment_api.get_overseas_price(req.ticker, exchange) req.price = float(price_data.get("last", 0)) if not req.price or req.price <= 0: raise HTTPException( status_code=400, detail=f"유효하지 않은 주문 가격: {req.ticker} 현재가 조회 실패 또는 0달러" ) result = await korea_investment_api.place_overseas_order( req.ticker, "buy", req.quantity, req.price, exchange ) elif req.order_type.lower() == "sell": if not req.price: price_data = await korea_investment_api.get_overseas_price(req.ticker, exchange) req.price = float(price_data.get("last", 0)) if not req.price or req.price <= 0: raise HTTPException( status_code=400, detail=f"유효하지 않은 주문 가격: {req.ticker} 현재가 조회 실패 또는 0달러" ) result = await korea_investment_api.place_overseas_order( req.ticker, "sell", req.quantity, req.price, exchange ) else: raise HTTPException(status_code=400, detail="order_type must be 'buy' or 'sell'") return result except Exception as e: logger.error(f"US trade execution failed: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.post("/cycle") async def trigger_us_trade_cycle(): """미국 매매 사이클 수동 즉시 실행 (테스트/긴급 용도)""" from app.services.us_market_scheduler import us_trade_cycle try: result = await us_trade_cycle() return result except Exception as e: logger.error(f"US trade cycle failed: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/positions") async def get_us_positions(): """현재 미국 주식 활성 포지션 조회""" from app.core.database import AsyncSessionLocal from app.models.models import ActiveTrade from sqlalchemy import select try: async with AsyncSessionLocal() as session: result = await session.execute( select(ActiveTrade).where( ActiveTrade.status == "active", ActiveTrade.currency_code == "USD", ) ) positions = result.scalars().all() return { "positions": [ { "ticker": p.stock_code, "name": p.stock_name, "quantity": p.quantity, "entry_price": float(p.entry_price or 0), "current_price": float(p.current_price or 0), "stop_loss": float(p.stop_loss or 0), "remaining_pct": float(p.remaining_pct or 1.0), "exchange": p.exchange_code, "entry_reason": p.entry_reason, } for p in positions ], "count": len(positions), } except Exception as e: logger.error(f"US positions fetch failed: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/report") async def get_us_daily_report(): """오늘 미국 매매 일일 리포트""" from app.services.us_market_scheduler import us_post_market_report try: return await us_post_market_report() except Exception as e: logger.error(f"US report failed: {e}") raise HTTPException(status_code=500, detail=str(e))