qtsurfer-sdk
Opinionated Python SDK for QTSurfer, built on top of
qtsurfer-api-client.
Where qtsurfer-api-client gives you one function per API endpoint, this package
adds auth helpers, token refresh, pluggable token storage, and
high-level workflows (strategies, backtest, sweep, datasets) — go from an API
key to a typed backtest in a few lines.
The strategy code itself stays on the JVM — QTSurfer's backtest engine is Java. This SDK is for orchestration: minting tokens, calling endpoints, processing results.
Guides
The hand-written guides mirror the SDK family structure.
- Authentication
- Exchanges and instruments
- Strategies and validation
- Backtests and parameter sweeps
- Dataset uploads
- API coverage
Installation
pip install qtsurfer-sdk
# or, with uv:
uv add qtsurfer-sdk
Requires Python 3.11+. The transitive qtsurfer-api-client
(auto-generated from the OpenAPI spec) comes along automatically.
Import path: this package imports as
qtsurfer_sdk(sibling to the auto-generatedqtsurfer.api.client.*tree). Two top-level names keep the SDK and the raw client cleanly separate.
Quick start
from qtsurfer_sdk import auth
# Reads QTSURFER_APIKEY from env when no argument is passed.
session = auth()
# Or point at a different API base (defaults to production):
# session = auth("ak_...", base_url="https://api.qtsurfer.net/v1")
exchanges = session.list_exchanges()
for ex in exchanges:
print(ex.id, ex.name)
JWT refresh on 401 is handled for you (refresh once, retry once).
Workflows
The SDK exposes a workflow surface mirroring sdk-java / sdk-ts. Every method
is routed through the session (refresh-on-401) and returns the api-client's
typed model objects.
Strategy
src = '''public class EmaCross extends AbstractTickerStrategy { ... }'''
comp = session.compile_strategy(src) # CompileStrategyResponse200
sid = comp.strategy_id
session.validate_strategy(sid) # 202/pending or already-recorded verdict
state = session.get_strategy(sid) # validation, notices, requiredSources
session.list_strategies() # your registered strategies
code = session.get_strategy_code(sid) # read back the exact source
session.delete_strategy(sid) # release it
Catalog
session.list_exchanges() # [Exchange, ...]
session.list_instruments("binance") # 1876 instruments
session.list_instruments("binance", segment="spot") # a specific segment
Backtest
# 1. prepare a data window -> a job id
acc = session.prepare(
exchange_id="binance", type_="ticker",
instrument="BTC/USDT", from_="2026-08-18", to="2026-08-19",
)
pid = acc.job_id
# 2. poll until Completed
import time
while True:
st = session.get_prepare_status(exchange_id="binance", type_="ticker", job_id=pid)
if st.status == "Completed":
break
time.sleep(3)
# 3. execute a compiled strategy over the prepared window
ex = session.execute(
exchange_id="binance", type_="ticker",
prepare_job_id=pid, strategy_id=sid,
)
jid = ex.job_id
# 4. poll the raw result (202 while running; parse results only on 200)
while True:
resp = session.get_backtest_result(exchange_id="binance", type_="ticker", job_id=jid)
if resp.status_code == 202:
time.sleep(3)
continue
resp.raise_for_status()
results = resp.json()["results"] # pnl, totalTrades, sharpeRatio, equityCurve...
break
Sweep
accepted = session.sweep(
exchange_id="binance", type_="ticker",
request_id=pid, # the prepare job id
strategy_id=sid,
params={"cycle.seconds": {"values": [10, 20, 30]}}, # note: annotation name, not field
objective="sharpe",
)
swid = accepted.sweep_id
res = session.get_sweep_result(exchange_id="binance", type_="ticker", request_id=pid, sweep_id=swid)
for row in res.leaderboard:
print(row.rank, row.sharpe, row.params)
session.get_sweep_sensitivity(exchange_id="binance", type_="ticker", request_id=pid, sweep_id=swid)
session.get_sweep_run_equity_curve(..., run_ix=0) # a retained trial's curve
session.cancel_sweep(...)
Datasets (bring your own data)
created = session.create_dataset(name="My BTC ticks", instrument="BTC/USDT")
# created.dataset_id, created.upload_id, created.upload.url (presigned R2 URL)
# PUT your CSV to created.upload.url (no auth header needed), then:
session.finalize_dataset_upload(dataset_id=created.dataset_id, upload_id=created.upload_id)
# poll:
state = session.get_dataset_upload(dataset_id=created.dataset_id, upload_id=created.upload_id)
# state.status == "ready" -> backtest against it with exchange_id="user":
acc = session.prepare(
exchange_id="user", type_="ticker", dataset_id=created.dataset_id,
dataset_version_id=state.version.id, from_=..., to=...,
)
session.list_datasets()
session.get_dataset(dataset_id)
session.delete_dataset(dataset_id)
To upload the file itself, pass the creation result or an upload session together
with a pathlib.Path (or an open binary file) to upload_dataset_file. The PUT
goes directly to the presigned URL without the session JWT or API key. A successful
PUT only stores the bytes; call finalize_dataset_upload to queue ingest.
from pathlib import Path
session.upload_dataset_file(created, Path("BTC_USDT.csv"))
session.finalize_dataset_upload(dataset_id=created.dataset_id, upload_id=created.upload_id)
# Add a subsequent version after the earlier upload has finalized:
next_upload = session.open_dataset_upload(created.dataset_id)
session.upload_dataset_file(next_upload, Path("BTC_USDT_corrected.csv"))
open_dataset_upload is safe to retry while an upload is open: it returns the
same session. Once an upload has produced a version, its upload_id is spent;
finalize_dataset_upload returns 409 and a new session is required.
Raw client access
Every workflow goes through the session's underlying generated
AuthenticatedClient. To call an endpoint the workflows don't wrap, or to
inspect a raw Response:
from qtsurfer.api.client._generated.api.exchange import get_exchanges
session.call(lambda c: get_exchanges.sync(client=c)) # plain
resp = session.call(lambda c: get_exchanges.sync_detailed(client=c)) # raw Response
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 (a Protocol) to
back them by file, secret manager, or keychain:
import json
from pathlib import Path
from qtsurfer.api.client._generated.models import AuthTokenResponse
from qtsurfer_sdk import TokenStore, auth
class FileStore(TokenStore):
def __init__(self, path: Path): self.path = path
def load(self) -> AuthTokenResponse | None:
return AuthTokenResponse.from_dict(json.loads(self.path.read_text())) if self.path.exists() else None
def save(self, token: AuthTokenResponse) -> None: self.path.write_text(json.dumps(token.to_dict()))
def clear(self) -> None: self.path.unlink(missing_ok=True)
session = auth(store=FileStore(Path.home() / ".qtsurfer" / "token.json"))
Error hierarchy
from qtsurfer_sdk import QTSError, QTSAuthError, QTSPreparationError, QTSExecutionError
QTSError— base for all SDK errors (carries optional.status).QTSAuthError— missing/invalid apikey, or non-2xx fromPOST /auth/token.QTSCompileError—POST /strategycould not compile/register.QTSPreparationError— apreparestage failed.QTSExecutionError— a backtest/sweep execution failed.QTSTimeoutError,QTSCanceledError,QTSDownloadError— reserved (poll deadline, cancel, downloads).
Roadmap
- v0.1 — auth helper ✅
- v0.2 — high-level workflows ✅ (strategies, catalog, backtest, sweep, datasets)
- v0.3 — async overload mirroring
*_asyncioapi-client variants - v0.4 — domain handles (
Strategy,Backtest) with progress callbacks
License
Apache-2.0 — see LICENSE.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file qtsurfer_sdk-0.2.0.tar.gz.
File metadata
- Download URL: qtsurfer_sdk-0.2.0.tar.gz
- Upload date:
- Size: 23.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fc82887ad19e8b16a6596b0f004f97d95e0b2106ec158ab3997e7222a6db3c78
|
|
| MD5 |
4b3c7ff8c1d4c078013634f18201cc60
|
|
| BLAKE2b-256 |
d295a7b13e3cc0f8b84957726257b09754f481fe98991f29419acb6772736664
|
Provenance
The following attestation bundles were made for qtsurfer_sdk-0.2.0.tar.gz:
Publisher:
publish.yml on QTSurfer/sdk-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
qtsurfer_sdk-0.2.0.tar.gz -
Subject digest:
fc82887ad19e8b16a6596b0f004f97d95e0b2106ec158ab3997e7222a6db3c78 - Sigstore transparency entry: 2629113166
- Sigstore integration time:
-
Permalink:
QTSurfer/sdk-python@a434d24cf98f70deee751eeae201ca2f91794d49 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/QTSurfer
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@a434d24cf98f70deee751eeae201ca2f91794d49 -
Trigger Event:
push
-
Statement type:
File details
Details for the file qtsurfer_sdk-0.2.0-py3-none-any.whl.
File metadata
- Download URL: qtsurfer_sdk-0.2.0-py3-none-any.whl
- Upload date:
- Size: 19.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
434286856969c0622c6ea13a808ef5060592ff633cae7f5ff760aaecead362a4
|
|
| MD5 |
40856fba5404f11384b315a8c88c52d0
|
|
| BLAKE2b-256 |
6801c72b3654e2bd69af735e731dfc571ea69c3ec722647441298e8ef57131e5
|
Provenance
The following attestation bundles were made for qtsurfer_sdk-0.2.0-py3-none-any.whl:
Publisher:
publish.yml on QTSurfer/sdk-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
qtsurfer_sdk-0.2.0-py3-none-any.whl -
Subject digest:
434286856969c0622c6ea13a808ef5060592ff633cae7f5ff760aaecead362a4 - Sigstore transparency entry: 2629113495
- Sigstore integration time:
-
Permalink:
QTSurfer/sdk-python@a434d24cf98f70deee751eeae201ca2f91794d49 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/QTSurfer
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@a434d24cf98f70deee751eeae201ca2f91794d49 -
Trigger Event:
push
-
Statement type: