been running synchronous data fetching since january.
works but slow during market open.
refactored to async this week.
3x speed improvement.
the problem with sync code #
# Old synchronous approach
def fetch_market_data(symbols):
results = []
for symbol in symbols:
data = fetch_from_api(symbol) # Blocks here
results.append(data)
return results
# With 10 symbols, takes 10 * 180ms = 1,800ms total
each API call blocks until complete.
10 symbols = 1.8 seconds waiting.
during volatile markets, this is too slow.
async solution #
import asyncio
import aiohttp
from typing import List, Dict, Any
import time
class AsyncMarketData:
"""Async market data fetcher with connection pooling"""
def __init__(self, api_key: str, max_concurrent: int = 10):
self.api_key = api_key
self.base_url = "https://api.polygon.io"
self.semaphore = asyncio.Semaphore(max_concurrent)
self.session = None
async def __aenter__(self):
"""Context manager entry"""
connector = aiohttp.TCPConnector(
limit=100, # Max connections
ttl_dns_cache=300 # DNS cache TTL
)
self.session = aiohttp.ClientSession(
connector=connector,
timeout=aiohttp.ClientTimeout(total=30)
)
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
"""Context manager exit"""
if self.session:
await self.session.close()
async def fetch_quote(self, symbol: str) -> Dict[str, Any]:
"""Fetch single quote with rate limiting"""
async with self.semaphore: # Rate limit concurrent requests
url = f"{self.base_url}/v2/last/trade/{symbol}"
params = {"apiKey": self.api_key}
try:
async with self.session.get(url, params=params) as resp:
if resp.status == 200:
data = await resp.json()
return {
'symbol': symbol,
'price': data['results']['p'],
'size': data['results']['s'],
'timestamp': data['results']['t'],
'success': True
}
else:
return {
'symbol': symbol,
'success': False,
'error': f"HTTP {resp.status}"
}
except Exception as e:
return {
'symbol': symbol,
'success': False,
'error': str(e)
}
async def fetch_multiple(self, symbols: List[str]) -> List[Dict[str, Any]]:
"""Fetch multiple quotes concurrently"""
tasks = [self.fetch_quote(symbol) for symbol in symbols]
results = await asyncio.gather(*tasks, return_exceptions=True)
# Handle any exceptions
clean_results = []
for result in results:
if isinstance(result, Exception):
clean_results.append({
'success': False,
'error': str(result)
})
else:
clean_results.append(result)
return clean_results
class StrategyDataFeed:
"""High-level interface for strategy data needs"""
def __init__(self, api_key: str):
self.api_key = api_key
self.cache = {} # Simple in-memory cache
async def get_current_prices(self, symbols: List[str]) -> Dict[str, float]:
"""Get current prices for list of symbols"""
# Check cache first (1 second TTL)
now = time.time()
cached = {}
to_fetch = []
for symbol in symbols:
if symbol in self.cache:
cached_time, cached_price = self.cache[symbol]
if now - cached_time < 1.0: # Cache valid
cached[symbol] = cached_price
continue
to_fetch.append(symbol)
# Fetch uncached symbols
if to_fetch:
async with AsyncMarketData(self.api_key) as fetcher:
results = await fetcher.fetch_multiple(to_fetch)
for result in results:
if result['success']:
symbol = result['symbol']
price = result['price']
cached[symbol] = price
self.cache[symbol] = (now, price)
return cached
# Usage in strategy
class TradingStrategy:
def __init__(self):
self.data_feed = StrategyDataFeed(
api_key=os.getenv("POLYGON_API_KEY")
)
async def evaluate_signals(self):
"""Main strategy loop"""
symbols = ['SPY', 'QQQ', 'IWM', 'TLT', 'GLD',
'VXX', 'XLF', 'XLE', 'XLK', 'XLV']
# Fetch all prices concurrently
start = time.time()
prices = await self.data_feed.get_current_prices(symbols)
elapsed = time.time() - start
print(f"Fetched {len(prices)} prices in {elapsed*1000:.1f}ms")
# Strategy logic here
for symbol, price in prices.items():
signal = self.calculate_signal(symbol, price)
if signal:
await self.execute_trade(symbol, signal)
# Main event loop
async def main():
strategy = TradingStrategy()
while True:
await strategy.evaluate_signals()
await asyncio.sleep(1) # Run every second
if __name__ == "__main__":
asyncio.run(main())
performance comparison #
synchronous (old):
- 10 symbols
- sequential fetching
- total time: 1,800ms
- throughput: 5.5 symbols/sec
async (new):
- 10 symbols
- concurrent fetching (max 10 concurrent)
- total time: 220ms (fastest API response)
- throughput: 45 symbols/sec
improvement: 8x faster in ideal conditions, 3x average
real-world testing #
ran both approaches during tuesday market hours.
sync approach:
- avg: 1,650ms for 10 symbols
- worst: 3,200ms (API slowness)
- missed 2 entry signals (too slow)
async approach:
- avg: 580ms for 10 symbols
- worst: 1,100ms (API slowness)
- caught all entry signals
key async patterns used #
1. connection pooling
reuse TCP connections instead of creating new ones.
saves connection setup overhead (100-200ms per connection).
2. rate limiting with semaphore
self.semaphore = asyncio.Semaphore(10) # Max 10 concurrent
prevents overwhelming API (polygon limits to 5/sec on my plan).
3. timeout handling
timeout=aiohttp.ClientTimeout(total=30)
prevents hanging on slow API responses.
4. graceful degradation
return_exceptions=True # Don't fail entire batch if one symbol fails
integration with existing code #
strategy code changes:
# Old sync
prices = self.data_feed.get_prices(symbols)
# New async
prices = await self.data_feed.get_current_prices(symbols)
minimal changes. mostly just adding async/await keywords.
redis caching layer integration #
async works with existing redis cache:
# Check redis first (0.3ms)
# Then async fetch if cache miss (220ms)
# Then cache result for next request
combined: 0.3ms cached, 220ms uncached.
vs old: 180ms always (no async benefit), 0.3ms cached (same).
improvement mainly for cache misses.
lessons learned #
1. async isn’t always faster
for single requests, overhead not worth it.
for multiple concurrent requests, huge win.
2. connection pooling matters
reusing connections saves 100-200ms per request.
3. rate limiting essential
without semaphore, got 429 errors from API.
4. error handling critical
one failed request shouldn’t kill entire batch.
next optimizations #
already planning:
- websocket feeds for real-time data (instead of polling)
- local market data cache with timescaleDB
- predictive prefetching based on strategy patterns
impact on trading #
faster data = faster decisions = better fills.
tuesday caught entry signal 800ms faster than old system.
got filled $0.15 better per contract = $15 profit difference.
over 100 trades, that’s $1,500 additional profit from speed alone.
infrastructure details #
running on:
- python 3.11 (native async improvements)
- aiohttp for async HTTP
- asyncio event loop
- dell server (8 cores help with async)
monitoring:
- grafana tracks fetch latency
- alerts if p95 > 1000ms
- dashboard shows cache hit rate
2:08pm thursday. refactored to async. 3x faster market data. better fills. code is cleaner too.
-AK