# SDK Requirements

This document defines the requirements for official Market Data SDKs. Use this as acceptance criteria when building or evaluating SDK implementations.

## Core Principles

### Language-Idiomatic Design

**SDKs must feel native to their language.** We adapt to the language's conventions—the language does not adapt to us.

This means:
- Use the language's standard patterns for errors, async, and data structures
- Follow the language's naming conventions (snake_case vs camelCase vs PascalCase)
- Return data types that developers in that ecosystem expect
- Don't force patterns from one language onto another
- **Follow official style guides and best practices for each language**

### Easy Default Requests

**Customers must be able to make a request and inspect the result in 2–3 lines of code.** Every endpoint must have a simple, zero-configuration path (beyond authentication) that returns a useful, printable result.

This means:
- Every endpoint method must work with just required parameters (e.g., a symbol) — no mandatory configuration or boilerplate
- Response objects must support string conversion that produces a readable summary (see §11.6)
- Environment-based token loading (`MARKETDATA_TOKEN`) must work automatically so users can skip explicit auth setup
- The combination of env-based auth + simple method call + print/log must produce a meaningful result

#### Examples

**Python (3 lines):**
```python
from marketdata import MarketDataClient
client = MarketDataClient()
print(client.stocks.quotes("AAPL"))
```

**Go (3 lines, excluding package/import):**
```go
client, _ := marketdata.NewClient()
quotes, _ := client.Stocks.Quotes("AAPL")
fmt.Println(quotes)
```

**PHP (3 lines):**
```php
$client = new MarketDataClient();
$quote = $client->stocks->quotes("AAPL");
echo $quote;
```

### Required Style Guides

