Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 146 additions & 0 deletions .sdk-sync/2.0.0-ffb34b7.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# sdk-auto-sync job log

| | |
|---|---|
| API release | 2.0.0 (commit `ffb34b7`) |
| SDK | sdk-java |
| Branch | `sdk-sync/2.0.0-ffb34b7` |
| API surface | `ae2b047c8f6f` → `c2a31e277345` |
| Date | 2026-09-30 12:45:13Z |
| Outcome | **done** |

## Changes in the API surface

~ GET /v1/options/chain/{underlying}/ ?dte: string (was integer)

## 1. test plan

Coder (10 turns, US$ 0.53) — test plan, 12 case(s) (wire 4, regression 1, validation 6, parity 1):

> dte on GET /v1/options/chain/{underlying}/ changes from integer to string with four forms: single value, comma-separated list, closed range, comparison operator. Plan introduces a new sealed DteFilter (exact/list/range/comparison, mirroring StrikeFilter's convention) consumed via ExpirationFilter.dte(DteFilter), keeping the existing ExpirationFilter.dte(int) overload as sugar for the single-value form so old call sites and their negative-value validation keep working; new validation adds the "at most 36500" upper bound across all forms.

| # | category | test | API form | input | expected |
|---|---|---|---|---|---|
| 1 | wire | `chainExpirationFilterDteSingleValueTranslatesToDteParam` | A single value | options.chainAsync(OptionsChainRequest.builder("AAPL").expirationFilter(ExpirationFilter.dte(30)).build()) | request URI equals http://localhost/v1/options/chain/AAPL/?dte=30 |
| 2 | wire | `chainExpirationFilterDteListTranslatesToCommaSeparatedDteParam` | A comma-separated list (15,30,45) | options.chainAsync(OptionsChainRequest.builder("AAPL").expirationFilter(ExpirationFilter.dte(DteFilter.list(15, 30, 45))).build()) | URL-decoded request URI equals http://localhost/v1/options/chain/AAPL/?dte=15,30,45 |
| 3 | wire | `chainExpirationFilterDteRangeTranslatesToDashedDteParam` | A closed range (0-45) | options.chainAsync(OptionsChainRequest.builder("AAPL").expirationFilter(ExpirationFilter.dte(DteFilter.range(0, 45))).build()) | request URI equals http://localhost/v1/options/chain/AAPL/?dte=0-45 |
| 4 | wire | `chainExpirationFilterDteComparisonRendersWithOperatorPrefix` | a comparison operator (>=30, <45) | options.chainAsync(OptionsChainRequest.builder("AAPL").expirationFilter(ExpirationFilter.dte(DteFilter.comparison(DteFilter.Operator.GTE, 30))).build()) | request URI contains dte=%3E%3D30 (URL-encoded >=30) |
| 5 | regression | `expirationFilterDteRejectsNegative` | n/a — pre-existing single-integer dte(int) factory | ExpirationFilter.dte(-1) | IllegalArgumentException "dte must be non-negative" (unchanged from before the type change) |
| 6 | validation | `expirationFilterDteRejectsAboveMaximum` | Values are whole numbers of days, at most 36500 | ExpirationFilter.dte(36501) | IllegalArgumentException "dte must be at most 36500" |
| 7 | validation | `dteFilterListRejectsEmptyList` | A comma-separated list (15,30,45) | DteFilter.list() | IllegalArgumentException "days must be non-empty" |
| 8 | validation | `dteFilterListRejectsNegativeElement` | A comma-separated list (15,30,45) | DteFilter.list(15, -1, 45) | IllegalArgumentException "dte must be non-negative" |
| 9 | validation | `dteFilterRangeRejectsMinGreaterThanMax` | A closed range (0-45) | DteFilter.range(45, 0) | IllegalArgumentException "min must be <= max" |
| 10 | validation | `dteFilterRangeRejectsMaxAboveMaximum` | Values are whole numbers of days, at most 36500 | DteFilter.range(0, 36501) | IllegalArgumentException "dte must be at most 36500" |
| 11 | validation | `dteFilterComparisonRejectsNegativeValue` | a comparison operator (>=30, <45) | DteFilter.comparison(DteFilter.Operator.LT, -1) | IllegalArgumentException "dte must be non-negative" |
| 12 | parity | `chainExpirationFilterDteListSyncSendsSameWireAsAsync` | A comma-separated list (15,30,45) | options.chain(OptionsChainRequest.builder("AAPL").expirationFilter(ExpirationFilter.dte(DteFilter.list(15, 30, 45))).build()) vs options.chainAsync(...).join() | both sync and async requests' URL-decoded URI equal http://localhost/v1/options/chain/AAPL/?dte=15,30,45 |

Plan reviewer (12 turns, US$ 0.25): approved

> The plan covers all four forms enumerated by the dte description (single, comma-list, closed range, comparison) with wire cases matching the exact URI, invents no extra forms, and follows the repo's established ExpirationFilter/StrikeFilter conventions for validation, regression, and parity (single representative parity test, mirroring how StrikeFilter is tested). No new-endpoint response case is needed since this is a parameter type change on an existing endpoint. Approved.

- no findings

## 2. tests

Coder (8 turns, US$ 0.62):

> Build fails as expected — `DteFilter` doesn't exist yet, which is exactly the compilation failure phase 1b requires.
>
> Added to `OptionsResourceTest.java` (no production code touched):
> - 4 wire tests for the new `?dte` forms (single value, comma list, closed range, comparison operator)
> - 1 parity test (sync vs async) for the list form
> - 6 validation tests (new 36500 upper bound on `dte(int)` and `DteFilter.range`, plus `DteFilter.list`/`range`/`comparison` input checks)
> - The regression case (`expirationFilterDteRejectsNegative`) already existed unchanged, so nothing was added for it
> - Added the `DteFilter` import needed by the new cases

Gates:

- tests (expected to fail), attempt 1: failed — Execution failed for task ':compileTestJava' (registered by plugin class 'org.gradle.api.plugins.JavaBasePlugin').

## 3. implement

Coder (18 turns, US$ 0.56):

> Build and tests pass, only production/test source files touched, no forbidden paths modified.
>
> Added `DteFilter.java` (new sealed type: `Exact`/`ListOf`/`Range`/`Comparison`, mirroring `StrikeFilter`) to model the four wire forms of the now-string `?dte=` parameter, with shared `[0, 36500]` validation. `ExpirationFilter.dte(int)` now delegates to `DteFilter.exact(...)` (keeping old call sites and negative-rejection behavior working) plus a new `dte(DteFilter)` overload. `OptionsResource` gained a `dteFilterWireValue` helper analogous to `strikeFilterWireValue` to render each variant onto the query string. `gradle test` and `gradle build` pass (required a `spotlessApply` formatting pass on the test file).

Gates:

- tests, attempt 1: passed
- build, attempt 1: passed

## 4. review 1

Reviewer (7 turns, US$ 0.45): approved

> The dte multi-form filter (DteFilter) is implemented faithfully to the API delta, mirrors the established StrikeFilter pattern (including the @Generated defensive-guard convention), and every test-plan case is present and correctly asserts the documented behavior without weakening existing tests. The only gaps are minor: the breaking API change isn't reflected in CHANGELOG.md (outside the author's allowed paths), and one new test duplicates a pre-existing one.

