GoshawkVortexAI commited on
Commit
064dc4e
·
verified ·
1 Parent(s): ccef12a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +784 -1155
app.py CHANGED
@@ -1,1218 +1,847 @@
1
- """
2
- OKX Value Intelligence v1 — Compact Fair-Value & Risk Platform (OKX Spot, USDT pairs)
3
- ======================================================================================
4
- Data integrity > model complexity. Fair value > momentum. Margin of safety > prediction.
5
-
6
- Veri kaynağı: OKX public REST API (anahtar gerekmez) — spot ticker, günlük mumlar, perp funding / open interest.
7
- Veri etiketleri: [R] OKX'ten alınan [D] türetilen [A] kullanıcı varsayımı
8
- UYARI: Kripto varlıkların bilançosu/nakit akışı yoktur. "Fair value" fiyat-hacim verisinden türetilen
9
- istatistiksel referanstır (içsel değer değildir). Backtest/yatırım tavsiyesi değildir.
10
- """
11
-
12
- import logging
13
- import math
14
- import tempfile
15
  import time
16
- from datetime import datetime, timedelta, timezone
17
- from typing import Any, Dict, List, Optional, Tuple
 
18
 
19
- import gradio as gr
20
  import numpy as np
21
  import pandas as pd
22
- import plotly.graph_objects as go
23
  import requests
 
 
 
 
 
 
 
24
 
25
- logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(levelname)s | %(message)s")
26
- logger = logging.getLogger("okx_v1")
27
-
28
- # ============================================================================
29
- # CONFIG & UNIVERSE
30
- # ============================================================================
31
-
32
- OKX_BASE = "https://www.okx.com"
33
- REQUEST_DELAY_SEC = 0.12 # OKX public limit: 20 req / 2 s per endpoint
34
- CACHE_TTL_HOURS = 1
35
- LOOKBACK_DAYS = 500 # günlük mum derinliği (300 + 2x100 sayfa)
36
- MIN_PEERS = 5
37
- DEFAULT_UNIVERSE_N = 100
38
- MIN_VOL_USDT = 1_000_000 # 24s hacim alt sınırı (evren filtresi)
39
-
40
- STABLES = {"USDC", "DAI", "TUSD", "FDUSD", "USDE", "PYUSD", "USDG", "USDD", "EURC", "BUSD", "USTC", "USDP", "EURT", "XAUT"}
41
-
42
- # Kategori (segment) haritası — listede olmayan varlıklar "Other"
43
- CATEGORY_MAP = {
44
- "Majors": ["BTC", "ETH", "SOL", "XRP", "BNB", "ADA", "TRX", "LTC", "BCH", "AVAX", "DOT", "TON"],
45
- "Alt L1": ["ATOM", "NEAR", "APT", "SUI", "SEI", "ALGO", "ICP", "FIL", "HBAR", "XLM", "ETC", "EOS", "XTZ", "VET",
46
- "THETA", "TIA", "KAS", "MINA", "FLOW", "S", "FTM", "KAVA", "CFX", "EGLD"],
47
- "DeFi": ["UNI", "AAVE", "MKR", "LDO", "CRV", "SNX", "COMP", "DYDX", "INJ", "JUP", "ENA", "ONDO", "PENDLE", "1INCH",
48
- "SUSHI", "CAKE", "RUNE", "GMX", "ETHFI", "EIGEN"],
49
- "Infra/L2": ["ARB", "OP", "POL", "MATIC", "LINK", "GRT", "STX", "IMX", "STRK", "ZK", "MNT", "PYTH", "API3", "W", "BLUR", "ZRO"],
50
- "Meme": ["DOGE", "SHIB", "PEPE", "WIF", "BONK", "FLOKI", "TRUMP", "PNUT", "MEME", "TURBO", "NEIRO", "BOME", "MEW", "ORDI"],
51
- "AI/Gaming": ["FET", "RENDER", "RNDR", "WLD", "TAO", "AGIX", "OCEAN", "AR", "SAND", "MANA", "AXS", "GALA", "APE", "ENS", "AIXBT", "VIRTUAL"],
52
- }
53
- CATEGORY = {b: cat for cat, bases in CATEGORY_MAP.items() for b in bases}
54
- SEGMENTS = list(CATEGORY_MAP.keys()) + ["Other"]
55
-
56
- FALLBACK_BASES = ["BTC", "ETH", "SOL", "XRP", "DOGE", "ADA", "AVAX", "LINK", "DOT", "LTC", "BCH", "TRX", "UNI", "ATOM",
57
- "NEAR", "APT", "ARB", "OP", "SUI", "FIL", "ICP", "ETC", "XLM", "HBAR", "AAVE", "MKR", "LDO", "INJ",
58
- "SHIB", "PEPE", "WIF", "BONK", "FET", "RENDER", "GRT", "IMX", "SAND", "MANA", "TIA", "SEI"]
59
-
60
- _cache: Dict[str, Tuple[datetime, Any]] = {}
61
- _TICK: Dict[str, Any] = {"ts": 0.0, "data": {}}
62
- SETTINGS: Dict[str, Any] = {"perp": True}
63
- LAST: Dict[str, Any] = {"df": None, "failed": [], "universe": []}
64
- _session = requests.Session()
65
- _session.headers.update({"User-Agent": "okx-value-intelligence/1.0"})
66
-
67
-
68
- def _cget(k):
69
- if k in _cache:
70
- ts, v = _cache[k]
71
- if datetime.now() - ts < timedelta(hours=CACHE_TTL_HOURS):
72
- return v
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  return None
74
 
75
 
76
- def _cset(k, v):
77
- _cache[k] = (datetime.now(), v)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
 
79
 
80
- # ============================================================================
81
- # SMALL HELPERS
82
- # ============================================================================
 
 
 
83
 
84
- def num(x) -> Optional[float]:
85
- try:
86
- if x is None or x == "":
87
- return None
88
- v = float(x)
89
- return None if (np.isnan(v) or np.isinf(v)) else v
90
- except Exception:
91
- return None
92
 
 
 
 
93
 
94
- def div(a, b) -> Optional[float]:
95
- a, b = num(a), num(b)
96
- if a is None or b is None or b == 0:
97
- return None
98
- return a / b
 
 
99
 
100
 
101
- def real_ret(nominal, infl) -> Optional[float]:
102
- return None if nominal is None else (1 + nominal) / (1 + infl) - 1
 
103
 
 
104
 
105
- def lin(x, bad, good) -> Optional[float]:
106
- """Linear 0-100 scale; works for bad>good as well."""
107
- if x is None:
108
- return None
109
- return float(np.clip((x - bad) / (good - bad), 0, 1) * 100)
110
 
 
111
 
112
- def wavg(items) -> Tuple[Optional[float], float]:
113
- v = [(s, w) for s, w in items if s is not None]
114
- tw = sum(w for _, w in items)
115
- if not v or tw == 0:
116
- return None, 0.0
117
- sw = sum(w for _, w in v)
118
- return sum(s * w for s, w in v) / sw, sw / tw
119
 
 
 
 
120
 
121
- def anyc(*c) -> Optional[bool]:
122
- v = [x for x in c if x is not None]
123
- return None if not v else any(v)
 
124
 
 
125
 
126
- def fm(x, d=2):
127
- return "N/A" if x is None else f"{x:,.{d}f}"
 
128
 
 
 
 
 
129
 
130
- def fpx(x):
131
- """Fiyat biçimi: BTC (100k) ile PEPE (0.00001) aynı yerde okunabilsin."""
132
- if x is None:
133
- return "N/A"
134
- a = abs(x)
135
- if a >= 1000:
136
- return f"{x:,.0f}"
137
- if a >= 1:
138
- return f"{x:,.2f}"
139
- if a >= 0.01:
140
- return f"{x:.4f}"
141
- return f"{x:.4g}"
142
 
 
143
 
144
- def rsig(x, n=5):
145
- return None if x is None else float(f"{x:.{n}g}")
146
 
147
 
148
- def fp(x, d=1):
149
- return "N/A" if x is None else f"{x * 100:.{d}f}%"
 
150
 
 
 
 
 
 
 
 
 
 
 
151
 
152
- def pct(x, d=1):
153
- return None if x is None else round(x * 100, d)
154
 
 
 
 
155
 
156
- def norm_sym(t: str) -> str:
157
- """'btc', 'eth/usdt', 'SOLUSDT', 'BTC-USDT-SWAP' -> OKX spot instId (BTC-USDT)."""
158
- s = (t or "").strip().upper().replace("/", "-").replace("_", "-").replace(" ", "")
159
- if not s:
160
- return ""
161
- if s.endswith("-SWAP"):
162
- s = s[:-5]
163
- if "-" in s:
164
- parts = s.split("-")
165
- return f"{parts[0]}-{parts[1]}" if len(parts) >= 2 and parts[1] else f"{parts[0]}-USDT"
166
- for q in ("USDT", "USDC"):
167
- if s.endswith(q) and len(s) > len(q):
168
- return f"{s[:-len(q)]}-{q}"
169
- return f"{s}-USDT"
170
-
171
-
172
- def mk_params(infl, rw, rtg, rg, pt, cv) -> Dict[str, float]:
173
- return dict(infl=infl / 100, rw=rw / 100, rtg=rtg / 100, rg=rg / 100, pt=pt / 100, conv=cv / 100)
174
-
175
-
176
- def tier_of(v: Optional[float]) -> str:
177
- if v is None:
178
- return "Tier-4 (<5M)"
179
- if v >= 100e6:
180
- return "Tier-1 (≥100M)"
181
- if v >= 20e6:
182
- return "Tier-2 (20-100M)"
183
- if v >= 5e6:
184
- return "Tier-3 (5-20M)"
185
- return "Tier-4 (<5M)"
186
-
187
-
188
- # ============================================================================
189
- # OKX DATA ACQUISITION
190
- # ============================================================================
191
-
192
- def okx_get(path: str, params: Optional[Dict[str, str]] = None, retries: int = 3) -> Optional[List[Any]]:
193
- """OKX v5 public GET. Returns list (maybe empty) on success, None on failure."""
194
- url = OKX_BASE + path
195
- for k in range(retries):
196
- try:
197
- r = _session.get(url, params=params, timeout=15)
198
- if r.status_code == 429:
199
- time.sleep(1.0 * (k + 1))
200
- continue
201
- r.raise_for_status()
202
- j = r.json()
203
- code = str(j.get("code"))
204
- if code == "0":
205
- return j.get("data") or []
206
- if code == "50011": # rate limit
207
- time.sleep(1.0 * (k + 1))
208
- continue
209
- logger.debug("OKX %s %s -> code %s %s", path, params, code, j.get("msg"))
210
- return None
211
- except Exception as e:
212
- logger.warning("OKX %s failed (%s/%s): %s", path, k + 1, retries, e)
213
- time.sleep(0.5 * (k + 1))
214
- return None
215
 
 
216
 
217
- def get_tickers(force: bool = False) -> Dict[str, Dict[str, Any]]:
218
- if not force and _TICK["data"] and time.time() - _TICK["ts"] < 120:
219
- return _TICK["data"]
220
- rows = okx_get("/api/v5/market/tickers", {"instType": "SPOT"})
221
- if rows:
222
- _TICK["data"] = {r["instId"]: r for r in rows if r.get("instId")}
223
- _TICK["ts"] = time.time()
224
- return _TICK["data"]
225
 
 
 
 
226
 
227
- def is_excluded(base: str) -> bool:
228
- return base in STABLES or base.endswith(("3L", "3S", "5L", "5S"))
229
 
230
 
231
- def build_universe(n: int = DEFAULT_UNIVERSE_N) -> Tuple[List[str], str]:
232
- """Top-N OKX spot USDT pairs by 24h quote volume (stablecoin / leveraged tokens excluded)."""
233
- tk = get_tickers(force=True)
234
- rows = []
235
- for inst, r in tk.items():
236
- if not inst.endswith("-USDT"):
237
- continue
238
- if is_excluded(inst[:-5]):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
  continue
240
- v = num(r.get("volCcy24h")) # SPOT: quote currency (USDT) cinsinden 24s hacim
241
- if v is None or v < MIN_VOL_USDT:
 
 
 
 
 
 
242
  continue
243
- rows.append((v, inst))
244
- rows.sort(reverse=True)
245
- if rows:
246
- return [i for _, i in rows[:n]], "OKX tickers (24s hacme göre)"
247
- return [f"{b}-USDT" for b in FALLBACK_BASES][:n], "⚠️ ticker listesi alınamadı → gömülü yedek liste"
248
 
 
 
249
 
