跳到正文
DEV Community· Prosper Otemuyiwa·· 3 小時前AI 評分51

Valyu Search API 如何取得歷史股票價格

How to Get Historical Stock Prices via Search API

AI 導讀

教程示範如何透過 Valyu Search API,按公司名稱和指定期間取得歷史股價,並以 TypeScript 比較 Apple 與 Microsoft 在 2024 年 1 月 2 至 10 日的收市價。

正文

Day 21 of 30 Days of Search.

TL;DR

To get historical stock prices with Valyu Search API, put a company name and the requested period in the search query. With stock-source access enabled, the tested query-only request returned native arrays of daily OHLCV records from valyu/valyu-stocks, plus the resolved ticker, exchange, currency and timezone.

  • This tutorial: a complete TypeScript script that retrieves Apple and Microsoft history for 2-10 January 2024, validates the returned records and compares their daily closing prices.
  • Required: Node.js 24.2+, valyu-js@2.10.5, and a Valyu API key with stock-dataset access. A valid key alone does not establish that access.
  • Verified result: the live SDK run returned seven daily observations for each company, on the same seven dates. No out-of-window rows or duplicate bars were removed in that run.
  • Important: the calculation uses returned closes only; it does not add dividend cashflows or model reinvestment. This example does not establish the feed's split/dividend-adjustment basis or provide a total-return or point-in-time backtesting service.

A developer is building a company-comparison view. A user asks: "How did Apple and Microsoft trade over the same period?" The application needs actual price observations, the right instruments and matching dates. A news story saying a stock fell that week is not a replacement for those records.

We'll retrieve the history, inspect the response, then build a comparison that keeps the currency, trading dates and source context visible. This extends the historical stock prices API guide with a complete TypeScript example and a fresh execution check, following the record-validation approach in the clinical-trials Search API tutorial.

What will the historical-price search return?

OHLCV means open, high, low, close and volume. The tested stock results contained arrays of those fields, with a datetime for each daily observation and instrument metadata alongside the array.

The useful application output is a sorted series that keeps the returned ticker, company name, currency, exchange, timezone, interval and source URL. It also records when the application received the response. That gives the developer a reviewable input for a table or chart, rather than asking a language model to invent a price series from prose.

Valyu Search is the retrieval interface. The application still validates the fields, aligns the observations and does the arithmetic.

A company-and-period query goes through Valyu Search to stock-source OHLCV arrays and metadata, then application checks produce a sorted and aligned price series

The company and trading period go in the query. Stock access is still required, and the returned source and record shape need checking.

Step 1: Set up TypeScript and stock-dataset access

Use Node.js 24.2 or later because the script uses import.meta.main. The example was checked with Node.js 24.12.0, TypeScript 5.9.3 and valyu-js@2.10.5.

mkdir historical-stock-prices
cd historical-stock-prices
npm init -y
npm pkg set type=module
npm install valyu-js@2.10.5

Set VALYU_API_KEY in your server environment. No separate market-data vendor key is needed for the Valyu calls. The script does not load a .env file automatically; if you use one locally, keep it out of Git and pass Node's --env-file=.env option explicitly.

Check the data-source catalogue and your account's access to valyu/valyu-stocks before integrating. The live catalogue lists the Stocks dataset as Search-enabled. Dataset access and basic API authentication are separate checks; see source access and coverage.

There was a useful failure case in this verification. Query-only requests with a key that lacked stock-source access returned successful web-search responses containing articles, not bars. Explicitly targeting the stock dataset with that key returned HTTP 403. Using an authorised stock-enabled key then returned the expected records.

Do not turn a successful HTTP response, or an empty list after filtering, into a claim that historical prices were retrieved or do not exist.

Step 2: Put the company and historical period in the query

Start with the same bounded question used in the reference guide:

Apple stock prices from January 2, 2024 to January 10, 2024

For the tested REST request, query was the only request-body field:

curl --fail-with-body https://api.valyu.ai/v1/search \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $VALYU_API_KEY" \
  -d '{
    "query": "Apple stock prices from January 2, 2024 to January 10, 2024"
  }'

