@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,26 @@
1
+ const {createHash} = require('crypto');
2
+
3
+ const template = require('./../outputs/template');
4
+
5
+ const methodVersion = 5;
6
+ const sha256 = n => createHash('sha256').update(n).digest('hex');
7
+ const {stringify} = JSON;
8
+ const templateHashLength = 8;
9
+
10
+ /** Get the version of the price derivation method
11
+
12
+ {}
13
+
14
+ @returns
15
+ {
16
+ version: <Method Version String>
17
+ }
18
+ */
19
+ module.exports = ({}) => {
20
+ // A change to the template of round amounts changes the version too
21
+ const hash = sha256(stringify(template));
22
+
23
+ const templateVersion = hash.slice(Number(), templateHashLength);
24
+
25
+ return {version: `${methodVersion}.${templateVersion}`};
26
+ };
@@ -0,0 +1,67 @@
1
+ const {isCalendarDay} = require('./../../dates');
2
+
3
+ const isCents = n => Number.isSafeInteger(n) && n > 0;
4
+ const isDay = day => isCalendarDay({day}).is_calendar_day;
5
+ const isObject = n => !!n && typeof n === 'object' && !Array.isArray(n);
6
+ const {keys} = Object;
7
+ const {parse} = JSON;
8
+
9
+ /** Parse the JSON of the prices shipped with the method
10
+
11
+ {
12
+ text: <Known Prices JSON String>
13
+ }
14
+
15
+ @throws
16
+ <Error>
17
+
18
+ @returns
19
+ {
20
+ rates: {
21
+ <Day YYYY-MM-DD String>: {
22
+ cents: <USD Cents Per BTC Number>
23
+ version: <Method Version String>
24
+ }
25
+ }
26
+ version: <Method Version String>
27
+ }
28
+ */
29
+ module.exports = ({text}) => {
30
+ if (typeof text !== 'string') {
31
+ throw new Error('ExpectedTextToParseKnownPrices');
32
+ }
33
+
34
+ const known = (() => {
35
+ try {
36
+ return parse(text);
37
+ } catch (err) {
38
+ return undefined;
39
+ }
40
+ })();
41
+
42
+ if (!isObject(known)) {
43
+ throw new Error('ExpectedJsonObjectToParseKnownPrices');
44
+ }
45
+
46
+ const {prices, version} = known;
47
+
48
+ if (!version || typeof version !== 'string') {
49
+ throw new Error('ExpectedMethodVersionToParseKnownPrices');
50
+ }
51
+
52
+ if (!isObject(prices) || !keys(prices).every(isDay)) {
53
+ throw new Error('ExpectedPricesByDayToParseKnownPrices');
54
+ }
55
+
56
+ const days = keys(prices).sort();
57
+
58
+ if (!days.every(day => isCents(prices[day]))) {
59
+ throw new Error('ExpectedPriceInCentsToParseKnownPrices');
60
+ }
61
+
62
+ const rates = {};
63
+
64
+ days.forEach(day => rates[day] = {version, cents: prices[day]});
65
+
66
+ return {rates, version};
67
+ };
@@ -0,0 +1,31 @@
1
+ const flatten = arr => [].concat(...arr);
2
+ const {log10} = Math;
3
+ const {round} = Math;
4
+ const uniq = arr => Array.from(new Set(arr));
5
+
6
+ /** Get the shifts in bins at which the round USD pattern lines up with itself
7
+
8
+ {
9
+ amounts: [<Round USD Amount Number>]
10
+ bins_per_decade: <Bins Per Factor of Ten Number>
11
+ }
12
+
13
+ @returns
14
+ {
15
+ shifts: [<Alias Shift in Bins Number>]
16
+ }
17
+ */
18
+ module.exports = args => {
19
+ // Shifting by the ratio between two amounts lands one amount on the other
20
+ const ratios = flatten(args.amounts.map(amount => {
21
+ return args.amounts.filter(n => n > amount).map(n => n / amount);
22
+ }));
23
+
24
+ // Convert each ratio to a distance in log-scale bins
25
+ const shifts = ratios.map(ratio => {
26
+ return round(log10(ratio) * args.bins_per_decade);
27
+ });
28
+
29
+ // A wrong price can look right at these partial self-match positions
30
+ return {shifts: uniq(shifts).filter(n => !!n).sort((a, b) => a - b)};
31
+ };
@@ -0,0 +1,83 @@
1
+ const {isRoundTokens} = require('./../histogram');
2
+ const {lowerBound} = require('./../histogram');
3
+
4
+ const bandShare = 0.005;
5
+ const decades = [4, 5, 6, 7, 8, 9, 10, 11];
6
+ const flatten = arr => [].concat(...arr);
7
+ const {floor} = Math;
8
+ const mantissas = [1, 2, 5];
9
+ const minExcessDeviations = 4;
10
+ const {pow} = Math;
11
+ const range = (a, b) => [...Array(b - a).keys()].map(i => a + i);
12
+ const {round} = Math;
13
+ const {sqrt} = Math;
14
+ const surroundShare = 0.03;
15
+
16
+ /** Cap the values that cluster just off round amounts of BTC
17
+
18
+ {
19
+ outputs: [<Output Tokens Number>]
20
+ }
21
+
22
+ @returns
23
+ {
24
+ outputs: [<Output Tokens Number>]
25
+ }
26
+ */
27
+ module.exports = ({outputs}) => {
28
+ const isRound = tokens => isRoundTokens({tokens}).is_round;
29
+
30
+ const amounts = flatten(decades.map(decade => {
31
+ return mantissas.map(mantissa => mantissa * pow(10, decade));
32
+ }));
33
+
34
+ // Exact round values are left in place, to be capped on their own
35
+ const exact = outputs.filter(tokens => isRound(tokens));
36
+ const sorted = Float64Array.from(outputs.filter(n => !isRound(n))).sort();
37
+
38
+ const indexOf = value => lowerBound({sorted, value}).index;
39
+
40
+ // The bands that clearly stand out, with how many of their values to keep
41
+ const capped = amounts.map(amount => {
42
+ const high = amount * (1 + bandShare);
43
+ const low = amount * (1 - bandShare);
44
+ const reach = amount * surroundShare;
45
+
46
+ const end = indexOf(high);
47
+ const start = indexOf(low);
48
+
49
+ const around = start - indexOf(low - reach) + indexOf(high + reach) - end;
50
+
51
+ const expected = around * (high - low) / (reach + reach);
52
+
53
+ // Exit early when the band does not clearly stand out from around it
54
+ if (end - start <= expected + minExcessDeviations * sqrt(expected)) {
55
+ return undefined;
56
+ }
57
+
58
+ return {end, start, keep: round(expected)};
59
+ });
60
+
61
+ const dropped = new Set();
62
+
63
+ // Keep evenly spaced values across each band that stands out
64
+ capped.filter(n => !!n).forEach(({end, keep, start}) => {
65
+ const count = end - start;
66
+
67
+ return range(start, end).forEach((index, position) => {
68
+ const before = floor(position * keep / count);
69
+
70
+ const isKept = floor((position + 1) * keep / count) > before;
71
+
72
+ if (!isKept) {
73
+ dropped.add(index);
74
+ }
75
+
76
+ return;
77
+ });
78
+ });
79
+
80
+ const kept = [...sorted].filter((n, i) => !dropped.has(i));
81
+
82
+ return {outputs: exact.concat(kept)};
83
+ };
@@ -0,0 +1,33 @@
1
+ const {isRoundTokens} = require('./../histogram');
2
+
3
+ const maxRepeats = 10;
4
+
5
+ /** Cap how many times any one exact amount of sats is counted
6
+
7
+ {
8
+ outputs: [<Output Tokens Number>]
9
+ }
10
+
11
+ @returns
12
+ {
13
+ outputs: [<Output Tokens Number>]
14
+ }
15
+ */
16
+ module.exports = ({outputs}) => {
17
+ const counts = new Map();
18
+
19
+ return {
20
+ outputs: outputs.filter(tokens => {
21
+ // Exit early when the amount is a round amount of BTC
22
+ if (isRoundTokens({tokens}).is_round) {
23
+ return true;
24
+ }
25
+
26
+ const count = (counts.get(tokens) || Number()) + 1;
27
+
28
+ counts.set(tokens, count);
29
+
30
+ return count <= maxRepeats;
31
+ }),
32
+ };
33
+ };
@@ -0,0 +1,19 @@
1
+ const {tokensPerBigUnit} = require('./../constants');
2
+
3
+ const {log10} = Math;
4
+
5
+ /** Get the histogram bin where one dollar lands at a price
6
+
7
+ {
8
+ bins_per_decade: <Bins Per Factor of Ten Number>
9
+ price: <USD Price Number>
10
+ }
11
+
12
+ @returns
13
+ {
14
+ bin: <Fractional Bin Where One Dollar Lands Number>
15
+ }
16
+ */
17
+ module.exports = args => {
18
+ return {bin: args.bins_per_decade * log10(tokensPerBigUnit / args.price)};
19
+ };
@@ -0,0 +1,321 @@
1
+ const aliasShifts = require('./alias_shifts');
2
+ const {bestPosition} = require('./../histogram');
3
+ const capNearRoundValues = require('./cap_near_round_values');
4
+ const capRepeatedValues = require('./cap_repeated_values');
5
+ const {centsPerDollar} = require('./../constants');
6
+ const dollarBinForPrice = require('./dollar_bin_for_price');
7
+ const {histogramExcess} = require('./../histogram');
8
+ const {isRoundTokens} = require('./../histogram');
9
+ const {kernelBins} = require('./../constants');
10
+ const {minTokens} = require('./../constants');
11
+ const {noiseStrengths} = require('./../histogram');
12
+ const {outputsHistogram} = require('./../histogram');
13
+ const priceForDollarBin = require('./price_for_dollar_bin');
14
+ const refinePrice = require('./refine_price');
15
+ const roundAmounts = require('./round_amounts');
16
+ const {scorePosition} = require('./../histogram');
17
+ const {spikeStrengths} = require('./../histogram');
18
+ const {templateAgreement} = require('./../histogram');
19
+
20
+ const {abs} = Math;
21
+ const aliasSearchBins = 2;
22
+ const {assign} = Object;
23
+ const {ceil} = Math;
24
+ const edgeBins = 3;
25
+ const flatten = arr => [].concat(...arr);
26
+ const {floor} = Math;
27
+ const isRound = tokens => isRoundTokens({tokens}).is_round;
28
+ const {log10} = Math;
29
+ const {max} = Math;
30
+ const maxConfidence = 100;
31
+ const maxPossiblePrice = 1e7;
32
+ const maxRivalBins = 57;
33
+ const {min} = Math;
34
+ const minPossiblePrice = 1e2;
35
+ const minRivalBins = 4;
36
+ const {pow} = Math;
37
+ const range = (a, b) => [...Array(b - a).keys()].map(i => a + i);
38
+ const {round} = Math;
39
+ const shortReach = 25;
40
+ const startShifts = [0.97, 1.03];
41
+
42
+ /** Estimate the USD price of BTC from payment output values
43
+
44
+ {
45
+ [is_capping_near_round]: <Cap Values Just Off Round BTC Amounts Too Bool>
46
+ [is_counting_round]: <Count Round BTC Values In Full Bool>
47
+ [is_quick]: <Skip the Stability Check Bool>
48
+ max_price: <Maximum Possible USD Price Number>
49
+ min_price: <Minimum Possible USD Price Number>
50
+ outputs: [<Output Tokens Number>]
51
+ }
52
+
53
+ @throws
54
+ <Error>
55
+
56
+ @returns
57
+ {
58
+ agreement: <Spike Strengths Agreement With Prior Weights Number>
59
+ cents: <USD Cents Per BTC Number>
60
+ confidence: <Best Position Score Over Best Patterned Alias Score Number>
61
+ coverage: <Share of Spike Excess At Round USD Positions Number>
62
+ dispersion: <Peak Width Beyond Smoothing As Price Fraction Number>
63
+ evidence: <Share of Prior Weight Observable At the Price Number>
64
+ is_at_edge: <Best Position Is At the Edge of the Price Window Bool>
65
+ [is_unsettled]: <Refinement Was Still Moving After Its Last Pass Bool>
66
+ noise: <Largest Payments Count Away From the Peak Number>
67
+ payments: <Estimated Round USD Payments Count Number>
68
+ rivalry: <Best Patterned Rival Position Score Over Best Score Number>
69
+ rough_cents: <Rough Estimate USD Cents Per BTC Number>
70
+ significance: <Payment Peak Beyond Noise In Standard Deviations Number>
71
+ spikes: [{
72
+ strength: <Spike Strength Relative to the Strongest Spike Number>
73
+ usd: <Round USD Amount Number>
74
+ }]
75
+ [stability]: <Largest Price Change From Perturbed Refinement Number>
76
+ }
77
+ */
78
+ module.exports = args => {
79
+ const isCapping = !!args.is_capping_near_round && !args.is_counting_round;
80
+
81
+ // Repeats of an exact amount beyond a few are not counted
82
+ const spread = capRepeatedValues({outputs: args.outputs});
83
+
84
+ const {outputs} = spread;
85
+
86
+ // Values just off round amounts of BTC are capped as well when asked
87
+ const kept = !isCapping ? spread : capNearRoundValues({outputs});
88
+
89
+ const histogram = outputsHistogram({
90
+ is_counting_round: args.is_counting_round,
91
+ outputs: kept.outputs,
92
+ });
93
+
94
+ if (!histogram.bins.some(share => !!share)) {
95
+ throw new Error('ExpectedOutputsInAnalysisRangeToDerivePriceFromOutputs');
96
+ }
97
+
98
+ const {amounts, weights} = roundAmounts({});
99
+ const binsPerDecade = histogram.bins_per_decade;
100
+ const {excess} = histogramExcess({bins: histogram.bins});
101
+ const minBin = round(log10(minTokens) * histogram.bins_per_decade);
102
+
103
+ // The strength that noise alone gives a spike at each bin of the histogram
104
+ const {noise} = noiseStrengths({excess, min_bin: minBin});
105
+
106
+ // The strength of a spike centered at each bin of the histogram
107
+ const {strengths} = spikeStrengths({excess});
108
+
109
+ const isCountingRound = !!args.is_counting_round;
110
+
111
+ // Round values are only refined on when they are counted in full
112
+ const refining = kept.outputs.filter(n => isCountingRound || !isRound(n));
113
+
114
+ // The histogram that every dollar position is scored against
115
+ const histogramArgs = {
116
+ amounts,
117
+ noise,
118
+ strengths,
119
+ bins_per_decade: binsPerDecade,
120
+ min_bin: minBin,
121
+ };
122
+
123
+ // Score a dollar position under a weighting of the template
124
+ const scoreAt = ({bin, weighting}) => {
125
+ return scorePosition({
126
+ amounts,
127
+ noise,
128
+ strengths,
129
+ bins_per_decade: binsPerDecade,
130
+ dollar_bin: bin,
131
+ min_bin: minBin,
132
+ weights: weighting,
133
+ });
134
+ };
135
+
136
+ // Higher prices put a dollar in lower bins, so the window is reversed
137
+ const lowDollarBin = floor(dollarBinForPrice({
138
+ bins_per_decade: binsPerDecade,
139
+ price: args.max_price,
140
+ })
141
+ .bin);
142
+
143
+ const highDollarBin = ceil(dollarBinForPrice({
144
+ bins_per_decade: binsPerDecade,
145
+ price: args.min_price,
146
+ })
147
+ .bin);
148
+
149
+ const positions = range(lowDollarBin, highDollarBin + 1);
150
+
151
+ // Find the alignment of the pattern using the prior weights
152
+ const prior = bestPosition(assign({}, histogramArgs, {
153
+ weights,
154
+ bins: positions,
155
+ }));
156
+
157
+ if (prior.score <= 0) {
158
+ throw new Error('ExpectedOutputsInPriceRangeToDerivePriceFromOutputs');
159
+ }
160
+
161
+ const atPrior = scoreAt({bin: prior.bin, weighting: weights});
162
+
163
+ const strongest = max(...atPrior.strengths);
164
+
165
+ // The measured spike strengths place the peak within the alignment
166
+ const measured = atPrior.strengths.map(n => n / strongest);
167
+
168
+ const within = range(prior.bin - kernelBins, prior.bin + kernelBins + 1);
169
+
170
+ const best = bestPosition(assign({}, histogramArgs, {
171
+ bins: within.filter(n => positions.includes(n)),
172
+ weights: measured,
173
+ }));
174
+
175
+ const roughPrice = priceForDollarBin({
176
+ bin: best.bin + best.delta,
177
+ bins_per_decade: binsPerDecade,
178
+ })
179
+ .price;
180
+
181
+ // Alias positions are where the pattern would also line up with itself
182
+ const {shifts} = aliasShifts({amounts, bins_per_decade: binsPerDecade});
183
+
184
+ // Only aliases at possible prices are candidates
185
+ const possibleLow = floor(dollarBinForPrice({
186
+ bins_per_decade: binsPerDecade,
187
+ price: maxPossiblePrice,
188
+ })
189
+ .bin);
190
+
191
+ const possibleHigh = ceil(dollarBinForPrice({
192
+ bins_per_decade: binsPerDecade,
193
+ price: minPossiblePrice,
194
+ })
195
+ .bin);
196
+
197
+ const aliasBins = flatten(shifts.map(n => [best.bin - n, best.bin + n]))
198
+ .filter(bin => bin >= possibleLow && bin <= possibleHigh);
199
+
200
+ const atBest = scoreAt({bin: best.bin, weighting: weights});
201
+
202
+ // An alias only competes when it shows a pattern of its own. Each position
203
+ // is scored against the whole histogram through the amounts it can see
204
+ // there, since both explain the same bins through different amounts
205
+ const margins = aliasBins.map(bin => {
206
+ const nearby = range(bin - aliasSearchBins, bin + aliasSearchBins + 1);
207
+
208
+ // The best alias position is the first of the highest scores nearby
209
+ const position = bestPosition(assign({}, histogramArgs, {
210
+ weights,
211
+ bins: nearby,
212
+ }));
213
+
214
+ const alias = scoreAt({bin: position.bin, weighting: weights});
215
+
216
+ // Exit early when the alias shows no pattern or nothing above noise
217
+ if (!alias.is_pattern || alias.score <= 0) {
218
+ return Infinity;
219
+ }
220
+
221
+ return atBest.score / alias.score;
222
+ });
223
+
224
+ // Without an alias that competes the confidence is capped, not infinite, so
225
+ // that it can be stored and logged as a number, and so is a larger margin
226
+ const confidence = min(maxConfidence, ...margins);
227
+
228
+ const peak = max(...atBest.strengths);
229
+
230
+ const spikes = amounts.map((usd, index) => ({
231
+ usd,
232
+ strength: !peak ? Number() : atBest.strengths[index] / peak,
233
+ }));
234
+
235
+ // The excess at round USD positions compared to all excess in their span
236
+ const centers = amounts.map(usd => {
237
+ return best.bin + round(log10(usd) * binsPerDecade);
238
+ });
239
+
240
+ const atPositions = flatten(centers.map(center => {
241
+ return range(center - kernelBins, center + kernelBins + 1);
242
+ }));
243
+
244
+ const span = range(
245
+ max(Number(), min(...atPositions)),
246
+ min(excess.length, max(...atPositions) + 1)
247
+ );
248
+
249
+ const excessAt = bins => bins.reduce((sum, n) => sum + (excess[n] || 0), 0);
250
+
251
+ const spanExcess = excessAt(span);
252
+
253
+ // Refine the price, recentering until it settles
254
+ const refine = ({price, reach}) => refinePrice({
255
+ amounts,
256
+ price,
257
+ reach,
258
+ outputs: refining,
259
+ weights: spikes.map(spike => spike.strength),
260
+ });
261
+
262
+ const refined = refine({price: roughPrice});
263
+
264
+ // A peak at the edge of the refinement range is not a refinement
265
+ const price = !refined.is_at_edge ? refined.price : roughPrice;
266
+
267
+ const isCheckingStability = !args.is_quick && !refined.is_at_edge;
268
+
269
+ // Perturbing the start and the reach of the refinement tests its stability
270
+ const starts = !isCheckingStability ? [] : startShifts;
271
+ const reaches = !isCheckingStability ? [] : [shortReach];
272
+
273
+ const variants = []
274
+ .concat(starts.map(shift => refine({price: roughPrice * shift})))
275
+ .concat(reaches.map(reach => refine({reach, price: roughPrice})));
276
+
277
+ const stability = variants.reduce((largest, variant) => {
278
+ return max(largest, abs(log10(variant.price / price)));
279
+ },
280
+ Number());
281
+
282
+ // A rival is the pattern fitting anywhere nearby, beyond the peak's spread
283
+ const spreadBins = ceil(log10(1 + refined.spread) * binsPerDecade);
284
+
285
+ const nearBins = max(minRivalBins, spreadBins + kernelBins);
286
+
287
+ const rivalBins = range(best.bin - maxRivalBins, best.bin + maxRivalBins + 1)
288
+ .filter(bin => abs(bin - best.bin) >= nearBins);
289
+
290
+ const rival = rivalBins
291
+ .map(bin => scoreAt({bin, weighting: measured}))
292
+ .filter(position => !!position.is_pattern)
293
+ .reduce((best, position) => max(best, position.score), Number());
294
+
295
+ // The best position may be at the edge of the search window
296
+ const fromHigh = highDollarBin - best.bin;
297
+ const fromLow = best.bin - lowDollarBin;
298
+
299
+ const {agreement} = templateAgreement({
300
+ weights,
301
+ strengths: atBest.strengths,
302
+ });
303
+
304
+ return {
305
+ agreement,
306
+ confidence,
307
+ spikes,
308
+ cents: round(price * centsPerDollar),
309
+ coverage: !spanExcess ? 0 : excessAt(atPositions) / spanExcess,
310
+ dispersion: refined.dispersion,
311
+ evidence: atBest.evidence,
312
+ is_at_edge: min(fromHigh, fromLow) < edgeBins,
313
+ is_unsettled: refined.is_unsettled,
314
+ noise: refined.noise,
315
+ payments: !refined.is_at_edge ? refined.payments : Number(),
316
+ rivalry: best.score <= 0 ? 0 : rival / best.score,
317
+ rough_cents: round(roughPrice * centsPerDollar),
318
+ significance: !refined.is_at_edge ? refined.significance : Number(),
319
+ stability: !variants.length ? undefined : pow(10, stability) - 1,
320
+ };
321
+ };
@@ -0,0 +1,11 @@
1
+ const aliasShifts = require('./alias_shifts');
2
+ const estimateFromOutputs = require('./estimate_from_outputs');
3
+ const priceFromOutputs = require('./price_from_outputs');
4
+ const roundAmounts = require('./round_amounts');
5
+
6
+ module.exports = {
7
+ aliasShifts,
8
+ estimateFromOutputs,
9
+ priceFromOutputs,
10
+ roundAmounts,
11
+ };
@@ -0,0 +1,86 @@
1
+ const {histogramPeak} = require('./../histogram');
2
+ const {impliedPricesHistogram} = require('./../histogram');
3
+ const {kernelWidth} = require('./../histogram');
4
+
5
+ const {ceil} = Math;
6
+ const defaultKernel = 8;
7
+ const exclusionMargin = 4;
8
+ const fineBinsPerDecade = 1000;
9
+ const {max} = Math;
10
+ const maxKernel = 12;
11
+ const {min} = Math;
12
+ const {pow} = Math;
13
+ const {sqrt} = Math;
14
+
15
+ /** Find the peak of round payments in a fine histogram centered on a price
16
+
17
+ {
18
+ amounts: [<Round USD Amount Number>]
19
+ outputs: [<Output Tokens Number>]
20
+ price: <Center USD Price Number>
21
+ reach: <Bins On Each Side of the Center Number>
22
+ weights: [<Round USD Amount Weight Number>]
23
+ }
24
+
25
+ @returns
26
+ {
27
+ dispersion: <Peak Width Beyond Smoothing As Price Fraction Number>
28
+ is_at_edge: <Peak Is At the Edge of the Histogram Bool>
29
+ noise: <Background Noise Number>
30
+ offset: <Peak Offset From Center In Bins Number>
31
+ payments: <Estimated Round USD Payments Count Number>
32
+ price: <Peak USD Price Number>
33
+ significance: <Payment Peak Beyond Noise In Standard Deviations Number>
34
+ spread: <Smoothed Peak Width At Half Height As a Fraction of Price Number>
35
+ }
36
+ */
37
+ module.exports = ({amounts, outputs, price, reach, weights}) => {
38
+ const {counts, weighted} = impliedPricesHistogram({
39
+ amounts,
40
+ outputs,
41
+ price,
42
+ reach,
43
+ weights,
44
+ bins_per_decade: fineBinsPerDecade,
45
+ });
46
+
47
+ const first = histogramPeak({
48
+ counts,
49
+ weighted,
50
+ exclude: defaultKernel + exclusionMargin,
51
+ kernel: defaultKernel,
52
+ });
53
+
54
+ // A wide peak gets wider smoothing, up to a limit
55
+ const wide = max(defaultKernel, ceil(first.width / 2));
56
+
57
+ const kernel = min(maxKernel, wide);
58
+
59
+ const resmoothing = {
60
+ counts,
61
+ kernel,
62
+ weighted,
63
+ exclude: kernel + exclusionMargin,
64
+ };
65
+
66
+ // The default kernel was already used to find the first peak
67
+ const peak = kernel === defaultKernel ? first : histogramPeak(resmoothing);
68
+
69
+ const offset = peak.peak + peak.delta - reach;
70
+
71
+ // The width the smoothing alone would give is taken out in quadrature
72
+ const smoothing = kernelWidth({half: kernel}).width;
73
+
74
+ const beyond = sqrt(max(Number(), peak.width ** 2 - smoothing ** 2));
75
+
76
+ return {
77
+ offset,
78
+ dispersion: pow(10, beyond / fineBinsPerDecade) - 1,
79
+ is_at_edge: peak.peak < kernel || peak.peak > reach * 2 - kernel,
80
+ noise: peak.noise,
81
+ payments: peak.payments,
82
+ price: price * pow(10, offset / fineBinsPerDecade),
83
+ significance: peak.significance,
84
+ spread: pow(10, peak.width / fineBinsPerDecade) - 1,
85
+ };
86
+ };