250
- def fetch_candles(inst: str, n: int = LOOKBACK_DAYS) -> Optional[pd.DataFrame]:
251
- rows = okx_get("/api/v5/market/candles", {"instId": inst, "bar": "1D", "limit": "300"})
252
- if not rows:
253
- return None
254
- allr = list(rows)
255
- oldest = int(rows[-1][0])
256
- while len(allr) < n:
257
- time.sleep(REQUEST_DELAY_SEC)
258
- more = okx_get("/api/v5/market/history-candles", {"instId": inst, "bar": "1D", "after": str(oldest), "limit": "100"})
259
- if not more:
260
- break
261
- allr.extend(more)
262
- new_oldest = int(more[-1][0])
263
- if new_oldest >= oldest:
264
- break
265
- oldest = new_oldest
266
- if len(more) < 100:
267
- break
268
- cols = ["ts", "open", "high", "low", "close", "vol", "volccy", "volq", "confirm"]
269
- recs = []
270
- for r in allr:
271
- r = list(r) + [None] * (9 - len(r))
272
- recs.append(r[:9])
273
- df = pd.DataFrame(recs, columns=cols)
274
- for c in cols:
275
- df[c] = pd.to_numeric(df[c], errors="coerce")
276
- df["confirm"] = df["confirm"].fillna(1)
277
- df = df.dropna(subset=["ts", "close"]).drop_duplicates("ts").sort_values("ts").reset_index(drop=True)
278
- df = df[df["close"] > 0].reset_index(drop=True)
279
- df["volq"] = df["volq"].where(df["volq"].notna(), df["close"] * df["vol"])
280
- df["date"] = pd.to_datetime(df["ts"], unit="ms", utc=True)
281
- return df.tail(n).reset_index(drop=True)
282
-
283
-
284
- def fetch_perp(inst: str) -> Tuple[Optional[float], Optional[float]]:
285
- """Perp funding rate (8h) ve open interest (USD). Perp yoksa (None, None)."""
286
- parts = inst.split("-")
287
- swap = f"{parts[0]}-{parts[1]}-SWAP" if len(parts) >= 2 else None
288
- if not swap:
289
- return None, None
290
- funding, oi_usd = None, None
291
- fr = okx_get("/api/v5/public/funding-rate", {"instId": swap})
292
- if fr:
293
- funding = num(fr[0].get("fundingRate"))
294
- if funding is not None:
295
- time.sleep(REQUEST_DELAY_SEC)
296
- oi = okx_get("/api/v5/public/open-interest", {"instType": "SWAP", "instId": swap})
297
- if oi:
298
- oi_usd = num(oi[0].get("oiUsd"))
299
- return funding, oi_usd
300
-
301
-
302
- def _tail(a, k, min_frac=0.6):
303
- return a[-k:] if len(a) > 0 and len(a) >= int(np.ceil(k * min_frac)) else None
304
-
305
-
306
- def metrics_from_candles(conf: pd.DataFrame, price: float, bid: Optional[float] = None, ask: Optional[float] = None) -> Dict[str, Any]:
307
- """Pure function: confirmed daily candles + live price -> derived [D] metrics."""
308
- c = conf["close"].to_numpy(float)
309
- h = conf["high"].to_numpy(float)
310
- vb = conf["vol"].to_numpy(float)
311
- vq = conf["volq"].to_numpy(float)
312
- n = len(c)
313
- m: Dict[str, Any] = {"hist_days": n}
314
- lr = np.diff(np.log(c)) if n >= 2 else np.array([])
315
- rets = c[1:] / c[:-1] - 1 if n >= 2 else np.array([])
316
-
317
- def ret_k(k):
318
- return price / c[-k] - 1 if n >= k else None
319
-
320
- m["ret30"], m["ret90"], m["ret365"] = ret_k(30), ret_k(90), ret_k(365)
321
-
322
- def vol_k(k):
323
- t = _tail(lr, k)
324
- return float(np.std(t, ddof=1) * np.sqrt(365)) if t is not None and len(t) >= 15 else None
325
-
326
- m["vol30"], m["vol90"], m["vol365"] = vol_k(30), vol_k(90), vol_k(365)
327
- m["sharpe"] = m["sortino"] = None
328
- t = _tail(lr, 365)
329
- if t is not None and len(t) >= 100:
330
- sd = float(np.std(t, ddof=1))
331
- if sd > 1e-12:
332
- m["sharpe"] = float(np.mean(t) / sd * np.sqrt(365))
333
- dd = float(np.sqrt(np.mean(np.minimum(t, 0) ** 2)))
334
- if dd > 1e-12:
335
- m["sortino"] = float(np.mean(t) / dd * np.sqrt(365))
336
- w = c[-365:]
337
- m["maxdd"] = None
338
- if len(w) >= 60:
339
- cs = np.append(w, price)
340
- m["maxdd"] = float(np.max(1 - cs / np.maximum.accumulate(cs)))
341
- m["hi365"], m["dd_high"] = None, None
342
- if n >= 30:
343
- hi = float(max(np.nanmax(h[-365:]), price))
344
- m["hi365"], m["dd_high"] = hi, 1 - price / hi
345
- m["sma50"] = float(np.mean(c[-50:])) if n >= 50 else None
346
- m["sma200"] = float(np.mean(c[-200:])) if n >= 200 else None
347
- m["vwap365"] = None
348
- if n >= 120:
349
- sb, sq = np.nansum(vb[-365:]), np.nansum(vq[-365:])
350
- if sb > 0 and sq > 0:
351
- m["vwap365"] = float(sq / sb)
352
- m["trend_fair"] = m["trend_g"] = m["trend_z"] = m["trend_sigma"] = None
353
- if n >= 120:
354
- mm = min(n, 500)
355
- y, x = np.log(c[-mm:]), np.arange(mm, dtype=float)
356
- slope, icpt = np.polyfit(x, y, 1)
357
- sigma = float(np.std(y - (icpt + slope * x), ddof=2))
358
- fit_now = icpt + slope * mm
359
- m["trend_fair"] = float(np.exp(fit_now))
360
- m["trend_g"] = float(np.exp(slope * 365) - 1)
361
- m["trend_sigma"] = sigma
362
- if sigma > 1e-9:
363
- m["trend_z"] = float((np.log(price) - fit_now) / sigma)
364
- q30, q90 = _tail(vq, 30), _tail(vq, 90)
365
- m["vq30"] = float(np.nanmean(q30)) if q30 is not None else None
366
- m["vq90"] = float(np.nanmean(q90)) if q90 is not None else None
367
- m["vol_ratio"] = div(m["vq30"], m["vq90"]) if (m["vq90"] or 0) > 0 else None
368
- m["vcv"] = float(np.nanstd(q90) / np.nanmean(q90)) if q90 is not None and np.nanmean(q90) > 0 else None
369
- r90 = _tail(rets, 90)
370
- m["worst90"] = float(np.min(r90)) if r90 is not None else None
371
- m["var95"] = float(-np.percentile(r90, 5)) if r90 is not None else None
372
- m["spread_bps"] = None
373
- if bid and ask and bid > 0 and ask > 0 and ask >= bid:
374
- m["spread_bps"] = (ask - bid) / ((ask + bid) / 2) * 1e4
375
- return m
376
-
377
-
378
- def fetch(inst: str, force: bool = False) -> Optional[Dict[str, Any]]:
379
- key = f"t_{inst}"
380
- if not force:
381
- c = _cget(key)
382
- if c is not None:
383
- return c
384
- try:
385
- df = fetch_candles(inst)
386
- tk = get_tickers().get(inst)
387
- if tk is None:
388
- rows = okx_get("/api/v5/market/ticker", {"instId": inst})
389
- tk = rows[0] if rows else None
390
- funding, oi_usd = fetch_perp(inst) if SETTINGS.get("perp", True) else (None, None)
391
- except Exception as e:
392
- logger.warning("%s fetch failed: %s", inst, e)
393
- return None
394
- if df is None or df.empty:
395
- return None
396
- conf = df[df["confirm"] >= 1].reset_index(drop=True)
397
- price = num(tk.get("last")) if tk else None
398
- if price is None:
399
- price = num(df["close"].iloc[-1])
400
- if price is None or price <= 0 or len(conf) < 10:
401
- return None
402
 
403
- flags: List[str] = []
404
- notes: List[str] = []
405
- mt = metrics_from_candles(conf, price, num(tk.get("bidPx")) if tk else None, num(tk.get("askPx")) if tk else None)
406
- n = mt["hist_days"]
407
- if tk is None:
408
- flags.append("ticker missing (price from last candle)")
409
- if n < 200:
410
- flags.append(f"short history ({n} days; SMA200 unavailable)")
411
- last_ts = int(conf["ts"].iloc[-1])
412
- last_dt = pd.to_datetime(last_ts, unit="ms", utc=True)
413
- age_h = (datetime.now(timezone.utc) - (last_dt + pd.Timedelta(days=1))).total_seconds() / 3600
414
- if age_h > 48:
415
- flags.append(f"stale candles ({age_h:.0f} h)")
416
- first_ts = int(conf["ts"].iloc[0])
417
- gaps = int(round((last_ts - first_ts) / 86_400_000)) + 1 - n
418
- if gaps > 3:
419
- flags.append(f"{gaps} missing daily candles")
420
- zero_days = int((conf["volq"].to_numpy(float)[-90:] == 0).sum())
421
- if zero_days > 3:
422
- flags.append(f"{zero_days} zero-volume days (last 90)")
423
- if abs(price / float(conf["close"].iloc[-1]) - 1) > 0.30:
424
- flags.append("live price deviates >30% from last close")
425
- if funding is None:
426
- notes.append("no perp (funding/OI) data")
427
-
428
- base = inst.split("-")[0]
429
- open24 = num(tk.get("open24h")) if tk else None
430
- vol24 = num(tk.get("volCcy24h")) if tk else None
431
- d = {
432
- "sym": inst, "name": base, "base": base, "price": price, "seg": CATEGORY.get(base, "Other"),
433
- "sector": tier_of(mt["vq30"] if mt["vq30"] is not None else vol24), "vol24": vol24,
434
- "chg24": (price / open24 - 1) if open24 else None, "funding": funding, "oi_usd": oi_usd,
435
- "flags": flags, "notes": notes, "unit_ok": True, "last_dt": last_dt, "age_h": age_h,
436
- "p_vwap": div(price, mt["vwap365"]), "p_sma200": div(price, mt["sma200"]),
437
- "p_trend": div(price, mt["trend_fair"]), "p_high": div(price, mt["hi365"]),
438
- "hist": conf[["date", "close"]].tail(365).reset_index(drop=True), "fetched": datetime.now().isoformat(),
439
- **mt,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
440
  }
441
- _cset(key, d)
442
- return d
443
 
444
 
445
- def cached_ds() -> List[Dict[str, Any]]:
446
- return [v for k, (ts, v) in _cache.items() if k.startswith("t_") and v is not None]
 
447
 
 
 
 
 
 
 
 
 
 
448
 
449
- MULTS = ["p_vwap", "p_sma200", "p_trend", "p_high", "ret90", "vol90", "sharpe"]
450
- POS_ONLY = {"p_vwap", "p_sma200", "p_trend", "p_high"}
 
 
 
451
 
 
 
 
452
 
453
- def build_medians(ds) -> Dict[str, Dict[str, Tuple[Optional[float], int]]]:
454
- groups: Dict[str, list] = {}
455
- for d in ds:
456
- groups.setdefault(d["seg"], []).append(d)
457
- med = {}
458
- for k, g in groups.items():
459
- m = {}
460
- for f in MULTS:
461
- v = [x[f] for x in g if x.get(f) is not None and (f not in POS_ONLY or (0 < x[f] < 300))]
462
- m[f] = (float(np.median(v)) if len(v) >= MIN_PEERS else None, len(v))
463
- med[k] = m
464
- return med
465
 
 
 
 
466
 
467
- # ============================================================================
468
- # VALUATION ENGINE (fiyat-bazlı fair value; fundamental yoktur)
469
- # ============================================================================
470
 
471
- def dep_value(anchor, g0, wacc, tg, years=5) -> Optional[Dict[str, float]]:
472
- """Discounted Expected Price: anchor fiyatı 5 yıl boyunca g0 -> tg doğrusal sönen drift ile büyütülür,
473
- gereken getiri (wacc) ile bugüne iskonto edilir. DCF'in nakit akışsız karşılığıdır."""
474
- if None in (anchor, g0, wacc, tg) or anchor <= 0 or wacc <= tg:
475
- return None
476
- p = anchor
477
- for t in range(1, years + 1):
478
- g = g0 + (tg - g0) * (t - 1) / (years - 1)
479
- p *= 1 + g
480
- return {"p_future": p, "per_unit": p / (1 + wacc) ** years}
481
-
482
-
483
- def assumptions(P, rg=None, rw=None):
484
- rg = P["rg"] if rg is None else rg
485
- rw = P["rw"] if rw is None else rw
486
- i = P["infl"]
487
- return (1 + rg) * (1 + i * P["pt"]) - 1, (1 + rw) * (1 + i) - 1, (1 + P["rtg"]) * (1 + i) - 1 # g0, wacc, tg (nominal)
488
-
489
-
490
- def anchor_of(d) -> Optional[float]:
491
- v = [x for x in (d.get("trend_fair"), d.get("vwap365"), d.get("sma200")) if x is not None and x > 0]
492
- return float(np.median(v)) if len(v) >= 2 else None
493
-
494
-
495
- def dep_try(anchor, g0, wacc, tg):
496
- if anchor is None:
497
- return None, "needs >=2 price anchors (trend / VWAP / SMA200)"
498
- if anchor <= 0:
499
- return None, "non-positive anchor"
500
- if wacc <= tg:
501
- return None, "discount rate <= terminal drift"
502
- r = dep_value(anchor, g0, wacc, tg)
503
- if r is None or r["per_unit"] <= 0:
504
- return None, "value <= 0"
505
- return r["per_unit"], f"anchor {fpx(anchor)} → {fpx(r['p_future'])} (5y) → discounted"
506
-
507
-
508
- def relative_value(d, med) -> Tuple[Optional[float], str]:
509
- m = med.get(d["seg"])
510
- if not m:
511
- return None, "no peer group"
512
- vals = []
513
- if m["p_vwap"][0] and d.get("vwap365"):
514
- vals.append(m["p_vwap"][0] * d["vwap365"])
515
- if m["p_sma200"][0] and d.get("sma200"):
516
- vals.append(m["p_sma200"][0] * d["sma200"])
517
- if m["p_trend"][0] and d.get("trend_fair"):
518
- vals.append(m["p_trend"][0] * d["trend_fair"])
519
- vals = [v for v in vals if v > 0]
520
- if not vals:
521
- return None, f"<{MIN_PEERS} valid peers"
522
- return float(np.mean(vals)), f"{len(vals)} category-median multiple(s)"
523
-
524
-
525
- def intrinsic(d, P, med, rg=None, rw=None):
526
- g0, wacc, tg = assumptions(P, rg, rw)
527
- m: Dict[str, Tuple[Optional[float], str]] = {}
528
- tf = d.get("trend_fair")
529
- m["Log-Trend Regression"] = ((tf, f"trend {fp(d.get('trend_g'))}/yıl, z={fm(d.get('trend_z'))}") if tf else (None, "needs >=120 days of candles"))
530
- vw = d.get("vwap365")
531
- m["VWAP 365D"] = ((vw, "hacim-ağırlıklı maliyet bazı vekili") if vw else (None, "needs >=120 days"))
532
- s2 = d.get("sma200")
533
- m["SMA 200D"] = ((s2, "200 günlük ortalama") if s2 else (None, "needs >=200 days"))
534
- m["Discounted Expected Price"] = dep_try(anchor_of(d), g0, wacc, tg)
535
- m["Relative"] = relative_value(d, med)
536
- vals = [v for v, _ in m.values() if v is not None and v > 0]
537
- return m, (float(np.median(vals)) if vals else None)
538
-
539
-
540
- def reverse_dcf(d, P):
541
- base = anchor_of(d)
542
- if base is None or base <= 0:
543
- return None, "anchor unavailable (needs >=2 of trend / VWAP / SMA200)"
544
- _, wacc0, tg = assumptions(P)
545
-
546
- def solve(wacc):
547
- if wacc <= tg:
548
- return None
549
- f = lambda g: dep_value(base, g, wacc, tg)["per_unit"]
550
- lo, hi = -0.9, 3.0
551
- if not (f(lo) <= d["price"] <= f(hi)):
552
- return None
553
- for _ in range(80):
554
- mid = (lo + hi) / 2
555
- lo, hi = (mid, hi) if f(mid) < d["price"] else (lo, mid)
556
- return (lo + hi) / 2
557
-
558
- rows = []
559
- for dw in (-0.03, 0.0, 0.03):
560
- rw = P["rw"] + dw
561
- w = (1 + rw) * (1 + P["infl"]) - 1
562
- g = solve(w)
563
- rows.append({"Real discount rate": fp(rw), "Nominal discount rate": fp(w), "Implied drift (nominal, yr1)": fp(g),
564
- "Implied (real)": fp(real_ret(g, P["infl"]))})
565
- return rows, "ok"
566
-
567
-
568
- def scenarios(d, P, med, probs):
569
- SC = [("Bear", -0.04, 0.03, 0.85), ("Base", 0.0, 0.0, 1.0), ("Bull", 0.04, -0.02, 1.10)]
570
- out, ok = [], True
571
- for (nm, dg, dw, mult), p in zip(SC, probs):
572
- _, iv = intrinsic(d, P, med, rg=P["rg"] + dg, rw=max(P["rw"] + dw, 0.0))
573
- tgt = None if iv is None else (d["price"] + P["conv"] * (iv - d["price"])) * mult
574
- ok &= tgt is not None
575
- out.append({"Scenario": nm, "Real growth": fp(P["rg"] + dg), "Real disc. rate": fp(max(P["rw"] + dw, 0)),
576
- "Multiple factor": mult, "Intrinsic": iv, "Target": tgt, "Prob": p})
577
- if not ok:
578
- return out, None
579
- pw = sum(o["Prob"] * o["Target"] for o in out)
580
- nom = pw / d["price"] - 1
581
- tot = nom # spot: temettü/gelir yok
582
- return out, dict(pw=pw, nominal=nom, total=tot, real=real_ret(tot, P["infl"]), real_price=real_ret(nom, P["infl"]), dy_known=True)
583
-
584
-
585
- def validate_probs(bear, base):
586
- bull = 100 - bear - base
587
- if bear < 0 or base < 0 or bull < -1e-9:
588
- return None
589
- return (bear / 100, base / 100, bull / 100)
590
-
591
-
592
- # ============================================================================
593
- # QUALITY, MARKET HEALTH, RISK
594
- # ============================================================================
595
-
596
- def quality(d):
597
- q = {k: d.get(k) for k in ("ret30", "ret90", "ret365", "vol30", "vol90", "vol365", "sharpe", "sortino", "maxdd", "dd_high",
598
- "trend_z", "spread_bps", "vq30", "vq90", "vol_ratio", "vcv", "worst90", "var95", "hist_days")}
599
- q["sma_gap"] = div(d.get("sma50"), d.get("sma200"))
600
- q["sma_gap"] = None if q["sma_gap"] is None else q["sma_gap"] - 1
601
- return q
602
-
603
-
604
- def market_health(d):
605
- """9 ikili bileşenli piyasa sağlığı skoru (Piotroski benzeri). Hesaplanamayan bileşen N/A."""
606
- def cmp(a, b, op):
607
- return None if a is None or b is None else op(a, b)
608
-
609
- fr = d.get("funding")
610
- comps = [
611
- ("1 Price > SMA200", cmp(d.get("price"), d.get("sma200"), lambda a, b: a > b)),
612
- ("2 SMA50 > SMA200", cmp(d.get("sma50"), d.get("sma200"), lambda a, b: a > b)),
613
- ("3 90d return > 0", cmp(d.get("ret90"), 0, lambda a, b: a > b)),
614
- ("4 Volatility falling (30d < 90d)", cmp(d.get("vol30"), d.get("vol90"), lambda a, b: a < b)),
615
- ("5 Volume not shrinking (30d >= 90d avg)", cmp(d.get("vq30"), d.get("vq90"), lambda a, b: a >= b)),
616
- ("6 Drawdown from 1Y high < 40%", cmp(d.get("dd_high"), 0.40, lambda a, b: a < b)),
617
- ("7 Sharpe (1Y) > 0", cmp(d.get("sharpe"), 0, lambda a, b: a > b)),
618
- ("8 Spread < 10 bps", cmp(d.get("spread_bps"), 10, lambda a, b: a < b)),
619
- ("9 Funding not crowded (|f| < 0.03%/8h)", None if fr is None else abs(fr) < 0.0003),
620
  ]
