@alexbosworth/fiat 2.0.0 → 3.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 (105) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +93 -1
  3. package/arrays/index.js +3 -0
  4. package/arrays/sort_by.js +44 -0
  5. package/chain/add_days.js +22 -0
  6. package/chain/blocks/block_header_hash.js +29 -0
  7. package/chain/blocks/block_header_time.js +25 -0
  8. package/chain/blocks/find_complete_day.js +90 -0
  9. package/chain/blocks/find_height_for_time.js +93 -0
  10. package/chain/blocks/get_block_hash.js +60 -0
  11. package/chain/blocks/get_block_header.js +45 -0
  12. package/chain/blocks/get_block_payments.js +100 -0
  13. package/chain/blocks/get_blocks_for_date.js +110 -0
  14. package/chain/blocks/get_median_time.js +92 -0
  15. package/chain/blocks/index.js +19 -0
  16. package/chain/blocks/payment_outputs_from_block.js +178 -0
  17. package/chain/blocks/sample_heights.js +33 -0
  18. package/chain/blocks/window_heights.js +34 -0
  19. package/chain/chain_rates.js +129 -0
  20. package/chain/constants.json +26 -0
  21. package/chain/estimates/check_known_estimates.js +119 -0
  22. package/chain/estimates/current_estimates.js +66 -0
  23. package/chain/estimates/estimate_and_remember.js +199 -0
  24. package/chain/estimates/estimate_day_price.js +361 -0
  25. package/chain/estimates/estimate_record.js +107 -0
  26. package/chain/estimates/estimate_windows.js +246 -0
  27. package/chain/estimates/find_starting_day.js +86 -0
  28. package/chain/estimates/forget_estimates.js +113 -0
  29. package/chain/estimates/get_chain_price.js +167 -0
  30. package/chain/estimates/get_price_for_date.js +607 -0
  31. package/chain/estimates/get_price_from_blocks.js +281 -0
  32. package/chain/estimates/get_provisional_price.js +163 -0
  33. package/chain/estimates/index.js +25 -0
  34. package/chain/estimates/is_alias_ratio.js +38 -0
  35. package/chain/estimates/is_supported_estimate.js +81 -0
  36. package/chain/estimates/nearest_estimate.js +49 -0
  37. package/chain/estimates/price_window.js +25 -0
  38. package/chain/estimates/remember_estimate.js +83 -0
  39. package/chain/estimates/store_estimate.js +198 -0
  40. package/chain/histogram/best_position.js +63 -0
  41. package/chain/histogram/histogram_excess.js +50 -0
  42. package/chain/histogram/histogram_peak.js +87 -0
  43. package/chain/histogram/implied_prices_histogram.js +53 -0
  44. package/chain/histogram/index.js +33 -0
  45. package/chain/histogram/is_round_tokens.js +26 -0
  46. package/chain/histogram/kernel_width.js +25 -0
  47. package/chain/histogram/lower_bound.js +28 -0
  48. package/chain/histogram/noise_of_neighbors.js +28 -0
  49. package/chain/histogram/noise_strength.js +35 -0
  50. package/chain/histogram/noise_strengths.js +29 -0
  51. package/chain/histogram/outputs_histogram.js +80 -0
  52. package/chain/histogram/parabola_peak.js +28 -0
  53. package/chain/histogram/payments_near_position.js +31 -0
  54. package/chain/histogram/peak_width.js +37 -0
  55. package/chain/histogram/position_score.js +38 -0
  56. package/chain/histogram/score_position.js +94 -0
  57. package/chain/histogram/smooth_bins.js +29 -0
  58. package/chain/histogram/sorted_neighbors.js +89 -0
  59. package/chain/histogram/spike_strengths.js +33 -0
  60. package/chain/histogram/template_agreement.js +27 -0
  61. package/chain/index.js +3 -0
  62. package/chain/known/earliest_supported_day.js +12 -0
  63. package/chain/known/index.js +11 -0
  64. package/chain/known/known_prices.js +43 -0
  65. package/chain/known/known_prices.json +1836 -0
  66. package/chain/known/method_version.js +26 -0
  67. package/chain/known/parse_known_prices.js +67 -0
  68. package/chain/outputs/alias_shifts.js +31 -0
  69. package/chain/outputs/cap_near_round_values.js +83 -0
  70. package/chain/outputs/cap_repeated_values.js +33 -0
  71. package/chain/outputs/dollar_bin_for_price.js +19 -0
  72. package/chain/outputs/estimate_from_outputs.js +321 -0
  73. package/chain/outputs/index.js +11 -0
  74. package/chain/outputs/peak_around_price.js +86 -0
  75. package/chain/outputs/price_for_dollar_bin.js +21 -0
  76. package/chain/outputs/price_from_outputs.js +98 -0
  77. package/chain/outputs/refine_price.js +73 -0
  78. package/chain/outputs/round_amounts.js +49 -0
  79. package/chain/outputs/template.json +22 -0
  80. package/coinbase/constants.json +5 -0
  81. package/coinbase/get_coinbase_day_candles.js +155 -0
  82. package/coinbase/get_coinbase_historic_price.js +135 -0
  83. package/coinbase/index.js +7 -1
  84. package/dates/constants.json +3 -0
  85. package/dates/day_of_date.js +26 -0
  86. package/dates/index.js +5 -0
  87. package/dates/is_calendar_day.js +16 -0
  88. package/dates/time_of_date.js +61 -0
  89. package/docs/METHODOLOGY.md +984 -0
  90. package/index.js +2 -1
  91. package/package.json +29 -8
  92. package/rates/get_prices.js +51 -8
  93. package/storage/constants.json +4 -0
  94. package/storage/get_prices_file.js +169 -0
  95. package/storage/index.js +4 -0
  96. package/storage/is_prices.js +31 -0
  97. package/storage/put_prices_file.js +252 -0
  98. package/storage/temp_path.js +22 -0
  99. package/.gitattributes +0 -1
  100. package/.tm_properties +0 -1
  101. package/test/coinbase/test_get_coinbase_current_price.js +0 -77
  102. package/test/coindesk/test_get_coindesk_current_price.js +0 -95
  103. package/test/coingecko/test_get_coingecko_rates.js +0 -93
  104. package/test/rates/test_get_current_price.js +0 -74
  105. package/test/rates/test_get_prices.js +0 -78
