diff --git a/package.json b/package.json new file mode 100644 index 0000000..27ccc5b --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name": "arber2", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/src/api/kalshi.ts b/src/api/kalshi.ts new file mode 100644 index 0000000..346fb2a --- /dev/null +++ b/src/api/kalshi.ts @@ -0,0 +1,239 @@ +// Kalshi API integration layer +import { + KalshiMarket, + KalshiOrderbook, + CommonMarket, + MarketCondition, + ApiResponse, + EnvironmentConfig +} from './types'; + +// Environment configuration with defaults +const config: EnvironmentConfig = { + KALSHI_API_KEY: process.env.KALSHI_API_KEY, + KALSHI_BASE_URL: process.env.KALSHI_BASE_URL || 'https://trading-api.kalshi.com', + POLYMARKET_GAMMA_URL: process.env.POLYMARKET_GAMMA_URL || 'https://gamma-api.polymarket.com', + POLYMARKET_CLOB_URL: process.env.POLYMARKET_CLOB_URL || 'https://clob.polymarket.com', + POLYMARKET_PRICE_URL: process.env.POLYMARKET_PRICE_URL || 'https://strapi-matic.poly.market', +}; + +/** + * Fetch open/active markets from Kalshi + */ +export async function fetchKalshiMarkets(): Promise> { + try { + const url = `${config.KALSHI_BASE_URL}/v1/markets?status=open&limit=100`; + const headers: HeadersInit = { + 'Accept': 'application/json', + }; + + // Add API key if available + if (config.KALSHI_API_KEY) { + headers['Authorization'] = `Bearer ${config.KALSHI_API_KEY}`; + } + + const response = await fetch(url, { headers }); + + if (!response.ok) { + throw new Error(`Kalshi API error: ${response.status} ${response.statusText}`); + } + + const data = await response.json(); + const markets = data.markets || data.data || []; + + return { + success: true, + data: markets, + timestamp: new Date(), + }; + } catch (error) { + console.error('Error fetching Kalshi markets:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + timestamp: new Date(), + }; + } +} + +/** + * Fetch orderbook for a specific Kalshi market + */ +export async function fetchKalshiOrderbook(ticker: string): Promise> { + try { + const url = `${config.KALSHI_BASE_URL}/v1/markets/${ticker}/orderbook`; + const headers: HeadersInit = { + 'Accept': 'application/json', + }; + + if (config.KALSHI_API_KEY) { + headers['Authorization'] = `Bearer ${config.KALSHI_API_KEY}`; + } + + const response = await fetch(url, { headers }); + + if (!response.ok) { + throw new Error(`Kalshi orderbook API error: ${response.status} ${response.statusText}`); + } + + const data = await response.json(); + + return { + success: true, + data: data.orderbook || data, + timestamp: new Date(), + }; + } catch (error) { + console.error(`Error fetching Kalshi orderbook for ${ticker}:`, error); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + timestamp: new Date(), + }; + } +} + +/** + * Convert Kalshi market to CommonMarket format + */ +export function normalizeKalshiMarket(market: KalshiMarket, orderbook?: KalshiOrderbook): CommonMarket { + // Extract pricing from orderbook if available, otherwise use market data + let yesBid = market.yes_bid; + let yesAsk = market.yes_ask; + let noBid = market.no_bid; + let noAsk = market.no_ask; + + if (orderbook) { + if (orderbook.yes && orderbook.yes.length > 0) { + yesBid = orderbook.yes[0].price; // Best bid + yesAsk = orderbook.yes.find(order => order.size > 0)?.price; + } + if (orderbook.no && orderbook.no.length > 0) { + noBid = orderbook.no[0].price; // Best bid + noAsk = orderbook.no.find(order => order.size > 0)?.price; + } + } + + // For Kalshi, calculate No prices as 1 - opposite side when needed + if (yesBid && !noBid) { + noBid = 1 - yesBid; + } + if (yesAsk && !noAsk) { + noAsk = 1 - yesAsk; + } + if (noBid && !yesBid) { + yesBid = 1 - noBid; + } + if (noAsk && !yesAsk) { + yesAsk = 1 - noAsk; + } + + // Create outcomes for binary markets + const outcomes: MarketCondition[] = []; + + if (yesBid !== undefined && yesAsk !== undefined) { + outcomes.push({ + id: `${market.ticker}-yes`, + name: 'Yes', + yesPrice: yesAsk, // Ask price to buy YES + noPrice: noBid || (1 - yesAsk), // Bid price to buy NO (or calculated) + volume: market.volume, + }); + } + + if (noBid !== undefined && noAsk !== undefined) { + outcomes.push({ + id: `${market.ticker}-no`, + name: 'No', + yesPrice: noAsk, // Ask price to buy NO + noPrice: yesBid || (1 - noAsk), // Bid price to buy YES (or calculated) + volume: market.volume, + }); + } + + return { + id: market.ticker, + platform: 'kalshi', + title: market.title, + subtitle: market.subtitle, + category: market.category || 'Unknown', + closeTime: new Date(market.close_time), + volume: market.dollar_volume || market.volume, + liquidity: market.open_interest, + yesBid, + yesAsk, + noBid, + noAsk, + outcomes, + }; +} + +/** + * Fetch and normalize all Kalshi markets + */ +export async function fetchNormalizedKalshiMarkets(): Promise> { + const marketsResponse = await fetchKalshiMarkets(); + + if (!marketsResponse.success || !marketsResponse.data) { + return marketsResponse as ApiResponse; + } + + try { + // Normalize markets, optionally with orderbook data + const normalizedMarkets: CommonMarket[] = marketsResponse.data.map(market => { + return normalizeKalshiMarket(market); + }); + + return { + success: true, + data: normalizedMarkets, + timestamp: new Date(), + }; + } catch (error) { + console.error('Error normalizing Kalshi markets:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Error normalizing markets', + timestamp: new Date(), + }; + } +} + +/** + * Fetch orderbooks for multiple markets (for live pricing updates) + */ +export async function fetchKalshiOrderbooks(tickers: string[]): Promise> { + const orderbooks = new Map(); + + // Fetch orderbooks in parallel with reasonable rate limiting + const promises = tickers.map(async (ticker, index) => { + // Add small delay to avoid rate limiting + await new Promise(resolve => setTimeout(resolve, index * 100)); + + const response = await fetchKalshiOrderbook(ticker); + if (response.success && response.data) { + orderbooks.set(ticker, response.data); + } + }); + + await Promise.allSettled(promises); + return orderbooks; +} + +/** + * Check if Kalshi API is accessible (health check) + */ +export async function checkKalshiConnection(): Promise { + try { + const response = await fetch(`${config.KALSHI_BASE_URL}/v1/status`, { + method: 'GET', + headers: { 'Accept': 'application/json' }, + signal: AbortSignal.timeout(5000), // 5 second timeout + }); + + return response.ok; + } catch (error) { + console.warn('Kalshi API connection check failed:', error); + return false; + } +} \ No newline at end of file diff --git a/src/api/polymarket.ts b/src/api/polymarket.ts new file mode 100644 index 0000000..8c11d76 --- /dev/null +++ b/src/api/polymarket.ts @@ -0,0 +1,331 @@ +// Polymarket API integration layer +import { + PolymarketMarket, + PolymarketOrderbook, + CommonMarket, + MarketCondition, + ApiResponse, + EnvironmentConfig +} from './types'; + +// Environment configuration (imported from kalshi.ts pattern) +const config: EnvironmentConfig = { + KALSHI_API_KEY: process.env.KALSHI_API_KEY, + KALSHI_BASE_URL: process.env.KALSHI_BASE_URL || 'https://trading-api.kalshi.com', + POLYMARKET_GAMMA_URL: process.env.POLYMARKET_GAMMA_URL || 'https://gamma-api.polymarket.com', + POLYMARKET_CLOB_URL: process.env.POLYMARKET_CLOB_URL || 'https://clob.polymarket.com', + POLYMARKET_PRICE_URL: process.env.POLYMARKET_PRICE_URL || 'https://strapi-matic.poly.market', +}; + +/** + * Fetch active markets from Polymarket Gamma API + */ +export async function fetchPolymarketMarkets(): Promise> { + try { + // Fetch from Gamma API - get active markets + const url = `${config.POLYMARKET_GAMMA_URL}/markets?active=true&limit=100`; + + const response = await fetch(url, { + headers: { + 'Accept': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error(`Polymarket Gamma API error: ${response.status} ${response.statusText}`); + } + + const data = await response.json(); + const markets = data.data || data.markets || data; + + return { + success: true, + data: Array.isArray(markets) ? markets : [], + timestamp: new Date(), + }; + } catch (error) { + console.error('Error fetching Polymarket markets:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + timestamp: new Date(), + }; + } +} + +/** + * Fetch orderbook/price data for specific Polymarket token + */ +export async function fetchPolymarketOrderbook(tokenId: string): Promise> { + try { + // Try CLOB API first + const clobUrl = `${config.POLYMARKET_CLOB_URL}/book?token_id=${tokenId}`; + + const response = await fetch(clobUrl, { + headers: { + 'Accept': 'application/json', + }, + }); + + if (response.ok) { + const data = await response.json(); + return { + success: true, + data: { + market: tokenId, + asset_id: tokenId, + bids: data.bids || [], + asks: data.asks || [], + }, + timestamp: new Date(), + }; + } + + // Fallback to price API if CLOB fails + const priceUrl = `${config.POLYMARKET_PRICE_URL}/markets/${tokenId}/price`; + const priceResponse = await fetch(priceUrl); + + if (priceResponse.ok) { + const priceData = await priceResponse.json(); + // Convert price data to orderbook format + return { + success: true, + data: { + market: tokenId, + asset_id: tokenId, + bids: [{ price: priceData.price || 0.5, size: 1 }], + asks: [{ price: priceData.price || 0.5, size: 1 }], + }, + timestamp: new Date(), + }; + } + + throw new Error('Both CLOB and price APIs failed'); + } catch (error) { + console.error(`Error fetching Polymarket orderbook for ${tokenId}:`, error); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + timestamp: new Date(), + }; + } +} + +/** + * Fetch price data for multiple Polymarket tokens + */ +export async function fetchPolymarketPrices(tokenIds: string[]): Promise> { + const prices = new Map(); + + // Batch fetch prices with rate limiting + const promises = tokenIds.map(async (tokenId, index) => { + // Add small delay to avoid rate limiting + await new Promise(resolve => setTimeout(resolve, index * 50)); + + const response = await fetchPolymarketOrderbook(tokenId); + if (response.success && response.data) { + const orderbook = response.data; + // Use mid price from best bid/ask + const bestBid = orderbook.bids[0]?.price || 0; + const bestAsk = orderbook.asks[0]?.price || 1; + const midPrice = (bestBid + bestAsk) / 2; + prices.set(tokenId, midPrice); + } + }); + + await Promise.allSettled(promises); + return prices; +} + +/** + * Convert Polymarket market to CommonMarket format + */ +export function normalizePolymarketMarket( + market: PolymarketMarket, + priceData?: Map +): CommonMarket { + const outcomes: MarketCondition[] = []; + + // Handle binary markets (2 outcomes) + if (market.outcomes.length === 2 && market.clobTokenIds.length >= 2) { + market.outcomes.forEach((outcomeName, index) => { + const tokenId = market.clobTokenIds[index]; + const orderbook = priceData?.get(tokenId); + + let yesPrice = 0.5; // Default mid price + let noPrice = 0.5; + + if (orderbook) { + const bestBid = orderbook.bids[0]?.price || 0; + const bestAsk = orderbook.asks[0]?.price || 1; + yesPrice = bestAsk; // Price to buy YES + noPrice = 1 - bestBid; // Price to buy NO (complement) + } + + outcomes.push({ + id: tokenId, + name: outcomeName, + yesPrice, + noPrice, + clobTokenId: tokenId, + }); + }); + } + // Handle multi-outcome markets (>2 outcomes) + else if (market.outcomes.length > 2) { + market.outcomes.forEach((outcomeName, index) => { + const tokenId = market.clobTokenIds[index]; + const orderbook = priceData?.get(tokenId); + + let yesPrice = 1 / market.outcomes.length; // Equal probability default + let noPrice = 1 - yesPrice; + + if (orderbook) { + const bestBid = orderbook.bids[0]?.price || 0; + const bestAsk = orderbook.asks[0]?.price || 1; + yesPrice = bestAsk; + // For multi-outcome, "no" price is more complex - use complement for now + noPrice = 1 - bestBid; + } + + outcomes.push({ + id: tokenId, + name: outcomeName, + yesPrice, + noPrice, + clobTokenId: tokenId, + }); + }); + } + + // Calculate overall market prices for binary markets + let yesBid, yesAsk, noBid, noAsk; + if (outcomes.length === 2) { + const yesOutcome = outcomes[0]; + const noOutcome = outcomes[1]; + + yesAsk = yesOutcome.yesPrice; + noAsk = noOutcome.yesPrice; + yesBid = 1 - noOutcome.yesPrice; + noBid = 1 - yesOutcome.yesPrice; + } + + return { + id: market.conditionId, + platform: 'polymarket', + title: market.question, + subtitle: market.description, + category: market.category || 'Unknown', + closeTime: new Date(market.endDate), + volume: market.volume, + settlementSource: market.resolutionSource, + yesBid, + yesAsk, + noBid, + noAsk, + outcomes, + }; +} + +/** + * Fetch and normalize all Polymarket markets + */ +export async function fetchNormalizedPolymarketMarkets(): Promise> { + const marketsResponse = await fetchPolymarketMarkets(); + + if (!marketsResponse.success || !marketsResponse.data) { + return marketsResponse as ApiResponse; + } + + try { + // Collect all token IDs for price fetching + const allTokenIds: string[] = []; + marketsResponse.data.forEach(market => { + allTokenIds.push(...market.clobTokenIds); + }); + + // Fetch prices for all tokens + const priceMap = new Map(); + + // Convert price data to orderbook format for normalization + const prices = await fetchPolymarketPrices(allTokenIds.slice(0, 50)); // Limit for initial load + prices.forEach((price, tokenId) => { + priceMap.set(tokenId, { + market: tokenId, + asset_id: tokenId, + bids: [{ price: price * 0.95, size: 1 }], // Approximate spread + asks: [{ price: price * 1.05, size: 1 }], + }); + }); + + // Normalize markets with price data + const normalizedMarkets: CommonMarket[] = marketsResponse.data + .filter(market => market.active && market.clobTokenIds.length > 0) + .map(market => normalizePolymarketMarket(market, priceMap)); + + return { + success: true, + data: normalizedMarkets, + timestamp: new Date(), + }; + } catch (error) { + console.error('Error normalizing Polymarket markets:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Error normalizing markets', + timestamp: new Date(), + }; + } +} + +/** + * Check if Polymarket APIs are accessible (health check) + */ +export async function checkPolymarketConnection(): Promise<{ gamma: boolean; clob: boolean }> { + const results = { gamma: false, clob: false }; + + try { + // Check Gamma API + const gammaResponse = await fetch(`${config.POLYMARKET_GAMMA_URL}/markets?limit=1`, { + method: 'GET', + headers: { 'Accept': 'application/json' }, + signal: AbortSignal.timeout(5000), + }); + results.gamma = gammaResponse.ok; + } catch (error) { + console.warn('Polymarket Gamma API connection check failed:', error); + } + + try { + // Check CLOB API with a known endpoint + const clobResponse = await fetch(`${config.POLYMARKET_CLOB_URL}/ping`, { + method: 'GET', + signal: AbortSignal.timeout(5000), + }); + results.clob = clobResponse.ok; + } catch (error) { + console.warn('Polymarket CLOB API connection check failed:', error); + } + + return results; +} + +/** + * Get real-time price updates for specific tokens (for SSE/polling) + */ +export async function getPolymarketLivePrices(tokenIds: string[]): Promise> { + const livePrices = new Map(); + + try { + const prices = await fetchPolymarketPrices(tokenIds); + const timestamp = new Date(); + + prices.forEach((price, tokenId) => { + livePrices.set(tokenId, { price, timestamp }); + }); + } catch (error) { + console.error('Error fetching live Polymarket prices:', error); + } + + return livePrices; +} \ No newline at end of file diff --git a/src/api/types.ts b/src/api/types.ts new file mode 100644 index 0000000..a4230ee --- /dev/null +++ b/src/api/types.ts @@ -0,0 +1,216 @@ +// Shared TypeScript types for arbitrage detection system + +// Platform identifier +export type Platform = 'kalshi' | 'polymarket'; + +// Relationship types for condition mapping +export type RelationshipType = 'same' | 'subset' | 'mutually-exclusive' | 'complementary' | 'opposites' | 'overlapping'; + +// Common market structure normalized across platforms +export interface CommonMarket { + id: string; + platform: Platform; + title: string; + subtitle?: string; + category: string; + closeTime: Date; + volume?: number; + liquidity?: number; + settlementSource?: string; + rules?: string; + // Pricing for binary markets + yesBid?: number; + yesAsk?: number; + noBid?: number; + noAsk?: number; + // Multi-outcome conditions + outcomes: MarketCondition[]; +} + +// Individual condition/outcome within a market +export interface MarketCondition { + id: string; + name: string; + yesPrice: number; + noPrice: number; + volume?: number; + // For Polymarket multi-outcome markets + clobTokenId?: string; +} + +// Kalshi-specific market data +export interface KalshiMarket { + event_ticker: string; + ticker: string; + title: string; + subtitle?: string; + status: string; + close_time: string; + expiration_time: string; + category: string; + yes_bid?: number; + yes_ask?: number; + no_bid?: number; + no_ask?: number; + volume?: number; + open_interest?: number; + dollar_volume?: number; +} + +// Kalshi orderbook structure +export interface KalshiOrderbook { + market: string; + yes?: Array<{ price: number; size: number }>; + no?: Array<{ price: number; size: number }>; +} + +// Polymarket-specific market data +export interface PolymarketMarket { + conditionId: string; + questionId: string; + question: string; + description?: string; + outcomes: string[]; + outcomeSlots: string[]; + resolutionSource: string; + endDate: string; + category: string; + clobTokenIds: string[]; + active: boolean; + volume?: number; +} + +// Polymarket orderbook/price data +export interface PolymarketOrderbook { + market: string; + asset_id: string; + bids: Array<{ price: number; size: number }>; + asks: Array<{ price: number; size: number }>; +} + +// Condition mapping between different platforms +export interface ConditionMapping { + id: string; + kalshiCondition?: string; + polymarketCondition?: string; + relationship: RelationshipType; + confidence?: number; + createdAt: Date; +} + +// Market match between platforms +export interface MarketMatch { + id: string; + kalshiMarketId: string; + polymarketMarketId: string; + createdAt: Date; + confidence: number; + conditionMappings: ConditionMapping[]; + conditionsMatched: boolean; + totalVolume?: number; +} + +// Ecosystem containing multiple markets across exchanges +export interface Ecosystem { + id: string; + name: string; + createdAt: Date; + marketRefs: Array<{ + platform: Platform; + marketId: string; + market?: CommonMarket; + }>; + conditionMappings: ConditionMapping[]; + conditionsMatched: boolean; + // Derived stats + exchanges: number; + markets: number; + conditions: number; + earliestEndTime?: Date; +} + +// Arbitrage opportunity +export interface ArbitrageOpportunity { + id: string; + eventName: string; + eventType: string; + market: string; + earliestEndTime: Date; + daysUntilClose: number; + // Arbitrage calculation + minYes: number; + minNo: number; + C: number; // minYes + minNo + periodReturn: number; // (1 - C) / C + apr: number; // periodReturn * (365 / daysUntilClose) + profitOn100: number; // 100 * periodReturn + // Underlying bets + yesBet: { + platform: Platform; + marketId: string; + condition: string; + price: number; + }; + noBet: { + platform: Platform; + marketId: string; + condition: string; + price: number; + }; + // Metadata + updatedAt: Date; + // All underlying conditions for expanded view + allConditions: Array<{ + platform: Platform; + marketId: string; + condition: string; + yesPrice: number; + noPrice: number; + }>; +} + +// API response wrapper +export interface ApiResponse { + success: boolean; + data?: T; + error?: string; + timestamp: Date; +} + +// Market similarity scoring +export interface SimilarityScore { + overall: number; + title: number; + date: number; + conditions: number; + settlement: number; +} + +// Polling configuration +export interface PollingConfig { + kalshiInterval: number; // milliseconds + polymarketInterval: number; // milliseconds + enabled: boolean; +} + +// Store state interface +export interface MatchesStore { + markets: { + kalshi: CommonMarket[]; + polymarket: CommonMarket[]; + lastUpdated: Date; + }; + matches: MarketMatch[]; + ecosystems: Ecosystem[]; + arbitrageOpportunities: ArbitrageOpportunity[]; + polling: PollingConfig; +} + +// Environment configuration +export interface EnvironmentConfig { + KALSHI_API_KEY?: string; + KALSHI_BASE_URL: string; + POLYMARKET_GAMMA_URL: string; + POLYMARKET_CLOB_URL: string; + POLYMARKET_PRICE_URL: string; +} \ No newline at end of file diff --git a/src/arbitrage-bets-component.tsx b/src/arbitrage-bets-component.tsx index 97acbbb..7cc1b68 100644 --- a/src/arbitrage-bets-component.tsx +++ b/src/arbitrage-bets-component.tsx @@ -1,144 +1,39 @@ import React, { useState, useEffect } from 'react'; import { Circle, Square, Search, Settings, RefreshCw, ChevronDown } from 'lucide-react'; +import { useLiveOrderbooks } from './hooks/useLiveOrderbooks'; +import { getStore, subscribeToStore } from './state/matches-store'; +import { ArbitrageOpportunity } from './api/types'; const ArbitrageBets = () => { const [activeTab, setActiveTab] = useState('pre-match'); const [searchTerm, setSearchTerm] = useState(''); - const [arbitrageData, setArbitrageData] = useState([]); - const [expandedRows, setExpandedRows] = useState([]); + const [arbitrageData, setArbitrageData] = useState([]); + const [expandedRows, setExpandedRows] = useState([]); - // Mock data for demonstration with conditions - const mockArbitrageData = [ - { - id: 1, - percentage: 1.64, - event: 'Toronto Blue Jays vs Cleveland Guardians', - eventType: 'Baseball | MLB', - market: '1st inning Total Runs', - startTime: '7:10 PM', - bets: [ - { outcome: 'Over 0.5', venue: 'NoVig', odds: '+125', stake: 100, payout: 225 }, - { outcome: 'Under 0.5', venue: 'Sporttrade', odds: '-117', stake: 117, payout: 217 } - ], - profit: 8, - totalStake: 217, - updated: '2 min ago', - kalshiConditions: [ - { name: 'Over 0.5', yesPrice: 0.44, noPrice: 0.56 }, - { name: 'Under 0.5', yesPrice: 0.54, noPrice: 0.46 } - ], - polymarketConditions: [ - { name: 'Over 0.5 runs', yesPrice: 0.42, noPrice: 0.58 }, - { name: 'Under 0.5 runs', yesPrice: 0.56, noPrice: 0.44 } - ] - }, - { - id: 2, - percentage: 1.6, - event: 'Atlanta Braves vs Pittsburgh Pirates', - eventType: 'Baseball | MLB', - market: 'Total Runs', - startTime: '8:39 PM', - bets: [ - { outcome: 'Over 8.5', venue: 'ProphetX', odds: '-108', stake: 108, payout: 208 }, - { outcome: 'Under 8.5', venue: 'NoVig', odds: '+113', stake: 100, payout: 213 } - ], - profit: 5, - totalStake: 208, - updated: '5 min ago', - kalshiConditions: [ - { name: 'Over 8.5', yesPrice: 0.52, noPrice: 0.48 }, - { name: 'Under 8.5', yesPrice: 0.47, noPrice: 0.53 } - ], - polymarketConditions: [ - { name: 'Over 8.5 total', yesPrice: 0.50, noPrice: 0.50 }, - { name: 'Under 8.5 total', yesPrice: 0.49, noPrice: 0.51 } - ] - }, - { - id: 3, - percentage: 1.35, - event: 'Atlanta Braves vs Pittsburgh Pirates', - eventType: 'Baseball | MLB', - market: '1st Half Total Runs', - startTime: '8:30 PM', - bets: [ - { outcome: 'Over 7.5', venue: 'Kalshi', odds: '-235', stake: 235, payout: 335 }, - { outcome: 'Under 7.5', venue: 'Polymarket', odds: '+240', stake: 100, payout: 340 } - ], - profit: 5, - totalStake: 335, - updated: '8 min ago', - kalshiConditions: [ - { name: 'Over 7.5', yesPrice: 0.70, noPrice: 0.30 }, - { name: 'Under 7.5', yesPrice: 0.29, noPrice: 0.71 } - ], - polymarketConditions: [ - { name: 'Over 7.5 runs', yesPrice: 0.68, noPrice: 0.32 }, - { name: 'Under 7.5 runs', yesPrice: 0.31, noPrice: 0.69 } - ] - }, - { - id: 4, - percentage: 2.1, - event: 'Fed Rate Decision - September 2025', - eventType: 'Politics | Economics', - market: 'Will Fed raise rates?', - startTime: 'Sep 18', - bets: [ - { outcome: 'Yes', venue: 'Kalshi', odds: '+180', stake: 100, payout: 280 }, - { outcome: 'No', venue: 'Polymarket', odds: '-165', stake: 165, payout: 265 } - ], - profit: 15, - totalStake: 265, - updated: '12 min ago', - kalshiConditions: [ - { name: '25 bps increase', yesPrice: 0.35, noPrice: 0.65 }, - { name: 'No change', yesPrice: 0.45, noPrice: 0.55 }, - { name: '25 bps decrease', yesPrice: 0.18, noPrice: 0.82 }, - { name: '50+ bps increase', yesPrice: 0.02, noPrice: 0.98 } - ], - polymarketConditions: [ - { name: 'Rate increase', yesPrice: 0.37, noPrice: 0.63 }, - { name: 'No change', yesPrice: 0.44, noPrice: 0.56 }, - { name: 'Rate decrease', yesPrice: 0.19, noPrice: 0.81 } - ] - }, - { - id: 5, - percentage: 1.8, - event: 'Bitcoin above $100k by EOY', - eventType: 'Crypto | Finance', - market: 'Bitcoin > $100,000', - startTime: 'Dec 31', - bets: [ - { outcome: 'Yes', venue: 'Polymarket', odds: '+145', stake: 100, payout: 245 }, - { outcome: 'No', venue: 'Kalshi', odds: '-135', stake: 135, payout: 235 } - ], - profit: 10, - totalStake: 235, - updated: '15 min ago', - kalshiConditions: [ - { name: 'Yes', yesPrice: 0.40, noPrice: 0.60 }, - { name: 'No', yesPrice: 0.60, noPrice: 0.40 } - ], - polymarketConditions: [ - { name: 'Yes', yesPrice: 0.38, noPrice: 0.62 }, - { name: 'No', yesPrice: 0.62, noPrice: 0.38 } - ] - } - ]; + // Use live data hook + const { loading, error, forceRefresh } = useLiveOrderbooks(); + // Subscribe to arbitrage opportunities from store useEffect(() => { - setArbitrageData(mockArbitrageData); + const updateArbitrageData = () => { + const store = getStore(); + setArbitrageData(store.arbitrageOpportunities); + }; + + // Initial load + updateArbitrageData(); + + // Subscribe to updates + const unsubscribe = subscribeToStore(updateArbitrageData); + return unsubscribe; }, []); const filteredData = arbitrageData.filter(item => - item.event.toLowerCase().includes(searchTerm.toLowerCase()) || + item.eventName.toLowerCase().includes(searchTerm.toLowerCase()) || item.market.toLowerCase().includes(searchTerm.toLowerCase()) ); - const toggleRowExpansion = (id) => { + const toggleRowExpansion = (id: string) => { setExpandedRows(prev => prev.includes(id) ? prev.filter(rowId => rowId !== id) @@ -146,27 +41,66 @@ const ArbitrageBets = () => { ); }; - const ExpandedConditions = ({ item }) => ( + const handleRefresh = () => { + forceRefresh(); + }; + + const formatTimeAgo = (date: Date): string => { + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffMin = Math.floor(diffMs / (1000 * 60)); + + if (diffMin < 1) return 'Just now'; + if (diffMin === 1) return '1 min ago'; + if (diffMin < 60) return `${diffMin} min ago`; + + const diffHours = Math.floor(diffMin / 60); + if (diffHours === 1) return '1 hour ago'; + if (diffHours < 24) return `${diffHours} hours ago`; + + const diffDays = Math.floor(diffHours / 24); + return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`; + }; + + const formatDate = (date: Date): string => { + return date.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: date.getFullYear() !== new Date().getFullYear() ? 'numeric' : undefined + }); + }; + + const ExpandedConditions = ({ item }: { item: ArbitrageOpportunity }) => (
- {/* Kalshi Conditions */} + {/* All Conditions */}
-

