source: server/db/advanced_db_tests.sql

main
Last change on this file was ef1c1c7, checked in by Stefan <trsunovstefan@…>, 6 days ago

Wiki docs, phase 6 and phase 7 added

  • Property mode set to 100644
File size: 16.4 KB
Line 
1-- advanced_db_tests.sql
2-- EduBerza - tests for the P7 rules in advanced_db.sql
3--
4-- Run right after data_load.sql (seed state: alice 8250 USD + 0.5 ETH,
5-- bob 5000 USD, charlie 2500 USD, ETH/USD last traded at 3520).
6-- It plays a short trading story and, along the way, tries to break every
7-- rule. Each check prints PASS/FAIL as a NOTICE; everything is rolled back
8-- at the end, so the data is left exactly as it was.
9--
10-- The reservation and balance checks are deferred to COMMIT; the tests force
11-- them with SET CONSTRAINTS ALL IMMEDIATE so a violation shows up inside the
12-- test instead of at the final COMMIT.
13
14BEGIN;
15SET search_path TO project, public;
16
17CREATE TEMP TABLE test_results (name text, passed boolean) ON COMMIT DROP;
18CREATE TEMP TABLE ids (name text PRIMARY KEY, id uuid) ON COMMIT DROP;
19
20-- expect_error: run p_sql (and the deferred checks); it must fail with an
21-- error containing p_fragment.
22CREATE PROCEDURE pg_temp.expect_error(p_name text, p_sql text, p_fragment text)
23LANGUAGE plpgsql AS $$
24BEGIN
25 BEGIN
26 EXECUTE p_sql;
27 SET CONSTRAINTS ALL IMMEDIATE;
28 RAISE NOTICE 'FAIL %: no error raised', p_name;
29 INSERT INTO test_results VALUES (p_name, false);
30 EXCEPTION WHEN OTHERS THEN
31 IF SQLERRM ILIKE '%' || p_fragment || '%' THEN
32 RAISE NOTICE 'PASS %: %', p_name, SQLERRM;
33 INSERT INTO test_results VALUES (p_name, true);
34 ELSE
35 RAISE NOTICE 'FAIL %: unexpected error: %', p_name, SQLERRM;
36 INSERT INTO test_results VALUES (p_name, false);
37 END IF;
38 END;
39 SET CONSTRAINTS ALL DEFERRED;
40END $$;
41
42CREATE FUNCTION pg_temp.expect_true(p_name text, p_ok boolean, p_detail text DEFAULT '')
43RETURNS void LANGUAGE plpgsql AS $$
44BEGIN
45 RAISE NOTICE '% %: %', CASE WHEN coalesce(p_ok, false) THEN 'PASS' ELSE 'FAIL' END, p_name, p_detail;
46 INSERT INTO test_results VALUES (p_name, coalesce(p_ok, false));
47END $$;
48
49-- consistent: all deferred checks pass right now
50CREATE FUNCTION pg_temp.consistent() RETURNS boolean LANGUAGE plpgsql AS $$
51BEGIN
52 SET CONSTRAINTS ALL IMMEDIATE;
53 SET CONSTRAINTS ALL DEFERRED;
54 RETURN true;
55EXCEPTION WHEN OTHERS THEN
56 RAISE NOTICE ' consistency check failed: %', SQLERRM;
57 RETURN false;
58END $$;
59
60CREATE FUNCTION pg_temp.uid(p_name text) RETURNS uuid LANGUAGE sql AS $$
61 SELECT id FROM users WHERE username = p_name
62$$;
63CREATE FUNCTION pg_temp.oid(p_name text) RETURNS uuid LANGUAGE sql AS $$
64 SELECT id FROM ids WHERE name = p_name
65$$;
66
67
68-- ===========================================================================
69-- A. Placing orders reserves what they commit
70-- ===========================================================================
71INSERT INTO ids VALUES ('alice_ask',
72 place_order(pg_temp.uid('alice'), 'a2222222-2222-2222-2222-222222222222', 'sell', 'limit', 0.3, 3600));
73INSERT INTO ids VALUES ('bob_bid',
74 place_order(pg_temp.uid('bob'), 'a2222222-2222-2222-2222-222222222222', 'buy', 'limit', 0.1, 3500));
75
76SELECT pg_temp.expect_true('place: limit sell above the market rests in the book, crypto reserved',
77 (SELECT status FROM orders WHERE id = pg_temp.oid('alice_ask')) = 'open'
78 AND (SELECT reserved_quantity FROM holdings WHERE user_id = pg_temp.uid('alice')) = 0.3,
79 'alice ETH reserved = ' || (SELECT reserved_quantity FROM holdings WHERE user_id = pg_temp.uid('alice')));
80SELECT pg_temp.expect_true('place: limit buy below the market rests in the book, cash reserved',
81 (SELECT (available_balance, reserved_balance) FROM users WHERE username = 'bob') = (4650.0000, 350.0000),
82 (SELECT format('bob available %s reserved %s', available_balance, reserved_balance) FROM users WHERE username = 'bob'));
83SELECT pg_temp.expect_true('event: placement recorded automatically',
84 (SELECT count(*) FROM order_events WHERE order_id IN (pg_temp.oid('alice_ask'), pg_temp.oid('bob_bid'))
85 AND event_type = 'placed') = 2);
86SELECT pg_temp.expect_true('view: order book shows both price levels',
87 (SELECT string_agg(side || ' ' || quantity || ' @ ' || price, ', ' ORDER BY side)
88 FROM v_order_book WHERE symbol = 'ETH') = 'buy 0.1000 @ 3500.000000, sell 0.3000 @ 3600.000000',
89 (SELECT string_agg(side || ' ' || quantity || ' @ ' || price, ', ' ORDER BY side) FROM v_order_book WHERE symbol = 'ETH'));
90SELECT pg_temp.expect_true('consistency holds after placing', pg_temp.consistent());
91
92CALL pg_temp.expect_error('place: buy without enough free cash',
93 $q$SELECT place_order(pg_temp.uid('charlie'), 'a1111111-1111-1111-1111-111111111111', 'buy', 'limit', 1, 60000)$q$,
94 'insufficient funds');
95CALL pg_temp.expect_error('place: sell more than is free (0.2 of 0.5 is already reserved)',
96 $q$SELECT place_order(pg_temp.uid('alice'), 'a2222222-2222-2222-2222-222222222222', 'sell', 'limit', 0.3, 3600)$q$,
97 'insufficient holding');
98
99-- ===========================================================================
100-- B. A trade between two compatible orders, partial fill
101-- ===========================================================================
102-- bob bids 0.2 at 3650: crosses alice's ask at 3600 -> trade 0.2 @ 3600
103INSERT INTO ids VALUES ('bob_bid2',
104 place_order(pg_temp.uid('bob'), 'a2222222-2222-2222-2222-222222222222', 'buy', 'limit', 0.2, 3650));
105
106SELECT pg_temp.expect_true('match: trade between the two orders at the resting price',
107 EXISTS (SELECT 1 FROM market_trades
108 WHERE buy_order_id = pg_temp.oid('bob_bid2') AND sell_order_id = pg_temp.oid('alice_ask')
109 AND quantity = 0.2 AND price = 3600 AND source = 'match'));
110SELECT pg_temp.expect_true('status: seller partially filled, buyer executed (automatic)',
111 (SELECT status || ' ' || filled_quantity FROM orders WHERE id = pg_temp.oid('alice_ask')) = 'partially_filled 0.2000'
112 AND (SELECT status FROM orders WHERE id = pg_temp.oid('bob_bid2')) = 'executed',
113 (SELECT format('alice_ask %s %s/%s', status, filled_quantity, quantity) FROM orders WHERE id = pg_temp.oid('alice_ask')));
114SELECT pg_temp.expect_true('money: buyer paid 720, got back the 10 reserved above the trade price',
115 (SELECT (available_balance, reserved_balance) FROM users WHERE username = 'bob') = (3930.0000, 350.0000),
116 (SELECT format('bob available %s reserved %s', available_balance, reserved_balance) FROM users WHERE username = 'bob'));
117SELECT pg_temp.expect_true('crypto: 0.2 ETH moved from alice (0.1 still reserved) to bob',
118 (SELECT (quantity, reserved_quantity) FROM holdings WHERE user_id = pg_temp.uid('alice')) = (0.3000, 0.1000)
119 AND (SELECT (quantity, avg_price) FROM holdings WHERE user_id = pg_temp.uid('bob')) = (0.2000, 3600.000000));
120SELECT pg_temp.expect_true('ledger: one buy and one sell row, linked to the orders',
121 (SELECT count(*) FROM transactions WHERE related_order IN (pg_temp.oid('bob_bid2'), pg_temp.oid('alice_ask'))) = 2);
122SELECT pg_temp.expect_true('event: fills recorded automatically',
123 (SELECT string_agg(event_type, ',' ORDER BY id) FROM order_events WHERE order_id = pg_temp.oid('alice_ask'))
124 = 'placed,partially_filled');
125SELECT pg_temp.expect_true('consistency holds after the trade', pg_temp.consistent());
126
127-- ===========================================================================
128-- C. Market order: book first, then the simulated market
129-- ===========================================================================
130-- market price is now 3600 (last trade); charlie buys 0.15 at market:
131-- 0.1 from alice's remaining ask @ 3600, the other 0.05 from the market @ 3600
132INSERT INTO ids VALUES ('charlie_mkt',
133 place_order(pg_temp.uid('charlie'), 'a2222222-2222-2222-2222-222222222222', 'buy', 'market', 0.15));
134
135SELECT pg_temp.expect_true('market order: filled completely in two trades',
136 (SELECT (status, trades, avg_fill_price) FROM v_order_history WHERE order_id = pg_temp.oid('charlie_mkt'))
137 = ('executed'::varchar, 2::bigint, 3600.000000::numeric),
138 (SELECT format('%s, %s trades, avg %s', status, trades, avg_fill_price) FROM v_order_history WHERE order_id = pg_temp.oid('charlie_mkt')));
139SELECT pg_temp.expect_true('market order: alice''s ask is now executed, nothing left reserved',
140 (SELECT status FROM orders WHERE id = pg_temp.oid('alice_ask')) = 'executed'
141 AND (SELECT reserved_quantity FROM holdings WHERE user_id = pg_temp.uid('alice')) = 0
142 AND (SELECT reserved_balance FROM users WHERE username = 'charlie') = 0);
143SELECT pg_temp.expect_true('consistency holds after the market order', pg_temp.consistent());
144
145-- ===========================================================================
146-- D. Trades are only possible between valid, compatible orders
147-- ===========================================================================
148INSERT INTO ids VALUES ('charlie_ask',
149 place_order(pg_temp.uid('charlie'), 'a2222222-2222-2222-2222-222222222222', 'sell', 'limit', 0.1, 3700));
150INSERT INTO ids VALUES ('bob_ask',
151 place_order(pg_temp.uid('bob'), 'a2222222-2222-2222-2222-222222222222', 'sell', 'limit', 0.1, 3800));
152
153CALL pg_temp.expect_error('trade: price above the buyer''s limit',
154 $q$INSERT INTO market_trades (market_id, executed_at, price, quantity, buy_order_id, sell_order_id)
155 VALUES ('a2222222-2222-2222-2222-222222222222', now(), 3700, 0.1, pg_temp.oid('bob_bid'), pg_temp.oid('charlie_ask'))$q$,
156 'outside the limit');
157CALL pg_temp.expect_error('trade: more than the order has remaining',
158 $q$INSERT INTO market_trades (market_id, executed_at, price, quantity, buy_order_id)
159 VALUES ('a2222222-2222-2222-2222-222222222222', now(), 3500, 0.5, pg_temp.oid('bob_bid'))$q$,
160 'exceeds the remaining quantity');
161CALL pg_temp.expect_error('trade: a sell order used as the buy side',
162 $q$INSERT INTO market_trades (market_id, executed_at, price, quantity, buy_order_id)
163 VALUES ('a2222222-2222-2222-2222-222222222222', now(), 3700, 0.1, pg_temp.oid('charlie_ask'))$q$,
164 'cannot be the buy side');
165CALL pg_temp.expect_error('trade: order of another market',
166 $q$INSERT INTO market_trades (market_id, executed_at, price, quantity, buy_order_id)
167 VALUES ('a1111111-1111-1111-1111-111111111111', now(), 3500, 0.1, pg_temp.oid('bob_bid'))$q$,
168 'different market');
169CALL pg_temp.expect_error('trade: executed order cannot trade again',
170 $q$INSERT INTO market_trades (market_id, executed_at, price, quantity, sell_order_id)
171 VALUES ('a2222222-2222-2222-2222-222222222222', now(), 3600, 0.1, pg_temp.oid('alice_ask'))$q$,
172 'is executed and cannot trade');
173CALL pg_temp.expect_error('trade: a user with their own order',
174 $q$INSERT INTO market_trades (market_id, executed_at, price, quantity, buy_order_id, sell_order_id)
175 VALUES ('a2222222-2222-2222-2222-222222222222', now(), 3500, 0.1, pg_temp.oid('bob_bid'), pg_temp.oid('bob_ask'))$q$,
176 'own order');
177CALL pg_temp.expect_error('trade: a trade that filled orders cannot be deleted',
178 $q$DELETE FROM market_trades WHERE buy_order_id = pg_temp.oid('bob_bid2')$q$,
179 'cannot be changed or deleted');
180
181-- ===========================================================================
182-- E. Order state changes
183-- ===========================================================================
184CALL pg_temp.expect_error('state: status cannot be set to executed by hand',
185 $q$UPDATE orders SET status = 'executed' WHERE id = pg_temp.oid('bob_bid')$q$,
186 'status is derived automatically');
187CALL pg_temp.expect_error('state: filled quantity cannot be changed by hand',
188 $q$UPDATE orders SET filled_quantity = 0.05 WHERE id = pg_temp.oid('bob_bid')$q$,
189 'can only change through a trade');
190CALL pg_temp.expect_error('state: executed order cannot be processed again',
191 $q$UPDATE orders SET status = 'cancelled' WHERE id = pg_temp.oid('alice_ask')$q$,
192 'already executed');
193CALL pg_temp.expect_error('state: ordered quantity cannot change',
194 $q$UPDATE orders SET quantity = 1 WHERE id = pg_temp.oid('bob_bid')$q$,
195 'cannot change');
196CALL pg_temp.expect_error('state: cancelling by hand without releasing the reservation',
197 $q$UPDATE orders SET status = 'cancelled' WHERE id = pg_temp.oid('bob_bid')$q$,
198 'reserved balance');
199
200-- ===========================================================================
201-- F. Cancelling releases the reservation, exactly once
202-- ===========================================================================
203CALL pg_temp.expect_error('cancel: someone else''s order',
204 $q$SELECT cancel_order(pg_temp.oid('bob_bid'), pg_temp.uid('charlie'))$q$,
205 'does not belong');
206SELECT cancel_order(pg_temp.oid('bob_bid'), pg_temp.uid('bob'));
207SELECT pg_temp.expect_true('cancel: 350 back from reserved to available, event recorded',
208 (SELECT (available_balance, reserved_balance) FROM users WHERE username = 'bob') = (4280.0000, 0.0000)
209 AND EXISTS (SELECT 1 FROM order_events WHERE order_id = pg_temp.oid('bob_bid') AND event_type = 'cancelled'),
210 (SELECT format('bob available %s reserved %s', available_balance, reserved_balance) FROM users WHERE username = 'bob'));
211CALL pg_temp.expect_error('cancel: a cancelled order cannot be cancelled again',
212 $q$SELECT cancel_order(pg_temp.oid('bob_bid'), pg_temp.uid('bob'))$q$,
213 'cannot be cancelled');
214CALL pg_temp.expect_error('trade: cancelled order cannot trade',
215 $q$INSERT INTO market_trades (market_id, executed_at, price, quantity, buy_order_id)
216 VALUES ('a2222222-2222-2222-2222-222222222222', now(), 3500, 0.1, pg_temp.oid('bob_bid'))$q$,
217 'is cancelled and cannot trade');
218
219-- ===========================================================================
220-- G. Balances cannot be put in an inconsistent state
221-- ===========================================================================
222CALL pg_temp.expect_error('balance: reserving cash with no order behind it',
223 $q$UPDATE users SET available_balance = available_balance - 100, reserved_balance = reserved_balance + 100
224 WHERE username = 'charlie'$q$,
225 'reserved balance');
226CALL pg_temp.expect_error('balance: reserving crypto with no order behind it',
227 $q$UPDATE holdings SET reserved_quantity = reserved_quantity + 0.01 WHERE user_id = pg_temp.uid('alice')$q$,
228 'reserved quantity');
229CALL pg_temp.expect_error('balance: cash changed without a ledger row',
230 $q$UPDATE users SET available_balance = available_balance + 100 WHERE username = 'charlie'$q$,
231 'does not match the ledger');
232
233-- ===========================================================================
234-- H. Background job: resting limit orders filled when the market reaches them
235-- ===========================================================================
236INSERT INTO ids VALUES ('bob_bid3',
237 place_order(pg_temp.uid('bob'), 'a2222222-2222-2222-2222-222222222222', 'buy', 'limit', 0.1, 3500));
238-- the simulator moves the price down to 3450 (a bot tick, no orders)
239INSERT INTO market_trades (market_id, executed_at, price, quantity, side)
240VALUES ('a2222222-2222-2222-2222-222222222222', now(), 3450, 0.01, 'sell');
241
242CREATE TEMP TABLE job_run ON COMMIT DROP AS SELECT fill_marketable_orders() AS filled;
243
244SELECT pg_temp.expect_true('job: fills exactly the orders the new price reached',
245 (SELECT filled FROM job_run) = 1
246 AND (SELECT (status, avg_fill_price) FROM v_order_history WHERE order_id = pg_temp.oid('bob_bid3'))
247 = ('executed'::varchar, 3450.000000::numeric)
248 AND (SELECT status FROM orders WHERE id = pg_temp.oid('charlie_ask')) = 'open',
249 (SELECT format('bob_bid3 %s @ %s, charlie_ask (3700) still %s', h.status, h.avg_fill_price, o.status)
250 FROM v_order_history h, orders o WHERE h.order_id = pg_temp.oid('bob_bid3') AND o.id = pg_temp.oid('charlie_ask')));
251SELECT pg_temp.expect_true('job: buyer paid 345, got the 5 above the fill price back',
252 (SELECT reserved_balance FROM users WHERE username = 'bob') = 0
253 AND (SELECT count(*) FROM transactions WHERE related_order = pg_temp.oid('bob_bid3') AND amount = -345) = 1);
254SELECT pg_temp.expect_true('job: nothing more to do on a second run', fill_marketable_orders() = 0);
255SELECT pg_temp.expect_true('consistency holds after the job', pg_temp.consistent());
256
257-- ===========================================================================
258-- Final state of every trader
259-- ===========================================================================
260SELECT pg_temp.expect_true('views: every trader''s cash equals their ledger',
261 NOT EXISTS (SELECT 1 FROM v_trader_balances WHERE total_cash <> ledger_total));
262
263SELECT username, available_balance, reserved_balance, ledger_total, holdings_value
264 FROM v_trader_balances ORDER BY username;
265
266SELECT count(*) FILTER (WHERE passed) AS passed,
267 count(*) FILTER (WHERE NOT passed) AS failed
268 FROM test_results;
269
270ROLLBACK;
Note: See TracBrowser for help on using the repository browser.