Source code for abce.trade

# Copyright 2012 Davoud Taghawi-Nejad
#
# Module Author: Davoud Taghawi-Nejad
#
# ABCE is open-source software. If you are using ABCE for your research you are
# requested the quote the use of this software.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License and quotation of the
# author. You may obtain a copy of the License at
#       http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
"""
The :class:`abceagent.Agent` class is the basic class for creating your agent. It
automatically handles the possession of goods of an agent. In order to produce/transform
goods you need to also subclass the :class:`abceagent.Firm` [1]_ or to create a consumer
the :class:`abceagent.Household`.

For detailed documentation on:

Trading:
    see :class:`abceagent.Trade`
Logging and data creation:
    see :class:`abceagent.Database` and :doc:`simulation_results`
Messaging between agents:
    see :class:`abceagent.Messaging`.

.. autoexception:: abcetools.NotEnoughGoods

.. [1] or :class:`abceagent.FirmMultiTechnologies` for simulations with complex technologies.
"""
# ***************************************************************************************** #
#  trade.pyx is written in cython. When you modify trade.pyx you need to compile it with    #
# compile.sh and compile.py because the resulting trade.c file is distributed.              #
# Don't forget to commit it to git                                                          #
# ***************************************************************************************** #
import random
from collections import defaultdict, OrderedDict
from abce.notenoughgoods import NotEnoughGoods

epsilon = 0.00000000001