Kalshi Conditions

+

All Market Conditions

+ - {item.kalshiConditions.map((condition, idx) => ( + {item.allConditions.map((condition, idx) => ( - + + @@ -175,27 +109,41 @@ const ArbitrageBets = () => {
Platform Condition Yes Price No Price
{condition.name} + + {condition.platform === 'kalshi' ? 'Kalshi' : 'Polymarket'} + + {condition.condition} {(condition.yesPrice * 100).toFixed(0)}¢ {(condition.noPrice * 100).toFixed(0)}¢
- {/* Polymarket Conditions */} + {/* Arbitrage Details */}
-

Polymarket Conditions

- - - - - - - - - - {item.polymarketConditions.map((condition, idx) => ( - - - - - - ))} - -
ConditionYes PriceNo Price
{condition.name}{(condition.yesPrice * 100).toFixed(0)}¢{(condition.noPrice * 100).toFixed(0)}¢
+

Arbitrage Calculation

+
+
+
Best YES Price
+
+ {(item.minYes * 100).toFixed(0)}¢ + + ({item.yesBet.platform}) + +
+
+
+
Best NO Price
+
+ {(item.minNo * 100).toFixed(0)}¢ + + ({item.noBet.platform}) + +
+
+
+
Total Cost (C)
+
{(item.C * 100).toFixed(0)}¢
+
+
+
Period Return
+
{(item.periodReturn * 100).toFixed(2)}%
+
+
+
Days to Close
+
{item.daysUntilClose} days
+
+
@@ -226,8 +174,8 @@ const ArbitrageBets = () => { />
- @@ -235,6 +183,32 @@ const ArbitrageBets = () => { {/* Main Content */}
+ {/* Error Display */} + {error && ( +
+
+ Connection Error: {error} +
+
+ )} + + {/* Loading State */} + {loading && arbitrageData.length === 0 && ( +
+ +
Loading arbitrage opportunities...
+
+ )} + + {/* No Data State */} + {!loading && arbitrageData.length === 0 && !error && ( +
+
No arbitrage opportunities found
+
+ Opportunities appear when matched markets have profitable price differences +
+
+ )} {/* Info Bar */}
@@ -253,102 +227,116 @@ const ArbitrageBets = () => {
{/* Table */} -
- - - - - - - - - - - - - - - - - {filteredData.map((item) => ( - - toggleRowExpansion(item.id)} - > - - - - - + + + + + {expandedRows.includes(item.id) && } + + ))} + +
Percent ↓Event NameStart TimeMarketBetsOddsStakeProfitUpdated
-
- - - {item.percentage}% -
-
-
{item.event}
-
{item.eventType}
-
- {item.startTime} - - {item.market} - -
- {item.bets.map((bet, i) => ( -
- {i === 0 ? ( - {bet.outcome} - ) : ( - {bet.outcome} - )} + {arbitrageData.length > 0 && ( +
+ + + + + + + + + + + + + + + + + {filteredData.map((item) => ( + + toggleRowExpansion(item.id)} + > + + + + + + - + - - - - - - {expandedRows.includes(item.id) && } - - ))} - -
APR ↓Event NameEnd TimeDays LeftMarketBetsOddsProfit on $100Updated
+
+ + + {(item.apr * 100).toFixed(1)}% +
+
+
{item.eventName}
+
{item.eventType}
+
+ {formatDate(item.earliestEndTime)} + + {item.daysUntilClose} + + {item.market} + +
+
+ {item.yesBet.condition} +
+
+ {item.noBet.condition}
- ))} -
-
-
- {item.bets.map((bet, i) => ( -
- - {bet.venue} +
+
+
+
+ + {item.yesBet.platform === 'kalshi' ? 'Kalshi' : 'Polymarket'} - {bet.odds} + {(item.yesBet.price * 100).toFixed(0)}¢
- ))} -
-
-
${item.totalStake}
-
-
+${item.profit}
-
- {item.updated} - - -
-
+
+ + {item.noBet.platform === 'kalshi' ? 'Kalshi' : 'Polymarket'} + + {(item.noBet.price * 100).toFixed(0)}¢ +
+
+
+
+${item.profitOn100.toFixed(2)}
+
+ {formatTimeAgo(item.updatedAt)} + + +
+
+ )} {/* Settings and Refresh buttons */}
-
diff --git a/src/ecosystem-matcher-component.tsx b/src/ecosystem-matcher-component.tsx index e00b2a1..1a408ac 100644 --- a/src/ecosystem-matcher-component.tsx +++ b/src/ecosystem-matcher-component.tsx @@ -1,194 +1,52 @@ import React, { useState, useEffect } from 'react'; import { Search, Check, X, CheckCircle, XCircle, Edit2 } from 'lucide-react'; +import { useMarketData } from './hooks/useLiveOrderbooks'; +import { + getStore, + subscribeToStore, + createEcosystem, + updateEcosystemConditions +} from './state/matches-store'; +import { + CommonMarket, + Ecosystem, + ConditionMapping, + RelationshipType, + Platform +} from './api/types'; const EcosystemMatcher = () => { const [activeTab, setActiveTab] = useState('manual'); - const [selectedMarkets, setSelectedMarkets] = useState([]); + const [selectedMarkets, setSelectedMarkets] = useState>([]); const [searchTermKalshi, setSearchTermKalshi] = useState(''); const [searchTermPoly, setSearchTermPoly] = useState(''); const [showConditionMatcher, setShowConditionMatcher] = useState(false); - const [selectedEcosystem, setSelectedEcosystem] = useState(null); - - // Mock data for markets - const [kalshiMarkets] = useState([ - { - id: 'FED-SEPT-2025', - title: 'Fed decision in September?', - category: 'Economics', - volume: 450000, - liquidity: 120000, - closeDate: '2025-09-18', - conditions: [ - { name: '25 bps decrease', yesPrice: 0.80, noPrice: 0.20 }, - { name: 'No change', yesPrice: 0.15, noPrice: 0.85 }, - { name: '25+ bps increase', yesPrice: 0.01, noPrice: 0.99 } - ] - }, - { - id: 'BTCPRICE-25DEC31', - title: 'Bitcoin above $100k by year end?', - category: 'Crypto', - volume: 890000, - liquidity: 250000, - closeDate: '2025-12-31', - conditions: [ - { name: 'Yes', yesPrice: 0.40, noPrice: 0.60 }, - { name: 'No', yesPrice: 0.60, noPrice: 0.40 } - ] - }, - { - id: 'ELECTION-2028-WINNER', - title: 'Presidential Election Winner 2028', - category: 'Politics', - volume: 6000000, - liquidity: 450000, - closeDate: '2028-11-05', - conditions: [ - { name: 'JD Vance', yesPrice: 0.28, noPrice: 0.72 }, - { name: 'Gavin Newsom', yesPrice: 0.13, noPrice: 0.87 }, - { name: 'Alexandria Ocasio-Cortez', yesPrice: 0.09, noPrice: 0.91 }, - { name: 'Pete Buttigieg', yesPrice: 0.07, noPrice: 0.93 }, - { name: 'Marco Rubio', yesPrice: 0.06, noPrice: 0.94 }, - { name: 'Andy Beshear', yesPrice: 0.05, noPrice: 0.95 }, - { name: 'Gretchen Whitmer', yesPrice: 0.04, noPrice: 0.96 } - ] - } - ]); - - const [polymarketMarkets] = useState([ - { - id: '0xfedrate092025', - title: 'Federal Reserve Rate Decision September 2025', - category: 'Macro', - volume: 420000, - liquidity: 110000, - closeDate: '2025-09-18', - conditions: [ - { name: 'Rate decrease', yesPrice: 0.78, noPrice: 0.22 }, - { name: 'No change', yesPrice: 0.16, noPrice: 0.84 }, - { name: 'Rate increase', yesPrice: 0.02, noPrice: 0.98 } - ] - }, - { - id: '0xbtc100keoy', - title: 'BTC above $100,000 by EOY', - category: 'Cryptocurrency', - volume: 670000, - liquidity: 180000, - closeDate: '2025-12-31', - conditions: [ - { name: 'Yes', yesPrice: 0.38, noPrice: 0.62 }, - { name: 'No', yesPrice: 0.62, noPrice: 0.38 } - ] - }, - { - id: '0x2028election', - title: '2028 US Presidential Election', - category: 'Politics', - volume: 5200000, - liquidity: 380000, - closeDate: '2028-11-05', - conditions: [ - { name: 'J.D. Vance', yesPrice: 0.27, noPrice: 0.73 }, - { name: 'Gavin Newsom', yesPrice: 0.14, noPrice: 0.86 }, - { name: 'AOC', yesPrice: 0.08, noPrice: 0.92 }, - { name: 'Pete Buttigieg', yesPrice: 0.07, noPrice: 0.93 }, - { name: 'Marco Rubio', yesPrice: 0.06, noPrice: 0.94 }, - { name: 'Andrew Beshear', yesPrice: 0.05, noPrice: 0.95 }, - { name: 'Gretchen Whitmer', yesPrice: 0.04, noPrice: 0.96 } - ] - } - ]); - - const [ecosystemHistory, setEcosystemHistory] = useState([ - { - id: 1, - name: 'Fed Rate Decision - September 2025', - createdDate: '2025-08-01', - exchanges: 4, - markets: 6, - conditions: 13, - status: 'active', - conditionsMatched: true, - conditionMappings: [ - { - conditions: { - 'Kalshi-Fed decision in September?': '25 bps decrease', - 'Polymarket-Federal Reserve Rate Decision September 2025': 'Rate decrease', - 'ProphetX-FOMC September Outcome': 'Dovish', - 'NoVig-Fed Rates Sep': null - }, - relationship: 'same' - }, - { - conditions: { - 'Kalshi-Fed decision in September?': 'No change', - 'Polymarket-Federal Reserve Rate Decision September 2025': 'No change', - 'ProphetX-FOMC September Outcome': 'Neutral', - 'NoVig-Fed Rates Sep': null - }, - relationship: 'same' - } - ], - marketData: [ - { - exchange: 'Kalshi', - market: 'Fed decision in September?', - conditions: [ - { name: '25 bps decrease', yesPrice: 0.80, noPrice: 0.20 }, - { name: 'No change', yesPrice: 0.15, noPrice: 0.85 }, - { name: '25+ bps increase', yesPrice: 0.01, noPrice: 0.99 } - ] - }, - { - exchange: 'Polymarket', - market: 'Federal Reserve Rate Decision September 2025', - conditions: [ - { name: 'Rate decrease', yesPrice: 0.78, noPrice: 0.22 }, - { name: 'No change', yesPrice: 0.16, noPrice: 0.84 }, - { name: 'Rate increase', yesPrice: 0.02, noPrice: 0.98 } - ] - }, - { - exchange: 'ProphetX', - market: 'FOMC September Outcome', - conditions: [ - { name: 'Hawkish', yesPrice: 0.40, noPrice: 0.60 }, - { name: 'Dovish', yesPrice: 0.35, noPrice: 0.65 }, - { name: 'Neutral', yesPrice: 0.25, noPrice: 0.75 } - ] - }, - { - exchange: 'NoVig', - market: 'Fed Rates Sep', - conditions: [ - { name: 'Increase', yesPrice: 0.38, noPrice: 0.62 } - ] - } - ] - }, - { - id: 2, - name: '2028 Presidential Election', - createdDate: '2025-07-15', - exchanges: 3, - markets: 5, - conditions: 21, - status: 'active', - conditionsMatched: false, - conditionMappings: [], - marketData: [] - } - ]); + const [selectedEcosystem, setSelectedEcosystem] = useState(null); + const [ecosystemHistory, setEcosystemHistory] = useState([]); + + // Use live market data + const { kalshiMarkets, polymarketMarkets, loading, error } = useMarketData(); + + // Subscribe to ecosystem history from store + useEffect(() => { + const updateEcosystemHistory = () => { + const store = getStore(); + setEcosystemHistory(store.ecosystems); + }; + + updateEcosystemHistory(); + const unsubscribe = subscribeToStore(updateEcosystemHistory); + return unsubscribe; + }, []); const EcosystemConditionMatcher = () => { - const [mappings, setMappings] = useState( + const [mappings, setMappings] = useState( selectedEcosystem?.conditionMappings || [] ); - const [selectedConditions, setSelectedConditions] = useState({}); - const [selectedRelationship, setSelectedRelationship] = useState('same'); + const [selectedConditions, setSelectedConditions] = useState>({}); + const [selectedRelationship, setSelectedRelationship] = useState('same'); - const relationshipTypes = [ + const relationshipTypes: Array<{ value: RelationshipType; label: string; color: string }> = [ { value: 'same', label: 'Same', color: 'text-green-400' }, { value: 'subset', label: 'Subset', color: 'text-blue-400' }, { value: 'mutually-exclusive', label: 'Mutually Exclusive', color: 'text-orange-400' }, @@ -199,9 +57,9 @@ const EcosystemMatcher = () => { // Initialize selected conditions for each market useEffect(() => { - const initialConditions = {}; - selectedEcosystem?.marketData.forEach(market => { - const key = `${market.exchange}-${market.market}`; + const initialConditions: Record = {}; + selectedEcosystem?.marketRefs.forEach(marketRef => { + const key = `${marketRef.platform}-${marketRef.marketId}`; initialConditions[key] = ''; }); setSelectedConditions(initialConditions); @@ -215,36 +73,45 @@ const EcosystemMatcher = () => { return; } - const newMapping = { - conditions: { ...selectedConditions }, - relationship: selectedRelationship + const newMapping: ConditionMapping = { + id: `mapping-${Date.now()}`, + relationship: selectedRelationship, + confidence: 1.0, + createdAt: new Date(), + // Store all selected conditions in a format that matches our types + kalshiCondition: Object.entries(selectedConditions) + .filter(([key, value]) => key.startsWith('kalshi-') && value) + .map(([key, value]) => `${key}: ${value}`) + .join(', '), + polymarketCondition: Object.entries(selectedConditions) + .filter(([key, value]) => key.startsWith('polymarket-') && value) + .map(([key, value]) => `${key}: ${value}`) + .join(', '), }; setMappings([...mappings, newMapping]); // Reset selections - const resetConditions = {}; + const resetConditions: Record = {}; Object.keys(selectedConditions).forEach(key => { resetConditions[key] = ''; }); setSelectedConditions(resetConditions); }; - const removeMapping = (index) => { + const removeMapping = (index: number) => { setMappings(mappings.filter((_, i) => i !== index)); }; const saveConditionMappings = () => { - setEcosystemHistory(ecosystemHistory.map(ecosystem => - ecosystem.id === selectedEcosystem.id - ? { ...ecosystem, conditionMappings: mappings, conditionsMatched: mappings.length > 0 } - : ecosystem - )); + if (selectedEcosystem) { + updateEcosystemConditions(selectedEcosystem.id, mappings); + } setShowConditionMatcher(false); setSelectedEcosystem(null); }; - const getRelationshipColor = (relationship) => { + const getRelationshipColor = (relationship: RelationshipType) => { const rel = relationshipTypes.find(r => r.value === relationship); return rel ? rel.color : 'text-gray-400'; }; @@ -417,23 +284,23 @@ const EcosystemMatcher = () => { market.id.toLowerCase().includes(searchTermPoly.toLowerCase()) ); - const toggleMarketSelection = (market, exchange) => { - const marketWithExchange = { ...market, exchange }; - const marketId = `${exchange}-${market.id}`; + const toggleMarketSelection = (market: CommonMarket, platform: Platform) => { + const marketWithPlatform = { market, platform }; + const marketId = `${platform}-${market.id}`; setSelectedMarkets(prev => { - const isSelected = prev.some(m => `${m.exchange}-${m.id}` === marketId); + const isSelected = prev.some(m => `${m.platform}-${m.market.id}` === marketId); if (isSelected) { - return prev.filter(m => `${m.exchange}-${m.id}` !== marketId); + return prev.filter(m => `${m.platform}-${m.market.id}` !== marketId); } else { - return [...prev, marketWithExchange]; + return [...prev, marketWithPlatform]; } }); }; - const isMarketSelected = (market, exchange) => { - const marketId = `${exchange}-${market.id}`; - return selectedMarkets.some(m => `${m.exchange}-${m.id}` === marketId); + const isMarketSelected = (market: CommonMarket, platform: Platform) => { + const marketId = `${platform}-${market.id}`; + return selectedMarkets.some(m => `${m.platform}-${m.market.id}` === marketId); }; const handleCreateEcosystem = () => { @@ -442,30 +309,11 @@ const EcosystemMatcher = () => { return; } - const exchanges = new Set(selectedMarkets.map(m => m.exchange)).size; - const totalConditions = selectedMarkets.reduce((sum, m) => sum + m.conditions.length, 0); + const ecosystemName = `Ecosystem - ${new Date().toLocaleDateString()}`; + const newEcosystem = createEcosystem(ecosystemName, selectedMarkets); - // Create new ecosystem - const newEcosystem = { - id: Date.now(), - name: `New Ecosystem - ${new Date().toLocaleDateString()}`, - createdDate: new Date().toISOString().split('T')[0], - exchanges: exchanges, - markets: selectedMarkets.length, - conditions: totalConditions, - status: 'active', - conditionsMatched: false, - conditionMappings: [], - marketData: selectedMarkets.map(m => ({ - exchange: m.exchange, - market: m.title, - conditions: m.conditions - })) - }; - - setEcosystemHistory([newEcosystem, ...ecosystemHistory]); setSelectedMarkets([]); - alert(`Created ecosystem with ${selectedMarkets.length} markets from ${exchanges} exchange(s)`); + alert(`Created ecosystem "${newEcosystem.name}" with ${selectedMarkets.length} markets from ${newEcosystem.exchanges} exchange(s)`); }; return ( @@ -479,6 +327,15 @@ const EcosystemMatcher = () => {
+ {/* Error Display */} + {error && ( +
+
+ Connection Error: {error} +
+
+ )} + {/* Tabs */}
- {filteredKalshiMarkets.map(market => ( -
toggleMarketSelection(market, 'Kalshi')} - className={`p-3 bg-gray-800 rounded cursor-pointer hover:bg-gray-700 transition-colors ${ - isMarketSelected(market, 'Kalshi') ? 'ring-2 ring-blue-500 bg-gray-700' : '' - }`} - > -
-
-
{market.title}
-
- {market.category} • ${(market.volume / 1000).toFixed(0)}k vol • {market.conditions.length} conditions + {loading ? ( +
+
+ Loading Kalshi markets... +
+ ) : filteredKalshiMarkets.length === 0 ? ( +
+ No markets found +
+ ) : ( + filteredKalshiMarkets.map(market => ( +
toggleMarketSelection(market, 'kalshi')} + className={`p-3 bg-gray-800 rounded cursor-pointer hover:bg-gray-700 transition-colors ${ + isMarketSelected(market, 'kalshi') ? 'ring-2 ring-blue-500 bg-gray-700' : '' + }`} + > +
+
+
{market.title}
+
+ {market.category} • ${((market.volume || 0) / 1000).toFixed(0)}k vol • {market.outcomes.length} conditions +
+
Closes {market.closeTime.toLocaleDateString()}
+
+
+ {isMarketSelected(market, 'kalshi') && ( + + )}
-
Closes {market.closeDate}
-
-
- {isMarketSelected(market, 'Kalshi') && ( - - )}
-
- ))} + )) + )}
@@ -587,34 +455,45 @@ const EcosystemMatcher = () => {
- {filteredPolyMarkets.map(market => ( -
toggleMarketSelection(market, 'Polymarket')} - className={`p-3 bg-gray-800 rounded cursor-pointer hover:bg-gray-700 transition-colors ${ - isMarketSelected(market, 'Polymarket') ? 'ring-2 ring-purple-500 bg-gray-700' : '' - }`} - > -
-
-
{market.title}
-
- {market.category} • ${(market.volume / 1000).toFixed(0)}k vol • {market.conditions.length} conditions + {loading ? ( +
+
+ Loading Polymarket markets... +
+ ) : filteredPolyMarkets.length === 0 ? ( +
+ No markets found +
+ ) : ( + filteredPolyMarkets.map(market => ( +
toggleMarketSelection(market, 'polymarket')} + className={`p-3 bg-gray-800 rounded cursor-pointer hover:bg-gray-700 transition-colors ${ + isMarketSelected(market, 'polymarket') ? 'ring-2 ring-purple-500 bg-gray-700' : '' + }`} + > +
+
+
{market.title}
+
+ {market.category} • ${((market.volume || 0) / 1000).toFixed(0)}k vol • {market.outcomes.length} conditions +
+
Closes {market.closeTime.toLocaleDateString()}
+
+
+ {isMarketSelected(market, 'polymarket') && ( + + )}
-
Closes {market.closeDate}
-
-
- {isMarketSelected(market, 'Polymarket') && ( - - )}
-
- ))} + )) + )}
@@ -624,23 +503,23 @@ const EcosystemMatcher = () => {

Selected Markets for Ecosystem

- {selectedMarkets.map((market) => ( -
+ {selectedMarkets.map((selectedMarket) => ( +
- {market.exchange} + {selectedMarket.platform === 'kalshi' ? 'Kalshi' : 'Polymarket'}
-
{market.title}
-
{market.conditions.length} conditions
+
{selectedMarket.market.title}
+
{selectedMarket.market.outcomes.length} conditions
Rules
-

{selectedMarkets.kalshi.rules}

+

{selectedMarkets.kalshi.rules || 'No rules specified'}

Settlement Source
-
{selectedMarkets.kalshi.settlementSource}
+
{selectedMarkets.kalshi.settlementSource || 'Not specified'}
Conditions
-
{selectedMarkets.kalshi.conditions.length} outcomes
+
{selectedMarkets.kalshi.outcomes.length} outcomes
@@ -542,15 +411,15 @@ const MarketMatcher = () => {
Rules
-

{selectedMarkets.polymarket.rules}

+

{selectedMarkets.polymarket.rules || 'No rules specified'}

Settlement Source
-
{selectedMarkets.polymarket.settlementSource}
+
{selectedMarkets.polymarket.settlementSource || 'Not specified'}
Conditions
-
{selectedMarkets.polymarket.conditions.length} outcomes
+
{selectedMarkets.polymarket.outcomes.length} outcomes
@@ -563,7 +432,7 @@ const MarketMatcher = () => {
Kalshi Conditions:
- {selectedMarkets.kalshi.conditions.map((condition, idx) => ( + {selectedMarkets.kalshi.outcomes.map((condition, idx) => (
• {condition.name} (Y: {(condition.yesPrice * 100).toFixed(0)}¢)
@@ -573,7 +442,7 @@ const MarketMatcher = () => {
Polymarket Conditions:
- {selectedMarkets.polymarket.conditions.map((condition, idx) => ( + {selectedMarkets.polymarket.outcomes.map((condition, idx) => (
• {condition.name} (Y: {(condition.yesPrice * 100).toFixed(0)}¢)
@@ -640,6 +509,15 @@ const MarketMatcher = () => {
+ {/* Error Display */} + {error && ( +
+
+ Connection Error: {error} +
+
+ )} + {/* Tabs */}
- {filteredPolyMarkets.map(market => ( -
setSelectedMarkets(prev => ({ ...prev, polymarket: market }))} - className={`p-3 bg-gray-800 rounded cursor-pointer hover:bg-gray-700 transition-colors ${ - selectedMarkets.polymarket?.id === market.id ? 'ring-2 ring-purple-500' : '' - }`} - > -
{market.title}
-
-
- {market.category} • ${(market.volume / 1000).toFixed(0)}k vol • {market.conditions.length} conditions -
-
- {market.conditions.slice(0, 3).map((condition, idx) => ( - - {condition.name}: {(condition.yesPrice * 100).toFixed(0)}¢ - - ))} - {market.conditions.length > 3 && ( - +{market.conditions.length - 3} more - )} + {loading ? ( +
+ + Loading Polymarket markets... +
+ ) : filteredPolyMarkets.length === 0 ? ( +
+ No markets found +
+ ) : ( + filteredPolyMarkets.map(market => ( +
setSelectedMarkets(prev => ({ ...prev, polymarket: market }))} + className={`p-3 bg-gray-800 rounded cursor-pointer hover:bg-gray-700 transition-colors ${ + selectedMarkets.polymarket?.id === market.id ? 'ring-2 ring-purple-500' : '' + }`} + > +
{market.title}
+
+
+ {market.category} • ${((market.volume || 0) / 1000).toFixed(0)}k vol • {market.outcomes.length} conditions +
+
+ {market.outcomes.slice(0, 3).map((condition, idx) => ( + + {condition.name}: {(condition.yesPrice * 100).toFixed(0)}¢ + + ))} + {market.outcomes.length > 3 && ( + +{market.outcomes.length - 3} more + )} +
+
Closes {market.closeTime.toLocaleDateString()}
-
Closes {market.closeDate}
-
- ))} + )) + )}
@@ -776,12 +684,12 @@ const MarketMatcher = () => {
Kalshi Market
{selectedMarkets.kalshi.title}
-
{selectedMarkets.kalshi.conditions.length} conditions
+
{selectedMarkets.kalshi.outcomes.length} conditions
Polymarket Market
{selectedMarkets.polymarket.title}
-
{selectedMarkets.polymarket.conditions.length} conditions
+
{selectedMarkets.polymarket.outcomes.length} conditions
@@ -971,67 +879,73 @@ const MarketMatcher = () => { - {matchHistory.map(match => ( - - -
{match.kalshi.title}
-
{match.kalshi.id}
-
{match.kalshi.conditions.length} conditions
- - -
{match.polymarket.title}
-
{match.polymarket.id}
-
{match.polymarket.conditions.length} conditions
- - - {match.matchDate} - - - - - - ${(match.totalVolume / 1000).toFixed(0)}k - - -
- {match.conditionsMatched ? ( - <> - - Yes - - ) : ( - <> - - No - - )} -
- - -
- + +
+ + + ); + })}
diff --git a/src/matched-ecosystems-component.tsx b/src/matched-ecosystems-component.tsx index c9a1f96..64b4746 100644 --- a/src/matched-ecosystems-component.tsx +++ b/src/matched-ecosystems-component.tsx @@ -1,493 +1,42 @@ import React, { useState, useEffect } from 'react'; import { Search, RefreshCw, ChevronDown, Edit2, CheckCircle, XCircle, X } from 'lucide-react'; +import { + getStore, + subscribeToStore, + updateEcosystemConditions +} from './state/matches-store'; +import { + Ecosystem, + ConditionMapping, + RelationshipType, + CommonMarket +} from './api/types'; const MatchedEcosystems = () => { - const [selectedItems, setSelectedItems] = useState([]); - const [expandedRows, setExpandedRows] = useState([]); + const [selectedItems, setSelectedItems] = useState([]); + const [expandedRows, setExpandedRows] = useState([]); const [searchTerm, setSearchTerm] = useState(''); const [showConditionMatcher, setShowConditionMatcher] = useState(false); - const [selectedEcosystem, setSelectedEcosystem] = useState(null); + const [selectedEcosystem, setSelectedEcosystem] = useState(null); + const [ecosystemData, setEcosystemData] = useState([]); - // Mock data for ecosystem matching - const [ecosystemData, setEcosystemData] = useState([ - { - id: 1, - name: 'Fed Rate Decision - September 2025', - endTime: 'Sep 18, 2025', - daysUntilClose: 42, - exchanges: 4, - markets: 6, - conditions: 13, - updated: '2 min ago', - conditionsMatched: true, - conditionMappings: [ - { - conditions: { - 'Kalshi-Fed September Decision': '25 bps increase', - 'Polymarket-Federal Reserve Rate Decision': 'Rate increase', - 'Kalshi-Fed Hike by 50bps+': null, - 'ProphetX-FOMC September Outcome': null, - 'NoVig-Fed Rates Sep': 'Increase', - 'Polymarket-Fed Terminal Rate >5.5%': null - }, - relationship: 'same' - }, - { - conditions: { - 'Kalshi-Fed September Decision': 'No change', - 'Polymarket-Federal Reserve Rate Decision': 'No change', - 'Kalshi-Fed Hike by 50bps+': null, - 'ProphetX-FOMC September Outcome': 'Neutral', - 'NoVig-Fed Rates Sep': null, - 'Polymarket-Fed Terminal Rate >5.5%': null - }, - relationship: 'same' - } - ], - marketData: [ - { - exchange: 'Kalshi', - market: 'Fed September Decision', - conditions: [ - { name: '25 bps increase', yesPrice: 0.35, noPrice: 0.65 }, - { name: 'No change', yesPrice: 0.45, noPrice: 0.55 }, - { name: '25 bps decrease', yesPrice: 0.18, noPrice: 0.82 }, - { name: '50+ bps increase', yesPrice: 0.02, noPrice: 0.98 } - ] - }, - { - exchange: 'Polymarket', - market: 'Federal Reserve Rate Decision', - conditions: [ - { name: 'Rate increase', yesPrice: 0.37, noPrice: 0.63 }, - { name: 'No change', yesPrice: 0.44, noPrice: 0.56 }, - { name: 'Rate decrease', yesPrice: 0.19, noPrice: 0.81 } - ] - }, - { - exchange: 'Kalshi', - market: 'Fed Hike by 50bps+', - conditions: [ - { name: 'Yes', yesPrice: 0.02, noPrice: 0.98 }, - { name: 'No', yesPrice: 0.98, noPrice: 0.02 } - ] - }, - { - exchange: 'ProphetX', - market: 'FOMC September Outcome', - conditions: [ - { name: 'Hawkish', yesPrice: 0.40, noPrice: 0.60 }, - { name: 'Dovish', yesPrice: 0.35, noPrice: 0.65 }, - { name: 'Neutral', yesPrice: 0.25, noPrice: 0.75 } - ] - }, - { - exchange: 'NoVig', - market: 'Fed Rates Sep', - conditions: [ - { name: 'Increase', yesPrice: 0.38, noPrice: 0.62 } - ] - }, - { - exchange: 'Polymarket', - market: 'Fed Terminal Rate >5.5%', - conditions: [ - { name: 'Yes', yesPrice: 0.15, noPrice: 0.85 }, - { name: 'No', yesPrice: 0.85, noPrice: 0.15 } - ] - } - ] - }, - { - id: 2, - name: '2028 Presidential Election', - endTime: 'Nov 5, 2028', - daysUntilClose: 1185, - exchanges: 3, - markets: 5, - conditions: 21, - updated: '5 min ago', - conditionsMatched: false, - conditionMappings: [], - marketData: [ - { - exchange: 'Kalshi', - market: 'Presidential Election Winner 2028', - conditions: [ - { name: 'JD Vance', yesPrice: 0.28, noPrice: 0.72 }, - { name: 'Gavin Newsom', yesPrice: 0.13, noPrice: 0.87 }, - { name: 'Alexandria Ocasio-Cortez', yesPrice: 0.09, noPrice: 0.91 }, - { name: 'Pete Buttigieg', yesPrice: 0.07, noPrice: 0.93 }, - { name: 'Marco Rubio', yesPrice: 0.06, noPrice: 0.94 }, - { name: 'Andy Beshear', yesPrice: 0.05, noPrice: 0.95 }, - { name: 'Gretchen Whitmer', yesPrice: 0.04, noPrice: 0.96 } - ] - }, - { - exchange: 'Polymarket', - market: '2028 US Presidential Election', - conditions: [ - { name: 'J.D. Vance', yesPrice: 0.27, noPrice: 0.73 }, - { name: 'Gavin Newsom', yesPrice: 0.14, noPrice: 0.86 }, - { name: 'AOC', yesPrice: 0.08, noPrice: 0.92 }, - { name: 'Pete Buttigieg', yesPrice: 0.07, noPrice: 0.93 }, - { name: 'Marco Rubio', yesPrice: 0.06, noPrice: 0.94 }, - { name: 'Andrew Beshear', yesPrice: 0.05, noPrice: 0.95 }, - { name: 'Gretchen Whitmer', yesPrice: 0.04, noPrice: 0.96 } - ] - }, - { - exchange: 'Kalshi', - market: 'Republican Nominee 2028', - conditions: [ - { name: 'JD Vance', yesPrice: 0.52, noPrice: 0.48 }, - { name: 'Marco Rubio', yesPrice: 0.15, noPrice: 0.85 }, - { name: 'Other', yesPrice: 0.33, noPrice: 0.67 } - ] - }, - { - exchange: 'Kalshi', - market: 'Democratic Nominee 2028', - conditions: [ - { name: 'Gavin Newsom', yesPrice: 0.35, noPrice: 0.65 }, - { name: 'Gretchen Whitmer', yesPrice: 0.18, noPrice: 0.82 }, - { name: 'Other', yesPrice: 0.47, noPrice: 0.53 } - ] - }, - { - exchange: 'Polymarket', - market: 'Party to Win 2028', - conditions: [ - { name: 'Republican', yesPrice: 0.48, noPrice: 0.52 }, - { name: 'Democrat', yesPrice: 0.52, noPrice: 0.48 } - ] - } - ] - }, - { - id: 3, - name: 'Bitcoin Price EOY 2025', - endTime: 'Dec 31, 2025', - daysUntilClose: 145, - exchanges: 5, - markets: 8, - conditions: 15, - updated: '8 min ago', - conditionsMatched: true, - conditionMappings: [ - { - conditions: { - 'Kalshi-Bitcoin > $100k': 'Yes', - 'Polymarket-BTC above $100,000 by EOY': 'Yes', - 'Kalshi-Bitcoin > $150k': null, - 'Polymarket-BTC Price Range EOY': '$100k-$150k', - 'ProphetX-Bitcoin EOY Price': 'Above $100k', - 'NoVig-BTC 100k EOY': 'Yes', - 'Sporttrade-Bitcoin Year End': 'Over $100,000', - 'Kalshi-Bitcoin ATH in 2025': null - }, - relationship: 'same' - } - ], - marketData: [ - { - exchange: 'Kalshi', - market: 'Bitcoin > $100k', - conditions: [ - { name: 'Yes', yesPrice: 0.40, noPrice: 0.60 }, - { name: 'No', yesPrice: 0.60, noPrice: 0.40 } - ] - }, - { - exchange: 'Polymarket', - market: 'BTC above $100,000 by EOY', - conditions: [ - { name: 'Yes', yesPrice: 0.38, noPrice: 0.62 }, - { name: 'No', yesPrice: 0.62, noPrice: 0.38 } - ] - }, - { - exchange: 'Kalshi', - market: 'Bitcoin > $150k', - conditions: [ - { name: 'Yes', yesPrice: 0.15, noPrice: 0.85 }, - { name: 'No', yesPrice: 0.85, noPrice: 0.15 } - ] - }, - { - exchange: 'Polymarket', - market: 'BTC Price Range EOY', - conditions: [ - { name: '<$50k', yesPrice: 0.05, noPrice: 0.95 }, - { name: '$50k-$100k', yesPrice: 0.57, noPrice: 0.43 }, - { name: '$100k-$150k', yesPrice: 0.25, noPrice: 0.75 }, - { name: '>$150k', yesPrice: 0.13, noPrice: 0.87 } - ] - }, - { - exchange: 'ProphetX', - market: 'Bitcoin EOY Price', - conditions: [ - { name: 'Above $100k', yesPrice: 0.39, noPrice: 0.61 } - ] - }, - { - exchange: 'NoVig', - market: 'BTC 100k EOY', - conditions: [ - { name: 'Yes', yesPrice: 0.41, noPrice: 0.59 }, - { name: 'No', yesPrice: 0.59, noPrice: 0.41 } - ] - }, - { - exchange: 'Sporttrade', - market: 'Bitcoin Year End', - conditions: [ - { name: 'Over $100,000', yesPrice: 0.37, noPrice: 0.63 } - ] - }, - { - exchange: 'Kalshi', - market: 'Bitcoin ATH in 2025', - conditions: [ - { name: 'Yes', yesPrice: 0.78, noPrice: 0.22 }, - { name: 'No', yesPrice: 0.22, noPrice: 0.78 } - ] - } - ] - } - ]); - - const EcosystemConditionMatcher = () => { - const [mappings, setMappings] = useState( - selectedEcosystem?.conditionMappings || [] - ); - const [selectedConditions, setSelectedConditions] = useState({}); - const [selectedRelationship, setSelectedRelationship] = useState('same'); - - const relationshipTypes = [ - { value: 'same', label: 'Same', color: 'text-green-400' }, - { value: 'subset', label: 'Subset', color: 'text-blue-400' }, - { value: 'mutually-exclusive', label: 'Mutually Exclusive', color: 'text-orange-400' }, - { value: 'complementary', label: 'Complementary', color: 'text-purple-400' }, - { value: 'opposites', label: 'Opposites', color: 'text-red-400' }, - { value: 'overlapping', label: 'Overlapping', color: 'text-yellow-400' } - ]; - - // Initialize selected conditions for each market - useEffect(() => { - const initialConditions = {}; - selectedEcosystem?.marketData.forEach(market => { - const key = `${market.exchange}-${market.market}`; - initialConditions[key] = ''; - }); - setSelectedConditions(initialConditions); - }, []); - - const addMapping = () => { - // Check if at least one condition is selected - const hasSelection = Object.values(selectedConditions).some(val => val !== ''); - if (!hasSelection) { - alert('Please select at least one condition'); - return; - } - - const newMapping = { - conditions: { ...selectedConditions }, - relationship: selectedRelationship - }; - - setMappings([...mappings, newMapping]); - - // Reset selections - const resetConditions = {}; - Object.keys(selectedConditions).forEach(key => { - resetConditions[key] = ''; - }); - setSelectedConditions(resetConditions); - }; - - const removeMapping = (index) => { - setMappings(mappings.filter((_, i) => i !== index)); - }; - - const saveConditionMappings = () => { - setEcosystemData(ecosystemData.map(ecosystem => - ecosystem.id === selectedEcosystem.id - ? { ...ecosystem, conditionMappings: mappings, conditionsMatched: mappings.length > 0 } - : ecosystem - )); - setShowConditionMatcher(false); - setSelectedEcosystem(null); + // Subscribe to ecosystem data from store + useEffect(() => { + const updateEcosystemData = () => { + const store = getStore(); + setEcosystemData(store.ecosystems); }; - const getRelationshipColor = (relationship) => { - const rel = relationshipTypes.find(r => r.value === relationship); - return rel ? rel.color : 'text-gray-400'; - }; - - return ( -
-
-
-

Match Ecosystem Conditions

- -
- -
-
-
Matching conditions for:
-
{selectedEcosystem?.name}
-
- {selectedEcosystem?.marketData.length} markets across {selectedEcosystem?.exchanges} exchanges -
-
-
- - {/* Conditions Grid */} -
-
-

Market Conditions

-
- {selectedEcosystem?.marketData.map((market, idx) => ( -
-
- {market.exchange} -
-
{market.market}
-
- {market.conditions.map((condition, condIdx) => ( -
-
{condition.name}
-
- Y: {(condition.yesPrice * 100).toFixed(0)}¢ / N: {(condition.noPrice * 100).toFixed(0)}¢ -
-
- ))} -
-
- ))} -
-
-
- - {/* Mapping Interface */} -
-

Add Condition Mapping

- - {/* Relationship Selection */} -
- - -
- - {/* Condition Dropdowns */} -
- {selectedEcosystem?.marketData.map((market, idx) => { - const key = `${market.exchange}-${market.market}`; - return ( -
- - -
- ); - })} -
- - -
- - {/* Current Mappings */} - {mappings.length > 0 && ( -
-

Current Mappings

-
- {mappings.map((mapping, idx) => ( -
-
-
- {relationshipTypes.find(r => r.value === mapping.relationship)?.label} Relationship -
- -
-
- {Object.entries(mapping.conditions).map(([market, condition], condIdx) => ( -
-
{market}
-
{condition || '-'}
-
- ))} -
-
- ))} -
-
- )} - -
- - -
-
-
- ); - }; + updateEcosystemData(); + const unsubscribe = subscribeToStore(updateEcosystemData); + return unsubscribe; + }, []); const filteredData = ecosystemData.filter(item => item.name.toLowerCase().includes(searchTerm.toLowerCase()) ); - const toggleRowExpansion = (id) => { + const toggleRowExpansion = (id: string) => { setExpandedRows(prev => prev.includes(id) ? prev.filter(rowId => rowId !== id) @@ -495,7 +44,7 @@ const MatchedEcosystems = () => { ); }; - const toggleSelection = (id) => { + const toggleSelection = (id: string) => { setSelectedItems(prev => prev.includes(id) ? prev.filter(itemId => itemId !== id) @@ -505,83 +54,89 @@ const MatchedEcosystems = () => { const handleApproveEcosystem = () => { if (selectedItems.length === 0) { - alert('Please select at least one market to create an ecosystem'); + alert('Please select at least one ecosystem'); return; } - alert(`Creating ecosystem with ${selectedItems.length} markets`); + alert(`Selected ${selectedItems.length} ecosystem${selectedItems.length > 1 ? 's' : ''}`); setSelectedItems([]); }; - const ExpandedEcosystem = ({ item }) => ( + const calculateDaysUntilClose = (ecosystem: Ecosystem): number => { + if (!ecosystem.earliestEndTime) return 0; + const now = new Date(); + const diffTime = ecosystem.earliestEndTime.getTime() - now.getTime(); + return Math.max(0, Math.ceil(diffTime / (1000 * 60 * 60 * 24))); + }; + + const formatTimeAgo = (date: Date): string => { + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffMin = Math.floor(diffMs / (1000 * 60)); + + if (diffMin < 1) return 'Just now'; + if (diffMin === 1) return '1 min ago'; + if (diffMin < 60) return `${diffMin} min ago`; + + const diffHours = Math.floor(diffMin / 60); + if (diffHours === 1) return '1 hour ago'; + if (diffHours < 24) return `${diffHours} hours ago`; + + const diffDays = Math.floor(diffHours / 24); + return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`; + }; + + const ExpandedEcosystem = ({ item }: { item: Ecosystem }) => (
-

Market Conditions Across Exchanges

+

Market Overview

-
- - - - - {item.marketData.map((market, idx) => ( - - ))} - - - - {item.marketData.map((_, idx) => ( - - - - - ))} - - - - {(() => { - // Get all unique conditions - const allConditions = new Set(); - item.marketData.forEach(market => { - market.conditions.forEach(condition => { - allConditions.add(condition.name); - }); - }); - - return Array.from(allConditions).map((conditionName, idx) => ( - - - {item.marketData.map((market, marketIdx) => { - const condition = market.conditions.find(c => c.name === conditionName); - return ( - - - - - ); - })} - - )); - })()} - -
Condition -
{market.exchange}
-
{market.market}
-
YesNo
{conditionName} - {condition ? ( - {(condition.yesPrice * 100).toFixed(0)}¢ - ) : ( - - - )} - - {condition ? ( - {(condition.noPrice * 100).toFixed(0)}¢ - ) : ( - - - )} -
+
+ {item.marketRefs.map((marketRef, idx) => ( +
+
+ {marketRef.platform === 'kalshi' ? 'Kalshi' : 'Polymarket'} +
+
+ {marketRef.market?.title || marketRef.marketId} +
+ {marketRef.market && ( +
+
+ {marketRef.market.outcomes.length} conditions +
+
+ Closes: {marketRef.market.closeTime.toLocaleDateString()} +
+ {marketRef.market.outcomes.slice(0, 2).map((condition, condIdx) => ( +
+ {condition.name}: {(condition.yesPrice * 100).toFixed(0)}¢ +
+ ))} + {marketRef.market.outcomes.length > 2 && ( +
+ +{marketRef.market.outcomes.length - 2} more conditions +
+ )} +
+ )} +
+ ))}
+ {item.conditionMappings.length > 0 && ( +
+

Condition Mappings

+
+ {item.conditionMappings.length} mapping{item.conditionMappings.length > 1 ? 's' : ''} configured +
+
+ )}
@@ -684,10 +239,10 @@ const MatchedEcosystems = () => {
{item.name}
toggleRowExpansion(item.id)}> - {item.endTime} + {item.earliestEndTime?.toLocaleDateString() || 'Not set'} toggleRowExpansion(item.id)}> - {item.daysUntilClose} days + {calculateDaysUntilClose(item)} days toggleRowExpansion(item.id)}> {item.exchanges} @@ -714,7 +269,7 @@ const MatchedEcosystems = () => {
toggleRowExpansion(item.id)}> - {item.updated} + {formatTimeAgo(item.createdAt)}
@@ -745,7 +300,30 @@ const MatchedEcosystems = () => {
- {showConditionMatcher && } + {showConditionMatcher && selectedEcosystem && ( +
+
+
+

Edit Ecosystem Conditions

+ +
+
+
Condition mapping for "{selectedEcosystem.name}"
+
+ This feature will allow editing condition mappings between markets in the ecosystem. +
+ +
+
+
+ )}
); }; diff --git a/src/state/matches-store.ts b/src/state/matches-store.ts new file mode 100644 index 0000000..d9dce9c --- /dev/null +++ b/src/state/matches-store.ts @@ -0,0 +1,406 @@ +// Central store for matched markets and ecosystems with localStorage persistence +import { + MatchesStore, + CommonMarket, + MarketMatch, + Ecosystem, + ArbitrageOpportunity, + PollingConfig, + Platform +} from '../api/types'; + +// Default store state +const defaultState: MatchesStore = { + markets: { + kalshi: [], + polymarket: [], + lastUpdated: new Date(), + }, + matches: [], + ecosystems: [], + arbitrageOpportunities: [], + polling: { + kalshiInterval: 5000, // 5 seconds + polymarketInterval: 2500, // 2.5 seconds + enabled: true, + }, +}; + +// Storage keys +const STORAGE_KEYS = { + MATCHES: 'arber2_matches', + ECOSYSTEMS: 'arber2_ecosystems', + POLLING_CONFIG: 'arber2_polling_config', + LAST_UPDATED: 'arber2_last_updated', +} as const; + +/** + * Load persisted data from localStorage + */ +function loadFromStorage(): Partial { + try { + const stored: Partial = {}; + + // Load matches + const matchesData = localStorage.getItem(STORAGE_KEYS.MATCHES); + if (matchesData) { + const matches = JSON.parse(matchesData); + // Convert date strings back to Date objects + stored.matches = matches.map((match: any) => ({ + ...match, + createdAt: new Date(match.createdAt), + conditionMappings: match.conditionMappings.map((mapping: any) => ({ + ...mapping, + createdAt: new Date(mapping.createdAt), + })), + })); + } + + // Load ecosystems + const ecosystemsData = localStorage.getItem(STORAGE_KEYS.ECOSYSTEMS); + if (ecosystemsData) { + const ecosystems = JSON.parse(ecosystemsData); + stored.ecosystems = ecosystems.map((ecosystem: any) => ({ + ...ecosystem, + createdAt: new Date(ecosystem.createdAt), + earliestEndTime: ecosystem.earliestEndTime ? new Date(ecosystem.earliestEndTime) : undefined, + conditionMappings: ecosystem.conditionMappings.map((mapping: any) => ({ + ...mapping, + createdAt: new Date(mapping.createdAt), + })), + })); + } + + // Load polling config + const pollingData = localStorage.getItem(STORAGE_KEYS.POLLING_CONFIG); + if (pollingData) { + stored.polling = JSON.parse(pollingData); + } + + // Load last updated timestamp + const lastUpdatedData = localStorage.getItem(STORAGE_KEYS.LAST_UPDATED); + if (lastUpdatedData) { + stored.markets = { + kalshi: [], + polymarket: [], + lastUpdated: new Date(lastUpdatedData), + }; + } + + return stored; + } catch (error) { + console.error('Error loading from localStorage:', error); + return {}; + } +} + +/** + * Save data to localStorage + */ +function saveToStorage(key: keyof typeof STORAGE_KEYS, data: any): void { + try { + localStorage.setItem(STORAGE_KEYS[key], JSON.stringify(data)); + } catch (error) { + console.error(`Error saving ${key} to localStorage:`, error); + } +} + +// Initialize store with persisted data +let store: MatchesStore = { + ...defaultState, + ...loadFromStorage(), +}; + +// Subscribers for store updates +type StoreSubscriber = (store: MatchesStore) => void; +const subscribers = new Set(); + +/** + * Subscribe to store updates + */ +export function subscribeToStore(callback: StoreSubscriber): () => void { + subscribers.add(callback); + return () => subscribers.delete(callback); +} + +/** + * Notify all subscribers of store changes + */ +function notifySubscribers(): void { + subscribers.forEach(callback => callback(store)); +} + +/** + * Get current store state (read-only) + */ +export function getStore(): Readonly { + return store; +} + +/** + * Update market data + */ +export function updateMarkets(kalshi: CommonMarket[], polymarket: CommonMarket[]): void { + store.markets = { + kalshi, + polymarket, + lastUpdated: new Date(), + }; + + saveToStorage('LAST_UPDATED', store.markets.lastUpdated); + notifySubscribers(); +} + +/** + * Add a new market match + */ +export function addMarketMatch(match: MarketMatch): void { + // Check if match already exists + const existingIndex = store.matches.findIndex(m => + m.kalshiMarketId === match.kalshiMarketId && + m.polymarketMarketId === match.polymarketMarketId + ); + + if (existingIndex >= 0) { + // Update existing match + store.matches[existingIndex] = match; + } else { + // Add new match + store.matches.push(match); + } + + saveToStorage('MATCHES', store.matches); + notifySubscribers(); +} + +/** + * Update condition mappings for a match + */ +export function updateMatchConditions(matchId: string, conditionMappings: any[]): void { + const matchIndex = store.matches.findIndex(m => m.id === matchId); + if (matchIndex >= 0) { + store.matches[matchIndex] = { + ...store.matches[matchIndex], + conditionMappings, + conditionsMatched: conditionMappings.length > 0, + }; + + saveToStorage('MATCHES', store.matches); + notifySubscribers(); + } +} + +/** + * Remove a market match + */ +export function removeMarketMatch(matchId: string): void { + store.matches = store.matches.filter(m => m.id !== matchId); + saveToStorage('MATCHES', store.matches); + notifySubscribers(); +} + +/** + * Add a new ecosystem + */ +export function addEcosystem(ecosystem: Ecosystem): void { + store.ecosystems.push(ecosystem); + saveToStorage('ECOSYSTEMS', store.ecosystems); + notifySubscribers(); +} + +/** + * Update ecosystem condition mappings + */ +export function updateEcosystemConditions(ecosystemId: string, conditionMappings: any[]): void { + const ecosystemIndex = store.ecosystems.findIndex(e => e.id === ecosystemId); + if (ecosystemIndex >= 0) { + store.ecosystems[ecosystemIndex] = { + ...store.ecosystems[ecosystemIndex], + conditionMappings, + conditionsMatched: conditionMappings.length > 0, + }; + + saveToStorage('ECOSYSTEMS', store.ecosystems); + notifySubscribers(); + } +} + +/** + * Remove an ecosystem + */ +export function removeEcosystem(ecosystemId: string): void { + store.ecosystems = store.ecosystems.filter(e => e.id !== ecosystemId); + saveToStorage('ECOSYSTEMS', store.ecosystems); + notifySubscribers(); +} + +/** + * Create a new ecosystem from selected markets + */ +export function createEcosystem( + name: string, + selectedMarkets: Array<{ market: CommonMarket; platform: Platform }> +): Ecosystem { + // Calculate derived stats + const exchanges = new Set(selectedMarkets.map(m => m.platform)).size; + const markets = selectedMarkets.length; + const conditions = selectedMarkets.reduce((sum, m) => sum + m.market.outcomes.length, 0); + + // Find earliest end time + const endTimes = selectedMarkets.map(m => m.market.closeTime.getTime()); + const earliestEndTime = endTimes.length > 0 ? new Date(Math.min(...endTimes)) : undefined; + + const ecosystem: Ecosystem = { + id: `ecosystem-${Date.now()}`, + name, + createdAt: new Date(), + marketRefs: selectedMarkets.map(m => ({ + platform: m.platform, + marketId: m.market.id, + market: m.market, + })), + conditionMappings: [], + conditionsMatched: false, + exchanges, + markets, + conditions, + earliestEndTime, + }; + + addEcosystem(ecosystem); + return ecosystem; +} + +/** + * Update arbitrage opportunities + */ +export function updateArbitrageOpportunities(opportunities: ArbitrageOpportunity[]): void { + store.arbitrageOpportunities = opportunities; + notifySubscribers(); +} + +/** + * Update polling configuration + */ +export function updatePollingConfig(config: Partial): void { + store.polling = { ...store.polling, ...config }; + saveToStorage('POLLING_CONFIG', store.polling); + notifySubscribers(); +} + +/** + * Get markets by platform + */ +export function getMarketsByPlatform(platform: Platform): CommonMarket[] { + return store.markets[platform] || []; +} + +/** + * Get market by ID and platform + */ +export function getMarket(platform: Platform, marketId: string): CommonMarket | undefined { + return store.markets[platform].find(m => m.id === marketId); +} + +/** + * Get all matched markets with condition mappings + */ +export function getMatchedMarketsWithConditions(): MarketMatch[] { + return store.matches.filter(match => match.conditionsMatched); +} + +/** + * Get ecosystems with matched conditions + */ +export function getEcosystemsWithConditions(): Ecosystem[] { + return store.ecosystems.filter(ecosystem => ecosystem.conditionsMatched); +} + +/** + * Clear all stored data (for testing/reset) + */ +export function clearStore(): void { + store = { ...defaultState }; + + // Clear localStorage + Object.values(STORAGE_KEYS).forEach(key => { + localStorage.removeItem(key); + }); + + notifySubscribers(); +} + +/** + * Export data for backup + */ +export function exportStoreData(): string { + const exportData = { + matches: store.matches, + ecosystems: store.ecosystems, + polling: store.polling, + exportedAt: new Date().toISOString(), + }; + + return JSON.stringify(exportData, null, 2); +} + +/** + * Import data from backup + */ +export function importStoreData(jsonData: string): boolean { + try { + const importData = JSON.parse(jsonData); + + if (importData.matches) { + store.matches = importData.matches.map((match: any) => ({ + ...match, + createdAt: new Date(match.createdAt), + conditionMappings: match.conditionMappings.map((mapping: any) => ({ + ...mapping, + createdAt: new Date(mapping.createdAt), + })), + })); + saveToStorage('MATCHES', store.matches); + } + + if (importData.ecosystems) { + store.ecosystems = importData.ecosystems.map((ecosystem: any) => ({ + ...ecosystem, + createdAt: new Date(ecosystem.createdAt), + earliestEndTime: ecosystem.earliestEndTime ? new Date(ecosystem.earliestEndTime) : undefined, + conditionMappings: ecosystem.conditionMappings.map((mapping: any) => ({ + ...mapping, + createdAt: new Date(mapping.createdAt), + })), + })); + saveToStorage('ECOSYSTEMS', store.ecosystems); + } + + if (importData.polling) { + store.polling = importData.polling; + saveToStorage('POLLING_CONFIG', store.polling); + } + + notifySubscribers(); + return true; + } catch (error) { + console.error('Error importing store data:', error); + return false; + } +} + +/** + * Get store statistics + */ +export function getStoreStats() { + return { + totalMatches: store.matches.length, + matchesWithConditions: store.matches.filter(m => m.conditionsMatched).length, + totalEcosystems: store.ecosystems.length, + ecosystemsWithConditions: store.ecosystems.filter(e => e.conditionsMatched).length, + totalArbitrageOpportunities: store.arbitrageOpportunities.length, + kalshiMarkets: store.markets.kalshi.length, + polymarketMarkets: store.markets.polymarket.length, + lastUpdated: store.markets.lastUpdated, + }; +} \ No newline at end of file