Repository: ccxt/ccxt
Pull request: Coinbase: fetchTicker, fetchTickers
Added fetchTicker and fetchTickers advanced trade support:
fixes: #16476

### fetchTicker:
```
coinbase.fetchTicker (BTC/USDT)
2023-01-14T03:42:27.405Z iteration 0 passed in 530 ms

{
  symbol: 'BTC/USDT',
  timestamp: 1673667747404,
  datetime: '2023-01-14T03:42:27.404Z',
  bid: undefined,
  ask: undefined,
  last: 20915.28,
  high: undefined,
  low: undefined,
  bidVolume: undefined,
  askVolume: undefined,
  vwap: undefined,
  open: undefined,
  close: 20915.28,
  previousClose: undefined,
  change: undefined,
  percentage: 10.98218548737918,
  average: undefined,
  baseVolume: undefined,
  quoteVolume: undefined,
  info: {
    trade_id: '10238729',
    product_id: 'BTC-USDT',
    price: '20915.28',
    size: '0.004',
    time: '2023-01-14T03:42:21.034078Z',
    side: 'SELL',
    bid: '',
    ask: ''
  }
}
```

### fetchTickers:
```
coinbase.fetchTickers (BTC/USDT,LTC/BTC)
2023-01-14T03:43:06.843Z iteration 0 passed in 1060 ms

{
  'BTC/USDT': {
    symbol: 'BTC/USDT',
    timestamp: 1673667786311,
    datetime: '2023-01-14T03:43:06.311Z',
    bid: undefined,
    ask: undefined,
    last: 20916,
    high: undefined,
    low: undefined,
    bidVolume: undefined,
    askVolume: undefined,
    vwap: undefined,
    open: undefined,
    close: 20916,
    previousClose: undefined,
    change: undefined,
    percentage: 10.98218548737918,
    average: undefined,
    baseVolume: undefined,
    quoteVolume: undefined,
    info: {
      trade_id: '10238735',
      product_id: 'BTC-USDT',
      price: '20916',
      size: '0.004',
      time: '2023-01-14T03:42:48.073409Z',
      side: 'BUY',
      bid: '',
      ask: ''
    }
  },
  'LTC/BTC': {
    symbol: 'LTC/BTC',
    timestamp: 1673667786843,
    datetime: '2023-01-14T03:43:06.843Z',
    bid: undefined,
    ask: undefined,
    last: 0.004225,
    high: undefined,
    low: undefined,
    bidVolume: undefined,
    askVolume: undefined,
    vwap: undefined,
    open: undefined,
    close: 0.004225,
    previousClose: undefined,
    change: undefined,
    percentage: -6.48517042939354,
    average: undefined,
    baseVolume: undefined,
    quoteVolume: undefined,
    info: {
      trade_id: '13818258',
      product_id: 'LTC-BTC',
      price: '0.004225',
      size: '0.23318175',
      time: '2023-01-14T03:40:19.139935Z',
      side: 'BUY',
      bid: '',
      ask: ''
    }
  }
}
```

Problem statement:
Coinbase new version load markets and fetch tickers show different trading pairs.
### Operating System

Windows 10

### Programming Languages

JS

### CCXT Version

2.6.7

### Description

