Skip to Main Content
Back to website
Indicator Disclaimer | Report this indicator

Winvest Fundamental Rating — Earnings Grade + Buy Zone

TSBuild26

A grade, a level to buy, and a stop — from the business, not just the chart.

It grades a company A to F from its recent earnings, shows whether that grade is improving or slipping, and marks the price where it's worth accumulating. When the business doesn't earn the grade, it still shows the level, greyed out and labelled suppressed, so you can see the setup it's refusing to take.

The grade

Five components, each scored from the company's own reports:

  • Beat rate — how often it beat estimates over the last eight quarters
  • Surprise size — by how much
  • EPS growth — year over year, same quarter
  • Growth quality — whether growth is backed by revenue or produced by margins. Two companies can both post 30%; only one of them can repeat it.
  • Cash conversion — how much of reported profit turned into free cash flow

Each component carries a weight you can change, so a value investor and a growth investor can both use it. At the defaults they count equally.

Grade trend — the grade as of each of the last four reports, oldest to newest, e.g. improving · D → C → B → B. A B that used to be a D is a different stock from a B that used to be an A.

On the chart

🟢 Buy zone — an unfilled gap sitting on a heavy volume shelf. Green on an A or B grade.

🟠 Watch zone — the same level on a C. Worth watching, not buying.

⚪ Suppressed — the level on a D or F, greyed out, with no stop. The tool found it and declined it.

🔴 Stop — below the volume shelf holding the zone up, with the risk percentage in the panel.

🔷 Earnings markers — a diamond on every graded quarter, below the bar for a beat and above it for a miss.

The panel also shows the sector, the current P/E, and the prices where the stock has historically been cheap and expensive on its own earnings. The volume shelves and P/E lines are one click away.

How to use it

A or B, near the zone — the zone is your accumulation range and the stop is your invalidation. Size from the risk percentage before you enter.

D or F — the level is shown and suppressed. A clean pullback in a deteriorating business is still a deteriorating business.

Scan for it. Eight registered signals: grade A or B, grade improving, in the buy zone, near the buy zone, level suppressed, strong cash conversion, at P/E floor, at P/E ceiling.

Daily chart, single stocks. The panel adapts to light and dark themes.


Works well with

Auto Option Contract Selector — Target, DTE & Contract — on an A or B name, turn the zone into a specific contract, including longer-dated ones for a longer hold.

Earnings Whale Flow + Expected Move — in earnings season, see whether big money is positioning into a name the grade says is sound.

Unusual Option Flows — when a whale buys a company you already trust, you know whether to follow.

📬 Free weekly newsletter — market structure, options flow, and the longer-term positions I'm building: winvest-insights.beehiiv.com

📈 Watch me trade — my live 30-minute setups on StockTwits, taken with the Multi-Target Options Trade Plan Pro: stocktwits.com/winvestinsights

For educational purposes only. Not financial advice.

describe_indicator('Winvest Fundamental Rating', 'price', { shortName: 'WFR' });

const lookQ = input.number('Quarters To Grade', 8);
const bigBeatPct = input.number('Big Beat %', 10);
const strongGrowth = input.number('Strong Growth %', 20);
const profBars = input.number('Profile Bars', 250, { min: 20, max: 2000 });
const profBins = input.number('Profile Bins', 50);
const hvnPct = input.number('HVN Threshold %', 70);
const fvgLook = input.number('FVG Lookback', 250, { min: 10, max: 2000 });
const stopAtr = input.number('Stop Buffer ATR', 0.5);
const peLook = input.number('PE Lookback Bars', 1250, { min: 60, max: 5000 });
const peLoPct = input.number('PE Low Pctile', 10);
const peHiPct = input.number('PE High Pctile', 90);
const pickNearest = input.number('Pick Nearest 1/0', 0);
const lineBars = input.number('Line Bars 0=all', 80);
const minZoneAtr = input.number('Min Zone ATR', 0.25);
const wBeat = input.number('Weight Beat Rate', 1);
const wSize = input.number('Weight Surprise', 1);
const wGrow = input.number('Weight EPS Growth', 1);
const wQual = input.number('Weight Growth Qual', 1);
const wCash = input.number('Weight Cash Conv', 1);

const n = close.length;
const spot = close[n - 1];
const lastBar = n - 1;

const atrArr = atr(14);
const atrNow = atrArr[n - 1] !== null && atrArr[n - 1] !== undefined ? atrArr[n - 1] : 0;

const GREEN = '#00c853';
const YELLOW = '#fdd835';
const DEEPRED = '#b71c1c';
const AMBER = '#ffa000';
const ORANGE = '#ff6d00';
const RED = '#e53935';
const GREY = '#9e9e9e';
const PURPLE = '#7b1fa2';
const BLUE = '#1e88e5';

