Eren Yilmaz преди 5 години
родител
ревизия
83372d2459
променени са 11 файла, в които са добавени 259 реда и са изтрити 46 реда
  1. 2 0
      db_setup/create_triggers.py
  2. 1 2
      db_setup/seeds/__init__.py
  3. 11 2
      db_setup/tables.py
  4. 4 4
      doc/documentation.py
  5. 8 3
      game.py
  6. 12 0
      jobs/analyze.py
  7. 12 0
      jobs/clear_wal.py
  8. 3 1
      jobs/run_multiple_jobs.py
  9. 147 23
      model.py
  10. 27 10
      server_controller.py
  11. 32 1
      test/do_some_requests/__init__.py

+ 2 - 0
db_setup/create_triggers.py

@@ -288,6 +288,8 @@ def create_triggers_that_restrict_rowid_update(cursor):
     ''')
     tables = [row[0] for row in cursor.fetchall()]
     for table_name in tables:
+        if table_name.startswith('sqlite_'):
+            continue
         cursor.execute('''-- noinspection SqlResolveForFile
                     CREATE TRIGGER IF NOT EXISTS restrict_rowid_update_on_{0}
                     AFTER UPDATE ON {0}

+ 1 - 2
db_setup/seeds/__init__.py

@@ -1,6 +1,6 @@
 from sqlite3 import Cursor
 
-from game import CURRENCY_NAME, MRO_NAME, BANK_NAME
+from game import CURRENCY_NAME, BANK_NAME
 
 
 def seed(cursor: Cursor):
@@ -12,7 +12,6 @@ def seed(cursor: Cursor):
                     VALUES (?)
                     ''', [
         (CURRENCY_NAME,),
-        (MRO_NAME,),
     ])
     # The bank/external investors
     cursor.execute('''

+ 11 - 2
db_setup/tables.py

@@ -102,12 +102,21 @@ def tables(cursor):
                     UNIQUE (value_name, dt)
                 )
                 ''')
+    cursor.execute('''
+                CREATE TABLE IF NOT EXISTS tender_calendar(
+                    rowid INTEGER PRIMARY KEY,
+                    dt TIMESTAMP UNIQUE NOT NULL,
+                    mro_interest CURRENCY NOT NULL,
+                    maturity_dt TIMESTAMP NOT NULL, 
+                    executed BOOLEAN NOT NULL DEFAULT FALSE
+                )
+                ''')
     cursor.execute('''
                 CREATE TABLE IF NOT EXISTS loans(
                     rowid INTEGER PRIMARY KEY,
                     user_id INTEGER NOT NULL REFERENCES users(rowid),
-                    total_amount CURRENCY NOT NULL, -- TODO trigger that total amount is strictly larger 0
-                    amount CURRENCY NOT NULL, -- TODO trigger that amount is strictly larger 0
+                    total_amount CURRENCY NOT NULL CHECK(total_amount > 0), 
+                    amount CURRENCY NOT NULL CHECK(amount > 0),
                     last_interest_pay_dt TIMESTAMP NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)),
                     interest_rate CURRENCY NOT NULL -- determined from the global value 'personal_loan_interest_rate'
                 )

+ 4 - 4
doc/documentation.py

@@ -5,11 +5,8 @@
 # {"email": "user123@example.org", "username": "user123", "password": "FILTERED", "preferred_language": "german"}
 # while a valid request to /events would be the empty object {}
 
-before_request_required_attributes = []
-before_request_possible_attributes = []
-
 bonds_required_attributes = []
-bonds_possible_attributes = []
+bonds_possible_attributes = ['issuer', 'only_next_mro_qualified']
 
 buy_banking_license_required_attributes = ['session_id']
 buy_banking_license_possible_attributes = ['session_id']
@@ -65,6 +62,9 @@ register_possible_attributes = ['password', 'username']
 repay_loan_required_attributes = ['amount', 'loan_id', 'session_id']
 repay_loan_possible_attributes = ['amount', 'loan_id', 'session_id']
 
+server_version_required_attributes = []
+server_version_possible_attributes = []
+
 take_out_personal_loan_required_attributes = ['amount', 'session_id']
 take_out_personal_loan_possible_attributes = ['amount', 'session_id']
 

+ 8 - 3
game.py

@@ -1,8 +1,9 @@
 from lib.db_log import DBLog
+from util import random_chars
 
 CURRENCY_NAME = "₭ollar"
