diff --git a/auctionsim/auction.go b/auctionsim/auction.go new file mode 100644 index 0000000..beeb227 --- /dev/null +++ b/auctionsim/auction.go @@ -0,0 +1,70 @@ +package auctionsim + +import ( + "math" + "slices" +) + +/** + * RunAuction simulates a single-bid second-price auction with no reserve price + * between all bidders emitted by the provided generator, returning the winning + * bidder and how much they paid (the maximum bid of the second-highest bidder). + * + * To simulate an auction between the first several bidders emitted by some + * generator, use CappedBidderGenerator (in generators.go). + */ +func RunAuction(g BidderGenerator) (float64, Bidder) { + highBidder := Bidder{} + maxBid := math.Inf(-1) + prevBid := math.Inf(-1) + + for b, ok := g.Generate(); ok; b, ok = g.Generate() { + if c := b.BidCeiling(); c > maxBid { + prevBid = maxBid + maxBid = c + highBidder = b + } + } + + return prevBid, highBidder +} + + +/** + * RunAuctionVerbosely simulates a single-bid second-price auction with no + * reserve price between all bidders emitted by the provided generator, + * returning all bidders sorted by the order they dropped out and how much the + * winning bidder paid. + * + * Use CappedBidderGenerator to limit the size of the auction and therefore + * the number of bidders on the list. + */ +func RunAuctionVerbosely(g BidderGenerator) float64, []Bidder { + var bidders []Bidder + for b, ok := g.Generate(); ok; b, ok = g.Generate() { + bidders = append(bidders, b) + } + slices.SortFunc(bidders, func(a, b Bidder) int { + // If both bid ceilings are infinite in the same direction, we get NaN here... + d := a.BidCeiling() - b.BidCeiling() + if d < 0 { + return -1 + } + if d > 0 { + return 1 + } + // ...which is fine, since NaN will fail both comparisons and return 0 here, so these + // (incomparable) infinities get reported as equal, which is good enough. + return 0 + }) + + if len(bidders) <= 1 { + // With one or zero bidders, there's no auction. The bidder (if any) gets it for the + // reserve price, but there is no reserve price, so the auctioneer pays the sole bidder + // an infinite amount of money to go away. Economics! + return math.Inf(-1), bidders + } + + // Final price is the second highest bidder's limit. + return bidders[len(bidders)-2].BidCeiling(), bidders +} diff --git a/auctionsim/bidder.go b/auctionsim/bidder.go new file mode 100644 index 0000000..8611915 --- /dev/null +++ b/auctionsim/bidder.go @@ -0,0 +1,90 @@ +package auctionsim + +/** + * A Bidder represents one competitor in an auction for some resource. It is + * not threadsafe. It is a pure value type. + */ +type Bidder struct { + /** + * Value stores the real value of the item being auctioned, to this bidder. + * If the bidder won the auction at this price, they would make exactly + * zero effective profit. It is the "rational bid" in a perfectly efficient + * marketplace. + * + * A bidder that wants some nonzero profit has a lower value on the item by + * the amount of profit they want -- that "minimum payoff" represents an + * unspecified cost for the time and effort of doing the project at all. + * Other costs for getting value out of the object also decrease its value. + * + * For example, a city to which hosting an F1 race is worth five billion + * dollars, but would spend three and a half billion dollars setting up for + * it, one billion dollars running it, and lose one billion dollars of tax + * revenue due to business disruption during setup and during the event, + * correctly values the race for negative five hundred million dollars. As + * demonstrated by this example, a value can be negative. + * + */ + Value float64 + + /** + * Irrationality is the amount the bidder is wrong by on their belief about + * how much the item being auctioned is worth to them. It can be positive + * or negative. If it is positive, the bidder will overbid and potentially + * lose money by winning for more than the benefit they get from the item. + * If it is negative, the bidder will stop bidding too soon and potentially + * fail to win an auction that, had they continued bidding, would have given + * them the item at a profitable price. + */ + Irrationality float64 + + /** + * Cash stores the bidder's maximum available cash to bid with (including + * any debt, liquid assets, etc. that they are willing to commit to the + * auction). It is a hard cap on their maximum bid. + * + * To simulate an auction without cash limits, set this to positive infinity + * for every bidder. + * + * A bidder's available cash can be negative. For example, a company that + * cannot offer a bid to perform a job below a certain price has negative + * effective cash on hand repersenting the maximum price they may "pay" + * to win the auction -- the inverse of the minimum amount of money they + * are allowed to accept as compensation for the obligation being auctioned. + */ + Cash float64 +} + +/** + * ValueBelief returns how much the bidder *thinks* the item is worth to them. + * The actual value to them is b.Value. +func (b Bidder) ValueBelief() float64 { + return b.Value + b.Irrationality; +} + +/** + * BidCeiling returns the highest bid this bidder would make for the item. This + * bid might be negative. A negative winning bid is possible and ordinary; this + * represents an auction between vendors competing to offer a service at the + * lowest price, for example. + */ +func (b Bidder) BidCeiling() float64 { + if b.ValueBelief() > b.Cash { + return b.Cash + } + return b.ValueBelief() +} + +/** + * CashCapped returns whether this bidder *would* make a strictly higher bid + * than their calculated ceiling if they had the money to do so, but they don't. + */ +func (b Bidder) CashCapped() bool { + return b.ValueBelief() > b.Cash; +} + +/** + * IsZero returns whether the given Bidder is indistinguishable from one that was zero-initialized. + */ +func (b Bidder) IsZero() bool { + return b.Value == 0 && b.Irrationality == 0 && b.Cash == 0 +} diff --git a/auctionsim/generators.go b/auctionsim/generators.go new file mode 100644 index 0000000..91edc0a --- /dev/null +++ b/auctionsim/generators.go @@ -0,0 +1,133 @@ +package auctionsim + +import ( + "math" +) + +/** + * A Distribution is any stream of float64s. + */ +type Distribution interface { + /** + * Draw returns the next float64 from the distribution and true, or + * zero and false if the distribution is exhausted or otherwise unusable. + */ + Draw() float64, bool +} + +/** + * A BidderGenerator is any stream of bidders. + */ +type BidderGenerator interface { + /** + * Generate returns the next Bidder and true, or a zero Bidder and false if + * the generator is exhausted or otherwise unusable. + */ + Generate() Bidder, bool +} + +/** + * BiddersFromDistributions is a BidderGenerator that draws the bidders' values, + * irrationality levels, and cash on hand from the provided distributions. + */ +type BiddersFromDistributions struct { + Values Distribution + Irrationalities Distribution + Bankrolls Distribution +} + +/** + * Generate implements BidderGenerator. + */ +func (b *BiddersFromDistributions) Generate() (Bidder, bool) { + value, ok := b.Values.Draw() + if !ok { + return Bidder{}, false + } + irrationality, ok := b.Irrationalities.Draw() + if !ok { + return Bidder{}, false + } + cash, ok := v.Bankrolls.Draw() + if !ok { + return Bidder{}, false + } + + return Bidder { + Value: value, + Irrationality: irrationality, + Cash: cash, + }, true +} + +/** + * NormalDistribution is a Distribution representing the normal distribution + * with some specified standard deviation and mean. Multiple instances of + * this type can share a math.Rand as long as they are not queried concurrently + * (since math.Rand is not threadsafe). + */ +type NormalDistribution struct { + Rand *math.Rand + StdDev float64 + Mean float64 +} + +/** + * Draw implements Distribution. + */ +func (n *NormalDistribution) Draw() (float64, bool) { + return n.Rand.NormFloat64() * n.StdDev + n.Mean, true +} + + +/** + * ConstDistribution is a Distribution that always returns the same value. + */ +type ConstDistribution float64 + +/** + * Draw implements Distribution. + */ +func (c ConstDistribution) Draw() (float64, bool) { + return (float64)c, true +} + +/** + * CappedDistribution returns some fixed number of values from a Distribution, + * then stops returning values. + */ +type CappedDistribution struct { + D Distribution + Lim int64 +} + +/** + * Draw implements Distribution. + */ +func (c *CappedDistribution) Draw() (float64, bool) { + if c.Lim <= 0 { + return 0, false + } + c.Lim-- + return c.D.Draw() +} + +/** + * CappedBidderGenerator returns some fixed number of bidders from a + * BidderGenerator, then stops returning values. + */ +type CappedBidderGenerator struct { + G BidderGenerator + Lim int64 +} + +/** + * Generate implements BidderGenerator. + */ +func (c *CappedBidderGenerator) Generate() (float64, bool) { + if c.Lim <= 0 { + return Bidder{}, false + } + c.Lim-- + return c.G.Generate() +}