Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.secapi.ai/llms.txt

Use this file to discover all available pages before exploring further.

Build an Earnings Preview Agent

Before a company reports earnings, analysts need a quick-hit research brief covering the company’s fundamentals, factor exposures, and recent insider activity. This tutorial builds a Node.js agent that pulls data from multiple OMNI Datastream endpoints and assembles a pre-earnings research brief you can share with your team.

What you will build

  • A Node.js script that generates pre-earnings research briefs
  • Company intelligence summary from the OMNI intelligence bundle
  • Factor decomposition showing what is driving the stock
  • Recent insider transactions leading up to earnings
  • A formatted markdown report ready for distribution

Prerequisites

  • An Omni Datastream API key (set as OMNI_DATASTREAM_API_KEY)
  • Node.js 18+
  • Basic familiarity with the OMNI Datastream API

Step 1 — Set up the project

Create the project and install dependencies.
mkdir -p earnings-preview-agent
cd earnings-preview-agent
npm init -y
Create package.json:
{
  "name": "earnings-preview-agent",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "preview": "node index.js"
  },
  "dependencies": {
    "@omni-datastream/sdk-js": "latest",
    "dotenv": "^16.4.0"
  }
}
Install dependencies:
npm install
Create a .env file:
OMNI_DATASTREAM_API_KEY=your-api-key

Step 2 — Build the data fetchers

Create index.js with functions that pull data from each OMNI Datastream endpoint.
import "dotenv/config";
import { writeFileSync } from "fs";
import { OmniDatastreamClient } from "@omni-datastream/sdk-js";

const client = new OmniDatastreamClient({
  apiKey: process.env.OMNI_DATASTREAM_API_KEY,
});

const ticker = process.argv[2] || "AAPL";

/**
 * Fetch company intelligence bundle — a high-level overview of the company
 * including business description, key metrics, and recent developments.
 */
async function fetchCompanyIntel(ticker) {
  const intel = await client.intelligence.company({ ticker });
  return intel;
}

/**
 * Fetch the earnings preview intelligence bundle — pre-built earnings
 * context including consensus estimates, recent guidance, and key topics.
 */
async function fetchEarningsPreview(ticker) {
  const preview = await client.intelligence.earningsPreview({ ticker });
  return preview;
}

/**
 * Fetch factor decomposition — shows how much of the stock's recent
 * return is explained by systematic factors vs. idiosyncratic moves.
 */
async function fetchFactorDecomposition(ticker) {
  const factors = await client.factors.decomposition({ ticker });
  return factors;
}

/**
 * Fetch recent insider transactions — Form 4 filings showing buys,
 * sells, and option exercises by officers and directors.
 */
async function fetchInsiderActivity(ticker) {
  const insiders = await client.insiders.list({
    ticker,
    limit: 20,
    sort: "filed_at:desc",
  });
  return insiders;
}

/**
 * Fetch upcoming earnings date from the market calendar.
 */
async function fetchEarningsDate(ticker) {
  const calendar = await client.market.earningsCalendar({ ticker });
  return calendar;
}

Step 3 — Build the report generator

Add the logic that assembles all the data into a structured markdown report.
function formatCurrency(value) {
  if (!value && value !== 0) return "N/A";
  if (Math.abs(value) >= 1e12) return `$${(value / 1e12).toFixed(2)}T`;
  if (Math.abs(value) >= 1e9) return `$${(value / 1e9).toFixed(2)}B`;
  if (Math.abs(value) >= 1e6) return `$${(value / 1e6).toFixed(2)}M`;
  return `$${value.toLocaleString()}`;
}

function formatDate(dateStr) {
  if (!dateStr) return "N/A";
  return new Date(dateStr).toLocaleDateString("en-US", {
    year: "numeric",
    month: "long",
    day: "numeric",
  });
}

