diff --git a/.github/workflows/sync-sdk-docs.yml b/.github/workflows/sync-sdk-docs.yml new file mode 100644 index 00000000..e56ad05c --- /dev/null +++ b/.github/workflows/sync-sdk-docs.yml @@ -0,0 +1,164 @@ +# Syncs SDK docs from /sdk// in this repo into MarketDataApp/sdk-/docs/ +# by converting MDX → clean Markdown and opening a PR against the target repo. +# +# Pilot scope: sdk-js only. Adding more SDKs is a one-line matrix change. +# +# ──────────────────────────────────────────────────────────────────────────── +# One-time setup (GitHub App auth — survives the eventual repo migration to +# the MarketData-App org without code changes): +# +# 1. Create a GitHub App owned by `MarketData-App` (org settings → Developer +# settings → GitHub Apps → New GitHub App). Suggested name: +# `marketdata-docs-sync`. Set "Where can this be installed?" to "Any +# account" so it can run against repos that still live under the +# MarketDataApp user (current home) and later the MarketData-App org. +# 2. Permissions: Contents = Read & write, Pull requests = Read & write, +# Metadata = Read. No webhook (uncheck "Active"). +# 3. Generate a private key (.pem) and download it. +# 4. Install the App on `MarketDataApp` (user), granting access to +# `documentation` and `sdk-js`. (Add more SDK repos as the matrix grows.) +# 5. In this repo: +# - Repo variable `SDK_DOCS_APP_ID` = the App ID (a number) +# - Repo secret `SDK_DOCS_APP_PRIVATE_KEY` = the .pem contents +# 6. After repo migration to the org, change `TARGET_OWNER` below from +# `MarketDataApp` to `MarketData-App`. +# ──────────────────────────────────────────────────────────────────────────── + +name: "SDK Docs: Sync to SDK Repos" + +on: + push: + branches: [staging] + paths: + - 'sdk/js/**' + - 'lib/mdx-to-md.js' + - 'scripts/export-sdk-docs.js' + - '.github/workflows/sync-sdk-docs.yml' + workflow_dispatch: + inputs: + sdk: + description: 'Which SDK to sync (pilot: js only)' + required: true + default: 'js' + type: choice + options: [js] + +env: + SOURCE_OWNER: MarketDataApp + TARGET_OWNER: MarketDataApp # change to MarketData-App after repo migration + MANIFEST_FILE: docs/.sync-manifest.txt + +jobs: + sync: + runs-on: ubuntu-latest + permissions: + contents: read + strategy: + fail-fast: false + matrix: + sdk: [js] + steps: + - name: Checkout documentation source + uses: actions/checkout@v4 + with: + path: docs-source + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Run unit tests for the conversion lib + working-directory: docs-source + run: node --test lib/__tests__/*.test.js + + - name: Convert sdk// → clean .md + working-directory: docs-source + env: + SDK: ${{ matrix.sdk }} + OUT_DIR: ${{ runner.temp }}/out + run: node scripts/export-sdk-docs.js --sdk "$SDK" --out "$OUT_DIR" + + - name: Mint cross-repo token (GitHub App) + id: app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ vars.SDK_DOCS_APP_ID }} + private-key: ${{ secrets.SDK_DOCS_APP_PRIVATE_KEY }} + owner: ${{ env.TARGET_OWNER }} + repositories: sdk-${{ matrix.sdk }} + + - name: Checkout target SDK repo + uses: actions/checkout@v4 + with: + repository: ${{ env.TARGET_OWNER }}/sdk-${{ matrix.sdk }} + token: ${{ steps.app-token.outputs.token }} + path: target + + - name: Replace previously-generated docs with fresh export + # We DO NOT wipe target/docs/ wholesale — it may contain SDK-repo-owned + # content (ADRs, internal READMEs). The previous sync wrote a manifest + # at $MANIFEST_FILE listing every file it generated. Delete exactly + # those paths, then drop the fresh export in (which writes a new + # manifest). + env: + OUT_DIR: ${{ runner.temp }}/out + run: | + set -euo pipefail + mkdir -p target/docs + if [ -f "target/$MANIFEST_FILE" ]; then + while IFS= read -r rel; do + # Skip blank lines and comments (lines beginning with '#') + case "$rel" in + ''|'#'*) continue ;; + esac + rm -f "target/docs/$rel" + done < "target/$MANIFEST_FILE" + fi + # Remove any now-empty directories left behind. + find target/docs -type d -empty -delete || true + mkdir -p target/docs + cp -R "$OUT_DIR/." target/docs/ + + - name: Compute diff summary for PR body + id: diff + working-directory: target + run: | + set -euo pipefail + git add -A docs/ + { + echo "summary<> "$GITHUB_OUTPUT" + + - name: Open / update PR in target SDK repo + uses: peter-evans/create-pull-request@v6 + with: + path: target + token: ${{ steps.app-token.outputs.token }} + branch: sync/docs-from-documentation + delete-branch: true + commit-message: 'docs: sync from documentation@${{ github.sha }}' + title: 'docs: sync from documentation@${{ github.sha }}' + body: | + Automated sync from [`${{ env.SOURCE_OWNER }}/documentation@${{ github.sha }}`](https://github.com/${{ env.SOURCE_OWNER }}/documentation/commit/${{ github.sha }}). + + **SDK:** `${{ matrix.sdk }}` + **Source path:** `sdk/${{ matrix.sdk }}/` + **Workflow run:** [#${{ github.run_id }}](https://github.com/${{ env.SOURCE_OWNER }}/documentation/actions/runs/${{ github.run_id }}) + + ### Changed files + + ``` + ${{ steps.diff.outputs.summary }} + ``` + + > **These files are auto-synced from + > [MarketDataApp/documentation](https://github.com/MarketDataApp/documentation/tree/staging/sdk/${{ matrix.sdk }})** — + > edit the source `.mdx` there, not the rendered `.md` here. + > Hand-edits to files listed in `docs/.sync-manifest.txt` will + > be overwritten on the next sync. diff --git a/.gitignore b/.gitignore index 7a6f4a60..6a12acd3 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,6 @@ static/robots.txt static/css/ worker/node_modules +# Internal SOPs (not for public docs site) +sop/ + diff --git a/account/data-freshness.md b/account/data-freshness.md new file mode 100644 index 00000000..1d4cf05d --- /dev/null +++ b/account/data-freshness.md @@ -0,0 +1,173 @@ +--- +title: Data Freshness +sidebar_position: 5.5 +--- + +Market Data classifies the data you receive into three freshness categories: + +- **Real-time** — under 15 minutes old. Live trading data. +- **Delayed** — 15+ minutes old, but from the current trading session. +- **Historical** — from a previous, fully-closed trading session. + +Which category applies depends on your plan, the data type, and the time of day. + +## When Delayed data becomes Historical + +Historical requires both a >15-minute delay floor *and* a data-type-specific session-closed condition. The session-closed condition is **different for stocks and options**: + +- **Stocks:** Historical at session close + 15 min — **4:15:01 PM ET** on a regular trading day. Between 4:00 PM and 4:15 PM ET, the data is still Delayed (the 15-minute Delayed window hasn't elapsed yet). +- **Options:** Historical at the *next* session's open — **9:30:01 AM ET** the next trading day, not at the prior session's close. + +Friday's options quotes therefore do **not** become Historical until **9:30:01 AM ET Monday** — they remain Delayed all weekend. + +If you query an options endpoint at 6:33 AM ET Wednesday on a plan that provides Historical-only options data, you will receive **Monday's** close, not Tuesday's. Tuesday's options data does not roll to Historical until 9:30:01 AM ET Wednesday. This is the most common cause of "the data doesn't match my broker" support requests — the behavior is correct, the customer is just querying before the next session has opened. + +## By Plan + +The tables below show the freshness category for every API endpoint, by plan. Real-time stock and options data also requires non-professional status. See [Exchange Entitlements](./entitlements) for the underlying entitlement model. + +### Free Forever + +All pricing data is Historical (24-hour delayed). Metadata and computed-index endpoints remain Real-time. + +| Endpoint | Freshness | Notes | +|----------------------------|------------|-----------------------------------| +| `/v1/stocks/quotes/` | Historical | | +| `/v1/stocks/candles/` | Historical | | +| `/v1/stocks/bulkcandles/` | Historical | | +| `/v1/stocks/prices/` | Historical | | +| `/v1/options/quotes/` | Historical | | +| `/v1/options/chain/` | Historical | | +| `/v1/options/strikes/` | Real-time | Contract metadata, no pricing | +| `/v1/options/expirations/` | Real-time | Contract metadata | +| `/v1/options/lookup/` | Real-time | Contract metadata | +| `/v1/indices/quotes/` | Real-time | Indices are computed continuously | +| `/v1/indices/candles/` | Real-time | Indices are computed continuously | +| `/v1/markets/status/` | Real-time | Calendar metadata | +| `/v1/funds/*` | See note | Pending product confirmation | + +### Starter Trial + +Same as Free Forever — all pricing data is Historical (24-hour delayed). + +| Endpoint | Freshness | Notes | +|----------------------------|------------|-----------------------------------| +| `/v1/stocks/quotes/` | Historical | | +| `/v1/stocks/candles/` | Historical | | +| `/v1/stocks/bulkcandles/` | Historical | | +| `/v1/stocks/prices/` | Historical | | +| `/v1/options/quotes/` | Historical | | +| `/v1/options/chain/` | Historical | | +| `/v1/options/strikes/` | Real-time | Contract metadata, no pricing | +| `/v1/options/expirations/` | Real-time | Contract metadata | +| `/v1/options/lookup/` | Real-time | Contract metadata | +| `/v1/indices/quotes/` | Real-time | Indices are computed continuously | +| `/v1/indices/candles/` | Real-time | Indices are computed continuously | +| `/v1/markets/status/` | Real-time | Calendar metadata | +| `/v1/funds/*` | See note | Pending product confirmation | + +### Trader Trial + +Same as Starter Trial — all pricing data is Historical (24-hour delayed). + +| Endpoint | Freshness | Notes | +|----------------------------|------------|-----------------------------------| +| `/v1/stocks/quotes/` | Historical | | +| `/v1/stocks/candles/` | Historical | | +| `/v1/stocks/bulkcandles/` | Historical | | +| `/v1/stocks/prices/` | Historical | | +| `/v1/options/quotes/` | Historical | | +| `/v1/options/chain/` | Historical | | +| `/v1/options/strikes/` | Real-time | Contract metadata, no pricing | +| `/v1/options/expirations/` | Real-time | Contract metadata | +| `/v1/options/lookup/` | Real-time | Contract metadata | +| `/v1/indices/quotes/` | Real-time | Indices are computed continuously | +| `/v1/indices/candles/` | Real-time | Indices are computed continuously | +| `/v1/markets/status/` | Real-time | Calendar metadata | +| `/v1/funds/*` | See note | Pending product confirmation | + +### Starter + +Real-time stock data, 15-minute Delayed options data. + +| Endpoint | Freshness | Notes | +|----------------------------|-----------|----------------------------------------------------------------------------| +| `/v1/stocks/quotes/` | Real-time | IEX entitlement | +| `/v1/stocks/candles/` | Real-time | UTP entitlement may impose 15-min delay on intraday candles — see footnote | +| `/v1/stocks/bulkcandles/` | Real-time | Same as `/candles/` | +| `/v1/stocks/prices/` | Real-time | | +| `/v1/options/quotes/` | Delayed | 15 minutes | +| `/v1/options/chain/` | Delayed | 15 minutes | +| `/v1/options/strikes/` | Real-time | Contract metadata | +| `/v1/options/expirations/` | Real-time | Contract metadata | +| `/v1/options/lookup/` | Real-time | Contract metadata | +| `/v1/indices/quotes/` | Real-time | | +| `/v1/indices/candles/` | Real-time | | +| `/v1/markets/status/` | Real-time | Calendar metadata | +| `/v1/funds/*` | See note | Pending product confirmation | + +### Trader + +Real-time data for both stocks and options. + +| Endpoint | Freshness | Notes | +|----------------------------|-----------|------------------------------| +| `/v1/stocks/quotes/` | Real-time | | +| `/v1/stocks/candles/` | Real-time | | +| `/v1/stocks/bulkcandles/` | Real-time | | +| `/v1/stocks/prices/` | Real-time | | +| `/v1/options/quotes/` | Real-time | OPRA entitlement | +| `/v1/options/chain/` | Real-time | OPRA entitlement | +| `/v1/options/strikes/` | Real-time | Contract metadata | +| `/v1/options/expirations/` | Real-time | Contract metadata | +| `/v1/options/lookup/` | Real-time | Contract metadata | +| `/v1/indices/quotes/` | Real-time | | +| `/v1/indices/candles/` | Real-time | | +| `/v1/markets/status/` | Real-time | Calendar metadata | +| `/v1/funds/*` | See note | Pending product confirmation | + +### Quant + +Real-time data for both stocks and options. Same freshness profile as Trader. + +| Endpoint | Freshness | Notes | +|----------------------------|-----------|------------------------------| +| `/v1/stocks/quotes/` | Real-time | | +| `/v1/stocks/candles/` | Real-time | | +| `/v1/stocks/bulkcandles/` | Real-time | | +| `/v1/stocks/prices/` | Real-time | | +| `/v1/options/quotes/` | Real-time | OPRA entitlement | +| `/v1/options/chain/` | Real-time | OPRA entitlement | +| `/v1/options/strikes/` | Real-time | Contract metadata | +| `/v1/options/expirations/` | Real-time | Contract metadata | +| `/v1/options/lookup/` | Real-time | Contract metadata | +| `/v1/indices/quotes/` | Real-time | | +| `/v1/indices/candles/` | Real-time | | +| `/v1/markets/status/` | Real-time | Calendar metadata | +| `/v1/funds/*` | See note | Pending product confirmation | + +### Prime + +Real-time data for both stocks and options. Same freshness profile as Trader and Quant. + +| Endpoint | Freshness | Notes | +|----------------------------|-----------|------------------------------| +| `/v1/stocks/quotes/` | Real-time | | +| `/v1/stocks/candles/` | Real-time | | +| `/v1/stocks/bulkcandles/` | Real-time | | +| `/v1/stocks/prices/` | Real-time | | +| `/v1/options/quotes/` | Real-time | OPRA entitlement | +| `/v1/options/chain/` | Real-time | OPRA entitlement | +| `/v1/options/strikes/` | Real-time | Contract metadata | +| `/v1/options/expirations/` | Real-time | Contract metadata | +| `/v1/options/lookup/` | Real-time | Contract metadata | +| `/v1/indices/quotes/` | Real-time | | +| `/v1/indices/candles/` | Real-time | | +| `/v1/markets/status/` | Real-time | Calendar metadata | +| `/v1/funds/*` | See note | Pending product confirmation | + +## Notes + +- **Real-time data and professional status:** Real-time stock and options data is only available to non-professional users. Professional users on any paid plan revert to Delayed data unless they have signed the OPRA professional subscriber agreement. See [Exchange Entitlements](./entitlements) and [Professional Status](https://www.marketdata.app/education/stocks/professional-status-explained/). +- **`/v1/funds/*` freshness** is documented per fund-data type and is pending publication here. Until then, refer to the individual endpoint pages under [Funds API](../api/funds/). +- **UTP entitlement and intraday stock candles:** the [UTP entitlement](./entitlements#utp-entitlement) grants "15-minute delayed intraday stock candles." On plans with Real-time stock quotes (Starter and above), this means intraday candles may carry a 15-minute delay even though quotes do not. Confirm with the [Plan Limits](./plan-limits) page for your plan. diff --git a/account/plan-limits.md b/account/plan-limits.md index 1d1a4b9d..93dff907 100644 --- a/account/plan-limits.md +++ b/account/plan-limits.md @@ -5,10 +5,6 @@ sidebar_position: 3 Each Market Data plan comes with certain limitations to allow for shared use of our servers. We offer several plans with different price points depending on the quantity of data needed. -:::tip -Most users don't run into trouble with our limits. However, if our standard plans do not fit your use-case, please do not hesitate to speak with sales. We'd be happy to provide you with a custom plan that meets your needs. -::: - ## Standard Plans | | Free Forever | Starter | Trader | Quant | Prime Plans | @@ -55,6 +51,8 @@ Free Forever accounts can access up to 1 year of historical data and Starter acc Free trials of paid plans provide delayed data. Real-time data is only available with paid versions of the Trader plan and above. ::: +For endpoint-level freshness rules and the Delayed → Historical rollover timing — including why options data only rolls at 9:30 AM ET the next trading day rather than at the prior session's close — see [Data Freshness](./data-freshness). + ## API Endpoints The Free Forever plan only provides access to pricing data. Premium endpoints and spreadsheet formulas that contain reference data are not available on free plans. Quant and Prime plans also have access to custom-built endpoints to satisfy the needs of their specific application. diff --git a/account/plans/commercial.mdx b/account/plans/commercial.mdx new file mode 100644 index 00000000..f62b3c5a --- /dev/null +++ b/account/plans/commercial.mdx @@ -0,0 +1,92 @@ +--- +title: Commercial Plan +unlisted: true +description: Market Data's commercially-licensed plan for businesses, indie developers, and apps that distribute derived data to end users. +--- + +import Head from '@docusaurus/Head'; + + + + + +The **Commercial Plan** is the most common starting point for businesses launching an app, dashboard, or product on top of Market Data. At **$250/month** it's the most affordable commercially-licensed market data subscription on the market — and it includes a generous package of data that you can redistribute to your end users under our [Commercial Use Addendum](https://www.marketdata.app/terms/commercial-use-addendum/). It's enough to get a product to market, on budget, with the option to add exchange licensing later as your business grows. + +## Pricing + +**$250/month** — month-to-month billing only. The Commercial Plan does not offer an annual billing option. + +## What You Get with the Commercial Plan + +- **Commercial Use License**: The only Market Data plan that permits business and commercial use of our data, governed by our [Commercial Use Addendum](https://www.marketdata.app/terms/commercial-use-addendum/). +- **No Daily API Credit Limit**: Use our API as much as your application needs, with no daily cap. +- **100,000 Credits Per Minute**: A generous per-minute rate limit suited to production workloads. +- **50 Concurrent Requests**: Run requests in parallel to keep latency low under load. +- **Real-Time SmartMid Stock Prices**: Get current-day real-time stock prices on the [`/stocks/prices`](/api/stocks/prices) endpoint via our proprietary **SmartMid** model — a Market Data product that you can redistribute to your end users under our Commercial Use Addendum. **SmartMid is the only real-time data source included on the Commercial Plan.** +- **Historical Data, Full Depth**: Full historical access on every endpoint — stocks, options, and more — for backtesting, analytics, and historical charts. +- **Premium Endpoints**: Full access to all premium endpoints, including fundamentals and earnings data. + +### Exchange Data Is Historical Only + +The Commercial Plan does **not** include any real-time exchange data. All exchange-sourced data — stock bid/ask quotes, daily and intraday candles, options chains and quotes, all of it — is delivered **historical only**, available starting from the **T+1 session** (today's session becomes available the following session). + +The single exception is **SmartMid stock prices**, which are real-time because SmartMid is our own derived product and not exchange data. + +This is what keeps the plan affordable and lets you redistribute data to your end users without additional exchange paperwork. When your business is ready, exchange licensing can be added on top of your Commercial Plan — see [Why SmartMid Instead of Exchange Real-Time?](#why-smartmid-instead-of-exchange-real-time) below. + +## Who the Commercial Plan Is For + +The Commercial Plan is the entry point for businesses bringing a product to market on top of Market Data. Common use cases include: + +- **Stock and options apps** that display data to paying users +- **Financial dashboards** distributed to clients or employees +- **Research products and newsletters** that publish data-driven content +- **Charting and analytics tools** built on historical data +- **Internal business analytics** at firms that need market data for internal use + +Most businesses start here when launching and add exchange licensing later, as their product matures and their budget grows. + +If you're an indie developer **still in development** and not yet charging users or distributing data, you can build on the [Starter Plan](starter) until you're ready to launch. Once you go live with a commercial product, the Commercial Plan is your next step. See our guide on [how stock market data licensing works](https://www.marketdata.app/education/stocks/stock-market-data-licensing) for background. + +### Not Suitable for Trading + +The Commercial Plan is **not suitable for trading applications** or any product where users make execution decisions based on the data. SmartMid is a derived midpoint price — useful for display, charting, and informational purposes — but it is not a live bid/ask quote and should not be used to determine trade prices, route orders, or trigger execution logic. Trading platforms and order-routing tools require real-time exchange data, which means direct exchange licensing. [Contact our sales team](https://www.marketdata.app/contact/) to discuss licensing options for trading use cases. + +## Redistribution Rules + +The Commercial Plan permits you to **distribute your own derived data** through your application — including via your own API — but does **not** permit redistributing Market Data's raw API responses. In other words: + +- ✅ You can build a UI, dashboard, or product that displays Market Data's data to your users. +- ✅ You can compute derived metrics, signals, or aggregates from our data and serve those to your users (including over your own API). +- ❌ You cannot resell or proxy our raw API responses to your customers. + +If you're looking to offer a market data API to other businesses, you'll need direct exchange licensing — which the Commercial Plan does not provide. [Contact our sales team](https://www.marketdata.app/contact/) if you'd like to discuss your use case. + +## Why SmartMid Instead of Exchange Real-Time? + +Real-time data sourced directly from exchanges has its own [licensing requirements](https://www.marketdata.app/education/stocks/stock-market-data-licensing) that apply on top of any data subscription. By using SmartMid for real-time prices, the Commercial Plan keeps the cost of launching low — most businesses don't need exchange licensing on day one. + +**SmartMid** is our own proprietary derived midpoint price. Because it's a Market Data product rather than exchange data, your application can deliver SmartMid prices to any end user under our Commercial Use Addendum. + +When your business is ready to add real-time exchange data — bid/ask quotes, intraday candles, or real-time options — exchange licensing can be added on top of your Commercial Plan. [Contact our sales team](https://www.marketdata.app/contact/) when you're ready. + +## Comparison with Other Plans + +| Feature | Starter | Trader | Prime | **Commercial** | +|-------------------------|---------------|---------------|---------------|--------------------------| +| Price | $30/mo | $75/mo | $250/mo | **$250/mo** | +| Commercial Use License | ❌ | ❌ | ❌ | ✅ | +| Daily API Credits | 10,000/day | 100,000/day | Unlimited | Unlimited | +| Per-Minute Rate Limit | No Limit | No Limit | 100,000 | 100,000 | +| Concurrent Requests | 50 | 50 | 50 | 50 | +| Real-Time Stock Prices | Exchange data | Exchange data | Exchange data | SmartMid (derived) | +| Real-Time Exchange Data | ✅ | ✅ | ✅ | ❌ (T+1 historical only) | +| Real-Time Options | 15-min delay | Real-time | Real-time | ❌ (T+1 historical only) | +| Premium Endpoints | ✅ | ✅ | ✅ | ✅ | +| Historical Data | 5 Years | Full Access | Full Access | Full Access (T+1) | + +## Why Choose the Commercial Plan? + +The Commercial Plan is where most businesses start. At **$250/month**, it's the cheapest commercially-licensed market data subscription on the market — and it's enough to launch a real product, with real-time SmartMid stock prices, full historical depth, no daily credit caps, and generous concurrency. Add exchange licensing later, when your business is ready for it. + +The Commercial Plan is sold through our sales team. [Contact us](https://www.marketdata.app/contact/) to get started or to discuss your specific use case. diff --git a/account/plans/free-forever.md b/account/plans/free-forever.md index 1d27a62e..b9194df0 100644 --- a/account/plans/free-forever.md +++ b/account/plans/free-forever.md @@ -37,6 +37,8 @@ To help you better understand the differences between our plans, here's a quick | Standard Endpoints | ✅ | ✅ | | Premium Endpoints | ❌ | ✅ | +For endpoint-level freshness rules and the Delayed → Historical rollover timing (especially for options, where data only rolls at 9:30 AM ET the next trading day), see [Data Freshness](../data-freshness.md). + ## Why Choose Free Forever? The Free Forever plan is an excellent choice for those new to Market Data or for projects that are in the early stages of development. It allows you to test our services and understand how our data can enhance your projects without any financial commitment. Plus, you can upgrade to a paid plan anytime as your needs grow. diff --git a/account/plans/prime.md b/account/plans/prime.md index d808b50b..ff308d8b 100644 --- a/account/plans/prime.md +++ b/account/plans/prime.md @@ -37,6 +37,8 @@ Here's how the Prime Plan compares to our other offerings: | Premium Endpoints | ❌ | ✅ | ✅ | ✅ + Custom | ✅ + Custom | | Historical Data | 1 Year | 5 Years | Full Access | Full Access | Full Access | +For endpoint-level freshness rules, see [Data Freshness](../data-freshness.md). + ## Why Choose the Prime Plan? The Prime Plan is the ultimate choice for high-powered traders and investors looking for the most comprehensive financial data solution. With its no-limit approach to data access, real-time information, and the ability to customize features, it provides the tools necessary for sophisticated analysis, trading, and decision-making at scale. diff --git a/account/plans/quant.md b/account/plans/quant.md index 5ef4c41e..cdeea8e0 100644 --- a/account/plans/quant.md +++ b/account/plans/quant.md @@ -37,6 +37,8 @@ Here's how the Quant Plan compares to our other offerings: | Premium Endpoints | ❌ | ✅ | ✅ | ✅ | ✅ | | Historical Data | 1 Year | 5 Years | Full Access | Full Access | Full Access | +For endpoint-level freshness rules, see [Data Freshness](../data-freshness.md). + ## Why Choose the Quant Plan? The Quant Plan is the ultimate choice for high-powered traders and investors looking for the most comprehensive financial data solution. With its no-limit approach to data access, real-time information, and the ability to customize features, it provides the tools necessary for sophisticated analysis, trading, and decision-making at scale. diff --git a/account/plans/starter-trial.md b/account/plans/starter-trial.md index e08e9cff..fe4d18af 100644 --- a/account/plans/starter-trial.md +++ b/account/plans/starter-trial.md @@ -47,6 +47,8 @@ Here's a quick comparison to help you understand the Starter Trial in relation t | Standard Endpoints | ✅ | ✅ | ✅ | | Premium Endpoints | ❌ | AAPL Only | ✅ | +For endpoint-level freshness rules and the Delayed → Historical rollover timing (especially for options, where data only rolls at 9:30 AM ET the next trading day), see [Data Freshness](../data-freshness.md). + ## Why Take A Trail of the Starter Plan? The Starter Trial is an excellent opportunity to test the waters and see how the enhanced features can benefit your project. Whether you're scaling up or need more detailed financial data for analysis, this trial provides a risk-free way to explore our services. diff --git a/account/plans/starter.md b/account/plans/starter.md index f32928d8..cd19463b 100644 --- a/account/plans/starter.md +++ b/account/plans/starter.md @@ -39,6 +39,8 @@ To give you a clear idea of how the Starter Plan stands against our other offeri | Premium Endpoints | ❌ | ✅ | ✅ | | Historical Data | 1 Year | 5 Years | Full Access | +For endpoint-level freshness rules and the Delayed → Historical rollover timing (especially for options), see [Data Freshness](../data-freshness.md). + ## Why Choose the Starter Plan? The Starter Plan is perfect for users who have outgrown the Free Forever plan and need more robust data access and capabilities. Whether you're scaling up your project or need more detailed financial data for analysis, the Starter Plan provides the tools and data you need to succeed. diff --git a/account/plans/trader-trial.md b/account/plans/trader-trial.md index 4082b9a5..82d68466 100644 --- a/account/plans/trader-trial.md +++ b/account/plans/trader-trial.md @@ -49,6 +49,8 @@ This comparison helps you understand the Trader Trial in relation to our other o | Standard Endpoints | ✅ | ✅ | ✅ | | Premium Endpoints | ❌ | AAPL Only | ✅ | +For endpoint-level freshness rules and the Delayed → Historical rollover timing (especially for options, where data only rolls at 9:30 AM ET the next trading day), see [Data Freshness](../data-freshness.md). + ## Why Try the Trader Trial? The Trader Trial offers an unparalleled opportunity to experience the full depth and breadth of our financial data services tailored for high-volume users and professional traders. It's a risk-free way to determine if the Trader Plan's extensive data access and capabilities align with your project's needs. diff --git a/account/plans/trader.md b/account/plans/trader.md index e6aef603..7b3bfab1 100644 --- a/account/plans/trader.md +++ b/account/plans/trader.md @@ -38,6 +38,8 @@ Here's how the Trader Plan compares to our other offerings: | Premium Endpoints | ❌ | ✅ | ✅ | | Historical Data | 1 Year | 5 Years | Full Access | +For endpoint-level freshness rules, see [Data Freshness](../data-freshness.md). + ## Why Choose the Trader Plan? The Trader Plan is ideal for professional traders, high-volume users, and projects that require the most detailed and up-to-date financial data available. It's designed to provide the tools and data necessary for sophisticated analysis and decision-making. diff --git a/api/authentication.mdx b/api/authentication.mdx index cf3333ba..0f4077fd 100644 --- a/api/authentication.mdx +++ b/api/authentication.mdx @@ -8,6 +8,10 @@ import TabItem from "@theme/TabItem"; The Market Data API uses a **Bearer Token** for authentication. The token is a programmatic representation of your username and password credentials, so you must keep it secret just as you would your username and password. The token is required for each request you make to the API. +:::caution Accept HTTP 203 as success +Any endpoint may return HTTP `203 Non-Authoritative Information` instead of `200 OK` when the response is served from our caching tier. The body is identical in shape — treat 203 exactly the same as 200. Code samples below that use raw HTTP libraries (rather than our SDKs) must accept both status codes; checking only `status == 200` will silently drop valid responses. See [Troubleshooting](/troubleshooting) for the full list of status codes. +::: + ## Obtaining a Token To obtain it, sign-in to your customer dashboard using your username and password and request a token. It will be delivered by email to the address you used to sign-in. @@ -44,44 +48,47 @@ The curly braces around token are a placeholder for this example. Do not actuall ::: - - -```javascript -const https = require('https'); - -// Your token -const token = 'your_token_here'; - -// The API endpoint for retrieving stock quotes for SPY -const url = 'https://api.marketdata.app/v1/stocks/quotes/SPY/'; - -// Making the GET request to the API -https.get(url, { - headers: { - 'Accept': 'application/json', - 'Authorization': `Bearer ${token}` - } -}, (response) => { - let data = ''; - - // A chunk of data has been received. - response.on('data', (chunk) => { - data += chunk; - }); - - // The whole response has been received. Print out the result. - response.on('end', () => { - if (response.statusCode === 200 || response.statusCode === 203) { - console.log(JSON.parse(data)); - } else { - console.log(`Failed to retrieve data: ${response.statusCode}`); - } - }); -}).on("error", (err) => { - console.log("Error: " + err.message); -}); + + +```js title="app.js" +import { MarketDataClient } from "@marketdata/sdk"; + +// The SDK reads your token from the MARKETDATA_TOKEN environment variable. +// Alternatively, pass it directly: new MarketDataClient({ token: "your_token_here" }) +const client = new MarketDataClient(); + +try { + const quotes = await client.stocks.quotes("SPY"); + console.log(quotes); +} catch (error) { + console.error(error); +} +``` + +For more information about using the JavaScript SDK, see our [JavaScript SDK documentation](/sdk/js) and [authentication guide](/sdk/js/authentication). + + + + + +```typescript title="app.ts" +import { MarketDataClient } from "@marketdata/sdk"; +import type { StockQuote } from "@marketdata/sdk"; + +// The SDK reads your token from the MARKETDATA_TOKEN environment variable. +// Alternatively, pass it directly: new MarketDataClient({ token: "your_token_here" }) +const client = new MarketDataClient(); + +try { + const quotes: StockQuote[] = await client.stocks.quotes("SPY"); + console.log(quotes); +} catch (error) { + console.error(error); +} ``` +For more information about using the JavaScript SDK, see our [JavaScript SDK documentation](/sdk/js) and [authentication guide](/sdk/js/authentication). + diff --git a/api/funds/candles.mdx b/api/funds/candles.mdx index 08b9a68f..83da93d8 100644 --- a/api/funds/candles.mdx +++ b/api/funds/candles.mdx @@ -30,19 +30,42 @@ GET **GET** [https://api.marketdata.app/v1/funds/candles/D/VFINX/?from=2020-01-01&to=2020-01-10](https://api.marketdata.app/v1/funds/candles/D/VFINX/?from=2020-01-01&to=2020-01-10) - + ```js title="fundCandles.js" -fetch( - "https://api.marketdata.app/v1/funds/candles/D/VFINX/?from=2020-01-01&to=2020-01-10" -) - .then((res) => { - console.log(res); - }) - .catch((err) => { - console.log(err); - }); +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + // VFINX is available without authentication (free test symbol). + const candles = await client.funds.candles("VFINX", { countback: 30 }); + for (const c of candles) { + console.log(`t=${c.t} o=${c.o} h=${c.h} l=${c.l} c=${c.c}`); + } +} catch (error) { + console.error(error); +} +``` + + + +```typescript title="fundCandles.ts" +import { MarketDataClient } from "@marketdata/sdk"; +import type { FundsCandle } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + // VFINX is available without authentication (free test symbol). + const candles: FundsCandle[] = await client.funds.candles("VFINX", { countback: 30 }); + for (const c of candles) { + console.log(`t=${c.t} o=${c.o} h=${c.h} l=${c.l} c=${c.c}`); + } +} catch (error) { + console.error(error); +} ``` diff --git a/api/index.md b/api/index.md index f36ab9d0..a1300678 100644 --- a/api/index.md +++ b/api/index.md @@ -10,6 +10,12 @@ The Market Data API is designed around REST and supports standard HTTP response https://api.marketdata.app/ ::: +:::caution Accept HTTP 203 as success +Any endpoint may return HTTP `203 Non-Authoritative Information` instead of `200 OK` when the response is served from our caching tier. This is normal — the body is identical in shape, and you should treat 203 exactly the same as 200. Many HTTP clients (and many OpenAPI-generated SDKs) default to treating only 200 as success; you must update your client to also accept 203 or your integration will silently fail in production. + +A `204 No Content` response can also occur when `mode=cached` is requested and no cached data is available — see [Data Mode](/api/universal-parameters/mode) and [Troubleshooting](/troubleshooting) for the full list of status codes. +::: + ## Try Our API The easiest way to try out our API is using our [Swagger User Interface](https://api.marketdata.app/), which will allow you to try out your API requests directly from your browser. diff --git a/api/markets/status.mdx b/api/markets/status.mdx index 11f25248..408dc2d1 100644 --- a/api/markets/status.mdx +++ b/api/markets/status.mdx @@ -26,26 +26,46 @@ GET **GET** [https://api.marketdata.app/v1/markets/status/?date=yesterday](https://api.marketdata.app/v1/markets/status/?date=yesterday) - + ```js title="app.js" -fetch( - "https://api.marketdata.app/v1/markets/status/?from=2020-01-01&to=2020-12-31" -) - .then((res) => { - console.log(res); - }) - .catch((err) => { - console.log(err); - }); - -fetch("https://api.marketdata.app/v1/markets/status/?date=yesterday") - .then((res) => { - console.log(res); - }) - .catch((err) => { - console.log(err); - }); +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const days = await client.markets.status({ from: "2020-01-01", to: "2020-12-31" }); + for (const d of days) { + console.log(`date=${d.date} status=${d.status}`); + } + + const yesterday = await client.markets.status({ date: "yesterday" }); + console.log(yesterday[0]); +} catch (error) { + console.error(error); +} +``` + + + + +```typescript title="app.ts" +import { MarketDataClient } from "@marketdata/sdk"; +import type { MarketStatusResponse } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const days: MarketStatusResponse = await client.markets.status({ from: "2020-01-01", to: "2020-12-31" }); + for (const d of days) { + console.log(`date=${d.date} status=${d.status}`); + } + + const yesterday: MarketStatusResponse = await client.markets.status({ date: "yesterday" }); + console.log(yesterday[0]); +} catch (error) { + console.error(error); +} ``` diff --git a/api/options/chain.mdx b/api/options/chain.mdx index 24de1868..f7db1f76 100644 --- a/api/options/chain.mdx +++ b/api/options/chain.mdx @@ -22,16 +22,36 @@ GET https://api.marketdata.app/v1/options/chain/{underlyingSymbol}/ **GET** [https://api.marketdata.app/v1/options/chain/AAPL/?expiration=2025-01-17&side=call](https://api.marketdata.app/v1/options/chain/AAPL/?expiration=2025-01-17&side=call) - + ```js title="app.js" -fetch("https://api.marketdata.app/v1/options/chain/AAPL/") - .then((res) => { - console.log(res); - }) - .catch((err) => { - console.log(err); - }); +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const contracts = await client.options.chain("AAPL"); + console.log(`Got ${contracts.length} contracts`); +} catch (error) { + console.error(error); +} +``` + + + + +```typescript title="app.ts" +import { MarketDataClient } from "@marketdata/sdk"; +import type { OptionsChain } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const contracts: OptionsChain[] = await client.options.chain("AAPL"); + console.log(`Got ${contracts.length} contracts`); +} catch (error) { + console.error(error); +} ``` diff --git a/api/options/expirations.mdx b/api/options/expirations.mdx index ad492e09..7e30e518 100644 --- a/api/options/expirations.mdx +++ b/api/options/expirations.mdx @@ -24,16 +24,42 @@ GET **GET** [https://api.marketdata.app/v1/options/expirations/AAPL/](https://api.marketdata.app/v1/options/expirations/AAPL/) - + ```js title="app.js" -fetch("https://api.marketdata.app/v1/options/expirations/AAPL/") - .then((res) => { - console.log(res); - }) - .catch((err) => { - console.log(err); - }); +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const data = await client.options.expirations("AAPL"); + console.log(`Updated: ${data.updated}`); + for (const exp of data.expirations) { + console.log(exp); + } +} catch (error) { + console.error(error); +} +``` + + + + +```typescript title="app.ts" +import { MarketDataClient } from "@marketdata/sdk"; +import type { OptionsExpirationsResponse } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const data: OptionsExpirationsResponse = await client.options.expirations("AAPL"); + console.log(`Updated: ${data.updated}`); + for (const exp of data.expirations) { + console.log(exp); + } +} catch (error) { + console.error(error); +} ``` diff --git a/api/options/lookup.mdx b/api/options/lookup.mdx index 57d8213a..8cd8b892 100644 --- a/api/options/lookup.mdx +++ b/api/options/lookup.mdx @@ -24,18 +24,36 @@ GET **GET** [https://api.marketdata.app/v1/options/lookup/AAPL%207/28/2023%20200%20Call/](https://api.marketdata.app/v1/options/lookup/AAPL%207/28/2023%20200%20Call/) - + ```js title="app.js" -fetch( - "https://api.marketdata.app/v1/options/lookup/AAPL%207/28/2023%20200%20Call/" -) - .then((res) => { - console.log(res); - }) - .catch((err) => { - console.log(err); - }); +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const data = await client.options.lookup("AAPL 7/28/2023 200 Call"); + console.log(data.optionSymbol); // "AAPL230728C00200000" +} catch (error) { + console.error(error); +} +``` + + + + +```typescript title="app.ts" +import { MarketDataClient } from "@marketdata/sdk"; +import type { OptionsLookupResponse } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const data: OptionsLookupResponse = await client.options.lookup("AAPL 7/28/2023 200 Call"); + console.log(data.optionSymbol); // "AAPL230728C00200000" +} catch (error) { + console.error(error); +} ``` diff --git a/api/options/quotes.mdx b/api/options/quotes.mdx index 23f756d7..43dd4f02 100644 --- a/api/options/quotes.mdx +++ b/api/options/quotes.mdx @@ -28,16 +28,42 @@ GET **GET** [https://api.marketdata.app/v1/options/quotes/AAPL271217C00250000/](https://api.marketdata.app/v1/options/quotes/AAPL271217C00250000/) - + ```js title="app.js" -fetch("https://api.marketdata.app/v1/options/quotes/AAPL271217C00250000/") - .then((res) => { - console.log(res); - }) - .catch((err) => { - console.log(err); - }); +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const quotes = await client.options.quotes("AAPL271217C00250000"); + const q = quotes[0]; + console.log( + `${q.optionSymbol}: bid=${q.bid} ask=${q.ask} delta=${q.delta}` + ); +} catch (error) { + console.error(error); +} +``` + + + + +```typescript title="app.ts" +import { MarketDataClient } from "@marketdata/sdk"; +import type { OptionsQuotes } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const quotes: OptionsQuotes[] = await client.options.quotes("AAPL271217C00250000"); + const q = quotes[0]; + console.log( + `${q.optionSymbol}: bid=${q.bid} ask=${q.ask} delta=${q.delta}` + ); +} catch (error) { + console.error(error); +} ``` diff --git a/api/stocks/bulkcandles.mdx b/api/stocks/bulkcandles.mdx index 902ac1be..15653446 100644 --- a/api/stocks/bulkcandles.mdx +++ b/api/stocks/bulkcandles.mdx @@ -24,20 +24,51 @@ GET **GET** [https://api.marketdata.app/v1/stocks/bulkcandles/D/?symbols=AAPL,META,MSFT](https://api.marketdata.app/v1/stocks/bulkcandles/D/?symbols=AAPL,META,MSFT) - + + +:::note +The JavaScript/TypeScript SDK does not yet provide a method for the bulk candles endpoint, so these examples use the raw `fetch()` API. They will be replaced with `@marketdata/sdk` examples once bulk support is added. +::: ```js title="app.js" fetch( "https://api.marketdata.app/v1/stocks/bulkcandles/D/?symbols=AAPL,META,MSFT" ) - .then((res) => { - console.log(res); + .then((res) => res.json()) + .then((data) => { + console.log(data); }) .catch((err) => { console.log(err); }); ``` + + + +:::note +The JavaScript/TypeScript SDK does not yet provide a method for the bulk candles endpoint, so these examples use the raw `fetch()` API. They will be replaced with `@marketdata/sdk` examples once bulk support is added. +::: + +```typescript title="app.ts" +interface BulkCandlesResponse { + s: string; + symbol: string[]; + o: number[]; + h: number[]; + l: number[]; + c: number[]; + v: number[]; + t: number[]; +} + +const response: Response = await fetch( + "https://api.marketdata.app/v1/stocks/bulkcandles/D/?symbols=AAPL,META,MSFT" +); +const data: BulkCandlesResponse = await response.json(); +console.log(data); +``` + diff --git a/api/stocks/candles.mdx b/api/stocks/candles.mdx index 4687da93..6f1d209b 100644 --- a/api/stocks/candles.mdx +++ b/api/stocks/candles.mdx @@ -24,19 +24,42 @@ GET **GET** [https://api.marketdata.app/v1/stocks/candles/D/AAPL/?from=2020-01-01&to=2020-12-31](https://api.marketdata.app/v1/stocks/candles/D/AAPL/?from=2020-01-01&to=2020-12-31) - + ```js title="app.js" -fetch( - "https://api.marketdata.app/v1/stocks/candles/D/AAPL/?from=2020-01-01&to=2020-12-31" -) - .then((res) => { - console.log(res); - }) - .catch((err) => { - console.log(err); - }); +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + // Default resolution is "D" (daily) + const candles = await client.stocks.candles("AAPL", { countback: 30 }); + for (const c of candles) { + console.log(`t=${c.t} o=${c.o} h=${c.h} l=${c.l} c=${c.c} v=${c.v}`); + } +} catch (error) { + console.error(error); +} +``` + + + +```typescript title="app.ts" +import { MarketDataClient } from "@marketdata/sdk"; +import type { StockCandle } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + // Default resolution is "D" (daily) + const candles: StockCandle[] = await client.stocks.candles("AAPL", { countback: 30 }); + for (const c of candles) { + console.log(`t=${c.t} o=${c.o} h=${c.h} l=${c.l} c=${c.c} v=${c.v}`); + } +} catch (error) { + console.error(error); +} ``` diff --git a/api/stocks/earnings.mdx b/api/stocks/earnings.mdx index 2e015750..357d9230 100644 --- a/api/stocks/earnings.mdx +++ b/api/stocks/earnings.mdx @@ -29,16 +29,46 @@ GET **GET** [https://api.marketdata.app/v1/stocks/earnings/AAPL/](https://api.marketdata.app/v1/stocks/earnings/AAPL/) - + ```js title="app.js" -fetch("https://api.marketdata.app/v1/stocks/earnings/AAPL/") - .then((res) => { - console.log(res); - }) - .catch((err) => { - console.log(err); - }); +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const earnings = await client.stocks.earnings("AAPL"); + for (const e of earnings) { + console.log( + `FY${e.fiscalYear} Q${e.fiscalQuarter}: ` + + `reported=${e.reportedEPS} estimated=${e.estimatedEPS}` + ); + } +} catch (error) { + console.error(error); +} +``` + + + + +```typescript title="app.ts" +import { MarketDataClient } from "@marketdata/sdk"; +import type { StockEarningsResponse } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const earnings: StockEarningsResponse = await client.stocks.earnings("AAPL"); + for (const e of earnings) { + console.log( + `FY${e.fiscalYear} Q${e.fiscalQuarter}: ` + + `reported=${e.reportedEPS} estimated=${e.estimatedEPS}` + ); + } +} catch (error) { + console.error(error); +} ``` diff --git a/api/stocks/news.mdx b/api/stocks/news.mdx index 37e34f63..cc2381e1 100644 --- a/api/stocks/news.mdx +++ b/api/stocks/news.mdx @@ -33,16 +33,40 @@ GET **GET** [https://api.marketdata.app/v1/stocks/news/AAPL/](https://api.marketdata.app/v1/stocks/news/AAPL/) - + ```js title="app.js" -fetch("https://api.marketdata.app/v1/stocks/news/AAPL/") - .then((res) => { - console.log(res); - }) - .catch((err) => { - console.log(err); - }); +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const articles = await client.stocks.news("AAPL"); + for (const a of articles) { + console.log(`${a.source}: ${a.headline}`); + } +} catch (error) { + console.error(error); +} +``` + + + + +```typescript title="app.ts" +import { MarketDataClient } from "@marketdata/sdk"; +import type { StockNews } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const articles: StockNews[] = await client.stocks.news("AAPL"); + for (const a of articles) { + console.log(`${a.source}: ${a.headline}`); + } +} catch (error) { + console.error(error); +} ``` diff --git a/api/stocks/prices.mdx b/api/stocks/prices.mdx index 0e4edec9..b399af4c 100644 --- a/api/stocks/prices.mdx +++ b/api/stocks/prices.mdx @@ -36,16 +36,36 @@ GET **GET** [https://api.marketdata.app/v1/stocks/prices/AAPL/](https://api.marketdata.app/v1/stocks/prices/AAPL/) - + ```js title="app.js" -fetch("https://api.marketdata.app/v1/stocks/prices/AAPL/") - .then((res) => { - console.log(res); - }) - .catch((err) => { - console.log(err); - }); +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const prices = await client.stocks.prices("AAPL"); + console.log(prices[0].mid); +} catch (error) { + console.error(error); +} +``` + + + + +```typescript title="app.ts" +import { MarketDataClient } from "@marketdata/sdk"; +import type { StockPrice } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const prices: StockPrice[] = await client.stocks.prices("AAPL"); + console.log(prices[0].mid); +} catch (error) { + console.error(error); +} ``` @@ -83,16 +103,40 @@ echo $prices; **GET** [https://api.marketdata.app/v1/stocks/prices/?symbols=AAPL,META,MSFT](https://api.marketdata.app/v1/stocks/prices/?symbols=AAPL,META,MSFT) - + ```js title="app.js" -fetch("https://api.marketdata.app/v1/stocks/prices/?symbols=AAPL,META,MSFT") - .then((res) => { - console.log(res); - }) - .catch((err) => { - console.log(err); - }); +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const prices = await client.stocks.prices(["AAPL", "META", "MSFT"]); + for (const p of prices) { + console.log(`${p.symbol}: mid=${p.mid}`); + } +} catch (error) { + console.error(error); +} +``` + + + + +```typescript title="app.ts" +import { MarketDataClient } from "@marketdata/sdk"; +import type { StockPrice } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const prices: StockPrice[] = await client.stocks.prices(["AAPL", "META", "MSFT"]); + for (const p of prices) { + console.log(`${p.symbol}: mid=${p.mid}`); + } +} catch (error) { + console.error(error); +} ``` diff --git a/api/stocks/quotes.mdx b/api/stocks/quotes.mdx index d6b4ecb9..4185986c 100644 --- a/api/stocks/quotes.mdx +++ b/api/stocks/quotes.mdx @@ -32,16 +32,36 @@ GET **GET** [https://api.marketdata.app/v1/stocks/quotes/AAPL/](https://api.marketdata.app/v1/stocks/quotes/AAPL/) - + ```js title="app.js" -fetch("https://api.marketdata.app/v1/stocks/quotes/AAPL/") - .then((res) => { - console.log(res); - }) - .catch((err) => { - console.log(err); - }); +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const quotes = await client.stocks.quotes("AAPL"); + console.log(quotes); +} catch (error) { + console.error(error); +} +``` + + + + +```typescript title="app.ts" +import { MarketDataClient } from "@marketdata/sdk"; +import type { StockQuote } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const quotes: StockQuote[] = await client.stocks.quotes("AAPL"); + console.log(quotes); +} catch (error) { + console.error(error); +} ``` @@ -102,16 +122,40 @@ echo $quote; **GET** [https://api.marketdata.app/v1/stocks/quotes/?symbols=AAPL,META,MSFT](https://api.marketdata.app/v1/stocks/quotes/?symbols=AAPL,META,MSFT) - + ```js title="app.js" -fetch("https://api.marketdata.app/v1/stocks/quotes/?symbols=AAPL,META,MSFT") - .then((res) => { - console.log(res); - }) - .catch((err) => { - console.log(err); - }); +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const quotes = await client.stocks.quotes(["AAPL", "META", "MSFT"]); + for (const q of quotes) { + console.log(`${q.symbol}: bid=${q.bid}, ask=${q.ask}, last=${q.last}`); + } +} catch (error) { + console.error(error); +} +``` + + + + +```typescript title="app.ts" +import { MarketDataClient } from "@marketdata/sdk"; +import type { StockQuote } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const quotes: StockQuote[] = await client.stocks.quotes(["AAPL", "META", "MSFT"]); + for (const q of quotes) { + console.log(`${q.symbol}: bid=${q.bid}, ask=${q.ask}, last=${q.last}`); + } +} catch (error) { + console.error(error); +} ``` diff --git a/api/troubleshooting/logging.mdx b/api/troubleshooting/logging.mdx index 5180a26e..19265a0b 100644 --- a/api/troubleshooting/logging.mdx +++ b/api/troubleshooting/logging.mdx @@ -19,29 +19,48 @@ When logging errors, the log should include the exact request made, Market Data' ## Logging Examples - + ```js title="logger.js" -const axios = require("axios"); -const fs = require("fs"); - -// Make the API request -axios - .get("https://api.marketdata.app/v1/your_endpoint_here/") - .then((response) => { - // Do nothing for successful responses - }) - .catch((error) => { - if (error.response.status !== 200 && error.response.status !== 203) { - const logData = { - request: error.config.method.toUpperCase() + " " + error.config.url, - response: error.response.data, - cfRayHeader: error.response.headers["cf-ray"] || "Not available", - }; - // Save to a logfile - fs.appendFileSync("api_error_log.json", JSON.stringify(logData) + "\n"); - } - }); +import { MarketDataClient, DefaultLogger, LogLevel } from "@marketdata/sdk"; + +// Enable DEBUG-level logging to capture full request/response diagnostics +// (including the CF-Ray header) for every API call. +const client = new MarketDataClient({ + logger: new DefaultLogger(LogLevel.DEBUG), +}); + +try { + const quotes = await client.stocks.quotes("AAPL"); + console.log(quotes); +} catch (error) { + // The SDK logs the failing request and response automatically; + // handle the error here as needed. + console.error(error); +} +``` + + + + +```typescript title="logger.ts" +import { MarketDataClient, DefaultLogger, LogLevel } from "@marketdata/sdk"; +import type { StockQuote } from "@marketdata/sdk"; + +// Enable DEBUG-level logging to capture full request/response diagnostics +// (including the CF-Ray header) for every API call. +const client = new MarketDataClient({ + logger: new DefaultLogger(LogLevel.DEBUG), +}); + +try { + const quotes: StockQuote[] = await client.stocks.quotes("AAPL"); + console.log(quotes); +} catch (error) { + // The SDK logs the failing request and response automatically; + // handle the error here as needed. + console.error(error); +} ``` diff --git a/api/troubleshooting/real-time-data.mdx b/api/troubleshooting/real-time-data.mdx index d9934470..d17e1f38 100644 --- a/api/troubleshooting/real-time-data.mdx +++ b/api/troubleshooting/real-time-data.mdx @@ -73,42 +73,81 @@ elif isinstance(quotes, dict) and quotes.get("s") == "ok" and "updated" in quote ``` - - -```javascript -const response = await fetch( - "https://api.marketdata.app/v1/stocks/quotes/AAPL/", - { - headers: { - "Authorization": "Bearer YOUR_TOKEN" + + +```js title="checkDataAge.js" +import { MarketDataClient, Mode } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + // Request live data, then inspect the `updated` timestamp. + const quotes = await client.stocks.quotes("AAPL", { mode: Mode.LIVE }); + const quote = quotes[0]; + + if (quote?.updated) { + // `updated` is a Unix timestamp in seconds. + const updatedTime = new Date(quote.updated * 1000); + const currentTime = new Date(); + + // Calculate the age of the data + const ageMinutes = (currentTime - updatedTime) / 1000 / 60; + + console.log(`Data timestamp: ${updatedTime}`); + console.log(`Current time: ${currentTime}`); + console.log(`Data age: ${ageMinutes.toFixed(1)} minutes`); + + // Determine data type + if (ageMinutes < 1) { + console.log("✅ Real-time data (less than 1 minute old)"); + } else if (ageMinutes < 20) { + console.log("⚠️ 15-minute delayed data"); + } else { + console.log("❌ Historical data (1 day old or more)"); } } -); - -const data = await response.json(); - -if (data.s === "ok" && data.updated) { - // Get the timestamp (Unix epoch seconds) - const updatedTimestamp = data.updated[0] * 1000; // Convert to milliseconds - const updatedTime = new Date(updatedTimestamp); - const currentTime = new Date(); - - // Calculate the age of the data - const ageSeconds = (currentTime - updatedTime) / 1000; - const ageMinutes = ageSeconds / 60; - - console.log(`Data timestamp: ${updatedTime}`); - console.log(`Current time: ${currentTime}`); - console.log(`Data age: ${ageMinutes.toFixed(1)} minutes`); - - // Determine data type - if (ageMinutes < 1) { - console.log("✅ Real-time data (less than 1 minute old)"); - } else if (ageMinutes < 20) { - console.log("⚠️ 15-minute delayed data"); - } else { - console.log("❌ Historical data (1 day old or more)"); +} catch (error) { + console.error(error); +} +``` + + + + +```typescript title="checkDataAge.ts" +import { MarketDataClient, Mode } from "@marketdata/sdk"; +import type { StockQuote } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + // Request live data, then inspect the `updated` timestamp. + const quotes: StockQuote[] = await client.stocks.quotes("AAPL", { mode: Mode.LIVE }); + const quote = quotes[0]; + + if (quote?.updated) { + // `updated` is a Unix timestamp in seconds. + const updatedTime = new Date(quote.updated * 1000); + const currentTime = new Date(); + + // Calculate the age of the data + const ageMinutes = (currentTime.getTime() - updatedTime.getTime()) / 1000 / 60; + + console.log(`Data timestamp: ${updatedTime}`); + console.log(`Current time: ${currentTime}`); + console.log(`Data age: ${ageMinutes.toFixed(1)} minutes`); + + // Determine data type + if (ageMinutes < 1) { + console.log("✅ Real-time data (less than 1 minute old)"); + } else if (ageMinutes < 20) { + console.log("⚠️ 15-minute delayed data"); + } else { + console.log("❌ Historical data (1 day old or more)"); + } } +} catch (error) { + console.error(error); } ``` @@ -342,56 +381,104 @@ test_realtime_data("AAPL") ``` - + + +```js title="testRealtimeData.js" +import { MarketDataClient, Mode } from "@marketdata/sdk"; + +const client = new MarketDataClient(); -```javascript async function testRealtimeData(symbol = "AAPL") { - const response = await fetch( - `https://api.marketdata.app/v1/stocks/quotes/${symbol}/`, - { - headers: { - "Authorization": "Bearer YOUR_TOKEN" - } + try { + const quotes = await client.stocks.quotes(symbol, { mode: Mode.LIVE }); + const quote = quotes[0]; + + if (!quote?.updated) { + console.log("Warning: No timestamp in response"); + return false; } - ); - - const data = await response.json(); - - if (data.s !== "ok") { - console.log(`Error: ${data.errmsg || "Unknown error"}`); - return false; - } - - if (!data.updated) { - console.log("Warning: No timestamp in response"); + + // `updated` is a Unix timestamp in seconds. + const updatedTime = new Date(quote.updated * 1000); + const currentTime = new Date(); + + // Calculate age + const ageMinutes = (currentTime - updatedTime) / 1000 / 60; + + console.log(`Symbol: ${symbol}`); + console.log(`Data timestamp: ${updatedTime.toISOString()}`); + console.log(`Current time: ${currentTime.toISOString()}`); + console.log(`Data age: ${ageMinutes.toFixed(2)} minutes`); + + // Check if real-time (less than 1 minute old during market hours) + if (ageMinutes < 1) { + console.log("✅ Real-time data is working!"); + return true; + } else if (ageMinutes < 20) { + console.log("⚠️ Receiving 15-minute delayed data"); + console.log(" → Check if IEX agreement is signed and approved"); + return false; + } else { + console.log("❌ Receiving historical data (1 day old)"); + console.log(" → Check if profile is complete and agreements are signed"); + return false; + } + } catch (error) { + console.error(`Error: ${error.message}`); return false; } - - // Get timestamp - const updatedTimestamp = data.updated[0] * 1000; // Convert to milliseconds - const updatedTime = new Date(updatedTimestamp); - const currentTime = new Date(); - - // Calculate age - const ageSeconds = (currentTime - updatedTime) / 1000; - const ageMinutes = ageSeconds / 60; - - console.log(`Symbol: ${symbol}`); - console.log(`Data timestamp: ${updatedTime.toISOString()}`); - console.log(`Current time: ${currentTime.toISOString()}`); - console.log(`Data age: ${ageMinutes.toFixed(2)} minutes`); - - // Check if real-time (less than 1 minute old during market hours) - if (ageMinutes < 1) { - console.log("✅ Real-time data is working!"); - return true; - } else if (ageMinutes < 20) { - console.log("⚠️ Receiving 15-minute delayed data"); - console.log(" → Check if IEX agreement is signed and approved"); - return false; - } else { - console.log("❌ Receiving historical data (1 day old)"); - console.log(" → Check if profile is complete and agreements are signed"); +} + +// Test during market hours for best results +testRealtimeData("AAPL"); +``` + + + + +```typescript title="testRealtimeData.ts" +import { MarketDataClient, Mode } from "@marketdata/sdk"; +import type { StockQuote } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +async function testRealtimeData(symbol: string = "AAPL"): Promise { + try { + const quotes: StockQuote[] = await client.stocks.quotes(symbol, { mode: Mode.LIVE }); + const quote = quotes[0]; + + if (!quote?.updated) { + console.log("Warning: No timestamp in response"); + return false; + } + + // `updated` is a Unix timestamp in seconds. + const updatedTime = new Date(quote.updated * 1000); + const currentTime = new Date(); + + // Calculate age + const ageMinutes = (currentTime.getTime() - updatedTime.getTime()) / 1000 / 60; + + console.log(`Symbol: ${symbol}`); + console.log(`Data timestamp: ${updatedTime.toISOString()}`); + console.log(`Current time: ${currentTime.toISOString()}`); + console.log(`Data age: ${ageMinutes.toFixed(2)} minutes`); + + // Check if real-time (less than 1 minute old during market hours) + if (ageMinutes < 1) { + console.log("✅ Real-time data is working!"); + return true; + } else if (ageMinutes < 20) { + console.log("⚠️ Receiving 15-minute delayed data"); + console.log(" → Check if IEX agreement is signed and approved"); + return false; + } else { + console.log("❌ Receiving historical data (1 day old)"); + console.log(" → Check if profile is complete and agreements are signed"); + return false; + } + } catch (error) { + console.error(`Error: ${(error as Error).message}`); return false; } } diff --git a/api/troubleshooting/service-outages.mdx b/api/troubleshooting/service-outages.mdx index 706d05b0..e95d7362 100644 --- a/api/troubleshooting/service-outages.mdx +++ b/api/troubleshooting/service-outages.mdx @@ -33,50 +33,74 @@ This endpoint is ideal to allow for automatic switching between Market Data and ::: - + ```js title="status.js" -// Importing the required library -const axios = require('axios'); - -// URL to the status endpoint -const url = "https://api.marketdata.app/status/"; - -// Function to check the status of a specific endpoint -async function checkEndpointStatus(endpointPath) { - try { - const response = await axios.get(url); - const jsonData = response.data; - const index = jsonData.service.indexOf(endpointPath); - - if (index !== -1) { - return { - online: jsonData.online[index], - status: jsonData.status[index], - uptime30d: jsonData.uptimePct30d[index], - uptime90d: jsonData.uptimePct90d[index] - }; - } else { - return null; - } - } catch (error) { - console.error("Error fetching API status:", error); - return null; - } +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +// Look up the status of a specific endpoint in the API status response. +function findEndpointStatus(status, endpointPath) { + const index = status.service.indexOf(endpointPath); + if (index === -1) return null; + return { + online: status.online[index], + status: status.status[index], + uptime30d: status.uptimePct30d?.[index], + uptime90d: status.uptimePct90d?.[index], + }; } -// Checking the status of specific endpoints -async function checkStatuses() { - const stocksQuotes = await checkEndpointStatus("/v1/stocks/quotes/"); - const optionsChain = await checkEndpointStatus("/v1/options/chain/"); - const stocksCandles = await checkEndpointStatus("/v1/stocks/candles/"); +try { + const status = await client.utilities.status(); + + const stocksQuotes = findEndpointStatus(status, "/v1/stocks/quotes/"); + const optionsChain = findEndpointStatus(status, "/v1/options/chain/"); + const stocksCandles = findEndpointStatus(status, "/v1/stocks/candles/"); - console.log(`Stocks Quotes: ${stocksQuotes ? (stocksQuotes.online ? "Online" : "Offline") : "Not found"}`); - console.log(`Options Chain: ${optionsChain ? (optionsChain.online ? "Online" : "Offline") : "Not found"}`); - console.log(`Stocks Candles: ${stocksCandles ? (stocksCandles.online ? "Online" : "Offline") : "Not found"}`); + console.log(`Stocks Quotes: ${stocksQuotes ? (stocksQuotes.online ? "Online" : "Offline") : "Not found"}`); + console.log(`Options Chain: ${optionsChain ? (optionsChain.online ? "Online" : "Offline") : "Not found"}`); + console.log(`Stocks Candles: ${stocksCandles ? (stocksCandles.online ? "Online" : "Offline") : "Not found"}`); +} catch (error) { + console.error("Error fetching API status:", error); } +``` -checkStatuses(); + + + +```typescript title="status.ts" +import { MarketDataClient } from "@marketdata/sdk"; +import type { ApiStatusResponse } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +// Look up the status of a specific endpoint in the API status response. +function findEndpointStatus(status: ApiStatusResponse, endpointPath: string) { + const index = status.service.indexOf(endpointPath); + if (index === -1) return null; + return { + online: status.online[index], + status: status.status[index], + uptime30d: status.uptimePct30d?.[index], + uptime90d: status.uptimePct90d?.[index], + }; +} + +try { + const status: ApiStatusResponse = await client.utilities.status(); + + const stocksQuotes = findEndpointStatus(status, "/v1/stocks/quotes/"); + const optionsChain = findEndpointStatus(status, "/v1/options/chain/"); + const stocksCandles = findEndpointStatus(status, "/v1/stocks/candles/"); + + console.log(`Stocks Quotes: ${stocksQuotes ? (stocksQuotes.online ? "Online" : "Offline") : "Not found"}`); + console.log(`Options Chain: ${optionsChain ? (optionsChain.online ? "Online" : "Offline") : "Not found"}`); + console.log(`Stocks Candles: ${stocksCandles ? (stocksCandles.online ? "Online" : "Offline") : "Not found"}`); +} catch (error) { + console.error("Error fetching API status:", error); +} ``` diff --git a/api/universal-parameters/mode.md b/api/universal-parameters/mode.md index 878d9faa..3c647403 100644 --- a/api/universal-parameters/mode.md +++ b/api/universal-parameters/mode.md @@ -108,10 +108,14 @@ When `mode=cached` is used, successful responses do not return `200 OK`. Instead * `203 NON-AUTHORITATIVE INFORMATION` – Request succeeded and was fulfilled from cache * `204 NO CONTENT` – No cached data available within constraints; no credits charged +`204` is the deterministic cache-miss signal — it can only be returned by `mode=cached` (or `mode=cache` / `mode=stale`). For the full mapping of status codes across all modes, see [Status Codes](#status-codes) below. + ## Delayed Mode The `delayed` mode returns data delayed by **at least 15 minutes**. This mode is the default for all free and trial accounts. Paid accounts may also request delayed data explicitly. +For when delayed data crosses into "historical" (a fully-closed prior session) — and why that happens at different times for stocks (4:15 PM ET) vs options (9:30 AM ET the next trading day) — see [Data Freshness](/docs/account/data-freshness). + ### Pricing for Delayed Mode * Pricing is identical to live mode. @@ -138,3 +142,47 @@ This request returns market data that is delayed by a minimum of 15 minutes. * Choose **`mode=live`** when immediate data freshness is required. * Use **`mode=cached`** to reduce credit usage when working with large symbol sets. * Select **`mode=delayed`** for applications where timing precision is not critical. + +## Status Codes + +Market Data uses HTTP status codes to communicate where a successful response came from. **All three of `200`, `203`, and `204` are success responses — your client must accept all of them.** + +| Status | Meaning | When it occurs | +|-------------------------------------|---------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------| +| `200 OK` | Response was freshly fetched from the upstream provider. | Any mode, when no cache layer can satisfy the request. | +| `203 Non-Authoritative Information` | Response was served from a cache layer (Redis, database quote cache, response log, option-chain cache, etc.). | Any mode. Common during market hours regardless of `mode=live`, `mode=delayed`, or no `mode` specified. The body is identical in shape to a `200`. | +| `204 No Content` | No cached data is available within the requested constraints. No credits charged. | Only when `mode=cached` (also `mode=cache` / `mode=stale`). Never returned by other modes. | + +:::caution Mode does not deterministically map to status code +A common (incorrect) assumption is that `mode=live` always returns `200` and `mode=delayed` always returns `203`. Both can return either `200` or `203` depending on whether a cache layer can satisfy the request at the moment. Only `mode=cached` is deterministic — it returns `203` on cache hit or `204` on cache miss, never `200`. +::: + +### Handling `204` (cache miss on `mode=cached`) + +When `mode=cached` returns `204`, the typical pattern is to fall back to a live request. A short Python example: + +```python +import requests + +def get_option_chain(token): + url = "https://api.marketdata.app/v1/options/chain/AAPL/" + headers = {"Authorization": f"Bearer {token}"} + + r = requests.get(url, params={"mode": "cached"}, headers=headers) + if r.status_code in (200, 203): + return r.json() + if r.status_code == 204: + # Cache miss — fall back to live (incurs normal live-mode credit cost) + r = requests.get(url, params={"mode": "live"}, headers=headers) + if r.status_code in (200, 203): + return r.json() + r.raise_for_status() +``` + +A few notes on the retry pattern: + +- A retry with `mode=live` is billed at the full live-mode credit cost — see [Pricing for Live Mode](#pricing-for-live-mode). The original `204` response is free. +- Cap the retry at one attempt. A subsequent `204` should not occur on `mode=live`, but a circuit breaker is still wise. +- If your plan does not include `mode=cached` access (Free/Trial), `mode=cached` requests return `402 Payment Required` rather than `204`. See [402: Payment Required](/api/troubleshooting/payment-required). + +For the full list of HTTP status codes returned by the API (including `4xx` and `5xx`), see [Troubleshooting](/troubleshooting). diff --git a/api/utilities/headers.mdx b/api/utilities/headers.mdx index bf429f0d..3b2b3821 100644 --- a/api/utilities/headers.mdx +++ b/api/utilities/headers.mdx @@ -29,13 +29,38 @@ GET **GET** [https://api.marketdata.app/headers/](https://api.marketdata.app/headers/) - + ```js title="headersCheck.js" -fetch("https://api.marketdata.app/headers/") - .then((res) => res.json()) - .then((json) => console.log(json)) - .catch((err) => console.log(err)); +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const h = await client.utilities.headers(); + console.log(h["user-agent"]); + console.log(h["accept-encoding"]); +} catch (error) { + console.error(error); +} +``` + + + + +```typescript title="headersCheck.ts" +import { MarketDataClient } from "@marketdata/sdk"; +import type { HeadersResponse } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const h: HeadersResponse = await client.utilities.headers(); + console.log(h["user-agent"]); + console.log(h["accept-encoding"]); +} catch (error) { + console.error(error); +} ``` diff --git a/api/utilities/status.mdx b/api/utilities/status.mdx index ac2f3180..4543f121 100644 --- a/api/utilities/status.mdx +++ b/api/utilities/status.mdx @@ -31,13 +31,40 @@ GET **GET** [https://api.marketdata.app/status/](https://api.marketdata.app/status/) - + ```js title="statusCheck.js" -fetch("https://api.marketdata.app/status/") - .then((res) => res.json()) - .then((json) => console.log(json)) - .catch((err) => console.log(err)); +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const s = await client.utilities.status(); + for (let i = 0; i < s.service.length; i++) { + console.log(`${s.service[i]}: ${s.status[i]} (online=${s.online[i]})`); + } +} catch (error) { + console.error(error); +} +``` + + + + +```typescript title="statusCheck.ts" +import { MarketDataClient } from "@marketdata/sdk"; +import type { ApiStatusResponse } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const s: ApiStatusResponse = await client.utilities.status(); + for (let i = 0; i < s.service.length; i++) { + console.log(`${s.service[i]}: ${s.status[i]} (online=${s.online[i]})`); + } +} catch (error) { + console.error(error); +} ``` diff --git a/lib/__tests__/mdx-to-md.test.js b/lib/__tests__/mdx-to-md.test.js new file mode 100644 index 00000000..d440629d --- /dev/null +++ b/lib/__tests__/mdx-to-md.test.js @@ -0,0 +1,407 @@ +'use strict'; + +const { test } = require('node:test'); +const assert = require('node:assert/strict'); +const { cleanMdx } = require('../mdx-to-md'); + +// --- Frontmatter --- + +test('extracts title from frontmatter and prepends as H1', () => { + const input = '---\ntitle: Test\nsidebar_position: 1\n---\n\nBody.\n'; + assert.equal(cleanMdx(input), '# Test\n\nBody.\n'); +}); + +test('strips frontmatter without a title field', () => { + const input = '---\nsidebar_position: 1\nslug: /foo\n---\n\n# Hello\n'; + assert.equal(cleanMdx(input), '# Hello\n'); +}); + +test('handles quoted frontmatter title', () => { + const input = '---\ntitle: "Quoted Title"\n---\n\nBody.\n'; + assert.equal(cleanMdx(input), '# Quoted Title\n\nBody.\n'); +}); + +test('no frontmatter passes through unchanged structure', () => { + const input = '# Hello\n\nBody.\n'; + assert.equal(cleanMdx(input), '# Hello\n\nBody.\n'); +}); + +// --- Imports --- + +test('strips theme imports outside code fences', () => { + const input = 'import Tabs from "@theme/Tabs";\nimport TabItem from "@theme/TabItem";\n\n# Hello\n'; + assert.equal(cleanMdx(input), '# Hello\n'); +}); + +test('preserves imports inside code fences (the long-standing bug fix)', () => { + const input = [ + '# Example', + '', + '```typescript', + 'import { MarketDataClient } from "marketdata-sdk-js";', + 'const c = new MarketDataClient();', + '```', + '', + ].join('\n'); + const result = cleanMdx(input); + assert.match(result, /import \{ MarketDataClient \} from "marketdata-sdk-js"/); +}); + +test('strips prose-level imports but keeps fenced ones in the same doc', () => { + const input = [ + 'import Tabs from "@theme/Tabs";', + '', + '# Doc', + '', + '```js', + 'import foo from "bar";', + '```', + '', + ].join('\n'); + const result = cleanMdx(input); + assert.ok(!result.includes('@theme/Tabs'), 'theme import should be stripped'); + assert.match(result, /import foo from "bar"/); +}); + +// --- Tabs / TabItem --- + +test('converts TabItem to ### heading and strips Tabs wrapper', () => { + const input = '\n\n\ncode\n\n\n\n'; + const result = cleanMdx(input); + assert.match(result, /### JavaScript/); + assert.ok(!result.includes('')); + assert.ok(!result.includes('')); + assert.ok(!result.includes('')); +}); + +test('handles TabItem with label before value', () => { + const input = '\nbody\n\n'; + const result = cleanMdx(input); + assert.match(result, /### PHP/); +}); + +// --- Admonitions --- + +test('converts :::tip to GitHub Alert', () => { + const input = ':::tip\nUse the SDK.\n:::\n'; + const result = cleanMdx(input); + assert.match(result, /> \[!TIP\]/); + assert.match(result, /> Use the SDK\./); +}); + +test('preserves admonition custom title as bold', () => { + const input = ':::info Premium Parameter\nThis is paid.\n:::\n'; + const result = cleanMdx(input); + assert.match(result, /> \[!NOTE\]/); + assert.match(result, /> \*\*Premium Parameter\*\*/); + assert.match(result, /> This is paid\./); +}); + +test('admonition mapping covers all Docusaurus levels', () => { + const cases = [ + [':::note\nx\n:::', 'NOTE'], + [':::tip\nx\n:::', 'TIP'], + [':::info\nx\n:::', 'NOTE'], + [':::warning\nx\n:::', 'WARNING'], + [':::caution\nx\n:::', 'CAUTION'], + [':::danger\nx\n:::', 'CAUTION'], + ]; + for (const [input, expected] of cases) { + const result = cleanMdx(input); + assert.match(result, new RegExp(`\\[!${expected}\\]`), `${input} should map to ${expected}`); + } +}); + +test('admonition containing a code fence preserves the fence inside the blockquote', () => { + const input = [ + ':::info', + 'Install from GitHub:', + '', + '```bash', + 'pnpm add github:MarketDataApp/sdk-js', + '```', + ':::', + '', + ].join('\n'); + const result = cleanMdx(input); + assert.match(result, /> \[!NOTE\]/); + assert.match(result, /> ```bash/); + assert.match(result, /> pnpm add github:MarketDataApp\/sdk-js/); + assert.match(result, /> ```$/m); +}); + +test('multi-line admonition body each line gets blockquote prefix', () => { + const input = ':::warning\nline one\nline two\n\nline four\n:::\n'; + const result = cleanMdx(input); + // Every body line begins with `> ` (or `>` for blank) + const lines = result.trim().split('\n'); + // Skip the [!WARNING] line; rest should be blockquoted + for (const line of lines.slice(1)) { + assert.match(line, /^>( |$)/); + } +}); + +// --- Link rewriting --- + +test('rewrites /api/... to absolute URL with default baseUrl', () => { + const input = 'See [the API](/api/stocks/quotes) for details.\n'; + const result = cleanMdx(input); + assert.match(result, /\[the API\]\(https:\/\/www\.marketdata\.app\/docs\/api\/stocks\/quotes\)/); +}); + +test('rewrites same-SDK link to relative .md when sdk + sourcePath given', () => { + const result = cleanMdx('See [Client](/sdk/js/client) docs.\n', { + sourcePath: 'sdk/js/stocks/quotes.mdx', + sdk: 'js', + }); + assert.match(result, /\[Client\]\(\.\.\/client\.md\)/); +}); + +test('rewrites same-SDK link from root file to ./neighbor.md', () => { + const result = cleanMdx('See [Auth](/sdk/js/authentication).\n', { + sourcePath: 'sdk/js/client.mdx', + sdk: 'js', + }); + assert.match(result, /\[Auth\]\(\.\/authentication\.md\)/); +}); + +test('rewrites cross-SDK link to absolute URL', () => { + const result = cleanMdx('Also see [Python](/sdk/py/client).\n', { + sourcePath: 'sdk/js/client.mdx', + sdk: 'js', + }); + assert.match(result, /\[Python\]\(https:\/\/www\.marketdata\.app\/docs\/sdk\/py\/client\)/); +}); + +test('preserves #anchor when rewriting same-SDK link', () => { + const result = cleanMdx('See [Result Pattern](/sdk/js/client#ResultPattern).\n', { + sourcePath: 'sdk/js/stocks/quotes.mdx', + sdk: 'js', + }); + assert.match(result, /\[Result Pattern\]\(\.\.\/client\.md#ResultPattern\)/); +}); + +test('preserves external URLs untouched', () => { + const input = 'See [GitHub](https://github.com/MarketDataApp).\n'; + assert.match(cleanMdx(input), /\[GitHub\]\(https:\/\/github\.com\/MarketDataApp\)/); +}); + +test('rewrites links whose label contains a code span with brackets', () => { + // Real-world case from sdk/js/stocks/quotes.mdx + const input = 'Returns [`MarketDataResult`](/sdk/js/client#MarketDataResult).\n'; + const result = cleanMdx(input, { + sourcePath: 'sdk/js/stocks/quotes.mdx', + sdk: 'js', + }); + assert.match(result, /\]\(\.\.\/client\.md#MarketDataResult\)/); +}); + +test('rewrites links whose label contains balanced [...] brackets', () => { + const input = 'See [name [v2] field](/sdk/js/client) details.\n'; + const result = cleanMdx(input, { + sourcePath: 'sdk/js/index.mdx', + sdk: 'js', + }); + assert.match(result, /\]\(\.\/client\.md\)/); +}); + +test('preserves bare #anchor links', () => { + const input = 'Jump to [the section](#section).\n'; + assert.match(cleanMdx(input), /\[the section\]\(#section\)/); +}); + +test('rewrites image paths the same way as links', () => { + const input = '![Logo](/img/logo.svg)\n'; + const result = cleanMdx(input); + assert.match(result, /!\[Logo\]\(https:\/\/www\.marketdata\.app\/docs\/img\/logo\.svg\)/); +}); + +test('rewrites /sdk/{sdk}/ (root with trailing slash) to relative index.md', () => { + const result = cleanMdx('Back to [JS SDK](/sdk/js/).\n', { + sourcePath: 'sdk/js/stocks/quotes.mdx', + sdk: 'js', + }); + assert.match(result, /\[JS SDK\]\(\.\.\/index\.md\)/); +}); + +// --- Source-aware link resolution (linkTargets) --- + +test('linkTargets: directory link resolves to README.md', () => { + const linkTargets = { + 'client': 'client.md', + 'stocks': 'stocks/README.md', + 'stocks/quotes': 'stocks/quotes.md', + '': 'README.md', + }; + const result = cleanMdx('Browse [Stocks](/sdk/js/stocks) for details.\n', { + sourcePath: 'sdk/js/client.mdx', + sdk: 'js', + linkTargets, + }); + assert.match(result, /\[Stocks\]\(\.\/stocks\/README\.md\)/); +}); + +test('linkTargets: sibling file gets ./neighbor.md (collapsed prefix)', () => { + const linkTargets = { + 'stocks/quotes': 'stocks/quotes.md', + 'stocks/candles': 'stocks/candles.md', + }; + const result = cleanMdx('See [Candles](/sdk/js/stocks/candles).\n', { + sourcePath: 'sdk/js/stocks/quotes.mdx', + sdk: 'js', + linkTargets, + }); + assert.match(result, /\[Candles\]\(\.\/candles\.md\)/); +}); + +test('linkTargets: file at root from subdir gets ../client.md', () => { + const linkTargets = { 'client': 'client.md' }; + const result = cleanMdx('See [Client](/sdk/js/client).\n', { + sourcePath: 'sdk/js/stocks/quotes.mdx', + sdk: 'js', + linkTargets, + }); + assert.match(result, /\[Client\]\(\.\.\/client\.md\)/); +}); + +test('linkTargets: directory link from inside that directory uses ./README.md', () => { + const linkTargets = { + 'stocks': 'stocks/README.md', + 'stocks/quotes': 'stocks/quotes.md', + }; + const result = cleanMdx('Back to [Stocks home](/sdk/js/stocks).\n', { + sourcePath: 'sdk/js/stocks/quotes.mdx', + sdk: 'js', + linkTargets, + }); + assert.match(result, /\[Stocks home\]\(\.\/README\.md\)/); +}); + +test('linkTargets: root /sdk/{sdk}/ from a subdirectory file resolves to ../README.md', () => { + const linkTargets = { '': 'README.md', 'client': 'client.md' }; + const result = cleanMdx('Home: [JS SDK](/sdk/js/).\n', { + sourcePath: 'sdk/js/stocks/quotes.mdx', + sdk: 'js', + linkTargets, + }); + assert.match(result, /\[JS SDK\]\(\.\.\/README\.md\)/); +}); + +// --- Empty components --- + +test('strips self-closing', () => { + const input = '# Sections\n\n\n'; + const result = cleanMdx(input); + assert.ok(!result.includes('DocCardList')); +}); + +test('strips ... paired', () => { + const input = '# Sections\n\nfallback\n'; + const result = cleanMdx(input); + assert.ok(!result.includes('DocCardList')); +}); + +test('strips JSX style={{...}} attribute from tags', () => { + const input = 'x\n'; + const result = cleanMdx(input); + assert.ok(!result.includes('style=')); +}); + +// --- MDX comments --- + +test('strips {/* MDX comments */}', () => { + const input = 'Before {/* this is hidden */} after.\n'; + const result = cleanMdx(input); + assert.ok(!result.includes('hidden')); + assert.match(result, /Before\s+after\./); +}); + +// --- Cleanup / shape --- + +test('collapses 3+ blank lines to 2', () => { + const input = '# A\n\n\n\n\n# B\n'; + assert.equal(cleanMdx(input), '# A\n\n# B\n'); +}); + +test('ensures trailing newline', () => { + const input = '# Trailing'; + assert.equal(cleanMdx(input).at(-1), '\n'); +}); + +// --- Idempotency --- + +test('cleanMdx is idempotent', () => { + const input = [ + '---', + 'title: Idempotent', + 'sidebar_position: 1', + '---', + '', + 'import Tabs from "@theme/Tabs";', + '', + '# Idempotent', + '', + ':::tip Pro Tip', + 'Use [the API](/api/stocks) wisely.', + ':::', + '', + '', + '', + '', + '```typescript', + 'import { MarketDataClient } from "marketdata-sdk-js";', + '```', + '', + '', + '', + '', + ].join('\n'); + const once = cleanMdx(input); + const twice = cleanMdx(once); + assert.equal(twice, once, 'second pass should produce identical output'); +}); + +// --- Composite real-world-ish doc --- + +test('processes a representative SDK page end-to-end', () => { + const input = [ + '---', + 'title: Quotes', + 'sidebar_position: 2', + '---', + '', + 'import Tabs from "@theme/Tabs";', + 'import TabItem from "@theme/TabItem";', + '', + 'Fetch [stock quotes](/api/stocks/quotes). See also [Client](/sdk/js/client#MarketDataClient).', + '', + ':::tip', + 'Use bulk endpoints for >1 symbol.', + ':::', + '', + '', + '', + '', + '```typescript', + 'import { MarketDataClient } from "marketdata-sdk-js";', + 'const r = await client.stocks.quotes("AAPL");', + '```', + '', + '', + '', + '', + ].join('\n'); + const result = cleanMdx(input, { + sourcePath: 'sdk/js/stocks/quotes.mdx', + sdk: 'js', + }); + assert.match(result, /^# Quotes\n\n/); + assert.match(result, /\[stock quotes\]\(https:\/\/www\.marketdata\.app\/docs\/api\/stocks\/quotes\)/); + assert.match(result, /\[Client\]\(\.\.\/client\.md#MarketDataClient\)/); + assert.match(result, /> \[!TIP\]\n> Use bulk endpoints/); + assert.match(result, /### Single Symbol/); + assert.match(result, /import \{ MarketDataClient \} from "marketdata-sdk-js"/); + assert.ok(!result.includes('@theme/Tabs')); + assert.ok(!result.includes('sidebar_position')); +}); diff --git a/lib/mdx-to-md.js b/lib/mdx-to-md.js new file mode 100644 index 00000000..c7e1ff1c --- /dev/null +++ b/lib/mdx-to-md.js @@ -0,0 +1,247 @@ +'use strict'; + +const ADMONITION_MAP = { + note: 'NOTE', + tip: 'TIP', + info: 'NOTE', + important: 'IMPORTANT', + warning: 'WARNING', + caution: 'CAUTION', + danger: 'CAUTION', +}; + +const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n*/; +const MDX_COMMENT_RE = /\{\s*\/\*[\s\S]*?\*\/\s*\}/g; +const IMPORT_FROM_RE = /^import\b[^\n]*?\bfrom\s+["'][^"'\n]+["'];?[ \t]*$/gm; +const IMPORT_BARE_RE = /^import\s+["'][^"'\n]+["'];?[ \t]*$/gm; +const TABS_OPEN_RE = /]*>[ \t]*\n?/g; +const TABS_CLOSE_RE = /<\/Tabs>[ \t]*\n?/g; +const TABITEM_OPEN_RE = /]*?\blabel="([^"]+)"[^>]*>[ \t]*\n?/g; +const TABITEM_CLOSE_RE = /<\/TabItem>[ \t]*\n?/g; +const DOCCARDLIST_SELF_RE = /]*\/>[ \t]*\n?/g; +const DOCCARDLIST_PAIRED_RE = /]*>[\s\S]*?<\/DocCardList>[ \t]*\n?/g; +const USECURRENT_SIDEBAR_RE = /]*\/>[ \t]*\n?/g; +const JSX_STYLE_RE = /\s+style=\{\{[^}]*\}\}/g; +// Link label may contain: non-special chars, inline code spans `...`, or balanced [...] +const LINK_RE = /(!?)\[((?:[^\[\]`]|`[^`]*`|\[[^\]]*\])*)\]\(([^)\s]+)\)/g; +const FENCE_OPEN_RE = /^([ \t]*)(`{3,}|~{3,})(.*)$/; +const ADMONITION_OPEN_RE = /^:::(\w+)(.*)$/; +const ADMONITION_CLOSE_RE = /^:::[ \t]*$/; + +function extractTitle(frontmatter) { + const m = frontmatter.match(/^title:\s*(.+)$/m); + if (!m) return null; + return m[1].trim().replace(/^["']|["']$/g, ''); +} + +// Compute a relative path from one repo-relative POSIX file to another, +// collapsing the common prefix so siblings get './x.md' instead of '../dir/x.md'. +function relativePath(fromFile, toFile) { + const fromParts = fromFile.split('/').slice(0, -1); // directory of source + const toParts = toFile.split('/'); + let i = 0; + while (i < fromParts.length && i < toParts.length - 1 && fromParts[i] === toParts[i]) { + i++; + } + const ups = fromParts.length - i; + const rest = toParts.slice(i).join('/'); + const prefix = ups === 0 ? './' : '../'.repeat(ups); + return prefix + rest; +} + +function resolveSameSdkLink(sourcePath, sdkBase, targetSub, linkTargets) { + const cleanedSub = targetSub.replace(/\/$/, ''); + // If the CLI gave us a source-aware target map, use it. This is the only + // way to disambiguate "/sdk/js/funds" (a directory → funds/README.md) from + // "/sdk/js/client" (a file → client.md) without false guesses. + if (linkTargets && linkTargets[cleanedSub]) { + const srcOutput = sourcePath + .slice(sdkBase.length + 1) + .replace(/\.mdx$/, '.md'); + return relativePath(srcOutput, linkTargets[cleanedSub]); + } + // Fallback for callers that don't supply a target map: assume "foo" → "foo.md". + // Worker/LLM-bundle paths use absolute URLs instead and never hit this. + const srcRelative = sourcePath.startsWith(sdkBase + '/') + ? sourcePath.slice(sdkBase.length + 1) + : sourcePath; + const lastSlash = srcRelative.lastIndexOf('/'); + const srcDir = lastSlash >= 0 ? srcRelative.slice(0, lastSlash) : ''; + const depth = srcDir ? srcDir.split('/').length : 0; + const up = depth === 0 ? './' : '../'.repeat(depth); + const target = cleanedSub === '' ? 'index' : cleanedSub; + return up + target + '.md'; +} + +function rewriteHref(href, opts) { + if (/^(https?:|mailto:|tel:|#)/i.test(href)) return href; + if (!href.startsWith('/')) return href; + + const { sourcePath, sdk, baseUrl, linkTargets } = opts; + + const hashIdx = href.indexOf('#'); + const pathPart = hashIdx >= 0 ? href.slice(0, hashIdx) : href; + const anchor = hashIdx >= 0 ? href.slice(hashIdx) : ''; + + if (sdk && sourcePath) { + const sdkPrefix = `/sdk/${sdk}/`; + const sdkRoot = `/sdk/${sdk}`; + if (pathPart === sdkRoot || pathPart === sdkRoot + '/') { + return resolveSameSdkLink(sourcePath, `sdk/${sdk}`, '', linkTargets) + anchor; + } + if (pathPart.startsWith(sdkPrefix)) { + const sub = pathPart.slice(sdkPrefix.length); + return resolveSameSdkLink(sourcePath, `sdk/${sdk}`, sub, linkTargets) + anchor; + } + } + + return baseUrl + href; +} + +function transformProse(text, opts) { + text = text.replace(MDX_COMMENT_RE, ''); + text = text.replace(IMPORT_FROM_RE, ''); + text = text.replace(IMPORT_BARE_RE, ''); + text = text.replace(TABS_OPEN_RE, ''); + text = text.replace(TABS_CLOSE_RE, ''); + text = text.replace(TABITEM_OPEN_RE, '### $1\n\n'); + text = text.replace(TABITEM_CLOSE_RE, '\n\n'); + text = text.replace(DOCCARDLIST_PAIRED_RE, ''); + text = text.replace(DOCCARDLIST_SELF_RE, ''); + text = text.replace(USECURRENT_SIDEBAR_RE, ''); + text = text.replace(JSX_STYLE_RE, ''); + text = text.replace(LINK_RE, (_, bang, label, href) => { + return `${bang}[${label}](${rewriteHref(href, opts)})`; + }); + return text; +} + +function isFenceClose(line, marker) { + const trimmed = line.trim(); + if (trimmed.length < marker.length) return false; + const ch = marker[0]; + for (let i = 0; i < trimmed.length; i++) { + if (trimmed[i] !== ch) return false; + } + return true; +} + +function tokenize(text) { + const lines = text.split('\n'); + const tokens = []; + let i = 0; + while (i < lines.length) { + const line = lines[i]; + + const fenceMatch = line.match(FENCE_OPEN_RE); + if (fenceMatch) { + const marker = fenceMatch[2]; + const fenceLines = [line]; + i++; + while (i < lines.length) { + fenceLines.push(lines[i]); + if (isFenceClose(lines[i], marker)) { i++; break; } + i++; + } + tokens.push({ type: 'fence', text: fenceLines.join('\n') }); + continue; + } + + const admMatch = line.match(ADMONITION_OPEN_RE); + if (admMatch && admMatch[1].toLowerCase() in ADMONITION_MAP) { + const kind = admMatch[1]; + const title = admMatch[2].trim(); + i++; + const bodyLines = []; + let closed = false; + while (i < lines.length) { + const l = lines[i]; + if (ADMONITION_CLOSE_RE.test(l)) { i++; closed = true; break; } + bodyLines.push(l); + i++; + } + if (!closed) { + // No closing fence — treat opening as literal prose + tokens.push({ type: 'prose-line', text: line }); + // Re-process the body lines normally + for (const bl of bodyLines) tokens.push({ type: 'prose-line', text: bl }); + continue; + } + tokens.push({ type: 'admonition', kind, title, body: bodyLines.join('\n') }); + continue; + } + + tokens.push({ type: 'prose-line', text: line }); + i++; + } + return tokens; +} + +function renderAdmonition({ kind, title, body }, opts) { + const alert = ADMONITION_MAP[kind.toLowerCase()] || 'NOTE'; + const innerProcessed = renderTokens(tokenize(body), opts).replace(/\s+$/, ''); + const bodyLines = innerProcessed.split('\n').map((l) => (l === '' ? '>' : `> ${l}`)); + const out = [`> [!${alert}]`]; + if (title) { + out.push(`> **${title}**`); + out.push('>'); + } + for (const l of bodyLines) out.push(l); + return out.join('\n'); +} + +function renderTokens(tokens, opts) { + const out = []; + let proseGroup = []; + + function flushProse() { + if (proseGroup.length === 0) return; + const text = proseGroup.join('\n'); + out.push(transformProse(text, opts)); + proseGroup = []; + } + + for (const tok of tokens) { + if (tok.type === 'prose-line') { + proseGroup.push(tok.text); + } else if (tok.type === 'fence') { + flushProse(); + out.push(tok.text); + } else if (tok.type === 'admonition') { + flushProse(); + out.push(renderAdmonition(tok, opts)); + } + } + flushProse(); + return out.join('\n'); +} + +function cleanMdx(text, opts = {}) { + const o = { + sourcePath: opts.sourcePath ?? null, + sdk: opts.sdk ?? null, + baseUrl: opts.baseUrl ?? 'https://www.marketdata.app/docs', + // Map of Docusaurus subpath (no leading slash, no trailing slash) → repo-relative + // output file. Only consulted for same-SDK link rewriting. CLI builds this from + // the source tree; worker / LLM-bundle leave it unset. + linkTargets: opts.linkTargets ?? null, + }; + + let title = null; + const fm = text.match(FRONTMATTER_RE); + if (fm) { + title = extractTitle(fm[1]); + text = text.slice(fm[0].length); + } + + let result = renderTokens(tokenize(text), o); + + if (title) { + result = `# ${title}\n\n` + result.replace(/^\s+/, ''); + } + + result = result.replace(/\n{3,}/g, '\n\n').trim() + '\n'; + return result; +} + +module.exports = { cleanMdx }; diff --git a/package.json b/package.json index ff44509c..2aed2465 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,8 @@ "write-heading-ids": "docusaurus write-heading-ids", "typecheck": "tsc", "generate-docs": "node scripts/generate-master-docs.js", + "export-sdk-docs": "node scripts/export-sdk-docs.js", + "test:lib": "node --test lib/__tests__/*.test.js", "lint:tables": "node scripts/fix-table-alignment.js", "fix:tables": "node scripts/fix-table-alignment.js --fix", "test:e2e": "playwright test" diff --git a/scripts/export-sdk-docs.js b/scripts/export-sdk-docs.js new file mode 100644 index 00000000..9f8dfd0a --- /dev/null +++ b/scripts/export-sdk-docs.js @@ -0,0 +1,148 @@ +#!/usr/bin/env node +'use strict'; + +/** + * Export SDK docs from /sdk/{lang}/ as clean .md, ready to land in MarketDataApp/sdk-{lang}/docs. + * + * node scripts/export-sdk-docs.js --sdk js [--out ./build/sdk-docs/js] + * + * The CLI is intentionally thin — all conversion lives in lib/mdx-to-md.js. + * Output dir is wiped before write so deleted source files don't linger in the target. + */ + +const fs = require('fs'); +const path = require('path'); +const { cleanMdx } = require('../lib/mdx-to-md'); + +const REPO_ROOT = path.resolve(__dirname, '..'); +const SUPPORTED_SDKS = ['js', 'py', 'go', 'php']; + +function parseArgs(argv) { + const args = { sdk: null, out: null }; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a === '--sdk') args.sdk = argv[++i]; + else if (a === '--out') args.out = argv[++i]; + else if (a === '-h' || a === '--help') args.help = true; + else { + console.error(`Unknown argument: ${a}`); + process.exit(2); + } + } + return args; +} + +function printUsage() { + console.log(`Usage: node scripts/export-sdk-docs.js --sdk <${SUPPORTED_SDKS.join('|')}> [--out ]`); + console.log(''); + console.log('Converts /sdk//*.mdx → clean .md files under (default: build/sdk-docs//).'); +} + +function walkMdx(dir, acc = []) { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) walkMdx(full, acc); + else if (entry.isFile() && /\.(mdx?|md)$/.test(entry.name)) acc.push(full); + } + return acc; +} + +function emptyDir(dir) { + if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true }); + fs.mkdirSync(dir, { recursive: true }); +} + +function main() { + const args = parseArgs(process.argv.slice(2)); + if (args.help || !args.sdk) { + printUsage(); + process.exit(args.help ? 0 : 2); + } + if (!SUPPORTED_SDKS.includes(args.sdk)) { + console.error(`--sdk must be one of: ${SUPPORTED_SDKS.join(', ')}`); + process.exit(2); + } + + const sourceDir = path.join(REPO_ROOT, 'sdk', args.sdk); + if (!fs.existsSync(sourceDir)) { + console.error(`Source directory not found: ${sourceDir}`); + process.exit(1); + } + + const outDir = path.resolve(args.out || path.join(REPO_ROOT, 'build', 'sdk-docs', args.sdk)); + emptyDir(outDir); + + const files = walkMdx(sourceDir); + + // Build the source-aware link target map BEFORE writing any file. For each + // source .mdx, compute what /sdk/{sdk}/... URLs in *other* docs would point + // at, and which output file we'll write it as. Directory indices land as + // README.md so GitHub auto-renders them at the directory URL. + // Map keys are Docusaurus subpaths (no leading slash, no extension); values + // are repo-relative POSIX output paths. + const linkTargets = {}; + for (const sourceAbs of files) { + const relFromSdk = path.relative(sourceDir, sourceAbs).split(path.sep).join('/'); + const noExt = relFromSdk.replace(/\.(mdx?|md)$/, ''); + const isIndex = path.posix.basename(noExt) === 'index'; + if (isIndex) { + const dirSub = path.posix.dirname(noExt); + const key = dirSub === '.' ? '' : dirSub; + const out = key === '' ? 'README.md' : `${key}/README.md`; + linkTargets[key] = out; + } else { + linkTargets[noExt] = `${noExt}.md`; + } + } + + const manifest = []; + + for (const sourceAbs of files) { + const relPosix = path.relative(sourceDir, sourceAbs).split(path.sep).join('/'); + const sourcePathRel = path.posix.join('sdk', args.sdk, relPosix); + const noExt = relPosix.replace(/\.(mdx?|md)$/, ''); + const isIndex = path.posix.basename(noExt) === 'index'; + const outRelative = isIndex + ? (path.posix.dirname(noExt) === '.' ? 'README.md' : `${path.posix.dirname(noExt)}/README.md`) + : `${noExt}.md`; + const outAbs = path.join(outDir, ...outRelative.split('/')); + + const raw = fs.readFileSync(sourceAbs, 'utf8'); + const body = cleanMdx(raw, { + sourcePath: sourcePathRel, + sdk: args.sdk, + linkTargets, + }); + + fs.mkdirSync(path.dirname(outAbs), { recursive: true }); + fs.writeFileSync(outAbs, body, 'utf8'); + manifest.push({ path: outRelative, bytes: Buffer.byteLength(body, 'utf8') }); + } + + // Write a manifest of every file we generated, so the sync workflow can + // delete exactly these paths on the next run without touching repo-owned + // content. The manifest itself is also listed so it gets refreshed each + // sync (and removed cleanly if the workflow is ever retired). + const generatedPaths = [...manifest.map((m) => m.path), '.sync-manifest.txt'].sort(); + const manifestPath = path.join(outDir, '.sync-manifest.txt'); + const manifestBody = + '# Auto-generated by MarketDataApp/documentation/scripts/export-sdk-docs.js.\n' + + '# These files are owned by the docs sync workflow — do not edit them by hand.\n' + + '# Editing this manifest will not break anything; the workflow rebuilds it each run.\n' + + generatedPaths.join('\n') + + '\n'; + fs.writeFileSync(manifestPath, manifestBody, 'utf8'); + manifest.push({ + path: '.sync-manifest.txt', + bytes: Buffer.byteLength(manifestBody, 'utf8'), + note: '(workflow manifest)', + }); + + manifest.sort((a, b) => a.path.localeCompare(b.path)); + console.log(`Exported ${manifest.length} file(s) to ${outDir}\n`); + for (const { path: p, bytes, note } of manifest) { + console.log(` ${String(bytes).padStart(7)} ${p}${note ? ' ' + note : ''}`); + } +} + +main(); diff --git a/scripts/generate-master-docs.js b/scripts/generate-master-docs.js index 4f762f06..aec6a42e 100644 --- a/scripts/generate-master-docs.js +++ b/scripts/generate-master-docs.js @@ -48,6 +48,7 @@ const fs = require('fs'); const path = require('path'); +const { cleanMdx } = require('../lib/mdx-to-md'); // Configuration // Output directory for generated master documentation files @@ -98,7 +99,7 @@ function collectFiles(dir, fileList = []) { } /** - * Strip "tg [letter]" tags from title + * Strip "tg [letter]" tags from title (LLM-bundle-specific, not a general MDX concern) */ function stripTitleTags(title) { if (!title) return title; @@ -106,95 +107,34 @@ function stripTitleTags(title) { } /** - * Extract title from frontmatter - */ -function extractTitle(content) { - const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n/); - if (frontmatterMatch) { - const frontmatter = frontmatterMatch[1]; - const titleMatch = frontmatter.match(/^title:\s*(.+)$/m); - if (titleMatch) { - let title = titleMatch[1].trim().replace(/^["']|["']$/g, ''); - title = stripTitleTags(title); - return title; - } - } - return null; -} - -/** - * Strip frontmatter from content - */ -function stripFrontmatter(content) { - return content.replace(/^---\s*\n[\s\S]*?\n---\s*\n/, ''); -} - -/** - * Remove all import statements - */ -function removeImports(content) { - return content.replace(/^import\s+.*?from\s+["'].*?["'];?\s*$/gm, ''); -} - -/** - * Convert Tabs/TabItem components to markdown headers - */ -function convertTabsToHeaders(content) { - let result = content; - - // Remove opening tag - result = result.replace(/]*>\s*/g, ''); - - // Convert to header and remove closing tag - result = result.replace(/]*>\s*/g, '### $1\n\n'); - result = result.replace(/<\/TabItem>\s*/g, '\n\n'); - - // Remove closing tag - result = result.replace(/<\/Tabs>\s*/g, ''); - - return result; -} - -/** - * Remove empty components like DocCardList - */ -function removeEmptyComponents(content) { - // Remove DocCardList and similar self-closing or empty components - result = content.replace(/]*\/>\s*/g, ''); - result = result.replace(/]*>[\s\S]*?<\/DocCardList>\s*/g, ''); - result = result.replace(/]*\/>\s*/g, ''); - - // Remove import statements that reference these components (already handled, but double-check) - result = result.replace(/import\s+.*?from\s+["']@theme\/DocCardList["'];?\s*$/gm, ''); - result = result.replace(/import\s+.*?from\s+["']@docusaurus\/theme-common["'];?\s*$/gm, ''); - - return result; -} - -/** - * Process a single file + * Process a single file. Conversion is delegated to the shared lib; + * this function only does file IO + extracting the title for the TOC + stripping the + * H1 the lib prepends (combineFiles re-adds it with the right wrapping). */ function processFile(filePath) { try { - let content = fs.readFileSync(filePath, 'utf8'); - let title = extractTitle(content); - if (!title) { + const raw = fs.readFileSync(filePath, 'utf8'); + + // Run the canonical MDX→MD transform with absolute link rewriting + let content = cleanMdx(raw, { baseUrl: 'https://www.marketdata.app/docs' }); + + // Pull the H1 (from frontmatter title) back out so combineFiles can re-emit it + // with its preferred formatting, and strip our own "tg X" suffix from it. + let title; + const h1Match = content.match(/^#\s+(.+?)\n+/); + if (h1Match) { + title = stripTitleTags(h1Match[1].trim()); + content = content.slice(h1Match[0].length); + } else { title = stripTitleTags(path.basename(filePath, path.extname(filePath))); } - - // Process content - content = stripFrontmatter(content); - content = removeImports(content); - content = removeEmptyComponents(content); - content = convertTabsToHeaders(content); - - // Clean up extra blank lines - content = content.replace(/\n{3,}/g, '\n\n').trim(); - + + content = content.trim(); + return { title, content, - path: filePath + path: filePath, }; } catch (error) { console.error(`Error processing ${filePath}:`, error.message); diff --git a/sdk/go/authentication.mdx b/sdk/go/authentication.mdx index 4cce34b5..5d7f6a26 100644 --- a/sdk/go/authentication.mdx +++ b/sdk/go/authentication.mdx @@ -28,7 +28,7 @@ Set the token in the environment variable `MARKETDATA_TOKEN`. Alternatively, you After setting the variable, build and run your code to make a test request. If you have set the variable correctly, you should see the output of the test request. If you see an error, double-check that you have set the variable correctly. - + This command should be run in the terminal. It sets the environment variable for the current session only. If you open a new terminal window or restart your computer, the environment variable will not persist. diff --git a/sdk/index.mdx b/sdk/index.mdx index 6fe0f21b..9f784137 100644 --- a/sdk/index.mdx +++ b/sdk/index.mdx @@ -24,6 +24,12 @@ Our SDKs are available for a multitude of programming languages and platforms, s [Perfect for data analysis, backend services, AI, ML, and automation scripts.](/sdk/py) +
+ [![JavaScript Logo](/img/javascript-logo.svg)![TypeScript Logo](/img/typescript-logo.svg)](/sdk/js) + ### [JavaScript / TypeScript SDK](/sdk/js) + [Type-safe Market Data integration for Node.js services and TypeScript applications.](/sdk/js) +
+
[![PHP SDK Logo](/img/php-logo.svg)](/sdk/php) ### [PHP SDK](/sdk/php) diff --git a/sdk/js/authentication.mdx b/sdk/js/authentication.mdx new file mode 100644 index 00000000..12ebf4e3 --- /dev/null +++ b/sdk/js/authentication.mdx @@ -0,0 +1,120 @@ +--- +title: Authentication +sidebar_position: 2 +--- + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +The Market Data API uses a **Bearer Token** for authentication. The token is required for each request you make to the API. Your token should have been e-mailed to you when you first signed up for an account. If you do not have a token or have lost your sign-up email, request a new token from the [Market Data Dashboard](https://www.marketdata.app/dashboard/). + +There are three ways to set your token when using the JavaScript SDK: + +1. Set it from an environment variable _(recommended for production)_ +2. Load it from a `.env` file _(recommended for local development)_ +3. Pass it directly when creating the [client](/sdk/js/client) + +On startup, the SDK will look for the `MARKETDATA_TOKEN` environment variable. If found, it will use the token for all requests. The SDK also loads a `.env` file automatically via `dotenv` if one is present in the working directory. + +:::tip +When your code is running in a production environment, we recommend using an environment variable to ensure your token is not stored with your code. This is the most secure way to set your token. +::: + +## How To Set-Up The Environment Variable + +Set the token in the environment variable `MARKETDATA_TOKEN`. Alternatively, you can pass it directly when creating the client, but please be aware that this is not secure and could pose a risk if your code is shared. + +### Set The Environment Variable In The Console + + + + +This command should be run in the terminal. It sets the environment variable for the current session only. If you open a new terminal window or restart your computer, the environment variable will not persist. + +```bash +export MARKETDATA_TOKEN="your_api_token" +``` + +#### Verify the Variable + +To verify that the `MARKETDATA_TOKEN` environment variable has been set properly, run `echo $MARKETDATA_TOKEN`. If set correctly, this prints the token value; if unset, the output is blank. + +#### Make The Variable Persistent + +Add the `export` line to your shell's profile script (`~/.bash_profile`, `~/.bashrc`, `~/.zshrc`, etc.), then either restart your terminal or run `source ~/.bashrc` (adjusting for your shell) to apply. + + + + +This command should be run in the Command Prompt. `setx` sets the variable permanently, but the new value is not available in the current Command Prompt session — you will need to open a new session. + +```bash +setx MARKETDATA_TOKEN "your_api_token" +``` + +#### Verify The Variable + +Open a new Command Prompt and run `echo %MARKETDATA_TOKEN%`. If the output prints the token, the variable is set correctly. If it prints `%MARKETDATA_TOKEN%` unchanged, the variable is not set. + + + + +### Using a .env File + +The SDK automatically loads a `.env` file from your working directory at import time (via [`dotenv`](https://github.com/motdotla/dotenv)). Create a file named `.env` in your project root: + +```env title=".env" +MARKETDATA_TOKEN=your_api_token +``` + +:::warning +Add `.env` to your `.gitignore` so the token is not committed to source control. +::: + +### Make A Test Request + +Use the following code to verify that your authentication is working by making a test request to `SPY` or any other symbol that requires authentication. Do not use `AAPL` to test your authentication because `AAPL` is a free test symbol and will return data even if you are not authenticated properly. + +```typescript +import { MarketDataClient } from "@marketdata/sdk"; + +// No need to pass a token here — the SDK reads it from +// the MARKETDATA_TOKEN environment variable automatically. +const client = new MarketDataClient(); + +try { + const quotes = await client.stocks.quotes("SPY"); + console.log(quotes); +} catch (error) { + console.error(`Error: ${error.message}`); +} +``` + +If your token is set correctly, you should see the output of the test request. If you see an error, double-check that you have set the token correctly. + +## How To Create a Client and Assign a Token Directly + +If you decide to pass the token directly when creating the client, you can do so by passing it as a property of the configuration object. This is not recommended for production code. + +### Example Code + +```typescript +import { MarketDataClient } from "@marketdata/sdk"; + +const token = "your_token_here"; + +const client = new MarketDataClient({ token }); + +try { + const quotes = await client.stocks.quotes("SPY"); + console.log(quotes); +} catch (error) { + console.error(`Error: ${error.message}`); +} +``` + +## Next Steps + +After successful authentication, you will be able to begin making requests to the Market Data API. We recommend reading the brief overview of how the [client](/sdk/js/client) works, so you can get familiar with interacting with the SDK. + +You may also want to configure [Settings](/sdk/js/settings) to customize output format, date format, and other universal parameters. diff --git a/sdk/js/client.mdx b/sdk/js/client.mdx new file mode 100644 index 00000000..2fe87b79 --- /dev/null +++ b/sdk/js/client.mdx @@ -0,0 +1,337 @@ +--- +title: Client +sidebar_position: 3 +--- + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +The Market Data JavaScript Client handles API requests, response parsing, rate-limit tracking, retries, and logging. The SDK supports stocks, options, funds, markets, and utility endpoints. + +### Get Started Quickly with the MarketDataClient + +1. Review the [documentation on authentication](/sdk/js/authentication) to learn how to set your API token. +2. Create a [`MarketDataClient`](#MarketDataClient) instance and use it to make requests to the Market Data API. +3. Make a test request and review the console output. The SDK includes logging capabilities to help you debug requests. +4. Check the [rate limit](#RateLimits) in the client to track credit usage and how many API credits remain. +5. Configure [Settings](/sdk/js/settings) to customize output format, date format, and other universal parameters. + + +## MarketDataClient + +```typescript +class MarketDataClient { + constructor(config?: MarketDataConfig); + readonly ready: Promise; +} + +interface MarketDataConfig { + token?: string; + baseUrl?: string; + apiVersion?: string; + maxRetries?: number; + retryInitialWait?: number; + retryMaxWait?: number; + retryFactor?: number; + skipStartupValidation?: boolean; + debug?: boolean; + logger?: Logger; +} +``` + +[MarketDataClient](#MarketDataClient) is the main client class for interacting with the Market Data API. It provides access to all resources (stocks, options, funds, markets, utilities) and handles authentication, rate limiting, and request management. + +#### Properties + +- `token` (string, optional): The authentication token for API requests. See [authentication documentation](/sdk/js/authentication) for details. +- `rateLimits` ([UserRateLimits](#RateLimits), optional): Current rate limit information. Populated by the startup `/user/` call (see `ready` below) or by the first successful API request when startup validation is skipped. +- `ready` (Promise<void>): Resolves when the eager `/user/` validation completes. Rejects with `AuthenticationError` on bad tokens; transient errors are logged and swallowed. Await it if you need the fail-fast behaviour on construction. +- `baseUrl` (string): The base URL for API requests (default: `https://api.marketdata.app`). +- `apiVersion` (string): The API version to use (default: `v1`). +- `headers` (Record<string, string>): HTTP headers including `Authorization` and `User-Agent`. +- `logger` ([Logger](#Logger)): The logger instance used for diagnostic output. +- `settings` ([MarketDataSettings](/sdk/js/settings)): Resolved configuration including env-var defaults. See [Settings](/sdk/js/settings) for details. + +#### Resources + +- `stocks` ([StocksResource](/sdk/js/stocks)): Access to stocks endpoints (prices, quotes, candles, earnings, news) +- `options` ([OptionsResource](/sdk/js/options)): Access to options endpoints (chain, expirations, quotes, lookup) +- `funds` ([FundsResource](/sdk/js/funds)): Access to funds endpoints (candles) +- `markets` ([MarketsResource](/sdk/js/markets)): Access to markets endpoints (status) +- `utilities` ([UtilitiesResource](/sdk/js/utilities)): Access to utility endpoints (`status`, `headers`) + + +### constructor + +```typescript +new MarketDataClient(config?: MarketDataConfig) +``` + +Creates and configures a new `MarketDataClient` instance. This initializes the client with the provided token (or reads it from the `MARKETDATA_TOKEN` environment variable), sets up HTTP headers, prepares the resource namespaces, and kicks off eager `/user/` validation (unless `skipStartupValidation: true`). + +#### Parameters + +- `config` ([MarketDataConfig](#MarketDataClient), optional) + + Configuration object. All properties are optional: + + - `token` (string): The authentication token. Falls back to `MARKETDATA_TOKEN` environment variable if not provided. + - `baseUrl` (string): Override the API base URL. Defaults to `https://api.marketdata.app`. + - `apiVersion` (string): Override the API version. Defaults to `v1`. + - `maxRetries` (number): Maximum retry attempts for retriable errors. Defaults to `3`. + - `retryInitialWait` (number): Initial wait in seconds before the first retry. Defaults to `0.5`. + - `retryMaxWait` (number): Maximum wait in seconds between retries. Defaults to `10`. + - `retryFactor` (number): Exponential backoff factor. Defaults to `2`. + - `skipStartupValidation` (boolean): If `true`, skips the eager `/user/` call the constructor makes to validate the token. Defaults to `false`. Use on serverless platforms where cold-start latency matters. + - `debug` (boolean): If `true`, sets the default logger level to `DEBUG`. Defaults to `false`. + - `logger` ([Logger](#Logger)): A custom logger instance. If omitted, the SDK uses its built-in `DefaultLogger`. + +#### Returns + +- `MarketDataClient` + + A new `MarketDataClient` instance ready to make API requests. + +#### Notes + +- The client sets a `User-Agent` header of the form `marketdata-sdk-js/{version}` (e.g. `marketdata-sdk-js/1.0.0`). +- All authenticated requests include an `Authorization: Bearer {token}` header. +- The client reuses a single underlying `fetch` client, which benefits from Node's global connection pooling, and enforces a global 50-request concurrency pool across every endpoint. +- Every request has a 99-second timeout; a timed-out fetch rejects with `NetworkError`. +- Configuration properties can also be provided via environment variables — see [Settings](/sdk/js/settings) for the full list and their resolution order. + +#### Example + +```typescript +import { MarketDataClient } from "@marketdata/sdk"; + +// Token will be read from MARKETDATA_TOKEN environment variable +const client = new MarketDataClient(); + +// Or provide the token explicitly +const clientWithToken = new MarketDataClient({ token: "your_token_here" }); + +// Fail fast on invalid tokens (default) — await readiness: +await client.ready; + +// Skip the startup /user/ call (e.g. on Lambda cold starts) +const fast = new MarketDataClient({ + token: "your_token_here", + skipStartupValidation: true, +}); + +// Enable debug logging for troubleshooting +const debugClient = new MarketDataClient({ debug: true }); + +// Provide a custom logger +import { DefaultLogger, LogLevel } from "@marketdata/sdk"; +const logger = new DefaultLogger(LogLevel.WARN); +const quietClient = new MarketDataClient({ logger }); +``` + + +## Error Handling + +Resource methods return a `MarketDataPromise` that resolves with the data on success and rejects with a subclass of `MarketDataClientError` on failure. Use standard JavaScript error-handling idioms. + + + + +The idiomatic approach. + +```typescript +import { + MarketDataClient, + AuthenticationError, + RateLimitError, + NotFoundError, +} from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const prices = await client.stocks.prices("AAPL"); + console.log("Success:", prices); +} catch (error) { + if (error instanceof AuthenticationError) { + console.error("Bad token:", error.message); + } else if (error instanceof RateLimitError) { + console.error("Rate limit exceeded"); + } else { + console.error("Failed:", error); + } +} +``` + + + + + +Fluent chaining without `try`/`catch`. + +```typescript +const prices = await client.stocks + .prices("AAPL") + .catch((error) => { + console.error("Failed:", error.message); + return []; + }); +``` + + + + + +```typescript +import { AuthenticationError } from "@marketdata/sdk"; + +await expect(client.stocks.prices("AAPL")) + .rejects.toBeInstanceOf(AuthenticationError); +``` + + + + +Every thrown error is a subclass of `MarketDataClientError` and carries HTTP context for support tickets: + +```typescript +try { + await client.stocks.prices("$$$"); +} catch (err) { + // Base class — use `instanceof` to narrow to specific errors. + console.log(err.status_code); // 400 + console.log(err.request_id); // cf-ray header, e.g. "8a1b2c-SJC" + console.log(err.request_url); // full URL that failed + console.log(err.timestamp); // Date of the request + console.log(err.support_info); // multi-line string to paste into tickets +} +``` + +### Error classes + +| Class | When | +|------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `AuthenticationError` | 401 — token missing, invalid, or expired | +| `BadRequestError` | 400 — malformed request or invalid parameters | +| `NotFoundError` | 404 — exported but not thrown by default. The SDK translates 404 into an empty response with `no_data: true`; see [no-data handling](#NoData). The one exception is `/user/`, which opts into the throw path. | +| `PaymentRequiredError` | 402 — request denied by your plan (data older than your plan allows, premium endpoint on Free/Trial, or `mode=cached` on Free/Trial) | +| `ForbiddenError` | 403 — access denied. Typically the multi-IP block: the account is temporarily locked when used from more than one IP. Wait ~5 minutes and retry. | +| `RateLimitError` | 429 — per-minute/day rate limit exceeded | +| `ServerError` | 5xx — retriable, server-side failure | +| `NetworkError` | Transport failure: DNS, connection, TLS, or 99s timeout | +| `ParseError` | Response body failed schema or JSON parsing | +| `ValidationError` | Client-side input validation failure (before the network call) | + + +### No-data responses + +When the server responds with 404 the SDK resolves the Promise with an empty array (or empty Blob for CSV) and flags the returned `MarketDataPromise` as `no_data: true`. Use `hasData()` to branch on it: + +```typescript +const pending = client.stocks.prices("UNKNOWN"); +const prices = await pending; + +if (!(await pending.hasData())) { + console.log("No data for that symbol"); +} else { + console.log(prices); +} +``` + + +### MarketDataPromise + +```typescript +class MarketDataPromise extends Promise { + isJson(): boolean; + isCsv(): boolean; + isHtml(): boolean; + readonly no_data: boolean; + hasData(): Promise; + save(filename?: string): Promise; + saveToFile(filename?: string): Promise; + blob(): Promise; +} +``` + +Every resource method returns a `MarketDataPromise`. It is a regular `Promise` — you `await` it to get the data — with a few extra response-model methods available before you unwrap it: + +- `isJson()` / `isCsv()` / `isHtml()`: reports the requested format. +- `no_data`: synchronous flag, `true` when the server returned 404. +- `hasData()`: awaits and returns whether the resolved value carries any rows. +- `save(filename?)` / `saveToFile(filename?)`: persist the response to disk. Returns a `Promise` resolving to the path written. Format is inferred from the extension when possible. +- `blob()`: materialise the response as a [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob). + +Chained saves work without an intermediate `await`: + +```typescript +const path = await client.stocks + .candles("AAPL", { resolution: "D", countback: 90 }) + .saveToFile("aapl-90d.csv"); +``` + + +## Accessing Rate Limits + +The client tracks rate limits from the API by reading the `X-Api-Ratelimit-*` headers on every response. The `client.rateLimits` property is populated by the eager `/user/` call during construction (awaited via `client.ready`). + +```typescript +const client = new MarketDataClient(); +await client.ready; // ensures rateLimits is populated + +if (client.rateLimits) { + console.log(`Limit: ${client.rateLimits.requestsLimit}`); + console.log(`Remaining: ${client.rateLimits.requestsRemaining}`); + console.log(`Consumed: ${client.rateLimits.requestsConsumed}`); + console.log(`Reset at: ${new Date(client.rateLimits.requestsReset * 1000)}`); +} +``` + +#### UserRateLimits + +```typescript +interface UserRateLimits { + requestsLimit: number; // Total API credits allowed + requestsRemaining: number; // API credits remaining + requestsConsumed: number; // API credits consumed + requestsReset: number; // Unix timestamp when the limit resets +} +``` + +**Note:** Rate limits are tracked via the following response headers: + +- `x-api-ratelimit-limit`: Total API credits allowed +- `x-api-ratelimit-remaining`: Number of API credits remaining +- `x-api-ratelimit-consumed`: Number of API credits consumed +- `x-api-ratelimit-reset`: Unix timestamp when the rate limit resets + +If rate limits have been fetched and `requestsRemaining` is `0`, the next resource call will fail fast with a `RateLimitError` rather than hitting the API. + + +## Logging + +The SDK includes a built-in logger that outputs diagnostic information. You can pass a custom logger or use the default. + +```typescript +import { MarketDataClient, DefaultLogger, LogLevel } from "@marketdata/sdk"; + +// Default logger (INFO level) +const client1 = new MarketDataClient(); + +// Debug logging (more verbose — shows request URLs, response timings, token suffix) +const client2 = new MarketDataClient({ debug: true }); + +// Custom log level +const logger = new DefaultLogger(LogLevel.WARN); +const client3 = new MarketDataClient({ logger }); +``` + +**Log levels** (in order of verbosity): `DEBUG`, `INFO`, `WARN`, `ERROR`. + +You can also set the default level via the `MARKETDATA_LOGGING_LEVEL` environment variable. + +The default logger obfuscates tokens in log output, showing only the last 4 characters. + +## Configuration + +The SDK supports flexible configuration of universal parameters through environment variables, constructor arguments, and per-method overrides. See the [Settings](/sdk/js/settings) documentation for complete details on all available configuration options and how they interact. diff --git a/sdk/js/funds/candles.mdx b/sdk/js/funds/candles.mdx new file mode 100644 index 00000000..5314564a --- /dev/null +++ b/sdk/js/funds/candles.mdx @@ -0,0 +1,180 @@ +--- +title: Candles +sidebar_position: 1 +--- + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +Retrieve historical OHLC (open/high/low/close) candles for any supported mutual fund symbol. + +## Making Requests + +Use the `candles()` method on the `funds` resource to fetch fund candles. + +:::info +Mutual fund candles are daily-resolution only (unlike stock candles). They do not include a `volume` field. +::: + +| Output Format | Result Payload | Description | +|------------------------|-----------------------------------------|-------------------------------------------------| +| **internal** (default) | `FundsCandle[]` or `FundsCandleHuman[]` | Array of decoded candle records, one per bar. | +| **json** | Raw JSON object | The compressed, array-keyed response object. | +| **csv** | `Blob` | CSV payload. Use `.save(filename)` to write it. | + + +## candles + +```typescript +// Positional form +candles

