@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,281 @@
1
+ const asyncAuto = require('async/auto');
2
+ const asyncMapLimit = require('async/mapLimit');
3
+ const {returnResult} = require('asyncjs-util');
4
+
5
+ const {centsPerDollar} = require('./../constants');
6
+ const {defaultMaxPrice} = require('./../constants');
7
+ const {defaultMinPrice} = require('./../constants');
8
+ const {estimateFromOutputs} = require('./../outputs');
9
+ const {fetchLimit} = require('./../constants');
10
+ const {getBlockPayments} = require('./../blocks');
11
+ const {minAliasMargin} = require('./../constants');
12
+ const {priceFromOutputs} = require('./../outputs');
13
+
14
+ const {assign} = Object;
15
+ const driftReach = 1.09;
16
+ const flatten = arr => [].concat(...arr);
17
+ const {floor} = Math;
18
+ const {isArray} = Array;
19
+ const isHeight = n => Number.isInteger(n) && n >= 0;
20
+ const logInterval = 24;
21
+ const {max} = Math;
22
+ const {min} = Math;
23
+ const minDriftBlocks = 24;
24
+ const widening = 2;
25
+
26
+ /** Get the USD price of BTC implied by the payments in a set of blocks
27
+
28
+ {
29
+ [blocks]: <Retrieved Blocks By Height Map Object>
30
+ [date]: <Day the Blocks Are On YYYY-MM-DD String>
31
+ heights: [<Block Height Number>]
32
+ [is_capping_near_round]: <Cap Values Just Off Round BTC Amounts Too Bool>
33
+ lnd: <Authenticated LND API Object>
34
+ [logger]: <Winston Logger Object>
35
+ [max_price]: <Maximum Possible USD Price Number>
36
+ [min_price]: <Minimum Possible USD Price Number>
37
+ }
38
+
39
+ @returns via cbk or Promise
40
+ {
41
+ agreement: <Spike Strengths Agreement With Prior Weights Number>
42
+ cents: <USD Cents Per BTC Number>
43
+ confidence: <Best Position Score Over Best Patterned Alias Score Number>
44
+ coverage: <Share of Spike Excess At Round USD Positions Number>
45
+ dispersion: <Peak Width Beyond Smoothing As Price Fraction Number>
46
+ [drift]: <Second Half Price Change From First Half Price Number>
47
+ evidence: <Share of Prior Weight Observable At the Price Number>
48
+ [is_unsettled]: <Refinement Was Still Moving After Its Last Pass Bool>
49
+ [is_widened]: <Search Was Widened Beyond the Price Window Bool>
50
+ outputs: <Payment Outputs Considered Count Number>
51
+ payments: <Estimated Round USD Payments Count Number>
52
+ rivalry: <Best Patterned Rival Position Score Over Best Score Number>
53
+ rough_cents: <Rough Estimate USD Cents Per BTC Number>
54
+ round_shift: <Price Change If Round BTC Values Were Counted Number>
55
+ significance: <Payment Peak Beyond Noise In Standard Deviations Number>
56
+ spikes: [{
57
+ strength: <Spike Strength Relative to the Strongest Spike Number>
58
+ usd: <Round USD Amount Number>
59
+ }]
60
+ stability: <Largest Price Change From Perturbed Refinement Number>
61
+ transactions: <Payment Transactions Considered Count Number>
62
+ }
63
+ */
64
+ module.exports = (args, cbk) => {
65
+ return new Promise((resolve, reject) => {
66
+ return asyncAuto({
67
+ // Check arguments
68
+ validate: cbk => {
69
+ if (!isArray(args.heights) || !args.heights.length) {
70
+ return cbk([400, 'ExpectedBlockHeightsToGetPriceFromBlocks']);
71
+ }
72
+
73
+ if (!args.heights.every(isHeight)) {
74
+ return cbk([400, 'ExpectedBlockHeightNumbersToGetPriceFromBlocks']);
75
+ }
76
+
77
+ if (!args.lnd) {
78
+ return cbk([400, 'ExpectedAuthenticatedLndToGetPriceFromBlocks']);
79
+ }
80
+
81
+ return cbk();
82
+ },
83
+
84
+ // Get the blocks and pull out the payment-like outputs
85
+ getBlocks: ['validate', ({}, cbk) => {
86
+ const heights = args.heights.slice().sort((a, b) => a - b);
87
+ const progress = {fetched: Number()};
88
+
89
+ const total = heights.length;
90
+
91
+ // Log the progress of fetching blocks like a progress bar
92
+ const log = fetched => {
93
+ // Exit early when there is no logger or the fetch is a short one
94
+ if (!args.logger || total < logInterval) {
95
+ return;
96
+ }
97
+
98
+ // Exit early when the progress is between marks
99
+ if (!!(fetched % logInterval) && fetched !== total) {
100
+ return;
101
+ }
102
+
103
+ return args.logger.info({
104
+ fetching_blocks: {date: args.date, blocks: `${fetched}/${total}`},
105
+ });
106
+ };
107
+
108
+ log(progress.fetched);
109
+
110
+ return asyncMapLimit(heights, fetchLimit, (height, cbk) => {
111
+ return getBlockPayments({
112
+ height,
113
+ blocks: args.blocks,
114
+ lnd: args.lnd,
115
+ },
116
+ (err, res) => {
117
+ if (!!err) {
118
+ return cbk(err);
119
+ }
120
+
121
+ log(++progress.fetched);
122
+
123
+ return cbk(null, res);
124
+ });
125
+ },
126
+ cbk);
127
+ }],
128
+
129
+ // Collect the payment outputs of every block
130
+ payments: ['getBlocks', ({getBlocks}, cbk) => {
131
+ const payments = getBlocks.map(block => block.outputs);
132
+
133
+ if (!flatten(payments).length) {
134
+ return cbk([503, 'ExpectedPaymentOutputsInBlocksToGetPrice']);
135
+ }
136
+
137
+ return cbk(null, payments);
138
+ }],
139
+
140
+ // Count the payment transactions
141
+ transactions: ['getBlocks', ({getBlocks}, cbk) => {
142
+ return cbk(null, getBlocks.reduce((sum, block) => {
143
+ return sum + block.transactions;
144
+ },
145
+ Number()));
146
+ }],
147
+
148
+ // Derive the price from the payment output values
149
+ price: ['payments', ({payments}, cbk) => {
150
+ const derived = (() => {
151
+ try {
152
+ return {
153
+ price: priceFromOutputs({
154
+ is_capping_near_round: args.is_capping_near_round,
155
+ max_price: args.max_price,
156
+ min_price: args.min_price,
157
+ outputs: flatten(payments),
158
+ }),
159
+ };
160
+ } catch (err) {
161
+ return {err};
162
+ }
163
+ })();
164
+
165
+ // Exit early when the outputs cannot be priced
166
+ if (!!derived.err) {
167
+ return cbk([503, 'FailedToDerivePriceFromOutputs', {
168
+ err: derived.err,
169
+ }]);
170
+ }
171
+
172
+ return cbk(null, derived.price);
173
+ }],
174
+
175
+ // Widen the search when the price does not fit well in the window
176
+ widen: ['payments', 'price', ({payments, price}, cbk) => {
177
+ // Exit early when there is no window to widen
178
+ if (!args.max_price || !args.min_price) {
179
+ return cbk(null, price);
180
+ }
181
+
182
+ const isConflict = price.confidence < minAliasMargin;
183
+
184
+ // Exit early when the price fits in the window
185
+ if (!price.is_at_edge && !isConflict) {
186
+ return cbk(null, price);
187
+ }
188
+
189
+ const maxPrice = min(defaultMaxPrice, args.max_price * widening);
190
+ const minPrice = max(defaultMinPrice, args.min_price / widening);
191
+
192
+ const widened = (() => {
193
+ try {
194
+ return {
195
+ price: priceFromOutputs({
196
+ is_capping_near_round: args.is_capping_near_round,
197
+ max_price: maxPrice,
198
+ min_price: minPrice,
199
+ outputs: flatten(payments),
200
+ }),
201
+ };
202
+ } catch (err) {
203
+ return {err};
204
+ }
205
+ })();
206
+
207
+ // Exit early when the outputs cannot be priced in the wider window
208
+ if (!!widened.err) {
209
+ return cbk([503, 'FailedToDerivePriceInWidenedWindow', {
210
+ err: widened.err,
211
+ }]);
212
+ }
213
+
214
+ return cbk(null, assign({is_widened: true}, widened.price));
215
+ }],
216
+
217
+ // Estimate the price in the first and second halves of the blocks
218
+ drift: ['payments', 'widen', ({payments, widen}, cbk) => {
219
+ // Exit early when there are too few blocks to split
220
+ if (payments.length < minDriftBlocks) {
221
+ return cbk();
222
+ }
223
+
224
+ const half = floor(payments.length / 2);
225
+ const price = widen.cents / centsPerDollar;
226
+
227
+ const halves = [payments.slice(Number(), half), payments.slice(half)];
228
+
229
+ const drift = (() => {
230
+ try {
231
+ const [first, second] = halves.map(blocks => estimateFromOutputs({
232
+ is_capping_near_round: args.is_capping_near_round,
233
+ is_quick: true,
234
+ max_price: price * driftReach,
235
+ min_price: price / driftReach,
236
+ outputs: flatten(blocks),
237
+ }));
238
+
239
+ return {drift: second.cents / first.cents - 1};
240
+ } catch (err) {
241
+ // A half without enough payments to price does not show a drift
242
+ return {};
243
+ }
244
+ })();
245
+
246
+ return cbk(null, drift);
247
+ }],
248
+
249
+ // The derived price
250
+ result: [
251
+ 'drift',
252
+ 'payments',
253
+ 'transactions',
254
+ 'widen',
255
+ ({drift, payments, transactions, widen}, cbk) =>
256
+ {
257
+ return cbk(null, {
258
+ transactions,
259
+ agreement: widen.agreement,
260
+ cents: widen.cents,
261
+ confidence: widen.confidence,
262
+ coverage: widen.coverage,
263
+ dispersion: widen.dispersion,
264
+ drift: !drift ? undefined : drift.drift,
265
+ evidence: widen.evidence,
266
+ is_unsettled: widen.is_unsettled,
267
+ is_widened: widen.is_widened,
268
+ outputs: flatten(payments).length,
269
+ payments: widen.payments,
270
+ rivalry: widen.rivalry,
271
+ rough_cents: widen.rough_cents,
272
+ round_shift: widen.round_shift,
273
+ significance: widen.significance,
274
+ spikes: widen.spikes,
275
+ stability: widen.stability,
276
+ });
277
+ }],
278
+ },
279
+ returnResult({reject, resolve, of: 'result'}, cbk));
280
+ });
281
+ };
@@ -0,0 +1,163 @@
1
+ const asyncAuto = require('async/auto');
2
+ const {returnResult} = require('asyncjs-util');
3
+
4
+ const estimateRecord = require('./estimate_record');
5
+ const {getBlockHash} = require('./../blocks');
6
+ const getPriceFromBlocks = require('./get_price_from_blocks');
7
+ const isAliasRatio = require('./is_alias_ratio');
8
+ const isSupportedEstimate = require('./is_supported_estimate');
9
+ const nearestEstimate = require('./nearest_estimate');
10
+ const priceWindow = require('./price_window');
11
+ const rememberEstimate = require('./remember_estimate');
12
+ const {unpricedErrors} = require('./../constants');
13
+
14
+ /** Derive and remember a provisional price for an incomplete day
15
+
16
+ {
17
+ day: <Day YYYY-MM-DD String>
18
+ estimates: {
19
+ priors: {<Day YYYY-MM-DD String>: <Estimate Object>}
20
+ provisional: {<Day YYYY-MM-DD String>: <Estimate Object>}
21
+ rates: {<Day YYYY-MM-DD String>: <Estimate Object>}
22
+ }
23
+ heights: [<Block Height Number>]
24
+ lnd: <Authenticated LND API Object>
25
+ [logger]: <Winston Logger Object>
26
+ put_price: <Remember Price Function> (record, cbk) => (err)
27
+ }
28
+
29
+ @returns via cbk or Promise
30
+ {
31
+ cents: <USD Cents Per BTC Number>
32
+ is_provisional: <Price Is Provisional Bool>
33
+ }
34
+ */
35
+ module.exports = (args, cbk) => {
36
+ return new Promise((resolve, reject) => {
37
+ return asyncAuto({
38
+ // The nearest estimate guides the search
39
+ guide: cbk => {
40
+ const {estimate} = nearestEstimate({
41
+ day: args.day,
42
+ estimates: args.estimates,
43
+ });
44
+
45
+ return cbk(null, {estimate});
46
+ },
47
+
48
+ // Derive the price from the blocks
49
+ getPrice: ['guide', ({guide}, cbk) => {
50
+ const {estimate} = guide;
51
+
52
+ const bounds = !estimate ? {} : priceWindow({cents: estimate.cents});
53
+
54
+ return getPriceFromBlocks({
55
+ date: args.day,
56
+ heights: args.heights,
57
+ lnd: args.lnd,
58
+ logger: args.logger,
59
+ max_price: bounds.max_price,
60
+ min_price: bounds.min_price,
61
+ },
62
+ (err, res) => {
63
+ // Exit early when the blocks give no price at all
64
+ if (!!err && unpricedErrors.includes(err[1])) {
65
+ return cbk([503, 'FailedToFindSupportedProvisionalPrice', {
66
+ day: args.day,
67
+ }]);
68
+ }
69
+
70
+ return cbk(err, res);
71
+ });
72
+ }],
73
+
74
+ // Check that the data support the price
75
+ checkPrice: ['getPrice', 'guide', ({getPrice, guide}, cbk) => {
76
+ const {estimate} = guide;
77
+
78
+ const support = isSupportedEstimate({
79
+ confidence: getPrice.confidence,
80
+ evidence: getPrice.evidence,
81
+ is_unconstrained: !estimate || !!getPrice.is_widened,
82
+ payments: getPrice.payments,
83
+ rivalry: getPrice.rivalry,
84
+ significance: getPrice.significance,
85
+ spikes: getPrice.spikes,
86
+ stability: getPrice.stability,
87
+ });
88
+
89
+ if (!support.is_supported) {
90
+ return cbk([503, 'FailedToFindSupportedProvisionalPrice', {
91
+ day: args.day,
92
+ }]);
93
+ }
94
+
95
+ // Exit early when the price was not found beyond a guide window
96
+ if (!getPrice.is_widened || !estimate) {
97
+ return cbk();
98
+ }
99
+
100
+ const ratio = isAliasRatio({
101
+ cents: getPrice.cents,
102
+ other: estimate.cents,
103
+ });
104
+
105
+ // A price at an alias of its guide means one of them is wrong
106
+ if (!!ratio.is_alias) {
107
+ return cbk([503, 'FoundProvisionalPriceAtAliasOfGuidePrice', {
108
+ cents: getPrice.cents,
109
+ day: args.day,
110
+ neighbor: estimate.day,
111
+ neighbor_cents: estimate.cents,
112
+ }]);
113
+ }
114
+
115
+ return cbk();
116
+ }],
117
+
118
+ // Get the hash of the last block
119
+ getHash: ['checkPrice', ({}, cbk) => {
120
+ const [last] = args.heights.slice(-1);
121
+
122
+ return getBlockHash({height: last, lnd: args.lnd}, cbk);
123
+ }],
124
+
125
+ // Remember the provisional price
126
+ remember: ['getHash', 'getPrice', ({getHash, getPrice}, cbk) => {
127
+ const [first] = args.heights;
128
+ const [last] = args.heights.slice(-1);
129
+
130
+ const {record} = estimateRecord({
131
+ date: args.day,
132
+ estimate: {
133
+ agreement: getPrice.agreement,
134
+ blocks: [first, last],
135
+ cents: getPrice.cents,
136
+ confidence: getPrice.confidence,
137
+ coverage: getPrice.coverage,
138
+ dispersion: getPrice.dispersion,
139
+ drift: getPrice.drift,
140
+ evidence: getPrice.evidence,
141
+ hash: getHash.hash,
142
+ is_widened: getPrice.is_widened,
143
+ outputs: getPrice.outputs,
144
+ payments: getPrice.payments,
145
+ rivalry: getPrice.rivalry,
146
+ round_shift: getPrice.round_shift,
147
+ significance: getPrice.significance,
148
+ spikes: getPrice.spikes,
149
+ stability: getPrice.stability,
150
+ },
151
+ is_provisional: true,
152
+ });
153
+
154
+ rememberEstimate({estimate: record, estimates: args.estimates});
155
+
156
+ return args.put_price(record, err => {
157
+ return cbk(err, {cents: getPrice.cents, is_provisional: true});
158
+ });
159
+ }],
160
+ },
161
+ returnResult({reject, resolve, of: 'remember'}, cbk));
162
+ });
163
+ };
@@ -0,0 +1,25 @@
1
+ const currentEstimates = require('./current_estimates');
2
+ const estimateDayPrice = require('./estimate_day_price');
3
+ const estimateRecord = require('./estimate_record');
4
+ const forgetEstimates = require('./forget_estimates');
5
+ const getChainPrice = require('./get_chain_price');
6
+ const getPriceForDate = require('./get_price_for_date');
7
+ const isAliasRatio = require('./is_alias_ratio');
8
+ const isSupportedEstimate = require('./is_supported_estimate');
9
+ const nearestEstimate = require('./nearest_estimate');
10
+ const priceWindow = require('./price_window');
11
+ const rememberEstimate = require('./remember_estimate');
12
+
13
+ module.exports = {
14
+ currentEstimates,
15
+ estimateDayPrice,
16
+ estimateRecord,
17
+ forgetEstimates,
18
+ getChainPrice,
19
+ getPriceForDate,
20
+ isAliasRatio,
21
+ isSupportedEstimate,
22
+ nearestEstimate,
23
+ priceWindow,
24
+ rememberEstimate,
25
+ };
@@ -0,0 +1,38 @@
1
+ const {roundAmounts} = require('./../outputs');
2
+
3
+ const {abs} = Math;
4
+ const flatten = arr => [].concat(...arr);
5
+ const {log10} = Math;
6
+ const {max} = Math;
7
+ const {min} = Math;
8
+ const minAliasRatio = 2;
9
+ const tolerance = Math.log10(1.06);
10
+
11
+ /** Determine if the ratio between two prices is an alias ratio of the pattern
12
+
13
+ {
14
+ cents: <USD Cents Per BTC Number>
15
+ other: <Other USD Cents Per BTC Number>
16
+ }
17
+
18
+ @returns
19
+ {
20
+ is_alias: <Prices Are At An Alias Ratio Bool>
21
+ }
22
+ */
23
+ module.exports = ({cents, other}) => {
24
+ const {amounts} = roundAmounts({});
25
+
26
+ // Ratios of two or more between round amounts are the pattern's aliases
27
+ const ratios = flatten(amounts.map(amount => {
28
+ const larger = amounts.filter(n => n >= amount * minAliasRatio);
29
+
30
+ return larger.map(n => n / amount);
31
+ }));
32
+
33
+ const distance = log10(max(cents, other) / min(cents, other));
34
+
35
+ const isNear = ratio => abs(log10(ratio) - distance) < tolerance;
36
+
37
+ return {is_alias: ratios.some(isNear)};
38
+ };
@@ -0,0 +1,81 @@
1
+ const {minAliasMargin} = require('./../constants');
2
+
3
+ const clearMargin = 0.1;
4
+ const {isArray} = Array;
5
+ const maxRivalry = 0.9;
6
+ const maxStability = 0.01;
7
+ const minEvidence = 0.5;
8
+ const minPayments = 100;
9
+ const minSignificance = 4;
10
+ const minSpikes = 3;
11
+ const minSpikeStrength = 0.2;
12
+
13
+ /** Determine if a price estimate is supported by the data behind it
14
+
15
+ {
16
+ confidence: <Best Position Score Over Best Patterned Alias Score Number>
17
+ evidence: <Share of Prior Weight Observable At the Price Number>
18
+ is_unconstrained: <Search Was Not Confined To a Window Bool>
19
+ payments: <Estimated Round USD Payments Count Number>
20
+ rivalry: <Best Patterned Rival Position Score Over Best Score Number>
21
+ significance: <Payment Peak Beyond Noise In Standard Deviations Number>
22
+ spikes: [{
23
+ strength: <Spike Strength Relative to the Strongest Spike Number>
24
+ usd: <Round USD Amount Number>
25
+ }]
26
+ stability: <Largest Price Change From Perturbed Refinement Number>
27
+ }
28
+
29
+ @returns
30
+ {
31
+ is_supported: <Estimate Is Supported By the Data Bool>
32
+ }
33
+ */
34
+ module.exports = args => {
35
+ const {confidence, evidence, payments, rivalry, significance} = args;
36
+
37
+ // Exit early when there are no spikes to judge
38
+ if (!isArray(args.spikes)) {
39
+ return {is_supported: false};
40
+ }
41
+
42
+ const strong = args.spikes.filter(n => n.strength >= minSpikeStrength);
43
+
44
+ // Exit early when there is no pattern, one spike fits any round amount
45
+ if (strong.length < minSpikes) {
46
+ return {is_supported: false};
47
+ }
48
+
49
+ // Exit early when too few round payments formed the peak
50
+ if (!(payments >= minPayments)) {
51
+ return {is_supported: false};
52
+ }
53
+
54
+ // Exit early when the peak does not stand out beyond noise
55
+ if (!(significance >= minSignificance)) {
56
+ return {is_supported: false};
57
+ }
58
+
59
+ // Exit early when other starting points refine to another price
60
+ if (!(args.stability < maxStability)) {
61
+ return {is_supported: false};
62
+ }
63
+
64
+ // Exit early when another position or currency fits nearly as well
65
+ if (!(rivalry < maxRivalry)) {
66
+ return {is_supported: false};
67
+ }
68
+
69
+ // Exit early when too little of the pattern was observable
70
+ if (!(evidence >= minEvidence)) {
71
+ return {is_supported: false};
72
+ }
73
+
74
+ // Less observable evidence demands a larger margin over the aliases
75
+ const clear = minAliasMargin + clearMargin / evidence;
76
+
77
+ // Without a window around a known price only the prior excludes aliases
78
+ const minConfidence = !!args.is_unconstrained ? clear : minAliasMargin;
79
+
80
+ return {is_supported: confidence >= minConfidence};
81
+ };
@@ -0,0 +1,49 @@
1
+ const {msPerDay} = require('./../constants');
2
+ const {sortBy} = require('./../../arrays');
3
+
4
+ const {abs} = Math;
5
+ const dayStart = day => Date.parse(`${day}T00:00:00.000Z`);
6
+ const flatten = arr => [].concat(...arr);
7
+ const {keys} = Object;
8
+
9
+ /** Find the known price estimate that is closest in time to a day
10
+
11
+ {
12
+ day: <Day YYYY-MM-DD String>
13
+ estimates: {
14
+ [priors]: {<Day YYYY-MM-DD String>: {cents: <USD Cents Number>}}
15
+ [provisional]: {<Day YYYY-MM-DD String>: {cents: <USD Cents Number>}}
16
+ [rates]: {<Day YYYY-MM-DD String>: {cents: <USD Cents Number>}}
17
+ }
18
+ [latest]: <Latest Day To Consider YYYY-MM-DD String>
19
+ }
20
+
21
+ @returns
22
+ {
23
+ [estimate]: {
24
+ cents: <USD Cents Per BTC Number>
25
+ day: <Day YYYY-MM-DD String>
26
+ distance: <Days Away Number>
27
+ }
28
+ }
29
+ */
30
+ module.exports = ({day, estimates, latest}) => {
31
+ const sources = [estimates.rates, estimates.priors, estimates.provisional];
32
+
33
+ const isConsidered = known => !latest || known <= latest;
34
+
35
+ const known = flatten(sources.filter(n => !!n).map(source => {
36
+ return keys(source).filter(isConsidered).map(known => ({
37
+ cents: source[known].cents,
38
+ day: known,
39
+ distance: abs(dayStart(day) - dayStart(known)) / msPerDay,
40
+ }));
41
+ }));
42
+
43
+ // The nearest estimate, the earliest of them when two are as near
44
+ const byDay = sortBy({array: known, attribute: 'day'}).sorted;
45
+
46
+ const [estimate] = sortBy({array: byDay, attribute: 'distance'}).sorted;
47
+
48
+ return {estimate};
49
+ };
@@ -0,0 +1,25 @@
1
+ const {centsPerDollar} = require('./../constants');
2
+
3
+ const {round} = Math;
4
+ const windowAbovePrior = 1.2;
5
+ const windowBelowPrior = 0.62;
6
+
7
+ /** Get the price window to search within given a nearby price estimate
8
+
9
+ {
10
+ cents: <Nearby Estimate USD Cents Per BTC Number>
11
+ }
12
+
13
+ @returns
14
+ {
15
+ max_price: <Maximum Possible USD Price Number>
16
+ min_price: <Minimum Possible USD Price Number>
17
+ }
18
+ */
19
+ module.exports = ({cents}) => {
20
+ // Less than a factor of two wide, so aliases at two or more are outside
21
+ return {
22
+ max_price: round(cents * windowAbovePrior) / centsPerDollar,
23
+ min_price: round(cents * windowBelowPrior) / centsPerDollar,
24
+ };
25
+ };