Skip to content

Performance Tips

Optimization techniques for faster queries and better resource usage.

Overview

Optimize screener performance through smart filtering, efficient pagination, and proper resource management.

Query Optimization

1. Most Restrictive Filters First

Apply the most restrictive filters early to reduce dataset size:

typescript
// Good: Most restrictive first
screener
  .where(StockField.MARKET_CAPITALIZATION.gt(100e9))  // Very restrictive
  .where(StockField.VOLUME.gte(1_000_000))            // Moderately restrictive
  .where(StockField.PRICE.gt(10));                    // Less restrictive

// Less efficient
screener
  .where(StockField.PRICE.gt(10))                     // Matches many stocks
  .where(StockField.VOLUME.gte(1_000_000))
  .where(StockField.MARKET_CAPITALIZATION.gt(100e9)); // Applied last

2. Use Specific Value Ranges

Narrow ranges reduce result sets:

typescript
// Good: Specific range
screener.where(StockField.MARKET_CAPITALIZATION.between(1e9, 10e9));

// Less efficient: Very broad
screener.where(StockField.MARKET_CAPITALIZATION.gt(0));

3. Combine Filters Wisely

typescript
// Efficient: Combined conditions
screener
  .where(StockField.MARKET_CAPITALIZATION.gt(1e9))
  .where(StockField.VOLUME.gte(100_000))
  .where(StockField.PRICE.between(10, 500));

// Inefficient: Too many overlapping conditions
screener
  .where(StockField.PRICE.gt(0))
  .where(StockField.PRICE.gt(5))
  .where(StockField.PRICE.gt(10))  // Just use this one
  .where(StockField.PRICE.lt(1000))
  .where(StockField.PRICE.lt(500)); // Just use this one

Field Selection

Select Only Needed Fields

typescript
// Good: Select specific fields
screener.select(
  StockField.NAME,
  StockField.PRICE,
  StockField.VOLUME
);

// Inefficient: Select all fields
screener.selectAll(); // Returns 15+ fields

Avoid Unused Fields

typescript
// Good: Only fields you use
screener
  .select(StockField.NAME, StockField.PRICE)
  .sortBy(StockField.PRICE, true);

// Wasteful: Selecting fields not used
screener
  .select(
    StockField.NAME,
    StockField.PRICE,
    StockField.VOLUME,
    StockField.MARKET_CAPITALIZATION,
    StockField.DIVIDEND_YIELD_FWD  // Not used anywhere
  )
  .sortBy(StockField.PRICE, true);

Pagination Strategies

Optimal Page Size

typescript
// Too small: Many requests
screener.setRange(0, 10);  // 10 results per page

// Good: Balanced
screener.setRange(0, 50);  // 50 results
screener.setRange(0, 100); // 100 results

// Large but acceptable
screener.setRange(0, 150); // Near limit

// Avoid: May timeout
screener.setRange(0, 1000); // Too large

Fetch Only What You Need

typescript
// Good: Fetch first page only
screener.setRange(0, 50);
const results = await screener.get();

// Inefficient: Fetch all when only need some
const allResults = await getAllResults(screener); // May take long
const firstPage = allResults.slice(0, 50);

Progressive Loading

typescript
async function* loadProgressively(
  screener: StockScreener,
  pageSize: number = 50
) {
  let page = 0;

  while (true) {
    screener.setRange(page * pageSize, (page + 1) * pageSize);
    const results = await screener.get();

    if (results.data.length === 0) break;

    yield results.data;

    if (results.data.length < pageSize) break;
    page++;
  }
}

// Usage: Load and process incrementally
for await (const page of loadProgressively(screener, 50)) {
  processPage(page); // Process each page as it arrives
}

Caching Strategies

Cache Expensive Queries

typescript
class CachedScreener {
  private cache = new Map<string, { data: any; timestamp: number }>();
  private ttl = 60000; // 1 minute

  async get(screener: StockScreener): Promise<ScreenerResult> {
    const key = this.getCacheKey(screener);
    const cached = this.cache.get(key);

    if (cached && Date.now() - cached.timestamp < this.ttl) {
      console.log('Cache hit');
      return cached.data;
    }

    console.log('Cache miss, fetching...');
    const data = await screener.get();

    this.cache.set(key, {
      data,
      timestamp: Date.now(),
    });

    return data;
  }

  private getCacheKey(screener: StockScreener): string {
    return JSON.stringify({
      filters: screener['filters'],
      range: screener['range'],
      sort: screener['sort'],
    });
  }
}

// Usage
const cached = new CachedScreener();
const results = await cached.get(screener); // Cached for 1 minute

Memoize Field Searches

typescript
const fieldSearchCache = new Map<string, BaseField[]>();