( + symbol: string, + params?: P, +): MarketDataPromise + +// Object form +candles

( + params: P & { symbol: string }, +): MarketDataPromise +``` + +Fetches historical daily candles for a single fund symbol. + +#### Parameters + +- `symbol` (string) + + The fund symbol (e.g. `"VFINX"`). + +- `resolution` (string, optional, default `"D"`) + + The candle resolution. Funds support daily-and-up only: `"D"`, `"daily"`, `"1D"`, `"W"`, `"weekly"`, `"1W"`, `"M"`, `"monthly"`, `"1M"`, `"Y"`, `"yearly"`, `"1Y"`, or any integer prefix of those units. + +- `from` (string | Date, optional) + + Start of the date range. + +- `to` (string | Date, optional) + + End of the date range. + +- `countback` (number, optional) + + Fetch `N` candles before `to` (mutually exclusive with `from`). + +- [`outputFormat`](/sdk/js/settings#output-format) (optional): The format of the returned data. Alias: `format`. +- [`dateFormat`](/sdk/js/settings#date-format) (optional): Date format. Alias: `dateformat`. +- [`columns`](/sdk/js/settings#columns) (optional): Columns to include. +- [`addHeaders`](/sdk/js/settings#headers) (optional): Whether to include headers in CSV output. Alias: `headers`. +- [`useHumanReadable`](/sdk/js/settings#human-readable) (optional): Use human-readable field names. Alias: `human`. +- [`mode`](/sdk/js/settings#data-mode) (optional): The data mode to use. + +#### Returns + +- [`MarketDataPromise`](/sdk/js/client#MarketDataPromise) + + + + +```typescript +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + // VFINX is available without authentication (free test symbol). + const candles = await client.funds.candles("VFINX", { countback: 30 }); + for (const c of candles) { + console.log(`t=${c.t} o=${c.o} h=${c.h} l=${c.l} c=${c.c}`); + } +} catch (error) { + console.error(error); +} +``` + + + + + +```typescript +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const candles = await client.funds.candles("VFINX", { + resolution: "M", + from: "2020-01-01", + to: "2024-12-31", + }); + console.log(`${candles.length} monthly bars`); +} catch (error) { + console.error(error); +} +``` + + + + + +```typescript +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const candles = await client.funds.candles("VFINX", { + countback: 10, + human: true, + }); + for (const c of candles) { + console.log(`Date=${c.Date} Open=${c.Open} Close=${c.Close}`); + } +} catch (error) { + console.error(error); +} +``` + + + + + +## FundsCandle + +```typescript +interface FundsCandle { + s?: string; + t: number; // timestamp + o: number; // open + h: number; // high + l: number; // low + c: number; // close +} +``` + +`FundsCandle` uses the API's short, machine-readable field names. + +#### Properties + +- `s` (string, optional): Status indicator. +- `t` (number): Unix timestamp of the bar. +- `o` (number): Opening NAV. +- `h` (number): High NAV. +- `l` (number): Low NAV. +- `c` (number): Closing NAV. + + +## FundsCandleHuman + +```typescript +interface FundsCandleHuman { + s?: string; + Date: number; + Open: number; + High: number; + Low: number; + Close: number; +} +``` + +`FundsCandleHuman` is returned when `human: true` is set. diff --git a/sdk/js/funds/index.mdx b/sdk/js/funds/index.mdx new file mode 100644 index 00000000..0e223724 --- /dev/null +++ b/sdk/js/funds/index.mdx @@ -0,0 +1,14 @@ +--- +title: Funds +slug: /js/funds +sidebar_position: 30 +--- + +The JavaScript SDK from Market Data provides methods designed to streamline your use of the following Funds endpoints. These intuitive methods provide a seamless interface for accessing mutual fund data. + +## Funds Endpoints + +import DocCardList from "@theme/DocCardList"; +import { useCurrentSidebarCategory } from "@docusaurus/theme-common"; + + diff --git a/sdk/js/index.mdx b/sdk/js/index.mdx new file mode 100644 index 00000000..752b930c --- /dev/null +++ b/sdk/js/index.mdx @@ -0,0 +1,61 @@ +--- +title: JavaScript SDK +sidebar_position: 4 +slug: /js +sidebar_custom_props: + badge: n +--- + +Welcome to the Market Data JavaScript SDK documentation. This SDK allows you to integrate Market Data services into your Node.js and TypeScript applications. It ships typed responses, runtime schema validation, and a functional error-handling pattern. + +## Quick Start + +```typescript +import { MarketDataClient } from "@marketdata/sdk"; + +// Initialize client +const client = new MarketDataClient({ + token: "YOUR_API_TOKEN", // Optional - runs in demo mode without token +}); + +// Get stock prices +const prices = await client.stocks.prices("AAPL"); +console.log(prices[0].mid); // 150.25 + +// Get historical candles +const candles = await client.stocks.candles("AAPL", { + resolution: "1H", + from: new Date("2024-01-01"), + to: new Date("2024-01-31"), +}); + +// Get market status +const status = await client.markets.status(); +``` + +## Open Source + +The SDK is open source and available on GitHub. Feel free to contribute to the project, report bugs, or request new features. + +- [JavaScript SDK GitHub](https://github.com/MarketDataApp/sdk-js/) + +## Documentation + +The best source for documentation on the SDK is right here. This documentation is the most up-to-date and accurate source of information on the SDK. + +## Using the SDK + +This SDK is designed to help you get up and running with Market Data's APIs as quickly as possible, providing you with all the tools you need to access real-time stock and options prices, historical data, and much more. + +### Getting Started + +1. [Install the SDK](/sdk/js/installation) into a Node.js or TypeScript project. +2. Set up your [authentication token](/sdk/js/authentication) to access the API. +3. Learn about the [client](/sdk/js/client) and how to make your first API requests. +4. Configure [Settings](/sdk/js/settings) to customize output format, date format, and other universal parameters. +5. Explore the available endpoints for [stocks](/sdk/js/stocks), [options](/sdk/js/options), [funds](/sdk/js/funds), [markets](/sdk/js/markets), and [utilities](/sdk/js/utilities). + +### Support + +- If you have any questions or need further assistance, please don't hesitate to open a ticket at our [helpdesk](https://www.marketdata.app/dashboard/). +- If you find a bug you may also [open an issue in our GitHub repository](https://github.com/MarketDataApp/sdk-js/issues). Please only open issues if you find a bug. Use our helpdesk for general questions or implementation assistance. diff --git a/sdk/js/installation.mdx b/sdk/js/installation.mdx new file mode 100644 index 00000000..fd2a14f5 --- /dev/null +++ b/sdk/js/installation.mdx @@ -0,0 +1,90 @@ +--- +title: Installation +sidebar_position: 1 +--- + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +This guide will help you install the Market Data JavaScript SDK and configure it for your needs. + +## Prerequisites + +- Node.js >= 20 +- A package manager: [pnpm](https://pnpm.io/) (used by the SDK itself), [npm](https://npmjs.com), or [yarn](https://yarnpkg.com) + +## Basic Installation + + + + +```bash +pnpm add @marketdata/sdk +``` + + + + +```bash +npm install @marketdata/sdk +``` + + + + +```bash +yarn add @marketdata/sdk +``` + + + + +## TypeScript Support + +The SDK is written in TypeScript and ships first-class type definitions. No `@types/*` package is needed. The SDK builds to both ESM and CommonJS, so it works with either module system. + +- **ESM**: `import { MarketDataClient } from "@marketdata/sdk";` +- **CommonJS**: `const { MarketDataClient } = require("@marketdata/sdk");` + +## Local Development Installation + +For local development, clone the repository and install from the project directory: + +```bash +# Clone the repository +git clone https://github.com/MarketDataApp/sdk-js.git +cd sdk-js + +# Install dependencies +pnpm install + +# Run the test suite (all mocked, no API calls) +pnpm test + +# Build the dual CJS+ESM bundle +pnpm build +``` + +The project uses [Corepack](https://nodejs.org/api/corepack.html) to pin the pnpm version. If `pnpm` is not available, enable Corepack first: + +```bash +corepack enable +``` + +## Core Dependencies + +The SDK includes the following core dependencies (installed automatically): + +- `dotenv`: Loads environment variables from a `.env` file +- `neverthrow`: Functional `Result` type used internally for error composition. The public API surfaces standard Promises — see [client error handling](/sdk/js/client#ErrorHandling). +- `p-limit`: Concurrency pool for fan-out requests +- `p-retry`: Retry logic with exponential backoff +- `zod`: Runtime schema validation and type inference + +## Next Steps + +After installation, you'll need to: + +1. Set up your [authentication token](/sdk/js/authentication) +2. Learn about the [client](/sdk/js/client) and how to make your first API requests +3. Configure [settings](/sdk/js/settings) to customize output format, date format, and other universal parameters diff --git a/sdk/js/markets/index.mdx b/sdk/js/markets/index.mdx new file mode 100644 index 00000000..c3d6a406 --- /dev/null +++ b/sdk/js/markets/index.mdx @@ -0,0 +1,14 @@ +--- +title: Markets +slug: /js/markets +sidebar_position: 40 +--- + +The JavaScript SDK from Market Data provides methods designed to streamline your use of the following Markets endpoints. These intuitive methods provide a seamless interface for accessing market status information. + +## Markets Endpoints + +import DocCardList from "@theme/DocCardList"; +import { useCurrentSidebarCategory } from "@docusaurus/theme-common"; + + diff --git a/sdk/js/markets/status.mdx b/sdk/js/markets/status.mdx new file mode 100644 index 00000000..b6a6a111 --- /dev/null +++ b/sdk/js/markets/status.mdx @@ -0,0 +1,172 @@ +--- +title: Status +sidebar_position: 1 +--- + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +Retrieve market status information (open/closed) for dates and countries. + +## Making Requests + +Use the `status()` method on the `markets` resource to fetch market status information. + +| Output Format | Result Payload | Description | +|------------------------|-------------------------------------------|-------------------------------------------------| +| **internal** (default) | `MarketStatus[]` or `MarketStatusHuman[]` | Array of decoded market-status records. | +| **json** | Raw JSON object | The compressed, array-keyed response object. | +| **csv** | `Blob` | CSV payload. Use `.save(filename)` to write it. | + + +## status + +```typescript +status

