Streamlit rerun 시 알림 상태 초기화 막기 (같은 캔들 반복 알림 버그 수정)

## 증상
30분봉 일반 숏 신호가 한 캔들(20:30)에 발화한 뒤 30초 ~ 수 분 간격으로
계속 같은 시간(20:30) 으로 반복 알림이 텔레그램에 도착.

## 원인
Streamlit 은 사용자가 페이지를 새로고침하거나 컨트롤을 조작할 때마다
스크립트 전체를 위에서부터 재실행한다. 이 과정에서 모듈 최상단의 mutable
state 들이 매번 재할당된다:
  _last_alert = {... 0 ...}                # 쿨다운 타이머 0 으로 리셋
  _last_fired_candle = {... None ...}      # per-candle dedup 상태 None 리셋
  _long_entry = None                       # 진입 추적 None 리셋
  _alert_started = False                   # 스레드 가드 False 리셋
  _alert_lock = threading.Lock()           # 새 락 객체 생성 (기존 락 무시)

결과적으로 매 rerun 마다:
1. 새 알림 스레드가 추가로 spawn → 동일 polling 을 중복 수행
2. dedup 상태가 None 으로 리셋되어 이미 알린 캔들도 새로 알림 처리
3. 새 락 객체로 기존 스레드와 동기화 깨짐

streamlit.log 에 "[일일리포트] 스레드 기동" 메시지가 50회 가까이 찍힌 것이
1번 원인의 직접 증거.

## 수정
모듈 최상단에 globals() 검사 가드 추가:
  if "_alert_state_initialized" not in globals():
      _last_alert = {...}
      _last_fired_candle = {...}
      _long_entry = None
      _short_entry = None
      _alert_state_initialized = True

같은 패턴으로 _alert_thread_state_initialized 와 _last_report_date 도 보호.
첫 실행 시에만 초기화되고, 이후 rerun 에서는 globals() 에 이미 키가 존재
하므로 if 블록을 건너뛴다 -> 이전 값이 그대로 유지된다.

## 검증
페이지 5회 hit 후 streamlit.log 의 "기동" 로그 카운트:
  이전: 50+ (rerun 마다 추가)
  이후: 1 (단 한 번만)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
ILSEON-RYU
2026-05-02 21:44:41 +09:00
parent 4cbd62e7e1
commit fedbf1c81e
+19 -11
View File
@@ -54,9 +54,6 @@ ALERT_COOLDOWN = 600
BASE = "https://fapi.binance.com"
KST = timedelta(hours=9)
_last_alert = {"strong_long": 0, "strong_short": 0, "long": 0, "short": 0, "vol_long": 0, "vol_short": 0, "short_caution": 0}
_last_fired_candle = {"strong_long": None, "strong_short": None, "long": None, "short": None, "vol_long": None, "vol_short": None, "short_caution": None}
STOP_LOSS_PCT = 0.02
LONG_SIGNALS = {"strong_long_signal", "long_signal", "vol_long_signal"}
SHORT_SIGNALS = {"strong_short_signal", "short_signal", "vol_short_signal"}
@@ -66,8 +63,16 @@ TF_LABEL_MAP = {
"1h": "1시간봉", "4h": "4시간봉", "12h": "12시간봉",
"1d": "1일봉", "3d": "3일봉", "1M": "1개월봉",
}
_long_entry = None
_short_entry = None
# Streamlit 은 매 페이지 rerun 마다 모듈 최상단 코드를 재실행한다. 알림용 mutable
# 상태 (dedup, 진입 추적, 스레드 기동 플래그 등)는 한 번만 초기화되어야 하므로
# globals() 검사로 일회성 초기화를 보장한다.
if "_alert_state_initialized" not in globals():
_last_alert = {"strong_long": 0, "strong_short": 0, "long": 0, "short": 0, "vol_long": 0, "vol_short": 0, "short_caution": 0}
_last_fired_candle = {"strong_long": None, "strong_short": None, "long": None, "short": None, "vol_long": None, "vol_short": None, "short_caution": None}
_long_entry = None
_short_entry = None
_alert_state_initialized = True
# ──────────────────────────────────────────────
# 텔레그램
@@ -626,11 +631,13 @@ def build_chart(symbol, interval, candle_limit=200):
# ──────────────────────────────────────────────
# 알림 스레드
# ──────────────────────────────────────────────
_alert_symbol = "BTCUSDT"
_alert_interval = "5m"
_alert_lock = threading.Lock()
_alert_started = False
_daily_report_started = False
if "_alert_thread_state_initialized" not in globals():
_alert_symbol = "BTCUSDT"
_alert_interval = "5m"
_alert_lock = threading.Lock()
_alert_started = False
_daily_report_started = False
_alert_thread_state_initialized = True
def _build_signal_df(symbol, interval, klines_limit=200):
df = get_klines(symbol, interval, klines_limit)
@@ -750,7 +757,8 @@ def send_daily_report(symbol="BTCUSDT"):
msg_2x = _build_daily_report_lines(dfs, cutoff_kst, now_kst, symbol, offset=2, header_suffix="2배 시간 (2번째 봉 검증)")
send_telegram(msg_2x)
_last_report_date = None
if "_last_report_date" not in globals():
_last_report_date = None
def _daily_report_loop():
global _last_report_date