GoshawkVortexAI commited on
Commit
cd0b125
·
verified ·
1 Parent(s): 59692d0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +170 -35
app.py CHANGED
@@ -26,7 +26,11 @@ import pandas as pd
26
  import plotly.graph_objects as go
27
  from mcp import ClientSession, StdioServerParameters
28
  from mcp.client.stdio import stdio_client
29
- from sklearn.ensemble import HistGradientBoostingClassifier
 
 
 
 
30
 
31
  def _launcher():
32
  """PATH'te uvx yoksa sırayla alternatifleri dener."""
@@ -123,61 +127,188 @@ def to_df(payload) -> pd.DataFrame:
123
 
124
  # ----------------------------------------------------------------- ML
125
  def make_features(df: pd.DataFrame):
126
- c, h, l, v = df["close"], df["high"], df["low"], df["volume"]
127
  f = pd.DataFrame(index=df.index)
128
- for n in (1, 3, 6, 12, 24):
 
 
 
129
  f[f"ret{n}"] = c.pct_change(n)
130
 
 
 
 
 
 
 
 
 
 
131
  d = c.diff()
132
- up = d.clip(lower=0).ewm(alpha=1 / 14, adjust=False).mean()
133
- dn = (-d.clip(upper=0)).ewm(alpha=1 / 14, adjust=False).mean()
134
- f["rsi"] = 100 - 100 / (1 + up / (dn + 1e-12))
 
135
 
136
  macd = c.ewm(span=12, adjust=False).mean() - c.ewm(span=26, adjust=False).mean()
137
  f["macd_h"] = (macd - macd.ewm(span=9, adjust=False).mean()) / c
138
- f["ema_dist"] = c / c.ewm(span=50, adjust=False).mean() - 1
 
 
139
 
140
  tr = pd.concat([h - l, (h - c.shift()).abs(), (l - c.shift()).abs()], axis=1).max(axis=1)
141
  atr = tr.ewm(alpha=1 / 14, adjust=False).mean()
142
  f["atr_pct"] = atr / c
143
  f["hl_range"] = (h - l) / c
144
- f["vol_z"] = (v - v.rolling(48).mean()) / (v.rolling(48).std() + 1e-12)
 
 
 
 
 
 
 
 
 
 
 
 
145
  f["bb_pos"] = (c - c.rolling(20).mean()) / (2 * c.rolling(20).std() + 1e-12)
146
- return f, atr
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
 
148
 
149
- def predict(df: pd.DataFrame, horizon: int):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  feats, atr = make_features(df)
151
  close = df["close"]
152
- y = (close.shift(-horizon) > close).astype(float)
153
- y[close.shift(-horizon).isna()] = np.nan
 
154
 
155
  data = feats.copy()
156
- data["y"] = y
157
- train_df = data.dropna()
158
- if len(train_df) < 200:
159
- raise ValueError("Yeterli veri yok (en az ~200 geçerli bar gerekir). Limit'i artır.")
160
-
161
- X, Y = train_df.drop(columns="y"), train_df["y"].astype(int)
162
- split = int(len(X) * 0.8)
163
- gap = horizon # sızıntıyı önlemek için train/test arası boşluk
164
- mk = lambda: HistGradientBoostingClassifier(max_depth=3, learning_rate=0.05,
165
- max_iter=200, l2_regularization=1.0, random_state=42)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
 
167
- m = mk().fit(X.iloc[:split - gap], Y.iloc[:split - gap])
168
- acc = float((m.predict(X.iloc[split:]) == Y.iloc[split:]).mean())
169
- base = float(max(Y.iloc[split:].mean(), 1 - Y.iloc[split:].mean()))
 
 
 
170
 
171
- final = mk().fit(X, Y)
172
- last = feats.iloc[[-1]].ffill().fillna(0)
173
- p_up = float(final.predict_proba(last)[0, 1])
174
 
175
  price, a = float(close.iloc[-1]), float(atr.iloc[-1])
176
  move = a * np.sqrt(horizon)
