Update app.py
Browse files
app.py
CHANGED
|
@@ -3,12 +3,10 @@ import os
|
|
| 3 |
import logging
|
| 4 |
import time
|
| 5 |
|
| 6 |
-
# π§ Force log output to HF container
|
| 7 |
logging.basicConfig(stream=sys.stdout, level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
| 8 |
logger = logging.getLogger("crypto_ml")
|
| 9 |
logger.info("π Starting HF Space container...")
|
| 10 |
|
| 11 |
-
# 1. IMPORTS (Wrapped for safe logging)
|
| 12 |
try:
|
| 13 |
import gradio as gr
|
| 14 |
import yfinance as yf
|
|
@@ -17,48 +15,71 @@ try:
|
|
| 17 |
from sklearn.ensemble import RandomForestClassifier
|
| 18 |
from sklearn.preprocessing import MinMaxScaler
|
| 19 |
import joblib
|
| 20 |
-
logger.info("β
|
| 21 |
except Exception as e:
|
| 22 |
logger.error(f"β Import failed: {e}")
|
| 23 |
sys.exit(1)
|
| 24 |
|
| 25 |
-
# 2. CONFIG
|
| 26 |
SYMBOL = "ETH-USD"
|
| 27 |
FEE = 0.0015
|
| 28 |
WINDOW = 60
|
| 29 |
MODEL_PATH = "/tmp/rf_model.pkl"
|
| 30 |
SCALER_PATH = "/tmp/scaler.pkl"
|
| 31 |
|
| 32 |
-
# 3. DATA & MODEL FUNCTIONS
|
| 33 |
def fetch_and_prepare():
|
| 34 |
logger.info("π₯ Fetching ETH-USD from Yahoo Finance...")
|
| 35 |
df = yf.download(SYMBOL, period="60d", interval="1h", progress=False)
|
| 36 |
-
|
| 37 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
df.index.name = "timestamp"
|
| 39 |
df = df.reset_index()
|
| 40 |
-
|
| 41 |
-
#
|
| 42 |
delta = df["close"].diff()
|
| 43 |
-
gain = (delta.where(delta>0,0)).rolling(14).mean()
|
| 44 |
-
loss = (-delta.where(delta<0,0)).rolling(14).mean()
|
| 45 |
-
df["rsi"] = 100 - (100/(1+gain/loss))
|
| 46 |
-
|
| 47 |
sma = df["close"].rolling(20).mean()
|
| 48 |
std = df["close"].rolling(20).std()
|
| 49 |
-
df["bb_upper"] = sma + (std*2)
|
| 50 |
-
df["bb_lower"] = sma - (std*2)
|
| 51 |
-
|
|
|
|
| 52 |
df["next_close"] = df["close"].shift(-1)
|
| 53 |
-
df["label"] = (df["next_close"] > df["close"]*(1+FEE)).astype(int)
|
| 54 |
df = df.dropna()
|
| 55 |
-
|
| 56 |
-
feat_cols = ["open","high","low","close","volume","rsi","bb_upper","bb_lower"]
|
| 57 |
X_list, y_list = [], []
|
| 58 |
for i in range(WINDOW, len(df)):
|
| 59 |
X_list.append(df.iloc[i-WINDOW:i][feat_cols].values.flatten())
|
| 60 |
y_list.append(df.iloc[i]["label"])
|
| 61 |
-
|
| 62 |
return np.array(X_list), np.array(y_list), df, feat_cols
|
| 63 |
|
| 64 |
def run_train():
|
|
@@ -68,7 +89,7 @@ def run_train():
|
|
| 68 |
scaler = MinMaxScaler()
|
| 69 |
X_scaled = scaler.fit_transform(X)
|
| 70 |
|
| 71 |
-
split = int(len(X)*0.95)
|
| 72 |
model = RandomForestClassifier(n_estimators=100, max_depth=10, random_state=42, n_jobs=-1)
|
| 73 |
model.fit(X_scaled[:split], y[:split])
|
| 74 |
|
|
@@ -77,10 +98,10 @@ def run_train():
|
|
| 77 |
joblib.dump(scaler, SCALER_PATH)
|
| 78 |
|
| 79 |
logger.info(f"β
Training complete. Accuracy: {acc:.4f}")
|
| 80 |
-
return {"status": "β
Success", "accuracy": round(acc,4), "message": "Model saved
|
| 81 |
except Exception as e:
|
| 82 |
logger.error(f"β Train failed: {e}")
|
| 83 |
-
return {"status": "β Error", "message": str(e)}
|
| 84 |
|
| 85 |
def run_predict():
|
| 86 |
try:
|
|
@@ -89,20 +110,24 @@ def run_predict():
|
|
| 89 |
|
| 90 |
X, y, df_full, feat_cols = fetch_and_prepare()
|
| 91 |
scaler = joblib.load(SCALER_PATH)
|
| 92 |
-
latest = df_full.iloc[-WINDOW:][feat_cols].values.flatten().reshape(1,-1)
|
| 93 |
latest_scaled = scaler.transform(latest)
|
| 94 |
|
| 95 |
model = joblib.load(MODEL_PATH)
|
| 96 |
pred = int(model.predict(latest_scaled)[0])
|
| 97 |
prob = float(model.predict_proba(latest_scaled)[0][1])
|
| 98 |
|
| 99 |
-
return {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
except Exception as e:
|
| 101 |
logger.error(f"β Predict failed: {e}")
|
| 102 |
-
return {"status": "β Error", "message": str(e)}
|
| 103 |
|
| 104 |
-
#
|
| 105 |
-
logger.info("π¨ Building Gradio UI...")
|
| 106 |
tradingview_html = """
|
| 107 |
<div style="width:100%;height:400px;margin:10px 0;">
|
| 108 |
<iframe src="https://s.tradingview.com/widgetembed/?symbol=BINANCE%3AETHUSDT&interval=240&hidesidetoolbar=1&symboledit=1&saveimage=1&toolbarbg=transparent&studies=%5B%5D&theme=dark&style=1&timezone=UTC&withdateranges=1&showpopupbutton=1&studies_overrides=%7B%7D&overrides=%7B%7D&enabled_features=%5B%5D&disabled_features=%5B%5D&showpopupbutton=1&locale=en"
|
|
@@ -124,12 +149,7 @@ with gr.Blocks(title="Crypto ML Strategy") as demo:
|
|
| 124 |
|
| 125 |
train_btn.click(run_train, outputs=train_out)
|
| 126 |
pred_btn.click(run_predict, outputs=pred_out)
|
| 127 |
-
gr.Markdown("π‘ *Model
|
| 128 |
|
| 129 |
-
# 5. QUEUE & LAUNCH
|
| 130 |
demo.queue(default_concurrency_limit=1)
|
| 131 |
-
logger.info("β
|
| 132 |
-
|
| 133 |
-
# Fallback untuk local testing, diabaikan di HF
|
| 134 |
-
if __name__ == "__main__":
|
| 135 |
-
demo.launch(server_name="0.0.0.0", server_port=7860)
|
|
|
|
| 3 |
import logging
|
| 4 |
import time
|
| 5 |
|
|
|
|
| 6 |
logging.basicConfig(stream=sys.stdout, level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
| 7 |
logger = logging.getLogger("crypto_ml")
|
| 8 |
logger.info("π Starting HF Space container...")
|
| 9 |
|
|
|
|
| 10 |
try:
|
| 11 |
import gradio as gr
|
| 12 |
import yfinance as yf
|
|
|
|
| 15 |
from sklearn.ensemble import RandomForestClassifier
|
| 16 |
from sklearn.preprocessing import MinMaxScaler
|
| 17 |
import joblib
|
| 18 |
+
logger.info("β
Dependencies loaded")
|
| 19 |
except Exception as e:
|
| 20 |
logger.error(f"β Import failed: {e}")
|
| 21 |
sys.exit(1)
|
| 22 |
|
|
|
|
| 23 |
SYMBOL = "ETH-USD"
|
| 24 |
FEE = 0.0015
|
| 25 |
WINDOW = 60
|
| 26 |
MODEL_PATH = "/tmp/rf_model.pkl"
|
| 27 |
SCALER_PATH = "/tmp/scaler.pkl"
|
| 28 |
|
|
|
|
| 29 |
def fetch_and_prepare():
|
| 30 |
logger.info("π₯ Fetching ETH-USD from Yahoo Finance...")
|
| 31 |
df = yf.download(SYMBOL, period="60d", interval="1h", progress=False)
|
| 32 |
+
if df.empty:
|
| 33 |
+
raise ValueError("yfinance returned empty data")
|
| 34 |
+
|
| 35 |
+
# π§ FIX: Handle yfinance MultiIndex columns (v0.2.30+)
|
| 36 |
+
if isinstance(df.columns, pd.MultiIndex):
|
| 37 |
+
df.columns = df.columns.get_level_values(1)
|
| 38 |
+
|
| 39 |
+
# Map Adj Close -> Close if present
|
| 40 |
+
if 'Adj Close' in df.columns:
|
| 41 |
+
df.rename(columns={'Adj Close': 'Close'}, inplace=True)
|
| 42 |
+
|
| 43 |
+
# Standardize to lowercase to avoid case-sensitivity issues
|
| 44 |
+
df.columns = [col.lower() for col in df.columns]
|
| 45 |
+
|
| 46 |
+
# Verify required columns
|
| 47 |
+
required = ['open', 'high', 'low', 'close', 'volume']
|
| 48 |
+
missing = [c for c in required if c not in df.columns]
|
| 49 |
+
if missing:
|
| 50 |
+
raise ValueError(f"Missing columns: {missing}. Found: {list(df.columns)}")
|
| 51 |
+
|
| 52 |
+
# Resample to 4h candles
|
| 53 |
+
df = df.resample("4h").agg({
|
| 54 |
+
"open": "first", "high": "max", "low": "min",
|
| 55 |
+
"close": "last", "volume": "sum"
|
| 56 |
+
}).dropna()
|
| 57 |
+
|
| 58 |
df.index.name = "timestamp"
|
| 59 |
df = df.reset_index()
|
| 60 |
+
|
| 61 |
+
# π Technical Indicators
|
| 62 |
delta = df["close"].diff()
|
| 63 |
+
gain = (delta.where(delta > 0, 0)).rolling(14).mean()
|
| 64 |
+
loss = (-delta.where(delta < 0, 0)).rolling(14).mean()
|
| 65 |
+
df["rsi"] = 100 - (100 / (1 + gain / loss))
|
| 66 |
+
|
| 67 |
sma = df["close"].rolling(20).mean()
|
| 68 |
std = df["close"].rolling(20).std()
|
| 69 |
+
df["bb_upper"] = sma + (std * 2)
|
| 70 |
+
df["bb_lower"] = sma - (std * 2)
|
| 71 |
+
|
| 72 |
+
# Labels
|
| 73 |
df["next_close"] = df["close"].shift(-1)
|
| 74 |
+
df["label"] = (df["next_close"] > df["close"] * (1 + FEE)).astype(int)
|
| 75 |
df = df.dropna()
|
| 76 |
+
|
| 77 |
+
feat_cols = ["open", "high", "low", "close", "volume", "rsi", "bb_upper", "bb_lower"]
|
| 78 |
X_list, y_list = [], []
|
| 79 |
for i in range(WINDOW, len(df)):
|
| 80 |
X_list.append(df.iloc[i-WINDOW:i][feat_cols].values.flatten())
|
| 81 |
y_list.append(df.iloc[i]["label"])
|
| 82 |
+
|
| 83 |
return np.array(X_list), np.array(y_list), df, feat_cols
|
| 84 |
|
| 85 |
def run_train():
|
|
|
|
| 89 |
scaler = MinMaxScaler()
|
| 90 |
X_scaled = scaler.fit_transform(X)
|
| 91 |
|
| 92 |
+
split = int(len(X) * 0.95)
|
| 93 |
model = RandomForestClassifier(n_estimators=100, max_depth=10, random_state=42, n_jobs=-1)
|
| 94 |
model.fit(X_scaled[:split], y[:split])
|
| 95 |
|
|
|
|
| 98 |
joblib.dump(scaler, SCALER_PATH)
|
| 99 |
|
| 100 |
logger.info(f"β
Training complete. Accuracy: {acc:.4f}")
|
| 101 |
+
return {"status": "β
Success", "accuracy": round(acc, 4), "message": "Model saved."}
|
| 102 |
except Exception as e:
|
| 103 |
logger.error(f"β Train failed: {e}")
|
| 104 |
+
return {"status": "β Error", "message": str(e)[:150]}
|
| 105 |
|
| 106 |
def run_predict():
|
| 107 |
try:
|
|
|
|
| 110 |
|
| 111 |
X, y, df_full, feat_cols = fetch_and_prepare()
|
| 112 |
scaler = joblib.load(SCALER_PATH)
|
| 113 |
+
latest = df_full.iloc[-WINDOW:][feat_cols].values.flatten().reshape(1, -1)
|
| 114 |
latest_scaled = scaler.transform(latest)
|
| 115 |
|
| 116 |
model = joblib.load(MODEL_PATH)
|
| 117 |
pred = int(model.predict(latest_scaled)[0])
|
| 118 |
prob = float(model.predict_proba(latest_scaled)[0][1])
|
| 119 |
|
| 120 |
+
return {
|
| 121 |
+
"status": "β
Prediction",
|
| 122 |
+
"signal": "BUY (Hold 4h)" if pred == 1 else "HOLD/SELL",
|
| 123 |
+
"confidence_pct": f"{prob*100:.1f}%",
|
| 124 |
+
"logic": "Predicts >0.15% move next 4h"
|
| 125 |
+
}
|
| 126 |
except Exception as e:
|
| 127 |
logger.error(f"β Predict failed: {e}")
|
| 128 |
+
return {"status": "β Error", "message": str(e)[:150]}
|
| 129 |
|
| 130 |
+
# π¨ UI
|
|
|
|
| 131 |
tradingview_html = """
|
| 132 |
<div style="width:100%;height:400px;margin:10px 0;">
|
| 133 |
<iframe src="https://s.tradingview.com/widgetembed/?symbol=BINANCE%3AETHUSDT&interval=240&hidesidetoolbar=1&symboledit=1&saveimage=1&toolbarbg=transparent&studies=%5B%5D&theme=dark&style=1&timezone=UTC&withdateranges=1&showpopupbutton=1&studies_overrides=%7B%7D&overrides=%7B%7D&enabled_features=%5B%5D&disabled_features=%5B%5D&showpopupbutton=1&locale=en"
|
|
|
|
| 149 |
|
| 150 |
train_btn.click(run_train, outputs=train_out)
|
| 151 |
pred_btn.click(run_predict, outputs=pred_out)
|
| 152 |
+
gr.Markdown("π‘ *Model saved in `/tmp/`. Retrain if Space restarts.*")
|
| 153 |
|
|
|
|
| 154 |
demo.queue(default_concurrency_limit=1)
|
| 155 |
+
logger.info("β
App ready. HF Spaces runner will take over.")
|
|
|
|
|
|
|
|
|
|
|
|