function generateReport(ticker, intel, preview, factors, insiders, earnings) {
  const lines = [];
  const now = new Date().toISOString().split("T")[0];

  // Header
  lines.push(`# Earnings Preview: ${ticker}`);
  lines.push("");
  lines.push(`**Generated:** ${now}  `);

  if (earnings?.data?.[0]) {
    const next = earnings.data[0];
    lines.push(
      `**Next earnings:** ${formatDate(next.date)} (${next.time || "TBD"})  `
    );
  }
  lines.push("");

  // Company overview
  lines.push("## Company Overview");
  lines.push("");
  if (intel?.data) {
    const d = intel.data;
    lines.push(`**${d.company_name || ticker}**`);
    lines.push("");
    if (d.description) {
      lines.push(d.description.slice(0, 500));
      lines.push("");
    }
    lines.push("| Metric | Value |");
    lines.push("|--------|-------|");
    if (d.market_cap) lines.push(`| Market Cap | ${formatCurrency(d.market_cap)} |`);
    if (d.sector) lines.push(`| Sector | ${d.sector} |`);
    if (d.industry) lines.push(`| Industry | ${d.industry} |`);
    if (d.employees) lines.push(`| Employees | ${d.employees.toLocaleString()} |`);
    lines.push("");
  }

  // Earnings preview context
  lines.push("## Earnings Context");
  lines.push("");
  if (preview?.data) {
    const p = preview.data;
    if (p.consensus_eps) {
      lines.push(`**Consensus EPS:** $${p.consensus_eps}  `);
    }
    if (p.consensus_revenue) {
      lines.push(
        `**Consensus Revenue:** ${formatCurrency(p.consensus_revenue)}  `
      );
    }
    if (p.key_topics && p.key_topics.length > 0) {
      lines.push("");
      lines.push("### Key topics to watch");
      lines.push("");
      for (const topic of p.key_topics) {
        lines.push(`- ${topic}`);
      }
    }
    if (p.recent_guidance) {
      lines.push("");
      lines.push("### Recent guidance");
      lines.push("");
      lines.push(p.recent_guidance);
    }
    lines.push("");
  } else {
    lines.push("_No earnings preview data available._");
    lines.push("");
  }

  // Factor decomposition
  lines.push("## Factor Decomposition");
  lines.push("");
  if (factors?.data) {
    lines.push(
      "Shows how much of the stock's recent return is attributable to systematic factors."
    );
    lines.push("");
    lines.push("| Factor | Exposure | Contribution |");
    lines.push("|--------|----------|-------------|");
    const exposures = factors.data.exposures || factors.data;
    if (Array.isArray(exposures)) {
      for (const f of exposures.slice(0, 10)) {
        const name = f.factor_name || f.factor || "Unknown";
        const exposure = f.exposure?.toFixed(3) ?? "N/A";
        const contribution = f.contribution
          ? `${(f.contribution * 100).toFixed(2)}%`
          : "N/A";
        lines.push(`| ${name} | ${exposure} | ${contribution} |`);
      }
    }
    lines.push("");
  } else {
    lines.push("_No factor decomposition available._");
    lines.push("");
  }

  // Insider activity
  lines.push("## Recent Insider Activity");
  lines.push("");
  if (insiders?.data && insiders.data.length > 0) {
    lines.push("| Date | Name | Title | Type | Shares | Price |");
    lines.push("|------|------|-------|------|--------|-------|");
    for (const txn of insiders.data.slice(0, 15)) {
      const date = txn.filed_at || txn.transaction_date || "N/A";
      const name = txn.reporting_owner || txn.insider_name || "Unknown";
      const title = txn.title || "";
      const type = txn.transaction_type || txn.type || "N/A";
      const shares = txn.shares ? txn.shares.toLocaleString() : "N/A";
      const price = txn.price ? `$${txn.price.toFixed(2)}` : "N/A";
      lines.push(`| ${date} | ${name} | ${title} | ${type} | ${shares} | ${price} |`);
    }
    lines.push("");

    // Insider sentiment summary
    const buys = insiders.data.filter(
      (t) =>
        t.transaction_type === "P" ||
        t.transaction_type === "Purchase"
    ).length;
    const sells = insiders.data.filter(
      (t) =>
        t.transaction_type === "S" ||
        t.transaction_type === "Sale"
    ).length;
    lines.push(
      `**Insider sentiment (last 20 transactions):** ${buys} buys, ${sells} sells`
    );
    lines.push("");
  } else {
    lines.push("_No recent insider activity found._");
    lines.push("");
  }

  // Footer
  lines.push("---");
  lines.push(
    "*Data sourced from SEC EDGAR via OMNI Datastream API. This is not investment advice.*"
  );

  return lines.join("\n");
}

Step 4 — Wire up the main function

Add the entry point that orchestrates all the data fetches and generates the report.
async function main() {
  console.log(`Generating earnings preview for ${ticker}...`);

  // Fetch all data in parallel for speed
  const [intel, preview, factors, insiders, earnings] =
    await Promise.allSettled([
      fetchCompanyIntel(ticker),
      fetchEarningsPreview(ticker),
      fetchFactorDecomposition(ticker),
      fetchInsiderActivity(ticker),
      fetchEarningsDate(ticker),
    ]);

  // Extract values, using null for any that failed
  const getValue = (result) =>
    result.status === "fulfilled" ? result.value : null;

  const report = generateReport(
    ticker,
    getValue(intel),
    getValue(preview),
    getValue(factors),
    getValue(insiders),
    getValue(earnings)
  );

  // Write report
  const filename = `earnings-preview-${ticker.toLowerCase()}-${new Date().toISOString().split("T")[0]}.md`;
  writeFileSync(filename, report);
  console.log(`Report saved to ${filename}`);
  console.log("");
  console.log(report);
}

