Skip to main content

refactored data pipeline to async - 3x faster market data processing

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:

  1. websocket feeds for real-time data (instead of polling)
  2. local market data cache with timescaleDB
  3. 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

Related

added redis caching - cut market data latency by 60%
been noticing market data latency creeping up. average fetch time: 180ms from polygon API. slowing down entry execution. the problem # every time algo needs current price:
rebuilt backtesting pipeline - 10x faster parameter optimization
spent last 3 days rebuilding backtest optimization pipeline. went from 6 hours to 35 minutes for full parameter sweep. the problem # old approach: sequential parameter testing.
using python async for real-time market data
rewrote my market data pipeline to use async. 3x faster, way cleaner code. the problem # old synchronous code:
how i organize my trading code on github
got asked on r/algotrading how i organize my trading repos. here’s my setup after 4 months of refactoring. repo structure # i have 4 main repos:
A. came over - gave her the server rack tour
A. came over sunday afternoon. gave her full tour of my trading setup. she fucking loved it. the setup tour # server rack:
building volatility regime detection
need to stop trading when volatility spikes. building detection system. the problem # this week VIX spiked 18% in 2 days. my strategies got stopped out twice.