*author of this document

This is the R markdown file associated with the project (tentatively) titled above. It is broken into two major parts: the code section and the paper section. The code section comes first and does all the analyses in the paper. The paper section comes second, reports all the results from the analyses, and interprets these results.

Ideally, this file will combine the entire code package into one code file to help readers follow along. The GitHub Repository associated with this project can be found here.

======= Code =======

Preamble

Everything we need to get started.

# Start fresh.
rm(list=ls())
set.seed(4)

# Libraries and custom scripts.
library(tidyverse)
library(patchwork)
library(gganimate)
library(truncnorm)
library(tictoc)
library(furrr)
source("../helpers/ddm/simulate_ddm.R")
source("../helpers/utilities/myPlot.R")

# Directories.
.outdir = file.path("../outputs/figures")

# Parallelize.
plan(multisession, workers=8)

Simulation set-up

Let’s get all the functions and inputs ready for simulations.

Price vector

These are the prices that buyers and sellers will see during the response stage. We do this in lieu of bidding and asking strategies. See Notebook 01 for results if buyers (sellers) bid (ask) their valuations. Results are robust.

prices = seq(0.05, 1.0, 0.05)

fun: Reservation values

Write a function that generates a valuation for each buyer and seller. Here, we are opting to draw \(v \sim U[.01, .99]\).

# Function.
simulate_valuations <- function(nbuy=50, nsel=50, lb=0.01, ub=0.99, buy_mean=.4, buy_sd=.2, sel_mean=.6, sel_sd=.2, seed=4, rnd=2) {
  old_seed = .Random.seed
  set.seed(seed)
  # valuations = list(
  #   buy = rtruncnorm(nbuy, a=lb, b=ub, mean=buy_mean, sd=buy_sd) %>% round(rnd),
  #   sel = rtruncnorm(nsel, a=lb, b=ub, mean=sel_mean, sd=sel_sd) %>% round(rnd)
  # )
  valuations = list(
    buy = runif(nbuy, min=lb, max=ub) %>% round(rnd),
    sel = runif(nsel, min=lb, max=ub) %>% round(rnd)
  )
  .Random.seed = old_seed
  return(valuations)
}

# Get buyer and seller values.
values = simulate_valuations(nbuy=1000, nsel=1000)

# Plot buyer and seller values to double check.
.df.b = data.frame(v = values$buy, ID = "Buyers")
.df.s = data.frame(v = values$sel, ID = "Sellers")
.plt_values = ggplot(data=bind_rows(.df.b,.df.s)) +
  .myPlot +
  geom_vline(xintercept=.5, color="grey") +
  geom_histogram(aes(x=v), binwidth=.1, color="white") +
  labs(x="Values", y = "Count") + 
  coord_cartesian(xlim=c(0,1)) +
  facet_grid(rows=vars(ID))
.plt_values

DDM functions

Buyer behavior

Let \(v_i^b\) be buyer \(i\)’s value of the product and \(\theta_i^b\) be the buyer’s DDM parameters. Buyer’s choice \(c_i^b \in \{0,1\}\) and response time \(t_i^b \in R^+\) are determined by the price of the product \(p\), \(v_i^b\) and \(\theta_i^b\) with \[c_i^b=C_b(p,v_i^b,\theta_i^b), \; t_i^b =RT_b(p,v_i^b,\theta_i^b)\]

# Function to simulate buyers' behavior.
sim_ddm_buy = function(nbuy=50, values, param, prices) {
  buy_choices = matrix(data=NA, nrow=nbuy, ncol=length(prices))
  buy_rts = matrix(data=NA, nrow=nbuy, ncol=length(prices))
  for (buyer in 1:nbuy) {
    .buy_value = values$buy[buyer]
    for (price_idx in 1:length(prices)) {
      .evidence = .buy_value - prices[price_idx]
      .buy_behav = do.call(simulate_ddm, c(.evidence, param$buy[buyer,]))
      buy_choices[buyer,price_idx] = .buy_behav$choice
      buy_rts[buyer,price_idx] = .buy_behav$rt
    }
  }
  return(tibble(choices = buy_choices, rts = buy_rts))
}

Seller behavior

Let \(v_j^s\) be seller \(j\)’s value of the product and \(\theta_j^s\) be the seller’s DDM parameters. Seller’s choice \(c_j^s \in \{0,1\}\) and response time \(t_j^s \in R^+\) are determined by the price of the product \(p\), \(v_j^s\) and \(\theta_j^s\) with \[c_j^s=C_s(p,v_j^s,\theta_j^s),t_j^s =RT_s(p,v_j^s,\theta_j^s)\]

# Function to get sellers' behavior.
sim_ddm_sel = function(nsel=50, values, param, prices) {
  sel_choices = matrix(data=NA, nrow=nsel, ncol=length(prices))
  sel_rts = matrix(data=NA, nrow=nsel, ncol=length(prices))
  for (seller in 1:nsel) {
    .sel_value = values$sel[seller]
    for (price_idx in 1:length(prices)) {
      .evidence =  prices[price_idx] - .sel_value
      .sel_behav = do.call(simulate_ddm, c(.evidence, param$sel[seller,]))
      sel_choices[seller,price_idx] = .sel_behav$choice
      sel_rts[seller,price_idx] = .sel_behav$rt
    }
  }
  return(tibble(choices = sel_choices, rts = sel_rts))
}

Exchanges

Find the fastest buyer and seller to agree at a price, and have them exchange at that price. Then, they exit the market. Record this exchange. Repeat this process until the market clears.

