<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Bruz</title>
    <description>The latest articles on DEV Community by Bruz (@bwj2310).</description>
    <link>https://dev.to/bwj2310</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3862891%2F8182d3e9-607c-4897-8abb-b6402210ab03.jpeg</url>
      <title>DEV Community: Bruz</title>
      <link>https://dev.to/bwj2310</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bwj2310"/>
    <language>en</language>
    <item>
      <title>Stock identity normalization across all data and trading platforms</title>
      <dc:creator>Bruz</dc:creator>
      <pubDate>Wed, 29 Apr 2026 20:39:44 +0000</pubDate>
      <link>https://dev.to/bwj2310/stock-identity-normalization-across-all-data-and-trading-platforms-28ff</link>
      <guid>https://dev.to/bwj2310/stock-identity-normalization-across-all-data-and-trading-platforms-28ff</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/bwj2310/tradinggoose-market-canonical-ticker-identity-across-market-data-providers-31g" class="crayons-story__hidden-navigation-link"&gt;TradingGoose-Market: canonical ticker identity across market data providers&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/bwj2310" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3862891%2F8182d3e9-607c-4897-8abb-b6402210ab03.jpeg" alt="bwj2310 profile" class="crayons-avatar__image" width="460" height="460"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/bwj2310" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Bruz
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Bruz
                
              
              &lt;div id="story-author-preview-content-3588516" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/bwj2310" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3862891%2F8182d3e9-607c-4897-8abb-b6402210ab03.jpeg" class="crayons-avatar__image" alt="" width="460" height="460"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Bruz&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/bwj2310/tradinggoose-market-canonical-ticker-identity-across-market-data-providers-31g" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Apr 29&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/bwj2310/tradinggoose-market-canonical-ticker-identity-across-market-data-providers-31g" id="article-link-3588516"&gt;
          TradingGoose-Market: canonical ticker identity across market data providers
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag crayons-tag--filled  " href="/t/showdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;showdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/opensource"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;opensource&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/nextjs"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;nextjs&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/bwj2310/tradinggoose-market-canonical-ticker-identity-across-market-data-providers-31g" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/fire-f60e7a582391810302117f987b22a8ef04a2fe0df7e3258a5f49332df1cec71e.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;3&lt;span class="hidden s:inline"&gt; reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/bwj2310/tradinggoose-market-canonical-ticker-identity-across-market-data-providers-31g#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              &lt;span class="hidden s:inline"&gt;Add Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            11 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>TradingGoose-Market: canonical ticker identity across market data providers</title>
      <dc:creator>Bruz</dc:creator>
      <pubDate>Wed, 29 Apr 2026 20:29:34 +0000</pubDate>
      <link>https://dev.to/bwj2310/tradinggoose-market-canonical-ticker-identity-across-market-data-providers-31g</link>
      <guid>https://dev.to/bwj2310/tradinggoose-market-canonical-ticker-identity-across-market-data-providers-31g</guid>
      <description>&lt;h3&gt;
  
  
  This's a repost, view original article &lt;a href="https://www.tradinggoose.ai/blog/building-tradinggoose-market" rel="noopener noreferrer"&gt;here&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;When we started building TradingGoose, the first thing we needed was market data. Real-time quotes, historical bars, fundamentals, the raw material for any trading application. Simple enough: pick a data provider, call their API, done.&lt;/p&gt;

&lt;p&gt;Except we didn't want to depend on just one provider. We wanted connectors to Alpaca, Yahoo Finance, Alpha Vantage, Finnhub, and more, so users could choose the source that fits their needs, or combine multiple sources for redundancy.&lt;/p&gt;

&lt;p&gt;That's when we ran into the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Ticker Identity Problem
&lt;/h2&gt;

&lt;p&gt;Every data platform has its own naming system for the same asset. Apple stock is &lt;code&gt;AAPL&lt;/code&gt; on Alpaca, &lt;code&gt;AAPL&lt;/code&gt; on Yahoo Finance, easy. But step outside US equities and things fall apart fast.&lt;/p&gt;