- [minor] CHANGELOG.md — The dte parameter type change (int to string forms) is a breaking public API change (ExpirationFilter.Dte record field replaced, new DteFilter type) but the Unreleased section of CHANGELOG.md is still empty. Per the repo's Keep a Changelog convention this should be documented. CHANGELOG.md is outside the author's allowed write paths, so a human needs to add it.
- [minor] src/test/java/com/marketdata/sdk/OptionsResourceTest.java:981 — chainExpirationFilterDteSingleValueTranslatesToDteParam duplicates the pre-existing chainExpirationFilterDteTranslatesToDteParam test verbatim (same request, same assertion). Required by the committed test plan, but the two could be consolidated.

Security (2 turns, US$ 0.28): approved

> The change adds a sealed DteFilter type (exact/list/range/comparison) mirroring the existing StrikeFilter pattern and wires it into the options chain `?dte=` query parameter. All wire-value formatting operates on validated, bounded integers (0–36500) and a fixed enum-derived operator prefix, so there is no injection surface, and query encoding is delegated to the existing, already-tested RequestSpec.Builder.query mechanism. No secrets, dependencies, build/CI files, or unrelated code were touched, and no unsafe defaults or data-exposure changes were introduced.

- no findings

## 5. assess

Analyst (4 turns, US$ 0.08) — public surface:

