Skip to content

Commit 99d8ce9

Browse files
authored
finnhub-secondary: Add market status endpoint (#3900)
* finnhub-secondary: Add market status endpoint * Fix test * Allow lower case market names * Fix test
1 parent 76e8737 commit 99d8ce9

File tree

12 files changed

+324
-5
lines changed

12 files changed

+324
-5
lines changed

.changeset/purple-dryers-fry.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@chainlink/finnhub-secondary-adapter': minor
3+
---
4+
5+
Add market-status endpoint

.changeset/shy-cheetahs-marry.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@chainlink/finnhub-adapter': patch
3+
---
4+
5+
Export market status endpoint and allow lower case market names
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
export { endpoint as marketStatus } from './market-status'
12
export { endpoint as quote } from './quote'
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { marketStatusEndpoint } from '@chainlink/finnhub-adapter'
2+
3+
export const endpoint = marketStatusEndpoint

packages/sources/finnhub-secondary/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { expose, ServerInstance } from '@chainlink/external-adapter-framework'
22
import { PriceAdapter } from '@chainlink/external-adapter-framework/adapter'
3-
import { quote } from './endpoint'
43
import { config, rateLimiting } from '@chainlink/finnhub-adapter'
54
import includes from './config/includes.json'
5+
import { marketStatus, quote } from './endpoint'
66

77
export const adapter = new PriceAdapter({
88
defaultEndpoint: quote.name,
99
name: 'FINNHUB-SECONDARY',
1010
config,
11-
endpoints: [quote],
11+
endpoints: [quote, marketStatus],
1212
rateLimiting,
1313
includes,
1414
})
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`Market status endpoint should return error for invalid market 1`] = `
4+
{
5+
"error": {
6+
"message": "[Param: market] input is not one of valid options (AD,AS,AT,AX,BA,BC,BD,BE,BH,BK,BO,BR,CA,CN,CO,CR,CS,DB,DE,DU,F,HE,HK,HM,IC,IR,IS,JK,JO,KL,KQ,KS,KW,L,LS,MC,ME,MI,MT,MU,MX,NE,NL,NS,NZ,OL,PA,PM,PR,QA,RO,RG,SA,SG,SI,SN,SR,SS,ST,SW,SZ,T,TA,TL,TO,TW,TWO,US,V,VI,VN,VS,WA,HA,SX,TG,SC,NYSE,ad,as,at,ax,ba,bc,bd,be,bh,bk,bo,br,ca,cn,co,cr,cs,db,de,du,f,he,hk,hm,ic,ir,is,jk,jo,kl,kq,ks,kw,l,ls,mc,me,mi,mt,mu,mx,ne,nl,ns,nz,ol,pa,pm,pr,qa,ro,rg,sa,sg,si,sn,sr,ss,st,sw,sz,t,ta,tl,to,tw,two,us,v,vi,vn,vs,wa,ha,sx,tg,sc,nyse)",
7+
"name": "AdapterError",
8+
},
9+
"status": "errored",
10+
"statusCode": 400,
11+
}
12+
`;
13+
14+
exports[`Market status endpoint should return success with closed 1`] = `
15+
{
16+
"data": {
17+
"result": 1,
18+
},
19+
"result": 1,
20+
"statusCode": 200,
21+
"timestamps": {
22+
"providerDataReceivedUnixMs": 1641035471111,
23+
"providerDataRequestedUnixMs": 1641035471111,
24+
"providerIndicatedTimeUnixMs": 1697018041000,
25+
},
26+
}
27+
`;
28+
29+
exports[`Market status endpoint should return success with closed; null status in response 1`] = `
30+
{
31+
"data": {
32+
"result": 1,
33+
},
34+
"result": 1,
35+
"statusCode": 200,
36+
"timestamps": {
37+
"providerDataReceivedUnixMs": 1641035471111,
38+
"providerDataRequestedUnixMs": 1641035471111,
39+
"providerIndicatedTimeUnixMs": 1697018041000,
40+
},
41+
}
42+
`;
43+
44+
exports[`Market status endpoint should return success with lower case market 1`] = `
45+
{
46+
"data": {
47+
"result": 2,
48+
},
49+
"result": 2,
50+
"statusCode": 200,
51+
"timestamps": {
52+
"providerDataReceivedUnixMs": 1641035471111,
53+
"providerDataRequestedUnixMs": 1641035471111,
54+
"providerIndicatedTimeUnixMs": 1697018041000,
55+
},
56+
}
57+
`;
58+
59+
exports[`Market status endpoint should return success with open 1`] = `
60+
{
61+
"data": {
62+
"result": 2,
63+
},
64+
"result": 2,
65+
"statusCode": 200,
66+
"timestamps": {
67+
"providerDataReceivedUnixMs": 1641035471111,
68+
"providerDataRequestedUnixMs": 1641035471111,
69+
"providerIndicatedTimeUnixMs": 1697018041000,
70+
},
71+
}
72+
`;
73+
74+
exports[`Market status endpoint should return success with unknown 1`] = `
75+
{
76+
"data": {
77+
"result": 0,
78+
},
79+
"result": 0,
80+
"statusCode": 200,
81+
"timestamps": {
82+
"providerDataReceivedUnixMs": 1641035471111,
83+
"providerDataRequestedUnixMs": 1641035471111,
84+
},
85+
}
86+
`;
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { MarketStatus } from '@chainlink/external-adapter-framework/adapter'
2+
import {
3+
TestAdapter,
4+
setEnvVariables,
5+
} from '@chainlink/external-adapter-framework/util/testing-utils'
6+
import * as nock from 'nock'
7+
import process from 'process'
8+
9+
import { mockMarketStatusResponseSuccess } from './fixtures'
10+
11+
describe('Market status endpoint', () => {
12+
let spy: jest.SpyInstance
13+
let testAdapter: TestAdapter
14+
let oldEnv: NodeJS.ProcessEnv
15+
16+
beforeAll(async () => {
17+
oldEnv = JSON.parse(JSON.stringify(process.env))
18+
process.env['API_KEY'] = 'fake-api-key'
19+
const mockDate = new Date('2022-01-01T11:11:11.111Z')
20+
spy = jest.spyOn(Date, 'now').mockReturnValue(mockDate.getTime())
21+
22+
const adapter = (await import('./../../src')).adapter
23+
adapter.rateLimiting = undefined
24+
testAdapter = await TestAdapter.startWithMockedCache(adapter, {
25+
testAdapter: {} as TestAdapter<never>,
26+
})
27+
})
28+
29+
afterAll(async () => {
30+
setEnvVariables(oldEnv)
31+
await testAdapter.api.close()
32+
nock.restore()
33+
nock.cleanAll()
34+
spy.mockRestore()
35+
})
36+
37+
const openMarket = {
38+
endpoint: 'market-status',
39+
market: 'NYSE',
40+
}
41+
const closedMarket = {
42+
endpoint: 'market-status',
43+
market: 'AD',
44+
}
45+
const nullMarket = {
46+
endpoint: 'market-status',
47+
market: 'AS',
48+
}
49+
const unknownMarket = {
50+
endpoint: 'market-status',
51+
market: 'AT',
52+
}
53+
const invalidMarket = {
54+
endpoint: 'market-status',
55+
market: 'invalid_market',
56+
}
57+
const lowerCaseMarket = {
58+
endpoint: 'market-status',
59+
market: 'nyse',
60+
}
61+
62+
it('should return success with open', async () => {
63+
mockMarketStatusResponseSuccess()
64+
65+
const response = await testAdapter.request(openMarket)
66+
expect(response.json()).toMatchSnapshot()
67+
expect(response.json().result).toEqual(MarketStatus.OPEN)
68+
})
69+
70+
it('should return success with closed', async () => {
71+
mockMarketStatusResponseSuccess()
72+
73+
const response = await testAdapter.request(closedMarket)
74+
expect(response.json()).toMatchSnapshot()
75+
expect(response.json().result).toEqual(MarketStatus.CLOSED)
76+
})
77+
78+
it('should return success with closed; null status in response', async () => {
79+
mockMarketStatusResponseSuccess()
80+
81+
const response = await testAdapter.request(nullMarket)
82+
expect(response.json()).toMatchSnapshot()
83+
expect(response.json().result).toEqual(MarketStatus.CLOSED)
84+
})
85+
86+
it('should return success with unknown', async () => {
87+
mockMarketStatusResponseSuccess()
88+
89+
const response = await testAdapter.request(unknownMarket)
90+
expect(response.json()).toMatchSnapshot()
91+
expect(response.json().result).toEqual(MarketStatus.UNKNOWN)
92+
})
93+
94+
it('should return error for invalid market', async () => {
95+
mockMarketStatusResponseSuccess()
96+
97+
const response = await testAdapter.request(invalidMarket)
98+
expect(response.json()).toMatchSnapshot()
99+
expect(response.statusCode).toBe(400)
100+
})
101+
102+
it('should return success with lower case market', async () => {
103+
mockMarketStatusResponseSuccess()
104+
105+
const response = await testAdapter.request(lowerCaseMarket)
106+
expect(response.json()).toMatchSnapshot()
107+
expect(response.json().result).toEqual(MarketStatus.OPEN)
108+
})
109+
})