function toMs(ts) {
    let t = ts;
    if (t < 1e12) t = t * 1000;
    return t;
}
let minStep = 1e15;
let stepFrom = n - 300;
if (stepFrom < 1) stepFrom = 1;
for (let i = stepFrom; i < n; i++) {
    const dt = toMs(time[i]) - toMs(time[i - 1]);
    if (dt > 0 && dt < minStep) minStep = dt;
}
const isDaily = minStep >= 82800000 && minStep <= 345600000;

let raw = [];
let fundRaw = null;
if (isDaily) {
    raw = await request.earnings(current.ticker, {});
    try {
        fundRaw = await request.fundamental(current.ticker, ['net_income', 'free_cash_flow'], 8);
    } catch (fundErr) {
        fundRaw = null;
    }
}

const recs = [];
if (raw && Array.isArray(raw)) {
    for (let i = 0; i < raw.length; i++) {
        const r = raw[i];
        if (!r) continue;
        if (r.isFuture === true) continue;
        if (r.timestamp === undefined) continue;
        if (r.eps === null || r.eps === undefined) continue;
        recs.push(r);
    }
}
recs.sort((a, b) => b.timestamp - a.timestamp);

const isRateable = recs.length > 0;

function findYearAgo(rec) {
    for (let i = 0; i < recs.length; i++) {
        const c = recs[i];
        if (c.period === rec.period && c.period_year === rec.period_year - 1) return c;
    }
    return null;
}

function fundOrderKey(rec, idx) {
    if (typeof rec.timestamp === 'number') return rec.timestamp;
    if (typeof rec.date === 'number') return rec.date;
    if (rec.year !== undefined && rec.quarter !== undefined) {
        const qMatch = String(rec.quarter).match(/[1-4]/);
        const qn = qMatch ? Number(qMatch[0]) : 0;
        return Number(rec.year) * 10 + qn;
    }
    return idx;
}
function lastFourSum(metricId, skip) {
    if (!fundRaw || fundRaw.error || !Array.isArray(fundRaw[metricId])) return null;
    const pts = [];
    for (let i = 0; i < fundRaw[metricId].length; i++) {
        const r = fundRaw[metricId][i];
        if (!r || typeof r.value !== 'number' || !isFinite(r.value)) continue;
        pts.push({ k: fundOrderKey(r, i), v: r.value });
    }
    if (pts.length < 4 + skip) return null;
    pts.sort((a, b) => a.k - b.k);
    let total = 0;
    const end = pts.length - skip;
    for (let i = end - 4; i < end; i++) total = total + pts[i].v;
    return total;
}

function gradeOf(pct, ok) {
    if (!ok) return 'N/A';
    if (pct >= 0.9) return 'A';
    if (pct >= 0.75) return 'B';
    if (pct >= 0.58) return 'C';
    if (pct >= 0.41) return 'D';
    return 'F';
}

function scoreAt(skip) {
    const part = recs.slice(skip);
    const o = { graded: 0, beatRate: 0, avgSurp: 0, epsGrowth: 0, revGrowth: 0, haveGrowth: false,
                sBeat: 0, sSize: 0, sGrow: 0, sQual: 0, qualTxt: 'no data',
                cashConv: null, haveCash: false, sCash: 0, pct: 0, grade: 'N/A' };
    let beatsN = 0;
    let surpSum = 0;
    let surpCount = 0;
    for (let i = 0; i < part.length; i++) {
        if (o.graded >= lookQ) break;
        const r = part[i];
        if (r.eps_est === null || r.eps_est === undefined) continue;
        o.graded = o.graded + 1;
        if (r.eps > r.eps_est) beatsN = beatsN + 1;
        const denom = Math.abs(r.eps_est);
        if (denom > 0.01) {
            surpSum = surpSum + ((r.eps - r.eps_est) / denom) * 100;
            surpCount = surpCount + 1;
        }
    }
    o.beatRate = o.graded > 0 ? (beatsN / o.graded) * 100 : 0;
    o.avgSurp = surpCount > 0 ? surpSum / surpCount : 0;

    if (part.length > 0) {
        const latest = part[0];
        const prior = findYearAgo(latest);
        if (prior !== null) {
            if (prior.eps !== null && Math.abs(prior.eps) > 0.01) {
                o.epsGrowth = ((latest.eps - prior.eps) / Math.abs(prior.eps)) * 100;
                o.haveGrowth = true;
            }
            if (latest.revenue !== null && prior.revenue !== null && prior.revenue > 0) {
                o.revGrowth = ((latest.revenue - prior.revenue) / prior.revenue) * 100;
            }
        }
    }

    if (o.beatRate >= 87.5) o.sBeat = 3;
    else if (o.beatRate >= 75) o.sBeat = 2;
    else if (o.beatRate >= 50) o.sBeat = 1;

    if (o.avgSurp >= bigBeatPct) o.sSize = 3;
    else if (o.avgSurp >= 5) o.sSize = 2;
    else if (o.avgSurp > 0) o.sSize = 1;

    if (o.epsGrowth >= strongGrowth) o.sGrow = 3;
    else if (o.epsGrowth >= 10) o.sGrow = 2;
    else if (o.epsGrowth > 0) o.sGrow = 1;

    if (o.haveGrowth) {
        if (o.epsGrowth <= 0) { o.sQual = 0; o.qualTxt = 'EPS declining'; }
        else if (o.revGrowth >= o.epsGrowth * 0.7) { o.sQual = 3; o.qualTxt = 'revenue backed'; }
        else if (o.revGrowth >= o.epsGrowth * 0.4) { o.sQual = 2; o.qualTxt = 'partly revenue backed'; }
        else if (o.revGrowth > 0) { o.sQual = 1; o.qualTxt = 'margin driven'; }
        else { o.sQual = 0; o.qualTxt = 'revenue flat or falling'; }
    }

    const ni = lastFourSum('net_income', skip);
    const fcf = lastFourSum('free_cash_flow', skip);
    if (ni !== null && fcf !== null && ni > 0) o.cashConv = fcf / ni;
    o.haveCash = o.cashConv !== null;
    if (o.haveCash) {
        if (o.cashConv >= 1.0) o.sCash = 3;
        else if (o.cashConv >= 0.8) o.sCash = 2;
        else if (o.cashConv >= 0.5) o.sCash = 1;
    }

    const wb = wBeat > 0 ? wBeat : 0;
    const ws = wSize > 0 ? wSize : 0;
    const wg = wGrow > 0 ? wGrow : 0;
    const wq = wQual > 0 ? wQual : 0;
    const wc = wCash > 0 ? wCash : 0;
    let got = wb * o.sBeat + ws * o.sSize + wg * o.sGrow + wq * o.sQual;
    let most = 3 * (wb + ws + wg + wq);
    if (o.haveCash) {
        got = got + wc * o.sCash;
        most = most + 3 * wc;
    }
    if (most <= 0) {
        got = o.sBeat + o.sSize + o.sGrow + o.sQual + (o.haveCash ? o.sCash : 0);
        most = o.haveCash ? 15 : 12;
    }
    o.pct = got / most;
    o.grade = gradeOf(o.pct, part.length > 0 && o.graded >= 3);
    return o;
}

