source: docs/P2-RelationalDesign/wiki/RelationalDesign.md@ 0cee8ec

main
Last change on this file since 0cee8ec 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: 27.9 KB
RevLine 
[ef1c1c7]1= Relational Design =
2
3== Descriptive representation of the relational schema ==
4
5Notation: '''bold''' = primary key, ''italic'' = foreign key.
6
7 * '''Users'''(__'''id'''__, username, email, full_name, password_hash, available_balance, invested_balance, created_at, updated_at)
8 * Candidate keys: `{id}`, `{username}`, `{email}`. `UNIQUE(username)`, `UNIQUE(email)`.
9 * '''Crypto'''(__'''id'''__, symbol, name, created_at)
10 * Candidate keys: `{id}`, `{symbol}`. `UNIQUE(symbol)`.
11 * '''Markets'''(__'''id'''__, ''crypto_id'', quote_currency, is_active, created_at)
12 * Candidate keys: `{id}`, `{crypto_id, quote_currency}`. `UNIQUE(crypto_id, quote_currency)`.
13 * '''Holdings'''(__'''id'''__, ''user_id'', ''crypto_id'', quantity, reserved_quantity, avg_price, created_at, updated_at)
14 * Transformation of the M:N relationship `Holds`. Candidate keys: `{id}` and
15 `{user_id, crypto_id}` — the latter is the relationship's own key and is
16 enforced with `UNIQUE(user_id, crypto_id)`. `id` was chosen as PK for
17 consistency with the other relations.
18 * `avg_price` is `NOT NULL DEFAULT 0 CHECK (avg_price >= 0)`.
19 * `reserved_quantity` is `NOT NULL DEFAULT 0 CHECK (reserved_quantity >= 0 AND reserved_quantity <= quantity)` — the amount already committed to the
20 user's own open sell orders. `quantity - reserved_quantity` (the amount
21 actually free to sell) is not a stored column; it is computed wherever
22 needed, in `v_portfolio` as `available_quantity` and in the sell path of
23 [wiki:UseCase0005]. See the `Holds` section of [wiki:ERModel]
24 for why this mirrors `available_balance`/`invested_balance` on `Users`.
25 * '''Orders'''(__'''id'''__, ''user_id'', ''market_id'', side, type, status, quantity, price, placed_at, executed_at)
26 * `side ∈ {buy, sell}`, `type ∈ {market, limit}`, `status ∈ {open, executed, cancelled}`.
27 * '''Transactions'''(__'''id'''__, ''user_id'', type, amount, currency, ''related_order'', created_at, description)
28 * `type ∈ {deposit, buy, sell, fee}`.
29 * '''!MarketTrades'''(__'''id'''__, ''market_id'', executed_at, price, quantity, side, source)
30 * '''!MarketCandles'''(__'''id'''__, ''market_id'', timeframe, open, high, low, close, volume, candle_time)
31 * `UNIQUE(market_id, timeframe, candle_time)`.
32 * '''Watchlists'''(__'''id'''__, ''user_id'', name, created_at)
33 * '''!WatchlistItems'''(__'''id'''__, ''watchlist_id'', ''crypto_id'', added_at)
34 * Transformation of the M:N relationship `Contains`. Candidate keys: `{id}`
35 and `{watchlist_id, crypto_id}`, the latter enforced with
36 `UNIQUE(watchlist_id, crypto_id)`.
37
38=== Transformation method used ===
39
40'''Partial transformation.''' Applied as follows:
41
42 * Each of the 8 entity sets in [wiki:ERModel] becomes one table, keeping
43 its UUID (or serial) primary key.
44 * Each '''1:N relationship without attributes''' is transformed by adding the
45 parent's primary key as a foreign-key column on the child table — the "N"
46 side. This is where every foreign key in the schema comes from, and it is why
47 no foreign keys appear in the ER diagram itself:
48 `QuotedOn` → `markets.crypto_id`, `PlacedOn` → `orders.market_id`,
49 `Places` → `orders.user_id`, `Records` → `transactions.user_id`,
50 `Settles` → `transactions.related_order`, `Fills` → `market_trades.market_id`,
51 `Aggregates` → `market_candles.market_id`, `Owns` → `watchlists.user_id`.
52 * Each '''M:N relationship''' becomes its own table holding the two foreign keys
53 plus the relationship's own attributes: `Holds` → `holdings`,
54 `Contains` → `watchlist_items`. The pair of foreign keys is the relationship's
55 key and is enforced as a `UNIQUE` constraint in both tables.
56 * '''Total participation''' in the ER model becomes `NOT NULL` on the
57 corresponding foreign key; partial participation stays nullable. `Settles` is
58 partial on both sides, which is exactly why `transactions.related_order` is
59 the one nullable foreign key in the schema — a deposit has no originating
60 order.
61
62=== Normalisation ===
63
64> '''Validated in P5.''' Normalization derives this
65> exact schema independently — starting only from a single de-normalized relation of every
66> model attribute and its functional dependencies, with no reference to the ER-to-relational
67> transformation below — and shows it decomposes to '''BCNF''', one normal form stronger than
68> the 3NF claimed here. The two designs agree relation for relation and key for key, so
69> nothing here changed as a result; see that page's
70> discussion section for what the one real
71> difference is (`avg_price`, a stored derived value, not a normalisation issue) and why this
72> design is still the one used from P5 onward.
73
74All relations are in '''3NF''':
75
76 * Every attribute is atomic (no repeating groups, no composite fields).
77 * No partial dependency exists because every primary key is a single UUID column.
78 * No transitive dependency exists: every non-key attribute depends directly on the row identifier. For example, `holdings.quantity` depends on `holdings.id`, not on `user_id` via some intermediate.
79 * `avg_price` in `Holdings` is a '''derived value''' cached for performance (it is
80 the weighted-average entry price across all `buy` transactions for that
81 `(user, crypto)` pair) — it is drawn as a derived attribute in the ER diagram.
82 We accept the denormalisation: it is recomputed by the database inside the same
83 transaction as each buy, in the same statement that changes the quantity
84 (`INSERT … ON CONFLICT (user_id, crypto_id) DO UPDATE`), so the stored average
85 and the stored quantity can never disagree.
86 * `avg_price` is declared `NOT NULL DEFAULT 0`. This matters: it is used in the
87 P/L arithmetic of `v_portfolio`, and in SQL any arithmetic involving `NULL`
88 yields `NULL`, so a nullable average would have silently blanked the
89 unrealised-P/L column for an existing position instead of failing loudly.
90 * `holdings.reserved_quantity`, unlike `avg_price`, is '''not''' derived — it is
91 written directly by the application (`trade.go`) as orders are placed and
92 settled, the same way `quantity` itself is. `quantity - reserved_quantity`
93 ("available") is the derived value here, and it is never stored, only
94 computed where it is needed.
95
96=== Reservation and the order lifecycle ===
97
98`holdings.reserved_quantity` exists so that placing a sell order can be
99checked against what a user actually has ''free'' to sell
100(`quantity - reserved_quantity`), not against the raw `quantity`, which also
101counts crypto already promised to another order that has not settled yet.
102`CHECK (reserved_quantity >= 0 AND reserved_quantity <= quantity)` makes an
103inconsistent reservation impossible at the database level, regardless of what
104application code does. The exact statement sequence — lock the row, check the
105available amount, reserve, then settle — is in
106[wiki:UseCase0005]; the same
107`SELECT … FOR UPDATE` locking that already protected `users.available_balance`
108on the buy path is what makes two concurrent sell orders against the same
109holding serialize correctly instead of racing.
110
111== DDL script ==
112
113The script that creates the entire schema is `../server/db/schema_creation.sql` (shown in full below). It is idempotent: it drops and recreates the `project` schema every run, so it works on an empty database and on a database that already has the schema.
114
115The script creates:
116 * 10 tables with check constraints, primary keys, foreign keys and unique constraints.
117 * 5 performance indexes.
118 * 2 views: `v_latest_prices` (latest trade price per market) and `v_portfolio` (per-user holdings valuation with unrealised P/L, plus `reserved_quantity` and the derived `available_quantity`).
119
120=== schema_creation.sql ===
121
122The two report functions at the end of the file (`report_top_traders` and `report_market_performance`) belong to Phase 6 ([wiki:AdvancedReports]) and are left out here.
123
124{{{
125-- schema_creation.sql
126-- EduBerza - crypto exchange simulation database
127-- Course: Databases 2025/2026 Winter, FINKI UKIM
128--
129-- This script is idempotent. It drops the `project` schema and all contained
130-- objects, then recreates them from scratch. Safe to run on an empty database
131-- or on a database where the schema already exists.
132
133DROP SCHEMA IF EXISTS project CASCADE;
134CREATE SCHEMA project;
135
136CREATE EXTENSION IF NOT EXISTS pgcrypto;
137
138SET search_path TO project, public;
139
140-- ============================================================================
141-- USERS
142-- Platform users. Each user has virtual (prop) balances used for simulation.
143-- ============================================================================
144CREATE TABLE project.users (
145 id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
146 username varchar(50) NOT NULL UNIQUE,
147 email varchar(255) NOT NULL UNIQUE,
148 full_name varchar(200),
149 password_hash varchar(255) NOT NULL,
150 available_balance numeric(18,4) NOT NULL DEFAULT 0 CHECK (available_balance >= 0),
151 invested_balance numeric(18,4) NOT NULL DEFAULT 0 CHECK (invested_balance >= 0),
152 created_at timestamptz NOT NULL DEFAULT now(),
153 updated_at timestamptz
154);
155
156-- ============================================================================
157-- CRYPTO
158-- Catalog of crypto assets available on the platform.
159-- ============================================================================
160CREATE TABLE project.crypto (
161 id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
162 symbol varchar(20) NOT NULL UNIQUE,
163 name varchar(255) NOT NULL,
164 created_at timestamptz NOT NULL DEFAULT now()
165);
166
167-- ============================================================================
168-- MARKETS
169-- A market is a (crypto, quote_currency) pair, e.g. BTC/USD.
170-- ============================================================================
171CREATE TABLE project.markets (
172 id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
173 crypto_id uuid NOT NULL REFERENCES project.crypto(id),
174 quote_currency char(3) NOT NULL DEFAULT 'USD',
175 is_active boolean NOT NULL DEFAULT true,
176 created_at timestamptz NOT NULL DEFAULT now(),
177 CONSTRAINT uq_markets UNIQUE (crypto_id, quote_currency)
178);
179
180-- ============================================================================
181-- HOLDINGS
182-- Per-user crypto position with running weighted average entry price.
183-- ============================================================================
184CREATE TABLE project.holdings (
185 id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
186 user_id uuid NOT NULL REFERENCES project.users(id) ON DELETE CASCADE,
187 crypto_id uuid NOT NULL REFERENCES project.crypto(id),
188 quantity numeric(20,4) NOT NULL CHECK (quantity >= 0),
189 -- Committed to the user's own open sell orders, not yet removed from the
190 -- position. quantity - reserved_quantity is what is actually free to
191 -- sell — the crypto-side equivalent of users.available_balance.
192 reserved_quantity numeric(20,4) NOT NULL DEFAULT 0
193 CHECK (reserved_quantity >= 0 AND reserved_quantity <= quantity),
194 -- Weighted-average entry price. NOT NULL so that the P/L arithmetic in
195 -- v_portfolio can never silently produce NULL for an existing position.
196 avg_price numeric(18,6) NOT NULL DEFAULT 0 CHECK (avg_price >= 0),
197 created_at timestamptz NOT NULL DEFAULT now(),
198 updated_at timestamptz,
199 CONSTRAINT uq_holdings_user_crypto UNIQUE (user_id, crypto_id)
200);
201
202-- ============================================================================
203-- ORDERS
204-- Orders placed by users on a market.
205-- ============================================================================
206CREATE TABLE project.orders (
207 id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
208 user_id uuid NOT NULL REFERENCES project.users(id) ON DELETE CASCADE,
209 market_id uuid NOT NULL REFERENCES project.markets(id),
210 side varchar(4) NOT NULL CHECK (side IN ('buy', 'sell')),
211 type varchar(20) NOT NULL CHECK (type IN ('market', 'limit')),
212 status varchar(20) NOT NULL CHECK (status IN ('open', 'executed', 'cancelled')),
213 quantity numeric(20,4) NOT NULL CHECK (quantity > 0),
214 price numeric(18,6),
215 placed_at timestamptz NOT NULL DEFAULT now(),
216 executed_at timestamptz
217);
218
219CREATE INDEX idx_orders_user ON project.orders(user_id);
220CREATE INDEX idx_orders_market ON project.orders(market_id);
221CREATE INDEX idx_orders_status ON project.orders(status);
222
223-- ============================================================================
224-- TRANSACTIONS
225-- Financial ledger: deposits, buys, sells, fees.
226-- ============================================================================
227CREATE TABLE project.transactions (
228 id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
229 user_id uuid NOT NULL REFERENCES project.users(id) ON DELETE CASCADE,
230 type varchar(50) NOT NULL CHECK (type IN ('deposit', 'buy', 'sell', 'fee')),
231 amount numeric(18,4) NOT NULL,
232 currency char(3) NOT NULL DEFAULT 'USD',
233 related_order uuid REFERENCES project.orders(id),
234 created_at timestamptz NOT NULL DEFAULT now(),
235 description text
236);
237
238CREATE INDEX idx_transactions_user ON project.transactions(user_id, created_at DESC);
239
240-- ============================================================================
241-- MARKET TRADES
242-- Raw executed trades on a market. Source of truth for current price.
243-- ============================================================================
244CREATE TABLE project.market_trades (
245 id bigserial PRIMARY KEY,
246 market_id uuid NOT NULL REFERENCES project.markets(id),
247 executed_at timestamptz NOT NULL,
248 price numeric(18,6) NOT NULL CHECK (price > 0),
249 quantity numeric(20,6) NOT NULL CHECK (quantity > 0),
250 side varchar(4) CHECK (side IN ('buy', 'sell')),
251 source varchar(50) NOT NULL DEFAULT 'simulation'
252);
253
254CREATE INDEX idx_market_trades_market_time ON project.market_trades(market_id, executed_at DESC);
255
256-- ============================================================================
257-- MARKET CANDLES
258-- OHLCV aggregates over standard timeframes.
259-- ============================================================================
260CREATE TABLE project.market_candles (
261 id bigserial PRIMARY KEY,
262 market_id uuid NOT NULL REFERENCES project.markets(id),
263 timeframe varchar(5) NOT NULL CHECK (timeframe IN ('1m', '5m', '1h', '1d')),
264 open numeric(18,6) NOT NULL,
265 high numeric(18,6) NOT NULL,
266 low numeric(18,6) NOT NULL,
267 close numeric(18,6) NOT NULL,
268 volume numeric(20,6) NOT NULL,
269 candle_time timestamptz NOT NULL,
270 CONSTRAINT uq_candle UNIQUE (market_id, timeframe, candle_time)
271);
272
273CREATE INDEX idx_market_candles_market_tf_time ON project.market_candles(market_id, timeframe, candle_time DESC);
274
275-- ============================================================================
276-- WATCHLISTS
277-- ============================================================================
278CREATE TABLE project.watchlists (
279 id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
280 user_id uuid NOT NULL REFERENCES project.users(id) ON DELETE CASCADE,
281 name varchar(100) NOT NULL,
282 created_at timestamptz NOT NULL DEFAULT now()
283);
284
285CREATE TABLE project.watchlist_items (
286 id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
287 watchlist_id uuid NOT NULL REFERENCES project.watchlists(id) ON DELETE CASCADE,
288 crypto_id uuid NOT NULL REFERENCES project.crypto(id),
289 added_at timestamptz NOT NULL DEFAULT now(),
290 CONSTRAINT uq_watchlist_crypto UNIQUE (watchlist_id, crypto_id)
291);
292
293-- ============================================================================
294-- VIEWS
295-- ============================================================================
296
297-- Latest trade price per market (current price).
298CREATE OR REPLACE VIEW project.v_latest_prices AS
299SELECT DISTINCT ON (t.market_id)
300 t.market_id,
301 c.symbol,
302 m.quote_currency,
303 t.price,
304 t.executed_at
305FROM project.market_trades t
306JOIN project.markets m ON m.id = t.market_id
307JOIN project.crypto c ON c.id = m.crypto_id
308ORDER BY t.market_id, t.executed_at DESC;
309
310-- Portfolio valuation per user (holdings x latest price).
311CREATE OR REPLACE VIEW project.v_portfolio AS
312SELECT h.user_id,
313 c.symbol,
314 h.quantity,
315 h.reserved_quantity,
316 (h.quantity - h.reserved_quantity) AS available_quantity,
317 h.avg_price,
318 lp.price AS current_price,
319 (h.quantity * lp.price) AS market_value,
320 (h.quantity * (lp.price - h.avg_price)) AS unrealized_pnl
321FROM project.holdings h
322JOIN project.crypto c ON c.id = h.crypto_id
323LEFT JOIN project.markets m ON m.crypto_id = c.id AND m.quote_currency = 'USD'
324LEFT JOIN project.v_latest_prices lp ON lp.market_id = m.id;
325}}}
326
327== DML script (sample data) ==
328
329The script that loads realistic sample data is `../server/db/data_load.sql` (shown in full below). It is idempotent: it truncates all tables with `CASCADE` then re-inserts. Loaded:
330 * 5 crypto assets (BTC, ETH, ADA, SOL, DOGE) and 5 USD-quoted markets.
331 * 3 sample users (`alice`, `bob`, `charlie`) with password `test123` (sha256 hex).
332 * 18 recent market trades across all markets so `v_latest_prices` is populated.
333 * 10 one-hour candles (BTC and ETH).
334 * One fully-executed market-buy order for Alice, the matching holding, and two ledger entries (deposit + buy), with Alice's balances updated accordingly.
335 * Two watchlists with five watchlist items.
336
337=== data_load.sql ===
338
339{{{
340-- data_load.sql
341-- EduBerza - sample data
342-- Course: Databases 2025/2026 Winter, FINKI UKIM
343--
344-- Idempotent. Truncates all tables in the `project` schema and reloads
345-- deterministic sample data. Run schema_creation.sql first if tables do
346-- not yet exist.
347--
348-- All sample users have the password: test123
349
350SET search_path TO project, public;
351
352TRUNCATE TABLE
353 project.watchlist_items,
354 project.watchlists,
355 project.market_candles,
356 project.market_trades,
357 project.transactions,
358 project.orders,
359 project.holdings,
360 project.markets,
361 project.crypto,
362 project.users
363RESTART IDENTITY CASCADE;
364
365-- ============================================================================
366-- CRYPTO
367-- ============================================================================
368INSERT INTO project.crypto (id, symbol, name) VALUES
369 ('11111111-1111-1111-1111-111111111111', 'BTC', 'Bitcoin'),
370 ('22222222-2222-2222-2222-222222222222', 'ETH', 'Ethereum'),
371 ('33333333-3333-3333-3333-333333333333', 'ADA', 'Cardano'),
372 ('44444444-4444-4444-4444-444444444444', 'SOL', 'Solana'),
373 ('55555555-5555-5555-5555-555555555555', 'DOGE', 'Dogecoin');
374
375-- ============================================================================
376-- MARKETS (all quoted in USD)
377-- ============================================================================
378INSERT INTO project.markets (id, crypto_id, quote_currency, is_active) VALUES
379 ('a1111111-1111-1111-1111-111111111111', '11111111-1111-1111-1111-111111111111', 'USD', true),
380 ('a2222222-2222-2222-2222-222222222222', '22222222-2222-2222-2222-222222222222', 'USD', true),
381 ('a3333333-3333-3333-3333-333333333333', '33333333-3333-3333-3333-333333333333', 'USD', true),
382 ('a4444444-4444-4444-4444-444444444444', '44444444-4444-4444-4444-444444444444', 'USD', true),
383 ('a5555555-5555-5555-5555-555555555555', '55555555-5555-5555-5555-555555555555', 'USD', true);
384
385-- ============================================================================
386-- USERS
387-- Password for all: test123 (stored as sha256 hex hash)
388-- ============================================================================
389INSERT INTO project.users (id, username, email, full_name, password_hash, available_balance, invested_balance) VALUES
390 ('b1111111-1111-1111-1111-111111111111', 'alice', 'alice@example.com', 'Alice Johnson',
391 encode(digest('test123', 'sha256'), 'hex'), 10000.0000, 0),
392 ('b2222222-2222-2222-2222-222222222222', 'bob', 'bob@example.com', 'Bob Smith',
393 encode(digest('test123', 'sha256'), 'hex'), 5000.0000, 0),
394 ('b3333333-3333-3333-3333-333333333333', 'charlie', 'charlie@example.com', 'Charlie Davis',
395 encode(digest('test123', 'sha256'), 'hex'), 2500.0000, 0);
396
397-- ============================================================================
398-- MARKET TRADES
399-- Recent simulated trades per market, used as price source.
400-- ============================================================================
401INSERT INTO project.market_trades (market_id, executed_at, price, quantity, side, source) VALUES
402 -- BTC/USD around $67,000
403 ('a1111111-1111-1111-1111-111111111111', now() - interval '10 min', 66850.250000, 0.120000, 'buy', 'simulation'),
404 ('a1111111-1111-1111-1111-111111111111', now() - interval '8 min', 66910.500000, 0.075000, 'sell', 'simulation'),
405 ('a1111111-1111-1111-1111-111111111111', now() - interval '5 min', 67020.750000, 0.200000, 'buy', 'simulation'),
406 ('a1111111-1111-1111-1111-111111111111', now() - interval '2 min', 67105.100000, 0.050000, 'buy', 'simulation'),
407 ('a1111111-1111-1111-1111-111111111111', now() - interval '30 second', 67140.000000, 0.030000, 'sell', 'simulation'),
408 -- ETH/USD around $3,500
409 ('a2222222-2222-2222-2222-222222222222', now() - interval '10 min', 3490.500000, 1.500000, 'buy', 'simulation'),
410 ('a2222222-2222-2222-2222-222222222222', now() - interval '6 min', 3502.750000, 0.800000, 'sell', 'simulation'),
411 ('a2222222-2222-2222-2222-222222222222', now() - interval '2 min', 3515.250000, 2.100000, 'buy', 'simulation'),
412 ('a2222222-2222-2222-2222-222222222222', now() - interval '30 second', 3520.000000, 0.650000, 'buy', 'simulation'),
413 -- ADA/USD around $0.45
414 ('a3333333-3333-3333-3333-333333333333', now() - interval '10 min', 0.446500, 500.000000, 'buy', 'simulation'),
415 ('a3333333-3333-3333-3333-333333333333', now() - interval '3 min', 0.452000, 1200.000000, 'buy', 'simulation'),
416 ('a3333333-3333-3333-3333-333333333333', now() - interval '30 second', 0.453750, 800.000000, 'sell', 'simulation'),
417 -- SOL/USD around $165
418 ('a4444444-4444-4444-4444-444444444444', now() - interval '10 min', 164.250000, 10.000000, 'buy', 'simulation'),
419 ('a4444444-4444-4444-4444-444444444444', now() - interval '4 min', 165.500000, 5.500000, 'sell', 'simulation'),
420 ('a4444444-4444-4444-4444-444444444444', now() - interval '30 second', 166.100000, 8.000000, 'buy', 'simulation'),
421 -- DOGE/USD around $0.12
422 ('a5555555-5555-5555-5555-555555555555', now() - interval '10 min', 0.118500, 10000.000000, 'buy', 'simulation'),
423 ('a5555555-5555-5555-5555-555555555555', now() - interval '3 min', 0.121250, 7500.000000, 'sell', 'simulation'),
424 ('a5555555-5555-5555-5555-555555555555', now() - interval '30 second', 0.122000, 12000.000000, 'buy', 'simulation');
425
426-- ============================================================================
427-- MARKET CANDLES (1h aggregates, last 5 hours per market)
428-- ============================================================================
429INSERT INTO project.market_candles (market_id, timeframe, open, high, low, close, volume, candle_time) VALUES
430 ('a1111111-1111-1111-1111-111111111111', '1h', 66200, 66500, 66050, 66400, 12.50, date_trunc('hour', now() - interval '5 hour')),
431 ('a1111111-1111-1111-1111-111111111111', '1h', 66400, 66800, 66380, 66700, 15.30, date_trunc('hour', now() - interval '4 hour')),
432 ('a1111111-1111-1111-1111-111111111111', '1h', 66700, 66950, 66650, 66900, 11.80, date_trunc('hour', now() - interval '3 hour')),
433 ('a1111111-1111-1111-1111-111111111111', '1h', 66900, 67100, 66800, 67050, 14.20, date_trunc('hour', now() - interval '2 hour')),
434 ('a1111111-1111-1111-1111-111111111111', '1h', 67050, 67200, 66900, 67140, 10.75, date_trunc('hour', now() - interval '1 hour')),
435 ('a2222222-2222-2222-2222-222222222222', '1h', 3460, 3490, 3450, 3485, 120.0, date_trunc('hour', now() - interval '5 hour')),
436 ('a2222222-2222-2222-2222-222222222222', '1h', 3485, 3510, 3480, 3500, 135.0, date_trunc('hour', now() - interval '4 hour')),
437 ('a2222222-2222-2222-2222-222222222222', '1h', 3500, 3520, 3495, 3515, 110.0, date_trunc('hour', now() - interval '3 hour')),
438 ('a2222222-2222-2222-2222-222222222222', '1h', 3515, 3525, 3500, 3520, 125.5, date_trunc('hour', now() - interval '2 hour')),
439 ('a2222222-2222-2222-2222-222222222222', '1h', 3520, 3530, 3510, 3520, 140.0, date_trunc('hour', now() - interval '1 hour'));
440
441-- ============================================================================
442-- EXAMPLE ORDERS, HOLDINGS AND TRANSACTIONS for alice
443-- Shows a fully-filled market buy and its resulting holding & ledger entry.
444-- ============================================================================
445INSERT INTO project.orders (id, user_id, market_id, side, type, status, quantity, price, placed_at, executed_at) VALUES
446 ('c1111111-1111-1111-1111-111111111111',
447 'b1111111-1111-1111-1111-111111111111',
448 'a2222222-2222-2222-2222-222222222222',
449 'buy', 'market', 'executed', 0.5000, 3500.000000,
450 now() - interval '1 hour', now() - interval '1 hour');
451
452INSERT INTO project.holdings (user_id, crypto_id, quantity, avg_price, updated_at) VALUES
453 ('b1111111-1111-1111-1111-111111111111',
454 '22222222-2222-2222-2222-222222222222',
455 0.5000, 3500.000000, now() - interval '1 hour');
456
457INSERT INTO project.transactions (user_id, type, amount, currency, related_order, description) VALUES
458 ('b1111111-1111-1111-1111-111111111111', 'deposit', 10000.0000, 'USD', NULL,
459 'Initial virtual deposit'),
460 ('b1111111-1111-1111-1111-111111111111', 'buy', -1750.0000, 'USD',
461 'c1111111-1111-1111-1111-111111111111',
462 'Market buy 0.5 ETH @ 3500.00');
463
464-- After the buy, alice's invested_balance reflects the used funds.
465UPDATE project.users
466 SET available_balance = 10000.0000 - 1750.0000,
467 invested_balance = 1750.0000,
468 updated_at = now()
469 WHERE id = 'b1111111-1111-1111-1111-111111111111';
470
471-- ============================================================================
472-- WATCHLISTS
473-- ============================================================================
474INSERT INTO project.watchlists (id, user_id, name) VALUES
475 ('d1111111-1111-1111-1111-111111111111', 'b1111111-1111-1111-1111-111111111111', 'Favorites'),
476 ('d2222222-2222-2222-2222-222222222222', 'b2222222-2222-2222-2222-222222222222', 'Bobs Picks');
477
478INSERT INTO project.watchlist_items (watchlist_id, crypto_id) VALUES
479 ('d1111111-1111-1111-1111-111111111111', '11111111-1111-1111-1111-111111111111'),
480 ('d1111111-1111-1111-1111-111111111111', '22222222-2222-2222-2222-222222222222'),
481 ('d1111111-1111-1111-1111-111111111111', '44444444-4444-4444-4444-444444444444'),
482 ('d2222222-2222-2222-2222-222222222222', '11111111-1111-1111-1111-111111111111'),
483 ('d2222222-2222-2222-2222-222222222222', '55555555-5555-5555-5555-555555555555');
484}}}
485
486== Relational diagram ==
487
488[[Image(relational_schema.jpg)]]
489
490Generated in '''Pgadmin''' from the '''live''' `project` schema, in crow's-foot
491notation — not drawn by hand, so it is evidence that the deployed database
492actually matches the design described above. Each box is a table with its
493columns and declared types; key icons mark primary keys and the arrowed lines
494are the 12 declared foreign keys.
495
496=== How to regenerate it ===
497
498'''With pgAdmin 4''', if DBeaver is unavailable — it reads the live schema the same
499way, so the result is equivalent in substance:
500
501 1. Connect to the project database.
502 2. Right-click the database → '''ERD For Database''' (or open a blank ERD and drag
503 the `project` tables in).
504 3. Arrange the tables to mirror `ERModel_v03.png`.
505 4. '''Download image''' → PNG, then convert:
506 `convert relational_schema.png relational_schema.jpg`
Note: See TracBrowser for help on using the repository browser.