# Matching Market

This simple model consists of a buyer, a supplier, and a market.

The buyer represents a group of customers whose willingness to pay for a single unit of the good is captured by a vector of prices wta. You can initiate the buyer with a set_quantity function which randomly assigns the willingness to pay according to your specifications. You may ask for these willingness to pay quantities with a getbid function.

The supplier is similiar, but instead the supplier is willing to be paid to sell a unit of technology. The supplier for instance may have non-zero variable costs that make them unwilling to produce the good unless they receive a specified price. Similarly the supplier has a get_ask function which returns a list of desired prices.

The willingness to pay or sell are set randomly using uniform random distributions. The resultant lists of bids are effectively a demand curve. Likewise the list of asks is effectively a supply curve. A more complex determination of bids and asks is possible, for instance using time of year to vary the quantities being demanded.

## New in version 5

• The biggest addition in this verison is the observer agent who controls the rest of the model.
• A start is made to make the buyer agent demand time dependent instead of deterministic.

## Microeconomic Foundations

The market assumes the presence of an auctioneer which will create a book, which seeks to match the bids and the asks as much as possible. If the auctioneer is neutral, then it is incentive compatible for the buyer and the supplier to truthfully announce their bids and asks. The auctioneer will find a single price which clears as much of the market as possible. Clearing the market means that as many willing swaps happens as possible. You may ask the market object at what price the market clears with the get_clearing_price function. You may also ask the market how many units were exchanged with the get_units_cleared function.

## Agent-Based Objects

The following section presents three objects which can be used to make an agent-based model of an efficient, two-sided market.

``````

In [1]:

%matplotlib inline
import matplotlib.pyplot as plt
import random as rnd
import pandas as pd
import numpy as np
import time
import datetime

``````
``````

In [2]:

# measure how long it takes to run the script
startit = time.time()
dtstartit = datetime.datetime.now()

class Seller():
def __init__(self, name):
self.name = name
self.wta = []

# the supplier has n quantities that they can sell
# they may be willing to sell this quantity anywhere from a lower price of l
# to a higher price of u
def set_quantity(self, n, l, u):
for i in range(n):
p = rnd.uniform(l, u)
self.wta.append(p)

def get_name(self):
return self.name

return self.wta

def clear_wta(self):
self.wta = []

def __init__(self, name):
self.name = name
self.wtp = []
self.step = 0

# the supplier has n quantities that they can buy
# they may be willing to sell this quantity anywhere from a lower price of l
# to a higher price of u
def set_quantity(self, n, l, u):
n = int(self.consumption(self.step))
for i in range(n):
p = rnd.uniform(l, u)
self.wtp.append(p)

def get_name(self):
return self.name

# return list of willingness to pay
def get_bids(self):
return self.wtp

def clear_wtp(self):
self.wtp = []

def consumption(self, x):
# make it initialise to seller
y = 603 + 3615 * (.5 * (1 + np.cos((x/6)*np.pi)))
return(y)

class Book():
def __init__(self):
self.ledger = pd.DataFrame(columns = ("role","name","price","cleared"))

# ask each seller their name
# ask each seller their willingness
# for each willingness append the data frame
for seller in seller_list:
seller_name = seller.get_name()
for price in seller_price:
self.ledger=self.ledger.append({"role":"seller","name":seller_name,"price":price,"cleared":"in process"},
ignore_index=True)

# ask each seller their name
# ask each seller their willingness
# for each willingness append the data frame
for price in buyer_price:
ignore_index=True)

def update_ledger(self,ledger):
self.ledger = ledger

def get_ledger(self):
return self.ledger

class Market():
def __init__(self):
self.count = 0
self.last_price = ''
self.book = Book()
self.b = []
self.s = []
self.ledger = ''

self.s.append(seller)

def set_book(self):
self.book.set_bids(self.b)

def get_ledger(self):
self.ledger = self.book.get_ledger()
return self.ledger

def get_bids(self):
# this is a data frame
ledger = self.book.get_ledger()
rows= ledger.loc[ledger['role'] == 'buyer']
# this is a series
prices=rows['price']
# this is a list
bids = prices.tolist()
return bids

# this is a data frame
ledger = self.book.get_ledger()
rows = ledger.loc[ledger['role'] == 'seller']
# this is a series
prices=rows['price']
# this is a list

# return the price at which the market clears
# this fails because there are more buyers then sellers

def get_clearing_price(self):
# buyer makes a bid starting with the buyer which wants it most
b = self.get_bids()
# highest to lowest
self.b=sorted(b, reverse=True)
# lowest to highest
self.s=sorted(s, reverse=False)

# find out whether there are more buyers or sellers
# then drop the excess buyers or sellers; they won't compete
n = len(b)
m = len(s)

# there are more sellers than buyers
# drop off the highest priced sellers
if (m > n):
s = s[0:n]
matcher = n
# There are more buyers than sellers
# drop off the lowest bidding buyers
else:
b = b[0:m]
matcher = m

# It's possible that not all items sold actually clear the market here
for i in range(matcher):
if (self.b[i] > self.s[i]):
self.count +=1
self.last_price = self.b[i]

