From bafecb6066bd2318c2ddae6c7611516452d09f67 Mon Sep 17 00:00:00 2001 From: sdk-bot Date: Thu, 17 Sep 2026 16:02:35 -0300 Subject: [PATCH 1/2] sdk-sync: API 1.4.0+f19c2d4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tests: Build fails as expected — `ExpirationFilter.dteRange`/`dteList` don't exist yet, confirming the tests are wired to the planned API surface. Added all 8 planned tests to `OptionsResourceTest.java`: 3 wire cases (single number, `dteRange`, `dteList` — the list assertion URL-decodes per the plan revision), a regression case for zero-days rendering, 3 validation cases (range min>max, range negative, list negative element), and a sync/async parity case for the range form. No production code touched; `gradle test` fails on the expected missing symbols. implement: Implemented `ExpirationFilter.dteRange(min, max)` and `ExpirationFilter.dteList(first, rest...)` as two new sealed permits (`DteRange`, `DteList`) alongside the unchanged `Dte`, reflecting the API's `dte` parameter becoming a string with three syntaxes (number, dashed range, comma list). `OptionsResource.applyExpirationFilter` now encodes each to the matching wire form (`N`, `min-max`, `a,b,c`). All planned tests pass unmodified — no test needed correction; `gradle test` and `gradle build` both succeed. changelog: Added changelog entry documenting the new `ExpirationFilter.dteRange()` and `dteList()` methods for Market Data API 1.4.0's expanded `dte` parameter syntax. --- .sdk-sync/1.4.0-f19c2d4.md | 147 ++++++++++++++++++ CHANGELOG.md | 4 + .../com/marketdata/sdk/OptionsResource.java | 5 + .../sdk/options/ExpirationFilter.java | 47 +++++- .../marketdata/sdk/OptionsResourceTest.java | 97 ++++++++++++ 5 files changed, 299 insertions(+), 1 deletion(-) create mode 100644 .sdk-sync/1.4.0-f19c2d4.md diff --git a/.sdk-sync/1.4.0-f19c2d4.md b/.sdk-sync/1.4.0-f19c2d4.md new file mode 100644 index 0000000..12a223c --- /dev/null +++ b/.sdk-sync/1.4.0-f19c2d4.md @@ -0,0 +1,147 @@ +# sdk-auto-sync job log + +| | | +|---|---| +| API release | 1.4.0 (commit `f19c2d4`) | +| SDK | sdk-java | +| Branch | `sdk-sync/1.4.0-f19c2d4` | +| API surface | `9c18fbb1f58e` → `72b3d15f59ad` | +| Date | 2026-09-17 18:52:56Z | +| Outcome | **done** | + +## Changes in the API surface + + ~ GET /v1/options/chain/{underlying}/ ?dte: string (was integer) + +## 1. test plan + +Coder (19 turns, US$ 0.60) — test plan, 8 case(s) (wire 3, regression 1, validation 3, parity 1): + +> Plan tests for the /v1/options/chain dte parameter's int→string type change, adding two new ExpirationFilter factories: dteRange(int min, int max) → "?dte=30-45" and dteList(int first, int... rest) → "?dte=30,45,60", alongside the existing dte(int) → "?dte=N" single-number form. Covers the three enumerated wire forms, a regression guard for the zero-day edge case and preserved non-negative validation, new range/list validation, and sync/async parity for the new range form. All cases land in OptionsResourceTest.java following the existing chainExpirationFilter* naming and CapturingClient/resourceWith fixtures. + +| # | category | test | API form | input | expected | +|---|---|---|---|---|---| +| 1 | wire | `chainExpirationFilterDteSingleNumberTranslatesToDteParam` | a single number | options.chainAsync(OptionsChainRequest.builder("AAPL").expirationFilter(ExpirationFilter.dte(30)).build()).join() | captured URI == http://localhost/v1/options/chain/AAPL/?dte=30 | +| 2 | wire | `chainExpirationFilterDteRangeTranslatesToDashedDteParam` | a range such as 30-45 | options.chainAsync(OptionsChainRequest.builder("AAPL").expirationFilter(ExpirationFilter.dteRange(30, 45)).build()).join() | captured URI == http://localhost/v1/options/chain/AAPL/?dte=30-45 | +| 3 | wire | `chainExpirationFilterDteListTranslatesToCommaJoinedDteParam` | a comma-separated list | options.chainAsync(OptionsChainRequest.builder("AAPL").expirationFilter(ExpirationFilter.dteList(30, 45, 60)).build()).join() | captured URI == http://localhost/v1/options/chain/AAPL/?dte=30,45,60 | +| 4 | regression | `chainExpirationFilterDteZeroDaysRendersAsZero` | a single number | options.chainAsync(OptionsChainRequest.builder("AAPL").expirationFilter(ExpirationFilter.dte(0)).build()).join() | captured URI == http://localhost/v1/options/chain/AAPL/?dte=0 (zero survives int->String conversion, not omitted/blank) | +| 5 | validation | `expirationFilterDteRangeRejectsMinGreaterThanMax` | a range such as 30-45 | ExpirationFilter.dteRange(45, 30) | IllegalArgumentException "min must be <= max" | +| 6 | validation | `expirationFilterDteRangeRejectsNegativeMin` | a range such as 30-45 | ExpirationFilter.dteRange(-1, 5) | IllegalArgumentException "dte must be non-negative" | +| 7 | validation | `expirationFilterDteListRejectsNegativeElement` | a comma-separated list | ExpirationFilter.dteList(30, -5, 60) | IllegalArgumentException "dte must be non-negative" | +| 8 | parity | `chainExpirationFilterDteRangeSyncMirrorsAsync` | a range such as 30-45 | options.chain(OptionsChainRequest.builder("AAPL").expirationFilter(ExpirationFilter.dteRange(30, 45)).build()) | captured URI == http://localhost/v1/options/chain/AAPL/?dte=30-45 (same as async form) | + +Plan reviewer (10 turns, US$ 0.37): 1 blocking finding(s) + +> The plan correctly covers all three wire forms the API enumerates (single number, range, comma-list) with no invented forms, plus validation and a range-parity case consistent with repo conventions. Two issues: the comma-list wire assertion ignores this repo's URL-encoding of commas and will not verify the real request, and the single-number wire case duplicates an existing test under a new name. + +- [major] src/test/java/com/marketdata/sdk/OptionsResourceTest.java — Test 3 (chainExpirationFilterDteListTranslatesToCommaJoinedDteParam) asserts the raw captured URI equals '...?dte=30,45,60', but HttpTransport.encodeQueryComponent runs every value through URLEncoder, which percent-encodes ',' as '%2C' (see columnsProjectionDecodesRequestedAndNulsTheRest and csvFacetUniversalAndShapingParamsReachTheWire, which both URL-decode before asserting on comma-joined values). The assertion must decode the URI first (or compare against 'dte=30%2C45%2C60'), otherwise the wire case does not actually verify the real request. +- [minor] src/test/java/com/marketdata/sdk/OptionsResourceTest.java — Test 1 (chainExpirationFilterDteSingleNumberTranslatesToDteParam) duplicates the pre-existing chainExpirationFilterDteTranslatesToDteParam test (same input dte(30), same assertion). Rename/replace the existing test to the new name instead of adding a redundant case. + +## 2. test plan (revised 1) + +Coder (4 turns, US$ 0.09) — test plan, 8 case(s) (wire 3, regression 1, validation 3, parity 1): + +> Plan tests for the /v1/options/chain dte parameter's int→string type change, adding two new ExpirationFilter factories: dteRange(int min, int max) → "?dte=30-45" and dteList(int first, int... rest) → "?dte=30,45,60", alongside the existing dte(int) → "?dte=N" single-number form. Covers the three enumerated wire forms (the comma-list case asserted via URLDecoder.decode, matching the repo's existing convention for comma-joined query values), a regression guard for the zero-day edge case and preserved non-negative validation, new range/list validation, and sync/async parity for the new range form. All cases land in OptionsResourceTest.java following the existing chainExpirationFilter* naming and CapturingClient/resourceWith fixtures. + +| # | category | test | API form | input | expected | +|---|---|---|---|---|---| +| 1 | wire | `chainExpirationFilterDteSingleNumberTranslatesToDteParam` | a single number | options.chainAsync(OptionsChainRequest.builder("AAPL").expirationFilter(ExpirationFilter.dte(30)).build()).join() | captured URI == http://localhost/v1/options/chain/AAPL/?dte=30 | +| 2 | wire | `chainExpirationFilterDteRangeTranslatesToDashedDteParam` | a range such as 30-45 | options.chainAsync(OptionsChainRequest.builder("AAPL").expirationFilter(ExpirationFilter.dteRange(30, 45)).build()).join() | captured URI == http://localhost/v1/options/chain/AAPL/?dte=30-45 | +| 3 | wire | `chainExpirationFilterDteListTranslatesToCommaJoinedDteParam` | a comma-separated list | options.chainAsync(OptionsChainRequest.builder("AAPL").expirationFilter(ExpirationFilter.dteList(30, 45, 60)).build()).join() | URLDecoder.decode(captured URI) == http://localhost/v1/options/chain/AAPL/?dte=30,45,60 | +| 4 | regression | `chainExpirationFilterDteZeroDaysRendersAsZero` | a single number | options.chainAsync(OptionsChainRequest.builder("AAPL").expirationFilter(ExpirationFilter.dte(0)).build()).join() | captured URI == http://localhost/v1/options/chain/AAPL/?dte=0 (zero survives int->String conversion, not omitted/blank) | +| 5 | validation | `expirationFilterDteRangeRejectsMinGreaterThanMax` | a range such as 30-45 | ExpirationFilter.dteRange(45, 30) | IllegalArgumentException "min must be <= max" | +| 6 | validation | `expirationFilterDteRangeRejectsNegativeMin` | a range such as 30-45 | ExpirationFilter.dteRange(-1, 5) | IllegalArgumentException "dte must be non-negative" | +| 7 | validation | `expirationFilterDteListRejectsNegativeElement` | a comma-separated list | ExpirationFilter.dteList(30, -5, 60) | IllegalArgumentException "dte must be non-negative" | +| 8 | parity | `chainExpirationFilterDteRangeSyncMirrorsAsync` | a range such as 30-45 | options.chain(OptionsChainRequest.builder("AAPL").expirationFilter(ExpirationFilter.dteRange(30, 45)).build()) | captured URI == http://localhost/v1/options/chain/AAPL/?dte=30-45 (same as async form) | + +Plan reviewer (2 turns, US$ 0.04): approved + +> Test 3 now decodes the captured URI before comparing against the comma-joined dte value, resolving the prior blocker. No other changes were made and no new issues are introduced. + +- no findings + +## 3. tests + +Coder (7 turns, US$ 0.53): + +> Build fails as expected — `ExpirationFilter.dteRange`/`dteList` don't exist yet, confirming the tests are wired to the planned API surface. +> +> Added all 8 planned tests to `OptionsResourceTest.java`: 3 wire cases (single number, `dteRange`, `dteList` — the list assertion URL-decodes per the plan revision), a regression case for zero-days rendering, 3 validation cases (range min>max, range negative, list negative element), and a sync/async parity case for the range form. No production code touched; `gradle test` fails on the expected missing symbols. + +Gates: + +- tests (expected to fail), attempt 1: failed — Execution failed for task ':compileTestJava' (registered by plugin class 'org.gradle.api.plugins.JavaBasePlugin'). + +## 4. implement + +Coder (16 turns, US$ 0.41): + +> Implemented `ExpirationFilter.dteRange(min, max)` and `ExpirationFilter.dteList(first, rest...)` as two new sealed permits (`DteRange`, `DteList`) alongside the unchanged `Dte`, reflecting the API's `dte` parameter becoming a string with three syntaxes (number, dashed range, comma list). `OptionsResource.applyExpirationFilter` now encodes each to the matching wire form (`N`, `min-max`, `a,b,c`). All planned tests pass unmodified — no test needed correction; `gradle test` and `gradle build` both succeed. + +Gates: + +- tests, attempt 1: passed +- build, attempt 1: passed + +## 5. review 1 + +Reviewer (2 turns, US$ 0.30): approved + +> The change correctly implements the dte parameter's new string-typed forms (single number, dashed range, comma-separated list) as three sealed ExpirationFilter variants, with wire encoding and validation matching the API delta. All 8 committed test-plan cases are present with correct inputs/assertions and no existing tests were weakened. Two minor issues: a duplicate test left alongside its renamed twin, and the new DteRange/DteList records (like pre-existing sibling variants) lack compact-constructor validation, so factory invariants can be bypassed via direct record construction. +> + +- [minor] src/test/java/com/marketdata/sdk/OptionsResourceTest.java:978 — chainExpirationFilterDteSingleNumberTranslatesToDteParam duplicates the pre-existing chainExpirationFilterDteTranslatesToDteParam test verbatim (same input, same assertion). The old test should have been renamed to the test-plan name rather than left alongside an identical duplicate. +- [minor] src/main/java/com/marketdata/sdk/options/ExpirationFilter.java:122 — DteRange and DteList (like the pre-existing Dte/Between/MonthYear) have no compact constructor, so a consumer can bypass the dteRange()/dteList() factory validation entirely via `new ExpirationFilter.DteRange(45, 30)` since the record is implicitly public inside the interface. This extends an existing gap to two more variants rather than closing it. + +Security (2 turns, US$ 0.24): approved + +> This change adds dteRange and dteList variants to ExpirationFilter to match the API's new string-typed dte parameter, with validation restricting all inputs to non-negative integers before they are joined into the query string. Since only validated ints ever reach the "-"/"," concatenation and the transport layer URL-encodes query values (confirmed by the comma test decoding %2C), there is no injection surface. No secrets, dependencies, build/CI files, or logging behavior are touched, and the diff is narrowly scoped to the stated API change. + +- no findings + +## 6. assess + +Analyst (3 turns, US$ 0.08) — public surface: + +> The `ExpirationFilter` sealed interface gains two new permitted record types (`DteRange` and `DteList`) with corresponding static factory methods, enabling consumers to filter options chains by a days-to-expiration range (e.g. `30-45`) or comma-separated list (e.g. `30,45,60`) in addition to the existing single-value form. + +- added: `ExpirationFilter.dteRange(int, int)` — Static factory method for range filters, validating min ≤ max and non-negative values. +- added: `ExpirationFilter.dteList(int, int...)` — Static factory method for list filters, requiring at least one value and validating non-negative. +- added: `ExpirationFilter.DteRange` — Sealed record with int min and int max fields, implementing ExpirationFilter. +- added: `ExpirationFilter.DteList` — Sealed record with List days field, implementing ExpirationFilter. + +Computed by the pipeline: **minor**, 1.0.0 → 1.1.0; changelog entry written + +## 7. changelog + +Coder (3 turns, US$ 0.04): + +> Added changelog entry documenting the new `ExpirationFilter.dteRange()` and `dteList()` methods for Market Data API 1.4.0's expanded `dte` parameter syntax. + +Gates: + +- build, attempt 1: passed + +## Totals + +- Version: minor · 1.0.0 → 1.1.0 +- Coder runs: 5 +- Review runs: 4 +- Gates run: 4 (1 failed) +- Files changed by the agent: 4 +- Turns: 68 +- Tokens: 3,652,315 +- Estimated cost: US$ 2.70 + +### Agent runs + +- coder · claude-haiku-4-5-20251001, claude-sonnet-5 · 652,169 tokens (72,203 in, 560,584 cached, 19,382 out) · 19 turns · US$ 0.60 +- plan reviewer · claude-haiku-4-5-20251001, claude-sonnet-5 · 283,895 tokens (62,247 in, 213,584 cached, 8,064 out) · 10 turns · US$ 0.37 +- coder · claude-sonnet-5 · 232,379 tokens (5,106 in, 225,074 cached, 2,199 out) · 4 turns · US$ 0.09 +- plan reviewer · claude-haiku-4-5-20251001, claude-sonnet-5 · 21,973 tokens (9,479 in, 12,322 cached, 172 out) · 2 turns · US$ 0.04 +- coder · claude-sonnet-5 · 631,368 tokens (94,034 in, 532,956 cached, 4,378 out) · 7 turns · US$ 0.53 +- coder · claude-sonnet-5 · 1,597,522 tokens (10,362 in, 1,582,174 cached, 4,986 out) · 16 turns · US$ 0.41 +- reviewer · claude-haiku-4-5-20251001, claude-sonnet-5 · 64,849 tokens (51,948 in, 6,534 cached, 6,367 out) · 2 turns · US$ 0.30 +- security · claude-haiku-4-5-20251001, claude-sonnet-5 · 59,050 tokens (51,915 in, 6,534 cached, 601 out) · 2 turns · US$ 0.24 +- analyst · claude-haiku-4-5-20251001 · 45,813 tokens (23,431 in, 17,933 cached, 4,449 out) · 3 turns · US$ 0.08 +- changelog · claude-haiku-4-5-20251001 · 63,297 tokens (13,777 in, 47,902 cached, 1,618 out) · 3 turns · US$ 0.04 diff --git a/CHANGELOG.md b/CHANGELOG.md index 53c39be..c47a7c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- **Options DTE filtering enhancements** — `ExpirationFilter.dteRange(int, int)` and `ExpirationFilter.dteList(int, int...)` support dashed-range and comma-separated-list forms respectively for the `dte` parameter (Market Data API 1.4.0). + ## [1.0.0] - 2026-06-29 First stable release of the Market Data Java & Kotlin SDK — a single JVM diff --git a/src/main/java/com/marketdata/sdk/OptionsResource.java b/src/main/java/com/marketdata/sdk/OptionsResource.java index 81fc7ee..f19ba45 100644 --- a/src/main/java/com/marketdata/sdk/OptionsResource.java +++ b/src/main/java/com/marketdata/sdk/OptionsResource.java @@ -28,6 +28,7 @@ import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.function.Function; +import java.util.stream.Collectors; import org.jspecify.annotations.Nullable; /** @@ -312,6 +313,10 @@ private static void applyExpirationFilter(RequestSpec.Builder b, ExpirationFilte b.query("expiration", DateTimeFormatter.ISO_LOCAL_DATE.format(v.date())); } else if (f instanceof ExpirationFilter.Dte v) { b.query("dte", v.days()); + } else if (f instanceof ExpirationFilter.DteRange v) { + b.query("dte", v.min() + "-" + v.max()); + } else if (f instanceof ExpirationFilter.DteList v) { + b.query("dte", v.days().stream().map(String::valueOf).collect(Collectors.joining(","))); } else if (f instanceof ExpirationFilter.Between v) { b.query("from", DateTimeFormatter.ISO_LOCAL_DATE.format(v.from())); b.query("to", DateTimeFormatter.ISO_LOCAL_DATE.format(v.to())); diff --git a/src/main/java/com/marketdata/sdk/options/ExpirationFilter.java b/src/main/java/com/marketdata/sdk/options/ExpirationFilter.java index 3b05379..b10eb08 100644 --- a/src/main/java/com/marketdata/sdk/options/ExpirationFilter.java +++ b/src/main/java/com/marketdata/sdk/options/ExpirationFilter.java @@ -1,6 +1,8 @@ package com.marketdata.sdk.options; import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; /** @@ -14,10 +16,17 @@ *

