/* * ============================================================================= * VibeQuant (vquant) — AI-Powered Financial Intelligence Platform * ----------------------------------------------------------------------------- * File: server/services/eodhdService.ts * * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai * Website: https://www.spboucher.ai * Demo: https://www.vquant.ai * License: MIT (see LICENSE) * * Copyright © 2026 Simon-Pierre Boucher. All rights reserved. * ============================================================================= */ import axios from 'axios'; const EODHD_API_KEY = process.env.EODHD_API_KEY; const EODHD_BASE_URL = 'https://eodhd.com/api'; export interface EodhdBar { date: string; open: number; high: number; low: number; close: number; adjusted_close: number; volume: number; } export interface EodhdRealTimeQuote { code: string; timestamp: number; open: number; high: number; low: number; close: number; volume: number; previousClose: number; change: number; change_p: number; } export interface EodhdIntradayBar { timestamp: number; datetime: string; open: number; high: number; low: number; close: number; volume: number; } export interface EodhdDividend { date: string; declarationDate?: string; recordDate?: string; paymentDate?: string; period?: string; value: number; unadjustedValue?: number; currency?: string; } export interface EodhdSplit { date: string; split: string; } export interface EodhdSearchResult { Code: string; Exchange: string; Name: string; Type: string; Country: string; Currency: string; ISIN?: string; previousClose?: number; } export interface EodhdNewsItem { date: string; title: string; content: string; link: string; symbols: string[]; tags?: string[]; sentiment?: { polarity: number; neg: number; neu: number; pos: number }; } /** * Normalize a symbol to EODHD format (TICKER.EXCHANGE). * Defaults to the US exchange when no exchange suffix is provided. */ function normalizeSymbol(symbol: string): string { const s = symbol.trim().toUpperCase(); return s.includes('.') ? s : `${s}.US`; } /** * Generic request function for the EODHD API */ async function eodhdRequest(endpoint: string, params: Record = {}): Promise { if (!EODHD_API_KEY) { throw new Error('EODHD_API_KEY not configured'); } try { console.log(`[EODHD API] Requesting ${endpoint} with params:`, JSON.stringify(params)); const response = await axios.get(`${EODHD_BASE_URL}${endpoint}`, { params: { ...params, api_token: EODHD_API_KEY, fmt: 'json', }, timeout: 20000, }); console.log(`[EODHD API] Response for ${endpoint}:`, Array.isArray(response.data) ? `Array with ${response.data.length} items` : typeof response.data ); return response.data; } catch (error) { console.error(`[EODHD API] ERROR for ${endpoint}:`, error); if (axios.isAxiosError(error) && error.response) { console.error(`[EODHD API] Response status: ${error.response.status}`); console.error(`[EODHD API] Response data:`, JSON.stringify(error.response.data)); throw new Error(`EODHD API error: ${error.response.status} - ${error.response.statusText}. Details: ${JSON.stringify(error.response.data)}`); } throw new Error(`Failed to fetch from EODHD API: ${(error as Error).message}`); } } /** * Get end-of-day historical OHLCV prices (with adjusted close) for any * instrument on 70+ exchanges worldwide. * * @param symbol - Ticker in EODHD format, e.g. "AAPL.US", "AIR.PA", "BTC-USD.CC". Bare tickers default to .US * @param from - Start date YYYY-MM-DD (optional) * @param to - End date YYYY-MM-DD (optional) * @param period - 'd' daily (default), 'w' weekly, 'm' monthly */ export async function getEodhdHistorical( symbol: string, from?: string, to?: string, period: 'd' | 'w' | 'm' = 'd' ): Promise { const params: Record = { period }; if (from) params.from = from; if (to) params.to = to; return eodhdRequest(`/eod/${normalizeSymbol(symbol)}`, params); } /** * Get a live (delayed ~15min) quote for one or more instruments. * * @param symbol - Primary ticker, e.g. "AAPL.US" * @param additionalSymbols - Optional extra tickers quoted in the same call */ export async function getEodhdRealTimeQuote( symbol: string, additionalSymbols?: string[] ): Promise { const params: Record = {}; if (additionalSymbols && additionalSymbols.length > 0) { params.s = additionalSymbols.map(normalizeSymbol).join(','); } return eodhdRequest( `/real-time/${normalizeSymbol(symbol)}`, params ); } /** * Get intraday historical OHLCV bars. * * @param symbol - Ticker, e.g. "AAPL.US" * @param interval - '1m', '5m' or '1h' * @param from - Unix timestamp (seconds) start (optional) * @param to - Unix timestamp (seconds) end (optional) */ export async function getEodhdIntraday( symbol: string, interval: '1m' | '5m' | '1h' = '5m', from?: number, to?: number ): Promise { const params: Record = { interval }; if (from) params.from = from; if (to) params.to = to; return eodhdRequest(`/intraday/${normalizeSymbol(symbol)}`, params); } /** * Get fundamentals for a company/ETF/fund. The full payload is very large, * so by default only the most useful sections are returned. * * @param symbol - Ticker, e.g. "AAPL.US" * @param filter - Comma-separated sections, e.g. "General,Highlights,Valuation" or a * deep path like "Financials::Balance_Sheet::yearly". Pass "full" for everything. */ export async function getEodhdFundamentals( symbol: string, filter?: string ): Promise { const params: Record = {}; if (filter?.toLowerCase() !== 'full') { params.filter = filter || 'General,Highlights,Valuation,SharesStats,Technicals,SplitsDividends'; } return eodhdRequest(`/fundamentals/${normalizeSymbol(symbol)}`, params); } /** * Get historical dividends for an instrument. */ export async function getEodhdDividends( symbol: string, from?: string, to?: string ): Promise { const params: Record = {}; if (from) params.from = from; if (to) params.to = to; return eodhdRequest(`/div/${normalizeSymbol(symbol)}`, params); } /** * Get historical stock splits for an instrument. */ export async function getEodhdSplits( symbol: string, from?: string, to?: string ): Promise { const params: Record = {}; if (from) params.from = from; if (to) params.to = to; return eodhdRequest(`/splits/${normalizeSymbol(symbol)}`, params); } /** * Search stocks, ETFs, funds, indices and crypto by ticker or company name * across all exchanges covered by EODHD. */ export async function searchEodhd(query: string, limit: number = 15): Promise { return eodhdRequest(`/search/${encodeURIComponent(query)}`, { limit }); } // --- Options (EODHD Marketplace / UnicornBay add-on) --- export interface EodhdOptionContractSummary { identifier: string; option_type: string; strike: number; expiration_date: string; days_to_expiration: number; bid?: number; ask?: number; last_price?: number; volume?: number; open_interest?: number; implied_volatility?: number; } export interface EodhdOptionsChain { symbol: string; total_contracts: number; returned_contracts: number; options: EodhdOptionContractSummary[]; } const OPTIONS_CHAIN_FIELDS = 'contract,underlying_symbol,exp_date,type,strike,bid,ask,last,volume,open_interest,volatility,dte'; /** * Underlying symbols for the options API are bare US tickers (AAPL, not AAPL.US). */ function toUnderlyingSymbol(symbol: string): string { return symbol.trim().toUpperCase().replace(/\.US$/, ''); } function optionsAddonError(error: unknown): Error | null { const status = axios.isAxiosError(error) && error.response ? error.response.status : Number(/EODHD API error: (\d{3}) /.exec((error as Error)?.message || '')?.[1]); if (status === 402 || status === 403) { return new Error( 'EODHD Options add-on (UnicornBay marketplace) is not active on this API key. ' + 'Subscribe at https://eodhd.com/marketplace/unicornbay/options to enable real options data.' ); } return null; } /** * Get the options chain (US stocks only) from the EODHD UnicornBay options API. * One call returns contracts with quotes, open interest and implied volatility. * * @param symbol - Underlying US ticker (e.g. "AAPL") * @param opts - Optional filters: expiration/strike ranges, type, pagination */ export async function getEodhdOptionsChain( symbol: string, opts: { expDateFrom?: string; expDateTo?: string; strikeFrom?: number; strikeTo?: number; type?: 'call' | 'put'; limit?: number; offset?: number; } = {} ): Promise { const underlying = toUnderlyingSymbol(symbol); const params: Record = { 'filter[underlying_symbol]': underlying, 'fields[options-contracts]': OPTIONS_CHAIN_FIELDS, sort: 'exp_date', 'page[limit]': Math.min(opts.limit ?? 1000, 1000), 'page[offset]': opts.offset ?? 0, }; if (opts.expDateFrom) params['filter[exp_date_from]'] = opts.expDateFrom; if (opts.expDateTo) params['filter[exp_date_to]'] = opts.expDateTo; if (opts.strikeFrom !== undefined) params['filter[strike_from]'] = opts.strikeFrom; if (opts.strikeTo !== undefined) params['filter[strike_to]'] = opts.strikeTo; if (opts.type) params['filter[type]'] = opts.type; try { const response = await eodhdRequest('/mp/unicornbay/options/contracts', params); const data = Array.isArray(response?.data) ? response.data : []; return { symbol: underlying, total_contracts: response?.meta?.total ?? data.length, returned_contracts: data.length, options: data.map((item: any) => { const a = item.attributes || {}; return { identifier: a.contract, option_type: a.type, strike: a.strike, expiration_date: a.exp_date, days_to_expiration: a.dte, bid: a.bid, ask: a.ask, last_price: a.last, volume: a.volume, open_interest: a.open_interest, implied_volatility: a.volatility, }; }), }; } catch (error) { throw optionsAddonError(error) ?? error; } } /** * Get full details for a single option contract: quote (bid/ask/last), * volume, open interest, implied volatility and Greeks (delta, gamma, * theta, vega, rho) — all in one call. * * @param contract - OCC contract identifier, e.g. "AAPL271217C00420000" */ export async function getEodhdOptionContract(contract: string): Promise { try { const response = await eodhdRequest('/mp/unicornbay/options/contracts', { 'filter[contract]': contract.trim().toUpperCase(), }); const item = Array.isArray(response?.data) ? response.data[0] : null; if (!item) { throw new Error(`Option contract not found: ${contract}`); } return item.attributes; } catch (error) { throw optionsAddonError(error) ?? error; } } /** * Get financial news for a ticker or a topic tag. * * @param symbol - Ticker, e.g. "AAPL.US" (optional if tag provided) * @param tag - Topic tag, e.g. "mergers and acquisitions" (optional) * @param limit - Number of articles (default 10, max 1000) */ export async function getEodhdNews( symbol?: string, tag?: string, limit: number = 10, from?: string, to?: string ): Promise { if (!symbol && !tag) { throw new Error('Either symbol or tag is required for EODHD news'); } const params: Record = { limit }; if (symbol) params.s = normalizeSymbol(symbol); if (tag) params.t = tag; if (from) params.from = from; if (to) params.to = to; const items = await eodhdRequest('/news', params); // Trim article bodies so tool results stay compact return items.map((item) => ({ ...item, content: item.content && item.content.length > 1500 ? `${item.content.slice(0, 1500)}…` : item.content, })); }