← Monday's Prompts

Automate Financial Analysis 🚀

From manual reports to automated pipeline in 1 day

June 17, 2025
💼 Finance🐍 Python + TypeScript📊 2 → 50+ reports/day

The Problem

On Monday you tested the 3 prompts in ChatGPT. You saw how data ingestion → analysis → formatting works. But here's reality: your analysts spend 4-6 hours per report. At $150/hour, that's $600-900 per report. You need 10+ reports daily. The math doesn't work.

4-6 hours
Per report with manual process
$600-900
Cost per manual report
Max 2-3/day
Reports per analyst capacity

See It Work

Watch the 3 prompts chain together automatically. This is what you'll build today.

The Code

Three levels: start simple, add reliability, then scale to production. Pick where you are.

Level 1: Simple API Calls

Good for: 0-10 reports/day | Setup time: 30 minutes

# Simple API Calls (0-10 reports/day)
import yfinance as yf
import openai
from datetime import datetime, timedelta
import json

def generate_market_report(ticker: str, period_days: int = 90) -> dict:
    """Chain the 3 prompts: ingest → analyze → format"""
    
    # Step 1: Ingest market data
    stock = yf.Ticker(ticker)
    end_date = datetime.now()
    start_date = end_date - timedelta(days=period_days)
    
    # Get price history
    hist = stock.history(start=start_date, end=end_date)
    
    # Get company info
    info = stock.info
    
    # Calculate metrics
    opening_price = hist['Close'].iloc[0]
    closing_price = hist['Close'].iloc[-1]
    price_change = ((closing_price - opening_price) / opening_price) * 100
    avg_volume = hist['Volume'].mean()
    
    market_data = {
        "ticker": ticker,
        "period": f"{period_days} days",
        "price_data": {
            "opening": round(opening_price, 2),
            "closing": round(closing_price, 2),
            "high": round(hist['High'].max(), 2),
            "low": round(hist['Low'].min(), 2),
            "change_percent": round(price_change, 2),
            "avg_volume": int(avg_volume)
        },
        "fundamentals": {
            "pe_ratio": info.get('trailingPE', 'N/A'),
            "market_cap": info.get('marketCap', 'N/A'),
            "dividend_yield": info.get('dividendYield', 0),
            "beta": info.get('beta', 'N/A')
        }
    }
    
    # Step 2: Generate analysis with OpenAI
    analysis_prompt = f"""You are a professional financial analyst. Analyze this market data and provide:
1. Executive summary (150 words)
2. Price trend analysis (bullish/bearish/neutral with reasoning)
3. Volume analysis
4. Risk factors (3 key risks with severity)
5. 30-day outlook with target price

Market Data:
{json.dumps(market_data, indent=2)}

Format as JSON with keys: executive_summary, price_analysis, volume_analysis, risk_factors (array), outlook_30day"""

    response = openai.chat.completions.create(
        model="gpt-4",
        messages=[{"role": "user", "content": analysis_prompt}],
        temperature=0.3
    )
    
    analysis = json.loads(response.choices[0].message.content)
    
    # Step 3: Format report structure
    format_prompt = f"""Create a professional report structure for this analysis.
Include: cover page details, section breakdown with page numbers, formatting specifications.

Analysis:
{json.dumps(analysis, indent=2)}

Format as JSON with keys: report_structure (with cover_page, sections array, formatting)"""

    response = openai.chat.completions.create(
        model="gpt-4",
        messages=[{"role": "user", "content": format_prompt}],
        temperature=0.3
    )
    
    report_format = json.loads(response.choices[0].message.content)
    
    return {
        "market_data": market_data,
        "analysis": analysis,
        "report_format": report_format,
        "generated_at": datetime.now().isoformat()
    }

# Usage
report = generate_market_report("AAPL", period_days=90)
print(f"Report generated: {report['analysis']['executive_summary'][:100]}...")
print(f"Target price: {report['analysis']['outlook_30day'].get('target_price', 'N/A')}")

Level 2: With Error Handling & Caching

Good for: 10-50 reports/day | Setup time: 2 hours