# Define a function to find the fastest pair for a specific price
find_fastest_pair_at_price = function(ddm_buy, ddm_sel, price_idx) {
  
  # Find buyers and sellers who accepted the price at the given price index
  buy_who_accept = which(ddm_buy$choices[, price_idx] == 1)
  sel_who_accept = which(ddm_sel$choices[, price_idx] == 1)
  
  # Return NULL if no buyers or sellers accepted the price
  if (length(buy_who_accept) == 0 || length(sel_who_accept) == 0) {
    return(NULL)
  }
  
  # Generate all combinations of buyer and seller indices
  pairs = expand.grid(buy_who_accept, sel_who_accept)
  names(pairs) = c("buy_idx", "sel_idx")
  
  # Calculate response times for each pair
  pairs = pairs %>%
    mutate(
      buy_rt = ddm_buy$rts[buy_idx, price_idx],
      sel_rt = ddm_sel$rts[sel_idx, price_idx],
      rt = pmax(buy_rt, sel_rt)  # Get the maximum RT for each pair
    )
  
  # Find the fastest pair with the minimum response time
  fastest_pair_at_price = pairs %>%
    filter(rt == min(rt)) %>%
    slice(1)  # Take the first one if there are ties
  
  # Return the fastest pair information as a list
  return(tibble(
    buy_idx = fastest_pair_at_price$buy_idx,
    sel_idx = fastest_pair_at_price$sel_idx,
    price_idx = price_idx,
    rt = fastest_pair_at_price$rt
  ))
  
}
# Get a dataframe with the fastest pair of them all
find_fastest_pair = function(.ddm_buy, .ddm_sel, prices) {
  
  # Get a dataframe with the fastest pair for each price.
  fastest_pair_at_price = map_dfr(
    1:length(prices),
    ~find_fastest_pair_at_price(.ddm_buy, .ddm_sel, .x)
  ) 
  
  # Fastest across prices, or nothing.
  if (nrow(fastest_pair_at_price)>0) {
    fastest_pair = fastest_pair_at_price %>% arrange(rt) %>% slice(1)
  } else {
    fastest_pair = NULL
  }
    
  return(fastest_pair)
}
# Define a function to find the fastest pair across all fastest pairs at each price
get_ddm_exchanges = function(ddm_buy, ddm_sel, prices, values) {
  
  # Initialize the transaction tibble to store buyer-seller pairs and transaction details
  ddm_exchanges = tibble(
    buy_idx = integer(),
    buy_val = numeric(),
    sel_idx = integer(),
    sel_val = numeric(),
    price = integer(),
    rt = numeric()
  )
  
  # Make a copy of simulated ddm behavior to manipulate for looping.
  .ddm_buy = ddm_buy
  .ddm_sel = ddm_sel
  
  # Main loop to iterate until no more transactions can be made
  repeat {
    
    # Get fastest pair across all prices
    fastest_pair = find_fastest_pair(.ddm_buy, .ddm_sel, prices)
    
    # If no more pairs can be matched, exit the loop
    if (fastest_pair %>% is.null()) {break}
    
    # Record the transaction
    ddm_exchanges = ddm_exchanges %>%
      add_row(
        buy_idx = fastest_pair$buy_idx,
        buy_val = values$buy[fastest_pair$buy_idx],
        sel_idx = fastest_pair$sel_idx,
        sel_val = values$sel[fastest_pair$sel_idx],
        price = prices[fastest_pair$price_idx],
        rt = fastest_pair$rt
      )
    
    # Remove the matched buyer and seller from future matches
    .ddm_buy$choices[fastest_pair$buy_idx, ] = 0
    .ddm_sel$choices[fastest_pair$sel_idx, ] = 0
  }
  
  return(ddm_exchanges)
}

ZInt functions

Bidding behavior

As in Gode and Sunder (1997): bids are drawn randomly between 0 and valuation, asks are drawn randomly between valuation and an upper bound on asking price (1 here).

# Function.
zint_bidding = function(nbuy=50, nsel=50, values) {
  buy_bids = rep(NA, nbuy)
  sel_asks = rep(NA, nsel)
  for (i in 1:nbuy) {
    for (j in 1:nsel) {
      buy_bids[i] = runif(1, min=0, max=values$buy[i]) #%>% round(2) # random bids w/in range
      sel_asks[i] = runif(1, min=values$sel[i], max=1) #%>% round(2)
    }
  }
  return(list(bids = buy_bids, asks = sel_asks))
}

Exchanges

Match the highest-bidding buyer with the lowest-asking seller for an exchange. Then, they exit the market. Record this exchange. Repeat until the market clears.

# Function to match buyers and sellers based on bids and asks for a single round.
zint_single_round = function(nbuy=50, nsel=50, bids_asks) {
  
  # Placeholders for transaction list.
  exchanges_single_round = tibble()
  
  # Make copies that we can fuck around with.
  bids = bids_asks$bids
  asks = bids_asks$asks
  
  # Keep looping until there's no more possible transactions this round.
  exchange_number = 1
  while (max(bids, na.rm=T) > min(asks, na.rm=T)) {
    
    # Get index of highest bid and lowest ask.
    highest_bid_idx = which.max(bids)
    lowest_ask_idx = which.min(asks)
    
    # If the highest bid >= lowest ask, then add these two fuckers to the transaction list.
    if (bids[highest_bid_idx] >= asks[lowest_ask_idx]) {
      
      # New data to be added to placeholder.
      new.row = tibble(
        exchange_number = exchange_number,
        buy=highest_bid_idx, sel=lowest_ask_idx, 
        bid=bids[highest_bid_idx], ask=asks[lowest_ask_idx]
      )
      exchanges_single_round = bind_rows(exchanges_single_round, new.row)
      exchange_number = exchange_number + 1
      
      # Take these matched participants out of the bids and asks vectors.
      bids[highest_bid_idx] = NA
      asks[lowest_ask_idx] = NA
      
    }
    
    # Break loop if entirely NA bids and asks.
    if (all(is.na(bids)) || all(is.na(asks))) {break}
    
  }
  
  return(exchanges_single_round)
}


zint_all_rounds = function(nbuy=50, nsel=50, values, zint_round_limit=50) {
  
  # Get bids and asks.
  .bids_asks = zint_bidding(nbuy=nbuy, nsel=nsel, values)

  # Execute the matching function once.
  round = 1
  zint_exchanges = zint_single_round(nbuy=nbuy, nsel=nsel, .bids_asks)
  zint_exchanges$round = round
  
  # Get list of unmatched buyer and seller valuations.
  .unmatched_buy_val = values$buy[-zint_exchanges$buy]
  .unmatched_sel_val = values$sel[-zint_exchanges$sel]
  
  # Repeat the rounds until all possible matches are made
  # (i.e. until max(buyer values) < min(seller values) for remaining buyers and sellers)
  while (max(.unmatched_buy_val) >= min(.unmatched_sel_val) & round < zint_round_limit) {
    
    # Iterate rounds.
    round = round + 1
    
    # Get new bids and asks for unmatched participants.
    .bids_asks = zint_bidding(nbuy=nbuy, nsel=nsel, values)
    .bids_asks$bids[zint_exchanges$buy] = NA # take out the people who already transacted
    .bids_asks$asks[zint_exchanges$sel] = NA
    
    # Execute the matching function once.
    .new_exchanges = zint_single_round(nbuy=nbuy, nsel=nsel, .bids_asks)
    .new_exchanges$round = round
    zint_exchanges = bind_rows(zint_exchanges, .new_exchanges)
    
    # Get list of unmatched buyer and seller valuations.
    .unmatched_buy_val = values$buy[-zint_exchanges$buy]
    .unmatched_sel_val = values$sel[-zint_exchanges$sel]
  }
  
  return(zint_exchanges)
}

