Index: docs/P1-ConceptualModel/ERModel.md
===================================================================
--- docs/P1-ConceptualModel/ERModel.md	(revision df058387ebe7d07864d874fd1376f15512418581)
+++ docs/P1-ConceptualModel/ERModel.md	(revision 9577c7944ff24ddc9145ff6637494f07d87c3dd4)
@@ -1,8 +1,7 @@
-# Entity-Relationship Model v.02
+# Entity-Relationship Model v.03
 
 ## Diagram
 
-![ERModel_v02](ERModel_v02.png)
-
+![ERModel_v03](ERModel_v03.png)
 
 Notation: Chen. Rectangles are entity sets, diamonds are relationships, ellipses
@@ -23,4 +22,9 @@
 ## Data requirements
 
+Each entity set is given as a short rationale for why it exists as its own set,
+its keys, and its attributes as a table. Each relationship is given as its
+cardinality and participation, a short rationale, and — where it carries data —
+an attribute table.
+
 ### Entity sets
 
@@ -28,24 +32,25 @@
 Registered participants of the platform. Every action in the simulation is
 attributed to a user, and the two balance attributes are what makes the
-simulation work: cash that is free to trade is tracked separately from cash that
-is currently committed to open positions, so the platform can refuse a purchase
-without having to recompute the whole portfolio first.
-
-- **Candidate keys:** `{id}`, `{username}`, `{email}`. Primary key: **`id`**.
-  A surrogate UUID was chosen because it is opaque and stable — `username` and
-  `email` are both things a user may legitimately want to change later, and
-  every relationship in the diagram points at `Users`, so a mutable key would
-  propagate changes across the whole database.
-- **Attributes:**
-  - `id` — UUID, required, primary key.
-  - `username` — text, max 50, required, unique.
-  - `email` — text, max 255, required, unique, must contain `@`.
-  - `full_name` — text, max 200, optional.
-  - `password_hash` — text, max 255, required. Never the password itself; the
-    prototype stores a SHA-256 hex digest.
-  - `available_balance` — numeric(18,4), required, default 0, must be ≥ 0.
-  - `invested_balance` — numeric(18,4), required, default 0, must be ≥ 0.
-  - `created_at` — timestamp with time zone, required, defaults to now.
-  - `updated_at` — timestamp with time zone, optional (null until first change).
+simulation work: cash that is free to trade is tracked separately from cash
+that is currently committed to open positions, so the platform can refuse a
+purchase without having to recompute the whole portfolio first.
+
+**Keys:** candidates `{id}`, `{username}`, `{email}`; primary key **`id`**. A
+surrogate UUID was chosen because it is opaque and stable — `username` and
+`email` are both things a user may legitimately want to change later, and
+every relationship in the diagram points at `Users`, so a mutable key would
+propagate changes across the whole database.
+
+| Attribute | Type | Constraints |
+|---|---|---|
+| `id` | UUID | PK, required |
+| `username` | text(50) | required, unique |
+| `email` | text(255) | required, unique, contains `@` |
+| `full_name` | text(200) | optional |
+| `password_hash` | text(255) | required — never the password itself; the prototype stores a SHA-256 hex digest |
+| `available_balance` | numeric(18,4) | required, default 0, ≥ 0 |
+| `invested_balance` | numeric(18,4) | required, default 0, ≥ 0 |
+| `created_at` | timestamptz | required, defaults to now |
+| `updated_at` | timestamptz | optional (null until first change) |
 
 #### Cryptos
@@ -55,12 +60,14 @@
 in the *asset*, not in a particular pair.
 
-- **Candidate keys:** `{id}`, `{symbol}`. Primary key: **`id`**, for the same
-  reason as in `Users`; `symbol` is kept as a unique natural key because that is
-  what users type and see.
-- **Attributes:**
-  - `id` — UUID, required, primary key.
-  - `symbol` — text, max 20, required, unique (e.g. `BTC`).
-  - `name` — text, max 255, required (e.g. `Bitcoin`).
-  - `created_at` — timestamptz, required, defaults to now.
+**Keys:** candidates `{id}`, `{symbol}`; primary key **`id`**, for the same
+reason as in `Users`. `symbol` is kept as a unique natural key because that is
+what users type and see.
+
+| Attribute | Type | Constraints |
+|---|---|---|
+| `id` | UUID | PK, required |
+| `symbol` | text(20) | required, unique (e.g. `BTC`) |
+| `name` | text(255) | required (e.g. `Bitcoin`) |
+| `created_at` | timestamptz | required, defaults to now |
 
 #### Markets
