Skip to content

Commit 2b7172b

Browse files
Ben05-sysclaude
andcommitted
Add intraday percent, the move since today's open
chg measures against yesterday's close, so a stock that gapped up 5% and then sold off all day still reads as unchanged if it lands back at yesterday's price. gap_pct only sees the overnight jump. Neither one tells you what happened *during* the session. intraday_pct fills that gap: (price - open) / open, screenable as `intraday` or `sinceopen`. Live-priced like gap_pct, since it depends on today's open and the live price ticking against it; null rather than zero when open is missing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017b8HGHfwwreddySUhn7Reh
1 parent 33fea32 commit 2b7172b

3 files changed

Lines changed: 37 additions & 2 deletions

File tree

app/screen.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
"earnings_when", "peg", "mktcap", "chg", "is_adr",
3737
"rs_sector", "sector_change_pct",
3838
"dollar_volume", "avg_dollar_volume", "day_range_pct", "gap_pct",
39-
"true_range", "atr_pct", "sma_spread"]
39+
"true_range", "atr_pct", "sma_spread", "intraday_pct"]
4040
COLUMNS = [n for n, _ in store.UNIVERSE_COLUMNS] + DERIVED
4141

4242
TEXT_COLUMNS = {"symbol", "name", "sector", "industry", "country",
@@ -58,7 +58,7 @@
5858
# moves with all three of its.
5959
"dollar_volume", "avg_dollar_volume", "day_range_pct",
6060
# Built from today's open, which a stale snapshot may not have yet.
61-
"gap_pct",
61+
"gap_pct", "intraday_pct",
6262
# Built from today's volume, which only rises as the session runs.
6363
"rel_volume_10d",
6464
# Built from today's high/low, which only widen as the session runs.
@@ -128,6 +128,7 @@
128128
"liquidity": "avg_dollar_volume",
129129
"dayrange": "day_range_pct", "rangepos": "day_range_pct",
130130
"inrange": "day_range_pct", "gap": "gap_pct",
131+
"intraday": "intraday_pct", "sinceopen": "intraday_pct",
131132
"tr": "true_range", "truerange": "true_range",
132133
"atr": "atr_pct", "atrpct": "atr_pct",
133134
"when": "earnings_when", "earningswhen": "earnings_when",

app/universe.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,15 @@ def safe(a, b):
281281
df["gap_pct"] = safe(df["open"] - df["prev_close"], df["prev_close"]) * 100
282282
else:
283283
df["gap_pct"] = np.nan
284+
# The move since *today's open*, not yesterday's close. `chg` conflates
285+
# the overnight gap with what has happened during the session; a name
286+
# can gap up 5% and then sell off all day, which is a very different
287+
# stock from one that opened flat and has run up 5% since. Splitting
288+
# the two is what `gap_pct` (overnight) and this (intraday) are for.
289+
if "open" in df.columns:
290+
df["intraday_pct"] = safe(df["price"] - df["open"], df["open"]) * 100
291+
else:
292+
df["intraday_pct"] = np.nan
284293
# The real daily range, in dollars: `day_high - day_low` alone misses
285294
# the overnight gap, so a stock that gapped down hard and then sat
286295
# still reads as barely having moved. skipna=False so a missing input

tests/test_screen.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,31 @@ def when_for(hour, minute=0):
455455
"sma50": [1.0], "sma200": [1.0], "earnings_ts": [np.nan],
456456
}))["gap_pct"].isna().all())
457457

458+
print("\nintraday percent")
459+
check("gapped up but sold off since the open reads negative",
460+
abs(row["CHEAP"]["intraday_pct"] - (-100 / 21)) < 1e-9,
461+
row["CHEAP"]["intraday_pct"])
462+
check("distinct from chg: CHEAP is flat on the day yet moved intraday",
463+
row["CHEAP"]["chg"] == 0.0 and row["CHEAP"]["intraday_pct"] != 0.0,
464+
(row["CHEAP"]["chg"], row["CHEAP"]["intraday_pct"]))
465+
check("gapped down but ran since the open reads positive",
466+
abs(row["DEAR"]["intraday_pct"] - 300 / 97) < 1e-9,
467+
row["DEAR"]["intraday_pct"])
468+
check("opened at price is a real zero, not null",
469+
row["FLAT"]["intraday_pct"] == 0.0, row["FLAT"]["intraday_pct"])
470+
check("intraday resolves", screen.resolve("intraday") == "intraday_pct")
471+
check("sinceopen resolves too", screen.resolve("sinceopen") == "intraday_pct")
472+
check("intraday needs a live price and today's open, so it isn't prefiltered stale",
473+
"intraday_pct" in screen.LIVE_COLUMNS
474+
and "intraday_pct" not in screen.STATIC_SAFE)
475+
check("a frame with no open derives rather than raising",
476+
derive(pd.DataFrame({
477+
"symbol": ["X"], "name": ["X"], "sector": ["T"], "price": [1.0],
478+
"change_pct": [0.0], "volume": [1.0], "avg_volume_3m": [1.0],
479+
"market_cap": [1.0], "week52_high": [1.0], "week52_low": [1.0],
480+
"sma50": [1.0], "sma200": [1.0], "earnings_ts": [np.nan],
481+
}))["intraday_pct"].isna().all())
482+
458483
print("\ntrue range")
459484
tr = derive(pd.DataFrame({
460485
"symbol": ["NORMAL", "GAPPED"], "name": ["N", "G"], "sector": ["T"] * 2,

0 commit comments

Comments
 (0)