Opinionated TypeScript SDK for QTSurfer, built on top of @qtsurfer/api-client.
Where @qtsurfer/api-client gives you one typed function per API endpoint, @qtsurfer/sdk adds workflow orchestration, normalized errors, and cancellation — run a backtest with a single await.
npm install @qtsurfer/sdk
# or
pnpm add @qtsurfer/sdkOne call: API key in, ready-to-use session out. JWT refresh on 401 is handled for you.
import { auth } from '@qtsurfer/sdk';
import { readFileSync } from 'node:fs';
// Reads QTSURFER_APIKEY from env when no argument is passed.
const qts = await auth();
// Or: const qts = await auth('ak_...');
const result = await qts.backtest({
strategy: readFileSync('./MyStrategy.java', 'utf8'),
exchangeId: 'binance',
instrument: 'BTC/USDT',
from: '2024-01-01',
to: '2024-12-31',
storeSignals: true,
});
console.log('PnL:', result.pnlTotal);
console.log('Trades:', result.totalTrades);| Variable | Purpose |
|---|---|
QTSURFER_APIKEY |
API key consumed by auth() when no arg is passed |
Tokens are kept in memory by default. Implement TokenStore to swap in
browser storage, a file, or a secret manager:
import { auth, type TokenStore, type AuthTokenResponse } from '@qtsurfer/sdk';
const browserStore: TokenStore = {
load: () => JSON.parse(localStorage.getItem('qts.jwt') ?? 'null'),
save: (t) => localStorage.setItem('qts.jwt', JSON.stringify(t)),
clear: () => localStorage.removeItem('qts.jwt'),
};
const qts = await auth(undefined, { store: browserStore });auth() also accepts { baseUrl, fetch } for staging, custom HTTP
transports, or a Node-fetch polyfill in legacy runtimes.
If you already hold a JWT and want to manage refresh yourself, the
QTSurfer constructor still accepts a token:
import { QTSurfer } from '@qtsurfer/sdk';
const qts = new QTSurfer({
baseUrl: 'https://api.qtsurfer.com/v1',
token: process.env.QTSURFER_TOKEN,
});Orchestrates the full four-step workflow that the raw API exposes:
- Compile the strategy (
POST /strategyin async mode) and pollGET /strategy/{id}untilCompleted. - Prepare the data range (
POST /backtest/{exchange}/ticker/prepare) and poll untilCompleted. - Execute the backtest (
POST /backtest/{exchange}/ticker/execute) and pollGET /backtest/.../execute/{jobId}untilCompleted. - Return the
ResultMap(pnlTotal,totalTrades,sharpeRatio,signalsUrl, …).
Polling uses exponential backoff (intervalMs * 1.5, capped at maxIntervalMs) with per-stage timeout.
Progress is emitted on every stage transition and after each poll whose size > 0.
Stream one hour of raw ticker or kline data for an instrument. The default wire format is Lastra (application/vnd.lastra); pass format: 'parquet' for on-the-fly Parquet conversion.
// Lastra (default)
const blob = await qts.tickers({
exchangeId: 'binance',
base: 'BTC',
quote: 'USDT',
hour: '2026-01-15T10',
});
await Bun.write('BTC_USDT_2026-01-15_h10.lastra', await blob.arrayBuffer());
// Parquet
const klines = await qts.klines({
exchangeId: 'binance',
base: 'BTC',
quote: 'USDT',
hour: '2026-01-15T10',
format: 'parquet',
});HTTP errors surface as QTSDownloadError (subclass of QTSError).
All SDK errors extend QTSError so you can catch them generically or match by subclass.
import {
QTSError,
QTSStrategyCompileError,
QTSPreparationError,
QTSExecutionError,
QTSDownloadError,
QTSTimeoutError,
QTSCanceledError,
} from '@qtsurfer/sdk';
try {
await qts.backtest(req);
} catch (e) {
if (e instanceof QTSStrategyCompileError) {
console.error('Compile failed:', e.message);
} else if (e instanceof QTSPreparationError) {
console.error('Data prep failed:', e.message);
} else if (e instanceof QTSExecutionError) {
console.error('Execution failed:', e.message);
} else if (e instanceof QTSDownloadError) {
console.error('Download failed:', e.message);
} else if (e instanceof QTSTimeoutError) {
console.error('Stage timed out');
} else if (e instanceof QTSCanceledError) {
console.error('Canceled by signal');
}
}Pass an AbortSignal. The SDK stops polling immediately and, if execution has already started server-side, best-effort calls cancelExecution on the QTSurfer API.
const controller = new AbortController();
setTimeout(() => controller.abort(), 60_000);
await qts.backtest(req, { signal: controller.signal });Polling, retry, backoff, timeout, and cancellation are delegated to cockatiel. Each workflow stage composes a retry policy (exponential backoff on in-progress statuses) with an optional timeout policy. If you need advanced resilience primitives (circuit breakers, bulkheads, fallbacks), import them directly from cockatiel.
-
QTSurferclient over@qtsurfer/api-client -
qts.backtest()orchestrating compile → prepare → execute - Backoff, timeout, and
AbortSignalcancellation viacockatielpolicies - Error hierarchy:
QTSError,QTSStrategyCompileError,QTSPreparationError,QTSExecutionError,QTSTimeoutError,QTSCanceledError
-
Strategyclass with.backtest(),.status() -
BacktestJobclass with.wait(),.cancel(),.stream() - TTL cache for
exchanges/instruments
-
job.stream()returnsAsyncIterator<BacktestProgress> - Server-side hooks (when the backend exposes SSE/WebSocket)
- Helpers to load
signalsUrlParquet into DuckDB / Lastra - Framework adapters (
@qtsurfer/sdk-react,@qtsurfer/sdk-svelte)
src/
├── index.ts # public exports
├── client.ts # QTSurfer class
├── errors.ts # QTSError hierarchy
└── workflows/
├── backtest.ts # compile → prepare → execute (cockatiel policies)
└── downloads.ts # hourly tickers/klines as Lastra/Parquet blobs
| Script | Description |
|---|---|
npm run lint |
Type-check without emitting |
npm run build |
Bundle to dist/ via tsup |
npm test |
Run unit tests |
npm run test:integration |
Run the integration test (requires JWT_API_TOKEN). Set QTSURFER_TEST_VERBOSE=1 to stream progress + final result |
npm run changeset |
Record a changeset for the next release |
npm run changeset:version |
Consume pending changesets: bump package.json and update CHANGELOG.md |
npm run changeset:publish |
Publish released packages to npm (used by CI) |
Versioning and changelogs are managed with changesets:
- Create a changeset describing your change:
npm run changeset - Commit the generated
.changeset/<slug>.mdwith your PR. - When ready to release, run
npm run changeset:versionlocally. It bumpspackage.jsonand appends toCHANGELOG.md. - Commit the version bump, tag
vX.Y.Z, and push the tag; thePublish to npmworkflow handles the rest.
Apache-2.0 — see LICENSE.