Browse Source

work on trades

Eren Yilmaz 6 years ago
parent
commit
242b36cb3d
7 changed files with 265 additions and 84 deletions
  1. 2 1
      client.py
  2. 24 4
      client_controller.py
  3. 26 3
      db_setup.py
  4. 165 68
      model.py
  5. 3 1
      server.py
  6. 36 7
      server_controller.py
  7. 9 0
      util.py

+ 2 - 1
client.py

@@ -59,7 +59,8 @@ allowed_commands = ['help',
                     'activate_key',
                     'buy',
                     'sell',
-                    'transactions']
+                    'transactions',
+                    'orders_on']
 
 
 def one_command():

+ 24 - 4
client_controller.py

@@ -7,7 +7,7 @@ from client import allowed_commands
 from connection import client_request
 from tabulate import tabulate
 
-from util import debug
+from util import debug, str2bool
 
 
 def login(username=None, password=None):
@@ -118,11 +118,12 @@ def buy(amount=None, object_name=None, limit='', stop_loss='', time_until_expira
         object_name = input('Name of object to buy: ')
     if amount is None:
         amount = input('Amount: ')
-    if limit != '':
+    if limit == '':
         set_limit = yn_dialog('Do you want to place a limit?')
         if set_limit:
             limit = input('Limit: ')
             stop_loss = yn_dialog('Is this a stop-loss limit?')
+    stop_loss = str2bool(stop_loss)
     if time_until_expiration is None:
         time_until_expiration = input('Time until order expires (minutes, default 60):')
         if time_until_expiration == '':
@@ -146,11 +147,14 @@ def sell(amount=None, object_name=None, limit='', stop_loss='', time_until_expir
         object_name = input('Name of object to sell: ')
     if amount is None:
         amount = input('Amount: ')
-    if limit != '':
+    if limit == '':
         set_limit = yn_dialog('Do you want to place a limit?')
         if set_limit:
             limit = input('Limit: ')
             stop_loss = yn_dialog('Is this a stop-loss limit?')
+        else:
+            limit = None
+            stop_loss = None
     if time_until_expiration is None:
         time_until_expiration = input('Time until order expires (minutes, default 60):')
         if time_until_expiration == '':
@@ -174,7 +178,23 @@ def orders():
     success = 'data' in response
     if success:
         print(tabulate(response['data'],
-                       headers=['Buy?', 'Name', 'Amount', 'Limit', 'Stop Loss?', 'Orig. Order Size', 'Expires'],
+                       headers=['Buy?', 'Name', 'Amount', 'Limit', 'Stop Loss?', 'Orig. Size', 'Expires', 'No.'],
+                       tablefmt="pipe"))
+    else:
+        if 'error_message' in response:
+            print('Order access failed with message:', response['error_message'])
+        else:
+            print('Order access failed.')
+
+
+def orders_on(object_name=None):
+    if object_name is None:  # TODO list some available objects
+        object_name = input('Name of object to check: ')
+    response = client_request('orders_on', {"session_id": connection.session_id, "ownable": object_name})
+    success = 'data' in response
+    if success:
+        print(tabulate(response['data'],
+                       headers=['Buy?', 'Name', 'Amount', 'Limit', 'Stop Loss?', 'Expires', 'No.'],
                        tablefmt="pipe"))
     else:
         if 'error_message' in response:

+ 26 - 3
db_setup.py

@@ -34,6 +34,11 @@ def drop_database(cursor):
     cursor.execute("DROP TRIGGER IF EXISTS order_limit_not_negative_after_update")
     cursor.execute("DROP TRIGGER IF EXISTS order_amount_positive_after_insert")
     cursor.execute("DROP TRIGGER IF EXISTS order_amount_positive_after_update")
+    cursor.execute("DROP TRIGGER IF EXISTS not_more_executed_than_ordered_after_insert")
+    cursor.execute("DROP TRIGGER IF EXISTS not_more_executed_than_ordered_after_update")
+    cursor.execute("DROP TRIGGER IF EXISTS expiry_dt_in_future_after_insert")
+    cursor.execute("DROP TRIGGER IF EXISTS expiry_dt_in_future_after_update")
+    cursor.execute("DROP TRIGGER IF EXISTS order_amount_positive_after_update")
 
 
 def seed(cursor):
@@ -152,7 +157,7 @@ def integrity_checks(cursor):
                 CREATE TRIGGER IF NOT EXISTS expiry_dt_in_future_after_insert
                 AFTER INSERT
                 ON orders
-                WHEN NEW.expiry_dt < NEW.dt
+                WHEN NEW.expiry_dt <= datetime('now')
                 BEGIN
                     SELECT RAISE(ROLLBACK, 'Order is already expired.');
                 END
@@ -161,11 +166,29 @@ def integrity_checks(cursor):
                 CREATE TRIGGER IF NOT EXISTS expiry_dt_in_future_after_update
                 AFTER UPDATE
                 ON orders
-                WHEN NEW.expiry_dt < NEW.dt
+                WHEN NEW.expiry_dt <= datetime('now')
                 BEGIN
                     SELECT RAISE(ROLLBACK, 'Order is already expired.');
                 END
                 ''')
+    cursor.execute('''
+                CREATE TRIGGER IF NOT EXISTS stop_loss_requires_limit_after_insert
+                AFTER INSERT
+                ON orders
+                WHEN NEW."limit" IS NULL AND NEW.stop_loss IS NOT NULL
+                BEGIN
+                    SELECT RAISE(ROLLBACK, 'Can only set `stop_loss` `for limit orders.');
+                END
+                ''')
+    cursor.execute('''
+                CREATE TRIGGER IF NOT EXISTS stop_loss_requires_limit_after_update
+                AFTER UPDATE
+                ON orders
+                WHEN NEW."limit" IS NULL AND NEW.stop_loss IS NOT NULL
+                BEGIN
+                    SELECT RAISE(ROLLBACK, 'Can only set `stop_loss` `for limit orders.');
+                END
+                ''')
 
 
 def tables(cursor):
@@ -204,6 +227,7 @@ def tables(cursor):
                     stop_loss BOOLEAN,
                     ordered_amount CURRENCY NOT NULL,
                     executed_amount CURRENCY DEFAULT 0 NOT NULL,
+                    expiry_dt DATETIME NOT NULL,
                     FOREIGN KEY (ownership_id) REFERENCES ownership(rowid)
                 )
                 ''')
@@ -213,7 +237,6 @@ def tables(cursor):
                     price CURRENCY NOT NULL,
                     ownable_id INTEGER NOT NULL,
                     amount CURRENCY NOT NULL,
-                    expiry_dt DATETIME NOT NULL,
                     FOREIGN KEY (ownable_id) REFERENCES ownable(rowid)
                 )
                 ''')

+ 165 - 68
model.py

@@ -4,6 +4,7 @@ import re
 import sqlite3 as db
 import sys
 import uuid
+from math import floor
 
 import db_setup
 from game import CURRENCY_NAME
@@ -103,6 +104,9 @@ def register(username, password, game_key):
 
 
 def own(user_id, ownable_name):
+    if not isinstance(ownable_name, str):
+        return AssertionError('A name must be a string.')
+
     cursor.execute('''
                 WITH one_ownable_id AS (SELECT rowid FROM ownables WHERE name = ?),
                      one_user_id AS (SELECT ?)
