@qtsurfer/api-client 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -705,9 +705,76 @@ export const ExecuteSweepRequestSchema = {
705
705
  walkForward: {
706
706
  $ref: "#/components/schemas/WalkForwardRequest",
707
707
  },
708
+ equityCurve: {
709
+ $ref: "#/components/schemas/EquityCurveRequest",
710
+ },
708
711
  },
709
712
  } as const;
710
713
 
714
+ export const EquityCurveOptionsSchema = {
715
+ type: "object",
716
+ description:
717
+ "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.",
718
+ properties: {
719
+ resample: {
720
+ type: "integer",
721
+ minimum: 2,
722
+ description:
723
+ "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.",
724
+ },
725
+ differential: {
726
+ type: "boolean",
727
+ default: false,
728
+ description:
729
+ "Delta-encode both fields from the second (post-resample) point onward.",
730
+ },
731
+ outMode: {
732
+ allOf: [
733
+ {
734
+ $ref: "#/components/schemas/EquityCurveOutMode",
735
+ },
736
+ ],
737
+ default: "ARRAY",
738
+ description: "Requested JSON shape.",
739
+ },
740
+ },
741
+ } as const;
742
+
743
+ export const EquityCurveRequestSchema = {
744
+ allOf: [
745
+ {
746
+ $ref: "#/components/schemas/EquityCurveOptions",
747
+ },
748
+ {
749
+ type: "object",
750
+ properties: {
751
+ mode: {
752
+ type: "string",
753
+ enum: ["auto", "topN", "topPct", "none"],
754
+ default: "auto",
755
+ description:
756
+ "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.",
757
+ },
758
+ n: {
759
+ type: "integer",
760
+ minimum: 1,
761
+ description: "Trial count to retain when mode is topN.",
762
+ },
763
+ maxPct: {
764
+ type: "number",
765
+ format: "double",
766
+ exclusiveMinimum: 0,
767
+ maximum: 100,
768
+ description:
769
+ "Top percentage of trials to retain when mode is topPct.",
770
+ },
771
+ },
772
+ },
773
+ ],
774
+ description:
775
+ "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.",
776
+ } as const;
777
+
711
778
  export const WalkForwardRequestSchema = {
712
779
  type: "object",
713
780
  description:
@@ -949,6 +1016,15 @@ export const SweepRunRowSchema = {
949
1016
  type: "integer",
950
1017
  format: "int64",
951
1018
  },
1019
+ equityCurve: {
1020
+ allOf: [
1021
+ {
1022
+ $ref: "#/components/schemas/EquityCurveResult",
1023
+ },
1024
+ ],
1025
+ description:
1026
+ "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.",
1027
+ },
952
1028
  },
953
1029
  } as const;
954
1030
 
@@ -1086,6 +1162,7 @@ export const ExecuteSweepResultSchema = {
1086
1162
  "leaderboardSize",
1087
1163
  "truncated",
1088
1164
  "leaderboard",
1165
+ "state",
1089
1166
  ],