// With Error Handling & Caching (10-50 reports/day)
import Anthropic from '@anthropic-ai/sdk';
import yahooFinance from 'yahoo-finance2';
import Redis from 'ioredis';

interface ReportResult {
  market_data: any;
  analysis: any;
  report_format: any;
  generated_at: string;
  cache_hit: boolean;
}

class MarketReportGenerator {
  private anthropic: Anthropic;
  private redis: Redis;
  private maxRetries = 3;

  constructor() {
    this.anthropic = new Anthropic({
      apiKey: process.env.ANTHROPIC_API_KEY!,
    });
    this.redis = new Redis({
      host: process.env.REDIS_HOST || 'localhost',
      port: parseInt(process.env.REDIS_PORT || '6379'),
    });
  }

  async generateReport(
    ticker: string,
    periodDays: number = 90
  ): Promise<ReportResult> {
    // Check cache first (market data valid for 1 hour)
    const cacheKey = `report:${ticker}:${periodDays}`;
    const cached = await this.redis.get(cacheKey);
    if (cached) {
      return { ...JSON.parse(cached), cache_hit: true };
    }

    // Step 1: Ingest market data with retry
    const marketData = await this.retryWithBackoff(async () => {
      const endDate = new Date();
      const startDate = new Date();
      startDate.setDate(startDate.getDate() - periodDays);

      const quote = await yahooFinance.quote(ticker);
      const historical = await yahooFinance.historical(ticker, {
        period1: startDate,
        period2: endDate,
      });

      if (!historical || historical.length === 0) {
        throw new Error(`No historical data for ${ticker}`);
      }

      const opening = historical[0].close;
      const closing = historical[historical.length - 1].close;
      const priceChange = ((closing - opening) / opening) * 100;
      const avgVolume =
        historical.reduce((sum, day) => sum + (day.volume || 0), 0) /
        historical.length;

      return {
        ticker,
        period: `${periodDays} days`,
        price_data: {
          opening: Math.round(opening * 100) / 100,
          closing: Math.round(closing * 100) / 100,
          high: Math.max(...historical.map((h) => h.high)),
          low: Math.min(...historical.map((h) => h.low)),
          change_percent: Math.round(priceChange * 100) / 100,
          avg_volume: Math.round(avgVolume),
        },
        fundamentals: {
          pe_ratio: quote.trailingPE || 'N/A',
          market_cap: quote.marketCap || 'N/A',
          dividend_yield: quote.dividendYield || 0,
          beta: quote.beta || 'N/A',
        },
      };
    });

    // Step 2: Generate analysis with retry
    const analysis = await this.retryWithBackoff(async () => {
      const prompt = `You are a professional financial analyst. Analyze this market data and provide:
1. Executive summary (150 words)
2. Price trend analysis (bullish/bearish/neutral with reasoning)
3. Volume analysis
4. Risk factors (3 key risks with severity: low/medium/high)
5. 30-day outlook with target price and confidence level

Market Data:
${JSON.stringify(marketData, null, 2)}

Format as JSON with keys: executive_summary, price_analysis (with trend, support_level, resistance_level, momentum), volume_analysis, risk_factors (array of {factor, severity, mitigation}), outlook_30day (with direction, target_price, confidence, rationale)`;

      const response = await this.anthropic.messages.create({
        model: 'claude-3-5-sonnet-20241022',
        max_tokens: 2048,
        messages: [{ role: 'user', content: prompt }],
      });

      const content = response.content[0];
      if (content.type !== 'text') throw new Error('Invalid response');
      return JSON.parse(content.text);
    });

    // Step 3: Format report structure
    const reportFormat = await this.retryWithBackoff(async () => {
      const prompt = `Create a professional report structure for this financial analysis.
Include:
- Cover page (title, subtitle, date, analyst, classification)
- Sections array (section name, page number, content_type, includes array)
- Formatting specs (fonts, colors, branding)

Analysis:
${JSON.stringify(analysis, null, 2)}

Format as JSON with keys: report_structure (with cover_page, sections, formatting)`;

      const response = await this.anthropic.messages.create({
        model: 'claude-3-5-sonnet-20241022',
        max_tokens: 1024,
        messages: [{ role: 'user', content: prompt }],
      });

      const content = response.content[0];
      if (content.type !== 'text') throw new Error('Invalid response');
      return JSON.parse(content.text);
    });

    const result: ReportResult = {
      market_data: marketData,
      analysis,
      report_format: reportFormat,
      generated_at: new Date().toISOString(),
      cache_hit: false,
    };

    // Cache for 1 hour
    await this.redis.setex(cacheKey, 3600, JSON.stringify(result));

    return result;
  }

