SPB Git

spb/vquant Public MIT

VibeQuant — AI-powered institutional-grade financial intelligence platform.

TypeScript 84.3% Python 11.7% JavaScript 1.6% CSS 1.5% HTML 0.7%
12.3 KB · 413 lines typescript
Raw Blame History
1/*2 * =============================================================================3 *  VibeQuant (vquant) — AI-Powered Financial Intelligence Platform4 * -----------------------------------------------------------------------------5 *  File:      server/services/eodhdService.ts6 *7 *  Author:    Simon-Pierre Boucher8 *  Contact:   contact@spboucher.ai9 *  Website:   https://www.spboucher.ai10 *  Demo:      https://www.vquant.ai11 *  License:   MIT (see LICENSE)12 *13 *  Copyright © 2026 Simon-Pierre Boucher. All rights reserved.14 * =============================================================================15 */1617import axios from 'axios';1819const EODHD_API_KEY = process.env.EODHD_API_KEY;20const EODHD_BASE_URL = 'https://eodhd.com/api';2122export interface EodhdBar {23  date: string;24  open: number;25  high: number;26  low: number;27  close: number;28  adjusted_close: number;29  volume: number;30}3132export interface EodhdRealTimeQuote {33  code: string;34  timestamp: number;35  open: number;36  high: number;37  low: number;38  close: number;39  volume: number;40  previousClose: number;41  change: number;42  change_p: number;43}4445export interface EodhdIntradayBar {46  timestamp: number;47  datetime: string;48  open: number;49  high: number;50  low: number;51  close: number;52  volume: number;53}5455export interface EodhdDividend {56  date: string;57  declarationDate?: string;58  recordDate?: string;59  paymentDate?: string;60  period?: string;61  value: number;62  unadjustedValue?: number;63  currency?: string;64}6566export interface EodhdSplit {67  date: string;68  split: string;69}7071export interface EodhdSearchResult {72  Code: string;73  Exchange: string;74  Name: string;75  Type: string;76  Country: string;77  Currency: string;78  ISIN?: string;79  previousClose?: number;80}8182export interface EodhdNewsItem {83  date: string;84  title: string;85  content: string;86  link: string;87  symbols: string[];88  tags?: string[];89  sentiment?: { polarity: number; neg: number; neu: number; pos: number };90}9192/**93 * Normalize a symbol to EODHD format (TICKER.EXCHANGE).94 * Defaults to the US exchange when no exchange suffix is provided.95 */96function normalizeSymbol(symbol: string): string {97  const s = symbol.trim().toUpperCase();98  return s.includes('.') ? s : `${s}.US`;99}100101/**102 * Generic request function for the EODHD API103 */104async function eodhdRequest<T>(endpoint: string, params: Record<string, any> = {}): Promise<T> {105  if (!EODHD_API_KEY) {106    throw new Error('EODHD_API_KEY not configured');107  }108109  try {110    console.log(`[EODHD API] Requesting ${endpoint} with params:`, JSON.stringify(params));111112    const response = await axios.get(`${EODHD_BASE_URL}${endpoint}`, {113      params: {114        ...params,115        api_token: EODHD_API_KEY,116        fmt: 'json',117      },118      timeout: 20000,119    });120121    console.log(`[EODHD API] Response for ${endpoint}:`,122      Array.isArray(response.data) ? `Array with ${response.data.length} items` : typeof response.data123    );124125    return response.data;126  } catch (error) {127    console.error(`[EODHD API] ERROR for ${endpoint}:`, error);128    if (axios.isAxiosError(error) && error.response) {129      console.error(`[EODHD API] Response status: ${error.response.status}`);130      console.error(`[EODHD API] Response data:`, JSON.stringify(error.response.data));131      throw new Error(`EODHD API error: ${error.response.status} - ${error.response.statusText}. Details: ${JSON.stringify(error.response.data)}`);132    }133    throw new Error(`Failed to fetch from EODHD API: ${(error as Error).message}`);134  }135}136137/**138 * Get end-of-day historical OHLCV prices (with adjusted close) for any139 * instrument on 70+ exchanges worldwide.140 *141 * @param symbol - Ticker in EODHD format, e.g. "AAPL.US", "AIR.PA", "BTC-USD.CC". Bare tickers default to .US142 * @param from - Start date YYYY-MM-DD (optional)143 * @param to - End date YYYY-MM-DD (optional)144 * @param period - 'd' daily (default), 'w' weekly, 'm' monthly145 */146export async function getEodhdHistorical(147  symbol: string,148  from?: string,149  to?: string,150  period: 'd' | 'w' | 'm' = 'd'151): Promise<EodhdBar[]> {152  const params: Record<string, any> = { period };153  if (from) params.from = from;154  if (to) params.to = to;155  return eodhdRequest<EodhdBar[]>(`/eod/${normalizeSymbol(symbol)}`, params);156}157158/**159 * Get a live (delayed ~15min) quote for one or more instruments.160 *161 * @param symbol - Primary ticker, e.g. "AAPL.US"162 * @param additionalSymbols - Optional extra tickers quoted in the same call163 */164export async function getEodhdRealTimeQuote(165  symbol: string,166  additionalSymbols?: string[]167): Promise<EodhdRealTimeQuote | EodhdRealTimeQuote[]> {168  const params: Record<string, any> = {};169  if (additionalSymbols && additionalSymbols.length > 0) {170    params.s = additionalSymbols.map(normalizeSymbol).join(',');171  }172  return eodhdRequest<EodhdRealTimeQuote | EodhdRealTimeQuote[]>(173    `/real-time/${normalizeSymbol(symbol)}`,174    params175  );176}177178/**179 * Get intraday historical OHLCV bars.180 *181 * @param symbol - Ticker, e.g. "AAPL.US"182 * @param interval - '1m', '5m' or '1h'183 * @param from - Unix timestamp (seconds) start (optional)184 * @param to - Unix timestamp (seconds) end (optional)185 */186export async function getEodhdIntraday(187  symbol: string,188  interval: '1m' | '5m' | '1h' = '5m',189  from?: number,190  to?: number191): Promise<EodhdIntradayBar[]> {192  const params: Record<string, any> = { interval };193  if (from) params.from = from;194  if (to) params.to = to;195  return eodhdRequest<EodhdIntradayBar[]>(`/intraday/${normalizeSymbol(symbol)}`, params);196}197198/**199 * Get fundamentals for a company/ETF/fund. The full payload is very large,200 * so by default only the most useful sections are returned.201 *202 * @param symbol - Ticker, e.g. "AAPL.US"203 * @param filter - Comma-separated sections, e.g. "General,Highlights,Valuation" or a204 *                 deep path like "Financials::Balance_Sheet::yearly". Pass "full" for everything.205 */206export async function getEodhdFundamentals(207  symbol: string,208  filter?: string209): Promise<any> {210  const params: Record<string, any> = {};211  if (filter?.toLowerCase() !== 'full') {212    params.filter = filter || 'General,Highlights,Valuation,SharesStats,Technicals,SplitsDividends';213  }214  return eodhdRequest<any>(`/fundamentals/${normalizeSymbol(symbol)}`, params);215}216217/**218 * Get historical dividends for an instrument.219 */220export async function getEodhdDividends(221  symbol: string,222  from?: string,223  to?: string224): Promise<EodhdDividend[]> {225  const params: Record<string, any> = {};226  if (from) params.from = from;227  if (to) params.to = to;228  return eodhdRequest<EodhdDividend[]>(`/div/${normalizeSymbol(symbol)}`, params);229}230231/**232 * Get historical stock splits for an instrument.233 */234export async function getEodhdSplits(235  symbol: string,236  from?: string,237  to?: string238): Promise<EodhdSplit[]> {239  const params: Record<string, any> = {};240  if (from) params.from = from;241  if (to) params.to = to;242  return eodhdRequest<EodhdSplit[]>(`/splits/${normalizeSymbol(symbol)}`, params);243}244245/**246 * Search stocks, ETFs, funds, indices and crypto by ticker or company name247 * across all exchanges covered by EODHD.248 */249export async function searchEodhd(query: string, limit: number = 15): Promise<EodhdSearchResult[]> {250  return eodhdRequest<EodhdSearchResult[]>(`/search/${encodeURIComponent(query)}`, { limit });251}252253// --- Options (EODHD Marketplace / UnicornBay add-on) ---254255export interface EodhdOptionContractSummary {256  identifier: string;257  option_type: string;258  strike: number;259  expiration_date: string;260  days_to_expiration: number;261  bid?: number;262  ask?: number;263  last_price?: number;264  volume?: number;265  open_interest?: number;266  implied_volatility?: number;267}268269export interface EodhdOptionsChain {270  symbol: string;271  total_contracts: number;272  returned_contracts: number;273  options: EodhdOptionContractSummary[];274}275276const OPTIONS_CHAIN_FIELDS = 'contract,underlying_symbol,exp_date,type,strike,bid,ask,last,volume,open_interest,volatility,dte';277278/**279 * Underlying symbols for the options API are bare US tickers (AAPL, not AAPL.US).280 */281function toUnderlyingSymbol(symbol: string): string {282  return symbol.trim().toUpperCase().replace(/\.US$/, '');283}284285function optionsAddonError(error: unknown): Error | null {286  const status = axios.isAxiosError(error) && error.response287    ? error.response.status288    : Number(/EODHD API error: (\d{3}) /.exec((error as Error)?.message || '')?.[1]);289  if (status === 402 || status === 403) {290    return new Error(291      'EODHD Options add-on (UnicornBay marketplace) is not active on this API key. ' +292      'Subscribe at https://eodhd.com/marketplace/unicornbay/options to enable real options data.'293    );294  }295  return null;296}297298/**299 * Get the options chain (US stocks only) from the EODHD UnicornBay options API.300 * One call returns contracts with quotes, open interest and implied volatility.301 *302 * @param symbol - Underlying US ticker (e.g. "AAPL")303 * @param opts - Optional filters: expiration/strike ranges, type, pagination304 */305export async function getEodhdOptionsChain(306  symbol: string,307  opts: {308    expDateFrom?: string;309    expDateTo?: string;310    strikeFrom?: number;311    strikeTo?: number;312    type?: 'call' | 'put';313    limit?: number;314    offset?: number;315  } = {}316): Promise<EodhdOptionsChain> {317  const underlying = toUnderlyingSymbol(symbol);318  const params: Record<string, any> = {319    'filter[underlying_symbol]': underlying,320    'fields[options-contracts]': OPTIONS_CHAIN_FIELDS,321    sort: 'exp_date',322    'page[limit]': Math.min(opts.limit ?? 1000, 1000),323    'page[offset]': opts.offset ?? 0,324  };325  if (opts.expDateFrom) params['filter[exp_date_from]'] = opts.expDateFrom;326  if (opts.expDateTo) params['filter[exp_date_to]'] = opts.expDateTo;327  if (opts.strikeFrom !== undefined) params['filter[strike_from]'] = opts.strikeFrom;328  if (opts.strikeTo !== undefined) params['filter[strike_to]'] = opts.strikeTo;329  if (opts.type) params['filter[type]'] = opts.type;330331  try {332    const response = await eodhdRequest<any>('/mp/unicornbay/options/contracts', params);333    const data = Array.isArray(response?.data) ? response.data : [];334    return {335      symbol: underlying,336      total_contracts: response?.meta?.total ?? data.length,337      returned_contracts: data.length,338      options: data.map((item: any) => {339        const a = item.attributes || {};340        return {341          identifier: a.contract,342          option_type: a.type,343          strike: a.strike,344          expiration_date: a.exp_date,345          days_to_expiration: a.dte,346          bid: a.bid,347          ask: a.ask,348          last_price: a.last,349          volume: a.volume,350          open_interest: a.open_interest,351          implied_volatility: a.volatility,352        };353      }),354    };355  } catch (error) {356    throw optionsAddonError(error) ?? error;357  }358}359360/**361 * Get full details for a single option contract: quote (bid/ask/last),362 * volume, open interest, implied volatility and Greeks (delta, gamma,363 * theta, vega, rho) — all in one call.364 *365 * @param contract - OCC contract identifier, e.g. "AAPL271217C00420000"366 */367export async function getEodhdOptionContract(contract: string): Promise<any> {368  try {369    const response = await eodhdRequest<any>('/mp/unicornbay/options/contracts', {370      'filter[contract]': contract.trim().toUpperCase(),371    });372    const item = Array.isArray(response?.data) ? response.data[0] : null;373    if (!item) {374      throw new Error(`Option contract not found: ${contract}`);375    }376    return item.attributes;377  } catch (error) {378    throw optionsAddonError(error) ?? error;379  }380}381382/**383 * Get financial news for a ticker or a topic tag.384 *385 * @param symbol - Ticker, e.g. "AAPL.US" (optional if tag provided)386 * @param tag - Topic tag, e.g. "mergers and acquisitions" (optional)387 * @param limit - Number of articles (default 10, max 1000)388 */389export async function getEodhdNews(390  symbol?: string,391  tag?: string,392  limit: number = 10,393  from?: string,394  to?: string395): Promise<EodhdNewsItem[]> {396  if (!symbol && !tag) {397    throw new Error('Either symbol or tag is required for EODHD news');398  }399  const params: Record<string, any> = { limit };400  if (symbol) params.s = normalizeSymbol(symbol);401  if (tag) params.t = tag;402  if (from) params.from = from;403  if (to) params.to = to;404  const items = await eodhdRequest<EodhdNewsItem[]>('/news', params);405  // Trim article bodies so tool results stay compact406  return items.map((item) => ({407    ...item,408    content: item.content && item.content.length > 1500409      ? `${item.content.slice(0, 1500)}…`410      : item.content,411  }));412}413