From 1ecd6241f2f078ae7ec5d0b3cc181cd3b3c092d8 Mon Sep 17 00:00:00 2001 From: ILSEON-RYU Date: Tue, 5 May 2026 22:12:43 +0900 Subject: [PATCH] =?UTF-8?q?Forming=20candle=20=EC=A7=84=EC=9E=85=20?= =?UTF-8?q?=EC=8B=A0=ED=98=B8=20=EC=95=88=EC=A0=95=EC=84=B1=20=EC=9A=94?= =?UTF-8?q?=EA=B5=AC=20=E2=80=94=202=20polls=20=EC=97=B0=EC=86=8D=20True?= =?UTF-8?q?=20=EB=A7=8C=20=EB=B0=9C=EC=82=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 문제 forming candle 동안 close 변동에 따라 신호 컬럼이 1폴링 동안 True → 다음 폴링 False 로 깜빡이는 케이스 다수. 1회 True 만으로 진입 알림 발사되어 곧바로 [취소 알림] 도착하는 패턴 반복. 사용자 22:04 5m/15m 취소 알림 폭주 사례. ## 수정 신호별 (interval, sig) 키로 연속 True polling 카운트 추적. - forming candle 의 신호는 count >= 2 (= 60s 안정 유지) 일 때만 발사 - closed candle 의 신호는 1회로 즉시 발사 (data 확정이라 깜빡 X) - 신호 False 로 바뀌면 count 리셋 (연속성 보장) - per-candle dedup 와 cooldown 은 그대로 위에 적용 ## 효과 - forming 깜빡 1회는 더 이상 알림 발사 X → false alert + 취소 알림 동반 감소 - 진짜 신호는 2폴링 (60s) 동안 안정 유지하므로 통과 → latency 증가 최대 30s - 닫힌 캔들 알림은 latency 변화 없음 ## 추적 state (alert_state.signal_seen_count) {(interval, sig): {"candle_time": ts, "count": int}} 새 candle 진입 시 자동 리셋. False 시 카운트 0 으로 리셋. Co-Authored-By: Claude Opus 4.7 (1M context) --- alert_state.py | 5 +++++ app_streamlit.py | 14 ++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/alert_state.py b/alert_state.py index 37e33b5..b11ce47 100644 --- a/alert_state.py +++ b/alert_state.py @@ -33,6 +33,11 @@ pending_groups = [] # skip. interval 별로 각각 한 번씩 sync. synced_intervals = set() +# (interval, sig) 별 forming candle 의 연속 True polling 카운트. +# 진입 신호가 forming 중 1회 깜빡으로 발사되는 false alert 차단용. +# 항목 형식: {(interval, sig): {"candle_time": ts, "count": int}} +signal_seen_count = {} + alert_symbol = "BTCUSDT" alert_interval = "5m" # UI 표시용; 알림 스레드는 multi-TF 모니터링이라 무시 alert_lock = threading.Lock() diff --git a/app_streamlit.py b/app_streamlit.py index 1286558..3fc79a3 100644 --- a/app_streamlit.py +++ b/app_streamlit.py @@ -149,7 +149,12 @@ def check_and_alert(df, symbol, interval): if sig not in recent.columns: continue triggered = recent[recent[sig].fillna(False)] + seen_key = (interval, sig) + prev_seen = alert_state.signal_seen_count.get(seen_key) if triggered.empty: + # 신호 사라짐 → 카운터 리셋 (다음 True 시점부터 다시 1회 카운트) + if prev_seen: + alert_state.signal_seen_count[seen_key] = {"candle_time": prev_seen["candle_time"], "count": 0} continue candle_time = triggered.iloc[-1]["open_time"] state_key = (interval, key) @@ -157,6 +162,15 @@ def check_and_alert(df, symbol, interval): continue if now - alert_state.last_alert.get(state_key, 0) <= ALERT_COOLDOWN: continue + # 연속 True polling 카운트 갱신 + if prev_seen is None or prev_seen["candle_time"] != candle_time: + alert_state.signal_seen_count[seen_key] = {"candle_time": candle_time, "count": 1} + else: + alert_state.signal_seen_count[seen_key] = {"candle_time": candle_time, "count": prev_seen["count"] + 1} + count = alert_state.signal_seen_count[seen_key]["count"] + # forming candle 만 안정성 (2 polls) 요구. 닫힌 캔들은 즉시 발사 (data 확정). + if candle_time == forming_ct and count < 2: + continue eligible.append({ "sig": sig, "key": key, "sub_label": sub_label, "direction": direction, "candle_time": candle_time, "row": triggered.iloc[-1],