From 9138f12bf11d121c9dcbc0567be525a837cbb85b Mon Sep 17 00:00:00 2001 From: Manuel Polo Date: Mon, 25 May 2026 19:21:43 +0000 Subject: [PATCH 1/3] feat: regenerate client for auth token endpoint (0.2.0) Picks up OpenAPI spec 0.95.0: - New POST /auth/token operation, exported as auth() - AuthTokenResponse and AuthError schemas - apiKeyAuth security scheme Also carries forward additive schema changes from a prior missed regen: EquityPoint, ResultMap.pnlTotalPercent, and ResultMap.equityCurve. --- package.json | 2 +- src/generated/schemas.gen.ts | 101 ++++++++++++++++++++++++++++++++++- src/generated/sdk.gen.ts | 27 +++++++++- src/generated/types.gen.ts | 90 ++++++++++++++++++++++++++++++- 4 files changed, 216 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 7c5edff..ca5c73c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@qtsurfer/api-client", - "version": "0.1.2", + "version": "0.2.0", "description": "Auto-generated TypeScript API client for the QTSurfer API (from OpenAPI 3.1 spec)", "type": "module", "main": "./dist/index.js", diff --git a/src/generated/schemas.gen.ts b/src/generated/schemas.gen.ts index 0d7ff03..7add5e1 100644 --- a/src/generated/schemas.gen.ts +++ b/src/generated/schemas.gen.ts @@ -185,7 +185,7 @@ export const BacktestJobResultSchema = { export const ResultMapSchema = { type: 'object', - description: 'Execution result map. Always includes core fields (hostName, iops, strategyId, instrument). Yield metrics (pnlTotal, totalTrades, winRate, etc.) are present when the strategy emitted at least one trade. When signal storage is enabled, includes signal fields described below.', + description: 'Execution result map. Always includes core fields (hostName, iops, strategyId, instrument). Yield metrics (pnlTotal, pnlTotalPercent, totalTrades, winRate, equityCurve, etc.) are present when the strategy emitted at least one trade. When signal storage is enabled, includes signal fields described below.', required: ['strategyId', 'instrument'], properties: { hostName: { @@ -215,6 +215,12 @@ export const ResultMapSchema = { description: 'Total profit and loss in the output currency', example: 42.75 }, + pnlTotalPercent: { + type: 'number', + format: 'double', + description: 'Total PnL as a percentage of the initial capital (`backtestFunding`). Zero when `backtestFunding` is 0.', + example: 42.75 + }, totalTrades: { type: 'integer', format: 'int64', @@ -257,6 +263,27 @@ export const ResultMapSchema = { description: 'Maximum percentage drawdown from peak equity', example: 8.75 }, + equityCurve: { + type: 'array', + description: "Equity curve over the backtest. Element 0 is an anchor at the backtest `from` with `initialCapital`; the remaining points are one sample per emitted yield, in order. Use it to plot the strategy's running equity without re-deriving it from the yield history.", + items: { + '$ref': '#/components/schemas/EquityPoint' + }, + example: [ + { + timestamp: 1700000000000, + equity: 100 + }, + { + timestamp: 1700000060000, + equity: 110.5 + }, + { + timestamp: 1700000120000, + equity: 90.25 + } + ] + }, signalCount: { type: 'integer', description: 'Number of signals emitted during strategy execution', @@ -293,8 +320,80 @@ export const ResultMapSchema = { } } as const; +export const EquityPointSchema = { + type: 'object', + description: 'Single sample of the running equity at a yield event.', + required: ['timestamp', 'equity'], + properties: { + timestamp: { + type: 'integer', + format: 'int64', + description: 'Epoch milliseconds. The first point in an equity curve is anchored at the backtest `from`; subsequent points carry the timestamp of each emitted yield.', + example: 1700000000000 + }, + equity: { + type: 'number', + format: 'double', + description: 'Running equity at this point (`initialCapital + cumulativePnl`).', + example: 110.5 + } + } +} as const; + export const strategyIdSchema = { description: 'Unique identifier for a compiled strategy', type: 'string', example: '6bsh31ikwkuivhtgcoa6s4' +} as const; + +export const AuthTokenResponseSchema = { + type: 'object', + required: ['access_token', 'token_type', 'expires_in', 'tier'], + properties: { + access_token: { + type: 'string', + description: 'Short-lived HS256 JWT. Send as `Authorization: Bearer ` on all other endpoints.' + }, + token_type: { + type: 'string', + enum: ['Bearer'], + description: 'Always `Bearer`.' + }, + expires_in: { + type: 'integer', + description: 'Seconds until the JWT expires (typically 3600).', + example: 3600 + }, + scopes: { + type: 'array', + description: 'Scopes granted to this token. Reserved for future use; currently always empty.', + items: { + type: 'string' + }, + example: [] + }, + tier: { + type: 'string', + enum: ['free', 'basic', 'pro', 'elite'], + description: 'Subscription tier this token was issued for. Drives rate limits and feature flags on downstream endpoints.', + example: 'free' + } + } +} as const; + +export const AuthErrorSchema = { + type: 'object', + required: ['code', 'message'], + description: 'Error envelope returned by `POST /auth/token` when the API key is rejected.', + properties: { + code: { + type: 'string', + enum: ['invalid_apikey', 'apikey_revoked', 'apikey_expired'], + description: 'Machine-readable error reason.' + }, + message: { + type: 'string', + description: 'Human-readable description of the failure.' + } + } } as const; \ No newline at end of file diff --git a/src/generated/sdk.gen.ts b/src/generated/sdk.gen.ts index e2bf0c7..71d3162 100644 --- a/src/generated/sdk.gen.ts +++ b/src/generated/sdk.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts import type { Options as ClientOptions, TDataShape, Client } from '@hey-api/client-fetch'; -import type { GetExchangesData, GetExchangesResponse, GetInstrumentsData, GetInstrumentsResponse, GetInstrumentsError, GetExchangeTickersHourData, GetExchangeTickersHourResponse, GetExchangeTickersHourError, GetExchangeKlinesHourData, GetExchangeKlinesHourResponse, GetExchangeKlinesHourError, PostStrategyData, PostStrategyResponse, PostStrategyError, GetStrategyStatusData, GetStrategyStatusResponse, GetStrategyStatusError, PrepareBacktestingData, PrepareBacktestingResponse, PrepareBacktestingError, GetPreparationStatusData, GetPreparationStatusResponse, GetPreparationStatusError, ExecuteBacktestingData, ExecuteBacktestingResponse, ExecuteBacktestingError, CancelExecutionData, CancelExecutionResponse, CancelExecutionError, GetExecutionResultData, GetExecutionResultResponse, GetExecutionResultError } from './types.gen'; +import type { AuthData, AuthResponse, AuthError2, GetExchangesData, GetExchangesResponse, GetInstrumentsData, GetInstrumentsResponse, GetInstrumentsError, GetExchangeTickersHourData, GetExchangeTickersHourResponse, GetExchangeTickersHourError, GetExchangeKlinesHourData, GetExchangeKlinesHourResponse, GetExchangeKlinesHourError, PostStrategyData, PostStrategyResponse, PostStrategyError, GetStrategyStatusData, GetStrategyStatusResponse, GetStrategyStatusError, PrepareBacktestingData, PrepareBacktestingResponse, PrepareBacktestingError, GetPreparationStatusData, GetPreparationStatusResponse, GetPreparationStatusError, ExecuteBacktestingData, ExecuteBacktestingResponse, ExecuteBacktestingError, CancelExecutionData, CancelExecutionResponse, CancelExecutionError, GetExecutionResultData, GetExecutionResultResponse, GetExecutionResultError } from './types.gen'; import { client as _heyApiClient } from './client.gen'; export type Options = ClientOptions & { @@ -18,6 +18,31 @@ export type Options; }; +/** + * Exchange API key for a short-lived JWT + * Exchanges a long-lived API key for a short-lived JWT used by every other + * endpoint. This is the only endpoint that accepts an API key directly — + * callers should obtain a JWT here, then send it as `Authorization: Bearer + * ` to all other operations. + * + * The returned JWT carries the caller's subscription `tier` as a claim and + * expires after `expires_in` seconds. Callers should refresh the token + * before expiry (or on a `401` response) by calling this endpoint again. + * + */ +export const auth = (options?: Options) => { + return (options?.client ?? _heyApiClient).post({ + security: [ + { + name: 'X-API-Key', + type: 'apiKey' + } + ], + url: '/auth/token', + ...options + }); +}; + /** * Get a list of available exchanges */ diff --git a/src/generated/types.gen.ts b/src/generated/types.gen.ts index 753831b..9335362 100644 --- a/src/generated/types.gen.ts +++ b/src/generated/types.gen.ts @@ -135,7 +135,7 @@ export type BacktestJobResult = { }; /** - * Execution result map. Always includes core fields (hostName, iops, strategyId, instrument). Yield metrics (pnlTotal, totalTrades, winRate, etc.) are present when the strategy emitted at least one trade. When signal storage is enabled, includes signal fields described below. + * Execution result map. Always includes core fields (hostName, iops, strategyId, instrument). Yield metrics (pnlTotal, pnlTotalPercent, totalTrades, winRate, equityCurve, etc.) are present when the strategy emitted at least one trade. When signal storage is enabled, includes signal fields described below. */ export type ResultMap = { /** @@ -158,6 +158,10 @@ export type ResultMap = { * Total profit and loss in the output currency */ pnlTotal?: number; + /** + * Total PnL as a percentage of the initial capital (`backtestFunding`). Zero when `backtestFunding` is 0. + */ + pnlTotalPercent?: number; /** * Total number of trades executed by the strategy */ @@ -186,6 +190,10 @@ export type ResultMap = { * Maximum percentage drawdown from peak equity */ maxDrawdownPercent?: number; + /** + * Equity curve over the backtest. Element 0 is an anchor at the backtest `from` with `initialCapital`; the remaining points are one sample per emitted yield, in order. Use it to plot the strategy's running equity without re-deriving it from the yield history. + */ + equityCurve?: Array; /** * Number of signals emitted during strategy execution */ @@ -212,11 +220,91 @@ export type ResultMap = { signalsUploadReason?: string; }; +/** + * Single sample of the running equity at a yield event. + */ +export type EquityPoint = { + /** + * Epoch milliseconds. The first point in an equity curve is anchored at the backtest `from`; subsequent points carry the timestamp of each emitted yield. + */ + timestamp: number; + /** + * Running equity at this point (`initialCapital + cumulativePnl`). + */ + equity: number; +}; + /** * Unique identifier for a compiled strategy */ export type StrategyId = string; +export type AuthTokenResponse = { + /** + * Short-lived HS256 JWT. Send as `Authorization: Bearer ` on all other endpoints. + */ + access_token: string; + /** + * Always `Bearer`. + */ + token_type: 'Bearer'; + /** + * Seconds until the JWT expires (typically 3600). + */ + expires_in: number; + /** + * Scopes granted to this token. Reserved for future use; currently always empty. + */ + scopes?: Array; + /** + * Subscription tier this token was issued for. Drives rate limits and feature flags on downstream endpoints. + */ + tier: 'free' | 'basic' | 'pro' | 'elite'; +}; + +/** + * Error envelope returned by `POST /auth/token` when the API key is rejected. + */ +export type AuthError = { + /** + * Machine-readable error reason. + */ + code: 'invalid_apikey' | 'apikey_revoked' | 'apikey_expired'; + /** + * Human-readable description of the failure. + */ + message: string; +}; + +export type AuthData = { + body?: never; + path?: never; + query?: never; + url: '/auth/token'; +}; + +export type AuthErrors = { + /** + * API key is invalid, revoked, or expired. + */ + 401: AuthError; + /** + * Rate limit exceeded for this API key. + */ + 429: unknown; +}; + +export type AuthError2 = AuthErrors[keyof AuthErrors]; + +export type AuthResponses = { + /** + * API key accepted; JWT returned. + */ + 200: AuthTokenResponse; +}; + +export type AuthResponse = AuthResponses[keyof AuthResponses]; + export type GetExchangesData = { body?: never; path?: never; From 92a1f0fbde2b8e7522c0a76e0ff56910290c86fd Mon Sep 17 00:00:00 2001 From: Manuel Polo Date: Mon, 25 May 2026 20:32:08 +0000 Subject: [PATCH 2/3] feat: regenerate client for openapi 0.95.1 (AuthError -> AuthTokenError) Picks up the AuthError schema rename in spec 0.95.1 (symmetric with AuthTokenResponse). All other contract details unchanged. Generated type renames: - AuthError (schema) -> AuthTokenError - AuthError2 (op error union) -> AuthError (clean now, no suffix) The schema model is exposed as AuthTokenError; the per-operation error union returned by auth() is the unsuffixed AuthError. Bumps @qtsurfer/api-client to 0.2.1. --- package.json | 2 +- src/generated/schemas.gen.ts | 2 +- src/generated/sdk.gen.ts | 4 ++-- src/generated/types.gen.ts | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index ca5c73c..52396f7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@qtsurfer/api-client", - "version": "0.2.0", + "version": "0.2.1", "description": "Auto-generated TypeScript API client for the QTSurfer API (from OpenAPI 3.1 spec)", "type": "module", "main": "./dist/index.js", diff --git a/src/generated/schemas.gen.ts b/src/generated/schemas.gen.ts index 7add5e1..ede59f4 100644 --- a/src/generated/schemas.gen.ts +++ b/src/generated/schemas.gen.ts @@ -381,7 +381,7 @@ export const AuthTokenResponseSchema = { } } as const; -export const AuthErrorSchema = { +export const AuthTokenErrorSchema = { type: 'object', required: ['code', 'message'], description: 'Error envelope returned by `POST /auth/token` when the API key is rejected.', diff --git a/src/generated/sdk.gen.ts b/src/generated/sdk.gen.ts index 71d3162..254b069 100644 --- a/src/generated/sdk.gen.ts +++ b/src/generated/sdk.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts import type { Options as ClientOptions, TDataShape, Client } from '@hey-api/client-fetch'; -import type { AuthData, AuthResponse, AuthError2, GetExchangesData, GetExchangesResponse, GetInstrumentsData, GetInstrumentsResponse, GetInstrumentsError, GetExchangeTickersHourData, GetExchangeTickersHourResponse, GetExchangeTickersHourError, GetExchangeKlinesHourData, GetExchangeKlinesHourResponse, GetExchangeKlinesHourError, PostStrategyData, PostStrategyResponse, PostStrategyError, GetStrategyStatusData, GetStrategyStatusResponse, GetStrategyStatusError, PrepareBacktestingData, PrepareBacktestingResponse, PrepareBacktestingError, GetPreparationStatusData, GetPreparationStatusResponse, GetPreparationStatusError, ExecuteBacktestingData, ExecuteBacktestingResponse, ExecuteBacktestingError, CancelExecutionData, CancelExecutionResponse, CancelExecutionError, GetExecutionResultData, GetExecutionResultResponse, GetExecutionResultError } from './types.gen'; +import type { AuthData, AuthResponse, AuthError, GetExchangesData, GetExchangesResponse, GetInstrumentsData, GetInstrumentsResponse, GetInstrumentsError, GetExchangeTickersHourData, GetExchangeTickersHourResponse, GetExchangeTickersHourError, GetExchangeKlinesHourData, GetExchangeKlinesHourResponse, GetExchangeKlinesHourError, PostStrategyData, PostStrategyResponse, PostStrategyError, GetStrategyStatusData, GetStrategyStatusResponse, GetStrategyStatusError, PrepareBacktestingData, PrepareBacktestingResponse, PrepareBacktestingError, GetPreparationStatusData, GetPreparationStatusResponse, GetPreparationStatusError, ExecuteBacktestingData, ExecuteBacktestingResponse, ExecuteBacktestingError, CancelExecutionData, CancelExecutionResponse, CancelExecutionError, GetExecutionResultData, GetExecutionResultResponse, GetExecutionResultError } from './types.gen'; import { client as _heyApiClient } from './client.gen'; export type Options = ClientOptions & { @@ -31,7 +31,7 @@ export type Options(options?: Options) => { - return (options?.client ?? _heyApiClient).post({ + return (options?.client ?? _heyApiClient).post({ security: [ { name: 'X-API-Key', diff --git a/src/generated/types.gen.ts b/src/generated/types.gen.ts index 9335362..1188ae7 100644 --- a/src/generated/types.gen.ts +++ b/src/generated/types.gen.ts @@ -265,7 +265,7 @@ export type AuthTokenResponse = { /** * Error envelope returned by `POST /auth/token` when the API key is rejected. */ -export type AuthError = { +export type AuthTokenError = { /** * Machine-readable error reason. */ @@ -287,14 +287,14 @@ export type AuthErrors = { /** * API key is invalid, revoked, or expired. */ - 401: AuthError; + 401: AuthTokenError; /** * Rate limit exceeded for this API key. */ 429: unknown; }; -export type AuthError2 = AuthErrors[keyof AuthErrors]; +export type AuthError = AuthErrors[keyof AuthErrors]; export type AuthResponses = { /** From cb932b3d1db4f81eedafc44ee6f661136de32359 Mon Sep 17 00:00:00 2001 From: Manuel Polo Date: Mon, 25 May 2026 20:54:07 +0000 Subject: [PATCH 3/3] docs(auth): add auth() to API surface, API key -> JWT snippet and SDK cross-link Generator regen does not touch the README. Add the `auth` row to the operations table, a short usage snippet showing how to call `await auth({ headers: { 'X-API-Key': ... } })` and destructure `data.access_token`, document the QTSURFER_APIKEY env-var convention, and point production callers at the sibling SDK for token-refresh handling. Part of openapi-auth-rollout Phase 2 (#168). --- README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/README.md b/README.md index d6fe81c..f481016 100644 --- a/README.md +++ b/README.md @@ -41,12 +41,35 @@ if (error) throw error; console.log(exchanges); ``` +### API key → JWT + +Every endpoint above expects a short-lived JWT in `Authorization: Bearer …`. +Exchange a long-lived API key for one via `auth`: + +```ts +import { auth } from '@qtsurfer/api-client'; + +const { data, error } = await auth({ + baseUrl: 'https://api.qtsurfer.com/v1', + headers: { 'X-API-Key': process.env.QTSURFER_APIKEY! }, +}); +if (error) throw error; + +const { access_token: jwt } = data; // feed to client.setConfig() for the rest +``` + +For production use, prefer the [`@qtsurfer/sdk`](https://github.com/QTSurfer/sdk-ts) +`auth(apikey)` helper — it returns a session that refreshes the JWT +transparently, reads `QTSURFER_APIKEY` from the environment, and supports +pluggable token stores so callers don't reinvent that plumbing. + ## API surface All operations are exported as standalone functions; every operation accepts an `Options` object and returns `{ data, error, response }`. | Function | Method | Path | Purpose | | -------- | ------ | ---- | ------- | +| `auth` | POST | `/auth/token` | Exchange an API key for a short-lived JWT | | `getExchanges` | GET | `/exchanges` | List available exchanges | | `getInstruments` | GET | `/exchange/{exchangeId}/instruments` | List instruments for an exchange | | `getExchangeTickersHour` | GET | `/exchange/{exchangeId}/tickers/{base}/{quote}` | Download one hour of tickers as Lastra/Parquet |