const cur = scoreAt(0);
const graded = cur.graded;
const beatRate = cur.beatRate;
const avgSurp = cur.avgSurp;
const epsGrowth = cur.epsGrowth;
const revGrowth = cur.revGrowth;
const haveGrowth = cur.haveGrowth;
const sBeat = cur.sBeat;
const sSize = cur.sSize;
const sGrow = cur.sGrow;
const sQual = cur.sQual;
const qualTxt = cur.qualTxt;
const cashConv = cur.cashConv;
const haveCash = cur.haveCash;
const sCash = cur.sCash;
const scorePct = cur.pct;
const grade = (!isRateable || graded < 3) ? 'N/A' : cur.grade;

let gColor = GREY;
if (grade === 'A') gColor = GREEN;
else if (grade === 'B') gColor = YELLOW;
else if (grade === 'C') gColor = ORANGE;
else if (grade === 'D') gColor = RED;
else if (grade === 'F') gColor = DEEPRED;

const trendPts = [];
for (let k = 3; k >= 1; k--) {
    const past = scoreAt(k);
    if (past.grade !== 'N/A') trendPts.push(past);
}
if (grade !== 'N/A') trendPts.push(cur);
let trendDir = 'steady';
let trendColor = '#8b97a4';
const haveTrend = trendPts.length >= 2 && grade !== 'N/A';
if (haveTrend) {
    const change = cur.pct - trendPts[0].pct;
    if (change >= 0.1) { trendDir = 'improving'; trendColor = GREEN; }
    else if (change <= -0.1) { trendDir = 'deteriorating'; trendColor = RED; }
}
const trendTxt = trendPts.map(p => p.grade).join(' u2192 ');

let verdict;
if (!isDaily) {
    verdict = 'Daily chart only';
} else if (!isRateable) {
    verdict = 'No reported earnings - index or fund';
} else if (graded < 3) {
    verdict = 'Not enough earnings history to grade';
} else if (grade === 'A') {
    verdict = 'Consistent beats with revenue-backed growth';
} else if (grade === 'B') {
    verdict = 'Solid execution, one soft component';
} else if (grade === 'C') {
    verdict = 'Mixed - check which component is weak';
} else if (grade === 'D') {
    verdict = 'Weak earnings behavior, treat with caution';
} else {
    verdict = 'Poor earnings quality';
}

let pFrom = n - profBars;
if (pFrom < 0) pFrom = 0;

let pHi = -1e12;
let pLo = 1e12;
for (let i = pFrom; i < n; i++) {
    if (high[i] > pHi) pHi = high[i];
    if (low[i] < pLo) pLo = low[i];
}

const binVol = Array(profBins).fill(0);
const binStep = (pHi - pLo) / profBins;
let haveVolume = false;

