진입가 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:
+64
-30
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user