Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- # Trying to simulate a simple economy, based on Doran & Parberry (2010)
- from __future__ import division
- import random
- import cProfile
- import pstats
- break_chances = {'tools':5, 'wood':10}
- HIST_WINDOW_SIZE = 10 # Amount of trades auctions keep in memory to determine avg price (used by agents)
- MAX_INVENTORY_SIZE = 15 # A not-really-enforced max inventory limit
- PROFIT_MARGIN = 1.5 # Won't sell an item for lower than this * normalized production cost
- STARTING_GOLD = 1000 # How much gold each agent starts with
- TAXES = 2 # How much gold is payed in taxes per turn
- MIN_CERTAINTY_VALUE = 8 ## Within what range traders are limited to estimating the market price
- BID_REJECTED_ADJUSTMENT = 5 # Adjust prices by this much when our bid is rejected
- ASK_REJECTED_ADJUSTMENT = 5 # Adjust prices by this much when nobody buys our stuff
- REJECTED_UNCERTAINTY_AMOUNT = 2 # We get this much more uncertain about a price when an offer is rejected
- ACCEPTED_CERTAINTY_AMOUNT = 1 # Out uncertainty about a price decreases by this amount when an offer is accepted
- P_DIF_ADJ = 3 # When offer is accepted and exceeds what we thought the price value was, adjust it upward by this amount
- 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
- P_DIF_THRESH = 2 #Threshhold at which adjustment of P_DIF_ADJ is added to the perceived value of a commodity
- N_DIF_THRESH = 1.6 #Threshhold at which adjustment of P_DIF_ADJ is subtracted from the perceived value of a commodity
- START_VAL = 50 # Arbitrary value that sets the starting price of good (in gold)
- START_UNCERT = 10 # Arbitrary uncertainty value (agent believes price is somewhere between (mean-this, mean+this)
- def roll(a, b):
- return random.randint(a, b)
- class Value:
- # Agents' perceived values of objects
- def __init__(self, center, uncertainty):
- self.center = center
- self.uncertainty = uncertainty
- class Auction:
- # Seperate "auction" for each commodity
- # Runs each round of bidding, as well as archives historical price info
- def __init__(self, commodity):
- self.commodity = commodity
- self.bids = []
- self.asks = []
- self.price_history = []
- self.mean_price = START_VAL
- def update_mean_price(self):
- # update the mean price for this commodity by averaging over the last HIST_WINDOW_SIZE items
- self.mean_price = int(round(sum(self.price_history[-HIST_WINDOW_SIZE:])/len(self.price_history[-HIST_WINDOW_SIZE:])))
- class Offer:
- # An offer that goes into the auction's "bids" or "asks".
- # Bids and asks are then compared against each other and agents meet at the middle
- def __init__(self, owner, commodity, price, quantity):
- self.owner = owner
- self.commodity = commodity
- self.price = price
- self.quantity = quantity
- class Agent:
- # An agent in the economy. They produce items, make bids and sell requests, and pay taxes
- def __init__(self, name, output, required_items, used_items, consumed_items):
- global num_agents
- # Add self to list of agents
- agents.append(self)
- self.name = name
- self.output = output #The product this agent produces (dict of items:amounts)
- self.required_items = required_items #Things you need, but don't use up in item creation (dict of items:amounts)
- self.used_items = used_items #Things you transform into the product (dict of items:amounts)
- self.consumed_items = consumed_items #Things you consume once per turn (food, etc)
- self.inventory_size = MAX_INVENTORY_SIZE
- self.gold = STARTING_GOLD
- ## Create a dict of all things we use. We won't sell anything below the price to produce it, hopefully
- self.inventory = []
- self.perceived_values = {} ## dict of what we believe the true price of an item is
- for item, amount in self.required_items.iteritems():
- self.perceived_values[item] = Value(START_VAL, START_UNCERT)
- for num in range(amount):
- self.inventory.append(item)
- for item, amount in self.consumed_items.iteritems():
- self.perceived_values[item] = Value(START_VAL, START_UNCERT)
- for num in range(amount):
- self.inventory.append(item)
- for item, amount in self.used_items.iteritems():
- self.perceived_values[item] = Value(START_VAL, START_UNCERT)
- for num in range(amount):
- self.inventory.append(item)
- ## Start off with 2x the stuff that you produce so you can begin selling right away
- self.num_produced_items = 0
- for item, amount in self.output.iteritems():
- self.perceived_values[item] = Value(START_VAL, START_UNCERT)
- self.num_produced_items += amount
- for num in range(amount*2):
- self.inventory.append(item)
- def produce_items(self):
- # Verbose way to test if we have the correct items
- can_produce = True
- for item, amount in self.required_items.iteritems():
- if self.inventory.count(item) < amount:
- can_produce = False
- break
- if len(self.used_items) > 0:
- for item, amount in self.used_items.iteritems():
- if self.inventory.count(item) < amount:
- can_produce = False
- break
- # For items like food that are consumed on the side
- if len(self.consumed_items) > 0:
- for item, amount in self.consumed_items.iteritems():
- if self.inventory.count(item) < amount:
- can_produce = False
- break
- ## If we can produce stuff, then actually do it
- if can_produce:
- # Remove the used_items in the inv.
- for item, amount in self.used_items.iteritems():
- for x in xrange(amount):
- self.inventory.remove(item)
- # Produce stuff
- for item, amount in self.output.iteritems():
- for x in xrange(amount):
- self.inventory.append(item)
- # Stuff can break:
- for item, amount in self.required_items.iteritems():
- for x in xrange(amount):
- if roll(1, break_chances[item]) == 1:
- self.inventory.remove(item)
- # Consume stuff
- for item, amount in self.consumed_items.iteritems():
- for x in xrange(amount):
- self.inventory.remove(item)
- def has_space(self, amount):
- return (self.inventory_size - len(self.inventory)) > amount
- def determine_purchase_quantity(self, commodity):
- # First find historical mean price and our observed mean prices
- historical_mean = auctions[commodity].mean_price
- our_belief = self.perceived_values[commodity].center
- # Pseudocode from Doran & Parberry (2010):
- #favorability = max price - position of mean within observed trading range
- # I guess this should be a float, >1 means good, <1 means bad. Will adjust requested amt accordingly
- favorability = our_belief/historical_mean
- #Force them to bid at least 1, for now
- available_space = self.inventory_size - len(self.inventory)
- return max(int(round(favorability * available_space)), 1)
- def determine_sale_quantity(self, commodity):
- # First find historical mean price and our observed mean prices
- historical_mean = auctions[commodity].mean_price
- our_belief = self.perceived_values[commodity].center
- # Pseudocode from Doran & Parberry (2010):
- #favorability = max price - position of mean within observed trading range
- # I guess this should be a float, >1 means good, <1 means bad. Will adjust requested amt accordingly
- favorability = historical_mean/our_belief
- #Force them to sell at least 1, for now
- return max(int(round(favorability * (self.inventory.count(commodity)))), 1)
- def has(self, commodity, amount=1):
- return self.inventory.count(commodity) >= amount
- def eval_need(self):
- # See if we need items to continue producing our goods
- for item, amount in self.used_items.iteritems():
- if not self.has(item, amount):
- self.create_bid(self, item, amount)
- for item, amount in self.required_items.iteritems():
- if not self.has(item, amount):
- self.create_bid(self, item, amount)
- for item, amount in self.consumed_items.iteritems():
- if not self.has(item, amount):
- self.create_bid(self, item, amount)
- def eval_sell(self):
- # If we have stuff we've made, offer to sell it
- for item, amount in self.output.iteritems():
- if self.inventory.count(item) > 0:
- self.create_ask(self, item, self.inventory.count(item))
- def create_bid(self, agent, commodity, limit):
- # If we decide to bid, here's how we do it
- est_price = self.perceived_values[commodity].center
- uncertainty = self.perceived_values[commodity].uncertainty
- bid_price = roll(est_price - uncertainty, est_price + uncertainty)
- if bid_price > self.gold:
- bid_price = self.gold
- # Find ideal amount - should we pass bid price to this and compare that to hist. mean?
- ideal_quantity = self.determine_sale_quantity(commodity)
- # Determine quantity
- quantity_to_buy = min(ideal_quantity, limit)
- if quantity_to_buy > 0:
- auctions[commodity].bids.append(Offer(owner=agent, commodity=commodity, price=bid_price, quantity=quantity_to_buy))
- def check_production_cost(self):
- ### Might change this to reflect agent's own beliefs about price, rather than historical avgs ###
- production_cost = 0
- # Cost of used_items is historical mean * the amount we use (since they are used up every
- # time we need to make something, we're using the full value of the items)
- for item, amount in self.used_items.iteritems():
- production_cost += (auctions[item].mean_price * amount)
- # These items are required for production, but not used. Each one has a break chance, so
- # the cost of buying one divided by the break chance is the avg cost to buy
- for item, amount in self.required_items.iteritems():
- production_cost += int(round(((auctions[item].mean_price * amount)/break_chances[item])))
- # These items are also used each time we make something, so the full cost of the items are used
- for item, amount in self.consumed_items.iteritems():
- production_cost += auctions[item].mean_price * amount
- # Take into account the taxes we pay
- production_cost += TAXES
- return production_cost
- def create_ask(self, agent, commodity, limit):
- # Determines how many items to sell, and at what cost
- production_cost = self.check_production_cost()
- min_sale_price = int(round((production_cost/self.num_produced_items)*PROFIT_MARGIN))
- est_price = self.perceived_values[commodity].center
- uncertainty = self.perceived_values[commodity].uncertainty
- # won't go below what they paid for it
- ask_price = max(roll(est_price - uncertainty, est_price + uncertainty), min_sale_price)
- ideal_quantity = self.determine_purchase_quantity(commodity)
- # Paper says should be max(ideal, limit), but that creates an offer bigger than what we have in inventory?
- quantity_to_sell = min(ideal_quantity, limit)
- if quantity_to_sell > 0:
- auctions[commodity].asks.append(Offer(owner=agent, commodity=commodity, price=ask_price, quantity=quantity_to_sell))
- def eval_trade_accepted(self, commodity, price):
- # Then, adjust our belief in the price
- if self.perceived_values[commodity].uncertainty >= MIN_CERTAINTY_VALUE:
- self.perceived_values[commodity].uncertainty -= ACCEPTED_CERTAINTY_AMOUNT
- our_mean = (self.perceived_values[commodity].center)
- if price > our_mean * P_DIF_THRESH:
- self.perceived_values[commodity].center += P_DIF_ADJ
- elif price < our_mean * N_DIF_THRESH:
- # We never let it's worth drop under a certain % of tax money.
- self.perceived_values[commodity].center = max(self.perceived_values[commodity].center - N_DIF_ADJ, TAXES*5 + self.perceived_values[commodity].uncertainty)
- def eval_ask_rejected(self, commodity, price):
- # What to do when we put something up for sale and nobody bought it
- self.perceived_values[commodity].center -= ASK_REJECTED_ADJUSTMENT
- self.perceived_values[commodity].uncertainty += REJECTED_UNCERTAINTY_AMOUNT
- def eval_bid_rejected(self, commodity, price):
- # What to do when we've bid on something and didn't get it
- self.perceived_values[commodity].center += BID_REJECTED_ADJUSTMENT
- self.perceived_values[commodity].uncertainty += REJECTED_UNCERTAINTY_AMOUNT
- def add_new_agent():
- # Create a new agent
- num = roll(1, 5)
- if num == 1: farmer = Agent(name='farmer', output={'food':4}, required_items={'wood':1, 'tools':1}, used_items={}, consumed_items={})
- elif num == 2: miner = Agent(name='miner', output={'ore':1}, required_items={'tools':1}, used_items={}, consumed_items={'food':1})
- elif num == 3: refiner = Agent(name='refiner', output={'metal':1}, required_items={'tools':1}, used_items={'ore':1}, consumed_items={'food':1})
- elif num == 4: woodcutter = Agent(name='woodcutter', output={'wood':2}, required_items={'tools':1}, used_items={}, consumed_items={'food':1})
- elif num == 5: blacksmith = Agent(name='blacksmith', output={'tools':2}, required_items={}, used_items={'metal':1, 'wood':1}, consumed_items={'food':1})
- def create_agents(num_agents):
- global agents
- ## Create some agents ##
- agents = []
- for iteration in range(num_agents):
- farmer = Agent(name='farmer', output={'food':4}, required_items={'wood':1, 'tools':1}, used_items={}, consumed_items={})
- miner = Agent(name='miner', output={'ore':4}, required_items={'tools':1}, used_items={}, consumed_items={'food':1})
- refiner = Agent(name='refiner', output={'metal':1}, required_items={'tools':1}, used_items={'ore':1}, consumed_items={'food':1})
- woodcutter = Agent(name='woodcutter', output={'wood':2}, required_items={'tools':1}, used_items={}, consumed_items={'food':1})
- blacksmith = Agent(name='blacksmith', output={'tools':1}, required_items={}, used_items={'metal':1, 'wood':1}, consumed_items={'food':1})
- def run_simulation(num_rounds):
- global agents
- ## Run a simulation ##
- for iteration in range(num_rounds):
- prices = {'wood':[], 'food':[], 'ore':[], 'metal':[], 'tools':[]}
- # First, each agent does what they do
- for agent in reversed(agents):
- agent.produce_items()
- agent.eval_sell()
- agent.gold -= TAXES
- if agent.gold > 0:
- agent.eval_need()
- else:
- agents.remove(agent)
- #print('Removing a ' + agent.name)
- add_new_agent()
- ## Run the auction
- for commodity, auction in auctions.iteritems():
- # Remove any bias? Not sure how much this does, if we're sorting them by price anyway
- random.shuffle(auction.bids)
- random.shuffle(auction.asks)
- ## Sort the bids by price (highest to lowest) ##
- auction.bids = sorted(auction.bids, key=lambda attr: attr.price, reverse=True)
- ## Sort the asks by price (lowest to hghest) ##
- auction.asks = sorted(auction.asks, key=lambda attr: attr.price)
- num_bids = len(auction.bids)
- num_asks = len(auction.asks)
- while not len(auction.bids) == 0 and not len(auction.asks) == 0:
- buyer = auction.bids[0]
- seller = auction.asks[0]
- # Determine price/amount
- quantity = min(buyer.quantity, seller.quantity)
- price = int(round((buyer.price + seller.price)/2))
- if quantity > 0:
- # Adjust buyer/seller requested amounts
- buyer.quantity -= quantity
- seller.quantity -= quantity
- buyer.owner.eval_trade_accepted(buyer.commodity, price)
- seller.owner.eval_trade_accepted(buyer.commodity, price)
- ## Update inventories and gold counts
- for i in range(quantity):
- buyer.owner.inventory.append(buyer.commodity)
- seller.owner.inventory.remove(seller.commodity)
- buyer.owner.gold -= (price*quantity)
- seller.owner.gold += (price*quantity)
- # Add to running tally of prices this turn
- prices[commodity].append(price)
- # Now that a transaction has occured, bump out the buyer or seller if either is satasfied
- if seller.quantity == 0: del auction.asks[0]
- if buyer.quantity == 0: del auction.bids[0]
- # All bidders re-evaluate prices - currently too simplistic
- if len(auction.bids) > 0:
- for buyer in auction.bids:
- buyer.owner.eval_bid_rejected(buyer.commodity, None)
- auctions[commodity].bids = []
- # All sellers re-evaluate prices - too simplistic as well
- elif len(auction.asks) > 0:
- for seller in auction.asks:
- seller.owner.eval_bid_rejected(seller.commodity, None)
- auctions[commodity].asks = []
- ## Average prices
- if len(prices[commodity]) > 0:
- price_mean = int(round(sum(prices[commodity])/len(prices[commodity])))
- auction.price_history.append(price_mean)
- # Track mean price for last N turns
- auction.update_mean_price()
- #print (auction.commodity + ': ' + str(auction.mean_price) + '. This round: ' +
- #str(len(prices[commodity])) + ' ' + commodity + ' averaged at $' + str(price_mean) +
- #' (' + str(num_bids) + ' bids, ' + str(num_asks) + ' asks)')
- else:
- auction.price_history.append(auction.price_history[-1])
- #print (commodity + ' was not sold this round (' + str(num_bids) + ' bids, ' + str(num_asks) + ' asks)')
- auctions = {'food':Auction('food'), 'wood':Auction('wood'), 'ore':Auction('ore'), 'metal':Auction('metal'), 'tools':Auction('tools')}
- create_agents(20)
- #cProfile.run('run_simulation(1)', 'economy')
- run_simulation(200)
- for auction in auctions.itervalues():
- print auction.commodity, auction.price_history
- agent_nums = {'farmer':0, 'woodcutter':0, 'miner':0, 'blacksmith':0, 'refiner':0}
- for agent in agents:
- agent_nums[agent.name] += 1
- #print(agent.name + ': ' + str(agent.gold))
- print str(len(agents)), agent_nums
- #p = pstats.Stats('economy')
- #p.sort_stats('cumulative').print_stats(37)
- #p.sort_stats('time').print_stats(10)
- #p.sort_stats('file').print_stats('__init__')
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement