← back to Wine Finder Next

src/hooks/useArbitrageOpportunities.ts

81 lines

/**
 * useArbitrageOpportunities Hook
 * Calculates arbitrage opportunities from wine listings
 */

import { useMemo } from 'react';
import { WineListing, ArbitrageOpportunity } from '../types/wine';

interface UseArbitrageOpportunitiesReturn {
  opportunities: ArbitrageOpportunity[];
  bestDeal: ArbitrageOpportunity | null;
  totalProfit: number;
  opportunityCount: number;
}

export function useArbitrageOpportunities(
  listings: WineListing[]
): UseArbitrageOpportunitiesReturn {
  const opportunities = useMemo(() => {
    const retail = listings.filter((l) => l.provider === 'retail');
    const auction = listings.filter((l) => l.provider === 'auction');

    const opps: ArbitrageOpportunity[] = [];

    auction.forEach((auctionListing) => {
      retail.forEach((retailListing) => {
        // Fuzzy match wine names
        const nameMatch =
          auctionListing.name.toLowerCase().includes(retailListing.name.toLowerCase()) ||
          retailListing.name.toLowerCase().includes(auctionListing.name.toLowerCase());

        // Vintage match (if both have vintage)
        const vintageMatch =
          !auctionListing.vintage ||
          !retailListing.vintage ||
          auctionListing.vintage === retailListing.vintage;

        if (nameMatch && vintageMatch) {
          const spread = auctionListing.currentPrice.amount - retailListing.currentPrice.amount;
          const spreadPercentage = (spread / retailListing.currentPrice.amount) * 100;

          // Only show significant arbitrage (>$10)
          if (spread > 10) {
            opps.push({
              wine: {
                id: retailListing.id,
                name: retailListing.name,
                vintage: retailListing.vintage,
                region: retailListing.region,
                rating: retailListing.rating,
              },
              retailListing,
              auctionListing,
              spread,
              spreadPercentage,
            });
          }
        }
      });
    });

    // Sort by dollar profit (highest first)
    return opps.sort((a, b) => b.spread - a.spread);
  }, [listings]);

  const bestDeal = useMemo(() => {
    return opportunities.length > 0 ? opportunities[0] : null;
  }, [opportunities]);

  const totalProfit = useMemo(() => {
    return opportunities.reduce((sum, opp) => sum + opp.spread, 0);
  }, [opportunities]);

  return {
    opportunities,
    bestDeal,
    totalProfit,
    opportunityCount: opportunities.length,
  };
}