| " + i18next.t("patterns." + pat_desc[poss.pattern_number]) + " | ";
316 | const style_price = buy_price || poss.prices[0].min;
317 | if (previous_pattern_number != poss.pattern_number) {
318 | previous_pattern_number = poss.pattern_number;
319 | pattern_count = analyzed_possibilities
320 | .filter(val => val.pattern_number == poss.pattern_number)
321 | .length;
322 | out_line += `${displayPercentage(poss.category_total_probability)} | `;
323 | }
324 | out_line += `${displayPercentage(poss.probability)} | `;
325 | for (let day of poss.prices.slice(2)) {
326 | let price_class = getPriceClass(style_price, day.max);
327 | if (day.min !== day.max) {
328 | out_line += `${day.min} ${i18next.t("output.to")} ${day.max} | `;
329 | } else {
330 | out_line += `${day.min} | `;
331 | }
332 | }
333 |
334 | var min_class = getPriceClass(style_price, poss.weekGuaranteedMinimum);
335 | var max_class = getPriceClass(style_price, poss.weekMax);
336 | out_line += `${poss.weekGuaranteedMinimum} | ${poss.weekMax} |
`;
337 | output_possibilities += out_line;
338 | }
339 |
340 | $("#output").html(output_possibilities);
341 |
342 | update_chart(data, analyzed_possibilities);
343 | };
344 |
345 | const generatePermalink = function (buy_price, sell_prices, first_buy, previous_pattern) {
346 | let searchParams = new URLSearchParams();
347 | let pricesParam = buy_price ? buy_price.toString() : '';
348 |
349 | if (!isEmpty(sell_prices)) {
350 | const filtered = sell_prices.map(price => isNaN(price) ? '' : price).join('.');
351 | pricesParam = pricesParam.concat('.', filtered);
352 | }
353 |
354 | if (pricesParam) {
355 | searchParams.append('prices', pricesParam);
356 | }
357 |
358 | if (first_buy) {
359 | searchParams.append('first', true);
360 | }
361 |
362 | if (previous_pattern !== -1) {
363 | searchParams.append('pattern', previous_pattern);
364 | }
365 |
366 | return searchParams.toString() && window.location.origin.concat('?', searchParams.toString());
367 | };
368 |
369 | const copyPermalink = function () {
370 | let text = permalink_input[0];
371 |
372 | permalink_input.show();
373 | text.select();
374 | text.setSelectionRange(0, 99999); /* for mobile devices */
375 |
376 | document.execCommand('copy');
377 | permalink_input.hide();
378 |
379 | flashMessage(i18next.t("prices.permalink-copied"));
380 | };
381 |
382 | const flashMessage = function(message) {
383 | snackbar.text(message);
384 | snackbar.addClass('show');
385 |
386 | setTimeout(function () {
387 | snackbar.removeClass('show');
388 | snackbar.text('');
389 | }, 3000);
390 | };
391 |
392 | const update = function () {
393 | if(!state.initialized){
394 | console.log('update function called before initial data load');
395 | // calls to update before the previous data has been initialized / loaded will reset the data.
396 | return;
397 | }
398 | const sell_prices = getSellPrices();
399 | const buy_price = parseInt(buy_input.val());
400 | const first_buy = getCheckedRadio(first_buy_radios) == 'true';
401 | const previous_pattern = parseInt(getCheckedRadio(previous_pattern_radios));
402 |
403 | const permalink = generatePermalink(buy_price, sell_prices, first_buy, previous_pattern);
404 | if (permalink) {
405 | permalink_button.show();
406 | } else {
407 | permalink_button.hide();
408 | }
409 | permalink_input.val(permalink);
410 |
411 | const prices = [buy_price, buy_price, ...sell_prices];
412 |
413 | if (!window.populated_from_query) {
414 | updateLocalStorage(prices, first_buy, previous_pattern);
415 | }
416 |
417 | calculateOutput(prices, first_buy, previous_pattern);
418 | };
419 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Some text some message...
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
360 |
361 |
362 |
--------------------------------------------------------------------------------
/css/styles.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Raleway:wght@800&family=Varela+Round&display=swap');
2 |
3 | /* - Variables - */
4 |
5 | :root {
6 | --color-blue: #0AB5CD;
7 | --color-light-blue: #5ECEDB;
8 |
9 | --bg-color: #DEF2D9;
10 | --bg-dot-color: #FFF;
11 |
12 | --shadow-3: rgba(0, 0, 0, 0.03);
13 | --shadow-5: rgba(0, 0, 0, 0.05);
14 | --shadow-6: rgba(0, 0, 0, 0.06);
15 | --shadow-8: rgba(0, 0, 0, 0.08);
16 | --shadow-9: rgba(0, 0, 0, 0.09);
17 | --shadow-10: rgba(0, 0, 0, 0.10);
18 | --shadow-15: rgba(0, 0, 0, 0.16);
19 | --shadow-16: rgba(0, 0, 0, 0.16);
20 | --shadow-20: rgba(0, 0, 0, 0.20);
21 |
22 | --center-bg-color: #FFF;
23 |
24 | --wave-1: rgba(255, 255, 255, 0);
25 | --wave-2: rgba(255, 255, 255, 0.2);
26 | --wave-3: rgba(255, 255, 255, 0.4);
27 | --wave-4: rgba(255, 255, 255, 0.6);
28 |
29 | --nook-phone-bg-color: #F5F8FF;
30 | --nook-phone-text-color: #686868;
31 |
32 | --dialog-bg-color: #FFFAE5;
33 | --dialog-text-color: #837865;
34 |
35 | --dialog-name-bg-color: #FF9A40;
36 | --dialog-name-text-color: #BA3B1F;
37 |
38 | --chart-fill-color: var(--bg-color);
39 | --chart-line-color: rgba(0, 0, 0, 0.1);
40 | --chart-point-color: rgba(0, 0, 0, 0.1);
41 |
42 | --select-text-color: var(--dialog-text-color);
43 | --select-border-color: var(--bg-color);
44 | --select-bg-color-hover: #EBFEFD;
45 |
46 | --italic-color: #AAA;
47 |
48 | --form-h6-text-color: #845E44;
49 |
50 | --radio-hover-bg-color: var(--nook-phone-bg-color);
51 | --radio-checked-text-color: #FFF;
52 |
53 | --input-bg-color: #F3F3F3;
54 | --input-focus-bg-color: white;
55 | --input-focus-text-color: var(--color-blue);
56 |
57 | --input-now-bg-color: var(--dialog-name-bg-color);
58 | --input-now-text-color: var(--dialog-name-text-color);
59 |
60 | --button-text-color: var(--nook-phone-text-color);
61 | --button-reset-text-color: #E45B5B;
62 |
63 | --table-range0: hsl(140, 80%, 85%);
64 | --table-range1: hsl(90, 80%, 85%);
65 | --table-range2: hsl(60, 80%, 85%);
66 | --table-range3: hsl(30, 80%, 85%);
67 | --table-range4: hsl(0, 80%, 85%);
68 | }
69 |
70 | [data-theme="dark"] {
71 | --bg-color: #1A1A1A;
72 | --bg-dot-color: #222;
73 |
74 | --shadow-3: rgba(255, 255, 255, 0.03);
75 | --shadow-15: rgba(255, 255, 255, 0.03);
76 |
77 | --center-bg-color: #101010;
78 |
79 | --wave-1: rgba(16, 16, 16, 0);
80 | --wave-2: rgba(16, 16, 16, 0.2);
81 | --wave-3: rgba(16, 16, 16, 0.4);
82 | --wave-4: rgba(16, 16, 16, 0.6);
83 |
84 | --nook-phone-bg-color: #000F33;
85 | --nook-phone-text-color: #CCC;
86 |
87 | --dialog-bg-color: #252422;
88 | --dialog-text-color: #BCB5A9;
89 |
90 | --dialog-name-bg-color: #BA3B1F;
91 | --dialog-name-text-color: #FF9A40;
92 |
93 | --chart-fill-color: #2D5F21;
94 | --chart-line-color: rgba(200, 200, 200, 0.4);
95 | --chart-point-color: rgba(200, 200, 200, 0.6);
96 |
97 | --select-text-color: #837865;
98 | --select-border-color: var(--bg-color);
99 | --select-bg-color-hover: #EBFEFD;
100 |
101 | --italic-color: #666;
102 |
103 | --form-h6-text-color: #E18B51;
104 |
105 | --radio-hover-bg-color: #00174D;
106 | --radio-checked-text-color: #FFF;
107 |
108 | --input-bg-color: #333;
109 | --input-focus-bg-color: #999;
110 | --input-focus-text-color: var(--radio-hover-bg-color);
111 |
112 | --button-text-color: var(--nook-phone-text-color);
113 | --button-reset-text-color: #E45B5B;
114 |
115 | --table-range0: hsl(140, 80%, 27%);
116 | --table-range1: hsl(90, 80%, 20%);
117 | --table-range2: hsl(60, 80%, 20%);
118 | --table-range3: hsl(30, 80%, 20%);
119 | --table-range4: hsl(0, 80%, 22%);
120 | }
121 |
122 | /* - Global Styles - */
123 |
124 | html {
125 | font-size: 14px;
126 | background: var(--bg-color);
127 | background-image:
128 | radial-gradient(var(--bg-dot-color) 20%, transparent 0),
129 | radial-gradient(var(--bg-dot-color) 20%, transparent 0);
130 | background-size: 30px 30px;
131 | background-position: 0 0, 15px 15px;
132 | }
133 |
134 | body {
135 | display: flex;
136 | flex-direction: column;
137 | align-items: center;
138 | justify-content: center;
139 | font-family: 'Varela Round', sans-serif;
140 | }
141 |
142 | h1 {
143 | text-align: center;
144 | font-size: 1.8rem;
145 | }
146 |
147 | h2 {
148 | text-align: center;
149 | font-size: 1.6rem;
150 | }
151 |
152 | .nook-phone {
153 | width: 100%;
154 | max-width: 1400px;
155 | box-sizing: border-box;
156 | margin: 16px auto;
157 | border-radius: 40px;
158 | padding: 16px 0px;
159 | padding-bottom: 16px;
160 | background: var(--nook-phone-bg-color);
161 | color: var(--nook-phone-text-color);
162 | overflow: hidden;
163 | box-shadow: 0 1px 3px var(--shadow-6), 0 1px 2px var(--shadow-8);
164 | }
165 |
166 | .nook-phone-center {
167 | background: var(--center-bg-color);
168 | display: flex;
169 | flex-direction: column;
170 | align-items: center;
171 | }
172 |
173 | .dialog-box {
174 | background: var(--dialog-bg-color);
175 | box-sizing: border-box;
176 | padding: 16px 24px;
177 | margin: 32px auto;
178 | position: relative;
179 | border-radius: 40px;
180 | max-width: 800px;
181 | box-shadow: 0 1px 3px var(--shadow-6), 0 1px 2px var(--shadow-8);
182 | }
183 |
184 | .dialog-box-option {
185 | text-align: center;
186 | }
187 |
188 | .dialog-box-option p,
189 | .dialog-box-option select {
190 | display: inline;
191 | }
192 |
193 | .dialog-box-option select {
194 | font-size: 1rem;
195 | padding: 4px;
196 | font-weight: bold;
197 | border-radius: 4px;
198 | border-color: var(--select-border-color);
199 | color: var(--select-text-color);
200 | cursor: pointer;
201 | transition: 0.2s all;
202 | }
203 |
204 | .dialog-box-option select:hover {
205 | background-color: var(--select-bg-color-hover);
206 | border-color: var(--color-light-blue);
207 | box-shadow: 0 2px 4px var(--shadow-16);
208 | }
209 |
210 | .dialog-box-option select:focus {
211 | outline: none;
212 | }
213 |
214 | .dialog-box p,
215 | .dialog-box label {
216 | font-family: 'Raleway', sans-serif;
217 | font-weight: 800;
218 | font-size: 1rem;
219 | color: var(--dialog-text-color);
220 | letter-spacing: 0.2px;
221 | line-height: 1.8rem;
222 | }
223 |
224 | .dialog-box b,
225 | .dialog-box a {
226 | color: var(--color-blue);
227 | transition: 0.2s all;
228 | }
229 |
230 | .dialog-box i {
231 | font-style: normal;
232 | color: var(--italic-color);
233 | }
234 |
235 | .dialog-box a:hover {
236 | color: var(--color-light-blue);
237 | }
238 |
239 | .dialog-box .dialog-box__name {
240 | position: absolute;
241 | left: 16px;
242 | top: -28px;
243 | font-size: 1rem;
244 | color: var(--dialog-name-text-color);
245 | padding: 4px 16px;
246 | background: var(--dialog-name-bg-color);
247 | border-radius: 40px;
248 | }
249 |
250 | .dialog-box.error {
251 | display: none;
252 | }
253 |
254 | .input__form {
255 | background: var(--center-bg-color);
256 | display: flex;
257 | flex-direction: column;
258 | padding: 16px;
259 | align-items: center;
260 | }
261 |
262 | .form__row {
263 | display: flex;
264 | flex-wrap: wrap;
265 | margin-bottom: 16px;
266 | justify-content: center;
267 | align-items: center;
268 | }
269 |
270 | .form__row h6 {
271 | width: 100%;
272 | display: block;
273 | font-weight: 800;
274 | font-size: 1.25rem;
275 | margin: 8px auto;
276 | color: var(--form-h6-text-color);
277 | text-align: center;
278 | }
279 |
280 | .form__flex-wrap {
281 | margin-top: 8px;
282 | display: flex;
283 | flex-wrap: wrap;
284 | width: 100%;
285 | max-width: 1080px;
286 | justify-content: center;
287 | }
288 |
289 | .input__group {
290 | display: flex;
291 | flex-direction: column;
292 | margin: 8px;
293 | align-items: center;
294 | }
295 |
296 | .input__group label {
297 | font-size: 1rem;
298 | font-weight: bold;
299 | margin-bottom: 8px;
300 | opacity: 0.7;
301 | text-align: center;
302 | }
303 |
304 | .form__flex-wrap .input__group label {
305 | margin-left: 0px;
306 | margin-bottom: 8px;
307 | }
308 |
309 | .input__form i {
310 | text-align: center;
311 | display: block;
312 | font-style: normal;
313 | color: var(--italic-color);
314 | font-size: 0.9rem;
315 | margin: 8px auto;
316 | }
317 |
318 | .input__form>.form__row input {
319 | margin: 0px auto;
320 | }
321 |
322 | input {
323 | border: 0px solid white;
324 | border-radius: 40px;
325 | padding: 8px 16px;
326 | font-size: 1.25rem;
327 | font-family: inherit;
328 | color: inherit;
329 | font-weight: bold;
330 | transition: 0.2s all;
331 | margin: 8px 0px;
332 | }
333 |
334 | input[type=number]:placeholder-shown {
335 | background: var(--input-bg-color);
336 | }
337 |
338 | input[type=number]:not(:placeholder-shown) {
339 | background: transparent;
340 | color: var(--color-blue);
341 | }
342 |
343 | input[type=number]:placeholder-shown:hover {
344 | cursor: pointer;
345 | background: var(--radio-hover-bg-color);
346 | transform: scale(1.1);
347 | box-shadow: 0 1px 6px var(--shadow-5), 0 3px 6px var(--shadow-9);
348 | }
349 |
350 | input[type=number]:focus {
351 | outline: none;
352 | transform: scale(1.1);
353 | color: var(--input-focus-text-color);
354 | background: var(--input-focus-bg-color);
355 | box-shadow: 0 1px 6px var(--shadow-5), 0 3px 6px var(--shadow-9);
356 | }
357 |
358 | input[type=number]:focus::placeholder {
359 | opacity: 0;
360 | }
361 |
362 | input[type=number] {
363 | width: 60px;
364 | text-align: center;
365 | }
366 |
367 | input[type=number]:disabled {
368 | background: inherit;
369 | }
370 |
371 | input[type=number]:disabled:hover {
372 | box-shadow: none;
373 | transform: none;
374 | cursor: default;
375 | }
376 |
377 | input::-webkit-outer-spin-button,
378 | input::-webkit-inner-spin-button {
379 | -webkit-appearance: none;
380 | margin: 0;
381 | }
382 |
383 | input[type=number] {
384 | -moz-appearance: textfield;
385 | }
386 |
387 | .input__radio-buttons {
388 | display: flex;
389 | flex-wrap: wrap;
390 | justify-content: center;
391 | margin-top: 8px;
392 | }
393 |
394 | .input__radio-buttons input[type=radio] {
395 | display: none;
396 | }
397 |
398 | .input__radio-buttons input[type="radio"]+label {
399 | opacity: 1;
400 | border: none;
401 | border-radius: 40px;
402 | background: var(--input-bg-color);
403 | padding: 8px 16px;
404 | font-size: 1.25rem;
405 | font-family: inherit;
406 | font-weight: bold;
407 | transition: 0.2s all;
408 | margin: 8px;
409 | }
410 |
411 | .input__radio-buttons input[type="radio"]:not(:checked)+label:hover {
412 | cursor: pointer;
413 | background: var(--radio-hover-bg-color);
414 | transform: scale(1.1);
415 | box-shadow: 0 1px 6px var(--shadow-5), 0 3px 6px var(--shadow-9);
416 | }
417 |
418 | .input__radio-buttons input[type="radio"]:checked+label {
419 | background: var(--color-blue);
420 | color: var(--radio-checked-text-color);
421 | }
422 |
423 | input[class=now]:placeholder-shown {
424 | background: var(--input-now-bg-color);
425 | }
426 |
427 | input[class=now]:placeholder-shown::placeholder {
428 | color: var(--input-now-text-color);
429 | }
430 |
431 | .button {
432 | color: var(--button-text-color);
433 | font-family: inherit;
434 | font-weight: bold;
435 | padding: 8px 16px;
436 | border-width: 0px;
437 | border-radius: 40px;
438 | background: var(--input-bg-color);
439 | font-size: 1.2rem;
440 | transition: 0.2s all;
441 | position: relative;
442 | margin: 16px auto;
443 | }
444 |
445 | .button:hover {
446 | transform: scale(1.1);
447 | cursor: pointer;
448 | background: var(--radio-hover-bg-color);
449 | opacity: 1;
450 | box-shadow: 0 1px 6px var(--shadow-5), 0 3px 6px var(--shadow-9);
451 | }
452 |
453 | .button.button--reset {
454 | color: var(--button-reset-text-color);
455 | }
456 |
457 | .table-wrapper {
458 | display: inline-block;
459 | max-width: 98%;
460 | padding: 16px;
461 | margin: 0px auto;
462 | box-sizing: border-box;
463 | overflow-x: auto;
464 | scrollbar-width: thin;
465 | }
466 |
467 | @media only screen and (max-width: 1440px) and (pointer: fine) {
468 | .table-wrapper {
469 | max-height: calc(75vh - 40px);
470 | }
471 | }
472 |
473 | .table-wrapper::-webkit-scrollbar {
474 | height: 8px;
475 | width: 5px;
476 | }
477 |
478 | .table-wrapper::-webkit-scrollbar-track {
479 | height: 8px;
480 | width: 5px;
481 | box-shadow: inset 0 0 6px var(--shadow-20);
482 | -webkit-box-shadow: inset 0 0 6px var(--shadow-20);
483 | }
484 |
485 | .table-wrapper::-webkit-scrollbar-thumb {
486 | height: 8px;
487 | width: 5px;
488 | background: var(--shadow-20);
489 | box-shadow: inset 0 0 6px var(--shadow-20);
490 | -webkit-box-shadow: inset 0 0 6px var(--shadow-10);
491 | }
492 |
493 | .table-wrapper::-webkit-scrollbar-thumb:window-inactive {
494 | height: 8px;
495 | width: 5px;
496 | background: var(--shadow-20);
497 | }
498 |
499 | #turnipTable {
500 | border-collapse: collapse;
501 | }
502 |
503 | #turnipTable th div:nth-of-type(1) {
504 | margin-bottom: 2px;
505 | }
506 |
507 | #turnipTable th div:nth-of-type(2) {
508 | display: flex;
509 | justify-content: space-around;
510 | opacity: 0.4;
511 | }
512 |
513 | #turnipTable td {
514 | white-space: nowrap;
515 | max-width: 150px;
516 | padding: 6px 4px;
517 | text-align: center;
518 | border-right: 1px solid var(--shadow-3);
519 | border-bottom: 1px solid var(--shadow-15);
520 | }
521 |
522 | #turnipTable tbody tr {
523 | opacity: 0.8;
524 | }
525 |
526 | #turnipTable tbody tr:hover {
527 | cursor: default;
528 | opacity: 1;
529 | }
530 |
531 | #turnipTable .table-pattern {
532 | white-space: nowrap;
533 | }
534 |
535 | #turnipTable td.range4 {
536 | background-color: var(--table-range4);
537 | }
538 |
539 | #turnipTable td.range3{
540 | background-color: var(--table-range3);
541 | }
542 |
543 | #turnipTable td.range2 {
544 | background-color: var(--table-range2);
545 | }
546 |
547 | #turnipTable td.range1 {
548 | background-color: var(--table-range1);
549 | }
550 |
551 | #turnipTable td.range0 {
552 | background-color: var(--table-range0);
553 | }
554 |
555 | .chart-wrapper {
556 | margin-top: 8px;
557 | display: flex;
558 | flex-wrap: wrap;
559 | height: 400px;
560 | width: 100%;
561 | max-width: 1080px;
562 | justify-content: center;
563 | }
564 |
565 | .waves {
566 | position: relative;
567 | width: 100%;
568 | height: 5vh;
569 | margin-bottom: -7px;
570 | /*Fix for safari gap*/
571 | max-height: 150px;
572 | }
573 |
574 | #permalink-input {
575 | display: none;
576 | position: fixed;
577 | }
578 |
579 | .permalink {
580 | display: none;
581 | white-space: nowrap;
582 | font-size: 18px;
583 | user-select: none;
584 | cursor: pointer;
585 | }
586 |
587 | .permalink .fa-copy {
588 | margin: 0 8px;
589 | height: 20px;
590 | color: var(--color-blue);
591 | }
592 |
593 | /* The snackbar - position it at the bottom and in the middle of the screen */
594 | #snackbar {
595 | visibility: hidden; /* Hidden by default. Visible on click */
596 | min-width: 250px; /* Set a default minimum width */
597 | background-color: var(--dialog-bg-color); /* Black background color */
598 | font-family: 'Raleway', sans-serif;
599 | font-weight: 800;
600 | font-size: 1rem;
601 | color: var(--dialog-text-color);
602 | letter-spacing: 0.2px;
603 | line-height: 1.8rem;
604 | text-align: center; /* Centered text */
605 | border-radius: 40px; /* Rounded borders */
606 | padding: 16px 24px; /* Padding */
607 | position: fixed; /* Sit on top of the screen */
608 | z-index: 1; /* Add a z-index if needed */
609 | bottom: 30px; /* 30px from the bottom */
610 | box-shadow: 0 1px 3px var(--shadow-6), 0 1px 2px var(--shadow-8);
611 | }
612 |
613 | /* Show the snackbar when clicking on a button (class added with JavaScript) */
614 | #snackbar.show {
615 | visibility: visible; /* Show the snackbar */
616 | /* Add animation: Take 0.5 seconds to fade in and out the snackbar.
617 | However, delay the fade out process for 2.5 seconds */
618 | -webkit-animation: fadein 0.5s, fadeout 0.5s 2.5s;
619 | animation: fadein 0.5s, fadeout 0.5s 2.5s;
620 | }
621 |
622 | /* Animations to fade the snackbar in and out */
623 | @-webkit-keyframes fadein {
624 | from {bottom: 0; opacity: 0;}
625 | to {bottom: 30px; opacity: 1;}
626 | }
627 |
628 | @keyframes fadein {
629 | from {bottom: 0; opacity: 0;}
630 | to {bottom: 30px; opacity: 1;}
631 | }
632 |
633 | @-webkit-keyframes fadeout {
634 | from {bottom: 30px; opacity: 1;}
635 | to {bottom: 0; opacity: 0;}
636 | }
637 |
638 | @keyframes fadeout {
639 | from {bottom: 30px; opacity: 1;}
640 | to {bottom: 0; opacity: 0;}
641 | }
642 |
643 | /* Cloud SVG placement */
644 | .parallax>use:nth-child(1) {
645 | transform: translate3d(-30px, 0, 0);
646 | fill: var(--wave-4);
647 | }
648 |
649 | .parallax>use:nth-child(2) {
650 | transform: translate3d(-90px, 0, 0);
651 | fill: var(--wave-3);
652 | }
653 |
654 | .parallax>use:nth-child(3) {
655 | transform: translate3d(45px, 0, 0);
656 | fill: var(--wave-2);
657 | }
658 |
659 | .parallax>use:nth-child(4) {
660 | transform: translate3d(20px, 0, 0);
661 | fill: var(--wave-1);
662 | }
663 |
664 | /*Shrinking for mobile*/
665 | @media (max-width: 768px) {
666 | .waves {
667 | height: 40px;
668 | min-height: 40px;
669 | }
670 | }
671 |
--------------------------------------------------------------------------------
/js/predictions.js:
--------------------------------------------------------------------------------
1 | const PATTERN = {
2 | FLUCTUATING: 0,
3 | LARGE_SPIKE: 1,
4 | DECREASING: 2,
5 | SMALL_SPIKE: 3,
6 | };
7 |
8 | const PROBABILITY_MATRIX = {
9 | [PATTERN.FLUCTUATING]: {
10 | [PATTERN.FLUCTUATING]: 0.20,
11 | [PATTERN.LARGE_SPIKE]: 0.30,
12 | [PATTERN.DECREASING]: 0.15,
13 | [PATTERN.SMALL_SPIKE]: 0.35,
14 | },
15 | [PATTERN.LARGE_SPIKE]: {
16 | [PATTERN.FLUCTUATING]: 0.50,
17 | [PATTERN.LARGE_SPIKE]: 0.05,
18 | [PATTERN.DECREASING]: 0.20,
19 | [PATTERN.SMALL_SPIKE]: 0.25,
20 | },
21 | [PATTERN.DECREASING]: {
22 | [PATTERN.FLUCTUATING]: 0.25,
23 | [PATTERN.LARGE_SPIKE]: 0.45,
24 | [PATTERN.DECREASING]: 0.05,
25 | [PATTERN.SMALL_SPIKE]: 0.25,
26 | },
27 | [PATTERN.SMALL_SPIKE]: {
28 | [PATTERN.FLUCTUATING]: 0.45,
29 | [PATTERN.LARGE_SPIKE]: 0.25,
30 | [PATTERN.DECREASING]: 0.15,
31 | [PATTERN.SMALL_SPIKE]: 0.15,
32 | },
33 | };
34 |
35 | const RATE_MULTIPLIER = 10000;
36 |
37 | function range_length(range) {
38 | return range[1] - range[0];
39 | }
40 |
41 | function clamp(x, min, max) {
42 | return Math.min(Math.max(x, min), max);
43 | }
44 |
45 | function range_intersect(range1, range2) {
46 | if (range1[0] > range2[1] || range1[1] < range2[0]) {
47 | return null;
48 | }
49 | return [Math.max(range1[0], range2[0]), Math.min(range1[1], range2[1])];
50 | }
51 |
52 | function range_intersect_length(range1, range2) {
53 | if (range1[0] > range2[1] || range1[1] < range2[0]) {
54 | return 0;
55 | }
56 | return range_length(range_intersect(range1, range2));
57 | }
58 |
59 | /**
60 | * Accurately sums a list of floating point numbers.
61 | * See https://en.wikipedia.org/wiki/Kahan_summation_algorithm#Further_enhancements
62 | * for more information.
63 | * @param {number[]} input
64 | * @returns {number} The sum of the input.
65 | */
66 | function float_sum(input) {
67 | // Uses the improved Kahan–Babuska algorithm introduced by Neumaier.
68 | let sum = 0;
69 | // The "lost bits" of sum.
70 | let c = 0;
71 | for (let i = 0; i < input.length; i++) {
72 | const cur = input[i];
73 | const t = sum + cur;
74 | if (Math.abs(sum) >= Math.abs(cur)) {
75 | c += (sum - t) + cur;
76 | } else {
77 | c += (cur - t) + sum;
78 | }
79 | sum = t;
80 | }
81 | return sum + c;
82 | }
83 |
84 | /**
85 | * Accurately returns the prefix sum of a list of floating point numbers.
86 | * See https://en.wikipedia.org/wiki/Kahan_summation_algorithm#Further_enhancements
87 | * for more information.
88 | * @param {number[]} input
89 | * @returns {[number, number][]} The prefix sum of the input, such that
90 | * output[i] = [sum of first i integers, error of the sum].
91 | * The "true" prefix sum is equal to the sum of the pair of numbers, but it is
92 | * explicitly returned as a pair of numbers to ensure that the error portion
93 | * isn't lost when subtracting prefix sums.
94 | */
95 | function prefix_float_sum(input) {
96 | const prefix_sum = [[0, 0]];
97 | let sum = 0;
98 | let c = 0;
99 | for (let i = 0; i < input.length; i++) {
100 | const cur = input[i];
101 | const t = sum + cur;
102 | if (Math.abs(sum) >= Math.abs(cur)) {
103 | c += (sum - t) + cur;
104 | } else {
105 | c += (cur - t) + sum;
106 | }
107 | sum = t;
108 | prefix_sum.push([sum, c]);
109 | }
110 | return prefix_sum;
111 | }
112 |
113 | /*
114 | * Probability Density Function of rates.
115 | * Since the PDF is continuous*, we approximate it by a discrete probability function:
116 | * the value in range [x, x + 1) has a uniform probability
117 | * prob[x - value_start];
118 | *
119 | * Note that we operate all rate on the (* RATE_MULTIPLIER) scale.
120 | *
121 | * (*): Well not really since it only takes values that "float" can represent in some form, but the
122 | * space is too large to compute directly in JS.
123 | */
124 | class PDF {
125 | /**
126 | * Initialize a PDF in range [a, b], a and b can be non-integer.
127 | * if uniform is true, then initialize the probability to be uniform, else initialize to a
128 | * all-zero (invalid) PDF.
129 | * @param {number} a - Left end-point.
130 | * @param {number} b - Right end-point end-point.
131 | * @param {boolean} uniform - If true, initialise with the uniform distribution.
132 | */
133 | constructor(a, b, uniform = true) {
134 | // We need to ensure that [a, b] is fully contained in [value_start, value_end].
135 | /** @type {number} */
136 | this.value_start = Math.floor(a);
137 | /** @type {number} */
138 | this.value_end = Math.ceil(b);
139 | const range = [a, b];
140 | const total_length = range_length(range);
141 | /** @type {number[]} */
142 | this.prob = Array(this.value_end - this.value_start);
143 | if (uniform) {
144 | for (let i = 0; i < this.prob.length; i++) {
145 | this.prob[i] =
146 | range_intersect_length(this.range_of(i), range) / total_length;
147 | }
148 | }
149 | }
150 |
151 | /**
152 | * Calculates the interval represented by this.prob[idx]
153 | * @param {number} idx - The index of this.prob
154 | * @returns {[number, number]} The interval representing this.prob[idx].
155 | */
156 | range_of(idx) {
157 | // We intentionally include the right end-point of the range.
158 | // The probability of getting exactly an endpoint is zero, so we can assume
159 | // the "probability ranges" are "touching".
160 | return [this.value_start + idx, this.value_start + idx + 1];
161 | }
162 |
163 | min_value() {
164 | return this.value_start;
165 | }
166 |
167 | max_value() {
168 | return this.value_end;
169 | }
170 |
171 | /**
172 | * @returns {number} The sum of probabilities before normalisation.
173 | */
174 | normalize() {
175 | const total_probability = float_sum(this.prob);
176 | for (let i = 0; i < this.prob.length; i++) {
177 | this.prob[i] /= total_probability;
178 | }
179 | return total_probability;
180 | }
181 |
182 | /*
183 | * Limit the values to be in the range, and return the probability that the value was in this
184 | * range.
185 | */
186 | range_limit(range) {
187 | let [start, end] = range;
188 | start = Math.max(start, this.min_value());
189 | end = Math.min(end, this.max_value());
190 | if (start >= end) {
191 | // Set this to invalid values
192 | this.value_start = this.value_end = 0;
193 | this.prob = [];
194 | return 0;
195 | }
196 | start = Math.floor(start);
197 | end = Math.ceil(end);
198 |
199 | const start_idx = start - this.value_start;
200 | const end_idx = end - this.value_start;
201 | for (let i = start_idx; i < end_idx; i++) {
202 | this.prob[i] *= range_intersect_length(this.range_of(i), range);
203 | }
204 |
205 | this.prob = this.prob.slice(start_idx, end_idx);
206 | this.value_start = start;
207 | this.value_end = end;
208 |
209 | // The probability that the value was in this range is equal to the total
210 | // sum of "un-normalised" values in the range.
211 | return this.normalize();
212 | }
213 |
214 | /**
215 | * Subtract the PDF by a uniform distribution in [rate_decay_min, rate_decay_max]
216 | *
217 | * For simplicity, we assume that rate_decay_min and rate_decay_max are both integers.
218 | * @param {number} rate_decay_min
219 | * @param {number} rate_decay_max
220 | * @returns {void}
221 | */
222 | decay(rate_decay_min, rate_decay_max) {
223 | // In case the arguments aren't integers, round them to the nearest integer.
224 | rate_decay_min = Math.round(rate_decay_min);
225 | rate_decay_max = Math.round(rate_decay_max);
226 | // The sum of this distribution with a uniform distribution.
227 | // Let's assume that both distributions start at 0 and X = this dist,
228 | // Y = uniform dist, and Z = X + Y.
229 | // Let's also assume that X is a "piecewise uniform" distribution, so
230 | // x(i) = this.prob[Math.floor(i)] - which matches our implementation.
231 | // We also know that y(i) = 1 / max(Y) - as we assume that min(Y) = 0.
232 | // In the end, we're interested in:
233 | // Pr(i <= Z < i+1) where i is an integer
234 | // = int. x(val) * Pr(i-val <= Y < i-val+1) dval from 0 to max(X)
235 | // = int. x(floor(val)) * Pr(i-val <= Y < i-val+1) dval from 0 to max(X)
236 | // = sum val from 0 to max(X)-1
237 | // x(val) * f_i(val) / max(Y)
238 | // where f_i(val) =
239 | // 0.5 if i-val = 0 or max(Y), so val = i-max(Y) or i
240 | // 1.0 if 0 < i-val < max(Y), so i-max(Y) < val < i
241 | // as x(val) is "constant" for each integer step, so we can consider the
242 | // integral in integer steps.
243 | // = sum val from max(0, i-max(Y)) to min(max(X)-1, i)
244 | // x(val) * f_i(val) / max(Y)
245 | // for example, max(X)=1, max(Y)=10, i=5
246 | // = sum val from max(0, 5-10)=0 to min(1-1, 5)=0
247 | // x(val) * f_i(val) / max(Y)
248 | // = x(0) * 1 / 10
249 |
250 | // Get a prefix sum / CDF of this so we can calculate sums in O(1).
251 | const prefix = prefix_float_sum(this.prob);
252 | const max_X = this.prob.length;
253 | const max_Y = rate_decay_max - rate_decay_min;
254 | const newProb = Array(this.prob.length + max_Y);
255 | for (let i = 0; i < newProb.length; i++) {
256 | // Note that left and right here are INCLUSIVE.
257 | const left = Math.max(0, i - max_Y);
258 | const right = Math.min(max_X - 1, i);
259 | // We want to sum, in total, prefix[right+1], -prefix[left], and subtract
260 | // the 0.5s if necessary.
261 | // This may involve numbers of differing magnitudes, so use the float sum
262 | // algorithm to sum these up.
263 | const numbers_to_sum = [
264 | prefix[right + 1][0], prefix[right + 1][1],
265 | -prefix[left][0], -prefix[left][1],
266 | ];
267 | if (left === i-max_Y) {
268 | // Need to halve the left endpoint.
269 | numbers_to_sum.push(-this.prob[left] / 2);
270 | }
271 | if (right === i) {
272 | // Need to halve the right endpoint.
273 | // It's guaranteed that we won't accidentally "halve" twice,
274 | // as that would require i-max_Y = i, so max_Y = 0 - which is
275 | // impossible.
276 | numbers_to_sum.push(-this.prob[right] / 2);
277 | }
278 | newProb[i] = float_sum(numbers_to_sum) / max_Y;
279 | }
280 |
281 | this.prob = newProb;
282 | this.value_start -= rate_decay_max;
283 | this.value_end -= rate_decay_min;
284 | // No need to normalise, as it is guaranteed that the sum of this.prob is 1.
285 | }
286 | }
287 |
288 | class Predictor {
289 |
290 | constructor(prices, first_buy, previous_pattern) {
291 | // The reverse-engineered code is not perfectly accurate, especially as it's not
292 | // 32-bit ARM floating point. So, be tolerant of slightly unexpected inputs
293 | this.fudge_factor = 0;
294 | this.prices = prices;
295 | this.first_buy = first_buy;
296 | this.previous_pattern = previous_pattern;
297 | }
298 |
299 | intceil(val) {
300 | return Math.trunc(val + 0.99999);
301 | }
302 |
303 | minimum_rate_from_given_and_base(given_price, buy_price) {
304 | return RATE_MULTIPLIER * (given_price - 0.99999) / buy_price;
305 | }
306 |
307 | maximum_rate_from_given_and_base(given_price, buy_price) {
308 | return RATE_MULTIPLIER * (given_price + 0.00001) / buy_price;
309 | }
310 |
311 | rate_range_from_given_and_base(given_price, buy_price) {
312 | return [
313 | this.minimum_rate_from_given_and_base(given_price, buy_price),
314 | this.maximum_rate_from_given_and_base(given_price, buy_price)
315 | ];
316 | }
317 |
318 | get_price(rate, basePrice) {
319 | return this.intceil(rate * basePrice / RATE_MULTIPLIER);
320 | }
321 |
322 | * multiply_generator_probability(generator, probability) {
323 | for (const it of generator) {
324 | yield {...it, probability: it.probability * probability};
325 | }
326 | }
327 |
328 | /*
329 | * This corresponds to the code:
330 | * for (int i = start; i < start + length; i++)
331 | * {
332 | * sellPrices[work++] =
333 | * intceil(randfloat(rate_min / RATE_MULTIPLIER, rate_max / RATE_MULTIPLIER) * basePrice);
334 | * }
335 | *
336 | * Would return the conditional probability given the given_prices, and modify
337 | * the predicted_prices array.
338 | * If the given_prices won't match, returns 0.
339 | */
340 | generate_individual_random_price(
341 | given_prices, predicted_prices, start, length, rate_min, rate_max) {
342 | rate_min *= RATE_MULTIPLIER;
343 | rate_max *= RATE_MULTIPLIER;
344 |
345 | const buy_price = given_prices[0];
346 | const rate_range = [rate_min, rate_max];
347 | let prob = 1;
348 |
349 | for (let i = start; i < start + length; i++) {
350 | let min_pred = this.get_price(rate_min, buy_price);
351 | let max_pred = this.get_price(rate_max, buy_price);
352 | if (!isNaN(given_prices[i])) {
353 | if (given_prices[i] < min_pred - this.fudge_factor || given_prices[i] > max_pred + this.fudge_factor) {
354 | // Given price is out of predicted range, so this is the wrong pattern
355 | return 0;
356 | }
357 | // TODO: How to deal with probability when there's fudge factor?
358 | // Clamp the value to be in range now so the probability won't be totally biased to fudged values.
359 | const real_rate_range =
360 | this.rate_range_from_given_and_base(clamp(given_prices[i], min_pred, max_pred), buy_price);
361 | prob *= range_intersect_length(rate_range, real_rate_range) /
362 | range_length(rate_range);
363 | min_pred = given_prices[i];
364 | max_pred = given_prices[i];
365 | }
366 |
367 | predicted_prices.push({
368 | min: min_pred,
369 | max: max_pred,
370 | });
371 | }
372 | return prob;
373 | }
374 |
375 | /*
376 | * This corresponds to the code:
377 | * rate = randfloat(start_rate_min, start_rate_max);
378 | * for (int i = start; i < start + length; i++)
379 | * {
380 | * sellPrices[work++] = intceil(rate * basePrice);
381 | * rate -= randfloat(rate_decay_min, rate_decay_max);
382 | * }
383 | *
384 | * Would return the conditional probability given the given_prices, and modify
385 | * the predicted_prices array.
386 | * If the given_prices won't match, returns 0.
387 | */
388 | generate_decreasing_random_price(
389 | given_prices, predicted_prices, start, length, start_rate_min,
390 | start_rate_max, rate_decay_min, rate_decay_max) {
391 | start_rate_min *= RATE_MULTIPLIER;
392 | start_rate_max *= RATE_MULTIPLIER;
393 | rate_decay_min *= RATE_MULTIPLIER;
394 | rate_decay_max *= RATE_MULTIPLIER;
395 |
396 | const buy_price = given_prices[0];
397 | let rate_pdf = new PDF(start_rate_min, start_rate_max);
398 | let prob = 1;
399 |
400 | for (let i = start; i < start + length; i++) {
401 | let min_pred = this.get_price(rate_pdf.min_value(), buy_price);
402 | let max_pred = this.get_price(rate_pdf.max_value(), buy_price);
403 | if (!isNaN(given_prices[i])) {
404 | if (given_prices[i] < min_pred - this.fudge_factor || given_prices[i] > max_pred + this.fudge_factor) {
405 | // Given price is out of predicted range, so this is the wrong pattern
406 | return 0;
407 | }
408 | // TODO: How to deal with probability when there's fudge factor?
409 | // Clamp the value to be in range now so the probability won't be totally biased to fudged values.
410 | const real_rate_range =
411 | this.rate_range_from_given_and_base(clamp(given_prices[i], min_pred, max_pred), buy_price);
412 | prob *= rate_pdf.range_limit(real_rate_range);
413 | if (prob == 0) {
414 | return 0;
415 | }
416 | min_pred = given_prices[i];
417 | max_pred = given_prices[i];
418 | }
419 |
420 | predicted_prices.push({
421 | min: min_pred,
422 | max: max_pred,
423 | });
424 |
425 | rate_pdf.decay(rate_decay_min, rate_decay_max);
426 | }
427 | return prob;
428 | }
429 |
430 |
431 | /*
432 | * This corresponds to the code:
433 | * rate = randfloat(rate_min, rate_max);
434 | * sellPrices[work++] = intceil(randfloat(rate_min, rate) * basePrice) - 1;
435 | * sellPrices[work++] = intceil(rate * basePrice);
436 | * sellPrices[work++] = intceil(randfloat(rate_min, rate) * basePrice) - 1;
437 | *
438 | * Would return the conditional probability given the given_prices, and modify
439 | * the predicted_prices array.
440 | * If the given_prices won't match, returns 0.
441 | */
442 | generate_peak_price(
443 | given_prices, predicted_prices, start, rate_min, rate_max) {
444 | rate_min *= RATE_MULTIPLIER;
445 | rate_max *= RATE_MULTIPLIER;
446 |
447 | const buy_price = given_prices[0];
448 | let prob = 1;
449 | let rate_range = [rate_min, rate_max];
450 |
451 | // * Calculate the probability first.
452 | // Prob(middle_price)
453 | const middle_price = given_prices[start + 1];
454 | if (!isNaN(middle_price)) {
455 | const min_pred = this.get_price(rate_min, buy_price);
456 | const max_pred = this.get_price(rate_max, buy_price);
457 | if (middle_price < min_pred - this.fudge_factor || middle_price > max_pred + this.fudge_factor) {
458 | // Given price is out of predicted range, so this is the wrong pattern
459 | return 0;
460 | }
461 | // TODO: How to deal with probability when there's fudge factor?
462 | // Clamp the value to be in range now so the probability won't be totally biased to fudged values.
463 | const real_rate_range =
464 | this.rate_range_from_given_and_base(clamp(middle_price, min_pred, max_pred), buy_price);
465 | prob *= range_intersect_length(rate_range, real_rate_range) /
466 | range_length(rate_range);
467 | if (prob == 0) {
468 | return 0;
469 | }
470 |
471 | rate_range = range_intersect(rate_range, real_rate_range);
472 | }
473 |
474 | const left_price = given_prices[start];
475 | const right_price = given_prices[start + 2];
476 | // Prob(left_price | middle_price), Prob(right_price | middle_price)
477 | //
478 | // A = rate_range[0], B = rate_range[1], C = rate_min, X = rate, Y = randfloat(rate_min, rate)
479 | // rate = randfloat(A, B); sellPrices[work++] = intceil(randfloat(C, rate) * basePrice) - 1;
480 | //
481 | // => 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),
482 | // let Z=Y-C, Z1=A-C, Z2=B-C, Z->U(0,1)*U(Z1,Z2)
483 | // Prob(Z<=t) = integral_{x=0}^{1} [min(t/x,Z2)-min(t/x,Z1)]/ (Z2-Z1)
484 | // let F(t, ZZ) = integral_{x=0}^{1} min(t/x, ZZ)
485 | // 1. if ZZ < t, then min(t/x, ZZ) = ZZ -> F(t, ZZ) = ZZ
486 | // 2. if ZZ >= t, then F(t, ZZ) = integral_{x=0}^{t/ZZ} ZZ + integral_{x=t/ZZ}^{1} t/x
487 | // = t - t log(t/ZZ)
488 | // Prob(Z<=t) = (F(t, Z2) - F(t, Z1)) / (Z2 - Z1)
489 | // Prob(Y<=t) = Prob(Z>=t-C)
490 | for (const price of [left_price, right_price]) {
491 | if (isNaN(price)) {
492 | continue;
493 | }
494 | const min_pred = this.get_price(rate_min, buy_price) - 1;
495 | const max_pred = this.get_price(rate_range[1], buy_price) - 1;
496 | if (price < min_pred - this.fudge_factor || price > max_pred + this.fudge_factor) {
497 | // Given price is out of predicted range, so this is the wrong pattern
498 | return 0;
499 | }
500 | // TODO: How to deal with probability when there's fudge factor?
501 | // Clamp the value to be in range now so the probability won't be totally biased to fudged values.
502 | const rate2_range = this.rate_range_from_given_and_base(clamp(price, min_pred, max_pred)+ 1, buy_price);
503 | const F = (t, ZZ) => {
504 | if (t <= 0) {
505 | return 0;
506 | }
507 | return ZZ < t ? ZZ : t - t * (Math.log(t) - Math.log(ZZ));
508 | };
509 | const [A, B] = rate_range;
510 | const C = rate_min;
511 | const Z1 = A - C;
512 | const Z2 = B - C;
513 | const PY = (t) => (F(t - C, Z2) - F(t - C, Z1)) / (Z2 - Z1);
514 | prob *= PY(rate2_range[1]) - PY(rate2_range[0]);
515 | if (prob == 0) {
516 | return 0;
517 | }
518 | }
519 |
520 | // * Then generate the real predicted range.
521 | // We're doing things in different order then how we calculate probability,
522 | // since forward prediction is more useful here.
523 | //
524 | // Main spike 1
525 | let min_pred = this.get_price(rate_min, buy_price) - 1;
526 | let max_pred = this.get_price(rate_max, buy_price) - 1;
527 | if (!isNaN(given_prices[start])) {
528 | min_pred = given_prices[start];
529 | max_pred = given_prices[start];
530 | }
531 | predicted_prices.push({
532 | min: min_pred,
533 | max: max_pred,
534 | });
535 |
536 | // Main spike 2
537 | min_pred = predicted_prices[start].min;
538 | max_pred = this.get_price(rate_max, buy_price);
539 | if (!isNaN(given_prices[start + 1])) {
540 | min_pred = given_prices[start + 1];
541 | max_pred = given_prices[start + 1];
542 | }
543 | predicted_prices.push({
544 | min: min_pred,
545 | max: max_pred,
546 | });
547 |
548 | // Main spike 3
549 | min_pred = this.get_price(rate_min, buy_price) - 1;
550 | max_pred = predicted_prices[start + 1].max - 1;
551 | if (!isNaN(given_prices[start + 2])) {
552 | min_pred = given_prices[start + 2];
553 | max_pred = given_prices[start + 2];
554 | }
555 | predicted_prices.push({
556 | min: min_pred,
557 | max: max_pred,
558 | });
559 |
560 | return prob;
561 | }
562 |
563 | * generate_pattern_0_with_lengths(
564 | given_prices, high_phase_1_len, dec_phase_1_len, high_phase_2_len,
565 | dec_phase_2_len, high_phase_3_len) {
566 | /*
567 | // PATTERN 0: high, decreasing, high, decreasing, high
568 | work = 2;
569 | // high phase 1
570 | for (int i = 0; i < hiPhaseLen1; i++)
571 | {
572 | sellPrices[work++] = intceil(randfloat(0.9, 1.4) * basePrice);
573 | }
574 | // decreasing phase 1
575 | rate = randfloat(0.8, 0.6);
576 | for (int i = 0; i < decPhaseLen1; i++)
577 | {
578 | sellPrices[work++] = intceil(rate * basePrice);
579 | rate -= 0.04;
580 | rate -= randfloat(0, 0.06);
581 | }
582 | // high phase 2
583 | for (int i = 0; i < (hiPhaseLen2and3 - hiPhaseLen3); i++)
584 | {
585 | sellPrices[work++] = intceil(randfloat(0.9, 1.4) * basePrice);
586 | }
587 | // decreasing phase 2
588 | rate = randfloat(0.8, 0.6);
589 | for (int i = 0; i < decPhaseLen2; i++)
590 | {
591 | sellPrices[work++] = intceil(rate * basePrice);
592 | rate -= 0.04;
593 | rate -= randfloat(0, 0.06);
594 | }
595 | // high phase 3
596 | for (int i = 0; i < hiPhaseLen3; i++)
597 | {
598 | sellPrices[work++] = intceil(randfloat(0.9, 1.4) * basePrice);
599 | }
600 | */
601 |
602 | const buy_price = given_prices[0];
603 | const predicted_prices = [
604 | {
605 | min: buy_price,
606 | max: buy_price,
607 | },
608 | {
609 | min: buy_price,
610 | max: buy_price,
611 | },
612 | ];
613 | let probability = 1;
614 |
615 | // High Phase 1
616 | probability *= this.generate_individual_random_price(
617 | given_prices, predicted_prices, 2, high_phase_1_len, 0.9, 1.4);
618 | if (probability == 0) {
619 | return;
620 | }
621 |
622 | // Dec Phase 1
623 | probability *= this.generate_decreasing_random_price(
624 | given_prices, predicted_prices, 2 + high_phase_1_len, dec_phase_1_len,
625 | 0.6, 0.8, 0.04, 0.1);
626 | if (probability == 0) {
627 | return;
628 | }
629 |
630 | // High Phase 2
631 | probability *= this.generate_individual_random_price(given_prices, predicted_prices,
632 | 2 + high_phase_1_len + dec_phase_1_len, high_phase_2_len, 0.9, 1.4);
633 | if (probability == 0) {
634 | return;
635 | }
636 |
637 | // Dec Phase 2
638 | probability *= this.generate_decreasing_random_price(
639 | given_prices, predicted_prices,
640 | 2 + high_phase_1_len + dec_phase_1_len + high_phase_2_len,
641 | dec_phase_2_len, 0.6, 0.8, 0.04, 0.1);
642 | if (probability == 0) {
643 | return;
644 | }
645 |
646 | // High Phase 3
647 | if (2 + high_phase_1_len + dec_phase_1_len + high_phase_2_len + dec_phase_2_len + high_phase_3_len != 14) {
648 | throw new Error("Phase lengths don't add up");
649 | }
650 |
651 | const prev_length = 2 + high_phase_1_len + dec_phase_1_len +
652 | high_phase_2_len + dec_phase_2_len;
653 | probability *= this.generate_individual_random_price(
654 | given_prices, predicted_prices, prev_length, 14 - prev_length, 0.9, 1.4);
655 | if (probability == 0) {
656 | return;
657 | }
658 |
659 | yield {
660 | pattern_number: 0,
661 | prices: predicted_prices,
662 | probability,
663 | };
664 | }
665 |
666 | * generate_pattern_0(given_prices) {
667 | /*
668 | decPhaseLen1 = randbool() ? 3 : 2;
669 | decPhaseLen2 = 5 - decPhaseLen1;
670 | hiPhaseLen1 = randint(0, 6);
671 | hiPhaseLen2and3 = 7 - hiPhaseLen1;
672 | hiPhaseLen3 = randint(0, hiPhaseLen2and3 - 1);
673 | */
674 | for (var dec_phase_1_len = 2; dec_phase_1_len < 4; dec_phase_1_len++) {
675 | for (var high_phase_1_len = 0; high_phase_1_len < 7; high_phase_1_len++) {
676 | for (var high_phase_3_len = 0; high_phase_3_len < (7 - high_phase_1_len - 1 + 1); high_phase_3_len++) {
677 | yield* this.multiply_generator_probability(
678 | 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),
679 | 1 / (4 - 2) / 7 / (7 - high_phase_1_len));
680 | }
681 | }
682 | }
683 | }
684 |
685 | * generate_pattern_1_with_peak(given_prices, peak_start) {
686 | /*
687 | // PATTERN 1: decreasing middle, high spike, random low
688 | peakStart = randint(3, 9);
689 | rate = randfloat(0.9, 0.85);
690 | for (work = 2; work < peakStart; work++)
691 | {
692 | sellPrices[work] = intceil(rate * basePrice);
693 | rate -= 0.03;
694 | rate -= randfloat(0, 0.02);
695 | }
696 | sellPrices[work++] = intceil(randfloat(0.9, 1.4) * basePrice);
697 | sellPrices[work++] = intceil(randfloat(1.4, 2.0) * basePrice);
698 | sellPrices[work++] = intceil(randfloat(2.0, 6.0) * basePrice);
699 | sellPrices[work++] = intceil(randfloat(1.4, 2.0) * basePrice);
700 | sellPrices[work++] = intceil(randfloat(0.9, 1.4) * basePrice);
701 | for (; work < 14; work++)
702 | {
703 | sellPrices[work] = intceil(randfloat(0.4, 0.9) * basePrice);
704 | }
705 | */
706 |
707 | const buy_price = given_prices[0];
708 | const predicted_prices = [
709 | {
710 | min: buy_price,
711 | max: buy_price,
712 | },
713 | {
714 | min: buy_price,
715 | max: buy_price,
716 | },
717 | ];
718 | let probability = 1;
719 |
720 | probability *= this.generate_decreasing_random_price(
721 | given_prices, predicted_prices, 2, peak_start - 2, 0.85, 0.9, 0.03, 0.05);
722 | if (probability == 0) {
723 | return;
724 | }
725 |
726 | // Now each day is independent of next
727 | 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];
728 | 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];
729 | for (let i = peak_start; i < 14; i++) {
730 | probability *= this.generate_individual_random_price(
731 | given_prices, predicted_prices, i, 1, min_randoms[i - peak_start],
732 | max_randoms[i - peak_start]);
733 | if (probability == 0) {
734 | return;
735 | }
736 | }
737 | yield {
738 | pattern_number: 1,
739 | prices: predicted_prices,
740 | probability,
741 | };
742 | }
743 |
744 | * generate_pattern_1(given_prices) {
745 | for (var peak_start = 3; peak_start < 10; peak_start++) {
746 | yield* this.multiply_generator_probability(this.generate_pattern_1_with_peak(given_prices, peak_start), 1 / (10 - 3));
747 | }
748 | }
749 |
750 | * generate_pattern_2(given_prices) {
751 | /*
752 | // PATTERN 2: consistently decreasing
753 | rate = 0.9;
754 | rate -= randfloat(0, 0.05);
755 | for (work = 2; work < 14; work++)
756 | {
757 | sellPrices[work] = intceil(rate * basePrice);
758 | rate -= 0.03;
759 | rate -= randfloat(0, 0.02);
760 | }
761 | break;
762 | */
763 |
764 | const buy_price = given_prices[0];
765 | const predicted_prices = [
766 | {
767 | min: buy_price,
768 | max: buy_price,
769 | },
770 | {
771 | min: buy_price,
772 | max: buy_price,
773 | },
774 | ];
775 | let probability = 1;
776 |
777 | probability *= this.generate_decreasing_random_price(
778 | given_prices, predicted_prices, 2, 14 - 2, 0.85, 0.9, 0.03, 0.05);
779 | if (probability == 0) {
780 | return;
781 | }
782 |
783 | yield {
784 | pattern_number: 2,
785 | prices: predicted_prices,
786 | probability,
787 | };
788 | }
789 |
790 | * generate_pattern_3_with_peak(given_prices, peak_start) {
791 |
792 | /*
793 | // PATTERN 3: decreasing, spike, decreasing
794 | peakStart = randint(2, 9);
795 | // decreasing phase before the peak
796 | rate = randfloat(0.9, 0.4);
797 | for (work = 2; work < peakStart; work++)
798 | {
799 | sellPrices[work] = intceil(rate * basePrice);
800 | rate -= 0.03;
801 | rate -= randfloat(0, 0.02);
802 | }
803 | sellPrices[work++] = intceil(randfloat(0.9, 1.4) * (float)basePrice);
804 | sellPrices[work++] = intceil(randfloat(0.9, 1.4) * basePrice);
805 | rate = randfloat(1.4, 2.0);
806 | sellPrices[work++] = intceil(randfloat(1.4, rate) * basePrice) - 1;
807 | sellPrices[work++] = intceil(rate * basePrice);
808 | sellPrices[work++] = intceil(randfloat(1.4, rate) * basePrice) - 1;
809 | // decreasing phase after the peak
810 | if (work < 14)
811 | {
812 | rate = randfloat(0.9, 0.4);
813 | for (; work < 14; work++)
814 | {
815 | sellPrices[work] = intceil(rate * basePrice);
816 | rate -= 0.03;
817 | rate -= randfloat(0, 0.02);
818 | }
819 | }
820 | */
821 |
822 | const buy_price = given_prices[0];
823 | const predicted_prices = [
824 | {
825 | min: buy_price,
826 | max: buy_price,
827 | },
828 | {
829 | min: buy_price,
830 | max: buy_price,
831 | },
832 | ];
833 | let probability = 1;
834 |
835 | probability *= this.generate_decreasing_random_price(
836 | given_prices, predicted_prices, 2, peak_start - 2, 0.4, 0.9, 0.03, 0.05);
837 | if (probability == 0) {
838 | return;
839 | }
840 |
841 | // The peak
842 | probability *= this.generate_individual_random_price(
843 | given_prices, predicted_prices, peak_start, 2, 0.9, 1.4);
844 | if (probability == 0) {
845 | return;
846 | }
847 |
848 | probability *= this.generate_peak_price(
849 | given_prices, predicted_prices, peak_start + 2, 1.4, 2.0);
850 | if (probability == 0) {
851 | return;
852 | }
853 |
854 | if (peak_start + 5 < 14) {
855 | probability *= this.generate_decreasing_random_price(
856 | given_prices, predicted_prices, peak_start + 5, 14 - (peak_start + 5),
857 | 0.4, 0.9, 0.03, 0.05);
858 | if (probability == 0) {
859 | return;
860 | }
861 | }
862 |
863 | yield {
864 | pattern_number: 3,
865 | prices: predicted_prices,
866 | probability,
867 | };
868 | }
869 |
870 | * generate_pattern_3(given_prices) {
871 | for (let peak_start = 2; peak_start < 10; peak_start++) {
872 | yield* this.multiply_generator_probability(this.generate_pattern_3_with_peak(given_prices, peak_start), 1 / (10 - 2));
873 | }
874 | }
875 |
876 | get_transition_probability(previous_pattern) {
877 | if (typeof previous_pattern === 'undefined' || Number.isNaN(previous_pattern) || previous_pattern === null || previous_pattern < 0 || previous_pattern > 3) {
878 | // Use the steady state probabilities of PROBABILITY_MATRIX if we don't
879 | // know what the previous pattern was.
880 | // See https://github.com/mikebryant/ac-nh-turnip-prices/issues/68
881 | // and https://github.com/mikebryant/ac-nh-turnip-prices/pull/90
882 | // for more information.
883 | return [4530/13082, 3236/13082, 1931/13082, 3385/13082];
884 | }
885 |
886 | return PROBABILITY_MATRIX[previous_pattern];
887 | }
888 |
889 | * generate_all_patterns(sell_prices, previous_pattern) {
890 | const generate_pattern_fns = [this.generate_pattern_0, this.generate_pattern_1, this.generate_pattern_2, this.generate_pattern_3];
891 | const transition_probability = this.get_transition_probability(previous_pattern);
892 |
893 | for (let i = 0; i < 4; i++) {
894 | yield* this.multiply_generator_probability(generate_pattern_fns[i].bind(this)(sell_prices), transition_probability[i]);
895 | }
896 | }
897 |
898 | * generate_possibilities(sell_prices, first_buy, previous_pattern) {
899 | if (first_buy || isNaN(sell_prices[0])) {
900 | for (var buy_price = 90; buy_price <= 110; buy_price++) {
901 | const temp_sell_prices = sell_prices.slice();
902 | temp_sell_prices[0] = temp_sell_prices[1] = buy_price;
903 | if (first_buy) {
904 | yield* this.generate_pattern_3(temp_sell_prices);
905 | } else {
906 | // All buy prices are equal probability and we're at the outmost layer,
907 | // so don't need to multiply_generator_probability here.
908 | yield* this.generate_all_patterns(temp_sell_prices, previous_pattern);
909 | }
910 | }
911 | } else {
912 | yield* this.generate_all_patterns(sell_prices, previous_pattern);
913 | }
914 | }
915 |
916 | analyze_possibilities() {
917 | const sell_prices = this.prices;
918 | const first_buy = this.first_buy;
919 | const previous_pattern = this.previous_pattern;
920 | let generated_possibilities = [];
921 | for (let i = 0; i < 6; i++) {
922 | this.fudge_factor = i;
923 | generated_possibilities = Array.from(this.generate_possibilities(sell_prices, first_buy, previous_pattern));
924 | if (generated_possibilities.length > 0) {
925 | console.log("Generated possibilities using fudge factor %d: ", i, generated_possibilities);
926 | break;
927 | }
928 | }
929 |
930 | const total_probability = generated_possibilities.reduce((acc, it) => acc + it.probability, 0);
931 | for (const it of generated_possibilities) {
932 | it.probability /= total_probability;
933 | }
934 |
935 | for (let poss of generated_possibilities) {
936 | var weekMins = [];
937 | var weekMaxes = [];
938 | for (let day of poss.prices.slice(2)) {
939 | // Check for a future date by checking for a range of prices
940 | if(day.min !== day.max){
941 | weekMins.push(day.min);
942 | weekMaxes.push(day.max);
943 | } else {
944 | // If we find a set price after one or more ranged prices, the user has missed a day. Discard that data and start again.
945 | weekMins = [];
946 | weekMaxes = [];
947 | }
948 | }
949 | if (!weekMins.length && !weekMaxes.length) {
950 | weekMins.push(poss.prices[poss.prices.length -1].min);
951 | weekMaxes.push(poss.prices[poss.prices.length -1].max);
952 | }
953 | poss.weekGuaranteedMinimum = Math.max(...weekMins);
954 | poss.weekMax = Math.max(...weekMaxes);
955 | }
956 |
957 | let category_totals = {};
958 | for (let i of [0, 1, 2, 3]) {
959 | category_totals[i] = generated_possibilities
960 | .filter(value => value.pattern_number == i)
961 | .map(value => value.probability)
962 | .reduce((previous, current) => previous + current, 0);
963 | }
964 |
965 | for (let pos of generated_possibilities) {
966 | pos.category_total_probability = category_totals[pos.pattern_number];
967 | }
968 |
969 | generated_possibilities.sort((a, b) => {
970 | return b.category_total_probability - a.category_total_probability || b.probability - a.probability;
971 | });
972 |
973 | let global_min_max = [];
974 | for (let day = 0; day < 14; day++) {
975 | const prices = {
976 | min: 999,
977 | max: 0,
978 | };
979 | for (let poss of generated_possibilities) {
980 | if (poss.prices[day].min < prices.min) {
981 | prices.min = poss.prices[day].min;
982 | }
983 | if (poss.prices[day].max > prices.max) {
984 | prices.max = poss.prices[day].max;
985 | }
986 | }
987 | global_min_max.push(prices);
988 | }
989 |
990 | generated_possibilities.unshift({
991 | pattern_number: 4,
992 | prices: global_min_max,
993 | weekGuaranteedMinimum: Math.min(...generated_possibilities.map(poss => poss.weekGuaranteedMinimum)),
994 | weekMax: Math.max(...generated_possibilities.map(poss => poss.weekMax))
995 | });
996 |
997 | return generated_possibilities;
998 | }
999 | }
1000 |
--------------------------------------------------------------------------------