Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ const PerpsMarketListView = ({
enablePolling: false,
showWatchlistOnly,
defaultMarketTypeFilter,
showZeroVolume: true, // Show $0.00 volume markets in list view
showZeroVolume: __DEV__, // Only show $0.00 volume markets in development
});

// Destructure search state for easier access
Expand Down
2 changes: 2 additions & 0 deletions app/components/UI/Perps/hooks/usePerpsHomeData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,14 @@ export const usePerpsHomeData = ({
});

// Fetch markets data for trending section (markets don't need real-time updates)
// Volume filtering is handled at the data layer in usePerpsMarkets
const {
markets: allMarkets,
isLoading: isMarketsLoading,
refresh: refreshMarkets,
} = usePerpsMarkets({
skipInitialFetch: false,
showZeroVolume: __DEV__,
});

// Get watchlist symbols from Redux
Expand Down
53 changes: 43 additions & 10 deletions app/components/UI/Perps/hooks/usePerpsMarketListView.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,25 @@ describe('usePerpsMarketListView', () => {
jest.clearAllMocks();

// Default mock implementations
// Cast to unknown to avoid volumeNumber type issues in tests
mockUsePerpsMarkets.mockReturnValue({
markets: mockAllMarkets as unknown as ReturnType<
typeof usePerpsMarkets
>['markets'],
isLoading: false,
isRefreshing: false,
error: null,
refresh: jest.fn(),
});
// Mock usePerpsMarkets to filter markets based on showZeroVolume parameter
mockUsePerpsMarkets.mockImplementation(
(options?: { showZeroVolume?: boolean }) => {
const shouldFilter = !options?.showZeroVolume; // Default is false (filter out zero volume)
const filteredMarkets = shouldFilter
? mockMarketsWithValidVolume
: mockAllMarkets;

return {
markets: filteredMarkets as unknown as ReturnType<
typeof usePerpsMarkets
>['markets'],
isLoading: false,
isRefreshing: false,
error: null,
refresh: jest.fn(),
};
},
);

mockUsePerpsSearch.mockReturnValue({
searchQuery: '',
Expand Down Expand Up @@ -157,15 +166,39 @@ describe('usePerpsMarketListView', () => {

expect(mockUsePerpsMarkets).toHaveBeenCalledWith({
enablePolling: true,
showZeroVolume: false,
});
});
});

describe('Volume Filtering', () => {
it('passes showZeroVolume=false to usePerpsMarkets by default', () => {
renderHook(() => usePerpsMarketListView());

// Default behavior: hide zero volume markets
expect(mockUsePerpsMarkets).toHaveBeenCalledWith(
expect.objectContaining({
showZeroVolume: false,
}),
);
});

it('passes showZeroVolume=true when showZeroVolume=true', () => {
renderHook(() => usePerpsMarketListView({ showZeroVolume: true }));

// When showZeroVolume is true, show them
expect(mockUsePerpsMarkets).toHaveBeenCalledWith(
expect.objectContaining({
showZeroVolume: true,
}),
);
});

it('filters out markets with zero volume displays', () => {
renderHook(() => usePerpsMarketListView());

// Should only pass markets with valid volume to usePerpsSearch
// (usePerpsMarkets mock filters them based on showZeroVolume parameter)
expect(mockUsePerpsSearch).toHaveBeenCalledWith(
expect.objectContaining({
markets: expect.not.arrayContaining([
Expand Down
42 changes: 6 additions & 36 deletions app/components/UI/Perps/hooks/usePerpsMarketListView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { usePerpsSearch } from './usePerpsSearch';
import { usePerpsSorting } from './usePerpsSorting';
import type { PerpsMarketData, MarketTypeFilter } from '../controllers/types';
import type { SortField, SortDirection } from '../utils/sortMarkets';
import { PERPS_CONSTANTS, type SortOptionId } from '../constants/perpsConfig';
import type { SortOptionId } from '../constants/perpsConfig';
import {
selectPerpsWatchlistMarkets,
selectPerpsMarketFilterPreferences,
Expand Down Expand Up @@ -140,12 +140,14 @@ export const usePerpsMarketListView = ({
showZeroVolume = false,
}: UsePerpsMarketListViewParams = {}): UsePerpsMarketListViewReturn => {
// Fetch markets data
// Volume filtering is handled at the data layer in usePerpsMarkets
const {
markets: allMarkets,
isLoading: isLoadingMarkets,
error,
} = usePerpsMarkets({
enablePolling,
showZeroVolume,
});

// Get Redux state
Expand All @@ -160,42 +162,10 @@ export const usePerpsMarketListView = ({
defaultMarketTypeFilter,
);

// Filter out markets with no valid volume
const marketsWithVolume = useMemo(
() =>
allMarkets.filter((market: PerpsMarketData) => {
// Always filter out fallback/error values
if (
market.volume === PERPS_CONSTANTS.FALLBACK_PRICE_DISPLAY ||
market.volume === PERPS_CONSTANTS.FALLBACK_DATA_DISPLAY
) {
return false;
}

// If showZeroVolume is true, allow $0.00 and $0 values
if (showZeroVolume) {
// Only filter if volume is completely missing
return !!market.volume;
}

// Default behavior: filter out zero and missing values
if (
!market.volume ||
market.volume === PERPS_CONSTANTS.ZERO_AMOUNT_DISPLAY ||
market.volume === PERPS_CONSTANTS.ZERO_AMOUNT_DETAILED_DISPLAY
) {
return false;
}

return true;
}),
[allMarkets, showZeroVolume],
);

// Use search hook for search state and filtering
// Pass ALL markets to search so it can search across all market types
const searchHook = usePerpsSearch({
markets: marketsWithVolume,
markets: allMarkets,
initialSearchVisible: defaultSearchVisible,
});

Expand Down Expand Up @@ -266,7 +236,7 @@ export const usePerpsMarketListView = ({
// Calculate market counts by type (for hiding empty tabs)
const marketCounts = useMemo(() => {
const counts = { crypto: 0, equity: 0, commodity: 0, forex: 0 };
marketsWithVolume.forEach((market) => {
allMarkets.forEach((market) => {
if (!market.marketType) {
counts.crypto++;
} else if (market.marketType === 'equity') {
Expand All @@ -278,7 +248,7 @@ export const usePerpsMarketListView = ({
}
});
return counts;
}, [marketsWithVolume]);
}, [allMarkets]);

return {
markets: finalMarkets,
Expand Down
58 changes: 55 additions & 3 deletions app/components/UI/Perps/hooks/usePerpsMarkets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,7 @@ describe('usePerpsMarkets', () => {
price: '$1',
change24h: '+0%',
change24hPercent: '0',
volume: '—',
volume: '--', // FALLBACK_DATA_DISPLAY
},
{
symbol: 'F',
Expand All @@ -463,7 +463,7 @@ describe('usePerpsMarkets', () => {
price: '$1',
change24h: '+0%',
change24hPercent: '0',
volume: '—',
volume: '--', // FALLBACK_DATA_DISPLAY
},
];

Expand All @@ -480,8 +480,60 @@ describe('usePerpsMarkets', () => {
});

// Assert - should be sorted by volume descending
// Note: F ($0), E (—), and G (—) are filtered out by default (showZeroVolume=false)
const sortedSymbols = result.current.markets.map((m) => m.symbol);
expect(sortedSymbols).toEqual(['B', 'D', 'A', 'C', 'F', 'E', 'G']);
expect(sortedSymbols).toEqual(['B', 'D', 'A', 'C']); // Only markets with valid volume
});

it('includes zero volume markets when showZeroVolume is true', async () => {
// Arrange - markets with various volume formats
const unsortedMarkets: PerpsMarketData[] = [
{
symbol: 'A',
name: 'A',
maxLeverage: '1x',
price: '$1',
change24h: '+0%',
change24hPercent: '0',
volume: '$100K',
},
{
symbol: 'B',
name: 'B',
maxLeverage: '1x',
price: '$1',
change24h: '+0%',
change24hPercent: '0',
volume: '$1.5B',
},
{
symbol: 'F',
name: 'F',
maxLeverage: '1x',
price: '$1',
change24h: '+0%',
change24hPercent: '0',
volume: '$0',
},
];

mockSubscribe.mockImplementation(({ callback }) => {
setTimeout(() => callback(unsortedMarkets), 0);
return jest.fn();
});

// Act
const { result } = renderHook(() =>
usePerpsMarkets({ showZeroVolume: true }),
);

await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});

// Assert - should include F ($0) when showZeroVolume is true
const sortedSymbols = result.current.markets.map((m) => m.symbol);
expect(sortedSymbols).toEqual(['B', 'A', 'F']);
});
});

Expand Down
53 changes: 42 additions & 11 deletions app/components/UI/Perps/hooks/usePerpsMarkets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ export interface UsePerpsMarketsOptions {
* @default false
*/
skipInitialFetch?: boolean;
/**
* Show markets with zero or invalid volume
* @default false
*/
showZeroVolume?: boolean;
}

const multipliers: Record<string, number> = {
Expand Down Expand Up @@ -107,6 +112,7 @@ export const usePerpsMarkets = (
enablePolling = false,
pollingInterval = 60000, // 1 minute default
skipInitialFetch = false,
showZeroVolume = false,
} = options;

const streamManager = usePerpsStream();
Expand All @@ -115,18 +121,43 @@ export const usePerpsMarkets = (
const [isRefreshing, setIsRefreshing] = useState(false);
const [error, setError] = useState<string | null>(null);

// Helper function to sort markets by volume
// Helper function to filter and sort markets by volume
const sortMarketsByVolume = useCallback(
(marketData: PerpsMarketData[]): PerpsMarketDataWithVolumeNumber[] =>
marketData
// pregenerate volumeNumber for sorting to avoid recalculating it on every sort
.map((item) => ({ ...item, volumeNumber: parseVolume(item.volume) }))
.sort((a, b) => {
const volumeA = a.volumeNumber;
const volumeB = b.volumeNumber;
return volumeB - volumeA;
}),
[],
(marketData: PerpsMarketData[]): PerpsMarketDataWithVolumeNumber[] => {
// Filter out invalid volume (unless showZeroVolume is true)
const filteredData = !showZeroVolume
? marketData.filter((market) => {
// Filter out fallback/error values
if (
market.volume === PERPS_CONSTANTS.FALLBACK_PRICE_DISPLAY ||
market.volume === PERPS_CONSTANTS.FALLBACK_DATA_DISPLAY
) {
return false;
}
// Filter out zero and missing values
if (
!market.volume ||
market.volume === PERPS_CONSTANTS.ZERO_AMOUNT_DISPLAY ||
market.volume === PERPS_CONSTANTS.ZERO_AMOUNT_DETAILED_DISPLAY
) {
return false;
}
return true;
})
: marketData;

return (
filteredData
// pregenerate volumeNumber for sorting to avoid recalculating it on every sort
.map((item) => ({ ...item, volumeNumber: parseVolume(item.volume) }))
.sort((a, b) => {
const volumeA = a.volumeNumber;
const volumeB = b.volumeNumber;
return volumeB - volumeA;
})
);
},
[showZeroVolume],
);

// Manual refresh function
Expand Down
Loading