-MRO_NAME = 'MRO'
-MRO_INTERVAL = 3 * 3600
+MRO_INTERVAL = 3 * 3600 # TODO somewhere expose this to the client, maybe in a tender calendar
+MRO_RUNNING_TIME = 6 * 3600  # TODO somewhere expose this to the client, maybe in a tender calendar
 CURRENCY_SYMBOL = "₭"
 MINIMUM_ORDER_AMOUNT = 1
 DEFAULT_ORDER_EXPIRY = 43200
@@ -13,4 +14,8 @@ ROOT_URL = "/orderer.zip"
 
 logger = DBLog()
 OWNABLE_NAME_PATTERN = r'[A-Z-a-z0-9]{1,6}'
-BANK_NAME = 'bank'
+BANK_NAME = 'bank'
+
+
+def random_ownable_name():
+    return random_chars(6)

+ 12 - 0
jobs/analyze.py

@@ -0,0 +1,12 @@
+import model
+from game import DB_NAME
+
+
+def run():
+    model.connect(DB_NAME)
+    model.execute('ANALYZE')
+    model.current_connection.commit()
+
+
+if __name__ == '__main__':
+    run()

+ 12 - 0
jobs/clear_wal.py

@@ -0,0 +1,12 @@
+import model
+from game import DB_NAME
+
+
+def run():
+    model.connect(DB_NAME)
+    model.execute('PRAGMA main.val_checkpoint(TRUNCATE)')
+    model.current_connection.commit()
+
+
+if __name__ == '__main__':
+    run()

+ 3 - 1
jobs/run_multiple_jobs.py

@@ -1,12 +1,14 @@
 from datetime import datetime
 
+from jobs import analyze, clear_wal
 from util import main_wrapper
 
 
 @main_wrapper
 def main():
     schedule = [
-        # TODO fill
+        analyze,
+        clear_wal,
     ]
     for job in schedule:
         print('Starting', job.__name__, datetime.now().strftime("%H:%M:%S"))

+ 147 - 23
model.py

@@ -13,8 +13,7 @@ from typing import Optional, Dict
 from passlib.handlers.sha2_crypt import sha256_crypt
 
 import db_setup
-from game import CURRENCY_NAME, logger, DB_NAME, MIN_INTEREST_INTERVAL, MRO_NAME, BANK_NAME
-from util import random_chars
+from game import CURRENCY_NAME, logger, DB_NAME, MIN_INTEREST_INTERVAL, BANK_NAME, MRO_INTERVAL, MRO_RUNNING_TIME, random_ownable_name
 
 DBName = str
 connections: Dict[DBName, db.Connection] = {}
@@ -349,8 +348,30 @@ def get_user_loans(user_id):
     return current_cursor.fetchall()
 
 
-def bonds():
-    execute('''
+def bonds(issuer_id=None, only_next_mro_qualified=False):
+    if issuer_id is not None:
+        issuer_condition = 'issuer.rowid = ?'
+        issuer_params = (issuer_id,)
+    else:
+        issuer_condition = '1'
+        issuer_params = ()
+    if only_next_mro_qualified:
+        only_next_mro_condition = ''' -- noinspection SqlResolve @ any/"bonds"
+            SELECT EXISTS(
+                SELECT *
+                FROM banks b
+                JOIN ownership o ON o.ownable_id = bonds.ownable_id
+                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 = (SELECT MIN(t2.maturity_dt) FROM tender_calendar t2 WHERE t2.maturity_dt > ?)
+            )
+            '''
+        only_next_mro_params = (current_db_timestamp(),)
+    else:
+        only_next_mro_condition = '1'
+        only_next_mro_params = ()
+    execute(f'''
         SELECT 
             name,
             coupon,
@@ -359,8 +380,10 @@ def bonds():
         FROM bonds
         JOIN ownables o on bonds.ownable_id = o.rowid
         JOIN users issuer on bonds.issuer_id = issuer.rowid
+        WHERE ({issuer_condition})
+        AND ({only_next_mro_condition})
         ORDER BY coupon * (maturity_dt - ?) DESC
-        ''', (current_db_timestamp(),))
+        ''', (*issuer_params, *only_next_mro_params, current_db_timestamp(), ))
 
     return current_cursor.fetchall()
 
@@ -477,10 +500,7 @@ def ownable_name_exists(name):
 
 
 def new_stock(expiry, name=None):
