/**
* Market data abstraction layer
*
* Provides a pluggable interface for fetching market data from various providers.
* Supported: Polygon.io (recommended), Yahoo Finance (unlimited fallback)
*/
import type { OptionContract, OptionsChain, OptionType } from "@quant-companion/core";
/** Yahoo Finance option quote structure */
interface YahooOptionQuote {
contractSymbol?: string;
strike?: number;
lastPrice?: number;
bid?: number;
ask?: number;
volume?: number;
openInterest?: number;
impliedVolatility?: number;
}
/** Helper to parse Yahoo option data into OptionContract */
function parseYahooOption(
underlying: string,
opt: YahooOptionQuote,
right: OptionType,
expiration: string,
timeToMaturityYears: number
): OptionContract {
const bid = opt.bid ?? 0;
const ask = opt.ask ?? 0;
const mid = (bid + ask) / 2;
return {
underlying,
optionSymbol: opt.contractSymbol || "",
right,
strike: opt.strike ?? 0,
expiration,
timeToMaturityYears,
lastPrice: opt.lastPrice ?? 0,
bid,
ask,
mid,
volume: opt.volume ?? 0,
openInterest: opt.openInterest ?? 0,
impliedVol: opt.impliedVolatility,
};
}
export interface OHLCV {
timestamp: number;
open: number;
high: number;
low: number;
close: number;
volume?: number;
}
export interface Quote {
symbol: string;
price: number;
timestamp: number;
}
export interface OptionsChainParams {
symbol: string;
/** Optional specific expiration date (ISO string). If omitted, returns all available expirations. */
expiration?: string;
}
/** Risk-free rate point */
export interface RatePoint {
/** Maturity in years */
maturityYears: number;
/** Annualized rate */
rate: number;
}
export interface MarketDataProvider {
readonly name: string;
/**
* Get current quote for a symbol
*/
getQuote(symbol: string): Promise<Quote>;
/**
* Get historical OHLCV data
*/
getHistoricalOHLCV(params: {
symbol: string;
start: Date;
end: Date;
interval: "1d" | "1h" | "5m";
}): Promise<OHLCV[]>;
/**
* Get options chain for a symbol
*/
getOptionsChain(params: OptionsChainParams): Promise<OptionsChain>;
/**
* Get risk-free rate curve (optional)
* Returns flat rate if not implemented by provider
*/
getRateCurve?(): Promise<RatePoint[]>;
/**
* Get dividend yield for a symbol (optional)
* Returns 0 if not implemented by provider
*/
getDividendYield?(symbol: string): Promise<number>;
}
/**
* Yahoo Finance provider using free public endpoints
*/
export class YahooFinanceProvider implements MarketDataProvider {
readonly name = "Yahoo Finance";
private crumb: string | null = null;
private cookies: string | null = null;
private async fetchCrumb(): Promise<void> {
if (this.crumb && this.cookies) return;
// First, get cookies from Yahoo Finance
const initResponse = await fetch("https://fc.yahoo.com", {
headers: {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
},
});
// Get set-cookie header
const setCookie = initResponse.headers.get("set-cookie");
if (setCookie) {
this.cookies = setCookie;
}
// Now get crumb
const crumbResponse = await fetch("https://query2.finance.yahoo.com/v1/test/getcrumb", {
headers: {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Cookie": this.cookies || "",
},
});
if (crumbResponse.ok) {
this.crumb = await crumbResponse.text();
}
}
async getQuote(symbol: string): Promise<Quote> {
const url = `https://query1.finance.yahoo.com/v8/finance/chart/${encodeURIComponent(symbol)}?interval=1d&range=1d`;
const response = await fetch(url, {
headers: {
"User-Agent": "Mozilla/5.0",
},
});
if (!response.ok) {
throw new Error(`Failed to fetch quote for ${symbol}: ${response.statusText}`);
}
const data = (await response.json()) as {
chart?: { result?: Array<{ meta: { symbol?: string; regularMarketPrice?: number; previousClose?: number } }> };
};
const result = data.chart?.result?.[0];
if (!result) {
throw new Error(`No data found for symbol ${symbol}`);
}
const meta = result.meta;
const price = meta.regularMarketPrice ?? meta.previousClose;
if (price === undefined) {
throw new Error(`No price data available for ${symbol}`);
}
return {
symbol: meta.symbol || symbol,
price,
timestamp: Date.now(),
};
}
async getOptionsChain(params: OptionsChainParams): Promise<OptionsChain> {
const { symbol, expiration } = params;
// Ensure we have auth
await this.fetchCrumb();
// Build URL - Yahoo's options endpoint
let url = `https://query2.finance.yahoo.com/v7/finance/options/${encodeURIComponent(symbol)}`;
const queryParams: string[] = [];
if (expiration) {
// Yahoo expects Unix timestamp for expiration
const expDate = new Date(expiration);
const expTimestamp = Math.floor(expDate.getTime() / 1000);
queryParams.push(`date=${expTimestamp}`);
}
if (this.crumb) {
queryParams.push(`crumb=${encodeURIComponent(this.crumb)}`);
}
if (queryParams.length > 0) {
url += `?${queryParams.join("&")}`;
}
const response = await fetch(url, {
headers: {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Accept": "application/json",
"Accept-Language": "en-US,en;q=0.9",
"Cookie": this.cookies || "",
},
});
if (!response.ok) {
throw new Error(`Failed to fetch options chain for ${symbol}: ${response.statusText}`);
}
const data = (await response.json()) as {
optionChain?: {
result?: Array<{
underlyingSymbol?: string;
expirationDates?: number[];
quote?: {
regularMarketPrice?: number;
};
options?: Array<{
expirationDate?: number;
calls?: YahooOptionQuote[];
puts?: YahooOptionQuote[];
}>;
}>;
error?: { description?: string };
};
};
if (data.optionChain?.error) {
throw new Error(`Options data not found for ${symbol}: ${data.optionChain.error.description || "Unknown error"}`);
}
const result = data.optionChain?.result?.[0];
if (!result) {
throw new Error(`No options data found for symbol ${symbol}`);
}
const underlyingPrice = result.quote?.regularMarketPrice ?? 0;
const now = new Date();
const asOf = now.toISOString();
// Convert expiration timestamps to ISO dates
const expirations = (result.expirationDates || []).map((ts) =>
new Date(ts * 1000).toISOString().split("T")[0]
);
// Parse option contracts
const contracts: OptionContract[] = [];
for (const optionData of result.options || []) {
const expDateTs = optionData.expirationDate || 0;
const expDate = new Date(expDateTs * 1000);
const expirationStr = expDate.toISOString().split("T")[0];
const timeToMaturityYears = Math.max(0, (expDate.getTime() - now.getTime()) / (365.25 * 24 * 60 * 60 * 1000));
// Process calls
for (const call of optionData.calls || []) {
contracts.push(parseYahooOption(symbol, call, "call", expirationStr, timeToMaturityYears));
}
// Process puts
for (const put of optionData.puts || []) {
contracts.push(parseYahooOption(symbol, put, "put", expirationStr, timeToMaturityYears));
}
}
return {
symbol,
asOf,
underlyingPrice,
expirations,
contracts,
};
}
async getHistoricalOHLCV(params: {
symbol: string;
start: Date;
end: Date;
interval: "1d" | "1h" | "5m";
}): Promise<OHLCV[]> {
const { symbol, start, end, interval } = params;
const period1 = Math.floor(start.getTime() / 1000);
const period2 = Math.floor(end.getTime() / 1000);
const intervalMap: Record<string, string> = {
"1d": "1d",
"1h": "1h",
"5m": "5m",
};
const url = `https://query1.finance.yahoo.com/v8/finance/chart/${encodeURIComponent(symbol)}?period1=${period1}&period2=${period2}&interval=${intervalMap[interval]}`;
const response = await fetch(url, {
headers: {
"User-Agent": "Mozilla/5.0",
},
});
if (!response.ok) {
throw new Error(
`Failed to fetch historical data for ${symbol}: ${response.statusText}`
);
}
const data = (await response.json()) as {
chart?: {
result?: Array<{
timestamp?: number[];
indicators?: {
quote?: Array<{
open?: (number | null)[];
high?: (number | null)[];
low?: (number | null)[];
close?: (number | null)[];
volume?: (number | null)[];
}>;
};
}>;
};
};
const result = data.chart?.result?.[0];
if (!result) {
throw new Error(`No historical data found for symbol ${symbol}`);
}
const timestamps = result.timestamp || [];
const quote = result.indicators?.quote?.[0] || {};
const candles: OHLCV[] = [];
for (let i = 0; i < timestamps.length; i++) {
const open = quote.open?.[i];
const high = quote.high?.[i];
const low = quote.low?.[i];
const close = quote.close?.[i];
const volume = quote.volume?.[i];
if (open != null && high != null && low != null && close != null) {
candles.push({
timestamp: timestamps[i] * 1000, // Convert to milliseconds
open,
high,
low,
close,
volume: volume ?? undefined,
});
}
}
return candles;
}
/**
* Get risk-free rate curve (simplified flat rate)
*/
async getRateCurve(): Promise<RatePoint[]> {
// Return a flat rate curve (simplified - in production would fetch from FRED or Treasury)
const flatRate = 0.045; // ~4.5% as of late 2024
return [
{ maturityYears: 0.25, rate: flatRate },
{ maturityYears: 0.5, rate: flatRate },
{ maturityYears: 1, rate: flatRate },
{ maturityYears: 2, rate: flatRate },
{ maturityYears: 5, rate: flatRate },
];
}
/**
* Get dividend yield for a symbol (simplified)
*/
async getDividendYield(_symbol: string): Promise<number> {
// Simplified: return 0 for all symbols
// In production, would fetch from Yahoo's quote data
return 0;
}
}
/**
* Alpha Vantage provider (requires API key)
*/
export class AlphaVantageProvider implements MarketDataProvider {
readonly name = "Alpha Vantage";
private apiKey: string;
private baseUrl = "https://www.alphavantage.co/query";
constructor(apiKey: string) {
this.apiKey = apiKey;
}
async getQuote(symbol: string): Promise<Quote> {
const url = `${this.baseUrl}?function=GLOBAL_QUOTE&symbol=${encodeURIComponent(symbol)}&apikey=${this.apiKey}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch quote for ${symbol}: ${response.statusText}`);
}
const data = (await response.json()) as {
"Global Quote"?: {
"01. symbol"?: string;
"05. price"?: string;
};
};
const quote = data["Global Quote"];
if (!quote || !quote["05. price"]) {
throw new Error(`No data found for symbol ${symbol}`);
}
return {
symbol: quote["01. symbol"] || symbol,
price: parseFloat(quote["05. price"]),
timestamp: Date.now(),
};
}
async getHistoricalOHLCV(params: {
symbol: string;
start: Date;
end: Date;
interval: "1d" | "1h" | "5m";
}): Promise<OHLCV[]> {
const { symbol, start, end, interval } = params;
let functionName: string;
let timeKey: string;
if (interval === "1d") {
functionName = "TIME_SERIES_DAILY";
timeKey = "Time Series (Daily)";
} else if (interval === "1h") {
functionName = "TIME_SERIES_INTRADAY";
timeKey = "Time Series (60min)";
} else {
functionName = "TIME_SERIES_INTRADAY";
timeKey = "Time Series (5min)";
}
let url = `${this.baseUrl}?function=${functionName}&symbol=${encodeURIComponent(symbol)}&apikey=${this.apiKey}&outputsize=full`;
if (interval !== "1d") {
url += `&interval=${interval === "1h" ? "60min" : "5min"}`;
}
const response = await fetch(url);
if (!response.ok) {
throw new Error(
`Failed to fetch historical data for ${symbol}: ${response.statusText}`
);
}
const data = (await response.json()) as Record<string, Record<string, Record<string, string>> | undefined>;
const timeSeries = data[timeKey];
if (!timeSeries) {
throw new Error(`No historical data found for symbol ${symbol}`);
}
const candles: OHLCV[] = [];
const startTime = start.getTime();
const endTime = end.getTime();
for (const [dateStr, values] of Object.entries(timeSeries)) {
const timestamp = new Date(dateStr).getTime();
if (timestamp >= startTime && timestamp <= endTime) {
const v = values as Record<string, string>;
candles.push({
timestamp,
open: parseFloat(v["1. open"]),
high: parseFloat(v["2. high"]),
low: parseFloat(v["3. low"]),
close: parseFloat(v["4. close"]),
volume: parseInt(v["5. volume"], 10),
});
}
}
// Sort by timestamp ascending
candles.sort((a, b) => a.timestamp - b.timestamp);
return candles;
}
async getOptionsChain(_params: OptionsChainParams): Promise<OptionsChain> {
throw new Error("Options chain data not available from Alpha Vantage provider");
}
}
// ============================================================================
// Polygon.io Types
// ============================================================================
interface PolygonTickerSnapshot {
ticker?: string;
day?: { c?: number; h?: number; l?: number; o?: number; v?: number };
lastTrade?: { p?: number; t?: number };
prevDay?: { c?: number };
}
interface PolygonAggBar {
t: number; // Unix ms timestamp
o: number; // Open
h: number; // High
l: number; // Low
c: number; // Close
v: number; // Volume
}
interface PolygonOptionContract {
ticker?: string;
underlying_ticker?: string;
contract_type?: "call" | "put";
strike_price?: number;
expiration_date?: string;
}
interface PolygonOptionQuote {
day?: { c?: number; o?: number; h?: number; l?: number; v?: number };
last_quote?: { bid?: number; ask?: number; bid_size?: number; ask_size?: number };
open_interest?: number;
implied_volatility?: number;
greeks?: {
delta?: number;
gamma?: number;
theta?: number;
vega?: number;
};
}
/**
* Polygon.io provider - Professional-grade market data
*
* Features:
* - Real-time & delayed US stocks, options, FX, crypto
* - Full options chain with Greeks and IV
* - High-quality historical data
*
* Requires API key: Set POLYGON_API_KEY environment variable
* Get one at: https://polygon.io (free tier available)
*/
export class PolygonProvider implements MarketDataProvider {
readonly name = "Polygon.io";
private apiKey: string;
private baseUrl = "https://api.polygon.io";
constructor(apiKey?: string) {
this.apiKey = apiKey || process.env.POLYGON_API_KEY || "";
if (!this.apiKey) {
throw new Error(
"Polygon API key required. Set POLYGON_API_KEY environment variable or pass to constructor."
);
}
}
private async fetchJson<T>(url: string): Promise<T> {
const separator = url.includes("?") ? "&" : "?";
const fullUrl = `${url}${separator}apiKey=${this.apiKey}`;
const response = await fetch(fullUrl, {
headers: {
"User-Agent": "QuantCompanion/1.0",
},
});
if (!response.ok) {
const errorText = await response.text().catch(() => "Unknown error");
if (response.status === 403) {
throw new Error(`Polygon API access denied. Check your API key and subscription tier.`);
}
if (response.status === 429) {
throw new Error(`Polygon API rate limit exceeded. Please wait and try again.`);
}
throw new Error(`Polygon API error (${response.status}): ${errorText}`);
}
return response.json() as Promise<T>;
}
async getQuote(symbol: string): Promise<Quote> {
const upperSymbol = symbol.toUpperCase();
// Use ticker snapshot endpoint for real-time quote
const data = await this.fetchJson<{
status?: string;
ticker?: PolygonTickerSnapshot;
}>(`${this.baseUrl}/v2/snapshot/locale/us/markets/stocks/tickers/${upperSymbol}`);
if (!data.ticker) {
throw new Error(`No data found for symbol ${upperSymbol}`);
}
const snapshot = data.ticker;
// Prefer last trade price, fall back to day close, then prev day close
const price =
snapshot.lastTrade?.p ??
snapshot.day?.c ??
snapshot.prevDay?.c;
if (price === undefined) {
throw new Error(`No price data available for ${upperSymbol}`);
}
return {
symbol: upperSymbol,
price,
timestamp: snapshot.lastTrade?.t ?? Date.now(),
};
}
async getHistoricalOHLCV(params: {
symbol: string;
start: Date;
end: Date;
interval: "1d" | "1h" | "5m";
}): Promise<OHLCV[]> {
const { symbol, start, end, interval } = params;
const upperSymbol = symbol.toUpperCase();
// Map interval to Polygon timespan/multiplier
const timespanMap: Record<string, { multiplier: number; timespan: string }> = {
"1d": { multiplier: 1, timespan: "day" },
"1h": { multiplier: 1, timespan: "hour" },
"5m": { multiplier: 5, timespan: "minute" },
};
const { multiplier, timespan } = timespanMap[interval];
const startStr = start.toISOString().split("T")[0];
const endStr = end.toISOString().split("T")[0];
// Polygon aggregates endpoint
const url = `${this.baseUrl}/v2/aggs/ticker/${upperSymbol}/range/${multiplier}/${timespan}/${startStr}/${endStr}?adjusted=true&sort=asc&limit=50000`;
const data = await this.fetchJson<{
status?: string;
resultsCount?: number;
results?: PolygonAggBar[];
}>(url);
if (!data.results || data.results.length === 0) {
throw new Error(`No historical data found for ${upperSymbol} in the specified period`);
}
return data.results.map((bar) => ({
timestamp: bar.t,
open: bar.o,
high: bar.h,
low: bar.l,
close: bar.c,
volume: bar.v,
}));
}
async getOptionsChain(params: OptionsChainParams): Promise<OptionsChain> {
const { symbol, expiration } = params;
const upperSymbol = symbol.toUpperCase();
// First get underlying price
const quote = await this.getQuote(upperSymbol);
const underlyingPrice = quote.price;
// Build options chain URL
// Reference endpoint for all contracts
let contractsUrl = `${this.baseUrl}/v3/reference/options/contracts?underlying_ticker=${upperSymbol}&limit=1000`;
if (expiration) {
contractsUrl += `&expiration_date=${expiration}`;
} else {
// Get near-term expirations (next 60 days)
const futureDate = new Date();
futureDate.setDate(futureDate.getDate() + 60);
contractsUrl += `&expiration_date.lte=${futureDate.toISOString().split("T")[0]}`;
}
// Fetch contracts
const contractsData = await this.fetchJson<{
status?: string;
results?: PolygonOptionContract[];
next_url?: string;
}>(contractsUrl);
if (!contractsData.results || contractsData.results.length === 0) {
throw new Error(`No options contracts found for ${upperSymbol}`);
}
// Build set of expirations
const expirationSet = new Set<string>();
const now = new Date();
// Now fetch snapshot data for these contracts (includes IV, Greeks, quotes)
const snapshotUrl = `${this.baseUrl}/v3/snapshot/options/${upperSymbol}?limit=250`;
const snapshotData = await this.fetchJson<{
status?: string;
results?: Array<{
details?: PolygonOptionContract;
day?: { c?: number; o?: number; h?: number; l?: number; v?: number };
last_quote?: { bid?: number; ask?: number };
open_interest?: number;
implied_volatility?: number;
greeks?: { delta?: number; gamma?: number; theta?: number; vega?: number };
}>;
}>(snapshotUrl);
const contracts: OptionContract[] = [];
// Process snapshot data if available
if (snapshotData.results && snapshotData.results.length > 0) {
for (const opt of snapshotData.results) {
const details = opt.details;
if (!details) continue;
const expDate = details.expiration_date;
if (!expDate) continue;
// Filter by requested expiration if specified
if (expiration && expDate !== expiration) continue;
expirationSet.add(expDate);
const strike = details.strike_price ?? 0;
const right = details.contract_type === "put" ? "put" : "call";
const optionExpDate = new Date(expDate);
const timeToMaturityYears = Math.max(
0,
(optionExpDate.getTime() - now.getTime()) / (365.25 * 24 * 60 * 60 * 1000)
);
const bid = opt.last_quote?.bid ?? 0;
const ask = opt.last_quote?.ask ?? 0;
const mid = (bid + ask) / 2;
contracts.push({
underlying: upperSymbol,
optionSymbol: details.ticker || "",
right,
strike,
expiration: expDate,
timeToMaturityYears,
lastPrice: opt.day?.c ?? mid,
bid,
ask,
mid,
volume: opt.day?.v ?? 0,
openInterest: opt.open_interest ?? 0,
impliedVol: opt.implied_volatility,
});
}
}
// If snapshot didn't return enough data, supplement from contracts reference
if (contracts.length === 0) {
// Fallback: build contracts from reference data (without live quotes)
for (const contract of contractsData.results) {
const expDate = contract.expiration_date;
if (!expDate) continue;
if (expiration && expDate !== expiration) continue;
expirationSet.add(expDate);
const strike = contract.strike_price ?? 0;
const right = contract.contract_type === "put" ? "put" : "call";
const optionExpDate = new Date(expDate);
const timeToMaturityYears = Math.max(
0,
(optionExpDate.getTime() - now.getTime()) / (365.25 * 24 * 60 * 60 * 1000)
);
contracts.push({
underlying: upperSymbol,
optionSymbol: contract.ticker || "",
right,
strike,
expiration: expDate,
timeToMaturityYears,
lastPrice: 0,
bid: 0,
ask: 0,
mid: 0,
volume: 0,
openInterest: 0,
impliedVol: undefined,
});
}
}
// Sort expirations
const expirations = Array.from(expirationSet).sort();
return {
symbol: upperSymbol,
asOf: new Date().toISOString(),
underlyingPrice,
expirations,
contracts,
};
}
/**
* Get risk-free rate curve (simplified flat rate)
*/
async getRateCurve(): Promise<RatePoint[]> {
const flatRate = 0.045;
return [
{ maturityYears: 0.25, rate: flatRate },
{ maturityYears: 0.5, rate: flatRate },
{ maturityYears: 1, rate: flatRate },
{ maturityYears: 2, rate: flatRate },
{ maturityYears: 5, rate: flatRate },
];
}
/**
* Get dividend yield for a symbol
*/
async getDividendYield(_symbol: string): Promise<number> {
return 0;
}
}
/**
* Fallback provider that cascades through multiple providers
*
* Tries providers in order, falling back to next on:
* - Rate limit errors (429)
* - API errors
* - Data not found
*
* This gives you Polygon quality when available, with Yahoo as unlimited backup.
*/
export class FallbackProvider implements MarketDataProvider {
readonly name: string;
private providers: MarketDataProvider[];
private lastUsed: Map<string, number> = new Map(); // Track which provider worked last
constructor(providers: MarketDataProvider[]) {
if (providers.length === 0) {
throw new Error("FallbackProvider requires at least one provider");
}
this.providers = providers;
this.name = `Fallback(${providers.map(p => p.name).join(" → ")})`;
}
private async tryProviders<T>(
operation: string,
fn: (provider: MarketDataProvider) => Promise<T>
): Promise<T> {
const errors: string[] = [];
for (let i = 0; i < this.providers.length; i++) {
const provider = this.providers[i];
try {
const result = await fn(provider);
// Track successful provider for logging
this.lastUsed.set(operation, i);
return result;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
errors.push(`[${provider.name}] ${message}`);
// Log fallback (but not for last provider)
if (i < this.providers.length - 1) {
console.error(`[FallbackProvider] ${provider.name} failed for ${operation}, trying ${this.providers[i + 1].name}...`);
}
}
}
// All providers failed
throw new Error(`All providers failed:\n${errors.join("\n")}`);
}
async getQuote(symbol: string): Promise<Quote> {
return this.tryProviders(`getQuote(${symbol})`, (p) => p.getQuote(symbol));
}
async getHistoricalOHLCV(params: {
symbol: string;
start: Date;
end: Date;
interval: "1d" | "1h" | "5m";
}): Promise<OHLCV[]> {
return this.tryProviders(
`getHistoricalOHLCV(${params.symbol})`,
(p) => p.getHistoricalOHLCV(params)
);
}
async getOptionsChain(params: OptionsChainParams): Promise<OptionsChain> {
return this.tryProviders(
`getOptionsChain(${params.symbol})`,
(p) => p.getOptionsChain(params)
);
}
async getRateCurve(): Promise<RatePoint[]> {
// Try first provider that implements it
for (const provider of this.providers) {
if (provider.getRateCurve) {
try {
return await provider.getRateCurve();
} catch {
continue;
}
}
}
// Default flat rate
return [{ maturityYears: 1, rate: 0.045 }];
}
async getDividendYield(symbol: string): Promise<number> {
// Try first provider that implements it
for (const provider of this.providers) {
if (provider.getDividendYield) {
try {
return await provider.getDividendYield(symbol);
} catch {
continue;
}
}
}
return 0;
}
}
// Default provider instance
let defaultProvider: MarketDataProvider = new YahooFinanceProvider();
export function setDefaultProvider(provider: MarketDataProvider): void {
defaultProvider = provider;
}
export function getDefaultProvider(): MarketDataProvider {
return defaultProvider;
}
/**
* Create the recommended provider setup:
* - If POLYGON_API_KEY is set: Polygon → Yahoo fallback
* - Otherwise: Yahoo only
*/
export function createDefaultProvider(): MarketDataProvider {
const polygonKey = process.env.POLYGON_API_KEY;
if (polygonKey) {
return new FallbackProvider([
new PolygonProvider(polygonKey),
new YahooFinanceProvider(),
]);
}
return new YahooFinanceProvider();
}