return self.last_price

# TODO: Annotate the ledger
def annotate_ledger(self,clearing_price):
ledger = self.book.get_ledger()
for index, row in ledger.iterrows():
if (row['role'] == 'seller'):
if (row['price'] < clearing_price):
ledger.loc[index,'cleared'] = 'True'
else:
ledger.loc[index,'cleared'] = 'False'
else:
if (row['price'] > clearing_price):
ledger.loc[index,'cleared'] = 'True'
else:
ledger.loc[index,'cleared'] = 'False'

self.book.update_ledger(ledger)

def get_units_cleared(self):
return self.count

class Observer():
def __init__(self, period, runtime):
self.wta = []
self.god_info = period
self.hist_book = []
self.seller_dict = {}
self.timetick = 0
self.maxrun = runtime

for name in buyerinfo:

def set_seller(self, seller_info):
for name in seller_info:
self.seller_dict[name] = Seller('%s' % name)

def update_seller(self, seller_info):
for i in self.seller_dict:
self.seller_dict[i].clear_wta()
self.seller_dict[i].set_quantity(*seller_info[i])
#supplier.clear_wta()
#supplier.set_quantity(*seller_info)

for i in self.buyer_dict:

def run_it(self):
self.timetick += 1
first_run = True
for period in range(self.maxrun):
print('#######################################')
print(god_info[period][0])
# time the period
startit_period = time.time()

# time initialising
startit_init = time.time()

# old book

# convert the array to a dictionary to create objects
buyerinfo = dict([(k, v) for k, v in zip([god_info[period][1][i][0] for i in range(len(god_info[period][1]))],
[god_info[period][1][i][1] for i in range(len(god_info[period][1]))])])
seller_info = {'natural gas' : (2000, 0, 10)}
# make a buyer and get the bids
#for name in buyerinfo:
if first_run:
self.set_seller(seller_info)
first_run=False

#for i in self.buyer_dict:
self.update_seller(seller_info)

#create a book and submit bids
book = Book()
book.set_asks([sell for sell in self.seller_dict.values()])
ledger = book.get_ledger()

# create a market
gas_market = Market()
#add suplliers and buyers to this market
for supplier in self.seller_dict.values():
gas_market.set_book()
# time init stop
stopit_init = time.time() - startit_init
print('%s : init' % stopit_init)

# start clearing
startit_clearing = time.time()
clearing = gas_market.get_clearing_price()
gas_market.annotate_ledger(clearing)
new_ledger = gas_market.get_ledger()

# write time and save result
stopit_clearing = time.time() - startit_clearing
print('%s : clearing' % stopit_clearing)
# since this operation can take quite a while, print after every operation
period_time = time.time() - startit_period
print('%s : period time' % period_time)
self.hist_book.append([god_info[period][0], clearing])
f = open('hist_book.csv', 'a')
for item in self.hist_book:
f.write('%s,%s\n' % (item[0], item[1]))
f.close()

``````

## Example Market

In the following code example we use the buyer and supplier objects to create a market. At the market a single price is announced which causes as many units of goods to be swapped as possible. The buyers and sellers stop trading when it is no longer in their own interest to continue.

``````

In [3]:

#record every run in a history book
hist_book = []

# is dictionary really suited? Sorted vs unsorted
# god_info is the the input for the model
# This is a example
'''
god_info = {
'jan': {'home': (100, 0, 10), 'industry': (50, 0, 10), 'cat': (75, 0, 10)},
'feb': {'home': (90, 0, 10), 'industry': (40, 0, 10), 'cat': (60, 0, 10)},
'march': {'home': (100, 0, 10), 'industry': (50, 0, 10), 'cat': (75, 0, 10)}
}
#'''

#read montly consumption data of 2010 into a dataframe
df = df.transpose()

#make an array of for the buyers from the csv in the following format
#buyerinfo = [time ['home', (100, 0, 10)], ['industry', (50, 0, 10)], ['cat', (75, 0, 10)]]
god_info = [[x, [['elec',(y,0,10)],['indu',(z,0,10)],['home',(u,0,10)]]]
for x,y,z,u in zip(df.index,df['elec'],df['indu'],df['home'])]

#plot the 2010 monthly consumption data
df.plot();
df

``````
``````

Out[3]:

.dataframe thead tr:only-child th {
text-align: right;
}

.dataframe thead th {
text-align: left;
}

.dataframe tbody tr th {
vertical-align: top;
}

elec
indu
home

jan
1066
1572
4218

feb
941
1349
3490

mar
965
1416
2636

apr
841
1215
1614

may
742
1285
1458

jun
673
1171
763

jul
698
1229
603

aug
668
1169
709

sep
729
1207
1042

okt
983
1362
1742

nov
944
1371
2632

dec
994
1505
4301

``````
``````

In [4]:

# create observer and run the model
obser1 = Observer(god_info,12)
obser1.run_it()
#get the info from the observer
hist_book = obser1.hist_book

``````
``````