  private async retryWithBackoff<T>(
    fn: () => Promise<T>,
    retries: number = this.maxRetries
  ): Promise<T> {
    let lastError: Error | null = null;

    for (let attempt = 0; attempt < retries; attempt++) {
      try {
        return await Promise.race([
          fn(),
          new Promise<never>((_, reject) =>
            setTimeout(() => reject(new Error('Timeout')), 60000)
          ),
        ]);
      } catch (error) {
        lastError = error as Error;
        if (attempt < retries - 1) {
          const delay = Math.pow(2, attempt) * 1000;
          await new Promise((resolve) => setTimeout(resolve, delay));
        }
      }
    }

    throw lastError;
  }
}

// Usage
const generator = new MarketReportGenerator();
const report = await generator.generateReport('AAPL', 90);
console.log(`Generated report (cache: ${report.cache_hit})`);
console.log(`Target: ${report.analysis.outlook_30day.target_price}`);

Level 3: Production Pattern with PDF Generation

Good for: 50+ reports/day | Setup time: 1 day

# Production Pattern with PDF Generation (50+ reports/day)
from langgraph.graph import Graph, END
from typing import TypedDict, List
import anthropic
import yfinance as yf
from reportlab.lib.pagesizes import letter
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, Image
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import inch
from reportlab.lib import colors
import matplotlib.pyplot as plt
import io
from datetime import datetime, timedelta
import json

class ReportState(TypedDict):
    ticker: str
    period_days: int
    market_data: dict
    analysis: dict
    report_format: dict
    pdf_path: str
    charts: List[bytes]
    retry_count: int

def ingest_data_node(state: ReportState) -> ReportState:
    """Ingest market data from multiple sources"""
    stock = yf.Ticker(state['ticker'])
    end_date = datetime.now()
    start_date = end_date - timedelta(days=state['period_days'])
    
    hist = stock.history(start=start_date, end=end_date)
    info = stock.info
    
    # Calculate metrics
    opening = hist['Close'].iloc[0]
    closing = hist['Close'].iloc[-1]
    price_change = ((closing - opening) / opening) * 100
    
    # Get sector comparison (simplified)
    sector_tickers = ['MSFT', 'GOOGL', 'META']  # Example tech peers
    sector_data = {}
    for t in sector_tickers:
        try:
            peer = yf.Ticker(t)
            peer_hist = peer.history(start=start_date, end=end_date)
            peer_change = ((peer_hist['Close'].iloc[-1] - peer_hist['Close'].iloc[0]) / peer_hist['Close'].iloc[0]) * 100
            sector_data[t] = round(peer_change, 2)
        except:
            pass
    
    sector_data[state['ticker']] = round(price_change, 2)
    
    state['market_data'] = {
        "ticker": state['ticker'],
        "period": f"{state['period_days']} days",
        "price_data": {
            "opening": round(opening, 2),
            "closing": round(closing, 2),
            "high": round(hist['High'].max(), 2),
            "low": round(hist['Low'].min(), 2),
            "change_percent": round(price_change, 2),
            "avg_volume": int(hist['Volume'].mean())
        },
        "sector_comparison": sector_data,
        "fundamentals": {
            "pe_ratio": info.get('trailingPE', 'N/A'),
            "market_cap": info.get('marketCap', 'N/A'),
            "dividend_yield": info.get('dividendYield', 0),
            "beta": info.get('beta', 'N/A')
        }
    }
    
    # Generate price chart
    fig, ax = plt.subplots(figsize=(10, 6))
    ax.plot(hist.index, hist['Close'], linewidth=2, color='#1E3A8A')
    ax.fill_between(hist.index, hist['Close'], alpha=0.3, color='#1E3A8A')
    ax.set_title(f'{state["ticker"]} Price History', fontsize=16, fontweight='bold')
    ax.set_xlabel('Date', fontsize=12)
    ax.set_ylabel('Price ($)', fontsize=12)
    ax.grid(True, alpha=0.3)
    
    # Save chart to bytes
    img_buffer = io.BytesIO()
    plt.savefig(img_buffer, format='png', dpi=300, bbox_inches='tight')
    img_buffer.seek(0)
    state['charts'] = [img_buffer.getvalue()]
    plt.close()
    
    return state

