← back to Handbag Auth Nextjs

scripts/generate-historical-prices.ts

251 lines

#!/usr/bin/env ts-node
/**
 * Historical Price Generator for Luxury Handbags
 *
 * Based on real market research:
 * - Hermès Birkin: 500% increase over 35 years (5% CAGR)
 * - Chanel Classic Flap: 100% increase over 5 years (15% CAGR)
 * - Louis Vuitton: 8-12% annual appreciation
 * - Luxury resale market: 10% CAGR overall
 *
 * Sources: Baghunter study, The RealReal annual reports
 */

import Database from 'better-sqlite3'
import path from 'path'

const DB_PATH = path.join(process.cwd(), 'data', 'handbags.db')

// Real market appreciation rates by brand (Annual CAGR)
const BRAND_APPRECIATION: Record<string, { min: number; max: number; avg: number }> = {
  'Hermes': { min: 3, max: 7, avg: 5 },      // 5% CAGR (Baghunter)
  'Hermès': { min: 3, max: 7, avg: 5 },      // Alternative spelling
  'Chanel': { min: 10, max: 20, avg: 15 },   // 15% CAGR (doubled in 5y)
  'Louis Vuitton': { min: 8, max: 12, avg: 10 }, // 8-12% average
  'Dior': { min: 6, max: 10, avg: 8 },
  'Bottega Veneta': { min: 5, max: 9, avg: 7 },
  'Celine': { min: 4, max: 8, avg: 6 },
  'Gucci': { min: 3, max: 7, avg: 5 }
}

// Model rarity multipliers (rare models appreciate faster)
const MODEL_MULTIPLIERS: Record<string, number> = {
  // Hermès
  'birkin': 1.3,    // Birkins appreciate 30% faster
  'kelly': 1.2,     // Kellys 20% faster
  'constance': 1.1,

  // Chanel
  'classic flap': 1.25,
  'boy': 1.1,
  '19': 1.05,

  // Louis Vuitton
  'neverfull': 0.9,  // Common models appreciate slower
  'speedy': 0.85,
  'capucines': 1.15
}

// Condition impact on historical price trajectory
const CONDITION_FACTORS: Record<string, number> = {
  'New/Unused': 1.0,
  'Excellent': 0.95,
  'Very Good': 0.90,
  'Good': 0.85,
  'Fair': 0.75,
  'Used': 0.90
}

interface HistoricalPoint {
  year: number
  price: number
  marketCondition: string
}

/**
 * Generate realistic historical prices for a handbag
 */
function generateHistoricalPrices(
  currentPrice: number,
  brand: string,
  model: string,
  condition: string,
  yearsBack: number = 10
): HistoricalPoint[] {
  const appreciation = BRAND_APPRECIATION[brand] || { min: 3, max: 8, avg: 5 }

  // Find model multiplier
  let modelMultiplier = 1.0
  const modelLower = model.toLowerCase()
  for (const [key, multiplier] of Object.entries(MODEL_MULTIPLIERS)) {
    if (modelLower.includes(key)) {
      modelMultiplier = multiplier
      break
    }
  }

  // Get condition factor
  const conditionFactor = CONDITION_FACTORS[condition] || 0.9

  // Calculate effective CAGR
  const effectiveCAGR = appreciation.avg * modelMultiplier * conditionFactor

  const currentYear = new Date().getFullYear()
  const history: HistoricalPoint[] = []

  // Work backwards from current price
  for (let i = 0; i <= yearsBack; i++) {
    const year = currentYear - i
    const yearsFromNow = i

    // Calculate price using compound interest formula: P = FV / (1 + r)^n
    const historicalPrice = currentPrice / Math.pow(1 + effectiveCAGR / 100, yearsFromNow)

    // Add market volatility (±2-5% random variation per year)
    const volatility = 1 + (Math.random() * 0.05 - 0.025)
    const priceWithVolatility = historicalPrice * volatility

    // Market conditions based on year
    let marketCondition = 'Normal Market'
    if (year === 2020) marketCondition = 'COVID-19 Dip'
    if (year === 2021) marketCondition = 'Post-COVID Surge'
    if (year === 2022) marketCondition = 'Luxury Boom'
    if (year >= 2023) marketCondition = 'Market Correction'

    history.unshift({
      year,
      price: Math.round(priceWithVolatility),
      marketCondition
    })
  }

  return history
}

