Compare commits

...

20 Commits

Author SHA1 Message Date
faacb1d4a0 Ignore main.exe if I'm lazy about go build 2023-11-18 18:59:40 -08:00
52684daefe actually fix text separators 2023-11-18 18:54:39 -08:00
3695f2704b fix text separators 2023-11-18 18:54:11 -08:00
0b68cb80bf Allow CSV format, multi runs, and fully customizable errors. 2023-11-18 18:53:13 -08:00
16f718dcdc fix more build errors 2023-11-18 18:20:22 -08:00
049378518a fix build errors 2023-11-18 18:19:41 -08:00
a63608bac5 Rewrite main to use summary statistics 2023-11-18 18:19:00 -08:00
0f0349810f WinnerMaxBid 2023-11-18 18:09:40 -08:00
91ebe9ddc3 Fix build errors. 2023-11-18 18:05:04 -08:00
2ebc2f904f format summary.go 2023-11-18 18:03:50 -08:00
b83a3ed5c3 Draft version of summary dumper 2023-11-18 18:03:28 -08:00
117a5e9f2b now with more details! 2023-11-18 17:27:50 -08:00
d08ee55795 fix compilation errors 2023-11-18 16:52:22 -08:00
6a90931c57 format main.go 2023-11-18 16:51:19 -08:00
0a7dd54597 add missing newline 2023-11-18 16:50:59 -08:00
67432448cf very basic UI 2023-11-18 16:50:53 -08:00
f151a9ad76 Fix compilation errors. 2023-11-18 16:30:36 -08:00
58efb6fec8 Fix unterminated comment in bidder.go and format. 2023-11-18 16:28:42 -08:00
88737d8f4c go fmt, with some syntax fixes 2023-11-18 16:27:21 -08:00
0c30e880cc add go.mod 2023-11-18 16:19:16 -08:00
8 changed files with 319 additions and 45 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
main/main.exe

View File