def analyze_node(state: ReportState) -> ReportState:
    """Generate professional analysis"""
    client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
    
    prompt = f"""You are a professional financial analyst writing for institutional investors.
Analyze this market data and provide:

1. Executive summary (150 words, professional tone)
2. Price analysis:
   - Trend (bullish/bearish/neutral)
   - Support and resistance levels
   - Momentum assessment
3. Volume analysis (what volume patterns indicate)
4. Sector positioning (vs peers)
5. Risk factors (3 key risks with severity and mitigation)
6. 30-day outlook:
   - Direction (positive/negative/neutral)
   - Target price with rationale
   - Confidence level (low/medium/high)

Market Data:
{json.dumps(state['market_data'], indent=2)}

Format as JSON with proper structure. Be specific with numbers and reasoning."""

    message = client.messages.create(
        model="claude-3-5-sonnet-20241022",
        max_tokens=2048,
        messages=[{"role": "user", "content": prompt}]
    )
    
    state['analysis'] = json.loads(message.content[0].text)
    return state

def format_node(state: ReportState) -> ReportState:
    """Generate formatted report structure"""
    client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
    
    prompt = f"""Create a professional financial report structure.
Include:
- Cover page details (title, date, classification)
- Section breakdown (7 sections with page numbers)
- Formatting specifications (fonts, colors, branding)

Analysis:
{json.dumps(state['analysis'], indent=2)}

Format as JSON."""

    message = client.messages.create(
        model="claude-3-5-sonnet-20241022",
        max_tokens=1024,
        messages=[{"role": "user", "content": prompt}]
    )
    
    state['report_format'] = json.loads(message.content[0].text)
    return state

def generate_pdf_node(state: ReportState) -> ReportState:
    """Generate final PDF report"""
    pdf_path = f"reports/{state['ticker']}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf"
    doc = SimpleDocTemplate(pdf_path, pagesize=letter)
    styles = getSampleStyleSheet()
    story = []
    
    # Custom styles
    title_style = ParagraphStyle(
        'CustomTitle',
        parent=styles['Heading1'],
        fontSize=24,
        textColor=colors.HexColor('#1E3A8A'),
        spaceAfter=30,
        alignment=1  # Center
    )
    
    heading_style = ParagraphStyle(
        'CustomHeading',
        parent=styles['Heading2'],
        fontSize=16,
        textColor=colors.HexColor('#1E3A8A'),
        spaceAfter=12
    )
    
    # Cover page
    cover = state['report_format']['report_structure']['cover_page']
    story.append(Spacer(1, 2*inch))
    story.append(Paragraph(cover['title'], title_style))
    story.append(Paragraph(cover['subtitle'], styles['Normal']))
    story.append(Spacer(1, 0.5*inch))
    story.append(Paragraph(f"Date: {cover['date']}", styles['Normal']))
    story.append(Paragraph(f"Classification: {cover['classification']}", styles['Italic']))
    
    # Executive Summary
    story.append(Paragraph("Executive Summary", heading_style))
    story.append(Paragraph(state['analysis']['executive_summary'], styles['Normal']))
    story.append(Spacer(1, 0.3*inch))
    
    # Price chart
    if state['charts']:
        img = Image(io.BytesIO(state['charts'][0]), width=6*inch, height=3.6*inch)
        story.append(img)
        story.append(Spacer(1, 0.3*inch))
    
    # Price Analysis
    story.append(Paragraph("Price Performance Analysis", heading_style))
    price_analysis = state['analysis']['price_analysis']
    story.append(Paragraph(f"Trend: {price_analysis['trend'].upper()}", styles['Normal']))
    story.append(Paragraph(f"Support Level: ${price_analysis['support_level']}", styles['Normal']))
    story.append(Paragraph(f"Resistance Level: ${price_analysis['resistance_level']}", styles['Normal']))
    story.append(Paragraph(price_analysis['momentum'], styles['Normal']))
    story.append(Spacer(1, 0.3*inch))
    
    # Risk Factors table
    story.append(Paragraph("Risk Assessment", heading_style))
    risk_data = [['Risk Factor', 'Severity', 'Mitigation']]
    for risk in state['analysis']['risk_factors']:
        risk_data.append([risk['factor'], risk['severity'], risk['mitigation']])
    
    risk_table = Table(risk_data, colWidths=[2*inch, 1*inch, 3*inch])
    risk_table.setStyle([
        ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#1E3A8A')),
        ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
        ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
        ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
        ('FONTSIZE', (0, 0), (-1, 0), 12),
        ('BOTTOMPADDING', (0, 0), (-1, 0), 12),
        ('BACKGROUND', (0, 1), (-1, -1), colors.beige),
        ('GRID', (0, 0), (-1, -1), 1, colors.black)
    ])
    story.append(risk_table)
    story.append(Spacer(1, 0.3*inch))
    
    # 30-Day Outlook
    story.append(Paragraph("30-Day Outlook", heading_style))
    outlook = state['analysis']['outlook_30day']
    story.append(Paragraph(f"Direction: {outlook['direction'].upper()}", styles['Normal']))
    story.append(Paragraph(f"Target Price: ${outlook['target_price']}", styles['Normal']))
    story.append(Paragraph(f"Confidence: {outlook['confidence'].upper()}", styles['Normal']))
    story.append(Paragraph(f"Rationale: {outlook['rationale']}", styles['Normal']))
    
    # Build PDF
    doc.build(story)
    state['pdf_path'] = pdf_path
    return state