( + params?: P, +): MarketDataPromise +``` + +Fetches market status information. All parameters are optional. + +#### Parameters + +- `country` (string, optional) + + Filter by country code (e.g. `"US"`, `"CA"`, `"GB"`). + +- `date` (string | Date, optional) + + Specific date to fetch status for. + +- `from` (string | Date, optional) + + Start of the date range. + +- `to` (string | Date, optional) + + End of the date range. + +- `countback` (number, optional) + + Number of days to return, counting backwards from `to` (or the current date if `to` is not provided). + +- [`outputFormat`](/sdk/js/settings#output-format) (optional): The format of the returned data. Alias: `format`. +- [`dateFormat`](/sdk/js/settings#date-format) (optional): Date format. Alias: `dateformat`. +- [`columns`](/sdk/js/settings#columns) (optional): Columns to include. +- [`addHeaders`](/sdk/js/settings#headers) (optional): Whether to include headers in CSV output. Alias: `headers`. +- [`useHumanReadable`](/sdk/js/settings#human-readable) (optional): Use human-readable field names. Alias: `human`. +- [`mode`](/sdk/js/settings#data-mode) (optional): The data mode to use. + +#### Returns + +- [`MarketDataPromise`](/sdk/js/client#MarketDataPromise) + + + + +```typescript +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const days = await client.markets.status(); + for (const d of days) { + console.log(`date=${d.date} status=${d.status}`); + } +} catch (error) { + console.error(error); +} +``` + + + + + +```typescript +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const days = await client.markets.status({ + from: "2024-01-01", + to: "2024-01-31", + country: "US", + }); + console.log(days); +} catch (error) { + console.error(error); +} +``` + + + + + +```typescript +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + // Last 7 trading-status days + const days = await client.markets.status({ countback: 7 }); + console.log(days); +} catch (error) { + console.error(error); +} +``` + + + + + +```typescript +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const days = await client.markets.status({ countback: 7, human: true }); + for (const d of days) { + console.log(`Date=${d.Date} Status=${d.Status}`); + } +} catch (error) { + console.error(error); +} +``` + + + + + +## MarketStatus + +```typescript +interface MarketStatus { + s?: string; + date: number; + status: string; +} +``` + +#### Properties + +- `s` (string, optional): Status indicator. +- `date` (number): Unix timestamp of the trading day. +- `status` (string): Market status (e.g. `"open"`, `"closed"`). + + +## MarketStatusHuman + +```typescript +interface MarketStatusHuman { + s?: string; + Date: number; + Status: string; +} +``` + +`MarketStatusHuman` is returned when `human: true` is set. diff --git a/sdk/js/options/chain.mdx b/sdk/js/options/chain.mdx new file mode 100644 index 00000000..d69eff43 --- /dev/null +++ b/sdk/js/options/chain.mdx @@ -0,0 +1,265 @@ +--- +title: Chain +sidebar_position: 1 +--- + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +Retrieve the full option chain — every call and put contract — for any supported underlying symbol, with extensive filtering options. + +## Making Requests + +Use the `chain()` method on the `options` resource to fetch an option chain. The response includes Greeks (delta, gamma, theta, vega, IV), intrinsic/extrinsic value, and underlying price alongside the quote fields. + +| Output Format | Result Payload | Description | +|------------------------|-------------------------------------------|-------------------------------------------------| +| **internal** (default) | `OptionsChain[]` or `OptionsChainHuman[]` | Array of decoded option-contract records. | +| **json** | Raw JSON object | The compressed, array-keyed response object. | +| **csv** | `Blob` | CSV payload. Use `.save(filename)` to write it. | + + +## chain + +```typescript +// Positional form +chain

