| | 1 | == Diagram |
| | 2 | |
| | 3 |  |
| | 4 | |
| | 5 | == Data requirements |
| | 6 | |
| | 7 | === Entity sets |
| | 8 | |
| | 9 | ==== Users |
| | 10 | Registered participants of the platform. Every action in the simulation is |
| | 11 | attributed to a user, and the two balance attributes are what makes the |
| | 12 | simulation work: cash that is free to trade is tracked separately from cash that |
| | 13 | is currently committed to open positions, so the platform can refuse a purchase |
| | 14 | without having to recompute the whole portfolio first. |
| | 15 | |
| | 16 | - **Candidate keys:** `{id}`, `{username}`, `{email}`. Primary key: **`id`**. |
| | 17 | A surrogate UUID was chosen because it is opaque and stable — `username` and |
| | 18 | `email` are both things a user may legitimately want to change later, and |
| | 19 | every relationship in the diagram points at `Users`, so a mutable key would |
| | 20 | propagate changes across the whole database. |
| | 21 | - **Attributes:** |
| | 22 | - `id` — UUID, required, primary key. |
| | 23 | - `username` — text, max 50, required, unique. |
| | 24 | - `email` — text, max 255, required, unique, must contain `@`. |
| | 25 | - `full_name` — text, max 200, optional. |
| | 26 | - `password_hash` — text, max 255, required. Never the password itself; the |
| | 27 | prototype stores a SHA-256 hex digest. |
| | 28 | - `available_balance` — numeric(18,4), required, default 0, must be ≥ 0. |
| | 29 | - `invested_balance` — numeric(18,4), required, default 0, must be ≥ 0. |
| | 30 | - `created_at` — timestamp with time zone, required, defaults to now. |
| | 31 | - `updated_at` — timestamp with time zone, optional (null until first change). |
| | 32 | |
| | 33 | ==== Cryptos |
| | 34 | The catalog of crypto assets the platform knows about. Kept separate from |
| | 35 | `Markets` because an asset exists independently of the pairs it is traded in — |
| | 36 | the same asset can be quoted against several currencies, and a user's holding is |
| | 37 | in the *asset*, not in a particular pair. |
| | 38 | |
| | 39 | - **Candidate keys:** `{id}`, `{symbol}`. Primary key: **`id`**, for the same |
| | 40 | reason as in `Users`; `symbol` is kept as a unique natural key because that is |
| | 41 | what users type and see. |
| | 42 | - **Attributes:** |
| | 43 | - `id` — UUID, required, primary key. |
| | 44 | - `symbol` — text, max 20, required, unique (e.g. `BTC`). |
| | 45 | - `name` — text, max 255, required (e.g. `Bitcoin`). |
| | 46 | - `created_at` — timestamptz, required, defaults to now. |
| | 47 | |
| | 48 | ==== Markets |
| | 49 | A tradeable pair: one crypto asset quoted in one currency, e.g. BTC/USD. This is |
| | 50 | where prices live, and it is the thing an order is placed *on*. Modeled as its |
| | 51 | own entity set rather than an attribute of `Cryptos` because a market has its own |
| | 52 | lifecycle — it can be deactivated without deleting the asset — and because |
| | 53 | trades, candles and orders all reference the pair, not the asset. |
| | 54 | |
| | 55 | - **Candidate keys:** `{id}`, `{crypto_id, quote_currency}` — that pair is |
| | 56 | unique by definition, since a given asset can only be quoted once per |
| | 57 | currency. Primary key: **`id`**, so that the many entity sets referencing a |
| | 58 | market carry one narrow column instead of a composite key. |
| | 59 | - **Attributes:** |
| | 60 | - `id` — UUID, required, primary key. |
| | 61 | - `quote_currency` — text, exactly 3 characters, required, default `USD`. |
| | 62 | - `is_active` — boolean, required, default true. Inactive markets are hidden |
| | 63 | from the trading menus but keep their history. |
| | 64 | - `created_at` — timestamptz, required, defaults to now. |
| | 65 | |
| | 66 | ==== Orders |
| | 67 | A user's instruction to buy or sell on a market. Needed as a separate entity set |
| | 68 | because an order is a record of *intent* that outlives its execution: it keeps |
| | 69 | the requested quantity and price even after it has been filled, which is what |
| | 70 | makes the ledger auditable. |
| | 71 | |
| | 72 | - **Candidate keys:** `{id}` only. There is no natural key — the same user can |
| | 73 | place two identical orders on the same market in the same second, and both are |
| | 74 | legitimately distinct. Primary key: **`id`**. |
| | 75 | - **Attributes:** |
| | 76 | - `id` — UUID, required, primary key. |
| | 77 | - `side` — text, required, restricted to `buy` or `sell`. |
| | 78 | - `type` — text, required, restricted to `market` or `limit`. The prototype |
| | 79 | executes only `market` orders; `limit` exists so the model does not have to |
| | 80 | change when limit orders are implemented. |
| | 81 | - `status` — text, required, restricted to `open`, `executed`, `cancelled`. |
| | 82 | - `quantity` — numeric(20,4), required, must be > 0. |
| | 83 | - `price` — numeric(18,6), optional — null for a market order until it fills, |
| | 84 | then the fill price. |
| | 85 | - `placed_at` — timestamptz, required, defaults to now. |
| | 86 | - `executed_at` — timestamptz, optional, set when the order fills. |
| | 87 | |
| | 88 | ==== Transactions |
| | 89 | The financial ledger: every movement of virtual cash, in one place. This exists |
| | 90 | so that a balance is never just a number someone edited — it is the sum of an |
| | 91 | auditable list of entries, which is also what the "explain every step" goal of |
| | 92 | the project needs. |
| | 93 | |
| | 94 | - **Candidate keys:** `{id}` only. Primary key: **`id`**. |
| | 95 | - **Attributes:** |
| | 96 | - `id` — UUID, required, primary key. |
| | 97 | - `type` — text, required, restricted to `deposit`, `buy`, `sell`, `fee`. |
| | 98 | - `amount` — numeric(18,4), required. Signed: negative for money leaving the |
| | 99 | cash balance, positive for money arriving. |
| | 100 | - `currency` — text, exactly 3 characters, required, default `USD`. |
| | 101 | - `created_at` — timestamptz, required, defaults to now. |
| | 102 | - `description` — text, optional, free-form human-readable explanation. |
| | 103 | |
| | 104 | ==== MarketTrades |
| | 105 | Individual executed trades on a market, from the user's own fills and from the |
| | 106 | market simulator. This is the single source of truth for the current price: the |
| | 107 | price of a market is the price of its most recent trade, never a column someone |
| | 108 | writes directly. |
| | 109 | |
| | 110 | - **Candidate keys:** `{id}`. In principle `{market_id, executed_at}` looks |
| | 111 | unique, but two trades can share a timestamp, so it is not a safe key. |
| | 112 | Primary key: **`id`** (a plain auto-incrementing integer here rather than a |
| | 113 | UUID, because this is the highest-volume entity set and it is only ever read |
| | 114 | in timestamp order, never referenced by anything else). |
| | 115 | - **Attributes:** |
| | 116 | - `id` — integer, required, primary key, auto-generated. |
| | 117 | - `executed_at` — timestamptz, required. |
| | 118 | - `price` — numeric(18,6), required, must be > 0. |
| | 119 | - `quantity` — numeric(20,6), required, must be > 0. |
| | 120 | - `side` — text, optional, `buy` or `sell`. |
| | 121 | - `source` — text, max 50, required, default `simulation`. Distinguishes a |
| | 122 | simulated trade from a user's own fill (`user`). |
| | 123 | |
| | 124 | ==== MarketCandles |
| | 125 | OHLCV aggregates per market and timeframe — the data a price chart is drawn |
| | 126 | from. Stored rather than computed on the fly because the point of the project is |
| | 127 | a chart-driven interface, and re-aggregating the whole trade history for every |
| | 128 | screen refresh does not scale. |
| | 129 | |
| | 130 | - **Candidate keys:** `{id}`, and `{market_id, timeframe, candle_time}` — a |
| | 131 | market has exactly one candle per timeframe per time bucket. Primary key: |
| | 132 | **`id`**; the composite is enforced as a uniqueness rule because it is the |
| | 133 | real-world constraint and it is what prevents duplicate candles. |
| | 134 | - **Attributes:** |
| | 135 | - `id` — integer, required, primary key, auto-generated. |
| | 136 | - `timeframe` — text, required, restricted to `1m`, `5m`, `1h`, `1d`. |
| | 137 | - `open`, `high`, `low`, `close` — numeric(18,6), all required. |
| | 138 | - `volume` — numeric(20,6), required. |
| | 139 | - `candle_time` — timestamptz, required — the start of the bucket. |
| | 140 | |
| | 141 | ==== Watchlists |
| | 142 | A named list of assets a user wants to monitor. A separate entity set rather than |
| | 143 | a flag on the relationship between users and assets, because a user may want |
| | 144 | several lists ("long term", "watching today") and each needs its own name. |
| | 145 | |
| | 146 | - **Candidate keys:** `{id}`. `{user_id, name}` would also work if list names |
| | 147 | are required to be unique per user; the model does not impose that, so it is |
| | 148 | not listed as a candidate key. Primary key: **`id`**. |
| | 149 | - **Attributes:** |
| | 150 | - `id` — UUID, required, primary key. |
| | 151 | - `name` — text, max 100, required. |
| | 152 | - `created_at` — timestamptz, required, defaults to now. |
| | 153 | |
| | 154 | ==== Relationships |
| | 155 | |
| | 156 | ===== QuotedOn — Cryptos (1) : Markets (N), total on Markets |
| | 157 | Ties a market to the asset it trades. One asset can be quoted in many markets; |
| | 158 | every market must have exactly one asset, hence total participation on the |
| | 159 | `Markets` side. No attributes of its own. |
| | 160 | |
| | 161 | ===== PlacedOn — Markets (1) : Orders (N), total on Orders |
| | 162 | Records which market an order was placed on. Every order must name a market; |
| | 163 | a market may have no orders yet. No attributes. |
| | 164 | |
| | 165 | ===== Places — Users (1) : Orders (N), total on Orders |
| | 166 | Records who placed an order. Every order belongs to exactly one user; a new user |
| | 167 | has no orders. No attributes. |
| | 168 | |
| | 169 | ===== Records — Users (1) : Transactions (N), total on Transactions |
| | 170 | Attributes each ledger entry to a user. Every entry belongs to exactly one user. |
| | 171 | No attributes. |
| | 172 | |
| | 173 | ===== Settles — Orders (1) : Transactions (N), partial on both sides |
| | 174 | Links a ledger entry to the order that caused it. Partial on the `Transactions` |
| | 175 | side because deposits have no originating order, and partial on the `Orders` side |
| | 176 | because an order that never executes never produces a ledger entry. This is why |
| | 177 | the corresponding column is nullable in P2. No attributes. |
| | 178 | |
| | 179 | ===== Fills — Markets (1) : MarketTrades (N), total on MarketTrades |
| | 180 | Every executed trade happened on exactly one market. No attributes. |
| | 181 | |
| | 182 | ===== Aggregates — Markets (1) : MarketCandles (N), total on MarketCandles |
| | 183 | Every candle summarises trades of exactly one market. No attributes. |
| | 184 | |
| | 185 | ===== Owns — Users (1) : Watchlists (N), total on Watchlists |
| | 186 | Every watchlist belongs to exactly one user. No attributes. |
| | 187 | |
| | 188 | ===== Holds — Users (M) : Cryptos (N), partial on both sides, **with attributes** |
| | 189 | A user's position in an asset. M:N because one user holds many assets and one |
| | 190 | asset is held by many users, and partial on both sides because a user may hold |
| | 191 | nothing and an asset may be held by nobody. Modeled as a relationship rather |
| | 192 | than an entity set because a position has no identity of its own — it is |
| | 193 | entirely described by *which user*, *which asset*, and how much. |
| | 194 | |
| | 195 | - **Attributes:** |
| | 196 | - `quantity` — numeric(20,4), required, must be ≥ 0. |
| | 197 | - `avg_price` — numeric(18,6), required, ≥ 0, **derived** (dashed ellipse): |
| | 198 | the weighted average of the prices at which the position was accumulated. |
| | 199 | It is derivable from the buy history, and is stored anyway so that |
| | 200 | unrealised P/L can be shown without replaying the whole ledger. |
| | 201 | - `created_at` — timestamptz, required, defaults to now. |
| | 202 | - `updated_at` — timestamptz, optional. |
| | 203 | |
| | 204 | ===== Contains — Watchlists (M) : Cryptos (N), partial on both sides, **with attribute** |
| | 205 | Which assets are on which watchlist. M:N: a list holds many assets, an asset |
| | 206 | appears on many lists. Partial on both sides — an empty list is valid and an |
| | 207 | asset need not be on any list. |
| | 208 | |
| | 209 | - **Attributes:** |
| | 210 | - `added_at` — timestamptz, required, defaults to now. Recorded so a list can |
| | 211 | be shown in the order the user built it. |
| | 212 | |
| | 213 | === Entity-Relationship Model History |
| | 214 | |
| | 215 | - **v01** — First complete version. Built from the entity notes in |
| | 216 | [`ep-diagram.md`](ep-diagram.md) (the initial hand-written model), with three |
| | 217 | changes made to that initial model while drawing it: |
| | 218 | 1. `Markets` was promoted from an implied attribute of the asset to its own |
| | 219 | entity set, so that prices, orders, trades and candles can all reference a |
| | 220 | pair rather than an asset. |
| | 221 | 2. `holdings` and `watchlist_items` were re-expressed as the M:N relationships |
| | 222 | `Holds` and `Contains` with their own attributes, instead of entity sets |
| | 223 | with foreign keys — the initial notes listed them as tables, which is a |
| | 224 | relational concept that does not belong in a Chen ERD. |
| | 225 | 3. `avg_price` was marked as a derived attribute rather than a plain one, to |
| | 226 | make the denormalisation explicit rather than hidden. |
| | 227 | |
| | 228 | Reasoning for the AI-assisted part of this phase, and the full interaction log, |
| | 229 | are on [ERModelAIUsage](ERModelAIUsage.md). |