Spaces:
Sleeping
Sleeping
Update app.py
Browse files
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 |
-
|
|
|
|
|
|
|
|
|
|
| 129 |
f[f"ret{n}"] = c.pct_change(n)
|
| 130 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
d = c.diff()
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
f["bb_pos"] = (c - c.rolling(20).mean()) / (2 * c.rolling(20).std() + 1e-12)
|
| 146 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
|
| 148 |
|
| 149 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
feats, atr = make_features(df)
|
| 151 |
close = df["close"]
|
| 152 |
-
|
| 153 |
-
y
|
|
|
|
| 154 |
|
| 155 |
data = feats.copy()
|
| 156 |
-
data["y"] = y
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
|
|
|
|
|
|
|
|
|
| 170 |
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
p_up = float(
|
| 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 >
|
| 225 |
-
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
| 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)
|