Finansal Sağlık skoru
Gelir, EPS, marj ve nakit akışıyla tam tablo örneği.
Gereksinimler
Desteklenen bir hisse grafiği gerekir. Gelir için son dönem ile 1, 2, 3, 4 ve 8 dönem öncesi; brüt kâr, seyreltilmiş EPS ve serbest nakit akışı için son dönem ile dört dönem öncesi kullanılır. Bu nedenle yalnız birkaç çeyreklik verisi olan şirkette VERİ YETERSİZ görünmesi beklenir.
Puanlama
| Bileşen | Azami puan |
|---|---|
| Yıllık gelir büyümesi | 20 |
| Gelir büyüme ivmesi | 10 |
| Gelir oynaklığı, CV | 10 |
| EPS büyümesi | 20 |
| EPS büyümesinin geliri geçmesi | 10 |
| Brüt marj | 15 |
| Brüt marj trendi | 5 |
| Serbest nakit akışı marjı | 7 |
| Serbest nakit akışı büyümesi | 3 |
Hedefe ulaşma oranı 0–1 aralığına sınırlanır ve math.pow(ratio, 1.8) ile puanlanır; doğrusal puanlama değildir. Gelir CV’si beş gözlemin popülasyon standart sapmasının mutlak ortalamaya oranıdır. Eksik gözlemler CV hesabına katılmaz; ancak tam skorun veri hazır kontrolü bütün gerekli dönemleri ister. CV %100 üzerindeyse 20 puan ceza vardır.
Boolean koşulu kapatmak o kriterin puanını devre dışı bırakmaz; şartı sağlanmış kabul eder. Ağırlık toplamı 100 olsa da nihai sonuç 0–99’a sınırlanıp yuvarlanır. Bu nedenle panel /100 yazsa da 100 puan üretmez. Bunlar örneğin bilinçli formül kurallarıdır.
Etiketler ve sınırlar
85 ve üzeri POZİTİF+, 70–84 POZİTİF, 51–69 NÖTR, altı NEGATİF. Veri hazır değilse sayısal puan yerine —, görünümde VERİ YETERSİZ yazılır. Son raporlanmış bilanço snapshot’ıdır; geçmiş performans veya işlem sinyali değildir.
Tablo konumu ayarlardan değiştirilebilir. Tam kod iki dilde aynı tutulur; içindeki kullanıcı başlıkları Türkçedir. Pine kullanıcı fonksiyonları ve dizileri açık formüllere dönüştürülmüştür.
Tam kod
//@selscript=1
indicator("Sağlık Skoru", overlay=true)
// Kullanıcı girişleri
i_revGrowthMin = input.float("Min. Yıllık Gelir Büyümesi (%)", 25.0, min=0, step=5)
i_revenueAccel = input.bool("Gelir ivmesi artıyor mu? (4 çeyrek)", true)
i_grossMarginMin = input.float("Min. Brüt Marj (%)", 30.0, min=0, step=5)
i_grossMarginTrend = input.bool("Brüt marj trendi yükselen mi?", true)
i_revenueCvMax = input.float("Max Gelir Oynaklığı — CV% eşiği", 30.0, min=0, step=5)
i_epsGrowthMin = input.float("Min. EPS Büyümesi YoY (%)", 25.0, min=0, step=5)
i_epsBeatRev = input.bool("EPS büyümesi geliri geçmeli mi?", true)
i_fcfMarginMin = input.float("Min. FCF Marjı (%)", 15.0, min=0, step=5)
// Puan ağırlıkları
W_REV_GROWTH = 20.0
W_REV_ACCEL = 10.0
W_REV_CV = 10.0
W_EPS_GROWTH = 20.0
W_EPS_BEAT = 10.0
W_GROSS_MGN = 15.0
W_GM_TREND = 5.0
W_FCF_MARGIN = 7.0
W_FCF_GROWTH = 3.0
HIGH_CV_PENALTY = 20.0
// Son raporlanmış bilanço snapshot’ı. offset, grafik barı değil mali dönem sayısıdır.
rev_q0 = request.financial(market.symbol, "TOTAL_REVENUE", "FQ", offset=0)
rev_q1 = request.financial(market.symbol, "TOTAL_REVENUE", "FQ", offset=1)
rev_q2 = request.financial(market.symbol, "TOTAL_REVENUE", "FQ", offset=2)
rev_q3 = request.financial(market.symbol, "TOTAL_REVENUE", "FQ", offset=3)
rev_q4 = request.financial(market.symbol, "TOTAL_REVENUE", "FQ", offset=4)
rev_q8 = request.financial(market.symbol, "TOTAL_REVENUE", "FQ", offset=8)
gp_q0 = request.financial(market.symbol, "GROSS_PROFIT", "FQ", offset=0)
gp_q4 = request.financial(market.symbol, "GROSS_PROFIT", "FQ", offset=4)
eps_q0 = request.financial(market.symbol, "EARNINGS_PER_SHARE_DILUTED", "FQ", offset=0)
eps_q4 = request.financial(market.symbol, "EARNINGS_PER_SHARE_DILUTED", "FQ", offset=4)
fcf_q0 = request.financial(market.symbol, "FREE_CASH_FLOW", "FQ", offset=0)
fcf_q4 = request.financial(market.symbol, "FREE_CASH_FLOW", "FQ", offset=4)
// Büyüme ve ivme
revGrowthNow = not is_na(rev_q0) and not is_na(rev_q4) and rev_q4 != 0 ? (rev_q0 - rev_q4) / math.abs(rev_q4) * 100 : na
revGrowthPrev = not is_na(rev_q4) and not is_na(rev_q8) and rev_q8 != 0 ? (rev_q4 - rev_q8) / math.abs(rev_q8) * 100 : na
accelOk = not i_revenueAccel or (not is_na(revGrowthNow) and not is_na(revGrowthPrev) and revGrowthNow > revGrowthPrev)
// Beş gelir gözlemi: array yerine açık toplam ve popülasyon varyansı.
// Eksik gözlemler ortalamaya/standart sapmaya katılmaz; hepsi eksikse na.
revValid0 = not is_na(rev_q0)
revValid1 = not is_na(rev_q1)
revValid2 = not is_na(rev_q2)
revValid3 = not is_na(rev_q3)
revValid4 = not is_na(rev_q4)
revCount = (revValid0 ? 1 : 0) + (revValid1 ? 1 : 0) + (revValid2 ? 1 : 0) + (revValid3 ? 1 : 0) + (revValid4 ? 1 : 0)
revSum = (revValid0 ? rev_q0 : 0) + (revValid1 ? rev_q1 : 0) + (revValid2 ? rev_q2 : 0) + (revValid3 ? rev_q3 : 0) + (revValid4 ? rev_q4 : 0)
revMean = revCount > 0 ? revSum / revCount : na
revDiff0 = revValid0 ? rev_q0 - revMean : 0
revDiff1 = revValid1 ? rev_q1 - revMean : 0
revDiff2 = revValid2 ? rev_q2 - revMean : 0
revDiff3 = revValid3 ? rev_q3 - revMean : 0
revDiff4 = revValid4 ? rev_q4 - revMean : 0
revSquaredSum = revDiff0 * revDiff0 + revDiff1 * revDiff1 + revDiff2 * revDiff2 + revDiff3 * revDiff3 + revDiff4 * revDiff4
revStdev = revCount > 0 ? math.sqrt(revSquaredSum / revCount) : na
revCV = not is_na(revMean) and revMean != 0 ? revStdev / math.abs(revMean) * 100 : na
epsGrowth = not is_na(eps_q0) and not is_na(eps_q4) and eps_q4 != 0 ? (eps_q0 - eps_q4) / math.abs(eps_q4) * 100 : na
epsBeatOk = not i_epsBeatRev or (not is_na(epsGrowth) and not is_na(revGrowthNow) and epsGrowth > revGrowthNow)
gmNow = not is_na(gp_q0) and not is_na(rev_q0) and rev_q0 != 0 ? gp_q0 / rev_q0 * 100 : na
gmPrev = not is_na(gp_q4) and not is_na(rev_q4) and rev_q4 != 0 ? gp_q4 / rev_q4 * 100 : na
gmTrendOk = not i_grossMarginTrend or (not is_na(gmNow) and not is_na(gmPrev) and gmNow > gmPrev)
fcfMargin = not is_na(fcf_q0) and not is_na(rev_q0) and rev_q0 != 0 ? fcf_q0 / rev_q0 * 100 : na
fcfGrowth = not is_na(fcf_q0) and not is_na(fcf_q4) and fcf_q4 != 0 ? (fcf_q0 - fcf_q4) / math.abs(fcf_q4) * 100 : na
fcfGrowthOk = not is_na(fcfGrowth) and fcfGrowth > 0
// Kullanıcı fonksiyonları yerine açılmış puan formülleri.
// math.pow(ratio, 1.8) korunur; doğrusal yaklaşımla değiştirilmez.
ratioRevGrowth = i_revGrowthMin > 0 and not is_na(revGrowthNow) ? math.max(0.0, math.min(1.0, revGrowthNow / i_revGrowthMin)) : na
scoreRevGrowth = not is_na(ratioRevGrowth) ? math.pow(ratioRevGrowth, 1.8) * W_REV_GROWTH : (not is_na(revGrowthNow) and i_revGrowthMin <= 0 and revGrowthNow >= 0 ? W_REV_GROWTH : 0.0)
ratioEpsGrowth = i_epsGrowthMin > 0 and not is_na(epsGrowth) ? math.max(0.0, math.min(1.0, epsGrowth / i_epsGrowthMin)) : na
scoreEpsGrowth = not is_na(ratioEpsGrowth) ? math.pow(ratioEpsGrowth, 1.8) * W_EPS_GROWTH : (not is_na(epsGrowth) and i_epsGrowthMin <= 0 and epsGrowth >= 0 ? W_EPS_GROWTH : 0.0)
ratioGrossMgn = i_grossMarginMin > 0 and not is_na(gmNow) ? math.max(0.0, math.min(1.0, gmNow / i_grossMarginMin)) : na
scoreGrossMgn = not is_na(ratioGrossMgn) ? math.pow(ratioGrossMgn, 1.8) * W_GROSS_MGN : (not is_na(gmNow) and i_grossMarginMin <= 0 and gmNow >= 0 ? W_GROSS_MGN : 0.0)
ratioFcfMargin = i_fcfMarginMin > 0 and not is_na(fcfMargin) ? math.max(0.0, math.min(1.0, fcfMargin / i_fcfMarginMin)) : na
scoreFcfMargin = not is_na(ratioFcfMargin) ? math.pow(ratioFcfMargin, 1.8) * W_FCF_MARGIN : (not is_na(fcfMargin) and i_fcfMarginMin <= 0 and fcfMargin >= 0 ? W_FCF_MARGIN : 0.0)
ratioRevCV = i_revenueCvMax > 0 and not is_na(revCV) ? math.max(0.0, math.min(1.0, (i_revenueCvMax * 2.0 - revCV) / i_revenueCvMax)) : na
scoreRevCV = not is_na(ratioRevCV) ? math.pow(ratioRevCV, 1.8) * W_REV_CV : (not is_na(revCV) and i_revenueCvMax <= 0 and revCV <= 0 ? W_REV_CV : 0.0)
scoreRevAccel = accelOk ? W_REV_ACCEL : 0.0
scoreEpsBeat = epsBeatOk ? W_EPS_BEAT : 0.0
scoreGmTrend = gmTrendOk ? W_GM_TREND : 0.0
scoreFcfGrowth = fcfGrowthOk ? W_FCF_GROWTH : 0.0
highCvPenalty = not is_na(revCV) and revCV > 100.0 ? HIGH_CV_PENALTY : 0.0
totalScoreRaw = (
scoreRevGrowth + scoreRevAccel + scoreRevCV + scoreEpsGrowth +
scoreEpsBeat + scoreGrossMgn + scoreGmTrend + scoreFcfMargin +
scoreFcfGrowth - highCvPenalty
)
totalScore = math.round(math.max(0.0, math.min(99.0, totalScoreRaw)))
dataReady = not is_na(rev_q0) and not is_na(rev_q1) and not is_na(rev_q2) and not is_na(rev_q3) and not is_na(rev_q4) and not is_na(rev_q8) and not is_na(gmNow) and not is_na(gmPrev) and not is_na(epsGrowth) and not is_na(revGrowthNow) and not is_na(revGrowthPrev) and not is_na(fcfMargin) and not is_na(fcfGrowth)
scoreLabel = not dataReady ? "VERİ YETERSİZ" : totalScore >= 85 ? "POZİTİF+" : totalScore >= 70 ? "POZİTİF" : totalScore >= 51 ? "NÖTR" : "NEGATİF"
scoreColor = not dataReady ? "#9ba3aa" : totalScore >= 85 ? "#69f08b" : totalScore >= 70 ? "#4ade80" : totalScore >= 51 ? "#fbbf24" : "#f87171"
trendIcon = not dataReady ? "—" : totalScore >= 70 ? "↗" : totalScore >= 51 ? "→" : "↘"
statusIcon = not dataReady ? "—" : totalScore >= 70 ? "✓" : totalScore >= 51 ? "—" : "✕"
// Sabit tablo slotu; yalnız son barın paneli güncellenir.
// table.new her barda yeni nesne oluşturmaz.
// Birleşimler hazırlıkta doğrulanır.
panelBg = "#031016f2"
lineColor = totalScore >= 85 ? "#69f08b4d" : totalScore >= 70 ? "#4ade804d" : totalScore >= 51 ? "#fbbf244d" : "#f871714d"
iconBg = totalScore >= 85 ? "#69f08b1f" : totalScore >= 70 ? "#4ade801f" : totalScore >= 51 ? "#fbbf241f" : "#f871711f"
mutedText = "#d7dfe6"
noteColor = "#9ba3aa"
W_COL_ICON = 3.0
W_COL_LABEL = 8.0
W_COL_VALUE = 8.0
W_COL_STATUS = 3.0
W_MERGED = W_COL_ICON * 4
t = table.new("top_left", 4, 6, bgcolor=panelBg, border_width=0, frame_width=2, frame_color=scoreColor)
table.merge_cells(t, 0, 0, 3, 0)
table.merge_cells(t, 0, 5, 3, 5)
if barstate.islast:
table.cell(t, 0, 0, " FİNANSAL SAĞLIK", text_color="#ffffff", bgcolor=panelBg, text_size="large", text_halign="left", text_valign="center", width=W_MERGED)
table.cell(t, 0, 1, "", bgcolor=lineColor, height=1, width=W_COL_ICON)
table.cell(t, 1, 1, "", bgcolor=lineColor, height=1, width=W_COL_LABEL)
table.cell(t, 2, 1, "", bgcolor=lineColor, height=1, width=W_COL_VALUE)
table.cell(t, 3, 1, "", bgcolor=lineColor, height=1, width=W_COL_STATUS)
table.cell(t, 0, 3, "", bgcolor=lineColor, height=1, width=W_COL_ICON)
table.cell(t, 1, 3, "", bgcolor=lineColor, height=1, width=W_COL_LABEL)
table.cell(t, 2, 3, "", bgcolor=lineColor, height=1, width=W_COL_VALUE)
table.cell(t, 3, 3, "", bgcolor=lineColor, height=1, width=W_COL_STATUS)
table.cell(t, 0, 2, trendIcon, text_color=scoreColor, bgcolor=iconBg, text_size="normal", text_halign="center", text_valign="center", width=W_COL_ICON)
table.cell(t, 1, 2, "Görünüm:", text_color=mutedText, bgcolor=panelBg, text_size="normal", text_halign="left", text_valign="center", width=W_COL_LABEL)
table.cell(t, 2, 2, scoreLabel, text_color=scoreColor, bgcolor=panelBg, text_size="normal", text_halign="center", text_valign="center", width=W_COL_VALUE)
table.cell(t, 3, 2, statusIcon, text_color=scoreColor, bgcolor=iconBg, text_size="normal", text_halign="center", text_valign="center", width=W_COL_STATUS)
table.cell(t, 0, 4, "🛡", text_color=scoreColor, bgcolor=iconBg, text_size="normal", text_halign="center", text_valign="center", width=W_COL_ICON)
table.cell(t, 1, 4, "Sağlık Skoru:", text_color=mutedText, bgcolor=panelBg, text_size="normal", text_halign="left", text_valign="center", width=W_COL_LABEL)
table.cell(t, 2, 4, (dataReady ? str.tostring(totalScore, 0) + "/100" : "—"), text_color=scoreColor, bgcolor=panelBg, text_size="normal", text_halign="center", text_valign="center", width=W_COL_VALUE)
table.cell(t, 3, 4, statusIcon, text_color=scoreColor, bgcolor=iconBg, text_size="normal", text_halign="center", text_valign="center", width=W_COL_STATUS)
table.cell(t, 0, 5, (dataReady ? "Son raporlanmış bilanço verileri · Tarihsel skor değildir." : "Puan için gerekli mali dönem veya metrik eksik."), text_color=noteColor, bgcolor=panelBg, text_size="tiny", text_halign="left", text_valign="center", width=W_MERGED)