Hello:
As Coinbase will phrase out old API and migrate to Coinbase advanced trade with new API, I tested latest version of CCXT (version 2.6.7), I can see the load markets function return some new trading pairs, like: BTC/USDT, but using fetch tickers function, I can not find such trading pair: BTC/USDT.
The following is the output of my code:
D:\nodejs\CCXT_Tickers>node coinbase_fetch1Ticker.js BTC/USDT
ExchangeError: coinbase {"errors":[{"id":"not_found","message":"Invalid currency"}]}
    at coinbase.throwExactlyMatchedException (D:\nodejs\CCXT_Tickers\node_modules\ccxt\js\base\Exchange.js:2400:19)
    at coinbase.handleErrors (D:\nodejs\CCXT_Tickers\node_modules\ccxt\js\coinbase.js:1723:30)
    at D:\nodejs\CCXT_Tickers\node_modules\ccxt\js\base\Exchange.js:603:51
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async timeout (D:\nodejs\CCXT_Tickers\node_modules\ccxt\js\base\functions\time.js:181:20)
    at async coinbase.fetch2 (D:\nodejs\CCXT_Tickers\node_modules\ccxt\js\base\Exchange.js:2023:16)
    at async coinbase.request (D:\nodejs\CCXT_Tickers\node_modules\ccxt\js\base\Exchange.js:2027:16)
    at async coinbase.fetchTicker (D:\nodejs\CCXT_Tickers\node_modules\ccxt\js\coinbase.js:1081:22)
    at async fetch1Ticker (D:\nodejs\CCXT_Tickers\coinbase_fetch1Ticker.js:29:17) {
  constructor: [class ExchangeError extends BaseError]
}

From the output of loadMarkets function, I can see BTC/USDT trading pair. The following is my code for loadMarkets:

const ccxt = require('ccxt');

let exchange = new ccxt.coinbase
({
apiKey: 'My_API_Key',
secret: 'My_Secret_Key',
'options': {
'adjustForTimeDifference': true,
'fetchCurrencies': true,
'verbose': false, }
});

async function loadMarkets()
{
try
{
const exchange_markets = await exchange.loadMarkets();
if (typeof exchange_markets !== 'undefined')
{
const json_markets = JSON.stringify(exchange_markets);
console.log(json_markets);
}
else
{
const obj_market0 = ({ 'Data': [] });
const json_markets0 = JSON.stringify(obj_market0);
console.log(json_markets0);
}
}
catch (err)
{
console.error(err)
}
};
loadMarkets();


### Code

```
const ccxt = require('ccxt');

if (process.argv.length != 3)
{
console.error('Usage: BTC/USDT');
console.error('Fetch 1 ticker price');
console.error('Please try again!');
process.exit(1);
}
const [nodejs, script1, pair1] = process.argv;

let exchange = new ccxt.coinbase
({
apiKey: 'My_API_Key',
secret: 'My_Secret_Key',
'options': {
'adjustForTimeDifference': true,
'fetchCurrencies': true,
'verbose': false, }
});

const fetch1Ticker = async(pair1) =>
{
try
{
if (exchange.has['fetchTicker'])
{
await exchange.loadMarkets();
const ticker1 = await exchange.fetchTicker(pair1);
if (typeof ticker1 !== 'undefined')
{
const json_ticker1 = JSON.stringify(ticker1);
console.log(json_ticker1);
}
else
{
const obj_jsonTicker0 = ({ 'Data': []});
const json_ticker0 = JSON.stringify(obj_jsonTicker0);
console.log(json_ticker0);
}
}
}
catch (err)
{
console.error(err)
}
};
fetch1Ticker(pair1);
```

Hints from issue discussion:
Hello @zydjohnHotmail, we're gradually upgrading to the new API and will try to do it as fast as possible. 
cc/ @Dan-krm 
Thanks for your quick reply.
Please let me know when the updating is done.

Commit message: Coinbase: fetchTicker, fetchTickers

Added fetchTicker and fetchTickers advanced trade support:
### fetchTicker:
```
coinbase.fetchTicker (BTC/USDT)
2023-01-14T03:42:27.405Z iteration 0 passed in 530 ms

{
  symbol: 'BTC/USDT',
  timestamp: 1673667747404,
  datetime: '2023-01-14T03:42:27.404Z',
  bid: undefined,
  ask: undefined,
  last: 20915.28,
  high: undefined,
  low: undefined,
  bidVolume: undefined,
  askVolume: undefined,
  vwap: undefined,
  open: undefined,
  close: 20915.28,
  previousClose: undefined,
  change: undefined,
  percentage: 10.98218548737918,
  average: undefined,
  baseVolume: undefined,
  quoteVolume: undefined,
  info: {
    trade_id: '10238729',
    product_id: 'BTC-USDT',
    price: '20915.28',
    size: '0.004',
    time: '2023-01-14T03:42:21.034078Z',
    side: 'SELL',
    bid: '',
    ask: ''
  }
}
```