In TypeScript, the corresponding retrieval step is:

import { Valyu } from "valyu-js";

const client = new Valyu(); // Reads VALYU_API_KEY.
const response = await client.search(
  "Apple stock prices from January 2, 2024 to January 10, 2024",
);

The SDK supplies its defaults; you supply one query string. Neither example requires a separate ticker lookup, exchange parameter or trade-date field for this tested request. Inspect the resolved instrument instead of assuming a company name always selects the listing you intended. For multiple listings or share classes, make the intended instrument clearer in the question.

Do not substitute the Search API's optional start_date and end_date parameters for the historical period in this example. Those are publication-date filters, not the trade-date controls demonstrated here.

Step 3: Parse the arrays and check the exact fields

The live REST and SDK results contained native arrays in content. The complete script also accepts a JSON-encoded array, but refuses prose, a current-price scalar or a malformed object as historical bars.

Returned data What the application checks or retains
source Must equal valyu/valyu-stocks
metadata.ticker, name Keep the resolved instrument visible
currency, exchange, exchange_timezone Retain the market context; reject mixed metadata
interval Must equal 1day for this daily example
datetime Validate the supported daily format and derive its trading-date label
open, high, low, close Require finite positive numbers and a consistent OHLC range
volume Require a finite nonnegative number; zero remains zero
Repeated dates Deduplicate identical values; reject conflicting bars
Requested window Exclude out-of-window rows and count them

Save this complete script as historical-prices.ts:

import { Valyu } from "valyu-js";

export const SOURCE = "valyu/valyu-stocks";
export const START = "2024-01-02", END = "2024-01-10";
export const QUERIES = [
  "Apple stock prices from January 2, 2024 to January 10, 2024",
  "Microsoft stock prices from January 2, 2024 to January 10, 2024",
];
type Data = Record<string, unknown>;
type Instrument = Record<"ticker" | "name" | "currency" | "exchange" |
  "exchange_timezone" | "interval", string>;
export type Bar = { trading_date: string; datetime: string; interval_end: string | null;
  open: number; high: number; low: number; close: number; volume: number };
export type Series = { instrument: Instrument; requested_window: { start: string; end: string };
  provider_windows: { start: string | null; end: string | null; semantics: string | null }[];
  source_urls: string[]; retrieved_at: string; adjustment_basis: "not_verified";
  filtered_out_of_window: number; duplicate_rows: number; bars: Bar[] };
type Client = { search(query: string): Promise<unknown> };

function object(value: unknown): Data | null {
  return value !== null && typeof value === "object" && !Array.isArray(value)
    ? value as Data : null;
}
function text(value: unknown): string | null {
  return typeof value === "string" && value.trim() ? value : null;
}
function date(value: string): boolean {
  if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
  try { return new Date(`${value}T00:00:00Z`).toISOString().slice(0, 10) === value; }
  catch { return false; }
}
function numeric(value: unknown, zeroAllowed = false): number {
  if (typeof value !== "number" || !Number.isFinite(value) ||
      (zeroAllowed ? value < 0 : value <= 0)) throw new Error("Invalid numeric stock field.");
  return value;
}
function records(content: unknown): unknown[] {
  try {
    const parsed: unknown = typeof content === "string" ? JSON.parse(content) : content;
    if (Array.isArray(parsed)) return parsed;
  } catch {}
  throw new Error("Stock content is not a historical-record array.");
}