def build_report_graph():
    """Build the report generation workflow"""
    graph = Graph()
    
    # Add nodes
    graph.add_node("ingest", ingest_data_node)
    graph.add_node("analyze", analyze_node)
    graph.add_node("format", format_node)
    graph.add_node("generate_pdf", generate_pdf_node)
    
    # Add edges
    graph.set_entry_point("ingest")
    graph.add_edge("ingest", "analyze")
    graph.add_edge("analyze", "format")
    graph.add_edge("format", "generate_pdf")
    graph.add_edge("generate_pdf", END)
    
    return graph.compile()

# Usage
import os
os.makedirs('reports', exist_ok=True)

report_graph = build_report_graph()

initial_state = {
    "ticker": "AAPL",
    "period_days": 90,
    "market_data": {},
    "analysis": {},
    "report_format": {},
    "pdf_path": "",
    "charts": [],
    "retry_count": 0
}

result = report_graph.invoke(initial_state)
print(f"Report generated: {result['pdf_path']}")
print(f"Target price: ${result['analysis']['outlook_30day']['target_price']}")

When to Level Up

1

Start: Simple API Calls

0-10 reports/day

  • Sequential API calls to Yahoo Finance and OpenAI
  • Basic error logging with print statements
  • Manual retry on API failures
  • JSON output only (no PDF generation)
2

Scale: Add Reliability & Caching

10-50 reports/day

  • Automatic retries with exponential backoff
  • Redis caching for market data (1 hour TTL)
  • Proper error handling and logging (Winston/Pino)
  • Rate limiting to avoid API throttling
  • Timeout protection (60s max per request)
3

Production: Framework & PDF Generation

50-200 reports/day

  • LangGraph workflow orchestration (ingest → analyze → format → PDF)
  • Professional PDF generation with charts and tables
  • Multiple data sources (Yahoo Finance + Alpha Vantage + Bloomberg)
  • Batch processing with queue management
  • Email delivery integration (SendGrid)
4

Enterprise: Multi-Agent System

200+ reports/day

  • Specialized agents (data ingestion, technical analysis, fundamental analysis, risk assessment)
  • Real-time market data streaming (WebSocket connections)
  • Load balancing across multiple LLM providers (Claude → GPT-4 → Gemini fallback)
  • Distributed processing with Kubernetes and message queues (RabbitMQ)
  • Real-time monitoring dashboard (Grafana + Prometheus)
  • Custom branding and white-label PDF templates

