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
5 changes: 5 additions & 0 deletions .changeset/yellow-laws-repeat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@chainlink/dxfeed-adapter': minor
---

Only use O exchange for overnight
5 changes: 5 additions & 0 deletions packages/sources/dxfeed/src/endpoint/stock-quotes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ export const stockQuotesInputParameters = new InputParameters({
description: 'If true, throw error if bid/ask volume is invalid',
type: 'boolean',
},
isOvernight: {
default: false,
description: 'If true, only accept O exchange codes for bid and ask',
type: 'boolean',
},
})

export type BaseEndpointTypes = {
Expand Down
64 changes: 39 additions & 25 deletions packages/sources/dxfeed/src/transport/stock-quotes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { makeLogger } from '@chainlink/external-adapter-framework/util'
import { makeLogger, splitArrayIntoChunks } from '@chainlink/external-adapter-framework/util'
import { BaseEndpointTypes } from '../endpoint/stock-quotes'
import { buildWsTransport } from './ws'

Expand All @@ -8,9 +8,11 @@ const logger = makeLogger('StockQuotesTransport')
// "bidPrice","bidSize","askTime","askExchangeCode","askPrice","askSize"]
const eventSymbolIndex = 0
const bidTimeIndex = 4
const bidExchangeCodeIndex = 5
const bidPriceIndex = 6
const bidSizeIndex = 7
const askTimeIndex = 8
const askExchangeCodeIndex = 9
const askPriceIndex = 10
const askSizeIndex = 11
const dataLength = 12
Expand All @@ -30,28 +32,28 @@ export const transport = buildWsTransport<BaseEndpointTypes>(
return []
}

return Array.from({ length: data.length / dataLength }, (_, i) => i * dataLength)
.map((i) => generateResponse(data, i))
.flat()
return splitArrayIntoChunks(data, dataLength).flatMap((chunk) => generateResponse(chunk))
},
)

