@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,83 @@
1
+ const rounded = n => Math.round(n * 1e3) / 1e3;
2
+
3
+ /** Remember a price estimate with the known estimates
4
+
5
+ {
6
+ estimate: {
7
+ [agreement]: <Spike Strengths Agreement With Template Weights Number>
8
+ [blocks]: [<First Block Height Number>, <Last Block Height Number>]
9
+ cents: <USD Cents Per BTC Number>
10
+ [confidence]: <Best Position Score Over Best Alias Score Number>
11
+ date: <Day YYYY-MM-DD String>
12
+ [dispersion]: <Peak Width Beyond Smoothing As Price Fraction Number>
13
+ [drift]: <Second Half Price Change From First Half Price Number>
14
+ [evidence]: <Share of Prior Weight Observable At the Price Number>
15
+ [hash]: <Last Block Hash Hex String>
16
+ [is_provisional]: <Price Is For An Incomplete Day Bool>
17
+ [is_sample]: <Price Is A Rough Estimate From Sample Blocks Bool>
18
+ [is_unguided]: <Price Was Estimated Without a Neighboring Guide Bool>
19
+ [is_unsettled]: <Refinement Was Still Moving After Its Last Pass Bool>
20
+ [outputs]: <Payment Outputs Considered Count Number>
21
+ [payments]: <Estimated Round USD Payments Count Number>
22
+ [rivalry]: <Best Rival Position Score Over Best Score Number>
23
+ [round_shift]: <Price Change If Round BTC Values Were Counted Number>
24
+ [stability]: <Largest Price Change From Perturbed Refinement Number>
25
+ [version]: <Method Version String>
26
+ }
27
+ estimates: {
28
+ priors: {<Day YYYY-MM-DD String>: <Estimate Object>}
29
+ provisional: {<Day YYYY-MM-DD String>: {cents: <USD Cents Number>}}
30
+ rates: {<Day YYYY-MM-DD String>: <Estimate Object>}
31
+ }
32
+ }
33
+
34
+ @returns
35
+ {
36
+ estimates: {
37
+ priors: {<Day YYYY-MM-DD String>: <Estimate Object>}
38
+ provisional: {<Day YYYY-MM-DD String>: {cents: <USD Cents Number>}}
39
+ rates: {<Day YYYY-MM-DD String>: <Estimate Object>}
40
+ }
41
+ }
42
+ */
43
+ module.exports = ({estimate, estimates}) => {
44
+ const {blocks, cents, date, hash, version} = estimate;
45
+
46
+ // Exit early when the estimate is provisional
47
+ if (!!estimate.is_provisional) {
48
+ estimates.provisional[date] = {blocks, cents, hash, version};
49
+
50
+ return {estimates};
51
+ }
52
+
53
+ // Exit early when the estimate is only a rough guide
54
+ if (!!estimate.is_sample) {
55
+ estimates.priors[date] = {blocks, cents, hash, version};
56
+
57
+ return {estimates};
58
+ }
59
+
60
+ estimates.rates[date] = {
61
+ blocks,
62
+ cents,
63
+ hash,
64
+ version,
65
+ agreement: rounded(estimate.agreement),
66
+ confidence: rounded(estimate.confidence),
67
+ dispersion: rounded(estimate.dispersion),
68
+ drift: estimate.drift === undefined ? undefined : rounded(estimate.drift),
69
+ evidence: rounded(estimate.evidence),
70
+ is_unguided: estimate.is_unguided || undefined,
71
+ is_unsettled: estimate.is_unsettled || undefined,
72
+ outputs: estimate.outputs,
73
+ payments: estimate.payments,
74
+ rivalry: rounded(estimate.rivalry),
75
+ round_shift: rounded(estimate.round_shift),
76
+ stability: rounded(estimate.stability),
77
+ };
78
+
79
+ // A full day rate replaces the prior estimate of its day
80
+ delete estimates.priors[date];
81
+
82
+ return {estimates};
83
+ };
@@ -0,0 +1,198 @@
1
+ const asyncAuto = require('async/auto');
2
+ const {returnResult} = require('asyncjs-util');
3
+
4
+ const {centsPerDollar} = require('./../constants');
5
+ const currentEstimates = require('./current_estimates');
6
+ const {putPricesFile} = require('./../../storage');
7
+ const rememberEstimate = require('./remember_estimate');
8
+ const {sampleLogInterval} = require('./../constants');
9
+
10
+ const {assign} = Object;
11
+ const dollars = cents => (cents / centsPerDollar).toFixed(2);
12
+ const rounded = n => Math.round(n * 1e3) / 1e3;
13
+
14
+ /** Store a price estimate with the known estimates and in the prices file
15
+
16
+ {
17
+ estimate: {
18
+ [agreement]: <Spike Strengths Agreement With Prior Weights Number>
19
+ blocks: [<First Block Height Number>, <Last Block Height Number>]
20
+ cents: <USD Cents Per BTC Number>
21
+ [confidence]: <Best Position Score Over Best Alias Score Number>
22
+ date: <Day YYYY-MM-DD String>
23
+ [hash]: <Last Block Hash Hex String>
24
+ [is_provisional]: <Price Is For An Incomplete Day Bool>
25
+ [is_sample]: <Price Is A Rough Estimate From Sample Blocks Bool>
26
+ [is_unguided]: <Price Was Estimated Without a Neighboring Guide Bool>
27
+ [is_unsettled]: <Refinement Was Still Moving After Its Last Pass Bool>
28
+ [is_widened]: <Search Was Widened Beyond the Price Window Bool>
29
+ outputs: <Payment Outputs Considered Count Number>
30
+ [payments]: <Estimated Round USD Payments Count Number>
31
+ [rivalry]: <Best Rival Position Score Over Best Score Number>
32
+ [dispersion]: <Peak Width Beyond Smoothing As Price Fraction Number>
33
+ [drift]: <Second Half Price Change From First Half Price Number>
34
+ [stability]: <Largest Price Change From Perturbed Refinement Number>
35
+ [version]: <Method Version String>
36
+ }
37
+ estimates: {
38
+ priors: {<Day YYYY-MM-DD String>: <Estimate Object>}
39
+ provisional: {<Day YYYY-MM-DD String>: {cents: <USD Cents Number>}}
40
+ rates: {<Day YYYY-MM-DD String>: <Estimate Object>}
41
+ samples: <Count of Sample Estimates Stored Number>
42
+ }
43
+ fs: {
44
+ getFile: <Read File Contents Function> (path, cbk) => {}
45
+ makeDirectory: <Make Directory Function> (path, cbk) => {}
46
+ renameFile: <Rename File Function> (from, to, cbk) => {}
47
+ writeFile: <Write File Contents Function> (path, contents, cbk) => {}
48
+ }
49
+ [logger]: <Winston Logger Object>
50
+ path: <Prices File Path String>
51
+ }
52
+
53
+ @returns via cbk or Promise
54
+ */
55
+ module.exports = ({estimate, estimates, fs, logger, path}, cbk) => {
56
+ return new Promise((resolve, reject) => {
57
+ return asyncAuto({
58
+ // Check arguments
59
+ validate: cbk => {
60
+ if (!estimate) {
61
+ return cbk([400, 'ExpectedEstimateToStoreEstimate']);
62
+ }
63
+
64
+ if (!estimates) {
65
+ return cbk([400, 'ExpectedKnownEstimatesToStoreEstimate']);
66
+ }
67
+
68
+ if (!fs) {
69
+ return cbk([400, 'ExpectedFileSystemMethodsToStoreEstimate']);
70
+ }
71
+
72
+ if (!path) {
73
+ return cbk([400, 'ExpectedPricesFilePathToStoreEstimate']);
74
+ }
75
+
76
+ return cbk();
77
+ },
78
+
79
+ // Log the estimate
80
+ log: ['validate', ({}, cbk) => {
81
+ const {blocks, cents, date, drift, outputs, payments} = estimate;
82
+
83
+ // Exit early when there is no logger
84
+ if (!logger) {
85
+ return cbk();
86
+ }
87
+
88
+ const price = dollars(cents);
89
+ const widened = estimate.is_widened || undefined;
90
+
91
+ if (!!estimate.is_provisional) {
92
+ logger.info({
93
+ chain_price: {
94
+ date,
95
+ price,
96
+ is_provisional: true,
97
+ is_widened: widened,
98
+ },
99
+ });
100
+
101
+ return cbk();
102
+ }
103
+
104
+ if (!!estimate.is_sample) {
105
+ estimates.samples++;
106
+
107
+ if (!(estimates.samples % sampleLogInterval) || !!widened) {
108
+ logger.info({
109
+ chain_price: {
110
+ date,
111
+ price,
112
+ is_sample: true,
113
+ is_widened: widened,
114
+ },
115
+ });
116
+ }
117
+
118
+ return cbk();
119
+ }
120
+
121
+ // A full day price estimated without a guide is a starting point
122
+ if (!!estimate.is_unguided) {
123
+ logger.info({chain_price: {starting_from: date}});
124
+ }
125
+
126
+ logger.info({
127
+ chain_price: {
128
+ date,
129
+ outputs,
130
+ payments,
131
+ price,
132
+ agreement: rounded(estimate.agreement),
133
+ blocks: blocks[1] - blocks[0] + 1,
134
+ confidence: rounded(estimate.confidence),
135
+ dispersion: rounded(estimate.dispersion),
136
+ drift: estimate.drift === undefined ? undefined : rounded(drift),
137
+ is_unsettled: estimate.is_unsettled || undefined,
138
+ is_widened: widened,
139
+ rivalry: rounded(estimate.rivalry),
140
+ stability: rounded(estimate.stability),
141
+ },
142
+ });
143
+
144
+ return cbk();
145
+ }],
146
+
147
+ // The known estimates with the estimate, kept apart until it is stored
148
+ remember: ['log', ({}, cbk) => {
149
+ const updated = {
150
+ priors: assign({}, estimates.priors),
151
+ provisional: assign({}, estimates.provisional),
152
+ rates: assign({}, estimates.rates),
153
+ };
154
+
155
+ return cbk(null, rememberEstimate({estimate, estimates: updated}));
156
+ }],
157
+
158
+ // Write the estimate to the prices file
159
+ write: ['remember', ({remember}, cbk) => {
160
+ return putPricesFile({
161
+ fs,
162
+ path,
163
+ priors: remember.estimates.priors,
164
+ provisional: remember.estimates.provisional,
165
+ rates: remember.estimates.rates,
166
+ },
167
+ cbk);
168
+ }],
169
+
170
+ // Keep the current prices that are now in the file
171
+ update: ['remember', 'write', ({remember, write}, cbk) => {
172
+ // Prices in the file from other versions of the method are not used
173
+ const current = currentEstimates({
174
+ priors: write.priors,
175
+ provisional: write.provisional,
176
+ rates: write.rates,
177
+ });
178
+
179
+ estimates.priors = current.priors;
180
+ estimates.provisional = current.provisional;
181
+ estimates.rates = current.rates;
182
+
183
+ // Exit early when the estimate is not a provisional price
184
+ if (!estimate.is_provisional) {
185
+ return cbk();
186
+ }
187
+
188
+ // A later provisional price in the file may not be usable here
189
+ const {date} = estimate;
190
+
191
+ estimates.provisional[date] = remember.estimates.provisional[date];
192
+
193
+ return cbk();
194
+ }],
195
+ },
196
+ returnResult({reject, resolve}, cbk));
197
+ });
198
+ };
@@ -0,0 +1,63 @@
1
+ const parabolaPeak = require('./parabola_peak');
2
+ const positionScore = require('./position_score');
3
+
4
+ const {log10} = Math;
5
+ const {round} = Math;
6
+
7
+ /** Find the best scoring dollar position among positions under a weighting
8
+
9
+ {
10
+ amounts: [<Round USD Amount Number>]
11
+ bins: [<Dollar Position Bin Number>]
12
+ bins_per_decade: <Bins Per Factor of Ten Number>
13
+ min_bin: <Lowest Observable Bin Number>
14
+ noise: [<Spike Strength From Noise Alone At Bin Number>]
15
+ strengths: [<Spike Strength At Bin Number>]
16
+ weights: [<Amount Weight Number>]
17
+ }
18
+
19
+ @returns
20
+ {
21
+ bin: <Best Scoring Dollar Position Bin Number>
22
+ delta: <Peak Offset From the Best Bin In Bins Number>
23
+ score: <Best Score Number>
24
+ }
25
+ */
26
+ module.exports = args => {
27
+ const {noise, strengths, weights} = args;
28
+
29
+ // The bins of the amounts above a dollar bin are the same at every position
30
+ const shifts = args.amounts.map(usd => {
31
+ return round(log10(usd) * args.bins_per_decade);
32
+ });
33
+
34
+ const scores = args.bins.map(bin => {
35
+ const {score} = positionScore({
36
+ noise,
37
+ shifts,
38
+ strengths,
39
+ weights,
40
+ dollar_bin: bin,
41
+ min_bin: args.min_bin,
42
+ });
43
+
44
+ return {bin, score};
45
+ });
46
+
47
+ // The first of the highest scores is the best
48
+ const best = scores.reduce((lead, n) => n.score > lead.score ? n : lead);
49
+
50
+ const index = scores.indexOf(best);
51
+
52
+ // The peak is between the best bin and its neighbors
53
+ const above = scores[index + 1] || best;
54
+ const below = scores[index - 1] || best;
55
+
56
+ const {delta} = parabolaPeak({
57
+ above: above.score,
58
+ below: below.score,
59
+ center: best.score,
60
+ });
61
+
62
+ return {delta, bin: best.bin, score: best.score};
63
+ };
@@ -0,0 +1,50 @@
1
+ const sortedNeighbors = require('./sorted_neighbors');
2
+
3
+ const defaultExclude = 2;
4
+ const defaultRadius = 12;
5
+ const {isArray} = Array;
6
+ const {max} = Math;
7
+
8
+ /** Get the excess of each histogram bin over its local background
9
+
10
+ {
11
+ bins: [<Share of Outputs in Bin Number>]
12
+ [exclude]: <Bins Next to the Bin to Leave Out of the Background Number>
13
+ [radius]: <Bins On Either Side to Take the Background From Number>
14
+ }
15
+
16
+ @throws
17
+ <Error>
18
+
19
+ @returns
20
+ {
21
+ background: [<Local Background Share Number>]
22
+ excess: [<Share of Outputs Above Local Background Number>]
23
+ }
24
+ */
25
+ module.exports = ({bins, exclude, radius}) => {
26
+ if (!isArray(bins)) {
27
+ throw new Error('ExpectedHistogramBinsToCalculateHistogramExcess');
28
+ }
29
+
30
+ const {values} = sortedNeighbors({
31
+ bins,
32
+ exclude: exclude === undefined ? defaultExclude : exclude,
33
+ min_bin: Number(),
34
+ radius: radius === undefined ? defaultRadius : radius,
35
+ value: sorted => {
36
+ // Exit early when there is no background to compare against
37
+ if (!sorted.length) {
38
+ return Number();
39
+ }
40
+
41
+ // The median neighbor, rounded down to the lower middle of an even count
42
+ return sorted[(sorted.length - 1) >>> 1];
43
+ },
44
+ });
45
+
46
+ return {
47
+ background: values,
48
+ excess: bins.map((share, bin) => max(0, share - values[bin])),
49
+ };
50
+ };
@@ -0,0 +1,87 @@
1
+ const {half} = require('./../constants');
2
+ const histogramExcess = require('./histogram_excess');
3
+ const parabolaPeak = require('./parabola_peak');
4
+ const paymentsNearPosition = require('./payments_near_position');
5
+ const peakWidth = require('./peak_width');
6
+ const smoothBins = require('./smooth_bins');
7
+
8
+ const {abs} = Math;
9
+ const {ceil} = Math;
10
+ const {max} = Math;
11
+ const range = (a, b) => [...Array(b - a).keys()].map(i => a + i);
12
+ const {round} = Math;
13
+ const {sqrt} = Math;
14
+
15
+ /** Find the peak in a fine histogram of implied prices
16
+
17
+ {
18
+ counts: [<Implied Prices In Bin Count Number>]
19
+ exclude: <Bins Next to a Bin to Leave Out of Its Background Number>
20
+ kernel: <Smoothing Kernel Half Width in Bins Number>
21
+ weighted: [<Weighted Implied Prices In Bin Number>]
22
+ }
23
+
24
+ @returns
25
+ {
26
+ delta: <Peak Offset From the Peak Bin In Bins Number>
27
+ noise: <Largest Payments Count Away From the Peak Number>
28
+ payments: <Estimated Round Payments In the Peak Number>
29
+ peak: <Peak Bin Index Number>
30
+ significance: <Peak Payments Beyond Noise In Standard Deviations Number>
31
+ width: <Peak Width At Half of Its Height in Bins Number>
32
+ }
33
+ */
34
+ module.exports = ({counts, exclude, kernel, weighted}) => {
35
+ const radius = counts.length;
36
+
37
+ const {excess} = histogramExcess({exclude, radius, bins: weighted});
38
+
39
+ // Smoothing the excess gathers the peak into one place
40
+ const {smoothed} = smoothBins({half: kernel, bins: excess});
41
+
42
+ const peak = smoothed.indexOf(max(...smoothed));
43
+
44
+ const after = smoothed[peak + 1];
45
+ const before = smoothed[peak - 1];
46
+ const center = smoothed[peak];
47
+
48
+ const {delta} = parabolaPeak({
49
+ center,
50
+ above: after === undefined ? center : after,
51
+ below: before === undefined ? center : before,
52
+ });
53
+
54
+ const {background} = histogramExcess({exclude, radius, bins: counts});
55
+
56
+ const {width} = peakWidth({peak, bins: smoothed});
57
+
58
+ const {above, observed} = paymentsNearPosition({
59
+ background,
60
+ counts,
61
+ kernel,
62
+ position: peak,
63
+ });
64
+
65
+ // Every other position that does not overlap the peak is a noise peak
66
+ const apart = ceil(width * half) + kernel;
67
+
68
+ const noise = range(Number(), counts.length)
69
+ .filter(position => abs(position - peak) > apart)
70
+ .map(position => {
71
+ return paymentsNearPosition({background, counts, kernel, position});
72
+ })
73
+ .map(payments => payments.above)
74
+ .reduce((largest, n) => max(largest, n), Number());
75
+
76
+ // Significance is the payments beyond noise in standard deviations
77
+ const beyond = max(Number(), above - noise);
78
+
79
+ return {
80
+ delta,
81
+ peak,
82
+ width,
83
+ noise: round(noise),
84
+ payments: max(Number(), round(above)),
85
+ significance: !observed ? Number() : beyond / sqrt(observed),
86
+ };
87
+ };
@@ -0,0 +1,53 @@
1
+ const {half} = require('./../constants');
2
+ const {tokensPerBigUnit} = require('./../constants');
3
+
4
+ const {log10} = Math;
5
+ const {pow} = Math;
6
+ const {round} = Math;
7
+
8
+ /** Make a fine histogram of the prices implied by outputs near round amounts
9
+
10
+ {
11
+ amounts: [<Round USD Amount Number>]
12
+ bins_per_decade: <Bins Per Factor of Ten Number>
13
+ outputs: [<Output Tokens Number>]
14
+ price: <Rough USD Price Number>
15
+ reach: <Bins On Either Side of the Rough Price Number>
16
+ weights: [<Round USD Amount Weight Number>]
17
+ }
18
+
19
+ @returns
20
+ {
21
+ counts: [<Implied Prices In Bin Count Number>]
22
+ weighted: [<Weighted Implied Prices In Bin Number>]
23
+ }
24
+ */
25
+ module.exports = args => {
26
+ const {amounts, outputs, price, reach, weights} = args;
27
+
28
+ const binsPerDecade = args.bins_per_decade;
29
+ const counts = Array(reach * 2 + 1).fill(Number());
30
+ const maxRatio = pow(10, (reach + half) / args.bins_per_decade);
31
+ const weighted = Array(reach * 2 + 1).fill(Number());
32
+
33
+ // Round amounts are further apart than the reach, so an output fits one
34
+ outputs.forEach(tokens => {
35
+ const paid = price * tokens / tokensPerBigUnit;
36
+
37
+ return amounts.forEach((usd, index) => {
38
+ const ratio = usd / paid;
39
+
40
+ // Exit early when the output is not near this round amount
41
+ if (ratio >= maxRatio || ratio <= 1 / maxRatio) {
42
+ return;
43
+ }
44
+
45
+ const bin = reach + round(log10(ratio) * binsPerDecade);
46
+
47
+ counts[bin]++;
48
+ weighted[bin] += weights[index];
49
+ });
50
+ });
51
+
52
+ return {counts, weighted};
53
+ };
@@ -0,0 +1,33 @@
1
+ const bestPosition = require('./best_position');
2
+ const histogramExcess = require('./histogram_excess');
3
+ const histogramPeak = require('./histogram_peak');
4
+ const impliedPricesHistogram = require('./implied_prices_histogram');
5
+ const isRoundTokens = require('./is_round_tokens');
6
+ const kernelWidth = require('./kernel_width');
7
+ const lowerBound = require('./lower_bound');
8
+ const noiseStrength = require('./noise_strength');
9
+ const noiseStrengths = require('./noise_strengths');
10
+ const outputsHistogram = require('./outputs_histogram');
11
+ const peakWidth = require('./peak_width');
12
+ const scorePosition = require('./score_position');
13
+ const smoothBins = require('./smooth_bins');
14
+ const spikeStrengths = require('./spike_strengths');
15
+ const templateAgreement = require('./template_agreement');
16
+
17
+ module.exports = {
18
+ bestPosition,
19
+ histogramExcess,
20
+ histogramPeak,
21
+ impliedPricesHistogram,
22
+ isRoundTokens,
23
+ kernelWidth,
24
+ lowerBound,
25
+ noiseStrength,
26
+ noiseStrengths,
27
+ outputsHistogram,
28
+ peakWidth,
29
+ scorePosition,
30
+ smoothBins,
31
+ spikeStrengths,
32
+ templateAgreement,
33
+ };
@@ -0,0 +1,26 @@
1
+ const {roundTokensMultiple} = require('./../constants');
2
+
3
+ const roundMantissas = [1, 2, 5];
4
+
5
+ /** Determine if a tokens amount is a round amount of BTC
6
+
7
+ {
8
+ tokens: <Tokens Number>
9
+ }
10
+
11
+ @returns
12
+ {
13
+ is_round: <Tokens Are A Round Amount Bool>
14
+ }
15
+ */
16
+ module.exports = ({tokens}) => {
17
+ // Exit early when the amount is a multiple of a thousand sats
18
+ if (!(tokens % roundTokensMultiple)) {
19
+ return {is_round: true};
20
+ }
21
+
22
+ // Smaller amounts are round as a one, two or five followed by zeros
23
+ const digits = String(tokens).replace(/0+$/, '');
24
+
25
+ return {is_round: roundMantissas.includes(Number(digits))};
26
+ };
@@ -0,0 +1,25 @@
1
+ const peakWidth = require('./peak_width');
2
+ const smoothBins = require('./smooth_bins');
3
+
4
+ const spike = 1;
5
+
6
+ /** Measure the width that smoothing alone gives a single bin spike
7
+
8
+ {
9
+ half: <Kernel Half Width in Bins Number>
10
+ }
11
+
12
+ @returns
13
+ {
14
+ width: <Smoothed Single Bin Spike Width At Half Height in Bins Number>
15
+ }
16
+ */
17
+ module.exports = ({half}) => {
18
+ const positions = [...Array(half * 2 + 1).keys()];
19
+
20
+ const bins = positions.map(i => i === half ? spike : 0);
21
+
22
+ const {smoothed} = smoothBins({half, bins});
23
+
24
+ return {width: peakWidth({peak: half, bins: smoothed}).width};
25
+ };
@@ -0,0 +1,28 @@
1
+ /** Find the first index in a sorted array that is not less than a value
2
+
3
+ {
4
+ sorted: [<Sorted Number>]
5
+ value: <Value Number>
6
+ }
7
+
8
+ @returns
9
+ {
10
+ index: <Index of First Element Not Less Than Value Number>
11
+ }
12
+ */
13
+ module.exports = ({sorted, value}) => {
14
+ const bounds = {high: sorted.length, low: Number()};
15
+
16
+ // Halve the range until it has narrowed to a single index
17
+ while (bounds.low < bounds.high) {
18
+ const mid = (bounds.low + bounds.high) >>> 1;
19
+
20
+ if (sorted[mid] < value) {
21
+ bounds.low = mid + 1;
22
+ } else {
23
+ bounds.high = mid;
24
+ }
25
+ }
26
+
27
+ return {index: bounds.low};
28
+ };
@@ -0,0 +1,28 @@
1
+ const {ceil} = Math;
2
+ const noiseTrimShare = 0.2;
3
+ const spikeKernelSum = 3;
4
+
5
+ /** Get the strength that noise alone would give a spike from its neighbors
6
+
7
+ {
8
+ sorted: [<Neighbor Share In Ascending Order Number>]
9
+ }
10
+
11
+ @returns
12
+ {
13
+ noise: <Spike Strength From Noise Alone Number>
14
+ }
15
+ */
16
+ module.exports = ({sorted}) => {
17
+ // The highest shares nearby are left out, they can be other spikes
18
+ const keep = ceil(sorted.length * (1 - noiseTrimShare));
19
+
20
+ // Exit early when there are no neighbors to measure noise from
21
+ if (!keep) {
22
+ return {noise: Number()};
23
+ }
24
+
25
+ const sum = sorted.reduce((total, n, i) => i < keep ? total + n : total, 0);
26
+
27
+ return {noise: spikeKernelSum * (sum / keep)};
28
+ };