177
  center = price + (p_up - 0.5) * 2 * move
178
  return {
179
- "p_up": p_up, "acc": acc, "baseline": base, "price": price, "atr": a,
180
  "center": center, "low": center - move, "high": center + move,
 
181
  }
182
 
183
 
@@ -220,19 +351,23 @@ def run(symbol, timeframe, exchange, horizon, limit):
220
  traceback.print_exc()
221
  return f"**Hata:** {_root(e)}", None, ""
222
 
223
- p = res["p_up"]
224
- signal = "LONG" if p > 0.55 else "SHORT" if p < 0.45 else "NÖTR"
225
- edge = "zayıf (baseline'a yakın)" if res["acc"] < res["baseline"] + 0.02 else "var"
 
226
  md = f"""### {symbol} · {timeframe} · {int(horizon)} bar sonrası
227
  | | |
228
  |---|---|
229
  | Son fiyat | **{res['price']:.4f}** |
230
- | Yükseliş olasılığı | **{p:.1%}** → **{signal}** |
231
  | Tahmini merkez | {res['center']:.4f} |
232
  | Aralık (ATR·√h) | {res['low']:.4f} – {res['high']:.4f} |
233
- | Test doğruluğu | {res['acc']:.1%} (baseline {res['baseline']:.1%}) → model edge: {edge} |
 
 
 
234
 
235
- *Eğitim/araştırma amaçlıdır; yatırım tavsiyesi değildir.*"""
236
 
237
  # Grafik
238
  tail = df.tail(150)
 
26
  import plotly.graph_objects as go
27
  from mcp import ClientSession, StdioServerParameters
28
  from mcp.client.stdio import stdio_client
29
+ from sklearn.ensemble import ExtraTreesClassifier, HistGradientBoostingClassifier
30
+ from sklearn.linear_model import LogisticRegression
31
+ from sklearn.metrics import roc_auc_score
32
+ from sklearn.pipeline import make_pipeline
33
+ from sklearn.preprocessing import StandardScaler
34
 
35
  def _launcher():
36
  """PATH'te uvx yoksa sırayla alternatifleri dener."""
 
127
 
128
  # ----------------------------------------------------------------- ML
129
  def make_features(df: pd.DataFrame):
130
+ c, h, l, o, v = df["close"], df["high"], df["low"], df["open"], df["volume"]
131
  f = pd.DataFrame(index=df.index)
132
+ lr = np.log(c).diff()
133
+
134
+ # Getiri / momentum (çok ölçekli)
135
+ for n in (1, 2, 3, 6, 12, 24, 48):
136
  f[f"ret{n}"] = c.pct_change(n)
137
 
138
+ # Volatilite rejimi
139
+ for n in (6, 24, 96):
140
+ f[f"vol{n}"] = lr.rolling(n).std()
141
+ f["vol_ratio"] = f["vol6"] / (f["vol96"] + 1e-12)
142
+ f["vol_ratio2"] = f["vol24"] / (f["vol96"] + 1e-12)
143
+ f["ret24_z"] = f["ret24"] / (f["vol24"] * np.sqrt(24) + 1e-12) # volatilite-normalize momentum
144
+ f["ret6_z"] = f["ret6"] / (f["vol24"] * np.sqrt(6) + 1e-12)
145
+
146
+ # RSI (iki periyot)
147
  d = c.diff()
148
+ for n in (7, 14):
149
+ up = d.clip(lower=0).ewm(alpha=1 / n, adjust=False).mean()
150
+ dn = (-d.clip(upper=0)).ewm(alpha=1 / n, adjust=False).mean()
151
+ f[f"rsi{n}"] = 100 - 100 / (1 + up / (dn + 1e-12))
152
 
153
  macd = c.ewm(span=12, adjust=False).mean() - c.ewm(span=26, adjust=False).mean()
154
  f["macd_h"] = (macd - macd.ewm(span=9, adjust=False).mean()) / c
