diff --git a/.env b/.env
new file mode 100644
index 0000000..bc4651e
--- /dev/null
+++ b/.env
@@ -0,0 +1,16 @@
+# 한국투자증권 Open API 설정
+KIS_APP_KEY=your_app_key_here
+KIS_APP_SECRET=your_app_secret_here
+KIS_ACCOUNT_NO=12345678
+KIS_ACCOUNT_CODE=01
+KIS_HTS_ID=your_hts_id
+
+# 서버 모드 (real: 실전투자, vps: 모의투자)
+KIS_SERVER_MODE=vps
+
+# 앱 설정
+APP_HOST=0.0.0.0
+APP_PORT=8000
+APP_DEBUG=true
+DB_PATH=./data/stock.db
+LOG_LEVEL=INFO
diff --git a/.gitignore b/.gitignore
index e99e7a5..f338af9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,4 @@
./.venv
./app/__pycache__/
+
+*.pyc
diff --git a/app/routers/trading.py b/app/routers/trading.py
index b7fdf99..96fbbf2 100644
--- a/app/routers/trading.py
+++ b/app/routers/trading.py
@@ -1,9 +1,12 @@
from __future__ import annotations
-from fastapi import APIRouter, Depends, HTTPException
+from datetime import date, timedelta
+
+from fastapi import APIRouter, Depends
from pydantic import BaseModel
from sqlalchemy.orm import Session
+from app.core.auth import token_manager
from app.core.database import get_db
from app.models.stock import Trade
from app.services.trading import trading_service
@@ -62,32 +65,57 @@ async def cancel_order(data: CancelRequest) -> dict:
@router.get("/history")
-def get_trade_history(
+async def get_trade_history(
stock_code: str | None = None,
- limit: int = 50,
+ start_date: str | None = None,
+ end_date: str | None = None,
+ limit: int = 100,
db: Session = Depends(get_db),
-) -> list[dict]:
+) -> dict:
+ source = "local"
+
+ if token_manager.is_authenticated:
+ if not start_date:
+ start_date = (date.today() - timedelta(days=30)).strftime("%Y%m%d")
+ if not end_date:
+ end_date = date.today().strftime("%Y%m%d")
+
+ try:
+ kis_trades = await trading_service.get_daily_ccled(
+ start_date=start_date,
+ end_date=end_date,
+ stock_code=stock_code or "",
+ )
+ if kis_trades:
+ source = "kis"
+ return {"source": source, "trades": kis_trades[:limit]}
+ except Exception:
+ pass
+
query = db.query(Trade).order_by(Trade.created_at.desc())
if stock_code:
query = query.filter(Trade.stock_code == stock_code)
trades = query.limit(limit).all()
- return [
- {
- "id": t.id,
- "order_no": t.order_no,
- "stock_code": t.stock_code,
- "stock_name": t.stock_name,
- "side": t.side,
- "qty": t.qty,
- "price": t.price,
- "order_type": t.order_type,
- "status": t.status,
- "strategy_id": t.strategy_id,
- "created_at": t.created_at.isoformat() if t.created_at else None,
- "filled_at": t.filled_at.isoformat() if t.filled_at else None,
- }
- for t in trades
- ]
+ return {
+ "source": source,
+ "trades": [
+ {
+ "id": t.id,
+ "order_no": t.order_no,
+ "stock_code": t.stock_code,
+ "stock_name": t.stock_name,
+ "side": t.side,
+ "qty": t.qty,
+ "price": t.price,
+ "order_type": t.order_type,
+ "status": t.status,
+ "strategy_id": t.strategy_id,
+ "created_at": t.created_at.isoformat() if t.created_at else None,
+ "filled_at": t.filled_at.isoformat() if t.filled_at else None,
+ }
+ for t in trades
+ ],
+ }
@router.get("/account")
diff --git a/app/services/trading.py b/app/services/trading.py
index 7aec2d5..d8291c6 100644
--- a/app/services/trading.py
+++ b/app/services/trading.py
@@ -11,10 +11,23 @@ logger = setup_logger("trading")
_ORDER_URL = "/uapi/domestic-stock/v1/trading/order-cash"
_MODIFY_CANCEL_URL = "/uapi/domestic-stock/v1/trading/order-rvsecncl"
+_DAILY_CCLED_URL = "/uapi/domestic-stock/v1/trading/inquire-daily-ccld"
_TR_IDS = {
- "real": {"buy": "TTTC0802U", "sell": "TTTC0801U", "modify": "TTTC0803U", "cancel": "TTTC0804U"},
- "vps": {"buy": "VTTC0802U", "sell": "VTTC0801U", "modify": "VTTC0803U", "cancel": "VTTC0804U"},
+ "real": {
+ "buy": "TTTC0802U",
+ "sell": "TTTC0801U",
+ "modify": "TTTC0803U",
+ "cancel": "TTTC0804U",
+ "daily_ccled": "TTTC0012R",
+ },
+ "vps": {
+ "buy": "VTTC0802U",
+ "sell": "VTTC0801U",
+ "modify": "VTTC0803U",
+ "cancel": "VTTC0804U",
+ "daily_ccled": "VTTC0012R",
+ },
}
@@ -173,6 +186,77 @@ class TradingService:
return result
+ async def get_daily_ccled(
+ self,
+ start_date: str,
+ end_date: str,
+ stock_code: str = "",
+ ccled_dvsn: str = "0",
+ ) -> list[dict]:
+ """KIS API 일일 체결 내역 조회"""
+ if not self._check_auth():
+ return []
+
+ await self._rate_limiter.acquire()
+
+ tr_id = _TR_IDS.get(settings.kis.server_mode, _TR_IDS["vps"])["daily_ccled"]
+ headers = token_manager.get_auth_headers(tr_id)
+
+ params = {
+ "CANO": settings.kis.account_no,
+ "ACNT_PRDT_CD": settings.kis.account_code,
+ "FH_PDNO": stock_code,
+ "CCLD_DVSN": ccled_dvsn,
+ "INQR_STRT_DAY": start_date,
+ "INQR_END_DAY": end_date,
+ "WCRC_FRCR_DVSN": "0",
+ "CTAC_TLNO": "",
+ "MKET_ID": "",
+ }
+
+ url = f"{get_base_url()}{_DAILY_CCLED_URL}"
+ resp = await self._client.get(url, headers=headers, params=params)
+ resp.raise_for_status()
+ data = resp.json()
+
+ if data.get("rt_cd") != "0":
+ logger.warning("일일 체결 내역 조회 실패: %s", data.get("msg1"))
+ return []
+
+ trades = []
+ for item in data.get("output", []):
+ qty = int(item.get("ft_ccld_qty", 0))
+ if qty == 0:
+ continue
+
+ side_code = item.get("sll_buy_dvsn_cd", "")
+ if side_code in ("02", "06"):
+ side = "sell"
+ elif side_code in ("01", "03"):
+ side = "buy"
+ else:
+ side = "buy" if side_code == "01" else "sell"
+
+ trades.append({
+ "order_no": item.get("ord_gno_brno", ""),
+ "stock_code": item.get("pdno", ""),
+ "stock_name": item.get("prdt_name", ""),
+ "side": side,
+ "qty": qty,
+ "price": int(float(item.get("ft_ccld_unpr3", 0))),
+ "order_type": item.get("ord_dvsn", "00"),
+ "status": "filled",
+ "order_amount": float(item.get("ft_ord_amt", 0)),
+ "settlement_amount": float(item.get("ft_ccld_amt", 0)),
+ "tax": float(item.get("sttl_evlu_amt", 0)),
+ "commission": float(item.get("ft_lof_ruse_amt", 0)),
+ "created_at": item.get("ord_sttm", ""),
+ "filled_at": item.get("ft_ccld_no", ""),
+ })
+
+ logger.info("일일 체결 내역 조회: %d건", len(trades))
+ return trades
+
async def close(self) -> None:
await self._client.aclose()
diff --git a/app/templates/index.html b/app/templates/index.html
index 2806626..60d9632 100644
--- a/app/templates/index.html
+++ b/app/templates/index.html
@@ -6,18 +6,29 @@
Stock Automation Dashboard
-