Update app.py
Browse files
app.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
import sys
|
| 2 |
import os
|
| 3 |
import logging
|
|
@@ -7,910 +8,590 @@ import random
|
|
| 7 |
from datetime import datetime, timedelta
|
| 8 |
|
| 9 |
logging.basicConfig(
|
| 10 |
-
stream=sys.stdout,
|
|
|
|
| 11 |
format="%(asctime)s [%(levelname)s] %(message)s"
|
| 12 |
)
|
|
|
|
| 13 |
logger = logging.getLogger("crypto_ml")
|
| 14 |
logger.info("๐ Starting HF Space container...")
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
try:
|
| 17 |
import gradio as gr
|
| 18 |
import yfinance as yf
|
| 19 |
import pandas as pd
|
| 20 |
import numpy as np
|
|
|
|
| 21 |
from sklearn.ensemble import RandomForestClassifier
|
| 22 |
from sklearn.preprocessing import MinMaxScaler
|
|
|
|
| 23 |
import joblib
|
| 24 |
-
|
| 25 |
logger.info("โ
All dependencies imported successfully")
|
|
|
|
| 26 |
except Exception as e:
|
| 27 |
logger.error(f"โ Import failed: {e}")
|
| 28 |
sys.exit(1)
|
| 29 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
SYMBOL = "ETH-USD"
|
| 31 |
FEE = 0.0015
|
| 32 |
WINDOW = 20
|
|
|
|
| 33 |
MODEL_PATH = "/tmp/rf_model.pkl"
|
| 34 |
SCALER_PATH = "/tmp/scaler.pkl"
|
| 35 |
|
| 36 |
-
|
| 37 |
# ============================================================
|
| 38 |
-
#
|
| 39 |
# ============================================================
|
| 40 |
|
| 41 |
def _clean_columns(df):
|
| 42 |
-
|
| 43 |
if df is None or df.empty:
|
| 44 |
return df
|
| 45 |
|
| 46 |
-
# Step 1: Handle MultiIndex
|
| 47 |
if isinstance(df.columns, pd.MultiIndex):
|
| 48 |
-
logger.info(f"๐ง MultiIndex detected, levels: {[list(l) for l in df.levels]}")
|
| 49 |
-
# Try droplevel(0) โ removes ticker name level
|
| 50 |
try:
|
| 51 |
-
df.columns = df.columns.
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
df = df.loc[:, ~df.columns.duplicated()]
|
| 63 |
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
for c in df.columns:
|
| 67 |
-
name = str(c).strip()
|
| 68 |
-
# Map common variations
|
| 69 |
-
name_map = {
|
| 70 |
-
"open": "Open", "high": "High", "low": "Low",
|
| 71 |
-
"close": "Close", "volume": "Volume",
|
| 72 |
-
"adj close": "Close", "adj_close": "Close",
|
| 73 |
-
}
|
| 74 |
-
name = name_map.get(name.lower(), name)
|
| 75 |
-
new_cols.append(name)
|
| 76 |
-
df.columns = new_cols
|
| 77 |
-
|
| 78 |
-
# Step 4: Ensure numeric
|
| 79 |
-
for col in df.columns:
|
| 80 |
-
if col in ["Open", "High", "Low", "Close", "Volume"]:
|
| 81 |
df[col] = pd.to_numeric(df[col], errors="coerce")
|
| 82 |
|
| 83 |
-
|
| 84 |
-
required = ["Open", "High", "Low", "Close", "Volume"]
|
| 85 |
-
available = [c for c in required if c in df.columns]
|
| 86 |
-
if available:
|
| 87 |
-
df = df.dropna(subset=available)
|
| 88 |
|
| 89 |
-
logger.info(f"
|
| 90 |
-
return df
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
def fetch_method_ticker_history(symbol, period="90d", interval="1h"):
|
| 94 |
-
"""Method 1: yf.Ticker().history() โ most reliable on cloud."""
|
| 95 |
-
try:
|
| 96 |
-
logger.info(f"๐ฅ Method 1: Ticker.history({symbol}, {period}, {interval})")
|
| 97 |
-
ticker = yf.Ticker(symbol)
|
| 98 |
-
df = ticker.history(period=period, interval=interval, auto_adjust=True)
|
| 99 |
-
if df is not None and not df.empty:
|
| 100 |
-
logger.info(f"โ
Method 1 success: {len(df)} rows")
|
| 101 |
-
return df
|
| 102 |
-
except Exception as e:
|
| 103 |
-
logger.warning(f"โ ๏ธ Method 1 failed: {e}")
|
| 104 |
-
return pd.DataFrame()
|
| 105 |
|
|
|
|
| 106 |
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
logger.info(f"๐ฅ Method 2: yf.download({symbol}, {period}, {interval})")
|
| 111 |
-
session = requests.Session()
|
| 112 |
-
session.headers.update({
|
| 113 |
-
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
| 114 |
-
})
|
| 115 |
-
df = yf.download(
|
| 116 |
-
symbol, period=period, interval=interval,
|
| 117 |
-
progress=False, threads=False,
|
| 118 |
-
session=session
|
| 119 |
-
)
|
| 120 |
-
if df is not None and not df.empty:
|
| 121 |
-
logger.info(f"โ
Method 2 success: {len(df)} rows")
|
| 122 |
-
return df
|
| 123 |
-
except Exception as e:
|
| 124 |
-
logger.warning(f"โ ๏ธ Method 2 failed: {e}")
|
| 125 |
-
return pd.DataFrame()
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
def fetch_method_alt_symbol(period="90d", interval="1h"):
|
| 129 |
-
"""Method 3: Try alternate symbol formats."""
|
| 130 |
-
alt_symbols = ["ETH-USD", "ETHUSD=X", "ETHUSDT=X", "ETH-USD"]
|
| 131 |
-
for sym in alt_symbols:
|
| 132 |
-
try:
|
| 133 |
-
logger.info(f"๐ฅ Method 3: Trying symbol '{sym}'")
|
| 134 |
-
ticker = yf.Ticker(sym)
|
| 135 |
-
df = ticker.history(period=period, interval=interval, auto_adjust=True)
|
| 136 |
-
if df is not None and not df.empty and len(df) > 20:
|
| 137 |
-
logger.info(f"โ
Method 3 success with '{sym}': {len(df)} rows")
|
| 138 |
-
return df
|
| 139 |
-
except Exception as e:
|
| 140 |
-
logger.warning(f"โ ๏ธ Method 3 symbol '{sym}' failed: {e}")
|
| 141 |
-
time.sleep(1)
|
| 142 |
-
return pd.DataFrame()
|
| 143 |
-
|
| 144 |
|
| 145 |
-
def
|
| 146 |
-
"""Method 4: Try shorter periods and intervals."""
|
| 147 |
-
configs = [
|
| 148 |
-
("60d", "1h"),
|
| 149 |
-
("30d", "1h"),
|
| 150 |
-
("7d", "15m"),
|
| 151 |
-
("60d", "1d"),
|
| 152 |
-
("5d", "5m"),
|
| 153 |
-
]
|
| 154 |
-
for period, interval in configs:
|
| 155 |
-
try:
|
| 156 |
-
logger.info(f"๐ฅ Method 4: Ticker.history({symbol}, {period}, {interval})")
|
| 157 |
-
ticker = yf.Ticker(symbol)
|
| 158 |
-
df = ticker.history(period=period, interval=interval, auto_adjust=True)
|
| 159 |
-
if df is not None and not df.empty and len(df) > 20:
|
| 160 |
-
logger.info(f"โ
Method 4 success ({period}/{interval}): {len(df)} rows")
|
| 161 |
-
return df
|
| 162 |
-
except Exception as e:
|
| 163 |
-
logger.warning(f"โ ๏ธ Method 4 ({period}/{interval}) failed: {e}")
|
| 164 |
-
time.sleep(1)
|
| 165 |
-
return pd.DataFrame()
|
| 166 |
|
|
|
|
| 167 |
|
| 168 |
-
def generate_synthetic_data(n_bars=500):
|
| 169 |
-
"""Method 5: Fallback synthetic data so the app ALWAYS works."""
|
| 170 |
-
logger.info("๐ฒ Generating synthetic ETH price data as fallback...")
|
| 171 |
-
random.seed(42)
|
| 172 |
np.random.seed(42)
|
| 173 |
|
| 174 |
end = datetime.utcnow()
|
| 175 |
-
|
| 176 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
|
|
|
|
|
|
| 181 |
|
| 182 |
for _ in range(n_bars):
|
|
|
|
| 183 |
ret = np.random.normal(0.0005, 0.015)
|
|
|
|
| 184 |
open_p = price
|
| 185 |
close_p = price * (1 + ret)
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
|
| 190 |
opens.append(open_p)
|
| 191 |
highs.append(high_p)
|
| 192 |
lows.append(low_p)
|
| 193 |
closes.append(close_p)
|
| 194 |
volumes.append(vol)
|
|
|
|
| 195 |
price = close_p
|
| 196 |
|
| 197 |
df = pd.DataFrame({
|
| 198 |
-
"Open": opens,
|
| 199 |
-
"
|
| 200 |
-
|
|
|
|
|
|
|
|
|
|
| 201 |
|
| 202 |
-
df.index.name = "Datetime"
|
| 203 |
-
logger.info(f"โ
Synthetic data: {len(df)} rows, price range ${df['Close'].min():.0f}-${df['Close'].max():.0f}")
|
| 204 |
return df
|
| 205 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
|
| 207 |
-
def safe_download(symbol=SYMBOL, period="90d", interval="1h"):
|
| 208 |
-
"""Try all methods, fall back to synthetic data."""
|
| 209 |
-
# Try real data methods in order
|
| 210 |
methods = [
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
]
|
| 216 |
|
| 217 |
-
for
|
|
|
|
| 218 |
try:
|
|
|
|
|
|
|
|
|
|
| 219 |
df = method()
|
| 220 |
-
|
|
|
|
|
|
|
| 221 |
df = _clean_columns(df)
|
| 222 |
-
|
| 223 |
-
|
|
|
|
|
|
|
|
|
|
| 224 |
except Exception as e:
|
| 225 |
-
logger.warning(f"โ ๏ธ
|
| 226 |
-
time.sleep(1)
|
| 227 |
|
| 228 |
-
|
| 229 |
-
logger.warning("โ ๏ธ All real data methods failed. Using synthetic data.")
|
| 230 |
-
df = generate_synthetic_data(500)
|
| 231 |
-
df = _clean_columns(df)
|
| 232 |
-
return df, False # False = synthetic data
|
| 233 |
|
|
|
|
| 234 |
|
| 235 |
# ============================================================
|
| 236 |
-
#
|
| 237 |
# ============================================================
|
| 238 |
|
| 239 |
def fetch_and_prepare():
|
| 240 |
-
|
| 241 |
df, is_real = safe_download()
|
| 242 |
|
| 243 |
if df.empty:
|
| 244 |
-
raise ValueError("Data kosong
|
| 245 |
|
| 246 |
-
|
| 247 |
-
|
| 248 |
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
logger.warning(f"โ ๏ธ Resample failed: {e}, using raw data")
|
| 259 |
|
| 260 |
-
|
| 261 |
-
|
|
|
|
|
|
|
|
|
|
| 262 |
|
| 263 |
df.columns = [c.lower() for c in df.columns]
|
| 264 |
-
df.index.name = "timestamp"
|
| 265 |
-
df = df.reset_index()
|
| 266 |
|
| 267 |
-
#
|
|
|
|
|
|
|
|
|
|
| 268 |
delta = df["close"].diff()
|
| 269 |
-
|
| 270 |
-
|
|
|
|
|
|
|
| 271 |
rs = gain / loss.replace(0, np.nan)
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 285 |
df["macd"] = ema12 - ema26
|
| 286 |
-
df["macd_signal"] = df["macd"].ewm(span=9
|
| 287 |
-
df["macd_hist"] =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 288 |
|
| 289 |
-
|
| 290 |
-
df["
|
| 291 |
-
df["ema_21"] = df["close"].ewm(span=21, adjust=False).mean()
|
| 292 |
|
| 293 |
-
#
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 297 |
|
| 298 |
-
# โโ Label โโ
|
| 299 |
df["next_close"] = df["close"].shift(-1)
|
| 300 |
-
df["label"] = (df["next_close"] > df["close"] * (1 + FEE)).astype(int)
|
| 301 |
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
|
|
|
| 305 |
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
|
|
|
| 311 |
|
| 312 |
feat_cols = [
|
| 313 |
-
"open",
|
| 314 |
-
"
|
| 315 |
-
"
|
| 316 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 317 |
]
|
| 318 |
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
raise ValueError(f"Missing feature columns: {missing_feats}")
|
| 322 |
|
| 323 |
-
X_list, y_list = [], []
|
| 324 |
for i in range(WINDOW, len(df)):
|
| 325 |
-
window_data = df.iloc[i - WINDOW:i][feat_cols].values
|
| 326 |
-
if window_data.shape[0] == WINDOW and not np.isnan(window_data).any() and not np.isinf(window_data).any():
|
| 327 |
-
X_list.append(window_data.flatten())
|
| 328 |
-
y_list.append(int(df.iloc[i]["label"]))
|
| 329 |
|
| 330 |
-
|
| 331 |
-
|
|
|
|
| 332 |
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
X = np.nan_to_num(X, nan=0.0, posinf=0.0, neginf=0.0)
|
| 336 |
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
logger.info(f"โ
Features ready: X={X.shape}, BUY={n_buy}, SELL={n_sell}, source={data_source}")
|
| 340 |
-
return X, y, df, feat_cols, is_real
|
| 341 |
|
|
|
|
|
|
|
| 342 |
|
| 343 |
-
|
| 344 |
-
"""Quick snapshot for price header."""
|
| 345 |
-
try:
|
| 346 |
-
df, is_real = safe_download(SYMBOL, period="5d", interval="1h")
|
| 347 |
-
if df.empty or len(df) < 2:
|
| 348 |
-
return {"price": 0, "change_pct": 0, "high_24h": 0, "low_24h": 0, "volume": 0, "is_real": False}
|
| 349 |
-
|
| 350 |
-
cur = float(df["Close"].iloc[-1])
|
| 351 |
-
prev = float(df["Close"].iloc[0])
|
| 352 |
-
tail_n = min(6, len(df))
|
| 353 |
-
return {
|
| 354 |
-
"price": cur,
|
| 355 |
-
"change_pct": ((cur - prev) / prev * 100) if prev > 0 else 0,
|
| 356 |
-
"high_24h": float(df["High"].tail(tail_n).max()),
|
| 357 |
-
"low_24h": float(df["Low"].tail(tail_n).min()),
|
| 358 |
-
"volume": float(df["Volume"].tail(tail_n).sum()),
|
| 359 |
-
"is_real": is_real,
|
| 360 |
-
}
|
| 361 |
-
except Exception as e:
|
| 362 |
-
logger.error(f"Market info error: {e}")
|
| 363 |
-
return {"price": 0, "change_pct": 0, "high_24h": 0, "low_24h": 0, "volume": 0, "is_real": False}
|
| 364 |
|
|
|
|
| 365 |
|
| 366 |
-
#
|
|
|
|
|
|
|
| 367 |
|
| 368 |
def run_train():
|
|
|
|
| 369 |
try:
|
| 370 |
-
logger.info("๐ง Starting training...")
|
| 371 |
-
X, y, df_full, feat_cols, is_real = fetch_and_prepare()
|
| 372 |
|
| 373 |
-
|
| 374 |
-
n_sell = int(np.sum(y == 0))
|
| 375 |
-
if n_buy < 3 or n_sell < 3:
|
| 376 |
-
raise ValueError(f"Kelas tidak seimbang: BUY={n_buy}, SELL={n_sell}.")
|
| 377 |
|
| 378 |
scaler = MinMaxScaler()
|
|
|
|
| 379 |
X_scaled = scaler.fit_transform(X)
|
| 380 |
|
| 381 |
-
split =
|
| 382 |
-
|
| 383 |
-
|
|
|
|
| 384 |
|
| 385 |
-
|
| 386 |
-
|
| 387 |
|
| 388 |
model = RandomForestClassifier(
|
| 389 |
-
n_estimators=100,
|
| 390 |
-
|
| 391 |
-
random_state=42,
|
| 392 |
-
class_weight="balanced"
|
|
|
|
| 393 |
)
|
|
|
|
| 394 |
model.fit(X_train, y_train)
|
| 395 |
|
| 396 |
-
acc =
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
wins = int(np.sum((y_pred == 1) & (y_test == 1)))
|
| 402 |
-
win_rate = (wins / max(buy_sig, 1)) * 100
|
| 403 |
-
|
| 404 |
-
# PnL
|
| 405 |
-
start_idx = split + WINDOW
|
| 406 |
-
end_idx = start_idx + len(y_test)
|
| 407 |
-
if end_idx <= len(df_full):
|
| 408 |
-
test_closes = df_full.iloc[start_idx:end_idx]["close"].values
|
| 409 |
-
else:
|
| 410 |
-
test_closes = df_full.iloc[-(len(y_test) + 1):]["close"].values
|
| 411 |
-
|
| 412 |
-
pnl = 0.0
|
| 413 |
-
for i in range(min(len(y_pred), len(test_closes) - 1)):
|
| 414 |
-
if y_pred[i] == 1:
|
| 415 |
-
ret = (test_closes[i + 1] - test_closes[i]) / test_closes[i] - FEE
|
| 416 |
-
pnl += ret
|
| 417 |
-
pnl_pct = pnl * 100
|
| 418 |
|
| 419 |
importances = model.feature_importances_
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 423 |
|
| 424 |
joblib.dump(model, MODEL_PATH)
|
| 425 |
joblib.dump(scaler, SCALER_PATH)
|
| 426 |
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 430 |
except Exception as e:
|
| 431 |
-
tb = traceback.format_exc()
|
| 432 |
-
logger.error(f"โ Train failed: {e}")
|
| 433 |
-
logger.error(tb)
|
| 434 |
-
return _html_error(str(e), tb)
|
| 435 |
|
|
|
|
| 436 |
|
| 437 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 438 |
|
| 439 |
def run_predict():
|
|
|
|
| 440 |
try:
|
|
|
|
| 441 |
if not os.path.exists(MODEL_PATH):
|
| 442 |
-
return
|
|
|
|
|
|
|
| 443 |
|
| 444 |
-
X, y, df_full, feat_cols, is_real = fetch_and_prepare()
|
| 445 |
scaler = joblib.load(SCALER_PATH)
|
| 446 |
model = joblib.load(MODEL_PATH)
|
| 447 |
|
| 448 |
-
|
| 449 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 450 |
|
| 451 |
-
latest = df_full.iloc[-WINDOW:][feat_cols].values.flatten().reshape(1, -1)
|
| 452 |
-
latest = np.nan_to_num(latest, nan=0.0, posinf=0.0, neginf=0.0)
|
| 453 |
latest_scaled = scaler.transform(latest)
|
| 454 |
|
| 455 |
-
pred =
|
| 456 |
-
proba = model.predict_proba(latest_scaled)[0]
|
| 457 |
-
confidence = float(max(proba)) * 100
|
| 458 |
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
|
| 465 |
signal = "BUY" if pred == 1 else "SELL"
|
| 466 |
-
logger.info(f"๐ฎ {signal} conf={confidence:.1f}%")
|
| 467 |
-
return _html_signal(signal, confidence, price, rsi, macd_val, bb_pct, is_real)
|
| 468 |
-
except Exception as e:
|
| 469 |
-
tb = traceback.format_exc()
|
| 470 |
-
logger.error(f"โ Predict failed: {e}")
|
| 471 |
-
logger.error(tb)
|
| 472 |
-
return _html_error(str(e), tb)
|
| 473 |
|
|
|
|
| 474 |
|
| 475 |
-
|
| 476 |
-
|
| 477 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 478 |
|
| 479 |
# ============================================================
|
| 480 |
-
#
|
| 481 |
# ============================================================
|
| 482 |
|
| 483 |
-
def
|
| 484 |
-
if is_real:
|
| 485 |
-
return '<span style="background:rgba(14,203,129,.15);color:#0ECB81;font-size:9px;padding:2px 8px;border-radius:10px;font-weight:600;">๐ด LIVE</span>'
|
| 486 |
-
else:
|
| 487 |
-
return '<span style="background:rgba(240,185,11,.15);color:#F0B90B;font-size:9px;padding:2px 8px;border-radius:10px;font-weight:600;">๐ก SIMULATED</span>'
|
| 488 |
|
|
|
|
| 489 |
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
is_real = info.get("is_real", False)
|
| 496 |
|
| 497 |
-
|
| 498 |
|
| 499 |
-
if p == 0:
|
| 500 |
return f"""
|
| 501 |
-
|
| 502 |
-
background:#1E2329;border-bottom:1px solid #2B3139;
|
| 503 |
-
font-family:'Inter',sans-serif;">
|
| 504 |
-
<span style="color:#F0B90B;font-size:17px;font-weight:800;">ETH / USDT</span>
|
| 505 |
-
<span style="color:#5E6673;font-size:13px;">โณ Loading price data...</span>
|
| 506 |
-
{badge}
|
| 507 |
-
</div>"""
|
| 508 |
-
|
| 509 |
-
cc = "#0ECB81" if chg >= 0 else "#F6465D"
|
| 510 |
-
sign = "+" if chg >= 0 else ""
|
| 511 |
-
vs = f"${vol/1e6:.1f}M" if vol >= 1e6 else f"${vol/1e3:.0f}K"
|
| 512 |
-
return f"""
|
| 513 |
-
<div style="display:flex;align-items:center;gap:28px;padding:14px 24px;
|
| 514 |
-
background:#1E2329;border-bottom:1px solid #2B3139;flex-wrap:wrap;
|
| 515 |
-
font-family:'Inter',sans-serif;">
|
| 516 |
-
<div style="display:flex;align-items:baseline;gap:8px;">
|
| 517 |
-
<span style="color:#F0B90B;font-size:17px;font-weight:800;">ETH / USDT</span>
|
| 518 |
-
<span style="color:#5E6673;font-size:11px;">Perpetual</span>
|
| 519 |
-
{badge}
|
| 520 |
-
</div>
|
| 521 |
-
<div style="display:flex;align-items:baseline;gap:8px;">
|
| 522 |
-
<span style="color:{cc};font-size:26px;font-weight:800;">${p:,.2f}</span>
|
| 523 |
-
<span style="color:{cc};font-size:13px;font-weight:600;">{sign}{chg:.2f}%</span>
|
| 524 |
-
</div>
|
| 525 |
-
<div style="display:flex;gap:22px;flex-wrap:wrap;">
|
| 526 |
-
<div><div style="color:#5E6673;font-size:10px;text-transform:uppercase;">24h High</div>
|
| 527 |
-
<div style="color:#EAECEF;font-size:13px;font-weight:500;">${hi:,.2f}</div></div>
|
| 528 |
-
<div><div style="color:#5E6673;font-size:10px;text-transform:uppercase;">24h Low</div>
|
| 529 |
-
<div style="color:#EAECEF;font-size:13px;font-weight:500;">${lo:,.2f}</div></div>
|
| 530 |
-
<div><div style="color:#5E6673;font-size:10px;text-transform:uppercase;">24h Volume</div>
|
| 531 |
-
<div style="color:#EAECEF;font-size:13px;font-weight:500;">{vs}</div></div>
|
| 532 |
-
</div>
|
| 533 |
-
</div>"""
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
def _html_signal(signal, confidence, price, rsi, macd_val, bb_pct, is_real=True):
|
| 537 |
-
buy = signal == "BUY"
|
| 538 |
-
sc = "#0ECB81" if buy else "#F6465D"
|
| 539 |
-
sbg = "rgba(14,203,129,.12)" if buy else "rgba(246,70,93,.12)"
|
| 540 |
-
sbd = "rgba(14,203,129,.35)" if buy else "rgba(246,70,93,.35)"
|
| 541 |
-
icon = "โฒ" if buy else "โผ"
|
| 542 |
-
tag = "LONG" if buy else "SHORT"
|
| 543 |
-
cw = min(confidence, 100)
|
| 544 |
-
cc = "#0ECB81" if confidence >= 60 else ("#F0B90B" if confidence >= 40 else "#F6465D")
|
| 545 |
-
|
| 546 |
-
rc = "#0ECB81" if 30 <= rsi <= 70 else ("#F6465D" if rsi > 70 else "#F0B90B")
|
| 547 |
-
rs = "Overbought" if rsi > 70 else ("Oversold" if rsi < 30 else "Neutral")
|
| 548 |
-
mc = "#0ECB81" if macd_val > 0 else "#F6465D"
|
| 549 |
-
ms = "Bullish" if macd_val > 0 else "Bearish"
|
| 550 |
-
bc = "#F6465D" if bb_pct > .8 else ("#0ECB81" if bb_pct < .2 else "#848E9C")
|
| 551 |
-
bs = "Upper" if bb_pct > .8 else ("Lower" if bb_pct < .2 else "Mid")
|
| 552 |
-
ts = datetime.utcnow().strftime("%Y-%m-%d %H:%M UTC")
|
| 553 |
-
|
| 554 |
-
badge = _html_data_badge(is_real)
|
| 555 |
-
|
| 556 |
-
return f"""
|
| 557 |
-
<div style="background:#1E2329;border-radius:8px;border:1px solid {sbd};overflow:hidden;
|
| 558 |
-
font-family:'Inter',sans-serif;">
|
| 559 |
-
<div style="background:{sbg};padding:22px 20px;text-align:center;border-bottom:1px solid {sbd};">
|
| 560 |
-
<div style="font-size:44px;color:{sc};font-weight:900;letter-spacing:5px;">{icon} {signal}</div>
|
| 561 |
-
<div style="color:{sc};font-size:13px;margin-top:2px;">{tag} SIGNAL ยท 4H TIMEFRAME</div>
|
| 562 |
-
</div>
|
| 563 |
-
<div style="padding:14px 20px;border-bottom:1px solid #2B3139;">
|
| 564 |
-
<div style="display:flex;justify-content:space-between;margin-bottom:6px;">
|
| 565 |
-
<span style="color:#848E9C;font-size:11px;">CONFIDENCE</span>
|
| 566 |
-
<span style="color:{cc};font-size:13px;font-weight:700;">{confidence:.1f}%</span>
|
| 567 |
-
</div>
|
| 568 |
-
<div style="background:#2B3139;border-radius:4px;height:7px;overflow:hidden;">
|
| 569 |
-
<div style="background:{cc};height:100%;width:{cw}%;border-radius:4px;
|
| 570 |
-
transition:width .6s ease;"></div>
|
| 571 |
-
</div>
|
| 572 |
-
</div>
|
| 573 |
-
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;border-bottom:1px solid #2B3139;">
|
| 574 |
-
<div style="padding:10px 14px;border-right:1px solid #2B3139;text-align:center;">
|
| 575 |
-
<div style="color:#5E6673;font-size:9px;text-transform:uppercase;margin-bottom:3px;">RSI(14)</div>
|
| 576 |
-
<div style="color:{rc};font-size:17px;font-weight:700;">{rsi:.1f}</div>
|
| 577 |
-
<div style="color:{rc};font-size:9px;">{rs}</div></div>
|
| 578 |
-
<div style="padding:10px 14px;border-right:1px solid #2B3139;text-align:center;">
|
| 579 |
-
<div style="color:#5E6673;font-size:9px;text-transform:uppercase;margin-bottom:3px;">MACD</div>
|
| 580 |
-
<div style="color:{mc};font-size:17px;font-weight:700;">{macd_val:.2f}</div>
|
| 581 |
-
<div style="color:{mc};font-size:9px;">{ms}</div></div>
|
| 582 |
-
<div style="padding:10px 14px;text-align:center;">
|
| 583 |
-
<div style="color:#5E6673;font-size:9px;text-transform:uppercase;margin-bottom:3px;">BB %B</div>
|
| 584 |
-
<div style="color:{bc};font-size:17px;font-weight:700;">{bb_pct:.2f}</div>
|
| 585 |
-
<div style="color:{bc};font-size:9px;">{bs}</div></div>
|
| 586 |
-
</div>
|
| 587 |
-
<div style="padding:10px 20px;display:flex;justify-content:space-between;align-items:center;">
|
| 588 |
-
<span style="color:#5E6673;font-size:10px;">โฐ {ts}</span>
|
| 589 |
-
<div style="display:flex;align-items:center;gap:8px;">
|
| 590 |
-
{badge}
|
| 591 |
-
<span style="color:#5E6673;font-size:10px;">Entry โ ${price:,.2f}</span>
|
| 592 |
-
</div>
|
| 593 |
-
</div>
|
| 594 |
-
</div>"""
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
def _html_train(acc, win_rate, pnl_pct, n_tr, n_te, buys, sells, top5, df_full, is_real=True):
|
| 598 |
-
ac = "#0ECB81" if acc >= .58 else ("#F0B90B" if acc >= .50 else "#F6465D")
|
| 599 |
-
wc = "#0ECB81" if win_rate >= 55 else ("#F0B90B" if win_rate >= 45 else "#F6465D")
|
| 600 |
-
pc = "#0ECB81" if pnl_pct >= 0 else "#F6465D"
|
| 601 |
-
ps = "+" if pnl_pct >= 0 else ""
|
| 602 |
-
ts = datetime.utcnow().strftime("%Y-%m-%d %H:%M UTC")
|
| 603 |
-
badge = _html_data_badge(is_real)
|
| 604 |
-
|
| 605 |
-
fi = ""
|
| 606 |
-
for name, imp in top5:
|
| 607 |
-
w = min(imp * 500, 100)
|
| 608 |
-
fi += f"""<div style="display:flex;justify-content:space-between;align-items:center;
|
| 609 |
-
padding:5px 0;border-bottom:1px solid #2B3139;">
|
| 610 |
-
<span style="color:#EAECEF;font-size:11px;">{name.upper()}</span>
|
| 611 |
-
<div style="display:flex;align-items:center;gap:6px;">
|
| 612 |
-
<div style="background:#2B3139;border-radius:2px;height:5px;width:70px;overflow:hidden;">
|
| 613 |
-
<div style="background:#F0B90B;height:100%;width:{w}%;border-radius:2px;"></div></div>
|
| 614 |
-
<span style="color:#848E9C;font-size:10px;min-width:36px;text-align:right;">{imp*100:.1f}%</span>
|
| 615 |
-
</div></div>"""
|
| 616 |
-
|
| 617 |
-
n_recent = min(5, len(df_full))
|
| 618 |
-
recent = df_full.tail(n_recent)
|
| 619 |
-
rows = ""
|
| 620 |
-
for _, r in recent.iterrows():
|
| 621 |
-
ts_val = r.get("timestamp", None)
|
| 622 |
-
t = pd.to_datetime(ts_val).strftime("%m/%d %H:%M") if pd.notna(ts_val) else ""
|
| 623 |
-
c = float(r.get("close", 0))
|
| 624 |
-
ch = float(r.get("return_1", 0)) * 100
|
| 625 |
-
ch2 = "#0ECB81" if ch >= 0 else "#F6465D"
|
| 626 |
-
rv = float(r.get("rsi", 50))
|
| 627 |
-
rows += f"""<tr style="border-bottom:1px solid #2B3139;">
|
| 628 |
-
<td style="color:#848E9C;font-size:10px;padding:5px 6px;">{t}</td>
|
| 629 |
-
<td style="color:#EAECEF;font-size:10px;padding:5px 6px;text-align:right;">${c:,.2f}</td>
|
| 630 |
-
<td style="color:{ch2};font-size:10px;padding:5px 6px;text-align:right;">{ch:+.2f}%</td>
|
| 631 |
-
<td style="color:#848E9C;font-size:10px;padding:5px 6px;text-align:right;">{rv:.1f}</td></tr>"""
|
| 632 |
-
|
| 633 |
-
data_notice = ""
|
| 634 |
-
if not is_real:
|
| 635 |
-
data_notice = """
|
| 636 |
-
<div style="background:rgba(240,185,11,.08);border:1px solid rgba(240,185,11,.25);
|
| 637 |
-
border-radius:4px;padding:8px 12px;margin:8px 18px;font-size:11px;">
|
| 638 |
-
<span style="color:#F0B90B;font-weight:600;">โ ๏ธ Yahoo Finance tidak tersedia.</span>
|
| 639 |
-
<span style="color:#848E9C;"> Model dilatih menggunakan data simulasi.
|
| 640 |
-
Sinyal mungkin tidak akurat untuk trading real.</span>
|
| 641 |
-
</div>"""
|
| 642 |
-
|
| 643 |
-
return f"""
|
| 644 |
-
<div style="background:#1E2329;border-radius:8px;border:1px solid #2B3139;overflow:hidden;
|
| 645 |
-
font-family:'Inter',sans-serif;">
|
| 646 |
-
<div style="padding:14px 20px;border-bottom:1px solid #2B3139;display:flex;
|
| 647 |
-
justify-content:space-between;align-items:center;">
|
| 648 |
-
<div style="display:flex;align-items:center;gap:8px;">
|
| 649 |
-
<span style="color:#F0B90B;font-size:13px;font-weight:700;">โ
MODEL TRAINED</span>
|
| 650 |
-
{badge}
|
| 651 |
-
</div>
|
| 652 |
-
<span style="color:#5E6673;font-size:10px;">{ts}</span>
|
| 653 |
-
</div>
|
| 654 |
-
{data_notice}
|
| 655 |
-
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;border-bottom:1px solid #2B3139;">
|
| 656 |
-
<div style="padding:10px 14px;border-right:1px solid #2B3139;text-align:center;">
|
| 657 |
-
<div style="color:#5E6673;font-size:9px;text-transform:uppercase;margin-bottom:2px;">Test Acc</div>
|
| 658 |
-
<div style="color:{ac};font-size:18px;font-weight:700;">{acc*100:.1f}%</div></div>
|
| 659 |
-
<div style="padding:10px 14px;border-right:1px solid #2B3139;text-align:center;">
|
| 660 |
-
<div style="color:#5E6673;font-size:9px;text-transform:uppercase;margin-bottom:2px;">Win Rate</div>
|
| 661 |
-
<div style="color:{wc};font-size:18px;font-weight:700;">{win_rate:.1f}%</div></div>
|
| 662 |
-
<div style="padding:10px 14px;border-right:1px solid #2B3139;text-align:center;">
|
| 663 |
-
<div style="color:#5E6673;font-size:9px;text-transform:uppercase;margin-bottom:2px;">Sim PnL</div>
|
| 664 |
-
<div style="color:{pc};font-size:18px;font-weight:700;">{ps}{pnl_pct:.2f}%</div></div>
|
| 665 |
-
<div style="padding:10px 14px;text-align:center;">
|
| 666 |
-
<div style="color:#5E6673;font-size:9px;text-transform:uppercase;margin-bottom:2px;">Signals</div>
|
| 667 |
-
<div style="font-size:18px;font-weight:700;">
|
| 668 |
-
<span style="color:#0ECB81;">{buys}B</span>
|
| 669 |
-
<span style="color:#5E6673;">/</span>
|
| 670 |
-
<span style="color:#F6465D;">{sells}S</span></div></div>
|
| 671 |
-
</div>
|
| 672 |
-
<div style="display:grid;grid-template-columns:1fr 1fr;border-bottom:1px solid #2B3139;">
|
| 673 |
-
<div style="padding:14px 18px;border-right:1px solid #2B3139;">
|
| 674 |
-
<div style="color:#848E9C;font-size:10px;text-transform:uppercase;margin-bottom:6px;">
|
| 675 |
-
Top Feature Importance</div>{fi}
|
| 676 |
-
</div>
|
| 677 |
-
<div style="padding:14px 18px;">
|
| 678 |
-
<div style="color:#848E9C;font-size:10px;text-transform:uppercase;margin-bottom:6px;">
|
| 679 |
-
Recent 4H Candles</div>
|
| 680 |
-
<table style="width:100%;border-collapse:collapse;">
|
| 681 |
-
<thead><tr style="border-bottom:1px solid #363C45;">
|
| 682 |
-
<th style="color:#5E6673;font-size:9px;text-transform:uppercase;padding:4px 6px;text-align:left;">Time</th>
|
| 683 |
-
<th style="color:#5E6673;font-size:9px;text-transform:uppercase;padding:4px 6px;text-align:right;">Close</th>
|
| 684 |
-
<th style="color:#5E6673;font-size:9px;text-transform:uppercase;padding:4px 6px;text-align:right;">Chg</th>
|
| 685 |
-
<th style="color:#5E6673;font-size:9px;text-transform:uppercase;padding:4px 6px;text-align:right;">RSI</th>
|
| 686 |
-
</tr></thead><tbody>{rows}</tbody></table>
|
| 687 |
-
</div>
|
| 688 |
-
</div>
|
| 689 |
-
<div style="padding:8px 18px;display:flex;justify-content:space-between;">
|
| 690 |
-
<span style="color:#5E6673;font-size:10px;">Train: {n_tr} ยท Test: {n_te}</span>
|
| 691 |
-
<span style="color:#5E6673;font-size:10px;">Split 88% / 12%</span>
|
| 692 |
-
</div>
|
| 693 |
-
</div>"""
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
def _html_error(msg, tb=""):
|
| 697 |
-
tb_lines = tb.strip().split("\n")[-6:] if tb else []
|
| 698 |
-
tb_html = ""
|
| 699 |
-
if tb_lines:
|
| 700 |
-
tb_text = "<br>".join(tb_lines)
|
| 701 |
-
tb_html = f"""<details style="margin-top:10px;text-align:left;">
|
| 702 |
-
<summary style="color:#5E6673;font-size:10px;cursor:pointer;">Show Details</summary>
|
| 703 |
-
<pre style="color:#848E9C;font-size:9px;background:#2B3139;padding:8px;border-radius:4px;
|
| 704 |
-
overflow-x:auto;margin-top:4px;white-space:pre-wrap;">{tb_text}</pre>
|
| 705 |
-
</details>"""
|
| 706 |
-
|
| 707 |
-
return f"""<div style="background:#1E2329;border-radius:8px;border:1px solid rgba(246,70,93,.35);
|
| 708 |
-
padding:20px;text-align:center;font-family:'Inter',sans-serif;">
|
| 709 |
-
<div style="color:#F6465D;font-size:18px;font-weight:700;margin-bottom:6px;">โ ERROR</div>
|
| 710 |
-
<div style="color:#848E9C;font-size:12px;word-break:break-word;">{msg}</div>
|
| 711 |
-
{tb_html}
|
| 712 |
-
</div>"""
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
def _html_warn(msg):
|
| 716 |
-
return f"""<div style="background:#1E2329;border-radius:8px;border:1px solid rgba(240,185,11,.35);
|
| 717 |
-
padding:20px;text-align:center;font-family:'Inter',sans-serif;">
|
| 718 |
-
<div style="color:#F0B90B;font-size:18px;font-weight:700;margin-bottom:6px;">โ ๏ธ</div>
|
| 719 |
-
<div style="color:#848E9C;font-size:12px;">{msg}</div>
|
| 720 |
-
</div>"""
|
| 721 |
|
|
|
|
| 722 |
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
# ============================================================
|
| 726 |
-
|
| 727 |
-
TRADINGVIEW_HTML = """
|
| 728 |
-
<div style="width:100%;height:440px;background:#1E2329;border-radius:8px;overflow:hidden;
|
| 729 |
-
border:1px solid #2B3139;">
|
| 730 |
-
<iframe src="https://s.tradingview.com/widgetembed/?frameElementId=tv&symbol=BINANCE%3AETHUSDT&interval=240&hidesidetoolbar=0&symboledit=1&saveimage=1&toolbarbg=%231E2329&studies=%5B%7B%22id%22%3A%22MASimple%40tv-basicstudies%22%7D%2C%7B%22id%22%3A%22RSI%40tv-basicstudies%22%7D%5D&theme=dark&style=1&timezone=UTC&withdateranges=1&showpopupbutton=0&studies_overrides=%7B%7D&overrides=%7B%7D&enabled_features=%5B%5D&disabled_features=%5B%5D&locale=en"
|
| 731 |
-
style="border:none;width:100%;height:100%;" allowfullscreen></iframe>
|
| 732 |
-
</div>
|
| 733 |
"""
|
| 734 |
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
:root {
|
| 739 |
-
--bg-primary: #181A20;
|
| 740 |
-
--bg-secondary: #1E2329;
|
| 741 |
-
--bg-card: #2B3139;
|
| 742 |
-
--text-primary: #EAECEF;
|
| 743 |
-
--text-secondary: #848E9C;
|
| 744 |
-
--text-muted: #5E6673;
|
| 745 |
-
--accent: #F0B90B;
|
| 746 |
-
--accent-hover: #F8D12F;
|
| 747 |
-
--green: #0ECB81;
|
| 748 |
-
--red: #F6465D;
|
| 749 |
-
--border: #2B3139;
|
| 750 |
-
--border-light: #363C45;
|
| 751 |
-
}
|
| 752 |
-
|
| 753 |
-
.gradio-container {
|
| 754 |
-
max-width: 1200px !important;
|
| 755 |
-
margin: 0 auto !important;
|
| 756 |
-
padding: 0 !important;
|
| 757 |
-
background: var(--bg-primary) !important;
|
| 758 |
-
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif !important;
|
| 759 |
-
}
|
| 760 |
-
.contain, .block { background: var(--bg-primary) !important; }
|
| 761 |
-
footer { display: none !important; }
|
| 762 |
-
|
| 763 |
-
button {
|
| 764 |
-
font-family: 'Inter', sans-serif !important;
|
| 765 |
-
font-weight: 700 !important;
|
| 766 |
-
border-radius: 6px !important;
|
| 767 |
-
transition: all .2s ease !important;
|
| 768 |
-
text-transform: uppercase !important;
|
| 769 |
-
letter-spacing: .5px !important;
|
| 770 |
-
font-size: 13px !important;
|
| 771 |
-
cursor: pointer !important;
|
| 772 |
-
}
|
| 773 |
-
button.primary {
|
| 774 |
-
background: var(--accent) !important;
|
| 775 |
-
color: var(--bg-primary) !important;
|
| 776 |
-
border: none !important;
|
| 777 |
-
box-shadow: 0 2px 8px rgba(240,185,11,.25) !important;
|
| 778 |
-
}
|
| 779 |
-
button.primary:hover {
|
| 780 |
-
background: var(--accent-hover) !important;
|
| 781 |
-
box-shadow: 0 4px 16px rgba(240,185,11,.4) !important;
|
| 782 |
-
transform: translateY(-1px) !important;
|
| 783 |
-
}
|
| 784 |
-
button.secondary {
|
| 785 |
-
background: var(--bg-card) !important;
|
| 786 |
-
color: var(--text-primary) !important;
|
| 787 |
-
border: 1px solid var(--border-light) !important;
|
| 788 |
-
}
|
| 789 |
-
button.secondary:hover {
|
| 790 |
-
background: var(--border-light) !important;
|
| 791 |
-
border-color: var(--accent) !important;
|
| 792 |
-
}
|
| 793 |
-
|
| 794 |
-
input, textarea, select {
|
| 795 |
-
background: var(--bg-card) !important;
|
| 796 |
-
color: var(--text-primary) !important;
|
| 797 |
-
border: 1px solid var(--border-light) !important;
|
| 798 |
-
border-radius: 6px !important;
|
| 799 |
-
}
|
| 800 |
-
|
| 801 |
-
::-webkit-scrollbar { width: 5px; }
|
| 802 |
-
::-webkit-scrollbar-track { background: var(--bg-primary); }
|
| 803 |
-
::-webkit-scrollbar-thumb { background: var(--border-light); border-radius: 3px; }
|
| 804 |
-
::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
|
| 805 |
-
|
| 806 |
-
.gap { gap: 12px !important; }
|
| 807 |
-
"""
|
| 808 |
|
| 809 |
# ============================================================
|
| 810 |
-
#
|
| 811 |
# ============================================================
|
| 812 |
|
| 813 |
-
|
| 814 |
-
|
| 815 |
-
|
| 816 |
-
|
| 817 |
-
|
| 818 |
-
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
neutral_hue="gray",
|
| 822 |
-
font=gr.themes.GoogleFont("Inter"),
|
| 823 |
-
),
|
| 824 |
-
) as demo:
|
| 825 |
-
|
| 826 |
-
gr.HTML("""
|
| 827 |
-
<div style="display:flex;align-items:center;justify-content:space-between;
|
| 828 |
-
padding:10px 24px;background:#181A20;border-bottom:2px solid #F0B90B;
|
| 829 |
-
font-family:'Inter',sans-serif;">
|
| 830 |
-
<div style="display:flex;align-items:center;gap:12px;">
|
| 831 |
-
<div style="background:#F0B90B;color:#181A20;font-weight:900;font-size:20px;
|
| 832 |
-
padding:4px 11px;border-radius:4px;">โฟ</div>
|
| 833 |
-
<div>
|
| 834 |
-
<div style="color:#EAECEF;font-size:15px;font-weight:700;">CryptoML Terminal</div>
|
| 835 |
-
<div style="color:#5E6673;font-size:10px;">Machine Learning ยท Trading Signals</div>
|
| 836 |
-
</div>
|
| 837 |
-
</div>
|
| 838 |
-
<div style="display:flex;gap:18px;align-items:center;">
|
| 839 |
-
<span style="color:#848E9C;font-size:11px;">๐ ETH/USDT</span>
|
| 840 |
-
<span style="color:#848E9C;font-size:11px;">โฑ 4H</span>
|
| 841 |
-
<span style="color:#848E9C;font-size:11px;">๐ค RandomForest</span>
|
| 842 |
-
<div style="width:7px;height:7px;background:#0ECB81;border-radius:50%;"></div>
|
| 843 |
-
<span style="color:#0ECB81;font-size:10px;font-weight:600;">LIVE</span>
|
| 844 |
-
</div>
|
| 845 |
-
</div>""")
|
| 846 |
-
|
| 847 |
-
price_header = gr.HTML(
|
| 848 |
-
value=_html_price_header({"price": 0, "change_pct": 0, "high_24h": 0, "low_24h": 0, "volume": 0, "is_real": False})
|
| 849 |
)
|
| 850 |
|
| 851 |
-
gr.
|
| 852 |
-
|
| 853 |
-
|
| 854 |
-
|
| 855 |
-
|
| 856 |
-
|
| 857 |
-
|
| 858 |
-
|
| 859 |
-
|
| 860 |
-
</div>""")
|
| 861 |
-
|
| 862 |
-
train_btn = gr.Button("โก Train Model", variant="primary", size="lg")
|
| 863 |
-
|
| 864 |
-
with gr.Row():
|
| 865 |
-
predict_btn = gr.Button("๐ฎ Predict Signal", variant="secondary", size="lg")
|
| 866 |
-
refresh_btn = gr.Button("๐ Refresh", variant="secondary", size="sm")
|
| 867 |
-
|
| 868 |
-
gr.HTML("""
|
| 869 |
-
<div style="margin-top:10px;padding:12px 14px;background:#2B3139;border-radius:6px;
|
| 870 |
-
border:1px solid #363C45;font-family:'Inter',sans-serif;">
|
| 871 |
-
<div style="color:#5E6673;font-size:9px;text-transform:uppercase;margin-bottom:8px;
|
| 872 |
-
letter-spacing:.5px;">Strategy Parameters</div>
|
| 873 |
-
<div style="display:flex;justify-content:space-between;margin-bottom:3px;">
|
| 874 |
-
<span style="color:#848E9C;font-size:10px;">Timeframe</span>
|
| 875 |
-
<span style="color:#EAECEF;font-size:10px;font-weight:500;">4 Hours</span></div>
|
| 876 |
-
<div style="display:flex;justify-content:space-between;margin-bottom:3px;">
|
| 877 |
-
<span style="color:#848E9C;font-size:10px;">Fee Rate</span>
|
| 878 |
-
<span style="color:#EAECEF;font-size:10px;font-weight:500;">0.15%</span></div>
|
| 879 |
-
<div style="display:flex;justify-content:space-between;margin-bottom:3px;">
|
| 880 |
-
<span style="color:#848E9C;font-size:10px;">Lookback Window</span>
|
| 881 |
-
<span style="color:#EAECEF;font-size:10px;font-weight:500;">20 bars</span></div>
|
| 882 |
-
<div style="display:flex;justify-content:space-between;margin-bottom:3px;">
|
| 883 |
-
<span style="color:#848E9C;font-size:10px;">Algorithm</span>
|
| 884 |
-
<span style="color:#EAECEF;font-size:10px;font-weight:500;">RF-100</span></div>
|
| 885 |
-
<div style="display:flex;justify-content:space-between;">
|
| 886 |
-
<span style="color:#848E9C;font-size:10px;">Indicators</span>
|
| 887 |
-
<span style="color:#EAECEF;font-size:10px;font-weight:500;">RSIยทBBยทMACDยทEMA</span></div>
|
| 888 |
-
</div>""")
|
| 889 |
-
|
| 890 |
-
with gr.Column(scale=3):
|
| 891 |
-
signal_output = gr.HTML(
|
| 892 |
-
value=_html_warn("Klik <b>๐ฎ Predict Signal</b> untuk mendapatkan sinyal.")
|
| 893 |
-
)
|
| 894 |
|
| 895 |
-
|
| 896 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 897 |
)
|
| 898 |
|
| 899 |
-
|
| 900 |
-
|
| 901 |
-
|
| 902 |
-
|
| 903 |
-
<span style="color:#5E6673;font-size:9px;">
|
| 904 |
-
โ ๏ธ Disclaimer: Hanya untuk edukasi. Bukan saran investasi. Selalu DYOR.</span>
|
| 905 |
-
<span style="color:#5E6673;font-size:9px;">
|
| 906 |
-
Data: Yahoo Finance ยท Chart: TradingView ยท ML: scikit-learn</span>
|
| 907 |
-
</div>""")
|
| 908 |
|
| 909 |
-
|
| 910 |
-
|
| 911 |
-
|
|
|
|
| 912 |
|
| 913 |
demo.queue(default_concurrency_limit=1)
|
| 914 |
-
logger.info("โ
App ready. Launching...")
|
| 915 |
|
| 916 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
```python
|
| 2 |
import sys
|
| 3 |
import os
|
| 4 |
import logging
|
|
|
|
| 8 |
from datetime import datetime, timedelta
|
| 9 |
|
| 10 |
logging.basicConfig(
|
| 11 |
+
stream=sys.stdout,
|
| 12 |
+
level=logging.INFO,
|
| 13 |
format="%(asctime)s [%(levelname)s] %(message)s"
|
| 14 |
)
|
| 15 |
+
|
| 16 |
logger = logging.getLogger("crypto_ml")
|
| 17 |
logger.info("๐ Starting HF Space container...")
|
| 18 |
|
| 19 |
+
# ============================================================
|
| 20 |
+
# IMPORTS
|
| 21 |
+
# ============================================================
|
| 22 |
+
|
| 23 |
try:
|
| 24 |
import gradio as gr
|
| 25 |
import yfinance as yf
|
| 26 |
import pandas as pd
|
| 27 |
import numpy as np
|
| 28 |
+
|
| 29 |
from sklearn.ensemble import RandomForestClassifier
|
| 30 |
from sklearn.preprocessing import MinMaxScaler
|
| 31 |
+
|
| 32 |
import joblib
|
| 33 |
+
|
| 34 |
logger.info("โ
All dependencies imported successfully")
|
| 35 |
+
|
| 36 |
except Exception as e:
|
| 37 |
logger.error(f"โ Import failed: {e}")
|
| 38 |
sys.exit(1)
|
| 39 |
|
| 40 |
+
# ============================================================
|
| 41 |
+
# CONFIG
|
| 42 |
+
# ============================================================
|
| 43 |
+
|
| 44 |
SYMBOL = "ETH-USD"
|
| 45 |
FEE = 0.0015
|
| 46 |
WINDOW = 20
|
| 47 |
+
|
| 48 |
MODEL_PATH = "/tmp/rf_model.pkl"
|
| 49 |
SCALER_PATH = "/tmp/scaler.pkl"
|
| 50 |
|
|
|
|
| 51 |
# ============================================================
|
| 52 |
+
# CLEAN COLUMNS
|
| 53 |
# ============================================================
|
| 54 |
|
| 55 |
def _clean_columns(df):
|
| 56 |
+
|
| 57 |
if df is None or df.empty:
|
| 58 |
return df
|
| 59 |
|
|
|
|
| 60 |
if isinstance(df.columns, pd.MultiIndex):
|
|
|
|
|
|
|
| 61 |
try:
|
| 62 |
+
df.columns = df.columns.get_level_values(0)
|
| 63 |
+
except:
|
| 64 |
+
df.columns = [
|
| 65 |
+
c[0] if isinstance(c, tuple) else c
|
| 66 |
+
for c in df.columns
|
| 67 |
+
]
|
| 68 |
+
|
| 69 |
+
df.columns = [str(c).strip() for c in df.columns]
|
| 70 |
+
|
| 71 |
+
rename_map = {
|
| 72 |
+
"open": "Open",
|
| 73 |
+
"high": "High",
|
| 74 |
+
"low": "Low",
|
| 75 |
+
"close": "Close",
|
| 76 |
+
"adj close": "Close",
|
| 77 |
+
"volume": "Volume"
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
df.columns = [
|
| 81 |
+
rename_map.get(c.lower(), c)
|
| 82 |
+
for c in df.columns
|
| 83 |
+
]
|
| 84 |
+
|
| 85 |
df = df.loc[:, ~df.columns.duplicated()]
|
| 86 |
|
| 87 |
+
for col in ["Open", "High", "Low", "Close", "Volume"]:
|
| 88 |
+
if col in df.columns:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
df[col] = pd.to_numeric(df[col], errors="coerce")
|
| 90 |
|
| 91 |
+
df = df.dropna()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
|
| 93 |
+
logger.info(f"โ
Cleaned columns: {df.columns.tolist()}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
|
| 95 |
+
return df
|
| 96 |
|
| 97 |
+
# ============================================================
|
| 98 |
+
# SYNTHETIC DATA
|
| 99 |
+
# ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
|
| 101 |
+
def generate_synthetic_data(n_bars=500):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
|
| 103 |
+
logger.warning("โ ๏ธ Using synthetic data")
|
| 104 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
np.random.seed(42)
|
| 106 |
|
| 107 |
end = datetime.utcnow()
|
| 108 |
+
dates = pd.date_range(
|
| 109 |
+
end=end,
|
| 110 |
+
periods=n_bars,
|
| 111 |
+
freq="4h"
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
price = 3000
|
| 115 |
|
| 116 |
+
opens = []
|
| 117 |
+
highs = []
|
| 118 |
+
lows = []
|
| 119 |
+
closes = []
|
| 120 |
+
volumes = []
|
| 121 |
|
| 122 |
for _ in range(n_bars):
|
| 123 |
+
|
| 124 |
ret = np.random.normal(0.0005, 0.015)
|
| 125 |
+
|
| 126 |
open_p = price
|
| 127 |
close_p = price * (1 + ret)
|
| 128 |
+
|
| 129 |
+
high_p = max(open_p, close_p) * (
|
| 130 |
+
1 + abs(np.random.normal(0, 0.005))
|
| 131 |
+
)
|
| 132 |
+
|
| 133 |
+
low_p = min(open_p, close_p) * (
|
| 134 |
+
1 - abs(np.random.normal(0, 0.005))
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
vol = np.random.lognormal(15, 1)
|
| 138 |
|
| 139 |
opens.append(open_p)
|
| 140 |
highs.append(high_p)
|
| 141 |
lows.append(low_p)
|
| 142 |
closes.append(close_p)
|
| 143 |
volumes.append(vol)
|
| 144 |
+
|
| 145 |
price = close_p
|
| 146 |
|
| 147 |
df = pd.DataFrame({
|
| 148 |
+
"Open": opens,
|
| 149 |
+
"High": highs,
|
| 150 |
+
"Low": lows,
|
| 151 |
+
"Close": closes,
|
| 152 |
+
"Volume": volumes
|
| 153 |
+
}, index=dates)
|
| 154 |
|
|
|
|
|
|
|
| 155 |
return df
|
| 156 |
|
| 157 |
+
# ============================================================
|
| 158 |
+
# DOWNLOAD DATA
|
| 159 |
+
# ============================================================
|
| 160 |
+
|
| 161 |
+
def safe_download(
|
| 162 |
+
symbol=SYMBOL,
|
| 163 |
+
period="90d",
|
| 164 |
+
interval="1h"
|
| 165 |
+
):
|
| 166 |
|
|
|
|
|
|
|
|
|
|
| 167 |
methods = [
|
| 168 |
+
("Ticker.history", lambda:
|
| 169 |
+
yf.Ticker(symbol).history(
|
| 170 |
+
period=period,
|
| 171 |
+
interval=interval,
|
| 172 |
+
auto_adjust=True
|
| 173 |
+
)
|
| 174 |
+
),
|
| 175 |
+
|
| 176 |
+
("yf.download", lambda:
|
| 177 |
+
yf.download(
|
| 178 |
+
tickers=symbol,
|
| 179 |
+
period=period,
|
| 180 |
+
interval=interval,
|
| 181 |
+
progress=False,
|
| 182 |
+
auto_adjust=True,
|
| 183 |
+
threads=False,
|
| 184 |
+
group_by="column"
|
| 185 |
+
)
|
| 186 |
+
)
|
| 187 |
]
|
| 188 |
|
| 189 |
+
for name, method in methods:
|
| 190 |
+
|
| 191 |
try:
|
| 192 |
+
|
| 193 |
+
logger.info(f"๐ฅ Trying {name}")
|
| 194 |
+
|
| 195 |
df = method()
|
| 196 |
+
|
| 197 |
+
if df is not None and not df.empty:
|
| 198 |
+
|
| 199 |
df = _clean_columns(df)
|
| 200 |
+
|
| 201 |
+
if len(df) > 30:
|
| 202 |
+
logger.info(f"โ
{name} success")
|
| 203 |
+
return df, True
|
| 204 |
+
|
| 205 |
except Exception as e:
|
| 206 |
+
logger.warning(f"โ ๏ธ {name} failed: {e}")
|
|
|
|
| 207 |
|
| 208 |
+
time.sleep(1)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
|
| 210 |
+
return generate_synthetic_data(), False
|
| 211 |
|
| 212 |
# ============================================================
|
| 213 |
+
# FEATURE ENGINEERING
|
| 214 |
# ============================================================
|
| 215 |
|
| 216 |
def fetch_and_prepare():
|
| 217 |
+
|
| 218 |
df, is_real = safe_download()
|
| 219 |
|
| 220 |
if df.empty:
|
| 221 |
+
raise ValueError("Data kosong")
|
| 222 |
|
| 223 |
+
if not isinstance(df.index, pd.DatetimeIndex):
|
| 224 |
+
df.index = pd.to_datetime(df.index)
|
| 225 |
|
| 226 |
+
try:
|
| 227 |
+
|
| 228 |
+
df = df.resample("4h").agg({
|
| 229 |
+
"Open": "first",
|
| 230 |
+
"High": "max",
|
| 231 |
+
"Low": "min",
|
| 232 |
+
"Close": "last",
|
| 233 |
+
"Volume": "sum"
|
| 234 |
+
}).dropna()
|
|
|
|
| 235 |
|
| 236 |
+
except Exception as e:
|
| 237 |
+
logger.warning(f"Resample failed: {e}")
|
| 238 |
+
|
| 239 |
+
if len(df) < 50:
|
| 240 |
+
raise ValueError("Data terlalu sedikit")
|
| 241 |
|
| 242 |
df.columns = [c.lower() for c in df.columns]
|
|
|
|
|
|
|
| 243 |
|
| 244 |
+
# ========================================================
|
| 245 |
+
# RSI
|
| 246 |
+
# ========================================================
|
| 247 |
+
|
| 248 |
delta = df["close"].diff()
|
| 249 |
+
|
| 250 |
+
gain = delta.where(delta > 0, 0).rolling(14).mean()
|
| 251 |
+
loss = (-delta.where(delta < 0, 0)).rolling(14).mean()
|
| 252 |
+
|
| 253 |
rs = gain / loss.replace(0, np.nan)
|
| 254 |
+
|
| 255 |
+
df["rsi"] = (
|
| 256 |
+
100 - (100 / (1 + rs))
|
| 257 |
+
).fillna(50)
|
| 258 |
+
|
| 259 |
+
# ========================================================
|
| 260 |
+
# BOLLINGER
|
| 261 |
+
# ========================================================
|
| 262 |
+
|
| 263 |
+
sma20 = df["close"].rolling(20).mean()
|
| 264 |
+
std20 = df["close"].rolling(20).std()
|
| 265 |
+
|
| 266 |
+
df["bb_upper"] = sma20 + 2 * std20
|
| 267 |
+
df["bb_lower"] = sma20 - 2 * std20
|
| 268 |
+
|
| 269 |
+
bb_width = (
|
| 270 |
+
df["bb_upper"] - df["bb_lower"]
|
| 271 |
+
).replace(0, np.nan)
|
| 272 |
+
|
| 273 |
+
df["bb_pct"] = (
|
| 274 |
+
(df["close"] - df["bb_lower"]) / bb_width
|
| 275 |
+
).fillna(0.5)
|
| 276 |
+
|
| 277 |
+
# ========================================================
|
| 278 |
+
# MACD
|
| 279 |
+
# ========================================================
|
| 280 |
+
|
| 281 |
+
ema12 = df["close"].ewm(span=12).mean()
|
| 282 |
+
ema26 = df["close"].ewm(span=26).mean()
|
| 283 |
+
|
| 284 |
df["macd"] = ema12 - ema26
|
| 285 |
+
df["macd_signal"] = df["macd"].ewm(span=9).mean()
|
| 286 |
+
df["macd_hist"] = (
|
| 287 |
+
df["macd"] - df["macd_signal"]
|
| 288 |
+
)
|
| 289 |
+
|
| 290 |
+
# ========================================================
|
| 291 |
+
# EMA
|
| 292 |
+
# ========================================================
|
| 293 |
|
| 294 |
+
df["ema_9"] = df["close"].ewm(span=9).mean()
|
| 295 |
+
df["ema_21"] = df["close"].ewm(span=21).mean()
|
|
|
|
| 296 |
|
| 297 |
+
# ========================================================
|
| 298 |
+
# RETURNS
|
| 299 |
+
# ========================================================
|
| 300 |
+
|
| 301 |
+
df["vol_change"] = (
|
| 302 |
+
df["volume"].pct_change()
|
| 303 |
+
)
|
| 304 |
+
|
| 305 |
+
df["return_1"] = (
|
| 306 |
+
df["close"].pct_change(1)
|
| 307 |
+
)
|
| 308 |
+
|
| 309 |
+
df["return_4"] = (
|
| 310 |
+
df["close"].pct_change(4)
|
| 311 |
+
)
|
| 312 |
+
|
| 313 |
+
# ========================================================
|
| 314 |
+
# LABEL
|
| 315 |
+
# ========================================================
|
| 316 |
|
|
|
|
| 317 |
df["next_close"] = df["close"].shift(-1)
|
|
|
|
| 318 |
|
| 319 |
+
df["label"] = (
|
| 320 |
+
df["next_close"] >
|
| 321 |
+
df["close"] * (1 + FEE)
|
| 322 |
+
).astype(int)
|
| 323 |
|
| 324 |
+
# ========================================================
|
| 325 |
+
# CLEANUP
|
| 326 |
+
# ========================================================
|
| 327 |
+
|
| 328 |
+
df = df.replace([np.inf, -np.inf], np.nan)
|
| 329 |
+
df = df.dropna()
|
| 330 |
|
| 331 |
feat_cols = [
|
| 332 |
+
"open",
|
| 333 |
+
"high",
|
| 334 |
+
"low",
|
| 335 |
+
"close",
|
| 336 |
+
"volume",
|
| 337 |
+
"rsi",
|
| 338 |
+
"bb_upper",
|
| 339 |
+
"bb_lower",
|
| 340 |
+
"bb_pct",
|
| 341 |
+
"macd",
|
| 342 |
+
"macd_signal",
|
| 343 |
+
"macd_hist",
|
| 344 |
+
"ema_9",
|
| 345 |
+
"ema_21",
|
| 346 |
+
"vol_change",
|
| 347 |
+
"return_1",
|
| 348 |
+
"return_4"
|
| 349 |
]
|
| 350 |
|
| 351 |
+
X_list = []
|
| 352 |
+
y_list = []
|
|
|
|
| 353 |
|
|
|
|
| 354 |
for i in range(WINDOW, len(df)):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 355 |
|
| 356 |
+
window_data = df.iloc[
|
| 357 |
+
i-WINDOW:i
|
| 358 |
+
][feat_cols].values
|
| 359 |
|
| 360 |
+
if np.isnan(window_data).any():
|
| 361 |
+
continue
|
|
|
|
| 362 |
|
| 363 |
+
X_list.append(window_data.flatten())
|
| 364 |
+
y_list.append(df.iloc[i]["label"])
|
|
|
|
|
|
|
| 365 |
|
| 366 |
+
X = np.array(X_list, dtype=np.float32)
|
| 367 |
+
y = np.array(y_list)
|
| 368 |
|
| 369 |
+
X = np.nan_to_num(X)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 370 |
|
| 371 |
+
return X, y, df, feat_cols, is_real
|
| 372 |
|
| 373 |
+
# ============================================================
|
| 374 |
+
# TRAIN
|
| 375 |
+
# ============================================================
|
| 376 |
|
| 377 |
def run_train():
|
| 378 |
+
|
| 379 |
try:
|
|
|
|
|
|
|
| 380 |
|
| 381 |
+
X, y, df, feat_cols, is_real = fetch_and_prepare()
|
|
|
|
|
|
|
|
|
|
| 382 |
|
| 383 |
scaler = MinMaxScaler()
|
| 384 |
+
|
| 385 |
X_scaled = scaler.fit_transform(X)
|
| 386 |
|
| 387 |
+
split = int(len(X_scaled) * 0.88)
|
| 388 |
+
|
| 389 |
+
X_train = X_scaled[:split]
|
| 390 |
+
X_test = X_scaled[split:]
|
| 391 |
|
| 392 |
+
y_train = y[:split]
|
| 393 |
+
y_test = y[split:]
|
| 394 |
|
| 395 |
model = RandomForestClassifier(
|
| 396 |
+
n_estimators=100,
|
| 397 |
+
max_depth=8,
|
| 398 |
+
random_state=42,
|
| 399 |
+
class_weight="balanced",
|
| 400 |
+
n_jobs=1
|
| 401 |
)
|
| 402 |
+
|
| 403 |
model.fit(X_train, y_train)
|
| 404 |
|
| 405 |
+
acc = model.score(X_test, y_test)
|
| 406 |
+
|
| 407 |
+
# ====================================================
|
| 408 |
+
# FIX FEATURE IMPORTANCE
|
| 409 |
+
# ====================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 410 |
|
| 411 |
importances = model.feature_importances_
|
| 412 |
+
|
| 413 |
+
imp_matrix = importances.reshape(
|
| 414 |
+
WINDOW,
|
| 415 |
+
len(feat_cols)
|
| 416 |
+
)
|
| 417 |
+
|
| 418 |
+
mean_importance = imp_matrix.mean(axis=0)
|
| 419 |
+
|
| 420 |
+
top_idx = np.argsort(mean_importance)[-5:][::-1]
|
| 421 |
+
|
| 422 |
+
top_features = [
|
| 423 |
+
(
|
| 424 |
+
feat_cols[i],
|
| 425 |
+
float(mean_importance[i])
|
| 426 |
+
)
|
| 427 |
+
for i in top_idx
|
| 428 |
+
]
|
| 429 |
|
| 430 |
joblib.dump(model, MODEL_PATH)
|
| 431 |
joblib.dump(scaler, SCALER_PATH)
|
| 432 |
|
| 433 |
+
txt = f"""
|
| 434 |
+
โ
MODEL TRAINED
|
| 435 |
+
|
| 436 |
+
Accuracy: {acc:.4f}
|
| 437 |
+
|
| 438 |
+
Top Features:
|
| 439 |
+
|
| 440 |
+
"""
|
| 441 |
+
|
| 442 |
+
for name, score in top_features:
|
| 443 |
+
txt += f"\n{name}: {score:.4f}"
|
| 444 |
+
|
| 445 |
+
return txt
|
| 446 |
+
|
| 447 |
except Exception as e:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 448 |
|
| 449 |
+
logger.error(traceback.format_exc())
|
| 450 |
|
| 451 |
+
return f"โ ERROR:\n\n{e}"
|
| 452 |
+
|
| 453 |
+
# ============================================================
|
| 454 |
+
# PREDICT
|
| 455 |
+
# ============================================================
|
| 456 |
|
| 457 |
def run_predict():
|
| 458 |
+
|
| 459 |
try:
|
| 460 |
+
|
| 461 |
if not os.path.exists(MODEL_PATH):
|
| 462 |
+
return "โ ๏ธ Train model terlebih dahulu"
|
| 463 |
+
|
| 464 |
+
X, y, df, feat_cols, is_real = fetch_and_prepare()
|
| 465 |
|
|
|
|
| 466 |
scaler = joblib.load(SCALER_PATH)
|
| 467 |
model = joblib.load(MODEL_PATH)
|
| 468 |
|
| 469 |
+
latest = df.iloc[
|
| 470 |
+
-WINDOW:
|
| 471 |
+
][feat_cols].values.flatten()
|
| 472 |
+
|
| 473 |
+
latest = latest.reshape(1, -1)
|
| 474 |
+
|
| 475 |
+
latest = np.nan_to_num(latest)
|
| 476 |
|
|
|
|
|
|
|
| 477 |
latest_scaled = scaler.transform(latest)
|
| 478 |
|
| 479 |
+
pred = model.predict(latest_scaled)[0]
|
|
|
|
|
|
|
| 480 |
|
| 481 |
+
proba = model.predict_proba(
|
| 482 |
+
latest_scaled
|
| 483 |
+
)[0]
|
| 484 |
+
|
| 485 |
+
confidence = float(np.max(proba)) * 100
|
| 486 |
|
| 487 |
signal = "BUY" if pred == 1 else "SELL"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 488 |
|
| 489 |
+
price = float(df.iloc[-1]["close"])
|
| 490 |
|
| 491 |
+
return f"""
|
| 492 |
+
๐ฎ SIGNAL: {signal}
|
| 493 |
|
| 494 |
+
Confidence: {confidence:.2f}%
|
| 495 |
+
|
| 496 |
+
Price: ${price:,.2f}
|
| 497 |
+
|
| 498 |
+
RSI: {df.iloc[-1]['rsi']:.2f}
|
| 499 |
+
MACD: {df.iloc[-1]['macd']:.2f}
|
| 500 |
+
"""
|
| 501 |
+
|
| 502 |
+
except Exception as e:
|
| 503 |
+
|
| 504 |
+
logger.error(traceback.format_exc())
|
| 505 |
+
|
| 506 |
+
return f"โ ERROR:\n\n{e}"
|
| 507 |
|
| 508 |
# ============================================================
|
| 509 |
+
# MARKET INFO
|
| 510 |
# ============================================================
|
| 511 |
|
| 512 |
+
def get_market_info():
|
|
|
|
|
|
|
|
|
|
|
|
|
| 513 |
|
| 514 |
+
try:
|
| 515 |
|
| 516 |
+
df, is_real = safe_download(
|
| 517 |
+
SYMBOL,
|
| 518 |
+
period="5d",
|
| 519 |
+
interval="1h"
|
| 520 |
+
)
|
|
|
|
| 521 |
|
| 522 |
+
price = float(df["Close"].iloc[-1])
|
| 523 |
|
|
|
|
| 524 |
return f"""
|
| 525 |
+
ETH PRICE
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 526 |
|
| 527 |
+
${price:,.2f}
|
| 528 |
|
| 529 |
+
Source:
|
| 530 |
+
{"LIVE DATA" if is_real else "SIMULATED"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 531 |
"""
|
| 532 |
|
| 533 |
+
except Exception as e:
|
| 534 |
+
return str(e)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 535 |
|
| 536 |
# ============================================================
|
| 537 |
+
# UI
|
| 538 |
# ============================================================
|
| 539 |
|
| 540 |
+
with gr.Blocks(title="CryptoML") as demo:
|
| 541 |
+
|
| 542 |
+
gr.Markdown("# ๐ CryptoML ETH/USDT")
|
| 543 |
+
|
| 544 |
+
market_box = gr.Textbox(
|
| 545 |
+
label="Market",
|
| 546 |
+
value=get_market_info(),
|
| 547 |
+
lines=5
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 548 |
)
|
| 549 |
|
| 550 |
+
with gr.Row():
|
| 551 |
+
|
| 552 |
+
train_btn = gr.Button(
|
| 553 |
+
"โก Train Model"
|
| 554 |
+
)
|
| 555 |
+
|
| 556 |
+
predict_btn = gr.Button(
|
| 557 |
+
"๐ฎ Predict"
|
| 558 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 559 |
|
| 560 |
+
refresh_btn = gr.Button(
|
| 561 |
+
"๐ Refresh"
|
| 562 |
+
)
|
| 563 |
+
|
| 564 |
+
train_output = gr.Textbox(
|
| 565 |
+
label="Training Result",
|
| 566 |
+
lines=15
|
| 567 |
+
)
|
| 568 |
+
|
| 569 |
+
predict_output = gr.Textbox(
|
| 570 |
+
label="Prediction",
|
| 571 |
+
lines=10
|
| 572 |
+
)
|
| 573 |
+
|
| 574 |
+
train_btn.click(
|
| 575 |
+
fn=run_train,
|
| 576 |
+
outputs=train_output
|
| 577 |
)
|
| 578 |
|
| 579 |
+
predict_btn.click(
|
| 580 |
+
fn=run_predict,
|
| 581 |
+
outputs=predict_output
|
| 582 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 583 |
|
| 584 |
+
refresh_btn.click(
|
| 585 |
+
fn=get_market_info,
|
| 586 |
+
outputs=market_box
|
| 587 |
+
)
|
| 588 |
|
| 589 |
demo.queue(default_concurrency_limit=1)
|
|
|
|
| 590 |
|
| 591 |
+
logger.info("โ
App ready")
|
| 592 |
+
|
| 593 |
+
demo.launch(
|
| 594 |
+
server_name="0.0.0.0",
|
| 595 |
+
server_port=7860
|
| 596 |
+
)
|
| 597 |
+
```
|