if (isDaily && binStep > 0) {
    for (let i = pFrom; i < n; i++) {
        const v = volume[i];
        if (v === null || v === undefined || v <= 0) continue;
        haveVolume = true;
        let bFrom = Math.floor((low[i] - pLo) / binStep);
        let bTo = Math.floor((high[i] - pLo) / binStep);
        if (bFrom < 0) bFrom = 0;
        if (bTo > profBins - 1) bTo = profBins - 1;
        const spread = bTo - bFrom + 1;
        const share = v / spread;
        for (let b = bFrom; b <= bTo; b++) binVol[b] = binVol[b] + share;
    }
}

let pocVol = 0;
let pocBin = -1;
for (let b = 0; b < profBins; b++) {
    if (binVol[b] > pocVol) { pocVol = binVol[b]; pocBin = b; }
}
const pocPrice = pocBin >= 0 ? pLo + (pocBin + 0.5) * binStep : null;

const hvnLo = [];
const hvnHi = [];
if (pocVol > 0) {
    let runStart = -1;
    for (let b = 0; b < profBins; b++) {
        const qualifies = binVol[b] >= pocVol * (hvnPct / 100);
        if (qualifies && runStart < 0) runStart = b;
        if ((!qualifies || b === profBins - 1) && runStart >= 0) {
            const runEnd = qualifies ? b : b - 1;
            hvnLo.push(pLo + runStart * binStep);
            hvnHi.push(pLo + (runEnd + 1) * binStep);
            runStart = -1;
        }
    }
}

const minWidth = atrNow > 0 ? atrNow * minZoneAtr : 0;

const fvgLo = [];
const fvgHi = [];
let fFrom = n - fvgLook;
if (fFrom < 2) fFrom = 2;

if (isDaily) {
    for (let i = fFrom; i < n; i++) {
        if (low[i] <= high[i - 2]) continue;
        const gLo = high[i - 2];
        const gHi = low[i];
        if (gHi >= spot) continue;
        let filled = false;
        for (let j = i + 1; j < n; j++) {
            if (low[j] <= gLo) { filled = true; break; }
        }
        if (filled) continue;

        if (gHi - gLo < minWidth) continue;
        fvgLo.push(gLo);
        fvgHi.push(gHi);
    }
}

let zoneLo = null;
let zoneHi = null;
let zoneKind = '';
let shelfFloor = null;
let bestVol = -1;

for (let f = 0; f < fvgLo.length; f++) {
    for (let h = 0; h < hvnLo.length; h++) {

        const oLo = fvgLo[f] > hvnLo[h] ? fvgLo[f] : hvnLo[h];
        const oHi = fvgHi[f] < hvnHi[h] ? fvgHi[f] : hvnHi[h];
        if (oHi <= oLo) continue;

        let shelfVol = 0;
        for (let b = 0; b < profBins; b++) {
            const bLo = pLo + b * binStep;
            const bHi = pLo + (b + 1) * binStep;
            if (bHi <= hvnLo[h]) continue;
            if (bLo >= hvnHi[h]) continue;
            shelfVol = shelfVol + binVol[b];
        }

        let better = shelfVol > bestVol;
        if (pickNearest === 1) better = zoneHi === null || fvgHi[f] > zoneHi;

        if (better) {
            bestVol = shelfVol;
            zoneLo = fvgLo[f];
            zoneHi = fvgHi[f];
            shelfFloor = hvnLo[h];
            zoneKind = 'gap on volume shelf';
        }
    }
}

if (zoneHi === null) {
    for (let f = 0; f < fvgLo.length; f++) {
        if (zoneHi === null || fvgHi[f] > zoneHi) {
            zoneLo = fvgLo[f];
            zoneHi = fvgHi[f];
            zoneKind = 'unfilled gap only';
        }
    }
}
if (zoneHi === null) {
    for (let h = 0; h < hvnLo.length; h++) {
        if (hvnHi[h] >= spot) continue;
        if (hvnHi[h] - hvnLo[h] < minWidth) continue;
        if (zoneHi === null || hvnHi[h] > zoneHi) {
            zoneLo = hvnLo[h];
            zoneHi = hvnHi[h];
            zoneKind = 'volume shelf only';
        }
    }
}

const haveZone = zoneHi !== null;
const inZone = haveZone && spot <= zoneHi && spot >= zoneLo;
const zoneDist = haveZone ? ((spot - zoneHi) / spot) * 100 : 0;

let stopPx = null;
let riskPct = 0;
if (haveZone) {
    const floorPx = shelfFloor !== null && shelfFloor < zoneLo ? shelfFloor : zoneLo;
    stopPx = floorPx - stopAtr * atrNow;
    if (zoneHi > 0) riskPct = ((zoneHi - stopPx) / zoneHi) * 100;
}

