浏览代码

Fix timestamps, implement IOC orders

Eren Yilmaz 5 年之前
父节点
当前提交
165f59e148
共有 9 个文件被更改,包括 224 次插入231 次删除
  1. 84 33
      client_controller.py
  2. 12 12
      create_new_stock.py
  3. 14 2
      db_setup/create_triggers.py
  4. 9 8
      db_setup/tables.py
  5. 1 1
      game.py
  6. 1 1
      lib/db_log.py
  7. 32 102
      model.py
  8. 66 69
      server_controller.py
  9. 5 3
      trading_bot.py

+ 84 - 33
client_controller.py

@@ -5,7 +5,7 @@ from inspect import signature
 import connection
 from connection import client_request
 from debug import debug
-from game import DEFAULT_ORDER_EXPIRY, CURRENCY_NAME, MIN_INTEREST_INTERVAL
+from game import DEFAULT_ORDER_EXPIRY, CURRENCY_NAME, MIN_INTEREST_INTERVAL, CURRENCY_SYMBOL
 from routes import client_commands
 from run_client import fake_loading_bar
 from util import my_tabulate, yn_dialog
@@ -126,28 +126,46 @@ def change_pw(password=None, retype_pw=None):
 
 
 # noinspection PyShadowingBuiltins
-def help():
+def help(command=None):
     print('Allowed commands:')
     command_table = []
-    for cmd in client_commands:
-        this_module = sys.modules[__name__]
-        method = getattr(this_module, cmd)
-        params = signature(method).parameters
-        command_table.append([cmd] + [p for p in params])
-
-    print(my_tabulate(command_table, tablefmt='pipe', headers=['command',
-                                                               'param 1',
-                                                               'param 2',
-                                                               'param 3',
-                                                               'param 4',
-                                                               'param 5',
-                                                               ]))
-    print('NOTE:')
-    print('  All parameters for all commands are optional!')
-    print('  Commands can be combined in one line with ; between them.')
-    print('  Parameters are separated with whitespace.')
-    print('  Use . as a decimal separator.')
-    print('  Use 0/1 for boolean parameters (other strings might work).')
+    if command is None:
+        for cmd in client_commands:
+            this_module = sys.modules[__name__]
+            method = getattr(this_module, cmd)
+            params = signature(method).parameters
+            command_table.append([cmd] + [p for p in params])
+
+        print(my_tabulate(command_table, tablefmt='pipe', headers=['command',
+                                                                   'param 1',
+                                                                   'param 2',
+                                                                   'param 3',
+                                                                   'param 4',
+                                                                   'param 5',
+                                                                   'param 6',
+                                                                   ]))
+        print('NOTE:')
+        print('  All parameters for all commands are optional!')
+        print('  Commands can be combined in one line with ; between them.')
+        print('  Parameters are separated with whitespace.')
+        print('  Use . as a decimal separator.')
+        print('  Use 0/1 for boolean parameters (other strings might work).')
+    else:
+        for cmd in client_commands:
+            if cmd.strip() == command.strip():
+                this_module = sys.modules[__name__]
+                method = getattr(this_module, cmd)
+                params = signature(method).parameters
+                command_table.append([cmd] + [p for p in params])
+                break
+        else:
+            print('Command not found:', command)
+            return
+
+        print(my_tabulate(command_table, tablefmt='pipe', headers=['command',
+                                                                   *[f'param {idx}' for idx in range(len(params))],
+                                                                   ]))
+
 
 
 def depot():
@@ -201,14 +219,26 @@ def activate_key(key=''):
         print('Key activation failed with message:', response['error'])
 
 
-def _order(is_buy_order, obj_name, amount, limit, stop_loss, expiry):
-    if stop_loss not in [None, '1', '0']:
-        print('Invalid value for flag stop loss (only 0 or 1 allowed).')
-        return
+def _order(is_buy_order, obj_name=None, amount=None, limit=None, stop_loss=None, expiry=None, ioc=None):
     if obj_name is None:  # TODO list some available objects
         obj_name = input('Name of object to sell: ')
+        obj_name = obj_name.strip()
+        if obj_name == '':
+            return
+
+    if stop_loss.strip() not in [None, '1', '0']:
+        print('Invalid value for flag stop loss (only 0 or 1 allowed).')
+        return
+    elif stop_loss is not None:
+        stop_loss = bool(int(stop_loss))
+
+    if obj_name == CURRENCY_NAME.replace(CURRENCY_SYMBOL, 'K'):
+        obj_name = CURRENCY_NAME
+
     if amount is None:
         amount = input('Amount: ')
+        if amount == '':
+            return
 
     if limit is None:
         set_limit = yn_dialog('Do you want to place a limit?')
@@ -241,6 +271,15 @@ def _order(is_buy_order, obj_name, amount, limit, stop_loss, expiry):
         print('Invalid expiration time.')
         return
 
+    if ioc is None and not stop_loss:
+        ioc = yn_dialog('Is this an IOC (immediate-or-cancel) order?')
+
+    if str(ioc).strip() not in ['1', '0', True, False]:
+        print('Invalid value for flag IOC (only 0 or 1 allowed).')
+        return
+    else:
+        ioc = bool(int(ioc))
+
     fake_loading_bar('Sending Data', duration=1.3)
     response = client_request('order', {
         "buy": is_buy_order,
@@ -249,7 +288,8 @@ def _order(is_buy_order, obj_name, amount, limit, stop_loss, expiry):
         "ownable": obj_name,
         "limit": limit,
         "stop_loss": stop_loss,
-        "time_until_expiration": expiry
+        "time_until_expiration": expiry,
+        "ioc": ioc,
     })
     if 'error' in response:
         print('Order placement failed with message:', response['error'])
