Every popular rule in Chinese retail trading has a cousin in English: a big up day on strong volume continues; a big up day on weak volume fails. In A-shares it is usually phrased as 量价配合 — price and volume must agree.
I wanted to know if it survives contact with real data. So I pulled daily bars for 584 liquid Shanghai/Shenzhen names from free public endpoints (no API key, no paid vendor), built 15,900 stock-days, and conditioned the next day's return on today's move and today's volume ratio. Then I split every bucket by calendar month.
The rule works beautifully — until you split it by month. Then it flips sign.
Repo with the full script: github.com/Felixwang007/a-share-signal-lab
Why this question, on this particular day
Right now (midday, Sep 10 2026) the Shanghai Composite is at 3937.78, down 0.35%, on about ¥1.09 trillion of turnover — and here is the interesting part: out of 5,217 stocks I scanned, only 1,078 are up (20.7%), while there are 36 limit-ups against 6 limit-downs.
That is a narrow, theme-driven tape. Breadth is terrible; a small set of names is being chased hard. This is exactly the regime in which momentum rules look infallible — and exactly the regime where "the setup" quietly stops working the week after you commit to it.
Method
For every stock-day t with at least 5 prior bars and one forward bar:
| field | definition |
|---|---|
today |
close[t] / close[t-1] - 1 |
vr |
volume[t] / mean(volume[t-5..t-1]) — volume ratio vs the 5-day average |
next |
close[t+1] / close[t] - 1 — the thing being predicted |
for i in range(6, len(bars) - 1):
b, p = bars[i], bars[i-1]
vols = [x["volume"] for x in bars[i-5:i]]
avg5 = sum(vols) / 5.0
rows.append((
code,
b["date"],
b["close"] / p["close"] - 1.0, # today
b["volume"] / avg5, # volume ratio
bars[i+1]["close"] / b["close"] - 1.0, # tomorrow
))
Everything below is computed from that. No synthetic data, no curve fitting.
Baseline first
everything n=15900 next-day mean=-0.063% median=-0.123% win=47.3%
The window drifts slightly down. Keep that number — it is the yardstick for every "signal" that follows. Most retail backtests never print this line, and that omission is where fake edges come from.
Conditioned on today's move
down >3% n= 2330 mean=-0.184% win=45.8%
down 1-3% n= 3530 mean=+0.110% win=49.5%
flat +/-1% n= 4743 mean=-0.061% win=47.5%
up 1-3% n= 3162 mean=-0.192% win=46.0%
up 3-9.5% n= 1797 mean=-0.260% win=44.8%
limit-up >9.5% n= 338 mean=+1.199% win=56.5%
Only limit-ups show positive aggregate follow-through. Note what this already tells you: buying a normal 3-9.5% up day is worse than doing nothing in this sample — about 1,797 opportunities per quarter, 44.8% win rate.
Conditioned on volume alone
vol <0.7x (dry) n= 2293 mean=-0.121% win=45.3%
vol 0.7-1.3x (normal) n=10852 mean=-0.050% win=47.7%
vol 1.3-2x (mild) n= 2291 mean=-0.099% win=46.7%
vol 2-3x (spike) n= 371 mean=+0.168% win=50.4%
vol >3x (huge) n= 93 mean=-0.169% win=48.4%
Volume by itself is close to information-free. And "huge volume" is not bullish — n=93, negative mean. Anyone who has traded A-shares long enough has watched a 巨量 day mark the top; the data agrees, weakly.
The cross — where the rule lives
up 3-9.5% vol <0.7x n= 37 mean=-0.295% win=37.8%
up 3-9.5% vol 0.7-1.3x n= 828 mean=-0.560% win=42.5%
up 3-9.5% vol 1.3-2x n= 709 mean=-0.049% win=46.4%
up 3-9.5% vol 2-3x n= 187 mean=+0.346% win=50.8%
up 3-9.5% vol >3x n= 36 mean=-0.617% win=41.7%
In aggregate the folk rule holds: the "big up day on weak volume" bucket is the second-worst in the entire study (-0.560%, 42.5% win), and it is the single most crowded bad trade in the market — 865 samples, 5.4% of all stock-days.
And the middle row is the confirmation: same move, volume 2-3x, and the sign flips to positive (+0.346%, 50.8% win).
So: rule confirmed. Right? Let me ruin it.
Split by month
setup 2026-06 2026-07 2026-08 stable?
limit-up >=9.5% +2.18%/64.2% -1.39%/35.6% +5.46%/100% flips
up 3-9.5%, vol >=2x +0.48%/52.0% -0.33%/44.2% +2.32%/71.4% flips
up 3-9.5%, vol 1.3-2x +0.43%/52.6% -0.71%/37.7% +0.69%/52.6% flips
up 3-9.5%, vol <1.3x +0.18%/50.2% -1.43%/32.5% -0.61%/42.9% flips
flat, vol <0.7x -0.12%/44.8% -0.17%/45.4% +0.52%/52.8% flips
down >3%, vol <0.7x +0.01%/53.4% -1.15%/33.9% +0.57%/60.0% flips
down >3%, vol >=2x +0.60%/46.2% -1.24%/36.8% n/a flips
All seven setups flip. The headline: limit-up follow-through was +2.18% with a 64.2% win rate in June and -1.39% with a 35.6% win rate in July. Same rule, same market, thirty days apart.
A trader who validated the rule in June, sized up in July, and then blamed their psychology has misdiagnosed the problem entirely. The rule was never a rule. It was June.
(The August column is shown for completeness, but n=5-36 per bucket. Do not read it as evidence — including the 100% win rate. This is exactly the trap the whole post is about.)
The one-line fix: measure net of drift
A setup's edge is not its raw mean. It is its mean minus whatever the market itself did that month:
setup month n setup drift excess
limit-up >=9.5% 2026-06 229 +2.18% +0.049% +2.13pp
limit-up >=9.5% 2026-07 101 -1.39% -0.236% -1.15pp
up 3-9.5%, vol >=2x 2026-06 127 +0.48% +0.049% +0.43pp
up 3-9.5%, vol >=2x 2026-07 86 -0.33% -0.236% -0.09pp
up 3-9.5%, vol <1.3x 2026-06 462 +0.18% +0.049% +0.13pp
up 3-9.5%, vol <1.3x 2026-07 369 -1.43% -0.236% -1.20pp
down >3%, vol <0.7x 2026-06 103 +0.01% +0.049% -0.04pp
down >3%, vol <0.7x 2026-07 174 -1.15% -0.236% -0.91pp
The excess flips as well — and it flips harder than the drift. These setups are not independent alpha. They are amplifiers of the current regime: when the tape's next-day drift is positive they magnify it, when it is negative they magnify the loss. Magnitude ratios in this sample run from 1.4x to 6x the baseline drift.
In code, the entire discipline is one extra line:
drift = mean([r.next for r in month_rows]) # equal-weighted market drift
excess = mean([r.next for r in setup_rows]) - drift
Two things stop being possible once you do this. You can no longer mistake a bull month for a skill. And you can no longer be surprised when your "validated" setup bleeds for six weeks.
What I actually took away
- Print the baseline before printing the signal. One line, removes the largest source of fake edge.
- Require the excess to keep its sign across at least two regimes before risking money. On this dataset that filter kills all seven setups. That is a correct output of an honest test, not a failure of the test.
- Sample size does not rescue you. The biggest buckets (10,852 normal-volume days, 8,219 flat days) sit closest to the baseline. The exciting buckets are the small ones. n=93 "huge volume" days will happily support any story you want to tell — mine included.
- The most dependable number in the whole study is the baseline: 47.3% win rate, negative mean. In a flat-to-down tape, most aggressive entries have negative expectancy before costs and before T+1 constraint — neither of which this study models.
Caveats, stated plainly
- 584 liquid, mostly well-known names — not the full ~5,200-stock market. Microcaps and new listings are absent.
- Four months, heavily weighted to June-July. Effective sample is smaller than n suggests because consecutive stock-days overlap and are not independent. No significance testing is claimed anywhere in this post.
- No costs, no slippage, no T+1 executability check, no limit-up fill assumption. A signal you cannot fill is not a signal.
- Volume ratio uses a 5-day average; other windows produce other buckets.
Reproduce it (free data, no API key)
git clone https://github.com/Felixwang007/a-share-signal-lab
python study.py bars.json
bars.json needs daily_history — a per-code list of {date, open, high, low, close, volume}. Data sources that work without a key:
-
web.ifzq.gtimg.cn/appstock/app/fqkline/get?param=sh600519,day,,,320,qfq— daily K-lines, 320 bars -
vip.stock.finance.sina.com.cn/.../Market_Center.getHQNodeData— full-market snapshots for breadth -
qt.gtimg.cn/q=sh000001,sz399001— index quotes (GBK-encoded, pipe throughiconvif you are on a Unix shell)
The script prints all six tables above, including the monthly split, so you can drop in your own market, your own window, and your own definition of "volume spike."
If you want the packaged version of this workflow as an agent skill — the same free-data pipeline, the drift normalizer, and a breadth/limit-up regime gauge — I publish skills on 虾评 (xiaping.coze.com), and the study repo above is the reference implementation. If you run the split-by-regime test on a different market (US, HK, crypto), I would genuinely like to see whether your setups flip too — my guess is that most of them do.
Top comments (0)