Results: Run DDM sims

Use the Drift-Diffusion-Model to model response times. Response times will depend on time pressure (the speed-accuracy trade-off), signal-to-noise ratio (ability of decision maker to process information for decisions), and the difference between valuation and price.

1: Equitable (nbuy = nsel)

Equal number of buyers and sellers. Assume buyer and seller parameter distributions are the same.

Structure & Valuations

# Market structure
nbuy = 500
nsel = 500

# Get buyer and seller values.
values = simulate_valuations(nbuy, nsel, seed=1)

Parameters

# DDM.
.ddm_param_buy = tibble(
  a = rtruncnorm(nbuy, a=0.9, b=1.1, mean=1.0, sd=0.05), #1.0, # bounds
  d = rtruncnorm(nbuy, a=0.010, b=0.020, mean=0.015, sd=0.005), # drift rate
  s = 0.04, #rtruncnorm(nbuy, a=0.01, b=0.10, mean=0.05, sd=0.02), # noise
  b = rtruncnorm(nbuy, a=-0.05, b=0.05, mean=0.00, sd=0.01), # bias
  c = 0.0, # rate of collapse
  ndt = 1.0, # non-decision time
  ts_size = .01, # timestep (s)
  rt_max = 60 # max simulation time (s)
)

.ddm_param_sel = tibble(
  a = rtruncnorm(nsel, a=0.9, b=1.1, mean=1.0, sd=0.05), #1.0, # bounds
  d = rtruncnorm(nsel, a=0.010, b=0.020, mean=0.015, sd=0.005), # drift rate
  s = 0.04, #rtruncnorm(nbuy, a=0.01, b=0.10, mean=0.05, sd=0.02), # noise
  b = rtruncnorm(nsel, a=-0.05, b=0.05, mean=0.00, sd=0.01), # bias
  c = 0.0, # rate of collapse
  ndt = 1.0, # non-decision time
  ts_size = .01, # timestep (s)
  rt_max = 60 # max simulation time (s)
)

ddm_param = list(buy = .ddm_param_buy, sel = .ddm_param_sel)

Behavior

Takes around 9 seconds.

# Get buyers' behavior.
ddm_buy = sim_ddm_buy(nbuy, values, ddm_param, prices)

# Get sellers' behavior.
ddm_sel = sim_ddm_sel(nsel, values, ddm_param, prices)

Exchanges

103 seconds without parallelization.

# Get transactions
ddm_exchanges = get_ddm_exchanges(ddm_buy, ddm_sel, prices, values)

# Sort ddm transactions by rt
ddm_exchanges = ddm_exchanges %>% arrange(rt)

# Display the transactions
#print(ddm_exchanges)

Price-time plot

ddm_exchanges$model = "DDM"

# Plot.
plt_ddm_prices = ggplot(data=ddm_exchanges) +
  .myPlot +
  geom_hline(yintercept=0.5, color="grey") +
  geom_point(aes(x=rt, y=price)) +
  labs(title = "DDM: RT Matching", x = "Time (secs)", y = "Price ($)") +
  coord_cartesian(xlim = c(0,NA), ylim = c(0,1), expand=T)

# # Animate.
# .ani_prices = plt_ddm_prices +
#   geom_line(aes(x=rt/1000, y=price), linewidth=1) +
#   scale_x_continuous(breaks = seq(0,15,5)) +
#   scale_y_continuous(breaks = seq(0,1,.5)) +
#   transition_reveal(rt)
# 
# anim_save(
#   file.path(.outdir, "ddm_sim-price_over_time.gif"),
#   animation = .ani_prices,
#   renderer = gifski_renderer()
# )
# 
# # Save .gif animation
# knitr::include_graphics(file.path(.outdir, "ddm_sim-price_over_time.gif"))

2: Excess demand (nbuy > nsel)

If there are more buyers than sellers, theory predicts the gains from exchange should go to sellers.

Structure & Valuations

# Market structure
nbuy = 1000
nsel = 500

# Get buyer and seller values.
values = simulate_valuations(nbuy, nsel)

Parameters

# DDM.
.ddm_param_buy = tibble(
  a = rtruncnorm(nbuy, a=0.9, b=1.1, mean=1.0, sd=0.05), # bounds
  d = rtruncnorm(nbuy, a=0.010, b=0.020, mean=0.015, sd=0.005), # drift rate
  s = 0.04, # noise
  b = rtruncnorm(nbuy, a=-0.05, b=0.05, mean=0.00, sd=0.01), # bias
  c = 0.0, # rate of collapse
  ndt = 1.0, # non-decision time
  ts_size = .01, # timestep (s)
  rt_max = 60 # max simulation time (s)
)

.ddm_param_sel = tibble(
  a = rtruncnorm(nsel, a=0.9, b=1.1, mean=1.0, sd=0.05), # bounds
  d = rtruncnorm(nsel, a=0.010, b=0.020, mean=0.015, sd=0.005), # drift rate
  s = 0.04, # noise
  b = rtruncnorm(nsel, a=-0.05, b=0.05, mean=0.00, sd=0.01), # bias
  c = 0.0, # rate of collapse
  ndt = 1.0, # non-decision time
  ts_size = .01, # timestep (s)
  rt_max = 60 # max simulation time (s)
)

ddm_param = list(buy = .ddm_param_buy, sel = .ddm_param_sel)

Behavior

# Get buyers' behavior.
ddm_buy = sim_ddm_buy(nbuy, values, ddm_param, prices)

# Get sellers' behavior.
ddm_sel = sim_ddm_sel(nsel, values, ddm_param, prices)

Exchanges

# Get transactions
ddm_exchanges_xs_buy = get_ddm_exchanges(ddm_buy, ddm_sel, prices, values)

# Sort ddm transactions by rt
ddm_exchanges_xs_buy = ddm_exchanges_xs_buy %>% arrange(rt)

# Display the transactions
#print(ddm_exchanges_xs_buy)

Price-time plot