packages/sources/finnhub-secondary/test/integration/fixtures.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,65 @@ export const mockResponseSuccess = (): nock.Scope => {
8181
],
8282
)
8383
}
84+
85+
export const mockMarketStatusResponseSuccess = (): nock.Scope =>
86+
nock('https://finnhub.io/api/v1', {
87+
encodedQueryParams: true,
88+
})
89+
.persist()
90+
.get('/stock/market-status')
91+
.query({ token: 'fake-api-key', exchange: 'US' })
92+
.reply(
93+
200,
94+
{
95+
exchange: 'US',
96+
holiday: null,
97+
isOpen: true,
98+
session: 'regular',
99+
timezone: 'America/New_York',
100+
t: 1697018041,
101+
},
102+
['Content-Type', 'application/json'],
103+
)
104+
.get('/stock/market-status')
105+
.query({ token: 'fake-api-key', exchange: 'AD' })
106+
.reply(
107+
200,
108+
{
109+
exchange: 'AD',
110+
holiday: null,
111+
isOpen: false,
112+
session: 'pre-market',
113+
timezone: 'Asia/Dubai',
114+
t: 1697018041,
115+
},
116+
['Content-Type', 'application/json'],
117+
)
118+
.get('/stock/market-status')
119+
.query({ token: 'fake-api-key', exchange: 'AS' })
120+
.reply(
121+
200,
122+
{
123+
exchange: 'AS',
124+
holiday: 'Fake Holiday',
125+
isOpen: false,
126+
session: null,
127+
timezone: 'Europe/Amsterdam',
128+
t: 1697018041,
129+
},
130+
['Content-Type', 'application/json'],
131+
)
132+
.get('/stock/market-status')
133+
.query({ token: 'fake-api-key', exchange: 'AT' })
134+
.reply(
135+
200,
136+
{
137+
exchange: '',
138+
holiday: null,
139+
isOpen: null,
140+
session: null,
141+
timezone: null,
142+
t: null,
143+
},
144+
['Content-Type', 'application/json'],
145+
)

