GUI Shop

GUI Shop

273k Downloads

Feature Request: Dynamic Pricing

A248 opened this issue · 1 comments

commented

Introduction
Setting static prices works fine if you know exactly how valuable the item is. Sometimes, however, you misjudge the value, and players buy too much or too little of the item. Or, perhaps, you just want to let the prices float.

I messed around with some graphs on desmos.com to try and find a reasonable function to describe the price. I looked at the source / decompiled two other plugins which use dynamic pricing to determine how they calculate the price. From these, I determined an improved formula for calculating the dynamic price of an item.

The Basic Idea
The price of the item is automatically adjusted based on:

  • The "stock" of the item, or its current "quantity." While there is not actually any "quantity" in the shop, we're going to pretend to keep track of the quantity and adjust the price accordingly.
  • The BASE value for an item. This the where the price starts. You can think of this like what the price "should" be if the price is neither too high nor too low.
  • The SPREAD value for an item. This determines the volatility of the price with respect to changes in the stock.

The formula we'll use for price is:
P(x) = b*e^(-x/s) where x = stock and P(x) = price for any defined x
It's an exponential function which looks like this:
Screenshot 2020-03-19 at 1 12 44 PM
(in the picture, b=1 and s=1, so the function is simply e^(-x) )

The initial stock is zero ("balanced"), so the price is the BASE. Further variance, resulting from later purchases or sales, will change the price based on the SPREAD.

The Caveat, the Solution, the Results
If this formula were to be implemented simply, there would be a massive problem. Players could gain infinite money. For example, a player could buy an item for $1, the price increases to $1.1, the player sells back the item, gaining $0.1. (The reverse is also true: If a player sells an item and buys it back, he or she will lose money).

Some plugins will attempt to "fix" this problem by charging a "tax" on purchases or sales. However, this remedy is suboptimal. It is more of a bandaid that a proper, mathematical solution. It's also unrealistic – in the real world, there may be a tax, but the tax exists so that the government can raise revenue, not to prevent a "bug" in reality.

Ideally, we would find a solution where a player could buy an item and sell it back without making a profit or a loss. Buying and selling five units of the same item should ensure the buyer/seller breaks perfectly even. After our solution has been applied, if we so desire, then we can apply taxes and fees, but that's a separate feature, and should remain a separate feature.

The source of this "bug" we've identified relates to the directionality of the function. If we're moving from a quantity of 5 to 4 (buy), the buyer sees and pays the price at the quantity of 5 -> 5 is at the upper bound of our quantity range. If we're moving from a quantity of 5 to 6 (sell), the seller also sees and pays the price at the quantity of 5 –> but this time,. Mathematically speaking, for a purchase at quantity a, the user pays P(a), but when the same users resells the item back to the market, the user receives P(a-1).

To solve this, we can use a midpoint formula between the starting point and the initial point. When a user buys an item at quantity a, the user must pay the price defined by the midpoint between P(a) and P(a-1), which is P(a-0.5). Therefore, the user will pay P(a-0.5) = b*e^(-(a-0.5)/s) = b*e^((-a+0.5)/s). Similarly, for a sale, the user will pay P(a+0.5) = b*e^(-(a+0.5)/s) = b*e^((-a-0.5)/s).

Accordingly, the final results are as follows:
For a purchase, P(x) = be^((-x+0.5)/s).
For a sale, P(x) = b
e^((x+0.5)/s).

More about the SPREAD value
The SPREAD determines the responsiveness of the price to changes in the quantity. If the SPREAD is small, a single purchase will increase the price considerably, and a single sale will decrease it likewise. If the SPREAD is large, many purchases must occur to achieve the same change in price.

Consequently, I would recommend you set a SPREAD value corresponding to the amount of players buying/selling the item at any given moment. If there are many players buying and selling, your SPREAD value should be high, so that a slight variation in buying/selling behavior won't cause a massive price swing. If there are less players buying and selling, the SPREAD value should be low, so that the price can more easily find equilibrium. Of course, this is just a guideline. Setting a higher SPREAD enables a finer-tuned equilibrium, since the function is discontinuous (there is no such thing as 1/2 apples).

More about the BASE value
Really, the base value is just the initial price. There's nothing much to it. After the initial price is set, the price is allowed to float until it reaches an equilibrium as determined by the players' demand and supply.

When the server restarts, the price will return to the BASE value. Then, it will find its equilibrium once again. If this presents a problem (players rush to buy/sell once the server starts up), the BASE value could perhaps be loaded from a datafile on server restart to prevent this situation.

Implementation
To prevent and explosion of configuration values, GuiShop can simply use the buy-price as the BASE value and the sell-price as the SPREAD value. Dynamic pricing should be able to be enabled per-item, by adding dynamic-price: true to the individual item's config node in the shops.yml. If dynamic-price key is false or not set, or the sell-price (SPREAD) is zero, negative, or unset, dynamic pricing will be disabled.

Some users may want GuiShop to save the BASE value on server stop (so that players don't rush to buy/sell once the price is reset to the BASE price). However, GuiShop is a shop plugin. It really shouldn't be saving/loading any data itself. It would make much more sense to create an API for loading the BASE value. Then, an addon/plugin could hook into GuiShop and inform it of the BASE value of an item.

commented

Completed.

I made an example implementation, SimplePricer