155
+ for n in (20, 50, 200):
156
+ f[f"ema_dist{n}"] = c / c.ewm(span=n, adjust=False).mean() - 1
157
+ f["ema_slope"] = c.ewm(span=20, adjust=False).mean().pct_change(5)
158
 
159
  tr = pd.concat([h - l, (h - c.shift()).abs(), (l - c.shift()).abs()], axis=1).max(axis=1)
160
  atr = tr.ewm(alpha=1 / 14, adjust=False).mean()
161
  f["atr_pct"] = atr / c
162
  f["hl_range"] = (h - l) / c
163
+
164
+ # ADX (trend gücü)
165
+ upm, dnm = h.diff(), -l.diff()
166
+ pdm = ((upm > dnm) & (upm > 0)) * upm
167
+ mdm = ((dnm > upm) & (dnm > 0)) * dnm
168
+ pdi = 100 * pdm.ewm(alpha=1 / 14, adjust=False).mean() / (atr + 1e-12)
169
+ mdi = 100 * mdm.ewm(alpha=1 / 14, adjust=False).mean() / (atr + 1e-12)
170
+ f["adx"] = (100 * (pdi - mdi).abs() / (pdi + mdi + 1e-12)).ewm(alpha=1 / 14, adjust=False).mean()
171
+ f["di_diff"] = pdi - mdi
172
+
173
+ # Stokastik + Bollinger + kanal konumu
174
+ ll, hh = l.rolling(14).min(), h.rolling(14).max()
175
+ f["stoch"] = (c - ll) / (hh - ll + 1e-12)
176
  f["bb_pos"] = (c - c.rolling(20).mean()) / (2 * c.rolling(20).std() + 1e-12)
177
+ f["bb_width"] = 4 * c.rolling(20).std() / c.rolling(20).mean()
178
+ f["dist_hi50"] = c / h.rolling(50).max() - 1
179
+ f["dist_lo50"] = c / l.rolling(50).min() - 1
180
+
181
+ # Mum yapısı
182
+ rng = (h - l) + 1e-12
183
+ f["body"] = (c - o) / rng
184
+ f["upper_wick"] = (h - np.maximum(o, c)) / rng
185
+ f["lower_wick"] = (np.minimum(o, c) - l) / rng
186
+ f["close_loc"] = (c - l) / rng
187
+
188
+ # Hacim
189
+ f["vol_z"] = (v - v.rolling(48).mean()) / (v.rolling(48).std() + 1e-12)
190
+ f["vol_trend"] = v.rolling(6).mean() / (v.rolling(48).mean() + 1e-12)
191
+ f["signed_vol"] = (np.sign(d) * v).rolling(12).sum() / (v.rolling(12).sum() + 1e-12) # OBV benzeri baskı
192
+ f["mfi_like"] = ((c - o) / rng * v).rolling(14).sum() / (v.rolling(14).sum() + 1e-12)
193
+
194
+ # İstatistiksel (otokorelasyon, çarpıklık)
195
+ f["autocorr"] = lr.rolling(48).corr(lr.shift(1))
196
+ f["skew48"] = lr.rolling(48).skew()
197
+
198
+ # Seans / zaman (döngüsel)
199
+ ts = df["ts"]
200
+ f["hour_sin"] = np.sin(2 * np.pi * ts.dt.hour / 24)
201
+ f["hour_cos"] = np.cos(2 * np.pi * ts.dt.hour / 24)
202
+ f["dow_sin"] = np.sin(2 * np.pi * ts.dt.dayofweek / 7)
203
+ f["dow_cos"] = np.cos(2 * np.pi * ts.dt.dayofweek / 7)
204
+ return f.replace([np.inf, -np.inf], np.nan), atr
205
 
206
 
