ls증권 히스토리 구독 넣음

This commit is contained in:
Your Name
2026-07-30 18:05:07 +09:00
parent 61bec4bd1d
commit 67eab24603
1593 changed files with 135733 additions and 1232 deletions

View File

@@ -36,7 +36,7 @@ CREATE TABLE IF NOT EXISTS permanent_subscriptions (
market_type VARCHAR(8) NOT NULL DEFAULT 'KR',
exchange VARCHAR(16) NOT NULL DEFAULT 'KRX',
symbol VARCHAR(32) NOT NULL DEFAULT '',
tf_min INT NOT NULL DEFAULT 60,
tf_min INT NOT NULL DEFAULT 1,
enabled TINYINT NOT NULL DEFAULT 1,
note VARCHAR(100) NOT NULL DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
@@ -85,11 +85,15 @@ def upsert_permanent_sub(
market_type: Optional[str] = None,
exchange: Optional[str] = None,
symbol: Optional[str] = None,
tf_min: int = 60,
tf_min: int = 1,
enabled: bool = True,
note: str = "",
) -> None:
"""코드 1건 등록/갱신 (code UNIQUE)."""
"""코드 1건 등록/갱신 (code UNIQUE).
tf_min 은 저장·표시용 기준봉. WS는 틱→1분 집계가 기본이고 상위봉은 롤업하므로
기본·권장은 1분 (env ``PERM_SUB_BASE_TF_MIN``).
"""
ensure_permanent_subs_table(db)
code = str(code or "").strip().upper()
if not code:
@@ -99,11 +103,18 @@ def upsert_permanent_sub(
ex = (str(exchange).strip().upper() if exchange else "") or (g_ex if mt == g_mt else ("KRX" if mt == "KR" else "NASD"))
sym = (str(symbol).strip().upper() if symbol else "") or g_sym or code
try:
tfv = int(tf_min)
from kis_trader.utils.env import get_env_int
base_tf = int(get_env_int("PERM_SUB_BASE_TF_MIN", 1))
except Exception:
base_tf = 1
if base_tf < 1:
base_tf = 1
# 요청값과 무관하게 기준봉으로 통일 (1분→롤업)
try:
_ = int(tf_min)
except (TypeError, ValueError):
tfv = 60
if tfv < 1:
tfv = 60
pass
tfv = base_tf
raw = _core(db)
raw.conn.execute(
"""
@@ -120,6 +131,27 @@ def upsert_permanent_sub(
raw.conn.commit()
def normalize_all_tf_to_base(db: Any) -> int:
"""기존 영구구독 tf_min 을 기준봉(기본 1)으로 일괄 정리. 변경 건수 반환."""
ensure_permanent_subs_table(db)
try:
from kis_trader.utils.env import get_env_int
base_tf = int(get_env_int("PERM_SUB_BASE_TF_MIN", 1))
except Exception:
base_tf = 1
if base_tf < 1:
base_tf = 1
raw = _core(db)
cur = raw.conn.execute(
"UPDATE permanent_subscriptions SET tf_min=%s WHERE tf_min<>%s",
[base_tf, base_tf],
)
raw.conn.commit()
try:
return int(cur.rowcount or 0)
except Exception:
return 0
def remove_permanent_sub(db: Any, code: str) -> bool:
ensure_permanent_subs_table(db)
code = str(code or "").strip().upper()
@@ -185,3 +217,103 @@ def migrate_env_codes(db: Any, env_csv: str) -> int:
existing.add(c)
n += 1
return n
def last_quotes_for_codes(
db: Any,
codes: List[str],
tf_min: int = 1,
) -> Dict[str, Dict[str, Any]]:
"""영구구독 UI용 현재가 — REST/틱 전수스캔 없이 ``ws_candles`` 만.
※ 예전 ``ws_ticks`` ROW_NUMBER 전종목 스캔은 수십 초 지연 → UI 절대 금지.
이 함수는 ``ws_candles`` 만 조회한다. ws_ticks 경로를 다시 넣지 말 것.
반환 code → {
price, price_src ('candle'|''), candle_time, tick_time(항상 ''),
chg_pct (직전 봉 종가 대비 %), volume, updated_at
}
"""
from datetime import datetime as _dt
out: Dict[str, Dict[str, Any]] = {}
uniq = []
seen = set()
for c in codes or []:
u = str(c or "").strip().upper()
if not u or u in seen:
continue
seen.add(u)
uniq.append(u)
out[u] = {
"price": 0.0,
"price_src": "",
"candle_time": "",
"tick_time": "",
"chg_pct": None,
"volume": 0,
"updated_at": "",
}
if not uniq:
return out
try:
tf = int(tf_min) if int(tf_min) >= 1 else 1
except (TypeError, ValueError):
tf = 1
# 미래 슬롯(이상봉) 제외 — 로컬 now + 2분 여유
try:
now_slot = (_dt.now()).strftime("%Y%m%d%H%M")
except Exception:
now_slot = "999999999999"
raw = _core(db)
for code in uniq:
try:
rows = raw.conn.execute(
"""
SELECT candle_time, open, close, volume, updated_at
FROM ws_candles
WHERE code=%s AND timeframe=%s AND candle_time<=%s
ORDER BY candle_time DESC
LIMIT 2
""",
[code, tf, now_slot],
).fetchall() or []
except Exception as e:
logger.debug("last_quotes candle %s: %s", code, e)
rows = []
if not rows:
continue
parsed: List[Dict[str, Any]] = []
for r in rows:
parsed.append(dict(r) if not isinstance(r, dict) else dict(r))
last = parsed[0]
try:
px = float(last.get("close") or 0)
except (TypeError, ValueError):
px = 0.0
if px <= 0:
try:
px = float(last.get("open") or 0)
except (TypeError, ValueError):
px = 0.0
chg = None
if len(parsed) >= 2 and px > 0:
try:
prev = float(parsed[1].get("close") or 0)
if prev > 0:
chg = (px - prev) / prev * 100.0
except (TypeError, ValueError, ZeroDivisionError):
chg = None
try:
vol = int(last.get("volume") or 0)
except (TypeError, ValueError):
vol = 0
out[code].update({
"price": px,
"price_src": "candle" if px > 0 else "",
"candle_time": str(last.get("candle_time") or ""),
"chg_pct": chg,
"volume": vol,
"updated_at": str(last.get("updated_at") or ""),
})
return out