export function normalize(results: unknown, start: string, end: string,
  retrievedAt = new Date().toISOString()): Series {
  if (!date(start) || !date(end) || start > end) throw new Error("Invalid requested window.");
  if (!Array.isArray(results)) throw new Error("Invalid search results.");
  let instrument: Instrument | undefined, filtered = 0, duplicates = 0;
  const bars = new Map<string, Bar>(), urls = new Set<string>();
  const windows: Series["provider_windows"] = [];
  for (const value of results) {
    const result = object(value);
    if (!result || result.source !== SOURCE) continue;
    const meta = object(result.metadata);
    if (!meta) throw new Error("Missing instrument metadata.");
    const keys = ["ticker", "name", "currency", "exchange", "exchange_timezone", "interval"] as const;
    const entry = Object.fromEntries(keys.map(key => {
      const value = text(meta[key]);
      if (!value) throw new Error(`Missing metadata field: ${key}`);
      return [key, value];
    })) as Instrument;
    if (entry.interval !== "1day") throw new Error("This example requires daily bars.");
    if (instrument && JSON.stringify(instrument) !== JSON.stringify(entry))
      throw new Error("Mixed instrument metadata; refine the query.");
    instrument = entry;
    let url: URL;
    try { url = new URL(String(result.url)); } catch { throw new Error("Invalid source URL."); }
    if (url.protocol !== "https:" || url.username || url.password ||
        [...url.searchParams.keys()].some(key => /^(api[-_]?key|key|token|access[-_]?token|secret|signature|authorization)$/i.test(key)))
      throw new Error("Unsafe source URL.");
    urls.add(url.href);
    windows.push({ start: text(meta.start), end: text(meta.end),
      semantics: text(meta.interval_end_semantics) });
    for (const value of records(result.content)) {
      const row = object(value), timestamp = row && text(row.datetime);
      const match = timestamp?.match(/^(\d{4}-\d{2}-\d{2})(?: 00:00:00)?$/);
      if (!row || !timestamp || !match || !date(match[1])) throw new Error("Invalid daily timestamp.");
      const day = match[1];
      if (day < start || day > end) { filtered++; continue; }
      const bar: Bar = { trading_date: day, datetime: timestamp,
        interval_end: text(row.interval_end), open: numeric(row.open), high: numeric(row.high),
        low: numeric(row.low), close: numeric(row.close), volume: numeric(row.volume, true) };
      if (bar.low > bar.high || bar.open < bar.low || bar.open > bar.high ||
          bar.close < bar.low || bar.close > bar.high) throw new Error("Inconsistent OHLC range.");
      const previous = bars.get(day);
      if (previous) {
        if (!["open", "high", "low", "close", "volume"].every(key =>
          previous[key as keyof Bar] === bar[key as keyof Bar])) throw new Error("Conflicting duplicate bars.");
        duplicates++; continue;
      }
      bars.set(day, bar);
    }
  }
  if (!instrument || !bars.size) throw new Error("No usable stock bars; check dataset access and query.");
  return { instrument, requested_window: { start, end }, provider_windows: windows,
    source_urls: [...urls], retrieved_at: retrievedAt, adjustment_basis: "not_verified",
    filtered_out_of_window: filtered, duplicate_rows: duplicates,
    bars: [...bars.values()].sort((a, b) => a.trading_date.localeCompare(b.trading_date)) };
}

export async function searchHistory(client: Client, query: string, start = START, end = END) {
  let response: unknown;
  try { response = await client.search(query); }
  catch { throw new Error("Stock search failed; check connectivity, key and dataset access."); }
  const retrievedAt = new Date().toISOString(), result = object(response);
  if (!result || result.success !== true) throw new Error("Stock search failed; check key and dataset access.");
  return normalize(result.results, start, end, retrievedAt);
}

export function compare(a: Series, b: Series) {
  if (a.instrument.currency !== b.instrument.currency ||
      a.instrument.exchange_timezone !== b.instrument.exchange_timezone ||
      a.instrument.interval !== "1day" || b.instrument.interval !== "1day")
    throw new Error("Check currency, timezone and interval before comparing.");
  const dates = (series: Series) => series.bars.map(bar => bar.trading_date);
  if (a.bars.length < 2 || JSON.stringify(dates(a)) !== JSON.stringify(dates(b)) ||
      !a.bars.every((bar, i) => i === 0 || a.bars[i - 1].trading_date < bar.trading_date))
    throw new Error("Need matching, ascending trading sessions; align explicitly.");
  if (![a, b].every(series => series.bars.every(bar =>
    Number.isFinite(bar.close) && bar.close > 0))) throw new Error("Invalid comparison close.");
  return [a, b].map(series => {
    const first = series.bars[0], last = series.bars.at(-1)!;
    return { ticker: series.instrument.ticker, start: first.trading_date, end: last.trading_date,
      first_close: first.close, last_close: last.close,
      close_price_change_pct: 100 * (last.close / first.close - 1),
      rebased: series.bars.map(bar => ({ trading_date: bar.trading_date,
        close_index: 100 * bar.close / first.close })) };
  });
}