@@ -0,0 +1,21 @@
1
+ const {tokensPerBigUnit} = require('./../constants');
2
+
3
+ const {pow} = Math;
4
+
5
+ /** Get the USD price at which one dollar lands in a histogram bin
6
+
7
+ {
8
+ bin: <Fractional Bin Where One Dollar Lands Number>
9
+ bins_per_decade: <Bins Per Factor of Ten Number>
10
+ }
11
+
12
+ @returns
13
+ {
14
+ price: <USD Price Number>
15
+ }
16
+ */
17
+ module.exports = args => {
18
+ const decades = args.bin / args.bins_per_decade;
19
+
20
+ return {price: tokensPerBigUnit / pow(10, decades)};
21
+ };
@@ -0,0 +1,98 @@
1
+ const {defaultMaxPrice} = require('./../constants');
2
+ const {defaultMinPrice} = require('./../constants');
3
+ const estimateFromOutputs = require('./estimate_from_outputs');
4
+ const {isRoundTokens} = require('./../histogram');
5
+
6
+ const {abs} = Math;
7
+ const {isArray} = Array;
8
+ const isTokens = n => Number.isSafeInteger(n) && n > 0;
9
+
10
+ /** Estimate the USD price of BTC from the values of payment outputs
11
+
12
+ {
13
+ [is_capping_near_round]: <Cap Values Just Off Round BTC Amounts Too Bool>
14
+ [max_price]: <Maximum Possible USD Price Number>
15
+ [min_price]: <Minimum Possible USD Price Number>
16
+ outputs: [<Output Tokens Number>]
17
+ }
18
+
19
+ @throws
20
+ <Error>
21
+
22
+ @returns
23
+ {
24
+ agreement: <Spike Strengths Agreement With Prior Weights Number>
25
+ cents: <USD Cents Per BTC Number>
26
+ confidence: <Best Position Score Over Best Patterned Alias Score Number>
27
+ coverage: <Share of Spike Excess At Round USD Positions Number>
28
+ dispersion: <Peak Width Beyond Smoothing As Price Fraction Number>
29
+ evidence: <Share of Prior Weight Observable At the Price Number>
30
+ is_at_edge: <Best Position Is At the Edge of the Price Window Bool>
31
+ [is_unsettled]: <Refinement Was Still Moving After Its Last Pass Bool>
32
+ noise: <Largest Payments Count Away From the Peak Number>
33
+ payments: <Estimated Round USD Payments Count Number>
34
+ rivalry: <Best Patterned Rival Position Score Over Best Score Number>
35
+ rough_cents: <Rough Estimate USD Cents Per BTC Number>
36
+ round_shift: <Price Change If Round BTC Values Were Counted Number>
37
+ significance: <Payment Peak Beyond Noise In Standard Deviations Number>
38
+ spikes: [{
39
+ strength: <Spike Strength Relative to the Strongest Spike Number>
40
+ usd: <Round USD Amount Number>
41
+ }]
42
+ stability: <Largest Price Change From Perturbed Refinement Number>
43
+ }
44
+ */
45
+ module.exports = args => {
46
+ if (!isArray(args.outputs) || !args.outputs.length) {
47
+ throw new Error('ExpectedOutputTokenValuesToDerivePriceFromOutputs');
48
+ }
49
+
50
+ const maxPrice = args.max_price || defaultMaxPrice;
51
+ const minPrice = args.min_price || defaultMinPrice;
52
+
53
+ if (!(minPrice > 0) || !(maxPrice > minPrice)) {
54
+ throw new Error('ExpectedValidPriceRangeToDerivePriceFromOutputs');
55
+ }
56
+
57
+ const outputs = args.outputs.filter(isTokens);
58
+
59
+ if (!outputs.some(tokens => !isRoundTokens({tokens}).is_round)) {
60
+ throw new Error('ExpectedNonRoundOutputValuesToDerivePriceFromOutputs');
61
+ }
62
+
63
+ // Round USD payments line up at the price, see docs/METHODOLOGY.md
64
+ const estimate = estimateFromOutputs({
65
+ outputs,
66
+ is_capping_near_round: args.is_capping_near_round,
67
+ max_price: maxPrice,
68
+ min_price: minPrice,
69
+ });
70
+
71
+ // Counting round BTC values in full shows what the correction changes
72
+ const counted = estimateFromOutputs({
73
+ outputs,
74
+ is_counting_round: true,
75
+ is_quick: true,
76
+ max_price: maxPrice,
77
+ min_price: minPrice,
78
+ });
79
+
80
+ return {
81
+ agreement: estimate.agreement,
82
+ cents: estimate.cents,
83
+ confidence: estimate.confidence,
84
+ coverage: estimate.coverage,
85
+ dispersion: estimate.dispersion,
86
+ evidence: estimate.evidence,
87
+ is_at_edge: estimate.is_at_edge,
88
+ is_unsettled: estimate.is_unsettled,
89
+ noise: estimate.noise,
90
+ payments: estimate.payments,
91
+ rivalry: estimate.rivalry,
92
+ rough_cents: estimate.rough_cents,
93
+ round_shift: abs(counted.cents / estimate.cents - 1),
94
+ significance: estimate.significance,
95
+ spikes: estimate.spikes,
96
+ stability: estimate.stability || Number(),
97
+ };
98
+ };
@@ -0,0 +1,73 @@
1
+ const peakAroundPrice = require('./peak_around_price');
2
+
3
+ const {abs} = Math;
4
+ const defaultReach = 37;
5
+ const maxIterations = 5;
6
+ const settledBins = 0.5;
7
+
8
+ /** Refine a rough price to the center of the peak of round payments
9
+
10
+ {
11
+ amounts: [<Round USD Amount Number>]
12
+ outputs: [<Output Tokens Number>]
13
+ price: <Rough USD Price Number>
14
+ [reach]: <Fine Bins On Either Side of the Price to Consider Number>
15
+ weights: [<Round USD Amount Weight Number>]
16
+ }
17
+
18
+ @returns
19
+ {
20
+ dispersion: <Peak Width Beyond Smoothing As Price Fraction Number>
21
+ is_at_edge: <Peak Is At the Edge of the Refinement Range Bool>
22
+ [is_unsettled]: <Peak Was Still Moving After the Last Pass Bool>
23
+ iterations: <Recentering Passes Count Number>
24
+ noise: <Largest Payments Count Away From the Peak Number>
25
+ payments: <Estimated Round Payments In the Peak Number>
26
+ price: <Refined USD Price Number>
27
+ significance: <Peak Payments Beyond Noise In Standard Deviations Number>
28
+ spread: <Smoothed Peak Width At Half Height As a Fraction of Price Number>
29
+ }
30
+ */
31
+ module.exports = ({amounts, outputs, price, reach, weights}) => {
32
+ const reachBins = reach || defaultReach;
33
+
34
+ // Recenter on the peak until it settles
35
+ const passes = [...Array(maxIterations).keys()].reduce(passes => {
36
+ const [latest] = passes;
37
+
38
+ const isSettled = !!latest && abs(latest.offset) < settledBins;
39
+
40
+ // Exit early when the peak has settled or cannot be followed
41
+ if (isSettled || (!!latest && latest.is_at_edge)) {
42
+ return passes;
43
+ }
44
+
45
+ const pass = peakAroundPrice({
46
+ amounts,
47
+ outputs,
48
+ weights,
49
+ price: !latest ? price : latest.price,
50
+ reach: reachBins,
51
+ });
52
+
53
+ return [pass].concat(passes);
54
+ },
55
+ []);
56
+
57
+ const [final] = passes;
58
+
59
+ // A peak still moving after the last pass did not settle
60
+ const isUnsettled = !final.is_at_edge && abs(final.offset) >= settledBins;
61
+
62
+ return {
63
+ dispersion: final.dispersion,
64
+ is_at_edge: final.is_at_edge,
65
+ is_unsettled: isUnsettled || undefined,
66
+ iterations: passes.length,
67
+ noise: final.noise,
68
+ payments: final.payments,
69
+ price: final.price,
70
+ significance: final.significance,
71
+ spread: final.spread,
72
+ };
73
+ };
@@ -0,0 +1,49 @@
1
+ const {half} = require('./../constants');
2
+ const {sortBy} = require('./../../arrays');
3
+ const template = require('./template');
4
+
5
+ const {exp} = Math;
6
+ const flatten = arr => [].concat(...arr);
7
+ const {log10} = Math;
8
+ const {pow} = Math;
9
+ const range = (a, b) => [...Array(b - a).keys()].map(i => a + i);
10
+
11
+ /** Get the round USD amounts people pay and the prior weight of each
12
+
13
+ {}
14
+
15
+ @returns
16
+ {
17
+ amounts: [<Round USD Amount Number>]
18
+ weights: [<Prior Weight Number>]
19
+ }
20
+ */
21
+ module.exports = ({}) => {
22
+ const {decades, envelope, mantissas} = template;
23
+
24
+ // Each weight is how common the digits are, times an envelope over decades
25
+ const center = log10(envelope.center_usd);
26
+ const width = envelope.width_decades;
27
+
28
+ const amounts = flatten(range(decades.min, decades.max + 1).map(decade => {
29
+ return mantissas.map(mantissa => ({
30
+ usd: mantissa.value * pow(10, decade),
31
+ weight: mantissa.weight,
32
+ }));
33
+ }));
34
+
35
+ const rounds = sortBy({
36
+ array: amounts.filter(amount => amount.usd <= template.max_usd),
37
+ attribute: 'usd',
38
+ })
39
+ .sorted;
40
+
41
+ return {
42
+ amounts: rounds.map(amount => amount.usd),
43
+ weights: rounds.map(amount => {
44
+ const distance = (log10(amount.usd) - center) / width;
45
+
46
+ return amount.weight * exp(-half * distance * distance);
47
+ }),
48
+ };
49
+ };
@@ -0,0 +1,22 @@
1
+ {
2
+ "decades": {
3
+ "max": 5,
4
+ "min": 0
5
+ },
6
+ "envelope": {
7
+ "center_usd": 100,
8
+ "width_decades": 1
9
+ },
10
+ "mantissas": [
11
+ {"value": 1, "weight": 1},
12
+ {"value": 1.5, "weight": 0.2},
13
+ {"value": 2, "weight": 0.6},
14
+ {"value": 2.5, "weight": 0.25},
15
+ {"value": 3, "weight": 0.2},
16
+ {"value": 4, "weight": 0.12},
17
+ {"value": 5, "weight": 0.75},
18
+ {"value": 6, "weight": 0.1},
19
+ {"value": 7.5, "weight": 0.1}
20
+ ],
21
+ "max_usd": 100000
22
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "msPerSecond": 1000,
3
+ "secondsPerDay": 86400,
4
+ "supportedFiats": ["EUR", "GBP", "USD"]
5
+ }
@@ -0,0 +1,155 @@
1
+ const asyncAuto = require('async/auto');
2
+ const asyncRetry = require('async/retry');
3
+ const {returnResult} = require('asyncjs-util');
4
+
5
+ const {isCalendarDay} = require('./../dates');
6
+ const {msPerSecond} = require('./constants');
7
+ const {name} = require('./../package');
8
+ const {secondsPerDay} = require('./constants');
9
+ const {sortBy} = require('./../arrays');
10
+ const {supportedFiats} = require('./constants');
11
+ const {version} = require('./../package');
12
+
13
+ const api = 'https://api.exchange.coinbase.com/products/';
14
+ const candleFields = 6;
15
+ const dayStartSeconds = day => Date.parse(`${day}T00:00:00Z`) / msPerSecond;
16
+ const defaultFiat = 'USD';
17
+ const granularitySeconds = 60 * 60;
18
+ const interval = n => (1 + Math.random()) * 1000 * Math.pow(2, n);
19
+ const {isArray} = Array;
20
+ const isNumber = n => typeof n === 'number' && !isNaN(n);
21
+ const isRateLimited = err => err[1] === 'CoinbaseRateLimitedCandlesRequest';
22
+ const lastSecond = day => `${day}T23:59:59.000Z`;
23
+ const rateLimitedCode = 429;
24
+ const remoteServiceTimeoutMs = 1000 * 30;
25
+ const times = 5;
26
+ const userAgent = `${name}/${version} (+https://github.com/alexbosworth/fiat)`;
27
+
28
+ /** Get the hourly BTC candles of a UTC day from Coinbase Exchange
29
+
30
+ {
31
+ date: <Day YYYY-MM-DD String>
32
+ [fiat]: <Fiat Currency Code String> // default USD
33
+ request: <Request Function> {headers, url}, (err, r, body) => {}
34
+ }
35
+
36
+ @returns via cbk or Promise
37
+ {
38
+ candles: [{
39
+ close: <Last Trade Fiat Price Number>
40
+ high: <Highest Trade Fiat Price Number>
41
+ low: <Lowest Trade Fiat Price Number>
42
+ open: <First Trade Fiat Price Number>
43
+ time: <Candle Start Epoch Seconds Number>
44
+ volume: <BTC Traded Number>
45
+ }]
46
+ }
47
+ */
48
+ module.exports = ({date, fiat, request}, cbk) => {
49
+ return new Promise((resolve, reject) => {
50
+ return asyncAuto({
51
+ // Check arguments
52
+ validate: cbk => {
53
+ if (!isCalendarDay({day: date}).is_calendar_day) {
54
+ return cbk([400, 'ExpectedYyyyMmDdDateToGetCoinbaseDayCandles']);
55
+ }
56
+
57
+ if (!!fiat && !supportedFiats.includes(fiat)) {
58
+ return cbk([400, 'UnsupportedFiatToGetCoinbaseDayCandles']);
59
+ }
60
+
61
+ if (!request) {
62
+ return cbk([400, 'ExpectedRequestFunctionToGetCoinbaseDayCandles']);
63
+ }
64
+
65
+ return cbk();
66
+ },
67
+
68
+ // Fetch the candles, retrying when rate limited
69
+ getCandles: ['validate', ({}, cbk) => {
70
+ const product = `BTC-${fiat || defaultFiat}`;
71
+
72
+ const query = [
73
+ `granularity=${granularitySeconds}`,
74
+ `start=${date}T00:00:00.000Z`,
75
+ `end=${lastSecond(date)}`,
76
+ ];
77
+
78
+ const retry = {interval, times, errorFilter: isRateLimited};
79
+
80
+ return asyncRetry(retry, cbk => {
81
+ return request({
82
+ headers: {'user-agent': userAgent},
83
+ json: true,
84
+ timeout: remoteServiceTimeoutMs,
85
+ url: `${api}${product}/candles?${query.join('&')}`,
86
+ },
87
+ (err, r, json) => {
88
+ if (!!err) {
89
+ return cbk([503, 'UnexpectedErrorGettingCoinbaseCandles', {
90
+ err,
91
+ }]);
92
+ }
93
+
94
+ // Exit early to retry when the request was rate limited
95
+ if (!!r && r.statusCode === rateLimitedCode) {
96
+ return cbk([503, 'CoinbaseRateLimitedCandlesRequest']);
97
+ }
98
+
99
+ // Coinbase Exchange explains its own refusals with a message
100
+ if (!!json && !isArray(json) && !!json.message) {
101
+ return cbk([503, 'CoinbaseRefusedCandlesRequest', {
102
+ err: json.message,
103
+ }]);
104
+ }
105
+
106
+ return cbk(null, json);
107
+ });
108
+ },
109
+ cbk);
110
+ }],
111
+
112
+ // Pull out the candles of the day
113
+ candles: ['getCandles', ({getCandles}, cbk) => {
114
+ if (!isArray(getCandles)) {
115
+ return cbk([503, 'ExpectedArrayOfCandlesInCoinbaseResponse']);
116
+ }
117
+
118
+ const isCandle = candle => {
119
+ const isComplete = isArray(candle) && candle.length >= candleFields;
120
+
121
+ return isComplete && candle.every(isNumber);
122
+ };
123
+
124
+ if (!getCandles.every(isCandle)) {
125
+ return cbk([503, 'ExpectedCandleValuesInCoinbaseResponse']);
126
+ }
127
+
128
+ const start = dayStartSeconds(date);
129
+
130
+ // Only candles that start within the day belong to it
131
+ const dayCandles = getCandles
132
+ .filter(([time]) => time >= start && time < start + secondsPerDay)
133
+ .map(([time, low, high, opening, closing, volume]) => ({
134
+ high,
135
+ low,
136
+ time,
137
+ volume,
138
+ close: closing,
139
+ open: opening,
140
+ }));
141
+
142
+ // An hour without trades has no candle
143
+ const candles = sortBy({array: dayCandles, attribute: 'time'}).sorted;
144
+
145
+ // Exit early when there were no trades, as before a market opened
146
+ if (!candles.length) {
147
+ return cbk([503, 'ExpectedCandlesForDayInCoinbaseResponse']);
148
+ }
149
+
150
+ return cbk(null, {candles});
151
+ }],
152
+ },
153
+ returnResult({reject, resolve, of: 'candles'}, cbk));
154
+ });
155
+ };
@@ -0,0 +1,135 @@
1
+ const asyncAuto = require('async/auto');
2
+ const {returnResult} = require('asyncjs-util');
3
+
4
+ const {dayLength} = require('./../dates/constants');
5
+ const getCoinbaseDayCandles = require('./get_coinbase_day_candles');
6
+ const {msPerSecond} = require('./constants');
7
+ const {secondsPerDay} = require('./constants');
8
+ const {sortBy} = require('./../arrays');
9
+ const {supportedFiats} = require('./constants');
10
+ const {timeOfDate} = require('./../dates');
11
+
12
+ const {abs} = Math;
13
+ const centsPerUnit = 100;
14
+ const dayStartSeconds = day => Date.parse(`${day}T00:00:00Z`) / msPerSecond;
15
+ const {floor} = Math;
16
+ const knownDays = new WeakMap();
17
+ const {round} = Math;
18
+ const secondsPerHour = 60 * 60;
19
+ const settleSeconds = 60 * 60;
20
+ const typicalPrice = n => (n.high + n.low + n.close) / 3;
21
+
22
+ /** Get the price of BTC at the hour of a date on Coinbase Exchange
23
+
24
+ {
25
+ date: <ISO 8601 Date String>
26
+ fiat: <Fiat Currency Code String>
27
+ request: <Request Function> {headers, url}, (err, r, body) => {}
28
+ }
29
+
30
+ @returns via cbk or Promise
31
+ {
32
+ cents: <Cents Per BTC Number>
33
+ date: <Start of the Hour Priced At ISO 8601 Date String>
34
+ }
35
+ */
36
+ module.exports = ({date, fiat, request}, cbk) => {
37
+ return new Promise((resolve, reject) => {
38
+ return asyncAuto({
39
+ // Check arguments
40
+ validate: cbk => {
41
+ if (timeOfDate({date}).time === undefined) {
42
+ return cbk([400, 'ExpectedDateToGetCoinbaseHistoricPrice']);
43
+ }
44
+
45
+ if (!supportedFiats.includes(fiat)) {
46
+ return cbk([400, 'UnsupportedFiatToGetCoinbaseHistoricPrice']);
47
+ }
48
+
49
+ if (!request) {
50
+ return cbk([400, 'ExpectedRequestToGetCoinbaseHistoricPrice']);
51
+ }
52
+
53
+ return cbk();
54
+ },
55
+
56
+ // Get the candles of the day of the date, sharing them across lookups
57
+ getCandles: ['validate', ({}, cbk) => {
58
+ // A time without an offset is taken to be in UTC
59
+ const {time} = timeOfDate({date});
60
+
61
+ const day = new Date(time).toISOString().slice(Number(), dayLength);
62
+
63
+ // Days are kept with the request function that they were fetched with
64
+ if (!knownDays.has(request)) {
65
+ knownDays.set(request, new Map());
66
+ }
67
+
68
+ const days = knownDays.get(request);
69
+ const key = `${fiat}:${day}`;
70
+
71
+ const known = days.get(key);
72
+
73
+ // Exit early when the candles of the day are already known
74
+ if (!!known && !!known.candles) {
75
+ return cbk(null, {candles: known.candles});
76
+ }
77
+
78
+ // Exit early when the candles of the day are already being fetched
79
+ if (!!known) {
80
+ known.waiting.push(cbk);
81
+
82
+ return;
83
+ }
84
+
85
+ const waiting = [cbk];
86
+
87
+ days.set(key, {waiting});
88
+
89
+ const dayEnd = dayStartSeconds(day) + secondsPerDay;
90
+
91
+ // The candles of a day are settled once it has been over for an hour
92
+ const isSettled = dayEnd + settleSeconds <= Date.now() / msPerSecond;
93
+
94
+ return getCoinbaseDayCandles({
95
+ fiat,
96
+ request,
97
+ date: day,
98
+ },
99
+ (err, res) => {
100
+ // A failed fetch or a day that is not settled is fetched again later
101
+ if (!!err || !isSettled) {
102
+ days.delete(key);
103
+ } else {
104
+ days.set(key, {candles: res.candles});
105
+ }
106
+
107
+ return waiting.forEach(cbk => !!err ? cbk(err) : cbk(null, res));
108
+ });
109
+ }],
110
+
111
+ // The typical price of the hour of the date, or the nearest with trades
112
+ price: ['getCandles', ({getCandles}, cbk) => {
113
+ const seconds = timeOfDate({date}).time / msPerSecond;
114
+
115
+ const hour = floor(seconds / secondsPerHour) * secondsPerHour;
116
+
117
+ const distances = getCandles.candles.map(candle => {
118
+ return {candle, distance: abs(candle.time - hour)};
119
+ });
120
+
121
+ const [{candle}] = sortBy({
122
+ array: distances,
123
+ attribute: 'distance',
124
+ })
125
+ .sorted;
126
+
127
+ return cbk(null, {
128
+ cents: round(typicalPrice(candle) * centsPerUnit),
129
+ date: new Date(candle.time * msPerSecond).toISOString(),
130
+ });
131
+ }],
132
+ },
133
+ returnResult({reject, resolve, of: 'price'}, cbk));
134
+ });
135
+ };
package/coinbase/index.js CHANGED
@@ -1,3 +1,9 @@
1
1
  const getCoinbaseCurrentPrice = require('./get_coinbase_current_price');
