From ad52ac3d51149d7c915adedc41873609e94656e5 Mon Sep 17 00:00:00 2001 From: anton Date: Thu, 29 Jan 2026 15:52:46 +0800 Subject: [PATCH 1/2] feat: use backend isTrending signal for trending markets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add isTrending + trendingReason to MarketMetrics type - useTrendingMarketKeys() now uses backend's isTrending flag - Add useCustomSignalMarketKeys() for user-defined filters - Rename isMarketTrending → matchesCustomSignal (with legacy alias) Backend now provides curated trending signal, user settings become 'Custom Signals' for personalized filters. --- src/hooks/queries/useMarketMetricsQuery.ts | 43 ++++++++++++++++++---- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/src/hooks/queries/useMarketMetricsQuery.ts b/src/hooks/queries/useMarketMetricsQuery.ts index b49b9b9f..6725882e 100644 --- a/src/hooks/queries/useMarketMetricsQuery.ts +++ b/src/hooks/queries/useMarketMetricsQuery.ts @@ -40,6 +40,9 @@ export type MarketMetrics = { // Key flags everLiquidated: boolean; marketScore: number | null; + // Backend-computed trending signal (our curated "hot markets") + isTrending: boolean; + trendingReason: string | null; // State and flows currentState: MarketCurrentState; flows: Record; @@ -193,14 +196,16 @@ export const parseFlowAssets = (flowAssets: string, decimals: number): number => }; /** - * Determines if a market is trending based on flow thresholds. + * Determines if a market matches user's custom signal thresholds. * All non-empty thresholds must be met (AND logic). * Only positive flows (inflows) are considered. + * + * Note: This is for user-defined signals, separate from backend trending. */ -export const isMarketTrending = (metrics: MarketMetrics, trendingConfig: TrendingConfig): boolean => { - if (!trendingConfig.enabled) return false; +export const matchesCustomSignal = (metrics: MarketMetrics, signalConfig: TrendingConfig): boolean => { + if (!signalConfig.enabled) return false; - for (const [window, config] of Object.entries(trendingConfig.windows)) { + for (const [window, config] of Object.entries(signalConfig.windows)) { const supplyPct = config?.minSupplyFlowPct ?? ''; const supplyUsd = config?.minSupplyFlowUsd ?? ''; const borrowPct = config?.minBorrowFlowPct ?? ''; @@ -234,7 +239,7 @@ export const isMarketTrending = (metrics: MarketMetrics, trendingConfig: Trendin } } - const hasAnyThreshold = Object.values(trendingConfig.windows).some((c) => { + const hasAnyThreshold = Object.values(signalConfig.windows).some((c) => { const supplyPct = c?.minSupplyFlowPct ?? ''; const supplyUsd = c?.minSupplyFlowUsd ?? ''; const borrowPct = c?.minBorrowFlowPct ?? ''; @@ -245,12 +250,34 @@ export const isMarketTrending = (metrics: MarketMetrics, trendingConfig: Trendin return hasAnyThreshold; }; +// Legacy alias for backwards compatibility +export const isMarketTrending = matchesCustomSignal; + /** - * Returns a Set of market keys that are currently trending. - * Uses metricsMap for O(1) lookup and filters based on trending config from preferences. + * Returns a Set of market keys that are trending (backend-computed). + * Uses isTrending field from Monarch API - our curated "hot markets" signal. */ export const useTrendingMarketKeys = () => { const { metricsMap } = useMarketMetricsMap(); + + return useMemo(() => { + const keys = new Set(); + + for (const [key, metrics] of metricsMap) { + if (metrics.isTrending) { + keys.add(key); + } + } + return keys; + }, [metricsMap]); +}; + +/** + * Returns a Set of market keys matching user's custom signal config. + * Separate from backend trending - for user-defined filters. + */ +export const useCustomSignalMarketKeys = () => { + const { metricsMap } = useMarketMetricsMap(); const { trendingConfig } = useMarketPreferences(); return useMemo(() => { @@ -258,7 +285,7 @@ export const useTrendingMarketKeys = () => { if (!trendingConfig.enabled) return keys; for (const [key, metrics] of metricsMap) { - if (isMarketTrending(metrics, trendingConfig)) { + if (matchesCustomSignal(metrics, trendingConfig)) { keys.add(key); } } From d135cb8ccd918d511d544ad1ea553a0baddef3db Mon Sep 17 00:00:00 2001 From: anton Date: Thu, 29 Jan 2026 15:57:22 +0800 Subject: [PATCH 2/2] feat: add icon picker for custom tags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add icon field to TrendingConfig (user's custom tag) - Add setTrendingIcon action - Add icon picker UI in TrendingDetail settings - Show both backend trending (🔥) and user's custom tag icon - Display trendingReason from API in tooltip Available icons: 🏷️ ⭐ 🔥 💎 🚀 📈 💰 ⚡ 🎯 👀 --- .../markets/components/market-indicators.tsx | 32 ++++++++++++++++-- .../details/TrendingDetail.tsx | 33 +++++++++++++++---- src/stores/useMarketPreferences.ts | 15 +++++++-- 3 files changed, 69 insertions(+), 11 deletions(-) diff --git a/src/features/markets/components/market-indicators.tsx b/src/features/markets/components/market-indicators.tsx index 99ccdfad..86b476d1 100644 --- a/src/features/markets/components/market-indicators.tsx +++ b/src/features/markets/components/market-indicators.tsx @@ -5,7 +5,7 @@ import { LuUser } from 'react-icons/lu'; import { IoWarningOutline } from 'react-icons/io5'; import { AiOutlineFire } from 'react-icons/ai'; import { TooltipContent } from '@/components/shared/tooltip-content'; -import { useTrendingMarketKeys, getMetricsKey, useEverLiquidated } from '@/hooks/queries/useMarketMetricsQuery'; +import { useTrendingMarketKeys, useCustomSignalMarketKeys, getMetricsKey, useEverLiquidated, useMarketMetricsMap } from '@/hooks/queries/useMarketMetricsQuery'; import { computeMarketWarnings } from '@/hooks/useMarketWarnings'; import { useMarketPreferences } from '@/stores/useMarketPreferences'; import type { Market } from '@/utils/types'; @@ -23,8 +23,17 @@ type MarketIndicatorsProps = { export function MarketIndicators({ market, showRisk = false, isStared = false, hasUserPosition = false }: MarketIndicatorsProps) { const hasLiquidationProtection = useEverLiquidated(market.morphoBlue.chain.id, market.uniqueKey); const { trendingConfig } = useMarketPreferences(); + const { metricsMap } = useMarketMetricsMap(); + + // Backend-computed trending (official "hot markets") const trendingKeys = useTrendingMarketKeys(); - const isTrending = trendingConfig.enabled && trendingKeys.has(getMetricsKey(market.morphoBlue.chain.id, market.uniqueKey)); + const marketKey = getMetricsKey(market.morphoBlue.chain.id, market.uniqueKey); + const isTrending = trendingKeys.has(marketKey); + const trendingReason = metricsMap.get(marketKey)?.trendingReason; + + // User's custom tag + const customTagKeys = useCustomSignalMarketKeys(); + const hasCustomTag = trendingConfig.enabled && customTagKeys.has(marketKey); const warnings = showRisk ? computeMarketWarnings(market, true) : []; const hasWarnings = warnings.length > 0; const alertWarning = warnings.find((w) => w.level === 'alert'); @@ -101,6 +110,7 @@ export function MarketIndicators({ market, showRisk = false, isStared = false, h whitelisted={market.whitelisted} /> + {/* Backend-computed trending (official) */} {isTrending && ( } - detail="This market is trending based on flow metrics" + detail={trendingReason ?? 'This market is trending'} /> } > @@ -124,6 +134,22 @@ export function MarketIndicators({ market, showRisk = false, isStared = false, h )} + {/* User's custom tag */} + {hasCustomTag && ( + {trendingConfig.icon}} + detail="Matches your custom tag criteria" + /> + } + > +
+ {trendingConfig.icon} +
+
+ )} + {showRisk && hasWarnings && ( }): string { - if (!config.enabled) return 'Trending detection is disabled'; +function generateFilterSummary(config: { enabled: boolean; icon: CustomTagIcon; windows: Record }): string { + if (!config.enabled) return 'Custom tag is disabled'; const parts: string[] = []; @@ -91,7 +91,7 @@ function CompactInput({ } export function TrendingDetail() { - const { trendingConfig, setTrendingEnabled, setTrendingWindowConfig } = useMarketPreferences(); + const { trendingConfig, setTrendingEnabled, setTrendingIcon, setTrendingWindowConfig } = useMarketPreferences(); const { metricsMap } = useMarketMetricsMap(); const { allMarkets } = useProcessedMarkets(); const isEnabled = trendingConfig.enabled; @@ -102,7 +102,7 @@ export function TrendingDetail() { const matches: Array<{ market: Market; supplyFlowPct1h: number }> = []; for (const [key, metrics] of metricsMap) { - if (isMarketTrending(metrics, trendingConfig)) { + if (matchesCustomSignal(metrics, trendingConfig)) { const market = allMarkets.find((m) => getMetricsKey(m.morphoBlue.chain.id, m.uniqueKey) === key); if (market) { matches.push({ @@ -126,6 +126,27 @@ export function TrendingDetail() { return (
+ {/* Icon Picker */} +
+ Tag icon: +
+ {CUSTOM_TAG_ICONS.map((icon) => ( + + ))} +
+
+ {/* Toggle + Summary */}
diff --git a/src/stores/useMarketPreferences.ts b/src/stores/useMarketPreferences.ts index c960bb44..3de480b7 100644 --- a/src/stores/useMarketPreferences.ts +++ b/src/stores/useMarketPreferences.ts @@ -4,7 +4,7 @@ import { SortColumn } from '@/features/markets/components/constants'; import { DEFAULT_MIN_SUPPLY_USD } from '@/constants/markets'; import { DEFAULT_COLUMN_VISIBILITY, type ColumnVisibility } from '@/features/markets/components/column-visibility'; -// Trending feature types +// Custom Tags feature types (user-defined market filters) export type FlowTimeWindow = '1h' | '24h' | '7d' | '30d'; export type TrendingWindowConfig = { @@ -16,13 +16,19 @@ export type TrendingWindowConfig = { minBorrowFlowUsd: string; }; +// Available icons for custom tags +export const CUSTOM_TAG_ICONS = ['🏷️', '⭐', '🔥', '💎', '🚀', '📈', '💰', '⚡', '🎯', '👀'] as const; +export type CustomTagIcon = (typeof CUSTOM_TAG_ICONS)[number]; + export type TrendingConfig = { enabled: boolean; + icon: CustomTagIcon; // User-selected icon for their custom tag windows: Record; }; const DEFAULT_TRENDING_CONFIG: TrendingConfig = { enabled: false, + icon: '🏷️', windows: { '1h': { minSupplyFlowPct: '6', minSupplyFlowUsd: '', minBorrowFlowPct: '', minBorrowFlowUsd: '' }, '24h': { minSupplyFlowPct: '', minSupplyFlowUsd: '', minBorrowFlowPct: '', minBorrowFlowUsd: '' }, @@ -95,8 +101,9 @@ type MarketPreferencesActions = { setMinBorrowEnabled: (enabled: boolean) => void; setMinLiquidityEnabled: (enabled: boolean) => void; - // Trending Config (Beta) + // Custom Tags Config (user-defined market filters) setTrendingEnabled: (enabled: boolean) => void; + setTrendingIcon: (icon: CustomTagIcon) => void; setTrendingWindowConfig: (window: FlowTimeWindow, config: Partial) => void; // Bulk update for migration @@ -170,6 +177,10 @@ export const useMarketPreferences = create()( set((state) => ({ trendingConfig: { ...state.trendingConfig, enabled }, })), + setTrendingIcon: (icon) => + set((state) => ({ + trendingConfig: { ...state.trendingConfig, icon }, + })), setTrendingWindowConfig: (window, config) => set((state) => ({ trendingConfig: {