@exodus/market-history 1.0.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.
Files changed (4) hide show
  1. package/README.md +39 -0
  2. package/index.js +171 -0
  3. package/package.json +30 -0
  4. package/utils.js +18 -0
package/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # @exodus/market-history
2
+
3
+ module fetches daily and hourly historical prices for assets by interval
4
+ returns prices on interval `close`.
5
+
6
+ ## Install
7
+
8
+ ```sh
9
+ yarn add @exodus/@exodus/market-history
10
+ ```
11
+
12
+ ## Usage
13
+
14
+ ```js
15
+ import createMarketHistoryMonitor from '@exodus/market-history'
16
+
17
+ const marketHistoryMonitor = createMarketHistoryMonitor({
18
+ assetsModule,
19
+ currencyAtom,
20
+ storage: marketHistoryStorage,
21
+ pricingServer,
22
+ })
23
+
24
+ marketHistoryMonitor.on('market-history', ({ currency, granularity, prices }) => {
25
+ // `currency` USD
26
+ // `granularity` daily|hourly
27
+ // prices
28
+ // {
29
+ // bitcoin: {
30
+ // 1663668000000: 90000,
31
+ // 1663671600000: 120000,
32
+ // 1663675200000: 100000000,
33
+ // }
34
+ // }
35
+ })
36
+
37
+ marketHistoryMonitor.start()
38
+ marketHistoryMonitor.sync() // force to emit prices
39
+ ```
package/index.js ADDED
@@ -0,0 +1,171 @@
1
+ import { mapValues, SynchronizedTime } from '@exodus/basic-utils'
2
+ import ExodusModule from '@exodus/module'
3
+ import { fetchHistoricalPrices, fetchPricesInterval } from '@exodus/price-api'
4
+ import ms from 'ms'
5
+
6
+ import { getAssetFromTicker, getAssetTickers, parseGranularity } from './utils'
7
+
8
+ const MARKET_HISTORY_CACHE_VERSION = 'v1'
9
+
10
+ const transformPriceEntries = (entries = []) => {
11
+ const closePrices = entries.map((entry) => [entry[0], entry[1].close])
12
+ return Object.fromEntries(closePrices)
13
+ }
14
+
15
+ const transformPricesByAssetName = (pricesByAssetName) =>
16
+ mapValues(pricesByAssetName, (entries) => transformPriceEntries(entries))
17
+
18
+ class MarketHistoryMonitor extends ExodusModule {
19
+ #pricingServer
20
+ #currencyAtom
21
+ #previousCurrency = null
22
+
23
+ constructor({ assetsModule, storage, currencyAtom, pricingServer }) {
24
+ super({ name: 'MarketHistoryMonitor' })
25
+
26
+ this.assetsModule = assetsModule
27
+ this.storage = storage
28
+ this.#pricingServer = pricingServer
29
+
30
+ this.#currencyAtom = currencyAtom
31
+ this.#currencyAtom.observe((value) => value && this.#onCurrencyChange(value))
32
+ }
33
+
34
+ #started = false
35
+
36
+ #onCurrencyChange = async (value) => {
37
+ if (this.#previousCurrency === null) {
38
+ this.#previousCurrency = value
39
+ return
40
+ }
41
+
42
+ this.#update('day')
43
+ this.#update('hour')
44
+
45
+ this.#previousCurrency = value
46
+ }
47
+
48
+ #getCacheKey = ({ currency, assetName, granularity }) => {
49
+ return `${currency}-${assetName}-${granularity}-${MARKET_HISTORY_CACHE_VERSION}`
50
+ }
51
+
52
+ #setCache = async ({ currency, granularity, pricesByAssetName }) => {
53
+ const assets = this.assetsModule.getAssets()
54
+
55
+ const promises = Object.values(assets).map((asset) => {
56
+ const key = this.#getCacheKey({ currency, assetName: asset.name, granularity })
57
+ const values = pricesByAssetName[asset.name]
58
+ return this.storage.set(key, values)
59
+ })
60
+
61
+ await Promise.all(promises)
62
+ }
63
+
64
+ #getCache = async ({ currency, granularity, assetName }) => {
65
+ const key = this.#getCacheKey({ currency, assetName, granularity })
66
+ const cache = await this.storage.get(key)
67
+ return cache || []
68
+ }
69
+
70
+ #fetch = async ({ currency, granularity }) => {
71
+ const assets = this.assetsModule.getAssets()
72
+ const assetTickers = getAssetTickers(assets)
73
+
74
+ const pricesMap = await fetchHistoricalPrices({
75
+ api: (...args) => this.#pricingServer.historicalPrice(...args),
76
+ assetTickers,
77
+ fiatTicker: currency,
78
+ granularity,
79
+ ignoreInvalidSymbols: true,
80
+ getCurrentTime: SynchronizedTime.now,
81
+ getCacheFromStorage: async (ticker) =>
82
+ this.#getCache({
83
+ currency,
84
+ granularity,
85
+ assetName: getAssetFromTicker(assets, ticker)?.name,
86
+ }),
87
+ })
88
+
89
+ const pricesByAssetName = mapValues(assets, (asset) => {
90
+ const assetPricesMap = pricesMap.get(asset.ticker)
91
+ return assetPricesMap ? [...assetPricesMap] : []
92
+ })
93
+
94
+ this.#setCache({ currency, granularity, pricesByAssetName })
95
+
96
+ return transformPricesByAssetName(pricesByAssetName)
97
+ }
98
+
99
+ #getCurrency = async () => {
100
+ return this.#currencyAtom.get()
101
+ }
102
+
103
+ #update = async (granularity) => {
104
+ const parsedGranularity = parseGranularity(granularity)
105
+ const currency = await this.#getCurrency()
106
+
107
+ try {
108
+ const prices = await this.#fetch({ currency, granularity })
109
+ this.emit('market-history', { currency, granularity: parsedGranularity, prices })
110
+ } catch (error) {
111
+ console.error(
112
+ 'MarketHistoryMonitor: Failed to fetch rates',
113
+ currency,
114
+ parsedGranularity,
115
+ error
116
+ )
117
+ }
118
+ }
119
+
120
+ #syncGranularity = async (granularity) => {
121
+ const parsedGranularity = parseGranularity(granularity)
122
+ const currency = await this.#getCurrency()
123
+ const assets = this.assetsModule.getAssets()
124
+
125
+ const keys = Object.keys(assets).map((assetName) =>
126
+ this.#getCacheKey({ currency, assetName, granularity })
127
+ )
128
+ const cachedPrices = await this.storage.batchGet(keys)
129
+
130
+ const cachedPricesByAssetName = Object.fromEntries(
131
+ Object.keys(assets).map((assetName, index) => [assetName, cachedPrices[index]])
132
+ )
133
+
134
+ const prices = transformPricesByAssetName(cachedPricesByAssetName)
135
+ this.emit('market-history', { currency, granularity: parsedGranularity, prices })
136
+ }
137
+
138
+ sync = async () => {
139
+ this.#syncGranularity('day')
140
+ this.#syncGranularity('hour')
141
+ }
142
+
143
+ start = async () => {
144
+ if (this.#started) return
145
+
146
+ this.#started = true
147
+
148
+ const jitter = ms('3s')
149
+ const getCurrentTime = SynchronizedTime.now
150
+ const updateDayPrices = this.#update.bind(this, 'day')
151
+ const updateHourPrices = this.#update.bind(this, 'hour')
152
+
153
+ fetchPricesInterval({
154
+ func: updateDayPrices,
155
+ granularity: 'day',
156
+ jitter,
157
+ getCurrentTime,
158
+ })
159
+
160
+ fetchPricesInterval({
161
+ func: updateHourPrices,
162
+ granularity: 'hour',
163
+ jitter,
164
+ getCurrentTime,
165
+ })
166
+ }
167
+ }
168
+
169
+ const createMarketHistoryMonitor = (args = {}) => new MarketHistoryMonitor({ ...args })
170
+
171
+ export default createMarketHistoryMonitor
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@exodus/market-history",
3
+ "version": "1.0.0",
4
+ "description": "module to fetch historical prices for assets",
5
+ "author": "Exodus Movement Inc",
6
+ "license": "UNLICENSED",
7
+ "scripts": {
8
+ "lint": "eslint .",
9
+ "lint:fix": "yarn lint --fix",
10
+ "test": "jest"
11
+ },
12
+ "files": [
13
+ "index.js",
14
+ "utils.js"
15
+ ],
16
+ "dependencies": {
17
+ "@exodus/basic-utils": "^1.0.0",
18
+ "@exodus/module": "^1.0.0",
19
+ "@exodus/price-api": "^2.0.11",
20
+ "ms": "^0.7.1"
21
+ },
22
+ "devDependencies": {
23
+ "@exodus/assets": "^8.0.72",
24
+ "@exodus/atoms": "^1.0.0",
25
+ "@exodus/storage-memory": "^1.0.0"
26
+ },
27
+ "peerDependencies": {
28
+ "debug": ">= 3.0.0"
29
+ }
30
+ }
package/utils.js ADDED
@@ -0,0 +1,18 @@
1
+ export const getAssetTickers = (assets) =>
2
+ Object.values(assets)
3
+ .map((asset) => asset.ticker)
4
+ .sort()
5
+
6
+ export const getAssetFromTicker = (assets, ticker) =>
7
+ Object.values(assets).find((asset) => asset.ticker === ticker)
8
+
9
+ export const parseGranularity = (granularity) => {
10
+ switch (granularity) {
11
+ case 'day':
12
+ return 'daily'
13
+ case 'hour':
14
+ return 'hourly'
15
+ default:
16
+ return granularity
17
+ }
18
+ }