engineeringfrontend 8 min read

Scoring IDX Stocks by Value and Momentum

July 7, 2026 · 11:48 UTC

Scoring IDX Stocks by Value and Momentum

Ranking hundreds of Indonesian stocks against each other sounds simple until the raw numbers plainly refuse to cooperate, because a price-to-earnings ratio sits near eight or twenty-five, a return on equity speaks in single-digit percentages, and a half-year return swings on its own scale, so a plain sum lets the biggest raw figure quietly bully every other signal.

The screener behind this article exists to solve exactly that unfairness, turning messy raw fundamentals into one single composite score that lets value, quality, and momentum share equal footing before any single stock climbs onto the leaderboard.

A ranking stays honest only when every single number is first forced onto the same shared scale before anything is truly compared.

Note

The example implementation behind this article is open source on GitHub at NeaByteLab/IDX-UI, where the full screener interface, scoring pipeline, and filter panels live as real code to read, run, and adapt freely.

Filter Before Anything Ranks

The first real decision here was the ordering, and it matters far more than it first looks, because filtering deliberately runs before scoring rather than after, so only the stocks that clear the fundamental gates ever reach the ranking stage at all, which keeps the whole leaderboard built purely from names that genuinely earned a real place in the running today.

Valuation bounds on price-to-earnings, a floor under return on equity, a ceiling over debt-to-equity, and a minimum momentum across the chosen horizon all trim the raw universe down to candidates, and only those survivors get normalized and scored.

Liquidity limits and risk exclusions land later, once the scores exist, so thin names simply drop out without distorting the math.

Numbers That Refuse to Compare

The core problem here is that every single indicator carries its own unit and its own natural range, so a raw sum quietly hands nearly all of the deciding power over to whichever metric happens to work in the biggest numbers, and the eventual winner ends up chosen by sheer scale rather than by any genuine and measurable merit spread evenly across the three pillars.

A price-to-earnings ratio of twenty-five is not automatically more important than a return on equity of fifteen percent, yet a naive average treats that larger figure as if it carried far more weight, and the ranking drifts toward valuation noise.

Comparing stocks fairly means stripping the units away entirely, so the question is not how big a number is but where it sits.

One Scale From Zero to One

The fix for all of this is min-max normalization, which quietly rescales every single indicator into the very same zero-to-one band based purely on where a given stock happens to land between the lowest and highest observed value seen across every remaining candidate, so the raw units simply fall away and only the relative position on each measure ends up mattering at all.

The formula stays plain, taking a value, subtracting the minimum, and dividing by the range between maximum and minimum, so the weakest candidate on a measure lands at zero and the strongest lands squarely at one, and shared values collapse it flat.

// Rescale one indicator into the shared 0 to 1 band
function normalize(value, min, max) {
  // Same value across all candidates means no spread to measure
  if (max - min === 0) {
    return 0.5
  }
  return (value - min) / (max - min)
}

Once every indicator lives inside the same band, the numbers finally compare like for like, and no metric can dominate on scale.

A pipeline turning raw fundamentals through a filter gate and normalization into one ranked list

Flipping the Metrics That Reward Less

Normalization alone is not quite enough here, because a handful of indicators quietly reward smaller values, so a raw zero-to-one scale would score them exactly backward against the plain logic of sound investing, handing the very highest marks to the priciest and most heavily indebted names instead of the cheaper and safer ones a fair ranking should truly favor.

A low price-to-earnings ratio signals a cheaper stock, a low price-to-book value points the same way, and a low debt-to-equity ratio means healthier leverage, so the three get inverted with a one-minus-score step that keeps cheap aligned with strong.

// Cheaper and safer should score higher, so invert
function scoreLowerIsBetter(value, min, max) {
  return 1 - normalize(value, min, max)
}

Return on equity, return on assets, and both momentum returns stay untouched, since higher already means better and needs no flip.

Three Pillars Into One Number

With every indicator now scored and aligned in the same direction, the pillars come together as plain averages, so value blends price-to-earnings and price-to-book, quality blends return on equity, return on assets, and the debt ratio, and momentum blends the half-year and full-year returns, with each pillar averaging only the indicators that truly exist for a stock.

The final composite leans hardest on value at forty percent, then splits the rest evenly between quality and momentum at thirty percent each, because buying cheap has the very deepest research behind it and the other two guard against a value trap.

// Weighted blend of the three pillars into one score
function composite(value, quality, momentum) {
  return 0.4 * value + 0.3 * quality + 0.3 * momentum
}

Every score stays inside the zero-to-one band, from single indicators through pillars to the composite, so the ranking stays fair.

Three weighted pillars of forty thirty thirty combining into one composite score dial

Reading the Rank in Context

A single global ranking only ever tells part of the whole story here, because one unusually strong sector can quietly crowd the very top of the list and bury genuinely good names that simply happen to compete inside a far tougher neighborhood, so a fairer reading really needs a little more honest context wrapped around that one raw number before anyone acts on it.

Ranking each stock inside its own sector answers a fairer question about who leads banking against banking or consumer against consumer, while a percentile then places it across the whole field, so ninety marks a top-tier name and fifty the median.

Global rank orders the entire list, sector rank narrows the view to one industry, and percentile reads relative strength fast.

Fair Math Is the Whole Point

Building this whole screener kept circling back to one stubborn idea, that a ranking only ever earns real trust when the quiet process behind it treats every single stock in exactly the same fair way, and normalization is precisely the step that finally makes that fairness promise real and measurable rather than mere decorative language stapled onto a sorted table.

Filtering first keeps the field honest, normalizing to a shared band keeps the comparison fair, inverting the right metrics keeps the logic sound, and weighting the pillars keeps the priorities clear, yet none of it replaces research into a business.

The score is only a starting point, never the whole decision, so treating it as an oracle quietly betrays the fairness behind it.

Share
On this page
No results found.