진입가 open 변경 + 알림 포맷 개선 + 일일 리포트 신호별 테이블화

## 변경 사항

### 진입가 open 가격 사용
- 신호 캔들의 close 가 아닌 open 을 진입가로 표기. "그 캔들이 알려준
  시점의 시작가" 를 진입가로 보고 싶다는 사용자 요청.
- 텔레그램 진입 알림, 손절가 알림 reference, 차트 hover 모두 일괄 변경.

### 진입 알림 포맷 확장
이전:
  🔼 롱 진입 신호
  BTCUSDT 5m
  진입가: ...
  손절가: ...

이후:
  🔼 롱 진입 신호
  BTCUSDT 5분봉
  시간: 2026-05-01 15:00
  진입가: ...
  손절가: ...

- 시간봉 코드(5m) 대신 한글 라벨(5분봉) 사용. TF_LABEL_MAP 으로 매핑.
- 신호 캔들 open_time 을 시간 행으로 추가.

### 손절가 알림 [손절가알림] 프리픽스
이전 손절가 알림은 별도 포맷이었음.
이후 진입 알림 메시지를 그대로 보존(_long_entry/_short_entry 에 entry_msg
저장)하고, 손절 도달 시 [손절가알림] 헤더와 현재가 한 줄만 추가:

  [손절가알림]
  🔼 롱 진입 신호
  BTCUSDT 5분봉
  시간: 2026-05-01 15:00
  진입가: ...
  손절가: ...
  현재가: ...

### 일일 리포트 신호별 테이블화
이전: 시간봉당 1줄 합계만 표기.
이후: 시간봉별 블록 안에 6개 신호(강한/일반/볼륨 × 롱/숏) 라인 + 합계.

  [5분봉]
  강한 롱: 1T 0F
  강한 숏: 1T 0F
  일반 롱: 7T 0F
  일반 숏: 8T 0F
  볼륨 롱: 9T 1F
  볼륨 숏: 9T 1F
  합계: 35T 2F (승률 94.59%)
  [15분봉] ...