@@ -258,22 +298,24 @@ def _order(is_buy_order, obj_name, amount, limit, stop_loss, expiry):
               'to see if the order has been executed already.')
 
 
-def sell(obj_name=None, amount=None, limit=None, stop_loss=None, expiry=None):
+def sell(obj_name=None, amount=None, limit=None, stop_loss=None, expiry=None, ioc=None):
     _order(is_buy_order=False,
            obj_name=obj_name,
            amount=amount,
            limit=limit,
            stop_loss=stop_loss,
-           expiry=expiry)
+           expiry=expiry,
+           ioc=ioc,)
 
 
-def buy(obj_name=None, amount=None, limit=None, stop_loss=None, expiry=None):
+def buy(obj_name=None, amount=None, limit=None, stop_loss=None, expiry=None, ioc=None):
     _order(is_buy_order=True,
            obj_name=obj_name,
            amount=amount,
            limit=limit,
            stop_loss=stop_loss,
-           expiry=expiry)
+           expiry=expiry,
+           ioc=ioc,)
 
 
 def orders():
@@ -363,12 +405,12 @@ def take_out_personal_loan(amount=None):
     response = client_request('take_out_personal_loan', {"session_id": connection.session_id, 'amount': amount})
     success = 'message' in response and 'error' not in response
     if success:
-        print(f'You took a personal loan of {amount} {CURRENCY_NAME}.')
+        print(f'You took out a personal loan of {amount} {CURRENCY_NAME}.')
     else:
         if 'error' in response:
-            print('Taking a personal loan failed with message:', response['error'])
+            print('Taking out a personal loan failed with message:', response['error'])
         else:
-            print('Taking a personal loan failed.')
+            print('Taking out a personal loan failed.')
 
 
 def loans():
@@ -424,6 +466,9 @@ def _global_variables():
 def orders_on(obj_name=None):
     if obj_name is None:  # TODO list some available objects
         obj_name = input('Name of object to check: ')
+
+    if obj_name == CURRENCY_NAME.replace(CURRENCY_SYMBOL, 'K'):
+        obj_name = CURRENCY_NAME
     fake_loading_bar('Loading Data', duration=2.3)
     response = client_request('orders_on', {"session_id": connection.session_id, "ownable": obj_name})
     success = 'data' in response
@@ -443,6 +488,9 @@ def gift(username=None, obj_name=None, amount=None):
         username = input('Username of recipient: ')
     if obj_name is None:
         obj_name = input('Name of object to give: ')
+
+    if obj_name == CURRENCY_NAME.replace(CURRENCY_SYMBOL, 'K'):
+        obj_name = CURRENCY_NAME
     if amount is None:
         amount = input('How many?: ')
     fake_loading_bar('Sending Gift', duration=4.2)
@@ -497,6 +545,9 @@ def trades_on(obj_name=None, limit=5):
     limit = float(limit)
     if obj_name is None:  # TODO list some available objects
         obj_name = input('Name of object to check: ')
