View difference between Paste ID: eQQLdi0a and 51N98yzu
SHOW: | | - or go back to the newest paste.
1
# Trying to simulate a simple economy, based on Doran & Parberry (2010)
2
3
from __future__ import division
4
import random
5
import cProfile
6
import pstats
7
8
break_chances = {'tools':5, 'wood':10}
9
10
HIST_WINDOW_SIZE = 10 # Amount of trades auctions keep in memory to determine avg price (used by agents)
11
MAX_INVENTORY_SIZE = 15 # A not-really-enforced max inventory limit
12
13
PROFIT_MARGIN = 1.5 # Won't sell an item for lower than this * normalized production cost
14
STARTING_GOLD = 1000 # How much gold each agent starts with
15
TAXES = 2 # How much gold is payed in taxes per turn
16
17
MIN_CERTAINTY_VALUE = 8 ## Within what range traders are limited to estimating the market price
18
BID_REJECTED_ADJUSTMENT = 5 # Adjust prices by this much when our bid is rejected
19
ASK_REJECTED_ADJUSTMENT = 5 # Adjust prices by this much when nobody buys our stuff
20
REJECTED_UNCERTAINTY_AMOUNT = 2 # We get this much more uncertain about a price when an offer is rejected
21
ACCEPTED_CERTAINTY_AMOUNT = 1  # Out uncertainty about a price decreases by this amount when an offer is accepted
22
23
P_DIF_ADJ = 3  # When offer is accepted and exceeds what we thought the price value was, adjust it upward by this amount
24
N_DIF_ADJ = 3  # When offer is accepted and is lower than what we thought the price value was, adjust it downward by this amount
25
P_DIF_THRESH = 2  #Threshhold at which adjustment of P_DIF_ADJ is added to the perceived value of a commodity
26
N_DIF_THRESH = 1.6 #Threshhold at which adjustment of P_DIF_ADJ is subtracted from the perceived value of a commodity
27
28
START_VAL = 50 # Arbitrary value that sets the starting price of good (in gold) 
29
START_UNCERT = 10 # Arbitrary uncertainty value (agent believes price is somewhere between (mean-this, mean+this)
30
31
def roll(a, b):
32
	return random.randint(a, b)
33
	
34
class Value:
35
	# Agents' perceived values of objects
36
	def __init__(self, center, uncertainty):
37
		self.center = center
38
		self.uncertainty = uncertainty
39
		
40
class Auction:
41
	# Seperate "auction" for each commodity
42
	# Runs each round of bidding, as well as archives historical price info
43
	def __init__(self, commodity):
44
		self.commodity = commodity
45
		self.bids = []
46
		self.asks = []
47
		self.price_history = []
48
		self.mean_price = START_VAL
49
	
50
	def update_mean_price(self):
51
		# update the mean price for this commodity by averaging over the last HIST_WINDOW_SIZE items
52
		self.mean_price = int(round(sum(self.price_history[-HIST_WINDOW_SIZE:])/len(self.price_history[-HIST_WINDOW_SIZE:])))
53
		
54
class Offer:
55
	# An offer that goes into the auction's "bids" or "asks".
56
	# Bids and asks are then compared against each other and agents meet at the middle
57
	def __init__(self, owner, commodity, price, quantity):
58
		self.owner = owner
59
		self.commodity = commodity
60
		self.price = price
61
		self.quantity = quantity
62
		
63
		
64
class Agent:
65
	# An agent in the economy. They produce items, make bids and sell requests, and pay taxes
66
	def __init__(self, name, output, required_items, used_items, consumed_items):
67
		global num_agents
68
		# Add self to list of agents
69
		agents.append(self)
70
		
71
		self.name = name
72
		self.output = output #The product this agent produces (dict of items:amounts)
73
		self.required_items = required_items #Things you need, but don't use up in item creation (dict of items:amounts)
74
		self.used_items = used_items #Things you transform into the product (dict of items:amounts)
75
		self.consumed_items = consumed_items #Things you consume once per turn (food, etc)
76
		
77
		self.inventory_size = MAX_INVENTORY_SIZE
78
		self.gold = STARTING_GOLD
79
		
