Skip to content

QTSurfer/sdk-ts

Repository files navigation

@qtsurfer/sdk

CI npm License

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/sdk

Quick start

One 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);

Environment

Variable Purpose
QTSURFER_APIKEY API key consumed by auth() 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 { 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.

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:

  1. Compile the strategy (POST /strategy in async mode) and poll GET /strategy/{id} until Completed.
  2. Prepare the data range (POST /backtest/{exchange}/ticker/prepare) and poll until Completed.
  3. Execute the backtest (POST /backtest/{exchange}/ticker/execute) and poll GET /backtest/.../execute/{jobId} until Completed.
  4. 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 ✅

  • QTSurfer client over @qtsurfer/api-client
  • qts.backtest() orchestrating compile → prepare → execute
  • Backoff, timeout, and AbortSignal cancellation via cockatiel policies
  • Error hierarchy: QTSError, QTSStrategyCompileError, QTSPreparationError, QTSExecutionError, QTSTimeoutError, QTSCanceledError

v0.2 — Domain objects

  • Strategy class with .backtest(), .status()
  • BacktestJob class with .wait(), .cancel(), .stream()
  • TTL cache for exchanges / instruments

v0.3 — Streaming progress

  • job.stream() returns AsyncIterator<BacktestProgress>
  • Server-side hooks (when the backend exposes SSE/WebSocket)

v0.4 — Ecosystem integration

  • Helpers to load signalsUrl Parquet 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 blobs

Development

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:

  1. Create a changeset describing your change: npm run changeset
  2. Commit the generated .changeset/<slug>.md with your PR.
  3. When ready to release, run npm run changeset:version locally. It bumps package.json and appends to CHANGELOG.md.
  4. Commit the version bump, tag vX.Y.Z, and push the tag; the Publish to npm workflow handles the rest.

License

Apache-2.0 — see LICENSE.

About

Opinionated TypeScript SDK for QTSurfer — workflow orchestration, domain objects, normalized errors over the API client.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors