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
77 changes: 77 additions & 0 deletions .sdk-sync/1.4.0-f19c2d4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# 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 13:43:54Z |
| Outcome | **failed — the reviewer did not finish: exited with 1: {"duration_api_ms":79402,"stop_reason":"tool_use","session_id":"8022d31f-1598-4663-b7c4-7c3794e92bc3","total_cost_usd":0.5814339999999999,"usage":{"input_tokens":30,"cache_creation_input_tokens":74259,"cache_read_input_tokens":932425,"output_tokens":6999,"output_tokens_details":{"thinking_tokens":4447},"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":74259,"ephemeral_5m_input_tokens":0},"inference_geo":"not_available","iterations":[{"input_tokens":2,"output_tokens":296,"cache_read_input_tokens":73910,"cache_creation_input_tokens":349,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":349},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-haiku-4-5-20251001":{"inputTokens":27788,"outputTokens":15,"cacheReadInputTokens":0,"cacheCreationInputTokens":0,"webSearchRequests":0,"costUSD":0.027863,"contextWindow":200000,"maxOutputTokens":32000,"thinkingTokens":0,"canonicalModel":"claude-haiku-4-5","provider":"firstParty","costBasis":"list"},"claude-sonnet-5":{"inputTokens":30,"outputTokens":6999,"cacheReadInputTokens":932425,"cacheCreationInputTokens":74259,"webSearchRequests":0,"costUSD":0.553571,"contextWindow":1000000,"maxOutputTokens":64000,"thinkingTokens":4447,"canonicalModel":"claude-sonnet-5","provider":"firstParty","costBasis":"list"}},"permission_denials":[],"terminal_reason":"max_turns","fast_mode_state":"off","fast_mode_disabled_reason":"sdk_opt_in_required","subagent_stats":{"spawned":0,"requested":{"background":0,"foreground":0,"unset":0},"started_in_background":0,"max_depth":0,"spawned_by_subagents":0,"completed":0,"failed":0,"killed":{"parent":0,"user":0,"system":0},"refused":{"depth_limit":0,"concurrency_limit":0,"budget":0},"by_type":{}},"is_error":true,"num_turns":16,"subtype":"error_max_turns","errors":["Reached maximum number of turns (15)"],"type":"result","duration_ms":78783,"uuid":"f65dcfc7-ec80-41f3-ad10-b7ee2cd96800","queued_turn_count":0,"result_index":0}** |

## Changes in the API surface

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

## 1. tests

Coder (24 turns, US$ 0.52):

> The build fails as required, with compilation errors on the new `dteRange`/`dteIn` symbols that don't exist yet — confirming the tests correctly specify not-yet-implemented behavior.
>
> ## Summary
>
> Added unit tests to `src/test/java/com/marketdata/sdk/OptionsResourceTest.java` specifying the new `dte` query-parameter shapes for `GET /v1/options/chain/{underlying}/` (single number, range, comma-separated list), replacing the prior integer-only form:
>
> - **URL translation tests** (alongside the existing `chainExpirationFilterDteTranslatesToDteParam`):
> - `chainExpirationFilterDteRangeTranslatesToDashedDteParam` — `ExpirationFilter.dteRange(30, 45)` → `?dte=30-45`
> - `chainExpirationFilterDteListTranslatesToCommaSeparatedDteParam` — `ExpirationFilter.dteIn(List.of(30, 45, 60))` → `?dte=30,45,60`
> - **Factory validation tests** (alongside the existing `expirationFilterDteRejectsNegative`):
> - `expirationFilterDteRangeRejectsNegativeBound`, `expirationFilterDteRangeRejectsMinGreaterThanMax`
> - `expirationFilterDteInRejectsEmptyList`, `expirationFilterDteInRejectsNegativeValue`
>
> These specify two new static factories, `ExpirationFilter.dteRange(int min, int max)` and `ExpirationFilter.dteIn(List<Integer> days)`, following the same sealed-filter/factory-validation convention as the existing `dte`/`between`/`monthYear` and the sibling `StrikeFilter.range`. No existing tests were weakened. Confirmed `gradle test --no-daemon --console=plain` fails at `compileTestJava` since these symbols don't exist yet.

Gates:

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

## 2. implement

Coder (23 turns, US$ 0.46):

> All within `src/main/java/**` and `src/test/java/**`. `gradle test` and `gradle build` both pass.
>
> ## Summary
>
> Implemented the `dte` query-parameter change for `GET /v1/options/chain/{underlying}/` (now a string accepting a single number, a range, or a comma-separated list, instead of a plain integer):
>
> - **`ExpirationFilter.java`**: `Dte` now wraps a formatted wire `String expression` instead of a raw `int days`. Added two new factories alongside the existing `dte(int)`: `dteRange(int min, int max)` (→ `min-max`, validates non-negative + `min <= max`) and `dteIn(List<Integer> days)` (→ comma-joined, validates non-empty + all non-negative), following the same sealed-filter/factory-validation style as the sibling `StrikeFilter`.
> - **`OptionsResource.java`**: `applyExpirationFilter` now writes `v.expression()` for the `dte` query param instead of the old `v.days()`.
>
> Also fixed one test from phase 1: `chainExpirationFilterDteListTranslatesToCommaSeparatedDteParam` asserted the URI with a literal comma, but the query encoder percent-encodes it (`%2C`); updated it to URL-decode before asserting, matching the existing convention used by `StocksResourceTest`'s `symbols=` assertions. No test was weakened — this only corrected an encoding-unaware expectation.
>
> `gradle test --no-daemon --console=plain` and `gradle build --no-daemon --console=plain` both pass, including Spotless formatting (applied via `spotlessApply` to reflow a javadoc comment) and JaCoCo checks.

Gates:

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

## 3. review 1

## Totals

- Version: not assessed
- Coder runs: 2
- Review runs: 0
- Gates run: 3 (1 failed)
- Files changed by the agent: 3
- Turns: 47
- Tokens: 2,722,109
- Estimated cost: US$ 0.98

### Agent runs

- coder · claude-haiku-4-5-20251001, claude-sonnet-5 · 1,088,579 tokens (49,730 in, 1,027,400 cached, 11,449 out) · 24 turns · US$ 0.52
- coder · claude-sonnet-5 · 1,633,530 tokens (19,282 in, 1,607,806 cached, 6,442 out) · 23 turns · US$ 0.46
2 changes: 1 addition & 1 deletion src/main/java/com/marketdata/sdk/OptionsResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,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", v.expression());
} 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 Down
41 changes: 39 additions & 2 deletions src/main/java/com/marketdata/sdk/options/ExpirationFilter.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.marketdata.sdk.options;