main().catch((err) => {
  console.error("Error generating earnings preview:", err.message);
  process.exit(1);
});

Step 5 — Run the agent

Generate an earnings preview for any ticker.
# Default: AAPL
npm run preview

# Specify a ticker
node index.js MSFT
node index.js NVDA

Expected output

Generating earnings preview for AAPL...
Report saved to earnings-preview-aapl-2025-04-11.md

# Earnings Preview: AAPL

**Generated:** 2025-04-11
**Next earnings:** May 1, 2025 (AMC)

## Company Overview

**Apple Inc**

Apple Inc. designs, manufactures, and markets smartphones, personal
computers, tablets, wearables, and accessories worldwide...

| Metric | Value |
|--------|-------|
| Market Cap | $3.45T |
| Sector | Technology |
| Industry | Consumer Electronics |
| Employees | 161,000 |

## Earnings Context

**Consensus EPS:** $1.62
**Consensus Revenue:** $94.2B

### Key topics to watch

- Services revenue growth trajectory
- China market headwinds
- AI feature adoption and monetization
- Capital return program updates

## Factor Decomposition

| Factor | Exposure | Contribution |
|--------|----------|-------------|
| Market | 1.102 | 8.24% |
| Momentum | 0.342 | 1.87% |
| Quality | 0.891 | 2.14% |
| Size | -0.234 | -0.42% |

## Recent Insider Activity

| Date | Name | Title | Type | Shares | Price |
|------|------|-------|------|--------|-------|
| 2025-04-01 | Tim Cook | CEO | Sale | 200,000 | $178.23 |
| 2025-03-15 | Luca Maestri | SVP, CFO | Sale | 50,000 | $171.45 |

**Insider sentiment (last 20 transactions):** 0 buys, 8 sells

Step 6 — Generate batch previews

Add support for generating previews for all companies reporting in a given week.
async function batchPreview() {
  // Fetch the earnings calendar for the next 7 days
  const calendar = await client.market.earningsCalendar({
    start_date: new Date().toISOString().split("T")[0],
    end_date: new Date(Date.now() + 7 * 86400000).toISOString().split("T")[0],
  });

  const tickers = (calendar.data || []).map((e) => e.ticker).slice(0, 10);
  console.log(`Generating previews for ${tickers.length} companies...`);

  for (const t of tickers) {
    try {
      // Re-run main logic for each ticker
      const [intel, preview, factors, insiders, earnings] =
        await Promise.allSettled([
          fetchCompanyIntel(t),
          fetchEarningsPreview(t),
          fetchFactorDecomposition(t),
          fetchInsiderActivity(t),
          fetchEarningsDate(t),
        ]);

      const getValue = (result) =>
        result.status === "fulfilled" ? result.value : null;

      const report = generateReport(
        t,
        getValue(intel),
        getValue(preview),
        getValue(factors),
        getValue(insiders),
        getValue(earnings)
      );

      const filename = `earnings-preview-${t.toLowerCase()}-${new Date().toISOString().split("T")[0]}.md`;
      writeFileSync(filename, report);
      console.log(`  ${t}: saved to ${filename}`);
    } catch (err) {
      console.error(`  ${t}: failed — ${err.message}`);
    }
  }
}

// Run batch mode with --batch flag
if (process.argv.includes("--batch")) {
  batchPreview().catch(console.error);
}

Step 7 — Extend with custom queries

Use the OMNI intelligence query endpoint to add custom research questions to the brief.
async function askQuestion(ticker, question) {
  const result = await client.intelligence.query({
    ticker,
    question,
  });
  return result?.data?.answer || null;
}

// Example: add to the report generation
const customQuestions = [
  "What were the key takeaways from the last earnings call?",
  "What are the biggest risks heading into this quarter?",
  "How has management guidance changed over the last two quarters?",
];

for (const q of customQuestions) {
  const answer = await askQuestion(ticker, q);
  if (answer) {
    lines.push(`### ${q}`);
    lines.push("");
    lines.push(answer);
    lines.push("");
  }
}

Next steps

  • Add technical indicators: Pull price bars from /v1/market/bars and compute moving averages, RSI, or other signals.
  • Include peer comparison: Use /v1/intelligence/security for multiple tickers to build a peer comparison table.
  • Automate distribution: Pipe the markdown report to a Slack channel, email, or Notion page.
  • Schedule before earnings: Use the earnings calendar to automatically trigger preview generation 3 days before each report date.
See the JavaScript SDK documentation for the full list of available methods.