# Plot.
plt_ddm_exchanges_xs_buy = ggplot(data=ddm_exchanges_xs_buy) +
  .myPlot +
  geom_hline(yintercept=0.67, color="grey") +
  geom_point(aes(x=rt, y=price)) +
  labs(title = "DDM: RT Matching", x = "Time (secs)", y = "Price ($)") +
  coord_cartesian(xlim = c(0,NA), ylim = c(0,1), expand=T)

3: Excess supply (nbuy < nsel)

What happens if there are more sellers than buyers?

Structure & Valuations

# Market structure
nbuy = 500
nsel = 1000

# Get buyer and seller values.
values = simulate_valuations(nbuy, nsel)

Parameters

# DDM.
.ddm_param_buy = tibble(
  a = rtruncnorm(nbuy, a=0.9, b=1.1, mean=1.0, sd=0.05) , # bounds
  d = rtruncnorm(nbuy, a=0.010, b=0.020, mean=0.015, sd=0.005), # drift rate
  s = 0.04, # noise
  b = rtruncnorm(nbuy, a=-0.05, b=0.05, mean=0.00, sd=0.01), # bias
  c = 0.0, # rate of collapse
  ndt = 1.0, # non-decision time
  ts_size = .01, # timestep (s)
  rt_max = 60 # max simulation time (s)
)

.ddm_param_sel = tibble(
  a = rtruncnorm(nsel, a=0.9, b=1.1, mean=1.0, sd=0.05), # bounds
  d = rtruncnorm(nsel, a=0.010, b=0.020, mean=0.015, sd=0.005), # drift rate
  s = 0.04, # noise
  b = rtruncnorm(nsel, a=-0.05, b=0.05, mean=0.00, sd=0.01), # bias
  c = 0.0, # rate of collapse
  ndt = 1.0, # non-decision time
  ts_size = .01, # timestep (s)
  rt_max = 60 # max simulation time (s)
)

ddm_param = list(buy = .ddm_param_buy, sel = .ddm_param_sel)

Behavior

# Get buyers' behavior.
ddm_buy = sim_ddm_buy(nbuy, values, ddm_param, prices)

# Get sellers' behavior.
ddm_sel = sim_ddm_sel(nsel, values, ddm_param, prices)

Exchanges

# Get transactions
ddm_exchanges_xs_sel = get_ddm_exchanges(ddm_buy, ddm_sel, prices, values)

# Sort ddm transactions by rt
ddm_exchanges_xs_sel = ddm_exchanges_xs_sel %>% arrange(rt)

# Display the transactions
#print(ddm_exchanges_xs_sel)

Price-time plot

# Plot.
plt_ddm_exchanges_xs_sel = ggplot(data=ddm_exchanges_xs_sel) +
  .myPlot +
  geom_hline(yintercept=0.33, color="grey") +
  geom_point(aes(x=rt, y=price)) +
  labs(title = "DDM: RT Matching", x = "Time (secs)", y = "Price ($)") +
  coord_cartesian(xlim = c(0,NA), ylim = c(0,1), expand=T)

4: Time-pressured buyers

What happens if buyers have more pressure to respond more quickly compared to sellers in an equitable market structure?

Structure & Valuations

# Market structure
nbuy = 500
nsel = 500

# Get buyer and seller values.
values = simulate_valuations(nbuy, nsel)

Parameters

# DDM.
.ddm_param_buy = tibble(
  a = rtruncnorm(nbuy, a=0.9, b=1.1, mean=1.0, sd=0.05) * 0.5, # bounds
  d = rtruncnorm(nbuy, a=0.010, b=0.020, mean=0.015, sd=0.005), # drift rate
  s = 0.04, # noise
  b = rtruncnorm(nbuy, a=-0.05, b=0.05, mean=0.00, sd=0.01), # bias
  c = 0.0, # rate of collapse
  ndt = 1.0, # non-decision time
  ts_size = .01, # timestep (s)
  rt_max = 60 # max simulation time (s)
)

.ddm_param_sel = tibble(
  a = rtruncnorm(nsel, a=0.9, b=1.1, mean=1.0, sd=0.05), # bounds
  d = rtruncnorm(nsel, a=0.010, b=0.020, mean=0.015, sd=0.005), # drift rate
  s = 0.04, # noise
  b = rtruncnorm(nsel, a=-0.05, b=0.05, mean=0.00, sd=0.01), # bias
  c = 0.0, # rate of collapse
  ndt = 1.0, # non-decision time
  ts_size = .01, # timestep (s)
  rt_max = 60 # max simulation time (s)
)

ddm_param = list(buy = .ddm_param_buy, sel = .ddm_param_sel)

Behavior

# Get buyers' behavior.
ddm_buy = sim_ddm_buy(nbuy, values, ddm_param, prices)

# Get sellers' behavior.
ddm_sel = sim_ddm_sel(nsel, values, ddm_param, prices)

Exchanges

# Get transactions
ddm_exchanges_tp_buy = get_ddm_exchanges(ddm_buy, ddm_sel, prices, values)

# Sort ddm transactions by rt
ddm_exchanges_tp_buy = ddm_exchanges_tp_buy %>% arrange(rt)

# Display the transactions
#print(ddm_exchanges_tp_buy)

Price-time plot

# Plot.
plt_ddm_exchanges_tp_buy = ggplot(data=ddm_exchanges_tp_buy) +
  .myPlot +
  geom_hline(yintercept=0.5, color="grey") +
  geom_point(aes(x=rt, y=price)) +
  labs(title = "DDM: RT Matching", x = "Time (secs)", y = "Price ($)") +
  coord_cartesian(xlim = c(0,NA), ylim = c(0,1), expand=T)

5: Time-pressured sellers

As a robustness check, what happens if sellers have more pressure to respond more quickly compared to buyers in an equitable market structure?

Structure & Valuations

# Market structure
nbuy = 500
nsel = 500

# Get buyer and seller values.
values = simulate_valuations(nbuy, nsel)

Parameters

# DDM.
.ddm_param_buy = tibble(
  a = rtruncnorm(nbuy, a=0.9, b=1.1, mean=1.0, sd=0.05) , # bounds
  d = rtruncnorm(nbuy, a=0.010, b=0.020, mean=0.015, sd=0.005), # drift rate
  s = 0.04, # noise
  b = rtruncnorm(nbuy, a=-0.05, b=0.05, mean=0.00, sd=0.01), # bias
  c = 0.0, # rate of collapse
  ndt = 1.0, # non-decision time
  ts_size = .01, # timestep (s)
  rt_max = 60 # max simulation time (s)
)