1090
1167
  properties: {
1091
1168
  sweepId: {
@@ -1094,6 +1171,8 @@ export const ExecuteSweepResultSchema = {
1094
1171
  status: {
1095
1172
  type: "string",
1096
1173
  enum: ["RUNNING", "COMPLETED", "PARTIAL", "CANCELLED"],
1174
+ description:
1175
+ "The sweep's own status vocabulary — not the same set `state.status` below uses. See `state` for why.",
1097
1176
  },
1098
1177
  objective: {
1099
1178
  type: "string",
@@ -1148,6 +1227,17 @@ First failure wins and later ones are not recorded, so on a sweep where several
1148
1227
  walkForward: {
1149
1228
  $ref: "#/components/schemas/WalkForwardResult",
1150
1229
  },
1230
+ state: {
1231
+ allOf: [
1232
+ {
1233
+ $ref: "#/components/schemas/JobState",
1234
+ },
1235
+ ],
1236
+ 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.
1237
+ \`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.
1238
+ \`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.
1239
+ \`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.`,
1240
+ },
1151
1241
  },
1152
1242
  } as const;
1153
1243
 
@@ -1389,26 +1479,36 @@ reading: a run that produced no trades often did so for a reason stated here.
1389
1479
  example: 8.75,
1390
1480
  },
1391
1481
  equityCurve: {
1392
- type: "array",
1393
- description:
1394
- "Equity curve over the backtest. Element 0 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.",
1395
- items: {
1396
- $ref: "#/components/schemas/EquityPoint",
1397
- },
1398
- example: [
1399
- {
1400
- timestamp: 1700000000000,
1401
- equity: 100,
1402
- },
1403
- {
1404
- timestamp: 1700000060000,
1405
- equity: 110.5,
1406
- },
1482
+ allOf: [
1407
1483
  {
1408
- timestamp: 1700000120000,
1409
- equity: 90.25,
1484
+ $ref: "#/components/schemas/EquityCurveResult",
1410
1485
  },
1411
1486
  ],
1487
+ description:
1488
+ "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.",
1489
+ example: {
1490
+ points: [
1491
+ {
1492
+ timestamp: 1700000000000,
1493
+ equity: 100,
1494
+ },
1495
+ {
1496
+ timestamp: 1700000060000,
1497
+ equity: 110.5,
1498
+ },
1499
+ {
1500
+ timestamp: 1700000120000,
1501
+ equity: 90.25,
1502
+ },
1503
+ ],
1504
+ meta: {
1505
+ inputPointCount: 3,
1506
+ outputPointCount: 3,
1507
+ resampled: false,
1508
+ differential: false,
1509
+ outMode: "ARRAY",
1510
+ },
1511
+ },
1412
1512
  },
1413
1513
  signalCount: {
1414
1514
  type: "integer",
@@ -1475,6 +1575,104 @@ export const EquityPointSchema = {
1475
1575
  },
1476
1576
  } as const;
1477
1577
 
1578
+ export const EquityCurveOutModeSchema = {
1579
+ type: "string",
1580
+ enum: ["ARRAY", "SHORT"],
1581
+ description:
1582
+ "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.",
1583
+ } as const;
1584
+
1585
+ export const EquityCurveMetaSchema = {
1586
+ type: "object",
1587
+ description:
1588
+ "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`).",
1589
+ required: [
1590
+ "inputPointCount",
1591
+ "outputPointCount",
1592
+ "resampled",
1593
+ "differential",
1594
+ "outMode",
1595
+ ],
1596
+ properties: {
1597
+ inputPointCount: {
1598
+ type: "integer",
1599
+ description: "Size of the curve the transform pipeline received.",
1600
+ example: 100000,
1601
+ },
1602
+ outputPointCount: {
1603
+ type: "integer",
1604
+ description:
1605
+ "Size after the full pipeline (resample, then differential, then outMode).",
1606
+ example: 100,
1607
+ },
1608
+ resampled: {
1609
+ type: "boolean",
1610
+ description:
1611
+ "True only if the resample stage actually changed the point count.",
1612
+ },
1613
+ differential: {
1614
+ type: "boolean",
1615
+ description:
1616
+ "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.",
1617
+ },
1618
+ outMode: {
1619
+ allOf: [
1620
+ {
1621
+ $ref: "#/components/schemas/EquityCurveOutMode",
1622
+ },
1623
+ ],
1624
+ description:
1625
+ "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.",
1626
+ example: "ARRAY",
1627
+ },
1628
+ },
1629
+ } as const;
1630
+
1631
+ export const EquityCurveResultSchema = {
1632
+ type: "object",
1633
+ description:
1634
+ "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.",
1635
+ required: ["meta"],
1636
+ properties: {
1637
+ meta: {
1638
+ $ref: "#/components/schemas/EquityCurveMeta",
1639
+ },
1640
+ points: {
1641
+ type: "array",
1642
+ description:
1643
+ "Present when `meta.outMode` is `ARRAY` and the curve is inline (not a pointer).",
1644
+ items: {
1645
+ $ref: "#/components/schemas/EquityPoint",
1646
+ },
1647
+ },
1648
+ timestamps: {
1649
+ type: "array",
1650
+ description:
1651
+ "Present when `meta.outMode` is `SHORT` and the curve is inline (not a pointer).",
1652
+ items: {
1653
+ type: "integer",
1654
+ format: "int64",
1655
+ },
1656
+ },
1657
+ equities: {
1658
+ type: "array",
1659
+ description:
1660
+ "Present when `meta.outMode` is `SHORT` and the curve is inline (not a pointer), parallel to `timestamps` (same index, same point).",
1661
+ items: {
1662
+ type: "number",
1663
+ format: "double",
1664
+ },
1665
+ },
1666
+ url: {
1667
+ type: "string",
1668
+ description:
1669
+ "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.",
1670
+ example:
1671
+ "/v1/backtest/binance/ticker/executeSweep/req-1/swp_test/runs/3/equityCurve",
1672
+ },
1673
+ },
1674
+ } as const;
1675
+
1478
1676
  export const strategyIdSchema = {
1479
1677
  description: `Unique identifier for a compiled strategy, derived from the source itself: the same code
1480
1678
  always yields the same id, for every caller, whatever its formatting. See
@@ -1484,6 +1682,68 @@ always yields the same id, for every caller, whatever its formatting. See
1484
1682
  example: "6bsh31ikwkuivhtgcoa6s4",
1485
1683
  } as const;
1486
1684
 
1685
+ export const DeclaredPropertySchema = {
1686
+ type: "object",
1687
+ description: `One property name \`POST /strategy\` could establish without constructing the strategy —
1688
+ either declared with \`@StrategyProperty\` on the compiled source, or one of the small set of
1689
+ base properties every strategy carries (\`amnt\`, \`enabled\`, \`multiEntry\`, ...).
1690
+
1691
+ **Best-effort, not exhaustive.** A property registered through an attached risk/backtest
1692
+ config needs a live instance to discover and is not listed here. Use this to catch a typo'd
1693
+ sweep key before submitting, not as the definitive list of what a sweep will accept — a
1694
+ name absent from this list may still be valid.
1695
+ `,
1696
+ required: ["name"],
1697
+ properties: {
1698
+ name: {
1699
+ type: "string",
1700
+ description:
1701
+ "The key a sweep or execute param map uses for this property.",
1702
+ example: "rsi.period",
1703
+ },
1704
+ description: {
1705
+ type: "string",
1706
+ description: "Human-readable label, as declared.",
1707
+ example: "RSI period",
1708
+ },
1709
+ defaultValue: {
1710
+ type: "string",
1711
+ description: `The declared default, as a string, if one was given. Absent, not null, when none was
1712
+ declared.
1713
+ `,
1714
+ example: "14",
1715
+ },
1716
+ reflected: {
1717
+ type: "boolean",
1718
+ description: `Whether a value for this key is injected into the strategy's field (\`true\`) or only
1719
+ available through the property map (\`false\`).
1720
+ `,
1721
+ example: true,
1722
+ },
1723
+ min: {
1724
+ type: "number",
1725
+ format: "double",
1726
+ description:
1727
+ "Suggested sweep/range minimum, if declared. Advisory only, never validated.",
1728
+ example: 2,
1729
+ },
1730
+ max: {
1731
+ type: "number",
1732
+ format: "double",
1733
+ description:
1734
+ "Suggested sweep/range maximum, if declared. Advisory only, never validated.",
1735
+ example: 50,
1736
+ },
1737
+ step: {
1738
+ type: "number",
1739
+ format: "double",
1740
+ description:
1741
+ "Suggested sweep/range step, if declared. Advisory only, never validated.",
1742
+ example: 1,
1743
+ },
1744
+ },
1745
+ } as const;
1746
+
1487
1747
  export const NoticeSchema = {
1488
1748
  type: "object",
1489
1749
  description: `A diagnostic the engine raised while the strategy ran. Advisory: it describes something worth
@@ -1767,52 +2027,91 @@ export const DatasetWithLinksSchema = {
1767
2027
  ],
1768
2028
  } as const;
1769
2029
 
2030
+ export const DatasetUploadTargetSchema = {
2031
+ description:
2032
+ "A presigned destination for uploading a raw dataset file directly to storage.",
2033
+ type: "object",
2034
+ required: ["url", "expiresInMinutes"],
2035
+ properties: {
2036
+ url: {
2037
+ type: "string",
2038
+ description: `Presigned URL. \`PUT\` the raw CSV file here directly — no \`Authorization\` header,
2039
+ no other API credentials.
2040
+ `,
2041
+ example:
2042
+ "https://storage.qtsurfer.com/uploads/00000000-.../up_1a2b3c4d5e6f7a8b/raw.csv?X-Amz-...",
2043
+ },
2044
+ expiresInMinutes: {
2045
+ type: "integer",
2046
+ description: "How long `url` stays valid.",
2047
+ example: 15,
2048
+ },
2049
+ },
2050
+ } as const;
2051
+
2052
+ export const DatasetUploadSessionSchema = {
2053
+ description: `An upload session — an id plus the presigned URL to PUT the raw file to. Returned both by
2054
+ \`POST /datasets\` (as part of the new dataset) and by \`POST /datasets/{datasetId}/uploads\`
2055
+ (on its own, for an existing one).
2056
+ `,
2057
+ type: "object",
2058
+ required: ["uploadId", "upload"],
2059
+ properties: {
2060
+ uploadId: {
2061
+ type: "string",
2062
+ description: `Identifies this upload session. Pass to
2063
+ \`POST /datasets/{datasetId}/uploads/{uploadId}/finalize\` once the PUT completes.
2064
+ `,
2065
+ example: "up_1a2b3c4d5e6f7a8b",
2066
+ },
2067
+ upload: {
2068
+ $ref: "#/components/schemas/DatasetUploadTarget",
2069
+ },
2070
+ },
2071
+ } as const;
2072
+
1770
2073
  export const DatasetCreatedSchema = {
1771
- description: `A \`Dataset\` plus the first upload session — the presigned URL to PUT the file to.
2074
+ description: `The metadata available immediately after creating a dataset, plus its first upload
2075
+ session — the presigned URL to PUT the file to. Version-derived fields such as
2076
+ \`createdAt\`, \`currentVersionId\`, range, and cadence are available from \`GET /datasets/{datasetId}\`
2077
+ after the relevant lifecycle stages, not in this creation response.
1772
2078
  `,
1773
2079
  allOf: [
1774
- {
1775
- $ref: "#/components/schemas/Dataset",
1776
- },
1777
2080
  {
1778
2081
  type: "object",
1779
- required: ["uploadId", "upload"],
2082
+ required: ["datasetId", "name", "type", "instrument"],
1780
2083
  properties: {
1781
- uploadId: {
2084
+ datasetId: {
1782
2085
  type: "string",
1783
- description: `Identifies this upload session. Pass to
1784
- \`POST /datasets/{datasetId}/uploads/{uploadId}/finalize\` once the PUT completes.
1785
- `,
1786
- example: "up_1a2b3c4d5e6f7a8b",
2086
+ description: "Opaque id of the newly created dataset.",
2087
+ example: "ds_3f9a1c2e7b0d4a5f",
1787
2088
  },
1788
- upload: {
1789
- type: "object",
1790
- required: ["url", "expiresInMinutes"],
1791
- properties: {
1792
- url: {
1793
- type: "string",
1794
- description: `Presigned URL. \`PUT\` the raw CSV file here directly — no \`Authorization\`
1795
- header, no other API credentials.
1796
- `,
1797
- example:
1798
- "https://storage.qtsurfer.com/00000000-.../uploads/up_1a2b3c4d5e6f7a8b/raw.csv?X-Amz-...",
1799
- },
1800
- expiresInMinutes: {
1801
- type: "integer",
1802
- description: "How long `url` stays valid.",
1803
- example: 15,
1804
- },
1805
- },
2089
+ name: {
2090
+ type: "string",
2091
+ description: "Unique name of the newly created dataset.",
2092
+ example: "My BTC ticks",
2093
+ },
2094
+ type: {
2095
+ type: "string",
2096
+ enum: ["ticker"],
2097
+ description: "Always `ticker` in v1.",
2098
+ example: "ticker",
2099
+ },
2100
+ instrument: {
2101
+ $ref: "#/components/schemas/Instrument",
1806
2102
  },
1807
2103
  },
1808
2104
  },
2105
+ {
2106
+ $ref: "#/components/schemas/DatasetUploadSession",
2107
+ },
1809
2108
  ],
1810
2109
  } as const;
1811
2110
 
1812
2111
  export const DatasetVersionSchema = {
1813
2112
  type: "object",
1814
2113
  description: `One successfully ingested upload. Cadence and timestamp unit are discovered from the file,
1815
- not declared by the caller (D-11).
2114
+ not declared by the caller.
1816
2115
  `,
1817
2116
  required: ["datasetId"],
1818
2117
  properties: {
@@ -58,6 +58,9 @@ import type {
58
58
  GetSweepSensitivityData,
59
59
  GetSweepSensitivityResponse,
60
60
  GetSweepSensitivityError,
61
+ GetSweepRunEquityCurveData,
62
+ GetSweepRunEquityCurveResponse,
63
+ GetSweepRunEquityCurveError,
61
64
  ExecuteBacktestData,
62
65
  ExecuteBacktestResponse,
63
66
  ExecuteBacktestError,
@@ -78,6 +81,9 @@ import type {
78
81
  GetDatasetData,
79
82
  GetDatasetResponse,
80
83
  GetDatasetError,
84
+ OpenDatasetUploadData,
85
+ OpenDatasetUploadResponse,
86
+ OpenDatasetUploadError,
81
87
  FinalizeDatasetUploadData,
82
88
  FinalizeDatasetUploadResponse,
83
89
  FinalizeDatasetUploadError,
@@ -316,6 +322,10 @@ export const listStrategies = <ThrowOnError extends boolean = false>(
316
322
  * different means are two strategies, because deciding otherwise would mean deciding program
317
323
  * equivalence.
318
324
  *
325
+ * The response also lists `declaredProperties` — the sweep/execute param keys this strategy
326
+ * is known to accept, so a caller can catch a typo'd key before submitting a sweep instead of
327
+ * only learning it from a rejected one. See `DeclaredProperty`: best-effort, not exhaustive.
328
+ *
319
329
  */
320
330
  export const compileStrategy = <ThrowOnError extends boolean = false>(
321
331
  options: Options<CompileStrategyData, ThrowOnError>
@@ -689,6 +699,41 @@ export const getSweepSensitivity = <ThrowOnError extends boolean = false>(
689
699
  });
690
700
  };
691
701
 
702
+ /**
703
+ * Get one sweep trial's equity curve
704
+ * The resource a leaderboard row's `equityCurve.url` points at — only reachable when that
705
+ * trial's curve was actually selected (`equityCurve.mode: topN` or `topPct` on the sweep
706
+ * submission, and this trial ranked among the winners). Returns the exact same
707
+ * `{points|timestamps+equities, meta}` shape a plain backtest's inline `equityCurve` carries.
708
+ *
709
+ * Query params reshape the response the same way a plain backtest's `equityCurve` options do.
710
+ * A param genuinely absent from the query string falls back to the `equityCurve` transform
711
+ * preference the sweep was submitted with — a param
712
+ * present but malformed does not fall back, it degrades the same way it always has. Above a
713
+ * server-side size threshold, the shape is forced regardless of either — `meta.outMode` in
714
+ * the response, not the query string or the submitted default, is the source of truth for
715
+ * what shape actually came back.
716
+ *
717
+ */
718
+ export const getSweepRunEquityCurve = <ThrowOnError extends boolean = false>(
719
+ options: Options<GetSweepRunEquityCurveData, ThrowOnError>
720
+ ) => {
721
+ return (options.client ?? _heyApiClient).get<
722
+ GetSweepRunEquityCurveResponse,
723
+ GetSweepRunEquityCurveError,
724
+ ThrowOnError
725
+ >({
726
+ security: [
727
+ {
728
+ scheme: "bearer",
729
+ type: "http",
730
+ },
731
+ ],
732
+ url: "/backtest/{exchangeId}/{type}/executeSweep/{requestId}/{sweepId}/runs/{runIx}/equityCurve",
733
+ ...options,
734
+ });
735
+ };
736
+
692
737
  /**
693
738
  * Execute a compiled strategy against a prepared dataset
694
739
  * Enqueues an execute task that runs the strategy identified by `strategyId` over the data
@@ -698,8 +743,9 @@ export const getSweepSensitivity = <ThrowOnError extends boolean = false>(
698
743
  * Returns immediately with a `jobId`; poll `GET /backtest/{exchangeId}/{type}/execute/{jobId}`
699
744
  * for the result.
700
745
  *
701
- * The same params (same `prepareJobId`, `strategyId`, `storeSignals`) always return the same
702
- * `jobId` (idempotent).
746
+ * The same params (same `prepareJobId`, `strategyId`, `storeSignals`, `equityCurve`) always
747
+ * return the same `jobId` (idempotent) — a request that omits `equityCurve` dedupes exactly
748
+ * as it did before that field existed.
703
749
  *
704
750
  * Works unchanged for a dataset-backed prepare (`exchangeId: user`) — the request body is
705
751
  * identical either way, since the instrument and range are recovered from `prepareJobId`.
@@ -816,6 +862,10 @@ export const listDatasets = <ThrowOnError extends boolean = false>(
816
862
  * `POST /datasets/{datasetId}/uploads/{uploadId}/finalize` once the upload completes to kick
817
863
  * off ingest.
818
864
  *
865
+ * Losing this response loses nothing: calling this dataset's
866
+ * `POST /datasets/{datasetId}/uploads` returns the very same upload session again rather than
867
+ * opening a new one, as long as nothing has been finalized against it yet.
868
+ *
819
869
  * v1 is ticker data only — `type` is not a request field, it is always `"ticker"` in the
820
870
  * response. `instrument` must be a plain spot pair (`BASE/QUOTE`, exactly one `/`); derivative
821
871
  * forms (e.g. `BTC/USDT:USDT`) are rejected.
@@ -899,14 +949,48 @@ export const getDataset = <ThrowOnError extends boolean = false>(
899
949
  });
900
950
  };
901
951
 
952
+ /**
953
+ * Open a new upload session for an existing dataset
954
+ * Get a fresh presigned URL to upload a new version into a dataset you already have — a
955
+ * corrected file, or the next chunk of history. Behaves the same way `POST /datasets` does
956
+ * for a brand-new dataset's own upload: at most one upload session is open per dataset at a
957
+ * time, so calling this again before finalizing just hands back that same session rather
958
+ * than opening a second one — safe to call repeatedly if a response gets lost.
959
+ *
960
+ * Once a session has been finalized (successfully or not), the next call here opens a
961
+ * genuinely new one for that dataset's next version.
962
+ *
963
+ */
964
+ export const openDatasetUpload = <ThrowOnError extends boolean = false>(
965
+ options: Options<OpenDatasetUploadData, ThrowOnError>
966
+ ) => {
967
+ return (options.client ?? _heyApiClient).post<
968
+ OpenDatasetUploadResponse,
969
+ OpenDatasetUploadError,
970
+ ThrowOnError
971
+ >({
972
+ security: [
973
+ {
974
+ scheme: "bearer",
975
+ type: "http",
976
+ },
977
+ ],
978
+ url: "/datasets/{datasetId}/uploads",
979
+ ...options,
980
+ });
981
+ };
982
+
902
983
  /**
903
984
  * Finalize an uploaded file and start ingest
904
- * Call once the file has been PUT to the `upload.url` from `POST /datasets`. Enqueues ingest
905
- * and returns immediately; poll
985
+ * Call once the file has been PUT to the `upload.url` from `POST /datasets` (or from
986
+ * `POST /datasets/{datasetId}/uploads`). Enqueues ingest and returns immediately; poll
906
987
  * `GET /datasets/{datasetId}/uploads/{uploadId}` for the result.
907
988
  *
908
- * Idempotent — a repeat finalize of the same upload returns the same `jobId` rather than
909
- * enqueueing a second ingest.
989
+ * Idempotent while the upload is still open — a repeat finalize before it has produced a
990
+ * version returns the same `jobId` rather than enqueueing a second ingest. Once it HAS
991
+ * produced a version, `uploadId` is spent: finalizing it again is a `409`, even with
992
+ * different bytes freshly PUT to the same URL — open a new upload session instead
993
+ * (`POST /datasets/{datasetId}/uploads`) rather than reusing a spent one.
910
994
  *
911
995
  */
912
996
  export const finalizeDatasetUpload = <ThrowOnError extends boolean = false>(