if (import.meta.main) {
  const key = process.env.VALYU_API_KEY;
  if (!key) { console.error("Set VALYU_API_KEY in the server environment."); process.exitCode = 1; }
  else {
    try {
      const client = new Valyu(key), series: Series[] = [];
      for (const query of QUERIES) series.push(await searchHistory(client, query));
      console.log(JSON.stringify({ series, comparison: compare(series[0], series[1]) }, null, 2));
    } catch { console.error("Could not build the price comparison. Check source access and returned records."); process.exitCode = 1; }
  }
}

Run it with the environment variable set:

node historical-prices.ts > prices.json

The main script makes two Search requests, one per company. Each consumes account credits. Importing the module does not start a search. Keep the key on the server, not in a browser bundle or committed example.

The code's dates and queries describe the same period. If you change the period, update both START/END and QUERIES. The application-side window checks do not change the request sent to the API.

Step 4: Inspect the live Apple and Microsoft results

The 21 October SDK execution resolved Apple to AAPL / Apple Inc. and Microsoft to MSFT / MICROSOFT CORP. Both reported NASDAQ, USD, America/New_York and a 1day interval.

Both returned the same dates: 2, 3, 4, 5, 8, 9 and 10 January 2024. There were no observations for the intervening weekend. This establishes what this bounded check returned, not that any arbitrary range is complete.

Here is an excerpt from the application's Apple series, shortened to one bar. This is not the full Search response:

{
  "instrument": {
    "ticker": "AAPL",
    "name": "Apple Inc.",
    "currency": "USD",
    "exchange": "NASDAQ",
    "exchange_timezone": "America/New_York",
    "interval": "1day"
  },
  "requested_window": {
    "start": "2024-01-02",
    "end": "2024-01-10"
  },
  "retrieved_at": "2026-10-21T19:41:00.137Z",
  "adjustment_basis": "not_verified",
  "bars": [
    {
      "trading_date": "2024-01-02",
      "datetime": "2024-01-02 00:00:00",
      "interval_end": "2024-01-02 23:59:59.999999",
      "open": 187.14999,
      "high": 188.44,
      "low": 183.89,
      "close": 185.64,
      "volume": 82488700
    }
  ]
}

trading_date, retrieved_at, the diagnostics and adjustment_basis are application-owned fields. The script preserves the original datetime and the available interval_end separately.

The provider window in this run ended at 2024-01-10 23:59:59, with inclusive_local semantics. The bar's midnight timestamp is a daily bucket label, not a claim that the closing trade occurred at midnight. Keep the exchange timezone with it; do not shift the trading date by blindly converting it to a UTC instant.

The supplied source_urls point to the returned dataset reference. Retaining that reference is provenance, not independent exchange verification of every observation. The displayed endpoint closes also matched the rounded values in the supplied reference article.

Step 5: Align the sessions before comparing the stocks

A raw share-price chart can be misleading when the stocks have different starting prices. The compare function rebases each close-price series so its first returned close equals 100.

It first requires matching, ascending trading dates, daily intervals, the same currency and the same exchange timezone. If a date is missing from one series, it stops rather than silently comparing different periods.

The calculations are:

  • First-session index: 100 * close / firstClose
  • Endpoint close-price change: 100 * (lastClose / firstClose - 1)
Company 2 January close, USD 10 January close, USD Close-price change
Apple (AAPL) 185.64 186.19 +0.30%
Microsoft (MSFT) 370.87 382.77 +3.21%

Displayed prices and changes are rounded for reading. The code uses the original returned numeric values for arithmetic, including Microsoft's returned last close of 382.76999.

Observed daily close-price paths for Apple and Microsoft from 2 to 10 January 2024, rebased to 100 on the first session; endpoint changes are +0.30% and +3.21%

