@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.
- package/CHANGELOG.md +22 -0
- package/README.md +93 -1
- package/arrays/index.js +3 -0
- package/arrays/sort_by.js +44 -0
- package/chain/add_days.js +22 -0
- package/chain/blocks/block_header_hash.js +29 -0
- package/chain/blocks/block_header_time.js +25 -0
- package/chain/blocks/find_complete_day.js +90 -0
- package/chain/blocks/find_height_for_time.js +93 -0
- package/chain/blocks/get_block_hash.js +60 -0
- package/chain/blocks/get_block_header.js +45 -0
- package/chain/blocks/get_block_payments.js +100 -0
- package/chain/blocks/get_blocks_for_date.js +110 -0
- package/chain/blocks/get_median_time.js +92 -0
- package/chain/blocks/index.js +19 -0
- package/chain/blocks/payment_outputs_from_block.js +178 -0
- package/chain/blocks/sample_heights.js +33 -0
- package/chain/blocks/window_heights.js +34 -0
- package/chain/chain_rates.js +129 -0
- package/chain/constants.json +26 -0
- package/chain/estimates/check_known_estimates.js +119 -0
- package/chain/estimates/current_estimates.js +66 -0
- package/chain/estimates/estimate_and_remember.js +199 -0
- package/chain/estimates/estimate_day_price.js +361 -0
- package/chain/estimates/estimate_record.js +107 -0
- package/chain/estimates/estimate_windows.js +246 -0
- package/chain/estimates/find_starting_day.js +86 -0
- package/chain/estimates/forget_estimates.js +113 -0
- package/chain/estimates/get_chain_price.js +167 -0
- package/chain/estimates/get_price_for_date.js +607 -0
- package/chain/estimates/get_price_from_blocks.js +281 -0
- package/chain/estimates/get_provisional_price.js +163 -0
- package/chain/estimates/index.js +25 -0
- package/chain/estimates/is_alias_ratio.js +38 -0
- package/chain/estimates/is_supported_estimate.js +81 -0
- package/chain/estimates/nearest_estimate.js +49 -0
- package/chain/estimates/price_window.js +25 -0
- package/chain/estimates/remember_estimate.js +83 -0
- package/chain/estimates/store_estimate.js +198 -0
- package/chain/histogram/best_position.js +63 -0
- package/chain/histogram/histogram_excess.js +50 -0
- package/chain/histogram/histogram_peak.js +87 -0
- package/chain/histogram/implied_prices_histogram.js +53 -0
- package/chain/histogram/index.js +33 -0
- package/chain/histogram/is_round_tokens.js +26 -0
- package/chain/histogram/kernel_width.js +25 -0
- package/chain/histogram/lower_bound.js +28 -0
- package/chain/histogram/noise_of_neighbors.js +28 -0
- package/chain/histogram/noise_strength.js +35 -0
- package/chain/histogram/noise_strengths.js +29 -0
- package/chain/histogram/outputs_histogram.js +80 -0
- package/chain/histogram/parabola_peak.js +28 -0
- package/chain/histogram/payments_near_position.js +31 -0
- package/chain/histogram/peak_width.js +37 -0
- package/chain/histogram/position_score.js +38 -0
- package/chain/histogram/score_position.js +94 -0
- package/chain/histogram/smooth_bins.js +29 -0
- package/chain/histogram/sorted_neighbors.js +89 -0
- package/chain/histogram/spike_strengths.js +33 -0
- package/chain/histogram/template_agreement.js +27 -0
- package/chain/index.js +3 -0
- package/chain/known/earliest_supported_day.js +12 -0
- package/chain/known/index.js +11 -0
- package/chain/known/known_prices.js +43 -0
- package/chain/known/known_prices.json +1836 -0
- package/chain/known/method_version.js +26 -0
- package/chain/known/parse_known_prices.js +67 -0
- package/chain/outputs/alias_shifts.js +31 -0
- package/chain/outputs/cap_near_round_values.js +83 -0
- package/chain/outputs/cap_repeated_values.js +33 -0
- package/chain/outputs/dollar_bin_for_price.js +19 -0
- package/chain/outputs/estimate_from_outputs.js +321 -0
- package/chain/outputs/index.js +11 -0
- package/chain/outputs/peak_around_price.js +86 -0
- package/chain/outputs/price_for_dollar_bin.js +21 -0
- package/chain/outputs/price_from_outputs.js +98 -0
- package/chain/outputs/refine_price.js +73 -0
- package/chain/outputs/round_amounts.js +49 -0
- package/chain/outputs/template.json +22 -0
- package/coinbase/constants.json +5 -0
- package/coinbase/get_coinbase_day_candles.js +155 -0
- package/coinbase/get_coinbase_historic_price.js +135 -0
- package/coinbase/index.js +7 -1
- package/dates/constants.json +3 -0
- package/dates/day_of_date.js +26 -0
- package/dates/index.js +5 -0
- package/dates/is_calendar_day.js +16 -0
- package/dates/time_of_date.js +61 -0
- package/docs/METHODOLOGY.md +984 -0
- package/index.js +2 -1
- package/package.json +29 -8
- package/rates/get_prices.js +51 -8
- package/storage/constants.json +4 -0
- package/storage/get_prices_file.js +169 -0
- package/storage/index.js +4 -0
- package/storage/is_prices.js +31 -0
- package/storage/put_prices_file.js +252 -0
- package/storage/temp_path.js +22 -0
- package/.gitattributes +0 -1
- package/.tm_properties +0 -1
- package/test/coinbase/test_get_coinbase_current_price.js +0 -77
- package/test/coindesk/test_get_coindesk_current_price.js +0 -95
- package/test/coingecko/test_get_coingecko_rates.js +0 -93
- package/test/rates/test_get_current_price.js +0 -74
- 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,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 = {
|
|
5
|
+
module.exports = {
|
|
6
|
+
getCoinbaseCurrentPrice,
|
|
7
|
+
getCoinbaseDayCandles,
|
|
8
|
+
getCoinbaseHistoricPrice,
|
|
9
|
+
};
|
|
@@ -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,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
|
+
};
|