@qtsurfer/sdk
v0.2.0
Published
Opinionated TypeScript SDK for QTSurfer: workflow orchestration, domain objects, normalized errors
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
import { QTSurfer } from '@qtsurfer/sdk';
import { readFileSync } from 'node:fs';
const qts = new QTSurfer({
baseUrl: 'https://api.qtsurfer.com/v1',
token: process.env.QTSURFER_TOKEN,
});
const controller = new AbortController();
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,
},
{
signal: controller.signal,
onProgress: (p) => console.log(`[${p.stage}] ${p.percent?.toFixed(1) ?? '-'}%`),
pollIntervalMs: 500,
maxPollIntervalMs: 5000,
timeoutMs: 10 * 60 * 1000,
},
);
console.log('PnL:', result.pnlTotal);
console.log('Trades:', result.totalTrades);
console.log('Signals:', result.signalsUrl);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 cancelExecution 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.