> The API expands the `?dte=` parameter on the options chain endpoint from a single integer to a string supporting four syntactic forms (single value, comma-separated list, range, comparison operator). This introduces a new sealed `DteFilter` interface with four variants and factory methods, and changes the `ExpirationFilter.Dte` record component from `int` to `DteFilter`, breaking existing accessors and constructors.

- added: `DteFilter`
- added: `DteFilter.MAX_DAYS`
- added: `DteFilter.exact(int)`
- added: `DteFilter.list(int[])`
- added: `DteFilter.range(int, int)`
- added: `DteFilter.comparison(DteFilter.Operator, int)`
- added: `DteFilter.Exact`
- added: `DteFilter.ListOf`
- added: `DteFilter.Range`
- added: `DteFilter.Comparison`
- added: `DteFilter.Operator`
- added: `DteFilter.Operator.GT`
- added: `DteFilter.Operator.GTE`
- added: `DteFilter.Operator.LT`
- added: `DteFilter.Operator.LTE`
- added: `DteFilter.Operator.wireValue()`
- added: `ExpirationFilter.dte(DteFilter)`
- changed: `ExpirationFilter.Dte` — Record component changed from int days to DteFilter filter; constructor and filter() accessor signatures now differ.
- removed: `ExpirationFilter.Dte.days()`

Computed by the pipeline: **major**, 1.0.0 → 2.0.0; changelog entry written

## 6. changelog

Coder (3 turns, US$ 0.05):

> Added changelog entry documenting the `DteFilter` sealed type for the string-valued `?dte=` parameter in API 2.0.0, with migration guidance for the removed `days()` accessor.

Gates:

- build, attempt 1: passed

## Totals

- Version: major · 1.0.0 → 2.0.0
- Coder runs: 4
- Review runs: 3
- Gates run: 4 (1 failed)
- Files changed by the agent: 5
- Turns: 64
- Tokens: 3,827,706
- Estimated cost: US$ 2.81

### Agent runs

- coder · claude-haiku-4-5-20251001, claude-sonnet-5 · 266,674 tokens (72,462 in, 174,364 cached, 19,848 out) · 10 turns · US$ 0.53
- plan reviewer · claude-haiku-4-5-20251001, claude-sonnet-5 · 215,709 tokens (38,422 in, 171,065 cached, 6,222 out) · 12 turns · US$ 0.25
- coder · claude-sonnet-5 · 809,972 tokens (104,973 in, 699,441 cached, 5,558 out) · 8 turns · US$ 0.62
- coder · claude-sonnet-5 · 1,978,579 tokens (20,522 in, 1,949,518 cached, 8,539 out) · 18 turns · US$ 0.56
- reviewer · claude-haiku-4-5-20251001, claude-sonnet-5 · 358,273 tokens (68,591 in, 280,942 cached, 8,740 out) · 7 turns · US$ 0.45
- security · claude-haiku-4-5-20251001, claude-sonnet-5 · 65,096 tokens (56,799 in, 6,534 cached, 1,763 out) · 2 turns · US$ 0.28
- analyst · claude-haiku-4-5-20251001 · 70,683 tokens (24,237 in, 42,600 cached, 3,846 out) · 4 turns · US$ 0.08
- changelog · claude-haiku-4-5-20251001 · 62,720 tokens (21,607 in, 40,152 cached, 961 out) · 3 turns · US$ 0.05
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- **Options `?dte` filter as string** (API 2.0.0) — `DteFilter` sealed type
(`exact()`, `list()`, `range()`, `comparison()`) models the new string-valued
`?dte=` parameter on `options.chain()`, supporting single values, comma-delimited
lists, ranges (`start-end`), and comparison operators. `ExpirationFilter.dte(DteFilter)`
accepts the new sealed type; `ExpirationFilter.dte(int)` still works (delegates to
`exact()`).