#######################################
jan
83.47528123855591 : init
7.320150852203369 : clearing
90.79643273353577 : period time
#######################################
feb
69.21287369728088 : init
6.126307964324951 : clearing
75.33918166160583 : period time
#######################################
mar
52.85672879219055 : init
4.403108596801758 : clearing
57.26183867454529 : period time
#######################################
apr
34.796578884124756 : init
2.9000465869903564 : clearing
37.69662547111511 : period time
#######################################
may
23.426520347595215 : init
1.9133505821228027 : clearing
25.34087109565735 : period time
#######################################
jun
19.48577117919922 : init
1.5851023197174072 : clearing
21.070873498916626 : period time
#######################################
jul
23.459576845169067 : init
1.9213557243347168 : clearing
25.3829345703125 : period time
#######################################
aug
34.94666790962219 : init
2.9070355892181396 : clearing
37.85370349884033 : period time
#######################################
sep
51.438326835632324 : init
4.401105642318726 : clearing
55.83943247795105 : period time
#######################################
okt
69.0257077217102 : init
6.071285009384155 : clearing
75.09699273109436 : period time
#######################################
nov
82.33313512802124 : init
7.436232089996338 : clearing
89.76936721801758 : period time
#######################################
dec
87.83900380134583 : init
7.943623781204224 : clearing
95.78262758255005 : period time

``````
``````

In [5]:

df_hb = pd.DataFrame(hist_book)
df_hb = df_hb.set_index(0)
df_hb.index.name = 'month'
df_hb.rename(columns={1: 'price'}, inplace=True)

``````

## Operations Research Formulation

The market can also be formulated as a very simple linear program or linear complementarity problem. It is clearer and easier to implement this market clearing mechanism with agents. One merit of the agent-based approach is that we don't need linear or linearizeable supply and demand function.

The auctioneer is effectively following a very simple linear program subject to constraints on units sold. The auctioneer is, in the primal model, maximizing the consumer utility received by customers, with respect to the price being paid, subject to a fixed supply curve. On the dual side the auctioneer is minimizing the cost of production for the supplier, with respect to quantity sold, subject to a fixed demand curve. It is the presumed neutrality of the auctioneer which justifies the honest statement of supply and demand.

An alternative formulation is a linear complementarity problem. Here the presence of an optimal space of trades ensures that there is a Pareto optimal front of possible trades. The perfect opposition of interests in dividing the consumer and producer surplus means that this is a zero sum game. Furthermore the solution to this zero-sum game maximizes societal welfare and is therefore the Hicks optimal solution.

## Next Steps

A possible addition of this model would be to have a weekly varying demand of customers, for instance caused by the use of natural gas as a heating agent. This would require the bids and asks to be time varying, and for the market to be run over successive time periods. A second addition would be to create transport costs, or enable intermediate goods to be produced. This would need a more elaborate market operator. Another possible addition would be to add a profit maximizing broker. This may require adding belief, fictitious play, or message passing.

The object-orientation of the models will probably need to be further rationalized. Right now the market requires very particular ordering of calls to function correctly.

``````

In [6]:

# timeit

stopit = time.time()
dtstopit = datetime.datetime.now()

print('it took us %s seconds to get to this conclusion' % (stopit-startit))
print('in another notation (h:m:s) %s'% (dtstopit - dtstartit))
df_hb.plot()
plt.ylabel('€ / unit')

``````
``````

it took us 687.4940564632416 seconds to get to this conclusion
in another notation (h:m:s) 0:11:27.494057

Out[6]:

<matplotlib.text.Text at 0x1c7ed3d47f0>

``````
``````

In [7]:

# print the run results
df_hb

``````
``````

Out[7]:

.dataframe thead tr:only-child th {
text-align: right;
}

.dataframe thead th {
text-align: left;
}

.dataframe tbody tr th {
vertical-align: top;
}

price

month

jan
8.584402

feb
8.317209

mar
7.846904

apr
6.864332

may
5.506911

jun
4.779780

jul
5.599869

aug
6.900777

sep
7.832436

okt
8.292389

nov
8.544724

dec
8.658203

``````
``````

In [8]:

# print the time of last run
print('last run of this notebook:')
time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime())

``````
``````

last run of this notebook:

Out[8]:

'Sat, 24 Jun 2017 17:58:53'

``````
``````

In [9]:

# demand aproximation of the home owners
x = np.arange(12)
y = 603 + 3615*(.5 * (1 + np.cos((x/6)*np.pi)))
plt.plot(x,y)

``````
``````

Out[9]:

[<matplotlib.lines.Line2D at 0x1c7ed88d518>]

``````
``````

In [10]:

def consumption(x):
y = 603 + 3615*(.5 * (1 + np.cos((x/6)*np.pi)))
return(y)

print([consumption(i) for i in np.arange(12)])

``````
``````

[4218.0, 3975.8409173403734, 3314.25, 2410.5, 1506.7500000000005, 845.15908265962707, 603.0, 845.1590826596273, 1506.7499999999991, 2410.4999999999995, 3314.25, 3975.8409173403725]

``````
``````

In [ ]:

``````