Skip to content

Commit 27ca10a

Browse files
authored
Add stock quote to finage (#4158)
* Add stock quote to finage * Comments * Adjust test
1 parent 8deb845 commit 27ca10a

File tree

9 files changed

+272
-10
lines changed

9 files changed

+272
-10
lines changed

.changeset/tame-donuts-hammer.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@chainlink/finage-adapter': minor
3+
---
4+
5+
Add stock quotes

packages/sources/finage/src/config/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,12 @@ export const config = new AdapterConfig({
2121
STOCK_WS_API_ENDPOINT: {
2222
type: 'string',
2323
default: 'wss://e4s39ar3mr.finage.ws:7002',
24-
description: 'The Websocket endpoint to connect to for stock data',
24+
description: 'The Websocket endpoint to connect to for stock trades data',
25+
},
26+
STOCK_QUOTES_WS_API_ENDPOINT: {
27+
type: 'string',
28+
default: 'wss://xs68rzvrjn.finage.ws:7003',
29+
description: 'The Websocket endpoint to connect to for stock quotes data',
2530
},
2631
FOREX_WS_API_ENDPOINT: {
2732
type: 'string',

packages/sources/finage/src/endpoint/index.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
export { endpoint as commodities } from './commodities'
12
export { endpoint as crypto } from './crypto'
2-
export { endpoint as stock } from './stock'
33
export { endpoint as eod } from './eod'
4-
export { endpoint as commodities } from './commodities'
4+
export { endpoint as etf } from './etf'
55
export { endpoint as forex } from './forex'
6+
export { endpoint as stock } from './stock'
7+
export { endpoint as stockQuotes } from './stock-quotes'
68
export { endpoint as ukEtf } from './uk-etf'
7-
export { endpoint as etf } from './etf'
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { AdapterEndpoint } from '@chainlink/external-adapter-framework/adapter'
2+
import { stockEndpointInputParametersDefinition } from '@chainlink/external-adapter-framework/adapter/stock'
3+
import { config } from '../config'
4+
import overrides from '../config/overrides.json'
5+
import { transport } from '../transport/stock-quotes'
6+
import { stockInputParameters } from './utils'
7+
8+
export type BaseEndpointTypes = {
9+
Parameters: typeof stockEndpointInputParametersDefinition
10+
Settings: typeof config.settings
11+
Response: {
12+
Result: null
13+
Data: {
14+
bid_price: number
15+
bid_volume: number
16+
ask_price: number
17+
ask_volume: number
18+
}
19+
}
20+
}
21+
22+
export const endpoint = new AdapterEndpoint({
23+
name: 'stock_quotes',
24+
aliases: [],
25+
transport,
26+
inputParameters: stockInputParameters,
27+
overrides: overrides.finage,
28+
})

packages/sources/finage/src/index.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
import { expose, ServerInstance } from '@chainlink/external-adapter-framework'
2-
import { IncludesFile } from '@chainlink/external-adapter-framework/adapter'
3-
import { PriceAdapter } from '@chainlink/external-adapter-framework/adapter'
4-
import { config } from './config'
5-
import { commodities, crypto, eod, etf, forex, stock, ukEtf } from './endpoint'
2+
import { IncludesFile, PriceAdapter } from '@chainlink/external-adapter-framework/adapter'
63
import { PriceEndpointInputParametersDefinition } from '@chainlink/external-adapter-framework/adapter/price'
74
import { AdapterParams } from '@chainlink/external-adapter-framework/adapter/types'
5+
import { CustomSettingsDefinition } from '@chainlink/external-adapter-framework/config'
86
import {
97
AdapterRequest,
108
AdapterRequestContext,
119
AdapterResponse,
1210
} from '@chainlink/external-adapter-framework/util'
13-
import { CustomSettingsDefinition } from '@chainlink/external-adapter-framework/config'
11+
import { config } from './config'
12+
import { commodities, crypto, eod, etf, forex, stock, stockQuotes, ukEtf } from './endpoint'
1413

1514
export type PriceAdapterRequest<T> = AdapterRequest<T> & {
1615
requestContext: AdapterRequestContext<T> & {
@@ -70,7 +69,7 @@ export const adapter = new FinageAdapter({
7069
defaultEndpoint: stock.name,
7170
name: 'FINAGE',
7271
config,
73-
endpoints: [crypto, stock, eod, commodities, forex, ukEtf, etf],
72+
endpoints: [crypto, stock, stockQuotes, eod, commodities, forex, ukEtf, etf],
7473
rateLimiting: {
7574
tiers: {
7675
professionalstocksusstockmarket: {
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { WebSocketTransport } from '@chainlink/external-adapter-framework/transports/websocket'
2+
import { makeLogger } from '@chainlink/external-adapter-framework/util'
3+
import { BaseEndpointTypes } from '../endpoint/stock-quotes'
4+
5+
const logger = makeLogger('StockQuotes')
6+
7+
export interface StockQuoteMessage {
8+
s: string // Symbol
9+
a: string // ask_price
10+
ap: string // ask_price fallback
11+
as: string // ask_volume
12+
b: string // bid_price
13+
bp: string // bid_price fallback
14+
bs: string // bid_volume
15+
t: number // providerIndicatedTime
16+
status_code: number
17+
message: string
18+
}
19+
20+
type WsTransportTypes = BaseEndpointTypes & {
21+
Provider: {
22+
WsMessage: StockQuoteMessage
23+
}
24+
}
25+
26+
const isValidNumber = (field: string) => field && field.length > 0 && !isNaN(Number(field))
27+
28+
export const transport = new WebSocketTransport<WsTransportTypes>({
29+
url: (context) => {
30+
return `${context.adapterSettings.STOCK_QUOTES_WS_API_ENDPOINT}/?token=${context.adapterSettings.WS_SOCKET_KEY}`
31+
},
32+
handlers: {
33+
message(message) {
34+
if (message.status_code) {
35+
logger.info(`Received general message: ${JSON.stringify(message)}`)
36+
return []
37+
}
38+
if (!message.s || !isValidNumber(message.as) || !isValidNumber(message.bs)) {
39+
logger.warn(`Received ${JSON.stringify(message)} with invalid s or as or bs field.`)
40+
return []
41+
}
42+
if (!isValidNumber(message.b) && !isValidNumber(message.bp)) {
43+
logger.warn(`Received ${JSON.stringify(message)} with invalid b and bp fields.`)
44+
return []
45+
}
46+
if (!isValidNumber(message.a) && !isValidNumber(message.ap)) {
47+
logger.warn(`Received ${JSON.stringify(message)} with invalid a and ap fields.`)
48+
return []
49+
}
50+
51+
return [
52+
{
53+
params: { base: message.s },
54+
response: {
55+
result: null,
56+
data: {
57+
bid_price: isValidNumber(message.b) ? Number(message.b) : Number(message.bp),
58+
bid_volume: Number(message.bs),
59+
ask_price: isValidNumber(message.a) ? Number(message.a) : Number(message.ap),
60+
ask_volume: Number(message.as),
61+
},
62+
timestamps: {
63+
providerIndicatedTimeUnixMs: message.t,
64+
},
65+
},
66+
},
67+
]
68+
},
69+
},
70+
71+
builders: {
72+
subscribeMessage: (params) => {
73+
return { action: 'subscribe', symbols: `${params.base}`.toUpperCase() }
74+
},
75+
unsubscribeMessage: (params) => {
76+
return { action: 'unsubscribe', symbols: `${params.base}`.toUpperCase() }
77+
},
78+
},
79+
})
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`stock quotes websocket stock quotes endpoint missing a and b fields should fallback 1`] = `
4+
{
5+
"data": {
6+
"ask_price": 10,
7+
"ask_volume": 11,
8+
"bid_price": 12,
9+
"bid_volume": 13,
10+
},
11+
"result": null,
12+
"statusCode": 200,
13+
"timestamps": {
14+
"providerDataReceivedUnixMs": 1018,
15+
"providerDataStreamEstablishedUnixMs": 1010,
16+
"providerIndicatedTimeUnixMs": 14,
17+
},
18+
}
19+
`;
20+
21+
exports[`stock quotes websocket stock quotes endpoint should return success 1`] = `
22+
{
23+
"data": {
24+
"ask_price": 5,
25+
"ask_volume": 6,
26+
"bid_price": 7,
27+
"bid_volume": 8,
28+
},
29+
"result": null,
30+
"statusCode": 200,
31+
"timestamps": {
32+
"providerDataReceivedUnixMs": 1018,
33+
"providerDataStreamEstablishedUnixMs": 1010,
34+
"providerIndicatedTimeUnixMs": 9,
35+
},
36+
}
37+
`;
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { WebSocketClassProvider } from '@chainlink/external-adapter-framework/transports'
2+
import {
3+
mockWebSocketProvider,
4+
MockWebsocketServer,
5+
setEnvVariables,
6+
TestAdapter,
7+
} from '@chainlink/external-adapter-framework/util/testing-utils'
8+
import FakeTimers from '@sinonjs/fake-timers'
9+
import { mockStockQuotesWebSocketServer } from './fixtures'
10+
11+
describe('stock quotes websocket', () => {
12+
let mockWsServerStockQuotes: MockWebsocketServer | undefined
13+
let testAdapter: TestAdapter
14+
const wsEndpointStockQuotes = 'ws://localhost:9094'
15+
let oldEnv: NodeJS.ProcessEnv
16+
const data = {
17+
endpoint: 'stock_quotes',
18+
base: 'AAPL',
19+
}
20+
const fallBackData = {
21+
endpoint: 'stock_quotes',
22+
base: 'FALLBACK',
23+
}
24+
25+
beforeAll(async () => {
26+
oldEnv = JSON.parse(JSON.stringify(process.env))
27+
process.env['STOCK_QUOTES_WS_API_ENDPOINT'] = wsEndpointStockQuotes
28+
process.env['WS_SOCKET_KEY'] = 'fake-api-key'
29+
process.env['API_KEY'] = 'fake-api-key'
30+
31+
// Start mock web socket server
32+
mockWebSocketProvider(WebSocketClassProvider)
33+
mockWsServerStockQuotes = mockStockQuotesWebSocketServer(wsEndpointStockQuotes)
34+
35+
const adapter = (await import('./../../src')).adapter
36+
testAdapter = await TestAdapter.startWithMockedCache(adapter, {
37+
clock: FakeTimers.install(),
38+
testAdapter: {} as TestAdapter<never>,
39+
})
40+
41+
// Send initial request to start background execute and wait for cache to be filled with results
42+
await testAdapter.request(data)
43+
await testAdapter.request(fallBackData)
44+
await testAdapter.waitForCache(2)
45+
})
46+
47+
afterAll(async () => {
48+
setEnvVariables(oldEnv)
49+
mockWsServerStockQuotes?.close()
50+
testAdapter.clock?.uninstall()
51+
await testAdapter.api.close()
52+
})
53+
54+
describe('stock quotes endpoint', () => {
55+
it('should return success', async () => {
56+
const response = await testAdapter.request(data)
57+
expect(response.json()).toMatchSnapshot()
58+
})
59+
60+
it('missing a and b fields should fallback', async () => {
61+
const response = await testAdapter.request(fallBackData)
62+
expect(response.json()).toMatchSnapshot()
63+
})
64+
})
65+
})

packages/sources/finage/test/integration/fixtures.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,3 +225,46 @@ export const mockEtfWebSocketServer = (URL: string): MockWebsocketServer => {
225225

226226
return mockWsServer
227227
}
228+
229+
export const mockStockQuotesWebSocketServer = (URL: string): MockWebsocketServer => {
230+
const wsResponse = [
231+
{
232+
status_code: 200,
233+
message: 'Connect',
234+
},
235+
{
236+
s: 'AAPL',
237+
a: 'lol', // In-valid fields
238+
as: '1',
239+
b: '2',
240+
bs: '3',
241+
t: 4,
242+
},
243+
{
244+
s: 'AAPL',
245+
a: '5',
246+
as: '6',
247+
b: '7',
248+
bs: '8',
249+
t: 9,
250+
},
251+
{
252+
s: 'FALLBACK',
253+
ap: '10',
254+
as: '11',
255+
bp: '12',
256+
bs: '13',
257+
t: 14,
258+
},
259+
]
260+
const mockWsServer = new MockWebsocketServer(URL, { mock: false })
261+
mockWsServer.on('connection', (socket) => {
262+
socket.on('message', () => {
263+
wsResponse.forEach((message) => {
264+
socket.send(JSON.stringify(message))
265+
})
266+
})
267+
})
268+
269+
return mockWsServer
270+
}

0 commit comments

Comments
 (0)