Derived from the live SDK output. The horizontal axis lists seven trading sessions, not every calendar day. These are returned-close comparisons with no dividend cashflows or reinvestment added; the feed's adjustment basis was not established by this example.

The outcome for the original job is a reproducible comparison table and chart input, with the dates and market context attached. A human can review the resolved instruments and data basis before using it in a financial dashboard.

Step 6: Adapt the question without losing the checks

Treat these as adaptation patterns, not additional tested requests:

Requirement What to change What still needs checking
Another company Replace the company in the query Returned ticker, listing, name and currency
Another period Change query dates and application bounds together Actual coverage, calendar closures and missing sessions
A different listing Describe the intended exchange or share class in the query Returned exchange, currency and resolved instrument
A different interval Request the intended interval and adapt the parser Returned interval, timestamps, timezone and alignment; the current script accepts daily bars only
An agent tool Expose a query-taking retrieval function Keep access checks, structured record validation and calculations in application code

For an agent, the useful interface can remain one query argument. Describe the tool as retrieving historical stock observations and ask the agent to include the company and period. Your code should check the result and calculate the comparison, rather than asking the model to manufacture OHLCV JSON or perform the arithmetic from a news article.

The script is a retrieval-and-validation example, not an autonomous agent loop. Its output can be used as an agent tool result. For a wider investigation involving filings, news and a cited company-research report, DeepResearch is a separate option; it is not required for the two price-history calls here.

Common historical stock-data mistakes

  • Accepting a web page as a price series. Check the dataset source, array shape and numeric fields. Successful Search responses can still be irrelevant to a price-chart task.
  • Treating the number of Search results as the number of daily bars. The tested stock result carried seven observations inside one content array. Inspect nested records and actual dates.
  • Conflating retrieval time with trading time. retrieved_at is when the application received the response. It does not make January 2024 prices current quotes.
  • Assuming all calendar days have bars. Weekends, holidays, halts or incomplete coverage need different explanations. For a production completeness check, use a suitable exchange calendar and investigate missing sessions. The example does not implement that calendar.
  • Calling close-price change a total return. Dividends, splits and the provider's adjustment policy matter. The returned metadata did not establish the adjustment basis, so the application says not_verified rather than inventing an adjusted flag.
  • Treating historical prices retrieved today as a point-in-time backtest feed. This tutorial does not establish what data or revisions were available to a decision-maker at the historical date. Confirm the necessary corporate-action and point-in-time conventions before using data for backtesting.

FAQ

How do I get historical stock prices with Search API?

With Valyu, send a company name and historical period in query. Check for valyu/valyu-stocks results, inspect the returned instrument metadata and parse the OHLCV array. The tested query-only request still required a key with stock-source access.

Do I need to know the ticker first?

Not for the tested Apple and Microsoft requests. They supplied company names and dates, and returned resolved tickers in metadata. Always inspect that resolution; an ambiguous name or multiple listings may need a more precise query.

Does the Search API return OHLCV data or text about a stock?

The stock-history responses tested here returned native arrays with open, high, low, close and volume. General web-search results are different. The script rejects prose and non-stock results instead of converting them into invented observations.

Where should I put the start and end dates?

Put the desired trading period in the natural-language query for this example. Search's optional startDate and endDate options filter publication dates; they were not used to request these stock-history periods.

Are these adjusted prices or total returns?

The example did not verify an adjustment policy. It computes changes from returned closes without adding dividends or modelling reinvestment; it does not establish total investment returns. Check the source's corporate-action and adjustment conventions before relying on a longer comparison or backtest.

Is stock-data access included with every API key?

No. Authentication and dataset access are separate. A restricted key was denied when explicitly targeting the stock dataset in this verification. Check the live catalogue, account access and current pricing.

Try a historical-price query you can verify

Get a Valyu API key, confirm stock-source access, and start with a company and short historical window whose identity and dates you can check.

Keep the original observations and metadata, calculate in code, and only then build the chart or agent explanation. This is a developer data-retrieval example, not an investment recommendation or financial advice.

來源:DEV Community · dev.to