621
- n = sum(1 for _, v in comps if v is not None)
622
- return sum(1 for _, v in comps if v), n, comps
623
-
624
-
625
- def risk_engine(d, q, cov):
626
- fl = []
627
-
628
- def add(name, w, cond, detail):
629
- fl.append((name, w, None if cond is None else ("TRIGGERED" if cond else "OK"), detail))
630
-
631
- price, s2, fr = d.get("price"), d.get("sma200"), d.get("funding")
632
- add("Extreme volatility", 12, None if q["vol90"] is None else q["vol90"] > 1.2, "90d annualized vol > 120%")
633
- add("Deep drawdown", 12, None if q["dd_high"] is None else q["dd_high"] > 0.5, "price >50% below 1Y high")
634
- add("Downtrend", 10, None if s2 is None else price < s2, "price < SMA200")
635
- add("Thin liquidity", 10, None if q["vq30"] is None else q["vq30"] < 5e6, "30d avg daily volume < 5M USDT")
636
- add("Wide spread", 8, None if q["spread_bps"] is None else q["spread_bps"] > 15, "bid/ask spread > 15 bps")
637
- add("Volume collapse", 8, None if q["vol_ratio"] is None else q["vol_ratio"] < 0.5, "30d avg volume < 50% of 90d avg")
638
- add("Crash day", 8, None if q["worst90"] is None else q["worst90"] < -0.15, "worst daily return (90d) < -15%")
639
- add("Tail risk (VaR95)", 8, None if q["var95"] is None else q["var95"] > 0.08, "daily 95% VaR (90d) > 8%")
640
- add("Overextension", 8, anyc(None if q["trend_z"] is None else q["trend_z"] > 2,
641
- None if d.get("p_sma200") is None else d["p_sma200"] > 1.6),
642
- "trend z-score > 2 or price > 1.6x SMA200")
643
- add("Funding crowding", 6, None if fr is None else abs(fr) > 0.0005, "|funding| > 0.05% per 8h")
644
- add("Data reliability", 10, cov < 0.7 or bool(d["flags"]), f"coverage<70% or integrity flags ({len(d['flags'])})")
645
- ev_w = sum(w for _, w, st, _ in fl if st is not None)
646
- score = None if ev_w == 0 else 100 * sum(w for _, w, st, _ in fl if st == "TRIGGERED") / ev_w
647
- return score, fl
648
-
649
-
650
- # ============================================================================
651
- # SCORING & ANALYSIS
652
- # ============================================================================
653
-
654
- WEIGHTS = {"Majors": (.30, .25, .25, .20), "Meme": (.25, .15, .20, .40)}
655
- DEFAULT_W = (.30, .25, .20, .25)
656
-
657
-
658
- def coverage(d):
659
- f = [d.get("price"), d.get("vq30"), d.get("spread_bps"), d.get("sma50"), d.get("sma200"), d.get("vwap365"),
660
- d.get("trend_fair"), d.get("vol90"), d.get("sharpe"), d.get("maxdd"), d.get("ret90"), d.get("ret365")]
661
- return sum(x is not None for x in f) / len(f)
662
-
663
-
664
- def analyze(d, P, med, probs=(0.25, 0.5, 0.25)):
665
- A: Dict[str, Any] = {"d": d}
666
- infl = P["infl"]
667
- q = quality(d)
668
- A["q"] = q
669
- A["methods"], A["iv"] = intrinsic(d, P, med)
670
- A["mos"] = (A["iv"] - d["price"]) / A["iv"] if A["iv"] else None
671
- rv, A["rel_note"] = relative_value(d, med)
672
- A["rel_mos"] = (rv - d["price"]) / rv if rv else None
673
- A["pio"] = market_health(d)
674
- A["cov"] = coverage(d)
675
- A["risk"], A["flags"] = risk_engine(d, q, A["cov"])
676
- rret365, rret90 = real_ret(q["ret365"], infl), real_ret(q["ret90"], infl)
677
- liq = math.log10(q["vq30"]) if q["vq30"] and q["vq30"] > 0 else None
678
-
679
- vi = [(lin(A["mos"], -0.3, 0.5), 4), (lin(A["rel_mos"], -0.3, 0.5), 2), (lin(d.get("trend_z"), 2, -2), 2),
680
- (lin(d.get("p_sma200"), 1.6, 0.8), 1), (lin(q["dd_high"], 0.0, 0.6), 1)]
681
- qi = [(lin(rret365, -0.5, 1.5), 3), (lin(q["sharpe"], -0.5, 2.0), 3), (lin(q["sortino"], -0.5, 3.0), 2),
682
- (lin(q["maxdd"], 0.8, 0.25), 2), (lin(rret90, -0.3, 0.5), 1), (lin(q["vcv"], 2, 0.3), 1)]
683
- sc, n, _ = A["pio"]
684
- si = [(lin(liq, 6, 9), 3), (lin(q["spread_bps"], 30, 2), 2), (lin(q["hist_days"], 90, 500), 2),
685
- (lin(q["vol90"], 1.5, 0.4), 2), (lin(sc / n, 0.3, 0.8) if n >= 5 else None, 2)]
686
- A["val"], A["val_cov"] = wavg(vi)
687
- A["qual"], A["qual_cov"] = wavg(qi)
688
- A["str"], A["str_cov"] = wavg(si)
689
- w = WEIGHTS.get(d["seg"], DEFAULT_W)
690
- comps = [A["val"], A["qual"], A["str"], None if A["risk"] is None else 100 - A["risk"]]
691
- if A["val"] is None or A["qual"] is None:
692
- A["composite"] = None
693
- else:
694
- av = [(c, wt) for c, wt in zip(comps, w) if c is not None]
695
- A["composite"] = sum(c * wt for c, wt in av) / sum(wt for _, wt in av) * min(1.0, A["cov"] / 0.7)
696
- sev = any("stale" in f or "missing daily" in f or "deviates" in f for f in d["flags"])
697
- lvl = 2 if A["cov"] >= 0.85 else 1 if A["cov"] >= 0.65 else 0
698
- lvl = max(lvl - (1 if sev else 0), 0)
699
- A["conf"] = ["Low", "Medium", "High"][lvl]
700
- pr = validate_probs(probs[0] * 100, probs[1] * 100) or (0.25, 0.5, 0.25)
701
- A["scen"], A["exp"] = scenarios(d, P, med, pr)
702
- return A
703
-
704
-
705
- # ============================================================================
706
- # OUTPUT BUILDERS
707
- # ============================================================================
708
-
709
- def get_A(ticker, P, probs=(0.25, 0.5, 0.25)):
710
- sym = norm_sym(ticker)
711
- d = fetch(sym)
712
- if d is None:
713
- return None, f"⚠️ {sym or '?'} OKX'ten çekilemedi (parite yok, geçmiş yetersiz ya da ağ hatası)."
714
- return analyze(d, P, build_medians(cached_ds()), probs), None
715
-
716
-
717
- def price_fig(d, extra=None):
718
  try:
719
- h = d["hist"]
720
- if h is None or h.empty:
721
- return None
722
- f = go.Figure(go.Scatter(x=h["date"], y=h["close"], mode="lines", name="Close"))
723
- for nm, v in (extra or {}).items():
724
- if v:
725
- f.add_hline(y=v, line_dash="dot", annotation_text=nm)
726
- f.update_layout(title=f"{d['sym']} — 1Y (OKX, USDT)", template="plotly_white", height=380, margin=dict(l=10, r=10, t=40, b=10))
727
- return f
728
- except Exception:
729
- return None
730
 
731
 
732
- def scan_row(A):
733
- d, q = A["d"], A["q"]
734
- sc, n, _ = A["pio"]
735
- e = A["exp"]
736
- return {
737
- "Pair": d["sym"], "Segment": d["seg"], "Tier": d["sector"], "Price": rsig(d["price"]),
738
- "Composite": None if A["composite"] is None else round(A["composite"], 1),
739
- "Fair value": rsig(A["iv"]),
740
- "MoS %": None if A["mos"] is None else round(A["mos"] * 100, 1),
741
- "vs VWAP365 %": None if d["p_vwap"] is None else round((d["p_vwap"] - 1) * 100, 1),
742
- "Ret 90d %": pct(q["ret90"]), "Vol 90d %": pct(q["vol90"], 0),
743
- "Sharpe 1Y": None if q["sharpe"] is None else round(q["sharpe"], 2),
744
- "MaxDD 1Y %": pct(q["maxdd"], 0),
745
- "Health": f"{sc}/{n} (cov {n}/9)" if n else "N/A",
746
- "Risk": None if A["risk"] is None else round(A["risk"], 0),
747
- "Exp Return % (12M)": None if not e else round(e["total"] * 100, 1),
748
- "Real Exp Return %": None if not e or e["real"] is None else round(e["real"] * 100, 1),
749
- "Vol30d avg (M USDT)": None if q["vq30"] is None else round(q["vq30"] / 1e6, 1),
750
- "Funding % (8h)": None if d["funding"] is None else round(d["funding"] * 100, 4),
751
- "Coverage %": round(A["cov"] * 100), "Confidence": A["conf"],
752
- }
753
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
754
 
