Methodology.

Amanoki is a computational tool for wholesale electricity markets — JEPX (Japan) live, ERCOT / PJM / CAISO / AEMO / NYISO catalogued and on the roadmap. It reads the price a market publishes for each of its settlement intervals and reduces that history to two kinds of number: a label for what state the market is in — low, normal, high, or scarcity — and probabilities attached to that label, both backward-looking (how often price has crossed a given threshold from a moment like this one) and forward-looking (how likely each label is 60, 120, or 240 minutes out).

This page is the exact rule behind each of those numbers, in the order the computation runs: what one interval is, how volatility is measured against its own recent history, how the four-way label is decided and kept from flickering, how the spike table is counted, and how the forward model is trained and scored. It then covers the Japanese fuel-cost adjustment (燃料費調整), which is a separate calculation on separate inputs and has its own section below, followed by the error surface — that part is for people writing client code — and the known limits.

One piece of vocabulary first, because everything after it rests on the word. A bar is one settlement interval: the smallest slice of time the market itself puts a price on. On JEPX that is 30 minutes, so a day is 48 bars. And volatility here means how far the price has been moving from one bar to the next — so a bar labelled high is one where the price is moving more than it usually does, which is not the same thing as the price being high.

Bar

Each market's native clearing interval is the atomic bar: 30 min for JEPX, 5 min for ERCOT / PJM-RT / CAISO / AEMO / NYISO (PJM-RT is PJM's real-time market, as distinct from its day-ahead one). Rolling windows are expressed in bars and scale with bar size, so the short window is ~1 day and the baseline is ~1 week whatever the bar length — that is a property of the rule, which currently runs on JEPX only; the other five adapters are catalogued and unwritten. Prices enter the computation at the interval the market published them at, and every figure below is arithmetic on that sequence.

Regime FSM

The label is produced by a finite state machine — a rule that holds one of four states and moves between them only when its entry condition has held for several bars in a row. Three of the four states are decided by the realized-volatility z-score, which is how far the current volatility sits from its own recent baseline, measured in standard deviations; the next section gives the formula. The fourth is decided by the price level alone.

Four states:

Hysteresis: entering a state requires enter_k consecutive bars; exiting requires exit_k. Defaults are 2 in / 4 out. Scarcity exits only when price drops below 0.8× threshold to avoid flapping at the commercial boundary.

z-score

In words: measure how much the price has been moving over roughly the last day, compare that against how much it has typically moved over roughly the last week, and express the gap in standard deviations of that weekly baseline. A z of 0 means the market is moving exactly as much as it recently has; +2 means today's movement is two standard deviations above its own recent norm. The comparison is always against the same series' own history, never against another market, which is what lets one threshold apply to areas with very different price levels.

Short-window realized volatility, annualised: σ_short = std(log-returns, window) × √(bars/year). Baseline is the mean and standard deviation of σ_short over a longer window. z = (σ_short − μ_baseline) / σ_baseline. This is the crypto volatility z-score applied unchanged to electricity prices.

Spike probability

The question this answers is: starting from a moment like this one, how often has price gone on to cross a given level within the next couple of hours? The answer is a frequency counted off past bars, and "a moment like this one" means the same hour of the day with the same weekday-or-weekend flag — a weekday 18:00 bar is scored against other weekday 18:00 bars.

For every historical bar, compute the forward maximum price over the next horizon_min minutes. Bucket those maxima by (hour-of-day, is-weekend) in the market's local timezone. Each cell reports the share of bars where the maximum crossed a given threshold. That's the empirical conditional probability:

P(max price over next H min ≥ threshold | hour-of-day, is-weekend)

The response carries bucket_size so callers can discount cells with thin sample support. If a market has no scarcity events in the loaded history, the corresponding cell is 0; base_rate carries the unconditional frequency for the same threshold.

Regime forecast

The spike table looks backward. This looks forward: given the current state, the hour, the area, and the weather, how likely is each of the four labels one, two, or four hours from now? The answer comes out as four probabilities that sum to one, and the thing being scored is whether those probabilities are honest — if the model says 30% and the outcome happens 30% of the time, it has done its job — rather than whether the single most likely label was right. That is what the Brier score below measures, on data the model was never fitted to.

The live forecast endpoint (/v1/power/{m}/{a}/regime-forecast) ships a multinomial logistic regression trained on ~284k labelled bars with a chronological 80/20 split and a 24-bar (12 h) embargo between train tail and test head to suppress autocorrelation leakage. Two variants are trained side-by-side on the identical split and the per-horizon winner serves:

