Eren Yilmaz 5 년 전
부모
커밋
99a5871a5e
4개의 변경된 파일165개의 추가작업 그리고 48개의 파일을 삭제
  1. 16 10
      lib/db_log.py
  2. 48 31
      model.py
  3. 9 4
      server_controller.py
  4. 92 3
      test/do_some_requests/__init__.py

+ 16 - 10
lib/db_log.py

@@ -27,6 +27,7 @@ class DBLog:
         self.connection: Optional[db.Connection] = None
         self.cursor: Optional[db.Cursor] = None
         self.db_name: Optional[DBName] = None
+        self.skip_db_logging = False
 
         db_name = db_name.lower()
         if not os.path.isfile(db_name) and not create_if_not_exists:
@@ -90,17 +91,22 @@ class DBLog:
             current_pid=None,
             current_head_hex_sha=CURRENT_SHA,
             data_serialization_method=lambda x: x):
-        if level < self.min_level:
+        if level < self.min_level or self.skip_db_logging:
             return
-        if dt_created is None:
-            dt_created = round(time.time())
-        if current_pid is None:
-            current_pid = os.getpid()
-        data: str = data_serialization_method(data)
-        self.cursor.execute('''
-        INSERT INTO entries(message, data, dt_created, pid, head_hex_sha, message_type, level)
-        VALUES (?, ?, ?, ?, ?, ?, ?)
-        ''', (message, data, dt_created, current_pid, current_head_hex_sha, message_type, level))
+        try:
+            if dt_created is None:
+                dt_created = round(time.time())
+            if current_pid is None:
+                current_pid = os.getpid()
+            data: str = data_serialization_method(data)
+            self.cursor.execute('''
+            INSERT INTO entries(message, data, dt_created, pid, head_hex_sha, message_type, level)
+            VALUES (?, ?, ?, ?, ?, ?, ?)
+            ''', (message, data, dt_created, current_pid, current_head_hex_sha, message_type, level))
+        except db.OperationalError as e:
+            if 'database is locked' in str(e):
+                print('WARNING: Unable to write to log database. No logs will be written.')
+                self.skip_db_logging = True
 
     def debug(self, message, *args, **kwargs):
         self.log(message, logging.DEBUG, *args, **kwargs)

+ 48 - 31
model.py

@@ -162,6 +162,13 @@ def own(user_id, ownable_name, amount=0):
                 ''', (user_id, ownable_name, amount))
 
 
+def own_id(user_id, ownable_id, amount=0):
+    execute('''
+                INSERT OR IGNORE INTO ownership (user_id, ownable_id, amount)
+                SELECT ?, ?, ?
+                ''', (user_id, ownable_id, amount))
+
+
 def send_ownable(from_user_id, to_user_id, ownable_id, amount):
     if amount < 0:
         raise AssertionError('Can not send negative amount')
@@ -369,6 +376,12 @@ def next_mro_interest(dt=None):
     ''', (next_mro_dt(dt),)).fetchone()[0]
 
 
+def next_mro_maturity(dt=None):
+    return execute('''
+    SELECT t.maturity_dt FROM tender_calendar t WHERE t.dt = ?
+    ''', (next_mro_dt(dt),)).fetchone()[0]
+
+
 def credits(issuer_id=None, only_next_mro_qualified=False):
     if issuer_id is not None:
         issuer_condition = 'issuer.rowid = ?'
@@ -463,6 +476,8 @@ def is_bond_of_user(ownable_id, user_id):
 def user_available_ownable(user_id, ownable_id):
     if is_bond_of_user(ownable_id, user_id):
         return inf
+    if user_id == bank_id() and ownable_id == currency_id():
+        return inf
 
     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)
@@ -620,7 +635,6 @@ def current_value(ownable_id):
 
 
 def execute_orders(ownable_id):
-    orders_traded = False
     while True:
         # find order to execute
         execute('''
@@ -692,18 +706,18 @@ def execute_orders(ownable_id):
             LIMIT 1
             ''', tuple(ownable_id for _ in range(8)))
 
-        matching_orders = current_cursor.fetchone()
+        matching_order = current_cursor.fetchone()
         # return type: (ownership_id,buy,limit,stop_loss,ordered_amount,executed_amount,expiry_dt,
         #               ownership_id,buy,limit,stop_loss,ordered_amount,executed_amount,expiry_dt,
         #               user_id,user_id,rowid,rowid)
 
-        if not matching_orders:
+        if not matching_order:
             break
 
         _, buy_ownership_id, _, buy_limit, _, buy_order_amount, buy_executed_amount, buy_expiry_dt, _, \
         _, sell_ownership_id, _, sell_limit, _, sell_order_amount, sell_executed_amount, sell_expiry_dt, _, \
         buyer_id, seller_id, buy_order_id, sell_order_id \
