daafa999 commited on
Commit
0eb78ab
·
verified ·
1 Parent(s): 7827619

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +156 -93
app.py CHANGED
@@ -5,7 +5,6 @@ import traceback
5
  import time
6
  from datetime import datetime
7
 
8
- # 🔧 Force log output to HF container
9
  logging.basicConfig(
10
  stream=sys.stdout, level=logging.INFO,
11
  format="%(asctime)s [%(levelname)s] %(message)s"
@@ -13,7 +12,6 @@ logging.basicConfig(
13
  logger = logging.getLogger("crypto_ml")
14
  logger.info("🚀 Starting HF Space container...")
15
 
16
- # 1. IMPORTS
17
  try:
18
  import gradio as gr
19
  import yfinance as yf
@@ -27,20 +25,19 @@ except Exception as e:
27
  logger.error(f"❌ Import failed: {e}")
28
  sys.exit(1)
29
 
30
- # 2. CONFIG
31
  SYMBOL = "ETH-USD"
32
  FEE = 0.0015
33
- WINDOW = 30
34
  MODEL_PATH = "/tmp/rf_model.pkl"
35
  SCALER_PATH = "/tmp/scaler.pkl"
36
 
37
 
38
  # ============================================================
39
- # 3. SAFE DATA FETCHING
40
  # ============================================================
41
 
42
- def safe_download(symbol, period="60d", interval="1h"):
43
- """Download with retry and robust column handling."""
44
  for attempt in range(3):
45
  try:
46
  logger.info(f"📥 Download attempt {attempt+1}/3: {symbol} {period} {interval}")
@@ -48,48 +45,87 @@ def safe_download(symbol, period="60d", interval="1h"):
48
 
49
  if df is None or df.empty:
50
  logger.warning(f"⚠️ Empty dataframe on attempt {attempt+1}")
51
- time.sleep(2)
52
  continue
53
 
54
- # FIX: Handle MultiIndex columns (yfinance >= 0.2.x)
 
 
 
55
  if isinstance(df.columns, pd.MultiIndex):
56
- logger.info("🔧 Detected MultiIndex columns, flattening...")
57
- df.columns = df.columns.get_level_values(0)
 
 
 
58
 
59
  # Remove any duplicate columns
60
  df = df.loc[:, ~df.columns.duplicated()]
61
 
62
- # Standardize column names to Title Case
63
- col_map = {}
64
- for c in df.columns:
65
- col_map[c] = c.strip().title()
66
- df.rename(columns=col_map, inplace=True)
67
-
68
- logger.info(f"📊 Columns after cleanup: {df.columns.tolist()}")
69
- logger.info(f"📊 Shape: {df.shape}")
70
 
71
- # Validate required columns exist
72
  required = ["Open", "High", "Low", "Close", "Volume"]
73
  missing = [c for c in required if c not in df.columns]
74
  if missing:
75
  logger.error(f"❌ Missing columns: {missing}. Available: {df.columns.tolist()}")
76
- time.sleep(2)
77
- continue
 
 
 
 
 
 
 
 
 
 
 
 
78
 
79
  # Drop rows with NaN in required columns
80
  df = df.dropna(subset=required)
81
 
82
- if len(df) < 10:
 
 
 
 
 
 
83
  logger.warning(f"⚠️ Too few rows: {len(df)}")
84
- time.sleep(2)
85
  continue
86
 
87
- logger.info(f"✅ Data fetched: {len(df)} rows")
88
  return df
89
 
90
  except Exception as e:
91
  logger.error(f"❌ Download attempt {attempt+1} failed: {e}")
92
- time.sleep(2)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
 
94
  logger.error("❌ All download attempts failed")
95
  return pd.DataFrame()
@@ -97,23 +133,37 @@ def safe_download(symbol, period="60d", interval="1h"):
97
 
98
  def fetch_and_prepare():
99
  """Fetch OHLCV data, compute indicators, build windowed features."""
100
- df = safe_download(SYMBOL, period="60d", interval="1h")
101
 
102
  if df.empty:
103
- raise ValueError("Gagal mengambil data dari Yahoo Finance. Coba lagi nanti.")
 
 
 
104
 
105
  # Resample to 4h
 
106
  df = df.resample("4h").agg({
107
  "Open": "first", "High": "max", "Low": "min",
108
  "Close": "last", "Volume": "sum"
109
  }).dropna()
110
 
111
- if len(df) < 20:
112
- raise ValueError(f"Data setelah resample terlalu sedikit: {len(df)} baris. Butuh minimal 20.")
113
-
114
- df.columns = [c.lower() for c in df.columns]
115
- df.index.name = "timestamp"
116
- df = df.reset_index()
 
 
 
 
 
 
 
 
 
 
117
 
118
  logger.info(f"📊 After resample: {len(df)} rows")
119
 
@@ -130,8 +180,7 @@ def fetch_and_prepare():
130
  df["bb_upper"] = sma20 + std20 * 2
131
  df["bb_lower"] = sma20 - std20 * 2
132
  bb_width = (df["bb_upper"] - df["bb_lower"]).replace(0, np.nan)
133
- df["bb_pct"] = ((df["close"] - df["bb_lower"]) / bb_width).fillna(0.5)
134
- df["bb_pct"] = df["bb_pct"].clip(0, 1)
135
 
136
  # ── MACD ──
137
  ema12 = df["close"].ewm(span=12, adjust=False).mean()
@@ -160,7 +209,8 @@ def fetch_and_prepare():
160
  if len(df) < WINDOW + 5:
161
  raise ValueError(
162
  f"Data tidak cukup setelah preprocessing: {len(df)} baris. "
163
- f"Butuh minimal {WINDOW + 5} baris. Coba kurangi WINDOW atau gunakan periode data lebih panjang."
 
164
  )
165
 
166
  feat_cols = [
@@ -170,23 +220,31 @@ def fetch_and_prepare():
170
  "ema_9", "ema_21", "vol_change", "return_1", "return_4"
171
  ]
172
 
 
 
 
 
 
173
  X_list, y_list = [], []
174
  for i in range(WINDOW, len(df)):
175
  window_data = df.iloc[i - WINDOW:i][feat_cols].values
176
- if window_data.shape[0] == WINDOW and not np.isnan(window_data).any():
177
  X_list.append(window_data.flatten())
178
  y_list.append(int(df.iloc[i]["label"]))
179
 
180
  if len(X_list) < 10:
181
- raise ValueError(f"Tidak cukup sampel valid setelah windowing: {len(X_list)}. Butuh minimal 10.")
 
 
 
182
 
183
  X = np.array(X_list, dtype=np.float64)
184
  y = np.array(y_list, dtype=np.int32)
185
-
186
- # Final NaN/Inf check on X
187
  X = np.nan_to_num(X, nan=0.0, posinf=0.0, neginf=0.0)
188
 
189
- logger.info(f"✅ Features prepared: X={X.shape}, y={y.shape}, BUY={np.sum(y==1)}, SELL={np.sum(y==0)}")
 
 
190
  return X, y, df, feat_cols
191
 
192
 
@@ -219,7 +277,6 @@ def run_train():
219
  logger.info("🧠 Starting training...")
220
  X, y, df_full, feat_cols = fetch_and_prepare()
221
 
222
- # Check class balance
223
  n_buy = int(np.sum(y == 1))
224
  n_sell = int(np.sum(y == 0))
225
  if n_buy < 3 or n_sell < 3:
@@ -228,18 +285,19 @@ def run_train():
228
  scaler = MinMaxScaler()
229
  X_scaled = scaler.fit_transform(X)
230
 
231
- # Use 90% train, 10% test
232
- split = max(int(len(X) * 0.90), 10)
233
- if split >= len(X) - 2:
234
  split = len(X) - 3
235
 
236
  X_train, X_test = X_scaled[:split], X_scaled[split:]
237
  y_train, y_test = y[:split], y[split:]
238
 
 
 
239
  model = RandomForestClassifier(
240
  n_estimators=100, max_depth=8,
241
  min_samples_split=5, min_samples_leaf=2,
242
- random_state=42, n_jobs=1, # n_jobs=1 lebih stabil di HF
243
  class_weight="balanced"
244
  )
245
  model.fit(X_train, y_train)
@@ -249,25 +307,27 @@ def run_train():
249
  buy_sig = int(np.sum(y_pred == 1))
250
  sell_sig = int(np.sum(y_pred == 0))
251
 
252
- # Win-rate on test
253
  wins = int(np.sum((y_pred == 1) & (y_test == 1)))
254
  win_rate = (wins / max(buy_sig, 1)) * 100
255
 
256
- # Simulated PnL
257
- test_closes = df_full.iloc[split + WINDOW: split + WINDOW + len(y_test)]["close"].values
258
- if len(test_closes) != len(y_pred):
259
- test_closes = df_full.iloc[-len(y_pred):]["close"].values
 
 
 
 
 
260
 
261
  pnl = 0.0
262
- for i in range(min(len(y_pred), len(test_closes))):
263
  if y_pred[i] == 1:
264
- if i < len(test_closes) - 1:
265
- ret = (test_closes[i + 1] - test_closes[i]) / test_closes[i] - FEE
266
- pnl += ret
267
 
268
  pnl_pct = pnl * 100
269
 
270
- # Feature importance top-5
271
  importances = model.feature_importances_
272
  top_n = min(5, len(feat_cols))
273
  top5_idx = np.argsort(importances)[-top_n:][::-1]
@@ -280,9 +340,10 @@ def run_train():
280
  return _html_train(acc, win_rate, pnl_pct, len(X_train),
281
  len(X_test), buy_sig, sell_sig, top5, df_full)
282
  except Exception as e:
 
283
  logger.error(f"❌ Train failed: {e}")
284
- logger.error(traceback.format_exc())
285
- return _html_error(str(e))
286
 
287
 
288
  # ── Prediction ──
@@ -296,9 +357,8 @@ def run_predict():
296
  scaler = joblib.load(SCALER_PATH)
297
  model = joblib.load(MODEL_PATH)
298
 
299
- # Get last WINDOW rows
300
  if len(df_full) < WINDOW:
301
- return _html_warn(f"Data tidak cukup untuk prediksi: {len(df_full)} baris, butuh {WINDOW}.")
302
 
303
  latest = df_full.iloc[-WINDOW:][feat_cols].values.flatten().reshape(1, -1)
304
  latest = np.nan_to_num(latest, nan=0.0, posinf=0.0, neginf=0.0)
@@ -318,9 +378,10 @@ def run_predict():
318
  logger.info(f"🔮 {signal} conf={confidence:.1f}%")
319
  return _html_signal(signal, confidence, price, rsi, macd_val, bb_pct)
320
  except Exception as e:
 
321
  logger.error(f"❌ Predict failed: {e}")
322
- logger.error(traceback.format_exc())
323
- return _html_error(str(e))
324
 
325
 
326
  def refresh_price():
@@ -328,7 +389,7 @@ def refresh_price():
328
 
329
 
330
  # ============================================================
331
- # 4. HTML BUILDERS
332
  # ============================================================
333
 
334
  def _html_price_header(info):
@@ -451,11 +512,12 @@ def _html_train(acc, win_rate, pnl_pct, n_tr, n_te, buys, sells, top5, df_full):
451
  recent = df_full.tail(n_recent)
452
  rows = ""
453
  for _, r in recent.iterrows():
454
- t = r["timestamp"].strftime("%m/%d %H:%M") if pd.notna(r.get("timestamp")) else ""
455
- c = float(r["close"])
 
456
  ch = float(r.get("return_1", 0)) * 100
457
  ch2 = "#0ECB81" if ch >= 0 else "#F6465D"
458
- rv = float(r["rsi"])
459
  rows += f"""<tr style="border-bottom:1px solid #2B3139;">
460
  <td style="color:#848E9C;font-size:10px;padding:5px 6px;">{t}</td>
461
  <td style="color:#EAECEF;font-size:10px;padding:5px 6px;text-align:right;">${c:,.2f}</td>
@@ -506,27 +568,41 @@ def _html_train(acc, win_rate, pnl_pct, n_tr, n_te, buys, sells, top5, df_full):
506
  </div>
507
  <div style="padding:8px 18px;display:flex;justify-content:space-between;">
508
  <span style="color:#5E6673;font-size:10px;">Train: {n_tr} · Test: {n_te}</span>
509
- <span style="color:#5E6673;font-size:10px;">Split 90% / 10%</span>
510
  </div>
511
  </div>"""
512
 
513
 
514
- def _html_error(msg):
 
 
 
 
 
 
 
 
 
 
 
515
  return f"""<div style="background:#1E2329;border-radius:8px;border:1px solid rgba(246,70,93,.35);
516
  padding:20px;text-align:center;font-family:'Inter',sans-serif;">
517
  <div style="color:#F6465D;font-size:18px;font-weight:700;margin-bottom:6px;">❌ ERROR</div>
518
- <div style="color:#848E9C;font-size:12px;word-break:break-word;">{msg}</div></div>"""
 
 
519
 
520
 
521
  def _html_warn(msg):
522
  return f"""<div style="background:#1E2329;border-radius:8px;border:1px solid rgba(240,185,11,.35);
523
  padding:20px;text-align:center;font-family:'Inter',sans-serif;">
524
- <div style="color:#F0B90B;font-size:18px;font-weight:700;margin-bottom:6px;">⚠️ WARNING</div>
525
- <div style="color:#848E9C;font-size:12px;">{msg}</div></div>"""
 
526
 
527
 
528
  # ============================================================
529
- # 5. TRADINGVIEW WIDGET
530
  # ============================================================
531
 
532
  TRADINGVIEW_HTML = """
@@ -538,7 +614,7 @@ TRADINGVIEW_HTML = """
538
  """
539
 
540
  # ============================================================
541
- # 6. CSS (BINANCE THEME)
542
  # ============================================================
543
 
544
  BINANCE_CSS = """
@@ -579,13 +655,13 @@ button {
579
  font-size: 13px !important;
580
  cursor: pointer !important;
581
  }
582
- button.primary, .btn-primary {
583
  background: var(--accent) !important;
584
  color: var(--bg-primary) !important;
585
  border: none !important;
586
  box-shadow: 0 2px 8px rgba(240,185,11,.25) !important;
587
  }
588
- button.primary:hover, .btn-primary:hover {
589
  background: var(--accent-hover) !important;
590
  box-shadow: 0 4px 16px rgba(240,185,11,.4) !important;
591
  transform: translateY(-1px) !important;
@@ -616,10 +692,10 @@ input, textarea, select {
616
  """
617
 
618
  # ============================================================
619
- # 7. GRADIO UI
620
  # ============================================================
621
 
622
- logger.info("🎨 Building Gradio UI with Binance theme...")
623
 
624
  with gr.Blocks(
625
  title="CryptoML Terminal — ETH/USDT",
@@ -632,7 +708,6 @@ with gr.Blocks(
632
  ),
633
  ) as demo:
634
 
635
- # ── Navigation Bar ──
636
  gr.HTML("""
637
  <div style="display:flex;align-items:center;justify-content:space-between;
638
  padding:10px 24px;background:#181A20;border-bottom:2px solid #F0B90B;
@@ -654,17 +729,13 @@ with gr.Blocks(
654
  </div>
655
  </div>""")
656
 
657
- # ── Price Header ──
658
  price_header = gr.HTML(
659
  value=_html_price_header({"price": 0, "change_pct": 0, "high_24h": 0, "low_24h": 0, "volume": 0})
660
  )
661
 
662
- # ── Chart ──
663
  gr.HTML(TRADINGVIEW_HTML)
664
 
665
- # ── Main Panels ──
666
  with gr.Row(equal_height=True):
667
- # LEFT — Controls
668
  with gr.Column(scale=2):
669
  gr.HTML("""
670
  <div style="padding:10px 0;border-bottom:1px solid #2B3139;margin-bottom:10px;
@@ -692,7 +763,7 @@ with gr.Blocks(
692
  <span style="color:#EAECEF;font-size:10px;font-weight:500;">0.15%</span></div>
693
  <div style="display:flex;justify-content:space-between;margin-bottom:3px;">
694
  <span style="color:#848E9C;font-size:10px;">Lookback Window</span>
695
- <span style="color:#EAECEF;font-size:10px;font-weight:500;">30 bars</span></div>
696
  <div style="display:flex;justify-content:space-between;margin-bottom:3px;">
697
  <span style="color:#848E9C;font-size:10px;">Algorithm</span>
698
  <span style="color:#EAECEF;font-size:10px;font-weight:500;">RF-100</span></div>
@@ -701,18 +772,15 @@ with gr.Blocks(
701
  <span style="color:#EAECEF;font-size:10px;font-weight:500;">RSI·BB·MACD·EMA</span></div>
702
  </div>""")
703
 
704
- # RIGHT — Signal Output
705
  with gr.Column(scale=3):
706
  signal_output = gr.HTML(
707
  value=_html_warn("Klik <b>🔮 Predict Signal</b> untuk mendapatkan sinyal.")
708
  )
709
 
710
- # ── Training Results (full width) ──
711
  train_output = gr.HTML(
712
  value=_html_warn("Klik <b>⚡ Train Model</b> untuk melatih dan melihat hasil backtest.")
713
  )
714
 
715
- # ── Footer ──
716
  gr.HTML("""
717
  <div style="display:flex;justify-content:space-between;align-items:center;padding:10px 24px;
718
  background:#181A20;border-top:1px solid #2B3139;margin-top:12px;
@@ -723,16 +791,11 @@ with gr.Blocks(
723
  Data: Yahoo Finance · Chart: TradingView · ML: scikit-learn</span>
724
  </div>""")
725
 
726
- # ── Events ──
727
  train_btn.click(fn=run_train, outputs=train_output)
728
  predict_btn.click(fn=run_predict, outputs=signal_output)
729
  refresh_btn.click(fn=refresh_price, outputs=price_header)
730
 
731
-
732
- # ============================================================
733
- # 8. LAUNCH
734
- # ============================================================
735
  demo.queue(default_concurrency_limit=1)
736
- logger.info("✅ Gradio app built & queued. Launching on 0.0.0.0:7860...")
737
 
738
  demo.launch(server_name="0.0.0.0", server_port=7860)
 
5
  import time
6
  from datetime import datetime
7
 
 
8
  logging.basicConfig(
9
  stream=sys.stdout, level=logging.INFO,
10
  format="%(asctime)s [%(levelname)s] %(message)s"
 
12
  logger = logging.getLogger("crypto_ml")
13
  logger.info("🚀 Starting HF Space container...")
14
 
 
15
  try:
16
  import gradio as gr
17
  import yfinance as yf
 
25
  logger.error(f"❌ Import failed: {e}")
26
  sys.exit(1)
27
 
 
28
  SYMBOL = "ETH-USD"
29
  FEE = 0.0015
30
+ WINDOW = 20
31
  MODEL_PATH = "/tmp/rf_model.pkl"
32
  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}")
 
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()
 
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
 
 
180
  df["bb_upper"] = sma20 + std20 * 2
181
  df["bb_lower"] = sma20 - std20 * 2
182
  bb_width = (df["bb_upper"] - df["bb_lower"]).replace(0, np.nan)
183
+ df["bb_pct"] = ((df["close"] - df["bb_lower"]) / bb_width).fillna(0.5).clip(0, 1)
 
184
 
185
  # ── MACD ──
186
  ema12 = df["close"].ewm(span=12, adjust=False).mean()
 
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
  "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)):
230
  window_data = df.iloc[i - WINDOW:i][feat_cols].values
231
+ if window_data.shape[0] == WINDOW and not np.isnan(window_data).any() and not np.isinf(window_data).any():
232
  X_list.append(window_data.flatten())
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)
 
 
243
  X = np.nan_to_num(X, nan=0.0, posinf=0.0, neginf=0.0)
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
 
 
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:
 
285
  scaler = MinMaxScaler()
286
  X_scaled = scaler.fit_transform(X)
287
 
288
+ split = max(int(len(X) * 0.88), 10)
289
+ if split >= len(X) - 3:
 
290
  split = len(X) - 3
291
 
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,
300
+ random_state=42, n_jobs=1,
301
  class_weight="balanced"
302
  )
303
  model.fit(X_train, y_train)
 
307
  buy_sig = int(np.sum(y_pred == 1))
308
  sell_sig = int(np.sum(y_pred == 0))
309
 
 
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
324
+ for i in range(min(len(y_pred), len(test_closes) - 1)):
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_
332
  top_n = min(5, len(feat_cols))
333
  top5_idx = np.argsort(importances)[-top_n:][::-1]
 
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}")
345
+ logger.error(tb)
346
+ return _html_error(str(e), tb)
347
 
348
 
349
  # ── Prediction ──
 
357
  scaler = joblib.load(SCALER_PATH)
358
  model = joblib.load(MODEL_PATH)
359
 
 
360
  if len(df_full) < WINDOW:
361
+ return _html_warn(f"Data tidak cukup: {len(df_full)} baris, butuh {WINDOW}.")
362
 
363
  latest = df_full.iloc[-WINDOW:][feat_cols].values.flatten().reshape(1, -1)
364
  latest = np.nan_to_num(latest, nan=0.0, posinf=0.0, neginf=0.0)
 
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}")
383
+ logger.error(tb)
384
+ return _html_error(str(e), tb)
385
 
386
 
387
  def refresh_price():
 
389
 
390
 
391
  # ============================================================
392
+ # HTML BUILDERS
393
  # ============================================================
394
 
395
  def _html_price_header(info):
 
512
  recent = df_full.tail(n_recent)
513
  rows = ""
514
  for _, r in recent.iterrows():
515
+ ts_val = r.get("timestamp", None)
516
+ t = pd.to_datetime(ts_val).strftime("%m/%d %H:%M") if pd.notna(ts_val) else ""
517
+ c = float(r.get("close", 0))
518
  ch = float(r.get("return_1", 0)) * 100
519
  ch2 = "#0ECB81" if ch >= 0 else "#F6465D"
520
+ rv = float(r.get("rsi", 50))
521
  rows += f"""<tr style="border-bottom:1px solid #2B3139;">
522
  <td style="color:#848E9C;font-size:10px;padding:5px 6px;">{t}</td>
523
  <td style="color:#EAECEF;font-size:10px;padding:5px 6px;text-align:right;">${c:,.2f}</td>
 
568
  </div>
569
  <div style="padding:8px 18px;display:flex;justify-content:space-between;">
570
  <span style="color:#5E6673;font-size:10px;">Train: {n_tr} · Test: {n_te}</span>
571
+ <span style="color:#5E6673;font-size:10px;">Split 88% / 12%</span>
572
  </div>
573
  </div>"""
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:
581
+ tb_text = "<br>".join(tb_lines)
582
+ tb_html = f"""<details style="margin-top:10px;text-align:left;">
583
+ <summary style="color:#5E6673;font-size:10px;cursor:pointer;">Show Details</summary>
584
+ <pre style="color:#848E9C;font-size:9px;background:#2B3139;padding:8px;border-radius:4px;
585
+ overflow-x:auto;margin-top:4px;white-space:pre-wrap;">{tb_text}</pre>
586
+ </details>"""
587
+
588
  return f"""<div style="background:#1E2329;border-radius:8px;border:1px solid rgba(246,70,93,.35);
589
  padding:20px;text-align:center;font-family:'Inter',sans-serif;">
590
  <div style="color:#F6465D;font-size:18px;font-weight:700;margin-bottom:6px;">❌ ERROR</div>
591
+ <div style="color:#848E9C;font-size:12px;word-break:break-word;">{msg}</div>
592
+ {tb_html}
593
+ </div>"""
594
 
595
 
596
  def _html_warn(msg):
597
  return f"""<div style="background:#1E2329;border-radius:8px;border:1px solid rgba(240,185,11,.35);
598
  padding:20px;text-align:center;font-family:'Inter',sans-serif;">
599
+ <div style="color:#F0B90B;font-size:18px;font-weight:700;margin-bottom:6px;">⚠️</div>
600
+ <div style="color:#848E9C;font-size:12px;">{msg}</div>
601
+ </div>"""
602
 
603
 
604
  # ============================================================
605
+ # TRADINGVIEW WIDGET
606
  # ============================================================
607
 
608
  TRADINGVIEW_HTML = """
 
614
  """
615
 
616
  # ============================================================
617
+ # CSS
618
  # ============================================================
619
 
620
  BINANCE_CSS = """
 
655
  font-size: 13px !important;
656
  cursor: pointer !important;
657
  }
658
+ button.primary {
659
  background: var(--accent) !important;
660
  color: var(--bg-primary) !important;
661
  border: none !important;
662
  box-shadow: 0 2px 8px rgba(240,185,11,.25) !important;
663
  }
664
+ button.primary:hover {
665
  background: var(--accent-hover) !important;
666
  box-shadow: 0 4px 16px rgba(240,185,11,.4) !important;
667
  transform: translateY(-1px) !important;
 
692
  """
693
 
694
  # ============================================================
695
+ # GRADIO UI
696
  # ============================================================
697
 
698
+ logger.info("🎨 Building Gradio UI...")
699
 
700
  with gr.Blocks(
701
  title="CryptoML Terminal — ETH/USDT",
 
708
  ),
709
  ) as demo:
710
 
 
711
  gr.HTML("""
712
  <div style="display:flex;align-items:center;justify-content:space-between;
713
  padding:10px 24px;background:#181A20;border-bottom:2px solid #F0B90B;
 
729
  </div>
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)
737
 
 
738
  with gr.Row(equal_height=True):
 
739
  with gr.Column(scale=2):
740
  gr.HTML("""
741
  <div style="padding:10px 0;border-bottom:1px solid #2B3139;margin-bottom:10px;
 
763
  <span style="color:#EAECEF;font-size:10px;font-weight:500;">0.15%</span></div>
764
  <div style="display:flex;justify-content:space-between;margin-bottom:3px;">
765
  <span style="color:#848E9C;font-size:10px;">Lookback Window</span>
766
+ <span style="color:#EAECEF;font-size:10px;font-weight:500;">20 bars</span></div>
767
  <div style="display:flex;justify-content:space-between;margin-bottom:3px;">
768
  <span style="color:#848E9C;font-size:10px;">Algorithm</span>
769
  <span style="color:#EAECEF;font-size:10px;font-weight:500;">RF-100</span></div>
 
772
  <span style="color:#EAECEF;font-size:10px;font-weight:500;">RSI·BB·MACD·EMA</span></div>
773
  </div>""")
774
 
 
775
  with gr.Column(scale=3):
776
  signal_output = gr.HTML(
777
  value=_html_warn("Klik <b>🔮 Predict Signal</b> untuk mendapatkan sinyal.")
778
  )
779
 
 
780
  train_output = gr.HTML(
781
  value=_html_warn("Klik <b>⚡ Train Model</b> untuk melatih dan melihat hasil backtest.")
782
  )
783
 
 
784
  gr.HTML("""
785
  <div style="display:flex;justify-content:space-between;align-items:center;padding:10px 24px;
786
  background:#181A20;border-top:1px solid #2B3139;margin-top:12px;
 
791
  Data: Yahoo Finance · Chart: TradingView · ML: scikit-learn</span>
792
  </div>""")
793
 
 
794
  train_btn.click(fn=run_train, outputs=train_output)
795
  predict_btn.click(fn=run_predict, outputs=signal_output)
796
  refresh_btn.click(fn=refresh_price, outputs=price_header)
797
 
 
 
 
 
798
  demo.queue(default_concurrency_limit=1)
799
+ logger.info("✅ App ready. Launching...")
800
 
801
  demo.launch(server_name="0.0.0.0", server_port=7860)