Cross-sectional momentum in agricultural futures

Buy the crops that have risen most over the past year, sell those that have fallen most. It lost money consistently — which turned out to be a clue about the data rather than about crops.

Conclusion: Failed

Failed on the reward for the risk, the confidence range, profit concentration, consistency across the history, the comparison against buying and holding and standing out from the search. Indicative performance of (6.1%) a year is 12.0 points below the 6.0% returned by holding the same universe continuously, return for risk taken sits at (0.53) against the benchmark's 0.48, and the worst fall of 196.2% is 134.3 points above the benchmark's 61.9%.

Figure 1 · Growth of $1

2002–2026 · net of 15bp round-trip costs
What one dollar became, after costs: $0.23 by 2026. Shown against Control, S&P 500, rebased to the same starting dollar.
Return for risk
(0.53)
range (0.97) to (0.10)
Ann. return
(6.1%)
volatility 11.5%
Max drawdown
196.2%
over 292 months
Beats the search
0.0%
chance, after correction

Method

Hypothesis

Futures-convention cross-sectional momentum: long the top tercile, short the bottom tercile of trailing 12m returns, inverse-vol weighted within each side, dollar-neutral.

Rule

UNIVERSE = 15 agricultural futures
LOOKBACK = 12 months
MIN_NAMES = 8
function positions(bar):
for symbol in UNIVERSE:
score[symbol] = sum(returns(symbol, LOOKBACK, ending bar))
if count(score) < MIN_NAMES:
return {} # too thin a field to rank
return long_short(score, count(score) / 3, bar)
# ── execution ────────────────────────────────────────────────────────
COST_PER_TRADE = 15 bps of the notional that changes hands
function on_bar(bar, book):
if not bar.is_month_end:
return # decisions are made monthly only
target = positions(bar) # the rule above
target = scale_to_gross(target, 1.0)
rebalance(book, target, bar)
function scale_to_gross(w, limit):
gross = sum(abs(w)) # total capital at work
if gross == 0:
return w # flat is a valid target
return w * limit / gross # leverage fixed, never implicit
function rebalance(book, target, bar):
for symbol in union(book.symbols, target.symbols):
delta = target[symbol] - book.weight(symbol)
if delta > 0:
buy(symbol, delta, fill = next_bar.close)
else if delta < 0:
sell(symbol, -delta, fill = next_bar.close)
book.charge(COST_PER_TRADE * abs(delta))
# Orders are filled at the next month's close, never the one the decision
# was made on. There is no stop_loss(), take_profit(), limit order or
# position cap: a holding changes only when positions() returns a
# different target at the next month end.

Analysis

Diagnostic value

Cross-sectional momentum is among the best-documented premia in finance. Finding it significantly negative in agricultural futures is not a refutation of momentum; it is a symptom.

Momentum applied to spliced continuous series buys instruments that jumped at the roll, then holds the new contract through its decay. The measured return is a property of the data-assembly convention with the sign reversed. This is the same artifact that inflated the calendar-seasonality result, observed from the opposite direction.

Evaluation

Table 1 · Performance

2002-03 – 2026-06 · 292 months, 292 invested
MeasureValue
Annualised return(6.1%)
Annualised volatility11.5%
Return for risk taken(0.53)
95% range(0.97) to (0.10)
t-statistic(2.60)
Maximum drawdown196.2%
Hit rate41.8%
Annual turnover6.45×
Return skew(0.04)
Return kurtosis4.35
The range is measured by resampling the history in blocks, so runs of good and bad months stay intact.

Table 2 · Robustness