@@ -71,14 +78,15 @@
 trades, candles and orders all reference the pair, not the asset.
 
-- **Candidate keys:** `{id}`, `{crypto_id, quote_currency}` — that pair is
-  unique by definition, since a given asset can only be quoted once per
-  currency. Primary key: **`id`**, so that the many entity sets referencing a
-  market carry one narrow column instead of a composite key.
-- **Attributes:**
-  - `id` — UUID, required, primary key.
-  - `quote_currency` — text, exactly 3 characters, required, default `USD`.
-  - `is_active` — boolean, required, default true. Inactive markets are hidden
-    from the trading menus but keep their history.
-  - `created_at` — timestamptz, required, defaults to now.
+**Keys:** candidates `{id}`, `{crypto_id, quote_currency}` — that pair is
+unique by definition, since a given asset can only be quoted once per
+currency; primary key **`id`**, so that the many entity sets referencing a
+market carry one narrow column instead of a composite key.
+
+| Attribute | Type | Constraints |
+|---|---|---|
+| `id` | UUID | PK, required |
+| `quote_currency` | text(3) | required, default `USD` |
+| `is_active` | boolean | required, default true — inactive markets are hidden from the trading menus but keep their history |
+| `created_at` | timestamptz | required, defaults to now |
 
 #### Orders
@@ -88,19 +96,29 @@
 makes the ledger auditable.
 
-- **Candidate keys:** `{id}` only. There is no natural key — the same user can
-  place two identical orders on the same market in the same second, and both are
-  legitimately distinct. Primary key: **`id`**.
-- **Attributes:**
-  - `id` — UUID, required, primary key.
-  - `side` — text, required, restricted to `buy` or `sell`.
-  - `type` — text, required, restricted to `market` or `limit`. The prototype
-    executes only `market` orders; `limit` exists so the model does not have to
-    change when limit orders are implemented.
-  - `status` — text, required, restricted to `open`, `executed`, `cancelled`.
-  - `quantity` — numeric(20,4), required, must be > 0.
-  - `price` — numeric(18,6), optional — null for a market order until it fills,
-    then the fill price.
-  - `placed_at` — timestamptz, required, defaults to now.
-  - `executed_at` — timestamptz, optional, set when the order fills.
+Placing an order is what triggers a **reservation** of whatever it commits —
+the crypto being sold (`Holds.reserved_quantity`, below) on a sell, cash
+already handled the same way on a buy via `available_balance` /
+`invested_balance`. `status` therefore has real meaning as a lifecycle, not
+just a label: `open` means reserved but not yet settled, `executed` means
+settled, `cancelled` would release the reservation without settling (not yet
+exercised by any use case, since only market orders — which settle
+immediately — are implemented). See
+[UseCase0005](../P3-UseCaseModel/UseCase0005.md) for the reserve-then-settle
+sequence.
+
+**Keys:** candidate `{id}` only — there is no natural key, since the same user
+can place two identical orders on the same market in the same second, and both
+are legitimately distinct; primary key **`id`**.
+
+| Attribute | Type | Constraints |
+|---|---|---|
+| `id` | UUID | PK, required |
+| `side` | text | required, `buy` or `sell` |
+| `type` | text | required, `market` or `limit` — the prototype executes only `market`; `limit` exists so the model does not have to change when limit orders are implemented |
+| `status` | text | required, `open`, `executed` or `cancelled` |
+| `quantity` | numeric(20,4) | required, > 0 |
+| `price` | numeric(18,6) | optional — null until the order settles, then the fill price |
+| `placed_at` | timestamptz | required, defaults to now |
+| `executed_at` | timestamptz | optional, set when the order settles |
 
 #### Transactions
@@ -110,13 +128,14 @@
 the project needs.
 
-- **Candidate keys:** `{id}` only. Primary key: **`id`**.
-- **Attributes:**
-  - `id` — UUID, required, primary key.
-  - `type` — text, required, restricted to `deposit`, `buy`, `sell`, `fee`.
-  - `amount` — numeric(18,4), required. Signed: negative for money leaving the
-    cash balance, positive for money arriving.
-  - `currency` — text, exactly 3 characters, required, default `USD`.
-  - `created_at` — timestamptz, required, defaults to now.
-  - `description` — text, optional, free-form human-readable explanation.
+**Keys:** candidate `{id}` only; primary key **`id`**.
+
+| Attribute | Type | Constraints |
+|---|---|---|
+| `id` | UUID | PK, required |
+| `type` | text | required, `deposit`, `buy`, `sell` or `fee` |
+| `amount` | numeric(18,4) | required, signed — negative for money leaving the cash balance, positive for money arriving |
+| `currency` | text(3) | required, default `USD` |
+| `created_at` | timestamptz | required, defaults to now |
+| `description` | text | optional, free-form |
 
 #### MarketTrades