755
- def run_scan(force, extra, P, progress, n=DEFAULT_UNIVERSE_N):
756
- base, src = build_universe(int(n))
757
- syms = base + [norm_sym(x) for x in (extra or "").split(",") if x.strip()]
758
- syms = [s for s in dict.fromkeys(syms) if s]
759
- LAST["universe"] = syms
760
- ds, failed = [], []
761
- for i, s in enumerate(syms):
762
- progress((i + 1) / len(syms), desc=f"Fetching {s}")
763
- was_cached = (not force) and _cget(f"t_{s}") is not None
764
- d = fetch(s, force)
765
- (ds.append(d) if d else failed.append(s))
766
- if not was_cached:
767
- time.sleep(REQUEST_DELAY_SEC)
768
- med = build_medians(ds)
769
- rows = [scan_row(analyze(d, P, med)) for d in ds]
770
- df = pd.DataFrame(rows)
771
- LAST["df"], LAST["failed"] = df, failed
772
- return df, failed, src
773
-
774
-
775
- def apply_filters(seg, minc, minm, sortby, desc):
776
- df = LAST["df"]
777
- if df is None or df.empty:
778
- return None, None
779
- x = df.copy()
780
- if seg != "All":
781
- x = x[x["Segment"] == seg]
782
- x = x[x["Composite"].fillna(-1) >= minc]
783
- if minm > -100:
784
- x = x[x["MoS %"].fillna(-1e9) >= minm]
785
- x = x.sort_values(sortby, ascending=not desc, na_position="last")
786
- f = tempfile.NamedTemporaryFile(delete=False, suffix=".csv", prefix="okx_v1_")
787
- x.to_csv(f.name, index=False)
788
- return x, f.name
789
-
790
-
791
- def md_report(A, P):
792
- d = A["d"]
793
- L = [f"## {d['sym']} — OKX Spot",
794
- f"**{d['seg']}** · {d['sector']} · Fiyat **{fpx(d['price'])} USDT** · 24s {fp(d['chg24'])} · "
795
- f"Son kapanmış mum: {d['last_dt'].date() if d['last_dt'] is not None else 'N/A'}",
796
- f"**Composite {fm(A['composite'], 0)}** | Valuation {fm(A['val'], 0)} | Quality {fm(A['qual'], 0)} | "
797
- f"Strength {fm(A['str'], 0)} | Risk {fm(A['risk'], 0)} (düşük=iyi) | Coverage {fp(A['cov'], 0)} | "
798
- f"Confidence **{A['conf']}**",
799
- f"**Fair value [D]:** {fpx(A['iv'])} USDT · Margin of Safety {fp(A['mos'])}"]
800
- if d["funding"] is not None:
801
- L.append(f"**Perp [R]:** funding {fp(d['funding'], 4)} / 8s · Open interest {fm((d['oi_usd'] or 0) / 1e6, 1) if d['oi_usd'] else 'N/A'} M USD")
802
- L.append("\n| Yöntem | Değer | Not |\n|---|---|---|")
803
- for k, (v, n) in A["methods"].items():
804
- L.append(f"| {k} | {fpx(v)} | {n} |")
805
- if d["flags"]:
806
- L.append("\n**⚠️ Veri bayrakları:** " + "; ".join(d["flags"]))
807
- L.append("\n*Fair value = mevcut yöntemlerin medyanı (fiyat-hacim bazlı referans, fundamental değil). "
808
- "Skorlar evren içi göreceli, açıklanabilir heuristiklerdir.*")
809
- return "\n".join(L)
810
-
811
-
812
- def radar_fig(A):
813
- cats = ["Valuation", "Quality", "Strength", "Safety (100-Risk)"]
814
- v = [A["val"], A["qual"], A["str"], None if A["risk"] is None else 100 - A["risk"]]
815
- v = [x or 0 for x in v]
816
- f = go.Figure(go.Scatterpolar(r=v + v[:1], theta=cats + cats[:1], fill="toself"))
817
- f.update_layout(polar=dict(radialaxis=dict(range=[0, 100])), height=340, margin=dict(l=30, r=30, t=30, b=20))
818
- return f
819
-
820
-
821
- # ============================================================================
822
- # SELF TEST (synthetic)
823
- # ============================================================================
824
-
825
- def self_test() -> str:
826
- res = []
827
-
828
- def chk(name, cond):
829
- res.append(("✅ " if cond else "❌ ") + name)
830
-
831
- r = dep_value(100, 0, 0.10, 0)
832
- chk("DEP: g=0,tg=0, 10% → 100/1.1^5", r is not None and abs(r["per_unit"] - 100 / 1.1 ** 5) < 1e-9)
833
- chk("DEP refuses wacc<=tg", dep_value(100, 0.1, 0.05, 0.05) is None)
834
- chk("DEP refuses anchor<=0", dep_value(-5, 0.1, 0.2, 0.05) is None)
835
- chk("DEP refuses missing input", dep_value(None, 0.1, 0.2, 0.05) is None)
836
- chk("Real return 50% nom / 25% infl = 20%", abs(real_ret(0.5, 0.25) - 0.2) < 1e-12)
837
- chk("div by zero → None", div(1, 0) is None and div(None, 2) is None)
838
- P = dict(infl=0.30, rw=0.10, rtg=0.01, rg=0.03, pt=0.5, conv=0.5)
839
- g0, w, tg = assumptions(P)
840
- chk("Discount rate > terminal drift (defaults)", w > tg)
841
- chk("Probabilities 25/50 → 25/50/25 sum=1", validate_probs(25, 50) is not None and abs(sum(validate_probs(25, 50)) - 1) < 1e-9)
842
- chk("Probabilities 70/50 rejected", validate_probs(70, 50) is None)
843
- # reverse valuation consistency
844
- d = {"trend_fair": 100.0, "vwap365": 100.0, "sma200": 100.0, "price": 100 / 1.1 ** 5}
845
- P2 = dict(infl=0.0, rw=0.10, rtg=0.0, rg=0.0, pt=0.0, conv=0.5)
846
- rows, _ = reverse_dcf(d, P2)
847
- chk("Reverse: price = 100/1.1^5 → implied drift≈0",
848
- rows is not None and abs(float(rows[1]["Implied drift (nominal, yr1)"].rstrip("%"))) < 0.05)
849
- # market health incompleteness
850
- sc, n, _ = market_health({})
851
- chk("Market health all-missing → 0 available (not 9/9)", n == 0 and sc == 0)
852
- # peer rule
853
- ds = [{"seg": "X", "p_vwap": 1.0 + 0.1 * i, **{m: None for m in MULTS if m != "p_vwap"}} for i in range(4)]
854
- chk("Category median needs >=5 peers", build_medians(ds)["X"]["p_vwap"][0] is None)
855
- ds += [{"seg": "X", "p_vwap": 1.2, **{m: None for m in MULTS if m != "p_vwap"}}]
856
- chk("Category median with 5 peers computed", build_medians(ds)["X"]["p_vwap"][0] is not None)
857
- chk("Symbol normalisation", norm_sym("btc") == "BTC-USDT" and norm_sym("eth/usdt") == "ETH-USDT"
858
- and norm_sym("SOLUSDT") == "SOL-USDT" and norm_sym("btc-usdt-swap") == "BTC-USDT" and norm_sym("BTC-USDC") == "BTC-USDC")
859
- chk("Stablecoin / leveraged exclusion", is_excluded("USDC") and is_excluded("BTC3L") and not is_excluded("BTC"))
860
- # metrics on a deterministic exponential series (+0.1% / day)
861
- nd = 400
862
- close = 100 * 1.001 ** np.arange(nd)
863
- conf = pd.DataFrame({"close": close, "high": close * 1.01, "vol": np.full(nd, 1000.0), "volq": close * 1000.0})
864
- mt = metrics_from_candles(conf, float(close[-1] * 1.001), 99.9, 100.1)
865
- chk("Metrics: log-trend growth ≈ 1.001^365-1", mt["trend_g"] is not None and abs(mt["trend_g"] - (1.001 ** 365 - 1)) < 1e-6)
866
- chk("Metrics: monotonic series → max drawdown 0", mt["maxdd"] is not None and abs(mt["maxdd"]) < 1e-12)
867
- chk("Metrics: SMA200 = mean of last 200 closes", mt["sma200"] is not None and abs(mt["sma200"] - close[-200:].mean()) < 1e-9)
868
- chk("Metrics: spread 20 bps for 99.9/100.1", mt["spread_bps"] is not None and abs(mt["spread_bps"] - 20.0) < 0.01)
869
- chk("Metrics: 90d return", mt["ret90"] is not None and abs(mt["ret90"] - (close[-1] * 1.001 / close[-90] - 1)) < 1e-12)
870
- return "\n".join(res)
871
-
872
-
873
- # ============================================================================
874
- # GRADIO UI
875
- # ============================================================================
876
-
877
- SORT_COLS = ["Composite", "MoS %", "vs VWAP365 %", "Ret 90d %", "Vol 90d %", "Sharpe 1Y", "MaxDD 1Y %", "Risk",
878
- "Exp Return % (12M)", "Real Exp Return %", "Vol30d avg (M USDT)", "Coverage %"]
879
-
880
-
881
- def build_ui():
882
- with gr.Blocks(title="OKX Value Intelligence v1", theme=gr.themes.Soft()) as demo:
883
- gr.Markdown("# OKX Value Intelligence v1\n**⚠️ Veri: OKX public API (spot fiyat/hacim, günlük mumlar, perp funding/OI). "
884
- "Kripto varlıkların bilançosu yoktur: 'Fair value' fiyat-hacim verisinden türetilen istatistiksel referanstır, içsel değer değildir. "
885
- "Eksik veri `N/A` gösterilir, asla sıfırla doldurulmaz. 12 aylık senaryolar heuristiktir, istatistiksel tahmin değildir. "
886
- "Yatırım tavsiyesi değildir.**")
887
- with gr.Accordion("⚙️ Varsayımlar [A] (tüm sekmeler için)", open=False):
888
- with gr.Row():
889
- s_infl = gr.Slider(0, 20, 3, step=0.5, label="Enflasyon (%)")
890
- s_rw = gr.Slider(1, 40, 8, step=0.5, label="Reel iskonto / gereken getiri (%)")
891
- s_rtg = gr.Slider(0, 15, 3, step=0.25, label="Reel terminal drift (%)")
892
- with gr.Row():
893
- s_rg = gr.Slider(-30, 60, 8, step=0.5, label="Baz reel drift (%)")
894
- s_pt = gr.Slider(0, 100, 50, step=5, label="Enflasyonun nominal drift'e geçişi (%)")
895
- s_cv = gr.Slider(0, 100, 50, step=5, label="12 ay içinde değer-fiyat farkının kapanma oranı (%)")
896
- PI = [s_infl, s_rw, s_rtg, s_rg, s_pt, s_cv]
897
-
898
- with gr.Tabs():
899
- with gr.Tab("📊 Market Scanner"):
900
- with gr.Row():
901
- scan_btn = gr.Button("Run Scanner", variant="primary", scale=2)
902
- force = gr.Checkbox(label="Force refresh", value=False)
903
- perp = gr.Checkbox(label="Perp verisi (funding/OI)", value=True)
904
- with gr.Row():
905
- nuni = gr.Slider(20, 200, DEFAULT_UNIVERSE_N, step=10, label="Evren: 24s hacme göre ilk N OKX USDT paritesi")
906
- extra = gr.Textbox(label="Ek pariteler (virgülle, örn: PEPE,WIF,SOL-USDT)", value="")
907
- with gr.Row():
908
- f_seg = gr.Dropdown(["All"] + SEGMENTS, value="All", label="Segment")
909
- f_c = gr.Slider(0, 100, 0, step=5, label="Min composite")
910
- f_m = gr.Slider(-100, 100, -100, step=5, label="Min MoS % (-100 = kapalı)")
911
- with gr.Row():
912
- f_sort = gr.Dropdown(SORT_COLS, value="Composite", label="Sırala")
913
- f_desc = gr.Checkbox(label="Azalan", value=True)
914
- f_btn = gr.Button("Filtreyi uygula")
915
- status = gr.Markdown()
916
- table = gr.Dataframe(interactive=False, wrap=True)
917
- csv = gr.File(label="CSV indir")
918
- FI = [f_seg, f_c, f_m, f_sort, f_desc]
919
-
920
- def do_scan(frc, prp, nu, ext, *a, progress=gr.Progress()):
921
- P = mk_params(*a[:6])
922
- seg, c, m, so, de = a[6:]
923
- SETTINGS["perp"] = bool(prp)
924
- df, failed, src = run_scan(frc, ext, P, progress, nu)
925
- if df.empty:
926
- return None, None, "⚠️ Veri çekilemedi (OKX'e erişilemiyor olabilir)."
927
- x, path = apply_filters(seg, c, m, so, de)
928
- msg = f"✅ {len(df)} parite tarandı · evren: {src} · coverage ort. %{df['Coverage %'].mean():.0f}"
929
- if failed:
930
- msg += f" · ⚠️ Çekilemeyen ({len(failed)}): " + ", ".join(failed)
931
- return x, path, msg
932
-
933
- def do_filter(*a):
934
- x, path = apply_filters(*a)
935
- return x, path
936
-
937
- scan_btn.click(do_scan, [force, perp, nuni, extra] + PI + FI, [table, csv, status])
938
- f_btn.click(do_filter, FI, [table, csv])
939
-
940
- with gr.Tab("🔍 Asset Analysis"):
941
- with gr.Row():
942
- t1 = gr.Textbox(value="BTC", label="Parite (örn: BTC, ETH-USDT)", scale=2)
943
- b1 = gr.Button("Analyze", variant="primary")
944
- md1 = gr.Markdown()
945
- with gr.Row():
946
- p1 = gr.Plot()
947
- r1 = gr.Plot()
948
-
949
- def company(t, *a):
950
- P = mk_params(*a)
951
- A, err = get_A(t, P)
952
- if err:
953
- return err, None, None
954
- d = A["d"]
955
- return md_report(A, P), price_fig(d, {"Fair value": A["iv"], "VWAP365": d.get("vwap365"), "SMA200": d.get("sma200")}), radar_fig(A)
956
-
957
- b1.click(company, [t1] + PI, [md1, p1, r1])
958
-
959
- with gr.Tab("🧮 Fair Value Calculator"):
960
- with gr.Row():
961
- t2 = gr.Textbox(value="ETH", label="Parite", scale=2)
962
- b2 = gr.Button("Calculate", variant="primary")
963
- md2 = gr.Markdown()
964
- p2 = gr.Plot()
965
-
966
- def calc(t, *a):
967
- P = mk_params(*a)
968
- A, err = get_A(t, P)
969
- if err:
970
- return err, None
971
- d = A["d"]
972
- g0, w, tg = assumptions(P)
973
- L = [f"### {d['sym']} — {d['seg']}",
974
- f"**Varsayımlar [A]:** yıl-1 nominal drift {fp(g0)} (reel {fp(P['rg'])}, enflasyon geçişi %{P['pt'] * 100:.0f}) · "
975
- f"nominal iskonto {fp(w)} · nominal terminal drift {fp(tg)}",
976
- f"**Fiyat çıpaları [D]:** Log-trend {fpx(d.get('trend_fair'))} · VWAP365 {fpx(d.get('vwap365'))} · SMA200 {fpx(d.get('sma200'))} "
977
- f"→ DEP çıpası (medyan) **{fpx(anchor_of(d))}**",
978
- f"**Trend [D]:** yıllık log-trend drift {fp(d.get('trend_g'))} · fiyatın trende göre sapması z={fm(d.get('trend_z'))}σ"]
979
- L.append("\n| Yöntem | Değer | Not |\n|---|---|---|")
980
- for k, (v, n) in A["methods"].items():
981
- L.append(f"| {k} | {fpx(v)} | {n} |")
982
- L.append(f"\n**Fair value (medyan): {fpx(A['iv'])}** · Fiyat {fpx(d['price'])} · MoS {fp(A['mos'])}")
983
- names = [k for k, (v, _) in A["methods"].items() if v]
984
- vals = [A["methods"][k][0] for k in names]
985
- f = go.Figure(go.Bar(x=["Price"] + names + ["Fair value"], y=[d["price"]] + vals + [A["iv"] or 0]))
986
- f.update_layout(template="plotly_white", height=340, margin=dict(l=10, r=10, t=20, b=10))
987
- return "\n".join(L), f
988
-
989
- b2.click(calc, [t2] + PI, [md2, p2])
990
-
991
- with gr.Tab("↩️ Reverse Valuation"):
992
- with gr.Row():
993
- t3 = gr.Textbox(value="SOL", label="Parite", scale=2)
994
- b3 = gr.Button("Solve", variant="primary")
995
- md3 = gr.Markdown()
996
- tb3 = gr.Dataframe(interactive=False)
997
-
998
- def rev(t, *a):
999
- P = mk_params(*a)
1000
- A, err = get_A(t, P)
1001
- if err:
1002
- return err, None
1003
- d = A["d"]
1004
- rows, note = reverse_dcf(d, P)
1005
- if rows is None:
1006
- return f"**{d['sym']}**: ⚠️ {note}", None
1007
- g0, _, _ = assumptions(P)
1008
- md = (f"**{d['sym']}** fiyat {fpx(d['price'])} → piyasanın fiyatladığı yıl-1 drift (5 yıl, terminale doğru sönümlenir; çıpa {fpx(anchor_of(d))}).\n\n"
1009
- f"Mevcut varsayım yıl-1 drift: {fp(g0)} · Tarihsel log-trend drift (yıllık): {fp(d.get('trend_g'))} · "
1010
- f"Son 365g getiri: {fp(d.get('ret365'))}\n\n"
1011
- "Not: Zorunlu bir 'doğru' yoktur; implied drift makul görünmüyorsa piyasa beklentisi ya aşırı iyimser ya da kötümserdir.")
1012
- return md, pd.DataFrame(rows)
1013
-
1014
- b3.click(rev, [t3] + PI, [md3, tb3])
1015
-
1016
- with gr.Tab("💎 Market Quality"):
1017
- with gr.Row():
1018
- t4 = gr.Textbox(value="BTC", label="Parite", scale=2)
1019
- b4 = gr.Button("Show", variant="primary")
1020
- md4 = gr.Markdown()
1021
- tb4 = gr.Dataframe(interactive=False)
1022
- tb4b = gr.Dataframe(interactive=False)
1023
-
1024
- def qual(t, *a):
1025
- P = mk_params(*a)
1026
- A, err = get_A(t, P)
1027
- if err:
1028
- return err, None, None
1029
- q, d = A["q"], A["d"]
1030
- pc = lambda x: fp(x)
1031
- rows = [("Return 30d [D]", pc(q["ret30"])), ("Return 90d [D]", pc(q["ret90"])), ("Return 365d [D]", pc(q["ret365"])),
1032
- ("Return 365d (reel) [D]", pc(real_ret(q["ret365"], P["infl"]))),
1033
- ("Volatility 30d (ann.) [D]", pc(q["vol30"])), ("Volatility 90d (ann.) [D]", pc(q["vol90"])),
1034
- ("Volatility 365d (ann.) [D]", pc(q["vol365"])), ("Sharpe 1Y (rf=0) [D]", fm(q["sharpe"])),
1035
- ("Sortino 1Y [D]", fm(q["sortino"])), ("Max drawdown 1Y [D]", pc(q["maxdd"])),
1036
- ("Drawdown from 1Y high [D]", pc(q["dd_high"])), ("SMA50 / SMA200 gap [D]", pc(q["sma_gap"])),
1037
- ("Trend z-score [D]", fm(q["trend_z"])), ("Spread (bps) [R]", fm(q["spread_bps"], 1)),
1038
- ("Avg daily volume 30d (M USDT) [R]", fm(None if q["vq30"] is None else q["vq30"] / 1e6, 1)),
1039
- ("Volume 30d / 90d [D]", fm(q["vol_ratio"])), ("Volume variation (CV) [D]", fm(q["vcv"])),
1040
- ("Worst day (90d) [D]", pc(q["worst90"])), ("Daily VaR95 (90d) [D]", pc(q["var95"])),
1041
- ("Funding (8h) [R]", fp(d["funding"], 4)), ("History (days) [R]", fm(q["hist_days"], 0))]
1042
- sc, n, comps = A["pio"]
1043
- cp = [(c, "N/A" if v is None else ("1" if v else "0")) for c, v in comps]
1044
- md = (f"**{d['sym']}** — Market health: **{sc}/{n}** (hesaplanabilen {n}/9)" +
1045
- ("" if n == 9 else " ⚠️ eksik bileşen var; skor tam değildir.") +
1046
- (" Perp bulunmayan varlıklarda funding bileşeni N/A olur." if d["funding"] is None else ""))
1047
- return md, pd.DataFrame(rows, columns=["Metric", "Value"]), pd.DataFrame(cp, columns=["Health component", "Result"])
1048
-
1049
- b4.click(qual, [t4] + PI, [md4, tb4, tb4b])
1050
-
1051
- with gr.Tab("🛡️ Risk Analysis"):
1052
- with gr.Row():
1053
- t5 = gr.Textbox(value="DOGE", label="Parite", scale=2)
1054
- b5 = gr.Button("Assess", variant="primary")
1055
- md5 = gr.Markdown()
1056
- tb5 = gr.Dataframe(interactive=False)
1057
-
1058
- def risk_tab(t, *a):
1059
- P = mk_params(*a)
1060
- A, err = get_A(t, P)
1061
- if err:
1062
- return err, None
1063
- rows = [{"Flag": n, "Weight": w, "Status": s or "N/A", "Rule": dt} for n, w, s, dt in A["flags"]]
1064
- return (f"**{A['d']['sym']}** Risk skoru: **{fm(A['risk'], 0)}/100** (yalnızca değerlendirilebilen bayraklar üzerinden; "
1065
- f"düşük = iyi). Coverage {fp(A['cov'], 0)}."), pd.DataFrame(rows)
1066
-
1067
- b5.click(risk_tab, [t5] + PI, [md5, tb5])
1068
-
1069
- with gr.Tab("🏭 Category Comparison"):
1070
- with gr.Row():
1071
- t6 = gr.Textbox(value="SOL", label="Parite (opsiyonel)", scale=2)
1072
- b6 = gr.Button("Compare", variant="primary")
1073
- md6 = gr.Markdown()
1074
- tb6 = gr.Dataframe(interactive=False)
1075
- p6 = gr.Plot()
1076
-
1077
- def sector(t, *a):
1078
- P = mk_params(*a)
1079
- ds = cached_ds()
1080
- if not ds:
1081
- return "Önce Market Scanner'ı çalıştırın (medyanlar önbellekteki paritelerden hesaplanır).", None, None
1082
- med = build_medians(ds)
1083
- rows = []
1084
- for seg, m in sorted(med.items()):
1085
- r = {"Segment": seg, "Peers": sum(1 for d in ds if d["seg"] == seg)}
1086
- for f in MULTS:
1087
- v, n = m[f]
1088
- r[f] = None if v is None else round(v, 3)
1089
- rows.append(r)
1090
- fig, md = None, (f"Kategori medyanı için en az {MIN_PEERS} geçerli peer gerekir; yetersizse `None`. "
1091
- "p_vwap = fiyat/VWAP365, p_sma200 = fiyat/SMA200, p_trend = fiyat/log-trend, p_high = fiyat/1Y zirve.")
1092
- d = fetch(norm_sym(t)) if t.strip() else None
1093
- if d:
1094
- m = med.get(d["seg"])
1095
- keys = ["p_vwap", "p_sma200", "p_trend", "p_high"]
1096
- cv_ = [d[k] for k in keys]
1097
- mv = [m[k][0] if m else None for k in keys]
1098
- fig = go.Figure([go.Bar(name=d["sym"], x=keys, y=cv_), go.Bar(name="Category median", x=keys, y=mv)])
1099
- fig.update_layout(barmode="group", template="plotly_white", height=340, margin=dict(l=10, r=10, t=20, b=10))
1100
- md = f"**{d['sym']}** — {d['seg']} / {d['sector']}. " + md
1101
- return md, pd.DataFrame(rows), fig
1102
-
1103
- b6.click(sector, [t6] + PI, [md6, tb6, p6])
1104
-
1105
- with gr.Tab("🎯 12-Month Scenarios"):
1106
- with gr.Row():
1107
- t7 = gr.Textbox(value="BTC", label="Parite", scale=2)
1108
- pb = gr.Slider(0, 100, 25, step=5, label="Bear olasılığı (%)")
1109
- pm = gr.Slider(0, 100, 50, step=5, label="Base olasılığı (%) — Bull = kalan")
1110
- b7 = gr.Button("Run", variant="primary")
1111
- md7 = gr.Markdown()
1112
- tb7 = gr.Dataframe(interactive=False)
1113
- p7 = gr.Plot()
1114
-
1115
- def scen(t, bear, base, *a):
1116
- P = mk_params(*a)
1117
- pr = validate_probs(bear, base)
1118
- if pr is None:
1119
- return "⚠️ Bear + Base 100'ü aşamaz (Bull = 100 − Bear − Base).", None, None
1120
- A, err = get_A(t, P, pr)
1121
- if err:
1122
- return err, None, None
1123
- d, e = A["d"], A["exp"]
1124
- rows = [{"Scenario": o["Scenario"], "Real growth": o["Real growth"], "Real disc. rate": o["Real disc. rate"],
1125
- "Multiple factor": o["Multiple factor"], "Fair value": rsig(o["Intrinsic"]),
1126
- "12M target": rsig(o["Target"]), "Probability": f"{o['Prob'] * 100:.0f}%"} for o in A["scen"]]
1127
- if e is None:
1128
- return f"**{d['sym']}**: ⚠️ Senaryo üretilemedi (fair value hesaplanamadı).", pd.DataFrame(rows), None
1129
- md = (f"**{d['sym']}** · Olasılık-ağırlıklı hedef **{fpx(e['pw'])}** · Beklenen nominal fiyat getirisi {fp(e['nominal'])} · "
1130
- f"Reel getiri {fp(e['real'])} (spot: temettü/gelir yok, getiri yalnızca fiyat)\n\n"
1131
- f"*Heuristik: hedef = fiyat + k×(senaryo değeri − fiyat), k={P['conv'] * 100:.0f}%, çarpan faktörü uygulanır. "
1132
- "İstatistiksel olarak doğrulanmış tahmin değildir.*")
1133
- f = go.Figure(go.Bar(x=["Price", "Bear", "Base", "Bull", "Prob-weighted"],
1134
- y=[d["price"]] + [o["Target"] for o in A["scen"]] + [e["pw"]]))
1135
- f.update_layout(template="plotly_white", height=340, margin=dict(l=10, r=10, t=20, b=10))
1136
- return md, pd.DataFrame(rows), f
1137
-
1138
- b7.click(scen, [t7, pb, pm] + PI, [md7, tb7, p7])
1139
-
1140
- with gr.Tab("📋 Data Quality Monitor"):
1141
- b8 = gr.Button("Check Data Quality", variant="primary")
1142
- md8 = gr.Markdown()
1143
- tb8 = gr.Dataframe(interactive=False, wrap=True)
1144
-
1145
- def dq(progress=gr.Progress()):
1146
- uni = LAST["universe"] or build_universe(DEFAULT_UNIVERSE_N)[0]
1147
- rows, failed = [], []
1148
- for i, s in enumerate(uni):
1149
- progress((i + 1) / len(uni), desc=f"Checking {s}")
1150
- was = _cget(f"t_{s}") is not None
1151
- d = fetch(s)
1152
- if d is None:
1153
- failed.append(s)
1154
- continue
1155
- c = coverage(d)
1156
- rows.append({"Pair": d["sym"], "Segment": d["seg"], "Coverage %": round(c * 100),
1157
- "Last candle": d["last_dt"].date().isoformat() if d["last_dt"] is not None else "N/A",
1158
- "Age (h)": round(d["age_h"]), "History (days)": d["hist_days"],
1159
- "Status": "OK" if c >= 0.7 and not d["flags"] else "CHECK",
1160
- "Flags": "; ".join(d["flags"] + d["notes"]) or "—"})
1161
- if not was:
1162
- time.sleep(REQUEST_DELAY_SEC * 0.5)
1163
- df = pd.DataFrame(rows)
1164
- avg = f"{df['Coverage %'].mean():.0f}%" if not df.empty else "N/A"
1165
- return (f"Ortalama coverage: **{avg}** · Başarılı {len(rows)} · Çekilemeyen {len(failed)}: "
1166
- + (", ".join(failed) or "—")), df
1167
-
1168
- b8.click(dq, None, [md8, tb8])
1169
-
1170
- with gr.Tab("📐 Methodology"):
1171
- gr.Markdown(METHODOLOGY)
1172
- b9 = gr.Button("Run self-test (synthetic data)")
1173
- o9 = gr.Textbox(label="Self-test", lines=18, interactive=False)
1174
- b9.click(self_test, None, o9)
1175
- return demo
1176
-
1177
-
1178
- METHODOLOGY = """
1179
- ### Veri
1180
- - **Kaynak:** yalnızca OKX public API (anahtar gerekmez): spot ticker (fiyat, bid/ask, 24s hacim), 500 günlük **günlük mumlar** (UTC), perp **funding rate** ve **open interest**.
1181
- - **Evren:** 24s USDT hacmine göre ilk N OKX spot USDT paritesi (stablecoin ve kaldıraçlı tokenlar hariç, min 1M USDT/24s) + eklediğiniz pariteler. Ticker listesi alınamazsa gömülü yedek liste kullanılır.
1182
- - Metrikler yalnızca **kapanmış** günlük mumlardan hesaplanır; canlı fiyat ticker'dan gelir.
1183
-
1184
- ### Değerleme mimarisi (fundamental yoktur → fiyat-bazlı fair value)
1185
- - **Log-Trend Regression:** son ≤500 günün ln(fiyat) doğrusal regresyonunun bugünkü değeri; yıllık trend drift'i ve fiyatın trendden σ cinsinden sapması (z) raporlanır.
1186
- - **VWAP 365D:** hacim-ağırlıklı ortalama fiyat (yatırımcı maliyet bazı vekili). **SMA 200D:** uzun vadeli ortalama.
1187
- - **Discounted Expected Price (DEP):** DCF'in nakit akışsız karşılığı. Çıpa = (trend, VWAP, SMA200) medyanı; 5 yıl boyunca yıl-1 drift'inden terminal drift'e doğrusal sönen büyüme uygulanır, gereken getiri (iskonto) ile bugüne indirgenir. Çıpa eksikse veya iskonto ≤ terminal drift ise **hesaplanmaz**.
1188
- - **Relative:** kategori medyanı (en az 5 geçerli peer) p_vwap, p_sma200, p_trend çarpanları × varlığın kendi VWAP/SMA200/trend değeri.
1189
- - **Fair value = mevcut yöntemlerin medyanı.** MoS = (FV − fiyat)/FV. Bu bir içsel değer değil, istatistiksel referanstır.
1190
- - **Reverse Valuation:** fiyatı veren yıl-1 drift (bisection), iskonto oranı ±3 puan duyarlılığı.
1191
- - **Enflasyon:** iskonto ve terminal drift reel girilir, nominale çevrilir; enflasyonun yalnızca ayarlanabilir bir kısmı drift'e yansır. Reel getiri = (1+nominal)/(1+enflasyon) − 1.
1192
- - **Senaryolar:** Bear/Base/Bull = reel drift ∓4 puan, reel iskonto +3/−2 puan, çarpan faktörü 0.85/1.0/1.10; hedef = fiyat + k×(değer − fiyat). Olasılıklar ayarlanabilir, toplam %100 (Bull = kalan). Spot'ta temettü/gelir olmadığından getiri yalnızca fiyattır.
1193
-
1194
- ### Skorlama (0-100, açıklanabilir)
1195
- Composite = ağırlıklı ortalama (mevcut bileşenler üzerinden yeniden normalize) × min(1, coverage/0.7).
1196
- | Segment | Valuation | Quality | Strength | Risk (ters) |
1197
- |---|---|---|---|---|
1198
- | Majors | 30 | 25 | 25 | 20 |
1199
- | Meme | 25 | 15 | 20 | 40 |
1200
- | Diğer (Alt L1, DeFi, Infra/L2, AI/Gaming, Other) | 30 | 25 | 20 | 25 |
1201
-
1202
- - **Valuation:** MoS, göreli MoS, trend z-score, fiyat/SMA200, 1Y zirveden iskonto.
1203
- - **Quality:** reel 365g getiri, Sharpe, Sortino, max drawdown, reel 90g getiri, hacim istikrarı.
1204
- - **Strength:** likidite (30g ort. hacim), spread, geçmiş uzunluğu, volatilite rejimi, market health skoru.
1205
- - **Market Health (9 bileşen, Piotroski benzeri):** fiyat>SMA200, SMA50>SMA200, 90g getiri>0, volatilite düşüyor, hacim küçülmüyor, zirveden düşüş<%40, Sharpe>0, spread<10bps, funding kalabalık değil. Hesaplanamayan bileşen "N/A" olur ve skor `x/n` gösterilir.
1206
- - **Risk (0-100):** 11 bayrak (aşırı volatilite, derin drawdown, düşüş trendi, ince likidite, geniş spread, hacim çöküşü, crash günü, VaR95, aşırı uzama, funding kalabalığı, veri güvenilirliği); yalnızca değerlendirilebilen bayrakların ağırlığı üzerinden hesaplanır.
1207
-
1208
- ### Bilinen veri sınırlamaları
1209
- - Fundamental/on-chain/market-cap verisi yoktur; fair value yalnızca fiyat-hacim geçmişine dayanır ve geçmiş trendin sürmesini ima etmez.
1210
- - Yeni listelenen paritelerde geçmiş kısadır (SMA200/VWAP/trend hesaplanamayabilir → N/A, düşük coverage).
1211
- - Kategori sınıflaması gömülü bir haritaya dayanır; haritada olmayan varlıklar "Other" olur. Peer sayısı <5 ise medyan hesaplanmaz.
1212
- - OKX public API hız limitine tabidir; tarama sırasında istekler yavaşlatılır, 429/50011 hatalarında tekrar denenir.
1213
- - Sonuçlar araştırma/eğitim amaçlıdır, yatırım tavsiyesi değildir.
1214
- """
1215
 
