Explorar el Código

Remove trading bot, add bonds table and route

Eren Yilmaz hace 5 años
padre
commit
056b26b934
Se han modificado 7 ficheros con 90 adiciones y 175 borrados
  1. 6 3
      db_setup/seeds/__init__.py
  2. 14 5
      db_setup/tables.py
  3. 2 0
      game.py
  4. 42 11
      model.py
  5. 4 1
      routes.py
  6. 22 1
      server_controller.py
  7. 0 154
      trading_bot.py

+ 6 - 3
db_setup/seeds/__init__.py

@@ -1,16 +1,19 @@
 from sqlite3 import Cursor
 
-from game import CURRENCY_NAME
+from game import CURRENCY_NAME, MRO_NAME
 
 
 def seed(cursor: Cursor):
     print(' - Seeding initial data...')
     # ₭ollar
-    cursor.execute('''
+    cursor.executemany('''
                     INSERT OR IGNORE INTO ownables
                     (name)
                     VALUES (?)
-                    ''', (CURRENCY_NAME,))
+                    ''', [
+        (CURRENCY_NAME,),
+        (MRO_NAME,),
+    ])
     # The bank/external investors
     cursor.execute('''
                     INSERT OR IGNORE INTO users

+ 14 - 5
db_setup/tables.py

@@ -58,14 +58,14 @@ def tables(cursor):
                     expiry_dt TIMESTAMP NOT NULL,
                     status VARCHAR(20) NOT NULL,
                     order_id INTEGER NOT NULL, -- order_id is not a FOREIGN KEY since orders are deleted from order table afterwards
-                    archived_dt TIMESTAMP NOT NULL DEFAULT (datetime('now','localtime')),
+                    archived_dt TIMESTAMP NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)),
                     FOREIGN KEY (ownership_id) REFERENCES ownership(rowid)
                 )
                 ''')
     cursor.execute('''
                 CREATE TABLE IF NOT EXISTS transactions(
                     rowid INTEGER PRIMARY KEY,
-                    dt TIMESTAMP NOT NULL DEFAULT (datetime('now','localtime')),
+                    dt TIMESTAMP NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)),
                     price CURRENCY NOT NULL,
                     ownable_id INTEGER NOT NULL,
                     amount CURRENCY NOT NULL,
@@ -91,7 +91,7 @@ def tables(cursor):
     cursor.execute('''
                 CREATE TABLE IF NOT EXISTS news(
                     rowid INTEGER PRIMARY KEY,
-                    dt TIMESTAMP NOT NULL DEFAULT (datetime('now','localtime')),
+                    dt TIMESTAMP NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)),
                     title VARCHAR(50) NOT NULL
                 )
                 ''')
@@ -104,7 +104,7 @@ def tables(cursor):
     cursor.execute('''
                 CREATE TABLE IF NOT EXISTS global_control_values(
                     rowid INTEGER PRIMARY KEY,
-                    dt TIMESTAMP NOT NULL DEFAULT (datetime('now','localtime')),
+                    dt TIMESTAMP NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)),
                     value_name VARCHAR NOT NULL,
                     value FLOAT NOT NULL,
                     UNIQUE (value_name, dt)
@@ -116,10 +116,19 @@ def tables(cursor):
                     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
-                    last_interest_pay_dt TIMESTAMP NOT NULL DEFAULT (datetime('now','localtime')),
+                    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'
                 )
                 ''')
+    cursor.execute('''
+                CREATE TABLE IF NOT EXISTS bonds(
+                    rowid INTEGER PRIMARY KEY,
+                    issuer_id INTEGER NOT NULL REFERENCES users(rowid),
+                    ownable_id INTEGER UNIQUE NOT NULL REFERENCES ownables(rowid),
+                    last_interest_pay_dt TIMESTAMP NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)),
+                    coupon CURRENCY NOT NULL -- fancy word for interest rate
+                )
+                ''')
     _add_column_if_not_exists(cursor, '''
                 -- there is a not null constraint for new values that is watched by triggers
                 ALTER TABLE users ADD COLUMN salt BLOB NOT NULL DEFAULT 'orderer_is_a_cool_application_]{][{²$%WT§$%GV§$%SF$%&S$%FGGFHBDHJZIF254325'

+ 2 - 0
game.py

@@ -1,6 +1,8 @@
 from lib.db_log import DBLog
 
 CURRENCY_NAME = "₭ollar"
+MRO_NAME = 'MRO'
+MRO_INTERVAL = 3 * 3600
 CURRENCY_SYMBOL = "₭"
 MINIMUM_ORDER_AMOUNT = 1
 DEFAULT_ORDER_EXPIRY = 43200

+ 42 - 11
model.py

@@ -13,8 +13,7 @@ from typing import Optional, Dict
 from passlib.handlers.sha2_crypt import sha256_crypt
 
 import db_setup
-import trading_bot
-from game import CURRENCY_NAME, logger, DB_NAME, MIN_INTEREST_INTERVAL
+from game import CURRENCY_NAME, logger, DB_NAME, MIN_INTEREST_INTERVAL, MRO_NAME
 from util import random_chars
 
 DBName = str
