Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/violet-zoos-walk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@chainlink/dxfeed-adapter': minor
'@chainlink/finage-adapter': minor
---

Add mid to stock quotes
1 change: 1 addition & 0 deletions packages/sources/dxfeed/src/endpoint/stock-quotes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type BaseEndpointTypes = {
Response: {
Result: null
Data: {
mid_price: number
bid_price: number
bid_volume: number
ask_price: number
Expand Down
37 changes: 35 additions & 2 deletions packages/sources/dxfeed/src/transport/stock-quotes.ts
Original file line number Diff line number Diff line change
@@ -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<BaseEndpointTypes>(
(params) => ({ Quote: [params.base.toUpperCase()] }),
Expand All @@ -16,17 +22,44 @@ export const transport = buildWsTransport<BaseEndpointTypes>(

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]),
),
},
},
},
]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,69 @@
// 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": {
"ask_price": 172,
"ask_volume": 100,
"bid_price": 170,
"bid_volume": 148,
"mid_price": 170.80645161290323,
},
"result": null,
"statusCode": 200,
"timestamps": {
"providerDataReceivedUnixMs": 1018,
"providerDataStreamEstablishedUnixMs": 1010,
"providerIndicatedTimeUnixMs": 1670868378000,
},
}
`;
Expand Down
26 changes: 25 additions & 1 deletion packages/sources/dxfeed/test/integration/adapter-ws.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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()
})
})
})
31 changes: 31 additions & 0 deletions packages/sources/dxfeed/test/integration/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down Expand Up @@ -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))
})
})
Expand Down
1 change: 1 addition & 0 deletions packages/sources/finage/src/endpoint/stock-quotes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export type BaseEndpointTypes = {
Response: {
Result: null
Data: {
mid_price: number
bid_price: number
bid_volume: number
ask_price: number
Expand Down
23 changes: 19 additions & 4 deletions packages/sources/finage/src/transport/stock-quotes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,31 @@ export const transport = new WebSocketTransport<WsTransportTypes>({
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -18,13 +19,52 @@ 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": {
"ask_price": 5,
"ask_volume": 6,
"bid_price": 7,
"bid_volume": 8,
"mid_price": 6.142857142857143,
},
"result": null,
"statusCode": 200,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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()
})
})
})
16 changes: 16 additions & 0 deletions packages/sources/finage/test/integration/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
Loading