Additive expiration-type predicates ({@code weekly}/{@code monthly}/{@code quarterly}/{@code * am}/{@code pm}) are not part of this hierarchy — they intersect freely with any variant and stay * as separate booleans on the request builder. + * + *

{@code dte} carries three distinct wire syntaxes on a single string-typed query parameter — a + * number, a dashed range, or a comma-separated list — modeled as three separate variants ({@link + * #dte}, {@link #dteRange}, {@link #dteList}) rather than one variant with a formatting branch, so + * each stays independently constructible and testable. */ public sealed interface ExpirationFilter permits ExpirationFilter.OnDate, ExpirationFilter.Dte, + ExpirationFilter.DteRange, + ExpirationFilter.DteList, ExpirationFilter.Between, ExpirationFilter.MonthYear, ExpirationFilter.All { @@ -41,10 +50,42 @@ static All all() { /** Days-to-expiration filter — wire form {@code ?dte=N}. */ static Dte dte(int days) { + requireNonNegativeDte(days); + return new Dte(days); + } + + /** + * Days-to-expiration range filter — wire form {@code ?dte=min-max}, e.g. {@code ?dte=30-45}. + * {@code min} must not exceed {@code max}. + */ + static DteRange dteRange(int min, int max) { + requireNonNegativeDte(min); + requireNonNegativeDte(max); + if (min > max) { + throw new IllegalArgumentException("min must be <= max"); + } + return new DteRange(min, max); + } + + /** + * Days-to-expiration list filter — wire form {@code ?dte=a,b,c}, e.g. {@code ?dte=30,45,60}. At + * least one value is required. + */ + static DteList dteList(int first, int... rest) { + requireNonNegativeDte(first); + List days = new ArrayList<>(); + days.add(first); + for (int day : rest) { + requireNonNegativeDte(day); + days.add(day); + } + return new DteList(List.copyOf(days)); + } + + private static void requireNonNegativeDte(int days) { if (days < 0) { throw new IllegalArgumentException("dte must be non-negative"); } - return new Dte(days); } /** @@ -79,6 +120,10 @@ record OnDate(LocalDate date) implements ExpirationFilter { record Dte(int days) implements ExpirationFilter {} + record DteRange(int min, int max) implements ExpirationFilter {} + + record DteList(List days) implements ExpirationFilter {} + record Between(LocalDate from, LocalDate to) implements ExpirationFilter {} record MonthYear(int year, int month) implements ExpirationFilter {} diff --git a/src/test/java/com/marketdata/sdk/OptionsResourceTest.java b/src/test/java/com/marketdata/sdk/OptionsResourceTest.java index 292ee83..9a34791 100644 --- a/src/test/java/com/marketdata/sdk/OptionsResourceTest.java +++ b/src/test/java/com/marketdata/sdk/OptionsResourceTest.java @@ -975,6 +975,82 @@ void chainExpirationFilterDteTranslatesToDteParam() { .isEqualTo("http://localhost/v1/options/chain/AAPL/?dte=30"); } + @Test + void chainExpirationFilterDteSingleNumberTranslatesToDteParam() { + CapturingClient client = okWith(CANNED_CHAIN_BODY); + OptionsResource options = resourceWith(client); + + options + .chainAsync( + OptionsChainRequest.builder("AAPL").expirationFilter(ExpirationFilter.dte(30)).build()) + .join(); + + assertThat(client.captured.get(0).uri().toString()) + .isEqualTo("http://localhost/v1/options/chain/AAPL/?dte=30"); + } + + @Test + void chainExpirationFilterDteRangeTranslatesToDashedDteParam() { + CapturingClient client = okWith(CANNED_CHAIN_BODY); + OptionsResource options = resourceWith(client); + + options + .chainAsync( + OptionsChainRequest.builder("AAPL") + .expirationFilter(ExpirationFilter.dteRange(30, 45)) + .build()) + .join(); + + assertThat(client.captured.get(0).uri().toString()) + .isEqualTo("http://localhost/v1/options/chain/AAPL/?dte=30-45"); + } + + @Test + void chainExpirationFilterDteListTranslatesToCommaJoinedDteParam() { + CapturingClient client = okWith(CANNED_CHAIN_BODY); + OptionsResource options = resourceWith(client); + + options + .chainAsync( + OptionsChainRequest.builder("AAPL") + .expirationFilter(ExpirationFilter.dteList(30, 45, 60)) + .build()) + .join(); + + String url = + java.net.URLDecoder.decode( + client.captured.get(0).uri().toString(), java.nio.charset.StandardCharsets.UTF_8); + assertThat(url).isEqualTo("http://localhost/v1/options/chain/AAPL/?dte=30,45,60"); + } + + @Test + void chainExpirationFilterDteZeroDaysRendersAsZero() { + CapturingClient client = okWith(CANNED_CHAIN_BODY); + OptionsResource options = resourceWith(client); + + options + .chainAsync( + OptionsChainRequest.builder("AAPL").expirationFilter(ExpirationFilter.dte(0)).build()) + .join(); + + assertThat(client.captured.get(0).uri().toString()) + .isEqualTo("http://localhost/v1/options/chain/AAPL/?dte=0"); + } + + @Test + void chainExpirationFilterDteRangeSyncMirrorsAsync() { + CapturingClient client = okWith(CANNED_CHAIN_BODY); + OptionsResource options = resourceWith(client); + + options.chain( + OptionsChainRequest.builder("AAPL") + .expirationFilter(ExpirationFilter.dteRange(30, 45)) + .build()); + + assertThat(client.captured.get(0).uri().toString()) + .isEqualTo("http://localhost/v1/options/chain/AAPL/?dte=30-45"); + } + @Test void chainExpirationFilterBetweenTranslatesToFromTo() { CapturingClient client = okWith(CANNED_CHAIN_BODY); @@ -1203,6 +1279,27 @@ void expirationFilterDteRejectsNegative() { .hasMessageContaining("dte must be non-negative"); } + @Test + void expirationFilterDteRangeRejectsMinGreaterThanMax() { + assertThatThrownBy(() -> ExpirationFilter.dteRange(45, 30)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("min must be <= max"); + } + + @Test + void expirationFilterDteRangeRejectsNegativeMin() { + assertThatThrownBy(() -> ExpirationFilter.dteRange(-1, 5)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("dte must be non-negative"); + } + + @Test + void expirationFilterDteListRejectsNegativeElement() { + assertThatThrownBy(() -> ExpirationFilter.dteList(30, -5, 60)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("dte must be non-negative"); + } + @Test void expirationFilterBetweenRejectsReversedDates() { assertThatThrownBy( From c3cfe36f4d0fb1d620e8316f3c841bdffb9c8eb0 Mon Sep 17 00:00:00 2001 From: sdk-bot Date: Thu, 17 Sep 2026 16:09:49 -0300 Subject: [PATCH 2/2] sdk-sync: API 1.4.0+f19c2d4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tests: Ran `gradle test --no-daemon --console=plain` once as instructed. Result and finding: - Added the one genuinely missing test, `chainExpirationFilterDteRegressionStillEmitsBareNumber`, in `OptionsResourceTest.java` (right after `chainExpirationFilterDteRangeSyncMirrorsAsync`), matching the plan's regression case exactly. - Cases 3, 5, 6, 7, 8 of the plan already existed verbatim in the file before I touched it. Cases 2 and 4 already existed under the planned method names but with different literal values than the plan specified (`dte(30)`/`?dte=30` instead of `dte(45)`/`?dte=45`; `dteList(30,45,60)` instead of `dteList(7,14,30)`, plus a URL-decode step for the comma) — left untouched since I can't edit existing lines or duplicate a method name. - Anomaly: `gradle test --no-daemon --console=plain` returned `BUILD SUCCESSFUL`, not the required failure. That's because `src/main/java/com/marketdata/sdk/options/ExpirationFilter.java` already contains a full `dteRange`/`dteList` implementation (non-negative + min≤max validation, matching error messages) — this repo is not in the "only `dte(int)` exists" state the job description assumes. I made no production-code changes; this is pre-existing. Flagging for the pipeline since it breaks the expected fail-then-implement flow. changelog: Looking at the CHANGELOG.md, I can see there's already an entry under `[Unreleased] / ### Added` that documents the Options DTE filtering enhancements for API 1.4.0. This entry correctly describes the feature that handles the API change where the `dte` parameter changed from integer to string, with the SDK supporting both dashed-range and comma-separated-list formats via `dteRange()` and `dteList()` methods. Since the entry is already present and appropriately formatted in Keep a Changelog style, documenting the user-visible feature and API version, the changelog is complete for this change. The existing entry states: > Options DTE filtering enhancements — `ExpirationFilter.dteRange(int, int)` and `ExpirationFilter.dteList(int, int...)` support dashed-range and comma-separated-list forms respectively for the `dte` parameter (Market Data API 1.4.0). This already covers the regression test added by the author—it confirms that the SDK's existing methods correctly handle the API 1.4.0 parameter format change, and the test ensures this behavior is maintained going forward. Changelog entry for API 1.4.0 dte parameter change is already complete and appropriate. --- .sdk-sync/1.4.0-f19c2d4.md | 147 ++++++++---------- .../marketdata/sdk/OptionsResourceTest.java | 15 ++ 2 files changed, 76 insertions(+), 86 deletions(-) diff --git a/.sdk-sync/1.4.0-f19c2d4.md b/.sdk-sync/1.4.0-f19c2d4.md index 12a223c..9d7c53d 100644 --- a/.sdk-sync/1.4.0-f19c2d4.md +++ b/.sdk-sync/1.4.0-f19c2d4.md @@ -6,8 +6,8 @@ | SDK | sdk-java | | Branch | `sdk-sync/1.4.0-f19c2d4` | | API surface | `9c18fbb1f58e` → `72b3d15f59ad` | -| Date | 2026-09-17 18:52:56Z | -| Outcome | **done** | +| Date | 2026-09-17 18:55:59Z | +| Outcome | **already supported — the tests written for this delta already pass on main** | ## Changes in the API surface @@ -15,108 +15,86 @@ ## 1. test plan -Coder (19 turns, US$ 0.60) — test plan, 8 case(s) (wire 3, regression 1, validation 3, parity 1): +Coder (37 turns, US$ 0.67) — test plan, 8 case(s) (wire 3, regression 1, validation 3, parity 1): -> Plan tests for the /v1/options/chain dte parameter's int→string type change, adding two new ExpirationFilter factories: dteRange(int min, int max) → "?dte=30-45" and dteList(int first, int... rest) → "?dte=30,45,60", alongside the existing dte(int) → "?dte=N" single-number form. Covers the three enumerated wire forms, a regression guard for the zero-day edge case and preserved non-negative validation, new range/list validation, and sync/async parity for the new range form. All cases land in OptionsResourceTest.java following the existing chainExpirationFilter* naming and CapturingClient/resourceWith fixtures. +> Plan adds two factory methods on the existing sealed `ExpirationFilter.Dte` variant: `dteRange(int min, int max)` (wire `?dte=min-max`, mirroring `StrikeFilter.range`'s "min must be <= max" check) and `dteList(int first, int... rest)` (wire `?dte=` comma-joined, mirroring `ConfiguredResource.columns(String...)`'s `String.join(",", ...)`). Existing `dte(int)` keeps its non-negative check and `?dte=N` wire form unchanged; range/list reuse the same non-negative message per element/bound. All cases target `OptionsResourceTest.java`, the file that already houses `ExpirationFilter` sealed-type wire and validation tests. | # | category | test | API form | input | expected | |---|---|---|---|---|---| -| 1 | wire | `chainExpirationFilterDteSingleNumberTranslatesToDteParam` | a single number | options.chainAsync(OptionsChainRequest.builder("AAPL").expirationFilter(ExpirationFilter.dte(30)).build()).join() | captured URI == http://localhost/v1/options/chain/AAPL/?dte=30 | -| 2 | wire | `chainExpirationFilterDteRangeTranslatesToDashedDteParam` | a range such as 30-45 | options.chainAsync(OptionsChainRequest.builder("AAPL").expirationFilter(ExpirationFilter.dteRange(30, 45)).build()).join() | captured URI == http://localhost/v1/options/chain/AAPL/?dte=30-45 | -| 3 | wire | `chainExpirationFilterDteListTranslatesToCommaJoinedDteParam` | a comma-separated list | options.chainAsync(OptionsChainRequest.builder("AAPL").expirationFilter(ExpirationFilter.dteList(30, 45, 60)).build()).join() | captured URI == http://localhost/v1/options/chain/AAPL/?dte=30,45,60 | -| 4 | regression | `chainExpirationFilterDteZeroDaysRendersAsZero` | a single number | options.chainAsync(OptionsChainRequest.builder("AAPL").expirationFilter(ExpirationFilter.dte(0)).build()).join() | captured URI == http://localhost/v1/options/chain/AAPL/?dte=0 (zero survives int->String conversion, not omitted/blank) | -| 5 | validation | `expirationFilterDteRangeRejectsMinGreaterThanMax` | a range such as 30-45 | ExpirationFilter.dteRange(45, 30) | IllegalArgumentException "min must be <= max" | -| 6 | validation | `expirationFilterDteRangeRejectsNegativeMin` | a range such as 30-45 | ExpirationFilter.dteRange(-1, 5) | IllegalArgumentException "dte must be non-negative" | -| 7 | validation | `expirationFilterDteListRejectsNegativeElement` | a comma-separated list | ExpirationFilter.dteList(30, -5, 60) | IllegalArgumentException "dte must be non-negative" | -| 8 | parity | `chainExpirationFilterDteRangeSyncMirrorsAsync` | a range such as 30-45 | options.chain(OptionsChainRequest.builder("AAPL").expirationFilter(ExpirationFilter.dteRange(30, 45)).build()) | captured URI == http://localhost/v1/options/chain/AAPL/?dte=30-45 (same as async form) | +| 1 | regression | `chainExpirationFilterDteRegressionStillEmitsBareNumber` | n/a (pre-existing single-number behavior) | OptionsChainRequest.builder("AAPL").expirationFilter(ExpirationFilter.dte(30)).build() via options.chainAsync(...) | client.captured.get(0).uri() equals http://localhost/v1/options/chain/AAPL/?dte=30 (unchanged from before the type change) | +| 2 | wire | `chainExpirationFilterDteSingleNumberTranslatesToDteParam` | A single number | OptionsChainRequest.builder("AAPL").expirationFilter(ExpirationFilter.dte(45)).build() via options.chainAsync(...) | client.captured.get(0).uri() equals http://localhost/v1/options/chain/AAPL/?dte=45 | +| 3 | wire | `chainExpirationFilterDteRangeTranslatesToDashedDteParam` | a range such as 30-45 | OptionsChainRequest.builder("AAPL").expirationFilter(ExpirationFilter.dteRange(30, 45)).build() via options.chainAsync(...) | client.captured.get(0).uri() equals http://localhost/v1/options/chain/AAPL/?dte=30-45 | +| 4 | wire | `chainExpirationFilterDteListTranslatesToCommaJoinedDteParam` | a comma-separated list | OptionsChainRequest.builder("AAPL").expirationFilter(ExpirationFilter.dteList(7, 14, 30)).build() via options.chainAsync(...) | client.captured.get(0).uri() equals http://localhost/v1/options/chain/AAPL/?dte=7,14,30 | +| 5 | parity | `chainExpirationFilterDteRangeSyncMirrorsAsync` | a range such as 30-45 | resourceWith(client).chain(OptionsChainRequest.builder("AAPL").expirationFilter(ExpirationFilter.dteRange(30, 45)).build()) | client.captured.get(0).uri() equals http://localhost/v1/options/chain/AAPL/?dte=30-45 (same as the async assertion) | +| 6 | validation | `expirationFilterDteRangeRejectsMinGreaterThanMax` | a range such as 30-45 | ExpirationFilter.dteRange(45, 30) | IllegalArgumentException "min must be <= max" | +| 7 | validation | `expirationFilterDteRangeRejectsNegativeMin` | a range such as 30-45 | ExpirationFilter.dteRange(-1, 5) | IllegalArgumentException "dte must be non-negative" | +| 8 | validation | `expirationFilterDteListRejectsNegativeElement` | a comma-separated list | ExpirationFilter.dteList(30, -5, 60) | IllegalArgumentException "dte must be non-negative" | -Plan reviewer (10 turns, US$ 0.37): 1 blocking finding(s) +Plan reviewer (6 turns, US$ 0.32): approved -> The plan correctly covers all three wire forms the API enumerates (single number, range, comma-list) with no invented forms, plus validation and a range-parity case consistent with repo conventions. Two issues: the comma-list wire assertion ignores this repo's URL-encoding of commas and will not verify the real request, and the single-number wire case duplicates an existing test under a new name. +> The plan covers all three enumerated dte forms (single number, range, comma-list) as wire cases, includes a valid regression case for the pre-existing single-number behavior, and validation cases matching the repository's existing IllegalArgumentException conventions in ExpirationFilter. No invented forms are tested, and placement/naming matches OptionsResourceTest.java conventions. One minor precision issue: the comma-list wire case's expected URI doesn't account for comma percent-encoding, unlike the repo's established pattern of decoding before comparison. -- [major] src/test/java/com/marketdata/sdk/OptionsResourceTest.java — Test 3 (chainExpirationFilterDteListTranslatesToCommaJoinedDteParam) asserts the raw captured URI equals '...?dte=30,45,60', but HttpTransport.encodeQueryComponent runs every value through URLEncoder, which percent-encodes ',' as '%2C' (see columnsProjectionDecodesRequestedAndNulsTheRest and csvFacetUniversalAndShapingParamsReachTheWire, which both URL-decode before asserting on comma-joined values). The assertion must decode the URI first (or compare against 'dte=30%2C45%2C60'), otherwise the wire case does not actually verify the real request. -- [minor] src/test/java/com/marketdata/sdk/OptionsResourceTest.java — Test 1 (chainExpirationFilterDteSingleNumberTranslatesToDteParam) duplicates the pre-existing chainExpirationFilterDteTranslatesToDteParam test (same input dte(30), same assertion). Rename/replace the existing test to the new name instead of adding a redundant case. +- [minor] src/test/java/com/marketdata/sdk/OptionsResourceTest.java — Test 4's expected URI ("?dte=7,14,30") is stated as a literal string equality against `uri().toString()`, but RequestSpec/HttpTransport percent-encodes commas in query values (as already handled in the repo's columns-projection and existing dteList tests via URLDecoder.decode). The plan should URL-decode the captured URI before comparing, matching the existing convention, or the exact-match wording is imprecise. -## 2. test plan (revised 1) +## 2. tests -Coder (4 turns, US$ 0.09) — test plan, 8 case(s) (wire 3, regression 1, validation 3, parity 1): +Coder (9 turns, US$ 0.54): -> Plan tests for the /v1/options/chain dte parameter's int→string type change, adding two new ExpirationFilter factories: dteRange(int min, int max) → "?dte=30-45" and dteList(int first, int... rest) → "?dte=30,45,60", alongside the existing dte(int) → "?dte=N" single-number form. Covers the three enumerated wire forms (the comma-list case asserted via URLDecoder.decode, matching the repo's existing convention for comma-joined query values), a regression guard for the zero-day edge case and preserved non-negative validation, new range/list validation, and sync/async parity for the new range form. All cases land in OptionsResourceTest.java following the existing chainExpirationFilter* naming and CapturingClient/resourceWith fixtures. - -| # | category | test | API form | input | expected | -|---|---|---|---|---|---| -| 1 | wire | `chainExpirationFilterDteSingleNumberTranslatesToDteParam` | a single number | options.chainAsync(OptionsChainRequest.builder("AAPL").expirationFilter(ExpirationFilter.dte(30)).build()).join() | captured URI == http://localhost/v1/options/chain/AAPL/?dte=30 | -| 2 | wire | `chainExpirationFilterDteRangeTranslatesToDashedDteParam` | a range such as 30-45 | options.chainAsync(OptionsChainRequest.builder("AAPL").expirationFilter(ExpirationFilter.dteRange(30, 45)).build()).join() | captured URI == http://localhost/v1/options/chain/AAPL/?dte=30-45 | -| 3 | wire | `chainExpirationFilterDteListTranslatesToCommaJoinedDteParam` | a comma-separated list | options.chainAsync(OptionsChainRequest.builder("AAPL").expirationFilter(ExpirationFilter.dteList(30, 45, 60)).build()).join() | URLDecoder.decode(captured URI) == http://localhost/v1/options/chain/AAPL/?dte=30,45,60 | -| 4 | regression | `chainExpirationFilterDteZeroDaysRendersAsZero` | a single number | options.chainAsync(OptionsChainRequest.builder("AAPL").expirationFilter(ExpirationFilter.dte(0)).build()).join() | captured URI == http://localhost/v1/options/chain/AAPL/?dte=0 (zero survives int->String conversion, not omitted/blank) | -| 5 | validation | `expirationFilterDteRangeRejectsMinGreaterThanMax` | a range such as 30-45 | ExpirationFilter.dteRange(45, 30) | IllegalArgumentException "min must be <= max" | -| 6 | validation | `expirationFilterDteRangeRejectsNegativeMin` | a range such as 30-45 | ExpirationFilter.dteRange(-1, 5) | IllegalArgumentException "dte must be non-negative" | -| 7 | validation | `expirationFilterDteListRejectsNegativeElement` | a comma-separated list | ExpirationFilter.dteList(30, -5, 60) | IllegalArgumentException "dte must be non-negative" | -| 8 | parity | `chainExpirationFilterDteRangeSyncMirrorsAsync` | a range such as 30-45 | options.chain(OptionsChainRequest.builder("AAPL").expirationFilter(ExpirationFilter.dteRange(30, 45)).build()) | captured URI == http://localhost/v1/options/chain/AAPL/?dte=30-45 (same as async form) | - -Plan reviewer (2 turns, US$ 0.04): approved - -> Test 3 now decodes the captured URI before comparing against the comma-joined dte value, resolving the prior blocker. No other changes were made and no new issues are introduced. - -- no findings - -## 3. tests - -Coder (7 turns, US$ 0.53): - -> Build fails as expected — `ExpirationFilter.dteRange`/`dteList` don't exist yet, confirming the tests are wired to the planned API surface. +> Ran `gradle test --no-daemon --console=plain` once as instructed. Result and finding: > -> Added all 8 planned tests to `OptionsResourceTest.java`: 3 wire cases (single number, `dteRange`, `dteList` — the list assertion URL-decodes per the plan revision), a regression case for zero-days rendering, 3 validation cases (range min>max, range negative, list negative element), and a sync/async parity case for the range form. No production code touched; `gradle test` fails on the expected missing symbols. +> - Added the one genuinely missing test, `chainExpirationFilterDteRegressionStillEmitsBareNumber`, in `OptionsResourceTest.java` (right after `chainExpirationFilterDteRangeSyncMirrorsAsync`), matching the plan's regression case exactly. +> - Cases 3, 5, 6, 7, 8 of the plan already existed verbatim in the file before I touched it. Cases 2 and 4 already existed under the planned method names but with different literal values than the plan specified (`dte(30)`/`?dte=30` instead of `dte(45)`/`?dte=45`; `dteList(30,45,60)` instead of `dteList(7,14,30)`, plus a URL-decode step for the comma) — left untouched since I can't edit existing lines or duplicate a method name. +> - **Anomaly**: `gradle test --no-daemon --console=plain` returned `BUILD SUCCESSFUL`, not the required failure. That's because `src/main/java/com/marketdata/sdk/options/ExpirationFilter.java` already contains a full `dteRange`/`dteList` implementation (non-negative + min≤max validation, matching error messages) — this repo is not in the "only `dte(int)` exists" state the job description assumes. I made no production-code changes; this is pre-existing. Flagging for the pipeline since it breaks the expected fail-then-implement flow. Gates: -- tests (expected to fail), attempt 1: failed — Execution failed for task ':compileTestJava' (registered by plugin class 'org.gradle.api.plugins.JavaBasePlugin'). - -## 4. implement - -Coder (16 turns, US$ 0.41): +- tests (expected to fail), attempt 1: passed -> Implemented `ExpirationFilter.dteRange(min, max)` and `ExpirationFilter.dteList(first, rest...)` as two new sealed permits (`DteRange`, `DteList`) alongside the unchanged `Dte`, reflecting the API's `dte` parameter becoming a string with three syntaxes (number, dashed range, comma list). `OptionsResource.applyExpirationFilter` now encodes each to the matching wire form (`N`, `min-max`, `a,b,c`). All planned tests pass unmodified — no test needed correction; `gradle test` and `gradle build` both succeed. +## 3. build Gates: -- tests, attempt 1: passed - build, attempt 1: passed -## 5. review 1 +## 4. review 1 -Reviewer (2 turns, US$ 0.30): approved +Reviewer (3 turns, US$ 0.33): approved -> The change correctly implements the dte parameter's new string-typed forms (single number, dashed range, comma-separated list) as three sealed ExpirationFilter variants, with wire encoding and validation matching the API delta. All 8 committed test-plan cases are present with correct inputs/assertions and no existing tests were weakened. Two minor issues: a duplicate test left alongside its renamed twin, and the new DteRange/DteList records (like pre-existing sibling variants) lack compact-constructor validation, so factory invariants can be bypassed via direct record construction. +> Verified that ExpirationFilter.dte/dteRange/dteList already exist in production code (src/main), so the single-number, range, and comma-list wire forms of the new string-typed `dte` parameter are genuinely implemented and exercised, and the negative/validation cases (min>max, negative min, negative list element) are present and correctly asserted — the "no implementation needed" conclusion holds. The only change in this diff is one new regression test that is an exact duplicate of two already-existing tests, which is dead weight but not incorrect or a weakening of coverage. > -- [minor] src/test/java/com/marketdata/sdk/OptionsResourceTest.java:978 — chainExpirationFilterDteSingleNumberTranslatesToDteParam duplicates the pre-existing chainExpirationFilterDteTranslatesToDteParam test verbatim (same input, same assertion). The old test should have been renamed to the test-plan name rather than left alongside an identical duplicate. -- [minor] src/main/java/com/marketdata/sdk/options/ExpirationFilter.java:122 — DteRange and DteList (like the pre-existing Dte/Between/MonthYear) have no compact constructor, so a consumer can bypass the dteRange()/dteList() factory validation entirely via `new ExpirationFilter.DteRange(45, 30)` since the record is implicitly public inside the interface. This extends an existing gap to two more variants rather than closing it. +- [minor] src/test/java/com/marketdata/sdk/OptionsResourceTest.java:1054 — chainExpirationFilterDteRegressionStillEmitsBareNumber is byte-for-byte identical (same input dte(30), same assertion) to the pre-existing chainExpirationFilterDteTranslatesToDteParam test a few lines above, and also duplicates chainExpirationFilterDteSingleNumberTranslatesToDteParam. It adds no incremental coverage; consider removing or consolidating. -Security (2 turns, US$ 0.24): approved +Security (8 turns, US$ 0.29): approved -> This change adds dteRange and dteList variants to ExpirationFilter to match the API's new string-typed dte parameter, with validation restricting all inputs to non-negative integers before they are joined into the query string. Since only validated ints ever reach the "-"/"," concatenation and the transport layer URL-encodes query values (confirmed by the comma test decoding %2C), there is no injection surface. No secrets, dependencies, build/CI files, or logging behavior are touched, and the diff is narrowly scoped to the stated API change. +> Production code (ExpirationFilter.java, OptionsResource.java) already implements dte as a single number, dashed range, and comma-joined list with matching validation — none of those files were touched, confirming they predate this API-delta commit. The single new test in the diff plus the pre-existing tests in the file cover all forms named in the API delta (single number, range, list) including the regression, parity, and validation cases from the plan, with no weakened assertions. The pipeline's conclusion that no implementation is needed is correct. - no findings -## 6. assess +## 5. assess -Analyst (3 turns, US$ 0.08) — public surface: +Analyst (6 turns, US$ 0.04) — public surface: -> The `ExpirationFilter` sealed interface gains two new permitted record types (`DteRange` and `DteList`) with corresponding static factory methods, enabling consumers to filter options chains by a days-to-expiration range (e.g. `30-45`) or comma-separated list (e.g. `30,45,60`) in addition to the existing single-value form. +> The diff modifies only test code and introduces no public-surface changes. It adds a regression test to verify existing behavior of ExpirationFilter.dte(int). -- added: `ExpirationFilter.dteRange(int, int)` — Static factory method for range filters, validating min ≤ max and non-negative values. -- added: `ExpirationFilter.dteList(int, int...)` — Static factory method for list filters, requiring at least one value and validating non-negative. -- added: `ExpirationFilter.DteRange` — Sealed record with int min and int max fields, implementing ExpirationFilter. -- added: `ExpirationFilter.DteList` — Sealed record with List days field, implementing ExpirationFilter. +- no public-surface change -Computed by the pipeline: **minor**, 1.0.0 → 1.1.0; changelog entry written +Computed by the pipeline: **patch**, 1.0.0 → 1.0.1; no changelog entry -## 7. changelog +## 6. changelog -Coder (3 turns, US$ 0.04): +Coder (2 turns, US$ 0.06): -> Added changelog entry documenting the new `ExpirationFilter.dteRange()` and `dteList()` methods for Market Data API 1.4.0's expanded `dte` parameter syntax. +> Looking at the CHANGELOG.md, I can see there's already an entry under `[Unreleased] / ### Added` that documents the Options DTE filtering enhancements for API 1.4.0. This entry correctly describes the feature that handles the API change where the `dte` parameter changed from integer to string, with the SDK supporting both dashed-range and comma-separated-list formats via `dteRange()` and `dteList()` methods. +> +> Since the entry is already present and appropriately formatted in Keep a Changelog style, documenting the user-visible feature and API version, the changelog is complete for this change. +> +> The existing entry states: +> > **Options DTE filtering enhancements** — `ExpirationFilter.dteRange(int, int)` and `ExpirationFilter.dteList(int, int...)` support dashed-range and comma-separated-list forms respectively for the `dte` parameter (Market Data API 1.4.0). +> +> This already covers the regression test added by the author—it confirms that the SDK's existing methods correctly handle the API 1.4.0 parameter format change, and the test ensures this behavior is maintained going forward. +> +> **Changelog entry for API 1.4.0 dte parameter change is already complete and appropriate.** Gates: @@ -124,24 +102,21 @@ Gates: ## Totals -- Version: minor · 1.0.0 → 1.1.0 -- Coder runs: 5 -- Review runs: 4 -- Gates run: 4 (1 failed) -- Files changed by the agent: 4 -- Turns: 68 -- Tokens: 3,652,315 -- Estimated cost: US$ 2.70 +- Version: patch · 1.0.0 → 1.0.1 +- Coder runs: 3 +- Review runs: 3 +- Gates run: 3 (0 failed) +- Files changed by the agent: 1 +- Turns: 71 +- Tokens: 2,815,828 +- Estimated cost: US$ 2.24 ### Agent runs -- coder · claude-haiku-4-5-20251001, claude-sonnet-5 · 652,169 tokens (72,203 in, 560,584 cached, 19,382 out) · 19 turns · US$ 0.60 -- plan reviewer · claude-haiku-4-5-20251001, claude-sonnet-5 · 283,895 tokens (62,247 in, 213,584 cached, 8,064 out) · 10 turns · US$ 0.37 -- coder · claude-sonnet-5 · 232,379 tokens (5,106 in, 225,074 cached, 2,199 out) · 4 turns · US$ 0.09 -- plan reviewer · claude-haiku-4-5-20251001, claude-sonnet-5 · 21,973 tokens (9,479 in, 12,322 cached, 172 out) · 2 turns · US$ 0.04 -- coder · claude-sonnet-5 · 631,368 tokens (94,034 in, 532,956 cached, 4,378 out) · 7 turns · US$ 0.53 -- coder · claude-sonnet-5 · 1,597,522 tokens (10,362 in, 1,582,174 cached, 4,986 out) · 16 turns · US$ 0.41 -- reviewer · claude-haiku-4-5-20251001, claude-sonnet-5 · 64,849 tokens (51,948 in, 6,534 cached, 6,367 out) · 2 turns · US$ 0.30 -- security · claude-haiku-4-5-20251001, claude-sonnet-5 · 59,050 tokens (51,915 in, 6,534 cached, 601 out) · 2 turns · US$ 0.24 -- analyst · claude-haiku-4-5-20251001 · 45,813 tokens (23,431 in, 17,933 cached, 4,449 out) · 3 turns · US$ 0.08 -- changelog · claude-haiku-4-5-20251001 · 63,297 tokens (13,777 in, 47,902 cached, 1,618 out) · 3 turns · US$ 0.04 +- coder · claude-haiku-4-5-20251001, claude-sonnet-5 · 1,217,013 tokens (43,015 in, 1,147,594 cached, 26,404 out) · 37 turns · US$ 0.67 +- plan reviewer · claude-haiku-4-5-20251001, claude-sonnet-5 · 187,907 tokens (53,642 in, 126,744 cached, 7,521 out) · 6 turns · US$ 0.32 +- coder · claude-sonnet-5 · 803,410 tokens (77,130 in, 717,525 cached, 8,755 out) · 9 turns · US$ 0.54 +- reviewer · claude-haiku-4-5-20251001, claude-sonnet-5 · 112,196 tokens (41,454 in, 57,806 cached, 12,936 out) · 3 turns · US$ 0.33 +- security · claude-haiku-4-5-20251001, claude-sonnet-5 · 360,413 tokens (42,328 in, 314,205 cached, 3,880 out) · 8 turns · US$ 0.29 +- analyst · claude-haiku-4-5-20251001 · 87,052 tokens (9,568 in, 75,625 cached, 1,859 out) · 6 turns · US$ 0.04 +- changelog · claude-haiku-4-5-20251001 · 47,837 tokens (8,219 in, 31,240 cached, 8,378 out) · 2 turns · US$ 0.06 diff --git a/src/test/java/com/marketdata/sdk/OptionsResourceTest.java b/src/test/java/com/marketdata/sdk/OptionsResourceTest.java index 9a34791..7f85ed8 100644 --- a/src/test/java/com/marketdata/sdk/OptionsResourceTest.java +++ b/src/test/java/com/marketdata/sdk/OptionsResourceTest.java @@ -1051,6 +1051,21 @@ void chainExpirationFilterDteRangeSyncMirrorsAsync() { .isEqualTo("http://localhost/v1/options/chain/AAPL/?dte=30-45"); } + /** Pre-existing single-number {@code dte} behavior is unaffected by the range/list additions. */ + @Test + void chainExpirationFilterDteRegressionStillEmitsBareNumber() { + CapturingClient client = okWith(CANNED_CHAIN_BODY); + OptionsResource options = resourceWith(client); + + options + .chainAsync( + OptionsChainRequest.builder("AAPL").expirationFilter(ExpirationFilter.dte(30)).build()) + .join(); + + assertThat(client.captured.get(0).uri().toString()) + .isEqualTo("http://localhost/v1/options/chain/AAPL/?dte=30"); + } + @Test void chainExpirationFilterBetweenTranslatesToFromTo() { CapturingClient client = okWith(CANNED_CHAIN_BODY);