GoshawkVortexAI commited on
Commit
151f1a9
·
verified ·
1 Parent(s): bb400f7

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +329 -0
app.py ADDED
@@ -0,0 +1,329 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py
2
+ # BTC/USD Multi-Model AI Radar: 7 ML models + Groq explanation
3
+ # Gradio app for Hugging Face Spaces.
4
+ # Required packages: gradio, pandas, numpy, scikit-learn, xgboost, groq
5
+
6
+ import os
7
+ import warnings
8
+ import numpy as np
9
+ import pandas as pd
10
+ import gradio as gr
11
+
12
+ from sklearn.preprocessing import StandardScaler
13
+ from sklearn.pipeline import Pipeline
14
+ from sklearn.impute import SimpleImputer
15
+ from sklearn.neighbors import KNeighborsClassifier
16
+ from sklearn.ensemble import (
17
+ RandomForestClassifier,
18
+ ExtraTreesClassifier,
19
+ HistGradientBoostingClassifier,
20
+ GradientBoostingClassifier,
21
+ VotingClassifier,
22
+ )
23
+ from sklearn.linear_model import LogisticRegression
24
+ from sklearn.svm import SVC
25
+ from sklearn.metrics import accuracy_score, balanced_accuracy_score, log_loss
26
+ from sklearn.model_selection import TimeSeriesSplit
27
+ from xgboost import XGBClassifier
28
+
29
+ try:
30
+ from groq import Groq
31
+ except Exception:
32
+ Groq = None
33
+
34
+ warnings.filterwarnings("ignore")
35
+ CSV_PATH = os.environ.get("BTC_CSV_PATH", "BTCUSD_M30.csv")
36
+ HORIZON = 1
37
+ MIN_ROWS = 120
38
+
39
+ api_key = os.environ.get("GROQ_API_KEY")
40
+ client = Groq(api_key=api_key) if (api_key and Groq) else None
41
+
42
+
43
+ def load_local_csv(path=CSV_PATH):
44
+ """Reads headerless or headed OHLCV CSV/TSV."""
45
+ if not os.path.exists(path):
46
+ return None, f"CSV bulunamadı: {path}"
47
+
48
+ try:
49
+ # Try common separators and both header formats.
50
+ raw = None
51
+ for sep in ("\t", ",", ";"):
52
+ for header in (None, 0):
53
+ try:
54
+ candidate = pd.read_csv(path, sep=sep, header=header)
55
+ if candidate.shape[1] >= 6:
56
+ raw = candidate
57
+ break
58
+ except Exception:
59
+ pass
60
+ if raw is not None:
61
+ break
62
+
63
+ if raw is None or raw.shape[1] < 6:
64
+ return None, "CSV en az 6 sütun içermeli: date, open, high, low, close, volume."
65
+
66
+ raw = raw.iloc[:, :6].copy()
67
+ raw.columns = ["date", "open", "high", "low", "close", "volume"]
68
+
69
+ # If the first row was actually a header, remove it after numeric conversion.
70
+ for col in ["open", "high", "low", "close", "volume"]:
71
+ raw[col] = pd.to_numeric(raw[col], errors="coerce")
72
+
73
+ raw["date"] = raw["date"].astype(str)
74
+ raw = raw.dropna(subset=["open", "high", "low", "close", "volume"])
75
+ raw = raw[(raw["close"] > 0) & (raw["high"] > 0) & (raw["low"] > 0)]
76
+ raw = raw.sort_values("date").drop_duplicates("date").reset_index(drop=True)
77
+
78
+ if len(raw) < MIN_ROWS:
79
+ return None, f"En az {MIN_ROWS} geçerli mum gerekli; bulunan: {len(raw)}."
80
+ return raw, None
81
+ except Exception as exc:
82
+ return None, f"CSV okuma hatası: {exc}"
83
+
84
+
85
+ def rsi(series, period=14):
86
+ delta = series.diff()
87
+ gain = delta.clip(lower=0).ewm(alpha=1 / period, adjust=False).mean()
88
+ loss = (-delta.clip(upper=0)).ewm(alpha=1 / period, adjust=False).mean()
89
+ rs = gain / loss.replace(0, np.nan)
90
+ return 100 - (100 / (1 + rs))
91
+
92
+
93
+ def make_features(raw):
94
+ d = raw.copy()
95
+ close = d["close"]
96
+ d["ret_1"] = close.pct_change()
97
+ d["ret_2"] = close.pct_change(2)
98
+ d["ret_3"] = close.pct_change(3)
99
+ d["ret_6"] = close.pct_change(6)
100
+ d["ret_12"] = close.pct_change(12)
101
+ d["range_pct"] = (d["high"] - d["low"]) / close
102
+ d["body_pct"] = (d["close"] - d["open"]) / d["open"]
103
+ d["upper_wick"] = (d["high"] - d[["open", "close"]].max(axis=1)) / close
104
+ d["lower_wick"] = (d[["open", "close"]].min(axis=1) - d["low"]) / close
105
+
106
+ for window in (5, 10, 20, 50):
107
+ ma = close.rolling(window).mean()
108
+ d[f"ma_gap_{window}"] = close / ma - 1
109
+ d[f"volatility_{window}"] = d["ret_1"].rolling(window).std()
110
+
111
+ d["rsi_14"] = rsi(close, 14)
112
+ ema12 = close.ewm(span=12, adjust=False).mean()
113
+ ema26 = close.ewm(span=26, adjust=False).mean()
114
+ d["macd_pct"] = (ema12 - ema26) / close
115
+ d["macd_signal_pct"] = (d["macd_pct"].ewm(span=9, adjust=False).mean())
116
+ d["volume_change"] = d["volume"].pct_change()
117
+ d["volume_z20"] = (
118
+ (d["volume"] - d["volume"].rolling(20).mean()) /
119
+ d["volume"].rolling(20).std()
120
+ )
121
+ d["target"] = (close.shift(-HORIZON) > close).astype(int)
122
+ # Last row has no known future label; exclude it from training.
123
+ d = d.iloc[:-HORIZON].replace([np.inf, -np.inf], np.nan)
124
+ feature_cols = [
125
+ c for c in d.columns
126
+ if c not in ["date", "open", "high", "low", "close", "volume", "target"]
127
+ ]
128
+ d = d.dropna(subset=feature_cols + ["target"]).reset_index(drop=True)
129
+ return d, feature_cols
130
+
131
+
132
+ def build_models():
133
+ # Seven independent classifiers. Scaling is included where distance/margin
134
+ # methods need it; tree models use the original feature scale.
135
+ return {
136
+ "XGBoost": XGBClassifier(
137
+ n_estimators=180, max_depth=3, learning_rate=0.035,
138
+ subsample=0.85, colsample_bytree=0.85, reg_lambda=2,
139
+ objective="binary:logistic", eval_metric="logloss",
140
+ random_state=42, n_jobs=-1
141
+ ),
142
+ "KNN": Pipeline([
143
+ ("imputer", SimpleImputer(strategy="median")),
144
+ ("scaler", StandardScaler()),
145
+ ("model", KNeighborsClassifier(n_neighbors=9, weights="distance"))
146
+ ]),
147
+ "Random Forest": RandomForestClassifier(
148
+ n_estimators=220, max_depth=7, min_samples_leaf=4,
149
+ class_weight="balanced_subsample", random_state=42, n_jobs=-1
150
+ ),
151
+ "Extra Trees": ExtraTreesClassifier(
152
+ n_estimators=220, max_depth=8, min_samples_leaf=4,
153
+ class_weight="balanced", random_state=43, n_jobs=-1
154
+ ),
155
+ "Hist Gradient Boosting": HistGradientBoostingClassifier(
156
+ max_iter=160, learning_rate=0.05, max_leaf_nodes=15,
157
+ l2_regularization=1.0, random_state=44
158
+ ),
159
+ "Logistic Regression": Pipeline([
160
+ ("imputer", SimpleImputer(strategy="median")),
161
+ ("scaler", StandardScaler()),
162
+ ("model", LogisticRegression(C=0.5, max_iter=1500, class_weight="balanced"))
163
+ ]),
164
+ "SVM": Pipeline([
165
+ ("imputer", SimpleImputer(strategy="median")),
166
+ ("scaler", StandardScaler()),
167
+ ("model", SVC(C=1.0, kernel="rbf", probability=True,
168
+ class_weight="balanced", random_state=45))
169
+ ]),
170
+ }
171
+
172
+
173
+ def fit_and_evaluate(data, features):
174
+ X = data[features].astype(float)
175
+ y = data["target"].astype(int)
176
+
177
+ # Chronological holdout: no random shuffle and no future-to-past leakage.
178
+ split = max(int(len(data) * 0.8), 50)
179
+ split = min(split, len(data) - 20)
180
+ X_train, X_test = X.iloc[:split], X.iloc[split:]
181
+ y_train, y_test = y.iloc[:split], y.iloc[split:]
182
+
183
+ models = build_models()
184
+ trained = {}
185
+ results = {}
186
+ latest_x = X.iloc[[-1]]
187
+
188
+ for name, model in models.items():
189
+ try:
190
+ model.fit(X_train, y_train)
191
+ trained[name] = model
192
+ prob = model.predict_proba(X_test)[:, 1]
193
+ latest_prob = float(model.predict_proba(latest_x)[0, 1])
194
+ pred = (prob >= 0.5).astype(int)
195
+ results[name] = {
196
+ "up": latest_prob,
197
+ "accuracy": accuracy_score(y_test, pred) * 100,
198
+ "balanced": balanced_accuracy_score(y_test, pred) * 100,
199
+ "logloss": log_loss(y_test, np.clip(prob, 1e-6, 1 - 1e-6)),
200
+ "test_n": len(y_test),
201
+ }
202
+ except Exception as exc:
203
+ results[name] = {"error": str(exc)}
204
+
205
+ valid = {k: v for k, v in results.items() if "up" in v}
206
+ if len(valid) < 2:
207
+ raise RuntimeError("Ensemble için en az iki model başarıyla eğitilemedi.")
208
+
209
+ # Equal-weight ensemble keeps a weak/overfit holdout score from dominating.
210
+ ensemble_up = float(np.mean([v["up"] for v in valid.values()]))
211
+ return trained, results, ensemble_up, len(X_train), len(X_test)
212
+
213
+
214
+ def groq_comment(prompt):
215
+ if not client:
216
+ return "Groq açıklaması için GROQ_API_KEY gerekli. Model tahminleri yine çalışır."
217
+ try:
218
+ response = client.chat.completions.create(
219
+ model=os.environ.get("GROQ_MODEL", "openai/gpt-oss-120b"),
220
+ messages=[
221
+ {"role": "system", "content": "Türkçe konuşan, temkinli bir nicel piyasa analistisin. Kesinlik veya kâr garantisi verme. Verilmeyen veri uydurma."},
222
+ {"role": "user", "content": prompt}
223
+ ],
224
+ temperature=0.25,
225
+ max_tokens=1100
226
+ )
227
+ return response.choices[0].message.content
228
+ except Exception as exc:
229
+ return f"Groq API hatası: {exc}"
230
+
231
+
232
+ def analyze():
233
+ raw, error = load_local_csv()
234
+ if error:
235
+ return error, {}, pd.DataFrame()
236
+
237
+ try:
238
+ data, features = make_features(raw)
239
+ if len(data) < MIN_ROWS - 35:
240
+ return f"Göstergelerden sonra veri yetersiz: {len(data)} satır.", {}, pd.DataFrame()
241
+
242
+ trained, results, ensemble_up, train_n, test_n = fit_and_evaluate(data, features)
243
+ ensemble_down = 1 - ensemble_up
244
+ direction = "YUKARI 📈" if ensemble_up >= 0.5 else "AŞAĞI 📉"
245
+ confidence = max(ensemble_up, ensemble_down) * 100
246
+
247
+ rows = []
248
+ valid_probs = []
249
+ for name, item in results.items():
250
+ if "up" in item:
251
+ valid_probs.append(item["up"])
252
+ rows.append([
253
+ name, f"{item['up'] * 100:.1f}%",
254
+ f"{item['accuracy']:.1f}%",
255
+ f"{item['balanced']:.1f}%",
256
+ f"{item['logloss']:.4f}", item["test_n"]
257
+ ])
258
+ else:
259
+ rows.append([name, "Hata", "-", "-", item["error"][:80], "-"])
260
+
261
+ last = raw.iloc[-1]
262
+ model_table = pd.DataFrame(
263
+ rows,
264
+ columns=["Model", "Yükseliş olasılığı", "Test doğruluğu",
265
+ "Dengeli doğruluk", "Log-loss / hata", "Test mum"]
266
+ )
267
+
268
+ prompt = f"""
269
+ BTC/USD M30 analizini Türkçe hazırla.
270
+ Son kapanış: {float(last['close']):.2f} USD
271
+ Son tarih: {last['date']}
272
+ Ensemble yükseliş olasılığı: %{ensemble_up*100:.1f}
273
+ Ensemble düşüş olasılığı: %{ensemble_down*100:.1f}
274
+ Yön: {direction}; yön güveni (olasılık): %{confidence:.1f}
275
+ Eğitim mum sayısı: {train_n}; kronolojik test mum sayısı: {test_n}.
276
+ Model sonuçları:
277
+ {model_table.to_string(index=False)}
278
+ Başlıklar:
279
+ 1. Çoklu model değerlendirmesi ve modeller arası uzlaşma/ayrışma
280
+ 2. Teknik görünüm
281
+ 3. Yukarı ve aşağı senaryolar
282
+ 4. Risk yönetimi ve belirsizlik
283
+ İşlem fiyatı, stop veya hedef seviyesi uydurma; OHLC verisinden hesaplanmayan seviyeleri açıkça belirt.
284
+ Test doğruluğunun gelecekteki başarı garantisi olmadığını belirt. Yatırım tavsiyesi değildir.
285
+ """
286
+ ai_text = groq_comment(prompt)
287
+ header = (
288
+ f"## BTC/USD Çoklu Model Radarı\n\n"
289
+ f"**Son kapanış:** ${float(last['close']):,.2f} | "
290
+ f"**Tarih:** {last['date']}\n\n"
291
+ f"**Ensemble:** {direction} | **Yükseliş:** %{ensemble_up*100:.1f} | "
292
+ f"**Düşüş:** %{ensemble_down*100:.1f}\n\n"
293
+ f"**Aktif model:** {len(valid_probs)}/7 | "
294
+ f"**Eğitim:** {train_n} mum | **Test:** {test_n} mum\n\n"
295
+ f"> Olasılık/güven skoru başarı oranı değildir. Test sonuçları geçmiş veriye aittir.\n\n"
296
+ f"---\n\n{ai_text}"
297
+ )
298
+ labels = {
299
+ name: float(item["up"])
300
+ for name, item in results.items() if "up" in item
301
+ }
302
+ labels["Ensemble"] = ensemble_up
303
+ return header, labels, model_table
304
+ except Exception as exc:
305
+ return f"Analiz hatası: {exc}", {}, pd.DataFrame()
306
+
307
+
308
+ with gr.Blocks(theme=gr.themes.Soft(primary_hue="orange", secondary_hue="slate")) as demo:
309
+ gr.Markdown(
310
+ f"""# ₿ BTC/USD Multi-Model AI Radar
311
+ **Veri:** `{CSV_PATH}` · **Zaman dilimi:** M30 · **Model sayısı:** 7 ML modeli + Groq AI açıklaması
312
+
313
+ CSV biçimi: `date, open, high, low, close, volume` (başlıklı/başlıksız CSV veya TSV).
314
+ """
315
+ )
316
+ with gr.Row():
317
+ run_btn = gr.Button("📊 CSV'yi Oku ve 7 Modeli Eğit", variant="primary", size="lg")
318
+ prediction = gr.Label(label="Model yükseliş olasılıkları")
319
+ report = gr.Markdown("Analizi başlatmak için butona basın.")
320
+ table = gr.Dataframe(label="Model karşılaştırması — kronolojik holdout testi",
321
+ interactive=False, wrap=True)
322
+ run_btn.click(fn=analyze, inputs=[], outputs=[report, prediction, table])
323
+ gr.Markdown(
324
+ "**Not:** Modeller aynı CSV üzerinde eğitilir. Test bölümü kronolojik olarak ayrılır; "
325
+ "bu bir canlı emir/broker bağlantısı değildir. Geçmiş test performansı geleceği garanti etmez."
326
+ )
327
+
328
+ if __name__ == "__main__":
329
+ demo.launch()