### Changed

- `ExpirationFilter.Dte` record now wraps a `DteFilter` instead of raw `int days`.

### Removed

- `ExpirationFilter.Dte.days()` accessor — use `DteFilter` methods on the
filter itself instead. Callers passing `int` to `ExpirationFilter.dte()` need
no changes; callers inspecting `Dte.days()` should adapt to the new
`DteFilter` sealed type.

## [1.0.0] - 2026-06-29

First stable release of the Market Data Java &amp; Kotlin SDK — a single JVM
Expand Down
18 changes: 17 additions & 1 deletion src/main/java/com/marketdata/sdk/OptionsResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.marketdata.sdk.options.DteFilter;
import com.marketdata.sdk.options.ExpirationFilter;
import com.marketdata.sdk.options.OptionQuote;
import com.marketdata.sdk.options.OptionsChain;
Expand All @@ -28,6 +29,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;

/**
Expand Down Expand Up @@ -311,7 +313,7 @@ private static void applyExpirationFilter(RequestSpec.Builder b, ExpirationFilte
if (f instanceof ExpirationFilter.OnDate v) {
b.query("expiration", DateTimeFormatter.ISO_LOCAL_DATE.format(v.date()));
} else if (f instanceof ExpirationFilter.Dte v) {
b.query("dte", v.days());
b.query("dte", dteFilterWireValue(v.filter()));
} 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()));
Expand All @@ -325,6 +327,20 @@ private static void applyExpirationFilter(RequestSpec.Builder b, ExpirationFilte
// an if-chain, but there is no reachable else, so no defensive throw is needed.
}

private static String dteFilterWireValue(DteFilter f) {
if (f instanceof DteFilter.Exact v) {
return Integer.toString(v.days());
} else if (f instanceof DteFilter.ListOf v) {
return v.days().stream().map(String::valueOf).collect(Collectors.joining(","));
} else if (f instanceof DteFilter.Range v) {
return v.min() + "-" + v.max();
}
// DteFilter is sealed; the only remaining variant is Comparison. The cast documents that
// exhaustiveness (Java 17 can't prove it in an if-chain) and fails fast if a variant is added.
DteFilter.Comparison v = (DteFilter.Comparison) f;
return v.operator().wireValue() + v.days();
}

private static String strikeFilterWireValue(StrikeFilter f) {
if (f instanceof StrikeFilter.Exact v) {
return formatStrike(v.price());
Expand Down
99 changes: 99 additions & 0 deletions src/main/java/com/marketdata/sdk/options/DteFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package com.marketdata.sdk.options;

import com.marketdata.sdk.Generated;
import java.util.ArrayList;
import java.util.List;

/**
* The chain endpoint's {@code ?dte=} parameter accepts four syntactic forms — a single value, a
* comma-separated list, a closed range, and a comparison ({@code >=30}, {@code <45}, …). The API
* docs say the range/comparison syntax "matches strike and delta", so this mirrors {@link
* StrikeFilter}'s treatment of {@code ?strike=} for those two forms; the comma-separated list is
* unique to {@code dte} (it selects the closest expiration for each value, deduplicated
* server-side).
*
* <p>Every value across every variant must be a whole number of days in {@code [0, 36500]}.
*/
public sealed interface DteFilter
permits DteFilter.Exact, DteFilter.ListOf, DteFilter.Range, DteFilter.Comparison {

/** Maximum value the API accepts for a dte day-count. */
int MAX_DAYS = 36500;

/** Match the expiration whose dte is closest to {@code days}. */
static Exact exact(int days) {
validateDays(days);
return new Exact(days);
}

/** Match the closest expiration for each value in {@code days} — deduplicated server-side. */
static ListOf list(int... days) {
if (days.length == 0) {
throw new IllegalArgumentException("days must be non-empty");
}
List<Integer> boxed = new ArrayList<>(days.length);
for (int d : days) {
validateDays(d);
boxed.add(d);
}
return new ListOf(List.copyOf(boxed));
}

/** Match every expiration whose dte falls in {@code [min, max]} inclusive. */
static Range range(int min, int max) {
if (min > max) {
throw new IllegalArgumentException("min must be <= max");
}
validateDays(min);
validateDays(max);
return new Range(min, max);
}

/** Match every expiration whose dte satisfies {@code operator days} (e.g. {@code >= 30}). */
// @Generated: the null-operator guard is unreachable through the public comparison factories,
// which always supply a non-null Operator from the typed enum.
@Generated
static Comparison comparison(Operator operator, int days) {
if (operator == null) {
throw new IllegalArgumentException("operator must not be null");
}
validateDays(days);
return new Comparison(operator, days);

Check warning on line 61 in src/main/java/com/marketdata/sdk/options/DteFilter.java

View check run for this annotation

marketdata-code-review / Code review

Reachable null guard is excluded from coverage

The comment says the null guard is unreachable, but any Java caller can invoke the public `DteFilter.comparison(null, 30)` method. Marking the method `@Generated` hides this reachable behavior from coverage. Remove the exclusion and add a test for the null-operator contract.
}

private static void validateDays(int days) {
if (days < 0) {
throw new IllegalArgumentException("dte must be non-negative");
}
if (days > MAX_DAYS) {
throw new IllegalArgumentException("dte must be at most " + MAX_DAYS);
}
}

/** Comparison operators accepted by the API — the same four as {@link StrikeFilter.Operator}. */
enum Operator {
GT(">"),
GTE(">="),
LT("<"),
LTE("<=");

private final String wireValue;

Operator(String wireValue) {
this.wireValue = wireValue;
}

/** The wire-form prefix the API expects, e.g. {@code ">="}. */
public String wireValue() {
return wireValue;
}
}

record Exact(int days) implements DteFilter {}

record ListOf(List<Integer> days) implements DteFilter {}

record Range(int min, int max) implements DteFilter {}

record Comparison(Operator operator, int days) implements DteFilter {}
}