### fetchTickers:
```
coinbase.fetchTickers (BTC/USDT,LTC/BTC)
2023-01-14T03:43:06.843Z iteration 0 passed in 1060 ms

{
  'BTC/USDT': {
    symbol: 'BTC/USDT',
    timestamp: 1673667786311,
    datetime: '2023-01-14T03:43:06.311Z',
    bid: undefined,
    ask: undefined,
    last: 20916,
    high: undefined,
    low: undefined,
    bidVolume: undefined,
    askVolume: undefined,
    vwap: undefined,
    open: undefined,
    close: 20916,
    previousClose: undefined,
    change: undefined,
    percentage: 10.98218548737918,
    average: undefined,
    baseVolume: undefined,
    quoteVolume: undefined,
    info: {
      trade_id: '10238735',
      product_id: 'BTC-USDT',
      price: '20916',
      size: '0.004',
      time: '2023-01-14T03:42:48.073409Z',
      side: 'BUY',
      bid: '',
      ask: ''
    }
  },
  'LTC/BTC': {
    symbol: 'LTC/BTC',
    timestamp: 1673667786843,
    datetime: '2023-01-14T03:43:06.843Z',
    bid: undefined,
    ask: undefined,
    last: 0.004225,
    high: undefined,
    low: undefined,
    bidVolume: undefined,
    askVolume: undefined,
    vwap: undefined,
    open: undefined,
    close: 0.004225,
    previousClose: undefined,
    change: undefined,
    percentage: -6.48517042939354,
    average: undefined,
    baseVolume: undefined,
    quoteVolume: undefined,
    info: {
      trade_id: '13818258',
      product_id: 'LTC-BTC',
      price: '0.004225',
      size: '0.23318175',
      time: '2023-01-14T03:40:19.139935Z',
      side: 'BUY',
      bid: '',
      ask: ''
    }
  }
}
```

Patch to review:
diff --git a/js/coinbase.js b/js/coinbase.js
index 04665f49fc745..03d9215c00be1 100644
--- a/js/coinbase.js
+++ b/js/coinbase.js
@@ -75,7 +75,7 @@ module.exports = class coinbase extends Exchange {
                 'fetchPositionsRisk': false,
                 'fetchPremiumIndexOHLCV': false,
                 'fetchTicker': true,
-                'fetchTickers': true,
+                'fetchTickers': 'emulated',
                 'fetchTime': true,
                 'fetchTrades': undefined,
                 'fetchTradingFee': false,
@@ -263,6 +263,7 @@ module.exports = class coinbase extends Exchange {
                 ],
                 'advanced': true, // set to true if using any v3 endpoints from the advanced trade API
                 'fetchMarkets': 'fetchMarketsV3', // 'fetchMarketsV3' or 'fetchMarketsV2'
+                'fetchTicker': 'fetchTickerV3', // 'fetchTickerV3' or 'fetchTickerV2'
             },
         });
     }
@@ -728,8 +729,7 @@ module.exports = class coinbase extends Exchange {
          * @param {object} params extra parameters specific to the exchange api endpoint
          * @returns {[object]} an array of objects representing market data
          */
-        const options = this.safeValue (this.options, 'fetchMarkets');
-        const method = (this.safeString (options, 'method', 'fetch_markets_v3'));
+        const method = this.safeString (this.options, 'fetchMarkets', 'fetchMarketsV3');
         return await this[method] (params);
     }
 