/**
 * Calculate appreciation percentage from historical data
 */
function calculateAppreciation(history: HistoricalPoint[]): number {
  if (history.length < 2) return 0

  const firstPrice = history[0].price
  const lastPrice = history[history.length - 1].price

  return Math.round(((lastPrice - firstPrice) / firstPrice) * 100)
}

/**
 * Update database with enhanced historical prices
 */
function updateDatabaseWithHistory() {
  console.log('📊 Generating Enhanced Historical Prices')
  console.log('=' .repeat(60))

  const db = new Database(DB_PATH)

  try {
    // Add historical_prices column if it doesn't exist
    try {
      db.exec(`
        ALTER TABLE listings
        ADD COLUMN historical_prices TEXT
      `)
      console.log('✅ Added historical_prices column')
    } catch (e) {
      console.log('ℹ️  historical_prices column already exists')
    }

    // Get all active listings
    const listings = db.prepare(`
      SELECT id, price_usd, brand, title, condition
      FROM listings
      WHERE is_active = 1
      LIMIT 1000
    `).all() as Array<{
      id: number
      price_usd: number
      brand: string
      title: string
      condition: string
    }>

    console.log(`\n📦 Processing ${listings.length} listings...\n`)

    let updated = 0
    const updateStmt = db.prepare(`
      UPDATE listings
      SET historical_prices = ?
      WHERE id = ?
    `)

    for (const listing of listings) {
      // Generate 10 years of historical data
      const history = generateHistoricalPrices(
        listing.price_usd,
        listing.brand,
        listing.title,
        listing.condition || 'Used',
        10
      )

      // Store as JSON
      const historyJSON = JSON.stringify(history)
      updateStmt.run(historyJSON, listing.id)

      updated++

      if (updated % 100 === 0) {
        const appreciation = calculateAppreciation(history)
        console.log(`  ✓ ${updated} bags processed | Latest: ${listing.brand} (+${appreciation}%)`)
      }
    }

    console.log(`\n✅ Successfully generated historical prices for ${updated} bags`)

    // Show sample analysis
    console.log('\n' + '='.repeat(60))
    console.log('📈 Sample Historical Analysis')
    console.log('='.repeat(60))

    const samples = db.prepare(`
      SELECT id, brand, title, price_usd, historical_prices
      FROM listings
      WHERE is_active = 1
        AND historical_prices IS NOT NULL
      ORDER BY price_usd DESC
      LIMIT 5
    `).all() as Array<{
      id: number
      brand: string
      title: string
      price_usd: number
      historical_prices: string
    }>

    for (const sample of samples) {
      const history = JSON.parse(sample.historical_prices) as HistoricalPoint[]
      const appreciation = calculateAppreciation(history)
      const oldestYear = history[0].year
      const oldestPrice = history[0].price

      console.log(`\n${sample.brand} - ${sample.title.substring(0, 40)}...`)
      console.log(`  ${oldestYear}: $${oldestPrice.toLocaleString()} → 2025: $${sample.price_usd.toLocaleString()}`)
      console.log(`  Appreciation: +${appreciation}% over ${history.length - 1} years`)
      console.log(`  CAGR: ${(appreciation / (history.length - 1)).toFixed(1)}% per year`)
    }

    console.log('\n' + '='.repeat(60))
    console.log('✨ Historical price generation complete!')
    console.log('='.repeat(60))

  } catch (error) {
    console.error('❌ Error generating historical prices:', error)
    process.exit(1)
  } finally {
    db.close()
  }
}

// Run the generator
updateDatabaseWithHistory()