const goodGrade = grade === 'A' || grade === 'B';
const okGrade = grade === 'C';
const showZone = isDaily && isRateable && haveZone && (goodGrade || okGrade);
const suppressed = isDaily && isRateable && haveZone && !goodGrade && !okGrade;
const drawZone = showZone || suppressed;
const zoneColor = suppressed ? GREY : (goodGrade ? GREEN : AMBER);

let action;
if (!isDaily) {
    action = 'Switch to the 1D timeframe';
} else if (!isRateable) {
    action = 'Nothing to grade - no levels shown';
} else if (graded < 3) {
    action = 'Too few quarters to grade - no levels shown';
} else if (!goodGrade && !okGrade) {
    action = 'Grade ' + grade + ' - not a dip worth buying';
} else if (!haveZone) {
    action = 'Grade ' + grade + ' - no level wide enough to trade';
} else if (inZone) {
    action = 'IN ZONE ' + zoneLo.toFixed(2) + ' - ' + zoneHi.toFixed(2);
} else if (goodGrade) {
    action = 'BUY ZONE ' + zoneLo.toFixed(2) + ' - ' + zoneHi.toFixed(2);
} else {
    action = 'WATCH ZONE ' + zoneLo.toFixed(2) + ' - ' + zoneHi.toFixed(2);
}

let drawFrom = 0;
if (lineBars > 0) {
    drawFrom = n - lineBars;
    if (drawFrom < 0) drawFrom = 0;
}
function levelSeries(px) {
    const arr = Array(n).fill(null);
    if (px === null || px === undefined) return arr;
    for (let i = drawFrom; i < n; i++) arr[i] = px;
    return arr;
}

for (let h = 0; h < 6; h++) {
    let hi = Array(n).fill(null);
    let lo = Array(n).fill(null);
    if (isDaily && h < hvnLo.length) {
        hi = levelSeries(hvnHi[h]);
        lo = levelSeries(hvnLo[h]);
    }
    const hId = paint(hi, { name: 'Shelf' + (h + 1) + 'H', color: BLUE, style: 'dotted', thickness: 1, hidden: true });
    const lId = paint(lo, { name: 'Shelf' + (h + 1) + 'L', color: BLUE, style: 'dotted', thickness: 1, hidden: true });
    fill(hId, lId, BLUE, 0.07);
}

const pocArr = isDaily ? levelSeries(pocPrice) : Array(n).fill(null);
const pocId = paint(pocArr, { name: 'POC', color: BLUE, style: 'dotted', thickness: 1 });

const zHi = drawZone ? levelSeries(zoneHi) : Array(n).fill(null);
const zLo = drawZone ? levelSeries(zoneLo) : Array(n).fill(null);

const zHiId = paint(zHi, { name: 'Zone High', color: zoneColor, style: 'line', thickness: 1 });
const zLoId = paint(zLo, { name: 'Zone Low', color: zoneColor, style: 'line', thickness: 1 });
fill(zHiId, zLoId, zoneColor, suppressed ? 0.14 : 0.30);


const barDayAll = Array(n).fill(0);
for (let i = 0; i < n; i++) barDayAll[i] = Math.floor(toMs(time[i]) / 86400000);

let peFloor = null;
let peCeil = null;
let peNow = null;
let ttmNow = null;

if (isDaily && isRateable) {
    const asc = [];
    for (let i = recs.length - 1; i >= 0; i--) asc.push(recs[i]);

    const timeInMs = time[n - 1] > 1e11;
    const ttmTs = [];
    const ttmVals = [];
    const rolling = [];
    for (let k = 0; k < asc.length; k++) {
        rolling.push(asc[k].eps);
        if (rolling.length > 4) rolling.shift();
        if (rolling.length < 4) continue;
        let four = 0;
        for (let j = 0; j < 4; j++) four = four + rolling[j];
        const ms = toMs(asc[k].timestamp);
        ttmTs.push(timeInMs ? ms : Math.floor(ms / 1000));
        ttmVals.push(four);
    }

    let ttm = null;
    const haveHelpers = typeof land_points_onto_series === 'function' && typeof interpolate_sparse_series === 'function';
    if (haveHelpers && ttmTs.length > 0) {
        ttm = interpolate_sparse_series(land_points_onto_series(ttmTs, ttmVals, time, 'ge'), 'constant');
    }
    if (!ttm || ttm.length !== n) {
        ttm = Array(n).fill(null);
        for (let i = 0; i < n; i++) {
            let used = 0;
            let total = 0;
            for (let q = asc.length - 1; q >= 0; q--) {
                const r = asc[q];
                if (r.eps === null || r.eps === undefined) continue;
                const rDay = Math.floor(toMs(r.timestamp) / 86400000);
                if (rDay > barDayAll[i]) continue;
                total = total + r.eps;
                used = used + 1;
                if (used === 4) break;
            }
            if (used === 4) ttm[i] = total;
        }
    }

    let pFrom2 = n - peLook;
    if (pFrom2 < 0) pFrom2 = 0;
    const peHist = [];
    for (let i = pFrom2; i < n; i++) {
        if (ttm[i] === null) continue;
        if (ttm[i] <= 0) continue;
        if (close[i] <= 0) continue;
        peHist.push(close[i] / ttm[i]);
    }

    ttmNow = ttm[n - 1];
    if (ttmNow !== null && ttmNow > 0) peNow = spot / ttmNow;

    if (peHist.length >= 60 && ttmNow !== null && ttmNow > 0) {
        peHist.sort((a, b) => a - b);
        function pctl(p) {
            let idx = Math.floor((p / 100) * (peHist.length - 1));
            if (idx < 0) idx = 0;
            if (idx > peHist.length - 1) idx = peHist.length - 1;
            return peHist[idx];
        }
        peFloor = pctl(peLoPct) * ttmNow;
        peCeil = pctl(peHiPct) * ttmNow;
    }
}

