Skip to content

Commit 65dce14

Browse files
Merge remote-tracking branch 'origin/main' into 13_market_resource
# Conflicts: # CHANGELOG.md # CLAUDE.md # Makefile # examples/consumer-test/build.gradle.kts # examples/consumer-test/src/main/java/com/marketdata/consumer/QuickstartApp.java # src/main/java/com/marketdata/sdk/MarketDataClient.java
2 parents 6ce83ed + 4e31afb commit 65dce14

22 files changed

Lines changed: 1754 additions & 7 deletions

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4141
copies, CSV facet via `asCsv()` with the `human`/`headers` shaping params,
4242
and the same nullable-fields + `columns` + Option A decoding contract as
4343
stocks/options.
44+
- **Funds resource** (`client.funds()`) — the single funds endpoint, `candles`,
45+
in sync + async form, taking a Builder-based `FundCandlesRequest` (window:
46+
`date` xor `from`/`to`/`countback`). Fund candles are NAV series: OHLC only
47+
(no volume column), daily-and-up resolutions only — `FundResolution` models
48+
`DAILY`/`WEEKLY`/`MONTHLY`/`YEARLY` and `days/weeks/months/years(n)`, with no
49+
intraday factories (the API rejects intraday tokens for funds) and therefore
50+
no §12 auto-chunking. Universal params (`dateFormat`/`mode`/`limit`/`offset`/
51+
`columns`) as configured copies, CSV facet via `asCsv()` with the
52+
`human`/`headers` shaping params, and the same nullable-fields + `columns` +
53+
Option A decoding contract as stocks/options.
4454
- **Stocks resource** (`client.stocks()`) — six endpoints, each in sync + async
4555
form: `candles`, `quote` (single symbol), `quotes` and `prices` (multi-symbol,
4656
batched into one request — one row per symbol, not a fan-out map like

CLAUDE.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
44

55
## Repository state
66

7-
This repo contains a working Java SDK in active development on branch `clean-architecture-restart`. Gradle 9.0 (Kotlin DSL) build per ADR-003; ~48 main + ~28 test source files; full CI matrix per ADR-002. The transport, retry, rate-limit, status-cache, and exception layers are wired; the `utilities`, `options`, `stocks`, and `markets` resource façades are implemented today — the funds resource lands on its own parallel branch (`12_funds_resource`; see "Deliberately deferred" below).
7+
This repo contains a working Java SDK in active development on branch `clean-architecture-restart`. Gradle 9.0 (Kotlin DSL) build per ADR-003; ~48 main + ~28 test source files; full CI matrix per ADR-002. The transport, retry, rate-limit, status-cache, and exception layers are wired; the `utilities`, `options`, `stocks`, `markets`, and `funds` resource façades are all implemented today — the per-resource series for v1 is complete.
88

99
Sibling repo: `../api/` is the backend (Python/Django). The Python SDK lives at `../../sdk-py/` (referenced from ADRs). The cross-language `sdk-requirements.md` is referenced as `../sdk-requirements.md` from inside `docs/`; it is canonical but not committed in this repo.
1010

@@ -83,8 +83,8 @@ The Java SDK must also satisfy the canonical, cross-language [SDK Requirements](
8383
- Coverage ratchet lives in `codecov.yml`: project status with `target: auto, threshold: 5%` (cannot drop >5 pp vs base branch) plus a patch-coverage requirement of 70 % on new code. Requires a `CODECOV_TOKEN` repo secret — without it the upload step fails because workflows pass `fail_ci_if_error: true`.
8484

8585
**Deliberately deferred (require the per-resource layer to land first):**
86-
- §1.2 resource groupings `client.utilities()`, `client.options()`, `client.stocks()`, and `client.markets()` are wired today; `client.funds` lands on its own parallel branch (`12_funds_resource`).
87-
- §2 endpoint method coverage; §3 universal parameters; §11 wire-format decoding for the remaining resource (funds, on its own branch). The plumbing (`ParallelArrays.zip` helper for the parallel-arrays shape, `JsonResponseParser`, the `MarketDataResponse<T>` named-response types) is in place — each new endpoint just declares its fields and row builder.
86+
- ~~§1.2 resource groupings~~ **DONE** `client.utilities()`, `client.options()`, `client.stocks()`, `client.markets()`, and `client.funds()` are all wired today; the per-resource series for v1 is complete.
87+
- ~~§2 endpoint method coverage; §3 universal parameters; §11 wire-format decoding~~ **DONE** for all resources. The plumbing (`ParallelArrays.zip` helper for the parallel-arrays shape, `JsonResponseParser`, the `MarketDataResponse<T>` named-response types) is in place — each new endpoint just declares its fields and row builder.
8888
- ~~§8 request-scoped rate-limit attachment~~ **DONE** — every `MarketDataResponse` now exposes `rateLimit()`, parsed from that response's own `x-api-ratelimit-*` headers (request-scoped), alongside the client-level `client.getRateLimits()`.
8989
- §13 100% coverage threshold via JaCoCo `violationRules`; deferred until the resource layer lands so the threshold meaningfully ratchets functional code, not just scaffolding.
9090

Makefile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@ demo-stocks: ## Full stocks surface: every endpoint + all params, CSV facet, col
9999
demo-markets: ## Full markets surface: status + all params (open/closed calendar, null cells), CSV facet, columns, Option A (needs mock-server)
100100
cd $(CONSUMER_DIR) && ./gradlew runMarkets
101101

102+
.PHONY: demo-funds
103+
demo-funds: ## Full funds surface: candles + all params (no volume/intraday/chunking), CSV facet, columns, Option A (needs mock-server)
104+
cd $(CONSUMER_DIR) && ./gradlew runFunds
105+
102106
.PHONY: demos-all
103107
demos-all: ## Run every mock-server-based demo back-to-back (needs mock-server)
104-
cd $(CONSUMER_DIR) && ./gradlew runDemoConfig runExceptions runRetry runResponse runConcurrency runOptions runStocks runMarkets
108+
cd $(CONSUMER_DIR) && ./gradlew runDemoConfig runExceptions runRetry runResponse runConcurrency runOptions runStocks runMarkets runFunds

docs/FUNDS_REVIEW_GUIDE.md

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
# Funds Review Guide — `12_funds_resource`
2+
3+
This guide walks a reviewer through the `funds` resource added on the `12_funds_resource` branch. It is organized by **flow**, not by file.
4+
5+
This PR is the smallest of the resource series: it **adopts the conventions the [`options`](OPTIONS_REVIEW_GUIDE.md) and [`stocks`](STOCKS_REVIEW_GUIDE.md) PRs established**`MarketDataResponse<T>` + named responses, the Builder-based per-endpoint request, nullable fields + `columns` + Option A, the CSV/HTML facets — and applies them to the single funds endpoint, `GET /v1/funds/candles/{resolution}/{symbol}/`. **No shared-layer changes at all**: transport, retry, rate-limit parsing, `ParallelArrays`, `JsonResponseParser`, `MarketDataDates`, and `RequestConfig` are reused untouched.
6+
7+
If you reviewed the stocks PR, the only genuinely new content is what funds *don't* have (§3) and the parameter surface choices (§4). ~10 minutes.
8+
9+
Suggested reading order: §1 (what's here) → §3 (the three deliberate absences) → §4 (request + query translation) → §5 (deserializer). `file:line` citations target `HEAD` on this branch.
10+
11+
## Table of contents
12+
13+
- [Running it locally](#running-it-locally)
14+
1. [What this PR adds](#1-what-this-pr-adds)
15+
2. [The response model (reused)](#2-the-response-model-reused)
16+
3. [What funds deliberately do NOT have](#3-what-funds-deliberately-do-not-have)
17+
4. [Request → query translation](#4-request--query-translation)
18+
5. [The row deserializer: nullable + columns + Option A](#5-the-row-deserializer-nullable--columns--option-a)
19+
6. [Universal parameters + the CSV/HTML facets](#6-universal-parameters--the-csvhtml-facets)
20+
- [Reviewer checklist](#reviewer-checklist)
21+
22+
---
23+
24+
## Running it locally
25+
26+
```bash
27+
make build # unit tests + Spotless + JaCoCo (JDK 17)
28+
29+
# Integration tests hit the live API (gated). A token in .env or the env is required:
30+
MARKETDATA_RUN_INTEGRATION_TESTS=true ./gradlew integrationTest
31+
32+
# Full funds demo against the mock server (all params, CSV facet, columns projection,
33+
# Option A, the no-chunking proof). Needs the mock server up:
34+
make publish && make mock-server # (in one terminal)
35+
make demo-funds # (in another) — or: ./gradlew -p examples/consumer-test runFunds
36+
```
37+
38+
`FundsIntegrationTest` (shape assertions, VFINX) runs against `api.marketdata.app`. `FundsApp` (`examples/consumer-test`) scripts the mock server's responses to demonstrate every scenario — it was run green end-to-end (`make demo-funds`).
39+
40+
---
41+
42+
## 1. What this PR adds
43+
44+
### 1.1 Public API surface (new)
45+
46+
```
47+
com.marketdata.sdk.FundsResource (returned from client.funds())
48+
com.marketdata.sdk.FundsCsvResource (returned from .asCsv())
49+
com.marketdata.sdk.FundCandlesResponse (named response)
50+
51+
com.marketdata.sdk.funds.FundCandlesRequest (Builder-based request)
52+
com.marketdata.sdk.funds.FundCandle (row record: time/open/high/low/close)
53+
com.marketdata.sdk.funds.FundResolution (candle-resolution value type, daily-and-up only)
54+
```
55+
56+
Same packaging rules as options/stocks (ADR-007): façades `public final` with package-private constructors in the **root** package; request/row records in the public `com.marketdata.sdk.funds` subpackage (`@NullMarked` via `package-info.java`); `FundsHtmlResource` built but package-private (`asHtml()` stays hidden until the backend serves HTML).
57+
58+
### 1.2 Files to review, by role
59+
60+
| Area | Files | What to check |
61+
|---|---|---|
62+
| Resource façade | `FundsResource.java` | universal-param config, `candlesSpec`, the row deserializer + Option A, `asCsv()`/`asHtml()` |
63+
| CSV/HTML facets | `FundsCsvResource.java`, `FundsHtmlResource.java` | reuse of the static `candlesSpec`, `format=csv/html`, **no chunking path** |
64+
| Response | `FundCandlesResponse.java` | thin subclass of `AbstractMarketDataResponse<List<FundCandle>>` |
65+
| Requests | `funds/FundCandlesRequest.java`, `FundRequests.java`, `FundResolution.java` | Builder validation, window rules, the daily-and-up resolution type |
66+
| Row record | `funds/FundCandle.java` | `@Nullable` fields; **no volume** |
67+
| Wiring | `MarketDataClient.java` | `client.funds()` |
68+
| Demos | `examples/.../FundsApp.java`, `QuickstartApp.java` | mock-server walk-through; quickstart section enabled |
69+
70+
---
71+
72+
## 2. The response model (reused)
73+
74+
No new model concepts. `FundCandlesResponse` is a thin subclass of `AbstractMarketDataResponse<T>`; `values()` is `List<FundCandle>`. Per-response `rateLimit()` (§8.2) and the full `MarketDataResponse` surface come from the base for free.
75+
76+
---
77+
78+
## 3. What funds deliberately do NOT have
79+
80+
These are the load-bearing review points — each is a *contract* fact, verified against the backend (`api/marketDataApi/funds/`) and the Python SDK:
81+
82+
1. **No volume column.** Funds are NAV series; the backend never emits `v` (nor `vwap`/`n`). `FundCandle` is OHLC-only and `CANDLE_FIELDS` is `t,o,h,l,c`. A stocks-style candle body with `v` would still decode (unknown root fields are ignored by `ParallelArrays.zip`).
83+
2. **No intraday resolutions.** The backend rejects minutely/hourly tokens with `"Intraday resolutions are not available for fund candles."` — so `FundResolution` offers only `DAILY`/`WEEKLY`/`MONTHLY`/`YEARLY` + `days/weeks/months/years(n)` factories (no `minutes`/`hours`, no `isIntraday()`). `of(String)` still passes arbitrary tokens through (value-type philosophy: client-side validation doesn't chase the server's grammar); a hand-built intraday token surfaces as the API's error envelope → `ParseError`.
84+
3. **No §12 auto-chunking.** The ~one-year span cap that chunking works around only applies to intraday candle requests. A multi-decade daily funds request is **one** HTTP request, on both the typed resource and the CSV facet (which is why `FundsCsvResource` has no `mergeCsvBodies` analogue). Test: `candlesLongDailyRangeIsASingleRequest`.
85+
4. **No `extended` parameter.** Extended-hours sessions only exist intraday; exposing the flag would be dead surface. (The backend's OpenAPI schema lists it because funds reuse the stock-candles schema helper, but it can never have an effect.)
86+
87+
Python-SDK parity note: `sdk-py` exposes `symbol/resolution/from/to/countback` only. This PR additionally exposes `date` (a window shape the backend honors). It does **not** expose `exchange`, `country`, `adjustsplits`, `adjustdividends`: although the funds OpenAPI schema declares them (it reuses the shared candles serializer), the funds handler ignores `exchange`/`country` entirely and `adjustdividends` is commented out — only `adjustsplits` is read, and `sdk-py` does not surface it either, so all four were dropped for cross-language parity.
88+
89+
---
90+
91+
## 4. Request → query translation
92+
93+
One spec builder, `FundsResource.candlesSpec` (package-private static, reused by both facets):
94+
95+
| Endpoint | Path | Params |
96+
|---|---|---|
97+
| `candlesSpec` | `funds/candles/{resolution}/{symbol}` | `date`/`from`/`to`/`countback` |
98+
99+
What to verify:
100+
- Path segments encoded via `PathSegments.encode` (resolution token too); dates ISO-formatted (`2025-01-17`).
101+
- Window rules in `FundRequests.validateWindow` (same as stocks): `date` mutually exclusive with `from`/`to`/`countback`; `countback` positive, pairs with `to` not `from`.
102+
- `FundCandlesRequest.Builder` rejects empty symbol; `of(resolution, symbol)` is the no-optionals shortcut.
103+
104+
---
105+
106+
## 5. The row deserializer: nullable + columns + Option A
107+
108+
Identical mechanics to stocks (see [Stocks guide §5](STOCKS_REVIEW_GUIDE.md#5-the-row-deserializers-nullable--columns--option-a)): the `rowsDeserializer`/`validateRequestedColumns` pair is **copied per resource** (the agreed pre-v1 dedup refactor will fold these together with the universal-param setters).
109+
110+
- `CANDLE_FIELDS = [t, o, h, l, c]` — all five are **required** (any may be projected away via `columns`).
111+
- `t` decodes through the tolerant `MarketDataDates.parseDateOrTimestampField` — under `dateformat=timestamp` the daily `t` comes back date-only (`"2025-01-17"`) and is lifted to market-zone midnight (`America/New_York`).
112+
- Envelope handling via `ParallelArrays.zip`: `"s":"error"``ParseError` carrying `errmsg`; `"s":"no_data"` → empty `values()`.
113+
- Option A: a *requested* (or not-projected-away) column the API omitted → `ParseError`, never a silent null.
114+
115+
The wire module registers under the name `marketdata-funds` in `FundsResource`'s client-facing constructor — same once-per-client pattern as the other resources (each registers its own `SimpleModule` on the shared `JsonResponseParser`).
116+
117+
---
118+
119+
## 6. Universal parameters + the CSV/HTML facets
120+
121+
Same shape as stocks/options: `dateFormat`/`mode`/`limit`/`offset`/`columns` return configured copies of `FundsResource` ("configure once, call many"; the config carries into `asCsv()`); the CSV facet adds the output-shaping `human`/`headers`. The known copy-paste of these setters across resources is tracked tech debt for the pre-v1 self-typed-base refactor — do not review it as accidental duplication.
122+
123+
---
124+
125+
## Reviewer checklist
126+
127+
- [ ] `client.funds().candles(...)` hits `GET /v1/funds/candles/{resolution}/{symbol}/` with every param translated (unit: `FundsResourceTest.candlesAttachesAllParams`, `candlesAttachesDateAndCountbackWindows`)
128+
- [ ] `FundCandle` is OHLC-only (no volume) and all five wire columns are required under Option A
129+
- [ ] `FundResolution` models daily-and-up only; no chunking anywhere (`candlesLongDailyRangeIsASingleRequest`)
130+
- [ ] Sync + async parity (`candles` / `candlesAsync`) via `transport.joinSync` (ADR-006)
131+
- [ ] CSV facet sends `format=csv` + shaping params; HTML facet stays package-private
132+
- [ ] `no_data` → empty list; error envelope → `ParseError` with `errmsg`
133+
- [ ] Per-response `rateLimit()` populated from the four `x-api-ratelimit-*` headers
134+
- [ ] `MarketDataClient` wires `funds()` like the other resources (constructed before `StatusCache`, inside the partial-construction guard)
135+
- [ ] Demos: `make demo-funds` green; `QuickstartApp` funds section enabled
136+
- [ ] Integration: `FundsIntegrationTest` (VFINX, shape assertions) passes with a token

examples/consumer-test/build.gradle.kts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@ val demoApps = mapOf(
4141
"runStocks" to ("com.marketdata.consumer.StocksApp" to
4242
"Full stocks surface: candles/quote/quotes/prices/news/earnings + all params, CSV facet, columns projection, Option A. Needs mock server."),
4343
"runMarkets" to ("com.marketdata.consumer.MarketsApp" to
44-
"Full markets surface: status + all params (open/closed calendar, null cells), CSV facet, columns projection, Option A. Needs mock server.")
44+
"Full markets surface: status + all params (open/closed calendar, null cells), CSV facet, columns projection, Option A. Needs mock server."),
45+
"runFunds" to ("com.marketdata.consumer.FundsApp" to
46+
"Full funds surface: candles + all params (no volume / no intraday / no chunking), CSV facet, columns projection, Option A. Needs mock server.")
4547
)
4648

4749
demoApps.forEach { (taskName, app) ->

0 commit comments

Comments
 (0)