The endpoint returns a probability distribution over the four states at t + horizon, so the scoring target is calibration (Brier), not direction accuracy. Current test-Brier (lower = better; unconditional baseline ≈ 0.43):

Horizonv1v1.5 (active)Ratio
60 min0.14060.01678.4×
120 min0.13480.03214.2×
240 min0.14460.07541.9×

Reading the table: a Brier score is the mean squared distance between the four predicted probabilities and the outcome that actually occurred, so 0 is a perfect forecast and larger is worse. The baseline is what you would score by ignoring every input and always predicting the long-run frequency of each label; on this data that is ≈ 0.43. The ratio column is baseline ÷ model on the same held-out months. It is a comparison against that fixed-frequency baseline specifically, and not against the much harder baseline of predicting that the next hour looks like this one — which, as the next paragraph says, is where most of the score comes from.

The t-1 regime one-hot is the dominant feature — persistence does most of the work over short horizons. Weather adds incremental signal, particularly at 240 min where the persistence term has decayed further. We publish both variants' scores so the weight of persistence vs weather is visible; future ablations will separate them further.

Current weather is fetched from Open-Meteo at process boot, cached in memory, and refreshed hourly by an in-process loop. Model weights are exported as JSON and evaluated with numpy at serve time.

Fuel-cost adjustment (燃料費調整)

Everything above is about wholesale spot prices. This section is about a different number entirely, computed from different inputs, and readers who arrived here from the fuel-adjustment page want only this part.

Every Japanese electricity tariff passes fuel costs through to the bill by a formula written into the supplier's 約款 (its published supply agreement). The formula tracks the price Japan paid to import crude oil, LNG, and steam coal, as recorded by 財務省貿易統計 (the Ministry of Finance customs statistics). Because that formula reads customs months that have already been published — three to five months back on the standard 省令 window, two months back on some 約款 — the unit rate for a given billing month stops being an estimate and becomes arithmetic the moment the last customs month in its own window is published. What follows is the parameter set, the calculation, the rule for when a month is settled, and how all of it is checked against what the utilities actually published.

The parameters

The shape of the formula is identical across suppliers; the numbers in it are not, and nobody publishes them in one machine-readable place. Amanoki keeps a registry of them, served at GET /v1/nencho/tariffs. Each entry carries α, β and γ (the fuel coefficients), 基準燃料価格 (the base fuel price the adjustment is measured from), 基準単価 (the ¥/kWh the rate moves per unit of fuel-price movement), the customs months the window covers, the cap multiplier where a regulated tariff has one, and a provenance record naming the document each number came from.

Entries are keyed on (supplier, 約款, voltage) rather than on (area, voltage), because any (area, voltage) scheme is wrong for at least one major supplier: 九州電力 高圧 and 特別高圧 share α/β/γ and 基準燃料価格 and differ only in 基準単価; 北陸電力 shares α/β/γ across all three voltages; and 東京電力エナジーパートナー runs two 燃調 regimes at once, for contracts signed either side of 2026-04-01, on different windows.

The registry holds 13 tariffs across six suppliers today, of which 9 carry a full parameter set. An entry with a hole in it is listed with evaluable: false and the engine refuses to produce a number for it — several suppliers publish their coefficients only as images inside a PDF, and a known gap is more useful than a confident wrong figure.

The calorific identity

資源エネルギー庁 defines α, β and γ as 換算係数 — a heat share divided by that fuel's calorific value relative to crude oil. That definition implies a check, because heat shares have to add up to one:

α + (H_LNG / H_crude)·β + (H_coal / H_crude)·γ ≈ 1

Using the 総合エネルギー統計 標準発熱量 (crude 38.2 GJ/kl, LNG 54.6 GJ/t, steam coal 25.7 GJ/t), every published set in the registry lands between 0.989 and 0.995. The offset from 1.000 is systematic — suppliers assume their own cargo calorific values — but the spread is tight, so the identity works as a transcription check: any set falling outside 0.96–1.04 is treated as a data-entry error rather than a genuine tariff. That gate matters precisely because so many of these coefficients have to be read off an image by eye.

Inverting the same identity gives a reading nobody publishes: each supplier's own assumed thermal mix, by heat rather than by volume. 関西電力 高圧 is 71% coal; 東京電力EP 低圧規制 is 55% LNG; 北海道電力 still carries 19% oil.

The calculation

Two steps, each with its own rounding.

平均燃料価格 = round100( α·A + β·B + γ·C )
燃料費調整単価 = round0.01( (平均燃料価格 − 基準燃料価格) × 基準単価 ÷ 1,000 ) − 政府支援