&lt;p&gt;A stock listed on the Shanghai Stock Exchange might be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;601988.SS&lt;/code&gt; on Yahoo Finance&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;601988.SHG&lt;/code&gt; on Finnhub&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;601988&lt;/code&gt; on Alpha Vantage, with the exchange passed separately&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Forex is another mess. The EUR/USD pair is &lt;code&gt;EURUSD=X&lt;/code&gt; on Yahoo Finance, but &lt;code&gt;OANDA:EUR_USD&lt;/code&gt; on Finnhub, with a completely different prefix and delimiter. Crypto is worse. Bitcoin against USD could be &lt;code&gt;BTC/USD&lt;/code&gt; on Alpaca, &lt;code&gt;BTC-USD&lt;/code&gt; on Yahoo, &lt;code&gt;BINANCE:BTCUSD&lt;/code&gt; on Finnhub, or &lt;code&gt;BTCUSD&lt;/code&gt; on Alpha Vantage.&lt;/p&gt;

&lt;p&gt;Some platforms use postfixes to indicate asset type: &lt;code&gt;^&lt;/code&gt; for indices, &lt;code&gt;.ETF&lt;/code&gt; suffixes, &lt;code&gt;=X&lt;/code&gt; for forex pairs. Some prepend exchange identifiers like &lt;code&gt;OANDA:EUR_USD&lt;/code&gt;. Some use no delimiter at all.&lt;/p&gt;

&lt;p&gt;When you're building a universal data connector that hooks into multiple sources, you need a single canonical identity for each asset, and a reliable way to translate that identity into whatever format each platform expects. Without it, you're maintaining a tangled mess of per-platform, per-asset-class string manipulation that breaks every time a provider changes their format.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Existing Projects Handle This
&lt;/h2&gt;

&lt;p&gt;We weren't the first to face this. Two major open-source projects tackle ticker identity at scale: CCXT for crypto exchanges, and QuantConnect's LEAN engine for multi-asset algorithmic trading.&lt;/p&gt;

&lt;h3&gt;
  
  
  CCXT: Per-Exchange Currency Dictionaries
&lt;/h3&gt;

&lt;p&gt;CCXT normalizes crypto trading pairs into a unified &lt;code&gt;BASE/QUOTE&lt;/code&gt; format, &lt;code&gt;BTC/USDT&lt;/code&gt; regardless of whether the exchange uses &lt;code&gt;BTCUSDT&lt;/code&gt; (Binance), &lt;code&gt;XXBTUSDT&lt;/code&gt; (Kraken with X/Z prefixes), &lt;code&gt;tBTCUSD&lt;/code&gt; (Bitfinex with t-prefix), or &lt;code&gt;BTC_USDT&lt;/code&gt; (Poloniex with underscores).&lt;/p&gt;

&lt;p&gt;Each exchange class maintains a hardcoded &lt;code&gt;commonCurrencies&lt;/code&gt; dictionary that maps non-standard codes to canonical ones. Kraken alone has 30+ entries mapping things like &lt;code&gt;XXBT→BTC&lt;/code&gt;, &lt;code&gt;XETH→ETH&lt;/code&gt;, &lt;code&gt;ZEUR→EUR&lt;/code&gt;. Bitfinex maps deprecated codes like &lt;code&gt;UST→USDT&lt;/code&gt; and &lt;code&gt;LUNA→LUNC&lt;/code&gt;. Every exchange fork or upgrade requires manual updates to these dictionaries.&lt;/p&gt;

&lt;p&gt;The system works well within the crypto domain, but it has fundamental limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Maintenance is manual and per-exchange.&lt;/strong&gt; There's no systematic way to discover new aliases. Someone has to notice and add them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Market ID conflicts are common.&lt;/strong&gt; Multiple market types like spot, futures, and perpetuals can share the same exchange ID such as &lt;code&gt;BTCUSDT&lt;/code&gt;. CCXT resolves this with a global &lt;code&gt;defaultType&lt;/code&gt; option, which means switching between spot and derivatives requires changing global state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's crypto-only.&lt;/strong&gt; The entire architecture assumes a &lt;code&gt;BASE/QUOTE&lt;/code&gt; pair structure. Traditional equities, forex, indices, and ETFs have fundamentally different identification patterns that don't fit this model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No persistent identity layer.&lt;/strong&gt; Everything is resolved at runtime from exchange API responses. There's no database, no admin interface, no way to curate or override mappings through a UI.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  LEAN: Bit-Packed Security Identifiers
&lt;/h3&gt;

