## CRITICAL/HIGH (S1-S8) - S1: Aggressive 모드 %B 스케일링 캡 1.3 (BB 중앙선 위 매수 방지) - S2: ML scaler train-only fit (데이터 누수 제거) - S3: SIDEWAYS = ADX<20 AND slope≈0 (트렌드 초기 차단 해소) - S4: EOD 청산 실패 시 재시도 + CRITICAL 로그 - S5: 일일 손실 한도에 미실현 P&L 포함 - S6: trailing stop 고점 매 사이클 갱신 - S7: 섹터 필터 미매핑 종목 "기타" 분류 (max 3 적용) - S8: 스캐너 RSI 기준 25→46, BB 점수 -3%~+1.5% 구간 ## MEDIUM (M1-M7) - M1: 스캐너 최소 거래량 100만→10만주 - M2: ADX 히스테리시스 18/22 (국면 플리핑 방지) - M3: 분할매도 최소 수익 1.0%→0.5% - M4: ORB 갭 필터 4% + 레인지 폭 0.3% 최소 - M5: 쿨다운 스트릭 부분매도 시 미변경 - M6: MDD 강제매도 loss_tracker 기록 제거 (허위 ban 방지) - M7: RSI Wilder 스무딩으로 통일 ## 인프라/보안 - 인증 누락 엔드포인트 수정 (analysis, journals) - nginx 보안 헤더 location 블록 누락 수정 - Alembic 마이그레이션 IF NOT EXISTS 가드 추가 - SellRequest/UsOrderRequest 입력 검증 강화 - 차트 네이버 폴백 추가 (DB 데이터 없을 때) - 서버 이전 시 자동 DB 마이그레이션 + KRX 초기 수집 - CLAUDE.md 섹션 5.1 백테스트 값 동기화 (4%→6%, -6%→-7%) - MAX_SWING_POSITIONS 25→20 (CLAUDE.md 준수) - mdd_manager failed_count에서 skip 종목 제외 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
162 lines
6.0 KiB
Python
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("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))
|