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:
low— realized-vol z-score below the low-enter threshold forenter_kconsecutive bars.normal— neither low nor high; the common case.high— realized-vol z-score above the high-enter threshold forenter_kconsecutive bars.scarcity— absolute price ≥ the market's scarcity threshold (¥80/kWh for JEPX; per-market thresholds in the catalog). Takes priority over the z-score ladder.
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:
- v1 (current-bar): area one-hot + UTC hour one-hot + is-weekend + current z-vol + five Open-Meteo weather variables (temperature, wind, shortwave radiation, cloud cover, humidity).
- v1.5 (lag-augmented): v1 features + z-vol
lags at
t-1 / -2 / -6 / -24bars + regime one-hot att-1.
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):
| Horizon | v1 | v1.5 (active) | Ratio |
|---|---|---|---|
| 60 min | 0.1406 | 0.0167 | 8.4× |
| 120 min | 0.1348 | 0.0321 | 4.2× |
| 240 min | 0.1446 | 0.0754 | 1.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 銭:
- The divisor is 1,000, not 100. 基準単価 is defined as the ¥/kWh move per 1,000円/kl move in 平均燃料価格. 東北電力's own worked example — (83,500 − 43,500) × 0.197 ÷ 1,000 = 7.88 — pins it. Getting this wrong is a 10× error.
- Rounding is 四捨五入 (round-half-up), twice —
平均燃料価格 to the nearest 100円, then 単価 to the nearest 銭
(0.01 ¥/kWh). Python's
round()andnumpy.roundare round-half-to-even and break published tie cases, so the whole chain is carried indecimal.Decimal. 北海道電力's 基準単価 of 0.173 puts several 平均燃料価格 values on an exact tie: computed in binary floats, 35,800円/kl gives ▲7.78 where the published figure is ▲7.79. - A cap replaces 平均燃料価格; it does not clip the 単価. On a regulated tariff the cap is round100(1.5 × 基準燃料価格), and where the raw average exceeds it the capped value is substituted before the second line runs. The difference between the capped and uncapped 単価 is reported as a wedge in ¥/kWh, so it is directly comparable with the same supplier's uncapped menu.
- The government grant is a separate leg. Utility 単価 tables publish net of 電気・ガス料金支援 where it applies, so the fuel formula on its own is short by exactly the grant. 関西電力 高圧 2026年2月分 published ▲3.50, of which ▲2.30 is the grant and ▲1.20 the fuel leg. The scheme has been switched on and off seven times since 2023-02 with no pattern, so months past the last announced block are treated as an unmodelled policy fork rather than extrapolated.
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:
- Point-in-time replication
(
tests/test_nencho_pit.py). Given only the committed customs archive and the registry's parameters, the engine reproduces 関西電力's published 平均燃料価格 and both 高圧 and 特別高圧 単価 for the 14 announced months from 2025年7月分 to 2026年8月分, and 東京電力EP 低圧's 単価 for 2026年4月分 to 2026年8月分 — 33 published unit rates across 19 billing months, with no tolerance and nothing fitted. Ground truth is biz.kepco.jp's 燃料費調整単価一覧 and tepco.co.jp's 単価一覧, both fetched 2026-07-27. - The 高圧 / 特別高圧 pair is a natural experiment on the grant leg. Both voltages share one 平均燃料価格 and differ only in 基準単価 (0.106 vs 0.105) and in grant eligibility — 特別高圧 is outside the scheme. So 2026年8月分's 40,400円/kl produces ▲2.50 for 高圧 and ▲0.69 for 特別高圧, and the 1.80 difference is the grant. An engine missing that leg passes on 特別高圧 and fails on 高圧, which is how the leg was found.
- Announcement-date cutoff. Replaying the 19 announced months with today's freshest vintage throughout reproduces 15 of them; cutting each month at its own announcement date reproduces all 19. 東京電力EP's 2026年6月分 is pinned as the case where the two disagree: the announcement vintage gives 46,200円/kl and ▲7.30, the published figure.
- Cross-supplier spot checks
(
tests/test_nencho.py). 北陸電力's 2026-04-28 release quotes 平均燃料価格 33,200円/kl and 低圧 ▲7.69 / 高圧 ▲7.32 / 特別高圧 ▲7.18; all three are pinned, alongside 関西電力 高圧 and 東京電力EP 低圧 for the same month, from the 2026年1〜3月 customs window (原油 65,969円/kl, LNG 87,003円/t, 一般炭 19,176円/t) that 北陸電力 prints verbatim in that release. - Structural checks. Every tariff in the registry is required to satisfy the calorific identity; quantity-weighting is pinned by showing it disagrees with the unweighted mean on a real window and that only the weighted figure matches; the round-half-up chain is pinned on seven of 北海道電力's tie cases; and billing months earlier than a 約款's effective date are excluded rather than replicated with parameters that did not yet apply.
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.
- Spike probability has a single conditioning dimension (hour × weekend). Weather / z-vol quartile conditioning on the spike table is deferred.
- Regime forecast v1.5 is linear in features. Reservoir / physics-informed predictors are under evaluation but have not beaten v1.5 on held-out Brier at this scale.
- No cross-market propagation yet — each (market, area) is independent.
- ERCOT / PJM / CAISO / AEMO / NYISO are catalogued only. Adapters land when their feeds are registered.
- Empirical frequencies; they do not account for regime changes
in the underlying market structure (rule changes, new capacity).
The empirical tables are rebuilt from the whole archive every
time the service boots, so the daily JEPX refresh reaches them
on the next deploy. The v1.5 regression weights do not move on
that schedule: they are a static artifact, trained 2026-04-19
and baked into the image, changing only when the training script
is run and a new artifact committed.
/v1/statusreports the serving artifact's date asmodel_trained_at_ms. - 燃調: point-in-time customs vintages start at 2024-01. The
customs URL scheme changed incompatibly before that, so genuine
as-of-announcement replication is not recoverable further back.
Billing months whose window reaches earlier than that are flagged
as an archive limit of ours rather than as an unpublished
government release, and those months' customs prices are still
served from
/v1/nencho/fuel-prices. - 燃調: 4 of the 13 registry entries are still short a parameter
the formula needs — α/β/γ on 関西電力 低圧規制, 基準単価 on
中国電力 低圧 and 九州電力 高圧, both on 東京電力EP's
2026-04 高圧約款 — and are marked
evaluable: false. Several suppliers publish those numbers only as images inside a PDF. - 燃調: the 電気・ガス料金支援 grant is announced block by block with no fixed schedule. Billing months past the last announced block carry the fuel leg only.
- 燃調 basis: fitted on monthly data, with a residual whose AR leaves a known echo on each leg (crude at lag 2, LNG at 5, coal at 3). Residual sigma is a lower bound at horizons near those lags, and the one-month variance-explained figure is negative on the LNG and coal legs.