const peFloorId = paint(isDaily ? levelSeries(peFloor) : Array(n).fill(null), {
    name: 'PE Floor', color: BLUE, style: 'line', thickness: 1, invisibleByDefault: true
});
const peCeilId = paint(isDaily ? levelSeries(peCeil) : Array(n).fill(null), {
    name: 'PE Ceiling', color: ORANGE, style: 'dotted', thickness: 1, invisibleByDefault: true
});

const stopArr = showZone ? levelSeries(stopPx) : Array(n).fill(null);
const stopId = paint(stopArr, { name: 'Zone Stop', color: RED, style: 'dotted', thickness: 2 });



function reactionBar(rec) {
    const eDay = Math.floor(toMs(rec.timestamp) / 86400000);
    if (eDay < barDayAll[0]) return -1;
    let hit = -1;
    for (let b = 0; b < n; b++) {
        if (barDayAll[b] >= eDay) { hit = b; break; }
    }
    if (hit < 0) return -1;

    const w = rec.when === undefined || rec.when === null ? '' : String(rec.when).toLowerCase();
    const isAfter = w.indexOf('after') > -1 || w.indexOf('post') > -1 || w.indexOf('amc') > -1;
    const isBefore = w.indexOf('before') > -1 || w.indexOf('pre') > -1 || w.indexOf('bmo') > -1;

    if (isAfter && barDayAll[hit] === eDay && hit + 1 < n) return hit + 1;
    if (isBefore) return hit;

    if (barDayAll[hit] === eDay && hit + 1 < n && hit > 0) {
        const gapSame = Math.abs(open[hit] - close[hit - 1]) / close[hit - 1] * 100;
        const gapNext = Math.abs(open[hit + 1] - close[hit]) / close[hit] * 100;
        if (gapNext > gapSame) return hit + 1;
    }
    return hit;
}

const marks = Array(n).fill(null);
if (isDaily) {
    let placed = 0;
    for (let i = 0; i < recs.length; i++) {
        if (placed >= lookQ) break;
        const r = recs[i];
        if (r.eps_est === null || r.eps_est === undefined) continue;
        const hit = reactionBar(r);
        if (hit < 0) continue;
        const denom = Math.abs(r.eps_est);
        let sp = 0;
        if (denom > 0.01) sp = ((r.eps - r.eps_est) / denom) * 100;
        marks[hit] = (sp >= 0 ? '+' : '') + sp.toFixed(0) + '%';
        placed = placed + 1;
    }
}

const beatMarks = Array(n).fill(null);
const missMarks = Array(n).fill(null);
if (isDaily) {
    let placed2 = 0;
    for (let i = 0; i < recs.length; i++) {
        if (placed2 >= lookQ) break;
        const r = recs[i];
        if (r.eps_est === null || r.eps_est === undefined) continue;
        const hit2 = reactionBar(r);
        if (hit2 < 0) continue;
        const den = Math.abs(r.eps_est);
        let sp2 = 0;
        if (den > 0.01) sp2 = ((r.eps - r.eps_est) / den) * 100;
        const big = Math.abs(sp2) >= bigBeatPct;
        let radius2 = 4 + Math.abs(sp2) / 4;
        if (radius2 > 10) radius2 = 10;
        const mk2 = {
            enabled: true,
            radius: radius2,
            symbol: 'diamond',
            fillColor: big ? PURPLE : 'rgba(0,0,0,0)',
            lineColor: PURPLE,
            lineWidth: 2
        };
        const pad2 = (high[hit2] - low[hit2]) * 0.8 + (high[hit2] * 0.002);
        if (sp2 >= 0) beatMarks[hit2] = { y: low[hit2] - pad2, marker: mk2 };
        else missMarks[hit2] = { y: high[hit2] + pad2, marker: mk2 };
        placed2 = placed2 + 1;
    }
}
paint(beatMarks, { name: 'EPS Beat', color: 'transparent' });
paint(missMarks, { name: 'EPS Miss', color: 'transparent' });
paint(marks, { style: 'labels_below', color: PURPLE, name: 'EPS Surprise', invisibleByDefault: true });