@@ -447,7 +446,22 @@ def available_amount(user_id, ownable_id):
     return current_cursor.fetchone()[0] - sell_ordered_amount(user_id, ownable_id)
 
 
+def is_bond_of_user(ownable_id, user_id):
+    execute('''
+    SELECT EXISTS(
+        SELECT * FROM bonds 
+        WHERE ownable_id = ?
+        AND issuer_id = ?
+    )
+    ''', (ownable_id, user_id,))
+
+    return current_cursor.fetchone()[0]
+
+
 def user_has_at_least_available(amount, user_id, ownable_id):
+    if is_bond_of_user(ownable_id, user_id):
+        return True
+
     if not isinstance(amount, float) and not isinstance(amount, int):
         # comparison of float with strings does not work so well in sql
         raise AssertionError()
@@ -553,6 +567,16 @@ 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
@@ -679,15 +703,7 @@ def execute_orders(ownable_id):
         #               user_id,user_id,rowid,rowid)
 
         if not matching_orders:
-            if not orders_traded:
-                break
-            # check if the trading bot has any new offers to make
-            new_order_was_placed = trading_bot.notify_order_traded(ownable_id)
-            if new_order_was_placed:
-                orders_traded = False
-                continue
-            else:
-                break
+            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, _, \
@@ -1306,3 +1322,18 @@ def loan_id_exists(loan_id):
         ''', (loan_id,))
 
     return current_cursor.fetchone()[0]
+
+
+def main_refinancing_operations():
+    ...  # TODO
+
+
+def issue_bond(user_id, ownable_name, coupon):
+    execute('''
+    INSERT INTO ownables(name)
+    VALUES (?)
+    ''', (ownable_name,))
+    execute('''
+    INSERT INTO bonds(issuer_id, ownable_id, coupon) 
+    VALUES (?, (SELECT MAX(rowid) FROM ownables), ?)
+    ''', (user_id, coupon))

+ 4 - 1
routes.py

@@ -19,12 +19,15 @@ valid_post_routes = {
     'buy_banking_license',
     'take_out_personal_loan',
     'repay_loan',
+    'issue_bond',
     'loans',
 }
 
 push_message_types = set()
 
-upload_filtered = set()  # TODO enable upload filter again when accuracy improves
+upload_filtered = {
+    'issue_bond'
+}
 
 assert len(set(valid_post_routes)) == len(valid_post_routes)
 assert upload_filtered.issubset(valid_post_routes)

+ 22 - 1
server_controller.py

@@ -77,6 +77,8 @@ def order(json_request):
         ioc = False
 
     session_id = json_request['session_id']
+    user_id = model.get_user_id_by_session_id(session_id)
+
     amount = json_request['amount']
     try:
         amount = int(amount)
@@ -90,11 +92,17 @@ def order(json_request):
     time_until_expiration = float(json_request['time_until_expiration'])
     if time_until_expiration < 0:
         return BadRequest('Invalid expiration time.')
+
     ownable_id = model.ownable_id_by_name(ownable_name)
-    user_id = model.get_user_id_by_session_id(session_id)
     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
@@ -136,6 +144,7 @@ def order(json_request):
         expiry = model.current_db_timestamp() + timedelta(minutes=time_until_expiration).total_seconds()
     except OverflowError:
         return BadRequest('The expiration time is too far in the future.')
+
     model.place_order(buy, ownership_id, limit, stop_loss, amount, expiry, ioc)
     return {'message': "Order placed."}
 
@@ -265,6 +274,15 @@ def take_out_personal_loan(json_request):
     return {'message': "Successfully took out personal loan"}
 
 
+def issue_bond(json_request):
+    check_missing_attributes(json_request, ['session_id', 'name', 'coupon', ])
+    user_id = model.get_user_id_by_session_id(json_request['session_id'])
+    coupon = json_request['coupon']
+    ownable_name = json_request['name']
+    model.issue_bond(user_id, ownable_name, coupon)
+    return {'message': "Successfully issued bond"}
+
+
 def repay_loan(json_request):
     check_missing_attributes(json_request, ['session_id', 'amount', 'loan_id'])
     amount = json_request['amount']
@@ -288,3 +306,6 @@ def repay_loan(json_request):
 def before_request(_json_request):
     # pay interest rates for loans
     model.pay_loan_interest()
+
+    # main refinancing operation
+    model.main_refinancing_operations()

+ 0 - 154
trading_bot.py

@@ -1,154 +0,0 @@
-import random
-from datetime import timedelta, datetime
-from math import log2, ceil
-
-import model
-from debug import debug
-from game import DEFAULT_ORDER_EXPIRY
-
-
-def place_order(ownable_id):
-    """
-    places a new order according to the algorithm described in `assets/follower.py`
-    :param ownable_id: on which ownable to place the order
-    :return: True iff a new order was placed
-    """
-
-    best_buy_order, cheapest_sell_order = model.abs_spread(ownable_id)
-    if best_buy_order is None or cheapest_sell_order is None:
-        return False
-    investors_id = model.bank_id()
-    ownership_id = model.get_ownership_id(ownable_id, investors_id)
-
-    if debug:  # the bot should only have one order
-        model.cursor.execute('''
-        SELECT COUNT(*) = 0
-        FROM orders
-        WHERE ownership_id = ? 
-        ''', (ownership_id,))
-        if not model.cursor.fetchone()[0]:
-            raise AssertionError('The bot should have no orders at this point.')
-
-    amounts = model.cursor.execute('''
-        SELECT ordered_amount 
-        FROM orders, ownership
-        WHERE orders.ownership_id = ownership.rowid
-        AND ownership.ownable_id = ?
-        ''', (ownable_id,)).fetchall()
-    if len(amounts) < 2:
-        raise AssertionError('We should have found at least two orders.')
-    amounts = [random.choice(amounts)[0] for _ in range(int(ceil(log2(len(amounts)))))]
-    amount = ceil(sum(amounts) / len(amounts))
-
-    expiry = model.current_db_timestamp() + timedelta(minutes=DEFAULT_ORDER_EXPIRY)
-
-    limit = round(random.uniform(best_buy_order, cheapest_sell_order) * 10000) / 10000
-    if limit - best_buy_order < cheapest_sell_order - limit:
-        model.place_order(buy=True,
-                          ownership_id=ownership_id,
-                          limit=limit,
-                          stop_loss=False,
-                          amount=amount,
-                          expiry=expiry,
-                          ioc=False)
-    else:
-        model.place_order(buy=False,
-                          ownership_id=ownership_id,
-                          limit=limit,
-                          stop_loss=False,
-                          amount=amount,
-                          expiry=expiry,
-                          ioc=False)
-    return True
-
-
-def notify_expired_orders(orders):
-    for order in orders:
-        # order_id = order[0]
-        ownership_id = order[1]
-
-        # check if that was one of the bots orders
-        bank_ownership_id = model.get_ownership_id(model.ownable_id_by_ownership_id(ownership_id), model.bank_id())
-        if ownership_id != bank_ownership_id:
-            continue
-
-        # create a new order
-        ownable_id = model.ownable_id_by_ownership_id(ownership_id)
-        place_order(ownable_id)
-
-
-def notify_order_traded(ownable_id):
-    """
-    Called after a trade has been done and now the auctions are finished.
-    :param ownable_id: the ownable that was traded
-    :return: True iff a new order was placed
-    """
-    model.connect()
-    if ownable_id == model.currency_id():
-        return False
-    ownership_id = model.get_ownership_id(ownable_id, model.bank_id())
-
-    if debug:  # the bot should only have one order
-        model.cursor.execute('''
-        SELECT COUNT(*) <= 1
-        FROM orders
-        WHERE ownership_id = ? 
-        ''', (ownership_id,))
-        if not model.cursor.fetchone()[0]:
-            raise AssertionError('The bot should have at most one order.')
-
-    model.cursor.execute('''
-        SELECT rowid, ordered_amount, expiry_dt
-        FROM orders 
-        WHERE ownership_id = ? 
-        -- no need for ORDER since the bot should have only one order
-        UNION ALL
-        SELECT * FROM (
-            SELECT NULL, ordered_amount, expiry_dt
-            FROM order_history
-            WHERE ownership_id = ?
-            ORDER BY rowid DESC -- equivalent to ordering by time created
-        )
-        LIMIT 1
-    ''', (ownership_id, ownership_id,))
-    data = model.cursor.fetchall()
-
-    if not data:
-        return place_order(ownable_id)
-    my_last_order = data[0]
-    last_order_open = my_last_order[0] is not None
-    last_order_id = my_last_order[0]
-    last_amount = my_last_order[1]
-    expiry = my_last_order[2]
-    dt_order_placed = datetime.strptime(expiry, '%Y-%m-%d %H:%M:%S') - timedelta(minutes=DEFAULT_ORDER_EXPIRY)
-
-    model.cursor.execute('''
-        SELECT COALESCE(SUM(amount), 0) >= 2 * ?
-        FROM transactions
-        WHERE ownable_id = ? 
-          AND dt > ? -- interestingly >= would be problematic
-    ''', (last_amount, ownable_id, dt_order_placed))
-
-    if model.cursor.fetchone()[0]:
-        if last_order_open:
-            model.delete_order(last_order_id, 'Canceled')
-        return place_order(ownable_id)
-
-    return False
-
-
-def main():
-    """the initial part of the trading bot algorithm"""
-    if model.get_user_orders(model.bank_id()):
-        raise AssertionError('The trading bot already has some orders.')
-    if input('Are you sure you want to place the initial orders? (type in "yes" or something else):') == 'yes':
-        for ownable_id in model.ownable_ids():
-            if ownable_id != model.currency_id():
-                place_order(ownable_id)
-    else:
-        print('Not placing orders.')
-    model.cleanup()
-
-
-if __name__ == '__main__':
-    main()