|
| 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 |
0 commit comments