Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
101 changes: 100 additions & 1 deletion src/generated/schemas.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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 <token>` 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;
27 changes: 26 additions & 1 deletion src/generated/sdk.gen.ts
Original file line number Diff line number Diff line change
@@ -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<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = ClientOptions<TData, ThrowOnError> & {
Expand All @@ -18,6 +18,31 @@ export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends
meta?: Record<string, unknown>;
};

/**
* 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
* <token>` 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 = <ThrowOnError extends boolean = false>(options?: Options<AuthData, ThrowOnError>) => {
return (options?.client ?? _heyApiClient).post<AuthResponse, AuthError, ThrowOnError>({
security: [
{
name: 'X-API-Key',
type: 'apiKey'
}
],
url: '/auth/token',
...options
});
};

/**
* Get a list of available exchanges
*/
Expand Down
90 changes: 89 additions & 1 deletion src/generated/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
/**
Expand All @@ -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
*/
Expand Down Expand Up @@ -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<EquityPoint>;
/**
* Number of signals emitted during strategy execution
*/
Expand All @@ -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 <token>` 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<string>;
/**
* 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;
Expand Down
Loading