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