80
		## Create a dict of all things we use. We won't sell anything below the price to produce it, hopefully
81
		self.inventory = []
82
		self.perceived_values = {} ## dict of what we believe the true price of an item is
83
		
84
		for item, amount in self.required_items.iteritems():
85
			self.perceived_values[item] = Value(START_VAL, START_UNCERT)
86
			for num in range(amount):
87
				self.inventory.append(item)
88
				
89
		for item, amount in self.consumed_items.iteritems():
90
			self.perceived_values[item] = Value(START_VAL, START_UNCERT)
91
			for num in range(amount):
92
				self.inventory.append(item)
93
				
94
		for item, amount in self.used_items.iteritems():
95
			self.perceived_values[item] = Value(START_VAL, START_UNCERT)
96
			for num in range(amount):
97
				self.inventory.append(item)
98
				
99
		## Start off with 2x the stuff that you produce so you can begin selling right away
100
		self.num_produced_items = 0
101
		for item, amount in self.output.iteritems():
102
			self.perceived_values[item] = Value(START_VAL, START_UNCERT)
103
			
104
			self.num_produced_items += amount
105
			for num in range(amount*2):
106
				self.inventory.append(item)
107
			
108
	def produce_items(self):
109
		# Verbose way to test if we have the correct items
110
		can_produce = True
111
		for item, amount in self.required_items.iteritems():
112
			if self.inventory.count(item) < amount:
113
				can_produce = False
114
				break
115
		if len(self.used_items) > 0:
116
			for item, amount in self.used_items.iteritems():
117
				if self.inventory.count(item) < amount:
118
					can_produce = False
119
					break
120
		# For items like food that are consumed on the side
121
		if len(self.consumed_items) > 0:
122
			for item, amount in self.consumed_items.iteritems():
123
				if self.inventory.count(item) < amount:
124
					can_produce = False
125
					break
126
		
127
		## If we can produce stuff, then actually do it
128
		if can_produce:
129
			# Remove the used_items in the inv.
130
			for item, amount in self.used_items.iteritems():
131
				for x in xrange(amount):
132
					self.inventory.remove(item)
133
			# Produce stuff
134
			for item, amount in self.output.iteritems():
135
				for x in xrange(amount):
136
					self.inventory.append(item)
137
			# Stuff can break:
138
			for item, amount in self.required_items.iteritems():
139
				for x in xrange(amount):
140
					if roll(1, break_chances[item]) == 1:
141
						self.inventory.remove(item)
142
			# Consume stuff
143
			for item, amount in self.consumed_items.iteritems():
144
				for x in xrange(amount):
145
					self.inventory.remove(item)
146
		
147
			
148
	def has_space(self, amount):
149
		return (self.inventory_size - len(self.inventory)) > amount
150
			
151
	def determine_purchase_quantity(self, commodity):
152
		# First find historical mean price and our observed mean prices
153
		historical_mean = auctions[commodity].mean_price
154
		our_belief = self.perceived_values[commodity].center
155
		# Pseudocode from Doran & Parberry (2010):
156
		#favorability = max price - position of mean within observed trading range
157
		
158
		# I guess this should be a float, >1 means good, <1 means bad. Will adjust requested amt accordingly
159
		favorability = our_belief/historical_mean
160
		
161
		#Force them to bid at least 1, for now
162
		available_space = self.inventory_size - len(self.inventory)
163
		return max(int(round(favorability * available_space)), 1)
164
	
165
	
166
	def determine_sale_quantity(self, commodity):
167
		# First find historical mean price and our observed mean prices
168
		historical_mean = auctions[commodity].mean_price
169
		our_belief = self.perceived_values[commodity].center
170
		
171
		# Pseudocode from Doran & Parberry (2010):
172
		#favorability = max price - position of mean within observed trading range
173
		
174
		# I guess this should be a float, >1 means good, <1 means bad. Will adjust requested amt accordingly
175
		favorability = historical_mean/our_belief
176
		#Force them to sell at least 1, for now
177
		return max(int(round(favorability * (self.inventory.count(commodity)))), 1)
178
	
179
	
180
	def has(self, commodity, amount=1):
181
		return self.inventory.count(commodity) >= amount
182
		
