-
-
Notifications
You must be signed in to change notification settings - Fork 282
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
Open
abretonc7s
wants to merge
6
commits into
main
Choose a base branch
from
feat/perps-centralize-market-category-filter
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+412
−93
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
7f3fe1c
feat(perps-controller): centralize market category classification
abretonc7s f9cb903
docs(perps-controller): add changelog entry for category helpers
abretonc7s 8bb1afd
test(perps-controller): apply oxfmt formatting
abretonc7s 50d4288
refactor(perps-controller): derive getMarketTypeFilter from matchesCa…
abretonc7s 1becc7b
refactor(perps-controller): clean up market category model
abretonc7s 998f88e
fix(perps-controller): classify HIP-3 partial markets consistently
abretonc7s File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,179 @@ | ||
| import type { PerpsMarketData } from '../types'; | ||
| import { MARKET_TYPE_FILTER, MarketCategory } from '../types'; | ||
| import type { | ||
| GetMarketDataWithPricesParams, | ||
| MarketType, | ||
| 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` is the single source of truth for the | ||
| // UI category model; `getMarketTypeFilter` is its inverse. | ||
| // ============================================================================ | ||
|
|
||
| /** | ||
| * Maps each data-model {@link MarketCategory} to its UI {@link MarketTypeFilter} | ||
| * (e.g. `stock` → `stocks`). 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]: MARKET_TYPE_FILTER.Crypto, | ||
| [MarketCategory.Stock]: MARKET_TYPE_FILTER.Stocks, | ||
| [MarketCategory.PreIpo]: MARKET_TYPE_FILTER.PreIpo, | ||
| [MarketCategory.Index]: MARKET_TYPE_FILTER.Indices, | ||
| [MarketCategory.Etf]: MARKET_TYPE_FILTER.Etfs, | ||
| [MarketCategory.Commodity]: MARKET_TYPE_FILTER.Commodities, | ||
| [MarketCategory.Forex]: MARKET_TYPE_FILTER.Forex, | ||
| }; | ||
|
|
||
| /** | ||
| * 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 MARKET_TYPE_FILTER.All: | ||
| return true; | ||
| case MARKET_TYPE_FILTER.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 MARKET_TYPE_FILTER.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 | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Stock-like market categories (stock, pre-ipo, index, etf). Clients collapse | ||
| * these into the single 'stocks' filter, and they share traditional market | ||
| * hours. Single source of truth for the equity bucket. | ||
| */ | ||
| export const STOCK_LIKE_MARKET_TYPES: ReadonlySet<MarketType> = new Set([ | ||
| MarketCategory.Stock, | ||
| MarketCategory.PreIpo, | ||
| MarketCategory.Index, | ||
| MarketCategory.Etf, | ||
| ]); | ||
|
Comment on lines
+87
to
+92
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. we shouln't have have this anymore, we are showing all categories now |
||
|
|
||
| /** | ||
| * Check whether a market type is stock-like (stock, pre-ipo, index, etf). | ||
| * | ||
| * @param marketType - The market type from {@link PerpsMarketData}. | ||
| * @returns True if the asset is stock-like. | ||
| */ | ||
| export const isEquityAsset = (marketType?: string): boolean => | ||
| marketType !== undefined && | ||
| STOCK_LIKE_MARKET_TYPES.has(marketType as MarketType); | ||
|
Comment on lines
+100
to
+102
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. same we this, we shouldn't need this |
||
|
|
||
| /** | ||
| * Resolve the user-facing category bucket for a market — one of `crypto`, | ||
| * `stocks`, `commodities`, `forex`, or `new`. Stock-like categories (stock, | ||
| * pre-ipo, index, etf) collapse into the single `stocks` bucket (see | ||
| * {@link isEquityAsset}); the remaining 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; | ||
| // Stock-like categories (stock, pre-ipo, index, etf) collapse into one pill. | ||
| if (isEquityAsset(marketType)) { | ||
| return MARKET_TYPE_FILTER.Stocks; | ||
| } | ||
| // Every other data-model category maps 1:1 (commodity, forex, crypto). | ||
| if (marketType) { | ||
| return MARKET_CATEGORY_TO_FILTER[marketType]; | ||
| } | ||
| // No data-model category: an uncategorized HIP-3 market is the 'new' bucket; | ||
| // otherwise it's a main-DEX crypto market. | ||
| return isHip3Market(market) | ||
| ? MARKET_TYPE_FILTER.New | ||
| : MARKET_TYPE_FILTER.Crypto; | ||
|
abretonc7s marked this conversation as resolved.
|
||
| } | ||
|
|
||
| /** | ||
| * 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) | ||
|
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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