@@ -1032,34 +1032,24 @@ module.exports = class coinbase extends Exchange {
          */
         await this.loadMarkets ();
         symbols = this.marketSymbols (symbols);
-        const request = {
-            // 'currency': 'USD',
-        };
-        const response = await this.v2PublicGetExchangeRates (this.extend (request, params));
-        //
-        //     {
-        //         "data":{
-        //             "currency":"USD",
-        //             "rates":{
-        //                 "AED":"3.6731",
-        //                 "AFN":"103.163942",
-        //                 "ALL":"106.973038",
-        //             }
-        //         }
-        //     }
-        //
-        const data = this.safeValue (response, 'data', {});
-        const rates = this.safeValue (data, 'rates', {});
-        const quoteId = this.safeString (data, 'currency');
+        let symbol = undefined;
         const result = {};
-        const baseIds = Object.keys (rates);
-        const delimiter = '-';
-        for (let i = 0; i < baseIds.length; i++) {
-            const baseId = baseIds[i];
-            const marketId = baseId + delimiter + quoteId;
-            const market = this.safeMarket (marketId, undefined, delimiter);
-            const symbol = market['symbol'];
-            result[symbol] = this.parseTicker (rates[baseId], market);
+        if (symbols !== undefined) {
+            for (let i = 0; i < symbols.length; i++) {
+                symbol = symbols[i];
+                result[symbol] = await this.fetchTicker (symbol);
+            }
+        } else {
+            const fetchTickerMethod = this.safeString (this.options, 'fetchTicker');
+            if (fetchTickerMethod !== 'fetchTickerV3') {
+                this.checkRequiredArgument ('fetchTickers', symbols, 'symbols');
+            }
+            const markets = await this.fetchMarkets ();
+            for (let i = 0; i < markets.length; i++) {
+                const entry = markets[i];
+                symbol = this.safeString (entry, 'symbol');
+                result[symbol] = await this.fetchTicker (symbol);
+            }
         }
         return this.filterByArray (result, 'symbol', symbols);
     }
@@ -1073,6 +1063,11 @@ module.exports = class coinbase extends Exchange {
          * @param {object} params extra parameters specific to the coinbase api endpoint
          * @returns {object} a [ticker structure]{@link https://docs.ccxt.com/en/latest/manual.html#ticker-structure}
          */
+        const method = this.safeString (this.options, 'fetchTicker', 'fetchTickerV3');
+        return await this[method] (symbol, params);
+    }
+
+    async fetchTickerV2 (symbol, params = {}) {
         await this.loadMarkets ();
         const market = this.market (symbol);
         const request = this.extend ({
@@ -1093,9 +1088,37 @@ module.exports = class coinbase extends Exchange {
         return this.parseTicker ([ spot, buy, sell ], market);
     }
 
-    parseTicker (ticker, market = undefined) {
+    async fetchTickerV3 (symbol, params = {}) {
+        await this.loadMarkets ();
+        const market = this.market (symbol);
+        const request = {
+            'product_id': market['id'],
+            'limit': 1,
+        };
+        const response = await this.v3PrivateGetBrokerageProductsProductIdTicker (this.extend (request, params));
         //
-        // fetchTicker
+        //     {
+        //         "trades": [
+        //             {
+        //                 "trade_id": "10209805",
+        //                 "product_id": "BTC-USDT",
+        //                 "price": "19381.27",
+        //                 "size": "0.1",
+        //                 "time": "2023-01-13T20:35:41.865970Z",
+        //                 "side": "BUY",
+        //                 "bid": "",
+        //                 "ask": ""
+        //             }
+        //         ]
+        //     }
+        //
+        const data = this.safeValue (response, 'trades', []);
+        return this.parseTicker (data[0], market);
+    }
+
+    async parseTicker (ticker, market = undefined) {
+        //
+        // fetchTickerV2
         //
         //     [
         //         "48691.23", // spot
@@ -1103,23 +1126,41 @@ module.exports = class coinbase extends Exchange {
         //         "48691.23",  // sell
         //     ]
         //
-        // fetchTickers
+        // fetchTickerV3
         //
-        //     "48691.23"
+        //     {
+        //         "trade_id": "10209805",
+        //         "product_id": "BTC-USDT",
+        //         "price": "19381.27",
+        //         "size": "0.1",
+        //         "time": "2023-01-13T20:35:41.865970Z",
+        //         "side": "BUY",
+        //         "bid": "",
+        //         "ask": ""
+        //     }
         //
+        const fetchTickerMethod = this.safeString (this.options, 'fetchTicker');
         const symbol = this.safeSymbol (undefined, market);
         let ask = undefined;
         let bid = undefined;
         let last = undefined;
+        let percentage = undefined;
         const timestamp = this.milliseconds ();
-        if (typeof ticker !== 'string') {
+        if (fetchTickerMethod === 'fetchTickerV2') {
             const [ spot, sell, buy ] = ticker;
             const spotData = this.safeValue (spot, 'data', {});
             const buyData = this.safeValue (buy, 'data', {});
             const sellData = this.safeValue (sell, 'data', {});
-            last = this.safeString (spotData, 'amount');
-            bid = this.safeString (buyData, 'amount');
-            ask = this.safeString (sellData, 'amount');
+            last = this.safeNumber (spotData, 'amount');
+            bid = this.safeNumber (buyData, 'amount');
+            ask = this.safeNumber (sellData, 'amount');
+        } else {
+            last = this.safeNumber (ticker, 'price');
+            bid = this.safeNumber (ticker, 'bid');
+            ask = this.safeNumber (ticker, 'ask');
+        }
+        if (market['info']['price_percentage_change_24h'] !== undefined) {
+            percentage = market['info']['price_percentage_change_24h'];
         }
         return this.safeTicker ({
             'symbol': symbol,
@@ -1137,7 +1178,7 @@ module.exports = class coinbase extends Exchange {
             'close': last,
             'previousClose': undefined,
             'change': undefined,
-            'percentage': undefined,
+            'percentage': percentage,
             'average': undefined,
             'baseVolume': undefined,
             'quoteVolume': undefined,


Code review comments:
diff_hunk: @@ -1093,33 +1088,79 @@ module.exports = class coinbase extends Exchange {
         return this.parseTicker ([ spot, buy, sell ], market);
     }
 
-    parseTicker (ticker, market = undefined) {
+    async fetchTickerV3 (symbol, params = {}) {
+        await this.loadMarkets ();
+        const market = this.market (symbol);
+        const request = {
+            'product_id': market['id'],
+            'limit': 1,
+        };
+        const response = await this.v3PrivateGetBrokerageProductsProductIdTicker (this.extend (request, params));
         //
-        // fetchTicker
+        //     {
+        //         "trades": [
+        //             {
+        //                 "trade_id": "10209805",
+        //                 "product_id": "BTC-USDT",
+        //                 "price": "19381.27",
+        //                 "size": "0.1",
+        //                 "time": "2023-01-13T20:35:41.865970Z",
+        //                 "side": "BUY",
+        //                 "bid": "",
+        //                 "ask": ""
+        //             }
+        //         ]
+        //     }
+        //
+        const data = this.safeValue (response, 'trades', []);
+        return this.parseTicker (data[0], market);
+    }
+
+    async parseTicker (ticker, market = undefined) {
+        //
+        // fetchTickerV2
         //
         //     [
         //         "48691.23", // spot
         //         "48691.23", // buy
         //         "48691.23",  // sell
         //     ]
         //
-        // fetchTickers
+        // fetchTickerV3
         //
-        //     "48691.23"
+        //     {
+        //         "trade_id": "10209805",
+        //         "product_id": "BTC-USDT",
+        //         "price": "19381.27",
+        //         "size": "0.1",
+        //         "time": "2023-01-13T20:35:41.865970Z",
+        //         "side": "BUY",
+        //         "bid": "",
+        //         "ask": ""
+        //     }
         //
+        const fetchTickerMethod = this.safeString (this.options, 'fetchTicker');
         const symbol = this.safeSymbol (undefined, market);
         let ask = undefined;
         let bid = undefined;
         let last = undefined;
+        let percentage = undefined;
         const timestamp = this.milliseconds ();
-        if (typeof ticker !== 'string') {
+        if (fetchTickerMethod === 'fetchTickerV2') {
path: js/coinbase.js
text: @user2:
@user1-krm we should avoid relying on the options here, why not use `safeNumber2` instead? 

@author:
☑️

diff_hunk: @@ -1093,33 +1088,79 @@ module.exports = class coinbase extends Exchange {
         return this.parseTicker ([ spot, buy, sell ], market);
     }
 
-    parseTicker (ticker, market = undefined) {
+    async fetchTickerV3 (symbol, params = {}) {
+        await this.loadMarkets ();
+        const market = this.market (symbol);
+        const request = {
+            'product_id': market['id'],
+            'limit': 1,
+        };
+        const response = await this.v3PrivateGetBrokerageProductsProductIdTicker (this.extend (request, params));
         //
-        // fetchTicker
+        //     {
+        //         "trades": [
+        //             {
+        //                 "trade_id": "10209805",
+        //                 "product_id": "BTC-USDT",
+        //                 "price": "19381.27",
+        //                 "size": "0.1",
+        //                 "time": "2023-01-13T20:35:41.865970Z",
+        //                 "side": "BUY",
+        //                 "bid": "",
+        //                 "ask": ""
+        //             }
+        //         ]
+        //     }
+        //
+        const data = this.safeValue (response, 'trades', []);
+        return this.parseTicker (data[0], market);
+    }
+
+    async parseTicker (ticker, market = undefined) {
+        //
+        // fetchTickerV2
         //
         //     [
         //         "48691.23", // spot
         //         "48691.23", // buy
         //         "48691.23",  // sell
         //     ]
         //
-        // fetchTickers
+        // fetchTickerV3
         //
-        //     "48691.23"
+        //     {
+        //         "trade_id": "10209805",
+        //         "product_id": "BTC-USDT",
+        //         "price": "19381.27",
+        //         "size": "0.1",
+        //         "time": "2023-01-13T20:35:41.865970Z",
+        //         "side": "BUY",
+        //         "bid": "",
+        //         "ask": ""
+        //     }
         //
+        const fetchTickerMethod = this.safeString (this.options, 'fetchTicker');
         const symbol = this.safeSymbol (undefined, market);
         let ask = undefined;
         let bid = undefined;
         let last = undefined;
+        let percentage = undefined;
         const timestamp = this.milliseconds ();
-        if (typeof ticker !== 'string') {
+        if (fetchTickerMethod === 'fetchTickerV2') {
             const [ spot, sell, buy ] = ticker;
             const spotData = this.safeValue (spot, 'data', {});
             const buyData = this.safeValue (buy, 'data', {});
             const sellData = this.safeValue (sell, 'data', {});
-            last = this.safeString (spotData, 'amount');
-            bid = this.safeString (buyData, 'amount');
-            ask = this.safeString (sellData, 'amount');
+            last = this.safeNumber (spotData, 'amount');
+            bid = this.safeNumber (buyData, 'amount');
+            ask = this.safeNumber (sellData, 'amount');
+        } else {
+            last = this.safeNumber (ticker, 'price');
+            bid = this.safeNumber (ticker, 'bid');
+            ask = this.safeNumber (ticker, 'ask');
+        }
+        if (market['info']['price_percentage_change_24h'] !== undefined) {
path: js/coinbase.js
text: @user2:
@user1-krm also we can't do this the user might have long-running instances, so the markets info could be from a few days ago

@author:
Interesting point, I'll remove this