1216
  if __name__ == "__main__":
1217
- logger.info("Self-test:\n%s", self_test())
1218
- build_ui().launch(server_name="0.0.0.0", server_port=7860)
 
1
+ # app.py
2
+ # GOSHAWK ALBION MARKET AI
3
+ # Tek dosyalık, çalıştırılabilir Market Intelligence uygulaması.
4
+ # Gerekli paketler: gradio, lightgbm, pandas, numpy, plotly, requests, scikit-learn
5
+
6
+ import os
 
 
 
 
 
 
 
 
7
  import time
8
+ import warnings
9
+ import datetime as dt
10
+ from typing import List, Dict, Tuple, Optional
11
 
 
12
  import numpy as np
13
  import pandas as pd
 
14
  import requests
15
+ import plotly.graph_objects as go
16
+ import plotly.express as px
17
+
18
+ import lightgbm as lgb
19
+ from sklearn.metrics import mean_absolute_error, mean_squared_error
20
+
21
+ import gradio as gr
22
 
23
+ warnings.filterwarnings("ignore")
24
+
25
+ # ---------------------------------------------------------------------------
26
+ # SABİTLER
27
+ # ---------------------------------------------------------------------------
28
+
29
+ API_BASE = "https://europe.albion-online-data.com/api/v2"
30
+
31
+ CITIES = [
32
+ "Bridgewatch",
33
+ "Fort Sterling",
34
+ "Lymhurst",
35
+ "Martlock",
36
+ "Thetford",
37
+ "Caerleon",
38
+ ]
39
+
40
+ # 100 popüler ürün (T4-T6 kaynaklar, ekipmanlar, tüketim malları)
41
+ _ITEM_POOL = [
42
+ "T4_BAG", "T5_BAG", "T6_BAG", "T7_BAG", "T8_BAG",
43
+ "T4_CAPE", "T5_CAPE", "T6_CAPE", "T7_CAPE", "T8_CAPE",
44
+ "T4_ARMOR_PLATE_SET1", "T5_ARMOR_PLATE_SET1", "T6_ARMOR_PLATE_SET1",
45
+ "T4_ARMOR_LEATHER_SET1", "T5_ARMOR_LEATHER_SET1", "T6_ARMOR_LEATHER_SET1",
46
+ "T4_ARMOR_CLOTH_SET1", "T5_ARMOR_CLOTH_SET1", "T6_ARMOR_CLOTH_SET1",
47
+ "T4_HEAD_PLATE_SET1", "T5_HEAD_PLATE_SET1", "T6_HEAD_PLATE_SET1",
48
+ "T4_HEAD_LEATHER_SET1", "T5_HEAD_LEATHER_SET1", "T6_HEAD_LEATHER_SET1",
49
+ "T4_HEAD_CLOTH_SET1", "T5_HEAD_CLOTH_SET1", "T6_HEAD_CLOTH_SET1",
50
+ "T4_SHOES_PLATE_SET1", "T5_SHOES_PLATE_SET1", "T6_SHOES_PLATE_SET1",
51
+ "T4_SHOES_LEATHER_SET1", "T5_SHOES_LEATHER_SET1", "T6_SHOES_LEATHER_SET1",
52
+ "T4_SHOES_CLOTH_SET1", "T5_SHOES_CLOTH_SET1", "T6_SHOES_CLOTH_SET1",
53
+ "T4_MAIN_SWORD", "T5_MAIN_SWORD", "T6_MAIN_SWORD",
54
+ "T4_MAIN_AXE", "T5_MAIN_AXE", "T6_MAIN_AXE",
55
+ "T4_MAIN_BOW", "T5_MAIN_BOW", "T6_MAIN_BOW",
56
+ "T4_MAIN_CROSSBOW", "T5_MAIN_CROSSBOW", "T6_MAIN_CROSSBOW",
57
+ "T4_MAIN_FIRE", "T5_MAIN_FIRE", "T6_MAIN_FIRE",
58
+ "T4_MAIN_FROST", "T5_MAIN_FROST", "T6_MAIN_FROST",
59
+ "T4_MAIN_HOLY", "T5_MAIN_HOLY", "T6_MAIN_HOLY",
60
+ "T4_MAIN_NATURE", "T5_MAIN_NATURE", "T6_MAIN_NATURE",
61
+ "T4_MAIN_CURSED", "T5_MAIN_CURSED", "T6_MAIN_CURSED",
62
+ "T4_MAIN_HAMMER", "T5_MAIN_HAMMER", "T6_MAIN_HAMMER",
63
+ "T4_MAIN_SPEAR", "T5_MAIN_SPEAR", "T6_MAIN_SPEAR",
64
+ "T4_MAIN_MACE", "T5_MAIN_MACE", "T6_MAIN_MACE",
65
+ "T4_OFF_SHIELD", "T5_OFF_SHIELD", "T6_OFF_SHIELD",
66
+ "T4_OFF_TORCH", "T5_OFF_TORCH", "T6_OFF_TORCH",
67
+ "T4_OFF_BOOK", "T5_OFF_BOOK", "T6_OFF_BOOK",
68
+ "T4_2H_STAFF", "T5_2H_STAFF", "T6_2H_STAFF",
69
+ "T4_2H_BOW", "T5_2H_BOW", "T6_2H_BOW",
70
+ "T4_2H_CROSSBOW", "T5_2H_CROSSBOW", "T6_2H_CROSSBOW",
71
+ "T4_2H_FIRE", "T5_2H_FIRE", "T6_2H_FIRE",
72
+ "T4_2H_FROST", "T5_2H_FROST", "T6_2H_FROST",
73
+ "T4_2H_HOLY", "T5_2H_HOLY", "T6_2H_HOLY",
74
+ "T4_2H_NATURE", "T5_2H_NATURE", "T6_2H_NATURE",
75
+ "T4_2H_CURSED", "T5_2H_CURSED", "T6_2H_CURSED",
76
+ "T4_2H_HAMMER", "T5_2H_HAMMER", "T6_2H_HAMMER",
77
+ "T4_2H_SPEAR", "T5_2H_SPEAR", "T6_2H_SPEAR",
78
+ "T4_2H_MACE", "T5_2H_MACE", "T6_2H_MACE",
79
+ ]
80
+ ITEM_IDS = list(dict.fromkeys(_ITEM_POOL))[:100]
81
+
82
+ def item_display_name(item_id: str) -> str:
83
+ return item_id.replace("_", " ").title()
84
+
85
+ SETUP_FEE_RATE = 0.025 # %2.5 listeleme ücreti
86
+ TRANSACTION_TAX_RATE = 0.08 # %8 satış vergisi (Premium olmayan varsayılan)
87
+
88
+ DATA_DIR = "gos_hawk_data"
89
+ os.makedirs(DATA_DIR, exist_ok=True)
90
+
91
+ MODEL_DIR = os.path.join(DATA_DIR, "models")
92
+ os.makedirs(MODEL_DIR, exist_ok=True)
93
+
94
+ PURGE_HOURS = 24
95
+ EMBARGO_HOURS = 24
96
+ MIN_TRAIN_SAMPLES = 500
97
+
98
+ REQUEST_TIMEOUT = 30
99
+ MAX_RETRIES = 3
100
+ RETRY_BACKOFF = 2
101
+
102
+ # ---------------------------------------------------------------------------
103
+ # YARDIMCI FONKSİYONLAR
104
+ # ---------------------------------------------------------------------------
105
+
106
+ def safe_request(url: str, params: Optional[dict] = None) -> Optional[list]:
107
+ for attempt in range(MAX_RETRIES):
108
+ try:
109
+ resp = requests.get(url, params=params, timeout=REQUEST_TIMEOUT)
110
+ if resp.status_code == 200:
111
+ return resp.json()
112
+ elif resp.status_code == 429:
113
+ time.sleep(RETRY_BACKOFF * (attempt + 1))
114
+ continue
115
+ else:
116
+ print(f"API hatası: {resp.status_code} - {url}")
117
+ return None
118
+ except requests.exceptions.Timeout:
119
+ print(f"Zaman aşımı (deneme {attempt+1}/{MAX_RETRIES}): {url}")
120
+ except requests.exceptions.RequestException as e:
121
+ print(f"İstek hatası (deneme {attempt+1}/{MAX_RETRIES}): {e}")
122
+ time.sleep(RETRY_BACKOFF * (attempt + 1))
123
  return None
