Imagine a marketplace where, every Sunday, a certain trade good becomes available for purchase. Throughout the following week (Monday to Saturday), the market announces two daily buyback prices (one before 12 AM and another after) allowing traders to sell the good back at those rates.
»When should one sell to maximize profits, and is there any way to predict future buyback prices based on already observed data?«
Answering this question is inherently challenging, and the quality and complexity of any solution depend heavily on the available information about the specific market. In this article we will develop one such solution for a particular market, covering every aspect in detail, from the mathematical background to the full implementation in code. Additionally, we will provide a fully functional prediction tool, along with comprehensive testing results.
The market in question is the turnip market from Animal Crossing: New Horizons, which, at a basic level, functions just as we described. However, thanks to data mining, conducted by Ninji, we now understand exactly how buyback prices are determined. While still random, this knowledge gives traders a significant advantage. For a comprehensive breakdown, check out this PDF, written by Edricus. Alternatively, you can explore the raw code on GitHub.
In this first part of the article, we will discuss, model, and implement these findings ourselves. This process will not only deepen our understanding of the market but also enable comprehensive testing later on. To begin, we will model the base price \( b \) at which turnips can be purchased on Sunday, as well as the twelve buyback prices \( X_0, \ldots, X_{11} \). The base price is determined as a uniformly distributed random integer between 90 and 110, i.e. \( b \sim \mathcal{U}\{90,\ldots,110\} \). The buyback prices are then allocated throughout the week as follows:
| mon | tue | wed | thu | fri | sat | |
|---|---|---|---|---|---|---|
| am | \(X_0\) | \(X_2\) | \(X_4\) | \(X_6\) | \(X_8\) | \(X_{10}\) |
| pm | \(X_1\) | \(X_3\) | \(X_5\) | \(X_7\) | \(X_9\) | \(X_{11}\) |
At the beginning of the week, buyback prices are fully determined but only revealed incrementally over time. These prices follow one of four general patterns, each with multiple variations. The four patterns are:
|
decreasing
contiually falling prices |
random
alternating prices |
|
small spike
first falling, then small spike |
large spike
first falling, then large spike |
First one of these patterns is choosen based on last weeks one. For example, if last weeks pattern was decreasing, there is only a \(5\%\) chance that this weeks pattern will also be decreasing. The table below lists the transition probabilities for all pattern combinations. Note how each row sums to \(1\) i.e. \(100\%\).
| last / next | dec | ss | ls | ran |
|---|---|---|---|---|
| dec | 0.05 | 0.25 | 0.45 | 0.25 |
| ss | 0.15 | 0.15 | 0.25 | 0.45 |
| ls | 0.20 | 0.25 | 0.05 | 0.50 |
| ran | 0.15 | 0.35 | 0.30 | 0.20 |
Simply knowing this table already allows us to make an informed decision about whether we should invest in any given week. For example, if last week's pattern was random, there is a \(65\%\) chance of experiencing at least a small spike this week. However, there is one caveat to this: What if we do not know last weeks pattern? In this case, it is helpfull to view this process (pattern choosing) as a so called Markov Chain. A Markov Chain is a stochastic process \(Z_\mathbb{N} \subset S\) where \(S\) is called its state space and \(Z_\mathbb{N}\) fullfills \[P(Z_{n+1} = s \,|\, Z_{n}, \ldots, Z_{1} ) = P(Z_{n+1} = s \,|\, Z_n) \] for all \(n \in \mathbb{N}\). This means that the probability of transitioning from one state to another depends only on the current state and not how the process arrived there. In the specific case of a finite space state \(S = \{s_1,\ldots,s_m\}\) we denote \(P(Z_{n+1} = s_i \,|\, Z_n = s_j) = p_{ij}\) and define the transition probability matrix \(P = (p_{ij})_{ij}\), which corresponds to the above table. Markov Chains are often visualized using diagrams that illustrate all possible states and their transition probabilities, as shown below:
Now, consider a row vector \(\pi_n\) where each entry represents the probability of being in a particular state at step \(n\). This vector evolves according to \(\pi_{n+1} = \pi_n P\). If the Markov Chain is irreducible (i.e., every state can be reached from any other state) and aperiodic (i.e., it does not follow a fixed cycle), then a unique limit distribution exists, satisfying \(\pi P = \pi\). This distribution provides the long-term probabilities of being in each state, even when the initial state is unknown. In the above example with the states \(A,B,C\) the limit distribution is \((0\ 0\ 1)\), try to think about why this is true (hint: that markov chain is not irreducible). In general one can find \(\pi\) simply by solving the defining equasion: \[\pi P = \pi\ \text{ where }\ \sum_i \pi_i = 1\] Using computational tools, we can efficiently solve for \(\pi\) by rewriting the equation as \(\pi \gets \pi^T\) and \((P^T-I)\pi = 0\) where \(\sum_i\pi_i = 1\). For example, using NumPy in Python we can use the following code. Also the first pattern is always small spike.
PY
def findStableState(transMat):
dim = transMat.shape[0]
target = transMat.T - np.eye(dim)
mat = np.vstack([target, np.ones(dim)])
vec = np.hstack([np.zeros(dim), 1])
return np.linalg.lstsq(mat, vec)[0]
After running this code on our matrix, we obtain the stable-state distribution for selecting the price pattern. This result answers our question about how to proceed when last weeks pattern is unknown (un).
| last \(\setminus\) next | dec | ss | ls | ran |
|---|---|---|---|---|
| un | 0.148 | 0.259 | 0.247 | 0.346 |
After a general pattern is choosen a concrete variation of it is determined. The way this works depends fully on the general pattern and will be discussed in the next four sections (one for each pattern).
We begin our discussion of individual buyback price patterns with the decreasing one. While it may be the least exciting from a traders perspective, it is the most interesting mathematically. Additionally, each of the other patterns contains a decreasing portion, making it necessary to start here.
This pattern is based on a set of random rates \(R_0,\ldots,R_{11}\) multiplied to the baseprice \(b\). Initially the rate is taken as \(R_0 \sim \mathcal{U}[0.85,\, 0.9]\) i.e. \(85\%\) to \(90\%\) of the base price. After that each halfday an independent change \(\Delta_i \sim \mathcal{U}[0.03,\, 0.05]\) of \(3\%\) to \(5\%\) is subtracted from the current rate to get the next one. Overall this pattern can be modeled as: \[ X_i := \lceil b \cdot R_i \rceil\ \text{ with }\ R_{i+1} = R_i - \Delta_i\] Implementing this with code is increadibly easy and our next step. However, finding the combined distribution of all twelfe prices \(X_0,\ldots,X_{11}\) is much more difficult and what makes this pattern so interesting. In JavaScript we first create a function that returns a uniform random float between a given max and min.
JS
UTIL.randFl = function(min, max) {
const delta = max - min;
return min + Math.random() * delta;
}
Next we perfectly represent the formula \(X_i := \lceil b \cdot R_i \rceil\) using an arrow function. Now we simply calculate the current rate (split in initial and regular) and push the corresponding price to an result array. Do not worry about the way I pass arguments into the function. It is just the JS way of defining a function with an variable amount of named parameters with default values.
JS
DEC.generate = function({
base = 100, length = 12,
init = { min: 0.85, max: 0.9 },
delta = { min: 0.03, max: 0.05 },
} = {}) {
const price = (rate) => Math.ceil(base * rate);
let prices = [];
// initial price calculation
let rate = UTIL.randFl(init.min, init.max);
prices.push(price(rate));
// regular price calculation
while (prices.length < length) {
rate -= UTIL.randFl(delta.min, delta.max);
prices.push(price(rate));
}
return prices;
}
Thats it! As promised increadibly easy. Running this code for a base price of \(100\) results in a week worths of possible buyback prices. Try it yourself below to get a feel for the general shape this pattern takes. Also note how there is no variation to the decreasing pattern; something that will change with further patterns.
Before we continue let's do a slight generalisation. This is necessary since the other patterns contain short decreasing phases that use a diffrent starting range and a diffrent change range. We restate our model \[X_i := \lceil b \cdot R_i \rceil\ \text{ with }\ R_{i+1} := R_i - \Delta_i\] by more generally defining \(R_0 \sim \mathcal{U}(H_0)\) and \(\Delta_i \sim \mathcal{U}(S)\) where \(H_0,\, S \subset \mathbb{R}\) are non-empty intervals while keeping the \(\Delta_i\) fully independent. Furthermore we define the smallest intervals \(H_i \subset \mathbb{R}\) such that \(R_i \in H_i\) almost surely. As an excercise you can think about how to find these in any concrete example. Now given an observation \(k_{i_1},\ldots,k_{i_m}\) of \(X_{i_j}\) we will need to calculate \[\begin{align} \mathcal{P} &:= P\Big(\forall\, j \leq m : X_{i_j} = k_{i_j}\Big)\\[5px] &= P\Big(\forall\, i \leq n : X_i \in A_i\Big)\ \text{ with }\ n \,:=\, i_m \end{align}\] the probability of the observed data being generated. Thereby we define \(A_i := \{k_i\}\) if \(i \in \{i_1,\ldots,i_m\}\) and \(A_i := \mathbb{Z}\) else. The later case models a missing observation. Next we reduce \(\mathcal{P}\) to the rate \(R_i\) by defining \( l\{k_i\} := \big(\frac{k_i-1}{b},\frac{k}{b}\big]\) and \(lA_i = H_i\) if \(A_i = \mathbb{Z}\). By definition of the \(X_i\) it follows \[ \mathcal{P} = P\Big( \forall\, i \leq n : R_i \in lA_i \Big) = P\big(R^{(n)} \in A\big) \] where \(R^{(n)} := (R_0,\ldots,R_n)\) and \(A := lA_0 \times \cdots \times lA_n\). Calculating this probability directly seems borderline impossible to me, since the resulting integration includes bounds that are dependend in such a way that even for \(n \geq 3\) the resulting formula gets just incomprehensibly messy. However, if you manage to do the integration or somehow otherwise find a neat general formula for \(P\big(R^{(n)} \in A\big)\) please contact me!
We will instead derive an asymptotic result that can be easily implemented in code, resulting in a clean way to approximate the needed probability. For that notice how \(\Delta^{(n)} := (R_0,\Delta_1,\ldots,\Delta_n)\) has fully independent components. Therefore it is easy to note its probability density function (pdf) as \[ \begin{align} f_{\Delta,n}\big(h_o,s_1,\ldots,s_n\big) &= f_0(h_0) \prod_{i\leq n} f_S(s_i) \\[5px] &= \frac{I\{h_0 \in H_0\}}{|H_0|} \prod_{i \leq n} \frac{I\{s_i \in S\}}{|S|} \end{align} \] where \(f_0 \sim \mathcal{U}(H_0)\) and \(f_S \sim \mathcal{U}(S)\). Using the pdf transformation theorem we can derive the pdf of \(R^{(n)}\). For this consider the linear map \(T : \mathbb{R}^{n+1} \to \mathbb{R}^{n+1},\, x \mapsto Tx\) given by the triangular matix \[ T = \left(\begin{smallmatrix} 1 & & \\ \vdots & \ddots & \\ 1 & \cdots & 1 \end{smallmatrix}\right)\ \text{ with }\ \det T \neq 0 \,. \] The latter point is needed to apply the theorem. Furthermore it holds \(\big(T\Delta^{(n)}\big)_i = R^{(n)}_i\), which leads us to \[ \begin{align} f_{R,n}\big(h_0,\ldots,h_1\big) &= f_{\Delta,n}\big(T^{-1}(\cdots)\big) \cdot \underbrace{\big|\det dT^{-1}(\cdots)\big|}_{=\ 1} \\[5px] &= f_{\Delta,n}\big( h_0, \underbrace{h_0-h_1}_{\widehat{=}\ s_1}, \ldots, \underbrace{h_{n-1} - h_{n}}_{\widehat{=}\ s_n} \big) \\[5px] &= \frac{I\{h_0 \in H_0\}}{|H_0|} \prod_{i \leq n} \frac{I\{h_{i-1} - h_{i} \in S\}}{|S|} \,. \end{align} \] This density takes only one value non-equal to zero on a set \(B \subset \mathbb{R}^{n+1}\) given by the indicators above. Since \(f_{R,n}\) is derived by density transformation it itself must be a density and hence \[ 1 = \int f_{R,n}\, d\lambda = \int_B \,d\lambda\, \frac{1}{|H_0| \cdot |S|^n} = \frac{|B|}{|H_0| \cdot |S|^n} \,.\] Therefore we can conclude \(f_{R,n} \sim \mathcal{U}(B)\) i.e. we now know the distribution of \(R^{(n)}\). However, make no mistake, directly calculating the needed probability still leads to the same integration. Luckly we now accuired the next formula \[ P\big( R^{(n)} \in A \big) = \frac{|A \cap B|}{|B|} = \frac{\phi \cdot |A|}{|H_0| \cdot |S|^n} \] where \(\phi\) is the ratio of \(A \cap B\) in \(A\). Next consider \(Y_i \sim \mathcal{U}(A)\) iid which leads to \(I\{Y_i \in B\} \sim Bin(1,\phi)\) iid and with \( \widehat{\phi}_\nu := \frac{1}{\nu} \sum_{i \leq \nu} I\{Y_i \in B\} \) we finally obtain the desired result for \(\mathcal{P}\) by applying the law of large numbers (LLN):
This can be easily implemented with code which is what we will do next! We will first implement \(Y_i \sim \mathcal{U}(A)\) and \(I\{Y_i \in B\}\) next we implement\(|A|\) and \(|H_0| \cdot |S|^n\) and finally \(\widehat{\phi}_\nu\) all based on an given observation. Let's start by defining a function head. Similary to generate() above we pass an settings object with some default values as well as the actual data.
JS
DEC.evaluate = function(data, {
base = 100, steps = 50000,
init = { min: 0.85, max: 0.9 },
delta = { min: 0.03, max: 0.05 },
} = {}) {
Everything we code from now on assumes to be inside the scope of calculate, i.e. we have acces to base, init, etc. To implement \(A \sim \mathcal{U}(A)\) first realise that \(Y_i \in \mathbb{R}^n\) where \(n\) is the number of observed datapoints including missing ones, but trimed on the right side. Furthermore remember how \(A = lA_0 \times \cdots \times lA_n\) where \(lA_i = H_i\) or \(lA_i = l\{k_i\}\) depending on whether a datapoint is missing or not. Now for each component simply pick a random point in the corresponding interval.
JS
// corresponds to Y_i ~ U(A)
const getRates = () => data.map((point, i) =>
// observed datapoint: pick from l{k_i}
point ? UTIL.randFl(point - 1, point) / base
// missing datapoint: pick from H_i
: UTIL.randFl(init.min - i * delta.max,
init.max - i * delta.min) );
To implement \(I\{Y_i \in B\}\) we need to check if the components of \(Y_i\) fulfil their corresponding indicators. Scrolling up we can see that there is an initial check for \(I\{h_0 \in H_0\}\) and a regular one for \(I\{h_i - h_{i-1} \in S\}\). Therby the intervals \(H_0\) and \(S\) correspond to init and delta in the code.
JS
// corresponds to I{Y_i ∈ B}
const areValid = (rates) => rates.every((rate, i) =>
// initial check: I{h_0 ∈ H_0}
!i ? rate >= init.min && rate <= init.max
// regular check: I{h_i - h_{i-1} ∈ S}
: rates[i - 1] - rate >= delta.min &&
rates[i - 1] - rate <= delta.max );
Calculating \(|A|\) requiers us to multiply the size of each \(lA_i\) where as before it can either be \(H_i\) or \(l{k_i}\) depending on whether the datapoint is missing or not. The sizes of these intervals can be easily found and they are \(|l\{k_i\}| = 1 / b\) as well as \(|H_i| = |H_0| + i |S|\). You might have gotten this last formula from before already!
JS
// corresponds to |A|
let rateSize = data.reduce((size, point, i) =>
// observed datapoint: multiply with |l{k_i}|
size * (point ? 1 / base
// missing datapoint: multiply with |H_i|
: init.max - init.min + i * (delta.max - delta.min)), 1 );
To obtain \(|H_0| \cdot |S|^n\) we simply multiply \(|H_0|\) and \(|S|\) the correct amount of times. It is very smilar to rateSize, so you already know how to do this!
JS
// corresponds to |H_0| * |S|^n
let normSize = data.reduce((size, _, i) =>
// initial point: multiply with |H_0|
size * (!i ? init.max - init.min
// regular point: multiply with |S|
: delta.max - delta.min), 1 );
Finally we can calculate \(\widehat{\phi}_\nu\) to use as an approximation for \(\phi\). Remember that \[\widehat{\phi}_\nu = \frac{1}{\nu} \sum_{i \leq \nu} I\{Y_i \in B\}\ \text{ and }\ P\big(R^{(n)} \in A\big) \approx \frac{\widehat{\phi}_\nu \cdot |A|}{|H_0| \cdot |S|^n} \,.\] You can stop here and try to see for yourself if you can put everything together now. Otherwise we will now restate calculate but do not fully repeat the above code blocks for readability.
JS
// settings object as stated above
DEC.evaluate = function(data, {...} = {}) {
// implemented as stated above
const getRates, areValid;
let rateSize, normSize;
// approximate phi by LLN
let ratio = 0;
for (let step = 0; step < steps; step++)
if (areValid(getRates())) ratio++;
ratio /= steps;
// corresonds to ~P(R^(n) in A)
return (ratio * rateSize) / normSize;
}
Thats it! Running this code on observed price data returns the probability that the decreasing pattern would generate that exact data. At this point we could stop and proceed with the next price pattern (don't worrie the other ones are much simpler from a mathematical standpoint). However, lets do a simple sanity check first by running calculate on data of length 2.
| \(k_0 \setminus k_1\) | 81 | 82 | 83 | 84 | 85 | 86 | 87 |
|---|---|---|---|---|---|---|---|
| 86 | 0.05 | 0.1 | 0.05 | ||||
| 87 | 0.05 | 0.1 | 0.05 | ||||
| 88 | 0.05 | 0.1 | 0.05 | ||||
| 89 | 0.05 | 0.1 | 0.05 | ||||
| 90 | 0.05 | 0.1 | 0.05 |
Empty cells have a probability of zero. Furthermore these probabilities can also be found by hand and are correct!
The next buyback price pattern we will discuss is the alternating one. Here we will see diffrent variations for the first time. A trader can make a profit of up to \(40\%\) making it somewhat interesting. Mathematically however, we already did everything difficult during the above discussion.
This pattern is based on alternating random and decreasing phases. These phases have variable length leeding to the diffrent variations of this pattern. Let \(S_i, D_i\) denote the length of the \(i\)-th random or decreasing phase. Then a variation is determined by uniformly pulling \(S_1, S_3\) and \(D_1\) while calculating the other lengths from that as follows.
| random #1 | decreasing #1 | random #2 | decreasing #2 | random #3 |
| \(S_1 \sim \mathcal{U}\{0,\ldots,6\}\) | \(D_1 \sim \mathcal{U}\{2,3\}\) | \(S_2 := 7-S_1-S_3\) | \(D_2 := 5 - D_1\) | \(S_3 \sim \mathcal{U}\{0,\ldots,6-S_1\}\) |
Note how \(S_1 + S_2 + S_3 = 7\) and \(D-1 + D_2 = 5\) which overall adds up to the \(12\) needed halfdays! Furthermore we can now calculate the number of possible variations, which will be needed later. \[ \#D_1 \cdot \#(S_1,S_3) = 2 \cdot \sum_{i=0}^6 (6-i) = 42 \] Let's now take a look at the two types of phases. First decreasing, which works exactly as the decreasing pattern above, with \(R_0 \sim \mathcal{U}[0.6, 0.8]\), \(\Delta_i \sim \mathcal{U}[0.04,0.1]\), and a length of \(D_i\). Basically an initial rate of \(60\%\) to \(80\%\) is choosen after which each halfday it is decreased by \(4\%\) to \(10\%\). Similary to this the random phase is also based on rates \(R_i\). However this time they do not depend on each other like before, insead we have \[ X_i := \lceil b \cdot R_i \rceil\ \text{ with }\ R_i \sim \mathcal{U}\big[0.9,1.4\big]\ \text{iid}\,.\] Implementing this with code is very easy and our next step. This time around it is also not to hard to find the combined distribution of the twelfe prices \(X_0,\ldots,X_{11}\). In JavaScript we first implement generate for the random phases. Doing this simply means creating a random rate between a given min and max for each halfday and calculating the corresponding price.
JS
RAN.generate = function({
base = 100, length = 4,
min = 0.9, max = 1.4
} = {}) {
const price = (rate) => Math.ceil(base * rate);
let prices = [];
// regular price calculation
while (prices.length < length) {
let rate = UTIL.randFl(min, max);
prices.push(price(rate));
}
return prices;
}
The generate function of the alternating pattern will need a base price, its variant information (\(S_1, S_3, D_1\)), and the settings for its random and decreasing phases. We therefore first state its head.
JS
ALT.generate = function({
base = 100, ran1 = 2, ran3 = 2, dec1 = 2,
ran = { min: 0.9, max: 1.4 },
dec = { init: { min: 0.6, max: 0.8 },
delta: { min: 0.04, max: 0.1 } }
} = {}) {...}
Now we are ready to implement it! To do this first we first find the phase lengths according to the above table. Next simply alternate between randoms and decreasings generate function while creating the corresponding number of prices.
JS
ALT.generate = function({...} = {}) {
// find phase lenghts from variation
let ran2 = 7 - ran1 - ran3, dec2 = 5 - dec1;
let phases = [ran1, dec1, ran2, dec2, ran3];
// alternating price generation
return phases.reduce((prices, length, i) => {
const phase = [RAN, DEC][i % 2].generate;
let settings = [ran, dec][i % 2];
return prices.concat(phase({ base, length, ...settings }));
}, []);
}
Thats it! Running this code for a base price of 100 results in a week worths of possible buyback prices. Try it yourself below to get a feel for the general shape this pattern takes. Also note the differences between different variations of this pattern!
We continue by finding an expression for the same probability as before. Given an observation \(k_{i_1},\ldots,k_{i_m}\) we define \(A_i := \{k_i\}\) if \(i \in \{i_1,\ldots,i_m\}\) and \(A_i := \mathbb{Z}\) which again allows us to rewrite the needed probability as \[ \begin{align} \mathcal{P} &:= P\Big(\forall\, j \leq m : X_{i_j} = k_{i_j}\Big)\\[5px] &= P\Big(\forall\, i \leq n : X_i \in A_i\Big)\ \text{ with }\ n \,:=\, i_m \,.\end{align} \] Next observe that the five phases of this pattern are fully independent. By spliting the indices \(I = I_1 \sqcup \cdots \sqcup I_5\) (below denoted by \(\text{RAN} := \{I_1, I_3, I_5\}\) and \(\text{DEC} := \{I_2, I_4\}\)) into their according phases, we therefore obtain:
The latter probabilities are simply given by the decreasing pattern. The first probabilities can easily be calculated. For that define \(l\{k_i\} := \big( \frac{k_i-1}{b}, \frac{k_i}{b} \big]\) and \(lA_i := \big[0.9,1.4\big]\) if \(A_i = \mathbb{Z}\). This allows us to do the same trick as before, where we think about the rates \(R_i\) instead of the prices \(X_i\) directly. Now simply integrate the pdf \(f \sim R_i\) over the according \(lA_i\). \[ \begin{align} P(X_i \in A_i) &= P(R_i \in lA_i) = \int_{lA_i} f \,d\lambda \\ &= \tfrac{1}{1.4-0.9} \int_{\frac{k_i-1}{b}}^{\frac{k_i}{b}} I_{[0.9,1.4]}(x) \,dx \\[5px] &= \tfrac{1}{1.4-0.9}\, \Big[ \Big(\tfrac{k_i}{b} \vee 1.4\Big) - \Big(\tfrac{k_i-1}{b} \wedge 0.9\Big) \Big] \cdot I\Big\{ \tfrac{k_i}{b} \geq 0.9,\, \tfrac{k_i-1}{b} \leq 1.4 \Big\} \end{align} \] Where \(\vee\) and \(\wedge\) are min and max. Also this assumes \(A_i = \{k_i\}\) since for \(A_i = \mathbb{Z}\) the above probability evaluates simply to 1. This can be nicly implemented with code which is what we will do next! To start we implement the formula for \(P(X_i \in A)\). Lets first take a look at the function head.
JS
RAN.evaluate = function(data, {
base = 100,
min = 0.9, max = 1.4
} = {}) {...}
From here on all code is assumed to be inside the scope of RAN.evaluate. We will need the indicator \(I\big\{ \tfrac{k_i}{b} \geq 0.9,\, \tfrac{k_i-1}{b} \leq 1.4 \big\}\) which can be implemented very similar to earlier indicators as follows.
JS
// corresponds to I{k_i / b >= 0.9, (k_i - 1) / b <= 1.4}
const isValid = (point) =>
point / base >= min && (point - 1) / base <= max;
Next we need to calculate the product \(\prod_{i \in S} P(R_i \in lA_i)\). For this we just follow the above formula, but keeping in mind, that a missing datapoint should result in a probability of 1.
JS
// corresponds to \prod_(i ∈ S) P(R_i ∈ lA_i)
return data.reduce((prob, point) =>
// missing datapoint: multiply with 1
prob * ( !point ? prob
// observed datapoint: multiply with P(R_i ∈ lA_i)
: ( Math.min(point / base, max)
- Math.max((point - 1) / base, min) )
/ (max - min) * isValid(point) ), 1 );
Putting everything together the evaluation for the random phases is actually quite simple:
JS
// settings object as stated above
RAN.evaluate = function(data, {...} = {}) {
// implemented as stated above
const isValid;
return data.reduce(...);
}
The evaluation function of the alternating pattern is extremly similar to its generate function. It starts with the the same function head but with added data of course.
JS
ALT.evaluate = function(
data, {
base = 100, ran1 = 2, ran3 = 2, dec1 = 2,
ran = { min: 0.9, max: 1.4 },
dec = { init: { min: 0.6, max: 0.8 },
delta: { min: 0.04, max: 0.1 } }
} = {}
) {...}
Next there are three steps. First we find the phase lengths from the variation. Then we split the data array into subarrays according to the phase lengths. And finally we apply RAN.evaluate and DEC.evaluate in an alternating fashion to these subarrays multiplying the results.
JS
ALT.evaluate = function(
data, {...} = {}
) {
// find phase lenghts from variation
let ran2 = 7 - ran1 - ran3, dec2 = 5 - dec1;
let phases = [ran1, dec1, ran2, dec2, ran3];
// split data into phases
phases.reduce((data, len, i) => {
phases[i] = data.slice(0, len);
return data.slice(len);
}, data);
// alternating price evaluation
return phases.reduce((prob, data, i) => {
const phase = [RAN, DEC][i % 2].evaluate;
let settings = [ran, dec][i % 2];
return prob * phase(data, { base, ...settings });
}, 1);
}
Thats it! Running this code on observed price data returns the probability that a specific variation of the alternating pattern would generate that exact data.
We continue our discussion with the small spike pattern. This one is quite exciting for the trader since a maximum profit of \(100\%\) is possible. Mathematically we again have almost everything needed ready, however there is at least some interesting math left here!
This pattern is based on a custom (small) spike phase surrounded by random and decreasing phases. This time around only the decreasing phases have variable lengths, leading to fewer variations in the pattern. Let \(D_i, B, S\) denote the lengths of the decreasing phases, the random phase, and the spike phase respectivly. Then a variation is determined by uniformlly pulling \(D_1\) while calculating the other lengths from that as follows.
| decreasing #1 | random | custom spike | decreasing #2 |
| \(D_1 \sim \mathcal{U}\{0,\ldots,7\}\) | \(S := 2\) | \(B := 3\) | \(D_2 := 7 - D_1\) |
Note how \(D_1 + D_2 = 7\) and how all phase lengths overall add up to \(12\). In this case it is also trivial to find the number of possible variations, which is simply \(\#D_1 = 8\). Again we will need this information later. Let's now take a look at the diffrent phases. Decreasing uses \(R_0 \sim \mathcal{U}[0.4,0.9]\) and \(\Delta_i \sim \mathcal{U}[0.03,0.05]\), while Random uses \(R_i \sim \mathcal{U}[0.9,1.4]\).
The Spike pattern is interessting in that it is always set to a length of \(3\) while being based on a max rate \(M \sim \mathcal{U}[1.4,2]\). Each price \(X_i\) is pulled based on this \(M\). The following table demonstrates all used formulas.
| price | \(X_{i_1} := \lceil b \cdot R_{i_1} \rceil\) - 1 | \(X_{i_2} := \lceil b \cdot R_{i_2} \rceil\) | \(X_{i_3} := \lceil b \cdot R_{i_3} \rceil\) - 1 |
|---|---|---|---|
| rate | \(R_{i_1} \sim \mathcal{U}[1.4,M]\) | \(R_{i_2} := M\) | \(R_{i_3} \sim \mathcal{U}[1.4,M]\) |
To implement this with code we basically just need to deal with the custom spike pattern, since everything else is already done. The spike pattern however just consists of the above three formulas so the code is extremly simple.
JS
CST.generate = function({
base = 100, min = 1.4, max = 2
} = {}) {
const price = (rate) => Math.ceil(base * rate);
let prices = [];
// generate prices according to formulas
let maxRate = UTIL.randFl(min, max);
prices[0] = price(UTIL.randFl(min, maxRate)) - 1;
prices[1] = price(maxRate);
prices[2] = price(UTIL.randFl(min, maxRate)) - 1;
return prices;
}
The generate function of the small spike pattern will need a base price, its variant information (\(D_1\)), and the settings for all of its diffrent phases. We therefore first state its head.
JS
SS.generate = function({
base = 100, dec1 = 4,
dec = { init: { min: 0.4, max: 0.9 },
delta: { min: 0.03, max: 0.05 } },
ran = { min: 0.9, max: 1.4 },
cst = { min: 1.4, max: 2 }
} = {}) {...}
The logic of the function is the exact same to what we already did with ALT.generate. First find all the phase lengths from the given variaation. Then simply generate that many prices from the correct phase and concat them all together. At this point you probably know the drill already!
JS
SS.generate = function({...} = {}) {
// find phase lengths from variation
let dec2 = 7 - dec1, ran1 = 2, cst1 = 3;
let phases = [dec1, ran1, cst1, dec2];
// individual phase price generation
return phases.reduce((prices, length, i) => {
const phase = [DEC, RAN, CST][i % 3].generate;
let settings = [dec, ran, cst][i % 3];
return prices.concat(phase({ base, length, ...settings}));
}, []);
}
Thats it! Running this code for a base price of 100 results in a week worths of possible buyback prices. Try it yourself below to get a feel for the general shape this pattern takes. Also note the diffrences between diffrent variations of this pattern!
We continue by finding an expression for the same probability as always. Given an observation \(k_{i_1},\ldots,k_{i_m}\) we define \(A_i := \{k_i\}\) if \(i \in \{i_1,\ldots,i_m\}\) and \(A_i := \mathbb{Z}\) which again allows us to rewrite the needed probability as \[ \begin{align} \mathbb{P} &:= P\Big(\forall\, j \leq m : X_{i_j} = k_{i_j}\Big)\\[5px] &= P\Big(\forall\, i \leq n : X_i \in A_i\Big)\,.\end{align} \] Next observe that the four phases of this pattern are fully independent. By spliting the indices \(I = I_1 \sqcup \cdots \sqcup I_4\) (below denoted by \(\text{DEC} := \{I_1, I_4\}\), \(\text{RAN} := I_2\), and \(\text{CST} := I_3\)) into their according phases we obtain therefore:
Most of this expression was already discussed. Finding an expression for the remaining probability is again slithly more difficult since there is the dependence of the max rate \(M\). We will analyse this in a more general sense. Let \(M \sim \mathcal{U}(H)\) and \(R,S \sim \mathcal{U}(T(M))\) iid where \(H := [h_{\text{low}}, h_{\text{high}}]\) and \(T(m) := [t_{\text{low}}, m]\). Consider the following probability. \[ \begin{align} \mathcal{P} &:= P\Big( M \in lA_M,\, (R,S) \in lA_{\neg M} \Big)\\[7.5px] &\overset{\star_1}{=} \int_{lA_M} f_M \int_{lA_{\neg M}} f_{(R,S) | M} \,d\lambda^2\ d\lambda\\[7.5px] &\overset{\star_2}{=} \int_{lA_M} f_M\, \bigg( \int_{lA_R}f_{R|M}\,d\lambda \int_{lA_S} f_{S|M}\,d\lambda \bigg)\, d\lambda \end{align} \] Where we used the law of total probability at \(\star_1\) and assumed \(lA_{\neg M} = lA_R \times lA_S\) at \(\star_2\). The inner two integrals are bascially the same and evaluate to a formula we have already seen in an concrete example (alternating pattern - random probability). Furthermore we will assume that \(M > t_\text{low}\) almost surely (this is the case in CST). \[ \begin{align} \rho_R(m) &:= \int_{lA_R} f_{R|M = m} (r) \,dr = \frac{1}{|T(m)|} \int_{lA_R} I_{T(m)}(r)\,dr = \frac{|T(m) \cap lA_R|}{|T(m)|}\\[7.5px] &= \tfrac{1}{m - t_\text{low}} \Big[ \Big( \tfrac{k_R}{b} \vee m \Big) - \Big( \tfrac{k_R-1}{b} \wedge t_\text{low} \Big) \Big] \cdot I\Big\{ \tfrac{k_R}{b} \geq t_\text{low},\, \tfrac{k_R-1}{b} \leq m \Big\} \end{align} \] We obviously assumed \(A_R = \{k_R\}\), else (if \(A_R = \mathbb{Z}\) i.e. unknown datapoint) the above formula simply evaluates to 1. Integrating \(\rho_R\) or even \(\rho_R \rho_S\) by hand might just be possible if one really wanted to, but similarry to the decreasing pattern a numerical approach might be more practical. We will use a so called Riemann approximation \[ \int_a^b f(x)\, dx \,\approx\, \sum_{k=0}^{n-1} f(x_k)\, \delta_n\] where \(x_k := a + k \cdot \delta_n\), \(\delta_n := (b-a) / n\), and \(k=0,\ldots,n-1\). The higher one chooses \(n \in \mathbb{N}\) the better the approximation. Next we define \(Y := qX\) where \(qX_i = X_i + 1\) if \(i = 1,3\) and \(qX_2 = X_2\). This fixes the issue of \(X_{i_1}\) and \(X_{i_3}\) not being directly defined via their corresponding rates, but rather shifted by \(-1\). Putting everything together, we start with \[ P_\text{CST} := P\Big(X_\text{CST} \subset A_\text{CST}\Big) = P\Big(Y_\text{CST} \subset qA_\text{CST}\Big) = P\Big( R_\text{CST} \subset lqA_\text{CST} \Big)\,. \] Next set \(M = R_{i_2},\, R = R_{i_1},\, S = R_{i_3}\) and update \(A_R \gets qA_R,\, A_S \gets qA_S\) and let \(x_k,\, \delta_n\), as described above, be a partition of \(lA_M \cap H\) (note that \(qA_M = A_M\)). Finally we get the desired formula for the missing porbability:
We can now start implementing everything with code! First the Riemann approximation. We will simply use the left most point of each section for the function evaluation. It is also possible to use the right or the middle point though.
JS
UTIL.integrate = function(
funct, low, high, steps = 50000) {
// step size and k-th step
const delta = (high - low) / steps;
const step = (k) => low + k * delta;
// riemann sum calculation
let result = 0;
for (let k = 0; k < steps; k++)
result += funct(step(k));
return result * delta;
}
Next let's work on implementing the formula for \(P_\text{CST}\). To do this we rewrite it one last time, in a way that should make it clear how to code it and where the observered data actually shows up. \[ P_\text{CST} = \tfrac{1}{h_\text{high}-h_\text{low}} \int_{h_\text{low} \wedge \frac{k_M-1}{b}}^{h_\text{high} \wedge \frac{k_M}{b}} \rho(k_R,m)\, \rho(k_S,m) \, dm \cdot I\Big\{ \textstyle \frac{k_M}{b} \geq h_\text{low},\, \frac{k_M-1}{b} \leq h_\text{high}\Big\}\] The extra indicator at the end shows up when changing the integration region from simply \(lA_M \cap H\) to concrete bounds, as to make sure that if the region is empty, we actually get \(0\). Finally let's take a look at the function head.
JS
CST.evaluate = function(data, {
base = 100,
min = 1.4, max = 2,
steps = 50000
} = {}) {...}
All further code is assumed to be inside evaluate. We now implement \(\rho(k,m)\) in two parts, the indicator and the actual calculation. Note that we overwrite the global max parameter with a local one. This corresponds to \(h_\text{high}\) (global max) and \(m\) (local max) as discussed above. Also if \(k_R\) or \(k_S\) are not observered we just return 1.
JS
// corresponds to I{k/b >= t_low, (k-1)/b <= m}
const isValid = (point, max) =>
point / base >= min && (point - 1) / base <= max;
// corresponds to rho(m)
const rho = (point, max) =>
// missing datapoint: return 1
!point ? 1
// observed datapoint: return rho(m)
: ( Math.min(point / base, max)
- Math.max((point - 1) / base, min) )
/ (max - min) * isValid(point, max);
The second indicator we need (see final formula for \(P_\text{CST}\)) can be created by reusing isValid above. Therefore we can now prepare the the Riemann Approximation. It is important to not forget the Transformation \(q\), however as it is so simple, we can just directly write down its effect (data[0]+1 and data[2]+1) in the code.
JS
// corresponds to rho(k_R,m) * rho(k_S,m)
const funct = x => rho(data[0] + 1, x) * rho(data[2] + 1, x);
// corresponds to max(h_low, (k_M-1)/b) and min(h_high, k_M/b)
const low = Math.max((data[1] - 1) / base, min);
const high = Math.min(data[1] / base, max);
The last part is to simply put everything together and perform the Riemann Approximation as above desribed. We therefore restate the function, but with reduced details for readibility. Also we will only perform an Approximation in the case that any data was observed at all, else we just return 1, or 0 if the integration region is emtpy.
JS
CST.evaluate = function(data, {...} = {}) {
// implemented as stated above
const isValid, rho;
const funct, low, high;
// only perform approximation if any data was
// observed and integration region not empty
if (data.every(x => !x)) return 1;
if (!isValid(data[1])) return 0;
return UTIL.integrate(funct, low, high, steps) / (max - min);
}
We are basically done now, as the Small Spike Pattern consists of three independent phases we simply need to go through each phase, evaluate it, and then multiply the results. In fact we did the exact same thing with the Alternating Pattern. Therefore we can go over this quiet quickly. First the function head.
JS
SS.evaluate = function(data, {
base = 100, dec1 = 4,
dec = { init: { min: 0.4, max: 0.9 },
delta: { min: 0.03, max: 0.05 } },
ran = { min: 0.9, max: 1.4 },
cst = { min: 1.4, max: 2 }
} = {}) {...}
And then the rest of the code. Again if you struggle to understand what we are doing, you can check with ALT.evaluate one section up. It is truly the exact same concept. Spoiler: The final pattern will be the same also.
JS
SS.evaluate = function(data, {...} = {}) {
// find phase lengths from variation
let dec2 = 7 - dec1, ran1 = 2, cst1 = 3;
let phases = [dec1, ran1, cst1, dec2];
// split data into phases
phases.reduce((data, len, i) => {
phases[i] = data.slice(0, len);
return data.slice(len);
}, data);
// phase based price evaluation
return phases.reduce((prob, data, i) => {
const phase = [DEC, RAN, CST][i % 3].evaluate;
let settings = [dec, ran, cst][i % 3];
return prob * phase(data, { base, ...settings });
}, 1);
}
Thats it! Running this code on observed price data returns the probability that a specific variation of the Small Spike Pattern would generate that exact data.
Last but certainly not least we discuss the Large Spike Pattern. This one is by far the best pattern, as if you recognise it early enough, a minimum profit of 100% while a maximum of 500% is possible. Also at this point we have already covered everything needed to fully model this pattern!
This pattern is, just as Small Spike before, also based on a custom (large) spike phase surrounded by random and decreasing phases. Again only the decreasing phase has a variable length. Let \(D, B, S\) denote the lengths of the decreasing phase, the random phase, and the spike phase respectivly. Then a variation is determined by uniformmly pulling \(D\) while calculating the other lengths from that as follows.
| decreasing | custom spike | random |
| \(D \sim \mathcal{U}\{1,\ldots,7\}\) | \(B := 5\) | \(S := 7 - D\) |
Note how all phase lengths overall add up to \(12\). Again it is trivial to find the number of possible variations, which is simply \(\#D = 7\). We will finally make use of this information in the next section. The decreasing phase uses \(R_0 \sim \mathcal{U}[0.85,0.9]\) and \(\Delta_i \sim \mathcal{U}[0.03,0.05]\), while the random phase uses \(R_i \sim \mathcal{U}[0.4,0.9]\).
The Spike Phase is (luckly) pretty boring, as it can be understood as five independend random phases. With \(X_i = \lceil b \cdot R_i \rceil\) as always each rate \(R_i \sim \mathcal{U}[h_\text{min}, h_\text{max}]\) is uniformlly pulled between a min and max value as follows.
| rate | \(R_1\) | \(R_2\) | \(R_3\) | \(R_4\) | \(R_5\) |
|---|---|---|---|---|---|
| max | 1.4 | 2 | 6 | 2 | 1.4 |
| min | 0.9 | 1.4 | 2 | 1.4 | 0.9 |
To implement this with code, we simply need to call RAN.generate 5 times with the above settings. This makes the code very simple, as it doesn't contain any new logic.
JS
CST.generate = function({
base = 100,
min = [0.9, 1.4, 2, 1.4, 0.9],
max = [1.4, 2, 6, 2, 1.4],
} = {}) {
// generate 5 random rates using RAN.generate
let prices = [];
for (let i = 0; i < 5; i++) {
let range = { min: min[i], max: max[i] };
let settings = { base, length: 1, ...range };
prices.push(...RAN.generate(settings));
}
return prices;
}
The generate function of the Large Spike Pattern will need a base price, its variant information \(D\), and the settings for all of its diffrent phases. We therefore first state its head.
JS
LS.generate = function({
base = 100, dec1 = 4,
dec = { init: { min: 0.85, max: 0.9 },
delta: { min: 0.03, max: 0.05 } },
ran = { min: 0.4, max: 0.9 },
cst = { min: [0.9, 1.4, 2, 1.4, 0.9],
max: [1.4, 2, 6, 2, 1.4] }
} = {}) {...}
The rest of this function is the analoug to the Small Spike and Alternating Pattern. Nothing to say here anymore.
JS
LS.generate = function({...} = {}) {
// find phase lengths from variation
let ran1 = 7 - dec1, cst1 = 5;
let phases = [dec1, cst1, ran1];
// individual phase price generation
return phases.reduce((prices, length, i) => {
const phase = [DEC, CST, RAN][i % 3].generate;
let settings = [dec, cst, ran][i % 3];
return prices.concat(phase({ base, length, ...settings }));
}, []);
}
Thats it! Running this code for a base price of 100 results in a week worths of possible buyback prices. Try it yourself below to get a feel for the general shape this pattern takes. Also note the diffrences between diffrent variations of this pattern!
Next we quickly find the formula for the needed probability. Luckly there is no more complicated math involved as everything we need was already discussed. Given an observation \(k_{i_1},\ldots,k_{i_m}\) we define \(A_i := \{k_i\}\) if \(i \in \{i_1,\ldots,i_m\}\) and \(A_i := \mathbb{Z}\) which allows us to state \[\mathbb{P} := P \Big( \forall j \leq m : X_{i_j} = k_{i_j} \Big) = P\Big( \forall i \leq n : X_i \in A_i \Big)\,.\] Next we use that the three phases of this pattern, as well as the halfdays of the custom phase, are fully independent. By spliting the indices \(I = I_1 \sqcup I_1 \sqcup I_2 \sqcup I_3\) (below denoted by \(\text{DEC} := I_1,\, \text{CST} := I_2,\, \text{RAN} := I_3\)) into their according phases we obtain:
The \(P(X_i \in A_i)\) for \(i \in \text{CST}\) can be calculated as before with the Random Pattern. Hence all of this expression was already covered and we are done with the math. Next we implement the evaluate function of this custom pattern i.e \(\prod_{i \in \text{CST}}P(X_i \in A_i)\). To do this we can simply call RAN.evaluate on each datapoint and multiply the results.
JS
CST.evaluate = function(data, {
base = 100,
min = [0.9, 1.4, 2, 1.4, 0.9],
max = [1.4, 2, 6, 2, 1.4]
} = {} ) {
// independent halfdays allow to siply use RAN.evaluate 5 times
return data.reduce((prob, point, i) => {
let range = { min: min[i], max: max[i] };
return prob * RAN.evaluate([point], { base, ...range });
}, 1);
}
The evaluate function of the entire Large Spike pattern looks basically the same as for Small Spike or even Alternating. Find phase lengths, split data, calculate probabilities for each phase, multiply together. You know the drill by now! First the function head.
JS
LS.evaluate = function(data, {
base = 100, dec1 = 4,
dec = { init: { min: 0.85, max: 0.9 },
delta: { min: 0.03, max: 0.05 } },
ran = { min: 0.4, max: 0.9 },
cst = { min: [0.9, 1.4, 2, 1.4, 0.9],
max: [1.4, 2, 6, 2, 1.4] }
} = {}) {...}
Finally for the last time in this section, and again analoug to what we did before the rest of the logic implementing the formula for \(\mathbb{P}\). You can scroll up to the Alternating Pattern for a more detailed explanation.
JS
LS.evaluate = function(data, {...} = {}) {
// find phase lengths from variation
let ran1 = 7 - dec1, cst1 = 5;
let phases = [dec1, cst1, ran1];
// split data into phases
phases.reduce((data, len, i) => {
phases[i] = data.slice(0, len);
return data.slice(len);
}, data);
// phase based price evaluation
return phases.reduce((prob, data, i) => {
const phase = [DEC, CST, RAN][i % 3].evaluate;
let settings = [dec, cst, ran][i % 3];
return prob * phase(data, { base, ...settings });
}, 1);
}
Thats it! Running this code on observed price data returns the probability that a specific variation of the Large Spike Pattern would generate that exact data. Moving on, we will put all of the results from the past four sections to good use. Most of the work, definetly at least the hard work, has now been done. Congrats if you made it this far!
We will now discuss how to maximize our profits from trading turnips. For that notice how each pattern has a clearly optimal spot to sell. Realizing that all we need to do is determine the most likely pattern to have produced our observed data \(X\). This then gives the most likely optimal selling point and thus our strategy. From the calculations we did before one can tell the total number of patterns \(N\) to be \[ N = \underbrace{\#\text{DEC}}_{=\,1} + \underbrace{\#\text{ALT}}_{=\,56} + \underbrace{\#\text{SS}}_{=\,8} + \underbrace{\#\text{LS}}_{=\,7} = 72\,.\] Each of these patterns might have a different optimal selling point, which is what we determine next. First let's look at the easiest pattern for this: Decreasing has only one possible variation and therefore only one optimal selling point. Remember that \(X_i = \lceil b \cdot R_i \rceil\) with \(R_{i+1} = R_i - \Delta_i\) and the settings: \[ R_0 \sim \mathcal{U}[0.85,0.9],\ \Delta_i \sim \mathcal{U}[0.03,0.05] \] Hence we can determine the best and worst possible outcome and thus also the optimal selling point. Basically this pattern will have you lose money everytime and you should exit as soon as possible to minimize your losses. The optimal halfday to sell is therefore always \(i_{\text{op}, \text{DEC}} = 0\).
| profit | how | when | |
|---|---|---|---|
| best | \(-10\%\) | \(R_0 = 0.9\) | mon am |
| worst | \(-70\%\) | \(R_0 = 0.85,\, \forall i: \Delta_i = 0.05\) | sat pm |
Next is the Alternating Pattern. This one has \(56\) variations determined by the length \(D_1\) of its initial Decreasing Phase, as well as the dependent lengths \((S_1,S_3)\) of the first and third Random Phases. Same as before we have \(X_i = \lceil b \cdot R_i \rceil\) but now either \(R_I \sim \text{RAN}\) or \(R_I \sim \text{DEC}\) with the settings: \[ \begin{array}{c} \text{RAN} : R_i \sim \mathcal{U}[0.9,1.4] \\[2.5px] \text{DEC} : R_0 \sim \mathcal{U}[0.6,0.8],\, \Delta_i \sim \mathcal{U}[0.04,0.1] \end{array} \] It is clear that the best possible outcome must be during a Random Phase and the worst during a Decreasing one, as the worst RAN produces is \(0.9\) while the best DEC produces is \(0.6\). However as RAN determines prices independently each halfday, there can not be pinpointed an exact halfday to sell. However the strategy here is to sell during a Random phase when \(R_i > 1\) and optimally close to \(1.4\). It is advised to not be greedy as profit is not guaranteed.
| profit | how | when | |
|---|---|---|---|
| best | \(+40\%\) | \(\text{RAN} : R_i = 1.4\) | any RAN HD |
| worst | \(-60\%\) | \(\text{DEC}\, (3\,\text{HD}) : R_0 = 0.6,\, \forall i : \Delta_i = 0.1\) | 3th DEC HD |
Given a variant of the Alternating Pattern desribed by \(D_1, (S_1,S_3)\) as said, one can calculate the halfdays of the three possible Random phases and therefore the halfdays, where profit is possible. The optimal halfday to sell \(i_{\text{op}, \text{ALT}}\) is therefore somewhere in \(I_{\text{op},\text{ALT},1} \cup I_{\text{op},\text{ALT},2} \cup I_{\text{op},\text{ALT},3}\) as defined below. \[ \begin{array}{c} I_{\text{op},\text{ALT},1} = \{0,\ldots, S_1 - 1\} \\[2.5px] I_{\text{op},\text{ALT},2} = \{ S_1 + D_1, \ldots, S_1 + D_1 + S_2 - 1\} \\[2.5px] I_{\text{op},\text{ALT},3} = \{S_1 + D_1 + S_2 + D_2,\ldots,11\} \end{array} \] Next we discuss the Small Spike Pattern. This one has \(8\) variations determined by the length \(D_1\) of its first Decreasing phase. Again we have \(X_i = \lceil b \cdot R_i \rceil\) while \(R_I \sim \text{RAN}\) or \(R_I \sim \text{DEC}\), however during the \(3\) HD long CST phase the first and third price gets subtracted by \(1\). If you scroll back up to the Small Spike discussion we already had, you will see that the 2nd halfday of the custom spike phase is guaranteed to be the best, because of the mentioned subtraction by \(1\). Hence we only state the settings for the maximum rate \(M\) of CST below. \[ \begin{array}{c} \text{RAN} : R_i \sim \mathcal{U}[0.9,1.4] \\[2.5px] \text{DEC} : R_0 \sim \mathcal{U}[0.4,0.9],\, \Delta_i \sim \mathcal{U}[0.03,0.05] \\[2.5px] \text{CST} : M \sim \mathcal{U}[1.4,2] \end{array} \] The best possible outcome will happen during the second halfday of CST. The worst possible outcome will happen during the last halfday of the longest possible Decreasing phase, similar to above. However it should be noted, that while not the best option, selling during a Random phase if \(R_i > 1\) close to \(1.4\) is also a decent option.
| profit | how | when | |
|---|---|---|---|
| best | \(+100\%\) | \(\text{CST} : M = 2\) | 2nd CST HD |
| worst | \(-90\%\) | \(\text{DEC}\, (7\,\text{HD}) : R_0 = 0.4,\, \forall i : \Delta_i = 0.05 \) | 7th DEC HD |
Given a variation of SS by \(D_1\) as explained above, we can find the optimal halfday \(i_\text{op}\) to sell by simply adding the legth \(S = 2\) of the random phase after the initial decreasing one and the optimal time \(i_\text{op, CST} = 2\) to sell during CST i.e. \(i_\text{op, SS} = D_1 + S + i_\text{op, CST} = D_1 + 4\).
Lastly we have the Large Spike Pattern. This one has \(7\) variations determined by the length \(D\) of its Decreasing phase. Here we again simply have \(X_i = \lceil b \cdot R_i \rceil\) with \(R_I \sim \text{DEC}, \text{CST}, \text{RAN}\). Just like with the Small Spike pattern, you can scroll up and see that just the rate \(R_3\) of CST matters for thi discussion, as it is the most extreme one. \[ \begin{array}{c} \text{RAN} : R_i \sim \mathcal{U}[0.4,0.9] \\[2.5px] \text{DEC} : R_0 \sim \mathcal{U}[0.85,0.9],\, \Delta_i \sim \mathcal{U}[0.03,0.05] \\[2.5px] \text{CST} : R_3 \sim \mathcal{U}[2,6] \end{array} \] The best possible outcome will happen during the thrid halfday of CST. The worst possible outcome will happen during the Random phase. Note that during a Large Spike Pattern the only way to make profit is during the CST phase, as both DEC and RAN only produce rates \(R_i \leq 0.9\).
| profit | how | when | |
|---|---|---|---|
| best | \(+500\%\) | \(\text{CST} : R_3 = 6\) | 3th CST HD |
| worst | \(-60\%\) | \(\text{RAN} : R_i = 0.4\) | any RAN HD |
Given a variation of LS by \(D\), we can now find the optimal halfday \(i_\text{op}\) to sell by simply adding \(i_\text{op, CST} = 2\) to \(D\), as we need to get to the third halfday of the custom phase, which comes right after the decreasing one i.e. \(i_\text{op,LS} = D + i_\text{op, CST} = D + 2\).
We will now focus on the the likelihood evaluation of each pattern. Given observed Data \(X\) we want to rank each of the \(72\) pattern variations \(V_k\) by the probability \(P(V_k | X)\). To do this we employ Bayes theorem as well as the law of total probability resulting in the following formula. \[ P(V_k | X) = \frac{P(X|V_k)P(V_k)}{P(X)} = \frac{P(X|V_k)P(V_k)}{\sum_i P(X | V_i)P(V_i)}\] Notice how most of this expression was already prepared by us: \(P(X|V_k)\) are exactly the probabilities we calculated in the four sections before and implemented as evaluate functions; \(P(V_k)\) are almost given by the probabilities discussed in the first chapter. However, as \(P(V_k)\) is the probability of an concrete pattern variation and not just a general pattern we still need to do some work.
Let \(P(\text{DEC}),\, P(\text{ALT}),\, P(\text{SS}),\, P(\text{LS})\) be the pattern transition probabilities as discussed in the first section. For this discussion their dependence on the previous weeks pattern does not matter and is therefore not reflected in the notation. Just note, that once we implement this with code, we obviously will have to choose the right ones. Then the probabilitie of choosen a concrete pattern variation can be found by considering the following chains of independent events. For Decreasing there is only one variation so we simply get \[ \bullet \xrightarrow{P(\text{DEC})} \text{DEC} \xrightarrow{1} V_\text{DEC}\] i.e. \(P(V_\text{DEC}) = P(\text{DEC})\). Next we will cover Small and Large Spike as they are basically the same. After getting these general patterns they pull \(D_1 \sim \mathcal{U}\{0,\ldots,7\}\) and \(D \sim \mathcal{U}\{1,\ldots,7\}\) respectivly to determine their concrete variation. \[ \begin{array}{c} \bullet \xrightarrow{P(\text{SS})} \text{SS} \xrightarrow{\text{choose}\, D_1\, :\ \frac{1}{8}} V_{\text{SS},\,D_1} \\[2.5px] \bullet \xrightarrow{P(\text{LS})} \text{LS} \xrightarrow{\text{choose}\, D\, :\ \frac{1}{7}} V_{\text{LS},\, D} \end{array} \] Hence \(P(V_{\text{SS},\, D_1}) = \frac{1}{8}P(\text{SS})\) and \(P(V_{\text{LS},\, D}) = \frac{1}{7}P(\text{LS})\). Finally Alternating is a bit more complicated since its variations are determined by dependend parameters. After getting an alternating pattern, first \(D_1 \sim \mathcal{U}{2,3}\) and then \((S_1,S_3) \sim \mathcal{U}\{0,\ldots,6\} \times \mathcal{U}\{0,\ldots,6-S_1\}\) are pulled. \[ \bullet \xrightarrow{P(\text{ALT})} \text{ALT} \xrightarrow{\text{choose}\, D_1\,:\ \frac{1}{2}} \text{ALT},\, D_1 \xrightarrow{\text{choose}\, (S_1,\,S_3)\,:\ f(S_1)} V_{\text{ALT},\,D_1,\,(S_1,\,S_3)} \] By writing out a simple table for all possible values \((S_1,\,S_3)\) can take one can quickly find \(f(S_1) = \frac{1}{7} \cdot \frac{1}{7-S_1}\). Therefore the full needed formula is \(P(V_{\text{ALT},\,D_1,\,(S_1,\,S_3)}) = \frac{1}{14}P(\text{ALT}) \cdot \frac{1}{7-S_1}\). We now have everything we need ready. Let's end this chapter by implementing both the optimal selling point and the variation probabilities into our code. We will show only the Alternating pattern, as it is the most complicated, and the other ones are just analoug.