@@ -126,17 +145,18 @@
 writes directly.
 
-- **Candidate keys:** `{id}`. In principle `{market_id, executed_at}` looks
-  unique, but two trades can share a timestamp, so it is not a safe key.
-  Primary key: **`id`** (a plain auto-incrementing integer here rather than a
-  UUID, because this is the highest-volume entity set and it is only ever read
-  in timestamp order, never referenced by anything else).
-- **Attributes:**
-  - `id` — integer, required, primary key, auto-generated.
-  - `executed_at` — timestamptz, required.
-  - `price` — numeric(18,6), required, must be > 0.
-  - `quantity` — numeric(20,6), required, must be > 0.
-  - `side` — text, optional, `buy` or `sell`.
-  - `source` — text, max 50, required, default `simulation`. Distinguishes a
-    simulated trade from a user's own fill (`user`).
+**Keys:** candidate `{id}` — `{market_id, executed_at}` looks unique in
+principle, but two trades can share a timestamp, so it is not a safe key;
+primary key **`id`** (a plain auto-incrementing integer here rather than a
+UUID, because this is the highest-volume entity set and it is only ever read
+in timestamp order, never referenced by anything else).
+
+| Attribute | Type | Constraints |
+|---|---|---|
+| `id` | integer | PK, required, auto-generated |
+| `executed_at` | timestamptz | required |
+| `price` | numeric(18,6) | required, > 0 |
+| `quantity` | numeric(20,6) | required, > 0 |
+| `side` | text | optional, `buy` or `sell` |
+| `source` | text(50) | required, default `simulation` — distinguishes a simulated trade from a user's own fill (`user`) |
 
 #### MarketCandles
@@ -146,14 +166,16 @@
 screen refresh does not scale.
 
-- **Candidate keys:** `{id}`, and `{market_id, timeframe, candle_time}` — a
-  market has exactly one candle per timeframe per time bucket. Primary key:
-  **`id`**; the composite is enforced as a uniqueness rule because it is the
-  real-world constraint and it is what prevents duplicate candles.
-- **Attributes:**
-  - `id` — integer, required, primary key, auto-generated.
-  - `timeframe` — text, required, restricted to `1m`, `5m`, `1h`, `1d`.
-  - `open`, `high`, `low`, `close` — numeric(18,6), all required.
-  - `volume` — numeric(20,6), required.
-  - `candle_time` — timestamptz, required — the start of the bucket.
+**Keys:** candidates `{id}`, `{market_id, timeframe, candle_time}` — a market
+has exactly one candle per timeframe per time bucket; primary key **`id`**, the
+composite is enforced as a uniqueness rule because it is the real-world
+constraint and it is what prevents duplicate candles.
+
+| Attribute | Type | Constraints |
+|---|---|---|
+| `id` | integer | PK, required, auto-generated |
+| `timeframe` | text | required, `1m`, `5m`, `1h` or `1d` |
+| `open`, `high`, `low`, `close` | numeric(18,6) | all required |
+| `volume` | numeric(20,6) | required |
+| `candle_time` | timestamptz | required — the start of the bucket |
 
 #### Watchlists
@@ -162,11 +184,13 @@
 several lists ("long term", "watching today") and each needs its own name.
 
-- **Candidate keys:** `{id}`. `{user_id, name}` would also work if list names
-  are required to be unique per user; the model does not impose that, so it is
-  not listed as a candidate key. Primary key: **`id`**.
-- **Attributes:**
-  - `id` — UUID, required, primary key.
-  - `name` — text, max 100, required.
-  - `created_at` — timestamptz, required, defaults to now.
+**Keys:** candidate `{id}` — `{user_id, name}` would also work if list names
+were required to be unique per user, which the model does not impose, so it is
+not listed as a candidate key; primary key **`id`**.
+
+| Attribute | Type | Constraints |
+|---|---|---|
+| `id` | UUID | PK, required |
+| `name` | text(100) | required |
+| `created_at` | timestamptz | required, defaults to now |
 
 ### Relationships