&lt;p&gt;QuantConnect's LEAN takes the opposite approach, engineering a permanent, immutable identifier for every security. Their &lt;code&gt;SecurityIdentifier&lt;/code&gt; packs the security type, market code, strike price, option style, expiry date, and put/call flag into a single 64-bit integer using nested modulo offsets. The &lt;code&gt;Symbol&lt;/code&gt; class wraps this with a human-readable &lt;code&gt;Value&lt;/code&gt;, the current ticker, that can change over time while the underlying ID stays constant.&lt;/p&gt;

&lt;p&gt;For handling ticker changes and delistings, LEAN uses CSV map files, one per security, that record which ticker was valid on which date:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;20150101,CHASE,
20150715,JPM,NYSE
20200101,DELISTED,
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Brokerage integration happens through &lt;code&gt;ISymbolMapper&lt;/code&gt;, an interface each brokerage implements to convert between LEAN symbols and brokerage-specific tickers. A central &lt;code&gt;SymbolPropertiesDatabase&lt;/code&gt; CSV maps &lt;code&gt;market × symbol × securitytype&lt;/code&gt; to properties including an optional &lt;code&gt;MarketTicker&lt;/code&gt; field for brokerage-specific overrides.&lt;/p&gt;

&lt;p&gt;The system is thorough, but the cost is complexity:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;42 hardcoded markets&lt;/strong&gt; defined as integer constants. Adding a new market requires recompilation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~50 hardcoded exchange definitions&lt;/strong&gt; as static fields, each manually mapping name, code, market, and security type.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Map files must be manually maintained&lt;/strong&gt; for every symbol change, delisting, and corporate action.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;SymbolPropertiesDatabase&lt;/code&gt; CSV has thousands of entries&lt;/strong&gt; with optional &lt;code&gt;MarketTicker&lt;/code&gt; fields, many of which are empty, meaning brokerage symbol mapping coverage is incomplete.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No universal identifier integration.&lt;/strong&gt; CUSIP, ISIN, and FIGI are only available through a lazy-loaded resolver, not part of the canonical identity.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both CCXT and LEAN solve real problems, but neither gives you a manageable, self-hostable system where you can curate ticker identities through a UI, define platform-specific mapping rules, and serve canonical data through an API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Off-the-Shelf Services Don't Work Either
&lt;/h2&gt;

&lt;p&gt;We also looked at hosted solutions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OpenFIGI&lt;/strong&gt; is the closest to what we needed, a free, global identifier mapping service maintained by Bloomberg. But it's not open source, you can't self-host it, and its current limits still make it awkward for high-volume applications that need to resolve large symbol sets during startup or bulk imports. Today that means 25 requests per minute for unauthenticated access, or 25 requests per 6 seconds with an API key, with smaller request payload limits on the unauthenticated path. You're also dependent on Bloomberg's infrastructure and data coverage decisions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6vzjun3knp6jempk83pq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6vzjun3knp6jempk83pq.png" alt="OpenFIGI — free and flexible, but not open source and rate-limited for high-volume use" width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FinnWorlds&lt;/strong&gt; offers comprehensive market data APIs including exchange and listing metadata. But after a discounted first month, regular pricing currently lands at $99/month for Individual, $199/month for Starter, $499/month for Developers, and $1,000/month for Enterprise. Those costs add up quickly for personal projects, indie traders, or small teams who mainly need canonical ticker identity. It's also closed-source, so you can't customize the data model or add mapping rules for niche platforms.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F30hinz4sd5ubayywbb7z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F30hinz4sd5ubayywbb7z.png" alt="FinnWorlds pricing — paid tiers after the discounted first month" width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We found that no single open-source project exists that handles canonical ticker identity management with customizable cross-platform mapping rules and provides a single source of truth you can self-host.&lt;/p&gt;

&lt;h2&gt;
  
  
  How We Designed It
&lt;/h2&gt;

&lt;h3&gt;
  
  
  First Attempt: MIC-Based Exchange Mapping
&lt;/h3&gt;

