Skip to content

Commit df9431c

Browse files
committed
Add optional weekend into market status
1 parent 91c62a2 commit df9431c

4 files changed

Lines changed: 317 additions & 3 deletions

File tree

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
"pino-pretty": "13.1.2",
1616
"prom-client": "15.1.3",
1717
"redlock": "5.0.0-beta.2",
18-
"ws": "8.18.3"
18+
"ws": "8.18.3",
19+
"@date-fns/tz": "1.4.1"
1920
},
2021
"scripts": {
2122
"build": "rm -rf dist/src && mkdir -p ./dist/src && cp package.json dist/src && cp README.md dist/src && tsc && yarn pre-build-generator",

src/adapter/market-status.ts

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { TransportGenerics } from '../transports'
2+
import { AdapterInputError } from '../validation/error'
23
import { AdapterEndpoint } from './endpoint'
3-
4+
import { AdapterEndpointParams } from './types'
5+
import { TZDate } from '@date-fns/tz'
46
/**
57
* Base input parameter config that any [[MarketStatusEndpoint]] must extend
68
*/
@@ -17,6 +19,11 @@ export const marketStatusEndpointInputParametersDefinition = {
1719
options: ['regular', '24/5'],
1820
default: 'regular',
1921
},
22+
weekend: {
23+
type: 'string',
24+
description:
25+
'DHH-DHH:TZ, 520-020:America/New_York means Fri 20:00 to Sun 20:00 Eastern Time Zone',
26+
},
2027
} as const
2128

