My first submittable alpha: cash-flow reversion
A public log of what it took to create “one submittable alpha” on the quant trading platform WorldQuant BRAIN. In Part 1, I built an alpha factory (an automated simulation harness). In Part 2, the short-term reversal strategy hit a wall at 0.96. In this part, I finally got over the wall. The place where it happened was unexpected.
0. One-line summary
Short-term price signals and textbook fundamental signals were all blocked, but the one-line mean reversion signal “has the company’s stock price become expensive relative to its cash flow compared with its own past?” finally cleared the submission criteria. In the process, I learned what separates a “signal that looks good” from a “signal that actually passes.”
1. Starting point: the gates, and how to read them
An alpha is a formula that assigns position weights to each stock. If the value is high, it goes long (buy); if it is low (negative), it goes short (sell). The platform normalizes this into a dollar-neutral long-short portfolio and simulates its profit and loss on historical data.
There are gates you have to pass before submission.
- Sharpe > 1.25 (consistency of returns, risk-adjusted return)
- Fitness > 1.0
- Turnover 1~70% (the share of the portfolio replaced each day)
- autocorrelation < 0.7 (it must not look too similar to alphas I have already submitted)
- survive in a smaller universe (robustness)
The Fitness formula is the main character of this part.
Fitness = Sharpe × sqrt( |Returns| / max(Turnover, 0.125) )
Here max(Turnover, 0.125) is the hidden key. Once turnover drops below 12.5%, lowering it further does not raise Fitness at all. The denominator stops at 0.125.
The reversal strategy in Part 2 lived in the exact opposite world. There, signals that raised Sharpe also raised turnover, and signals that cut turnover cut Sharpe, so the two canceled each other out right before 1.0. That is why 0.96 was the ceiling. What I realized this time was that slow-moving fundamental signals already have turnover below 12.5%, so turnover is “free.” In other words, in the fundamentals world, the battlefield shrinks to Sharpe alone. That one line became the compass for the entire session.
2. Adversarial research: be wary of pleasant conclusions
Before pivoting toward fundamentals, I spun up a separate research agent to skeptically test, “Will this really work?” I told it to look for disproof, not optimism. The conclusion was “cautious optimism.” There were indeed public code examples that had cleared the gates with fundamentals, but they were all one-person self-reports, so they should not be trusted as-is; a single field would never be enough; and the core bottleneck was Sharpe, not turnover. Later, every one of these warnings proved right.
3. First attempt: copy-pasting public winners as-is
The fastest route is to take a recipe someone else publicly said “passed” and run it unchanged in my own account. I picked five. Examples included these.
- EV/EBITDA reversion: a value multiple that divides enterprise value by operating income (before depreciation and amortization). The more expensive, the more short.
- Retained earnings rank: earnings the company has accumulated over time. The more it has, (in the report) the more short.
- ROE × asset turnover: a quality metric that multiplies return on equity by sales ÷ assets.
Where does the data come from? The platform has datasets such as fundamental6 (quarterly financial statements, 886 fields), model16 (processed scores, 24 fields), and analyst4 (analyst estimates, 1324 fields). The problem was that field names in other people’s code (mdf_oey, fam_roe_rank, and the like) were old vendor names that did not exist on my platform. So I first queried the datasets, found the actual field names, and mapped them. Operating income was operating_income, net income was income, and return on equity fortunately existed as return_equity. After aligning all the names, I ran simulations with each recipe’s settings (degree of smoothing, neutralization group, weight cap).
The result was a complete failure to reproduce. But the shape of the failure was meaningful. Turnover came out almost identical to the reported values down to the third decimal place (EV/EBITDA was 0.254 versus 0.253). That means my field mapping and signal mechanics were correct. But Sharpe alone dropped sharply to 0.6~0.8 times the reported value. The retained-earnings signal died outright (Sharpe -0.03).
Lesson: a single self-reported simulation is not reliable. It was probably a number that happened to look good in another period or another universe, or it was overfit. With 4 simulations, I discarded one entire “copy-paste” path. It was a valuable negative result.
4. Second attempt: splitting fundamentals into pieces and combining them
The prescription the research trusted most was, “Combine several weak signals (diversification), and Sharpe will rise.” So I split fundamentals into 11 small signals (legs) by concept and measured each one. Here is what each leg was and how I interpreted it.
- assetturn = sales ÷ assets (efficiency: a company that uses its assets well). Sharpe 0.90, the strongest.
- salesyield = sales ÷ enterprise value (a company cheap relative to sales). 0.86.
- delever = negative of debt change ÷ assets (a company paying down debt). 0.65.
- Quality signals such as ROE, ROA, gross margin, operating cash flow÷assets. They were weak, at 0.20~0.47.
Each leg was made by first ranking companies against one another (rank), then subtracting the average within the same industry (group_neutralize). The point was to ask where a company stood relative to peers in its own industry.
Then I picked legs whose concepts did not overlap and combined them with equal weights. The result was disappointing. The best composite was 0.93, essentially the same as the best single signal (0.90). Adding more legs made it worse (with 7 legs, it fell to 0.56). Mixing weak signals at equal weights was not diversification. It was watering down. The weak signals dragged down the one strong signal. The prescription the research trusted most did not work on our data.
Another thing confirmed here was that cross-sectional value signals (comparing companies at the same point in time) are almost dead among large caps. The signal that ranked companies by EV/EBITDA had a negative Sharpe (-0.16). This is a place where the value premium has become crowded and worn out.
5. Breakthrough: same data, different “shape”
The turning point came from a one-line contradiction. EV/EBITDA was dead at -0.16 when ranking companies against one another (cross-sectional), but when compared with its own past (time-series), Sharpe 1.19 came out. The data was the same, but the shape of the signal split the result.
Time-series reversion is interpreted like this. If a value multiple has become abnormally high relative to that stock’s average over the last 63 trading days, it usually returns to the mean. So a stock “expensive relative to its own history” is short, and a stock “cheap relative to its own history” is long. As a formula, ts_zscore(배수, 63) gives how many standard deviations it is from its own mean, and putting a minus sign in front makes expensive names short.
When I ran several value multiples in this shape at once, one after another crossed Sharpe 1.0. One of them cleared the gates.
pcf (주가/영업현금흐름) Sharpe 1.62 Fitness 1.03 <- 통과
pb (주가/순자산) 1.36 0.93
ev_ebitda 1.19 0.69
ev_sales 1.08 0.70
pcf = -ts_zscore(cap / cashflow_op, 63). Market capitalization (cap) divided by operating cash flow (cashflow_op) is the price/cash-flow multiple. Operating cash flow, unlike income on the income statement, is actual cash that came in, so it is harder to manipulate through accounting. That is why mean reversion in “price relative to cash flow” is a sturdier signal than other value metrics. The data is simply cashflow_op (net cash flow from operating activities) from fundamental6 divided by cap (market capitalization) from a price dataset.
6. What it means to clear the gates: eight doors
Right before submission, the platform checks 8 items on the server: Sharpe, Fitness, turnover upper and lower bounds, weight concentration, overlap with competing alphas, autocorrelation, and Sharpe in a smaller universe (robustness). pcf passed all 8. In particular, the smaller-universe Sharpe was high at 1.47. It means the signal was not only accidentally right in the broad market, but remained robust even after reducing the stock set.
Finally, I made a small tuning change by raising smoothing (decay) from 1 to 2. When the signal is spread smoothly over two days, turnover falls from 0.244 to 0.188, and when turnover falls (it is still above 12.5%, so the effect matters), Fitness rises from 1.03 to 1.12. Since there is now room above the gate (1.0), it has a buffer that can withstand some performance decay in future data. This is the final submission version.
-ts_zscore(cap / cashflow_op, 63)
USA / TOP3000 / 지연 1일 / decay 2 / 업종 중립화 / 비중상한 1%
→ Sharpe 1.57, Fitness 1.12, 턴오버 18.8%, 작은 유니버스 1.47, 8/8 통과
7. Going further: scattering hypotheses with 17 agents
I did not stop with pcf. To find the next alpha, and the one after that, I needed diverse signals that did not resemble pcf (autocorrelation below 0.7). So I released 17 AI agents at the same time and scattered hypotheses. I gave them 8 exploration axes and had each one dig through different data: fundamental reversion, analyst estimate revisions, news sentiment, option volatility, social sentiment, corporate relationship graphs, signal interactions, and structural transformations. When each axis produced hypotheses, another agent skeptically validated and cut them down, and at the end one agent synthesized everything into a priority queue of 34 items.
One important operational constraint here: this account can run exactly 1 simulation at a time. So the agents only parallelized design, validation, and research, while I ran the actual simulations centrally in a single line. Creativity was scattered, execution was serial. That was the division of labor.
Here are a few examples of how each dataset can become an alpha.
- Analyst estimate changes:
analyst4contains analysts’ consensus EPS estimate (anl4_afv4_eps_mean). Looking at the change in this consensus (ts_delta) can capture stocks whose earnings outlook analysts are revising upward. Estimates change slowly, so turnover is low. - Option volatility:
option8contains implied volatility for calls and puts by expiration. If the implied volatility of calls is more expensive than that of puts (implied_volatility_call_30 - implied_volatility_put_30), it can be read as a signal that informed traders are buying calls (Cremers-Weinbaum put-call parity deviation). It is interpreted as an upside signal and goes long. - News sentiment:
news18is Ravenpack’s topic-level sentiment score. It is split by topic, such as earnings-news sentiment and target-price news sentiment (rp_css_ptg). It targets post-earnings announcement drift (PEAD), where price slowly follows when a positive earnings tone persists. - Corporate relationships:
pv13contains the average returns of a company’s customers, competitors, and partners (rel_ret_cust, etc.). It targets supply-chain information diffusion (lead-lag), where suppliers follow after customers rise first.
8. The highest Sharpe was the most dangerous
Among the scattered hypotheses, the session’s highest Sharpe came from the option call-put implied-volatility spread. A full Sharpe 1.83. Much higher than pcf. And because it came from option data, a completely different source from pcf (cash flow), it looked like candidate number two with no autocorrelation concerns. The only problem was that turnover was high at 38.8%, leaving Fitness at 0.77. When I smoothed the spread over 44 days and drove turnover down to 4.7%, Fitness shot up to 1.29. It was even higher than pcf.
But it failed the server’s eighth door, small-universe Sharpe (0.59, threshold 0.74). It was 1.72 across the full broad market, but when the stock set was reduced, it collapsed to 0.59. When I checked, that high Sharpe was concentrated in a small number of less actively traded stocks. Once narrowed to liquid large caps (TOP1000), the honest Sharpe was 1.01, below the gate.
This is overfitting. A number that looked good once was actually concentrated in a specific corner. The platform’s robustness check caught exactly that. The flashiest number was the least trustworthy number, and I was glad there was a gate to filter it out. cremers was honestly marked as deferred.
9. What I learned in this part
- The shape of the signal is more fundamental than the data. The same EV/EBITDA died cross-sectionally and came alive as time-series reversion. How you look matters as much as what you look at.
- Understand the battlefield first. For fundamentals, turnover was free, so I only had to look at Sharpe. Reversal was the opposite. Even with the same gates, the equation changes depending on the character of the signal.
- Suspect flashy numbers. The robustness check (small universe) is the real overfitting gate. Even if broad-market Sharpe is high, it is useless if it is concentrated in one corner.
- Only one from the same family. pcf, pb, and EV reversion are cousins, so their correlations are high. Submit only the strongest one among them, and look for the rest in other families.
- Where automation belongs. AI agents are strong at scattering hypotheses and skeptically filtering them. But because real validation has a serial bottleneck, the right division of labor was creativity in parallel and execution in serial.
In the next part, I look for a second alpha unlike pcf among the 23 hypotheses left in the queue. This is the beginning of the long grind of stacking up points.
Technical notes (appendix)
- Submitted alpha no. 1 (pcf):
-ts_zscore(cap / cashflow_op, 63), USA/TOP3000/delay1/decay2/INDUSTRY/truncation0.01. Sharpe 1.57, Fitness 1.12, turnover 0.188, returns 0.095, sub-universe 1.47. All 8 server checks PASS. (decay1 variant: Sharpe 1.62, Fitness 1.03.) - Fitness = Sharpe × sqrt(|Returns| / max(Turnover, 0.125)). Turnover below 12.5% fixes the denominator and becomes “free.”
- Operators:
ts_zscore(x, d)= how many standard deviations x is from its own past d-day average (time-series standardization).rank(x)= ranking stocks at the same point in time from 0 to 1 (cross-sectional).group_neutralize(x, g)= subtracting the group (industry, etc.) average to remove group bias.ts_mean(x, d)= d-day moving average (smoothing).ts_delta(x, d)= change versus d days ago.decay= a simulation setting that lowers turnover by spreading the signal smoothly over several days. - Time-series reversion family (decay1/INDUSTRY/63): pcf(cap/cashflow_op) S1.62, pb(cap/equity) S1.36, ev_ebitda S1.19, ev_sales S1.08, ev_ebit S1.14, pe(cap/income) S0.84. They are highly correlated with one another, so submit only 1.
- Cross-sectional fundamental legs (decay4/SUBINDUSTRY, group_neutralize+rank): assetturn(sales/assets) S0.90, salesyield(sales/EV) S0.86, delever S0.65, ROE/bookyield S0.47, grossprof S0.44, ev_ebitda cross-sectional S-0.16. Best equal-weight composite S0.93 (no diversification gain).
- 17-agent workflow: 8 axes (fundamental reversion, analyst revisions, news, options, social, pv13 relationships, interactions, structural transformations) × generation → adversarial validation → synthesis. Output = priority queue of 34. Because the simultaneous simulation limit is 1, agents did only design/validation/research, and simulations were serial.
- cremers-weinbaum (deferred):
group_neutralize(rank(ts_mean((iv_call_30 - iv_put_30)/iv_mean_30, 44)), industry), TOP3000 → Sharpe 1.72, Fitness 1.29, but FAIL on sub-universe Sharpe 0.59. In TOP1000, Sharpe 1.01. Concentrated overfit. - Datasets (USA TOP3000): fundamental6(financials 886), model16(scores 24), analyst4(estimates 1324), news18(Ravenpack sentiment 121), option8(IV/HV 64), pv13(relationship graph 165), and 14 total datasets.