const generateResponse = (data: (string | number)[], i: number) => {
const bidVolume = Number(data[i + bidSizeIndex])
const askVolume = Number(data[i + askSizeIndex])
const generateResponse = (data: (string | number)[]) => {
const bidVolume = Number(data[bidSizeIndex])
const askVolume = Number(data[askSizeIndex])
const invalidVolume = Number.isNaN(Number(bidVolume)) || Number.isNaN(Number(askVolume))
const bidPrice = Number(data[i + bidPriceIndex])
const askPrice = Number(data[i + askPriceIndex])
const bidPrice = Number(data[bidPriceIndex])
const askPrice = Number(data[askPriceIndex])

const params = { base: data[i + eventSymbolIndex].toString() }
const params = { base: data[eventSymbolIndex].toString() }
if (Number.isNaN(bidPrice) || Number.isNaN(askPrice)) {
const response = {
statusCode: 502,
errorMessage: `Bid price: ${bidPrice} or Ask price: ${askPrice} for ${params.base} is invalid.`,
}
return [
{ params: { ...params, requireVolume: false }, response },
{ params: { ...params, requireVolume: true }, response },
{ params: { ...params, requireVolume: false, isOvernight: true }, response },
{ params: { ...params, requireVolume: true, isOvernight: true }, response },
{ params: { ...params, requireVolume: false, isOvernight: false }, response },
{ params: { ...params, requireVolume: true, isOvernight: false }, response },
]
}

Expand All @@ -75,23 +77,35 @@ const generateResponse = (data: (string | number)[], i: number) => {
ask_volume: askVolume,
},
timestamps: {
providerIndicatedTimeUnixMs: Math.max(
Number(data[i + bidTimeIndex]),
Number(data[i + askTimeIndex]),
),
providerIndicatedTimeUnixMs: Math.max(Number(data[bidTimeIndex]), Number(data[askTimeIndex])),
},
}

return [
{ params: { ...params, requireVolume: false }, response },
const requireVolumeResponse = invalidVolume
? {
statusCode: 502,
errorMessage: `Bid volume: ${bidVolume} or Ask volume: ${askVolume} for ${params.base} is invalid.`,
}
: response

const responses = [
{ params: { ...params, requireVolume: false, isOvernight: false }, response },
{
params: { ...params, requireVolume: true },
response: invalidVolume
? {
statusCode: 502,
errorMessage: `Bid volume: ${bidVolume} or Ask volume: ${askVolume} for ${params.base} is invalid.`,
}
: response,
params: { ...params, requireVolume: true, isOvernight: false },
response: requireVolumeResponse,
},
]

if (data[bidExchangeCodeIndex] === 'O' && data[askExchangeCodeIndex] === 'O') {
responses.push({
params: { ...params, requireVolume: false, isOvernight: true },
response,
})
responses.push({
params: { ...params, requireVolume: true, isOvernight: true },
response: requireVolumeResponse,
})
}

return responses
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,44 @@ exports[`websocket quote endpoint error when data length is not valid 1`] = `
}
`;

exports[`websocket quote endpoint return O when isOvernight 1`] = `
{
"data": {
"ask_price": 172,
"ask_volume": 100,
"bid_price": 170,
"bid_volume": 148,
"mid_price": 171,
},
"result": null,
"statusCode": 200,
"timestamps": {
"providerDataReceivedUnixMs": 1018,
"providerDataStreamEstablishedUnixMs": 1010,
"providerIndicatedTimeUnixMs": 1670868378000,
},
}
`;

exports[`websocket quote endpoint return X when not isOvernight 1`] = `
{
"data": {
"ask_price": 172.1,
"ask_volume": 100.1,
"bid_price": 170.1,
"bid_volume": 148.1,
"mid_price": 171.1,
},
"result": null,
"statusCode": 200,
"timestamps": {
"providerDataReceivedUnixMs": 1018,
"providerDataStreamEstablishedUnixMs": 1010,
"providerIndicatedTimeUnixMs": 1670868378000,
},
}
`;

exports[`websocket quote endpoint should return ask when bid is 0 1`] = `
{
"data": {
Expand Down
14 changes: 14 additions & 0 deletions packages/sources/dxfeed/test/integration/adapter-ws.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ describe('websocket', () => {
base: 'MULTI_2',
endpoint: 'stock_quotes',
}
const quoteOXData = {
base: 'OX',
endpoint: 'stock_quotes',
}

beforeAll(async () => {
oldEnv = JSON.parse(JSON.stringify(process.env))
Expand Down Expand Up @@ -173,5 +177,15 @@ describe('websocket', () => {
})
expect(response.json()).toMatchSnapshot()
})

it('return O when isOvernight', async () => {
const response = await testAdapter.request({ ...quoteOXData, isOvernight: true })
expect(response.json()).toMatchSnapshot()
})

it('return X when not isOvernight', async () => {
const response = await testAdapter.request({ ...quoteOXData })
expect(response.json()).toMatchSnapshot()
})
})
})
38 changes: 29 additions & 9 deletions packages/sources/dxfeed/test/integration/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export const mockWebSocketServer = (URL: string): MockWebsocketServer => {
{
data: [
'Quote',
['TSLA', 0, 0, 0, 1670868378000, 'V', 170.0, 148.0, 1670868370000, 'V', 172.0, 100.0],
['TSLA', 0, 0, 0, 1670868378000, 'O', 170.0, 148.0, 1670868370000, 'O', 172.0, 100.0],
],
channel: '/service/data',
},
Expand All @@ -67,8 +67,8 @@ export const mockWebSocketServer = (URL: string): MockWebsocketServer => {
data: [
'Quote',
[
...['MULTI_1', 1, 2, 3, 4, 'V', 5, 6, 7, 'V', 8, 9],
...['MULTI_2', 10, 11, 12, 13, 'V', 14, 15, 16, 'V', 17, 18],
...['MULTI_1', 1, 2, 3, 4, 'O', 5, 6, 7, 'O', 8, 9],
...['MULTI_2', 10, 11, 12, 13, 'O', 14, 15, 16, 'O', 17, 18],
],
],
channel: '/service/data',
Expand All @@ -78,7 +78,7 @@ export const mockWebSocketServer = (URL: string): MockWebsocketServer => {
{
data: [
'Quote',
['NO_BID', 0, 0, 0, 1670868378000, 'V', 0, 148.0, 1670868370000, 'V', 172.0, 100.0],
['NO_BID', 0, 0, 0, 1670868378000, 'O', 0, 148.0, 1670868370000, 'O', 172.0, 100.0],
],
channel: '/service/data',
},
Expand All @@ -87,7 +87,7 @@ export const mockWebSocketServer = (URL: string): MockWebsocketServer => {
{
data: [
'Quote',
['NO_ASK', 0, 0, 0, 1670868378000, 'V', 170.0, 148.0, 1670868370000, 'V', 0, 100.0],
['NO_ASK', 0, 0, 0, 1670868378000, 'O', 170.0, 148.0, 1670868370000, 'O', 0, 100.0],
],
channel: '/service/data',
},
Expand All @@ -96,7 +96,7 @@ export const mockWebSocketServer = (URL: string): MockWebsocketServer => {
{
data: [
'Quote',
['NO_VOLUME', 0, 0, 0, 1670868378000, 'V', 170.0, 'NaN', 1670868370000, 'V', 172.0, 'NaN'],
['NO_VOLUME', 0, 0, 0, 1670868378000, 'O', 170.0, 'NaN', 1670868370000, 'O', 172.0, 'NaN'],
],
channel: '/service/data',
},
Expand All @@ -105,7 +105,7 @@ export const mockWebSocketServer = (URL: string): MockWebsocketServer => {
{
data: [
'Quote',
['INVALID_DATA', 0, 0, 0, 1670868378000, 'V', 170.0, 148.0, 1670868370000, 'V', 0],
['INVALID_DATA', 0, 0, 0, 1670868378000, 'O', 170.0, 148.0, 1670868370000, 'O', 0],
],
channel: '/service/data',
},
Expand All @@ -114,7 +114,7 @@ export const mockWebSocketServer = (URL: string): MockWebsocketServer => {
{
data: [
'Quote',
['NULL_BID', 0, 0, 0, 1670868378000, 'V', 'NaN', 148.0, 1670868370000, 'V', 172.0, 100.0],
['NULL_BID', 0, 0, 0, 1670868378000, 'O', 'NaN', 148.0, 1670868370000, 'O', 172.0, 100.0],
],
channel: '/service/data',
},
Expand All @@ -123,7 +123,25 @@ export const mockWebSocketServer = (URL: string): MockWebsocketServer => {
{
data: [
'Quote',
['NULL_ASK', 0, 0, 0, 1670868378000, 'V', 170.0, 148.0, 1670868370000, 'V', 'NaN', 100.0],
['NULL_ASK', 0, 0, 0, 1670868378000, 'O', 170.0, 148.0, 1670868370000, 'O', 'NaN', 100.0],
],
channel: '/service/data',
},
]
const quoteReponseInO = [
{
data: [
'Quote',
['OX', 0, 0, 0, 1670868378000, 'O', 170.0, 148.0, 1670868370000, 'O', 172.0, 100.0],
],
channel: '/service/data',
},
]
const quoteReponseInX = [
{
data: [
'Quote',
['OX', 0, 0, 0, 1670868378000, 'O', 170.1, 148.1, 1670868370000, 'X', 172.1, 100.1],
],
channel: '/service/data',
},
Expand Down Expand Up @@ -316,6 +334,8 @@ export const mockWebSocketServer = (URL: string): MockWebsocketServer => {
socket.send(JSON.stringify(invalidQuoteReponse))
socket.send(JSON.stringify(nullBidQuoteResponse))
socket.send(JSON.stringify(nullAskQuoteResponse))
socket.send(JSON.stringify(quoteReponseInO))
socket.send(JSON.stringify(quoteReponseInX))
socket.send(JSON.stringify(tradeResponse))
socket.send(JSON.stringify(tradeResponseIgnored))
socket.send(JSON.stringify(tradeResponseOvernight))
Expand Down
Loading