GoshawkVortexAI commited on
Commit
cececf9
·
verified ·
1 Parent(s): 7414e34

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +1915 -0
app.py ADDED
@@ -0,0 +1,1915 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ OKX QUANT TERMINAL · v3 — SMART MONEY CONCEPTS (SMC) SİNYAL MOTORU
3
+ Kripto piyasa analitiği paneli (Gradio).
4
+
5
+ Yalnızca OKX'in herkese açık (anahtarsız) piyasa verisi REST API'si kullanılır:
6
+ - /api/v5/market/candles, /ticker, /tickers, /books
7
+ - /api/v5/public/funding-rate (türev duyarlılığı, isteğe bağlı bilgi)
8
+
9
+ API anahtarı yok, emir gönderimi yok, hesap erişimi yok. Salt-okunur analiz / sinyal terminalidir.
10
+
11
+ v3 — AĞIRLIKSIZ SMC SİNYAL MOTORU
12
+ * Eski ağırlıklı kompozit skor (trend/reversion/Fib/ısı haritası/RSI uyumsuzluğu ağırlıkları, ADX rejim
13
+ geçişi, tanh sıkıştırma, skor ivmesi, MTF/emir defteri/funding puan ekleri) TAMAMEN KALDIRILDI.
14
+ * YÖN = piyasa yapısı: son onaylı swing'in kapanışla kırılması (BOS = devam, CHoCH = dönüş).
15
+ Yapı yükselişse yalnızca long, düşüşse yalnızca short aranır; yapıya ters teyit varsa sinyal yok.
16
+ * TEYİTLER (her biri eşit, ±1): taze BOS/CHoCH · premium/discount · order block · FVG retest · likidite avı.
17
+ Skor = net teyit sayısı (±5). AL/SAT: |skor| ≥ 2, GÜÇLÜ: |skor| ≥ 4 + teyit uyumu + üst zaman dilimi yapısı uyumu.
18
+ * Üst zaman dilimi (MTF), emir defteri, funding, S/R: yalnızca BİLGİ / filtre — skora puan eklemez.
19
+ * LLM katmanı kaldırıldı. Fiyat grafikleri 3D (Plotly Scatter3d / Mesh3d / Surface).
20
+ * Güven skoru bileşenlerin eşit ağırlıklı ortalamasıdır. Stop: order block / likidite avı ekstremi arkası.
21
+ * Gerçek işlem simülasyonu (SL/TP/zaman çıkışı + komisyon): kazanma oranı, profit factor, beklenti, R, SQN,
22
+ drawdown, Al&Tut karşılaştırması, yarım-Kelly risk önerisi. Canlı sinyal ve backtest AYNI nedensel fonksiyonu kullanır.
23
+ * RSI / MACD / Stokastik / ADX grafikleri bilgi amaçlıdır; sinyale dahil değildir.
24
+ """
25
+ import math
26
+ import threading
27
+ import time
28
+ from concurrent.futures import ThreadPoolExecutor, as_completed
29
+
30
+ import numpy as np
31
+ import pandas as pd
32
+ import plotly.graph_objects as go
33
+ from plotly.subplots import make_subplots
34
+ import requests
35
+ import gradio as gr
36
+
37
+ try: # Hugging Face Spaces (ZeroGPU) — yerelde yoksa sessizce atla
38
+ import spaces
39
+ _gpu = spaces.GPU(duration=30)
40
+ except Exception:
41
+ def _gpu(fn):
42
+ return fn
43
+
44
+ # ---------------------------------------------------------------------------
45
+ # CONFIG
46
+ # ---------------------------------------------------------------------------
47
+ OKX_BASE = "https://www.okx.com"
48
+ TIMEOUT = 10
49
+ CACHE_TTL = 8 # saniye — aynı mum verisini sekmeler arasında tekrar çekmemek için
50
+
51
+ POPULAR_SYMBOLS = [
52
+ "BTC-USDT", "ETH-USDT", "SOL-USDT", "BNB-USDT", "XRP-USDT",
53
+ "DOGE-USDT", "ADA-USDT", "AVAX-USDT", "LINK-USDT", "TON-USDT",
54
+ "TRX-USDT", "DOT-USDT", "MATIC-USDT", "LTC-USDT", "NEAR-USDT",
55
+ "SUI-USDT", "APT-USDT", "ARB-USDT", "OP-USDT", "INJ-USDT",
56
+ ]
57
+
58
+ BAR_OPTIONS = ["1m", "3m", "5m", "15m", "30m", "1H", "2H", "4H", "6H", "12H", "1D", "1W"]
59
+
60
+ # approximate periods-per-year for each bar size, used to annualize risk stats
61
+ BAR_TO_PERIODS_PER_YEAR = {
62
+ "1m": 365 * 24 * 60, "3m": 365 * 24 * 20, "5m": 365 * 24 * 12,
63
+ "15m": 365 * 24 * 4, "30m": 365 * 24 * 2, "1H": 365 * 24,
64
+ "2H": 365 * 12, "4H": 365 * 6, "6H": 365 * 4, "12H": 365 * 2,
65
+ "1D": 365, "1W": 52,
66
+ }
67
+
68
+ CHART_BG = "#0a0e14"
69
+
70
+ # her zaman dilimi için "bir üst" zaman dilimi -> çoklu zaman dilimi (MTF) teyidi için
71
+ MTF_MAP = {
72
+ "1m": "15m", "3m": "30m", "5m": "1H", "15m": "4H", "30m": "4H",
73
+ "1H": "4H", "2H": "1D", "4H": "1D", "6H": "1D", "12H": "1W",
74
+ "1D": "1W", "1W": "1W",
75
+ }
76
+
77
+ # --- Sinyal motoru parametreleri ---
78
+ SIGNAL_THRESHOLD = 2.0 # |net SMC teyit sayısı| >= bu değer -> AL / SAT (her teyit eşit: ±1)
79
+ STRONG_THRESHOLD = 4.0 # |net teyit| >= bu değer + konsensüs + MTF uyumu -> GÜÇLÜ
80
+ WARMUP_BARS = 60 # yapı (swing/BOS) oluşması için minimum bar
81
+ BACKTEST_LOOKBACK = 220 # walk-forward test penceresi (bar)
82
+ SL_ATR, TP_ATR = 1.5, 2.5 # backtest ve varsayılan işlem planı (ATR çarpanları)
83
+ MAX_HOLD_BARS = 24 # zaman çıkışı
84
+ HEAT_WINDOW = 100 # hacim ısı haritası penceresi (bar)
85
+ HEAT_BINS = 40 # fiyat dilimi sayısı
86
+ HEAT_COLD = 0.30 # yoğunluk bu orandan düşükse "soğuk bölge" (hacim boşluğu)
87
+ WALL_MULT = 3.0 # emir defteri duvarı: seviye, taraf medyanının en az bu kadar katı
88
+ FEE_PER_SIDE = 0.001 # OKX spot taker ≈ %0.1 / yön
89
+ TP_R_MULTIPLES = (TP_ATR / SL_ATR, 2.5, 4.0) # TP1/TP2/TP3 (R cinsinden)
90
+
91
+ # --- Smart Money Concepts (SMC) parametreleri ---
92
+ SMC_SWING = 3 # swing pivotu için her iki yanda gereken bar (pivot w bar sonra onaylanır)
93
+ SMC_FRESH_BARS = 8 # BOS/CHoCH bu kadar bar boyunca 'taze' sayılır
94
+ SMC_SWEEP_BARS = 5 # likidite avı bu kadar bar boyunca geçerli
95
+ SMC_OB_LOOKBACK = 40 # order block orijini için geriye bakış (bar)
96
+ SMC_OB_MAX_AGE = 120 # order block / FVG en fazla bu kadar bar yaşar
97
+ SMC_FVG_MIN_ATR = 0.15 # FVG boşluğu en az bu kadar ATR olmalı (gürültü filtresi)
98
+ SMC_FVG_KEEP = 6 # takip edilen açık FVG sayısı (yön başına)
99
+ SMC_PD_BAND = 0.05 # equilibrium (%50) etrafındaki nötr band
100
+
101
+
102
+ # ---------------------------------------------------------------------------
103
+ # OKX PUBLIC API WRAPPERS (önbellek + yeniden deneme)
104
+ # ---------------------------------------------------------------------------
105
+ _CACHE = {}
106
+ _CACHE_LOCK = threading.Lock()
107
+
108
+
109
+ def _cached(key, ttl, loader):
110
+ now = time.time()
111
+ with _CACHE_LOCK:
112
+ hit = _CACHE.get(key)
113
+ if hit and now - hit[0] < ttl:
114
+ return hit[1]
115
+ val = loader()
116
+ with _CACHE_LOCK:
117
+ _CACHE[key] = (time.time(), val)
118
+ if len(_CACHE) > 200: # eski kayıtları buda
119
+ for k in sorted(_CACHE, key=lambda x: _CACHE[x][0])[:60]:
120
+ _CACHE.pop(k, None)
121
+ return val
122
+
123
+
124
+ def _get(path, params=None, retries=2):
125
+ for attempt in range(retries + 1):
126
+ try:
127
+ r = requests.get(f"{OKX_BASE}{path}", params=params, timeout=TIMEOUT)
128
+ r.raise_for_status()
129
+ data = r.json()
130
+ code = str(data.get("code"))
131
+ if code == "50011" and attempt < retries: # OKX rate-limit
132
+ time.sleep(0.5 * (attempt + 1))
133
+ continue
134
+ if code != "0":
135
+ raise RuntimeError(data.get("msg") or "OKX API hata döndürdü.")
136
+ return data["data"]
137
+ except requests.exceptions.RequestException as e:
138
+ if attempt < retries:
139
+ time.sleep(0.3 * (attempt + 1))
140
+ continue
141
+ raise RuntimeError(f"Ağ hatası: {e}")
142
+
143
+
144
+ def fetch_candles(inst_id, bar="1H", limit=300):
145
+ def _load():
146
+ raw = _get("/api/v5/market/candles", {"instId": inst_id, "bar": bar, "limit": limit})
147
+ if not raw:
148
+ raise RuntimeError(f"{inst_id} için mum verisi bulunamadı (sembolü kontrol edin).")
149
+ cols = ["ts", "o", "h", "l", "c", "vol", "volCcy", "volCcyQuote", "confirm"]
150
+ df = pd.DataFrame(raw, columns=cols)
151
+ for c in ["o", "h", "l", "c", "vol"]:
152
+ df[c] = df[c].astype(float)
153
+ df["confirm"] = df["confirm"].astype(int)
154
+ df["ts"] = pd.to_datetime(df["ts"].astype(np.int64), unit="ms")
155
+ return df.sort_values("ts").reset_index(drop=True)
156
+
157
+ return _cached(("candles", inst_id, bar, limit), CACHE_TTL, _load).copy()
158
+
159
+
160
+ def fetch_ticker(inst_id):
161
+ raw = _get("/api/v5/market/ticker", {"instId": inst_id})
162
+ return raw[0] if raw else {}
163
+
164
+
165
+ def fetch_all_tickers(inst_type="SPOT"):
166
+ return _cached(("tickers", inst_type), 20, lambda: _get("/api/v5/market/tickers", {"instType": inst_type}))
167
+
168
+
169
+ def fetch_orderbook(inst_id, depth=20):
170
+ raw = _get("/api/v5/market/books", {"instId": inst_id, "sz": depth})
171
+ return raw[0] if raw else {}
172
+
173
+
174
+ def fetch_funding_rate(inst_id):
175
+ """Spot paritenin (BTC-USDT) sürekli vadeli karşılığının (BTC-USDT-SWAP) funding oranı."""
176
+ parts = inst_id.split("-")
177
+ if len(parts) != 2:
178
+ return None
179
+ raw = _get("/api/v5/public/funding-rate", {"instId": f"{inst_id}-SWAP"})
180
+ if not raw:
181
+ return None
182
+ return float(raw[0].get("fundingRate") or 0.0)
183
+
184
+
185
+ # ---------------------------------------------------------------------------
186
+ # TECHNICAL INDICATORS
187
+ # ---------------------------------------------------------------------------
188
+ def ema(s, p):
189
+ return s.ewm(span=p, adjust=False).mean()
190
+
191
+
192
+ def sma(s, p):
193
+ return s.rolling(p).mean()
194
+
195
+
196
+ def rsi(s, p=14):
197
+ d = s.diff()
198
+ gain = d.clip(lower=0)
199
+ loss = -d.clip(upper=0)
200
+ ag = gain.ewm(alpha=1 / p, adjust=False).mean()
201
+ al = loss.ewm(alpha=1 / p, adjust=False).mean()
202
+ rs = ag / al.replace(0, np.nan)
203
+ return (100 - 100 / (1 + rs)).fillna(50)
204
+
205
+
206
+ def macd(s, fast=12, slow=26, signal=9):
207
+ line = ema(s, fast) - ema(s, slow)
208
+ sig = ema(line, signal)
209
+ return line, sig, line - sig
210
+
211
+
212
+ def bollinger(s, p=20, k=2):
213
+ mid = sma(s, p)
214
+ std = s.rolling(p).std()
215
+ return mid + k * std, mid, mid - k * std
216
+
217
+
218
+ def atr(df, p=14):
219
+ h, l, c = df["h"], df["l"], df["c"]
220
+ pc = c.shift(1)
221
+ tr = pd.concat([h - l, (h - pc).abs(), (l - pc).abs()], axis=1).max(axis=1)
222
+ return tr.ewm(alpha=1 / p, adjust=False).mean()
223
+
224
+
225
+ def stochastic(df, k_p=14, d_p=3):
226
+ lo = df["l"].rolling(k_p).min()
227
+ hi = df["h"].rolling(k_p).max()
228
+ k = 100 * (df["c"] - lo) / (hi - lo).replace(0, np.nan)
229
+ d = k.rolling(d_p).mean()
230
+ return k.fillna(50), d.fillna(50)
231
+
232
+
233
+
234
+
235
+ def adx(df, p=14):
236
+ """ADX + yönsel indikatörler (+DI / -DI). Trend gücünü ölçer (0-100)."""
237
+ h, l, c = df["h"], df["l"], df["c"]
238
+ up_move = h.diff()
239
+ down_move = -l.diff()
240
+ plus_dm = np.where((up_move > down_move) & (up_move > 0), up_move, 0.0)
241
+ minus_dm = np.where((down_move > up_move) & (down_move > 0), down_move, 0.0)
242
+ tr = pd.concat([h - l, (h - c.shift(1)).abs(), (l - c.shift(1)).abs()], axis=1).max(axis=1)
243
+ atr_ = tr.ewm(alpha=1 / p, adjust=False).mean()
244
+ plus_di = 100 * pd.Series(plus_dm, index=df.index).ewm(alpha=1 / p, adjust=False).mean() / atr_.replace(0, np.nan)
245
+ minus_di = 100 * pd.Series(minus_dm, index=df.index).ewm(alpha=1 / p, adjust=False).mean() / atr_.replace(0, np.nan)
246
+ dx = ((plus_di - minus_di).abs() / (plus_di + minus_di).replace(0, np.nan)) * 100
247
+ adx_ = dx.ewm(alpha=1 / p, adjust=False).mean()
248
+ return adx_.fillna(0), plus_di.fillna(0), minus_di.fillna(0)
249
+
250
+
251
+
252
+
253
+
254
+
255
+
256
+
257
+
258
+
259
+
260
+
261
+
262
+
263
+ def _volume_grid(h, l, vol, edges):
264
+ """Her barın hacmini, [low, high] aralığının kestiği fiyat dilimlerine eşit dağıtır. -> (bar × dilim)"""
265
+ lo_e, hi_e = edges[:-1][None, :], edges[1:][None, :]
266
+ overlap = np.clip(np.minimum(h[:, None], hi_e) - np.maximum(l[:, None], lo_e), 0.0, None)
267
+ span = np.maximum(h - l, 1e-12)[:, None]
268
+ return overlap / span * vol[:, None]
269
+
270
+
271
+ def volume_heat(df, window=HEAT_WINDOW, bins=HEAT_BINS):
272
+ """Hacim ısı haritası profili (nedensel: her bar yalnızca kendisine kadar olan pencereyi kullanır).
273
+ Dönüş: poc, val, vah (%70 değer alanı), heat (kapanışın bulunduğu dilimin yoğunluğu / en yoğun dilim, 0..1)"""
274
+ h, l, c, v = (df[k].to_numpy(dtype=float) for k in ("h", "l", "c", "vol"))
275
+ n = len(c)
276
+ poc, val, vah, heat = (np.full(n, np.nan) for _ in range(4))
277
+ for i in range(min(window, n) - 1, n):
278
+ a = max(0, i - window + 1)
279
+ hh, ll = h[a:i + 1], l[a:i + 1]
280
+ top, bot = hh.max(), ll.min()
281
+ if top <= bot:
282
+ continue
283
+ edges = np.linspace(bot, top, bins + 1)
284
+ prof = _volume_grid(hh, ll, v[a:i + 1], edges).sum(axis=0)
285
+ if prof.max() <= 0:
286
+ continue
287
+ mids = (edges[:-1] + edges[1:]) / 2
288
+ k = int(prof.argmax())
289
+ poc[i] = mids[k]
290
+ # değer alanı: POC'tan başlayıp toplam hacmin %70'ine ulaşana dek komşu dilimleri ekle
291
+ lo_k = hi_k = k
292
+ acc, tot = prof[k], prof.sum()
293
+ while acc < 0.7 * tot and (lo_k > 0 or hi_k < bins - 1):
294
+ up = prof[hi_k + 1] if hi_k < bins - 1 else -1.0
295
+ dn = prof[lo_k - 1] if lo_k > 0 else -1.0
296
+ if up >= dn:
297
+ hi_k += 1; acc += prof[hi_k]
298
+ else:
299
+ lo_k -= 1; acc += prof[lo_k]
300
+ val[i], vah[i] = edges[lo_k], edges[hi_k + 1]
301
+ ci = int(np.clip(np.searchsorted(edges, c[i], side="right") - 1, 0, bins - 1))
302
+ heat[i] = prof[ci] / prof.max()
303
+ return (pd.Series(x, index=df.index) for x in (poc, val, vah, heat))
304
+
305
+
306
+ def book_walls(ob, price, atr_v, mult=WALL_MULT, reach=1.5):
307
+ """Emir defterindeki 'sıcak' likidite duvarlarını bulur (yakın ve taraf medyanına göre çok büyük seviyeler).
308
+ Dönüş: (destek_duvarı, direnç_duvarı) — her biri (fiyat, büyüklük_kat) ya da None."""
309
+ def pick(levels, below):
310
+ if not levels or atr_v <= 0:
311
+ return None
312
+ sizes = np.array([float(x[1]) for x in levels])
313
+ px = np.array([float(x[0]) for x in levels])
314
+ med = float(np.median(sizes))
315
+ if med <= 0:
316
+ return None
317
+ dist = (price - px) / atr_v if below else (px - price) / atr_v
318
+ ok = (sizes >= mult * med) & (dist > 0) & (dist <= reach)
319
+ if not ok.any():
320
+ return None
321
+ j = int(np.where(ok, sizes, -1).argmax())
322
+ return float(px[j]), float(sizes[j] / med)
323
+ return pick(ob.get("bids"), True), pick(ob.get("asks"), False)
324
+
325
+
326
+
327
+
328
+
329
+
330
+
331
+
332
+
333
+
334
+
335
+
336
+
337
+
338
+ def find_swings(series, window=3):
339
+ """Yerel tepe/dip (swing high/low) indekslerini bulur."""
340
+ n = len(series)
341
+ highs, lows = [], []
342
+ for i in range(window, n - window):
343
+ seg = series.iloc[i - window:i + window + 1]
344
+ if series.iloc[i] == seg.max():
345
+ highs.append(i)
346
+ if series.iloc[i] == seg.min():
347
+ lows.append(i)
348
+ return highs, lows
349
+
350
+
351
+
352
+
353
+
354
+
355
+ def find_sr_levels(df, atr_v, window=5, lookback=200, max_levels=4):
356
+ """Swing tepe/diplerini kümeleyerek destek/direnç seviyeleri çıkarır.
357
+ Dönüş: (destekler, dirençler) — her biri [(seviye, temas_sayısı), ...], fiyata yakınlık sırasıyla."""
358
+ sub = df.iloc[-lookback:].reset_index(drop=True)
359
+ highs, _ = find_swings(sub["h"], window)
360
+ _, lows = find_swings(sub["l"], window)
361
+ pts = [float(sub["h"].iloc[i]) for i in highs] + [float(sub["l"].iloc[i]) for i in lows]
362
+ if not pts:
363
+ return [], []
364
+ price = float(df["c"].iloc[-1])
365
+ tol = max(0.6 * (atr_v if atr_v and atr_v > 0 else 0.0), price * 0.002)
366
+ pts.sort()
367
+ clusters, cur = [], [pts[0]]
368
+ for p in pts[1:]:
369
+ if p - float(np.mean(cur)) <= tol:
370
+ cur.append(p)
371
+ else:
372
+ clusters.append(cur)
373
+ cur = [p]
374
+ clusters.append(cur)
375
+ levels = [(float(np.mean(c)), len(c)) for c in clusters]
376
+ supports = sorted([lv for lv in levels if lv[0] < price], key=lambda x: -x[0])[:max_levels]
377
+ resists = sorted([lv for lv in levels if lv[0] > price], key=lambda x: x[0])[:max_levels]
378
+ return supports, resists
379
+
380
+
381
+ def smc_structure(df, atr_s, w=SMC_SWING):
382
+ """Smart Money Concepts yapısı — tamamen NEDENSEL (bar i yalnızca i'ye kadar olan veriyi kullanır;
383
+ swing pivotları `w` bar sonra onaylanır). Üretir:
384
+ * Piyasa yapısı: son onaylı swing high/low'un KAPANIŞLA kırılması. Trend yönünde kırılım = BOS (devam),
385
+ trende karşı kırılım = CHoCH (dönüş).
386
+ * Order Block: kırılımı başlatan hareketin orijinindeki son karşı yönlü mum (kapanışla aşılırsa geçersiz).
387
+ * Fair Value Gap: 3 mumluk fiyat boşluğu (kapanışla tamamen dolarsa geçersiz) + retest takibi.
388
+ * Likidite avı (sweep): fitil swing seviyesini aşar, kapanış seviyenin gerisinde kalır.
389
+ Dönüş: (seri sözlüğü, grafik için bölgeler)"""
390
+ o, h, l, c = (df[k].to_numpy(dtype=float) for k in ("o", "h", "l", "c"))
391
+ a = atr_s.to_numpy(dtype=float)
392
+ n = len(c)
393
+ full = lambda v: np.full(n, v, dtype=float)
394
+ out = {k: full(np.nan) for k in ("sh", "sl", "obb_lo", "obb_hi", "obs_lo", "obs_hi", "swb_px", "sws_px")}
395
+ out.update({k: full(0.0) for k in ("trend", "brk_dir", "brk_choch", "fvgb", "fvgs")})
396
+ out.update({k: full(-1.0) for k in ("brk_i", "swb_i", "sws_i")})
397
+
398
+ last_sh = last_sl = None # (indeks, fiyat) — son onaylı swing high / low
399
+ sh_broken = sl_broken = True
400
+ sh_swept = sl_swept = False
401
+ trend = 0
402
+ brk_dir, brk_i, brk_choch = 0, -1, 0
403
+ bull_ob = bear_ob = None # (alt, üst, oluşum_bar, mum_bar)
404
+ bull_fvg, bear_fvg = [], [] # [(alt, üst, oluşum_bar), ...]
405
+ swb_i = sws_i = -1
406
+ swb_px = sws_px = np.nan
407
+ events, sweeps = [], []
408
+
409
+ for i in range(n):
410
+ j = i - w
411
+ if j >= w:
412
+ if h[j] == h[j - w:j + w + 1].max():
413
+ last_sh, sh_broken, sh_swept = (j, h[j]), False, False
414
+ if l[j] == l[j - w:j + w + 1].min():
415
+ last_sl, sl_broken, sl_swept = (j, l[j]), False, False
416
+ ci = c[i]
417
+
418
+ # --- yapı kırılımı (kapanışla): BOS / CHoCH + order block ---
419
+ if last_sh and not sh_broken and ci > last_sh[1]:
420
+ choch = 1 if trend < 0 else 0
421
+ events.append({"i": i, "pivot_i": last_sh[0], "dir": 1, "choch": bool(choch), "level": float(last_sh[1])})
422
+ trend, sh_broken = 1, True
423
+ brk_dir, brk_i, brk_choch = 1, i, choch
424
+ a0 = max(last_sl[0] if last_sl else 0, i - SMC_OB_LOOKBACK)
425
+ cand = [k for k in range(a0, i) if c[k] < o[k]] # düşüş mumları
426
+ if cand:
427
+ k = min(cand, key=lambda x: l[x]) # hareketin orijini: en düşük düşüş mumu
428
+ bull_ob = (float(l[k]), float(h[k]), i, k)
429
+ elif last_sl and not sl_broken and ci < last_sl[1]:
430
+ choch = 1 if trend > 0 else 0
431
+ events.append({"i": i, "pivot_i": last_sl[0], "dir": -1, "choch": bool(choch), "level": float(last_sl[1])})
432
+ trend, sl_broken = -1, True
433
+ brk_dir, brk_i, brk_choch = -1, i, choch
434
+ a0 = max(last_sh[0] if last_sh else 0, i - SMC_OB_LOOKBACK)
435
+ cand = [k for k in range(a0, i) if c[k] > o[k]] # yükseliş mumları
436
+ if cand:
437
+ k = max(cand, key=lambda x: h[x])
438
+ bear_ob = (float(l[k]), float(h[k]), i, k)
439
+
440
+ # --- likidite avı (sweep): fitil seviyeyi aşar, kapanış geri döner ---
441
+ if last_sh and not sh_broken and not sh_swept and h[i] > last_sh[1] and ci <= last_sh[1]:
442
+ sh_swept, sws_i, sws_px = True, i, float(h[i])
443
+ sweeps.append({"i": i, "dir": -1, "level": float(last_sh[1])}) # buy-side likidite alındı
444
+ if last_sl and not sl_broken and not sl_swept and l[i] < last_sl[1] and ci >= last_sl[1]:
445
+ sl_swept, swb_i, swb_px = True, i, float(l[i])
446
+ sweeps.append({"i": i, "dir": 1, "level": float(last_sl[1])}) # sell-side likidite alındı
447
+
448
+ # --- order block mitigasyonu (kapanışla aşıldıysa ya da çok eskiyse geçersiz) ---
449
+ if bull_ob and bull_ob[2] < i and (ci < bull_ob[0] or i - bull_ob[2] > SMC_OB_MAX_AGE):
450
+ bull_ob = None
451
+ if bear_ob and bear_ob[2] < i and (ci > bear_ob[1] or i - bear_ob[2] > SMC_OB_MAX_AGE):
452
+ bear_ob = None
453
+
454
+ # --- Fair Value Gap: oluşum, mitigasyon, retest ---
455
+ if i >= 2:
456
+ min_gap = SMC_FVG_MIN_ATR * a[i] if a[i] == a[i] else 0.0
457
+ if l[i] > h[i - 2] and l[i] - h[i - 2] >= min_gap:
458
+ bull_fvg.append((float(h[i - 2]), float(l[i]), i))
459
+ if h[i] < l[i - 2] and l[i - 2] - h[i] >= min_gap:
460
+ bear_fvg.append((float(h[i]), float(l[i - 2]), i))
461
+ bull_fvg = [z for z in bull_fvg if not (z[2] < i and ci < z[0]) and i - z[2] <= SMC_OB_MAX_AGE][-SMC_FVG_KEEP:]
462
+ bear_fvg = [z for z in bear_fvg if not (z[2] < i and ci > z[1]) and i - z[2] <= SMC_OB_MAX_AGE][-SMC_FVG_KEEP:]
463
+ out["fvgb"][i] = 1.0 if any(z[2] <= i - 2 and l[i] <= z[1] and ci >= z[0] for z in bull_fvg) else 0.0
464
+ out["fvgs"][i] = 1.0 if any(z[2] <= i - 2 and h[i] >= z[0] and ci <= z[1] for z in bear_fvg) else 0.0
465
+
466
+ out["trend"][i] = trend
467
+ out["brk_dir"][i], out["brk_i"][i], out["brk_choch"][i] = brk_dir, brk_i, brk_choch
468
+ if last_sh:
469
+ out["sh"][i] = last_sh[1]
470
+ if last_sl:
471
+ out["sl"][i] = last_sl[1]
472
+ if bull_ob:
473
+ out["obb_lo"][i], out["obb_hi"][i] = bull_ob[0], bull_ob[1]
474
+ if bear_ob:
475
+ out["obs_lo"][i], out["obs_hi"][i] = bear_ob[0], bear_ob[1]
476
+ out["swb_i"][i], out["sws_i"][i], out["swb_px"][i], out["sws_px"][i] = swb_i, sws_i, swb_px, sws_px
477
+
478
+ zones = {"bull_ob": bull_ob, "bear_ob": bear_ob, "bull_fvg": bull_fvg, "bear_fvg": bear_fvg,
479
+ "events": events, "sweeps": sweeps}
480
+ return {k: pd.Series(v, index=df.index) for k, v in out.items()}, zones
481
+
482
+
483
+ def compute_indicators(df):
484
+ close = df["c"]
485
+ ind = {}
486
+ # --- bilgi amaçlı göstergeler (SİNYALE DAHİL DEĞİL) ---
487
+ ind["rsi"] = rsi(close)
488
+ ind["macd_line"], ind["macd_signal"], ind["macd_hist"] = macd(close)
489
+ ind["ema20"] = ema(close, 20)
490
+ ind["ema50"] = ema(close, 50)
491
+ ind["ema200"] = ema(close, 200 if len(close) >= 200 else max(5, len(close) // 2))
492
+ ind["bb_upper"], ind["bb_mid"], ind["bb_lower"] = bollinger(close)
493
+ ind["atr"] = atr(df)
494
+ ind["stoch_k"], ind["stoch_d"] = stochastic(df)
495
+ ind["adx"], ind["plus_di"], ind["minus_di"] = adx(df)
496
+ ind["vp_poc"], ind["vp_val"], ind["vp_vah"], ind["vp_heat"] = volume_heat(df)
497
+ ind["rvol"] = (df["vol"] / sma(df["vol"], 20).replace(0, np.nan)).fillna(1.0)
498
+ ind["atr_rank"] = (ind["atr"] / close).rolling(100, min_periods=30).apply(
499
+ lambda x: float((x <= x[-1]).mean()), raw=True).fillna(0.5)
500
+ # --- Smart Money Concepts (sinyalin tek kaynağı) ---
501
+ smc, zones = smc_structure(df, ind["atr"])
502
+ for k, v in smc.items():
503
+ ind["smc_" + k] = v
504
+ # hızlı, nedensel erişim için numpy önbelleği (backtest döngüsünde .iloc yerine kullanılır)
505
+ arr = {k: v.to_numpy(dtype=float) for k, v in ind.items()}
506
+ for k in ("o", "h", "l", "c", "vol"):
507
+ arr[k] = df[k].to_numpy(dtype=float)
508
+ ind["_arr"] = arr
509
+ ind["smc_zones"] = zones
510
+ return ind
511
+
512
+
513
+ # ---------------------------------------------------------------------------
514
+ # COMPOSITE SIGNAL ENGINE v2 (multi-factor, rejim-uyarlamalı, konsensüs filtreli)
515
+ # ---------------------------------------------------------------------------
516
+ def _score_at(ind, i, verbose=False):
517
+ """Smart Money Concepts teyit sayımı — AĞIRLIK YOK. Verilen bar indeksinde yalnızca o ana kadarki veri kullanılır.
518
+
519
+ YÖN FİLTRESİ (kural, ağırlık değil): piyasa yapısı. Son kırılım yukarıysa (BOS/CHoCH ▲) yalnızca LONG,
520
+ aşağıysa yalnızca SHORT aranır. Yapıya ters teyitler varsa sinyal verilmez — önce CHoCH beklenir.
521
+
522
+ TEYİTLER (her biri eşit: +1 boğa / −1 ayı / 0 yok):
523
+ 1) Taze BOS / CHoCH (son SMC_FRESH_BARS bar içinde yapı kırılımı)
524
+ 2) Premium / Discount (swing aralığının alt %45'i = discount → +1, üst %45'i = premium → −1)
525
+ 3) Order Block (fiyat aktif talep / arz bloğuna değdi)
526
+ 4) Fair Value Gap retest (fiyat açık FVG'ye geri döndü)
527
+ 5) Likidite avı (sweep) (son SMC_SWEEP_BARS bar içinde swing fitille süpürülüp geri dönüldü)
528
+
529
+ Skor = net teyit sayısı (−5…+5), yapı filtresini geçerse. Canlı sinyal ve backtest AYNI fonksiyonu kullanır."""
530
+ A = ind["_arr"]
531
+ price, hi_i, lo_i = A["c"][i], A["h"][i], A["l"][i]
532
+ trend = int(A["smc_trend"][i])
533
+ votes, tags = [], []
534
+ long_refs, short_refs = [], [] # stop yerleşimi için SMC referans seviyeleri
535
+
536
+ def vote(label, d, text, tag=None):
537
+ votes.append((label, int(d), text))
538
+ if tag:
539
+ tags.append((tag, int(d)))
540
+
541
+ # --- 1) taze BOS / CHoCH ---
542
+ bi = A["smc_brk_i"][i]
543
+ if bi >= 0 and i - bi <= SMC_FRESH_BARS:
544
+ d = int(A["smc_brk_dir"][i])
545
+ name = "CHoCH" if A["smc_brk_choch"][i] > 0 else "BOS"
546
+ arrow = "▲" if d > 0 else "▼"
547
+ age = int(i - bi)
548
+ when = "bu barda" if age == 0 else f"{age} bar önce"
549
+ kind = "trend dönüşü" if name == "CHoCH" else "trend devamı"
550
+ vote(name, d, f"{name} {arrow}: piyasa yapısı {when} {'yukarı' if d > 0 else 'aşağı'} kırıldı ({kind})",
551
+ f"{name} {arrow}")
552
+
553
+ # --- 2) Premium / Discount ---
554
+ sh, sl = A["smc_sh"][i], A["smc_sl"][i]
555
+ pd_pos = None
556
+ if sh == sh and sl == sl and sh > sl:
557
+ pd_pos = float((price - sl) / (sh - sl))
558
+ if pd_pos <= 0.5 - SMC_PD_BAND:
559
+ vote("Discount", 1, f"Fiyat swing aralığının DISCOUNT bölgesinde (%{pd_pos * 100:.0f}) — alım için ucuz bölge", "DISCOUNT")
560
+ elif pd_pos >= 0.5 + SMC_PD_BAND:
561
+ vote("Premium", -1, f"Fiyat swing aralığının PREMIUM bölgesinde (%{pd_pos * 100:.0f}) — satım için pahalı bölge", "PREMIUM")
562
+ else:
563
+ vote("Denge", 0, f"Fiyat equilibrium (%50) civarında (%{pd_pos * 100:.0f}) — bölge avantajı yok")
564
+
565
+ # --- 3) Order Block ---
566
+ bl, bh = A["smc_obb_lo"][i], A["smc_obb_hi"][i]
567
+ if bl == bl and lo_i <= bh and price >= bl:
568
+ vote("Order Block ▲", 1, f"Fiyat aktif talep (bullish) ORDER BLOCK bölgesinde ({fmt_price(bl)} – {fmt_price(bh)})", "OB ▲")
569
+ long_refs.append(float(bl))
570
+ rl, rh = A["smc_obs_lo"][i], A["smc_obs_hi"][i]
571
+ if rl == rl and hi_i >= rl and price <= rh:
572
+ vote("Order Block ▼", -1, f"Fiyat aktif arz (bearish) ORDER BLOCK bölgesinde ({fmt_price(rl)} – {fmt_price(rh)})", "OB ▼")
573
+ short_refs.append(float(rh))
574
+
575
+ # --- 4) Fair Value Gap retest ---
576
+ if A["smc_fvgb"][i] > 0:
577
+ vote("FVG ▲", 1, "Fiyat açık yükseliş FVG'sine (dengesizlik boşluğu) geri döndü — destek bölgesi", "FVG ▲")
578
+ if A["smc_fvgs"][i] > 0:
579
+ vote("FVG ▼", -1, "Fiyat açık düşüş FVG'sine (dengesizlik boşluğu) geri döndü — direnç bölgesi", "FVG ▼")
580
+
581
+ # --- 5) Likidite avı ---
582
+ sb, ss = A["smc_swb_i"][i], A["smc_sws_i"][i]
583
+ if sb >= 0 and i - sb <= SMC_SWEEP_BARS:
584
+ vote("Likidite Avı ▲", 1, f"Sell-side likidite alındı ({int(i - sb)} bar önce): dip fitille süpürüldü, fiyat geri döndü", "SWEEP ▲")
585
+ long_refs.append(float(A["smc_swb_px"][i]))
586
+ if ss >= 0 and i - ss <= SMC_SWEEP_BARS:
587
+ vote("Likidite Avı ▼", -1, f"Buy-side likidite alındı ({int(i - ss)} bar önce): tepe fitille süpürüldü, fiyat geri döndü", "SWEEP ▼")
588
+ short_refs.append(float(A["smc_sws_px"][i]))
589
+
590
+ # --- net teyit + yapı filtresi ---
591
+ net = sum(d for _, d, _ in votes)
592
+ bull_w = float(sum(1 for _, d, _ in votes if d > 0))
593
+ bear_w = float(sum(1 for _, d, _ in votes if d < 0))
594
+ gate = None
595
+ if trend == 0:
596
+ score = 0.0
597
+ gate = "Piyasa yapısı henüz belirsiz (onaylı swing kırılımı yok) — SMC sinyali üretilmez"
598
+ elif (net > 0 and trend < 0) or (net < 0 and trend > 0):
599
+ score = 0.0
600
+ gate = (f"Teyitler {'yükseliş' if net > 0 else 'düşüş'} yönlü ama piyasa yapısı "
601
+ f"{'düşüş' if trend < 0 else 'yükseliş'} — yapı dönmeden (CHoCH) işlem yok")
602
+ else:
603
+ score = float(net)
604
+
605
+ regime = "YÜKSELİŞ" if trend > 0 else ("DÜŞÜŞ" if trend < 0 else "BELİRSİZ")
606
+ out = {
607
+ "score": score, "price": price, "regime": regime, "trend": trend, "net": net,
608
+ "bull_w": bull_w, "bear_w": bear_w, "atr_rank": A["atr_rank"][i], "pd": pd_pos, "tags": tags,
609
+ "gate": gate, "heat": A["vp_heat"][i],
610
+ "stop_ref": {"long": min(long_refs) if long_refs else None, "short": max(short_refs) if short_refs else None},
611
+ }
612
+ if verbose:
613
+ def tone(d):
614
+ return "up" if d > 0 else ("down" if d < 0 else "flat")
615
+ bars_txt = ""
616
+ if bi >= 0:
617
+ bars_txt = f" — son kırılım {int(i - bi)} bar önce ({'CHoCH' if A['smc_brk_choch'][i] > 0 else 'BOS'} {'▲' if A['smc_brk_dir'][i] > 0 else '▼'})"
618
+ struct_txt = {1: "Piyasa yapısı YÜKSELİŞ (HH/HL) — yalnızca long aranır",
619
+ -1: "Piyasa yapısı DÜŞÜŞ (LH/LL) — yalnızca short aranır",
620
+ 0: "Piyasa yapısı belirsiz"}[trend] + (bars_txt if trend else "")
621
+ reasons = [(tone(trend), struct_txt)]
622
+ reasons += [(tone(d), text) for _, d, text in votes]
623
+ if gate:
624
+ reasons.append(("flat", gate))
625
+ out["reasons"] = reasons
626
+ out["contribs"] = [(lab, float(d)) for lab, d, _ in votes if d != 0]
627
+ return out
628
+
629
+
630
+ def compute_score_series(ind, n):
631
+ """Son BACKTEST_LOOKBACK bar için (canlı bar dahil) skor serisi. Dönüş: (başlangıç_indeksi, [skor_dict,...])"""
632
+ start = min(max(WARMUP_BARS, n - BACKTEST_LOOKBACK), n - 1)
633
+ return start, [_score_at(ind, i) for i in range(start, n)]
634
+
635
+
636
+ # ---------------------------------------------------------------------------
637
+ # BACKTEST — yön doğruluğu + gerçek işlem simülasyonu (SL/TP/zaman çıkışı + komisyon)
638
+ # ---------------------------------------------------------------------------
639
+ def directional_accuracy(df, start, scores, forward=5, threshold=SIGNAL_THRESHOLD):
640
+ """Klasik test: sinyal 'forward' bar sonra doğru yönü göstermiş mi? (long / short kırılımlı)"""
641
+ c = df["c"].to_numpy()
642
+ n = len(c)
643
+ stats = {"all": [0, 0], "long": [0, 0], "short": [0, 0]}
644
+ for i in range(start, n - forward):
645
+ s = scores[i - start]
646
+ if abs(s["score"]) < threshold:
647
+ continue
648
+ up = s["score"] > 0
649
+ correct = up == (c[i + forward] > c[i])
650
+ for key in ("all", "long" if up else "short"):
651
+ stats[key][1] += 1
652
+ if correct:
653
+ stats[key][0] += 1
654
+ if stats["all"][1] < 5:
655
+ return None
656
+
657
+ def pack(key):
658
+ ok, t = stats[key]
659
+ return {"accuracy": ok / t, "n_signals": t} if t > 0 else None
660
+
661
+ return {
662
+ "accuracy": stats["all"][0] / stats["all"][1], "correct": stats["all"][0],
663
+ "n_signals": stats["all"][1], "forward_bars": forward,
664
+ "long_side": pack("long"), "short_side": pack("short"),
665
+ }
666
+
667
+
668
+ def simulate_trades(df, ind, start, scores, threshold=SIGNAL_THRESHOLD):
669
+ """Sinyalleri sırayla (üst üste binmeden) işleme çevirir.
670
+ Giriş: sinyal barını izleyen barın AÇILIŞI (gerçekçi, ileri sızıntı yok).
671
+ Çıkış: SL (1.5×ATR) / TP (2.5×ATR) / 24 bar zaman çıkışı. Aynı barda ikisi de değerse SL öncelikli (kötümser)."""
672
+ A = ind["_arr"]
673
+ o, h, l, c, atr_a = A["o"], A["h"], A["l"], A["c"], A["atr"]
674
+ n = len(c)
675
+ trades = []
676
+ i = start
677
+ while i <= n - 2:
678
+ s = scores[i - start]["score"]
679
+ a = atr_a[i]
680
+ if abs(s) < threshold or not (a > 0):
681
+ i += 1
682
+ continue
683
+ d = 1 if s > 0 else -1
684
+ entry = o[i + 1]
685
+ risk = SL_ATR * a
686
+ sl, tp = entry - d * risk, entry + d * TP_ATR * a
687
+ last = min(n - 1, i + 1 + MAX_HOLD_BARS)
688
+ exit_i, exit_px, why = None, None, None
689
+ for j in range(i + 1, last + 1):
690
+ hit_sl = (l[j] <= sl) if d > 0 else (h[j] >= sl)
691
+ hit_tp = (h[j] >= tp) if d > 0 else (l[j] <= tp)
692
+ if hit_sl:
693
+ exit_i, exit_px, why = j, sl, "SL"
694
+ break
695
+ if hit_tp:
696
+ exit_i, exit_px, why = j, tp, "TP"
697
+ break
698
+ if exit_i is None:
699
+ if last == i + 1 + MAX_HOLD_BARS:
700
+ exit_i, exit_px, why = last, c[last], "ZAMAN"
701
+ else:
702
+ break # veri bitti, işlem hâlâ açık — sonuçlandırılamaz
703
+ pnl = d * (exit_px - entry) / entry - 2 * FEE_PER_SIDE
704
+ r_mult = pnl / (risk / entry)
705
+ trades.append({"signal_i": i, "entry_i": i + 1, "exit_i": exit_i, "dir": d, "entry": entry,
706
+ "exit": exit_px, "pnl": pnl, "r": r_mult, "why": why, "score": s})
707
+ i = exit_i
708
+ return trades
709
+
710
+
711
+ def summarize_trades(trades, df, start):
712
+ if len(trades) < 3:
713
+ return None
714
+ n = len(df)
715
+ pnl = np.array([t["pnl"] for t in trades])
716
+ r = np.array([t["r"] for t in trades])
717
+ wins, losses = pnl[pnl > 0], pnl[pnl <= 0]
718
+ gross_loss = abs(losses.sum())
719
+ pf = float(wins.sum() / gross_loss) if gross_loss > 0 else (99.0 if wins.sum() > 0 else 0.0)
720
+ win_rate = len(wins) / len(pnl)
721
+ win_r, loss_r = r[r > 0], r[r <= 0]
722
+ avg_win_r = float(win_r.mean()) if len(win_r) else 0.0
723
+ avg_loss_r = float(abs(loss_r.mean())) if len(loss_r) else 0.0
724
+ kelly = None
725
+ if avg_win_r > 0 and avg_loss_r > 0:
726
+ kelly = win_rate - (1 - win_rate) / (avg_win_r / avg_loss_r)
727
+ sqn = float(np.sqrt(min(len(r), 100)) * r.mean() / r.std()) if r.std() > 0 else 0.0
728
+
729
+ mult = np.ones(n - start)
730
+ for t in trades:
731
+ mult[t["exit_i"] - start] *= (1 + t["pnl"])
732
+ equity = np.cumprod(mult)
733
+ peak = np.maximum.accumulate(equity)
734
+ max_dd = float(((equity - peak) / peak).min())
735
+ c = df["c"].to_numpy()
736
+ bh = c[start:] / c[start]
737
+
738
+ streak = best = 0
739
+ for p in pnl:
740
+ streak = streak + 1 if p <= 0 else 0
741
+ best = max(best, streak)
742
+
743
+ def side(dirn):
744
+ sub = [t for t in trades if t["dir"] == dirn]
745
+ return {"n": len(sub), "win_rate": (sum(1 for t in sub if t["pnl"] > 0) / len(sub)) if sub else None}
746
+
747
+ stats = {
748
+ "n": len(trades), "win_rate": win_rate, "profit_factor": pf, "expectancy": float(pnl.mean()),
749
+ "avg_r": float(r.mean()), "sqn": sqn, "max_dd": max_dd, "total_return": float(equity[-1] - 1),
750
+ "bh_return": float(bh[-1] - 1), "max_consec_loss": best, "avg_win_r": avg_win_r,
751
+ "avg_loss_r": avg_loss_r, "kelly": kelly, "long": side(1), "short": side(-1),
752
+ }
753
+ return stats, equity, bh
754
+
755
+
756
+ def run_backtest(df, ind, start, scores):
757
+ n = len(df)
758
+ if n - start < 20:
759
+ return None
760
+ acc = directional_accuracy(df, start, scores)
761
+ trades = simulate_trades(df, ind, start, scores)
762
+ summ = summarize_trades(trades, df, start)
763
+ bt = {"start": start, "acc": acc, "trades": trades, "stats": None, "equity": None, "bh": None}
764
+ if summ:
765
+ bt["stats"], bt["equity"], bt["bh"] = summ
766
+ return bt
767
+
768
+
769
+ # ---------------------------------------------------------------------------
770
+ # CANLI KATMAN — üst zaman dilimleri, emir defteri, funding (backtest EDİLMEZ)
771
+ # ---------------------------------------------------------------------------
772
+ def _mtf_score(symbol, bar, closed_only):
773
+ hdf = fetch_candles(symbol, bar, 300)
774
+ if closed_only:
775
+ hdf = hdf[hdf["confirm"] == 1].reset_index(drop=True)
776
+ hind = compute_indicators(hdf)
777
+ r = _score_at(hind, len(hdf) - 1)
778
+ return {"bar": bar, "score": r["score"], "trend": r["trend"]}
779
+
780
+
781
+ def _fetch_live_extras(symbol, bar, mtf, micro, closed_only):
782
+ """MTF / emir defteri / funding çağrılarını PARALEL yapar; hata veren parça sessizce atlanır."""
783
+ tasks = {}
784
+ with ThreadPoolExecutor(max_workers=4) as ex:
785
+ if mtf and symbol and bar:
786
+ b1 = MTF_MAP.get(bar)
787
+ if b1 and b1 != bar:
788
+ tasks["mtf1"] = ex.submit(_mtf_score, symbol, b1, closed_only)
789
+ b2 = MTF_MAP.get(b1)
790
+ if b2 and b2 not in (b1, bar):
791
+ tasks["mtf2"] = ex.submit(_mtf_score, symbol, b2, closed_only)
792
+ if micro and symbol:
793
+ tasks["ob"] = ex.submit(fetch_orderbook, symbol, 20)
794
+ tasks["fund"] = ex.submit(fetch_funding_rate, symbol)
795
+ out = {}
796
+ for k, f in tasks.items():
797
+ try:
798
+ out[k] = f.result()
799
+ except Exception:
800
+ out[k] = None
801
+ return out
802
+
803
+
804
+ def build_trade_plan(price, direction, atr_v, supports, resists, acct=None, risk_pct=None, smc_level=None):
805
+ """Stop önceliği: 1) SMC seviyesi (order block / likidite avı ekstremi arkası), 2) yapısal S/R arkası, 3) ATR stop.
806
+ Aday stop, fiyata 0.8–3.0 ATR uzaklıktaysa kullanılır. 3 kademeli hedef + hesap büyüklüğüne göre pozisyon boyutu."""
807
+ d = direction
808
+ stop, stop_type = price - d * SL_ATR * atr_v, f"ATR × {SL_ATR}"
809
+ cands = []
810
+ if smc_level is not None:
811
+ cands.append((smc_level - d * 0.25 * atr_v, "SMC (OB / likidite arkası)"))
812
+ if d > 0 and supports:
813
+ cands.append((supports[0][0] - 0.25 * atr_v, "Yapısal (S/R arkası)"))
814
+ elif d < 0 and resists:
815
+ cands.append((resists[0][0] + 0.25 * atr_v, "Yapısal (S/R arkası)"))
816
+ for cand, ctype in cands:
817
+ dist = abs(price - cand)
818
+ if 0.8 * atr_v <= dist <= 3.0 * atr_v and (cand < price if d > 0 else cand > price):
819
+ stop, stop_type = cand, ctype
820
+ break
821
+ risk = abs(price - stop)
822
+ plan = {
823
+ "direction": "LONG" if d > 0 else "SHORT", "entry": price, "stop_loss": stop, "stop_type": stop_type,
824
+ "risk_dist": risk, "risk_frac": risk / price,
825
+ "targets": [(price + d * risk * m, m) for m in TP_R_MULTIPLES], "sizing": None,
826
+ }
827
+ if acct and acct > 0 and risk_pct and risk_pct > 0 and risk > 0:
828
+ risk_amt = acct * risk_pct / 100.0
829
+ qty = risk_amt / risk
830
+ notional = qty * price
831
+ plan["sizing"] = {"risk_amount": risk_amt, "qty": qty, "notional": notional, "leverage": notional / acct}
832
+ return plan
833
+
834
+
835
+ def compute_confidence(score, agree, mtf_state, hist_q, vol_q):
836
+ """Güven = bileşenlerin EŞİT ağırlıklı ortalaması (bileşen ağırlığı yok)."""
837
+ mag = min(abs(score) / 5.0, 1.0)
838
+ parts = {"Teyit Sayısı": mag, "Teyit Uyumu": agree, "MTF Yapı Teyidi": mtf_state,
839
+ "Tarihsel Kenar": hist_q, "Volatilite Rejimi": vol_q}
840
+ conf = 100.0 * sum(parts.values()) / len(parts)
841
+ return min(conf, 100.0), parts
842
+
843
+
844
+ def grade_from_conf(conf):
845
+ for thr, g, col in ((75, "A+", "#3ddc97"), (62, "A", "#7fe6b8"), (50, "B", "#f2b84b"), (38, "C", "#ff9aa8")):
846
+ if conf >= thr:
847
+ return g, col
848
+ return "D", "#ff5c72"
849
+
850
+
851
+ def generate_signal(df, symbol=None, bar=None, mtf=True, micro=True, closed_only=False,
852
+ acct=None, risk_pct=None):
853
+ ind = compute_indicators(df)
854
+ n = len(df)
855
+ start, scores = compute_score_series(ind, n)
856
+ base = _score_at(ind, n - 1, verbose=True)
857
+ score, price = base["score"], base["price"]
858
+ reasons, contribs = list(base["reasons"]), list(base["contribs"])
859
+ up_c, dn_c = "#3ddc97", "#ff5c72"
860
+ tags = []
861
+ if base["trend"] != 0:
862
+ tags.append(("YAPI ▲" if base["trend"] > 0 else "YAPI ▼", up_c if base["trend"] > 0 else dn_c))
863
+ for t, d in base["tags"]:
864
+ tags.append((t, up_c if d > 0 else dn_c))
865
+
866
+ def note(text):
867
+ """Bilgi notu — skoru DEĞİŞTİRMEZ (ağırlık / ek puan yok)."""
868
+ reasons.append(("flat", text))
869
+
870
+ # --- Canlı ekstralar (paralel çekim) ---
871
+ extras = _fetch_live_extras(symbol, bar, mtf, micro, closed_only)
872
+ sgn = 1 if score > 0 else (-1 if score < 0 else 0)
873
+
874
+ # --- 2 kademeli MTF yapı teyidi (üst zaman diliminin piyasa yapısı sinyal yönüyle uyumlu mu?) ---
875
+ mtf_info, align_flags = [], []
876
+ for key in ("mtf1", "mtf2"):
877
+ m = extras.get(key)
878
+ if not m:
879
+ continue
880
+ aligned = None
881
+ if sgn != 0 and m["trend"] != 0:
882
+ aligned = bool(m["trend"] == sgn)
883
+ align_flags.append(aligned)
884
+ nm = "yükseliş" if m["trend"] > 0 else "düşüş"
885
+ note(f"{m['bar']} piyasa yapısı {nm} — sinyalle "
886
+ + ("uyumlu ✓" if aligned else "ÇELİŞKİLİ ⚠ (GÜÇLÜ sinyal verilmez)"))
887
+ mtf_info.append({"bar": m["bar"], "score": round(m["score"], 2), "trend": m["trend"], "aligned": aligned})
888
+ mtf_state = (sum(1.0 for a in align_flags if a) / len(align_flags)) if align_flags else 0.5
889
+ mtf_opposed = any(a is False for a in align_flags)
890
+
891
+ # --- Emir defteri dengesizliği (bilgi) ---
892
+ imb = None
893
+ ob = extras.get("ob")
894
+ if ob and ob.get("bids") and ob.get("asks"):
895
+ bw = sum(float(x[1]) / (k + 1) for k, x in enumerate(ob["bids"]))
896
+ aw = sum(float(x[1]) / (k + 1) for k, x in enumerate(ob["asks"]))
897
+ if bw + aw > 0:
898
+ imb = (bw - aw) / (bw + aw)
899
+ if abs(imb) >= 0.2:
900
+ note(f"Emir defterinde {'alış' if imb > 0 else 'satış'} baskısı (dengesizlik {imb * 100:+.0f}%)")
901
+
902
+ # --- Likidite duvarları (bilgi) ---
903
+ walls = None
904
+ atr_w = float(ind["atr"].iloc[-1])
905
+ if ob and ob.get("bids") and ob.get("asks") and atr_w > 0:
906
+ sup_w, res_w = book_walls(ob, price, atr_w)
907
+ walls = {"support": sup_w, "resist": res_w}
908
+ if sup_w:
909
+ note(f"Yakında büyük alış duvarı {fmt_price(sup_w[0])} (medyanın {sup_w[1]:.1f}×'i) — destek olabilir")
910
+ if res_w:
911
+ note(f"Yakında büyük satış duvarı {fmt_price(res_w[0])} (medyanın {res_w[1]:.1f}×'i) — direnç olabilir")
912
+
913
+ # --- Funding rate (bilgi) ---
914
+ fr = extras.get("fund")
915
+ if fr is not None and abs(fr) > 0.0002:
916
+ side = "long" if fr > 0 else "short"
917
+ note(f"Funding {fr * 100:+.3f}% — {side} tarafı kalabalık ({'tasfiye riski' if fr > 0 else 'sıkışma potansiyeli'})")
918
+
919
+ # --- Destek / direnç (bilgi + stop yerleşimi) ---
920
+ atr_v = float(ind["atr"].iloc[-1])
921
+ supports, resists = find_sr_levels(df, atr_v)
922
+ if atr_v > 0 and sgn > 0 and resists and resists[0][1] >= 2 and (resists[0][0] - price) / atr_v <= 1.0:
923
+ lv, tc = resists[0]
924
+ note(f"Dikkat: yakın direnç {fmt_price(lv)} ({tc} temas, {(lv - price) / atr_v:.1f} ATR) — long hedeflerini sınırlayabilir")
925
+ elif atr_v > 0 and sgn < 0 and supports and supports[0][1] >= 2 and (price - supports[0][0]) / atr_v <= 1.0:
926
+ lv, tc = supports[0]
927
+ note(f"Dikkat: yakın destek {fmt_price(lv)} ({tc} temas, {(price - lv) / atr_v:.1f} ATR) — short hedeflerini sınırlayabilir")
928
+
929
+ # --- Konsensüs + sınıflandırma ---
930
+ direction = 1 if score > 0 else -1
931
+ tot = base["bull_w"] + base["bear_w"]
932
+ agree = ((base["bull_w"] if direction > 0 else base["bear_w"]) / tot) if tot > 0 else 0.5
933
+ if abs(score) >= STRONG_THRESHOLD and agree >= 0.65 and not mtf_opposed:
934
+ signal, color = ("GÜÇLÜ AL", "#3ddc97") if score > 0 else ("GÜÇLÜ SAT", "#ff5c72")
935
+ elif abs(score) >= SIGNAL_THRESHOLD:
936
+ signal, color = ("AL", "#7fe6b8") if score > 0 else ("SAT", "#ff9aa8")
937
+ if abs(score) >= STRONG_THRESHOLD:
938
+ note(f"Teyit sayısı güçlü bölgede ancak uyum (%{agree * 100:.0f}) veya MTF yapı filtresini geçemedi — GÜÇLÜ yerine normal sinyal verildi")
939
+ else:
940
+ signal, color = "BEKLE / NÖTR", "#f2b84b"
941
+ is_neutral = signal == "BEKLE / NÖTR"
942
+
943
+ # --- Tarihsel doğrulama ---
944
+ backtest = run_backtest(df, ind, start, scores)
945
+ hist_q = 0.4
946
+ if backtest:
947
+ qs = []
948
+ acc = backtest["acc"]
949
+ if acc:
950
+ p_shrunk = (acc["correct"] + 5) / (acc["n_signals"] + 10) # Bayes büzülmesi (önsel %50)
951
+ qs.append(min(max((p_shrunk - 0.45) / 0.20, 0.0), 1.0))
952
+ st = backtest["stats"]
953
+ if st:
954
+ qs.append(min(max((st["profit_factor"] - 0.8) / 0.8, 0.0), 1.0))
955
+ if qs:
956
+ hist_q = float(np.mean(qs))
957
+ rank = base["atr_rank"]
958
+ vol_q = 1.0 if 0.10 <= rank <= 0.90 else 0.5
959
+ if rank > 0.90:
960
+ note(f"Volatilite aşırı yüksek (ATR yüzdeliği %{rank * 100:.0f}) — stoplar geniş tutulmalı, güven düşürüldü")
961
+ conf, conf_parts = compute_confidence(score, agree, mtf_state, hist_q, vol_q)
962
+ grade, grade_color = ("—", "#7c8797") if is_neutral else grade_from_conf(conf)
963
+
964
+ plan = None
965
+ if not is_neutral and atr_v > 0:
966
+ ref = base["stop_ref"]["long" if direction > 0 else "short"]
967
+ plan = build_trade_plan(price, direction, atr_v, supports, resists, acct, risk_pct, smc_level=ref)
968
+
969
+ return {
970
+ "signal": signal, "score": round(score, 2), "confidence": round(conf, 1), "grade": grade,
971
+ "grade_color": grade_color, "color": color, "reasons": reasons, "contribs": contribs,
972
+ "indicators": ind, "price": price, "mtf": mtf_info, "plan": plan,
973
+ "backtest": backtest, "regime": base["regime"], "trend": base["trend"], "pd": base["pd"],
974
+ "atr_rank": rank, "agree": agree, "bull_w": base["bull_w"], "bear_w": base["bear_w"],
975
+ "conf_parts": conf_parts, "tags": tags, "funding": fr, "imbalance": imb,
976
+ "supports": supports, "resists": resists, "atr": atr_v, "walls": walls,
977
+ "smc": ind["smc_zones"],
978
+ "vp": {"poc": float(ind["vp_poc"].iloc[-1]), "val": float(ind["vp_val"].iloc[-1]),
979
+ "vah": float(ind["vp_vah"].iloc[-1]), "heat": float(ind["vp_heat"].iloc[-1])},
980
+ }
981
+
982
+
983
+ # ---------------------------------------------------------------------------
984
+ # RISK ANALYTICS
985
+ # ---------------------------------------------------------------------------
986
+
987
+
988
+
989
+
990
+
991
+
992
+
993
+
994
+
995
+
996
+
997
+
998
+
999
+
1000
+
1001
+
1002
+
1003
+
1004
+ def risk_metrics(df, bar):
1005
+ ret = df["c"].pct_change().dropna()
1006
+ if len(ret) < 5:
1007
+ return None
1008
+ periods = BAR_TO_PERIODS_PER_YEAR.get(bar, 365)
1009
+ vol_annual = ret.std() * np.sqrt(periods)
1010
+ mean_annual = ret.mean() * periods
1011
+ sharpe = mean_annual / vol_annual if vol_annual > 0 else 0.0
1012
+ cum = (1 + ret).cumprod()
1013
+ peak = cum.cummax()
1014
+ dd = (cum - peak) / peak
1015
+ var95 = np.percentile(ret, 5)
1016
+ tail = ret[ret <= var95]
1017
+ cvar95 = tail.mean() if len(tail) else var95
1018
+ return {
1019
+ "volatility_annual": vol_annual, "return_annual": mean_annual, "sharpe": sharpe,
1020
+ "max_drawdown": dd.min(), "current_dd": dd.iloc[-1],
1021
+ "var95_bar": var95, "cvar95_bar": cvar95,
1022
+ }
1023
+
1024
+
1025
+ # ---------------------------------------------------------------------------
1026
+ # FORMAT HELPERS
1027
+ # ---------------------------------------------------------------------------
1028
+ def _isnan(v):
1029
+ return v is None or (isinstance(v, float) and math.isnan(v))
1030
+
1031
+
1032
+ def fmt_price(v):
1033
+ if _isnan(v):
1034
+ return "—"
1035
+ av = abs(v)
1036
+ if av >= 1000:
1037
+ return f"{v:,.2f}"
1038
+ if av >= 1:
1039
+ return f"{v:,.4f}"
1040
+ if av >= 0.01:
1041
+ return f"{v:.6f}"
1042
+ return f"{v:.8f}"
1043
+
1044
+
1045
+ def fmt_pct(v):
1046
+ if _isnan(v):
1047
+ return "—"
1048
+ sign = "+" if v > 0 else ""
1049
+ return f"{sign}{v * 100:.2f}%"
1050
+
1051
+
1052
+ def fmt_large(v):
1053
+ if _isnan(v):
1054
+ return "—"
1055
+ av = abs(v)
1056
+ if av >= 1e9:
1057
+ return f"{v / 1e9:.2f}B"
1058
+ if av >= 1e6:
1059
+ return f"{v / 1e6:.2f}M"
1060
+ if av >= 1e3:
1061
+ return f"{v / 1e3:.2f}K"
1062
+ return f"{v:.2f}"
1063
+
1064
+
1065
+ def metric_card(label, value, sub="", tone="neutral"):
1066
+ tone_class = {"up": "mc-up", "down": "mc-down", "neutral": "mc-neutral"}.get(tone, "mc-neutral")
1067
+ sub_html = f'<div class="mc-sub">{sub}</div>' if sub else ""
1068
+ return (f'<div class="metric-card {tone_class}"><div class="mc-label">{label}</div>'
1069
+ f'<div class="mc-value">{value}</div>{sub_html}</div>')
1070
+
1071
+
1072
+ def section(title):
1073
+ return f'<div class="section-title">{title}</div>'
1074
+
1075
+
1076
+ def cards_row(cards):
1077
+ return f'<div class="metric-row">{"".join(cards)}</div>' if cards else ""
1078
+
1079
+
1080
+ def error_box(msg):
1081
+ return f'<div class="error-box">⚠ {msg}</div>'
1082
+
1083
+
1084
+ # ---------------------------------------------------------------------------
1085
+ # CHART BUILDERS
1086
+ # ---------------------------------------------------------------------------
1087
+ def _base_layout(fig, title, height):
1088
+ fig.update_layout(
1089
+ template="plotly_dark", height=height, margin=dict(l=10, r=10, t=40, b=10),
1090
+ paper_bgcolor=CHART_BG, plot_bgcolor=CHART_BG, title=title,
1091
+ legend=dict(orientation="h", y=1.05, font=dict(size=10)),
1092
+ font=dict(family="IBM Plex Mono, monospace", size=11),
1093
+ )
1094
+ return fig
1095
+
1096
+
1097
+ def _empty_fig(msg, title="", height=220):
1098
+ fig = go.Figure()
1099
+ fig.add_annotation(text=msg, x=0.5, y=0.5, xref="paper", yref="paper", showarrow=False,
1100
+ font=dict(size=13, color="#7c8797"))
1101
+ fig.update_xaxes(visible=False)
1102
+ fig.update_yaxes(visible=False)
1103
+ return _base_layout(fig, title, height)
1104
+
1105
+
1106
+ # ---------------------------------------------------------------------------
1107
+ # 3D GRAFİK YARDIMCILARI (x = zaman/bar, y = fiyat, z = hacim yoğunluğu / katman yüksekliği)
1108
+ # ---------------------------------------------------------------------------
1109
+ _CUBE_I = [7, 0, 0, 0, 4, 4, 6, 6, 4, 0, 3, 2]
1110
+ _CUBE_J = [3, 4, 1, 2, 5, 6, 5, 2, 0, 1, 6, 3]
1111
+ _CUBE_K = [0, 7, 2, 3, 6, 7, 1, 1, 5, 5, 7, 6]
1112
+
1113
+
1114
+ def _ticks3d(df, v0, n, k=6):
1115
+ idx = sorted({int(round(x)) for x in np.linspace(v0, n - 1, k)})
1116
+ return idx, [df["ts"].iloc[i].strftime("%d.%m %H:%M") for i in idx]
1117
+
1118
+
1119
+ def _scene3d(fig, df, v0, n, ylo, yhi, zmax, zlabel, x_pad=12, aspect=(2.8, 1.5, 0.85)):
1120
+ tv, tt = _ticks3d(df, v0, n)
1121
+ base = dict(backgroundcolor=CHART_BG, gridcolor="#232a35", zerolinecolor="#232a35",
1122
+ showbackground=True, color="#7c8797")
1123
+ fig.update_layout(scene=dict(
1124
+ xaxis=dict(base, title="", tickmode="array", tickvals=tv, ticktext=tt, range=[v0, n - 1 + x_pad]),
1125
+ yaxis=dict(base, title="Fiyat", range=[ylo, yhi]),
1126
+ zaxis=dict(base, title=zlabel, range=[0, zmax], showticklabels=False),
1127
+ aspectmode="manual", aspectratio=dict(x=aspect[0], y=aspect[1], z=aspect[2]),
1128
+ camera=dict(eye=dict(x=1.55, y=-1.85, z=0.85)),
1129
+ ))
1130
+
1131
+
1132
+ def _candles3d(fig, df, v0, n, symbol):
1133
+ """Mumlar z=0 zemin düzleminde: fitil ince, gövde kalın çizgi (Scatter3d)."""
1134
+ idx = np.arange(v0, n)
1135
+ o, h, l, c = (df[k].to_numpy(dtype=float)[v0:n] for k in ("o", "h", "l", "c"))
1136
+ eps = max((h.max() - l.min()) * 0.002, 1e-12)
1137
+ for up, col, nm in ((True, "#3ddc97", "Yükseliş mumu"), (False, "#ff5c72", "Düşüş mumu")):
1138
+ m = (c >= o) if up else (c < o)
1139
+ xw, yw, zw, xb, yb, zb = [], [], [], [], [], []
1140
+ for k in np.where(m)[0]:
1141
+ x = float(idx[k])
1142
+ xw += [x, x, None]; yw += [float(l[k]), float(h[k]), None]; zw += [0.0, 0.0, None]
1143
+ top, bot = float(max(o[k], c[k])), float(min(o[k], c[k]))
1144
+ if top - bot < eps:
1145
+ top = bot + eps
1146
+ xb += [x, x, None]; yb += [bot, top, None]; zb += [0.0, 0.0, None]
1147
+ if not xw:
1148
+ continue
1149
+ fig.add_trace(go.Scatter3d(x=xw, y=yw, z=zw, mode="lines", line=dict(color=col, width=2),
1150
+ showlegend=False, hoverinfo="skip"))
1151
+ fig.add_trace(go.Scatter3d(x=xb, y=yb, z=zb, mode="lines", line=dict(color=col, width=7),
1152
+ name=f"{symbol} · {nm}", hoverinfo="skip"))
1153
+
1154
+
1155
+ def _box3d(fig, x0, x1, y0, y1, z0, z1, color, opacity):
1156
+ fig.add_trace(go.Mesh3d(
1157
+ x=[x0, x0, x1, x1, x0, x0, x1, x1], y=[y0, y1, y1, y0, y0, y1, y1, y0], z=[z0, z0, z0, z0, z1, z1, z1, z1],
1158
+ i=_CUBE_I, j=_CUBE_J, k=_CUBE_K, color=color, opacity=opacity, flatshading=True,
1159
+ showlegend=False, hoverinfo="skip"))
1160
+
1161
+
1162
+ def _rect3d(fig, x0, x1, y0, y1, color, opacity, z=0.0):
1163
+ fig.add_trace(go.Mesh3d(x=[x0, x1, x1, x0], y=[y0, y0, y1, y1], z=[z, z, z, z], i=[0, 0], j=[1, 2], k=[2, 3],
1164
+ color=color, opacity=opacity, showlegend=False, hoverinfo="skip"))
1165
+
1166
+
1167
+ def _label3d(fig, x, y, z, text, color, size=9):
1168
+ fig.add_trace(go.Scatter3d(x=[x], y=[y], z=[z], mode="text", text=[text], textposition="middle right",
1169
+ textfont=dict(size=size, color=color), showlegend=False, hoverinfo="skip"))
1170
+
1171
+
1172
+ def _hline3d(fig, x0, x1, y, color, width=2, dash="solid", label=None, z=0.0):
1173
+ fig.add_trace(go.Scatter3d(
1174
+ x=[x0, x1], y=[y, y], z=[z, z], mode="lines+text" if label else "lines",
1175
+ line=dict(color=color, width=width, dash=dash), text=["", label] if label else None,
1176
+ textposition="middle right", textfont=dict(size=9, color=color), showlegend=False, hoverinfo="skip"))
1177
+
1178
+
1179
+ def _volume_surface3d(fig, df, v0, n, opacity=0.40):
1180
+ """Hacim ısı haritası 3D yüzey: yükseklik = o fiyat seviyesindeki hacim yoğunluğu (0..1)."""
1181
+ hh, ll = df["h"].to_numpy(dtype=float)[v0:n], df["l"].to_numpy(dtype=float)[v0:n]
1182
+ edges = np.linspace(ll.min(), hh.max(), 41)
1183
+ z = _volume_grid(hh, ll, df["vol"].to_numpy(dtype=float)[v0:n], edges).T # (dilim × bar)
1184
+ z = z / max(float(z.max()), 1e-12)
1185
+ fig.add_trace(go.Surface(
1186
+ x=np.arange(v0, n), y=(edges[:-1] + edges[1:]) / 2, z=z, name="Hacim Isı Haritası",
1187
+ colorscale=[[0, "#12171f"], [0.35, "#5b8cff"], [0.7, "#f2b84b"], [1, "#ff5c72"]],
1188
+ opacity=opacity, showscale=False, hoverinfo="skip", showlegend=False))
1189
+
1190
+
1191
+ def build_price_chart(df, ind, symbol, bar):
1192
+ """Genel bakış: 3D mum grafiği + EMA/Bollinger çizgileri + arka kenarda 3D hacim sütunları."""
1193
+ n = len(df)
1194
+ v0 = max(0, n - 150)
1195
+ hi, lo = float(df["h"].iloc[v0:].max()), float(df["l"].iloc[v0:].min())
1196
+ pad = (hi - lo) * 0.05 or hi * 0.01
1197
+ ylo, yhi = lo - pad, hi + pad
1198
+ fig = go.Figure()
1199
+ _candles3d(fig, df, v0, n, symbol)
1200
+ xs = list(range(v0, n))
1201
+ for key, nm, col, w, dash in (("ema20", "EMA20", "#5b8cff", 3, "solid"), ("ema50", "EMA50", "#f2b84b", 3, "solid"),
1202
+ ("bb_upper", "BB Üst", "rgba(150,160,255,0.6)", 2, "dot"),
1203
+ ("bb_lower", "BB Alt", "rgba(150,160,255,0.6)", 2, "dot")):
1204
+ ys = ind[key].iloc[v0:].astype(float).tolist()
1205
+ fig.add_trace(go.Scatter3d(x=xs, y=ys, z=[0.0] * len(xs), mode="lines", name=nm,
1206
+ line=dict(color=col, width=w, dash=dash), hoverinfo="skip"))
1207
+ vol = df["vol"].to_numpy(dtype=float)[v0:n]
1208
+ vmax = max(float(vol.max()), 1e-12)
1209
+ up = (df["c"].to_numpy()[v0:n] >= df["o"].to_numpy()[v0:n])
1210
+ for flag, col, nm in ((True, "#3ddc97", "Hacim ▲"), (False, "#ff5c72", "Hacim ▼")):
1211
+ xv, yv, zv = [], [], []
1212
+ for k in np.where(up == flag)[0]:
1213
+ xv += [float(v0 + k), float(v0 + k), None]
1214
+ yv += [ylo, ylo, None]
1215
+ zv += [0.0, float(vol[k] / vmax * 0.7), None]
1216
+ if xv:
1217
+ fig.add_trace(go.Scatter3d(x=xv, y=yv, z=zv, mode="lines", name=nm, line=dict(color=col, width=5),
1218
+ hoverinfo="skip"))
1219
+ _scene3d(fig, df, v0, n, ylo, yhi, 0.8, "Hacim", aspect=(2.8, 1.5, 0.7))
1220
+ return _base_layout(fig, f"{symbol} · {bar} · 3D Fiyat & Hacim", 560)
1221
+
1222
+
1223
+ def build_signal_price_chart(df, ind, symbol, bar, res):
1224
+ """3D SMC sinyal haritası. x = zaman, y = fiyat, z = hacim yoğunluğu.
1225
+ Zemin: mumlar + BOS/CHoCH çizgileri + seviyeler. Yükselen katmanlar: hacim ısı dağı, order block / FVG kutuları.
1226
+ Fare ile döndür / yakınlaştır."""
1227
+ n = len(df)
1228
+ v0 = max(0, n - 150)
1229
+ x_end = n - 1 + 10
1230
+ hi, lo = float(df["h"].iloc[v0:].max()), float(df["l"].iloc[v0:].min())
1231
+ plan = res["plan"]
1232
+ if plan:
1233
+ ext = [plan["stop_loss"]] + [px for px, _ in plan["targets"]]
1234
+ hi, lo = max(hi, max(ext)), min(lo, min(ext))
1235
+ pad = (hi - lo) * 0.05 or hi * 0.01
1236
+ ylo, yhi = lo - pad, hi + pad
1237
+ inr = lambda y: y is not None and y == y and ylo <= y <= yhi
1238
+ fig = go.Figure()
1239
+
1240
+ try:
1241
+ _volume_surface3d(fig, df, v0, n)
1242
+ except Exception:
1243
+ pass
1244
+ _candles3d(fig, df, v0, n, symbol)
1245
+
1246
+ zones = res.get("smc") or {}
1247
+ A_ = ind["_arr"]
1248
+ # Premium / Discount zemin bölgeleri + equilibrium
1249
+ sh_v, sl_v = float(A_["smc_sh"][-1]), float(A_["smc_sl"][-1])
1250
+ if sh_v == sh_v and sl_v == sl_v and sh_v > sl_v:
1251
+ eq = (sh_v + sl_v) / 2
1252
+ x_pd = max(v0, n - 120)
1253
+ top_, bot_ = min(sh_v, yhi), max(sl_v, ylo)
1254
+ if top_ > eq:
1255
+ _rect3d(fig, x_pd, x_end, max(eq, ylo), top_, "#ff5c72", 0.10)
1256
+ if eq > bot_:
1257
+ _rect3d(fig, x_pd, x_end, bot_, min(eq, yhi), "#3ddc97", 0.10)
1258
+ if inr(eq):
1259
+ _hline3d(fig, x_pd, x_end, eq, "rgba(230,235,242,0.6)", 2, "dash", f"EQ {fmt_price(eq)}")
1260
+
1261
+ # Order Block kutuları (zeminden yükselir)
1262
+ for key, col, nm in (("bull_ob", "#3ddc97", "OB ▲"), ("bear_ob", "#ff5c72", "OB ▼")):
1263
+ z_ = zones.get(key)
1264
+ if z_:
1265
+ x0_ = max(v0, z_[3])
1266
+ _box3d(fig, x0_, x_end, z_[0], z_[1], 0.0, 0.30, col, 0.45)
1267
+ _label3d(fig, x_end, (z_[0] + z_[1]) / 2, 0.15, nm, col)
1268
+ # Fair Value Gap kutuları (daha alçak)
1269
+ for key, col, nm in (("bull_fvg", "#3ddc97", "FVG ▲"), ("bear_fvg", "#ff5c72", "FVG ▼")):
1270
+ for z_ in zones.get(key, [])[-3:]:
1271
+ x0_ = max(v0, z_[2] - 2)
1272
+ _box3d(fig, x0_, x_end, z_[0], z_[1], 0.0, 0.14, col, 0.28)
1273
+ _label3d(fig, x_end, (z_[0] + z_[1]) / 2, 0.07, nm, col, 8)
1274
+ # BOS / CHoCH çizgileri
1275
+ for e in zones.get("events", []):
1276
+ if e["i"] < v0 or not inr(e["level"]):
1277
+ continue
1278
+ col = "#3ddc97" if e["dir"] > 0 else "#ff5c72"
1279
+ x0_ = max(v0, e["pivot_i"])
1280
+ fig.add_trace(go.Scatter3d(
1281
+ x=[x0_, (x0_ + e["i"]) / 2, e["i"]], y=[e["level"]] * 3, z=[0.0] * 3, mode="lines+text",
1282
+ line=dict(color=col, width=4, dash="solid" if e["choch"] else "dot"),
1283
+ text=["", "CHoCH" if e["choch"] else "BOS", ""], textposition="top center",
1284
+ textfont=dict(size=9, color=col), showlegend=False, hoverinfo="skip"))
1285
+ # Likidite avı işaretleri
1286
+ for d, col, nm in ((1, "#3ddc97", "Likidite Avı ▲"), (-1, "#ff5c72", "Likidite Avı ▼")):
1287
+ pts = [sw for sw in zones.get("sweeps", []) if sw["dir"] == d and sw["i"] >= v0]
1288
+ if pts:
1289
+ fig.add_trace(go.Scatter3d(
1290
+ x=[float(sw["i"]) for sw in pts],
1291
+ y=[float(df["l"].iloc[sw["i"]] if d > 0 else df["h"].iloc[sw["i"]]) for sw in pts],
1292
+ z=[0.0] * len(pts), mode="markers", name=nm,
1293
+ marker=dict(symbol="x", size=5, color=col), hoverinfo="skip"))
1294
+
1295
+ # Hacim profili seviyeleri
1296
+ vp = res.get("vp")
1297
+ if vp and vp["poc"] == vp["poc"]:
1298
+ for lab, y, col, w, dash in (("POC", vp["poc"], "#f2b84b", 4, "solid"), ("VAH", vp["vah"], "rgba(242,184,75,0.7)", 2, "dot"),
1299
+ ("VAL", vp["val"], "rgba(242,184,75,0.7)", 2, "dot")):
1300
+ if inr(y):
1301
+ _hline3d(fig, v0, x_end, y, col, w, dash, f"{lab} {fmt_price(y)}")
1302
+ # Emir defteri duvarları
1303
+ for key, col, nm in (("support", "#3ddc97", "Alış Duvarı"), ("resist", "#ff5c72", "Satış Duvarı")):
1304
+ w_ = (res.get("walls") or {}).get(key)
1305
+ if w_ and inr(w_[0]):
1306
+ _hline3d(fig, n - 30, x_end, w_[0], col, 6, "solid", f"{nm} {fmt_price(w_[0])} ×{w_[1]:.1f}")
1307
+ # Destek / direnç
1308
+ for lv, tc in res["supports"][:3]:
1309
+ if inr(lv):
1310
+ _hline3d(fig, v0, x_end, lv, "rgba(61,220,151,0.6)", 2, "dash", f"D {fmt_price(lv)} ×{tc}")
1311
+ for lv, tc in res["resists"][:3]:
1312
+ if inr(lv):
1313
+ _hline3d(fig, v0, x_end, lv, "rgba(255,92,114,0.6)", 2, "dash", f"R {fmt_price(lv)} ×{tc}")
1314
+
1315
+ # Geçmiş sinyal işlemleri
1316
+ bt = res["backtest"]
1317
+ if bt and bt["trades"]:
1318
+ atr_a = ind["_arr"]["atr"]
1319
+ for d, col, nm in ((1, "#3ddc97", "Geçmiş LONG"), (-1, "#ff5c72", "Geçmiş SHORT")):
1320
+ tr = [t for t in bt["trades"] if t["dir"] == d and t["entry_i"] >= v0]
1321
+ if not tr:
1322
+ continue
1323
+ ys = [float((df["l"].iloc[t["entry_i"]] - 0.6 * atr_a[t["entry_i"]]) if d > 0
1324
+ else (df["h"].iloc[t["entry_i"]] + 0.6 * atr_a[t["entry_i"]])) for t in tr]
1325
+ hov = [f"{nm} · net teyit {t['score']:+.0f}<br>Sonuç: {t['why']} ({t['pnl'] * 100:+.2f}% · {t['r']:+.2f}R)" for t in tr]
1326
+ fig.add_trace(go.Scatter3d(
1327
+ x=[float(t["entry_i"]) for t in tr], y=ys, z=[0.0] * len(tr), mode="markers", name=nm, text=hov,
1328
+ hoverinfo="text", marker=dict(symbol="diamond", size=5, color=col)))
1329
+
1330
+ # Güncel işlem planı
1331
+ if plan:
1332
+ x0 = max(v0, n - 30)
1333
+ lines = [("GİRİŞ", plan["entry"], "#e6ebf2"), ("SL", plan["stop_loss"], "#ff5c72")]
1334
+ lines += [(f"TP{k + 1}", px, "#3ddc97") for k, (px, _) in enumerate(plan["targets"])]
1335
+ for lab, y, col in lines:
1336
+ _hline3d(fig, x0, x_end, y, col, 5, "dash", f"{lab} {fmt_price(y)}")
1337
+
1338
+ _scene3d(fig, df, v0, n, ylo, yhi, 1.05, "Hacim yoğunluğu / SMC katmanı")
1339
+ return _base_layout(fig, f"{symbol} · {bar} · 3D SMC Sinyal Haritası", 680)
1340
+
1341
+
1342
+ def build_factor_chart(contribs, symbol):
1343
+ items = [(l, v) for l, v in contribs if abs(v) > 1e-9]
1344
+ if not items:
1345
+ return _empty_fig("Aktif SMC teyidi yok", f"{symbol} · SMC Teyitleri")
1346
+ items.sort(key=lambda x: x[1])
1347
+ labels, vals = [l for l, _ in items], [v for _, v in items]
1348
+ fig = go.Figure(go.Bar(
1349
+ x=vals, y=labels, orientation="h", marker_color=["#3ddc97" if v > 0 else "#ff5c72" for v in vals],
1350
+ text=[f"{v:+.0f}" for v in vals], textposition="outside", cliponaxis=False))
1351
+ fig.update_layout(showlegend=False, xaxis_title="SMC teyidi (her biri eşit: ±1 — ağırlık yok)")
1352
+ return _base_layout(fig, f"{symbol} · SMC Teyitleri (sinyali ne yönlendiriyor?)", max(300, 25 * len(items) + 100))
1353
+
1354
+
1355
+ def build_backtest_chart(bt, df, symbol):
1356
+ if not bt or not bt.get("stats"):
1357
+ return _empty_fig("Backtest için yeterli işlem oluşmadı (en az 3 işlem gerekir)", f"{symbol} · Sinyal Backtesti")
1358
+ start = bt["start"]
1359
+ ts = df["ts"].iloc[start:]
1360
+ fig = make_subplots(rows=2, cols=1, row_heights=[0.62, 0.38], vertical_spacing=0.14,
1361
+ subplot_titles=("Sinyal Stratejisi vs Al&Tut (komisyon dahil, tam pozisyon)", "İşlem Bazında R Çarpanı"))
1362
+ fig.add_trace(go.Scatter(x=ts, y=bt["equity"], name="Sinyal Stratejisi", line=dict(color="#5b8cff", width=2, shape="hv")),
1363
+ row=1, col=1)
1364
+ 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)
1365
+ rs = [t["r"] for t in bt["trades"]]
1366
+ fig.add_trace(go.Bar(x=list(range(1, len(rs) + 1)), y=rs, name="R",
1367
+ marker_color=["#3ddc97" if r > 0 else "#ff5c72" for r in rs],
1368
+ text=[t["why"] for t in bt["trades"]], hoverinfo="text+y"), row=2, col=1)
1369
+ fig.update_xaxes(title_text="İşlem #", row=2, col=1)
1370
+ return _base_layout(fig, f"{symbol} · Walk-Forward İşlem Simülasyonu", 470)
1371
+
1372
+
1373
+ def build_signal_chart(df, ind, symbol):
1374
+ fig = make_subplots(rows=4, cols=1, shared_xaxes=True, row_heights=[0.27, 0.25, 0.24, 0.24],
1375
+ vertical_spacing=0.05,
1376
+ subplot_titles=("RSI (14)", "MACD (12,26,9)", "Stokastik (14,3)", "ADX / DI (14)"))
1377
+ fig.add_trace(go.Scatter(x=df["ts"], y=ind["rsi"], name="RSI", line=dict(color="#5b8cff")), row=1, col=1)
1378
+ fig.add_hline(y=70, line=dict(color="#ff5c72", dash="dot", width=1), row=1, col=1)
1379
+ fig.add_hline(y=30, line=dict(color="#3ddc97", dash="dot", width=1), row=1, col=1)
1380
+
1381
+ hist_colors = np.where(ind["macd_hist"] >= 0, "#3ddc97", "#ff5c72")
1382
+ fig.add_trace(go.Bar(x=df["ts"], y=ind["macd_hist"], name="Histogram", marker_color=hist_colors), row=2, col=1)
1383
+ fig.add_trace(go.Scatter(x=df["ts"], y=ind["macd_line"], name="MACD",
1384
+ line=dict(color="#f2b84b", width=1)), row=2, col=1)
1385
+ fig.add_trace(go.Scatter(x=df["ts"], y=ind["macd_signal"], name="Sinyal",
1386
+ line=dict(color="#5b8cff", width=1)), row=2, col=1)
1387
+
1388
+ fig.add_trace(go.Scatter(x=df["ts"], y=ind["stoch_k"], name="%K", line=dict(color="#5b8cff")), row=3, col=1)
1389
+ fig.add_trace(go.Scatter(x=df["ts"], y=ind["stoch_d"], name="%D", line=dict(color="#f2b84b")), row=3, col=1)
1390
+ fig.add_hline(y=80, line=dict(color="#ff5c72", dash="dot", width=1), row=3, col=1)
1391
+ fig.add_hline(y=20, line=dict(color="#3ddc97", dash="dot", width=1), row=3, col=1)
1392
+
1393
+ fig.add_trace(go.Scatter(x=df["ts"], y=ind["adx"], name="ADX", line=dict(color="#e6ebf2", width=1.5)), row=4, col=1)
1394
+ fig.add_trace(go.Scatter(x=df["ts"], y=ind["plus_di"], name="+DI", line=dict(color="#3ddc97", width=1)), row=4, col=1)
1395
+ fig.add_trace(go.Scatter(x=df["ts"], y=ind["minus_di"], name="-DI", line=dict(color="#ff5c72", width=1)), row=4, col=1)
1396
+ fig.add_hline(y=25, line=dict(color="#f2b84b", dash="dot", width=1), row=4, col=1)
1397
+
1398
+ return _base_layout(fig, f"{symbol} · Momentum Göstergeleri (bilgi amaçlı — sinyale dahil değil)", 720)
1399
+
1400
+
1401
+ def build_equity_chart(df, symbol):
1402
+ ret = df["c"].pct_change().fillna(0)
1403
+ cum = (1 + ret).cumprod()
1404
+ peak = cum.cummax()
1405
+ dd = (cum - peak) / peak
1406
+ fig = make_subplots(rows=2, cols=1, shared_xaxes=True, row_heights=[0.65, 0.35], vertical_spacing=0.06,
1407
+ subplot_titles=("Kümülatif Getiri (Al ve Tut)", "Drawdown"))
1408
+ fig.add_trace(go.Scatter(x=df["ts"], y=cum, name="Kümülatif Getiri", line=dict(color="#5b8cff"),
1409
+ fill="tozeroy", fillcolor="rgba(91,140,255,0.08)"), row=1, col=1)
1410
+ fig.add_trace(go.Scatter(x=df["ts"], y=dd, name="Drawdown", line=dict(color="#ff5c72"),
1411
+ fill="tozeroy", fillcolor="rgba(255,92,114,0.12)"), row=2, col=1)
1412
+ fig.update_layout(showlegend=False)
1413
+ return _base_layout(fig, f"{symbol} · Risk Profili", 440)
1414
+
1415
+
1416
+ def build_depth_chart(bids, asks, symbol):
1417
+ bids_sorted = sorted(bids, key=lambda x: -x[0])
1418
+ asks_sorted = sorted(asks, key=lambda x: x[0])
1419
+ bid_prices = [p for p, _ in bids_sorted]
1420
+ bid_cum = np.cumsum([s for _, s in bids_sorted])
1421
+ ask_prices = [p for p, _ in asks_sorted]
1422
+ ask_cum = np.cumsum([s for _, s in asks_sorted])
1423
+ fig = go.Figure()
1424
+ fig.add_trace(go.Scatter(x=bid_prices, y=bid_cum, name="Alış (Bid)", line=dict(color="#3ddc97", width=2),
1425
+ fill="tozeroy", fillcolor="rgba(61,220,151,0.15)"))
1426
+ fig.add_trace(go.Scatter(x=ask_prices, y=ask_cum, name="Satış (Ask)", line=dict(color="#ff5c72", width=2),
1427
+ fill="tozeroy", fillcolor="rgba(255,92,114,0.15)"))
1428
+ fig.update_layout(xaxis_title="Fiyat", yaxis_title="Kümülatif Miktar")
1429
+ return _base_layout(fig, f"{symbol} · Emir Defteri Derinliği", 380)
1430
+
1431
+
1432
+ # ---------------------------------------------------------------------------
1433
+ # TAB CALLBACKS
1434
+ # ---------------------------------------------------------------------------
1435
+ def load_overview(symbol, bar):
1436
+ try:
1437
+ df = fetch_candles(symbol, bar, 300)
1438
+ ticker = fetch_ticker(symbol)
1439
+ ind = compute_indicators(df)
1440
+ last = float(ticker.get("last") or df["c"].iloc[-1])
1441
+ open24h = float(ticker.get("open24h") or df["c"].iloc[0])
1442
+ high24h = float(ticker.get("high24h") or df["h"].max())
1443
+ low24h = float(ticker.get("low24h") or df["l"].min())
1444
+ vol24h = float(ticker.get("volCcy24h") or df["vol"].sum())
1445
+ chg = (last - open24h) / open24h if open24h else 0.0
1446
+ tone = "up" if chg >= 0 else "down"
1447
+ cards = [
1448
+ metric_card("SON FİYAT", f"${fmt_price(last)}", "", tone),
1449
+ metric_card("24S DEĞİŞİM", fmt_pct(chg), "", tone),
1450
+ metric_card("24S YÜKSEK", f"${fmt_price(high24h)}"),
1451
+ metric_card("24S DÜŞÜK", f"${fmt_price(low24h)}"),
1452
+ metric_card("24S HACİM (USDT)", f"${fmt_large(vol24h)}"),
1453
+ ]
1454
+ return cards_row(cards), build_price_chart(df, ind, symbol, bar)
1455
+ except Exception as e:
1456
+ return error_box(f"Veri alınamadı: {e}"), go.Figure()
1457
+
1458
+
1459
+ def _reasons_html(reasons):
1460
+ icon = {"up": "▲", "down": "▼", "flat": "•"}
1461
+ return "".join(f'<li class="r-{t}"><span class="ri">{icon[t]}</span>{txt}</li>' for t, txt in reasons)
1462
+
1463
+
1464
+ def load_signal(symbol, bar, closed_only=False, acct=10000, risk_pct=1.0):
1465
+ empty5 = lambda html: (html, go.Figure(), go.Figure(), go.Figure(), go.Figure())
1466
+ try:
1467
+ df = fetch_candles(symbol, bar, 300)
1468
+ if closed_only:
1469
+ df = df[df["confirm"] == 1].reset_index(drop=True)
1470
+ if len(df) < 30:
1471
+ return empty5(error_box("Sinyal için yeterli mum verisi yok (en az 30 bar gerekir)."))
1472
+ acct = float(acct or 0)
1473
+ risk_pct = float(risk_pct or 0)
1474
+ res = generate_signal(df, symbol=symbol, bar=bar, mtf=True, micro=True, closed_only=closed_only,
1475
+ acct=acct, risk_pct=risk_pct)
1476
+ ind, plan, bt = res["indicators"], res["plan"], res["backtest"]
1477
+
1478
+ # ---- Ana panel ----
1479
+ regime_col = {"YÜKSELİŞ": "#3ddc97", "DÜŞÜŞ": "#ff5c72", "BELİRSİZ": "#7c8797"}[res["regime"]]
1480
+ panel = (
1481
+ f'<div class="signal-panel">'
1482
+ f'<div class="signal-badge" style="--tone:{res["color"]}">{res["signal"]}</div>'
1483
+ f'<div class="signal-meta"><div class="mc-label">SMC TEYİT SKORU</div>'
1484
+ f'<div class="mc-value">{res["score"]:+.0f} / ±5</div></div>'
1485
+ f'<div class="signal-meta"><div class="mc-label">GÜVEN</div>'
1486
+ f'<div class="mc-value">%{res["confidence"]}</div></div>'
1487
+ f'<div class="signal-meta"><div class="mc-label">KALİTE NOTU</div>'
1488
+ f'<div class="mc-value" style="color:{res["grade_color"]}">{res["grade"]}</div></div>'
1489
+ f'<div class="signal-meta"><div class="mc-label">PİYASA YAPISI</div>'
1490
+ f'<div class="mc-value" style="color:{regime_col}">{res["regime"]}</div></div>'
1491
+ f'</div>'
1492
+ )
1493
+ tot = res["bull_w"] + res["bear_w"]
1494
+ bull_pct = (res["bull_w"] / tot * 100) if tot > 0 else 50.0
1495
+ consensus = (
1496
+ f'<div class="consensus"><div class="mc-label">SMC TEYİT DAĞILIMI · BOĞA %{bull_pct:.0f} / AYI %{100 - bull_pct:.0f}</div>'
1497
+ f'<div class="cbar"><div class="cbull" style="width:{bull_pct:.1f}%"></div>'
1498
+ f'<div class="cbear" style="width:{100 - bull_pct:.1f}%"></div></div></div>'
1499
+ )
1500
+ conf_tags = "".join(
1501
+ f'<div class="tag" style="color:var(--text-muted)">{k} %{v * 100:.0f}</div>' for k, v in res["conf_parts"].items())
1502
+ tag_html = ""
1503
+ if res["tags"]:
1504
+ tag_html = '<div class="tags" style="margin-bottom:12px;">' + "".join(
1505
+ f'<div class="tag" style="border-color:{c};color:{c};">{t}</div>' for t, c in res["tags"]) + "</div>"
1506
+ conf_html = f'<div class="tags" style="margin-bottom:12px;">{conf_tags}</div>'
1507
+
1508
+ # ---- İşlem planı + pozisyon boyutu ----
1509
+ plan_cards, size_cards = [], []
1510
+ if plan:
1511
+ up = plan["direction"] == "LONG"
1512
+ plan_cards = [
1513
+ metric_card("ÖNERİLEN YÖN", plan["direction"], "", "up" if up else "down"),
1514
+ metric_card("GİRİŞ", f"${fmt_price(plan['entry'])}"),
1515
+ metric_card("STOP-LOSS", f"${fmt_price(plan['stop_loss'])}",
1516
+ f"{plan['stop_type']} · %{plan['risk_frac'] * 100:.2f} risk", "down"),
1517
+ ]
1518
+ for k, (px, m) in enumerate(plan["targets"]):
1519
+ plan_cards.append(metric_card(f"HEDEF {k + 1}", f"${fmt_price(px)}", f"{m:.2f}R", "up"))
1520
+ sz = plan["sizing"]
1521
+ if sz:
1522
+ size_cards = [
1523
+ metric_card("RİSK TUTARI", f"${fmt_large(sz['risk_amount'])}", f"Hesabın %{risk_pct:g}'i"),
1524
+ metric_card("POZİSYON (USDT)", f"${fmt_large(sz['notional'])}", f"Miktar: {sz['qty']:.6g}"),
1525
+ metric_card("KALDIRAÇ ETKİSİ", f"{sz['leverage']:.2f}x",
1526
+ "Spot ile yetmez — kaldıraç gerekir" if sz["leverage"] > 1.0 else "Spot ile uygulanabilir",
1527
+ "down" if sz["leverage"] > 1.0 else "up"),
1528
+ ]
1529
+ stats = bt["stats"] if bt else None
1530
+ if stats is not None:
1531
+ k = stats["kelly"]
1532
+ if stats["n"] < 20:
1533
+ kv, ks, kt = "—", "Örnek az (<20 işlem)", "neutral"
1534
+ elif k is None or k <= 0:
1535
+ kv, ks, kt = "Kenar yok", "Kelly ≤ 0", "down"
1536
+ else:
1537
+ half = min(k / 2, 0.03)
1538
+ kv, ks, kt = f"%{half * 100:.2f}", f"Yarım-Kelly (tavan %3) · bilgi amaçlı", "up"
1539
+ size_cards.append(metric_card("ÖNERİLEN RİSK", kv, ks, kt))
1540
+
1541
+ # ---- Backtest kartları ----
1542
+ bt_cards = []
1543
+ if stats:
1544
+ L, S = stats["long"], stats["short"]
1545
+ wr = lambda x: f"%{x['win_rate'] * 100:.0f}" if x["win_rate"] is not None else "—"
1546
+ bt_cards = [
1547
+ metric_card("İŞLEM SAYISI", f"{stats['n']}", f"Long {L['n']} ({wr(L)}) · Short {S['n']} ({wr(S)})"),
1548
+ metric_card("KAZANMA ORANI", f"%{stats['win_rate'] * 100:.1f}", f"Maks. üst üste kayıp: {stats['max_consec_loss']}",
1549
+ "up" if stats["win_rate"] >= 0.45 else "down"),
1550
+ metric_card("PROFIT FACTOR", f"{stats['profit_factor']:.2f}", "Brüt kazanç / brüt kayıp",
1551
+ "up" if stats["profit_factor"] >= 1.2 else "down"),
1552
+ metric_card("BEKLENTİ / İŞLEM", fmt_pct(stats["expectancy"]), f"Ort. {stats['avg_r']:+.2f}R (komisyon dahil)",
1553
+ "up" if stats["expectancy"] > 0 else "down"),
1554
+ metric_card("SQN", f"{stats['sqn']:.2f}", "Sistem kalitesi (>2 iyi, >3 çok iyi)",
1555
+ "up" if stats["sqn"] >= 2 else "neutral"),
1556
+ metric_card("STRATEJİ MAKS. DD", fmt_pct(stats["max_dd"]), "", "down"),
1557
+ metric_card("STRATEJİ vs AL&TUT", f"{fmt_pct(stats['total_return'])} / {fmt_pct(stats['bh_return'])}",
1558
+ "Aynı dönem, komisyon dahil", "up" if stats["total_return"] > stats["bh_return"] else "down"),
1559
+ ]
1560
+ acc_cards = []
1561
+ if bt and bt["acc"]:
1562
+ a = bt["acc"]
1563
+ acc_cards.append(metric_card("YÖN DOĞRULUĞU", f"%{a['accuracy'] * 100:.1f}",
1564
+ f"{a['n_signals']} sinyal · {a['forward_bars']} bar ileri",
1565
+ "up" if a["accuracy"] >= 0.5 else "down"))
1566
+ for key_, lab_ in (("long_side", "DOĞRULUK · LONG"), ("short_side", "DOĞRULUK · SHORT")):
1567
+ t = a.get(key_)
1568
+ if t:
1569
+ acc_cards.append(metric_card(lab_, f"%{t['accuracy'] * 100:.1f}", f"{t['n_signals']} sinyal",
1570
+ "up" if t["accuracy"] >= 0.5 else "down"))
1571
+
1572
+ # ---- Piyasa bağlamı ----
1573
+ ctx = []
1574
+ _tt = lambda t: "Yükseliş" if t > 0 else ("Düşüş" if t < 0 else "Belirsiz")
1575
+ if res.get("pd") is not None:
1576
+ pdv = res["pd"]
1577
+ ctx.append(metric_card("KONUM (PREMIUM / DISCOUNT)", f"%{pdv * 100:.0f}",
1578
+ "Discount (<%45) · Premium (>%55) · swing aralığı",
1579
+ "up" if pdv <= 0.45 else ("down" if pdv >= 0.55 else "neutral")))
1580
+ for m in res["mtf"]:
1581
+ if m["aligned"] is None:
1582
+ ctx.append(metric_card(f"{m['bar']} TEYİDİ", "Nötr", f"Üst zaman dilimi yapısı: {_tt(m['trend'])}"))
1583
+ else:
1584
+ ctx.append(metric_card(f"{m['bar']} TEYİDİ", "Uyumlu ✓" if m["aligned"] else "Ters Yönlü ⚠",
1585
+ f"Üst zaman dilimi yapısı: {_tt(m['trend'])}", "up" if m["aligned"] else "down"))
1586
+ if res["funding"] is not None:
1587
+ f_ = res["funding"]
1588
+ ctx.append(metric_card("FUNDING (8S)", f"{f_ * 100:+.4f}%", "Pozitif = long kalabalık",
1589
+ "down" if f_ > 0.0002 else ("up" if f_ < -0.0002 else "neutral")))
1590
+ if res["imbalance"] is not None:
1591
+ im = res["imbalance"]
1592
+ ctx.append(metric_card("DEFTER DENGESİ", f"{im * 100:+.0f}%", "Derinlik ağırlıklı",
1593
+ "up" if im > 0.2 else ("down" if im < -0.2 else "neutral")))
1594
+ if res["supports"]:
1595
+ lv, tc = res["supports"][0]
1596
+ ctx.append(metric_card("YAKIN DESTEK", f"${fmt_price(lv)}", f"{tc} temas · {(res['price'] - lv) / res['atr']:.1f} ATR", "up"))
1597
+ if res["resists"]:
1598
+ lv, tc = res["resists"][0]
1599
+ ctx.append(metric_card("YAKIN DİRENÇ", f"${fmt_price(lv)}", f"{tc} temas · {(lv - res['price']) / res['atr']:.1f} ATR", "down"))
1600
+ ctx.append(metric_card("VOLATİLİTE YÜZDELİĞİ", f"%{res['atr_rank'] * 100:.0f}", "ATR/fiyat, son 100 bar",
1601
+ "down" if res["atr_rank"] > 0.9 else "neutral"))
1602
+
1603
+ reasons_box = (f'<div class="reasons-box"><div class="mc-label">SMC ANALİZİ</div>'
1604
+ f'<ul>{_reasons_html(res["reasons"])}</ul></div>')
1605
+ bt_note = ""
1606
+ if stats:
1607
+ bt_note = ('<div class="note">Backtest: son ~220 barda sinyaller üst üste binmeden işleme çevrilir; giriş bir sonraki barın açılışı, '
1608
+ f'çıkış SL {SL_ATR}×ATR / TP {TP_ATR}×ATR / {MAX_HOLD_BARS} bar; komisyon %{FEE_PER_SIDE * 100:.1f}/yön dahildir. '
1609
+ 'Emir defteri, funding, S/R ve MTF katmanları canlıdır, geçmişe uygulanamaz.</div>')
1610
+
1611
+ html = (panel + consensus + conf_html + tag_html
1612
+ + (section("İŞLEM PLANI") + cards_row(plan_cards) if plan_cards else "")
1613
+ + (section("POZİSYON BOYUTU") + cards_row(size_cards) if size_cards else "")
1614
+ + (section("GEÇMİŞ PERFORMANS (WALK-FORWARD)") + cards_row(bt_cards) + cards_row(acc_cards) + bt_note
1615
+ if (bt_cards or acc_cards) else "")
1616
+ + section("PİYASA BAĞLAMI") + cards_row(ctx) + reasons_box)
1617
+ return (html, build_signal_price_chart(df, ind, symbol, bar, res), build_factor_chart(res["contribs"], symbol),
1618
+ build_backtest_chart(bt, df, symbol), build_signal_chart(df, ind, symbol))
1619
+ except Exception as e:
1620
+ return empty5(error_box(f"Veri alınamadı: {e}"))
1621
+
1622
+
1623
+ def load_risk(symbol, bar):
1624
+ try:
1625
+ df = fetch_candles(symbol, bar, 300)
1626
+ rm = risk_metrics(df, bar)
1627
+ if rm is None:
1628
+ return error_box("Risk hesaplaması için yeterli veri yok."), go.Figure()
1629
+ cards = [
1630
+ metric_card("YILLIKLANDIRILMIŞ VOLATİLİTE", fmt_pct(rm["volatility_annual"])),
1631
+ metric_card("YILLIKLANDIRILMIŞ GETİRİ", fmt_pct(rm["return_annual"]), "",
1632
+ "up" if rm["return_annual"] >= 0 else "down"),
1633
+ metric_card("SHARPE ORANI", f"{rm['sharpe']:.2f}", "", "up" if rm["sharpe"] > 0 else "down"),
1634
+ metric_card("MAKS. DRAWDOWN", fmt_pct(rm["max_drawdown"]), "", "down"),
1635
+ metric_card("GÜNCEL DRAWDOWN", fmt_pct(rm["current_dd"]), "",
1636
+ "down" if rm["current_dd"] < 0 else "neutral"),
1637
+ metric_card("VaR %95 (BAR)", fmt_pct(rm["var95_bar"]), "", "down"),
1638
+ metric_card("CVaR %95 (BAR)", fmt_pct(rm["cvar95_bar"]), "", "down"),
1639
+ ]
1640
+ return cards_row(cards), build_equity_chart(df, symbol)
1641
+ except Exception as e:
1642
+ return error_box(f"Veri alınamadı: {e}"), go.Figure()
1643
+
1644
+
1645
+ def load_orderbook(symbol, depth):
1646
+ try:
1647
+ ob = fetch_orderbook(symbol, int(depth))
1648
+ bids = [(float(p), float(s)) for p, s, *_ in ob.get("bids", [])]
1649
+ asks = [(float(p), float(s)) for p, s, *_ in ob.get("asks", [])]
1650
+ if not bids or not asks:
1651
+ return error_box("Emir defteri verisi alınamadı."), go.Figure()
1652
+ best_bid, best_ask = bids[0][0], asks[0][0]
1653
+ spread = best_ask - best_bid
1654
+ spread_pct = spread / best_ask if best_ask else 0.0
1655
+ bid_vol = sum(s for _, s in bids)
1656
+ ask_vol = sum(s for _, s in asks)
1657
+ imbalance = (bid_vol - ask_vol) / (bid_vol + ask_vol) if (bid_vol + ask_vol) else 0.0
1658
+ tone = "up" if imbalance > 0.05 else ("down" if imbalance < -0.05 else "neutral")
1659
+ cards = [
1660
+ metric_card("EN İYİ ALIŞ", f"${fmt_price(best_bid)}", "", "up"),
1661
+ metric_card("EN İYİ SATIŞ", f"${fmt_price(best_ask)}", "", "down"),
1662
+ metric_card("SPREAD", f"${fmt_price(spread)}", fmt_pct(spread_pct)),
1663
+ metric_card("DEFTER DENGESİZLİĞİ", fmt_pct(imbalance), "Pozitif = alış baskısı", tone),
1664
+ ]
1665
+ return cards_row(cards), build_depth_chart(bids, asks, symbol)
1666
+ except Exception as e:
1667
+ return error_box(f"Veri alınamadı: {e}"), go.Figure()
1668
+
1669
+
1670
+ @_gpu
1671
+ def refresh_all(symbol, bar, depth, closed_only, acct, risk_pct):
1672
+ ov_html, ov_fig = load_overview(symbol, bar)
1673
+ sig = load_signal(symbol, bar, closed_only, acct, risk_pct)
1674
+ risk_html, risk_fig = load_risk(symbol, bar)
1675
+ ob_html, ob_fig = load_orderbook(symbol, depth)
1676
+ return (ov_html, ov_fig, *sig, risk_html, risk_fig, ob_html, ob_fig)
1677
+
1678
+
1679
+ def refresh_signal(symbol, bar, closed_only, acct, risk_pct):
1680
+ """Yalnızca sinyal sekmesini yeniler (risk/kapanmış-mum ayarı değişince hızlı güncelleme)."""
1681
+ return load_signal(symbol, bar, closed_only, acct, risk_pct)
1682
+
1683
+
1684
+ SCAN_MODES = ["Tümü (skora göre)", "Yalnızca AL yönlü", "Yalnızca SAT yönlü", "Yalnızca yüksek kalite (A / A+)"]
1685
+
1686
+
1687
+ def _scan_one(sym, bar, all_tickers, closed_only):
1688
+ df = fetch_candles(sym, bar, 300)
1689
+ if closed_only:
1690
+ df = df[df["confirm"] == 1].reset_index(drop=True)
1691
+ # MTF ve mikro-yapı katmanları taramada kapalı (API yükünü azaltmak için); tekil analizde açık.
1692
+ res = generate_signal(df, symbol=sym, bar=bar, mtf=False, micro=False, closed_only=closed_only)
1693
+ rm = risk_metrics(df, bar)
1694
+ t = all_tickers.get(sym, {})
1695
+ last = float(t.get("last") or df["c"].iloc[-1])
1696
+ open24h = float(t.get("open24h") or df["c"].iloc[0])
1697
+ chg24h = (last - open24h) / open24h if open24h else 0.0
1698
+ pdv = res["pd"]
1699
+ zone = "—" if pdv is None else ("Discount" if pdv <= 0.45 else ("Premium" if pdv >= 0.55 else "Denge"))
1700
+ events = res["smc"]["events"]
1701
+ last_ev = "—"
1702
+ if events:
1703
+ e = events[-1]
1704
+ last_ev = f"{'CHoCH' if e['choch'] else 'BOS'} {'▲' if e['dir'] > 0 else '▼'} ({len(df) - 1 - e['i']} bar önce)"
1705
+ bt = res["backtest"]
1706
+ st = bt["stats"] if bt else None
1707
+ acc = bt["acc"] if bt else None
1708
+ return {
1709
+ "Sembol": sym, "Fiyat": last, "24S %": round(chg24h * 100, 2), "Sinyal": res["signal"],
1710
+ "Skor": res["score"], "Kalite": res["grade"], "Güven %": res["confidence"], "Yapı": res["regime"],
1711
+ "Bölge": zone, "Son Kırılım": last_ev,
1712
+ "RSI": round(res["indicators"]["rsi"].iloc[-1], 1),
1713
+ "Kazanma %": round(st["win_rate"] * 100, 1) if st else None,
1714
+ "Profit Factor": round(st["profit_factor"], 2) if st else None,
1715
+ "Beklenti %": round(st["expectancy"] * 100, 2) if st else None,
1716
+ "Yön Doğruluğu %": round(acc["accuracy"] * 100, 1) if acc else None,
1717
+ "Volatilite (Yıllık) %": round(rm["volatility_annual"] * 100, 1) if rm else None,
1718
+ "Sharpe": round(rm["sharpe"], 2) if rm else None,
1719
+ }
1720
+
1721
+
1722
+ def run_scanner(symbols_text, bar, closed_only=False, mode=SCAN_MODES[0], progress=gr.Progress()):
1723
+ symbols = [s.strip().upper() for s in symbols_text.split(",") if s.strip()][:25]
1724
+ if not symbols:
1725
+ return pd.DataFrame()
1726
+ try:
1727
+ all_tickers = {t["instId"]: t for t in fetch_all_tickers("SPOT")}
1728
+ except Exception:
1729
+ all_tickers = {}
1730
+
1731
+ rows, done = [], 0
1732
+ with ThreadPoolExecutor(max_workers=4) as ex: # ağ beklemesini paralelle (OKX limiti için 4 ile sınırlı)
1733
+ futs = {ex.submit(_scan_one, s, bar, all_tickers, closed_only): s for s in symbols}
1734
+ for f in as_completed(futs):
1735
+ done += 1
1736
+ progress(done / len(symbols), desc=f"{futs[f]} tarandı ({done}/{len(symbols)})")
1737
+ try:
1738
+ rows.append(f.result())
1739
+ except Exception:
1740
+ continue
1741
+ if not rows:
1742
+ return pd.DataFrame()
1743
+ out = pd.DataFrame(rows)
1744
+ if mode == SCAN_MODES[1]:
1745
+ out = out[out["Skor"] >= SIGNAL_THRESHOLD].sort_values("Skor", ascending=False)
1746
+ elif mode == SCAN_MODES[2]:
1747
+ out = out[out["Skor"] <= -SIGNAL_THRESHOLD].sort_values("Skor", ascending=True)
1748
+ elif mode == SCAN_MODES[3]:
1749
+ out = out[out["Kalite"].isin(["A", "A+"])].sort_values("Güven %", ascending=False)
1750
+ else:
1751
+ out = out.sort_values("Skor", ascending=False)
1752
+ return out.reset_index(drop=True)
1753
+
1754
+
1755
+ # ---------------------------------------------------------------------------
1756
+ # STYLE — dark "trading terminal" identity (deliberate, not default SaaS chrome)
1757
+ # ---------------------------------------------------------------------------
1758
+ HEAD = """
1759
+ <link rel="preconnect" href="https://fonts.googleapis.com">
1760
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
1761
+ <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">
1762
+ """
1763
+
1764
+ CSS = """
1765
+ :root {
1766
+ --bg-void: #0a0e14;
1767
+ --bg-panel: #12171f;
1768
+ --line: #232a35;
1769
+ --text-primary: #e6ebf2;
1770
+ --text-muted: #7c8797;
1771
+ --accent-long: #3ddc97;
1772
+ --accent-short: #ff5c72;
1773
+ --accent-signal: #f2b84b;
1774
+ --accent-primary: #5b8cff;
1775
+ }
1776
+ .gradio-container { background: var(--bg-void) !important; color: var(--text-primary) !important;
1777
+ font-family: 'IBM Plex Sans', system-ui, sans-serif !important; }
1778
+ h1, h2, h3 { font-family: 'Space Grotesk', sans-serif !important; letter-spacing: -0.01em; }
1779
+
1780
+ #masthead { display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:10px;
1781
+ border-bottom: 1px solid var(--line); padding: 14px 4px 18px 4px; margin-bottom: 6px; }
1782
+ #masthead .wordmark { font-family:'Space Grotesk',sans-serif; font-weight:700; font-size:1.5rem; letter-spacing:-0.02em; }
1783
+ #masthead .wordmark span { color: var(--accent-primary); }
1784
+ #masthead .tags { display:flex; gap:8px; flex-wrap:wrap; }
1785
+ #masthead .tag { font-size:0.7rem; padding:3px 9px; border:1px solid var(--line); border-radius:3px;
1786
+ color:var(--text-muted); font-family:'IBM Plex Mono',monospace; }
1787
+
1788
+ .metric-row { display:flex; flex-wrap:wrap; gap:10px; margin:8px 0 16px 0; }
1789
+ .metric-card { background: var(--bg-panel); border:1px solid var(--line); border-radius:4px;
1790
+ padding:12px 16px; min-width:150px; flex:1; }
1791
+ .mc-label { font-size:0.66rem; color:var(--text-muted); letter-spacing:0.04em; margin-bottom:6px; }
1792
+ .mc-value { font-size:1.24rem; font-weight:600; color:var(--text-primary);
1793
+ font-family:'IBM Plex Mono',monospace; }
1794
+ .mc-sub { font-size:0.72rem; color:var(--text-muted); margin-top:4px; font-family:'IBM Plex Mono',monospace; }
1795
+ .metric-card.mc-up .mc-value { color: var(--accent-long); }
1796
+ .metric-card.mc-down .mc-value { color: var(--accent-short); }
1797
+
1798
+ .section-title { font-family:'Space Grotesk',sans-serif; font-size:0.78rem; font-weight:600; letter-spacing:0.08em;
1799
+ color:var(--text-muted); margin:18px 0 0 0; padding-top:12px; border-top:1px solid var(--line); }
1800
+
1801
+ .signal-panel { display:flex; align-items:center; flex-wrap:wrap; gap:20px; background:var(--bg-panel);
1802
+ border:1px solid var(--line); border-radius:4px; padding:18px 20px; margin-bottom:12px; }
1803
+ .signal-badge { font-family:'Space Grotesk',sans-serif; font-weight:700; font-size:1.6rem; color:var(--tone);
1804
+ border:2px solid var(--tone); border-radius:4px; padding:10px 22px; }
1805
+ .signal-meta .mc-value { font-size:1.35rem; }
1806
+
1807
+ .consensus { background:var(--bg-panel); border:1px solid var(--line); border-radius:4px; padding:12px 16px; margin-bottom:12px; }
1808
+ .cbar { display:flex; height:8px; border-radius:2px; overflow:hidden; background:var(--line); }
1809
+ .cbull { background: var(--accent-long); }
1810
+ .cbear { background: var(--accent-short); }
1811
+
1812
+ .tags { display:flex; gap:8px; flex-wrap:wrap; }
1813
+ .tags .tag { font-size:0.72rem; font-weight:600; padding:5px 12px; border:1px solid var(--line);
1814
+ border-radius:3px; font-family:'IBM Plex Mono',monospace; letter-spacing:0.03em; }
1815
+
1816
+ .reasons-box { background:var(--bg-panel); border:1px solid var(--line); border-radius:4px; padding:16px 20px; margin-top:8px; }
1817
+ .reasons-box ul { margin:8px 0 0 0; padding-left:0; list-style:none; }
1818
+ .reasons-box li { margin-bottom:6px; color:var(--text-primary); font-size:0.9rem; }
1819
+ .reasons-box .ri { display:inline-block; width:18px; font-size:0.7rem; }
1820
+ .reasons-box .r-up .ri { color: var(--accent-long); }
1821
+ .reasons-box .r-down .ri { color: var(--accent-short); }
1822
+ .reasons-box .r-flat .ri { color: var(--text-muted); }
1823
+
1824
+ .note { color:var(--text-muted); font-size:0.74rem; line-height:1.5; margin:-6px 0 8px 0; }
1825
+
1826
+ .error-box { background: rgba(255,92,114,0.08); border:1px solid var(--accent-short); color:var(--accent-short);
1827
+ padding:14px 18px; border-radius:4px; font-family:'IBM Plex Mono',monospace; font-size:0.85rem; }
1828
+
1829
+ .disclaimer { border-top:1px solid var(--line); margin-top:22px; padding-top:14px; color:var(--text-muted);
1830
+ font-size:0.76rem; line-height:1.55; }
1831
+ """
1832
+
1833
+ # ---------------------------------------------------------------------------
1834
+ # GRADIO UI
1835
+ # ---------------------------------------------------------------------------
1836
+ with gr.Blocks(title="OKX Quant Terminal") as demo:
1837
+
1838
+ gr.HTML(
1839
+ '<div id="masthead">'
1840
+ '<div class="wordmark">OKX <span>QUANT</span> TERMINAL</div>'
1841
+ '<div class="tags"><div class="tag">SMART MONEY ENGINE v3 · 3D</div>'
1842
+ '<div class="tag">PUBLIC MARKET DATA</div>'
1843
+ '<div class="tag">READ-ONLY · EMİR GÖNDERİLMEZ</div></div>'
1844
+ '</div>'
1845
+ )
1846
+
1847
+ with gr.Row():
1848
+ symbol_dd = gr.Dropdown(choices=POPULAR_SYMBOLS, value="BTC-USDT", allow_custom_value=True,
1849
+ label="Sembol", scale=2)
1850
+ bar_dd = gr.Dropdown(choices=BAR_OPTIONS, value="1H", label="Zaman Dilimi", scale=1)
1851
+ depth_slider = gr.Slider(5, 50, value=20, step=5, label="Defter Derinliği", scale=1)
1852
+ refresh_btn = gr.Button("🔄 Yenile", variant="primary", scale=1)
1853
+ auto_cb = gr.Checkbox(label="Otomatik (15sn)", value=False, scale=1)
1854
+
1855
+ with gr.Row():
1856
+ closed_cb = gr.Checkbox(label="Sadece kapanmış mumlarla sinyal (repaint yok)", value=False, scale=2)
1857
+ acct_num = gr.Number(value=10000, label="Hesap Büyüklüğü (USDT)", minimum=0, scale=1)
1858
+ risk_sl = gr.Slider(0.25, 5, value=1, step=0.25, label="İşlem Başına Risk (%)", scale=1)
1859
+
1860
+ timer = gr.Timer(15, active=False)
1861
+
1862
+ with gr.Tabs():
1863
+ with gr.Tab("📊 Genel Bakış"):
1864
+ ov_html = gr.HTML()
1865
+ ov_plot = gr.Plot()
1866
+ with gr.Tab("🎯 Sinyal Motoru"):
1867
+ sig_html = gr.HTML()
1868
+ sig_map_plot = gr.Plot()
1869
+ sig_factor_plot = gr.Plot()
1870
+ sig_bt_plot = gr.Plot()
1871
+ sig_ind_plot = gr.Plot()
1872
+ with gr.Tab("⚠️ Risk Analitiği"):
1873
+ risk_html = gr.HTML()
1874
+ risk_plot = gr.Plot()
1875
+ with gr.Tab("📖 Emir Defteri"):
1876
+ ob_html = gr.HTML()
1877
+ ob_plot = gr.Plot()
1878
+ with gr.Tab("🔍 Çoklu Varlık Tarayıcı"):
1879
+ gr.Markdown("Popüler pariteleri SMC teyit skoruna göre sıralar. Kendi listenizi virgülle ayırıp girebilirsiniz (en fazla 25 sembol).")
1880
+ with gr.Row():
1881
+ scan_symbols = gr.Textbox(value=",".join(POPULAR_SYMBOLS), label="Semboller", scale=3)
1882
+ scan_bar = gr.Dropdown(choices=BAR_OPTIONS, value="4H", label="Zaman Dilimi", scale=1)
1883
+ scan_mode = gr.Dropdown(choices=SCAN_MODES, value=SCAN_MODES[0], label="Filtre", scale=2)
1884
+ scan_btn = gr.Button("🔍 Taramayı Başlat", variant="primary")
1885
+ scan_out = gr.Dataframe(interactive=False, wrap=True)
1886
+
1887
+ gr.HTML(
1888
+ '<div class="disclaimer">Bu panel yalnızca eğitim ve bilgilendirme amaçlıdır; yatırım tavsiyesi '
1889
+ 'değildir. Tüm veriler OKX genel (public) piyasa API\'sinden alınır; hiçbir API anahtarı '
1890
+ 'kullanılmaz ve hiçbir gerçek emir gönderilmez. Kripto varlıklar yüksek oynaklık içerir; geçmiş '
1891
+ 'performans (backtest dahil) gelecekteki sonuçların göstergesi değildir. Backtest sonuçları '
1892
+ 'kısa örneklem üzerindedir ve istatistiksel olarak sınırlıdır.</div>'
1893
+ )
1894
+
1895
+ io_inputs = [symbol_dd, bar_dd, depth_slider, closed_cb, acct_num, risk_sl]
1896
+ io_outputs = [ov_html, ov_plot, sig_html, sig_map_plot, sig_factor_plot, sig_bt_plot, sig_ind_plot,
1897
+ risk_html, risk_plot, ob_html, ob_plot]
1898
+ sig_inputs = [symbol_dd, bar_dd, closed_cb, acct_num, risk_sl]
1899
+ sig_outputs = [sig_html, sig_map_plot, sig_factor_plot, sig_bt_plot, sig_ind_plot]
1900
+
1901
+ refresh_btn.click(refresh_all, io_inputs, io_outputs)
1902
+ timer.tick(refresh_all, io_inputs, io_outputs)
1903
+ auto_cb.change(lambda active: gr.Timer(active=active), auto_cb, timer)
1904
+ closed_cb.change(refresh_signal, sig_inputs, sig_outputs)
1905
+ acct_num.submit(refresh_signal, sig_inputs, sig_outputs)
1906
+ risk_sl.release(refresh_signal, sig_inputs, sig_outputs)
1907
+ scan_btn.click(run_scanner, [scan_symbols, scan_bar, closed_cb, scan_mode], scan_out)
1908
+ demo.load(refresh_all, io_inputs, io_outputs)
1909
+
1910
+ demo.queue().launch(
1911
+ theme=gr.themes.Base(primary_hue="blue", neutral_hue="slate"),
1912
+ css=CSS,
1913
+ head=HEAD,
1914
+ ssr_mode=False,
1915
+ )