GoshawkVortexAI commited on
Commit
29cc41a
·
verified ·
1 Parent(s): a97715e

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -1916
app.py DELETED
@@ -1,1916 +0,0 @@
1
- """
2
- OKX QUANT TERMINAL · v2 — ELITE SIGNAL ENGINE
3
- Hedge-fund-style crypto market analytics dashboard built on Gradio.
4
-
5
- Uses ONLY OKX's public (unauthenticated) market-data REST API:
6
- - /api/v5/market/candles
7
- - /api/v5/market/ticker
8
- - /api/v5/market/tickers
9
- - /api/v5/market/books
10
- - /api/v5/public/funding-rate (türev duyarlılığı, isteğe bağlı)
11
-
12
- No API key, no order placement, no account access. This is a read-only
13
- analytics / signal-generation terminal, not an execution bot.
14
-
15
- v2 SİNYAL MOTORU — YENİLİKLER
16
- * Yeni faktörler: Supertrend, Ichimoku bulutu, Donchian kırılımı (hacim teyitli),
17
- piyasa yapısı (HH/HL – LH/LL), CMF, MFI, mum formasyonları, Bollinger/Keltner sıkışması
18
- * Rejim ağırlıkları artık ADX ile SÜREKLİ (yumuşak) geçiş yapar (eski: sert eşik)
19
- * Skor tanh ile ±10'a yumuşak sıkıştırılır; "GÜÇLÜ" sinyal için faktör konsensüsü + MTF şartı
20
- * 2 kademeli çoklu zaman dilimi (MTF) teyidi
21
- * Canlı mikro-yapı katmanı: emir defteri dengesizliği, funding rate, yakın destek/direnç
22
- * Yapısal (S/R) stop + ATR stop, 3 kademeli hedef, hesap büyüklüğüne göre pozisyon boyutu
23
- * Gerçek işlem simülasyonu (SL/TP/zaman çıkışı + komisyon): kazanma oranı, profit factor,
24
- beklenti, R çarpanı, SQN, drawdown, Al&Tut karşılaştırması, yarım-Kelly risk önerisi
25
- * Bileşenli GÜVEN skoru + A+ … D kalite notu, faktör katkı grafiği, sinyal işaretli fiyat grafiği
26
- * "Sadece kapanmış mumlar" modu (repaint yok), veri önbelleği, paralel veri çekimi
27
- * Gann HiLo Activator (trend yönü + dönüş) ve Fibonacci düzeltme seviyeleri (0.236–0.786,
28
- altın bölge 0.382–0.618) artık kompozit skoru ve dolayısıyla AL/SAT sinyalini değiştirir
29
- """
30
-
31
- import math
32
- import threading
33
- import time
34
- from concurrent.futures import ThreadPoolExecutor, as_completed
35
-
36
- import numpy as np
37
- import pandas as pd
38
- import plotly.graph_objects as go
39
- from plotly.subplots import make_subplots
40
- import requests
41
- import gradio as gr
42
-
43
- try: # Hugging Face Spaces (ZeroGPU) — yerelde yoksa sessizce atla
44
- import spaces
45
- _gpu = spaces.GPU(duration=30)
46
- except Exception:
47
- def _gpu(fn):
48
- return fn
49
-
50
- # ---------------------------------------------------------------------------
51
- # CONFIG
52
- # ---------------------------------------------------------------------------
53
- OKX_BASE = "https://www.okx.com"
54
- TIMEOUT = 10
55
- CACHE_TTL = 8 # saniye — aynı mum verisini sekmeler arasında tekrar çekmemek için
56
-
57
- POPULAR_SYMBOLS = [
58
- "BTC-USDT", "ETH-USDT", "SOL-USDT", "BNB-USDT", "XRP-USDT",
59
- "DOGE-USDT", "ADA-USDT", "AVAX-USDT", "LINK-USDT", "TON-USDT",
60
- "TRX-USDT", "DOT-USDT", "MATIC-USDT", "LTC-USDT", "NEAR-USDT",
61
- "SUI-USDT", "APT-USDT", "ARB-USDT", "OP-USDT", "INJ-USDT",
62
- ]
63
-
64
- BAR_OPTIONS = ["1m", "3m", "5m", "15m", "30m", "1H", "2H", "4H", "6H", "12H", "1D", "1W"]
65
-
66
- # approximate periods-per-year for each bar size, used to annualize risk stats
67
- BAR_TO_PERIODS_PER_YEAR = {
68
- "1m": 365 * 24 * 60, "3m": 365 * 24 * 20, "5m": 365 * 24 * 12,
69
- "15m": 365 * 24 * 4, "30m": 365 * 24 * 2, "1H": 365 * 24,
70
- "2H": 365 * 12, "4H": 365 * 6, "6H": 365 * 4, "12H": 365 * 2,
71
- "1D": 365, "1W": 52,
72
- }
73
-
74
- CHART_BG = "#0a0e14"
75
-
76
- # her zaman dilimi için "bir üst" zaman dilimi -> çoklu zaman dilimi (MTF) teyidi için
77
- MTF_MAP = {
78
- "1m": "15m", "3m": "30m", "5m": "1H", "15m": "4H", "30m": "4H",
79
- "1H": "4H", "2H": "1D", "4H": "1D", "6H": "1D", "12H": "1W",
80
- "1D": "1W", "1W": "1W",
81
- }
82
- SLOW_BARS = {"4H", "6H", "12H", "1D", "1W"} # bu vadelerde emir defteri etkisi yarıya iner
83
-
84
- # --- Sinyal motoru parametreleri ---
85
- SIGNAL_THRESHOLD = 2.0 # |skor| >= bu değer -> AL / SAT
86
- STRONG_THRESHOLD = 4.5 # |skor| >= bu değer + konsensüs -> GÜÇLÜ
87
- WARMUP_BARS = 80 # Ichimoku (52+26) ısınması için minimum bar
88
- BACKTEST_LOOKBACK = 220 # walk-forward test penceresi (bar)
89
- SL_ATR, TP_ATR = 1.5, 2.5 # backtest ve varsayılan işlem planı (ATR çarpanları)
90
- MAX_HOLD_BARS = 24 # zaman çıkışı
91
- HILO_PERIOD = 10 # Gann HiLo Activator ortalama periyodu
92
- FIB_LOOKBACK = 89 # Fibonacci swing penceresi (bar) — bir Fibonacci sayısı
93
- FIB_MIN_BARS = 34 # pencere dolmadan da hesaplamaya başlamak için minimum bar
94
- FIB_MIN_RANGE_ATR = 2.5 # swing aralığı bu kadar ATR'den küçükse Fib anlamsız sayılır
95
- FIB_RATIOS = (0.236, 0.382, 0.5, 0.618, 0.786)
96
- FEE_PER_SIDE = 0.001 # OKX spot taker ≈ %0.1 / yön
97
- TP_R_MULTIPLES = (TP_ATR / SL_ATR, 2.5, 4.0) # TP1/TP2/TP3 (R cinsinden)
98
-
99
-
100
- # ---------------------------------------------------------------------------
101
- # OKX PUBLIC API WRAPPERS (önbellek + yeniden deneme)
102
- # ---------------------------------------------------------------------------
103
- _CACHE = {}
104
- _CACHE_LOCK = threading.Lock()
105
-
106
-
107
- def _cached(key, ttl, loader):
108
- now = time.time()
109
- with _CACHE_LOCK:
110
- hit = _CACHE.get(key)
111
- if hit and now - hit[0] < ttl:
112
- return hit[1]
113
- val = loader()
114
- with _CACHE_LOCK:
115
- _CACHE[key] = (time.time(), val)
116
- if len(_CACHE) > 200: # eski kayıtları buda
117
- for k in sorted(_CACHE, key=lambda x: _CACHE[x][0])[:60]:
118
- _CACHE.pop(k, None)
119
- return val
120
-
121
-
122
- def _get(path, params=None, retries=2):
123
- for attempt in range(retries + 1):
124
- try:
125
- r = requests.get(f"{OKX_BASE}{path}", params=params, timeout=TIMEOUT)
126
- r.raise_for_status()
127
- data = r.json()
128
- code = str(data.get("code"))
129
- if code == "50011" and attempt < retries: # OKX rate-limit
130
- time.sleep(0.5 * (attempt + 1))
131
- continue
132
- if code != "0":
133
- raise RuntimeError(data.get("msg") or "OKX API hata döndürdü.")
134
- return data["data"]
135
- except requests.exceptions.RequestException as e:
136
- if attempt < retries:
137
- time.sleep(0.3 * (attempt + 1))
138
- continue
139
- raise RuntimeError(f"Ağ hatası: {e}")
140
-
141
-
142
- def fetch_candles(inst_id, bar="1H", limit=300):
143
- def _load():
144
- raw = _get("/api/v5/market/candles", {"instId": inst_id, "bar": bar, "limit": limit})
145
- if not raw:
146
- raise RuntimeError(f"{inst_id} için mum verisi bulunamadı (sembolü kontrol edin).")
147
- cols = ["ts", "o", "h", "l", "c", "vol", "volCcy", "volCcyQuote", "confirm"]
148
- df = pd.DataFrame(raw, columns=cols)
149
- for c in ["o", "h", "l", "c", "vol"]:
150
- df[c] = df[c].astype(float)
151
- df["confirm"] = df["confirm"].astype(int)
152
- df["ts"] = pd.to_datetime(df["ts"].astype(np.int64), unit="ms")
153
- return df.sort_values("ts").reset_index(drop=True)
154
-
155
- return _cached(("candles", inst_id, bar, limit), CACHE_TTL, _load).copy()
156
-
157
-
158
- def fetch_ticker(inst_id):
159
- raw = _get("/api/v5/market/ticker", {"instId": inst_id})
160
- return raw[0] if raw else {}
161
-
162
-
163
- def fetch_all_tickers(inst_type="SPOT"):
164
- return _cached(("tickers", inst_type), 20, lambda: _get("/api/v5/market/tickers", {"instType": inst_type}))
165
-
166
-
167
- def fetch_orderbook(inst_id, depth=20):
168
- raw = _get("/api/v5/market/books", {"instId": inst_id, "sz": depth})
169
- return raw[0] if raw else {}
170
-
171
-
172
- def fetch_funding_rate(inst_id):
173
- """Spot paritenin (BTC-USDT) sürekli vadeli karşılığının (BTC-USDT-SWAP) funding oranı."""
174
- parts = inst_id.split("-")
175
- if len(parts) != 2:
176
- return None
177
- raw = _get("/api/v5/public/funding-rate", {"instId": f"{inst_id}-SWAP"})
178
- if not raw:
179
- return None
180
- return float(raw[0].get("fundingRate") or 0.0)
181
-
182
-
183
- # ---------------------------------------------------------------------------
184
- # TECHNICAL INDICATORS
185
- # ---------------------------------------------------------------------------
186
- def ema(s, p):
187
- return s.ewm(span=p, adjust=False).mean()
188
-
189
-
190
- def sma(s, p):
191
- return s.rolling(p).mean()
192
-
193
-
194
- def rsi(s, p=14):
195
- d = s.diff()
196
- gain = d.clip(lower=0)
197
- loss = -d.clip(upper=0)
198
- ag = gain.ewm(alpha=1 / p, adjust=False).mean()
199
- al = loss.ewm(alpha=1 / p, adjust=False).mean()
200
- rs = ag / al.replace(0, np.nan)
201
- return (100 - 100 / (1 + rs)).fillna(50)
202
-
203
-
204
- def macd(s, fast=12, slow=26, signal=9):
205
- line = ema(s, fast) - ema(s, slow)
206
- sig = ema(line, signal)
207
- return line, sig, line - sig
208
-
209
-
210
- def bollinger(s, p=20, k=2):
211
- mid = sma(s, p)
212
- std = s.rolling(p).std()
213
- return mid + k * std, mid, mid - k * std
214
-
215
-
216
- def atr(df, p=14):
217
- h, l, c = df["h"], df["l"], df["c"]
218
- pc = c.shift(1)
219
- tr = pd.concat([h - l, (h - pc).abs(), (l - pc).abs()], axis=1).max(axis=1)
220
- return tr.ewm(alpha=1 / p, adjust=False).mean()
221
-
222
-
223
- def stochastic(df, k_p=14, d_p=3):
224
- lo = df["l"].rolling(k_p).min()
225
- hi = df["h"].rolling(k_p).max()
226
- k = 100 * (df["c"] - lo) / (hi - lo).replace(0, np.nan)
227
- d = k.rolling(d_p).mean()
228
- return k.fillna(50), d.fillna(50)
229
-
230
-
231
- def vwap_window(df):
232
- tp = (df["h"] + df["l"] + df["c"]) / 3
233
- return (tp * df["vol"]).cumsum() / df["vol"].cumsum().replace(0, np.nan)
234
-
235
-
236
- def adx(df, p=14):
237
- """ADX + yönsel indikatörler (+DI / -DI). Trend gücünü ölçer (0-100)."""
238
- h, l, c = df["h"], df["l"], df["c"]
239
- up_move = h.diff()
240
- down_move = -l.diff()
241
- plus_dm = np.where((up_move > down_move) & (up_move > 0), up_move, 0.0)
242
- minus_dm = np.where((down_move > up_move) & (down_move > 0), down_move, 0.0)
243
- tr = pd.concat([h - l, (h - c.shift(1)).abs(), (l - c.shift(1)).abs()], axis=1).max(axis=1)
244
- atr_ = tr.ewm(alpha=1 / p, adjust=False).mean()
245
- plus_di = 100 * pd.Series(plus_dm, index=df.index).ewm(alpha=1 / p, adjust=False).mean() / atr_.replace(0, np.nan)
246
- minus_di = 100 * pd.Series(minus_dm, index=df.index).ewm(alpha=1 / p, adjust=False).mean() / atr_.replace(0, np.nan)
247
- dx = ((plus_di - minus_di).abs() / (plus_di + minus_di).replace(0, np.nan)) * 100
248
- adx_ = dx.ewm(alpha=1 / p, adjust=False).mean()
249
- return adx_.fillna(0), plus_di.fillna(0), minus_di.fillna(0)
250
-
251
-
252
- def obv(df):
253
- """On-Balance Volume: hacmin fiyat yönüyle kümülatif toplamı."""
254
- direction = np.sign(df["c"].diff()).fillna(0)
255
- return (direction * df["vol"]).cumsum()
256
-
257
-
258
- def supertrend(df, period=10, mult=3.0):
259
- """Supertrend: ATR bantlı trend takipçisi. dir=+1 yükseliş, -1 düşüş. (nedensel, geleceğe bakmaz)"""
260
- h, l, c = df["h"].to_numpy(), df["l"].to_numpy(), df["c"].to_numpy()
261
- a = atr(df, period).to_numpy()
262
- hl2 = (h + l) / 2
263
- ub, lb = hl2 + mult * a, hl2 - mult * a
264
- n = len(c)
265
- fub, flb = ub.copy(), lb.copy()
266
- direction = np.ones(n)
267
- line = np.full(n, np.nan)
268
- if n:
269
- line[0] = flb[0]
270
- for i in range(1, n):
271
- fub[i] = ub[i] if (ub[i] < fub[i - 1] or c[i - 1] > fub[i - 1]) else fub[i - 1]
272
- flb[i] = lb[i] if (lb[i] > flb[i - 1] or c[i - 1] < flb[i - 1]) else flb[i - 1]
273
- if c[i] > fub[i - 1]:
274
- direction[i] = 1
275
- elif c[i] < flb[i - 1]:
276
- direction[i] = -1
277
- else:
278
- direction[i] = direction[i - 1]
279
- line[i] = flb[i] if direction[i] == 1 else fub[i]
280
- return pd.Series(line, index=df.index), pd.Series(direction, index=df.index)
281
-
282
-
283
- def ichimoku(df):
284
- """Tenkan/Kijun + bulut. Bulut, 26 bar önce hesaplanan Senkou A/B'nin bugüne kaydırılmış halidir
285
- (yani nedensel: gelecek veri kullanılmaz)."""
286
- h, l = df["h"], df["l"]
287
- tenkan = (h.rolling(9).max() + l.rolling(9).min()) / 2
288
- kijun = (h.rolling(26).max() + l.rolling(26).min()) / 2
289
- span_a = (tenkan + kijun) / 2
290
- span_b = (h.rolling(52).max() + l.rolling(52).min()) / 2
291
- return tenkan, kijun, span_a.shift(26), span_b.shift(26)
292
-
293
-
294
- def hilo_activator(df, period=HILO_PERIOD):
295
- """Gann HiLo Activator. Kapanış önceki barın SMA(High) değerini aşarsa YÜKSELİŞ (çizgi=SMA(Low)),
296
- önceki barın SMA(Low) değerinin altına inerse DÜŞÜŞ (çizgi=SMA(High)) moduna geçer; arada mod korunur.
297
- Dönüş: (çizgi, yön) yön: +1 / -1 / 0 (henüz belirsiz). Nedensel — geleceğe bakmaz."""
298
- hi_ma = df["h"].rolling(period).mean()
299
- lo_ma = df["l"].rolling(period).mean()
300
- c = df["c"].to_numpy(dtype=float)
301
- hi_p, lo_p = hi_ma.shift(1).to_numpy(), lo_ma.shift(1).to_numpy()
302
- hi_c, lo_c = hi_ma.to_numpy(), lo_ma.to_numpy()
303
- n = len(c)
304
- direction = np.zeros(n)
305
- line = np.full(n, np.nan)
306
- cur = 0.0
307
- for i in range(n):
308
- if hi_p[i] == hi_p[i] and c[i] > hi_p[i]:
309
- cur = 1.0
310
- elif lo_p[i] == lo_p[i] and c[i] < lo_p[i]:
311
- cur = -1.0
312
- direction[i] = cur
313
- if cur > 0:
314
- line[i] = lo_c[i]
315
- elif cur < 0:
316
- line[i] = hi_c[i]
317
- return pd.Series(line, index=df.index), pd.Series(direction, index=df.index)
318
-
319
-
320
- def fib_swing(df, lookback=FIB_LOOKBACK, min_bars=FIB_MIN_BARS):
321
- """Her bar için son `lookback` bardaki en yüksek/en düşük nokta ve dalga yönü.
322
- yön=+1: dip tepeden ÖNCE (yükseliş dalgası -> düzeltmede destek aranır)
323
- yön=-1: tepe dipten ÖNCE (düşüş dalgası -> tepkide direnç aranır). Nedensel."""
324
- h, l = df["h"], df["l"]
325
- hi = h.rolling(lookback, min_periods=min_bars).max()
326
- lo = l.rolling(lookback, min_periods=min_bars).min()
327
- hi_pos = h.rolling(lookback, min_periods=min_bars).apply(np.argmax, raw=True)
328
- lo_pos = l.rolling(lookback, min_periods=min_bars).apply(np.argmin, raw=True)
329
- direction = pd.Series(np.where(lo_pos < hi_pos, 1.0, -1.0), index=df.index)
330
- direction = direction.where(hi_pos.notna() & lo_pos.notna())
331
- return hi, lo, direction
332
-
333
-
334
- def fib_levels(hi, lo, direction):
335
- """Dalga yönüne göre Fibonacci düzeltme fiyatları: [(oran, fiyat), ...]"""
336
- rng = hi - lo
337
- if direction > 0:
338
- return [(r, hi - r * rng) for r in FIB_RATIOS]
339
- return [(r, lo + r * rng) for r in FIB_RATIOS]
340
-
341
-
342
- def mfi(df, p=14):
343
- """Money Flow Index: hacim ağırlıklı RSI."""
344
- tp = (df["h"] + df["l"] + df["c"]) / 3
345
- rmf = tp * df["vol"]
346
- d = tp.diff()
347
- pos = rmf.where(d > 0, 0.0).rolling(p).sum()
348
- neg = rmf.where(d < 0, 0.0).rolling(p).sum()
349
- out = 100 - 100 / (1 + pos / neg.replace(0, np.nan))
350
- out = out.where(~((neg == 0) & (pos > 0)), 100.0)
351
- return out.fillna(50)
352
-
353
-
354
- def cmf(df, p=20):
355
- """Chaikin Money Flow: kapanışın bar aralığındaki konumuna göre hacim akışı (-1..+1)."""
356
- rng = (df["h"] - df["l"]).replace(0, np.nan)
357
- mfm = ((df["c"] - df["l"]) - (df["h"] - df["c"])) / rng
358
- mfv = mfm.fillna(0) * df["vol"]
359
- return (mfv.rolling(p).sum() / df["vol"].rolling(p).sum().replace(0, np.nan)).fillna(0)
360
-
361
-
362
- def market_structure(df, w=10):
363
- """+1: Higher-High & Higher-Low (yükseliş yapısı), -1: Lower-High & Lower-Low, 0: karışık."""
364
- hi, lo = df["h"].rolling(w).max(), df["l"].rolling(w).min()
365
- up = (hi > hi.shift(w)) & (lo > lo.shift(w))
366
- dn = (hi < hi.shift(w)) & (lo < lo.shift(w))
367
- return pd.Series(np.where(up, 1.0, np.where(dn, -1.0, 0.0)), index=df.index)
368
-
369
-
370
- def squeeze_indicator(df, ind):
371
- """TTM tarzı sıkışma: Bollinger bantları Keltner kanalının İÇİNE girerse volatilite sıkışmıştır.
372
- Dönüş: (sıkışma_açık, son 3 barda sıkışma çözüldü mü, momentum)"""
373
- kc_mid = ind["ema20"]
374
- kc_u, kc_l = kc_mid + 1.5 * ind["atr"], kc_mid - 1.5 * ind["atr"]
375
- on = (ind["bb_lower"] > kc_l) & (ind["bb_upper"] < kc_u)
376
- release = (~on) & on.shift(1, fill_value=False)
377
- rel3 = release.astype(float).rolling(3, min_periods=1).max()
378
- hh, ll = df["h"].rolling(20).max(), df["l"].rolling(20).min()
379
- mom = df["c"] - ((hh + ll) / 2 + sma(df["c"], 20)) / 2
380
- return on.astype(float), rel3, mom
381
-
382
-
383
- CANDLE_PATTERNS = [
384
- ("pat_hammer", "Çekiç", 1),
385
- ("pat_bull_eng", "Yükseliş Yutan Mum", 1),
386
- ("pat_morning", "Sabah Yıldızı", 1),
387
- ("pat_shooting", "Kayan Yıldız", -1),
388
- ("pat_bear_eng", "Düşüş Yutan Mum", -1),
389
- ("pat_evening", "Akşam Yıldızı", -1),
390
- ]
391
-
392
-
393
- def candle_patterns(df):
394
- """Vektörel mum formasyonu tespiti (yalnızca geçmiş barlar kullanılır)."""
395
- o, h, l, c = df["o"], df["h"], df["l"], df["c"]
396
- body = (c - o).abs()
397
- rng = (h - l).replace(0, np.nan)
398
- upper = h - np.maximum(o, c)
399
- lower = np.minimum(o, c) - l
400
- avg_body = body.rolling(10, min_periods=3).mean()
401
- prior_down = c.shift(1) < c.shift(4)
402
- prior_up = c.shift(1) > c.shift(4)
403
-
404
- hammer = (lower >= 0.6 * rng) & (body <= 0.3 * rng) & (upper <= 0.15 * rng) & prior_down
405
- shooting = (upper >= 0.6 * rng) & (body <= 0.3 * rng) & (lower <= 0.15 * rng) & prior_up
406
- bull_eng = ((c.shift(1) < o.shift(1)) & (c > o) & (c >= o.shift(1)) & (o <= c.shift(1))
407
- & (body > body.shift(1)) & prior_down)
408
- bear_eng = ((c.shift(1) > o.shift(1)) & (c < o) & (c <= o.shift(1)) & (o >= c.shift(1))
409
- & (body > body.shift(1)) & prior_up)
410
- big2 = body.shift(2) > avg_body.shift(2)
411
- small1 = body.shift(1) < 0.35 * body.shift(2)
412
- mid2 = (o.shift(2) + c.shift(2)) / 2
413
- morning = (c.shift(2) < o.shift(2)) & big2 & small1 & (c > o) & (c > mid2)
414
- evening = (c.shift(2) > o.shift(2)) & big2 & small1 & (c < o) & (c < mid2)
415
-
416
- raw = {"pat_hammer": hammer, "pat_shooting": shooting, "pat_bull_eng": bull_eng,
417
- "pat_bear_eng": bear_eng, "pat_morning": morning, "pat_evening": evening}
418
- return {k: v.fillna(False).astype(float) for k, v in raw.items()}
419
-
420
-
421
- def find_swings(series, window=3):
422
- """Yerel tepe/dip (swing high/low) indekslerini bulur."""
423
- n = len(series)
424
- highs, lows = [], []
425
- for i in range(window, n - window):
426
- seg = series.iloc[i - window:i + window + 1]
427
- if series.iloc[i] == seg.max():
428
- highs.append(i)
429
- if series.iloc[i] == seg.min():
430
- lows.append(i)
431
- return highs, lows
432
-
433
-
434
- def detect_divergence(df, ind, lookback=60, window=3):
435
- """Fiyat ile RSI arasındaki son swing'leri karşılaştırarak klasik uyumsuzluğu tespit eder."""
436
- n = len(df)
437
- lb = min(lookback, n)
438
- close = df["c"].iloc[-lb:].reset_index(drop=True)
439
- rsi_s = ind["rsi"].iloc[-lb:].reset_index(drop=True)
440
- highs, lows = find_swings(close, window)
441
- result = {"bullish": False, "bearish": False}
442
- if len(highs) >= 2:
443
- h1, h2 = highs[-2], highs[-1]
444
- if close.iloc[h2] > close.iloc[h1] and rsi_s.iloc[h2] < rsi_s.iloc[h1]:
445
- result["bearish"] = True
446
- if len(lows) >= 2:
447
- l1, l2 = lows[-2], lows[-1]
448
- if close.iloc[l2] < close.iloc[l1] and rsi_s.iloc[l2] > rsi_s.iloc[l1]:
449
- result["bullish"] = True
450
- return result
451
-
452
-
453
- def find_sr_levels(df, atr_v, window=5, lookback=200, max_levels=4):
454
- """Swing tepe/diplerini kümeleyerek destek/direnç seviyeleri çıkarır.
455
- Dönüş: (destekler, dirençler) — her biri [(seviye, temas_sayısı), ...], fiyata yakınlık sırasıyla."""
456
- sub = df.iloc[-lookback:].reset_index(drop=True)
457
- highs, _ = find_swings(sub["h"], window)
458
- _, lows = find_swings(sub["l"], window)
459
- pts = [float(sub["h"].iloc[i]) for i in highs] + [float(sub["l"].iloc[i]) for i in lows]
460
- if not pts:
461
- return [], []
462
- price = float(df["c"].iloc[-1])
463
- tol = max(0.6 * (atr_v if atr_v and atr_v > 0 else 0.0), price * 0.002)
464
- pts.sort()
465
- clusters, cur = [], [pts[0]]
466
- for p in pts[1:]:
467
- if p - float(np.mean(cur)) <= tol:
468
- cur.append(p)
469
- else:
470
- clusters.append(cur)
471
- cur = [p]
472
- clusters.append(cur)
473
- levels = [(float(np.mean(c)), len(c)) for c in clusters]
474
- supports = sorted([lv for lv in levels if lv[0] < price], key=lambda x: -x[0])[:max_levels]
475
- resists = sorted([lv for lv in levels if lv[0] > price], key=lambda x: x[0])[:max_levels]
476
- return supports, resists
477
-
478
-
479
- def compute_indicators(df):
480
- close = df["c"]
481
- ind = {}
482
- ind["rsi"] = rsi(close)
483
- ind["macd_line"], ind["macd_signal"], ind["macd_hist"] = macd(close)
484
- ind["ema20"] = ema(close, 20)
485
- ind["ema50"] = ema(close, 50)
486
- ind["ema200"] = ema(close, 200 if len(close) >= 200 else max(5, len(close) // 2))
487
- ind["bb_upper"], ind["bb_mid"], ind["bb_lower"] = bollinger(close)
488
- ind["atr"] = atr(df)
489
- ind["stoch_k"], ind["stoch_d"] = stochastic(df)
490
- ind["vwap"] = vwap_window(df)
491
- ind["adx"], ind["plus_di"], ind["minus_di"] = adx(df)
492
- ind["obv"] = obv(df)
493
- ind["obv_ema"] = ema(ind["obv"], 20)
494
- # --- v2 ek faktörler ---
495
- ind["st_line"], ind["st_dir"] = supertrend(df)
496
- ind["tenkan"], ind["kijun"], ind["ich_a"], ind["ich_b"] = ichimoku(df)
497
- ind["hilo_line"], ind["hilo_dir"] = hilo_activator(df)
498
- ind["fib_hi"], ind["fib_lo"], ind["fib_dir"] = fib_swing(df)
499
- ind["mfi"] = mfi(df)
500
- ind["cmf"] = cmf(df)
501
- ind["rvol"] = (df["vol"] / sma(df["vol"], 20).replace(0, np.nan)).fillna(1.0)
502
- ind["dc_hi20"] = df["h"].rolling(20).max().shift(1)
503
- ind["dc_lo20"] = df["l"].rolling(20).min().shift(1)
504
- ind["struct"] = market_structure(df)
505
- ind["sq_on"], ind["sq_rel3"], ind["sq_mom"] = squeeze_indicator(df, ind)
506
- ind["atr_rank"] = (ind["atr"] / close).rolling(100, min_periods=30).apply(
507
- lambda x: float((x <= x[-1]).mean()), raw=True).fillna(0.5)
508
- for name, ser in candle_patterns(df).items():
509
- ind[name] = ser
510
- # hızlı, nedensel erişim için numpy önbelleği (backtest döngüsünde .iloc yerine kullanılır)
511
- arr = {k: v.to_numpy(dtype=float) for k, v in ind.items()}
512
- for k in ("o", "h", "l", "c", "vol"):
513
- arr[k] = df[k].to_numpy(dtype=float)
514
- ind["_arr"] = arr
515
- return ind
516
-
517
-
518
- # ---------------------------------------------------------------------------
519
- # COMPOSITE SIGNAL ENGINE v2 (multi-factor, rejim-uyarlamalı, konsensüs filtreli)
520
- # ---------------------------------------------------------------------------
521
- def _score_at(ind, i, verbose=False):
522
- """Çoklu faktör puanlamasını verilen bar indeksinde hesaplar (yalnızca o ana kadarki veri).
523
-
524
- İki faktör ailesi vardır:
525
- T (TREND) : MACD, EMA dizilimi, VWAP, OBV, Supertrend, HiLo Activator, Ichimoku,
526
- Donchian kırılımı, piyasa yapısı, CMF, sıkışma kırılımı, ADX/DI yönü
527
- F (FİBONACCI) : düzeltme seviyeleri (rejimden bağımsız, ağırlık 1.0)
528
- R (REVERSION) : RSI, Bollinger, Stokastik, MFI, mum formasyonları
529
-
530
- ADX'e göre iki ailenin ağırlığı SÜREKLİ olarak kayar (w: 0=yatay … 1=güçlü trend).
531
- Güçlü trendde karşı-trend (aşırı alım/satım) sinyalleri bastırılır; yatay piyasada trend takibi bastırılır.
532
- Sonuç, tanh ile ±10 aralığına yumuşak sıkıştırılır.
533
- Canlı sinyal ve tarihsel backtest AYNI fonksiyonu kullanır (tutarlılık + gelecek sızıntısı yok)."""
534
- A = ind["_arr"]
535
- price = A["c"][i]
536
- items = [] # (etiket, grup, ham_puan, açıklama)
537
-
538
- def add(label, group, raw, text):
539
- items.append((label, group, float(raw), text))
540
-
541
- # --- RSI (R) ---
542
- rsi_v = A["rsi"][i]
543
- if rsi_v < 30:
544
- add("RSI", "R", 2, f"RSI aşırı satım bölgesinde ({rsi_v:.1f})")
545
- elif rsi_v > 70:
546
- add("RSI", "R", -2, f"RSI aşırı alım bölgesinde ({rsi_v:.1f})")
547
- else:
548
- add("RSI", "R", 0, f"RSI nötr bölgede ({rsi_v:.1f})")
549
-
550
- # --- MACD histogramı (T) ---
551
- hist_v = A["macd_hist"][i]
552
- hist_prev = A["macd_hist"][i - 1] if i > 0 else 0.0
553
- if hist_v > 0 and hist_v > hist_prev:
554
- add("MACD", "T", 1.5, "MACD histogramı pozitif ve güçleniyor")
555
- elif hist_v > 0:
556
- add("MACD", "T", 0.5, "MACD histogramı pozitif")
557
- elif hist_v < 0 and hist_v < hist_prev:
558
- add("MACD", "T", -1.5, "MACD histogramı negatif ve zayıflıyor")
559
- else:
560
- add("MACD", "T", -0.5, "MACD histogramı negatif")
561
-
562
- # --- EMA dizilimi (T) ---
563
- e20, e50, e200 = A["ema20"][i], A["ema50"][i], A["ema200"][i]
564
- if price > e20 > e50 > e200:
565
- add("EMA Dizilimi", "T", 2, "Güçlü yükseliş trendi (Fiyat > EMA20 > EMA50 > EMA200)")
566
- elif price > e20 and e20 > e50:
567
- add("EMA Dizilimi", "T", 1, "Yükseliş trendi (kısa vadeli ortalamaların üzerinde)")
568
- elif price < e20 < e50 < e200:
569
- add("EMA Dizilimi", "T", -2, "Güçlü düşüş trendi (Fiyat < EMA20 < EMA50 < EMA200)")
570
- elif price < e20 and e20 < e50:
571
- add("EMA Dizilimi", "T", -1, "Düşüş trendi (kısa vadeli ortalamaların altında)")
572
- else:
573
- add("EMA Dizilimi", "T", 0, "Trend yatay / belirsiz")
574
-
575
- # --- Bollinger konumu (R) ---
576
- bb_u, bb_l = A["bb_upper"][i], A["bb_lower"][i]
577
- if bb_u == bb_u and bb_l == bb_l and bb_u != bb_l:
578
- bb_pos = (price - bb_l) / (bb_u - bb_l)
579
- if bb_pos < 0.15:
580
- add("Bollinger", "R", 1, "Fiyat Bollinger alt bandına yakın (olası tepki)")
581
- elif bb_pos > 0.85:
582
- add("Bollinger", "R", -1, "Fiyat Bollinger üst bandına yakın (olası tepki)")
583
-
584
- # --- Stokastik (R) ---
585
- k_v = A["stoch_k"][i]
586
- if k_v < 20:
587
- add("Stokastik", "R", 1, f"Stokastik aşırı satım bölgesinde ({k_v:.1f})")
588
- elif k_v > 80:
589
- add("Stokastik", "R", -1, f"Stokastik aşırı alım bölgesinde ({k_v:.1f})")
590
-
591
- # --- MFI (R) ---
592
- mfi_v = A["mfi"][i]
593
- if mfi_v < 20:
594
- add("MFI", "R", 1, f"MFI (hacimli RSI) aşırı satımda ({mfi_v:.1f})")
595
- elif mfi_v > 80:
596
- add("MFI", "R", -1, f"MFI (hacimli RSI) aşırı alımda ({mfi_v:.1f})")
597
-
598
- # --- Mum formasyonları (R) ---
599
- hits = []
600
- for key, name, sgn in CANDLE_PATTERNS:
601
- if A[key][i] > 0:
602
- hits.append((name, sgn))
603
- if hits:
604
- raw = max(-1.5, min(1.5, float(sum(s for _, s in hits))))
605
- add("Mum Formasyonu", "R", raw, "Mum formasyonu: " + ", ".join(n for n, _ in hits))
606
-
607
- # --- VWAP (T) ---
608
- vwap_v = A["vwap"][i]
609
- if vwap_v == vwap_v:
610
- if price > vwap_v:
611
- add("VWAP", "T", 0.5, "Fiyat pencere VWAP'ının üzerinde")
612
- else:
613
- add("VWAP", "T", -0.5, "Fiyat pencere VWAP'ının altında")
614
-
615
- # --- OBV (T) ---
616
- obv_v, obv_e = A["obv"][i], A["obv_ema"][i]
617
- if obv_e == obv_e:
618
- if obv_v > obv_e:
619
- add("OBV", "T", 0.5, "OBV hacim akışı yükseliş yönünde")
620
- else:
621
- add("OBV", "T", -0.5, "OBV hacim akışı düşüş yönünde")
622
-
623
- # --- CMF (T) ---
624
- cmf_v = A["cmf"][i]
625
- if cmf_v > 0.05:
626
- add("CMF", "T", 0.5, f"Chaikin para akışı pozitif ({cmf_v:+.2f}) — alıcılar baskın")
627
- elif cmf_v < -0.05:
628
- add("CMF", "T", -0.5, f"Chaikin para akışı negatif ({cmf_v:+.2f}) — satıcılar baskın")
629
-
630
- # --- Supertrend (T) ---
631
- st_d = A["st_dir"][i]
632
- st_flip = i > 0 and A["st_dir"][i] != A["st_dir"][i - 1]
633
- if st_d > 0:
634
- add("Supertrend", "T", 1.5 + (0.5 if st_flip else 0),
635
- "Supertrend YÜKSELİŞ moduna döndü (taze sinyal)" if st_flip else "Supertrend yükseliş modunda")
636
- else:
637
- add("Supertrend", "T", -(1.5 + (0.5 if st_flip else 0)),
638
- "Supertrend DÜŞÜŞ moduna döndü (taze sinyal)" if st_flip else "Supertrend düşüş modunda")
639
-
640
- # --- Gann HiLo Activator (T) ---
641
- hl_d = A["hilo_dir"][i]
642
- hilo_flip = 0
643
- if hl_d != 0:
644
- prev_d = A["hilo_dir"][i - 1] if i > 0 else 0.0
645
- if prev_d != 0 and prev_d != hl_d:
646
- hilo_flip = int(hl_d)
647
- mag = 1.25 + (0.5 if hilo_flip else 0.0)
648
- if hl_d > 0:
649
- add("HiLo Activator", "T", mag,
650
- "HiLo Activator YÜKSELİŞ moduna döndü (taze sinyal)" if hilo_flip else "HiLo Activator yükseliş modunda (fiyat HiLo çizgisinin üzerinde)")
651
- else:
652
- add("HiLo Activator", "T", -mag,
653
- "HiLo Activator DÜŞÜŞ moduna döndü (taze sinyal)" if hilo_flip else "HiLo Activator düşüş modunda (fiyat HiLo çizgisinin altında)")
654
-
655
- # --- Fibonacci düzeltme seviyeleri (F) ---
656
- fh, fl, fd, atr_i = A["fib_hi"][i], A["fib_lo"][i], A["fib_dir"][i], A["atr"][i]
657
- fib_zone, fib_depth = 0, None
658
- if fh == fh and fl == fl and fd == fd and atr_i == atr_i and atr_i > 0 and (fh - fl) >= FIB_MIN_RANGE_ATR * atr_i:
659
- rng = fh - fl
660
- depth = (fh - price) / rng if fd > 0 else (price - fl) / rng
661
- fib_depth = depth
662
- d = 1 if fd > 0 else -1
663
- bounce = (A["c"][i] > A["o"][i]) if d > 0 else (A["c"][i] < A["o"][i])
664
- near = min(FIB_RATIOS, key=lambda r: abs(r - depth))
665
- wave = "yükseliş" if d > 0 else "düşüş"
666
- role = "destek" if d > 0 else "direnç"
667
- if 0.35 <= depth <= 0.65:
668
- fib_zone = d
669
- add("Fibonacci", "F", d * (1.5 if bounce else 0.5),
670
- f"Fibonacci ALTIN BÖLGE: {wave} dalgasının %{depth * 100:.1f} düzeltmesinde (Fib {near:g} yakını) — {role} bölgesi"
671
- + (", mum tepkiyi teyit ediyor" if bounce else ", henüz mum teyidi yok"))
672
- elif 0.236 <= depth < 0.35:
673
- add("Fibonacci", "F", d * 0.5,
674
- f"Sığ düzeltme (%{depth * 100:.1f}, Fib 0.236–0.382) — {wave} dalgası güçlü, trend devamı lehine")
675
- elif 0.65 < depth <= 0.82:
676
- add("Fibonacci", "F", d * (0.5 if bounce else 0.0),
677
- f"Derin düzeltme (%{depth * 100:.1f}, Fib 0.786 yakını) — son {role} bölgesi")
678
- elif depth > 0.82:
679
- add("Fibonacci", "F", -d * 1.0,
680
- f"Fibonacci 0.786 aşıldı (%{depth * 100:.1f}) — {wave} dalgası geçersizleşiyor, ters yön riski")
681
-
682
- # --- Ichimoku (T) ---
683
- sa, sb = A["ich_a"][i], A["ich_b"][i]
684
- if sa == sa and sb == sb:
685
- top, bot = max(sa, sb), min(sa, sb)
686
- if price > top:
687
- add("Ichimoku Bulutu", "T", 1.5, "Fiyat Ichimoku bulutunun üzerinde (boğa bölgesi)")
688
- elif price < bot:
689
- add("Ichimoku Bulutu", "T", -1.5, "Fiyat Ichimoku bulutunun altında (ayı bölgesi)")
690
- else:
691
- add("Ichimoku Bulutu", "T", 0, "Fiyat Ichimoku bulutunun içinde (kararsız bölge)")
692
- tk, kj = A["tenkan"][i], A["kijun"][i]
693
- if tk == tk and kj == kj:
694
- add("Tenkan/Kijun", "T", 0.5 if tk > kj else -0.5,
695
- "Tenkan > Kijun (kısa vadeli momentum yukarı)" if tk > kj else "Tenkan < Kijun (kısa vadeli momentum aşağı)")
696
-
697
- # --- Donchian kırılımı (T) ---
698
- dhi, dlo, rvol = A["dc_hi20"][i], A["dc_lo20"][i], A["rvol"][i]
699
- if dhi == dhi and dlo == dlo:
700
- if price > dhi:
701
- if rvol >= 1.2:
702
- add("Donchian Kırılımı", "T", 1.5, f"20 barlık tepe HACİMLİ kırıldı (göreli hacim {rvol:.1f}x)")
703
- else:
704
- add("Donchian Kırılımı", "T", 0.75, f"20 barlık tepe kırıldı ama hacim zayıf ({rvol:.1f}x)")
705
- elif price < dlo:
706
- if rvol >= 1.2:
707
- add("Donchian Kırılımı", "T", -1.5, f"20 barlık dip HACİMLİ kırıldı (göreli hacim {rvol:.1f}x)")
708
- else:
709
- add("Donchian Kırılımı", "T", -0.75, f"20 barlık dip kırıldı ama hacim zayıf ({rvol:.1f}x)")
710
-
711
- # --- Piyasa yapısı (T) ---
712
- st_v = A["struct"][i]
713
- if st_v > 0:
714
- add("Piyasa Yapısı", "T", 1, "Piyasa yapısı: Higher-High / Higher-Low (yükseliş yapısı)")
715
- elif st_v < 0:
716
- add("Piyasa Yapısı", "T", -1, "Piyasa yapısı: Lower-High / Lower-Low (düşüş yapısı)")
717
-
718
- # --- Sıkışma (Bollinger ⊂ Keltner) ---
719
- squeeze_on = A["sq_on"][i] > 0
720
- mom = A["sq_mom"][i]
721
- if A["sq_rel3"][i] > 0 and mom == mom and mom != 0:
722
- d = 1 if mom > 0 else -1
723
- add("Sıkışma Kırılımı", "T", 1.5 * d,
724
- "Volatilite sıkışması yukarı yönde çözüldü" if d > 0 else "Volatilite sıkışması aşağı yönde çözüldü")
725
- elif squeeze_on:
726
- add("Sıkışma", "T", 0, "Volatilite sıkışması aktif — kırılım bekleniyor, sinyal güveni düşürüldü")
727
-
728
- # --- REJİM (ADX) — sürekli ağırlıklandırma ---
729
- adx_v = A["adx"][i]
730
- w = min(max((adx_v - 15.0) / 15.0, 0.0), 1.0) if adx_v == adx_v else 0.0 # 0 = yatay, 1 = güçlü trend
731
- trend_w = 0.5 + 0.8 * w
732
- rev_w = 1.3 - 0.95 * w
733
- di_dir = 1 if A["plus_di"][i] > A["minus_di"][i] else -1
734
- add("ADX / DI Yönü", "T", di_dir * w, "") # yönsel bonus; w kadar ölçeklenir
735
- if w >= 0.67:
736
- regime = "TREND"
737
- add("Rejim", "T", 0, f"ADX güçlü trend gösteriyor ({adx_v:.1f}) — trend faktörleri ağırlıklı, karşı-trend sinyaller bastırıldı")
738
- elif w <= 0.33:
739
- regime = "YATAY"
740
- add("Rejim", "T", 0, f"ADX zayıf/yatay piyasa ({adx_v:.1f}) — ortalamaya dönüş faktörleri ağırlıklı")
741
- else:
742
- regime = "GEÇİŞ"
743
- add("Rejim", "T", 0, f"ADX geçiş bölgesinde ({adx_v:.1f}) — faktör ağırlıkları dengeli karışım")
744
-
745
- gw = {"T": trend_w, "R": rev_w} # "F" (Fibonacci) rejimden bağımsız: ağırlık 1.0
746
- contribs = [(lab, gw.get(g, 1.0) * raw, raw) for lab, g, raw, _ in items]
747
- raw_total = sum(c for _, c, _ in contribs)
748
- if squeeze_on:
749
- raw_total *= 0.75
750
- score = 10.0 * math.tanh(raw_total / 10.0) # 2 yeni faktör eklendiği için ölçek 9 -> 10
751
- bull_w = sum(c for _, c, _ in contribs if c > 0)
752
- bear_w = -sum(c for _, c, _ in contribs if c < 0)
753
-
754
- out = {
755
- "score": score, "price": price, "adx": adx_v, "w": w, "regime": regime,
756
- "bull_w": bull_w, "bear_w": bear_w, "squeeze_on": bool(squeeze_on),
757
- "atr_rank": A["atr_rank"][i], "pat_hits": hits,
758
- "hilo_flip": hilo_flip, "fib_zone": fib_zone, "fib_depth": fib_depth,
759
- }
760
- if verbose:
761
- def tone(raw):
762
- return "up" if raw > 0 else ("down" if raw < 0 else "flat")
763
- out["reasons"] = [(tone(raw), text) for _, _, raw, text in items if text]
764
- out["contribs"] = [(lab, c) for lab, c, raw in contribs if abs(c) > 1e-9]
765
- return out
766
-
767
-
768
- def compute_score_series(ind, n):
769
- """Son BACKTEST_LOOKBACK bar için (canlı bar dahil) skor serisi. Dönüş: (başlangıç_indeksi, [skor_dict,...])"""
770
- start = min(max(WARMUP_BARS, n - BACKTEST_LOOKBACK), n - 1)
771
- return start, [_score_at(ind, i) for i in range(start, n)]
772
-
773
-
774
- # ---------------------------------------------------------------------------
775
- # BACKTEST — yön doğruluğu + gerçek işlem simülasyonu (SL/TP/zaman çıkışı + komisyon)
776
- # ---------------------------------------------------------------------------
777
- def directional_accuracy(df, start, scores, forward=5, threshold=SIGNAL_THRESHOLD):
778
- """Klasik test: sinyal 'forward' bar sonra doğru yönü göstermiş mi? (rejim kırılımlı)"""
779
- c = df["c"].to_numpy()
780
- n = len(c)
781
- stats = {"all": [0, 0], "trend": [0, 0], "range": [0, 0]}
782
- for i in range(start, n - forward):
783
- s = scores[i - start]
784
- if abs(s["score"]) < threshold:
785
- continue
786
- correct = (s["score"] > 0) == (c[i + forward] > c[i])
787
- regime = "trend" if s["adx"] >= 25 else "range"
788
- for key in ("all", regime):
789
- stats[key][1] += 1
790
- if correct:
791
- stats[key][0] += 1
792
- if stats["all"][1] < 5:
793
- return None
794
-
795
- def pack(key):
796
- ok, t = stats[key]
797
- return {"accuracy": ok / t, "n_signals": t} if t > 0 else None
798
-
799
- return {
800
- "accuracy": stats["all"][0] / stats["all"][1], "correct": stats["all"][0],
801
- "n_signals": stats["all"][1], "forward_bars": forward,
802
- "trend_regime": pack("trend"), "range_regime": pack("range"),
803
- }
804
-
805
-
806
- def simulate_trades(df, ind, start, scores, threshold=SIGNAL_THRESHOLD):
807
- """Sinyalleri sırayla (üst üste binmeden) işleme çevirir.
808
- Giriş: sinyal barını izleyen barın AÇILIŞI (gerçekçi, ileri sızıntı yok).
809
- Çıkış: SL (1.5×ATR) / TP (2.5×ATR) / 24 bar zaman çıkışı. Aynı barda ikisi de değerse SL öncelikli (kötümser)."""
810
- A = ind["_arr"]
811
- o, h, l, c, atr_a = A["o"], A["h"], A["l"], A["c"], A["atr"]
812
- n = len(c)
813
- trades = []
814
- i = start
815
- while i <= n - 2:
816
- s = scores[i - start]["score"]
817
- a = atr_a[i]
818
- if abs(s) < threshold or not (a > 0):
819
- i += 1
820
- continue
821
- d = 1 if s > 0 else -1
822
- entry = o[i + 1]
823
- risk = SL_ATR * a
824
- sl, tp = entry - d * risk, entry + d * TP_ATR * a
825
- last = min(n - 1, i + 1 + MAX_HOLD_BARS)
826
- exit_i, exit_px, why = None, None, None
827
- for j in range(i + 1, last + 1):
828
- hit_sl = (l[j] <= sl) if d > 0 else (h[j] >= sl)
829
- hit_tp = (h[j] >= tp) if d > 0 else (l[j] <= tp)
830
- if hit_sl:
831
- exit_i, exit_px, why = j, sl, "SL"
832
- break
833
- if hit_tp:
834
- exit_i, exit_px, why = j, tp, "TP"
835
- break
836
- if exit_i is None:
837
- if last == i + 1 + MAX_HOLD_BARS:
838
- exit_i, exit_px, why = last, c[last], "ZAMAN"
839
- else:
840
- break # veri bitti, işlem hâlâ açık — sonuçlandırılamaz
841
- pnl = d * (exit_px - entry) / entry - 2 * FEE_PER_SIDE
842
- r_mult = pnl / (risk / entry)
843
- trades.append({"signal_i": i, "entry_i": i + 1, "exit_i": exit_i, "dir": d, "entry": entry,
844
- "exit": exit_px, "pnl": pnl, "r": r_mult, "why": why, "score": s})
845
- i = exit_i
846
- return trades
847
-
848
-
849
- def summarize_trades(trades, df, start):
850
- if len(trades) < 3:
851
- return None
852
- n = len(df)
853
- pnl = np.array([t["pnl"] for t in trades])
854
- r = np.array([t["r"] for t in trades])
855
- wins, losses = pnl[pnl > 0], pnl[pnl <= 0]
856
- gross_loss = abs(losses.sum())
857
- pf = float(wins.sum() / gross_loss) if gross_loss > 0 else (99.0 if wins.sum() > 0 else 0.0)
858
- win_rate = len(wins) / len(pnl)
859
- win_r, loss_r = r[r > 0], r[r <= 0]
860
- avg_win_r = float(win_r.mean()) if len(win_r) else 0.0
861
- avg_loss_r = float(abs(loss_r.mean())) if len(loss_r) else 0.0
862
- kelly = None
863
- if avg_win_r > 0 and avg_loss_r > 0:
864
- kelly = win_rate - (1 - win_rate) / (avg_win_r / avg_loss_r)
865
- sqn = float(np.sqrt(min(len(r), 100)) * r.mean() / r.std()) if r.std() > 0 else 0.0
866
-
867
- mult = np.ones(n - start)
868
- for t in trades:
869
- mult[t["exit_i"] - start] *= (1 + t["pnl"])
870
- equity = np.cumprod(mult)
871
- peak = np.maximum.accumulate(equity)
872
- max_dd = float(((equity - peak) / peak).min())
873
- c = df["c"].to_numpy()
874
- bh = c[start:] / c[start]
875
-
876
- streak = best = 0
877
- for p in pnl:
878
- streak = streak + 1 if p <= 0 else 0
879
- best = max(best, streak)
880
-
881
- def side(dirn):
882
- sub = [t for t in trades if t["dir"] == dirn]
883
- return {"n": len(sub), "win_rate": (sum(1 for t in sub if t["pnl"] > 0) / len(sub)) if sub else None}
884
-
885
- stats = {
886
- "n": len(trades), "win_rate": win_rate, "profit_factor": pf, "expectancy": float(pnl.mean()),
887
- "avg_r": float(r.mean()), "sqn": sqn, "max_dd": max_dd, "total_return": float(equity[-1] - 1),
888
- "bh_return": float(bh[-1] - 1), "max_consec_loss": best, "avg_win_r": avg_win_r,
889
- "avg_loss_r": avg_loss_r, "kelly": kelly, "long": side(1), "short": side(-1),
890
- }
891
- return stats, equity, bh
892
-
893
-
894
- def run_backtest(df, ind, start, scores):
895
- n = len(df)
896
- if n - start < 20:
897
- return None
898
- acc = directional_accuracy(df, start, scores)
899
- trades = simulate_trades(df, ind, start, scores)
900
- summ = summarize_trades(trades, df, start)
901
- bt = {"start": start, "acc": acc, "trades": trades, "stats": None, "equity": None, "bh": None}
902
- if summ:
903
- bt["stats"], bt["equity"], bt["bh"] = summ
904
- return bt
905
-
906
-
907
- # ---------------------------------------------------------------------------
908
- # CANLI KATMAN — üst zaman dilimleri, emir defteri, funding (backtest EDİLMEZ)
909
- # ---------------------------------------------------------------------------
910
- def _mtf_score(symbol, bar, closed_only):
911
- hdf = fetch_candles(symbol, bar, 300)
912
- if closed_only:
913
- hdf = hdf[hdf["confirm"] == 1].reset_index(drop=True)
914
- hind = compute_indicators(hdf)
915
- r = _score_at(hind, len(hdf) - 1)
916
- return {"bar": bar, "score": r["score"]}
917
-
918
-
919
- def _fetch_live_extras(symbol, bar, mtf, micro, closed_only):
920
- """MTF / emir defteri / funding çağrılarını PARALEL yapar; hata veren parça sessizce atlanır."""
921
- tasks = {}
922
- with ThreadPoolExecutor(max_workers=4) as ex:
923
- if mtf and symbol and bar:
924
- b1 = MTF_MAP.get(bar)
925
- if b1 and b1 != bar:
926
- tasks["mtf1"] = ex.submit(_mtf_score, symbol, b1, closed_only)
927
- b2 = MTF_MAP.get(b1)
928
- if b2 and b2 not in (b1, bar):
929
- tasks["mtf2"] = ex.submit(_mtf_score, symbol, b2, closed_only)
930
- if micro and symbol:
931
- tasks["ob"] = ex.submit(fetch_orderbook, symbol, 20)
932
- tasks["fund"] = ex.submit(fetch_funding_rate, symbol)
933
- out = {}
934
- for k, f in tasks.items():
935
- try:
936
- out[k] = f.result()
937
- except Exception:
938
- out[k] = None
939
- return out
940
-
941
-
942
- def build_trade_plan(price, direction, atr_v, supports, resists, acct=None, risk_pct=None):
943
- """Yapısal (destek/direnç) stop uygun aralıktaysa onu, değilse ATR stop'unu kullanır.
944
- 3 kademeli hedef + hesap büyüklüğüne göre pozisyon boyutu."""
945
- d = direction
946
- stop, stop_type = price - d * SL_ATR * atr_v, f"ATR × {SL_ATR}"
947
- cand = None
948
- if d > 0 and supports:
949
- cand = supports[0][0] - 0.25 * atr_v
950
- elif d < 0 and resists:
951
- cand = resists[0][0] + 0.25 * atr_v
952
- if cand is not None:
953
- dist = abs(price - cand)
954
- if 0.8 * atr_v <= dist <= 3.0 * atr_v and (cand < price if d > 0 else cand > price):
955
- stop, stop_type = cand, "Yapısal (S/R arkası)"
956
- risk = abs(price - stop)
957
- plan = {
958
- "direction": "LONG" if d > 0 else "SHORT", "entry": price, "stop_loss": stop, "stop_type": stop_type,
959
- "risk_dist": risk, "risk_frac": risk / price,
960
- "targets": [(price + d * risk * m, m) for m in TP_R_MULTIPLES], "sizing": None,
961
- }
962
- if acct and acct > 0 and risk_pct and risk_pct > 0 and risk > 0:
963
- risk_amt = acct * risk_pct / 100.0
964
- qty = risk_amt / risk
965
- notional = qty * price
966
- plan["sizing"] = {"risk_amount": risk_amt, "qty": qty, "notional": notional, "leverage": notional / acct}
967
- return plan
968
-
969
-
970
- def compute_confidence(score, agree, mtf_state, hist_q, vol_q):
971
- mag = min(abs(score) / 8.0, 1.0)
972
- conf = 100.0 * (0.35 * mag + 0.25 * agree + 0.15 * mtf_state + 0.20 * hist_q + 0.05 * vol_q)
973
- parts = {"Skor Büyüklüğü": mag, "Faktör Uyumu": agree, "MTF Teyidi": mtf_state,
974
- "Tarihsel Kenar": hist_q, "Volatilite Rejimi": vol_q}
975
- return min(conf, 100.0), parts
976
-
977
-
978
- def grade_from_conf(conf):
979
- for thr, g, col in ((75, "A+", "#3ddc97"), (62, "A", "#7fe6b8"), (50, "B", "#f2b84b"), (38, "C", "#ff9aa8")):
980
- if conf >= thr:
981
- return g, col
982
- return "D", "#ff5c72"
983
-
984
-
985
- def generate_signal(df, symbol=None, bar=None, mtf=True, micro=True, closed_only=False,
986
- acct=None, risk_pct=None):
987
- ind = compute_indicators(df)
988
- n = len(df)
989
- start, scores = compute_score_series(ind, n)
990
- base = _score_at(ind, n - 1, verbose=True)
991
- score, price, w = base["score"], base["price"], base["w"]
992
- reasons, contribs = list(base["reasons"]), list(base["contribs"])
993
- tags = []
994
- if base["squeeze_on"]:
995
- tags.append(("SIKIŞMA AKTİF", "#f2b84b"))
996
- for name, sgn in base["pat_hits"]:
997
- tags.append((name.upper(), "#3ddc97" if sgn > 0 else "#ff5c72"))
998
- if base["hilo_flip"]:
999
- tags.append(("HiLo YUKARI DÖNÜŞ" if base["hilo_flip"] > 0 else "HiLo AŞAĞI DÖNÜŞ",
1000
- "#3ddc97" if base["hilo_flip"] > 0 else "#ff5c72"))
1001
- if base["fib_zone"]:
1002
- tags.append(("FİB ALTIN BÖLGE ▲" if base["fib_zone"] > 0 else "FİB ALTIN BÖLGE ▼",
1003
- "#3ddc97" if base["fib_zone"] > 0 else "#ff5c72"))
1004
-
1005
- def overlay(label, delta, text):
1006
- nonlocal score
1007
- score += delta
1008
- reasons.append(("up" if delta > 0 else "down", text))
1009
- contribs.append((label, delta))
1010
-
1011
- # --- Uyumsuzluk (RSI divergence) — reversion sinyali, rejime göre ağırlıklı ---
1012
- div = detect_divergence(df, ind)
1013
- div_weight = 1.5 - 1.1 * w # güçlü trendde 0.4, yatayda 1.5 (eski davranışla aynı uçlar)
1014
- if div["bullish"]:
1015
- overlay("Yükseliş Uyumsuzluğu", div_weight, "Yükseliş uyumsuzluğu tespit edildi (fiyat düşük dip, RSI yüksek dip)")
1016
- tags.append(("YÜKSELİŞ UYUMSUZLUĞU", "#3ddc97"))
1017
- if div["bearish"]:
1018
- overlay("Düşüş Uyumsuzluğu", -div_weight, "Düşüş uyumsuzluğu tespit edildi (fiyat yüksek tepe, RSI düşük tepe)")
1019
- tags.append(("DÜŞÜŞ UYUMSUZLUĞU", "#ff5c72"))
1020
-
1021
- # --- Skor ivmesi ---
1022
- if n - 4 >= start:
1023
- momentum = base["score"] - scores[n - 4 - start]["score"]
1024
- if momentum > 1.5:
1025
- overlay("Skor İvmesi", 0.3, "Kompozit skor son barlarda hızla güçleniyor")
1026
- elif momentum < -1.5:
1027
- overlay("Skor İvmesi", -0.3, "Kompozit skor son barlarda hızla zayıflıyor")
1028
-
1029
- # --- Canlı ekstralar (paralel çekim) ---
1030
- extras = _fetch_live_extras(symbol, bar, mtf, micro, closed_only)
1031
- sgn = 1 if score > 0 else (-1 if score < 0 else 0)
1032
-
1033
- # --- 2 kademeli MTF teyidi ---
1034
- mtf_info, align_flags = [], []
1035
- for key, wt in (("mtf1", 1.0), ("mtf2", 0.6)):
1036
- m = extras.get(key)
1037
- if not m:
1038
- continue
1039
- hs = m["score"]
1040
- aligned = None
1041
- if abs(hs) >= 1.0 and sgn != 0:
1042
- aligned = bool(np.sign(hs) == sgn)
1043
- align_flags.append(aligned)
1044
- if aligned:
1045
- overlay(f"{m['bar']} Teyidi", sgn * wt, f"{m['bar']} zaman diliminde de aynı yönlü teyit var (skor {hs:+.1f})")
1046
- else:
1047
- overlay(f"{m['bar']} Çelişkisi", -sgn * wt, f"{m['bar']} zaman dilimi ters yönlü sinyal veriyor (skor {hs:+.1f}) — dikkat")
1048
- mtf_info.append({"bar": m["bar"], "score": round(hs, 2), "aligned": aligned})
1049
- mtf_state = (sum(1.0 for a in align_flags if a) / len(align_flags)) if align_flags else 0.5
1050
- mtf_opposed = any(a is False for a in align_flags)
1051
-
1052
- # --- Emir defteri dengesizliği (derinlik ağırlıklı, kısa vadede anlamlı) ---
1053
- imb = None
1054
- ob = extras.get("ob")
1055
- if ob and ob.get("bids") and ob.get("asks"):
1056
- bw = sum(float(x[1]) / (k + 1) for k, x in enumerate(ob["bids"]))
1057
- aw = sum(float(x[1]) / (k + 1) for k, x in enumerate(ob["asks"]))
1058
- if bw + aw > 0:
1059
- imb = (bw - aw) / (bw + aw)
1060
- if abs(imb) >= 0.2:
1061
- delta = max(-0.8, min(0.8, imb * 2)) * (0.5 if bar in SLOW_BARS else 1.0)
1062
- overlay("Emir Defteri", delta,
1063
- f"Emir defterinde {'alış' if imb > 0 else 'satış'} baskısı (dengesizlik {imb * 100:+.0f}%)")
1064
-
1065
- # --- Funding rate (kalabalık pozisyon = kontraryen) ---
1066
- fr = extras.get("fund")
1067
- if fr is not None:
1068
- if fr > 0.0005:
1069
- overlay("Funding", -0.6, f"Funding aşırı pozitif ({fr * 100:.3f}%) — kalabalık long, tasfiye riski")
1070
- elif fr > 0.0002:
1071
- overlay("Funding", -0.3, f"Funding yüksek pozitif ({fr * 100:.3f}%) — long tarafı kalabalık")
1072
- elif fr < -0.0005:
1073
- overlay("Funding", 0.6, f"Funding aşırı negatif ({fr * 100:.3f}%) — kalabalık short, sıkışma potansiyeli")
1074
- elif fr < -0.0002:
1075
- overlay("Funding", 0.3, f"Funding negatif ({fr * 100:.3f}%) — short tarafı kalabalık")
1076
-
1077
- # --- Destek / direnç yakınlığı ---
1078
- atr_v = float(ind["atr"].iloc[-1])
1079
- supports, resists = find_sr_levels(df, atr_v)
1080
- fib = None
1081
- _fh, _fl, _fd = (float(ind["fib_hi"].iloc[-1]), float(ind["fib_lo"].iloc[-1]), float(ind["fib_dir"].iloc[-1]))
1082
- if _fh == _fh and _fl == _fl and _fd == _fd and atr_v > 0 and (_fh - _fl) >= FIB_MIN_RANGE_ATR * atr_v:
1083
- fib = {"hi": _fh, "lo": _fl, "dir": int(_fd), "levels": fib_levels(_fh, _fl, _fd), "depth": base["fib_depth"]}
1084
- sgn = 1 if score > 0 else (-1 if score < 0 else 0)
1085
- if atr_v > 0 and sgn != 0:
1086
- if sgn > 0:
1087
- if resists and resists[0][1] >= 2 and (resists[0][0] - price) / atr_v <= 1.0:
1088
- lv, tc = resists[0]
1089
- overlay("Yakın Direnç", -0.8, f"Yakın direnç {fmt_price(lv)} ({tc} temas, {(lv - price) / atr_v:.1f} ATR) — yükselişi sınırlayabilir")
1090
- elif supports and supports[0][1] >= 2 and (price - supports[0][0]) / atr_v <= 0.5:
1091
- lv, tc = supports[0]
1092
- overlay("Destek Üstünde", 0.4, f"Fiyat güçlü destek {fmt_price(lv)} ({tc} temas) üzerinde")
1093
- else:
1094
- if supports and supports[0][1] >= 2 and (price - supports[0][0]) / atr_v <= 1.0:
1095
- lv, tc = supports[0]
1096
- overlay("Yakın Destek", 0.8, f"Yakın destek {fmt_price(lv)} ({tc} temas, {(price - lv) / atr_v:.1f} ATR) — düşüşü sınırlayabilir")
1097
- elif resists and resists[0][1] >= 2 and (resists[0][0] - price) / atr_v <= 0.5:
1098
- lv, tc = resists[0]
1099
- overlay("Direnç Altında", -0.4, f"Fiyat güçlü direnç {fmt_price(lv)} ({tc} temas) altında")
1100
-
1101
- score = max(-10.0, min(10.0, score))
1102
-
1103
- # --- Konsensüs + sınıflandırma ---
1104
- direction = 1 if score > 0 else -1
1105
- tot = base["bull_w"] + base["bear_w"]
1106
- agree = ((base["bull_w"] if direction > 0 else base["bear_w"]) / tot) if tot > 0 else 0.5
1107
- if abs(score) >= STRONG_THRESHOLD and agree >= 0.65 and not mtf_opposed:
1108
- signal, color = ("GÜÇLÜ AL", "#3ddc97") if score > 0 else ("GÜÇLÜ SAT", "#ff5c72")
1109
- elif abs(score) >= SIGNAL_THRESHOLD:
1110
- signal, color = ("AL", "#7fe6b8") if score > 0 else ("SAT", "#ff9aa8")
1111
- if abs(score) >= STRONG_THRESHOLD:
1112
- reasons.append(("flat", f"Skor güçlü bölgede ancak konsensüs ({agree * 100:.0f}%) veya MTF filtresini geçemedi — GÜÇLÜ yerine normal sinyal verildi"))
1113
- else:
1114
- signal, color = "BEKLE / NÖTR", "#f2b84b"
1115
- is_neutral = signal == "BEKLE / NÖTR"
1116
-
1117
- # --- Tarihsel doğrulama ---
1118
- backtest = run_backtest(df, ind, start, scores)
1119
- hist_q = 0.4
1120
- if backtest:
1121
- qs = []
1122
- acc = backtest["acc"]
1123
- if acc:
1124
- p_shrunk = (acc["correct"] + 5) / (acc["n_signals"] + 10) # Bayes büzülmesi (önsel %50)
1125
- qs.append(min(max((p_shrunk - 0.45) / 0.20, 0.0), 1.0))
1126
- st = backtest["stats"]
1127
- if st:
1128
- qs.append(min(max((st["profit_factor"] - 0.8) / 0.8, 0.0), 1.0))
1129
- if qs:
1130
- hist_q = float(np.mean(qs))
1131
- rank = base["atr_rank"]
1132
- vol_q = 1.0 if 0.10 <= rank <= 0.90 else 0.5
1133
- if rank > 0.90:
1134
- reasons.append(("flat", f"Volatilite aşırı yüksek (ATR yüzdeliği %{rank * 100:.0f}) — stoplar geniş tutulmalı, güven düşürüldü"))
1135
- conf, conf_parts = compute_confidence(score, agree, mtf_state, hist_q, vol_q)
1136
- grade, grade_color = ("—", "#7c8797") if is_neutral else grade_from_conf(conf)
1137
-
1138
- plan = None
1139
- if not is_neutral and atr_v > 0:
1140
- plan = build_trade_plan(price, direction, atr_v, supports, resists, acct, risk_pct)
1141
-
1142
- return {
1143
- "signal": signal, "score": round(score, 2), "confidence": round(conf, 1), "grade": grade,
1144
- "grade_color": grade_color, "color": color, "reasons": reasons, "contribs": contribs,
1145
- "indicators": ind, "price": price, "divergence": div, "mtf": mtf_info, "plan": plan,
1146
- "backtest": backtest, "regime": base["regime"], "adx": base["adx"], "atr_rank": rank,
1147
- "squeeze_on": base["squeeze_on"], "agree": agree, "bull_w": base["bull_w"], "bear_w": base["bear_w"],
1148
- "conf_parts": conf_parts, "tags": tags, "funding": fr, "imbalance": imb,
1149
- "supports": supports, "resists": resists, "atr": atr_v, "fib": fib,
1150
- "hilo_dir": int(ind["hilo_dir"].iloc[-1]),
1151
- }
1152
-
1153
-
1154
- # ---------------------------------------------------------------------------
1155
- # RISK ANALYTICS
1156
- # ---------------------------------------------------------------------------
1157
- def risk_metrics(df, bar):
1158
- ret = df["c"].pct_change().dropna()
1159
- if len(ret) < 5:
1160
- return None
1161
- periods = BAR_TO_PERIODS_PER_YEAR.get(bar, 365)
1162
- vol_annual = ret.std() * np.sqrt(periods)
1163
- mean_annual = ret.mean() * periods
1164
- sharpe = mean_annual / vol_annual if vol_annual > 0 else 0.0
1165
- cum = (1 + ret).cumprod()
1166
- peak = cum.cummax()
1167
- dd = (cum - peak) / peak
1168
- var95 = np.percentile(ret, 5)
1169
- tail = ret[ret <= var95]
1170
- cvar95 = tail.mean() if len(tail) else var95
1171
- return {
1172
- "volatility_annual": vol_annual, "return_annual": mean_annual, "sharpe": sharpe,
1173
- "max_drawdown": dd.min(), "current_dd": dd.iloc[-1],
1174
- "var95_bar": var95, "cvar95_bar": cvar95,
1175
- }
1176
-
1177
-
1178
- # ---------------------------------------------------------------------------
1179
- # FORMAT HELPERS
1180
- # ---------------------------------------------------------------------------
1181
- def _isnan(v):
1182
- return v is None or (isinstance(v, float) and math.isnan(v))
1183
-
1184
-
1185
- def fmt_price(v):
1186
- if _isnan(v):
1187
- return "—"
1188
- av = abs(v)
1189
- if av >= 1000:
1190
- return f"{v:,.2f}"
1191
- if av >= 1:
1192
- return f"{v:,.4f}"
1193
- if av >= 0.01:
1194
- return f"{v:.6f}"
1195
- return f"{v:.8f}"
1196
-
1197
-
1198
- def fmt_pct(v):
1199
- if _isnan(v):
1200
- return "—"
1201
- sign = "+" if v > 0 else ""
1202
- return f"{sign}{v * 100:.2f}%"
1203
-
1204
-
1205
- def fmt_large(v):
1206
- if _isnan(v):
1207
- return "—"
1208
- av = abs(v)
1209
- if av >= 1e9:
1210
- return f"{v / 1e9:.2f}B"
1211
- if av >= 1e6:
1212
- return f"{v / 1e6:.2f}M"
1213
- if av >= 1e3:
1214
- return f"{v / 1e3:.2f}K"
1215
- return f"{v:.2f}"
1216
-
1217
-
1218
- def metric_card(label, value, sub="", tone="neutral"):
1219
- tone_class = {"up": "mc-up", "down": "mc-down", "neutral": "mc-neutral"}.get(tone, "mc-neutral")
1220
- sub_html = f'<div class="mc-sub">{sub}</div>' if sub else ""
1221
- return (f'<div class="metric-card {tone_class}"><div class="mc-label">{label}</div>'
1222
- f'<div class="mc-value">{value}</div>{sub_html}</div>')
1223
-
1224
-
1225
- def section(title):
1226
- return f'<div class="section-title">{title}</div>'
1227
-
1228
-
1229
- def cards_row(cards):
1230
- return f'<div class="metric-row">{"".join(cards)}</div>' if cards else ""
1231
-
1232
-
1233
- def error_box(msg):
1234
- return f'<div class="error-box">⚠ {msg}</div>'
1235
-
1236
-
1237
- # ---------------------------------------------------------------------------
1238
- # CHART BUILDERS
1239
- # ---------------------------------------------------------------------------
1240
- def _base_layout(fig, title, height):
1241
- fig.update_layout(
1242
- template="plotly_dark", height=height, margin=dict(l=10, r=10, t=40, b=10),
1243
- paper_bgcolor=CHART_BG, plot_bgcolor=CHART_BG, title=title,
1244
- legend=dict(orientation="h", y=1.05, font=dict(size=10)),
1245
- font=dict(family="IBM Plex Mono, monospace", size=11),
1246
- )
1247
- return fig
1248
-
1249
-
1250
- def _empty_fig(msg, title="", height=220):
1251
- fig = go.Figure()
1252
- fig.add_annotation(text=msg, x=0.5, y=0.5, xref="paper", yref="paper", showarrow=False,
1253
- font=dict(size=13, color="#7c8797"))
1254
- fig.update_xaxes(visible=False)
1255
- fig.update_yaxes(visible=False)
1256
- return _base_layout(fig, title, height)
1257
-
1258
-
1259
- def build_price_chart(df, ind, symbol, bar):
1260
- fig = make_subplots(rows=2, cols=1, shared_xaxes=True, row_heights=[0.75, 0.25], vertical_spacing=0.03)
1261
- fig.add_trace(go.Candlestick(
1262
- x=df["ts"], open=df["o"], high=df["h"], low=df["l"], close=df["c"], name=symbol,
1263
- increasing_line_color="#3ddc97", decreasing_line_color="#ff5c72"), row=1, col=1)
1264
- fig.add_trace(go.Scatter(x=df["ts"], y=ind["ema20"], name="EMA20",
1265
- line=dict(color="#5b8cff", width=1)), row=1, col=1)
1266
- fig.add_trace(go.Scatter(x=df["ts"], y=ind["ema50"], name="EMA50",
1267
- line=dict(color="#f2b84b", width=1)), row=1, col=1)
1268
- fig.add_trace(go.Scatter(x=df["ts"], y=ind["bb_upper"], name="BB Üst",
1269
- line=dict(color="rgba(150,160,255,0.45)", width=1, dash="dot")), row=1, col=1)
1270
- fig.add_trace(go.Scatter(x=df["ts"], y=ind["bb_lower"], name="BB Alt",
1271
- line=dict(color="rgba(150,160,255,0.45)", width=1, dash="dot"),
1272
- fill="tonexty", fillcolor="rgba(150,160,255,0.05)"), row=1, col=1)
1273
- vol_colors = np.where(df["c"] >= df["o"], "#3ddc97", "#ff5c72")
1274
- fig.add_trace(go.Bar(x=df["ts"], y=df["vol"], name="Hacim", marker_color=vol_colors, opacity=0.55),
1275
- row=2, col=1)
1276
- fig.update_layout(xaxis_rangeslider_visible=False)
1277
- return _base_layout(fig, f"{symbol} · {bar}", 520)
1278
-
1279
-
1280
- def build_signal_price_chart(df, ind, symbol, bar, res):
1281
- """Sinyal sekmesinin ana grafiği: mum + Supertrend + HiLo + Fibonacci + Ichimoku bulutu + S/R + geçmiş sinyal işaretleri + işlem planı."""
1282
- n = len(df)
1283
- v0 = max(0, n - 200)
1284
- sl = slice(v0, n)
1285
- ts = df["ts"].iloc[sl]
1286
- fig = go.Figure()
1287
- fig.add_trace(go.Candlestick(
1288
- x=ts, open=df["o"].iloc[sl], high=df["h"].iloc[sl], low=df["l"].iloc[sl], close=df["c"].iloc[sl],
1289
- name=symbol, increasing_line_color="#3ddc97", decreasing_line_color="#ff5c72"))
1290
- # Ichimoku bulutu
1291
- fig.add_trace(go.Scatter(x=ts, y=ind["ich_a"].iloc[sl], line=dict(width=0), showlegend=False, hoverinfo="skip"))
1292
- fig.add_trace(go.Scatter(x=ts, y=ind["ich_b"].iloc[sl], line=dict(width=0), name="Ichimoku Bulutu",
1293
- fill="tonexty", fillcolor="rgba(120,140,255,0.10)", hoverinfo="skip"))
1294
- # Supertrend (yükseliş/düşüş ayrı renk)
1295
- st_line, st_dir = ind["st_line"].iloc[sl], ind["st_dir"].iloc[sl]
1296
- fig.add_trace(go.Scatter(x=ts, y=st_line.where(st_dir > 0), name="Supertrend ▲",
1297
- line=dict(color="#3ddc97", width=1.5), connectgaps=False))
1298
- fig.add_trace(go.Scatter(x=ts, y=st_line.where(st_dir < 0), name="Supertrend ▼",
1299
- line=dict(color="#ff5c72", width=1.5), connectgaps=False))
1300
- # Gann HiLo Activator (yükseliş/düşüş ayrı renk)
1301
- hl_line, hl_dir = ind["hilo_line"].iloc[sl], ind["hilo_dir"].iloc[sl]
1302
- fig.add_trace(go.Scatter(x=ts, y=hl_line.where(hl_dir > 0), name="HiLo ▲", mode="lines",
1303
- line=dict(color="#7fe6b8", width=1.2, dash="dot", shape="hv"), connectgaps=False))
1304
- fig.add_trace(go.Scatter(x=ts, y=hl_line.where(hl_dir < 0), name="HiLo ▼", mode="lines",
1305
- line=dict(color="#ff9aa8", width=1.2, dash="dot", shape="hv"), connectgaps=False))
1306
- # Fibonacci düzeltme seviyeleri
1307
- fib = res.get("fib")
1308
- if fib:
1309
- for r, px in fib["levels"]:
1310
- key = r in (0.382, 0.5, 0.618)
1311
- fig.add_hline(y=px, line=dict(color="rgba(242,184,75,%s)" % ("0.65" if key else "0.3"), width=1 if key else 0.7, dash="dashdot"),
1312
- annotation_text=f"Fib {r:g} · {fmt_price(px)}", annotation_position="right",
1313
- annotation_font=dict(size=9, color="#f2b84b"))
1314
- # Destek / direnç
1315
- for lv, tc in res["supports"][:3]:
1316
- fig.add_hline(y=lv, line=dict(color="rgba(61,220,151,0.55)", dash="dash", width=1),
1317
- annotation_text=f"D {fmt_price(lv)} ×{tc}", annotation_position="left",
1318
- annotation_font=dict(size=9, color="#3ddc97"))
1319
- for lv, tc in res["resists"][:3]:
1320
- fig.add_hline(y=lv, line=dict(color="rgba(255,92,114,0.55)", dash="dash", width=1),
1321
- annotation_text=f"R {fmt_price(lv)} ×{tc}", annotation_position="left",
1322
- annotation_font=dict(size=9, color="#ff5c72"))
1323
- # Geçmiş sinyal işlemleri
1324
- bt = res["backtest"]
1325
- if bt and bt["trades"]:
1326
- atr_a = ind["_arr"]["atr"]
1327
- for d, sym_, col, nm in ((1, "triangle-up", "#3ddc97", "Geçmiş LONG"), (-1, "triangle-down", "#ff5c72", "Geçmiş SHORT")):
1328
- tr = [t for t in bt["trades"] if t["dir"] == d and t["entry_i"] >= v0]
1329
- if not tr:
1330
- continue
1331
- xs = [df["ts"].iloc[t["entry_i"]] for t in tr]
1332
- ys = [(df["l"].iloc[t["entry_i"]] - 0.6 * atr_a[t["entry_i"]]) if d > 0
1333
- else (df["h"].iloc[t["entry_i"]] + 0.6 * atr_a[t["entry_i"]]) for t in tr]
1334
- hov = [f"{nm} · skor {t['score']:+.1f}<br>Sonuç: {t['why']} ({t['pnl'] * 100:+.2f}% · {t['r']:+.2f}R)" for t in tr]
1335
- fig.add_trace(go.Scatter(x=xs, y=ys, mode="markers", name=nm, text=hov, hoverinfo="text",
1336
- marker=dict(symbol=sym_, size=9, color=col, line=dict(width=0.5, color="#0a0e14"))))
1337
- # Güncel işlem planı
1338
- plan = res["plan"]
1339
- if plan:
1340
- x0, x1 = df["ts"].iloc[max(0, n - 30)], df["ts"].iloc[-1]
1341
- lines = [("GİRİŞ", plan["entry"], "#e6ebf2"), ("SL", plan["stop_loss"], "#ff5c72")]
1342
- lines += [(f"TP{k + 1}", px, "#3ddc97") for k, (px, _) in enumerate(plan["targets"])]
1343
- for lab, y, col in lines:
1344
- fig.add_shape(type="line", x0=x0, x1=x1, y0=y, y1=y, line=dict(color=col, width=1.4, dash="dot"))
1345
- fig.add_annotation(x=x1, y=y, text=f"{lab} {fmt_price(y)}", showarrow=False, xanchor="left",
1346
- font=dict(size=9, color=col))
1347
- fig.update_layout(xaxis_rangeslider_visible=False)
1348
- return _base_layout(fig, f"{symbol} · {bar} · Sinyal Haritası", 560)
1349
-
1350
-
1351
- def build_factor_chart(contribs, symbol):
1352
- items = [(l, v) for l, v in contribs if abs(v) > 1e-9]
1353
- if not items:
1354
- return _empty_fig("Faktör verisi yok", f"{symbol} · Faktör Katkıları")
1355
- items.sort(key=lambda x: x[1])
1356
- labels, vals = [l for l, _ in items], [v for _, v in items]
1357
- fig = go.Figure(go.Bar(
1358
- x=vals, y=labels, orientation="h", marker_color=["#3ddc97" if v > 0 else "#ff5c72" for v in vals],
1359
- text=[f"{v:+.2f}" for v in vals], textposition="outside", cliponaxis=False))
1360
- fig.update_layout(showlegend=False, xaxis_title="Ağırlıklı katkı (rejim ağırlığı uygulanmış)")
1361
- return _base_layout(fig, f"{symbol} · Faktör Katkıları (skoru ne yönlendiriyor?)", max(300, 25 * len(items) + 100))
1362
-
1363
-
1364
- def build_backtest_chart(bt, df, symbol):
1365
- if not bt or not bt.get("stats"):
1366
- return _empty_fig("Backtest için yeterli işlem oluşmadı (en az 3 işlem gerekir)", f"{symbol} · Sinyal Backtesti")
1367
- start = bt["start"]
1368
- ts = df["ts"].iloc[start:]
1369
- fig = make_subplots(rows=2, cols=1, row_heights=[0.62, 0.38], vertical_spacing=0.14,
1370
- subplot_titles=("Sinyal Stratejisi vs Al&Tut (komisyon dahil, tam pozisyon)", "İşlem Bazında R Çarpanı"))
1371
- fig.add_trace(go.Scatter(x=ts, y=bt["equity"], name="Sinyal Stratejisi", line=dict(color="#5b8cff", width=2, shape="hv")),
1372
- row=1, col=1)
1373
- fig.add_trace(go.Scatter(x=ts, y=bt["bh"], name="Al ve Tut", line=dict(color="#7c8797", width=1.2)), row=1, col=1)
1374
- rs = [t["r"] for t in bt["trades"]]
1375
- fig.add_trace(go.Bar(x=list(range(1, len(rs) + 1)), y=rs, name="R",
1376
- marker_color=["#3ddc97" if r > 0 else "#ff5c72" for r in rs],
1377
- text=[t["why"] for t in bt["trades"]], hoverinfo="text+y"), row=2, col=1)
1378
- fig.update_xaxes(title_text="İşlem #", row=2, col=1)
1379
- return _base_layout(fig, f"{symbol} · Walk-Forward İşlem Simülasyonu", 470)
1380
-
1381
-
1382
- def build_signal_chart(df, ind, symbol):
1383
- fig = make_subplots(rows=4, cols=1, shared_xaxes=True, row_heights=[0.27, 0.25, 0.24, 0.24],
1384
- vertical_spacing=0.05,
1385
- subplot_titles=("RSI (14)", "MACD (12,26,9)", "Stokastik (14,3)", "ADX / DI (14)"))
1386
- fig.add_trace(go.Scatter(x=df["ts"], y=ind["rsi"], name="RSI", line=dict(color="#5b8cff")), row=1, col=1)
1387
- fig.add_hline(y=70, line=dict(color="#ff5c72", dash="dot", width=1), row=1, col=1)
1388
- fig.add_hline(y=30, line=dict(color="#3ddc97", dash="dot", width=1), row=1, col=1)
1389
-
1390
- hist_colors = np.where(ind["macd_hist"] >= 0, "#3ddc97", "#ff5c72")
1391
- fig.add_trace(go.Bar(x=df["ts"], y=ind["macd_hist"], name="Histogram", marker_color=hist_colors), row=2, col=1)
1392
- fig.add_trace(go.Scatter(x=df["ts"], y=ind["macd_line"], name="MACD",
1393
- line=dict(color="#f2b84b", width=1)), row=2, col=1)
1394
- fig.add_trace(go.Scatter(x=df["ts"], y=ind["macd_signal"], name="Sinyal",
1395
- line=dict(color="#5b8cff", width=1)), row=2, col=1)
1396
-
1397
- fig.add_trace(go.Scatter(x=df["ts"], y=ind["stoch_k"], name="%K", line=dict(color="#5b8cff")), row=3, col=1)
1398
- fig.add_trace(go.Scatter(x=df["ts"], y=ind["stoch_d"], name="%D", line=dict(color="#f2b84b")), row=3, col=1)
1399
- fig.add_hline(y=80, line=dict(color="#ff5c72", dash="dot", width=1), row=3, col=1)
1400
- fig.add_hline(y=20, line=dict(color="#3ddc97", dash="dot", width=1), row=3, col=1)
1401
-
1402
- fig.add_trace(go.Scatter(x=df["ts"], y=ind["adx"], name="ADX", line=dict(color="#e6ebf2", width=1.5)), row=4, col=1)
1403
- fig.add_trace(go.Scatter(x=df["ts"], y=ind["plus_di"], name="+DI", line=dict(color="#3ddc97", width=1)), row=4, col=1)
1404
- fig.add_trace(go.Scatter(x=df["ts"], y=ind["minus_di"], name="-DI", line=dict(color="#ff5c72", width=1)), row=4, col=1)
1405
- fig.add_hline(y=25, line=dict(color="#f2b84b", dash="dot", width=1), row=4, col=1)
1406
-
1407
- return _base_layout(fig, f"{symbol} · Momentum Göstergeleri", 720)
1408
-
1409
-
1410
- def build_equity_chart(df, symbol):
1411
- ret = df["c"].pct_change().fillna(0)
1412
- cum = (1 + ret).cumprod()
1413
- peak = cum.cummax()
1414
- dd = (cum - peak) / peak
1415
- fig = make_subplots(rows=2, cols=1, shared_xaxes=True, row_heights=[0.65, 0.35], vertical_spacing=0.06,
1416
- subplot_titles=("Kümülatif Getiri (Al ve Tut)", "Drawdown"))
1417
- fig.add_trace(go.Scatter(x=df["ts"], y=cum, name="Kümülatif Getiri", line=dict(color="#5b8cff"),
1418
- fill="tozeroy", fillcolor="rgba(91,140,255,0.08)"), row=1, col=1)
1419
- fig.add_trace(go.Scatter(x=df["ts"], y=dd, name="Drawdown", line=dict(color="#ff5c72"),
1420
- fill="tozeroy", fillcolor="rgba(255,92,114,0.12)"), row=2, col=1)
1421
- fig.update_layout(showlegend=False)
1422
- return _base_layout(fig, f"{symbol} · Risk Profili", 440)
1423
-
1424
-
1425
- def build_depth_chart(bids, asks, symbol):
1426
- bids_sorted = sorted(bids, key=lambda x: -x[0])
1427
- asks_sorted = sorted(asks, key=lambda x: x[0])
1428
- bid_prices = [p for p, _ in bids_sorted]
1429
- bid_cum = np.cumsum([s for _, s in bids_sorted])
1430
- ask_prices = [p for p, _ in asks_sorted]
1431
- ask_cum = np.cumsum([s for _, s in asks_sorted])
1432
- fig = go.Figure()
1433
- fig.add_trace(go.Scatter(x=bid_prices, y=bid_cum, name="Alış (Bid)", line=dict(color="#3ddc97", width=2),
1434
- fill="tozeroy", fillcolor="rgba(61,220,151,0.15)"))
1435
- fig.add_trace(go.Scatter(x=ask_prices, y=ask_cum, name="Satış (Ask)", line=dict(color="#ff5c72", width=2),
1436
- fill="tozeroy", fillcolor="rgba(255,92,114,0.15)"))
1437
- fig.update_layout(xaxis_title="Fiyat", yaxis_title="Kümülatif Miktar")
1438
- return _base_layout(fig, f"{symbol} · Emir Defteri Derinliği", 380)
1439
-
1440
-
1441
- # ---------------------------------------------------------------------------
1442
- # TAB CALLBACKS
1443
- # ---------------------------------------------------------------------------
1444
- def load_overview(symbol, bar):
1445
- try:
1446
- df = fetch_candles(symbol, bar, 300)
1447
- ticker = fetch_ticker(symbol)
1448
- ind = compute_indicators(df)
1449
- last = float(ticker.get("last") or df["c"].iloc[-1])
1450
- open24h = float(ticker.get("open24h") or df["c"].iloc[0])
1451
- high24h = float(ticker.get("high24h") or df["h"].max())
1452
- low24h = float(ticker.get("low24h") or df["l"].min())
1453
- vol24h = float(ticker.get("volCcy24h") or df["vol"].sum())
1454
- chg = (last - open24h) / open24h if open24h else 0.0
1455
- tone = "up" if chg >= 0 else "down"
1456
- cards = [
1457
- metric_card("SON FİYAT", f"${fmt_price(last)}", "", tone),
1458
- metric_card("24S DEĞİŞİM", fmt_pct(chg), "", tone),
1459
- metric_card("24S YÜKSEK", f"${fmt_price(high24h)}"),
1460
- metric_card("24S DÜŞÜK", f"${fmt_price(low24h)}"),
1461
- metric_card("24S HACİM (USDT)", f"${fmt_large(vol24h)}"),
1462
- ]
1463
- return cards_row(cards), build_price_chart(df, ind, symbol, bar)
1464
- except Exception as e:
1465
- return error_box(f"Veri alınamadı: {e}"), go.Figure()
1466
-
1467
-
1468
- def _reasons_html(reasons):
1469
- icon = {"up": "▲", "down": "▼", "flat": "•"}
1470
- return "".join(f'<li class="r-{t}"><span class="ri">{icon[t]}</span>{txt}</li>' for t, txt in reasons)
1471
-
1472
-
1473
- def load_signal(symbol, bar, closed_only=False, acct=10000, risk_pct=1.0):
1474
- empty5 = lambda html: (html, go.Figure(), go.Figure(), go.Figure(), go.Figure())
1475
- try:
1476
- df = fetch_candles(symbol, bar, 300)
1477
- if closed_only:
1478
- df = df[df["confirm"] == 1].reset_index(drop=True)
1479
- if len(df) < 30:
1480
- return empty5(error_box("Sinyal için yeterli mum verisi yok (en az 30 bar gerekir)."))
1481
- acct = float(acct or 0)
1482
- risk_pct = float(risk_pct or 0)
1483
- res = generate_signal(df, symbol=symbol, bar=bar, mtf=True, micro=True, closed_only=closed_only,
1484
- acct=acct, risk_pct=risk_pct)
1485
- ind, plan, bt = res["indicators"], res["plan"], res["backtest"]
1486
-
1487
- # ---- Ana panel ----
1488
- regime_col = {"TREND": "#5b8cff", "YATAY": "#f2b84b", "GEÇİŞ": "#7c8797"}[res["regime"]]
1489
- panel = (
1490
- f'<div class="signal-panel">'
1491
- f'<div class="signal-badge" style="--tone:{res["color"]}">{res["signal"]}</div>'
1492
- f'<div class="signal-meta"><div class="mc-label">KOMPOZİT SKOR</div>'
1493
- f'<div class="mc-value">{res["score"]:+.2f} / ±10</div></div>'
1494
- f'<div class="signal-meta"><div class="mc-label">GÜVEN</div>'
1495
- f'<div class="mc-value">%{res["confidence"]}</div></div>'
1496
- f'<div class="signal-meta"><div class="mc-label">KALİTE NOTU</div>'
1497
- f'<div class="mc-value" style="color:{res["grade_color"]}">{res["grade"]}</div></div>'
1498
- f'<div class="signal-meta"><div class="mc-label">REJİM</div>'
1499
- f'<div class="mc-value" style="color:{regime_col}">{res["regime"]}</div></div>'
1500
- f'</div>'
1501
- )
1502
- tot = res["bull_w"] + res["bear_w"]
1503
- bull_pct = (res["bull_w"] / tot * 100) if tot > 0 else 50.0
1504
- consensus = (
1505
- f'<div class="consensus"><div class="mc-label">FAKTÖR KONSENSÜSÜ · BOĞA %{bull_pct:.0f} / AYI %{100 - bull_pct:.0f}</div>'
1506
- f'<div class="cbar"><div class="cbull" style="width:{bull_pct:.1f}%"></div>'
1507
- f'<div class="cbear" style="width:{100 - bull_pct:.1f}%"></div></div></div>'
1508
- )
1509
- conf_tags = "".join(
1510
- f'<div class="tag" style="color:var(--text-muted)">{k} %{v * 100:.0f}</div>' for k, v in res["conf_parts"].items())
1511
- tag_html = ""
1512
- if res["tags"]:
1513
- tag_html = '<div class="tags" style="margin-bottom:12px;">' + "".join(
1514
- f'<div class="tag" style="border-color:{c};color:{c};">{t}</div>' for t, c in res["tags"]) + "</div>"
1515
- conf_html = f'<div class="tags" style="margin-bottom:12px;">{conf_tags}</div>'
1516
-
1517
- # ---- İşlem planı + pozisyon boyutu ----
1518
- plan_cards, size_cards = [], []
1519
- if plan:
1520
- up = plan["direction"] == "LONG"
1521
- plan_cards = [
1522
- metric_card("ÖNERİLEN YÖN", plan["direction"], "", "up" if up else "down"),
1523
- metric_card("GİRİŞ", f"${fmt_price(plan['entry'])}"),
1524
- metric_card("STOP-LOSS", f"${fmt_price(plan['stop_loss'])}",
1525
- f"{plan['stop_type']} · %{plan['risk_frac'] * 100:.2f} risk", "down"),
1526
- ]
1527
- for k, (px, m) in enumerate(plan["targets"]):
1528
- plan_cards.append(metric_card(f"HEDEF {k + 1}", f"${fmt_price(px)}", f"{m:.2f}R", "up"))
1529
- sz = plan["sizing"]
1530
- if sz:
1531
- size_cards = [
1532
- metric_card("RİSK TUTARI", f"${fmt_large(sz['risk_amount'])}", f"Hesabın %{risk_pct:g}'i"),
1533
- metric_card("POZİSYON (USDT)", f"${fmt_large(sz['notional'])}", f"Miktar: {sz['qty']:.6g}"),
1534
- metric_card("KALDIRAÇ ETKİSİ", f"{sz['leverage']:.2f}x",
1535
- "Spot ile yetmez — kaldıraç gerekir" if sz["leverage"] > 1.0 else "Spot ile uygulanabilir",
1536
- "down" if sz["leverage"] > 1.0 else "up"),
1537
- ]
1538
- stats = bt["stats"] if bt else None
1539
- if stats is not None:
1540
- k = stats["kelly"]
1541
- if stats["n"] < 20:
1542
- kv, ks, kt = "—", "Örnek az (<20 işlem)", "neutral"
1543
- elif k is None or k <= 0:
1544
- kv, ks, kt = "Kenar yok", "Kelly ≤ 0", "down"
1545
- else:
1546
- half = min(k / 2, 0.03)
1547
- kv, ks, kt = f"%{half * 100:.2f}", f"Yarım-Kelly (tavan %3) · bilgi amaçlı", "up"
1548
- size_cards.append(metric_card("ÖNERİLEN RİSK", kv, ks, kt))
1549
-
1550
- # ---- Backtest kartları ----
1551
- bt_cards = []
1552
- if stats:
1553
- L, S = stats["long"], stats["short"]
1554
- wr = lambda x: f"%{x['win_rate'] * 100:.0f}" if x["win_rate"] is not None else "—"
1555
- bt_cards = [
1556
- metric_card("İŞLEM SAYISI", f"{stats['n']}", f"Long {L['n']} ({wr(L)}) · Short {S['n']} ({wr(S)})"),
1557
- metric_card("KAZANMA ORANI", f"%{stats['win_rate'] * 100:.1f}", f"Maks. üst üste kayıp: {stats['max_consec_loss']}",
1558
- "up" if stats["win_rate"] >= 0.45 else "down"),
1559
- metric_card("PROFIT FACTOR", f"{stats['profit_factor']:.2f}", "Brüt kazanç / brüt kayıp",
1560
- "up" if stats["profit_factor"] >= 1.2 else "down"),
1561
- metric_card("BEKLENTİ / İŞLEM", fmt_pct(stats["expectancy"]), f"Ort. {stats['avg_r']:+.2f}R (komisyon dahil)",
1562
- "up" if stats["expectancy"] > 0 else "down"),
1563
- metric_card("SQN", f"{stats['sqn']:.2f}", "Sistem kalitesi (>2 iyi, >3 çok iyi)",
1564
- "up" if stats["sqn"] >= 2 else "neutral"),
1565
- metric_card("STRATEJİ MAKS. DD", fmt_pct(stats["max_dd"]), "", "down"),
1566
- metric_card("STRATEJİ vs AL&TUT", f"{fmt_pct(stats['total_return'])} / {fmt_pct(stats['bh_return'])}",
1567
- "Aynı dönem, komisyon dahil", "up" if stats["total_return"] > stats["bh_return"] else "down"),
1568
- ]
1569
- acc_cards = []
1570
- if bt and bt["acc"]:
1571
- a = bt["acc"]
1572
- acc_cards.append(metric_card("YÖN DOĞRULUĞU", f"%{a['accuracy'] * 100:.1f}",
1573
- f"{a['n_signals']} sinyal · {a['forward_bars']} bar ileri",
1574
- "up" if a["accuracy"] >= 0.5 else "down"))
1575
- if a.get("trend_regime"):
1576
- t = a["trend_regime"]
1577
- acc_cards.append(metric_card("DOĞRULUK · TREND", f"%{t['accuracy'] * 100:.1f}",
1578
- f"{t['n_signals']} sinyal (ADX ≥ 25)", "up" if t["accuracy"] >= 0.5 else "down"))
1579
- if a.get("range_regime"):
1580
- t = a["range_regime"]
1581
- acc_cards.append(metric_card("DOĞRULUK · YATAY", f"%{t['accuracy'] * 100:.1f}",
1582
- f"{t['n_signals']} sinyal (ADX &lt; 25)", "up" if t["accuracy"] >= 0.5 else "down"))
1583
-
1584
- # ---- Piyasa bağlamı ----
1585
- ctx = []
1586
- for m in res["mtf"]:
1587
- if m["aligned"] is None:
1588
- ctx.append(metric_card(f"{m['bar']} TEYİDİ", "Nötr", f"Üst zaman dilimi skoru: {m['score']}"))
1589
- else:
1590
- ctx.append(metric_card(f"{m['bar']} TEYİDİ", "Uyumlu ✓" if m["aligned"] else "Ters Yönlü ⚠",
1591
- f"Üst zaman dilimi skoru: {m['score']}", "up" if m["aligned"] else "down"))
1592
- if res["funding"] is not None:
1593
- f_ = res["funding"]
1594
- ctx.append(metric_card("FUNDING (8S)", f"{f_ * 100:+.4f}%", "Pozitif = long kalabalık",
1595
- "down" if f_ > 0.0002 else ("up" if f_ < -0.0002 else "neutral")))
1596
- if res["imbalance"] is not None:
1597
- im = res["imbalance"]
1598
- ctx.append(metric_card("DEFTER DENGESİ", f"{im * 100:+.0f}%", "Derinlik ağırlıklı",
1599
- "up" if im > 0.2 else ("down" if im < -0.2 else "neutral")))
1600
- if res["supports"]:
1601
- lv, tc = res["supports"][0]
1602
- ctx.append(metric_card("YAKIN DESTEK", f"${fmt_price(lv)}", f"{tc} temas · {(res['price'] - lv) / res['atr']:.1f} ATR", "up"))
1603
- if res["resists"]:
1604
- lv, tc = res["resists"][0]
1605
- ctx.append(metric_card("YAKIN DİRENÇ", f"${fmt_price(lv)}", f"{tc} temas · {(lv - res['price']) / res['atr']:.1f} ATR", "down"))
1606
- ctx.append(metric_card("VOLATİLİTE YÜZDELİĞİ", f"%{res['atr_rank'] * 100:.0f}", "ATR/fiyat, son 100 bar",
1607
- "down" if res["atr_rank"] > 0.9 else "neutral"))
1608
-
1609
- reasons_box = (f'<div class="reasons-box"><div class="mc-label">FAKTÖR ANALİZİ</div>'
1610
- f'<ul>{_reasons_html(res["reasons"])}</ul></div>')
1611
- bt_note = ""
1612
- if stats:
1613
- bt_note = ('<div class="note">Backtest: son ~220 barda sinyaller üst üste binmeden işleme çevrilir; giriş bir sonraki barın açılışı, '
1614
- f'çıkış SL {SL_ATR}×ATR / TP {TP_ATR}×ATR / {MAX_HOLD_BARS} bar; komisyon %{FEE_PER_SIDE * 100:.1f}/yön dahildir. '
1615
- 'Emir defteri, funding, S/R ve MTF katmanları canlıdır, geçmişe uygulanamaz.</div>')
1616
-
1617
- html = (panel + consensus + conf_html + tag_html
1618
- + (section("İŞLEM PLANI") + cards_row(plan_cards) if plan_cards else "")
1619
- + (section("POZİSYON BOYUTU") + cards_row(size_cards) if size_cards else "")
1620
- + (section("GEÇMİŞ PERFORMANS (WALK-FORWARD)") + cards_row(bt_cards) + cards_row(acc_cards) + bt_note
1621
- if (bt_cards or acc_cards) else "")
1622
- + section("PİYASA BAĞLAMI") + cards_row(ctx) + reasons_box)
1623
- return (html, build_signal_price_chart(df, ind, symbol, bar, res), build_factor_chart(res["contribs"], symbol),
1624
- build_backtest_chart(bt, df, symbol), build_signal_chart(df, ind, symbol))
1625
- except Exception as e:
1626
- return empty5(error_box(f"Veri alınamadı: {e}"))
1627
-
1628
-
1629
- def load_risk(symbol, bar):
1630
- try:
1631
- df = fetch_candles(symbol, bar, 300)
1632
- rm = risk_metrics(df, bar)
1633
- if rm is None:
1634
- return error_box("Risk hesaplaması için yeterli veri yok."), go.Figure()
1635
- cards = [
1636
- metric_card("YILLIKLANDIRILMIŞ VOLATİLİTE", fmt_pct(rm["volatility_annual"])),
1637
- metric_card("YILLIKLANDIRILMIŞ GETİRİ", fmt_pct(rm["return_annual"]), "",
1638
- "up" if rm["return_annual"] >= 0 else "down"),
1639
- metric_card("SHARPE ORANI", f"{rm['sharpe']:.2f}", "", "up" if rm["sharpe"] > 0 else "down"),
1640
- metric_card("MAKS. DRAWDOWN", fmt_pct(rm["max_drawdown"]), "", "down"),
1641
- metric_card("GÜNCEL DRAWDOWN", fmt_pct(rm["current_dd"]), "",
1642
- "down" if rm["current_dd"] < 0 else "neutral"),
1643
- metric_card("VaR %95 (BAR)", fmt_pct(rm["var95_bar"]), "", "down"),
1644
- metric_card("CVaR %95 (BAR)", fmt_pct(rm["cvar95_bar"]), "", "down"),
1645
- ]
1646
- return cards_row(cards), build_equity_chart(df, symbol)
1647
- except Exception as e:
1648
- return error_box(f"Veri alınamadı: {e}"), go.Figure()
1649
-
1650
-
1651
- def load_orderbook(symbol, depth):
1652
- try:
1653
- ob = fetch_orderbook(symbol, int(depth))
1654
- bids = [(float(p), float(s)) for p, s, *_ in ob.get("bids", [])]
1655
- asks = [(float(p), float(s)) for p, s, *_ in ob.get("asks", [])]
1656
- if not bids or not asks:
1657
- return error_box("Emir defteri verisi alınamadı."), go.Figure()
1658
- best_bid, best_ask = bids[0][0], asks[0][0]
1659
- spread = best_ask - best_bid
1660
- spread_pct = spread / best_ask if best_ask else 0.0
1661
- bid_vol = sum(s for _, s in bids)
1662
- ask_vol = sum(s for _, s in asks)
1663
- imbalance = (bid_vol - ask_vol) / (bid_vol + ask_vol) if (bid_vol + ask_vol) else 0.0
1664
- tone = "up" if imbalance > 0.05 else ("down" if imbalance < -0.05 else "neutral")
1665
- cards = [
1666
- metric_card("EN İYİ ALIŞ", f"${fmt_price(best_bid)}", "", "up"),
1667
- metric_card("EN İYİ SATIŞ", f"${fmt_price(best_ask)}", "", "down"),
1668
- metric_card("SPREAD", f"${fmt_price(spread)}", fmt_pct(spread_pct)),
1669
- metric_card("DEFTER DENGESİZLİĞİ", fmt_pct(imbalance), "Pozitif = alış baskısı", tone),
1670
- ]
1671
- return cards_row(cards), build_depth_chart(bids, asks, symbol)
1672
- except Exception as e:
1673
- return error_box(f"Veri alınamadı: {e}"), go.Figure()
1674
-
1675
-
1676
- @_gpu
1677
- def refresh_all(symbol, bar, depth, closed_only, acct, risk_pct):
1678
- ov_html, ov_fig = load_overview(symbol, bar)
1679
- sig = load_signal(symbol, bar, closed_only, acct, risk_pct)
1680
- risk_html, risk_fig = load_risk(symbol, bar)
1681
- ob_html, ob_fig = load_orderbook(symbol, depth)
1682
- return (ov_html, ov_fig, *sig, risk_html, risk_fig, ob_html, ob_fig)
1683
-
1684
-
1685
- def refresh_signal(symbol, bar, closed_only, acct, risk_pct):
1686
- """Yalnızca sinyal sekmesini yeniler (risk/kapanmış-mum ayarı değişince hızlı güncelleme)."""
1687
- return load_signal(symbol, bar, closed_only, acct, risk_pct)
1688
-
1689
-
1690
- SCAN_MODES = ["Tümü (skora göre)", "Yalnızca AL yönlü", "Yalnızca SAT yönlü", "Yalnızca yüksek kalite (A / A+)"]
1691
-
1692
-
1693
- def _scan_one(sym, bar, all_tickers, closed_only):
1694
- df = fetch_candles(sym, bar, 300)
1695
- if closed_only:
1696
- df = df[df["confirm"] == 1].reset_index(drop=True)
1697
- # MTF ve mikro-yapı katmanları taramada kapalı (API yükünü azaltmak için); tekil analizde açık.
1698
- res = generate_signal(df, symbol=sym, bar=bar, mtf=False, micro=False, closed_only=closed_only)
1699
- rm = risk_metrics(df, bar)
1700
- t = all_tickers.get(sym, {})
1701
- last = float(t.get("last") or df["c"].iloc[-1])
1702
- open24h = float(t.get("open24h") or df["c"].iloc[0])
1703
- chg24h = (last - open24h) / open24h if open24h else 0.0
1704
- div = res["divergence"]
1705
- div_txt = "Yükseliş" if div["bullish"] else ("Düşüş" if div["bearish"] else "—")
1706
- bt = res["backtest"]
1707
- st = bt["stats"] if bt else None
1708
- acc = bt["acc"] if bt else None
1709
- return {
1710
- "Sembol": sym, "Fiyat": last, "24S %": round(chg24h * 100, 2), "Sinyal": res["signal"],
1711
- "Skor": res["score"], "Kalite": res["grade"], "Güven %": res["confidence"], "Rejim": res["regime"],
1712
- "RSI": round(res["indicators"]["rsi"].iloc[-1], 1), "ADX": round(res["adx"], 1),
1713
- "Sıkışma": "●" if res["squeeze_on"] else "—", "Uyumsuzluk": div_txt,
1714
- "Kazanma %": round(st["win_rate"] * 100, 1) if st else None,
1715
- "Profit Factor": round(st["profit_factor"], 2) if st else None,
1716
- "Beklenti %": round(st["expectancy"] * 100, 2) if st else None,
1717
- "Yön Doğruluğu %": round(acc["accuracy"] * 100, 1) if acc else None,
1718
- "Volatilite (Yıllık) %": round(rm["volatility_annual"] * 100, 1) if rm else None,
1719
- "Sharpe": round(rm["sharpe"], 2) if rm else None,
1720
- }
1721
-
1722
-
1723
- def run_scanner(symbols_text, bar, closed_only=False, mode=SCAN_MODES[0], progress=gr.Progress()):
1724
- symbols = [s.strip().upper() for s in symbols_text.split(",") if s.strip()][:25]
1725
- if not symbols:
1726
- return pd.DataFrame()
1727
- try:
1728
- all_tickers = {t["instId"]: t for t in fetch_all_tickers("SPOT")}
1729
- except Exception:
1730
- all_tickers = {}
1731
-
1732
- rows, done = [], 0
1733
- with ThreadPoolExecutor(max_workers=4) as ex: # ağ beklemesini paralelle (OKX limiti için 4 ile sınırlı)
1734
- futs = {ex.submit(_scan_one, s, bar, all_tickers, closed_only): s for s in symbols}
1735
- for f in as_completed(futs):
1736
- done += 1
1737
- progress(done / len(symbols), desc=f"{futs[f]} tarandı ({done}/{len(symbols)})")
1738
- try:
1739
- rows.append(f.result())
1740
- except Exception:
1741
- continue
1742
- if not rows:
1743
- return pd.DataFrame()
1744
- out = pd.DataFrame(rows)
1745
- if mode == SCAN_MODES[1]:
1746
- out = out[out["Skor"] >= SIGNAL_THRESHOLD].sort_values("Skor", ascending=False)
1747
- elif mode == SCAN_MODES[2]:
1748
- out = out[out["Skor"] <= -SIGNAL_THRESHOLD].sort_values("Skor", ascending=True)
1749
- elif mode == SCAN_MODES[3]:
1750
- out = out[out["Kalite"].isin(["A", "A+"])].sort_values("Güven %", ascending=False)
1751
- else:
1752
- out = out.sort_values("Skor", ascending=False)
1753
- return out.reset_index(drop=True)
1754
-
1755
-
1756
- # ---------------------------------------------------------------------------
1757
- # STYLE — dark "trading terminal" identity (deliberate, not default SaaS chrome)
1758
- # ---------------------------------------------------------------------------
1759
- HEAD = """
1760
- <link rel="preconnect" href="https://fonts.googleapis.com">
1761
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
1762
- <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=IBM+Plex+Sans:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
1763
- """
1764
-
1765
- CSS = """
1766
- :root {
1767
- --bg-void: #0a0e14;
1768
- --bg-panel: #12171f;
1769
- --line: #232a35;
1770
- --text-primary: #e6ebf2;
1771
- --text-muted: #7c8797;
1772
- --accent-long: #3ddc97;
1773
- --accent-short: #ff5c72;
1774
- --accent-signal: #f2b84b;
1775
- --accent-primary: #5b8cff;
1776
- }
1777
- .gradio-container { background: var(--bg-void) !important; color: var(--text-primary) !important;
1778
- font-family: 'IBM Plex Sans', system-ui, sans-serif !important; }
1779
- h1, h2, h3 { font-family: 'Space Grotesk', sans-serif !important; letter-spacing: -0.01em; }
1780
-
1781
- #masthead { display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:10px;
1782
- border-bottom: 1px solid var(--line); padding: 14px 4px 18px 4px; margin-bottom: 6px; }
1783
- #masthead .wordmark { font-family:'Space Grotesk',sans-serif; font-weight:700; font-size:1.5rem; letter-spacing:-0.02em; }
1784
- #masthead .wordmark span { color: var(--accent-primary); }
1785
- #masthead .tags { display:flex; gap:8px; flex-wrap:wrap; }
1786
- #masthead .tag { font-size:0.7rem; padding:3px 9px; border:1px solid var(--line); border-radius:3px;
1787
- color:var(--text-muted); font-family:'IBM Plex Mono',monospace; }
1788
-
1789
- .metric-row { display:flex; flex-wrap:wrap; gap:10px; margin:8px 0 16px 0; }
1790
- .metric-card { background: var(--bg-panel); border:1px solid var(--line); border-radius:4px;
1791
- padding:12px 16px; min-width:150px; flex:1; }
1792
- .mc-label { font-size:0.66rem; color:var(--text-muted); letter-spacing:0.04em; margin-bottom:6px; }
1793
- .mc-value { font-size:1.24rem; font-weight:600; color:var(--text-primary);
1794
- font-family:'IBM Plex Mono',monospace; }
1795
- .mc-sub { font-size:0.72rem; color:var(--text-muted); margin-top:4px; font-family:'IBM Plex Mono',monospace; }
1796
- .metric-card.mc-up .mc-value { color: var(--accent-long); }
1797
- .metric-card.mc-down .mc-value { color: var(--accent-short); }
1798
-
1799
- .section-title { font-family:'Space Grotesk',sans-serif; font-size:0.78rem; font-weight:600; letter-spacing:0.08em;
1800
- color:var(--text-muted); margin:18px 0 0 0; padding-top:12px; border-top:1px solid var(--line); }
1801
-
1802
- .signal-panel { display:flex; align-items:center; flex-wrap:wrap; gap:20px; background:var(--bg-panel);
1803
- border:1px solid var(--line); border-radius:4px; padding:18px 20px; margin-bottom:12px; }
1804
- .signal-badge { font-family:'Space Grotesk',sans-serif; font-weight:700; font-size:1.6rem; color:var(--tone);
1805
- border:2px solid var(--tone); border-radius:4px; padding:10px 22px; }
1806
- .signal-meta .mc-value { font-size:1.35rem; }
1807
-
1808
- .consensus { background:var(--bg-panel); border:1px solid var(--line); border-radius:4px; padding:12px 16px; margin-bottom:12px; }
1809
- .cbar { display:flex; height:8px; border-radius:2px; overflow:hidden; background:var(--line); }
1810
- .cbull { background: var(--accent-long); }
1811
- .cbear { background: var(--accent-short); }
1812
-
1813
- .tags { display:flex; gap:8px; flex-wrap:wrap; }
1814
- .tags .tag { font-size:0.72rem; font-weight:600; padding:5px 12px; border:1px solid var(--line);
1815
- border-radius:3px; font-family:'IBM Plex Mono',monospace; letter-spacing:0.03em; }
1816
-
1817
- .reasons-box { background:var(--bg-panel); border:1px solid var(--line); border-radius:4px; padding:16px 20px; margin-top:8px; }
1818
- .reasons-box ul { margin:8px 0 0 0; padding-left:0; list-style:none; }
1819
- .reasons-box li { margin-bottom:6px; color:var(--text-primary); font-size:0.9rem; }
1820
- .reasons-box .ri { display:inline-block; width:18px; font-size:0.7rem; }
1821
- .reasons-box .r-up .ri { color: var(--accent-long); }
1822
- .reasons-box .r-down .ri { color: var(--accent-short); }
1823
- .reasons-box .r-flat .ri { color: var(--text-muted); }
1824
-
1825
- .note { color:var(--text-muted); font-size:0.74rem; line-height:1.5; margin:-6px 0 8px 0; }
1826
-
1827
- .error-box { background: rgba(255,92,114,0.08); border:1px solid var(--accent-short); color:var(--accent-short);
1828
- padding:14px 18px; border-radius:4px; font-family:'IBM Plex Mono',monospace; font-size:0.85rem; }
1829
-
1830
- .disclaimer { border-top:1px solid var(--line); margin-top:22px; padding-top:14px; color:var(--text-muted);
1831
- font-size:0.76rem; line-height:1.55; }
1832
- """
1833
-
1834
- # ---------------------------------------------------------------------------
1835
- # GRADIO UI
1836
- # ---------------------------------------------------------------------------
1837
- with gr.Blocks(title="OKX Quant Terminal") as demo:
1838
-
1839
- gr.HTML(
1840
- '<div id="masthead">'
1841
- '<div class="wordmark">OKX <span>QUANT</span> TERMINAL</div>'
1842
- '<div class="tags"><div class="tag">ELITE SIGNAL ENGINE v2</div>'
1843
- '<div class="tag">PUBLIC MARKET DATA</div>'
1844
- '<div class="tag">READ-ONLY · EMİR GÖNDERİLMEZ</div></div>'
1845
- '</div>'
1846
- )
1847
-
1848
- with gr.Row():
1849
- symbol_dd = gr.Dropdown(choices=POPULAR_SYMBOLS, value="BTC-USDT", allow_custom_value=True,
1850
- label="Sembol", scale=2)
1851
- bar_dd = gr.Dropdown(choices=BAR_OPTIONS, value="1H", label="Zaman Dilimi", scale=1)
1852
- depth_slider = gr.Slider(5, 50, value=20, step=5, label="Defter Derinliği", scale=1)
1853
- refresh_btn = gr.Button("🔄 Yenile", variant="primary", scale=1)
1854
- auto_cb = gr.Checkbox(label="Otomatik (15sn)", value=False, scale=1)
1855
-
1856
- with gr.Row():
1857
- closed_cb = gr.Checkbox(label="Sadece kapanmış mumlarla sinyal (repaint yok)", value=False, scale=2)
1858
- acct_num = gr.Number(value=10000, label="Hesap Büyüklüğü (USDT)", minimum=0, scale=1)
1859
- risk_sl = gr.Slider(0.25, 5, value=1, step=0.25, label="İşlem Başına Risk (%)", scale=1)
1860
-
1861
- timer = gr.Timer(15, active=False)
1862
-
1863
- with gr.Tabs():
1864
- with gr.Tab("📊 Genel Bakış"):
1865
- ov_html = gr.HTML()
1866
- ov_plot = gr.Plot()
1867
- with gr.Tab("🎯 Sinyal Motoru"):
1868
- sig_html = gr.HTML()
1869
- sig_map_plot = gr.Plot()
1870
- sig_factor_plot = gr.Plot()
1871
- sig_bt_plot = gr.Plot()
1872
- sig_ind_plot = gr.Plot()
1873
- with gr.Tab("⚠️ Risk Analitiği"):
1874
- risk_html = gr.HTML()
1875
- risk_plot = gr.Plot()
1876
- with gr.Tab("📖 Emir Defteri"):
1877
- ob_html = gr.HTML()
1878
- ob_plot = gr.Plot()
1879
- with gr.Tab("🔍 Çoklu Varlık Tarayıcı"):
1880
- gr.Markdown("Popüler pariteleri kompozit sinyal skoruna göre sıralar. Kendi listenizi virgülle ayırıp girebilirsiniz (en fazla 25 sembol).")
1881
- with gr.Row():
1882
- scan_symbols = gr.Textbox(value=",".join(POPULAR_SYMBOLS), label="Semboller", scale=3)
1883
- scan_bar = gr.Dropdown(choices=BAR_OPTIONS, value="4H", label="Zaman Dilimi", scale=1)
1884
- scan_mode = gr.Dropdown(choices=SCAN_MODES, value=SCAN_MODES[0], label="Filtre", scale=2)
1885
- scan_btn = gr.Button("🔍 Taramayı Başlat", variant="primary")
1886
- scan_out = gr.Dataframe(interactive=False, wrap=True)
1887
-
1888
- gr.HTML(
1889
- '<div class="disclaimer">Bu panel yalnızca eğitim ve bilgilendirme amaçlıdır; yatırım tavsiyesi '
1890
- 'değildir. Tüm veriler OKX genel (public) piyasa API\'sinden alınır; hiçbir API anahtarı '
1891
- 'kullanılmaz ve hiçbir gerçek emir gönderilmez. Kripto varlıklar yüksek oynaklık içerir; geçmiş '
1892
- 'performans (backtest dahil) gelecekteki sonuçların göstergesi değildir. Backtest sonuçları '
1893
- 'kısa örneklem üzerindedir ve istatistiksel olarak sınırlıdır.</div>'
1894
- )
1895
-
1896
- io_inputs = [symbol_dd, bar_dd, depth_slider, closed_cb, acct_num, risk_sl]
1897
- io_outputs = [ov_html, ov_plot, sig_html, sig_map_plot, sig_factor_plot, sig_bt_plot, sig_ind_plot,
1898
- risk_html, risk_plot, ob_html, ob_plot]
1899
- sig_inputs = [symbol_dd, bar_dd, closed_cb, acct_num, risk_sl]
1900
- sig_outputs = [sig_html, sig_map_plot, sig_factor_plot, sig_bt_plot, sig_ind_plot]
1901
-
1902
- refresh_btn.click(refresh_all, io_inputs, io_outputs)
1903
- timer.tick(refresh_all, io_inputs, io_outputs)
1904
- auto_cb.change(lambda active: gr.Timer(active=active), auto_cb, timer)
1905
- closed_cb.change(refresh_signal, sig_inputs, sig_outputs)
1906
- acct_num.submit(refresh_signal, sig_inputs, sig_outputs)
1907
- risk_sl.release(refresh_signal, sig_inputs, sig_outputs)
1908
- scan_btn.click(run_scanner, [scan_symbols, scan_bar, closed_cb, scan_mode], scan_out)
1909
- demo.load(refresh_all, io_inputs, io_outputs)
1910
-
1911
- demo.queue().launch(
1912
- theme=gr.themes.Base(primary_hue="blue", neutral_hue="slate"),
1913
- css=CSS,
1914
- head=HEAD,
1915
- ssr_mode=False,
1916
- )