Skip to content

Learn

Stock Factor Research

Trace missing stock factor scores into portfolio decisions

A missing stock factor observation should enter a decision policy, not silently become a zero score or an indefinitely valid old value. Store the last usable score and its age, decide whether it remains eligible, and then trace any target change into actual sales, purchases and costs. The output is a daily state ledger, not merely a count of missing cells.

This guide compares immediate exclusion, bounded reuse and unlimited reuse on the same invented US-stock panel. Prices, observations and account values are synthetic. CoTrading coverage tools remain planned or unverified. The example does not reconstruct undisclosed fundamentals or claim that a data service can fill history that was never available.

Separate an observation from its current status

Save security identifier, score value, source timestamp, availability timestamp and the session when it first became usable. A missing incoming value says there is no new observation; it does not erase the provenance of the old observation. Keep both the incoming status and the retained value so a later reviewer can distinguish reuse from a fresh update.

Use explicit states such as fresh, reusable-old, expired and never-observed. Do not encode all of them as zero. A real numeric zero may have an economic meaning and enter a ranking; an unknown value lacks that information. Python’s math documentation describes finite-number checks that can help reject NaN or infinity before comparison. An absent value still needs a separate status rather than merely a numeric filter.

The executable policy treats None, NaN and positive or negative infinity as unavailable incoming observations. They never overwrite the last finite score or reset its age. A previously finite score can still be reused until its declared expiry. If A has never supplied a finite score, its age is null and its state is never_observed; it cannot enter the ranking. The ledger emits score_status separately from numeric age.

Define age in exchange sessions for this exercise. The current session index minus the last usable observation’s session is its age. A weekend or holiday should not consume several session-age units. If the actual policy instead uses elapsed hours, name that different clock and test it explicitly; do not mix the two conventions in one field.

Declare three policies before reading outcomes

The immediate-exclusion policy accepts only age zero. The bounded policy accepts age zero or one. Unlimited reuse is a deliberately weak counterexample: it accepts the last score regardless of age. All three rank eligible stocks descending and choose one name, with a stable identifier resolving ties. There is no post-result threshold search.

An expired held stock loses eligibility under the bounded policy. At the next supplied executable open, sell it and buy the new top eligible name if there is one. If no stock qualifies, remain in cash after the sale. A missing execution bar would require a separate blocked-exit state; the supplied fixture contains executable opens for every requested trade and does not claim to test that branch.

The SEC EDGAR API documentation distinguishes reporting facts from their submissions context. This supports preserving source and availability information when the score comes from disclosures. It does not prescribe a one-session expiry. That threshold is an author-defined research policy here and may be inappropriate for other factor families.

Walk through the four decision sessions

At session zero, A has score nine and B eight. At session one and two, A’s incoming value is missing while B supplies a fresh eight. At session three, A supplies eleven and B again supplies eight. Every observation in this fixture is assumed available before its corresponding supplied execution open; none uses that session’s later close.

With maximum age one, A is selected at session zero, retained at session one with age one, excluded at session two with age two and selected again at session three after its update resets age to zero. The target sequence is A, A, B, A. The absence on session one therefore creates no automatic trade, while the expiry on session two does.

Immediate exclusion gives A, B, B, A. Unlimited reuse gives A, A, A, A. These are three different decisions produced from exactly the same incoming panel. The later A score of eleven must not be backfilled into its missing sessions. Replaying with that backfill would remove the very information shortage the exercise is intended to investigate.

Rebuild the bounded-reuse account

Start with 1,000 cash. Each position is forty shares and each executed order pays a fictional fee of one. Session-zero opens are ten for both stocks. Buy A for 400 plus one, leaving 599 cash and equity 999 at the supplied same-open valuation. The one-unit initial reduction is a fee, not a factor return.

At session one, A’s open is 10.2 and B’s ten. Keep A without a new fee: equity is 599 plus 408, or 1,007. At session two, A opens eleven and B ten. Sell forty A for 440 less one, then buy forty B for 400 plus one. Cash becomes 637 and marked equity 1,037.

At session three, sell B at 10.5, receiving 419 after fee, then buy A at twelve for 481 including fee. Cash is 575 and forty A shares are worth 480, giving equity 1,055. Five orders have paid five fees. Final equity includes an open position; a liquidation result would need a separate exit and fee.

Use the other policies as mechanism checks

Immediate exclusion sells A one session earlier at 10.2 and enters B then. Its final cash is 543 and marked equity 1,023. It also executes five orders in this fixture, so the difference from bounded reuse is chiefly the replacement timing and price path, not an extra invented fee count.

Unlimited reuse pays only the initial entry fee and ends with cash 599 plus A value 480, or 1,079. Its higher synthetic value does not validate the policy. It held A at session two using a score that the approved bounded rule considered expired. Choosing unlimited reuse because this constructed outcome is higher would turn the eligibility rule into an after-the-fact performance choice.

Execute and review the state ledger

Download the complete example, save it as R564.py, and run python R564.py with Python 3.12 or later. The standard-library source is docs/growth/research/round-5/examples/R564.py; it uses synthetic data only. The output includes score age, eligible candidates, target, orders, cash and equity for every bounded-policy session.

Assertions verify all three target sequences, A’s exclusion at age two, the sale-and-replacement pair and final account values. Inspect the old-score session as well as the expired-score session. A test that checks only the final A holding could miss the entire intermediate B position and still appear to pass.

Additional assertions replace both missing A updates with NaN, positive infinity or negative infinity and require the same A/A/B/A path, finite eligible scores, ages zero/one/two/zero and unchanged final cash 575 and equity 1,055. If A is missing from the first observation, B is selected and A’s age remains null. If the entire first cross section is missing, there is no target, invented score, order or fee: cash and equity remain 1,000. These boundary cases are executed, not inferred from the ordinary path.

Maintain an explicit update contract

Store the maximum age, its clock, the replacement rule and the policy for existing holdings in the run configuration. Count fresh, reused, expired and never-observed securities each session. A sudden shift from fresh to reused can reveal a data pipeline outage even while rankings and account curves continue to look smooth.

When a late observation arrives, append its provenance and apply it only from its true availability. Distinguish a correction to an old fact from a new period’s measurement. Preserve the previous run so changes can be traced to eligibility or source revision rather than described as a better factor. The completion condition is an accountable path from each missing observation through score age to holdings and cash, with untested execution constraints remaining visible.

Check the age origin during handoff

An observation’s age begins when it becomes usable under the stated policy, not automatically at its accounting period end or download date. The synthetic session-three update is supplied before that session’s open. If a real update arrives afterward, it cannot reset the age for an earlier fill; the pipeline must move its first usable session accordingly.

Review the retained-value row and the source-event row side by side. A stored nine with age one is not a newly reported nine, even though both sort identically. The distinction affects outage detection, later revision handling and the explanation of every carried position. Keep a never-observed security in a separate status instead of inventing an initial retained score. These checks preserve the information policy when the small fixture is adapted to a larger panel.

FAQ

Should I replace a missing factor with zero?

Only if zero is a deliberately specified imputation with separate evidence; it is not a neutral representation of unknown information.

Does the highest synthetic ending value identify the best policy?

No. Policies must be selected for their information and trading contract before observing outcomes.