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%
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