Coverage for view / web / routes / stock.py: 95%
108 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-04 15:08 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-04 15:08 +0000
1"""
2종목 조회 관련 API 엔드포인트 (index.html).
3현재가, 차트(OHLCV), 기술 지표, 시장 상태, 환경 전환.
4"""
5import asyncio
6import time
7from fastapi import APIRouter, HTTPException, Query
8from common.types import Exchange
9from view.web.api_common import _get_ctx, _serialize_response, EnvironmentRequest
10import view.web.api_common as api_common
12router = APIRouter()
14# /api/status 결과를 5초간 캐시하여 페이지 전환 시 broker API 반복 호출 방지
15_status_cache = None
16_status_cache_ts = 0.0
17_STATUS_CACHE_TTL = 5.0
20@router.get("/status")
21async def get_status():
22 """시장 상태 및 환경 정보."""
23 global _status_cache, _status_cache_ts
25 ctx = _get_ctx()
26 if ctx is None:
27 return {"market_open": False, "env_type": "미설정", "current_time": "", "initialized": False}
29 now = time.monotonic()
30 if _status_cache is not None and (now - _status_cache_ts) < _STATUS_CACHE_TTL:
31 # 캐시된 결과 반환 (현재 시각만 갱신)
32 _status_cache["current_time"] = ctx.get_current_time_str()
33 return _status_cache
35 result = {
36 "market_open": await ctx.is_market_open_now(),
37 "env_type": ctx.get_env_type(),
38 "current_time": ctx.get_current_time_str(),
39 "initialized": ctx.initialized
40 }
41 _status_cache = result
42 _status_cache_ts = now
43 return result
46@router.get("/stocks/list")
47async def get_stocks_list():
48 """전 종목 리스트 반환 (클라이언트 자동완성용, localStorage 캐싱 대상)."""
49 ctx = _get_ctx()
50 stock_list = [
51 {"c": code, "n": name}
52 for name, code in ctx.stock_code_repository.name_to_code.items()
53 ]
54 return {"stocks": stock_list, "count": len(stock_list)}
57@router.get("/stock/search")
58async def search_stock_by_name(q: str = ""):
59 """종목명 부분 일치 검색 (자동완성용)."""
60 ctx = _get_ctx()
61 if not q or len(q.strip()) == 0:
62 return {"results": []}
63 results = ctx.stock_code_repository.search_by_name(q.strip())
64 return {"results": results}
67@router.get("/stock/{code}")
68async def get_stock_price(code: str, exchange: str = Query("KRX")):
69 """현재가 조회. 종목명이 들어오면 종목코드로 변환 후 조회. exchange=KRX|NXT|UN 선택 가능."""
70 ctx = _get_ctx()
71 # 숫자가 아닌 입력(종목명)이면 코드로 변환
72 if not code.isdigit():
73 resolved = ctx.stock_code_repository.get_code_by_name(code)
74 if not resolved: 74 ↛ 76line 74 didn't jump to line 76 because the condition on line 74 was always true
75 return {"rt_cd": "1", "msg1": f"종목명 '{code}'에 해당하는 종목코드를 찾을 수 없습니다.", "data": None}
76 code = resolved
77 try:
78 exchange_enum = Exchange(exchange.upper())
79 except ValueError:
80 exchange_enum = Exchange.KRX
81 t_start = ctx.pm.start_timer()
82 resp = await ctx.stock_query_service.handle_get_current_stock_price(code, caller="stock.py - get_stock_price", exchange=exchange_enum)
83 result = _serialize_response(resp)
85 ctx.pm.log_timer(f"get_stock_price({code})", t_start)
87 # 현재가 조회 후 OHLCV 2년치 백그라운드 프리로드 (캐시 miss 시에만 실제 API 호출)
88 async def _preload_ohlcv():
89 try:
90 await ctx.stock_query_service.get_ohlcv(code, caller="preload_on_price_query")
91 except Exception:
92 pass
93 asyncio.create_task(_preload_ohlcv())
95 return result
98@router.get("/chart/{code}")
99async def get_stock_chart(code: str, period: str = "D", indicators: bool = False, exchange: str = Query("KRX")):
100 """종목의 OHLCV 차트 데이터 조회 (기본 일봉). indicators=true 시 MA+BB 지표 포함. exchange=KRX|NXT|UN 선택 가능."""
101 ctx = _get_ctx()
102 try:
103 exchange_enum = Exchange(exchange.upper())
104 except ValueError:
105 exchange_enum = Exchange.KRX
106 t_start = ctx.pm.start_timer()
107 if indicators:
108 resp = await ctx.stock_query_service.get_ohlcv_with_indicators(code, period, caller="stock.py - get_stock_chart")
109 else:
110 resp = await ctx.stock_query_service.get_ohlcv(code, period, caller="stock.py - get_stock_chart", exchange=exchange_enum)
111 result = _serialize_response(resp)
113 ctx.pm.log_timer(f"get_stock_chart({code}, indicators={indicators})", t_start)
114 return result
117@router.get("/indicator/bollinger/{code}")
118async def get_bollinger_bands(code: str, period: int = 20, std_dev: float = 2.0):
119 """볼린저 밴드 조회 (기본: 20일, 2표준편차)"""
120 ctx = _get_ctx()
121 t_start = ctx.pm.start_timer()
122 resp = await ctx.indicator_service.get_bollinger_bands(code, period, std_dev)
123 result = _serialize_response(resp)
125 ctx.pm.log_timer(f"get_bollinger_bands({code})", t_start)
126 return result
129@router.get("/indicator/rsi/{code}")
130async def get_rsi(code: str, period: int = 14):
131 """RSI 조회 (기본: 14일)"""
132 ctx = _get_ctx()
133 t_start = ctx.pm.start_timer()
134 resp = await ctx.indicator_service.get_rsi(code, period)
135 result = _serialize_response(resp)
137 ctx.pm.log_timer(f"get_rsi({code})", t_start)
138 return result
141@router.get("/indicator/ma/{code}")
142async def get_moving_average(code: str, period: int = 20, method: str = "sma"):
143 """이동평균선 조회 (기본: 20일, sma)"""
144 ctx = _get_ctx()
145 t_start = ctx.pm.start_timer()
146 resp = await ctx.indicator_service.get_moving_average(code, period, method)
147 result = _serialize_response(resp)
149 ctx.pm.log_timer(f"get_moving_average({code})", t_start)
150 return result
153@router.post("/environment")
154async def change_environment(req: EnvironmentRequest):
155 """거래 환경 변경 (모의/실전)."""
156 global _status_cache, _status_cache_ts
157 ctx = api_common._ctx
158 if ctx is None:
159 raise HTTPException(status_code=503, detail="서비스가 초기화되지 않았습니다.")
160 success = await ctx.initialize_services(is_paper_trading=req.is_paper)
161 # 환경 전환 시 상태 캐시 무효화
162 _status_cache = None
163 _status_cache_ts = 0.0
164 if not success:
165 raise HTTPException(status_code=500, detail="환경 전환 실패 (토큰 발급 오류)")
166 ctx.start_background_tasks()
167 return {"success": True, "env_type": ctx.get_env_type()}