| [1549dae] | 1 | = Entity-Relationship Model v.05 =
|
|---|
| [ef1c1c7] | 2 |
|
|---|
| 3 | == Diagram ==
|
|---|
| 4 |
|
|---|
| [1549dae] | 5 | [[Image(ERModel_v05.png, 800px)]]
|
|---|
| [ef1c1c7] | 6 |
|
|---|
| 7 | Notation: Chen. Rectangles are entity sets, diamonds are relationships, ellipses
|
|---|
| 8 | are attributes, underlined ellipses are primary keys, the dashed ellipse is a
|
|---|
| 9 | derived attribute. A double line between an entity set and a relationship marks
|
|---|
| 10 | '''total participation''' (every instance of that entity set must participate); a
|
|---|
| 11 | single line marks partial participation.
|
|---|
| 12 |
|
|---|
| [1549dae] | 13 | Three deliberate modeling decisions worth stating up front:
|
|---|
| [ef1c1c7] | 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].
|
|---|
| [1549dae] | 16 | * '''A position and a watchlist entry are entity sets, not M:N relationships.''' `Holdings` (a user's position in an asset) and `WatchlistItems` (an asset on a watchlist) each have their own identifier `id`, and each is connected by two 1:N relationships: `Holds` and `PositionIn` for a holding, `Contains` and `Lists` for a watchlist item. Until v04 they were drawn as the M:N relationships `Holds` and `Contains`, but the database has always given `holdings` and `watchlist_items` their own `id` primary key. That is how an entity set is implemented, not an M:N relationship, whose key would be the pair of participating keys. v05 corrects the model to match; see history.
|
|---|
| 17 | * '''Key and uniqueness rules are stated with entity and relationship names, never with foreign-key columns.''' For example: "a crypto is quoted at most once per currency", not "`{crypto_id, quote_currency}` is unique".
|
|---|
| [ef1c1c7] | 18 |
|
|---|
| 19 | == Data requirements ==
|
|---|
| 20 |
|
|---|
| 21 | Each entity set is given as a short rationale for why it exists as its own set,
|
|---|
| 22 | its keys, and its attributes as a table. Each relationship is given as its
|
|---|
| 23 | cardinality and participation, a short rationale, and — where it carries data —
|
|---|
| 24 | an attribute table.
|
|---|
| 25 |
|
|---|
| 26 | === Entity sets ===
|
|---|
| 27 |
|
|---|
| 28 | ==== Users ====
|
|---|
| 29 | Registered participants of the platform. Every action in the simulation is
|
|---|
| 30 | attributed to a user, and the two balance attributes are what makes the
|
|---|
| 31 | simulation work: cash that is free to trade is tracked separately from cash
|
|---|
| 32 | that is currently committed to open positions, so the platform can refuse a
|
|---|
| 33 | purchase without having to recompute the whole portfolio first.
|
|---|
| 34 |
|
|---|
| 35 | '''Keys:''' candidates `{id}`, `{username}`, `{email}`; primary key '''`id`'''. A
|
|---|
| 36 | surrogate UUID was chosen because it is opaque and stable — `username` and
|
|---|
| 37 | `email` are both things a user may legitimately want to change later, and
|
|---|
| 38 | every relationship in the diagram points at `Users`, so a mutable key would
|
|---|
| 39 | propagate changes across the whole database.
|
|---|
| 40 |
|
|---|
| 41 | ||= Attribute =||= Type =||= Constraints =||
|
|---|
| 42 | || `id` || UUID || PK, required ||
|
|---|
| 43 | || `username` || text(50) || required, unique ||
|
|---|
| [1549dae] | 44 | || `email` || text(255) || required, unique, contains `@` (checked by the application at registration, not by a database constraint) ||
|
|---|
| [ef1c1c7] | 45 | || `full_name` || text(200) || optional ||
|
|---|
| 46 | || `password_hash` || text(255) || required — never the password itself; the prototype stores a SHA-256 hex digest ||
|
|---|
| 47 | || `available_balance` || numeric(18,4) || required, default 0, ≥ 0 ||
|
|---|
| 48 | || `invested_balance` || numeric(18,4) || required, default 0, ≥ 0 ||
|
|---|
| 49 | || `reserved_balance` || numeric(18,4) || required, default 0, ≥ 0 — cash set aside for the user's open buy orders (added in v04, after P7) ||
|
|---|
| 50 | || `created_at` || timestamptz || required, defaults to now ||
|
|---|
| 51 | || `updated_at` || timestamptz || optional (null until first change) ||
|
|---|
| 52 |
|
|---|
| 53 | ==== Cryptos ====
|
|---|
| 54 | The catalog of crypto assets the platform knows about. Kept separate from
|
|---|
| 55 | `Markets` because an asset exists independently of the pairs it is traded in —
|
|---|
| 56 | the same asset can be quoted against several currencies, and a user's holding is
|
|---|
| 57 | in the ''asset'', not in a particular pair.
|
|---|
| 58 |
|
|---|
| 59 | '''Keys:''' candidates `{id}`, `{symbol}`; primary key '''`id`''', for the same
|
|---|
| 60 | reason as in `Users`. `symbol` is kept as a unique natural key because that is
|
|---|
| 61 | what users type and see.
|
|---|
| 62 |
|
|---|
| 63 | ||= Attribute =||= Type =||= Constraints =||
|
|---|
| 64 | || `id` || UUID || PK, required ||
|
|---|
| 65 | || `symbol` || text(20) || required, unique (e.g. `BTC`) ||
|
|---|
| 66 | || `name` || text(255) || required (e.g. `Bitcoin`) ||
|
|---|
| 67 | || `created_at` || timestamptz || required, defaults to now ||
|
|---|
| 68 |
|
|---|
| 69 | ==== Markets ====
|
|---|
| 70 | A tradeable pair: one crypto asset quoted in one currency, e.g. BTC/USD. This is
|
|---|
| 71 | where prices live, and it is the thing an order is placed ''on''. Modeled as its
|
|---|
| 72 | own entity set rather than an attribute of `Cryptos` because a market has its own
|
|---|
| 73 | lifecycle — it can be deactivated without deleting the asset — and because
|
|---|
| 74 | trades, candles and orders all reference the pair, not the asset.
|
|---|
| 75 |
|
|---|
| [1549dae] | 76 | '''Keys:''' candidate `{id}`; primary key '''`id`''', so that the many entity sets
|
|---|
| 77 | related to a market need one narrow identifier instead of a composite one.
|
|---|
| 78 | '''Uniqueness rule:''' a crypto is quoted at most once per currency, so the crypto
|
|---|
| 79 | a market is `QuotedOn` together with its `quote_currency` identifies the market
|
|---|
| 80 | as well. Chen notation cannot draw this, because half of it comes through a
|
|---|
| 81 | relationship. P2 enforces it as `UNIQUE(crypto_id, quote_currency)`.
|
|---|
| [ef1c1c7] | 82 |
|
|---|
| 83 | ||= Attribute =||= Type =||= Constraints =||
|
|---|
| 84 | || `id` || UUID || PK, required ||
|
|---|
| 85 | || `quote_currency` || text(3) || required, default `USD` ||
|
|---|
| 86 | || `is_active` || boolean || required, default true — inactive markets are hidden from the trading menus but keep their history ||
|
|---|
| 87 | || `created_at` || timestamptz || required, defaults to now ||
|
|---|
| 88 |
|
|---|
| 89 | ==== Orders ====
|
|---|
| 90 | A user's instruction to buy or sell on a market. Needed as a separate entity set
|
|---|
| 91 | because an order is a record of ''intent'' that outlives its execution: it keeps
|
|---|
| 92 | the requested quantity and price even after it has been filled, which is what
|
|---|
| 93 | makes the ledger auditable.
|
|---|
| 94 |
|
|---|
| 95 | Placing an order is what triggers a '''reservation''' of whatever it commits:
|
|---|
| [1549dae] | 96 | the crypto being sold (`Holdings.reserved_quantity`, below) on a sell, and the
|
|---|
| [ef1c1c7] | 97 | cash (`Users.reserved_balance`) on a buy. Since v04 (after P7) an order can
|
|---|
| 98 | wait in the order book and be filled in parts, so `status` is a real
|
|---|
| 99 | lifecycle driven by `filled_quantity`: `open` (nothing filled yet),
|
|---|
| 100 | `partially_filled`, `executed` (completely filled), or `cancelled`, which
|
|---|
| 101 | releases what is still reserved. See
|
|---|
| 102 | [wiki:UseCase0005] for the reserve-then-settle
|
|---|
| 103 | sequence and
|
|---|
| 104 | [wiki:AdvancedDatabaseDevelopment]
|
|---|
| 105 | for the rules that keep it consistent.
|
|---|
| 106 |
|
|---|
| 107 | '''Keys:''' candidate `{id}` only — there is no natural key, since the same user
|
|---|
| 108 | can place two identical orders on the same market in the same second, and both
|
|---|
| 109 | are legitimately distinct; primary key '''`id`'''.
|
|---|
| 110 |
|
|---|
| 111 | ||= Attribute =||= Type =||= Constraints =||
|
|---|
| 112 | || `id` || UUID || PK, required ||
|
|---|
| 113 | || `side` || text || required, `buy` or `sell` ||
|
|---|
| 114 | || `type` || text || required, `market` or `limit` (both executed since P7) ||
|
|---|
| 115 | || `status` || text || required, `open`, `partially_filled`, `executed` or `cancelled` ||
|
|---|
| 116 | || `quantity` || numeric(20,4) || required, > 0 ||
|
|---|
| 117 | || `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) ||
|
|---|
| [1549dae] | 118 | || `price` || numeric(18,6) || optional — the limit price; for a market order, the market price when it was placed ||
|
|---|
| [ef1c1c7] | 119 | || `placed_at` || timestamptz || required, defaults to now ||
|
|---|
| 120 | || `executed_at` || timestamptz || optional, set when the order settles ||
|
|---|
| 121 |
|
|---|
| 122 | ==== Transactions ====
|
|---|
| 123 | The financial ledger: every movement of virtual cash, in one place. This exists
|
|---|
| 124 | so that a balance is never just a number someone edited — it is the sum of an
|
|---|
| 125 | auditable list of entries, which is also what the "explain every step" goal of
|
|---|
| 126 | the project needs.
|
|---|
| 127 |
|
|---|
| 128 | '''Keys:''' candidate `{id}` only; primary key '''`id`'''.
|
|---|
| 129 |
|
|---|
| 130 | ||= Attribute =||= Type =||= Constraints =||
|
|---|
| 131 | || `id` || UUID || PK, required ||
|
|---|
| 132 | || `type` || text || required, `deposit`, `buy`, `sell` or `fee` ||
|
|---|
| 133 | || `amount` || numeric(18,4) || required, signed — negative for money leaving the cash balance, positive for money arriving ||
|
|---|
| 134 | || `currency` || text(3) || required, default `USD` ||
|
|---|
| 135 | || `created_at` || timestamptz || required, defaults to now ||
|
|---|
| 136 | || `description` || text || optional, free-form ||
|
|---|
| 137 |
|
|---|
| 138 | ==== !MarketTrades ====
|
|---|
| 139 | Individual executed trades on a market, from the user's own fills and from the
|
|---|
| 140 | market simulator. This is the single source of truth for the current price: the
|
|---|
| 141 | price of a market is the price of its most recent trade, never a column someone
|
|---|
| 142 | writes directly.
|
|---|
| 143 |
|
|---|
| [1549dae] | 144 | '''Keys:''' candidate `{id}` — "market plus `executed_at`" looks unique in
|
|---|
| 145 | principle, but two trades on a market can share a timestamp, so it is not a safe key;
|
|---|
| [ef1c1c7] | 146 | primary key '''`id`''' (a plain auto-incrementing integer here rather than a
|
|---|
| 147 | UUID, because this is the highest-volume entity set and it is only ever read
|
|---|
| 148 | in timestamp order, never referenced by anything else).
|
|---|
| 149 |
|
|---|
| 150 | ||= Attribute =||= Type =||= Constraints =||
|
|---|
| [1549dae] | 151 | || `id` || big integer || PK, required, auto-generated (`bigserial` in P2) ||
|
|---|
| [ef1c1c7] | 152 | || `executed_at` || timestamptz || required ||
|
|---|
| 153 | || `price` || numeric(18,6) || required, > 0 ||
|
|---|
| 154 | || `quantity` || numeric(20,6) || required, > 0 ||
|
|---|
| 155 | || `side` || text || optional, `buy` or `sell` ||
|
|---|
| 156 | || `source` || text(50) || required, default `simulation` — distinguishes a simulated trade from a user's own fill (`user`) ||
|
|---|
| 157 |
|
|---|
| 158 | Since v04 (after P7) a trade also records which orders it filled, through the
|
|---|
| 159 | relationships `FillsBuy` and `FillsSell` below.
|
|---|
| 160 |
|
|---|
| 161 | ==== !OrderEvents ====
|
|---|
| 162 | ''Added in v04, after P7.'' The audit trail of an order: one event for its
|
|---|
| 163 | placement, one for every (partial) fill, and one for a cancellation. The
|
|---|
| 164 | `Orders` row only holds the current state; this entity keeps the history of
|
|---|
| 165 | how the order got there. Events are recorded automatically by the database.
|
|---|
| 166 |
|
|---|
| 167 | '''Keys:''' candidate `{id}` only; primary key '''`id`''' (auto-incrementing
|
|---|
| 168 | integer, events are only read in order).
|
|---|
| 169 |
|
|---|
| 170 | ||= Attribute =||= Type =||= Constraints =||
|
|---|
| [1549dae] | 171 | || `id` || big integer || PK, required, auto-generated (`bigserial` in P2) ||
|
|---|
| [ef1c1c7] | 172 | || `event_type` || text || required, `placed`, `partially_filled`, `filled` or `cancelled` ||
|
|---|
| 173 | || `quantity` || numeric(20,4) || required — the ordered quantity for `placed`, the filled amount for a fill, the unfilled rest for `cancelled` ||
|
|---|
| 174 | || `price` || numeric(18,6) || optional — the order price, or the trade price for a fill ||
|
|---|
| 175 | || `status_after` || text || required, the order's status after the event ||
|
|---|
| [1549dae] | 176 | || `created_at` || timestamptz || required, set automatically when the event is recorded (`clock_timestamp()`, so events inside one transaction keep their real order) ||
|
|---|
| [ef1c1c7] | 177 |
|
|---|
| 178 | ==== !MarketCandles ====
|
|---|
| 179 | OHLCV aggregates per market and timeframe — the data a price chart is drawn
|
|---|
| 180 | from. Stored rather than computed on the fly because the point of the project is
|
|---|
| 181 | a chart-driven interface, and re-aggregating the whole trade history for every
|
|---|
| 182 | screen refresh does not scale.
|
|---|
| 183 |
|
|---|
| [1549dae] | 184 | '''Keys:''' candidate `{id}`; primary key '''`id`'''. '''Uniqueness rule:''' a market
|
|---|
| 185 | has exactly one candle per timeframe per time bucket, so the market a candle
|
|---|
| 186 | `Aggregates` together with `timeframe` and `candle_time` also identifies it.
|
|---|
| 187 | This is the real-world constraint that prevents duplicate candles. P2 enforces
|
|---|
| 188 | it as `UNIQUE(market_id, timeframe, candle_time)`.
|
|---|
| [ef1c1c7] | 189 |
|
|---|
| 190 | ||= Attribute =||= Type =||= Constraints =||
|
|---|
| [1549dae] | 191 | || `id` || big integer || PK, required, auto-generated (`bigserial` in P2) ||
|
|---|
| [ef1c1c7] | 192 | || `timeframe` || text || required, `1m`, `5m`, `1h` or `1d` ||
|
|---|
| 193 | || `open`, `high`, `low`, `close` || numeric(18,6) || all required ||
|
|---|
| 194 | || `volume` || numeric(20,6) || required ||
|
|---|
| 195 | || `candle_time` || timestamptz || required — the start of the bucket ||
|
|---|
| 196 |
|
|---|
| 197 | ==== Watchlists ====
|
|---|
| 198 | A named list of assets a user wants to monitor. A separate entity set rather than
|
|---|
| 199 | a flag on the relationship between users and assets, because a user may want
|
|---|
| 200 | several lists ("long term", "watching today") and each needs its own name.
|
|---|
| 201 |
|
|---|
| [1549dae] | 202 | '''Keys:''' candidate `{id}`; primary key '''`id`'''. "Owner plus `name`" would
|
|---|
| 203 | also identify a list if names had to be unique per user, but the model does
|
|---|
| 204 | not require that, so there is no uniqueness rule here.
|
|---|
| [ef1c1c7] | 205 |
|
|---|
| 206 | ||= Attribute =||= Type =||= Constraints =||
|
|---|
| 207 | || `id` || UUID || PK, required ||
|
|---|
| 208 | || `name` || text(100) || required ||
|
|---|
| 209 | || `created_at` || timestamptz || required, defaults to now ||
|
|---|
| 210 |
|
|---|
| [1549dae] | 211 | ==== Holdings ====
|
|---|
| 212 | A user's position in one crypto asset: how much of it the user owns, how much
|
|---|
| 213 | of that is already promised to open sell orders, and at what average price it
|
|---|
| 214 | was accumulated. ''An entity set since v05'' (until v04 it was the M:N
|
|---|
| 215 | relationship `Holds`). A holding has its own identifier and its own
|
|---|
| 216 | lifecycle: it is created on the first buy, updated on every later fill, and
|
|---|
| 217 | the prototype reads and locks it as a unit (`SELECT … FOR UPDATE` on the sell
|
|---|
| 218 | path). It is linked to its owner through `Holds` and to its asset through
|
|---|
| 219 | `PositionIn`.
|
|---|
| 220 |
|
|---|
| 221 | '''Keys:''' candidate `{id}`; primary key '''`id`'''. '''Uniqueness rule:''' a user
|
|---|
| 222 | has at most one holding per crypto, so the user who `Holds` it together with
|
|---|
| 223 | the crypto it is a `PositionIn` also identifies a holding. P2 enforces this as
|
|---|
| 224 | `UNIQUE(user_id, crypto_id)`.
|
|---|
| 225 |
|
|---|
| 226 | `reserved_quantity` mirrors `available_balance`/`invested_balance` on `Users`:
|
|---|
| 227 | two independently updated stored numbers, with the amount actually free to use
|
|---|
| 228 | computed on demand rather than stored (`quantity − reserved_quantity` here,
|
|---|
| 229 | `available_balance` alone on the cash side). Without it, nothing stopped a
|
|---|
| 230 | user from placing a second sell order against crypto already promised to a
|
|---|
| 231 | first one — `quantity` alone cannot tell "owned" apart from "owned, but
|
|---|
| 232 | already committed elsewhere." See history, v03.
|
|---|
| 233 |
|
|---|
| 234 | ||= Attribute =||= Type =||= Constraints =||
|
|---|
| 235 | || `id` || UUID || PK, required ||
|
|---|
| 236 | || `quantity` || numeric(20,4) || required, ≥ 0 — total amount owned ||
|
|---|
| 237 | || `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 ||
|
|---|
| 238 | || `avg_price` || numeric(18,6) || required, default 0, ≥ 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 ||
|
|---|
| 239 | || `created_at` || timestamptz || required, defaults to now ||
|
|---|
| 240 | || `updated_at` || timestamptz || optional ||
|
|---|
| 241 |
|
|---|
| 242 | ==== !WatchlistItems ====
|
|---|
| 243 | One asset placed on one watchlist. ''An entity set since v05'' (until v04 it was
|
|---|
| 244 | the M:N relationship `Contains`). It has its own identifier, and it is linked
|
|---|
| 245 | to its list through `Contains` and to its asset through `Lists`.
|
|---|
| 246 |
|
|---|
| 247 | '''Keys:''' candidate `{id}`; primary key '''`id`'''. '''Uniqueness rule:''' an asset
|
|---|
| 248 | appears at most once on a given list, so the watchlist that `Contains` an item
|
|---|
| 249 | together with the crypto it `Lists` also identifies the item. P2 enforces this
|
|---|
| 250 | as `UNIQUE(watchlist_id, crypto_id)`.
|
|---|
| 251 |
|
|---|
| 252 | ||= Attribute =||= Type =||= Constraints =||
|
|---|
| 253 | || `id` || UUID || PK, required ||
|
|---|
| 254 | || `added_at` || timestamptz || required, defaults to now — recorded so a list can be shown in the order the user built it ||
|
|---|
| 255 |
|
|---|
| [ef1c1c7] | 256 | === Relationships ===
|
|---|
| 257 |
|
|---|
| 258 | ==== !QuotedOn — Cryptos (1) : Markets (N), total on Markets ====
|
|---|
| 259 | Ties a market to the asset it trades. One asset can be quoted in many markets;
|
|---|
| 260 | every market must have exactly one asset, hence total participation on the
|
|---|
| 261 | `Markets` side. No attributes.
|
|---|
| 262 |
|
|---|
| 263 | ==== !PlacedOn — Markets (1) : Orders (N), total on Orders ====
|
|---|
| 264 | Records which market an order was placed on. Every order must name a market;
|
|---|
| 265 | a market may have no orders yet. No attributes.
|
|---|
| 266 |
|
|---|
| 267 | ==== Places — Users (1) : Orders (N), total on Orders ====
|
|---|
| 268 | Records who placed an order. Every order belongs to exactly one user; a new
|
|---|
| 269 | user has no orders. No attributes.
|
|---|
| 270 |
|
|---|
| 271 | ==== Records — Users (1) : Transactions (N), total on Transactions ====
|
|---|
| 272 | Attributes each ledger entry to a user. Every entry belongs to exactly one
|
|---|
| 273 | user. No attributes.
|
|---|
| 274 |
|
|---|
| 275 | ==== Settles — Orders (1) : Transactions (N), partial on both sides ====
|
|---|
| 276 | Links a ledger entry to the order that caused it. Partial on the
|
|---|
| 277 | `Transactions` side because deposits have no originating order, and partial on
|
|---|
| 278 | the `Orders` side because an order that never executes never produces a
|
|---|
| 279 | ledger entry — which is why the corresponding column is nullable in P2. No
|
|---|
| 280 | attributes.
|
|---|
| 281 |
|
|---|
| 282 | ==== Fills — Markets (1) : !MarketTrades (N), total on !MarketTrades ====
|
|---|
| 283 | Every executed trade happened on exactly one market. No attributes.
|
|---|
| 284 |
|
|---|
| 285 | ==== !FillsBuy — Orders (1) : !MarketTrades (N), partial on both sides ====
|
|---|
| 286 | ''Added in v04, after P7.'' The buy order a trade filled. An order can be
|
|---|
| 287 | filled by many trades (partial fills); a trade fills at most one buy order,
|
|---|
| [1549dae] | 288 | and none when the simulated market was the buyer. The role of `Orders` in this
|
|---|
| 289 | relationship is ''the buy order'' of the trade. No attributes.
|
|---|
| [ef1c1c7] | 290 |
|
|---|
| 291 | ==== !FillsSell — Orders (1) : !MarketTrades (N), partial on both sides ====
|
|---|
| 292 | ''Added in v04, after P7.'' The sell order a trade filled, symmetric to
|
|---|
| [1549dae] | 293 | `FillsBuy`; the role of `Orders` here is ''the sell order'' of the trade. A
|
|---|
| 294 | trade between two users' orders participates in both. No attributes.
|
|---|
| [ef1c1c7] | 295 |
|
|---|
| 296 | ==== Logs — Orders (1) : !OrderEvents (N), total on !OrderEvents ====
|
|---|
| 297 | ''Added in v04, after P7.'' Every event belongs to exactly one order. No
|
|---|
| 298 | attributes.
|
|---|
| 299 |
|
|---|
| 300 | ==== Aggregates — Markets (1) : !MarketCandles (N), total on !MarketCandles ====
|
|---|
| 301 | Every candle summarises trades of exactly one market. No attributes.
|
|---|
| 302 |
|
|---|
| 303 | ==== Owns — Users (1) : Watchlists (N), total on Watchlists ====
|
|---|
| 304 | Every watchlist belongs to exactly one user. No attributes.
|
|---|
| 305 |
|
|---|
| [1549dae] | 306 | ==== Holds — Users (1) : Holdings (N), total on Holdings ====
|
|---|
| 307 | ''1:N since v05.'' Every holding belongs to exactly one user. A user may hold
|
|---|
| 308 | nothing yet, so participation is partial on the `Users` side. No attributes.
|
|---|
| [ef1c1c7] | 309 |
|
|---|
| [1549dae] | 310 | ==== !PositionIn — Cryptos (1) : Holdings (N), total on Holdings ====
|
|---|
| 311 | ''Added in v05.'' Every holding is a position in exactly one crypto asset. An
|
|---|
| 312 | asset may be held by nobody. No attributes.
|
|---|
| [ef1c1c7] | 313 |
|
|---|
| [1549dae] | 314 | Together, `Holds` and `PositionIn` still say what the old M:N `Holds` said:
|
|---|
| 315 | a user can hold many assets and an asset can be held by many users. The
|
|---|
| 316 | difference is that the position is now a thing with its own identity, not
|
|---|
| 317 | just a pair. The rule "at most one holding per user and crypto" is stated
|
|---|
| 318 | under Holdings.
|
|---|
| [ef1c1c7] | 319 |
|
|---|
| [1549dae] | 320 | ==== Contains — Watchlists (1) : !WatchlistItems (N), total on !WatchlistItems ====
|
|---|
| 321 | ''1:N since v05.'' Every watchlist item is on exactly one list. An empty list is
|
|---|
| 322 | valid, so participation is partial on the `Watchlists` side. No attributes.
|
|---|
| [ef1c1c7] | 323 |
|
|---|
| [1549dae] | 324 | ==== Lists — Cryptos (1) : !WatchlistItems (N), total on !WatchlistItems ====
|
|---|
| 325 | ''Added in v05.'' Every watchlist item names exactly one crypto asset. An asset
|
|---|
| 326 | need not be on any list. No attributes.
|
|---|
| [ef1c1c7] | 327 |
|
|---|
| 328 | == Entity-Relationship Model History ==
|
|---|
| 329 |
|
|---|
| 330 | * '''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:
|
|---|
| 331 | 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.
|
|---|
| 332 | 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.
|
|---|
| 333 | 3. `avg_price` was marked as a derived attribute rather than a plain one, to make the denormalisation explicit rather than hidden.
|
|---|
| 334 | * '''v02''' — Student review pass over the AI-generated v01 in the TerraER GUI.
|
|---|
| 335 | * '''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.
|
|---|
| 336 | * '''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:
|
|---|
| 337 | * `Users.reserved_balance`: cash reserved by open buy orders;
|
|---|
| 338 | * `Orders.filled_quantity` and the status value `partially_filled`: orders can now be filled in parts;
|
|---|
| 339 | * the relationships `FillsBuy` and `FillsSell` between `Orders` and `MarketTrades`: which orders a trade filled;
|
|---|
| 340 | * the entity set `OrderEvents` with the relationship `Logs`: the automatically recorded history of every order.
|
|---|
| 341 |
|
|---|
| 342 | Nothing existing was removed or changed. See
|
|---|
| 343 | [wiki:AdvancedDatabaseDevelopment].
|
|---|
| [1549dae] | 344 | The diagram files are `ERModel_v04.xml` / `ERModel_v04.png`.
|
|---|
| 345 | * '''v05 — correction after review.''' The review of P2 found that two parts of the model were implemented differently in the database:
|
|---|
| 346 | * `Contains` was an M:N relationship in the model, but `watchlist_items` has its own `id` primary key;
|
|---|
| 347 | * `Holds` was an M:N relationship in the model, but `holdings` has its own `id` primary key.
|
|---|
| 348 |
|
|---|
| 349 | An M:N relationship has no identifier of its own; its table's key is the pair
|
|---|
| 350 | of participating keys. A table with its own `id` is the implementation of an
|
|---|
| 351 | entity set. Every phase after P2 (the prototype, the reports and the P7
|
|---|
| 352 | logic) already uses the database as it is. So the '''model''' was corrected to
|
|---|
| 353 | match P2, not the other way round:
|
|---|
| 354 | * `Holds` (M:N, with attributes) became the entity set `Holdings` (its former attributes plus `id`) with two 1:N relationships, `Holds` (Users → Holdings) and `PositionIn` (Cryptos → Holdings), both total on the `Holdings` side;
|
|---|
| 355 | * `Contains` (M:N, with `added_at`) became the entity set `WatchlistItems` (`id`, `added_at`) with `Contains` (Watchlists → !WatchlistItems) and `Lists` (Cryptos → !WatchlistItems), both total on the `WatchlistItems` side;
|
|---|
| 356 | * the former keys of the two relationships are kept as uniqueness rules ("one holding per user and crypto", "an asset at most once per list");
|
|---|
| 357 | * the key descriptions of `Markets`, `MarketTrades`, `MarketCandles` and `Watchlists` no longer name foreign-key columns (`crypto_id`, `market_id`, `user_id`), which do not exist in an ER model;
|
|---|
| 358 | * the diagram was redrawn on a grid with no overlapping attributes. In v04, `Watchlists.id` was hidden behind `added_at`, and several attributes of `Orders`, `Transactions`, `MarketTrades` and `MarketCandles` overlapped. The grid also makes it easier to compare the diagram with the P2 relational diagram.
|
|---|
| 359 |
|
|---|
| 360 | The diagram files are `ERModel_v05.xml` / `ERModel_v05.png`; earlier versions
|
|---|
| [ef1c1c7] | 361 | are kept.
|
|---|
| 362 |
|
|---|
| 363 | Reasoning for the AI-assisted part of this phase, and the full interaction log,
|
|---|
| 364 | are on [wiki:ERModelAIUsage].
|
|---|
| 365 |
|
|---|