Why is the volume column zero on every FX row?
/eod/EURUSD.FOREX for 2026-07-10 to 2026-07-27, pulled on 2026-07-28, returned fifteen rows with seven fields each: date, open, high, low, close, adjusted_close, volume. The volume field is present on every row and its value is the integer 0.
Not null. Not absent. Zero.
The live endpoint agrees, and you can read it here rather than take it on trust:
Why the number cannot exist
Volume is the quantity traded at a venue — shares, contracts or units, counted by whoever runs the book. FX has no single venue, no central order book and no consolidated tape — the structure the Foreign Exchange Foundations course describes in detail. Nobody, including the largest banks, sees the whole market. Reported FX turnover figures are surveys and estimates, not counts.
So the field is not zero because trading was quiet. It is zero because the quantity it names is not measurable in this market, and the column exists only because the schema is shared with equities, where it is.
Re-measured: zero is per pair, and so is everything else
Re-pulled on 29 September 2026, the same /eod/EURUSD.FOREX history is no longer zero. Every EURUSD row of 2026 carries a volume, as do all but one row each of GBPUSD and AUDUSD; USDJPY and EURGBP are still 0 on every row. The scale moves as well: EURUSD's 10 July 2026 row now reads a few hundred, its 25 September row sixteen million. And /real-time sends 0 for all three majors in the same hour.
None of that changes the argument above. A figure a vendor fills in for some pairs, over some periods, at a scale that jumps by more than four orders of magnitude, is not a count of the market. What changes is the guard: test the column per pair and per period, and treat a non-zero FX volume as no more usable for liquidity than a zero one.
The bug this causes
Zero is a number, and that is exactly the problem. Every one of these silently misbehaves:
df[df.volume > 0]deletes your entire FX universe and reports no error.- Any VWAP or volume-weighted average divides by zero.
- A "volume-confirmed breakout" rule never fires, so the strategy appears to have no signals rather than no data.
- A liquidity screen that ranks by dollar volume puts every currency pair last.
If the field had been null, most of these would have thrown. Because it is 0, they return an empty answer that looks like a result. This is the same failure mode as the option rows in the previous unit, and it is worth a defensive rule: on any virtual exchange, check whether a column is structurally empty before you compute with it.
The close is a convention, not an event
An equity close is an event: the venue runs a closing auction at a published time, and that single cross is the official close — not merely the last print, which is why trades still report after it. The auction has a published methodology, and that is the thing FX has no equivalent of. FX never stops, so a daily close is whatever cut-off the data provider chose — very commonly 17:00 New York, but that is a choice.
Two vendors with different cut-offs publish different closes for the same pair on the same day, and neither is wrong. This is the single most common source of "your data disagrees with my data" tickets in FX, and the resolution is never to determine who is right. It is to find out what each one cut at.
Six days a week, or seven, depending on the pair and the day
Now count the dates in that fifteen-row sample as it stood on 2026-07-28. Present: 07-10 (Fri), 07-12 (Sun), 07-13 to 07-17, 07-19 (Sun), 07-20 to 07-24, 07-26 (Sun), 07-27 (Mon). Missing: 07-11, 07-18 and 07-25, every Saturday.
So FX in that data ran six days a week: Sunday through Friday. The Sunday bar is the sliver between the Wellington and Sydney reopen and midnight, and it was tiny. On Sunday 2026-07-26 the bar was open 1.1391, high 1.1394, low 1.1391, close 1.1393, a range of 0.0003, or three pips. The Friday before, 07-10, ranged from 1.1410 to 1.1460, which is fifty pips.
Re-pulled on 28 September 2026, the same request returns eighteen rows. Every Saturday is now present, and the weekend bars were restated: the Sunday 07-26 bar now runs from 1.13674 to 1.13999, about 33 pips. Other pairs did not change. Over calendar 2025, measured the same day, GBPUSD.FOREX has 365 rows, USDJPY.FOREX 363 and EURUSD.FOREX 361, while EURGBP.FOREX has 294 and USDZAR.FOREX 278: weekdays, some Sundays and no Saturdays. The calendar of an FX series is a property of the pair and of the day you pulled it, not of the market.
Any statistic computed per bar, such as average true range, a rolling standard deviation or a per-bar volume proxy, is contaminated by those stub weekend bars unless you handle them deliberately.
When metadata and data disagree
One last check worth doing on any exchange. Read WorkingDays in the table below and count the days in the series above it:
/exchange-details/FOREX reports seven working days. On 2026-07-28 the EURUSD series had six; by 28 September it had seven, and a cross such as EURGBP still has six.
The data wins. Metadata endpoints describe intent; the rows describe what arrived. When they disagree, believe the rows and count them yourself. That is the whole of the trust-but-verify habit, applied to a calendar.
Try it now
- Here are the dates
/eod/returns for 9 to 20 July 2026, first forEURUSD.FOREXand then forEURGBP.FOREX. Count the rows per weekday for each. Which pair has Saturdays, which has Sundays, and what would a join of the two ondatedo to the weekend rows?
- Here are the first and last rows of the same eleven-week window for EURUSD and for USDJPY. Divide EURUSD's last volume by its first, and say what a filter like
volume > 0keeps from each pair. Then write the guard as a per-pair, per-period check rather than a per-asset-class rule.