.ddm_param_sel = tibble(
  a = rtruncnorm(nsel, a=0.9, b=1.1, mean=1.0, sd=0.05) * 0.5, # bounds
  d = rtruncnorm(nsel, a=0.010, b=0.020, mean=0.015, sd=0.005), # drift rate
  s = 0.04, # noise
  b = rtruncnorm(nsel, a=-0.05, b=0.05, mean=0.00, sd=0.01), # bias
  c = 0.0, # rate of collapse
  ndt = 1.0, # non-decision time
  ts_size = .01, # timestep (s)
  rt_max = 60 # max simulation time (s)
)

ddm_param = list(buy = .ddm_param_buy, sel = .ddm_param_sel)

Behavior

# Get buyers' behavior.
ddm_buy = sim_ddm_buy(nbuy, values, ddm_param, prices)

# Get sellers' behavior.
ddm_sel = sim_ddm_sel(nsel, values, ddm_param, prices)

Exchanges

# Get transactions
ddm_exchanges_tp_sel = get_ddm_exchanges(ddm_buy, ddm_sel, prices, values)

# Sort ddm transactions by rt
ddm_exchanges_tp_sel = ddm_exchanges_tp_sel %>% arrange(rt)

# Display the transactions
#print(ddm_exchanges_tp_sel)

Price-time plot

# Plot.
plt_ddm_exchanges_tp_sel = ggplot(data=ddm_exchanges_tp_sel) +
  .myPlot +
  geom_hline(yintercept=0.5, color="grey") +
  geom_point(aes(x=rt, y=price)) +
  labs(title = "DDM: RT Matching", x = "Time (secs)", y = "Price ($)") +
  coord_cartesian(xlim = c(0,NA), ylim = c(0,1), expand=T)

Results: Run ZInt sims

1: Equitable (nbuy = nsel)

For a given round, iterate through the following process. The highest bidding buyer and the lowest asking seller are matched, conditional on bid \(\geq\) ask. Repeat this process until the condition is no longer satisfied.

Structure & Valuations

# Market structure
nbuy = 1000
nsel = 1000

# Get buyer and seller values.
values = simulate_valuations(nbuy, nsel, seed=1)

Exchanges

# Get ZInt exchanges
zint_exchanges = zint_all_rounds(nbuy, nsel, values, zint_round_limit=50)

Prices

Randomly select buyer or seller price.

# Randomly select buyer or seller price.
zint_exchanges = zint_exchanges %>%
 mutate(price = ifelse(runif(n())>0.5, bid, ask))

# # Average of buyer and seller price.
# zint_transactions = zint_transactions %>%
#   mutate(price = (bid+ask)/2)

# # Asking price.
# zint_transactions = zint_transactions %>%
#  mutate(price = ask)

Price-time plot

zint_exchanges$model = "ZInt"

pdata = zint_exchanges # %>%
#  mutate(round = ifelse(round>100, 100, round)) # Truncate to 100 rounds max

plt_zint_simple_prices = ggplot(data=pdata, aes(x=round, y=price)) +
  .myPlot + 
  geom_hline(yintercept=0.5, color="grey") +
  geom_jitter(width=.2) +
  labs(title = "ZInt: Simple Matching, Rounds (trunc 40), 50-50 Chance Price", y = "Price", x = "Rounds") +
  coord_cartesian(ylim=c(0,1))

Compare with DDM

How fast would each round have to be in order for this to compare to the DDM?

# What is the time at which the market cleared with DDM simulations?
ddm_time_marketclear = max(ddm_exchanges$rt)

# How fast would each ZInt round need to be to match ddm market clear time?
pdata_zint = zint_exchanges
zint_time_per_round = (ddm_time_marketclear/max(pdata_zint$round)) %>% round(2)
pdata_zint$rt = pdata_zint$round * zint_time_per_round

# Combine transaction tibbles for plot data.
pdata = bind_rows(ddm_exchanges, pdata_zint)

# Plot.
plt_zint_ddm_compare = ggplot(data = pdata, aes(x=rt, y=price, group=model, color=model)) +
  .myPlot +
  geom_hline(yintercept=0.5, color="grey") +
  geom_point(alpha=.75) +
  coord_cartesian(ylim=c(0,1)) +
  labs(y = "Transaction Price ($)", x = "Time (secs)", color = "Simulated with") +
  labs(title = paste0(
    "ZInt: Each round would need to be ", 
    zint_time_per_round, 
    " seconds to match DDM."
  ))

Results: Metrics from Ikica

Load and clean data

ikica <- read.csv("../../data/ikica/ikica_data.csv")

Speed of mean convergence

Speed of volatility convergence

Direction of convergence

Exchange-Offer Ratio

======= Paper =======

Introduction

To study competitive markets in an experimental setting, I use the double auction, which is widely employed in real-world marketplaces (e.g. stock exchanges, electricity markets, carbon permits). Buyers and sellers simultaneously post their prices. When a buyer’s bid exceeds a seller’s asking price, they are matched and a transaction occurs. Over time, prices stabilize at the “competitive equilibrium” price, and this forms the basis for a market economy. Theoretically, this stabilization at the competitive price occurs if the highest bidding buyers are most likely to match with the lowest asking sellers (Gode & Sunder, 1997). This project suggests a novel, neuro-cognitive reason why: these buyers and sellers bid earlier, and are therefore more likely to match, ensuring that prices stabilize in a competitive market.

Sequential sampling models from neuroscience have been applied to describe how individuals gather information over time before making an economic decision (Forstmann et al., 2016; Webb, 2019). These models capture how individuals trade-off speed and accuracy when making a decision. A key prediction of these models is that individuals are faster to decide when evidence strongly favors one option (Mormann et al., 2010; Rasanan et al., 2024; Ratcliff et al., 2016). This key fact could have enormously important—but previously unrecognized—implications for market clearing in double auctions, since these models would predict that, for any given price, the fastest individuals to engage with the marketplace are (1) the seller with the lowest valuation and (2) the buyer with the highest. Coincidentally, these two individuals are also the most likely match for a transaction, since the buyer’s bid simply needs to be larger than the seller’s asking price. Therefore, this theory predicts that a competitive market equilibrium arises naturally through the timing of market engagement. My research will test this prediction theoretically and empirically by studying participants in a large set of double auction experiments, where I can observe market dynamics and compare them to the predictions of sequential sampling models for both discrete (accept/reject) and continuous (bid) behavior.

