52 Week High Definition (and Why Sources Disagree)
· OkamiStocks Team · 12 min read
Ask three sites for a stock's 52-week high and you can get three numbers. None of them is wrong. They just answer slightly different questions. The short 52 week high definition is simple: the highest price a stock traded at over the past year, with the 52-week low as its mirror. The long version has four choices hidden inside it, and if you are building a dashboard, a screener column or a backtest, those choices decide whether your number matches anyone else's.
This guide gives the definition, walks through each choice, shows how the OkamiStocks 52-week range endpoint makes them, and ends with a pandas version you can run on your own daily bars.
What is a 52-week high?
The 52-week high is the highest price a stock reached during the last 52 weeks; the 52-week low is the lowest. Together they are the 52-week range, the pair of numbers quote pages show as something like "548.20 - 689.70". The 52 week high meaning in everyday use is "the most this stock has cost in the past year", and that is close enough for conversation.
For code it is not enough, because "price" and "year" each need pinning down. Here is how two published definitions handle the first one:
- StockTitan: the numbers are "not closing prices, but the actual intraday highs and lows" (StockTitan, 52-Week Highs and Lows).
- Freetrade: the closing price "is the price which is generally used to determine if a stock has reached a 52 week high" (Freetrade dictionary, 52-week high/low).
One says intraday, the other says close. Both are in use. That is the first of the four choices.
The four choices that make sources disagree
1. Intraday high or closing high
A daily bar has four prices: open, high, low and close. An intraday 52-week high takes the maximum of the daily highs. A closing 52-week high takes the maximum of the daily closes. The intraday number is always greater than or equal to the closing number, and the two can be set on different days: a stock can spike to a new high at 10:15 a.m. and close well below it.
Sources pick one or the other, as the two definitions above show. If your number is a few percent below a quote page's, this is the first thing to check.
2. Regular session or extended hours
The NYSE core trading session runs 9:30 a.m. to 4:00 p.m. ET (NYSE, Hours and Calendars). Trades also happen before and after it: NYSE's affiliated markets, for example, list a late session from 4:00 p.m. to 8:00 p.m. ET on the same page. Extended-hours trading is thinner, so a single after-hours print can sit well above anything printed during the day.
A data source that builds its daily high from regular-session trades only will never report that print as the 52-week high. One that includes extended hours will. Neither is wrong, but they differ, and the difference shows up precisely on the days people care about: earnings releases land outside the core session.
3. How the 52 weeks are counted
"52 weeks" sounds exact and is not. Sources vary on all of these:
- Calendar or trading days. 52 calendar weeks is 364 days. A trading-day window counts sessions instead, and a year holds roughly 250 of them. The two drift apart around holidays and long weekends.
- Whether today counts. A live quote page updates the 52-week high the moment a new intraday high prints. A daily job that runs after the close usually works from finished sessions only, so today's trading joins the window tomorrow.
- Which day the window ends on, in which time zone. For US stocks the natural choice is New York time, so a window boundary moves at midnight ET, not midnight UTC. A job that uses UTC dates can be off by one day for several hours each evening.
- Inclusive or exclusive ends. Whether the first day of the window is "364 days ago" or "365 days ago" is a one-day difference, and once a year that one day holds the high.
In pandas terms, rolling("364D") and rolling(252) are both reasonable 52-week windows and give different answers on some days. The pandas documentation describes the distinction: an integer window is "the delta between the start and end of each window" in observations, while an offset string such as '2D' makes "each window ... a variable sized based on the observations included in the time-period" (pandas, DataFrame.rolling).
4. Split adjustment
In a 2-for-1 split, FINRA's example, a holder of 10 shares worth $100 each ends up with 20 shares worth $50 each, and "the total value of those shares is the same as it was before the split: $1,000" (FINRA, A Piece-by-Piece Guide to Stock Splits). The stock did not fall 50%, but a raw price series says it did, and a raw 52-week high computed after the split will sit at the old, pre-split level for up to a year.
A split-adjusted series divides every pre-split price by the split ratio (and multiplies pre-split volume by it), so the range compares like with like. Dividend adjustment is a separate question: it lowers earlier prices slightly on each ex-date, so check whether a published range applies it. Our corporate actions guide covers what each action does to a price series.
Put the four together and you get a definition that actually pins the number down. For example: the highest regular-session daily high, split-adjusted, over the 364 calendar days ending yesterday in New York time. Any source that states its four choices can be reproduced. Most don't, which is why the numbers disagree.
Percent from the 52-week high
The derived number most dashboards want is the distance from the high: (close - high_52w) / high_52w * 100. It is zero or negative by construction; a stock sitting at its high is at 0.00%. The mirror, (close - low_52w) / low_52w * 100, is zero or positive.
Two details worth deciding up front. First, which close: the last finished session's, or the live price? Mixing a live price with a finished-session high gives a number that can go positive during the day, which is confusing when the field is labelled "from 52-week high". Second, compute the percent from the same rounded prices you display, otherwise the percent and the prices beside it can contradict each other by a cent's worth.
Getting the 52-week range from the API
POST /api/v1/quote/range-stats returns the 52-week high and low with the day each was set, the latest close, the two percents, and the average daily volume over the last 10 and 50 trading days, on the Standard and Supreme plans. It makes each of the four choices explicitly, and the documentation states them (52-week range and average volume, data behavior):
| Choice | What the endpoint does |
|---|---|
| Intraday or close | Intraday: each day's highest and lowest price |
| Session | Regular session for the high and low; daily volume also counts pre-market and after-hours trades |
| Window | The 52 weeks (364 days) ending the day before your request, New York time; today never counts, and the window moves at midnight ET |
| Adjustment | split by default; raw, dividend or all on request |
| Ties | When two days share the high or the low, the later day is reported |
The request takes your API key as token in the body, the ticker, and an optional adjustment. As the documentation shows it:
export OKAMI_API_TOKEN='YOUR_API_TOKEN'
curl https://okamistocks.io/api/v1/quote/range-stats \
-d token="$OKAMI_API_TOKEN" \
-d ticker=SPY \
-d adjustment=split
The same call in Python:
import os
import requests
response = requests.post(
"https://okamistocks.io/api/v1/quote/range-stats",
headers={"Accept": "application/json"},
json={"token": os.environ["OKAMI_API_TOKEN"], "ticker": "SPY", "adjustment": "split"},
timeout=30,
)
print(response.status_code)
print(response.json())
response.raise_for_status()
A 200 response in the shape the documentation shows:
{
"data": {
"ticker": "SPY",
"adjustment": "split",
"start": "2025-10-31",
"end": "2026-10-29",
"as_of": "2026-10-29",
"sessions_covered": 250,
"high_52w": 689.7,
"high_52w_date": "2026-10-27",
"low_52w": 548.2,
"low_52w_date": "2025-11-20",
"close_price": 684.95,
"percent_from_high_52w": -0.69,
"percent_from_low_52w": 24.95,
"average_volume_10d": 58431207,
"average_volume_50d": 64118950
}
}
Reading it against the four choices:
startandendname the window, so you never have to guess the boundaries.endis the day before the request in New York time.as_ofis the last trading day actually found, andclose_priceis that day's close. The percents are computed from the rounded prices shown, so(684.95 - 689.7) / 689.7gives the-0.69you see.sessions_coveredis how many trading days the window held: about 250 for a stock that traded all year, fewer for a recent listing, 0 for a symbol with no trading data. A ticker with no data returns nulls andsessions_covered: 0, not an error.average_volume_10dandaverage_volume_50dare whole shares and are null until that many sessions exist.adjustmentechoes what was applied. Store it next to the numbers, as you would with historical bars, so arawrange never gets compared with asplitone.
A computed result is stored for the rest of that New York day and reused, so the first request for a ticker each day is the one that fetches a year of bars; a 503 with a Retry-After header or a data 422 can only come from that first fetch, and a failed request is never stored. Every call costs one request against your plan's limit. On the Free plan the endpoint answers 403 with {"message": "52-week range statistics need the Standard or Supreme plan."}; plan details are on the pricing section. The same numbers are available to an AI assistant as the stock_range_stats tool on the MCP server.
Computing it yourself in pandas
If you want a different set of choices (closing highs, a trading-day window, dividend adjustment), compute the range from daily bars. On Supreme, POST /api/v1/quote/historical returns 1Day bars with open_price, high_price, low_price, close_price, volume, volume_weighted and a UTC timestamp (historical bars). This reproduces the endpoint's defaults, then shows where to change each choice:
import os
import pandas as pd
import requests
NY = "America/New_York"
today = pd.Timestamp.now(tz=NY).normalize()
end = (today - pd.Timedelta(days=1)).date() # choice 3: finished sessions only
start = (today - pd.Timedelta(days=364)).date() # choice 3: 52 calendar weeks
response = requests.post(
"https://okamistocks.io/api/v1/quote/historical",
headers={"Accept": "application/json"},
json={
"token": os.environ["OKAMI_API_TOKEN"],
"ticker": "SPY",
"start": start.isoformat(),
"end": end.isoformat(),
"adjustment": "split", # choice 4: raw, split, dividend or all
"timeframe": "1Day",
},
timeout=30,
)
response.raise_for_status()
bars = pd.DataFrame(response.json()["data"]["bars"])
bars["date"] = (
pd.to_datetime(bars["timestamp"], utc=True).dt.tz_convert(NY).dt.date
)
bars = bars[(bars["date"] >= start) & (bars["date"] <= end)]
bars = bars.drop_duplicates("date", keep=False).sort_values("date")
latest = bars.iloc[-1]
newest_first = bars.iloc[::-1] # idxmax/idxmin return the first match, so a tie goes to the later day
high = newest_first.loc[newest_first["high_price"].idxmax()] # choice 1: high_price or close_price
low = newest_first.loc[newest_first["low_price"].idxmin()]
high_52w, low_52w, close = round(high["high_price"], 2), round(low["low_price"], 2), round(latest["close_price"], 2)
print("sessions_covered", len(bars))
print("high_52w", high_52w, high["date"])
print("low_52w", low_52w, low["date"])
print("close_price", close, latest["date"])
print("percent_from_high_52w", round((close - high_52w) / high_52w * 100, 2))
print("percent_from_low_52w", round((close - low_52w) / low_52w * 100, 2))
Things the code does on purpose:
- The date comes from the timestamp converted to New York time, not from the UTC string. The documentation's daily bars carry timestamps like
2024-01-02T05:00:00Z; taking the date in UTC happens to work for those, but converting first means a bar stamped at a different hour still lands on the right day. - Duplicate dates are dropped entirely (
keep=False), not deduplicated to the first copy. A repeated bar is a sign something went wrong upstream, and silently keeping one of them is how a wrong high survives. - Ties go to the later day.
idxmaxreturns the first match, so the frame is reversed first. - Rounding happens before the percent. The high is rounded to cents, then the percent is taken from the rounded values, so the printed percent agrees with the printed prices.
To switch to a closing-price definition, replace high_price with close_price and low_price with close_price. For a trading-day window, drop the date filter and take the last 252 rows (bars.tail(252)) instead. For a rolling series of 52-week highs over a long history, give the frame a datetime index first (highs = bars.set_index(pd.to_datetime(bars["date"]))["high_price"]); then highs.rolling("364D").max() gives the calendar version and highs.rolling(252).max() the trading-day version; note from the pandas documentation that min_periods defaults to the window size for an integer window and to 1 for an offset window, so the first year of the integer version is NaN and the first year of the offset version is a partial-window maximum.
Session choice is the one you can't change in pandas after the fact: it depends on how the daily bar's high was built, so check your source's documentation for it rather than inferring it from the numbers.
Mistakes to avoid in code
- Comparing a live price with a finished-session high and calling the result "from 52-week high". Decide whether the field is live or end-of-day and label it.
- Taking dates in UTC. A US session's bar can carry a UTC timestamp that belongs to the wrong calendar day in New York. Convert, then take the date.
- Mixing adjustments. A
rawhigh from last quarter and asplitclose from today are not comparable. Store the adjustment with every number. - Treating a thin window as a full one. A stock listed four months ago has a four-month "52-week" range. Report the sessions covered so the reader can tell.
- Treating "no data" as an error. A symbol with no trading data in the window has no range. Nulls and a zero session count are the honest answer, and that is what the endpoint returns.
- Recomputing a year of bars on every request. The range changes once a day at most. Compute or fetch it once per ticker per day and keep it.
The 52-week high is one of the simplest statistics in market data and one of the easiest to get subtly wrong, because the definition everyone agrees on stops one step short of the details that decide the number. Write down your four choices, or use a source that writes them down for you.
Ready to try it? Get a free API key, then see the 52-week range endpoint and its data behavior notes in the documentation. Range statistics need the Standard plan ($4 a month) or Supreme; the plans are compared on the pricing section.