183
	def eval_need(self):
184
		# See if we need items to continue producing our goods
185
		for item, amount in self.used_items.iteritems():
186
			if not self.has(item, amount):
187
				self.create_bid(self, item, amount)
188
	
189
		for item, amount in self.required_items.iteritems():
190
			if not self.has(item, amount):
191
				self.create_bid(self, item, amount)
192
193
		for item, amount in self.consumed_items.iteritems():
194
			if not self.has(item, amount):
195
				self.create_bid(self, item, amount)
196
				
197
				
198
	def eval_sell(self):
199
		# If we have stuff we've made, offer to sell it
200
		for item, amount in self.output.iteritems():
201
			if self.inventory.count(item) > 0:
202
				self.create_ask(self, item, self.inventory.count(item))
203
	
204
	def create_bid(self, agent, commodity, limit):
205
		# If we decide to bid, here's how we do it
206
		est_price = self.perceived_values[commodity].center
207
		uncertainty = self.perceived_values[commodity].uncertainty
208
		bid_price = roll(est_price - uncertainty, est_price + uncertainty)
209
		if bid_price > self.gold:
210
			bid_price = self.gold
211
			
212
		# Find ideal amount - should we pass bid price to this and compare that to hist. mean?
213
		ideal_quantity = self.determine_sale_quantity(commodity)
214
		# Determine quantity
215
		quantity_to_buy = min(ideal_quantity, limit)
216
		
217
		if quantity_to_buy > 0:
218
			auctions[commodity].bids.append(Offer(owner=agent, commodity=commodity, price=bid_price, quantity=quantity_to_buy))
219
		
220
	
221
	def check_production_cost(self):
222
		### Might change this to reflect agent's own beliefs about price, rather than historical avgs ###
223
		production_cost = 0
224
		
225
		# Cost of used_items is historical mean * the amount we use (since they are used up every 
226
		# time we need to make something, we're using the full value of the items)
227
		for item, amount in self.used_items.iteritems():
228
			production_cost += (auctions[item].mean_price * amount)
229
			
230
		# These items are required for production, but not used. Each one has a break chance, so 
231
		# the cost of buying one divided by the break chance is the avg cost to buy
232
		for item, amount in self.required_items.iteritems():
233
			production_cost += int(round(((auctions[item].mean_price * amount)/break_chances[item])))
234
			
235
		# These items are also used each time we make something, so the full cost of the items are used
236
		for item, amount in self.consumed_items.iteritems():
237
			production_cost += auctions[item].mean_price * amount
238
			
239
		# Take into account the taxes we pay
240
		production_cost += TAXES
241
		
242
		return production_cost
243
		
244
	def create_ask(self, agent, commodity, limit):	
245
		# Determines how many items to sell, and at what cost
246
		production_cost = self.check_production_cost()
247
		min_sale_price = int(round((production_cost/self.num_produced_items)*PROFIT_MARGIN))
248
		
249
		est_price = self.perceived_values[commodity].center
250
		uncertainty = self.perceived_values[commodity].uncertainty
251
		# won't go below what they paid for it	
252
		ask_price = max(roll(est_price - uncertainty, est_price + uncertainty), min_sale_price)
253
		
254
		ideal_quantity = self.determine_purchase_quantity(commodity)
255
		# Paper says should be max(ideal, limit), but that creates an offer bigger than what we have in inventory?
256
		quantity_to_sell = min(ideal_quantity, limit)
257
		
258
		if quantity_to_sell > 0:
259
			auctions[commodity].asks.append(Offer(owner=agent, commodity=commodity, price=ask_price, quantity=quantity_to_sell))
260
261
			
262
	def eval_trade_accepted(self, commodity, price):	
263
		# Then, adjust our belief in the price
264
		if self.perceived_values[commodity].uncertainty >= MIN_CERTAINTY_VALUE:
265
			self.perceived_values[commodity].uncertainty -= ACCEPTED_CERTAINTY_AMOUNT
266
		
267
		our_mean = (self.perceived_values[commodity].center)
268
		
269
		if price > our_mean * P_DIF_THRESH:
270
			self.perceived_values[commodity].center += P_DIF_ADJ
271
		elif price < our_mean * N_DIF_THRESH:
