Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Real-time and historical stock and forex market data via Twelve Data API: quotes, OHLCV time series, and symbol search.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
tools/client.py
1"""2Twelve Data API Client — Async HTTP client for stocks and forex market data.34Supports stocks and forex (FX) data via REST API.5Configured for Pro subscription tier endpoints only.67Environment Variables:8- TWELVEDATA_API_KEY: Twelve Data API key (required, get from twelvedata.com)910Supported Endpoints (Pro Tier):11- Time series data (price, quote, EOD, historical OHLCV)12- Reference data (search, stocks list, forex pairs, exchanges)13- Batch requests (multiple symbols)1415Not Included (Requires Grow/Pro+/Ultra/Enterprise):16- Fundamental data (financials, statistics, earnings)17- Executive data (key executives, compensation)1819API Documentation: https://twelvedata.com/docs20"""2122import logging23import os24from typing import Any, Dict, Optional, List2526import aiohttp2728from core.http_client import get_aiohttp_proxy_kwargs29from core.tool import ToolResult3031logger = logging.getLogger(__name__)3233# ── Singleton client ──────────────────────────────────────────────34_client: Optional["TwelveDataClient"] = None353637def get_client() -> "TwelveDataClient":38"""Return a shared TwelveDataClient singleton."""39global _client40if _client is None:41_client = TwelveDataClient()42return _client434445def handle_api_error(e: Exception) -> ToolResult:46"""Unified error handler for all Twelve Data tool execute() methods."""47error_str = str(e)48if "401" in error_str:49return ToolResult(50success=False,51error="API key error. The TWELVEDATA_API_KEY may be invalid or missing.",52)53if "429" in error_str:54return ToolResult(55success=False,56error="Rate limit exceeded. Please wait before making more requests.",57)58return ToolResult(success=False, error=f"Twelve Data API error: {error_str}")5960# API Configuration61BASE_URL = "https://api.twelvedata.com"6263# Supported intervals for time series64INTERVALS = ["1min", "5min", "15min", "30min", "45min", "1h", "2h", "4h", "8h", "1day", "1week", "1month"]656667class TwelveDataClient:68"""69Async Twelve Data client for stocks and forex.7071All methods call the Twelve Data REST API with API key authentication.72Supports both header and query parameter authentication methods.73"""7475def __init__(self, api_key: Optional[str] = None):76self.api_key = api_key or os.environ.get("TWELVEDATA_API_KEY", "")77if not self.api_key:78logger.warning("TWELVEDATA_API_KEY not set — API calls will fail")7980# ── Internal helpers ─────────────────────────────────────────────────8182async def _get(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Any:83"""GET request to Twelve Data API with API key auth."""84url = f"{BASE_URL}/{endpoint}"8586# Add API key to params (can also use header: Authorization: apikey YOUR_KEY)87if params is None:88params = {}89params["apikey"] = self.api_key9091headers = {92"Accept": "application/json",93}9495proxy_kw = get_aiohttp_proxy_kwargs(url)96async with aiohttp.ClientSession() as session:97async with session.get(98url,99headers=headers,100params=params,101timeout=aiohttp.ClientTimeout(total=30),102**proxy_kw,103) as resp:104if resp.status >= 400:105body = await resp.text()106raise Exception(f"Twelve Data API {resp.status}: {body}")107return await resp.json()108109# ── Time Series & Price Data ─────────────────────────────────────────110111async def get_time_series(112self,113symbol: str,114interval: str = "1day",115outputsize: int = 30,116start_date: Optional[str] = None,117end_date: Optional[str] = None,118prepost: bool = False,119) -> dict:120"""121Get historical OHLCV time series data.122123Args:124symbol: Stock symbol (AAPL, MSFT) or forex pair (EUR/USD, GBP/JPY)125interval: Time interval (1min, 5min, 15min, 30min, 1h, 4h, 1day, 1week, 1month)126outputsize: Number of data points to return (1-5000). Default 30.127start_date: Start date in YYYY-MM-DD format (optional)128end_date: End date in YYYY-MM-DD format (optional)129130Returns:131dict with meta and values (OHLCV data)132"""133params = {134"symbol": symbol,135"interval": interval,136"outputsize": outputsize,137}138139if start_date:140params["start_date"] = start_date141if end_date:142params["end_date"] = end_date143if prepost:144params["prepost"] = "true"145146return await self._get("time_series", params)147148async def get_quote(self, symbol: str, prepost: bool = False) -> dict:149"""150Get real-time quote for a stock or forex pair.151152Args:153symbol: Stock symbol (AAPL) or forex pair (EUR/USD)154prepost: Include pre/post-market data when available (US/Cboe Europe, Pro+)155156Returns:157dict with current price, open, high, low, volume, change, etc.158"""159params = {"symbol": symbol}160if prepost:161params["prepost"] = "true"162return await self._get("quote", params)163164async def get_price(self, symbol: str, prepost: bool = False) -> dict:165"""166Get latest trading price.167168Args:169symbol: Stock symbol or forex pair170prepost: Include pre/post-market data when available (US/Cboe Europe, Pro+)171172Returns:173dict with price value174"""175params = {"symbol": symbol}176if prepost:177params["prepost"] = "true"178return await self._get("price", params)179180async def get_eod(self, symbol: str, date: Optional[str] = None, prepost: bool = False) -> dict:181"""182Get end-of-day price.183184Args:185symbol: Stock symbol or forex pair186date: Specific date in YYYY-MM-DD format (optional, defaults to latest)187prepost: Include pre/post-market data when available (US/Cboe Europe, Pro+)188189Returns:190dict with EOD price data191"""192params = {"symbol": symbol}193if date:194params["date"] = date195if prepost:196params["prepost"] = "true"197return await self._get("eod", params)198199# ── Reference Data ───────────────────────────────────────────────────200201async def search_symbol(self, query: str) -> dict:202"""203Search for stocks or forex pairs by name or symbol.204205Args:206query: Search query (company name, stock symbol, or currency pair)207208Returns:209dict with search results array210"""211params = {"symbol": query}212return await self._get("symbol_search", params)213214async def get_stocks(215self,216exchange: Optional[str] = None,217country: Optional[str] = None,218) -> dict:219"""220Get list of available stocks.221222Args:223exchange: Filter by exchange (NASDAQ, NYSE, etc.)224country: Filter by country code (US, GB, etc.)225226Returns:227dict with stocks array228"""229params = {}230if exchange:231params["exchange"] = exchange232if country:233params["country"] = country234return await self._get("stocks", params)235236async def get_forex_pairs(self) -> dict:237"""238Get list of available forex pairs.239240Returns:241dict with forex pairs array242"""243return await self._get("forex_pairs")244245async def get_exchanges(self) -> dict:246"""247Get list of supported exchanges.248249Returns:250dict with exchanges array251"""252return await self._get("exchanges")253254# ── Batch Requests ───────────────────────────────────────────────────255256async def get_quote_batch(self, symbols: List[str], prepost: bool = False) -> dict:257"""258Get quotes for multiple symbols in one request.259260Args:261symbols: List of stock symbols or forex pairs (max 120)262prepost: Include pre/post-market data when available (US/Cboe Europe, Pro+)263264Returns:265dict with quotes for each symbol266"""267if len(symbols) > 120:268logger.warning(f"Maximum 120 symbols per batch request. Truncating from {len(symbols)} to 120.")269symbols = symbols[:120]270271params = {"symbol": ",".join(symbols)}272if prepost:273params["prepost"] = "true"274return await self._get("quote", params)275276async def get_price_batch(self, symbols: List[str], prepost: bool = False) -> dict:277"""278Get prices for multiple symbols in one request.279280Args:281symbols: List of stock symbols or forex pairs (max 120)282prepost: Include pre/post-market data when available (US/Cboe Europe, Pro+)283284Returns:285dict with prices for each symbol286"""287if len(symbols) > 120:288logger.warning(f"Maximum 120 symbols per batch request. Truncating from {len(symbols)} to 120.")289symbols = symbols[:120]290291params = {"symbol": ",".join(symbols)}292if prepost:293params["prepost"] = "true"294return await self._get("price", params)295