@@ -175,5 +199,5 @@
 Ties a market to the asset it trades. One asset can be quoted in many markets;
 every market must have exactly one asset, hence total participation on the
-`Markets` side. No attributes of its own.
+`Markets` side. No attributes.
 
 #### PlacedOn — Markets (1) : Orders (N), total on Orders
@@ -182,16 +206,17 @@
 
 #### Places — Users (1) : Orders (N), total on Orders
-Records who placed an order. Every order belongs to exactly one user; a new user
-has no orders. No attributes.
+Records who placed an order. Every order belongs to exactly one user; a new
+user has no orders. No attributes.
 
 #### Records — Users (1) : Transactions (N), total on Transactions
-Attributes each ledger entry to a user. Every entry belongs to exactly one user.
-No attributes.
+Attributes each ledger entry to a user. Every entry belongs to exactly one
+user. No attributes.
 
 #### Settles — Orders (1) : Transactions (N), partial on both sides
-Links a ledger entry to the order that caused it. Partial on the `Transactions`
-side because deposits have no originating order, and partial on the `Orders` side
-because an order that never executes never produces a ledger entry. This is why
-the corresponding column is nullable in P2. No attributes.
+Links a ledger entry to the order that caused it. Partial on the
+`Transactions` side because deposits have no originating order, and partial on
+the `Orders` side because an order that never executes never produces a
+ledger entry — which is why the corresponding column is nullable in P2. No
+attributes.
 
 #### Fills — Markets (1) : MarketTrades (N), total on MarketTrades
@@ -211,12 +236,19 @@
 entirely described by *which user*, *which asset*, and how much.
 
-- **Attributes:**
-  - `quantity` — numeric(20,4), required, must be ≥ 0.
-  - `avg_price` — numeric(18,6), required, ≥ 0, **derived** (dashed ellipse):
-    the weighted average of the prices at which the position was accumulated.
-    It is derivable from the buy history, and is stored anyway so that
-    unrealised P/L can be shown without replaying the whole ledger.
-  - `created_at` — timestamptz, required, defaults to now.
-  - `updated_at` — timestamptz, optional.
+`reserved_quantity` mirrors `available_balance`/`invested_balance` on `Users`:
+two independently updated stored numbers, with the amount actually free to use
+computed on demand rather than stored (`quantity − reserved_quantity` here,
+`available_balance` alone on the cash side). Without it, nothing stopped a
+user from placing a second sell order against crypto already promised to a
+first one — `quantity` alone cannot tell "owned" apart from "owned, but
+already committed elsewhere." See [history](#entity-relationship-model-history), v03.
+
+| Attribute | Type | Constraints |
+|---|---|---|
+| `quantity` | numeric(20,4) | required, ≥ 0 — total amount owned |
+| `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 |
+| `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 |
+| `created_at` | timestamptz | required, defaults to now |
+| `updated_at` | timestamptz | optional |
 
 #### Contains — Watchlists (M) : Cryptos (N), partial on both sides, **with attribute**
@@ -225,7 +257,7 @@
 asset need not be on any list.
 
-- **Attributes:**
-  - `added_at` — timestamptz, required, defaults to now. Recorded so a list can
-    be shown in the order the user built it.
+| Attribute | Type | Constraints |
+|---|---|---|
+| `added_at` | timestamptz | required, defaults to now — recorded so a list can be shown in the order the user built it |
 
 ## Entity-Relationship Model History
@@ -243,5 +275,26 @@
   3. `avg_price` was marked as a derived attribute rather than a plain one, to
      make the denormalisation explicit rather than hidden.
+- **v02** — Student review pass over the AI-generated v01 in the TerraER GUI.
+- **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 [ERModelAIUsage](ERModelAIUsage.md) for
+  the reasoning and how the diagram file itself was produced, and
+  [RelationalDesign](../P2-RelationalDesign/RelationalDesign.md) and
+  [UseCase0005](../P3-UseCaseModel/UseCase0005.md) for how the new attribute
+  is enforced.
 
 Reasoning for the AI-assisted part of this phase, and the full interaction log,
 are on [ERModelAIUsage](ERModelAIUsage.md).
+
+> **Student action required.** Open `ERModel_v03.xml` in TerraER, read the
+> whole diagram — not just the new `reserved_quantity` ellipse — and change
+> anything you disagree with, including the compaction. The phase rules
+> require the model to be yours; this is a generated revision to review and
+> take over, not an answer to submit unread.