-    while name is None:
-        name = random_chars(6)
-        if ownable_name_exists(name):
-            name = None
+    name = new_random_ownable_name(name)
 
     execute('''
         INSERT INTO ownables(name)
@@ -506,6 +526,14 @@ def new_stock(expiry, name=None):
     return name
 
 
+def new_random_ownable_name(name):
+    while name is None:
+        name = random_ownable_name()
+        if ownable_name_exists(name):
+            name = None
+    return name
+
+
 def ownable_id_by_name(ownable_name):
     execute('''
         SELECT rowid
@@ -537,16 +565,6 @@ def currency_id():
     return current_cursor.fetchone()[0]
 
 
-def mro_id():
-    execute('''
-        SELECT rowid
-        FROM ownables
-        WHERE name = ?
-        ''', (MRO_NAME,))
-
-    return current_cursor.fetchone()[0]
-
-
 def user_money(user_id):
     execute('''
         SELECT amount
@@ -897,6 +915,7 @@ def user_has_order_with_id(session_id, order_id):
 
 def leaderboard():
     score_expression = '''
+    -- noinspection SqlResolve @ any/"users"
     SELECT (
         SELECT COALESCE(SUM(
             CASE -- sum score for each of the users ownables
@@ -1226,8 +1245,11 @@ def assign_banking_licence(user_id):
         ''', (user_id,))
 
 
-def pay_bond_interest():
-    current_dt = execute("SELECT CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)").fetchone()[0]
+def pay_bond_interest(until=None):
+    if until is None:
+        current_dt = current_db_timestamp()
+    else:
+        current_dt = until
     sec_per_year = 3600 * 24 * 365
     interests = execute('''
     SELECT 
@@ -1266,13 +1288,60 @@ def pay_bond_interest():
 
     # delete matured bonds
     execute('''
+    DELETE FROM transactions 
+    WHERE ownable_id IN (
+        SELECT ownable_id
+        FROM bonds
+        WHERE ? > maturity_dt
+    )
+    ''', (current_dt,))
+    execute('''
+    DELETE FROM orders 
+    WHERE ownership_id IN (
+        SELECT o2.rowid
+        FROM bonds
+        JOIN ownables o on bonds.ownable_id = o.rowid
+        JOIN ownership o2 on o.rowid = o2.ownable_id
+        WHERE ? > maturity_dt
+    )
+    ''', (current_dt,))
+    execute('''
+    DELETE FROM order_history
+    WHERE ownership_id IN (
+        SELECT o2.rowid
+        FROM bonds
+        JOIN ownables o on bonds.ownable_id = o.rowid
+        JOIN ownership o2 on o.rowid = o2.ownable_id
+        WHERE ? > maturity_dt
+    )
+    ''', (current_dt,))
+    execute('''
+    DELETE FROM ownership 
+    WHERE ownable_id IN (
+        SELECT ownable_id
+        FROM bonds
+        WHERE ? > maturity_dt
+    )
+    ''', (current_dt,))
+    execute('''
+    DELETE FROM ownables 
+    WHERE rowid IN (
+        SELECT ownable_id
+        FROM bonds
+        WHERE ? > maturity_dt
+    )
+    ''', (current_dt,))
+    execute('''
     DELETE FROM bonds 
     WHERE ? > maturity_dt
     ''', (current_dt,))
 
 
-def pay_loan_interest():
-    current_dt = execute("SELECT CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)").fetchone()[0]
+def pay_loan_interest(until=None):
+    if until is None:
+        current_dt = current_db_timestamp()
+    else:
+        current_dt = until
     sec_per_year = 3600 * 24 * 365
     interests = execute('''
     SELECT 
@@ -1296,6 +1365,43 @@ def pay_loan_interest():
     ''', (current_dt, current_dt, MIN_INTEREST_INTERVAL,))
 
 
+def triggered_mros():
+    return execute('''
+    SELECT 
+        rowid AS mro_id, 
+        maturity_dt AS expiry,
+        mro_interest AS min_interest, 
+        dt AS mro_dt
+    FROM tender_calendar
+    WHERE NOT executed
+    AND dt < ?
+    ''', (current_db_timestamp(),)).fetchall()
+
+
+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
+    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:
+        bank_order(buy=True,
+                   ownable_id=ownable_id,
+                   limit=1,
+                   amount=amount,
+                   expiry=expiry,
+                   ioc=True)
+        execute('''
+        UPDATE tender_calendar
+        SET executed = TRUE
+        WHERE rowid = ?''', (mro_id,))  # TODO set mro to executed
+
+
 def loan_recipient_id(loan_id):
     execute('''
         SELECT user_id
@@ -1365,3 +1471,21 @@ def issue_bond(user_id, ownable_name, coupon, maturity_dt):
     INSERT INTO bonds(issuer_id, ownable_id, coupon, maturity_dt) 
     VALUES (?, (SELECT MAX(rowid) FROM ownables), ?, ?)
     ''', (user_id, coupon, maturity_dt))
+
+
+def update_tender_calendar():
+    last_mro_dt = execute('''
+    SELECT COALESCE((SELECT dt
+    FROM tender_calendar
+    ORDER BY dt DESC
+    LIMIT 1), ?)
+    ''', (current_db_timestamp(),)).fetchone()[0]
+
+    one_day = 24 * 3600
+    while last_mro_dt < current_db_timestamp() + one_day:
+        last_mro_dt += MRO_INTERVAL
+        maturity_dt = last_mro_dt + MRO_RUNNING_TIME
+        execute('''
+        INSERT INTO tender_calendar(dt, mro_interest, maturity_dt) 
+        VALUES (?, ?, ?)
+        ''', (last_mro_dt, global_control_value('main_refinancing_operations'), maturity_dt))

+ 27 - 10
server_controller.py

@@ -88,12 +88,6 @@ def order(json_request):
     model.own(user_id, ownable_name)
     ownership_id = model.get_ownership_id(ownable_id, user_id)
 
-    mro_id = model.mro_id()
-    if ownable_id == mro_id and sell:
-        raise Forbidden('You need to be a central bank to sell MROs.')
-    if ownable_id == mro_id and buy and not model.user_has_banking_license(user_id):
-        raise Forbidden('You need to be a bank to buy MROs.')
-
     try:
         if json_request['limit'] == '':
             limit = None
@@ -181,8 +175,18 @@ def loans(json_request):
     return {'data': data}
 
 
-def bonds(_json_request):
-    data = model.bonds()
+def bonds(json_request):
+    if 'issuer' in json_request:
+        issuer_id = model.get_user_id_by_name(json_request['issuer'])
+    else:
+        issuer_id = None
+    if 'only_next_mro_qualified' in json_request:
+        only_next_mro_qualified = json_request['only_next_mro_qualified']
+        if isinstance(only_next_mro_qualified, str):
+            raise BadRequest
+    else:
+        only_next_mro_qualified = False
+    data = model.bonds(issuer_id, only_next_mro_qualified)
     return {'data': data}
 
 
@@ -324,8 +328,21 @@ def server_version(_json_request):
 
 
 def _before_request(_json_request):
-    # pay interest rates for loans
+    # update tender calendar
+    model.update_tender_calendar()
+
+    for mro_id, expiry, min_interest, mro_dt in model.triggered_mros():
+        # pay interest rates for loans until this mro
+        model.pay_loan_interest(until=mro_dt)
+
+        # pay interest rates for bonds until this mro
+        model.pay_bond_interest(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
+    # pay interest rates for bonds until current time
     model.pay_bond_interest()

+ 32 - 1
test/do_some_requests/__init__.py

@@ -11,6 +11,7 @@ import requests
 
 import connection
 import test.do_some_requests.current_websocket
+from game import random_ownable_name
 from test import failed_requests
 from util import round_to_n
 
@@ -189,13 +190,43 @@ def run_tests():
         route = 'buy_banking_license'
         default_request_method(route, message)
 
+        message = {'session_id': session_ids[username],
+                   'coupon': 1.05,
+                   'name': random_ownable_name(),
+                   'run_time': 43200}
+        route = 'issue_bond'
+        default_request_method(route, message)
+
+        message = {'issuer': username}
+        route = 'bonds'
+        default_request_method(route, message)
+
+        message = {'issuer': username, 'only_next_mro_qualified': True}
+        route = 'bonds'
+        default_request_method(route, message)
+
+        message = {'issuer': username, 'only_next_mro_qualified': False}
+        route = 'bonds'
+        default_request_method(route, message)
+
+    message = {}
+    route = 'bonds'
+    default_request_method(route, message)
+
+    message = {'only_next_mro_qualified': True}
+    route = 'bonds'
+    default_request_method(route, message)
+
+    message = {'only_next_mro_qualified': False}
+    route = 'bonds'
+    default_request_method(route, message)
+
     for session_id in session_ids.values():
         message = {'session_id': session_id}
         route = 'logout'
         default_request_method(route, message)
 
 
-
 def main():
     global default_request_method
     for m in [