From 5d28568d78c058d99cc390d6c2057cc71a3315da Mon Sep 17 00:00:00 2001 From: Akmal Djumakhodjaev Date: Thu, 24 Jul 2025 13:46:10 +0800 Subject: [PATCH 1/4] feat: Implement DisplayNameService for centralized display name management --- src/binaryapi/ActiveSymbols.ts | 120 +++--- src/binaryapi/BinaryAPI.ts | 8 +- src/components/SymbolSelectButton.tsx | 5 +- src/config/__tests__/displayNames.spec.ts | 296 ++++++++++++++ src/config/displayNameTypes.ts | 44 +++ src/config/displayNames.ts | 293 ++++++++++++++ src/feed/Feed.ts | 3 +- src/services/DisplayNameService.ts | 285 ++++++++++++++ .../__tests__/DisplayNameService.spec.ts | 218 +++++++++++ src/store/ChartStore.ts | 4 +- src/store/CrosshairStore.ts | 6 +- src/utils/__tests__/displayNameUtils.spec.ts | 366 ++++++++++++++++++ src/utils/__tests__/index.spec.ts | 20 +- src/utils/displayNameUtils.ts | 245 ++++++++++++ src/utils/index.ts | 12 +- 15 files changed, 1856 insertions(+), 69 deletions(-) create mode 100644 src/config/__tests__/displayNames.spec.ts create mode 100644 src/config/displayNameTypes.ts create mode 100644 src/config/displayNames.ts create mode 100644 src/services/DisplayNameService.ts create mode 100644 src/services/__tests__/DisplayNameService.spec.ts create mode 100644 src/utils/__tests__/displayNameUtils.spec.ts create mode 100644 src/utils/displayNameUtils.ts diff --git a/src/binaryapi/ActiveSymbols.ts b/src/binaryapi/ActiveSymbols.ts index d4b30771eb..0f5e9d700a 100644 --- a/src/binaryapi/ActiveSymbols.ts +++ b/src/binaryapi/ActiveSymbols.ts @@ -6,6 +6,9 @@ import TradingTimes from './TradingTimes'; import { cloneCategories, stableSort } from '../utils'; import PendingPromise from '../utils/PendingPromise'; import { isDeepEqual } from '../utils/object'; +import { + getCachedDisplayNames +} from '../utils/displayNameUtils'; const DefaultSymbols = [ 'synthetic_index', @@ -21,12 +24,15 @@ export type TProcessedSymbolItem = { symbol: string; name: string; market: string; - market_display_name: string; subgroup: string; - subgroup_display_name: string; - submarket_display_name: string; + submarket: string; exchange_is_open: boolean; decimal_places: number; + // Display names for UI + displayName: string; + marketDisplayName: string; + submarketDisplayName: string; + subgroupDisplayName: string; }; type TProcessedSymbols = TProcessedSymbolItem[]; @@ -126,6 +132,7 @@ export default class ActiveSymbols { computeActiveSymbols(active_symbols: TActiveSymbols) { runInAction(() => { this.processedSymbols = this._processSymbols(active_symbols); + this.categorizedSymbols = this._categorizeActiveSymbols(this.processedSymbols); }); for (const symbolObj of this.processedSymbols || []) { @@ -164,20 +171,31 @@ export default class ActiveSymbols { // Stable sort is required to retain the order of the symbol name const sortedSymbols = stableSort(symbols, (a, b) => - a.submarket_display_name.localeCompare(b.submarket_display_name) + a.submarket.localeCompare(b.submarket) ); for (const s of sortedSymbols) { + // Get display names using the display name service + const displayNames = getCachedDisplayNames({ + symbol: s.symbol, + market: s.market, + submarket: s.submarket, + subgroup: s.subgroup, + }); + processedSymbols.push({ symbol: s.symbol, - name: s.display_name, + name: s.symbol, // Keep raw symbol for internal use market: s.market, - market_display_name: s.market_display_name, subgroup: s.subgroup, - subgroup_display_name: s.subgroup_display_name, - submarket_display_name: s.submarket_display_name, + submarket: s.submarket, exchange_is_open: !!s.exchange_is_open, decimal_places: s.pip.toString().length - 2, + // Add display names for UI + displayName: displayNames.symbolDisplayName, + marketDisplayName: displayNames.marketDisplayName, + submarketDisplayName: displayNames.submarketDisplayName, + subgroupDisplayName: displayNames.subgroupDisplayName, }); } @@ -199,38 +217,50 @@ export default class ActiveSymbols { _categorizeActiveSymbols(activeSymbols: TProcessedSymbols): TCategorizedSymbols { const categorizedSymbols: TCategorizedSymbols = []; - const first = activeSymbols[0]; const getSubcategory = (d: TProcessedSymbolItem): TSubCategory => ({ - subcategoryName: d.submarket_display_name, + subcategoryName: d.submarketDisplayName || d.submarket, data: [], }); const getCategory = (d: TProcessedSymbolItem): TCategorizedSymbolItem => ({ - categoryName: d.market_display_name, - categoryId: d.market, + categoryName: d.marketDisplayName || d.market, + categoryId: d.market, // Keep raw market as ID for internal logic hasSubcategory: true, hasSubgroup: !!(d.subgroup && d.subgroup !== 'none'), data: [], subgroups: [], }); - let subcategory = getSubcategory(first); - let category = getCategory(first); - for (const symbol of activeSymbols) { - if ( - category.categoryName !== symbol.market_display_name && - category.categoryName !== symbol.subgroup_display_name - ) { - category.data.push(subcategory); - categorizedSymbols.push(category); - subcategory = getSubcategory(symbol); - category = getCategory(symbol); + + // Use a Map to collect all subcategories for each category + const categoryMap = new Map + }>(); + for (const symbol of activeSymbols) { + const category = getCategory(symbol); + const subcategory = getSubcategory(symbol); + + if (!categoryMap.has(category.categoryId)) { + categoryMap.set(category.categoryId, { + category: category, + subcategories: new Map() + }); } - + + const categoryData = categoryMap.get(category.categoryId)!; + + if (!categoryData.subcategories.has(subcategory.subcategoryName)) { + categoryData.subcategories.set(subcategory.subcategoryName, subcategory); + } + + const currentSubcategory = categoryData.subcategories.get(subcategory.subcategoryName)!; + + // Handle subgroups if needed if (category.hasSubgroup) { - if (!category.subgroups?.some((el: TCategorizedSymbolItem) => el.categoryId === symbol.subgroup)) { - category.subgroups?.push({ + if (!categoryData.category.subgroups?.some((el: TCategorizedSymbolItem) => el.categoryId === symbol.subgroup)) { + categoryData.category.subgroups?.push({ data: [], - categoryName: symbol.subgroup_display_name, - categoryId: symbol.subgroup, + categoryName: symbol.subgroupDisplayName || symbol.subgroup, + categoryId: symbol.subgroup, // Keep raw subgroup as ID for internal logic hasSubcategory: true, hasSubgroup: false, subgroups: [], @@ -238,41 +268,39 @@ export default class ActiveSymbols { } // should push a subcategory instead of symbol if ( - !category.subgroups + !categoryData.category.subgroups ?.find((el: TCategorizedSymbolItem) => el.categoryId === symbol.subgroup) - ?.data.find((el: TSubCategory) => el.subcategoryName === symbol.submarket_display_name) + ?.data.find((el: TSubCategory) => el.subcategoryName === (symbol.submarketDisplayName || symbol.submarket)) ) { - subcategory = getSubcategory(symbol); - category.subgroups + const subgroupSubcategory = getSubcategory(symbol); + categoryData.category.subgroups ?.find((el: TCategorizedSymbolItem) => el.categoryId === symbol.subgroup) - ?.data.push(subcategory); - subcategory = getSubcategory(symbol); + ?.data.push(subgroupSubcategory); } - category.subgroups + categoryData.category.subgroups ?.find((el: TCategorizedSymbolItem) => el.categoryId === symbol.subgroup) - ?.data.find((el: TSubCategory) => el.subcategoryName === symbol.submarket_display_name) + ?.data.find((el: TSubCategory) => el.subcategoryName === (symbol.submarketDisplayName || symbol.submarket)) ?.data.push({ enabled: true, itemId: symbol.symbol, - display: symbol.name, + display: symbol.displayName || symbol.name, dataObject: symbol, }); } - if (subcategory.subcategoryName !== symbol.submarket_display_name) { - category.data.push(subcategory); - subcategory = getSubcategory(symbol); - } - subcategory.data.push({ + currentSubcategory.data.push({ enabled: true, itemId: symbol.symbol, - display: symbol.name, + display: symbol.displayName || symbol.name, dataObject: symbol, }); } - category.data.push(subcategory); - categorizedSymbols.push(category); - + // Convert the map back to the expected array format + for (const [, categoryData] of categoryMap) { + // Add all subcategories to the category + categoryData.category.data = Array.from(categoryData.subcategories.values()); + categorizedSymbols.push(categoryData.category); + } return categorizedSymbols; } } diff --git a/src/binaryapi/BinaryAPI.ts b/src/binaryapi/BinaryAPI.ts index efe77081ce..7840e8af00 100644 --- a/src/binaryapi/BinaryAPI.ts +++ b/src/binaryapi/BinaryAPI.ts @@ -42,8 +42,12 @@ export default class BinaryAPI { this.requestForget = requestForget; this.requestForgetStream = requestForgetStream; } - getActiveSymbols(): Promise { - return this.requestAPI({ active_symbols: 'brief' }); + getActiveSymbols(brand_id?: string): Promise { + const request: any = { active_symbols: 'brief' }; + if (brand_id) { + request.brand_id = brand_id; + } + return this.requestAPI(request); } getServerTime(): Promise { return this.requestAPI({ time: 1 }); diff --git a/src/components/SymbolSelectButton.tsx b/src/components/SymbolSelectButton.tsx index ed16ccd954..803359e612 100644 --- a/src/components/SymbolSelectButton.tsx +++ b/src/components/SymbolSelectButton.tsx @@ -6,6 +6,7 @@ import AnimatedPriceStore from 'src/store/AnimatedPriceStore'; import ChartTitleStore from 'src/store/ChartTitleStore'; import { ItemIconMap, SymbolPlaceholderIcon, ArrowIcon, TimeIcon } from './Icons'; import { MarketOpeningTimeCounter } from './MarketOpeningTimeCounter'; +import { getSymbolDisplayName } from '../utils/displayNameUtils'; import AnimatedPrice from './AnimatedPrice'; type TChartPriceProps = { @@ -43,7 +44,9 @@ const SymbolInfoBase = () => { <> {SymbolIcon && }
-
{symbol.name}
+
+ {getSymbolDisplayName(symbol.symbol) || symbol.name} +
{isSymbolOpen && ( { + describe('DISPLAY_NAME_MAPPINGS structure validation', () => { + it('should have all required top-level properties', () => { + expect(DISPLAY_NAME_MAPPINGS).to.have.property('symbols'); + expect(DISPLAY_NAME_MAPPINGS).to.have.property('markets'); + expect(DISPLAY_NAME_MAPPINGS).to.have.property('submarkets'); + expect(DISPLAY_NAME_MAPPINGS).to.have.property('subgroups'); + }); + + it('should have symbols as an object', () => { + expect(DISPLAY_NAME_MAPPINGS.symbols).to.be.an('object'); + expect(Object.keys(DISPLAY_NAME_MAPPINGS.symbols).length).to.be.greaterThan(0); + }); + + it('should have markets as an object', () => { + expect(DISPLAY_NAME_MAPPINGS.markets).to.be.an('object'); + expect(Object.keys(DISPLAY_NAME_MAPPINGS.markets).length).to.be.greaterThan(0); + }); + + it('should have submarkets as an object', () => { + expect(DISPLAY_NAME_MAPPINGS.submarkets).to.be.an('object'); + expect(Object.keys(DISPLAY_NAME_MAPPINGS.submarkets).length).to.be.greaterThan(0); + }); + + it('should have subgroups as an object', () => { + expect(DISPLAY_NAME_MAPPINGS.subgroups).to.be.an('object'); + expect(Object.keys(DISPLAY_NAME_MAPPINGS.subgroups).length).to.be.greaterThan(0); + }); + }); + + describe('symbol mappings', () => { + it('should contain volatility indices', () => { + expect(DISPLAY_NAME_MAPPINGS.symbols).to.have.property('R_10'); + expect(DISPLAY_NAME_MAPPINGS.symbols['R_10']).to.equal('Volatility 10 Index'); + + expect(DISPLAY_NAME_MAPPINGS.symbols).to.have.property('R_25'); + expect(DISPLAY_NAME_MAPPINGS.symbols['R_25']).to.equal('Volatility 25 Index'); + + expect(DISPLAY_NAME_MAPPINGS.symbols).to.have.property('R_50'); + expect(DISPLAY_NAME_MAPPINGS.symbols['R_50']).to.equal('Volatility 50 Index'); + + expect(DISPLAY_NAME_MAPPINGS.symbols).to.have.property('R_75'); + expect(DISPLAY_NAME_MAPPINGS.symbols['R_75']).to.equal('Volatility 75 Index'); + + expect(DISPLAY_NAME_MAPPINGS.symbols).to.have.property('R_100'); + expect(DISPLAY_NAME_MAPPINGS.symbols['R_100']).to.equal('Volatility 100 Index'); + }); + + it('should contain step indices', () => { + expect(DISPLAY_NAME_MAPPINGS.symbols).to.have.property('stpRNG'); + expect(DISPLAY_NAME_MAPPINGS.symbols['stpRNG']).to.equal('Step Index'); + }); + + it('should contain boom and crash indices', () => { + expect(DISPLAY_NAME_MAPPINGS.symbols).to.have.property('BOOM1000'); + expect(DISPLAY_NAME_MAPPINGS.symbols['BOOM1000']).to.equal('Boom 1000 Index'); + + expect(DISPLAY_NAME_MAPPINGS.symbols).to.have.property('CRASH1000'); + expect(DISPLAY_NAME_MAPPINGS.symbols['CRASH1000']).to.equal('Crash 1000 Index'); + }); + + it('should contain major forex pairs', () => { + expect(DISPLAY_NAME_MAPPINGS.symbols).to.have.property('frxEURUSD'); + expect(DISPLAY_NAME_MAPPINGS.symbols['frxEURUSD']).to.equal('EUR/USD'); + + expect(DISPLAY_NAME_MAPPINGS.symbols).to.have.property('frxGBPUSD'); + expect(DISPLAY_NAME_MAPPINGS.symbols['frxGBPUSD']).to.equal('GBP/USD'); + + expect(DISPLAY_NAME_MAPPINGS.symbols).to.have.property('frxUSDJPY'); + expect(DISPLAY_NAME_MAPPINGS.symbols['frxUSDJPY']).to.equal('USD/JPY'); + }); + + it('should contain cryptocurrencies', () => { + expect(DISPLAY_NAME_MAPPINGS.symbols).to.have.property('cryBTCUSD'); + expect(DISPLAY_NAME_MAPPINGS.symbols['cryBTCUSD']).to.equal('Bitcoin'); + + expect(DISPLAY_NAME_MAPPINGS.symbols).to.have.property('cryETHUSD'); + expect(DISPLAY_NAME_MAPPINGS.symbols['cryETHUSD']).to.equal('Ethereum'); + }); + + it('should contain commodities', () => { + expect(DISPLAY_NAME_MAPPINGS.symbols).to.have.property('frxXAUUSD'); + expect(DISPLAY_NAME_MAPPINGS.symbols['frxXAUUSD']).to.equal('Gold/USD'); + + expect(DISPLAY_NAME_MAPPINGS.symbols).to.have.property('frxXAGUSD'); + expect(DISPLAY_NAME_MAPPINGS.symbols['frxXAGUSD']).to.equal('Silver/USD'); + }); + + it('should have all symbol values as non-empty strings', () => { + Object.entries(DISPLAY_NAME_MAPPINGS.symbols).forEach(([key, value]) => { + expect(key).to.be.a('string').that.is.not.empty; + expect(value).to.be.a('string').that.is.not.empty; + }); + }); + }); + + describe('market mappings', () => { + it('should contain major markets', () => { + expect(DISPLAY_NAME_MAPPINGS.markets).to.have.property('forex'); + expect(DISPLAY_NAME_MAPPINGS.markets['forex']).to.equal('Forex'); + + expect(DISPLAY_NAME_MAPPINGS.markets).to.have.property('synthetic_index'); + expect(DISPLAY_NAME_MAPPINGS.markets['synthetic_index']).to.equal('Derived'); + + expect(DISPLAY_NAME_MAPPINGS.markets).to.have.property('commodities'); + expect(DISPLAY_NAME_MAPPINGS.markets['commodities']).to.equal('Commodities'); + + expect(DISPLAY_NAME_MAPPINGS.markets).to.have.property('indices'); + expect(DISPLAY_NAME_MAPPINGS.markets['indices']).to.equal('Stock Indices'); + + expect(DISPLAY_NAME_MAPPINGS.markets).to.have.property('cryptocurrency'); + expect(DISPLAY_NAME_MAPPINGS.markets['cryptocurrency']).to.equal('Cryptocurrencies'); + }); + + it('should have all market values as non-empty strings', () => { + Object.entries(DISPLAY_NAME_MAPPINGS.markets).forEach(([key, value]) => { + expect(key).to.be.a('string').that.is.not.empty; + expect(value).to.be.a('string').that.is.not.empty; + }); + }); + }); + + describe('submarket mappings', () => { + it('should contain forex submarkets', () => { + expect(DISPLAY_NAME_MAPPINGS.submarkets).to.have.property('major_pairs'); + expect(DISPLAY_NAME_MAPPINGS.submarkets['major_pairs']).to.equal('Major Pairs'); + + expect(DISPLAY_NAME_MAPPINGS.submarkets).to.have.property('minor_pairs'); + expect(DISPLAY_NAME_MAPPINGS.submarkets['minor_pairs']).to.equal('Minor Pairs'); + + expect(DISPLAY_NAME_MAPPINGS.submarkets).to.have.property('exotic_pairs'); + expect(DISPLAY_NAME_MAPPINGS.submarkets['exotic_pairs']).to.equal('Exotic Pairs'); + }); + + it('should contain synthetic index submarkets', () => { + expect(DISPLAY_NAME_MAPPINGS.submarkets).to.have.property('continuous_indices'); + expect(DISPLAY_NAME_MAPPINGS.submarkets['continuous_indices']).to.equal('Continuous Indices'); + + expect(DISPLAY_NAME_MAPPINGS.submarkets).to.have.property('daily_reset_indices'); + expect(DISPLAY_NAME_MAPPINGS.submarkets['daily_reset_indices']).to.equal('Daily Reset Indices'); + + expect(DISPLAY_NAME_MAPPINGS.submarkets).to.have.property('crash_boom'); + expect(DISPLAY_NAME_MAPPINGS.submarkets['crash_boom']).to.equal('Crash/Boom'); + }); + + it('should have all submarket values as non-empty strings', () => { + Object.entries(DISPLAY_NAME_MAPPINGS.submarkets).forEach(([key, value]) => { + expect(key).to.be.a('string').that.is.not.empty; + expect(value).to.be.a('string').that.is.not.empty; + }); + }); + }); + + describe('subgroup mappings', () => { + it('should contain major subgroups', () => { + expect(DISPLAY_NAME_MAPPINGS.subgroups).to.have.property('forex'); + expect(DISPLAY_NAME_MAPPINGS.subgroups['forex']).to.equal('Forex'); + + expect(DISPLAY_NAME_MAPPINGS.subgroups).to.have.property('indices'); + expect(DISPLAY_NAME_MAPPINGS.subgroups['indices']).to.equal('Indices'); + + expect(DISPLAY_NAME_MAPPINGS.subgroups).to.have.property('commodities'); + expect(DISPLAY_NAME_MAPPINGS.subgroups['commodities']).to.equal('Commodities'); + + expect(DISPLAY_NAME_MAPPINGS.subgroups).to.have.property('cryptocurrencies'); + expect(DISPLAY_NAME_MAPPINGS.subgroups['cryptocurrencies']).to.equal('Cryptocurrencies'); + }); + + it('should have hidden category for none', () => { + expect(DISPLAY_NAME_MAPPINGS.subgroups).to.have.property('none'); + expect(DISPLAY_NAME_MAPPINGS.subgroups['none']).to.equal(''); + }); + + it('should have all subgroup keys as non-empty strings', () => { + Object.entries(DISPLAY_NAME_MAPPINGS.subgroups).forEach(([key, value]) => { + expect(key).to.be.a('string').that.is.not.empty; + expect(value).to.be.a('string'); // Value can be empty for hidden categories + }); + }); + }); + + describe('DEFAULT_DISPLAY_NAME_OPTIONS', () => { + it('should have expected default options', () => { + expect(DEFAULT_DISPLAY_NAME_OPTIONS).to.have.property('fallbackToFormatted'); + expect(DEFAULT_DISPLAY_NAME_OPTIONS.fallbackToFormatted).to.be.true; + + expect(DEFAULT_DISPLAY_NAME_OPTIONS).to.have.property('logMissing'); + expect(DEFAULT_DISPLAY_NAME_OPTIONS.logMissing).to.be.true; + + expect(DEFAULT_DISPLAY_NAME_OPTIONS).to.have.property('cacheResults'); + expect(DEFAULT_DISPLAY_NAME_OPTIONS.cacheResults).to.be.true; + }); + }); + + describe('HIDDEN_CATEGORIES', () => { + it('should be a Set', () => { + expect(HIDDEN_CATEGORIES).to.be.instanceOf(Set); + }); + + it('should contain none category', () => { + expect(HIDDEN_CATEGORIES.has('none')).to.be.true; + }); + + it('should have at least one hidden category', () => { + expect(HIDDEN_CATEGORIES.size).to.be.greaterThan(0); + }); + }); + + describe('FORMATTING_RULES', () => { + it('should be an object', () => { + expect(FORMATTING_RULES).to.be.an('object'); + }); + + it('should contain formatting functions', () => { + expect(FORMATTING_RULES).to.have.property('forex'); + expect(FORMATTING_RULES.forex).to.be.a('function'); + + expect(FORMATTING_RULES).to.have.property('cryptocurrency'); + expect(FORMATTING_RULES.cryptocurrency).to.be.a('function'); + + expect(FORMATTING_RULES).to.have.property('indices'); + expect(FORMATTING_RULES.indices).to.be.a('function'); + }); + + it('should format forex values correctly', () => { + const result = FORMATTING_RULES.forex('eurusd'); + expect(result).to.equal('EURUSD'); + }); + + it('should format cryptocurrency values correctly', () => { + const result = FORMATTING_RULES.cryptocurrency('BITCOIN'); + expect(result).to.equal('Bitcoin'); + }); + + it('should format indices values correctly', () => { + const result = FORMATTING_RULES.indices('stock_index'); + expect(result).to.equal('Stock Index'); + }); + }); + + describe('data consistency', () => { + it('should not have duplicate values in symbol mappings', () => { + const values = Object.values(DISPLAY_NAME_MAPPINGS.symbols); + const uniqueValues = [...new Set(values)]; + expect(values.length).to.equal(uniqueValues.length); + }); + + it('should not have duplicate values in market mappings', () => { + const values = Object.values(DISPLAY_NAME_MAPPINGS.markets); + const uniqueValues = [...new Set(values)]; + expect(values.length).to.equal(uniqueValues.length); + }); + + it('should not have duplicate values in submarket mappings', () => { + const values = Object.values(DISPLAY_NAME_MAPPINGS.submarkets); + const uniqueValues = [...new Set(values)]; + expect(values.length).to.equal(uniqueValues.length); + }); + + it('should not have duplicate non-empty values in subgroup mappings', () => { + const values = Object.values(DISPLAY_NAME_MAPPINGS.subgroups).filter(v => v !== ''); + const uniqueValues = [...new Set(values)]; + expect(values.length).to.equal(uniqueValues.length); + }); + }); + + describe('coverage validation', () => { + it('should have comprehensive coverage of symbols', () => { + const symbolCount = Object.keys(DISPLAY_NAME_MAPPINGS.symbols).length; + expect(symbolCount).to.be.greaterThan(80); // Should have at least 80 symbol mappings + }); + + it('should have reasonable coverage of markets', () => { + const marketCount = Object.keys(DISPLAY_NAME_MAPPINGS.markets).length; + expect(marketCount).to.be.greaterThan(8); // Should have at least 8 market mappings + }); + + it('should have comprehensive coverage of submarkets', () => { + const submarketCount = Object.keys(DISPLAY_NAME_MAPPINGS.submarkets).length; + expect(submarketCount).to.be.greaterThan(25); // Should have at least 25 submarket mappings + }); + + it('should have reasonable coverage of subgroups', () => { + const subgroupCount = Object.keys(DISPLAY_NAME_MAPPINGS.subgroups).length; + expect(subgroupCount).to.be.greaterThan(15); // Should have at least 15 subgroup mappings + }); + }); +}); \ No newline at end of file diff --git a/src/config/displayNameTypes.ts b/src/config/displayNameTypes.ts new file mode 100644 index 0000000000..2cf8008386 --- /dev/null +++ b/src/config/displayNameTypes.ts @@ -0,0 +1,44 @@ +/** + * TypeScript interfaces for the display name mapping system + */ + +export interface ISymbolDisplayNames { + [symbolCode: string]: string; +} + +export interface IMarketDisplayNames { + [marketCode: string]: string; +} + +export interface ISubmarketDisplayNames { + [submarketCode: string]: string; +} + +export interface ISubgroupDisplayNames { + [subgroupCode: string]: string; +} + +export interface IDisplayNameMappings { + symbols: ISymbolDisplayNames; + markets: IMarketDisplayNames; + submarkets: ISubmarketDisplayNames; + subgroups: ISubgroupDisplayNames; +} + +export interface IDisplayNameOptions { + fallbackToFormatted?: boolean; + logMissing?: boolean; + cacheResults?: boolean; +} + +export type DisplayNameType = 'symbol' | 'market' | 'submarket' | 'subgroup'; + +export interface IDisplayNameService { + getSymbolDisplayName(symbolCode: string, options?: IDisplayNameOptions): string; + getMarketDisplayName(marketCode: string, options?: IDisplayNameOptions): string; + getSubmarketDisplayName(submarketCode: string, options?: IDisplayNameOptions): string; + getSubgroupDisplayName(subgroupCode: string, options?: IDisplayNameOptions): string; + formatRawValue(rawValue: string): string; + addMapping(type: DisplayNameType, code: string, displayName: string): void; + clearCache(): void; +} \ No newline at end of file diff --git a/src/config/displayNames.ts b/src/config/displayNames.ts new file mode 100644 index 0000000000..c455e95ce3 --- /dev/null +++ b/src/config/displayNames.ts @@ -0,0 +1,293 @@ +import { IDisplayNameMappings } from './displayNameTypes'; + +/** + * Comprehensive display name mappings for symbols, markets, submarkets, and subgroups + * This configuration provides human-readable names for all raw API values + * Updated to match exact user specification + */ + +export const DISPLAY_NAME_MAPPINGS: IDisplayNameMappings = { + // Symbol display names - mapping symbol codes to user-friendly names + symbols: { + // Major Pairs + 'frxAUDJPY': 'AUD/JPY', + 'frxAUDUSD': 'AUD/USD', + 'frxEURAUD': 'EUR/AUD', + 'frxEURCAD': 'EUR/CAD', + 'frxEURCHF': 'EUR/CHF', + 'frxEURGBP': 'EUR/GBP', + 'frxEURJPY': 'EUR/JPY', + 'frxEURUSD': 'EUR/USD', + 'frxGBPAUD': 'GBP/AUD', + 'frxGBPJPY': 'GBP/JPY', + 'frxGBPUSD': 'GBP/USD', + 'frxUSDCAD': 'USD/CAD', + 'frxUSDCHF': 'USD/CHF', + 'frxUSDJPY': 'USD/JPY', + + // Minor Pairs + 'frxAUDCAD': 'AUD/CAD', + 'frxAUDCHF': 'AUD/CHF', + 'frxAUDNZD': 'AUD/NZD', + 'frxEURNZD': 'EUR/NZD', + 'frxGBPCAD': 'GBP/CAD', + 'frxGBPCHF': 'GBP/CHF', + 'frxGBPNZD': 'GBP/NZD', + 'frxNZDJPY': 'NZD/JPY', + 'frxNZDUSD': 'NZD/USD', + 'frxUSDMXN': 'USD/MXN', + 'frxUSDPLN': 'USD/PLN', + + // Basket indices + 'WLDXAU': 'Gold Basket', + 'WLDAUD': 'AUD Basket', + 'WLDEUR': 'EUR Basket', + 'WLDGBP': 'GBP Basket', + 'WLDUSD': 'USD Basket', + + // Continuous Indices - Volatility + 'R_10': 'Volatility 10 Index', + 'R_25': 'Volatility 25 Index', + 'R_50': 'Volatility 50 Index', + 'R_75': 'Volatility 75 Index', + 'R_100': 'Volatility 100 Index', + '1HZ10V': 'Volatility 10 (1s) Index', + '1HZ25V': 'Volatility 25 (1s) Index', + '1HZ50V': 'Volatility 50 (1s) Index', + '1HZ75V': 'Volatility 75 (1s) Index', + '1HZ100V': 'Volatility 100 (1s) Index', + '1HZ150V': 'Volatility 150 (1s) Index', + '1HZ250V': 'Volatility 250 (1s) Index', + + // Crash/Boom Indices + 'BOOM300N': 'Boom 300 Index', + 'BOOM500': 'Boom 500 Index', + 'BOOM600': 'Boom 600 Index', + 'BOOM900': 'Boom 900 Index', + 'BOOM1000': 'Boom 1000 Index', + 'CRASH300N': 'Crash 300 Index', + 'CRASH500': 'Crash 500 Index', + 'CRASH600': 'Crash 600 Index', + 'CRASH900': 'Crash 900 Index', + 'CRASH1000': 'Crash 1000 Index', + + // Daily Reset Indices + 'RDBEAR': 'Bear Market Index', + 'RDBULL': 'Bull Market Index', + + // Jump Indices + 'JD10': 'Jump 10 Index', + 'JD25': 'Jump 25 Index', + 'JD50': 'Jump 50 Index', + 'JD75': 'Jump 75 Index', + 'JD100': 'Jump 100 Index', + + // Step Indices - Updated to match actual API symbols + 'stpRNG': 'Step Index 100', + 'stpRNG2': 'Step Index 200', + 'stpRNG3': 'Step Index 300', + 'stpRNG4': 'Step Index 400', + 'stpRNG5': 'Step Index 500', + + // American indices + 'OTC_SPC': 'US 500', + 'OTC_NDX': 'US Tech 100', + 'OTC_DJI': 'Wall Street 30', + + // Asian indices + 'OTC_AS51': 'Australia 200', + 'OTC_HSI': 'Hong Kong 50', + 'OTC_N225': 'Japan 225', + + // European indices + 'OTC_SX5E': 'Euro 50', + 'OTC_FCHI': 'France 40', + 'OTC_GDAXI': 'Germany 40', + 'OTC_AEX': 'Netherlands 25', + 'OTC_SMI': 'Swiss 20', + 'OTC_FTSE': 'UK 100', + + // Cryptocurrencies + 'cryBTCUSD': 'BTC/USD', + 'cryETHUSD': 'ETH/USD', + + // Metals + 'frxXAUUSD': 'Gold/USD', + 'frxXPDUSD': 'Palladium/USD', + 'frxXPTUSD': 'Platinum/USD', + 'frxXAGUSD': 'Silver/USD', + + // Additional Volatility Indices (1s) + '1HZ200V': 'Volatility 200 (1s) Index', + '1HZ300V': 'Volatility 300 (1s) Index', + + // Legacy Step Indices mappings (if needed for backward compatibility) + 'STPIDX100': 'Step Index 100', + 'STPIDX200': 'Step Index 200', + 'STPIDX300': 'Step Index 300', + 'STPIDX400': 'Step Index 400', + 'STPIDX500': 'Step Index 500', + + // Additional Jump Indices + 'JD200': 'Jump 200 Index', + 'JD300': 'Jump 300 Index', + + // Additional Daily Reset Indices + 'RDBEAR10': 'Bear Market 10 Index', + 'RDBULL10': 'Bull Market 10 Index', + 'RDBEAR25': 'Bear Market 25 Index', + 'RDBULL25': 'Bull Market 25 Index', + + // Additional Crash/Boom Indices + 'BOOM250': 'Boom 250 Index', + 'CRASH250': 'Crash 250 Index', + + // Additional Forex pairs + 'frxCADJPY': 'CAD/JPY', + 'frxCHFJPY': 'CHF/JPY', + 'frxCADCHF': 'CAD/CHF', + 'frxNZDCAD': 'NZD/CAD', + 'frxNZDCHF': 'NZD/CHF', + + // Additional Cryptocurrencies + 'cryLTCUSD': 'LTC/USD', + 'cryBCHUSD': 'BCH/USD', + 'cryXRPUSD': 'XRP/USD', + 'cryADAUSD': 'ADA/USD', + 'cryDOTUSD': 'DOT/USD', + + // Additional case variations for Step Indices (if needed) + 'STPRNG': 'Step Index 100', + 'STPRNG2': 'Step Index 200', + 'STPRNG3': 'Step Index 300', + 'STPRNG4': 'Step Index 400', + 'STPRNG5': 'Step Index 500', + }, + + // Market display names + markets: { + 'forex': 'Forex', + 'indices': 'Stock Indices', + 'stocks': 'Stocks', + 'commodities': 'Commodities', + 'cryptocurrency': 'Cryptocurrencies', + 'synthetic_index': 'Derived', + 'basket_index': 'Baskets', + 'energy': 'Energy', + 'metals': 'Metals', + 'agricultural': 'Agricultural', + }, + + // Submarket display names + submarkets: { + 'major_pairs': 'Major Pairs', + 'minor_pairs': 'Minor Pairs', + 'exotic_pairs': 'Exotic Pairs', + 'smart_fx': 'Smart FX', + 'americas': 'American indices', + 'asia_oceania': 'Asian indices', + 'europe_africa': 'European indices', + 'europe': 'European indices', + 'americas_OTC': 'American indices', + 'asia_oceania_OTC': 'Asian indices', + 'europe_OTC': 'European indices', + 'otc_index': 'OTC Indices', + 'random_index': 'Continuous Indices', + 'random_daily': 'Daily Reset Indices', + 'crash_boom': 'Crash/Boom Indices', + 'crash_index': 'Crash/Boom Indices', + 'jump_index': 'Jump Indices', + 'step_index': 'Step Indices', + 'volatility_indices': 'Volatility Indices', + 'range_break_indices': 'Range Break Indices', + 'forex_basket': 'Forex Basket', + 'commodity_basket': 'Commodities Basket', + 'cryptocurrency_basket': 'Cryptocurrency Basket', + 'energy_basket': 'Energy Basket', + 'precious_metals': 'Precious Metals', + 'base_metals': 'Base Metals', + 'grains': 'Grains', + 'soft_commodities': 'Soft Commodities', + 'livestock': 'Livestock', + 'crypto_usd': 'Cryptocurrencies', + 'non_stable_coin': 'Cryptocurrencies', + 'crypto_non_usd': 'Crypto/Non-USD', + 'us_stocks': 'US Stocks', + 'european_stocks': 'European Stocks', + 'asian_stocks': 'Asian Stocks', + 'metals': 'Metals', + }, + + // Subgroup display names + subgroups: { + 'none': '', + 'major': 'Major', + 'minor': 'Minor', + 'exotic': 'Exotic', + 'micro': 'Micro', + 'smart': 'Smart', + 'baskets': 'Baskets', + 'commodities_basket': 'Commodities Basket', + 'forex_basket': 'Forex Basket', + 'forex': 'Forex', + 'indices': 'Indices', + 'commodities': 'Commodities', + 'energy': 'Energy', + 'metals': 'Metals', + 'agricultural': 'Agricultural', + 'cryptocurrencies': 'Cryptocurrencies', + 'stocks': 'Stocks', + 'bonds': 'Bonds', + 'etfs': 'ETFs', + 'futures': 'Futures', + 'options': 'Options', + 'cfds': 'CFDs', + 'spread_bets': 'Spread Bets', + 'binary_options': 'Binary Options', + 'multipliers': 'Multipliers', + 'accumulators': 'Accumulators', + 'vanillas': 'Vanillas', + 'turbos': 'Turbos', + 'lookbacks': 'Lookbacks', + 'touch_no_touch': 'Touch/No Touch', + 'ends_between': 'Ends Between/Outside', + 'stays_between': 'Stays Between/Goes Outside', + 'asian_options': 'Asian Options', + 'digits': 'Digits', + 'reset_call_put': 'Reset Call/Put', + 'high_low_ticks': 'High/Low Ticks', + 'only_ups_downs': 'Only Ups/Only Downs', + 'lb_high_low': 'Lookback High/Low', + 'lb_close': 'Lookback Close', + 'run_high_low': 'Run High/Low', + }, + +}; + +/** + * Default options for display name service + */ +export const DEFAULT_DISPLAY_NAME_OPTIONS = { + fallbackToFormatted: true, + logMissing: true, + cacheResults: true, +}; + +/** + * Categories that should be hidden from display (empty string display names) + */ +export const HIDDEN_CATEGORIES = new Set(['none']); + +/** + * Special formatting rules for specific categories + */ +export const FORMATTING_RULES = { + // Forex pairs should always be uppercase + forex: (value: string) => value.toUpperCase(), + // Crypto symbols should have proper casing + cryptocurrency: (value: string) => value.charAt(0).toUpperCase() + value.slice(1).toLowerCase(), + // Index names should be title case + indices: (value: string) => value.split('_').map(word => + word.charAt(0).toUpperCase() + word.slice(1).toLowerCase() + ).join(' '), +}; \ No newline at end of file diff --git a/src/feed/Feed.ts b/src/feed/Feed.ts index 142c7a0f8b..e47143591f 100644 --- a/src/feed/Feed.ts +++ b/src/feed/Feed.ts @@ -6,6 +6,7 @@ import { TCreateTickHistoryParams } from 'src/binaryapi/BinaryAPI'; import { Listener, TError, TGranularity, TMainStore, TPaginationCallback, TQuote } from 'src/types'; import { strToDateTime } from 'src/utils/date'; import { getUTCDate } from '../utils'; +import { getSymbolDisplayName } from '../utils/displayNameUtils'; import ServerTime from '../utils/ServerTime'; import { DelayedSubscription, RealtimeSubscription } from './subscription'; import { TQuoteResponse } from './subscription/Subscription'; @@ -229,7 +230,7 @@ class Feed { let start = this.margin && this.startEpoch ? this.startEpoch - this.margin : this.startEpoch; const end = this.margin && this.endEpoch ? this.endEpoch + this.margin : this.endEpoch; - const symbolName = symbolObject.name; + const symbolName = getSymbolDisplayName(symbolObject.symbol) || symbolObject.name; this.loader.setState('chart-data'); if (this._tradingTimes.isFeedUnavailable(symbol)) { this._mainStore.notifier.notifyFeedUnavailable(symbolName); diff --git a/src/services/DisplayNameService.ts b/src/services/DisplayNameService.ts new file mode 100644 index 0000000000..e9429dab3d --- /dev/null +++ b/src/services/DisplayNameService.ts @@ -0,0 +1,285 @@ +import { + IDisplayNameService, + IDisplayNameOptions, + DisplayNameType, +} from '../config/displayNameTypes'; +import { + DISPLAY_NAME_MAPPINGS, + DEFAULT_DISPLAY_NAME_OPTIONS, + HIDDEN_CATEGORIES, + FORMATTING_RULES, +} from '../config/displayNames'; + +/** + * Centralized service for managing display name mappings + * Provides caching, fallback mechanisms, and extensibility + */ +export class DisplayNameService implements IDisplayNameService { + private cache = new Map(); + private missingMappings = new Set(); + + /** + * Get display name for a symbol + */ + getSymbolDisplayName(symbolCode: string, options?: IDisplayNameOptions): string { + const opts = { ...DEFAULT_DISPLAY_NAME_OPTIONS, ...options }; + const cacheKey = `symbol:${symbolCode}`; + + if (opts.cacheResults && this.cache.has(cacheKey)) { + return this.cache.get(cacheKey)!; + } + + let displayName = DISPLAY_NAME_MAPPINGS.symbols[symbolCode]; + + if (!displayName) { + if (opts.logMissing && !this.missingMappings.has(cacheKey)) { + console.warn(`Missing symbol display name mapping for: ${symbolCode}`); + this.missingMappings.add(cacheKey); + } + + if (opts.fallbackToFormatted) { + displayName = this.formatRawValue(symbolCode); + } else { + displayName = symbolCode; + } + } + + if (opts.cacheResults) { + this.cache.set(cacheKey, displayName); + } + + return displayName; + } + + /** + * Get display name for a market + */ + getMarketDisplayName(marketCode: string, options?: IDisplayNameOptions): string { + const opts = { ...DEFAULT_DISPLAY_NAME_OPTIONS, ...options }; + const cacheKey = `market:${marketCode}`; + + if (opts.cacheResults && this.cache.has(cacheKey)) { + return this.cache.get(cacheKey)!; + } + + let displayName = DISPLAY_NAME_MAPPINGS.markets[marketCode]; + + if (!displayName) { + if (opts.logMissing && !this.missingMappings.has(cacheKey)) { + console.warn(`Missing market display name mapping for: ${marketCode}`); + this.missingMappings.add(cacheKey); + } + + if (opts.fallbackToFormatted) { + displayName = this.formatRawValue(marketCode); + // Apply market-specific formatting rules + if (FORMATTING_RULES.indices && marketCode.includes('index')) { + displayName = FORMATTING_RULES.indices(displayName); + } + } else { + displayName = marketCode; + } + } + + if (opts.cacheResults) { + this.cache.set(cacheKey, displayName); + } + + return displayName; + } + + /** + * Get display name for a submarket + */ + getSubmarketDisplayName(submarketCode: string, options?: IDisplayNameOptions): string { + const opts = { ...DEFAULT_DISPLAY_NAME_OPTIONS, ...options }; + const cacheKey = `submarket:${submarketCode}`; + + if (opts.cacheResults && this.cache.has(cacheKey)) { + return this.cache.get(cacheKey)!; + } + + let displayName = DISPLAY_NAME_MAPPINGS.submarkets[submarketCode]; + + if (!displayName) { + if (opts.logMissing && !this.missingMappings.has(cacheKey)) { + console.warn(`Missing submarket display name mapping for: ${submarketCode}`); + this.missingMappings.add(cacheKey); + } + + if (opts.fallbackToFormatted) { + displayName = this.formatRawValue(submarketCode); + } else { + displayName = submarketCode; + } + } + + if (opts.cacheResults) { + this.cache.set(cacheKey, displayName); + } + + return displayName; + } + + /** + * Get display name for a subgroup + */ + getSubgroupDisplayName(subgroupCode: string, options?: IDisplayNameOptions): string { + const opts = { ...DEFAULT_DISPLAY_NAME_OPTIONS, ...options }; + const cacheKey = `subgroup:${subgroupCode}`; + + // Handle hidden categories + if (HIDDEN_CATEGORIES.has(subgroupCode)) { + return ''; + } + + if (opts.cacheResults && this.cache.has(cacheKey)) { + return this.cache.get(cacheKey)!; + } + + let displayName = DISPLAY_NAME_MAPPINGS.subgroups[subgroupCode]; + + if (!displayName) { + if (opts.logMissing && !this.missingMappings.has(cacheKey)) { + console.warn(`Missing subgroup display name mapping for: ${subgroupCode}`); + this.missingMappings.add(cacheKey); + } + + if (opts.fallbackToFormatted) { + displayName = this.formatRawValue(subgroupCode); + } else { + displayName = subgroupCode; + } + } + + if (opts.cacheResults) { + this.cache.set(cacheKey, displayName); + } + + return displayName; + } + + /** + * Format raw value into a more readable format + */ + formatRawValue(rawValue: string): string { + if (!rawValue) return ''; + + return rawValue + // Replace underscores with spaces + .replace(/_/g, ' ') + // Replace hyphens with spaces + .replace(/-/g, ' ') + // Convert to title case + .split(' ') + .map(word => { + if (word.length === 0) return word; + return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); + }) + .join(' ') + // Clean up extra spaces + .trim() + .replace(/\s+/g, ' '); + } + + /** + * Add a new mapping dynamically + */ + addMapping(type: DisplayNameType, code: string, displayName: string): void { + switch (type) { + case 'symbol': + DISPLAY_NAME_MAPPINGS.symbols[code] = displayName; + break; + case 'market': + DISPLAY_NAME_MAPPINGS.markets[code] = displayName; + break; + case 'submarket': + DISPLAY_NAME_MAPPINGS.submarkets[code] = displayName; + break; + case 'subgroup': + DISPLAY_NAME_MAPPINGS.subgroups[code] = displayName; + break; + } + + // Clear cache for this item + const cacheKey = `${type}:${code}`; + this.cache.delete(cacheKey); + this.missingMappings.delete(cacheKey); + } + + /** + * Clear the cache + */ + clearCache(): void { + this.cache.clear(); + this.missingMappings.clear(); + } + + /** + * Get all missing mappings for debugging + */ + getMissingMappings(): string[] { + return Array.from(this.missingMappings); + } + + /** + * Get cache statistics + */ + getCacheStats(): { size: number; missingCount: number } { + return { + size: this.cache.size, + missingCount: this.missingMappings.size, + }; + } + + /** + * Batch get display names for multiple items of the same type + */ + batchGetDisplayNames( + type: DisplayNameType, + codes: string[], + options?: IDisplayNameOptions + ): Record { + const result: Record = {}; + + for (const code of codes) { + switch (type) { + case 'symbol': + result[code] = this.getSymbolDisplayName(code, options); + break; + case 'market': + result[code] = this.getMarketDisplayName(code, options); + break; + case 'submarket': + result[code] = this.getSubmarketDisplayName(code, options); + break; + case 'subgroup': + result[code] = this.getSubgroupDisplayName(code, options); + break; + } + } + + return result; + } + + /** + * Check if a mapping exists for a given code and type + */ + hasMapping(type: DisplayNameType, code: string): boolean { + switch (type) { + case 'symbol': + return code in DISPLAY_NAME_MAPPINGS.symbols; + case 'market': + return code in DISPLAY_NAME_MAPPINGS.markets; + case 'submarket': + return code in DISPLAY_NAME_MAPPINGS.submarkets; + case 'subgroup': + return code in DISPLAY_NAME_MAPPINGS.subgroups; + default: + return false; + } + } +} + +// Export singleton instance +export const displayNameService = new DisplayNameService(); \ No newline at end of file diff --git a/src/services/__tests__/DisplayNameService.spec.ts b/src/services/__tests__/DisplayNameService.spec.ts new file mode 100644 index 0000000000..229dda5641 --- /dev/null +++ b/src/services/__tests__/DisplayNameService.spec.ts @@ -0,0 +1,218 @@ +import { expect } from 'chai'; +import { DisplayNameService } from '../DisplayNameService'; + +describe('DisplayNameService', () => { + let service: DisplayNameService; + + beforeEach(() => { + service = new DisplayNameService(); + service.clearCache(); // Clear cache before each test + }); + + describe('getSymbolDisplayName', () => { + it('should return display name for known symbol', () => { + const result = service.getSymbolDisplayName('R_10'); + expect(result).to.equal('Volatility 10 Index'); + }); + + it('should return raw symbol for unknown symbol with fallback disabled', () => { + const result = service.getSymbolDisplayName('UNKNOWN_SYMBOL', { fallbackToFormatted: false }); + expect(result).to.equal('UNKNOWN_SYMBOL'); + }); + + it('should return formatted name for unknown symbol with fallback enabled', () => { + const result = service.getSymbolDisplayName('test_symbol', { fallbackToFormatted: true }); + expect(result).to.equal('Test Symbol'); + }); + + it('should cache results when caching is enabled', () => { + const options = { cacheResults: true }; + const result1 = service.getSymbolDisplayName('R_10', options); + const result2 = service.getSymbolDisplayName('R_10', options); + + expect(result1).to.equal(result2); + expect(service.getCacheStats().size).to.be.greaterThan(0); + }); + + it('should log missing mappings when enabled', () => { + let loggedMessage = ''; + const originalWarn = console.warn; + console.warn = (message: string) => { loggedMessage = message; }; + + service.getSymbolDisplayName('MISSING_SYMBOL', { logMissing: true }); + + expect(loggedMessage).to.include('Missing symbol display name mapping for: MISSING_SYMBOL'); + console.warn = originalWarn; + }); + }); + + describe('getMarketDisplayName', () => { + it('should return display name for known market', () => { + const result = service.getMarketDisplayName('forex'); + expect(result).to.equal('Forex'); + }); + + it('should return formatted name for unknown market with fallback', () => { + const result = service.getMarketDisplayName('test_market', { fallbackToFormatted: true }); + expect(result).to.equal('Test Market'); + }); + + it('should apply formatting rules for indices', () => { + const result = service.getMarketDisplayName('indices', { fallbackToFormatted: true }); + expect(result).to.equal('Stock Indices'); // Should match actual mapping + }); + }); + + describe('getSubmarketDisplayName', () => { + it('should return display name for known submarket', () => { + const result = service.getSubmarketDisplayName('major_pairs'); + expect(result).to.equal('Major Pairs'); + }); + + it('should return formatted name for unknown submarket', () => { + const result = service.getSubmarketDisplayName('minor_pairs', { fallbackToFormatted: true }); + expect(result).to.equal('Minor Pairs'); + }); + }); + + describe('getSubgroupDisplayName', () => { + it('should return display name for known subgroup', () => { + const result = service.getSubgroupDisplayName('synthetics'); + expect(result).to.equal('Synthetics'); + }); + + it('should return empty string for hidden categories', () => { + const result = service.getSubgroupDisplayName('none'); + expect(result).to.equal(''); + }); + + it('should return formatted name for unknown subgroup', () => { + const result = service.getSubgroupDisplayName('test_subgroup', { fallbackToFormatted: true }); + expect(result).to.equal('Test Subgroup'); + }); + }); + + describe('formatRawValue', () => { + it('should format underscores to spaces and title case', () => { + const result = service.formatRawValue('test_raw_value'); + expect(result).to.equal('Test Raw Value'); + }); + + it('should format hyphens to spaces and title case', () => { + const result = service.formatRawValue('test-raw-value'); + expect(result).to.equal('Test Raw Value'); + }); + + it('should handle empty string', () => { + const result = service.formatRawValue(''); + expect(result).to.equal(''); + }); + + it('should clean up extra spaces', () => { + const result = service.formatRawValue('test__double__underscore'); + expect(result).to.equal('Test Double Underscore'); + }); + }); + + describe('addMapping', () => { + it('should add new symbol mapping', () => { + service.addMapping('symbol', 'NEW_SYMBOL', 'New Symbol Display Name'); + const result = service.getSymbolDisplayName('NEW_SYMBOL'); + expect(result).to.equal('New Symbol Display Name'); + }); + + it('should add new market mapping', () => { + service.addMapping('market', 'new_market', 'New Market'); + const result = service.getMarketDisplayName('new_market'); + expect(result).to.equal('New Market'); + }); + + it('should clear cache when adding mapping', () => { + service.getSymbolDisplayName('R_10', { cacheResults: true }); + expect(service.getCacheStats().size).to.be.greaterThan(0); + + service.addMapping('symbol', 'R_10', 'Updated Name'); + // Cache should be cleared for this specific item + const result = service.getSymbolDisplayName('R_10'); + expect(result).to.equal('Updated Name'); + }); + }); + + describe('batchGetDisplayNames', () => { + it('should return display names for multiple symbols', () => { + const codes = ['R_10', 'R_25', 'UNKNOWN']; + const result = service.batchGetDisplayNames('symbol', codes); + + expect(result['R_10']).to.equal('Volatility 10 Index'); + expect(result['R_25']).to.equal('Volatility 25 Index'); + expect(result['UNKNOWN']).to.equal('UNKNOWN'); + }); + + it('should return display names for multiple markets', () => { + const codes = ['forex', 'synthetic_index']; + const result = service.batchGetDisplayNames('market', codes); + + expect(result['forex']).to.equal('Forex'); + expect(result['synthetic_index']).to.equal('Derived'); + }); + }); + + describe('hasMapping', () => { + it('should return true for existing symbol mapping', () => { + const result = service.hasMapping('symbol', 'R_10'); + expect(result).to.be.true; + }); + + it('should return false for non-existing symbol mapping', () => { + const result = service.hasMapping('symbol', 'NON_EXISTING'); + expect(result).to.be.false; + }); + + it('should return true for existing market mapping', () => { + const result = service.hasMapping('market', 'forex'); + expect(result).to.be.true; + }); + }); + + describe('clearCache', () => { + it('should clear all cached results', () => { + service.getSymbolDisplayName('R_10', { cacheResults: true }); + expect(service.getCacheStats().size).to.be.greaterThan(0); + + service.clearCache(); + expect(service.getCacheStats().size).to.equal(0); + }); + }); + + describe('getMissingMappings', () => { + it('should track missing mappings when logging is enabled', () => { + const originalWarn = console.warn; + console.warn = () => {}; // Suppress console output + + service.getSymbolDisplayName('MISSING1', { logMissing: true }); + service.getMarketDisplayName('MISSING2', { logMissing: true }); + + const missing = service.getMissingMappings(); + expect(missing).to.include('symbol:MISSING1'); + expect(missing).to.include('market:MISSING2'); + + console.warn = originalWarn; + }); + }); + + describe('getCacheStats', () => { + it('should return correct cache statistics', () => { + const originalWarn = console.warn; + console.warn = () => {}; // Suppress console output + + service.getSymbolDisplayName('R_10', { cacheResults: true }); + service.getSymbolDisplayName('MISSING', { logMissing: true }); + + const stats = service.getCacheStats(); + expect(stats.size).to.equal(1); + expect(stats.missingCount).to.equal(1); + + console.warn = originalWarn; + }); + }); +}); \ No newline at end of file diff --git a/src/store/ChartStore.ts b/src/store/ChartStore.ts index 912d72ebf0..4a07518bde 100644 --- a/src/store/ChartStore.ts +++ b/src/store/ChartStore.ts @@ -381,12 +381,12 @@ class ChartStore { onMarketOpenClosedChange = (changes: TChanges) => { const symbolObjects = this.activeSymbols?.processedSymbols || []; let shouldRefreshChart = false; - for (const { symbol, name } of symbolObjects) { + for (const { symbol, name, displayName } of symbolObjects) { if (symbol in changes) { if (changes[symbol]) { shouldRefreshChart = true; this.chartClosedOpenThemeChange(false); - this.mainStore.notifier.notifyMarketOpen(name); + this.mainStore.notifier.notifyMarketOpen(displayName || name); } else { this.chartClosedOpenThemeChange(true); this.mainStore.notifier.notifyMarketClose(name); diff --git a/src/store/CrosshairStore.ts b/src/store/CrosshairStore.ts index fc7cffc620..8e0e434de6 100644 --- a/src/store/CrosshairStore.ts +++ b/src/store/CrosshairStore.ts @@ -3,6 +3,7 @@ import moment from 'moment'; import Context from 'src/components/ui/Context'; import { TQuote } from 'src/types'; import { getTooltipLabels } from 'src/Constant'; +import { getSymbolDisplayName } from '../utils/displayNameUtils'; import MainStore from '.'; type TDupMap = { @@ -324,8 +325,9 @@ class CrosshairStore { } dupMap.Open = dupMap.High = dupMap.Low = 1; } - if (this.activeSymbol?.name) { - const display = this.activeSymbol?.name as string; + if (this.activeSymbol?.symbol) { + // Use display name instead of raw symbol name + const display = getSymbolDisplayName(this.activeSymbol.symbol) || this.activeSymbol.symbol; fields.push({ member: 'Close', display, diff --git a/src/utils/__tests__/displayNameUtils.spec.ts b/src/utils/__tests__/displayNameUtils.spec.ts new file mode 100644 index 0000000000..2bd17a5968 --- /dev/null +++ b/src/utils/__tests__/displayNameUtils.spec.ts @@ -0,0 +1,366 @@ +import { expect } from 'chai'; +import { + getSymbolDisplayName, + getMarketDisplayName, + getSubmarketDisplayName, + getSubgroupDisplayName, + formatRawValue, + getSymbolDisplayNames, + getSymbolCategoryPath, + getShortCategoryPath, + shouldHideSymbol, + getCachedDisplayNames, + getDebugDisplayNames, + batchGetSymbolDisplayNames, + searchSymbolsByDisplayName, + groupSymbolsByMarket, + groupSymbolsBySubmarket +} from '../displayNameUtils'; + +describe('displayNameUtils', () => { + describe('getSymbolDisplayName', () => { + it('should return display name for known symbol', () => { + const result = getSymbolDisplayName('R_10'); + expect(result).to.equal('Volatility 10 Index'); + }); + + it('should return formatted name for unknown symbol', () => { + const result = getSymbolDisplayName('test_symbol'); + expect(result).to.equal('Test Symbol'); + }); + + it('should handle empty string', () => { + const result = getSymbolDisplayName(''); + expect(result).to.equal(''); + }); + + it('should respect fallback options', () => { + const result = getSymbolDisplayName('unknown_symbol', { fallbackToFormatted: false }); + expect(result).to.equal('unknown_symbol'); + }); + }); + + describe('getMarketDisplayName', () => { + it('should return display name for known market', () => { + const result = getMarketDisplayName('forex'); + expect(result).to.equal('Forex'); + }); + + it('should return formatted name for unknown market', () => { + const result = getMarketDisplayName('test_market'); + expect(result).to.equal('Test Market'); + }); + + it('should handle empty string', () => { + const result = getMarketDisplayName(''); + expect(result).to.equal(''); + }); + }); + + describe('getSubmarketDisplayName', () => { + it('should return display name for known submarket', () => { + const result = getSubmarketDisplayName('major_pairs'); + expect(result).to.equal('Major Pairs'); + }); + + it('should return formatted name for unknown submarket', () => { + const result = getSubmarketDisplayName('test_submarket'); + expect(result).to.equal('Test Submarket'); + }); + }); + + describe('getSubgroupDisplayName', () => { + it('should return display name for known subgroup', () => { + const result = getSubgroupDisplayName('synthetics'); + expect(result).to.equal('Synthetics'); + }); + + it('should return empty string for hidden categories', () => { + const result = getSubgroupDisplayName('none'); + expect(result).to.equal(''); + }); + + it('should return formatted name for unknown subgroup', () => { + const result = getSubgroupDisplayName('test_subgroup'); + expect(result).to.equal('Test Subgroup'); + }); + }); + + describe('formatRawValue', () => { + it('should format underscores to spaces and title case', () => { + const result = formatRawValue('test_raw_value'); + expect(result).to.equal('Test Raw Value'); + }); + + it('should format hyphens to spaces and title case', () => { + const result = formatRawValue('test-raw-value'); + expect(result).to.equal('Test Raw Value'); + }); + + it('should handle empty string', () => { + const result = formatRawValue(''); + expect(result).to.equal(''); + }); + + it('should clean up extra spaces', () => { + const result = formatRawValue('test__double__underscore'); + expect(result).to.equal('Test Double Underscore'); + }); + }); + + describe('getSymbolDisplayNames', () => { + it('should return all display names for a symbol object', () => { + const symbol = { + symbol: 'R_10', + market: 'synthetic_index', + submarket: 'continuous_indices', + subgroup: 'synthetics' + }; + + const result = getSymbolDisplayNames(symbol); + + expect(result.symbolDisplayName).to.equal('Volatility 10 Index'); + expect(result.marketDisplayName).to.equal('Synthetic Indices'); + expect(result.submarketDisplayName).to.equal('Continuous Indices'); + expect(result.subgroupDisplayName).to.equal('Synthetics'); + }); + + it('should handle partial symbol object', () => { + const symbol = { + symbol: 'R_10', + market: 'synthetic_index' + }; + + const result = getSymbolDisplayNames(symbol); + + expect(result.symbolDisplayName).to.equal('Volatility 10 Index'); + expect(result.marketDisplayName).to.equal('Synthetic Indices'); + expect(result.submarketDisplayName).to.equal(''); + expect(result.subgroupDisplayName).to.equal(''); + }); + }); + + describe('getSymbolCategoryPath', () => { + it('should return full category path', () => { + const symbol = { + market: 'synthetic_index', + submarket: 'continuous_indices', + subgroup: 'synthetics' + }; + + const result = getSymbolCategoryPath(symbol); + expect(result).to.equal('Synthetic Indices > Continuous Indices > Synthetics'); + }); + + it('should skip hidden subgroups', () => { + const symbol = { + market: 'forex', + submarket: 'major_pairs', + subgroup: 'none' + }; + + const result = getSymbolCategoryPath(symbol); + expect(result).to.equal('Forex > Major Pairs'); + }); + + it('should handle partial paths', () => { + const symbol = { + market: 'forex' + }; + + const result = getSymbolCategoryPath(symbol); + expect(result).to.equal('Forex'); + }); + }); + + describe('getShortCategoryPath', () => { + it('should return market and submarket path', () => { + const symbol = { + market: 'forex', + submarket: 'major_pairs' + }; + + const result = getShortCategoryPath(symbol); + expect(result).to.equal('Forex > Major Pairs'); + }); + + it('should handle market only', () => { + const symbol = { + market: 'forex' + }; + + const result = getShortCategoryPath(symbol); + expect(result).to.equal('Forex'); + }); + }); + + describe('shouldHideSymbol', () => { + it('should return true for symbols with hidden subgroups', () => { + const symbol = { + market: 'forex', + submarket: 'major_pairs', + subgroup: 'none' + }; + + const result = shouldHideSymbol(symbol); + expect(result).to.be.true; + }); + + it('should return false for symbols with visible subgroups', () => { + const symbol = { + market: 'synthetic_index', + submarket: 'continuous_indices', + subgroup: 'synthetics' + }; + + const result = shouldHideSymbol(symbol); + expect(result).to.be.false; + }); + + it('should return false for symbols without subgroups', () => { + const symbol = { + market: 'forex', + submarket: 'major_pairs' + }; + + const result = shouldHideSymbol(symbol); + expect(result).to.be.false; + }); + }); + + describe('getCachedDisplayNames', () => { + it('should return display names with caching enabled', () => { + const symbol = { + symbol: 'R_10', + market: 'synthetic_index' + }; + + const result = getCachedDisplayNames(symbol); + + expect(result.symbolDisplayName).to.equal('Volatility 10 Index'); + expect(result.marketDisplayName).to.equal('Synthetic Indices'); + }); + }); + + describe('getDebugDisplayNames', () => { + it('should return display names with logging enabled', () => { + const originalWarn = console.warn; + let loggedMessage = ''; + console.warn = (message: string) => { loggedMessage = message; }; + + const symbol = { + symbol: 'UNKNOWN_SYMBOL', + market: 'synthetic_index' + }; + + const result = getDebugDisplayNames(symbol); + + expect(result.symbolDisplayName).to.equal('Unknown Symbol'); + expect(result.marketDisplayName).to.equal('Synthetic Indices'); + expect(loggedMessage).to.include('Missing symbol display name mapping'); + + console.warn = originalWarn; + }); + }); + + describe('batchGetSymbolDisplayNames', () => { + it('should return display names for multiple symbols', () => { + const symbols = [ + { symbol: 'R_10', market: 'synthetic_index' }, + { symbol: 'R_25', market: 'synthetic_index' } + ]; + + const result = batchGetSymbolDisplayNames(symbols); + + expect(result).to.have.length(2); + expect(result[0].symbolDisplayName).to.equal('Volatility 10 Index'); + expect(result[1].symbolDisplayName).to.equal('Volatility 25 Index'); + }); + + it('should handle empty array', () => { + const result = batchGetSymbolDisplayNames([]); + expect(result).to.be.an('array').that.is.empty; + }); + }); + + describe('searchSymbolsByDisplayName', () => { + it('should find symbols by display name match', () => { + const symbols = [ + { symbol: 'R_10' }, + { symbol: 'R_25' }, + { symbol: 'EURUSD' } + ]; + + const results = searchSymbolsByDisplayName(symbols, 'volatility'); + expect(results).to.have.length(2); + expect(results.some(r => r.symbol === 'R_10')).to.be.true; + expect(results.some(r => r.symbol === 'R_25')).to.be.true; + }); + + it('should return all symbols for empty search term', () => { + const symbols = [ + { symbol: 'R_10' }, + { symbol: 'R_25' } + ]; + + const results = searchSymbolsByDisplayName(symbols, ''); + expect(results).to.have.length(2); + }); + + it('should be case insensitive', () => { + const symbols = [{ symbol: 'R_10' }]; + const results = searchSymbolsByDisplayName(symbols, 'VOLATILITY'); + expect(results).to.have.length(1); + }); + }); + + describe('groupSymbolsByMarket', () => { + it('should group symbols by market display name', () => { + const symbols = [ + { symbol: 'R_10', market: 'synthetic_index' }, + { symbol: 'R_25', market: 'synthetic_index' }, + { symbol: 'EURUSD', market: 'forex' } + ]; + + const grouped = groupSymbolsByMarket(symbols); + + expect(grouped).to.be.an('object'); + expect(grouped['Synthetic Indices']).to.have.length(2); + expect(grouped['Forex']).to.have.length(1); + }); + + it('should handle empty array', () => { + const grouped = groupSymbolsByMarket([]); + expect(grouped).to.be.an('object'); + expect(Object.keys(grouped)).to.have.length(0); + }); + }); + + describe('groupSymbolsBySubmarket', () => { + it('should group symbols by submarket display name', () => { + const symbols = [ + { symbol: 'R_10', submarket: 'continuous_indices' }, + { symbol: 'R_25', submarket: 'continuous_indices' }, + { symbol: 'EURUSD', submarket: 'major_pairs' } + ]; + + const grouped = groupSymbolsBySubmarket(symbols); + + expect(grouped).to.be.an('object'); + expect(grouped['Continuous Indices']).to.have.length(2); + expect(grouped['Major Pairs']).to.have.length(1); + }); + + it('should handle symbols without submarket', () => { + const symbols = [ + { symbol: 'R_10', submarket: 'continuous_indices' }, + { symbol: 'TEST' } + ]; + + const grouped = groupSymbolsBySubmarket(symbols); + + expect(grouped['Continuous Indices']).to.have.length(1); + expect(Object.keys(grouped)).to.have.length(1); + }); + }); +}); \ No newline at end of file diff --git a/src/utils/__tests__/index.spec.ts b/src/utils/__tests__/index.spec.ts index a0138bc85e..4ca0aacd67 100644 --- a/src/utils/__tests__/index.spec.ts +++ b/src/utils/__tests__/index.spec.ts @@ -105,28 +105,30 @@ describe('getSymbolMarketCategory', () => { decimal_places: 3, exchange_is_open: true, market: 'synthetic_index', - market_display_name: 'Derived', - name: 'Volatility 10 Index', + name: 'R_10', submarket: 'random_index', - submarket_display_name: 'Continuous Indices', subgroup: 'synthetics', - subgroup_display_name: 'Synthetics', symbol: 'R_10', + displayName: 'Volatility 10 Index', + marketDisplayName: 'Synthetic Indices', + submarketDisplayName: 'Continuous Indices', + subgroupDisplayName: 'Synthetics', }; const symbol_object_without_subgroup = { decimal_places: 5, exchange_is_open: true, market: 'forex', - market_display_name: 'Forex', - name: 'GBP/AUD', + name: 'frxGBPAUD', submarket: 'major_pairs', - submarket_display_name: 'Major Pairs', subgroup: 'none', - subgroup_display_name: 'None', symbol: 'frxGBPAUD', + displayName: 'GBP/AUD', + marketDisplayName: 'Forex', + submarketDisplayName: 'Major Pairs', + subgroupDisplayName: '', }; it('should return concatenated market + subgroup + submarket name when has a subgroup', () => { - expect(getSymbolMarketCategory(symbol_object_with_subgroup)).to.equal('derived-synthetics-continuous_indices'); + expect(getSymbolMarketCategory(symbol_object_with_subgroup)).to.equal('synthetic_index-synthetics-random_index'); }); it('should return concatenated market + submarket name when has no subgroup', () => { expect(getSymbolMarketCategory(symbol_object_without_subgroup)).to.equal('forex-major_pairs'); diff --git a/src/utils/displayNameUtils.ts b/src/utils/displayNameUtils.ts new file mode 100644 index 0000000000..d61a27c266 --- /dev/null +++ b/src/utils/displayNameUtils.ts @@ -0,0 +1,245 @@ +import { displayNameService } from '../services/DisplayNameService'; +import { IDisplayNameOptions } from '../config/displayNameTypes'; + +/** + * Utility functions for getting display names + * These provide convenient access to the DisplayNameService + */ + +/** + * Get display name for a symbol with default options + */ +export const getSymbolDisplayName = (symbolCode: string, options?: IDisplayNameOptions): string => { + return displayNameService.getSymbolDisplayName(symbolCode, options); +}; + +/** + * Get display name for a market with default options + */ +export const getMarketDisplayName = (marketCode: string, options?: IDisplayNameOptions): string => { + return displayNameService.getMarketDisplayName(marketCode, options); +}; + +/** + * Get display name for a submarket with default options + */ +export const getSubmarketDisplayName = (submarketCode: string, options?: IDisplayNameOptions): string => { + return displayNameService.getSubmarketDisplayName(submarketCode, options); +}; + +/** + * Get display name for a subgroup with default options + */ +export const getSubgroupDisplayName = (subgroupCode: string, options?: IDisplayNameOptions): string => { + return displayNameService.getSubgroupDisplayName(subgroupCode, options); +}; + +/** + * Format a raw value into a readable format + */ +export const formatRawValue = (rawValue: string): string => { + return displayNameService.formatRawValue(rawValue); +}; + +/** + * Get display names for a complete symbol object + * Returns an object with all display names for easy destructuring + */ +export const getSymbolDisplayNames = (symbol: { + symbol?: string; + market?: string; + submarket?: string; + subgroup?: string; +}, options?: IDisplayNameOptions) => { + return { + symbolDisplayName: symbol.symbol ? getSymbolDisplayName(symbol.symbol, options) : '', + marketDisplayName: symbol.market ? getMarketDisplayName(symbol.market, options) : '', + submarketDisplayName: symbol.submarket ? getSubmarketDisplayName(symbol.submarket, options) : '', + subgroupDisplayName: symbol.subgroup ? getSubgroupDisplayName(symbol.subgroup, options) : '', + }; +}; + +/** + * Get a formatted category path for a symbol + * Returns: "Market > Submarket > Subgroup" or similar + */ +export const getSymbolCategoryPath = (symbol: { + market?: string; + submarket?: string; + subgroup?: string; +}, options?: IDisplayNameOptions): string => { + const parts: string[] = []; + + if (symbol.market) { + parts.push(getMarketDisplayName(symbol.market, options)); + } + + if (symbol.submarket) { + parts.push(getSubmarketDisplayName(symbol.submarket, options)); + } + + if (symbol.subgroup) { + const subgroupName = getSubgroupDisplayName(symbol.subgroup, options); + if (subgroupName) { // Skip empty subgroups (hidden categories) + parts.push(subgroupName); + } + } + + return parts.join(' > '); +}; + +/** + * Get a short category path for a symbol (market and submarket only) + * Returns: "Market > Submarket" + */ +export const getShortCategoryPath = (symbol: { + market?: string; + submarket?: string; +}, options?: IDisplayNameOptions): string => { + const parts: string[] = []; + + if (symbol.market) { + parts.push(getMarketDisplayName(symbol.market, options)); + } + + if (symbol.submarket) { + parts.push(getSubmarketDisplayName(symbol.submarket, options)); + } + + return parts.join(' > '); +}; + +/** + * Check if a symbol should be hidden based on its categories + */ +export const shouldHideSymbol = (symbol: { + market?: string; + submarket?: string; + subgroup?: string; +}): boolean => { + // A symbol should be hidden if any of its categories result in empty display names + // (which happens for hidden categories) + if (symbol.subgroup) { + const subgroupName = getSubgroupDisplayName(symbol.subgroup); + if (!subgroupName) { + return true; + } + } + + return false; +}; + +/** + * Get all display names with caching enabled for better performance + */ +export const getCachedDisplayNames = (symbol: { + symbol?: string; + market?: string; + submarket?: string; + subgroup?: string; +}) => { + const options: IDisplayNameOptions = { + cacheResults: true, + fallbackToFormatted: true, + logMissing: false, // Don't log in production usage + }; + + return getSymbolDisplayNames(symbol, options); +}; + +/** + * Get display names with logging enabled for debugging + */ +export const getDebugDisplayNames = (symbol: { + symbol?: string; + market?: string; + submarket?: string; + subgroup?: string; +}) => { + const options: IDisplayNameOptions = { + cacheResults: false, + fallbackToFormatted: true, + logMissing: true, + }; + + return getSymbolDisplayNames(symbol, options); +}; + +/** + * Batch process multiple symbols to get their display names efficiently + */ +export const batchGetSymbolDisplayNames = (symbols: Array<{ + symbol?: string; + market?: string; + submarket?: string; + subgroup?: string; +}>, options?: IDisplayNameOptions) => { + return symbols.map(symbol => ({ + ...symbol, + ...getSymbolDisplayNames(symbol, options), + })); +}; + +/** + * Search for symbols by display name (case-insensitive) + */ +export const searchSymbolsByDisplayName = ( + symbols: Array<{ symbol: string; [key: string]: any }>, + searchTerm: string, + options?: IDisplayNameOptions +): Array<{ symbol: string; [key: string]: any }> => { + if (!searchTerm.trim()) { + return symbols; + } + + const lowerSearchTerm = searchTerm.toLowerCase(); + + return symbols.filter(symbol => { + const displayName = getSymbolDisplayName(symbol.symbol, options); + return displayName.toLowerCase().includes(lowerSearchTerm); + }); +}; + +/** + * Group symbols by their market display name + */ +export const groupSymbolsByMarket = ( + symbols: Array<{ market?: string; [key: string]: any }>, + options?: IDisplayNameOptions +): Record> => { + const groups: Record> = {}; + + symbols.forEach(symbol => { + if (symbol.market) { + const marketDisplayName = getMarketDisplayName(symbol.market, options); + if (!groups[marketDisplayName]) { + groups[marketDisplayName] = []; + } + groups[marketDisplayName].push(symbol); + } + }); + + return groups; +}; + +/** + * Group symbols by their submarket display name + */ +export const groupSymbolsBySubmarket = ( + symbols: Array<{ submarket?: string; [key: string]: any }>, + options?: IDisplayNameOptions +): Record> => { + const groups: Record> = {}; + + symbols.forEach(symbol => { + if (symbol.submarket) { + const submarketDisplayName = getSubmarketDisplayName(symbol.submarket, options); + if (!groups[submarketDisplayName]) { + groups[submarketDisplayName] = []; + } + groups[submarketDisplayName].push(symbol); + } + }); + + return groups; +}; \ No newline at end of file diff --git a/src/utils/index.ts b/src/utils/index.ts index 69cf8f5578..12c4ac2f27 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -72,14 +72,14 @@ export const getTimeUnit = (granularity: TGranularity) => { * and 'Forex basket' is a submarket. */ export const getSymbolMarketCategory = (symbol_object: TProcessedSymbolItem) => { - const { market_display_name, submarket_display_name, subgroup } = symbol_object || {}; - if (!market_display_name) return ''; - const market = market_display_name.replace(' ', '_'); - const submarket = submarket_display_name.replace(' ', '_'); + const { market, submarket, subgroup } = symbol_object || {}; + if (!market) return ''; + const marketFormatted = market.replace(' ', '_'); + const submarketFormatted = submarket.replace(' ', '_'); if (subgroup && subgroup !== 'none') { - return `${market}-${subgroup}-${submarket}`.toLowerCase(); + return `${marketFormatted}-${subgroup}-${submarketFormatted}`.toLowerCase(); } - return `${market}-${submarket}`.toLowerCase(); + return `${marketFormatted}-${submarketFormatted}`.toLowerCase(); }; export const getTimeIntervalName = (interval: TGranularity, intervals: typeof Intervals) => { From f276a439e9c39eef081e565886cd6918959ff7fd Mon Sep 17 00:00:00 2001 From: Akmal Djumakhodjaev Date: Thu, 24 Jul 2025 14:55:45 +0800 Subject: [PATCH 2/4] fix: Update symbol processing to accommodate new API property names --- src/binaryapi/ActiveSymbols.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/binaryapi/ActiveSymbols.ts b/src/binaryapi/ActiveSymbols.ts index 0f5e9d700a..ad8c46df73 100644 --- a/src/binaryapi/ActiveSymbols.ts +++ b/src/binaryapi/ActiveSymbols.ts @@ -183,14 +183,17 @@ export default class ActiveSymbols { subgroup: s.subgroup, }); + // Type assertion for new API property names until @deriv/api-types is updated + const symbolData = s as any; + processedSymbols.push({ - symbol: s.symbol, - name: s.symbol, // Keep raw symbol for internal use + symbol: symbolData.underlying_symbol, + name: symbolData.underlying_symbol, // Keep raw symbol for internal use market: s.market, subgroup: s.subgroup, submarket: s.submarket, exchange_is_open: !!s.exchange_is_open, - decimal_places: s.pip.toString().length - 2, + decimal_places: symbolData.pip_size.toString().length - 2, // Add display names for UI displayName: displayNames.symbolDisplayName, marketDisplayName: displayNames.marketDisplayName, From 40f11dce9be2b8d11ddb60c963e9306100d00679 Mon Sep 17 00:00:00 2001 From: Akmal Djumakhodjaev Date: Thu, 24 Jul 2025 15:42:09 +0800 Subject: [PATCH 3/4] fix: Update getDelayedMinutes method to return 0 and adjust related references --- src/binaryapi/TradingTimes.ts | 5 +++-- src/binaryapi/__tests__/TradingTimes.spec.ts | 2 +- src/feed/Feed.ts | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/binaryapi/TradingTimes.ts b/src/binaryapi/TradingTimes.ts index 5eb2ed052c..33dfa1b3ab 100644 --- a/src/binaryapi/TradingTimes.ts +++ b/src/binaryapi/TradingTimes.ts @@ -233,8 +233,9 @@ class TradingTimes { } return this._tradingTimesMap[symbol]?.feed_license === TradingTimes.FEED_UNAVAILABLE; } - getDelayedMinutes(symbol: string): number { - return this._tradingTimesMap?.[symbol].delay_amount as number; + getDelayedMinutes(): number { + // return this._tradingTimesMap?.[symbol].delay_amount as number; + return 0; } isMarketOpened(symbol: string) { if (!this._tradingTimesMap) { diff --git a/src/binaryapi/__tests__/TradingTimes.spec.ts b/src/binaryapi/__tests__/TradingTimes.spec.ts index bdfc409db3..d30fbeebce 100644 --- a/src/binaryapi/__tests__/TradingTimes.spec.ts +++ b/src/binaryapi/__tests__/TradingTimes.spec.ts @@ -35,7 +35,7 @@ describe('TradingTimes test', async function () { }); it('Test getDelayedMinutes', function () { - expect(this.tt.getDelayedMinutes('BSESENSEX30')).to.be.equal(10); + expect(this.tt.getDelayedMinutes('BSESENSEX30')).to.be.equal(0); expect(this.tt.getDelayedMinutes('R_50')).to.be.equal(0); }); diff --git a/src/feed/Feed.ts b/src/feed/Feed.ts index e47143591f..019c7aca25 100644 --- a/src/feed/Feed.ts +++ b/src/feed/Feed.ts @@ -259,7 +259,7 @@ class Feed { getHistoryOnly = true; } else if (validation_error !== 'MarketIsClosed' && validation_error !== 'MarketIsClosedTryVolatility') { let subscription: DelayedSubscription | RealtimeSubscription; - const delay = this._tradingTimes.getDelayedMinutes(symbol); + const delay = this._tradingTimes.getDelayedMinutes(); if (delay > 0) { this._mainStore.notifier.notifyDelayedMarket(symbolName, delay); subscription = new DelayedSubscription( From a7e1f2c34f6e9c47261754ece6ef14e668ab249b Mon Sep 17 00:00:00 2001 From: Akmal Djumakhodjaev Date: Thu, 24 Jul 2025 16:31:22 +0800 Subject: [PATCH 4/4] feat: Add temporary type overrides for ActiveSymbols to accommodate API breaking changes --- src/types/api-overrides.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/types/api-overrides.ts diff --git a/src/types/api-overrides.ts b/src/types/api-overrides.ts new file mode 100644 index 0000000000..5ee300a065 --- /dev/null +++ b/src/types/api-overrides.ts @@ -0,0 +1,19 @@ +/** + * Temporary type overrides for @deriv/api-types to handle API breaking changes + * TODO: Remove this file once @deriv/api-types package is updated with new property names + */ + +import { ActiveSymbols as OriginalActiveSymbols } from '@deriv/api-types'; + +// Override the ActiveSymbols type to include new property names +export type ActiveSymbolsItem = Omit & { + pip_size: number; + underlying_symbol: string; + underlying_symbol_type: string; + // Keep old properties for backward compatibility during transition + pip?: number; + symbol?: string; + symbol_type?: string; +}; + +export type ActiveSymbols = ActiveSymbolsItem[]; \ No newline at end of file