( + symbol: string, + params?: P, +): MarketDataPromise + +// Object form +chain

( + params: P & { symbol: string }, +): MarketDataPromise +``` + +Fetches the option chain for a single underlying symbol. + +#### Parameters + +- `symbol` (string) + + The underlying stock symbol (e.g. `"AAPL"`). + +- `date` (string | Date, optional) + + Fetch the chain as-of a specific historical date (end-of-day snapshot). + +- `expiration` (string | Date, optional) + + Filter by specific expiration date. + +- `dte` (number, optional) + + Filter by number of days to expiration. + +- `from` / `to` (string | Date, optional) + + Range filter for expiration dates. + +- `month` (number, optional, 1–12), `year` (number, optional) + + Filter by expiration month and/or year. + +- `weekly` / `monthly` / `quarterly` (boolean, optional) + + Filter by expiration cycle type. + +- `strike` (number | string, optional) + + Filter by specific strike price. + +- `delta` (number, optional) + + Filter by target delta value. + +- `strikeLimit` (number, optional) + + Limit the response to the N strikes nearest the underlying price. + +- `range` (string, optional) + + Filter by in-the-money / out-of-the-money range (e.g. `"itm"`, `"otm"`). + +- `minBid` / `maxBid` / `minAsk` / `maxAsk` (number, optional) + + Filter by quote price thresholds. + +- `maxBidAskSpread` (number, optional), `maxBidAskSpreadPct` (number, optional) + + Filter by spread thresholds. + +- `minOpenInterest` (number, optional), `minVolume` (number, optional) + + Filter by liquidity thresholds. + +- `nonstandard` (boolean, optional) + + Include or exclude non-standard contracts. + +- `side` (`"call"` | `"put"` | `"both"`, optional) + + Filter by option side. + +- `am` / `pm` (boolean, optional) + + Filter by AM-settled or PM-settled contracts. + +- [`outputFormat`](/sdk/js/settings#output-format) (optional): The format of the returned data. Alias: `format`. +- [`dateFormat`](/sdk/js/settings#date-format) (optional): Date format. Alias: `dateformat`. +- [`columns`](/sdk/js/settings#columns) (optional): Columns to include. +- [`addHeaders`](/sdk/js/settings#headers) (optional): Whether to include headers in CSV output. Alias: `headers`. +- [`useHumanReadable`](/sdk/js/settings#human-readable) (optional): Use human-readable field names. Alias: `human`. +- [`mode`](/sdk/js/settings#data-mode) (optional): The data mode to use. + +#### Returns + +- [`MarketDataPromise`](/sdk/js/client#MarketDataPromise) + + + + +```typescript +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const contracts = await client.options.chain("AAPL"); + console.log(`Got ${contracts.length} contracts`); +} catch (error) { + console.error(error); +} +``` + + + + + +```typescript +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + // Calls only, narrow strike window, high open interest + const contracts = await client.options.chain("AAPL", { + side: "call", + strikeLimit: 10, + minOpenInterest: 100, + }); + for (const c of contracts) { + console.log( + `${c.optionSymbol} strike=${c.strike} mid=${c.mid} delta=${c.delta} oi=${c.openInterest}` + ); + } +} catch (error) { + console.error(error); +} +``` + + + + + +```typescript +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const contracts = await client.options.chain("AAPL", { + date: "2024-01-15", + expiration: "2024-02-16", + side: "call", + }); + console.log(contracts); +} catch (error) { + console.error(error); +} +``` + + + + + +```typescript +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const contracts = await client.options.chain("AAPL", { + side: "put", + human: true, + }); + for (const c of contracts) { + console.log( + `${c.Symbol} Strike=${c.Strike} Mid=${c.Mid} Delta=${c.Delta}` + ); + } +} catch (error) { + console.error(error); +} +``` + + + + + +## OptionsChain + +```typescript +interface OptionsChain { + s?: string; + optionSymbol: string; + underlying: string; + expiration: number; + side: string; + strike: number; + firstTraded: number; + dte: number; + updated: number; + bid: number | null; + bidSize: number | null; + mid: number | null; + ask: number | null; + askSize: number | null; + last: number | null; + openInterest: number | null; + volume: number | null; + inTheMoney: boolean; + intrinsicValue: number; + extrinsicValue: number; + underlyingPrice: number; + iv: number | null; + delta: number | null; + gamma: number | null; + theta: number | null; + vega: number | null; +} +``` + +#### Properties + +- `optionSymbol` (string): OCC-format option symbol (e.g. `"AAPL271217C00250000"`). +- `underlying` (string): The underlying stock symbol. +- `expiration` (number): Unix timestamp of the expiration date. +- `side` (string): `"call"` or `"put"`. +- `strike` (number): The strike price. +- `firstTraded` (number): Unix timestamp when the contract first traded. +- `dte` (number): Days to expiration. +- `updated` (number): Unix timestamp when the quote was last updated. +- `bid`, `ask`, `mid`, `last` (number | null): Quote prices. +- `bidSize`, `askSize` (number | null): Quote sizes. +- `openInterest`, `volume` (number | null): Liquidity metrics. +- `inTheMoney` (boolean): Whether the contract is currently in the money. +- `intrinsicValue`, `extrinsicValue` (number): Value decomposition. +- `underlyingPrice` (number): Price of the underlying at quote time. +- `iv`, `delta`, `gamma`, `theta`, `vega` (number | null): Implied volatility and Greeks. + + +## OptionsChainHuman + +When `human: true` is set, field names are returned in `Title_Case`: `Symbol`, `Underlying`, `Expiration_Date`, `Option_Side`, `Strike`, `Days_To_Expiration`, `Bid`, `Ask`, `Mid`, `Open_Interest`, `Volume`, `In_The_Money`, `IV`, `Delta`, `Gamma`, `Theta`, `Vega`, etc. diff --git a/sdk/js/options/expirations.mdx b/sdk/js/options/expirations.mdx new file mode 100644 index 00000000..55cbfb48 --- /dev/null +++ b/sdk/js/options/expirations.mdx @@ -0,0 +1,146 @@ +--- +title: Expirations +sidebar_position: 3 +--- + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +Retrieve the list of current or historical option expiration dates for a given underlying symbol. + +## Making Requests + +Use the `expirations()` method on the `options` resource to fetch available expiration dates. + +| Output Format | Result Payload | Description | +|------------------------|-------------------------------------------------------------------|------------------------------------------| +| **internal** (default) | `OptionsExpirationsResponse` or `OptionsExpirationsHumanResponse` | Object with the list of expirations. | +| **json** | Raw JSON object | The raw response as returned by the API. | + + +## expirations + +```typescript +// Positional form +expirations

