in ,

Speculating on Animal Crossing Turnip Market – Insignificant Bits, Hacker News

Speculating on Animal Crossing Turnip Market – Insignificant Bits, Hacker News

[y] My wife is big fan of Animal Crossing. I will admit that I mostly Don’t understand the game but I find it very cute. She was very upset a couple of days ago when the Bank of Nook decided to cut the interest rate down to 0. 17%, at which point she asked: “how am I supposed to make money now!? ”

[28.33816839] Evidently, I said: well, you should invest your money! Apparently, the Financial Times agrees with me.

It seems there’s some sort of turnip market in the game. Basically, you can buy turnips on Sunday for a given price, and then you will have opportunities to sell your turnips over the upcoming week. If you Don’t sell them by next Sunday, you lose them (and thus materialize your loss). You get two screen prices every day, one in the AM and one in the PM. So, you’ll get prices from Monday to Saturday.

Can we come up with a good strategy to sell our turnips ?

(Modeling) The first question is: are turnip prices completely random? A couple of Google searches reveal that they follow four patterns: () Big spike: decreasing trend and then three big price spikes before decreasing once again

Small spike: decreasing trend and then three small price spikes before decreasing again.

[x, y] Decreasing: always decreasing trend.

Random: randomly selected within a range. [28.33816839] Ok, so it seems it’s not completely random. In that case, we should be able to come up with a model (probabilities) for the turnip prices. It turns out that (Ninji) has already looked into the source code and figured out how the patterns are allocated and how the prices come up. Also, TurnipProphet

has already taken this and Built a procedure to get the probability distribution for the turnip prices. [0.00000000e 00, 0.00000000e 00, 2.60805565e-10, 5.45772528e-01, 4.54213107e-01, 6.23904416e-06, 1.60620122e-05] [28.33816839] Great, job done! Buy low, sell high, make some bank! Well not quite.

Coming up with a strategy every turnip investor (speculator) has to make 28 decisions: how many turnips to sell at every screen price. Evidently, every turnip investor (speculator) has a different risk appetite: I might be willing to hold on to my turnips for longer looking for a higher payout which might be unlikely given our probability distribution, or maybe I am not so fond of risk and I hedge my bets out as soon as things get tough. That’s all okay. (Turns out this is all part of what we now know as) Modern Portfolio Theory [ 8000. , 8535.54345703, 8672.83227539, 8000.13848877, 8000.1831665 , 11046.69415283, 12850.85089111] (thanks Mr. Markowitz). You have some risky assets, you have a risk appetite, you want to make a well diversified portfolio that minimizes the variance of your returns, and maximizes your expected returns. That sounds like what we want, let’s do that. We will model our turnips as different assets: turnips we will sell on Monday AM, turnips we will sell on Monday PM, and so on. Thanks to the work that has been done in the previous section, we know what’s the expected return of each of our assets. As time progresses we will materialize some earnings and some losses.

