feat(stock): 매매알람 쿨다운 중복억제 + 종목명 해석
- 쿨다운(TRADE_ALERT_COOLDOWN_HOURS 기본 6h): 같은 종목·조건 해제→재발화 오실레이션 시 재알림 억제(set_alert_firing mark_fired=False로 firing 유지·발동시각 미갱신, suppressed 카운트). - 종목명: 워커 firing에 name 없어도 NAS가 watchlist→portfolio→krx_master로 해석해 알림·이력에 포함.
This commit is contained in:
@@ -465,10 +465,17 @@ def get_alert_state_firing() -> set:
|
||||
return {(r["ticker"], r["kind"], r["condition"]) for r in rows}
|
||||
|
||||
|
||||
def set_alert_firing(ticker: str, kind: str, condition: str, firing: bool, at_iso: str = None) -> None:
|
||||
def set_alert_firing(ticker: str, kind: str, condition: str, firing: bool,
|
||||
at_iso: str = None, mark_fired: bool = True) -> None:
|
||||
"""currently_firing 상태 갱신.
|
||||
|
||||
mark_fired=True(기본): 실제 알림 발송 → first/last_fired_at 갱신.
|
||||
mark_fired=False: 쿨다운으로 발송 억제하되 firing 상태만 유지 → 발동시각 미갱신
|
||||
(쿨다운이 계속 연장되지 않도록).
|
||||
"""
|
||||
now = at_iso or _now_iso()
|
||||
with _conn() as conn:
|
||||
if firing:
|
||||
if firing and mark_fired:
|
||||
conn.execute(
|
||||
"""INSERT INTO trade_alert_state(ticker,kind,condition,currently_firing,first_fired_at,last_fired_at,last_seen_at)
|
||||
VALUES(?,?,?,1,?,?,?)
|
||||
@@ -479,6 +486,14 @@ def set_alert_firing(ticker: str, kind: str, condition: str, firing: bool, at_is
|
||||
last_seen_at=excluded.last_seen_at""",
|
||||
(ticker, kind, condition, now, now, now),
|
||||
)
|
||||
elif firing and not mark_fired:
|
||||
conn.execute(
|
||||
"""INSERT INTO trade_alert_state(ticker,kind,condition,currently_firing,last_seen_at)
|
||||
VALUES(?,?,?,1,?)
|
||||
ON CONFLICT(ticker,kind,condition) DO UPDATE SET
|
||||
currently_firing=1, last_seen_at=excluded.last_seen_at""",
|
||||
(ticker, kind, condition, now),
|
||||
)
|
||||
else:
|
||||
conn.execute(
|
||||
"UPDATE trade_alert_state SET currently_firing=0, last_seen_at=? WHERE ticker=? AND kind=? AND condition=?",
|
||||
@@ -486,6 +501,32 @@ def set_alert_firing(ticker: str, kind: str, condition: str, firing: bool, at_is
|
||||
)
|
||||
|
||||
|
||||
def get_alert_last_fired_map() -> dict:
|
||||
"""{(ticker,kind,condition): last_fired_at ISO} — 쿨다운 판정용."""
|
||||
with _conn() as conn:
|
||||
rows = conn.execute(
|
||||
"SELECT ticker,kind,condition,last_fired_at FROM trade_alert_state"
|
||||
).fetchall()
|
||||
return {(r["ticker"], r["kind"], r["condition"]): r["last_fired_at"] for r in rows}
|
||||
|
||||
|
||||
def get_ticker_name(ticker: str) -> Optional[str]:
|
||||
"""종목명 해석 — watchlist → portfolio → krx_master 순. 없으면 None."""
|
||||
with _conn() as conn:
|
||||
for sql in (
|
||||
"SELECT name FROM watchlist WHERE ticker=?",
|
||||
"SELECT name FROM portfolio WHERE ticker=? LIMIT 1",
|
||||
"SELECT name FROM krx_master WHERE ticker=?",
|
||||
):
|
||||
try:
|
||||
row = conn.execute(sql, (ticker,)).fetchone()
|
||||
except sqlite3.OperationalError:
|
||||
continue # 일부 테스트 DB엔 해당 테이블 부재
|
||||
if row and row["name"]:
|
||||
return row["name"]
|
||||
return None
|
||||
|
||||
|
||||
def touch_alert_seen(keys: list, at_iso: str) -> None:
|
||||
with _conn() as conn:
|
||||
for (ticker, kind, condition) in keys:
|
||||
|
||||
Reference in New Issue
Block a user