[docs]def get_epsilon(): return epsilon
[docs]class Offer(object): __slots__ = ('sender', 'receiver', 'good', 'quantity', 'price', 'currency', 'sell', 'status', 'final_quantity', 'id', 'made', 'status_round') """ This is an offer container that is send to the other agent. You can access the offer container both at the receiver as well as at the sender, if you have saved the offer. (e.G. self.offer = self.sell(...)) it has the following properties: sender_group: this is the group name of the sender sender: this is the the sender receiver: this is the the sender currency: The other good against which the good is traded. good: the good offered or demanded quantity: the quantity offered or demanded price: the suggested transaction price sell: this can have the values False for buy; True for sell status: 'new': has been created, but not answered 'accepted': trade fully accepted 'rejected': trade rejected 'pending': offer has not yet answered, and is not older than one round. 'perished': the **perishable** good was not accepted by the end of the round and therefore perished. final_quantity: If the offer has been answerd this returns the actual quantity bought or sold. (Equal to quantity if the offer was accepted fully) id: a unique identifier """ def __init__(self, sender, receiver, good, quantity, price, currency, sell, id, made): self.sender = sender self.receiver = receiver self.good = good self.currency = currency self.quantity = quantity self.price = price self.sell = sell self.status = 'new' self.final_quantity = None self.id = id self.made = made self.status_round = None def __repr__(self): return ("""<{sender: %s, receiver: %s, good: %s, quantity: %f, price: %f, currency: %f, sell: %s, status: %s, final_quantity: % f, id: %i, made: %s, status_round: %s }>""" % (self.sender, self.receiver, self.good, self.quantity, self.price, self.currency, self.sell, self.status, self.final_quantity, self.id, self.made, self.status_round))
[docs]class Trade: """ Agents can trade with each other. The clearing of the trade is taken care of fully by ABCE. Selling a good works in the following way: 1. An agent sends an offer. :meth:`~.sell` *ABCE does not allow you to sell the same good twice; self.free(good) shows how much good is not reserved yet* 2. **Next subround:** An agent receives the offer :meth:`~.get_offers`, and can :meth:`~.accept`, :meth:`~.reject` or partially accept it. :meth:`~.accept` *The good is credited and the price is deducted from the agent's possessions.* 3. **Next subround:** - in case of acceptance *the money is automatically credited.* - in case of partial acceptance *the money is credited and part of the reserved good is unblocked.* - in case of rejection *the good is unblocked.* Analogously for buying: :meth:`~.buy` Example:: # Agent 1 def sales(self): self.remember_trade = self.sell('Household', 0, 'cookies', quantity=5, price=self.price) # Agent 2 def receive_sale(self): oo = self.get_offers('cookies') for offer in oo: if offer.price < 0.3: try: self.accept(offer) except NotEnoughGoods: self.accept(offer, self['money'] / offer.price) else: self.reject(offer) # Agent 1, subround 3 def learning(self): offer = self.info(self.remember_trade) if offer.status == 'reject': self.price *= .9 elif offer.status = 'accepted': self.price *= offer.final_quantity / offer.quantity Example:: # Agent 1 def sales(self): self.remember_trade = self.sell('Household', 0, 'cookies', quantity=5, price=self.price, currency='dollars') # Agent 2 def receive_sale(self): oo = self.get_offers('cookies') for offer in oo: if ((offer.currency == 'dollars' and offer.price < 0.3 * exchange_rate) or (offer.currency == 'euros' and dollars'offer.price < 0.3)): try: self.accept(offer) except NotEnoughGoods: self.accept(offer, self['money'] / offer.price) else: self.reject(offer) If we did not implement a barter class, but one can use this class as a barter class, """ def __init__(self, id, agent_parameters, simulation_parameters, group, trade_logging, database, check_unchecked_msgs, expiring, perishable, resource_endowment, start_round=None): super(Trade, self).__init__(id, agent_parameters, simulation_parameters, group, trade_logging, database, check_unchecked_msgs, expiring, perishable, resource_endowment, start_round) self.given_offers = OrderedDict() self._open_offers_buy = defaultdict(dict) self._open_offers_sell = defaultdict(dict) self._polled_offers = {} self._offer_count = 0 self.trade_logging = {'individual': 1, 'group': 2, 'off': 0}[trade_logging] self._trade_log = defaultdict(int) self._quotes = {} def _offer_counter(self): """ returns a unique number for an offer (containing the agent's name) """ self._offer_count += 1 return hash((self.name, self._offer_count)) def _advance_round(self, time): if self.trade_logging > 0: self.database_connection.put(["trade_log", self._trade_log, self.round]) self._trade_log = defaultdict(int)
[docs] def get_buy_offers_all(self, descending=False, sorted=True): """ """ goods = list(self._open_offers_buy.keys()) return {good: self.get_buy_offers(good, descending, sorted) for good in goods}
[docs] def get_sell_offers_all(self, descending=False, sorted=True): """ """ goods = list(self._open_offers_sell.keys()) return {good: self.get_sell_offers(good, descending, sorted) for good in goods}
[docs] def get_offers_all(self, descending=False, sorted=True): """ returns all offers in a dictionary, with goods as key. The in each goods-category the goods are ordered by price. The order can be reversed by setting descending=True *Offers that are not accepted in the same subround (def block) are automatically rejected.* However you can also manually reject. Args: descending(optional): is a bool. False for descending True for ascending by price sorted(default=True): Whether offers are sorted by price. Faster if False. Returns: a dictionary with good types as keys and list of :class:`abce.trade.Offer` as values Example:: oo = get_offers_all(descending=False) for good_category in oo: print('The cheapest good of category' + good_category + ' is ' + good_category[0]) for offer in oo[good_category]: if offer.price < 0.5: self.accept(offer) for offer in oo.beer: print(offer.price, offer.sender) """ goods = list(list(self._open_offers_sell.keys()) + list(self._open_offers_buy.keys())) return {good: self.get_offers(good, descending, sorted) for good in goods}
[docs] def get_buy_offers(self, good, sorted=True, descending=False, shuffled=True): """ """ ret = list(self._open_offers_buy[good].values()) self._polled_offers.update(self._open_offers_buy[good]) del self._open_offers_buy[good] if shuffled: random.shuffle(ret) if sorted: ret.sort(key=lambda objects: objects.price, reverse=descending) return ret
[docs] def get_sell_offers(self, good, sorted=True, descending=False, shuffled=True): """ """ ret = list(self._open_offers_sell[good].values()) self._polled_offers.update(self._open_offers_sell[good]) del self._open_offers_sell[good] if shuffled: random.shuffle(ret) if sorted: ret.sort(key=lambda objects: objects.price, reverse=descending) return ret
[docs] def get_offers(self, good, sorted=True, descending=False, shuffled=True): """ returns all offers of the 'good' ordered by price. *Offers that are not accepted in the same subround (def block) are automatically rejected.* However you can also manually reject. peek_offers can be used to look at the offers without them being rejected automatically Args: good: the good which should be retrieved sorted(bool, default=True): Whether offers are sorted by price. Faster if False. descending(bool, default=False): False for descending True for ascending by price shuffled(bool, default=True): whether the order of messages is randomized or correlated with the ID of the agent. Setting this to False speeds up the simulation considerably, but introduces a bias. Returns: A list of :class:`abce.trade.Offer` ordered by price. Example:: offers = get_offers('books') for offer in offers: if offer.price < 50: self.accept(offer) elif offer.price < 100: self.accept(offer, 1) else: self.reject(offer) # optional """ ret = (self.get_buy_offers(good, descending=False, sorted=False, shuffled=False) + self.get_sell_offers(good, descending=False, sorted=False, shuffled=False)) if shuffled: random.shuffle(ret) if sorted: ret.sort(key=lambda objects: objects.price, reverse=descending) return ret
[docs] def peak_buy_offers(self, good, sorted=True, descending=False, shuffled=True): """ """ ret = [] for offer in list(self._open_offers_buy[good].values()): ret.append(offer) if shuffled: random.shuffle(ret) if sorted: ret.sort(key=lambda objects: objects.price, reverse=descending) return ret
[docs] def peak_sell_offers(self, good, sorted=True, descending=False, shuffled=True): """ """ ret = [] for offer in list(self._open_offers_sell[good].values()): ret.append(offer) if shuffled: random.shuffle(ret) if sorted: ret.sort(key=lambda objects: objects.price, reverse=descending) return ret
[docs] def peak_offers(self, good, sorted=True, descending=False, shuffled=True): """ returns a peak on all offers of the 'good' ordered by price. Peaked offers can not be accepted or rejected and they do not expire. Args: good: the good which should be retrieved descending(bool, default=False): False for descending True for ascending by price Returns: A list of offers ordered by price Example:: offers = get_offers('books') for offer in offers: if offer.price < 50: self.accept(offer) elif offer.price < 100: self.accept(offer, 1) else: self.reject(offer) # optional """ ret = (self.peak_buy_offers(good, sorted=False, descending=False, shuffled=False) + self.peak_sell_offers(good, sorted=False, descending=False, shuffled=False)) if shuffled: random.shuffle(ret) if sorted: ret.sort(key=lambda objects: objects.price, reverse=descending) return ret
[docs] def sell(self, receiver, good, quantity, price, currency='money', epsilon=epsilon): """ Sends a offer to sell a particular good to somebody. The amount promised is reserved. (self.free(good), shows the not yet reserved goods) Args: receiver: the receiving agent 'good': name of the good quantity: maximum units disposed to buy at this price price: price per unit currency: is the currency of this transaction (defaults to 'money') epsilon (optional): if you have floating point errors, a quantity or prices is a fraction of number to high or low. You can increase the floating point tolerance. See troubleshooting -- floating point problems Returns: A reference to the offer. The offer and the offer status can be accessed with `self.info(offer_reference)`. Example:: def subround_1(self): self.offer = self.sell('household', 1, 'cookies', quantity=5, price=0.1) def subround_2(self): offer = self.info(self.offer) if offer.status == 'accepted': print(offer.final_quantity , 'cookies have be bougth') else: offer.status == 'rejected': print('On diet') """ assert price > - epsilon, 'price %.30f is smaller than 0 - epsilon (%.30f)' % (price, - epsilon) if price < 0: price = 0 # makes sure the quantity is between zero and maximum available, but # if its only a little bit above or below its set to the bounds assert quantity > - epsilon, 'quantity %.30f is smaller than 0 - epsilon (%.30f)' % (quantity, - epsilon) if quantity < 0: quantity = 0 self._inventory.reserve(good, quantity) offer_id = self._offer_counter() offer = Offer(self.name, receiver, good, quantity, price, currency, True, offer_id, self.round) self.given_offers[offer_id] = offer self._send(receiver, '!s', offer) return offer
[docs] def buy(self, receiver, good, quantity, price, currency='money', epsilon=epsilon): """ Sends a offer to buy a particular good to somebody. The money promised is reserved. (self.free(currency), shows the not yet reserved goods) Args: receiver: The name of the receiving agent a tuple (group, id). e.G. ('firm', 15) 'good': name of the good quantity: maximum units disposed to buy at this price price: price per unit currency: is the currency of this transaction (defaults to 'money') epsilon (optional): if you have floating point errors, a quantity or prices is a fraction of number to high or low. You can increase the floating point tolerance. See troubleshooting -- floating point problems """ assert price > - epsilon, 'price %.30f is smaller than 0 - epsilon (%.30f)' % (price, - epsilon) if price < 0: price = 0 money_amount = quantity * price # makes sure the money_amount is between zero and maximum available, but # if its only a little bit above or below its set to the bounds available = self._inventory[currency] assert money_amount > - epsilon, '%s (price * quantity) %.30f is smaller than 0 - epsilon (%.30f)' % (currency, money_amount, - epsilon) if money_amount < 0: money_amount = 0 if money_amount > available: money_amount = available offer_id = self._offer_counter() self._inventory.reserve(currency, money_amount) offer = Offer(self.name, receiver, good, quantity, price, currency, False, offer_id, self.round) self._send(receiver, '!b', offer) self.given_offers[offer_id] = offer return offer
[docs] def accept(self, offer, quantity=-999, epsilon=epsilon): """ The buy or sell offer is accepted and cleared. If no quantity is given the offer is fully accepted; If a quantity is given the offer is partial accepted. Args: offer: the offer the other party made quantity: quantity to accept. If not given all is accepted epsilon (optional): if you have floating point errors, a quantity or prices is a fraction of number to high or low. You can increase the floating point tolerance. See troubleshooting -- floating point problems Return: Returns a dictionary with the good's quantity and the amount paid. """ offer_quantity = offer.quantity if quantity == -999: quantity = offer_quantity assert quantity > - epsilon, 'quantity %.30f is smaller than 0 - epsilon (%.30f)' % (quantity, - epsilon) if quantity < 0: quantity = 0 if quantity > offer_quantity + epsilon * max(quantity, offer_quantity): raise AssertionError('accepted more than offered %s: %.100f >= %.100f' % (offer.good, quantity, offer_quantity)) if quantity > offer_quantity: quantity = offer_quantity if quantity == 0: self.reject(offer) return {offer.good: 0, offer.currency: 0} money_amount = quantity * offer.price if offer.sell: # ord('s') assert money_amount > - epsilon, 'money = quantity * offer.price %.30f is smaller than 0 - epsilon (%.30f)' % (money_amount, - epsilon) if money_amount < 0: money_amount = 0 available = self._inventory[offer.currency] if money_amount > available + epsilon + epsilon * max(money_amount, available): raise NotEnoughGoods(self.name, offer.currency, money_amount - available) if money_amount > available: money_amount = available self._inventory.haves[offer.good] += quantity self._inventory.haves[offer.currency] -= quantity * offer.price else: assert quantity > - epsilon, 'quantity %.30f is smaller than 0 - epsilon (%.30f)' % (quantity, - epsilon) if quantity < 0: quantity = 0 available = self._inventory[offer.good] if quantity > available + epsilon + epsilon * max(quantity, available): raise NotEnoughGoods(self.name, offer.good, quantity - available) if quantity > available: quantity = available self._inventory.haves[offer.good] -= quantity self._inventory.haves[offer.currency] += quantity * offer.price offer.final_quantity = quantity self._send(offer.sender, '_p', (offer.id, quantity)) del self._polled_offers[offer.id] if offer.sell: return {offer.good: - quantity, offer.currency: money_amount} else: return {offer.good: quantity, offer.currency: - money_amount}
def _reject_polled_but_not_accepted_offers(self): for offer in list(self._polled_offers.values()): self._reject(offer) self._polled_offers = {} def _reject(self, offer): """ Rejects the offer offer Args: offer: the offer the other party made (offer not quote!) """ self._send(offer.sender, '_r', offer.id)
[docs] def reject(self, offer): """ Rejects and offer, if the offer is subsequently accepted in the same subround it is accepted'. Peaked offers can not be rejected. Args: offer: the offer to be rejected """ pass
def _receive_accept(self, offer_id_final_quantity): """ When the other party partially accepted the money or good is received, remaining good or money is added back to haves and the offer is deleted """ offer = self.given_offers[offer_id_final_quantity[0]] offer.final_quantity = offer_id_final_quantity[1] if offer.sell: self._inventory.commit(offer.good, offer.quantity, offer.final_quantity) self._inventory.haves[offer.currency] += offer.final_quantity * offer.price else: self._inventory.haves[offer.good] += offer.final_quantity self._inventory.commit(offer.currency, offer.quantity * offer.price, offer.final_quantity * offer.price) offer.status = "accepted" offer.status_round = self.round del self.given_offers[offer.id] return offer def _log_receive_accept_group(self, offer): if offer.sell: self._trade_log[(offer.good, self.group, offer.receiver_group, offer.price)] += offer.final_quantity else: self._trade_log[(offer.good, offer.receiver_group, self.group, offer.price)] += offer.final_quantity def _log_receive_accept_agent(self, offer): if offer.sell: self._trade_log[(offer.good, self.name_without_colon, '%s_%i' % (*offer.receiver, offer.price))] += offer.final_quantity else: self._trade_log[(offer.good, '%s_%i' % (*offer.receiver, self.name_without_colon, offer.price))] += offer.final_quantity def _receive_reject(self, offer_id): """ deletes a given offer is used by _do_message_clearing, when the other party rejects or at the end of the subround when agent retracted the offer """ offer = self.given_offers[offer_id] if offer.sell: self._inventory.rewind(offer.good, offer.quantity) else: self._inventory.rewind(offer.currency, offer.quantity * offer.price) offer.status = "rejected" offer.status_round = self.round offer.final_quantity = 0 del self.given_offers[offer_id] def _delete_given_offer(self, offer_id): offer = self.given_offers.pop(offer_id) if offer.sell: self._inventory.rewind(offer.good, offer.quantity) else: self._inventory.rewind(offer.currency, offer.quantity * offer.price)
[docs] def give(self, receiver, good, quantity, epsilon=epsilon): """ gives a good to another agent Args: receiver: The name of the receiving agent a tuple (group, id). e.G. ('firm', 15) good: the good to be transfered quantity: amount to be transfered epsilon (optional): if you have floating point errors, a quantity or prices is a fraction of number to high or low. You can increase the floating point tolerance. See troubleshooting -- floating point problems Raises: AssertionError, when good smaller than 0. Return: Dictionary, with the transfer, which can be used by self.log(...). Example:: self.log('taxes', self.give('money': 0.05 * self['money']) """ assert quantity > - epsilon, 'quantity %.30f is smaller than 0 - epsilon (%.30f)' % (quantity, - epsilon) if quantity < 0: quantity = 0 available = self._inventory[good] if quantity > available + epsilon + epsilon * max(quantity, available): raise NotEnoughGoods(self.name, good, quantity - available) if quantity > available: quantity = available self._inventory.haves[good] -= quantity self._send(receiver, '_g', [good, quantity]) return {good: quantity}
[docs] def take(self, receiver, good, quantity, epsilon=epsilon): """ take a good from another agent. The other agent has to accept. using self.accept() Args: receiver: the receiving agent good: the good to be taken quantity: the quantity to be taken epsilon (optional): if you have floating point errors, a quantity or prices is a fraction of number to high or low. You can increase the floating point tolerance. See troubleshooting -- floating point problems """ self.buy(receiver[0], receiver[1], good=good, quantity=quantity, price=0, epsilon=epsilon)
[docs]def compare_with_ties(x, y): if x < y: return -1 elif x > y: return 1 else: return random.randint(0, 1) * 2 - 1