Sort by what matters to you
Your list is currently in the order you typed it. That is a real ordering and often the right one. Changing it is a decision with a trap underneath.
Ordering is an opinion
A watchlist sorted by "biggest mover" is not neutral. It puts whatever is loud today at the top and quietly suggests that is what deserves your attention. Sometimes that is exactly what you want. Sometimes the instrument that has not moved is the story, and a mover-sorted list buries it.
So decide deliberately, and know what each choice is telling you:
| Sort | What it surfaces | What it hides |
|---|---|---|
| As written | your own priority | today's outliers |
| Change % | what moved most | anything stable |
| Absolute change | big-price names | small-price names that moved more |
| Volume | where the activity is | thin names doing something unusual |
Note the third row. Sorting by change rather than change_p mostly sorts by share price: a 1% move on a 500-dollar stock beats a 5% move on a 20-dollar one. That is almost never what a person means by "biggest mover", and it is the sort of thing that looks fine until you notice the same three names are always on top.
Implement it as a pure function
type SortKey = "manual" | "change_p" | "volume";
function sortRows(rows: Quote[], key: SortKey, order: string[]): Quote[] {
if (key === "manual") {
return order
.map((code) => rows.find((r) => r.code === code))
.filter((r): r is Quote => Boolean(r));
}
return [...rows].sort((a, b) => b[key] - a[key]);
}
Two details that matter more than they look.
[...rows] before .sort(). Array.prototype.sort mutates in place. Sorting the array you were handed changes it for everything else holding a reference, and produces bugs that only appear when a second component renders the same data. Copy first, always.
Manual order is rebuilt from your list, not from the response. The API does not promise to return rows in the order you asked. If you want your ordering, reconstruct it from WATCHLIST.
The tie-break trap
Ask your assistant for a sort and you will usually get exactly the comparator above. It is fine, until two rows tie: two instruments both up 0.32%, or a market where nothing has moved yet and change_p is 0 for everything.
sort will not shuffle them. It has been stable since ES2019, so tied rows keep exactly the order they arrived in. That is the problem: the order they arrived in is the API's, and the API does not promise one. A fresh fetch can hand you the same two rows the other way round, and your faithfully-stable sort will faithfully preserve that. A list that reorders itself while you look at it is worse than an unsorted one, because you stop trusting the whole screen.
Break the tie explicitly:
return [...rows].sort((a, b) => b.change_p - a.change_p || a.code.localeCompare(b.code));
That || is the entire fix, and it is the kind of thing you only put in if you have thought about ties — two names up exactly 0.32%, or a row of thinly traded names that all last printed at the same price.
Persist the choice, or do not offer it
A sort control that resets on every reload is an irritation. Keep it in localStorage alongside the list, or leave the feature out. Half-built preferences are worse than no preferences: they teach the user their input does not stick.
Try it now
Add sorting with at least two keys plus your manual order, and then test the case nobody tests: force every change_p to zero in the data and reload five times. If the row order changes between reloads, your comparator has no tie-break. Add one.