+
+    if obj_name == CURRENCY_NAME.replace(CURRENCY_SYMBOL, 'K'):
+        obj_name = CURRENCY_NAME
     fake_loading_bar('Loading Data', duration=0.34 * limit)
     response = client_request('trades_on', {"session_id": connection.session_id,
                                             "ownable": obj_name,

+ 12 - 12
create_new_stock.py

@@ -1,12 +1,12 @@
-from datetime import timedelta, datetime
-
-import model
-
-if __name__ == '__main__':
-    timeout = float(input('How long will the initial selling be available? (minutes):'))
-    try:
-        expiry = datetime.strptime(model.current_db_time(), '%Y-%m-%d %H:%M:%S') + timedelta(minutes=timeout)
-        print(model.new_stock(expiry))
-        model.cleanup()
-    except OverflowError:
-        print('The expiration time is too far in the future.')
+from datetime import timedelta
+
+import model
+
+if __name__ == '__main__':
+    timeout = float(input('How long will the initial selling be available? (minutes):'))
+    try:
+        expiry = model.current_db_timestamp() + timedelta(minutes=timeout)
+        print(model.new_stock(expiry))
+        model.cleanup()
+    except OverflowError:
+        print('The expiration time is too far in the future.')

+ 14 - 2
db_setup/create_triggers.py

@@ -56,6 +56,18 @@ def create_triggers(cursor: sqlite3.Cursor):
                 WHEN NEW.ordered_amount <= 0 OR NEW.executed_amount < 0
                 BEGIN SELECT RAISE(ROLLBACK, 'Can not order 0 or less.'); END
                 ''')
+    cursor.execute('''
+                CREATE TRIGGER IF NOT EXISTS stop_loss_order_not_ioc_after_insert
+                AFTER INSERT ON orders
+                WHEN NEW.stop_loss AND NEW.ioc
+                BEGIN SELECT RAISE(ROLLBACK, 'Stop-loss orders can not be Immediate-or-cancel.'); END
+                ''')
+    cursor.execute('''
+                CREATE TRIGGER IF NOT EXISTS stop_loss_order_not_ioc_after_update
+                AFTER UPDATE ON orders
+                WHEN NEW.stop_loss AND NEW.ioc
+                BEGIN SELECT RAISE(ROLLBACK, 'Stop-loss orders can not be Immediate-or-cancel.'); END
+                ''')
     cursor.execute('''
                 CREATE TRIGGER IF NOT EXISTS not_more_executed_than_ordered_after_insert
                 AFTER INSERT ON orders
@@ -105,13 +117,13 @@ def create_triggers(cursor: sqlite3.Cursor):
     cursor.execute('''
                 CREATE TRIGGER IF NOT EXISTS expiry_dt_in_future_after_insert
                 AFTER INSERT ON orders
-                WHEN NEW.expiry_dt <= datetime('now')
+                WHEN NEW.expiry_dt <= CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)
                 BEGIN SELECT RAISE(ROLLBACK, 'Order is already expired.'); END
                 ''')
     cursor.execute('''
                 CREATE TRIGGER IF NOT EXISTS expiry_dt_in_future_after_update
                 AFTER UPDATE ON orders
-                WHEN NEW.expiry_dt <= datetime('now')
+                WHEN NEW.expiry_dt <= CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)
                 BEGIN SELECT RAISE(ROLLBACK, 'Order is already expired.'); END
                 ''')
     cursor.execute('''

+ 9 - 8
db_setup/tables.py

@@ -39,10 +39,11 @@ def tables(cursor):
                     ownership_id INTEGER NOT NULL,
                     buy BOOLEAN NOT NULL,
                     "limit" CURRENCY,
-                    stop_loss BOOLEAN,
+                    stop_loss BOOLEAN, -- TODO clarify what it means if this is NULL
                     ordered_amount CURRENCY NOT NULL,
                     executed_amount CURRENCY DEFAULT 0 NOT NULL,
-                    expiry_dt DATETIME NOT NULL,
+                    expiry_dt TIMESTAMP NOT NULL,
+                    ioc BOOLEAN NOT NULL,
                     FOREIGN KEY (ownership_id) REFERENCES ownership(rowid)
                 )
                 ''')
@@ -54,17 +55,17 @@ def tables(cursor):
                     "limit" CURRENCY,
                     ordered_amount CURRENCY NOT NULL,
                     executed_amount CURRENCY NOT NULL,
-                    expiry_dt DATETIME NOT NULL,
+                    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 DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+                    archived_dt TIMESTAMP NOT NULL DEFAULT (datetime('now','localtime')),
                     FOREIGN KEY (ownership_id) REFERENCES ownership(rowid)
                 )
                 ''')
     cursor.execute('''
                 CREATE TABLE IF NOT EXISTS transactions(
                     rowid INTEGER PRIMARY KEY,
-                    dt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+                    dt TIMESTAMP NOT NULL DEFAULT (datetime('now','localtime')),
                     price CURRENCY NOT NULL,
                     ownable_id INTEGER NOT NULL,
                     amount CURRENCY NOT NULL,
@@ -90,7 +91,7 @@ def tables(cursor):
     cursor.execute('''
                 CREATE TABLE IF NOT EXISTS news(
                     rowid INTEGER PRIMARY KEY,
-                    dt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+                    dt TIMESTAMP NOT NULL DEFAULT (datetime('now','localtime')),
                     title VARCHAR(50) NOT NULL
                 )
                 ''')
@@ -103,7 +104,7 @@ def tables(cursor):
     cursor.execute('''
                 CREATE TABLE IF NOT EXISTS global_control_values(
                     rowid INTEGER PRIMARY KEY,
-                    dt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+                    dt TIMESTAMP NOT NULL DEFAULT (datetime('now','localtime')),
                     value_name VARCHAR NOT NULL,
                     value FLOAT NOT NULL,
                     UNIQUE (value_name, dt)
@@ -115,7 +116,7 @@ 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 CURRENT_TIMESTAMP,
+                    last_interest_pay_dt TIMESTAMP NOT NULL DEFAULT (datetime('now','localtime')),
                     interest_rate CURRENCY NOT NULL -- determined from the global value 'personal_loan_interest_rate'
                 )
                 ''')

+ 1 - 1
game.py

@@ -1,7 +1,7 @@
 from lib.db_log import DBLog
 
 CURRENCY_NAME = "₭ollar"
-CURRENCY_Symbol = "₭"
+CURRENCY_SYMBOL = "₭"
 MINIMUM_ORDER_AMOUNT = 1
 DEFAULT_ORDER_EXPIRY = 43200
 DB_NAME = 'orderer'

+ 1 - 1
lib/db_log.py

@@ -71,7 +71,7 @@ class DBLog:
         self.cursor.execute('''
         CREATE TABLE IF NOT EXISTS entries(
             rowid INTEGER PRIMARY KEY,
-            dt_created DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+            dt_created TIMESTAMP NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)),
             message TEXT NOT NULL,
             data BLOB, -- can be null
             pid INTEGER NOT NULl,

+ 32 - 102
model.py

@@ -4,6 +4,7 @@ import random
 import re
 import sqlite3 as db
 import uuid
+from datetime import datetime
 from logging import INFO
 from math import floor
 from shutil import copyfile
@@ -120,8 +121,6 @@ def setup():
 
 
 def used_key_count():
-    connect()
-
     execute('''
         SELECT COUNT(*) -- rarely executed, no index needed, O(n) query
         FROM keys
@@ -179,8 +178,6 @@ def own(user_id, ownable_name, amount=0):
 
 
 def send_ownable(from_user_id, to_user_id, ownable_id, amount):
-    connect()
-
     if amount < 0:
         raise AssertionError('Can not send negative amount')
 
@@ -195,7 +192,7 @@ def send_ownable(from_user_id, to_user_id, ownable_id, amount):
 
     own(to_user_id, ownable_name_by_id(ownable_id))
 
-    if from_user_id != bank_id_ or ownable_id != currency_id():
+    if to_user_id != bank_id_ or ownable_id != currency_id():
         execute('''
                     UPDATE ownership
                     SET amount = amount + ?
@@ -206,8 +203,6 @@ def send_ownable(from_user_id, to_user_id, ownable_id, amount):
 
 
 def valid_key(key):
-    connect()
-
     execute('''
                 SELECT key
                 FROM keys
@@ -222,8 +217,6 @@ def valid_key(key):
 
 
 def new_session(user_id):
-    connect()
-
     session_id = str(uuid.uuid4())
 
     execute('''
@@ -236,8 +229,6 @@ def new_session(user_id):
 
 
 def save_key(key):
-    connect()
-
     execute('''
                 INSERT INTO keys 
                 (key)
@@ -246,8 +237,6 @@ def save_key(key):
 
 
 def drop_old_sessions():
-    connect()
-
     execute(''' -- no need to optimize this very well
                 DELETE FROM sessions
                 WHERE 
@@ -259,8 +248,6 @@ def drop_old_sessions():
 
 
 def user_exists(username):
-    connect()
-
     execute('''
                 SELECT rowid
                 FROM users
@@ -274,8 +261,6 @@ def user_exists(username):
 
 
 def get_user_id_by_session_id(session_id):
-    connect()
-
     execute('''
         SELECT users.rowid
         FROM sessions, users
@@ -290,8 +275,6 @@ def get_user_id_by_session_id(session_id):
 
 
 def get_user_id_by_name(username):
-    connect()
-
     execute('''
         SELECT users.rowid
         FROM users
@@ -302,8 +285,6 @@ def get_user_id_by_name(username):
 
 
 def get_user_ownership(user_id):
-    connect()
-
     execute('''
         SELECT 
             ownables.name, 
@@ -340,7 +321,6 @@ def get_user_ownership(user_id):
 
 
 def activate_key(key, user_id):
-    connect()
     execute('''
                 UPDATE keys
                 SET used_by_user_id = ?
@@ -352,8 +332,6 @@ def activate_key(key, user_id):
 
 
 def bank_id():
-    connect()
-
     execute('''
         SELECT users.rowid
         FROM users
@@ -364,8 +342,6 @@ def bank_id():
 
 
 def valid_session_id(session_id):
-    connect()
-
     execute('''
                 SELECT rowid
                 FROM sessions
@@ -379,8 +355,6 @@ def valid_session_id(session_id):
 
 
 def get_user_orders(user_id):
-    connect()
-
     execute('''
         SELECT 
             CASE 
@@ -408,8 +382,6 @@ def get_user_orders(user_id):
 
 
 def get_user_loans(user_id):
-    connect()
-
     execute('''
         SELECT 
             rowid, 
@@ -425,8 +397,6 @@ def get_user_loans(user_id):
 
 
 def get_ownable_orders(user_id, ownable_id):
-    connect()
-
     execute('''
         SELECT 
             CASE 
@@ -454,8 +424,6 @@ def get_ownable_orders(user_id, ownable_id):
 
 
 def sell_ordered_amount(user_id, ownable_id):
-    connect()
-
     execute('''
                 SELECT COALESCE(SUM(orders.ordered_amount - orders.executed_amount),0)
                 FROM orders, ownership
@@ -469,8 +437,6 @@ def sell_ordered_amount(user_id, ownable_id):
 
 
 def available_amount(user_id, ownable_id):
-    connect()
-
     execute('''
                 SELECT amount
                 FROM ownership
@@ -482,8 +448,6 @@ def available_amount(user_id, ownable_id):
 
 
 def user_has_at_least_available(amount, user_id, ownable_id):
-    connect()
-
     if not isinstance(amount, float) and not isinstance(amount, int):
         # comparison of float with strings does not work so well in sql
         raise AssertionError()
@@ -503,13 +467,11 @@ def user_has_at_least_available(amount, user_id, ownable_id):
 
 
 def news():
-    connect()
-
     execute('''
         SELECT dt, title FROM
             (SELECT *, rowid 
             FROM news
-            ORDER BY rowid DESC -- equivalent to order by dt
+            ORDER BY news.rowid DESC -- equivalent to order by dt
             LIMIT 20) n
         ORDER BY rowid ASC -- equivalent to order by dt
         ''')
@@ -518,8 +480,6 @@ def news():
 
 
 def ownable_name_exists(name):
-    connect()
-
     execute('''
                 SELECT rowid
                 FROM ownables
@@ -533,8 +493,6 @@ def ownable_name_exists(name):
 
 
 def new_stock(expiry, name=None):
-    connect()
-
     while name is None:
         name = random_chars(6)
         if ownable_name_exists(name):
@@ -559,13 +517,12 @@ def new_stock(expiry, name=None):
                ownable_id,
                price,
                amount,
-               expiry)
+               expiry,
+               ioc=False)
     return name
 
 
 def ownable_id_by_name(ownable_name):
-    connect()
-
     execute('''
         SELECT rowid
         FROM ownables
@@ -576,8 +533,6 @@ def ownable_id_by_name(ownable_name):
 
 
 def get_ownership_id(ownable_id, user_id):
-    connect()
-
     execute('''
         SELECT rowid
         FROM ownership
@@ -589,8 +544,6 @@ def get_ownership_id(ownable_id, user_id):
 
 
 def currency_id():
-    connect()
-
     execute('''
         SELECT rowid
         FROM ownables
@@ -601,8 +554,6 @@ def currency_id():
 
 
 def user_money(user_id):
-    connect()
-
     execute('''
         SELECT amount
         FROM ownership
@@ -614,8 +565,6 @@ def user_money(user_id):
 
 
 def delete_order(order_id, new_order_status):
-    connect()
-
     execute('''
         INSERT INTO order_history
         (ownership_id, buy, "limit", ordered_amount, executed_amount, expiry_dt, status, order_id)
@@ -639,8 +588,6 @@ def delete_order(order_id, new_order_status):
 
 
 def current_value(ownable_id):
-    connect()
-
     if ownable_id == currency_id():
         return 1
 
@@ -654,7 +601,6 @@ def current_value(ownable_id):
 
 
 def execute_orders(ownable_id):
-    connect()
     orders_traded = False
     while True:
         # find order to execute
@@ -743,8 +689,8 @@ def execute_orders(ownable_id):
             else:
                 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, \
+        _, 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
 
@@ -773,6 +719,9 @@ def execute_orders(ownable_id):
                      sell_order_amount - sell_executed_amount,
                      _my_division(buyer_money, price))
 
+        if amount < 0:
+            amount = 0
+
         if amount == 0:  # probable because buyer has not enough money
             delete_order(buy_order_id, 'Unable to pay')
             continue
@@ -824,8 +773,6 @@ def execute_orders(ownable_id):
 
 
 def ownable_id_by_ownership_id(ownership_id):
-    connect()
-
     execute('''
         SELECT ownable_id
         FROM ownership
@@ -836,8 +783,6 @@ def ownable_id_by_ownership_id(ownership_id):
 
 
 def ownable_name_by_id(ownable_id):
-    connect()
-
     execute('''
         SELECT name
         FROM ownables
@@ -847,7 +792,7 @@ def ownable_name_by_id(ownable_id):
     return current_cursor.fetchone()[0]
 
 
-def bank_order(buy, ownable_id, limit, amount, expiry):
+def bank_order(buy, ownable_id, limit, amount, expiry, ioc):
     if not limit:
         raise AssertionError('The bank does not give away anything.')
     place_order(buy,
@@ -855,7 +800,8 @@ def bank_order(buy, ownable_id, limit, amount, expiry):
                 limit,
                 False,
                 amount,
-                expiry)
+                expiry,
+                ioc=ioc)
     ownable_name = ownable_name_by_id(ownable_id)
     new_news('External investors are selling ' + ownable_name + ' atm')
 
@@ -870,21 +816,31 @@ def current_db_time():  # might differ from datetime.datetime.now() for time zon
     return current_cursor.fetchone()[0]
 
 
-def place_order(buy, ownership_id, limit, stop_loss, amount, expiry):
+def current_db_timestamp():
     connect()
+
     execute('''
-                INSERT INTO orders 
-                (buy, ownership_id, "limit", stop_loss, ordered_amount, expiry_dt)
-                VALUES (?, ?, ?, ?, ?, ?)
-                ''', (buy, ownership_id, limit, stop_loss, amount, expiry))
+        SELECT CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)
+        ''')
+
+    return int(current_cursor.fetchone()[0])
+
+
+def place_order(buy, ownership_id, limit, stop_loss, amount, expiry, ioc: bool):
+    if isinstance(expiry, datetime):
+        expiry = expiry.timestamp()
+    execute(''' INSERT INTO orders 
+                (buy, ownership_id, "limit", stop_loss, ordered_amount, expiry_dt, ioc)
+                VALUES (?, ?, ?, ?, ?, ?, ?)
+                ''', (buy, ownership_id, limit, stop_loss, amount, expiry, ioc))
 
     execute_orders(ownable_id_by_ownership_id(ownership_id))
+
+    execute('''DELETE FROM orders WHERE ioc''')
     return True
 
 
 def trades_on(ownable_id, limit):
-    connect()
-
     execute('''
         SELECT datetime(dt,'localtime'), amount, price
         FROM transactions
@@ -897,7 +853,6 @@ def trades_on(ownable_id, limit):
 
 
 def trades(user_id, limit):
-    connect()
     execute('''
         SELECT 
             (CASE WHEN seller_id = ? THEN 'Sell' ELSE 'Buy' END), 
@@ -915,11 +870,9 @@ def trades(user_id, limit):
 
 
 def drop_expired_orders():
-    connect()
-
     execute('''
         SELECT rowid, ownership_id, * FROM orders 
-        WHERE expiry_dt < DATETIME('now')
+        WHERE expiry_dt < CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)
         ''')
 
     data = current_cursor.fetchall()
@@ -940,8 +893,6 @@ def generate_keys(count=1):
 
 
 def user_has_order_with_id(session_id, order_id):
-    connect()
-
     execute('''
                 SELECT orders.rowid
                 FROM orders, ownership, sessions
@@ -958,8 +909,6 @@ def user_has_order_with_id(session_id, order_id):
 
 
 def leaderboard():
-    connect()
-
     execute('''
         SELECT * 
         FROM ( -- one score for each user
@@ -987,8 +936,6 @@ def leaderboard():
 
 
 def user_wealth(user_id):
-    connect()
-
     execute('''
     SELECT (
         SELECT COALESCE(SUM(
@@ -1015,8 +962,6 @@ def user_wealth(user_id):
 
 
 def change_password(session_id, password, salt):
-    connect()
-
     execute('''
                 UPDATE users
                 SET password = ?, salt= ?
@@ -1025,8 +970,6 @@ def change_password(session_id, password, salt):
 
 
 def sign_out_user(session_id):
-    connect()
-
     execute('''
         DELETE FROM sessions
         WHERE user_id = (SELECT user_id FROM sessions s2 WHERE s2.session_id = ?)
@@ -1034,8 +977,6 @@ def sign_out_user(session_id):
 
 
 def delete_user(user_id):
-    connect()
-
     execute('''
         DELETE FROM sessions
         WHERE user_id = ?
@@ -1069,8 +1010,6 @@ def delete_user(user_id):
 
 
 def delete_ownable(ownable_id):
-    connect()
-
     execute('''
         DELETE FROM transactions
         WHERE ownable_id = ?
@@ -1107,8 +1046,6 @@ def delete_ownable(ownable_id):
 
 
 def hash_all_users_passwords():
-    connect()
-
     execute('''
         SELECT rowid, password, salt
         FROM users
@@ -1133,7 +1070,6 @@ def hash_all_users_passwords():
 
 
 def new_news(message):
-    connect()
     execute('''
         INSERT INTO news(title)
         VALUES (?)
@@ -1141,8 +1077,6 @@ def new_news(message):
 
 
 def abs_spread(ownable_id):
-    connect()
-
     execute('''
         SELECT 
             (SELECT MAX("limit") 
@@ -1163,8 +1097,6 @@ def abs_spread(ownable_id):
 
 
 def ownables():
-    connect()
-
     execute('''
         SELECT name, course,
             (SELECT SUM(amount)
@@ -1199,7 +1131,6 @@ def ownables():
 
 
 def reset_bank():
-    connect()
     execute('''
         DELETE FROM ownership 
         WHERE user_id = ?
@@ -1231,7 +1162,6 @@ def ownable_ids():
 
 
 def get_old_orders(user_id, include_executed, include_canceled, limit):
-    connect()
     execute('''
         SELECT 
             (CASE WHEN order_history.buy THEN 'Buy' ELSE 'Sell' END),
@@ -1296,7 +1226,7 @@ def assign_banking_licence(user_id):
 
 
 def pay_loan_interest():
-    current_dt = execute("SELECT strftime('%s', CURRENT_TIMESTAMP)").fetchone()[0]
+    current_dt = execute("SELECT CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)").fetchone()[0]
     sec_per_year = 3600 * 24 * 365
     interests = execute('''
     SELECT 

+ 66 - 69
server_controller.py

@@ -1,31 +1,17 @@
 import uuid
-from datetime import timedelta, datetime
+from datetime import timedelta
 from math import ceil, floor
 
-from bottle import request
 from passlib.hash import sha256_crypt
 
 import model
 from connection import check_missing_attributes, BadRequest, Forbidden, PreconditionFailed, NotFound
 
 
-def missing_attributes(attributes):
-    for attr in attributes:
-        if attr not in request.json or request.json[attr] == '' or request.json[attr] is None:
-            if str(attr) == 'session_id':
-                return 'You are not signed in.'
-            return 'Missing value for attribute ' + str(attr)
-        if str(attr) == 'session_id':
-            if not model.valid_session_id(request.json['session_id']):
-                return 'You are not signed in.'
-    return False
-
-
 def login(json_request):
-
     check_missing_attributes(json_request, ['username', 'password'])
-    username = request.json['username']
-    password = request.json['password']
+    username = json_request['username']
+    password = json_request['password']
     session_id = model.login(username, password)
     if session_id:
         return {'session_id': session_id}
@@ -35,28 +21,29 @@ def login(json_request):
 
 def depot(json_request):
     check_missing_attributes(json_request, ['session_id'])
-    user_id = model.get_user_id_by_session_id(request.json['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}',
             'banking_license': model.user_has_banking_license(user_id)}
 
+
 def global_variables(_json_request):
     return model.global_control_values()
 
 
 def register(json_request):
     check_missing_attributes(json_request, ['username', 'password'])
-    username = request.json['username'].strip()
+    username = json_request['username'].strip()
     if username == '':
         return BadRequest('Username can not be empty.')
     if model.user_exists(username):
         return BadRequest('User already exists.')
     game_key = ''
-    if 'game_key' in request.json:
-        game_key = request.json['game_key'].strip().upper()
+    if 'game_key' in json_request:
+        game_key = json_request['game_key'].strip().upper()
         if game_key != '' and not model.valid_key(game_key):
             return BadRequest('Game key is not valid.')
-    if model.register(username, request.json['password'], game_key):
+    if model.register(username, json_request['password'], game_key):
         return {'message': "successfully registered user"}
     else:
         return BadRequest('Registration not successful')
@@ -64,9 +51,9 @@ def register(json_request):
 
 def activate_key(json_request):
     check_missing_attributes(json_request, ['key', 'session_id'])
-    if model.valid_key(request.json['key']):
-        user_id = model.get_user_id_by_session_id(request.json['session_id'])
-        model.activate_key(request.json['key'], user_id)
+    if model.valid_key(json_request['key']):
+        user_id = model.get_user_id_by_session_id(json_request['session_id'])
+        model.activate_key(json_request['key'], user_id)
         return {'message': "successfully activated key"}
     else:
         return BadRequest('Invalid key.')
@@ -74,16 +61,23 @@ def activate_key(json_request):
 
 def order(json_request):
     check_missing_attributes(json_request, ['buy', 'session_id', 'amount', 'ownable', 'time_until_expiration'])
-    if not model.ownable_name_exists(request.json['ownable']):
+    if not model.ownable_name_exists(json_request['ownable']):
         return BadRequest('This kind of object can not be ordered.')
 
-    buy = request.json['buy']
+    buy = json_request['buy']
     sell = not buy
     if not isinstance(buy, bool):
         return BadRequest('`buy` must be a boolean')
 
-    session_id = request.json['session_id']
-    amount = request.json['amount']
+    if 'ioc' in json_request:
+        ioc = json_request['ioc']
+        if not isinstance(ioc, bool):
+            raise BadRequest('IOC must be a boolean.')
+    else:
+        ioc = False
+
+    session_id = json_request['session_id']
+    amount = json_request['amount']
     try:
         amount = int(amount)
     except ValueError:
@@ -92,8 +86,8 @@ def order(json_request):
         return BadRequest('You can not order a negative amount.')
     if amount < 1:
         return BadRequest('The minimum order size is 1.')
-    ownable_name = request.json['ownable']
-    time_until_expiration = float(request.json['time_until_expiration'])
+    ownable_name = json_request['ownable']
+    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)
@@ -102,59 +96,62 @@ def order(json_request):
     ownership_id = model.get_ownership_id(ownable_id, user_id)
 
     try:
-        if request.json['limit'] == '':
+        if json_request['limit'] == '':
             limit = None
-        elif request.json['limit'] is None:
+        elif json_request['limit'] is None:
             limit = None
         else:
             if buy:
-                limit = floor(float(request.json['limit']) * 10000) / 10000
+                limit = floor(float(json_request['limit']) * 10000) / 10000
             else:
-                limit = ceil(float(request.json['limit']) * 10000) / 10000
+                limit = ceil(float(json_request['limit']) * 10000) / 10000
     except ValueError:  # for example when float fails
         return BadRequest('Invalid limit.')
     except KeyError:  # for example when limit was not specified
         limit = None
 
-    if limit < 0:
+    if limit is not None and limit < 0:
         return BadRequest('Limit must not be negative.')
 
-    try:
-        if request.json['stop_loss'] == '':
+    if 'stop_loss' in json_request:
+        if json_request['stop_loss'] == '':
             stop_loss = None
-        elif request.json['stop_loss'] is None:
+        elif json_request['stop_loss'] is None:
             stop_loss = None
         else:
-            stop_loss = 'stop_loss' in request.json and request.json['stop_loss']
-        if stop_loss is not None and limit is None:
-            return BadRequest('Can only set stop-loss for limit orders')
-    except KeyError:  # for example when stop_loss was not specified
+            stop_loss = json_request['stop_loss']
+    else:
         stop_loss = None
 
+    if stop_loss and limit is None:
+        return BadRequest('You need to specify a limit for stop-loss orders')
+
+    if ioc and stop_loss:
+        raise BadRequest('Stop loss orders can not be IOC orders.')
+
     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.')
     try:
-        expiry = datetime.strptime(model.current_db_time(), '%Y-%m-%d %H:%M:%S') + \
-                 timedelta(minutes=time_until_expiration)
+        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)
+    model.place_order(buy, ownership_id, limit, stop_loss, amount, expiry, ioc)
     return {'message': "Order placed."}
 
 
 def gift(json_request):
     check_missing_attributes(json_request, ['session_id', 'amount', 'object_name', 'username'])
-    if not model.ownable_name_exists(request.json['object_name']):
+    if not model.ownable_name_exists(json_request['object_name']):
         return BadRequest('This kind of object can not be given away.')
-    if request.json['username'] == 'bank' or not model.user_exists(request.json['username']):
+    if json_request['username'] == 'bank' or not model.user_exists(json_request['username']):
         return BadRequest('There is no user with this name.')
     try:
-        amount = float(request.json['amount'])
+        amount = float(json_request['amount'])
     except ValueError:
         return BadRequest('Invalid amount.')
-    ownable_id = model.ownable_id_by_name(request.json['object_name'])
-    sender_id = model.get_user_id_by_session_id(request.json['session_id'])
+    ownable_id = model.ownable_id_by_name(json_request['object_name'])
+    sender_id = model.get_user_id_by_session_id(json_request['session_id'])
 
     if model.available_amount(sender_id, ownable_id) == 0:
         return BadRequest('You do not own any of these.')
@@ -162,7 +159,7 @@ def gift(json_request):
         # for example if you have a 1.23532143213 Kollar and want to give them all away
         amount = model.available_amount(sender_id, ownable_id)
 
-    recipient_id = model.get_user_id_by_name(request.json['username'])
+    recipient_id = model.get_user_id_by_name(json_request['username'])
 
     model.send_ownable(sender_id,
                        recipient_id,
@@ -174,50 +171,50 @@ def gift(json_request):
 
 def orders(json_request):
     check_missing_attributes(json_request, ['session_id'])
-    data = model.get_user_orders(model.get_user_id_by_session_id(request.json['session_id']))
+    data = model.get_user_orders(model.get_user_id_by_session_id(json_request['session_id']))
     return {'data': data}
 
 
 def loans(json_request):
     check_missing_attributes(json_request, ['session_id'])
-    data = model.get_user_loans(model.get_user_id_by_session_id(request.json['session_id']))
+    data = model.get_user_loans(model.get_user_id_by_session_id(json_request['session_id']))
     return {'data': data}
 
 
 def orders_on(json_request):
     check_missing_attributes(json_request, ['session_id', 'ownable'])
-    if not model.ownable_name_exists(request.json['ownable']):
+    if not model.ownable_name_exists(json_request['ownable']):
         return BadRequest('This kind of object can not be ordered.')
-    user_id = model.get_user_id_by_session_id(request.json['session_id'])
-    ownable_id = model.ownable_id_by_name(request.json['ownable'])
+    user_id = model.get_user_id_by_session_id(json_request['session_id'])
+    ownable_id = model.ownable_id_by_name(json_request['ownable'])
     data = model.get_ownable_orders(user_id, ownable_id)
     return {'data': data}
 
 
 def old_orders(json_request):
     check_missing_attributes(json_request, ['session_id', 'include_canceled', 'include_executed', 'limit'])
-    include_executed = request.json['include_executed']
-    include_canceled = request.json['include_canceled']
-    user_id = model.get_user_id_by_session_id(request.json['session_id'])
-    limit = request.json['limit']
+    include_executed = json_request['include_executed']
+    include_canceled = json_request['include_canceled']
+    user_id = model.get_user_id_by_session_id(json_request['session_id'])
+    limit = json_request['limit']
     data = model.get_old_orders(user_id, include_executed, include_canceled, limit)
     return {'data': data}
 
 
 def cancel_order(json_request):
     check_missing_attributes(json_request, ['session_id', 'order_id'])
-    if not model.user_has_order_with_id(request.json['session_id'], request.json['order_id']):
+    if not model.user_has_order_with_id(json_request['session_id'], json_request['order_id']):
         return BadRequest('You do not have an order with that number.')
-    model.delete_order(request.json['order_id'], 'Canceled')
+    model.delete_order(json_request['order_id'], 'Canceled')
     return {'message': "Successfully deleted order"}
 
 
 def change_password(json_request):
     check_missing_attributes(json_request, ['session_id', 'password'])
     salt = str(uuid.uuid4())
-    hashed_password = sha256_crypt.encrypt(request.json['password'] + salt)
-    model.change_password(request.json['session_id'], hashed_password, salt)
-    model.sign_out_user(request.json['session_id'])
+    hashed_password = sha256_crypt.encrypt(json_request['password'] + salt)
+    model.change_password(json_request['session_id'], hashed_password, salt)
+    model.sign_out_user(json_request['session_id'])
     return {'message': "Successfully changed password"}
 
 
@@ -244,14 +241,14 @@ def tradables(_json_request):
 
 def trades(json_request):
     check_missing_attributes(json_request, ['session_id', 'limit'])
-    return {'data': model.trades(model.get_user_id_by_session_id(request.json['session_id']), request.json['limit'])}
+    return {'data': model.trades(model.get_user_id_by_session_id(json_request['session_id']), json_request['limit'])}
 
 
 def trades_on(json_request):
     check_missing_attributes(json_request, ['session_id', 'ownable', 'limit'])
-    if not model.ownable_name_exists(request.json['ownable']):
+    if not model.ownable_name_exists(json_request['ownable']):
         return BadRequest('This kind of object can not have transactions.')
-    return {'data': model.trades_on(model.ownable_id_by_name(request.json['ownable']), request.json['limit'])}
+    return {'data': model.trades_on(model.ownable_id_by_name(json_request['ownable']), json_request['limit'])}
 
 
 def leaderboard(_json_request):

+ 5 - 3
trading_bot.py

@@ -40,7 +40,7 @@ def place_order(ownable_id):
     amounts = [random.choice(amounts)[0] for _ in range(int(ceil(log2(len(amounts)))))]
     amount = ceil(sum(amounts) / len(amounts))
 
-    expiry = datetime.strptime(model.current_db_time(), '%Y-%m-%d %H:%M:%S') + timedelta(minutes=DEFAULT_ORDER_EXPIRY)
+    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:
@@ -49,14 +49,16 @@ def place_order(ownable_id):
                           limit=limit,
                           stop_loss=False,
                           amount=amount,
-                          expiry=expiry)
+                          expiry=expiry,
+                          ioc=False)
     else:
         model.place_order(buy=False,
                           ownership_id=ownership_id,
                           limit=limit,
                           stop_loss=False,
                           amount=amount,
-                          expiry=expiry)
+                          expiry=expiry,
+                          ioc=False)
     return True