model.py 37 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262
  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. own(get_user_id_by_name(username), CURRENCY_NAME)
  139. return True
  140. def own(user_id, ownable_name, amount=0):
  141. if not isinstance(ownable_name, str):
  142. return AssertionError('A name must be a string.')
  143. execute('''
  144. INSERT OR IGNORE INTO ownership (user_id, ownable_id, amount)
  145. SELECT ?, (SELECT rowid FROM ownables WHERE name = ?), ?
  146. ''', (user_id, ownable_name, amount))
  147. def send_ownable(from_user_id, to_user_id, ownable_id, amount):
  148. connect()
  149. if amount < 0:
  150. raise AssertionError('Can not send negative amount')
  151. execute('''
  152. UPDATE ownership
  153. SET amount = amount - ?
  154. WHERE user_id = ?
  155. AND ownable_id = ?
  156. ''', (amount, from_user_id, ownable_id,))
  157. own(to_user_id, ownable_name_by_id(ownable_id))
  158. execute('''
  159. UPDATE ownership
  160. SET amount = amount + ?
  161. WHERE user_id = ?
  162. AND ownable_id = ?
  163. ''', (amount, to_user_id, ownable_id,))
  164. return True
  165. def valid_key(key):
  166. connect()
  167. execute('''
  168. SELECT key
  169. FROM keys
  170. WHERE used_by_user_id IS NULL
  171. AND key = ?
  172. ''', (key,))
  173. if current_cursor.fetchone():
  174. return True
  175. else:
  176. return False
  177. def new_session(user_id):
  178. connect()
  179. session_id = str(uuid.uuid4())
  180. execute('''
  181. INSERT INTO SESSIONS
  182. (user_id, session_id)
  183. VALUES (? , ?)
  184. ''', (user_id, session_id))
  185. return session_id
  186. def save_key(key):
  187. connect()
  188. execute('''
  189. INSERT INTO keys
  190. (key)
  191. VALUES (?)
  192. ''', (key,))
  193. def drop_old_sessions():
  194. connect()
  195. execute(''' -- no need to optimize this very well
  196. DELETE FROM sessions
  197. WHERE
  198. (SELECT COUNT(*) as newer
  199. FROM sessions s2
  200. WHERE user_id = s2.user_id
  201. AND rowid < s2.rowid) >= 10
  202. ''')
  203. def user_exists(username):
  204. connect()
  205. execute('''
  206. SELECT rowid
  207. FROM users
  208. WHERE username = ?
  209. ''', (username,))
  210. if current_cursor.fetchone():
  211. return True
  212. else:
  213. return False
  214. def get_user_id_by_session_id(session_id):
  215. connect()
  216. execute('''
  217. SELECT users.rowid
  218. FROM sessions, users
  219. WHERE sessions.session_id = ?
  220. AND users.rowid = sessions.user_id
  221. ''', (session_id,))
  222. ids = current_cursor.fetchone()
  223. if not ids:
  224. return False
  225. return ids[0]
  226. def get_user_id_by_name(username):
  227. connect()
  228. execute('''
  229. SELECT users.rowid
  230. FROM users
  231. WHERE username = ?
  232. ''', (username,))
  233. return current_cursor.fetchone()[0]
  234. def get_user_ownership(user_id):
  235. connect()
  236. execute('''
  237. SELECT
  238. ownables.name,
  239. ownership.amount,
  240. COALESCE (
  241. CASE -- sum score for each of the users ownables
  242. WHEN ownership.ownable_id = ? THEN 1
  243. ELSE (SELECT price
  244. FROM transactions
  245. WHERE ownable_id = ownership.ownable_id
  246. ORDER BY rowid DESC -- equivalent to ordering by dt
  247. LIMIT 1)
  248. END, 0) AS price,
  249. (SELECT MAX("limit")
  250. FROM orders, ownership o2
  251. WHERE o2.rowid = orders.ownership_id
  252. AND o2.ownable_id = ownership.ownable_id
  253. AND buy
  254. AND NOT stop_loss) AS bid,
  255. (SELECT MIN("limit")
  256. FROM orders, ownership o2
  257. WHERE o2.rowid = orders.ownership_id
  258. AND o2.ownable_id = ownership.ownable_id
  259. AND NOT buy
  260. AND NOT stop_loss) AS ask
  261. FROM ownership, ownables
  262. WHERE user_id = ?
  263. AND (ownership.amount > 0 OR ownership.ownable_id = ?)
  264. AND ownership.ownable_id = ownables.rowid
  265. ORDER BY ownables.rowid ASC
  266. ''', (currency_id(), user_id, currency_id(),))
  267. return current_cursor.fetchall()
  268. def activate_key(key, user_id):
  269. connect()
  270. execute('''
  271. UPDATE keys
  272. SET used_by_user_id = ?
  273. WHERE used_by_user_id IS NULL
  274. AND key = ?
  275. ''', (user_id, key,))
  276. send_ownable(bank_id(), user_id, currency_id(), 1000)
  277. def bank_id():
  278. connect()
  279. execute('''
  280. SELECT users.rowid
  281. FROM users
  282. WHERE username = 'bank'
  283. ''')
  284. return current_cursor.fetchone()[0]
  285. def valid_session_id(session_id):
  286. connect()
  287. execute('''
  288. SELECT rowid
  289. FROM sessions
  290. WHERE session_id = ?
  291. ''', (session_id,))
  292. if current_cursor.fetchone():
  293. return True
  294. else:
  295. return False
  296. def get_user_orders(user_id):
  297. connect()
  298. execute('''
  299. SELECT
  300. CASE
  301. WHEN orders.buy THEN 'Buy'
  302. ELSE 'Sell'
  303. END,
  304. ownables.name,
  305. (orders.ordered_amount - orders.executed_amount) || '/' || orders.ordered_amount,
  306. orders."limit",
  307. CASE
  308. WHEN orders."limit" IS NULL THEN NULL
  309. WHEN orders.stop_loss THEN 'Yes'
  310. ELSE 'No'
  311. END,
  312. datetime(orders.expiry_dt, 'localtime'),
  313. orders.rowid
  314. FROM orders, ownables, ownership
  315. WHERE ownership.user_id = ?
  316. AND ownership.ownable_id = ownables.rowid
  317. AND orders.ownership_id = ownership.rowid
  318. ORDER BY ownables.name ASC, orders.stop_loss ASC, orders.buy DESC, orders."limit" ASC
  319. ''', (user_id,))
  320. return current_cursor.fetchall()
  321. def get_ownable_orders(user_id, ownable_id):
  322. connect()
  323. execute('''
  324. SELECT
  325. CASE
  326. WHEN ownership.user_id = ? THEN 'X'
  327. ELSE NULL
  328. END,
  329. CASE
  330. WHEN orders.buy THEN 'Buy'
  331. ELSE 'Sell'
  332. END,
  333. ownables.name,
  334. orders.ordered_amount - orders.executed_amount,
  335. orders."limit",
  336. datetime(orders.expiry_dt, 'localtime'),
  337. orders.rowid
  338. FROM orders, ownables, ownership
  339. WHERE ownership.ownable_id = ?
  340. AND ownership.ownable_id = ownables.rowid
  341. AND orders.ownership_id = ownership.rowid
  342. AND (orders.stop_loss IS NULL OR NOT orders.stop_loss)
  343. ORDER BY ownables.name ASC, orders.stop_loss ASC, orders.buy DESC, orders."limit" ASC
  344. ''', (user_id, ownable_id,))
  345. return current_cursor.fetchall()
  346. def sell_ordered_amount(user_id, ownable_id):
  347. connect()
  348. execute('''
  349. SELECT COALESCE(SUM(orders.ordered_amount - orders.executed_amount),0)
  350. FROM orders, ownership
  351. WHERE ownership.rowid = orders.ownership_id
  352. AND ownership.user_id = ?
  353. AND ownership.ownable_id = ?
  354. AND NOT orders.buy
  355. ''', (user_id, ownable_id))
  356. return current_cursor.fetchone()[0]
  357. def available_amount(user_id, ownable_id):
  358. connect()
  359. execute('''
  360. SELECT amount
  361. FROM ownership
  362. WHERE user_id = ?
  363. AND ownable_id = ?
  364. ''', (user_id, ownable_id))
  365. return current_cursor.fetchone()[0] - sell_ordered_amount(user_id, ownable_id)
  366. def user_has_at_least_available(amount, user_id, ownable_id):
  367. connect()
  368. if not isinstance(amount, float) and not isinstance(amount, int):
  369. # comparison of float with strings does not work so well in sql
  370. raise AssertionError()
  371. execute('''
  372. SELECT rowid
  373. FROM ownership
  374. WHERE user_id = ?
  375. AND ownable_id = ?
  376. AND amount - ? >= ?
  377. ''', (user_id, ownable_id, sell_ordered_amount(user_id, ownable_id), amount))
  378. if current_cursor.fetchone():
  379. return True
  380. else:
  381. return False
  382. def news():
  383. connect()
  384. execute('''
  385. SELECT dt, title FROM
  386. (SELECT *, rowid
  387. FROM news
  388. ORDER BY rowid DESC -- equivalent to order by dt
  389. LIMIT 20) n
  390. ORDER BY rowid ASC -- equivalent to order by dt
  391. ''')
  392. return current_cursor.fetchall()
  393. def ownable_name_exists(name):
  394. connect()
  395. execute('''
  396. SELECT rowid
  397. FROM ownables
  398. WHERE name = ?
  399. ''', (name,))
  400. if current_cursor.fetchone():
  401. return True
  402. else:
  403. return False
  404. def new_stock(expiry, name=None):
  405. connect()
  406. while name is None:
  407. name = random_chars(6)
  408. if ownable_name_exists(name):
  409. name = None
  410. execute('''
  411. INSERT INTO ownables(name)
  412. VALUES (?)
  413. ''', (name,))
  414. new_news('A new stock can now be bought: ' + name)
  415. if random.getrandbits(1):
  416. new_news('Experts expect the price of ' + name + ' to fall')
  417. else:
  418. new_news('Experts expect the price of ' + name + ' to rise')
  419. amount = random.randrange(100, 10000)
  420. price = random.randrange(10000, 20000) / amount
  421. ownable_id = ownable_id_by_name(name)
  422. own(bank_id(), name, amount)
  423. bank_order(False,
  424. ownable_id,
  425. price,
  426. amount,
  427. expiry)
  428. return name
  429. def ownable_id_by_name(ownable_name):
  430. connect()
  431. execute('''
  432. SELECT rowid
  433. FROM ownables
  434. WHERE name = ?
  435. ''', (ownable_name,))
  436. return current_cursor.fetchone()[0]
  437. def get_ownership_id(ownable_id, user_id):
  438. connect()
  439. execute('''
  440. SELECT rowid
  441. FROM ownership
  442. WHERE ownable_id = ?
  443. AND user_id = ?
  444. ''', (ownable_id, user_id,))
  445. return current_cursor.fetchone()[0]
  446. def currency_id():
  447. connect()
  448. execute('''
  449. SELECT rowid
  450. FROM ownables
  451. WHERE name = ?
  452. ''', (CURRENCY_NAME,))
  453. return current_cursor.fetchone()[0]
  454. def user_money(user_id):
  455. connect()
  456. execute('''
  457. SELECT amount
  458. FROM ownership
  459. WHERE user_id = ?
  460. AND ownable_id = ?
  461. ''', (user_id, currency_id()))
  462. return current_cursor.fetchone()[0]
  463. def delete_order(order_id, new_order_status):
  464. connect()
  465. execute('''
  466. INSERT INTO order_history
  467. (ownership_id, buy, "limit", ordered_amount, executed_amount, expiry_dt, status, order_id)
  468. SELECT
  469. ownership_id,
  470. buy,
  471. "limit",
  472. ordered_amount,
  473. executed_amount,
  474. expiry_dt,
  475. ?,
  476. rowid
  477. FROM orders
  478. WHERE rowid = ?
  479. ''', (new_order_status, order_id,))
  480. execute('''
  481. DELETE FROM orders
  482. WHERE rowid = ?
  483. ''', (order_id,))
  484. def current_value(ownable_id):
  485. connect()
  486. if ownable_id == currency_id():
  487. return 1
  488. execute('''SELECT price
  489. FROM transactions
  490. WHERE ownable_id = ?
  491. ORDER BY rowid DESC -- equivalent to order by dt
  492. LIMIT 1
  493. ''', (ownable_id,))
  494. return current_cursor.fetchone()[0]
  495. def execute_orders(ownable_id):
  496. connect()
  497. orders_traded = False
  498. while True:
  499. # find order to execute
  500. execute('''
  501. -- two best orders
  502. SELECT * FROM (
  503. SELECT buy_order.*, sell_order.*, buyer.user_id, seller.user_id, buy_order.rowid, sell_order.rowid
  504. FROM orders buy_order, orders sell_order, ownership buyer, ownership seller
  505. WHERE buy_order.buy AND NOT sell_order.buy
  506. AND buyer.rowid = buy_order.ownership_id
  507. AND seller.rowid = sell_order.ownership_id
  508. AND buyer.ownable_id = ?
  509. AND seller.ownable_id = ?
  510. AND buy_order."limit" IS NULL
  511. AND sell_order."limit" IS NULL
  512. ORDER BY buy_order.rowid ASC,
  513. sell_order.rowid ASC
  514. LIMIT 1)
  515. UNION ALL -- best buy orders
  516. SELECT * FROM (
  517. SELECT buy_order.*, sell_order.*, buyer.user_id, seller.user_id, buy_order.rowid, sell_order.rowid
  518. FROM orders buy_order, orders sell_order, ownership buyer, ownership seller
  519. WHERE buy_order.buy AND NOT sell_order.buy
  520. AND buyer.rowid = buy_order.ownership_id
  521. AND seller.rowid = sell_order.ownership_id
  522. AND buyer.ownable_id = ?
  523. AND seller.ownable_id = ?
  524. AND buy_order."limit" IS NULL
  525. AND sell_order."limit" IS NOT NULL
  526. AND NOT sell_order.stop_loss
  527. ORDER BY sell_order."limit" ASC,
  528. buy_order.rowid ASC,
  529. sell_order.rowid ASC
  530. LIMIT 1)
  531. UNION ALL -- best sell orders
  532. SELECT * FROM (
  533. SELECT buy_order.*, sell_order.*, buyer.user_id, seller.user_id, buy_order.rowid, sell_order.rowid
  534. FROM orders buy_order, orders sell_order, ownership buyer, ownership seller
  535. WHERE buy_order.buy AND NOT sell_order.buy
  536. AND buyer.rowid = buy_order.ownership_id
  537. AND seller.rowid = sell_order.ownership_id
  538. AND buyer.ownable_id = ?
  539. AND seller.ownable_id = ?
  540. AND buy_order."limit" IS NOT NULL
  541. AND NOT buy_order.stop_loss
  542. AND sell_order."limit" IS NULL
  543. ORDER BY buy_order."limit" DESC,
  544. buy_order.rowid ASC,
  545. sell_order.rowid ASC
  546. LIMIT 1)
  547. UNION ALL -- both limit orders
  548. SELECT * FROM (
  549. SELECT buy_order.*, sell_order.*, buyer.user_id, seller.user_id, buy_order.rowid, sell_order.rowid
  550. FROM orders buy_order, orders sell_order, ownership buyer, ownership seller
  551. WHERE buy_order.buy AND NOT sell_order.buy
  552. AND buyer.rowid = buy_order.ownership_id
  553. AND seller.rowid = sell_order.ownership_id
  554. AND buyer.ownable_id = ?
  555. AND seller.ownable_id = ?
  556. AND buy_order."limit" IS NOT NULL
  557. AND sell_order."limit" IS NOT NULL
  558. AND sell_order."limit" <= buy_order."limit"
  559. AND NOT sell_order.stop_loss
  560. AND NOT buy_order.stop_loss
  561. ORDER BY buy_order."limit" DESC,
  562. sell_order."limit" ASC,
  563. buy_order.rowid ASC,
  564. sell_order.rowid ASC
  565. LIMIT 1)
  566. LIMIT 1
  567. ''', tuple(ownable_id for _ in range(8)))
  568. matching_orders = current_cursor.fetchone()
  569. # return type: (ownership_id,buy,limit,stop_loss,ordered_amount,executed_amount,expiry_dt,
  570. # ownership_id,buy,limit,stop_loss,ordered_amount,executed_amount,expiry_dt,
  571. # user_id,user_id,rowid,rowid)
  572. if not matching_orders:
  573. if not orders_traded:
  574. break
  575. # check if the trading bot has any new offers to make
  576. new_order_was_placed = trading_bot.notify_order_traded(ownable_id)
  577. if new_order_was_placed:
  578. orders_traded = False
  579. continue
  580. else:
  581. break
  582. buy_ownership_id, _, buy_limit, _, buy_order_amount, buy_executed_amount, buy_expiry_dt, \
  583. sell_ownership_id, _, sell_limit, _, sell_order_amount, sell_executed_amount, sell_expiry_dt, \
  584. buyer_id, seller_id, buy_order_id, sell_order_id \
  585. = matching_orders
  586. if buy_limit is None and sell_limit is None:
  587. price = current_value(ownable_id)
  588. elif buy_limit is None:
  589. price = sell_limit
  590. elif sell_limit is None:
  591. price = buy_limit
  592. else: # both not NULL
  593. # the price of the older order is used, just like in the real exchange
  594. if buy_order_id < sell_order_id:
  595. price = buy_limit
  596. else:
  597. price = sell_limit
  598. buyer_money = user_money(buyer_id)
  599. def _my_division(x, y):
  600. try:
  601. return floor(x / y)
  602. except ZeroDivisionError:
  603. return float('Inf')
  604. amount = min(buy_order_amount - buy_executed_amount,
  605. sell_order_amount - sell_executed_amount,
  606. _my_division(buyer_money, price))
  607. if amount == 0: # probable because buyer has not enough money
  608. delete_order(buy_order_id, 'Unable to pay')
  609. continue
  610. buy_order_finished = (buy_order_amount - buy_executed_amount - amount <= 0) or (
  611. buyer_money - amount * price < price)
  612. sell_order_finished = (sell_order_amount - sell_executed_amount - amount <= 0)
  613. if price < 0 or amount <= 0: # price of 0 is possible though unlikely
  614. return AssertionError()
  615. # actually execute the order, but the bank does not send or receive anything
  616. send_ownable(buyer_id, seller_id, currency_id(), price * amount)
  617. send_ownable(seller_id, buyer_id, ownable_id, amount)
  618. # update order execution state
  619. execute('''
  620. UPDATE orders
  621. SET executed_amount = executed_amount + ?
  622. WHERE rowid = ?
  623. OR rowid = ?
  624. ''', (amount, buy_order_id, sell_order_id))
  625. if buy_order_finished:
  626. delete_order(buy_order_id, 'Executed')
  627. orders_traded = True
  628. if sell_order_finished:
  629. delete_order(sell_order_id, 'Executed')
  630. orders_traded = True
  631. if seller_id != buyer_id: # prevent showing self-transactions
  632. execute('''
  633. INSERT INTO transactions
  634. (price, ownable_id, amount, buyer_id, seller_id)
  635. VALUES(?, ?, ?, ?, ?)
  636. ''', (price, ownable_id, amount, buyer_id, seller_id))
  637. # trigger stop-loss orders
  638. if buyer_id != seller_id:
  639. execute('''
  640. UPDATE orders
  641. SET stop_loss = NULL,
  642. "limit" = NULL
  643. WHERE stop_loss IS NOT NULL
  644. AND stop_loss
  645. AND ? IN (SELECT ownable_id FROM ownership WHERE rowid = ownership_id)
  646. AND ((buy AND "limit" < ?) OR (NOT buy AND "limit" > ?))
  647. ''', (ownable_id, price, price,))
  648. def ownable_id_by_ownership_id(ownership_id):
  649. connect()
  650. execute('''
  651. SELECT ownable_id
  652. FROM ownership
  653. WHERE rowid = ?
  654. ''', (ownership_id,))
  655. return current_cursor.fetchone()[0]
  656. def ownable_name_by_id(ownable_id):
  657. connect()
  658. execute('''
  659. SELECT name
  660. FROM ownables
  661. WHERE rowid = ?
  662. ''', (ownable_id,))
  663. return current_cursor.fetchone()[0]
  664. def bank_order(buy, ownable_id, limit, amount, expiry):
  665. if not limit:
  666. raise AssertionError('The bank does not give away anything.')
  667. place_order(buy,
  668. get_ownership_id(ownable_id, bank_id()),
  669. limit,
  670. False,
  671. amount,
  672. expiry)
  673. ownable_name = ownable_name_by_id(ownable_id)
  674. new_news('External investors are selling ' + ownable_name + ' atm')
  675. def current_db_time(): # might differ from datetime.datetime.now() for time zone reasons
  676. connect()
  677. execute('''
  678. SELECT datetime('now')
  679. ''')
  680. return current_cursor.fetchone()[0]
  681. def place_order(buy, ownership_id, limit, stop_loss, amount, expiry):
  682. connect()
  683. execute('''
  684. INSERT INTO orders
  685. (buy, ownership_id, "limit", stop_loss, ordered_amount, expiry_dt)
  686. VALUES (?, ?, ?, ?, ?, ?)
  687. ''', (buy, ownership_id, limit, stop_loss, amount, expiry))
  688. execute_orders(ownable_id_by_ownership_id(ownership_id))
  689. return True
  690. def trades_on(ownable_id, limit):
  691. connect()
  692. execute('''
  693. SELECT datetime(dt,'localtime'), amount, price
  694. FROM transactions
  695. WHERE ownable_id = ?
  696. ORDER BY rowid DESC -- equivalent to order by dt
  697. LIMIT ?
  698. ''', (ownable_id, limit,))
  699. return current_cursor.fetchall()
  700. def trades(user_id, limit):
  701. connect()
  702. execute('''
  703. SELECT
  704. (CASE WHEN seller_id = ? THEN 'Sell' ELSE 'Buy' END),
  705. (SELECT name FROM ownables WHERE rowid = transactions.ownable_id),
  706. amount,
  707. price,
  708. datetime(dt,'localtime')
  709. FROM transactions
  710. WHERE seller_id = ? OR buyer_id = ?
  711. ORDER BY rowid DESC -- equivalent to order by dt
  712. LIMIT ?
  713. ''', (user_id, user_id, user_id, limit,))
  714. return current_cursor.fetchall()
  715. def drop_expired_orders():
  716. connect()
  717. execute('''
  718. SELECT rowid, ownership_id, * FROM orders
  719. WHERE expiry_dt < DATETIME('now')
  720. ''')
  721. data = current_cursor.fetchall()
  722. for order in data:
  723. order_id = order[0]
  724. delete_order(order_id, 'Expired')
  725. return data
  726. def generate_keys(count=1):
  727. # source https://stackoverflow.com/questions/17049308/python-3-3-serial-key-generator-list-problems
  728. for i in range(count):
  729. key = '-'.join(random_chars(5) for _ in range(5))
  730. save_key(key)
  731. print(key)
  732. def user_has_order_with_id(session_id, order_id):
  733. connect()
  734. execute('''
  735. SELECT orders.rowid
  736. FROM orders, ownership, sessions
  737. WHERE orders.rowid = ?
  738. AND sessions.session_id = ?
  739. AND sessions.user_id = ownership.user_id
  740. AND ownership.rowid = orders.ownership_id
  741. ''', (order_id, session_id,))
  742. if current_cursor.fetchone():
  743. return True
  744. else:
  745. return False
  746. def leaderboard():
  747. connect()
  748. execute('''
  749. SELECT *
  750. FROM ( -- one score for each user
  751. SELECT
  752. username,
  753. SUM(CASE -- sum score for each of the users ownables
  754. WHEN ownership.ownable_id = ? THEN ownership.amount
  755. ELSE ownership.amount * (SELECT price
  756. FROM transactions
  757. WHERE ownable_id = ownership.ownable_id
  758. ORDER BY rowid DESC -- equivalent to ordering by dt
  759. LIMIT 1)
  760. END
  761. ) score
  762. FROM users, ownership
  763. WHERE ownership.user_id = users.rowid
  764. AND users.username != 'bank'
  765. GROUP BY users.rowid
  766. ) AS scores
  767. ORDER BY score DESC
  768. LIMIT 50
  769. ''', (currency_id(),))
  770. return current_cursor.fetchall()
  771. def user_wealth(user_id):
  772. connect()
  773. execute('''
  774. SELECT COALESCE(SUM(
  775. CASE -- sum score for each of the users ownables
  776. WHEN ownership.ownable_id = ? THEN ownership.amount
  777. ELSE ownership.amount * (SELECT price
  778. FROM transactions
  779. WHERE ownable_id = ownership.ownable_id
  780. ORDER BY rowid DESC -- equivalent to ordering by dt
  781. LIMIT 1)
  782. END
  783. ), 0) score
  784. FROM ownership
  785. WHERE ownership.user_id = ?
  786. ''', (currency_id(), user_id,))
  787. return current_cursor.fetchone()[0]
  788. def change_password(session_id, password, salt):
  789. connect()
  790. execute('''
  791. UPDATE users
  792. SET password = ?, salt= ?
  793. WHERE rowid = (SELECT user_id FROM sessions WHERE sessions.session_id = ?)
  794. ''', (password, salt, session_id,))
  795. def sign_out_user(session_id):
  796. connect()
  797. execute('''
  798. DELETE FROM sessions
  799. WHERE user_id = (SELECT user_id FROM sessions s2 WHERE s2.session_id = ?)
  800. ''', (session_id,))
  801. def delete_user(user_id):
  802. connect()
  803. execute('''
  804. DELETE FROM sessions
  805. WHERE user_id = ?
  806. ''', (user_id,))
  807. execute('''
  808. DELETE FROM orders
  809. WHERE ownership_id IN (
  810. SELECT rowid FROM ownership WHERE user_id = ?)
  811. ''', (user_id,))
  812. execute('''
  813. DELETE FROM ownership
  814. WHERE user_id = ?
  815. ''', (user_id,))
  816. execute('''
  817. DELETE FROM keys
  818. WHERE used_by_user_id = ?
  819. ''', (user_id,))
  820. execute('''
  821. INSERT INTO news(title)
  822. VALUES ((SELECT username FROM users WHERE rowid = ?) || ' retired.')
  823. ''', (user_id,))
  824. execute('''
  825. DELETE FROM users
  826. WHERE rowid = ?
  827. ''', (user_id,))
  828. def delete_ownable(ownable_id):
  829. connect()
  830. execute('''
  831. DELETE FROM transactions
  832. WHERE ownable_id = ?
  833. ''', (ownable_id,))
  834. execute('''
  835. DELETE FROM orders
  836. WHERE ownership_id IN (
  837. SELECT rowid FROM ownership WHERE ownable_id = ?)
  838. ''', (ownable_id,))
  839. execute('''
  840. DELETE FROM order_history
  841. WHERE ownership_id IN (
  842. SELECT rowid FROM ownership WHERE ownable_id = ?)
  843. ''', (ownable_id,))
  844. # only delete empty ownerships
  845. execute('''
  846. DELETE FROM ownership
  847. WHERE ownable_id = ?
  848. AND amount = 0
  849. ''', (ownable_id,))
  850. execute('''
  851. INSERT INTO news(title)
  852. VALUES ((SELECT name FROM ownables WHERE rowid = ?) || ' can not be traded any more.')
  853. ''', (ownable_id,))
  854. execute('''
  855. DELETE FROM ownables
  856. WHERE rowid = ?
  857. ''', (ownable_id,))
  858. def hash_all_users_passwords():
  859. connect()
  860. execute('''
  861. SELECT rowid, password, salt
  862. FROM users
  863. ''')
  864. users = current_cursor.fetchall()
  865. for user_id, pw, salt in users:
  866. valid_hash = True
  867. try:
  868. sha256_crypt.verify('password' + salt, pw)
  869. except ValueError:
  870. valid_hash = False
  871. if valid_hash:
  872. raise AssertionError('There is already a hashed password in the database! Be careful what you are doing!')
  873. pw = sha256_crypt.encrypt(pw + salt)
  874. execute('''
  875. UPDATE users
  876. SET password = ?
  877. WHERE rowid = ?
  878. ''', (pw, user_id,))
  879. def new_news(message):
  880. connect()
  881. execute('''
  882. INSERT INTO news(title)
  883. VALUES (?)
  884. ''', (message,))
  885. def abs_spread(ownable_id):
  886. connect()
  887. execute('''
  888. SELECT
  889. (SELECT MAX("limit")
  890. FROM orders, ownership
  891. WHERE ownership.rowid = orders.ownership_id
  892. AND ownership.ownable_id = ?
  893. AND buy
  894. AND NOT stop_loss) AS bid,
  895. (SELECT MIN("limit")
  896. FROM orders, ownership
  897. WHERE ownership.rowid = orders.ownership_id
  898. AND ownership.ownable_id = ?
  899. AND NOT buy
  900. AND NOT stop_loss) AS ask
  901. ''', (ownable_id, ownable_id,))
  902. return current_cursor.fetchone()
  903. def ownables():
  904. connect()
  905. execute('''
  906. SELECT name, course,
  907. (SELECT SUM(amount)
  908. FROM ownership
  909. WHERE ownership.ownable_id = ownables_with_course.rowid) market_size
  910. FROM (SELECT
  911. name, ownables.rowid,
  912. CASE WHEN ownables.rowid = ?
  913. THEN 1
  914. ELSE (SELECT price
  915. FROM transactions
  916. WHERE ownable_id = ownables.rowid
  917. ORDER BY rowid DESC -- equivalent to ordering by dt
  918. LIMIT 1) END course
  919. FROM ownables) ownables_with_course
  920. ''', (currency_id(),))
  921. data = current_cursor.fetchall()
  922. for idx in range(len(data)):
  923. # compute market cap
  924. row = data[idx]
  925. if row[1] is None:
  926. market_cap = None
  927. elif row[2] is None:
  928. market_cap = None
  929. else:
  930. market_cap = row[1] * row[2]
  931. data[idx] = (row[0], row[1], market_cap)
  932. return data
  933. def reset_bank():
  934. connect()
  935. execute('''
  936. DELETE FROM ownership
  937. WHERE user_id = ?
  938. ''', (bank_id(),))
  939. def cleanup():
  940. global connections
  941. global current_connection
  942. global current_cursor
  943. global current_db_name
  944. global current_user_id
  945. for name in connections:
  946. connections[name].rollback()
  947. connections[name].close()
  948. connections = []
  949. current_connection = None
  950. current_cursor = None
  951. current_db_name = None
  952. current_user_id = None
  953. def ownable_ids():
  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()
  982. def user_has_banking_license(user_id):
  983. execute('''
  984. SELECT EXISTS (SELECT * FROM banks WHERE user_id = ?)
  985. ''', (user_id,))
  986. return current_cursor.fetchone()[0]
  987. def global_control_value(value_name):
  988. execute('''
  989. SELECT value
  990. FROM global_control_values
  991. WHERE value_name = ?
  992. AND dt = (SELECT MAX(dt) FROM global_control_values WHERE value_name = ?)
  993. ''', (value_name, value_name,))
  994. return current_cursor.fetchone()[0]
  995. def global_control_values():
  996. execute('''
  997. SELECT value_name, value
  998. FROM global_control_values v1
  999. WHERE dt IN (SELECT MAX(dt) FROM global_control_values v2 GROUP BY v2.value_name)
  1000. ''')
  1001. return {
  1002. row[0]: row[1] for row in current_cursor.fetchall()
  1003. }
  1004. def assign_banking_licence(user_id):
  1005. execute('''
  1006. INSERT INTO banks(user_id)
  1007. VALUES (?)
  1008. ''', (user_id,))