A, B and C are the window's crude (円/kl), LNG (円/t) and steam-coal (円/t) import prices. Where the window spans several months they are quantity-weighted — ΣValue ÷ ΣQuantity over the whole window, not the mean of the three monthly unit prices. Checked against 東京電力EP's own published 貿易統計価格 over four windows, quantity-weighting won 11 of 12 fuel-months and cut the residual from as much as 140円 down to ±3円; 140円 on the LNG leg is enough to move 平均燃料価格 across a 100円 rounding boundary and change the published rate.

Four details decide whether the output matches to the last 銭:

Window handling lives with the parameters rather than with the formula, because it differs per 約款: the 省令 default is a trailing three-month mean at m−5..m−3, but 東京電力EP 高圧 moved to a single month at m−2 in 2026-04, and 中部電力ミライズ to a single month on a different offset.

When the rate is settled

A billing month's 単価 is settled once every 貿易統計 month in that month's window has been published. Until then it is not, and a month whose window is still open is reported as such rather than estimated.

That boundary is set by one published lag: 貿易統計 prints the 速報 for a reference month 17 to 23 days after that month ends. On the 省令 default window it is the m−3 速報 that settles the rate, so across the 23 billing months the committed archive settles this way, the rate is fixed 37 to 46 days before the billing month begins — about six weeks — and 5 to 11 days before the supplier announces it, taking the announcement as the last week of m−2. Measured instead against the end-of-m−2 vintage cutoff the engine applies when replicating, that second gap is 8 to 14 days.

Those are two different gaps, and neither figure means anything without the convention it was measured against. Both also move with the window: 東京電力EP 高圧, on its single month at m−2, settles 9 to 15 days before its billing month. The date the last window input first appeared is returned as determined_on, and it is a fact about the publication calendar rather than a projection. GET /v1/nencho/schedule classifies consecutive billing months this way.

One consequence for reproducing history: an announced 単価 is final, and a later customs revision does not reopen a bill. So replaying a past month has to use the vintage that had printed by the supplier's announcement date, not today's freshest data. The archive keeps every publication vintage as an additional row — 速報, 確速 and 確報 each with their own 公表日 — which is what makes that possible; GET /v1/nencho/fuel-prices serves them.

The verification procedure

The claim is that the engine reproduces the figure the utility published, exactly, to the last 銭. That claim is only worth something if it is tested against the published figure rather than against our own arithmetic, so the test suite is built from press releases and 単価一覧 pages:

The basis against tradable instruments

The formula references import unit prices that nobody can trade. Anyone wanting to stand in front of that exposure has to use Brent, Henry Hub, Newcastle coal and USD/JPY instead, and the gap between the two is the basis. Amanoki fits and publishes that gap; the numbers live in data/nencho/calibration.json and scripts/calibrate_nencho.py produces them.

The model is one regression per fuel leg, on log levels, with an autoregressive model on the residual:

log CIF_i(t) = a_i + Σ_j b_ij · log( P_ij(t − L_ij) · FX(t − L_ij) ) + ε_i(t)

The lags are fitted, not assumed. Japanese CIF prints late by construction — a cargo prices weeks to months before customs records it on arrival, and term formulas add their own averaging — and the received account (LNG 3–6 months behind JCC, coal 1–3 behind Newcastle, crude 1–2 behind Dubai/Oman) predicts a lag without pinning it. Selected out-of-sample on the 2015-onward window, the answer is crude 1 month, coal 2, and for LNG a two-regressor form at 5 months on the Brent leg and 1 on the Henry Hub leg. The crude optimum is sharp: L=1 scores 0.069 against 0.096 at L=2, so assuming two months would cost about 40% of that leg's accuracy.

Selection is by walk-forward out-of-sample error, never by R². In-sample R² rises with any regressor that trends, and over 38 years everything here trends: on the full sample the coal leg's R² moves only 0.940 → 0.970 across all nine candidate lags, which cannot discriminate between them. The walk-forward refits on strictly earlier observations at every evaluation point. It is not a forecast — a customs month prints about two months late, so the regressor is already public when the target prints — it holds out the parameters, which is the part that overfits.

On the recommended 2015-onward window the fitted slopes are 1.005 for crude on Brent×FX (a USD commodity priced into a JPY import price should pass through at unit elasticity, and it does), 0.752 / 0.287 for LNG on the Brent and Henry Hub legs, and 0.881 for coal. Level R² is 0.979 / 0.933 / 0.973. Coefficient intervals are bootstrapped in contiguous blocks rather than row-by-row, because the residual is strongly autocorrelated and an independent resample would reproduce the same too-narrow interval the textbook standard error already gives.