Check failure on line 99 in src/main/java/com/marketdata/sdk/options/DteFilter.java

View check run for this annotation

marketdata-code-review / Code review

Public records bypass DTE validation

The nested records have public canonical constructors, so callers can bypass every factory check with values such as `new Exact(-1)`, `new Range(45, 0)`, or `new ListOf(null)`. This permits malformed requests, null failures, and mutable list state despite the type's documented invariant. Add validating compact constructors to every record and defensively copy `ListOf.days`; the factories can then delegate to them.
22 changes: 16 additions & 6 deletions src/main/java/com/marketdata/sdk/options/ExpirationFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,22 @@
return new All();
}

/** Days-to-expiration filter — wire form {@code ?dte=N}. */
/**
* Days-to-expiration filter, single value — wire form {@code ?dte=N}. Shorthand for {@link
* #dte(DteFilter)} with {@link DteFilter#exact(int)}.
*/
static Dte dte(int days) {
if (days < 0) {
throw new IllegalArgumentException("dte must be non-negative");
}
return new Dte(days);
return new Dte(DteFilter.exact(days));
}

/**
* Days-to-expiration filter accepting any of the API's four syntactic forms — see {@link
* DteFilter} ({@link DteFilter#exact}, {@link DteFilter#list}, {@link DteFilter#range}, {@link
* DteFilter#comparison}).
*/
static Dte dte(DteFilter filter) {
Objects.requireNonNull(filter, "filter");
return new Dte(filter);

Check warning on line 57 in src/main/java/com/marketdata/sdk/options/ExpirationFilter.java

View check run for this annotation

marketdata-code-review / Code review

Public DTE API lacks resource documentation

The pull request adds a public overload and changes the public `Dte` constructor/accessor contract, but no resource page under `docs/` is changed. Update the options-chain documentation with all four forms and the migration from `days()` to `filter()`.
}

/**
Expand Down Expand Up @@ -77,7 +87,7 @@
}
}

record Dte(int days) implements ExpirationFilter {}
record Dte(DteFilter filter) implements ExpirationFilter {}

record Between(LocalDate from, LocalDate to) implements ExpirationFilter {}

Expand Down
Loading
Loading