Browse Source

Rename bonds to credits, enforce a minimum reserve

Eren Yilmaz 5 years ago
parent
commit
87cd8843dd
8 changed files with 141 additions and 97 deletions
  1. 29 10
      client_controller.py
  2. 6 6
      db_setup/create_triggers.py
  3. 1 1
      db_setup/tables.py
  4. 2 2
      doc/documentation.py
  5. 75 61
      model.py
  6. 3 2
      routes.py
  7. 16 6
      server_controller.py
  8. 9 9
      test/do_some_requests/__init__.py

+ 29 - 10
client_controller.py

@@ -411,22 +411,41 @@ def loans():
             print('Order access failed.')
 
 
-def bonds():
+def credits():
     _fake_loading_bar('Loading Data', duration=1.6)
-    response = client_request('bonds', {"session_id": connection.session_id})
+    response = client_request('credits', {"session_id": connection.session_id})
     success = 'data' in response
     if success:
-        for row in response['data']:
-            row[1] = f'{row[1] * 100:.4f}%'
-        print(my_tabulate(response['data'],
-                          headers=['Bond', 'Coupon', 'Maturity', 'Issuer', ],
-                          floatfmt='.2f',
-                          tablefmt="pipe"))
+        _print_credits_table(response)
+    else:
+        if 'error' in response:
+            print('Listing credits failed with message:', response['error'])
+        else:
+            print('Listing credits failed.')
+
+
+def mro_qualified_credits():
+    print('This command lists credits that you can sell to the central bank during the next mean refinancing operations.')
+    print('The schedule is called tender calendar and visible through by command `tender_calendar`.')
+    _fake_loading_bar('Loading Data', duration=1.6)
+    response = client_request('credits', {"session_id": connection.session_id, 'only_next_mro_qualified': True})
+    success = 'data' in response
+    if success:
+        _print_credits_table(response)
     else:
         if 'error' in response:
-            print('Listing bonds failed with message:', response['error'])
+            print('Listing credits failed with message:', response['error'])
         else:
-            print('Listing bonds failed.')
+            print('Listing credits failed.')
+
+
+def _print_credits_table(response):
+    for row in response['data']:
+        row[1] = f'{row[1] * 100:.4f}%'
+    print(my_tabulate(response['data'],
+                      headers=['Bond', 'Coupon', 'Maturity', 'Issuer', ],
+                      floatfmt='.2f',
+                      tablefmt="pipe"))
 
 
 def repay_loan(loan_id=None, amount=None):

+ 6 - 6
db_setup/create_triggers.py

@@ -86,9 +86,9 @@ def create_triggers(cursor: sqlite3.Cursor):
                 WHEN (SELECT 
                     NOT NEW.buy -- is a selling order
                     AND NOT EXISTS(
-                        SELECT * FROM bonds
-                        WHERE bonds.issuer_id = u.rowid
-                        AND bonds.ownable_id = o.ownable_id) -- not an self-issued bond
+                        SELECT * FROM credits
+                        WHERE credits.issuer_id = u.rowid
+                        AND credits.ownable_id = o.ownable_id) -- not an self-issued bond
                     AND u.username != '{BANK_NAME}' -- bank may sell any amount
                     AND 0 >
                         -- owned_amount
@@ -113,9 +113,9 @@ def create_triggers(cursor: sqlite3.Cursor):
                 WHEN (SELECT 
                     NOT NEW.buy -- is a selling order
                     AND NOT EXISTS(
-                        SELECT * FROM bonds
-                        WHERE bonds.issuer_id = u.rowid
-                        AND bonds.ownable_id = o.ownable_id) -- not an self-issued bond
+                        SELECT * FROM credits
+                        WHERE credits.issuer_id = u.rowid
+                        AND credits.ownable_id = o.ownable_id) -- not an self-issued bond
                     AND u.username != '{BANK_NAME}' -- bank may sell any amount
                     AND 0 >
                         -- owned_amount

+ 1 - 1
db_setup/tables.py

@@ -122,7 +122,7 @@ def tables(cursor):
                 )
                 ''')
     cursor.execute('''
