유효하지 않은 KEY 가 설정되어 있어도, 일단 UI 페이지가 나오도록 수정.
This commit is contained in:
BIN
app/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
app/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/main.cpython-313.pyc
Normal file
BIN
app/__pycache__/main.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/core/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
app/core/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/core/__pycache__/auth.cpython-313.pyc
Normal file
BIN
app/core/__pycache__/auth.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/core/__pycache__/config.cpython-313.pyc
Normal file
BIN
app/core/__pycache__/config.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/core/__pycache__/database.cpython-313.pyc
Normal file
BIN
app/core/__pycache__/database.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/core/__pycache__/logger.cpython-313.pyc
Normal file
BIN
app/core/__pycache__/logger.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/core/__pycache__/rate_limiter.cpython-313.pyc
Normal file
BIN
app/core/__pycache__/rate_limiter.cpython-313.pyc
Normal file
Binary file not shown.
@@ -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]:
|
||||
|
||||
BIN
app/engine/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
app/engine/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/engine/__pycache__/collector.cpython-313.pyc
Normal file
BIN
app/engine/__pycache__/collector.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/engine/__pycache__/scheduler.cpython-313.pyc
Normal file
BIN
app/engine/__pycache__/scheduler.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/engine/__pycache__/strategy_engine.cpython-313.pyc
Normal file
BIN
app/engine/__pycache__/strategy_engine.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/engine/strategies/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
app/engine/strategies/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/engine/strategies/__pycache__/base.cpython-313.pyc
Normal file
BIN
app/engine/strategies/__pycache__/base.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/engine/strategies/__pycache__/conditional.cpython-313.pyc
Normal file
BIN
app/engine/strategies/__pycache__/conditional.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/engine/strategies/__pycache__/periodic.cpython-313.pyc
Normal file
BIN
app/engine/strategies/__pycache__/periodic.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/engine/strategies/__pycache__/technical.cpython-313.pyc
Normal file
BIN
app/engine/strategies/__pycache__/technical.cpython-313.pyc
Normal file
Binary file not shown.
17
app/main.py
17
app/main.py
@@ -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")
|
||||
|
||||
BIN
app/models/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
app/models/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/models/__pycache__/stock.cpython-313.pyc
Normal file
BIN
app/models/__pycache__/stock.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/routers/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
app/routers/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/routers/__pycache__/dashboard.cpython-313.pyc
Normal file
BIN
app/routers/__pycache__/dashboard.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/routers/__pycache__/stocks.cpython-313.pyc
Normal file
BIN
app/routers/__pycache__/stocks.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/routers/__pycache__/strategies.cpython-313.pyc
Normal file
BIN
app/routers/__pycache__/strategies.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/routers/__pycache__/trading.cpython-313.pyc
Normal file
BIN
app/routers/__pycache__/trading.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/routers/__pycache__/websocket.cpython-313.pyc
Normal file
BIN
app/routers/__pycache__/websocket.cpython-313.pyc
Normal file
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,
|
||||
|
||||
BIN
app/services/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
app/services/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/services/__pycache__/account.cpython-313.pyc
Normal file
BIN
app/services/__pycache__/account.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/services/__pycache__/market_data.cpython-313.pyc
Normal file
BIN
app/services/__pycache__/market_data.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/services/__pycache__/realtime.cpython-313.pyc
Normal file
BIN
app/services/__pycache__/realtime.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/services/__pycache__/trading.cpython-313.pyc
Normal file
BIN
app/services/__pycache__/trading.cpython-313.pyc
Normal file
Binary file not shown.
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user