GoshawkVortexAI commited on
Commit
4a26eb2
·
verified ·
1 Parent(s): a0044f2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1019 -936
app.py CHANGED
@@ -1,985 +1,1068 @@
1
- """
2
- Tickscope MCP + ML tahmin uygulaması (Gradio)
3
-
4
- Kurulum:
5
- pip install gradio mcp pandas numpy scikit-learn plotly
6
- # uv gerekli (uvx tickscope-mcp için): pip install uv veya https://docs.astral.sh/uv/
7
- Çalıştırma:
8
- python app.py
9
-
10
- Akış:
11
- 1) uvx tickscope-mcp'yi stdio MCP sunucusu olarak başlatır
12
- 2) get_ohlcv ile mum verisini çeker
13
- 3) compute_indicators / deep_analyze çıktısını (varsa) Tickscope'tan alır
14
- 4) Yerel HistGradientBoosting modeli ile N bar sonrası yön olasılığı + aralık tahmini üretir
15
-
16
- Not: Eğitim/araştırma amaçlıdır, yatırım tavsiyesi değildir.
17
- """
18
 
19
- import asyncio
20
- import json
21
- import math
22
- import os
23
 
24
- import gradio as gr
25
  import numpy as np
26
  import pandas as pd
27
- import plotly.graph_objects as go
28
- from mcp import ClientSession, StdioServerParameters
29
- from mcp.client.stdio import stdio_client
30
- from sklearn.ensemble import ExtraTreesClassifier, HistGradientBoostingClassifier
 
 
 
31
  from sklearn.linear_model import LogisticRegression
32
- from sklearn.metrics import roc_auc_score
33
- from sklearn.pipeline import make_pipeline
34
  from sklearn.preprocessing import StandardScaler
35
-
36
- def _launcher():
37
- """PATH'te uvx yoksa sırayla alternatifleri dener."""
38
- import importlib.util
39
- import shutil
40
- import sys
41
-
42
- if shutil.which("uvx"):
43
- return "uvx", ["tickscope-mcp"]
44
- if shutil.which("tickscope-mcp"): # pip install tickscope-mcp
45
- return "tickscope-mcp", []
46
- if importlib.util.find_spec("uv"): # pip install uv -> python -m uv
47
- return sys.executable, ["-m", "uv", "tool", "run", "tickscope-mcp"]
48
- raise RuntimeError("Tickscope başlatılamadı. Çalıştır: pip install tickscope-mcp")
49
-
50
-
51
- _CMD, _ARGS = _launcher()
52
-
53
- SERVER = StdioServerParameters(
54
- command=_CMD,
55
- args=_ARGS,
56
- env={
57
- **os.environ,
58
- "TICKSCOPE_EXCHANGES": "binance,bybit,okx",
59
- "TICKSCOPE_DEFAULT_EXCHANGE": "binance",
60
- },
61
  )
62
 