124
 
125
 
126
+ def fetch_current_prices(item_ids: List[str], cities: List[str], qualities: str = "1") -> pd.DataFrame:
127
+ all_data = []
128
+ chunk_size = 50
129
+ for i in range(0, len(item_ids), chunk_size):
130
+ chunk = item_ids[i:i+chunk_size]
131
+ items_str = ",".join(chunk)
132
+ locations_str = ",".join(cities)
133
+ url = f"{API_BASE}/stats/prices/{items_str}.json"
134
+ params = {"locations": locations_str, "qualities": qualities}
135
+ data = safe_request(url, params)
136
+ if data:
137
+ all_data.extend(data)
138
+ time.sleep(0.2)
139
+ if not all_data:
140
+ return pd.DataFrame()
141
+ df = pd.DataFrame(all_data)
142
+ for col in ["sell_price_min_date", "sell_price_max_date", "buy_price_min_date", "buy_price_max_date"]:
143
+ if col in df.columns:
144
+ df[col] = pd.to_datetime(df[col], errors="coerce")
145
+ return df
146
+
147
+
148
+ def fetch_history(item_ids: List[str], cities: List[str], time_scale: int = 24, qualities: str = "1") -> pd.DataFrame:
149
+ all_records = []
150
+ chunk_size = 30
151
+ for i in range(0, len(item_ids), chunk_size):
152
+ chunk = item_ids[i:i+chunk_size]
153
+ items_str = ",".join(chunk)
154
+ locations_str = ",".join(cities)
155
+ url = f"{API_BASE}/stats/history/{items_str}.json"
156
+ params = {"locations": locations_str, "qualities": qualities, "time-scale": time_scale}
157
+ data = safe_request(url, params)
158
+ if data:
159
+ for entry in data:
160
+ item_id = entry.get("item_id")
161
+ loc = entry.get("location")
162
+ quality = entry.get("quality", 1)
163
+ for point in entry.get("data", []):
164
+ all_records.append({
165
+ "item_id": item_id,
166
+ "city": loc,
167
+ "quality": quality,
168
+ "timestamp": point.get("timestamp"),
169
+ "avg_price": point.get("avg_price"),
170
+ "item_count": point.get("item_count", 0),
171
+ })
172
+ time.sleep(0.3)
173
+ if not all_records:
174
+ return pd.DataFrame()
175
+ df = pd.DataFrame(all_records)
176
+ df["timestamp"] = pd.to_datetime(df["timestamp"], errors="coerce")
177
+ df["avg_price"] = pd.to_numeric(df["avg_price"], errors="coerce")
178
+ df["item_count"] = pd.to_numeric(df["item_count"], errors="coerce").fillna(0)
179
+ return df
180
+
181
+
182
+ def clean_market_data(df: pd.DataFrame) -> pd.DataFrame:
183
+ if df.empty:
184
+ return df
185
+ df = df[df["avg_price"] > 0].copy()
186
+ if "item_id" in df.columns:
187
+ for item in df["item_id"].unique():
188
+ mask = df["item_id"] == item
189
+ if mask.sum() < 10:
190
+ continue
191
+ lower = df.loc[mask, "avg_price"].quantile(0.005)
192
+ upper = df.loc[mask, "avg_price"].quantile(0.995)
193
+ df.loc[mask, "avg_price"] = df.loc[mask, "avg_price"].clip(lower, upper)
194
+ df = df.sort_values(["item_id", "city", "timestamp"]).reset_index(drop=True)
195
+ return df
196
 
197
 
198
+ def save_to_csv(df: pd.DataFrame, name: str) -> str:
199
+ if df.empty:
200
+ return ""
201
+ path = os.path.join(DATA_DIR, f"{name}_{dt.datetime.now().strftime('%Y%m%d_%H%M%S')}.csv")
202
+ df.to_csv(path, index=False)
203
+ return path
204
 
 
 
 
 
 
 
 
 
205
 
206
+ # ---------------------------------------------------------------------------
207
+ # FEATURE ENGINEERING
208
+ # ---------------------------------------------------------------------------
209
 
210
+ def compute_rsi(series: pd.Series, period: int = 14) -> pd.Series:
211
+ delta = series.diff()
212
+ gain = delta.clip(lower=0).rolling(period).mean()
213
+ loss = (-delta.clip(upper=0)).rolling(period).mean()
214
+ rs = gain / loss.replace(0, np.nan)
215
+ rsi = 100 - (100 / (1 + rs))
216
+ return rsi.fillna(50)
217
 
218
 
219
+ def build_features(df: pd.DataFrame) -> pd.DataFrame:
220
+ if df.empty:
221
+ return df
222
 
223
+ df = df.sort_values(["item_id", "city", "timestamp"]).copy()
224
 
225
+ df["hour"] = df["timestamp"].dt.hour
226
+ df["dayofweek"] = df["timestamp"].dt.dayofweek
227
+ df["is_weekend"] = (df["dayofweek"] >= 5).astype(int)
 
 
228
 
229
+ group_cols = ["item_id", "city"]
230
 
