Skip to content

Commit 3be7ccd

Browse files
Ben05-sysclaude
andcommitted
Stop prefiltering avg_dollar_volume on a stale price
avg_dollar_volume was marked STATIC_SAFE, meaning a screen like `avgdvol > 20m` narrowed the universe against the snapshot before any quote came back. But the field is price times avg_volume_3m, and price is the one live input in that product — a stock trading through the threshold since the snapshot was taken would get dropped before it ever had a chance to be repriced. That's the two-clocks-in-one-number mistake pct_52w_range already avoids by staying off STATIC_SAFE; this field was doing the same math and hadn't gotten the same treatment. It was already in LIVE_COLUMNS, so no repricing logic changes, only which bucket a screen condition on it lands in. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRdJUc8CJPpH9hHjJQ6S7m
1 parent 953e498 commit 3be7ccd

2 files changed

Lines changed: 16 additions & 13 deletions

File tree

app/screen.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -122,13 +122,12 @@
122122
# today's tape — same drift tolerance as `earnings_yield`, whose
123123
# denominator this shares.
124124
"payout_ratio",
125-
# Average dollar volume is a liquidity floor, not a reading of the
126-
# tape: `avgdvol > 20m` means "this name normally trades enough to get
127-
# out of", and half a day of price drift does not change that answer.
128-
# Today's `dollar_volume` is deliberately absent, for the same reason
129-
# `volume` is — it only rises, so a stale value drops the very names
130-
# that have since crossed the threshold.
131-
"avg_dollar_volume",
125+
# avg_dollar_volume is NOT here, though it looks like a standing fact:
126+
# it multiplies avg_volume_3m (slow) by `price` (live), so prefiltering
127+
# on a stale snapshot answers with yesterday's price baked into today's
128+
# threshold — the same two-clocks-in-one-number mistake `pct_52w_range`
129+
# avoids by staying off this set. It's in LIVE_COLUMNS instead, same as
130+
# `dollar_volume`, and gets re-priced before a screen sees it.
132131
# Two rolling averages, same drift tolerance as `sma_spread`. Today's
133132
# raw `volume` is deliberately absent, same reasoning as the line above.
134133
"volume_trend",

tests/test_screen.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -760,17 +760,21 @@ def when_for(hour, minute=0):
760760
and "volume_trend" not in screen.LIVE_COLUMNS)
761761

762762
# Today's dollar volume only rises, so prefiltering on a stale one would
763-
# drop the names that have since crossed the line. The average is a
764-
# standing fact about the name and narrows safely.
763+
# drop the names that have since crossed the line. The average looks
764+
# like a standing fact but multiplies a static volume by a live price,
765+
# so it waits for a live quote too, same as the raw figure.
765766
static, live = screen.split_live("avgdvol > 20m and dvol > 100m")
766-
check("average dollar volume narrows before re-pricing",
767-
static == "avgdvol > 20000000.0", static)
768-
check("today's dollar volume waits for live quotes",
769-
live == "dvol > 100000000.0", live)
767+
check("neither dollar-volume line narrows before re-pricing",
768+
static == "", static)
769+
check("both wait for live quotes",
770+
live == "avgdvol > 20000000.0 and dvol > 100000000.0", live)
770771
check("day range position is live",
771772
"day_range_pct" in screen.LIVE_COLUMNS)
772773
check("today's dollar volume is never prefiltered",
773774
"dollar_volume" not in screen.STATIC_SAFE)
775+
check("nor is its average, which bakes in today's price",
776+
"avg_dollar_volume" in screen.LIVE_COLUMNS
777+
and "avg_dollar_volume" not in screen.STATIC_SAFE)
774778

775779
dvol_filter = screen.filters_to_expr({"dvol": "Over $10M"})
776780
check("the dropdown compiles", dvol_filter == "avgdvol > 10m", dvol_filter)

0 commit comments

Comments
 (0)