packages/sources/finnhub/src/endpoint/market-status.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,25 @@ import {
22
MarketStatusEndpoint,
33
MarketStatusResultResponse,
44
} from '@chainlink/external-adapter-framework/adapter'
5+
import { AdapterRequest } from '@chainlink/external-adapter-framework/util'
56
import { InputParameters } from '@chainlink/external-adapter-framework/validation'
7+
import { TypeFromDefinition } from '@chainlink/external-adapter-framework/validation/input-params'
68

79
import { config } from '../config'
810
import { marketAliases, transport } from '../transport/market-status'
911
import { validMarkets } from '../transport/utils'
1012

13+
const getAllMarketOptions = (): string[] => {
14+
const allMarkets = [...validMarkets, ...marketAliases]
15+
return [...allMarkets, ...allMarkets.map((market) => market.toLowerCase())]
16+
}
17+
1118
const inputParameters = new InputParameters({
1219
market: {
1320
aliases: [],
1421
type: 'string',
1522
description: 'The name of the market',
16-
options: [...validMarkets, ...marketAliases],
23+
options: getAllMarketOptions(),
1724
required: true,
1825
},
1926
})
@@ -28,4 +35,11 @@ export const marketStatusEndpoint = new MarketStatusEndpoint({
2835
name: 'market-status',
2936
transport,
3037
inputParameters,
38+
requestTransforms: [
39+
(req: AdapterRequest<TypeFromDefinition<typeof inputParameters.definition>>) => {
40+
const data = req.requestContext.data
41+
data.market = data.market.toUpperCase()
42+
return req
43+
},
44+
],
3145
})

packages/sources/finnhub/src/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,11 @@ const adapter = new PriceAdapter({
2727

2828
const server = (): Promise<ServerInstance | undefined> => expose(adapter)
2929

30-
export { adapter, buildQuoteEndpoint, config, rateLimiting, server }
30+
export {
31+
adapter,
32+
buildQuoteEndpoint,
33+
config,
34+
marketStatus as marketStatusEndpoint,
35+
rateLimiting,
36+
server,
37+
}

0 commit comments

Comments
 (0)