@@ -130,8 +134,6 @@ def send_ownable(from_user_id, to_user_id, ownable_name, amount):
                     WHERE user_id = ?
                     AND ownable_id = (SELECT rowid FROM ownables WHERE name = ?)
                     ''', (amount, from_user_id, ownable_name,))
-        if not cursor.fetchone():
-            return False
 
     cursor.execute('''
                 UPDATE ownership
@@ -139,8 +141,6 @@ def send_ownable(from_user_id, to_user_id, ownable_name, amount):
                 WHERE user_id = ?
                 AND ownable_id = (SELECT rowid FROM ownables WHERE name = ?)
                 ''', (amount, to_user_id, ownable_name))
-    if cursor.rowcount == 0:
-        return False
     return True
 
 
@@ -235,7 +235,7 @@ def get_user_id_by_session_id(session_id):
         ''', (session_id,))
 
     ids = cursor.fetchone()
-    if ids is None:
+    if not ids:
         return False
     return ids[0]
 
@@ -274,8 +274,6 @@ def activate_key(key, user_id):
                 AND key = ?
                 ''', (user_id, key,))
 
-    if cursor.rowcount == 0:
-        raise AssertionError
     send_ownable(bank_id(), user_id, CURRENCY_NAME, 1000)
 
 
@@ -323,18 +321,48 @@ def get_user_orders(user_id):
                 WHEN orders.stop_loss THEN 'Yes'
                 ELSE 'No'
             END, 
-            orders.ordered_amount
-            orders.expiry_dt
+            orders.ordered_amount,
+            datetime(orders.expiry_dt),
+            orders.rowid
         FROM orders, ownables, ownership
         WHERE ownership.user_id = ?
         AND ownership.ownable_id = ownables.rowid
         AND orders.ownership_id = ownership.rowid
-        ORDER BY orders.buy DESC, ownables.name ASC
+        ORDER BY ownables.name ASC, orders.stop_loss ASC, orders.buy DESC, orders."limit" ASC
         ''', (user_id,))
 
     return cursor.fetchall()
 
 