import java.time.LocalDate;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

/**
* Mutually-exclusive expiration filter for {@code /v1/options/chain/}. The chain endpoint's
Expand Down Expand Up @@ -44,7 +46,37 @@ static Dte dte(int days) {
if (days < 0) {
throw new IllegalArgumentException("dte must be non-negative");
}
return new Dte(days);
return new Dte(Integer.toString(days));
}

/**
* Inclusive days-to-expiration range — wire form {@code ?dte=min-max}, e.g. {@code 30-45}. {@code
* min} must not exceed {@code max}.
*/
static Dte dteRange(int min, int max) {
if (min < 0 || max < 0) {
throw new IllegalArgumentException("dte must be non-negative");
}
if (min > max) {
throw new IllegalArgumentException("min must be <= max");
}
return new Dte(min + "-" + max);
}

/**
* A discrete set of days-to-expiration values — wire form {@code ?dte=d1,d2,...}. {@code days}
* must not be empty.
*/
static Dte dteIn(List<Integer> days) {
if (days.isEmpty()) {
throw new IllegalArgumentException("dte list must not be empty");
}
for (int d : days) {
if (d < 0) {
throw new IllegalArgumentException("dte must be non-negative");
}
}
return new Dte(days.stream().map(String::valueOf).collect(Collectors.joining(",")));
}

/**
Expand Down Expand Up @@ -77,7 +109,12 @@ record OnDate(LocalDate date) implements ExpirationFilter {
}
}

record Dte(int days) implements ExpirationFilter {}
/**
* The {@code dte} wire expression — a single number ({@link #dte}), a dashed range ({@link
* #dteRange}), or a comma-separated list ({@link #dteIn}). Formatting is done at the factory so
* the record just carries the already-valid wire string.
*/
record Dte(String expression) implements ExpirationFilter {}

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

Expand Down
62 changes: 62 additions & 0 deletions src/test/java/com/marketdata/sdk/OptionsResourceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@
import com.marketdata.sdk.options.StrikeFilter;
import com.marketdata.sdk.options.StrikeRange;
import java.net.URI;
import java.net.URLDecoder;
import java.net.http.HttpClient;
import java.net.http.HttpHeaders;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Clock;
import java.time.Duration;
import java.time.LocalDate;
Expand Down Expand Up @@ -975,6 +977,38 @@ void chainExpirationFilterDteTranslatesToDteParam() {
.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 chainExpirationFilterDteListTranslatesToCommaSeparatedDteParam() {
CapturingClient client = okWith(CANNED_CHAIN_BODY);
OptionsResource options = resourceWith(client);

options
.chainAsync(
OptionsChainRequest.builder("AAPL")
.expirationFilter(ExpirationFilter.dteIn(List.of(30, 45, 60)))
.build())
.join();

String url = URLDecoder.decode(client.captured.get(0).uri().toString(), StandardCharsets.UTF_8);
assertThat(url).isEqualTo("http://localhost/v1/options/chain/AAPL/?dte=30,45,60");
}

@Test
void chainExpirationFilterBetweenTranslatesToFromTo() {
CapturingClient client = okWith(CANNED_CHAIN_BODY);
Expand Down Expand Up @@ -1203,6 +1237,34 @@ void expirationFilterDteRejectsNegative() {
.hasMessageContaining("dte must be non-negative");
}

@Test
void expirationFilterDteRangeRejectsNegativeBound() {
assertThatThrownBy(() -> ExpirationFilter.dteRange(-1, 45))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("dte must be non-negative");
}

@Test
void expirationFilterDteRangeRejectsMinGreaterThanMax() {
assertThatThrownBy(() -> ExpirationFilter.dteRange(45, 30))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("min must be <= max");
}

@Test
void expirationFilterDteInRejectsEmptyList() {
assertThatThrownBy(() -> ExpirationFilter.dteIn(List.of()))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("dte list must not be empty");
}

@Test
void expirationFilterDteInRejectsNegativeValue() {
assertThatThrownBy(() -> ExpirationFilter.dteIn(List.of(30, -1, 60)))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("dte must be non-negative");
}

@Test
void expirationFilterBetweenRejectsReversedDates() {
assertThatThrownBy(
Expand Down
Loading