Data

Every number on this site is a public file

The aggregates behind the dashboard are static JSON on a CDN. No key, no account, no quota, no rate limit, and CORS is open to any origin. This page is the format — what the arrays mean, how to index them, and the four things you have to know before you quote a figure from them.

Vendors in this category sell the data and give away the chart. We do the opposite: the numbers are free to take, and what you pay for is the product that reads them — the dashboard, the alerts, the strategy board. It also means nobody has to trust our arithmetic. You can redo it.

The files

PathWhat it isSize
/data/symbols.jsonThe catalogue: 19 markets in 5 groups, with display names and price units.1KB
/data/cube_h1/{id}.jsonThe main aggregate. Month × weekday × hour counts for one market, pooled across the whole history.383KB total
/data/cube_year/{id}.jsonThe same weekday × hour grid, split by calendar year instead of month — for asking whether a bucket was built evenly.309KB total
/data/monthend.jsonMovement at the 4pm London fix on the last trading day of the month, per market, with the Welch t.1KB
/data/strategies.jsonThe free preview of the backtest board (the full board is account-gated).4KB

Cached one hour at the edge. They change at most once a week, when the Saturday refresh adds new bars. Access-Control-Allow-Origin: * on all of them.

The cube format

One file per market. Three flat arrays of 2016 numbers — 12 months × 7 weekdays × 24 hours — plus metadata:

FieldMeaning
pairthe id, e.g. eurusd
namedisplay name, e.g. EUR/USD
unitpips or pts — the unit the P array is in
basisUTC, always. Not broker time, not local time.
spanfirst and last year that hold any bar, e.g. 2003..2026
yearsyears that actually carry data — 24 for this market. Read this, not span: three of our markets span more years than they cover.
N[i]hourly candles in that bucket
UP[i]how many of them closed above their open
P[i]summed signed move, in unit. Divide by N for the average.

The index, with weekday 1 = Monday and hour 0–23 UTC:

i = ((month - 1) * 7 + (weekday - 1)) * 24 + hour

To ask a question of a slice, sum the buckets it covers before dividing — never average the percentages, since the buckets hold different numbers of candles.

A worked example you can check

EUR/USD, Sunday, 21:00 UTC, all months — the reopen of the FX week:

const cube = await (await fetch(
  'https://perfectindicators.com/data/cube_h1/eurusd.json')).json();

let n = 0, up = 0, p = 0;
for (let m = 1; m <= 12; m++) {
  const i = ((m - 1) * 7 + (7 - 1)) * 24 + 21;
  n += cube.N[i]; up += cube.UP[i]; p += cube.P[i];
}

n            // 849 candles
100 * up / n // 60.7% closed up
p / n        // 2.08 pips average move

Four things to know before you quote a number

  1. Compare against the market's own rate, not 50%. EUR/USD closed up 50.07% of all its hours; NAS100 and SPX500 close up 52.3% of theirs. Scoring a bucket against a flat coin flip credits that drift to the clock — it is how three NAS100 windows sat on our own dashboard wearing a significance flag until we caught it. The z we publish is (UP − N·base) / √(N·base·(1−base)), where base is that market's own up-rate. For this bucket: z = 6.17.
  2. A percentage from a finite sample is a range. Put a Wilson interval on it. This bucket's 60.7% is 57.3–63.9% at 95%. We do not publish a bucket below n = 300 or under |z| ≥ 3, and neither should you.
  3. The cube has no year in it. A pooled figure can hide an effect that died in 2015, so the year split lives in /data/cube_year/ — same grid, one dimension swapped, i = ((yearIndex * 7) + (weekday − 1)) * 24 + hour with the calendar years in years[]. The example bucket above holds up in 21 of 23 individual years.
  4. Search enough buckets and noise obliges. There are ~2,200 buckets across all markets that clear our sample minimum; at a 1-in-500 bar, four hits arrive before any market does anything. The screener prints that expected count beside every result, and /evidence runs the whole permutation test on our own grid, including the markets where we come back empty.

Other languages

# curl + jq — average move in the same bucket
curl -s https://perfectindicators.com/data/cube_h1/eurusd.json |
  jq '[range(0;12) as $m | ((($m)*7 + 6) * 24 + 21)] as $ix
      | {n: ([.N[$ix[]]] | add), up: ([.UP[$ix[]]] | add)}'
# python, standard library only
import json, urllib.request
c = json.load(urllib.request.urlopen(
    "https://perfectindicators.com/data/cube_h1/eurusd.json"))
ix = [((m - 1) * 7 + (7 - 1)) * 24 + 21 for m in range(1, 13)]
n  = sum(c["N"][i] for i in ix)
up = sum(c["UP"][i] for i in ix)
print(n, round(100 * up / n, 1))     # 849 60.7

What is not public

The 5-minute cubes and the full backtest board are account-gated — those live in Firestore behind an auth rule, not on this CDN. Note also that the 5-minute data is built on a broker EET clock rather than UTC, which is the sort of detail that silently ruins an analysis, so it is not something we would hand over undocumented.

Use it

Free for any use, including commercial, with attribution: a visible credit to Perfect Indicators and a link to perfectindicators.com. No key to ask for and no quota to hit — but these are files on a CDN, so please cache them rather than refetching per page view; they change once a week at most.

Two requests, both about honesty rather than licensing: keep the sample size attached to any figure you republish, and do not present these as forecasts. They are historical frequencies. If you build something with them, we would like to see it — get in touch.

Generated from the shipped files on 2026-08-15. The example numbers above are computed at build time from the same JSON you will download, so this page cannot drift from the data.

See what we built on it →