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.
The files
| 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. | 391KB 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. | 314KB total |
| /data/cube_slot1m/{id}.json | The 1-minute grid. For each of the 1,440 minutes of the UTC day, Monday to Friday pooled: how often this pair closed up, down, or exactly on its open, the 95% interval on the up share, how far it typically moved against its own median minute, and the mean ask-bid spread. Built from 220 million minute bars out of Dukascopy's requester-pays archive. 28 FX pairs, of which this site's own catalogue holds 10 — the other 18 are crosses with no evidence grade and no hourly cube here, and every file says which it is. ⚠ Each file carries an axes block holding the measured per-pair verdict: the direction figures are history and predict nothing — minutes chosen for leaning on the first half of the archive beat the minutes nobody chose by −0.01 points on the second, in all 28 — while the movement timetable survives that test in all 28 and the tie rate in 12. Pooled across weekdays on purpose: one weekday × one minute is ±2.8 points, wider than the gap between a coin and a binary option's breakeven. Built by hand from the minute archive, which is not refreshed weekly. | 8014KB total |
| /data/cube_slot5m/{id}.json | The 5-minute slot grid for the four majors we hold minute data on: for each of the 288 slots of the UTC day, how many bars closed up, down or flat, pooled across every weekday, with the 95% interval on the up share and the pair's own base rate to read it against. Pooled on purpose: one weekday × one slot holds about 400 bars in this archive and its interval is ±5 points — wider than any lean the grid contains — so that split is not published. Each slot carries a thin flag for 20:00–02:25 UTC, where both New York closes (20:55 in summer, 21:55 in winter), the rollover and the Tokyo open sit, and where a bid-only feed records settlement spread rather than direction. holdout chooses slots on the first half of the archive and scores them on the second, for pooled slots and for the withheld weekday split, with the unchosen cells as the control; holdout_reading is the sentence those numbers choose. Carries its own coverage share, because the archive holds about two weekdays in three (see /data/coverage5m.json). Built by hand from the 5-minute archive, which is not refreshed weekly; the build gate recomputes it from the bars. | 157KB total |
| /data/cube_doy/{id}.json | Every calendar year's path through the year: 365 slots of cumulative percent return, close-to-close, 29 February dropped. firstSlot/lastSlot mark where each year's real data starts and stops. | 691KB total |
| /data/correlation.json | How closely each pair of markets moves hour to hour: Pearson correlation of hourly returns on the hours both traded, plus the same figure for the first and second halves of the shared history so a drifting relationship can be seen. | 26KB |
| /data/markets.json | Per market: how many weekday × hour windows clear our bar against how many a permuted grid produces, the sample floor reached, years covered, and the smallest lean we could detect there. Twelve markets demonstrate structure; six do not; one cannot be tested at all. | 3KB |
| /data/oosleak.json | What the strategy board’s out-of-sample column is actually worth. The board filters candidates on the whole history and then scores its last 30%, so a rule arrives at that column having been chosen with its help. Selecting on the first 70% alone and scoring the last 30% is the honest version, and this file holds both. | 1KB |
| /data/hold.json | What taking every window we flag would actually have returned: entered in the direction it leans, held for 1 to 24 hours, paying the round trip once, in multiples of each market’s own spread. In-sample by construction, and negative at every horizon on both the median and the mean. | 6KB |
| /data/regime.json | Whether a flagged window works differently depending on how big the previous hour was, measured against that hour’s own normal so the split is not the trading session in disguise. It does — in the opposite direction to the intuition, a flag is LESS reliable after a big hour — but almost all of it sits in the thin band and neither state clears its own round trip, so it is not a filter. | 2KB |
| /data/firsthour.json | Whether the first hour after the open says anything about the rest of the session — opening-candle continuation and the initial balance, two more reports this category sells by name. Continuation is scored against a permutation that re-pairs each first hour with a DIFFERENT day's session, so both marginals survive and only the link is destroyed; it finds nothing in any market we hold. The open is resolved through each market's own clock per day, and crypto is excluded because it has none. | 7KB |
| /data/spread.json | What the bid-ask spread ACTUALLY cost, hour by hour and year by year, for the ten FX pairs our catalogue shares with the minute archive. Every cost figure elsewhere on this site divides by one assumed round trip per market applied flat to 23 years, and sections 9 and 12 said the real spread could not be recovered because our hourly feed is bid-only. Dukascopy's S3 archive ships BID and ASK minute candles and we kept both, so it can be. Carries the full 168-bucket weekday-by-hour grid of median entry and exit spreads (a long pays the entry, a short the exit), the same grid for 2020 onward, per-year medians, and every board rule re-costed at the spread its own window carried against two controls. Metals, indices and crypto have no minute archive here and keep the assumed figure. Not rebuilt weekly: the minute archive is not refreshed weekly either, and the build gate recomputes the finding from this file so it cannot go quietly out of date. | 82KB |
| /data/crossnull.json | How much of section 1's headline survives the fact that our markets move together. The permutation behind that number is drawn one market at a time and the totals are a SUM across nineteen of them, which is only a fair experiment if they are independent — and /data/correlation.json says they are not. Two arms differing in exactly one thing: a permutation of instant-to-clock-label shared by every market, versus one drawn per market. varianceInflation is the ratio of the two arms' variances with its bootstrapped interval, and effectiveMarkets is what that ratio makes nineteen markets worth as independent ones — read both from the file, not from this sentence. Not rebuilt weekly: it measures something structural and costs about ten minutes, and the build gate reconciles its observed count against the live grid so it cannot go quietly out of date. | 2KB |
| /data/coverage5m.json | How much of the 5-minute archive we actually hold. Section 7 of /evidence (the opening range) and the Pro 5-minute card are measured on four majors across a span labelled 2015 to 2026, and a span is not a coverage: the feed serves minute data one DAY per file, so a refused request loses a whole day, and this file counts the weekdays we hold against the weekdays in the span. The cause is our own download, which was built on the dukascopy-node defaults under which an HTTP 429 returns an empty answer and no error. Each missing day carries a control against the HOURLY archive — a different download of the same ticks — and the same share is given for the days we do hold, so the two are read against each other rather than against 100%. Also per year, because the losses are not spread evenly: the early years are much thinner than the recent ones. Not rebuilt weekly (the archive is not refreshed weekly either); the build gate recomputes it from the archive by a different reader, so it cannot go quietly stale. | 4KB |
| /data/round.json | Whether round numbers act as support and resistance. The control is the SAME price lattice, spacing unchanged, phase-shifted onto the 80 whole-pip offsets that are not multiples of five, so the geometry survives and only roundness dies. The folklore turns out to have the sign backwards — an hour that trades through the big figure finishes PAST it more often than one crossing an ordinary level the same distance from the same open — and the effect grades with how round the level is, from the big figure down to nothing at every ten pips. It clears a round trip in no market we hold, which is why nothing on this site is built on it. | 16KB |
| /data/flagsize.json | What a window this site flags actually READS. One row per market plus the pooled distribution of the raw up-rate folded to the side it leans, for every weekday x hour window clearing our published bar (n ≥ 300, |z| ≥ 3 against that market's own up-rate). Carries the count inside, below and above the 55–65% range /about advertises, and the split between the thin band (the Sunday reopen, or 20:00 UTC onward) and ordinary trading hours. The control is the whole point and it is in the file: `floor` is the smallest lean each bucket's own sample size could have flagged at all, and `excess` is the reading net of it, because a small bucket cannot be flagged unless it leans hard. | 4KB |
| /data/convention.json | Whether the site's own definition of an UP hour is doing the work. Every number here counts an hour as up when its close beats its OWN OPEN; this file swaps that for the equally defensible alternative — a close above the PREVIOUS HOUR'S CLOSE — on exactly the same bars, and counts how many flagged windows survive. The control keeps each market's own inter-bar gaps and deals them onto different bars, so the same amount of tick-sized noise is injected and only WHERE IN THE WEEK it lands is destroyed; a second control permutes within deciles of bar size, so a big gap still travels with a big bar. Carries the per-market survival, both controls, the mean signed gap by UTC hour in multiples of each market's own round trip, and the share of each market's price path that falls between bars and is therefore in no bucket at all. | 9KB |
| /data/asianrange.json | Whether London breaking the Asian session range tells you anything — the most-taught day-trading setup in retail FX. The Asian range is 00:00–06:00 UTC (Tokyo 09:00–15:00 every day of the year, since Japan keeps no daylight saving); the London window is resolved through London's own clock per day. Scored against a control that keeps this day's London session bar for bar and hands it a DIFFERENT day's range, drawn from within 60 trading days so the volatility regime survives and only the link dies. The range is broken on almost every session, and given a break the borrowed level is closed beyond as often as the real one. Carries a narrowness ladder with its own control, per-market rows, costs in each market's own round trips, and the smallest gap the test could have resolved. | 16KB |
| /data/pivot.json | Do the floor-trader pivot points do anything? The classic set (PP, R1/S1, R2/S2, R3/S3) from the previous session high, low and close, on a day cut at 22:00 UTC, with a midnight arm beside it. Given a level was reached, did the day finish beyond it — which answers BOTH halves of the folklore at once, since ‘the level holds’ and ‘the level breaks and runs’ are complements of one statistic. Three controls, all in the file. (1) A BORROWED day’s set laid on today’s open, in two arms: a pivot set encodes where the levels sit AND how wide they are, and yesterday’s range predicts today’s, so the naive arm credits pivots with volatility clustering and the second arm requires the donor day’s own previous range to match yesterday’s. (2) The same set PHASE-SHIFTED off the pivots by a fraction of a range, keeping spacing, scale and sides and destroying only the identity. (3) The plainest baseline: how often the day simply closes past its own open. A minimum distance of one round trip applies to every arm. Also carries the selection rate per level class, the folklore’s own importance ladder, and the cost in multiples of each market’s own round trip. | 11213KB |
| /data/weekday.json | Which half of our own weekday x hour grid is doing the work. Every published number here is a weekday x hour bucket and nobody ever argued for the weekday half of it: splitting each hour five ways multiplies the search space by five. Each publishable cell (n ≥ 300) is scored TWICE and symmetrically — against the REST of its own hour pooled across weekdays (does the weekday add anything the hour did not already say?) and against the REST of its own weekday pooled across hours — so neither axis gets the friendlier instrument. The comparison is LEAVE-ONE-OUT, a two-sample proportion test against an independent sample: scoring a cell against a pool it is inside deflates z by sqrt(1 - n/n_group), about 11% in the weekday arm and 2% in the hour arm, and that bias sits on the arm the conclusion wants to find weak. Carries both counts against a seeded simulated null of the same shape, the split of each between the thin band (the Sunday reopen, or 20:00 UTC onward) and ordinary trading hours, every ordinary-hour weekday-specific window by name, how many of the windows the site publishes are distinguishable from their own hour, and the same finding with no test in it at all: the median published window's distance from its market average against its distance from its own hour on other weekdays. | 8KB |
| /data/adr.json | Once a market has covered its average daily range (the 14-day mean of high minus low), is the day done? For every day that reaches k times its ADR, k from 0.6 to 1.4: did it go a further quarter-ADR, and did it close back through the k-ADR level? The control is the LADDER: a heavy-tailed range makes going further less likely at every multiple, so the claim that the ADR is a ceiling is tested as whether 1.0x sits below its neighbours, with every rung reweighted to the hours at which days reach 1.0x. Interval resampled by calendar year across all markets at once. Day cut at 22:00 UTC with a midnight arm beside it; crypto excluded. | 4KB |
| /data/rsi.json | Whether an oversold reading picks a better moment than the fall that produced it already does — RSI(14) below 30 and above 70, the numbers every charting platform ships as its default and the first OSCILLATOR tested here. RSI is a CONJUNCTION and only one term of it is the indicator: "below 30" is, by construction, "price has been falling for a fortnight", so the headline control is every day matched on the SAME TRAILING RETURN that was never called oversold — same size of fall, no oscillator verdict. Carries that comparison as a mean move and as an up-rate with an interval resampled by CALENDAR YEAR (an oversold spell throws several crossings whose forward windows overlap, so signals are not independent tries), a pooled random-date null over all markets at once, a ladder over every (period, threshold) cell on a grid scored BOTH raw and matched, the crossing against the ordinary days of its own oversold state, and the plainest baseline of all: price simply below its own close 14 days ago, with no oscillator anywhere in it. Costs are in multiples of each market's own round trip, and the smallest edge the test could have seen is in the file. RSI divergence is not tested and the page says why. | 17KB |
| /data/ma.json | Whether the moving average picks a better moment than a coin does — the golden cross and the 200-day trend filter, the fourth object retail trading teaches most after the round number, the candlestick and the Fibonacci ratio. Two claims tested apart: the CROSS (an event) and the FILTER (a state). Every arm is scored against that market's own unconditional forward move over exactly the same pool of days, because this folklore was born on US equities where being long at all pays; a second control deals the same events onto randomly chosen days 400 times, seeded. Carries a ladder over every (fast, slow) pair on a grid — the claim is that FIFTY AND TWO HUNDRED are the numbers, and the file says where that pair ranks inside its own family — the days after a cross against the ordinary days of the same state, the same filter at every trailing length, and the plainest baseline of all: price simply above its own close 200 days ago, with no average anywhere in it. Costs are in multiples of each market's own round trip, and the smallest edge the test could have seen is in the file. | 16KB |
| /data/fib.json | Whether Fibonacci retracement levels do anything, against the cleanest control this axis allows: the SAME legs, the same bars and the same forward window, measured at fractions nobody draws (41.5%, 57.0%, 64.5%). Every property of the geometry survives the control and only the ratio's fame dies. Two arms — how far past a level price closes, and where retracements actually stop in equal one-point bins — plus a horizon ladder, an alternative swing definition, and a placebo spread that states the smallest effect the test could have seen. Not one famous depth clears its own placebo. | 8KB |
| /data/hilo.json | Which hour of the day holds the day's high and low, and which weekday holds the week's — against a control that keeps every bar in its own hour and randomises only its direction. That control is the file's reason to exist: compared against a flat 1-in-24 the opening hour looks decisive, and it is the arcsine law plus the fact that London and New York are simply bigger hours. Includes a second arm on a day that starts at 22:00 UTC, which moves the answer in every market. | 104KB |
| /data/version.json | What version of the site is live, when it was built, and from which commit. The version is the number of published changelog entries, so it cannot move without the change being described on /changelog. The browser extension reads this to tell a side-loaded copy that a newer build exists — it has no store listing and no other update channel. | 1KB |
| /data/crypto.json | The crypto weekend, measured before range.json was widened to cover it: BTC and ETH trade all seven days, and until 2026-09-02 the movement file was 120 numbers long and stopped at Friday. Weekend size against the weekday half, the 24-hour movement profile of each half with the control that makes the comparison mean anything, weekend direction against the published bar, an era split, and whether the weekend sets up Monday (it does not, once the control is shape-matched). | 4KB |
| /data/independence.json | How much each flagged window rests on weeks that resemble one another. Our z assumes every occurrence is an independent coin; correcting for serial dependence inflates the error bar by about 4% on the median window, which is the same as saying our |z| >= 3 bar is really nearer 3.06. Per-window inflation for the flagged set, plus how many of the casualties were already marked on the line. | 8KB |
| /data/axes.json | Whether a market leans hardest in the hours it moves most. Spearman correlation of |z| against the hour’s median move, per market — the answer is that they do not line up anywhere, and it includes the negative result that size does not separate the flags that pay from the ones that do not. | 2KB |
| /data/cluster.json | Volatility clustering on the hour-of-week grid, with each hour divided by the median of its own weekday × hour bucket first so the London session cannot masquerade as memory. Includes the decay curve: the lift is still there a day later, which makes it a regime rather than a reaction. | 6KB |
| /data/suspect.json | For each market with years we do not trust: how many of its significant windows survive removing them, against a control that removes every other combination of the same number of years. Losing flags is what dropping years does — the control is what says whether these particular years cost more than any others would. | 1KB |
| /data/quality.json | Which calendar years of which feed we would not quote: mean absolute hourly move per year against that market's own median year (one market is flagged today). And, under holes, the whole calendar months our own archive is missing — 812 across 16 markets today, almost all a download fault of ours, not yet repaired. | 31KB |
| /data/alertrecord.json | The forward record of the alerts: every occurrence of an alertable window since the day Radar alerts went live, scored on raw hourly bars — with its confidence interval, a second one resampled by window (the unit the windows were chosen at), and the board’s own expectation to read both against. | 3KB |
| /data/uptime.json | Availability of this site, probed from our own server every ten minutes with content checks rather than status codes: 30-day availability, response percentiles, TLS expiry and any incidents. A record published at each deploy, not a live health check. | 2KB |
| /data/candle.json | Whether a candlestick pattern adds anything the two-day sequence it is made of does not — the engulfing candle and the pin bar, at the daily unit, each at three strengths. A pattern is a conjunction and only one term is the pattern, so the control is every day with the same two-day sequence and the same normalised range but WITHOUT the named geometry, and the base rate is each market's own rather than 50%. Includes the plainest baseline of all (follow whatever today did, on every day in the archive), which is the level both patterns sit at. The day is cut at 22:00 UTC; the midnight arm ships beside it. | 7KB |
| /data/coil.json | Whether a narrow-range day precedes an expansion — the coil, the squeeze, Crabel's NR4 and NR7. Every range is divided by the median of that market's previous 60 trading days, so a regime is never mistaken for a signal. Three controls: selecting a minimum guarantees regression, so the next day is compared to an ORDINARY day rather than to the narrow one; an NR day is also just a small day, so it is matched against non-NR days of the same normalised size; and because being the narrowest of seven also means the previous six were bigger, it is matched again on the neighbourhood. The expansion the setup is sold on turns out to be the regression, the tightest coils are followed by the smallest days, and the breakout is indistinguishable from breaking any level the same distance from the same open. | 18KB |
| /data/seasonal.json | Walk-forward test of that overlay: the mean of every year before Y forecasting each month of Y, scored on sign and correlation, plus the spread of years around their own average. | 11KB |
| /data/nfp.json | Movement in the 08:30 New York hour on the first Friday of the month (US non-farm payrolls) against the same hour on every other Friday, per market. Bigger in 18 of 18 markets; direction silent in all 18. | 2KB |
| /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/neighbour.json | How often a significant window stands alone — neither adjacent hour leaning the same way — on this grid and on a permuted grid with no hour-of-week structure. | 2KB |
| /data/range.json | How far an hour moves: median |close−open| per weekday × hour with quartiles, the 90th percentile and the median high−low range, plus the same pooled by hour. | 107KB |
| /data/drift.json | The average day added up: mean hourly move accumulated across each weekday, per market, with the standard error of the running total and every calendar year’s own whole-day total. | 70KB |
| /data/strategies.json | The free preview of the backtest board (the full board is account-gated). | 5KB |
| /data/sessions.json | Where the trading sessions actually are: membership resolved through each city’s own daylight-saving calendar for every one of the 5,952 trading days, so an hour belongs to a session on a fraction of days rather than always or never. Five of the 24 UTC hours are split. | 9KB |
| /data/holdout.json | The out-of-sample table. Windows chosen using only candles before the split year by the published bar, then scored using only candles after it — as a 2×2, because the cell that matters most is the control: unselected ordinary-hour buckets, which must read zero. | 6KB |
| /data/events.json | The named hours — the New York close, the London 4pm fix, the two opens — resolved through each city’s clock per day, so each event lands on more than one UTC hour with the share of days it falls in each. | 13KB |
| /data/revisions.json | What changed between two data vintages: which windows crossed our significance bar in either direction, and how many of the ones we publish sit within a hair of it. | 3KB |
| /data/cost.json | Cost sensitivity: the break-even spread of every alertable rule (its gross edge) and the cushion over the spread we assume, plus what share of liquid buckets move more than a round trip at all. | 4KB |
| /data/sizeoos.json | The same holdout for the size axis: rank correlation between the pre-split timetable of big hours and the post-split one, with the busy-band control that stops it being “London is busy” restated. | 4KB |
| /data/seasonsize.json | Whether that timetable is the same all year: the shape (which hours are biggest) and the level (how big they all are) per quarter, against each market’s own all-year ranking. | 7KB |
| /data/status.json | What /status reads: per-market freshness, the outcome of the last refresh, and every derived file with the script that writes it and the day it was measured. | 7KB |
What we promise about these files
They are free and open, which is easy to say and worth being precise about. Three commitments, each one small enough that we can keep it:
| We do promise | We do not |
|---|---|
| Fields do not disappear. The shape of every file on this page is recorded in the repository and the build refuses to ship a version that has lost a field. If one ever has to go, it appears in the changelog as a correction first — a reader of raw JSON has no other way to find out. | Values are not stable. They are re-measured every Saturday and any number here can move, including ones you have quoted. That is the point of a weekly refresh. |
| The URLs stay put. Same paths, same host, Access-Control-Allow-Origin: *. | New fields will appear without warning. Read the ones you need by name and ignore the rest; that is the only way to be safe against a file that is still growing. |
| Every file we serve is described on this page. The table above is generated from the directory, so publishing a file without describing it stops the build. | No rate limit, no key — and no uptime guarantee either. What availability has actually been is published at /status rather than promised here. |
The cube format
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. |
| lastBar | the newest real bar in the file, ISO — the freshness of the aggregate: 2026-09-25T20:00. A stale cube renders perfectly, so read this before quoting anything as current. |
| built | the date this file was written — 2026-09-26 |
| 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 // 856 candles
100 * up / n // 60.6% closed up
p / n // 2.06 pips average move
Four things to know before you quote a number
- Compare against the market's own rate, not 50%. EUR/USD closed up 50.05% 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.19.
- A percentage from a finite sample is a range. Put a Wilson interval on it. This bucket's 60.6% is 57.3–63.8% at 95%. We do not publish a bucket below n = 300 or under |z| ≥ 3, and neither should you.
- 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 22 of 24 individual years.
- 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)) # 856 60.6
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.
How to cite it
These files change every week, so a figure taken from them is only reproducible if the citation says which week. Dataset-citation practice is unambiguous about that: version, or it cannot be checked. Ours is the date of the newest bar, which every cube carries in its lastBar field and every generated page on this site prints in its footer.
Perfect Indicators (2026). Hourly seasonality aggregates for 19 FX, metals, index and crypto markets [Data set]. Version: bars through 2026-09-25. https://perfectindicators.com/api
Use it
Free for any use, including commercial, under CC BY 4.0, with attribution: a visible credit to Perfect Indicators and a link to perfectindicators.com. ⚠ That licence covers our aggregates, not the prices underneath. The counts, medians and test results on this site are ours to give away; the hourly bars they are built from are Dukascopy’s (dukascopy.com) and are not ours to relicense. Every file says so in its own source and license fields, because a file that travels without its terms is a file whose terms do not travel. 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.