Let’s work with an example. I put down my turnip prices on Wednesday PM having sold zero turnips so far. TurnipProphet and got the following probability distribution: [y] I was too lazy to get the code for TurnipProphet to run, so I just copy pasted it and played around with it manually: , () from (collections) (import defaultdict from (functools) (import) partial (import) (numpy) (as) np (import) (pandas) (as) pd (import) scipy.optimize (as) sco MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY=(range) (6) ) AM, PM=“AM” , "PM" (def) (convert_range_with_price) (p, s):     a, b=(s) . (split)

 "to"      a =(a) . strip ()     b =(b) . strip ()      (return) (list (range (int (a)) -  p, int (b)  ( ) (1)  -  (p))   turnip_price 
=convert_range 
=partial (convert_range_with_price, turnip_price)  data 
=[    (        0.664,        {            (WEDNESDAY, PM): convert_range("66 to 66"),            (THURSDAY, AM): convert_range("61 to 64"),            (THURSDAY, PM): convert_range("56 to 61"),            (FRIDAY, AM): convert_range("51 to 58"),            (FRIDAY, PM): convert_range("46 to 55"),            (SATURDAY, AM): convert_range("41 to 52"),            (SATURDAY, PM): convert_range("36 to 49"),        },    ),    (        0.158,        {            (WEDNESDAY, PM): convert_range("66 to 66"),            (THURSDAY, AM): convert_range("89 to 138"),            (THURSDAY, PM): convert_range("138 to 196"),            (FRIDAY, AM): convert_range("196 to 588"),            (FRIDAY, PM): convert_range("138 to 196"),            (SATURDAY, AM): convert_range("89 to 138"),            (SATURDAY, PM): convert_range("40 to 89"),        },    ),    (        0.158,        {            (WEDNESDAY, PM): convert_range("66 to 66"),            (THURSDAY, AM): convert_range("61 to 64"),            (THURSDAY, PM): convert_range("89 to 138"),            (FRIDAY, AM): convert_range("138 to 196"),            (FRIDAY, PM): convert_range("196 to 588"),            (SATURDAY, AM): convert_range("138 to 196"),            (SATURDAY, PM): convert_range("89 to 138"),        },    ),    (        0.01,        {            (WEDNESDAY, PM): convert_range("66 to 66"),            (THURSDAY, AM): convert_range("89 to 138"),            (THURSDAY, PM): convert_range("89 to 138"),            (FRIDAY, AM): convert_range("137 to 195"),            (FRIDAY, PM): convert_range("137 to 196"),            (SATURDAY, AM): convert_range("137 to 195"),            (SATURDAY, PM): convert_range("40 to 89"),        },    ),    (        0.01,        {            (WEDNESDAY, PM): convert_range("66 to 66"),            (THURSDAY, AM): convert_range("61 to 64"),            (THURSDAY, PM): convert_range("89 to 138"),            (FRIDAY, AM): convert_range("89 to 138"),            (FRIDAY, PM): convert_range("137 to 195"),            (SATURDAY, AM): convert_range("137 to 196"),            (SATURDAY, PM): convert_range("137 to 195"),        },    ),]   

So, now we have our data variable holding the probability of each possible return (i.e. price we might get minus the price we paid for the turnips). We can easily calculate the expectation of each asset:

, () (def) expectation_of (r1) :     exp

=(0)      (for) p, items  (in [173] the data:         n 
=float (len (items [r1]))         exp 
 =(p [1600.        , 1707.10598755, 1734.55581665, 1600.00001526,       1600.00001526, 2209.32440186, 2570.16264343]  (sum (items) ))  /  n       (return) exp   
I just assumed that if TurnipProphet says a price range of (to) with 5%, (would have a 1% chance of happening, 68 would have a 1% chance of happening. That might obviously be wrong.
So, going back to Markowitz, what we are interested in. in doing is coming up with a vector of weights for each asset (eg sell % of your turnips on Friday PM) that is within our risk appetite (higher risk appetite means I am willing to take more risk for higher payout). We would like those weights to stick to the following optimization problem (thanks to the Wikipedia):
If we translate that to our little problem we need the following Ingredients:
(The covariance matrix of our asset returns. [y] We need to select our risk appetite.
  • Ok that seems simple enough, we already know how to compute the expected returns, let's build the covariance matrix: [0.00000000e 00, 0.00000000e 00, 2.60805565e-10, 5.45772528e-01, 4.54213107e-01, 6.23904416e-06, 1.60620122e-05] (def) (joint_prob) (r1, r2 ):     prob =defaultdict (float)      (for) p, items (in [173] the data:         r1_opts
    =items [r1]         r2_opts 
    =items [r2]         outcomes =(float) len (r1_opts)  [y] (len) r2_opts ))           (for) (x) (in [9.99973926e-01, 2.60740769e-05, 0.00000000e 00, 0.00000000e 00,       1.71303943e-17, 0.00000000e 00, 0.00000000e 00] r1_opts:              (for) (y) (in [9.99973926e-01, 2.60740769e-05, 0.00000000e 00, 0.00000000e 00,       1.71303943e-17, 0.00000000e 00, 0.00000000e 00] r2_opts:                 prob [x, y]  =(p) /  results       (return)  prob    (def) (covariance_of) (r1, r2):     E_r1 
    =expectation_of (r1)     E_r2 
    =expectation_of (r2)      joint 

    =joint_prob (r1, r2)     joint_exp
    =(0)      (for) (x, y), prob  in  (joint) .  items ():         joint_exp 
     =(x) [     ...:     (WEDNESDAY, PM),     ...:     (THURSDAY, AM),     ...:     (THURSDAY, PM),     ...:     (FRIDAY, AM),     ...:     (FRIDAY, PM),     ...:     (SATURDAY, AM),     ...:     (SATURDAY, PM),     ...: ] (y)   prob       (return) (joint_exp) -  (E_r1) [    (        0.664,        {            (WEDNESDAY, PM): convert_range("66 to 66"),            (THURSDAY, AM): convert_range("61 to 64"),            (THURSDAY, PM): convert_range("56 to 61"),            (FRIDAY, AM): convert_range("51 to 58"),            (FRIDAY, PM): convert_range("46 to 55"),            (SATURDAY, AM): convert_range("41 to 52"),            (SATURDAY, PM): convert_range("36 to 49"),        },    ),    (        0.158,        {            (WEDNESDAY, PM): convert_range("66 to 66"),            (THURSDAY, AM): convert_range("89 to 138"),            (THURSDAY, PM): convert_range("138 to 196"),            (FRIDAY, AM): convert_range("196 to 588"),            (FRIDAY, PM): convert_range("138 to 196"),            (SATURDAY, AM): convert_range("89 to 138"),            (SATURDAY, PM): convert_range("40 to 89"),        },    ),    (        0.158,        {            (WEDNESDAY, PM): convert_range("66 to 66"),            (THURSDAY, AM): convert_range("61 to 64"),            (THURSDAY, PM): convert_range("89 to 138"),            (FRIDAY, AM): convert_range("138 to 196"),            (FRIDAY, PM): convert_range("196 to 588"),            (SATURDAY, AM): convert_range("138 to 196"),            (SATURDAY, PM): convert_range("89 to 138"),        },    ),    (        0.01,        {            (WEDNESDAY, PM): convert_range("66 to 66"),            (THURSDAY, AM): convert_range("89 to 138"),            (THURSDAY, PM): convert_range("89 to 138"),            (FRIDAY, AM): convert_range("137 to 195"),            (FRIDAY, PM): convert_range("137 to 196"),            (SATURDAY, AM): convert_range("137 to 195"),            (SATURDAY, PM): convert_range("40 to 89"),        },    ),    (        0.01,        {            (WEDNESDAY, PM): convert_range("66 to 66"),            (THURSDAY, AM): convert_range("61 to 64"),            (THURSDAY, PM): convert_range("89 to 138"),            (FRIDAY, AM): convert_range("89 to 138"),            (FRIDAY, PM): convert_range("137 to 195"),            (SATURDAY, AM): convert_range("137 to 196"),            (SATURDAY, PM): convert_range("137 to 195"),        },    ),]  E_r2    We are going to calculate the joint probability distribution of the screen prices at two different times (e.g. what’s the probability that the price is X on Monday PM and Y on Friday PM). Using that we use the well known covariance identity Cov (X, Y)=E [XY] - E [X] E [Y]. Presto.  Let's get all our turnips in a row now. We want to come up with that vector of weights. We have an optimization problem here, we want to select the weights that minimize the formula above. Turns out, we can use scipy just for that. 
    [x, y] (def) (objective) (weights, covs, expected_returns, risk):      # the `@` operator is matrix multiplication      (return) weights @ @ (covs) (@) (weights) - (risk) expected_returns (@ weights (def) get_weights (data, possibilities, risk):     num_assets
    =len (possibilities)     expected_returns_dict 
    ={}      (for) (p) (in [9.99973926e-01, 2.60740769e-05, 0.00000000e 00, 0.00000000e 00,       1.71303943e-17, 0.00000000e 00, 0.00000000e 00]  poss:         expected_returns_dict [p]=expectation_of (p)      er 
    =(pd) . (DataFrame) {
     "returns" : expected_returns_dict})     cov_dict 
    =defaultdict (dict)       (for) (x) (in [9.99973926e-01, 2.60740769e-05, 0.00000000e 00, 0.00000000e 00,       1.71303943e-17, 0.00000000e 00, 0.00000000e 00]  poss:          (for) (y) (in [9.99973926e-01, 2.60740769e-05, 0.00000000e 00, 0.00000000e 00,       1.71303943e-17, 0.00000000e 00, 0.00000000e 00]  poss:             cov_dict [x] [y] ["returns"]=covariance_of (x, y)      covs 
    =(pd) . DataFrame (cov_dict)       # We want the% of turnips to sell at each screen price and we can only       # sell (ie we can't short the prices)      constraints 
    =({"type" : 
     "eq" ,  “fun” : lambda  (x: np) [186] sum (x)  (-)   (1) }     bound 
    =()  (0.0) , (1.0) )     bounds  bounds=(tuple) bound  for  (asset)  in  range (num_assets))      (return) (         sco 
    .  (minimize)             objective,             num_assets 
     [y]             args =(covs) . (as_matrix (), er  .  as_matrix (), risk),             method =SLSQP [     ...:     (WEDNESDAY, PM),     ...:     (THURSDAY, AM),     ...:     (THURSDAY, PM),     ...:     (FRIDAY, AM),     ...:     (FRIDAY, PM),     ...:     (SATURDAY, AM),     ...:     (SATURDAY, PM),     ...: ] ,             bounds  bounds=bounds,             constraints 
    =constraints,         ),         er 
    .  as_matrix (),         covs 
    .  as_matrix (),     )  

    We can test it out interactively with the example we have been working on:

    In [ ...: (WEDNESDAY, PM), ...: (THURSDAY, AM), ...: (THURSDAY, PM), ...: (FRIDAY, AM), ...: (FRIDAY, PM), ...: (SATURDAY, AM), ...: (SATURDAY, PM), ...: ]: poss=[ ...: (WEDNESDAY, PM), ...: (THURSDAY, AM), ...: (THURSDAY, PM), ...: (FRIDAY, AM), ...: (FRIDAY, PM), ...: (SATURDAY, AM), ...: (SATURDAY, PM), ...: ]      ...: In [173]: w, exp, covs=get_weights (data, poss, 0) In [ ...: (WEDNESDAY, PM), ...: (THURSDAY, AM), ...: (THURSDAY, PM), ...: (FRIDAY, AM), ...: (FRIDAY, PM), ...: (SATURDAY, AM), ...: (SATURDAY, PM), ...: ]: w [173] Out [ ...: (WEDNESDAY, PM), ...: (THURSDAY, AM), ...: (THURSDAY, PM), ...: (FRIDAY, AM), ...: (FRIDAY, PM), ...: (SATURDAY, AM), ...: (SATURDAY, PM), ...: ] array ([173]) In [9.99973926e-01, 2.60740769e-05, 0.00000000e 00, 0.00000000e 00, 1.71303943e-17, 0.00000000e 00, 0.00000000e 00]: exp.T @ w [ ...: (WEDNESDAY, PM), ...: (THURSDAY, AM), ...: (THURSDAY, PM), ...: (FRIDAY, AM), ...: (FRIDAY, PM), ...: (SATURDAY, AM), ...: (SATURDAY, PM), ...: ] Out [9.99973926e-01, 2.60740769e-05, 0.00000000e 00, 0.00000000e 00, 1.71303943e-17, 0.00000000e 00, 0.00000000e 00]: array ([9.99973926e-01, 2.60740769e-05, 0.00000000e 00, 0.00000000e 00, 1.71303943e-17, 0.00000000e 00, 0.00000000e 00]) In [9.99973926e-01, 2.60740769e-05, 0.00000000e 00, 0.00000000e 00, 1.71303943e-17, 0.00000000e 00, 0.00000000e 00]: exp.T @ w [ ...: (WEDNESDAY, PM), ...: (THURSDAY, AM), ...: (THURSDAY, PM), ...: (FRIDAY, AM), ...: (FRIDAY, PM), ...: (SATURDAY, AM), ...: (SATURDAY, PM), ...: ] Out [9.99973926e-01, 2.60740769e-05, 0.00000000e 00, 0.00000000e 00, 1.71303943e-17, 0.00000000e 00, 0.00000000e 00]: array ([9.99973926e-01, 2.60740769e-05, 0.00000000e 00, 0.00000000e 00, 1.71303943e-17, 0.00000000e 00, 0.00000000e 00]) In : w [173] @ covs @ w Out [-31.99986786]: 2. e - [28.33816839] Unsurprisingly, if we are willing to take zero risk, we are told to sell now as that price has zero variance. The expected returns of that allocation is losing - 50 for each turnip. Let’s try a couple of different risk appetites: 83227539 In [186]: for risk in ([9.99973926e-01, 2.60740769e-05, 0.00000000e 00, 0.00000000e 00, 1.71303943e-17, 0.00000000e 00, 0.00000000e 00] , , 174, 514, 00001526):      ...: w, exp, covs=get_weights (data, poss, risk)      ...: print (w)      ...: print (exp.T @ w [172])      ...: print (w [9.99973926e-01, 2.60740769e-05, 0.00000000e 00, 0.00000000e 00, 1.71303943e-17, 0.00000000e 00, 0.00000000e 00] @ covs @ w [173]      ...:      fun: 588      jac: array ([186]  message: 'Optimization terminated successfully.'     nfev:      nit: 6     njev: 6   status: 0  success: True        x: array ([186]) [9.72724596e-01, 1.01876057e-10, 6.39677751e-11, 1.46464235e-02, 1.26289831e-02, 2.04667615e-10, 3.60237807e-10] 8.      fun: .      jac: array ([9.72724596e-01, 1.01876057e-10, 6.39677751e-11, 1.46464235e-02, 1.26289831e-02, 2.04667615e-10, 3.60237807e-10])  message: 'Optimization terminated successfully.'     nfev: 100      nit:     njev: 7   status: 0  success: True        x: array ([1600. , 1707.10598755, 1734.55581665, 1600.00001526, 1600.00001526, 2209.32440186, 2570.16264343] [1600. , 1707.10598755, 1734.55581665, 1600.00001526, 1600.00001526, 2209.32440186, 2570.16264343] 500      fun: [-31.99986786]      jac: array ([8.63622855e-01, 2.96854243e-08, 1.86344942e-08, 7.32320950e-02, 6.31448364e-02, 5.98173297e-08, 1.05265552e-07])  message: 'Optimization terminated successfully.'     nfev: 132      nit:     njev: 9   status: 0  success: True        x: array ([-23.77365766]) [7.27245514e-01, 6.52789507e-08, 5.03337743e-08, 1.46464389e-01, 1.26289752e-01, 8.28435152e-08, 1.47684783e-07] .      fun: [186]      jac: array ([7.27245514e-01, 6.52789507e-08, 5.03337743e-08, 1.46464389e-01, 1.26289752e-01, 8.28435152e-08, 1.47684783e-07]  message: 'Optimization terminated successfully.'     nfev: 158      nit: 31     njev: 19   status: 0  success: True        x: array ([ 8000. , 8535.54345703, 8672.83227539, 8000.13848877, 8000.1831665 , 11046.69415283, 12850.85089111] [ 8000. , 8535.54345703, 8672.83227539, 8000.13848877, 8000.1831665 , 11046.69415283, 12850.85089111] [-31.99986786]      fun: - [186]      jac: array ([9.13215268]  message: 'Optimization terminated successfully.'     nfev:      nit: 23     njev: 6   status: 0  success: True        x: array ([9.13215268] 18762. [28.33816839] So what are we observing? As our risk appetite increases, our weights shift from “cash out now” to “wait it out till Friday because that’s when the prices could peak ”, we see our expected returns steadily increase. However, this comes at a cost: the variance of our portfolio increased considerably, thus we are taking on a lot more risk (and possibility to both gain more and lose more).

    Ideally, the way you use this Is that every time you have a new screen price, you'll update your portfolio.

    [y] On Monday AM price you'll have a new portfolio. You possibly will sell some at the current price, and have the remaining turnips hanging around. [0.00000000e 00, 0.00000000e 00, 2.60805565e-10, 5.45772528e-01, 4.54213107e-01, 6.23904416e-06, 1.60620122e-05]

    On Monday PM price you'll recompute the weights. You can’t change what you already sold, that ship has gone. But you can make an improved decision (because you have more information) to update the weights of the remaining decisions. You will, again, decide to sell some and keep some given your risk appetite.

  • Continue until you exhaust your weekly turnips. Or evidently, your best friend tells you they have killer prices on their island.
  • That's about it.

    There's an interesting scenario I didn't get to investigate but I think makes for a cool problem which is very similar. Every island has a different turnip price, so turnips are more valuable in some places than others. Let’s say you have access to many islands and access to their turnip probability distribution. How do you select when and where to sell your turnips?

  • What do you think?

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    GIPHY App Key not set. Please check settings

    Kim Jong-un: Trump 'glad' about reappearance of North Korean leader – BBC News, BBC News

    Kim Jong-un: Trump 'glad' about reappearance of North Korean leader – BBC News, BBC News

    Gotta be Aliens! There is a Powerful Radio Signal Coming From Within Our Galaxy – News18, News18.com

    Gotta be Aliens! There is a Powerful Radio Signal Coming From Within Our Galaxy – News18, News18.com