-            = matching_orders
+            = matching_order
 
         if buy_limit is None and sell_limit is None:
             price = current_value(ownable_id)
@@ -720,15 +734,15 @@ def execute_orders(ownable_id):
 
         buyer_money = user_available_money(buyer_id)
 
-        def _my_division(x, y):
-            try:
-                return floor(x / y)
-            except ZeroDivisionError:
-                return float('Inf')
+        def affordable_nominal(money, price_per_nominal):
+            if money == inf or price_per_nominal <= 0:
+                return inf
+            else:
+                return floor(money / price_per_nominal)
 
         amount = min(buy_order_amount - buy_executed_amount,
                      sell_order_amount - sell_executed_amount,
-                     _my_division(buyer_money, price))
+                     affordable_nominal(buyer_money, price))
 
         if amount < 0:
             amount = 0
@@ -758,10 +772,8 @@ def execute_orders(ownable_id):
 
         if buy_order_finished:
             delete_order(buy_order_id, 'Executed')
-            orders_traded = True
         if sell_order_finished:
             delete_order(sell_order_id, 'Executed')
-            orders_traded = True
 
         if seller_id != buyer_id:  # prevent showing self-transactions
             execute('''
@@ -816,6 +828,7 @@ def user_name_by_id(user_id):
 def bank_order(buy, ownable_id, limit, amount, expiry, ioc):
     if not limit:
         raise AssertionError('The bank does not give away anything.')
+    own_id(bank_id(), ownable_id)
     place_order(buy,
                 get_ownership_id(ownable_id, bank_id()),
                 limit,
@@ -823,8 +836,6 @@ def bank_order(buy, ownable_id, limit, amount, expiry, ioc):
                 amount,
                 expiry,
                 ioc=ioc)
-    ownable_name = ownable_name_by_id(ownable_id)
-    new_news('External investors are selling ' + ownable_name + ' atm')
 
 
 def current_db_time():  # might differ from datetime.datetime.now() for time zone reasons
@@ -1182,7 +1193,7 @@ def cleanup():
     for name in connections:
         connections[name].rollback()
         connections[name].close()
-    connections = []
+    connections = {}
     current_connection = None
     current_cursor = None
     current_db_name = None
@@ -1274,7 +1285,7 @@ def pay_bond_interest(until=None):
         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
+    WHERE (? - last_interest_pay_dt > ? OR ? > maturity_dt) -- every interval or when the bond expired
     AND amount != 0
     GROUP BY o.user_id, credits.issuer_id
     ''', (current_dt, sec_per_year, current_dt, MIN_INTEREST_INTERVAL, current_dt)).fetchall()
@@ -1285,7 +1296,7 @@ def pay_bond_interest(until=None):
         o.user_id AS to_user_id,
         credits.issuer_id AS from_user_id
     FROM credits
-    JOIN ownership o on credits.ownable_id = o.ownable_id
+    JOIN ownership o ON credits.ownable_id = o.ownable_id
     WHERE ? > maturity_dt
     ''', (current_dt,)).fetchall()
 
@@ -1303,6 +1314,10 @@ def pay_bond_interest(until=None):
     WHERE ? - last_interest_pay_dt > ?''', (current_dt, current_dt, MIN_INTEREST_INTERVAL,))
 
     # delete matured credits
+    delete_matured_credits(current_dt)
+
+
+def delete_matured_credits(current_dt):
     execute('''
     DELETE FROM transactions 
     WHERE ownable_id IN (
@@ -1411,27 +1426,28 @@ def triggered_mros():
     return execute('''
     SELECT 
         rowid AS mro_id, 
-        maturity_dt AS expiry,
+        maturity_dt,
         mro_interest AS min_interest, 
         dt AS mro_dt
     FROM tender_calendar
     WHERE NOT executed
     AND dt < ?
+    ORDER BY dt ASC
     ''', (current_db_timestamp(),)).fetchall()
 
 
-def mro(mro_id, expiry, min_interest):
+def mro(mro_id, maturity_dt, min_interest):
     qualified_credits = execute('''
-    SELECT credits.ownable_id, SUM(amount)
+    SELECT credits.ownable_id, SUM(ordered_amount)
     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
+    AND ("limit" IS NULL OR "limit" <= 1)
     GROUP BY credits.ownable_id
-    ''', (expiry, min_interest)).fetchall()
+    ''', (maturity_dt, min_interest)).fetchall()
     for ownable_id, amount in qualified_credits:
         if amount == 0:
             continue
@@ -1440,12 +1456,12 @@ def mro(mro_id, expiry, min_interest):
                    ownable_id=ownable_id,
                    limit=1,
                    amount=amount,
