Why Your Trading Strategy Takes No Trades: Two Sensible Rules That Cancelled Out
Why a trading strategy takes no trades: our QQQ scalp's bar-close confirmation and 0.15% VWAP guard could never both pass. How to check your own rules.
When a trading strategy takes no trades, the cause is often two of its own rules whose thresholds can’t both be met on the bars it trades. That’s what happened to our intraday QQQ strategy: a bar-close confirmation rule and a 0.15% VWAP extension guard each made sense on their own, but QQQ’s 1-minute bars run about 0.15–0.20% tall, so every confirmed entry was already too far from VWAP to be allowed. In one session, three textbook triggers fired and the guard vetoed all three.
Nothing errored. The strategy ran, tracked its bias, saw the triggers and declined every one, which from the outside looks exactly like a disciplined system waiting for a better setup. This page covers what the record shows, the arithmetic that makes the two rules incompatible, a second timing problem the same session exposed, and a check you can run on your own rules before they reach a market.
No trading result appears on this page. The session took no trades, and its price levels, P&L and account figures are not reproduced here.
What the journal recorded
The strategy is a VWAP-based scalp on QQQ 1-minute bars. Here’s the desk’s strategy journal entry for version 1’s session on 18 June 2026. It’s verbatim, except that price levels are redacted and one clause is omitted, marked […]:
- 2026-06-18 (v1, live): 0 trades. Window 9:30–10:20 ET (stood down early). Bias cycled chop→short→chop→long as QQQ whipsawed [REDACTED]–[REDACTED] around a flat-then-rising VWAP. 3 textbook triggers fired but all were vetoed by the 0.15% extension guard (2 shorts ~9:43/9:47, 1 long ~9:46/10:16) — QQQ 1-min bars run ~0.15–0.20% tall, so every confirmation bar closed >0.15% from VWAP. Conclusion: v1’s “bar-close confirmation” and “0.15% extension guard” are mutually exclusive on QQQ → strategy cannot fire. Also noted: ~90s loop latency misses the precise trigger bar. → v2 recalibration required. […] discipline held (no chasing).
The same record as a table:
| What the record shows | Value |
|---|---|
| Session window | 9:30–10:20 ET (stood down early) |
| Textbook triggers | 3 (2 short, 1 long) |
| Triggers vetoed by the extension guard | 3 |
| Trades taken | 0 |
| Extension guard | entry no more than 0.15% from VWAP |
| Typical QQQ 1-minute bar height | ~0.15–0.20% |
| Decision-loop cycle | ~90 s |
| Bar length the rules decide on | 60 s |
Why the two rules cancel out
Each rule guards against a real failure:
- Bar-close confirmation waits for a bar to close in the trade’s direction instead of acting on a touch inside the bar. It filters out the wick that pokes through a level and snaps back, which is a common false signal on a 1-minute chart.
- The 0.15% extension guard refuses an entry when price is more than 0.15% from VWAP. It stops a VWAP strategy from chasing a move that has already left the level it was built around.
The conflict comes from bar size. A confirmation bar has to move in the trade’s direction and close there, so its close lands roughly one bar’s height away from where the move started. These were VWAP setups in a session where, per the journal, QQQ “whipsawed” around VWAP, so each move started close to it. When the typical bar is 0.15–0.20% tall and the guard allows 0.15%, the confirmation close lands at or beyond the guard’s edge almost by construction. The journal says it directly: “every confirmation bar closed >0.15% from VWAP.”
So on this instrument at this bar size, the two rules don’t just make trades rare. Together they allow none: a bar big enough to confirm an entry is, because of its size, a bar that breaks the guard. Neither rule is wrong, which is why the problem is easy to miss. The conflict exists only when the rules meet a property of the data, the bar’s height, which appears nowhere in the strategy’s code. Reviewing each rule on its own finds nothing.
That makes it a different problem from the data defects in backtesting mistakes that only real data reveals. Those are flaws in the price series. This one is a flaw in rule design, and it shows up on clean data. The journal’s next step was “v2 recalibration required”. This page covers the design failure, not the recalibrated version.
The second problem: a loop slower than the bar
The same entry records a timing fact: “~90s loop latency misses the precise trigger bar.” A strategy that decides on 1-minute bar closes but only checks the market about every 90 seconds can’t see each close when it happens. Here’s the arithmetic. It’s an illustration, not the desk’s code, and it assumes a loop that wakes exactly every 90 s and ignores processing time:
# Illustrative arithmetic, not the desk's code: a 90 s loop reading 1-minute bars.
BAR, LOOP, HOUR = 60, 90, 3600 # seconds
closes = range(BAR, HOUR + 1, BAR) # bar closes in one hour
wakes = range(LOOP, HOUR + 1, LOOP) # loop wake-ups in the same hour
lag = [min(w for w in wakes if w >= c) - c for c in closes]
print(len(closes), len(wakes)) # 60 40
print(max(lag), sum(lag) / len(lag)) # 60 30.0 (seconds late)
seen_two = sum(1 for w in wakes if sum(1 for c in closes if w - LOOP < c <= w) == 2)
print(seen_two) # 20 wake-ups find two new bars waiting
Every hour has 60 bar closes and 40 wake-ups. A close gets noticed up to a full bar late, 30 seconds late on average, before any processing time. Half the wake-ups find two new bars waiting. A loop that only evaluates the most recent bar never treats the other one as a possible trigger bar. On a strategy where the entry is a specific bar’s close, being up to a full bar late is a design flaw, not a performance detail. The fix is a loop that runs well inside the bar, or one that reacts to each bar close as an event. The broader latency problem is covered in running an autonomous trading agent.
How to check your rules before they trade
The lesson the desk recorded is short: check every threshold against the instrument’s typical bar range before going live. In practice:
- Measure the bar. Compute the median high-low range of the bars you’ll trade, as a percent of price. Measure it over the window you actually trade, because a range measured over the whole day can differ from the open.
- Put every distance threshold next to that number. If a rule says “within X%” or “at least X% away” and X is about one bar’s height, the rule is responding to bar noise, not to the setup.
- Replay the rules as a funnel. Count how many triggers each filter removes, alone and in combination. If the combined count is zero over weeks of history, the strategy can’t trade, however good its logic looks.
- Log every veto with its reason. A strategy that records “trigger fired, vetoed by guard” turns a silent zero into one you can diagnose. This journal entry could name the cause after one session because the vetoes and their reason were recorded.
- Match the loop to the bar. The decision loop should run several times per bar, or react to each bar close as it happens.
Step 3 as code. This is a simplified illustration, not the desk’s strategy. The trigger is defined here as a bar closing across session VWAP, and the confirmation as the next bar closing further in the same direction. Swap in your own definitions:
# Illustrative pre-live check, not the desk's code: replay one rule pair on
# historical 1-minute bars and count how many triggers survive each filter.
import pandas as pd
def rule_pair_funnel(b: pd.DataFrame, guard_pct: float = 0.15) -> dict:
day = pd.Series(b.index.date, index=b.index)
tp = (b["high"] + b["low"] + b["close"]) / 3
vwap = (tp * b["volume"]).groupby(day).cumsum() / b["volume"].groupby(day).cumsum()
above = b["close"] > vwap
# trigger: a bar closes across VWAP (same session as the bar before it)
trigger = above.ne(above.shift(1)) & day.eq(day.shift(1))
# confirmation: the next bar closes further in the direction of the cross
nxt_close, nxt_vwap = b["close"].shift(-1), vwap.shift(-1)
confirmed = trigger & day.eq(day.shift(-1)) & (
(above & (nxt_close > b["close"])) | (~above & (nxt_close < b["close"])))
# extension guard, measured where the entry happens: at the confirmation close
within_guard = (nxt_close - nxt_vwap).abs() / nxt_vwap * 100 <= guard_pct
bar_range_pct = (b["high"] - b["low"]) / b["close"] * 100
return {
"median_bar_range_pct": round(float(bar_range_pct.median()), 3),
"triggers": int(trigger.sum()),
"confirmed": int(confirmed.sum()),
"confirmed_and_within_guard": int((confirmed & within_guard).sum()),
}
If median_bar_range_pct is about the size of guard_pct and confirmed_and_within_guard is near zero while confirmed isn’t, you have this page’s failure. Then re-run with the guard set to a multiple of the measured bar range instead of a fixed percent, and see whether the funnel opens.
A zero-trade session is easy to misread
The journal’s last clause, “discipline held (no chasing)”, is accurate, and it’s also the trap. A system that can’t fire and a system that’s correctly waiting produce the same session: no trades. Nothing in the result tells them apart. Only the veto record does: three triggers, three vetoes, every one from the same rule, for the same reason.
That’s why one zero-trade session was enough to diagnose this. A strategy that trades rarely by design needs its “no” decisions logged as carefully as its “yes” decisions, or a broken rule pair can look like patience for weeks. The full sequence of pre-trade checks is in how to validate a trading strategy.
Frequently asked questions
Why does my trading strategy take no trades?
A common cause is filters that can’t all be true at the same moment on your instrument’s real bars, not a quiet market. In our case a bar-close confirmation and a 0.15% VWAP extension guard were mutually exclusive on QQQ 1-minute bars, which run about 0.15–0.20% tall. Every confirmed entry was already outside the guard, so three triggers in one session produced zero trades.
How do I find which rule is blocking my trades?
Log every trigger with the reason it was rejected, then replay your rules on history as a funnel: count the triggers, then how many survive each filter alone and all of them together. The filter where the count collapses is the one to examine. Compare its threshold with the median bar range of the instrument over the hours you trade.
What is a VWAP extension guard?
It’s a rule that blocks an entry when price is more than a set distance from the volume-weighted average price, 0.15% in this strategy. It’s meant to stop a VWAP-based strategy from chasing a move that has already run away from the level. It can conflict with a bar-close confirmation rule when the typical bar is as tall as the guard is wide.
Is a 90-second polling loop fast enough for 1-minute bars?
Not for rules that act on a specific bar’s close. With a loop that wakes exactly every 90 seconds, an hour has 60 bar closes but only 40 wake-ups. Each close is noticed up to 60 seconds late, 30 on average, and half the wake-ups find two new bars at once, before any processing time.
Does loosening one filter fix it?
Not by itself. The conflict is between the pair and the instrument’s bar size, so the fix is to recalibrate the pair against that bar size and re-test. For example, size the guard as a multiple of the measured bar range instead of a fixed percent. The journal’s own conclusion was that the strategy needed recalibrating, and this page does not report how the recalibrated version performed.
This is educational content about strategy design and testing method, not financial advice. No positions, live signals, account values, price levels or trading results appear on this page.