_count_daily_signals 를 _count_daily_signals_per_type 으로 교체.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
ILSEON-RYU
2026-05-01 12:46:18 +09:00
parent a0cb5b466a
commit 29a36a1bc5
+64 -30
View File
@@ -58,6 +58,12 @@ _last_fired_candle = {"strong_long": None, "strong_short": None, "long": None, "
STOP_LOSS_PCT = 0.10
LONG_SIGNALS = {"strong_long_signal", "long_signal", "vol_long_signal"}
SHORT_SIGNALS = {"strong_short_signal", "short_signal", "vol_short_signal"}
TF_LABEL_MAP = {
"1m": "1분봉", "3m": "3분봉", "5m": "5분봉",
"15m": "15분봉", "30m": "30분봉",
"1h": "1시간봉", "4h": "4시간봉", "12h": "12시간봉",
"1d": "1일봉", "3d": "3일봉", "1M": "1개월봉",
}
_long_entry = None
_short_entry = None
@@ -95,18 +101,34 @@ def check_and_alert(df, symbol, interval):
if now - _last_alert[key] <= ALERT_COOLDOWN:
continue
tf_label = TF_LABEL_MAP.get(interval, interval)
candle_time_str = pd.Timestamp(candle_time).strftime("%Y-%m-%d %H:%M")
if sig in LONG_SIGNALS:
entry_price = float(triggered.iloc[-1]["close"])
entry_price = float(triggered.iloc[-1]["open"])
stop_price = entry_price * (1 - STOP_LOSS_PCT)
msg = f"{label}\n{symbol} {interval}\n진입가: {entry_price:.2f}\n손절가: {stop_price:.2f}"
_long_entry = {"price": entry_price, "stop": stop_price, "open_time": candle_time}
msg = (
f"{label}\n{symbol} {tf_label}\n"
f"시간: {candle_time_str}\n"
f"진입가: {entry_price:.2f}\n"
f"손절가: {stop_price:.2f}"
)
_long_entry = {"price": entry_price, "stop": stop_price, "open_time": candle_time, "entry_msg": msg}
elif sig in SHORT_SIGNALS:
entry_price = float(triggered.iloc[-1]["close"])
entry_price = float(triggered.iloc[-1]["open"])
stop_price = entry_price * (1 + STOP_LOSS_PCT)
msg = f"{label}\n{symbol} {interval}\n진입가: {entry_price:.2f}\n손절가: {stop_price:.2f}"
_short_entry = {"price": entry_price, "stop": stop_price, "open_time": candle_time}
msg = (
f"{label}\n{symbol} {tf_label}\n"
f"시간: {candle_time_str}\n"
f"진입가: {entry_price:.2f}\n"
f"손절가: {stop_price:.2f}"
)
_short_entry = {"price": entry_price, "stop": stop_price, "open_time": candle_time, "entry_msg": msg}
else:
msg = f"{label}\n{symbol} {interval}"
msg = (
f"{label}\n{symbol} {tf_label}\n"
f"시간: {candle_time_str}"
)
send_telegram(msg)
_last_alert[key] = now
@@ -115,17 +137,13 @@ def check_and_alert(df, symbol, interval):
current_price = float(df.iloc[-1]["close"])
if _long_entry is not None and current_price <= _long_entry["stop"]:
send_telegram(
f"🛑 롱 손절가 도달 (-{int(STOP_LOSS_PCT * 100)}%)\n{symbol} {interval}\n"
f"진입가: {_long_entry['price']:.2f}\n"
f"손절가: {_long_entry['stop']:.2f}\n"
f"[손절가알림]\n{_long_entry['entry_msg']}\n"
f"현재가: {current_price:.2f}"
)
_long_entry = None
if _short_entry is not None and current_price >= _short_entry["stop"]:
send_telegram(
f"🛑 숏 손절가 도달 (+{int(STOP_LOSS_PCT * 100)}%)\n{symbol} {interval}\n"
f"진입가: {_short_entry['price']:.2f}\n"
f"손절가: {_short_entry['stop']:.2f}\n"
f"[손절가알림]\n{_short_entry['entry_msg']}\n"
f"현재가: {current_price:.2f}"
)
_short_entry = None
@@ -453,7 +471,7 @@ def build_chart(symbol, interval, candle_limit=200):
]:
d = df[mask]
if not d.empty:
cd = list(zip(d["open_time"].dt.strftime("%m/%d %H:%M").tolist(), d["close"].tolist()))
cd = list(zip(d["open_time"].dt.strftime("%m/%d %H:%M").tolist(), d["open"].tolist()))
_long_sigs = ["강한 롱 진입 신호", "볼륨급등 롱 신호", "롱 진입 신호", "매도소진(롱)"]
_short_sigs = ["강한 숏 진입 신호", "볼륨급등 숏 신호", "숏 진입 신호", "매수소진(숏)", "롱소진경고(숏전환)", "숏 진입(주의)"]
if sig_name in _long_sigs:
@@ -630,21 +648,27 @@ def _alert_loop():
# ──────────────────────────────────────────────
DAILY_REPORT_TIMEFRAMES = ["5m", "15m", "30m", "1h", "4h"]
DAILY_REPORT_KLINES_LIMIT = {"5m": 500, "15m": 250, "30m": 200, "1h": 200, "4h": 200}
DAILY_REPORT_TF_LABEL = {"5m": "5분봉", "15m": "15분봉", "30m": "30분봉", "1h": "1시간봉", "4h": "4시간봉"}
DAILY_REPORT_PAIRS = [
("strong_long_signal", "strong_short_signal"),
("long_signal", "short_signal"),
("vol_long_signal", "vol_short_signal"),
]
DAILY_REPORT_SIGNAL_LABELS = [
("strong_long_signal", "강한 롱"),
("strong_short_signal", "강한 숏"),
("long_signal", "일반 롱"),
("short_signal", "일반 숏"),
("vol_long_signal", "볼륨 롱"),
("vol_short_signal", "볼륨 숏"),
]
def _count_daily_signals(df, cutoff_kst):
def _count_daily_signals_per_type(df, cutoff_kst):
result = {sig: [0, 0] for sig, _ in DAILY_REPORT_SIGNAL_LABELS}
if df is None or df.empty or "open_time" not in df.columns:
return 0, 0
return result
recent = df[df["open_time"] >= cutoff_kst].reset_index(drop=True)
if len(recent) < 2:
return 0, 0
total = 0
failed = 0
return result
for long_sig, short_sig in DAILY_REPORT_PAIRS:
if long_sig not in recent.columns or short_sig not in recent.columns:
continue
@@ -652,14 +676,14 @@ def _count_daily_signals(df, cutoff_kst):
row = recent.iloc[i]
nxt = recent.iloc[i + 1]
if bool(row.get(long_sig, False)):
total += 1
result[long_sig][0] += 1
if bool(nxt.get(short_sig, False)):
failed += 1
result[long_sig][1] += 1
if bool(row.get(short_sig, False)):
total += 1
result[short_sig][0] += 1
if bool(nxt.get(long_sig, False)):
failed += 1
return total, failed
result[short_sig][1] += 1
return result
def send_daily_report(symbol="BTCUSDT"):
now_kst = (datetime.now(timezone.utc) + KST).replace(tzinfo=None)
@@ -668,13 +692,23 @@ def send_daily_report(symbol="BTCUSDT"):
for tf in DAILY_REPORT_TIMEFRAMES:
try:
df = _build_signal_df(symbol, tf, DAILY_REPORT_KLINES_LIMIT[tf])
total, failed = _count_daily_signals(df, cutoff_kst)
counts = _count_daily_signals_per_type(df, cutoff_kst)
except Exception as e:
print(f"[일일리포트 {tf} 오류] {e}")
total, failed = 0, 0
passed = total - failed
rate = (passed / total * 100) if total > 0 else 0.0
lines.append(f"{DAILY_REPORT_TF_LABEL[tf]} {passed}번 T, {failed}번 F (승률 {rate:.2f}%)")
counts = {sig: [0, 0] for sig, _ in DAILY_REPORT_SIGNAL_LABELS}
lines.append("")
lines.append(f"[{TF_LABEL_MAP.get(tf, tf)}]")
total_all = 0
failed_all = 0
for sig, sig_label in DAILY_REPORT_SIGNAL_LABELS:
t, f = counts.get(sig, [0, 0])
passed = t - f
lines.append(f"{sig_label}: {passed}T {f}F")
total_all += t
failed_all += f
passed_all = total_all - failed_all
rate = (passed_all / total_all * 100) if total_all > 0 else 0.0
lines.append(f"합계: {passed_all}T {failed_all}F (승률 {rate:.2f}%)")
send_telegram("\n".join(lines))
def _daily_report_loop():