daafa999 commited on
Commit
254cbf6
ยท
verified ยท
1 Parent(s): 092edaf

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +412 -731
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, level=logging.INFO,
 
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
- import requests
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
- # ROBUST DATA FETCHING โ€” Multiple methods
39
  # ============================================================
40
 
41
  def _clean_columns(df):
42
- """Universal column cleaner โ€” handles any yfinance format."""
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.droplevel(0)
52
- logger.info(f"๐Ÿ”ง After droplevel(0): {df.columns.tolist()}")
53
- except Exception:
54
- # Fallback: take last level
55
- try:
56
- df.columns = df.columns.get_level_values(-1)
57
- logger.info(f"๐Ÿ”ง After get_level_values(-1): {df.columns.tolist()}")
58
- except Exception:
59
- logger.error("โŒ Cannot fix MultiIndex columns")
60
-
61
- # Step 2: Remove duplicates
 
 
 
 
 
 
 
 
 
 
 
 
62
  df = df.loc[:, ~df.columns.duplicated()]
63
 
64
- # Step 3: Standardize names
65
- new_cols = []
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
- # Step 5: Drop NaN in OHLCV
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"๐Ÿ”ง Cleaned columns: {df.columns.tolist()}, shape: {df.shape}")
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
- def fetch_method_download(symbol, period="90d", interval="1h"):
108
- """Method 2: yf.download() with session."""
109
- try:
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 fetch_method_shorter_period(symbol):
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
- start = end - timedelta(hours=n_bars * 4)
176
- dates = pd.date_range(start=start, end=end, freq="4h")[:n_bars]
 
 
 
 
 
177
 
178
- # Simulate realistic ETH price movement
179
- price = 3000.0
180
- opens, highs, lows, closes, volumes = [], [], [], [], []
 
 
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
- high_p = max(open_p, close_p) * (1 + abs(np.random.normal(0, 0.005)))
187
- low_p = min(open_p, close_p) * (1 - abs(np.random.normal(0, 0.005)))
188
- vol = np.random.lognormal(15, 1.5)
 
 
 
 
 
 
 
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, "High": highs, "Low": lows,
199
- "Close": closes, "Volume": volumes
200
- }, index=dates[:len(opens)])
 
 
 
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
- lambda: fetch_method_ticker_history(symbol, period, interval),
212
- lambda: fetch_method_download(symbol, period, interval),
213
- lambda: fetch_method_alt_symbol(period, interval),
214
- lambda: fetch_method_shorter_period(symbol),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
  ]
216
 
217
- for i, method in enumerate(methods):
 
218
  try:
 
 
 
219
  df = method()
220
- if df is not None and not df.empty and len(df) > 20:
 
 
221
  df = _clean_columns(df)
222
- if "Close" in df.columns and len(df) > 20:
223
- return df, True # True = real data
 
 
 
224
  except Exception as e:
225
- logger.warning(f"โš ๏ธ Method {i+1} exception: {e}")
226
- time.sleep(1)
227
 
228
- # All real methods failed โ€” use synthetic
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
- # DATA PREPARATION
237
  # ============================================================
238
 
239
  def fetch_and_prepare():
240
- """Fetch OHLCV data, compute indicators, build windowed features."""
241
  df, is_real = safe_download()
242
 
243
  if df.empty:
244
- raise ValueError("Data kosong setelah semua percobaan. Coba lagi nanti.")
245
 
246
- data_source = "๐Ÿ”ด LIVE DATA" if is_real else "๐ŸŸก SYNTHETIC DATA (Yahoo Finance tidak tersedia)"
247
- logger.info(f"๐Ÿ“Š Data source: {data_source}")
248
 
249
- # Resample to 4h if data has enough rows and isn't already 4h
250
- if len(df) > 50:
251
- try:
252
- df = df.resample("4h").agg({
253
- "Open": "first", "High": "max", "Low": "min",
254
- "Close": "last", "Volume": "sum"
255
- }).dropna()
256
- logger.info(f"๐Ÿ“Š After 4h resample: {len(df)} rows")
257
- except Exception as e:
258
- logger.warning(f"โš ๏ธ Resample failed: {e}, using raw data")
259
 
260
- if len(df) < 30:
261
- raise ValueError(f"Data terlalu sedikit setelah resample: {len(df)} baris.")
 
 
 
262
 
263
  df.columns = [c.lower() for c in df.columns]
264
- df.index.name = "timestamp"
265
- df = df.reset_index()
266
 
