const PATTERN = { FLUCTUATING: 0, LARGE_SPIKE: 1, DECREASING: 2, SMALL_SPIKE: 3, }; const PROBABILITY_MATRIX = { [PATTERN.FLUCTUATING]: { [PATTERN.FLUCTUATING]: 0.20, [PATTERN.LARGE_SPIKE]: 0.30, [PATTERN.DECREASING]: 0.15, [PATTERN.SMALL_SPIKE]: 0.35, }, [PATTERN.LARGE_SPIKE]: { [PATTERN.FLUCTUATING]: 0.50, [PATTERN.LARGE_SPIKE]: 0.05, [PATTERN.DECREASING]: 0.20, [PATTERN.SMALL_SPIKE]: 0.25, }, [PATTERN.DECREASING]: { [PATTERN.FLUCTUATING]: 0.25, [PATTERN.LARGE_SPIKE]: 0.45, [PATTERN.DECREASING]: 0.05, [PATTERN.SMALL_SPIKE]: 0.25, }, [PATTERN.SMALL_SPIKE]: { [PATTERN.FLUCTUATING]: 0.45, [PATTERN.LARGE_SPIKE]: 0.25, [PATTERN.DECREASING]: 0.15, [PATTERN.SMALL_SPIKE]: 0.15, }, }; const RATE_MULTIPLIER = 10000; function range_length(range) { return range[1] - range[0]; } function clamp(x, min, max) { return Math.min(Math.max(x, min), max); } function range_intersect(range1, range2) { if (range1[0] > range2[1] || range1[1] < range2[0]) { return null; } return [Math.max(range1[0], range2[0]), Math.min(range1[1], range2[1])]; } function range_intersect_length(range1, range2) { if (range1[0] > range2[1] || range1[1] < range2[0]) { return 0; } return range_length(range_intersect(range1, range2)); } /** * Accurately sums a list of floating point numbers. * See https://en.wikipedia.org/wiki/Kahan_summation_algorithm#Further_enhancements * for more information. * @param {number[]} input * @returns {number} The sum of the input. */ function float_sum(input) { // Uses the improved Kahan–Babuska algorithm introduced by Neumaier. let sum = 0; // The "lost bits" of sum. let c = 0; for (let i = 0; i < input.length; i++) { const cur = input[i]; const t = sum + cur; if (Math.abs(sum) >= Math.abs(cur)) { c += (sum - t) + cur; } else { c += (cur - t) + sum; } sum = t; } return sum + c; } /** * Accurately returns the prefix sum of a list of floating point numbers. * See https://en.wikipedia.org/wiki/Kahan_summation_algorithm#Further_enhancements * for more information. * @param {number[]} input * @returns {[number, number][]} The prefix sum of the input, such that * output[i] = [sum of first i integers, error of the sum]. * The "true" prefix sum is equal to the sum of the pair of numbers, but it is * explicitly returned as a pair of numbers to ensure that the error portion * isn't lost when subtracting prefix sums. */ function prefix_float_sum(input) { const prefix_sum = [[0, 0]]; let sum = 0; let c = 0; for (let i = 0; i < input.length; i++) { const cur = input[i]; const t = sum + cur; if (Math.abs(sum) >= Math.abs(cur)) { c += (sum - t) + cur; } else { c += (cur - t) + sum; } sum = t; prefix_sum.push([sum, c]); } return prefix_sum; } /* * Probability Density Function of rates. * Since the PDF is continuous*, we approximate it by a discrete probability function: * the value in range [x, x + 1) has a uniform probability * prob[x - value_start]; * * Note that we operate all rate on the (* RATE_MULTIPLIER) scale. * * (*): Well not really since it only takes values that "float" can represent in some form, but the * space is too large to compute directly in JS. */ class PDF { /** * Initialize a PDF in range [a, b], a and b can be non-integer. * if uniform is true, then initialize the probability to be uniform, else initialize to a * all-zero (invalid) PDF. * @param {number} a - Left end-point. * @param {number} b - Right end-point end-point. * @param {boolean} uniform - If true, initialise with the uniform distribution. */ constructor(a, b, uniform = true) { // We need to ensure that [a, b] is fully contained in [value_start, value_end]. /** @type {number} */ this.value_start = Math.floor(a); /** @type {number} */ this.value_end = Math.ceil(b); const range = [a, b]; const total_length = range_length(range); /** @type {number[]} */ this.prob = Array(this.value_end - this.value_start); if (uniform) { for (let i = 0; i < this.prob.length; i++) { this.prob[i] = range_intersect_length(this.range_of(i), range) / total_length; } } } /** * Calculates the interval represented by this.prob[idx] * @param {number} idx - The index of this.prob * @returns {[number, number]} The interval representing this.prob[idx]. */ range_of(idx) { // We intentionally include the right end-point of the range. // The probability of getting exactly an endpoint is zero, so we can assume // the "probability ranges" are "touching". return [this.value_start + idx, this.value_start + idx + 1]; } min_value() { return this.value_start; } max_value() { return this.value_end; } /** * @returns {number} The sum of probabilities before normalisation. */ normalize() { const total_probability = float_sum(this.prob); for (let i = 0; i < this.prob.length; i++) { this.prob[i] /= total_probability; } return total_probability; } /* * Limit the values to be in the range, and return the probability that the value was in this * range. */ range_limit(range) { let [start, end] = range; start = Math.max(start, this.min_value()); end = Math.min(end, this.max_value()); if (start >= end) { // Set this to invalid values this.value_start = this.value_end = 0; this.prob = []; return 0; } start = Math.floor(start); end = Math.ceil(end); const start_idx = start - this.value_start; const end_idx = end - this.value_start; for (let i = start_idx; i < end_idx; i++) { this.prob[i] *= range_intersect_length(this.range_of(i), range); } this.prob = this.prob.slice(start_idx, end_idx); this.value_start = start; this.value_end = end; // The probability that the value was in this range is equal to the total // sum of "un-normalised" values in the range. return this.normalize(); } /** * Subtract the PDF by a uniform distribution in [rate_decay_min, rate_decay_max] * * For simplicity, we assume that rate_decay_min and rate_decay_max are both integers. * @param {number} rate_decay_min * @param {number} rate_decay_max * @returns {void} */ decay(rate_decay_min, rate_decay_max) { // In case the arguments aren't integers, round them to the nearest integer. rate_decay_min = Math.round(rate_decay_min); rate_decay_max = Math.round(rate_decay_max); // The sum of this distribution with a uniform distribution. // Let's assume that both distributions start at 0 and X = this dist, // Y = uniform dist, and Z = X + Y. // Let's also assume that X is a "piecewise uniform" distribution, so // x(i) = this.prob[Math.floor(i)] - which matches our implementation. // We also know that y(i) = 1 / max(Y) - as we assume that min(Y) = 0. // In the end, we're interested in: // Pr(i <= Z < i+1) where i is an integer // = int. x(val) * Pr(i-val <= Y < i-val+1) dval from 0 to max(X) // = int. x(floor(val)) * Pr(i-val <= Y < i-val+1) dval from 0 to max(X) // = sum val from 0 to max(X)-1 // x(val) * f_i(val) / max(Y) // where f_i(val) = // 0.5 if i-val = 0 or max(Y), so val = i-max(Y) or i // 1.0 if 0 < i-val < max(Y), so i-max(Y) < val < i // as x(val) is "constant" for each integer step, so we can consider the // integral in integer steps. // = sum val from max(0, i-max(Y)) to min(max(X)-1, i) // x(val) * f_i(val) / max(Y) // for example, max(X)=1, max(Y)=10, i=5 // = sum val from max(0, 5-10)=0 to min(1-1, 5)=0 // x(val) * f_i(val) / max(Y) // = x(0) * 1 / 10 // Get a prefix sum / CDF of this so we can calculate sums in O(1). const prefix = prefix_float_sum(this.prob); const max_X = this.prob.length; const max_Y = rate_decay_max - rate_decay_min; const newProb = Array(this.prob.length + max_Y); for (let i = 0; i < newProb.length; i++) { // Note that left and right here are INCLUSIVE. const left = Math.max(0, i - max_Y); const right = Math.min(max_X - 1, i); // We want to sum, in total, prefix[right+1], -prefix[left], and subtract // the 0.5s if necessary. // This may involve numbers of differing magnitudes, so use the float sum // algorithm to sum these up. const numbers_to_sum = [ prefix[right + 1][0], prefix[right + 1][1], -prefix[left][0], -prefix[left][1], ]; if (left === i-max_Y) { // Need to halve the left endpoint. numbers_to_sum.push(-this.prob[left] / 2); } if (right === i) { // Need to halve the right endpoint. // It's guaranteed that we won't accidentally "halve" twice, // as that would require i-max_Y = i, so max_Y = 0 - which is // impossible. numbers_to_sum.push(-this.prob[right] / 2); } newProb[i] = float_sum(numbers_to_sum) / max_Y; } this.prob = newProb; this.value_start -= rate_decay_max; this.value_end -= rate_decay_min; // No need to normalise, as it is guaranteed that the sum of this.prob is 1. } } class Predictor { constructor(prices, first_buy, previous_pattern) { // The reverse-engineered code is not perfectly accurate, especially as it's not // 32-bit ARM floating point. So, be tolerant of slightly unexpected inputs this.fudge_factor = 0; this.prices = prices; this.first_buy = first_buy; this.previous_pattern = previous_pattern; } intceil(val) { return Math.trunc(val + 0.99999); } minimum_rate_from_given_and_base(given_price, buy_price) { return RATE_MULTIPLIER * (given_price - 0.99999) / buy_price; } maximum_rate_from_given_and_base(given_price, buy_price) { return RATE_MULTIPLIER * (given_price + 0.00001) / buy_price; } rate_range_from_given_and_base(given_price, buy_price) { return [ this.minimum_rate_from_given_and_base(given_price, buy_price), this.maximum_rate_from_given_and_base(given_price, buy_price) ]; } get_price(rate, basePrice) { return this.intceil(rate * basePrice / RATE_MULTIPLIER); } * multiply_generator_probability(generator, probability) { for (const it of generator) { yield {...it, probability: it.probability * probability}; } } /* * This corresponds to the code: * for (int i = start; i < start + length; i++) * { * sellPrices[work++] = * intceil(randfloat(rate_min / RATE_MULTIPLIER, rate_max / RATE_MULTIPLIER) * basePrice); * } * * Would return the conditional probability given the given_prices, and modify * the predicted_prices array. * If the given_prices won't match, returns 0. */ generate_individual_random_price( given_prices, predicted_prices, start, length, rate_min, rate_max) { rate_min *= RATE_MULTIPLIER; rate_max *= RATE_MULTIPLIER; const buy_price = given_prices[0]; const rate_range = [rate_min, rate_max]; let prob = 1; for (let i = start; i < start + length; i++) { let min_pred = this.get_price(rate_min, buy_price); let max_pred = this.get_price(rate_max, buy_price); if (!isNaN(given_prices[i])) { if (given_prices[i] < min_pred - this.fudge_factor || given_prices[i] > max_pred + this.fudge_factor) { // Given price is out of predicted range, so this is the wrong pattern return 0; } // TODO: How to deal with probability when there's fudge factor? // Clamp the value to be in range now so the probability won't be totally biased to fudged values. const real_rate_range = this.rate_range_from_given_and_base(clamp(given_prices[i], min_pred, max_pred), buy_price); prob *= range_intersect_length(rate_range, real_rate_range) / range_length(rate_range); min_pred = given_prices[i]; max_pred = given_prices[i]; } predicted_prices.push({ min: min_pred, max: max_pred, }); } return prob; } /* * This corresponds to the code: * rate = randfloat(start_rate_min, start_rate_max); * for (int i = start; i < start + length; i++) * { * sellPrices[work++] = intceil(rate * basePrice); * rate -= randfloat(rate_decay_min, rate_decay_max); * } * * Would return the conditional probability given the given_prices, and modify * the predicted_prices array. * If the given_prices won't match, returns 0. */ generate_decreasing_random_price( given_prices, predicted_prices, start, length, start_rate_min, start_rate_max, rate_decay_min, rate_decay_max) { start_rate_min *= RATE_MULTIPLIER; start_rate_max *= RATE_MULTIPLIER; rate_decay_min *= RATE_MULTIPLIER; rate_decay_max *= RATE_MULTIPLIER; const buy_price = given_prices[0]; let rate_pdf = new PDF(start_rate_min, start_rate_max); let prob = 1; for (let i = start; i < start + length; i++) { let min_pred = this.get_price(rate_pdf.min_value(), buy_price); let max_pred = this.get_price(rate_pdf.max_value(), buy_price); if (!isNaN(given_prices[i])) { if (given_prices[i] < min_pred - this.fudge_factor || given_prices[i] > max_pred + this.fudge_factor) { // Given price is out of predicted range, so this is the wrong pattern return 0; } // TODO: How to deal with probability when there's fudge factor? // Clamp the value to be in range now so the probability won't be totally biased to fudged values. const real_rate_range = this.rate_range_from_given_and_base(clamp(given_prices[i], min_pred, max_pred), buy_price); prob *= rate_pdf.range_limit(real_rate_range); if (prob == 0) { return 0; } min_pred = given_prices[i]; max_pred = given_prices[i]; } predicted_prices.push({ min: min_pred, max: max_pred, }); rate_pdf.decay(rate_decay_min, rate_decay_max); } return prob; } /* * This corresponds to the code: * rate = randfloat(rate_min, rate_max); * sellPrices[work++] = intceil(randfloat(rate_min, rate) * basePrice) - 1; * sellPrices[work++] = intceil(rate * basePrice); * sellPrices[work++] = intceil(randfloat(rate_min, rate) * basePrice) - 1; * * Would return the conditional probability given the given_prices, and modify * the predicted_prices array. * If the given_prices won't match, returns 0. */ generate_peak_price( given_prices, predicted_prices, start, rate_min, rate_max) { rate_min *= RATE_MULTIPLIER; rate_max *= RATE_MULTIPLIER; const buy_price = given_prices[0]; let prob = 1; let rate_range = [rate_min, rate_max]; // * Calculate the probability first. // Prob(middle_price) const middle_price = given_prices[start + 1]; if (!isNaN(middle_price)) { const min_pred = this.get_price(rate_min, buy_price); const max_pred = this.get_price(rate_max, buy_price); if (middle_price < min_pred - this.fudge_factor || middle_price > max_pred + this.fudge_factor) { // Given price is out of predicted range, so this is the wrong pattern return 0; } // TODO: How to deal with probability when there's fudge factor? // Clamp the value to be in range now so the probability won't be totally biased to fudged values. const real_rate_range = this.rate_range_from_given_and_base(clamp(middle_price, min_pred, max_pred), buy_price); prob *= range_intersect_length(rate_range, real_rate_range) / range_length(rate_range); if (prob == 0) { return 0; } rate_range = range_intersect(rate_range, real_rate_range); } const left_price = given_prices[start]; const right_price = given_prices[start + 2]; // Prob(left_price | middle_price), Prob(right_price | middle_price) // // A = rate_range[0], B = rate_range[1], C = rate_min, X = rate, Y = randfloat(rate_min, rate) // rate = randfloat(A, B); sellPrices[work++] = intceil(randfloat(C, rate) * basePrice) - 1; // // => X->U(A,B), Y->U(C,X), Y-C->U(0,X-C), Y-C->U(0,1)*(X-C), Y-C->U(0,1)*U(A-C,B-C), // let Z=Y-C, Z1=A-C, Z2=B-C, Z->U(0,1)*U(Z1,Z2) // Prob(Z<=t) = integral_{x=0}^{1} [min(t/x,Z2)-min(t/x,Z1)]/ (Z2-Z1) // let F(t, ZZ) = integral_{x=0}^{1} min(t/x, ZZ) // 1. if ZZ < t, then min(t/x, ZZ) = ZZ -> F(t, ZZ) = ZZ // 2. if ZZ >= t, then F(t, ZZ) = integral_{x=0}^{t/ZZ} ZZ + integral_{x=t/ZZ}^{1} t/x // = t - t log(t/ZZ) // Prob(Z<=t) = (F(t, Z2) - F(t, Z1)) / (Z2 - Z1) // Prob(Y<=t) = Prob(Z>=t-C) for (const price of [left_price, right_price]) { if (isNaN(price)) { continue; } const min_pred = this.get_price(rate_min, buy_price) - 1; const max_pred = this.get_price(rate_range[1], buy_price) - 1; if (price < min_pred - this.fudge_factor || price > max_pred + this.fudge_factor) { // Given price is out of predicted range, so this is the wrong pattern return 0; } // TODO: How to deal with probability when there's fudge factor? // Clamp the value to be in range now so the probability won't be totally biased to fudged values. const rate2_range = this.rate_range_from_given_and_base(clamp(price, min_pred, max_pred)+ 1, buy_price); const F = (t, ZZ) => { if (t <= 0) { return 0; } return ZZ < t ? ZZ : t - t * (Math.log(t) - Math.log(ZZ)); }; const [A, B] = rate_range; const C = rate_min; const Z1 = A - C; const Z2 = B - C; const PY = (t) => (F(t - C, Z2) - F(t - C, Z1)) / (Z2 - Z1); prob *= PY(rate2_range[1]) - PY(rate2_range[0]); if (prob == 0) { return 0; } } // * Then generate the real predicted range. // We're doing things in different order then how we calculate probability, // since forward prediction is more useful here. // // Main spike 1 let min_pred = this.get_price(rate_min, buy_price) - 1; let max_pred = this.get_price(rate_max, buy_price) - 1; if (!isNaN(given_prices[start])) { min_pred = given_prices[start]; max_pred = given_prices[start]; } predicted_prices.push({ min: min_pred, max: max_pred, }); // Main spike 2 min_pred = predicted_prices[start].min; max_pred = this.get_price(rate_max, buy_price); if (!isNaN(given_prices[start + 1])) { min_pred = given_prices[start + 1]; max_pred = given_prices[start + 1]; } predicted_prices.push({ min: min_pred, max: max_pred, }); // Main spike 3 min_pred = this.get_price(rate_min, buy_price) - 1; max_pred = predicted_prices[start + 1].max - 1; if (!isNaN(given_prices[start + 2])) { min_pred = given_prices[start + 2]; max_pred = given_prices[start + 2]; } predicted_prices.push({ min: min_pred, max: max_pred, }); return prob; } * generate_pattern_0_with_lengths( given_prices, high_phase_1_len, dec_phase_1_len, high_phase_2_len, dec_phase_2_len, high_phase_3_len) { /* // PATTERN 0: high, decreasing, high, decreasing, high work = 2; // high phase 1 for (int i = 0; i < hiPhaseLen1; i++) { sellPrices[work++] = intceil(randfloat(0.9, 1.4) * basePrice); } // decreasing phase 1 rate = randfloat(0.8, 0.6); for (int i = 0; i < decPhaseLen1; i++) { sellPrices[work++] = intceil(rate * basePrice); rate -= 0.04; rate -= randfloat(0, 0.06); } // high phase 2 for (int i = 0; i < (hiPhaseLen2and3 - hiPhaseLen3); i++) { sellPrices[work++] = intceil(randfloat(0.9, 1.4) * basePrice); } // decreasing phase 2 rate = randfloat(0.8, 0.6); for (int i = 0; i < decPhaseLen2; i++) { sellPrices[work++] = intceil(rate * basePrice); rate -= 0.04; rate -= randfloat(0, 0.06); } // high phase 3 for (int i = 0; i < hiPhaseLen3; i++) { sellPrices[work++] = intceil(randfloat(0.9, 1.4) * basePrice); } */ const buy_price = given_prices[0]; const predicted_prices = [ { min: buy_price, max: buy_price, }, { min: buy_price, max: buy_price, }, ]; let probability = 1; // High Phase 1 probability *= this.generate_individual_random_price( given_prices, predicted_prices, 2, high_phase_1_len, 0.9, 1.4); if (probability == 0) { return; } // Dec Phase 1 probability *= this.generate_decreasing_random_price( given_prices, predicted_prices, 2 + high_phase_1_len, dec_phase_1_len, 0.6, 0.8, 0.04, 0.1); if (probability == 0) { return; } // High Phase 2 probability *= this.generate_individual_random_price(given_prices, predicted_prices, 2 + high_phase_1_len + dec_phase_1_len, high_phase_2_len, 0.9, 1.4); if (probability == 0) { return; } // Dec Phase 2 probability *= this.generate_decreasing_random_price( given_prices, predicted_prices, 2 + high_phase_1_len + dec_phase_1_len + high_phase_2_len, dec_phase_2_len, 0.6, 0.8, 0.04, 0.1); if (probability == 0) { return; } // High Phase 3 if (2 + high_phase_1_len + dec_phase_1_len + high_phase_2_len + dec_phase_2_len + high_phase_3_len != 14) { throw new Error("Phase lengths don't add up"); } const prev_length = 2 + high_phase_1_len + dec_phase_1_len + high_phase_2_len + dec_phase_2_len; probability *= this.generate_individual_random_price( given_prices, predicted_prices, prev_length, 14 - prev_length, 0.9, 1.4); if (probability == 0) { return; } yield { pattern_number: 0, prices: predicted_prices, probability, }; } * generate_pattern_0(given_prices) { /* decPhaseLen1 = randbool() ? 3 : 2; decPhaseLen2 = 5 - decPhaseLen1; hiPhaseLen1 = randint(0, 6); hiPhaseLen2and3 = 7 - hiPhaseLen1; hiPhaseLen3 = randint(0, hiPhaseLen2and3 - 1); */ for (var dec_phase_1_len = 2; dec_phase_1_len < 4; dec_phase_1_len++) { for (var high_phase_1_len = 0; high_phase_1_len < 7; high_phase_1_len++) { for (var high_phase_3_len = 0; high_phase_3_len < (7 - high_phase_1_len - 1 + 1); high_phase_3_len++) { yield* this.multiply_generator_probability( this.generate_pattern_0_with_lengths(given_prices, high_phase_1_len, dec_phase_1_len, 7 - high_phase_1_len - high_phase_3_len, 5 - dec_phase_1_len, high_phase_3_len), 1 / (4 - 2) / 7 / (7 - high_phase_1_len)); } } } } * generate_pattern_1_with_peak(given_prices, peak_start) { /* // PATTERN 1: decreasing middle, high spike, random low peakStart = randint(3, 9); rate = randfloat(0.9, 0.85); for (work = 2; work < peakStart; work++) { sellPrices[work] = intceil(rate * basePrice); rate -= 0.03; rate -= randfloat(0, 0.02); } sellPrices[work++] = intceil(randfloat(0.9, 1.4) * basePrice); sellPrices[work++] = intceil(randfloat(1.4, 2.0) * basePrice); sellPrices[work++] = intceil(randfloat(2.0, 6.0) * basePrice); sellPrices[work++] = intceil(randfloat(1.4, 2.0) * basePrice); sellPrices[work++] = intceil(randfloat(0.9, 1.4) * basePrice); for (; work < 14; work++) { sellPrices[work] = intceil(randfloat(0.4, 0.9) * basePrice); } */ const buy_price = given_prices[0]; const predicted_prices = [ { min: buy_price, max: buy_price, }, { min: buy_price, max: buy_price, }, ]; let probability = 1; probability *= this.generate_decreasing_random_price( given_prices, predicted_prices, 2, peak_start - 2, 0.85, 0.9, 0.03, 0.05); if (probability == 0) { return; } // Now each day is independent of next let min_randoms = [0.9, 1.4, 2.0, 1.4, 0.9, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4]; let max_randoms = [1.4, 2.0, 6.0, 2.0, 1.4, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9]; for (let i = peak_start; i < 14; i++) { probability *= this.generate_individual_random_price( given_prices, predicted_prices, i, 1, min_randoms[i - peak_start], max_randoms[i - peak_start]); if (probability == 0) { return; } } yield { pattern_number: 1, prices: predicted_prices, probability, }; } * generate_pattern_1(given_prices) { for (var peak_start = 3; peak_start < 10; peak_start++) { yield* this.multiply_generator_probability(this.generate_pattern_1_with_peak(given_prices, peak_start), 1 / (10 - 3)); } } * generate_pattern_2(given_prices) { /* // PATTERN 2: consistently decreasing rate = 0.9; rate -= randfloat(0, 0.05); for (work = 2; work < 14; work++) { sellPrices[work] = intceil(rate * basePrice); rate -= 0.03; rate -= randfloat(0, 0.02); } break; */ const buy_price = given_prices[0]; const predicted_prices = [ { min: buy_price, max: buy_price, }, { min: buy_price, max: buy_price, }, ]; let probability = 1; probability *= this.generate_decreasing_random_price( given_prices, predicted_prices, 2, 14 - 2, 0.85, 0.9, 0.03, 0.05); if (probability == 0) { return; } yield { pattern_number: 2, prices: predicted_prices, probability, }; } * generate_pattern_3_with_peak(given_prices, peak_start) { /* // PATTERN 3: decreasing, spike, decreasing peakStart = randint(2, 9); // decreasing phase before the peak rate = randfloat(0.9, 0.4); for (work = 2; work < peakStart; work++) { sellPrices[work] = intceil(rate * basePrice); rate -= 0.03; rate -= randfloat(0, 0.02); } sellPrices[work++] = intceil(randfloat(0.9, 1.4) * (float)basePrice); sellPrices[work++] = intceil(randfloat(0.9, 1.4) * basePrice); rate = randfloat(1.4, 2.0); sellPrices[work++] = intceil(randfloat(1.4, rate) * basePrice) - 1; sellPrices[work++] = intceil(rate * basePrice); sellPrices[work++] = intceil(randfloat(1.4, rate) * basePrice) - 1; // decreasing phase after the peak if (work < 14) { rate = randfloat(0.9, 0.4); for (; work < 14; work++) { sellPrices[work] = intceil(rate * basePrice); rate -= 0.03; rate -= randfloat(0, 0.02); } } */ const buy_price = given_prices[0]; const predicted_prices = [ { min: buy_price, max: buy_price, }, { min: buy_price, max: buy_price, }, ]; let probability = 1; probability *= this.generate_decreasing_random_price( given_prices, predicted_prices, 2, peak_start - 2, 0.4, 0.9, 0.03, 0.05); if (probability == 0) { return; } // The peak probability *= this.generate_individual_random_price( given_prices, predicted_prices, peak_start, 2, 0.9, 1.4); if (probability == 0) { return; } probability *= this.generate_peak_price( given_prices, predicted_prices, peak_start + 2, 1.4, 2.0); if (probability == 0) { return; } if (peak_start + 5 < 14) { probability *= this.generate_decreasing_random_price( given_prices, predicted_prices, peak_start + 5, 14 - (peak_start + 5), 0.4, 0.9, 0.03, 0.05); if (probability == 0) { return; } } yield { pattern_number: 3, prices: predicted_prices, probability, }; } * generate_pattern_3(given_prices) { for (let peak_start = 2; peak_start < 10; peak_start++) { yield* this.multiply_generator_probability(this.generate_pattern_3_with_peak(given_prices, peak_start), 1 / (10 - 2)); } } get_transition_probability(previous_pattern) { if (typeof previous_pattern === 'undefined' || Number.isNaN(previous_pattern) || previous_pattern === null || previous_pattern < 0 || previous_pattern > 3) { // Use the steady state probabilities of PROBABILITY_MATRIX if we don't // know what the previous pattern was. // See https://github.com/mikebryant/ac-nh-turnip-prices/issues/68 // and https://github.com/mikebryant/ac-nh-turnip-prices/pull/90 // for more information. return [4530/13082, 3236/13082, 1931/13082, 3385/13082]; } return PROBABILITY_MATRIX[previous_pattern]; } * generate_all_patterns(sell_prices, previous_pattern) { const generate_pattern_fns = [this.generate_pattern_0, this.generate_pattern_1, this.generate_pattern_2, this.generate_pattern_3]; const transition_probability = this.get_transition_probability(previous_pattern); for (let i = 0; i < 4; i++) { yield* this.multiply_generator_probability(generate_pattern_fns[i].bind(this)(sell_prices), transition_probability[i]); } } * generate_possibilities(sell_prices, first_buy, previous_pattern) { if (first_buy || isNaN(sell_prices[0])) { for (var buy_price = 90; buy_price <= 110; buy_price++) { const temp_sell_prices = sell_prices.slice(); temp_sell_prices[0] = temp_sell_prices[1] = buy_price; if (first_buy) { yield* this.generate_pattern_3(temp_sell_prices); } else { // All buy prices are equal probability and we're at the outmost layer, // so don't need to multiply_generator_probability here. yield* this.generate_all_patterns(temp_sell_prices, previous_pattern); } } } else { yield* this.generate_all_patterns(sell_prices, previous_pattern); } } analyze_possibilities() { const sell_prices = this.prices; const first_buy = this.first_buy; const previous_pattern = this.previous_pattern; let generated_possibilities = []; for (let i = 0; i < 6; i++) { this.fudge_factor = i; generated_possibilities = Array.from(this.generate_possibilities(sell_prices, first_buy, previous_pattern)); if (generated_possibilities.length > 0) { console.log("Generated possibilities using fudge factor %d: ", i, generated_possibilities); break; } } const total_probability = generated_possibilities.reduce((acc, it) => acc + it.probability, 0); for (const it of generated_possibilities) { it.probability /= total_probability; } for (let poss of generated_possibilities) { var weekMins = []; var weekMaxes = []; for (let day of poss.prices.slice(2)) { // Check for a future date by checking for a range of prices if(day.min !== day.max){ weekMins.push(day.min); weekMaxes.push(day.max); } else { // If we find a set price after one or more ranged prices, the user has missed a day. Discard that data and start again. weekMins = []; weekMaxes = []; } } if (!weekMins.length && !weekMaxes.length) { weekMins.push(poss.prices[poss.prices.length -1].min); weekMaxes.push(poss.prices[poss.prices.length -1].max); } poss.weekGuaranteedMinimum = Math.max(...weekMins); poss.weekMax = Math.max(...weekMaxes); } let category_totals = {}; for (let i of [0, 1, 2, 3]) { category_totals[i] = generated_possibilities .filter(value => value.pattern_number == i) .map(value => value.probability) .reduce((previous, current) => previous + current, 0); } for (let pos of generated_possibilities) { pos.category_total_probability = category_totals[pos.pattern_number]; } generated_possibilities.sort((a, b) => { return b.category_total_probability - a.category_total_probability || b.probability - a.probability; }); let global_min_max = []; for (let day = 0; day < 14; day++) { const prices = { min: 999, max: 0, }; for (let poss of generated_possibilities) { if (poss.prices[day].min < prices.min) { prices.min = poss.prices[day].min; } if (poss.prices[day].max > prices.max) { prices.max = poss.prices[day].max; } } global_min_max.push(prices); } generated_possibilities.unshift({ pattern_number: 4, prices: global_min_max, weekGuaranteedMinimum: Math.min(...generated_possibilities.map(poss => poss.weekGuaranteedMinimum)), weekMax: Math.max(...generated_possibilities.map(poss => poss.weekMax)) }); return generated_possibilities; } }