diff --git a/README.md b/README.md index a84cb50..0d60a57 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,12 @@ -# python-distilledESL +# ABCESL [![DOI](https://zenodo.org/badge/95235014.svg)](https://zenodo.org/badge/latestdoi/95235014) -[![Build Status](https://travis-ci.org/rht/py-distilledESL.svg?branch=master)](https://travis-ci.org/rht/py-distilledESL) + This is a python port of the entirety of https://github.com/EconomicSL/distilledESL, further trimmed to its essence. -`distilledESL` is a rewrite of Core-ESL, a reference implementation of Economic +It is a plugin to [ABCE](https://github.com/AB-CE/abce); +`ABCESL` is a reference implementation of Economic Simulation Library. I anticipate that the spec for the EconomicSL library will be cross-language, just like in the case of a spec of an IETF RFC. @@ -13,16 +14,21 @@ Currently it has a feature and implementation parity with: https://github.com/EconomicSL/distilledESL/commit/1ef2d0a9fed490990f2a600b2916391de50bdea8 (May 10 2017) The goal of this repo is to become a catalyst for a convergent evolution between the -API designs in Core-ESL / distilled-ESL / ABCE. Ideas can be tested much more +API designs distilled-ESL / ABCE. Ideas can be tested much more easily here. +While the ABCESL is currently used in Oxford for our stress testing model, +it is sparsely documented and lacks behavioral and unit tests. This is in +sharp contrast to ABCE, which has a good documentation and a test coverage +of all essential functions. + ## Installation This library uses Python >=3.5 type system, so it runs only on python>=3.5. To install, run ``` -$ pip install git+https://github.com/rht/py-distilledESL +$ pip install git+https://github.com/AB-CE/ABCESL.git ``` or from this repo diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..523a5cc --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = python3 -msphinx +SPHINXPROJ = abcESL +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/account.rst b/docs/account.rst new file mode 100644 index 0000000..7a8571d --- /dev/null +++ b/docs/account.rst @@ -0,0 +1,8 @@ +Account +======= + +.. automodule:: economicsl.accounttype + +.. automodule:: economicsl.account + :members: + :show-inheritance: diff --git a/docs/action.rst b/docs/action.rst new file mode 100644 index 0000000..e69de29 diff --git a/docs/agent.rst b/docs/agent.rst new file mode 100644 index 0000000..34fa918 --- /dev/null +++ b/docs/agent.rst @@ -0,0 +1,6 @@ +abcesl.Agent +============ + +.. automodule:: economicsl.agent + :members: + :show-inheritance: diff --git a/docs/bankersrounding.rst b/docs/bankersrounding.rst new file mode 100644 index 0000000..f56b451 --- /dev/null +++ b/docs/bankersrounding.rst @@ -0,0 +1,6 @@ +Bankersrounding +=============== + +.. automodule:: economicsl.bankersrounding + :members: + :show-inheritance: diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..11d91a9 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# abcESL documentation build configuration file, created by +# sphinx-quickstart on Wed Sep 6 20:33:17 2017. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = ['sphinx.ext.autodoc'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = 'abcESL' +copyright = '2017, Rudy Tanin, Davoud Taghawi-Nejad, Alissa Kleinnijenhuis, Thom Wetzer' +author = 'R. Tanin, D. Taghawi-Nejad, A. Kleinnijenhuis, T. Wetzer, D. Farmer' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '0.1' +# The full version, including alpha/beta/rc tags. +release = '0.1' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'alabaster' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# This is required for the alabaster theme +# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars +html_sidebars = { + '**': [ + 'about.html', + 'navigation.html', + 'relations.html', # needs 'show_related': True theme option to display + 'searchbox.html', + 'donate.html', + ] +} + + +# -- Options for HTMLHelp output ------------------------------------------ + +# Output file base name for HTML help builder. +htmlhelp_basename = 'abcESLdoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'abcESL.tex', 'abcESL Documentation', + 'R. Tanin, D. Taghawi-Nejad, A. Kleinnijenhuis, T. Wetzer, D. Farmer', 'manual'), +] + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'abcesl', 'abcESL Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'abcESL', 'abcESL Documentation', + author, 'abcESL', 'One line description of project.', + 'Miscellaneous'), +] + + + diff --git a/docs/contract.rst b/docs/contract.rst new file mode 100644 index 0000000..88e6b43 --- /dev/null +++ b/docs/contract.rst @@ -0,0 +1,6 @@ +Contract +======== + +.. automodule:: economicsl.contract + :members: + :show-inheritance: diff --git a/docs/contracting.rst b/docs/contracting.rst new file mode 100644 index 0000000..28d6705 --- /dev/null +++ b/docs/contracting.rst @@ -0,0 +1,6 @@ +Contracting +=========== + +.. automodule:: abce.contracts.flexiblecontracting + :members: + :show-inheritance: diff --git a/docs/exception.rst b/docs/exception.rst new file mode 100644 index 0000000..4f630ed --- /dev/null +++ b/docs/exception.rst @@ -0,0 +1,6 @@ +Exception +========= + +.. autoexception:: abce.NotEnoughGoods + :members: + :show-inheritance: diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..218b7dd --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,52 @@ +.. abcESL documentation master file, created by + sphinx-quickstart on Wed Sep 6 20:33:17 2017. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to abcESL's documentation! +================================== + +Introduction +------------ + +ABCESL is an accounting and contracting framework for ABCE. It was originally developed by +Rafa Babtista, Alissa Kleinnijenhuis and Thom Wetzer and subsequently distilled and refined by +Rudy Tanin, Davoud Taghawi-Nejad. Originally it was conceived as a framework to run +systemic stress testing exercises for the financial sector. +Currently it is used in the resilience project as well as in an insurance modeling +project. + +In order to install abcesl, first install ABCE (abce.readthedocs.io) and then +install :code:`sudo python3 -m pip install abcesl` or +:code:`sudo pypy3 -m pip install abcesl`. + +You can than create agents that inherit from abcesl.Agent instead of abce.Agent. +Like abce agents these agents have an inventory of goods :code:`self['cookies']`, +but they also have an inventory of contracts and set of accounts that book the +values of goods and contracts. + +ABCESL is part of the wider effort of the Economic Simulation Library at +the Institute for New Economic Thinking at Oxford University. + + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + + agent + ledger + account + contract + contracting + obligations + obligationmessage + exception + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/ledger.rst b/docs/ledger.rst new file mode 100644 index 0000000..9f80a8e --- /dev/null +++ b/docs/ledger.rst @@ -0,0 +1,10 @@ +Ledger +====== + +.. automodule:: economicsl.ledger + +.. autoclass:: economicsl.ledger.Ledger + :members: + :show-inheritance: + + diff --git a/docs/obligationmessage.rst b/docs/obligationmessage.rst new file mode 100644 index 0000000..943c14d --- /dev/null +++ b/docs/obligationmessage.rst @@ -0,0 +1,6 @@ +ObligationsMessage +================== + +.. automodule:: economicsl.obligationmessage + :members: + :show-inheritance: diff --git a/docs/obligations.rst b/docs/obligations.rst new file mode 100644 index 0000000..551ddf5 --- /dev/null +++ b/docs/obligations.rst @@ -0,0 +1,6 @@ +Obligations +=========== + +.. automodule:: economicsl.obligations + :members: + :show-inheritance: diff --git a/docs/source/economicsl.rst b/docs/source/economicsl.rst new file mode 100644 index 0000000..c69989f --- /dev/null +++ b/docs/source/economicsl.rst @@ -0,0 +1,94 @@ +economicsl package +================== + +Submodules +---------- + +economicsl\.account module +-------------------------- + +.. automodule:: economicsl.account + :members: + :undoc-members: + :show-inheritance: + +economicsl\.accounttype module +------------------------------ + +.. automodule:: economicsl.accounttype + :members: + :undoc-members: + :show-inheritance: + +economicsl\.action module +------------------------- + +.. automodule:: economicsl.action + :members: + :undoc-members: + :show-inheritance: + +economicsl\.agent module +------------------------ + +.. automodule:: economicsl.agent + :members: + :undoc-members: + :show-inheritance: + +economicsl\.contract module +--------------------------- + +.. automodule:: economicsl.contract + :members: + :undoc-members: + :show-inheritance: + +economicsl\.contracts module +---------------------------- + +.. automodule:: economicsl.contracts + :members: + :undoc-members: + :show-inheritance: + +economicsl\.ledger module +------------------------- + +.. automodule:: economicsl.ledger + :members: + :undoc-members: + :show-inheritance: + +economicsl\.obligationmessage module +------------------------------------ + +.. automodule:: economicsl.obligationmessage + :members: + :undoc-members: + :show-inheritance: + +economicsl\.obligations module +------------------------------ + +.. automodule:: economicsl.obligations + :members: + :undoc-members: + :show-inheritance: + +economicsl\.obligationsmailbox module +------------------------------------- + +.. automodule:: economicsl.obligationsmailbox + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: economicsl + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/modules.rst b/docs/source/modules.rst new file mode 100644 index 0000000..a0fdc50 --- /dev/null +++ b/docs/source/modules.rst @@ -0,0 +1,12 @@ +economicsl +========== + + + +Sep 06, 2017 +W + +.. toctree:: + :maxdepth: 4 + + economicsl diff --git a/economicsl/__init__.py b/economicsl/__init__.py index 99af35c..d668a76 100644 --- a/economicsl/__init__.py +++ b/economicsl/__init__.py @@ -1,148 +1,12 @@ +""" abcesl is an Contracting and accounting plugin for ABCE. ABCE is the agent-based computational +economics library. www.github.com/AB-CE/abce. +""" from typing import List -import numpy as np - -from .accounting import Ledger -from .obligations import ObligationMessage, ObligationsAndGoodsMailbox - +from .ledger import Ledger from .obligations import Obligation -from .accounting import AccountType # NOQA +from .accounttype import AccountType # NOQA from abce import NotEnoughGoods # NOQA import abce - - -class Agent(abce.Agent): - def __init__(self, id, group, trade_logging, - database, logger, random_seed, num_managers): - super().__init__(id, group, trade_logging, - database, logger, random_seed, num_managers) - self.name = (group, self.id) - self.alive = True - self.mainLedger = Ledger(self, self._haves) - self.obligationsAndGoodsMailbox = ObligationsAndGoodsMailbox(self) - - def init(self, agent_parameters, sim_parameters): - super().init(agent_parameters, sim_parameters) - self.simulation = sim_parameters - - def _begin_subround(self): - super()._begin_subround() - self.step() - - def add(self, contract) -> None: - if (contract.getAssetParty() == self.name): - # This contract is an asset for me. - self.mainLedger.addAsset(contract) - elif (contract.getLiabilityParty() == self.name): - # This contract is a liability for me - self.mainLedger.addLiability(contract) - else: - print(contract, contract.getAssetParty(), contract.getLiabilityParty(), (self.name)) - raise Exception("who the fuck is this" ) - - def getName(self): - return str(self.name) - - def getTime(self): - return self.simulation.time - - def getSimulation(self): - return self.simulation - - def isAlive(self) -> bool: - return self.alive - - def addCash(self, amount: np.longdouble) -> None: - self.mainLedger.inventory.create('money', amount) - - def getCash_(self) -> np.longdouble: - return self.mainLedger.inventory['money'] - - def getMainLedger(self) -> Ledger: - return self.mainLedger - - def step(self) -> None: - self.obligationsAndGoodsMailbox.step() - - def sendObligation(self, recipient, obligation: Obligation) -> None: - if isinstance(obligation, ObligationMessage): - self.message(recipient.group, recipient.id, '!oblmsg', obligation) - self.obligationsAndGoodsMailbox.addToObligationOutbox(obligation) - else: - msg = ObligationMessage(self, obligation) - self.message(recipient.group, recipient.id, '!oblmsg', msg) - - - def printMailbox(self) -> None: - self.obligationsAndGoodsMailbox.printMailbox() - - def message(self, receiver, topic, content, overload=None): - - if not isinstance(receiver, tuple): - receiver = receiver.name - super().message(receiver[0], receiver[1], topic, content) - - - def get_obligation_outbox(self) -> List[Obligation]: - return self.obligationsAndGoodsMailbox.getObligation_outbox() - - -class Action: - def __init__(self, me: Agent) -> None: - self.me = me - self.amount = np.longdouble(0.0) - - def perform(self) -> None: - print("Model.actionsRecorder.recordAction(this); not called because deleted") - - def getAmount(self) -> np.longdouble: - return self.amount - - def setAmount(self, amount: np.longdouble): - self.amount = np.longdouble(amount) - - def getTime(self) -> int: - return self.me.getTime() - - def print(self, actions=None) -> None: - if actions: - counter = 1 - for action in actions: - print("Action", counter, "->", action.getName()) - counter += 1 - - def getAgent(self) -> Agent: - return self.me - - def getSimulation(self): - return self.me.getSimulation() - - -class Contract: - def getAssetParty(self): - pass - - def getLiabilityParty(self): - pass - - def getValue(self, me): - pass - - def getAvailableActions(self, me): - pass - - def getName(self, me): - pass - - -class BankersRounding: - def bankersRounding(self, value: np.longdouble) -> int: - s = int(value) - t = abs(value - s) - - if ((t < 0.5) or (t == 0.5 and s % 2 == 0)): - return s - else: - if (value < 0): - return s - 1 - else: - return s + 1 +from .agent import Agent +from .action import Action +from .contract import Contract diff --git a/economicsl/account.py b/economicsl/account.py new file mode 100644 index 0000000..8e660f2 --- /dev/null +++ b/economicsl/account.py @@ -0,0 +1,33 @@ +from .accounttype import AccountType + + +class Account: + """ An account either an ASSET, LIABILITY, INCOME, EXPENSES or GOOD account. + It's value can be increase or decreased according to a double entry operation """ + def __init__(self, name, accountType, startingBalance=0) -> None: + self.name = name + self.accountType = accountType + self.balance = startingBalance + + def debit(self, amount): + """ A Debit is a positive change for ASSET and EXPENSES accounts, and negative for the rest. """ + if (self.accountType == AccountType.ASSET) or (self.accountType == AccountType.EXPENSES): + self.balance += amount + else: + self.balance -= amount + + def credit(self, amount): + """ A Credit is a negative change for ASSET and EXPENSES accounts, and positive for the rest. """ + if ((self.accountType == AccountType.ASSET) or (self.accountType == AccountType.EXPENSES)): + self.balance -= amount + else: + self.balance += amount + + def getAccountType(self): + return self.accountType + + def getBalance(self): + return self.balance + + def getName(self): + return self.name diff --git a/economicsl/accounting.py b/economicsl/accounting.py deleted file mode 100644 index 87ac789..0000000 --- a/economicsl/accounting.py +++ /dev/null @@ -1,279 +0,0 @@ -import numpy as np -from abce import NotEnoughGoods - - -def doubleEntry(debitAccount, creditAccount, amount: np.longdouble): - debitAccount.debit(amount) - creditAccount.credit(amount) - - -class Account: - def __init__(self, name, accountType, startingBalance: np.longdouble=0.0) -> None: - self.name = name - self.accountType = accountType - self.balance = np.longdouble(startingBalance) - - # A Debit is a positive change for ASSET and EXPENSES accounts, and negative for the rest. - def debit(self, amount: np.longdouble): - if (self.accountType == AccountType.ASSET) or (self.accountType == AccountType.EXPENSES): - self.balance += amount - else: - self.balance -= amount - - # A Credit is a negative change for ASSET and EXPENSES accounts, and positive for the rest. - def credit(self, amount: np.longdouble): - if ((self.accountType == AccountType.ASSET) or (self.accountType == AccountType.EXPENSES)): - self.balance -= amount - else: - self.balance += amount - - def getAccountType(self): - return self.accountType - - def getBalance(self) -> np.longdouble: - return self.balance - - def getName(self) -> str: - return self.name - - -def enum(**enums): - return type('Enum', (), enums) - - -AccountType = enum(ASSET=1, - LIABILITY=2, - INCOME=4, - EXPENSES=5, - GOOD=6) - - -# This is the main class implementing double entry org.economicsl.accounting. All public operations provided by this class -# are performed as a double entry operation, i.e. a pair of (dr, cr) operations. -# -# A Ledger contains a set of accounts, and is the interface between an agent and its accounts. Agents cannot -# directly interact with accounts other than via a Ledger. -# -# At the moment, a Ledger contains an account for each type of contract, plus an equity account and a cash account. -# -# A simple economic agent will usually have a single Ledger, whereas complex firms and banks can have several books -# (as in branch banking for example). -class Ledger: - def __init__(self, me, inventory) -> None: - # A StressLedger is a list of accounts (for quicker searching) - - # Each Account includes an inventory to hold one type of contract. - # These hashmaps are used to access the correct account for a given type of contract. - # Note that separate hashmaps are needed for asset accounts and liability accounts: the same contract - # type (such as Loan) can sometimes be an asset and sometimes a liability. - - # A book is initially created with a cash account (it's the simplest possible book) - self.assetAccounts = {} # a hashmap from a contract to a assetAccount - self.inventory = inventory - self.contracts = Contracts() - self.goodsAccounts = {} - self.liabilityAccounts = {} # a hashmap from a contract to a liabilityAccount - self.me = me - - def getAssetValue(self) -> np.longdouble: - return sum([aa.getBalance() for aa in self.assetAccounts.values()]) - - def getLiabilityValue(self) -> np.longdouble: - return sum([la.getBalance() for la in self.liabilityAccounts.values()]) - - def getEquityValue(self) -> np.longdouble: - return self.getAssetValue() - self.getLiabilityValue() - - def getAssetValueOf(self, contractType) -> np.longdouble: - # return assetAccounts.get(contractType).getBalance(); - return sum([c.getValue() for c in self.contracts.allAssets if isinstance(c, contractType)]) - - def getLiabilityValueOf(self, contractType) -> np.longdouble: - # return liabilityAccounts.get(contractType).getBalance(); - return sum([c.getValue() for c in self.contracts.allLiabilities if isinstance(c, contractType)]) - - def getAllAssets(self): - return self.contracts.allAssets - - def getAllLiabilities(self): - return self.contracts.allLiabilities - - def getAssetsOfType(self, contractType): - return [c for c in self.contracts.allAssets if isinstance(c, contractType)] - - def getLiabilitiesOfType(self, contractType): - return [c for c in self.contracts.allLiabilities if isinstance(c, contractType)] - - def addAccount(self, account, contractType): - switch = account.getAccountType() - if switch == AccountType.ASSET: - self.assetAccounts[contractType] = account - elif switch == AccountType.LIABILITY: - self.liabilityAccounts[contractType] = account - - # Not sure what to do with INCOME, EXPENSES - - # Adding an asset means debiting the account relevant to that type of contract - # and crediting equity. - # @param contract an Asset contract to add - def addAsset(self, contract): - assetAccount = self.assetAccounts.get(contract) - - if assetAccount is None: - # If there doesn't exist an Account to hold this type of contract, we create it - assetAccount = Account(contract.getName(self.me), AccountType.ASSET) - self.addAccount(assetAccount, contract) - - assetAccount.debit(contract.getValue()) - - self.contracts.allAssets.append(contract) - - # Adding a liability means debiting equity and crediting the account - # relevant to that type of contract. - # @param contract a Liability contract to add - def addLiability(self, contract): - liabilityAccount = self.liabilityAccounts.get(contract) - - if liabilityAccount is None: - # If there doesn't exist an Account to hold this type of contract, we create it - liabilityAccount = Account(contract.getName(self.me), AccountType.LIABILITY) - self.addAccount(liabilityAccount, contract) - - liabilityAccount.credit(contract.getValue()) - - # Add to the general inventory? - self.contracts.allLiabilities.append(contract) - - def create(self, name, amount, value): - self.inventory.create(name, amount) - physicalthingsaccount = self.getGoodsAccount(name) - physicalthingsaccount.debit(amount * value) - - def destroy(self, name, amount, value=None): - if value is None: - try: - value = self.getPhysicalThingValue(name) - self.destroy(name, amount, value) - except: - raise NotEnoughGoods(name, 0, amount) - else: - self.inventory.destroy(name, amount) - self.getGoodsAccount(name).credit(amount * value) - - def getGoodsAccount(self, name): - account = self.goodsAccounts.get(name) - if account is None: - account = Account(name, AccountType.GOOD) - self.goodsAccounts[name] = account - return account - - def getPhysicalThingValue(self, name): - try: - return self.getGoodsAccount(name).getBalance() / self.inventory.getGood(name) - except: - return 0.0 - - # Reevaluates the current stock of phisical goods at a specified value and books - # the change to org.economicsl.accounting - def revalueGoods(self, name, value): - old_value = self.getGoodsAccount(name).getBalance() - new_value = self.inventory.getGood(name) * value - if (new_value > old_value): - self.getGoodsAccount(name).debit(new_value - old_value) - elif (new_value < old_value): - self.getGoodsAccount(name).credit(old_value - new_value) - - def addCash(self, amount: np.longdouble) -> None: - # (dr cash, cr equity) - self.create("money", np.longdouble(amount), 1.0) - - def subtractCash(self, amount: np.longdouble) -> None: - self.destroy("money", np.longdouble(amount), 1.0) - - # Operation to pay back a liability loan; debit liability and credit cash - # @param amount amount to pay back - # @param loan the loan which is being paid back - def payLiability(self, amount, loan): - self.liabilityAccount = self.liabilityAccounts.get(loan) - - assert self.inventory.getCash() >= amount # Pre-condition: liquidity has been raised. - - # (dr liability, cr cash ) - doubleEntry(self.liabilityAccount, self['money'], amount) - - # If I've sold an asset, debit cash and credit asset - # @param amount the *value* of the asset - def sellAsset(self, amount, assetType): - assetAccount = self.assetAccounts.get(assetType) - - # (dr cash, cr asset) - doubleEntry(self["money"], assetAccount, amount) - - # Operation to cancel a Loan to someone (i.e. cash in a Loan in the Assets side). - # - # I'm using this for simplicity but note that this is equivalent to selling an asset. - # @param amount the amount of loan that is cancelled - def pullFunding(self, amount, loan): - loanAccount = self.getAccountFromContract(loan) - # (dr cash, cr asset ) - doubleEntry(self.getCashAccount(), loanAccount, amount) - - def printBalanceSheet(self, me): - print("Asset accounts:\n---------------") - for a in self.assetAccounts.values(): - print(a.getName(), "-> %.2f" % a.getBalance()) - - print("Breakdown: ") - for c in self.contracts.allAssets: - print("\t", c.getName(me), " > ", c.getValue()) - print("TOTAL ASSETS: %.2f" % self.getAssetValue()) - - print("\nLiability accounts:\n---------------") - for a in self.liabilityAccounts.values(): - print(a.getName(), " -> %.2f" % a.getBalance()) - for c in self.contracts.allLiabilities: - print("\t", c.getName(me), " > ", c.getValue()) - print("TOTAL LIABILITIES: %.2f" % self.getLiabilityValue()) - print("\nTOTAL EQUITY: %.2f" % self.getEquityValue()) - - print("\nSummary of encumbered collateral:") - # for (Contract contract : getLiabilitiesOfType(Repo.class)) { - # ((Repo) contract).printCollateral(); - # } - print("\n\nTotal cash: ", self.getGoodsAccount("cash").getBalance()) - # print("Encumbered cash: "+me.getEncumberedCash()); - # print("Unencumbered cash: " + (me.getCash_() - me.getEncumberedCash())); - - def getInitialEquity(self): - return self.initialEquity - - def setInitialValues(self): - self.initialEquity = self.getEquityValue() - - def getAccountFromContract(self, contract): - return self.assetAccounts.get(contract) - - def getCashAccount(self): - return self["money"] - - # if an Asset loses value, I must debit equity and credit asset - # @param valueLost the value lost - def devalueAsset(self, asset, valueLost): - self.assetAccounts.get(asset).credit(valueLost) - - # Todo: perform a check here that the Asset account balances match the value of the assets. (?) - - def appreciateAsset(self, asset, valueLost): - self.assetAccounts.get(asset).debit(valueLost) - - def devalueLiability(self, liability, valueLost): - self.liabilityAccounts.get(liability).debit(valueLost) - - def appreciateLiability(self, liability, valueLost): - self.liabilityAccounts.get(liability).credit(valueLost) - - -class Contracts: - def __init__(self): - self.allAssets = [] - self.allLiabilities = [] diff --git a/economicsl/accounttype.py b/economicsl/accounttype.py new file mode 100644 index 0000000..0d004ab --- /dev/null +++ b/economicsl/accounttype.py @@ -0,0 +1,10 @@ +""" There are 5 account types in the AccountType class ASSET, LIABILITY, INCOME, EXPENSES, GOOD """ +import enum + + +class AccountType(enum.Enum): + ASSET=1 + LIABILITY=2 + INCOME=4 + EXPENSES=5 + GOOD=6 diff --git a/economicsl/action.py b/economicsl/action.py new file mode 100644 index 0000000..a4cdbc1 --- /dev/null +++ b/economicsl/action.py @@ -0,0 +1,29 @@ +class Action: + def __init__(self, me) -> None: + self.me = me + self.amount = 0.0 + + def perform(self) -> None: + print("Model.actionsRecorder.recordAction(this); not called because deleted") + + def getAmount(self): + return self.amount + + def setAmount(self, amount): + self.amount = amount + + def getTime(self) -> int: + return self.me.getTime() + + def print(self, actions=None) -> None: + if actions: + counter = 1 + for action in actions: + print("Action", counter, "->", action.getName()) + counter += 1 + + def getAgent(self): + return self.me + + def getSimulation(self): + return self.me.getSimulation() diff --git a/economicsl/agent.py b/economicsl/agent.py new file mode 100644 index 0000000..4aec3bf --- /dev/null +++ b/economicsl/agent.py @@ -0,0 +1,133 @@ +""" test """ +from typing import List +from .ledger import Ledger +from .obligationmessage import ObligationMessage +from .obligationsmailbox import ObligationsMailbox +from .obligations import Obligation +from .accounttype import AccountType # NOQA +from abce import NotEnoughGoods # NOQA +import abce +from math import isclose + + +class Agent(abce.Agent): + """ The abcesl.Agent inherits from abce.Agent, it additionally provides + contracting and accounting capabilities """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.alive = True + self.mainLedger = Ledger(self, self._inventory) + self.inventory = self.mainLedger.inventory + self.obligationsMailbox = ObligationsMailbox(self) + self.simulation = kwargs['simulation_parameters'] + + def _begin_subround(self): + """ This means step is called every new subround in ABCE, it's + from the plugin API """ + super()._begin_subround() + self._step() + + def add(self, contract): + """ Adds a contract to the agent's ledger, figures out + whether the contract is an asset or an liability """ + if (contract.getAssetParty() == self.name): + # This contract is an asset for me. + self.mainLedger.addAsset(contract) + elif (contract.getLiabilityParty() == self.name): + # This contract is a liability for me + self.mainLedger.addLiability(contract) + else: + print(contract, contract.getAssetParty(), contract.getLiabilityParty(), (self.name)) + raise Exception("who the fuck is this") + + def getName(self): + """ depreciated """ + return str(self.name) + + def getTime(self): + """ depreciated """ + return self.simulation.time + + def getSimulation(self): + return self.simulation + + def isAlive(self): + return self.alive + + def addCash(self, amount): + """ depreciated """ + self.create('money', amount, 1) + + def create(self, good, amount, value): + """ Creates a good and books it into the according account with + the value value + + Args: + good + + amount + + value + This function overwrites the abce.Agent's create function""" + self.mainLedger.create(good, amount, value) + + def destroy(self, good, amount, value=None): + """ deletes a good and books it into the according account. + If no value is specified, the book value is calculated: + account_value / number of goods. + + Args: + good + + amount + + value (optional) + + This function overwrites the abce.Agent's delete function""" + self.mainLedger.destroy(good, amount, value) + + def getCash_(self): + """ depreciated """ + return self.mainLedger.inventory['money'] + + def getMainLedger(self): + """ depreciated """ + return self.mainLedger + + def _step(self): + """ Processes received obligations """ + self.obligationsMailbox._step() + + def sendObligation(self, recipient, obligation): + """ sends an obligation to an agent """ + if isinstance(obligation, ObligationMessage): + self.send(recipient.group, '!oblmsg', obligation) + self.obligationsMailbox.addToObligationOutbox(obligation) + else: + msg = ObligationMessage(self, obligation) + self.send(recipient, '!oblmsg', msg) + + def printMailbox(self): + """ Prints all obligations in the mailbox """ + self.obligationsMailbox.printMailbox() + + def message(self, receiver, topic, content): + """ depreciated use ABCE's send method """ + if not isinstance(receiver, tuple): + receiver = receiver.name + super().send(receiver, topic, content) + + def get_obligation_outbox(self): + """ Prints all obligations in the outbox """ + return self.obligationsMailbox.getObligation_outbox() + + def sell_asset(self, asset, quantity, value, price, receiver=None): + """ Sells a quantity of an asset for a price. And books it + accordingly. If no receiver is specified, the good is sold to + a party outside of the model. + """ + self.ledger.sell_asset(quantity, asset) + + + + diff --git a/economicsl/contract.py b/economicsl/contract.py new file mode 100644 index 0000000..ae2b799 --- /dev/null +++ b/economicsl/contract.py @@ -0,0 +1,21 @@ +class Contract: + """ a contract must minimally implement the following functions""" + def getAssetParty(self): + """ Returns the asset party as a tuple (group, ID) e.G. ('bank', 5) """ + pass + + def getLiabilityParty(self): + """ Returns the liability party as a tuple (group, ID) e.G. ('bank', 5) """ + pass + + def getValue(self, me): + """ Returns the value of the contract conditional on the :code:`me` parameter. + Me is a name tuple to determine, whether the valuating is from the perspective + of the asset or liability side """ + pass + + def getAvailableActions(self, me): + pass + + def getName(self, me): + pass diff --git a/economicsl/contracts.py b/economicsl/contracts.py new file mode 100644 index 0000000..4e9a82b --- /dev/null +++ b/economicsl/contracts.py @@ -0,0 +1,11 @@ +from collections import defaultdict + + +class Contracts: + """ Collection of all contracts an agent holds. Contracts can be on the + asset or the liability side """ + def __init__(self): + self.allAssets = defaultdict(list) + """ contains all assets in a dictionary, with the assettype as a key """ + self.allLiabilities = defaultdict(list) + """ contains all liabilities in a dictionary, with the assettype as a key """ diff --git a/economicsl/ledger.py b/economicsl/ledger.py new file mode 100644 index 0000000..7bf2835 --- /dev/null +++ b/economicsl/ledger.py @@ -0,0 +1,343 @@ +""" This is the main class implementing double entry org.economicsl.accounting. All public operations provided by this class +are performed as a double entry operation, i.e. a pair of (dr, cr) operations. + +A Ledger contains a set of accounts, and is the interface between an agent and its accounts. Agents cannot +directly interact with accounts other than via a Ledger. + +At the moment, a Ledger contains an account for each type of contract, plus an equity account and a cash account. +It also has a book for every type of good it holds. Good accounts are not to be confused with the inventory. + +A simple economic agent will usually have a single Ledger, whereas complex firms and banks can have several books +(as in branch banking for example). + +There are three different things: +1. The contracts which is an inventory of contracts. +3. The inventory which is the inventory of physical goods +3. The accounts which running value of each contract type or good type + +Every operation either changes contracts and accounts or inventory and accounts. +Except for reevaluation of a good or a contract which does only affect the accounts. + +""" +from .contracts import Contracts +from .account import Account +from .accounttype import AccountType +from math import isclose + +class Ledger: + def __init__(self, me, inventory): + """ A StressLedger is a list of accounts (for quicker searching) + + Each Account includes an inventory to hold one type of contract. + These hashmaps are used to access the correct account for a given type of contract. + Note that separate hashmaps are needed for asset accounts and liability accounts: the same contract + type (such as Loan) can sometimes be an asset and sometimes a liability. + + A book is initially created with a cash account (it's the simplest possible book) + """ + self.assetAccounts = {} # a dict from a contract to a assetAccount + self.inventory = inventory + self.contracts = Contracts() + self.goodsAccounts = {} + self.liabilityAccounts = {} # a dict from a contract to a liabilityAccount + self.me = me + + def getAssetValue(self): + """ Value of all assets and goods. + Whether they are booked as assets or liabilities depends on the value at acquisition, + not the current value """ + return (sum([aa.getBalance() for aa in self.assetAccounts.values()]) + + sum([aa.getBalance() for aa in self.goodsAccounts.values()])) + + def getLiabilityValue(self): + """ Value of all Liabilities. + Whether they are booked as assets or liabilities depends on the value at acquisition, + not the current value """ + return sum([la.getBalance() for la in self.liabilityAccounts.values()]) + + def getEquityValue(self): + return self.getAssetValue() - self.getLiabilityValue() + + def getAssetValueOf(self, contractType): + """ Evaluates all contracts of a certain type, given that there are treated as + assets, and returns their value + Not to be confused with the book value. + Whether they are booked as assets or liabilities depends on the value at acquisition, + not the current value""" + return sum((c.getValue() for c in self.contracts.allAssets[contractType])) + + def getLiabilityValueOf(self, contractType): + """ Evaluates all contracts of a certain type, given that they are treated + as liabilities, and returns their value. Not to be confused with the book value. + Whether they are booked as assets or liabilities depends on the value at acquisition, + not the current value""" + return sum((c.getValue() for c in self.contracts.allLiabilities[contractType])) + + def getAllAssets(self): + """ returns all contracts assets """ + return [item for sublist in self.contracts.allAssets.values() for item in sublist] + + def getAllLiabilities(self): + """ return all contracts that are liabilities """ + return [item for sublist in self.contracts.allLiabilities.values() for item in sublist] + + def getAssetsOfType(self, contractType): + """ returns all contracts of a certain type that are booked as assets, + whether they are booked as assets or liabilities depends on the value at acquisition, + not the current value. + """ + return self.contracts.allAssets[contractType] + + def getLiabilitiesOfType(self, contractType): + """ returns all contracts of a certain type that are booked as liabilities, + whether they are booked as assets or liabilities depends on the value at acquisition, + not the current value. + """ + return self.contracts.allLiabilities[contractType] + + def _addAccount(self, account, contractType): + """ adds a new account """ + switch = account.getAccountType() + if switch == AccountType.ASSET: + assert contractType not in self.assetAccounts + self.assetAccounts[contractType] = account + elif switch == AccountType.LIABILITY: + assert contractType not in self.liabilityAccounts + self.liabilityAccounts[contractType] = account + + # Not sure what to do with INCOME, EXPENSES + + def addAsset(self, contract): + """ Adding an asset means debiting the account relevant to that type of contract + and crediting equity. + Args: + + contract: + an Asset contract to add """ + assetAccount = self.assetAccounts.get(type(contract)) + + if assetAccount is None: + # If there doesn't exist an Account to hold this type of contract, we create it + assetAccount = Account(contract.getName(self.me), AccountType.ASSET) + self._addAccount(assetAccount, type(contract)) + + assetAccount.debit(contract.getValue()) + + self.contracts.allAssets[type(contract)].append(contract) + + def addLiability(self, contract): + """ Adding a liability means debiting equity and crediting the account + relevant to that type of contract. + @param contract a Liability contract to add """ + liabilityAccount = self.liabilityAccounts.get(type(contract)) + + if liabilityAccount is None: + # If there doesn't exist an Account to hold this type of contract, we create it + liabilityAccount = Account(contract.getName(self.me), AccountType.LIABILITY) + self._addAccount(liabilityAccount, type(contract)) + + liabilityAccount.credit(contract.getValue()) + + + self.contracts.allLiabilities[type(contract)].append(contract) + + def create(self, name, amount, value): + """ Creates a good and books it into the according account with + the value value + + Args: + good + + amount + + value + This function overwrites the abce.Agent's create function """ + self.inventory.create(name, amount) + physicalthingsaccount = self.getGoodsAccount(name) + physicalthingsaccount.debit(amount * value) + + def destroy(self, name, amount, value=None): + """ deletes a good and books it into the according account. + If no value is specified, the book value is calculated: + account_value / number of goods. + + Args: + good + + amount + + value (optional) + + This function overwrites the abce.Agent's delete function""" + if value is None: + try: + value = self.getPhysicalThingValue(name) + self.destroy(name, amount, value) + except: + raise NotEnoughGoods(name, 0, amount) + else: + self.inventory.destroy(name, amount) + self.getGoodsAccount(name).credit(amount * value) + + def getGoodsAccount(self, name): + """ access to a particular goods account """ + account = self.goodsAccounts.get(name) + if account is None: + account = Account(name, AccountType.GOOD) + self.goodsAccounts[name] = account + return account + + def getPhysicalThingValue(self, name): + """ Returns the value of a physical thing """ + try: + return self.getGoodsAccount(name).getBalance() / self.inventory.getGood(name) + except: + return 0.0 + + def revalueGoods(self, name, value): + """ Reevaluates the current stock of physical goods at a specified value and books. + """ + old_value = self.getGoodsAccount(name).getBalance() + new_value = self.inventory.getGood(name) * value + if (new_value > old_value): + self.getGoodsAccount(name).debit(new_value - old_value) + elif (new_value < old_value): + self.getGoodsAccount(name).credit(old_value - new_value) + + def addCash(self, amount) -> None: + """ depreciated adds "money" as a physical good """ + # (dr cash, cr equity) + self.create("money", amount, 1.0) + + def subtractCash(self, amount) -> None: + self.destroy("money", amount, 1.0) + + def payLiability(self, amount, loan): + """ Operation to pay back a liability loan; debit liability and credit cash + + Args: + amount: + amount to pay back + + loan: + the loan which is being paid back """ + self.liabilityAccount = self.liabilityAccounts.get(loan) + + assert self.inventory.getCash() >= amount # Pre-condition: liquidity has been raised. + + # (dr liability, cr cash ) + doubleEntry(self.liabilityAccount, self['money'], amount) + + def sell_asset(self, amount, asset): + """ Books the sales of an asset. + Args: + amount: + the *value* of the asset + """ + assert amount <= asset.quantity + asset.quantity -= amount + if isclose(asset.quantity, 0): + self.contracts.allAssets(type(asset)).remove(asset) + assetAccount = self.assetAccounts[type(asset)] + + # (dr cash, cr asset) + doubleEntry(self["money"], assetAccount, amount) + + def pullFunding(self, amount, loan): + """Operation to cancel a Loan to someone (i.e. cash in a Loan in the Assets side). + + I'm using this for simplicity but note that this is equivalent to selling an asset. + + Args: + amount: + the amount of loan that is cancelled + """ + loanAccount = self.getAccountFromContract(loan) + # (dr cash, cr asset ) + doubleEntry(self.getCashAccount(), loanAccount, amount) + # TODO make sure that adds money + + def printBalanceSheet(self, me): + """ prints a balance sheet """ + print("Asset accounts:\n---------------") + for a in self.assetAccounts.values(): + print(a.getName(), "-> %.2f" % a.getBalance()) + + print("Breakdown: ") + for c in self.contracts.allAssets: + print("\t", c.getName(me), " > ", c.getValue()) + print("TOTAL ASSETS: %.2f" % self.getAssetValue()) + + print("\nLiability accounts:\n---------------") + for a in self.liabilityAccounts.values(): + print(a.getName(), " -> %.2f" % a.getBalance()) + for c in self.contracts.allLiabilities: + print("\t", c.getName(me), " > ", c.getValue()) + print("TOTAL LIABILITIES: %.2f" % self.getLiabilityValue()) + print("\nTOTAL EQUITY: %.2f" % self.getEquityValue()) + + print("\nSummary of encumbered collateral:") + # for (Contract contract : getLiabilitiesOfType(Repo.class)) { + # ((Repo) contract).printCollateral(); + # } + print("\n\nTotal cash: ", self.getGoodsAccount("cash").getBalance()) + # print("Encumbered cash: "+me.getEncumberedCash()); + # print("Unencumbered cash: " + (me.getCash_() - me.getEncumberedCash())); + + def getInitialEquity(self): + return self.initialEquity + + def setInitialValues(self): + self.initialEquity = self.getEquityValue() + + def getAccountFromContract(self, contract): + return self.assetAccounts.get(contract) + + def getCashAccount(self): + return self["money"] + + def devalueAsset(self, asset, delta_value): + """ if an Asset loses value, I must debit equity and credit asset + Args: + asset + + delta_value: + the value lost + """ + self.assetAccounts[type(asset)].credit(delta_value) + + # Todo: perform a check here that the Asset account balances match the value of the assets. (?) + + def appreciateAsset(self, asset, delta_value): + """ appreciates a certain asset + Args: + asset + + delta_value: + the value gained + """ + self.assetAccounts[type(asset)].debit(delta_value) + + def devalueLiability(self, liability, delta_value): + """ devalues a certain liability + Args: + liability + + delta_value: + the value lost + """ + self.liabilityAccounts[type(liability)].debit(delta_value) + + def appreciateLiability(self, liability, delta_value): + """ appreciates a certain liability + Args: + liability + + delta_value: + the value gained + """ + + self.liabilityAccounts[type(liability)].credit(delta_value) + + + diff --git a/economicsl/obligationmessage.py b/economicsl/obligationmessage.py new file mode 100644 index 0000000..1821cc7 --- /dev/null +++ b/economicsl/obligationmessage.py @@ -0,0 +1,16 @@ +class ObligationMessage: + def __init__(self, sender, message) -> None: + self.sender = sender + self.message = message + self._is_read = False + + def getSender(self): + return self.sender + + def getMessage(self): + self._is_read = True + return self.message + + def is_read(self) -> bool: + return self._is_read + diff --git a/economicsl/obligations.py b/economicsl/obligations.py index 278d348..8b2840a 100644 --- a/economicsl/obligations.py +++ b/economicsl/obligations.py @@ -1,9 +1,6 @@ -import numpy as np - - class Obligation: - def __init__(self, contract, amount: np.longdouble, timeLeftToPay: int) -> None: - self.amount = np.longdouble(amount) + def __init__(self, contract, amount, timeLeftToPay: int) -> None: + self.amount = amount self.from_ = contract.getLiabilityParty() self.to = contract.getAssetParty() @@ -21,7 +18,7 @@ def __init__(self, contract, amount: np.longdouble, timeLeftToPay: int) -> None: def fulfil(self): pass - def getAmount(self) -> np.longdouble: + def getAmount(self): return self.amount def isFulfilled(self) -> bool: @@ -58,109 +55,3 @@ def printObligation(self) -> None: self.getTimeToReceive()) -class ObligationMessage: - def __init__(self, sender, message) -> None: - self.sender = sender - self.message = message - self._is_read = False - - def getSender(self): - return self.sender - - def getMessage(self): - self._is_read = True - return self.message - - def is_read(self) -> bool: - return self._is_read - - -class ObligationsAndGoodsMailbox: - def __init__(self, owner) -> None: - self.obligation_unopened = [] - self.obligation_outbox = [] - self.obligation_inbox = [] - self.obligationMessage_unopened = [] - self.obligationMessage_inbox = [] - self.goods_inbox = [] - self.owner = owner - - - def addToObligationOutbox(self, obligation) -> None: - self.obligation_outbox.append(obligation) - - def getMaturedObligations(self) -> np.longdouble: - return sum([o.getAmount() for o in self.obligation_inbox if o.isDue() and not o.isFulfilled()]) - - def getAllPendingObligations(self) -> np.longdouble: - return sum([o.getAmount() for o in self.obligation_inbox if not o.isFulfilled()]) - - def getPendingPaymentsToMe(self) -> np.longdouble: - return sum([o.getAmount() for o in self.obligation_outbox if o.isFulfilled()]) - - def fulfilAllRequests(self) -> None: - for o in self.obligation_inbox: - if not o.isFulfilled(): - o.fulfil() - - def fulfilMaturedRequests(self) -> None: - for o in self.obligation_inbox: - if o.isDue() and not o.isFulfilled(): - o.fulfil() - - def step(self) -> None: - self.obligation_inbox.extend(self.owner.get_messages('!oblmsg')) - # Remove all fulfilled requests - self.obligation_inbox = [o for o in self.obligation_inbox if not o.isFulfilled()] - self.obligation_outbox = [o for o in self.obligation_outbox if not o.isFulfilled()] - - # Remove all requests from agents who have defaulted. - # TODO should be in model not in the library - self.obligation_outbox = [o for o in self.obligation_outbox if o.getFrom().isAlive()] - - # Move all messages in the obligation_unopened to the obligation_inbox - self.obligation_inbox += [o for o in self.obligation_unopened if o.hasArrived()] - self.obligation_unopened = [o for o in self.obligation_unopened if not o.hasArrived()] - - # Remove all fulfilled requests - self.obligationMessage_inbox = [o for o in self.obligationMessage_inbox if not o.is_read()] - - # Move all messages in the obligation_unopened to the obligation_inbox - self.obligationMessage_inbox += list(self.obligationMessage_unopened) - self.obligationMessage_unopened = [] - - # Remove all fulfilled requests - assert not self.goods_inbox - - # Move all messages in the obligation_unopened to the obligation_inbox - - def printMailbox(self) -> None: - if ((not self.obligation_unopened) and (not self.obligation_inbox) and - (not self.obligation_outbox)): - print("\nObligationsAndGoodsMailbox is empty.") - else: - print("\nObligationsAndGoodsMailbox contents:") - if not (not self.obligation_unopened): - print("Unopened messages:") - for o in self.obligation_unopened: - o.printObligation() - - if not (not self.obligation_inbox): - print("Inbox:") - for o in self.obligation_inbox: - o.printObligation() - - if not (not self.obligation_outbox): - print("Outbox:") - for o in self.obligation_outbox: - o.printObligation() - print() - - def getMessageInbox(self): - return self.obligationMessage_inbox - - def getObligation_outbox(self): - return self.obligation_outbox - - def getObligation_inbox(self): - return self.obligation_inbox diff --git a/economicsl/obligationsmailbox.py b/economicsl/obligationsmailbox.py new file mode 100644 index 0000000..68d6d16 --- /dev/null +++ b/economicsl/obligationsmailbox.py @@ -0,0 +1,90 @@ + +class ObligationsMailbox: + def __init__(self, owner) -> None: + self.obligation_unopened = [] + self.obligation_outbox = [] + self.obligation_inbox = [] + self.obligationMessage_unopened = [] + self.obligationMessage_inbox = [] + self.goods_inbox = [] + self.owner = owner + + + def addToObligationOutbox(self, obligation) -> None: + self.obligation_outbox.append(obligation) + + def getMaturedObligations(self): + return sum([o.getAmount() for o in self.obligation_inbox if o.isDue() and not o.isFulfilled()]) + + def getAllPendingObligations(self): + return sum([o.getAmount() for o in self.obligation_inbox if not o.isFulfilled()]) + + def getPendingPaymentsToMe(self): + return sum([o.getAmount() for o in self.obligation_outbox if o.isFulfilled()]) + + def fulfilAllRequests(self) -> None: + for o in self.obligation_inbox: + if not o.isFulfilled(): + o.fulfil() + + def fulfilMaturedRequests(self) -> None: + for o in self.obligation_inbox: + if o.isDue() and not o.isFulfilled(): + o.fulfil() + + def _step(self) -> None: + self.obligation_inbox.extend(self.owner.get_messages('!oblmsg')) + # Remove all fulfilled requests + self.obligation_inbox = [o for o in self.obligation_inbox if not o.isFulfilled()] + self.obligation_outbox = [o for o in self.obligation_outbox if not o.isFulfilled()] + + # Remove all requests from agents who have defaulted. + # TODO should be in model not in the library + self.obligation_outbox = [o for o in self.obligation_outbox if o.getFrom().isAlive()] + + # Move all messages in the obligation_unopened to the obligation_inbox + self.obligation_inbox += [o for o in self.obligation_unopened if o.hasArrived()] + self.obligation_unopened = [o for o in self.obligation_unopened if not o.hasArrived()] + + # Remove all fulfilled requests + self.obligationMessage_inbox = [o for o in self.obligationMessage_inbox if not o.is_read()] + + # Move all messages in the obligation_unopened to the obligation_inbox + self.obligationMessage_inbox += list(self.obligationMessage_unopened) + self.obligationMessage_unopened = [] + + # Remove all fulfilled requests + assert not self.goods_inbox + + # Move all messages in the obligation_unopened to the obligation_inbox + + def printMailbox(self) -> None: + if ((not self.obligation_unopened) and (not self.obligation_inbox) and + (not self.obligation_outbox)): + print("\nObligationsAndGoodsMailbox is empty.") + else: + print("\nObligationsAndGoodsMailbox contents:") + if not (not self.obligation_unopened): + print("Unopened messages:") + for o in self.obligation_unopened: + o.printObligation() + + if not (not self.obligation_inbox): + print("Inbox:") + for o in self.obligation_inbox: + o.printObligation() + + if not (not self.obligation_outbox): + print("Outbox:") + for o in self.obligation_outbox: + o.printObligation() + print() + + def getMessageInbox(self): + return self.obligationMessage_inbox + + def getObligation_outbox(self): + return self.obligation_outbox + + def getObligation_inbox(self): + return self.obligation_inbox diff --git a/setup.py b/setup.py index c70f3b4..819a91f 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,4 @@ +"""Installs abcesl, the contracting and accounting package for ABCE""" from setuptools import setup @@ -9,4 +10,5 @@ author_email='rhtbot@protonmail.com', license='MIT', packages=['economicsl'], - zip_safe=False) + zip_safe=False, + install_requires=['abce'])