Finance-Specific Gotchas

The code examples above work. But finance has unique challenges you need to handle.

Real-Time vs Cached Data

Market data changes every second during trading hours. You can't cache for 24 hours like other industries. Use 15-minute delayed data for free APIs, or pay for real-time if needed. Know when your data was last updated.

from datetime import datetime, timedelta
import pytz

def is_market_open() -> bool:
    """Check if US stock market is currently open"""
    ny_tz = pytz.timezone('America/New_York')
    now = datetime.now(ny_tz)
    
    # Market closed on weekends
    if now.weekday() >= 5:
        return False
    
    # Market hours: 9:30 AM - 4:00 PM ET
    market_open = now.replace(hour=9, minute=30, second=0)
    market_close = now.replace(hour=16, minute=0, second=0)
    
    return market_open <= now <= market_close

def get_cache_ttl() -> int:
    """Dynamic cache TTL based on market status"""
    if is_market_open():
        return 60  # 1 minute during market hours
    else:
        return 3600  # 1 hour after market close

# Use in Redis caching
ttl = get_cache_ttl()
await redis.setex(cache_key, ttl, json.dumps(market_data))

Market Hours Handling

Don't generate reports at 3 AM when markets are closed. Schedule batch processing for after market close (4:30 PM ET). Handle different market hours for international stocks (LSE, TSE, etc).

import cron from 'node-cron';
import { DateTime } from 'luxon';

// Schedule report generation for after US market close
cron.schedule('30 16 * * 1-5', async () => {
  // Runs at 4:30 PM ET, Monday-Friday
  const nyTime = DateTime.now().setZone('America/New_York');
  console.log(`Generating daily reports at ${nyTime.toISO()}`);
  
  const tickers = ['AAPL', 'MSFT', 'GOOGL', 'AMZN'];
  
  for (const ticker of tickers) {
    try {
      await generateReport(ticker, 90);
      console.log(`✓ Generated report for ${ticker}`);
    } catch (error) {
      console.error(`✗ Failed to generate ${ticker}:`, error);
    }
  }
}, {
  timezone: 'America/New_York'
});

// Handle international markets
const MARKET_SCHEDULES = {
  'NYSE': { timezone: 'America/New_York', hours: '09:30-16:00' },
  'LSE': { timezone: 'Europe/London', hours: '08:00-16:30' },
  'TSE': { timezone: 'Asia/Tokyo', hours: '09:00-15:00' }
};

SEC Filing Format Changes

SEC EDGAR filings change format periodically. Your parsing code breaks. Use structured data APIs (Financial Modeling Prep, Polygon.io) instead of parsing HTML. Have fallback logic when APIs fail.

import requests
from typing import Optional

def get_company_financials(ticker: str) -> Optional[dict]:
    """Get financials with fallback chain"""
    
    # Primary: Financial Modeling Prep (structured data)
    try:
        response = requests.get(
            f"https://financialmodelingprep.com/api/v3/income-statement/{ticker}",
            params={'apikey': os.environ['FMP_API_KEY']},
            timeout=10
        )
        if response.status_code == 200:
            return response.json()[0]  # Latest quarter
    except Exception as e:
        print(f"FMP failed: {e}")
    
    # Fallback 1: Alpha Vantage
    try:
        response = requests.get(
            "https://www.alphavantage.co/query",
            params={
                'function': 'INCOME_STATEMENT',
                'symbol': ticker,
                'apikey': os.environ['ALPHA_VANTAGE_KEY']
            },
            timeout=10
        )
        if response.status_code == 200:
            data = response.json()
            return data['quarterlyReports'][0]
    except Exception as e:
        print(f"Alpha Vantage failed: {e}")
    
    # Fallback 2: Yahoo Finance (less detailed)
    try:
        import yfinance as yf
        stock = yf.Ticker(ticker)
        return stock.quarterly_financials.to_dict()
    except Exception as e:
        print(f"Yahoo Finance failed: {e}")
    
    return None  # All sources failed

Multi-Currency Support

