diff --git a/.github/BUG_FINDING.md b/.github/BUG_FINDING.md new file mode 100644 index 0000000..acf40cc --- /dev/null +++ b/.github/BUG_FINDING.md @@ -0,0 +1,701 @@ +# Bug Finding Workflow + +This document defines a systematic process for proactively discovering bugs through codebase exploration and testing. + +> **IMPORTANT: Every bug found MUST be submitted as a GitHub issue.** +> +> Do NOT just document bugs in markdown files, notes, or comments. Each bug you find must result in an actual GitHub issue created via: +> - **CLI**: `gh issue create --label "bug" --title "[Bug]: ..." --body "..."` +> - **Web**: [Create Bug Report](https://github.com/MarketDataApp/sdk-java/issues/new?template=bug.yml) +> +> A bug hunt is not complete until all discovered bugs exist as GitHub issues. + +## Overview + +**Purpose**: Proactive bug discovery vs reactive bug processing + +- **BUG_FINDING.md** (this document): Find bugs before users encounter them +- **ISSUE_WORKFLOW.md**: Process bug reports submitted by users + +**Workflow**: Find Bug → **Create GitHub Issue (REQUIRED)** → [ISSUE_WORKFLOW.md] → Fix + +Each bug found MUST result in a GitHub issue. No exceptions. + +**When to use this document**: +- QA passes before releases +- Pre-release validation +- Exploratory testing sessions +- After significant refactors +- When onboarding to understand edge cases + +--- + +## Prerequisites + +### Environment Setup + +```bash +# Required +./gradlew build +java -version # Must be JDK 17, 21, or 25 + +# API token for integration tests +export MARKETDATA_TOKEN="your_token_here" +``` + +### Baseline Verification + +Before hunting for bugs, confirm the test suite passes: + +```bash +./gradlew test +``` + +If tests fail, fix those issues first. Bug finding assumes a working baseline. + +### Architecture Understanding + +Familiarize yourself with key components: +- `MarketDataClient` - Main entry point +- `HttpTransport` - HTTP handling, retry logic, rate-limit tracking, async dispatch +- `Configuration` / `EnvVars` - Configuration and token resolution (cascade) +- Resource façades - `StocksResource`, `OptionsResource`, `MarketsResource`, `FundsResource`, `UtilitiesResource` +- Response records - the named `*Response` types implementing `MarketDataResponse`, payload via `values()` + +--- + +## Exploration Areas + +Prioritized by historical bug likelihood: + +| Priority | Area | Bug Likelihood | Common Issues | +|----------|------|----------------|---------------| +| 1 | Response Format Handling | High | CSV/HTML decoding, column projection nulls | +| 2 | Collection Boundary Conditions | High | Index access on empty `values()` lists | +| 3 | Concurrent Request Handling | Medium | Partial failures, header merging | +| 4 | Date/Time Parsing | Medium | Timestamp formats, boundaries | +| 5 | Multi-Symbol Operations | Medium | Empty lists, deduplication | + +--- + +## Area 1: Response Format Handling + +### What Can Go Wrong + +- Fields fail to decode when the response format is CSV or HTML +- Human-readable CSV has different column names than the typed JSON +- Optional fields present in JSON but absent in CSV/HTML +- Column projection (`columns(...)`) silently nulls a field that should raise `ParseError` + +### Test Scenarios + +#### 1.1 Format Switching + +Test each endpoint with all three formats. Typed JSON is the default (`values()`); +CSV is reached via `asCsv()` and HTML via the HTML facet: + +```java +try (var client = new MarketDataClient()) { + + // JSON (default, typed) — should work + var jsonResult = client.stocks().candles( + StockCandlesRequest.builder(StockResolution.DAILY, "AAPL") + .from(LocalDate.parse("2024-01-02")) + .to(LocalDate.parse("2024-01-03")) + .build()); + + // CSV — check the CsvResponse decodes + CsvResponse csvResult = client.stocks().asCsv().candles( + StockCandlesRequest.builder(StockResolution.DAILY, "AAPL") + .from(LocalDate.parse("2024-01-02")) + .to(LocalDate.parse("2024-01-03")) + .build()); + + // HTML — check the HTML facet decodes + var htmlResult = client.stocks().asHtml().candles( + StockCandlesRequest.builder(StockResolution.DAILY, "AAPL") + .from(LocalDate.parse("2024-01-02")) + .to(LocalDate.parse("2024-01-03")) + .build()); + + // Verify: Does accessing values()/rawBody() throw for any format? + // Bug indicator: ParseError, ClassCastException, or NullPointerException +} +``` + +> Kotlin equivalent: same call shape inside `MarketDataClient().use { client -> ... }`, +> reading `resp.values()` / `csvResult.rawBody()`. + +#### 1.2 Human-Readable & Column Projection + +The `human`/`headers` shaping params live on the CSV facet; `columns(...)` projects fields: + +```java +try (var client = new MarketDataClient()) { + + // Regular CSV + CsvResponse regular = client.stocks().asCsv().human(false).candles( + StockCandlesRequest.builder(StockResolution.DAILY, "AAPL") + .from(LocalDate.parse("2024-01-02")) + .to(LocalDate.parse("2024-01-03")) + .build()); + + // Human-readable CSV + CsvResponse human = client.stocks().asCsv().human(true).candles( + StockCandlesRequest.builder(StockResolution.DAILY, "AAPL") + .from(LocalDate.parse("2024-01-02")) + .to(LocalDate.parse("2024-01-03")) + .build()); + + // Column projection (Option A strict decoding) + var projected = client.stocks().columns("time", "close").candles( + StockCandlesRequest.builder(StockResolution.DAILY, "AAPL") + .from(LocalDate.parse("2024-01-02")) + .to(LocalDate.parse("2024-01-03")) + .build()); + + // Verify: Are all fields correctly mapped in both human modes? + // Verify: A non-requested column decodes to null (OK); a required column the + // API omits must raise ParseError — never silently null. + // Bug indicator: Missing data in human mode, wrong field mappings, + // a required field nulled instead of raising ParseError +} +``` + +### Red Flags + +- `com.marketdata.sdk.exception.ParseError` thrown on a format that should decode +- `ClassCastException` — a value decoded to the wrong type +- `NullPointerException` on a field that should be present +- Missing data only in non-JSON (CSV/HTML) formats +- Different results between `human(true)` and `human(false)` +- A required field nulled by `columns(...)` instead of raising `ParseError` + +### Pass/Fail Criteria + +| Scenario | Pass | Fail | +|----------|------|------| +| JSON format | Returns typed response (`values()`) | Exception thrown | +| CSV format | Returns `CsvResponse` | `ParseError` / `ClassCastException` | +| HTML format | Returns HTML response | `ParseError` / `ClassCastException` | +| Human-readable | Same data as regular CSV | Data missing or incorrect | +| Column projection | Non-requested field → null; required omission → `ParseError` | Required field silently nulled | + +--- + +## Area 2: Collection Boundary Conditions + +### What Can Go Wrong + +- Accessing `values().get(0)` without checking the list is non-empty +- Single item returned inconsistently vs a list +- Missing optional fields causing `NullPointerException` on access + +> The SDK contract: `values()` returns an **empty `List` (never `null`)** when there is +> no data, and `isNoData()` is `true`. Frame "empty results" checks around that contract. + +### Test Scenarios + +#### 2.1 Empty Results + +```java +try (var client = new MarketDataClient()) { + + // Request data for a date range with no trading (e.g., a weekend) + var result = client.stocks().candles( + StockCandlesRequest.builder(StockResolution.DAILY, "AAPL") + .from(LocalDate.parse("2024-01-06")) // Saturday + .to(LocalDate.parse("2024-01-07")) // Sunday + .build()); + + // Verify: Does the SDK handle empty results gracefully? + // Bug indicator: IndexOutOfBoundsException, or values() returning null + + // Check all access patterns: + // - Direct indexing: result.values().get(0) + // - Empty check: result.isNoData() / result.values().isEmpty() + // - Iteration: for (StockCandle c : result.values()) { ... } +} +``` + +#### 2.2 Single Item Response + +```java +try (var client = new MarketDataClient()) { + + // Request exactly one item + var result = client.stocks().quote(StockQuotesRequest.of("AAPL")); + + // Verify: Is the response consistently a list off values()? + // Bug indicator: Single item modeled inconsistently, iteration fails +} +``` + +#### 2.3 Missing Optional Fields + +```java +try (var client = new MarketDataClient()) { + + // Earnings often have missing fields (forward quarters null reportedEPS, etc.) + var result = client.stocks().earnings( + StockEarningsRequest.builder("AAPL") + .from(LocalDate.parse("2024-01-01")) + .build()); + + // Verify: Are @Nullable fields handled without throwing? + // Bug indicator: NullPointerException when an optional field is absent +} +``` + +### Red Flags + +- `IndexOutOfBoundsException` on `values().get(0)` +- `values()` returning `null` instead of an empty `List` +- `NullPointerException` on an optional/`@Nullable` field +- Different behavior with 0, 1, or 2+ results + +### Pass/Fail Criteria + +| Scenario | Pass | Fail | +|----------|------|------| +| Empty list | Empty `List`, `isNoData()` true, no error | `IndexOutOfBoundsException` or `null` | +| Single item | Consistent `List` off `values()` | Type changes based on count | +| Missing optional | `@Nullable` field is `null`, no error | `NullPointerException` | + +--- + +## Area 3: Concurrent Request Handling + +### What Can Go Wrong + +- Partial failures not properly reported across a concurrent fan-out +- Headers / rate-limit info from multiple requests conflicting +- Auto-chunk boundaries causing data loss or duplication +- Rate limiting not properly handled in parallel + +> Two concurrency shapes exist: `options.quotes` **fans out one request per contract** +> and returns a `Map` (per-symbol status observable); +> intraday `stocks.candles` spanning more than ~one year **auto-splits** into concurrent +> year-sized sub-requests that are merged into one response. + +### Test Scenarios + +#### 3.1 Partial Failures + +```java +try (var client = new MarketDataClient()) { + + // Fan out across contracts, one valid, one bogus + Map bySymbol = client.options().quotes( + OptionsQuotesRequest.builder( + "AAPL250117C00150000", "BOGUS_OPTION_XYZ", "AAPL250117P00150000") + .build()); + + // Verify: How are partial failures reported per key? + // Bug indicator: Silent failures, missing map entries without errors + bySymbol.forEach((sym, resp) -> + System.out.println(sym + " → " + resp.values().size() + " rows")); +} +``` + +#### 3.2 Header / Rate-Limit Handling + +```java +try (var client = new MarketDataClient()) { + + // A batched multi-symbol request (one HTTP request, one row per symbol) + StockQuotesResponse result = client.stocks().quotes( + StockQuotesRequest.of("AAPL", "GOOGL", "MSFT", "AMZN", "META")); + + // Verify: Is the per-response rate-limit snapshot coherent? + // Bug indicator: Wrong rate-limit info, stale client-level snapshot + System.out.println("request-scoped: " + result.rateLimit()); + System.out.println("client-level: " + client.getRateLimits()); +} +``` + +#### 3.3 Large Range Auto-Chunking + +```java +try (var client = new MarketDataClient()) { + + // Intraday request spanning > ~1 year triggers the concurrent auto-split + var result = client.stocks().candles( + StockCandlesRequest.builder(StockResolution.minutes(5), "AAPL") + .from(LocalDate.now().minusYears(2)) + .to(LocalDate.now()) + .build()); + + // Verify: Is the merged data complete? Any gaps/dupes at chunk boundaries? + // Bug indicator: Missing rows, duplicate rows at year boundaries +} +``` + +### Red Flags + +- Missing data without error messages or missing map entries +- Duplicate rows after a merge +- Rate-limit snapshot showing incorrect counts +- Rate limit exhaustion with few requests + +### Pass/Fail Criteria + +| Scenario | Pass | Fail | +|----------|------|------| +| Partial failure | Clear per-key error for failed, data for successful | Silent data loss | +| Header / rate-limit | Coherent request-scoped + client-level snapshots | Wrong or stale rate-limit info | +| Auto-chunking | Complete merged data, no duplicates | Data loss or duplication at boundaries | + +--- + +## Area 4: Date/Time Parsing + +### What Can Go Wrong + +- Unix timestamps decoded differently from ISO timestamps +- `LocalDate` vs `ZonedDateTime` boundary handling +- Year boundary edge cases +- Timezone assumptions + +### Test Scenarios + +#### 4.1 Timestamp Formats + +```java +try (var client = new MarketDataClient()) { + + // The request builders take java.time types; the dateFormat universal param + // controls how the response encodes dates. + for (DateFormat df : new DateFormat[]{ + DateFormat.TIMESTAMP, DateFormat.UNIX, DateFormat.SPREADSHEET}) { + try { + var result = client.stocks().dateFormat(df).candles( + StockCandlesRequest.builder(StockResolution.DAILY, "AAPL") + .from(LocalDate.parse("2024-01-02")) + .to(LocalDate.now()) + .build()); + System.out.println("DateFormat " + df + ": OK"); + } catch (MarketDataException e) { + System.out.println("DateFormat " + df + ": FAILED - " + e.getMessage()); + } + } + + // Verify: All response date encodings decode to the right java.time value + // Bug indicator: Some formats fail or decode to the wrong instant +} +``` + +#### 4.2 Year Boundaries + +```java +try (var client = new MarketDataClient()) { + + // Year boundary — Dec 29 to Jan 2 + var result = client.stocks().candles( + StockCandlesRequest.builder(StockResolution.DAILY, "AAPL") + .from(LocalDate.parse("2023-12-29")) + .to(LocalDate.parse("2024-01-02")) + .build()); + + // Verify: Data spans the year boundary correctly + // Bug indicator: Missing data around the year change +} +``` + +#### 4.3 Market Hours + +```java +try (var client = new MarketDataClient()) { + + // Intraday data near market open/close + var result = client.stocks().candles( + StockCandlesRequest.builder(StockResolution.minutes(5), "AAPL") + .from(LocalDate.parse("2024-01-02")) + .to(LocalDate.parse("2024-01-02")) + .build()); + + // Verify: Candle times (ZonedDateTime) land in the expected timezone + // Bug indicator: Data offset by hours, wrong trading session +} +``` + +### Red Flags + +- Different results for equivalent `DateFormat` encodings +- Gaps in data at year/month boundaries +- Timezone-related offsets on `ZonedDateTime` fields +- Unix timestamp decoded as the wrong instant + +### Pass/Fail Criteria + +| Scenario | Pass | Fail | +|----------|------|------| +| Multiple formats | Consistent decoded values | Format-dependent behavior | +| Year boundary | Continuous data | Gap in data | +| Timezone | Correct market hours | Offset data | + +--- + +## Area 5: Multi-Symbol Operations + +### What Can Go Wrong + +- Empty symbol set causes an error instead of a clear validation failure +- Duplicate symbols not deduplicated +- Single vs multiple symbols handled differently +- Symbol case sensitivity issues + +### Test Scenarios + +#### 5.1 Empty Symbol Set + +```java +try (var client = new MarketDataClient()) { + + // Empty varargs / list + try { + StockQuotesResponse result = client.stocks().quotes(StockQuotesRequest.of()); + System.out.println("Empty set: returned " + result.values().size() + " rows"); + } catch (IllegalArgumentException e) { + System.out.println("Empty set: " + e.getMessage()); + } + + // Verify: Should fail fast with a clear IllegalArgumentException at request build, + // or return an empty result — never a crash or ambiguous server error. + // Bug indicator: NullPointerException, IndexOutOfBoundsException, opaque API error +} +``` + +#### 5.2 Duplicate Symbols + +```java +try (var client = new MarketDataClient()) { + + // Duplicates in the symbol list + StockQuotesResponse result = client.stocks().quotes( + StockQuotesRequest.of("AAPL", "AAPL", "GOOGL")); + + // Verify: Duplicates should be deduplicated or handled gracefully + // Bug indicator: Duplicate rows, wasted rate-limit budget +} +``` + +#### 5.3 Case Sensitivity + +```java +try (var client = new MarketDataClient()) { + + var upper = client.stocks().quote(StockQuotesRequest.of("AAPL")); + var lower = client.stocks().quote(StockQuotesRequest.of("aapl")); + var mixed = client.stocks().quote(StockQuotesRequest.of("AaPl")); + + // Verify: All cases should return the same data + // Bug indicator: Case-dependent failures or different data +} +``` + +### Red Flags + +- Empty set causes a crash instead of graceful handling +- Duplicate data in results +- Different behavior for uppercase vs lowercase +- Single symbol returns a different structure than multiple + +### Pass/Fail Criteria + +| Scenario | Pass | Fail | +|----------|------|------| +| Empty set | Empty result or clear `IllegalArgumentException` | Crash or ambiguous error | +| Duplicates | Deduplicated or single request | Duplicate data returned | +| Case handling | Consistent results | Case-dependent behavior | + +--- + +## Bug Documentation + +When you find a bug, you MUST create a GitHub issue for it. Do not just document it in a file or note. + +### Required Information + +Capture these details for each bug: + +1. **Minimal reproduction code** - Smallest code that demonstrates the bug +2. **Expected behavior** - What should happen +3. **Actual behavior** - What actually happens (include error messages) +4. **Environment**: + - SDK version: the `app.marketdata:marketdata-sdk-java` version in your `build.gradle(.kts)` or `pom.xml` + - JDK version: `java -version` + - OS: macOS/Windows/Linux + +### Creating the GitHub Issue (REQUIRED) + +**Option 1: CLI (Preferred)** + +```bash +gh issue create --label "bug" --title "[Bug]: Brief description" --body "$(cat <<'EOF' +## API Documentation Verification +- [x] I have reviewed the [API documentation](https://www.marketdata.app/docs/api) for this endpoint +- [x] The behavior I'm reporting differs from what the API documentation describes + +## SDK Resource +stocks + +## Method +candles + +## Reproduction Code +```java +// Your minimal reproduction code here +``` + +## Expected Behavior +What should happen + +## Actual Behavior +What actually happens (include error messages) + +## SDK Version +1.0.0 + +## JDK Version +17+ + +## Additional Context +Found via BUG_FINDING.md [Area N] + +Location: `src/main/java/com/marketdata/sdk/SomeClass.java:LINE` +EOF +)" +``` + +**Option 2: Web Form** + +1. Go to [Create Bug Report](https://github.com/MarketDataApp/sdk-java/issues/new?template=bug.yml) +2. Fill out ALL fields with captured information +3. In "Additional Context", note: `Found via BUG_FINDING.md [Area N]` +4. Click "Submit new issue" + +> **The bug hunt is NOT complete until the GitHub issue URL exists.** Documenting bugs in markdown files, notes, or any other format is NOT a substitute for creating the actual issue. + +### Example Bug Report + +```markdown +**Resource**: stocks +**Method**: candles + +**Reproduction Code**: +try (var client = new MarketDataClient()) { + CsvResponse result = client.stocks().asCsv().candles( + StockCandlesRequest.builder(StockResolution.DAILY, "AAPL") + .from(LocalDate.parse("2024-01-02")) + .build()); + System.out.println(result.values().get(0).high()); // Throws +} + +**Expected**: Decode CSV row and access the high field without error +**Actual**: ParseError: required column 'high' missing from CSV response + +**SDK Version**: 1.0.0 +**JDK Version**: 17.0.10 + +**Additional Context**: Found via BUG_FINDING.md [Area 1 - Format Switching] +``` + +--- + +## Endpoint Checklists + +Use these checklists for systematic testing of each resource. + +### Stocks Resource + +| Method | Area 1 (Formats) | Area 2 (Collections) | Area 3 (Concurrent) | Area 4 (Dates) | Area 5 (Multi) | +|--------|------------------|----------------------|---------------------|----------------|----------------| +| `candles` | [ ] JSON [ ] CSV [ ] HTML | [ ] Empty [ ] Single | [ ] Auto-chunk | [ ] Formats [ ] Boundaries | [ ] Multi-symbol | +| `quote` | [ ] JSON [ ] CSV [ ] HTML | [ ] Empty | N/A | N/A | N/A | +| `quotes` | [ ] JSON [ ] CSV [ ] HTML | [ ] Empty | [ ] Headers | N/A | [ ] Empty [ ] Dupe [ ] Case | +| `prices` | [ ] JSON [ ] CSV [ ] HTML | [ ] Empty | [ ] Headers | N/A | [ ] Empty [ ] Dupe [ ] Case | +| `news` | [ ] JSON [ ] CSV [ ] HTML | [ ] Empty | N/A | [ ] Formats | [ ] Multi-symbol | +| `earnings` | [ ] JSON [ ] CSV [ ] HTML | [ ] Empty [ ] Optional | N/A | [ ] Formats | N/A | + +### Options Resource + +| Method | Area 1 (Formats) | Area 2 (Collections) | Area 3 (Concurrent) | Area 4 (Dates) | Area 5 (Multi) | +|--------|------------------|----------------------|---------------------|----------------|----------------| +| `lookup` | [ ] JSON [ ] CSV [ ] HTML | [ ] Empty | N/A | N/A | N/A | +| `expirations` | [ ] JSON [ ] CSV [ ] HTML | [ ] Empty | N/A | [ ] Formats | N/A | +| `strikes` | [ ] JSON [ ] CSV [ ] HTML | [ ] Empty | N/A | [ ] Formats | N/A | +| `quote` | [ ] JSON [ ] CSV [ ] HTML | [ ] Empty | N/A | N/A | N/A | +| `quotes` | [ ] JSON [ ] CSV [ ] HTML | [ ] Empty | [ ] Partial [ ] Headers | N/A | [ ] Multi-contract | +| `chain` | [ ] JSON [ ] CSV [ ] HTML | [ ] Empty | N/A | [ ] Formats | N/A | + +### Markets Resource + +| Method | Area 1 (Formats) | Area 2 (Collections) | Area 4 (Dates) | +|--------|------------------|----------------------|----------------| +| `status` | [ ] JSON [ ] CSV [ ] HTML | [ ] Empty | [ ] Formats | + +### Funds Resource + +| Method | Area 1 (Formats) | Area 2 (Collections) | Area 4 (Dates) | +|--------|------------------|----------------------|----------------| +| `candles` | [ ] JSON [ ] CSV [ ] HTML | [ ] Empty [ ] Single | [ ] Formats [ ] Boundaries | + +### Utilities Resource + +| Method | Area 1 (Formats) | Area 2 (Collections) | +|--------|------------------|----------------------| +| `status` | [ ] JSON | N/A | +| `headers` | [ ] JSON | N/A | + +--- + +## Quick Reference + +### Common Test Commands + +```bash +# Run a single exploration scenario via a scratch JUnit test +./gradlew test --tests '*Scratch*' + +# Run the full unit suite after finding a potential bug +./gradlew test + +# Integration tests hit the live API — gated by env var +MARKETDATA_RUN_INTEGRATION_TESTS=true ./gradlew integrationTest +``` + +> Tip: drop a `ScratchTest` in `src/test/java/com/marketdata/sdk/` with a single +> `@Test` method holding your exploration code, then run it with the `--tests '*Scratch*'` +> filter above. This keeps exploration inside the build (Spotless, classpath, JDK toolchain) +> rather than a standalone `main`. + +### Common Bug Indicators + +| Error Message | Likely Area | Likely Cause | +|---------------|-------------|--------------| +| `IndexOutOfBoundsException` | Area 2 | Empty `values()` list access | +| `NullPointerException` | Area 1/2 | Unhandled `@Nullable` field or null response | +| `com.marketdata.sdk.exception.ParseError` | Area 1 | CSV/HTML decode or required-column omission | +| `ClassCastException` | Area 1 | Value decoded to the wrong type | +| `IllegalArgumentException` | Area 5 | Request validation (empty/invalid symbols) | +| Data missing without error | Area 3 | Silent partial failure | +| Duplicate data | Area 3/5 | Chunk boundary or deduplication issue | + +### Links + +- [Bug Report Template](https://github.com/MarketDataApp/sdk-java/issues/new?template=bug.yml) +- [Issue Workflow (for processing bugs)](ISSUE_WORKFLOW.md) + +--- + +## Completion Checklist + +Before considering a bug hunt complete, verify: + +- [ ] All discovered bugs have been created as GitHub issues (not just documented) +- [ ] Each issue has a URL (e.g., `https://github.com/MarketDataApp/sdk-java/issues/123`) +- [ ] Each issue follows the bug template format +- [ ] Each issue includes `Found via BUG_FINDING.md [Area N]` in Additional Context + +**If you documented bugs but did not create GitHub issues, the bug hunt is NOT complete. Go back and create the issues now.** diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..b71677f --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: MarketDataApp diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000..0589a3a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,146 @@ +name: Bug Report +description: Report a bug in the Market Data Java & Kotlin SDK +title: "[Bug]: " +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Thanks for reporting a bug. Please fill out all required fields to help us reproduce and fix the issue. + + **Important:** The SDK returns data exactly as the API provides it. Before reporting, please verify the behavior you're seeing differs from what the [API documentation](https://www.marketdata.app/docs/api) describes. + + - type: checkboxes + id: api-docs-verified + attributes: + label: API Documentation Verification + description: Confirm you've checked the API documentation before reporting this as an SDK bug. + options: + - label: I have reviewed the [API documentation](https://www.marketdata.app/docs/api) for this endpoint + required: true + - label: The behavior I'm reporting differs from what the API documentation describes (not just unexpected to me) + required: true + + - type: dropdown + id: language + attributes: + label: Language + description: Which language are you calling the SDK from? + options: + - Java + - Kotlin + - Other JVM language + validations: + required: true + + - type: dropdown + id: endpoint + attributes: + label: SDK Resource + description: Which part of the SDK is affected? + options: + - stocks + - options + - markets + - funds + - utilities + - Client (general) + - Other + validations: + required: true + + - type: input + id: method + attributes: + label: Method + description: Which method are you calling? (e.g., candles, quote, chain) + placeholder: candles + validations: + required: true + + - type: textarea + id: reproduction + attributes: + label: Reproduction Code + description: Complete, runnable Java or Kotlin code that demonstrates the bug. Must be self-contained. + placeholder: | + try (var client = new MarketDataClient()) { + var resp = client.stocks().candles( + StockCandlesRequest.builder(StockResolution.DAILY, "AAPL") + .from(LocalDate.parse("2024-01-01")) + .build()); + // Bug: ... + } + render: java + validations: + required: true + + - type: textarea + id: expected + attributes: + label: Expected Behavior + description: What should happen? + placeholder: The method should return candle data for the specified date range. + validations: + required: true + + - type: textarea + id: actual + attributes: + label: Actual Behavior + description: What actually happens? Include any error messages and stack traces. + placeholder: | + com.marketdata.sdk.exception.ParseError: ... + at com.marketdata.sdk.... + validations: + required: true + + - type: input + id: sdk-version + attributes: + label: SDK Version + description: The `app.marketdata:marketdata-sdk-java` version declared in your build.gradle(.kts) or pom.xml + placeholder: "1.0.0" + validations: + required: true + + - type: input + id: jdk-version + attributes: + label: JDK Version + description: Run `java -version` to find this + placeholder: "17.0.10" + validations: + required: true + + - type: dropdown + id: build-tool + attributes: + label: Build Tool + options: + - Gradle (Kotlin DSL) + - Gradle (Groovy DSL) + - Maven + - Other + validations: + required: false + + - type: dropdown + id: os + attributes: + label: Operating System + multiple: true + options: + - macOS + - Windows + - Linux + validations: + required: false + + - type: textarea + id: notes + attributes: + label: Additional Context + description: Any other relevant information (config, workarounds tried, etc.) + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..80f280d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: false +contact_links: + - name: Ask a question + url: https://github.com/MarketDataApp/sdk-java/discussions/new?category=q-a + about: Ask the community for help + - name: Request a feature + url: https://github.com/MarketDataApp/sdk-java/discussions/new?category=ideas + about: Share ideas for new features + - name: Report a security issue + url: https://github.com/MarketDataApp/sdk-java/security/policy + about: Learn how to notify us for sensitive bugs diff --git a/.github/ISSUE_WORKFLOW.md b/.github/ISSUE_WORKFLOW.md new file mode 100644 index 0000000..710d9ae --- /dev/null +++ b/.github/ISSUE_WORKFLOW.md @@ -0,0 +1,423 @@ +# Issue Workflow + +This document defines the process for triaging and resolving bug reports. It is designed to be followed by maintainers (human or automated). + +## Overview + +``` +Verify Permissions → New Issue → Validate → [Valid] → Accept → Fix → Close + → [Needs Info] → Request Info → Wait 7 days → Close + → [Not a Bug] → Explain → Close +``` + +--- + +## Step 0: Verify Permissions + +Before processing issues, verify you have maintainer/contributor access to the repository. + +### Check Access Level + +```bash +gh api repos/MarketDataApp/sdk-java/collaborators/$( gh api user --jq '.login' )/permission --jq '.permission' +``` + +**Expected output for issue management:** +- `admin` - Full access ✓ +- `maintain` - Can manage issues ✓ +- `write` - Can manage issues ✓ +- `triage` - Can manage issues ✓ +- `read` - Cannot manage issues ✗ + +### If Permission Check Fails + +| Result | Meaning | Action | +|--------|---------|--------| +| `admin`, `maintain`, `write`, or `triage` | You have sufficient permissions | Proceed to Step 1 | +| `read` | Read-only access | Stop - request elevated permissions from a maintainer | +| Error: "404 Not Found" | Not a collaborator | Stop - you cannot manage issues | +| Error: "401 Unauthorized" | Not authenticated | Run `gh auth login` first | + +### Quick Verification + +```bash +# One-liner: exits 0 if you can manage issues, exits 1 if not +gh api repos/MarketDataApp/sdk-java/collaborators/$(gh api user --jq '.login')/permission --jq '.permission' | grep -qE '^(admin|maintain|write|triage)$' +``` + +--- + +## Step 1: Validate the Bug Report + +Run through this checklist for every new bug report. All items in the "Required" section must pass. + +### Required Criteria + +| # | Criterion | How to Check | Pass | Fail | +|---|-----------|--------------|------|------| +| 1 | **API docs verified** | Check "API Documentation Verification" checkboxes | Both boxes checked | One or both unchecked | +| 2 | **Has reproduction code** | Look for code block in "Reproduction Code" field | Contains a ```java (or ```kotlin) code block | Empty, pseudocode, or prose description only | +| 3 | **Code is complete** | Check for client instantiation and imports | Has `new MarketDataClient()` AND the necessary imports / try-with-resources | Missing client setup or imports | +| 4 | **Specifies SDK version** | Check "SDK Version" field | Version number present (e.g., `1.0.0`) | Empty or "latest" | +| 5 | **Specifies JDK version** | Check "JDK Version" field | Version number present (e.g., `17.0.10`) | Empty or vague (e.g., "17.x") | +| 6 | **Describes expected behavior** | Check "Expected Behavior" field | Clear statement of what should happen | Empty or unclear | +| 7 | **Describes actual behavior** | Check "Actual Behavior" field | Clear statement of what happens, ideally with error message | Empty or unclear | + +### Validation Decision + +- **All 7 criteria pass** → Proceed to Step 2 (Reproduce) +- **Any criterion fails** → Go to Step 4 (Request More Information) + +--- + +## Step 2: Reproduce the Bug + +Attempt to reproduce the reported behavior. + +### Reproduction Steps + +1. Create a small Java class (or JUnit test) with the provided reproduction code +2. Ensure you're using the reported SDK version: declare `app.marketdata:marketdata-sdk-java:X.Y.Z` in a Gradle/Maven test project +3. Ensure you're using the reported JDK version (or close to it) +4. Run the code with `./gradlew` +5. Compare output to the reported "Actual Behavior" + +### Reproduction Decision + +| Outcome | Next Step | +|---------|-----------| +| **Bug reproduces** - Output matches reported actual behavior | → Step 3A (Accept as Bug) | +| **Bug does not reproduce** - Code works correctly | → Step 3B (Cannot Reproduce) | +| **Different error occurs** - Code fails but differently than reported | → Step 4 (Request More Information) | +| **API error, not SDK error** - The API itself returns an error | → Step 3C (Not an SDK Bug) | +| **Expected API behavior** - The SDK correctly returns what the API provides | → Step 3C (Not an SDK Bug) | +| **User error in code** - The reproduction code has mistakes | → Step 3C (Not an SDK Bug) | + +--- + +## Step 3A: Accept as Bug + +The bug has been validated and reproduced. + +### Actions + +1. **Add label**: `accepted` +2. **Remove label**: `bug` (if you want to distinguish new from accepted, otherwise keep both) +3. **Comment** (use template below) +4. **Proceed to fixing** (see Step 5) + +### Comment Template: Accepted + +```markdown +Thanks for the detailed report. I've reproduced this issue. + +**Reproduction confirmed:** +- SDK version: [version] +- JDK version: [version] +- Behavior: [brief description of what you observed] + +Working on a fix. +``` + +--- + +## Step 3B: Cannot Reproduce + +The code runs without exhibiting the reported bug. + +### Actions + +1. **Add label**: `needs-info` +2. **Comment** (use template below) + +### Comment Template: Cannot Reproduce + +```markdown +I wasn't able to reproduce this issue with the information provided. + +**My environment:** +- SDK version: [version] +- JDK version: [version] +- OS: [os] + +**What I observed:** +[Describe what happened when you ran the code - it worked correctly, different output, etc.] + +Could you provide: +- [ ] Any additional configuration (custom settings, environment variables) +- [ ] The complete error output including stack trace +- [ ] Confirmation of your exact SDK and JDK versions (the `app.marketdata:marketdata-sdk-java` version from your build file and `java -version`) + +I'll keep this open for 7 days for additional information. +``` + +--- + +## Step 3C: Not an SDK Bug + +The issue is not a bug in the SDK. + +### Actions + +1. **Add label**: `wontfix` +2. **Comment** (use appropriate template below) +3. **Close issue** + +### Comment Template: API Issue (Not SDK) + +```markdown +Thanks for the report. After investigation, this appears to be related to the Market Data API itself rather than the Java & Kotlin SDK. + +**What's happening:** +[Explain the API behavior] + +**Suggested next steps:** +- Check the [API documentation](https://www.marketdata.app/docs/api) for this endpoint +- Contact Market Data support if you believe the API behavior is incorrect +- Join the [Discord](https://discord.com/invite/GmdeAVRtnT) for community help + +Closing this as it's outside the SDK's scope, but feel free to open a new issue if you find an SDK-specific problem. +``` + +### Comment Template: Expected API Behavior + +```markdown +Thanks for the report. After checking the [API documentation](https://www.marketdata.app/docs/api), this behavior is consistent with how the API is designed to work. + +**What you're seeing:** +[Describe the behavior] + +**API documentation reference:** +[Link to specific docs section or quote relevant documentation] + +The SDK returns data exactly as provided by the API. If you believe the API documentation is incorrect or the API should behave differently, please contact Market Data support or join the [Discord](https://discord.com/invite/GmdeAVRtnT). + +Closing this as working-as-designed. +``` + +### Comment Template: User Error + +~~~markdown +Thanks for the report. After reviewing the reproduction code, I found an issue with the implementation rather than a bug in the SDK. + +**The issue:** +[Explain what's wrong with their code] + +**Suggested fix:** +```java +// Show corrected code +``` + +**Documentation reference:** +[Link to relevant docs if applicable] + +Feel free to ask questions in [GitHub Discussions](https://github.com/MarketDataApp/sdk-java/discussions) if you need more help. Closing this issue, but you're welcome to reopen if you believe there's still an SDK bug. +~~~ + +### Comment Template: Works as Designed + +```markdown +Thanks for the report. After investigation, the SDK is behaving as designed here. + +**Expected behavior:** +[Explain why the current behavior is correct] + +**Documentation reference:** +[Link to docs explaining this behavior] + +If you'd like to suggest a change to this behavior, please open a feature request in [Discussions](https://github.com/MarketDataApp/sdk-java/discussions/new?category=ideas). +``` + +--- + +## Step 4: Request More Information + +The report is incomplete or unclear. + +### Actions + +1. **Add label**: `needs-info` +2. **Comment** specifying exactly what's needed (use template below) +3. **Set reminder**: Check back in 7 days + +### Comment Template: Needs Information + +```markdown +Thanks for the report. To investigate this issue, I need some additional information: + +[Select applicable items:] + +- [ ] **API documentation verification**: Please confirm you've checked the [API documentation](https://www.marketdata.app/docs/api) and that the behavior you're seeing differs from what's documented +- [ ] **Complete reproduction code**: Please provide a full, runnable Java class (or JUnit test) including the necessary imports and a `new MarketDataClient()` instantiation +- [ ] **SDK version**: Provide the `app.marketdata:marketdata-sdk-java` version from your build file +- [ ] **JDK version**: Run `java -version` and provide the version number +- [ ] **Expected behavior**: What did you expect to happen? +- [ ] **Actual behavior**: What actually happened? Please include the complete error message and stack trace if applicable +- [ ] **Additional context**: [Specify what else is needed] + +I'll keep this open for 7 days. If there's no response, I'll close it—but you're always welcome to reopen with the additional details. +``` + +### 7-Day Follow-up + +If no response after 7 days: + +1. **Comment** (use template below) +2. **Close issue** + +### Comment Template: Closing for Inactivity + +```markdown +Closing this issue due to inactivity. If you're able to provide the requested information, feel free to reopen or create a new issue with the additional details. +``` + +--- + +## Step 5: Fix the Bug + +Follow the standard bug-fixing process. + +### Fixing Checklist + +1. [ ] **Create failing test**: Write a JUnit (JUnit 5) test under `src/test/java` that reproduces the bug and verify it fails +2. [ ] **Implement fix**: Make the minimal code change to fix the issue +3. [ ] **Verify test passes**: Run the new test and confirm it passes +4. [ ] **Run full test suite**: `./gradlew test` - ensure no regressions (and, if the fix touches live-API behavior, `MARKETDATA_RUN_INTEGRATION_TESTS=true ./gradlew integrationTest`) +5. [ ] **Commit**: Use message format `fix: Description (closes #NNN)` +6. [ ] **Push**: Push the fix to the appropriate branch + +### Commit Message Format + +``` +fix: Brief description of what was fixed (closes #123) +``` + +Examples: +- `fix: Handle null in candles response decoder (closes #45)` +- `fix: Correct date parsing for earnings with timezone (closes #67)` + +--- + +## Step 6: Close the Issue + +After the fix is merged: + +1. **Verify auto-close**: GitHub should auto-close from the commit message `closes #NNN` +2. **If not auto-closed**: Manually close with a comment + +### Comment Template: Fixed + +~~~markdown +Fixed in [commit hash or PR link]. + +This will ship in the next release. If you need the fix immediately, you can build from source with `./gradlew publishToMavenLocal` and depend on the local snapshot, or wait for the Maven Central release. +~~~ + +--- + +## Labels Reference + +| Label | Meaning | When to Apply | +|-------|---------|---------------| +| `bug` | Default label from template | Applied automatically on new issues | +| `accepted` | Bug validated and reproduced | After successful reproduction | +| `needs-info` | Waiting for reporter input | When report is incomplete or cannot reproduce | +| `wontfix` | Not a bug / won't be fixed | When closing as not-a-bug | + +--- + +## CLI Commands Reference + +Common `gh` commands for issue management: + +```bash +# Add a label +gh issue edit NUMBER --add-label "accepted" +gh issue edit NUMBER --add-label "needs-info" + +# Remove a label +gh issue edit NUMBER --remove-label "bug" + +# Close an issue +gh issue close NUMBER + +# Reopen an issue +gh issue reopen NUMBER + +# Add a comment +gh issue comment NUMBER --body "Comment text here" + +# View issue details +gh issue view NUMBER + +# List open bugs +gh issue list --label "bug" +gh issue list --label "needs-info" +``` + +--- + +## Examples + +### Example A: Valid Bug Report + +**Issue #42:** +- Endpoint: `stocks` +- Method: `candles` +- Reproduction code: Complete Java class with imports and client instantiation +- Expected: Returns candle data +- Actual: `NullPointerException: Cannot invoke method on null` +- SDK Version: 1.0.0 +- JDK Version: 17.0.10 + +**Action**: Passes all criteria → Reproduce → If confirmed, accept and fix + +--- + +### Example B: Incomplete Report + +**Issue #43:** +- Endpoint: `options` +- Method: `chain` +- Reproduction code: "I called the options chain method and it broke" +- Expected: "It should work" +- Actual: "It doesn't work" +- SDK Version: (empty) +- JDK Version: 17.x + +**Action**: Fails criteria 2, 3, 4, 5, 6, 7 → Request more information with specific asks + +--- + +### Example C: Not a Bug (API Behavior) + +**Issue #44:** +- Endpoint: `stocks` +- Method: `quote` +- Reproduction code: Complete Java class +- Expected: "Should return after-hours price" +- Actual: "Returns regular session price" +- SDK Version: 1.0.0 +- JDK Version: 17.x.x + +**After investigation**: The API returns regular session prices by default; after-hours requires a different endpoint or parameter. + +**Action**: Close as "Not an SDK Bug" (API behavior) with explanation and pointer to docs + +--- + +### Example D: Expected API Behavior + +**Issue #45:** +- Endpoint: `stocks` +- Method: `earnings` +- Reproduction code: Complete Java class +- Expected: "Percentage values should be like 5.2 for 5.2%" +- Actual: "Returns 0.052 instead of 5.2" +- SDK Version: 1.0.0 +- JDK Version: 17.x.x +- API docs verified: Both checkboxes checked + +**After investigation**: The API documentation specifies that percentage fields are returned as decimal values (0.052 = 5.2%). The SDK correctly passes through the API response. + +**Action**: Close as "Expected API Behavior" with reference to API documentation diff --git a/.github/RELEASE_PROCESS.md b/.github/RELEASE_PROCESS.md new file mode 100644 index 0000000..7e521ed --- /dev/null +++ b/.github/RELEASE_PROCESS.md @@ -0,0 +1,139 @@ +# Java SDK Release Process + +This document defines the release process for `MarketDataApp/sdk-java`, including the pre-release workflow we use before cutting a tag. + +## 1. Scope + +Use this process for: +- patch releases (`vX.Y.Z`) +- minor releases (`vX.Y.0`) +- major releases (`vX.0.0`) + +## 2. Release Inputs + +Before starting, confirm: +- target release version `X.Y.Z` +- release tag format: `vX.Y.Z` +- release title format: `Version X.Y.Z` +- release owner +- included PRs/issues +- intended release date/time + +## 3. Pre-Release Workflow (Current) + +Our pre-release gate artifacts live in `release-readiness/` and are reviewed as a package before tag cut. + +Required gate docs: +- `release-readiness/01-api-contract.md` +- `release-readiness/02-quality-and-tests.md` +- `release-readiness/03-compatibility.md` +- `release-readiness/04-security.md` +- `release-readiness/05-docs-dx.md` +- `release-readiness/06-release-rollback.md` +- `release-readiness/final-go-no-go.md` + +Gate execution checklist: +1. API contract gate: + - Confirm intended API/signature changes and migration impact. + - Record pass/fail in `01-api-contract.md`. +2. Quality/test gate: + - `./gradlew build` (runs unit tests + Spotless formatting check + JaCoCo coverage). + - Forward-compat matrix: `./gradlew test -PtestJdk=<17|21|25>` for each target JDK. + - Integration tests against the live API: `MARKETDATA_RUN_INTEGRATION_TESTS=true ./gradlew integrationTest -PtestJdk=` (a valid `MARKETDATA_TOKEN` and network-enabled context required). + - Record evidence paths and pass/fail in `02-quality-and-tests.md`. +3. Compatibility gate: + - Confirm the `Main` workflow (`.github/workflows/main.yml`) is green for the release commit — it runs the full `{17, 21, 25}` unit + integration matrix on every push to `main`. + - Record results in `03-compatibility.md`. +4. Security gate: + - Review transitive dependencies (e.g. `./gradlew dependencies`) and confirm no unexpected runtime additions slipped in. + - Confirm token handling stays header-based (`Authorization: Bearer`, redacted in logs via the SDK's `Tokens` utility) and that TLS verification is never disabled. + - Record results in `04-security.md`. +5. Docs/DX gate: + - Verify `README.md`, `CHANGELOG.md`, `build.gradle.kts` (default version), and `docs/installation.md` version/support messaging align. + - Run the executable examples in `examples/consumer-test` (the `examples/common` and `examples/resources` sample apps, and the Kotlin `Quickstart.kt`). + - Record results in `05-docs-dx.md`. +6. Release/rollback gate: + - Confirm no open blockers. + - Update rollback path for a patch follow-up release. + - Record in `06-release-rollback.md`. +7. Final decision: + - Set `GO` or `NO-GO` in `final-go-no-go.md`. + - No tag is cut unless status is `GO` and P0 blockers are empty. + +## 4. Release Preparation + +1. Ensure `main` is current and CI is green. + +2. **Update version numbers** in the following files: + + | File | Location | Example | + |------|----------|---------| + | `README.md` | Title header | `# Market Data Java & Kotlin SDK v1.0` | + | `build.gradle.kts` | `version = ... ?: "X.Y.Z-SNAPSHOT"` default | `1.0.0-SNAPSHOT` | + | `docs/installation.md` | Gradle/Maven coordinates | `app.marketdata:marketdata-sdk-java:X.Y.Z` | + + > **Note**: The published artifact version is injected at publish time via `-PsdkVersion=X.Y.Z`. The version committed in `build.gradle.kts` intentionally stays a `-SNAPSHOT` default — it is overridden by the release and publish workflows, not by hand-editing for each release. + +3. **Update CHANGELOG.md** with final release notes (Keep a Changelog bracket format): + - Add a new `## [X.Y.Z] - YYYY-MM-DD` section (move items out of `## [Unreleased]`; do **not** use a `## vX.Y.Z` heading — the release workflow matches `## [X.Y.Z]`). + - Update the compare-link references at the bottom of the file (e.g. set `[Unreleased]` to `compare/vX.Y.Z...HEAD` and add a `[X.Y.Z]` link). + - Verify all breaking changes have migration guides. + - Ensure highlights, breaking changes, and migration notes are complete. + +4. Commit and push all changes to `main`. + +5. Confirm target tag does not already exist. + +> **Important**: The release workflow extracts release notes directly from CHANGELOG.md. +> The `## [X.Y.Z]` section must be present and complete before triggering the release. + +## 5. Publish Release + +One workflow drives the whole release. **Tag and Release** (`tag-and-release.yml`) runs the test gate, cuts the tag and GitHub Release, and then — unless you opt out — chains directly into the Maven Central publish. Each stage gates the next, so a red test or a non-green `main.yml` stops the release before anything is pushed. + +Go to Actions → "Tag and Release", click "Run workflow", and fill in: + - **version**: `X.Y.Z` (without `v` prefix) + - **ref**: `main` (or specific commit SHA) + - **prerelease**: `false` (unless it's a prerelease) + - **publish_to_central**: `true` (default — chain into Maven Central; set `false` to stop after the GitHub Release) + - **confirm**: `RELEASE` (exactly, to confirm) + +The run proceeds through three gated jobs: + +1. **`gate`** — JDK `{17, 21, 25}` matrix (`./gradlew build -PtestJdk= -PsdkVersion=X.Y.Z`). Must pass before anything else. +2. **`release`** — verifies the tag `vX.Y.Z` is new, extracts release notes from CHANGELOG.md (the `## [X.Y.Z]` section), creates the tag `vX.Y.Z` and the GitHub Release "Version X.Y.Z". +3. **`publish-central`** (only when `publish_to_central` is `true`) — calls `publish.yml`, which **independently re-checks that `main.yml` is green for the commit**, rebuilds + tests, then pushes the artifact (`app.marketdata:marketdata-sdk-java`) to Maven Central. + +> **Stopping before Central.** Set **publish_to_central** to `false` to cut only the tag + GitHub Release. You can then publish later by running the **Publish to Maven Central** workflow (`publish.yml`) directly: **version** = `X.Y.Z`, **release** = `true`. Running that workflow with **release** = `false` uploads to the Sonatype Portal and stops at `VALIDATED` for manual review — useful for inspecting the staged artifact before promoting it. + +## 6. Post-Release Checks + +1. Verify the GitHub Release was created with correct notes from CHANGELOG. +2. Confirm the artifact is visible on Maven Central (https://central.sonatype.com/artifact/app.marketdata/marketdata-sdk-java) — note that Central indexing can lag a few minutes after publish. +3. Smoke-test resolution in a clean project by adding the dependency and resolving it: + +```kotlin +// build.gradle.kts +dependencies { + implementation("app.marketdata:marketdata-sdk-java:X.Y.Z") +} +``` + +```xml + + + app.marketdata + marketdata-sdk-java + X.Y.Z + +``` + +## 7. Rollback and Hotfix + +If release issues are discovered: +1. Stop promotion messaging. +2. Publish corrective note in release/changelog. +3. Ship a patch release (`vX.Y.(Z+1)`) from `main` with targeted fix. +4. Document root cause and remediation in next changelog entry. + + diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..cf64ad3 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,19 @@ +# Please see the documentation for all configuration options: +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + labels: + - "dependencies" + + - package-ecosystem: "gradle" + directory: "/" + schedule: + interval: "weekly" + labels: + - "dependencies" diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml new file mode 100644 index 0000000..a4ebb3e --- /dev/null +++ b/.github/workflows/dependabot-auto-merge.yml @@ -0,0 +1,33 @@ +name: dependabot-auto-merge +on: pull_request_target + +permissions: + pull-requests: write + contents: write + +jobs: + dependabot: + runs-on: ubuntu-latest + timeout-minutes: 5 + if: ${{ github.actor == 'dependabot[bot]' }} + steps: + + - name: Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@v2.5.0 + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" + + - name: Auto-merge Dependabot PRs for semver-minor updates + if: ${{steps.metadata.outputs.update-type == 'version-update:semver-minor'}} + run: gh pr merge --auto --merge "$PR_URL" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + + - name: Auto-merge Dependabot PRs for semver-patch updates + if: ${{steps.metadata.outputs.update-type == 'version-update:semver-patch'}} + run: gh pr merge --auto --merge "$PR_URL" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 117aa58..145a15c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,17 +1,40 @@ name: Publish to Maven Central on: + # Manual: publish any version on demand. workflow_dispatch: inputs: version: - description: "Version to publish (e.g. 0.1.0, without the 'v')" + description: "Version to publish (e.g. 1.0.0, without the 'v')" required: true type: string + ref: + description: "Commit SHA to build & publish from (defaults to the launch commit)" + required: false + default: '' + type: string release: description: "Publish and release. Off = upload to the Portal and stop at VALIDATED for manual review." required: true type: boolean default: false + # Chained: invoked by tag-and-release.yml after a release is cut. + workflow_call: + inputs: + version: + description: "Version to publish (e.g. 1.0.0, without the 'v')" + required: true + type: string + ref: + description: "Commit SHA to build & publish from (defaults to the launch commit)" + required: false + default: '' + type: string + release: + description: "Publish and release. Off = upload to the Portal and stop at VALIDATED." + required: false + default: false + type: boolean jobs: guard: @@ -23,13 +46,17 @@ jobs: - name: Check latest main.yml conclusion env: GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + # Build/guard against the explicit commit when given (chained path); + # otherwise the commit the workflow was launched from. + SHA: ${{ inputs.ref || github.sha }} run: | conclusion=$(gh api \ - "repos/${{ github.repository }}/actions/workflows/main.yml/runs?head_sha=${{ github.sha }}&status=completed" \ + "repos/${REPO}/actions/workflows/main.yml/runs?head_sha=${SHA}&status=completed" \ --jq '.workflow_runs[0].conclusion // "missing"') - echo "main.yml conclusion for ${{ github.sha }}: $conclusion" + echo "main.yml conclusion for ${SHA}: $conclusion" if [ "$conclusion" != "success" ]; then - echo "::error::main.yml is not green for ${{ github.sha }} (got: $conclusion). Run publish from a commit on main whose CI passed." + echo "::error::main.yml is not green for ${SHA} (got: $conclusion). Run publish from a commit on main whose CI passed." exit 1 fi @@ -41,6 +68,7 @@ jobs: name: maven-central url: https://central.sonatype.com/artifact/app.marketdata/marketdata-sdk-java env: + VERSION: ${{ inputs.version }} ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }} ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_KEY }} @@ -48,6 +76,9 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + # Build from the explicit commit when chained; else the launch commit. + ref: ${{ inputs.ref || github.sha }} - name: Set up JDK 17 uses: actions/setup-java@v4 @@ -59,12 +90,12 @@ jobs: uses: gradle/actions/setup-gradle@v4 - name: Build and test - run: ./gradlew clean build -PsdkVersion=${{ inputs.version }} + run: ./gradlew clean build -PsdkVersion="$VERSION" - name: Upload to Portal (validate only) if: ${{ !inputs.release }} - run: ./gradlew publishToMavenCentral -PsdkVersion=${{ inputs.version }} + run: ./gradlew publishToMavenCentral -PsdkVersion="$VERSION" - name: Publish and release if: ${{ inputs.release }} - run: ./gradlew publishAndReleaseToMavenCentral -PsdkVersion=${{ inputs.version }} + run: ./gradlew publishAndReleaseToMavenCentral -PsdkVersion="$VERSION" diff --git a/.github/workflows/tag-and-release.yml b/.github/workflows/tag-and-release.yml new file mode 100644 index 0000000..33a10ad --- /dev/null +++ b/.github/workflows/tag-and-release.yml @@ -0,0 +1,202 @@ +name: Tag and Release + +# Cuts the git tag and GitHub Release for a version, then (by default) chains +# straight into the Maven Central publish. Each stage gates the next: the JDK +# matrix must pass before the tag is cut, and the publish step independently +# refuses to run unless main.yml is green for the commit. Set +# publish_to_central=false to stop after the GitHub Release and publish later. + +on: + workflow_dispatch: + inputs: + version: + description: "Release version without v prefix (example: 1.0.0)" + required: true + type: string + ref: + description: "Git ref to release (branch, tag, or commit SHA)" + required: false + default: "main" + type: string + prerelease: + description: "Mark GitHub Release as prerelease" + required: false + default: false + type: boolean + publish_to_central: + description: "Publish to Maven Central automatically after the release is cut" + required: false + default: true + type: boolean + confirm: + description: "Type RELEASE to confirm publish" + required: true + type: string + +permissions: + contents: write + actions: read + +concurrency: + group: release-${{ github.event.inputs.version }} + cancel-in-progress: false + +jobs: + gate: + name: Gate / JDK ${{ matrix.java }} + runs-on: ubuntu-latest + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + # ADR-002: tests run on JDK 17, 21, 25 to catch forward-compat + # regressions. Compilation is always pinned to --release 17. + java: ['17', '21', '25'] + + steps: + - name: Validate release request + if: matrix.java == '17' + shell: bash + env: + CONFIRM: ${{ inputs.confirm }} + VERSION: ${{ inputs.version }} + run: | + test "$CONFIRM" = "RELEASE" || { + echo "::error::confirm input must be exactly RELEASE" + exit 1 + } + [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]] || { + echo "::error::version must be semver-like (for example: 1.0.0)" + exit 1 + } + + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref }} + + # Mirror main.yml: install the matrix JDK (test execution) plus JDK 17 + # (compile + Gradle daemon runtime). Order matters — setup-java points + # JAVA_HOME at the LAST entry, so 17 must be last for matrix.java = 25. + - name: Set up JDKs (test=${{ matrix.java }}, compile/daemon=17) + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: | + ${{ matrix.java }} + 17 + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Build, test, lint, coverage + env: + VERSION: ${{ inputs.version }} + run: ./gradlew build -PtestJdk=${{ matrix.java }} -PsdkVersion="$VERSION" --stacktrace + + - name: Upload test reports on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: release-test-reports-jdk${{ matrix.java }} + path: | + build/reports/tests/ + build/test-results/ + retention-days: 14 + + release: + name: Tag and GitHub Release + runs-on: ubuntu-latest + needs: gate + outputs: + sha: ${{ steps.resolve.outputs.sha }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ inputs.ref }} + + # Resolve inputs.ref to a concrete commit SHA so the tag, the GitHub + # Release, and the chained Maven Central build all point at the SAME + # commit — even if the workflow was launched from a different branch. + - name: Resolve commit SHA + id: resolve + run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + + - name: Verify tag does not already exist + shell: bash + env: + VERSION: ${{ inputs.version }} + run: | + TAG="v${VERSION}" + if git ls-remote --exit-code --tags origin "refs/tags/${TAG}" >/dev/null 2>&1; then + echo "::error::Tag ${TAG} already exists on origin" + exit 1 + fi + + - name: Extract release notes from CHANGELOG + env: + VERSION: ${{ inputs.version }} + shell: bash + run: | + # Extract the section for this version from CHANGELOG.md. + # Keep a Changelog format: matches from "## [X.Y.Z]" until the next + # "## [" heading or the trailing "[x]: url" link-reference block. + awk -v ver="$VERSION" ' + $0 ~ "^## \\[" ver "\\]" { found=1; next } + found && /^## \[/ { exit } + found && /^\[[^]]+\]: / { exit } + found { print } + ' CHANGELOG.md > release-notes.md + + # Verify we extracted something + if [ ! -s release-notes.md ]; then + echo "::error::Could not extract release notes for v${VERSION} from CHANGELOG.md" + exit 1 + fi + + echo "=== Extracted release notes ===" + cat release-notes.md + + - name: Create and publish release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ inputs.version }} + TARGET_REF: ${{ steps.resolve.outputs.sha }} + IS_PRERELEASE: ${{ inputs.prerelease }} + shell: bash + run: | + TAG="v${VERSION}" + FLAGS="" + if [ "$IS_PRERELEASE" = "true" ]; then + FLAGS="${FLAGS} --prerelease" + fi + + gh release create "${TAG}" \ + --target "${TARGET_REF}" \ + --title "Version ${VERSION}" \ + --notes-file release-notes.md \ + ${FLAGS} + + - name: Note next step (chaining disabled) + if: ${{ ! inputs.publish_to_central }} + env: + VERSION: ${{ inputs.version }} + run: echo "::notice::Release v${VERSION} created. Chaining is off — run the 'Publish to Maven Central' workflow (publish.yml) for this commit to push the artifact." + + # Chained Maven Central publish. Reuses publish.yml so there is one source of + # truth for publishing; its guard job still independently requires main.yml to + # be green for this commit before anything is pushed. + publish-central: + name: Publish to Maven Central + needs: release + if: ${{ inputs.publish_to_central }} + uses: ./.github/workflows/publish.yml + with: + version: ${{ inputs.version }} + # Build & publish the exact commit that was tagged/released, not the + # workflow's launch commit (github.sha) — Maven Central is immutable. + ref: ${{ needs.release.outputs.sha }} + release: true + secrets: inherit diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e7af26..53c39be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,119 +7,56 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -### Fixed -- Default retry attempts corrected from 3 to 4 (one initial + three retries) to - match SDK requirements §9.3 ("max 3 retries, yielding 4 total attempts"). -- `.env` parser now strips trailing inline `# comment` markers (quote-aware: a - `#` inside single/double quotes or adjacent to value chars stays part of the - value). Previously a line like `MARKETDATA_TOKEN=abc # prod` produced the - literal value `abc # prod`, which passes `validateApiKey` (printable ASCII) - and surfaces later as a confusing `AuthenticationError` far from the .env - source that caused it. -- `RequestHeaders` canonical constructor now rejects a `null` `headers` map - with a clear `NullPointerException` naming the field, replacing the bare - `Map.copyOf(null)` NPE that left consumers hunting for the offending - argument. The wire-format deserializer additionally intercepts a top-level - JSON `null` body via `JsonDeserializer#getNullValue` and surfaces it as a - `ParseError` carrying the endpoint URL, status, and request id — preventing - a malformed `/headers/` response from manifesting as an opaque NPE further - down the call stack. +## [1.0.0] - 2026-06-29 + +First stable release of the Market Data Java & Kotlin SDK — a single JVM +artifact, idiomatic from both Java and Kotlin, covering the full v1 API surface +with sync + async parity on every endpoint. ### Added -- **Markets resource** (`client.markets()`) — the single markets endpoint, - `status`, in sync + async form: the exchange open/closed calendar ("was/is - the market open on these days?"), distinct from `utilities().status()` (the - API's own service health). Takes a Builder-based `MarketStatusRequest` where - *every* parameter is optional — a bare `of()` returns today's status, US - calendar; window is `date` xor `from`/`to` xor `to`+`countback`, plus - `country` (two-digit ISO 3166; the backend serves US today and answers - `no_data` for others). Rows are `MarketStatus(date, status)` with derived - `isOpen()`/`isClosed()` predicates; a `status` cell comes back null for days - outside the backend's holiday-calendar coverage and decodes to null (the - Option A column guarantee still applies to the column itself). Universal - params (`dateFormat`/`mode`/`limit`/`offset`/`columns`) as configured - copies, CSV facet via `asCsv()` with the `human`/`headers` shaping params, - and the same nullable-fields + `columns` + Option A decoding contract as - stocks/options. -- **Funds resource** (`client.funds()`) — the single funds endpoint, `candles`, - in sync + async form, taking a Builder-based `FundCandlesRequest` (window: - `date` xor `from`/`to`/`countback`). Fund candles are NAV series: OHLC only - (no volume column), daily-and-up resolutions only — `FundResolution` models - `DAILY`/`WEEKLY`/`MONTHLY`/`YEARLY` and `days/weeks/months/years(n)`, with no - intraday factories (the API rejects intraday tokens for funds) and therefore - no §12 auto-chunking. Universal params (`dateFormat`/`mode`/`limit`/`offset`/ - `columns`) as configured copies, CSV facet via `asCsv()` with the - `human`/`headers` shaping params, and the same nullable-fields + `columns` + - Option A decoding contract as stocks/options. -- **Stocks resource** (`client.stocks()`) — six endpoints, each in sync + async - form: `candles`, `quote` (single symbol), `quotes` and `prices` (multi-symbol, - batched into one request — one row per symbol, not a fan-out map like - `options.quotes`), `news`, and `earnings`. Every endpoint takes a Builder-based - per-endpoint request object. Candle resolution is a `StockResolution` value - type (`DAILY`, `minutes(15)`, `hours(1)`, …) rather than an enum, since the API - accepts an open-ended family of resolutions. Quote/price numeric fields are - nullable (the backend nulls them for a closed/illiquid market); the OHLC and - 52-week columns are opt-in via `candle` / `week52`. `news` exposes the feed's - scalar `updated()` off the response (distinct from each article's - `publicationDate`); `earnings` tolerates the nullable fundamentals/report fields - on synthesized forward-quarter rows. Mixed date/timestamp wire shapes (a daily - candle's date-only `t` vs. an intraday full timestamp) decode uniformly. Carries - the same universal-parameter setters, `columns` projection (with the Option A - strict guarantee), and `asCsv()` facet as `options`. Intraday candle requests - spanning more than ~one year are **auto-split** into year-sized sub-requests, - fetched concurrently through the 50-permit pool and merged into one response - (SDK requirements §12), on both the typed and CSV paths. -- **Per-response rate limits** — every `MarketDataResponse` now exposes - `rateLimit()` returning a `RateLimitSnapshot` parsed from that response's own - `x-api-ratelimit-*` headers (request-scoped, SDK requirements §8.2), distinct - from the client-level `MarketDataClient.getRateLimits()`. Applies to every - resource (options/utilities/stocks) and the CSV/HTML responses. -- **Options resource** (`client.options()`) — all six endpoints, each in sync + - async form: `lookup`, `expirations`, `strikes`, `quote` (single contract), - `quotes` (multi-contract fan-out returning a per-symbol - `Map`), and `chain`. Every endpoint takes a - Builder-based per-endpoint request object (no `String` convenience overloads) - and returns a named typed response (`OptionsChainResponse`, - `OptionsLookupResponse`, …) implementing `MarketDataResponse` — the payload - is reached via `values()`. The `chain` request models its mutually-exclusive - expiration and strike groups as sealed types (`ExpirationFilter`, - `StrikeFilter`) so the exclusivity is compiler-enforced. Covers the `rho` greek - (decoded as an optional, nullable column — absent on some feeds) plus the - `Greek` enum with `presentGreeks()` / `greek(Greek)` accessors, the - `expiration=all` filter (the full chain vs. the default front-month), and the - `countback` historical-window parameter (validated: positive, and mutually - exclusive with `date`/`from`). Universal parameters - (`dateFormat`/`mode`/`limit`/`offset`) and `columns` projection are set fluently - on the resource; a non-requested column decodes to `null`, while a required - column you *did* request that the API omits raises a `ParseError` (Option A). - The `asCsv()` facet returns CSV (`CsvResponse`) for every endpoint and adds the - output-shaping `human` / `headers` params. -- Project scaffold per ADRs 001–007: Gradle Kotlin DSL build, JDK 17 toolchain, - `integrationTest` source set, Spotless + JaCoCo, Vanniktech Maven Publish. -- `MarketDataClient` skeleton with two public constructors — a no-arg one - for production (everything resolved from the cascade) and a 4-arg one - (`apiKey`, `baseUrl`, `apiVersion`, `validateOnStartup`) for tests and - short-lived runtimes. Default base URL (`https://api.marketdata.app`), - default API version (`v1`), 99 s request / 2 s connect timeouts, HTTP/2, - demo mode, `validateOnStartup` toggle, and a 50-permit concurrency - semaphore (wiring lands with the request layer). -- Configuration cascade: explicit constructor parameters → `MARKETDATA_*` - environment variables → `.env` file in CWD → built-in defaults. -- Sealed `MarketDataException` hierarchy with the seven canonical subtypes + +- **`MarketDataClient`** — no-arg constructor (everything resolved from the + configuration cascade) and a 4-arg constructor for tests/short-lived runtimes. + Single shared `HttpClient` (HTTP/2), 99 s request / 2 s connect timeouts, + `close()` for resource release, and `getRateLimits()`. +- **Configuration cascade** — explicit constructor parameters → `MARKETDATA_*` + environment variables → `.env` in the working directory → built-in defaults. + `baseUrl` / `apiVersion` are normalized and validated at construction. +- **Demo mode** — without a token the client omits the `Authorization` header + and serves public, read-only endpoints; `validateOnStartup` verifies auth on + construction otherwise. +- **Options resource** (`client.options()`) — `lookup`, `expirations`, + `strikes`, `quote`, `quotes` (per-symbol fan-out map), and `chain` with the + full filter surface (sealed `ExpirationFilter` / `StrikeFilter`, greeks, + `expiration=all`, `countback`). +- **Stocks resource** (`client.stocks()`) — `candles`, `quote`, `quotes` and + `prices` (batched multi-symbol), `news`, and `earnings`. `StockResolution` + value type for open-ended resolutions; intraday windows over ~1 year are + auto-split, fetched concurrently, and merged. +- **Markets resource** (`client.markets()`) — `status`, the exchange + open/closed calendar with `date` / `from`-`to` / `countback` windowing and + `country` selection. +- **Funds resource** (`client.funds()`) — `candles`, NAV OHLC series at + daily-and-up resolutions (`FundResolution`). +- **Utilities resource** (`client.utilities()`) — service `status` and auth + validation. +- **Universal parameters & formats** — `dateFormat` / `mode` / `limit` / + `offset` / `columns` set fluently on every resource (immutable configured + copies), plus an `asCsv()` facet (with `human` / `headers` shaping) for every + endpoint. `columns` projection enforces the Option A strict-decoding contract. +- **Reliability** — retry with exponential backoff (4 total attempts, + `Retry-After` honored), client-level and per-response rate-limit snapshots + with preflight, `/status/` stale-while-revalidate cache gating retries, and a + 50-permit async concurrency pool. +- **Sealed `MarketDataException` hierarchy** — seven canonical subtypes (`AuthenticationError`, `BadRequestError`, `NotFoundError`, `RateLimitError`, `ServerError`, `NetworkError`, `ParseError`), each carrying support context - (`requestId`, `requestUrl`, `statusCode`, `timestamp`) and a - `getSupportInfo()` helper. -- `RateLimitSnapshot` record exposed via `MarketDataClient.getRateLimits()`. -- JSpecify `@NullMarked` on every public package; JSpecify on `compileOnlyApi` - so consumers get the annotations at compile time without a runtime dep. -- Token redaction utility (`Tokens`, package-private in the SDK root) for - log output. -- MIT license; SDK version auto-detected from the JAR manifest - (`Implementation-Version`). -- Single-package architecture per ADR-007: every infra class - (`Configuration`, `EnvVars`, `Tokens`, `Version`) lives in - `com.marketdata.sdk` as package-private. The `internal/` subpackage - was removed; the consumer's compiler cannot reference these types, - closing the "internal type leaks via constructor signature" gap that - every non-modular Java SDK has. + (`requestId`, `requestUrl`, `statusCode`, `timestamp`) and `getSupportInfo()`. +- **First-class Kotlin interop** — `@NullMarked` (JSpecify) on every public + package, no Kotlin-stdlib or coroutines runtime dependency, SAM callbacks, and + Kotlin-reserved-word-free public API. +- **Packaging** — MIT license, SemVer, version auto-detected from the JAR + manifest, published to Maven Central as `app.marketdata:marketdata-sdk-java`. + +[Unreleased]: https://github.com/MarketDataApp/sdk-java/compare/v1.0.0...HEAD +[1.0.0]: https://github.com/MarketDataApp/sdk-java/releases/tag/v1.0.0 diff --git a/README.md b/README.md index 48be050..a8e3d71 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,39 @@ -# Market Data Java SDK +
-Java SDK for the [Market Data API](https://www.marketdata.app/). **Pre-release** -— the `utilities`, `options`, and `stocks` resources are implemented; `funds` -and `markets` are forthcoming. The build, package layout, configuration cascade, -exception taxonomy, and Kotlin-interop foundations are in place. +# Market Data Java & Kotlin SDK v1.0 +### Access Financial Data with Ease + +> This is the official JVM SDK for [Market Data](https://www.marketdata.app/), built for **Java and Kotlin** alike. It provides developers with a powerful, easy-to-use interface to obtain real-time and historical financial data. Ideal for building financial applications, trading bots, and investment strategies. + +[![Tests](https://github.com/MarketDataApp/sdk-java/actions/workflows/main.yml/badge.svg)](https://github.com/MarketDataApp/sdk-java/actions/workflows/main.yml) +[![Coverage](https://codecov.io/gh/MarketDataApp/sdk-java/graph/badge.svg)](https://codecov.io/gh/MarketDataApp/sdk-java) +[![License](https://img.shields.io/github/license/MarketDataApp/sdk-java.svg)](https://github.com/MarketDataApp/sdk-java/blob/main/LICENSE) +[![Maven Central](https://img.shields.io/maven-central/v/app.marketdata/marketdata-sdk-java)](https://central.sonatype.com/artifact/app.marketdata/marketdata-sdk-java) +[![Java](https://img.shields.io/badge/Java-17%20%7C%2021%20%7C%2025-007396.svg?logo=openjdk&logoColor=white)](https://adoptium.net/) +[![Kotlin](https://img.shields.io/badge/Kotlin-first--class-7F52FF.svg?logo=kotlin&logoColor=white)](https://kotlinlang.org/) + +#### Connect With The Market Data Community + +[![Website](https://img.shields.io/badge/Website-marketdata.app-blue)](https://www.marketdata.app/) +[![Discord](https://img.shields.io/badge/Discord-join%20chat-7389D8.svg?logo=discord&logoColor=ffffff)](https://discord.com/invite/GmdeAVRtnT) +[![Twitter](https://img.shields.io/twitter/follow/MarketDataApp?style=social)](https://twitter.com/MarketDataApp) +[![Helpdesk](https://img.shields.io/badge/Support-Ticketing-ff69b4.svg?logo=TicketTailor&logoColor=white)](https://www.marketdata.app/dashboard/) + +
+ +## Features + +- **First-Class Kotlin Support**: Every public type is `@NullMarked` (JSpecify), so Kotlin sees real nullable / non-null types instead of platform types — no Kotlin artifact, no coroutines dependency, idiomatic from both languages +- **Sync & Async Parity**: Every endpoint offers a blocking method and a `CompletableFuture` variant, bridgeable to Kotlin coroutines via `await()` +- **Real-time Stock Data**: Prices, quotes, candles (OHLCV), earnings, and news +- **Options Trading Data**: Complete options chains, expirations, strikes, quotes, and lookup +- **Mutual Funds**: Historical candles and pricing data +- **Market Status**: Real-time market open/closed status for multiple countries +- **Multiple Output Formats**: Typed objects, JSON, CSV, or HTML +- **Built-in Retry Logic**: Automatic retry with exponential backoff for reliable data fetching +- **Rate Limit Tracking**: Per-response and client-level rate-limit snapshots +- **Type-Safe**: Records, a sealed exception hierarchy, and builder-based request objects +- **Zero Config**: Works out of the box with sensible defaults ## Requirements @@ -12,17 +42,15 @@ exception taxonomy, and Kotlin-interop foundations are in place. - **Jackson 2.18+** on the runtime classpath. Pulled transitively; consumers may align to a newer 2.x. -## Install (planned) +## Install ```kotlin // build.gradle.kts dependencies { - implementation("app.marketdata:marketdata-sdk-java:0.1.0") + implementation("app.marketdata:marketdata-sdk-java:1.0.0") } ``` -Coordinates are placeholders until the first publication to Maven Central. - ## Quick start The SDK reads `MARKETDATA_TOKEN` from the environment by default, so the @@ -285,8 +313,9 @@ hierarchy and carry support context (`requestId`, `requestUrl`, tickets: ```java -try { - // call endpoint method (forthcoming) +try (var client = new MarketDataClient()) { + var resp = client.stocks().quote(StockQuotesRequest.of("AAPL")); + System.out.println(resp.values()); } catch (RateLimitError e) { System.err.println(e.getSupportInfo()); } diff --git a/build.gradle.kts b/build.gradle.kts index a5573d2..f6aa6f9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,9 +12,9 @@ plugins { group = "app.marketdata" // Version is overridable from the command line so a manual Central Portal -// validation run can use a real release version (e.g. `-PsdkVersion=0.1.0`) +// validation run can use a real release version (e.g. `-PsdkVersion=1.0.0`) // without committing it. Default stays on the in-development SNAPSHOT. -version = (findProperty("sdkVersion") as String?) ?: "0.1.0-SNAPSHOT" +version = (findProperty("sdkVersion") as String?) ?: "1.0.0-SNAPSHOT" // ADR-002: minimum JDK 17, build with --release 17, single bytecode level. java { @@ -199,9 +199,12 @@ mavenPublishing { coordinates(group.toString(), "marketdata-sdk-java", version.toString()) pom { - name.set("Market Data Java SDK") - description.set("Java SDK for the Market Data API.") - url.set("https://github.com/MarketDataApp/sdk-java") + name.set("Market Data Java & Kotlin SDK") + description.set( + "Official Java & Kotlin SDK for the Market Data API — real-time and " + + "historical financial data: stocks, options, mutual funds, and market status." + ) + url.set("https://www.marketdata.app/docs/sdk/java") inceptionYear.set("2026") licenses { @@ -223,5 +226,9 @@ mavenPublishing { connection.set("scm:git:git://github.com/MarketDataApp/sdk-java.git") developerConnection.set("scm:git:ssh://git@github.com/MarketDataApp/sdk-java.git") } + issueManagement { + system.set("GitHub") + url.set("https://github.com/MarketDataApp/sdk-java/issues") + } } } diff --git a/docs/installation.md b/docs/installation.md index 50d6852..bda0107 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -16,7 +16,7 @@ Add the SDK as a dependency. The published artifact is a single JAR; it does **n ```kotlin // build.gradle.kts dependencies { - implementation("app.marketdata:marketdata-sdk-java:0.1.0") + implementation("app.marketdata:marketdata-sdk-java:1.0.0") } ``` @@ -25,7 +25,7 @@ dependencies { ```groovy // build.gradle dependencies { - implementation 'app.marketdata:marketdata-sdk-java:0.1.0' + implementation 'app.marketdata:marketdata-sdk-java:1.0.0' } ``` @@ -35,7 +35,7 @@ dependencies { app.marketdata marketdata-sdk-java - 0.1.0 + 1.0.0 ``` diff --git a/examples/consumer-test/build.gradle.kts b/examples/consumer-test/build.gradle.kts index 1bd71b9..0ff5d62 100644 --- a/examples/consumer-test/build.gradle.kts +++ b/examples/consumer-test/build.gradle.kts @@ -14,7 +14,7 @@ kotlin { } dependencies { - implementation("app.marketdata:marketdata-sdk-java:0.1.0-SNAPSHOT") + implementation("app.marketdata:marketdata-sdk-java:1.0.0-SNAPSHOT") } // Default `./gradlew run` lands on the stocks resource example (live API).