267
- # โ”€โ”€ RSI 14 โ”€โ”€
 
 
 
268
  delta = df["close"].diff()
269
- gain = delta.where(delta > 0, 0.0).rolling(14, min_periods=1).mean()
270
- loss = (-delta.where(delta < 0, 0.0)).rolling(14, min_periods=1).mean()
 
 
271
  rs = gain / loss.replace(0, np.nan)
272
- df["rsi"] = (100 - (100 / (1 + rs))).fillna(50)
273
-
274
- # โ”€โ”€ Bollinger Bands โ”€โ”€
275
- sma20 = df["close"].rolling(20, min_periods=1).mean()
276
- std20 = df["close"].rolling(20, min_periods=1).std().fillna(0)
277
- df["bb_upper"] = sma20 + std20 * 2
278
- df["bb_lower"] = sma20 - std20 * 2
279
- bb_width = (df["bb_upper"] - df["bb_lower"]).replace(0, np.nan)
280
- df["bb_pct"] = ((df["close"] - df["bb_lower"]) / bb_width).fillna(0.5).clip(0, 1)
281
-
282
- # โ”€โ”€ MACD โ”€โ”€
283
- ema12 = df["close"].ewm(span=12, adjust=False).mean()
284
- ema26 = df["close"].ewm(span=26, adjust=False).mean()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
285
  df["macd"] = ema12 - ema26
286
- df["macd_signal"] = df["macd"].ewm(span=9, adjust=False).mean()
287
- df["macd_hist"] = df["macd"] - df["macd_signal"]
 
 
 
 
 
 
288
 
289
- # โ”€โ”€ EMAs โ”€โ”€
290
- df["ema_9"] = df["close"].ewm(span=9, adjust=False).mean()
291
- df["ema_21"] = df["close"].ewm(span=21, adjust=False).mean()
292
 