function searchFieldsCached(
  fieldClass: typeof BaseField,
  query: string
): BaseField[] {
  const key = `${fieldClass.name}:${query}`;

  if (fieldSearchCache.has(key)) {
    return fieldSearchCache.get(key)!;
  }

  const results = fieldClass.search(query);
  fieldSearchCache.set(key, results);

  return results;
}

Request Batching

Parallel Requests

typescript
// Good: Parallel requests for independent queries
const [stocks, crypto, forex] = await Promise.all([
  new StockScreener().where(StockField.PRICE.gt(100)).get(),
  new CryptoScreener().where(CryptoField.MARKET_CAP.gt(1e9)).get(),
  new ForexScreener().setRange(0, 20).get(),
]);

// Inefficient: Sequential
const stocks = await new StockScreener().where(StockField.PRICE.gt(100)).get();
const crypto = await new CryptoScreener().where(CryptoField.MARKET_CAP.gt(1e9)).get();
const forex = await new ForexScreener().setRange(0, 20).get();

Batch Page Fetching

typescript
async function fetchPagesBatch(
  screener: StockScreener,
  pages: number[],
  pageSize: number = 50
): Promise<any[][]> {
  const promises = pages.map(page => {
    const s = new StockScreener();
    // Copy filters from original screener
    s.setRange(page * pageSize, (page + 1) * pageSize);
    return s.get().then(r => r.data);
  });

  return await Promise.all(promises);
}

// Fetch pages 0, 1, 2 in parallel
const results = await fetchPagesBatch(screener, [0, 1, 2]);
const allData = results.flat();

Memory Management

Stream Instead of Load All

typescript
// Good: Stream for large datasets
for await (const data of screener.stream({ interval: 5000 })) {
  if (data) {
    processData(data.data); // Process and discard
  }
}

// Inefficient: Load everything into memory
const allData = await getAllResults(screener); // May use GBs of RAM
processData(allData);

Process in Chunks

typescript
async function processInChunks(
  screener: StockScreener,
  chunkSize: number = 100
) {
  let offset = 0;

  while (true) {
    screener.setRange(offset, offset + chunkSize);
    const results = await screener.get();

    if (results.data.length === 0) break;

    // Process chunk
    await processChunk(results.data);

    // Free memory
    results.data.length = 0;

    if (results.data.length < chunkSize) break;
    offset += chunkSize;
  }
}

Limit Result Size

typescript
// Good: Reasonable limits
screener.setRange(0, 150);

// Risky: Very large result sets
screener.setRange(0, 10000); // May cause memory issues

Rate Limiting

Add Delays Between Requests

