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>
This commit is contained in:
2026-09-19 23:37:08 +09:00
parent b1da208fd5
commit 028a1c352b
39 changed files with 1073 additions and 694 deletions

View File

@@ -10,7 +10,7 @@ KIS API 호출량을 절감한다.
- market_analyzer.get_all_tradable_stocks() 대체
- market_analyzer.analyze_stock() 배치 분석용 일봉 데이터 제공
URL (종목 목록): https://finance.naver.com/sise/sise_market_sum.naver?sosok=0&page=N
URL (종목 목록): https://m.stock.naver.com/api/stocks/marketValue/{KOSPI|KOSDAQ}?page=N&pageSize=50
URL (일봉 차트): https://fchart.stock.naver.com/siseJson.naver?symbol=CODE&requestType=1&startTime=YYYYMMDD&endTime=YYYYMMDD&timeframe=day
"""
@@ -65,16 +65,43 @@ def _parse_page(html: bytes) -> List[Tuple[str, str, int]]:
return result
_LIST_API_URL = "https://m.stock.naver.com/api/stocks/marketValue/{market}"
_LIST_PAGE_SIZE = 50 # 구 HTML 페이지(~50종목/페이지)와 동일한 페이지 의미 유지
_SOSOK_TO_MARKET = {0: "KOSPI", 1: "KOSDAQ"}
def _parse_json_page(data: Dict) -> List[Tuple[str, str, int]]:
"""m.stock.naver.com JSON → [(종목코드, 종목명, 현재가), ...]"""
result = []
for item in data.get("stocks") or []:
code = item.get("itemCode") or ""
name = (item.get("stockName") or "").strip()
if not code or not name:
continue
try:
price = int(str(item.get("closePrice") or "0").replace(",", ""))
except ValueError:
price = 0
result.append((code, name, price))
return result
async def _fetch_page(client: httpx.AsyncClient, sosok: int, page: int) -> List[Tuple[str, str, int]]:
"""시가총액 순위 1페이지 조회.
2026-09 네이버가 finance.naver.com/sise/sise_market_sum.naver(HTML)를 폐기하고
stock.naver.com으로 302 리다이렉트 → m.stock.naver.com JSON API로 교체.
"""
market = _SOSOK_TO_MARKET.get(sosok, "KOSPI")
try:
resp = await client.get(
_BASE_URL,
params={"sosok": sosok, "page": page},
_LIST_API_URL.format(market=market),
params={"page": page, "pageSize": _LIST_PAGE_SIZE},
headers=_HEADERS,
timeout=10,
)
resp.raise_for_status()
return _parse_page(resp.content)
return _parse_json_page(resp.json())
except Exception as exc:
logger.warning("네이버 파이낸스 조회 실패 sosok=%d page=%d: %s", sosok, page, exc)
return []