Compare commits
2
Commits
d3f3931c38
...
f1ad744a14
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1ad744a14 | ||
|
|
182c2b38e6 |
@@ -1,16 +1,2 @@
|
||||
# 한국투자증권 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
|
||||
# 이 파일은 더 이상 사용되지 않습니다.
|
||||
# 설정은 config.yaml에서 관리합니다.
|
||||
|
||||
+6
-3
@@ -1,4 +1,7 @@
|
||||
./.venv
|
||||
./app/__pycache__/
|
||||
|
||||
.venv
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.auth_cache.json
|
||||
config.yaml
|
||||
.env
|
||||
output.log
|
||||
|
||||
+79
-20
@@ -1,10 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import hashlib
|
||||
import hmac
|
||||
import time
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
|
||||
@@ -17,6 +16,7 @@ logger = setup_logger("auth")
|
||||
_TOKEN_URL = "/oauth2/tokenP"
|
||||
_APPROVAL_URL = "/oauth2/Approval"
|
||||
_HASHKEY_URL = "/uapi/hashkey"
|
||||
_TOKEN_CACHE_PATH = Path(settings.app.db_path).parent / ".auth_cache.json"
|
||||
|
||||
|
||||
class TokenManager:
|
||||
@@ -27,8 +27,41 @@ class TokenManager:
|
||||
self._approval_expires_at: datetime = datetime.min
|
||||
self._rate_limiter = RateLimiter(requests_per_second=1.0)
|
||||
self._lock = asyncio.Lock()
|
||||
self._client = httpx.AsyncClient(timeout=10.0)
|
||||
self._client = httpx.AsyncClient(timeout=15.0)
|
||||
self._authenticated: bool = False
|
||||
self._load_cache()
|
||||
|
||||
def _load_cache(self) -> None:
|
||||
if not _TOKEN_CACHE_PATH.exists():
|
||||
return
|
||||
try:
|
||||
data = json.loads(_TOKEN_CACHE_PATH.read_text())
|
||||
now = datetime.now()
|
||||
token_exp = datetime.fromisoformat(data.get("token_expires_at", ""))
|
||||
if data.get("access_token") and now < token_exp:
|
||||
self._access_token = data["access_token"]
|
||||
self._token_expires_at = token_exp
|
||||
self._authenticated = True
|
||||
logger.info("캐시에서 토큰 복원 완료 (만료: %s)", token_exp)
|
||||
approval_exp = datetime.fromisoformat(data.get("approval_expires_at", ""))
|
||||
if data.get("approval_key") and now < approval_exp:
|
||||
self._approval_key = data["approval_key"]
|
||||
self._approval_expires_at = approval_exp
|
||||
logger.info("캐시에서 approval_key 복원 완료 (만료: %s)", approval_exp)
|
||||
except Exception as e:
|
||||
logger.debug("토큰 캐시 로드 실패: %s", e)
|
||||
|
||||
def _save_cache(self) -> None:
|
||||
try:
|
||||
_TOKEN_CACHE_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
_TOKEN_CACHE_PATH.write_text(json.dumps({
|
||||
"access_token": self._access_token,
|
||||
"token_expires_at": self._token_expires_at.isoformat(),
|
||||
"approval_key": self._approval_key,
|
||||
"approval_expires_at": self._approval_expires_at.isoformat(),
|
||||
}))
|
||||
except Exception as e:
|
||||
logger.debug("토큰 캐시 저장 실패: %s", e)
|
||||
|
||||
@property
|
||||
def is_authenticated(self) -> bool:
|
||||
@@ -49,18 +82,30 @@ class TokenManager:
|
||||
"appkey": settings.kis.app_key,
|
||||
"appsecret": settings.kis.app_secret,
|
||||
}
|
||||
resp = await self._client.post(url, json=payload)
|
||||
data = resp.json()
|
||||
|
||||
if resp.status_code != 200 or "access_token" not in data:
|
||||
logger.warning("토큰 발급 실패: %s", data.get("message", resp.status_code))
|
||||
for attempt in range(3):
|
||||
resp = await self._client.post(url, json=payload)
|
||||
data = resp.json()
|
||||
|
||||
if data.get("error_code") == "EGW00133":
|
||||
wait = 65 * (attempt + 1)
|
||||
logger.warning("토큰 발급 1분 제한 - %d초 후 재시도 (%d/3)", wait, attempt + 1)
|
||||
await asyncio.sleep(wait)
|
||||
continue
|
||||
|
||||
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
|
||||
self._save_cache()
|
||||
logger.info("REST access_token 발급 완료 (만료: %s)", self._token_expires_at)
|
||||
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)
|
||||
logger.error("토큰 발급 3회 모두 실패 (1분 제한)")
|
||||
|
||||
async def _request_approval_key(self) -> None:
|
||||
if not settings.kis.app_key or not settings.kis.app_secret:
|
||||
@@ -72,16 +117,30 @@ class TokenManager:
|
||||
"appkey": settings.kis.app_key,
|
||||
"secretkey": settings.kis.app_secret,
|
||||
}
|
||||
resp = await self._client.post(url, json=payload)
|
||||
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))
|
||||
for attempt in range(3):
|
||||
resp = await self._client.post(url, json=payload)
|
||||
data = resp.json()
|
||||
|
||||
if data.get("error_code") == "EGW00133":
|
||||
wait = 65 * (attempt + 1)
|
||||
logger.warning(
|
||||
"approval_key 발급 1분 제한 - %d초 후 재시도 (%d/3)", wait, attempt + 1
|
||||
)
|
||||
await asyncio.sleep(wait)
|
||||
continue
|
||||
|
||||
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)
|
||||
self._save_cache()
|
||||
logger.info("WebSocket approval_key 발급 완료 (만료: %s)", self._approval_expires_at)
|
||||
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)
|
||||
logger.error("approval_key 발급 3회 모두 실패 (1분 제한)")
|
||||
|
||||
async def get_access_token(self) -> str:
|
||||
async with self._lock:
|
||||
|
||||
+26
-22
@@ -1,33 +1,28 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
from pydantic import Field
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class KISConfig(BaseSettings):
|
||||
app_key: str = Field(default="", alias="KIS_APP_KEY")
|
||||
app_secret: str = Field(default="", alias="KIS_APP_SECRET")
|
||||
account_no: str = Field(default="", alias="KIS_ACCOUNT_NO")
|
||||
account_code: str = Field(default="01", alias="KIS_ACCOUNT_CODE")
|
||||
hts_id: str = Field(default="", alias="KIS_HTS_ID")
|
||||
server_mode: str = Field(default="vps", alias="KIS_SERVER_MODE")
|
||||
|
||||
model_config = {"env_file": ".env", "extra": "ignore"}
|
||||
class KISConfig:
|
||||
app_key: str = ""
|
||||
app_secret: str = ""
|
||||
account_no: str = ""
|
||||
account_code: str = "01"
|
||||
hts_id: str = ""
|
||||
server_mode: str = "vps"
|
||||
|
||||
|
||||
class AppConfig(BaseSettings):
|
||||
host: str = Field(default="0.0.0.0", alias="APP_HOST")
|
||||
port: int = Field(default=8000, alias="APP_PORT")
|
||||
debug: bool = Field(default=False, alias="APP_DEBUG")
|
||||
db_path: str = Field(default="./data/stock.db", alias="DB_PATH")
|
||||
log_level: str = Field(default="INFO", alias="LOG_LEVEL")
|
||||
|
||||
model_config = {"env_file": ".env", "extra": "ignore"}
|
||||
class AppConfig:
|
||||
name: str = "StockAutomation"
|
||||
version: str = "0.1.0"
|
||||
host: str = "0.0.0.0"
|
||||
port: int = 8000
|
||||
debug: bool = False
|
||||
db_path: str = "./data/stock.db"
|
||||
log_level: str = "INFO"
|
||||
|
||||
|
||||
class CollectorConfig:
|
||||
@@ -72,9 +67,18 @@ class Settings:
|
||||
with open(yaml_path, encoding="utf-8") as f:
|
||||
data: dict[str, Any] = yaml.safe_load(f) or {}
|
||||
|
||||
app = data.get("app", {})
|
||||
for key, val in app.items():
|
||||
if hasattr(self.app, key):
|
||||
setattr(self.app, key, val)
|
||||
|
||||
kis = data.get("kis", {})
|
||||
if "server_mode" in kis:
|
||||
self.kis.server_mode = kis["server_mode"]
|
||||
for key, val in kis.items():
|
||||
if key == "rate_limit":
|
||||
continue
|
||||
if hasattr(self.kis, key):
|
||||
setattr(self.kis, key, val)
|
||||
|
||||
rl = kis.get("rate_limit", {})
|
||||
if "requests_per_second" in rl:
|
||||
self.rate_limit.requests_per_second = rl["requests_per_second"]
|
||||
|
||||
@@ -44,6 +44,11 @@ async def lifespan(app: FastAPI):
|
||||
|
||||
if token_manager.is_authenticated:
|
||||
start_scheduler()
|
||||
try:
|
||||
await realtime_service.connect()
|
||||
logger.info("KIS WebSocket 연결 완료")
|
||||
except Exception as e:
|
||||
logger.warning("KIS WebSocket 연결 실패: %s", e)
|
||||
else:
|
||||
logger.info("인증 실패 - 스케줄러 미시작 (인증 후 수동 시작 가능)")
|
||||
|
||||
|
||||
+52
-18
@@ -5,22 +5,59 @@ 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.models.stock import Holding, Strategy, Trade, Stock
|
||||
from app.engine.scheduler import scheduler
|
||||
from app.services.account import account_service
|
||||
from app.services.realtime import realtime_service
|
||||
|
||||
router = APIRouter(prefix="/api/dashboard", tags=["dashboard"])
|
||||
|
||||
|
||||
@router.get("/")
|
||||
def dashboard(db: Session = Depends(get_db)) -> dict:
|
||||
holdings = db.query(Holding).all()
|
||||
async def dashboard(db: Session = Depends(get_db)) -> dict:
|
||||
recent_trades = db.query(Trade).order_by(Trade.created_at.desc()).limit(20).all()
|
||||
active_strategies = db.query(Strategy).filter(Strategy.is_active == True).all()
|
||||
active_stocks = db.query(Stock).filter(Stock.is_active == True).all()
|
||||
|
||||
total_profit = sum(h.profit for h in holdings)
|
||||
total_invested = sum(h.avg_price * h.qty for h in holdings if h.avg_price > 0)
|
||||
total_evaluated = sum(h.current_price * h.qty for h in holdings if h.current_price > 0)
|
||||
holdings = []
|
||||
total_profit = 0.0
|
||||
total_invested = 0.0
|
||||
total_evaluated = 0.0
|
||||
|
||||
if token_manager.is_authenticated:
|
||||
try:
|
||||
balance = await account_service.get_balance()
|
||||
for s in balance.get("stocks", []):
|
||||
holdings.append({
|
||||
"stock_code": s["stock_code"],
|
||||
"stock_name": s["stock_name"],
|
||||
"qty": s["qty"],
|
||||
"avg_price": s["avg_price"],
|
||||
"current_price": s["current_price"],
|
||||
"profit": s["profit"],
|
||||
"profit_rate": s["profit_rate"],
|
||||
})
|
||||
total_invested += s.get("buy_amount", s["avg_price"] * s["qty"])
|
||||
total_evaluated += s.get("eval_amount", s["current_price"] * s["qty"])
|
||||
total_profit += s["profit"]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not holdings:
|
||||
local = db.query(Holding).all()
|
||||
for h in local:
|
||||
holdings.append({
|
||||
"stock_code": h.stock_code,
|
||||
"stock_name": h.stock_name,
|
||||
"qty": h.qty,
|
||||
"avg_price": h.avg_price,
|
||||
"current_price": h.current_price,
|
||||
"profit": h.profit,
|
||||
"profit_rate": h.profit_rate,
|
||||
})
|
||||
total_invested += h.avg_price * h.qty
|
||||
total_evaluated += h.current_price * h.qty
|
||||
total_profit += h.profit
|
||||
|
||||
jobs = []
|
||||
if scheduler.running:
|
||||
@@ -42,18 +79,7 @@ def dashboard(db: Session = Depends(get_db)) -> dict:
|
||||
"active_strategies": len(active_strategies),
|
||||
"active_stocks": len(active_stocks),
|
||||
},
|
||||
"holdings": [
|
||||
{
|
||||
"stock_code": h.stock_code,
|
||||
"stock_name": h.stock_name,
|
||||
"qty": h.qty,
|
||||
"avg_price": h.avg_price,
|
||||
"current_price": h.current_price,
|
||||
"profit": h.profit,
|
||||
"profit_rate": h.profit_rate,
|
||||
}
|
||||
for h in holdings
|
||||
],
|
||||
"holdings": holdings,
|
||||
"recent_trades": [
|
||||
{
|
||||
"id": t.id,
|
||||
@@ -68,3 +94,11 @@ def dashboard(db: Session = Depends(get_db)) -> dict:
|
||||
],
|
||||
"scheduler_jobs": jobs,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/status")
|
||||
def get_status() -> dict:
|
||||
return {
|
||||
"authenticated": token_manager.is_authenticated,
|
||||
"realtime_connected": realtime_service.is_connected,
|
||||
}
|
||||
|
||||
+20
-2
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -63,7 +64,10 @@ def remove_stock(stock_code: str, db: Session = Depends(get_db)) -> dict:
|
||||
|
||||
@router.get("/{stock_code}/price")
|
||||
async def get_price(stock_code: str) -> dict:
|
||||
price = await market_data_service.get_current_price(stock_code)
|
||||
try:
|
||||
price = await market_data_service.get_current_price(stock_code)
|
||||
except (httpx.TimeoutException, httpx.ConnectError):
|
||||
raise HTTPException(status_code=504, detail="KIS 서버 응답 시간 초과")
|
||||
if not price:
|
||||
raise HTTPException(status_code=404, detail="시세 조회 실패")
|
||||
return price
|
||||
@@ -75,5 +79,19 @@ async def get_chart(stock_code: str, days: int = 30) -> list[dict]:
|
||||
|
||||
end = datetime.now().strftime("%Y%m%d")
|
||||
start = (datetime.now() - timedelta(days=days)).strftime("%Y%m%d")
|
||||
chart = await market_data_service.get_daily_chart(stock_code, start, end, days)
|
||||
try:
|
||||
chart = await market_data_service.get_daily_chart(stock_code, start, end, days)
|
||||
except (httpx.TimeoutException, httpx.ConnectError):
|
||||
raise HTTPException(status_code=504, detail="KIS 서버 응답 시간 초과")
|
||||
return chart
|
||||
|
||||
|
||||
@router.get("/{stock_code}/orderbook")
|
||||
async def get_orderbook(stock_code: str) -> dict:
|
||||
try:
|
||||
orderbook = await market_data_service.get_orderbook(stock_code)
|
||||
except (httpx.TimeoutException, httpx.ConnectError):
|
||||
raise HTTPException(status_code=504, detail="KIS 서버 응답 시간 초과")
|
||||
if not orderbook:
|
||||
raise HTTPException(status_code=404, detail="호가 조회 실패")
|
||||
return orderbook
|
||||
|
||||
@@ -19,7 +19,7 @@ _FID_COND_MRKT_DIV_CODE = "FID_COND_MRKT_DIV_CODE"
|
||||
|
||||
class MarketDataService:
|
||||
def __init__(self) -> None:
|
||||
self._client = httpx.AsyncClient(timeout=10.0)
|
||||
self._client = httpx.AsyncClient(timeout=30.0)
|
||||
self._rate_limiter = RateLimiter(settings.rate_limit.requests_per_second)
|
||||
|
||||
def _check_auth(self) -> bool:
|
||||
|
||||
@@ -30,6 +30,10 @@ class RealtimeService:
|
||||
self._heartbeat_task: asyncio.Task | None = None
|
||||
self._receive_task: asyncio.Task | None = None
|
||||
|
||||
@property
|
||||
def is_connected(self) -> bool:
|
||||
return self._ws is not None and self._running
|
||||
|
||||
def on(self, event: str, callback: Callable) -> None:
|
||||
self._callbacks.setdefault(event, []).append(callback)
|
||||
|
||||
@@ -40,7 +44,7 @@ class RealtimeService:
|
||||
try:
|
||||
self._ws = await websockets.connect(
|
||||
url,
|
||||
extra_headers={"approval_key": approval_key, "type": "Y"},
|
||||
additional_headers={"approval_key": approval_key, "type": "Y"},
|
||||
)
|
||||
self._running = True
|
||||
self._receive_task = asyncio.create_task(self._receive_loop())
|
||||
@@ -106,11 +110,14 @@ class RealtimeService:
|
||||
await self._handle_message(raw_msg)
|
||||
except websockets.ConnectionClosed:
|
||||
logger.warning("WebSocket 연결 끊김. 5초 후 재연결...")
|
||||
if self._running:
|
||||
await asyncio.sleep(5)
|
||||
await self.connect()
|
||||
except Exception as e:
|
||||
logger.error("WebSocket 수신 오류: %s", e)
|
||||
if self._running:
|
||||
await asyncio.sleep(5)
|
||||
try:
|
||||
await self.connect()
|
||||
except Exception:
|
||||
logger.error("WebSocket 재연결 실패")
|
||||
|
||||
async def _handle_message(self, raw_msg: str) -> None:
|
||||
try:
|
||||
|
||||
+209
-14
@@ -60,6 +60,19 @@
|
||||
.filter-row { display: flex; gap: 12px; align-items: end; margin-bottom: 16px; flex-wrap: wrap; }
|
||||
.positive { color: #3fb950; }
|
||||
.negative { color: #f85149; }
|
||||
.stock-header { display: flex; align-items: baseline; gap: 12px; margin-bottom: 16px; }
|
||||
.stock-header .name { font-size: 22px; font-weight: 700; color: #e1e4e8; }
|
||||
.stock-header .code { font-size: 14px; color: #8b949e; }
|
||||
.stock-price-main { font-size: 32px; font-weight: 700; margin-bottom: 4px; }
|
||||
.stock-change { font-size: 15px; margin-bottom: 20px; }
|
||||
.info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
|
||||
.orderbook-table { width: 100%; }
|
||||
.orderbook-table td { text-align: right; padding: 5px 10px; font-size: 13px; font-variant-numeric: tabular-nums; }
|
||||
.orderbook-table td:first-child { text-align: center; color: #8b949e; width: 40px; }
|
||||
.orderbook-table .ask-row td { color: #f85149; }
|
||||
.orderbook-table .bid-row td { color: #3fb950; }
|
||||
.orderbook-table .mid-row td { color: #8b949e; font-weight: 600; border-top: 1px solid #30363d; border-bottom: 1px solid #30363d; }
|
||||
.chart-canvas { width: 100%; height: 300px; background: #0d1117; border-radius: 6px; margin-top: 12px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -74,6 +87,9 @@
|
||||
<a href="#" data-page="history">
|
||||
<span class="icon">📋</span> 과거 기록 조회
|
||||
</a>
|
||||
<a href="#" data-page="stock-info">
|
||||
<span class="icon">📈</span> 종목 조회
|
||||
</a>
|
||||
</nav>
|
||||
<div class="sidebar-footer">v0.1.0</div>
|
||||
</aside>
|
||||
@@ -91,7 +107,7 @@
|
||||
<!-- 실거래 페이지 -->
|
||||
<div id="page-trading" class="page active">
|
||||
<div id="auth-alert" class="alert warning">
|
||||
API 키가 설정되지 않았거나 유효하지 않습니다. .env 파일에 KIS_APP_KEY와 KIS_APP_SECRET을 입력하세요.
|
||||
API 키가 설정되지 않았거나 유효하지 않습니다. config.yaml 파일에 KIS 설정을 확인하세요.
|
||||
현재 UI 표시만 가능하며, 시세 조회 및 매매 기능은 작동하지 않습니다.
|
||||
</div>
|
||||
|
||||
@@ -200,6 +216,65 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 종목 조회 페이지 -->
|
||||
<div id="page-stock-info" class="page">
|
||||
<div class="section">
|
||||
<div class="filter-row">
|
||||
<div class="form-group">
|
||||
<label>종목코드</label>
|
||||
<input id="stock-search-code" type="text" placeholder="005930" />
|
||||
</div>
|
||||
<button class="btn primary" onclick="loadStockInfo()">조회</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="stock-info-content" style="display:none;">
|
||||
<div class="section">
|
||||
<div class="stock-header">
|
||||
<span class="name" id="si-name">-</span>
|
||||
<span class="code" id="si-code">-</span>
|
||||
</div>
|
||||
<div class="stock-price-main" id="si-price">-</div>
|
||||
<div class="stock-change" id="si-change">-</div>
|
||||
|
||||
<div class="info-grid">
|
||||
<div>
|
||||
<table>
|
||||
<tr><th style="width:100px;">시가</th><td id="si-open">-</td></tr>
|
||||
<tr><th>고가</th><td id="si-high">-</td></tr>
|
||||
<tr><th>저가</th><td id="si-low">-</td></tr>
|
||||
<tr><th>거래량</th><td id="si-volume">-</td></tr>
|
||||
<tr><th>거래대금</th><td id="si-amount">-</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
<div>
|
||||
<table class="orderbook-table">
|
||||
<thead><tr><th>#</th><th>매도</th><th>수량</th></tr></thead>
|
||||
<tbody id="ob-asks"></tbody>
|
||||
<tr class="mid-row"><td></td><td id="ob-mid">-</td><td></td></tr>
|
||||
<thead><tr><th>#</th><th>매수</th><th>수량</th></tr></thead>
|
||||
<tbody id="ob-bids"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>일봉 차트</h2>
|
||||
<div style="display:flex;gap:8px;margin-bottom:8px;">
|
||||
<button class="btn" onclick="loadChart(30)">30일</button>
|
||||
<button class="btn" onclick="loadChart(60)">60일</button>
|
||||
<button class="btn" onclick="loadChart(120)">120일</button>
|
||||
</div>
|
||||
<canvas id="chart-canvas" class="chart-canvas"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="stock-info-empty" style="display:none; text-align:center; padding:40px; color:#8b949e;">
|
||||
종목코드를 입력하고 조회 버튼을 누르세요.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -336,25 +411,145 @@
|
||||
function connectWS() {
|
||||
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
|
||||
ws = new WebSocket(`${proto}://${location.host}/ws/realtime`);
|
||||
ws.onopen = () => {
|
||||
document.getElementById('ws-status').className = 'connected';
|
||||
document.getElementById('status-text').textContent = '실시간 연결됨';
|
||||
};
|
||||
ws.onclose = () => {
|
||||
document.getElementById('ws-status').className = 'disconnected';
|
||||
document.getElementById('status-text').textContent = '연결 끊김 - 재연결 중...';
|
||||
setTimeout(connectWS, 3000);
|
||||
};
|
||||
ws.onmessage = (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.type === 'price') loadDashboard();
|
||||
};
|
||||
ws.onclose = () => setTimeout(connectWS, 3000);
|
||||
}
|
||||
|
||||
async function loadStatus() {
|
||||
try {
|
||||
const res = await fetch(API + '/api/dashboard/status');
|
||||
const data = await res.json();
|
||||
const el = document.getElementById('ws-status');
|
||||
const text = document.getElementById('status-text');
|
||||
if (data.realtime_connected) {
|
||||
el.className = 'connected';
|
||||
text.textContent = '실시간 연결됨';
|
||||
} else {
|
||||
el.className = 'disconnected';
|
||||
text.textContent = data.authenticated ? '연결 끊김' : '인증 필요';
|
||||
}
|
||||
} catch (e) { console.error('상태 조회 실패:', e); }
|
||||
}
|
||||
|
||||
async function loadStockInfo() {
|
||||
const code = document.getElementById('stock-search-code').value.trim();
|
||||
if (!code) return;
|
||||
const content = document.getElementById('stock-info-content');
|
||||
const empty = document.getElementById('stock-info-empty');
|
||||
content.style.display = 'none';
|
||||
empty.style.display = 'none';
|
||||
try {
|
||||
const [priceRes, obRes] = await Promise.all([
|
||||
fetch(API + `/api/stocks/${code}/price`),
|
||||
fetch(API + `/api/stocks/${code}/orderbook`),
|
||||
]);
|
||||
if (!priceRes.ok) { empty.style.display = 'block'; empty.textContent = '시세 조회 실패'; return; }
|
||||
const p = await priceRes.json();
|
||||
document.getElementById('si-name').textContent = p.stock_name || code;
|
||||
document.getElementById('si-code').textContent = code;
|
||||
const priceEl = document.getElementById('si-price');
|
||||
priceEl.textContent = formatPrice(p.current_price);
|
||||
priceEl.className = 'stock-price-main ' + (p.change_price >= 0 ? 'positive' : 'negative');
|
||||
const chEl = document.getElementById('si-change');
|
||||
const sign = p.change_price >= 0 ? '+' : '';
|
||||
chEl.textContent = `${sign}${p.change_price.toLocaleString()} (${sign}${p.change_rate.toFixed(2)}%)`;
|
||||
chEl.className = 'stock-change ' + (p.change_price >= 0 ? 'positive' : 'negative');
|
||||
document.getElementById('si-open').textContent = formatPrice(p.open_price);
|
||||
document.getElementById('si-high').textContent = formatPrice(p.high_price);
|
||||
document.getElementById('si-low').textContent = formatPrice(p.low_price);
|
||||
document.getElementById('si-volume').textContent = p.volume ? p.volume.toLocaleString() : '-';
|
||||
document.getElementById('si-amount').textContent = p.trade_amount ? formatPrice(p.trade_amount) : '-';
|
||||
if (obRes.ok) {
|
||||
const ob = await obRes.json();
|
||||
const askTbody = document.getElementById('ob-asks');
|
||||
askTbody.innerHTML = ob.ask_prices.slice().reverse().map((price, i) => {
|
||||
const vol = ob.ask_volumes[4 - i] || 0;
|
||||
return `<tr class="ask-row"><td>${5 - i}</td><td>${price ? price.toLocaleString() : '-'}</td><td>${vol.toLocaleString()}</td></tr>`;
|
||||
}).join('');
|
||||
document.getElementById('ob-mid').textContent = p.current_price ? p.current_price.toLocaleString() : '-';
|
||||
const bidTbody = document.getElementById('ob-bids');
|
||||
bidTbody.innerHTML = ob.bid_prices.map((price, i) => {
|
||||
const vol = ob.bid_volumes[i] || 0;
|
||||
return `<tr class="bid-row"><td>${i + 1}</td><td>${price ? price.toLocaleString() : '-'}</td><td>${vol.toLocaleString()}</td></tr>`;
|
||||
}).join('');
|
||||
}
|
||||
content.style.display = 'block';
|
||||
loadChart(30);
|
||||
} catch (e) {
|
||||
console.error('종목 조회 실패:', e);
|
||||
empty.style.display = 'block';
|
||||
empty.textContent = '조회 중 오류가 발생했습니다.';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadChart(days) {
|
||||
const code = document.getElementById('stock-search-code').value.trim();
|
||||
if (!code) return;
|
||||
try {
|
||||
const res = await fetch(API + `/api/stocks/${code}/chart?days=${days}`);
|
||||
const data = await res.json();
|
||||
if (!data.length) return;
|
||||
drawChart(data);
|
||||
} catch (e) { console.error('차트 조회 실패:', e); }
|
||||
}
|
||||
|
||||
function drawChart(data) {
|
||||
const canvas = document.getElementById('chart-canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const rect = canvas.parentElement.getBoundingClientRect();
|
||||
canvas.width = rect.width;
|
||||
canvas.height = 300;
|
||||
const W = canvas.width, H = canvas.height;
|
||||
const pad = { top: 20, right: 60, bottom: 30, left: 10 };
|
||||
const chartW = W - pad.left - pad.right;
|
||||
const chartH = H - pad.top - pad.bottom;
|
||||
ctx.clearRect(0, 0, W, H);
|
||||
const prices = data.flatMap(d => [d.high, d.low]);
|
||||
const minP = Math.min(...prices);
|
||||
const maxP = Math.max(...prices);
|
||||
const range = maxP - minP || 1;
|
||||
const barW = Math.max(1, (chartW / data.length) - 1);
|
||||
const gap = chartW / data.length;
|
||||
const toY = (p) => pad.top + chartH - ((p - minP) / range) * chartH;
|
||||
ctx.strokeStyle = '#30363d';
|
||||
ctx.lineWidth = 0.5;
|
||||
for (let i = 0; i <= 4; i++) {
|
||||
const y = pad.top + (chartH / 4) * i;
|
||||
ctx.beginPath(); ctx.moveTo(pad.left, y); ctx.lineTo(W - pad.right, y); ctx.stroke();
|
||||
const val = maxP - (range / 4) * i;
|
||||
ctx.fillStyle = '#8b949e'; ctx.font = '11px sans-serif'; ctx.textAlign = 'left';
|
||||
ctx.fillText(val.toLocaleString(), W - pad.right + 4, y + 4);
|
||||
}
|
||||
data.forEach((d, i) => {
|
||||
const x = pad.left + gap * i + gap / 2;
|
||||
const isUp = d.close >= d.open;
|
||||
ctx.strokeStyle = isUp ? '#3fb950' : '#f85149';
|
||||
ctx.fillStyle = isUp ? '#3fb950' : '#f85149';
|
||||
ctx.beginPath(); ctx.moveTo(x, toY(d.high)); ctx.lineTo(x, toY(d.low)); ctx.stroke();
|
||||
const oY = toY(d.open), cY = toY(d.close);
|
||||
const bodyTop = Math.min(oY, cY);
|
||||
const bodyH = Math.max(Math.abs(oY - cY), 1);
|
||||
ctx.fillRect(x - barW / 2, bodyTop, barW, bodyH);
|
||||
});
|
||||
if (data.length > 0) {
|
||||
const step = Math.max(1, Math.floor(data.length / 6));
|
||||
ctx.fillStyle = '#8b949e'; ctx.font = '11px sans-serif'; ctx.textAlign = 'center';
|
||||
for (let i = 0; i < data.length; i += step) {
|
||||
const x = pad.left + gap * i + gap / 2;
|
||||
const label = data[i].date;
|
||||
ctx.fillText(label.slice(4, 6) + '/' + label.slice(6, 8), x, H - 8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadDashboard();
|
||||
loadStatus();
|
||||
setInterval(loadDashboard, 10000);
|
||||
setInterval(loadStatus, 5000);
|
||||
connectWS();
|
||||
initHistoryDates();
|
||||
document.getElementById('stock-search-code').addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') loadStockInfo();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+11
-1
@@ -1,11 +1,21 @@
|
||||
app:
|
||||
name: "StockAutomation"
|
||||
version: "0.1.0"
|
||||
host: "0.0.0.0"
|
||||
port: 8000
|
||||
debug: true
|
||||
db_path: "./data/stock.db"
|
||||
log_level: "INFO"
|
||||
|
||||
kis:
|
||||
app_key: "PS1Hj6x9hy7Rw1bL1nPn3X2zw4VExJsGp7hJ"
|
||||
app_secret: "IGeQPUmgQZvdQgH3pHUD6dOIefRpWtRD6TSzQhwx5grj7pPyvOeUK0mxG+g4jsP6e89UJQnisWE6yLelLnOrhemftWoJS/ld1+xyYbMgHy230SzKU2cZbje89WNlVYoXn25NWbrjMTsi7Y1WdX3eZ0IswH/VQPtYYxj7ZkwrjBXJAvvgXV0="
|
||||
account_no: "50198112"
|
||||
account_code: "01"
|
||||
hts_id: "moneman"
|
||||
server_mode: "vps" # vps: 모의투자, real: 실전투자
|
||||
rate_limit:
|
||||
requests_per_second: 1.0 # 모의투자: 1.0, 실전투자: 2.0
|
||||
requests_per_second: 2.0 # 모의투자/실전투자 공통: 초당 2회
|
||||
retry_delay: 1.5
|
||||
|
||||
collector:
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/*****************************************************************************
|
||||
* 코스닥 종목 코드 파일 구조
|
||||
****************************************************************************/
|
||||
typedef struct
|
||||
{
|
||||
char mksc_shrn_iscd[SZ_SHRNCODE]; /* 단축코드 */
|
||||
char stnd_iscd[SZ_STNDCODE]; /* 표준코드 */
|
||||
char hts_kor_isnm[SZ_KORNAME]; /* 한글종목명 */
|
||||
char scrt_grp_cls_code[2]; /* 증권그룹구분코드 */
|
||||
/* ST:주권 MF:증권투자회사 RT:부동산투자회사 */
|
||||
/* SC:선박투자회사 IF:사회간접자본투융자회사 */
|
||||
/* DR:주식예탁증서 EW:ELW EF:ETF */
|
||||
/* SW:신주인수권증권 SR:신주인수권증서 */
|
||||
/* BC:수익증권 FE:해외ETF FS:외국주권 */
|
||||
char avls_scal_cls_code[1]; /* 시가총액 규모 구분 코드 유가 */
|
||||
/* 0:제외 1:KOSDAQ100 2:KOSDAQmid300 3:KOSDAQsmall) */
|
||||
char bstp_larg_div_code[4]; /* 지수업종 대분류 코드 */
|
||||
char bstp_medm_div_code[4]; /* 지수 업종 중분류 코드 */
|
||||
char bstp_smal_div_code[4]; /* 지수업종 소분류 코드 */
|
||||
char vntr_issu_yn[1]; /* 벤처기업 여부 (Y/N) */
|
||||
char low_current_yn[1]; /* 저유동성종목 여부 */
|
||||
char krx_issu_yn[1]; /* KRX 종목 여부 */
|
||||
char etp_prod_cls_code[1]; /* ETP 상품구분코드 */
|
||||
/* 0:해당없음 1:투자회사형 2:수익증권형 */
|
||||
/* 3:ETN 4:손실제한ETN */
|
||||
char krx100_issu_yn[1]; /* KRX100 종목 여부 (Y/N) */
|
||||
char krx_car_yn[1]; /* KRX 자동차 여부 */
|
||||
char krx_smcn_yn[1]; /* KRX 반도체 여부 */
|
||||
char krx_bio_yn[1]; /* KRX 바이오 여부 */
|
||||
char krx_bank_yn[1]; /* KRX 은행 여부 */
|
||||
char etpr_undt_objt_co_yn[1]; /* 기업인수목적회사여부 */
|
||||
char krx_enrg_chms_yn[1]; /* KRX 에너지 화학 여부 */
|
||||
char krx_stel_yn[1]; /* KRX 철강 여부 */
|
||||
char short_over_cls_code[1]; /* 단기과열종목구분코드 0:해당없음 */
|
||||
/* 1:지정예고 2:지정 3:지정연장(해제연기) */
|
||||
char krx_medi_cmnc_yn[1]; /* KRX 미디어 통신 여부 */
|
||||
char krx_cnst_yn[1]; /* KRX 건설 여부 */
|
||||
char invt_alrm_yn[1]; /* (코스닥)투자주의환기종목여부 */
|
||||
char krx_scrt_yn [1]; /* KRX 증권 구분 */
|
||||
char krx_ship_yn [1]; /* KRX 선박 구분 */
|
||||
char krx_insu_yn[1]; /* KRX섹터지수 보험여부 */
|
||||
char krx_trnp_yn[1]; /* KRX섹터지수 운송여부 */
|
||||
char ksq150_nmix_yn[1]; /* KOSDAQ150지수여부 (Y,N) */
|
||||
char stck_sdpr[9]; /* 주식 기준가 */
|
||||
char frml_mrkt_deal_qty_unit[5]; /* 정규 시장 매매 수량 단위 */
|
||||
char ovtm_mrkt_deal_qty_unit[5]; /* 시간외 시장 매매 수량 단위 */
|
||||
char trht_yn[1]; /* 거래정지 여부 */
|
||||
char sltr_yn[1]; /* 정리매매 여부 */
|
||||
char mang_issu_yn[1]; /* 관리 종목 여부 */
|
||||
char mrkt_alrm_cls_code[2]; /* 시장 경고 구분 코드 (00:해당없음 01:투자주의 */
|
||||
/* 02:투자경고 03:투자위험) */
|
||||
char mrkt_alrm_risk_adnt_yn[1]; /* 시장 경고위험 예고 여부 */
|
||||
char insn_pbnt_yn[1]; /* 불성실 공시 여부 */
|
||||
char byps_lstn_yn[1]; /* 우회 상장 여부 */
|
||||
char flng_cls_code[2]; /* 락구분 코드 (00:해당사항없음 01:권리락 */
|
||||
/* 02:배당락 03:분배락 04:권배락 05:중간배당락 */
|
||||
/* 06:권리중간배당락 99:기타 */
|
||||
/* SW,SR,EW는 미해당(SPACE) */
|
||||
char fcam_mod_cls_code[2]; /* 액면가 변경 구분 코드 (00:해당없음 */
|
||||
/* 01:액면분할 02:액면병합 99:기타 */
|
||||
char icic_cls_code[2]; /* 증자 구분 코드 (00:해당없음 01:유상증자 */
|
||||
/* 02:무상증자 03:유무상증자 99:기타) */
|
||||
char marg_rate[3]; /* 증거금 비율 */
|
||||
char crdt_able[1]; /* 신용주문 가능 여부 */
|
||||
char crdt_days[3]; /* 신용기간 */
|
||||
char prdy_vol[12]; /* 전일 거래량 */
|
||||
char stck_fcam[12]; /* 주식 액면가 */
|
||||
char stck_lstn_date[8]; /* 주식 상장 일자 */
|
||||
char lstn_stcn[15]; /* 상장 주수(천) */
|
||||
char cpfn[21]; /* 자본금 */
|
||||
char stac_month[2]; /* 결산 월 */
|
||||
char po_prc[7]; /* 공모 가격 */
|
||||
char prst_cls_code[1]; /* 우선주 구분 코드 (0:해당없음(보통주) */
|
||||
/* 1:구형우선주 2:신형우선주 */
|
||||
char ssts_hot_yn[1]; /* 공매도과열종목여부 */
|
||||
char stange_runup_yn[1]; /* 이상급등종목여부 */
|
||||
char krx300_issu_yn[1]; /* KRX300 종목 여부 (Y/N) */
|
||||
char sale_account[9]; /* 매출액 */
|
||||
char bsop_prfi[9]; /* 영업이익 */
|
||||
char op_prfi[9]; /* 경상이익 */
|
||||
char thtr_ntin[5]; /* 당기순이익 */
|
||||
char roe[9]; /* ROE(자기자본이익률) */
|
||||
char base_date[8]; /* 기준년월 */
|
||||
char prdy_avls_scal[9]; /* 전일기준 시가총액 (억) */
|
||||
char grp_code[3]; /* 그룹사 코드 */
|
||||
char co_crdt_limt_over_yn[1]; /* 회사신용한도초과여부 */
|
||||
char secu_lend_able_yn[1]; /* 담보대출가능여부 */
|
||||
char stln_able_yn[1]; /* 대주가능여부 */
|
||||
} ST_KSQ_CODE;
|
||||
@@ -0,0 +1,99 @@
|
||||
/*****************************************************************************
|
||||
* 코스피 종목 코드 파일 구조
|
||||
****************************************************************************/
|
||||
typedef struct
|
||||
{
|
||||
char mksc_shrn_iscd[SZ_SHRNCODE]; /* 단축코드 */
|
||||
char stnd_iscd[SZ_STNDCODE]; /* 표준코드 */
|
||||
char hts_kor_isnm[SZ_KORNAME]; /* 한글종목명 */
|
||||
char scrt_grp_cls_code[2]; /* 증권그룹구분코드 */
|
||||
/* ST:주권 MF:증권투자회사 RT:부동산투자회사 */
|
||||
/* SC:선박투자회사 IF:사회간접자본투융자회사 */
|
||||
/* DR:주식예탁증서 EW:ELW EF:ETF */
|
||||
/* SW:신주인수권증권 SR:신주인수권증서 */
|
||||
/* BC:수익증권 FE:해외ETF FS:외국주권 */
|
||||
char avls_scal_cls_code[1]; /* 시가총액 규모 구분 코드 유가 */
|
||||
/* (0:제외 1:대 2:중 3:소) */
|
||||
char bstp_larg_div_code[4]; /* 지수 업종 대분류 코드 */
|
||||
char bstp_medm_div_code[4]; /* 지수 업종 중분류 코드 */
|
||||
char bstp_smal_div_code[4]; /* 지수 업종 소분류 코드 */
|
||||
char mnin_cls_code_yn[1]; /* 제조업 구분 코드 (Y/N) */
|
||||
char low_current_yn[1]; /* 저유동성종목 여부 */
|
||||
char sprn_strr_nmix_issu_yn[1]; /* 지배 구조 지수 종목 여부 (Y/N) */
|
||||
char kospi200_apnt_cls_code[1]; /* KOSPI200 섹터업종(20110401 변경됨) */
|
||||
/* 0:해당없음 1:건설 2:중공업 3:철강소재 */
|
||||
/* 4:에너지화학 5:정보통신 6:금융 7:필수소비재 */
|
||||
/* 8:자유소비재 9:산업재 */
|
||||
/* A:건강관리 B:커뮤니케이션서비스 */
|
||||
char kospi100_issu_yn[1]; /* KOSPI100여부 */
|
||||
char kospi50_issu_yn[1]; /* KOSPI50 종목 여부 */
|
||||
char krx_issu_yn[1]; /* KRX 종목 여부 */
|
||||
char etp_prod_cls_code[1]; /* ETP 상품구분코드 */
|
||||
/* 0:해당없음 1:투자회사형 2:수익증권형 */
|
||||
/* 3:ETN 4:손실제한ETN 5:상장형수익증권 */
|
||||
char elw_pblc_yn[1]; /* ELW 발행여부 (Y/N) */
|
||||
char krx100_issu_yn[1]; /* KRX100 종목 여부 (Y/N) */
|
||||
char krx_car_yn[1]; /* KRX 자동차 여부 */
|
||||
char krx_smcn_yn[1]; /* KRX 반도체 여부 */
|
||||
char krx_bio_yn[1]; /* KRX 바이오 여부 */
|
||||
char krx_bank_yn[1]; /* KRX 은행 여부 */
|
||||
char etpr_undt_objt_co_yn[1]; /* 기업인수목적회사여부 */
|
||||
char krx_enrg_chms_yn[1]; /* KRX 에너지 화학 여부 */
|
||||
char krx_stel_yn[1]; /* KRX 철강 여부 */
|
||||
char short_over_cls_code[1]; /* 단기과열종목구분코드 0:해당없음 */
|
||||
/* 1:지정예고 2:지정 3:지정연장(해제연기) */
|
||||
char krx_medi_cmnc_yn[1]; /* KRX 미디어 통신 여부 */
|
||||
char krx_cnst_yn[1]; /* KRX 건설 여부 */
|
||||
char krx_fnnc_svc_yn[1]; /* 삭제됨(20151218) */
|
||||
char krx_scrt_yn [1]; /* KRX 증권 구분 */
|
||||
char krx_ship_yn [1]; /* KRX 선박 구분 */
|
||||
char krx_insu_yn[1]; /* KRX섹터지수 보험여부 */
|
||||
char krx_trnp_yn[1]; /* KRX섹터지수 운송여부 */
|
||||
char sri_nmix_yn[1]; /* SRI 지수여부 (Y,N) */
|
||||
char stck_sdpr[9]; /* 주식 기준가 */
|
||||
char frml_mrkt_deal_qty_unit[5]; /* 정규 시장 매매 수량 단위 */
|
||||
char ovtm_mrkt_deal_qty_unit[5]; /* 시간외 시장 매매 수량 단위 */
|
||||
char trht_yn[1]; /* 거래정지 여부 */
|
||||
char sltr_yn[1]; /* 정리매매 여부 */
|
||||
char mang_issu_yn[1]; /* 관리 종목 여부 */
|
||||
char mrkt_alrm_cls_code[2]; /* 시장 경고 구분 코드 (00:해당없음 01:투자주의 */
|
||||
/* 02:투자경고 03:투자위험 */
|
||||
char mrkt_alrm_risk_adnt_yn[1]; /* 시장 경고위험 예고 여부 */
|
||||
char insn_pbnt_yn[1]; /* 불성실 공시 여부 */
|
||||
char byps_lstn_yn[1]; /* 우회 상장 여부 */
|
||||
char flng_cls_code[2]; /* 락구분 코드 (00:해당사항없음 01:권리락 */
|
||||
/* 02:배당락 03:분배락 04:권배락 05:중간배당락 */
|
||||
/* 06:권리중간배당락 99:기타 */
|
||||
/* S?W,SR,EW는 미해당(SPACE) */
|
||||
char fcam_mod_cls_code[2]; /* 액면가 변경 구분 코드 (00:해당없음 */
|
||||
/* 01:액면분할 02:액면병합 99:기타 */
|
||||
char icic_cls_code[2]; /* 증자 구분 코드 (00:해당없음 01:유상증자 */
|
||||
/* 02:무상증자 03:유무상증자 99:기타) */
|
||||
char marg_rate[3]; /* 증거금 비율 */
|
||||
char crdt_able[1]; /* 신용주문 가능 여부 */
|
||||
char crdt_days[3]; /* 신용기간 */
|
||||
char prdy_vol[12]; /* 전일 거래량 */
|
||||
char stck_fcam[12]; /* 주식 액면가 */
|
||||
char stck_lstn_date[8]; /* 주식 상장 일자 */
|
||||
char lstn_stcn[15]; /* 상장 주수(천) */
|
||||
char cpfn[21]; /* 자본금 */
|
||||
char stac_month[2]; /* 결산 월 */
|
||||
char po_prc[7]; /* 공모 가격 */
|
||||
char prst_cls_code[1]; /* 우선주 구분 코드 (0:해당없음(보통주) */
|
||||
/* 1:구형우선주 2:신형우선주 */
|
||||
char ssts_hot_yn[1]; /* 공매도과열종목여부 */
|
||||
char stange_runup_yn[1]; /* 이상급등종목여부 */
|
||||
char krx300_issu_yn[1]; /* KRX300 종목 여부 (Y/N) */
|
||||
char kospi_issu_yn[1]; /* KOSPI여부 */
|
||||
char sale_account[9]; /* 매출액 */
|
||||
char bsop_prfi[9]; /* 영업이익 */
|
||||
char op_prfi[9]; /* 경상이익 */
|
||||
char thtr_ntin[5]; /* 당기순이익 */
|
||||
char roe[9]; /* ROE(자기자본이익률) */
|
||||
char base_date[8]; /* 기준년월 */
|
||||
char prdy_avls_scal[9]; /* 전일기준 시가총액 (억) */
|
||||
char grp_code[3]; /* 그룹사 코드 */
|
||||
char co_crdt_limt_over_yn[1]; /* 회사신용한도초과여부 */
|
||||
char secu_lend_able_yn[1]; /* 담보대출가능여부 */
|
||||
char stln_able_yn[1]; /* 대주가능여부 */
|
||||
} ST_KSP_CODE;
|
||||
Reference in New Issue
Block a user