Files
auto-stock/backend/app/api/endpoints/us_trading.py
shuim 028a1c352b feat: 자동매매 신뢰성 개선 — 시세 실서버 라우팅·일일 한도 전역화·수동 매수 API
- korea_investment_api: KIS_QUOTES_VIA_REAL — 모의투자 모드에서 시세/차트/호가 조회를
  실계좌 시세 서버(20/s)로 위임, 주문·잔고는 모의서버 유지 (EGW00201 대응)
- recommendations: execute_auto_trade_background asyncio.Lock 중복 실행 방지 +
  MAX_DAILY_TRADES를 당일 BUY 건수 차감 전역 기준으로 적용 (11종목 동시 매수 재발 방지)
- manual_buy.py 신규: POST /api/v1/portfolio/buy (리스크 가드 + safe_place_order + db_worker)
- mcp-server: claude_code_client(claude -p 헤드리스 모드) 추가, trade_monitor 부분매도 시
  active 유지·remaining_pct 갱신, API 키 헤더·/api/v1 경로 수정, ODNO 주문ID 수정
- config: MCP_REQUEST_TIMEOUT, MCP_RISK_CACHE_TTL, BATCH_ANALYSIS_INTERVAL,
  SWING_INDEX_BREAKER_ENABLED(OFF), API_TRIGGERED_AUTO_TRADE(OFF) 추가
- docker-compose: 포트 충돌 해소 (postgres 15433, frontend 3001)
- frontend: 추천 패널 매수 버튼, 차트/호가/저널 패널 개선
- CLAUDE.md 압축 최적화 (규칙 변경 없음), _sweep_single_axis.py 백테스트 단일축 스윕

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 23:37:08 +09:00

162 lines
6.0 KiB
Python

"""미국 주식 매매 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, Field
from typing import Literal, Optional, List
import logging
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/us-trading", tags=["US Trading"])
class UsOrderRequest(BaseModel):
ticker: str
order_type: Literal["buy", "sell"]
quantity: int = Field(gt=0, description="주문 수량 (1 이상)")
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("output", {}).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, req.order_type.upper(), 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("output", {}).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, req.order_type.upper(), 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))