231
+ for lag in [1, 3, 6, 12, 24, 48]:
232
+ df[f"lag_{lag}h"] = df.groupby(group_cols)["avg_price"].shift(lag)
 
 
 
 
 
233
 
234
+ df["log_price"] = np.log(df["avg_price"].replace(0, np.nan))
235
+ for chg in [1, 6, 12, 24]:
236
+ df[f"chg_{chg}h"] = df.groupby(group_cols)["log_price"].shift(chg)
237
 
238
+ for window in [6, 12, 24, 48]:
239
+ df[f"ma_{window}h"] = df.groupby(group_cols)["avg_price"].transform(
240
+ lambda x: x.rolling(window, min_periods=window).mean()
241
+ )
242
 
243
+ df["rsi_14"] = df.groupby(group_cols)["avg_price"].transform(lambda x: compute_rsi(x, 14))
244
 
245
+ df["volatility_24h"] = df.groupby(group_cols)["log_price"].transform(
246
+ lambda x: x.rolling(24, min_periods=12).std()
247
+ )
248
 
249
+ df["volume_ma_24h"] = df.groupby(group_cols)["item_count"].transform(
250
+ lambda x: x.rolling(24, min_periods=12).mean()
251
+ )
252
+ df["volume_ratio"] = df["item_count"] / df["volume_ma_24h"].replace(0, np.nan)
253
 
254
+ city_avg = df.groupby(["item_id", "timestamp"])["avg_price"].transform("mean")
255
+ df["city_price_deviation"] = (df["avg_price"] - city_avg) / city_avg.replace(0, np.nan)
 
 
 
 
 
 
 
 
 
 
256
 
257
+ df["target_return_24h"] = df.groupby(group_cols)["log_price"].shift(-24)
258
 
259
+ return df
 
260
 
261
 
262
+ # ---------------------------------------------------------------------------
263
+ # MODEL EĞİTİMİ
264
+ # ---------------------------------------------------------------------------
265
 
266
+ FEATURE_COLS = [
267
+ "hour", "dayofweek", "is_weekend",
268
+ "lag_1h", "lag_3h", "lag_6h", "lag_12h", "lag_24h", "lag_48h",
269
+ "chg_1h", "chg_6h", "chg_12h", "chg_24h",
270
+ "ma_6h", "ma_12h", "ma_24h", "ma_48h",
271
+ "rsi_14", "volatility_24h",
272
+ "volume_ma_24h", "volume_ratio",
273
+ "city_price_deviation",
274
+ "item_code", "city_code",
275
+ ]
276
 
 
 
277
 
278
+ def prepare_model_data(featured_df: pd.DataFrame) -> Tuple[pd.DataFrame, pd.Series, pd.DataFrame]:
279
+ if featured_df.empty:
280
+ return pd.DataFrame(), pd.Series(dtype=float), pd.DataFrame()
281
 
282
+ df = featured_df.copy()
283
+ df["item_code"] = df["item_id"].astype("category").cat.codes
284
+ df["city_code"] = df["city"].astype("category").cat.codes
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
285
 
286
+ model_df = df.dropna(subset=FEATURE_COLS + ["target_return_24h"]).copy()
287
 
288
+ if len(model_df) > 100:
289
+ lower = model_df["target_return_24h"].quantile(0.001)
290
+ upper = model_df["target_return_24h"].quantile(0.999)
291
+ model_df["target_return_24h"] = model_df["target_return_24h"].clip(lower, upper)
 
 
 
 
292
 
293
+ X = model_df[FEATURE_COLS]
294
+ y = model_df["target_return_24h"]
295
+ meta = model_df[["item_id", "city", "timestamp", "avg_price", "target_return_24h"]].copy()
296
 
297
+ return X, y, meta
 
298
 
299
 
300
+ def train_global_model(X: pd.DataFrame, y: pd.Series) -> Optional[lgb.LGBMRegressor]:
301
+ if len(X) < MIN_TRAIN_SAMPLES:
302
+ print(f"Yetersiz veri: {len(X)} < {MIN_TRAIN_SAMPLES}")
303
+ return None
304
+
305
+ split_idx = int(len(X) * 0.8)
306
+ X_train, X_val = X.iloc[:split_idx], X.iloc[split_idx:]
307
+ y_train, y_val = y.iloc[:split_idx], y.iloc[split_idx:]
308
+
309
+ model = lgb.LGBMRegressor(
310
+ objective="regression",
311
+ metric="mae",
312
+ boosting_type="gbdt",
313
+ num_leaves=31,
314
+ learning_rate=0.05,
315
+ n_estimators=500,
316
+ feature_fraction=0.8,
317
+ bagging_fraction=0.8,
318
+ bagging_freq=5,
319
+ verbose=-1,
320
+ random_state=42,
321
+ n_jobs=-1,
322
+ )
323
+
324
+ callbacks = [
325
+ lgb.early_stopping(stopping_rounds=50, verbose=False),
326
+ lgb.log_evaluation(period=0),
327
+ ]
328
+
329
+ model.fit(
330
+ X_train, y_train,
331
+ eval_set=[(X_val, y_val)],
332
+ eval_metric="mae",
333
+ callbacks=callbacks,
334
+ )
335
+
336
+ return model
337
+
338
+
339
+ def predict_24h_returns(model: lgb.LGBMRegressor, latest_features: pd.DataFrame) -> pd.DataFrame:
340
+ if latest_features.empty or model is None:
341
+ return pd.DataFrame()
342
+
343
+ missing = [c for c in FEATURE_COLS if c not in latest_features.columns]
344
+ if missing:
345
+ print(f"Eksik özellikler: {missing}")
346
+ return pd.DataFrame()
347
+
348
+ X_pred = latest_features[FEATURE_COLS].dropna()
349
+ if X_pred.empty:
350
+ return pd.DataFrame()
351
+
352
+ preds = model.predict(X_pred)
353
+ result = latest_features.loc[X_pred.index, ["item_id", "city", "avg_price", "item_count"]].copy()
354
+ result["predicted_log_return_24h"] = preds
355
+ result["predicted_return_pct"] = (np.exp(preds) - 1) * 100
356
+ result["predicted_price_24h"] = result["avg_price"] * np.exp(preds)
357
+
358
+ return result
359
+
360
+
361
+ # ---------------------------------------------------------------------------
362
+ # WALK-FORWARD BACKTEST
363
+ # ---------------------------------------------------------------------------
364
+
365
+ def walk_forward_backtest(featured_df: pd.DataFrame, n_splits: int = 3) -> Dict:
366
+ if featured_df.empty:
367
+ return {"error": "Veri yok."}
368
+
369
+ df = featured_df.sort_values("timestamp").copy()
370
+ unique_times = df["timestamp"].sort_values().unique()
371
+
372
+ if len(unique_times) < 100:
373
+ return {"error": "Backtest için yeterli zaman noktası yok."}
374
+
375
+ test_size = len(unique_times) // (n_splits + 1)
376
+ results = []
377
+ all_predictions = []
378
+
379
+ for i in range(n_splits):
380
+ test_start_idx = test_size * (i + 1) + PURGE_HOURS
381
+ test_end_idx = test_start_idx + test_size
382
+ if test_end_idx > len(unique_times):
383
+ break
384
+
385
+ train_end_idx = test_start_idx - EMBARGO_HOURS - PURGE_HOURS
386
+ if train_end_idx <= 0:
387
  continue
388
+
389
+ train_times = unique_times[:train_end_idx]
390
+ test_times = unique_times[test_start_idx:test_end_idx]
391
+
392
+ train_df = df[df["timestamp"].isin(train_times)]
393
+ test_df = df[df["timestamp"].isin(test_times)]
394
+
395
+ if len(train_df) < MIN_TRAIN_SAMPLES or len(test_df) < 50:
396
  continue
 
 
 
 
 
397
 
398
+ X_train, y_train, _ = prepare_model_data(train_df)
399
+ X_test, y_test, meta_test = prepare_model_data(test_df)
400
 
401
+ if len(X_train) < MIN_TRAIN_SAMPLES or len(X_test) < 10:
402
+ continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
403
 
404
+ model = lgb.LGBMRegressor(
405
+ objective="regression",
406
+ metric="mae",
407
+ num_leaves=31,
408
+ learning_rate=0.05,
409
+ n_estimators=300,
410
+ verbose=-1,
411
+ random_state=42,
412
+ n_jobs=-1,
413
+ )
414
+ model.fit(X_train, y_train)
415
+
416
+ preds = model.predict(X_test)
417
+
418
+ mae = mean_absolute_error(y_test, preds)
419
+ rmse = np.sqrt(mean_squared_error(y_test, preds))
420
+ direction_correct = np.mean((y_test > 0) == (preds > 0))
421
+
422
+ baseline_preds = np.zeros_like(y_test)
423
+ baseline_mae = mean_absolute_error(y_test, baseline_preds)
424
+ baseline_rmse = np.sqrt(mean_squared_error(y_test, baseline_preds))
425
+
426
+ results.append({
427
+ "split": i + 1,
428
+ "train_samples": len(X_train),
429
+ "test_samples": len(X_test),
430
+ "mae": round(float(mae), 6),
431
+ "rmse": round(float(rmse), 6),
432
+ "direction_accuracy": round(float(direction_correct), 4),
433
+ "baseline_mae": round(float(baseline_mae), 6),
434
+ "baseline_rmse": round(float(baseline_rmse), 6),
435
+ "mae_improvement_pct": round(float((1 - mae / baseline_mae) * 100), 2) if baseline_mae > 0 else 0.0,
436
+ })
437
+
438
+ test_meta = meta_test.copy()
439
+ test_meta["predicted_return"] = preds
440
+ test_meta["actual_return"] = y_test.values
441
+ all_predictions.append(test_meta)
442
+
443
+ if not results:
444
+ return {"error": "Backtest tamamlanamadı (yeterli veri yok)."}
445
+
446
+ if all_predictions:
447
+ sim_df = pd.concat(all_predictions)
448
+ total_cost_rate = SETUP_FEE_RATE + TRANSACTION_TAX_RATE
449
+ sim_df["net_return"] = sim_df["actual_return"] - total_cost_rate
450
+ sim_df["gross_return"] = sim_df["actual_return"]
451
+ sim_df["trade_signal"] = sim_df["predicted_return"] > 0
452
+ sim_trades = sim_df[sim_df["trade_signal"]].copy()
453
+
454
+ if len(sim_trades) > 0:
455
+ avg_net_return = sim_trades["net_return"].mean()
456
+ win_rate = (sim_trades["net_return"] > 0).mean()
457
+ total_trades = len(sim_trades)
458
+ profitable_trades = int((sim_trades["net_return"] > 0).sum())
459
+ simulation = {
460
+ "total_trades": int(total_trades),
461
+ "avg_net_return_per_trade_pct": round(float(avg_net_return * 100), 4),
462
+ "win_rate_pct": round(float(win_rate * 100), 2),
463
+ "profitable_trades": profitable_trades,
464
+ }
465
+ else:
466
+ simulation = {"total_trades": 0, "avg_net_return_per_trade_pct": 0.0, "win_rate_pct": 0.0, "profitable_trades": 0}
467
+ else:
468
+ simulation = {}
469
+
470
+ return {
471
+ "results": results,
472
+ "simulation": simulation,
473
+ "n_splits_completed": len(results),
474
  }
 
 
475
 
476
 
477
+ # ---------------------------------------------------------------------------
478
+ # VERİ HATTI
479
+ # ---------------------------------------------------------------------------
480
 
481
+ class DataPipeline:
482
+ def __init__(self):
483
+ self.history_df = pd.DataFrame()
484
+ self.prices_df = pd.DataFrame()
485
+ self.featured_df = pd.DataFrame()
486
+ self.model = None
487
+ self.last_update = None
488
+ self.status_message = "Veri yüklenmedi."
489
+ self._backtest_cache = None
490
 
491
+ def fetch_data(self, item_ids: List[str] = None, cities: List[str] = None):
492
+ if item_ids is None:
493
+ item_ids = ITEM_IDS
494
+ if cities is None:
495
+ cities = CITIES
496
 
497
+ self.status_message = "API'den veri çekiliyor..."
498
+ hist = fetch_history(item_ids, cities)
499
+ prices = fetch_current_prices(item_ids, cities)
500
 
501
+ if hist.empty:
502
+ self.status_message = "Tarihsel veri alınamadı. API bağlantısını kontrol edin."
503
+ return
 
 
 
 
 
 
 
 
 
504
 
505
+ self.history_df = clean_market_data(hist)
506
+ self.prices_df = prices
507
+ self.status_message = f"{len(self.history_df)} satır tarihsel veri, {len(self.prices_df)} anlık fiyat alındı."
508
 
509
+ save_to_csv(self.history_df, "history")
510
+ save_to_csv(self.prices_df, "prices")
 
511
 
