daafa999 commited on
Commit
092edaf
ยท
verified ยท
1 Parent(s): 0eb78ab

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +270 -155
app.py CHANGED
@@ -3,7 +3,8 @@ import os
3
  import logging
4
  import traceback
5
  import time
6
- from datetime import datetime
 
7
 
8
  logging.basicConfig(
9
  stream=sys.stdout, level=logging.INFO,
@@ -20,6 +21,7 @@ try:
20
  from sklearn.ensemble import RandomForestClassifier
21
  from sklearn.preprocessing import MinMaxScaler
22
  import joblib
 
23
  logger.info("โœ… All dependencies imported successfully")
24
  except Exception as e:
25
  logger.error(f"โŒ Import failed: {e}")
@@ -33,139 +35,234 @@ SCALER_PATH = "/tmp/scaler.pkl"
33
 
34
 
35
  # ============================================================
36
- # SAFE DATA FETCHING โ€” Fixed MultiIndex handling
37
  # ============================================================
38
 
39
- def safe_download(symbol, period="90d", interval="1h"):
40
- """Download with retry and ROBUST column handling."""
41
- for attempt in range(3):
 
 
 
 
 
 
42
  try:
43
- logger.info(f"๐Ÿ“ฅ Download attempt {attempt+1}/3: {symbol} {period} {interval}")
44
- df = yf.download(symbol, period=period, interval=interval, progress=False, threads=False)
45
-
46
- if df is None or df.empty:
47
- logger.warning(f"โš ๏ธ Empty dataframe on attempt {attempt+1}")
48
- time.sleep(3)
49
- continue
50
-
51
- logger.info(f"๐Ÿ“Š Raw shape: {df.shape}, Raw columns type: {type(df.columns)}")
52
- logger.info(f"๐Ÿ“Š Raw columns: {df.columns.tolist()}")
53
-
54
- # โ”โ”โ” FIX: Handle MultiIndex columns properly โ”โ”โ”
55
- if isinstance(df.columns, pd.MultiIndex):
56
- logger.info("๐Ÿ”ง Detected MultiIndex columns โ€” using droplevel(0)...")
57
- # Level 0 = ticker name (e.g. "ETH-USD"), Level 1 = metric (e.g. "Open")
58
- # We need Level 1 (the metric names)
59
- df.columns = df.columns.droplevel(0)
60
- logger.info(f"๐Ÿ”ง After droplevel: {df.columns.tolist()}")
61
-
62
- # Remove any duplicate columns
63
- df = df.loc[:, ~df.columns.duplicated()]
64
-
65
- # Strip whitespace and standardize to Title Case
66
- df.columns = [str(c).strip().title() for c in df.columns]
67
- logger.info(f"๐Ÿ”ง After title case: {df.columns.tolist()}")
68
-
69
- # Validate required columns
70
- required = ["Open", "High", "Low", "Close", "Volume"]
71
- missing = [c for c in required if c not in df.columns]
72
- if missing:
73
- logger.error(f"โŒ Missing columns: {missing}. Available: {df.columns.tolist()}")
74
- # Try alternate: maybe columns are lowercase
75
- alt_map = {c.lower(): c for c in df.columns}
76
- found_all = True
77
- for req in missing:
78
- if req.lower() in alt_map:
79
- df.rename(columns={alt_map[req.lower()]: req}, inplace=True)
80
- else:
81
- found_all = False
82
-
83
- if not found_all:
84
- still_missing = [c for c in required if c not in df.columns]
85
- logger.error(f"โŒ Still missing after alternate: {still_missing}")
86
- time.sleep(3)
87
- continue
88
-
89
- # Drop rows with NaN in required columns
90
- df = df.dropna(subset=required)
91
-
92
- # Ensure numeric types
93
- for col in required:
94
- df[col] = pd.to_numeric(df[col], errors="coerce")
95
-
96
- df = df.dropna(subset=required)
97
-
98
- if len(df) < 20:
99
- logger.warning(f"โš ๏ธ Too few rows: {len(df)}")
100
- time.sleep(3)
101
- continue
102
-
103
- logger.info(f"โœ… Data fetched successfully: {len(df)} rows, columns: {df.columns.tolist()}")
104
  return df
 
 
 
105
 
106
- except Exception as e:
107
- logger.error(f"โŒ Download attempt {attempt+1} failed: {e}")
108
- logger.error(traceback.format_exc())
109
- time.sleep(3)
110
 
111
- # Fallback: try different interval
112
- logger.warning("โš ๏ธ Trying fallback: 15m interval...")
113
  try:
114
- df = yf.download(symbol, period="60d", interval="15m", progress=False, threads=False)
 
 
 
 
 
 
 
 
 
115
  if df is not None and not df.empty:
116
- if isinstance(df.columns, pd.MultiIndex):
117
- df.columns = df.columns.droplevel(0)
118
- df.columns = [str(c).strip().title() for c in df.columns]
119
- df = df.loc[:, ~df.columns.duplicated()]
120
- df = df.dropna(subset=["Open", "High", "Low", "Close", "Volume"])
121
- for col in ["Open", "High", "Low", "Close", "Volume"]:
122
- df[col] = pd.to_numeric(df[col], errors="coerce")
123
- df = df.dropna(subset=["Open", "High", "Low", "Close", "Volume"])
124
- if len(df) >= 20:
125
- logger.info(f"โœ… Fallback data: {len(df)} rows")
 
 
 
 
 
 
 
126
  return df
127
- except Exception as e2:
128
- logger.error(f"โŒ Fallback also failed: {e2}")
 
 
129
 
130
- logger.error("โŒ All download attempts failed")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  return pd.DataFrame()
132
 
133
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  def fetch_and_prepare():
135
  """Fetch OHLCV data, compute indicators, build windowed features."""
136
- df = safe_download(SYMBOL, period="90d", interval="1h")
137
 
138
  if df.empty:
139
- raise ValueError(
140
- "Gagal mengambil data dari Yahoo Finance. "
141
- "Kemungkinan koneksi bermasalah atau rate-limited. Tunggu beberapa menit lalu coba lagi."
142
- )
143
 
144
- # Resample to 4h
145
- logger.info(f"๐Ÿ“Š Before resample: {len(df)} rows")
146
- df = df.resample("4h").agg({
147
- "Open": "first", "High": "max", "Low": "min",
148
- "Close": "last", "Volume": "sum"
149
- }).dropna()
 
 
 
 
 
 
 
150
 
151
  if len(df) < 30:
152
- logger.warning(f"โš ๏ธ Only {len(df)} rows after 4h resample, trying 1h resample...")
153
- df = safe_download(SYMBOL, period="90d", interval="1h")
154
- if not df.empty:
155
- # Use 1h data directly
156
- df.columns = [c.lower() for c in df.columns]
157
- df.index.name = "timestamp"
158
- df = df.reset_index()
159
- logger.info(f"๐Ÿ“Š Using 1h data: {len(df)} rows")
160
- # Skip resample, continue with 1h
161
- else:
162
- raise ValueError(f"Data terlalu sedikit setelah resample: {len(df)} baris.")
163
- else:
164
- df.columns = [c.lower() for c in df.columns]
165
- df.index.name = "timestamp"
166
- df = df.reset_index()
167
 
168
- logger.info(f"๐Ÿ“Š After resample: {len(df)} rows")
 
 
169
 
170
  # โ”€โ”€ RSI 14 โ”€โ”€
171
  delta = df["close"].diff()
@@ -209,8 +306,7 @@ def fetch_and_prepare():
209
  if len(df) < WINDOW + 5:
210
  raise ValueError(
211
  f"Data tidak cukup setelah preprocessing: {len(df)} baris. "
212
- f"Butuh minimal {WINDOW + 5} baris. "
213
- f"WINDOW={WINDOW}. Coba lagi nanti saat data lebih tersedia."
214
  )
215
 
216
  feat_cols = [
@@ -220,10 +316,9 @@ def fetch_and_prepare():
220
  "ema_9", "ema_21", "vol_change", "return_1", "return_4"
221
  ]
222
 
223
- # Validate all feature columns exist
224
  missing_feats = [f for f in feat_cols if f not in df.columns]
225
  if missing_feats:
226
- raise ValueError(f"Missing feature columns: {missing_feats}. Available: {df.columns.tolist()}")
227
 
228
  X_list, y_list = [], []
229
  for i in range(WINDOW, len(df)):
@@ -233,10 +328,7 @@ def fetch_and_prepare():
233
  y_list.append(int(df.iloc[i]["label"]))
234
 
235
  if len(X_list) < 10:
236
- raise ValueError(
237
- f"Tidak cukup sampel valid setelah windowing: {len(X_list)}. "
238
- f"Butuh minimal 10. Total rows={len(df)}, WINDOW={WINDOW}"
239
- )
240
 
241
  X = np.array(X_list, dtype=np.float64)
242
  y = np.array(y_list, dtype=np.int32)
@@ -244,16 +336,16 @@ def fetch_and_prepare():
244
 
245
  n_buy = int(np.sum(y == 1))
246
  n_sell = int(np.sum(y == 0))
247
- logger.info(f"โœ… Features ready: X={X.shape}, y={y.shape}, BUY={n_buy}, SELL={n_sell}")
248
- return X, y, df, feat_cols
249
 
250
 
251
  def get_market_info():
252
  """Quick snapshot for price header."""
253
  try:
254
- df = safe_download(SYMBOL, period="5d", interval="1h")
255
  if df.empty or len(df) < 2:
256
- return {"price": 0, "change_pct": 0, "high_24h": 0, "low_24h": 0, "volume": 0}
257
 
258
  cur = float(df["Close"].iloc[-1])
259
  prev = float(df["Close"].iloc[0])
@@ -264,10 +356,11 @@ def get_market_info():
264
  "high_24h": float(df["High"].tail(tail_n).max()),
265
  "low_24h": float(df["Low"].tail(tail_n).min()),
266
  "volume": float(df["Volume"].tail(tail_n).sum()),
 
267
  }
