유효하지 않은 KEY 가 설정되어 있어도, 일단 UI 페이지가 나오도록 수정.
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+23
-3
@@ -28,12 +28,21 @@ class TokenManager:
|
||||
self._rate_limiter = RateLimiter(requests_per_second=1.0)
|
||||
self._lock = asyncio.Lock()
|
||||
self._client = httpx.AsyncClient(timeout=10.0)
|
||||
self._authenticated: bool = False
|
||||
|
||||
@property
|
||||
def is_authenticated(self) -> bool:
|
||||
return self._authenticated
|
||||
|
||||
@property
|
||||
def is_vps(self) -> bool:
|
||||
return settings.kis.server_mode == "vps"
|
||||
|
||||
async def _request_token(self) -> None:
|
||||
if not settings.kis.app_key or not settings.kis.app_secret:
|
||||
logger.debug("API 키가 설정되지 않음")
|
||||
return
|
||||
|
||||
url = f"{get_base_url()}{_TOKEN_URL}"
|
||||
payload = {
|
||||
"grant_type": "client_credentials",
|
||||
@@ -41,15 +50,22 @@ class TokenManager:
|
||||
"appsecret": settings.kis.app_secret,
|
||||
}
|
||||
resp = await self._client.post(url, json=payload)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
|
||||
if resp.status_code != 200 or "access_token" not in data:
|
||||
logger.warning("토큰 발급 실패: %s", data.get("message", resp.status_code))
|
||||
return
|
||||
|
||||
self._access_token = data["access_token"]
|
||||
expires_in = int(data.get("expires_in", 7776000))
|
||||
self._token_expires_at = datetime.now() + timedelta(seconds=expires_in)
|
||||
self._authenticated = True
|
||||
logger.info("REST access_token 발급 완료 (만료: %s)", self._token_expires_at)
|
||||
|
||||
async def _request_approval_key(self) -> None:
|
||||
if not settings.kis.app_key or not settings.kis.app_secret:
|
||||
return
|
||||
|
||||
url = f"{get_base_url()}{_APPROVAL_URL}"
|
||||
payload = {
|
||||
"grant_type": "client_credentials",
|
||||
@@ -57,9 +73,12 @@ class TokenManager:
|
||||
"secretkey": settings.kis.app_secret,
|
||||
}
|
||||
resp = await self._client.post(url, json=payload)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
|
||||
if resp.status_code != 200 or "approval_key" not in data:
|
||||
logger.warning("approval_key 발급 실패: %s", data.get("message", resp.status_code))
|
||||
return
|
||||
|
||||
self._approval_key = data["approval_key"]
|
||||
self._approval_expires_at = datetime.now() + timedelta(hours=23, minutes=50)
|
||||
logger.info("WebSocket approval_key 발급 완료 (만료: %s)", self._approval_expires_at)
|
||||
@@ -85,7 +104,8 @@ class TokenManager:
|
||||
"appsecret": settings.kis.app_secret,
|
||||
}
|
||||
resp = await self._client.post(url, json=data, headers=headers)
|
||||
resp.raise_for_status()
|
||||
if resp.status_code != 200:
|
||||
raise RuntimeError(f"hashkey 생성 실패: {resp.status_code}")
|
||||
return resp.json()["HASH"]
|
||||
|
||||
def get_auth_headers(self, tr_id: str) -> dict[str, str]:
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+13
-4
@@ -32,11 +32,20 @@ async def lifespan(app: FastAPI):
|
||||
|
||||
try:
|
||||
await token_manager.get_access_token()
|
||||
logger.info("KIS 인증 완료")
|
||||
if token_manager.is_authenticated:
|
||||
logger.info("KIS 인증 완료")
|
||||
else:
|
||||
logger.warning(
|
||||
"KIS 인증 실패 - API 키를 확인하세요 (.env 파일). "
|
||||
"UI는 표시되지만 시세 조회/매매 기능은 작동하지 않습니다."
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning("KIS 인증 실패 (API 키를 확인하세요): %s", e)
|
||||
logger.warning("KIS 인증 오류: %s", e)
|
||||
|
||||
start_scheduler()
|
||||
if token_manager.is_authenticated:
|
||||
start_scheduler()
|
||||
else:
|
||||
logger.info("인증 실패 - 스케줄러 미시작 (인증 후 수동 시작 가능)")
|
||||
|
||||
yield
|
||||
|
||||
@@ -67,7 +76,7 @@ templates = Jinja2Templates(directory="app/templates")
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
async def index(request: Request):
|
||||
return templates.TemplateResponse("index.html", {"request": request})
|
||||
return templates.TemplateResponse(request, "index.html")
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.auth import token_manager
|
||||
from app.core.database import get_db
|
||||
from app.models.stock import Holding, PriceHistory, Strategy, Trade, Stock
|
||||
from app.engine.scheduler import scheduler
|
||||
@@ -31,6 +32,7 @@ def dashboard(db: Session = Depends(get_db)) -> dict:
|
||||
})
|
||||
|
||||
return {
|
||||
"authenticated": token_manager.is_authenticated,
|
||||
"summary": {
|
||||
"total_holdings": len(holdings),
|
||||
"total_profit": total_profit,
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+12
-2
@@ -20,9 +20,17 @@ class AccountService:
|
||||
self._client = httpx.AsyncClient(timeout=10.0)
|
||||
self._rate_limiter = RateLimiter(settings.rate_limit.requests_per_second)
|
||||
|
||||
def _check_auth(self) -> bool:
|
||||
if not token_manager.is_authenticated:
|
||||
logger.debug("API 인증되지 않음 - 잔고 조회 불가")
|
||||
return False
|
||||
return True
|
||||
|
||||
async def get_balance(self) -> dict:
|
||||
if not self._check_auth():
|
||||
return {"deposits": [], "stocks": []}
|
||||
|
||||
await self._rate_limiter.acquire()
|
||||
token = await token_manager.get_access_token()
|
||||
tr_id = _BALANCE_TR_IDS.get(settings.kis.server_mode, "VTTC0311R")
|
||||
headers = token_manager.get_auth_headers(tr_id)
|
||||
|
||||
@@ -71,8 +79,10 @@ class AccountService:
|
||||
return {"deposits": deposits, "stocks": stocks}
|
||||
|
||||
async def get_orderable_amount(self, stock_code: str, price: int) -> dict:
|
||||
if not self._check_auth():
|
||||
return {"orderable_amount": 0, "orderable_qty": 0}
|
||||
|
||||
await self._rate_limiter.acquire()
|
||||
token = await token_manager.get_access_token()
|
||||
headers = token_manager.get_auth_headers("VTTC0830R" if settings.kis.server_mode == "vps" else "TTTC0830R")
|
||||
|
||||
params = {
|
||||
|
||||
@@ -22,9 +22,17 @@ class MarketDataService:
|
||||
self._client = httpx.AsyncClient(timeout=10.0)
|
||||
self._rate_limiter = RateLimiter(settings.rate_limit.requests_per_second)
|
||||
|
||||
def _check_auth(self) -> bool:
|
||||
if not token_manager.is_authenticated:
|
||||
logger.debug("API 인증되지 않음 - 시세 조회 불가")
|
||||
return False
|
||||
return True
|
||||
|
||||
async def get_current_price(self, stock_code: str) -> dict:
|
||||
if not self._check_auth():
|
||||
return {}
|
||||
|
||||
await self._rate_limiter.acquire()
|
||||
token = await token_manager.get_access_token()
|
||||
headers = token_manager.get_auth_headers("FHKST01010100")
|
||||
params = {_FID_COND_MRKT_DIV_CODE: "J", _FID_INPUT_ISCD: stock_code}
|
||||
url = f"{get_base_url()}{_PRICE_URL}"
|
||||
@@ -54,8 +62,10 @@ class MarketDataService:
|
||||
async def get_daily_chart(
|
||||
self, stock_code: str, start_date: str, end_date: str, count: int = 30
|
||||
) -> list[dict]:
|
||||
if not self._check_auth():
|
||||
return []
|
||||
|
||||
await self._rate_limiter.acquire()
|
||||
token = await token_manager.get_access_token()
|
||||
headers = token_manager.get_auth_headers("FHKST03010200")
|
||||
params = {
|
||||
_FID_COND_MRKT_DIV_CODE: "J",
|
||||
@@ -88,8 +98,10 @@ class MarketDataService:
|
||||
return result
|
||||
|
||||
async def get_orderbook(self, stock_code: str) -> dict:
|
||||
if not self._check_auth():
|
||||
return {}
|
||||
|
||||
await self._rate_limiter.acquire()
|
||||
token = await token_manager.get_access_token()
|
||||
headers = token_manager.get_auth_headers("FHKST01010200")
|
||||
params = {_FID_COND_MRKT_DIV_CODE: "J", _FID_INPUT_ISCD: stock_code}
|
||||
url = f"{get_base_url()}{_ORDERBOOK_URL}"
|
||||
|
||||
@@ -23,6 +23,12 @@ class TradingService:
|
||||
self._client = httpx.AsyncClient(timeout=10.0)
|
||||
self._rate_limiter = RateLimiter(settings.rate_limit.requests_per_second)
|
||||
|
||||
def _check_auth(self) -> bool:
|
||||
if not token_manager.is_authenticated:
|
||||
logger.debug("API 인증되지 않음 - 매매 불가")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _get_tr_id(self, side: str) -> str:
|
||||
mode = settings.kis.server_mode
|
||||
return _TR_IDS.get(mode, _TR_IDS["vps"]).get(side, "VTTC0802U")
|
||||
@@ -35,6 +41,9 @@ class TradingService:
|
||||
price: int = 0,
|
||||
order_type: str = "00",
|
||||
) -> dict:
|
||||
if not self._check_auth():
|
||||
return {"rt_cd": "-1", "msg": "API 인증되지 않음", "order_no": ""}
|
||||
|
||||
await self._rate_limiter.acquire()
|
||||
|
||||
tr_id = self._get_tr_id(side)
|
||||
@@ -86,6 +95,9 @@ class TradingService:
|
||||
async def modify_order(
|
||||
self, order_no: str, stock_code: str, qty: int, price: int, order_type: str = "00"
|
||||
) -> dict:
|
||||
if not self._check_auth():
|
||||
return {"rt_cd": "-1", "msg": "API 인증되지 않음"}
|
||||
|
||||
await self._rate_limiter.acquire()
|
||||
|
||||
tr_id = self._get_tr_id("modify")
|
||||
@@ -123,6 +135,9 @@ class TradingService:
|
||||
return result
|
||||
|
||||
async def cancel_order(self, order_no: str, stock_code: str, qty: int) -> dict:
|
||||
if not self._check_auth():
|
||||
return {"rt_cd": "-1", "msg": "API 인증되지 않음"}
|
||||
|
||||
await self._rate_limiter.acquire()
|
||||
|
||||
tr_id = self._get_tr_id("cancel")
|
||||
|
||||
@@ -41,6 +41,9 @@
|
||||
#ws-status { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; }
|
||||
#ws-status.connected { background: #3fb950; }
|
||||
#ws-status.disconnected { background: #f85149; }
|
||||
.alert { padding: 12px 16px; border-radius: 6px; margin-bottom: 16px; font-size: 13px; display: none; }
|
||||
.alert.warning { background: #3a2a00; border: 1px solid #d29922; color: #e3b341; }
|
||||
.alert.warning.show { display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -53,6 +56,11 @@
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div id="auth-alert" class="alert warning">
|
||||
API 키가 설정되지 않았거나 유효하지 않습니다. .env 파일에 KIS_APP_KEY와 KIS_APP_SECRET을 입력하세요.
|
||||
현재 UI 표시만 가능하며, 시세 조회 및 매매 기능은 작동하지 않습니다.
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<h3>총 투자금</h3>
|
||||
@@ -136,6 +144,11 @@
|
||||
try {
|
||||
const res = await fetch(API + '/api/dashboard/');
|
||||
const data = await res.json();
|
||||
|
||||
if (!data.authenticated) {
|
||||
document.getElementById('auth-alert').classList.add('show');
|
||||
}
|
||||
|
||||
const s = data.summary;
|
||||
|
||||
document.getElementById('total-invested').textContent = formatPrice(s.total_invested);
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user