International stocks trade in different currencies. Don't compare JPY prices directly to USD. Convert to common currency using real-time FX rates. Show both local currency and USD equivalent.

import axios from 'axios';

interface CurrencyConverter {
  convert(amount: number, from: string, to: string): Promise<number>;
}

class ForexConverter implements CurrencyConverter {
  private ratesCache: Map<string, { rate: number; timestamp: number }> = new Map();
  private cacheTTL = 3600000; // 1 hour

  async convert(amount: number, from: string, to: string): Promise<number> {
    if (from === to) return amount;

    const cacheKey = `${from}_${to}`;
    const cached = this.ratesCache.get(cacheKey);

    if (cached && Date.now() - cached.timestamp < this.cacheTTL) {
      return amount * cached.rate;
    }

    // Get latest FX rate
    const response = await axios.get(
      `https://api.exchangerate-api.com/v4/latest/${from}`
    );

    const rate = response.data.rates[to];
    this.ratesCache.set(cacheKey, { rate, timestamp: Date.now() });

    return amount * rate;
  }
}

// Usage in report
const converter = new ForexConverter();
const localPrice = 15000; // JPY
const usdPrice = await converter.convert(localPrice, 'JPY', 'USD');

console.log(`Price: ¥${localPrice.toLocaleString()} (${usdPrice.toFixed(2)} USD)`);

Regulatory Compliance & Disclaimers

Financial advice has legal implications. Include proper disclaimers on every report. Don't make specific buy/sell recommendations unless you're a registered advisor. Log all generated reports for audit trail.

REQUIRED_DISCLAIMERS = """
DISCLAIMER: This report is for informational purposes only and does not constitute 
financial advice, investment recommendation, or an offer to buy or sell securities. 
Past performance is not indicative of future results. All investments carry risk of loss.

This analysis is generated using AI and may contain errors. Always consult with a 
qualified financial advisor before making investment decisions.

Data sources: Yahoo Finance (15-minute delayed), Alpha Vantage. 
Generated: {timestamp}
Report ID: {report_id}
"""

def add_compliance_footer(report_data: dict) -> dict:
    """Add required disclaimers and audit trail"""
    import uuid
    from datetime import datetime
    
    report_id = str(uuid.uuid4())
    timestamp = datetime.now().isoformat()
    
    # Add to report
    report_data['disclaimer'] = REQUIRED_DISCLAIMERS.format(
        timestamp=timestamp,
        report_id=report_id
    )
    
    # Log for audit trail (keep for 7 years per SEC)
    audit_log = {
        'report_id': report_id,
        'timestamp': timestamp,
        'ticker': report_data['market_data']['ticker'],
        'generated_by': 'AI_System_v1.0',
        'data_sources': ['Yahoo Finance', 'Alpha Vantage'],
        'user_id': os.environ.get('USER_ID', 'system')
    }
    
    # Store in audit database
    db.audit_logs.insert_one(audit_log)
    
    return report_data

Cost Calculator

Manual Process

Analyst time (4-6 hours @ $150/hr)
$600-900
Data subscriptions (Bloomberg Terminal)
$2,000/month
Design/formatting time (1 hour @ $100/hr)
$100
Total:$700-1,000 per report
2-3 reports/day maximum

Limitations:

  • Can't scale past 60 reports/month
  • Inconsistent quality (analyst fatigue)
  • High error rate on repetitive tasks
  • Delayed delivery during busy periods

Automated Process

API costs (OpenAI GPT-4)
$0.50-1.00
Market data (Yahoo Finance free tier)
$0
Infrastructure (AWS/Redis)
$50/month
Human review (15 min @ $150/hr)
$37.50
Total:$38-39 per report
50+ reports/day capacity

Benefits:

  • Scale to 1,500 reports/month
  • Consistent quality (same prompts)
  • Near-zero error rate
  • Delivery in minutes, not hours
$6,620/day saved
95% cost reduction | $33,000/month | $396,000/year
💡 Break-even in 2 weeks at 10 reports/day
💼

Want This Running in Your Trading Desk?

We build custom finance AI systems that integrate with Bloomberg, generate compliant reports, and scale to thousands of reports per day. From prototype to production in 4-6 weeks.