207
+ def _models():
208
+ return {
209
+ "hgb": HistGradientBoostingClassifier(max_depth=3, learning_rate=0.04, max_iter=250,
210
+ l2_regularization=2.0, min_samples_leaf=40,
211
+ random_state=42),
212
+ "et": ExtraTreesClassifier(n_estimators=300, max_depth=6, min_samples_leaf=30,
213
+ max_features=0.5, n_jobs=-1, random_state=42),
214
+ "lr": make_pipeline(StandardScaler(), LogisticRegression(C=0.05, max_iter=500)),
215
+ }
216
+
217
+
218
+ def _fit_predict(Xtr, ytr, Xte, w=None):
219
+ """Ensemble: HGB + ExtraTrees + Logistic -> olasılık ortalaması."""
220
+ ps = []
221
+ for name, m in _models().items():
222
+ if name == "lr":
223
+ m.fit(Xtr.fillna(0), ytr, logisticregression__sample_weight=w)
224
+ ps.append(m.predict_proba(Xte.fillna(0))[:, 1])
225
+ elif name == "et":
226
+ m.fit(Xtr.fillna(0), ytr, sample_weight=w)
227
+ ps.append(m.predict_proba(Xte.fillna(0))[:, 1])
228
+ else:
229
+ m.fit(Xtr, ytr, sample_weight=w)
230
+ ps.append(m.predict_proba(Xte)[:, 1])
231
+ return np.mean(ps, axis=0)
232
+
233
+
234
+ def _backtest(p, fwd, horizon, thr, fee):
235
+ """Örtüşmeyen (her `horizon` barda bir) işlemlerle net getiri istatistikleri."""
236
+ p, fwd = p[::horizon], fwd[::horizon]
237
+ pos = np.where(p > thr, 1, np.where(p < 1 - thr, -1, 0))
238
+ pnl = pos * fwd - np.abs(pos) * fee
239
+ n = int((pos != 0).sum())
240
+ if n < 20:
241
+ return {"n": n, "mean": 0.0, "t": 0.0, "hit": np.nan, "pf": np.nan}
242
+ tr = pnl[pos != 0]
243
+ gp, gl = tr[tr > 0].sum(), -tr[tr < 0].sum()
244
+ return {"n": n, "mean": float(tr.mean()),
245
+ "t": float(tr.mean() / (tr.std() / np.sqrt(n) + 1e-12)),
246
+ "hit": float((tr > 0).mean()), "pf": float(gp / gl) if gl > 0 else np.inf}
247
+
248
+
249
+ def predict(df: pd.DataFrame, horizon: int, fee: float = 0.0006, n_folds: int = 5):
250
  feats, atr = make_features(df)
251
  close = df["close"]
252
+ fwd = close.shift(-horizon) / close - 1
253
+ y = (fwd > 0).astype(float)
254
+ y[fwd.isna()] = np.nan
255
 
256
  data = feats.copy()
257
+ data["y"], data["fwd"] = y, fwd
258
+ data["thr_move"] = (atr / close) * np.sqrt(horizon)
259
+ train_df = data.dropna(subset=list(feats.columns[feats.notna().any()]) + ["y"]).copy()
260
+ train_df = train_df.dropna()
261
+ if len(train_df) < 400:
262
+ raise ValueError("Yeterli veri yok (en az ~400 geçerli bar gerekir). Limit'i artır.")
263
+
264
+ cols = list(feats.columns)
265
+ X, Y = train_df[cols], train_df["y"].astype(int)
266
+ F, TM = train_df["fwd"].values, train_df["thr_move"].values
267
+ # Gürültü filtresi: çok küçük hareketleri (≈ yazı-tura) eğitimde ağırlıksız bırak
268
+ # ve büyük hareketlere daha fazla ağırlık ver.
269
+ W = np.clip(np.abs(F) / (TM + 1e-12), 0.05, 3.0)
270
+
271
+ # --- Purged walk-forward (expanding window) ---
272
+ n = len(X)
273
+ start = int(n * 0.4)
274
+ edges = np.linspace(start, n, n_folds + 1).astype(int)
275
+ oos_p, oos_idx = [], []
276
+ for k in range(n_folds):
277
+ tr_end, te0, te1 = edges[k] - horizon, edges[k], edges[k + 1]
278
+ if tr_end < 200:
279
+ continue
280
+ p = _fit_predict(X.iloc[:tr_end], Y.iloc[:tr_end], X.iloc[te0:te1], W[:tr_end])
281
+ oos_p.append(p)
282
+ oos_idx.extend(range(te0, te1))
283
+ oos_p = np.concatenate(oos_p)
284
+ oos_idx = np.array(oos_idx)
285
+ yo, fo = Y.values[oos_idx], F[oos_idx]
286
+
287
+ acc = float(((oos_p > 0.5).astype(int) == yo).mean())
288
+ base = float(max(yo.mean(), 1 - yo.mean()))
289
+ try:
290
+ auc = float(roc_auc_score(yo, oos_p))
291
+ except ValueError:
292
+ auc = 0.5
293
 