Related Literature and History:

  • Hicks (1934, Econometrica): Cournot laid out a framework for monopoly and duopoly (limited competition), and suggested that under perfect competition, no single producer has the power to influence prices of the market. He was only concerned about the behavior of producers. From this, Walras converted the concept of perfect competition to a technique of using prices as economic parameters. In other words, Walras formalized the idea of price-taking behavior under competitive equilibrium. By doing so, Walras was able to model the exchange of commodities under competitive equilibrium, concerning himself with more than just the behavior of producers.

  • Edgeworth (1881): In response to Walras, Edgeworth claimed individuals do not need to be price-takers from the outset. Instead, through “recontracting”, competitive prices and allocations emerge as agents seek and exhaust opportunities for mutually beneficial trades.

  • “Walrasian auction” (aka tatonnement): A hypothetical auctioneer announces tentative prices for goods, and buyers and sellers respond with the quantities they are willing to buy or sell at those prices. The process iterates until market clears, achieving equilibrium without any agent influencing prices. Limitation: The tatonnement process doesnt allow trading until equilibrium is reached… which doesn’t allow for real-world situations where transactions occur continuously, often out of equilibrium.

Theory

Here, we will define the Response Time Matching Rule for a double auction and show that the final transaction price under this rule approximates the competitive equilibrium price as the number of buyers and sellers grows large.

Ass. 1: Value Distrib.

Reservation Value Distributions. Suppose there are \(n\) buyers and \(mn\) sellers. Assume \(n\) is large.

  • Buyer’s reservation value \(v_i^b \sim U[0,1]\)

  • Seller’s reservation value \(v_i^s \sim U[0,1]\)

Competitive Equilibrium (q*,p*)

Because buyers and sellers have uniformly distributed reservation prices between 0 and 1, the total supply and total demand functions look like:

  • Total demand function: \(q = n(1-p)\)

  • Total supply function: \(q = mnp\)

For market clearing to occur (total supply equals total demand), equilibrium price and quantity need to be:

  • \(p^* = \frac{1}{m+1}\)

  • \(q^* = \frac{mn}{m+1}\)

Ass. 2: Choices and RTs

Choices and Response Times. Choices are made without error.

  • Buyer accepts when \(p \leq v_i^b\)

  • Seller accepts when \(p \geq v_i^s\)