( + symbol: string, + params?: P, +): MarketDataPromise + +// Object form +expirations

( + params: P & { symbol: string }, +): MarketDataPromise +``` + +Fetches the list of expirations for a single underlying symbol. + +#### Parameters + +- `symbol` (string) + + The underlying stock symbol. + +- `strike` (number, optional) + + Filter to expirations that have at least one contract at the given strike. + +- `date` (string | Date, optional) + + Fetch expirations as-of a specific historical date. + +- [`outputFormat`](/sdk/js/settings#output-format) (optional): The format of the returned data. Alias: `format`. +- [`dateFormat`](/sdk/js/settings#date-format) (optional): Date format. Alias: `dateformat`. +- [`columns`](/sdk/js/settings#columns) (optional): Columns to include. +- [`useHumanReadable`](/sdk/js/settings#human-readable) (optional): Use human-readable field names. Alias: `human`. + +#### Returns + +- [`MarketDataPromise`](/sdk/js/client#MarketDataPromise) + + + + +```typescript +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const data = await client.options.expirations("AAPL"); + console.log(`Updated: ${data.updated}`); + for (const exp of data.expirations) { + console.log(exp); + } +} catch (error) { + console.error(error); +} +``` + + + + + +```typescript +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + // Only expirations that trade the $250 strike + const data = await client.options.expirations("AAPL", { strike: 250 }); + console.log(data.expirations); +} catch (error) { + console.error(error); +} +``` + + + + + +```typescript +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const data = await client.options.expirations("AAPL", { human: true }); + console.log(data.Expirations); +} catch (error) { + console.error(error); +} +``` + + + + + +## OptionsExpirationsResponse + +```typescript +interface OptionsExpirationsResponse { + s: string; + expirations: string[]; + updated: number; +} +``` + +#### Properties + +- `s` (string): Status indicator. +- `expirations` (string[]): List of expiration dates in `YYYY-MM-DD` format. +- `updated` (number): Unix timestamp when the data was last updated. + + +## OptionsExpirationsHumanResponse + +```typescript +interface OptionsExpirationsHumanResponse { + s?: string; + Expirations: string[]; + Date: number; +} +``` + +`OptionsExpirationsHumanResponse` is returned when `human: true` is set. diff --git a/sdk/js/options/index.mdx b/sdk/js/options/index.mdx new file mode 100644 index 00000000..6169c7ec --- /dev/null +++ b/sdk/js/options/index.mdx @@ -0,0 +1,14 @@ +--- +title: Options +slug: /js/options +sidebar_position: 20 +--- + +The JavaScript SDK from Market Data provides methods designed to streamline your use of the following Options endpoints. These intuitive methods provide a seamless interface for accessing options data including chains, expirations, quotes, and lookup functionality. + +## Options Endpoints + +import DocCardList from "@theme/DocCardList"; +import { useCurrentSidebarCategory } from "@docusaurus/theme-common"; + + diff --git a/sdk/js/options/lookup.mdx b/sdk/js/options/lookup.mdx new file mode 100644 index 00000000..7b017855 --- /dev/null +++ b/sdk/js/options/lookup.mdx @@ -0,0 +1,111 @@ +--- +title: Lookup +sidebar_position: 4 +--- + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +Convert a human-readable option description (e.g. `"AAPL 7/28/2023 200 Call"`) into its standard OCC option symbol. + +## Making Requests + +Use the `lookup()` method on the `options` resource to resolve an OCC symbol from a natural-language description. The URL-encoded lookup string is handled for you by the SDK. + + +## lookup + +```typescript +// Positional form +lookup

