ls증권 히스토리 구독 넣음
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user