-                   expiry=expiry,
+                   expiry=maturity_dt,
                    ioc=True)
-        execute('''
-        UPDATE tender_calendar
-        SET executed = TRUE
-        WHERE rowid = ?''', (mro_id,))  # TODO set mro to executed
+    execute('''
+    UPDATE tender_calendar
+    SET executed = TRUE
+    WHERE rowid = ?''', (mro_id,))
 
 
 def loan_recipient_id(loan_id):
@@ -1511,6 +1527,7 @@ def time_travel(delta_t):
     Be careful with time travel into the past though.
     :param delta_t: time in seconds to travel
     """
+    print(f'DEBUG INFO: Time traveling {round(delta_t)}s into the future by reducing all timestamps by {round(delta_t)}...')
     tables = execute('''
     SELECT name
     FROM sqlite_master
@@ -1532,7 +1549,7 @@ def time_travel(delta_t):
             execute(f'''
             UPDATE {table}
             SET {updates}
-            ''', tuple(delta_t for _ in timestamp_columns))
+            ''', tuple(-delta_t for _ in timestamp_columns))
 
 
 def user_has_loan_with_id(user_id, loan_id):
@@ -1555,7 +1572,7 @@ def tender_calendar():
 def required_minimum_reserve(user_id):
     assert user_has_banking_license(user_id)
     borrowed_money = execute('''
-    SELECT SUM(amount)
+    SELECT COALESCE(SUM(amount), 0)
     FROM ownership
     JOIN credits b on ownership.ownable_id = b.ownable_id
     WHERE b.issuer_id = ?
@@ -1583,8 +1600,8 @@ def update_tender_calendar():
     LIMIT 1), ?)
     ''', (current_db_timestamp(),)).fetchone()[0]
 
-    one_day = 24 * 3600
-    while last_mro_dt < current_db_timestamp() + one_day:
+    one_month = 30 * 24 * 3600
+    while last_mro_dt < current_db_timestamp() + one_month:
         last_mro_dt += MRO_INTERVAL
         maturity_dt = last_mro_dt + MRO_RUNNING_TIME
         execute('''

+ 9 - 4
server_controller.py

@@ -26,7 +26,7 @@ def depot(json_request):
     check_missing_attributes(json_request, ['session_id'])
     user_id = model.get_user_id_by_session_id(json_request['session_id'])
     return {'data': model.get_user_ownership(user_id),
-            'own_wealth': f'{model.user_wealth(user_id):.2f}',
+            'own_wealth': float(f'{model.user_wealth(user_id):.2f}'),
             'minimum_reserve': model.required_minimum_reserve(user_id) if model.user_has_banking_license(user_id) else None,
             'banking_license': model.user_has_banking_license(user_id)}
 
@@ -89,6 +89,9 @@ def order(json_request):
     model.own(user_id, ownable_name)
     ownership_id = model.get_ownership_id(ownable_id, user_id)
 
+    if 'limit' in json_request and 'stop_loss' not in json_request:
+        raise BadRequest('Need to set stop_loss to either True or False for limit orders.')
+
     try:
         if json_request['limit'] == '':
             limit = None
@@ -305,7 +308,7 @@ def issue_bond(json_request):
 
     run_time = json_request['run_time']
     if run_time == 'next_mro':
-        maturity_dt = model.next_mro_dt()
+        maturity_dt = model.next_mro_maturity()
     else:
         try:
             run_time = int(run_time)
@@ -355,7 +358,9 @@ def _before_request(_json_request):
     # update tender calendar
     model.update_tender_calendar()
 
-    for mro_id, expiry, min_interest, mro_dt in model.triggered_mros():
+    for mro_id, maturity_dt, min_interest, mro_dt in model.triggered_mros():
+        assert maturity_dt > mro_dt
+
         # pay interest rates for loans until this mro
         model.pay_loan_interest(until=mro_dt)
 
@@ -366,7 +371,7 @@ def _before_request(_json_request):
         model.pay_deposit_facility(until=mro_dt)
 
         # handle MROs
-        model.mro(mro_id, expiry, min_interest)
+        model.mro(mro_id, maturity_dt, min_interest)
 
     # pay interest rates for loans until current time
     model.pay_loan_interest()

+ 92 - 3
test/do_some_requests/__init__.py

@@ -2,7 +2,9 @@ import json
 import random
 import re
 import sys
+import time
 from datetime import datetime
+from math import inf
 from time import perf_counter
 from typing import Dict, Callable
 from uuid import uuid4
@@ -10,11 +12,15 @@ from uuid import uuid4
 import requests
 
 import connection
+import model
 import test.do_some_requests.current_websocket
-from game import random_ownable_name
+from debug import debug
+from game import random_ownable_name, DB_NAME, CURRENCY_NAME
 from test import failed_requests
 from util import round_to_n
 
+MRO_AMOUNT = 1e9
+
 DEFAULT_PW = 'pw'
 
 PORT = connection.PORT
@@ -116,7 +122,8 @@ def random_time():
 
 
 def run_tests():
-    print('You are currently in debug mode.')
+    if debug:
+        print('You are currently in debug mode.')
     print('Host:', str(HOST))
     usernames = [f'user{datetime.now().timestamp()}',
                  f'user{datetime.now().timestamp()}+1']
@@ -209,13 +216,29 @@ def run_tests():
         route = 'credits'
         default_request_method(route, message)
 
+        my_mro_name = random_ownable_name()
         message = {'session_id': session_ids[username],
                    'coupon': 'next_mro',
-                   'name': random_ownable_name(),
+                   'name': my_mro_name,
                    'run_time': 'next_mro'}
         route = 'issue_bond'
         default_request_method(route, message)
 
+        message = {'session_id': session_ids[username],
+                   'buy': False,
+                   'ownable': my_mro_name,
+                   'amount': MRO_AMOUNT,
+                   'limit': 1,
+                   'stop_loss': False,
+                   'time_until_expiration': 43200}
+        route = 'order'
+        default_request_method(route, message)
+
+        message = {'session_id': session_ids[username]}
+        route = 'orders'
+        orders_before_tt = default_request_method(route, message)['data']
+        assert len(orders_before_tt) == 1
+
         message = {'issuer': username}
         route = 'credits'
         default_request_method(route, message)
@@ -228,6 +251,72 @@ def run_tests():
         route = 'credits'
         default_request_method(route, message)
 
+        message = {'session_id': session_ids[username]}
+        route = 'depot'
+        depot_before_tt = default_request_method(route, message)
+
+        message = {}
+        route = 'tender_calendar'
+        tender_calendar = default_request_method(route, message)['data']
+        next_mro_dt = inf
+        for row in tender_calendar:
+            if row[0] > time.time():
+                next_mro_dt = min(next_mro_dt, row[0])
+        assert next_mro_dt < inf
+
+        model.connect(DB_NAME)
+        assert next_mro_dt - time.time() - 10 > 0, 'Random test fail, this can happen'
+        model.time_travel(next_mro_dt - time.time() - 10)  # 10 seconds before MRO
+        model.current_connection.commit()
+        model.cleanup()
+
+        message = {'session_id': session_ids[username]}
+        route = 'depot'
+        depot_before_mro = default_request_method(route, message)
+
+        message = {'session_id': session_ids[username]}
+        route = 'orders'
+        orders_before_mro = default_request_method(route, message)['data']
+        assert len(orders_before_mro) == 1
+
+        # some money was spent as interest
+        assert depot_before_tt['own_wealth'] > depot_before_mro['own_wealth']
+        assert depot_before_tt['minimum_reserve'] == depot_before_mro['minimum_reserve'] == 0
+        assert depot_before_tt['banking_license'] and depot_before_mro['banking_license']
+        for row1 in depot_before_tt['data']:
+            for row2 in depot_before_mro['data']:
+                if row1[0] == row2[0]:
+                    if row1[0] == CURRENCY_NAME:
+                        assert row1[1] > row2[1]
+                    else:
+                        assert row1[1] == row2[1]
+
+        model.connect(DB_NAME)
+        model.time_travel(20)  # 10 seconds after MRO
+        model.current_connection.commit()
+        model.cleanup()
+
+        message = {'session_id': session_ids[username]}
+        route = 'orders'
+        orders_after_mro = default_request_method(route, message)['data']
+        assert len(orders_after_mro) == 0
+
+        message = {'session_id': session_ids[username]}
+        route = 'depot'
+        depot_after_mro = default_request_method(route, message)
+
+        # some money was spent as interest and some MROs were sold
+        assert depot_before_mro['own_wealth'] >= depot_after_mro['own_wealth']  # can be equal since interest rates are only subtracted once per time interval
+        assert depot_after_mro['minimum_reserve'] == max(0., MRO_AMOUNT * 0.01 - 100000)
+        assert depot_after_mro['banking_license']
+        for row1 in depot_before_mro['data']:
+            for row2 in depot_after_mro['data']:
+                if row1[0] == row2[0]:
+                    if row1[0] == CURRENCY_NAME:
+                        assert row1[1] < row2[1] < row1[1] + MRO_AMOUNT
+                    else:
+                        assert row1[1] == row2[1]
+
     message = {}
     route = 'credits'
     default_request_method(route, message)