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__') |