source: docs/P1-ConceptualModel/wiki/ERModel.md@ ef1c1c7

main
Last change on this file since ef1c1c7 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: 17.7 KB
RevLine 
[ef1c1c7]1= Entity-Relationship Model v.04 =
2
3== Diagram ==
4
5[[Image(ERModel_v04.png, 800px)]]
6
7Notation: Chen. Rectangles are entity sets, diamonds are relationships, ellipses
8are attributes, underlined ellipses are primary keys, the dashed ellipse is a
9derived attribute. A double line between an entity set and a relationship marks
10'''total participation''' (every instance of that entity set must participate); a
11single line marks partial participation.
12
13Two deliberate modeling decisions worth stating up front:
14
15 * '''No foreign keys appear in the diagram.''' Connections between entity sets are expressed as relationships, per the notation. Foreign-key columns appear only in the relational model in [wiki:RelationalDesign].
16 * '''`Holds` and `Contains` are relationships, not entity sets.''' Both are M:N and both carry their own attributes, which is exactly what a Chen relationship is for. They become tables (`holdings`, `watchlist_items`) only in P2.
17
18== Data requirements ==
19
20Each entity set is given as a short rationale for why it exists as its own set,
21its keys, and its attributes as a table. Each relationship is given as its
22cardinality and participation, a short rationale, and — where it carries data —
23an attribute table.
24
25=== Entity sets ===
26
27==== Users ====
28Registered participants of the platform. Every action in the simulation is
29attributed to a user, and the two balance attributes are what makes the
30simulation work: cash that is free to trade is tracked separately from cash
31that is currently committed to open positions, so the platform can refuse a
32purchase without having to recompute the whole portfolio first.
33
34'''Keys:''' candidates `{id}`, `{username}`, `{email}`; primary key '''`id`'''. A
35surrogate UUID was chosen because it is opaque and stable — `username` and
36`email` are both things a user may legitimately want to change later, and
37every relationship in the diagram points at `Users`, so a mutable key would
38propagate changes across the whole database.
39
40||= Attribute =||= Type =||= Constraints =||
41|| `id` || UUID || PK, required ||
42|| `username` || text(50) || required, unique ||
43|| `email` || text(255) || required, unique, contains `@` ||
44|| `full_name` || text(200) || optional ||
45|| `password_hash` || text(255) || required — never the password itself; the prototype stores a SHA-256 hex digest ||
46|| `available_balance` || numeric(18,4) || required, default 0, ≥ 0 ||
47|| `invested_balance` || numeric(18,4) || required, default 0, ≥ 0 ||
48|| `reserved_balance` || numeric(18,4) || required, default 0, ≥ 0 — cash set aside for the user's open buy orders (added in v04, after P7) ||
49|| `created_at` || timestamptz || required, defaults to now ||
50|| `updated_at` || timestamptz || optional (null until first change) ||
51
52==== Cryptos ====
53The catalog of crypto assets the platform knows about. Kept separate from
54`Markets` because an asset exists independently of the pairs it is traded in —
55the same asset can be quoted against several currencies, and a user's holding is
56in the ''asset'', not in a particular pair.
57
58'''Keys:''' candidates `{id}`, `{symbol}`; primary key '''`id`''', for the same
59reason as in `Users`. `symbol` is kept as a unique natural key because that is
60what users type and see.
61
62||= Attribute =||= Type =||= Constraints =||
63|| `id` || UUID || PK, required ||
64|| `symbol` || text(20) || required, unique (e.g. `BTC`) ||
65|| `name` || text(255) || required (e.g. `Bitcoin`) ||
66|| `created_at` || timestamptz || required, defaults to now ||
67
68==== Markets ====
69A tradeable pair: one crypto asset quoted in one currency, e.g. BTC/USD. This is
70where prices live, and it is the thing an order is placed ''on''. Modeled as its
71own entity set rather than an attribute of `Cryptos` because a market has its own
72lifecycle — it can be deactivated without deleting the asset — and because
73trades, candles and orders all reference the pair, not the asset.
74
75'''Keys:''' candidates `{id}`, `{crypto_id, quote_currency}` — that pair is
76unique by definition, since a given asset can only be quoted once per
77currency; primary key '''`id`''', so that the many entity sets referencing a
78market carry one narrow column instead of a composite key.
79
80||= Attribute =||= Type =||= Constraints =||
81|| `id` || UUID || PK, required ||
82|| `quote_currency` || text(3) || required, default `USD` ||
83|| `is_active` || boolean || required, default true — inactive markets are hidden from the trading menus but keep their history ||
84|| `created_at` || timestamptz || required, defaults to now ||
85
86==== Orders ====
87A user's instruction to buy or sell on a market. Needed as a separate entity set
88because an order is a record of ''intent'' that outlives its execution: it keeps
89the requested quantity and price even after it has been filled, which is what
90makes the ledger auditable.
91
92Placing an order is what triggers a '''reservation''' of whatever it commits:
93the crypto being sold (`Holds.reserved_quantity`, below) on a sell, and the
94cash (`Users.reserved_balance`) on a buy. Since v04 (after P7) an order can
95wait in the order book and be filled in parts, so `status` is a real
96lifecycle driven by `filled_quantity`: `open` (nothing filled yet),
97`partially_filled`, `executed` (completely filled), or `cancelled`, which
98releases what is still reserved. See
99[wiki:UseCase0005] for the reserve-then-settle
100sequence and
101[wiki:AdvancedDatabaseDevelopment]
102for the rules that keep it consistent.
103
104'''Keys:''' candidate `{id}` only — there is no natural key, since the same user
105can place two identical orders on the same market in the same second, and both
106are legitimately distinct; primary key '''`id`'''.
107
108||= Attribute =||= Type =||= Constraints =||
109|| `id` || UUID || PK, required ||
110|| `side` || text || required, `buy` or `sell` ||
111|| `type` || text || required, `market` or `limit` (both executed since P7) ||
112|| `status` || text || required, `open`, `partially_filled`, `executed` or `cancelled` ||
113|| `quantity` || numeric(20,4) || required, > 0 ||
114|| `filled_quantity` || numeric(20,4) || required, default 0, between 0 and `quantity` — how much has been traded; remaining = `quantity − filled_quantity` (added in v04, after P7) ||
115|| `price` || numeric(18,6) || the limit price; for a market order, the market price when it was placed ||
116|| `placed_at` || timestamptz || required, defaults to now ||
117|| `executed_at` || timestamptz || optional, set when the order settles ||
118
119==== Transactions ====
120The financial ledger: every movement of virtual cash, in one place. This exists
121so that a balance is never just a number someone edited — it is the sum of an
122auditable list of entries, which is also what the "explain every step" goal of
123the project needs.
124
125'''Keys:''' candidate `{id}` only; primary key '''`id`'''.
126
127||= Attribute =||= Type =||= Constraints =||
128|| `id` || UUID || PK, required ||
129|| `type` || text || required, `deposit`, `buy`, `sell` or `fee` ||
130|| `amount` || numeric(18,4) || required, signed — negative for money leaving the cash balance, positive for money arriving ||
131|| `currency` || text(3) || required, default `USD` ||
132|| `created_at` || timestamptz || required, defaults to now ||
133|| `description` || text || optional, free-form ||
134
135==== !MarketTrades ====
136Individual executed trades on a market, from the user's own fills and from the
137market simulator. This is the single source of truth for the current price: the
138price of a market is the price of its most recent trade, never a column someone
139writes directly.
140
141'''Keys:''' candidate `{id}` — `{market_id, executed_at}` looks unique in
142principle, but two trades can share a timestamp, so it is not a safe key;
143primary key '''`id`''' (a plain auto-incrementing integer here rather than a
144UUID, because this is the highest-volume entity set and it is only ever read
145in timestamp order, never referenced by anything else).
146
147||= Attribute =||= Type =||= Constraints =||
148|| `id` || integer || PK, required, auto-generated ||
149|| `executed_at` || timestamptz || required ||
150|| `price` || numeric(18,6) || required, > 0 ||
151|| `quantity` || numeric(20,6) || required, > 0 ||
152|| `side` || text || optional, `buy` or `sell` ||
153|| `source` || text(50) || required, default `simulation` — distinguishes a simulated trade from a user's own fill (`user`) ||
154
155Since v04 (after P7) a trade also records which orders it filled, through the
156relationships `FillsBuy` and `FillsSell` below.
157
158==== !OrderEvents ====
159''Added in v04, after P7.'' The audit trail of an order: one event for its
160placement, one for every (partial) fill, and one for a cancellation. The
161`Orders` row only holds the current state; this entity keeps the history of
162how the order got there. Events are recorded automatically by the database.
163
164'''Keys:''' candidate `{id}` only; primary key '''`id`''' (auto-incrementing
165integer, events are only read in order).
166
167||= Attribute =||= Type =||= Constraints =||
168|| `id` || integer || PK, required, auto-generated ||
169|| `event_type` || text || required, `placed`, `partially_filled`, `filled` or `cancelled` ||
170|| `quantity` || numeric(20,4) || required — the ordered quantity for `placed`, the filled amount for a fill, the unfilled rest for `cancelled` ||
171|| `price` || numeric(18,6) || optional — the order price, or the trade price for a fill ||
172|| `status_after` || text || required, the order's status after the event ||
173|| `created_at` || timestamptz || required, defaults to now ||
174
175==== !MarketCandles ====
176OHLCV aggregates per market and timeframe — the data a price chart is drawn
177from. Stored rather than computed on the fly because the point of the project is
178a chart-driven interface, and re-aggregating the whole trade history for every
179screen refresh does not scale.
180
181'''Keys:''' candidates `{id}`, `{market_id, timeframe, candle_time}` — a market
182has exactly one candle per timeframe per time bucket; primary key '''`id`''', the
183composite is enforced as a uniqueness rule because it is the real-world
184constraint and it is what prevents duplicate candles.
185
186||= Attribute =||= Type =||= Constraints =||
187|| `id` || integer || PK, required, auto-generated ||
188|| `timeframe` || text || required, `1m`, `5m`, `1h` or `1d` ||
189|| `open`, `high`, `low`, `close` || numeric(18,6) || all required ||
190|| `volume` || numeric(20,6) || required ||
191|| `candle_time` || timestamptz || required — the start of the bucket ||
192
193==== Watchlists ====
194A named list of assets a user wants to monitor. A separate entity set rather than
195a flag on the relationship between users and assets, because a user may want
196several lists ("long term", "watching today") and each needs its own name.
197
198'''Keys:''' candidate `{id}` — `{user_id, name}` would also work if list names
199were required to be unique per user, which the model does not impose, so it is
200not listed as a candidate key; primary key '''`id`'''.
201
202||= Attribute =||= Type =||= Constraints =||
203|| `id` || UUID || PK, required ||
204|| `name` || text(100) || required ||
205|| `created_at` || timestamptz || required, defaults to now ||
206
207=== Relationships ===
208
209==== !QuotedOn — Cryptos (1) : Markets (N), total on Markets ====
210Ties a market to the asset it trades. One asset can be quoted in many markets;
211every market must have exactly one asset, hence total participation on the
212`Markets` side. No attributes.
213
214==== !PlacedOn — Markets (1) : Orders (N), total on Orders ====
215Records which market an order was placed on. Every order must name a market;
216a market may have no orders yet. No attributes.
217
218==== Places — Users (1) : Orders (N), total on Orders ====
219Records who placed an order. Every order belongs to exactly one user; a new
220user has no orders. No attributes.
221
222==== Records — Users (1) : Transactions (N), total on Transactions ====
223Attributes each ledger entry to a user. Every entry belongs to exactly one
224user. No attributes.
225
226==== Settles — Orders (1) : Transactions (N), partial on both sides ====
227Links a ledger entry to the order that caused it. Partial on the
228`Transactions` side because deposits have no originating order, and partial on
229the `Orders` side because an order that never executes never produces a
230ledger entry — which is why the corresponding column is nullable in P2. No
231attributes.
232
233==== Fills — Markets (1) : !MarketTrades (N), total on !MarketTrades ====
234Every executed trade happened on exactly one market. No attributes.
235
236==== !FillsBuy — Orders (1) : !MarketTrades (N), partial on both sides ====
237''Added in v04, after P7.'' The buy order a trade filled. An order can be
238filled by many trades (partial fills); a trade fills at most one buy order,
239and none when the simulated market was the buyer. No attributes.
240
241==== !FillsSell — Orders (1) : !MarketTrades (N), partial on both sides ====
242''Added in v04, after P7.'' The sell order a trade filled, symmetric to
243`FillsBuy`. A trade between two users' orders participates in both. No
244attributes.
245
246==== Logs — Orders (1) : !OrderEvents (N), total on !OrderEvents ====
247''Added in v04, after P7.'' Every event belongs to exactly one order. No
248attributes.
249
250==== Aggregates — Markets (1) : !MarketCandles (N), total on !MarketCandles ====
251Every candle summarises trades of exactly one market. No attributes.
252
253==== Owns — Users (1) : Watchlists (N), total on Watchlists ====
254Every watchlist belongs to exactly one user. No attributes.
255
256==== Holds — Users (M) : Cryptos (N), partial on both sides, '''with attributes''' ====
257A user's position in an asset. M:N because one user holds many assets and one
258asset is held by many users, and partial on both sides because a user may hold
259nothing and an asset may be held by nobody. Modeled as a relationship rather
260than an entity set because a position has no identity of its own — it is
261entirely described by ''which user'', ''which asset'', and how much.
262
263`reserved_quantity` mirrors `available_balance`/`invested_balance` on `Users`:
264two independently updated stored numbers, with the amount actually free to use
265computed on demand rather than stored (`quantity − reserved_quantity` here,
266`available_balance` alone on the cash side). Without it, nothing stopped a
267user from placing a second sell order against crypto already promised to a
268first one — `quantity` alone cannot tell "owned" apart from "owned, but
269already committed elsewhere." See history, v03.
270
271||= Attribute =||= Type =||= Constraints =||
272|| `quantity` || numeric(20,4) || required, ≥ 0 — total amount owned ||
273|| `reserved_quantity` || numeric(20,4) || required, default 0, `0 ≤ reserved_quantity ≤ quantity` — committed to the user's own open sell orders, not yet removed from the position ||
274|| `avg_price` || numeric(18,6) || required, ≥ 0, '''derived''' (dashed ellipse) — the weighted average of the prices at which the position was accumulated; derivable from the buy history, stored anyway so unrealised P/L can be shown without replaying the whole ledger ||
275|| `created_at` || timestamptz || required, defaults to now ||
276|| `updated_at` || timestamptz || optional ||
277
278==== Contains — Watchlists (M) : Cryptos (N), partial on both sides, '''with attribute''' ====
279Which assets are on which watchlist. M:N: a list holds many assets, an asset
280appears on many lists. Partial on both sides — an empty list is valid and an
281asset need not be on any list.
282
283||= Attribute =||= Type =||= Constraints =||
284|| `added_at` || timestamptz || required, defaults to now — recorded so a list can be shown in the order the user built it ||
285
286== Entity-Relationship Model History ==
287
288 * '''v01''' — First complete version. Built from the entity notes in `ep-diagram.md` (the initial hand-written model), with three changes made to that initial model while drawing it:
289 1. `Markets` was promoted from an implied attribute of the asset to its own entity set, so that prices, orders, trades and candles can all reference a pair rather than an asset.
290 2. `holdings` and `watchlist_items` were re-expressed as the M:N relationships `Holds` and `Contains` with their own attributes, instead of entity sets with foreign keys — the initial notes listed them as tables, which is a relational concept that does not belong in a Chen ERD.
291 3. `avg_price` was marked as a derived attribute rather than a plain one, to make the denormalisation explicit rather than hidden.
292 * '''v02''' — Student review pass over the AI-generated v01 in the TerraER GUI.
293 * '''v03''' — Added `reserved_quantity` to `Holds`, and reworded `Orders.status` to state its reserve → settle → (cancel) lifecycle explicitly, instead of leaving `open`/`cancelled` as unused enum values. Triggered by a design review that pointed out the model had no way to stop a user from placing a second sell order against crypto already promised to a first, unsettled one — `quantity` alone cannot distinguish "owned" from "owned, but already committed." Also redrawn more compactly: every entity and relationship (with its own attributes moved along with it) was pulled proportionally toward the diagram's centroid, shrinking the canvas by roughly 45% with the same topology and no new overlaps. See [wiki:ERModelAIUsage] for the reasoning and how the diagram file itself was produced, and [wiki:RelationalDesign] and [wiki:UseCase0005] for how the new attribute is enforced.
294 * '''v04 — after P7.''' Phase 7 (order, balance and trade consistency) needed data the model did not have, so the model was extended to stay in line with the database:
295 * `Users.reserved_balance`: cash reserved by open buy orders;
296 * `Orders.filled_quantity` and the status value `partially_filled`: orders can now be filled in parts;
297 * the relationships `FillsBuy` and `FillsSell` between `Orders` and `MarketTrades`: which orders a trade filled;
298 * the entity set `OrderEvents` with the relationship `Logs`: the automatically recorded history of every order.
299
300 Nothing existing was removed or changed. See
301 [wiki:AdvancedDatabaseDevelopment].
302 The diagram files are `ERModel_v04.xml` / `ERModel_v04.png`; earlier versions
303 are kept.
304
305Reasoning for the AI-assisted part of this phase, and the full interaction log,
306are on [wiki:ERModelAIUsage].
307
Note: See TracBrowser for help on using the repository browser.