268
  except Exception as e:
269
  logger.error(f"Market info error: {e}")
270
- return {"price": 0, "change_pct": 0, "high_24h": 0, "low_24h": 0, "volume": 0}
271
 
272
 
273
  # โ”€โ”€ Training โ”€โ”€
@@ -275,12 +368,12 @@ def get_market_info():
275
  def run_train():
276
  try:
277
  logger.info("๐Ÿง  Starting training...")
278
- X, y, df_full, feat_cols = fetch_and_prepare()
279
 
280
  n_buy = int(np.sum(y == 1))
281
  n_sell = int(np.sum(y == 0))
282
  if n_buy < 3 or n_sell < 3:
283
- raise ValueError(f"Kelas tidak seimbang: BUY={n_buy}, SELL={n_sell}. Butuh minimal 3 tiap kelas.")
284
 
285
  scaler = MinMaxScaler()
286
  X_scaled = scaler.fit_transform(X)
@@ -292,8 +385,6 @@ def run_train():
292
  X_train, X_test = X_scaled[:split], X_scaled[split:]
293
  y_train, y_test = y[:split], y[split:]
294
 
295
- logger.info(f"๐Ÿ“Š Train: {len(X_train)}, Test: {len(X_test)}")
296
-
297
  model = RandomForestClassifier(
298
  n_estimators=100, max_depth=8,
299
  min_samples_split=5, min_samples_leaf=2,
@@ -310,14 +401,12 @@ def run_train():
310
  wins = int(np.sum((y_pred == 1) & (y_test == 1)))
311
  win_rate = (wins / max(buy_sig, 1)) * 100
312
 
313
- # Simulated PnL using df_full close prices
314
- # X_test corresponds to df_full rows [split+WINDOW : split+WINDOW+len(y_test)]
315
  start_idx = split + WINDOW
316
  end_idx = start_idx + len(y_test)
317
  if end_idx <= len(df_full):
318
  test_closes = df_full.iloc[start_idx:end_idx]["close"].values
319
  else:
320
- # Fallback: use last len(y_test) rows
321
  test_closes = df_full.iloc[-(len(y_test) + 1):]["close"].values
322
 
323
  pnl = 0.0
@@ -325,7 +414,6 @@ def run_train():
325
  if y_pred[i] == 1:
326
  ret = (test_closes[i + 1] - test_closes[i]) / test_closes[i] - FEE
327
  pnl += ret
328
-
329
  pnl_pct = pnl * 100
330
 
331
  importances = model.feature_importances_
@@ -336,9 +424,9 @@ def run_train():
336
  joblib.dump(model, MODEL_PATH)
337
  joblib.dump(scaler, SCALER_PATH)
338
 
339
- logger.info(f"โœ… Training done โ€” acc={acc:.4f} win_rate={win_rate:.1f}%")
340
  return _html_train(acc, win_rate, pnl_pct, len(X_train),
341
- len(X_test), buy_sig, sell_sig, top5, df_full)
342
  except Exception as e:
343
  tb = traceback.format_exc()
344
  logger.error(f"โŒ Train failed: {e}")
@@ -353,7 +441,7 @@ def run_predict():
353
  if not os.path.exists(MODEL_PATH):
354
  return _html_warn("Model belum di-training. Klik <b>โšก Train Model</b> terlebih dahulu.")
355
 
356
- X, y, df_full, feat_cols = fetch_and_prepare()
357
  scaler = joblib.load(SCALER_PATH)
358
  model = joblib.load(MODEL_PATH)
359
 
@@ -375,8 +463,8 @@ def run_predict():
375
  bb_pct = float(row.get("bb_pct", 0.5))
376
 
377
  signal = "BUY" if pred == 1 else "SELL"
378
- logger.info(f"๐Ÿ”ฎ {signal} conf={confidence:.1f}%")
379
- return _html_signal(signal, confidence, price, rsi, macd_val, bb_pct)
380
  except Exception as e:
381
  tb = traceback.format_exc()
382
  logger.error(f"โŒ Predict failed: {e}")
@@ -392,19 +480,30 @@ def refresh_price():
392
  # HTML BUILDERS
393
  # ============================================================
394
 
 
 
 
 
 
 
 
395
  def _html_price_header(info):
396
- p = info["price"]
397
- chg = info["change_pct"]
398
- hi, lo = info["high_24h"], info["low_24h"]
399
- vol = info["volume"]
 
 
 
400
 
401
  if p == 0:
402
- return """
403
  <div style="display:flex;align-items:center;gap:28px;padding:14px 24px;
404
  background:#1E2329;border-bottom:1px solid #2B3139;
405
  font-family:'Inter',sans-serif;">
406
  <span style="color:#F0B90B;font-size:17px;font-weight:800;">ETH / USDT</span>
407
  <span style="color:#5E6673;font-size:13px;">โณ Loading price data...</span>
 
408
  </div>"""
409
 
410
  cc = "#0ECB81" if chg >= 0 else "#F6465D"
@@ -417,6 +516,7 @@ def _html_price_header(info):
417
  <div style="display:flex;align-items:baseline;gap:8px;">
418
  <span style="color:#F0B90B;font-size:17px;font-weight:800;">ETH / USDT</span>
419
  <span style="color:#5E6673;font-size:11px;">Perpetual</span>
 
420
  </div>
421
  <div style="display:flex;align-items:baseline;gap:8px;">
422
  <span style="color:{cc};font-size:26px;font-weight:800;">${p:,.2f}</span>
@@ -433,7 +533,7 @@ def _html_price_header(info):
433
  </div>"""
434
 
435
 
436
- def _html_signal(signal, confidence, price, rsi, macd_val, bb_pct):
437
  buy = signal == "BUY"
438
  sc = "#0ECB81" if buy else "#F6465D"
439
  sbg = "rgba(14,203,129,.12)" if buy else "rgba(246,70,93,.12)"
@@ -451,6 +551,8 @@ def _html_signal(signal, confidence, price, rsi, macd_val, bb_pct):
451
  bs = "Upper" if bb_pct > .8 else ("Lower" if bb_pct < .2 else "Mid")
452
  ts = datetime.utcnow().strftime("%Y-%m-%d %H:%M UTC")
453
 
 
 
454
  return f"""
455
  <div style="background:#1E2329;border-radius:8px;border:1px solid {sbd};overflow:hidden;
456
  font-family:'Inter',sans-serif;">
@@ -484,17 +586,21 @@ def _html_signal(signal, confidence, price, rsi, macd_val, bb_pct):
484
  </div>
485
  <div style="padding:10px 20px;display:flex;justify-content:space-between;align-items:center;">
486
  <span style="color:#5E6673;font-size:10px;">โฐ {ts}</span>
487
- <span style="color:#5E6673;font-size:10px;">Entry โ‰ˆ ${price:,.2f}</span>
 
 
 
488
  </div>
489
  </div>"""
490
 
491
 
492
- def _html_train(acc, win_rate, pnl_pct, n_tr, n_te, buys, sells, top5, df_full):
493
  ac = "#0ECB81" if acc >= .58 else ("#F0B90B" if acc >= .50 else "#F6465D")
494
  wc = "#0ECB81" if win_rate >= 55 else ("#F0B90B" if win_rate >= 45 else "#F6465D")
495
  pc = "#0ECB81" if pnl_pct >= 0 else "#F6465D"
496
  ps = "+" if pnl_pct >= 0 else ""
497
  ts = datetime.utcnow().strftime("%Y-%m-%d %H:%M UTC")
 
498
 
499
  fi = ""
500
  for name, imp in top5:
@@ -524,14 +630,28 @@ def _html_train(acc, win_rate, pnl_pct, n_tr, n_te, buys, sells, top5, df_full):
524
  <td style="color:{ch2};font-size:10px;padding:5px 6px;text-align:right;">{ch:+.2f}%</td>
525
  <td style="color:#848E9C;font-size:10px;padding:5px 6px;text-align:right;">{rv:.1f}</td></tr>"""
526
 
 
 
 
 
 
 
 
 
 
 
527
  return f"""
528
  <div style="background:#1E2329;border-radius:8px;border:1px solid #2B3139;overflow:hidden;
529
  font-family:'Inter',sans-serif;">
530
  <div style="padding:14px 20px;border-bottom:1px solid #2B3139;display:flex;
531
  justify-content:space-between;align-items:center;">
532
- <span style="color:#F0B90B;font-size:13px;font-weight:700;">โœ… MODEL TRAINED</span>
 
 
 
533
  <span style="color:#5E6673;font-size:10px;">{ts}</span>
534
  </div>
 
535
  <div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;border-bottom:1px solid #2B3139;">
536
  <div style="padding:10px 14px;border-right:1px solid #2B3139;text-align:center;">
537
  <div style="color:#5E6673;font-size:9px;text-transform:uppercase;margin-bottom:2px;">Test Acc</div>
@@ -574,7 +694,6 @@ def _html_train(acc, win_rate, pnl_pct, n_tr, n_te, buys, sells, top5, df_full):
574
 
575
 
576
  def _html_error(msg, tb=""):
577
- # Show first 3 lines of traceback for debugging
578
  tb_lines = tb.strip().split("\n")[-6:] if tb else []
579
  tb_html = ""
580
  if tb_lines:
@@ -602,7 +721,7 @@ def _html_warn(msg):
602
 
603
 
604
  # ============================================================
605
- # TRADINGVIEW WIDGET
606
  # ============================================================
607
 
608
  TRADINGVIEW_HTML = """
@@ -613,10 +732,6 @@ TRADINGVIEW_HTML = """
613
  </div>
614
  """
615
 
616
- # ============================================================
617
- # CSS
618
- # ============================================================
619
-
620
  BINANCE_CSS = """
621
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap');
622
 
@@ -730,7 +845,7 @@ with gr.Blocks(
730
  </div>""")
731
 
732
  price_header = gr.HTML(
733
- value=_html_price_header({"price": 0, "change_pct": 0, "high_24h": 0, "low_24h": 0, "volume": 0})
734
  )
735
 
736
  gr.HTML(TRADINGVIEW_HTML)
 
3
  import logging
4
  import traceback
5
  import time
6
+ import random
7
+ from datetime import datetime, timedelta
8
 
9
  logging.basicConfig(
10
  stream=sys.stdout, level=logging.INFO,
 
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}")
 
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()
 
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 = [
 
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)):
 
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)
 
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])
 
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 โ”€โ”€
 
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)
 
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,
 
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
 
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_
 
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}")
 
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
 
 
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}")
 
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"
 
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>
 
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)"
 
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;">
 
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:
 
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>
 
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:
 
721
 
722
 
723
  # ============================================================
724
+ # TRADINGVIEW & CSS
725
  # ============================================================
726
 
727
  TRADINGVIEW_HTML = """
 
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
 
 
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)