What an AR(1) on the residual leaves behind is reported rather than smoothed over. Ljung–Box on the AR(1) innovations rejects at 5% for all three legs on that window (p = 0.0072 / 0.0071 / 0.0059). The failures are three specific echoes, not a uniformly-too-low order: crude has acf(2) = −0.331, which an AR(2) removes cleanly (p rises to 0.35), and that is the signature of a CIF averaging arrivals priced in two different months; LNG has an isolated acf(5) = +0.250 and coal an acf(3) = +0.233, which are the term formulas' own averaging windows showing through and which no low-order AR touches. So: AR(2) on crude, AR(1) on the other two, and treat the residual sigma as a lower bound at horizons near the offending lag.

R² on log levels overstates what a hedge does, since both sides are near unit-root over the sample and a level R² partly measures a shared trend. The honest number is the fraction of an h-month change in the CIF that a static proxy position removes. On the crude leg that runs 0.63 at one month to 0.92 at six; on LNG it is −0.96 at one month and 0.76 at six; on coal −0.29 and 0.84. A negative figure at one month is a real property of the exposure: a proxy lagged five months contributes nothing to the CIF's next month and still moves, so subtracting it adds variance. This basis is a term-structure object, hedgeable at a quarter and beyond.

The tariff is more hedgeable than any of its legs. The AR(1) innovation correlations across legs are +0.124 crude–LNG, −0.064 crude–coal and +0.047 LNG–coal, all inside the ±0.168 band that 136 observations can resolve — the three bases are separate accidents. 東京電力EP 低圧規制 mixes them at α=0.0048, β=0.3827, γ=0.6584 and then averages over three months, so both the cross-sectional and the temporal averaging bite: its 単価 has a level R² of 0.977 against 0.933 for the worst leg alone, and 0.774 of its month-to-month variance is removed against −0.960 for the LNG leg alone. That is a diversification result rather than a fit result, and it is the number the product is about.

Two cautions carried in the same file. The proxy sigmas are fitted to monthly-average levels, which for a driftless random walk carry ⅔ the variance of the walk — so they are low by about √(3/2) ≈ 1.22 against daily realised vol, and by 1.87 for Henry Hub, which is not a random walk at daily frequency. A simulation stepping monthly must use the monthly-average sigma; the realised sigma belongs to the traded hedge. And the fitted coefficients are publishable while the price levels behind the coal leg are not: that leg is fitted on the World Bank Pink Sheet, whose licence carves out third-party data, so the calibration file carries coefficients and diagnostics and the calibration script refuses to write any array longer than 12 numbers to stop a series arriving in it by accident.

Errors

This section is for callers writing client code; the method ends above. Every non-success response ships as application/problem+json (RFC 7807) with {type, title, status, detail, instance}. The type URI anchors into this section so callers can follow a failing response back to its definition.

400 Bad Request

The request was structurally valid but semantically rejected — e.g. target_regime=normal on spatial-influence (normal is uninformative). Fix: re-read the endpoint's accepted values in /v1/reference.

404 Not Found

Either the path is not routed, the market is not catalogued (GET /v1/markets for the full list), or the area is not tracked in the requested market (GET /v1/markets/{market}/areas). /v1/admin/usage also returns 404 when its bearer token is missing or wrong — the 404 covers "you can't tell if this exists" by design.

422 Unprocessable Entity

Query or path parameter failed validation (type, range). limit must be a positive integer within the endpoint's cap; horizon_min must be one of 60 / 120 / 240; max_lag_bars must be 1..48. The response body carries an errors array with the field-level Pydantic errors so SDKs can surface them without re-parsing the detail string.

405 Method Not Allowed

The endpoint exists but your HTTP method isn't accepted. All public endpoints are GET-only.

429 Too Many Requests

Rate limit hit (free tier: 60 req/min/IP). Response carries Retry-After (seconds until a token is back), X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (UNIX timestamp) headers. SDK clients use Retry-After for backoff; the raw JSON body is identical in structure to other problem+json responses.

500 Internal Server Error

Unexpected failure inside Amanoki. These are captured to Sentry when error telemetry is enabled, and retried-safe on your side — our endpoints are idempotent reads. If the error persists over a span, see /status for the public health snapshot.

503 Service Unavailable

Dependency is degraded but not crashed — typically the weather cache failed to refresh and we fall back to a stale snapshot or to the v0 baseline. /v1/health/deep breaks down the affected dependencies. Retry with a modest backoff.

Limitations

Where each method is known to be weak, so a reader can judge it before relying on it.