const PROJ_BARS = 30;
function aheadOf(v) {
    const arr = [];
    for (let i = 0; i < PROJ_BARS; i++) arr.push(v);
    return arr;
}
const zHiP = paint_projection(aheadOf(drawZone ? zoneHi : null), { name: 'Zone High Ahead', color: zoneColor, style: 'line', thickness: 1, ignoreWhenScaling: true, hideInLegend: true });
const zLoP = paint_projection(aheadOf(drawZone ? zoneLo : null), { name: 'Zone Low Ahead', color: zoneColor, style: 'line', thickness: 1, ignoreWhenScaling: true, hideInLegend: true });
const stopP = paint_projection(aheadOf(showZone ? stopPx : null), { name: 'Stop Ahead', color: RED, style: 'dotted', thickness: 2, ignoreWhenScaling: true, hideInLegend: true });
const pocP = paint_projection(aheadOf(isDaily ? pocPrice : null), { name: 'POC Ahead', color: BLUE, style: 'dotted', thickness: 1, ignoreWhenScaling: true, hideInLegend: true });

const tagSep = pHi > pLo ? (pHi - pLo) * 0.045 : spot * 0.02;
const tagItems = [];
if (drawZone) {
    const zoneWord = suppressed ? 'SUPPRESSED ' : (goodGrade ? 'BUY ' : 'WATCH ');
    tagItems.push({ px: zoneHi, ref: zHiP, text: zoneWord + zoneLo.toFixed(2) + ' - ' + zoneHi.toFixed(2),
        bg: suppressed ? 'rgba(153,153,153,0.9)' : (goodGrade ? 'rgba(0,200,83,0.9)' : 'rgba(255,160,0,0.9)') });
}
if (showZone && stopPx !== null) {
    tagItems.push({ px: stopPx, ref: stopP, text: 'Stop ' + stopPx.toFixed(2), bg: 'rgba(229,57,53,0.85)' });
}
if (isDaily && pocPrice !== null) {
    tagItems.push({ px: pocPrice, ref: pocP, text: 'POC ' + pocPrice.toFixed(2), bg: 'rgba(30,136,229,0.8)' });
}
tagItems.sort((x, y) => y.px - x.px);
const tagGroups = [];
for (let i = 0; i < tagItems.length; i++) {
    const it = tagItems[i];
    const last = tagGroups.length > 0 ? tagGroups[tagGroups.length - 1] : null;
    if (last !== null && Math.abs(last.anchorPx - it.px) < tagSep) last.text = last.text + '  u00b7  ' + it.text;
    else tagGroups.push({ anchorPx: it.px, ref: it.ref, text: it.text, bg: it.bg });
}
for (let g = 0; g < tagGroups.length; g++) {
    paint_label_at_line(tagGroups[g].ref, 10, tagGroups[g].text, {
        color: '#ffffff', background_color: tagGroups[g].bg, border_radius: 4, border_width: 0
    });
}

function headerCell(text, bg) {
    return {
        text: text,
        colspan: 2,
        color: '#ffffff',
        background: bg,
        padding: '6px 8px',
        fontSize: '11px',
        fontWeight: 'bold',
        textAlign: 'center',
        whiteSpace: 'nowrap'
    };
}
function keyCell(text) {
    return {
        text: text,
        color: '#8b97a4',
        background: 'rgba(128,128,128,0.10)',
        border: 'solid var(--border-color) 1px',
        padding: '4px 8px',
        fontSize: '10px',
        fontWeight: 'bold',
        textAlign: 'left',
        whiteSpace: 'nowrap'
    };
}
function valCell(text, color) {
    return {
        text: text,
        color: color || 'var(--text-color)',
        background: 'var(--background-color)',
        border: 'solid var(--border-color) 1px',
        padding: '4px 8px',
        fontSize: '10px',
        fontWeight: 'bold',
        textAlign: 'right',
        whiteSpace: 'nowrap'
    };
}

const rows = [];
const fullRead = isDaily && isRateable && graded >= 3;

rows.push({ cells: [headerCell('WINVEST FUNDAMENTAL RATING', 'linear-gradient(90deg,#37474f,#546e7a)')] });
rows.push({ cells: [headerCell('GRADE  ' + grade, gColor)] });
if (haveTrend) {
    rows.push({ cells: [keyCell('TREND'), valCell(trendDir + '  u00b7  ' + trendTxt, trendColor)] });
}
const sectorName = (typeof current !== 'undefined' && current !== null && typeof current.sector === 'string') ? current.sector : '';
if (isDaily && sectorName !== '') {
    rows.push({ cells: [keyCell('SECTOR'), valCell(sectorName.length > 26 ? sectorName.slice(0, 25) + '.' : sectorName, '#8b97a4')] });
}

if (!fullRead) {
    rows.push({ cells: [keyCell('STATUS'), valCell(verdict, '#8b97a4')] });
}

