Forming candle 알림 + 마감 후 재검증 + [취소 알림] 패턴

## 동작
이전 커밋 8fd47d0 에서 forming candle 을 알림 대상에서 아예 제외했었지만,
이제 사용자 요청에 따라 "실시간 알림 + 잘못되면 취소 알림" 방식으로 변경.

흐름:
1. 알림 스레드는 매 polling 마다 df.tail(3) (forming candle 포함) 으로
   신호 검사 -> 알림 발사. 빠른 반응 유지.
2. forming candle 기반으로 발사된 알림은 alert_state.pending_groups 에
   등록 (interval, candle_time, msg, sig_cols, direction 보관).
3. 다음 polling 부터, 그 candle 이 더 이상 forming 이 아니면 (=닫힘)
   동일 candle 의 신호 컬럼들을 다시 확인:
   - 하나라도 True 로 살아있음 -> 확정, pending 에서 제거 (조용히)
   - 모두 False 로 바뀜          -> [취소 알림] 발송 + 진입 추적 클리어

## 메시지 예
원래:
  🔽 일반 숏 진입 신호
  BTCUSDT 30분봉
  시간: 2026-05-04 09:30
  진입가: 78,318.10
  손절가: 78,905.49

캔들 마감 후 신호 사라진 경우:
  [취소 알림]
  🔽 일반 숏 진입 신호
  BTCUSDT 30분봉
  시간: 2026-05-04 09:30
  진입가: 78,318.10
  손절가: 78,905.49

## 부가
- pending entry 중 long_entry/short_entry 와 open_time 이 일치하면
  같이 None 으로 클리어 -> 잘못된 손절가 알림 방지.
- 다른 시간봉으로 polling 가는 동안에는 pending 항목 그대로 보존
  (interval 매칭 시점까지 대기). 시간봉 다시 돌아오면 그때 검증 시도.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
ILSEON-RYU
2026-05-04 13:14:11 +09:00
parent 8fd47d0926
commit b27e2fcf51
2 changed files with 47 additions and 3 deletions
+5
View File
@@ -26,6 +26,11 @@ last_fired_candle = {
long_entry = None
short_entry = None
# forming candle 에서 발사된 알림은 캔들 마감 후 신호 재검증을 받는다.
# 마감 시점에 신호가 사라졌으면 [취소 알림] 을 보낸다.
# 항목 형식: {"interval", "direction", "candle_time", "msg", "sig_cols"}
pending_groups = []
alert_symbol = "BTCUSDT"
alert_interval = "5m"
alert_lock = threading.Lock()
+42 -3
View File
@@ -91,9 +91,39 @@ SIG_DEFS = [
def check_and_alert(df, symbol, interval):
now = time.time()
# 마지막 행은 형성 중(forming) 캔들 — 지표가 close 변동에 따라 깜빡일 수 있어
# false alert 의 주범. 닫힌 캔들 (최근 3개) 만 신호 검사 대상으로 사용.
recent = df.iloc[:-1].tail(3)
if df is None or df.empty:
return
forming_ct = df.iloc[-1]["open_time"]
# Phase 1 — pending_groups 검증.
# 이전 polling 에서 forming candle 기준으로 발사한 알림들은 그 캔들이 닫히면
# 신호가 살아남았는지 다시 확인한다. 모든 신호가 False 로 바뀌었으면 [취소 알림]
# 을 발송하고, 진입 추적(_long_entry/_short_entry) 도 클리어.
new_pending = []
for p in alert_state.pending_groups:
if p["interval"] != interval:
new_pending.append(p)
continue
ct = p["candle_time"]
if ct == forming_ct:
new_pending.append(p)
continue
row_match = df[df["open_time"] == ct]
if row_match.empty:
continue # 캔들이 df 윈도우 밖으로 빠짐 — 검증 포기, drop
row = row_match.iloc[0]
any_still_true = any(bool(row.get(s, False)) for s in p["sig_cols"])
if not any_still_true:
send_telegram(f"[취소 알림]\n{p['msg']}")
if p["direction"] == "long" and alert_state.long_entry is not None and alert_state.long_entry.get("open_time") == ct:
alert_state.long_entry = None
elif p["direction"] == "short" and alert_state.short_entry is not None and alert_state.short_entry.get("open_time") == ct:
alert_state.short_entry = None
# 검증 완료 (확정 또는 취소) — pending 에서 제거 (new_pending 에 추가 X)
alert_state.pending_groups = new_pending
# Phase 2 — 신호 검사 + 알림 발사 (forming candle 포함)
recent = df.tail(3)
eligible = []
for sig, key, sub_label, direction in SIG_DEFS:
@@ -155,6 +185,15 @@ def check_and_alert(df, symbol, interval):
for e in group:
alert_state.last_alert[e["key"]] = now
alert_state.last_fired_candle[e["key"]] = e["candle_time"]
# forming candle 기반 알림이면 마감 후 재검증을 위해 pending_groups 에 등록.
if candle_time == forming_ct:
alert_state.pending_groups.append({
"interval": interval,
"direction": direction,
"candle_time": candle_time,
"msg": msg,
"sig_cols": [e["sig"] for e in group],
})
_send_group(groups.get("long", []))
_send_group(groups.get("short", []))