512
+ self.featured_df = build_features(self.history_df)
513
+ self._backtest_cache = None
514
+
515
+ def train_model(self):
516
+ if self.featured_df.empty:
517
+ self.status_message = "Önce veri çekmelisiniz."
518
+ return False
519
+
520
+ X, y, _ = prepare_model_data(self.featured_df)
521
+ if len(X) < MIN_TRAIN_SAMPLES:
522
+ self.status_message = f"Yetersiz örnek: {len(X)} (min {MIN_TRAIN_SAMPLES})"
523
+ return False
524
+
525
+ self.model = train_global_model(X, y)
526
+ if self.model is None:
527
+ self.status_message = "Model eğitimi başarısız."
528
+ return False
529
+
530
+ self.status_message = f"Model eğitildi ({len(X)} örnek)."
531
+ self.last_update = dt.datetime.now()
532
+ return True
533
+
534
+ def get_latest_predictions(self) -> pd.DataFrame:
535
+ if self.featured_df.empty or self.model is None:
536
+ return pd.DataFrame()
537
+
538
+ latest = self.featured_df.sort_values("timestamp").groupby(["item_id", "city"]).tail(1).copy()
539
+ latest["item_code"] = latest["item_id"].astype("category").cat.codes
540
+ latest["city_code"] = latest["city"].astype("category").cat.codes
541
+
542
+ preds = predict_24h_returns(self.model, latest)
543
+
544
+ if not preds.empty and not self.prices_df.empty:
545
+ price_info = self.prices_df[["item_id", "city", "sell_price_min", "buy_price_max"]].copy()
546
+ preds = preds.merge(price_info, on=["item_id", "city"], how="left")
547
+ preds["spread"] = preds["sell_price_min"] - preds["buy_price_max"]
548
+ else:
549
+ preds["spread"] = np.nan
550
+
551
+ preds["confidence"] = (1.0 - preds["volatility_24h"].clip(0, 0.2) / 0.2).fillna(0.5).clip(0, 1)
552
+
553
+ total_cost = SETUP_FEE_RATE + TRANSACTION_TAX_RATE
554
+ preds["net_return_pct"] = preds["predicted_return_pct"] - (total_cost * 100)
555
+
556
+ return preds
557
+
558
+ def get_backtest(self):
559
+ if self._backtest_cache is None:
560
+ self._backtest_cache = walk_forward_backtest(self.featured_df)
561
+ return self._backtest_cache
562
+
563
+
564
+ pipeline = DataPipeline()
565
+
566
+
567
+ # ---------------------------------------------------------------------------
568
+ # GRADIO YARDIMCI FONKSİYONLARI
569
+ # ---------------------------------------------------------------------------
570
+
571
+ def get_dashboard_metrics():
572
+ if pipeline.featured_df.empty:
573
+ return pd.DataFrame({"Metrik": ["Durum"], "Değer": ["Veri yok"]})
574
+
575
+ df = pipeline.featured_df
576
+ metrics = {
577
+ "Toplam Gözlem": len(df),
578
+ "Ürün Sayısı": df["item_id"].nunique(),
579
+ "Şehir Sayısı": df["city"].nunique(),
580
+ "Tarih Aralığı": f"{df['timestamp'].min().date()} - {df['timestamp'].max().date()}",
581
+ "Ortalama Fiyat": f"{df['avg_price'].mean():.0f} silver",
582
+ "Ortalama Hacim": f"{df['item_count'].mean():.1f}",
583
+ "Model Durumu": "Eğitildi" if pipeline.model else "Eğitilmedi",
584
+ }
585
+ return pd.DataFrame(list(metrics.items()), columns=["Metrik", "Değer"])
586
+
587
+
588
+ def get_analysis_table():
589
+ if pipeline.featured_df.empty:
590
+ return pd.DataFrame()
591
+
592
+ latest = pipeline.featured_df.sort_values("timestamp").groupby(["item_id", "city"]).tail(1).copy()
593
+
594
+ if not pipeline.prices_df.empty:
595
+ latest = latest.merge(
596
+ pipeline.prices_df[["item_id", "city", "sell_price_min", "buy_price_max"]],
597
+ on=["item_id", "city"], how="left"
598
+ )
599
+
600
+ latest["Ürün"] = latest["item_id"].apply(item_display_name)
601
+ latest["Şehir"] = latest["city"]
602
+ latest["Mevcut Fiyat"] = latest["avg_price"].round(0)
603
+ latest["Hacim (24s)"] = latest["item_count"].fillna(0).astype(int)
604
+ latest["Spread"] = (latest.get("sell_price_min", np.nan) - latest.get("buy_price_max", np.nan)).round(0)
605
+ latest["Likidite"] = pd.cut(latest["item_count"], bins=[-1, 10, 50, 200, np.inf], labels=["Düşük", "Orta", "İyi", "Yüksek"])
606
+
607
+ cols = ["Ürün", "Şehir", "Mevcut Fiyat", "Hacim (24s)", "Spread", "Likidite"]
608
+ return latest[cols].sort_values("Hacim (24s)", ascending=False).head(100)
609
+
610
+
611
+ def get_prediction_table():
612
+ preds = pipeline.get_latest_predictions()
613
+ if preds.empty:
614
+ return pd.DataFrame()
615
+
616
+ preds["Ürün"] = preds["item_id"].apply(item_display_name)
617
+ preds["Şehir"] = preds["city"]
618
+ preds["Mevcut Fiyat"] = preds["avg_price"].round(0)
619
+ preds["Tahmini Fiyat (24s)"] = preds["predicted_price_24h"].round(0)
620
+ preds["Tahmini Değişim %"] = preds["predicted_return_pct"].round(2)
621
+ preds["Güvenilirlik"] = (preds["confidence"] * 100).round(0).astype(int).astype(str) + "%"
622
+ preds["Spread"] = preds["spread"].round(0)
623
+ preds["Net Getiri % (maliyet sonrası)"] = preds["net_return_pct"].round(2)
624
+ preds["Likidite"] = pd.cut(preds["item_count"], bins=[-1, 10, 50, 200, np.inf], labels=["Düşük", "Orta", "İyi", "Yüksek"])
625
+
626
+ cols = [
627
+ "Ürün", "Şehir", "Mevcut Fiyat", "Tahmini Fiyat (24s)",
628
+ "Tahmini Değişim %", "Güvenilirlik", "Spread", "Likidite",
629
+ "Net Getiri % (maliyet sonrası)"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
630
  ]
631
+ return preds[cols].sort_values("Tahmini Değişim %", ascending=False)
632
+
633
+
634
+ def get_arbitrage_table():
635
+ if pipeline.prices_df.empty:
636
+ return pd.DataFrame()
637
+
638
+ prices = pipeline.prices_df.copy()
639
+ prices = prices[prices["sell_price_min"] > 0]
640
+
641
+ buy = prices.loc[prices.groupby("item_id")["sell_price_min"].idxmin()][["item_id", "city", "sell_price_min"]]
642
+ buy.columns = ["item_id", "buy_city", "buy_price"]
643
+
644
+ sell = prices.loc[prices.groupby("item_id")["buy_price_max"].idxmax()][["item_id", "city", "buy_price_max"]]
645
+ sell.columns = ["item_id", "sell_city", "sell_price"]
646
+
647
+ arb = buy.merge(sell, on="item_id")
648
+ arb = arb[arb["sell_price"] > arb["buy_price"]].copy()
649
+
650
+ # Alışta setup fee, satışta setup fee + transaction tax
651
+ arb["brüt_kâr"] = arb["sell_price"] - arb["buy_price"]
652
+ arb["toplam_maliyet"] = (
653
+ arb["buy_price"] * SETUP_FEE_RATE
654
+ + arb["sell_price"] * (SETUP_FEE_RATE + TRANSACTION_TAX_RATE)
655
+ )
656
+ arb["net_kâr"] = arb["brüt_kâr"] - arb["toplam_maliyet"]
657
+ arb["kâr_marjı_%"] = (arb["net_kâr"] / arb["buy_price"]) * 100
658
+
659
+ arb = arb[arb["net_kâr"] > 0].sort_values("kâr_marjı_%", ascending=False)
660
+
661
+ arb["Ürün"] = arb["item_id"].apply(item_display_name)
662
+ arb["Alış Şehri"] = arb["buy_city"]
663
+ arb["Satış Şehri"] = arb["sell_city"]
664
+ arb["Alış Fiyatı"] = arb["buy_price"].round(0)
665
+ arb["Satış Fiyatı"] = arb["sell_price"].round(0)
666
+ arb["Net Kâr"] = arb["net_kâr"].round(0)
667
+ arb["Kâr Marjı %"] = arb["kâr_marjı_%"].round(1)
668
+
669
+ cols = ["Ürün", "Alış Şehri", "Satış Şehri", "Alış Fiyatı", "Satış Fiyatı", "Net Kâr", "Kâr Marjı %"]
670
+ return arb[cols].head(50)
671
+
672
+
673
+ def get_model_performance_plot():
674
+ if pipeline.model is None:
675
+ return go.Figure().update_layout(title="Model eğitilmedi", template="plotly_dark")
676
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
677
  try:
678
+ importance = pipeline.model.feature_importances_
679
+ feature_names = pipeline.model.feature_name_
680
+ df_imp = pd.DataFrame({"Özellik": feature_names, "Önem": importance}).sort_values("Önem", ascending=True).tail(20)
681
+ fig = px.bar(df_imp, x="Önem", y="Özellik", orientation="h", title="Feature Importance (Top 20)", template="plotly_dark")
682
+ return fig
683
+ except Exception as e:
684
+ return go.Figure().update_layout(title=f"Grafik hatası: {e}", template="plotly_dark")
 
 
 
 
685
 
686
 
687
+ def get_backtest_outputs():
688
+ bt = pipeline.get_backtest()
689
+
690
+ if "error" in bt:
691
+ return pd.DataFrame({"Hata": [bt["error"]]}), go.Figure().update_layout(title="Backtest hatası", template="plotly_dark"), pd.DataFrame()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
692
 
693
+ results_df = pd.DataFrame(bt["results"])
694
+
695
+ fig = go.Figure()
696
+ fig.add_trace(go.Bar(name="Model MAE", x=results_df["split"], y=results_df["mae"], marker_color="cyan"))
697
+ fig.add_trace(go.Bar(name="Baseline MAE", x=results_df["split"], y=results_df["baseline_mae"], marker_color="gray"))
698
+ fig.update_layout(
699
+ title="Walk-Forward Backtest: MAE Karşılaştırması",
700
+ xaxis_title="Split",
701
+ yaxis_title="MAE (log getiri)",
702
+ barmode="group",
703
+ template="plotly_dark",
704
+ )
705
+
706
+ if bt.get("simulation"):
707
+ sim_df = pd.DataFrame([bt["simulation"]])
708
+ else:
709
+ sim_df = pd.DataFrame()
710
+
711
+ return results_df, fig, sim_df
712
+
713
+
714
+ def refresh_data_ui():
715
+ pipeline.fetch_data()
716
+ if not pipeline.featured_df.empty:
717
+ pipeline.train_model()
718
+
719
+ bt_table, bt_fig, sim_table = get_backtest_outputs()
720
+
721
+ return (
722
+ pipeline.status_message,
723
+ get_dashboard_metrics(),
724
+ get_analysis_table(),
725
+ get_prediction_table(),
726
+ get_arbitrage_table(),
727
+ get_model_performance_plot(),
728
+ bt_table,
729
+ bt_fig,
730
+ sim_table,
731
+ )
732
+
733
+
734
+ def initial_load():
735
+ return (
736
+ get_dashboard_metrics(),
737
+ get_analysis_table(),
738
+ get_prediction_table(),
739
+ get_arbitrage_table(),
740
+ get_model_performance_plot(),
741
+ pd.DataFrame(),
742
+ go.Figure().update_layout(title="Verileri yenilemek için butona tıklayın", template="plotly_dark"),
743
+ pd.DataFrame(),
744
+ )
745
+
746
+
747
+ # ---------------------------------------------------------------------------
748
+ # GRADIO ARAYÜZÜ
749
+ # ---------------------------------------------------------------------------
750
+
751
+ with gr.Blocks(title="GOSHAWK ALBION MARKET AI", theme=gr.themes.Soft(primary_hue="cyan", secondary_hue="slate")) as demo:
752
+
753
+ gr.Markdown(
754
+ """
755
+ # 🦅 GOSHAWK ALBION MARKET AI
756
+ **Profesyonel Market Intelligence Dashboard** | Avrupa Sunucusu
757
+ *LightGBM tabanlı 24 saatlik fiyat tahmini ve arbitraj analizi.*
758
+
759
+ **⚠️ Risk Uyarısı:** Bu uygulama yatırım tavsiyesi değildir. Model tahminleri belirsizlik içerir ve kesin kazanç garantisi sunmaz. Geçmiş performans gelecek sonuçların göstergesi değildir.
760
+ """
761
+ )
762
+
763
+ with gr.Row():
764
+ refresh_btn = gr.Button("🔄 Verileri Yenile ve Modeli Eğit", variant="primary", scale=1)
765
+ status_text = gr.Textbox(label="Durum", interactive=False, scale=2)
766
+
767
+ with gr.Tabs():
768
+
769
+ with gr.TabItem("📊 Dashboard"):
770
+ dashboard_table = gr.Dataframe(label="Özet Metrikler", interactive=False)
771
+ gr.Markdown(
772
+ "### Kullanım\n"
773
+ "1. 'Verileri Yenile ve Modeli Eğit' butonuna tıklayın.\n"
774
+ "2. API'den gerçek piyasa verileri çekilir ve global LightGBM modeli eğitilir.\n"
775
+ "3. Diğer sekmelerden analizleri inceleyebilirsiniz.\n\n"
776
+ "*Veriler `gos_hawk_data/` dizininde CSV olarak saklanır.*"
777
+ )
778
+
779
+ with gr.TabItem("📦 100 Ürün Analizi"):
780
+ analysis_table = gr.Dataframe(label="Ürün Analizi (En yüksek hacimli 100 ürün)", interactive=False)
781
+
782
+ with gr.TabItem("🔮 24 Saatlik Fiyat Tahmini"):
783
+ prediction_table = gr.Dataframe(label="Tahminler (24 saatlik log getiri modeli)", interactive=False)
784
+ gr.Markdown(
785
+ "*Güvenilirlik, volatilite ve RSI tabanlı bir göstergedir. "
786
+ "Net getiri, %2.5 setup + %8 vergi düşülerek hesaplanmıştır. "
787
+ "Spread ve likidite düşükse işlem maliyeti artabilir.*"
788
+ )
789
+
790
+ with gr.TabItem("🚚 Şehirler Arası Arbitraj"):
791
+ arbitrage_table = gr.Dataframe(label="Arbitraj Fırsatları (işlem maliyetleri dahil)", interactive=False)
792
+ gr.Markdown(
793
+ "*Net kâr; alışta %2.5 setup fee, satışta %2.5 setup + %8 transaction tax düşülerek hesaplanmıştır. "
794
+ "Seyahat maliyeti ve zaman riski dahil değildir. Caerleon'a giriş/çıkış riski ayrıca değerlendirilmelidir.*"
795
+ )
796
+
797
+ with gr.TabItem("📈 Model Performansı"):
798
+ performance_plot = gr.Plot(label="Feature Importance")
799
+
800
+ with gr.TabItem("🧪 Walk-forward Backtest"):
801
+ backtest_table = gr.Dataframe(label="Split Sonuçları")
802
+ backtest_plot = gr.Plot(label="MAE Karşılaştırması")
803
+ simulation_table = gr.Dataframe(label="İşlem Simülasyonu (maliyetler sonrası)")
804
+ gr.Markdown(
805
+ "*Walk-forward backtest kronolojik split kullanır; purge (24s) ve embargo (24s) uygulanır. "
806
+ "Baseline, 'fiyat değişmez' (getiri=0) varsayımıdır. Simülasyon yalnızca modelin pozitif getiri öngördüğü işlemleri içerir.*"
807
+ )
808
+
809
+ with gr.TabItem("🗄️ Veri Yönetimi"):
810
+ gr.Markdown("Veriler `gos_hawk_data/` dizininde CSV olarak saklanır.")
811
+ data_status = gr.Textbox(label="Veri Durumu", interactive=False)
812
+ csv_files = gr.FileExplorer(root=DATA_DIR, label="Kayıtlı CSV Dosyaları")
813
+
814
+ refresh_btn.click(
815
+ fn=refresh_data_ui,
816
+ inputs=[],
817
+ outputs=[
818
+ status_text,
819
+ dashboard_table,
820
+ analysis_table,
821
+ prediction_table,
822
+ arbitrage_table,
823
+ performance_plot,
824
+ backtest_table,
825
+ backtest_plot,
826
+ simulation_table,
827
+ ],
828
+ )
829
+
830
+ demo.load(
831
+ fn=initial_load,
832
+ inputs=[],
833
+ outputs=[
834
+ dashboard_table,
835
+ analysis_table,
836
+ prediction_table,
837
+ arbitrage_table,
838
+ performance_plot,
839
+ backtest_table,
840
+ backtest_plot,
841
+ simulation_table,
842
+ ],
843
+ )
844
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
845
 
846
  if __name__ == "__main__":
847
+ demo.launch(server_name="0.0.0.0", server_port=7860, share=False)