갱신시간 위치 KST 수정
This commit is contained in:
+41
-95
@@ -1,7 +1,3 @@
|
|||||||
"""
|
|
||||||
BTC/ETH Futures Trading Dashboard — Streamlit 버전
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
sys.stdout.reconfigure(line_buffering=True)
|
sys.stdout.reconfigure(line_buffering=True)
|
||||||
@@ -19,7 +15,9 @@ import streamlit as st
|
|||||||
import plotly.graph_objects as go
|
import plotly.graph_objects as go
|
||||||
from plotly.subplots import make_subplots
|
from plotly.subplots import make_subplots
|
||||||
import ta
|
import ta
|
||||||
from pyngrok import ngrok
|
import urllib3
|
||||||
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||||
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────
|
# ──────────────────────────────────────────────
|
||||||
# 페이지 설정 (반드시 최상단)
|
# 페이지 설정 (반드시 최상단)
|
||||||
@@ -49,7 +47,7 @@ st.markdown("""
|
|||||||
# ──────────────────────────────────────────────
|
# ──────────────────────────────────────────────
|
||||||
TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN", "")
|
TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN", "")
|
||||||
TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "")
|
TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "")
|
||||||
ALERT_COOLDOWN = 30
|
ALERT_COOLDOWN = 300
|
||||||
|
|
||||||
BASE = "https://fapi.binance.com"
|
BASE = "https://fapi.binance.com"
|
||||||
KST = timedelta(hours=9)
|
KST = timedelta(hours=9)
|
||||||
@@ -62,7 +60,7 @@ _last_alert = {"strong_long": 0, "strong_short": 0, "long": 0, "short": 0, "vol_
|
|||||||
def send_telegram(message: str):
|
def send_telegram(message: str):
|
||||||
try:
|
try:
|
||||||
url = f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendMessage"
|
url = f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendMessage"
|
||||||
requests.post(url, data={"chat_id": TELEGRAM_CHAT_ID, "text": message}, timeout=5)
|
requests.post(url, data={"chat_id": TELEGRAM_CHAT_ID, "text": message}, timeout=10)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[텔레그램 오류] {e}")
|
print(f"[텔레그램 오류] {e}")
|
||||||
|
|
||||||
@@ -85,9 +83,9 @@ def check_and_alert(df, symbol, interval):
|
|||||||
# ──────────────────────────────────────────────
|
# ──────────────────────────────────────────────
|
||||||
# 데이터 수집
|
# 데이터 수집
|
||||||
# ──────────────────────────────────────────────
|
# ──────────────────────────────────────────────
|
||||||
def get_klines(symbol="BTCUSDT", interval="5m", limit=1500):
|
def get_klines(symbol="BTCUSDT", interval="5m", limit=375):
|
||||||
url = f"{BASE}/fapi/v1/klines"
|
url = f"{BASE}/fapi/v1/klines"
|
||||||
r = requests.get(url, params={"symbol": symbol, "interval": interval, "limit": limit}, timeout=10)
|
r = requests.get(url, params={"symbol": symbol, "interval": interval, "limit": limit}, timeout=10, verify=False)
|
||||||
df = pd.DataFrame(r.json(), columns=[
|
df = pd.DataFrame(r.json(), columns=[
|
||||||
"open_time","open","high","low","close","volume",
|
"open_time","open","high","low","close","volume",
|
||||||
"close_time","quote_vol","trades","taker_buy_vol","taker_sell_vol","ignore"
|
"close_time","quote_vol","trades","taker_buy_vol","taker_sell_vol","ignore"
|
||||||
@@ -100,7 +98,7 @@ def get_klines(symbol="BTCUSDT", interval="5m", limit=1500):
|
|||||||
|
|
||||||
def get_funding_rate(symbol="BTCUSDT", limit=100):
|
def get_funding_rate(symbol="BTCUSDT", limit=100):
|
||||||
url = f"{BASE}/fapi/v1/fundingRate"
|
url = f"{BASE}/fapi/v1/fundingRate"
|
||||||
r = requests.get(url, params={"symbol": symbol, "limit": limit}, timeout=10)
|
r = requests.get(url, params={"symbol": symbol, "limit": limit}, timeout=10, verify=False)
|
||||||
df = pd.DataFrame(r.json())
|
df = pd.DataFrame(r.json())
|
||||||
df["fundingRate"] = df["fundingRate"].astype(float) * 100
|
df["fundingRate"] = df["fundingRate"].astype(float) * 100
|
||||||
df["fundingTime"] = pd.to_datetime(df["fundingTime"], unit="ms") + KST
|
df["fundingTime"] = pd.to_datetime(df["fundingTime"], unit="ms") + KST
|
||||||
@@ -108,7 +106,7 @@ def get_funding_rate(symbol="BTCUSDT", limit=100):
|
|||||||
|
|
||||||
def get_open_interest_history(symbol="BTCUSDT", period="5m", limit=100):
|
def get_open_interest_history(symbol="BTCUSDT", period="5m", limit=100):
|
||||||
url = f"{BASE}/futures/data/openInterestHist"
|
url = f"{BASE}/futures/data/openInterestHist"
|
||||||
r = requests.get(url, params={"symbol": symbol, "period": period, "limit": limit}, timeout=10)
|
r = requests.get(url, params={"symbol": symbol, "period": period, "limit": limit}, timeout=10, verify=False)
|
||||||
df = pd.DataFrame(r.json())
|
df = pd.DataFrame(r.json())
|
||||||
df["sumOpenInterest"] = df["sumOpenInterest"].astype(float)
|
df["sumOpenInterest"] = df["sumOpenInterest"].astype(float)
|
||||||
df["timestamp"] = pd.to_datetime(df["timestamp"], unit="ms") + KST
|
df["timestamp"] = pd.to_datetime(df["timestamp"], unit="ms") + KST
|
||||||
@@ -116,7 +114,7 @@ def get_open_interest_history(symbol="BTCUSDT", period="5m", limit=100):
|
|||||||
|
|
||||||
def get_long_short_ratio(symbol="BTCUSDT", period="5m", limit=500):
|
def get_long_short_ratio(symbol="BTCUSDT", period="5m", limit=500):
|
||||||
url = f"{BASE}/futures/data/topLongShortPositionRatio"
|
url = f"{BASE}/futures/data/topLongShortPositionRatio"
|
||||||
r = requests.get(url, params={"symbol": symbol, "period": period, "limit": limit}, timeout=10)
|
r = requests.get(url, params={"symbol": symbol, "period": period, "limit": limit}, timeout=10, verify=False)
|
||||||
df = pd.DataFrame(r.json())
|
df = pd.DataFrame(r.json())
|
||||||
df["longShortRatio"] = df["longShortRatio"].astype(float)
|
df["longShortRatio"] = df["longShortRatio"].astype(float)
|
||||||
df["timestamp"] = pd.to_datetime(df["timestamp"], unit="ms") + KST
|
df["timestamp"] = pd.to_datetime(df["timestamp"], unit="ms") + KST
|
||||||
@@ -124,7 +122,7 @@ def get_long_short_ratio(symbol="BTCUSDT", period="5m", limit=500):
|
|||||||
|
|
||||||
def get_taker_buy_sell_ratio(symbol="BTCUSDT", period="5m", limit=100):
|
def get_taker_buy_sell_ratio(symbol="BTCUSDT", period="5m", limit=100):
|
||||||
url = f"{BASE}/futures/data/takerlongshortRatio"
|
url = f"{BASE}/futures/data/takerlongshortRatio"
|
||||||
r = requests.get(url, params={"symbol": symbol, "period": period, "limit": limit}, timeout=10)
|
r = requests.get(url, params={"symbol": symbol, "period": period, "limit": limit}, timeout=10, verify=False)
|
||||||
df = pd.DataFrame(r.json())
|
df = pd.DataFrame(r.json())
|
||||||
df["buySellRatio"] = df["buySellRatio"].astype(float)
|
df["buySellRatio"] = df["buySellRatio"].astype(float)
|
||||||
df["timestamp"] = pd.to_datetime(df["timestamp"], unit="ms") + KST
|
df["timestamp"] = pd.to_datetime(df["timestamp"], unit="ms") + KST
|
||||||
@@ -156,14 +154,12 @@ def compute_indicators(df, interval="5m"):
|
|||||||
return df
|
return df
|
||||||
|
|
||||||
def compute_signals(df, interval="5m"):
|
def compute_signals(df, interval="5m"):
|
||||||
# 일반 신호용 — MA7/MA25 2개 기준
|
|
||||||
df["bull_ma_2"] = (
|
df["bull_ma_2"] = (
|
||||||
(df["close"] > df["MA7"]) & (df["MA7"] > df["MA25"])
|
(df["close"] > df["MA7"]) & (df["MA7"] > df["MA25"])
|
||||||
)
|
)
|
||||||
df["bear_ma_2"] = (
|
df["bear_ma_2"] = (
|
||||||
(df["close"] < df["MA7"]) & (df["MA7"] < df["MA25"])
|
(df["close"] < df["MA7"]) & (df["MA7"] < df["MA25"])
|
||||||
)
|
)
|
||||||
# 강한 신호용 — MA7/MA25/MA99 3개 기준
|
|
||||||
df["bull_ma"] = (
|
df["bull_ma"] = (
|
||||||
(df["close"] > df["MA7"]) & (df["MA7"] > df["MA25"]) &
|
(df["close"] > df["MA7"]) & (df["MA7"] > df["MA25"]) &
|
||||||
(df["MA25"] > df["MA99"])
|
(df["MA25"] > df["MA99"])
|
||||||
@@ -172,9 +168,8 @@ def compute_signals(df, interval="5m"):
|
|||||||
(df["close"] < df["MA7"]) & (df["MA7"] < df["MA25"]) &
|
(df["close"] < df["MA7"]) & (df["MA7"] < df["MA25"]) &
|
||||||
(df["MA25"] < df["MA99"])
|
(df["MA25"] < df["MA99"])
|
||||||
)
|
)
|
||||||
df["long_signal"] = df["bull_ma_2"] & (df["RSI"] < 60) & (df["MACD_hist"] > df["MACD_hist"].shift(1)) & (df["close"] > df["BB_mid"]) & (df["StochRSI_k"] < 80)
|
df["long_signal"] = df["bull_ma_2"] & (df["RSI"] < 60) & (df["MACD_hist"] > df["MACD_hist"].shift(1)) & (df["close"] > df["BB_mid"])
|
||||||
df["short_signal"] = df["bear_ma_2"] & (df["RSI"] > 40) & (df["MACD_hist"] < df["MACD_hist"].shift(1)) & (df["close"] < df["BB_mid"]) & (df["StochRSI_k"] > 20)
|
df["short_signal"] = df["bear_ma_2"] & (df["RSI"] > 35) & (df["MACD_hist"] < df["MACD_hist"].shift(1)) & (df["close"] < df["BB_mid"])
|
||||||
# 쿨다운: 직전 5캔들 내 같은 신호 있으면 억제
|
|
||||||
df["long_signal"] = df["long_signal"] & (df["long_signal"].rolling(5, min_periods=1).sum().shift(1).fillna(0) == 0)
|
df["long_signal"] = df["long_signal"] & (df["long_signal"].rolling(5, min_periods=1).sum().shift(1).fillna(0) == 0)
|
||||||
df["short_signal"] = df["short_signal"] & (df["short_signal"].rolling(5, min_periods=1).sum().shift(1).fillna(0) == 0)
|
df["short_signal"] = df["short_signal"] & (df["short_signal"].rolling(5, min_periods=1).sum().shift(1).fillna(0) == 0)
|
||||||
|
|
||||||
@@ -197,7 +192,6 @@ def compute_signals(df, interval="5m"):
|
|||||||
|
|
||||||
df["strong_long_signal"] = df["bull_ma"] & (df["RSI"] < 65) & (df["MACD_hist"] > df["MACD_hist"].shift(1)) & df["oi_up_2"] & df["taker_buy_2"] & df["fr_long_favor"]
|
df["strong_long_signal"] = df["bull_ma"] & (df["RSI"] < 65) & (df["MACD_hist"] > df["MACD_hist"].shift(1)) & df["oi_up_2"] & df["taker_buy_2"] & df["fr_long_favor"]
|
||||||
df["strong_short_signal"] = df["bear_ma"] & (df["RSI"] > 35) & (df["MACD_hist"] < df["MACD_hist"].shift(1)) & df["oi_down_2"] & df["taker_sell_2"] & df["fr_short_favor"]
|
df["strong_short_signal"] = df["bear_ma"] & (df["RSI"] > 35) & (df["MACD_hist"] < df["MACD_hist"].shift(1)) & df["oi_down_2"] & df["taker_sell_2"] & df["fr_short_favor"]
|
||||||
# 강한 신호 쿨다운: 직전 10캔들 내 억제
|
|
||||||
df["strong_long_signal"] = df["strong_long_signal"] & (df["strong_long_signal"].rolling(10, min_periods=1).sum().shift(1).fillna(0) == 0)
|
df["strong_long_signal"] = df["strong_long_signal"] & (df["strong_long_signal"].rolling(10, min_periods=1).sum().shift(1).fillna(0) == 0)
|
||||||
df["strong_short_signal"] = df["strong_short_signal"] & (df["strong_short_signal"].rolling(10, min_periods=1).sum().shift(1).fillna(0) == 0)
|
df["strong_short_signal"] = df["strong_short_signal"] & (df["strong_short_signal"].rolling(10, min_periods=1).sum().shift(1).fillna(0) == 0)
|
||||||
|
|
||||||
@@ -208,9 +202,7 @@ def compute_signals(df, interval="5m"):
|
|||||||
df["exhaustion_short"] = buy_spike.shift(1).fillna(False)
|
df["exhaustion_short"] = buy_spike.shift(1).fillna(False)
|
||||||
df["exhaustion_long"] = sell_spike.shift(1).fillna(False)
|
df["exhaustion_long"] = sell_spike.shift(1).fillna(False)
|
||||||
|
|
||||||
# ── Taker Sell Net(Sell-Buy) 극값 강한 숏 신호 (MA배열 무관, 볼륨 우선) ──
|
_vol_min_map = {"1m": 33, "3m": 100, "5m": 100, "15m": 300, "30m": 600, "1h": 1200, "2h": 2400, "4h": 4800, "12h": 14400, "1d": 28800, "3d": 86400, "1M": 864000}
|
||||||
# 시간봉별 볼륨 임계값 (5분봉 기준 100, 비례 적용)
|
|
||||||
_vol_min_map = {"1m": 33, "3m": 100, "5m": 100, "15m": 300, "30m": 600, "1h": 1200, "2h": 2400, "4h": 4800, "1d": 28800}
|
|
||||||
_vol_min = _vol_min_map.get(interval, 100)
|
_vol_min = _vol_min_map.get(interval, 100)
|
||||||
|
|
||||||
df["sell_net"] = df["taker_sell_vol"] - df["taker_buy_vol"]
|
df["sell_net"] = df["taker_sell_vol"] - df["taker_buy_vol"]
|
||||||
@@ -224,7 +216,6 @@ def compute_signals(df, interval="5m"):
|
|||||||
cooldown_vol_short = sell_spike_strong.rolling(10, min_periods=1).sum().shift(1).fillna(0) == 0
|
cooldown_vol_short = sell_spike_strong.rolling(10, min_periods=1).sum().shift(1).fillna(0) == 0
|
||||||
df["vol_short_signal"] = sell_spike_strong & cooldown_vol_short
|
df["vol_short_signal"] = sell_spike_strong & cooldown_vol_short
|
||||||
|
|
||||||
# ── Taker Buy Net(Buy-Sell) 극값 강한 롱 신호 (MA배열 무관, 볼륨 우선) ──
|
|
||||||
df["buy_net"] = df["taker_buy_vol"] - df["taker_sell_vol"]
|
df["buy_net"] = df["taker_buy_vol"] - df["taker_sell_vol"]
|
||||||
buy_net_avg = df["buy_net"].rolling(10).mean()
|
buy_net_avg = df["buy_net"].rolling(10).mean()
|
||||||
buy_spike_strong = (
|
buy_spike_strong = (
|
||||||
@@ -236,11 +227,9 @@ def compute_signals(df, interval="5m"):
|
|||||||
cooldown_vol_long = buy_spike_strong.rolling(10, min_periods=1).sum().shift(1).fillna(0) == 0
|
cooldown_vol_long = buy_spike_strong.rolling(10, min_periods=1).sum().shift(1).fillna(0) == 0
|
||||||
df["vol_long_signal"] = buy_spike_strong & cooldown_vol_long
|
df["vol_long_signal"] = buy_spike_strong & cooldown_vol_long
|
||||||
|
|
||||||
# ── OI 2캔들 하락 + FR ≤ -0.007% → 숏 주의 신호 (쿨다운 5캔들) ──
|
|
||||||
if "fundingRate" in df.columns and "sumOpenInterest" in df.columns:
|
if "fundingRate" in df.columns and "sumOpenInterest" in df.columns:
|
||||||
fr_extreme = df["fundingRate"] <= -0.007
|
fr_extreme = df["fundingRate"] <= -0.007
|
||||||
raw_signal = df["oi_down_2"] & fr_extreme
|
raw_signal = df["oi_down_2"] & fr_extreme
|
||||||
# 쿨다운: 직전 5캔들 내 이미 신호 있으면 억제
|
|
||||||
cooldown_mask = raw_signal.rolling(5, min_periods=1).sum().shift(1).fillna(0) == 0
|
cooldown_mask = raw_signal.rolling(5, min_periods=1).sum().shift(1).fillna(0) == 0
|
||||||
df["short_caution_signal"] = raw_signal & cooldown_mask
|
df["short_caution_signal"] = raw_signal & cooldown_mask
|
||||||
else:
|
else:
|
||||||
@@ -269,14 +258,14 @@ COLORS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def _to_floor_freq(period):
|
def _to_floor_freq(period):
|
||||||
return {"1m":"1min","3m":"3min","5m":"5min","15m":"15min","30m":"30min","1h":"1h","4h":"4h"}.get(period, period)
|
return {"1m":"1min","3m":"3min","5m":"5min","15m":"15min","30m":"30min","1h":"1h","4h":"4h","12h":"12h","1d":"1D","3d":"3D","1M":"1ME"}.get(period, period)
|
||||||
|
|
||||||
def build_chart(symbol, interval):
|
def build_chart(symbol, interval, candle_limit=200):
|
||||||
df = get_klines(symbol, interval, 1500)
|
df = get_klines(symbol, interval, candle_limit)
|
||||||
oi_period = interval if interval in ["5m","15m","30m","1h","4h"] else "5m"
|
oi_period = interval if interval in ["5m","15m","30m","1h","4h","12h","1d","3d","1M"] else "5m"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
oi = get_open_interest_history(symbol, oi_period, 500)
|
oi = get_open_interest_history(symbol, oi_period, 200)
|
||||||
if not oi.empty:
|
if not oi.empty:
|
||||||
oi_m = oi[["timestamp","sumOpenInterest"]].rename(columns={"timestamp":"open_time"})
|
oi_m = oi[["timestamp","sumOpenInterest"]].rename(columns={"timestamp":"open_time"})
|
||||||
df["open_time_r"] = df["open_time"].dt.floor(_to_floor_freq(oi_period))
|
df["open_time_r"] = df["open_time"].dt.floor(_to_floor_freq(oi_period))
|
||||||
@@ -287,7 +276,7 @@ def build_chart(symbol, interval):
|
|||||||
except: pass
|
except: pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
fr = get_funding_rate(symbol, 500)
|
fr = get_funding_rate(symbol, 200)
|
||||||
if not fr.empty:
|
if not fr.empty:
|
||||||
fr_m = fr[["fundingTime","fundingRate"]].rename(columns={"fundingTime":"open_time"})
|
fr_m = fr[["fundingTime","fundingRate"]].rename(columns={"fundingTime":"open_time"})
|
||||||
fr_m["open_time"] = fr_m["open_time"].dt.floor(_to_floor_freq("1h"))
|
fr_m["open_time"] = fr_m["open_time"].dt.floor(_to_floor_freq("1h"))
|
||||||
@@ -298,7 +287,7 @@ def build_chart(symbol, interval):
|
|||||||
except: pass
|
except: pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ls = get_long_short_ratio(symbol, oi_period, 500)
|
ls = get_long_short_ratio(symbol, oi_period, 200)
|
||||||
if not ls.empty:
|
if not ls.empty:
|
||||||
ls_m = ls[["timestamp","longShortRatio"]].rename(columns={"timestamp":"open_time"})
|
ls_m = ls[["timestamp","longShortRatio"]].rename(columns={"timestamp":"open_time"})
|
||||||
df["open_time_r3"] = df["open_time"].dt.floor(_to_floor_freq(oi_period))
|
df["open_time_r3"] = df["open_time"].dt.floor(_to_floor_freq(oi_period))
|
||||||
@@ -310,22 +299,19 @@ def build_chart(symbol, interval):
|
|||||||
|
|
||||||
df = compute_indicators(df, interval)
|
df = compute_indicators(df, interval)
|
||||||
|
|
||||||
# ── 1시간봉 MA 배열 — 각 캔들 시점별로 소급 체크 ──
|
|
||||||
try:
|
try:
|
||||||
if interval != "1h":
|
if interval != "1h":
|
||||||
df_1h = get_klines(symbol, "1h", 300)
|
df_1h = get_klines(symbol, "1h", 150)
|
||||||
df_1h["MA7"] = df_1h["close"].rolling(7).mean()
|
df_1h["MA7"] = df_1h["close"].rolling(7).mean()
|
||||||
df_1h["MA25"] = df_1h["close"].rolling(25).mean()
|
df_1h["MA25"] = df_1h["close"].rolling(25).mean()
|
||||||
df_1h["MA99"] = df_1h["close"].rolling(99).mean()
|
df_1h["MA99"] = df_1h["close"].rolling(99).mean()
|
||||||
df_1h["MA200"] = df_1h["close"].rolling(200).mean()
|
df_1h["MA200"] = df_1h["close"].rolling(200).mean()
|
||||||
# 일반 신호용 2개 기준
|
|
||||||
df_1h["h1_bull_2"] = (
|
df_1h["h1_bull_2"] = (
|
||||||
(df_1h["close"] > df_1h["MA7"]) & (df_1h["MA7"] > df_1h["MA25"])
|
(df_1h["close"] > df_1h["MA7"]) & (df_1h["MA7"] > df_1h["MA25"])
|
||||||
)
|
)
|
||||||
df_1h["h1_bear_2"] = (
|
df_1h["h1_bear_2"] = (
|
||||||
(df_1h["close"] < df_1h["MA7"]) & (df_1h["MA7"] < df_1h["MA25"])
|
(df_1h["close"] < df_1h["MA7"]) & (df_1h["MA7"] < df_1h["MA25"])
|
||||||
)
|
)
|
||||||
# 강한 신호용 3개 기준
|
|
||||||
df_1h["h1_bull"] = (
|
df_1h["h1_bull"] = (
|
||||||
(df_1h["close"] > df_1h["MA7"]) &
|
(df_1h["close"] > df_1h["MA7"]) &
|
||||||
(df_1h["MA7"] > df_1h["MA25"]) &
|
(df_1h["MA7"] > df_1h["MA25"]) &
|
||||||
@@ -342,7 +328,6 @@ def build_chart(symbol, interval):
|
|||||||
df = df.merge(df_1h_m, left_on="open_time_1h", right_on="open_time",
|
df = df.merge(df_1h_m, left_on="open_time_1h", right_on="open_time",
|
||||||
how="left", suffixes=("","_1h"))
|
how="left", suffixes=("","_1h"))
|
||||||
df = df.drop(columns=["open_time_1h","open_time_1h_x","open_time_1h_y"], errors="ignore")
|
df = df.drop(columns=["open_time_1h","open_time_1h_x","open_time_1h_y"], errors="ignore")
|
||||||
# ffill은 당일(KST 날짜) 내에서만 허용 — 전날 신호 전이 방지
|
|
||||||
df["_date"] = df["open_time"].dt.date
|
df["_date"] = df["open_time"].dt.date
|
||||||
for col in ["h1_bull","h1_bear","h1_bull_2","h1_bear_2"]:
|
for col in ["h1_bull","h1_bear","h1_bull_2","h1_bear_2"]:
|
||||||
df[col] = df.groupby("_date")[col].transform(lambda x: x.ffill())
|
df[col] = df.groupby("_date")[col].transform(lambda x: x.ffill())
|
||||||
@@ -360,17 +345,11 @@ def build_chart(symbol, interval):
|
|||||||
df["h1_bull"] = False
|
df["h1_bull"] = False
|
||||||
df["h1_bear"] = False
|
df["h1_bear"] = False
|
||||||
|
|
||||||
# 각 캔들 시점의 1시간봉 배열로 신호 억제
|
df["long_signal"] = df["long_signal"] & df["taker_buy_dom"]
|
||||||
# 일반 신호 — 1h 2개 기준
|
df["short_signal"] = df["short_signal"] & df["taker_sell_dom"]
|
||||||
h1_bull_2 = df.get("h1_bull_2", df["h1_bull"])
|
df["strong_long_signal"] = df["strong_long_signal"]
|
||||||
h1_bear_2 = df.get("h1_bear_2", df["h1_bear"])
|
df["strong_short_signal"] = df["strong_short_signal"]
|
||||||
df["long_signal"] = df["long_signal"] & h1_bull_2 & df["taker_buy_dom"]
|
df["short_caution_signal"]= df["short_caution_signal"]
|
||||||
df["short_signal"] = df["short_signal"] & h1_bear_2 & df["taker_sell_dom"]
|
|
||||||
# 강한 신호 — 1h 3개 기준
|
|
||||||
df["strong_long_signal"] = df["strong_long_signal"] & df["h1_bull"]
|
|
||||||
df["strong_short_signal"]= df["strong_short_signal"]& df["h1_bear"]
|
|
||||||
df["short_caution_signal"]= df["short_caution_signal"] & h1_bear_2
|
|
||||||
|
|
||||||
df["long_exhaustion_warn"] = False
|
df["long_exhaustion_warn"] = False
|
||||||
|
|
||||||
t = df["open_time"]
|
t = df["open_time"]
|
||||||
@@ -385,7 +364,6 @@ def build_chart(symbol, interval):
|
|||||||
"RSI / StochRSI", "MACD"]
|
"RSI / StochRSI", "MACD"]
|
||||||
)
|
)
|
||||||
|
|
||||||
# ── 캔들 ──
|
|
||||||
fig.add_trace(go.Candlestick(
|
fig.add_trace(go.Candlestick(
|
||||||
x=t, open=df["open"], high=df["high"], low=df["low"], close=df["close"],
|
x=t, open=df["open"], high=df["high"], low=df["low"], close=df["close"],
|
||||||
increasing_line_color=COLORS["green"], decreasing_line_color=COLORS["red"],
|
increasing_line_color=COLORS["green"], decreasing_line_color=COLORS["red"],
|
||||||
@@ -393,31 +371,24 @@ def build_chart(symbol, interval):
|
|||||||
name="캔들", line=dict(width=1)
|
name="캔들", line=dict(width=1)
|
||||||
), row=1, col=1)
|
), row=1, col=1)
|
||||||
|
|
||||||
# ── BB ──
|
|
||||||
fig.add_trace(go.Scatter(x=t, y=df["BB_upper"], line=dict(color=COLORS["BB"].replace("0.1","0.6"), width=0.8), name="BB상단", showlegend=False), row=1, col=1)
|
fig.add_trace(go.Scatter(x=t, y=df["BB_upper"], line=dict(color=COLORS["BB"].replace("0.1","0.6"), width=0.8), name="BB상단", showlegend=False), row=1, col=1)
|
||||||
fig.add_trace(go.Scatter(x=t, y=df["BB_lower"], line=dict(color=COLORS["BB"].replace("0.1","0.6"), width=0.8), fill="tonexty", fillcolor=COLORS["BB"], name="BB하단", showlegend=False), row=1, col=1)
|
fig.add_trace(go.Scatter(x=t, y=df["BB_lower"], line=dict(color=COLORS["BB"].replace("0.1","0.6"), width=0.8), fill="tonexty", fillcolor=COLORS["BB"], name="BB하단", showlegend=False), row=1, col=1)
|
||||||
|
|
||||||
# ── MA ──
|
|
||||||
for ma, col in [("MA200", COLORS["MA200"]), ("MA99", COLORS["MA99"]), ("MA25", COLORS["MA25"]), ("MA7", COLORS["MA7"])]:
|
for ma, col in [("MA200", COLORS["MA200"]), ("MA99", COLORS["MA99"]), ("MA25", COLORS["MA25"]), ("MA7", COLORS["MA7"])]:
|
||||||
fig.add_trace(go.Scatter(x=t, y=df[ma], line=dict(color=col, width=1.2), name=ma), row=1, col=1)
|
fig.add_trace(go.Scatter(x=t, y=df[ma], line=dict(color=col, width=1.2), name=ma), row=1, col=1)
|
||||||
|
|
||||||
# ── 탑트레이더 L/S ──
|
|
||||||
if "longShortRatio" in df.columns:
|
if "longShortRatio" in df.columns:
|
||||||
fig.add_trace(go.Scatter(x=t, y=df["longShortRatio"], line=dict(color=COLORS["orange"], width=1), name="탑트레이더 L/S"), row=1, col=1)
|
fig.add_trace(go.Scatter(x=t, y=df["longShortRatio"], line=dict(color=COLORS["orange"], width=1), name="탑트레이더 L/S"), row=1, col=1)
|
||||||
|
|
||||||
# ── FR 선 ──
|
|
||||||
if "fundingRate" in df.columns:
|
if "fundingRate" in df.columns:
|
||||||
fig.add_trace(go.Scatter(x=t, y=df["fundingRate"], line=dict(color=COLORS["purple"], width=1), name="Funding Rate"), row=1, col=1)
|
fig.add_trace(go.Scatter(x=t, y=df["fundingRate"], line=dict(color=COLORS["purple"], width=1), name="Funding Rate"), row=1, col=1)
|
||||||
|
|
||||||
# ── OI 선 ──
|
|
||||||
if "sumOpenInterest" in df.columns:
|
if "sumOpenInterest" in df.columns:
|
||||||
fig.add_trace(go.Scatter(x=t, y=df["sumOpenInterest"], line=dict(color=COLORS["blue"], width=1), name="OI"), row=1, col=1)
|
fig.add_trace(go.Scatter(x=t, y=df["sumOpenInterest"], line=dict(color=COLORS["blue"], width=1), name="OI"), row=1, col=1)
|
||||||
|
|
||||||
# ── Taker sell/buy scatter ──
|
|
||||||
fig.add_trace(go.Scatter(x=t, y=df["taker_sell_vol"], mode="markers", marker=dict(color=COLORS["red"], size=3), name="Taker Sell"), row=1, col=1)
|
fig.add_trace(go.Scatter(x=t, y=df["taker_sell_vol"], mode="markers", marker=dict(color=COLORS["red"], size=3), name="Taker Sell"), row=1, col=1)
|
||||||
fig.add_trace(go.Scatter(x=t, y=df["taker_buy_vol"], mode="markers", marker=dict(color=COLORS["green"], size=3), name="Taker Buy"), row=1, col=1)
|
fig.add_trace(go.Scatter(x=t, y=df["taker_buy_vol"], mode="markers", marker=dict(color=COLORS["green"], size=3), name="Taker Buy"), row=1, col=1)
|
||||||
|
|
||||||
# ── 신호 마커 ──
|
|
||||||
for mask, sym, color, sig_name in [
|
for mask, sym, color, sig_name in [
|
||||||
(df["exhaustion_short"], "star", COLORS["red"], "매수소진(숏)"),
|
(df["exhaustion_short"], "star", COLORS["red"], "매수소진(숏)"),
|
||||||
(df["exhaustion_long"], "star", COLORS["green"], "매도소진(롱)"),
|
(df["exhaustion_long"], "star", COLORS["green"], "매도소진(롱)"),
|
||||||
@@ -436,9 +407,9 @@ def build_chart(symbol, interval):
|
|||||||
_long_sigs = ["강한 롱 진입 신호", "볼륨급등 롱 신호", "롱 진입 신호", "매도소진(롱)"]
|
_long_sigs = ["강한 롱 진입 신호", "볼륨급등 롱 신호", "롱 진입 신호", "매도소진(롱)"]
|
||||||
_short_sigs = ["강한 숏 진입 신호", "볼륨급등 숏 신호", "숏 진입 신호", "매수소진(숏)", "롱소진경고(숏전환)", "숏 진입(주의)"]
|
_short_sigs = ["강한 숏 진입 신호", "볼륨급등 숏 신호", "숏 진입 신호", "매수소진(숏)", "롱소진경고(숏전환)", "숏 진입(주의)"]
|
||||||
if sig_name in _long_sigs:
|
if sig_name in _long_sigs:
|
||||||
y_val = d["low"] * 0.9998 # 캔들 하단 아래
|
y_val = d["low"] * 0.9998
|
||||||
elif sig_name in _short_sigs:
|
elif sig_name in _short_sigs:
|
||||||
y_val = d["high"] * 1.0002 # 캔들 상단 위
|
y_val = d["high"] * 1.0002
|
||||||
else:
|
else:
|
||||||
y_val = d["close"]
|
y_val = d["close"]
|
||||||
fig.add_trace(go.Scatter(
|
fig.add_trace(go.Scatter(
|
||||||
@@ -450,7 +421,6 @@ def build_chart(symbol, interval):
|
|||||||
showlegend=True,
|
showlegend=True,
|
||||||
), row=1, col=1)
|
), row=1, col=1)
|
||||||
else:
|
else:
|
||||||
# 신호 없어도 범례에 항상 표시
|
|
||||||
fig.add_trace(go.Scatter(
|
fig.add_trace(go.Scatter(
|
||||||
x=[None], y=[None],
|
x=[None], y=[None],
|
||||||
mode="markers", marker=dict(symbol=sym, color=color, size=10),
|
mode="markers", marker=dict(symbol=sym, color=color, size=10),
|
||||||
@@ -458,20 +428,16 @@ def build_chart(symbol, interval):
|
|||||||
showlegend=True,
|
showlegend=True,
|
||||||
), row=1, col=1)
|
), row=1, col=1)
|
||||||
|
|
||||||
# ── Taker Volume 바 ──
|
|
||||||
buy_vol = df["taker_buy_vol"] - df["taker_sell_vol"]
|
buy_vol = df["taker_buy_vol"] - df["taker_sell_vol"]
|
||||||
colors_v = [COLORS["green"] if v >= 0 else COLORS["red"] for v in buy_vol]
|
colors_v = [COLORS["green"] if v >= 0 else COLORS["red"] for v in buy_vol]
|
||||||
fig.add_trace(go.Bar(x=t, y=buy_vol, marker_color=colors_v, name="Taker Net"), row=2, col=1)
|
fig.add_trace(go.Bar(x=t, y=buy_vol, marker_color=colors_v, name="Taker Net"), row=2, col=1)
|
||||||
if "sumOpenInterest" not in df.columns:
|
if "sumOpenInterest" not in df.columns:
|
||||||
df["spike_avg"] = df["volume"].rolling(10).mean()
|
df["spike_avg"] = df["volume"].rolling(10).mean()
|
||||||
spike_mask = df["volume"] > df["spike_avg"] * 3
|
|
||||||
fig.add_trace(go.Scatter(x=t, y=df["spike_avg"] * 3, line=dict(color=COLORS["yellow"], width=0.8, dash="dot"), name="스파이크 기준(3x)"), row=2, col=1)
|
fig.add_trace(go.Scatter(x=t, y=df["spike_avg"] * 3, line=dict(color=COLORS["yellow"], width=0.8, dash="dot"), name="스파이크 기준(3x)"), row=2, col=1)
|
||||||
|
|
||||||
# ── OI ──
|
|
||||||
if "sumOpenInterest" in df.columns:
|
if "sumOpenInterest" in df.columns:
|
||||||
fig.add_trace(go.Scatter(x=t, y=df["sumOpenInterest"], line=dict(color=COLORS["purple"], width=1.5), fill="tozeroy", fillcolor="rgba(156,39,176,0.15)", name="OI"), row=3, col=1)
|
fig.add_trace(go.Scatter(x=t, y=df["sumOpenInterest"], line=dict(color=COLORS["purple"], width=1.5), fill="tozeroy", fillcolor="rgba(156,39,176,0.15)", name="OI"), row=3, col=1)
|
||||||
|
|
||||||
# ── FR ──
|
|
||||||
if "fundingRate" in df.columns:
|
if "fundingRate" in df.columns:
|
||||||
fr_colors = [COLORS["red"] if v < 0 else COLORS["green"] for v in df["fundingRate"]]
|
fr_colors = [COLORS["red"] if v < 0 else COLORS["green"] for v in df["fundingRate"]]
|
||||||
fig.add_trace(go.Bar(x=t, y=df["fundingRate"], marker_color=fr_colors, name="FR"), row=4, col=1)
|
fig.add_trace(go.Bar(x=t, y=df["fundingRate"], marker_color=fr_colors, name="FR"), row=4, col=1)
|
||||||
@@ -479,26 +445,22 @@ def build_chart(symbol, interval):
|
|||||||
fig.add_hline(y=-0.005, line=dict(color=COLORS["orange"], width=1, dash="dash"), row=4, col=1)
|
fig.add_hline(y=-0.005, line=dict(color=COLORS["orange"], width=1, dash="dash"), row=4, col=1)
|
||||||
fig.add_hline(y=-0.007, line=dict(color=COLORS["red"], width=1, dash="dash"), row=4, col=1)
|
fig.add_hline(y=-0.007, line=dict(color=COLORS["red"], width=1, dash="dash"), row=4, col=1)
|
||||||
|
|
||||||
# ── L/S Ratio ──
|
|
||||||
if "longShortRatio" in df.columns:
|
if "longShortRatio" in df.columns:
|
||||||
fig.add_trace(go.Scatter(x=t, y=df["longShortRatio"], line=dict(color=COLORS["orange"], width=1.5), name="탑트레이더 L/S"), row=5, col=1)
|
fig.add_trace(go.Scatter(x=t, y=df["longShortRatio"], line=dict(color=COLORS["orange"], width=1.5), name="탑트레이더 L/S"), row=5, col=1)
|
||||||
fig.add_hline(y=1.0, line=dict(color=COLORS["grid"], width=0.8, dash="dash"), row=5, col=1)
|
fig.add_hline(y=1.0, line=dict(color=COLORS["grid"], width=0.8, dash="dash"), row=5, col=1)
|
||||||
|
|
||||||
# ── RSI / StochRSI ──
|
|
||||||
fig.add_trace(go.Scatter(x=t, y=df["RSI"], line=dict(color=COLORS["blue"], width=1.5), name="RSI(14)"), row=6, col=1)
|
fig.add_trace(go.Scatter(x=t, y=df["RSI"], line=dict(color=COLORS["blue"], width=1.5), name="RSI(14)"), row=6, col=1)
|
||||||
fig.add_trace(go.Scatter(x=t, y=df["StochRSI_k"],line=dict(color=COLORS["yellow"], width=1.2, dash="dot"), name="StochRSI K"), row=6, col=1)
|
fig.add_trace(go.Scatter(x=t, y=df["StochRSI_k"],line=dict(color=COLORS["yellow"], width=1.2, dash="dot"), name="StochRSI K"), row=6, col=1)
|
||||||
fig.add_trace(go.Scatter(x=t, y=df["StochRSI_d"],line=dict(color=COLORS["orange"], width=1.0, dash="dot"), name="StochRSI D"), row=6, col=1)
|
fig.add_trace(go.Scatter(x=t, y=df["StochRSI_d"],line=dict(color=COLORS["orange"], width=1.0, dash="dot"), name="StochRSI D"), row=6, col=1)
|
||||||
for lvl in [20, 50, 80]:
|
for lvl in [20, 50, 80]:
|
||||||
fig.add_hline(y=lvl, line=dict(color=COLORS["grid"], width=0.6, dash="dash"), row=6, col=1)
|
fig.add_hline(y=lvl, line=dict(color=COLORS["grid"], width=0.6, dash="dash"), row=6, col=1)
|
||||||
|
|
||||||
# ── MACD ──
|
|
||||||
hist_colors = [COLORS["green"] if v >= 0 else COLORS["red"] for v in df["MACD_hist"].fillna(0)]
|
hist_colors = [COLORS["green"] if v >= 0 else COLORS["red"] for v in df["MACD_hist"].fillna(0)]
|
||||||
fig.add_trace(go.Bar(x=t, y=df["MACD_hist"], marker_color=hist_colors, name="MACD Hist"), row=7, col=1)
|
fig.add_trace(go.Bar(x=t, y=df["MACD_hist"], marker_color=hist_colors, name="MACD Hist"), row=7, col=1)
|
||||||
fig.add_trace(go.Scatter(x=t, y=df["MACD"], line=dict(color=COLORS["blue"], width=1.2), name="MACD"), row=7, col=1)
|
fig.add_trace(go.Scatter(x=t, y=df["MACD"], line=dict(color=COLORS["blue"], width=1.2), name="MACD"), row=7, col=1)
|
||||||
fig.add_trace(go.Scatter(x=t, y=df["MACD_signal"], line=dict(color=COLORS["orange"], width=1.2), name="Signal"), row=7, col=1)
|
fig.add_trace(go.Scatter(x=t, y=df["MACD_signal"], line=dict(color=COLORS["orange"], width=1.2), name="Signal"), row=7, col=1)
|
||||||
fig.add_hline(y=0, line=dict(color=COLORS["grid"], width=0.5), row=7, col=1)
|
fig.add_hline(y=0, line=dict(color=COLORS["grid"], width=0.5), row=7, col=1)
|
||||||
|
|
||||||
# ── 현재가 라인 ──
|
|
||||||
last_price = df["close"].iloc[-1]
|
last_price = df["close"].iloc[-1]
|
||||||
fig.add_hline(y=last_price, line=dict(color=COLORS["yellow"], width=1, dash="dash"), row=1, col=1)
|
fig.add_hline(y=last_price, line=dict(color=COLORS["yellow"], width=1, dash="dash"), row=1, col=1)
|
||||||
fig.add_annotation(
|
fig.add_annotation(
|
||||||
@@ -512,7 +474,6 @@ def build_chart(symbol, interval):
|
|||||||
row=1, col=1
|
row=1, col=1
|
||||||
)
|
)
|
||||||
|
|
||||||
# ── 레이아웃 ──
|
|
||||||
fig.update_layout(
|
fig.update_layout(
|
||||||
height=1600,
|
height=1600,
|
||||||
paper_bgcolor="#ffffff",
|
paper_bgcolor="#ffffff",
|
||||||
@@ -540,34 +501,27 @@ def build_chart(symbol, interval):
|
|||||||
margin = (hi - lo) * pad if (hi - lo) > 0 else abs(lo) * pad + 1
|
margin = (hi - lo) * pad if (hi - lo) > 0 else abs(lo) * pad + 1
|
||||||
return lo - margin, hi + margin
|
return lo - margin, hi + margin
|
||||||
|
|
||||||
# 캔들 Y축
|
|
||||||
lo, hi = tight(pd.concat([df["low"], df["high"]]), pad=0.02)
|
lo, hi = tight(pd.concat([df["low"], df["high"]]), pad=0.02)
|
||||||
if lo: fig.update_yaxes(range=[lo, hi], row=1, col=1)
|
if lo: fig.update_yaxes(range=[lo, hi], row=1, col=1)
|
||||||
|
|
||||||
# Taker Volume — 98% 분위수 기준 대칭 범위
|
|
||||||
buy_vol = df["taker_buy_vol"] - df["taker_sell_vol"]
|
buy_vol = df["taker_buy_vol"] - df["taker_sell_vol"]
|
||||||
abs_max = buy_vol.abs().quantile(0.98) * 1.5
|
abs_max = buy_vol.abs().quantile(0.98) * 1.5
|
||||||
if abs_max > 0: fig.update_yaxes(range=[-abs_max, abs_max], row=2, col=1)
|
if abs_max > 0: fig.update_yaxes(range=[-abs_max, abs_max], row=2, col=1)
|
||||||
|
|
||||||
# OI
|
|
||||||
if "sumOpenInterest" in df.columns:
|
if "sumOpenInterest" in df.columns:
|
||||||
lo, hi = tight(df["sumOpenInterest"], pad=0.05)
|
lo, hi = tight(df["sumOpenInterest"], pad=0.05)
|
||||||
if lo: fig.update_yaxes(range=[lo, hi], row=3, col=1)
|
if lo: fig.update_yaxes(range=[lo, hi], row=3, col=1)
|
||||||
|
|
||||||
# FR
|
|
||||||
if "fundingRate" in df.columns:
|
if "fundingRate" in df.columns:
|
||||||
lo, hi = tight(df["fundingRate"], pad=0.2)
|
lo, hi = tight(df["fundingRate"], pad=0.2)
|
||||||
if lo: fig.update_yaxes(range=[lo, hi], row=4, col=1)
|
if lo: fig.update_yaxes(range=[lo, hi], row=4, col=1)
|
||||||
|
|
||||||
# L/S
|
|
||||||
if "longShortRatio" in df.columns:
|
if "longShortRatio" in df.columns:
|
||||||
lo, hi = tight(df["longShortRatio"], pad=0.05)
|
lo, hi = tight(df["longShortRatio"], pad=0.05)
|
||||||
if lo: fig.update_yaxes(range=[lo, hi], row=5, col=1)
|
if lo: fig.update_yaxes(range=[lo, hi], row=5, col=1)
|
||||||
|
|
||||||
# RSI
|
|
||||||
fig.update_yaxes(range=[0, 100], row=6, col=1)
|
fig.update_yaxes(range=[0, 100], row=6, col=1)
|
||||||
|
|
||||||
# MACD
|
|
||||||
macd_all = pd.concat([df["MACD"], df["MACD_signal"], df["MACD_hist"]]).dropna()
|
macd_all = pd.concat([df["MACD"], df["MACD_signal"], df["MACD_hist"]]).dropna()
|
||||||
lo, hi = tight(macd_all, pad=0.1)
|
lo, hi = tight(macd_all, pad=0.1)
|
||||||
if lo: fig.update_yaxes(range=[lo, hi], row=7, col=1)
|
if lo: fig.update_yaxes(range=[lo, hi], row=7, col=1)
|
||||||
@@ -589,7 +543,7 @@ def _alert_loop():
|
|||||||
symbol = _alert_symbol
|
symbol = _alert_symbol
|
||||||
interval = _alert_interval
|
interval = _alert_interval
|
||||||
df = get_klines(symbol, interval, 50)
|
df = get_klines(symbol, interval, 50)
|
||||||
oi_period = interval if interval in ["5m","15m","30m","1h","4h"] else "5m"
|
oi_period = interval if interval in ["5m","15m","30m","1h","4h","12h","1d","3d","1M"] else "5m"
|
||||||
try:
|
try:
|
||||||
oi = get_open_interest_history(symbol, oi_period, 50)
|
oi = get_open_interest_history(symbol, oi_period, 50)
|
||||||
if not oi.empty:
|
if not oi.empty:
|
||||||
@@ -606,51 +560,49 @@ def _alert_loop():
|
|||||||
print(f"[알림스레드 오류] {e}")
|
print(f"[알림스레드 오류] {e}")
|
||||||
time.sleep(30)
|
time.sleep(30)
|
||||||
|
|
||||||
def _to_floor_freq(period):
|
|
||||||
return {"1m":"1min","3m":"3min","5m":"5min","15m":"15min","30m":"30min","1h":"1h","4h":"4h"}.get(period, period)
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────
|
# ──────────────────────────────────────────────
|
||||||
# 메인 UI
|
# 메인 UI
|
||||||
# ──────────────────────────────────────────────
|
# ──────────────────────────────────────────────
|
||||||
def main():
|
def main():
|
||||||
global _alert_started, _alert_symbol, _alert_interval
|
global _alert_started, _alert_symbol, _alert_interval
|
||||||
|
|
||||||
# 알림 스레드 시작 (1회만)
|
|
||||||
if not _alert_started:
|
if not _alert_started:
|
||||||
t = threading.Thread(target=_alert_loop, daemon=True)
|
t = threading.Thread(target=_alert_loop, daemon=True)
|
||||||
t.start()
|
t.start()
|
||||||
_alert_started = True
|
_alert_started = True
|
||||||
|
|
||||||
# 헤더
|
|
||||||
col1, col2, col3, col4, col5 = st.columns([2, 1, 1, 1, 2])
|
col1, col2, col3, col4, col5 = st.columns([2, 1, 1, 1, 2])
|
||||||
with col1:
|
with col1:
|
||||||
st.markdown("### 📊 Futures Dashboard")
|
st.markdown("### 📊 Futures Dashboard")
|
||||||
with col2:
|
with col2:
|
||||||
symbol = st.selectbox("심볼", ["BTCUSDT","ETHUSDT","SOLUSDT","BNBUSDT"], index=0, label_visibility="collapsed")
|
symbol = st.selectbox("심볼", ["BTCUSDT","ETHUSDT","SOLUSDT","BNBUSDT"], index=0, label_visibility="collapsed")
|
||||||
with col3:
|
with col3:
|
||||||
interval = st.selectbox("시간축", ["1m","3m","5m","15m","30m","1h","4h"], index=2, label_visibility="collapsed")
|
interval = st.selectbox("시간축", ["1m","3m","5m","15m","30m","1h","4h","12h","1d","3d","1M"], index=2, label_visibility="collapsed")
|
||||||
with col4:
|
with col4:
|
||||||
refresh_sec = st.number_input("갱신(초)", min_value=10, max_value=300, value=30)
|
refresh_sec = st.number_input("갱신(초)", min_value=10, max_value=300, value=30)
|
||||||
with col5:
|
with col5:
|
||||||
col5a, col5b, col5c = st.columns(3)
|
now_kst = datetime.now(timezone.utc) + KST
|
||||||
|
st.markdown(f"<div style='text-align:right; font-size:12px; color:#888; margin-bottom:2px;'>🕐 마지막 갱신: {now_kst.strftime('%Y-%m-%d %H:%M:%S')} KST</div>", unsafe_allow_html=True)
|
||||||
|
col5a, col5b, col5c, col5d = st.columns(4)
|
||||||
with col5a:
|
with col5a:
|
||||||
refresh_btn = st.button("🔄 새로고침")
|
refresh_btn = st.button("🔄 새로고침")
|
||||||
with col5b:
|
with col5b:
|
||||||
auto = st.checkbox("자동갱신", value=True)
|
auto = st.checkbox("자동갱신", value=True)
|
||||||
with col5c:
|
with col5c:
|
||||||
show_legend = st.checkbox("범례", value=False)
|
show_legend = st.checkbox("범례", value=False)
|
||||||
|
with col5d:
|
||||||
|
mobile_mode = st.checkbox("모바일", value=False)
|
||||||
|
|
||||||
|
candle_limit = 53 if mobile_mode else 200
|
||||||
|
|
||||||
# 알림 타겟 업데이트
|
|
||||||
with _alert_lock:
|
with _alert_lock:
|
||||||
_alert_symbol = symbol
|
_alert_symbol = symbol
|
||||||
_alert_interval = interval
|
_alert_interval = interval
|
||||||
|
|
||||||
# 데이터 로드 & 차트
|
|
||||||
try:
|
try:
|
||||||
with st.spinner("데이터 로딩 중..."):
|
with st.spinner("데이터 로딩 중..."):
|
||||||
fig, df = build_chart(symbol, interval)
|
fig, df = build_chart(symbol, interval, candle_limit)
|
||||||
|
|
||||||
# FR 배너
|
|
||||||
try:
|
try:
|
||||||
fr_df = get_funding_rate(symbol, 1)
|
fr_df = get_funding_rate(symbol, 1)
|
||||||
if not fr_df.empty:
|
if not fr_df.empty:
|
||||||
@@ -668,27 +620,21 @@ def main():
|
|||||||
fig.update_layout(showlegend=show_legend)
|
fig.update_layout(showlegend=show_legend)
|
||||||
st.plotly_chart(fig, use_container_width=True, config={
|
st.plotly_chart(fig, use_container_width=True, config={
|
||||||
"scrollZoom": True,
|
"scrollZoom": True,
|
||||||
|
"doubleClick": "reset",
|
||||||
"displayModeBar": True,
|
"displayModeBar": True,
|
||||||
"modeBarButtonsToRemove": ["lasso2d", "select2d"],
|
"modeBarButtonsToRemove": ["lasso2d", "select2d"],
|
||||||
})
|
})
|
||||||
|
|
||||||
st.caption(f"마지막 갱신: {datetime.now().strftime('%H:%M:%S')}")
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
st.error(f"데이터 로드 오류: {e}")
|
st.error(f"데이터 로드 오류: {e}")
|
||||||
import traceback
|
import traceback
|
||||||
st.code(traceback.format_exc())
|
st.code(traceback.format_exc())
|
||||||
|
|
||||||
# 자동갱신
|
|
||||||
if auto:
|
if auto:
|
||||||
time.sleep(refresh_sec)
|
time.sleep(refresh_sec)
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
try:
|
|
||||||
public_url = ngrok.connect(8501)
|
|
||||||
print(f"핸드폰: {public_url}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"ngrok 오류: {e}")
|
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
|||||||
Reference in New Issue
Block a user