GoshawkVortexAI commited on
Commit
6127560
·
verified ·
1 Parent(s): 410df8f

Upload app (18).py

Browse files
Files changed (1) hide show
  1. app (18).py +1155 -0
app (18).py ADDED
@@ -0,0 +1,1155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ US Value Intelligence v3 — Compact Value & Intrinsic Valuation Platform
3
+ =========================================================================
4
+ Data integrity > model complexity. Intrinsic value > momentum. Margin of safety > prediction.
5
+
6
+ Veri etiketleri: [R] raporlanan [D] türetilen [A] kullanıcı varsayımı
7
+ UYARI: yfinance verisi point-in-time DEĞİLDİR; backtest için kullanılamaz.
8
+ """
9
+
10
+ import logging
11
+ import tempfile
12
+ import time
13
+ from datetime import datetime, timedelta
14
+ from typing import Any, Dict, List, Optional, Tuple
15
+
16
+ import gradio as gr
17
+ import numpy as np
18
+ import pandas as pd
19
+ import plotly.graph_objects as go
20
+ import yfinance as yf
21
+
22
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(levelname)s | %(message)s")
23
+ logger = logging.getLogger("us_v3")
24
+
25
+ US_UNIVERSE = [
26
+ # Teknoloji
27
+ "AAPL", "MSFT", "NVDA", "GOOGL", "META", "AVGO", "ORCL", "ADBE", "CRM", "CSCO",
28
+ "INTC", "AMD", "QCOM", "TXN", "IBM", "INTU", "AMAT", "MU", "NOW", "ACN",
29
+ # Tüketici
30
+ "AMZN", "TSLA", "HD", "MCD", "NKE", "SBUX", "LOW", "TJX", "COST", "WMT",
31
+ "PG", "KO", "PEP", "PM", "MO", "MDLZ", "CL", "TGT",
32
+ # Sağlık
33
+ "UNH", "JNJ", "LLY", "PFE", "MRK", "ABBV", "TMO", "ABT", "DHR", "BMY",
34
+ "AMGN", "GILD", "MDT", "CVS", "ISRG",
35
+ # Bankalar
36
+ "JPM", "BAC", "WFC", "C", "GS", "MS", "USB", "PNC", "TFC", "SCHW", "BK",
37
+ # Sigorta
38
+ "BRK-B", "PGR", "CB", "MMC", "AIG", "MET", "PRU", "ALL", "TRV", "AFL",
39
+ # Sanayi
40
+ "CAT", "DE", "HON", "GE", "UPS", "UNP", "RTX", "LMT", "BA", "MMM",
41
+ "FDX", "EMR", "ETN", "ITW", "WM",
42
+ # Enerji
43
+ "XOM", "CVX", "COP", "SLB", "EOG", "OXY", "PSX", "MPC", "KMI",
44
+ # Kamu hizmetleri
45
+ "NEE", "DUK", "SO", "D", "AEP",
46
+ # Malzeme
47
+ "LIN", "SHW", "APD", "NEM", "FCX", "DOW", "NUE",
48
+ # REIT
49
+ "PLD", "AMT", "EQIX", "O", "SPG", "PSA", "WELL", "DLR", "CCI",
50
+ # İletişim / medya
51
+ "T", "VZ", "CMCSA", "DIS", "NFLX", "TMUS",
52
+ ]
53
+ US_UNIVERSE = list(dict.fromkeys(US_UNIVERSE)) # tekrarları temizle
54
+ assert len(US_UNIVERSE) >= 100, f"Evren {len(US_UNIVERSE)} hisse; en az 100 olmalı"
55
+
56
+ REQUEST_DELAY_SEC = 0.3
57
+ CACHE_TTL_HOURS = 6
58
+ MIN_PEERS = 5
59
+ _cache: Dict[str, Tuple[datetime, Any]] = {}
60
+ LAST: Dict[str, Any] = {"df": None, "failed": []}
61
+
62
+
63
+ def _cget(k):
64
+ if k in _cache:
65
+ ts, v = _cache[k]
66
+ if datetime.now() - ts < timedelta(hours=CACHE_TTL_HOURS):
67
+ return v
68
+ return None
69
+
70
+
71
+ def _cset(k, v):
72
+ _cache[k] = (datetime.now(), v)
73
+
74
+
75
+ # ============================================================================
76
+ # SMALL HELPERS
77
+ # ============================================================================
78
+
79
+ def num(x) -> Optional[float]:
80
+ try:
81
+ if x is None:
82
+ return None
83
+ v = float(x)
84
+ return None if (np.isnan(v) or np.isinf(v)) else v
85
+ except Exception:
86
+ return None
87
+
88
+
89
+ def div(a, b) -> Optional[float]:
90
+ a, b = num(a), num(b)
91
+ if a is None or b is None or b == 0:
92
+ return None
93
+ return a / b
94
+
95
+
96
+ def growth(cur, prev) -> Optional[float]:
97
+ cur, prev = num(cur), num(prev)
98
+ if cur is None or prev is None or prev <= 0:
99
+ return None
100
+ return cur / prev - 1
101
+
102
+
103
+ def real_ret(nominal, infl) -> Optional[float]:
104
+ return None if nominal is None else (1 + nominal) / (1 + infl) - 1
105
+
106
+
107
+ def lin(x, bad, good) -> Optional[float]:
108
+ """Linear 0-100 scale; works for bad>good as well."""
109
+ if x is None:
110
+ return None
111
+ return float(np.clip((x - bad) / (good - bad), 0, 1) * 100)
112
+
113
+
114
+ def wavg(items) -> Tuple[Optional[float], float]:
115
+ v = [(s, w) for s, w in items if s is not None]
116
+ tw = sum(w for _, w in items)
117
+ if not v or tw == 0:
118
+ return None, 0.0
119
+ sw = sum(w for _, w in v)
120
+ return sum(s * w for s, w in v) / sw, sw / tw
121
+
122
+
123
+ def anyc(*c) -> Optional[bool]:
124
+ v = [x for x in c if x is not None]
125
+ return None if not v else any(v)
126
+
127
+
128
+ def fm(x, d=2):
129
+ return "N/A" if x is None else f"{x:,.{d}f}"
130
+
131
+
132
+ def fp(x, d=1):
133
+ return "N/A" if x is None else f"{x * 100:.{d}f}%"
134
+
135
+
136
+ def norm_sym(t: str) -> str:
137
+ t = (t or "").strip().upper().replace(".", "-") # Yahoo: BRK.B -> BRK-B
138
+ return t
139
+
140
+
141
+ def mk_params(infl, rw, rtg, rg, pt, cv) -> Dict[str, float]:
142
+ return dict(infl=infl / 100, rw=rw / 100, rtg=rtg / 100, rg=rg / 100, pt=pt / 100, conv=cv / 100)
143
+
144
+
145
+ # ============================================================================
146
+ # DATA ACQUISITION & INTEGRITY
147
+ # ============================================================================
148
+
149
+ STMT = {
150
+ "ni": (("Net Income", "Net Income Common Stockholders"), "fin"),
151
+ "rev": (("Total Revenue", "Operating Revenue"), "fin"),
152
+ "gp": (("Gross Profit",), "fin"),
153
+ "opinc": (("Operating Income", "EBIT"), "fin"),
154
+ "ebitda": (("EBITDA", "Normalized EBITDA"), "fin"),
155
+ "interest": (("Interest Expense", "Interest Expense Non Operating"), "fin"),
156
+ "tax": (("Tax Provision",), "fin"),
157
+ "pretax": (("Pretax Income",), "fin"),
158
+ "ta": (("Total Assets",), "bs"),
159
+ "eq": (("Stockholders Equity", "Common Stock Equity", "Total Equity Gross Minority Interest"), "bs"),
160
+ "debt": (("Total Debt",), "bs"),
161
+ "ltd": (("Long Term Debt", "Long Term Debt And Capital Lease Obligation"), "bs"),
162
+ "cash": (("Cash Cash Equivalents And Short Term Investments", "Cash And Cash Equivalents"), "bs"),
163
+ "ca": (("Current Assets",), "bs"),
164
+ "cl": (("Current Liabilities",), "bs"),
165
+ "shares": (("Ordinary Shares Number", "Share Issued"), "bs"),
166
+ "cfo": (("Operating Cash Flow", "Cash Flow From Continuing Operating Activities"), "cf"),
167
+ "capex": (("Capital Expenditure",), "cf"),
168
+ "fcf": (("Free Cash Flow",), "cf"),
169
+ "da": (("Depreciation And Amortization", "Depreciation Amortization Depletion"), "cf"),
170
+ "dwc": (("Change In Working Capital",), "cf"),
171
+ }
172
+
173
+
174
+ def stmt(df, names, i=0) -> Optional[float]:
175
+ if df is None or getattr(df, "empty", True) or i >= df.shape[1]:
176
+ return None
177
+ for n in names:
178
+ if n in df.index:
179
+ r = df.loc[n]
180
+ if isinstance(r, pd.DataFrame):
181
+ r = r.iloc[0]
182
+ return num(r.iloc[i])
183
+ return None
184
+
185
+
186
+ def segment_of(industry: str, sym: str) -> str:
187
+ ind = (industry or "").lower()
188
+ if "bank" in ind:
189
+ return "Bank"
190
+ if "insurance" in ind:
191
+ return "Insurance"
192
+ if "reit" in ind or "real estate" in ind:
193
+ return "REIT"
194
+ return "Industrial"
195
+
196
+
197
+ def fetch(symbol: str, force: bool = False) -> Optional[Dict[str, Any]]:
198
+ key = f"t_{symbol}"
199
+ if not force:
200
+ c = _cget(key)
201
+ if c is not None:
202
+ return c
203
+ try:
204
+ t = yf.Ticker(symbol)
205
+ info = t.info or {}
206
+ fin, bs, cf = t.financials, t.balance_sheet, t.cashflow
207
+ try:
208
+ dv = t.dividends
209
+ except Exception:
210
+ dv = None
211
+ price = num(info.get("currentPrice")) or num(info.get("regularMarketPrice"))
212
+ if price is None:
213
+ h = t.history(period="5d")
214
+ price = num(h["Close"].iloc[-1]) if not h.empty else None
215
+ except Exception as e:
216
+ logger.warning("%s fetch failed: %s", symbol, e)
217
+ return None
218
+ if price is None or price <= 0:
219
+ return None
220
+
221
+ flags: List[str] = []
222
+ dfs = {"fin": fin, "bs": bs, "cf": cf}
223
+ s = {k: [stmt(dfs[src], names, i) for i in range(3)] for k, (names, src) in STMT.items()}
224
+ s["capex"] = [abs(x) if x is not None else None for x in s["capex"]]
225
+ s["interest"] = [abs(x) if x is not None else None for x in s["interest"]]
226
+ for i in range(3):
227
+ if s["fcf"][i] is None and s["cfo"][i] is not None and s["capex"][i] is not None:
228
+ s["fcf"][i] = s["cfo"][i] - s["capex"][i]
229
+ if s["ebitda"][i] is None and s["opinc"][i] is not None and s["da"][i] is not None:
230
+ s["ebitda"][i] = s["opinc"][i] + s["da"][i]
231
+ if s["debt"][0] is None:
232
+ s["debt"][0] = num(info.get("totalDebt"))
233
+ if s["debt"][0] is not None:
234
+ flags.append("debt from info (not statement)")
235
+ if s["cash"][0] is None:
236
+ s["cash"][0] = num(info.get("totalCash"))
237
+ if s["cash"][0] is not None:
238
+ flags.append("cash from info (not statement)")
239
+
240
+ # period / currency / unit validation
241
+ sd = None
242
+ try:
243
+ sd = pd.Timestamp(fin.columns[0]).to_pydatetime() if fin is not None and not fin.empty else None
244
+ for nm, df in (("balance sheet", bs), ("cash flow", cf)):
245
+ if df is not None and not df.empty and sd is not None and pd.Timestamp(df.columns[0]).to_pydatetime() != sd:
246
+ flags.append(f"{nm} period != income statement period")
247
+ except Exception:
248
+ pass
249
+ age = (datetime.now() - sd).days if sd else None
250
+ if age is not None and age > 550:
251
+ flags.append(f"stale statements ({age} days)")
252
+ if sd is None:
253
+ flags.append("no annual statements")
254
+ fc, cur = info.get("financialCurrency"), info.get("currency")
255
+ unit_ok = (fc is None) or (cur is None) or (fc == cur)
256
+ if not unit_ok:
257
+ flags.append(f"currency mismatch (fin {fc} vs price {cur})")
258
+ if fc is None:
259
+ flags.append("financial currency unknown")
260
+
261
+ shares = num(info.get("sharesOutstanding")) or s["shares"][0]
262
+ mcap = num(info.get("marketCap")) or (price * shares if shares else None)
263
+ if shares and mcap and abs(price * shares / mcap - 1) > 0.35:
264
+ flags.append("shares x price differs >35% from market cap")
265
+ eq0, ta0 = s["eq"][0], s["ta"][0]
266
+ if eq0 is not None and ta0 is not None and eq0 > ta0 * 1.01:
267
+ flags.append("equity > total assets (suspicious)")
268
+ if s["rev"][0] and s["ni"][0] is not None and abs(s["ni"][0]) > 1.0 * s["rev"][0] and s["rev"][0] > 0:
269
+ flags.append("|net income| > revenue (suspicious)")
270
+
271
+ bvps = num(info.get("bookValue")) or div(eq0, shares)
272
+ eps = num(info.get("trailingEps")) or div(s["ni"][0], shares)
273
+ if eps is not None and shares and s["ni"][0] not in (None, 0) and abs(eps * shares / s["ni"][0] - 1) > 0.5:
274
+ flags.append("EPS x shares differs >50% from net income")
275
+
276
+ # dividends: 0 only if history fetched and empty; None if unavailable
277
+ dy = None
278
+ try:
279
+ if dv is not None:
280
+ if len(dv) > 0:
281
+ cutoff = pd.Timestamp.now(tz=dv.index.tz) - pd.Timedelta(days=365)
282
+ dy = float(dv[dv.index >= cutoff].sum()) / price
283
+ else:
284
+ dy = 0.0
285
+ except Exception:
286
+ dy = None
287
+ if dy is None:
288
+ y = num(info.get("dividendYield"))
289
+ if y is not None:
290
+ dy = y / 100 if y > 1 else y
291
+
292
+ tax_assumed = False
293
+ tr = div(s["tax"][0], s["pretax"][0]) if (s["pretax"][0] or 0) > 0 else None
294
+ if tr is None:
295
+ tr, tax_assumed = 0.25, True
296
+ tr = float(np.clip(tr, 0, 0.35))
297
+
298
+ net_debt = (s["debt"][0] - s["cash"][0]) if (s["debt"][0] is not None and s["cash"][0] is not None) else None
299
+ fcf0, int0 = s["fcf"][0], s["interest"][0]
300
+ fcff = (fcf0 + int0 * (1 - tr)) if (fcf0 is not None and int0 is not None) else None # unlevered -> net debt bridge
301
+ oe, oe_parts = None, None
302
+ ni0, da0, cx0, wc0 = s["ni"][0], s["da"][0], s["capex"][0], s["dwc"][0]
303
+ if None not in (ni0, da0, cx0, wc0):
304
+ maint = min(cx0, da0) # [D] estimate: maintenance capex ~ min(total capex, D&A)
305
+ oe = ni0 + da0 - maint + wc0
306
+ oe_parts = dict(ni=ni0, da=da0, capex_total=cx0, maint_est=maint, dwc=wc0)
307
+ ev = (mcap + net_debt) if (mcap and net_debt is not None) else num(info.get("enterpriseValue"))
308
+ ebitda0 = s["ebitda"][0]
309
+ industry = info.get("industry") or "Unknown"
310
+ seg = segment_of(industry, symbol)
311
+ roes = [div(s["ni"][i], s["eq"][i]) for i in range(3) if (s["eq"][i] or 0) > 0]
312
+ roes = [r for r in roes if r is not None]
313
+ ir = num(info.get("returnOnEquity"))
314
+ if ir is not None:
315
+ roes.append(ir)
316
+ sust_roe = float(np.clip(np.mean(roes), -1, 1.5)) if roes else None
317
+
318
+ d = {
319
+ "sym": symbol, "name": info.get("shortName") or symbol, "price": price, "mcap": mcap,
320
+ "shares": shares, "sector": info.get("sector") or "Unknown", "industry": industry, "seg": seg,
321
+ "bvps": bvps, "eps": eps, "dy": dy, "s": s, "flags": flags, "unit_ok": unit_ok, "stmt_date": sd, "age": age,
322
+ "tax_rate": tr, "tax_assumed": tax_assumed, "net_debt": net_debt, "fcff": fcff, "fcf0": fcf0, "oe": oe,
323
+ "oe_parts": oe_parts, "ev": ev, "sust_roe": sust_roe,
324
+ "pe": price / eps if eps and eps > 0 else None,
325
+ "fpe": num(info.get("forwardPE")) if (num(info.get("forwardPE")) or 0) > 0 else None,
326
+ "pb": price / bvps if bvps and bvps > 0 else None,
327
+ "ps": div(mcap, s["rev"][0]) if (s["rev"][0] or 0) > 0 else None,
328
+ "ev_ebitda": div(ev, ebitda0) if (ebitda0 or 0) > 0 and ev else None,
329
+ "ev_fcf": div(ev, fcf0) if (fcf0 or 0) > 0 and ev else None,
330
+ "fcf_yield": div(fcf0, mcap), "earn_yield": div(eps, price), "fetched": datetime.now().isoformat(),
331
+ }
332
+ _cset(key, d)
333
+ return d
334
+
335
+
336
+ def cached_ds() -> List[Dict[str, Any]]:
337
+ return [v for k, (ts, v) in _cache.items() if k.startswith("t_") and v is not None]
338
+
339
+
340
+ MULTS = ["pe", "fpe", "pb", "ev_ebitda", "ev_fcf", "ps", "fcf_yield", "earn_yield"]
341
+ POS_ONLY = {"pe", "fpe", "pb", "ev_ebitda", "ev_fcf", "ps"}
342
+
343
+
344
+ def build_medians(ds) -> Dict[Tuple[str, str], Dict[str, Tuple[Optional[float], int]]]:
345
+ groups: Dict[Tuple[str, str], list] = {}
346
+ for d in ds:
347
+ groups.setdefault((d["seg"], d["sector"]), []).append(d)
348
+ med = {}
349
+ for k, g in groups.items():
350
+ m = {}
351
+ for f in MULTS:
352
+ 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))]
353
+ m[f] = (float(np.median(v)) if len(v) >= MIN_PEERS else None, len(v))
354
+ med[k] = m
355
+ return med
356
+
357
+
358
+ # ============================================================================
359
+ # VALUATION ENGINE
360
+ # ============================================================================
361
+
362
+ def dcf_value(base, g0, wacc, tg, shares, net_debt, years=5) -> Optional[Dict[str, float]]:
363
+ """5-year explicit DCF, growth fades linearly g0 -> tg. Returns None if inputs insufficient."""
364
+ if None in (base, g0, wacc, tg, shares, net_debt) or base <= 0 or shares <= 0 or wacc <= tg:
365
+ return None
366
+ flows, f = [], base
367
+ for t in range(1, years + 1):
368
+ g = g0 + (tg - g0) * (t - 1) / (years - 1)
369
+ f *= 1 + g
370
+ flows.append(f)
371
+ pv = sum(fl / (1 + wacc) ** (i + 1) for i, fl in enumerate(flows))
372
+ pvtv = flows[-1] * (1 + tg) / (wacc - tg) / (1 + wacc) ** years
373
+ ev = pv + pvtv
374
+ eq = ev - net_debt
375
+ return {"ev": ev, "equity": eq, "per_share": eq / shares, "tv_share": pvtv / ev if ev else 0.0}
376
+
377
+
378
+ def assumptions(P, rg=None, rw=None):
379
+ rg = P["rg"] if rg is None else rg
380
+ rw = P["rw"] if rw is None else rw
381
+ i = P["infl"]
382
+ return (1 + rg) * (1 + i * P["pt"]) - 1, (1 + rw) * (1 + i) - 1, (1 + P["rtg"]) * (1 + i) - 1 # g0, wacc, tg (nominal)
383
+
384
+
385
+ def dcf_try(base, g0, wacc, tg, shares, nd, ok):
386
+ if not ok:
387
+ return None, "currency/unit mismatch"
388
+ if base is None:
389
+ return None, "essential inputs missing"
390
+ if base <= 0:
391
+ return None, "non-positive base cash flow"
392
+ if not shares or shares <= 0:
393
+ return None, "shares unavailable"
394
+ if nd is None:
395
+ return None, "net debt unavailable"
396
+ if wacc <= tg:
397
+ return None, "discount rate <= terminal growth"
398
+ r = dcf_value(base, g0, wacc, tg, shares, nd)
399
+ if r is None or r["per_share"] <= 0:
400
+ return None, "equity value <= 0"
401
+ return r["per_share"], f"TV = {r['tv_share'] * 100:.0f}% of EV"
402
+
403
+
404
+ def relative_value(d, med) -> Tuple[Optional[float], str]:
405
+ m = med.get((d["seg"], d["sector"]))
406
+ if not m or not d["unit_ok"]:
407
+ return None, "no peer group / unit mismatch"
408
+ seg, vals = d["seg"], []
409
+ if m["pb"][0] and d["bvps"] and d["bvps"] > 0:
410
+ vals.append(m["pb"][0] * d["bvps"])
411
+ if seg != "REIT" and m["pe"][0] and d["eps"] and d["eps"] > 0:
412
+ vals.append(m["pe"][0] * d["eps"])
413
+ if seg == "Industrial" and m["ev_ebitda"][0] and (d["s"]["ebitda"][0] or 0) > 0 and d["net_debt"] is not None and d["shares"]:
414
+ vals.append((m["ev_ebitda"][0] * d["s"]["ebitda"][0] - d["net_debt"]) / d["shares"])
415
+ vals = [v for v in vals if v > 0]
416
+ if not vals:
417
+ return None, f"<{MIN_PEERS} valid peers"
418
+ return float(np.mean(vals)), f"{len(vals)} sector-median multiple(s)"
419
+
420
+
421
+ def intrinsic(d, P, med, rg=None, rw=None):
422
+ g0, wacc, tg = assumptions(P, rg, rw)
423
+ m: Dict[str, Tuple[Optional[float], str]] = {}
424
+ seg, bv = d["seg"], d["bvps"]
425
+ if seg in ("Bank", "Insurance"):
426
+ roe = d["sust_roe"]
427
+ if d["unit_ok"] and roe is not None and bv and bv > 0 and wacc > tg:
428
+ m["Justified P/B"] = (max((roe - tg) / (wacc - tg), 0) * bv, f"sust. ROE {fp(roe)}, COE {fp(wacc)}, g {fp(tg)}")
429
+ else:
430
+ m["Justified P/B"] = (None, "ROE/book value unavailable")
431
+ m["DCF"] = (None, "not applied to banks/insurers")
432
+ elif seg == "REIT":
433
+ m["NAV proxy (book)"] = ((bv, "book value; appraised NAV not in Yahoo") if (d["unit_ok"] and bv and bv > 0) else (None, "book value unavailable"))
434
+ else:
435
+ m["DCF"] = dcf_try(d["fcff"], g0, wacc, tg, d["shares"], d["net_debt"], d["unit_ok"])
436
+ m["Owner Earnings"] = dcf_try(d["oe"], g0, wacc, tg, d["shares"], 0.0, d["unit_ok"]) # equity CF: no debt bridge
437
+ m["Relative"] = relative_value(d, med)
438
+ vals = [v for v, _ in m.values() if v is not None and v > 0]
439
+ return m, (float(np.median(vals)) if vals else None)
440
+
441
+
442
+ def reverse_dcf(d, P):
443
+ if d["seg"] in ("Bank", "Insurance"):
444
+ return None, "FCF-based reverse DCF not applicable to banks/insurers"
445
+ base, shares, nd = d["fcff"], d["shares"], d["net_debt"]
446
+ if not d["unit_ok"] or base is None or base <= 0 or not shares or nd is None:
447
+ return None, "essential inputs missing or FCFF <= 0"
448
+ _, wacc0, tg = assumptions(P)
449
+
450
+ def solve(wacc):
451
+ if wacc <= tg:
452
+ return None
453
+ f = lambda g: dcf_value(base, g, wacc, tg, shares, nd)["per_share"]
454
+ lo, hi = -0.9, 3.0
455
+ if not (f(lo) <= d["price"] <= f(hi)):
456
+ return None
457
+ for _ in range(80):
458
+ mid = (lo + hi) / 2
459
+ lo, hi = (mid, hi) if f(mid) < d["price"] else (lo, mid)
460
+ return (lo + hi) / 2
461
+
462
+ rows = []
463
+ for dw in (-0.03, 0.0, 0.03):
464
+ rw = P["rw"] + dw
465
+ w = (1 + rw) * (1 + P["infl"]) - 1
466
+ g = solve(w)
467
+ rows.append({"Real discount rate": fp(rw), "Nominal WACC": fp(w), "Implied FCF growth (nominal, yr1)": fp(g),
468
+ "Implied (real)": fp(real_ret(g, P["infl"]))})
469
+ return rows, "ok"
470
+
471
+
472
+ def scenarios(d, P, med, probs):
473
+ SC = [("Bear", -0.04, 0.03, 0.85), ("Base", 0.0, 0.0, 1.0), ("Bull", 0.04, -0.02, 1.10)]
474
+ out, ok = [], True
475
+ for (nm, dg, dw, mult), p in zip(SC, probs):
476
+ _, iv = intrinsic(d, P, med, rg=P["rg"] + dg, rw=max(P["rw"] + dw, 0.0))
477
+ tgt = None if iv is None else (d["price"] + P["conv"] * (iv - d["price"])) * mult
478
+ ok &= tgt is not None
479
+ out.append({"Scenario": nm, "Real growth": fp(P["rg"] + dg), "Real disc. rate": fp(max(P["rw"] + dw, 0)),
480
+ "Multiple factor": mult, "Intrinsic": iv, "Target": tgt, "Prob": p})
481
+ if not ok:
482
+ return out, None
483
+ pw = sum(o["Prob"] * o["Target"] for o in out)
484
+ nom = pw / d["price"] - 1
485
+ dy = d["dy"] or 0.0
486
+ tot = nom + dy
487
+ return out, dict(pw=pw, nominal=nom, total=tot, real=real_ret(tot, P["infl"]), real_price=real_ret(nom, P["infl"]),
488
+ dy_known=d["dy"] is not None)
489
+
490
+
491
+ def validate_probs(bear, base):
492
+ bull = 100 - bear - base
493
+ if bear < 0 or base < 0 or bull < -1e-9:
494
+ return None
495
+ return (bear / 100, base / 100, bull / 100)
496
+
497
+
498
+ # ============================================================================
499
+ # QUALITY, PIOTROSKI, RISK
500
+ # ============================================================================
501
+
502
+ def cv(series):
503
+ v = [x for x in series if x is not None]
504
+ if len(v) < 3 or np.mean(v) <= 0:
505
+ return None
506
+ return float(np.std(v) / np.mean(v))
507
+
508
+
509
+ def quality(d):
510
+ s, q = d["s"], {}
511
+ ni0, eq0, ta0, rev0 = s["ni"][0], s["eq"][0], s["ta"][0], s["rev"][0]
512
+ q["roe"] = div(ni0, eq0) if (eq0 or 0) > 0 else None
513
+ q["roa"] = div(ni0, ta0)
514
+ ic = None
515
+ if None not in (eq0, s["debt"][0], s["cash"][0]):
516
+ ic = eq0 + s["debt"][0] - s["cash"][0]
517
+ q["roic"] = div(s["opinc"][0] * (1 - d["tax_rate"]), ic) if (s["opinc"][0] is not None and ic and ic > 0) else None
518
+ q["gm"] = div(s["gp"][0], rev0)
519
+ q["opm"] = div(s["opinc"][0], rev0)
520
+ q["npm"] = div(ni0, rev0)
521
+ q["de"] = div(s["debt"][0], eq0) * 100 if (eq0 or 0) > 0 and s["debt"][0] is not None else None
522
+ e0 = s["ebitda"][0]
523
+ q["nd_ebitda"] = div(d["net_debt"], e0) if (e0 or 0) > 0 and d["net_debt"] is not None else None
524
+ q["cr"] = div(s["ca"][0], s["cl"][0])
525
+ q["icov"] = div(s["opinc"][0], s["interest"][0]) if (s["interest"][0] or 0) > 0 else None
526
+ q["fcf_conv"] = div(s["fcf"][0], ni0) if (ni0 or 0) > 0 else None
527
+ q["fcf_cv"] = cv(s["fcf"])
528
+ q["ni_cv"] = cv(s["ni"])
529
+ q["rev_g"] = growth(rev0, s["rev"][1])
530
+ q["ni_g"] = growth(ni0, s["ni"][1])
531
+ q["eq_ta"] = div(eq0, ta0)
532
+ return q
533
+
534
+
535
+ def piotroski(d):
536
+ s = d["s"]
537
+
538
+ def cmp(a, b, op):
539
+ return None if a is None or b is None else op(a, b)
540
+
541
+ roa = [div(s["ni"][i], s["ta"][i]) for i in (0, 1)]
542
+ cr = [div(s["ca"][i], s["cl"][i]) for i in (0, 1)]
543
+ lev = [div(s["ltd"][i] if s["ltd"][i] is not None else s["debt"][i], s["ta"][i]) for i in (0, 1)]
544
+ gm = [div(s["gp"][i], s["rev"][i]) for i in (0, 1)]
545
+ at = [div(s["rev"][i], s["ta"][i]) for i in (0, 1)]
546
+ comps = [
547
+ ("1 Net income > 0", cmp(s["ni"][0], 0, lambda a, b: a > b)),
548
+ ("2 CFO > 0", cmp(s["cfo"][0], 0, lambda a, b: a > b)),
549
+ ("3 ROA improved", cmp(roa[0], roa[1], lambda a, b: a > b)),
550
+ ("4 CFO > net income", cmp(s["cfo"][0], s["ni"][0], lambda a, b: a > b)),
551
+ ("5 Leverage decreased", cmp(lev[0], lev[1], lambda a, b: a < b)),
552
+ ("6 Current ratio improved", cmp(cr[0], cr[1], lambda a, b: a > b)),
553
+ ("7 No share dilution", cmp(s["shares"][0], s["shares"][1], lambda a, b: a <= b * 1.005)),
554
+ ("8 Gross margin improved", cmp(gm[0], gm[1], lambda a, b: a > b)),
555
+ ("9 Asset turnover improved", cmp(at[0], at[1], lambda a, b: a > b)),
556
+ ]
557
+ n = sum(1 for _, v in comps if v is not None)
558
+ return sum(1 for _, v in comps if v), n, comps
559
+
560
+
561
+ def risk_engine(d, q, cov):
562
+ s, bank = d["s"], d["seg"] in ("Bank", "Insurance")
563
+ ni0, ni1 = s["ni"][0], s["ni"][1]
564
+ fl = []
565
+
566
+ def add(name, w, cond, detail):
567
+ fl.append((name, w, None if cond is None else ("TRIGGERED" if cond else "OK"), detail))
568
+
569
+ add("Financial distress", 12, anyc(None if s["eq"][0] is None else s["eq"][0] <= 0,
570
+ None if q["icov"] is None else q["icov"] < 1.5,
571
+ None if (ni0 is None or s["cfo"][0] is None) else (ni0 < 0 and s["cfo"][0] < 0)),
572
+ "equity<=0, interest cover<1.5, or NI<0 & CFO<0")
573
+ lev = anyc(None if q["eq_ta"] is None else q["eq_ta"] < 0.06) if bank else anyc(
574
+ None if q["nd_ebitda"] is None else q["nd_ebitda"] > 4, None if q["de"] is None else q["de"] > 200)
575
+ add("Leverage", 12, lev, "bank: equity/assets<6%; else ND/EBITDA>4 or D/E>200%")
576
+ add("Liquidity", 10, None if bank else (None if q["cr"] is None else q["cr"] < 1), "current ratio < 1")
577
+ add("Negative FCF", 10, None if bank else (None if s["fcf"][0] is None else s["fcf"][0] < 0), "FCF < 0")
578
+ ed = None
579
+ if ni0 is not None and ni1 is not None:
580
+ ed = ni0 < 0 or (ni1 > 0 and ni0 < 0.8 * ni1)
581
+ add("Earnings deterioration", 10, ed, "NI < 0 or down >20% y/y")
582
+ md = None
583
+ if not bank and q["opm"] is not None and s["opinc"][1] is not None and (s["rev"][1] or 0) > 0:
584
+ md = q["opm"] < s["opinc"][1] / s["rev"][1] - 0.02
585
+ add("Margin deterioration", 8, md, "operating margin down >2 pts")
586
+ dv = None if bank or ni0 is None or s["cfo"][0] is None or ni0 <= 0 else s["cfo"][0] < 0.5 * ni0
587
+ add("CFO vs NI divergence", 10, dv, "CFO < 50% of positive NI")
588
+ dil = cmp_g = growth(s["shares"][0], s["shares"][1])
589
+ add("Dilution", 8, None if dil is None else dil > 0.03, "share count +3% y/y")
590
+ ev = anyc(None if d["pe"] is None and d["eps"] is None else (d["eps"] is not None and d["eps"] <= 0) or (d["pe"] is not None and d["pe"] > 40),
591
+ None if d["pb"] is None else d["pb"] > 6)
592
+ add("Extreme valuation", 10, ev, "P/E>40, loss-making, or P/B>6")
593
+ add("Data reliability", 10, cov < 0.7 or bool(d["flags"]), f"coverage<70% or integrity flags ({len(d['flags'])})")
594
+ ev_w = sum(w for _, w, st, _ in fl if st is not None)
595
+ score = None if ev_w == 0 else 100 * sum(w for _, w, st, _ in fl if st == "TRIGGERED") / ev_w
596
+ return score, fl
597
+
598
+
599
+ # ============================================================================
600
+ # SCORING & ANALYSIS
601
+ # ============================================================================
602
+
603
+ WEIGHTS = {"Industrial": (.35, .30, .20, .15), "Bank": (.40, .35, .10, .15),
604
+ "Insurance": (.40, .30, .15, .15), "REIT": (.45, .20, .20, .15)}
605
+
606
+
607
+ def coverage(d):
608
+ s, seg = d["s"], d["seg"]
609
+ f = [d["price"], d["mcap"], d["shares"], d["pb"], d["pe"], s["ni"][0], s["eq"][0], s["ta"][0], d["bvps"]]
610
+ if seg not in ("Bank", "Insurance"):
611
+ f += [s["rev"][0], s["cfo"][0], s["debt"][0], s["cash"][0], d["fcff"], s["ebitda"][0], d["oe"]]
612
+ return sum(x is not None for x in f) / len(f)
613
+
614
+
615
+ def analyze(d, P, med, probs=(0.25, 0.5, 0.25)):
616
+ A: Dict[str, Any] = {"d": d}
617
+ infl, seg = P["infl"], d["seg"]
618
+ q = quality(d)
619
+ A["q"] = q
620
+ A["methods"], A["iv"] = intrinsic(d, P, med)
621
+ A["mos"] = (A["iv"] - d["price"]) / A["iv"] if A["iv"] else None
622
+ rv, A["rel_note"] = relative_value(d, med)
623
+ A["rel_mos"] = (rv - d["price"]) / rv if rv else None
624
+ A["pio"] = piotroski(d)
625
+ A["cov"] = coverage(d)
626
+ A["risk"], A["flags"] = risk_engine(d, q, A["cov"])
627
+ bank = seg in ("Bank", "Insurance")
628
+ rroe, rroic = real_ret(q["roe"], infl), real_ret(q["roic"], infl)
629
+ rrev, rni = real_ret(q["rev_g"], infl), real_ret(q["ni_g"], infl)
630
+ pe_s = (0.0 if d["eps"] <= 0 else lin(d["pe"], 25, 4)) if d["eps"] is not None else None
631
+ pb_s = lin(d["pb"], 4, 0.5) if d["pb"] else (0.0 if d["bvps"] is not None and d["bvps"] <= 0 else None)
632
+ vi = [(lin(A["mos"], -0.3, 0.5), 4), (lin(A["rel_mos"], -0.3, 0.5), 2)]
633
+ if bank:
634
+ vi += [(pe_s, 2), (pb_s, 3), (lin(d["dy"], 0, 0.08), 1)]
635
+ elif seg == "REIT":
636
+ vi += [(lin(d["pb"], 1.5, 0.4), 3), (pe_s, 1)]
637
+ else:
638
+ vi += [(pe_s, 1), (pb_s, 1), (lin(d["ev_ebitda"], 15, 3), 1), (lin(d["fcf_yield"], -0.02, 0.12), 1),
639
+ (lin(d["earn_yield"], 0, 0.25), 1)]
640
+ if bank:
641
+ qi = [(lin(rroe, -0.1, 0.2), 4), (lin(q["roa"], 0, 0.03), 2), (lin(q["ni_cv"], 2, 0.3), 2),
642
+ (lin(d["dy"], 0, 0.08), 1), (lin(rni, -0.2, 0.3), 1)]
643
+ else:
644
+ qi = [(lin(rroe, -0.1, 0.2), 3), (lin(rroic, -0.05, 0.15), 3), (lin(q["opm"], 0.02, 0.25), 2),
645
+ (lin(q["fcf_conv"], 0, 1.2), 2), (lin(q["fcf_cv"], 2, 0.3), 1), (lin(rrev, -0.1, 0.2), 1), (lin(rni, -0.2, 0.3), 1)]
646
+ sc, n, _ = A["pio"]
647
+ e0 = d["s"]["ebitda"][0]
648
+ if bank:
649
+ si = [(lin(q["eq_ta"], 0.04, 0.12), 4), (lin(q["roa"], 0, 0.02), 1)]
650
+ else:
651
+ nde = 0.0 if (e0 is not None and e0 <= 0 and (d["net_debt"] or 0) > 0) else lin(q["nd_ebitda"], 5, 0)
652
+ if q["nd_ebitda"] is None and d["net_debt"] is not None and d["net_debt"] <= 0 and (e0 or 0) > 0:
653
+ nde = 100.0 # net cash
654
+ si = [(nde, 3), (lin(q["cr"], 0.8, 2.0), 2), (lin(q["icov"], 1, 8), 2), (lin(q["de"], 250, 30), 2),
655
+ (lin(sc / n, 0.3, 0.8) if n >= 5 else None, 2)]
656
+ A["val"], A["val_cov"] = wavg(vi)
657
+ A["qual"], A["qual_cov"] = wavg(qi)
658
+ A["str"], A["str_cov"] = wavg(si)
659
+ w = WEIGHTS[seg]
660
+ comps = [A["val"], A["qual"], A["str"], None if A["risk"] is None else 100 - A["risk"]]
661
+ if A["val"] is None or A["qual"] is None:
662
+ A["composite"] = None
663
+ else:
664
+ av = [(c, wt) for c, wt in zip(comps, w) if c is not None]
665
+ A["composite"] = sum(c * wt for c, wt in av) / sum(wt for _, wt in av) * min(1.0, A["cov"] / 0.7)
666
+ sev = (not d["unit_ok"]) or any("stale" in f or "period" in f for f in d["flags"])
667
+ lvl = 2 if A["cov"] >= 0.85 else 1 if A["cov"] >= 0.65 else 0
668
+ lvl = max(lvl - (1 if sev else 0), 0)
669
+ A["conf"] = ["Low", "Medium", "High"][lvl]
670
+ pr = validate_probs(probs[0] * 100, probs[1] * 100) or (0.25, 0.5, 0.25)
671
+ A["scen"], A["exp"] = scenarios(d, P, med, pr)
672
+ return A
673
+
674
+
675
+ # ============================================================================
676
+ # OUTPUT BUILDERS
677
+ # ============================================================================
678
+
679
+ def get_A(ticker, P, probs=(0.25, 0.5, 0.25)):
680
+ sym = norm_sym(ticker)
681
+ d = fetch(sym)
682
+ if d is None:
683
+ return None, f"⚠️ {sym} verisi çekilemedi."
684
+ return analyze(d, P, build_medians(cached_ds()), probs), None
685
+
686
+
687
+ def price_fig(sym, extra=None):
688
+ try:
689
+ h = yf.Ticker(norm_sym(sym)).history(period="1y")
690
+ if h.empty:
691
+ return None
692
+ f = go.Figure(go.Scatter(x=h.index, y=h["Close"], mode="lines", name="Close"))
693
+ for nm, v in (extra or {}).items():
694
+ if v:
695
+ f.add_hline(y=v, line_dash="dot", annotation_text=nm)
696
+ f.update_layout(title=f"{sym} — 1Y", template="plotly_white", height=380, margin=dict(l=10, r=10, t=40, b=10))
697
+ return f
698
+ except Exception:
699
+ return None
700
+
701
+
702
+ def scan_row(A):
703
+ d, q = A["d"], A["q"]
704
+ sc, n, _ = A["pio"]
705
+ e = A["exp"]
706
+ return {
707
+ "Ticker": d["sym"], "Segment": d["seg"], "Sector": d["sector"], "Price": round(d["price"], 2),
708
+ "Composite": None if A["composite"] is None else round(A["composite"], 1),
709
+ "Intrinsic": None if A["iv"] is None else round(A["iv"], 2),
710
+ "MoS %": None if A["mos"] is None else round(A["mos"] * 100, 1),
711
+ "P/E": None if d["pe"] is None else round(d["pe"], 1), "P/B": None if d["pb"] is None else round(d["pb"], 2),
712
+ "ROE %": None if q["roe"] is None else round(q["roe"] * 100, 1),
713
+ "ROIC %": None if q["roic"] is None else round(q["roic"] * 100, 1),
714
+ "FCF Yield %": None if d["fcf_yield"] is None else round(d["fcf_yield"] * 100, 1),
715
+ "Piotroski": f"{sc}/{n} (cov {n}/9)" if n else "N/A",
716
+ "Risk": None if A["risk"] is None else round(A["risk"], 0),
717
+ "Exp Return % (nom, incl div)": None if not e else round(e["total"] * 100, 1),
718
+ "Real Exp Return %": None if not e or e["real"] is None else round(e["real"] * 100, 1),
719
+ "Coverage %": round(A["cov"] * 100), "Confidence": A["conf"],
720
+ }
721
+
722
+
723
+ def run_scan(force, extra, P, progress):
724
+ syms = US_UNIVERSE + [norm_sym(x) for x in (extra or "").split(",") if x.strip()]
725
+ syms = list(dict.fromkeys(syms))
726
+ ds, failed = [], []
727
+ for i, s in enumerate(syms):
728
+ progress((i + 1) / len(syms), desc=f"Fetching {s}")
729
+ was_cached = (not force) and _cget(f"t_{s}") is not None
730
+ d = fetch(s, force)
731
+ (ds.append(d) if d else failed.append(s))
732
+ if not was_cached:
733
+ time.sleep(REQUEST_DELAY_SEC)
734
+ med = build_medians(ds)
735
+ rows = [scan_row(analyze(d, P, med)) for d in ds]
736
+ df = pd.DataFrame(rows)
737
+ LAST["df"], LAST["failed"] = df, failed
738
+ return df, failed
739
+
740
+
741
+ def apply_filters(seg, minc, minm, sortby, desc):
742
+ df = LAST["df"]
743
+ if df is None or df.empty:
744
+ return None, None
745
+ x = df.copy()
746
+ if seg != "All":
747
+ x = x[x["Segment"] == seg]
748
+ x = x[x["Composite"].fillna(-1) >= minc]
749
+ if minm > -100:
750
+ x = x[x["MoS %"].fillna(-1e9) >= minm]
751
+ x = x.sort_values(sortby, ascending=not desc, na_position="last")
752
+ f = tempfile.NamedTemporaryFile(delete=False, suffix=".csv", prefix="us_v3_")
753
+ x.to_csv(f.name, index=False)
754
+ return x, f.name
755
+
756
+
757
+ def md_report(A, P):
758
+ d = A["d"]
759
+ L = [f"## {d['sym']} — {d['name']}",
760
+ f"**{d['seg']}** · {d['sector']} / {d['industry']} · Fiyat **{fm(d['price'])} USD** · Mali tablo: "
761
+ f"{d['stmt_date'].date() if d['stmt_date'] else 'N/A'}",
762
+ f"**Composite {fm(A['composite'], 0)}** | Valuation {fm(A['val'], 0)} | Quality {fm(A['qual'], 0)} | "
763
+ f"Strength {fm(A['str'], 0)} | Risk {fm(A['risk'], 0)} (düşük=iyi) | Coverage {fp(A['cov'], 0)} | "
764
+ f"Confidence **{A['conf']}**",
765
+ f"**Intrinsic value [D]:** {fm(A['iv'])} USD · Margin of Safety {fp(A['mos'])}"]
766
+ L.append("\n| Yöntem | Değer/hisse | Not |\n|---|---|---|")
767
+ for k, (v, n) in A["methods"].items():
768
+ L.append(f"| {k} | {fm(v)} | {n} |")
769
+ if d["flags"]:
770
+ L.append("\n**⚠️ Veri bayrakları:** " + "; ".join(d["flags"]))
771
+ L.append("\n*Intrinsic = mevcut yöntemlerin medyanı. Skorlar evren içi göreceli, açıklanabilir heuristiklerdir.*")
772
+ return "\n".join(L)
773
+
774
+
775
+ def radar_fig(A):
776
+ cats = ["Valuation", "Quality", "Strength", "Safety (100-Risk)"]
777
+ v = [A["val"], A["qual"], A["str"], None if A["risk"] is None else 100 - A["risk"]]
778
+ v = [x or 0 for x in v]
779
+ f = go.Figure(go.Scatterpolar(r=v + v[:1], theta=cats + cats[:1], fill="toself"))
780
+ f.update_layout(polar=dict(radialaxis=dict(range=[0, 100])), height=340, margin=dict(l=30, r=30, t=30, b=20))
781
+ return f
782
+
783
+
784
+ # ============================================================================
785
+ # SELF TEST (synthetic)
786
+ # ============================================================================
787
+
788
+ def self_test() -> str:
789
+ res = []
790
+
791
+ def chk(name, cond):
792
+ res.append(("✅ " if cond else "❌ ") + name)
793
+
794
+ r = dcf_value(100, 0, 0.10, 0, 10, 200)
795
+ chk("DCF: g=0,tg=0 → EV=1000", r is not None and abs(r["ev"] - 1000) < 1e-6)
796
+ chk("EV→Equity: 1000-200=800, 80/share", r is not None and abs(r["equity"] - 800) < 1e-6 and abs(r["per_share"] - 80) < 1e-6)
797
+ chk("DCF refuses wacc<=tg", dcf_value(100, 0.1, 0.05, 0.05, 10, 0) is None)
798
+ chk("DCF refuses FCF<=0", dcf_value(-5, 0.1, 0.2, 0.05, 10, 0) is None)
799
+ chk("DCF refuses missing input", dcf_value(100, 0.1, 0.2, 0.05, None, 0) is None)
800
+ chk("Real return 50% nom / 25% infl = 20%", abs(real_ret(0.5, 0.25) - 0.2) < 1e-12)
801
+ chk("div by zero → None", div(1, 0) is None and div(None, 2) is None)
802
+ P = dict(infl=0.30, rw=0.10, rtg=0.01, rg=0.03, pt=0.5, conv=0.5)
803
+ g0, w, tg = assumptions(P)
804
+ chk("WACC > terminal growth (defaults)", w > tg)
805
+ 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)
806
+ chk("Probabilities 70/50 rejected", validate_probs(70, 50) is None)
807
+ # reverse DCF consistency
808
+ d = {"fcff": 100.0, "shares": 10.0, "net_debt": 200.0, "price": 80.0, "unit_ok": True, "seg": "Industrial"}
809
+ P2 = dict(infl=0.0, rw=0.10, rtg=0.0, rg=0.0, pt=0.0, conv=0.5)
810
+ rows, _ = reverse_dcf(d, P2)
811
+ chk("Reverse DCF: price 80 → implied g≈0", rows is not None and rows[1]["Implied FCF growth (nominal, yr1)"] == "0.0%")
812
+ # Piotroski incompleteness
813
+ empty = {"s": {k: [None, None, None] for k in STMT}}
814
+ sc, n, _ = piotroski(empty)
815
+ chk("Piotroski all-missing → 0 available (not 9/9)", n == 0 and sc == 0)
816
+ # peer rule
817
+ ds = [{"seg": "Industrial", "sector": "X", "pe": 10.0 + i, **{m: None for m in MULTS if m != "pe"}} for i in range(4)]
818
+ chk("Sector median needs >=5 peers", build_medians(ds)[("Industrial", "X")]["pe"][0] is None)
819
+ ds += [{"seg": "Industrial", "sector": "X", "pe": 20.0, **{m: None for m in MULTS if m != "pe"}}]
820
+ chk("Sector median with 5 peers computed", build_medians(ds)[("Industrial", "X")]["pe"][0] == 12.0 or build_medians(ds)[("Industrial", "X")]["pe"][0] is not None)
821
+ chk("Segment classification", segment_of("Banks - Regional", "JPM") == "Bank" and segment_of("REIT - Industrial", "PLD") == "REIT"
822
+ and segment_of("Insurance - Life", "MET") == "Insurance" and segment_of("Steel", "NUE") == "Industrial")
823
+ return "\n".join(res)
824
+
825
+
826
+ # ============================================================================
827
+ # GRADIO UI
828
+ # ============================================================================
829
+
830
+ def build_ui():
831
+ with gr.Blocks(title="US Value Intelligence v3", theme=gr.themes.Soft()) as demo:
832
+ gr.Markdown("# US Value Intelligence v3\n**⚠️ yfinance verisi point-in-time değildir; backtest için kullanılamaz. "
833
+ "Eksik veri `N/A` gösterilir, asla sıfırla doldurulmaz. 12 aylık senaryolar heuristiktir, istatistiksel tahmin değildir. "
834
+ "Yatırım tavsiyesi değildir.**")
835
+ with gr.Accordion("⚙️ Varsayımlar [A] (tüm sekmeler için)", open=False):
836
+ with gr.Row():
837
+ s_infl = gr.Slider(0, 20, 3, step=0.5, label="Enflasyon (%)")
838
+ s_rw = gr.Slider(1, 20, 7, step=0.5, label="Reel iskonto oranı (%)")
839
+ s_rtg = gr.Slider(0, 5, 1, step=0.25, label="Reel terminal büyüme (%)")
840
+ with gr.Row():
841
+ s_rg = gr.Slider(-10, 20, 3, step=0.5, label="Baz reel büyüme (%)")
842
+ s_pt = gr.Slider(0, 100, 50, step=5, label="Enflasyonun nominal büyümeye geçişi (%)")
843
+ s_cv = gr.Slider(0, 100, 50, step=5, label="12 ay içinde değer-fiyat farkının kapanma oranı (%)")
844
+ PI = [s_infl, s_rw, s_rtg, s_rg, s_pt, s_cv]
845
+
846
+ with gr.Tabs():
847
+ with gr.Tab("📊 Market Scanner"):
848
+ with gr.Row():
849
+ scan_btn = gr.Button("Run Scanner", variant="primary", scale=2)
850
+ force = gr.Checkbox(label="Force refresh", value=False)
851
+ extra = gr.Textbox(label="Ek tickerlar (virgülle, örn: BRK-B,AAPL)", value="")
852
+ with gr.Row():
853
+ f_seg = gr.Dropdown(["All", "Industrial", "Bank", "Insurance", "REIT"], value="All", label="Segment")
854
+ f_c = gr.Slider(0, 100, 0, step=5, label="Min composite")
855
+ f_m = gr.Slider(-100, 100, -100, step=5, label="Min MoS % (-100 = kapalı)")
856
+ with gr.Row():
857
+ f_sort = gr.Dropdown(["Composite", "MoS %", "P/E", "P/B", "ROE %", "ROIC %", "FCF Yield %", "Risk",
858
+ "Exp Return % (nom, incl div)", "Real Exp Return %", "Coverage %"],
859
+ value="Composite", label="Sırala")
860
+ f_desc = gr.Checkbox(label="Azalan", value=True)
861
+ f_btn = gr.Button("Filtreyi uygula")
862
+ status = gr.Markdown()
863
+ table = gr.Dataframe(interactive=False, wrap=True)
864
+ csv = gr.File(label="CSV indir")
865
+ FI = [f_seg, f_c, f_m, f_sort, f_desc]
866
+
867
+ def do_scan(frc, ext, *a, progress=gr.Progress()):
868
+ P = mk_params(*a[:6])
869
+ seg, c, m, so, de = a[6:]
870
+ df, failed = run_scan(frc, ext, P, progress)
871
+ if df.empty:
872
+ return None, None, "⚠️ Veri çekilemedi."
873
+ x, path = apply_filters(seg, c, m, so, de)
874
+ msg = f"✅ {len(df)} hisse tarandı · coverage ort. %{df['Coverage %'].mean():.0f}"
875
+ if failed:
876
+ msg += f" · ⚠️ Çekilemeyen ({len(failed)}): " + ", ".join(failed)
877
+ return x, path, msg
878
+
879
+ def do_filter(*a):
880
+ x, path = apply_filters(*a)
881
+ return x, path
882
+
883
+ scan_btn.click(do_scan, [force, extra] + PI + FI, [table, csv, status])
884
+ f_btn.click(do_filter, FI, [table, csv])
885
+
886
+ with gr.Tab("🔍 Company Analysis"):
887
+ with gr.Row():
888
+ t1 = gr.Textbox(value="THYAO", label="Ticker", scale=2)
889
+ b1 = gr.Button("Analyze", variant="primary")
890
+ md1 = gr.Markdown()
891
+ with gr.Row():
892
+ p1 = gr.Plot()
893
+ r1 = gr.Plot()
894
+
895
+ def company(t, *a):
896
+ P = mk_params(*a)
897
+ A, err = get_A(t, P)
898
+ if err:
899
+ return err, None, None
900
+ return md_report(A, P), price_fig(A["d"]["sym"], {"Intrinsic": A["iv"]}), radar_fig(A)
901
+
902
+ b1.click(company, [t1] + PI, [md1, p1, r1])
903
+
904
+ with gr.Tab("🧮 Intrinsic Value Calculator"):
905
+ with gr.Row():
906
+ t2 = gr.Textbox(value="EREGL", label="Ticker", scale=2)
907
+ b2 = gr.Button("Calculate", variant="primary")
908
+ md2 = gr.Markdown()
909
+ p2 = gr.Plot()
910
+
911
+ def calc(t, *a):
912
+ P = mk_params(*a)
913
+ A, err = get_A(t, P)
914
+ if err:
915
+ return err, None
916
+ d = A["d"]
917
+ g0, w, tg = assumptions(P)
918
+ L = [f"### {d['sym']} — {d['seg']}",
919
+ f"**Varsayımlar [A]:** yıl-1 nominal büyüme {fp(g0)} (reel {fp(P['rg'])}, enflasyon geçişi %{P['pt'] * 100:.0f}) · "
920
+ f"nominal iskonto {fp(w)} · nominal terminal {fp(tg)}",
921
+ f"**Girdiler [R/D]:** FCFF {fm(d['fcff'], 0)} (FCF + faiz×(1−vergi{' varsayım' if d['tax_assumed'] else ''})) · "
922
+ f"net borç {fm(d['net_debt'], 0)} · hisse {fm(d['shares'], 0)} · vergi {fp(d['tax_rate'], 0)}"]
923
+ if d["oe_parts"]:
924
+ o = d["oe_parts"]
925
+ L.append(f"**Owner Earnings [D]:** NI {fm(o['ni'], 0)} + D&A {fm(o['da'], 0)} − bakım capex (tahmin=min(capex, D&A)) "
926
+ f"{fm(o['maint_est'], 0)} + ΔİS sermayesi {fm(o['dwc'], 0)} = **{fm(d['oe'], 0)}** (toplam capex {fm(o['capex_total'], 0)})")
927
+ L.append("\n| Yöntem | Değer | Not |\n|---|---|---|")
928
+ for k, (v, n) in A["methods"].items():
929
+ L.append(f"| {k} | {fm(v)} | {n} |")
930
+ L.append(f"\n**Intrinsic (medyan): {fm(A['iv'])}** · Fiyat {fm(d['price'])} · MoS {fp(A['mos'])}")
931
+ names = [k for k, (v, _) in A["methods"].items() if v]
932
+ vals = [A["methods"][k][0] for k in names]
933
+ f = go.Figure(go.Bar(x=["Price"] + names + ["Intrinsic"], y=[d["price"]] + vals + [A["iv"] or 0]))
934
+ f.update_layout(template="plotly_white", height=340, margin=dict(l=10, r=10, t=20, b=10))
935
+ return "\n".join(L), f
936
+
937
+ b2.click(calc, [t2] + PI, [md2, p2])
938
+
939
+ with gr.Tab("↩️ Reverse DCF"):
940
+ with gr.Row():
941
+ t3 = gr.Textbox(value="TOASO", label="Ticker", scale=2)
942
+ b3 = gr.Button("Solve", variant="primary")
943
+ md3 = gr.Markdown()
944
+ tb3 = gr.Dataframe(interactive=False)
945
+
946
+ def rev(t, *a):
947
+ P = mk_params(*a)
948
+ A, err = get_A(t, P)
949
+ if err:
950
+ return err, None
951
+ d = A["d"]
952
+ rows, note = reverse_dcf(d, P)
953
+ if rows is None:
954
+ return f"**{d['sym']}**: ⚠️ {note}", None
955
+ g0, _, _ = assumptions(P)
956
+ h = d["s"]["fcf"]
957
+ hist = f"{fp((h[0] / h[2]) ** 0.5 - 1)}" if (h[0] or 0) > 0 and (h[2] or 0) > 0 else "N/A"
958
+ md = (f"**{d['sym']}** fiyat {fm(d['price'])} → piyasanın fiyatladığı FCF büyümesi (5 yıl, terminale doğru sönümlenir).\n\n"
959
+ f"Mevcut varsayım yıl-1 büyüme: {fp(g0)} · Tarihsel FCF CAGR (2y, nominal): {hist}\n\n"
960
+ "Not: Zorunlu bir 'doğru' yoktur; implied büyüme makul görünmüyorsa piyasa beklentisi ya aşırı iyimser ya da kötümserdir.")
961
+ return md, pd.DataFrame(rows)
962
+
963
+ b3.click(rev, [t3] + PI, [md3, tb3])
964
+
965
+ with gr.Tab("💎 Financial Quality"):
966
+ with gr.Row():
967
+ t4 = gr.Textbox(value="BIMAS", label="Ticker", scale=2)
968
+ b4 = gr.Button("Show", variant="primary")
969
+ md4 = gr.Markdown()
970
+ tb4 = gr.Dataframe(interactive=False)
971
+ tb4b = gr.Dataframe(interactive=False)
972
+
973
+ def qual(t, *a):
974
+ P = mk_params(*a)
975
+ A, err = get_A(t, P)
976
+ if err:
977
+ return err, None, None
978
+ q, d = A["q"], A["d"]
979
+ pc = lambda x: fp(x)
980
+ rows = [("ROE [D]", pc(q["roe"])), ("ROE (reel) [D]", pc(real_ret(q["roe"], P["infl"]))), ("ROA [D]", pc(q["roa"])),
981
+ ("ROIC [D]", pc(q["roic"])), ("Gross margin [D]", pc(q["gm"])), ("Operating margin [D]", pc(q["opm"])),
982
+ ("Net margin [D]", pc(q["npm"])), ("Debt/Equity % [D]", fm(q["de"], 0)), ("Net debt/EBITDA [D]", fm(q["nd_ebitda"])),
983
+ ("Current ratio [D]", fm(q["cr"])), ("Interest coverage [D]", fm(q["icov"])), ("FCF/NI [D]", fm(q["fcf_conv"])),
984
+ ("FCF variation (CV) [D]", fm(q["fcf_cv"])), ("Revenue growth nominal [D]", pc(q["rev_g"])),
985
+ ("Revenue growth real [D]", pc(real_ret(q["rev_g"], P["infl"]))), ("Earnings growth nominal [D]", pc(q["ni_g"])),
986
+ ("Earnings growth real [D]", pc(real_ret(q["ni_g"], P["infl"]))), ("Equity/Assets [D]", pc(q["eq_ta"]))]
987
+ sc, n, comps = A["pio"]
988
+ cp = [(c, "N/A" if v is None else ("1" if v else "0")) for c, v in comps]
989
+ md = (f"**{d['sym']}** — Piotroski: **{sc}/{n}** (hesaplanabilen {n}/9)" +
990
+ ("" if n == 9 else " ⚠️ eksik bileşen var; skor tam değildir.") +
991
+ (" Bankalar için Piotroski sınırlı anlam taşır." if d["seg"] in ("Bank", "Insurance") else ""))
992
+ return md, pd.DataFrame(rows, columns=["Metric", "Value"]), pd.DataFrame(cp, columns=["Piotroski component", "Result"])
993
+
994
+ b4.click(qual, [t4] + PI, [md4, tb4, tb4b])
995
+
996
+ with gr.Tab("🛡️ Risk Analysis"):
997
+ with gr.Row():
998
+ t5 = gr.Textbox(value="PGSUS", label="Ticker", scale=2)
999
+ b5 = gr.Button("Assess", variant="primary")
1000
+ md5 = gr.Markdown()
1001
+ tb5 = gr.Dataframe(interactive=False)
1002
+
1003
+ def risk_tab(t, *a):
1004
+ P = mk_params(*a)
1005
+ A, err = get_A(t, P)
1006
+ if err:
1007
+ return err, None
1008
+ rows = [{"Flag": n, "Weight": w, "Status": s or "N/A", "Rule": dt} for n, w, s, dt in A["flags"]]
1009
+ return (f"**{A['d']['sym']}** Risk skoru: **{fm(A['risk'], 0)}/100** (yalnızca değerlendirilebilen bayraklar üzerinden; "
1010
+ f"düşük = iyi). Coverage {fp(A['cov'], 0)}."), pd.DataFrame(rows)
1011
+
1012
+ b5.click(risk_tab, [t5] + PI, [md5, tb5])
1013
+
1014
+ with gr.Tab("🏭 Sector Comparison"):
1015
+ with gr.Row():
1016
+ t6 = gr.Textbox(value="EREGL", label="Ticker (opsiyonel)", scale=2)
1017
+ b6 = gr.Button("Compare", variant="primary")
1018
+ md6 = gr.Markdown()
1019
+ tb6 = gr.Dataframe(interactive=False)
1020
+ p6 = gr.Plot()
1021
+
1022
+ def sector(t, *a):
1023
+ P = mk_params(*a)
1024
+ ds = cached_ds()
1025
+ if not ds:
1026
+ return "Önce Market Scanner'ı çalıştırın (medyanlar önbellekteki hisselerden hesaplanır).", None, None
1027
+ med = build_medians(ds)
1028
+ rows = []
1029
+ for (seg, sec), m in sorted(med.items()):
1030
+ r = {"Segment": seg, "Sector": sec, "Peers": sum(1 for d in ds if d["seg"] == seg and d["sector"] == sec)}
1031
+ for f in ("pe", "pb", "ev_ebitda", "ev_fcf", "ps", "fcf_yield", "earn_yield"):
1032
+ v, n = m[f]
1033
+ r[f] = None if v is None else round(v, 3)
1034
+ rows.append(r)
1035
+ fig, md = None, f"Sektör medyanı için en az {MIN_PEERS} geçerli peer gerekir; yetersizse `None`."
1036
+ d = fetch(norm_sym(t)) if t.strip() else None
1037
+ if d:
1038
+ m = med.get((d["seg"], d["sector"]))
1039
+ keys = ["pe", "pb", "ev_ebitda", "ps"]
1040
+ cv_ = [d[k] for k in keys]
1041
+ mv = [m[k][0] if m else None for k in keys]
1042
+ fig = go.Figure([go.Bar(name=d["sym"], x=keys, y=cv_), go.Bar(name="Sector median", x=keys, y=mv)])
1043
+ fig.update_layout(barmode="group", template="plotly_white", height=340, margin=dict(l=10, r=10, t=20, b=10))
1044
+ md = f"**{d['sym']}** — {d['seg']} / {d['sector']}. " + md
1045
+ return md, pd.DataFrame(rows), fig
1046
+
1047
+ b6.click(sector, [t6] + PI, [md6, tb6, p6])
1048
+
1049
+ with gr.Tab("🎯 12-Month Scenarios"):
1050
+ with gr.Row():
1051
+ t7 = gr.Textbox(value="AKBNK", label="Ticker", scale=2)
1052
+ pb = gr.Slider(0, 100, 25, step=5, label="Bear olasılığı (%)")
1053
+ pm = gr.Slider(0, 100, 50, step=5, label="Base olasılığı (%) — Bull = kalan")
1054
+ b7 = gr.Button("Run", variant="primary")
1055
+ md7 = gr.Markdown()
1056
+ tb7 = gr.Dataframe(interactive=False)
1057
+ p7 = gr.Plot()
1058
+
1059
+ def scen(t, bear, base, *a):
1060
+ P = mk_params(*a)
1061
+ pr = validate_probs(bear, base)
1062
+ if pr is None:
1063
+ return "⚠️ Bear + Base 100'ü aşamaz (Bull = 100 − Bear − Base).", None, None
1064
+ A, err = get_A(t, P, pr)
1065
+ if err:
1066
+ return err, None, None
1067
+ d, e = A["d"], A["exp"]
1068
+ rows = [{"Scenario": o["Scenario"], "Real growth": o["Real growth"], "Real disc. rate": o["Real disc. rate"],
1069
+ "Multiple factor": o["Multiple factor"], "Intrinsic": None if o["Intrinsic"] is None else round(o["Intrinsic"], 2),
1070
+ "12M target": None if o["Target"] is None else round(o["Target"], 2),
1071
+ "Probability": f"{o['Prob'] * 100:.0f}%"} for o in A["scen"]]
1072
+ if e is None:
1073
+ return f"**{d['sym']}**: ⚠️ Senaryo üretilemedi (intrinsic value hesaplanamadı).", pd.DataFrame(rows), None
1074
+ md = (f"**{d['sym']}** · Olasılık-ağırlıklı hedef **{fm(e['pw'])}** · Beklenen nominal fiyat getirisi {fp(e['nominal'])} · "
1075
+ f"Temettü-dahil nominal {fp(e['total'])}" + ("" if e["dy_known"] else " (temettü verisi yok, 0 sayıldı)") +
1076
+ f" · Reel (temettü-dahil) {fp(e['real'])} · Reel fiyat getirisi {fp(e['real_price'])}\n\n"
1077
+ f"*Heuristik: hedef = fiyat + k×(senaryo değeri − fiyat), k={P['conv'] * 100:.0f}%, çarpan faktörü uygulanır. "
1078
+ "İstatistiksel olarak doğrulanmış tahmin değildir. Temettü yalnızca bir kez eklenir.*")
1079
+ f = go.Figure(go.Bar(x=["Price", "Bear", "Base", "Bull", "Prob-weighted"],
1080
+ y=[d["price"]] + [o["Target"] for o in A["scen"]] + [e["pw"]]))
1081
+ f.update_layout(template="plotly_white", height=340, margin=dict(l=10, r=10, t=20, b=10))
1082
+ return md, pd.DataFrame(rows), f
1083
+
1084
+ b7.click(scen, [t7, pb, pm] + PI, [md7, tb7, p7])
1085
+
1086
+ with gr.Tab("📋 Data Quality Monitor"):
1087
+ b8 = gr.Button("Check Data Quality", variant="primary")
1088
+ md8 = gr.Markdown()
1089
+ tb8 = gr.Dataframe(interactive=False, wrap=True)
1090
+
1091
+ def dq(progress=gr.Progress()):
1092
+ rows, failed = [], []
1093
+ for i, s in enumerate(US_UNIVERSE):
1094
+ progress((i + 1) / len(US_UNIVERSE), desc=f"Checking {s}")
1095
+ was = _cget(f"t_{s}") is not None
1096
+ d = fetch(s)
1097
+ if d is None:
1098
+ failed.append(s)
1099
+ continue
1100
+ c = coverage(d)
1101
+ rows.append({"Ticker": d["sym"], "Segment": d["seg"], "Coverage %": round(c * 100),
1102
+ "Statement date": d["stmt_date"].date().isoformat() if d["stmt_date"] else "N/A",
1103
+ "Age (days)": d["age"], "Status": "OK" if c >= 0.7 and not d["flags"] else "CHECK",
1104
+ "Flags": "; ".join(d["flags"]) or "—"})
1105
+ if not was:
1106
+ time.sleep(REQUEST_DELAY_SEC * 0.5)
1107
+ df = pd.DataFrame(rows)
1108
+ avg = f"{df['Coverage %'].mean():.0f}%" if not df.empty else "N/A"
1109
+ return (f"Ortalama coverage: **{avg}** · Başarılı {len(rows)} · Çekilemeyen {len(failed)}: "
1110
+ + (", ".join(failed) or "—")), df
1111
+
1112
+ b8.click(dq, None, [md8, tb8])
1113
+
1114
+ with gr.Tab("📐 Methodology"):
1115
+ gr.Markdown(METHODOLOGY)
1116
+ b9 = gr.Button("Run self-test (synthetic data)")
1117
+ o9 = gr.Textbox(label="Self-test", lines=18, interactive=False)
1118
+ b9.click(self_test, None, o9)
1119
+ return demo
1120
+
1121
+
1122
+ METHODOLOGY = """
1123
+ ### Değerleme mimarisi
1124
+ - **DCF (sanayi/REIT dışı):** FCFF = FCF + faiz×(1−vergi) (Yahoo FCF'si faiz sonrası olduğu için borç çift sayılmasın diye). 5 yıl, büyüme yıl-1'den terminale doğrusal sönümlenir; EV − net borç = özsermaye; / hisse. Girdi eksik, FCFF ≤ 0 veya iskonto ≤ terminal büyüme ise **hesaplanmaz**.
1125
+ - **Owner Earnings:** NI + D&A − bakım capex + ΔİSermayesi. Bakım capex = min(toplam capex, D&A) **tahminidir**; veri eksikse yöntem kullanılmaz. Özsermaye nakit akışı olduğu için net borç düşülmez.
1126
+ - **Banka/Sigorta:** Gordon gerekçeli P/B = (sürdürülebilir ROE − g)/(COE − g) × defter değeri. FCF/EV-EBITDA kullanılmaz. Sermaye yeterliliği/aktif kalitesi Yahoo'da yok.
1127
+ - **REIT:** NAV vekili = defter değeri (ekspertiz NAV'ı yok), P/NAV. Doluluk verisi yok.
1128
+ - **Relative:** sektör medyanı (en az 5 geçerli peer) P/E, P/B, EV/EBITDA → hisse başına değer.
1129
+ - **Intrinsic = mevcut yöntemlerin medyanı.** MoS = (IV − fiyat)/IV.
1130
+ - **Reverse DCF:** fiyatı veren yıl-1 FCF büyümesi (bisection), iskonto oranı ±3 puan duyarlılığı.
1131
+ - **Enflasyon:** iskonto ve terminal büyüme reel girilir, nominale çevrilir; enflasyonun yalnızca ayarlanabilir bir kısmı (geçiş oranı) kâr büyümesine yansır. Reel getiri = (1+nominal)/(1+enflasyon) − 1.
1132
+ - **Senaryolar:** Bear/Base/Bull = reel büyüme ∓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).
1133
+
1134
+ ### Skorlama (0-100, açıklanabilir)
1135
+ Composite = ağırlıklı ortalama (mevcut bileşenler üzerinden yeniden normalize) × min(1, coverage/0.7).
1136
+ | Segment | Valuation | Quality | Strength | Risk (ters) |
1137
+ |---|---|---|---|---|
1138
+ | Sanayi | 35 | 30 | 20 | 15 |
1139
+ | Banka | 40 | 35 | 10 | 15 |
1140
+ | Sigorta | 40 | 30 | 15 | 15 |
1141
+ | REIT | 45 | 20 | 20 | 15 |
1142
+
1143
+ Risk (0-100): 10 bayrak (distress, kaldıraç, likidite, negatif FCF, kâr/marj bozulması, CFO-NI ayrışması, seyrelme, aşırı değerleme, veri güvenilirliği); yalnızca değerlendirilebilen bayrakların ağırlığı üzerinden hesaplanır. Piotroski 9 bileşenli; hesaplanamayan bileşen "N/A" olur ve skor `x/n` olarak gösterilir.
1144
+
1145
+ ### Bilinen veri sınırlamaları
1146
+ - yfinance point-in-time değildir; backtest geçersizdir. Bilanço tarihleri ve kapsam hisseden hisseye değişir.
1147
+ - Yıllık tablolar gecikmelidir; Yıllık tablolar GAAP/ADR farklarına ve tek seferlik kalemlere duyarlıdır; yabancı (ADR) şirketlerde para birimi uyuşmazlığı bayraklanır.
1148
+ - Evren: gömülü liste + eklediğiniz tickerlar (tüm ABD piyasası Yahoo'dan ücretsiz çekilemez).
1149
+ - Sektör/segment sınıflaması Yahoo `industry` alanına dayanır; peer sayısı <5 ise medyan hesaplanmaz.
1150
+ - Sonuçlar araştırma/eğitim amaçlıdır, yatırım tavsiyesi değildir.
1151
+ """
1152
+
1153
+ if __name__ == "__main__":
1154
+ logger.info("Self-test:\n%s", self_test())
1155
+ build_ui().launch(server_name="0.0.0.0", server_port=7860)