293
- # โ”€โ”€ Momentum / Volume โ”€โ”€
294
- df["vol_change"] = df["volume"].pct_change().fillna(0).replace([np.inf, -np.inf], 0)
295
- df["return_1"] = df["close"].pct_change(1).fillna(0).replace([np.inf, -np.inf], 0)
296
- df["return_4"] = df["close"].pct_change(4).fillna(0).replace([np.inf, -np.inf], 0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- # Final cleanup
303
- df = df.replace([np.inf, -np.inf], np.nan)
304
- df = df.dropna().reset_index(drop=True)
 
305
 
306
- if len(df) < WINDOW + 5:
307
- raise ValueError(
308
- f"Data tidak cukup setelah preprocessing: {len(df)} baris. "
309
- f"Butuh minimal {WINDOW + 5}."
310
- )
 
311
 
312
  feat_cols = [
313
- "open", "high", "low", "close", "volume",
314
- "rsi", "bb_upper", "bb_lower", "bb_pct",
315
- "macd", "macd_signal", "macd_hist",
316
- "ema_9", "ema_21", "vol_change", "return_1", "return_4"
 
 
 
 
 
 
 
 
 
 
 
 
 
317
  ]
318
 
319
- missing_feats = [f for f in feat_cols if f not in df.columns]
320
- if missing_feats:
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
- if len(X_list) < 10:
331
- raise ValueError(f"Tidak cukup sampel valid: {len(X_list)}.")
 
332
 
333
- X = np.array(X_list, dtype=np.float64)
334
- y = np.array(y_list, dtype=np.int32)
335
- X = np.nan_to_num(X, nan=0.0, posinf=0.0, neginf=0.0)
336
 
337
- n_buy = int(np.sum(y == 1))
338
- n_sell = int(np.sum(y == 0))
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
- def get_market_info():
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
- # โ”€โ”€ Training โ”€โ”€
 
 
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
- n_buy = int(np.sum(y == 1))
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 = max(int(len(X) * 0.88), 10)
382
- if split >= len(X) - 3:
383
- split = len(X) - 3
 
384
 
385
- X_train, X_test = X_scaled[:split], X_scaled[split:]
386
- y_train, y_test = y[:split], y[split:]
387
 
388
  model = RandomForestClassifier(
389
- n_estimators=100, max_depth=8,
390
- min_samples_split=5, min_samples_leaf=2,
391
- random_state=42, n_jobs=1,
392
- class_weight="balanced"
 
393
  )
 
394
  model.fit(X_train, y_train)
395
 
396
- acc = float(model.score(X_test, y_test))
397
- y_pred = model.predict(X_test)
398
- buy_sig = int(np.sum(y_pred == 1))
399
- sell_sig = int(np.sum(y_pred == 0))
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
- top_n = min(5, len(feat_cols))
421
- top5_idx = np.argsort(importances)[-top_n:][::-1]
422
- top5 = [(feat_cols[i], float(importances[i])) for i in top5_idx]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
423
 
424
  joblib.dump(model, MODEL_PATH)
425
  joblib.dump(scaler, SCALER_PATH)
426
 
427
- logger.info(f"โœ… Training done โ€” acc={acc:.4f}")
428
- return _html_train(acc, win_rate, pnl_pct, len(X_train),
429
- len(X_test), buy_sig, sell_sig, top5, df_full, is_real)
 
 
 
 
 
 
 
 
 
 
 
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
- # โ”€โ”€ Prediction โ”€โ”€
 
 
 
 
438
 
439
  def run_predict():
 
440
  try:
 
441
  if not os.path.exists(MODEL_PATH):
442
- return _html_warn("Model belum di-training. Klik <b>โšก Train Model</b> terlebih dahulu.")
 
 
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
- if len(df_full) < WINDOW:
449
- return _html_warn(f"Data tidak cukup: {len(df_full)} baris, butuh {WINDOW}.")
 
 
 
 
 
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 = int(model.predict(latest_scaled)[0])
456
- proba = model.predict_proba(latest_scaled)[0]
457
- confidence = float(max(proba)) * 100
458
 
459
- row = df_full.iloc[-1]
460
- price = float(row["close"])
461
- rsi = float(row.get("rsi", 50))
462
- macd_val = float(row.get("macd", 0))
463
- bb_pct = float(row.get("bb_pct", 0.5))
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
- def refresh_price():
476
- return _html_price_header(get_market_info())
477
 
 
 
 
 
 
 
 
 
 
 
 
 
 
478
 
479
  # ============================================================
480
- # HTML BUILDERS
481
  # ============================================================
482
 
483
- def _html_data_badge(is_real):
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
- def _html_price_header(info):
491
- p = info.get("price", 0)
492
- chg = info.get("change_pct", 0)
493
- hi, lo = info.get("high_24h", 0), info.get("low_24h", 0)
494
- vol = info.get("volume", 0)
495
- is_real = info.get("is_real", False)
496
 
497
- badge = _html_data_badge(is_real)
498
 
499
- if p == 0:
500
  return f"""
501
- <div style="display:flex;align-items:center;gap:28px;padding:14px 24px;
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
- # TRADINGVIEW & CSS
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
- BINANCE_CSS = """
736
- @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap');
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
- # GRADIO UI
811
  # ============================================================
812
 
813
- logger.info("๐ŸŽจ Building Gradio UI...")
814
-
815
- with gr.Blocks(
816
- title="CryptoML Terminal โ€” ETH/USDT",
817
- css=BINANCE_CSS,
818
- theme=gr.themes.Base(
819
- primary_hue="yellow",
820
- secondary_hue="gray",
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.HTML(TRADINGVIEW_HTML)
852
-
853
- with gr.Row(equal_height=True):
854
- with gr.Column(scale=2):
855
- gr.HTML("""
856
- <div style="padding:10px 0;border-bottom:1px solid #2B3139;margin-bottom:10px;
857
- font-family:'Inter',sans-serif;">
858
- <span style="color:#F0B90B;font-size:12px;font-weight:700;text-transform:uppercase;
859
- letter-spacing:1px;">๐Ÿง  Model Control</span>
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
- train_output = gr.HTML(
896
- value=_html_warn("Klik <b>โšก Train Model</b> untuk melatih dan melihat hasil backtest.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
897
  )
898
 
899
- gr.HTML("""
900
- <div style="display:flex;justify-content:space-between;align-items:center;padding:10px 24px;
901
- background:#181A20;border-top:1px solid #2B3139;margin-top:12px;
902
- font-family:'Inter',sans-serif;">
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
- train_btn.click(fn=run_train, outputs=train_output)
910
- predict_btn.click(fn=run_predict, outputs=signal_output)
911
- refresh_btn.click(fn=refresh_price, outputs=price_header)
 
912
 
913
  demo.queue(default_concurrency_limit=1)
914
- logger.info("โœ… App ready. Launching...")
915
 
916
- demo.launch(server_name="0.0.0.0", server_port=7860)
 
 
 
 
 
 
 
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
+ ```