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