typescript
async function getAllWithDelay(
  screener: StockScreener,
  delay: number = 1000
): Promise<any[]> {
  const pageSize = 150;
  const allResults: any[] = [];
  let page = 0;

  while (true) {
    screener.setRange(page * pageSize, (page + 1) * pageSize);
    const results = await screener.get();

    allResults.push(...results.data);

    if (results.data.length < pageSize) break;

    page++;

    // Delay between pages
    if (results.data.length === pageSize) {
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }

  return allResults;
}

Throttle Requests

typescript
class Throttler {
  private queue: Array<() => Promise<any>> = [];
  private running = 0;
  private maxConcurrent = 3;

  async add<T>(fn: () => Promise<T>): Promise<T> {
    while (this.running >= this.maxConcurrent) {
      await new Promise(resolve => setTimeout(resolve, 100));
    }

    this.running++;

    try {
      return await fn();
    } finally {
      this.running--;
    }
  }
}

// Usage
const throttler = new Throttler();

const results = await Promise.all([
  throttler.add(() => screener1.get()),
  throttler.add(() => screener2.get()),
  throttler.add(() => screener3.get()),
  throttler.add(() => screener4.get()), // Waits for slot
]);

Network Optimization

Connection Reuse

The HTTP client automatically reuses connections with keep-alive:

typescript
// Built-in: Axios uses connection pooling
// Multiple requests reuse the same TCP connection
const results1 = await screener1.get();
const results2 = await screener2.get(); // Reuses connection

Request Compression

typescript
// If implementing custom HTTP client
import axios from 'axios';

const client = axios.create({
  headers: {
    'Accept-Encoding': 'gzip, deflate, br',
  },
  decompress: true, // Auto decompress
});

Timeout Configuration

typescript
// Set appropriate timeouts
const client = axios.create({
  timeout: 30000, // 30 seconds

  // Retry on timeout
  validateStatus: (status) => status < 500,
});

Filter Optimization

Use Indexed Fields

Some fields are indexed and filter faster:

typescript
// Fast: Market cap is indexed
screener.where(StockField.MARKET_CAPITALIZATION.gt(1e9));

// Fast: Volume is indexed
screener.where(StockField.VOLUME.gte(100_000));

// Fast: Price is indexed
screener.where(StockField.PRICE.between(10, 500));

Avoid Redundant Filters

typescript
// Good: No redundancy
screener
  .where(StockField.MARKET_CAPITALIZATION.gt(1e9))
  .where(StockField.VOLUME.gte(100_000));

// Wasteful: Redundant conditions
screener
  .where(StockField.MARKET_CAPITALIZATION.gt(1e9))
  .where(StockField.MARKET_CAPITALIZATION.gt(500e6)) // Redundant
  .where(StockField.VOLUME.gte(100_000))
  .where(StockField.VOLUME.gt(50_000)); // Redundant

Streaming Performance

Choose Appropriate Intervals

typescript
// Fast updates: Every 5 seconds
screener.stream({ interval: 5000 });

// Balanced: Every 30 seconds
screener.stream({ interval: 30000 });

// Slow but efficient: Every 5 minutes
screener.stream({ interval: 300000 });

// Don't go too fast
screener.stream({ interval: 100 }); // Capped at 1000ms

Limit Stream Duration

typescript
// Good: Set max iterations
screener.stream({ maxIterations: 100 });

// Or: Use timeout
const timeout = setTimeout(() => {
  process.exit(0);
}, 3600000); // 1 hour

for await (const data of screener.stream()) {
  // Process data
}

clearTimeout(timeout);

Monitoring Performance

Measure Query Time

typescript
async function measureQuery(screener: StockScreener): Promise<void> {
  const start = Date.now();

  const results = await screener.get();

  const duration = Date.now() - start;

  console.log(`Query completed in ${duration}ms`);
  console.log(`Results: ${results.totalCount} total, ${results.data.length} fetched`);
  console.log(`Throughput: ${(results.data.length / duration * 1000).toFixed(0)} results/sec`);
}

Track Resource Usage

typescript
async function monitorMemory(screener: StockScreener): Promise<void> {
  const before = process.memoryUsage();

  const results = await screener.get();

  const after = process.memoryUsage();

  console.log('Memory delta:', {
    heapUsed: `${((after.heapUsed - before.heapUsed) / 1024 / 1024).toFixed(2)} MB`,
    external: `${((after.external - before.external) / 1024 / 1024).toFixed(2)} MB`,
  });
}

Best Practices Summary

Query Design

  • ✅ Apply most restrictive filters first
  • ✅ Use specific value ranges
  • ✅ Select only needed fields
  • ❌ Don't use overly broad filters
  • ❌ Don't select all fields unless necessary

Pagination

  • ✅ Use page sizes of 50-150
  • ✅ Fetch progressively
  • ✅ Process and discard chunks
  • ❌ Don't load entire datasets into memory
  • ❌ Don't use tiny page sizes (< 20)

Request Management

  • ✅ Batch parallel independent requests
  • ✅ Cache expensive queries
  • ✅ Add delays between rapid requests
  • ❌ Don't make sequential requests when parallel is possible
  • ❌ Don't fetch same data repeatedly

Memory Management

  • ✅ Stream large datasets
  • ✅ Process in chunks
  • ✅ Clear references when done
  • ❌ Don't accumulate unlimited data
  • ❌ Don't keep unused results in memory

Error Handling

  • ✅ Implement retry with backoff
  • ✅ Handle errors gracefully
  • ✅ Set appropriate timeouts
  • ❌ Don't ignore errors
  • ❌ Don't retry indefinitely

Benchmarking

Compare Query Strategies

typescript
async function benchmark() {
  const iterations = 10;

  // Strategy 1: Restrictive filters
  const start1 = Date.now();
  for (let i = 0; i < iterations; i++) {
    await new StockScreener()
      .where(StockField.MARKET_CAPITALIZATION.gt(10e9))
      .where(StockField.VOLUME.gte(1_000_000))
      .setRange(0, 50)
      .get();
  }
  const time1 = Date.now() - start1;

  // Strategy 2: Broad filters
  const start2 = Date.now();
  for (let i = 0; i < iterations; i++) {
    await new StockScreener()
      .where(StockField.PRICE.gt(0))
      .setRange(0, 50)
      .get();
  }
  const time2 = Date.now() - start2;

  console.log(`Strategy 1: ${time1}ms (${(time1/iterations).toFixed(0)}ms avg)`);
  console.log(`Strategy 2: ${time2}ms (${(time2/iterations).toFixed(0)}ms avg)`);
  console.log(`Improvement: ${((time2-time1)/time2*100).toFixed(1)}%`);
}

Next Steps

Released under the MIT License.