2229
export enum MarketStatus {
@@ -58,4 +65,77 @@ export type MarketStatusEndpointGenerics = TransportGenerics & {
5865
*/
5966
export class MarketStatusEndpoint<
6067
T extends MarketStatusEndpointGenerics,
61-
> extends AdapterEndpoint<T> {}
68+
> extends AdapterEndpoint<T> {
69+
constructor(params: AdapterEndpointParams<T>) {
70+
params.customInputValidation = (req, _adapterSettings) => {
71+
const data = req.requestContext.data as Record<string, string>
72+
if (data['type'] === '24/5') {
73+
validateWeekend(data['weekend'])
74+
}
75+
return undefined
76+
}
77+
super(params)
78+
}
79+
}
80+
81+
export const validateWeekend = (weekend?: string) => {
82+
const dayHour = /[0-6](0\d|1\d|2[0-3])/
83+
const timezonePattern = /[^\s]+/
84+
const regex = new RegExp(`^(${dayHour.source})-(${dayHour.source}):(${timezonePattern.source})$`)
85+
86+
const match = weekend?.match(regex)
87+
if (!match) {
88+
throw new AdapterInputError({
89+
statusCode: 400,
90+
message: '[Param: weekend] does not match format of DHH-DHH:TZ',
91+
})
92+
}
93+
94+
try {
95+
// eslint-disable-next-line new-cap
96+
Intl.DateTimeFormat(undefined, { timeZone: weekend?.split(':')[1] })
97+
} catch (error) {
98+
throw new AdapterInputError({
99+
statusCode: 400,
100+
message: `[Param: weekend] is not valid: ${error}`,
101+
})
102+
}
103+
}
104+
105+
export const isWeekend = (weekend?: string) => {
106+
validateWeekend(weekend)
107+
108+
// Weekend looks like 520-020:America/New_York
109+
const [range, tz] = (weekend || '').split(':')
110+
const [start, end] = range.split('-')
111+
112+
const startDay = Number(start[0])
113+
const startHour = Number(start.slice(1))
114+
const endDay = Number(end[0])
115+
const endHour = Number(end.slice(1))
116+
117+
const nowDay = TZDate.tz(tz).getDay()
118+
const nowHour = TZDate.tz(tz).getHours()
119+
120+
// Case 1: weekend does NOT wrap around the week
121+
if (startDay < endDay || (startDay === endDay && startHour < endHour)) {
122+
if (nowDay < startDay || nowDay > endDay) {
123+
return false
124+
} else if (nowDay === startDay && nowHour < startHour) {
125+
return false
126+
} else if (nowDay === endDay && nowHour >= endHour) {
127+
return false
128+
}
129+
return true
130+
}
131+
132+
// Case 2: weekend wraps around (e.g. Fri → Sun)
133+
if (nowDay > startDay || nowDay < endDay) {
134+
return true
135+
} else if (nowDay === startDay && nowHour >= startHour) {
136+
return true
137+
} else if (nowDay === endDay && nowHour < endHour) {
138+
return true
139+
}
140+
return false
141+
}

test/adapter/market-status.test.ts

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
import test from 'ava'
2+
import '../../src/adapter'
3+
import {
4+
validateWeekend,
5+
isWeekend,
6+
MarketStatusEndpoint,
7+
marketStatusEndpointInputParametersDefinition,
8+
MarketStatusEndpointGenerics,
9+
} from '../../src/adapter/market-status'
10+
import { InputParameters } from '../../src/validation'
11+
import { TestAdapter } from '../../src/util/testing-utils'
12+
import { Adapter } from '../../src/adapter/basic'
13+
import FakeTimers from '@sinonjs/fake-timers'
14+
import { Transport } from '../../src/transports'
15+
import { ResponseCache } from '../../src/cache/response'
16+
17+
test('MarketStatusEndpoint - validates weekend when type is 24/5', async (t) => {
18+
class MarketStatusTestTransport implements Transport<MarketStatusEndpointGenerics> {
19+
name!: string
20+
responseCache!: ResponseCache<MarketStatusEndpointGenerics>
21+
22+
async initialize() {}
23+
24+
async foregroundExecute() {
25+
return {
26+
data: {
27+
result: 2,
28+
statusString: 'OPEN',
29+
},
30+
result: 2,
31+
statusCode: 200,
32+
timestamps: {
33+
providerDataRequestedUnixMs: 0,
34+
providerDataReceivedUnixMs: 0,
35+
providerIndicatedTimeUnixMs: 0,
36+
},
37+
}
38+
}
39+
}
40+
41+
const adapter = new Adapter({
42+
name: 'TEST',
43+
endpoints: [
44+
new MarketStatusEndpoint({
45+
name: 'test',
46+
inputParameters: new InputParameters(marketStatusEndpointInputParametersDefinition),
47+
transport: new MarketStatusTestTransport(),
48+
}),
49+
],
50+
})
51+
52+
const testAdapter = await TestAdapter.start(
53+
adapter,
54+
{} as {
55+
testAdapter: TestAdapter
56+
},
57+
)
58+
59+
const response1 = await testAdapter.request({
60+
market: 'BTC',
61+
type: 'regular',
62+
weekend: '520-020',
63+
endpoint: 'test',
64+
})
65+
t.is(response1.statusCode, 200, 'Should succeed with invalid weekend when type is regular')
66+
67+
const response2 = await testAdapter.request({
68+
market: 'BTC',
69+
type: '24/5',
70+
weekend: '520-020',
71+
endpoint: 'test',
72+
})
73+
t.is(response2.statusCode, 400, 'Should fail with invalid weekend format')
74+
t.true(
75+
response2.json().error.message.includes('[Param: weekend] does not match format'),
76+
'Error message should mention weekend format',
77+
)
78+
79+
const response3 = await testAdapter.request({
80+
market: 'BTC',
81+
type: '24/5',
82+
weekend: '520-020:America/New_York',
83+
endpoint: 'test',
84+
})
85+
t.is(response3.statusCode, 200, 'Should succeed with valid weekend')
86+
87+
await testAdapter.api.close()
88+
})
89+
90+
test('validateWeekend - success', (t) => {
91+
t.notThrows(() => {
92+
validateWeekend('520-020:America/New_York')
93+
validateWeekend('000-123:UTC')
94+
validateWeekend('123-423:Europe/London')
95+
validateWeekend('600-023:Asia/Tokyo')
96+
})
97+
})
98+
99+
test('validateWeekend - bad format', (t) => {
100+
t.throws(() => {
101+
validateWeekend('520020:America/New_York')
102+
})
103+
t.throws(() => {
104+
validateWeekend('520-020America/New_York')
105+
})
106+
t.throws(() => {
107+
validateWeekend('520-020:')
108+
})
109+
t.throws(() => {
110+
validateWeekend('55-020:UTC')
111+
})
112+
t.throws(() => {
113+
validateWeekend('55-20:UTC')
114+
})
115+
t.throws(() => {
116+
validateWeekend('')
117+
})
118+
t.throws(() => {
119+
validateWeekend()
120+
})
121+
t.throws(() => {
122+
validateWeekend('520:020-America/New_York')
123+
})
124+
t.throws(() => {
125+
validateWeekend('520-020: ')
126+
})
127+
})
128+
129+
test('validateWeekend - bad number', (t) => {
130+
t.throws(() => {
131+
validateWeekend('720-020:UTC')
132+
})
133+
t.throws(() => {
134+
validateWeekend('524-020:UTC')
135+
})
136+
t.throws(() => {
137+
validateWeekend('525-020:UTC')
138+
})
139+
})
140+
141+
test('validateWeekend - invalid timezone', (t) => {
142+
t.throws(() => {
143+
validateWeekend('520-020:Invalid/Timezone')
144+
validateWeekend('520-020:AmericaNew_York')
145+
})
146+
})
147+
148+
const clock = FakeTimers.install({ toFake: ['Date'] })
149+
150+
test.after(() => {
151+
clock.uninstall()
152+
})
153+
154+
test('isWeekend - UTC', (t) => {
155+
// Saturday 12:00 -> 612
156+
clock.setSystemTime(new Date('2024-01-06T12:00:00Z').getTime())
157+
158+
t.false(isWeekend('000-123:UTC'), 'Before start day')
159+
t.false(isWeekend('400-500:UTC'), 'After end day')
160+
t.false(isWeekend('613-620:UTC'), 'Before start hour')
161+
t.false(isWeekend('610-612:UTC'), 'After end hour')
162+
163+
t.true(isWeekend('610-620:UTC'), 'Non-wrapping: middle of weekend should return true')
164+
t.true(isWeekend('600-023:UTC'), 'Non-wrapping: spanning multiple days should return true')
165+
t.true(isWeekend('612-615:UTC'), 'Non-wrapping: same day, at start hour should return true')
166+
167+
t.true(isWeekend('520-020:UTC'), 'Wrapping: nowDay > startDay should return true')
168+
t.true(isWeekend('400-200:UTC'), 'Wrapping: nowDay > startDay should return true')
169+
170+
t.true(isWeekend('612-020:UTC'), 'After start hour')
171+
t.true(isWeekend('520-613:UTC'), 'Before end hour')
172+
173+
t.false(isWeekend('620-610:UTC'), 'Wrapping same day: between end and start should return false')
174+
})
175+
176+
test('isWeekend - ET', (t) => {
177+
// Saturday 12:00 UTC = Saturday 07:00 EST -> 607
178+
clock.setSystemTime(new Date('2024-01-06T12:00:00Z').getTime())
179+
180+
t.false(isWeekend('000-123:America/New_York'), 'Before start day')
181+
t.false(isWeekend('400-500:America/New_York'), 'After end day')
182+
t.false(isWeekend('608-620:America/New_York'), 'Before start hour')
183+
t.false(isWeekend('605-607:America/New_York'), 'After end hour')
184+
185+
t.true(isWeekend('605-620:America/New_York'), 'Non-wrapping: middle of weekend')
186+
t.true(isWeekend('600-023:America/New_York'), 'Non-wrapping: spanning multiple days')
187+
t.true(isWeekend('607-610:America/New_York'), 'Non-wrapping: same day, at start hour ')
188+
189+
t.true(isWeekend('520-020:America/New_York'), 'Wrapping: nowDay > startDay should return true')
190+
t.true(isWeekend('400-200:America/New_York'), 'Wrapping: nowDay > startDay should return true')
191+
192+
t.true(isWeekend('607-020:America/New_York'), 'After start hour')
193+
t.true(isWeekend('520-608:America/New_York'), 'Before end hour')
194+
195+
t.false(isWeekend('620-607:America/New_York'), 'Wrapping same day: at end hour should')
196+
})
197+
198+
test('isWeekend - ET - Fri to Sun 8 to 8', (t) => {
199+
// Weekend: Fri 20:00 to Sun 20:00 ET (520-020:America/New_York)
200+
const range = '520-020:America/New_York'
201+
// Thu 21:00 ET
202+
clock.setSystemTime(new Date('2024-01-05T02:00:00Z').getTime())
203+
t.false(isWeekend(range), 'Before start day')
204+
// Fri 19:00 ET
205+
clock.setSystemTime(new Date('2024-01-06T00:00:00Z').getTime())
206+
t.false(isWeekend(range), 'On start day, before start hour')
207+
// Fri 20:00 ET
208+
clock.setSystemTime(new Date('2024-01-06T01:00:00Z').getTime())
209+
t.true(isWeekend(range), 'On start day, at start hour')
210+
// Fri 23:00 ET
211+
clock.setSystemTime(new Date('2024-01-06T04:00:00Z').getTime())
212+
t.true(isWeekend(range), 'On start day, after start hour')
213+
// Sat 12:00 ET
214+
clock.setSystemTime(new Date('2024-01-06T17:00:00Z').getTime())
215+
t.true(isWeekend(range), 'Middle day (Saturday)')
216+
// Sun 19:00 ET
217+
clock.setSystemTime(new Date('2024-01-08T00:00:00Z').getTime())
218+
t.true(isWeekend(range), 'On end day, before end hour')
219+
// Sun 20:00 ET
220+
clock.setSystemTime(new Date('2024-01-08T01:00:00Z').getTime())
221+
t.false(isWeekend(range), 'On end day, at end hour')
222+
// Sun 21:00 ET
223+
clock.setSystemTime(new Date('2024-01-08T02:00:00Z').getTime())
224+
t.false(isWeekend(range), 'On end day, after end hour')
225+
// Mon 10:00 ET
226+
clock.setSystemTime(new Date('2024-01-08T15:00:00Z').getTime())
227+
t.false(isWeekend(range), 'After end day')
228+
})

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@
1414
dependencies:
1515
"@jridgewell/trace-mapping" "0.3.9"
1616

17+
"@date-fns/tz@1.4.1":
18+
version "1.4.1"
19+
resolved "https://registry.yarnpkg.com/@date-fns/tz/-/tz-1.4.1.tgz#2d905f282304630e07bef6d02d2e7dbf3f0cc4e4"
20+
integrity sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==
21+
1722
"@eslint-community/eslint-utils@^4.7.0":
1823
version "4.7.0"
1924
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz#607084630c6c033992a082de6e6fbc1a8b52175a"

0 commit comments

Comments
 (0)