Conjuntos de datos
Sube datos históricos de ticker y úsalos en el flujo estándar de backtesting.
Haz backtest contra un CSV, un fichero parquet o un fichero lastra que subes en lugar de contra un exchange
gestionado: crea un conjunto de datos, sube el fichero (PUT) a una URL prefirmada, finalízalo
para disparar la ingesta, y luego prepara/ejecuta exactamente como de
costumbre pero con el exchangeId: user reservado.
| Método | Ruta | Propósito |
|---|---|---|
POST | /datasets | Crear un conjunto de datos + primera sesión de subida |
GET | /datasets | Listar tus conjuntos de datos |
GET | /datasets/{datasetId} | Obtener uno |
DELETE | /datasets/{datasetId} | Eliminar |
POST | /datasets/{datasetId}/uploads | Abrir una nueva sesión de subida para un conjunto de datos existente |
POST | /datasets/{datasetId}/uploads/{uploadId}/finalize | Disparar la ingesta |
GET | /datasets/{datasetId}/uploads/{uploadId} | Sondear el estado de subida/ingesta |
POST | /datasets/imports | Crear un conjunto de datos buscando el histórico en lugar de subirlo |
GET | /datasets/{datasetId}/imports/{importId} | Sondear el estado de búsqueda/ingesta |
v1 es solo datos de ticker — type es siempre "ticker". instrument debe ser un par spot llano
(BASE/QUOTE, exactamente una /); las formas de derivados (BTC/USDT:USDT) se rechazan.
Crear un conjunto de datos
POST /datasets — crea el conjunto de datos y su primera sesión de subida en una sola
llamada: una URL prefirmada a la que tu cliente sube (PUT) el fichero directamente, sin
credenciales de la API implicadas en ese PUT.
| Campo | Tipo | Notas |
|---|---|---|
name | string | obligatorio, único entre tus conjuntos de datos. 409 si ya está en uso |
instrument | string | obligatorio, par spot llano |
curl -X POST https://api.qtsurfer.net/v1/datasets
-H "Authorization: Bearer $TOKEN"
-H "Content-Type: application/json"
-d '{"name":"My BTC ticks","instrument":"BTC/USDT"}' DatasetCreated (201) — los metadatos disponibles inmediatamente tras la creación más la
sesión de subida. Todavía no es el Dataset completo: campos de
ciclo de vida como createdAt, currentVersionId, el rango y la cadencia se obtienen de GET /datasets/{datasetId} tras las etapas correspondientes del ciclo de vida.
{
"datasetId": "ds_3f9a1c2e7b0d4a5f", "name": "My BTC ticks",
"type": "ticker", "instrument": "BTC/USDT",
"uploadId": "up_1a2b3c4d5e6f7a8b",
"upload": {
"url": "https://storage.qtsurfer.com/.../uploads/up_1a2b3c4d5e6f7a8b/raw.csv?X-Amz-...",
"expiresInMinutes": 15
}
} uploadId es lo que pasas a finalizar; upload.url es el destino prefirmado — sube (PUT) el fichero en bruto ahí directamente, sin cabecera Authorization.
¿Perdiste esta respuesta? No se pierde nada — llama a POST .../uploads sobre el id de este conjunto de datos y recuperas exactamente la misma sesión de subida, siempre
que aún no la hayas finalizado.
Errores: 400 petición inválida, o instrument no es un par spot llano · 409 nombre de
conjunto de datos ya en uso · 429 se ha alcanzado el límite de conjuntos de datos de tu plan —
elimina uno, o mejora de plan.
Abrir una nueva sesión de subida
POST /datasets/{datasetId}/uploads — obtén una sesión de subida nueva para un conjunto de datos
que ya tienes: un fichero corregido, o el siguiente tramo de histórico. Mismo contrato de
idempotencia que la mitad de subida propia de POST /datasets: como mucho hay una sesión abierta
por conjunto de datos a la vez, así que volver a llamar a esto antes de finalizar simplemente
devuelve esa misma sesión — seguro de reintentar si se pierde una respuesta. Una vez que una
sesión se ha finalizado (con éxito o no), la siguiente llamada aquí abre una genuinamente nueva.
curl -X POST https://api.qtsurfer.net/v1/datasets/$DATASET_ID/uploads
-H "Authorization: Bearer $TOKEN" 201 — la misma forma {uploadId, upload} que devuelve POST /datasets, sin los metadatos del
conjunto de datos alrededor:
{
"uploadId": "up_1a2b3c4d5e6f7a8b",
"upload": {
"url": "https://storage.qtsurfer.com/.../uploads/up_1a2b3c4d5e6f7a8b/raw.csv?X-Amz-...",
"expiresInMinutes": 15
}
} Errores: 404 no existe ese conjunto de datos para este usuario.
Subir el fichero
CSV, parquet o lastra. Un CSV necesita fila de cabecera; un fichero parquet ya lleva sus
columnas con nombre; un fichero lastra es nuestro propio formato columnar nativo — el mismo que
te devuelve por defecto el dataUrl de un conjunto de datos, así que descargar un conjunto de
datos y entregarle ese mismo fichero a otro usuario para que lo suba funciona sin ninguna
conversión de por medio. Para CSV/parquet, timestamp (ISO-8601, o segundos/milisegundos/
microsegundos de época numéricos — detectado en la primera fila, y exigido después para cada fila
posterior) y close son obligatorias; opcionales: open, high, low, volume, quoteVolume, bid, bidSize, ask, askSize. Una subida lastra lleva su propio conjunto fijo de columnas en
su lugar (ya es una descarga de dataUrl, no un formato que construyas a mano) y solo necesita que
estén presentes una serie de marca de tiempo y una de cierre. La cadencia y la unidad de la marca
de tiempo se descubren a partir de los datos, no se declaran, para las tres.
Una subida CSV se convierte a nuestro formato columnar nativo (lastra) para almacenarla. Una
subida parquet o lastra se guarda hoy tal cual. En ambos casos, consulta dataFormat en la versión lista para saber qué recibes de vuelta
realmente — no lo des por hecho a partir de cómo la subiste (un CSV convertido y un fichero lastra
subido reportan ambos dataFormat: "lastra").
Los bytes que se suben (PUT) a upload.url pueden ser ese fichero directamente, comprimidos en
gzip (.gz), o en zip (.zip, exactamente un fichero dentro — un conjunto de datos es un único
fichero sin importar cómo viaje). El formato se detecta a partir del propio contenido: en este
flujo no hay ni nombre de fichero ni Content-Type con los que un cliente pueda declararlo, así
que no hace falta enviar nada más que los bytes.
curl -X PUT "$UPLOAD_URL" --data-binary @my-btc-ticks.csv
# or gzip/zip it first -- detected from content, no extra parameter needed
curl -X PUT "$UPLOAD_URL" --data-binary @my-btc-ticks.csv.gz Finalizar una subida (disparar la ingesta)
POST /datasets/{datasetId}/uploads/{uploadId}/finalize — llama a esto una vez que el PUT de
arriba se ha completado. Encola la ingesta y devuelve el control de inmediato; sondea GET .../uploads/{uploadId} más abajo. Idempotente mientras la subida
siga abierta — un finalize repetido antes de que haya producido una versión devuelve el mismo jobId en lugar de encolar una segunda ingesta. Una vez que SÍ ha producido una versión, uploadId queda gastado: volver a finalizarlo es un 409, incluso con bytes distintos recién
subidos (PUT) a la misma URL — abre una nueva sesión de subida en lugar de reutilizar una ya gastada.
curl -X POST https://api.qtsurfer.net/v1/datasets/$DATASET_ID/uploads/$UPLOAD_ID/finalize
-H "Authorization: Bearer $TOKEN"
# → 202 {"jobId": "dataset-upload:.../ds_3f9a1c2e7b0d4a5f:up_1a2b3c4d5e6f7a8b"} Errores: 404 no existe ese conjunto de datos para este usuario; uploadId no se emitió para
este conjunto de datos (nunca se acuñó, o se acuñó para otro distinto); o todavía no se ha subido
(PUT) nada a upload.url — un finalize sin nada que finalizar · 409 uploadId ya produjo una
versión (el mensaje de error lo nombra) · 413 el fichero subido es muchas veces el límite de tamaño de tu
plan para un conjunto de datos (consulta Límites de tamaño: el límite en sí se
aplica al tamaño almacenado, que solo se conoce tras la conversión) · 429 se alcanzó o se superaría el límite de almacenamiento total
de tu cuenta (maxTotalStorageBytes de GET /account) — elimina un conjunto de datos
para liberar espacio, o mejora de plan.
Límites de tamaño
maxDatasetBytes (GET /account) limita el tamaño almacenado de una versión de un
conjunto de datos: los bytes que reporta su versión lista, que para una subida CSV es el fichero lastra convertido, no el fichero que subiste. Estima a partir de las filas, no del tamaño del
fichero. El tamaño almacenado sigue a las filas y a las columnas, y un CSV puede salir más grande o
más pequeño que el fichero. Medido con datos sintéticos de un segundo: unos 92 bytes por fila para un
CSV timestamp,close, unos 55 para timestamp,open,high,low,close,volume. Los datos reales se
comprimen de otra manera, así que toma estas cifras como un orden de magnitud y, para un fichero
grande, sube primero un trozo pequeño y escala a partir de sus bytes y rows.
El tamaño almacenado solo se conoce una vez convertido el fichero, así que ahí es donde decide el
límite: POST .../finalize responde 202, y una subida por encima del límite acaba failed cuando
la sondeas, con un error como Dataset is 196976000 bytes, exceeds the tier's 100000000 byte limit. Sondea en lugar de dar por hecho que el 202 significa que se almacenó. Lo
que finalize rechaza por sí mismo con 413 es un fichero muchas veces mayor que el límite, muy por
encima de lo que la conversión podría dejar por debajo.
Sondear la ingesta
GET /datasets/{datasetId}/uploads/{uploadId} — sondea tras finalizar hasta que status sea ready o failed. También informa de uploading (finalize aún no llamado, pero el fichero ya se
subió) antes de que finalices. Se registra de forma duradera una vez existe una versión, así que ready/failed son respuestas permanentes; uploading/ingesting reflejan estado en curso que
puede caducar por sí mismo (ver el caso 404 más abajo).
Respuesta — DatasetUploadState
| Campo | Notas |
|---|---|
status | uploading (fichero subido con PUT, aún no finalizado) → ingesting (finalize llamado, el worker analiza/valida) → ready (version lleva el resultado) | failed (por ejemplo, contrato CSV incorrecto, unidades de marca de tiempo mezcladas, un .zip sin fichero dentro o con más de uno) |
jobId | el id del job de ingesta, mientras status es ingesting |
error | motivo legible, presente cuando status es failed. Se registra de forma duradera junto con el fallo — sigue disponible sondees cuando sondees después |
version | una DatasetVersion, presente cuando status es ready o failed |
DatasetVersion — una subida ingerida con éxito
| Campo | Notas |
|---|---|
id | el id de la versión — pásalo como datasetVersionId en la preparación para fijarla |
bytes | tamaño del fichero almacenado (dataUrl) — un lastra convertido para una subida CSV (descomprimida antes, si llegó como .gz/.zip), o el propio fichero parquet/lastra, sin convertir, para una subida parquet o lastra. No el tamaño de los bytes subidos originalmente (PUT) |
rows | número de filas de datos |
cadence | descubierta a partir de las propias marcas de tiempo de los datos: una cuadrícula fija (1s, 5s, 15s, 1m, 5m, 15m, 30m, 1h, 4h, 1d) cuando al menos la mitad de los intervalos entre filas caen en ese paso (se tolera un pequeño desajuste de reloj), o rt — datos nativos a la tasa en que se capturaron, cada fila con su propia marca de tiempo sin paso fijo (swaps on-chain por operación, ticks espaciados por bloque o de menos de un segundo, intervalos irregulares). Un conjunto de datos rt se puede remuestrear a cualquier cadencia fija al preparar |
timestampUnit | iso | s | ms | us — la unidad en la que llegó la columna timestamp |
gaps, largestGapSteps | número de huecos a la cadencia descubierta, y el tamaño del mayor en pasos de esa cadencia. Siempre 0 para rt |
dataUrl | URL GET prefirmada al fichero almacenado — consulta dataFormat. Presente una vez ready |
dataFormat | lastra (convertido, desde una subida CSV/gzip/zip, o sin convertir, desde una subida lastra — el valor por sí solo no distingue cuál) | parquet (sin convertir, desde una subida parquet) |
curl https://api.qtsurfer.net/v1/datasets/$DATASET_ID/uploads/$UPLOAD_ID
-H "Authorization: Bearer $TOKEN" {
"uploadId": "up_1a2b3c4d5e6f7a8b",
"status": "ready",
"version": {
"datasetId": "ds_3f9a1c2e7b0d4a5f", "id": "dsv_8e2b4f19c6a03d7e",
"bytes": 4831022, "rows": 86400, "cadence": "1s",
"timestampUnit": "iso", "gaps": 0, "largestGapSteps": 0,
"dataUrl": "https://storage.qtsurfer.com/.../dsv_8e2b4f19c6a03d7e/ticker_BTC_USDT_....lastra?X-Amz-...",
"dataFormat": "lastra"
}
} Errores: 404 no existe ese conjunto de datos para este usuario, o realmente no se sabe nada de
este uploadId — sin versión, sin job en curso, nunca se subió (PUT) nada a su URL de subida.
Importar un conjunto de datos en lugar de subirlo
POST /datasets/imports — una segunda forma de meter datos en un conjunto de datos: en lugar de
subir (PUT) un fichero tú mismo, le pides a la API que vaya a buscar el histórico por ti. Crea el
conjunto de datos y arranca la búsqueda en la misma llamada — no hay un paso de subida aparte, y el
resultado acaba siendo una versión de conjunto de datos indistinguible de una subida una vez lista.
type elige la fuente. dex — histórico sobre el propio mercado on-chain de un pool/par — es el
único valor por ahora; otros tipos de fuente se sumarán a este mismo endpoint más adelante. Una
importación dex tiene dos formas de datos, elegidas por la cadence de nivel superior:
- Omitida/en blanco (por defecto) — histórico de swaps on-chain, reproducido directamente desde la propia cadena del pool/par, cada swap con su propia marca de tiempo (cadencia nativa por operación).
1s|1m|5m— velas pre-agregadas a esa anchura en lugar de operaciones en bruto. Eltypedel conjunto de datos resultante esklines, noticker.
| Campo | Tipo | Notas |
|---|---|---|
name | cadena | obligatorio, único entre tus conjuntos de datos. 409 si ya está en uso |
instrument | cadena | obligatorio, par spot llano — la etiqueta propia del conjunto de datos, independiente del orden de los tokens on-chain del pool |
from, to | cadena (fecha-hora) | obligatorio, ISO-8601 UTC. from inclusivo, to exclusivo, from < to. El rango total está limitado por tu plan |
cadence | cadena | opcional. Omitida/en blanco = cadencia nativa por operación (ver abajo). Uno de 1s | 1m | 5m pide velas pre-agregadas a esa anchura en una red que las soporte (cualquier otro valor es 400) — en una red que no las soporte, se ignora silenciosamente, ver abajo |
type | cadena | obligatorio, "dex" es el único valor por ahora |
dex.network | cadena | obligatorio, uno de ethereum | robinhood |
dex.id | cadena | obligatorio salvo que una cadence de velas realmente se resuelva (ver abajo) — una cadence de velas ignorada en una red sin velas sigue exigiendo este campo. "uniswap" es el único valor por ahora — qué protocolo DEX on-chain implementa dex.contract |
dex.version | cadena | obligatorio salvo que una cadence de velas realmente se resuelva (ver abajo). "v2" | "v3" |
dex.contract | cadena | obligatorio, la dirección del contrato del pool (v3) o par (v2) |
dex.factory | cadena | opcional — omite para autodescubrirla on-chain a partir de contract; indícala solo si ya la conoces o el pool/par pertenece a una factory no canónica. En cualquier caso el pool/par se valida contra la factory que se use antes de buscar nada. Se ignora si una cadence de velas realmente se resuelve (ver abajo) |
La cadencia on-chain es nativa, no se remuestrea. Una importación dex normal (sin cadence)
mantiene la propia cadencia de eventos por operación de la fuente — cada swap en la marca de tiempo
en la que ocurrió, así que la cadence de la versión resultante es rt salvo que los swaps caigan
por casualidad en una cuadrícula fija — en lugar de agruparlos en velas; remuestrea a una cadencia
más gruesa después, como un paso aparte, si necesitas una a partir de datos on-chain. Pedir cadence: "1s"/"1m"/"5m" en su lugar te da velas pre-agregadas a esa anchura directamente —
pero solo en una red que realmente tenga una fuente de velas detrás. En cualquier otra red, una cadence de velas se ignora silenciosamente y la importación sigue como si cadence nunca se
hubiera enviado (nativa, dex.id/dex.version obligatorios) — no falla, ni de forma síncrona ni
asíncrona. Qué redes soportan velas hoy no forma parte de este contrato y puede cambiar; si
necesitas saberlo antes de importar, pide la cadencia nativa y comprueba la propia cadence de la
versión resultante en lugar de asumirlo.
curl -X POST https://api.qtsurfer.net/v1/datasets/imports
-H "Authorization: Bearer $TOKEN"
-H "Content-Type: application/json"
-d '{
"name": "weth-usdc-week",
"instrument": "WETH/USDC",
"from": "2026-08-01T00:00:00Z",
"to": "2026-08-08T00:00:00Z",
"type": "dex",
"dex": {
"network": "ethereum",
"id": "uniswap",
"version": "v3",
"contract": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640"
}
}'
# → 202 {"datasetId":"ds_3f9a1c2e7b0d4a5f","importId":"imp_01j9z...","jobId":"dataset-import:...","status":"fetching"} O, para velas pre-agregadas en lugar de swaps on-chain en bruto:
curl -X POST https://api.qtsurfer.net/v1/datasets/imports
-H "Authorization: Bearer $TOKEN"
-H "Content-Type: application/json"
-d '{
"name": "weth-usdc-1s",
"instrument": "WETH/USDC",
"from": "2026-08-01T00:00:00Z",
"to": "2026-08-01T06:00:00Z",
"cadence": "1s",
"type": "dex",
"dex": {
"network": "ethereum",
"contract": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640"
}
}' importId es con lo que sondeas, más abajo — no hay un paso de “finalizar” aparte como en una
subida.
Errores: 400 petición inválida, instrument no es un par spot llano, from >= to, cadence presente pero no uno de sus valores soportados, el rango excede el límite de importación de tu
plan, la estimación aproximada de tamaño del rango excede el límite de filas de tu plan, dex.network/dex.id no es uno de sus valores soportados, dex.contract/dex.factory fallan la
validación básica de forma, o (cuando se omite cadence) falta dex.id/dex.version (si el
pool/par realmente se resuelve, y para una cadence de velas si esa combinación se puede servir en
la red pedida, se comprueba después, de forma asíncrona — ver failed abajo) · 409 nombre de
conjunto de datos ya en uso · 429 se alcanzó el límite de conjuntos de datos de tu plan, o ya se
alcanzó el límite de almacenamiento total de tu cuenta (maxTotalStorageBytes de GET /account) — elimina un conjunto de datos para liberar hueco o espacio, o mejora de
plan.
Sondear una importación
GET /datasets/{datasetId}/imports/{importId} — sondea tras POST /datasets/imports hasta que status sea ready o failed. Una importación pasa tiempo real buscando en su fuente antes de
que se aloje nada; una vez buscada, reentra exactamente en la misma cadena de ingesta que usa una
subida.
Respuesta — DatasetImportState
| Campo | Notas |
|---|---|
status | fetching (leyendo de la fuente, nada alojado todavía — el único estado que solo reporta una importación) → ingesting (buscado, alojado, el worker analiza/valida) → ready (version lleva el resultado) | failed |
jobId | el id del job de búsqueda/ingesta, mientras status es fetching o ingesting |
error | motivo legible, presente cuando status es failed — un pool/par que no se resuelve, sin datos en el rango pedido, un rango más antiguo de lo que retiene la fuente, la búsqueda superando el límite de tiempo de tu plan, o cualquiera de los motivos del lado de ingesta que puede llevar DatasetUploadState.error, una vez que la búsqueda pasa el testigo a esa misma cadena. Se registra de forma duradera, igual que en la vía de subida |
version | una DatasetVersion, presente cuando status es ready o failed |
curl https://api.qtsurfer.net/v1/datasets/$DATASET_ID/imports/$IMPORT_ID
-H "Authorization: Bearer $TOKEN" {
"importId": "imp_01j9z1x2y3z4a5b6c7d8e9f0g1",
"status": "ready",
"version": {
"datasetId": "ds_3f9a1c2e7b0d4a5f", "id": "dsv_8e2b4f19c6a03d7e",
"bytes": 4831022, "rows": 604800, "cadence": "rt",
"timestampUnit": "us", "gaps": 0, "largestGapSteps": 0,
"dataUrl": "https://storage.qtsurfer.com/.../dsv_8e2b4f19c6a03d7e/ticker_WETH_USDC_....lastra?X-Amz-...",
"dataFormat": "lastra"
}
} Errores: 404 no existe ese conjunto de datos para este usuario, o realmente no se sabe nada de
este importId.
Forma del conjunto de datos
Tanto GET /datasets como GET /datasets/{datasetId} devuelven esto — from/to/cadence/ timestampUnit reflejan el rango, la cadencia y la unidad de marca de tiempo propios descubiertos
de la versión actual, y status, bytes, rows, gaps y largestGapSteps dicen si es
utilizable y qué tamaño tiene, así que no necesitas una segunda llamada para ver qué cubre un
conjunto de datos.
| Campo | Notas |
|---|---|
datasetId, name, type ("ticker" | "klines"), instrument, createdAt, status | siempre presentes. type es "klines" solo para una importación dex que pidió una cadence de velas; "ticker" para todo lo demás (subidas, e importaciones dex de cadencia nativa) |
currentVersionId | la versión finalizada e ingerida con éxito más reciente. Ausente hasta que al menos una subida ha terminado de ingerirse |
status | ready — currentVersionId está fijado y los campos de abajo lo describen; failed — el intento de subida o importación más reciente falló, no hay nada que leer todavía (consulta error); pending — nunca se intentó nada (recién creado, o una subida nunca se finalizó) |
updatedAt | cuándo cambió currentVersionId por última vez; ausente hasta que tiene un valor |
from, to, cadence, timestampUnit | el rango/cadencia/unidad de marca de tiempo propios de la versión actual (una cuadrícula fija o cadencia rt, iso|s|ms|us para timestampUnit, consulta DatasetVersion), tal como se descubrieron en la ingesta. Ausentes hasta que existe una versión |
bytes, rows, gaps, largestGapSteps | el tamaño almacenado, el número de filas, el número de huecos y el mayor hueco (en pasos de su cadencia) propios de la versión actual. Presentes solo cuando status es ready — consulta DatasetVersion para saber qué mide bytes exactamente |
error | por qué falló el intento más reciente. Presente solo cuando status es failed |
GET /datasets/{datasetId} por sí solo añade dataUrl/dataFormat (con el mismo significado que
en DatasetVersion) una vez que la versión
actual está ready, más _links.self. El listado masivo no los emite nunca — una URL de descarga
prefirmada para cada conjunto de datos en una pantalla que no dibuja ninguna gráfica no compensa
la exposición.
Listar tus conjuntos de datos
GET /datasets — todos los conjuntos de datos que has creado y no has eliminado, los creados más
recientemente primero. Nunca un 404 — un array vacío si no tienes ninguno, la misma
convención que GET /strategies.
curl https://api.qtsurfer.net/v1/datasets -H "Authorization: Bearer $TOKEN" GET /datasets?includeDeleted=true lista también los conjuntos de datos que has eliminado, cada uno
con el deletedAt en que se eliminó — útil para mantener tu propia copia de la lista sincronizada,
de modo que un conjunto eliminado aparezca como eliminado en lugar de limitarse a desaparecer.
Obtener un conjunto de datos
GET /datasets/{datasetId} — un Dataset más un _links.self.
curl https://api.qtsurfer.net/v1/datasets/$DATASET_ID -H "Authorization: Bearer $TOKEN" Errores: 404 no existe ese conjunto de datos para este usuario.
Eliminar un conjunto de datos
DELETE /datasets/{datasetId} — eliminación suave. Deja de aparecer en los endpoints de listado/
obtención y ya no puede usarse para preparar, pero sus datos de objeto se recuperan más tarde en
lugar de purgarse en el acto, así que un backtest ya en marcha contra una de sus versiones no se
interrumpe.
curl -X DELETE https://api.qtsurfer.net/v1/datasets/$DATASET_ID -H "Authorization: Bearer $TOKEN"
# → {"datasetId": "ds_3f9a1c2e7b0d4a5f", "deleted": true} Errores: 404 no existe ese conjunto de datos para este usuario, o ya está eliminado.
Hacer backtest contra un conjunto de datos
Una vez que una versión está ready, prepara y ejecuta exactamente igual que contra un exchange
gestionado, pero con exchangeId: user y datasetId en lugar de instrument:
curl -X POST https://api.qtsurfer.net/v1/backtest/user/ticker/prepare
-H "Authorization: Bearer $TOKEN"
-H "Content-Type: application/json"
-d '{"datasetId":"ds_3f9a1c2e7b0d4a5f","from":"2026-03-14","to":"2026-03-15"}'
# → 202 {"jobId":"5ikYAMIO...","datasetId":"ds_3f9a1c2e7b0d4a5f","datasetVersionId":"dsv_8e2b4f19c6a03d7e"} execute no cambia — el mismo cuerpo de petición que contra un exchange gestionado, ya que el
instrumento y el rango se recuperan de prepareJobId en cualquier caso. Consulta docs/backtest_execute.md para la referencia completa de prepare/execute,
incluidos los campos datasetId/datasetVersionId de PrepareRequest y la forma de cobertura
respaldada por conjunto de datos en PrepareJobState (cadence/gaps/largestGapSteps en lugar
de los totalHours/hoursWithData/hoursWithoutData recorridos por hora).