272
			# We never let it's worth drop under a certain % of tax money.
273
			self.perceived_values[commodity].center = max(self.perceived_values[commodity].center - N_DIF_ADJ, TAXES*5 + self.perceived_values[commodity].uncertainty)
274
275
276
	def eval_ask_rejected(self, commodity, price):
277
		# What to do when we put something up for sale and nobody bought it
278
		self.perceived_values[commodity].center -= ASK_REJECTED_ADJUSTMENT
279
		self.perceived_values[commodity].uncertainty += REJECTED_UNCERTAINTY_AMOUNT
280
			
281
	def eval_bid_rejected(self, commodity, price):
282
		# What to do when we've bid on something and didn't get it
283
		self.perceived_values[commodity].center += BID_REJECTED_ADJUSTMENT
284
		self.perceived_values[commodity].uncertainty += REJECTED_UNCERTAINTY_AMOUNT
285
286
	
287
def add_new_agent():
288
	# Create a new agent
289
	num = roll(1, 5)
290
	
291
	if num == 1: farmer = Agent(name='farmer', output={'food':4}, required_items={'wood':1, 'tools':1}, used_items={}, consumed_items={})
292
	elif num == 2: miner = Agent(name='miner', output={'ore':1}, required_items={'tools':1}, used_items={}, consumed_items={'food':1})
293
	elif num == 3: refiner = Agent(name='refiner', output={'metal':1}, required_items={'tools':1}, used_items={'ore':1}, consumed_items={'food':1})
294
	elif num == 4: woodcutter = Agent(name='woodcutter', output={'wood':2}, required_items={'tools':1}, used_items={}, consumed_items={'food':1})
295
	elif num == 5: blacksmith = Agent(name='blacksmith', output={'tools':2}, required_items={}, used_items={'metal':1, 'wood':1}, consumed_items={'food':1})
296
	
297
298
def create_agents(num_agents):
299
	global agents
300
	## Create some agents ##
301
	agents = []
302
	for iteration in range(num_agents):
303-
		farmer = Agent(name='farmer', output={'food':4}, required_items={'wood':1, 'tools':1}, used_items={}, 
303+
		farmer = Agent(name='farmer', output={'food':4}, required_items={'wood':1, 'tools':1}, used_items={}, consumed_items={})
304-
					consumed_items={})
304+
		miner = Agent(name='miner', output={'ore':4}, required_items={'tools':1}, used_items={}, consumed_items={'food':1})
305-
		miner = Agent(name='miner', output={'ore':4}, required_items={'tools':1}, used_items={}, 
305+
		refiner = Agent(name='refiner', output={'metal':1}, required_items={'tools':1}, used_items={'ore':1}, consumed_items={'food':1})
306-
					consumed_items={'food':1})
306+
		woodcutter = Agent(name='woodcutter', output={'wood':2}, required_items={'tools':1}, used_items={}, consumed_items={'food':1})
307-
		refiner = Agent(name='refiner', output={'metal':1}, required_items={'tools':1}, used_items={'ore':1}, 
307+
		blacksmith = Agent(name='blacksmith', output={'tools':1}, required_items={}, used_items={'metal':1, 'wood':1}, consumed_items={'food':1})
308-
					consumed_items={'food':1})
308+
309-
		woodcutter = Agent(name='woodcutter', output={'wood':2}, required_items={'tools':1}, used_items={}, 
309+
310-
					consumed_items={'food':1})
310+
311-
		blacksmith = Agent(name='blacksmith', output={'tools':1}, required_items={}, used_items={'metal':1, 'wood':1}, 
311+
312-
					consumed_items={'food':1})
312+
313
		prices = {'wood':[], 'food':[], 'ore':[], 'metal':[], 'tools':[]}
314
		# First, each agent does what they do
315
		for agent in reversed(agents):
316
			agent.produce_items()
317
			agent.eval_sell()
318
			agent.gold -= TAXES
319
			if agent.gold > 0:
320
				agent.eval_need()
321
			else:
322
				agents.remove(agent)
323
				#print('Removing a ' + agent.name)
324
				add_new_agent()
325
				
326
		## Run the auction