&lt;p&gt;Our initial design centered on the &lt;strong&gt;Market Identifier Code (MIC)&lt;/strong&gt;, the ISO 10383 standard maintained by SWIFT that assigns a unique code to every trading venue worldwide. Under this standard, every exchange has an &lt;strong&gt;operating MIC&lt;/strong&gt;, the legal entity that operates the venue, and one or more &lt;strong&gt;segment MICs&lt;/strong&gt;, the specific trading segments or platforms within that venue. For example, &lt;code&gt;XNYS&lt;/code&gt; is the operating MIC for New York Stock Exchange, Inc. Under it sit segment MICs such as &lt;code&gt;ARCX&lt;/code&gt; for NYSE Arca, &lt;code&gt;XASE&lt;/code&gt; for NYSE American, and others, each representing a distinct venue operated by the same group.&lt;/p&gt;

&lt;p&gt;The idea was straightforward: map each data source's exchange identifier to one or more MICs, then map specific tickers under each MIC. For outbound requests, asking a data source for data, we'd resolve &lt;code&gt;canonical ticker → MIC → data source exchange → platform-specific symbol&lt;/code&gt;. For inbound data, receiving data from a source, we'd reverse it: &lt;code&gt;platform symbol → data source exchange → MIC → canonical ticker&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This gave every asset a canonical identity anchored to a real-world standard. We could reuse the same pattern to store and manage trading hours per MIC, market holidays, and timezone associations.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Simplification: Markets Over MICs
&lt;/h3&gt;

&lt;p&gt;But we quickly realized that many segment MICs under a single operating MIC share the same properties. NYSE's segment MICs largely share the same trading hours, the same geographic location, the same timezone, and the same practical suffix behavior across data platforms. Configuring trading hours, location data, and mapping context for each segment MIC independently was redundant busywork.&lt;/p&gt;

&lt;p&gt;We introduced a &lt;strong&gt;Market&lt;/strong&gt; entity, a higher-level grouping defined by a short code such as &lt;code&gt;NYSE&lt;/code&gt;, &lt;code&gt;NASDAQ&lt;/code&gt;, &lt;code&gt;LSE&lt;/code&gt;, or &lt;code&gt;SHG&lt;/code&gt;. In the actual data model, exchanges still retain their MICs, segment flags, parent relationships, and optional market assignment. But listings, search results, and trading-hours resolution now work primarily against the market-level record instead of forcing every rule to repeat itself for every segment MIC.&lt;/p&gt;

&lt;p&gt;The individual MICs are still stored and queryable. The &lt;code&gt;exchanges&lt;/code&gt; table keeps every MIC with its &lt;code&gt;isSegment&lt;/code&gt; flag and &lt;code&gt;parentId&lt;/code&gt; reference. But the shared metadata, trading-hours defaults, and most client-side symbol logic operate at the market level. Configure once per market instead of repeating the same context for every segment MIC.&lt;/p&gt;

&lt;h3&gt;
  
  
  Moving Mapping Rules to the Client
&lt;/h3&gt;

&lt;p&gt;We originally planned to handle cross-platform symbol mapping inside &lt;a href="https://github.com/TradingGoose/TradingGoose-Market" rel="noopener noreferrer"&gt;TradingGoose-Market&lt;/a&gt; itself. The server would know how to translate &lt;code&gt;601988&lt;/code&gt; into &lt;code&gt;601988.ss&lt;/code&gt; for Yahoo Finance or &lt;code&gt;EUR/USD&lt;/code&gt; into &lt;code&gt;OANDA:EUR_USD&lt;/code&gt; for Finnhub.&lt;/p&gt;

&lt;p&gt;But we realized this was the wrong boundary. The canonical identity service should be concerned with &lt;strong&gt;what exists&lt;/strong&gt;: which listings exist, on which markets, with which attributes. Not with &lt;strong&gt;how each data platform formats its symbols&lt;/strong&gt;. Platform-specific formatting is a concern of the client consuming the data. It changes when you add a new provider, and different clients might connect to different providers.&lt;/p&gt;