63
- # Tool şemasındaki argüman adları sürümler arası değişebilir; şemaya bakıp eşleştiriyoruz.
64
- ALIASES = {
65
- "symbol": ["symbol", "pair"],
66
- "timeframe": ["timeframe", "interval", "tf", "period"],
67
- "exchange": ["exchange"],
68
- "limit": ["limit", "count", "n", "bars"],
69
- "until": ["until", "end", "end_time", "endTime", "to", "before"],
70
- "include_series": ["include_series", "includeSeries", "series", "with_series"],
71
- }
72
-
73
- EXTRA_TOOLS = ["detect_divergence", "detect_patterns", "analyze_structure",
74
- "find_support_resistance", "detect_cross", "get_orderbook",
75
- "get_ticker", "get_recent_trades", "server_status"]
76
-
77
- TF_MS = {"1m": 60_000, "5m": 300_000, "15m": 900_000, "1h": 3_600_000,
78
- "4h": 14_400_000, "1d": 86_400_000}
79
-
80
-
81
- # ----------------------------------------------------------------- MCP yardımcıları
82
- def build_args(tool, **wanted):
83
- props = (tool.inputSchema or {}).get("properties", {})
84
- out = {}
85
- for key, val in wanted.items():
86
- if val in (None, ""):
87
- continue
88
- for name in ALIASES.get(key, [key]):
89
- if name in props:
90
- out[name] = val
91
- break
92
- return out
93
-
94
-
95
- async def call_tool(session, tools, name, **wanted):
96
- tool = tools.get(name)
97
- if tool is None:
98
- return None
99
- res = await session.call_tool(name, build_args(tool, **wanted))
100
- text = "\n".join(getattr(c, "text", "") for c in res.content)
101
- if res.isError:
102
- return {"error": text}
103
- if getattr(res, "structuredContent", None):
104
- return res.structuredContent
105
- try:
106
- return json.loads(text)
107
- except Exception:
108
- return {"text": text}
109
-
110
-
111
- async def fetch_ohlcv(session, tools, symbol, timeframe, exchange, total):
112
- """Borsa sayfa limiti (~1000) yüzünden geriye doğru sayfalayarak `total` muma kadar çeker.
113
- Tool bitiş-zamanı parametresi desteklemiyorsa ilk sayfayla yetinir."""
114
- tool = tools.get("get_ohlcv")
115
- if tool is None:
116
- return None
117
- page = min(1000, total)
118
- first = await call_tool(session, tools, "get_ohlcv", symbol=symbol,
119
- timeframe=timeframe, exchange=exchange, limit=page)
120
- if first is None or (isinstance(first, dict) and "error" in first) or total <= page:
121
- return first
122
- props = (tool.inputSchema or {}).get("properties", {})
123
- if not any(n in props for n in ALIASES["until"]):
124
- return first # sayfalama desteklenmiyor
125
- try:
126
- frames = [to_df(first)]
127
- except Exception:
128
- return first
129
- got = len(frames[0])
130
- epoch = pd.Timestamp("1970-01-01")
131
- while got < total:
132
- oldest = frames[-1]["ts"].iloc[0]
133
- end_ms = int((oldest - epoch) // pd.Timedelta("1ms")) - 1
134
- try:
135
- res = await call_tool(session, tools, "get_ohlcv", symbol=symbol, timeframe=timeframe,
136
- exchange=exchange, limit=page, until=end_ms)
137
- except Exception: # noqa: BLE001 (ör. Connection closed) -> eldeki sayfalarla devam
138
- break
139
- if res is None or (isinstance(res, dict) and "error" in res):
140
  break
141
- try:
142
- d = to_df(res)
143
- except Exception:
 
 
 
 
144
  break
145
- d = d[d["ts"] < oldest]
146
- if d.empty:
 
 
147
  break
148
- frames.append(d)
149
- got += len(d)
150
- out = pd.concat(frames[::-1]).drop_duplicates("ts").sort_values("ts").tail(total)
151
- return out.to_dict("records")
152
-
153
-
154
- # ----------------------------------------------------------------- 2. MCP: LiquidVision (türev verisi)
155
- # Uzak (HTTP) MCP, kurulum yok: https://liquidvision.app/mcp
156
- # - Anahtarsız: yalnızca "sonuç" araçları (brief, positioning regime, cascades...) -> canlı bağlam.
157
- # - Funding / OI GEÇMİŞİ (eğitim özelliği) ham-satır araçlarıdır; Pro/Bot anahtarı ister:
158
- # LIQUIDVISION_API_KEY=lv_... (X-API-Key olarak gönderilir)
159
- DERIV_URL = os.environ.get("LIQUIDVISION_URL", "https://liquidvision.app/mcp")
160
- DERIV_KEY = os.environ.get("LIQUIDVISION_API_KEY", "")
161
- LIVE_TOOLS = ["get_market_brief", "get_positioning_regime", "get_liquidation_cascades",
162
- "get_microstructure", "get_funding_arbitrage", "get_funding_dispersion"]
163
-
164
-
165
- def _usdt(symbol):
166
- """BTC/USDT veya BTC/USDT:USDT -> BTCUSDT"""
167
- return symbol.strip().upper().split(":")[0].replace("/", "")
168
-
169
-
170
- def _find_rows(o):
171
- if isinstance(o, list) and len(o) >= 5 and all(isinstance(x, dict) for x in o[:3]):
172
- return o
173
- if isinstance(o, dict):
174
- for v in o.values():
175
- r = _find_rows(v)
176
- if r:
177
- return r
178
- return None
179
-
180
-
181
- def parse_series(payload, vkeys):
182
- """Geçmiş çıktısında (şemadan bağımsız) zaman + değer kolonlarını bulup ts/v tablosu döndürür."""
183
- rows = _find_rows(payload)
184
- if not rows:
185
- return None
186
- d = pd.DataFrame(rows)
187
- tcol = next((c for c in ("timestamp", "time", "ts", "t", "datetime", "fundingTime", "ts_ms", "hour", "bucket")
188
- if c in d), None)
189
- vcol = next((c for c in vkeys if c in d), None)
190
- if tcol is None or vcol is None:
191
- return None
192
- ts = d[tcol]
193
- if pd.api.types.is_numeric_dtype(ts):
194
- ts = pd.to_datetime(ts, unit="ms" if ts.iloc[-1] > 1e11 else "s")
195
- else:
196
- ts = pd.to_datetime(ts)
197
- if getattr(ts.dt, "tz", None) is not None:
198
- ts = ts.dt.tz_convert(None)
199
- out = pd.DataFrame({"ts": ts.astype("datetime64[ns]"), "v": pd.to_numeric(d[vcol], errors="coerce")})
200
- return out.dropna().drop_duplicates("ts").sort_values("ts").reset_index(drop=True)
201
-
202
-
203
- def _funding_feats(f):
204
- r = f["v"]
205
- f["fund"] = r * 1e4 # baz puan
206
- f["fund_z"] = (r - r.rolling(30).mean()) / (r.rolling(30).std() + 1e-12)
207
- f["fund_chg"] = r.diff() * 1e4
208
- f["fund_ma9"] = r.rolling(9).mean() * 1e4
209
- return f.drop(columns=["v"]).dropna().reset_index(drop=True)
210
-
211
-
212
- def _oi_feats(f):
213
- o = f["v"]
214
- for k in (1, 4, 24):
215
- f[f"oi_chg{k}"] = o.pct_change(k)
216
- f["oi_z"] = (o - o.rolling(48).mean()) / (o.rolling(48).std() + 1e-12)
217
- return f.drop(columns=["v"]).replace([np.inf, -np.inf], np.nan).dropna().reset_index(drop=True)
218
-
219
-
220
- async def fetch_deriv_features(symbol, exchange, timeframe, df):
221
- """LiquidVision MCP'den canlı bağlam (ham çıktıya) + funding/OI geçmişi (özellik) çeker.
222
- Dönen: (özellik tablosu | None, df (geçmiş kapsamına kırpılmış olabilir), ham çıktı sözlüğü)."""
223
- extra = {}
224
- try:
225
- from mcp.client.streamable_http import streamablehttp_client
226
- except Exception as e: # noqa: BLE001
227
- extra["deriv_error"] = f"mcp sürümü streamable HTTP desteklemiyor: {_root(e)} (pip install -U mcp)"
228
- return None, df, extra
229
- headers = {"X-API-Key": DERIV_KEY} if DERIV_KEY else {}
230
- sym = _usdt(symbol)
231
- hist = {}
232
- try:
233
- async with streamablehttp_client(DERIV_URL, headers=headers) as (r, w, _):
234
- async with ClientSession(r, w) as session:
235
- await session.initialize()
236
- tools = {t.name: t for t in (await session.list_tools()).tools}
237
- extra["deriv_available_tools"] = list(tools)
238
- for name in LIVE_TOOLS:
239
- if name in tools:
240
- try:
241
- extra[f"deriv_{name}"] = await call_tool(
242
- session, tools, name, symbol=sym, exchange=exchange)
243
- except Exception as e: # noqa: BLE001
244
- extra[f"deriv_{name}"] = {"error": _root(e)}
245
- for name in ("get_funding_history", "get_open_interest_history"):
246
- if name in tools:
247
- try:
248
- hist[name] = await call_tool(session, tools, name, symbol=sym, exchange=exchange,
249
- timeframe=timeframe, limit=1000)
250
- except Exception as e: # noqa: BLE001
251
- hist[name] = {"error": _root(e)}
252
- except Exception as e: # noqa: BLE001
253
- extra["deriv_error"] = _root(e)
254
- return None, df, extra
255
-
256
- specs = {
257
- "get_funding_history": (("funding_rate", "fundingRate", "rate", "funding", "value"), _funding_feats),
258
- "get_open_interest_history": (("open_interest", "oi", "openInterest", "oi_usd", "value"), _oi_feats),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
259
  }
260
- feats = {}
261
- for name, (vkeys, fn) in specs.items():
262
- h = hist.get(name)
263
- if h is None:
264
- continue
265
- if isinstance(h, dict) and "error" in h:
266
- extra[f"deriv_{name}_error"] = _strip_series(h)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
  continue
268
- s = parse_series(h, vkeys)
269
- if s is None or len(s) < 80:
270
- extra[f"deriv_{name}_raw"] = _strip_series(h) # şema beklenenden farklı
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
271
  continue
272
- feats[name] = fn(s)
273
- if not feats:
274
- extra["deriv_note"] = ("Geçmiş özellik eklenmedi (ham-satır araçları için Pro/Bot anahtarı gerekir: "
275
- "LIQUIDVISION_API_KEY). Canlı bağlam ham çıktıda.")
276
- return None, df, extra
277
-
278
- # Geçmişin kapsamadığı eski mumları kırp; <800 bar kalıyorsa o seriyi at
279
- usable = {k: v for k, v in feats.items() if (df["ts"] >= v["ts"].iloc[0]).sum() >= 800}
280
- if not usable:
281
- extra["deriv_note"] = "Geçmiş mum verisinin yalnızca küçük kısmını kapsıyor (<800 bar); özellik eklenmedi"
282
- return None, df, extra
283
- start = max(v["ts"].iloc[0] for v in usable.values())
284
- d2 = df[df["ts"] >= start].reset_index(drop=True)
285
- d2["ts"] = d2["ts"].astype("datetime64[ns]")
286
- m = d2[["ts"]].copy()
287
- for v in usable.values():
288
- m = pd.merge_asof(m, v, on="ts", direction="backward") # lookahead yok
289
- m.index = d2.index
290
- extra["deriv_used"] = {"seriler": list(usable), "baslangic": str(start), "bar": len(d2)}
291
- return m.drop(columns=["ts"]).add_prefix("dv_"), d2, extra
292
-
293
-
294
- # compute_indicators'ın `indicators` parametresi ZORUNLUDUR ("rsi:14", "macd:12,26,9" biçiminde spec listesi).
295
- # 73 göstergenin adlarını bilmediğimiz için geniş bir aday listesi denenir; geçersiz adlar atlanır.
296
- IND_NAMES = [
297
- "sma", "ema", "wma", "hma", "vwma", "dema", "tema", "zlema", "kama", "alma", "rma", "smma", "lsma", "vwap",
298
- "bb", "bbands", "bollinger", "bbw", "bbp", "kc", "keltner", "donchian", "dc", "envelope", "ichimoku",
299
- "supertrend", "psar", "sar", "rsi", "stochrsi", "stoch", "macd", "ppo", "roc", "mom", "momentum", "cci",
300
- "willr", "williams", "ao", "awesome", "ac", "uo", "tsi", "trix", "cmo", "dpo", "kst", "coppock", "fisher",
301
- "rvi", "mfi", "obv", "cmf", "ad", "adl", "eom", "fi", "force", "vpt", "nvi", "pvi", "kvo", "atr", "natr",
302
- "tr", "stdev", "hv", "ulcer", "chop", "massindex", "adx", "dmi", "aroon", "vortex", "wavetrend", "wt",
303
- "squeeze", "ttm", "heikinashi", "ha", "pivot", "zigzag", "linreg", "zscore", "cog", "bop", "qstick",
304
- "pgo", "smi", "stc", "er", "efi",
305
- ]
306
 
 
 
 
307
 
308
- IND_CHUNK = 6
 
309
 
 
 
310
 
311
- async def fetch_indicators(symbol, timeframe, exchange, limit):
312
- """Göstergeleri KÜÇÜK PARÇALAR halinde (her parça birkaç gösterge), ana oturumdan AYRI bir Tickscope sürecinde çeker.
313
- Bir parça sunucuyu çökertirse ("Connection closed") yeni süreç açılır, o parçanın adları atlanır, kalanlarla devam edilir."""
314
- out = {"by_indicator": {}, "failed": [], "mode": "chunked", "reconnects": 0, "last_error": None}
315
- kw = dict(symbol=symbol, timeframe=timeframe, exchange=exchange, limit=limit, include_series=True)
316
- chunks = [IND_NAMES[i:i + IND_CHUNK] for i in range(0, len(IND_NAMES), IND_CHUNK)]
317
- i = 0
318
- while i < len(chunks):
319
- try:
320
- async with stdio_client(SERVER) as (r, w):
321
- async with ClientSession(r, w) as session:
322
- await session.initialize()
323
- tools = {t.name: t for t in (await session.list_tools()).tools}
324
- if "compute_indicators" not in tools:
325
- return None
326
- while i < len(chunks):
327
- names = chunks[i]
328
- res = await call_tool(session, tools, "compute_indicators", indicators=names, **kw)
329
- if isinstance(res, dict) and "error" not in res and extract_series(res, 0):
330
- out["by_indicator"][f"c{i}"] = res
331
- else: # parçada geçersiz ad var: tek tek dene
332
- for name in names:
333
- r1 = await call_tool(session, tools, "compute_indicators", indicators=[name], **kw)
334
- if r1 is None or (isinstance(r1, dict) and "error" in r1):
335
- out["failed"].append(name)
336
- else:
337
- out["by_indicator"][name] = r1
338
- i += 1
339
- except Exception as e: # noqa: BLE001
340
- out["reconnects"] += 1
341
- out["last_error"] = _root(e)
342
- out["failed"] += [n for n in chunks[i] if n not in out["by_indicator"]] # çökertene dokunma, atla
343
- i += 1
344
- if out["reconnects"] > 6:
345
- break
346
- return out
347
-
348
-
349
- def _numeric_list(o, min_len):
350
- return (isinstance(o, list) and len(o) >= min_len and
351
- all(x is None or (isinstance(x, (int, float)) and not isinstance(x, bool)) for x in o[:50]))
352
-
353
-
354
- def extract_series(payload, n):
355
- """compute_indicators çıktısında gezinip bar bazlı sayısal serileri bulur (şemadan bağımsız)."""
356
- min_len = max(50, int(n * 0.5))
357
- out = {}
358
-
359
- def walk(o, path):
360
- if isinstance(o, list):
361
- if _numeric_list(o, min_len):
362
- out[path] = o
363
- elif len(o) >= min_len and all(isinstance(x, dict) for x in o[:3]):
364
- for key in o[0]:
365
- vals = [r.get(key) if isinstance(r, dict) else None for r in o]
366
- if _numeric_list(vals, min_len) and key.lower() not in ("ts", "time", "timestamp", "t"):
367
- out[f"{path}.{key}"] = vals
368
- elif isinstance(o, dict):
369
- for k, v in o.items():
370
- walk(v, f"{path}.{k}" if path else str(k))
371
-
372
- walk(payload, "")
373
- return out
374
-
375
-
376
- def indicator_features(ind, df: pd.DataFrame):
377
- """Tickscope'un 73 göstergesinin zaman serilerini (varsa) df'e hizalı özellik tablosuna çevirir.
378
- Fiyat seviyesi gibi duran (SMA/EMA/bant vb.) seriler kapanışa göre normalize edilir."""
379
- if not isinstance(ind, dict) or "error" in ind:
380
- return None
381
- n = len(df)
382
- close = df["close"].values
383
- cols = {}
384
- for path, vals in extract_series(ind, n).items():
385
- s = pd.to_numeric(pd.Series(vals), errors="coerce").values.astype(float)
386
- arr = np.full(n, np.nan)
387
- m = min(len(s), n)
388
- arr[-m:] = s[-m:] # seriler sondan hizalanır
389
- if np.isnan(arr).all() or np.nanstd(arr) == 0:
390
  continue
391
- ref = np.nanmedian(close[-m:])
392
- if ref and abs(np.nanmedian(arr) / ref - 1) < 0.5: # fiyat seviyesi -> göreli mesafe
393
- arr = arr / close - 1
394
- name = "ts_" + "".join(ch if ch.isalnum() else "_" for ch in path).strip("_")
395
- cols[name] = arr
396
- if not cols:
397
- return None
398
- return pd.DataFrame(cols, index=df.index)
399
-
400
-
401
- def _strip_series(o, keep=3):
402
- """Ham çıktıyı okunur tutmak için uzun serileri son `keep` değere kısaltır."""
403
- if isinstance(o, list) and len(o) > 20:
404
- return [f"... {len(o)} değer, son {keep}:"] + o[-keep:]
405
- if isinstance(o, list):
406
- return [_strip_series(x, keep) for x in o]
407
- if isinstance(o, dict):
408
- return {k: _strip_series(v, keep) for k, v in o.items()}
409
- return o
410
-
411
-
412
- def to_df(payload) -> pd.DataFrame:
413
- rows = payload
414
- if isinstance(payload, dict):
415
- for k in ("candles", "ohlcv", "data", "rows", "result"):
416
- if k in payload:
417
- rows = payload[k]
418
- break
419
- if not isinstance(rows, list) or not rows:
420
- raise ValueError(f"OHLCV verisi okunamadı: {str(payload)[:300]}")
421
-
422
- if isinstance(rows[0], (list, tuple)):
423
- cols = ["ts", "open", "high", "low", "close", "volume"][: len(rows[0])]
424
- df = pd.DataFrame(rows, columns=cols)
425
- else:
426
- df = pd.DataFrame(rows)
427
- df = df.rename(columns={"timestamp": "ts", "time": "ts", "t": "ts", "date": "ts",
428
- "o": "open", "h": "high", "l": "low", "c": "close", "v": "volume"})
429
- for c in ("open", "high", "low", "close", "volume"):
430
- df[c] = pd.to_numeric(df[c], errors="coerce")
431
- if pd.api.types.is_numeric_dtype(df["ts"]):
432
- unit = "ms" if df["ts"].iloc[-1] > 1e11 else "s"
433
- df["ts"] = pd.to_datetime(df["ts"], unit=unit)
434
- else:
435
- df["ts"] = pd.to_datetime(df["ts"])
436
- return df.dropna(subset=["close"]).sort_values("ts").reset_index(drop=True)
437
-
438
-
439
- # ----------------------------------------------------------------- ML
440
- def make_features(df: pd.DataFrame):
441
- c, h, l, o, v = df["close"], df["high"], df["low"], df["open"], df["volume"]
442
- f = pd.DataFrame(index=df.index)
443
- lr = np.log(c).diff()
444
-
445
- # Getiri / momentum (çok ölçekli)
446
- for n in (1, 2, 3, 6, 12, 24, 48, 96, 192):
447
- f[f"ret{n}"] = c.pct_change(n)
448
-
449
- # Çoklu zaman dilimi (lookahead'siz yuvarlanan pencereler): ~4x ve ~16x üst TF benzeri
450
- dd = c.diff()
451
- for k in (4, 16):
452
- up_k = dd.clip(lower=0).ewm(alpha=1 / (14 * k), adjust=False).mean()
453
- dn_k = (-dd.clip(upper=0)).ewm(alpha=1 / (14 * k), adjust=False).mean()
454
- f[f"rsi_htf{k}"] = 100 - 100 / (1 + up_k / (dn_k + 1e-12))
455
- f[f"ema_dist_htf{k}"] = c / c.ewm(span=20 * k, adjust=False).mean() - 1
456
- f[f"chan_pos{k}"] = (c - l.rolling(20 * k).min()) / (h.rolling(20 * k).max() - l.rolling(20 * k).min() + 1e-12)
457
- f[f"vol_htf{k}"] = lr.rolling(20 * k).std()
458
-
459
- # Volatilite rejimi
460
- for n in (6, 24, 96):
461
- f[f"vol{n}"] = lr.rolling(n).std()
462
- f["vol_ratio"] = f["vol6"] / (f["vol96"] + 1e-12)
463
- f["vol_ratio2"] = f["vol24"] / (f["vol96"] + 1e-12)
464
- f["ret24_z"] = f["ret24"] / (f["vol24"] * np.sqrt(24) + 1e-12) # volatilite-normalize momentum
465
- f["ret6_z"] = f["ret6"] / (f["vol24"] * np.sqrt(6) + 1e-12)
466
-
467
- # RSI (iki periyot)
468
- d = c.diff()
469
- for n in (7, 14):
470
- up = d.clip(lower=0).ewm(alpha=1 / n, adjust=False).mean()
471
- dn = (-d.clip(upper=0)).ewm(alpha=1 / n, adjust=False).mean()
472
- f[f"rsi{n}"] = 100 - 100 / (1 + up / (dn + 1e-12))
473
-
474
- macd = c.ewm(span=12, adjust=False).mean() - c.ewm(span=26, adjust=False).mean()
475
- f["macd_h"] = (macd - macd.ewm(span=9, adjust=False).mean()) / c
476
- for n in (20, 50, 200):
477
- f[f"ema_dist{n}"] = c / c.ewm(span=n, adjust=False).mean() - 1
478
- f["ema_slope"] = c.ewm(span=20, adjust=False).mean().pct_change(5)
479
-
480
- tr = pd.concat([h - l, (h - c.shift()).abs(), (l - c.shift()).abs()], axis=1).max(axis=1)
481
- atr = tr.ewm(alpha=1 / 14, adjust=False).mean()
482
- f["atr_pct"] = atr / c
483
- f["hl_range"] = (h - l) / c
484
-
485
- # ADX (trend gücü)
486
- upm, dnm = h.diff(), -l.diff()
487
- pdm = ((upm > dnm) & (upm > 0)) * upm
488
- mdm = ((dnm > upm) & (dnm > 0)) * dnm
489
- pdi = 100 * pdm.ewm(alpha=1 / 14, adjust=False).mean() / (atr + 1e-12)
490
- mdi = 100 * mdm.ewm(alpha=1 / 14, adjust=False).mean() / (atr + 1e-12)
491
- f["adx"] = (100 * (pdi - mdi).abs() / (pdi + mdi + 1e-12)).ewm(alpha=1 / 14, adjust=False).mean()
492
- f["di_diff"] = pdi - mdi
493
-
494
- # Stokastik + Bollinger + kanal konumu
495
- ll, hh = l.rolling(14).min(), h.rolling(14).max()
496
- f["stoch"] = (c - ll) / (hh - ll + 1e-12)
497
- f["bb_pos"] = (c - c.rolling(20).mean()) / (2 * c.rolling(20).std() + 1e-12)
498
- f["bb_width"] = 4 * c.rolling(20).std() / c.rolling(20).mean()
499
- f["dist_hi50"] = c / h.rolling(50).max() - 1
500
- f["dist_lo50"] = c / l.rolling(50).min() - 1
501
-
502
- # Mum yapısı
503
- rng = (h - l) + 1e-12
504
- f["body"] = (c - o) / rng
505
- f["upper_wick"] = (h - np.maximum(o, c)) / rng
506
- f["lower_wick"] = (np.minimum(o, c) - l) / rng
507
- f["close_loc"] = (c - l) / rng
508
-
509
- # Hacim
510
- f["vol_z"] = (v - v.rolling(48).mean()) / (v.rolling(48).std() + 1e-12)
511
- f["vol_trend"] = v.rolling(6).mean() / (v.rolling(48).mean() + 1e-12)
512
- f["signed_vol"] = (np.sign(d) * v).rolling(12).sum() / (v.rolling(12).sum() + 1e-12) # OBV benzeri baskı
513
- f["mfi_like"] = ((c - o) / rng * v).rolling(14).sum() / (v.rolling(14).sum() + 1e-12)
514
-
515
- # İstatistiksel (otokorelasyon, çarpıklık)
516
- f["autocorr"] = lr.rolling(48).corr(lr.shift(1))
517
- f["skew48"] = lr.rolling(48).skew()
518
-
519
- # Seans / zaman (döngüsel)
520
- ts = df["ts"]
521
- f["hour_sin"] = np.sin(2 * np.pi * ts.dt.hour / 24)
522
- f["hour_cos"] = np.cos(2 * np.pi * ts.dt.hour / 24)
523
- f["dow_sin"] = np.sin(2 * np.pi * ts.dt.dayofweek / 7)
524
- f["dow_cos"] = np.cos(2 * np.pi * ts.dt.dayofweek / 7)
525
- return f.replace([np.inf, -np.inf], np.nan), atr
526
-
527
-
528
- def _models(hp=None):
529
- """hp (Optuna'dan): {"hgb": {...}, "et": {...}, "lr": {"C": ...}}; yoksa varsayılanlar."""
530
- hp = hp or {}
531
- h = {"max_depth": 3, "learning_rate": 0.04, "max_iter": 250, "l2_regularization": 2.0,
532
- "min_samples_leaf": 40, **hp.get("hgb", {})}
533
- e = {"n_estimators": 300, "max_depth": 6, "min_samples_leaf": 30, "max_features": 0.5, **hp.get("et", {})}
534
- c = hp.get("lr", {}).get("C", 0.05)
535
- return {
536
- "hgb": HistGradientBoostingClassifier(**h, random_state=42),
537
- "et": ExtraTreesClassifier(**e, n_jobs=-1, random_state=42),
538
- "lr": make_pipeline(StandardScaler(), LogisticRegression(C=c, max_iter=500)),
539
- }
540
 
 
 
 
 
 
 
 
 
 
 
 
 
541
 
542
- def _select(Xtr, ftr, k, thr=0.95):
543
- """Yalnızca EĞİTİM verisiyle: |Spearman IC| sırasına göre, birbirine çok benzeyenleri (>thr) eleyerek k özellik seç."""
544
- if k <= 0 or Xtr.shape[1] <= k:
545
- return list(Xtr.columns)
546
- ic = Xtr.rank().corrwith(pd.Series(np.asarray(ftr), index=Xtr.index).rank()).abs().fillna(0)
547
- ic = ic.sort_values(ascending=False)
548
- corr = Xtr.corr().abs().fillna(0)
549
- keep = []
550
- for c in ic.index:
551
- if all(corr.at[c, o] < thr for o in keep):
552
- keep.append(c)
553
- if len(keep) >= k:
554
- break
555
- return keep
556
-
557
-
558
- def _fit_predict(Xtr, ytr, Xte, w=None, ftr=None, k=0, with_train=False, hp=None):
559
- """Ensemble: HGB + ExtraTrees + Logistic -> olasılık ortalaması.
560
- k>0: fold-içi özellik seçimi. with_train: eğitim verisi üzerindeki olasılıkları da döndür (aşırı uyum ölçümü için)."""
561
- if k and ftr is not None:
562
- sel = _select(Xtr, ftr, k)
563
- Xtr, Xte = Xtr[sel], Xte[sel]
564
- ps, pt = [], []
565
- for name, m in _models(hp).items():
566
- if name == "lr":
567
- A, B = Xtr.fillna(0), Xte.fillna(0)
568
- m.fit(A, ytr, logisticregression__sample_weight=w)
569
- elif name == "et":
570
- A, B = Xtr.fillna(0), Xte.fillna(0)
571
- m.fit(A, ytr, sample_weight=w)
572
  else:
573
- A, B = Xtr, Xte
574
- m.fit(A, ytr, sample_weight=w)
575
- ps.append(m.predict_proba(B)[:, 1])
576
- if with_train:
577
- pt.append(m.predict_proba(A)[:, 1])
578
- p = np.mean(ps, axis=0)
579
- return (p, np.mean(pt, axis=0)) if with_train else p
580
 
 
 
581
 
582
- def _trades(p, fwd, horizon, thr, fee):
583
- """Örtüşmeyen (her `horizon` barda bir) işlemlerin net getirileri."""
584
- p, fwd = p[::horizon], fwd[::horizon]
585
- pos = np.where(p > thr, 1, np.where(p < 1 - thr, -1, 0))
586
- pnl = pos * fwd - np.abs(pos) * fee
587
- return pnl[pos != 0]
588
 
 
 
589
 
590
- def _stats(tr):
591
- n = len(tr)
592
- if n < 20:
593
- return {"n": n, "mean": 0.0, "t": 0.0, "hit": np.nan, "pf": np.nan}
594
- gp, gl = tr[tr > 0].sum(), -tr[tr < 0].sum()
595
- return {"n": n, "mean": float(tr.mean()),
596
- "t": float(tr.mean() / (tr.std() / np.sqrt(n) + 1e-12)),
597
- "hit": float((tr > 0).mean()), "pf": float(gp / gl) if gl > 0 else np.inf}
598
 
 
599
 
600
- def _backtest(p, fwd, horizon, thr, fee):
601
- return _stats(_trades(p, fwd, horizon, thr, fee))
602
 
 
603
 
604
- def _nested(p, f, sizes, horizon, fee, grid=(0.52, 0.54, 0.56, 0.58, 0.60, 0.62)):
605
- """Dürüst backtest: eşik her fold için YALNIZCA önceki fold'ların OOS'unda seçilir, sonra o fold'a uygulanır."""
606
- bounds = np.cumsum([0] + list(sizes))
607
- trades = []
608
- for j in range(1, len(sizes)):
609
- pp, ff = p[:bounds[j]], f[:bounds[j]]
610
- best_thr, best_m = 0.55, -np.inf
611
- for thr in (0.55,) + tuple(grid):
612
- r = _backtest(pp, ff, horizon, thr, fee)
613
- if r["n"] >= 30 and r["mean"] > best_m:
614
- best_thr, best_m = thr, r["mean"]
615
- trades.append(_trades(p[bounds[j]:bounds[j + 1]], f[bounds[j]:bounds[j + 1]], horizon, best_thr, fee))
616
- return _stats(np.concatenate(trades) if trades else np.array([]))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
617
 
 
 
 
 
 
 
 
618
 
619
- class _Fixed:
620
- """En iyi parametreleri aynı `suggest_*` arayüzüyle geri üretmek için."""
621
- def __init__(self, params):
622
- self.p = params
623
 
624
- def suggest_int(self, name, *a, **k):
625
- return self.p[name]
 
626
 
627
- def suggest_float(self, name, *a, **k):
628
- return self.p[name]
 
629
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
630
 
631
- def _hp_space(t):
632
  return {
633
- "hgb": {"max_depth": t.suggest_int("hgb_depth", 2, 5),
634
- "learning_rate": t.suggest_float("hgb_lr", 0.01, 0.1, log=True),
635
- "max_iter": t.suggest_int("hgb_iter", 100, 400, step=50),
636
- "l2_regularization": t.suggest_float("hgb_l2", 0.5, 10.0, log=True),
637
- "min_samples_leaf": t.suggest_int("hgb_leaf", 20, 200, step=20)},
638
- "et": {"max_depth": t.suggest_int("et_depth", 3, 10),
639
- "min_samples_leaf": t.suggest_int("et_leaf", 10, 100, step=10),
640
- "max_features": t.suggest_float("et_mf", 0.2, 0.8)},
641
- "lr": {"C": t.suggest_float("lr_C", 0.005, 0.5, log=True)},
642
  }
643
 
644
 
645
- def _tune(X, Y, F, W, horizon, n_trials, timeout=240):
646
- """Optuna: hiperparametre + özellik sayısı (k) ayarı. YALNIZCA geliştirme verisinin ilk %40'ında
647
- (iç walk-forward, 3 dilim) yapılır; asıl walk-forward OOS ve hold-out'a hiç dokunmaz.
648
- Amaç: ortalama AUC − 0.5·std (kararsız/şanslı ayarları cezalandırır)."""
649
- try:
650
- import optuna
651
- except Exception: # noqa: BLE001
652
- return {"error": "optuna kurulu değil (pip install optuna); varsayılan ayarlar kullanıldı"}
653
- m = int(len(X) * 0.4)
654
- if m < 500:
655
- return {"error": f"Ayar bölgesi çok küçük ({m} bar < 500); Optuna atlandı, varsayılanlar kullanıldı"}
656
- Xt, Yt, Ft, Wt = X.iloc[:m], Y.iloc[:m], F[:m], W[:m]
657
- edges = np.linspace(int(m * 0.4), m, 4).astype(int)
658
-
659
- def obj(trial):
660
- hp = _hp_space(trial)
661
- k = trial.suggest_int("k", 10, 80, step=10)
662
- aucs = []
663
- for j in range(3):
664
- tr_end, te0, te1 = edges[j] - horizon, edges[j], edges[j + 1]
665
- if tr_end < 80:
666
- continue
667
- p = _fit_predict(Xt.iloc[:tr_end], Yt.iloc[:tr_end], Xt.iloc[te0:te1],
668
- Wt[:tr_end], Ft[:tr_end], k, hp=hp)
669
- try:
670
- aucs.append(roc_auc_score(Yt.iloc[te0:te1], p))
671
- except ValueError:
672
- pass
673
- return float(np.mean(aucs) - 0.5 * np.std(aucs)) if aucs else 0.5
674
-
675
- optuna.logging.set_verbosity(optuna.logging.WARNING)
676
- study = optuna.create_study(direction="maximize", sampler=optuna.samplers.TPESampler(seed=42))
677
- study.optimize(obj, n_trials=int(n_trials), timeout=timeout)
678
- bp = study.best_params
679
- return {"hp": _hp_space(_Fixed(bp)), "k": int(bp["k"]), "score": float(study.best_value),
680
- "trials": len(study.trials), "region": m}
681
-
682
-
683
- def _walk(X, Y, F, W, horizon, n_folds, k, hp=None):
684
- """Purged expanding-window walk-forward. Döner: OOS olasılık, satır indeksi, fold boyutları, ort. train-test AUC farkı."""
685
- n = len(X)
686
- edges = np.linspace(int(n * 0.4), n, n_folds + 1).astype(int)
687
- oos_p, oos_idx, sizes, gaps = [], [], [], []
688
- for j in range(n_folds):
689
- tr_end, te0, te1 = edges[j] - horizon, edges[j], edges[j + 1]
690
- if tr_end < 80:
691
- continue
692
- p, ptr = _fit_predict(X.iloc[:tr_end], Y.iloc[:tr_end], X.iloc[te0:te1],
693
- W[:tr_end], F[:tr_end], k, with_train=True, hp=hp)
694
- try:
695
- gaps.append(roc_auc_score(Y.iloc[:tr_end], ptr) - roc_auc_score(Y.iloc[te0:te1], p))
696
- except ValueError:
697
- pass
698
- oos_p.append(p)
699
- oos_idx.extend(range(te0, te1))
700
- sizes.append(te1 - te0)
701
- if not oos_p:
702
- raise ValueError("Walk-forward için yeterli veri yok. Mum sayısını artır.")
703
- return (np.concatenate(oos_p), np.array(oos_idx), sizes,
704
- float(np.mean(gaps)) if gaps else float("nan"))
705
-
706
-
707
- def predict(df: pd.DataFrame, horizon: int, fee: float = 0.0006, n_folds: int = 5, ext=None,
708
- max_feat: int = 40, use_optuna: bool = False, n_trials: int = 25):
709
- feats, atr = make_features(df)
710
- n_ext = 0
711
- if ext is not None:
712
- ext = ext.replace([np.inf, -np.inf], np.nan)
713
- w0 = min(60, len(df) // 5)
714
- # ısınma sonrası %10'dan fazla boş olan Tickscope kolonlarını at
715
- ext = ext.loc[:, ext.iloc[w0:].isna().mean() <= 0.10]
716
- n_ext = ext.shape[1]
717
- feats = pd.concat([feats, ext], axis=1)
718
- close = df["close"]
719
- fwd = close.shift(-horizon) / close - 1
720
- y = (fwd > 0).astype(float)
721
- y[fwd.isna()] = np.nan
722
-
723
- feats = feats.dropna(axis=1, how="all") # tamamen boş kolonları at (ör. hacim yoksa)
724
- data = feats.copy()
725
- data["y"], data["fwd"] = y, fwd
726
- data["thr_move"] = (atr / close) * np.sqrt(horizon)
727
- warm = min(60, len(data) // 5) # gösterge ısınma barları
728
- train_df = data.iloc[warm:].dropna(subset=["y", "fwd", "thr_move"]).copy()
729
- # Kalan NaN'ler (uzun pencereli göstergeler) satır silmek yerine doldurulur
730
- train_df[list(feats.columns)] = train_df[list(feats.columns)].ffill().fillna(0)
731
- if len(train_df) < 200:
732
- raise ValueError(
733
- f"Yeterli veri yok: {len(df)} mum geldi, {len(train_df)} kullanılabilir bar kaldı "
734
- f"(en az 200 gerekir; ufuk={horizon}). Mum sayısını artır veya borsayı/sembolü kontrol et.")
735
-
736
- cols = list(feats.columns)
737
- X, Y = train_df[cols], train_df["y"].astype(int)
738
- F, TM = train_df["fwd"].values, train_df["thr_move"].values
739
- # Gürültü filtresi: çok küçük hareketleri (≈ yazı-tura) eğitimde ağırlıksız bırak
740
- # ve büyük hareketlere daha fazla ağırlık ver.
741
- W = np.clip(np.abs(F) / (TM + 1e-12), 0.05, 3.0)
742
-
743
- # --- Geliştirme (ilk %80) / hiç dokunulmayan hold-out (son %20) ---
744
- n_all = len(X)
745
- n_dev = int(n_all * 0.8)
746
- Xd, Yd, Fd, Wd = X.iloc[:n_dev], Y.iloc[:n_dev], F[:n_dev], W[:n_dev]
747
-
748
- hp, tuned = None, None
749
- if use_optuna:
750
- tuned = _tune(Xd, Yd, Fd, Wd, horizon, n_trials)
751
- if "error" not in tuned:
752
- hp, max_feat = tuned["hp"], tuned["k"]
753
-
754
- oos_p, oos_idx, sizes, gap = _walk(Xd, Yd, Fd, Wd, horizon, n_folds, max_feat, hp)
755
- yo, fo = Yd.values[oos_idx], Fd[oos_idx]
756
-
757
- # Teşhis (ablation): yalnızca temel (fiyat/hacim) özelliklerle aynı test
758
- base_cols = [c for c in cols if not c.startswith(("ts_", "dv_"))]
759
- auc_base = None
760
- if len(base_cols) < len(cols):
761
- pb, ib, _, _ = _walk(Xd[base_cols], Yd, Fd, Wd, horizon, n_folds, max_feat, hp)
762
- try:
763
- auc_base = float(roc_auc_score(Yd.values[ib], pb))
764
- except ValueError:
765
- auc_base = 0.5
766
 
767
- acc = float(((oos_p > 0.5).astype(int) == yo).mean())
768
- base = float(max(yo.mean(), 1 - yo.mean()))
769
  try:
770
- auc = float(roc_auc_score(yo, oos_p))
771
- except ValueError:
772
- auc = 0.5
773
- # AUC anlamlılığı: H0 (AUC=0.5) altında SE ≈ sqrt(1/(3·n_eff)); örtüşen etiketler için n_eff = n/ufuk
774
- n_eff = max(len(oos_p) / horizon, 2.0)
775
- z = (auc - 0.5) / math.sqrt(1.0 / (3.0 * n_eff))
776
- auc_p = 0.5 * math.erfc(z / math.sqrt(2.0))
777
-
778
- # Eşik taraması (yalnızca OOS üzerinde, işlem sayısı ≥ 30 şartıyla) — iyimser
779
- best = {"thr": 0.55, **_backtest(oos_p, fo, horizon, 0.55, fee)}
780
- for thr in (0.52, 0.54, 0.56, 0.58, 0.60, 0.62):
781
- r = _backtest(oos_p, fo, horizon, thr, fee)
782
- if r["n"] >= 30 and r["mean"] > best["mean"]:
783
- best = {"thr": thr, **r}
784
-
785
- # Dürüst (nested) backtest: eşik hiç görmediği fold'a uygulanır
786
- nested = _nested(oos_p, fo, sizes, horizon, fee)
787
-
788
- # Hold-out: gelişitirmede hiç kullanılmayan son %20; seçilen eşikle bir kez değerlendirilir
789
- ho = None
790
- tr_end = n_dev - horizon
791
- if tr_end >= 200 and n_all - n_dev >= 100:
792
- ph = _fit_predict(X.iloc[:tr_end], Y.iloc[:tr_end], X.iloc[n_dev:], W[:tr_end], F[:tr_end], max_feat, hp=hp)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
793
  try:
794
- auc_h = float(roc_auc_score(Y.values[n_dev:], ph))
795
- except ValueError:
796
- auc_h = 0.5
797
- ho = {"auc": auc_h, "bars": len(ph),
798
- **_stats(_trades(ph, F[n_dev:], horizon, best["thr"], fee))}
799
-
800
- # Son model: tüm veriyle, canlı tahmin
801
- final_p = _fit_predict(X, Y, feats.iloc[[-1]][cols].ffill().fillna(0), W, F, max_feat, hp=hp)
802
- p_up = float(final_p[0])
803
-
804
- price, a = float(close.iloc[-1]), float(atr.iloc[-1])
805
- move = a * np.sqrt(horizon)
806
- center = price + (p_up - 0.5) * 2 * move
807
- return {
808
- "p_up": p_up, "acc": acc, "baseline": base, "auc": auc, "price": price, "atr": a,
809
- "center": center, "low": center - move, "high": center + move,
810
- "thr": best["thr"], "bt": best, "n_oos": len(oos_p), "fee": fee,
811
- "n_ext": n_ext, "n_feat": len(cols), "auc_base": auc_base, "max_feat": max_feat,
812
- "gap": gap, "auc_p": auc_p, "nested": nested, "holdout": ho, "tuned": tuned,
813
- }
814
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
815
 
816
- # ----------------------------------------------------------------- Ana akış
817
- async def run_async(symbol, timeframe, exchange, horizon, limit):
818
- extra = {}
819
- async with stdio_client(SERVER) as (r, w):
820
- async with ClientSession(r, w) as session:
821
- await session.initialize()
822
- tools = {t.name: t for t in (await session.list_tools()).tools}
823
-
824
- ohlcv = await fetch_ohlcv(session, tools, symbol, timeframe, exchange, int(limit))
825
- if ohlcv is None:
826
- raise RuntimeError(f"get_ohlcv bulunamadı. Mevcut tool'lar: {list(tools)}")
827
- if isinstance(ohlcv, dict) and "error" in ohlcv:
828
- raise RuntimeError(ohlcv["error"])
829
-
830
- # Buradan sonrası opsiyonel: sunucu çökse bile elimizdeki mum verisiyle devam edilir
831
- deep = None
832
- try:
833
- deep = await call_tool(session, tools, "deep_analyze", symbol=symbol,
834
- timeframe=timeframe, exchange=exchange)
835
- except Exception as e: # noqa: BLE001
836
- extra["deep_analyze_error"] = _root(e)
837
- for name in EXTRA_TOOLS:
838
- if name not in tools:
839
- continue
840
- try:
841
- extra[name] = await call_tool(session, tools, name, symbol=symbol,
842
- timeframe=timeframe, exchange=exchange)
843
- except Exception as e: # noqa: BLE001
844
- extra[name] = {"error": _root(e)}
845
- extra["_available_tools"] = list(tools)
846
- ci = tools.get("compute_indicators")
847
- if ci is not None: # gerçek gösterge adlarını görebilmek için araç açıklaması
848
- extra["_compute_indicators_description"] = (getattr(ci, "description", "") or "")[:4000]
849
-
850
- # Göstergeler: ayrı Tickscope süreçleri (ana oturum kapandıktan sonra; DuckDB kilidi çakışmasın)
851
- ind = None
852
- try:
853
- ind = await fetch_indicators(symbol, timeframe, exchange, int(limit))
854
- except Exception as e: # noqa: BLE001
855
- extra["indicators_error"] = _root(e)
856
- if isinstance(ind, dict):
857
- ser = extract_series(ind, 0)
858
- extra["_ts_diag"] = {"seri_sayisi": len(ser),
859
- "seri_uzunluklari": sorted({len(v) for v in ser.values()})[:10],
860
- "yeniden_baglanma": ind.get("reconnects"), "son_hata": ind.get("last_error"),
861
- "calismayan_adlar": ind.get("failed")}
862
- return ohlcv, deep, ind, extra
863
-
864
-
865
- def _root(e):
866
- """ExceptionGroup/TaskGroup sarmalını açıp asıl hatayı döndürür."""
867
- while getattr(e, "exceptions", None):
868
- e = e.exceptions[0]
869
- return f"{type(e).__name__}: {e}"
870
-
871
-
872
- def run(symbol, timeframe, exchange, horizon, limit, use_deriv=True, max_feat=40, use_optuna=False, n_trials=25):
873
- try:
874
- ohlcv, deep, ind, extra = asyncio.run(run_async(symbol, timeframe, exchange, horizon, limit))
875
- df = to_df(ohlcv)
876
- dfeat = None
877
- if use_deriv:
878
- dfeat, df, dextra = asyncio.run(fetch_deriv_features(symbol, exchange, timeframe, df))
879
- extra.update(dextra)
880
- ext = indicator_features(ind, df)
881
- if dfeat is not None:
882
- ext = dfeat if ext is None else pd.concat([ext, dfeat], axis=1)
883
- res = predict(df, int(horizon), ext=ext, max_feat=int(max_feat),
884
- use_optuna=bool(use_optuna), n_trials=int(n_trials))
885
- except BaseException as e: # noqa: BLE001
886
- import traceback
887
- traceback.print_exc()
888
- msg = _root(e)
889
- hint = ""
890
- if "closed" in msg.lower():
891
- hint = ("\n\nMCP süreci kapandı. Terminaldeki Tickscope/uvx hata çıktısına bakın; "
892
- "ayrıca aynı anda açık başka bir Tickscope süreci DuckDB kilidini tutuyor olabilir "
893
- "(eski `uvx tickscope-mcp` süreçlerini kapatıp tekrar deneyin).")
894
- return f"**Hata:** {msg}{hint}", None, ""
895
-
896
- p, thr, bt = res["p_up"], res["thr"], res["bt"]
897
- signal = "LONG" if p > thr else "SHORT" if p < 1 - thr else "NÖTR"
898
- nest, ho = res["nested"], res["holdout"]
899
- checks = [
900
- ("AUC > 0.52 ve p < 0.05", res["auc"] > 0.52 and res["auc_p"] < 0.05),
901
- ("Train–test AUC farkı < 0.20 (aşırı uyum düşük)", res["gap"] < 0.20),
902
- ("Nested net getiri > 0 ve t > 1.5 (≥30 işlem)", nest["n"] >= 30 and nest["mean"] > 0 and nest["t"] > 1.5),
903
- ("Hold-out AUC > 0.50", ho is not None and ho["auc"] > 0.50),
904
- ("Hold-out net getiri > 0 (≥20 işlem)", ho is not None and ho["n"] >= 20 and ho["mean"] > 0),
905
- ]
906
- t = res["tuned"]
907
- if t is None:
908
- opt_txt = "kapalı (varsayılan hiperparametreler)"
909
- elif "error" in t:
910
- opt_txt = t["error"]
911
- else:
912
- g, e = t["hp"]["hgb"], t["hp"]["et"]
913
- opt_txt = (f"{t['trials']} deneme · iç skor {t['score']:.3f} · k={t['k']} · "
914
- f"HGB derinlik {g['max_depth']}, lr {g['learning_rate']:.3f} · ET derinlik {e['max_depth']} · "
915
- f"LR C={t['hp']['lr']['C']:.3f} (yalnızca ilk %40'ta ayarlandı)")
916
- passed = sum(ok for _, ok in checks)
917
- edge = f"✅ {passed}/5 kriter" if passed >= 4 else f"⚠️ {passed}/5 kriter — kanıtlanamadı"
918
- checks_md = "**Aşırı uyum / güvenilirlik göstergesi**\n\n" + "\n".join(
919
- f"- {'✅' if ok else '❌'} {name}" for name, ok in checks)
920
- ho_txt = (f"AUC {ho['auc']:.3f} · {ho['n']} işlem · net {ho['mean']:.3%} (t = {ho['t']:.2f}) · {ho['bars']} bar"
921
- if ho is not None else "veri yetersiz")
922
- md = f"""### {symbol} · {timeframe} · {int(horizon)} bar sonrası
923
- | | |
924
- |---|---|
925
- | Son fiyat | **{res['price']:.4f}** |
926
- | Yükseliş olasılığı | **{p:.1%}** → **{signal}** (eşik {thr:.2f}) |
927
- | Tahmini merkez | {res['center']:.4f} |
928
- | Aralık (ATR·√h) | {res['low']:.4f} – {res['high']:.4f} |
929
- | OOS AUC / doğruluk | {res['auc']:.3f} / {res['acc']:.1%} (baseline {res['baseline']:.1%}) |
930
- | OOS işlem sayısı | {bt['n']} (hit {bt['hit']:.1%}, PF {bt['pf']:.2f}) |
931
- | Ort. net getiri / işlem | {bt['mean']:.3%} (t = {bt['t']:.2f}, ücret {res['fee']:.2%}/işlem) |
932
- | Özellik sayısı | {res['n_feat']} (Tickscope/türev: {res['n_ext']}) → fold başına en fazla {res['max_feat']} seçilir |
933
- | AUC yalnızca temel özelliklerle | {('%.3f' % res['auc_base']) if res['auc_base'] is not None else '—'} (ek veri kaynakları yardım ediyor mu?) |
934
- | Optuna | {opt_txt} |
935
- | Train–test AUC farkı (aşırı uyum) | {res['gap']:.3f} |
936
- | AUC anlamlılığı (tek yönlü p) | {res['auc_p']:.3f} |
937
- | Nested (dürüst) net getiri / işlem | {nest['mean']:.3%} (t = {nest['t']:.2f}, {nest['n']} işlem) |
938
- | Hold-out (dokunulmamış son %20) | {ho_txt} |
939
- | Model edge | {edge} |
940
-
941
- {checks_md}
942
-
943
- *Üstteki OOS/backtest satırları geliştirme kısmında (ilk %80) walk-forward ve eşik OOS'ta seçildiği için iyimserdir; güvenilir olanlar nested ve hold-out satırlarıdır. Eğitim/araştırma amaçlıdır; yatırım tavsiyesi değildir.*"""
944
-
945
- # Grafik
946
- tail = df.tail(150)
947
- step = df["ts"].diff().median()
948
- fx = df["ts"].iloc[-1] + step * int(horizon)
949
- fig = go.Figure(go.Candlestick(x=tail["ts"], open=tail["open"], high=tail["high"],
950
- low=tail["low"], close=tail["close"], name="Fiyat"))
951
- fig.add_trace(go.Scatter(x=[fx], y=[res["center"]], mode="markers", name="Tahmin",
952
- marker=dict(size=12, symbol="diamond"),
953
- error_y=dict(type="data", symmetric=False,
954
- array=[res["high"] - res["center"]],
955
- arrayminus=[res["center"] - res["low"]])))
956
- fig.update_layout(height=450, xaxis_rangeslider_visible=False, margin=dict(l=10, r=10, t=30, b=10))
957
-
958
- raw = json.dumps(_strip_series({"deep_analyze": deep, "compute_indicators": ind, **extra}),
959
- indent=2, ensure_ascii=False, default=str)
960
- return md, fig, raw
961
-
962
-
963
- with gr.Blocks(title="Tickscope Tahmin") as demo:
964
- gr.Markdown("## Tickscope MCP → ML Tahmin")
965
- with gr.Row():
966
- symbol = gr.Textbox("BTC/USDT", label="Sembol")
967
- exchange = gr.Dropdown(["binance", "bybit", "okx"], value="binance", label="Borsa")
968
- timeframe = gr.Dropdown(["5m", "15m", "1h", "4h", "1d"], value="1h", label="Timeframe")
969
  with gr.Row():
970
- horizon = gr.Slider(1, 24, value=4, step=1, label="Tahmin ufku (bar)")
971
- limit = gr.Slider(300, 20000, value=5000, step=100, label="Mum sayısı (1000 üstü sayfalanır)")
972
- use_deriv = gr.Checkbox(True, label="Türev verisi (LiquidVision MCP: funding/OI) — mumları geçmişin kapsadığı aralığa kırpabilir")
973
- max_feat = gr.Slider(10, 150, value=40, step=5, label="Maks. özellik (fold içinde seçilir; az = daha az aşırı uyum)")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
974
  with gr.Row():
975
- use_optuna = gr.Checkbox(False, label="Optuna ile hiperparametre ayarı (OOS/hold-out'a dokunmaz; yavaş)")
976
- n_trials = gr.Slider(10, 100, value=25, step=5, label="Optuna deneme sayısı")
977
- btn = gr.Button("Tahmin et", variant="primary")
978
- out_md = gr.Markdown()
979
- out_plot = gr.Plot()
980
- with gr.Accordion("Tickscope ham çıktı (deep_analyze / indicators)", open=False):
981
- out_raw = gr.Code(language="json")
982
- btn.click(run, [symbol, timeframe, exchange, horizon, limit, use_deriv, max_feat, use_optuna, n_trials], [out_md, out_plot, out_raw])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
983
 
984
  if __name__ == "__main__":
985
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
 
2
+ import warnings
3
+ warnings.filterwarnings("ignore")
 
 
4
 
5
+ import time
6
  import numpy as np
7
  import pandas as pd
8
+ import ccxt
9
+ import gradio as gr
10
+
11
+ from sklearn.ensemble import (
12
+ ExtraTreesClassifier,
13
+ HistGradientBoostingClassifier
14
+ )
15
  from sklearn.linear_model import LogisticRegression
16
+ from sklearn.pipeline import Pipeline
 
17
  from sklearn.preprocessing import StandardScaler
18
+ from sklearn.model_selection import TimeSeriesSplit
19
+ from sklearn.metrics import (
20
+ accuracy_score,
21
+ balanced_accuracy_score,
22
+ roc_auc_score
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  )
24
 
25
+
26
+ # =========================================================
27
+ # CONFIG
28
+ # =========================================================
29
+
30
+ SYMBOL = "BTC/USDT"
31
+
32
+ FEATURES = [
33
+ "rsi",
34
+ "macd_pct",
35
+ "macd_signal_pct",
36
+ "macd_hist_pct",
37
+ "supertrend_direction",
38
+ "supertrend_distance",
39
+ "vwap_distance",
40
+ "atr_pct",
41
+ "return_1",
42
+ "return_3",
43
+ "return_6",
44
+ "volatility_14",
45
+ "volume_ratio",
46
+ "body_pct",
47
+ "high_low_pct"
48
+ ]
49
+
50
+
51
+ # =========================================================
52
+ # BINANCE DATA
53
+ # =========================================================
54
+
55
+ def fetch_data(timeframe="1h", candles=2000):
56
+
57
+ exchange = ccxt.binance({
58
+ "enableRateLimit": True,
59
+ "timeout": 20000
60
+ })
61
+
62
+ timeframe_ms = exchange.parse_timeframe(timeframe) * 1000
63
+
64
+ since = exchange.milliseconds() - candles * timeframe_ms
65
+ all_rows = []
66
+
67
+ while len(all_rows) < candles:
68
+
69
+ remaining = candles - len(all_rows)
70
+ limit = min(1000, remaining)
71
+
72
+ batch = exchange.fetch_ohlcv(
73
+ SYMBOL,
74
+ timeframe=timeframe,
75
+ since=since,
76
+ limit=limit
77
+ )
78
+
79
+ if not batch:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  break
81
+
82
+ all_rows.extend(batch)
83
+
84
+ last_timestamp = batch[-1][0]
85
+ new_since = last_timestamp + timeframe_ms
86
+
87
+ if new_since <= since:
88
  break
89
+
90
+ since = new_since
91
+
92
+ if len(batch) < limit:
93
  break
94
+
95
+ time.sleep(exchange.rateLimit / 1000)
96
+
97
+ if not all_rows:
98
+ raise RuntimeError("Binance veri döndürmedi.")
99
+
100
+ df = pd.DataFrame(
101
+ all_rows,
102
+ columns=[
103
+ "timestamp",
104
+ "open",
105
+ "high",
106
+ "low",
107
+ "close",
108
+ "volume"
109
+ ]
110
+ )
111
+
112
+ df = df.drop_duplicates("timestamp")
113
+ df = df.sort_values("timestamp")
114
+
115
+ df["timestamp"] = pd.to_datetime(
116
+ df["timestamp"],
117
+ unit="ms",
118
+ utc=True
119
+ )
120
+
121
+ df = df.set_index("timestamp")
122
+
123
+ for col in ["open", "high", "low", "close", "volume"]:
124
+ df[col] = pd.to_numeric(df[col], errors="coerce")
125
+
126
+ df = df.dropna()
127
+
128
+ # Son mum henüz tamamlanmamış olabilir.
129
+ if len(df) > 2:
130
+ df = df.iloc[:-1]
131
+
132
+ return df.tail(candles)
133
+
134
+
135
+ # =========================================================
136
+ # INDICATOR CALCULATIONS
137
+ # =========================================================
138
+
139
+ def calculate_indicators(df):
140
+
141
+ df = df.copy()
142
+
143
+ # -----------------------------------------------------
144
+ # ATR - Wilder RMA
145
+ # -----------------------------------------------------
146
+
147
+ previous_close = df["close"].shift(1)
148
+
149
+ tr = pd.concat([
150
+ df["high"] - df["low"],
151
+ (df["high"] - previous_close).abs(),
152
+ (df["low"] - previous_close).abs()
153
+ ], axis=1).max(axis=1)
154
+
155
+ df["atr"] = tr.ewm(
156
+ alpha=1 / 10,
157
+ adjust=False,
158
+ min_periods=10
159
+ ).mean()
160
+
161
+ # -----------------------------------------------------
162
+ # RSI 14
163
+ # -----------------------------------------------------
164
+
165
+ delta = df["close"].diff()
166
+
167
+ gain = delta.clip(lower=0)
168
+ loss = -delta.clip(upper=0)
169
+
170
+ avg_gain = gain.ewm(
171
+ alpha=1 / 14,
172
+ adjust=False,
173
+ min_periods=14
174
+ ).mean()
175
+
176
+ avg_loss = loss.ewm(
177
+ alpha=1 / 14,
178
+ adjust=False,
179
+ min_periods=14
180
+ ).mean()
181
+
182
+ rs = avg_gain / avg_loss.replace(0, np.nan)
183
+
184
+ df["rsi"] = 100 - (100 / (1 + rs))
185
+
186
+ # -----------------------------------------------------
187
+ # MACD 12,26,9
188
+ # -----------------------------------------------------
189
+
190
+ ema12 = df["close"].ewm(
191
+ span=12,
192
+ adjust=False
193
+ ).mean()
194
+
195
+ ema26 = df["close"].ewm(
196
+ span=26,
197
+ adjust=False
198
+ ).mean()
199
+
200
+ df["macd"] = ema12 - ema26
201
+
202
+ df["macd_signal"] = df["macd"].ewm(
203
+ span=9,
204
+ adjust=False
205
+ ).mean()
206
+
207
+ df["macd_hist"] = (
208
+ df["macd"] - df["macd_signal"]
209
+ )
210
+
211
+ # -----------------------------------------------------
212
+ # SUPERTREND 10,3
213
+ # -----------------------------------------------------
214
+
215
+ multiplier = 3.0
216
+
217
+ hl2 = (df["high"] + df["low"]) / 2
218
+
219
+ basic_upper = hl2 + multiplier * df["atr"]
220
+ basic_lower = hl2 - multiplier * df["atr"]
221
+
222
+ final_upper = basic_upper.copy()
223
+ final_lower = basic_lower.copy()
224
+
225
+ supertrend = pd.Series(
226
+ np.nan,
227
+ index=df.index
228
+ )
229
+
230
+ direction = pd.Series(
231
+ 1,
232
+ index=df.index,
233
+ dtype=float
234
+ )
235
+
236
+ for i in range(1, len(df)):
237
+
238
+ previous_upper = final_upper.iloc[i - 1]
239
+ previous_lower = final_lower.iloc[i - 1]
240
+ previous_close_value = df["close"].iloc[i - 1]
241
+
242
+ current_upper = basic_upper.iloc[i]
243
+ current_lower = basic_lower.iloc[i]
244
+
245
+ if (
246
+ current_upper < previous_upper
247
+ or previous_close_value > previous_upper
248
+ ):
249
+ final_upper.iloc[i] = current_upper
250
+ else:
251
+ final_upper.iloc[i] = previous_upper
252
+
253
+ if (
254
+ current_lower > previous_lower
255
+ or previous_close_value < previous_lower
256
+ ):
257
+ final_lower.iloc[i] = current_lower
258
+ else:
259
+ final_lower.iloc[i] = previous_lower
260
+
261
+ previous_direction = direction.iloc[i - 1]
262
+ current_close = df["close"].iloc[i]
263
+
264
+ if previous_direction == -1:
265
+
266
+ if current_close > final_upper.iloc[i]:
267
+ direction.iloc[i] = 1
268
+ else:
269
+ direction.iloc[i] = -1
270
+
271
+ else:
272
+
273
+ if current_close < final_lower.iloc[i]:
274
+ direction.iloc[i] = -1
275
+ else:
276
+ direction.iloc[i] = 1
277
+
278
+ if direction.iloc[i] == 1:
279
+ supertrend.iloc[i] = final_lower.iloc[i]
280
+ else:
281
+ supertrend.iloc[i] = final_upper.iloc[i]
282
+
283
+ df["supertrend"] = supertrend
284
+ df["supertrend_direction"] = direction
285
+
286
+ # -----------------------------------------------------
287
+ # VWAP - UTC DAILY RESET
288
+ # -----------------------------------------------------
289
+
290
+ typical_price = (
291
+ df["high"] + df["low"] + df["close"]
292
+ ) / 3
293
+
294
+ daily_group = df.index.date
295
+
296
+ pv = typical_price * df["volume"]
297
+
298
+ cumulative_pv = pv.groupby(daily_group).cumsum()
299
+
300
+ cumulative_volume = (
301
+ df["volume"].groupby(daily_group).cumsum()
302
+ )
303
+
304
+ df["vwap"] = (
305
+ cumulative_pv /
306
+ cumulative_volume.replace(0, np.nan)
307
+ )
308
+
309
+ # -----------------------------------------------------
310
+ # ADDITIONAL FEATURES
311
+ # -----------------------------------------------------
312
+
313
+ df["macd_pct"] = df["macd"] / df["close"] * 100
314
+
315
+ df["macd_signal_pct"] = (
316
+ df["macd_signal"] / df["close"] * 100
317
+ )
318
+
319
+ df["macd_hist_pct"] = (
320
+ df["macd_hist"] / df["close"] * 100
321
+ )
322
+
323
+ df["supertrend_distance"] = (
324
+ df["close"] - df["supertrend"]
325
+ ) / df["close"] * 100
326
+
327
+ df["vwap_distance"] = (
328
+ df["close"] - df["vwap"]
329
+ ) / df["close"] * 100
330
+
331
+ df["atr_pct"] = df["atr"] / df["close"] * 100
332
+
333
+ df["return_1"] = df["close"].pct_change(1) * 100
334
+ df["return_3"] = df["close"].pct_change(3) * 100
335
+ df["return_6"] = df["close"].pct_change(6) * 100
336
+
337
+ df["volatility_14"] = (
338
+ df["return_1"].rolling(14).std()
339
+ )
340
+
341
+ volume_average = df["volume"].rolling(20).mean()
342
+
343
+ df["volume_ratio"] = (
344
+ df["volume"] /
345
+ volume_average.replace(0, np.nan)
346
+ )
347
+
348
+ df["body_pct"] = (
349
+ df["close"] - df["open"]
350
+ ) / df["open"] * 100
351
+
352
+ df["high_low_pct"] = (
353
+ df["high"] - df["low"]
354
+ ) / df["close"] * 100
355
+
356
+ return df
357
+
358
+
359
+ # =========================================================
360
+ # TARGET
361
+ # =========================================================
362
+
363
+ def prepare_dataset(df, horizon):
364
+
365
+ data = calculate_indicators(df)
366
+
367
+ future_close = data["close"].shift(-horizon)
368
+
369
+ data["target"] = (
370
+ future_close > data["close"]
371
+ ).astype(int)
372
+
373
+ # Son horizon mumunda gelecek hedefi bilinmez.
374
+ data = data.iloc[:-horizon].copy()
375
+
376
+ data = data.replace(
377
+ [np.inf, -np.inf],
378
+ np.nan
379
+ )
380
+
381
+ data = data.dropna(
382
+ subset=FEATURES + ["target", "atr"]
383
+ )
384
+
385
+ if len(data) < 500:
386
+ raise ValueError(
387
+ "Yeterli veri yok. Mum sayısını artır."
388
+ )
389
+
390
+ return data
391
+
392
+
393
+ # =========================================================
394
+ # MODELS
395
+ # =========================================================
396
+
397
+ def create_models():
398
+
399
+ models = {
400
+
401
+ "HistGradientBoosting":
402
+ HistGradientBoostingClassifier(
403
+ max_iter=180,
404
+ learning_rate=0.05,
405
+ max_leaf_nodes=15,
406
+ l2_regularization=2.0,
407
+ random_state=42
408
+ ),
409
+
410
+ "ExtraTrees":
411
+ ExtraTreesClassifier(
412
+ n_estimators=300,
413
+ max_depth=8,
414
+ min_samples_leaf=5,
415
+ max_features=0.8,
416
+ class_weight="balanced",
417
+ random_state=42,
418
+ n_jobs=-1
419
+ ),
420
+
421
+ "LogisticRegression":
422
+ Pipeline([
423
+ ("scaler", StandardScaler()),
424
+ ("model", LogisticRegression(
425
+ C=0.2,
426
+ max_iter=2000,
427
+ class_weight="balanced",
428
+ random_state=42
429
+ ))
430
+ ])
431
  }
432
+
433
+ return models
434
+
435
+
436
+ def ensemble_predict(X_train, y_train, X_test):
437
+
438
+ models = create_models()
439
+
440
+ probabilities = []
441
+
442
+ for name, model in models.items():
443
+
444
+ model.fit(X_train, y_train)
445
+
446
+ probability = model.predict_proba(X_test)[:, 1]
447
+
448
+ probabilities.append(probability)
449
+
450
+ ensemble_probability = np.mean(
451
+ probabilities,
452
+ axis=0
453
+ )
454
+
455
+ return ensemble_probability
456
+
457
+
458
+ # =========================================================
459
+ # WALK FORWARD VALIDATION
460
+ # =========================================================
461
+
462
+ def walk_forward_validation(X, y):
463
+
464
+ splitter = TimeSeriesSplit(n_splits=4)
465
+
466
+ scores = []
467
+ fold_results = []
468
+
469
+ for fold, (train_idx, test_idx) in enumerate(
470
+ splitter.split(X),
471
+ start=1
472
+ ):
473
+
474
+ X_train = X.iloc[train_idx]
475
+ X_test = X.iloc[test_idx]
476
+
477
+ y_train = y.iloc[train_idx]
478
+ y_test = y.iloc[test_idx]
479
+
480
+ if y_train.nunique() < 2:
481
  continue
482
+
483
+ probabilities = ensemble_predict(
484
+ X_train,
485
+ y_train,
486
+ X_test
487
+ )
488
+
489
+ predictions = (
490
+ probabilities >= 0.5
491
+ ).astype(int)
492
+
493
+ score = balanced_accuracy_score(
494
+ y_test,
495
+ predictions
496
+ )
497
+
498
+ scores.append(score)
499
+
500
+ fold_results.append(
501
+ f"Fold {fold}: {score * 100:.2f}%"
502
+ )
503
+
504
+ if not scores:
505
+ raise ValueError(
506
+ "Walk-forward validation yapılamadı."
507
+ )
508
+
509
+ return np.mean(scores), fold_results
510
+
511
+
512
+ # =========================================================
513
+ # BACKTEST
514
+ # =========================================================
515
+
516
+ def run_backtest(
517
+ raw,
518
+ test,
519
+ probabilities,
520
+ horizon,
521
+ threshold,
522
+ fee_pct
523
+ ):
524
+
525
+ trades = []
526
+
527
+ next_allowed_bar = -1
528
+
529
+ fee_rate = fee_pct / 100
530
+
531
+ for timestamp, row in test.iterrows():
532
+
533
+ raw_position = raw.index.get_loc(timestamp)
534
+
535
+ if raw_position < next_allowed_bar:
536
  continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
537
 
538
+ probability = float(
539
+ probabilities[test.index.get_loc(timestamp)]
540
+ )
541
 
542
+ if probability >= threshold:
543
+ side = "LONG"
544
 
545
+ elif probability <= (1 - threshold):
546
+ side = "SHORT"
547
 
548
+ else:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
549
  continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
550
 
551
+ entry = float(row["close"])
552
+ atr = float(row["atr"])
553
+
554
+ if not np.isfinite(atr) or atr <= 0:
555
+ continue
556
+
557
+ risk_distance = atr * 1.5
558
+
559
+ if side == "LONG":
560
+
561
+ stop = entry - risk_distance
562
+ target = entry + risk_distance * 2
563
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
564
  else:
 
 
 
 
 
 
 
565
 
566
+ stop = entry + risk_distance
567
+ target = entry - risk_distance * 2
568
 
569
+ future = raw.iloc[
570
+ raw_position + 1:
571
+ raw_position + horizon + 1
572
+ ]
 
 
573
 
574
+ if len(future) < horizon:
575
+ continue
576
 
577
+ exit_price = float(future["close"].iloc[-1])
578
+ exit_reason = "HORIZON"
 
 
 
 
 
 
579
 
580
+ for candle in future.itertuples():
581
 
582
+ high = float(candle.high)
583
+ low = float(candle.low)
584
 
585
+ if side == "LONG":
586
 
587
+ # Aynı mumda iki seviyeye de dokunursa
588
+ # ihtiyatlı olarak stop önce kabul edilir.
589
+ if low <= stop:
590
+ exit_price = stop
591
+ exit_reason = "STOP"
592
+ break
593
+
594
+ if high >= target:
595
+ exit_price = target
596
+ exit_reason = "TARGET"
597
+ break
598
+
599
+ else:
600
+
601
+ if high >= stop:
602
+ exit_price = stop
603
+ exit_reason = "STOP"
604
+ break
605
+
606
+ if low <= target:
607
+ exit_price = target
608
+ exit_reason = "TARGET"
609
+ break
610
+
611
+ if side == "LONG":
612
+ gross_return = (exit_price - entry) / entry
613
+
614
+ else:
615
+ gross_return = (entry - exit_price) / entry
616
+
617
+ net_return = gross_return - (2 * fee_rate)
618
+
619
+ trades.append({
620
+ "time": timestamp,
621
+ "side": side,
622
+ "entry": entry,
623
+ "exit": exit_price,
624
+ "probability": probability,
625
+ "gross_return": gross_return * 100,
626
+ "net_return": net_return * 100,
627
+ "result": "WIN" if net_return > 0 else "LOSS",
628
+ "exit_reason": exit_reason
629
+ })
630
+
631
+ # Pozisyonlar çakışmasın.
632
+ next_allowed_bar = raw_position + horizon
633
+
634
+ if not trades:
635
 
636
+ return {
637
+ "count": 0,
638
+ "winrate": 0,
639
+ "profit_factor": 0,
640
+ "total_return": 0,
641
+ "trades": []
642
+ }
643
 
644
+ trade_df = pd.DataFrame(trades)
 
 
 
645
 
646
+ wins = trade_df[
647
+ trade_df["net_return"] > 0
648
+ ]["net_return"]
649
 
650
+ losses = trade_df[
651
+ trade_df["net_return"] < 0
652
+ ]["net_return"]
653
 
654
+ gross_profit = wins.sum()
655
+ gross_loss = abs(losses.sum())
656
+
657
+ profit_factor = (
658
+ gross_profit / gross_loss
659
+ if gross_loss > 0
660
+ else float("inf") if gross_profit > 0 else 0
661
+ )
662
+
663
+ winrate = (
664
+ len(wins) / len(trade_df)
665
+ ) * 100
666
+
667
+ total_return = trade_df["net_return"].sum()
668
 
 
669
  return {
670
+ "count": len(trade_df),
671
+ "winrate": winrate,
672
+ "profit_factor": profit_factor,
673
+ "total_return": total_return,
674
+ "trades": trades
 
 
 
 
675
  }
676
 
677
 
678
+ # =========================================================
679
+ # MAIN ANALYSIS
680
+ # =========================================================
681
+
682
+ def analyze(
683
+ timeframe,
684
+ candles,
685
+ horizon,
686
+ threshold,
687
+ fee_pct
688
+ ):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
689
 
 
 
690
  try:
691
+
692
+ candles = int(candles)
693
+ horizon = int(horizon)
694
+ threshold = float(threshold)
695
+ fee_pct = float(fee_pct)
696
+
697
+ if candles < 700:
698
+ return "En az 700 mum gerekli."
699
+
700
+ if horizon < 1 or horizon > 48:
701
+ return "Horizon 1 ile 48 arasında olmalı."
702
+
703
+ if not 0.50 < threshold < 0.90:
704
+ return "Threshold 0.50 ile 0.90 arasında olmalı."
705
+
706
+ if fee_pct < 0 or fee_pct > 1:
707
+ return "Komisyon 0 ile 1 arasında olmalı."
708
+
709
+ # -------------------------------------------------
710
+ # DATA
711
+ # -------------------------------------------------
712
+
713
+ raw = fetch_data(
714
+ timeframe=timeframe,
715
+ candles=candles
716
+ )
717
+
718
+ data = prepare_dataset(
719
+ raw,
720
+ horizon
721
+ )
722
+
723
+ X = data[FEATURES]
724
+ y = data["target"].astype(int)
725
+
726
+ # -------------------------------------------------
727
+ # CHRONOLOGICAL SPLIT
728
+ # -------------------------------------------------
729
+
730
+ split = int(len(data) * 0.80)
731
+
732
+ X_train = X.iloc[:split]
733
+ X_test = X.iloc[split:]
734
+
735
+ y_train = y.iloc[:split]
736
+ y_test = y.iloc[split:]
737
+
738
+ if y_train.nunique() < 2:
739
+ return "Eğitim verisinde iki sınıf da bulunmalı."
740
+
741
+ # -------------------------------------------------
742
+ # WALK FORWARD
743
+ # -------------------------------------------------
744
+
745
+ wf_score, fold_results = walk_forward_validation(
746
+ X_train,
747
+ y_train
748
+ )
749
+
750
+ # -------------------------------------------------
751
+ # FINAL HOLDOUT
752
+ # -------------------------------------------------
753
+
754
+ probabilities = ensemble_predict(
755
+ X_train,
756
+ y_train,
757
+ X_test
758
+ )
759
+
760
+ predictions = (
761
+ probabilities >= 0.5
762
+ ).astype(int)
763
+
764
+ accuracy = accuracy_score(
765
+ y_test,
766
+ predictions
767
+ )
768
+
769
+ balanced = balanced_accuracy_score(
770
+ y_test,
771
+ predictions
772
+ )
773
+
774
  try:
775
+ auc = roc_auc_score(
776
+ y_test,
777
+ probabilities
778
+ )
779
+ except Exception:
780
+ auc = 0.5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
781
 
782
+ # -------------------------------------------------
783
+ # BACKTEST
784
+ # -------------------------------------------------
785
+
786
+ backtest = run_backtest(
787
+ raw=raw,
788
+ test=data.iloc[split:],
789
+ probabilities=probabilities,
790
+ horizon=horizon,
791
+ threshold=threshold,
792
+ fee_pct=fee_pct
793
+ )
794
+
795
+ # -------------------------------------------------
796
+ # CURRENT SIGNAL
797
+ # -------------------------------------------------
798
+
799
+ latest = calculate_indicators(raw).dropna(
800
+ subset=FEATURES + ["atr"]
801
+ ).iloc[-1]
802
+
803
+ latest_X = pd.DataFrame(
804
+ [latest[FEATURES].values],
805
+ columns=FEATURES
806
+ )
807
+
808
+ latest_probability = ensemble_predict(
809
+ X,
810
+ y,
811
+ latest_X
812
+ )[0]
813
+
814
+ p_up = float(latest_probability)
815
+ p_down = 1 - p_up
816
+
817
+ if p_up >= threshold:
818
+ signal = "LONG"
819
+
820
+ elif p_down >= threshold:
821
+ signal = "SHORT"
822
+
823
+ else:
824
+ signal = "NÖTR"
825
+
826
+ # -------------------------------------------------
827
+ # ENTRY / STOP / TARGET
828
+ # -------------------------------------------------
829
+
830
+ entry = float(latest["close"])
831
+ atr = float(latest["atr"])
832
+
833
+ if signal == "LONG":
834
+
835
+ stop = entry - 1.5 * atr
836
+ target = entry + 3.0 * atr
837
+
838
+ elif signal == "SHORT":
839
+
840
+ stop = entry + 1.5 * atr
841
+ target = entry - 3.0 * atr
842
+
843
+ else:
844
+
845
+ stop = None
846
+ target = None
847
+
848
+ # -------------------------------------------------
849
+ # INDICATOR INTERPRETATION
850
+ # -------------------------------------------------
851
+
852
+ rsi = float(latest["rsi"])
853
+
854
+ macd_value = float(latest["macd"])
855
+ macd_signal = float(latest["macd_signal"])
856
+
857
+ st_direction = int(
858
+ latest["supertrend_direction"]
859
+ )
860
+
861
+ vwap_distance = float(
862
+ latest["vwap_distance"]
863
+ )
864
+
865
+ indicator_text = (
866
+ f"RSI: {rsi:.2f}\n"
867
+ f"MACD: {'BULLISH' if macd_value > macd_signal else 'BEARISH'}\n"
868
+ f"Supertrend: {'BULLISH' if st_direction == 1 else 'BEARISH'}\n"
869
+ f"VWAP: {'ÜSTÜNDE' if vwap_distance > 0 else 'ALTINDA'}\n"
870
+ f"VWAP uzaklığı: {vwap_distance:.3f}%\n"
871
+ )
872
+
873
+ # -------------------------------------------------
874
+ # RESULTS
875
+ # -------------------------------------------------
876
+
877
+ pf = backtest["profit_factor"]
878
+
879
+ if np.isinf(pf):
880
+ pf_text = "∞"
881
+ else:
882
+ pf_text = f"{pf:.2f}"
883
+
884
+ if signal == "LONG":
885
+ signal_text = "LONG"
886
+ elif signal == "SHORT":
887
+ signal_text = "SHORT"
888
+ else:
889
+ signal_text = "NÖTR"
890
+
891
+ output = f"""
892
+ # BTC/USDT AI ANALİZİ
893
+
894
+ <badge> {signal_text} </badge>
895
+
896
+ ## Güncel tahmin
897
+
898
+ | Metrik | Sonuç |
899
+ |---|---:|
900
+ | BTC fiyatı | {entry:,.2f} USDT |
901
+ | LONG olasılığı | {p_up * 100:.2f}% |
902
+ | SHORT olasılığı | {p_down * 100:.2f}% |
903
+ | Model sinyali | **{signal}** |
904
+ | Zaman dilimi | {timeframe} |
905
+ | Tahmin horizon | {horizon} mum |
906
+
907
+ ## TradingView indikatörleri
908
+
909
+ ```text
910
+ {indicator_text}
911
+ ```
912
+
913
+ ## İşlem seviyeleri
914
+
915
+ | Seviye | Fiyat |
916
+ |---|---:|
917
+ | Giriş | {entry:,.2f} |
918
+ | Stop Loss | {stop:,.2f if stop is not None else "—"} |
919
+ | 2R Hedef | {target:,.2f if target is not None else "—"} |
920
+
921
+ ## Bağımsız hold-out test
922
+
923
+ | Metrik | Sonuç |
924
+ |---|---:|
925
+ | Accuracy | {accuracy * 100:.2f}% |
926
+ | Balanced Accuracy | {balanced * 100:.2f}% |
927
+ | AUC | {auc:.4f} |
928
+ | Walk-forward Balanced Accuracy | {wf_score * 100:.2f}% |
929
+
930
+ ## Backtest sonuçları
931
+
932
+ | Metrik | Sonuç |
933
+ |---|---:|
934
+ | İşlem sayısı | {backtest["count"]} |
935
+ | Win Rate | {backtest["winrate"]:.2f}% |
936
+ | Profit Factor | {pf_text} |
937
+ | Net toplam getiri | {backtest["total_return"]:.2f}% |
938
+ | İşlem komisyonu | %{fee_pct:.3f} / taraf |
939
+
940
+ ## Walk-forward fold sonuçları
941
+
942
+ """ + "\n".join(
943
+ f"- {x}" for x in fold_results
944
+ )
945
+
946
+ if backtest["trades"]:
947
+
948
+ trade_df = pd.DataFrame(
949
+ backtest["trades"]
950
+ )
951
+
952
+ output += "\n\n## Son backtest işlemleri\n\n"
953
+
954
+ output += trade_df.tail(15).to_markdown(
955
+ index=False
956
+ )
957
+
958
+ output += """
959
+
960
+ ---
961
+
962
+ **Uyarı:** Bu bir istatistiksel model tahminidir, kesin fiyat tahmini değildir. Hold-out sonuçları geçmiş veriye aittir. Komisyon, kayma ve gerçek emir gerçekleşmesi sonuçları değiştirebilir. NÖTR sinyali işlem açmama anlamına gelir.
963
+ """
964
+
965
+ return output
966
+
967
+ except Exception as e:
968
+
969
+ return (
970
+ "# HATA\n\n"
971
+ f"`{type(e).__name__}: {str(e)}`\n\n"
972
+ "Bağlantıyı, Binance erişimini ve paket kurulumlarını kontrol et."
973
+ )
974
+
975
+
976
+ # =========================================================
977
+ # GRADIO UI
978
+ # =========================================================
979
+
980
+ with gr.Blocks(
981
+ title="BTC AI Indicator Ensemble"
982
+ ) as app:
983
+
984
+ gr.Markdown(
985
+ """
986
+ # BTC AI — Indicator Ensemble
987
+ MCP'siz, Binance verisiyle çalışan BTC/USDT tahmin sistemi.
988
+
989
+ **Supertrend + RSI + MACD + VWAP + Machine Learning**
990
+ """
991
+ )
992
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
993
  with gr.Row():
994
+
995
+ timeframe = gr.Dropdown(
996
+ choices=[
997
+ "5m",
998
+ "15m",
999
+ "30m",
1000
+ "1h",
1001
+ "4h",
1002
+ "1d"
1003
+ ],
1004
+ value="1h",
1005
+ label="Timeframe"
1006
+ )
1007
+
1008
+ candles = gr.Dropdown(
1009
+ choices=[
1010
+ 1000,
1011
+ 1500,
1012
+ 2000,
1013
+ 3000,
1014
+ 4000,
1015
+ 5000
1016
+ ],
1017
+ value=2000,
1018
+ label="Mum sayısı"
1019
+ )
1020
+
1021
  with gr.Row():
1022
+
1023
+ horizon = gr.Slider(
1024
+ minimum=1,
1025
+ maximum=24,
1026
+ value=6,
1027
+ step=1,
1028
+ label="Tahmin horizon (mum)"
1029
+ )
1030
+
1031
+ threshold = gr.Slider(
1032
+ minimum=0.51,
1033
+ maximum=0.80,
1034
+ value=0.56,
1035
+ step=0.01,
1036
+ label="Sinyal güven eşiği"
1037
+ )
1038
+
1039
+ fee = gr.Slider(
1040
+ minimum=0,
1041
+ maximum=0.20,
1042
+ value=0.10,
1043
+ step=0.01,
1044
+ label="Komisyon (%) / taraf"
1045
+ )
1046
+
1047
+ run_button = gr.Button(
1048
+ "BTC ANALİZ ET",
1049
+ variant="primary"
1050
+ )
1051
+
1052
+ output = gr.Markdown()
1053
+
1054
+ run_button.click(
1055
+ fn=analyze,
1056
+ inputs=[
1057
+ timeframe,
1058
+ candles,
1059
+ horizon,
1060
+ threshold,
1061
+ fee
1062
+ ],
1063
+ outputs=output
1064
+ )
1065
+
1066
 
1067
  if __name__ == "__main__":
1068
+ app.launch()