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.
| Path | What it is | Size |
|---|---|---|
| /data/symbols.json | The catalogue: 19 markets in 5 groups, with display names and price units. | 1KB |
| /data/cube_h1/{id}.json | The main aggregate. Month × weekday × hour counts for one market, pooled across the whole history. | 383KB total |
| /data/cube_year/{id}.json | The same weekday × hour grid, split by calendar year instead of month — for asking whether a bucket was built evenly. | 309KB total |
| /data/monthend.json | Movement at the 4pm London fix on the last trading day of the month, per market, with the Welch t. | 1KB |
| /data/strategies.json | The free preview of the backtest board (the full board is account-gated). | 4KB |
One file per market. Three flat arrays of 2016 numbers — 12 months × 7 weekdays × 24 hours — plus metadata:
| Field | Meaning |
|---|---|
| pair | the id, e.g. eurusd |
| name | display name, e.g. EUR/USD |
| unit | pips or pts — the unit the P array is in |
| basis | UTC, always. Not broker time, not local time. |
| span | first and last year that hold any bar, e.g. 2003..2026 |
| years | years 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.
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
# 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
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.
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.