&lt;p&gt;So we moved the mapping rules to the client side. In &lt;a href="https://github.com/TradingGoose/TradingGoose-Studio" rel="noopener noreferrer"&gt;TradingGoose-Studio&lt;/a&gt;, each data provider defines a set of &lt;strong&gt;symbol rules&lt;/strong&gt;, declarative objects with conditional scope fields and a template string:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;MarketSymbolRule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;assetClass&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;AssetClass&lt;/span&gt;  &lt;span class="c1"&gt;// 'stock' | 'etf' | 'crypto' | 'currency' | ...&lt;/span&gt;
  &lt;span class="nx"&gt;market&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;          &lt;span class="c1"&gt;// Market code: 'NYSE', 'HKEX', 'SHG', ...&lt;/span&gt;
  &lt;span class="nx"&gt;country&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;         &lt;span class="c1"&gt;// ISO country: 'HK', 'DE', 'CN', ...&lt;/span&gt;
  &lt;span class="nx"&gt;city&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;            &lt;span class="c1"&gt;// City name: 'SHANGHAI', 'SHENZHEN', ...&lt;/span&gt;
  &lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;        &lt;span class="c1"&gt;// Quote currency: 'USD', 'EUR', ...&lt;/span&gt;
  &lt;span class="nx"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;           &lt;span class="c1"&gt;// Optional regex tested against source symbol&lt;/span&gt;
  &lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;         &lt;span class="c1"&gt;// Output template with {variable} placeholders&lt;/span&gt;
  &lt;span class="nx"&gt;active&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;         &lt;span class="c1"&gt;// Toggle rule on/off&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each provider registers an array of these rules plus a &lt;strong&gt;precedence configuration&lt;/strong&gt; that controls how rules are ranked per asset class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Precedence determines which scope fields carry the most weight when scoring&lt;/span&gt;
&lt;span class="nx"&gt;rulePrecedence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;market&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;currency&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;assetClass&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;country&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;city&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;listing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="nx"&gt;stock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;market&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;currency&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;country&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;city&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;listing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;currency&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;market&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;country&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;city&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;listing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;currency&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;market&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;country&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;city&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;listing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice how &lt;code&gt;stock&lt;/code&gt; precedence puts &lt;code&gt;market&lt;/code&gt; first, because for equities the exchange matters most. But &lt;code&gt;crypto&lt;/code&gt; and &lt;code&gt;currency&lt;/code&gt; precedence puts &lt;code&gt;currency&lt;/code&gt; first, because pairs are fundamentally defined by their quote denomination.&lt;/p&gt;

&lt;h4&gt;
  
  
  How Rule Resolution Works
&lt;/h4&gt;

&lt;p&gt;When Studio needs to fetch data for a listing, it runs a four-step pipeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1, build context.&lt;/strong&gt; The listing's metadata is resolved from &lt;a href="https://github.com/TradingGoose/TradingGoose-Market" rel="noopener noreferrer"&gt;TradingGoose-Market&lt;/a&gt; into a &lt;code&gt;ListingContext&lt;/code&gt;: base ticker, quote currency, asset class, market code, country code, and city name. The provider's &lt;code&gt;marketToExchangeCode&lt;/code&gt; map converts the market code into a platform-specific exchange code and suffix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2, filter matching rules.&lt;/strong&gt; Every active rule is tested against the context. Each scope field in a rule acts as a filter. If a rule specifies &lt;code&gt;market: 'HKEX'&lt;/code&gt;, it only matches when the listing's market is HKEX. If a rule specifies &lt;code&gt;city: 'SHANGHAI'&lt;/code&gt; and &lt;code&gt;assetClass: 'stock'&lt;/code&gt;, both must match. A rule with no scope fields matches everything, which makes it the catch-all fallback. If a &lt;code&gt;regex&lt;/code&gt; field is present, it's tested against a source symbol string built from the context. For stocks this is just the base ticker, for crypto it's &lt;code&gt;{base}-{quote}&lt;/code&gt;, for currency it's &lt;code&gt;{base}{quote}&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3, score and rank.&lt;/strong&gt; Each matching rule is scored based on the precedence configuration for the current asset class. The score formula is simple: for each scope field the rule specifies, add &lt;code&gt;precedence.length - index&lt;/code&gt;, where &lt;code&gt;index&lt;/code&gt; is that field's position in the precedence array. Fields earlier in the precedence array carry more weight. Rules with a &lt;code&gt;regex&lt;/code&gt; field get a &lt;code&gt;+0.5&lt;/code&gt; tiebreaker bonus.&lt;/p&gt;

