{
	"openapi": "3.1.0",
	"info": {
		"title": "QTSurfer API",
		"summary": "QTSurfer backend services API",
		"version": "0.111.2",
		"contact": {
			"name": "QTSurfer Support",
			"email": "support@qtsurfer.com",
			"url": "https://qtsurfer.com/support"
		},
		"termsOfService": "https://qtsurfer.com/terms",
		"license": {
			"name": "Apache-2.0",
			"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
		}
	},
	"servers": [
		{
			"url": "https://api.qtsurfer.net/v1",
			"description": "Staging — the API this specification describes, and the one to develop against today. Generated clients take their default base URL from here."
		},
		{
			"url": "https://api.qtsurfer.com/v1",
			"description": "Production — reserved, not yet serving. It is listed so the eventual address is known in advance; pointing a client at it today will not reach the API. Stay on staging until this one is announced."
		}
	],
	"paths": {
		"/auth/token": {
			"post": {
				"operationId": "authenticate",
				"summary": "Exchange API key for a short-lived JWT",
				"description": "Exchanges a long-lived API key for a short-lived JWT used by every other\nendpoint. This is the only endpoint that accepts an API key directly —\ncallers should obtain a JWT here, then send it as `Authorization: Bearer\n<token>` to all other operations.\n\nThe returned JWT carries the caller's subscription `tier` as a claim and\nexpires after `expires_in` seconds. Callers should refresh the token\nbefore expiry (or on a `401` response) by calling this endpoint again.\n",
				"responses": {
					"200": {
						"description": "API key accepted; JWT returned.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/AuthTokenResponse"
								},
								"example": {
									"access_token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI3NmI5MDIwMy0wM2MyLTQ2ZjYtYjM2Ni05OTQ0ZjE2N2U4MTgiLCJzY29wZXMiOltdLCJ0aWVyIjoiZnJlZSIsImlhdCI6MTc3OTczNTQ2MiwiZXhwIjoxNzc5NzM5MDYyfQ.signature",
									"token_type": "Bearer",
									"expires_in": 3600,
									"scopes": [],
									"tier": "free"
								}
							}
						}
					},
					"401": {
						"description": "API key is invalid, revoked, or expired.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/AuthTokenError"
								},
								"example": {
									"code": "invalid_apikey",
									"message": "API key not recognized"
								}
							}
						}
					},
					"429": {
						"description": "Rate limit exceeded for this API key."
					}
				},
				"security": [
					{
						"apiKeyAuth": []
					}
				],
				"tags": [
					"Auth"
				]
			}
		},
		"/exchanges": {
			"get": {
				"operationId": "listExchanges",
				"summary": "List the available exchanges",
				"responses": {
					"200": {
						"description": "A JSON array of Exchanges",
						"content": {
							"application/json": {
								"schema": {
									"type": "array",
									"items": {
										"$ref": "#/components/schemas/Exchange"
									}
								},
								"example": [
									{
										"id": "binance",
										"name": "Binance",
										"description": "Binance cryptocurrency exchange"
									}
								]
							}
						}
					}
				},
				"tags": [
					"Exchange"
				]
			}
		},
		"/exchange/{exchangeId}/instruments": {
			"get": {
				"operationId": "listInstruments",
				"summary": "List an exchange's instruments (default spot segment)",
				"description": "\"Give me binance instruments\" — returns the exchange's DEFAULT segment (`spot`)\nin `data`, each instrument with per-data-type coverage and market info. `meta`\nconfirms the served `segment` (`spot`); HAL `_links` carry `self` plus the\n`spot` / `futures` segment-discovery links.\n",
				"parameters": [
					{
						"name": "exchangeId",
						"in": "path",
						"description": "ID of the exchange to retrieve instruments for",
						"required": true,
						"schema": {
							"type": "string",
							"example": "binance"
						}
					}
				],
				"responses": {
					"200": {
						"description": "The default (spot) segment's instruments in `data`, `meta`, and HAL `_links` (self + spot/futures segment discovery)",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/InstrumentListResponse"
								},
								"example": {
									"data": [
										{
											"id": "BTC/USDT",
											"base": "BTC",
											"quote": "USDT",
											"coverage": {
												"tickers": {
													"from": "2026-04-10T21:00:00Z",
													"to": "2026-07-09T20:29:05Z"
												},
												"klines": {
													"from": "2026-04-22T17:00:00Z",
													"to": "2026-07-09T20:31:08Z"
												}
											},
											"lastPrice": 84250.5,
											"volume24h": 1234567.89
										}
									],
									"meta": {
										"updatedAt": "2026-07-09T19:09:07Z",
										"exchange": "binance",
										"segment": "spot"
									},
									"_links": {
										"self": {
											"href": "/v1/exchange/binance/instruments"
										},
										"spot": {
											"href": "/v1/exchange/binance/spot/instruments"
										},
										"futures": {
											"href": "/v1/exchange/binance/futures/instruments"
										}
									}
								}
							}
						}
					},
					"404": {
						"description": "Exchange not found or instrument catalog not available",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ResponseError"
								}
							}
						}
					}
				},
				"tags": [
					"Exchange"
				]
			}
		},
		"/exchange/{exchangeId}/{segment}/instruments": {
			"get": {
				"operationId": "listSegmentInstruments",
				"summary": "List an exchange segment's instruments",
				"description": "Returns the instruments for one market segment of the exchange, each with\nper-data-type coverage and market info. HAL `_links` carry `self` plus the\n`spot` / `futures` segment-discovery links; the default-segment shortcut is\n`GET /exchange/{exchangeId}/instruments` (spot).\n",
				"parameters": [
					{
						"name": "exchangeId",
						"in": "path",
						"description": "ID of the exchange to retrieve instruments for",
						"required": true,
						"schema": {
							"type": "string",
							"example": "binance"
						}
					},
					{
						"name": "segment",
						"in": "path",
						"description": "Market segment to list instruments for",
						"required": true,
						"schema": {
							"type": "string",
							"enum": [
								"spot",
								"futures"
							],
							"example": "spot"
						}
					}
				],
				"responses": {
					"200": {
						"description": "An object with a `data` array of instrument details (each with per-data-type coverage) and a `meta` block",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/InstrumentListResponse"
								},
								"example": {
									"data": [
										{
											"id": "BTC/USDT",
											"base": "BTC",
											"quote": "USDT",
											"coverage": {
												"tickers": {
													"from": "2026-04-10T21:00:00Z",
													"to": "2026-07-09T20:29:05Z"
												},
												"klines": {
													"from": "2026-04-22T17:00:00Z",
													"to": "2026-07-09T20:31:08Z"
												}
											},
											"lastPrice": 84250.5,
											"volume24h": 1234567.89
										},
										{
											"id": "ETH/USDT",
											"base": "ETH",
											"quote": "USDT",
											"coverage": {
												"tickers": {
													"from": "2026-04-10T21:00:00Z",
													"to": "2026-07-09T20:28:22Z",
													"inactiveSince": "2026-07-09T20:28:22Z"
												},
												"klines": {
													"from": "2026-04-22T17:00:00Z",
													"to": "2026-07-09T20:31:09Z",
													"inactiveSince": "2026-07-09T20:31:09Z"
												}
											},
											"lastPrice": 3120.75,
											"volume24h": 456789.12
										}
									],
									"meta": {
										"updatedAt": "2026-07-09T19:09:07Z",
										"exchange": "binance",
										"segment": "spot"
									},
									"_links": {
										"self": {
											"href": "/v1/exchange/binance/spot/instruments"
										},
										"spot": {
											"href": "/v1/exchange/binance/spot/instruments"
										},
										"futures": {
											"href": "/v1/exchange/binance/futures/instruments"
										}
									}
								}
							}
						}
					},
					"404": {
						"description": "Exchange, segment, or instrument catalog not found",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ResponseError"
								}
							}
						}
					}
				},
				"tags": [
					"Exchange"
				]
			}
		},
		"/exchange/{exchangeId}/tickers/{base}/{quote}": {
			"get": {
				"operationId": "downloadTickers",
				"summary": "Download one hour of tickers for an instrument as a Lastra segment",
				"description": "Serves exactly one hour of raw ticker data for the given instrument on the\nrequested exchange. The payload is a native [Lastra](https://github.com/QTSurfer/lastra-java)\nfile — QTSurfer's columnar format for tick-precision timeseries — with\nno JSON envelope.\n\nOne segment = one hour, aligned to UTC. The `hour` query parameter selects\nthe segment and must match `YYYY-MM-DDTHH` (no minutes/seconds, no timezone\nsuffix). Example: `hour=2026-01-15T10` returns `h10.lastra` for\n2026-01-15, covering `[10:00:00Z, 11:00:00Z)`. Hours not yet available\nreturn `404`.\n\nA `format=parquet` query parameter switches the response to on-the-fly\nParquet conversion via [lastra-convert](https://github.com/QTSurfer/lastra-convert)\nfor clients that don't yet read Lastra. Lastra is the primary format\nand cheaper when the client can consume it.\n\nClients:\n- [lastra-java](https://github.com/QTSurfer/lastra-java) — reference\n  Java reader/writer with per-column codecs (ALP, Gorilla, delta-varint,\n  ZSTD) and CRC32 integrity.\n- [lastra-ts](https://github.com/QTSurfer/lastra-ts) — TypeScript reader\n  (~4 kB bundle, browser + Node.js).\n- [duckdb-lastra](https://github.com/QTSurfer/duckdb-lastra) — DuckDB\n  extension for ad-hoc SQL over Lastra files.\n- [lastra-convert](https://github.com/QTSurfer/lastra-convert) — CLI + Java\n  API for converting to/from Parquet, Reef, and CSV.\n- `curl -OJ` for offline dumps (the `Content-Disposition` header sets a\n  descriptive filename).\n",
				"parameters": [
					{
						"name": "exchangeId",
						"in": "path",
						"required": true,
						"description": "ID of the exchange (e.g. `binance`).",
						"schema": {
							"type": "string",
							"example": "binance"
						}
					},
					{
						"name": "base",
						"in": "path",
						"required": true,
						"description": "Base asset symbol (first leg of the pair).",
						"schema": {
							"type": "string",
							"example": "BTC"
						}
					},
					{
						"name": "quote",
						"in": "path",
						"required": true,
						"description": "Quote asset symbol (second leg of the pair).",
						"schema": {
							"type": "string",
							"example": "USDT"
						}
					},
					{
						"name": "hour",
						"in": "query",
						"required": true,
						"description": "Hour selector in `YYYY-MM-DDTHH` (UTC). The returned segment covers\n`[HH:00:00Z, HH+1:00:00Z)`.\n",
						"schema": {
							"type": "string",
							"pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}$",
							"example": "2026-01-15T10"
						}
					},
					{
						"name": "format",
						"in": "query",
						"required": false,
						"description": "Response wire format. `lastra` (default) returns raw Lastra bytes.\n`parquet` returns Parquet via on-the-fly conversion using\n[lastra-convert](https://github.com/QTSurfer/lastra-convert).\n",
						"schema": {
							"type": "string",
							"enum": [
								"lastra",
								"parquet"
							],
							"default": "lastra",
							"example": "lastra"
						}
					}
				],
				"responses": {
					"200": {
						"description": "One hour of tickers for the instrument. `Content-Type` is\n`application/vnd.lastra` by default or\n`application/vnd.apache.parquet` when `format=parquet` was\nrequested.\n",
						"headers": {
							"Content-Disposition": {
								"description": "Attachment filename `{BASE}_{QUOTE}_{YYYY-MM-DD}_h{HH}.{ext}`\nwhere `{ext}` is `lastra` or `parquet` to match the format.\n",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/vnd.lastra": {
								"schema": {
									"type": "string",
									"format": "binary"
								}
							},
							"application/vnd.apache.parquet": {
								"schema": {
									"type": "string",
									"format": "binary"
								}
							}
						}
					},
					"400": {
						"description": "Missing or malformed parameters (e.g. `hour` not `YYYY-MM-DDTHH`).",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ResponseError"
								}
							}
						}
					},
					"404": {
						"description": "No Lastra segment exists for the requested instrument/hour.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ResponseError"
								}
							}
						}
					},
					"500": {
						"description": "Unexpected I/O error serving the file.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ResponseError"
								}
							}
						}
					}
				},
				"tags": [
					"Exchange"
				]
			}
		},
		"/exchange/{exchangeId}/klines/{base}/{quote}": {
			"get": {
				"operationId": "downloadKlines",
				"summary": "Download one hour of klines for an instrument as a Lastra segment",
				"description": "Same shape and semantics as `/exchange/{exchangeId}/tickers/{base}/{quote}`,\nbut serves klines (aggregated bars) instead of raw ticks. One\n[Lastra](https://github.com/QTSurfer/lastra-java) segment = one hour of\nklines at the exchange's native kline cadence, aligned to UTC.\n\nKlines use the same columnar layout as tickers — readers that handle one\nformat read the other with the same code. Use this endpoint when a\nper-tick payload would be too large for the window of interest.\n",
				"parameters": [
					{
						"name": "exchangeId",
						"in": "path",
						"required": true,
						"description": "ID of the exchange (e.g. `binance`).",
						"schema": {
							"type": "string",
							"example": "binance"
						}
					},
					{
						"name": "base",
						"in": "path",
						"required": true,
						"description": "Base asset symbol.",
						"schema": {
							"type": "string",
							"example": "BTC"
						}
					},
					{
						"name": "quote",
						"in": "path",
						"required": true,
						"description": "Quote asset symbol.",
						"schema": {
							"type": "string",
							"example": "USDT"
						}
					},
					{
						"name": "hour",
						"in": "query",
						"required": true,
						"description": "Hour selector in `YYYY-MM-DDTHH` (UTC). The returned segment covers\n`[HH:00:00Z, HH+1:00:00Z)`.\n",
						"schema": {
							"type": "string",
							"pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}$",
							"example": "2026-01-15T10"
						}
					},
					{
						"name": "format",
						"in": "query",
						"required": false,
						"description": "Response wire format. `lastra` (default) returns raw Lastra bytes.\n`parquet` returns Parquet via on-the-fly conversion.\n",
						"schema": {
							"type": "string",
							"enum": [
								"lastra",
								"parquet"
							],
							"default": "lastra",
							"example": "lastra"
						}
					}
				],
				"responses": {
					"200": {
						"description": "One hour of klines for the instrument. `Content-Type` is\n`application/vnd.lastra` by default or\n`application/vnd.apache.parquet` when `format=parquet`.\n",
						"headers": {
							"Content-Disposition": {
								"description": "Attachment filename\n`{BASE}_{QUOTE}_{YYYY-MM-DD}_h{HH}_klines.{ext}`\nwhere `{ext}` is `lastra` or `parquet` to match the format.\n",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/vnd.lastra": {
								"schema": {
									"type": "string",
									"format": "binary"
								}
							},
							"application/vnd.apache.parquet": {
								"schema": {
									"type": "string",
									"format": "binary"
								}
							}
						}
					},
					"400": {
						"description": "Missing or malformed parameters (e.g. `hour` not `YYYY-MM-DDTHH`).",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ResponseError"
								}
							}
						}
					},
					"404": {
						"description": "No Lastra segment exists for the requested instrument/hour.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ResponseError"
								}
							}
						}
					},
					"500": {
						"description": "Unexpected I/O error serving the file.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ResponseError"
								}
							}
						}
					}
				},
				"tags": [
					"Exchange"
				]
			}
		},
		"/strategies": {
			"get": {
				"operationId": "listStrategies",
				"summary": "List your registered strategies",
				"description": "Every strategy you have registered and not deleted, most recently compiled first.\n\nEach entry carries the same provenance `GET /strategy/{strategyId}` does — `compiledAt`,\n`requiredSources` — but not its validation state, so listing stays cheap regardless of how\nmany strategies you have. Check a specific strategy's validation with `GET\n/strategy/{strategyId}`.\n",
				"responses": {
					"200": {
						"description": "Your registered strategies. An empty array if you have none — this is never a `404`.\n",
						"content": {
							"application/json": {
								"schema": {
									"type": "object",
									"required": [
										"strategies"
									],
									"properties": {
										"strategies": {
											"type": "array",
											"items": {
												"$ref": "#/components/schemas/StrategySummary"
											}
										}
									}
								},
								"example": {
									"strategies": [
										{
											"strategyId": "6bsh31ikwkuivhtgcoa6s4",
											"compiledAt": "2026-08-19T10:15:00Z",
											"requiredSources": [
												"Ticker"
											]
										},
										{
											"strategyId": "2ul144qe9tlwzu5anhwvc6",
											"compiledAt": "2026-08-12T09:02:11Z"
										}
									]
								}
							}
						}
					}
				},
				"security": [
					{
						"bearerAuth": []
					}
				],
				"tags": [
					"Strategy"
				]
			}
		},
		"/strategy": {
			"post": {
				"operationId": "compileStrategy",
				"summary": "Compile and register a strategy",
				"description": "Compiles raw strategy source and registers it, returning its `strategyId`.\n\n**This answers one question: is the source valid Java.** It compiles, registers, and hands\nback the id — nothing more. Whether the class can actually run is\n`POST /strategy/{strategyId}/validate`, and everything known about a strategy, validation\nincluded, is read from `GET /strategy/{strategyId}`. One place to ask, so there is no second\nanswer to keep in step.\n\nThe `strategyId` is derived from what the code *means*, not from how it is written. Adding a\ncomment, inserting a blank line, re-indenting, reordering imports, or moving a method around\nall return the **same** id — you have not created a second strategy. Renaming a variable,\nchanging an identifier's case, reordering fields, or reordering statements inside a method\nreturn a **different** one.\n\nTwo rules follow, and they are worth designing around:\n\n- re-submitting a strategy you have only reformatted is free, and gives you back the id you\n  already had, along with any validation already recorded against it;\n- the id says nothing about *behaviour*. Two sources that compute the same thing by\n  different means are two strategies, because deciding otherwise would mean deciding program\n  equivalence.\n\nThe response also lists `declaredProperties` — the sweep/execute param keys this strategy\nis known to accept, so a caller can catch a typo'd key before submitting a sweep instead of\nonly learning it from a rejected one. See `DeclaredProperty`: best-effort, not exhaustive.\n",
				"requestBody": {
					"description": "The raw strategy source code",
					"required": true,
					"content": {
						"text/plain": {
							"schema": {
								"type": "string",
								"description": "Raw strategy Java source code"
							}
						}
					}
				},
				"responses": {
					"200": {
						"description": "Compiled and registered",
						"content": {
							"application/json": {
								"schema": {
									"type": "object",
									"required": [
										"strategyId"
									],
									"properties": {
										"strategyId": {
											"$ref": "#/components/schemas/strategyId"
										},
										"declaredProperties": {
											"type": "array",
											"description": "What could be established about this strategy's sweep-key vocabulary without\nconstructing it. See `DeclaredProperty` — best-effort, not exhaustive.\n",
											"items": {
												"$ref": "#/components/schemas/DeclaredProperty"
											}
										}
									}
								},
								"example": {
									"strategyId": "6bsh31ikwkuivhtgcoa6s4",
									"declaredProperties": [
										{
											"name": "rsi.period",
											"description": "RSI period",
											"defaultValue": "14",
											"reflected": true,
											"min": 2,
											"max": 50,
											"step": 1
										},
										{
											"name": "enabled",
											"description": "Enabled",
											"reflected": true
										}
									]
								}
							}
						}
					},
					"400": {
						"description": "The source is not valid Java; the message carries the compiler diagnostics. Nothing is\nregistered, so there is no id to look up afterwards.\n",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ResponseError"
								},
								"example": {
									"code": 400,
									"message": "Line 1, Column 18: syntax error"
								}
							}
						}
					},
					"429": {
						"description": "Too many compilations in flight. Retry later.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ResponseError"
								}
							}
						}
					}
				},
				"security": [
					{
						"bearerAuth": []
					}
				],
				"tags": [
					"Strategy"
				]
			}
		},
		"/strategy/{strategyId}/validate": {
			"post": {
				"operationId": "validateStrategy",
				"summary": "Check that a registered strategy can actually run",
				"description": "Instantiates the compiled class and drives it through a bounded synthetic series, so a wiring\nfault surfaces here instead of at your first backtest. The verdict — pass or fail, plus any\nengine notices — is recorded and served from `GET /strategy/{strategyId}`.\n\n**Idempotent.** If a verdict already exists for the current compilation it comes straight\nback with `200` and nothing is queued. Otherwise the check is queued and this returns `202`;\npoll `GET /strategy/{strategyId}` until `validation` is `passed` or `failed`.\n\nRecompiling supersedes a verdict, which makes this callable again — the old answer described\nbytecode that is no longer what would run.\n",
				"parameters": [
					{
						"name": "strategyId",
						"in": "path",
						"required": true,
						"description": "The id returned by `POST /strategy`",
						"schema": {
							"$ref": "#/components/schemas/strategyId"
						}
					}
				],
				"responses": {
					"200": {
						"description": "Already validated; the recorded verdict, unchanged",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/StrategyState"
								}
							}
						}
					},
					"202": {
						"description": "Validation queued. Not a terminal outcome — poll `GET /strategy/{strategyId}` until\n`validation` leaves `pending`.\n\nThe body is a `StrategyState` carrying only what is known at this point: the id and\n`validation: pending`. **The status code, not the body, is what tells the two responses\napart** — a `200` can also carry `validation: pending`, left by a check an earlier call\nqueued. So `202` means *this call started a check*, while `pending` means only *a check\nis outstanding*.\n",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/StrategyState"
								},
								"example": {
									"strategyId": "6bsh31ikwkuivhtgcoa6s4",
									"validation": "pending"
								}
							}
						}
					},
					"404": {
						"description": "No such registered strategy for this user",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ResponseError"
								}
							}
						}
					}
				},
				"security": [
					{
						"bearerAuth": []
					}
				],
				"tags": [
					"Strategy"
				]
			}
		},
		"/strategy/{strategyId}": {
			"get": {
				"operationId": "getStrategy",
				"summary": "Get a strategy by id, including its validation state",
				"description": "Reports that the strategy is registered — implied by a `200` at all — and what validating it\nfound.\n\nA `404` means one thing: no such registered strategy for this user. It is never a stale or\nexpired answer; registration and verdict are stored durably, not cached.\n",
				"parameters": [
					{
						"name": "strategyId",
						"in": "path",
						"required": true,
						"description": "The id returned by `POST /strategy`",
						"schema": {
							"$ref": "#/components/schemas/strategyId"
						}
					}
				],
				"responses": {
					"200": {
						"description": "Strategy state",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/StrategyState"
								}
							}
						}
					},
					"404": {
						"description": "No such registered strategy for this user",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ResponseError"
								}
							}
						}
					}
				},
				"security": [
					{
						"bearerAuth": []
					}
				],
				"tags": [
					"Strategy"
				]
			},
			"delete": {
				"operationId": "deleteStrategy",
				"summary": "Release a registered strategy",
				"description": "Removes a strategy from `GET /strategy/{strategyId}` and `GET /strategies`. This is not\nundone by re-submitting the same source to `POST /strategy` — that registers a new\nstrategy, with a new id.\n\n**Backtests you already ran against this strategy are unaffected.** Deleting it stops it\nfrom counting against your account and stops you from validating or re-running it under\nthis id — it does not erase what already happened.\n\nOnly removes a strategy you registered yourself. If you copied someone else's strategy\n(a shared/marketplace listing), deleting your copy never affects theirs, or anyone else's.\n",
				"parameters": [
					{
						"name": "strategyId",
						"in": "path",
						"required": true,
						"description": "The id returned by `POST /strategy`",
						"schema": {
							"$ref": "#/components/schemas/strategyId"
						}
					}
				],
				"responses": {
					"200": {
						"description": "Deleted",
						"content": {
							"application/json": {
								"schema": {
									"type": "object",
									"required": [
										"strategyId",
										"deleted"
									],
									"properties": {
										"strategyId": {
											"$ref": "#/components/schemas/strategyId"
										},
										"deleted": {
											"type": "boolean",
											"enum": [
												true
											]
										}
									}
								},
								"example": {
									"strategyId": "6bsh31ikwkuivhtgcoa6s4",
									"deleted": true
								}
							}
						}
					},
					"404": {
						"description": "No such registered strategy for this user",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ResponseError"
								}
							}
						}
					}
				},
				"security": [
					{
						"bearerAuth": []
					}
				],
				"tags": [
					"Strategy"
				]
			}
		},
		"/strategy/{strategyId}/code": {
			"get": {
				"operationId": "getStrategyCode",
				"summary": "Get a registered strategy's source, if you still have one to read",
				"description": "The exact source you last submitted for this id — the same text `POST /strategy` derives\n`strategyId` from, whitespace and comments included.\n\n**\"If available\", not \"always\".** A strategy you resolve only through a shared/marketplace\nlisting you copied by reference carries no source of its own, and reads as a `404` here the\nsame as a `strategyId` you never registered — that is the honest answer either way, since\nfrom this endpoint's point of view nothing is there to return.\n",
				"parameters": [
					{
						"name": "strategyId",
						"in": "path",
						"required": true,
						"description": "The id returned by `POST /strategy`",
						"schema": {
							"$ref": "#/components/schemas/strategyId"
						}
					}
				],
				"responses": {
					"200": {
						"description": "The registered source",
						"content": {
							"application/json": {
								"schema": {
									"type": "object",
									"required": [
										"strategyId",
										"code"
									],
									"properties": {
										"strategyId": {
											"$ref": "#/components/schemas/strategyId"
										},
										"code": {
											"type": "string",
											"description": "Raw strategy Java source code, exactly as registered."
										}
									}
								},
								"example": {
									"strategyId": "6bsh31ikwkuivhtgcoa6s4",
									"code": "package strategy;\npublic class EmaCrossStrategy extends AbstractTickerStrategy { ... }\n"
								}
							}
						}
					},
					"404": {
						"description": "No such registered strategy for this user, or nothing to read for this id",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ResponseError"
								}
							}
						}
					}
				},
				"security": [
					{
						"bearerAuth": []
					}
				],
				"tags": [
					"Strategy"
				]
			}
		},
		"/backtest/{exchangeId}/{type}/prepare": {
			"post": {
				"operationId": "prepareBacktest",
				"summary": "Prepare backtest data",
				"description": "Enqueues a prepare task over the requested date range. Returns immediately with a `jobId`;\npoll `GET /backtest/{exchangeId}/{type}/prepare/{jobId}` for completion.\n\nThe same params always return the same `jobId` (idempotent). Repeated calls with identical\nparams do not enqueue duplicate work — they reuse the existing job.\n\n**`exchangeId: user` is reserved for your own uploaded data.** Instead of a managed\nexchange, it prepares from a dataset you created via `POST /datasets` (see the **Dataset**\nendpoints) — send `datasetId` in place of `instrument`. See `PrepareRequest` below for the\ntwo request shapes.\n",
				"parameters": [
					{
						"name": "exchangeId",
						"in": "path",
						"description": "ID of the exchange to prepare the backtesting for (e.g. `binance`), or the reserved\nvalue `user` to prepare from a dataset you uploaded instead of a managed exchange.\n",
						"required": true,
						"schema": {
							"type": "string",
							"example": "binance"
						}
					},
					{
						"name": "type",
						"in": "path",
						"description": "The type of data source to prepare from",
						"required": true,
						"schema": {
							"$ref": "#/components/schemas/DataSourceType"
						}
					}
				],
				"requestBody": {
					"description": "The required data to prepare a backtesting",
					"required": true,
					"content": {
						"application/json": {
							"schema": {
								"$ref": "#/components/schemas/PrepareRequest"
							}
						}
					}
				},
				"responses": {
					"202": {
						"description": "Prepare task accepted (queued for processing)",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/AcceptedJob"
								}
							}
						}
					},
					"400": {
						"description": "Invalid request or parameters. Also returned when `from` is older than the configured\nlookback window or `to` is in the future. For `exchangeId: user`, also returned when\nthe dataset's current upload has not finished ingesting, or `cadence` asks for a finer\ngranularity than the dataset's own discovered cadence, or the requested range exceeds\nyour tier's range limit.\n",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ResponseError"
								}
							}
						}
					},
					"404": {
						"description": "Exchange or data source type not found. For `exchangeId: user`, also returned when\n`datasetId` (or a pinned `datasetVersionId`) doesn't exist or isn't yours.\n",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ResponseError"
								}
							}
						}
					},
					"429": {
						"description": "Rate limited. Returned when the global queue exceeds capacity or the user has too many\nactive backtests. Does not apply to `exchangeId: user` — a dataset prepare reads an\nalready-ingested file rather than enqueueing worker capacity.\n",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ResponseError"
								},
								"example": {
									"code": 429,
									"message": "Too many active backtests. Wait for some to complete."
								}
							}
						}
					}
				},
				"security": [
					{
						"bearerAuth": []
					}
				],
				"tags": [
					"Backtesting"
				]
			}
		},
		"/backtest/{exchangeId}/{type}/prepare/{jobId}": {
			"get": {
				"operationId": "getPrepareStatus",
				"summary": "Get the status of a prepare job",
				"description": "Retrieves the current state of the prepare job identified by `jobId`.\nPoll until `status` is `Completed`, `Failed`, or `Aborted`.\n\nFor a dataset prepare (`exchangeId: user`), coverage is reported against the dataset's own\ncadence grid instead of hours — see `cadence`/`gaps`/`largestGapSteps` on `PrepareJobState`.\n",
				"parameters": [
					{
						"name": "exchangeId",
						"in": "path",
						"description": "ID of the exchange for the backtesting process, or the reserved value `user` for a\ndataset-backed prepare.\n",
						"required": true,
						"schema": {
							"type": "string",
							"example": "binance"
						}
					},
					{
						"name": "type",
						"in": "path",
						"description": "The type of data source to prepare from",
						"required": true,
						"schema": {
							"$ref": "#/components/schemas/DataSourceType"
						}
					},
					{
						"name": "jobId",
						"in": "path",
						"description": "Job ID returned by `POST /prepare`",
						"required": true,
						"schema": {
							"type": "string",
							"example": "13RBLGQlPnfDjO6wyKSX8i"
						}
					}
				],
				"responses": {
					"200": {
						"description": "Current prepare job state",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/PrepareJobState"
								},
								"example": {
									"contextId": "ctx_0bjmoxd4vahkgc0hnvdldh",
									"status": "Completed",
									"statusDetail": null,
									"size": 0,
									"completed": 24,
									"startTime": "2026-04-14T15:00:00Z",
									"endTime": "2026-04-14T15:00:01Z",
									"dataFrom": "2026-04-14T13:00:00Z",
									"dataTo": "2026-04-14T15:30:05Z",
									"coverageRatio": 0.994,
									"totalHours": 168,
									"hoursWithData": 167,
									"hoursWithoutData": [
										{
											"hour": "2026-04-14T02:00:00Z",
											"expected": 0,
											"rationale": "low_activity"
										}
									]
								}
							}
						}
					},
					"400": {
						"description": "Invalid request or parameters",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ResponseError"
								}
							}
						}
					},
					"404": {
						"description": "Prepare job not found or expired",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ResponseError"
								}
							}
						}
					}
				},
				"security": [
					{
						"bearerAuth": []
					}
				],
				"tags": [
					"Backtesting"
				]
			}
		},
		"/backtest/{exchangeId}/{type}/executeSweep/{requestId}": {
			"post": {
				"operationId": "executeSweep",
				"summary": "Execute a parameter sweep over prepared data",
				"description": "Runs a parameter matrix over the single immutable dataset identified by `requestId`.\nThe backend expands and executes the matrix internally; clients poll the returned\n`sweepId` for incremental results.\n\nSupplying `walkForward` runs the sweep in a different mode entirely. Instead of scoring\nevery parameter vector once over the whole range, the data is split into F sequential\nfolds; each fold optimizes the full grid on its own window and then scores only its winner\non the window immediately after — data that winner was not chosen on. It answers a harder\nquestion than a leaderboard: not \"which parameters won\", but \"does re-optimizing this\nperiodically actually work\". Omit the block and nothing changes, including the response.\n\nThe cost is the reason it is opt-in rather than always on: F folds × N vectors, so a\n4-fold run over a 500-point grid is 2004 backtests where the plain sweep is 500. The\nrequest is rejected when `folds × totalRuns` exceeds the server's sweep budget.\n",
				"parameters": [
					{
						"name": "exchangeId",
						"in": "path",
						"required": true,
						"schema": {
							"type": "string",
							"example": "binance"
						}
					},
					{
						"name": "type",
						"in": "path",
						"required": true,
						"schema": {
							"$ref": "#/components/schemas/DataSourceType"
						}
					},
					{
						"name": "requestId",
						"in": "path",
						"required": true,
						"description": "Job ID returned by `POST /backtest/{exchangeId}/{type}/prepare`.",
						"schema": {
							"type": "string"
						}
					}
				],
				"requestBody": {
					"required": true,
					"content": {
						"application/json": {
							"schema": {
								"$ref": "#/components/schemas/ExecuteSweepRequest"
							}
						}
					}
				},
				"responses": {
					"202": {
						"description": "Sweep accepted. The effective seed is returned for reproducibility.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ExecuteSweepAccepted"
								}
							}
						}
					},
					"400": {
						"description": "Invalid sweep specification or the expanded grid exceeds the server limit.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ResponseError"
								}
							}
						}
					},
					"404": {
						"description": "Prepared request not found or expired.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ResponseError"
								}
							}
						}
					},
					"429": {
						"description": "Sweep queue or user concurrency limit reached.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ResponseError"
								}
							}
						}
					}
				},
				"security": [
					{
						"bearerAuth": []
					}
				],
				"tags": [
					"Backtesting"
				]
			}
		},
		"/backtest/{exchangeId}/{type}/executeSweep/{requestId}/{sweepId}": {
			"get": {
				"operationId": "getSweepResult",
				"summary": "Get sweep progress and results",
				"description": "Returns incremental sweep progress. The default `ranked` view sorts and may truncate the\ndisplay leaderboard. `order=natural` returns every available row, untruncated, ordered by\ndeterministic `runIx`; use that view when materialising durable trial rows.\n\nThe `ranked` view is ordered by **plateau score** by default, not by the raw objective. A\nplateau score is the objective of the worst run in a parameter point's immediate\nneighbourhood, so a point scores well only if the region around it also does — the highest\nraw score is frequently a spike that does not survive the parameters moving slightly. Pass\n`ranking=raw` for the unadjusted objective order.\n\nRows in the `ranked` view carry `plateauScore` and `neighbourCount` when plateau ranking\napplied. Read them together: `neighbourCount: 0` means the point had no neighbours to\ncompare against, so its plateau score is unevidenced rather than confirmed. Sweeps\nsubmitted before plateau ranking existed have no stored parameter grid to rebuild a\nneighbourhood from and are always ranked raw; the response's `ranking` field says which\nordering was actually used.\n\nA sweep submitted with `walkForward` answers in a different shape, and the `walkForward`\nfield on the response is what tells the two apart — it appears as soon as the sweep is\naccepted, before any fold has finished, so it is safe to branch on while polling. There\nthe leaderboard is one row per completed fold: that fold's winner as it scored\n**out-of-sample**, with `runIx` carrying the fold index rather than a grid position. The\nin-sample runs behind those winners are not retained — they are an optimization's working\nset, and only the winner survives its fold. `ranking` is always `raw` and no plateau, DSR\nor PBO figure is reported: the out-of-sample scores are already the honest number, and\nlayering a certification computed over F observations on top of them would overstate what\nwas measured.\n",
				"parameters": [
					{
						"name": "exchangeId",
						"in": "path",
						"required": true,
						"schema": {
							"type": "string",
							"example": "binance"
						}
					},
					{
						"name": "type",
						"in": "path",
						"required": true,
						"schema": {
							"$ref": "#/components/schemas/DataSourceType"
						}
					},
					{
						"name": "requestId",
						"in": "path",
						"required": true,
						"schema": {
							"type": "string"
						}
					},
					{
						"name": "sweepId",
						"in": "path",
						"required": true,
						"schema": {
							"type": "string"
						}
					},
					{
						"name": "objective",
						"in": "query",
						"required": false,
						"schema": {
							"type": "string",
							"enum": [
								"sharpe",
								"sortino",
								"pnl",
								"maxdd"
							]
						}
					},
					{
						"name": "order",
						"in": "query",
						"required": false,
						"description": "`natural` is stable materialisation order; `ranked` is the display view.",
						"schema": {
							"type": "string",
							"enum": [
								"ranked",
								"natural"
							],
							"default": "ranked"
						}
					},
					{
						"name": "ranking",
						"in": "query",
						"required": false,
						"description": "How the `ranked` view is ordered. `plateau` prefers points whose neighbourhood also scores well; `raw` uses the objective alone. Ignored when `order=natural`, which is always ordered by `runIx`.",
						"schema": {
							"type": "string",
							"enum": [
								"plateau",
								"raw"
							],
							"default": "plateau"
						}
					}
				],
				"responses": {
					"200": {
						"description": "Current sweep snapshot and all currently available result rows for the selected view.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ExecuteSweepResult"
								}
							}
						}
					},
					"404": {
						"description": "Sweep not found or expired.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ResponseError"
								}
							}
						}
					}
				},
				"security": [
					{
						"bearerAuth": []
					}
				],
				"tags": [
					"Backtesting"
				]
			},
			"delete": {
				"operationId": "cancelSweep",
				"summary": "Cancel a running parameter sweep",
				"description": "Requests cancellation between parameter vectors. Completed rows remain readable.",
				"parameters": [
					{
						"name": "exchangeId",
						"in": "path",
						"required": true,
						"schema": {
							"type": "string",
							"example": "binance"
						}
					},
					{
						"name": "type",
						"in": "path",
						"required": true,
						"schema": {
							"$ref": "#/components/schemas/DataSourceType"
						}
					},
					{
						"name": "requestId",
						"in": "path",
						"required": true,
						"schema": {
							"type": "string"
						}
					},
					{
						"name": "sweepId",
						"in": "path",
						"required": true,
						"schema": {
							"type": "string"
						}
					}
				],
				"responses": {
					"200": {
						"description": "Cancellation requested.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object",
									"required": [
										"status",
										"sweepId"
									],
									"properties": {
										"status": {
											"type": "string",
											"enum": [
												"cancelling"
											]
										},
										"sweepId": {
											"type": "string"
										}
									}
								}
							}
						}
					},
					"404": {
						"description": "Sweep not found.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ResponseError"
								}
							}
						}
					}
				},
				"security": [
					{
						"bearerAuth": []
					}
				],
				"tags": [
					"Backtesting"
				]
			}
		},
		"/backtest/{exchangeId}/{type}/executeSweep/{requestId}/{sweepId}/sensitivity": {
			"get": {
				"operationId": "getSweepSensitivity",
				"summary": "Get sweep sensitivity surfaces",
				"description": "How the objective moves as each parameter moves — the question a leaderboard cannot answer.\nA leaderboard says which point won; a sweep can spend its entire budget on an axis that\nnever moved the objective at all, and showing only the top rows hides that completely.\n\nA **marginal** takes one axis and collapses every other one: for each value of that axis,\nit aggregates every run that used it, whatever the rest of the parameters were. A flat\nmarginal means the axis is irrelevant over the range swept. `best`, `mean` and `worst` are\nall reported because them disagreeing is itself the signal — a value with a high `best` and\na poor `mean` works only in specific company, which is an interaction between parameters\nand would be invisible behind a single number.\n\nA **heatmap** does the same over a pair of axes, where that interaction becomes visible\ndirectly.\n\nServed from the sweep's stored rows: no re-run, no engine call, and it works on a sweep\nstill in flight — the aggregates then describe the runs finished so far. Aborted runs are\nexcluded throughout, since a run that threw measured nothing and counting it as a bad\noutcome would invent evidence against a parameter value that was never really tested.\n\nThis is a separate endpoint rather than extra fields on the result view because the\ntwo-dimensional half is quadratic in the axis count (N axes give N(N-1)/2 surfaces, each\nthe product of two axes' value counts) and is not wanted on the poll that drives progress.\n",
				"parameters": [
					{
						"name": "exchangeId",
						"in": "path",
						"required": true,
						"schema": {
							"type": "string",
							"example": "binance"
						}
					},
					{
						"name": "type",
						"in": "path",
						"required": true,
						"schema": {
							"$ref": "#/components/schemas/DataSourceType"
						}
					},
					{
						"name": "requestId",
						"in": "path",
						"required": true,
						"schema": {
							"type": "string"
						}
					},
					{
						"name": "sweepId",
						"in": "path",
						"required": true,
						"schema": {
							"type": "string"
						}
					},
					{
						"name": "objective",
						"in": "query",
						"required": false,
						"description": "Which metric to aggregate. Defaults to the objective the sweep was submitted with.",
						"schema": {
							"type": "string",
							"enum": [
								"sharpe",
								"sortino",
								"pnl",
								"maxdd"
							]
						}
					}
				],
				"responses": {
					"200": {
						"description": "Sensitivity aggregates over the rows available so far.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/SweepSensitivity"
								}
							}
						}
					},
					"404": {
						"description": "Sweep not found or expired.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ResponseError"
								}
							}
						}
					}
				},
				"security": [
					{
						"bearerAuth": []
					}
				],
				"tags": [
					"Backtesting"
				]
			}
		},
		"/backtest/{exchangeId}/{type}/executeSweep/{requestId}/{sweepId}/runs/{runIx}/equityCurve": {
			"get": {
				"operationId": "getSweepRunEquityCurve",
				"summary": "Get one sweep trial's equity curve",
				"description": "The resource a leaderboard row's `equityCurve.url` points at — only reachable when that\ntrial's curve was actually selected (`equityCurve.mode: topN` or `topPct` on the sweep\nsubmission, and this trial ranked among the winners). Returns the exact same\n`{points|timestamps+equities, meta}` shape a plain backtest's inline `equityCurve` carries.\n\nQuery params reshape the response the same way a plain backtest's `equityCurve` options do.\nA param genuinely absent from the query string falls back to the `equityCurve` transform\npreference the sweep was submitted with — a param\npresent but malformed does not fall back, it degrades the same way it always has. Above a\nserver-side size threshold, the shape is forced regardless of either — `meta.outMode` in\nthe response, not the query string or the submitted default, is the source of truth for\nwhat shape actually came back.\n",
				"parameters": [
					{
						"name": "exchangeId",
						"in": "path",
						"required": true,
						"schema": {
							"type": "string",
							"example": "binance"
						}
					},
					{
						"name": "type",
						"in": "path",
						"required": true,
						"schema": {
							"$ref": "#/components/schemas/DataSourceType"
						}
					},
					{
						"name": "requestId",
						"in": "path",
						"required": true,
						"schema": {
							"type": "string"
						}
					},
					{
						"name": "sweepId",
						"in": "path",
						"required": true,
						"schema": {
							"type": "string"
						}
					},
					{
						"name": "runIx",
						"in": "path",
						"required": true,
						"description": "The trial's `runIx`, as it appears on its leaderboard row.",
						"schema": {
							"type": "integer",
							"minimum": 0
						}
					},
					{
						"name": "outMode",
						"in": "query",
						"required": false,
						"description": "Requested JSON shape. Omit to use the sweep's submitted default; may be overridden either way by the server's size guard.",
						"schema": {
							"allOf": [
								{
									"$ref": "#/components/schemas/EquityCurveOutMode"
								}
							],
							"default": "ARRAY"
						}
					},
					{
						"name": "resample",
						"in": "query",
						"required": false,
						"description": "Downsample to at most this many points (extrema-preserving — the global max/min and the exact first/last point are always kept). Omit to use the sweep's submitted default (itself omittable, for no downsampling).",
						"schema": {
							"type": "integer",
							"minimum": 2
						}
					},
					{
						"name": "differential",
						"in": "query",
						"required": false,
						"description": "Delta-encode both fields from the second point onward. Omit to use the sweep's submitted default.",
						"schema": {
							"type": "boolean",
							"default": false
						}
					}
				],
				"responses": {
					"200": {
						"description": "The trial's equity curve, shaped per the resolved options.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/EquityCurveResult"
								}
							}
						}
					},
					"404": {
						"description": "Sweep or `runIx` not found, or that trial's curve was never selected — indistinguishable from a caller's perspective.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ResponseError"
								}
							}
						}
					}
				},
				"security": [
					{
						"bearerAuth": []
					}
				],
				"tags": [
					"Backtesting"
				]
			}
		},
		"/backtest/{exchangeId}/{type}/execute": {
			"post": {
				"operationId": "executeBacktest",
				"summary": "Execute a compiled strategy against a prepared dataset",
				"description": "Enqueues an execute task that runs the strategy identified by `strategyId` over the data\nprepared by the prepare job identified by `prepareJobId`. The instrument and date range are\nrecovered from the prepare job — they do not need to be sent again.\n\nReturns immediately with a `jobId`; poll `GET /backtest/{exchangeId}/{type}/execute/{jobId}`\nfor the result.\n\nThe same params (same `prepareJobId`, `strategyId`, `storeSignals`, `equityCurve`) always\nreturn the same `jobId` (idempotent) — a request that omits `equityCurve` dedupes exactly\nas it did before that field existed.\n\nWorks unchanged for a dataset-backed prepare (`exchangeId: user`) — the request body is\nidentical either way, since the instrument and range are recovered from `prepareJobId`.\n",
				"parameters": [
					{
						"name": "exchangeId",
						"in": "path",
						"description": "ID of the exchange for the backtesting process, or the reserved value `user` if\n`prepareJobId` came from a dataset-backed prepare.\n",
						"required": true,
						"schema": {
							"type": "string",
							"example": "binance"
						}
					},
					{
						"name": "type",
						"in": "path",
						"description": "The type of data source to execute from",
						"required": true,
						"schema": {
							"$ref": "#/components/schemas/DataSourceType"
						}
					}
				],
				"requestBody": {
					"description": "Execute task parameters",
					"required": true,
					"content": {
						"application/json": {
							"schema": {
								"type": "object",
								"required": [
									"prepareJobId",
									"strategyId"
								],
								"properties": {
									"prepareJobId": {
										"type": "string",
										"description": "Job ID returned by `POST /prepare` (must be in `Completed` state)",
										"example": "13RBLGQlPnfDjO6wyKSX8i"
									},
									"strategyId": {
										"$ref": "#/components/schemas/strategyId"
									},
									"storeSignals": {
										"type": "boolean",
										"description": "When true, the worker uploads emitted signals to object storage and the\nresponse includes `signalsUrl` / `signalsId` fields. Defaults to false.\n",
										"default": false
									},
									"equityCurve": {
										"$ref": "#/components/schemas/EquityCurveOptions"
									}
								}
							},
							"example": {
								"prepareJobId": "13RBLGQlPnfDjO6wyKSX8i",
								"strategyId": "6bsh31ikwkuivhtgcoa6s4",
								"storeSignals": false
							}
						}
					}
				},
				"responses": {
					"202": {
						"description": "Execute task accepted (queued for processing)",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/AcceptedJob"
								}
							}
						}
					},
					"400": {
						"description": "Invalid request or parameters",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ResponseError"
								}
							}
						}
					},
					"404": {
						"description": "Prepare job not found or expired",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ResponseError"
								}
							}
						}
					},
					"429": {
						"description": "Rate limited (global queue at capacity or per-user limit reached)",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ResponseError"
								}
							}
						}
					}
				},
				"security": [
					{
						"bearerAuth": []
					}
				],
				"tags": [
					"Backtesting"
				]
			}
		},
		"/backtest/{exchangeId}/{type}/execute/{jobId}": {
			"delete": {
				"operationId": "cancelBacktest",
				"summary": "Cancel a running backtest execution",
				"description": "Requests cancellation of the specified execution. The execution\nstatus will transition to `Aborted` once the cancellation is\nprocessed. Cancellation is asynchronous — poll the GET endpoint\nto confirm the final status.\n",
				"parameters": [
					{
						"name": "exchangeId",
						"in": "path",
						"required": true,
						"schema": {
							"type": "string",
							"example": "binance"
						}
					},
					{
						"name": "type",
						"in": "path",
						"required": true,
						"schema": {
							"$ref": "#/components/schemas/DataSourceType"
						}
					},
					{
						"name": "jobId",
						"in": "path",
						"description": "Job ID returned by `POST /execute`",
						"required": true,
						"schema": {
							"type": "string",
							"example": "13RBLGQlPnfDjO6wyKSX8i"
						}
					}
				],
				"responses": {
					"200": {
						"description": "Cancellation request accepted",
						"content": {
							"application/json": {
								"schema": {
									"type": "object",
									"properties": {
										"status": {
											"type": "string",
											"enum": [
												"cancelling"
											],
											"example": "cancelling"
										},
										"jobId": {
											"type": "string",
											"example": "13RBLGQlPnfDjO6wyKSX8i"
										}
									}
								}
							}
						}
					},
					"404": {
						"description": "Execution not found",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ResponseError"
								}
							}
						}
					}
				},
				"security": [
					{
						"bearerAuth": []
					}
				],
				"tags": [
					"Backtesting"
				]
			},
			"get": {
				"operationId": "getBacktestResult",
				"summary": "Get the result of a backtest execution job",
				"description": "Retrieves the current state and results of the execute job identified by `jobId`.\nPoll until `state.status` is `Completed`, `Failed`, or `Aborted`.\n\nA `202` means the result is not readable yet — keep polling. It is never a terminal\noutcome, and it carries no `state`, so a poll loop that stops on a terminal status will\nnot stop on it.\n",
				"parameters": [
					{
						"name": "exchangeId",
						"in": "path",
						"description": "ID of the exchange for the backtesting process",
						"required": true,
						"schema": {
							"type": "string",
							"example": "binance"
						}
					},
					{
						"name": "type",
						"in": "path",
						"description": "The type of data source to execute from",
						"required": true,
						"schema": {
							"$ref": "#/components/schemas/DataSourceType"
						}
					},
					{
						"name": "jobId",
						"in": "path",
						"description": "Job ID returned by `POST /execute`",
						"required": true,
						"schema": {
							"type": "string",
							"example": "13RBLGQlPnfDjO6wyKSX8i"
						}
					}
				],
				"responses": {
					"200": {
						"description": "Backtesting execution result",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/BacktestJobResult"
								},
								"example": {
									"state": {
										"contextId": "ctx_2mnyblatqqw34kix2echpb",
										"status": "Completed",
										"statusDetail": null,
										"size": 0,
										"completed": 73158,
										"startTime": "2026-03-18T13:21:28.958Z",
										"endTime": "2026-03-18T13:21:29.605Z"
									},
									"results": {
										"hostName": "executor10",
										"iops": 123956.53,
										"strategyId": "strategy:00000000-0000-0000-0000-000000000000:2iyvtenlzh9dabqtxn7nbv",
										"instrument": "BTC/USDT",
										"notices": [
											{
												"level": "WARN",
												"code": "indicator.bar-data-on-ticker-path",
												"message": "Indicator requires bar data but is on the ticker path",
												"provenance": "execute"
											}
										],
										"pnlTotal": 42.75,
										"pnlTotalPercent": 2.25,
										"totalTrades": 156,
										"winRate": 58.33,
										"sharpeRatio": 1.245,
										"sortinoRatio": 1.872,
										"cagr": 0.1534,
										"maxDrawdown": 12.5,
										"maxDrawdownPercent": 8.75,
										"equityCurve": {
											"points": [
												{
													"timestamp": 1700000000000,
													"equity": 100
												},
												{
													"timestamp": 1700000060000,
													"equity": 110.5
												},
												{
													"timestamp": 1700000120000,
													"equity": 90.25
												}
											],
											"meta": {
												"inputPointCount": 3,
												"outputPointCount": 3,
												"resampled": false,
												"differential": false,
												"outMode": "ARRAY"
											}
										},
										"signalCount": 100000,
										"signalsId": "00000000-0000-0000-0000-000000000000/exec/binance/3vsndwikcuaatjmb83fjtl",
										"signalsUrl": "https://storage.qtsurfer.com/00000000-0000-0000-0000-000000000000/exec/binance/3vsndwikcuaatjmb83fjtl.parquet",
										"signalsUpload": "Done",
										"signalsUploadedAt": "2026-03-18T13:21:48.170Z"
									}
								}
							}
						}
					},
					"202": {
						"description": "The job is known but its result is not readable yet — keep polling.\n\nReturned in two situations, both of which mean \"ask again\", never \"you are done\":\nthe job has not produced its result yet, or the job reached a terminal status while\nits stored result could not be read back. The response body is an empty object: it\ndeliberately carries no `state`, so a client cannot mistake it for a finished result.\n\nTreat any `202` as a signal to continue the poll loop under your existing timeout.\nNever treat it as a terminal outcome.\n",
						"content": {
							"application/json": {
								"schema": {
									"type": "object",
									"additionalProperties": false
								},
								"example": {}
							}
						}
					},
					"400": {
						"description": "Invalid request or parameters",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ResponseError"
								}
							}
						}
					},
					"404": {
						"description": "Execution job not found",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ResponseError"
								}
							}
						}
					}
				},
				"security": [
					{
						"bearerAuth": []
					}
				],
				"tags": [
					"Backtesting"
				]
			}
		},
		"/datasets": {
			"post": {
				"operationId": "createDataset",
				"summary": "Create a dataset and get a URL to upload it to",
				"description": "Creates a dataset AND its first upload session in one call — a presigned URL your client\nPUTs the file to directly, no API credentials involved in that PUT. Call\n`POST /datasets/{datasetId}/uploads/{uploadId}/finalize` once the upload completes to kick\noff ingest.\n\nLosing this response loses nothing: calling this dataset's\n`POST /datasets/{datasetId}/uploads` returns the very same upload session again rather than\nopening a new one, as long as nothing has been finalized against it yet.\n\nv1 is ticker data only — `type` is not a request field, it is always `\"ticker\"` in the\nresponse. `instrument` must be a plain spot pair (`BASE/QUOTE`, exactly one `/`); derivative\nforms (e.g. `BTC/USDT:USDT`) are rejected.\n\n**Upload format.** A CSV with a header row. Required columns: `timestamp` (ISO-8601, or\nnumeric epoch seconds/millis/micros — detected from the first row, then enforced for every\nlater row), `close`. Optional columns: `open`, `high`, `low`, `volume`, `quoteVolume`,\n`bid`, `bidSize`, `ask`, `askSize`. Cadence and timestamp unit are discovered from the data,\nnot declared.\n",
				"requestBody": {
					"description": "The dataset to create",
					"required": true,
					"content": {
						"application/json": {
							"schema": {
								"type": "object",
								"required": [
									"name",
									"instrument"
								],
								"properties": {
									"name": {
										"type": "string",
										"description": "A name unique among your datasets. `409` if already taken.",
										"example": "My BTC ticks"
									},
									"instrument": {
										"$ref": "#/components/schemas/Instrument"
									}
								}
							},
							"example": {
								"name": "My BTC ticks",
								"instrument": "BTC/USDT"
							}
						}
					}
				},
				"responses": {
					"201": {
						"description": "Dataset created, with an upload session ready for its first version",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/DatasetCreated"
								},
								"example": {
									"datasetId": "ds_3f9a1c2e7b0d4a5f",
									"name": "My BTC ticks",
									"type": "ticker",
									"instrument": "BTC/USDT",
									"uploadId": "up_1a2b3c4d5e6f7a8b",
									"upload": {
										"url": "https://storage.qtsurfer.com/00000000-.../uploads/up_1a2b3c4d5e6f7a8b/raw.csv?X-Amz-...",
										"expiresInMinutes": 15
									}
								}
							}
						}
					},
					"400": {
						"description": "Invalid request, or `instrument` is not a plain spot pair",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ResponseError"
								}
							}
						}
					},
					"409": {
						"description": "You already have a dataset with this `name`",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ResponseError"
								}
							}
						}
					},
					"429": {
						"description": "Your tier's dataset count limit is reached. Delete one, or upgrade.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ResponseError"
								}
							}
						}
					}
				},
				"security": [
					{
						"bearerAuth": []
					}
				],
				"tags": [
					"Dataset"
				]
			},
			"get": {
				"operationId": "listDatasets",
				"summary": "List your datasets",
				"description": "Every dataset you have created and not deleted, most recently created first. Never a `404`\n— an empty array if you have none, same convention as `GET /strategies`.\n",
				"responses": {
					"200": {
						"description": "Your datasets",
						"content": {
							"application/json": {
								"schema": {
									"type": "object",
									"required": [
										"datasets"
									],
									"properties": {
										"datasets": {
											"type": "array",
											"items": {
												"$ref": "#/components/schemas/Dataset"
											}
										}
									}
								},
								"example": {
									"datasets": [
										{
											"datasetId": "ds_3f9a1c2e7b0d4a5f",
											"name": "My BTC ticks",
											"type": "ticker",
											"instrument": "BTC/USDT",
											"createdAt": "2026-08-20T09:00:00Z",
											"currentVersionId": "dsv_8e2b4f19c6a03d7e",
											"updatedAt": "2026-08-20T09:04:12Z",
											"from": "2026-03-01T00:00:00Z",
											"to": "2026-03-08T00:00:00Z",
											"cadence": "1m"
										}
									]
								}
							}
						}
					}
				},
				"security": [
					{
						"bearerAuth": []
					}
				],
				"tags": [
					"Dataset"
				]
			}
		},
		"/datasets/{datasetId}": {
			"get": {
				"operationId": "getDataset",
				"summary": "Get a dataset by id",
				"description": "Detail for one dataset, plus a self link.",
				"parameters": [
					{
						"name": "datasetId",
						"in": "path",
						"required": true,
						"description": "The id returned by `POST /datasets`",
						"schema": {
							"type": "string",
							"example": "ds_3f9a1c2e7b0d4a5f"
						}
					}
				],
				"responses": {
					"200": {
						"description": "Dataset detail",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/DatasetWithLinks"
								},
								"example": {
									"datasetId": "ds_3f9a1c2e7b0d4a5f",
									"name": "My BTC ticks",
									"type": "ticker",
									"instrument": "BTC/USDT",
									"createdAt": "2026-08-20T09:00:00Z",
									"currentVersionId": "dsv_8e2b4f19c6a03d7e",
									"updatedAt": "2026-08-20T09:04:12Z",
									"from": "2026-03-01T00:00:00Z",
									"to": "2026-03-08T00:00:00Z",
									"cadence": "1m",
									"_links": {
										"self": {
											"href": "/v1/datasets/ds_3f9a1c2e7b0d4a5f"
										}
									}
								}
							}
						}
					},
					"404": {
						"description": "No such dataset for this user",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ResponseError"
								}
							}
						}
					}
				},
				"security": [
					{
						"bearerAuth": []
					}
				],
				"tags": [
					"Dataset"
				]
			},
			"delete": {
				"operationId": "deleteDataset",
				"summary": "Delete a dataset",
				"description": "Soft-delete — the dataset stops appearing in `GET /datasets`/`GET /datasets/{datasetId}` and\ncan no longer be prepared from, but its object data is reclaimed later rather than purged\ninline, so a backtest already running against one of its versions is not disrupted.\n",
				"parameters": [
					{
						"name": "datasetId",
						"in": "path",
						"required": true,
						"description": "The id returned by `POST /datasets`",
						"schema": {
							"type": "string",
							"example": "ds_3f9a1c2e7b0d4a5f"
						}
					}
				],
				"responses": {
					"200": {
						"description": "Deleted",
						"content": {
							"application/json": {
								"schema": {
									"type": "object",
									"required": [
										"datasetId",
										"deleted"
									],
									"properties": {
										"datasetId": {
											"type": "string"
										},
										"deleted": {
											"type": "boolean",
											"enum": [
												true
											]
										}
									}
								},
								"example": {
									"datasetId": "ds_3f9a1c2e7b0d4a5f",
									"deleted": true
								}
							}
						}
					},
					"404": {
						"description": "No such dataset for this user, or already deleted",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ResponseError"
								}
							}
						}
					}
				},
				"security": [
					{
						"bearerAuth": []
					}
				],
				"tags": [
					"Dataset"
				]
			}
		},
		"/datasets/{datasetId}/uploads": {
			"post": {
				"operationId": "openDatasetUpload",
				"summary": "Open a new upload session for an existing dataset",
				"description": "Get a fresh presigned URL to upload a new version into a dataset you already have — a\ncorrected file, or the next chunk of history. Behaves the same way `POST /datasets` does\nfor a brand-new dataset's own upload: at most one upload session is open per dataset at a\ntime, so calling this again before finalizing just hands back that same session rather\nthan opening a second one — safe to call repeatedly if a response gets lost.\n\nOnce a session has been finalized (successfully or not), the next call here opens a\ngenuinely new one for that dataset's next version.\n",
				"parameters": [
					{
						"name": "datasetId",
						"in": "path",
						"required": true,
						"description": "The id returned by `POST /datasets`",
						"schema": {
							"type": "string",
							"example": "ds_3f9a1c2e7b0d4a5f"
						}
					}
				],
				"responses": {
					"201": {
						"description": "An upload session — new, or the one already open for this dataset",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/DatasetUploadSession"
								},
								"example": {
									"uploadId": "up_1a2b3c4d5e6f7a8b",
									"upload": {
										"url": "https://storage.qtsurfer.com/00000000-.../uploads/up_1a2b3c4d5e6f7a8b/raw.csv?X-Amz-...",
										"expiresInMinutes": 15
									}
								}
							}
						}
					},
					"404": {
						"description": "No such dataset for this user",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ResponseError"
								}
							}
						}
					}
				},
				"security": [
					{
						"bearerAuth": []
					}
				],
				"tags": [
					"Dataset"
				]
			}
		},
		"/datasets/{datasetId}/uploads/{uploadId}/finalize": {
			"post": {
				"operationId": "finalizeDatasetUpload",
				"summary": "Finalize an uploaded file and start ingest",
				"description": "Call once the file has been PUT to the `upload.url` from `POST /datasets` (or from\n`POST /datasets/{datasetId}/uploads`). Enqueues ingest and returns immediately; poll\n`GET /datasets/{datasetId}/uploads/{uploadId}` for the result.\n\nIdempotent while the upload is still open — a repeat finalize before it has produced a\nversion returns the same `jobId` rather than enqueueing a second ingest. Once it HAS\nproduced a version, `uploadId` is spent: finalizing it again is a `409`, even with\ndifferent bytes freshly PUT to the same URL — open a new upload session instead\n(`POST /datasets/{datasetId}/uploads`) rather than reusing a spent one.\n",
				"parameters": [
					{
						"name": "datasetId",
						"in": "path",
						"required": true,
						"description": "The id returned by `POST /datasets`",
						"schema": {
							"type": "string",
							"example": "ds_3f9a1c2e7b0d4a5f"
						}
					},
					{
						"name": "uploadId",
						"in": "path",
						"required": true,
						"description": "The `uploadId` returned by `POST /datasets`",
						"schema": {
							"type": "string",
							"example": "up_1a2b3c4d5e6f7a8b"
						}
					}
				],
				"responses": {
					"202": {
						"description": "Ingest queued",
						"content": {
							"application/json": {
								"schema": {
									"type": "object",
									"required": [
										"jobId"
									],
									"properties": {
										"jobId": {
											"type": "string"
										}
									}
								},
								"example": {
									"jobId": "dataset-upload:00000000-.../ds_3f9a1c2e7b0d4a5f:up_1a2b3c4d5e6f7a8b"
								}
							}
						}
					},
					"404": {
						"description": "No such dataset for this user; `uploadId` was not issued for this dataset (never\nminted, or minted for a different one); or nothing was PUT to `upload.url` yet — a\nfinalize with nothing to finalize.\n",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ResponseError"
								}
							}
						}
					},
					"409": {
						"description": "`uploadId` already produced a version. The error message names it. Open a new upload\nsession (`POST /datasets/{datasetId}/uploads`) for anything new.\n",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ResponseError"
								}
							}
						}
					},
					"413": {
						"description": "The uploaded file exceeds your tier's size limit for a dataset.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ResponseError"
								}
							}
						}
					}
				},
				"security": [
					{
						"bearerAuth": []
					}
				],
				"tags": [
					"Dataset"
				]
			}
		},
		"/datasets/{datasetId}/uploads/{uploadId}": {
			"get": {
				"operationId": "getDatasetUpload",
				"summary": "Get the state of an upload/ingest",
				"description": "Poll after `POST .../finalize` until `status` is `ready` or `failed`. Also reports\n`uploading` (finalize not called yet, but the file was PUT) before you finalize at all.\n",
				"parameters": [
					{
						"name": "datasetId",
						"in": "path",
						"required": true,
						"description": "The id returned by `POST /datasets`",
						"schema": {
							"type": "string",
							"example": "ds_3f9a1c2e7b0d4a5f"
						}
					},
					{
						"name": "uploadId",
						"in": "path",
						"required": true,
						"description": "The `uploadId` returned by `POST /datasets`",
						"schema": {
							"type": "string",
							"example": "up_1a2b3c4d5e6f7a8b"
						}
					}
				],
				"responses": {
					"200": {
						"description": "Current upload/ingest state",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/DatasetUploadState"
								},
								"example": {
									"uploadId": "up_1a2b3c4d5e6f7a8b",
									"status": "ready",
									"version": {
										"datasetId": "ds_3f9a1c2e7b0d4a5f",
										"id": "dsv_8e2b4f19c6a03d7e",
										"bytes": 4831022,
										"rows": 86400,
										"cadence": "1s",
										"timestampUnit": "iso",
										"gaps": 0,
										"largestGapSteps": 0
									}
								}
							}
						}
					},
					"404": {
						"description": "No such dataset for this user, or genuinely nothing is known about this `uploadId` — no\nversion, no in-flight job, and nothing was ever PUT to its upload URL.\n",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ResponseError"
								}
							}
						}
					}
				},
				"security": [
					{
						"bearerAuth": []
					}
				],
				"tags": [
					"Dataset"
				]
			}
		}
	},
	"components": {
		"schemas": {
			"ResponseError": {
				"description": "General response error",
				"type": "object",
				"required": [
					"code",
					"message"
				],
				"properties": {
					"code": {
						"description": "Status code",
						"type": "integer",
						"example": 400
					},
					"message": {
						"description": "Error description",
						"type": "string",
						"example": "Invalid request"
					}
				}
			},
			"Instrument": {
				"description": "Exchange instrument identifier (e.g. a currency pair)",
				"type": "string",
				"example": "BTC/USDT"
			},
			"InstrumentListResponse": {
				"description": "HAL-style response envelope for the instruments listing",
				"type": "object",
				"required": [
					"data",
					"meta",
					"_links"
				],
				"properties": {
					"data": {
						"type": "array",
						"description": "The list of instruments for the segment",
						"items": {
							"$ref": "#/components/schemas/InstrumentDetail"
						}
					},
					"meta": {
						"$ref": "#/components/schemas/InstrumentListMeta"
					},
					"_links": {
						"$ref": "#/components/schemas/InstrumentLinks"
					}
				}
			},
			"InstrumentListMeta": {
				"description": "Metadata describing the instruments listing",
				"type": "object",
				"required": [
					"updatedAt",
					"exchange",
					"segment"
				],
				"properties": {
					"updatedAt": {
						"type": "string",
						"format": "date-time",
						"description": "When this listing was last refreshed",
						"example": "2026-07-09T19:09:07Z"
					},
					"exchange": {
						"type": "string",
						"description": "The exchange the instruments belong to",
						"example": "binance"
					},
					"segment": {
						"type": "string",
						"enum": [
							"spot",
							"futures"
						],
						"description": "The market segment served in `data`",
						"example": "spot"
					}
				}
			},
			"InstrumentLinks": {
				"description": "HAL `_links` — segment discovery for the instruments listing",
				"type": "object",
				"required": [
					"self"
				],
				"properties": {
					"self": {
						"allOf": [
							{
								"$ref": "#/components/schemas/HalLink"
							}
						],
						"description": "Link to this listing"
					},
					"spot": {
						"allOf": [
							{
								"$ref": "#/components/schemas/HalLink"
							}
						],
						"description": "Link to the spot instruments listing. Present when the exchange has a spot segment."
					},
					"futures": {
						"allOf": [
							{
								"$ref": "#/components/schemas/HalLink"
							}
						],
						"description": "Link to the futures instruments listing. Present only when the exchange has a futures segment."
					}
				}
			},
			"StrategyLinks": {
				"description": "HAL `_links` for a strategy — present on a full `StrategyState` body (`GET\n/strategy/{strategyId}`, and `POST /strategy/{strategyId}/validate`'s already-validated\n`200`), absent from that same endpoint's `202` — a deliberately partial stub carrying only\nwhat is known before a check has even started. Following `code` can still `404` once\npresent: it documents its own honest \"nothing to return\" for a strategy with no source of\nits own (a `REFERENCE` marketplace copy, or one resolved only through the platform's shared\npool). This link says where to look, not that something is there.\n",
				"type": "object",
				"required": [
					"code"
				],
				"properties": {
					"code": {
						"allOf": [
							{
								"$ref": "#/components/schemas/HalLink"
							}
						],
						"description": "Link to this strategy's registered source, `GET /strategy/{strategyId}/code`."
					}
				}
			},
			"HalLink": {
				"description": "A HAL link object (Hypertext Application Language)",
				"type": "object",
				"required": [
					"href"
				],
				"properties": {
					"href": {
						"type": "string",
						"format": "uri-reference",
						"description": "The link target as an absolute-path URI reference (resolve against the API base). A URI Template (RFC 6570) when `templated` is true.",
						"example": "/v1/exchange/binance/spot/instruments"
					},
					"templated": {
						"type": "boolean",
						"description": "True when `href` is an RFC 6570 URI Template.",
						"example": false
					}
				}
			},
			"InstrumentDetail": {
				"description": "Exchange instrument with per-data-type coverage and market info",
				"type": "object",
				"required": [
					"id",
					"base",
					"quote"
				],
				"properties": {
					"id": {
						"type": "string",
						"description": "Instrument identifier (e.g. currency pair)",
						"example": "BTC/USDT"
					},
					"base": {
						"type": "string",
						"description": "Base currency",
						"example": "BTC"
					},
					"quote": {
						"type": "string",
						"description": "Quote currency",
						"example": "USDT"
					},
					"coverage": {
						"$ref": "#/components/schemas/InstrumentCoverage"
					},
					"lastPrice": {
						"type": "number",
						"format": "double",
						"description": "Last traded price",
						"example": 84250.5
					},
					"volume24h": {
						"type": "number",
						"format": "double",
						"description": "Trading volume in the last 24 hours (in quote currency)",
						"example": 1234567.89
					}
				}
			},
			"InstrumentCoverage": {
				"description": "Time coverage of available data for this instrument, per data type",
				"type": "object",
				"properties": {
					"tickers": {
						"allOf": [
							{
								"$ref": "#/components/schemas/CoverageWindow"
							}
						],
						"description": "Coverage of ticker data"
					},
					"klines": {
						"allOf": [
							{
								"$ref": "#/components/schemas/CoverageWindow"
							}
						],
						"description": "Coverage of kline (candlestick) data"
					}
				}
			},
			"CoverageWindow": {
				"description": "The time range of available data for a single data type",
				"type": "object",
				"properties": {
					"from": {
						"type": "string",
						"format": "date-time",
						"description": "Earliest timestamp with data available",
						"example": "2026-04-10T21:00:00Z"
					},
					"to": {
						"type": "string",
						"format": "date-time",
						"description": "Latest timestamp with data available",
						"example": "2026-07-09T20:31:08Z"
					},
					"inactiveSince": {
						"type": "string",
						"format": "date-time",
						"description": "If the instrument stopped producing this data type (delisted/inactive), the timestamp it went inactive. Optional — omitted while the instrument is active.",
						"example": "2026-06-30T12:00:00Z"
					}
				}
			},
			"Exchange": {
				"description": "Exchange service provider",
				"type": "object",
				"required": [
					"id",
					"name"
				],
				"properties": {
					"id": {
						"description": "Unique identifier for the exchange",
						"type": "string",
						"example": "binance"
					},
					"name": {
						"description": "Name of the exchange",
						"type": "string",
						"example": "Binance"
					},
					"description": {
						"description": "Description of the exchange",
						"type": "string",
						"example": "Binance cryptocurrency exchange"
					}
				}
			},
			"DataSourceType": {
				"type": "string",
				"description": "Managed exchange data sources available for backtesting.",
				"enum": [
					"ticker"
				],
				"example": "ticker"
			},
			"PrepareRequest": {
				"type": "object",
				"description": "Two shapes, chosen by the `exchangeId` path segment. Against a managed exchange,\n`instrument` is required and `datasetId`/`datasetVersionId` are ignored. Against the\nreserved `exchangeId: user`, send `datasetId` instead of `instrument` — `instrument` is\nignored there, since it comes from the dataset itself.\n",
				"required": [
					"from",
					"to"
				],
				"properties": {
					"instrument": {
						"allOf": [
							{
								"$ref": "#/components/schemas/Instrument"
							}
						],
						"description": "Required unless `exchangeId` is the reserved value `user`, in which case send\n`datasetId` instead.\n"
					},
					"datasetId": {
						"type": "string",
						"description": "Only for `exchangeId: user`: the id of a dataset created via `POST /datasets`, in place\nof `instrument`. Ignored against a managed exchange.\n",
						"example": "ds_3f9a1c2e7b0d4a5f"
					},
					"datasetVersionId": {
						"type": "string",
						"description": "Only for `exchangeId: user`, and optional even then: pins a specific past version of\nthe dataset instead of its current one. Defaults to the dataset's current version.\n",
						"example": "dsv_8e2b4f19c6a03d7e"
					},
					"from": {
						"type": "string",
						"description": "Start date for the preparation process. Supports the following formats:\n- ISO-8601 (e.g. 2024-12-14T23:59:59Z)\n- ISO DATE (e.g. 2024-12-14)\n- BASIC ISO DATE (e.g., 20241214)\n",
						"example": "2024-12-13T00:00:00Z"
					},
					"to": {
						"type": "string",
						"description": "End date for the preparation process. Supports the following formats:\n- ISO-8601 (e.g. 2024-12-14T23:59:59Z)\n- ISO DATE (e.g. 2024-12-14)\n- BASIC ISO DATE (e.g., 20241214)\n",
						"example": "2024-12-14"
					},
					"cadence": {
						"type": "string",
						"description": "Output bar cadence for the prepared range. Defaults to the publisher's\nnative cadence (`1s`); coarser cadences are produced on demand via\nresampling and stored alongside the native blob in cache. Coarser-than-\nsource values must be exact multiples of the source cadence — invalid\nlabels return `400`.\n",
						"enum": [
							"1s",
							"5s",
							"1m",
							"3m",
							"5m",
							"15m",
							"30m",
							"1h",
							"2h",
							"4h",
							"8h",
							"12h",
							"1d",
							"1w",
							"1q"
						],
						"default": "1s"
					}
				},
				"example": {
					"instrument": "BTC/USDT",
					"from": "2024-12-13T00:00:00Z",
					"to": "2024-12-14T00:00:00Z",
					"cadence": "1m"
				}
			},
			"JobState": {
				"type": "object",
				"description": "Information about a single job",
				"required": [
					"contextId",
					"status",
					"size",
					"completed"
				],
				"properties": {
					"contextId": {
						"type": "string",
						"description": "Opaque context identifier for the job",
						"example": "ctx_2o8heaioicr0edvx5ybcap"
					},
					"status": {
						"type": "string",
						"description": "Current status of the job. Treat `Completed | Aborted | Failed` as\nterminal; `New | Started` mean keep polling. A single-instrument prepare\nis always terminal (`Completed`) — decide from\n`PrepareJobState.coverageRatio`, not by polling.\n",
						"enum": [
							"New",
							"Started",
							"Completed",
							"Aborted",
							"Failed"
						],
						"example": "Completed"
					},
					"statusDetail": {
						"type": [
							"string",
							"null"
						],
						"description": "Detailed status information, if available",
						"example": "Job completed with error code 5001"
					},
					"size": {
						"type": "integer",
						"description": "Total size of the data being prepared",
						"example": 100
					},
					"completed": {
						"type": "integer",
						"description": "The amount of data processed so far",
						"example": 50
					},
					"startTime": {
						"type": [
							"string",
							"null"
						],
						"format": "date-time",
						"description": "Timestamp for when the preparation started",
						"example": "2025-01-04T14:00:00Z"
					},
					"endTime": {
						"type": [
							"string",
							"null"
						],
						"format": "date-time",
						"description": "Timestamp for when the preparation finished",
						"example": "2025-01-04T14:00:20Z"
					}
				}
			},
			"PrepareJobState": {
				"description": "State of a single-instrument prepare job — the `JobState` shape plus a coverage summary.\nA single-instrument prepare is always terminal (`status: Completed`): the client decides\nwhat to do from `coverageRatio` (e.g. execute if it is at or above a chosen threshold)\nrather than polling for missing hours that may never arrive — a missing hour for one\ninstrument usually means low activity, not missing data.\n\n**Two coverage shapes, by exchange vs. dataset.** Against a managed exchange, coverage is\nwalked hour by hour: `totalHours`/`hoursWithData`/`hoursWithoutData`. Against a\ndataset-backed prepare (`exchangeId: user`), coverage is reported on the dataset's own\ncadence grid instead — hour-walking a daily dataset would report `1/24` and read as\nbroken — via `cadence`/`gaps`/`largestGapSteps`; `totalHours`/`hoursWithData`/\n`hoursWithoutData` are absent in that case. `dataFrom`/`dataTo`/`coverageRatio` are present\neither way, computed accordingly.\n",
				"allOf": [
					{
						"$ref": "#/components/schemas/JobState"
					},
					{
						"type": "object",
						"properties": {
							"dataFrom": {
								"type": [
									"string",
									"null"
								],
								"format": "date-time",
								"description": "Start of the available data range for the prepared instrument.",
								"example": "2026-04-14T13:00:00Z"
							},
							"dataTo": {
								"type": [
									"string",
									"null"
								],
								"format": "date-time",
								"description": "End of the available data range for the prepared instrument.",
								"example": "2026-04-14T15:30:05Z"
							},
							"coverageRatio": {
								"type": "number",
								"format": "double",
								"minimum": 0,
								"maximum": 1,
								"description": "Against a managed exchange: `hoursWithData / totalHours` in `[0,1]` (`1.0` when\n`totalHours` is 0), the fraction of hours in the requested range that have served\ndata. Against a dataset (`exchangeId: user`): `rows / expectedStepsAtCadence`\nover the dataset version's own range — echoing what ingest computed once, not\nrecomputed against a narrower prepare request.\n",
								"example": 0.994
							},
							"totalHours": {
								"type": "integer",
								"description": "Number of whole hours in the requested prepare range. Managed exchanges only —\nabsent for a dataset-backed prepare.\n",
								"example": 168
							},
							"hoursWithData": {
								"type": "integer",
								"description": "Number of hours in the range that have data. Managed exchanges only — absent for\na dataset-backed prepare.\n",
								"example": 167
							},
							"cadence": {
								"type": "string",
								"description": "The dataset version's own discovered cadence (e.g. `1m`, `1h`). Only present for a\ndataset-backed prepare (`exchangeId: user`).\n",
								"example": "1m"
							},
							"gaps": {
								"type": "integer",
								"description": "Number of gaps in the dataset version at its own cadence, as discovered at ingest\ntime. Only present for a dataset-backed prepare.\n",
								"example": 0
							},
							"largestGapSteps": {
								"type": "integer",
								"description": "The largest gap in the dataset version, in units of its own cadence step. Only\npresent for a dataset-backed prepare.\n",
								"example": 0
							},
							"hoursWithoutData": {
								"type": "array",
								"description": "One entry per hour in the range that has no data, with a rationale. Managed\nexchanges only — absent for a dataset-backed prepare.\n",
								"items": {
									"type": "object",
									"properties": {
										"hour": {
											"type": "string",
											"format": "date-time",
											"description": "The hour (UTC, hour-aligned) that has no data.",
											"example": "2026-04-14T02:00:00Z"
										},
										"expected": {
											"type": "integer",
											"description": "Expected row count for the hour (currently always 0; reserved for\nfuture use). The rationale never depends on it.\n",
											"example": 0
										},
										"rationale": {
											"type": "string",
											"description": "Why the hour has no data. `pending_conversion`: data for this hour is\nstill being produced — a re-poll may fill it. `low_activity`: the\ninstrument did not trade that hour. `unknown`: no data to classify by.\n",
											"enum": [
												"pending_conversion",
												"low_activity",
												"unknown"
											],
											"example": "low_activity"
										}
									}
								}
							}
						}
					}
				]
			},
			"SweepAxis": {
				"description": "A numeric range or an explicit list of values for one strategy property.",
				"oneOf": [
					{
						"type": "object",
						"required": [
							"from",
							"to",
							"step"
						],
						"additionalProperties": false,
						"properties": {
							"from": {
								"type": "number",
								"format": "double"
							},
							"to": {
								"type": "number",
								"format": "double"
							},
							"step": {
								"type": "number",
								"format": "double",
								"exclusiveMinimum": 0
							}
						}
					},
					{
						"type": "object",
						"required": [
							"values"
						],
						"additionalProperties": false,
						"properties": {
							"values": {
								"type": "array",
								"minItems": 1,
								"items": {
									"oneOf": [
										{
											"type": "number"
										},
										{
											"type": "boolean"
										}
									]
								}
							}
						}
					}
				]
			},
			"SweepSpecRequest": {
				"type": "object",
				"required": [
					"params"
				],
				"properties": {
					"sampler": {
						"type": "string",
						"enum": [
							"grid",
							"random",
							"lhs"
						],
						"default": "grid"
					},
					"seed": {
						"type": "integer",
						"format": "int64",
						"minimum": -9007199254740991,
						"maximum": 9007199254740991,
						"description": "Reproducibility seed. If omitted, the server generates one with Java's\n`L64X128MixRandom` generator and returns the effective value. The range\nis limited to JavaScript-safe integers so generated clients can replay it exactly.\n"
					},
					"samples": {
						"type": "integer",
						"minimum": 1,
						"description": "Number of samples for `random` and `lhs`; ignored by `grid`."
					},
					"objective": {
						"type": "string",
						"enum": [
							"sharpe",
							"sortino",
							"pnl",
							"maxdd"
						],
						"default": "sharpe"
					},
					"params": {
						"type": "object",
						"minProperties": 1,
						"additionalProperties": {
							"$ref": "#/components/schemas/SweepAxis"
						}
					}
				},
				"example": {
					"sampler": "lhs",
					"seed": 487221,
					"samples": 100,
					"objective": "sharpe",
					"params": {
						"rsiPeriod": {
							"from": 7,
							"to": 28,
							"step": 1
						},
						"useTrendFilter": {
							"values": [
								true,
								false
							]
						}
					}
				}
			},
			"SweepBaseConfig": {
				"type": "object",
				"properties": {
					"initialFunding": {
						"type": "number",
						"format": "double",
						"exclusiveMinimum": 0,
						"default": 10000
					},
					"feeRate": {
						"type": "number",
						"format": "double",
						"minimum": 0,
						"default": 0.001
					},
					"buyFeeRate": {
						"type": "number",
						"format": "double",
						"minimum": 0
					},
					"sellFeeRate": {
						"type": "number",
						"format": "double",
						"minimum": 0
					},
					"feeLeg": {
						"type": "string",
						"enum": [
							"RECEIVED",
							"QUOTE",
							"BASE"
						],
						"default": "RECEIVED"
					},
					"percentAmountToLock": {
						"type": "number",
						"format": "double",
						"exclusiveMinimum": 0,
						"maximum": 100
					}
				}
			},
			"ExecuteSweepRequest": {
				"type": "object",
				"required": [
					"strategyId",
					"sweep"
				],
				"properties": {
					"strategyId": {
						"$ref": "#/components/schemas/strategyId"
					},
					"sweep": {
						"$ref": "#/components/schemas/SweepSpecRequest"
					},
					"baseConfig": {
						"$ref": "#/components/schemas/SweepBaseConfig"
					},
					"storeSignals": {
						"type": "boolean",
						"default": false,
						"description": "Store signals for every trial. Keep false for normal sweeps."
					},
					"shards": {
						"type": "integer",
						"minimum": 0,
						"description": "Requested horizontal shard count; 0 or omitted selects automatically.",
						"default": 0
					},
					"minTradeFloor": {
						"type": "integer",
						"minimum": 0,
						"default": 30,
						"description": "Trials below this trade count are flagged but remain in the results."
					},
					"walkForward": {
						"$ref": "#/components/schemas/WalkForwardRequest"
					},
					"equityCurve": {
						"$ref": "#/components/schemas/EquityCurveRequest"
					}
				}
			},
			"EquityCurveOptions": {
				"type": "object",
				"description": "Requested equity-curve transform, applied server-side in a fixed pipeline order: `resample` (point count) then `differential` (encoding) then `outMode` (JSON shape) — each stage assumes the previous one already ran. A server-side size guard can still force a smaller/deflated shape above its thresholds regardless of what is requested here — see `EquityCurveMeta` for what actually happened.",
				"properties": {
					"resample": {
						"type": "integer",
						"minimum": 2,
						"description": "Downsample to at most this many points (extrema-preserving — the global max/min and the exact first/last point are always kept). Omit for no downsampling."
					},
					"differential": {
						"type": "boolean",
						"default": false,
						"description": "Delta-encode both fields from the second (post-resample) point onward."
					},
					"outMode": {
						"allOf": [
							{
								"$ref": "#/components/schemas/EquityCurveOutMode"
							}
						],
						"default": "ARRAY",
						"description": "Requested JSON shape."
					}
				}
			},
			"EquityCurveRequest": {
				"allOf": [
					{
						"$ref": "#/components/schemas/EquityCurveOptions"
					},
					{
						"type": "object",
						"properties": {
							"mode": {
								"type": "string",
								"enum": [
									"auto",
									"topN",
									"topPct",
									"none"
								],
								"default": "auto",
								"description": "Which trials keep their per-point equity curve. `auto` retains curves only while the accumulated size stays within server limits; `topN`/`topPct` retain curves for the best-ranked trials explicitly; `none` retains no curves."
							},
							"n": {
								"type": "integer",
								"minimum": 1,
								"description": "Trial count to retain when mode is topN."
							},
							"maxPct": {
								"type": "number",
								"format": "double",
								"exclusiveMinimum": 0,
								"maximum": 100,
								"description": "Top percentage of trials to retain when mode is topPct."
							}
						}
					}
				],
				"description": "Selection (`mode`/`n`/`maxPct`) plus the transform preference (`resample`/`differential`/`outMode`) applied by `GET .../equityCurve` whenever ITS OWN query params are absent, for a curve this sweep retained. The transform half never affects retention or `sweepId` — a caller can always override it per-request at read time regardless of what was submitted here."
			},
			"WalkForwardRequest": {
				"type": "object",
				"description": "Opt in to walk-forward validation. Present, the sweep runs as F sequential folds and the result gains a `walkForward` section; absent, nothing about the sweep changes. Two requests that differ only in this block are two different sweeps and do not deduplicate against each other.",
				"required": [
					"folds"
				],
				"properties": {
					"folds": {
						"type": "integer",
						"minimum": 2,
						"description": "How many sequential optimize-then-score windows to run. Two is the minimum for a reason, and it is structural rather than a tuning choice: parameter drift is measured between consecutive fold winners, and a single fold — one train/test split with no sequence — has no consecutive pair to compare, so it would report the strongest possible stability having measured nothing.\nThe upper bound is a server setting (12 by default) and is deliberately not pinned here, since a spec that hardcodes a tunable limit lies the day it is raised. Exceeding it, or exceeding the sweep budget once multiplied by the grid size, is a 400."
					},
					"inSamplePct": {
						"type": "integer",
						"minimum": 10,
						"maximum": 90,
						"default": 66,
						"description": "Share of the session each fold spends optimizing; the remainder is where its winner is scored. Lower values leave more data to be scored on and, on short sessions, are also what lets the requested fold count tile the data at all."
					}
				}
			},
			"ExecuteSweepAccepted": {
				"type": "object",
				"required": [
					"sweepId",
					"requestId",
					"totalRuns",
					"shards",
					"seed",
					"queued"
				],
				"properties": {
					"sweepId": {
						"type": "string",
						"example": "swp_95e47a7f0966ce11"
					},
					"requestId": {
						"type": "string"
					},
					"totalRuns": {
						"type": "integer",
						"minimum": 1
					},
					"shards": {
						"type": "integer",
						"minimum": 1
					},
					"seed": {
						"type": "integer",
						"format": "int64",
						"minimum": -9007199254740991,
						"maximum": 9007199254740991,
						"description": "Effective seed used to expand the sweep."
					},
					"queued": {
						"type": "boolean",
						"description": "False when an identical sweep already exists and was not enqueued again."
					},
					"walkForward": {
						"$ref": "#/components/schemas/WalkForwardAccepted"
					}
				}
			},
			"WalkForwardAccepted": {
				"type": "object",
				"description": "Echo of the accepted walk-forward configuration, present only when the submit carried one. `inSamplePct` is the resolved value, so a request that omitted it can see what it got.",
				"required": [
					"folds",
					"inSamplePct",
					"totalRuns"
				],
				"properties": {
					"folds": {
						"type": "integer"
					},
					"inSamplePct": {
						"type": "integer"
					},
					"totalRuns": {
						"type": "integer",
						"description": "What this sweep actually costs, `folds × (grid size + 1)` — the in-sample runs for every fold plus each fold's one out-of-sample run. Deliberately distinct from the top-level `totalRuns`, which stays the size of the grid that was submitted."
					}
				}
			},
			"SweepProgress": {
				"type": "object",
				"description": "How far along a sweep is, and — when the sweep is still running — enough to tell a healthy one from a stuck one. The counts partition the shards (or, for a walk-forward sweep, the folds): every unit is either finished, failed, waiting to be retried, or not yet started.",
				"required": [
					"done",
					"total",
					"aborted",
					"shardCount",
					"pendingShards",
					"failedShards",
					"retrying",
					"notStarted"
				],
				"properties": {
					"done": {
						"type": "integer",
						"format": "int64"
					},
					"total": {
						"type": "integer"
					},
					"aborted": {
						"type": "integer",
						"format": "int64",
						"description": "Individual runs that executed and aborted. A row-level count: a shard that fails before producing any rows leaves this at 0, which is why `failedShards` exists alongside it."
					},
					"shardCount": {
						"type": "integer"
					},
					"pendingShards": {
						"type": "integer"
					},
					"failedShards": {
						"type": "integer",
						"format": "int64",
						"description": "Shards (or folds) that failed and will not be retried. Distinct from `aborted`: this counts whole units that never reported, not runs that ran badly."
					},
					"retrying": {
						"type": "integer",
						"description": "Units whose last attempt failed on something transient — an I/O error, a worker that died mid-read — and which are queued to be attempted again. Not counted as failures, because they have not failed yet; a sweep with a non-zero value here is still expected to complete."
					},
					"notStarted": {
						"type": "integer",
						"description": "Units that have not reported anything yet. Covers both work still queued behind other work and work claimed by a worker that stopped before it began, which is why a sweep with a persistent value here and a rising `stalledSeconds` is worth looking at."
					},
					"stalledSeconds": {
						"type": "integer",
						"format": "int64",
						"description": "Seconds since anything last advanced. Omitted on a finished sweep, where it would only measure how long ago it finished, and on sweeps submitted before this field existed."
					},
					"etaSeconds": {
						"type": "integer",
						"format": "int64",
						"description": "Rough seconds remaining, extrapolated from the rate observed so far and assuming nothing else competes for workers. Runs conservative in practice — it has measured 2–5× long when a sweep spent part of its life waiting to be retried, since that wait dilutes the observed rate. **Omitted, never zero, when it cannot be computed**: a sweep with nothing finished yet has no rate to extrapolate from, and a zero would read as \"about to finish\". Excludes queue wait entirely; `retrying` and `stalledSeconds` are where that shows up."
					}
				}
			},
			"SweepRunRow": {
				"type": "object",
				"required": [
					"runIx",
					"params",
					"sharpe",
					"sortino",
					"pnl",
					"pnlPct",
					"cagr",
					"maxDdPct",
					"trades",
					"winRate",
					"belowTradeFloor",
					"aborted",
					"runtimeMs"
				],
				"properties": {
					"runIx": {
						"type": "integer",
						"minimum": 0,
						"description": "Deterministic zero-based expansion index, stable across shards and ranking."
					},
					"rank": {
						"type": "integer",
						"minimum": 1,
						"description": "Present only in the `ranked` view."
					},
					"plateauScore": {
						"type": "number",
						"format": "double",
						"description": "The objective of the worst run in this point's immediate neighbourhood — how well the region around it holds up, not how well it scored itself. Present only in the `ranked` view when plateau ranking applied. Always read together with `neighbourCount`."
					},
					"neighbourCount": {
						"type": "integer",
						"minimum": 0,
						"description": "How many neighbouring parameter points backed the `plateauScore`. Zero means the point had no neighbours in the grid, so its score is unevidenced rather than confirmed — the value alone cannot be distinguished from a genuinely robust one."
					},
					"deflatedSharpe": {
						"type": "number",
						"format": "double",
						"minimum": 0,
						"maximum": 1,
						"description": "Probability that this run's Sharpe reflects real edge rather than the best draw from however many parameter vectors were tried. Above ~0.95 the result survives the multiple-testing correction; near 0.5 or below it is indistinguishable from the best of a pile of coin flips. Absent on aborted runs, and on sweeps with too few trials to establish any dispersion to deflate against."
					},
					"params": {
						"type": "object",
						"additionalProperties": true
					},
					"sharpe": {
						"type": "number",
						"format": "double"
					},
					"sortino": {
						"type": "number",
						"format": "double"
					},
					"pnl": {
						"type": "number",
						"format": "double",
						"description": "Absolute net PnL in the output currency."
					},
					"pnlPct": {
						"type": "number",
						"format": "double"
					},
					"cagr": {
						"type": "number",
						"format": "double"
					},
					"maxDdPct": {
						"type": "number",
						"format": "double"
					},
					"trades": {
						"type": "integer",
						"format": "int64"
					},
					"winRate": {
						"type": "number",
						"format": "double"
					},
					"belowTradeFloor": {
						"type": "boolean"
					},
					"aborted": {
						"type": "boolean"
					},
					"runtimeMs": {
						"type": "integer",
						"format": "int64"
					},
					"equityCurve": {
						"allOf": [
							{
								"$ref": "#/components/schemas/EquityCurveResult"
							}
						],
						"description": "Present only when this trial's curve was selected (`equityCurve.mode` was `topN`/`topPct` on the request, and this row ranked among the winners) — absent, not null, otherwise; a run that was never spilled and one that was spilled but not selected look identical here. Always a pointer today (`url` present, no inline points): a sweep's curves live in a spill store, not the response, until fetched separately. `GET` the `url` to fetch the curve itself; its own `meta` there is the real, possibly size-guarded outcome, while this outer `meta` is a raw, untransformed preview from selection time and can differ."
					}
				}
			},
			"SweepSensitivity": {
				"type": "object",
				"description": "Sensitivity aggregates over a sweep's stored rows. Marginals are always complete; heatmaps may be capped, in which case `heatmapsTruncated` is true.",
				"properties": {
					"sweepId": {
						"type": "string"
					},
					"status": {
						"type": "string",
						"enum": [
							"RUNNING",
							"COMPLETED",
							"PARTIAL",
							"CANCELLED"
						]
					},
					"objective": {
						"type": "string",
						"enum": [
							"sharpe",
							"sortino",
							"pnl",
							"maxdd"
						]
					},
					"rowsAnalysed": {
						"type": "integer",
						"description": "Rows available when this was computed. Grows while a sweep is still running."
					},
					"marginals": {
						"type": "array",
						"items": {
							"$ref": "#/components/schemas/SweepMarginal"
						}
					},
					"heatmaps": {
						"type": "array",
						"items": {
							"$ref": "#/components/schemas/SweepHeatmap"
						}
					},
					"heatmapsTruncated": {
						"type": "boolean",
						"description": "True when at least one two-parameter surface was left out to stay inside the response budget. Told explicitly because a silently short list would read as \"these are all the interactions\", which is the wrong thing to conclude from a sensitivity view."
					}
				}
			},
			"SweepMarginal": {
				"type": "object",
				"description": "One axis, with every other axis collapsed away.",
				"properties": {
					"param": {
						"type": "string"
					},
					"points": {
						"type": "array",
						"items": {
							"$ref": "#/components/schemas/SweepMarginalPoint"
						}
					}
				}
			},
			"SweepMarginalPoint": {
				"type": "object",
				"description": "How the objective behaved at one value of one axis. `best` and `mean` disagreeing is informative rather than noise: a high `best` with a poor `mean` marks a value that only works alongside particular settings of the other axes.",
				"properties": {
					"value": {
						"description": "The axis value, as it appears in a run's parameters."
					},
					"count": {
						"type": "integer",
						"description": "Non-aborted runs that used this value."
					},
					"best": {
						"type": "number",
						"format": "double"
					},
					"mean": {
						"type": "number",
						"format": "double"
					},
					"worst": {
						"type": "number",
						"format": "double"
					}
				}
			},
			"SweepHeatmap": {
				"type": "object",
				"description": "The surface for one pair of axes, with all others collapsed away.",
				"properties": {
					"paramA": {
						"type": "string"
					},
					"paramB": {
						"type": "string"
					},
					"cells": {
						"type": "array",
						"items": {
							"$ref": "#/components/schemas/SweepHeatmapCell"
						}
					}
				}
			},
			"SweepHeatmapCell": {
				"type": "object",
				"properties": {
					"valueA": {},
					"valueB": {},
					"count": {
						"type": "integer"
					},
					"best": {
						"type": "number",
						"format": "double"
					},
					"mean": {
						"type": "number",
						"format": "double"
					}
				}
			},
			"ExecuteSweepResult": {
				"type": "object",
				"required": [
					"sweepId",
					"status",
					"objective",
					"order",
					"progress",
					"leaderboardSize",
					"truncated",
					"leaderboard",
					"state"
				],
				"properties": {
					"sweepId": {
						"type": "string"
					},
					"status": {
						"type": "string",
						"enum": [
							"RUNNING",
							"COMPLETED",
							"PARTIAL",
							"CANCELLED"
						],
						"description": "The sweep's own status vocabulary — not the same set `state.status` below uses. See `state` for why."
					},
					"objective": {
						"type": "string",
						"enum": [
							"sharpe",
							"sortino",
							"pnl",
							"maxdd"
						]
					},
					"order": {
						"type": "string",
						"enum": [
							"ranked",
							"natural"
						]
					},
					"ranking": {
						"type": "string",
						"enum": [
							"plateau",
							"raw"
						],
						"description": "Which ordering was actually applied, which is not always the one requested: a sweep with no stored parameter grid cannot be plateau-ranked and falls back to `raw`. Always `raw` when `order=natural`."
					},
					"pbo": {
						"type": "number",
						"format": "double",
						"minimum": 0,
						"maximum": 1,
						"description": "Probability of backtest overfitting for the sweep as a whole, by combinatorially symmetric cross-validation: how often the configuration that won in-sample lands below median out-of-sample. Above ~0.5 the sweep is selecting noise, whatever its top row says. Computed once when the last shard finishes, so it is absent while the sweep is still running and on sweeps too small for the statistic to mean anything."
					},
					"pboSplits": {
						"type": "integer",
						"description": "How many train/test splits the `pbo` figure was averaged over."
					},
					"failReason": {
						"type": "string",
						"description": "Why the sweep produced less than it should have — the cause reported by the **first** shard to fail, not a list. It is what turns an inscrutable empty leaderboard into an answer: a sweep can come back `PARTIAL` with `done: 0` because the strategy could not be loaded at all, and without this the response says only that nothing finished.\nFirst failure wins and later ones are not recorded, so on a sweep where several shards failed for different reasons this names one of them rather than all. Absent when no shard reported a cause, which is the normal case for a healthy sweep — read it together with `progress.failedShards` rather than as a count of anything.",
						"example": "Failed to load/configure strategy"
					},
					"progress": {
						"$ref": "#/components/schemas/SweepProgress"
					},
					"leaderboardSize": {
						"type": "integer",
						"description": "Total result rows currently available."
					},
					"truncated": {
						"type": "boolean",
						"description": "True only when the ranked view exceeds its display limit."
					},
					"leaderboard": {
						"type": "array",
						"items": {
							"$ref": "#/components/schemas/SweepRunRow"
						}
					},
					"walkForward": {
						"$ref": "#/components/schemas/WalkForwardResult"
					},
					"state": {
						"allOf": [
							{
								"$ref": "#/components/schemas/JobState"
							}
						],
						"description": "The same `JobState` shape a single-execute `BacktestJobResult` carries — not a sweep-specific lookalike, the actual type, so field names and timestamp formatting match exactly.\n`state.status` uses `JobState`'s own vocabulary (`New`/`Started`/`Completed`/ `Aborted`/`Failed`), mapped from the sweep's `status` field above rather than copying it: `PARTIAL` and `CANCELLED` both map to `Aborted`, because a sweep's `PARTIAL` is already terminal (some shards finished, some failed, nothing more is coming) unlike a single job's non-terminal `Partial`, which has no equivalent here at all.\n`state.completed` is real ticks processed on a plain sweep. On a `walkForward` sweep it is currently always `0` — the walk-forward fold runner was not wired to count ticks when this shipped, unlike the plain shard path.\n`state.size` is always `0` on every path (single execute, plain sweep, walk-forward alike) — nothing populates it anywhere yet. That is a known gap across the whole API, not a sweep-specific omission."
					}
				}
			},
			"WalkForwardResult": {
				"type": "object",
				"description": "Present only on a sweep submitted with `walkForward`, and present from acceptance onward — its presence, not its contents, is what identifies a walk-forward sweep. `completedFolds` is 0 while the first fold is still running.",
				"required": [
					"folds",
					"completedFolds",
					"results"
				],
				"properties": {
					"folds": {
						"type": "integer",
						"description": "Folds requested at submit."
					},
					"inSamplePct": {
						"type": "integer",
						"description": "Resolved in-sample share each fold optimized on."
					},
					"completedFolds": {
						"type": "integer",
						"description": "Folds that have finished and reported a winner."
					},
					"paramDrift": {
						"type": "number",
						"format": "double",
						"minimum": 0,
						"description": "Mean normalized lattice distance between consecutive fold winners. Low is good: winners that stay in a tight band fold after fold are evidence the parameter means something, while winners that jump across the grid every time are the sweep re-fitting noise, and that backtest will not survive contact with live data. **Absent is not zero** — the field is omitted whenever the figure could not be computed (fewer than two folds finished, no stored grid to place winners on), because zero is itself a meaningful reading here and a placeholder would be indistinguishable from perfect stability."
					},
					"results": {
						"type": "array",
						"description": "One entry per completed fold, oldest first.",
						"items": {
							"$ref": "#/components/schemas/WalkForwardFold"
						}
					}
				}
			},
			"WalkForwardFold": {
				"type": "object",
				"description": "What one fold concluded. The out-of-sample row is the answer; the in-sample figure is only there to be compared against it, since any grid produces a flattering in-sample winner — that is what optimizing does. The gap between them is the whole reading.",
				"required": [
					"foldIx",
					"inSampleFrom",
					"inSampleTo",
					"outOfSampleTo",
					"params",
					"inSampleSharpe",
					"outOfSample",
					"vectorsRun"
				],
				"properties": {
					"foldIx": {
						"type": "integer",
						"description": "Position in the walk-forward sequence, oldest first."
					},
					"inSampleFrom": {
						"type": "integer",
						"description": "First index of the optimization window, into the prepared session."
					},
					"inSampleTo": {
						"type": "integer",
						"description": "End of the optimization window, exclusive — and where scoring begins."
					},
					"outOfSampleTo": {
						"type": "integer",
						"description": "End of the scoring window, exclusive."
					},
					"params": {
						"type": "object",
						"additionalProperties": true,
						"description": "The parameter vector that won this fold's optimization window."
					},
					"inSampleSharpe": {
						"type": "number",
						"format": "double",
						"description": "How that winner scored on the window it was chosen on."
					},
					"outOfSample": {
						"$ref": "#/components/schemas/SweepRunRow"
					},
					"vectorsRun": {
						"type": "integer",
						"description": "Vectors this fold evaluated in-sample before picking its winner."
					}
				}
			},
			"AcceptedJob": {
				"type": "object",
				"description": "Response returned by async endpoints (`202 Accepted`). The `jobId` is deterministic for the\nsame input parameters — repeated calls with identical params return the same id.\n",
				"required": [
					"jobId"
				],
				"properties": {
					"jobId": {
						"type": "string",
						"description": "Unique job identifier; use this to poll for completion.",
						"example": "13RBLGQlPnfDjO6wyKSX8i"
					}
				},
				"example": {
					"jobId": "13RBLGQlPnfDjO6wyKSX8i"
				}
			},
			"BacktestJobResult": {
				"type": "object",
				"description": "Backtest job result.",
				"required": [
					"results",
					"state"
				],
				"properties": {
					"results": {
						"$ref": "#/components/schemas/ResultMap"
					},
					"state": {
						"$ref": "#/components/schemas/JobState"
					}
				}
			},
			"ResultMap": {
				"type": "object",
				"description": "Execution result map. Always includes core fields (hostName, iops, strategyId, instrument). Yield metrics (pnlTotal, pnlTotalPercent, totalTrades, winRate, equityCurve, etc.) are present when the strategy emitted at least one trade. When signal storage is enabled, includes signal fields described below. `notices` carries what the run had to say about itself, and is absent when it had nothing.",
				"required": [
					"strategyId",
					"instrument"
				],
				"properties": {
					"hostName": {
						"type": "string",
						"description": "Identifier of the worker that executed the strategy. Useful when reporting issues so support can correlate with logs.",
						"example": "executor10"
					},
					"iops": {
						"type": "number",
						"format": "double",
						"description": "Instrument operations per second throughput during execution",
						"example": 123956.53
					},
					"strategyId": {
						"type": "string",
						"description": "**Not the `strategyId` you compiled with** — this is the execution context id,\n`strategy:<user>:<strategyId>`. The compiled strategy's id is the last `:`-separated\nsegment; that, not this whole string, is what `GET /strategy/{strategyId}` takes.\n\nTake the segment after the last `:` rather than counting from the front: the shape has\nchanged once already and callers that indexed a fixed position broke on it.\n",
						"example": "strategy:00000000-0000-0000-0000-000000000000:2iyvtenlzh9dabqtxn7nbv"
					},
					"instrument": {
						"type": "string",
						"description": "The instrument (currency pair) that was backtested",
						"example": "BTC/USDT"
					},
					"notices": {
						"type": "array",
						"description": "Diagnostics the engine raised over this run, each with `provenance: execute`.\n\n**Absent means nothing was raised.** This is the one surface where silence is a real\nanswer: the run happened, over your data, start to finish, and the engine found nothing\nworth saying. That is not true of the compile path, where an empty list only means a\nshort synthetic series reached nothing — see `GET /strategy/{strategyId}`.\n\nNotices are raised on failed and aborted runs too, and those are the ones most worth\nreading: a run that produced no trades often did so for a reason stated here.\n",
						"items": {
							"$ref": "#/components/schemas/Notice"
						}
					},
					"noticesTruncated": {
						"type": "integer",
						"description": "How many notices were dropped past the cap of 50. Absent when none were. A large value usually means one fault repeating per instrument or per parameter vector rather than 50 distinct problems.",
						"example": 3
					},
					"pnlTotal": {
						"type": "number",
						"format": "double",
						"description": "Total profit and loss in the output currency",
						"example": 42.75
					},
					"pnlTotalPercent": {
						"type": "number",
						"format": "double",
						"description": "Total PnL as a percentage of the initial capital (`backtestFunding`). Zero when `backtestFunding` is 0.",
						"example": 42.75
					},
					"totalTrades": {
						"type": "integer",
						"format": "int64",
						"description": "Total number of trades executed by the strategy",
						"example": 156
					},
					"winRate": {
						"type": "number",
						"format": "double",
						"description": "Percentage of profitable trades (0-100)",
						"example": 58.33
					},
					"sharpeRatio": {
						"type": "number",
						"format": "double",
						"description": "Risk-adjusted return ratio (mean return / standard deviation of returns)",
						"example": 1.245
					},
					"sortinoRatio": {
						"type": "number",
						"format": "double",
						"description": "Downside risk-adjusted return ratio (mean return / downside deviation)",
						"example": 1.872
					},
					"cagr": {
						"type": "number",
						"format": "double",
						"description": "Compound Annual Growth Rate",
						"example": 0.1534
					},
					"maxDrawdown": {
						"type": "number",
						"format": "double",
						"description": "Maximum absolute drawdown in the output currency",
						"example": 12.5
					},
					"maxDrawdownPercent": {
						"type": "number",
						"format": "double",
						"description": "Maximum percentage drawdown from peak equity",
						"example": 8.75
					},
					"equityCurve": {
						"allOf": [
							{
								"$ref": "#/components/schemas/EquityCurveResult"
							}
						],
						"description": "Equity curve over the backtest. `points[0]` (or `timestamps[0]`/`equities[0]` in `SHORT` mode) is an anchor at the backtest `from` with `initialCapital`; the remaining points are one sample per emitted yield, in order. Use it to plot the strategy's running equity without re-deriving it from the yield history. Always inline for a plain backtest today — never a pointer (`url`); that shape exists only for a sweep row's top-N winners.",
						"example": {
							"points": [
								{
									"timestamp": 1700000000000,
									"equity": 100
								},
								{
									"timestamp": 1700000060000,
									"equity": 110.5
								},
								{
									"timestamp": 1700000120000,
									"equity": 90.25
								}
							],
							"meta": {
								"inputPointCount": 3,
								"outputPointCount": 3,
								"resampled": false,
								"differential": false,
								"outMode": "ARRAY"
							}
						}
					},
					"signalCount": {
						"type": "integer",
						"description": "Number of signals emitted during strategy execution",
						"example": 100000
					},
					"signalsId": {
						"type": "string",
						"description": "Storage key for the signals file. Treat as opaque; use signalsUrl to download.",
						"example": "00000000-0000-0000-0000-000000000000/exec/binance/3vsndwikcuaatjmb83fjtl"
					},
					"signalsUrl": {
						"type": "string",
						"format": "uri",
						"description": "HTTPS URL to download the signals Parquet file. Use signalsUpload to know when it's ready.",
						"example": "https://storage.qtsurfer.com/00000000-0000-0000-0000-000000000000/exec/binance/3vsndwikcuaatjmb83fjtl.parquet"
					},
					"signalsUpload": {
						"type": "string",
						"enum": [
							"Done",
							"Failed",
							"Skipped"
						],
						"description": "Upload status. Done = signal file is available at signalsUrl. Failed = upload error (see signalsUploadReason). Skipped = no signals emitted.",
						"example": "Done"
					},
					"signalsUploadedAt": {
						"type": "string",
						"format": "date-time",
						"description": "ISO 8601 timestamp of when the upload completed. Only present when signalsUpload is Done.",
						"example": "2026-03-18T13:21:48.170Z"
					},
					"signalsUploadReason": {
						"type": "string",
						"description": "Human-readable reason when signalsUpload is Failed or Skipped.",
						"example": "signal file generation failed"
					}
				}
			},
			"EquityPoint": {
				"type": "object",
				"description": "Single sample of the running equity at a yield event.",
				"required": [
					"timestamp",
					"equity"
				],
				"properties": {
					"timestamp": {
						"type": "integer",
						"format": "int64",
						"description": "Epoch milliseconds. The first point in an equity curve is anchored at the backtest `from`; subsequent points carry the timestamp of each emitted yield.",
						"example": 1700000000000
					},
					"equity": {
						"type": "number",
						"format": "double",
						"description": "Running equity at this point (`initialCapital + cumulativePnl`).",
						"example": 110.5
					}
				}
			},
			"EquityCurveOutMode": {
				"type": "string",
				"enum": [
					"ARRAY",
					"SHORT"
				],
				"description": "JSON shape for an equity curve's points. `ARRAY` is `[{timestamp, equity}, ...]`; `SHORT` is `{timestamps: [...], equities: [...]}` (parallel arrays, no repeated key text). The one schema shared by every place `outMode` appears, request or response, so the two cannot drift to different value sets."
			},
			"EquityCurveMeta": {
				"type": "object",
				"description": "What the transform pipeline actually did, computed from the observed outcome — never a copy of what was requested. Lets a caller detect a forced or no-op transform (e.g. a `resample` ceiling already above the curve's size is a legal no-op, reported honestly as `resampled: false`).",
				"required": [
					"inputPointCount",
					"outputPointCount",
					"resampled",
					"differential",
					"outMode"
				],
				"properties": {
					"inputPointCount": {
						"type": "integer",
						"description": "Size of the curve the transform pipeline received.",
						"example": 100000
					},
					"outputPointCount": {
						"type": "integer",
						"description": "Size after the full pipeline (resample, then differential, then outMode).",
						"example": 100
					},
					"resampled": {
						"type": "boolean",
						"description": "True only if the resample stage actually changed the point count."
					},
					"differential": {
						"type": "boolean",
						"description": "True only if delta-encoding actually ran. Requesting it on a curve of 0 or 1 points has nothing to encode, so it does not run even if asked."
					},
					"outMode": {
						"allOf": [
							{
								"$ref": "#/components/schemas/EquityCurveOutMode"
							}
						],
						"description": "The actual JSON shape served. Present even when the points themselves are not (a pointer curve, `EquityCurveResult.url`) — a caller resolving that URL separately still needs to know how to parse what it gets back before fetching it. May override an explicit request above a server-side size threshold; this field, not the request, is the source of truth for what shape actually came back.",
						"example": "ARRAY"
					}
				}
			},
			"EquityCurveResult": {
				"type": "object",
				"description": "An equity curve, shaped per `meta.outMode`: `points` when `ARRAY`, `timestamps` + `equities` (parallel arrays) when `SHORT`. Used identically wherever a curve is returned — a plain backtest's inline `equityCurve` and a sweep row's `equityCurve` are the same type. `url` is present *instead of* any points when the curve is served by pointer rather than inline (a sweep row's top-N winners only): `GET` it separately to fetch this exact same shape with the points populated.",
				"required": [
					"meta"
				],
				"properties": {
					"meta": {
						"$ref": "#/components/schemas/EquityCurveMeta"
					},
					"points": {
						"type": "array",
						"description": "Present when `meta.outMode` is `ARRAY` and the curve is inline (not a pointer).",
						"items": {
							"$ref": "#/components/schemas/EquityPoint"
						}
					},
					"timestamps": {
						"type": "array",
						"description": "Present when `meta.outMode` is `SHORT` and the curve is inline (not a pointer).",
						"items": {
							"type": "integer",
							"format": "int64"
						}
					},
					"equities": {
						"type": "array",
						"description": "Present when `meta.outMode` is `SHORT` and the curve is inline (not a pointer), parallel to `timestamps` (same index, same point).",
						"items": {
							"type": "number",
							"format": "double"
						}
					},
					"url": {
						"type": "string",
						"description": "Present only for a sweep row's pointer curve. `GET` this to fetch the curve itself, in this exact `{points|timestamps+equities, meta}` shape — `meta` there is the real, possibly size-guarded outcome; this outer `meta` is a raw, untransformed preview from the moment the sweep selected this trial's curve, and the two can legitimately differ.",
						"example": "/v1/backtest/binance/ticker/executeSweep/req-1/swp_test/runs/3/equityCurve"
					}
				}
			},
			"strategyId": {
				"description": "Unique identifier for a compiled strategy, derived from the source itself: the same code\nalways yields the same id, for every caller, whatever its formatting. See\n`POST /strategy` for exactly which rewrites preserve it and which do not.\n",
				"type": "string",
				"example": "6bsh31ikwkuivhtgcoa6s4"
			},
			"DeclaredProperty": {
				"type": "object",
				"description": "One property name `POST /strategy` could establish without constructing the strategy —\neither declared with `@StrategyProperty` on the compiled source, or one of the small set of\nbase properties every strategy carries (`amnt`, `enabled`, `multiEntry`, ...).\n\n**Best-effort, not exhaustive.** A property registered through an attached risk/backtest\nconfig needs a live instance to discover and is not listed here. Use this to catch a typo'd\nsweep key before submitting, not as the definitive list of what a sweep will accept — a\nname absent from this list may still be valid.\n",
				"required": [
					"name"
				],
				"properties": {
					"name": {
						"type": "string",
						"description": "The key a sweep or execute param map uses for this property.",
						"example": "rsi.period"
					},
					"description": {
						"type": "string",
						"description": "Human-readable label, as declared.",
						"example": "RSI period"
					},
					"defaultValue": {
						"type": "string",
						"description": "The declared default, as a string, if one was given. Absent, not null, when none was\ndeclared.\n",
						"example": "14"
					},
					"reflected": {
						"type": "boolean",
						"description": "Whether a value for this key is injected into the strategy's field (`true`) or only\navailable through the property map (`false`).\n",
						"example": true
					},
					"min": {
						"type": "number",
						"format": "double",
						"description": "Suggested sweep/range minimum, if declared. Advisory only, never validated.",
						"example": 2
					},
					"max": {
						"type": "number",
						"format": "double",
						"description": "Suggested sweep/range maximum, if declared. Advisory only, never validated.",
						"example": 50
					},
					"step": {
						"type": "number",
						"format": "double",
						"description": "Suggested sweep/range step, if declared. Advisory only, never validated.",
						"example": 1
					}
				}
			},
			"Notice": {
				"type": "object",
				"description": "A diagnostic the engine raised while the strategy ran. Advisory: it describes something worth\nknowing about how the strategy is wired, not necessarily an error.\n",
				"required": [
					"level",
					"code",
					"message"
				],
				"properties": {
					"level": {
						"type": "string",
						"description": "Severity as the engine classified it.",
						"example": "WARN"
					},
					"code": {
						"type": "string",
						"description": "Stable identifier for the kind of finding; safe to match on.",
						"example": "indicator.bar-data-on-ticker-path"
					},
					"message": {
						"type": "string",
						"description": "Human-readable explanation.",
						"example": "Indicator requires bar data but is on the ticker path"
					},
					"provenance": {
						"type": "string",
						"enum": [
							"execute",
							"compile-dry-run"
						],
						"description": "Where it came from, which matters because the two silences differ: an empty list from a\nreal run (`execute`) is a clean bill of health, while an empty list from\n`compile-dry-run` is only a lower bound over a bounded synthetic series.\n",
						"example": "compile-dry-run"
					}
				}
			},
			"StrategySummary": {
				"type": "object",
				"description": "One entry from `GET /strategies` — the same provenance a full `StrategyState` carries\n(`compiledAt`, `requiredSources`), without its validation state, so listing stays cheap\nregardless of how many strategies you have registered. Check a specific strategy's\nvalidation with `GET /strategy/{strategyId}`.\n",
				"required": [
					"strategyId"
				],
				"properties": {
					"strategyId": {
						"$ref": "#/components/schemas/strategyId"
					},
					"compiledAt": {
						"type": "string",
						"format": "date-time",
						"description": "When the live compilation was produced."
					},
					"requiredSources": {
						"type": "array",
						"description": "The market data this strategy needs. Absent, not empty, when it could\nnot be established without constructing the strategy.\n",
						"items": {
							"type": "string"
						}
					}
				}
			},
			"StrategyState": {
				"type": "object",
				"description": "What is known about a registered strategy: that it compiled, and what validating it found.\n\n**`validation: passed` does not mean the strategy is correct.** It means the class loaded and\nsurvived the first event of a short synthetic run — a floor, not a guarantee. When\n`dryRunIncomplete` is true it is a lower floor still, because the run did not finish.\n",
				"required": [
					"strategyId",
					"validation"
				],
				"properties": {
					"strategyId": {
						"$ref": "#/components/schemas/strategyId"
					},
					"validation": {
						"type": "string",
						"enum": [
							"not_validated",
							"pending",
							"passed",
							"failed"
						],
						"description": "* `not_validated` — registered, never checked. `POST /strategy/{strategyId}/validate`\n  checks it.\n* `pending` — a check was asked for and has not answered yet.\n* `passed` — the class loaded and survived its first event.\n* `failed` — it did not; `detail` says how.\n",
						"example": "passed"
					},
					"compiledAt": {
						"type": "string",
						"format": "date-time",
						"description": "When the live compilation was produced."
					},
					"requiredSources": {
						"type": "array",
						"description": "The market data a strategy needs, read off the compiled class rather than off anything\nyou sent — `TickerStrategy`, `KlineStrategy` and `FundingRateStrategy` each declare one,\nand a `MultiSourceStrategy` declares a set.\n\n**Absent is not \"needs nothing\".** A strategy always needs market data, so an absent\nfield never means an empty requirement — it means the platform could not establish the\nanswer without constructing your strategy, which it will not do to fill in a field.\nThat happens for a `MultiSourceStrategy`, for a class that overrides\n`getMarketDataSource()`, and for anything registered before this field existed;\nre-registering the source fills it in.\n",
						"items": {
							"type": "string",
							"enum": [
								"Ticker",
								"KLine",
								"FundingRate"
							]
						},
						"example": [
							"Ticker"
						]
					},
					"validatedAt": {
						"type": "string",
						"format": "date-time",
						"description": "When the verdict was recorded. Absent until there is one."
					},
					"detail": {
						"type": "string",
						"description": "Why validation failed, or why a queued check has not reported. Present on `failed`, and\nalongside `validationStalled`.\n"
					},
					"notices": {
						"type": "array",
						"description": "What the run surfaced. An empty or absent list is not a clean bill of health when\n`dryRunIncomplete` is true — see that field.\n",
						"items": {
							"$ref": "#/components/schemas/Notice"
						}
					},
					"noticesTruncated": {
						"type": "integer",
						"description": "How many notices were dropped past the cap. Absent when none were.",
						"example": 3
					},
					"dryRunIncomplete": {
						"type": "boolean",
						"description": "The check did not finish its budget — it ran out of time, was refused because the\nplatform was already holding too many unfinishable runs, or hit a failure attributable to\nthe synthetic instrument rather than to your strategy. The verdict stands as far as it\nwent; it simply reached less than a full run would.\n"
					},
					"validationStalled": {
						"type": "boolean",
						"description": "A queued check has not reported for far longer than one takes. Nothing is disproved about\nthe strategy — the check has not run. Stop waiting and re-request it later.\n"
					},
					"_links": {
						"$ref": "#/components/schemas/StrategyLinks"
					}
				},
				"example": {
					"strategyId": "6bsh31ikwkuivhtgcoa6s4",
					"validation": "passed",
					"compiledAt": "2026-08-04T16:23:04Z",
					"requiredSources": [
						"Ticker"
					],
					"validatedAt": "2026-08-04T16:24:11Z",
					"notices": [
						{
							"level": "WARN",
							"code": "indicator.bar-data-on-ticker-path",
							"message": "Indicator requires bar data but is on the ticker path",
							"provenance": "compile-dry-run"
						}
					],
					"_links": {
						"code": {
							"href": "/v1/strategy/6bsh31ikwkuivhtgcoa6s4/code"
						}
					}
				}
			},
			"Dataset": {
				"type": "object",
				"description": "A dataset's own metadata — not its data. `currentVersionId` is what a prepare against\n`exchangeId: user` reads by default; see `DatasetVersion` for what a version carries.\n\n`from`/`to`/`cadence` mirror that current version's own discovered range and cadence, so\nyou don't need a second call to `GET /datasets/{datasetId}/uploads/{uploadId}` just to see\nwhat a dataset covers. Absent until a version exists.\n",
				"required": [
					"datasetId",
					"name",
					"type",
					"instrument",
					"createdAt"
				],
				"properties": {
					"datasetId": {
						"type": "string",
						"description": "Opaque id, returned by `POST /datasets`.",
						"example": "ds_3f9a1c2e7b0d4a5f"
					},
					"name": {
						"type": "string",
						"description": "Unique among your datasets.",
						"example": "My BTC ticks"
					},
					"type": {
						"type": "string",
						"enum": [
							"ticker"
						],
						"description": "Always `ticker` in v1.",
						"example": "ticker"
					},
					"instrument": {
						"$ref": "#/components/schemas/Instrument"
					},
					"createdAt": {
						"type": "string",
						"format": "date-time",
						"description": "When the dataset was created.",
						"example": "2026-08-20T09:00:00Z"
					},
					"currentVersionId": {
						"type": "string",
						"description": "The id of the most recently finalized, successfully ingested version. Absent until at\nleast one upload has finished ingesting.\n",
						"example": "dsv_8e2b4f19c6a03d7e"
					},
					"updatedAt": {
						"type": "string",
						"format": "date-time",
						"description": "When `currentVersionId` last changed. Absent until it has a value.",
						"example": "2026-08-20T09:04:12Z"
					},
					"from": {
						"type": "string",
						"format": "date-time",
						"description": "Start of `currentVersionId`'s own data range, as discovered at ingest time. Absent\nuntil a version exists.\n",
						"example": "2026-03-01T00:00:00Z"
					},
					"to": {
						"type": "string",
						"format": "date-time",
						"description": "End of `currentVersionId`'s own data range, as discovered at ingest time. Absent until\na version exists.\n",
						"example": "2026-03-08T00:00:00Z"
					},
					"cadence": {
						"type": "string",
						"description": "`currentVersionId`'s own discovered bar cadence (e.g. `1s`, `1m`, `1h`). Absent until a\nversion exists.\n",
						"example": "1m"
					}
				}
			},
			"DatasetWithLinks": {
				"description": "A `Dataset` plus a self link. Returned by `GET /datasets/{datasetId}`.",
				"allOf": [
					{
						"$ref": "#/components/schemas/Dataset"
					},
					{
						"type": "object",
						"properties": {
							"_links": {
								"type": "object",
								"properties": {
									"self": {
										"type": "object",
										"properties": {
											"href": {
												"type": "string",
												"example": "/v1/datasets/ds_3f9a1c2e7b0d4a5f"
											}
										}
									}
								}
							}
						}
					}
				]
			},
			"DatasetUploadTarget": {
				"description": "A presigned destination for uploading a raw dataset file directly to storage.",
				"type": "object",
				"required": [
					"url",
					"expiresInMinutes"
				],
				"properties": {
					"url": {
						"type": "string",
						"description": "Presigned URL. `PUT` the raw CSV file here directly — no `Authorization` header,\nno other API credentials.\n",
						"example": "https://storage.qtsurfer.com/uploads/00000000-.../up_1a2b3c4d5e6f7a8b/raw.csv?X-Amz-..."
					},
					"expiresInMinutes": {
						"type": "integer",
						"description": "How long `url` stays valid.",
						"example": 15
					}
				}
			},
			"DatasetUploadSession": {
				"description": "An upload session — an id plus the presigned URL to PUT the raw file to. Returned both by\n`POST /datasets` (as part of the new dataset) and by `POST /datasets/{datasetId}/uploads`\n(on its own, for an existing one).\n",
				"type": "object",
				"required": [
					"uploadId",
					"upload"
				],
				"properties": {
					"uploadId": {
						"type": "string",
						"description": "Identifies this upload session. Pass to\n`POST /datasets/{datasetId}/uploads/{uploadId}/finalize` once the PUT completes.\n",
						"example": "up_1a2b3c4d5e6f7a8b"
					},
					"upload": {
						"$ref": "#/components/schemas/DatasetUploadTarget"
					}
				}
			},
			"DatasetCreated": {
				"description": "The metadata available immediately after creating a dataset, plus its first upload\nsession — the presigned URL to PUT the file to. Version-derived fields such as\n`createdAt`, `currentVersionId`, range, and cadence are available from `GET /datasets/{datasetId}`\nafter the relevant lifecycle stages, not in this creation response.\n",
				"allOf": [
					{
						"type": "object",
						"required": [
							"datasetId",
							"name",
							"type",
							"instrument"
						],
						"properties": {
							"datasetId": {
								"type": "string",
								"description": "Opaque id of the newly created dataset.",
								"example": "ds_3f9a1c2e7b0d4a5f"
							},
							"name": {
								"type": "string",
								"description": "Unique name of the newly created dataset.",
								"example": "My BTC ticks"
							},
							"type": {
								"type": "string",
								"enum": [
									"ticker"
								],
								"description": "Always `ticker` in v1.",
								"example": "ticker"
							},
							"instrument": {
								"$ref": "#/components/schemas/Instrument"
							}
						}
					},
					{
						"$ref": "#/components/schemas/DatasetUploadSession"
					}
				]
			},
			"DatasetVersion": {
				"type": "object",
				"description": "One successfully ingested upload. Cadence and timestamp unit are discovered from the file,\nnot declared by the caller.\n",
				"required": [
					"datasetId"
				],
				"properties": {
					"datasetId": {
						"type": "string",
						"example": "ds_3f9a1c2e7b0d4a5f"
					},
					"id": {
						"type": "string",
						"description": "The version id. Pass as `datasetVersionId` on `POST .../prepare` to pin it.",
						"example": "dsv_8e2b4f19c6a03d7e"
					},
					"bytes": {
						"type": "integer",
						"description": "Size of the uploaded file.",
						"example": 4831022
					},
					"rows": {
						"type": "integer",
						"description": "Number of data rows.",
						"example": 86400
					},
					"cadence": {
						"type": "string",
						"description": "The discovered bar cadence (e.g. `1s`, `1m`, `1h`).",
						"example": "1s"
					},
					"timestampUnit": {
						"type": "string",
						"enum": [
							"iso",
							"s",
							"ms",
							"us"
						],
						"description": "The unit the `timestamp` column was uploaded in — ISO-8601, or the epoch band its\nnumeric values fell in (seconds, millis, or micros).\n",
						"example": "iso"
					},
					"gaps": {
						"type": "integer",
						"description": "Number of gaps at the discovered cadence.",
						"example": 0
					},
					"largestGapSteps": {
						"type": "integer",
						"description": "The largest gap, in units of the discovered cadence step.",
						"example": 0
					}
				}
			},
			"DatasetUploadState": {
				"type": "object",
				"description": "Progress of one upload, from staged through ingest. Postgres-backed once a version exists,\nso `ready`/`failed` are permanent answers; `uploading`/`ingesting` reflect in-flight state\nthat can itself age out — see the `404` case on `GET .../uploads/{uploadId}`.\n",
				"required": [
					"uploadId",
					"status"
				],
				"properties": {
					"uploadId": {
						"type": "string",
						"example": "up_1a2b3c4d5e6f7a8b"
					},
					"status": {
						"type": "string",
						"enum": [
							"uploading",
							"ingesting",
							"ready",
							"failed"
						],
						"description": "* `uploading` — the file was PUT to the presigned URL, but `finalize` has not been\n  called yet.\n* `ingesting` — `finalize` was called; the worker is parsing and validating the file.\n* `ready` — ingested successfully. `version` carries the result.\n* `failed` — ingest rejected the file (e.g. bad CSV contract, mixed timestamp units).\n",
						"example": "ready"
					},
					"jobId": {
						"type": "string",
						"description": "The ingest job id, while `status` is `ingesting`."
					},
					"version": {
						"allOf": [
							{
								"$ref": "#/components/schemas/DatasetVersion"
							}
						],
						"description": "Present when `status` is `ready` or `failed`."
					}
				}
			},
			"AuthTokenResponse": {
				"type": "object",
				"required": [
					"access_token",
					"token_type",
					"expires_in",
					"tier"
				],
				"properties": {
					"access_token": {
						"type": "string",
						"description": "Short-lived HS256 JWT. Send as `Authorization: Bearer <token>` on all other endpoints."
					},
					"token_type": {
						"type": "string",
						"enum": [
							"Bearer"
						],
						"description": "Always `Bearer`."
					},
					"expires_in": {
						"type": "integer",
						"description": "Seconds until the JWT expires (typically 3600).",
						"example": 3600
					},
					"scopes": {
						"type": "array",
						"description": "Scopes granted to this token. Reserved for future use; currently always empty.",
						"items": {
							"type": "string"
						},
						"example": []
					},
					"tier": {
						"type": "string",
						"enum": [
							"free",
							"basic",
							"pro",
							"elite"
						],
						"description": "Subscription tier this token was issued for. Drives rate limits and feature flags on downstream endpoints.",
						"example": "free"
					}
				}
			},
			"AuthTokenError": {
				"type": "object",
				"required": [
					"code",
					"message"
				],
				"description": "Error envelope returned by `POST /auth/token` when the API key is rejected.",
				"properties": {
					"code": {
						"type": "string",
						"enum": [
							"invalid_apikey",
							"apikey_revoked",
							"apikey_expired"
						],
						"description": "Machine-readable error reason."
					},
					"message": {
						"type": "string",
						"description": "Human-readable description of the failure."
					}
				}
			}
		},
		"securitySchemes": {
			"bearerAuth": {
				"type": "http",
				"scheme": "bearer",
				"bearerFormat": "JWT",
				"description": "JWT bearer token. The `sub` claim must contain the user identifier."
			},
			"apiKeyAuth": {
				"type": "apiKey",
				"in": "header",
				"name": "X-API-Key",
				"description": "Long-lived API key, issued via the web app. Used **only** against\n`POST /auth/token` to exchange for a short-lived JWT. All other\nendpoints expect the resulting JWT via `bearerAuth`.\n"
			}
		}
	},
	"tags": [
		{
			"name": "Auth",
			"description": "The **auth** endpoint exchanges a long-lived API key for a short-lived\nJWT used by every other endpoint. Issue an API key via the web app, then\ncall `POST /auth/token` at the start of each session (and on `401`\nresponses) to obtain a fresh JWT.\n\n**Key functionalities**:\n- Exchange an API key for a JWT carrying the caller's subscription tier.\n- Refresh the JWT before expiry without re-using the API key against any\n  other endpoint."
		},
		{
			"name": "Exchange",
			"description": "This set of endpoints allows interaction with various **exchanges** for cryptocurrencies and financial assets. With these endpoints, users can access information about available exchanges, \nretrieve the instruments (currency pairs or assets) offered by each exchange, and perform analyses on them. The data provided by these endpoints is crucial for strategic decision-making \nwithin the trading platform.\n\n**Key functionalities**:\n- Retrieve the list of available exchanges on the platform.\n- Get the instruments available on a specific exchange (currency pairs, assets, etc.)."
		},
		{
			"name": "Backtesting",
			"description": "The **backtest** endpoints enable users to test trading strategies based on historical data before applying them in real-time trading. This functionality is essential for traders who want\nto evaluate the performance of a strategy under past market conditions and optimize it before implementing it in a live trading environment.\n\n**Key functionalities**:\n- Prepare historical market data for fast access.\n- Run simulations of trading strategies using prepared historical market data.\n- Assess performance and optimize strategies based on past results and metrics."
		},
		{
			"name": "Strategy",
			"description": "The **strategy** endpoints allow users to submit, compile, and validate trading strategies within the platform. This feature is crucial for traders who develop automated trading systems and\nwish to ensure their strategy is both executable and effective before deploying it in a live/backtesting environment.\n\n**Key functionalities**:\n- Submit rich Java&trade; based trading strategies for validation and compilation.\n- Ensure strategies meet the required syntax and operational constraints.\n- Validate strategies to identify potential issues or errors before execution.\n- Integrate strategies with specific exchanges and market instruments for tailored use."
		},
		{
			"name": "Dataset",
			"description": "The **dataset** endpoints let you upload your own historical ticker data and backtest against\nit the same way you would against a managed exchange — via the reserved `exchangeId: user`\nvalue on the existing `prepare`/`execute` endpoints. This is for data QTSurfer doesn't\ncapture itself: your own exports, a venue not yet integrated, or a private feed.\n\n**Key functionalities**:\n- Create a dataset and get a presigned URL to upload a CSV directly to object storage.\n- Finalize an upload to trigger validated ingest (CSV contract check, cadence and timestamp\n  unit discovery, gap detection) and poll its result.\n- List and inspect your datasets, including their current ingested version.\n- Prepare and execute backtests against a dataset exactly like against a managed exchange.\n- Delete a dataset you no longer need."
		}
	],
	"externalDocs": {
		"description": "Find out more about QTSurfer API",
		"url": "https://qtsurfer.com/developers"
	}
}