DiagnosticValueReads asBadOkayYayHmm
Design half (pre-2015)(0.47)return for risk≤ 00 – 0.40≥ 0.40
Holdout half (2015+)(0.60)return for risk≤ 00 – 0.40≥ 0.40
Top-5-month share of profit(29.8%)how much rode on a few months — lower is better≥ 40%25 – 40%< 25%
Top-2-year share of profit(20.1%)concentration
Distinct trading episodes1how many independent runs this really is< 5050 – 100≥ 100
Chance the edge is real0.5%non-normality corrected
Chance it beats the whole search0.0%chance of beating the whole search by luck
bad/ok boundary is the pre-registered criterion · ok/good is a margin above it, not a criterion · blank where none was registered
The last row asks whether the result would stand out from the hundred-odd ideas tried here, rather than being judged alone.

Table 3 · Pre-registered criteria

measured against C3_RP_LONG
1FAILED2FAILED3FAILED4FAILED5FAILED6FAILED
CheckWhat it asksResult
1Reward large enough for the riskFAIL
2Confidence range clear of zeroFAIL
3Profit not concentrated in a few monthsFAIL
4Worked in both halves of the historyFAIL
5Beat buying and holdingFAIL
6Stood out from the whole searchFAIL
These six checks were written down before the strategy was run.

Table 4 · Net return by calendar year

after costs
2002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026+27%-27%
YearNet return
2002(3.5%)
2003(13.5%)
2004(20.5%)
2005(9.0%)
20065.1%
20077.5%
2008(6.1%)
20091.1%
2010(15.2%)
2011(27.1%)
2012(2.5%)
20130.7%
20147.6%
2015(21.2%)
20161.1%
2017(23.1%)
2018(10.2%)
2019(20.3%)
2020(14.6%)
2021(12.1%)
2022(6.7%)
202312.1%
202417.6%
20253.0%
20261.9%

Table 5 · Concentration detail

MeasureValue
Best years2024:+18%, 2023:+12%, 2014:+8%
Worst year2011:-27%

Reference

Instruments

last price and one-month change, live from Yahoo Finance
SymbolNotes
ZW=FChicago SRW Wheat — continuous futures on CBOT, quoted in US cents.
KE=FKC HRW Wheat — continuous futures on CBOT, quoted in US cents.
ZC=FCorn — continuous futures on CBOT, quoted in US cents.
ZS=FSoybeans — continuous futures on CBOT, quoted in US cents.
ZM=FSoybean Meal — continuous futures on CBOT, in USD.
ZL=FSoybean Oil — continuous futures on CBOT, quoted in US cents.
ZR=FRough Rice — continuous futures on CBOT, in USD.
SB=FSugar No. 11 — continuous futures on ICE Futures, quoted in US cents.
KC=FCoffee C — continuous futures on ICE Futures, quoted in US cents.
CC=FCocoa — continuous futures on ICE Futures, in USD.
CT=FCotton No. 2 — continuous futures on ICE Futures, quoted in US cents.
OJ=FOrange Juice — continuous futures on ICE Futures, quoted in US cents.
LE=FLive Cattle — continuous futures on CME, quoted in US cents.
GF=FFeeder Cattle — continuous futures on CME, quoted in US cents.
list truncated in the source record

Sample

2002-03 – 2026-06 · 292 months

Sources

NOAA Physical Sciences Laboratory. Climate indices. ONI, Niño 3.4, MEI v2, DMI, PDO, AMO, TNA, QBO, solar flux. psl.noaa.gov/data/climateindices/list
Queensland Department of Agriculture and Fisheries. Southern Oscillation Index. The Long Paddock. Monthly, 1876–. www.longpaddock.qld.gov.au/soi/soi-data-files
Yahoo Finance. Historical market prices. Adjusted close, month-end. Retrieved with yfinance. finance.yahoo.com

Revision history

RevStageStatusChange
01DraftDirection and criteria fixed before the experiment ran
02TestedFailedFell short on the reward for the risk, the confidence range, profit concentration, consistency across the history, the comparison against buying and holding, standing out from the search; control C3_RP_LONG
algorithmic-trading-anthology-july-2026-cross-sectional-momentum-agricultural-futures | d/rksci