Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -4,7 +4,7 @@ BIST Value Intelligence v3 — Compact Value & Intrinsic Valuation Platform
|
|
| 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;
|
| 8 |
"""
|
| 9 |
|
| 10 |
import logging
|
|
@@ -128,6 +128,14 @@ def norm_sym(t: str) -> str:
|
|
| 128 |
return t if t.endswith(".IS") else t + ".IS"
|
| 129 |
|
| 130 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
def mk_params(infl, rw, rtg, rg, pt, cv) -> Dict[str, float]:
|
| 132 |
return dict(infl=infl / 100, rw=rw / 100, rtg=rtg / 100, rg=rg / 100, pt=pt / 100, conv=cv / 100)
|
| 133 |
|
|
@@ -288,12 +296,8 @@ def fetch(symbol: str, force: bool = False) -> Optional[Dict[str, Any]]:
|
|
| 288 |
net_debt = (s["debt"][0] - s["cash"][0]) if (s["debt"][0] is not None and s["cash"][0] is not None) else None
|
| 289 |
fcf0, int0 = s["fcf"][0], s["interest"][0]
|
| 290 |
fcff = (fcf0 + int0 * (1 - tr)) if (fcf0 is not None and int0 is not None) else None # unlevered -> net debt bridge
|
| 291 |
-
oe, oe_parts = None, None
|
| 292 |
ni0, da0, cx0, wc0 = s["ni"][0], s["da"][0], s["capex"][0], s["dwc"][0]
|
| 293 |
-
|
| 294 |
-
maint = min(cx0, da0) # [D] estimate: maintenance capex ~ min(total capex, D&A)
|
| 295 |
-
oe = ni0 + da0 - maint + wc0
|
| 296 |
-
oe_parts = dict(ni=ni0, da=da0, capex_total=cx0, maint_est=maint, dwc=wc0)
|
| 297 |
ev = (mcap + net_debt) if (mcap and net_debt is not None) else num(info.get("enterpriseValue"))
|
| 298 |
ebitda0 = s["ebitda"][0]
|
| 299 |
industry = info.get("industry") or "Unknown"
|
|
@@ -310,7 +314,7 @@ def fetch(symbol: str, force: bool = False) -> Optional[Dict[str, Any]]:
|
|
| 310 |
"shares": shares, "sector": info.get("sector") or "Unknown", "industry": industry, "seg": seg,
|
| 311 |
"bvps": bvps, "eps": eps, "dy": dy, "s": s, "flags": flags, "unit_ok": unit_ok, "stmt_date": sd, "age": age,
|
| 312 |
"tax_rate": tr, "tax_assumed": tax_assumed, "net_debt": net_debt, "fcff": fcff, "fcf0": fcf0, "oe": oe,
|
| 313 |
-
"oe_parts": oe_parts, "ev": ev, "sust_roe": sust_roe,
|
| 314 |
"pe": price / eps if eps and eps > 0 else None,
|
| 315 |
"fpe": num(info.get("forwardPE")) if (num(info.get("forwardPE")) or 0) > 0 else None,
|
| 316 |
"pb": price / bvps if bvps and bvps > 0 else None,
|
|
@@ -626,7 +630,7 @@ def analyze(d, P, med, probs=(0.25, 0.5, 0.25)):
|
|
| 626 |
vi += [(lin(d["pb"], 1.5, 0.4), 3), (pe_s, 1)]
|
| 627 |
else:
|
| 628 |
vi += [(pe_s, 1), (pb_s, 1), (lin(d["ev_ebitda"], 15, 3), 1), (lin(d["fcf_yield"], -0.02, 0.12), 1),
|
| 629 |
-
(lin(d["earn_yield"], 0, 0.25), 1)]
|
| 630 |
if bank:
|
| 631 |
qi = [(lin(rroe, -0.1, 0.2), 4), (lin(q["roa"], 0, 0.03), 2), (lin(q["ni_cv"], 2, 0.3), 2),
|
| 632 |
(lin(d["dy"], 0, 0.08), 1), (lin(rni, -0.2, 0.3), 1)]
|
|
@@ -693,6 +697,7 @@ def scan_row(A):
|
|
| 693 |
d, q = A["d"], A["q"]
|
| 694 |
sc, n, _ = A["pio"]
|
| 695 |
e = A["exp"]
|
|
|
|
| 696 |
return {
|
| 697 |
"Ticker": d["sym"], "Segment": d["seg"], "Sector": d["sector"], "Price": round(d["price"], 2),
|
| 698 |
"Composite": None if A["composite"] is None else round(A["composite"], 1),
|
|
@@ -702,6 +707,9 @@ def scan_row(A):
|
|
| 702 |
"ROE %": None if q["roe"] is None else round(q["roe"] * 100, 1),
|
| 703 |
"ROIC %": None if q["roic"] is None else round(q["roic"] * 100, 1),
|
| 704 |
"FCF Yield %": None if d["fcf_yield"] is None else round(d["fcf_yield"] * 100, 1),
|
|
|
|
|
|
|
|
|
|
| 705 |
"Piotroski": f"{sc}/{n} (cov {n}/9)" if n else "N/A",
|
| 706 |
"Risk": None if A["risk"] is None else round(A["risk"], 0),
|
| 707 |
"Exp Return % (nom, incl div)": None if not e else round(e["total"] * 100, 1),
|
|
@@ -756,6 +764,8 @@ def md_report(A, P):
|
|
| 756 |
L.append("\n| Yöntem | Değer/hisse | Not |\n|---|---|---|")
|
| 757 |
for k, (v, n) in A["methods"].items():
|
| 758 |
L.append(f"| {k} | {fm(v)} | {n} |")
|
|
|
|
|
|
|
| 759 |
if d["flags"]:
|
| 760 |
L.append("\n**⚠️ Veri bayrakları:** " + "; ".join(d["flags"]))
|
| 761 |
L.append("\n*Intrinsic = mevcut yöntemlerin medyanı. Skorlar evren içi göreceli, açıklanabilir heuristiklerdir.*")
|
|
@@ -810,16 +820,255 @@ def self_test() -> str:
|
|
| 810 |
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)
|
| 811 |
chk("Segment classification", segment_of("Banks - Regional", "GARAN") == "Bank" and segment_of("Real Estate", "EKGYO") == "GYO"
|
| 812 |
and segment_of("Insurance - Life", "ANHYT") == "Insurance" and segment_of("Steel", "EREGL") == "Industrial")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 813 |
return "\n".join(res)
|
| 814 |
|
| 815 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 816 |
# ============================================================================
|
| 817 |
# GRADIO UI
|
| 818 |
# ============================================================================
|
| 819 |
|
| 820 |
def build_ui():
|
| 821 |
with gr.Blocks(title="BIST Value Intelligence v3", theme=gr.themes.Soft()) as demo:
|
| 822 |
-
gr.Markdown("# BIST Value Intelligence v3\n**⚠️ yfinance verisi point-in-time değildir;
|
| 823 |
"Eksik veri `N/A` gösterilir, asla sıfırla doldurulmaz. 12 aylık senaryolar heuristiktir, istatistiksel tahmin değildir. "
|
| 824 |
"Yatırım tavsiyesi değildir.**")
|
| 825 |
with gr.Accordion("⚙️ Varsayımlar [A] (tüm sekmeler için)", open=False):
|
|
@@ -844,7 +1093,7 @@ def build_ui():
|
|
| 844 |
f_c = gr.Slider(0, 100, 0, step=5, label="Min composite")
|
| 845 |
f_m = gr.Slider(-100, 100, -100, step=5, label="Min MoS % (-100 = kapalı)")
|
| 846 |
with gr.Row():
|
| 847 |
-
f_sort = gr.Dropdown(["Composite", "MoS %", "P/E", "P/B", "ROE %", "ROIC %", "FCF Yield %", "Risk",
|
| 848 |
"Exp Return % (nom, incl div)", "Real Exp Return %", "Coverage %"],
|
| 849 |
value="Composite", label="Sırala")
|
| 850 |
f_desc = gr.Checkbox(label="Azalan", value=True)
|
|
@@ -913,7 +1162,7 @@ def build_ui():
|
|
| 913 |
if d["oe_parts"]:
|
| 914 |
o = d["oe_parts"]
|
| 915 |
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)) "
|
| 916 |
-
f"{fm(o['maint_est'], 0)} + ΔİS sermayesi {fm(o['dwc'], 0)} = **{fm(d['oe'], 0)}** (toplam capex {fm(o['capex_total'], 0)})")
|
| 917 |
L.append("\n| Yöntem | Değer | Not |\n|---|---|---|")
|
| 918 |
for k, (v, n) in A["methods"].items():
|
| 919 |
L.append(f"| {k} | {fm(v)} | {n} |")
|
|
@@ -1101,6 +1350,28 @@ def build_ui():
|
|
| 1101 |
|
| 1102 |
b8.click(dq, None, [md8, tb8])
|
| 1103 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1104 |
with gr.Tab("📐 Methodology"):
|
| 1105 |
gr.Markdown(METHODOLOGY)
|
| 1106 |
b9 = gr.Button("Run self-test (synthetic data)")
|
|
@@ -1112,7 +1383,7 @@ def build_ui():
|
|
| 1112 |
METHODOLOGY = """
|
| 1113 |
### Değerleme mimarisi
|
| 1114 |
- **DCF (sanayi/GYO 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**.
|
| 1115 |
-
- **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.
|
| 1116 |
- **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.
|
| 1117 |
- **GYO:** NAV vekili = defter değeri (ekspertiz NAV'ı yok), P/NAV. Doluluk verisi yok.
|
| 1118 |
- **Relative:** sektör medyanı (en az 5 geçerli peer) P/E, P/B, EV/EBITDA → hisse başına değer.
|
|
@@ -1133,7 +1404,7 @@ Composite = ağırlıklı ortalama (mevcut bileşenler üzerinden yeniden normal
|
|
| 1133 |
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.
|
| 1134 |
|
| 1135 |
### Bilinen veri sınırlamaları
|
| 1136 |
-
- yfinance point-in-time değildir;
|
| 1137 |
- Yıllık tablolar gecikmelidir; TFRS/enflasyon muhasebesi (TMS 29) etkileri nominal rakamları çarpıtabilir.
|
| 1138 |
- Evren: gömülü liste + eklediğiniz tickerlar (tüm BIST listesi Yahoo'dan ücretsiz çekilemez).
|
| 1139 |
- Sektör/segment sınıflaması Yahoo `industry` alanına dayanır; peer sayısı <5 ise medyan hesaplanmaz.
|
|
|
|
| 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 sekmesi yalnızca DENEYSEL/göstergedir (bkz. sınırlamalar).
|
| 8 |
"""
|
| 9 |
|
| 10 |
import logging
|
|
|
|
| 128 |
return t if t.endswith(".IS") else t + ".IS"
|
| 129 |
|
| 130 |
|
| 131 |
+
def owner_earnings(ni, da, capex, dwc):
|
| 132 |
+
"""OE = NI + D&A − bakım capex + ΔİSermayesi; bakım capex = min(toplam capex, D&A) [D tahmin]. Eksik girdi → None."""
|
| 133 |
+
if None in (ni, da, capex, dwc):
|
| 134 |
+
return None, None
|
| 135 |
+
maint = min(capex, da)
|
| 136 |
+
return ni + da - maint + dwc, dict(ni=ni, da=da, capex_total=capex, maint_est=maint, dwc=dwc)
|
| 137 |
+
|
| 138 |
+
|
| 139 |
def mk_params(infl, rw, rtg, rg, pt, cv) -> Dict[str, float]:
|
| 140 |
return dict(infl=infl / 100, rw=rw / 100, rtg=rtg / 100, rg=rg / 100, pt=pt / 100, conv=cv / 100)
|
| 141 |
|
|
|
|
| 296 |
net_debt = (s["debt"][0] - s["cash"][0]) if (s["debt"][0] is not None and s["cash"][0] is not None) else None
|
| 297 |
fcf0, int0 = s["fcf"][0], s["interest"][0]
|
| 298 |
fcff = (fcf0 + int0 * (1 - tr)) if (fcf0 is not None and int0 is not None) else None # unlevered -> net debt bridge
|
|
|
|
| 299 |
ni0, da0, cx0, wc0 = s["ni"][0], s["da"][0], s["capex"][0], s["dwc"][0]
|
| 300 |
+
oe, oe_parts = owner_earnings(ni0, da0, cx0, wc0) # bakım capex = min(capex, D&A) [D tahmin]
|
|
|
|
|
|
|
|
|
|
| 301 |
ev = (mcap + net_debt) if (mcap and net_debt is not None) else num(info.get("enterpriseValue"))
|
| 302 |
ebitda0 = s["ebitda"][0]
|
| 303 |
industry = info.get("industry") or "Unknown"
|
|
|
|
| 314 |
"shares": shares, "sector": info.get("sector") or "Unknown", "industry": industry, "seg": seg,
|
| 315 |
"bvps": bvps, "eps": eps, "dy": dy, "s": s, "flags": flags, "unit_ok": unit_ok, "stmt_date": sd, "age": age,
|
| 316 |
"tax_rate": tr, "tax_assumed": tax_assumed, "net_debt": net_debt, "fcff": fcff, "fcf0": fcf0, "oe": oe,
|
| 317 |
+
"oe_yield": div(oe, mcap) if unit_ok else None, "oe_parts": oe_parts, "ev": ev, "sust_roe": sust_roe,
|
| 318 |
"pe": price / eps if eps and eps > 0 else None,
|
| 319 |
"fpe": num(info.get("forwardPE")) if (num(info.get("forwardPE")) or 0) > 0 else None,
|
| 320 |
"pb": price / bvps if bvps and bvps > 0 else None,
|
|
|
|
| 630 |
vi += [(lin(d["pb"], 1.5, 0.4), 3), (pe_s, 1)]
|
| 631 |
else:
|
| 632 |
vi += [(pe_s, 1), (pb_s, 1), (lin(d["ev_ebitda"], 15, 3), 1), (lin(d["fcf_yield"], -0.02, 0.12), 1),
|
| 633 |
+
(lin(d["oe_yield"], -0.02, 0.12), 1), (lin(d["earn_yield"], 0, 0.25), 1)]
|
| 634 |
if bank:
|
| 635 |
qi = [(lin(rroe, -0.1, 0.2), 4), (lin(q["roa"], 0, 0.03), 2), (lin(q["ni_cv"], 2, 0.3), 2),
|
| 636 |
(lin(d["dy"], 0, 0.08), 1), (lin(rni, -0.2, 0.3), 1)]
|
|
|
|
| 697 |
d, q = A["d"], A["q"]
|
| 698 |
sc, n, _ = A["pio"]
|
| 699 |
e = A["exp"]
|
| 700 |
+
oe_v = A["methods"].get("Owner Earnings", (None, ""))[0]
|
| 701 |
return {
|
| 702 |
"Ticker": d["sym"], "Segment": d["seg"], "Sector": d["sector"], "Price": round(d["price"], 2),
|
| 703 |
"Composite": None if A["composite"] is None else round(A["composite"], 1),
|
|
|
|
| 707 |
"ROE %": None if q["roe"] is None else round(q["roe"] * 100, 1),
|
| 708 |
"ROIC %": None if q["roic"] is None else round(q["roic"] * 100, 1),
|
| 709 |
"FCF Yield %": None if d["fcf_yield"] is None else round(d["fcf_yield"] * 100, 1),
|
| 710 |
+
"OE Yield %": None if d["oe_yield"] is None else round(d["oe_yield"] * 100, 1),
|
| 711 |
+
"OE Value": None if oe_v is None else round(oe_v, 2),
|
| 712 |
+
"OE MoS %": None if oe_v is None else round((oe_v - d["price"]) / oe_v * 100, 1),
|
| 713 |
"Piotroski": f"{sc}/{n} (cov {n}/9)" if n else "N/A",
|
| 714 |
"Risk": None if A["risk"] is None else round(A["risk"], 0),
|
| 715 |
"Exp Return % (nom, incl div)": None if not e else round(e["total"] * 100, 1),
|
|
|
|
| 764 |
L.append("\n| Yöntem | Değer/hisse | Not |\n|---|---|---|")
|
| 765 |
for k, (v, n) in A["methods"].items():
|
| 766 |
L.append(f"| {k} | {fm(v)} | {n} |")
|
| 767 |
+
if d["oe"] is not None:
|
| 768 |
+
L.append(f"\n**Owner Earnings [D]:** {fm(d['oe'], 0)} · OE Yield {fp(d['oe_yield'])}")
|
| 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.*")
|
|
|
|
| 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", "GARAN") == "Bank" and segment_of("Real Estate", "EKGYO") == "GYO"
|
| 822 |
and segment_of("Insurance - Life", "ANHYT") == "Insurance" and segment_of("Steel", "EREGL") == "Industrial")
|
| 823 |
+
oe, _ = owner_earnings(100, 50, 80, -10)
|
| 824 |
+
chk("OE: NI100 + DA50 − min(capex80, DA50) + ΔWC(−10) = 90", oe == 90)
|
| 825 |
+
chk("OE: eksik girdi → None (0 değil)", owner_earnings(100, None, 80, 0)[0] is None)
|
| 826 |
+
pr = [dict(end=pd.Timestamp("2023-12-31"), oe=1, fcf=1, ni=1, shares=1)]
|
| 827 |
+
chk("Backtest: tablo dönem sonu+gecikmeden önce kullanılmaz (look-ahead yok)",
|
| 828 |
+
pick_period(pr, pd.Timestamp("2024-02-15"), 90) is None and pick_period(pr, pd.Timestamp("2024-04-15"), 90) is not None)
|
| 829 |
+
chk("Backtest: >550 gün eski tablo reddedilir", pick_period(pr, pd.Timestamp("2025-09-01"), 90) is None)
|
| 830 |
+
st = bt_stats(pd.Series([100, 110, 121], index=pd.to_datetime(["2020-01-01", "2021-01-01", "2022-01-01"])))
|
| 831 |
+
chk("Backtest: 100→121 / 2 yıl → CAGR≈10%", st is not None and abs(st["cagr"] - 0.10) < 0.005)
|
| 832 |
+
st2 = bt_stats(pd.Series([100, 80, 90], index=pd.to_datetime(["2020-01-01", "2020-06-01", "2021-01-01"])))
|
| 833 |
+
chk("Backtest: max drawdown −20%", st2 is not None and abs(st2["mdd"] + 0.2) < 1e-9)
|
| 834 |
return "\n".join(res)
|
| 835 |
|
| 836 |
|
| 837 |
+
# ============================================================================
|
| 838 |
+
# BACKTEST (DENEYSEL, OE odaklı) — point-in-time DEĞİL, yalnızca gösterge
|
| 839 |
+
# ============================================================================
|
| 840 |
+
BT_DEFAULT_LAG = 90 # yıllık tablo, dönem sonu + N gün sonra "yayınlanmış" sayılır
|
| 841 |
+
BT_MAX_AGE = 550 # dönem sonu üzerinden bu kadar gün geçmiş tablo kullanılmaz
|
| 842 |
+
BT_SIGS = {"OE": "OE Yield (top N)", "FCF": "FCF Yield (top N)", "NI": "Kazanç Getirisi (top N)"}
|
| 843 |
+
|
| 844 |
+
|
| 845 |
+
def stmt_at(df, names, date):
|
| 846 |
+
"""Tabloda tam o dönem sonu sütununu bul (kolon sırasına güvenme)."""
|
| 847 |
+
if df is None or getattr(df, "empty", True):
|
| 848 |
+
return None
|
| 849 |
+
cols = [pd.Timestamp(c).normalize() for c in df.columns]
|
| 850 |
+
try:
|
| 851 |
+
i = cols.index(pd.Timestamp(date).normalize())
|
| 852 |
+
except ValueError:
|
| 853 |
+
return None
|
| 854 |
+
return stmt(df, names, i)
|
| 855 |
+
|
| 856 |
+
|
| 857 |
+
def _naive_index(s):
|
| 858 |
+
s = s[~s.index.duplicated(keep="last")].sort_index()
|
| 859 |
+
s.index = pd.DatetimeIndex(s.index).tz_localize(None).normalize()
|
| 860 |
+
return s
|
| 861 |
+
|
| 862 |
+
|
| 863 |
+
def fetch_bt(symbol: str, force: bool = False) -> Optional[Dict[str, Any]]:
|
| 864 |
+
key = f"bt_{symbol}"
|
| 865 |
+
if not force:
|
| 866 |
+
c = _cget(key)
|
| 867 |
+
if c is not None:
|
| 868 |
+
return c
|
| 869 |
+
try:
|
| 870 |
+
t = yf.Ticker(symbol)
|
| 871 |
+
info = t.info or {}
|
| 872 |
+
fin, bs, cf = t.financials, t.balance_sheet, t.cashflow
|
| 873 |
+
h = t.history(period="max", auto_adjust=False)
|
| 874 |
+
try:
|
| 875 |
+
spl = t.splits
|
| 876 |
+
except Exception:
|
| 877 |
+
spl = None
|
| 878 |
+
except Exception as e:
|
| 879 |
+
logger.warning("%s bt fetch failed: %s", symbol, e)
|
| 880 |
+
return None
|
| 881 |
+
if h is None or h.empty or fin is None or fin.empty or "Close" not in h:
|
| 882 |
+
return None
|
| 883 |
+
close = _naive_index(h["Close"].dropna()) # split-ayarlı, temettü-ayarsız → piyasa değeri için
|
| 884 |
+
adj = _naive_index(h["Adj Close"].dropna()) if "Adj Close" in h else close # toplam getiri için
|
| 885 |
+
if spl is not None and len(spl):
|
| 886 |
+
spl = _naive_index(spl)
|
| 887 |
+
fc, cur = info.get("financialCurrency"), info.get("currency")
|
| 888 |
+
unit_ok = (fc is None) or (cur is None) or (fc == cur)
|
| 889 |
+
seg = segment_of(info.get("industry") or "", symbol.replace(".IS", ""))
|
| 890 |
+
shares_now = num(info.get("sharesOutstanding"))
|
| 891 |
+
periods = []
|
| 892 |
+
for c in fin.columns:
|
| 893 |
+
end = pd.Timestamp(c).normalize()
|
| 894 |
+
ni = stmt_at(fin, STMT["ni"][0], end)
|
| 895 |
+
da, cx, wc = (stmt_at(cf, STMT[k][0], end) for k in ("da", "capex", "dwc"))
|
| 896 |
+
cx = abs(cx) if cx is not None else None
|
| 897 |
+
cfo, fcf = stmt_at(cf, STMT["cfo"][0], end), stmt_at(cf, STMT["fcf"][0], end)
|
| 898 |
+
if fcf is None and cfo is not None and cx is not None:
|
| 899 |
+
fcf = cfo - cx
|
| 900 |
+
oe, _ = owner_earnings(ni, da, cx, wc)
|
| 901 |
+
sh = stmt_at(bs, STMT["shares"][0], end)
|
| 902 |
+
if sh is not None and spl is not None and len(spl):
|
| 903 |
+
sh *= float(spl[spl.index > end].prod()) # bedelsiz/bölünme sonrası hisse sayısına getir (fiyat serisiyle tutarlı)
|
| 904 |
+
elif sh is None:
|
| 905 |
+
sh = shares_now # fallback: güncel hisse sayısı (güncel split-ayarlı fiyatla tutarlı)
|
| 906 |
+
periods.append(dict(end=end, oe=oe, fcf=fcf, ni=ni, shares=sh))
|
| 907 |
+
periods.sort(key=lambda p: p["end"])
|
| 908 |
+
d = dict(sym=symbol.replace(".IS", ""), seg=seg, unit_ok=unit_ok, periods=periods, close=close, adj=adj)
|
| 909 |
+
_cset(key, d)
|
| 910 |
+
return d
|
| 911 |
+
|
| 912 |
+
|
| 913 |
+
def pick_period(periods, date, lag):
|
| 914 |
+
"""Look-ahead koruması: yalnızca (dönem sonu + lag) <= tarih olan ve çok eski olmayan en güncel tablo."""
|
| 915 |
+
best = None
|
| 916 |
+
for p in periods:
|
| 917 |
+
if p["end"] + pd.Timedelta(days=lag) <= date and (date - p["end"]).days <= BT_MAX_AGE:
|
| 918 |
+
if best is None or p["end"] > best["end"]:
|
| 919 |
+
best = p
|
| 920 |
+
return best
|
| 921 |
+
|
| 922 |
+
|
| 923 |
+
def px_asof(s, date, tol=10):
|
| 924 |
+
i = s.index.searchsorted(date, side="right") - 1
|
| 925 |
+
if i < 0 or (date - s.index[i]).days > tol:
|
| 926 |
+
return None
|
| 927 |
+
return float(s.iloc[i])
|
| 928 |
+
|
| 929 |
+
|
| 930 |
+
def bt_signals(bd, date, lag):
|
| 931 |
+
p = pick_period(bd["periods"], date, lag)
|
| 932 |
+
if p is None or not bd["unit_ok"] or not p["shares"] or p["shares"] <= 0:
|
| 933 |
+
return None
|
| 934 |
+
c = px_asof(bd["close"], date)
|
| 935 |
+
if c is None or c <= 0:
|
| 936 |
+
return None
|
| 937 |
+
mc = c * p["shares"]
|
| 938 |
+
return {"OE": div(p["oe"], mc), "FCF": div(p["fcf"], mc), "NI": div(p["ni"], mc), "end": p["end"]}
|
| 939 |
+
|
| 940 |
+
|
| 941 |
+
def bt_stats(curve):
|
| 942 |
+
c = curve.dropna()
|
| 943 |
+
if len(c) < 3:
|
| 944 |
+
return None
|
| 945 |
+
yrs = (c.index[-1] - c.index[0]).days / 365.25
|
| 946 |
+
tot = c.iloc[-1] / c.iloc[0] - 1
|
| 947 |
+
cagr = (1 + tot) ** (1 / yrs) - 1 if yrs >= 0.25 and tot > -1 else None
|
| 948 |
+
vol = float(c.pct_change().dropna().std() * np.sqrt(252))
|
| 949 |
+
mdd = float((c / c.cummax() - 1).min())
|
| 950 |
+
return dict(total=tot, cagr=cagr, vol=vol, mdd=mdd, rv=(cagr / vol) if (cagr is not None and vol > 0) else None)
|
| 951 |
+
|
| 952 |
+
|
| 953 |
+
def run_backtest(syms, step_m, top_n, lag, cost_bps, progress):
|
| 954 |
+
data, failed = {}, []
|
| 955 |
+
for i, s in enumerate(syms):
|
| 956 |
+
progress((i + 1) / len(syms), desc=f"Fetching {s}")
|
| 957 |
+
was = _cget(f"bt_{s}") is not None
|
| 958 |
+
bd = fetch_bt(s)
|
| 959 |
+
if bd is None:
|
| 960 |
+
failed.append(s.replace(".IS", ""))
|
| 961 |
+
elif bd["seg"] == "Industrial": # OE banka/sigorta/GYO için anlamlı değil
|
| 962 |
+
data[s] = bd
|
| 963 |
+
if not was:
|
| 964 |
+
time.sleep(REQUEST_DELAY_SEC)
|
| 965 |
+
ends = [p["end"] for bd in data.values() for p in bd["periods"]]
|
| 966 |
+
if not ends:
|
| 967 |
+
return None, None, None, "⚠️ Backtest için yeterli veri çekilemedi."
|
| 968 |
+
T = max(bd["close"].index[-1] for bd in data.values())
|
| 969 |
+
start = (min(ends) + pd.Timedelta(days=lag)).replace(day=1)
|
| 970 |
+
dates = [d for d in pd.date_range(start, T, freq=pd.DateOffset(months=int(step_m))) if d < T]
|
| 971 |
+
keys = list(BT_SIGS) + ["EW"]
|
| 972 |
+
equity, hold, segs, per_ret = {k: 100.0 for k in keys}, {k: [] for k in keys}, {k: [] for k in keys}, {k: [] for k in keys}
|
| 973 |
+
started, n_reb, first_date, n_hold = False, 0, None, []
|
| 974 |
+
for j, D in enumerate(dates):
|
| 975 |
+
D1 = dates[j + 1] if j + 1 < len(dates) else T
|
| 976 |
+
sig = {s: bt_signals(bd, D, lag) for s, bd in data.items()}
|
| 977 |
+
sig = {s: v for s, v in sig.items() if v is not None}
|
| 978 |
+
n_oe = sum(1 for v in sig.values() if v["OE"] is not None)
|
| 979 |
+
if not started:
|
| 980 |
+
if n_oe < max(10, top_n):
|
| 981 |
+
continue
|
| 982 |
+
started, first_date = True, D
|
| 983 |
+
n_reb += 1
|
| 984 |
+
for k in keys:
|
| 985 |
+
if k == "EW":
|
| 986 |
+
new_h = [s for s, v in sig.items() if v["OE"] is not None]
|
| 987 |
+
else:
|
| 988 |
+
rk = sorted(((v[k], s) for s, v in sig.items() if v[k] is not None and v[k] > 0), reverse=True)
|
| 989 |
+
new_h = [s for _, s in rk[:top_n]] if len(rk) >= 3 else hold[k]
|
| 990 |
+
rels = []
|
| 991 |
+
for s in new_h:
|
| 992 |
+
a = data[s]["adj"]
|
| 993 |
+
a0 = px_asof(a, D)
|
| 994 |
+
seg = a[(a.index > D) & (a.index <= D1)]
|
| 995 |
+
if a0 is None or seg.empty:
|
| 996 |
+
continue
|
| 997 |
+
rels.append(seg / a0)
|
| 998 |
+
old = hold[k]
|
| 999 |
+
turn = (1 - len(set(new_h) & set(old)) / max(len(new_h), 1)) if old else 1.0
|
| 1000 |
+
equity[k] *= 1 - cost_bps / 1e4 * (turn * 2 if old else 1)
|
| 1001 |
+
hold[k] = new_h
|
| 1002 |
+
if not rels:
|
| 1003 |
+
continue
|
| 1004 |
+
path = pd.concat(rels, axis=1).sort_index().ffill().fillna(1.0).mean(axis=1)
|
| 1005 |
+
per_ret[k].append(float(path.iloc[-1] - 1))
|
| 1006 |
+
segs[k].append(path * equity[k])
|
| 1007 |
+
equity[k] = float(segs[k][-1].iloc[-1])
|
| 1008 |
+
if k == "OE":
|
| 1009 |
+
n_hold.append(len(rels))
|
| 1010 |
+
if not started or not segs["OE"]:
|
| 1011 |
+
return None, None, None, ("⚠️ Yeterli sinyal yok: yfinance yıllık tablo geçmişi kısa/eksik olduğundan "
|
| 1012 |
+
"en az 10 hissede OE hesaplanabilen bir başlangıç tarihi bulunamadı.")
|
| 1013 |
+
curves = {}
|
| 1014 |
+
for k in keys:
|
| 1015 |
+
if segs[k]:
|
| 1016 |
+
s = pd.concat(segs[k])
|
| 1017 |
+
curves[k] = pd.concat([pd.Series({first_date: 100.0}), s[~s.index.duplicated(keep="last")]]).sort_index()
|
| 1018 |
+
try: # benchmark: BIST100
|
| 1019 |
+
hb = _naive_index(yf.Ticker("XU100.IS").history(start=(first_date - pd.Timedelta(days=10)).strftime("%Y-%m-%d"))["Close"].dropna())
|
| 1020 |
+
b0 = px_asof(hb, first_date)
|
| 1021 |
+
if b0:
|
| 1022 |
+
curves["XU100"] = hb[hb.index >= first_date] / b0 * 100
|
| 1023 |
+
except Exception as e:
|
| 1024 |
+
logger.warning("XU100 fetch failed: %s", e)
|
| 1025 |
+
names = {**BT_SIGS, "EW": "Evren eşit ağırlık (OE verisi olan)", "XU100": "BIST100 (XU100)"}
|
| 1026 |
+
fig = go.Figure()
|
| 1027 |
+
for k, c in curves.items():
|
| 1028 |
+
fig.add_trace(go.Scatter(x=c.index, y=c.values, mode="lines", name=names[k],
|
| 1029 |
+
line=dict(width=3 if k == "OE" else 1.5, dash="dot" if k in ("EW", "XU100") else "solid")))
|
| 1030 |
+
fig.update_layout(title="Kümülatif değer (başlangıç=100, nominal TRY, toplam getiri)", template="plotly_white", height=420,
|
| 1031 |
+
margin=dict(l=10, r=10, t=40, b=10), legend=dict(orientation="h", y=-0.15))
|
| 1032 |
+
rows = []
|
| 1033 |
+
for k, c in curves.items():
|
| 1034 |
+
st = bt_stats(c)
|
| 1035 |
+
if st:
|
| 1036 |
+
rows.append({"Strateji": names[k], "Toplam %": round(st["total"] * 100, 1),
|
| 1037 |
+
"CAGR %": None if st["cagr"] is None else round(st["cagr"] * 100, 1),
|
| 1038 |
+
"Vol % (yıllık)": round(st["vol"] * 100, 1), "Max DD %": round(st["mdd"] * 100, 1),
|
| 1039 |
+
"CAGR/Vol": None if st["rv"] is None else round(st["rv"], 2)})
|
| 1040 |
+
stats = pd.DataFrame(rows)
|
| 1041 |
+
pairs = list(zip(per_ret["OE"], per_ret["EW"]))
|
| 1042 |
+
hit = (sum(1 for a, b in pairs if a > b) / len(pairs)) if pairs else None
|
| 1043 |
+
# güncel OE sepeti (bugünkü sinyal)
|
| 1044 |
+
cur = {s: bt_signals(bd, T, lag) for s, bd in data.items()}
|
| 1045 |
+
cur = {s: v for s, v in cur.items() if v and v["OE"] is not None and v["OE"] > 0}
|
| 1046 |
+
top = sorted(cur.items(), key=lambda kv: kv[1]["OE"], reverse=True)[:top_n]
|
| 1047 |
+
picks = pd.DataFrame([{"Ticker": data[s]["sym"], "OE Yield %": round(v["OE"] * 100, 1),
|
| 1048 |
+
"FCF Yield %": None if v["FCF"] is None else round(v["FCF"] * 100, 1),
|
| 1049 |
+
"Kazanç Get. %": None if v["NI"] is None else round(v["NI"] * 100, 1),
|
| 1050 |
+
"Tablo dönemi": v["end"].date().isoformat()} for s, v in top])
|
| 1051 |
+
msg = (f"✅ {len(data)} sanayi hissesi · {n_reb} rebalans ({first_date.date()} → {T.date()}) · ort. {np.mean(n_hold):.0f} hisse/sepet · "
|
| 1052 |
+
f"OE sepeti, evren eşit ağırlığı {('%' + format(hit * 100, '.0f') + ' dönemde geçti') if hit is not None else 'N/A'} "
|
| 1053 |
+
f"({len(pairs)} dönem)." + (f" ⚠️ Çekilemeyen ({len(failed)}): " + ", ".join(failed) if failed else ""))
|
| 1054 |
+
return fig, stats, picks, msg
|
| 1055 |
+
|
| 1056 |
+
|
| 1057 |
+
BT_NOTES = """**⚠️ Deneysel — sonuçlar yatırım kararı için kullanılamaz.**
|
| 1058 |
+
- **Dönem kısa:** yfinance yalnızca ~4 yıllık yıllık tablo verir → backtest ~3 yıl; istatistiksel olarak anlamsızdır.
|
| 1059 |
+
- **Look-ahead kısmen önlenir:** tablo, dönem sonu + gecikme (varsayılan 90 gün) sonra kullanılır; gerçek yayın tarihi bilinmez, yeniden düzenlemeler (TMS 29 dahil) yansımaz.
|
| 1060 |
+
- **Survivorship bias:** evren bugünün hisse listesi; çıkmış/sönmüş hisseler yok.
|
| 1061 |
+
- **Hisse sayısı:** dönem sonu bilanço hisse sayısı × sonraki bölünme/bedelsiz çarpanı; sermaye artırımları/geri alımlar tam yakalanmaz.
|
| 1062 |
+
- Banka/sigorta/GYO hariç (OE anlamlı değil). Getiriler nominal TRY, temettü dahil (Adj Close); maliyet tek yön bps, eşit ağırlık, buy-and-hold dönem içi."""
|
| 1063 |
+
|
| 1064 |
+
|
| 1065 |
# ============================================================================
|
| 1066 |
# GRADIO UI
|
| 1067 |
# ============================================================================
|
| 1068 |
|
| 1069 |
def build_ui():
|
| 1070 |
with gr.Blocks(title="BIST Value Intelligence v3", theme=gr.themes.Soft()) as demo:
|
| 1071 |
+
gr.Markdown("# BIST Value Intelligence v3\n**⚠️ yfinance verisi point-in-time değildir; Backtest sekmesi yalnızca deneyseldir/göstergedir. "
|
| 1072 |
"Eksik veri `N/A` gösterilir, asla sıfırla doldurulmaz. 12 aylık senaryolar heuristiktir, istatistiksel tahmin değildir. "
|
| 1073 |
"Yatırım tavsiyesi değildir.**")
|
| 1074 |
with gr.Accordion("⚙️ Varsayımlar [A] (tüm sekmeler için)", open=False):
|
|
|
|
| 1093 |
f_c = gr.Slider(0, 100, 0, step=5, label="Min composite")
|
| 1094 |
f_m = gr.Slider(-100, 100, -100, step=5, label="Min MoS % (-100 = kapalı)")
|
| 1095 |
with gr.Row():
|
| 1096 |
+
f_sort = gr.Dropdown(["Composite", "MoS %", "P/E", "P/B", "ROE %", "ROIC %", "FCF Yield %", "OE Yield %", "OE MoS %", "Risk",
|
| 1097 |
"Exp Return % (nom, incl div)", "Real Exp Return %", "Coverage %"],
|
| 1098 |
value="Composite", label="Sırala")
|
| 1099 |
f_desc = gr.Checkbox(label="Azalan", value=True)
|
|
|
|
| 1162 |
if d["oe_parts"]:
|
| 1163 |
o = d["oe_parts"]
|
| 1164 |
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)) "
|
| 1165 |
+
f"{fm(o['maint_est'], 0)} + ΔİS sermayesi {fm(o['dwc'], 0)} = **{fm(d['oe'], 0)}** (toplam capex {fm(o['capex_total'], 0)}) · OE Yield {fp(d['oe_yield'])}")
|
| 1166 |
L.append("\n| Yöntem | Değer | Not |\n|---|---|---|")
|
| 1167 |
for k, (v, n) in A["methods"].items():
|
| 1168 |
L.append(f"| {k} | {fm(v)} | {n} |")
|
|
|
|
| 1350 |
|
| 1351 |
b8.click(dq, None, [md8, tb8])
|
| 1352 |
|
| 1353 |
+
with gr.Tab("📈 Backtest (OE, deneysel)"):
|
| 1354 |
+
gr.Markdown(BT_NOTES)
|
| 1355 |
+
with gr.Row():
|
| 1356 |
+
bt_top = gr.Slider(5, 30, 10, step=1, label="Sepet büyüklüğü (top N)")
|
| 1357 |
+
bt_step = gr.Dropdown([3, 6, 12], value=3, label="Rebalans (ay)")
|
| 1358 |
+
bt_lag = gr.Slider(30, 180, BT_DEFAULT_LAG, step=15, label="Yayın gecikmesi (gün)")
|
| 1359 |
+
with gr.Row():
|
| 1360 |
+
bt_cost = gr.Slider(0, 100, 20, step=5, label="İşlem maliyeti (bps, tek yön)")
|
| 1361 |
+
bt_n = gr.Slider(20, len(BIST_UNIVERSE), len(BIST_UNIVERSE), step=1, label="Evren: ilk N hisse (az = hızlı)")
|
| 1362 |
+
bt_btn = gr.Button("Backtest çalıştır", variant="primary")
|
| 1363 |
+
bt_msg = gr.Markdown()
|
| 1364 |
+
bt_plot = gr.Plot()
|
| 1365 |
+
bt_stats_df = gr.Dataframe(interactive=False, label="Performans")
|
| 1366 |
+
gr.Markdown("**Güncel OE sepeti** (bugünkü sinyal; öneri değildir)")
|
| 1367 |
+
bt_picks = gr.Dataframe(interactive=False)
|
| 1368 |
+
|
| 1369 |
+
def do_bt(top, step, lag, cost, n, progress=gr.Progress()):
|
| 1370 |
+
fig, stats, picks, msg = run_backtest(BIST_UNIVERSE[: int(n)], int(step), int(top), int(lag), float(cost), progress)
|
| 1371 |
+
return msg, fig, stats, picks
|
| 1372 |
+
|
| 1373 |
+
bt_btn.click(do_bt, [bt_top, bt_step, bt_lag, bt_cost, bt_n], [bt_msg, bt_plot, bt_stats_df, bt_picks])
|
| 1374 |
+
|
| 1375 |
with gr.Tab("📐 Methodology"):
|
| 1376 |
gr.Markdown(METHODOLOGY)
|
| 1377 |
b9 = gr.Button("Run self-test (synthetic data)")
|
|
|
|
| 1383 |
METHODOLOGY = """
|
| 1384 |
### Değerleme mimarisi
|
| 1385 |
- **DCF (sanayi/GYO 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**.
|
| 1386 |
+
- **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. **OE Yield** = OE / piyasa değeri; sanayi segmentinde Valuation skoruna (ağırlık 1) dahildir, taramada OE Yield / OE Value / OE MoS sütunları vardır.
|
| 1387 |
- **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.
|
| 1388 |
- **GYO:** NAV vekili = defter değeri (ekspertiz NAV'ı yok), P/NAV. Doluluk verisi yok.
|
| 1389 |
- **Relative:** sektör medyanı (en az 5 geçerli peer) P/E, P/B, EV/EBITDA → hisse başına değer.
|
|
|
|
| 1404 |
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.
|
| 1405 |
|
| 1406 |
### Bilinen veri sınırlamaları
|
| 1407 |
+
- yfinance point-in-time değildir; Backtest sekmesi deneyseldir (≈3 yıl, survivorship bias, tahmini yayın gecikmesi) ve kanıt değil göstergedir. Bilanço tarihleri ve kapsam hisseden hisseye değişir.
|
| 1408 |
- Yıllık tablolar gecikmelidir; TFRS/enflasyon muhasebesi (TMS 29) etkileri nominal rakamları çarpıtabilir.
|
| 1409 |
- Evren: gömülü liste + eklediğiniz tickerlar (tüm BIST listesi Yahoo'dan ücretsiz çekilemez).
|
| 1410 |
- Sektör/segment sınıflaması Yahoo `industry` alanına dayanır; peer sayısı <5 ise medyan hesaplanmaz.
|