&lt;p&gt;For example, with stock precedence &lt;code&gt;['market', 'currency', 'country', 'city', 'listing']&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rule&lt;/th&gt;
&lt;th&gt;Score calculation&lt;/th&gt;
&lt;th&gt;Total&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{ market: 'NYSE', template: '{base}' }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;market(5)&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{ country: 'HK', template: '{base}.HK' }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;country(3)&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{ market: 'HKEX', country: 'HK', template: '{base}.HK' }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;market(5) + country(3)&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{ template: '{base}' }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;(no fields)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The rule with the highest score wins. This means a rule that specifies both &lt;code&gt;market&lt;/code&gt; and &lt;code&gt;country&lt;/code&gt; will always beat a rule that specifies only &lt;code&gt;market&lt;/code&gt;, which will always beat the catch-all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4, render template.&lt;/strong&gt; The winning rule's template is interpolated with context values. Available variables include &lt;code&gt;{base}&lt;/code&gt;, &lt;code&gt;{quote}&lt;/code&gt;, &lt;code&gt;{exchangeCode}&lt;/code&gt;, &lt;code&gt;{exchangeSuffix}&lt;/code&gt;, &lt;code&gt;{country}&lt;/code&gt;, &lt;code&gt;{city}&lt;/code&gt;, &lt;code&gt;{market}&lt;/code&gt;, &lt;code&gt;{assetClass}&lt;/code&gt;, and &lt;code&gt;{listing}&lt;/code&gt;. If no rules match or the template renders empty, a built-in fallback kicks in: crypto defaults to &lt;code&gt;{base}-{quote}&lt;/code&gt;, currency to &lt;code&gt;{base}{quote}=X&lt;/code&gt;, and everything else to &lt;code&gt;{base}{exchangeSuffix}&lt;/code&gt; or just &lt;code&gt;{base}&lt;/code&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Rules in Practice
&lt;/h4&gt;

&lt;p&gt;Here's what the rules look like across three providers. Same assets, different output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Yahoo Finance&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;city&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SHANGHAI&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{base}.ss&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;                       &lt;span class="c1"&gt;// 601988 → 601988.ss&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;city&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SHENZHEN&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{base}.sz&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;                       &lt;span class="c1"&gt;// 000858 → 000858.sz&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;assetClass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stock&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;market&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HKEX&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{base}.HK&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;// 0700 → 0700.HK&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;assetClass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;etf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;country&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{base}.DE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;      &lt;span class="c1"&gt;// EWG → EWG.DE&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;assetClass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{base}-{quote}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;             &lt;span class="c1"&gt;// BTC → BTC-USD&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;assetClass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;currency&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{base}{quote}=X&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;          &lt;span class="c1"&gt;// EUR → EURUSD=X&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;assetClass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stock&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;market&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;TO&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{base}.TO&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;     &lt;span class="c1"&gt;// RY → RY.TO&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{base}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;                                            &lt;span class="c1"&gt;// AAPL → AAPL (fallback)&lt;/span&gt;

&lt;span class="c1"&gt;// Finnhub&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;assetClass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;currency&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OANDA:{base}_{quote}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;     &lt;span class="c1"&gt;// EUR → OANDA:EUR_USD&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;assetClass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;BINANCE:{base}{quote}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;      &lt;span class="c1"&gt;// BTC → BINANCE:BTCUSD&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;market&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NYSE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{base}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;                           &lt;span class="c1"&gt;// AAPL → AAPL&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;market&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NASDAQ&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{base}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;                         &lt;span class="c1"&gt;// MSFT → MSFT&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{base}{exchangeSuffix}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;                           &lt;span class="c1"&gt;// VOD → VOD.L (fallback)&lt;/span&gt;

&lt;span class="c1"&gt;// Alpaca&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;assetClass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{base}/{quote}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;             &lt;span class="c1"&gt;// BTC → BTC/USD&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;market&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NYSE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{base}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;                           &lt;span class="c1"&gt;// AAPL → AAPL&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;market&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NASDAQ&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{base}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;                         &lt;span class="c1"&gt;// MSFT → MSFT&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{base}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;                                           &lt;span class="c1"&gt;// (fallback)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same listing, say Bitcoin quoted in USD, resolves to &lt;code&gt;BTC-USD&lt;/code&gt; for Yahoo, &lt;code&gt;BINANCE:BTCUSD&lt;/code&gt; for Finnhub, and &lt;code&gt;BTC/USD&lt;/code&gt; for Alpaca. The canonical identity stays the same. Only the final rendered symbol changes per provider.&lt;/p&gt;

&lt;h4&gt;
  
  
  Why Inbound Data Doesn't Need Reverse Mapping
&lt;/h4&gt;

