-
-
Notifications
You must be signed in to change notification settings - Fork 283
feat(perps-controller): centralize market category classification #9009
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
7f3fe1c
f9cb903
8bb1afd
50d4288
1becc7b
998f88e
049099d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,5 +1,148 @@ | ||||||||||||||||||||||||||
| import type { PerpsMarketData } from '../types'; | ||||||||||||||||||||||||||
| import { MarketCategory } from '../types'; | ||||||||||||||||||||||||||
| import type { | ||||||||||||||||||||||||||
| GetMarketDataWithPricesParams, | ||||||||||||||||||||||||||
| MarketTypeFilter, | ||||||||||||||||||||||||||
| PerpsMarketData, | ||||||||||||||||||||||||||
| } from '../types'; | ||||||||||||||||||||||||||
| import type { CandleData, CandleStick } from '../types/perps-types'; | ||||||||||||||||||||||||||
| import { sortMarkets } from './sortMarkets'; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // ============================================================================ | ||||||||||||||||||||||||||
| // Market category classification (pure functions) | ||||||||||||||||||||||||||
| // No service dependencies — pure data transformations that can be tested and | ||||||||||||||||||||||||||
| // reused independently. `matchesCategory` and `getMarketTypeFilter` share the | ||||||||||||||||||||||||||
| // same category model. | ||||||||||||||||||||||||||
| // ============================================================================ | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||
| * Maps each data-model {@link MarketCategory} to its UI {@link MarketTypeFilter} | ||||||||||||||||||||||||||
| * with matching singular values. Exhaustive: adding a `MarketCategory` is a | ||||||||||||||||||||||||||
| * compile error here until it is mapped, so the model can't silently drift. | ||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||
| const MARKET_CATEGORY_TO_FILTER: Record<MarketCategory, MarketTypeFilter> = { | ||||||||||||||||||||||||||
| [MarketCategory.CryptoCurrency]: 'crypto', | ||||||||||||||||||||||||||
| [MarketCategory.Stock]: 'stock', | ||||||||||||||||||||||||||
| [MarketCategory.PreIpo]: 'pre-ipo', | ||||||||||||||||||||||||||
| [MarketCategory.Index]: 'index', | ||||||||||||||||||||||||||
| [MarketCategory.Etf]: 'etf', | ||||||||||||||||||||||||||
| [MarketCategory.Commodity]: 'commodity', | ||||||||||||||||||||||||||
| [MarketCategory.Forex]: 'forex', | ||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||
|
Comment on lines
+22
to
+30
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we are aligning, do we need this anymore? |
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||
| * Whether a market is a HIP-3 (non-main-DEX) market. A `marketSource` DEX id | ||||||||||||||||||||||||||
| * marks a HIP-3 market even when the `isHip3` flag is unset (e.g. partial route | ||||||||||||||||||||||||||
| * params), so both signals are checked. Used as the single HIP-3 signal so the | ||||||||||||||||||||||||||
| * classifiers stay consistent. | ||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||
| * @param market - The market data to test. | ||||||||||||||||||||||||||
| * @returns True if the market is HIP-3. | ||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||
| export const isHip3Market = ( | ||||||||||||||||||||||||||
| market: Pick<PerpsMarketData, 'isHip3' | 'marketSource'>, | ||||||||||||||||||||||||||
| ): boolean => Boolean(market.isHip3) || Boolean(market.marketSource); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||
| * Returns true when a market matches the given UI filter category. | ||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||
| * @param market - The market data to test. | ||||||||||||||||||||||||||
| * @param category - The filter category to test against. | ||||||||||||||||||||||||||
| * @returns Whether the market matches the category. | ||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||
| export function matchesCategory( | ||||||||||||||||||||||||||
| market: PerpsMarketData, | ||||||||||||||||||||||||||
| category: MarketTypeFilter, | ||||||||||||||||||||||||||
| ): boolean { | ||||||||||||||||||||||||||
| switch (category) { | ||||||||||||||||||||||||||
| case 'all': | ||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||
| case 'new': | ||||||||||||||||||||||||||
| // Explicitly flagged, or an uncategorized HIP-3 market (kept in sync with | ||||||||||||||||||||||||||
| // getMarketTypeFilter's 'new' bucket). | ||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||
| market.isNewMarket === true || | ||||||||||||||||||||||||||
| (isHip3Market(market) && market.marketType === undefined) | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
| case 'crypto': | ||||||||||||||||||||||||||
| // Main-DEX markets, plus HIP-3 assets explicitly typed as CryptoCurrency. | ||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||
| !isHip3Market(market) || | ||||||||||||||||||||||||||
| market.marketType === MarketCategory.CryptoCurrency | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
| default: | ||||||||||||||||||||||||||
| // Every other filter is a 1:1 data-model category match. | ||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||
| market.marketType !== undefined && | ||||||||||||||||||||||||||
| MARKET_CATEGORY_TO_FILTER[market.marketType] === category | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
|
Comment on lines
+72
to
+77
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ❤️ way better, but I believe we can just say
Suggested change
|
||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||
| * Resolve the user-facing category bucket for a market — one of `crypto`, | ||||||||||||||||||||||||||
| * `stock`, `pre-ipo`, `index`, `etf`, `commodity`, `forex`, or `new`. Data-model | ||||||||||||||||||||||||||
| * categories map 1:1. A market with no data-model category is `crypto` when it | ||||||||||||||||||||||||||
| * is main-DEX, or `new` when it is an uncategorized HIP-3 market (`isHip3`, or a | ||||||||||||||||||||||||||
| * `marketSource` DEX id when `isHip3` is unset, e.g. minimal route params). | ||||||||||||||||||||||||||
| * Never returns the `all` sentinel. | ||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||
| * Centralised as the single source of truth so consumers (e.g. category | ||||||||||||||||||||||||||
| * shortcuts, related markets) share one classification instead of re-deriving | ||||||||||||||||||||||||||
| * it per client and drifting as new categories are added. | ||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||
| * @param market - The market data to classify. | ||||||||||||||||||||||||||
| * @returns The market type filter bucket. | ||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||
| export function getMarketTypeFilter(market: PerpsMarketData): MarketTypeFilter { | ||||||||||||||||||||||||||
| const { marketType } = market; | ||||||||||||||||||||||||||
| if (marketType) { | ||||||||||||||||||||||||||
| return MARKET_CATEGORY_TO_FILTER[marketType]; | ||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| // No data-model category: an uncategorized HIP-3 market is the 'new' bucket; | ||||||||||||||||||||||||||
| // otherwise it's a main-DEX crypto market. | ||||||||||||||||||||||||||
| return isHip3Market(market) ? 'new' : 'crypto'; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||
| * Applies optional category filtering, sorting, and limit to a list of markets. | ||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||
| * @param markets - Source market array. | ||||||||||||||||||||||||||
| * @param params - Optional filter/sort/limit params. | ||||||||||||||||||||||||||
| * @returns Filtered, sorted, and/or sliced market array. | ||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||
| export function applyMarketFilters( | ||||||||||||||||||||||||||
| markets: PerpsMarketData[], | ||||||||||||||||||||||||||
| params?: GetMarketDataWithPricesParams, | ||||||||||||||||||||||||||
| ): PerpsMarketData[] { | ||||||||||||||||||||||||||
| let result = markets; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if (params?.categories?.length) { | ||||||||||||||||||||||||||
| const { categories } = params; | ||||||||||||||||||||||||||
| result = result.filter((market) => | ||||||||||||||||||||||||||
| // A market is included if it matches ANY of the requested categories. | ||||||||||||||||||||||||||
| categories.some((category) => matchesCategory(market, category)), | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if (params?.excludeSymbols?.length) { | ||||||||||||||||||||||||||
| const excluded = new Set(params.excludeSymbols); | ||||||||||||||||||||||||||
| result = result.filter((market) => !excluded.has(market.symbol)); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if (params?.sortBy) { | ||||||||||||||||||||||||||
| result = sortMarkets({ | ||||||||||||||||||||||||||
| markets: result, | ||||||||||||||||||||||||||
| sortBy: params.sortBy, | ||||||||||||||||||||||||||
| direction: params.direction, | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if (params?.limit !== undefined) { | ||||||||||||||||||||||||||
| result = result.slice(0, params.limit); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| return result; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||
| * Maximum length for market filter patterns (prevents DoS attacks) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should align market category with filter category to be the same all singular. The current approach is a bit confusing