( + lookupStr: string, + params?: P, +): MarketDataPromise + +// Object form +lookup

( + params: P & { lookup: string }, +): MarketDataPromise +``` + +Resolves a human-readable option description to its OCC symbol. + +#### Parameters + +- `lookupStr` (string) + + A natural-language description of the option contract (e.g. `"AAPL 7/28/2023 200 Call"`, `"TSLA Jan 2025 300 Put"`). + +- [`outputFormat`](/sdk/js/settings#output-format) (optional): The format of the returned data. Alias: `format`. +- [`useHumanReadable`](/sdk/js/settings#human-readable) (optional): Use human-readable field names. Alias: `human`. + +#### Returns + +- [`MarketDataPromise`](/sdk/js/client#MarketDataPromise) + + + + +```typescript +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const data = await client.options.lookup("AAPL 7/28/2023 200 Call"); + console.log(data.optionSymbol); // "AAPL230728C00200000" +} catch (error) { + console.error(error); +} +``` + + + + + +```typescript +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const data = await client.options.lookup("AAPL 7/28/2023 200 Call", { + human: true, + }); + console.log(data.Symbol); +} catch (error) { + console.error(error); +} +``` + + + + + +## OptionsLookupResponse + +```typescript +interface OptionsLookupResponse { + s: string; + optionSymbol: string; + updated?: number; +} +``` + +#### Properties + +- `s` (string): Status indicator. +- `optionSymbol` (string): The OCC-format option symbol. +- `updated` (number, optional): Unix timestamp when the lookup resolved. + + +## OptionsLookupHumanResponse + +```typescript +interface OptionsLookupHumanResponse { + s?: string; + Symbol: string; +} +``` + +`OptionsLookupHumanResponse` is returned when `human: true` is set. diff --git a/sdk/js/options/quotes.mdx b/sdk/js/options/quotes.mdx new file mode 100644 index 00000000..8d0637e1 --- /dev/null +++ b/sdk/js/options/quotes.mdx @@ -0,0 +1,132 @@ +--- +title: Quotes +sidebar_position: 2 +--- + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +Retrieve quotes for one or more specific option contracts, including full Greeks. + +## Making Requests + +Use the `quotes()` method on the `options` resource to fetch quotes for individual option contracts identified by their OCC symbol. When multiple symbols are provided, the SDK fans out concurrently and merges the results. + +| Output Format | Result Payload | Description | +|------------------------|---------------------------------------------|---------------------------------------------------| +| **internal** (default) | `OptionsQuotes[]` or `OptionsQuotesHuman[]` | Array of decoded quote records, one per contract. | +| **json** | Raw JSON object | The compressed, array-keyed response object. | +| **csv** | `Blob` | CSV payload. Use `.save(filename)` to write it. | + + +## quotes + +```typescript +// Positional form +quotes

