Branch: Quant Developer (quant-dev) · Node QD1.02 · ~20 focused hours · Gate: quiz ≥ 85% + code-pack ≥ 85% (hidden tests green on at least 85% of exercises)
Every systematic strategy begins as a sentence a human believes: "cheap stocks tend to do better than expensive ones." "A stock that has gone up tends to keep going up, for a while." "A boring, profitable, low-debt company is a safer bet than an exciting one." Those sentences are not strategies. A signal (sometimes called a factor when it is built the same way, cross-sectionally, for a whole universe of names) is what you get when you force a sentence like that to become a number: one score per company, per period, computed the same way for every name, so that companies can be ranked against each other instead of merely discussed. This module is where that forcing happens in code, and (just as importantly) where you learn to ask the one question that separates a real edge from a plausible-sounding story: does this number, measured honestly against what actually happened next, predict anything at all?
Advertisement
That second half is not optional, and it is not a formality after the "real" work of building the signal. A raw ratio computed from a spreadsheet and an information coefficient computed from a decade of history are, mechanically, both just arithmetic, but only one of them tells you whether to trust the other. This module's code pack (pack id QD1.02, exercises QD1.02-e01 through QD1.02-e10) is built around exactly that discipline: four exercises construct real signals, value, momentum, quality, low-volatility, from this program's bundled India and US price and fundamentals data; the remaining six build the evaluation pipeline, cross-sectional standardization, the Information Coefficient, the Information Ratio, a decay utility, portfolio turnover, and a double-counting check, that tells you, without flattery, whether any of the first four are worth a rebalance's transaction costs. You will build all ten by hand once, in plain Python, before you ever let an AI assistant draft a line of it, because the entire point of this module is that you cannot review what you cannot compute yourself.
One honest preview, because this module practices what QD2.05 will later grade on: not every signal in this document turns out to work. You will measure one (carefully, the same way as the other three) and find that it shows essentially no predictive power in this program's own bundled evaluation panel. That is not a mistake in the module. It is the module's single most important worked result, because a curriculum that only ever shows you signals that work would be teaching you to trust names, not numbers.
Learning objectives
By the end you can:
- Define what a cross-sectional signal (or factor) actually is (one score per company per period, meaningful only in comparison to the other scores in the same period) and explain why factor construction is fundamentally a cross-sectional, not a time-series, discipline.
- Construct a value signal (an earnings-yield ratio) from bundled fundamentals and price data in code, including the explicit per-share-versus-aggregate simplification this module states and flags rather than hides.
- Construct a momentum signal (12-month-minus-1-month price momentum) from a price panel in code, and explain (from a named, real finding in the literature) why the most recent month is deliberately skipped.
- Construct a quality signal (a profitability, leverage, and earnings-stability composite) from fundamentals data in code.
- Construct a low-volatility signal (inverted trailing realized volatility) from a price panel in code.
- Cross-sectionally standardize (z-score) and winsorize a raw signal, separately in every period, so that raw units, scale, and outliers never contaminate a comparison across companies or across the India and US markets.
- Compute the Information Coefficient (Pearson and rank/Spearman) between a factor score and the forward return it is supposed to predict, period by period, and summarize an IC series into an Information Ratio, and use both, together, to tell a real signal from a plausible-looking one.
- Measure a factor's decay across forward-return horizons and its portfolio turnover, and connect both numbers to the practical cost of actually trading the signal.
- Diagnose double-counting between two signals via their pairwise correlation and an orthogonalized residual IC, so that "combining factors" does not silently re-weight the same underlying bet under two different names.
- (Productivity objective: R10 duality.) Use an AI coding assistant to accelerate signal-construction boilerplate under the Copilot Discipline, understand the by-hand version yourself first, then accelerate, then always review every line and run the hidden tests, because a hallucinated column name or a silently backwards sign reads exactly as fluent as a correct one.
The duality, stated once (R10). Objectives 1–9 are the understanding objective: the gate rewards being able to derive a signal, standardize it, and evaluate it honestly, by hand and in code, on data the exercise has never shown you before. Objective 10 is the productivity objective riding on top of it: a working quant developer absolutely uses an AI assistant to draft the tenth nearly-identical factor pipeline of the week, but only after they could have written the first one alone, and only with every line reviewed against the same hidden tests that grade you here.
Prerequisites & connections
Builds on. QD1.01 (Market & Fundamental Data Pipelines) owns the data layer this module simply assumes: corporate-action adjustment, and (critically) point-in-time, survivorship-controlled panels. Every fundamentals figure this module uses is treated as available only from its stated filed_date onward, never earlier; that discipline is QD1.01's to teach in full, and this module inherits it rather than re-deriving it (R1). DA1.01 (NumPy) and DA1.02/DA1.03 (pandas) teach the general-purpose array and dataframe mechanics (groupby, rolling windows, vectorized arithmetic) that make this module's constructions fast and idiomatic in real quant work; they are a strong, advisory companion, not a hard gate, because cross-branch prerequisites in this program are recommendations, never locks (this module's own code pack is written in plain Python precisely so that a QD-track learner who has not yet taken the DA branch is never blocked). QM1.01 (Probability, Distributions & Estimation) already defined covariance and correlation, ρ(X, Y) = Cov(X, Y) / (σ_X · σ_Y), and QM1.02 (Hypothesis Testing, Correlation & Regression) already taught you to test whether a sample correlation is distinguishable from zero. This module does not re-derive either. It assumes you hold both as working tools and spends its own time on the part that is genuinely this branch's job under the program's binding boundary: C-3: G1's QM branch owns the statistics and ML concepts (intuition, math, failure modes); this branch owns ML in code: implementation, validation, pipelines, and evaluation. Every time this module computes a correlation, it is applying QM1.01's formula, not reinventing it; every time it discusses whether an IC is "real," it is one concrete, repeatable instance of QM1.02's hypothesis-testing habit of mind, applied at production scale across dozens of periods rather than once by hand.
Feeds forward. QD1.03 (Building a Backtesting Engine) draws the division of labor explicitly: QD1.03 owns the mechanics of turning a signal into a position, lagging it correctly, and pricing it against realistic fills and costs, but "QD1.02 owns signal and factor construction, the content of the signal that this node merely schedules and pays for." The turnover you measure here is the direct ancestor of the transaction-cost drag QD1.03 will charge against it, and the decay you measure here is the direct reason a backtest's rebalance frequency is a real design decision, not an afterthought. QD1.04 (Portfolio Optimization) takes a combined, evaluated signal as its expected-return input; an optimizer cannot fix a signal with no real IC, and this module is where you learn to notice that before you hand it to an optimizer. QD2.03 (Performance & Attribution) will apply the exact same "a good-looking number can still be noise" discipline to the deflated Sharpe ratio that this module applies to the Information Ratio, same habit of mind, a different formula. QD2.05, the branch capstone, grades honesty about a strategy's limitations as its own scored line item; the IC/IR/decay/turnover vocabulary you build here is the concrete language that memo will be written in.
Cross-branch companions (soft, not gates). None required to start. If you have already taken DA1.05 (Statistics in Code), its scipy/statsmodels fluency will make the correlation machinery in §7 feel entirely familiar; if you have not, every formula this module needs is derived from arithmetic you already have from QM1.01, and the code pack is plain Python throughout.
Core teaching content
1. What a signal actually is, and why it is a cross-sectional discipline
Picture two numbers: "Company X's stock is up 40% this year" and "Company X ranks 4th out of 60 companies on 12-month price momentum, this month." The first is a fact about one company across time. The second is a signal, and notice that it says nothing at all about X in isolation; it is entirely a statement about X relative to 59 other companies, at the same moment. This is the single idea the rest of this module builds on, so hold it precisely: a factor is not a prediction that a stock will go up or down in absolute terms; it is a cross-sectional ranking device. You compute it the same way, from the same kind of inputs, for every name in a universe, at the same point in time, and what you are really claiming is a relative one, that the names ranking highest this period will, on average, out-earn the names ranking lowest, over whatever forward horizon you specify.
That single reframing (from "will this stock go up" to "will this rank predict that rank") is why almost every tool in this module is a per-period operation applied identically across many periods, never a single pooled calculation across all time and all names at once. Standardize within a period (§6). Correlate a period's scores against that period's forward returns (§7). Rank a period's top and bottom names into portfolios (§10). The moment you catch yourself computing a statistic across the whole panel at once, stop and ask whether you have quietly turned a cross-sectional question into a time-series one, it is the single most common structural bug in signal-evaluation code, and §6 names it directly as this module's first Common Mistake.
One further consequence follows directly from "cross-sectional": every name being ranked in a given period must be part of the same, consistently-defined universe that period, you cannot honestly compare a score computed over 15 US names against a score computed over 60 names that include those same 15 plus 45 unrelated others, because the mean and spread each z-score (§6) is measured against would differ for reasons that have nothing to do with any individual company's merit. This module keeps its four running universes explicit throughout: 15 US names, 15 India names, and, for the evaluation exercises, a separate 60-name bundled panel, precisely so a learner never accidentally mixes them mid-calculation. It is also why this branch takes survivorship so seriously (QD1.01's job in full, inherited here): a universe silently defined as "companies that still exist today" quietly excludes every company that failed or was acquired along the way, which, since failure and being acquired are not random with respect to a company's own past factor scores, can bias an evaluation's measured IC in ways that have nothing to do with the signal's real predictive power and everything to do with which companies happened to survive to be counted.
This module's four signals (value, momentum, quality, and low-volatility) are four of the best-documented, longest-running cross-sectional anomalies in the empirical finance literature, chosen because each has a distinct construction recipe and a distinct economic story, and because together they let you practice every evaluation tool on genuinely different inputs (price-only, fundamentals-only, and one of each). All four are built here from this program's bundled, seeded synthetic India (nse-eod, india-fundamentals) and US (us-eod, sec-fundamentals) datasets, every number that follows is either taken directly from those files or computed from them, never invented, and every one is reproducible by re-running this module's own code pack.
Value factor
The value factor is a stock-selection signal that ranks companies by how cheaply they are priced against what they earn. The usual measure is earnings yield, net income divided by price: a high yield means a cheap stock and a low yield an expensive one. Fama and French documented that cheap stocks have out-earned expensive ones over decades of US data, which is called the value premium.
4 min read, on its own page
3. Momentum: winners, for a while, minus the last month
Jegadeesh and Titman's landmark 1993 study documented momentum, that stocks which have outperformed over the past several months to a year tend to keep outperforming over the following few months, a finding replicated across markets and decades since. The standard construction is 12-1 month momentum: the cumulative price return from twelve months ago to one month ago, deliberately excluding the most recent month. That exclusion is not a stylistic choice; Jegadeesh's own earlier work (1990) had already documented a short-term reversal effect, a stock that has just moved sharply in the last few weeks tends to partially revert, for reasons tied to bid-ask bounce and short-term liquidity/microstructure noise rather than to any genuine earnings-driven trend. Skip the most recent month, and you keep the genuine trend-following signal while dropping the noise most likely to fight it.
In code, with a daily price series indexed so that prices[-1] is the most recent close, "twelve months ago" and "one month ago" are trading-day offsets, approximately 252 and 21 trading days, respectively (the standard convention: roughly 21 trading days per month, 252 per year). This module's momentum(prices, lookback, skip) is written generally, prices[-(skip+1)] / prices[-(lookback+1)] - 1, so the same function proves the concept on a short, hand-checkable list and grades the realistic case with real offsets.
As of the last bundled trading day, 29 May 2026, two contrasting real US names: BRPEAK shows 12-1 momentum of −0.2395 (a loser, its price one month ago, 56.26, sits well below its price twelve months ago, 73.98) while CRSTNE shows +0.1730 (a winner: 189.01 one month ago against 161.13 a year back). The same contrast appears in India: NILAM at −0.2275, CHANDANI at +0.1132. QD1.02-e03 grades momentum against real 253-trading-day slices of all four names (R6: BRPEAK/CRSTNE for US, NILAM/CHANDANI for India).
4. Quality: profitable, safe, and stable: without needing a price at all
"Quality" is a looser umbrella than value or momentum, but a specific, well-cited implementation is Asness, Frazzini, and Pedersen's Quality Minus Junk (2019) framework, which composites profitability, safety (low leverage), and stability into one score; Novy-Marx (2013) had already shown that gross-profitability alone carries real, separately-priced predictive power distinct from value. This module builds a three-component composite entirely from fundamentals, no price needed at all, which is itself worth noticing: quality, unlike value or momentum, is a statement purely about the business, not about what the market is charging for it.
- Profitability: return on assets,
net_income ÷ total_assets(most recent fiscal year). Higher is better. - Safety: leverage,
total_liabilities ÷ total_assets(same year). Lower is better, so this component is z-scored with its sign flipped. - Stability: the population standard deviation of the net-income margin (
net_income ÷ revenue) across the four fiscal years the bundled data carries. Lower volatility of margin is better, so this, too, is sign-flipped.
Each of the three is standardized cross-sectionally (§6) and the three z-scores are averaged into one composite. Real, computed results, same 2 June 2025 universe: in the US, WLBROK (composite +1.2356) and CRSTNE (+1.0123) lead: CRSTNE, notice, is both cheap (§2) and high-quality here, an entirely legitimate and unsurprising overlap, not evidence of a bug, while NRTHBY trails at −0.8088, dragged down mainly by leverage (its total liabilities run to roughly half of a 2,662 total-asset base) despite respectable absolute profits. In India, NILAM (+0.4986) and ARUNIMA (+0.4884) lead; GIRIRAJ trails at −0.2683. QD1.02-e02 grades quality_score against the full 15-name fundamentals cross-section in both markets (R6).
Low-volatility anomaly
Standard asset-pricing logic says riskier assets should earn more, on average, to compensate investors for bearing more risk. Ang, Hodrick, Xing, and Zhang (2006) documented a genuine puzzle running the other way: stocks with high idiosyncratic volatility have historically earned lower, not higher, average returns, the low-volatility anomaly. Baker, Bradley, and Wurgler (2011) offered one leading explanation: benchmark-relative mandates and leverage constraints push many real-world investors away from calm, "boring" names and toward volatile, lottery-like ones, bidding the volatile ones up (and their subsequent returns down) beyond what their true risk warrants.
4 min read, on its own page
6. Cross-sectional neutralization: standardize per period, or your comparison is meaningless
Look back at §2's raw earnings-yield numbers: US values cluster between roughly 0.13 and 0.67; India's between roughly 0.14 and 0.77. A raw ratio from one market is not automatically comparable to a raw ratio from the other, different currencies, different typical valuation levels, different scales entirely. The fix is the same one QM1.01 already gave you a name for: standardize. Within a single period, across the whole cross-section you are ranking, compute
z_i = (x_i − mean(x)) / std(x)
using the population standard deviation (dividing by n, not n − 1, this module's convention throughout, since a cross-section of, say, 15 or 60 names is the full population you are ranking that period, not a sample standing in for a larger one). A z-score of +1.5 means "one and a half standard deviations above this period's average, whichever market or currency the raw number happened to be quoted in", a directly comparable, unit-free quantity, ready to combine with a z-score built the same way from an entirely different raw signal.
Two further, necessary refinements. First, winsorize: clip each z-score to a fixed band (this module uses ±3 by default) before using it further, so that one wildly mis-recorded or genuinely extreme value cannot single-handedly dominate a combined score, a single company at z = +11 would otherwise swamp fifty-nine ordinary companies clustered between −2 and +2. Concretely: five raw values 1, 2, 3, 4, 100 z-score to roughly −0.54, −0.51, −0.49, −0.46, and +2.00, the outlier's raw value is twenty-five times the next-largest, and its unclipped z-score of +2.00 would already dwarf the other four's tight clustering near −0.5 in any combined average; winsorizing at ±1.5 pulls that one value back to exactly +1.50, keeping it the clear standout (it should rank highest) without letting it swamp everything else by an amount driven purely by how extreme one data point happened to be. Second, named here, not built as a graded exercise, because it is a real refinement beyond this module's tractable scope, a production pipeline often goes one step further and neutralizes a signal against sector or region membership (subtracting each sector's own mean, or regressing the raw signal on sector dummy variables and keeping only the residual) so that a "value" score is not secretly just a bet on which sector happens to look cheap this month. If "dummy variable" is a new construct, QM1.04 section 13 builds it from scratch and shows the trap that makes such a regression unsolvable, which is the one thing that will bite you when you try this at home. This module's own four signals are evaluated on a single combined universe deliberately to keep the graded work self-contained; know that this next layer exists, and that it is a straightforward extension of the same z-score-and-residual machinery you are about to build.
QD1.02-e05 grades zscore_cross_section(raw, winsor_limit) against the real US and India earnings-yield dictionaries from §2: standardizing BLUSTM's US-market 0.6740 into a z-score of +1.83 (the US universe's clear standout) and CHANDANI's India-market 0.7748 into +2.44 (India's, more extreme still, since India's raw yields cluster more tightly around a lower mean).
Information coefficient
The information coefficient (IC) measures how well a factor's ranking of stocks predicts what they go on to earn. Each period you correlate the factor score with the forward return, the return earned after the score was known, then average the results over many periods. A mean IC near zero means the factor has no predictive power, whatever its name suggests.
3 min read, on its own page
8. The Information Ratio: is the edge repeatable, or just one lucky month?
A single month's IC, even a strongly positive one, could be luck. The Information Ratio (IR) answers the more useful question, across many periods, is this factor's IC reliably positive, or does it swing wildly from strongly positive to strongly negative month to month? IR is exactly the mean divided by the (population) standard deviation of the IC series itself:
IR = mean(IC) / std(IC)
- the same signal-to-noise idea as a Sharpe ratio, applied to a series of correlations instead of a series of returns. Real numbers, the same 48-month panel: momentum's IR is 0.9633 (mean IC 0.1209, standard deviation of the IC series 0.1255, a strong, consistent edge, rarely swinging negative). Value's IR is 0.7902; quality's, 0.6811, both solid, if a notch less consistent. Low-volatility's IR is −0.0083, not just a weak mean, an essentially non-existent one relative to its own noise, exactly consistent with §7's finding. Two factors can share a similar-looking mean IC and still be very differently trustworthy; IR is what tells them apart, and it is the fairer basis for ranking multiple candidate factors against each other than mean IC alone.
QD1.02-e07 grades information_ratio(ic_series) against the real, full 48-value IC series for both momentum (expected 0.9633) and value (expected 0.7902), computed directly from the bundled panel.
One warning before you carry the phrase anywhere else. "Information ratio" names two different quantities in this curriculum, and both are standard usage in their own literature. The one defined just above, mean(IC) / std(IC), is a property of a forecast: it asks whether a ranking's predictive correlation is reliably positive across periods, and it can be computed before a single rupee is invested. The portfolio-management literature, and AA1.03, use the same two words for active return / tracking error: a property of a realised portfolio, computed from returns a manager actually earned against a benchmark. Momentum's 0.9633 here and a manager's information ratio of 0.9633 there are not comparable numbers and are not measuring the same thing.
Which one is meant is settled by what is in the numerator, so read that first and never the label. An exam question naming a benchmark, tracking error or active return wants AA1.03's definition; one naming an IC series wants this one. The two meet in the fundamental law of active management, which consumes a forecasting correlation rather than a realised ratio, so feeding this module's mean IC of 0.1209 into it with a naive breadth of 720 returns an implied information ratio of 3.24, a figure no manager has ever sustained. AA1.07 works that collision through and shows what the correct forecast correlation does to it. Keep both definitions and keep them labelled.
Alpha decay in trading
Alpha decay is the loss of a trading signal's predictive power as time passes and the market absorbs the information it captured. You measure it by recomputing the factor's IC against longer forward returns, such as 3 and 6 months. A fast-decaying signal only pays if you rebalance often, which raises trading costs, while a slower one leaves room to rebalance cheaply.
3 min read, on its own page
Portfolio turnover
Portfolio turnover is the fraction of a portfolio that has to be bought or sold to move from one period's holdings to the next. A turnover of 0 means an unchanged portfolio; a turnover of 1.0 means every single name changed. It matters because every trade has a cost, so a signal that needs heavy trading can lose its edge to those costs.
2 min read, on its own page
11. Double-counting: when a "new" factor is just an old one wearing a disguise
A genuine hazard in factor work: build a fifth signal, discover it has a respectable-looking standalone IC, and combine it into your model as if it were a fifth independent bet, when in fact it is 90% a restatement of a factor you already have. The check has two steps. First, the pairwise correlation matrix among your candidate factors: in this module's own bundled panel, every pairwise correlation among value, momentum, quality, and low-volatility sits between −0.001 and +0.031, essentially zero, a clean bill of health, meaning these four really are close to four independent bets, safe to combine without this particular caution.
Second (because a clean correlation matrix alone can still miss a subtler overlap) the sharper test is orthogonalization: regress the candidate signal on the existing one, keep only the residual (the part genuinely not explainable by the existing factor), and re-measure that residual's IC. If the residual's IC collapses toward zero, the candidate was mostly a repackaging, not new information. Demonstrate this deliberately, with a manufactured example built to fail the check: construct a "new" signal as 90% momentum plus a little independent noise (a fixed, seeded construction, reproducible exactly). Averaged across the panel, this candidate's correlation to momentum is 0.8747, and its standalone IC is 0.1200, nearly identical to momentum's own 0.1209, and every bit as tempting to add to a model. Orthogonalize it against momentum, though, and the residual IC falls to 0.0282, the overwhelming majority of its apparent standalone edge evaporates the moment momentum's own contribution is subtracted out, because that is exactly where it came from. A real desk orthogonalizes a new candidate against every existing factor jointly, via multiple regression (a straightforward extension using numpy.linalg.lstsq or scipy, outside this exercise's deliberately tractable single-comparison scope), and the joint version genuinely can catch something the pairwise version misses: a candidate signal could show a modest, unremarkable pairwise correlation against each existing factor individually, say 0.3 against momentum, 0.3 against quality, 0.3 against low-volatility, while still being almost entirely explainable as a weighted combination of the three, a redundancy no single pairwise check would ever surface on its own. The pairwise version taught and graded here is the same idea at the smallest scale that still teaches the lesson honestly; know that the joint version exists, and why it is strictly the more thorough check, before you assume a clean set of pairwise correlations is the whole story on a real, many-factor model.
QD1.02-e10 grades orthogonalize_and_ic(candidate_by_period, existing_by_period, fwd_return_by_period), returning the mean correlation, mean standalone IC, and mean residual IC across periods, against a real 8-month, 20-name slice built with exactly the 90%-momentum-plus-noise construction above.
12. Putting it together: the pipeline, and the code pack
Read top to bottom, this module's ten graded exercises are one pipeline, not ten unrelated puzzles: QD1.02-e01 through -e04 build raw signals (value, quality from fundamentals; momentum, low-vol from price panels); -e05 standardizes any of them into a comparable cross-sectional score; -e06 and -e07 are the evaluation core (IC, then IR); -e08 extends evaluation across time horizons (decay); -e09 and -e10 guard against the two ways a signal can look better than it is (unrealistic turnover assumptions, and double-counted "new" information). Every function in this module's code pack takes plain Python dictionaries and lists as input (a cross-section of scores keyed by ticker, a list of per-period cross-sections) deliberately, so that every one of them is testable in isolation, with a hand-checkable toy example, before you ever run it against a real 15- or 60-name universe. Build them in that order, and by QD1.02-e10 you will have a complete, working signal-evaluation pipeline you wrote yourself, one honest measurement at a time.
Common mistakes & how experts think differently
- Mixing a per-share figure with an aggregate one, or vice versa. §2's earnings-yield simplification (net income ÷ price, with an implicit constant-and-uniform share count) is stated and flagged in this module precisely because it is otherwise the single easiest value-signal bug to make invisibly, a real desk that divides aggregate net income by a per-share price without first converting to market capitalization gets a number that looks like a ratio and behaves like nonsense the moment share counts differ across companies. The expert's habit: before dividing anything, name out loud whether the numerator and denominator are both "per share," both "in aggregate," or deliberately, explicitly, a stated simplification, never silently mixed.
- Using a fundamentals figure before its filing date. A company's FY2025 net income was not knowable to the market until its filed date, not its fiscal year-end, a full-year figure with a December year-end might not be filed until the following May. Using it any earlier in a "point-in-time" panel is a small, easy-to-miss look-ahead bias, and it is exactly QD1.01's job (and this module's inherited assumption) to guard against it; §2's own worked cross-section deliberately uses each company's
filed_date, not itsfiscal_year, as the cutoff for exactly this reason. Concretely: BRPEAK's FY2025 net income (43.44) was not filed until 2025-05-18; an as-of date of 1 May 2025 must still use its FY2024 figure (42.96, filed 2024-05-17) even though FY2025 already existed "in the data" by then, a pipeline that joins on fiscal year alone rather than onfiled_datewould silently hand a 1 May 2025 backtest information that did not yet exist in the real world on that date. - Standardizing across the whole panel's history instead of within each period. Z-score a factor once, across every ticker and every month at once, and you have quietly turned a cross-sectional comparison into a time-series one, a name's score now partly reflects whether the whole market was generally cheap or expensive that decade, not whether it was cheap or expensive relative to its contemporaries that month. Standardize within each period, separately, always (§6).
- Forgetting to skip the most recent month in momentum. Skip §3's one-month gap and you have quietly re-introduced the short-term reversal effect Jegadeesh (1990) documented as fighting momentum, not reinforcing it, a "12-month momentum" signal that actually includes the most recent month often measures a weaker, noisier blend of trend and reversal rather than a clean trend signal.
- Trusting a factor's name, or its mean IC alone, without checking its IR. §7 and §8's low-volatility result is this module's sharpest example: a factor with a famous name and a plausible economic story can still measure at essentially zero predictive power in a given universe and period, and a factor's mean IC alone can look respectable while its IR reveals the mean is unstable, flipping sign period to period. Always compute both before trusting either.
- Generalizing a turnover or persistence measurement from one dataset without checking what that dataset was built to demonstrate. §10's near-random turnover finding is a property of this evaluation panel's own construction (independently drawn scores each period), not a universal fact about how real factors behave, a real price-derived momentum signal persists far more than this panel's pre-scored one does, and assuming otherwise from a single measurement is exactly the kind of over-generalization a careful quant developer catches before it reaches a report.
- Combining correlated signals as if they were independent bets. §11's orthogonalization check exists because a "fifth factor" that is 90% a restatement of an existing one, added naively, does not diversify a combined score, it just quietly overweights the one true underlying bet twice, under two different names.
- Reaching for Pearson IC when a handful of outliers dominate the cross-section. Pearson IC is sensitive to extreme values in either the factor score or the forward return; Rank IC (Spearman), built on ranks rather than raw magnitudes, is materially more robust in exactly that situation (§7), when in doubt, compute both, and treat a large gap between them as itself informative about how outlier-driven a period's result is.
- Confusing "the IC is statistically real" with "this is a tradeable edge after costs." A genuine, repeatable IC (§7–§8) says a ranking carries real information. It says nothing, by itself, about whether that information survives the turnover-driven transaction costs of actually trading it (§10), that reconciliation is precisely QD1.03's job, and treating IC/IR as the finish line rather than one necessary checkpoint is a common and consequential shortcut.
The thread through all nine: an expert never lets a plausible economic story, a famous name, or a good-looking single number stand in for the actual, measured, honestly-reported evaluation, the same discipline this program's AI-Augment callouts apply to a machine's confident answer applies with equal force to your own signal's confident-looking backtest.
Worked examples
Worked example 1: Value, computed and ranked (India, ₹)
Using §2's India universe as of 2 June 2025, rank NILAM and CHANDANI by earnings yield and interpret the gap.
Solution. NILAM: net income ₹391.20 (crore, FY2025, filed 2025-05-18), price ₹1,054.23 → earnings yield = 391.20 / 1,054.23 = 0.3711. CHANDANI: net income ₹376.23, price ₹485.56 → 376.23 / 485.56 = 0.7748. CHANDANI's yield is more than double NILAM's, under this module's ranking convention, CHANDANI looks meaningfully cheaper relative to its own earnings than NILAM does, even though NILAM is the larger absolute company by both revenue and net income. This is the value signal's entire point: it says nothing about which company is "better" in absolute size or quality (that is §4's job), only which one you are paying less for, per rupee of current earnings, right now. Read together with Worked Example 5's IC result, remember that this ranking is a bet, not a certainty: value's real, measured IC across the bundled panel is a modest 0.1013, meaning the ranking predicts on average, across many periods, not for any one pair of companies in isolation.
Worked example 2: Value and quality together, then neutralized (US, $)
CRSTNE showed up as both cheap (§2, earnings yield 0.6411, 2nd-highest in the US universe) and high-quality (§4, composite +1.0123, 2nd-highest). Confirm this is not a coding bug by re-deriving CRSTNE's quality components by hand, then compute what a simple equal-weight combination of its standardized value and quality z-scores would look like.
Solution. Quality, by hand. CRSTNE FY2025: net income 100.62, total assets 815.89 → ROA = 100.62 / 815.89 = 0.1233, among the highest ROAs in the US universe. Total liabilities 381.87 → leverage = 381.87 / 815.89 = 0.4680, below the US universe's mean leverage (roughly 0.51), so its safety component is favorable once sign-flipped. Its four-year net-income margins (91.77/443.79, 82.26/502.87, 77.87/514.54, 100.62/527.87) work out to roughly 0.2068, 0.1636, 0.1513, 0.1907, a population standard deviation of about 0.0218, one of the lowest (most stable) margin volatilities in the US universe, again favorable once sign-flipped. All three components point the same direction (CRSTNE really is profitable, safely levered, and stable) so its composite +1.0123 is not a fluke of the averaging; it is three genuinely good ingredients agreeing. Combining with value. §6 z-scored CRSTNE's earnings yield (0.6411, US-universe-relative) to +1.65. A simple equal-weight combined score would be (1.65 + 1.01) / 2 ≈ +1.33, a strong combined signal precisely because value and quality are, per §11's clean pairwise-correlation finding, close to independent bets here, so an agreement between them is more informative than either alone, not double-counted.
Worked example 3: Momentum, by hand, both directions (US, $)
Confirm BRPEAK's and CRSTNE's 12-1 momentum figures from §3 by hand, using the exact momentum(prices, lookback, skip) formula this module's code pack grades.
Solution. The formula is prices[-(skip+1)] / prices[-(lookback+1)] - 1, with skip=21, lookback=252. For BRPEAK: the close 21 trading days before the 29 May 2026 as-of date (i.e., prices[-22]) is 56.26; the close 252 trading days before that same as-of date (prices[-253]) is 73.98. Momentum = 56.26 / 73.98 − 1 = −0.2395: BRPEAK is down roughly 24% over the trailing year (ex-last-month), a clear momentum loser. For CRSTNE: prices[-22] = 189.01, prices[-253] = 161.13. Momentum = 189.01 / 161.13 − 1 = +0.1730: up 17.3%, a clear winner. Two real names, opposite signs, same exact formula, same as-of date, precisely the kind of contrast a cross-sectional ranking is built to separate, and precisely why QD1.02-e03's hidden tests include both directions rather than only a positive example (a solution that hardcodes "always return a positive number" would pass a positive-only test suite and fail this one immediately).
Worked example 4: Momentum and low-volatility together (India, ₹)
NILAM and CHANDANI showed contrasting momentum in §3 (−0.2275 vs. +0.1132). Compute both names' low-volatility scores and discuss whether momentum and low-vol appear to be telling the same story here.
Solution. NILAM's trailing 60-day annualized realized volatility (ending 29 May 2026, the same as-of date) is 0.2574, giving a low-vol score of −0.2574. CHANDANI's is 0.2576, essentially identical, a low-vol score of −0.2576. Despite a large momentum gap (34 percentage points apart), the two names' realized volatility is nearly indistinguishable. This is a useful, concrete illustration of §11's clean pairwise-correlation finding: momentum and low-volatility are close to independent signals in this bundled data, telling genuinely different stories about the same two companies, one about recent price trend, the other about recent price choppiness, and neither one is a disguised restatement of the other for this pair, exactly as the panel-wide correlation check in §11 found in aggregate.
Worked example 5: IC and IR, the honest comparison (evaluation panel)
Using the bundled synthetic-factor-panel, compare momentum's and low-volatility's IC and IR, and explain what each statistic adds that the other does not.
Solution. Momentum: mean IC across 48 months = +0.1209, standard deviation of that IC series = 0.1255, so IR = 0.1209 / 0.1255 = 0.9633. Low-volatility: mean IC = −0.0012, standard deviation = 0.1466, IR = −0.0012 / 0.1466 = −0.0083. The mean IC alone already tells most of the story here (momentum's is clearly positive, low-volatility's is essentially zero) but IR adds the reliability dimension: momentum's IR near 1.0 says its edge is not just positive on average but rarely swings deeply negative from month to month, while low-volatility's near-zero IR confirms its near-zero mean is not an unlucky measurement of a real-but-noisy edge, it genuinely has almost nothing to measure. If a hypothetical fifth factor showed a mean IC of +0.05 (smaller than momentum's) but an IR of 1.2 (higher), IR alone would correctly flag it as the more reliable bet of the two, even with the smaller average edge, exactly the kind of ranking mean IC alone cannot make.
Worked example 6: Decay, turnover, and a double-counting check, together (evaluation panel)
A junior colleague proposes rebalancing a momentum strategy quarterly instead of monthly "to save on trading costs," and separately proposes adding a new candidate signal that "looks great, 0.12 standalone IC, almost as good as momentum itself." Evaluate both proposals using this module's tools.
Solution. Rebalance frequency. Momentum's mean IC decays 0.1209 (1-month) → 0.0632 (3-month) → 0.0496 (6-month), moving to a quarterly rebalance does not simply "save costs for free"; it gives up 48% of the one-month edge along the way (0.0632 versus 0.1209, a 52% retention). Whether that trade is worth it depends on comparing the saved turnover-driven costs (§10, and ultimately QD1.03's transaction-cost model) against the given-up IC, a real analysis, not a slogan, and the colleague's "to save on costs" framing skips the half of the trade-off that is actually a cost, not a saving. The new candidate. A standalone IC of 0.12 sitting "almost as good as momentum itself" (0.1209) is exactly the profile §11's worked demonstration warns about, recall that the manufactured 90%-momentum-plus-noise candidate there had a standalone IC of 0.1200, nearly identical to momentum's own, and a correlation to momentum of 0.8747. Before accepting the colleague's candidate, run precisely that check: compute its correlation to the existing momentum factor, and if it is high, compute its orthogonalized residual IC. If the residual collapses the way the worked demonstration's did (0.1200 standalone down to 0.0282 residual), the "almost as good as momentum" pitch was mostly momentum wearing a new name, not a fifth independent bet, a five-minute check that would have caught a real double-counting mistake before it reached a live model.
Worked example 7: Quantile spread as a plain-English check on IC (evaluation panel)
A colleague, uneasy with correlation coefficients, asks "forget the statistics, if I had actually traded this, would it have made money?" Answer using quantile spread for momentum and for low-volatility, and reconcile with their IC results from Worked Example 5.
Solution. Momentum's top-quintile-minus-bottom-quintile average forward return, across the same 48 months, is +0.0149: 149 basis points a month of spread between the names momentum ranked most and least favorably, a plain, tradeable-sounding number that tells the same story as its IC of +0.1209 and IR of 0.9633: a real, repeatable edge. Low-volatility's quantile spread is −0.0003, three basis points, and in the wrong direction from what the "low-vol should win" story would predict, though far too small relative to typical month-to-month noise to read as evidence the anomaly runs backwards here; it is simply further confirmation, in return terms a trading desk cares about directly, of the same near-zero result IC already reported. The two metrics agree because they are measuring the same underlying relationship from two angles (a correlation coefficient and a portfolio-sort spread) not because one independently double-checks the other; when you compute both, you are testing your arithmetic and your intuition against each other, not gathering two separate pieces of evidence.
Worked example 8: A point-in-time bug, caught by the filing-date rule (US, $)
A junior developer builds a "value" panel by joining each price date to whichever fiscal year is numerically closest, for a price on 1 May 2025, they join FY2025 fundamentals (reasoning: "2025 is the closer year"). Using BRPEAK, show what breaks.
Solution. BRPEAK's FY2025 net income, 43.44, was filed on 2025-05-18, seventeen days after 1 May 2025. On 1 May 2025, the market could only have known BRPEAK's FY2024 net income, 42.96 (filed 2024-05-17, over a year prior). The "numerically closest fiscal year" rule hands the 1 May 2025 evaluation a net-income figure that would not actually exist in the world for another two and a half weeks, a textbook look-ahead bug, and in this specific case a small one in magnitude (42.96 versus 43.44 changes BRPEAK's earnings yield by under 2%) but structural and systematic: every company in the panel whose filing lands between "numerically closest fiscal year" and its true filing date is quietly leaking future information into a supposedly point-in-time snapshot, all in the same direction, all the time, precisely the kind of small, consistent bias that a single spot-check on one company might miss, but that a real backtest evaluated over hundreds of such dates would compound into a materially overstated, unearnable result. The fix, as §2 and Common Mistake #2 both state: join on filed_date ≤ as-of date, take the most recent such row, never "closest fiscal year."
Practice set
Work every problem fully (by hand, with a calculator) before reading its solution. Every problem uses data or numbers distinct from the Worked examples above, so you cannot simply recall an answer instead of computing it.
P1 (guided). Earnings yield, from scratch. A company reports net income of $24.50 (million) and trades at $98.00. Compute its earnings yield.
Solution. Earnings yield = net income / price = 24.50 / 98.00 = 0.2500 (25.00%).
P2 (guided). Ranking two companies by value. Company A: net income 40.0, price 200.0. Company B: net income 15.0, price 50.0. Which is cheaper by earnings yield, and by how much?
Solution. A: 40.0 / 200.0 = 0.20. B: 15.0 / 50.0 = 0.30. B is cheaper (higher earnings yield) despite having a smaller absolute net income, value ranks relative to price paid, not absolute size, exactly Worked Example 1's point.
P3. The per-share simplification, spotted. A colleague computes "earnings yield" as net income (in millions) divided by price per share, for two companies with very different share counts, and ranks them directly against each other. Explain precisely what could go wrong.
Solution. Net income ÷ price-per-share is only a valid, comparable ratio across companies if every company is implicitly assumed to have the same share count (§2), with genuinely different share counts, the ratio silently mixes an aggregate quantity (total net income) with a per-unit one (price per share) differently for each company, so the resulting numbers are not measuring the same thing and cannot be honestly ranked against each other. The fix is to use actual market capitalization (price × real shares outstanding) in the denominator, or aggregate net income over aggregate market cap, never an unstated mix of aggregate-over-per-share.
P4. Quality composite, by hand. A company has total assets 500.0, total liabilities 200.0, and this year's net income 60.0. In a cross-section where the ROA z-score works out to +0.8, the leverage z-score (sign-flipped, i.e. already applied to −leverage) works out to +0.3, and the stability z-score (sign-flipped) works out to −0.5, what is its quality composite?
Solution. Composite = mean of the three z-scores = (0.8 + 0.3 + (−0.5)) / 3 = 0.6 / 3 = 0.20. (Its own raw ROA, incidentally, is 60.0 / 500.0 = 0.12, and leverage is 200.0 / 500.0 = 0.40, neither figure is needed to answer the question once the z-scores are given, since the composite operates entirely on the standardized components.)
P5. Why quality needs no price. Explain, in one sentence, why the quality signal in this module (unlike value) can be computed with no price data at all.
Solution. Quality is a statement purely about the underlying business's profitability, leverage, and earnings stability (facts recorded in the fundamentals themselves) whereas value is explicitly a statement about cheapness relative to what the market is currently charging, which by definition requires a price.
P6. Momentum, by hand. A price series (oldest to newest) is [80, 82, 85, 88, 90, 92, 95, 98, 100, 105]. Using lookback=6, skip=2, compute the momentum.
Solution. prices[-(skip+1)] = prices[-3] = 98 (the 8th of 10 elements). prices[-(lookback+1)] = prices[-7] = 85 (the 4th of 10 elements). Momentum = 98 / 85 − 1 = 0.1529 (15.29%).
P7. Why skip the most recent month. A learner builds "13-month momentum" using the full trailing 13 months with no skip at all, arguing "more data must be better." Explain, from a named finding, why this is not obviously true.
Solution. Jegadeesh (1990) documented a genuine short-term reversal effect, a stock's most recent one-to-four-week move tends to partially revert, for reasons tied to bid-ask bounce and short-term liquidity effects rather than a genuine earnings-driven trend. Including the most recent month un-skipped blends that reversal noise into the trend-following momentum signal, typically weakening rather than strengthening it, "more data" is not automatically better if some of that data is measuring a different, partially offsetting effect.
P8. Low-volatility, by hand. Daily closes (oldest to newest, already the trailing window you need): [50.00, 51.00, 49.50, 50.50, 49.00]. Compute the annualized realized volatility (population standard deviation of the 4 daily returns, × √252).
Solution. Daily returns: 51.00/50.00−1 = 0.0200; 49.50/51.00−1 = −0.0294; 50.50/49.50−1 = 0.0202; 49.00/50.50−1 = −0.0297. Mean ≈ −0.00473. Squared deviations from the mean ≈ (0.0247)², (−0.0247)², (0.0249), (−0.0250)² ≈ 0.00061, 0.00061, 0.00062, 0.00062; population variance ≈ 0.000616; standard deviation ≈ 0.02482. Annualized: 0.02482 × √252 ≈ 0.02482 × 15.8745 ≈ 0.394 (39.4%). (Low-vol score would be −0.394.)
P9. Z-score, by hand. Four raw values: 5, 10, 15, 20. Compute each one's cross-sectional z-score (population standard deviation).
Solution. Mean = 12.5. Squared deviations: 56.25, 6.25, 6.25, 56.25; population variance = 125/4 = 31.25; std = 5.5902. Z-scores: (5−12.5)/5.5902 = −1.3416; (10−12.5)/5.5902 = −0.4472; (15−12.5)/5.5902 = +0.4472; (20−12.5)/5.5902 = +1.3416. (Notice the symmetry, an evenly-spaced raw series always produces a symmetric z-score series.)
P10. Winsorization, by hand. Five raw values: 1, 2, 3, 4, 100. Z-scoring gives the outlier (100) a z-score of roughly +2.00. With winsor_limit = 1.5, what is its winsorized z-score, and why does this matter for a combined signal?
Solution. Winsorization clips any z-score beyond ±1.5 back to exactly ±1.5, so the outlier's z-score is clipped to +1.50 regardless of how extreme its raw value or unclipped z-score actually was. Without this clip, a single mis-recorded or genuinely extreme value could dominate an equal-weight combination with several other well-behaved signals, distorting the combined ranking far out of proportion to how much real information that one data point actually carries.
P11. Pearson IC, by hand. Factor scores for four names: A=1, B=2, C=3, D=4. Forward returns: A=0.01, B=0.02, C=0.015, D=0.04. Compute the Pearson IC.
Solution. Means: factor 2.5, return 0.02125. Deviations (factor): −1.5, −0.5, 0.5, 1.5; (return): −0.01125, −0.00125, −0.00625, 0.01875. Products: 0.016875, 0.000625, −0.003125, 0.028125; sum = 0.0425. Sum of squared factor deviations: 2.25+0.25+0.25+2.25=5.0. Sum of squared return deviations: 0.0001266+0.0000016+0.0000391+0.0003516≈0.0005188. Pearson IC = 0.0425 / √(5.0 × 0.0005188) = 0.0425 / √0.002594 = 0.0425 / 0.05093 ≈ 0.8345. (A strongly positive IC. C's slightly-out-of-order return, 0.015 sitting below B's 0.02, is what keeps this from being a perfect 1.0.)
P12. Rank IC versus Pearson IC. Using P11's same four names, first convert both series to ranks (rank 1 = smallest), then compute the Spearman (rank) IC, and compare to P11's Pearson answer.
Solution. Factor ranks (already 1,2,3,4 in order): A=1, B=2, C=3, D=4. Return ranks, sorted 0.01 < 0.015 < 0.02 < 0.04: A=1, C=2, B=3, D=4, so as a series in A,B,C,D order the return ranks are 1, 3, 2, 4. Pearson correlation of [1,2,3,4] against [1,3,2,4]: means both 2.5; deviations factor −1.5,−0.5,0.5,1.5, return-rank −1.5,0.5,−0.5,1.5; products: 2.25,−0.25,−0.25,2.25, sum=4.0; sum of squared deviations both series = 5.0 each; Rank IC = 4.0 / √(5.0×5.0) = 4.0/5.0 = 0.80. Slightly lower than Pearson's 0.8345 here, the single rank inversion (C and B swapping places) costs the rank-based measure a little more than it cost the magnitude-based one in this particular case, illustrating that the two measures can and do disagree modestly even on identical data, which is exactly why this module computes and reports both rather than picking one.
P13. Information Ratio, by hand. A factor's IC across five periods: 0.10, 0.05, 0.15, −0.02, 0.12. Compute its IR.
Solution. Mean = (0.10+0.05+0.15−0.02+0.12)/5 = 0.40/5 = 0.08. Deviations: 0.02, −0.03, 0.07, −0.10, 0.04; squared: 0.0004, 0.0009, 0.0049, 0.0100, 0.0016; sum = 0.0178; population variance = 0.00356; std = 0.05966. IR = 0.08 / 0.05966 ≈ 1.341. (A strong IR, this factor's IC is positive in four of five periods and never deeply negative.)
P14. Interpreting decay. A factor's mean IC runs 0.09 (1-month) → 0.08 (3-month) → 0.075 (6-month). A second factor's runs 0.09 (1-month) → 0.02 (3-month) → 0.005 (6-month). Both start at the same 1-month IC. Which factor tolerates a slower rebalance schedule better, and why?
Solution. The first factor. Its IC barely decays (0.09 → 0.08 → 0.075, retaining over 83% of its edge even at six months), so rebalancing quarterly or even semi-annually gives up very little predictive power. The second factor's edge is almost entirely gone by three months (0.02, just 22% retained) and essentially fully decayed by six (0.005), it must be rebalanced close to monthly to capture most of its edge at all, which in turn commits it to whatever turnover-driven trading costs that frequency requires.
P15. Turnover, by hand. A top-quartile portfolio (25% of 20 names = 5 names) shows last period's top 5 as {A, B, C, D, E} and this period's top 5 as {A, B, F, G, H}. Compute the turnover.
Solution. Entries (new this period, not last): F, G, H → 3. Exits (were in, now out): C, D, E → 3. Turnover = (3+3) / (2×5) = 6/10 = 0.60.
P16. The random-reshuffle turnover ceiling. A top-decile portfolio (10% of 100 names = 10 names) is drawn independently at random each period. What turnover would you expect, in the long run, and show the arithmetic?
Solution. Two independent random 10-name subsets of 100 overlap, in expectation, by 10 × 10 / 100 = 1.0 name. Expected entries = expected exits = 10 − 1.0 = 9.0. Expected turnover = (9.0 + 9.0) / (2×10) = 18/20 = 0.90, the general formula being 1 − (portfolio size / universe size), here 1 − 10/100 = 0.90, matching this module's own §10 derivation (1 − 12/60 = 0.80) at a different portfolio-to-universe ratio.
P17. Orthogonalization, interpreted. A candidate signal shows a standalone IC of 0.09 and a correlation of 0.95 to an existing factor already in your model. After orthogonalizing against that existing factor, its residual IC is 0.01. Should you add this candidate to your model as a genuinely new, fifth bet? Justify from the numbers.
Solution. No, not as advertised. A 0.95 correlation to the existing factor already signals heavy overlap, and the collapse from a 0.09 standalone IC to a 0.01 residual IC after removing that overlap confirms it directly, roughly 89% of the candidate's apparent standalone edge (0.09 down to 0.01) was simply a restatement of the existing factor's own signal, not new information. Adding it as-is would not diversify the model; it would quietly double the weight on the one factor it is really measuring, under a second name.
P18. AI-drafted signal, reviewed. An AI assistant, asked to draft a "low-volatility score," returns a function computing trailing realized volatility and returning it directly, with no sign flip. A learner plugs this straight into a combined signal alongside value, momentum, and quality (all built so that higher = better). What goes wrong, and how does this module's review discipline catch it?
Solution. Every other component of the combined score is built so that a higher z-score means a more attractive name; an un-flipped realized-volatility score means higher = more volatile, the opposite convention, silently added into an equal-weight combination, it would push the model toward high-volatility names exactly where the low-volatility anomaly (§5) says it should push toward calm ones, quietly inverting a quarter of the combined signal's intent. The ai-augment-json callout's discipline in §5 catches this exactly the way Common Mistake #5 and this module's whole ethos insist on: run the draft against a tiny, hand-built example first (two names, one obviously calmer than the other) and check, by eye, that the calmer one gets the higher score, a check that takes ten seconds and would have caught this specific, common, and easy-to-miss sign error before it ever touched real data.
P19. Point-in-time join, applied. A company's FY2023 fundamentals were filed 2023-06-02; its FY2024 fundamentals were filed 2024-06-04. For an as-of date of 2024-03-15, which fiscal year's figures are the correct, legitimately knowable ones to use, and why?
Solution. FY2023. As of 2024-03-15, FY2024 has not yet been filed (its filing date, 2024-06-04, is nearly three months in the future relative to this as-of date), so the most recent fundamentals row whose filed_date is on or before 2024-03-15 is still FY2023 (filed 2023-06-02). Using FY2024 here, even though "2024" looks like the calendrically closer year to a 2024-03-15 as-of date, would be exactly Worked Example 8's look-ahead bug.
P20. Quantile spread, by hand. A top-quintile portfolio (3 of 15 names) averages a forward return of 0.045 this month; the bottom quintile (the other 3 lowest-ranked names) averages −0.010. What is the quantile spread, and how does its sign relate to whether the factor is behaving as expected?
Solution. Quantile spread = top-quintile average − bottom-quintile average = 0.045 − (−0.010) = 0.055 (5.5 percentage points). A positive spread means the factor is ranking correctly on average this period, the names it favored genuinely outperformed the names it disfavored; a persistently near-zero or negative spread (as this module found for low-volatility, §7) would indicate the factor is not doing the job its construction and economic story claim it does, regardless of how sensible that story sounds.
P21. Universe consistency, diagnosed. A learner computes z-scores for 15 US names in January but accidentally includes 3 extra India names that slipped into the same dictionary by a copy-paste error, before standardizing. Explain what goes wrong, referencing §1.
Solution. The z-score formula's mean and standard deviation are now computed over an 18-name group that mixes two different markets' typical valuation levels and currencies (§1's universe-consistency point), every one of the 15 intended US names' z-scores is silently distorted by the presence of 3 names that do not belong to the comparison, and the distortion is invisible unless you specifically check the input dictionary's key count and contents before standardizing, rather than only checking the output's shape.
P22. Synthesis: which of two candidate factors would you trust more? Factor P shows mean IC 0.06, IR 0.85, decays only mildly (0.06 → 0.05 → 0.045 at 1/3/6 months), and correlates 0.10 with your existing momentum factor. Factor Q shows mean IC 0.11, IR 0.30, decays sharply (0.11 → 0.02 → 0.00), and correlates 0.75 with your existing momentum factor (residual IC after orthogonalizing: 0.02). Which would you add to a combined model, and why?
Solution. Factor P. Despite Q's larger headline mean IC (0.11 versus 0.06), Q's IR of 0.30 shows that edge is far less reliable period to period, its sharp decay means it demands a rebalance frequency high enough to drive real turnover costs, and its 0.75 correlation to an existing factor plus a residual IC that falls to 0.02 (versus its 0.11 standalone) shows most of that headline number was already captured by momentum. Q adds turnover, cost, and complexity for very little genuinely new information. Factor P's smaller mean IC is more reliable (higher IR), decays gently (supporting a lower-turnover rebalance schedule), and, at only 0.10 correlation to the existing factor, is a genuinely close to independent bet, exactly the profile §11 and this module's whole evaluation pipeline are built to surface over a merely plausible-looking headline number.
Applied mini-project
Build, standardize, and honestly evaluate a two-factor composite, in both markets.
Using this module's code pack (QD1.02) opened in the Practice tab, build a small end-to-end pipeline of your own design:
- Construct. Pick any two of this module's four signals (value, momentum, quality, low-volatility) and compute them for both the US (
us-eod/sec-fundamentals) and India (nse-eod/india-fundamentals) universes, usingQD1.02-e01through-e04's functions. - Standardize. Z-score and winsorize each signal, separately, within each market (
QD1.02-e05), never pool US and India raw values together before standardizing. - Combine. Build a simple equal-weight combined score (the average of the two standardized signals) for each market.
- Check for double-counting. Before trusting the combination, compute the pairwise correlation between your two chosen signals in each market (adapt
QD1.02-e10's correlation step). If it runs high, orthogonalize and report the residual IC honestly rather than proceeding as if the two were independent. - Evaluate. Using the bundled
synthetic-factor-panel(which already carries pre-scored value/momentum/quality/low-vol columns matching this module's four signals) andQD1.02-e06/-e07, compute the Information Coefficient and Information Ratio of your two chosen factors, individually and combined. - Write it up, honestly. One paragraph stating which of your two factors carried the stronger, more reliable edge (by IC and IR, not IC alone), whether combining them added anything beyond what the stronger one alone would have given you (per your double-counting check), and (if either factor showed a weak or unreliable result) say so plainly rather than emphasizing only the flattering half of the analysis.
Self-check rubric, score yourself out of 10, pass at 8/10 with no zero on any item:
- (2 pts) Both signals are correctly computed in both the US and India universes, using the module's stated formulas (including the §2 per-share simplification, stated if value is one of your two choices).
- (2 pts) Standardization is performed separately per market, never pooling raw US and India values into one z-score calculation.
- (2 pts) The pairwise correlation/orthogonalization check is actually run, not assumed, and if it revealed meaningful overlap, the write-up says so rather than silently proceeding as if the two factors were independent.
- (2 pts) IC and IR are both reported for each factor individually (not just the combination), with the correct interpretation of what each adds.
- (2 pts) The write-up states which result was strongest plainly, and states any weak or unreliable result just as plainly, no result is omitted or downplayed because it was unflattering.
Lab: open the QD1.02 code pack in this node's Practice tab and clear all ten exercises (QD1.02-e01 through -e10) to green, hidden tests included, before starting this project, every one of the six steps above is a direct application of a function you will have already built and verified there.
Reading & resources
Everything below is real published work or reference documentation; treat this module's own worked numbers as the reproducible, hands-on companion, and these as the deeper "why" behind each signal's economic story. Free unless marked; tags show the ramp from first contact to primary literature.
The four signals, in the primary literature:
- Fama, E. & French, K., The Cross-Section of Expected Stock Returns (1992) and Common Risk Factors in the Returns on Stocks and Bonds (1993), the foundational documentation of the value premium. [Paid (journal); summaries widely available free] [Advanced]
- Jegadeesh, N. & Titman, S., Returns to Buying Winners and Selling Losers (1993), the original momentum study. [Paid (journal)] [Advanced]
- Jegadeesh, N., Evidence of Predictable Behavior of Security Returns (1990), the short-term reversal finding behind §3's one-month skip. [Paid (journal)] [Advanced]
- Novy-Marx, R., The Other Side of Value: The Gross Profitability Premium (2013), gross profitability as a quality dimension. [Free (SSRN)] [Advanced]
- Asness, C., Frazzini, A. & Pedersen, L., Quality Minus Junk (2019), the profitability/safety/stability composite this module's §4 is modeled on. [Free (SSRN/AQR)] [Advanced]
- Ang, A., Hodrick, R., Xing, Y. & Zhang, X., The Cross-Section of Volatility and Expected Returns (2006), the low-volatility anomaly. [Free (SSRN)] [Advanced]
- Baker, M., Bradley, B. & Wurgler, J., Benchmarks as Limits to Arbitrage: Understanding the Low-Volatility Anomaly (2011), a leading behavioral/institutional explanation for why the anomaly persists. [Free (SSRN)] [Advanced]
The evaluation framework:
- Grinold, R. & Kahn, R., Active Portfolio Management, the standard textbook source for IC, IR, and the "Fundamental Law of Active Management" connecting them to breadth and expected outperformance; the deeper theoretical companion to §7–§8. [Paid] [Advanced]
- López de Prado, M., Advances in Financial Machine Learning, anti-overfitting protocols for evaluating many candidate signals at once, without fooling yourself; the direct forward reference for QD2.04. [Paid] [Advanced]
- scipy.stats documentation (
pearsonr,spearmanr,rankdata), the real library functions this module's code hand-derives from first principles; read once you have built §7's IC by hand, to see the same idea in its production form. [Free] [Beginner]
This program's own cross-links:
- QM1.01 (Probability, Distributions & Estimation), the correlation and standard-deviation formulas this module applies, never re-derives.
- QM1.02 (Hypothesis Testing, Correlation & Regression), how to test whether a correlation is statistically distinguishable from zero, the natural next question after computing an IC.
- QD1.01 (Market & Fundamental Data Pipelines), the point-in-time, survivorship-controlled discipline this module's fundamentals cutoffs (§2) assume throughout.
- QD1.03 (Building a Backtesting Engine), where this module's turnover becomes a real transaction-cost line item, and its decay becomes a real rebalance-frequency decision.
The primary-source anchors, per this program's R6 discipline:
- SEC EDGAR (full-text search + XBRL), the real-world US primary source a genuine value or quality signal's fundamentals would be pulled from. [Free] [Beginner–Advanced]
- BSE / NSE company filings, the corresponding India primary source. [Free] [Beginner–Advanced]
Do this, not just read. Open this module's code pack, pick QD1.02-e04 (realized_vol) once you have it passing, and deliberately break the sign flip a combined signal depends on (§5's ai-augment-json callout, and Practice P18), feed it two hand-picked price series, one obviously calmer than the other, and confirm by eye which one the unflipped function ranks higher versus which one a correctly-flipped low-vol score ranks higher. Watching a sign convention actually flip a ranking, on data small enough to check by eye, is worth more than re-reading the paragraph that describes the risk in the abstract.
Teach it back & journal
Feynman prompt. Explain to a smart friend who has just heard the phrase "value investing" why a value signal, as built in this module, is not the same thing as "buying good companies", using the idea that a factor is a cross-sectional ranking device, not an absolute prediction, and that CRSTNE in this module's own worked examples was ranked highly on both value and quality at once, for two entirely separate, independently-computed reasons. Then explain, in the same breath, why this module bothered to build and report a factor (low-volatility, in its own bundled evaluation panel) that turned out to show essentially zero measured predictive power, and why hiding that result, rather than reporting it plainly, would have been the actual mistake. If your friend can, unprompted, ask "but does it actually predict anything, or does it just sound plausible?" about the next factor idea either of you encounters, you have installed this module's real lesson.
Journal prompt. Pick one of this module's four signals and, using the code pack's Practice tab, compute it on a universe or as-of date different from any used in this document's worked examples. Then run the full evaluation pipeline on it: standardize it, compute its IC and IR against the bundled panel's forward returns, check its decay across at least two horizons, and check its pairwise correlation against at least one of the other three signals. Log what you find, including if the result is unflattering. Then write a paragraph on this: which single habit from this module, stating a simplification instead of hiding it, standardizing per period instead of pooling history, checking IR alongside mean IC, or orthogonalizing before trusting a "new" signal's standalone number, would most change how a colleague evaluated a signal you handed them, and what will you do this week to make that habit automatic before you next build one?