@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,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
|
+
};
|