( + symbols: string | string[], + params?: P, +): MarketDataPromise + +// Object form +quotes

( + params: P & { symbols: string | string[] }, +): MarketDataPromise +``` + +Fetches quotes for one or more option contracts. + +#### Parameters + +- `symbols` (string | string[]) + + A single OCC-format option symbol (e.g. `"AAPL271217C00250000"`) or an array of symbols. When you pass an array, the SDK fetches each contract concurrently and merges the results. + +- [`outputFormat`](/sdk/js/settings#output-format) (optional): The format of the returned data. Alias: `format`. +- [`dateFormat`](/sdk/js/settings#date-format) (optional): Date format. Alias: `dateformat`. +- [`columns`](/sdk/js/settings#columns) (optional): Columns to include. +- [`addHeaders`](/sdk/js/settings#headers) (optional): Whether to include headers in CSV output. Alias: `headers`. +- [`useHumanReadable`](/sdk/js/settings#human-readable) (optional): Use human-readable field names. Alias: `human`. +- [`mode`](/sdk/js/settings#data-mode) (optional): The data mode to use. + +Additional endpoint-specific parameters like `from`, `to`, and `date` are passed through to the REST API. See [REST API Options Quotes](/api/options/quotes) for the full list. + +#### Returns + +- [`MarketDataPromise`](/sdk/js/client#MarketDataPromise) + + + + +```typescript +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const quotes = await client.options.quotes("AAPL271217C00250000"); + const q = quotes[0]; + console.log( + `${q.optionSymbol}: bid=${q.bid} ask=${q.ask} delta=${q.delta}` + ); +} catch (error) { + console.error(error); +} +``` + + + + + +```typescript +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const quotes = await client.options.quotes([ + "AAPL271217C00250000", + "AAPL271217P00250000", + "AAPL271217C00300000", + ]); + for (const q of quotes) { + console.log(`${q.optionSymbol}: mid=${q.mid}`); + } +} catch (error) { + console.error(error); +} +``` + + + + + +```typescript +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const quotes = await client.options.quotes("AAPL271217C00250000", { + human: true, + }); + console.log(quotes[0]); +} catch (error) { + console.error(error); +} +``` + + + + + +## OptionsQuotes + +`OptionsQuotes` uses the same field shape as [`OptionsChain`](/sdk/js/options/chain#OptionsChain): `optionSymbol`, `underlying`, `expiration`, `side`, `strike`, `bid`, `ask`, `mid`, `last`, `openInterest`, `volume`, `inTheMoney`, `intrinsicValue`, `extrinsicValue`, `iv`, `delta`, `gamma`, `theta`, `vega`, and so on. + +See [OptionsChain](/sdk/js/options/chain#OptionsChain) for the complete list of fields. + + +## OptionsQuotesHuman + +`OptionsQuotesHuman` is returned when `human: true` is set. Field shape mirrors [`OptionsChainHuman`](/sdk/js/options/chain#OptionsChainHuman) — same columns, `Title_Case` names. diff --git a/sdk/js/settings.mdx b/sdk/js/settings.mdx new file mode 100644 index 00000000..600f84d9 --- /dev/null +++ b/sdk/js/settings.mdx @@ -0,0 +1,326 @@ +--- +title: Settings +sidebar_position: 4 +--- + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +The Market Data JavaScript SDK provides flexible configuration for customizing API requests. You can configure universal parameters like output format, date format, data mode, and more through multiple methods with different priority levels. + +## Configuration Priority + +The SDK supports three ways to configure universal parameters, with a clear priority hierarchy. + +### Priority Order (Lowest to Highest) + +1. **Environment Variables** (lowest priority) + - Read from variables like `MARKETDATA_OUTPUT_FORMAT`, `MARKETDATA_DATE_FORMAT`, etc. + - A `.env` file in your working directory is loaded automatically via [`dotenv`](https://github.com/motdotla/dotenv). + - Applied globally to all API calls unless overridden. + +2. **Client Constructor Arguments** (medium priority) + - Set via the `MarketDataConfig` object passed to `new MarketDataClient({ ... })`. + - Applied to all API calls made with that client instance unless overridden. + - Only applies to config-level settings (`token`, `baseUrl`, `apiVersion`, retry options). + +3. **Direct Method Parameters** (highest priority) + - Passed directly as parameters to resource methods. + - Applied only to that specific API call. + +### How It Works + +When making an API call, the SDK merges parameters in this order: + +1. **Start with environment variables** (lowest priority) + - Values are read from `MARKETDATA_*` variables on startup. + +2. **Override with constructor arguments** (medium priority) + - Values passed to `new MarketDataClient({ ... })` replace env values for config-level settings. + +3. **Override with direct method parameters** (highest priority) + - Parameters passed directly to resource methods like `client.stocks.prices("AAPL", { human: true })` take final priority. + +### Examples + + + + +Set universal parameters via environment variables: + +```bash +export MARKETDATA_OUTPUT_FORMAT=json +export MARKETDATA_DATE_FORMAT=timestamp +export MARKETDATA_USE_HUMAN_READABLE=true +``` + +```typescript +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +// All calls will use the env-var defaults unless overridden. +const result = await client.stocks.prices("AAPL"); +``` + + + + + +Set client-level configuration via the constructor: + +```typescript +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient({ + token: "your_token_here", + baseUrl: "https://api.marketdata.app", + apiVersion: "v1", + maxRetries: 5, + retryInitialWait: 1, + retryMaxWait: 15, + retryFactor: 2, + debug: true, +}); +``` + +Constructor arguments override environment variables for the settings they cover. + + + + + +Pass parameters directly to resource methods: + +```typescript +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +// Override per call — these parameters apply to this request only. +const result = await client.stocks.prices("AAPL", { + human: true, + dateformat: "timestamp", + columns: ["ask", "bid", "mid"], +}); +``` + +Direct method parameters override both environment variables and constructor arguments. + + + + +## Available Configuration Options + +### Output Format + +Controls the format of the API response data. + +**Available Values:** +- `"internal"` (default): Returns decoded plain JavaScript objects (the array-keyed wire format is unpacked into one record per row). +- `"json"`: Returns the raw JSON response from the API, without unpacking. +- `"csv"`: Returns a `Blob` containing CSV data. Use `.save(filename)` on the result to write it to disk. + +The SDK also exposes an `OutputFormat` enum you can import for type safety: + +```typescript +import { OutputFormat } from "@marketdata/sdk"; +// OutputFormat.INTERNAL, OutputFormat.JSON, OutputFormat.CSV +``` + +**Configuration Methods:** + +1. **Environment Variable:** `MARKETDATA_OUTPUT_FORMAT` (values: `internal`, `json`, `csv`) +2. **Per-call parameter:** `format: "csv"` or `outputFormat: "csv"` + +**Example:** + +```typescript +import { MarketDataClient, OutputFormat } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +// Default (internal) — returns plain JS objects +const defaultResult = await client.stocks.prices("AAPL"); + +// CSV — returns a Blob +const csvResult = await client.stocks.prices("AAPL", { outputFormat: OutputFormat.CSV }); +await csvResult.save("prices.csv"); // writes to disk +``` + +For more details, see the [API Format documentation](/api/universal-parameters/format). + +### Date Format + +Specifies the format for date and time information in responses. + +**Available Values:** +- `"timestamp"`: ISO 8601 timestamp format (e.g., `2020-12-30 16:00:00 -05:00`) +- `"unix"`: Unix timestamp in seconds (e.g., `1609362000`) +- `"spreadsheet"`: Excel spreadsheet format (e.g., `44195.66667`) + +The SDK also exposes a `DateFormat` enum: + +```typescript +import { DateFormat } from "@marketdata/sdk"; +// DateFormat.TIMESTAMP, DateFormat.UNIX, DateFormat.SPREADSHEET +``` + +**Configuration Methods:** + +1. **Environment Variable:** `MARKETDATA_DATE_FORMAT` (values: `timestamp`, `unix`, `spreadsheet`) +2. **Per-call parameter:** `dateformat: "unix"` or `dateFormat: "unix"` + +**Example:** + +```typescript +import { MarketDataClient, DateFormat } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +const candles = await client.stocks.candles("AAPL", { dateFormat: DateFormat.TIMESTAMP }); +``` + +For more details, see the [API Date Format documentation](/api/universal-parameters/date-format). + +### Columns + +Limits the response to only the columns you need, reducing data transfer and processing time. + +**Type:** `string[]` + +**Configuration Methods:** + +1. **Environment Variable:** `MARKETDATA_COLUMNS` (comma-separated list, e.g., `ask,bid`) +2. **Per-call parameter:** `columns: ["ask", "bid"]` + +**Example:** + +```typescript +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +const quotes = await client.stocks.quotes("AAPL", { columns: ["ask", "bid"] }); +``` + +For more details, see the [API Columns documentation](/api/universal-parameters/columns). + +### Headers + +Controls whether headers are included in CSV output. + +**Type:** `boolean` + +**Default:** `true` + +**Configuration Methods:** + +1. **Environment Variable:** `MARKETDATA_ADD_HEADERS` (values: `true`, `false`) +2. **Per-call parameter:** `headers: false` or `addHeaders: false` + +**Example:** + +```typescript +import { MarketDataClient, OutputFormat } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +const csv = await client.stocks.candles("AAPL", { + outputFormat: OutputFormat.CSV, + addHeaders: false, +}); +``` + +For more details, see the [API Headers documentation](/api/universal-parameters/headers). + +### Human Readable + +Uses human-readable field names in responses instead of short, machine-friendly ones. Field names are returned in `Title_Case` (e.g. `Symbol`, `Change_Price`) rather than `camelCase`. + +**Type:** `boolean` + +**Default:** `false` + +**Configuration Methods:** + +1. **Environment Variable:** `MARKETDATA_USE_HUMAN_READABLE` (values: `true`, `false`) +2. **Per-call parameter:** `human: true` or `useHumanReadable: true` + +**Example:** + +```typescript +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const quotes = await client.stocks.quotes("AAPL", { human: true }); + console.log(quotes[0].Symbol, quotes[0].Mid); +} catch (error) { + console.error(error); +} +``` + +For more details, see the [API Human Readable documentation](/api/universal-parameters/human-readable). + +### Data Mode + +Controls whether the API returns live, cached, or delayed data. + +**Available Values:** +- `"live"`: Real-time data (paid plans). +- `"cached"`: Cached data. Lower cost per request; honours an optional `maxage` freshness window. +- `"delayed"`: 15+ minute delayed data. + +The SDK also exposes a `Mode` enum: + +```typescript +import { Mode } from "@marketdata/sdk"; +// Mode.LIVE, Mode.CACHED, Mode.DELAYED +``` + +**Configuration Methods:** + +1. **Environment Variable:** `MARKETDATA_MODE` (values: `live`, `cached`, `delayed`) +2. **Per-call parameter:** `mode: "cached"` + +**Example:** + +```typescript +import { MarketDataClient, Mode } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +const quotes = await client.stocks.quotes("AAPL", { mode: Mode.CACHED }); +``` + +For more details, see the [API Data Mode documentation](/api/universal-parameters/mode). + +## Environment Variables Reference + +| 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_OUTPUT_FORMAT` | Default output format | `internal` | +| `MARKETDATA_DATE_FORMAT` | Default date format | _(API default)_ | +| `MARKETDATA_USE_HUMAN_READABLE` | Use human-readable field names by default | `false` | +| `MARKETDATA_ADD_HEADERS` | Include headers in CSV output | `true` | +| `MARKETDATA_MODE` | Default data mode | _(API default)_ | +| `MARKETDATA_MAX_RETRIES` | Maximum retry attempts | `3` | +| `MARKETDATA_RETRY_INITIAL_WAIT` | Initial retry wait (seconds) | `0.5` | +| `MARKETDATA_RETRY_MAX_WAIT` | Maximum retry wait (seconds) | `10` | +| `MARKETDATA_RETRY_FACTOR` | Exponential backoff factor | `2` | + +## Parameter Aliases + +For convenience, the SDK accepts both the short form used by the REST API and the camelCase TypeScript form. Both resolve to the same underlying parameter: + +| Short (REST style) | Long (camelCase) | +|--------------------|--------------------| +| `dateformat` | `dateFormat` | +| `format` | `outputFormat` | +| `headers` | `addHeaders` | +| `human` | `useHumanReadable` | diff --git a/sdk/js/stocks/candles.mdx b/sdk/js/stocks/candles.mdx new file mode 100644 index 00000000..31c9686e --- /dev/null +++ b/sdk/js/stocks/candles.mdx @@ -0,0 +1,218 @@ +--- +title: Candles +sidebar_position: 3 +--- + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +Retrieve historical OHLCV (open/high/low/close/volume) candles for any supported stock symbol. + +## Making Requests + +Use the `candles()` method on the `stocks` resource to fetch candles. The method handles large intraday date ranges automatically by splitting them into year-sized chunks and fetching them concurrently (up to 50 in-flight at once), then merging the results. + +| Output Format | Result Payload | Description | +|------------------------|-----------------------------------------|-------------------------------------------------| +| **internal** (default) | `StockCandle[]` or `StockCandleHuman[]` | Array of decoded candle records, one per bar. | +| **json** | Raw JSON object | The compressed, array-keyed response object. | +| **csv** | `Blob` | CSV payload. Use `.save(filename)` to write it. | + + +## candles + +```typescript +// Positional form +candles

( + symbol: string, + params?: P, +): MarketDataPromise + +// Object form +candles

( + params: P & { symbol: string }, +): MarketDataPromise +``` + +Fetches historical candles for a single symbol. + +#### Parameters + +- `symbol` (string) + + The stock symbol to fetch candles for. + +- `resolution` (string, optional, default `"D"`) + + The candle resolution. Common values: + - Minute-based: `"1"`, `"5"`, `"15"`, `"30"`, `"45"`, `"minutely"` + - Hour-based: `"H"`, `"1H"`, `"2H"`, `"hourly"` + - Day-and-up: `"D"`, `"daily"`, `"W"`, `"weekly"`, `"M"`, `"monthly"`, `"Y"`, `"yearly"` + +- `from` (string | Date, optional) + + Start of the date range. Accepts ISO strings, Unix seconds as a string, or a `Date`. + +- `to` (string | Date, optional) + + End of the date range. Accepts ISO strings, Unix seconds as a string, or a `Date`. + +- `countback` (number, optional) + + Fetch `N` candles before `to` (mutually exclusive with `from`). + +- `extended` (boolean, optional) + + Include extended-hours data for intraday resolutions. + +- `adjustsplits` (boolean, optional) + + Adjust historical prices for splits. + +- [`outputFormat`](/sdk/js/settings#output-format) (optional): The format of the returned data. Alias: `format`. +- [`dateFormat`](/sdk/js/settings#date-format) (optional): Date format for response timestamps. Alias: `dateformat`. +- [`columns`](/sdk/js/settings#columns) (optional): Columns to include. +- [`addHeaders`](/sdk/js/settings#headers) (optional): Whether to include headers in CSV output. Alias: `headers`. +- [`useHumanReadable`](/sdk/js/settings#human-readable) (optional): Use human-readable field names. Alias: `human`. +- [`mode`](/sdk/js/settings#data-mode) (optional): The data mode to use. + +#### Returns + +- [`MarketDataPromise`](/sdk/js/client#MarketDataPromise) + +#### Notes + +- For intraday resolutions (minute- or hour-based), date ranges longer than 365 days are split into yearly chunks and fetched concurrently. The individual responses are merged back into a single chronologically ordered result. +- Up to 50 chunks can be in flight simultaneously (sliding window, not batching). + + + + +```typescript +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + // Default resolution is "D" (daily) + const candles = await client.stocks.candles("AAPL", { countback: 30 }); + for (const c of candles) { + console.log(`t=${c.t} o=${c.o} h=${c.h} l=${c.l} c=${c.c} v=${c.v}`); + } +} catch (error) { + console.error(error); +} +``` + + + + + +```typescript +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + // Hourly candles over a multi-year range. + // The SDK automatically splits into year-sized chunks + // and fetches them concurrently. + const candles = await client.stocks.candles("AAPL", { + resolution: "1H", + from: new Date("2023-01-01"), + to: new Date("2024-12-31"), + }); + console.log(`Got ${candles.length} bars`); +} catch (error) { + console.error(error); +} +``` + + + + + +```typescript +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const candles = await client.stocks.candles("AAPL", { + resolution: "D", + countback: 10, + human: true, + }); + for (const c of candles) { + console.log(`Date=${c.Date} Open=${c.Open} Close=${c.Close}`); + } +} catch (error) { + console.error(error); +} +``` + + + + + +```typescript +import { MarketDataClient, OutputFormat } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +const csv = await client.stocks.candles("AAPL", { + resolution: "D", + countback: 100, + outputFormat: OutputFormat.CSV, +}); + +const filename = await csv.save("aapl_daily.csv"); +console.log(`CSV saved to: ${filename}`); +``` + + + + + +## StockCandle + +```typescript +interface StockCandle { + s?: string; + t: number; // timestamp + o: number; // open + h: number; // high + l: number; // low + c: number; // close + v: number; // volume +} +``` + +`StockCandle` uses the API's short, machine-readable field names. + +#### Properties + +- `s` (string, optional): Status indicator. +- `t` (number): Unix timestamp of the bar. +- `o` (number): Opening price. +- `h` (number): High price. +- `l` (number): Low price. +- `c` (number): Closing price. +- `v` (number): Volume. + + +## StockCandleHuman + +```typescript +interface StockCandleHuman { + s?: string; + Date: number; + Open: number; + High: number; + Low: number; + Close: number; + Volume: number; +} +``` + +`StockCandleHuman` is returned when `human: true` is set. diff --git a/sdk/js/stocks/earnings.mdx b/sdk/js/stocks/earnings.mdx new file mode 100644 index 00000000..a2cc89d0 --- /dev/null +++ b/sdk/js/stocks/earnings.mdx @@ -0,0 +1,162 @@ +--- +title: Earnings +sidebar_position: 4 +--- + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +Retrieve historical and upcoming earnings information for any supported stock symbol. + +## Making Requests + +Use the `earnings()` method on the `stocks` resource to fetch earnings data. + +| Output Format | Result Payload | Description | +|------------------------|---------------------------------------------|-------------------------------------------------| +| **internal** (default) | `StockEarnings[]` or `StockEarningsHuman[]` | Array of decoded earnings records. | +| **json** | Raw JSON object | The compressed, array-keyed response object. | +| **csv** | `Blob` | CSV payload. Use `.save(filename)` to write it. | + + +## earnings + +```typescript +// Positional form +earnings

( + symbol: string, + params?: P, +): MarketDataPromise + +// Object form +earnings

( + params: P & { symbol: string }, +): MarketDataPromise +``` + +Fetches earnings data for a single symbol. + +#### Parameters + +- `symbol` (string) + + The stock symbol to fetch earnings for. + +- [`outputFormat`](/sdk/js/settings#output-format) (optional): The format of the returned data. Alias: `format`. +- [`dateFormat`](/sdk/js/settings#date-format) (optional): Date format for response timestamps. Alias: `dateformat`. +- [`columns`](/sdk/js/settings#columns) (optional): Columns to include. +- [`addHeaders`](/sdk/js/settings#headers) (optional): Whether to include headers in CSV output. Alias: `headers`. +- [`useHumanReadable`](/sdk/js/settings#human-readable) (optional): Use human-readable field names. Alias: `human`. +- [`mode`](/sdk/js/settings#data-mode) (optional): The data mode to use. + +Additional endpoint-specific parameters (e.g. `from`, `to`, `date`) are passed through to the REST API as-is. See the [REST API Earnings documentation](/api/stocks/earnings) for the complete list. + +#### Returns + +- [`MarketDataPromise`](/sdk/js/client#MarketDataPromise) + +:::info +Earnings data is a premium endpoint on the Market Data API. Your plan must include earnings access for this method to return data. +::: + + + + +```typescript +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const earnings = await client.stocks.earnings("AAPL"); + for (const e of earnings) { + console.log( + `FY${e.fiscalYear} Q${e.fiscalQuarter}: ` + + `reported=${e.reportedEPS} estimated=${e.estimatedEPS}` + ); + } +} catch (error) { + console.error(error); +} +``` + + + + + +```typescript +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const earnings = await client.stocks.earnings("AAPL", { human: true }); + for (const e of earnings) { + console.log(`${e.Symbol} FY${e.Fiscal_Year} Q${e.Fiscal_Quarter}: ${e.Reported_EPS}`); + } +} catch (error) { + console.error(error); +} +``` + + + + + +## StockEarnings + +```typescript +interface StockEarnings { + s?: string; + symbol: string; + fiscalYear: number; + fiscalQuarter: number; + date: number; + reportDate: number; + reportTime: string | null; + currency: string | null; + reportedEPS: number | null; + estimatedEPS: number | null; + surpriseEPS: number | null; + surpriseEPSpct: number | null; + updated: number; +} +``` + +#### Properties + +- `symbol` (string): The stock symbol. +- `fiscalYear` (number): The fiscal year of the earnings report. +- `fiscalQuarter` (number): The fiscal quarter (1–4). +- `date` (number): Unix timestamp of the earnings event. +- `reportDate` (number): Unix timestamp when the earnings were reported. +- `reportTime` (string | null): Time of day the earnings were reported (e.g. `"bmo"`, `"amc"`). +- `currency` (string | null): Currency of the EPS figures. +- `reportedEPS` (number | null): The actual reported earnings per share. +- `estimatedEPS` (number | null): The analyst consensus estimate. +- `surpriseEPS` (number | null): The difference between reported and estimated EPS. +- `surpriseEPSpct` (number | null): The surprise as a fraction of the estimate. +- `updated` (number): Unix timestamp when the record was last updated. + + +## StockEarningsHuman + +```typescript +interface StockEarningsHuman { + s?: string; + Symbol: string; + Fiscal_Year: number; + Fiscal_Quarter: number; + Date: number; + Report_Date: number; + Report_Time: string | null; + Currency: string | null; + Reported_EPS: number | null; + Estimated_EPS: number | null; + Surprise_EPS: number | null; + Surprise_EPS_Percent: number | null; + Updated: number; +} +``` + +`StockEarningsHuman` is returned when `human: true` is set. Field names use `Title_Case`. diff --git a/sdk/js/stocks/index.mdx b/sdk/js/stocks/index.mdx new file mode 100644 index 00000000..dfd8ca84 --- /dev/null +++ b/sdk/js/stocks/index.mdx @@ -0,0 +1,14 @@ +--- +title: Stocks +slug: /js/stocks +sidebar_position: 10 +--- + +The JavaScript SDK from Market Data provides methods designed to streamline your use of the following Stocks endpoints. These intuitive methods provide a seamless interface, effectively masking the intricacies involved in managing HTTP requests and responses. + +## Stocks Endpoints + +import DocCardList from "@theme/DocCardList"; +import { useCurrentSidebarCategory } from "@docusaurus/theme-common"; + + diff --git a/sdk/js/stocks/news.mdx b/sdk/js/stocks/news.mdx new file mode 100644 index 00000000..7f2c9f22 --- /dev/null +++ b/sdk/js/stocks/news.mdx @@ -0,0 +1,138 @@ +--- +title: News +sidebar_position: 5 +--- + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +Retrieve news articles for any supported stock symbol. + +## Making Requests + +Use the `news()` method on the `stocks` resource to fetch news. + +| Output Format | Result Payload | Description | +|------------------------|-------------------------------------|-------------------------------------------------| +| **internal** (default) | `StockNews[]` or `StockNewsHuman[]` | Array of decoded news articles. | +| **json** | Raw JSON object | The compressed, array-keyed response object. | +| **csv** | `Blob` | CSV payload. Use `.save(filename)` to write it. | + + +## news + +```typescript +// Positional form +news

