Skip to content

Learn

Stock Factor Research

Make tied stock factor scores produce deterministic portfolios

Tied stock factor scores need an explicit portfolio rule. A stable sort can preserve the order of equal records while still letting an arbitrary provider column order determine the last selected stock. Freeze group capacity, a tie key and any incumbent-retention policy, then scramble the input to prove that the same research state produces the same holdings and costs.

This guide uses five fictional US stocks, discrete scores and synthetic USD holdings. CoTrading ranking checks remain planned or unverified. The downloadable example does not assume that an internal grouping function already implements these policies; it specifies and tests an independent deterministic construction.

Decide what equality means before sorting

Define the score field and its precision. If the source provides only grades, equal grades are genuinely indistinguishable on that measurement. Do not invent decimal differences to force a preferred ordering. If unrounded observations exist, decide whether those values are permitted inputs before evaluating results; switching precision after seeing the selected stock changes the research rule.

For floating-point scores, an approximate-equality rule requires care. Pairwise closeness may not create a consistent grouping relation. A declared rounding or bucket definition is easier to reconstruct than comparing each new value with whichever stock happens to precede it. This fixture uses exact integer grades, so equality has no tolerance parameter.

Grades one through five do not establish equal increments of expected return. The portfolio uses their order only. A score of four is not assumed to forecast twice the return of two, and an average grade is not used as an allocation weight. Keep that interpretation separate from the implementation of deterministic order.

Fix capacity and the boundary policy

The selected group has two slots, with the other three names outside it. Sort descending by score. The identifier-only policy sorts ascending by stable identifier within each tie. The incumbent-retention policy instead prefers an already held stock inside an exact tie, then uses the same identifier key for any remaining ambiguity.

Incumbent preference is subordinate to score. It cannot keep a lower-scoring stock ahead of an eligible higher-scoring replacement. It also cannot retain a name absent from the eligible input. Those constraints distinguish a tie rule from a turnover-buffer strategy, which might deliberately tolerate a wider rank band and needs separate parameters.

The previous holding set is therefore an explicit input to the retention policy. Save it with the score snapshot. Two runs with different prior holdings can legitimately choose different tied names; that is not nondeterminism if the state difference is declared. Conversely, a hidden mutable holding set can make apparently identical inputs produce unexplained outcomes.

See why stable sorting alone is insufficient

Python’s sorting documentation states that equal-key records retain their input order. This property is useful, but it does not create an economically justified tie key. If a provider changes column order, a score-only sort can choose another member without any score change.

The fixture scores A at five, B/C/D at four and E at one. In the original input order, a score-only top-two sort chooses A and B. Reverse the incoming order and it chooses A and D. Both sorted outputs satisfy descending scores; the difference comes entirely from unspecified order inside the grade-four tie.

Do not fix this by sorting symbol display names if those names can be reused or change historically. A production reference needs a stable security identity with a documented ordering representation. The teaching identifiers A through E are fixed and unique, so they are sufficient for this fixture’s deterministic comparison.

Reconstruct the two approved selections

The account initially holds A and C. Under identifier-only ordering, A takes the first slot and B wins the tied second slot, so targets are A/B. Under incumbent retention, C wins that same boundary tie and targets are A/C. Both groups have exactly two members; retention does not create a larger group to avoid making a decision.

Run every permutation of the five input records. There are 120 arrangements. The identifier-only result must remain A/B and the retention result A/C on all of them. This is stronger than checking one random shuffle, although it still covers only this five-record score/state fixture rather than every possible production input.

Now reduce C’s grade to three while B and D remain four. Retention must select A/B because C is no longer tied at the boundary. Remove C from the eligible input entirely and the result must also be A/B. These are different failure mechanisms from input-order dependence and prevent an overly broad “always keep the old stock” implementation.

Follow the policies into actual costs

Both accounts begin with forty A shares, forty C shares and 200 cash, with reference and later mark prices ten. Starting equity is 1,000. The retention policy keeps both holdings, so there is no order and equity stays 1,000. The fixture does not add a daily fee simply because the ranking was evaluated again.

Identifier-only selection sells forty C at ten, paying a fictional fee of one, and buys forty B at ten, paying another. Net cash is 198 and shares remain worth 800, giving equity 998. The two-unit difference is exactly the replacement cost under flat prices; it is not an estimate of which tied stock has better future returns.

Keep the pair of actual orders alongside the selection. A changed ranking list can be harmless if quantities are unchanged, while one boundary replacement creates a sale and a purchase. Group returns that ignore this distinction can conceal tie-driven turnover even when every grade appears stable over time.

Run the deterministic acceptance pack

Download the complete example, save it as R566.py and run python R566.py with Python 3.12 or later. The standard-library source is docs/growth/research/round-5/examples/R566.py; all inputs are synthetic and no service is contacted.

Assertions check all 120 permutations for both approved policies, demonstrate the score-only reversal failure, reject inappropriate retention when C loses score or eligibility, and reconcile the flat-price accounts. The output preserves the naive and approved lists so the reason for each difference can be independently inspected.

Review repeated dates without hiding state

For a multi-date panel, archive score precision, universe version, previous holdings, selected capacities, tie policy and final target quantities at each decision. Reconstruct one boundary tie from those artifacts before comparing any performance. A state reset during a rerun can change retention behavior even when every score is identical.

Count tie-boundary opportunities separately from actual replacements and execution rejections. The policy can request a deterministic replacement that fails to trade; the actual holding state then matters for the next decision. Never update prior holdings from unfilled targets. The supplied fixture assumes complete fills and does not claim to validate partial execution handling.

If a library changes its sorting or grouping implementation, rerun both the permutation fixture and one saved account-state transition. A new deterministic rule may still be different from the old approved rule. Version the change and explain which historical holdings would change. Completion means the same declared data and state reproduce the same groups and fee path, not that a tie policy has created additional predictive information.

Keep the incumbent snapshot immutable

Capture the previous actual holdings before evaluating any member of the current cross section. Updating the retained set while scanning candidates can make later tie decisions depend on iteration order again, defeating the permutation guarantee. The approved sorting key reads one frozen snapshot for the whole decision.

Store the snapshot identifier, score snapshot and policy version together. If two implementations disagree, first compare those inputs before changing the tie key. An old target that never filled is not an incumbent holding, and a name removed from eligibility cannot claim retention merely because it appears in yesterday’s account. These distinctions explain why the losing-eligibility and losing-score tests accompany the shuffle tests. The goal is deterministic use of a fully specified research state, not a preference for one symbol justified after its future return is known.

FAQ

Does a stable sort guarantee the same portfolio after a shuffle?

No. Equal-key records preserve their incoming order unless a separate deterministic key resolves the tie.

Can retention keep a lower-grade stock?

Not under this policy. It applies only within exact score ties among eligible names.