327
		for commodity, auction in auctions.iteritems():
328
			# Remove any bias? Not sure how much this does, if we're sorting them by price anyway
329
			random.shuffle(auction.bids)
330
			random.shuffle(auction.asks)
331
			## Sort the bids by price (highest to lowest) ##
332
			auction.bids = sorted(auction.bids, key=lambda attr: attr.price, reverse=True)
333
			## Sort the asks by price (lowest to hghest) ##
334
			auction.asks = sorted(auction.asks, key=lambda attr: attr.price)
335
336
			num_bids = len(auction.bids)
337
			num_asks = len(auction.asks)
338
			
339
			while not len(auction.bids) == 0 and not len(auction.asks) == 0:
340
				buyer = auction.bids[0]
341
				seller = auction.asks[0]
342
				# Determine price/amount
343
				quantity = min(buyer.quantity, seller.quantity)
344
				
345
				price = int(round((buyer.price + seller.price)/2))
346
				
347
				if quantity > 0:
348
					# Adjust buyer/seller requested amounts
349
					buyer.quantity -= quantity
350
					seller.quantity -= quantity
351
					
352
					buyer.owner.eval_trade_accepted(buyer.commodity, price)
353
					seller.owner.eval_trade_accepted(buyer.commodity, price)
354
					
355
					## Update inventories and gold counts
356
					for i in range(quantity):
357
						buyer.owner.inventory.append(buyer.commodity)
358
						seller.owner.inventory.remove(seller.commodity)
359
					
360
					buyer.owner.gold -= (price*quantity)
361
					seller.owner.gold += (price*quantity)
362
					
363
				# Add to running tally of prices this turn
364
				prices[commodity].append(price)
365
				
366
				# Now that a transaction has occured, bump out the buyer or seller if either is satasfied
367
				if seller.quantity == 0: del auction.asks[0]
368
				if buyer.quantity == 0: del auction.bids[0]
369
					
370
			# All bidders re-evaluate prices - currently too simplistic
371
			if len(auction.bids) > 0:
372
				for buyer in auction.bids:
373
					buyer.owner.eval_bid_rejected(buyer.commodity, None)
374
					auctions[commodity].bids = []
375
			
376
			# All sellers re-evaluate prices - too simplistic as well
377
			elif len(auction.asks) > 0:
378
				for seller in auction.asks:
379
					seller.owner.eval_bid_rejected(seller.commodity, None)	
380
					auctions[commodity].asks = []
381
					
382
			## Average prices		
383
			if len(prices[commodity]) > 0:
384
				price_mean = int(round(sum(prices[commodity])/len(prices[commodity])))
385
				auction.price_history.append(price_mean)
386
				# Track mean price for last N turns
387
				auction.update_mean_price()
388
				#print (auction.commodity + ': ' + str(auction.mean_price) + '. This round: ' + 
389
				#str(len(prices[commodity])) + ' ' + commodity + ' averaged at $' + str(price_mean) +
390
				#' (' + str(num_bids) + ' bids, ' + str(num_asks) + ' asks)')
391
			else:
392
				auction.price_history.append(auction.price_history[-1])
393
				#print (commodity + ' was not sold this round (' + str(num_bids) + ' bids, ' + str(num_asks) + ' asks)')
394
395
396
auctions = {'food':Auction('food'), 'wood':Auction('wood'), 'ore':Auction('ore'), 'metal':Auction('metal'), 'tools':Auction('tools')}
397
398
399
create_agents(20)
400
#cProfile.run('run_simulation(1)', 'economy')
401
run_simulation(200)
402
				
403
for auction in auctions.itervalues():
404
	print auction.commodity, auction.price_history
405
406
agent_nums = {'farmer':0, 'woodcutter':0, 'miner':0, 'blacksmith':0, 'refiner':0}
407
for agent in agents:
408
	agent_nums[agent.name] += 1
409
	#print(agent.name + ': ' + str(agent.gold))
410
	
411
print str(len(agents)), agent_nums
412
413
#p = pstats.Stats('economy')
414
#p.sort_stats('cumulative').print_stats(37)
415
#p.sort_stats('time').print_stats(10)
416
#p.sort_stats('file').print_stats('__init__')