1
1

model.py 36 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225
  1. import json
  2. import os
  3. import random
  4. import re
  5. import sqlite3 as db
  6. import uuid
  7. from logging import INFO
  8. from math import floor
  9. from shutil import copyfile
  10. from typing import Optional, Dict
  11. from passlib.handlers.sha2_crypt import sha256_crypt
  12. import db_setup
  13. import trading_bot
  14. from game import CURRENCY_NAME, logger, DB_NAME
  15. from util import random_chars
  16. DBName = str
  17. connections: Dict[DBName, db.Connection] = {}
  18. current_connection: Optional[db.Connection] = None
  19. current_cursor: Optional[db.Cursor] = None
  20. current_db_name: Optional[DBName] = None
  21. current_user_id: Optional[int] = None
  22. def execute(sql, parameters=()):
  23. if not re.search(r"(?i)\s*SELECT", sql):
  24. logger.info(sql, 'sql_query', data=json.dumps(parameters))
  25. return current_cursor.execute(sql, parameters)
  26. def valid_db_name(name):
  27. return re.match(r"[a-z0-9.-]{0,20}", name)
  28. def query_save_name():
  29. while True:
  30. # save_name = input('Name of the database (You can also enter a new filename here): ')
  31. save_name = DB_NAME
  32. if valid_db_name(save_name):
  33. return save_name
  34. else:
  35. print('Must match "[a-z0-9.-]{0,20}"')
  36. def connect(db_name=None, create_if_not_exists=False):
  37. """
  38. connects to the database with the given name, if it exists
  39. if the database does not exist an exception is raised
  40. (unless create_if_not_exists is true, then the database is created)
  41. if there is already a connection to this database, that connection is used
  42. :return: the connection and the connections' cursor
  43. """
  44. if db_name is None:
  45. db_name = query_save_name()
  46. if not db_name.endswith('.db'):
  47. db_name += '.db'
  48. db_name = db_name.lower()
  49. if not os.path.isfile(db_name) and not create_if_not_exists:
  50. raise FileNotFoundError('There is no database with this name.')
  51. creating_new_db = not os.path.isfile(db_name)
  52. if db_name not in connections:
  53. try:
  54. db_connection = db.connect(db_name, check_same_thread=False)
  55. db_setup.create_functions(db_connection)
  56. db_setup.set_pragmas(db_connection.cursor())
  57. # connection.text_factory = lambda x: x.encode('latin-1')
  58. except db.Error as e:
  59. print("Database error %s:" % e.args[0])
  60. raise
  61. connections[db_name] = db_connection
  62. global current_connection
  63. global current_db_name
  64. global current_cursor
  65. current_connection = connections[db_name]
  66. current_cursor = connections[db_name].cursor()
  67. current_db_name = db_name
  68. if creating_new_db:
  69. try:
  70. if os.path.isfile('/test-db/' + db_name):
  71. print('Using test database containing fake data')
  72. copyfile('/test-db/' + db_name, db_name)
  73. else:
  74. logger.log('Creating database', INFO, 'database_creation')
  75. logger.commit()
  76. setup()
  77. except Exception:
  78. if current_connection is not None:
  79. current_connection.rollback()
  80. if db_name in connections:
  81. disconnect(db_name, rollback=True)
  82. os.remove(db_name)
  83. current_connection = None
  84. current_cursor = None
  85. current_db_name = None
  86. raise
  87. def disconnect(connection_name, rollback=True):
  88. global connections
  89. if connection_name not in connections:
  90. raise ValueError('Invalid connection')
  91. if rollback:
  92. connections[connection_name].rollback()
  93. else:
  94. connections[connection_name].commit()
  95. connections[connection_name].close()
  96. del connections[connection_name]
  97. def setup():
  98. db_setup.setup(current_cursor)
  99. def used_key_count():
  100. connect()
  101. execute('''
  102. SELECT COUNT(*) -- rarely executed, no index needed, O(n) query
  103. FROM keys
  104. WHERE used_by_user_id IS NOT NULL
  105. ''')
  106. return current_cursor.fetchone()[0]
  107. def login(username, password):
  108. execute('''
  109. SELECT rowid, password, salt
  110. FROM users
  111. WHERE username = ?
  112. ''', (username,))
  113. data = current_cursor.fetchone()
  114. if not data:
  115. return None
  116. user_id, hashed_password, salt = data
  117. # if a ValueError occurs here, then most likely a password that was stored as plain text
  118. if sha256_crypt.verify(password + salt, hashed_password):
  119. return new_session(user_id)
  120. else:
  121. return None
  122. def register(username, password, game_key):
  123. salt = str(uuid.uuid4())
  124. hashed_password = sha256_crypt.using(rounds=100000).encrypt(str(password) + salt)
  125. connect()
  126. if username == '':
  127. return False
  128. if password == '':
  129. return False
  130. execute('''
  131. INSERT INTO users
  132. (username, password, salt)
  133. VALUES (? , ?, ?)
  134. ''', (username, hashed_password, salt))
  135. if game_key != '':
  136. if valid_key(game_key):
  137. activate_key(game_key, get_user_id_by_name(username))
  138. return True
  139. def own(user_id, ownable_name, amount=0):
  140. if not isinstance(ownable_name, str):
  141. return AssertionError('A name must be a string.')
  142. execute('''
  143. INSERT OR IGNORE INTO ownership (user_id, ownable_id, amount)
  144. SELECT ?, (SELECT rowid FROM ownables WHERE name = ?), ?
  145. ''', (user_id, ownable_name, amount))
  146. def send_ownable(from_user_id, to_user_id, ownable_id, amount):
  147. connect()
  148. if amount < 0:
  149. raise AssertionError('Can not send negative amount')
  150. execute('''
  151. UPDATE ownership
  152. SET amount = amount - ?
  153. WHERE user_id = ?
  154. AND ownable_id = ?
  155. ''', (amount, from_user_id, ownable_id,))
  156. own(to_user_id, ownable_name_by_id(ownable_id))
  157. execute('''
  158. UPDATE ownership
  159. SET amount = amount + ?
  160. WHERE user_id = ?
  161. AND ownable_id = ?
  162. ''', (amount, to_user_id, ownable_id,))
  163. return True
  164. def valid_key(key):
  165. connect()
  166. execute('''
  167. SELECT key
  168. FROM keys
  169. WHERE used_by_user_id IS NULL
  170. AND key = ?
  171. ''', (key,))
  172. if current_cursor.fetchone():
  173. return True
  174. else:
  175. return False
  176. def new_session(user_id):
  177. connect()
  178. session_id = str(uuid.uuid4())
  179. execute('''
  180. INSERT INTO SESSIONS
  181. (user_id, session_id)
  182. VALUES (? , ?)
  183. ''', (user_id, session_id))
  184. return session_id
  185. def save_key(key):
  186. connect()
  187. execute('''
  188. INSERT INTO keys
  189. (key)
  190. VALUES (?)
  191. ''', (key,))
  192. def drop_old_sessions():
  193. connect()
  194. execute(''' -- no need to optimize this very well
  195. DELETE FROM sessions
  196. WHERE
  197. (SELECT COUNT(*) as newer
  198. FROM sessions s2
  199. WHERE user_id = s2.user_id
  200. AND rowid < s2.rowid) >= 10
  201. ''')
  202. def user_exists(username):
  203. connect()
  204. execute('''
  205. SELECT rowid
  206. FROM users
  207. WHERE username = ?
  208. ''', (username,))
  209. if current_cursor.fetchone():
  210. return True
  211. else:
  212. return False
  213. def get_user_id_by_session_id(session_id):
  214. connect()
  215. execute('''
  216. SELECT users.rowid
  217. FROM sessions, users
  218. WHERE sessions.session_id = ?
  219. AND users.rowid = sessions.user_id
  220. ''', (session_id,))
  221. ids = current_cursor.fetchone()
  222. if not ids:
  223. return False
  224. return ids[0]
  225. def get_user_id_by_name(username):
  226. connect()
  227. execute('''
  228. SELECT users.rowid
  229. FROM users
  230. WHERE username = ?
  231. ''', (username,))
  232. return current_cursor.fetchone()[0]
  233. def get_user_ownership(user_id):
  234. connect()
  235. execute('''
  236. SELECT
  237. ownables.name,
  238. ownership.amount,
  239. COALESCE (
  240. CASE -- sum score for each of the users ownables
  241. WHEN ownership.ownable_id = ? THEN 1
  242. ELSE (SELECT price
  243. FROM transactions
  244. WHERE ownable_id = ownership.ownable_id
  245. ORDER BY rowid DESC -- equivalent to ordering by dt
  246. LIMIT 1)
  247. END, 0) AS price,
  248. (SELECT MAX("limit")
  249. FROM orders, ownership o2
  250. WHERE o2.rowid = orders.ownership_id
  251. AND o2.ownable_id = ownership.ownable_id
  252. AND buy
  253. AND NOT stop_loss) AS bid,
  254. (SELECT MIN("limit")
  255. FROM orders, ownership o2
  256. WHERE o2.rowid = orders.ownership_id
  257. AND o2.ownable_id = ownership.ownable_id
  258. AND NOT buy
  259. AND NOT stop_loss) AS ask
  260. FROM ownership, ownables
  261. WHERE user_id = ?
  262. AND (ownership.amount > 0 OR ownership.ownable_id = ?)
  263. AND ownership.ownable_id = ownables.rowid
  264. ORDER BY ownables.rowid ASC
  265. ''', (currency_id(), user_id, currency_id(),))
  266. return current_cursor.fetchall()
  267. def activate_key(key, user_id):
  268. connect()
  269. execute('''
  270. UPDATE keys
  271. SET used_by_user_id = ?
  272. WHERE used_by_user_id IS NULL
  273. AND key = ?
  274. ''', (user_id, key,))
  275. send_ownable(bank_id(), user_id, currency_id(), 1000)
  276. def bank_id():
  277. connect()
  278. execute('''
  279. SELECT users.rowid
  280. FROM users
  281. WHERE username = 'bank'
  282. ''')
  283. return current_cursor.fetchone()[0]
  284. def valid_session_id(session_id):
  285. connect()
  286. execute('''
  287. SELECT rowid
  288. FROM sessions
  289. WHERE session_id = ?
  290. ''', (session_id,))
  291. if current_cursor.fetchone():
  292. return True
  293. else:
  294. return False
  295. def get_user_orders(user_id):
  296. connect()
  297. execute('''
  298. SELECT
  299. CASE
  300. WHEN orders.buy THEN 'Buy'
  301. ELSE 'Sell'
  302. END,
  303. ownables.name,
  304. (orders.ordered_amount - orders.executed_amount) || '/' || orders.ordered_amount,
  305. orders."limit",
  306. CASE
  307. WHEN orders."limit" IS NULL THEN NULL
  308. WHEN orders.stop_loss THEN 'Yes'
  309. ELSE 'No'
  310. END,
  311. datetime(orders.expiry_dt, 'localtime'),
  312. orders.rowid
  313. FROM orders, ownables, ownership
  314. WHERE ownership.user_id = ?
  315. AND ownership.ownable_id = ownables.rowid
  316. AND orders.ownership_id = ownership.rowid
  317. ORDER BY ownables.name ASC, orders.stop_loss ASC, orders.buy DESC, orders."limit" ASC
  318. ''', (user_id,))
  319. return current_cursor.fetchall()
  320. def get_ownable_orders(user_id, ownable_id):
  321. connect()
  322. execute('''
  323. SELECT
  324. CASE
  325. WHEN ownership.user_id = ? THEN 'X'
  326. ELSE NULL
  327. END,
  328. CASE
  329. WHEN orders.buy THEN 'Buy'
  330. ELSE 'Sell'
  331. END,
  332. ownables.name,
  333. orders.ordered_amount - orders.executed_amount,
  334. orders."limit",
  335. datetime(orders.expiry_dt, 'localtime'),
  336. orders.rowid
  337. FROM orders, ownables, ownership
  338. WHERE ownership.ownable_id = ?
  339. AND ownership.ownable_id = ownables.rowid
  340. AND orders.ownership_id = ownership.rowid
  341. AND (orders.stop_loss IS NULL OR NOT orders.stop_loss)
  342. ORDER BY ownables.name ASC, orders.stop_loss ASC, orders.buy DESC, orders."limit" ASC
  343. ''', (user_id, ownable_id,))
  344. return current_cursor.fetchall()
  345. def sell_ordered_amount(user_id, ownable_id):
  346. connect()
  347. execute('''
  348. SELECT COALESCE(SUM(orders.ordered_amount - orders.executed_amount),0)
  349. FROM orders, ownership
  350. WHERE ownership.rowid = orders.ownership_id
  351. AND ownership.user_id = ?
  352. AND ownership.ownable_id = ?
  353. AND NOT orders.buy
  354. ''', (user_id, ownable_id))
  355. return current_cursor.fetchone()[0]
  356. def available_amount(user_id, ownable_id):
  357. connect()
  358. execute('''
  359. SELECT amount
  360. FROM ownership
  361. WHERE user_id = ?
  362. AND ownable_id = ?
  363. ''', (user_id, ownable_id))
  364. return current_cursor.fetchone()[0] - sell_ordered_amount(user_id, ownable_id)
  365. def user_has_at_least_available(amount, user_id, ownable_id):
  366. connect()
  367. if not isinstance(amount, float) and not isinstance(amount, int):
  368. # comparison of float with strings does not work so well in sql
  369. raise AssertionError()
  370. execute('''
  371. SELECT rowid
  372. FROM ownership
  373. WHERE user_id = ?
  374. AND ownable_id = ?
  375. AND amount - ? >= ?
  376. ''', (user_id, ownable_id, sell_ordered_amount(user_id, ownable_id), amount))
  377. if current_cursor.fetchone():
  378. return True
  379. else:
  380. return False
  381. def news():
  382. connect()
  383. execute('''
  384. SELECT dt, title FROM
  385. (SELECT *, rowid
  386. FROM news
  387. ORDER BY rowid DESC -- equivalent to order by dt
  388. LIMIT 20) n
  389. ORDER BY rowid ASC -- equivalent to order by dt
  390. ''')
  391. return current_cursor.fetchall()
  392. def ownable_name_exists(name):
  393. connect()
  394. execute('''
  395. SELECT rowid
  396. FROM ownables
  397. WHERE name = ?
  398. ''', (name,))
  399. if current_cursor.fetchone():
  400. return True
  401. else:
  402. return False
  403. def new_stock(expiry, name=None):
  404. connect()
  405. while name is None:
  406. name = random_chars(6)
  407. if ownable_name_exists(name):
  408. name = None
  409. execute('''
  410. INSERT INTO ownables(name)
  411. VALUES (?)
  412. ''', (name,))
  413. new_news('A new stock can now be bought: ' + name)
  414. if random.getrandbits(1):
  415. new_news('Experts expect the price of ' + name + ' to fall')
  416. else:
  417. new_news('Experts expect the price of ' + name + ' to rise')
  418. amount = random.randrange(100, 10000)
  419. price = random.randrange(10000, 20000) / amount
  420. ownable_id = ownable_id_by_name(name)
  421. own(bank_id(), name, amount)
  422. bank_order(False,
  423. ownable_id,
  424. price,
  425. amount,
  426. expiry)
  427. return name
  428. def ownable_id_by_name(ownable_name):
  429. connect()
  430. execute('''
  431. SELECT rowid
  432. FROM ownables
  433. WHERE name = ?
  434. ''', (ownable_name,))
  435. return current_cursor.fetchone()[0]
  436. def get_ownership_id(ownable_id, user_id):
  437. connect()
  438. execute('''
  439. SELECT rowid
  440. FROM ownership
  441. WHERE ownable_id = ?
  442. AND user_id = ?
  443. ''', (ownable_id, user_id,))
  444. return current_cursor.fetchone()[0]
  445. def currency_id():
  446. connect()
  447. execute('''
  448. SELECT rowid
  449. FROM ownables
  450. WHERE name = ?
  451. ''', (CURRENCY_NAME,))
  452. return current_cursor.fetchone()[0]
  453. def user_money(user_id):
  454. connect()
  455. execute('''
  456. SELECT amount
  457. FROM ownership
  458. WHERE user_id = ?
  459. AND ownable_id = ?
  460. ''', (user_id, currency_id()))
  461. return current_cursor.fetchone()[0]
  462. def delete_order(order_id, new_order_status):
  463. connect()
  464. execute('''
  465. INSERT INTO order_history
  466. (ownership_id, buy, "limit", ordered_amount, executed_amount, expiry_dt, status, order_id)
  467. SELECT
  468. ownership_id,
  469. buy,
  470. "limit",
  471. ordered_amount,
  472. executed_amount,
  473. expiry_dt,
  474. ?,
  475. rowid
  476. FROM orders
  477. WHERE rowid = ?
  478. ''', (new_order_status, order_id,))
  479. execute('''
  480. DELETE FROM orders
  481. WHERE rowid = ?
  482. ''', (order_id,))
  483. def current_value(ownable_id):
  484. connect()
  485. if ownable_id == currency_id():
  486. return 1
  487. execute('''SELECT price
  488. FROM transactions
  489. WHERE ownable_id = ?
  490. ORDER BY rowid DESC -- equivalent to order by dt
  491. LIMIT 1
  492. ''', (ownable_id,))
  493. return current_cursor.fetchone()[0]
  494. def execute_orders(ownable_id):
  495. connect()
  496. orders_traded = False
  497. while True:
  498. # find order to execute
  499. execute('''
  500. -- two best orders
  501. SELECT * FROM (
  502. SELECT buy_order.*, sell_order.*, buyer.user_id, seller.user_id, buy_order.rowid, sell_order.rowid
  503. FROM orders buy_order, orders sell_order, ownership buyer, ownership seller
  504. WHERE buy_order.buy AND NOT sell_order.buy
  505. AND buyer.rowid = buy_order.ownership_id
  506. AND seller.rowid = sell_order.ownership_id
  507. AND buyer.ownable_id = ?
  508. AND seller.ownable_id = ?
  509. AND buy_order."limit" IS NULL
  510. AND sell_order."limit" IS NULL
  511. ORDER BY buy_order.rowid ASC,
  512. sell_order.rowid ASC
  513. LIMIT 1)
  514. UNION ALL -- best buy orders
  515. SELECT * FROM (
  516. SELECT buy_order.*, sell_order.*, buyer.user_id, seller.user_id, buy_order.rowid, sell_order.rowid
  517. FROM orders buy_order, orders sell_order, ownership buyer, ownership seller
  518. WHERE buy_order.buy AND NOT sell_order.buy
  519. AND buyer.rowid = buy_order.ownership_id
  520. AND seller.rowid = sell_order.ownership_id
  521. AND buyer.ownable_id = ?
  522. AND seller.ownable_id = ?
  523. AND buy_order."limit" IS NULL
  524. AND sell_order."limit" IS NOT NULL
  525. AND NOT sell_order.stop_loss
  526. ORDER BY sell_order."limit" ASC,
  527. buy_order.rowid ASC,
  528. sell_order.rowid ASC
  529. LIMIT 1)
  530. UNION ALL -- best sell orders
  531. SELECT * FROM (
  532. SELECT buy_order.*, sell_order.*, buyer.user_id, seller.user_id, buy_order.rowid, sell_order.rowid
  533. FROM orders buy_order, orders sell_order, ownership buyer, ownership seller
  534. WHERE buy_order.buy AND NOT sell_order.buy
  535. AND buyer.rowid = buy_order.ownership_id
  536. AND seller.rowid = sell_order.ownership_id
  537. AND buyer.ownable_id = ?
  538. AND seller.ownable_id = ?
  539. AND buy_order."limit" IS NOT NULL
  540. AND NOT buy_order.stop_loss
  541. AND sell_order."limit" IS NULL
  542. ORDER BY buy_order."limit" DESC,
  543. buy_order.rowid ASC,
  544. sell_order.rowid ASC
  545. LIMIT 1)
  546. UNION ALL -- both limit orders
  547. SELECT * FROM (
  548. SELECT buy_order.*, sell_order.*, buyer.user_id, seller.user_id, buy_order.rowid, sell_order.rowid
  549. FROM orders buy_order, orders sell_order, ownership buyer, ownership seller
  550. WHERE buy_order.buy AND NOT sell_order.buy
  551. AND buyer.rowid = buy_order.ownership_id
  552. AND seller.rowid = sell_order.ownership_id
  553. AND buyer.ownable_id = ?
  554. AND seller.ownable_id = ?
  555. AND buy_order."limit" IS NOT NULL
  556. AND sell_order."limit" IS NOT NULL
  557. AND sell_order."limit" <= buy_order."limit"
  558. AND NOT sell_order.stop_loss
  559. AND NOT buy_order.stop_loss
  560. ORDER BY buy_order."limit" DESC,
  561. sell_order."limit" ASC,
  562. buy_order.rowid ASC,
  563. sell_order.rowid ASC
  564. LIMIT 1)
  565. LIMIT 1
  566. ''', tuple(ownable_id for _ in range(8)))
  567. matching_orders = current_cursor.fetchone()
  568. # return type: (ownership_id,buy,limit,stop_loss,ordered_amount,executed_amount,expiry_dt,
  569. # ownership_id,buy,limit,stop_loss,ordered_amount,executed_amount,expiry_dt,
  570. # user_id,user_id,rowid,rowid)
  571. if not matching_orders:
  572. if not orders_traded:
  573. break
  574. # check if the trading bot has any new offers to make
  575. new_order_was_placed = trading_bot.notify_order_traded(ownable_id)
  576. if new_order_was_placed:
  577. orders_traded = False
  578. continue
  579. else:
  580. break
  581. buy_ownership_id, _, buy_limit, _, buy_order_amount, buy_executed_amount, buy_expiry_dt, \
  582. sell_ownership_id, _, sell_limit, _, sell_order_amount, sell_executed_amount, sell_expiry_dt, \
  583. buyer_id, seller_id, buy_order_id, sell_order_id \
  584. = matching_orders
  585. if buy_limit is None and sell_limit is None:
  586. price = current_value(ownable_id)
  587. elif buy_limit is None:
  588. price = sell_limit
  589. elif sell_limit is None:
  590. price = buy_limit
  591. else: # both not NULL
  592. # the price of the older order is used, just like in the real exchange
  593. if buy_order_id < sell_order_id:
  594. price = buy_limit
  595. else:
  596. price = sell_limit
  597. buyer_money = user_money(buyer_id)
  598. def _my_division(x, y):
  599. try:
  600. return floor(x / y)
  601. except ZeroDivisionError:
  602. return float('Inf')
  603. amount = min(buy_order_amount - buy_executed_amount,
  604. sell_order_amount - sell_executed_amount,
  605. _my_division(buyer_money, price))
  606. if amount == 0: # probable because buyer has not enough money
  607. delete_order(buy_order_id, 'Unable to pay')
  608. continue
  609. buy_order_finished = (buy_order_amount - buy_executed_amount - amount <= 0) or (
  610. buyer_money - amount * price < price)
  611. sell_order_finished = (sell_order_amount - sell_executed_amount - amount <= 0)
  612. if price < 0 or amount <= 0: # price of 0 is possible though unlikely
  613. return AssertionError()
  614. # actually execute the order, but the bank does not send or receive anything
  615. send_ownable(buyer_id, seller_id, currency_id(), price * amount)
  616. send_ownable(seller_id, buyer_id, ownable_id, amount)
  617. # update order execution state
  618. execute('''
  619. UPDATE orders
  620. SET executed_amount = executed_amount + ?
  621. WHERE rowid = ?
  622. OR rowid = ?
  623. ''', (amount, buy_order_id, sell_order_id))
  624. if buy_order_finished:
  625. delete_order(buy_order_id, 'Executed')
  626. orders_traded = True
  627. if sell_order_finished:
  628. delete_order(sell_order_id, 'Executed')
  629. orders_traded = True
  630. if seller_id != buyer_id: # prevent showing self-transactions
  631. execute('''
  632. INSERT INTO transactions
  633. (price, ownable_id, amount, buyer_id, seller_id)
  634. VALUES(?, ?, ?, ?, ?)
  635. ''', (price, ownable_id, amount, buyer_id, seller_id))
  636. # trigger stop-loss orders
  637. if buyer_id != seller_id:
  638. execute('''
  639. UPDATE orders
  640. SET stop_loss = NULL,
  641. "limit" = NULL
  642. WHERE stop_loss IS NOT NULL
  643. AND stop_loss
  644. AND ? IN (SELECT ownable_id FROM ownership WHERE rowid = ownership_id)
  645. AND ((buy AND "limit" < ?) OR (NOT buy AND "limit" > ?))
  646. ''', (ownable_id, price, price,))
  647. def ownable_id_by_ownership_id(ownership_id):
  648. connect()
  649. execute('''
  650. SELECT ownable_id
  651. FROM ownership
  652. WHERE rowid = ?
  653. ''', (ownership_id,))
  654. return current_cursor.fetchone()[0]
  655. def ownable_name_by_id(ownable_id):
  656. connect()
  657. execute('''
  658. SELECT name
  659. FROM ownables
  660. WHERE rowid = ?
  661. ''', (ownable_id,))
  662. return current_cursor.fetchone()[0]
  663. def bank_order(buy, ownable_id, limit, amount, expiry):
  664. if not limit:
  665. raise AssertionError('The bank does not give away anything.')
  666. place_order(buy,
  667. get_ownership_id(ownable_id, bank_id()),
  668. limit,
  669. False,
  670. amount,
  671. expiry)
  672. ownable_name = ownable_name_by_id(ownable_id)
  673. new_news('External investors are selling ' + ownable_name + ' atm')
  674. def current_db_time(): # might differ from datetime.datetime.now() for time zone reasons
  675. connect()
  676. execute('''
  677. SELECT datetime('now')
  678. ''')
  679. return current_cursor.fetchone()[0]
  680. def place_order(buy, ownership_id, limit, stop_loss, amount, expiry):
  681. connect()
  682. execute('''
  683. INSERT INTO orders
  684. (buy, ownership_id, "limit", stop_loss, ordered_amount, expiry_dt)
  685. VALUES (?, ?, ?, ?, ?, ?)
  686. ''', (buy, ownership_id, limit, stop_loss, amount, expiry))
  687. execute_orders(ownable_id_by_ownership_id(ownership_id))
  688. return True
  689. def trades_on(ownable_id, limit):
  690. connect()
  691. execute('''
  692. SELECT datetime(dt,'localtime'), amount, price
  693. FROM transactions
  694. WHERE ownable_id = ?
  695. ORDER BY rowid DESC -- equivalent to order by dt
  696. LIMIT ?
  697. ''', (ownable_id, limit,))
  698. return current_cursor.fetchall()
  699. def trades(user_id, limit):
  700. connect()
  701. execute('''
  702. SELECT
  703. (CASE WHEN seller_id = ? THEN 'Sell' ELSE 'Buy' END),
  704. (SELECT name FROM ownables WHERE rowid = transactions.ownable_id),
  705. amount,
  706. price,
  707. datetime(dt,'localtime')
  708. FROM transactions
  709. WHERE seller_id = ? OR buyer_id = ?
  710. ORDER BY rowid DESC -- equivalent to order by dt
  711. LIMIT ?
  712. ''', (user_id, user_id, user_id, limit,))
  713. return current_cursor.fetchall()
  714. def drop_expired_orders():
  715. connect()
  716. execute('''
  717. SELECT rowid, ownership_id, * FROM orders
  718. WHERE expiry_dt < DATETIME('now')
  719. ''')
  720. data = current_cursor.fetchall()
  721. for order in data:
  722. order_id = order[0]
  723. delete_order(order_id, 'Expired')
  724. return data
  725. def generate_keys(count=1):
  726. # source https://stackoverflow.com/questions/17049308/python-3-3-serial-key-generator-list-problems
  727. for i in range(count):
  728. key = '-'.join(random_chars(5) for _ in range(5))
  729. save_key(key)
  730. print(key)
  731. def user_has_order_with_id(session_id, order_id):
  732. connect()
  733. execute('''
  734. SELECT orders.rowid
  735. FROM orders, ownership, sessions
  736. WHERE orders.rowid = ?
  737. AND sessions.session_id = ?
  738. AND sessions.user_id = ownership.user_id
  739. AND ownership.rowid = orders.ownership_id
  740. ''', (order_id, session_id,))
  741. if current_cursor.fetchone():
  742. return True
  743. else:
  744. return False
  745. def leaderboard():
  746. connect()
  747. execute('''
  748. SELECT *
  749. FROM ( -- one score for each user
  750. SELECT
  751. username,
  752. SUM(CASE -- sum score for each of the users ownables
  753. WHEN ownership.ownable_id = ? THEN ownership.amount
  754. ELSE ownership.amount * (SELECT price
  755. FROM transactions
  756. WHERE ownable_id = ownership.ownable_id
  757. ORDER BY rowid DESC -- equivalent to ordering by dt
  758. LIMIT 1)
  759. END
  760. ) score
  761. FROM users, ownership
  762. WHERE ownership.user_id = users.rowid
  763. AND users.username != 'bank'
  764. GROUP BY users.rowid
  765. ) AS scores
  766. ORDER BY score DESC
  767. LIMIT 50
  768. ''', (currency_id(),))
  769. return current_cursor.fetchall()
  770. def user_wealth(user_id):
  771. connect()
  772. execute('''
  773. SELECT COALESCE(SUM(
  774. CASE -- sum score for each of the users ownables
  775. WHEN ownership.ownable_id = ? THEN ownership.amount
  776. ELSE ownership.amount * (SELECT price
  777. FROM transactions
  778. WHERE ownable_id = ownership.ownable_id
  779. ORDER BY rowid DESC -- equivalent to ordering by dt
  780. LIMIT 1)
  781. END
  782. ), 0) score
  783. FROM ownership
  784. WHERE ownership.user_id = ?
  785. ''', (currency_id(), user_id,))
  786. return current_cursor.fetchone()[0]
  787. def change_password(session_id, password, salt):
  788. connect()
  789. execute('''
  790. UPDATE users
  791. SET password = ?, salt= ?
  792. WHERE rowid = (SELECT user_id FROM sessions WHERE sessions.session_id = ?)
  793. ''', (password, salt, session_id,))
  794. def sign_out_user(session_id):
  795. connect()
  796. execute('''
  797. DELETE FROM sessions
  798. WHERE user_id = (SELECT user_id FROM sessions s2 WHERE s2.session_id = ?)
  799. ''', (session_id,))
  800. def delete_user(user_id):
  801. connect()
  802. execute('''
  803. DELETE FROM sessions
  804. WHERE user_id = ?
  805. ''', (user_id,))
  806. execute('''
  807. DELETE FROM orders
  808. WHERE ownership_id IN (
  809. SELECT rowid FROM ownership WHERE user_id = ?)
  810. ''', (user_id,))
  811. execute('''
  812. DELETE FROM ownership
  813. WHERE user_id = ?
  814. ''', (user_id,))
  815. execute('''
  816. DELETE FROM keys
  817. WHERE used_by_user_id = ?
  818. ''', (user_id,))
  819. execute('''
  820. INSERT INTO news(title)
  821. VALUES ((SELECT username FROM users WHERE rowid = ?) || ' retired.')
  822. ''', (user_id,))
  823. execute('''
  824. DELETE FROM users
  825. WHERE rowid = ?
  826. ''', (user_id,))
  827. def delete_ownable(ownable_id):
  828. connect()
  829. execute('''
  830. DELETE FROM transactions
  831. WHERE ownable_id = ?
  832. ''', (ownable_id,))
  833. execute('''
  834. DELETE FROM orders
  835. WHERE ownership_id IN (
  836. SELECT rowid FROM ownership WHERE ownable_id = ?)
  837. ''', (ownable_id,))
  838. execute('''
  839. DELETE FROM order_history
  840. WHERE ownership_id IN (
  841. SELECT rowid FROM ownership WHERE ownable_id = ?)
  842. ''', (ownable_id,))
  843. # only delete empty ownerships
  844. execute('''
  845. DELETE FROM ownership
  846. WHERE ownable_id = ?
  847. AND amount = 0
  848. ''', (ownable_id,))
  849. execute('''
  850. INSERT INTO news(title)
  851. VALUES ((SELECT name FROM ownables WHERE rowid = ?) || ' can not be traded any more.')
  852. ''', (ownable_id,))
  853. execute('''
  854. DELETE FROM ownables
  855. WHERE rowid = ?
  856. ''', (ownable_id,))
  857. def hash_all_users_passwords():
  858. connect()
  859. execute('''
  860. SELECT rowid, password, salt
  861. FROM users
  862. ''')
  863. users = current_cursor.fetchall()
  864. for user_id, pw, salt in users:
  865. valid_hash = True
  866. try:
  867. sha256_crypt.verify('password' + salt, pw)
  868. except ValueError:
  869. valid_hash = False
  870. if valid_hash:
  871. raise AssertionError('There is already a hashed password in the database! Be careful what you are doing!')
  872. pw = sha256_crypt.encrypt(pw + salt)
  873. execute('''
  874. UPDATE users
  875. SET password = ?
  876. WHERE rowid = ?
  877. ''', (pw, user_id,))
  878. def new_news(message):
  879. connect()
  880. execute('''
  881. INSERT INTO news(title)
  882. VALUES (?)
  883. ''', (message,))
  884. def abs_spread(ownable_id):
  885. connect()
  886. execute('''
  887. SELECT
  888. (SELECT MAX("limit")
  889. FROM orders, ownership
  890. WHERE ownership.rowid = orders.ownership_id
  891. AND ownership.ownable_id = ?
  892. AND buy
  893. AND NOT stop_loss) AS bid,
  894. (SELECT MIN("limit")
  895. FROM orders, ownership
  896. WHERE ownership.rowid = orders.ownership_id
  897. AND ownership.ownable_id = ?
  898. AND NOT buy
  899. AND NOT stop_loss) AS ask
  900. ''', (ownable_id, ownable_id,))
  901. return current_cursor.fetchone()
  902. def ownables():
  903. connect()
  904. execute('''
  905. SELECT name, course,
  906. (SELECT SUM(amount)
  907. FROM ownership
  908. WHERE ownership.ownable_id = ownables_with_course.rowid) market_size
  909. FROM (SELECT
  910. name, ownables.rowid,
  911. CASE WHEN ownables.rowid = ?
  912. THEN 1
  913. ELSE (SELECT price
  914. FROM transactions
  915. WHERE ownable_id = ownables.rowid
  916. ORDER BY rowid DESC -- equivalent to ordering by dt
  917. LIMIT 1) END course
  918. FROM ownables) ownables_with_course
  919. ''', (currency_id(),))
  920. data = current_cursor.fetchall()
  921. for idx in range(len(data)):
  922. # compute market cap
  923. row = data[idx]
  924. if row[1] is None:
  925. market_cap = None
  926. elif row[2] is None:
  927. market_cap = None
  928. else:
  929. market_cap = row[1] * row[2]
  930. data[idx] = (row[0], row[1], market_cap)
  931. return data
  932. def reset_bank():
  933. connect()
  934. execute('''
  935. DELETE FROM ownership
  936. WHERE user_id = ?
  937. ''', (bank_id(),))
  938. def cleanup():
  939. global connections
  940. global current_connection
  941. global current_cursor
  942. global current_db_name
  943. global current_user_id
  944. for name in connections:
  945. connections[name].rollback()
  946. connections[name].close()
  947. connections = []
  948. current_connection = None
  949. current_cursor = None
  950. current_db_name = None
  951. current_user_id = None
  952. def ownable_ids():
  953. connect()
  954. execute('''
  955. SELECT rowid FROM ownables
  956. ''')
  957. return [ownable_id[0] for ownable_id in current_cursor.fetchall()]
  958. def get_old_orders(user_id, include_executed, include_canceled, limit):
  959. connect()
  960. execute('''
  961. SELECT
  962. (CASE WHEN order_history.buy THEN 'Buy' ELSE 'Sell' END),
  963. ownables.name,
  964. (order_history.ordered_amount - order_history.executed_amount) || '/' || order_history.ordered_amount,
  965. order_history."limit",
  966. order_history.expiry_dt,
  967. order_history.order_id,
  968. order_history.status
  969. FROM order_history, ownership, ownables
  970. WHERE ownership.user_id = ?
  971. AND ownership.rowid = order_history.ownership_id
  972. AND ownables.rowid = ownership.ownable_id
  973. AND (
  974. (order_history.status = 'Executed' AND ?)
  975. OR
  976. ((order_history.status = 'Expired' OR order_history.status = 'Canceled') AND ?)
  977. )
  978. ORDER BY order_history.rowid DESC -- equivalent to ordering by creation time
  979. LIMIT ?
  980. ''', (user_id, include_executed, include_canceled, limit))
  981. return current_cursor.fetchall()