← Back
import { useState, useEffect, useCallback } from 'react';

export type SignalType = 'WHALE' | 'LONGSHOT' | 'SPIKE' | 'MOMENTUM';

interface SignalBase {
  type: SignalType;
  strength: number; // 0..1 normalized — directional conviction (scale 1)
  marketId: string;
  question: string | null;
  category: string | null;
  detectedAt: string | null;
  endDate: string | null; // ISO market resolution time
}

export interface WhaleSignal extends SignalBase {
  type: 'WHALE';
  imageUrl: string | null;
  marketPrice: number;
  edgeScore: number;
  signal: 'BUY_YES' | 'BUY_NO';
  confidence: 'HIGH' | 'MED' | 'LOW';
  whaleYesPct: number;
  volumeRatio: number;
  recommendedSide: 'YES' | 'NO' | null;
  impliedProb: number | null; // 0..1 market-implied probability of the recommended side
  entryQuality: number;       // 0..1 — spread tightness + liquidity (scale 2)
  whaleCount: number;       // # of qualifying whale trades behind the signal
  whaleVolumeUsd: number;   // total $ whale volume behind the signal
  whaleWr: number | null; // 0..1 avg win-rate of whales in this market, null if unknown
}

export interface LongshotSignal extends SignalBase {
  type: 'LONGSHOT';
  imageUrl: string | null;
  marketPrice: number;
  signal: 'BUY_YES' | 'BUY_NO';
  confidence: 'HIGH' | 'MED' | 'LOW';
  recommendedSide: 'YES' | 'NO' | null;
  impliedProb: number | null; // 0..1 market-implied probability of the recommended side
  entryQuality: number;       // 0..1 (scale 2)
}

export interface SpikeSignal extends SignalBase {
  type: 'SPIKE';
  score: number; // volume_24h / avg (e.g. 5.0 = 500%)
  volume24h: number;
  avgVolume: number;
  liquidity: number;
}

export interface MomentumSignal extends SignalBase {
  type: 'MOMENTUM';
  imageUrl: string | null;
  marketPrice: number;
  momentumFactor: number; // 0..1
  confidence: 'HIGH' | 'MED' | 'LOW';
  volumeRatio: number;
  entryQuality: number;       // 0..1 (scale 2)
}

export type SignalCard = WhaleSignal | LongshotSignal | SpikeSignal | MomentumSignal;

export type Horizon = 'fast' | 'mid' | 'long' | '';

interface FeedParams {
  limit?: number;
  category?: string;        // 'All' or empty = no filter
  type?: SignalType | '';   // '' = both
  minStrength?: number;     // 0..1
  horizon?: Horizon;        // '' = any time-to-resolution
}

export function useSignalsFeed(params: FeedParams = {}) {
  const { limit = 40, category, type, minStrength, horizon } = params;
  const [signals, setSignals] = useState<SignalCard[]>([]);
  const [loading, setLoading] = useState(true);

  const fetch_ = useCallback(async () => {
    setLoading(true);
    try {
      const qs = new URLSearchParams({ limit: String(limit) });
      if (category && category !== 'All') qs.set('category', category);
      if (type) qs.set('type', type);
      if (minStrength && minStrength > 0) qs.set('min_strength', String(minStrength));
      if (horizon) qs.set('horizon', horizon);
      const res = await fetch(`/api/signals/feed?${qs.toString()}`);
      const d = await res.json();
      if (d.success) {
        setSignals(d.data ?? []);
      }
    } catch { /* ignore */ }
    finally { setLoading(false); }
  }, [limit, category, type, minStrength, horizon]);

  useEffect(() => {
    const t = setTimeout(fetch_, 0);
    const interval = setInterval(fetch_, 2 * 60_000);
    return () => { clearTimeout(t); clearInterval(interval); };
  }, [fetch_]);

  return { signals, loading, refresh: fetch_ };
}

📜 Git History

6c47fa4chore: local Polikopi project home + Phase 1 redesign artifacts12 days ago
Show last diff
Loading...