&lt;p&gt;The key insight is that the original canonical context is preserved throughout the request and response cycle. When Studio sends a request for &lt;code&gt;601988.ss&lt;/code&gt; to Yahoo Finance, it already knows this is listing &lt;code&gt;TG_LSTG_XXXX&lt;/code&gt; with base &lt;code&gt;601988&lt;/code&gt; on the Shanghai market. The response data, price bars, volume, and timestamps, are associated back to the canonical listing directly. There's no need to parse &lt;code&gt;601988.ss&lt;/code&gt; and reverse-engineer which listing it belongs to.&lt;/p&gt;

&lt;p&gt;This means adding a new data provider requires zero changes to &lt;a href="https://github.com/TradingGoose/TradingGoose-Market" rel="noopener noreferrer"&gt;TradingGoose-Market&lt;/a&gt;. You define a new set of rules and exchange code mappings in your client, and the canonical identity layer stays untouched.&lt;/p&gt;

&lt;h2&gt;
  
  
  What &lt;a href="https://github.com/TradingGoose/TradingGoose-Market" rel="noopener noreferrer"&gt;TradingGoose-Market&lt;/a&gt; Is Now
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/TradingGoose/TradingGoose-Market" rel="noopener noreferrer"&gt;TradingGoose-Market&lt;/a&gt; is a self-hostable, open market reference data platform that acts as a single source of truth for listing identity, exchange metadata, market groupings, and trading hours. Clients like &lt;a href="https://github.com/TradingGoose/TradingGoose-Studio" rel="noopener noreferrer"&gt;TradingGoose-Studio&lt;/a&gt; resolve canonical records through its API, then apply provider-specific symbol formatting rules at the edge.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It manages listings, exchanges, markets, cryptocurrencies, currencies, countries, cities, time zones, blockchain networks, and trading hours through both an admin dashboard and a versioned public API.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1irai5tdfovufhp1cd8m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1irai5tdfovufhp1cd8m.png" alt="The listings table in [TradingGoose-Market](https://github.com/TradingGoose/TradingGoose-Market) — browsing stocks across US and international markets with icons, asset class, country, quote currency, and market assignments" width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The tech stack is deliberately simple: Next.js on Bun, PostgreSQL with Drizzle ORM, Better Auth for sessions and HMAC-signed API keys, and shadcn/ui for the admin interface. One deployable unit, one database, minimal operational overhead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Beyond Ticker Identity: Trading Hours, Holidays, and Early Closes
&lt;/h3&gt;

&lt;p&gt;Once we had a relational system linking listings to markets, countries, cities, and time zones, we realized it could manage more than just ticker identity. Trading hours are a natural extension. They're tied to the same market entity, and every trading application eventually needs to know when a market is open.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/TradingGoose/TradingGoose-Market" rel="noopener noreferrer"&gt;TradingGoose-Market&lt;/a&gt; stores trading hours as structured data: regular sessions by day of week, holiday closures, and early close dates with their shortened hours. The admin UI provides a visual weekly calendar where you can drag and configure sessions for each market, scoped by country, asset class, or even individual listing when needed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0md745kt4n6tp3gg66xt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0md745kt4n6tp3gg66xt.png" alt="Editing market hours in [TradingGoose-Market](https://github.com/TradingGoose/TradingGoose-Market) — regular sessions shown as a visual weekly calendar, with holidays and early closes managed alongside" width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is another area where the market-level grouping pays off. Instead of maintaining separate trading hour records for every segment MIC under NYSE, you configure it once for the market and let the API resolve the most specific match, whether that means a listing-specific schedule, an asset-class override, or the market default.&lt;/p&gt;

&lt;p&gt;For teams and individuals building trading applications that connect to multiple data sources, &lt;a href="https://github.com/TradingGoose/TradingGoose-Market" rel="noopener noreferrer"&gt;TradingGoose-Market&lt;/a&gt; removes the need to reinvent ticker identity inside every connector. You curate canonical records once in Market, define provider-specific mapping rules in clients like &lt;a href="https://github.com/TradingGoose/TradingGoose-Studio" rel="noopener noreferrer"&gt;TradingGoose-Studio&lt;/a&gt;, and keep the rest of the stack speaking the same language.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>showdev</category>
      <category>opensource</category>
      <category>nextjs</category>
    </item>
  </channel>
</rss>