-                CREATE TABLE IF NOT EXISTS bonds(
+                CREATE TABLE IF NOT EXISTS credits(
                     rowid INTEGER PRIMARY KEY,
                     issuer_id INTEGER NOT NULL REFERENCES users(rowid),
                     ownable_id INTEGER UNIQUE NOT NULL REFERENCES ownables(rowid),

+ 2 - 2
doc/documentation.py

@@ -5,8 +5,8 @@
 # {"email": "user123@example.org", "username": "user123", "password": "FILTERED", "preferred_language": "german"}
 # while a valid request to /events would be the empty object {}
 
-bonds_required_attributes = []
-bonds_possible_attributes = ['issuer', 'only_next_mro_qualified']
+credits_required_attributes = []
+credits_possible_attributes = ['issuer', 'only_next_mro_qualified']
 
 buy_banking_license_required_attributes = ['session_id']
 buy_banking_license_possible_attributes = ['session_id']

+ 75 - 61
model.py

@@ -6,7 +6,7 @@ import sqlite3 as db
 import uuid
 from datetime import datetime
 from logging import INFO
-from math import floor
+from math import floor, inf
 from shutil import copyfile
 from typing import Optional, Dict
 
@@ -177,7 +177,7 @@ def send_ownable(from_user_id, to_user_id, ownable_id, amount):
 
     own(to_user_id, ownable_name_by_id(ownable_id))
 
-    if to_user_id != bank_id_ and not is_bond_of_user(ownable_id, to_user_id):
+    if not is_bond_of_user(ownable_id, to_user_id):
         execute('''
                     UPDATE ownership
                     SET amount = amount + ?
@@ -352,17 +352,17 @@ def next_mro_dt(dt=None):
     if dt is None:
         dt = current_db_timestamp()
     return execute('''
-    SELECT MIN(t.maturity_dt) FROM tender_calendar t WHERE t.maturity_dt > ?
+    SELECT MIN(t.dt) FROM tender_calendar t WHERE t.dt > ?
     ''', (dt,)).fetchone()[0]
 
 
 def next_mro_interest(dt=None):
     return execute('''
-    SELECT t.mro_interest FROM tender_calendar t WHERE t.maturity_dt = ?
+    SELECT t.mro_interest FROM tender_calendar t WHERE t.dt = ?
     ''', (next_mro_dt(dt),)).fetchone()[0]
 
 
-def bonds(issuer_id=None, only_next_mro_qualified=False):
+def credits(issuer_id=None, only_next_mro_qualified=False):
     if issuer_id is not None:
         issuer_condition = 'issuer.rowid = ?'
         issuer_params = (issuer_id,)
@@ -370,14 +370,14 @@ def bonds(issuer_id=None, only_next_mro_qualified=False):
         issuer_condition = '1'
         issuer_params = ()
     if only_next_mro_qualified:
-        only_next_mro_condition = ''' -- noinspection SqlResolve @ any/"bonds"
+        only_next_mro_condition = ''' -- noinspection SqlResolve @ any/"credits"
             SELECT EXISTS(
                 SELECT *
                 FROM banks b
-                JOIN tender_calendar t ON t.maturity_dt = bonds.maturity_dt
-                WHERE bonds.issuer_id = b.user_id
-                AND bonds.coupon >= t.mro_interest
-                AND t.maturity_dt = ?
+                JOIN tender_calendar t ON t.maturity_dt = credits.maturity_dt
+                WHERE credits.issuer_id = b.user_id
+                AND credits.coupon >= t.mro_interest
+                AND t.dt = ?
             )
             '''
         only_next_mro_params = (next_mro_dt(),)
@@ -390,9 +390,9 @@ def bonds(issuer_id=None, only_next_mro_qualified=False):
             coupon,
             datetime(maturity_dt, 'unixepoch', 'localtime'),
             username           
-        FROM bonds
-        JOIN ownables o on bonds.ownable_id = o.rowid
-        JOIN users issuer on bonds.issuer_id = issuer.rowid
+        FROM credits
+        JOIN ownables o on credits.ownable_id = o.rowid
+        JOIN users issuer on credits.issuer_id = issuer.rowid
         WHERE ({issuer_condition})
         AND ({only_next_mro_condition})
         ORDER BY coupon * (maturity_dt - ?) DESC
@@ -455,7 +455,7 @@ def available_amount(user_id, ownable_id):
 def is_bond_of_user(ownable_id, user_id):
     execute('''
     SELECT EXISTS(
-        SELECT * FROM bonds 
+        SELECT * FROM credits 
         WHERE ownable_id = ?
         AND issuer_id = ?
     )
@@ -464,26 +464,33 @@ def is_bond_of_user(ownable_id, user_id):
     return current_cursor.fetchone()[0]
 
 
-def user_has_at_least_available(amount, user_id, ownable_id):
+def user_available_ownable(user_id, ownable_id):
     if is_bond_of_user(ownable_id, user_id):
-        return True
+        return inf
 
-    if not isinstance(amount, float) and not isinstance(amount, int):
-        # comparison of float with strings does not work so well in sql
-        raise AssertionError()
+    if ownable_id == currency_id() and user_has_banking_license(user_id):
+        minimum_reserve = required_minimum_reserve(user_id) + sell_ordered_amount(user_id, ownable_id)
+    else:
+        minimum_reserve = sell_ordered_amount(user_id, ownable_id)
 
     execute('''
-                SELECT rowid
+                SELECT amount
                 FROM ownership
                 WHERE user_id = ?
                 AND ownable_id = ?
-                AND amount - ? >= ?
-                ''', (user_id, ownable_id, sell_ordered_amount(user_id, ownable_id), amount))
+                ''', (user_id, ownable_id))
+
+    return current_cursor.fetchone()[0] - minimum_reserve
+
+
+
+def user_has_at_least_available(amount, user_id, ownable_id):
+    if not isinstance(amount, float) and not isinstance(amount, int):
+        # comparison of float with strings does not work so well in sql
+        raise ValueError()
+
+    return user_available_ownable(user_id, ownable_id) >= amount
 
-    if current_cursor.fetchone():
-        return True
-    else:
-        return False
 
 
 def news():
@@ -579,14 +586,7 @@ def currency_id():
 
 
 def user_money(user_id):
-    execute('''
-        SELECT amount
-        FROM ownership
-        WHERE user_id = ?
-        AND ownable_id = ?
-        ''', (user_id, currency_id()))
-
-    return current_cursor.fetchone()[0]
+    return user_available_ownable(user_id, currency_id())
 
 
 def delete_order(order_id, new_order_status):
@@ -1268,21 +1268,21 @@ def pay_bond_interest(until=None):
     SELECT 
         SUM(amount * coupon * (MIN(CAST(? AS FLOAT), maturity_dt) - last_interest_pay_dt) / ?) AS interest_since_last_pay,
         o.user_id AS to_user_id,
-        bonds.issuer_id AS from_user_id
-    FROM bonds
-    JOIN ownership o on bonds.ownable_id = o.ownable_id
+        credits.issuer_id AS from_user_id
+    FROM credits
+    JOIN ownership o on credits.ownable_id = o.ownable_id
     WHERE ? - last_interest_pay_dt > ? OR ? > maturity_dt -- every interval or when the bond expired
     AND amount != 0
-    GROUP BY o.user_id, bonds.issuer_id
+    GROUP BY o.user_id, credits.issuer_id
     ''', (current_dt, sec_per_year, current_dt, MIN_INTEREST_INTERVAL, current_dt)).fetchall()
 
-    matured_bonds = execute('''
+    matured_credits = execute('''
     SELECT 
         amount,
         o.user_id AS to_user_id,
-        bonds.issuer_id AS from_user_id
-    FROM bonds
-    JOIN ownership o on bonds.ownable_id = o.ownable_id
+        credits.issuer_id AS from_user_id
+    FROM credits
+    JOIN ownership o on credits.ownable_id = o.ownable_id
     WHERE ? > maturity_dt
     ''', (current_dt,)).fetchall()
 
@@ -1290,21 +1290,21 @@ def pay_bond_interest(until=None):
     for amount, to_user_id, from_user_id in interests:
         send_ownable(from_user_id, to_user_id, currency_id(), amount)
 
-    # pay back matured bonds
-    for amount, to_user_id, from_user_id in matured_bonds:
+    # pay back matured credits
+    for amount, to_user_id, from_user_id in matured_credits:
         send_ownable(from_user_id, to_user_id, currency_id(), amount)
 
     execute('''
-    UPDATE bonds 
+    UPDATE credits 
     SET last_interest_pay_dt = ?
     WHERE ? - last_interest_pay_dt > ?''', (current_dt, current_dt, MIN_INTEREST_INTERVAL,))
 
-    # delete matured bonds
+    # delete matured credits
     execute('''
     DELETE FROM transactions 
     WHERE ownable_id IN (
         SELECT ownable_id
-        FROM bonds
+        FROM credits
         WHERE ? > maturity_dt
     )
     ''', (current_dt,))
@@ -1312,8 +1312,8 @@ def pay_bond_interest(until=None):
     DELETE FROM orders 
     WHERE ownership_id IN (
         SELECT o2.rowid
-        FROM bonds
-        JOIN ownables o on bonds.ownable_id = o.rowid
+        FROM credits
+        JOIN ownables o on credits.ownable_id = o.rowid
         JOIN ownership o2 on o.rowid = o2.ownable_id
         WHERE ? > maturity_dt
     )
@@ -1322,8 +1322,8 @@ def pay_bond_interest(until=None):
     DELETE FROM order_history
     WHERE ownership_id IN (
         SELECT o2.rowid
-        FROM bonds
-        JOIN ownables o on bonds.ownable_id = o.rowid
+        FROM credits
+        JOIN ownables o on credits.ownable_id = o.rowid
         JOIN ownership o2 on o.rowid = o2.ownable_id
         WHERE ? > maturity_dt
     )
@@ -1332,7 +1332,7 @@ def pay_bond_interest(until=None):
     DELETE FROM ownership 
     WHERE ownable_id IN (
         SELECT ownable_id
-        FROM bonds
+        FROM credits
         WHERE ? > maturity_dt
     )
     ''', (current_dt,))
@@ -1340,12 +1340,12 @@ def pay_bond_interest(until=None):
     DELETE FROM ownables 
     WHERE rowid IN (
         SELECT ownable_id
-        FROM bonds
+        FROM credits
         WHERE ? > maturity_dt
     )
     ''', (current_dt,))
     execute('''
-    DELETE FROM bonds 
+    DELETE FROM credits 
     WHERE ? > maturity_dt
     ''', (current_dt,))
 
@@ -1392,17 +1392,17 @@ def triggered_mros():
 
 
 def mro(mro_id, expiry, min_interest):
-    qualified_bonds = execute('''
-    SELECT bonds.ownable_id
-    FROM bonds
-    JOIN banks b ON bonds.issuer_id = b.user_id
-    JOIN ownership o ON o.ownable_id = bonds.ownable_id -- AND bonds.issuer_id = o.user_id
+    qualified_credits = execute('''
+    SELECT credits.ownable_id
+    FROM credits
+    JOIN banks b ON credits.issuer_id = b.user_id
+    JOIN ownership o ON o.ownable_id = credits.ownable_id -- AND credits.issuer_id = o.user_id
     JOIN orders o2 ON o.rowid = o2.ownership_id AND NOT o2.buy
     WHERE maturity_dt = ?
     AND coupon >= ?
     AND "limit" IS NULL or "limit" <= 1
     ''', (expiry, min_interest)).fetchall()
-    for ownable_id, amount in qualified_bonds:
+    for ownable_id, amount in qualified_credits:
         bank_order(buy=True,
                    ownable_id=ownable_id,
                    limit=1,
@@ -1475,16 +1475,30 @@ def tender_calendar():
     return execute('''
     SELECT dt, mro_interest, maturity_dt
     FROM tender_calendar
+    ORDER BY dt DESC
+    LIMIT 20
     ''', ).fetchall()
 
 
+def required_minimum_reserve(user_id):
+    assert user_has_banking_license(user_id)
+    borrowed_money = execute('''
+    SELECT SUM(amount)
+    FROM ownership
+    JOIN credits b on ownership.ownable_id = b.ownable_id
+    WHERE b.issuer_id = ?
+    AND ownership.user_id = ?
+    ''', (user_id, bank_id())).fetchone()[0]
+    return min(0, global_control_value('cash_reserve_ratio') * borrowed_money - global_control_value('cash_reserve_free_amount'))
+
+
 def issue_bond(user_id, ownable_name, coupon, maturity_dt):
     execute('''
     INSERT INTO ownables(name)
     VALUES (?)
     ''', (ownable_name,))
     execute('''
-    INSERT INTO bonds(issuer_id, ownable_id, coupon, maturity_dt) 
+    INSERT INTO credits(issuer_id, ownable_id, coupon, maturity_dt) 
     VALUES (?, (SELECT MAX(rowid) FROM ownables), ?, ?)
     ''', (user_id, coupon, maturity_dt))
 

+ 3 - 2
routes.py

@@ -20,7 +20,7 @@ valid_post_routes = {
     'repay_loan',
     'issue_bond',
     'loans',
-    'bonds',
+    'credits',
     'server_version',
     'tender_calendar',
 }
@@ -41,7 +41,7 @@ client_commands = ['help',
                    'login',
                    'register',
                    'change_pw',
-                   'bonds',
+                   'credits',
                    'news',
                    'tradables',
                    'take_out_personal_loan',
@@ -60,6 +60,7 @@ client_commands = ['help',
                    'issue_bond',
                    'buy_banking_license',
                    'tender_calendar',
+                   'mro_qualified_credits',
                    'leaderboard',
                    'exit',
                    ]

+ 16 - 6
server_controller.py

@@ -124,7 +124,7 @@ def order(json_request):
 
     if sell:
         if not model.user_has_at_least_available(amount, user_id, ownable_id):
-            return BadRequest('You can not sell more than you own.')
+            return BadRequest('You can not sell more than you own (this also takes into account existing sell orders and, if you are a bank, required minimum reserves at the ).')
     try:
         expiry = model.current_db_timestamp() + timedelta(minutes=time_until_expiration).total_seconds()
     except OverflowError:
@@ -175,7 +175,7 @@ def loans(json_request):
     return {'data': data}
 
 
-def bonds(json_request):
+def credits(json_request):
     if 'issuer' in json_request:
         issuer_id = model.get_user_id_by_name(json_request['issuer'])
     else:
@@ -186,7 +186,7 @@ def bonds(json_request):
             raise BadRequest
     else:
         only_next_mro_qualified = False
-    data = model.bonds(issuer_id, only_next_mro_qualified)
+    data = model.credits(issuer_id, only_next_mro_qualified)
     return {'data': data}
 
 
@@ -327,7 +327,11 @@ def repay_loan(json_request):
     if amount < 0:
         raise BadRequest('You can not repay negative amounts.')
     if model.user_money(user_id) < amount:
-        raise PreconditionFailed('You do not have enough money.')
+        if model.user_has_banking_license(user_id):
+            raise PreconditionFailed('You do not have enough money. '
+                                     'If you are a bank this also takes into account the minimum reserve you need to keep at the central bank.')
+        else:
+            raise PreconditionFailed('You do not have enough money.')
     if not model.loan_id_exists(loan_id) or model.loan_recipient_id(loan_id) != user_id:
         raise NotFound(f'You do not have a loan with that id.')
     loan_volume = model.loan_remaining_amount(loan_id)
@@ -349,14 +353,20 @@ def _before_request(_json_request):
         # pay interest rates for loans until this mro
         model.pay_loan_interest(until=mro_dt)
 
-        # pay interest rates for bonds until this mro
+        # pay interest rates for credits until this mro
         model.pay_bond_interest(until=mro_dt)
 
+        # pay deposit facility for minimum reserves until this mro
+        model.pay_deposit_facility(until=mro_dt)
+
         # handle MROs
         model.mro(mro_id, expiry, min_interest)
 
     # pay interest rates for loans until current time
     model.pay_loan_interest()
 
-    # pay interest rates for bonds until current time
+    # pay interest rates for credits until current time
     model.pay_bond_interest()
+
+    # pay deposit facility for minimum reserves until current time
+    model.pay_deposit_facility()

+ 9 - 9
test/do_some_requests/__init__.py

@@ -198,15 +198,15 @@ def run_tests():
         default_request_method(route, message)
 
         message = {'issuer': username}
-        route = 'bonds'
+        route = 'credits'
         default_request_method(route, message)
 
         message = {'issuer': username, 'only_next_mro_qualified': True}
-        route = 'bonds'
+        route = 'credits'
         default_request_method(route, message)
 
         message = {'issuer': username, 'only_next_mro_qualified': False}
-        route = 'bonds'
+        route = 'credits'
         default_request_method(route, message)
 
         message = {'session_id': session_ids[username],
@@ -217,27 +217,27 @@ def run_tests():
         default_request_method(route, message)
 
         message = {'issuer': username}
-        route = 'bonds'
+        route = 'credits'
         default_request_method(route, message)
 
         message = {'issuer': username, 'only_next_mro_qualified': True}
-        route = 'bonds'
+        route = 'credits'
         default_request_method(route, message)
 
         message = {'issuer': username, 'only_next_mro_qualified': False}
-        route = 'bonds'
+        route = 'credits'
         default_request_method(route, message)
 
     message = {}
-    route = 'bonds'
+    route = 'credits'
     default_request_method(route, message)
 
     message = {'only_next_mro_qualified': True}
-    route = 'bonds'
+    route = 'credits'
     default_request_method(route, message)
 
     message = {'only_next_mro_qualified': False}
-    route = 'bonds'
+    route = 'credits'
     default_request_method(route, message)
 
     for session_id in session_ids.values():