2
+ const getCoinbaseDayCandles = require('./get_coinbase_day_candles');
3
+ const getCoinbaseHistoricPrice = require('./get_coinbase_historic_price');
2
4
 
3
- module.exports = {getCoinbaseCurrentPrice};
5
+ module.exports = {
6
+ getCoinbaseCurrentPrice,
7
+ getCoinbaseDayCandles,
8
+ getCoinbaseHistoricPrice,
9
+ };
@@ -0,0 +1,3 @@
1
+ {
2
+ "dayLength": 10
3
+ }
@@ -0,0 +1,26 @@
1
+ const {dayLength} = require('./constants');
2
+ const timeOfDate = require('./time_of_date');
3
+
4
+ const isoDay = time => new Date(time).toISOString().slice(Number(), dayLength);
5
+
6
+ /** Get the day in UTC of an ISO 8601 date
7
+
8
+ {
9
+ date: <ISO 8601 Date String>
10
+ }
11
+
12
+ @returns
13
+ {
14
+ [day]: <Day In UTC YYYY-MM-DD String>
15
+ }
16
+ */
17
+ module.exports = ({date}) => {
18
+ const {time} = timeOfDate({date});
19
+
20
+ // Exit early when the date cannot be read
21
+ if (time === undefined) {
22
+ return {};
23
+ }
24
+
25
+ return {day: isoDay(time)};
26
+ };
package/dates/index.js ADDED
@@ -0,0 +1,5 @@
1
+ const dayOfDate = require('./day_of_date');
2
+ const isCalendarDay = require('./is_calendar_day');
3
+ const timeOfDate = require('./time_of_date');
4
+
5
+ module.exports = {dayOfDate, isCalendarDay, timeOfDate};
@@ -0,0 +1,16 @@
1
+ const dayOfDate = require('./day_of_date');
2
+
3
+ /** Determine whether a day is a day of the calendar written as YYYY-MM-DD
4
+
5
+ {
6
+ day: <Day YYYY-MM-DD String>
7
+ }
8
+
9
+ @returns
10
+ {
11
+ is_calendar_day: <Day Is a Day of the Calendar Bool>
12
+ }
13
+ */
14
+ module.exports = ({day}) => {
15
+ return {is_calendar_day: !!day && dayOfDate({date: day}).day === day};
16
+ };
@@ -0,0 +1,61 @@
1
+ const {dayLength} = require('./constants');
2
+
3
+ const clockFormat = /^((?:[01]\d|2[0-3]):\d{2}(?::\d{2}(?:\.\d+)?)?)(.*)$/;
4
+ const dateFormat = /^(\d{4}-\d{2}-\d{2})(?:[Tt ](.*))?$/;
5
+ const isoDay = time => new Date(time).toISOString().slice(Number(), dayLength);
6
+ const {isInteger} = Number;
7
+ const offsetFormat = /^(?:[Zz]|[+-]\d{2}:?\d{2})?$/;
8
+
9
+ /** Get the time of an ISO 8601 date
10
+
11
+ {
12
+ date: <ISO 8601 Date String>
13
+ }
14
+
15
+ @returns
16
+ {
17
+ [time]: <Epoch Time Milliseconds Number>
18
+ }
19
+ */
20
+ module.exports = ({date}) => {
21
+ // Exit early when there is no date to read
22
+ if (typeof date !== 'string') {
23
+ return {};
24
+ }
25
+
26
+ const [, day, rest] = dateFormat.exec(date) || [];
27
+
28
+ // Exit early when the date does not start with a day
29
+ if (!day) {
30
+ return {};
31
+ }
32
+
33
+ const start = Date.parse(`${day}T00:00:00.000Z`);
34
+
35
+ // Exit early when the day is not a day of the calendar
36
+ if (!isInteger(start) || isoDay(start) !== day) {
37
+ return {};
38
+ }
39
+
40
+ // Exit early when the date is only a day
41
+ if (rest === undefined) {
42
+ return {time: start};
43
+ }
44
+
45
+ const [, clock, offset] = clockFormat.exec(rest) || [];
46
+
47
+ // Exit early when the time of day or its offset is not written as one
48
+ if (!clock || !offsetFormat.test(offset)) {
49
+ return {};
50
+ }
51
+
52
+ // A time without an offset is in UTC, so it is the same on every machine
53
+ const time = Date.parse(`${day}T${clock}${(offset || 'Z').toUpperCase()}`);
54
+
55
+ // Exit early when the time of day or the offset is not a real one
56
+ if (!isInteger(time)) {
57
+ return {};
58
+ }
59
+
60
+ return {time};
61
+ };