( + symbol: string, + params?: P, +): MarketDataPromise + +// Object form +news

( + params: P & { symbol: string }, +): MarketDataPromise +``` + +Fetches news articles for a single symbol. + +#### Parameters + +- `symbol` (string) + + The stock symbol to fetch news for. + +- `from` (string | Date, optional) + + Start of the date range for news articles. + +- `to` (string | Date, optional) + + End of the date range for news articles. + +- `date` (string | Date, optional) + + Fetch news for a specific date. + +- `countback` (number, optional) + + Number of articles to return, counting backwards from `to`. + +- [`outputFormat`](/sdk/js/settings#output-format) (optional): The format of the returned data. Alias: `format`. +- [`dateFormat`](/sdk/js/settings#date-format) (optional): Date format. Alias: `dateformat`. +- [`columns`](/sdk/js/settings#columns) (optional): Columns to include. +- [`addHeaders`](/sdk/js/settings#headers) (optional): Whether to include headers in CSV output. Alias: `headers`. +- [`useHumanReadable`](/sdk/js/settings#human-readable) (optional): Use human-readable field names. Alias: `human`. +- [`mode`](/sdk/js/settings#data-mode) (optional): The data mode to use. + +#### Returns + +- [`MarketDataPromise`](/sdk/js/client#MarketDataPromise) + + + + +```typescript +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const articles = await client.stocks.news("AAPL"); + for (const a of articles) { + console.log(`${a.source}: ${a.headline}`); + } +} catch (error) { + console.error(error); +} +``` + + + + + +```typescript +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const articles = await client.stocks.news("AAPL", { + from: "2024-01-01", + to: "2024-01-31", + }); + console.log(`Got ${articles.length} articles`); +} catch (error) { + console.error(error); +} +``` + + + + + +## StockNews + +```typescript +interface StockNews { + s?: string; + symbol: string; + headline: string; + content: string; + source: string; + publicationDate: number; +} +``` + +#### Properties + +- `symbol` (string): The stock symbol. +- `headline` (string): The news headline. +- `content` (string): The article content or summary. +- `source` (string): The news source publisher. +- `publicationDate` (number): Unix timestamp when the article was published. + + +## StockNewsHuman + +`StockNewsHuman` is returned when `human: true` is set. It uses the same field names as `StockNews` for most columns, but with `Symbol` capitalized. diff --git a/sdk/js/stocks/prices.mdx b/sdk/js/stocks/prices.mdx new file mode 100644 index 00000000..94a65042 --- /dev/null +++ b/sdk/js/stocks/prices.mdx @@ -0,0 +1,220 @@ +--- +title: Prices +sidebar_position: 1 +--- + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +Retrieve stock prices for any supported stock symbol. + +## Making Requests + +Use the `prices()` method on the `stocks` resource to fetch stock prices. The method returns a [`MarketDataPromise`](/sdk/js/client#MarketDataPromise) that resolves to decoded records by default, or rejects with a `MarketDataClientError` subclass on failure. The output format is controlled by the `outputFormat` parameter (or `MARKETDATA_OUTPUT_FORMAT` env var): + +| Output Format | Result Payload | Description | +|------------------------|---------------------------------------|-----------------------------------------------------------------------| +| **internal** (default) | `StockPrice[]` or `StockPriceHuman[]` | Array of decoded price records, one per symbol. | +| **json** | Raw JSON object | The compressed, array-keyed response object as returned by the API. | +| **csv** | `Blob` | CSV payload. Use `.save(filename)` on the result to write it to disk. | + + +## prices + +```typescript +// Positional form +prices

( + symbols: string | string[], + params?: P, +): MarketDataPromise + +// Object form +prices

( + params: P & { symbols: string | string[] }, +): MarketDataPromise +``` + +Fetches stock prices for one or more symbols. The return type is narrowed automatically: + +- If `human: true` (or `useHumanReadable: true`) → payload is `StockPriceHuman[]` +- If `outputFormat: "csv"` → payload is `Blob` +- Otherwise → payload is `StockPrice[]` + +#### Parameters + +- `symbols` (string | string[]) + + A single symbol string or an array of symbol strings for which to fetch prices. + +- [`outputFormat`](/sdk/js/settings#output-format) (optional) + + The format of the returned data. See [Settings](/sdk/js/settings#output-format) for details. Also accepts the alias `format`. + +- [`dateFormat`](/sdk/js/settings#date-format) (optional) + + The date format to use in the response. Also accepts the alias `dateformat`. See [Settings](/sdk/js/settings#date-format) for details. + +- [`columns`](/sdk/js/settings#columns) (optional) + + Specify which columns to include in the response. See [Settings](/sdk/js/settings#columns) for details. + +- [`addHeaders`](/sdk/js/settings#headers) (optional) + + Whether to include headers in CSV output. Also accepts the alias `headers`. See [Settings](/sdk/js/settings#headers) for details. + +- [`useHumanReadable`](/sdk/js/settings#human-readable) (optional) + + Whether to use human-readable field names. Also accepts the alias `human`. See [Settings](/sdk/js/settings#human-readable) for details. + +- [`mode`](/sdk/js/settings#data-mode) (optional) + + The data mode to use (`"live"`, `"cached"`, `"delayed"`). See [Settings](/sdk/js/settings#data-mode) for details. + +#### Returns + +- [`MarketDataPromise`](/sdk/js/client#MarketDataPromise) + + A Promise that resolves to the prices data in the requested format, or rejects with a `MarketDataClientError` subclass on failure. Handle with `await` + `try/catch`. + + + + +```typescript +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const prices = await client.stocks.prices("AAPL"); + console.log(prices[0].mid); +} catch (error) { + console.error(error); +} +``` + + + + + +```typescript +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const prices = await client.stocks.prices(["AAPL", "MSFT", "GOOGL"]); + for (const p of prices) { + console.log(`${p.symbol}: ${p.mid}`); + } +} catch (error) { + console.error(error); +} +``` + + + + + +```typescript +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const prices = await client.stocks.prices("AAPL", { human: true }); + for (const p of prices) { + console.log(`${p.Symbol}: mid=${p.Mid}, change=${p.Change_Percent}`); + } +} catch (error) { + console.error(error); +} +``` + + + + + +```typescript +import { MarketDataClient, OutputFormat } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const json = await client.stocks.prices(["AAPL", "MSFT"], { + outputFormat: OutputFormat.JSON, + }); + console.log(json); +} catch (error) { + console.error(error); +} +``` + + + + + +```typescript +import { MarketDataClient, OutputFormat } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +const csv = await client.stocks.prices(["AAPL", "MSFT"], { + outputFormat: OutputFormat.CSV, +}); + +// Save the Blob to disk (Node.js) +const filename = await csv.save("prices.csv"); +console.log(`CSV saved to: ${filename}`); +``` + + + + + +## StockPrice + +```typescript +interface StockPrice { + s?: string; + symbol: string; + mid: number; + change: number; + changepct: number; + updated: number; +} +``` + +`StockPrice` represents a single stock price, with short machine-readable field names. + +#### Properties + +- `s` (string, optional): Status indicator (`"ok"` for successful responses). +- `symbol` (string): The stock symbol. +- `mid` (number): The mid price calculated between the ask and bid. +- `change` (number): The price change. +- `changepct` (number): The fractional price change (e.g. `0.0124` for +1.24%). +- `updated` (number): Unix timestamp when the price was last updated. + + +## StockPriceHuman + +```typescript +interface StockPriceHuman { + s?: string; + Symbol: string; + Mid: number; + Change_Price: number; + Change_Percent: number; + Date: number; +} +``` + +`StockPriceHuman` represents a stock price in human-readable format with capitalized, snake-case field names. Returned when `human: true` (or `useHumanReadable: true`) is set. + +#### Properties + +- `Symbol` (string): The stock symbol. +- `Mid` (number): The mid price. +- `Change_Price` (number): The price change. +- `Change_Percent` (number): The percent change. +- `Date` (number): Unix timestamp of the update. diff --git a/sdk/js/stocks/quotes.mdx b/sdk/js/stocks/quotes.mdx new file mode 100644 index 00000000..7e10a159 --- /dev/null +++ b/sdk/js/stocks/quotes.mdx @@ -0,0 +1,194 @@ +--- +title: Quotes +sidebar_position: 2 +--- + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +Retrieve stock quotes (bid, ask, mid, last, volume, etc.) for one or more supported stock symbols. + +## Making Requests + +Use the `quotes()` method on the `stocks` resource to fetch stock quotes. The method returns a [`MarketDataPromise`](/sdk/js/client#MarketDataPromise) that resolves to decoded records by default, or rejects with a `MarketDataClientError` subclass on failure. + +| Output Format | Result Payload | Description | +|------------------------|---------------------------------------|-------------------------------------------------| +| **internal** (default) | `StockQuote[]` or `StockQuoteHuman[]` | Array of decoded quote records, one per symbol. | +| **json** | Raw JSON object | The compressed, array-keyed response object. | +| **csv** | `Blob` | CSV payload. Use `.save(filename)` to write it. | + + +## quotes + +```typescript +// Positional form +quotes

( + symbols: string | string[], + params?: P, +): MarketDataPromise + +// Object form +quotes

( + params: P & { symbols: string | string[] }, +): MarketDataPromise +``` + +Fetches stock quotes for one or more symbols. + +#### Parameters + +- `symbols` (string | string[]) + + A single symbol string or an array of symbol strings for which to fetch quotes. + +- `52week` (boolean, optional) + + Include the 52-week high and low in the response. + +- `extended` (boolean, optional) + + Include extended-hours quote data. + +- [`outputFormat`](/sdk/js/settings#output-format) (optional): The format of the returned data. Alias: `format`. +- [`dateFormat`](/sdk/js/settings#date-format) (optional): The date format to use. Alias: `dateformat`. +- [`columns`](/sdk/js/settings#columns) (optional): Columns to include. +- [`addHeaders`](/sdk/js/settings#headers) (optional): Whether to include headers in CSV output. Alias: `headers`. +- [`useHumanReadable`](/sdk/js/settings#human-readable) (optional): Use human-readable field names. Alias: `human`. +- [`mode`](/sdk/js/settings#data-mode) (optional): The data mode to use. + +#### Returns + +- [`MarketDataPromise`](/sdk/js/client#MarketDataPromise) + + + + +```typescript +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const quotes = await client.stocks.quotes("AAPL"); + console.log(quotes[0]); +} catch (error) { + console.error(error); +} +``` + + + + + +```typescript +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const quotes = await client.stocks.quotes(["AAPL", "MSFT", "GOOGL"]); + for (const q of quotes) { + console.log(`${q.symbol}: bid=${q.bid}, ask=${q.ask}, last=${q.last}`); + } +} catch (error) { + console.error(error); +} +``` + + + + + +```typescript +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const quotes = await client.stocks.quotes("AAPL", { "52week": true }); + console.log(quotes[0]); +} catch (error) { + console.error(error); +} +``` + + + + + +```typescript +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const quotes = await client.stocks.quotes("AAPL", { human: true }); + const q = quotes[0]; + console.log(`${q.Symbol}: Bid=${q.Bid} / Ask=${q.Ask}, Volume=${q.Volume}`); +} catch (error) { + console.error(error); +} +``` + + + + + +## StockQuote + +```typescript +interface StockQuote { + s?: string; + symbol: string; + ask: number; + askSize: number; + bid: number; + bidSize: number; + mid: number; + last: number; + change: number; + changepct: number; + volume: number; + updated: number; +} +``` + +`StockQuote` represents a single stock quote with short, machine-readable field names. + +#### Properties + +- `s` (string, optional): Status indicator (`"ok"` for successful responses). +- `symbol` (string): The stock symbol. +- `ask` (number): The ask price. +- `askSize` (number): The ask size. +- `bid` (number): The bid price. +- `bidSize` (number): The bid size. +- `mid` (number): The mid price. +- `last` (number): The last traded price. +- `change` (number): The price change. +- `changepct` (number): The fractional price change. +- `volume` (number): The trading volume. +- `updated` (number): Unix timestamp when the quote was last updated. + + +## StockQuoteHuman + +```typescript +interface StockQuoteHuman { + s?: string; + Symbol: string; + Ask: number; + Ask_Size: number; + Bid: number; + Bid_Size: number; + Mid: number; + Last: number; + Change_Price: number; + Change_Percent: number; + Volume: number; + Date: number; +} +``` + +`StockQuoteHuman` is returned when `human: true` is set. Field names are in `Title_Case` rather than `camelCase`. diff --git a/sdk/js/utilities/headers.mdx b/sdk/js/utilities/headers.mdx new file mode 100644 index 00000000..4ba23aa8 --- /dev/null +++ b/sdk/js/utilities/headers.mdx @@ -0,0 +1,66 @@ +--- +title: Headers +sidebar_position: 2 +--- + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +Echo back the request headers your client actually sent to Market Data. Useful for debugging proxies, custom header plumbing, or verifying the User-Agent the SDK is presenting. + +The `Authorization` header is redacted server-side before the response is returned. + +## Making Requests + +Use the `headers()` method on the `utilities` resource. + +| Output Format | Result Payload | Description | +|------------------------|-------------------|------------------------------------------------| +| **internal** (default) | `HeadersResponse` | Map of header name to value as the API saw it. | +| **json** | Raw JSON object | The raw response as returned by the API. | + + +## headers + +```typescript +headers(): MarketDataPromise +``` + +Fetches the request-header echo. Takes no parameters. + +#### Returns + +- [`MarketDataPromise`](/sdk/js/client#MarketDataPromise) + + + + +```typescript +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const h = await client.utilities.headers(); + console.log(h["user-agent"]); // "marketdata-sdk-js/1.0.0" + console.log(h["accept-encoding"]); +} catch (error) { + console.error(error); +} +``` + + + + + +## HeadersResponse + +```typescript +type HeadersResponse = Record; +``` + +A bag of header name → value pairs. Header names are lowercased; values may be strings or arrays of strings depending on whether the header was sent multiple times. + +:::tip +Use this to confirm what gets through when you wire the SDK behind a corporate proxy or add custom headers via your runtime's `fetch` interception. +::: diff --git a/sdk/js/utilities/index.mdx b/sdk/js/utilities/index.mdx new file mode 100644 index 00000000..7b1d15c6 --- /dev/null +++ b/sdk/js/utilities/index.mdx @@ -0,0 +1,18 @@ +--- +title: Utilities +slug: /js/utilities +sidebar_position: 50 +--- + +The JavaScript SDK from Market Data provides methods designed to streamline your use of the following Utilities endpoints. These methods provide a seamless interface for accessing diagnostic data: service status and request-header inspection. + +Both utilities skip the per-token rate-limit gate and bypass the `/v1/` URL prefix, so they remain reachable even when your account is throttled. + +Rate-limit data is exposed on the client itself — see [`client.rateLimits`](/sdk/js/client#RateLimits), which the SDK populates automatically at startup from the same `/user/` response headers. + +## Utilities Endpoints + +import DocCardList from "@theme/DocCardList"; +import { useCurrentSidebarCategory } from "@docusaurus/theme-common"; + + diff --git a/sdk/js/utilities/status.mdx b/sdk/js/utilities/status.mdx new file mode 100644 index 00000000..6ea0c008 --- /dev/null +++ b/sdk/js/utilities/status.mdx @@ -0,0 +1,102 @@ +--- +title: Status +sidebar_position: 1 +--- + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +Retrieve service-level health information for every Market Data endpoint. No authentication token is required — `status()` is reachable in demo mode and is the SDK's own probe before retrying a 5xx response. + +## Making Requests + +Use the `status()` method on the `utilities` resource to fetch the per-service availability snapshot. + +| Output Format | Result Payload | Description | +|------------------------|---------------------|-----------------------------------------------| +| **internal** (default) | `ApiStatusResponse` | Decoded status snapshot with parallel arrays. | +| **json** | Raw JSON object | The raw response as returned by the API. | + + +## status + +```typescript +status(): MarketDataPromise +``` + +Fetches the current service-status payload. Takes no parameters. + +#### Returns + +- [`MarketDataPromise`](/sdk/js/client#MarketDataPromise) + + + + +```typescript +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const s = await client.utilities.status(); + for (let i = 0; i < s.service.length; i++) { + console.log(`${s.service[i]}: ${s.status[i]} (online=${s.online[i]})`); + } +} catch (error) { + console.error(error); +} +``` + + + + + +```typescript +import { MarketDataClient } from "@marketdata/sdk"; + +const client = new MarketDataClient(); + +try { + const s = await client.utilities.status(); + for (let i = 0; i < s.service.length; i++) { + const u30 = s.uptimePct30d?.[i] ?? null; + const u90 = s.uptimePct90d?.[i] ?? null; + console.log(`${s.service[i]} — 30d=${u30}% 90d=${u90}%`); + } +} catch (error) { + console.error(error); +} +``` + + + + + +## ApiStatusResponse + +```typescript +interface ApiStatusResponse { + service: string[]; + status: string[]; + online: boolean[]; + uptimePct30d?: number[]; + uptimePct90d?: number[]; + updated: number[]; +} +``` + +#### Properties + +- `service` (string[]): Service names (e.g. `"stocks/candles"`, `"options/chain"`). +- `status` (string[]): Human-readable status for each service (e.g. `"OPERATIONAL"`, `"OFFLINE"`). +- `online` (boolean[]): Boolean availability flag for each service. +- `uptimePct30d` (number[], optional): Trailing 30-day uptime percentage per service. +- `uptimePct90d` (number[], optional): Trailing 90-day uptime percentage per service. +- `updated` (number[]): Unix timestamps of the last status check per service. + +All arrays are parallel — index `i` across `service`, `status`, `online`, and `updated` refers to the same service. + +:::info +The SDK's internal retry loop calls `utilities.status()` before retrying a 5xx response. If the service that originally failed is marked `online: false`, the retry short-circuits so calls fail fast instead of grinding through the full backoff. +::: diff --git a/sdk/php/index.mdx b/sdk/php/index.mdx index 6404e561..97bec08a 100644 --- a/sdk/php/index.mdx +++ b/sdk/php/index.mdx @@ -2,8 +2,6 @@ title: PHP SDK sidebar_position: 4 slug: /php -sidebar_custom_props: - badge: n --- Welcome to the Market Data PHP SDK documentation. This SDK allows you to easily integrate Market Data services into your PHP applications. diff --git a/sdk/py/index.mdx b/sdk/py/index.mdx index 14158e9f..e48cf189 100644 --- a/sdk/py/index.mdx +++ b/sdk/py/index.mdx @@ -2,8 +2,6 @@ title: Python SDK sidebar_position: 3 slug: /py -sidebar_custom_props: - badge: n --- Welcome to the Market Data Python SDK documentation. This SDK allows you to easily integrate Market Data services into your Python applications. diff --git a/sdk/sdk-requirements.md b/sdk/sdk-requirements.md index 5cdc172a..91ade6b7 100644 --- a/sdk/sdk-requirements.md +++ b/sdk/sdk-requirements.md @@ -431,10 +431,15 @@ Retry requests only when: ### 9.3 Backoff Strategy -- Use exponential backoff: `min(initial * 2^attempt, max_backoff)` -- Default initial: 1 second -- Default max backoff: 30 seconds -- Default max attempts: 3 +- 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 diff --git a/src/css/custom.css b/src/css/custom.css index 4a02201c..c3b5185b 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -152,6 +152,10 @@ align-self: center; /* Center the image in the flex container */ } +.sdk-item p img + img { + margin-left: 12px; +} + [data-theme="dark"] .sdk-item { background-color: #1e1e1e; /* Dark background for the items */ color: #ffffff; /* Light text color for readability */ diff --git a/static/img/javascript-logo.svg b/static/img/javascript-logo.svg new file mode 100644 index 00000000..700f5e34 --- /dev/null +++ b/static/img/javascript-logo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/static/img/typescript-logo.svg b/static/img/typescript-logo.svg new file mode 100644 index 00000000..dedef548 --- /dev/null +++ b/static/img/typescript-logo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/worker/handler.js b/worker/handler.js index 879f3c7d..51fa714d 100644 --- a/worker/handler.js +++ b/worker/handler.js @@ -14,6 +14,8 @@ * are passed through unchanged. */ +const { cleanMdx } = require('../lib/mdx-to-md'); + /** * Hostname-based routing: maps each hostname to its Cloudflare Pages target. */ @@ -22,36 +24,6 @@ const TARGETS = { 'www-staging.marketdata.app': 'www-staging-marketdata-app.pages.dev', }; -/** - * Converts raw MDX/markdown source into clean markdown by stripping - * frontmatter, imports, and converting Docusaurus components to headings. - * - * @param {string} text - Raw file contents - * @returns {string} - */ -function cleanMarkdown(text) { - // Extract title from frontmatter, then strip it - const fmMatch = text.match(/^---\n([\s\S]*?)\n---/); - let title = ''; - if (fmMatch) { - const titleMatch = fmMatch[1].match(/^title:\s*(.+)$/m); - if (titleMatch) title = titleMatch[1].trim().replace(/^["']|["']$/g, ''); - } - text = text.replace(/^---\n[\s\S]*?\n---\n*/, ''); - // Add title as H1 if present - if (title) text = `# ${title}\n\n${text}`; - // Strip import statements - text = text.replace(/^import\s+.*;\s*\n/gm, ''); - // Convert to ### headings, strip wrappers - text = text.replace(/\n?/g, ''); - text = text.replace(/<\/Tabs>\n?/g, ''); - text = text.replace(/"]*(?:"[^"]*")?)*?label="([^"]*)"(?:[^>"]*(?:"[^"]*")?)*?>\n?/g, '### $1\n\n'); - text = text.replace(/<\/TabItem>\n?/g, ''); - // Clean up excess blank lines - text = text.replace(/\n{3,}/g, '\n\n').trim() + '\n'; - return text; -} - /** * Routes incoming requests to the appropriate Cloudflare Pages deployment * based on the hostname. Non-matching requests pass through to the @@ -121,7 +93,9 @@ async function handleRequest(request) { for (const candidate of candidates) { const res = await fetch(candidate); if (res.ok) { - const text = cleanMarkdown(await res.text()); + const text = cleanMdx(await res.text(), { + baseUrl: 'https://www.marketdata.app/docs', + }); const canonicalUrl = `https://www.marketdata.app/docs/${stem}/`; return new Response(text, { headers: { @@ -166,4 +140,4 @@ async function handleRequest(request) { return response; } -module.exports = { handleRequest, cleanMarkdown, TARGETS }; +module.exports = { handleRequest, TARGETS }; diff --git a/worker/handler.test.js b/worker/handler.test.js index 48182568..c91f1e53 100644 --- a/worker/handler.test.js +++ b/worker/handler.test.js @@ -4,7 +4,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; const mockFetch = vi.fn(); vi.stubGlobal('fetch', mockFetch); -const { handleRequest, cleanMarkdown } = require('./handler'); +const { handleRequest } = require('./handler'); function makeRequest(url, options = {}) { return new Request(url, options); @@ -14,41 +14,12 @@ beforeEach(() => { vi.clearAllMocks(); }); -// --- cleanMarkdown --- - -describe('cleanMarkdown', () => { - it('extracts title from frontmatter as H1', () => { - const input = '---\ntitle: Test\nsidebar_position: 1\n---\n\n# Hello\n'; - expect(cleanMarkdown(input)).toBe('# Test\n\n# Hello\n'); - }); - - it('strips frontmatter without title', () => { - const input = '---\nsidebar_position: 1\n---\n\n# Hello\n'; - expect(cleanMarkdown(input)).toBe('# Hello\n'); - }); - - it('strips import statements', () => { - const input = 'import Tabs from "@theme/Tabs";\nimport TabItem from "@theme/TabItem";\n\n# Hello\n'; - expect(cleanMarkdown(input)).toBe('# Hello\n'); - }); - - it('converts TabItem to headings and strips Tabs wrappers', () => { - const input = '\n\n\ncode here\n\n\n\n'; - const result = cleanMarkdown(input); - expect(result).toContain('### JavaScript'); - expect(result).not.toContain(''); - expect(result).not.toContain(''); - }); - - it('collapses excess blank lines', () => { - const input = '# A\n\n\n\n\n# B\n'; - expect(cleanMarkdown(input)).toBe('# A\n\n# B\n'); - }); -}); - // --- handleRequest --- +// Note: cleanMarkdown/cleanMdx unit tests live in /lib/__tests__/mdx-to-md.test.js +// (run with `node --test`). The tests below cover the worker's request-handling +// behavior — they exercise the conversion in passing via the markdown-serving block. + describe('handleRequest', () => { // --- Non-matching hostname ---