# historicaldata.net - U.S. Stock Bars (daily and one-minute) This file describes everything needed to read, interpret, and independently verify the data. It ships with every download so the data explains itself without reference to any website. Sections 1-9 describe the full product; a free sample zip carries its own opening section, generated from the sample's contents, ahead of this text. --- ## 1. What you have Plain CSV files, no archive to unpack. The same bars are published in two arrangements; take either or both. History starts **2003-10-01**, which is itself a trading day. A security's own file starts on its first trading day, which is usually later. Exchange test symbols (NTEST, CTEST, ZVZZT and their kin) are excluded from every file here, including `symbols.csv`: the SIP feeds carry them, but they are not securities. The exclusion list is fixed and is checked before every publish. | Path | Contents | |---|---| | `day_by_symbol/{SYMBOL}_day.csv` | One file per currently listed security, its whole history. 17 columns, header row included. | | `day_by_symbol/{SYMBOL}_day_delisted_{YYYY-MM-DD}.csv` | One file per delisted lifecycle. A symbol can be reassigned to a different company, so each life is a separate file. | | `day_by_date/{YYYY-MM-DD}_day.csv` | One file per trading day, every security that traded that day. Same 17 columns plus `symbol`. **Rolling window - roughly the last 25 calendar days, not the whole history.** | | `*/manifest.json` | The file list for that collection: name, byte size, SHA-256, and which trading days are missing. | | `day_by_date/last-21-days_manifest.json` | The same, restricted to the trading days inside the last 21 calendar days. Subscribers fetch this one. | | `symbols.csv` | One row per lifecycle: name, security type, exchange, delisting date, CIK, FIGI. | | `README.md` | This document. | | `update.py` | Keeps a local library current from your subscription. | ### File names {SYMBOL}_{grain}.csv A_day.csv {SYMBOL}_{grain}_delisted_{date}.csv AACT.WS_day_delisted_2025-09-25.csv {date}_{grain}.csv 2025-01-02_day.csv `grain` is `day` or `minute`, and it sits in the same position in a listed and a delisted name, so one rule reads either. It is part of the name and not only of the directory, so the two granularities can be kept in one folder without colliding. **Underscore separates the fields; a symbol never contains one.** A symbol can contain a **dot** - `AAC.U`, `EBR.B`, `AACT.WS`, 211 of them in this dataset. Strip the literal trailing `.csv` (Python's `os.path.splitext` does this correctly - it takes only the final `.csv`), then split the rest on `_`; never split on dots. **Which one do you want?** `day_by_symbol` is the archive - a time series per security, covering the whole period, and the shape you want for backtesting one name. `day_by_date` is a cross-section per day, the shape you want for ranking the whole market on a given date. **`day_by_date` only holds a rolling window**, roughly the last 25 calendar days. It is a delivery channel rather than a second archive: `update.py` merges each day's cross-section into a `day_by_symbol` library, so a subscriber downloads one small file a day and ends up holding the full history. Nothing is lost by the rotation - once a day has settled (see below) the raw columns in `day_by_symbol` never change, and a dated file's `adj_*` columns equal its raw ones by design, so any past cross-section can be cut back out of `day_by_symbol` exactly as it was published. **One difference between them, and it matters.** In `day_by_date` the `adj_*` columns equal the raw columns. A cross-section file is a point-in-time record: it contains no information from after its own date, so a later dividend or split never rewrites it. Two things can still change while a day sits inside the window - its own record. First, the consolidated feed keeps revising a day's `volume`, `transactions` and occasionally `vwap` for about two trading weeks after it (late-reported and cancelled trades; prices do not move), so we refetch the last 15 trading days every night and republish any day whose rows changed. Measured across five years, nothing changes after that settling period - a day older than two weeks is final. Second, if the exchange revises or withdraws a dividend that fell on that date, we correct it, and the corrected file replaces the one you downloaded. That is why `update.py` re-checks the days you already hold against the published checksums instead of matching on file names alone. Once a day has rolled out of the window it can no longer be refetched, so corrections to it travel a different way: the dated manifests carry a `corrections` list (section 6) covering the last 90 days of them, and `update.py` applies it to your library. Adjustment looks backwards from today, so it lives in `day_by_symbol` (and in the library `update.py` builds for you, which it recomputes whenever a dividend or split arrives). If you need adjusted prices, use `day_by_symbol` or let `update.py` build it - do not read `adj_close` out of a dated file and expect it to be adjusted. **Security type (`CS`, `ETF`, `PFD`, ...) is not in the file name.** Read it from `symbols.csv`. Do not infer it from the symbol. Throughout this dataset the identifier column is named `symbol`, matching the exchanges' own listing directories and FIX tag 55. Where you see the word "ticker" elsewhere in the industry, it means the same thing. --- ## 2. Column dictionary (17 columns, fixed order) | # | Column | Type | Meaning | |---|---|---|---| | 1 | `date` | date | Trading day, `YYYY-MM-DD`, U.S. Eastern. | | 2 | `open` | number | Session open, as traded. Never adjusted. | | 3 | `high` | number | Session high, as traded. | | 4 | `low` | number | Session low, as traded. | | 5 | `close` | number | Session close, as traded. | | 6 | `volume` | number | Shares traded. Counts every trade, including those outside the regular session (section 7). Whole shares until 2026-02-22; fractional from 2026-02-23 (section 7). | | 7 | `vwap` | number | Volume-weighted average price for the day. | | 8 | `transactions` | integer | Number of trades. | | 9 | `adj_open` | number | Column 2 adjusted for splits and cash distributions (section 4). | | 10 | `adj_high` | number | Column 3 adjusted. | | 11 | `adj_low` | number | Column 4 adjusted. | | 12 | `adj_close` | number | Column 5 adjusted. | | 13 | `adj_volume` | number | Column 6 divided by the row's adjustment factor - the **same** factor that multiplies the prices, cash distributions included - so adjusted price x adjusted volume equals raw price x raw volume. Six decimals. Derived from the `volume` printed here; section 4 shows the reproduction and its rounding. | | 14 | `adj_vwap` | number | Column 7 adjusted. | | 15 | `dividend` | number | Cash per share with this row's date as the ex-date. Blank when there was none. | | 16 | `dividend_type` | text | Passed through from the source's classification: `CD` regular, `SC` special, joined with `+` when several fall on the same ex-date. A trailing `+CUR:XXX` is our own marker: column 15 is denominated in `XXX`, not USD - see section 4. **The regular/special distinction is reliable in recent years but not historically** - the source only began labeling `SC` consistently around 2023 (Costco's 2012-2020 special dividends all carry `CD`; the 2023 one carries `SC`). Amounts and ex-dates are unaffected, and the adjustment math treats both types identically, so `adj_*` is correct either way. Treat the label as advisory for older rows. | | 17 | `split` | text | Split ratio written `from:to`, e.g. `1:4`. Blank when there was none. | Rows are ordered by `date`, ascending. Files are ASCII with CRLF line endings and a header row. Files in `day_by_date/` carry one extra column, `symbol`, inserted as column 2; columns 1 and 3 through 18 are the table above, in the same order. Rows there are ordered by `symbol`. ### Minute files (18 columns) `minute_by_symbol/{SYMBOL}_minute.csv` uses the same 17 columns in the same order, with two differences: - column 1 is `timestamp` instead of `date`: `YYYY-MM-DD HH:MM:SS`, the start of the minute, U.S. Eastern wall-clock time (EST or EDT as the date dictates); - column 18, `session`, is appended: `pre` before 09:30, `reg` from 09:30 to 15:59, `post` from 16:00. On an early-close day the regular session ends at 13:00, so `reg` runs to 12:59 and `post` starts at 13:00. So the minute header is `timestamp,open,high,low,close,volume,vwap,transactions,adj_open,adj_high,adj_low,adj_close,adj_volume,adj_vwap,dividend,dividend_type,split,session`. Within a row, `open`/`high`/`low`/`close`/`volume`/`transactions` describe that one minute; a minute with no trade has no row. `vwap` and `adj_vwap` are present for layout only and are **blank in every minute row** - the minute feed does not carry a VWAP and we do not compute one. `dividend`, `dividend_type` and `split` are written once per event, on the **first row of the effective date** (usually a pre-market minute), and are blank on every other minute of that day; the adjusted columns of the whole day, and of every earlier row, reflect the event exactly as in the daily file. `minute_by_date/{date}_minute.csv` inserts `symbol` as column 2, like the daily cross-sections. Every column answers to one test: an algorithm consumes it. `dividend_type` is here because adjustment conventions branch on it - excluding special distributions from a capital-events-only series, or from a dividend-yield factor, requires knowing which is which. Fields that no formula consumes (declaration, record and pay dates, currency, frequency) are deliberately **not** inlined; they live in the dividends master table that ships separately, so bookkeeping detail never rides along with every price row. --- ## 3. The blank rule - read this before writing any parser > **A blank field means "not available". It never means zero. > We never write 0 to stand in for a missing value.** This is the central design commitment of this dataset, and it is the same commitment across every product we publish. Where an upstream source uses `0` as a missing-value marker, we detect it and write a blank instead. | Column | Blank means | It does **not** mean | |---|---|---| | `dividend`, `dividend_type` | No ex-dividend on this date | A distribution of $0.00 | | `split` | No split on this date | A 1:1 split | | `vwap`, `transactions` | The value was not carried for this bar | No trading occurred | | `adj_*` | The adjustment could not be derived for this row | An adjustment factor of 1.0 | If you require a numeric column, substitute your own sentinel deliberately - do not assume blank can be read as 0. A blank `dividend` and a `dividend` of 0.000000 are different statements: the first says nothing happened, the second says a distribution of zero was declared. --- ## 4. How the adjusted columns are computed Each adjusted column is the raw column multiplied by one factor per row: the product of every corporate action dated **after** that row. | Action | Factor | |---|---| | Split, written `from:to` | `from / to` - a 1:4 split gives 0.25 | | Cash distribution | `(P - D) / P`, where `P` is the close on the last trading day before the ex-date and `D` is the cash per share on the ex-date | | Volume | `adj_volume = volume / f`, where `f` is the row's factor above - splits **and** cash distributions - so `adj_close x adj_volume = close x volume`. Written to six decimals. | Nothing else is applied. No smoothing, no back-filling, no vendor-specific conventions. **Reproducing `adj_volume`, and how close you can get.** KO on 2022-09-15, an ex-dividend date (0.44 cash) with no split anywhere in the file: volume 11451929 adj_volume 12790928.203067 close 59.53 adj_close 53.2982 The factor is the product of every later distribution, and the printed prices reproduce it only to four decimals: `adj_close / close = 0.8953166...`, and `volume / 0.8953166 = 12790926.02` - within 2.2 shares (1.7e-7) of the printed `adj_volume`. That residue is the rounding of `adj_close` to four places, not a different formula: `adj_volume` was computed with the full-precision factor. It is a real distribution effect - the file holds no split, so the split-only reading of this column would predict `adj_volume = volume`. `verify.py` applies exactly this tolerance when it checks the column. Two cases are worth stating outright, because they look like errors and are not: - **Several actions on one day.** Cash distributions sharing an ex-date are summed and produce a single factor, `(P - D1 - D2) / P`. Splits sharing a date are written joined with `+` (`3000:1+1:3000`) and their factors multiply. That pairing is the ordinary shape of a going-private transaction and its net effect is 1, so the prices correctly do not move. - **A distribution larger than the price.** A liquidating payout can exceed the prior close, and `(P - D) / P` would then be zero or negative. No positive factor exists, so the distribution is written into the row but **not** applied to earlier prices. The event stays visible; the adjustment is the thing that cannot be defined. - **A distribution declared in another currency.** Cross-listed companies - Canadian banks and pipelines, mainly - declare in their home currency while their shares here trade in USD. Column 15 then holds the declared amount and column 16 carries a `CUR:` tag, e.g. `CD+CUR:CAD`. `P` is in USD and `D` is not, so `(P - D) / P` is not a number we can form, and the conversion actually applied to holders is done by the depositary at the pay date, at a rate with a spread, which is not knowable on the ex-date. **A row's factor is the product of every event after it**, so one unknown factor makes every earlier row unknown as well. Those rows are published with **blank** `adj_*` columns. This is deliberate and it is the same commitment as section 3: a number that quietly omitted one term would look entirely ordinary, and you would have no way to notice. Where the adjusted columns are blank, use the raw prices and apply your own FX series. Where an upstream record exists in both the declared currency and USD for the same distribution, we use the USD one and adjust normally; no tag appears. Every factor is re-derivable from the `dividend` and `split` columns inside these files, and `verify.py` does exactly that: it recomputes the adjustment chain from the file's own contents and compares it against the `adj_*` columns. **The arithmetic can be checked here, without trusting us and without network access.** --- ## 5. Trading days, and how to tell a gap from a holiday We do not ship a general trading calendar. It is public reference data available from several maintained libraries, and a data vendor reprinting it adds nothing. What only we can tell you is where **our** data has holes, and that is in the manifest. There are four cases, and the manifest separates them: | Case | A file for that day? | Where it is declared | |---|---|---| | Market closed (weekend, holiday) | No | Nowhere - it was never expected | | Trading day, but we received no data | No | `trading_days_missing` | | Normal | Yes, in `files` | `files` | | Market closed, but a corporate action is dated that day | Yes, **event rows only** | `files` *and* `event_only_days` | So a date with no file and no entry in `trading_days_missing` was a market holiday, and you can stop looking. A date in `trading_days_missing` is a real gap in what we hold, stated plainly rather than smoothed over. **No file is ever written with a header and no rows.** If your loop reads a dated file, it has rows. ### The fourth case, and why we do not quietly fix it An exchange cannot set an ex-date on a day it is closed, so when a source dates one there, something is wrong upstream. It is rare, and the days it happened on are listed in full under `event_only_days` in the `day_by_symbol` manifest - count them there rather than trusting a number printed here, which would go stale the next time the archive is rebuilt. The largest single cluster is 2012-10-29, when Hurricane Sandy closed the market for two days after the ex-dates had already been announced. That is not a data error at all; it is what happened. We considered moving such an event to the neighbouring trading day and measured whether we could tell which one. We could not: reconstructing each issuer's own ex-date and record-date convention decided a handful in each direction and left most of them undecidable. Any single rule would silently move a portion of them the wrong way, so **the date is left exactly as the source recorded it**, and the rows for that day exist so the event still reaches you. They are event rows only - never a price - and the day is listed in `event_only_days` so a trading-day loop can skip it while a merge tool still picks the event up. One consequence, stated plainly: for whichever of these events truly belonged to the *preceding* trading day, that one preceding bar carries one adjustment too many. Correcting them would require guessing which ones, so we do not. Every day involved is named in `event_only_days`, so you can locate the affected bars yourself and exclude them if your work cannot tolerate them. --- ## 6. What is in `manifest.json` Each collection has its own manifest, and it describes **files**, not their contents: ```json { "product": "stock_day_by_date", "files_count": 405, "bytes_total": 490286907, "trading_days_missing": [], "corrections": [ { "symbol": "GBDC", "date": "2024-08-16", "dividend": "", "dividend_type": "", "split": "" } ], "files": [ { "name": "2025-01-02_day.csv", "bytes": 1092870, "sha256": "d99c6c..." }, { "name": "2025-01-08_day.csv", "bytes": 1092709, "sha256": "d9c24a...", "delisted": ["CPTN", "CPTNW"] } ] } ``` Every field passes one test: **can you know it without opening the file?** Row counts and date ranges fail it, and they are not republished anywhere - they are in the files themselves, one pass away. Security type, exchange and the identifiers fail it too but are properties of the security rather than of our data, so they live in `symbols.csv`. What remains here is what you need in order to download and to check: the name, the size, and the digest. - `delisted` appears on a day when one or more securities were delisted as of it. It is the exception that proves the rule - it is *not* in the file, so opening the file will not tell you. `update.py` uses it to seal `{SYMBOL}_day.csv` as `{SYMBOL}_day_delisted_{date}.csv` at the correct point in time, which matters if you fall behind: a symbol reassigned to a new company while you were away must not have both companies' rows merged into one file. - `trading_days_missing` is scoped to the days that manifest actually covers, so the dated manifests speak only for the rolling window. The same field on `day_by_symbol/manifest.json` covers the **whole** period - that is where to look for a gap in the archive. - `corrections` appears on the dated manifests and carries the last **90 days** of corrections to days that have already rolled out of the window. Each entry is the three event columns as we publish them **now** for that security and date - a state, not an instruction, so applying the whole list again changes nothing. An entry whose three columns are empty means the exchange withdrew what it had announced; if that day carried no trading, the row itself goes. `update.py` applies these and recomputes the affected security's adjusted columns. This is the one thing a dated file cannot tell you: it stops being downloadable after about 25 days, so a revision after that would otherwise never reach a library you maintain yourself. Run at least every 90 days and you stay in step; if you have been away longer, re-download the securities you care about rather than trusting the merge. There is deliberately **no version number, no build timestamp and no generator string**. A version field is a bill paid by every reader for a future that may not arrive; the absence of one is itself the marker of this format. To tell two builds apart, compare `sha256`. ### `symbols.csv` One row per **lifecycle**, not per symbol - a reassigned symbol has one row per life. Every column is a property of the **security**. None of them describes what we hold. That is why a single file serves both collections: nothing in it changes with the grain you bought. | Column | Meaning | |---|---| | `symbol` | The symbol as of the end of this lifecycle. | | `symbol_history` | Every symbol this company traded under, `SYMBOL:from_date` joined with `\|`, oldest first, current one included. Example: `FB:2012-05-18\|META:2022-06-09`. Blank means **no symbol change is recorded for this security** - usually because there was none. It is not a guarantee that none ever happened. | | `name`, `type`, `exchange` | Company name, security type (`CS`, `ETF`, `PFD`, ...), listing exchange. | | `status` | `active`, `delisted`, or `renamed`. A `renamed` row is a **previous holder** of the same symbol that left it by renaming rather than delisting (Alcoa Inc handed AA to Alcoa Corp in 2016 this way). Its rows live in the current holder's file - there is no delisting date to name a separate file by - so this row exists to let you look the handover up: the identity here is the old company's, and the takeover date is in the current holder's `symbol_history`. | | `delisted_at` | Delisting date, blank when still listed. | | `cik`, `figi` | Regulatory and instrument identifiers, for joining to other datasets. | **There is no file-name column.** Build the name from this row and the grain you want: delisted_at empty -> {symbol}_{grain}.csv otherwise -> {symbol}_{grain}_delisted_{delisted_at}.csv `grain` is `day` or `minute`. `symbol` plus `delisted_at` identifies one lifecycle, which is exactly what the file name encodes, so the result is exact rather than a guess. Whether that file is published is a separate question, and the manifest of that collection is the authority - the minute collection covers fewer securities than the day collection. **There are no coverage columns either** - no first date, last date, or row count. Those describe our data, not the security, so they would differ between the two collections while this file does not. They are in the files you downloaded: one pass gives you all three, computed from the data rather than taken on trust. --- ## 7. Known limitations Stated plainly, because you should know them before building on the data: 1. **Daily volume and the sum of minute volume do not agree, and cannot be made to.** They are built by two different pipelines. Daily bars follow the end-of-day aggregate guidelines published by the Securities Information Processors; minute bars are assembled from individual trades under the intraday rules that decide which sale conditions may update a bar. Our upstream states plainly that the sum of intraday bars may not match the daily bar and that this is intentional. Measured here on 2026-08-14: adding up **every** minute bar of the day - pre-market, regular session and post-market together - reaches 21.6M for AAPL, 12.2M for MSFT and 5.9M for KO, while the daily bars for the same securities read 28.2M, 16.2M and 9.2M. The shortfall is 23% to 35%, and it is not explained by session coverage: the extended-hours bars are already included in those sums. Practical consequence: use daily volume for daily work and minute volume for intraday work, and do not treat either as a check on the other. Different vendors also resolve these rules differently - two authoritative sources we compared disagreed with each other by 17% on the same day - so a volume that differs from another provider is not by itself evidence of an error. 2. **`volume` becomes fractional on 2026-02-23, and that is a real break in the series.** Until 2026-02-20 a trade with a fractional component had to be reported to the tape as a whole number of shares, so every volume in this dataset before that date is a whole number. FINRA's fractional-share reporting took effect on Monday 2026-02-23, and the consolidated tapes began disseminating fractional quantities to six decimal places; from that day about 85% of securities show a fractional volume on a typical day. We publish what the tape publishes and do not round it away - rounding would hide a real change behind a second, invented one. The difference is always under one share, so it is immaterial to almost any calculation, but if you compare turnover across that boundary you are comparing two definitions. 3. **Securities with no trading inside the covered window are not included.** Absence from this dataset means "no bars in this window", not "never existed". 4. **Adjustment can only reflect the corporate actions recorded in these files.** Two different failures hide behind that sentence, and they are worth separating. The first is us losing an event between our source table and your file; we reconcile the two - ex-date by ex-date, amount by amount - and that is our job, not yours. The second is an event our source never carried at all, and that one cannot be ruled out from this data by anyone: a file and the table it was built from say the same thing, so publishing the table alongside it would prove nothing it does not already prove. It is why `verify.py` reports a dividend-history warning when a security's payments stop or change cadence - a heuristic, offered as one, rather than a proof we cannot give. Pay date, record date, declaration date, currency and frequency are **not published**; they take no part in any adjustment formula, and this dataset carries what a calculation uses. 5. **A reassigned symbol is split across files, and matching them up is your decision.** `ANET_day_delisted_2024-12-03.csv` and a later `ANET_day.csv` may be unrelated companies. `symbols.csv` carries the CIK and FIGI needed to tell, and `symbol_history` covers the other direction - the same company under an earlier symbol. 6. **Early closes are not marked in daily files.** Half-day sessions are ordinary trading days there; nothing in a daily row distinguishes a 1pm close from a 4pm one. Minute files do carry the boundary: on a half session `reg` ends at 12:59 and `post` begins at 13:00 (section 2), and `verify.py` checks it against the exchange's early-close list. 7. **Minute adjustment can drift from daily adjustment, because a minute file cannot carry an event that fell on a day the security did not trade.** A daily file publishes such an event as a row of its own - blank prices, zero volume - so the adjustment picks it up. A minute file has no bar that day and therefore no row to carry it, and its factor chain is built from the rows it holds. Measured across the archive, 10,338 events across 2,355 securities fall on a day their security did not trade. Where it happens the two grains disagree: AADR's daily file records 50 corporate actions and its minute file 47, so on 2010-07-21 the daily factor is 0.871951 and the minute factor 0.875892 - the minute history reads 0.45% high. Each file remains internally consistent, so this shows up only if you compare them. **If the exact adjustment matters, take it from the daily files**; the event columns there are the complete record, and both grains carry the raw columns unchanged. 8. **Securities that distribute in a foreign currency have no adjusted prices before their most recent such distribution.** Section 4 explains why. This affects cross-listed names - Canadian banks, pipelines and miners in particular. The raw columns are complete and unaffected; only `adj_*` is blank, and `dividend_type` tells you exactly which distribution caused it. --- ## 8. Verifying what you received Every collection ships a `manifest.json` listing each file's byte size and SHA-256. It is the check that matters: it proves the bytes you hold are the bytes we published, offline, years from now. `verify.py`, included here, does that check and goes further: it tests every sentence in section 9 against every row and re-derives the adjustment chain from each file's own dividend and split columns, without trusting us and without network access. It needs only Python 3 (3.7 or later, standard library) and reads nothing outside the paths you give it. Run it from the directory that holds `verify.py`, naming the files, a directory (searched recursively; a `manifest.json` found in it covers the files below it), or a zip: python verify.py day_by_symbol/AAPL_day.csv # one file python verify.py --strict day_by_symbol # a directory you downloaded python verify.py --strict --extract . # the free sample, unzipped python verify.py --extract stocks_sample_2022H2.zip On Windows PowerShell use `python` (or `py`); on macOS and Linux `python3` may be the command. Paths are the same on both. Two switches decide how strict the reading is: - `--strict` treats anything that could not be checked as a failure: a column a check needs is absent, no `manifest.json` covers the files, a file is present that the manifest does not list. Use it on files we delivered; leave it off when the folder also holds other data. - `--extract` declares that the files are a slice of a longer history, as the free sample is. Corporate actions after the slice still apply inside it, so I8 (adjusted equals raw after the last corporate action) is reported with the final `close / adj_close` ratio instead of enforced; every other invariant is enforced exactly as on a full file. The report names each section 9 invariant it checked, by number, per file, and counts data files separately from manifest and archive reports. Exit code 0 means every check passed; 1 means at least one failed; **2 means nothing was verified** - no data file of ours was found - which is not a pass. `update.py`, also included, applies the same arithmetic when it maintains your library, and runs `verify.py` over whatever a run wrote, so the check happens whether or not you remember to ask for it. ## 9. What every file guarantees Below is everything we promise about the contents of a delivered file. It is a closed list: `verify.py` carries this table verbatim and checks each sentence on every row of every file, reporting each by its number; nothing here goes unchecked, and its report says which numbers applied to the file at hand (I13 to I15 are about minute files, I4 about daily files). The checksum, calendar and adjustment-chain checks it also performs are reported separately, as "other checks". A property not stated here is not a promise - it may hold today and stop holding tomorrow without that being a defect. Each was measured against the whole published archive before being written down, not inferred from how the files are produced. | # | Applies to | Guarantee | |---|---|---| | I1 | all files | Dates are written YYYY-MM-DD, and minute timestamps YYYY-MM-DD HH:MM:SS, in US Eastern time. | | I2 | per-security files | Rows are ordered by date, ascending, with no repeated date. | | I3 | all files | low <= open <= high and low <= close <= high. | | I4 | day files | Every published price is greater than zero. | | I5 | all files | volume is never negative. | | I6 | all files | A row that carries prices carries a volume. | | I7 | all files | On a row carrying prices, the adjusted columns are either all present or all blank. | | I8 | per-security files | From the most recent corporate action onward, adj_close equals close. | | I9 | per-security files | Rows dated before a distribution paid in a foreign currency carry no adjusted values. | | I10 | all files | split is written from:to, with several on one date joined by +. | | I11 | all files | dividend and dividend_type are either both present or both blank. | | I12 | all files | A published dividend is greater than zero. | | I13 | minute files | session is one of pre, reg or post. | | I14 | minute files | Bars from 09:30 to 15:59 ET carry session reg, and to 12:59 on a half session. | | I15 | minute files | Minute files carry no vwap: both vwap columns are empty in every row. | Three of these read differently from what you might expect, and the difference is the point: **I6 does not have a converse.** A row can carry a volume and no prices. On an ex-dividend date with no trading we publish a row with `volume` 0, the event columns filled and every price blank, because zero trading is something we observed while a price would be something we invented. **I7 is scoped to rows carrying prices.** Those event rows legitimately carry `adj_volume` - zero divided by a known factor is zero - with the four adjusted prices blank, since there is no price to adjust. **I9 says before, not on.** A row is adjusted by the events dated after it, never by its own, so the row on the distribution date itself is unaffected and keeps its adjusted values; the blanks begin with the row before it.