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%
11.4 KB · 311 lines typescript
Raw Blame History
1/*2 * =============================================================================3 *  VibeQuant (vquant) — AI-Powered Financial Intelligence Platform4 * -----------------------------------------------------------------------------5 *  File:      server/services/financialDataService.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 FINANCIAL_DATA_API_KEY = process.env.FINANCIAL_DATA_API_KEY || 'a44ec329c25a58c0bff4947cb649eec3';20const FINANCIAL_DATA_BASE_URL = 'https://financialdata.net/api/v1';2122// Option Chain Interface23export interface OptionContract {24  identifier: string;25  option_type: string; // 'call' or 'put'26  strike: number;27  expiration_date: string;28  last_price?: number;29  bid?: number;30  ask?: number;31  volume?: number;32  open_interest?: number;33  implied_volatility?: number;34}3536export interface OptionChain {37  symbol: string;38  options: OptionContract[];39  timestamp?: string;40}4142// Option Prices Interface43export interface OptionPrice {44  identifier: string;45  symbol: string;46  option_type: string;47  strike: number;48  expiration_date: string;49  last_price: number;50  bid: number;51  ask: number;52  bid_size?: number;53  ask_size?: number;54  volume: number;55  open_interest: number;56  implied_volatility: number;57  in_the_money: boolean;58  intrinsic_value: number;59  time_value: number;60  last_trade_date?: string;61  timestamp?: string;62}6364// Option Greeks Interface65export interface OptionGreeks {66  identifier: string;67  symbol: string;68  option_type: string;69  strike: number;70  expiration_date: string;71  delta: number;72  gamma: number;73  theta: number;74  vega: number;75  rho: number;76  implied_volatility: number;77  theoretical_price?: number;78  timestamp?: string;79}8081/**82 * Generic request function for Financial Data API83 */84async function financialDataRequest<T>(endpoint: string, params: Record<string, any> = {}): Promise<T> {85  if (!FINANCIAL_DATA_API_KEY) {86    throw new Error('FINANCIAL_DATA_API_KEY not configured');87  }8889  try {90    console.log(`[Financial Data API] Requesting ${endpoint} with params:`, JSON.stringify(params));9192    const response = await axios.get(`${FINANCIAL_DATA_BASE_URL}${endpoint}`, {93      params: {94        ...params,95        key: FINANCIAL_DATA_API_KEY,96      },97      timeout: 15000,98    });99100    console.log(`[Financial Data API] Response for ${endpoint}:`,101      Array.isArray(response.data) ? `Array with ${response.data.length} items` : typeof response.data102    );103104    return response.data;105  } catch (error) {106    console.error(`[Financial Data API] ERROR for ${endpoint}:`, error);107    if (axios.isAxiosError(error) && error.response) {108      console.error(`[Financial Data API] Response status: ${error.response.status}`);109      console.error(`[Financial Data API] Response data:`, JSON.stringify(error.response.data));110      throw new Error(`Financial Data API error: ${error.response.status} - ${error.response.statusText}. Details: ${JSON.stringify(error.response.data)}`);111    }112    throw new Error(`Failed to fetch from Financial Data API: ${(error as Error).message}`);113  }114}115116/**117 * Get option chain for a stock symbol118 * Returns all available option contracts (calls and puts) with their identifiers119 *120 * @param symbol - Stock ticker symbol (e.g., "AAPL", "MSFT")121 * @returns Option chain with all available contracts122 */123export async function getOptionChain(symbol: string): Promise<OptionChain> {124  console.log(`[getOptionChain] Fetching option chain for symbol: ${symbol}`);125126  const data = await financialDataRequest<any>('/option-chain', { identifier: symbol });127128  // Map API response to our interface129  // API returns: contract_name, put_or_call, strike_price, expiration_date130  const rawOptions = data.options || data;131132  console.log(`[getOptionChain] Processing ${rawOptions.length} option contracts`);133134  const options = rawOptions.map((opt: any, index: number) => {135    // Log first 3 identifiers to understand the format136    if (index < 3) {137      console.log(`[getOptionChain] Sample identifier #${index + 1}:`, opt.contract_name);138    }139140    return {141      identifier: opt.contract_name, // Map contract_name to identifier142      option_type: opt.put_or_call?.toLowerCase() || 'call', // Convert "Call"/"Put" to "call"/"put"143      strike: opt.strike_price || 0,144      expiration_date: opt.expiration_date || '',145      // Optional fields that may not be in the chain response146      last_price: opt.last_price,147      bid: opt.bid,148      ask: opt.ask,149      volume: opt.volume,150      open_interest: opt.open_interest,151      implied_volatility: opt.implied_volatility152    };153  });154155  console.log(`[getOptionChain] Successfully mapped ${options.length} option contracts`);156157  return {158    symbol: symbol,159    options: options,160    timestamp: data.timestamp || new Date().toISOString()161  };162}163164/**165 * Get detailed option prices for a specific option contract166 *167 * @param identifier - Option contract identifier (e.g., "MSFT250417C00400000")168 *                     Format: SYMBOL + YYMMDD + C/P + Strike*1000169 * @returns Detailed pricing information for the option170 */171export async function getOptionPrices(identifier: string): Promise<OptionPrice> {172  console.log(`[getOptionPrices] Fetching prices for identifier: ${identifier}`);173174  // Validate identifier format before making request175  const match = identifier.match(/^([A-Z]+)(\d{6})([CP])(\d{8})$/);176  if (!match) {177    console.warn(`[getOptionPrices] Invalid identifier format: ${identifier}`);178    console.warn(`[getOptionPrices] Expected format: SYMBOL + YYMMDD + C/P + Strike*1000 (e.g., "AAPL280121C00005000")`);179  }180181  const data = await financialDataRequest<any>('/option-prices', { identifier });182183  // API returns an array of historical prices, get the most recent one184  let latestData: any;185  if (Array.isArray(data)) {186    console.log(`[getOptionPrices] Received array with ${data.length} items`);187    if (data.length === 0) {188      throw new Error(189        `No price data found for option contract: ${identifier}. ` +190        `This may indicate that the contract identifier is invalid or the option has no recent trading data. ` +191        `Please verify the identifier format and try again.`192      );193    }194    // Sort by date descending and take the most recent195    latestData = data.sort((a: any, b: any) => {196      const dateA = new Date(a.date || 0).getTime();197      const dateB = new Date(b.date || 0).getTime();198      return dateB - dateA; // Descending order (most recent first)199    })[0];200    console.log(`[getOptionPrices] Using most recent data from: ${latestData.date}`);201  } else {202    console.log(`[getOptionPrices] Received single object`);203    latestData = data;204  }205206  // Parse identifier to extract symbol, option_type, strike, and expiration207  // Format: SYMBOL + YYMMDD + C/P + Strike*1000 (e.g., "AAPL280121C00005000")208  // Note: match was already computed above during validation209  const symbol = match ? match[1] : identifier.substring(0, 4);210  const optionType = match ? (match[3] === 'C' ? 'call' : 'put') : 'call';211  const strike = match ? parseInt(match[4]) / 1000 : 0;212  const expirationDate = match ? `20${match[2].substring(0, 2)}-${match[2].substring(2, 4)}-${match[2].substring(4, 6)}` : '';213214  // Calculate derived fields if not present215  const lastPrice = latestData.close || latestData.last_price || 0;216  const bid = latestData.bid || lastPrice * 0.99; // Estimate if not available217  const ask = latestData.ask || lastPrice * 1.01; // Estimate if not available218  const intrinsicValue = optionType === 'call'219    ? Math.max(0, lastPrice - strike)220    : Math.max(0, strike - lastPrice);221  const timeValue = Math.max(0, lastPrice - intrinsicValue);222223  return {224    identifier: latestData.contract_name || identifier,225    symbol: symbol,226    option_type: optionType,227    strike: strike,228    expiration_date: expirationDate,229    last_price: lastPrice,230    bid: bid,231    ask: ask,232    bid_size: latestData.bid_size,233    ask_size: latestData.ask_size,234    volume: latestData.volume || 0,235    open_interest: latestData.open_interest || 0,236    implied_volatility: latestData.implied_volatility || 0,237    in_the_money: (optionType === 'call' && lastPrice > strike) || (optionType === 'put' && lastPrice < strike),238    intrinsic_value: intrinsicValue,239    time_value: timeValue,240    last_trade_date: latestData.date,241    timestamp: latestData.timestamp || new Date().toISOString()242  };243}244245/**246 * Get option Greeks for a specific option contract247 * Greeks measure different risk sensitivities of options248 *249 * @param identifier - Option contract identifier (e.g., "MSFT250417C00400000")250 * @returns Greek values (Delta, Gamma, Theta, Vega, Rho) for the option251 */252export async function getOptionGreeks(identifier: string): Promise<OptionGreeks> {253  console.log(`[getOptionGreeks] Fetching Greeks for identifier: ${identifier}`);254255  // Validate identifier format before making request256  const match = identifier.match(/^([A-Z]+)(\d{6})([CP])(\d{8})$/);257  if (!match) {258    console.warn(`[getOptionGreeks] Invalid identifier format: ${identifier}`);259    console.warn(`[getOptionGreeks] Expected format: SYMBOL + YYMMDD + C/P + Strike*1000 (e.g., "AAPL280121C00005000")`);260  }261262  const data = await financialDataRequest<any>('/option-greeks', { identifier });263264  // API returns an array of historical Greeks, get the most recent one265  let latestData: any;266  if (Array.isArray(data)) {267    console.log(`[getOptionGreeks] Received array with ${data.length} items`);268    if (data.length === 0) {269      throw new Error(270        `No Greeks data found for option contract: ${identifier}. ` +271        `This may indicate that the contract identifier is invalid or the option has no recent trading data. ` +272        `Please verify the identifier format and try again.`273      );274    }275    // Sort by date descending and take the most recent276    latestData = data.sort((a: any, b: any) => {277      const dateA = new Date(a.date || 0).getTime();278      const dateB = new Date(b.date || 0).getTime();279      return dateB - dateA; // Descending order (most recent first)280    })[0];281    console.log(`[getOptionGreeks] Using most recent data from: ${latestData.date}`);282  } else {283    console.log(`[getOptionGreeks] Received single object`);284    latestData = data;285  }286287  // Parse identifier to extract symbol, option_type, strike, and expiration288  // Format: SYMBOL + YYMMDD + C/P + Strike*1000 (e.g., "AAPL280121C00005000")289  // Note: match was already computed above during validation290  const symbol = match ? match[1] : identifier.substring(0, 4);291  const optionType = match ? (match[3] === 'C' ? 'call' : 'put') : 'call';292  const strike = match ? parseInt(match[4]) / 1000 : 0;293  const expirationDate = match ? `20${match[2].substring(0, 2)}-${match[2].substring(2, 4)}-${match[2].substring(4, 6)}` : '';294295  return {296    identifier: latestData.contract_name || identifier,297    symbol: symbol,298    option_type: optionType,299    strike: strike,300    expiration_date: expirationDate,301    delta: latestData.delta || 0,302    gamma: latestData.gamma || 0,303    theta: latestData.theta || 0,304    vega: latestData.vega || 0,305    rho: latestData.rho || 0,306    implied_volatility: latestData.implied_volatility || 0,307    theoretical_price: latestData.theoretical_price,308    timestamp: latestData.date || new Date().toISOString()309  };310}311