@ -9,7 +9,7 @@ import (
* 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).
*/
@ -29,17 +29,16 @@ func RunAuction(g BidderGenerator) (float64, Bidder) {
return prevBid, highBidder
}
/**
* RunAuctionVerbosely simulates a single-bid second-price auction with no
* reserve price between all bidders emitted by the provided generator,
* 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 {
func RunAuctionVerbosely(g BidderGenerator) (float64, []Bidder) {
var bidders []Bidder
for b, ok := g.Generate(); ok; b, ok = g.Generate() {
bidders = append(bidders, b)

View File

@ -5,25 +5,25 @@ package auctionsim
* 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 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
/**
@ -44,7 +44,7 @@ type Bidder struct {
*
* 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"
@ -57,8 +57,9 @@ type Bidder struct {
/**
* 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;
return b.Value + b.Irrationality
}
/**
@ -79,7 +80,7 @@ func (b Bidder) BidCeiling() float64 {
* 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;
return b.ValueBelief() > b.Cash
}
/**

View File

@ -1,6 +1,7 @@
package auctionsim
import (
"math"
"math/rand"
)
@ -10,9 +11,9 @@ import (
*/
func NormalestDistribution() *NormalDistribution {
return &NormalDistribution{
Rand: rand.New(rand.NewSource(int64(rand.Uint64()))),
Rand: rand.New(rand.NewSource(int64(rand.Uint64()))),
StdDev: 1,
Mean: 0,
Mean: 0,
}
}
@ -24,8 +25,8 @@ func NormalestDistribution() *NormalDistribution {
func NormalestBidderGenerator() BidderGenerator {
d := NormalestDistribution()
return &BiddersFromDistributions{
Values: d,
Values: d,
Irrationalities: d,
Bankrolls: ConstDistribution(math.Inf(1)),
Bankrolls: ConstDistribution(math.Inf(1)),
}
}

View File

@ -12,7 +12,7 @@ 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
Draw() (float64, bool)
}
/**
@ -23,7 +23,7 @@ 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
Generate() (Bidder, bool)
}
/**
@ -31,9 +31,9 @@ type BidderGenerator interface {
* irrationality levels, and cash on hand from the provided distributions.
*/
type BiddersFromDistributions struct {
Values Distribution
Values Distribution
Irrationalities Distribution
Bankrolls Distribution
Bankrolls Distribution
}
/**
@ -48,15 +48,15 @@ func (b *BiddersFromDistributions) Generate() (Bidder, bool) {
if !ok {
return Bidder{}, false
}
cash, ok := v.Bankrolls.Draw()
cash, ok := b.Bankrolls.Draw()
if !ok {
return Bidder{}, false
}
return Bidder {
Value: value,
return Bidder{
Value: value,
Irrationality: irrationality,
Cash: cash,
Cash: cash,
}, true
}
@ -67,16 +67,16 @@ func (b *BiddersFromDistributions) Generate() (Bidder, bool) {
* (since rand.Rand is not threadsafe).
*/
type NormalDistribution struct {
Rand *rand.Rand
Rand *rand.Rand
StdDev float64
Mean float64
Mean float64
}
/**
* Draw implements Distribution.
*/
func (n *NormalDistribution) Draw() (float64, bool) {
return n.Rand.NormFloat64() * n.StdDev + n.Mean, true
return n.Rand.NormFloat64()*n.StdDev + n.Mean, true
}
/**
@ -88,7 +88,7 @@ type ConstDistribution float64
* Draw implements Distribution.
*/
func (c ConstDistribution) Draw() (float64, bool) {
return (float64)c, true
return float64(c), true
}
/**
@ -96,7 +96,7 @@ func (c ConstDistribution) Draw() (float64, bool) {
* then stops returning values.
*/
type CappedDistribution struct {
D Distribution
D Distribution
Lim int64
}
@ -116,14 +116,14 @@ func (c *CappedDistribution) Draw() (float64, bool) {
* BidderGenerator, then stops returning values.
*/
type CappedBidderGenerator struct {
G BidderGenerator
G BidderGenerator
Lim int64
}
/**
* Generate implements BidderGenerator.
*/
func (c *CappedBidderGenerator) Generate() (float64, bool) {
func (c *CappedBidderGenerator) Generate() (Bidder, bool) {
if c.Lim <= 0 {
return Bidder{}, false
}

139
auctionsim/summary.go Normal file
View File

@ -0,0 +1,139 @@
package auctionsim
import (
"math"
"strconv"
)
// Digits of precision in floating-point values in CSVs.
const CSVPrecision = 10
/**
* ResultSummary contains notable results of an auction.
*/
type ResultSummary struct {
/// The number of bidders in the auction.
Bidders int
/// The price the item sold for.
Price float64
/// The amount of value the winner actually got out of the auctioned item.
WinnerValue float64
/// The highest bid the auction winner was willing to make.
WinnerMaxBid float64
/**
* The amount by which the winner's value of the item exceeded the price
* they paid. Often negative.
*/
WinnerProfit float64
/**
* Number of losing bidders who had a "true value" of the item in excess
* of the price paid.
*/
LosersWithRegrets int
/// The most the item was genuinely worth to any bidder.
HighestValue float64
/// The most the bidder with the highest value would have been willing to pay.
HighestValuatorBid float64
/**
* How much the bidder with the highest value would have made in profit had
* they won the auction for the price the auction's actual winner paid.
* Often negative, indicating nobody would have made money on the auction.
* If this is zero or negative, there are 0 losers with regrets.
*/
MissedProfit float64
/**
* How much more valuable the item was to the bidder with the highest value
* than it was to the auction's eventual winner, irrespective of winning bid.
*/
ValueDelta float64
/**
* The rank of the bidder who actually had the highest value of the item.
* The winner of the auction has rank 1. If all bidders had a true value of
* negative infinity for the item, this will be greater than the number of
* bidders.
*/
HighestValueRank int
}
/**
* Summarize takes an already-sorted list of bidders (see RunAuctionVerbosely)
* and the winning auction price, and calculates a ResultSummary for that auction.
*/
func Summarize(price float64, allBidders []Bidder) *ResultSummary {
regrets := 0
maxValue := math.Inf(-1)
maxIdx := -1
for i, b := range allBidders {
if b.Value > maxValue {
maxValue = b.Value
maxIdx = i
}
if b.Value > price {
regrets++
}
}
winner := allBidders[len(allBidders)-1]
rube := Bidder{Value: math.Inf(-1)}
if maxIdx >= 0 {
rube = allBidders[maxIdx]
}
return &ResultSummary{
Bidders: len(allBidders),
Price: price,
WinnerValue: winner.Value,
WinnerMaxBid: winner.BidCeiling(),
WinnerProfit: winner.Value - price,
LosersWithRegrets: regrets,
HighestValue: maxValue,
HighestValuatorBid: rube.BidCeiling(),
MissedProfit: rube.Value - price,
ValueDelta: maxValue - winner.Value,
HighestValueRank: len(allBidders) - maxIdx,
}
}
/**
* ResultSummaryCSVHeader returns column labels for the records emitted by
* (*ResultSummary).CSVRecord().
*/
func ResultSummaryCSVHeader() []string {
return []string{
"Bidders",
"Price",
"WinnerValue",
"WinnerMaxBid",
"WinnerProfit",
"LosersWithRegrets",
"HighestValue",
"HighestValuatorBid",
"MissedProfit",
"ValueDelta",
"HighestValueRank",
}
}
func csvFloat(f float64) string {
return strconv.FormatFloat(f, 'g', CSVPrecision, 64)
}
/**
* CSVRecord returns rows intended for use with encoding/csv.Writer, with
* columns in the order specified by ResultSummaryCSVHeader().
*/
func (s *ResultSummary) CSVRecord() []string {
return []string{
strconv.Itoa(s.Bidders),
csvFloat(s.Price),
csvFloat(s.WinnerValue),
csvFloat(s.WinnerMaxBid),
csvFloat(s.WinnerProfit),
strconv.Itoa(s.LosersWithRegrets),
csvFloat(s.HighestValue),
csvFloat(s.HighestValuatorBid),
csvFloat(s.MissedProfit),
csvFloat(s.ValueDelta),
strconv.Itoa(s.HighestValueRank),
}
}

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module git.chromaticdragon.app/kistaro/auctionsim
go 1.20

130
main/main.go Normal file
View File

@ -0,0 +1,130 @@
package main
import (
"encoding/csv"
"flag"
"fmt"
"git.chromaticdragon.app/kistaro/auctionsim/auctionsim"
"log"
"math"
"math/rand"
"os"
"strings"
)
var (
bidders = flag.Int("b", 1000, "Number of bidders in each auction.")
runs = flag.Int("r", 1, "Number of auctions to run.")
errorDev = flag.Float64("d", 1.0, "Standard deviation of bidder irrationality.")
errorMean = flag.Float64("m", 0.0, "Mean of bidder irrationality. Negative values represent a bias towards underbidding.")
format = flag.String("f", "txt", "Output format: plain text (\"txt\") or CSV (\"csv\").")
)
func deltaStr(delta float64) string {
if delta > 0 {
return fmt.Sprintf("gained ¤%f", delta)
}
if delta < 0 {
return fmt.Sprintf("lost ¤%f", -delta)
}
return "broken even"
}
func normalizeFormat() error {
*format = strings.ToLower(*format)
if *format == "txt" {
return nil
}
if *format == "csv" {
return nil
}
return fmt.Errorf("unrecognized format: %q", *format)
}
type emitter interface {
Emit(*auctionsim.ResultSummary)
Flush()
}
type textEmitter struct {
notFirst bool
}
func (t *textEmitter) Emit(summary *auctionsim.ResultSummary) {
if t.notFirst {
fmt.Println()
fmt.Println("-----------------")
fmt.Println()
}
t.notFirst = true
fmt.Printf("The auction winner paid ¤%f. They have %s.\n", summary.Price, deltaStr(summary.WinnerProfit))
fmt.Printf("The item was worth ¤%f to them.\n", summary.WinnerValue)
fmt.Printf("They would have paid up to ¤%f for it.\n", summary.WinnerMaxBid)
fmt.Println()
if summary.LosersWithRegrets < 1 {
fmt.Println("The item was not worth that to anybody.")
} else {
fmt.Printf("The item was worth that to %d bidders, who stopped bidding too soon.\n", summary.LosersWithRegrets)
}
fmt.Println()
if summary.HighestValueRank > summary.Bidders {
fmt.Println("Wow! It was infinitely beyond worthless to everybody.")
} else {
fmt.Printf("The bidder who would have gotten the most value was outbid by %d bidders.\n", summary.HighestValueRank-1)
fmt.Printf("The item was worth at most ¤%f to them. Their maximum bid was ¤%f.\n", summary.HighestValue, summary.HighestValuatorBid)
fmt.Printf("If they had paid the winning bid, they would have %s.\n", deltaStr(summary.MissedProfit))
fmt.Printf("They valued it ¤%f more than the winning bidder.\n", summary.ValueDelta)
}
}
func (*textEmitter) Flush() {}
type csvEmitter struct {
w *csv.Writer
}
func (c *csvEmitter) Emit(summary *auctionsim.ResultSummary) {
c.w.Write(summary.CSVRecord())
}
func (c *csvEmitter) Flush() {
c.w.Flush()
}
func main() {
flag.Parse()
if err := normalizeFormat(); err != nil {
log.Fatal(err)
}
e := emitter(&textEmitter{})
if *format == "csv" {
w := csv.NewWriter(os.Stdout)
e = &csvEmitter{w}
w.Write(auctionsim.ResultSummaryCSVHeader())
}
defer e.Flush()
for i := 0; i < *runs; i++ {
g := &auctionsim.BiddersFromDistributions{
Values: auctionsim.NormalestDistribution(),
Irrationalities: &auctionsim.NormalDistribution{
Rand: rand.New(rand.NewSource(int64(rand.Uint64()))),
StdDev: *errorDev,
Mean: *errorMean,
},
Bankrolls: auctionsim.ConstDistribution(math.Inf(1)),
}
e.Emit(auctionsim.Summarize(auctionsim.RunAuctionVerbosely(
&auctionsim.CappedBidderGenerator{
G: g,
Lim: int64(*bidders),
},
)))
}
}