Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,1218 +1,847 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 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 |
-
|
| 17 |
-
|
|
|
|
| 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 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
#
|
| 29 |
-
#
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
#
|
| 43 |
-
|
| 44 |
-
"
|
| 45 |
-
"
|
| 46 |
-
|
| 47 |
-
"
|
| 48 |
-
|
| 49 |
-
"
|
| 50 |
-
"
|
| 51 |
-
"
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
return None
|
| 74 |
|
| 75 |
|
| 76 |
-
def
|
| 77 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
|
| 80 |
-
|
| 81 |
-
|
| 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
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
|
|
|
|
|
|
| 99 |
|
| 100 |
|
| 101 |
-
def
|
| 102 |
-
|
|
|
|
| 103 |
|
|
|
|
| 104 |
|
| 105 |
-
|
| 106 |
-
""
|
| 107 |
-
|
| 108 |
-
return None
|
| 109 |
-
return float(np.clip((x - bad) / (good - bad), 0, 1) * 100)
|
| 110 |
|
|
|
|
| 111 |
|
| 112 |
-
|
| 113 |
-
|
| 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 |
-
|
| 122 |
-
|
| 123 |
-
|
|
|
|
| 124 |
|
|
|
|
| 125 |
|
| 126 |
-
|
| 127 |
-
|
|
|
|
| 128 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
|
| 130 |
-
|
| 131 |
-
""
|
| 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 |
-
|
| 145 |
-
return None if x is None else float(f"{x:.{n}g}")
|
| 146 |
|
| 147 |
|
| 148 |
-
|
| 149 |
-
|
|
|
|
| 150 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
|
| 152 |
-
def pct(x, d=1):
|
| 153 |
-
return None if x is None else round(x * 100, d)
|
| 154 |
|
|
|
|
|
|
|
|
|
|
| 155 |
|
| 156 |
-
|
| 157 |
-
""
|
| 158 |
-
|
| 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 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 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 |
-
|
| 228 |
-
return base in STABLES or base.endswith(("3L", "3S", "5L", "5S"))
|
| 229 |
|
| 230 |
|
| 231 |
-
def
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
continue
|
| 240 |
-
|
| 241 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 251 |
-
|
| 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 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 440 |
}
|
| 441 |
-
_cset(key, d)
|
| 442 |
-
return d
|
| 443 |
|
| 444 |
|
| 445 |
-
|
| 446 |
-
|
|
|
|
| 447 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 448 |
|
| 449 |
-
|
| 450 |
-
|
|
|
|
|
|
|
|
|
|
| 451 |
|
|
|
|
|
|
|
|
|
|
| 452 |
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 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 |
-
|
| 469 |
-
# ============================================================================
|
| 470 |
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 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 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 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 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 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
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 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 |
-
|
| 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)
|
|
|