diff --git a/.changeset/violet-zoos-walk.md b/.changeset/violet-zoos-walk.md new file mode 100644 index 0000000000..52506aed49 --- /dev/null +++ b/.changeset/violet-zoos-walk.md @@ -0,0 +1,6 @@ +--- +'@chainlink/dxfeed-adapter': minor +'@chainlink/finage-adapter': minor +--- + +Add mid to stock quotes diff --git a/packages/sources/dxfeed/src/endpoint/stock-quotes.ts b/packages/sources/dxfeed/src/endpoint/stock-quotes.ts index 92d2753a5d..b3428c55d5 100644 --- a/packages/sources/dxfeed/src/endpoint/stock-quotes.ts +++ b/packages/sources/dxfeed/src/endpoint/stock-quotes.ts @@ -14,6 +14,7 @@ export type BaseEndpointTypes = { Response: { Result: null Data: { + mid_price: number bid_price: number bid_volume: number ask_price: number diff --git a/packages/sources/dxfeed/src/transport/stock-quotes.ts b/packages/sources/dxfeed/src/transport/stock-quotes.ts index 243f2c1828..84ab340e44 100644 --- a/packages/sources/dxfeed/src/transport/stock-quotes.ts +++ b/packages/sources/dxfeed/src/transport/stock-quotes.ts @@ -1,11 +1,17 @@ +import { makeLogger } from '@chainlink/external-adapter-framework/util' import { BaseEndpointTypes } from '../endpoint/stock-quotes' import { buildWsTransport } from './ws' +const logger = makeLogger('StockQuotesTransport') + const eventSymbolIndex = 0 +const bidTimeIndex = 4 const bidPriceIndex = 6 const bidSizeIndex = 7 +const askTimeIndex = 8 const askPriceIndex = 10 const askSizeIndex = 11 +const dataLength = 12 export const transport = buildWsTransport( (params) => ({ Quote: [params.base.toUpperCase()] }), @@ -16,17 +22,44 @@ export const transport = buildWsTransport( const data = message[0].data[1] + if (data.length != dataLength) { + logger.warn(`${JSON.stringify(data)} is invalid since it doesn't have ${dataLength} fields.`) + return [] + } + + const bidPrice = Number(data[bidPriceIndex]) + const askPrice = Number(data[askPriceIndex]) + + let midPrice: number + + if (bidPrice == 0) { + midPrice = askPrice + } else if (askPrice == 0) { + midPrice = bidPrice + } else { + midPrice = + (bidPrice * Number(data[bidSizeIndex]) + askPrice * Number(data[askSizeIndex])) / + (Number(data[bidSizeIndex]) + Number(data[askSizeIndex])) + } + return [ { params: { base: data[eventSymbolIndex] }, response: { result: null, data: { - bid_price: Number(data[bidPriceIndex]), + mid_price: midPrice, + bid_price: bidPrice, bid_volume: Number(data[bidSizeIndex]), - ask_price: Number(data[askPriceIndex]), + ask_price: askPrice, ask_volume: Number(data[askSizeIndex]), }, + timestamps: { + providerIndicatedTimeUnixMs: Math.max( + Number(data[bidTimeIndex]), + Number(data[askTimeIndex]), + ), + }, }, }, ] diff --git a/packages/sources/dxfeed/test/integration/__snapshots__/adapter-ws.test.ts.snap b/packages/sources/dxfeed/test/integration/__snapshots__/adapter-ws.test.ts.snap index a452ee2358..78eb33faae 100644 --- a/packages/sources/dxfeed/test/integration/__snapshots__/adapter-ws.test.ts.snap +++ b/packages/sources/dxfeed/test/integration/__snapshots__/adapter-ws.test.ts.snap @@ -1,5 +1,54 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`websocket quote endpoint error when data length is not valid 1`] = ` +{ + "error": { + "message": "The EA has not received any values from the Data Provider for the requested data yet. Retry after a short delay, and if the problem persists raise this issue in the relevant channels.", + "name": "AdapterError", + }, + "status": "errored", + "statusCode": 504, +} +`; + +exports[`websocket quote endpoint should return ask when bid is 0 1`] = ` +{ + "data": { + "ask_price": 172, + "ask_volume": 100, + "bid_price": 0, + "bid_volume": 148, + "mid_price": 172, + }, + "result": null, + "statusCode": 200, + "timestamps": { + "providerDataReceivedUnixMs": 1018, + "providerDataStreamEstablishedUnixMs": 1010, + "providerIndicatedTimeUnixMs": 1670868378000, + }, +} +`; + +exports[`websocket quote endpoint should return bid when ask is 0 1`] = ` +{ + "data": { + "ask_price": 0, + "ask_volume": 100, + "bid_price": 170, + "bid_volume": 148, + "mid_price": 170, + }, + "result": null, + "statusCode": 200, + "timestamps": { + "providerDataReceivedUnixMs": 1018, + "providerDataStreamEstablishedUnixMs": 1010, + "providerIndicatedTimeUnixMs": 1670868378000, + }, +} +`; + exports[`websocket quote endpoint should return success 1`] = ` { "data": { @@ -7,12 +56,14 @@ exports[`websocket quote endpoint should return success 1`] = ` "ask_volume": 100, "bid_price": 170, "bid_volume": 148, + "mid_price": 170.80645161290323, }, "result": null, "statusCode": 200, "timestamps": { "providerDataReceivedUnixMs": 1018, "providerDataStreamEstablishedUnixMs": 1010, + "providerIndicatedTimeUnixMs": 1670868378000, }, } `; diff --git a/packages/sources/dxfeed/test/integration/adapter-ws.test.ts b/packages/sources/dxfeed/test/integration/adapter-ws.test.ts index 6b0cffa7ea..dde7044d4e 100644 --- a/packages/sources/dxfeed/test/integration/adapter-ws.test.ts +++ b/packages/sources/dxfeed/test/integration/adapter-ws.test.ts @@ -45,7 +45,7 @@ describe('websocket', () => { // Send initial request to start background execute and wait for cache to be filled with result await testAdapter.request(quoteData) await testAdapter.request(stockData) - await testAdapter.waitForCache(2) + await testAdapter.waitForCache(4) }) afterAll(async () => { @@ -67,5 +67,29 @@ describe('websocket', () => { const response = await testAdapter.request(quoteData) expect(response.json()).toMatchSnapshot() }) + + it('should return bid when ask is 0', async () => { + const response = await testAdapter.request({ + base: 'NO_ASK', + endpoint: 'stock_quotes', + }) + expect(response.json()).toMatchSnapshot() + }) + + it('should return ask when bid is 0', async () => { + const response = await testAdapter.request({ + base: 'NO_BID', + endpoint: 'stock_quotes', + }) + expect(response.json()).toMatchSnapshot() + }) + + it('error when data length is not valid', async () => { + const response = await testAdapter.request({ + base: 'INVALID_DATA', + endpoint: 'stock_quotes', + }) + expect(response.json()).toMatchSnapshot() + }) }) }) diff --git a/packages/sources/dxfeed/test/integration/fixtures.ts b/packages/sources/dxfeed/test/integration/fixtures.ts index 5831201f7f..02d271fb3c 100644 --- a/packages/sources/dxfeed/test/integration/fixtures.ts +++ b/packages/sources/dxfeed/test/integration/fixtures.ts @@ -62,6 +62,34 @@ export const mockWebSocketServer = (URL: string): MockWebsocketServer => { channel: '/service/data', }, ] + + const noBidQuoteReponse = [ + { + data: [ + 'Quote', + ['NO_BID', 0, 0, 0, 1670868378000, 'V', 0, 148.0, 1670868370000, 'V', 172.0, 100.0], + ], + channel: '/service/data', + }, + ] + const noAskQuoteReponse = [ + { + data: [ + 'Quote', + ['NO_ASK', 0, 0, 0, 1670868378000, 'V', 170.0, 148.0, 1670868370000, 'V', 0, 100.0], + ], + channel: '/service/data', + }, + ] + const invalidQuoteReponse = [ + { + data: [ + 'Quote', + ['INVALID_DATA', 0, 0, 0, 1670868378000, 'V', 170.0, 148.0, 1670868370000, 'V', 0], + ], + channel: '/service/data', + }, + ] const tradeResponse = [ { data: [ @@ -97,6 +125,9 @@ export const mockWebSocketServer = (URL: string): MockWebsocketServer => { ) socket.on('message', () => { socket.send(JSON.stringify(quoteReponse)) + socket.send(JSON.stringify(noBidQuoteReponse)) + socket.send(JSON.stringify(noAskQuoteReponse)) + socket.send(JSON.stringify(invalidQuoteReponse)) socket.send(JSON.stringify(tradeResponse)) }) }) diff --git a/packages/sources/finage/src/endpoint/stock-quotes.ts b/packages/sources/finage/src/endpoint/stock-quotes.ts index 406490a18b..762628ee46 100644 --- a/packages/sources/finage/src/endpoint/stock-quotes.ts +++ b/packages/sources/finage/src/endpoint/stock-quotes.ts @@ -11,6 +11,7 @@ export type BaseEndpointTypes = { Response: { Result: null Data: { + mid_price: number bid_price: number bid_volume: number ask_price: number diff --git a/packages/sources/finage/src/transport/stock-quotes.ts b/packages/sources/finage/src/transport/stock-quotes.ts index 621927b120..2c40b6abe4 100644 --- a/packages/sources/finage/src/transport/stock-quotes.ts +++ b/packages/sources/finage/src/transport/stock-quotes.ts @@ -48,16 +48,31 @@ export const transport = new WebSocketTransport({ return [] } + const bidPrice = isValidNumber(message.b) ? Number(message.b) : Number(message.bp) + const bidVolume = Number(message.bs) + const askPrice = isValidNumber(message.a) ? Number(message.a) : Number(message.ap) + const askVolume = Number(message.as) + + let midPrice: number + if (bidPrice == 0) { + midPrice = askPrice + } else if (askPrice == 0) { + midPrice = bidPrice + } else { + midPrice = (bidPrice * bidVolume + askPrice * askVolume) / (bidVolume + askVolume) + } + return [ { params: { base: message.s }, response: { result: null, data: { - bid_price: isValidNumber(message.b) ? Number(message.b) : Number(message.bp), - bid_volume: Number(message.bs), - ask_price: isValidNumber(message.a) ? Number(message.a) : Number(message.ap), - ask_volume: Number(message.as), + mid_price: midPrice, + bid_price: bidPrice, + bid_volume: bidVolume, + ask_price: askPrice, + ask_volume: askVolume, }, timestamps: { providerIndicatedTimeUnixMs: message.t, diff --git a/packages/sources/finage/test/integration/__snapshots__/adapter-stock-quote.test.ts.snap b/packages/sources/finage/test/integration/__snapshots__/adapter-stock-quote.test.ts.snap index f119e5e362..3c26ebd208 100644 --- a/packages/sources/finage/test/integration/__snapshots__/adapter-stock-quote.test.ts.snap +++ b/packages/sources/finage/test/integration/__snapshots__/adapter-stock-quote.test.ts.snap @@ -7,6 +7,7 @@ exports[`stock quotes websocket stock quotes endpoint missing a and b fields sho "ask_volume": 11, "bid_price": 12, "bid_volume": 13, + "mid_price": 11.083333333333334, }, "result": null, "statusCode": 200, @@ -18,6 +19,44 @@ exports[`stock quotes websocket stock quotes endpoint missing a and b fields sho } `; +exports[`stock quotes websocket stock quotes endpoint should return ask when bid is 0 1`] = ` +{ + "data": { + "ask_price": 5, + "ask_volume": 6, + "bid_price": 0, + "bid_volume": 8, + "mid_price": 5, + }, + "result": null, + "statusCode": 200, + "timestamps": { + "providerDataReceivedUnixMs": 1018, + "providerDataStreamEstablishedUnixMs": 1010, + "providerIndicatedTimeUnixMs": 7, + }, +} +`; + +exports[`stock quotes websocket stock quotes endpoint should return bid when ask is 0 1`] = ` +{ + "data": { + "ask_price": 0, + "ask_volume": 6, + "bid_price": 7, + "bid_volume": 8, + "mid_price": 7, + }, + "result": null, + "statusCode": 200, + "timestamps": { + "providerDataReceivedUnixMs": 1018, + "providerDataStreamEstablishedUnixMs": 1010, + "providerIndicatedTimeUnixMs": 5, + }, +} +`; + exports[`stock quotes websocket stock quotes endpoint should return success 1`] = ` { "data": { @@ -25,6 +64,7 @@ exports[`stock quotes websocket stock quotes endpoint should return success 1`] "ask_volume": 6, "bid_price": 7, "bid_volume": 8, + "mid_price": 6.142857142857143, }, "result": null, "statusCode": 200, diff --git a/packages/sources/finage/test/integration/adapter-stock-quote.test.ts b/packages/sources/finage/test/integration/adapter-stock-quote.test.ts index a723d98bb7..eb71559f6e 100644 --- a/packages/sources/finage/test/integration/adapter-stock-quote.test.ts +++ b/packages/sources/finage/test/integration/adapter-stock-quote.test.ts @@ -41,7 +41,7 @@ describe('stock quotes websocket', () => { // Send initial request to start background execute and wait for cache to be filled with results await testAdapter.request(data) await testAdapter.request(fallBackData) - await testAdapter.waitForCache(2) + await testAdapter.waitForCache(4) }) afterAll(async () => { @@ -61,5 +61,21 @@ describe('stock quotes websocket', () => { const response = await testAdapter.request(fallBackData) expect(response.json()).toMatchSnapshot() }) + + it('should return bid when ask is 0', async () => { + const response = await testAdapter.request({ + base: 'NO_ASK', + endpoint: 'stock_quotes', + }) + expect(response.json()).toMatchSnapshot() + }) + + it('should return ask when bid is 0', async () => { + const response = await testAdapter.request({ + base: 'NO_BID', + endpoint: 'stock_quotes', + }) + expect(response.json()).toMatchSnapshot() + }) }) }) diff --git a/packages/sources/finage/test/integration/fixtures.ts b/packages/sources/finage/test/integration/fixtures.ts index 276bd3f577..3c1937b088 100644 --- a/packages/sources/finage/test/integration/fixtures.ts +++ b/packages/sources/finage/test/integration/fixtures.ts @@ -256,6 +256,22 @@ export const mockStockQuotesWebSocketServer = (URL: string): MockWebsocketServer bs: '13', t: 14, }, + { + s: 'NO_BID', + a: '5', + as: '6', + b: '0', + bs: '8', + t: 7, + }, + { + s: 'NO_ASK', + a: '0', + as: '6', + b: '7', + bs: '8', + t: 5, + }, ] const mockWsServer = new MockWebsocketServer(URL, { mock: false }) mockWsServer.on('connection', (socket) => {