@exodus/market-history 10.2.3 → 10.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/module/constants.js +10 -0
- package/module/crop-history.js +13 -0
- package/module/fetch-and-process-tickers.js +61 -0
- package/module/fetch-historical-prices.js +88 -0
- package/module/find-invalid-price.js +20 -0
- package/module/get-limit-to-fetch.js +42 -0
- package/module/index.js +48 -33
- package/module/last-timestamp-from-prices-map.js +4 -0
- package/module/prepare-fetch-instructions.js +48 -0
- package/module/process-api-response.js +56 -0
- package/module/try-to-load-cache.js +41 -0
- package/module/utils.js +5 -0
- package/package.json +2 -3
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,18 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [10.3.0](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/market-history@10.2.4...@exodus/market-history@10.3.0) (2025-07-01)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
- feat: merge price-api with market-history (#12962)
|
|
11
|
+
|
|
12
|
+
## [10.2.4](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/market-history@10.2.3...@exodus/market-history@10.2.4) (2025-06-17)
|
|
13
|
+
|
|
14
|
+
### Performance
|
|
15
|
+
|
|
16
|
+
- perf(market-history): Optimize price transformation logic (#12932)
|
|
17
|
+
|
|
6
18
|
## [10.2.3](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/market-history@10.2.2...@exodus/market-history@10.2.3) (2025-06-16)
|
|
7
19
|
|
|
8
20
|
### Performance
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const HOURLY_LIMIT = 24 * 7
|
|
2
|
+
const DAILY_LIMIT = 3000
|
|
3
|
+
const MINUTE_LIMIT = 60 * 2 // we have only 2 hours of minute level data cached
|
|
4
|
+
|
|
5
|
+
export const LIMIT = {
|
|
6
|
+
minute: MINUTE_LIMIT,
|
|
7
|
+
hour: HOURLY_LIMIT,
|
|
8
|
+
day: DAILY_LIMIT,
|
|
9
|
+
}
|
|
10
|
+
export const START_TIME = new Date('2015-12-09').getTime()
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import ms from 'ms'
|
|
2
|
+
import lastTimestampFromPricesMap from './last-timestamp-from-prices-map.js'
|
|
3
|
+
|
|
4
|
+
const hourMs = ms('1h')
|
|
5
|
+
|
|
6
|
+
const cropHistory = ({ history, hourlyLimit }) => {
|
|
7
|
+
const lastCachedTime = lastTimestampFromPricesMap(history)
|
|
8
|
+
if (!lastCachedTime) return history
|
|
9
|
+
const limitedTime = lastCachedTime - hourMs * hourlyLimit
|
|
10
|
+
return new Map([...history].filter(([key]) => key > limitedTime))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default cropHistory
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import processApiResponse from './process-api-response.js'
|
|
2
|
+
|
|
3
|
+
export default async function fetchAndProcessTickers({
|
|
4
|
+
assetTickersToFetch,
|
|
5
|
+
api,
|
|
6
|
+
fiatTicker,
|
|
7
|
+
granularity,
|
|
8
|
+
timestamp,
|
|
9
|
+
ignoreInvalidSymbols,
|
|
10
|
+
historicalPricesMap,
|
|
11
|
+
fetchedPricesMap,
|
|
12
|
+
specificTimestamp,
|
|
13
|
+
requestLimit,
|
|
14
|
+
}) {
|
|
15
|
+
if (assetTickersToFetch.length === 0) {
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const maxLimit = Math.max(...assetTickersToFetch.map(({ limit }) => limit))
|
|
20
|
+
|
|
21
|
+
const requests = [
|
|
22
|
+
assetTickersToFetch.filter(({ limit }) => limit !== maxLimit),
|
|
23
|
+
assetTickersToFetch.filter(({ limit }) => limit === maxLimit),
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
await Promise.all(
|
|
27
|
+
requests
|
|
28
|
+
.filter((tickers) => tickers.length > 0)
|
|
29
|
+
.map(async (currentTickersToFetch) => {
|
|
30
|
+
const limit = Math.max(...currentTickersToFetch.map(({ limit }) => limit))
|
|
31
|
+
const requestParams = Object.create(null)
|
|
32
|
+
requestParams.assets = currentTickersToFetch.map((item) => item.tickerSymbol)
|
|
33
|
+
requestParams.fiatArray = [fiatTicker]
|
|
34
|
+
requestParams.granularity = granularity
|
|
35
|
+
requestParams.limit = limit
|
|
36
|
+
requestParams.timestamp = timestamp
|
|
37
|
+
requestParams.ignoreInvalidSymbols = ignoreInvalidSymbols
|
|
38
|
+
|
|
39
|
+
const fetchedPrices = await api(requestParams)
|
|
40
|
+
|
|
41
|
+
Object.keys(fetchedPrices).forEach((assetTicker) => {
|
|
42
|
+
const newHistory = processApiResponse({
|
|
43
|
+
assetTicker,
|
|
44
|
+
fiatTicker,
|
|
45
|
+
fetchedPrices,
|
|
46
|
+
assetTickersToFetch,
|
|
47
|
+
ignoreInvalidSymbols,
|
|
48
|
+
historicalPricesMap,
|
|
49
|
+
granularity,
|
|
50
|
+
specificTimestamp,
|
|
51
|
+
requestLimit,
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
if (newHistory) {
|
|
55
|
+
fetchedPricesMap.set(assetTicker, newHistory)
|
|
56
|
+
historicalPricesMap.set(assetTicker, newHistory)
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
)
|
|
61
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import dayjs from '@exodus/dayjs'
|
|
2
|
+
import prepareFetchInstructions from './prepare-fetch-instructions.js'
|
|
3
|
+
import fetchAndProcessTickers from './fetch-and-process-tickers.js'
|
|
4
|
+
|
|
5
|
+
const runtimeCacheDefault = new Map()
|
|
6
|
+
const getRuntimeCacheKeyDefault = ({ fiatTicker, assetTicker, granularity }) =>
|
|
7
|
+
`${assetTicker}_${fiatTicker}_${granularity}`
|
|
8
|
+
|
|
9
|
+
export default async function fetchHistoricalPrices({
|
|
10
|
+
api,
|
|
11
|
+
assetTickers,
|
|
12
|
+
fiatTicker,
|
|
13
|
+
granularity = 'day',
|
|
14
|
+
getCacheFromStorage,
|
|
15
|
+
requestLimit,
|
|
16
|
+
timestamp,
|
|
17
|
+
getCurrentTime = () => Date.now(),
|
|
18
|
+
ignoreInvalidSymbols = false,
|
|
19
|
+
ignoreCache = false,
|
|
20
|
+
runtimeCache = runtimeCacheDefault,
|
|
21
|
+
getRuntimeCacheKey = getRuntimeCacheKeyDefault,
|
|
22
|
+
}) {
|
|
23
|
+
const specificTimestamp = timestamp ? dayjs(timestamp).utc().startOf(granularity).valueOf() : null
|
|
24
|
+
|
|
25
|
+
const requestTimestamp =
|
|
26
|
+
specificTimestamp ||
|
|
27
|
+
dayjs.utc(getCurrentTime()).subtract(1, granularity).startOf(granularity).valueOf()
|
|
28
|
+
|
|
29
|
+
const historicalPricesMap = new Map()
|
|
30
|
+
const fetchedPricesMap = new Map()
|
|
31
|
+
|
|
32
|
+
const tickerSymbols = Array.isArray(assetTickers) ? assetTickers : [assetTickers]
|
|
33
|
+
|
|
34
|
+
const instructions = await Promise.all(
|
|
35
|
+
tickerSymbols.map((tickerSymbol) =>
|
|
36
|
+
prepareFetchInstructions({
|
|
37
|
+
tickerSymbol,
|
|
38
|
+
fiatTicker,
|
|
39
|
+
granularity,
|
|
40
|
+
getCacheFromStorage,
|
|
41
|
+
requestLimit,
|
|
42
|
+
specificTimestamp,
|
|
43
|
+
ignoreCache,
|
|
44
|
+
runtimeCache,
|
|
45
|
+
getRuntimeCacheKey,
|
|
46
|
+
requestTimestamp,
|
|
47
|
+
})
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
const assetTickersToFetch = []
|
|
52
|
+
instructions.forEach(({ history, limit, lastCachedItem, tickerSymbol }) => {
|
|
53
|
+
historicalPricesMap.set(tickerSymbol, history)
|
|
54
|
+
if (limit) {
|
|
55
|
+
assetTickersToFetch.push({ tickerSymbol, limit, lastCachedItem })
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
await fetchAndProcessTickers({
|
|
60
|
+
assetTickersToFetch,
|
|
61
|
+
api,
|
|
62
|
+
fiatTicker,
|
|
63
|
+
granularity,
|
|
64
|
+
timestamp,
|
|
65
|
+
ignoreInvalidSymbols,
|
|
66
|
+
historicalPricesMap,
|
|
67
|
+
fetchedPricesMap,
|
|
68
|
+
specificTimestamp,
|
|
69
|
+
requestLimit,
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
for (const tickerSymbol of tickerSymbols) {
|
|
73
|
+
const runtimeCacheKey = getRuntimeCacheKey({
|
|
74
|
+
fiatTicker,
|
|
75
|
+
assetTicker: tickerSymbol,
|
|
76
|
+
granularity,
|
|
77
|
+
})
|
|
78
|
+
const history = historicalPricesMap.get(tickerSymbol)
|
|
79
|
+
if (history && history.size > 0) {
|
|
80
|
+
runtimeCache.set(runtimeCacheKey, [...history])
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
historicalPricesMap,
|
|
86
|
+
fetchedPricesMap,
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const isValid = (value) => value !== 0 && Number.isFinite(value)
|
|
2
|
+
|
|
3
|
+
const findInvalidPrice = (data, lastCachedItem) =>
|
|
4
|
+
data.find((item, index) => {
|
|
5
|
+
if (!Number.isFinite(item.close) || !Number.isFinite(item.open)) return true
|
|
6
|
+
|
|
7
|
+
if (lastCachedItem && isValid(lastCachedItem.close) && index === 0) {
|
|
8
|
+
// check first item has value. It must, because we have cache for previous period.
|
|
9
|
+
return !isValid(item.close)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (index > 0) {
|
|
13
|
+
const prevItem = data[index - 1]
|
|
14
|
+
// check if price is zero but previous wasn't
|
|
15
|
+
if (!isValid(item.close) && isValid(prevItem.close)) return true
|
|
16
|
+
if (!isValid(item.open) && isValid(prevItem.open)) return true
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
export default findInvalidPrice
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { timeToDays, timeToHours, timeToMinutes } from './utils.js'
|
|
2
|
+
import { LIMIT, START_TIME } from './constants.js'
|
|
3
|
+
|
|
4
|
+
import lastTimestampFromPricesMap from './last-timestamp-from-prices-map.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {import('./types.d.ts').GranularityType} GranularityType
|
|
8
|
+
* @typedef {import('./types.d.ts').HistoryType} HistoryType
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {object} options
|
|
13
|
+
* @param {GranularityType} options.granularity
|
|
14
|
+
* @param {HistoryType} options.history
|
|
15
|
+
* @param {number} options.requestTimestamp
|
|
16
|
+
* @param {number} [options.requestLimit]
|
|
17
|
+
* @param {number} [options.specificTimestamp]
|
|
18
|
+
* @returns {number}
|
|
19
|
+
*/
|
|
20
|
+
const getLimit = ({ granularity, history, requestLimit, requestTimestamp, specificTimestamp }) => {
|
|
21
|
+
if (specificTimestamp) return 1
|
|
22
|
+
// lastCachedTime is always UTC start of hour or day
|
|
23
|
+
const lastCachedTime = lastTimestampFromPricesMap(history)
|
|
24
|
+
|
|
25
|
+
const limitFromOptions = requestLimit || LIMIT[granularity]
|
|
26
|
+
|
|
27
|
+
if (granularity === 'minute') {
|
|
28
|
+
// don't fetch only if it's still same UTC minute
|
|
29
|
+
if (!lastCachedTime) return limitFromOptions
|
|
30
|
+
return Math.ceil(Math.min(limitFromOptions, timeToMinutes(requestTimestamp - lastCachedTime)))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (granularity === 'hour') {
|
|
34
|
+
// don't fetch only if it's still same UTC hour
|
|
35
|
+
if (!lastCachedTime) return limitFromOptions
|
|
36
|
+
return Math.min(limitFromOptions, timeToHours(requestTimestamp - lastCachedTime))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return Math.min(limitFromOptions, timeToDays(requestTimestamp - (lastCachedTime || START_TIME)))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default getLimit
|
package/module/index.js
CHANGED
|
@@ -1,25 +1,16 @@
|
|
|
1
|
-
import { mapValues } from '@exodus/basic-utils'
|
|
2
1
|
import dayjs from '@exodus/dayjs'
|
|
3
|
-
import { fetchHistoricalPrices } from '@exodus/price-api'
|
|
4
2
|
import delay from 'delay'
|
|
5
3
|
import makeConcurrent, { byArguments as makeConcurrentByArguments } from 'make-concurrent'
|
|
6
4
|
import ms from 'ms'
|
|
7
5
|
import { createInMemoryAtom, difference } from '@exodus/atoms'
|
|
8
6
|
import { getAssetFromTicker, parseGranularity } from '../utils.js'
|
|
7
|
+
import fetchHistoricalPrices from './fetch-historical-prices.js'
|
|
9
8
|
|
|
10
9
|
const CLEAR_MARKET_HISTORY_CACHE_KEY = 'clear-market-history-cache'
|
|
11
10
|
const CLEAR_MARKET_HISTORY_CACHE_FROM_REMOTE_CONFIG_KEY =
|
|
12
11
|
'clear-market-history-cache-from-remote-config'
|
|
13
12
|
const MARKET_HISTORY_REFRESH_KEY = 'market-history-cache-refresh'
|
|
14
13
|
|
|
15
|
-
const transformPriceEntries = (entries = []) => {
|
|
16
|
-
const closePrices = entries.map((entry) => [entry[0], entry[1].close])
|
|
17
|
-
return Object.fromEntries(closePrices)
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const transformPricesByAssetName = (pricesByAssetName) =>
|
|
21
|
-
mapValues(pricesByAssetName, (entries) => transformPriceEntries(entries))
|
|
22
|
-
|
|
23
14
|
// when provided cache version from local or remote config is different from stored on device we clear the cache and fetch new prices
|
|
24
15
|
const _invalidateStorageCache = async ({
|
|
25
16
|
storage,
|
|
@@ -58,9 +49,37 @@ const getOldestTimestampFromCache = (cache) =>
|
|
|
58
49
|
cache.length === 0 ? null : Math.min(...cache.map((item) => item[0]))
|
|
59
50
|
|
|
60
51
|
const filterMapAssetDataFromServer = (data) =>
|
|
61
|
-
data
|
|
62
|
-
|
|
63
|
-
|
|
52
|
+
data.filter((item) => item.close !== 0).map((item) => [item.time * 1000, { close: item.close }])
|
|
53
|
+
|
|
54
|
+
const mapPricesForCache = (pricesMap, assetNames, assets) => {
|
|
55
|
+
const result = Object.create(null)
|
|
56
|
+
for (const assetName of assetNames) {
|
|
57
|
+
const assetPricesMap = pricesMap.get(assets[assetName].ticker)
|
|
58
|
+
result[assetName] = assetPricesMap ? [...assetPricesMap] : []
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return result
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const transformHistoricalPrices = (pricesMap, assetNames, assets) => {
|
|
65
|
+
const result = Object.create(null)
|
|
66
|
+
for (const assetName of assetNames) {
|
|
67
|
+
const assetPrices = pricesMap.get(assets[assetName].ticker)
|
|
68
|
+
if (assetPrices) {
|
|
69
|
+
const closePrices = Object.create(null)
|
|
70
|
+
// assetPrices is a Map, iterate it directly to avoid creating an intermediate array
|
|
71
|
+
for (const [time, priceData] of assetPrices.entries()) {
|
|
72
|
+
closePrices[time] = priceData.close
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
result[assetName] = closePrices
|
|
76
|
+
} else {
|
|
77
|
+
result[assetName] = Object.create(null)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return result
|
|
82
|
+
}
|
|
64
83
|
|
|
65
84
|
const delayWithJitter = (ms, jitter = 0, signal) =>
|
|
66
85
|
delay(Math.floor(ms + Math.random() * jitter), { signal })
|
|
@@ -229,29 +248,23 @@ class MarketHistoryMonitor {
|
|
|
229
248
|
requestLimit: requestLimit || this.#granularityRequestLimits[granularity],
|
|
230
249
|
})
|
|
231
250
|
|
|
232
|
-
const mapPrices = (pricesMap) => {
|
|
233
|
-
return Object.fromEntries(
|
|
234
|
-
assetNames.map((assetName) => {
|
|
235
|
-
const assetPricesMap = pricesMap.get(assets[assetName].ticker)
|
|
236
|
-
const value = assetPricesMap ? [...assetPricesMap] : []
|
|
237
|
-
return [assetName, value]
|
|
238
|
-
})
|
|
239
|
-
)
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const pricesByAssetName = mapPrices(fetchedPricesMap)
|
|
243
|
-
|
|
244
|
-
const fullHistoryByAssetName =
|
|
245
|
-
fetchedPricesMap.size === historicalPricesMap.size
|
|
246
|
-
? pricesByAssetName
|
|
247
|
-
: mapPrices(historicalPricesMap)
|
|
248
|
-
|
|
249
251
|
if (fetchedPricesMap.size > 0) {
|
|
250
|
-
|
|
252
|
+
const pricesByAssetNameForCache = mapPricesForCache(fetchedPricesMap, assetNames, assets)
|
|
253
|
+
await this.#setCache({
|
|
254
|
+
currency,
|
|
255
|
+
granularity,
|
|
256
|
+
pricesByAssetName: pricesByAssetNameForCache,
|
|
257
|
+
})
|
|
251
258
|
}
|
|
252
259
|
|
|
260
|
+
const fullHistoryByAssetName = transformHistoricalPrices(
|
|
261
|
+
historicalPricesMap,
|
|
262
|
+
assetNames,
|
|
263
|
+
assets
|
|
264
|
+
)
|
|
265
|
+
|
|
253
266
|
return {
|
|
254
|
-
prices:
|
|
267
|
+
prices: fullHistoryByAssetName,
|
|
255
268
|
hasNewPrices: fetchedPricesMap.size > 0,
|
|
256
269
|
}
|
|
257
270
|
}
|
|
@@ -631,7 +644,9 @@ class MarketHistoryMonitor {
|
|
|
631
644
|
})
|
|
632
645
|
|
|
633
646
|
await this.#marketHistoryAtom.set(async (current) => {
|
|
634
|
-
const transformedPrices =
|
|
647
|
+
const transformedPrices = Object.fromEntries(
|
|
648
|
+
[...updatedAssetHistoryMap].map(([time, priceData]) => [time, priceData.close])
|
|
649
|
+
)
|
|
635
650
|
const parsedGranularity = parseGranularity(granularity)
|
|
636
651
|
|
|
637
652
|
return {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import getLimit from './get-limit-to-fetch.js'
|
|
2
|
+
import tryToGetCache from './try-to-load-cache.js'
|
|
3
|
+
import lastTimestampFromPricesMap from './last-timestamp-from-prices-map.js'
|
|
4
|
+
|
|
5
|
+
export default async function prepareFetchInstructions({
|
|
6
|
+
tickerSymbol,
|
|
7
|
+
fiatTicker,
|
|
8
|
+
granularity,
|
|
9
|
+
getCacheFromStorage,
|
|
10
|
+
requestLimit,
|
|
11
|
+
specificTimestamp,
|
|
12
|
+
ignoreCache,
|
|
13
|
+
runtimeCache,
|
|
14
|
+
getRuntimeCacheKey,
|
|
15
|
+
requestTimestamp,
|
|
16
|
+
}) {
|
|
17
|
+
const runtimeCacheKey = getRuntimeCacheKey({
|
|
18
|
+
fiatTicker,
|
|
19
|
+
assetTicker: tickerSymbol,
|
|
20
|
+
granularity,
|
|
21
|
+
})
|
|
22
|
+
const history = await tryToGetCache({
|
|
23
|
+
runtimeCacheKey,
|
|
24
|
+
runtimeCache,
|
|
25
|
+
getCacheFromStorage: () => getCacheFromStorage(tickerSymbol),
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
if (specificTimestamp && history.get(specificTimestamp)) {
|
|
29
|
+
return { history }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const limit = getLimit({
|
|
33
|
+
granularity,
|
|
34
|
+
history: ignoreCache ? new Map() : history,
|
|
35
|
+
requestLimit,
|
|
36
|
+
requestTimestamp,
|
|
37
|
+
specificTimestamp,
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
if (limit <= 0) {
|
|
41
|
+
return { history }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const lastCachedTime = lastTimestampFromPricesMap(history)
|
|
45
|
+
const lastCachedItem = history.get(lastCachedTime)
|
|
46
|
+
|
|
47
|
+
return { history, limit, lastCachedItem, tickerSymbol }
|
|
48
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import findInvalidPrice from './find-invalid-price.js'
|
|
2
|
+
import cropHistory from './crop-history.js'
|
|
3
|
+
|
|
4
|
+
export default function processApiResponse({
|
|
5
|
+
assetTicker,
|
|
6
|
+
fiatTicker,
|
|
7
|
+
fetchedPrices,
|
|
8
|
+
assetTickersToFetch,
|
|
9
|
+
ignoreInvalidSymbols,
|
|
10
|
+
historicalPricesMap,
|
|
11
|
+
granularity,
|
|
12
|
+
specificTimestamp,
|
|
13
|
+
requestLimit,
|
|
14
|
+
}) {
|
|
15
|
+
if (assetTicker === 'requestErrors') {
|
|
16
|
+
const errors = fetchedPrices[assetTicker]
|
|
17
|
+
Object.keys(errors).forEach((key) => {
|
|
18
|
+
if (key !== 'invalidCryptoSymbols' || !ignoreInvalidSymbols) {
|
|
19
|
+
console.warn(`pricing-server: ${key}: `, errors[key])
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
return null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const newData = fetchedPrices[assetTicker][fiatTicker]
|
|
26
|
+
const { lastCachedItem } =
|
|
27
|
+
assetTickersToFetch.find(({ tickerSymbol }) => tickerSymbol === assetTicker) ||
|
|
28
|
+
Object.create(null)
|
|
29
|
+
const wrongDataItem = findInvalidPrice(newData, lastCachedItem)
|
|
30
|
+
|
|
31
|
+
if (wrongDataItem) {
|
|
32
|
+
const date = new Date(wrongDataItem.time * 1000)
|
|
33
|
+
console.warn(
|
|
34
|
+
`pricing-server: invalid ${assetTicker} price for ${date.toISOString()}: ${JSON.stringify(
|
|
35
|
+
wrongDataItem
|
|
36
|
+
)}`
|
|
37
|
+
)
|
|
38
|
+
return null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const filteredData = newData
|
|
42
|
+
.filter((piece) => piece.close !== 0)
|
|
43
|
+
.map((piece) => ({ time: piece.time, close: piece.close }))
|
|
44
|
+
|
|
45
|
+
let history = historicalPricesMap.get(assetTicker) || new Map()
|
|
46
|
+
|
|
47
|
+
for (const price of filteredData) {
|
|
48
|
+
history.set(price.time * 1000, { close: price.close })
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (granularity === 'hour' && !specificTimestamp && filteredData.length > 0 && requestLimit) {
|
|
52
|
+
history = cropHistory({ history, hourlyLimit: requestLimit })
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return history
|
|
56
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import('./types.js').RuntimeCacheType} RuntimeCacheType
|
|
3
|
+
* @typedef {import('./types.js').HistoryType} HistoryType
|
|
4
|
+
* @typedef {import('./types.js').GetCacheFromStorageResponseType} GetCacheFromStorageResponseType
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {object} options
|
|
9
|
+
* @param {string} options.runtimeCacheKey
|
|
10
|
+
* @param {RuntimeCacheType} options.runtimeCache
|
|
11
|
+
* @param {() => Promise<GetCacheFromStorageResponseType>} options.getCacheFromStorage
|
|
12
|
+
* @returns {Promise<HistoryType>}
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const tryToGetCache = async ({ runtimeCacheKey, runtimeCache, getCacheFromStorage }) => {
|
|
16
|
+
const cache = runtimeCache.get(runtimeCacheKey)
|
|
17
|
+
|
|
18
|
+
if (cache) {
|
|
19
|
+
return new Map(cache)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!getCacheFromStorage) {
|
|
23
|
+
return new Map()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
let cacheFromStorage = await getCacheFromStorage()
|
|
28
|
+
|
|
29
|
+
if (typeof cacheFromStorage === 'string') {
|
|
30
|
+
// backwards compatibility
|
|
31
|
+
const fixedCache = JSON.parse(cacheFromStorage)
|
|
32
|
+
cacheFromStorage = fixedCache
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return new Map(cacheFromStorage)
|
|
36
|
+
} catch (e) {
|
|
37
|
+
throw new Error('failed to load cache from storage. ' + e)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default tryToGetCache
|
package/module/utils.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/market-history",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.3.0",
|
|
4
4
|
"description": "Fetches historical prices for assets",
|
|
5
5
|
"author": "Exodus Movement, Inc.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,7 +27,6 @@
|
|
|
27
27
|
"@exodus/atoms": "^9.0.0",
|
|
28
28
|
"@exodus/basic-utils": "^3.0.1",
|
|
29
29
|
"@exodus/dayjs": "^1.0.2",
|
|
30
|
-
"@exodus/price-api": "^4.1.1",
|
|
31
30
|
"@exodus/remote-config-atoms": "^1.1.0",
|
|
32
31
|
"delay": "^5.0.0",
|
|
33
32
|
"make-concurrent": "^5.4.0",
|
|
@@ -59,5 +58,5 @@
|
|
|
59
58
|
"publishConfig": {
|
|
60
59
|
"access": "public"
|
|
61
60
|
},
|
|
62
|
-
"gitHead": "
|
|
61
|
+
"gitHead": "891258ec0890e3af85055d95b59be88d69b79de6"
|
|
63
62
|
}
|