294
+ # Eşik taraması (yalnızca OOS üzerinde, işlem sayısı ≥ 30 şartıyla)
295
+ best = {"thr": 0.55, **_backtest(oos_p, fo, horizon, 0.55, fee)}
296
+ for thr in (0.52, 0.54, 0.56, 0.58, 0.60, 0.62):
297
+ r = _backtest(oos_p, fo, horizon, thr, fee)
298
+ if r["n"] >= 30 and r["mean"] > best["mean"]:
299
+ best = {"thr": thr, **r}
300
 
301
+ # Son model: tüm veriyle, canlı tahmin
302
+ final_p = _fit_predict(X, Y, feats.iloc[[-1]][cols].ffill().fillna(0), W)
303
+ p_up = float(final_p[0])
304
 
305
  price, a = float(close.iloc[-1]), float(atr.iloc[-1])
306
  move = a * np.sqrt(horizon)
307
  center = price + (p_up - 0.5) * 2 * move
308
  return {
309
+ "p_up": p_up, "acc": acc, "baseline": base, "auc": auc, "price": price, "atr": a,
310
  "center": center, "low": center - move, "high": center + move,
311
+ "thr": best["thr"], "bt": best, "n_oos": len(oos_p), "fee": fee,
312
  }
313
 
314
 
 
351
  traceback.print_exc()
352
  return f"**Hata:** {_root(e)}", None, ""
353
 
354
+ p, thr, bt = res["p_up"], res["thr"], res["bt"]
355
+ signal = "LONG" if p > thr else "SHORT" if p < 1 - thr else "NÖTR"
356
+ has_edge = res["auc"] > 0.52 and bt["mean"] > 0 and bt["t"] > 1.5 and bt["n"] >= 30
357
+ edge = "✅ var (OOS, ücret sonrası)" if has_edge else "⚠️ kanıtlanamadı"
358
  md = f"""### {symbol} · {timeframe} · {int(horizon)} bar sonrası
359
  | | |
360
  |---|---|
361
  | Son fiyat | **{res['price']:.4f}** |
362
+ | Yükseliş olasılığı | **{p:.1%}** → **{signal}** (eşik {thr:.2f}) |
363
  | Tahmini merkez | {res['center']:.4f} |
364
  | Aralık (ATR·√h) | {res['low']:.4f} – {res['high']:.4f} |
365
+ | OOS AUC / doğruluk | {res['auc']:.3f} / {res['acc']:.1%} (baseline {res['baseline']:.1%}) |
366
+ | OOS işlem sayısı | {bt['n']} (hit {bt['hit']:.1%}, PF {bt['pf']:.2f}) |
367
+ | Ort. net getiri / işlem | {bt['mean']:.3%} (t = {bt['t']:.2f}, ücret {res['fee']:.2%}/işlem) |
368
+ | Model edge | {edge} |
369
 
370
+ *Walk-forward (purged) out-of-sample sonuçlar. Eşik OOS'ta seçildiği için hafif iyimser olabilir. Eğitim/araştırma amaçlıdır; yatırım tavsiyesi değildir.*"""
371
 
372
  # Grafik
373
  tail = df.tail(150)