rows.push({ cells: [
    keyCell(showZone ? (goodGrade ? 'BUY ZONE' : 'WATCH ZONE') : (suppressed ? 'SUPPRESSED' : 'LEVEL')),
    valCell(drawZone ? zoneLo.toFixed(2) + ' - ' + zoneHi.toFixed(2) : action, drawZone ? zoneColor : '#8b97a4')
] });

if (showZone) {
    rows.push({ cells: [keyCell('STOP'), valCell(stopPx.toFixed(2) + '  u00b7  ' + riskPct.toFixed(1) + '% risk', RED)] });
    rows.push({ cells: [keyCell('SOURCE'), valCell(zoneKind + (haveVolume ? '' : ' (no vol)'), '#8b97a4')] });
}

if (fullRead) {
    if (peFloor !== null) {
        rows.push({ cells: [headerCell('VALUATION', 'linear-gradient(90deg,#0d47a1,#1976d2)')] });
        rows.push({ cells: [keyCell('PE NOW'), valCell(peNow.toFixed(1) + 'x', 'var(--text-color)')] });
        rows.push({ cells: [keyCell('PE FLOOR'), valCell(peFloor.toFixed(2), spot <= peFloor ? GREEN : 'var(--text-color)')] });
        rows.push({ cells: [keyCell('PE CEILING'), valCell(peCeil.toFixed(2), spot >= peCeil ? RED : 'var(--text-color)')] });
    }
    rows.push({ cells: [headerCell('EARNINGS QUALITY', 'linear-gradient(90deg,#4a148c,#7b1fa2)')] });
    rows.push({ cells: [keyCell('BEAT RATE'), valCell(beatRate.toFixed(0) + '% of ' + graded, sBeat >= 2 ? GREEN : 'var(--text-color)')] });
    rows.push({ cells: [keyCell('AVG SURPRISE'), valCell((avgSurp >= 0 ? '+' : '') + avgSurp.toFixed(1) + '%', sSize >= 2 ? GREEN : 'var(--text-color)')] });
    if (haveGrowth) {
        rows.push({ cells: [keyCell('EPS YoY'), valCell((epsGrowth >= 0 ? '+' : '') + epsGrowth.toFixed(0) + '%', sGrow >= 2 ? GREEN : 'var(--text-color)')] });
        rows.push({ cells: [keyCell('REVENUE YoY'), valCell((revGrowth >= 0 ? '+' : '') + revGrowth.toFixed(0) + '%', 'var(--text-color)')] });
        rows.push({ cells: [keyCell('GROWTH QUALITY'), valCell(qualTxt, sQual >= 2 ? GREEN : (sQual === 1 ? AMBER : RED))] });
    } else {
        rows.push({ cells: [keyCell('GROWTH'), valCell('no year-ago quarter', GREY)] });
    }
    if (haveCash) {
        const convPct = Math.round(cashConv * 100);
        let convColor = 'var(--text-color)';
        if (sCash === 3) convColor = GREEN;
        else if (sCash === 1) convColor = AMBER;
        else if (sCash === 0) convColor = RED;
        rows.push({ cells: [keyCell('CASH CONVERSION'), valCell(convPct + '% of profit', convColor)] });
    }
}


const sigGood = series_of(false);
const sigInZone = series_of(false);
const sigNearZone = series_of(false);
const sigSuppressed = series_of(false);
const sigCheap = series_of(false);
const sigImproving = series_of(false);
const sigCashStrong = series_of(false);
const sigDear = series_of(false);

sigGood[lastBar] = isDaily && isRateable && goodGrade;
sigInZone[lastBar] = showZone && inZone;
sigNearZone[lastBar] = showZone && !inZone && zoneDist <= 3;
sigSuppressed[lastBar] = suppressed;
sigCheap[lastBar] = peFloor !== null && spot <= peFloor;
sigImproving[lastBar] = haveTrend && trendDir === 'improving';
sigCashStrong[lastBar] = isDaily && isRateable && haveCash && sCash === 3;
sigDear[lastBar] = peCeil !== null && spot >= peCeil;

register_signal(sigGood, 'Grade A or B');
register_signal(sigInZone, 'Price in buy zone');
register_signal(sigNearZone, 'Near buy zone');
register_signal(sigSuppressed, 'Level suppressed');
register_signal(sigCheap, 'At PE floor');
register_signal(sigImproving, 'Grade improving');
register_signal(sigCashStrong, 'Strong cash conversion');
register_signal(sigDear, 'At PE ceiling');

paint_overlay('Winvest Rating', {
    position: 'top_right',
    parent: 'chart',
    order: 'above_all',
    offset_x: -60,
    offset_y: 30
}, {
    width: '250px',
    borderCollapse: 'collapse',
    background: 'var(--background-color)',
    border: 'solid var(--border-color) 1px',
    borderRadius: '8px',
    boxShadow: '0 3px 12px rgba(0,0,0,0.18)',
    overflow: 'hidden',
    rows: rows
});