@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.
- package/README.md +39 -0
- package/index.js +171 -0
- package/package.json +30 -0
- 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
|
+
}
|