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 | diff --git a/package.json b/package.json index 7c5edff..52396f7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@qtsurfer/api-client", - "version": "0.1.2", + "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 0d7ff03..ede59f4 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 AuthTokenErrorSchema = { + 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..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 { 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 & { @@ -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..1188ae7 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 AuthTokenError = { + /** + * 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: AuthTokenError; + /** + * Rate limit exceeded for this API key. + */ + 429: unknown; +}; + +export type AuthError = 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;