Response Times (RT) are proportional to the difference between value and price, as in the DDM.

  • \(t_i = f(\mu_i)\) where \(f'(x)<0\)

    • For buyers, \(\mu_i = |v_i^b - p|\)

    • For sellers, \(\mu_i = |p - v_i^s|\)

RT Matching Rule

At time \(t_i^b = f(|v_i^b-p|)\), each buyer announces the price, \(p_i^b\), he is willing to buy at. At time \(t_js = f(|p-v_js|)\), each seller announces the price, \(p_j^s\), she is willing to sell at. At any time \(t\), if there exists \(p_i^b \geq p_j^s\), then buyer \(i\) and seller \(j\) make a transaction at \(p \in [p_j^s, p_i^b]\) and exit the market. Since \(T=f(0)\) is the maximum time for each buyer and seller to make a decision, there are no more transactions after \(T\). Call the last transaction price \(p^E\). We want to prove that \(p^E = p* = \frac{1}{m+1}\).

Price-Time Functions

Let’s start with an example. If buyer \(i\) has reservation value \(v_i^b=0.5\), then he will announce \(p_i^b=0\) at time \(t_i=f(0.5)\), and then gradually increase his price to \(p_i^b = v_i^b=0.5\) at time \(t_i=T\) if no seller is matched.

If seller \(j\) has reservation value \(v_j^s=0.3\), then she will announce \(p_j^s=1\) at time \(t_j=f(0.3)\), and then gradually decrease her price to \(p_j^s = v_j^s=0.3\) at time \(t_j=T\) if no buyer is matched.

This illustrates that for every buyer, the price he announces is a function of \(t\) and \(v_i^b\). \(\because t_i=f(v_i^b-p) \; \forall p \leq v_i^b, \; v_i^b - p = f^{-1}(t) \implies p_i^b = v_i^b - f^{-1}(t)\). Let \(g(t) = f^{-1}(t)\). \(\because f'(x)<0, \; g'(t)<0\) since the derivative of the inverse function maintains the sign. \(\therefore p_i^b=v_i^b - g(t)\) is an increasing function in \(t\) that passes through \((f(v_i^b), 0)\) and \((T, v_i^b)\). This means that the price-time functions for buyers are parallel, increasing functions.

For every seller, the price she announces is also a function of \(v_j^s\) and \(t\). \(\because t_j = f(p-v_j^s) \; \forall p \geq v_j^s, \; p_j^s = v_j^s + f^{-1}(t) = v_j^s + g(t)\). \(\therefore p_j^s = v_j^s + g(t)\) is a decreasing function in \(t\) that passes through \((f(1-v_j^s), 1)\) and \((T,v_j^s)\). Therefore, the price-time functions for sellers are parallel, decreasing functions.

TLDR:

  • \(p_i^b = v_i^b - g(t)\)

  • \(p_j^s = v_j^s + g(t)\)

Ass. 3: Res. Values

Reservation Values. Let’s assign reservation values that agree with our earlier assumptions about the distribution of reservation values.

  • \(v_1^b=1, v_2^b=\frac{n-2}{n-1}, \ldots, v_k^b=\frac{n-k}{n-1}, \ldots, v_n^b=0\)

  • \(v_1^s=0, v_2^s=\frac{1}{mn-1}, \ldots, v_k^b=\frac{k-1}{mn-1}, \ldots, v_{mn}^b=1\)

Proposition 1

Under the RT Matching Rule, the \(k^{th}\) buyer will match for transaction with the \(k^{th}\) seller, \(\forall k \leq \frac{mn}{m+1}\) . The final transaction price, \(p^E\), will approximate \(p^* = \frac{1}{m+1}\) as \(n \rightarrow \infty\).

Proof of Proposition 1

Step 1: Assuming \(v_k^s \leq p \leq v_k^b\), show the \(k^{th}\) buyer and \(k^{th}\) seller will match for transaction.

In order for a match to occur between buyer \(i\) and either seller \(j\) or \(j+1\), \(v_j^s \leq v_{j+1}^s \leq p \leq v_i^b\). \(\because f'(x)<0\) and \(t_j = f(p-v_j^s) \; \forall v_j^s \leq p\), \(f(p-v_j^s) \leq f(p-v_{j+1}^s)\). This means the fastest seller to match with buyer \(i\) is seller \(j\), or the lowest reservation value seller available.

Similarly, we can consider a match between seller \(j\) and either buyer \(i-1\) or \(i\), where \(v_j^s \leq p \leq v_{i-1}^b \leq v_i^b\). \(\because f'(x)<0\) and \(t_i = f(v_i^b - p) \; \forall v_i^b \geq p\), \(f(v_i^b - p) \leq f(v_{i-1}^b - p)\). This means the fastest buyer to match with seller \(j\) is buyer \(i\), or the highest reservation value buyer available.

At the start of the market, all buyers and sellers are available to match for transaction. By definition, the highest reservation value buyer and lowest reservation value seller were assigned an index of 1, the second highest reservation value buyer and second lowest reservation value seller were assigned an index of 2, and so on. This means the \(k^{th}\) buyer and \(k^{th}\) seller will match for transaction, as long as \(v_k^s \leq p \leq v_k^b\).

Step 2: Show the \(k^{th}\) pair will transact at a price equal to the mean of their reservation values.

\(\because p_k = v_k^b - g(t) = \frac{n-k}{n-1} - g(t)\) and \(p_k = v_k^s + g(t) = \frac{k-1}{mn-1} + g(t)\),

\(\therefore\)

\[\begin{align} g(t) &= \frac{1}{2}(v_k^b - v_k^s) = \frac{1}{2}\left(\frac{n-k}{n-1} - \frac{k-1}{mn-1}\right) \\ p_k(t) &= \frac{1}{2}(v_k^b + v_k^s) = \frac{1}{2}\left(\frac{n-k}{n-1} + \frac{k-1}{mn-1}\right) \end{align}\]

The transaction price for the \(k^{th}\) pair is the mean of their reservation values.

Step 3: Show that if the number of market participants is large, the final transaction price is equal to the competitive equilibrium price.

The transaction can only occur if \(v_k^b \geq v_k^s\), \(\therefore \frac{n-k}{n-1} \geq \frac{k-1}{mn-1} \implies k \leq \frac{mn^2-1}{mn+n-2}=\frac{mn-1/n}{m+1-2/n} \approx \frac{mn}{m+1}\) when \(n\) is large. Then the largest that \(k\) can be is \(\frac{mn}{m+1} = q^*\), which is the competitive equilibrium quantity.

Let \(k^* = \frac{mn}{m+1}\). Then the last transaction price is:

\[\begin{align} p_{k^*}^E &= \frac{1}{2}\left(\frac{n-\frac{mn}{m+1}}{n-1} + \frac{\frac{mn}{m+1}-1}{mn-1}\right) \\ &= \frac{1}{2(m+1)} \left( \frac{1}{1+\frac{1}{n}} + \frac{m-\frac{m}{n}-\frac{1}{n}}{m-\frac{1}{n}} \right) \\ \lim_{n\rightarrow\infty} p_{k^*}^E &= \lim_{n\rightarrow\infty} \frac{1}{2(m+1)} \left( \frac{1}{1+\frac{1}{n}} + \frac{m-\frac{m}{n}-\frac{1}{n}}{m-\frac{1}{n}} \right) \\ &= \frac{2}{2(m+1)} \\ &= \frac{1}{m+1} = p^* \end{align}\]

Therefore, we have shown that under the RT Matching Rule, the final transaction price will approximate the competitive equilibrium price as \(n \rightarrow \infty\). This is true for any ratio of buyers to sellers, \(m\). \(\square\)

Hypotheses

These are hypotheses generated from the framework above. Our goal is to test these hypotheses with two methods. First, via simulations. Second, via experimental evidence. Proofs for the corollaries can be found in the appendix.

  • Proposition 1 (Competitive Equilibrium): Prices in a market with the Response Time Matching Rule will converge to the competitive equilibrium price as the number of buyers and sellers grows large, regardless of the ratio of buyers to sellers.

  • Proposition 2 (Excess Buyers): Buyers with the same reservation values will collect fewer gains from exchange in a market with excess buyers and a RT Matching Rule.

  • Corollary 2.1 (Excess Sellers): Sellers with the same reservation values will collect fewer gains from exchange in a market with excess sellers and a RT Matching Rule.

  • Proposition 3 (Time-Pressured Buyers): Most of the gains from exchange will go to the sellers in a RT-Matching Market with time-pressured buyers.

  • Corollary 3.1 (Time-Pressured Sellers): Most of the gains from exchange will go to the buyers in a RT-Matching Market with time-pressured sellers.

Methods

Experiment design

A vector of prices will be generated in lieu of bidding strategies. The auction is carried out in two stages: the response stage and the matching stage.

During the response stage, buyers and sellers go through each price in the price vector and respond “accept” if they are willing to exchange at this price, or “reject” if they are unwilling. A choice and response time is recorded for each participant at each price.

During the matching stage, we find the fastest buyer and seller to agree at a price. They are matched for exchange at that price, then exit the market. This process is repeated until the market clears.

Simulation Methods

We run simulations of a double auction experiment and demonstrate the predictions of our model under various market conditions.

Let \(v_i^b\) be buyer \(i\)’s value of the product and \(\theta_i^b\) be the buyer’s DDM parameters. Buyer’s choice \(c_i^b \in \{0,1\}\) and response time \(t_i^b \in R^+\) are determined by the price of the product \(p\), \(v_i^b\) and \(\theta_i^b\) with \[c_i^b=C_b(p,v_i^b,\theta_i^b), \; t_i^b =RT_b(p,v_i^b,\theta_i^b)\]

Let \(v_j^s\) be seller \(j\)’s value of the product and \(\theta_j^s\) be the seller’s DDM parameters. Seller’s choice \(c_j^s \in \{0,1\}\) and response time \(t_j^s \in R^+\) are determined by the price of the product \(p\), \(v_j^s\) and \(\theta_j^s\) with \[c_j^s=C_s(p,v_j^s,\theta_j^s),t_j^s =RT_s(p,v_j^s,\theta_j^s)\]

Results: DDM Simulations

1: Equitable (nbuy = nsel)

With an equal number of buyers and sellers in the market, the competitive equilibrium price, \(p^*\), is $0.50. In a RT-Matching Market, \(p\) converges to \(p^*\) in time, relatively quickly.

plt_ddm_prices

2: Excess demand (nbuy > nsel)

The competitive equilibrium price is $0.75 since there are twice as many buyers than sellers. The RT-Matching Market converges to competitive equilibrium price.

plt_ddm_exchanges_xs_buy

3: Excess supply (nbuy < nsel)

The competitive equilibrium price is $0.25 since there are twice as many sellers than buyers. The RT-Matching Market converges to competitive equilibrium price.

plt_ddm_exchanges_xs_sel

4: Time-pressured buyers

The market still converges to the competitive equilibrium price. However, as it converges, buyers agree to prices with a larger error rate (i.e. they are faster to accept larger prices than before). Note that sellers were already faster to accept larger prices since they prefer larger prices. This means that the majority of transactions are occurring at prices above competitive equilibrium price during the market clearing process.

plt_ddm_exchanges_tp_buy

5: Time-pressured sellers

The logic is the same compared to the last section. Sellers are faster to accept at lower prices since they are trading off accuracy for speed. This results in earlier transactions occurring at lower prices, despite the observation that the market still converges to the competitive equilibrium price over time. This means that most of the gains from exchange are going to the buyers in this equitable market, simply due to time pressure.

plt_ddm_exchanges_tp_sel

Results: ZInt Simulations

1: Equitable (nbuy = nsel)

Price-time plot

Prices in a double auction with zero-intelligence traders also converge to competitive equilibrium prices, albeit over a large number of rounds.

plt_zint_simple_prices

Compare with DDM

How fast would each round have to be in order for this to compare to the DDM?

Note that each round would need to be unrealistically fast in order for the market to clear within the time that DDM predicts.

In Ikica et al. (2023), one round typically lasted around 2 minutes.

plt_zint_ddm_compare

Results: Metrics with Ikica

Speed of Mean Convergence

Speed of Volatility Convergence

Direction of Convergence

Exchange-Offer Ratio

Results: Experiment

Take a look at Ikica et al. (2023) to see how DDM compares against ZInt for explaining data.

Discussion

Hypothesis

  • We propose that there are multiple mechanisms that make markets allocationally efficient. Economists have shown that the probabilistic matching of the highest bidding buyer and the lowest asking seller given zero-intelligence is sufficient for allocative efficiency in a double auction. We propose that response time matching is also sufficient for allocative efficiency, and that it generates more realistic predictions about convergence dynamics.

Methods

  • To model response times, we borrow a canonical model from computational neuroscience called the Drift-Diffusion-Model. Cite papers from Rangel, Fudenberg, Drugowitsch, Pouget, Ortoleva, Ratcliff.

Results

  • We posit a framework to think about response times in a double auction. We show that prices in a market with a RT Matching Rule will approximate the competitive equilibrium price as the number of participants grows large.

  • Through a series of simulations, we show that modeling response times and using the speed at which decisions are made to match market participants is sufficient for the market to converge to the competitive equilibrium. This convergence occurs much faster than predicted by ZInt, and it can be modeled in real-time. It does not rely on randomly drawing response times from a distribution of times, it is sensitive to the valuations of the market participants, and can be extended to include preferences over other psychological forces (e.g. limited attention, social preferences, strategic thinking).

  • Our simulations also show that RT matching is capable of explaining inequitable equilibria in markets with excess buyers or sellers. Inequitable markets impose time pressure on either the buyers or the sellers, forcing them to trade off accuracy for speed. As a result, prices will disfavor the party with excess members, and the majority of gains from exchange will go to the other party.

Acknowledgements

The authors would like to thank Yujia Wan for her invaluable help with developing the theory and Colin Camerer for early comments and suggestions.

Appendix

1: Proof of Proposition 2

Recall from the Proof of Proposition 1 that the \(k^{th}\) buyer and seller transact at \(p_k = \frac{1}{2} \left( \frac{n-k}{n-1} + \frac{k-1}{mn-1} \right)\). \(\because\) in an equitable market \(mn=n\), \(p_k^{EQ} = \frac{1}{2} \; \forall k\). EQ stands for equitable market.

Excess Buyers. Suppose \(m \in (0,1)\).

\[\begin{align} p_k &= \frac{1}{2} \left( \frac{n-k}{n-1} + \frac{k-1}{mn-1} \right) \\ &= \frac{1}{2} \left( \frac{mn^2-2n-kmn+kn+1}{mn^2-n-mn+1} \right) \end{align}\]

WTS: \(\frac{mn^2-2n-kmn+kn+1}{mn^2-n-mn+1}>1\)

\(mn^2-2n-kmn+kn+1-(mn^2-n-mn+1)>0 \implies (k-1)(1-m)n>0\). This is true, therefore WTS holds. If \(k=1\), then \(p_1^{EB} = p_1^{EQ}\) since the first buyer and seller to match are still those with the most extreme reservation values. EB stands for excess buyers. However \(\forall k>1\), \(p_k^{EB}>p_k^{EQ}\) since \(m \in (0,1)\). The degree of the difference in prices is decreasing in \(m\) and increasing in \(k\) and \(n\).

Prices will be higher in a market with excess buyers compared to an equitable market. This means buyers with the same reservation values will experience less gains from trade in the market with excess buyers than in the equitable market. \(\square\)

2: Proof of Corollary 2.1

Recall from the Proof of Proposition 2 that \(p_k^{EQ} = \frac{1}{2} \; \forall k\).

Excess Sellers. Suppose \(m>1\). Recall from the Proof of Proposition 2 that \(p_k = \frac{1}{2} \left( \frac{mn^2-2n-kmn+kn+1}{mn^2-n-mn+1} \right)\). While before, we took the difference between the numerator and denominator in the second term and showed it was greater than 0, this time we will show that this difference is less than 0. In other words, \((k-1)(1-m)n<0\). This is true, therefore WTS holds. Again, when \(k=1\), \(p_1^{ES}=p_1^{EQ}\) since the first buyer and seller to match are those with the most extreme reservation values. ES stands for excess sellers. However \(\forall k>1\), \(p_k^{ES}<p_k^{EQ}\) since \(m>1\). The degree of the difference in prices is increasing in \(k\), \(n\), and \(m\).

Prices will be lower in a market with excess sellers compared to an equitable market. This means sellers with the same reservation values will experience less gains from trade in the market with excess sellers than in the equitable market. \(\square\)

3: Proof of Proposition 3

Time-Pressured Buyers. This has to do with error rate in the DDM. We haven’t incorporated this into the framework yet, as Assumption 2 assumes buyers and sellers dont make mistakes. Next steps!

4: Proof of Corollary 3.1

Time-Pressured Sellers. If you can dodge a wrench, you can dodge a ball.