@qtsurfer/sdk
v0.6.0
Published
Opinionated TypeScript SDK for QTSurfer: workflow orchestration, domain objects, normalized errors
Downloads
614
Maintainers
Readme
@qtsurfer/sdk
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.
Installation
npm install @qtsurfer/sdk
# or
pnpm add @qtsurfer/sdkQuick start
One call: API key in, ready-to-use session out. JWT refresh on 401 is handled for you.
import { authenticate } from '@qtsurfer/sdk';
import { readFileSync } from 'node:fs';
// Reads QTSURFER_APIKEY from env when no argument is passed.
const qts = await authenticate();
// Or: const qts = await authenticate('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);Environment
| Variable | Purpose |
| ------------------ | ------------------------------------------------------------ |
| QTSURFER_APIKEY | API key consumed by authenticate() when no arg is passed |
Pluggable token storage
Tokens are kept in memory by default. Implement TokenStore to swap in
browser storage, a file, or a secret manager:
import { authenticate, 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 authenticate(undefined, { store: browserStore });authenticate() also accepts { baseUrl, fetch } for staging, custom HTTP
transports, or a Node-fetch polyfill in legacy runtimes.
Lower-level: hand-managed JWT
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,
});What backtest() does
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.
Hourly tickers/klines downloads
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).
Error hierarchy
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');
}
}Cancellation
Pass an AbortSignal. The SDK stops polling immediately and, if execution has already started server-side, best-effort calls cancelBacktest on the QTSurfer API.
const controller = new AbortController();
setTimeout(() => controller.abort(), 60_000);
await qts.backtest(req, { signal: controller.signal });Under the hood
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.
Roadmap
v0.1 — Core workflow ✅
- [x]
QTSurferclient over@qtsurfer/api-client - [x]
qts.backtest()orchestrating compile → prepare → execute - [x] Backoff, timeout, and
AbortSignalcancellation viacockatielpolicies - [x] Error hierarchy:
QTSError,QTSStrategyCompileError,QTSPreparationError,QTSExecutionError,QTSTimeoutError,QTSCanceledError
v0.2 — Domain objects
- [ ]
Strategyclass with.backtest(),.status() - [ ]
BacktestJobclass with.wait(),.cancel(),.stream() - [ ] TTL cache for
exchanges/instruments
v0.3 — Streaming progress
- [ ]
job.stream()returnsAsyncIterator<BacktestProgress> - [ ] Server-side hooks (when the backend exposes SSE/WebSocket)
v0.4 — Ecosystem integration
- [ ] Helpers to load
signalsUrlParquet into DuckDB / Lastra - [ ] Framework adapters (
@qtsurfer/sdk-react,@qtsurfer/sdk-svelte)
Project layout
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 blobsDevelopment
| 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) |
Releasing
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.
License
Apache-2.0 — see LICENSE.
