跳到主要内容

Calendar REST & cBot API

The economic calendar is exposed as a versioned, JWT-secured, rate-limited REST API — the flagship integration surface. Any external service, dashboard or cBot integrates against it as a product. It has feature parity with the FXStreet Calendar API and goes past it: point-in-time asOf, full revision chains, deterministic impact rationale, surprise analytics, country→symbol resolution, and blackout math that other calendar APIs do not expose.

Status. The JWT security (client issuance + token exchange), the gating, and the core read endpoints — token, events, events/{id}, history, series, surprises, next, blackout, affected-symbols, health — are implemented and integration-tested (auth, scope enforcement, feature/white-label 404), plus events/batch (bounded multiplex) and a discoverable /openapi.json document, ETag/If-None-Match 304 on the event/history reads, and keyset cursor pagination (Link: rel="next"), the SSE stream (live event: release push, poll-backed), HMAC-signed webhooks (X-CMind-Signature: sha256=…, owner-registered, delivered by a config-gated worker off a persisted watermark), and the shipped typed client (CmindCalendarClient). The full public API surface is implemented.

Security — JWT​

The API reuses the repo's existing HS256 token machinery (the same pattern the CtraderCliNode agents use), not a new scheme:

  • An app admin issues a Calendar API client (name + scopes + expiry). The client exchanges its id and secret at POST /api/calendar/v1/token for a short-lived HS256 JWT (iss=cmind-calendar, aud=calendar-api, exp ~15 min, scope claim). Only the short JWT rides on requests (Authorization: Bearer <jwt>).
  • The client secret is stored encrypted via ISecretProtector — never plaintext, never logged.
  • Scopes (least-privilege): calendar:read, calendar:blackout, calendar:surprises, calendar:stream. A cBot token typically gets read + blackout only.
  • Standard JwtBearer validation (issuer, audience, lifetime, signing key; alg=none rejected; tight clock skew). Per-client token-bucket rate limit + global limiter; 429 with Retry-After. All auth failures are audited.
  • Disabling the client stops future token issuance immediately; the short JWT lifetime bounds a leaked token. The whole /api/calendar/** tree 404s when the feature is disabled.

Conventions​

  • Base path & versioning: /api/calendar/v1/... (URL-versioned; additive changes don't bump).
  • Format: JSON; RFC 3339 UTC instants plus an explicit sourceTimeZone; optional tz= renders a convenience local time without losing the UTC anchor.
  • Pagination: cursor-based (cursor, limit ≤ 1000); next cursor in the body and a Link header.
  • Caching: ETag + If-None-Match; historical ranges get a long TTL, upcoming a short one.
  • Errors: RFC 7807 problem+json, never a bare 500.
  • Degraded reads: a source/DB fault returns 200 best-known data plus an X-Calendar-Freshness / stale=true signal (or 503 Retry-After only if truly nothing is known) — the cBot decides.

Endpoints​

Method & pathPurposeKey params
POST /v1/tokenExchange client id+secret → short JWTbody: clientId, clientSecret
GET /v1/eventsEvents in a window (upcoming or historical)from,to,countries,currencies,series,minImpact,category,q,asOf,cursor,limit,tz
GET /v1/events/{id}One event: full revision chain, surprise, impact rationale, affected symbolswatchlist?,asOf?
GET /v1/events/{id}/revisionsOrdered revision history—
GET /v1/historyDeep historical pull for a series (≥10y)series,from,to,asOf,cursor,limit
GET /v1/seriesCatalog of tracked indicators + cadence + sourcecountries,currencies,q
GET /v1/surprisesHistorical actual/forecast/surprise z-score seriesseries,count/from,to
GET /v1/nextNext relevant release for a symbol (country→symbol mapped)symbol,minImpact
GET /v1/blackoutIs a symbol inside a high-impact window now/at Tsymbol,at?,minImpact,before,after
GET /v1/affected-symbolsResolve an event → symbols in a watchlisteventId,watchlist
POST /v1/events:batchMultiplex several queries in one round-tripbody: array of queries
GET /v1/stream (SSE)Live push: releases/revisions/window-entercurrencies,minImpact (scope calendar:stream)
POST /v1/webhooksRegister an HMAC-signed callback for release/revision/blackoutbody: url, filters, secret
GET /v1/healthPer-source freshness + coverage—

Blackout — the cBot news filter​

GET /v1/blackout returns { inBlackout, event, startsAt, endsAt, stale }. On uncertainty it defaults to the configured conservative answer (fail-closed by default: "assume in-blackout" for risk-off bots), plus a stale flag — a data gap never green-lights trading through NFP. The endpoint is a pure DB/cache read with a hard server timeout; there is no synchronous origin fetch on the hot path.

A shipped typed client (Infrastructure.Calendar.CmindCalendarClient) wraps this: point its HttpClient at the API root, call GetTokenAsync(clientId, clientSecret) once, then GetBlackoutAsync(token, symbol) before each order — it is fail-safe by construction (any non-success or parse error returns InBlackout = true, Stale = true, so a data gap never green-lights trading). A cBot pauses around news like this:

// Pseudocode for a cTrader cBot using WebRequest + a Calendar API client token.
var jwt = CalendarApi.GetToken(clientId, clientSecret); // POST /v1/token
var res = CalendarApi.Blackout(jwt, symbol: SymbolName, // GET /v1/blackout
minImpact: "High", before: 15, after: 15);
if (res.InBlackout || res.Stale) // fail-safe: stale ⇒ treat as blackout
return; // skip new entries in the news window
// ...otherwise proceed to place the order

Point-in-time for backtests​

Pass asOf on any read to get the calendar exactly as it stood at a past instant — the actuals, forecasts and revisions as they were then. Because asOf reads are pure and cacheable, a backtest hammering history gets identical bytes every time, and a backtested news rule behaves exactly like the live one (no look-ahead from revised values).

Resilience for algo callers​

The API sits in a trading hot path, so it never throws into a live bot: every path returns a well-formed problem+json or a typed degraded body. It reuses copy-trading's resilience primitives — the standard HTTP resilience handler on each source client, a domain circuit breaker per source, a lease-guarded singleton ingestion worker with startup reconciliation, and health checks wired into /health. The shipped typed client snippet comes with retry + timeout + circuit-breaker preconfigured so bot authors inherit resilience.

Sibling: AI currency-strength (market:read)​

The AI macro currency-strength read model rides the same JWT machinery — one scheme, one signing secret, one rate-limiter — adding only a market:read scope. Register an API client with that scope, exchange it for a token exactly as above, and call:

GET /api/market/v1/currency-strength/latest?horizon=3M&tier=Majors
GET /api/market/v1/currency-strength/history?days=30
GET /api/market/v1/currency-strength/pair/EUR/USD?horizon=3M
// obtain a token via POST /api/calendar/v1/token as above, then:
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var view = await http.GetFromJsonAsync<JsonElement>(
baseUrl + "/api/market/v1/currency-strength/latest?horizon=3M");
// view.ranking[], view.forecasts[], view.pairs[] (bias/conviction), view.narrative

A token missing market:read gets 403; an expired/tampered token gets 401. The endpoints are gated on the AI feature flag and served under /api/market/v1 so they stay independent of the calendar feature gate. At run/backtest dispatch a deployment may inject CMIND_API_BASEURL + a short-lived market:read token so a cBot calls back with zero client registration.