QTSurfer API Client · Python
Auto-generated Python client for the QTSurfer API, built from the OpenAPI 3.1 spec with openapi-python-client on top of httpx.
pip install qtsurfer-api-client
Intentionally thin: one function per endpoint, 1:1 with the spec. For workflow orchestration (polling, retries, domain objects, unified errors), use qtsurfer-sdk (coming soon).
- Sync-first, httpx-powered — same call site shape as the Java/TS siblings.
- Spec-driven — generated sources fetched from
QTSurfer/qtsurfer-apibyscripts/regenerate.sh. - Fully typed —
py.typedmarker,mypy --strictclean (consumers see precise types for every request/response). - Python 3.11+ — modern type hints, no compat shims.
Installation
pip install qtsurfer-api-client
Or with uv:
uv add qtsurfer-api-client
Quick start
import os
from qtsurfer.api.client import AuthenticatedClient
from qtsurfer.api.client.api.exchange import list_exchanges, list_instruments
client = AuthenticatedClient(
base_url="https://api.qtsurfer.com/v1",
token=os.environ["QTSURFER_TOKEN"],
)
exchanges = list_exchanges.sync(client=client)
for ex in exchanges or []:
print(ex.id, ex.name)
instruments = list_instruments.sync(client=client, exchange_id="binance")
print(f"{len(instruments.data)} instruments on binance ({instruments.meta.segment.value})")
API key → JWT
Every endpoint above expects a short-lived JWT in the Authorization: Bearer …
header. Exchange a long-lived API key for one via authenticate:
import os
from qtsurfer.api.client import AuthenticatedClient
from qtsurfer.api.client.api.auth import authenticate
# AuthenticatedClient also drives the apikey header — set prefix="" so it
# sends `X-API-Key: <key>` instead of `Authorization: Bearer <key>`.
apikey_client = AuthenticatedClient(
base_url="https://api.qtsurfer.com/v1",
token=os.environ["QTSURFER_APIKEY"],
prefix="",
auth_header_name="X-API-Key",
)
token_response = authenticate.sync(client=apikey_client)
jwt = token_response.access_token # use this in subsequent calls
For production use, prefer the qtsurfer-sdk
auth(apikey) helper — it handles token refresh, env-var pickup
(QTSURFER_APIKEY), and pluggable token storage so callers don't reinvent any
of it on top of the raw client.
Each generated endpoint module exposes four entrypoints:
| Function | Returns |
|---|---|
sync(...) |
parsed model (or None on a defined error response) |
sync_detailed(...) |
full Response[...] (status, headers, parsed, content) |
asyncio(...) |
parsed model, awaitable |
asyncio_detailed(...) |
full Response[...], awaitable |
API surface
| Module | Operation | Method · Path |
|---|---|---|
api.auth |
authenticate |
POST /auth/token — exchange API key for a short-lived JWT |
api.exchange |
list_exchanges |
GET /exchanges |
api.exchange |
list_instruments |
GET /exchange/{exchangeId}/instruments (default spot segment) |
api.exchange |
list_segment_instruments |
GET /exchange/{exchangeId}/{segment}/instruments |
api.exchange |
download_tickers |
GET /exchange/{exchangeId}/tickers/{base}/{quote} |
api.exchange |
download_klines |
GET /exchange/{exchangeId}/klines/{base}/{quote} |
api.strategy |
get_strategy |
GET /strategy/{strategyId} |
api.strategy |
list_strategies |
GET /strategies |
api.strategy |
delete_strategy |
DELETE /strategy/{strategyId} |
api.strategy |
get_strategy_code |
GET /strategy/{strategyId}/code |
api.strategy |
validate_strategy |
POST /strategy/{strategyId}/validate |
api.backtesting |
prepare_backtest |
POST /backtest/{exchangeId}/{type}/prepare |
api.backtesting |
get_prepare_status |
GET /backtest/{exchangeId}/{type}/prepare/{jobId} |
api.backtesting |
execute_backtest |
POST /backtest/{exchangeId}/{type}/execute |
api.backtesting |
cancel_backtest |
DELETE /backtest/{exchangeId}/{type}/execute/{jobId} |
api.backtesting |
get_backtest_result |
GET /backtest/{exchangeId}/{type}/execute/{jobId} |
api.backtesting |
execute_sweep |
POST /backtest/{exchangeId}/{type}/executeSweep/{requestId} |
api.backtesting |
get_sweep_result |
GET /backtest/{exchangeId}/{type}/executeSweep/{requestId}/{sweepId} |
api.backtesting |
cancel_sweep |
DELETE /backtest/{exchangeId}/{type}/executeSweep/{requestId}/{sweepId} |
api.backtesting |
get_sweep_sensitivity |
GET /backtest/{exchangeId}/{type}/executeSweep/{requestId}/{sweepId}/sensitivity |
api.dataset |
create_dataset |
POST /datasets — create a dataset and get a presigned upload URL |
api.dataset |
list_datasets |
GET /datasets |
api.dataset |
get_dataset |
GET /datasets/{datasetId} |
api.dataset |
delete_dataset |
DELETE /datasets/{datasetId} |
api.dataset |
finalize_dataset_upload |
POST /datasets/{datasetId}/uploads/{uploadId}/finalize |
api.dataset |
get_dataset_upload |
GET /datasets/{datasetId}/uploads/{uploadId} |
Twenty-six of the spec's twenty-seven operations, all reachable through qtsurfer.api.client.api
as listed. The exception is compileStrategy (POST /strategy), whose text/plain request body
openapi-python-client does not support, so no module is generated for it — call it through the
underlying httpx client.
Datasets — backtest against your own data
Upload CSV ticker data and prepare/execute a backtest against it via the reserved
exchangeId: user value on the existing prepare_backtest/execute_backtest endpoints:
create_dataset returns a datasetId plus a presigned URL to PUT the CSV to directly, then
finalize_dataset_upload kicks off ingest and get_dataset_upload polls until status is
ready or failed. PrepareRequest.instrument is optional for this reason — pass dataset_id
(and optionally dataset_version_id to pin a specific past version) instead when the exchange is
user. list_datasets/get_dataset never 404 for "none yet", same convention as
list_strategies; delete_dataset is a soft delete that doesn't disrupt a backtest already
running against one of the dataset's versions.
Exact module/function names are produced from
operationIdin the OpenAPI spec. Runscripts/regenerate.shto refresh and checksrc/qtsurfer/api/client/_generated/api/for the authoritative listing.
All generated model types (Exchange, InstrumentDetail, InstrumentCoverage, CoverageWindow, JobState, PrepareJobState, StrategyState, StrategyLinks, BacktestJobResult, ResultMap, ResponseError, Dataset, DatasetWithLinks, DatasetCreated, DatasetVersion, DatasetUploadState, …) live under qtsurfer.api.client.models. list_instruments/list_segment_instruments return an InstrumentListResponse (HAL envelope: data + meta + _links), not a bare list — each InstrumentDetail.coverage carries per-data-type CoverageWindows instead of flat dataFrom/dataTo. A single-instrument get_prepare_status returns a PrepareJobState — always terminal (status: Completed), with a coverage_ratio and a per-hour hours_without_data breakdown to act on instead of polling. Against a dataset-backed prepare (exchangeId: user), PrepareJobState reports coverage on the dataset's own cadence grid instead — cadence/gaps/largest_gap_steps — with total_hours/hours_with_data/hours_without_data absent in that case. get_strategy returns a StrategyState, whose validation field (not_validated / pending / passed / failed) reports the outcome of the most recent validate_strategy check rather than a compile job status.
A full StrategyState (from get_strategy, and from validate_strategy's already-validated 200) carries an optional field_links (_links on the wire) — a StrategyLinks with a code: HalLink pointing at get_strategy_code. validate_strategy's 202 omits it, since a check that just started has nothing to link to yet. list_strategies returns every strategy you've registered and not deleted, most recently compiled first, but deliberately without each one's validation state — check that per strategy with get_strategy. delete_strategy removes a strategy from both get_strategy and list_strategies; it doesn't touch backtests already run against it, and re-submitting the same source afterwards registers a new strategy under a new id. get_strategy_code's 404 covers two indistinguishable cases: an id never registered by you, or one that resolves only through a shared/marketplace reference with no source of its own.
POST /strategy(compileStrategy) is currently omitted by the generator because the spec declares its request body astext/plainandopenapi-python-clientonly emits JSON / form / multipart bodies. Call it directly via the underlyinghttpxclient (client.get_httpx_client().post("/strategy", content=src, headers={"Content-Type": "text/plain"})) until the spec is restructured. The otherapi.strategyoperations have no such restriction and generate normally.
Binary downloads (/exchange/{ex}/tickers|klines/{base}/{quote})
These endpoints return raw Lastra bytes (default) or Parquet (format=parquet). The generated sync() helpers parse the response body as JSON and will raise on binary payloads; use sync_detailed() and read response.content directly:
from qtsurfer.api.client import AuthenticatedClient
from qtsurfer.api.client.api.exchange import download_tickers
client = AuthenticatedClient(base_url="https://api.qtsurfer.com/v1", token=token)
response = download_tickers.sync_detailed(
client=client,
exchange_id="binance",
base="BTC",
quote="USDT",
hour="2026-01-15T10",
)
with open("BTC_USDT_2026-01-15_h10.lastra", "wb") as f:
f.write(response.content)
For very large segments, drop down to the underlying httpx.Client (client.get_httpx_client()) and stream:
with client.get_httpx_client().stream(
"GET",
"/exchange/binance/klines/BTC/USDT",
params={"hour": "2026-01-15T10", "format": "parquet"},
) as r:
r.raise_for_status()
with open("out.parquet", "wb") as f:
for chunk in r.iter_bytes():
f.write(chunk)
Configuring the client
Both Client and AuthenticatedClient accept the standard hooks of the upstream generator:
from qtsurfer.api.client import AuthenticatedClient
client = AuthenticatedClient(
base_url="https://api.qtsurfer.com/v1",
token=token,
timeout=httpx.Timeout(30.0),
verify_ssl=True,
headers={"X-Request-Id": "..."},
raise_on_unexpected_status=True,
)
Need per-call customisation (e.g. swap the underlying httpx.Client for one with a custom transport)? Use client.with_httpx_client(my_httpx_client) or client.set_httpx_client(...).
Regenerating the client
The src/qtsurfer/api/client/_generated/ directory is a committed build artifact produced from the OpenAPI spec hosted at QTSurfer/qtsurfer-api. Never hand-edit it.
uv sync # install pinned dev deps
./scripts/regenerate.sh # fetch spec + regenerate + sync pyproject version
uv run ruff check src/ tests/
uv run mypy src/
uv run pytest -v
Generator configuration lives in codegen.config.yaml. The spec URL is hard-coded in scripts/regenerate.sh; point it at a tag/commit for fully reproducible builds.
Development
| Command | Description |
|---|---|
uv sync |
Install dependencies from uv.lock |
./scripts/regenerate.sh |
Re-fetch spec + regenerate client |
uv run ruff check src/ tests/ |
Lint |
uv run ruff format src/ tests/ |
Format |
uv run mypy src/ |
Type-check (--strict) |
uv run pytest -v |
Run tests |
uv run python -m build |
Build wheel + sdist into dist/ |
Versioning
pyproject.toml's version field is kept in lockstep with the info.version of the OpenAPI spec by scripts/regenerate.sh. Tags pushed to main (vX.Y.Z) trigger the PyPI publish workflow via OIDC trusted publishing.
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_api_client-0.110.1.tar.gz.
File metadata
- Download URL: qtsurfer_api_client-0.110.1.tar.gz
- Upload date:
- Size: 69.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
89d89634ed7a5efe1b0e5faf0f2a91b374b42a623d61ab6d4fa05d9fd538add7
|
|
| MD5 |
0f4044f1a1aa28d3895438f9ee52b335
|
|
| BLAKE2b-256 |
26ffff7ee6a0e37e75796ce4c3d3dc6e3ae79426c6a8e376a58244ae70e6de2f
|
Provenance
The following attestation bundles were made for qtsurfer_api_client-0.110.1.tar.gz:
Publisher:
publish.yml on QTSurfer/api-client-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
qtsurfer_api_client-0.110.1.tar.gz -
Subject digest:
89d89634ed7a5efe1b0e5faf0f2a91b374b42a623d61ab6d4fa05d9fd538add7 - Sigstore transparency entry: 2586028206
- Sigstore integration time:
-
Permalink:
QTSurfer/api-client-python@aab9e89307acda0a9105000609f5c9ce6d1591d3 -
Branch / Tag:
refs/tags/v0.110.1 - Owner: https://github.com/QTSurfer
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@aab9e89307acda0a9105000609f5c9ce6d1591d3 -
Trigger Event:
push
-
Statement type:
File details
Details for the file qtsurfer_api_client-0.110.1-py3-none-any.whl.
File metadata
- Download URL: qtsurfer_api_client-0.110.1-py3-none-any.whl
- Upload date:
- Size: 163.1 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 |
d9b065dd4664d2f6c339430db799ca4bc09346b66e40dc167ee35d86a985ab92
|
|
| MD5 |
6f2218b799ef5a94a90d661af522d090
|
|
| BLAKE2b-256 |
ae54ac4d53b1db6469ca41c5b929452dde6eb4dadc4d2a653c218527fad34b13
|
Provenance
The following attestation bundles were made for qtsurfer_api_client-0.110.1-py3-none-any.whl:
Publisher:
publish.yml on QTSurfer/api-client-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
qtsurfer_api_client-0.110.1-py3-none-any.whl -
Subject digest:
d9b065dd4664d2f6c339430db799ca4bc09346b66e40dc167ee35d86a985ab92 - Sigstore transparency entry: 2586028838
- Sigstore integration time:
-
Permalink:
QTSurfer/api-client-python@aab9e89307acda0a9105000609f5c9ce6d1591d3 -
Branch / Tag:
refs/tags/v0.110.1 - Owner: https://github.com/QTSurfer
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@aab9e89307acda0a9105000609f5c9ce6d1591d3 -
Trigger Event:
push
-
Statement type: