Skip to content

Commit 725470a

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

6 files changed

Lines changed: 319 additions & 2 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: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { TransportGenerics } from '../transports'
22
import { AdapterEndpoint } from './endpoint'
3+
import { AdapterEndpointParams } from './types'
4+
import { validateWeekend } from '../validation/market-status'
35

46
/**
57
* Base input parameter config that any [[MarketStatusEndpoint]] must extend
@@ -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,15 @@ 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+
}

src/validation/market-status.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { AdapterInputError } from '../validation/error'
2+
import { TZDate } from '@date-fns/tz'
3+
4+
export const validateWeekend = (weekend?: string) => {
5+
const dayHour = /[0-6](0\d|1\d|2[0-3])/
6+
const timezonePattern = /[^\s]+/
7+
const regex = new RegExp(`^(${dayHour.source})-(${dayHour.source}):(${timezonePattern.source})$`)
8+
9+
const match = weekend?.match(regex)
10+
if (!match) {
11+
throw new AdapterInputError({
12+
statusCode: 400,
13+
message: '[Param: weekend] does not match format of DHH-DHH:TZ',
14+
})
15+
}
16+
17+
try {
18+
// eslint-disable-next-line new-cap
19+
Intl.DateTimeFormat(undefined, { timeZone: weekend?.split(':')[1] })
20+
} catch (error) {
21+
throw new AdapterInputError({
22+
statusCode: 400,
23+
message: `[Param: weekend] is not valid: ${error}`,
24+
})
25+
}
26+
}
27+
28+
export const isWeekend = (weekend?: string) => {
29+
validateWeekend(weekend)
30+
31+
// Weekend looks like 520-020:America/New_York
32+
const [range, tz] = (weekend || '').split(':')
33+
const [start, end] = range.split('-')
34+
35+
const startDay = Number(start[0])
36+
const startHour = Number(start.slice(1))
37+
const endDay = Number(end[0])
38+
const endHour = Number(end.slice(1))
39+
40+
const nowDay = TZDate.tz(tz).getDay()
41+
const nowHour = TZDate.tz(tz).getHours()
42+
43+
// Case 1: weekend does NOT wrap around the week
44+
if (startDay < endDay || (startDay === endDay && startHour < endHour)) {
45+
if (nowDay < startDay || nowDay > endDay) {
46+
return false
47+
} else if (nowDay === startDay && nowHour < startHour) {
48+
return false
49+
} else if (nowDay === endDay && nowHour >= endHour) {
50+
return false
51+
}
52+
return true
53+
}
54+
55+
// Case 2: weekend wraps around (e.g. Fri → Sun)
56+
if (nowDay > startDay || nowDay < endDay) {
57+
return true
58+
} else if (nowDay === startDay && nowHour >= startHour) {
59+
return true
60+
} else if (nowDay === endDay && nowHour < endHour) {
61+
return true
62+
}
63+
return false
64+
}

test/adapter/market-status.test.ts

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

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)