| Language   | Style Guide / Best Practices                                                                                                      |
|------------|-----------------------------------------------------------------------------------------------------------------------------------|
| Python     | [PEP 8](https://peps.python.org/pep-0008/), [PEP 257](https://peps.python.org/pep-0257/) (docstrings)                             |
| PHP        | [PSR-12](https://www.php-fig.org/psr/psr-12/), [PSR-4](https://www.php-fig.org/psr/psr-4/) (autoloading)                          |
| JavaScript | [MDN Guidelines](https://developer.mozilla.org/en-US/docs/MDN/Writing_guidelines/Writing_style_guide/Code_style_guide/JavaScript) |
| TypeScript | [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/)                                                              |
| Go         | [Effective Go](https://go.dev/doc/effective_go), [Go Code Review Comments](https://go.dev/wiki/CodeReviewComments)                |
| Java       | [Google Java Style Guide](https://google.github.io/styleguide/javaguide.html)                                                     |
| C#         | [.NET Framework Design Guidelines](https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/)                           |
| Rust       | [Rust API Guidelines](https://rust-lang.github.io/api-guidelines/)                                                                |

SDKs must pass the standard linter for their language (e.g., `ruff` for Python, `phpcs` for PHP, `golint` for Go).

| Language   | Error Handling        | Async Pattern         | Default Output   | Naming              |
|------------|-----------------------|-----------------------|------------------|---------------------|
| Python     | Exceptions*           | Sync (async optional) | pandas DataFrame | snake_case          |
| JavaScript | Promise rejection     | async/await           | Plain objects    | camelCase           |
| PHP        | Exceptions            | Sync                  | Objects/arrays   | camelCase           |
| Go         | Return `error`        | Blocking + context    | Structs          | PascalCase exported |
| Java       | Exceptions            | CompletableFuture     | POJOs            | camelCase           |
| C#         | Exceptions            | Task async/await      | Objects          | PascalCase          |
| Rust       | Return `Result<T, E>` | async with runtime    | Structs          | snake_case          |

> **\* Python SDK Technical Debt:** The current Python SDK (v1.x) returns `MarketDataClientErrorResult` instead of raising exceptions. This is non-idiomatic and will be fixed in v2.0. **New SDKs must use idiomatic patterns from day one.**

---

## Quick Reference

| Requirement                       | Priority |
|-----------------------------------|----------|
| Easy default requests (2–3 lines) | Must     |
| Bearer token authentication       | Must     |
| Environment-based configuration   | Must     |
| Connection pooling                | Must     |
| Fixed 99-second timeout           | Must     |
| Typed input validation            | Must     |
| Typed output models               | Must     |
| Decimal type for monetary values  | Must     |
| Structured error handling         | Must     |
| API credit tracking               | Must     |
| Retry with exponential backoff    | Must     |
| API status awareness              | Should   |
| Multiple output formats           | Should   |
| Batch/concurrency helpers         | Should   |
| Ecosystem-native advanced formats | Optional |

---

## 1. Client Architecture

### 1.1 Client Object

The SDK must expose a top-level client object (e.g., `MarketDataClient`) that:

- Accepts an API token via constructor parameter OR reads from environment variable `MARKETDATA_TOKEN`
- Allows base URL override (default: `https://api.marketdata.app`)
- Allows API version override (default: `v1`)
- Uses a single HTTP client instance with connection pooling
- Provides explicit cleanup/close method for releasing resources
- Exposes current rate limit state
- Sets User-Agent header: `marketdata-sdk-{language}/{version}` (e.g., `marketdata-sdk-php/1.0.0`)
- Fixed HTTP timeout: **99 seconds**

### 1.2 Resource Groupings

The client must expose these resource namespaces:

```
client.stocks
client.options
client.funds
client.markets
client.utilities
```

---

## 2. Required API Coverage (Canonical Docs Are Source of Truth)

Do not duplicate REST paths, payload schemas, or parameter contracts in this document.

- **Canonical REST documentation is authoritative** for endpoint paths, request parameters, response schemas, and behavior.
- If this SDK requirements doc conflicts with REST API docs, the REST API docs win.
- SDK acceptance should verify method coverage against canonical docs, not against hardcoded paths here.

### 2.1 Canonical API Documentation

- [API Overview](https://www.marketdata.app/docs/api)
- [Stocks API](https://www.marketdata.app/docs/api/stocks/)
- [Options API](https://www.marketdata.app/docs/api/options/)
- [Funds API](https://www.marketdata.app/docs/api/funds/)
- [Markets API](https://www.marketdata.app/docs/api/markets/)
- [Utilities API](https://www.marketdata.app/docs/api/utilities/)
- [Universal Parameters](https://www.marketdata.app/docs/api/universal-parameters/)

### 2.2 Required SDK Method Coverage

SDKs must provide first-class methods for the following capabilities (language-idiomatic naming is expected):

| Resource    | Required SDK Methods (capabilities)               |
|-------------|---------------------------------------------------|
| `stocks`    | `prices`, `quotes`, `candles`, `earnings`, `news` |
| `options`   | `chain`, `expirations`, `quotes`, `lookup`        |
| `funds`     | `candles`                                         |
| `markets`   | `status`                                          |
| `utilities` | `status`, `headers`, `user`                       |

---

## 3. Universal Parameters

All endpoint methods must support these parameters:

| Parameter    | Values                             | Description                         |
|--------------|------------------------------------|-------------------------------------|
| `dateformat` | `timestamp`, `unix`, `spreadsheet` | Controls date output format         |
| `columns`    | array of strings                   | Filter response to specific columns |
| `headers`    | boolean                            | Include headers in CSV output       |
| `human`      | boolean                            | Use human-readable field names      |

---

## 4. Configuration Cascade

All SDKs must implement a three-tier configuration system where more specific settings override less specific ones:

### Priority Order (Lowest → Highest)

| Priority    | Source                | Scope               | Example                                             |
|-------------|-----------------------|---------------------|-----------------------------------------------------|
| 1 (lowest)  | `.env` file           | Project-level       | `.env` file in project root                         |
| 2           | Environment variables | System/shell        | `export MARKETDATA_TOKEN=...`                       |
| 3           | Client defaults       | Per-client instance | `client.default_params.output_format = "json"`      |
| 4 (highest) | Method parameters     | Per-request         | `client.stocks.prices("AAPL", output_format="csv")` |

SDKs should automatically load `.env` files from the current working directory if present.

### How It Works

When making a request, merge configuration in order:
1. Load `.env` file values (if present)
2. Override with system environment variables
3. Override with client default parameters
4. Override with direct method parameters

### Example Flow

```
.env file: MARKETDATA_OUTPUT_FORMAT=internal
Shell env: MARKETDATA_OUTPUT_FORMAT=json
Client default: output_format=dataframe
Method call: client.stocks.prices("AAPL", output_format="csv")

Result: output_format="csv" (method parameter wins)
```

### Supported Environment Variables

| Variable                        | Purpose                         | Default                      |
|---------------------------------|---------------------------------|------------------------------|
| `MARKETDATA_TOKEN`              | API authentication token        | (none)                       |
| `MARKETDATA_BASE_URL`           | API base URL                    | `https://api.marketdata.app` |
| `MARKETDATA_API_VERSION`        | API version                     | `v1`                         |
| `MARKETDATA_LOGGING_LEVEL`      | SDK logging level               | `INFO`                       |
| `MARKETDATA_OUTPUT_FORMAT`      | Default output format           | (language default)           |
| `MARKETDATA_DATE_FORMAT`        | Default date format             | `timestamp`                  |
| `MARKETDATA_COLUMNS`            | Columns to include              | (all)                        |
| `MARKETDATA_ADD_HEADERS`        | Include headers in CSV          | `true`                       |
| `MARKETDATA_USE_HUMAN_READABLE` | Human-readable field names      | `false`                      |
| `MARKETDATA_MODE`               | Data mode (live/cached/delayed) | `live`                       |

---

## 5. Authentication

### Requirements

- Set `Authorization: Bearer {token}` header on all requests
- Token resolution follows the configuration cascade:
  1. `.env` file (lowest)
  2. System environment variable `MARKETDATA_TOKEN`
  3. Client constructor parameter (highest)
- **Validate token on startup by default**: SDKs should call `/user/` during client initialization and fail fast on invalid tokens.
- **Constructor override required**: SDKs must provide a client-constructor option to disable startup validation for short-lived runtimes (e.g., serverless/CLI jobs). When disabled, token/auth errors must surface on the first authenticated request.
- **Never log tokens** - redact in all log output

### Demo Mode

When no token is provided:

- Omit Authorization header
- Log a warning about demo mode limitations
- Skip rate limit initialization

---

## 6. Error Handling

### 6.1 Error Taxonomy

All SDKs must define these error types:

| Error Type             | When to Use                                               |
|------------------------|-----------------------------------------------------------|
| `AuthenticationError`  | 401 responses, invalid/missing token                      |
| `BadRequestError`      | 400 responses, invalid parameters                         |
| `PaymentRequiredError` | 402 responses, request valid but not included in the plan |
| `ForbiddenError`       | 403 responses, token rejected for this request            |
| `NotFoundError`        | 404 responses                                             |
| `RateLimitError`       | 429 responses, rate limit exceeded                        |
| `ServerError`          | 5xx responses                                             |
| `NetworkError`         | Connection failures, timeouts                             |
| `ParseError`           | Failed to parse response                                  |

Use the naming convention the language already uses for its other error types. Go returns a sentinel alongside the type, so `PaymentRequiredError` pairs with `ErrPaymentRequired` and `ForbiddenError` with `ErrForbidden`.

**Why 402 is its own type.** A 402 says the token is valid and the request is well-formed — only the plan does not cover it. That is a different fact from a 401, where no usable identity was presented, and from a 403, where an identity was presented and refused. A caller that must tell "you may not ask this" apart from "you asked this wrong" cannot do so once all three collapse into one class, and must not have to read `.status_code` off a generic HTTP error to find out. Capability and entitlement discovery is the concrete case: a consumer probing what its plan allows needs 402 to be a typed, catchable answer. See [402: Payment Required](https://www.marketdata.app/docs/api/troubleshooting/payment-required/) for the canonical semantics.

### 6.2 Support Context

Every error must include these fields for support staff troubleshooting:

| Field            | Source                     | Example                                          |
|------------------|----------------------------|--------------------------------------------------|
| `request_id`     | `cf-ray` response header   | `"8a1b2c3d4e5f6g7h-SJC"`                         |
| `request_url`    | Full request URL           | `"https://api.marketdata.app/v1/stocks/quotes/"` |
| `status_code`    | HTTP status code           | `429`                                            |
| `timestamp`      | SDK-generated (US/Eastern) | `"2025-02-21 12:00:00"`                          |
| `message`        | Error description          | `"Rate limit exceeded"`                          |
| `exception_type` | Error class name           | `"RateLimitError"`                               |

In exception-based SDKs (Python, PHP, Java, C#, JavaScript/TypeScript), these support-context fields must be properties on the thrown exception object. In returned-error SDKs (Go, Rust), these fields must be present on the returned error value/type.

### 6.3 Support Info Helper

SDKs must provide a formatted `support_info` string for users to send to support:

```
--- MARKET DATA SUPPORT INFO ---
request_id:     8a1b2c3d4e5f6g7h-SJC
request_url:    https://api.marketdata.app/v1/stocks/quotes/
status_code:    429
timestamp:      2025-02-21 12:00:00
message:        Rate limit exceeded
exception_type: RateLimitError
--------------------------------
```

This must be accessible via `error.support_info` or equivalent.

### 6.4 Language-Specific Patterns

Follow the idiomatic error handling pattern for each language:

| Language   | Pattern          | Example                                 |
|------------|------------------|-----------------------------------------|
| Python     | Raise exceptions | `raise RateLimitError(...)`             |
| JavaScript | Reject promises  | `throw new RateLimitError(...)`         |
| PHP        | Throw exceptions | `throw new RateLimitException(...)`     |
| Go         | Return error     | `return nil, &RateLimitError{...}`      |
| Java       | Throw exceptions | `throw new RateLimitException(...)`     |
| C#         | Throw exceptions | `throw new RateLimitException(...)`     |
| Rust       | Return Result    | `Err(MarketDataError::RateLimit {...})` |

**Do not** force exception-based error handling onto Go or Rust. **Do not** force Result types onto Python or PHP.

---

## 7. Logging

### Log Format

Use a consistent log format across all SDKs:

```
{timestamp} - {logger_name} - {level} - {message}
```

Example: `2025-02-21 12:00:00 - marketdata.client - INFO - Making request to /v1/stocks/quotes/`

### Log Levels

Configurable via `MARKETDATA_LOGGING_LEVEL` environment variable:

| Level   | What to Log                                         |
|---------|-----------------------------------------------------|
| DEBUG   | Token (redacted), request details, response headers |
| INFO    | Client initialization, base URL, API version        |
| WARNING | Demo mode, deprecated features                      |
| ERROR   | Request failures, rate limit errors                 |

### Required Log Points

- Client initialization (INFO)
- Token value - **redacted** (DEBUG): show only last 4 chars
- Request URL (DEBUG)
- Response status and timing (DEBUG/INFO based on status)
- Errors with support context (ERROR)

### Token Redaction

Never log full tokens. Redact to show only last 4 characters:

```
Token: ************************************YKT0
```

---

## 8. Rate Limiting

### 8.1 Initialization

On client creation (with valid token):

1. Call `GET /user/` to fetch rate limit info
2. Store initial credit state: `credit_limit`, `credits_remaining`, `reset_time`, `credits_consumed`

### 8.2 Tracking

After every request:

1. Parse rate limit headers from response:
   - `x-api-ratelimit-limit`
   - `x-api-ratelimit-remaining`
   - `x-api-ratelimit-reset`
   - `x-api-ratelimit-consumed`
2. Attach parsed rate limit metadata to the returned response/result object (request-scoped)
3. Update client-level rate limit state as a convenience latest snapshot

### 8.3 Pre-flight Check

Before each request:

1. Check if `credits_remaining <= 0`
2. If exhausted, throw `RateLimitError` immediately (don't make the request)

### 8.4 Expose State

Provide method to check current rate limits:

```
client.rate_limits
// Returns: { limit: 50000, remaining: 49500, reset: 1708531200, consumed: 1 }
```

`client.rate_limits` is a client-level latest snapshot and is not request-scoped. In concurrent/async execution, this value is non-deterministic (last completed request wins). Per-request logic must use the rate limit metadata attached to each returned response/result.

---

## 9. Retries and Backoff

### 9.1 HTTP Status Code Handling

| Status Code | Action                                                           |
|-------------|------------------------------------------------------------------|
| 200         | Success - parse response                                         |
| 203         | Success - non-authoritative (cached data) - parse response       |
| 400         | Throw `BadRequestError` - do not retry                           |
| 401         | Throw `AuthenticationError` - **fail immediately**, do not retry |
| 402         | Throw `PaymentRequiredError` - do not retry                      |
| 403         | Throw `ForbiddenError` - do not retry                            |
| 404         | Return empty/no-data response (not an error for most endpoints)  |
| 429         | Throw `RateLimitError` - do not retry, expose retry-after        |
| 500         | Throw `ServerError` - do not retry                               |
| 501-599     | Retry with exponential backoff                                   |

**Note on 404**: Most endpoints return 404 when no data exists (e.g., no quotes for a delisted symbol). SDKs should return an empty result object with `no_data = true` rather than throwing an exception.

**Note on 402**: Unlike 404, a 402 must never be returned as an empty or no-data result. [402: Payment Required](https://www.marketdata.app/docs/api/troubleshooting/payment-required/) states that the request itself is valid — the API declined to answer it, and 402 is that refusal. Rendering it as no-data tells the caller the data does not exist, which is false, and it fails silently: there is no exception anywhere for the caller to catch. An SDK that probes a customer's history depth by walking back until the API stops answering reads a plan denial as the end of the archive, and silently truncates that customer's price history. 402 must throw.

**Note on 402 and 403 retries**: Both rows forbid the SDK's automatic retry, and they are terminal for different lengths of time. A 402 stands until the plan changes, so an immediate retry only spends a request. A 403 is terminal for that request, but the condition behind it can clear on its own — the common cause is the multi-IP block, which lifts after about 5 minutes. The SDK must not retry either code itself; the caller may try a 403 again once the block clears.

### 9.2 Retry Conditions

Retry requests only when:

- HTTP status > 500 (server errors)
- Network/connection errors

**Never retry**: 4xx errors, rate limit errors

### 9.3 Backoff Strategy

- Use exponential backoff: `initial * 2^retry`, where `retry` is the **0-indexed retry number** (0 = first retry, 1 = second retry, 2 = third retry)
- Initial delay: 1 second (fixed)
- Default max retries: **3** (in addition to the initial attempt, for up to 4 total attempts)
- Backoff schedule at the default of 3 retries:
  - Before retry 0 (1st retry): **1s** (`1 * 2^0`)
  - Before retry 1 (2nd retry): **2s** (`1 * 2^1`)
  - Before retry 2 (3rd retry): **4s** (`1 * 2^2`)

Max retries is **user-configurable** via a client constructor parameter (e.g., `max_retries`). The default applies when not specified. The initial delay and exponential base are fixed and not configurable.

### 9.4 Retry-After Header

If response includes `Retry-After` header:

- Respect the server-specified delay
- Override calculated backoff with server value

### 9.5 API Status Check

Before each retry attempt for retryable server errors (`501-599`), apply this status-check workflow:

1. Use cached `/status/` data with two thresholds:
   - **Refresh interval**: 270 seconds (4.5 minutes)
   - **Cache validity**: 300 seconds (5 minutes)
2. If cache age is **< 270s**: use cached status, no refresh.
3. If cache age is **270s to < 300s**: use cached status and trigger **non-blocking async refresh**.
4. If cache is **stale or empty** (>= 300s): trigger **non-blocking async refresh** and treat current status as `unknown` for this attempt.
5. Retry decision:
   - If service status is `offline`, do not retry - fail immediately.
   - If service status is `online` or `unknown`, proceed with retry/backoff.

Do not perform blocking status refresh inside retry loops.

---

## 10. Timeouts

### Requirements

- Default request timeout: **99 seconds**
- Timeout is fixed and must not be configurable
- If the HTTP client supports a separate connection timeout, set connection timeout to **2 seconds** (fixed)
- If separate connection timeout is not supported, use the fixed **99-second** request timeout
- Apply timeout to all HTTP requests

---

## 11. Output Formats

### 11.1 Wire Format Decoding (Required)

The API wire format uses compressed, array-keyed JSON. SDKs must decode this into user-usable typed models before returning results.

- Expand compressed response payloads into named fields
- Return idiomatic typed models for the target language
- Do not expose raw compressed arrays as the default SDK output
- Raw compressed payloads may be exposed only as an explicit opt-in debug/raw mode

### 11.2 Language-Specific Defaults

Each SDK should return data in the format most natural for that language ecosystem:

| Language   | Default Output                | Notes                                                    |
|------------|-------------------------------|----------------------------------------------------------|
| Python     | pandas DataFrame              | DataFrames are the standard for financial data in Python |
| R          | data.frame / tibble           | DataFrames are standard in R workflows                   |
| JavaScript | Plain objects/arrays          | Native JS objects, easily JSON-serializable              |
| PHP        | Objects or associative arrays | Follow PHP conventions                                   |
| Go         | Typed structs                 | Strongly typed, no reflection magic                      |
| Java       | POJOs                         | Standard Java bean patterns                              |
| C#         | Typed objects                 | .NET conventions                                         |
| Rust       | Typed structs                 | With serde for serialization                             |

### 11.3 Additional Format Support

SDKs may support additional formats only when they are idiomatic for that language:

| Language / Ecosystem  | Additional Formats to Support                        |
|-----------------------|------------------------------------------------------|
| Python                | Optional Polars DataFrame output                     |
| R                     | Optional tibble/data.table output                    |
| JavaScript/TypeScript | Plain JSON objects/arrays (no DataFrame requirement) |
| PHP                   | Objects/arrays and optional CSV export               |
| Go/Java/C#/Rust       | Typed models; no DataFrame requirement               |

Do not add ecosystem-misaligned formats just for parity across SDKs.

### 11.4 Date Handling

- Parse Unix timestamps to the language's native datetime type
- Normalize to US/Eastern timezone by default
- Respect `dateformat` parameter for output

### 11.5 Monetary Value Precision

**Money must not be represented as a binary floating-point number in typed models when the language provides an exact decimal type.**

Binary floats cannot represent decimal money exactly (`0.1 + 0.2 != 0.3`), so any comparison, rounding, or accumulation a customer performs on SDK output drifts. Prices are the primary product of this API; they must round-trip exactly.

#### Language Applicability

| Language              | Exact Decimal Type     | Monetary Fields      |
|-----------------------|------------------------|----------------------|
| Python                | `decimal.Decimal`      | **Must** use decimal |
| Java                  | `java.math.BigDecimal` | **Must** use decimal |
| C#                    | `decimal`              | **Must** use decimal |
| Go                    | None native            | Exempt — `float64`   |
| Rust                  | None native            | Exempt — `f64`       |
| PHP                   | None native            | Exempt — `float`     |
| JavaScript/TypeScript | None native            | Exempt — `number`    |
| R                     | None native            | Exempt — `numeric`   |

Exempt SDKs **must not** add a third-party decimal library to satisfy this requirement. An ecosystem-foreign money type in public model fields conflicts with the Language-Idiomatic Design principle and forces a dependency on every consumer. Float is the correct choice where the language offers no exact decimal type.

#### Preserve Precision at the Parse Boundary

**Converting an already-parsed float to a decimal does not satisfy this requirement.** The exact value is destroyed at the JSON decode step, and a later conversion only locks in the float error. SDKs subject to this requirement must decode the response so the API's decimal string reaches the decimal type directly.

| Language | Decimal-Preserving Decode                                   |
|----------|-------------------------------------------------------------|
| Python   | `json.loads(response.text, parse_float=Decimal)`            |
| Java     | Jackson `DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS` |
| C#       | `System.Text.Json` reader → `GetDecimal()` on the raw token |

#### What Counts as Money

The rule: **if the value is denominated in currency, it is money.** Ratios, statistics, and counts are not.

- **Monetary (decimal):** bid, ask, mid, last, change, open/high/low/close, strike, intrinsic value, extrinsic value, underlying price, and EPS figures (reported, estimated, surprise)
- **Not monetary (float or int):** Greeks (delta, gamma, theta, vega), implied volatility, percentages (e.g. `changepct`, `surpriseEPSpct`), and sizes/counts (`bidSize`, `askSize`, `volume`, `openInterest`, `dte`)

Canonical REST documentation remains the source of truth for which fields exist on each endpoint; apply the rule above to that schema.

#### Format Exemptions

This requirement governs **typed models only**. The following output formats are exempt:

| Output Format                 | Money Type    | Why Exempt                                                                 |
|-------------------------------|---------------|----------------------------------------------------------------------------|
| DataFrame (pandas, polars, R) | `float64`     | Analytics surface for vectorized math; pandas has no decimal dtype         |
| CSV                           | Raw text      | Already byte-faithful exact decimal strings straight from the API          |
| JSON                          | Native decode | "JSON output" means the decoded object; exactness is the typed model's job |

**On DataFrames:** a DataFrame is an analytics surface — users load data to run vectorized statistics (returns, rolling means, volatility). In pandas, decimals degrade to `object` dtype, which kills vectorized math on the default output. Where a backend *does* offer a decimal dtype (polars), still use `float64`: making only one backend exact would mean identical code returns different dtypes depending on which optional dependency is installed. Consistency across interchangeable backends wins.

Customers who need exactness go through the typed model. SDK documentation must state plainly which formats are exact and which are not.

#### Arithmetic

SDKs must not perform their own price arithmetic. Computed values (`mid`, `change`, and similar) are supplied by the API and are passed through as received. This requirement is about representation, not calculation.

### 11.6 Response Object Features

All response objects must provide:

#### Format Detection Methods

| Method     | Returns                           |
|------------|-----------------------------------|
| `isJson()` | `true` if response is JSON format |
| `isCsv()`  | `true` if response is CSV format  |
| `isHtml()` | `true` if response is HTML format |

#### File Saving

```
response.saveToFile("/path/to/output.csv")
```

- Save response data to a file
- Automatically determine format from extension or response type
- Return the file path or a success indicator

#### No-Data Detection

```
response.no_data  // or response.hasData()
```

- Indicate when a 404 response returned no data
- Allow callers to distinguish "no data" from errors

#### String Conversion (Required)

All public response models must support idiomatic string conversion for fast inspection/logging in example code and day-to-day usage.

- Python: implement `__str__` (and `__repr__` where appropriate)
- PHP: implement `__toString()`
- JavaScript/TypeScript: implement `toString()`
- Go: implement `String()` on formatter types where applicable
- Java/C#: provide `toString()` / `ToString()`
- Rust: implement `Display` (and derive/provide `Debug` as needed)

String conversion should produce concise, developer-friendly summaries (not raw compressed payloads).

---

## 12. Concurrency Helpers

### Global Concurrency Pool

SDKs must enforce a **maximum of 50 concurrent requests** at the client level using a **sliding window** (not batching):

- All endpoints share a single concurrency pool
- Maintain exactly 50 active requests at all times (when demand exists)
- As soon as one request completes, start the next immediately
- **Not batching**: Don't wait for all 50 to finish before starting more
- Must be thread-safe / goroutine-safe / async-safe depending on language

```
# Correct (sliding window):
# Request 1 completes → Request 51 starts immediately
# Request 2 completes → Request 52 starts immediately
# Always 50 in-flight until queue is exhausted

# Wrong (batching):
# Wait for all 50 to complete → Start next 50
# This wastes time waiting for slowest request
```

### stocks.candles() - Date Range Splitting

For intraday resolutions with date ranges > 1 year:

1. Split into year-sized chunks
2. Fetch chunks concurrently (respects global pool limit)
3. Merge results into single response

### options.quotes() - Batch Processing

For multiple option symbols:

1. Fetch quotes concurrently (respects global pool limit)
2. Merge results into single response

---

## 13. Testing Requirements

### Unit Tests (No Real API Calls)

Unit tests must use **mocks for all HTTP requests** - never hit the real API:

- Use language-appropriate HTTP mocking library
- Mock `/user/` and `/status/` endpoints in test fixtures
- Test both success and error scenarios for each endpoint

**Must cover:**
- [ ] URL construction for all endpoints
- [ ] Parameter validation and error messages
- [ ] Error type mapping (status code → error type)
- [ ] Retry behavior and backoff timing
- [ ] Rate limit header parsing
- [ ] Request-scoped rate limit metadata (`consumed` included) under concurrent requests
- [ ] Response parsing and typed model creation
- [ ] Date/time normalization

### Integration Tests (Real API Calls)

Integration tests make **actual REST requests** to the live API:

- Gated by environment variable (e.g., `MARKETDATA_RUN_INTEGRATION_TESTS=true`)
- Require valid API token
- Test real request/response cycles
- Verify actual API behavior matches expectations
- **Run in CI by default for all pull requests and release pipelines**
- **Not run for every commit/push** and not part of standard default/local test runs

#### Minimum coverage: at least one live test per endpoint

**Every endpoint in §2.2 must have at least one integration test that calls it against
the live API.** A mocked test does not satisfy this: a mock proves the SDK parses the
response the author *expected*, which is precisely the assumption that goes stale when the
API changes. Only a live call can tell you the contract still holds.

| Resource    | Endpoints requiring live coverage                 |
|-------------|---------------------------------------------------|
| `stocks`    | `prices`, `quotes`, `candles`, `earnings`, `news` |
| `options`   | `chain`, `expirations`, `quotes`, `lookup`        |
| `funds`     | `candles`                                         |
| `markets`   | `status`                                          |
| `utilities` | `status`, `headers`, `user`                       |

Each test must assert on the **shape of the decoded response** — that the fields the SDK
models actually arrived and carry usable values — not merely that the call returned a
2xx. A test that checks only the status code passes against an endpoint that has silently
started returning an empty body.

Where an endpoint has materially different response shapes, cover each one: a no-data
answer is a different contract from a populated one, and both are worth a witness.

> **A missing token must fail the suite, never skip it.** An integration suite that skips
> when unauthenticated reports success while testing nothing, which is worse than having
> no suite at all — it produces a green check that means nothing. If the token is absent,
> fail and say so.

### Code Coverage

- **100% code coverage required**
- Lines that cannot be tested must be explicitly marked with coverage ignore comments
  - Python: `# pragma: no cover`
  - PHP: `// @codeCoverageIgnore`
  - Go: Use build tags or explicit skip
  - Use language-appropriate ignore syntax
- Coverage ignore must include a comment explaining why

### CI Requirements

- Run **unit tests** on every PR and push to main
- Run on multiple runtime versions
- Produce coverage reports (must be 100% or CI fails)
- Integration tests: required on every PR and in release pipelines; excluded from per-commit/push jobs and default test runs

---

## 14. Documentation Requirements

### README Must Include

- [ ] Installation instructions
- [ ] Quick start example showing a complete request in 2–3 lines of code
- [ ] Environment variable configuration
- [ ] Error handling example
- [ ] String conversion example(s) showing ergonomic object output
- [ ] Link to full documentation

### API Documentation

- Document all public methods
- Include parameter types and descriptions
- Show example usage for each endpoint
- Include examples that demonstrate string conversion on response models
- Document error types and when they occur

---

## 15. Packaging Requirements

### Version & Changelog

- Use Semantic Versioning (MAJOR.MINOR.PATCH)
- Maintain CHANGELOG.md following Keep a Changelog format
- **Version must be automatically detected** from the package manager metadata (e.g., `composer.json`, `setup.py`, `go.mod`) - do not hardcode version strings

### Package Registry

| Language   | Registry                |
|------------|-------------------------|
| Python     | PyPI                    |
| JavaScript | npm                     |
| Java       | Maven Central           |
| Go         | Go modules (pkg.go.dev) |
| C#         | NuGet                   |
| Rust       | crates.io               |

### License

- MIT license required
- Include LICENSE file in distribution

---

## 16. Security Requirements

- [ ] Never log API tokens - redact/obfuscate in all output
- [ ] Support environment variable token loading (no hardcoding)
- [ ] Error messages must not contain sensitive data
- [ ] Validate TLS certificates (no skip-verify options)

---

## 17. Repository and Release Configuration

Sections 1–16 govern the SDK's code. This section governs the **repository that ships
it**. These items are invisible in a code review and are exactly the ones that get
skipped: a documented label that was never created, a security policy pointing at a
disabled button, a release cut by hand because no workflow existed.

Every SDK repository must satisfy this section, regardless of language.

### 17.1 Required Repository Files

| Path                                | Purpose                                                          |
|-------------------------------------|------------------------------------------------------------------|
| `SECURITY.md`                       | Vulnerability reporting instructions and the security fix policy |
| `.github/RELEASE_PROCESS.md`        | How to cut a release for this SDK                                |
| `.github/ISSUE_WORKFLOW.md`         | How a maintainer triages an incoming bug report                  |
| `.github/BUG_FINDING.md`            | How to hunt for bugs proactively, before users hit them          |
| `.github/ISSUE_TEMPLATE/bug.yml`    | Structured bug report form                                       |
| `.github/ISSUE_TEMPLATE/config.yml` | Contact links; `blank_issues_enabled: false`                     |
| `.github/dependabot.yml`            | Dependency updates (see §17.4)                                   |
| `.github/FUNDING.yml`               | `github: MarketDataApp`                                          |
| `CHANGELOG.md`                      | Keep a Changelog format (see §15)                                |
| `LICENSE`                           | MIT (see §15)                                                    |

These are **language-adapted, not copied**. A bug template must ask for the runtime
version, package manager and platform values that actually exist for that language, and
must name real methods and error types from that SDK. A template that asks a Go user for
their target framework is worse than no template.

### 17.2 Repository Settings

- [ ] **Private Vulnerability Reporting is enabled.** `SECURITY.md` tells reporters to use
      Security → "Report a vulnerability". If the setting is off, that button does not
      exist and the instruction is a dead end.
- [ ] **Every label referenced by `ISSUE_WORKFLOW.md` exists.** At minimum `bug`,
      `accepted`, `needs-info`, `wontfix`, `dependencies`. A documented triage step that
      applies a non-existent label fails on execution.
- [ ] **Description is set** and names the language and what the SDK covers.
- [ ] **Topics are set** — at minimum the language, `sdk`, and the asset classes the SDK
      actually supports. Remove topics for resources the SDK does not cover; they
      misrepresent the package in search.
- [ ] **`ISSUE_TEMPLATE/config.yml` links only to destinations that exist.** If
      Discussions are disabled on the repository, the config must not link to them.
- [ ] **Default branch is `main`**, with a `development` branch when the CI workflows
      reference one.
- [ ] **Branch protection on the default branch**, or a deliberate, recorded decision not
      to have it. `RELEASE_PROCESS.md` must state which, because it determines whether a
      release merge needs an approval.

### 17.3 Required Secrets

| Secret               | Required     | Purpose                                                                          |
|----------------------|--------------|----------------------------------------------------------------------------------|
| `MARKETDATA_TOKEN`   | Yes          | Live integration suite (§13). Its absence must **fail** the job, never skip it   |
| `CODECOV_TOKEN`      | Optional     | Coverage badge; absence must not break CI                                        |
| Registry credentials | Per language | Publishing (see §15) — e.g. Maven Central signing keys, NuGet Trusted Publishing |

> **A skipped integration suite must never report success.** The failure mode this
> prevents is a green pipeline that ran nothing. If the token is absent, the job fails and
> says so.

### 17.4 Dependency Updates

- [ ] `dependabot.yml` covers the CI ecosystem (`github-actions`) **and** every package
      manifest in the repository — including example or sample applications that carry
      their own manifest.
- [ ] **Updates are grouped.** Ungrouped Dependabot opens one pull request per dependency
      per manifest. Where several sample apps share a dependency set, one upstream release
      becomes a wall of near-identical pull requests and the updates stop being read.
- [ ] **Major versions are kept out of the routine group** — either in a group of their
      own or as individual pull requests — so routine bumps merge without waiting on a
      major, and each major still gets read on its own.
- [ ] **Dependencies satisfied by a local path override are ignored** (for example a Go
      `replace` directive, or an npm `file:` dependency). A pull request bumping one
      changes nothing and can never merge.
- [ ] Dependabot pull requests carry the `dependencies` label.

### 17.5 Release Automation

Every SDK needs a release workflow. Cutting a release by hand is not acceptable as a
steady state: the steps that get skipped under pressure are exactly the ones that matter.

- [ ] A **`tag-and-release` workflow** exists, triggered **only** by manual
      `workflow_dispatch`. No push, merge, or tag event may reach it.
- [ ] It requires a **typed confirmation input** (`confirm: RELEASE`).
- [ ] It **validates before spending runner time**: the version is well-formed SemVer, the
      tag does not already exist, and `CHANGELOG.md` contains the section for the version.
- [ ] It **runs the full test suite, including live integration tests, before the tag is
      cut**, against the exact ref being released. Reuse the existing CI workflow rather
      than copying it — a second copy of the suite will drift.
- [ ] **Release notes are extracted from `CHANGELOG.md`**, never written by hand at
      release time and never generated back into the changelog. The heading format is
      matched exactly (`## [X.Y.Z]`), so the changelog must carry a `## [Unreleased]`
      section for maintainers to promote.
- [ ] The workflow **resolves the ref to a concrete commit SHA** and tags that SHA, so a
      branch moving mid-run cannot change what ships.
- [ ] It **verifies the release after publishing**: the artifact resolves from the public
      registry, and a throwaway project can install it and report the expected version.

> **A tag or release created by a workflow using the default `GITHUB_TOKEN` does not start
> another workflow run.** GitHub suppresses this to prevent recursive triggering. Any
> workflow gated on `release: published` or `push: tags` therefore stays **silent** for an
> automated release. Do not rely on it to test or publish; chain the work explicitly from
> the release workflow, and say so in `RELEASE_PROCESS.md`.

#### Registries and irreversibility

`RELEASE_PROCESS.md` must state where the point of no return is and what the remedy is,
because it differs sharply by language:

| Language                          | Publication                                                                              | Can a bad version be withdrawn?                                                                              |
|-----------------------------------|------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------|
| Python, JavaScript, Java, C#, PHP | Upload to a registry                                                                     | No — versions are immutable. Unlist or yank where supported, then ship a patch                               |
| Go                                | **The git tag itself.** `proxy.golang.org` reads the tag; `sum.golang.org` logs the hash | No. Deleting the tag does not withdraw it. The only remedy is a later version carrying a `retract` directive |

Go has no publish step and no staging feed: the moment the tag exists and one person
fetches it, the version is public and permanent. Its release workflow therefore ends in
verification rather than upload, and all validation must precede the tag.

### 17.6 Verifying a Repository

These checks are mechanical and should be run against every SDK repository periodically,
not only at acceptance.

```bash
REPO=MarketDataApp/sdk-<lang>

# Required files
for f in SECURITY.md .github/RELEASE_PROCESS.md .github/ISSUE_WORKFLOW.md \
         .github/BUG_FINDING.md .github/dependabot.yml .github/FUNDING.yml \
         .github/ISSUE_TEMPLATE/bug.yml .github/ISSUE_TEMPLATE/config.yml; do
  gh api "repos/$REPO/contents/$f" --jq '.name' >/dev/null 2>&1 \
    && echo "ok      $f" || echo "MISSING $f"
done

# Private Vulnerability Reporting
gh api "repos/$REPO/private-vulnerability-reporting" --jq '.enabled'

# Labels the triage workflow depends on
for l in bug accepted needs-info wontfix dependencies; do
  gh api "repos/$REPO/labels/$l" --jq '.name' >/dev/null 2>&1 \
    && echo "ok      label:$l" || echo "MISSING label:$l"
done

# Integration token, description, topics
gh api "repos/$REPO/actions/secrets" --jq '[.secrets[].name] | join(", ")'
gh api "repos/$REPO" --jq '{description, topics: (.topics | length), discussions: .has_discussions}'
```

---

## Acceptance Checklist

Before accepting an SDK, verify:

### Core Functionality
- [ ] Every endpoint works in 2–3 lines of code (env auth + method call + print)
- [ ] All required SDK method capabilities implemented (as defined in this doc and canonical API docs)
- [ ] Bearer authentication works
- [ ] Token validation defaults to startup (fail fast) and supports constructor override to disable startup validation
- [ ] Environment variable configuration works
- [ ] Configuration cascade (env → client defaults → method params)
- [ ] Rate limits are tracked and exposed
- [ ] Errors are typed with support context
- [ ] 401 errors fail immediately (no retry)
- [ ] 404 returns no-data response (not exception)
- [ ] Retries use exponential backoff (>500 only)
- [ ] Fixed timeout policy enforced (99s request timeout; 2s connect timeout when supported)
- [ ] Concurrency pool enforced (50 max sliding window)
- [ ] User-Agent header set correctly

### Code Quality
- [ ] Unit tests exist and pass (mocked, no real API calls)
- [ ] Integration tests exist (gated by env var)
- [ ] At least one live integration test per §2.2 endpoint, asserting on the decoded response shape
- [ ] A missing API token fails the integration suite rather than skipping it
- [ ] 100% code coverage (or explicit ignores with comments)
- [ ] CI pipeline configured
- [ ] Code follows language style guide (PEP 8, PSR-12, etc.)
- [ ] Passes standard linter for language
- [ ] No security issues (token leakage, etc.)

### Response Objects
- [ ] Format detection methods (isJson, isCsv, isHtml)
- [ ] File saving capability (saveToFile)
- [ ] No-data detection property
- [ ] Monetary fields use the language's exact decimal type, or the language is exempt per §11.5
- [ ] JSON decoding preserves decimal precision (no float round-trip before the model)
- [ ] Non-monetary fields (Greeks, IV, percentages, sizes) remain float/int
- [ ] DataFrame output remains float64 on every supported backend

### Documentation
- [ ] README with quick start
- [ ] All methods documented
- [ ] Examples provided

### Distribution
- [ ] Published to appropriate registry
- [ ] SemVer versioning
- [ ] Version auto-detected from package metadata
- [ ] Changelog exists
- [ ] MIT license included

### Repository & Release Configuration
- [ ] All required repository files present (§17.1), language-adapted rather than copied
- [ ] Private Vulnerability Reporting enabled
- [ ] Every label referenced by `ISSUE_WORKFLOW.md` exists in the repository
- [ ] Description and topics set, and topics do not claim unsupported resources
- [ ] `ISSUE_TEMPLATE/config.yml` links only to destinations that exist
- [ ] `MARKETDATA_TOKEN` secret present; a missing token fails the integration job rather than skipping it
- [ ] Dependabot covers every manifest, groups its updates, and separates majors
- [ ] Manual-dispatch release workflow with typed confirmation, gated on the full suite
- [ ] Release notes extracted from `CHANGELOG.md`; an `## [Unreleased]` section exists
- [ ] `RELEASE_PROCESS.md` states where the point of no return is and how to remedy a bad release