+def get_ownable_orders(ownable_id):
+    connect()
+
+    cursor.execute('''
+        SELECT 
+            CASE 
+                WHEN orders.buy THEN 'Buy'
+                ELSE 'Sell'
+            END,
+            ownables.name, 
+            orders.ordered_amount - orders.executed_amount, 
+            orders."limit", 
+            CASE 
+                WHEN orders."limit" IS NULL THEN NULL 
+                WHEN orders.stop_loss THEN 'Yes'
+                ELSE 'No'
+            END, 
+            datetime(orders.expiry_dt),
+            orders.rowid
+        FROM orders, ownables, ownership
+        WHERE ownership.ownable_id = ?
+        AND ownership.ownable_id = ownables.rowid
+        AND orders.ownership_id = ownership.rowid
+        ORDER BY ownables.name ASC, orders.stop_loss ASC, orders.buy DESC, orders."limit" ASC
+        ''', (ownable_id,))
+
+    return cursor.fetchall()
+
+
 def sell_ordered_amount(user_id, ownable_id):
     connect()
 
@@ -428,8 +456,10 @@ def new_stock(name=None):
 
     amount = random.randrange(100, 10000)
     price = random.randrange(10000, 20000) / amount
-    bank_order(True,
-               ownable_id_by_name(name),
+    ownable_id = ownable_id_by_name(name)
+    own(bank_id(), name)
+    bank_order(False,
+               ownable_id,
                price,
                amount,
                60)
@@ -477,14 +507,34 @@ def currency_id():
     return cursor.fetchone()[0]
 
 
+def user_money(user_id):
+    connect()
+
+    cursor.execute('''
+        SELECT amount
+        FROM ownership
+        WHERE user_id = ?
+        AND ownable_id = ?
+        ''', (user_id, currency_id()))
+
+    return cursor.fetchone()[0]
+
+
+def delete_order(order_id):
+    connect()
+
+    cursor.execute('''
+        DELETE FROM orders
+        WHERE rowid = ?
+        ''', (order_id,))
+
+
 def execute_orders(ownable_id):
     connect()
-    executed_any = True
-    while executed_any:
-        executed_any = False
+    while True:
         # find order to execute
         cursor.execute('''
-            SELECT buy_order.*, sell_order.*, buyer.*, seller.*
+            SELECT buy_order.*, sell_order.*, buyer.user_id, seller.user_id, buy_order.rowid, sell_order.rowid
             FROM orders buy_order, orders sell_order, ownership buyer, ownership seller
             WHERE buy_order.buy AND NOT sell_order.buy
             AND buyer.rowid = buy_order.ownership_id
@@ -494,67 +544,92 @@ def execute_orders(ownable_id):
             AND (buy_order."limit" IS NULL
                 OR sell_order."limit" IS NULL
                 OR (sell_order."limit" < buy_order."limit"
-                    AND NOT sell_order.stop_loss
-                    AND NOT buy_order.stop_loss))
+                    AND sell_order.stop_loss IS NULL
+                    AND buy_order.stop_loss IS NULL))
             ORDER BY COALESCE(sell_order."limit", 0) ASC, 
-                     COALESCE(buy_order."limit", 0) DESC
+                     -COALESCE(buy_order."limit", 0) ASC
             LIMIT 1
             ''', (ownable_id, ownable_id,))
 
         matching_orders = 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:
-            continue
+            break
+
+        # TODO continue and delete order if buyer has not enough money
 
-        # TODO compute execution price, amount, buyer_id and seller_id from matching_orders
-        price = -1
+        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
 
-        if price < 0 or amount < 0:
+        if not isinstance(buy_limit, str) and not isinstance(buy_limit, float):
+            return AssertionError()
+        if not isinstance(sell_limit, str) and not isinstance(sell_limit, float):
             return AssertionError()
 
-        # actually execute the order
-        cursor.execute('''
-            UPDATE ownership 
-            SET amount = amount - ?
-            WHERE user_id = ?
-            ''', (price, buyer_id))
-        if not cursor.fetchone():
+        if buy_limit == '' and sell_limit == '':
             raise AssertionError()
+        elif buy_limit == '':
+            price = sell_limit
+        elif sell_limit == '':
+            price = buy_limit
+        else:  # both not NULL
+            price = (float(sell_limit) + float(buy_limit)) / 2
 
-        cursor.execute('''
-            UPDATE ownership 
-            SET amount = amount - ?
-            WHERE user_id = ?
-            ''', (amount, seller_id))
+        buyer_money = user_money(buyer_id)
 
-        if not cursor.fetchone():
-            raise AssertionError()
-        cursor.execute('''
-            UPDATE ownership 
-            SET amount = amount + ?
-            WHERE user_id = ?
-            ''', (amount, buyer_id))
+        amount = min(buy_order_amount - buy_executed_amount,
+                     sell_order_amount - sell_executed_amount,
+                     floor(buyer_money / price))
 
-        if not cursor.fetchone():
-            raise AssertionError()
-        cursor.execute('''
-            UPDATE ownership 
-            SET amount = amount + ?
-            WHERE user_id = ?
-            ''', (price, seller_id))
-        if not cursor.fetchone():
-            raise AssertionError()
+        buy_order_finished = (buy_order_amount - buy_executed_amount - amount <= 0) or (
+                buyer_money - amount * price < price)
+        sell_order_finished = (sell_order_amount - sell_executed_amount - amount <= 0)
+
+        if price < 0 or amount <= 0:
+            return AssertionError()
+
+        # actually execute the order, but the bank does not send or recieve anything
+        if buyer_id != bank_id():  # buyer pays
+            cursor.execute('''
+                UPDATE ownership 
+                SET amount = amount - ?
+                WHERE user_id = ?
+                AND ownable_id = ?
+                ''', (price * amount, buyer_id, currency_id()))
+        if seller_id != bank_id():  # seller pays
+            cursor.execute('''
+                UPDATE ownership 
+                SET amount = amount - ?
+                WHERE rowid = ?
+                ''', (amount, sell_ownership_id))
+        if buyer_id != bank_id():  # buyer receives
+            cursor.execute('''
+                UPDATE ownership 
+                SET amount = amount + ?
+                WHERE rowid = ?
+                ''', (amount, buy_ownership_id))
+        if seller_id != bank_id():  # seller receives
+            cursor.execute('''
+                UPDATE ownership 
+                SET amount = amount + ?
+                WHERE user_id = ?
+                AND ownable_id = ?
+                ''', (amount, seller_id, currency_id()))
         cursor.execute('''
             UPDATE orders 
             SET executed_amount = executed_amount + ?
             WHERE rowid = ?
             OR rowid = ?
             ''', (amount, buy_order_id, sell_order_id))
-        if not cursor.fetchone():
-            raise AssertionError()
 
-        executed_any = True
+        if buy_order_finished:
+            delete_order(buy_order_id)
+        if sell_order_finished:
+            delete_order(sell_order_id)
 
-        if seller_id != buyer_id:  # prevent showing self-transactions to keep the price reasonable
+        if seller_id != buyer_id:  # prevent showing self-transactions
             cursor.execute('''
                 INSERT INTO transactions
                 (price, ownable_id, amount)
@@ -562,16 +637,15 @@ def execute_orders(ownable_id):
                 ''', (price, ownable_id, amount,))
 
         # trigger stop loss orders
-        cursor.execute('''
-            UPDATE orders
-            SET stop_loss = FALSE,
-                "limit" = NULL
-            WHERE stop_loss
-            AND (buy AND "limit" > ?)
-                OR (NOT buy AND "limit" < ?)
-            (price, ownable_id, amount)
-            VALUES(?, ?, ?)
-            ''', (price, price,))
+        if buyer_id != seller_id:
+            cursor.execute('''
+                UPDATE orders
+                SET stop_loss = NULL
+                    "limit" = NULL
+                WHERE stop_loss
+                AND (buy AND "limit" > ?)
+                    OR (NOT buy AND "limit" < ?)
+                ''', (price, price,))
 
 
 def ownable_id_by_ownership_id(ownership_id):
@@ -586,9 +660,21 @@ def ownable_id_by_ownership_id(ownership_id):
     return cursor.fetchone()[0]
 
 
+def ownable_name_by_id(ownable_id):
+    connect()
+
+    cursor.execute('''
+        SELECT name
+        FROM ownables
+        WHERE rowid = ?
+        ''', (ownable_id,))
+
+    return cursor.fetchone()[0]
+
+
 def bank_order(buy, ownable_id, limit, amount, time_until_expiration):
-    if limit is None:
-        raise AssertionError()
+    if not limit:
+        raise AssertionError('The bank does not give away anything.')
     place_order(buy,
                 get_ownership_id(ownable_id, bank_id()),
                 limit,
@@ -599,7 +685,7 @@ def bank_order(buy, ownable_id, limit, amount, time_until_expiration):
     cursor.execute('''
         INSERT INTO news(title)
         VALUES (?)
-        ''', ('External investors are selling ' + ownable_name))
+        ''', ('External investors are selling ' + ownable_name,))
 
 
 def place_order(buy, ownership_id, limit, stop_loss, amount, time_until_expiration):
@@ -628,3 +714,14 @@ def transactions(ownable_id):
         ''', (ownable_id,))
 
     return cursor.fetchall()
+
+
+def drop_expired_orders():
+    connect()
+
+    cursor.execute('''
+        DELETE FROM orders 
+        WHERE expiry_dt < DATETIME('now')
+        ''')
+
+    return cursor.fetchall()

+ 3 - 1
server.py

@@ -13,7 +13,7 @@ if __name__ == '__main__':
 
     model.setup()
 
-    valid_routes = ['login', 'register', 'depot', 'activate_key', 'order', 'orders', 'news', 'transactions']
+    valid_routes = ['login', 'register', 'depot', 'activate_key', 'order', 'orders', 'news', 'transactions', 'orders_on']
 
 
     @route('/<path>', method='POST')
@@ -23,6 +23,7 @@ if __name__ == '__main__':
         response.content_type = 'application/json'
         method_to_call = getattr(server_controller, path)
         try:
+            model.drop_expired_orders()
             resp = method_to_call()
             if response.status_code == 200:
                 model.connection.commit()
@@ -31,6 +32,7 @@ if __name__ == '__main__':
             return resp
         except sqlite3.IntegrityError as e:
             print(e)
+            model.connection.rollback()
             return server_controller.bad_request('Action violates database constraints.')
 
     run(host='localhost', port=connection.port, debug=debug)

+ 36 - 7
server_controller.py

@@ -3,7 +3,7 @@ import json
 from bottle import request, response
 
 import model
-from util import debug
+from util import debug, str2bool
 
 
 def missing_attributes(attributes):
@@ -76,6 +76,7 @@ def order():
         return bad_request(missing)
     if not model.ownable_name_exists(request.json['ownable']):
         return bad_request('This kind of object can not be ordered.')
+
     buy = request.json['buy']
     sell = not buy
     session_id = request.json['session_id']
@@ -89,10 +90,28 @@ def order():
     model.own(user_id, ownable_name)
     ownership_id = model.get_ownership_id(ownable_id, user_id)
 
-    limit = None
-    if 'limit' in request.json:
-        limit = request.json['limit']
-    stop_loss = 'stop_loss' in request.json and bool(request.json['stop_loss'])
+    try:
+        if request.json['limit'] == '':
+            limit = None
+        else:
+            limit = float(request.json['limit'])
+    except ValueError:  # for example when float fails
+        limit = None
+    except KeyError:  # for example when limit was not specified
+        limit = None
+
+    try:
+        if request.json['stop_loss'] == '':
+            stop_loss = None
+        else:
+            stop_loss = 'stop_loss' in request.json and str2bool(request.json['stop_loss'])
+        if stop_loss is not None and limit is None:
+            return bad_request('Can only set stop loss for limit orders')
+    except ValueError:  # for example when bool fails
+        return bad_request('Invalid value for stop loss')
+    except KeyError:  # for example when stop_loss was not specified
+        limit = None
+
     # TODO test if stop loss works
 
     if sell:
@@ -111,6 +130,16 @@ def orders():
     return {'data': data}
 
 
+def orders_on():
+    missing = missing_attributes(['session_id', 'ownable'])
+    if missing:
+        return bad_request(missing)
+    if not model.ownable_name_exists(request.json['ownable']):
+        return bad_request('This kind of object can not be ordered.')
+    data = model.get_ownable_orders(model.ownable_id_by_name(request.json['ownable']))
+    return {'data': data}
+
+
 def news():
     missing = missing_attributes(['session_id'])
     if missing:
@@ -119,12 +148,12 @@ def news():
 
 
 def transactions():
-    missing = missing_attributes(['session_id', 'ownable_id'])
+    missing = missing_attributes(['session_id', 'ownable'])
     if missing:
         return bad_request(missing)
     if not model.ownable_name_exists(request.json['ownable']):
         return bad_request('This kind of object can not have transactions.')
-    return {'data': model.transactions()}
+    return {'data': model.transactions(model.ownable_id_by_name(request.json['ownable']))}
 
 
 def not_found(msg=''):

+ 9 - 0
util.py

@@ -16,3 +16,12 @@ p = numpy.array(p) / sum(p)
 
 def random_chars(count):
     return ''.join(numpy.random.choice(chars, p=p) for _ in range(count))
+
+
+def str2bool(v):
+    v = v.strip().lower()
+    if v in ["yes", 'y' "true", "t", "1"]:
+        return True
+    if v in ["no", 'n' "false", "f", "0", '', 'null', 'none']:
+        return False
+    raise ValueError('Can not convert `' + v + '` to bool')