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.
Index: docs/P1-ConceptualModel/ERModelAIUsage.md
===================================================================
--- docs/P1-ConceptualModel/ERModelAIUsage.md	(revision df058387ebe7d07864d874fd1376f15512418581)
+++ docs/P1-ConceptualModel/ERModelAIUsage.md	(revision 9577c7944ff24ddc9145ff6637494f07d87c3dd4)
@@ -34,6 +34,25 @@
 by reading it back through TerraER's own reader and comparing the figure count
 (144), and it opens and can be edited in TerraER 3.11 like any hand-drawn
-diagram. Subsequent versions (`ERModel_v02.xml` onward) are edited by the student
-in the GUI.
+diagram. `ERModel_v02.xml` is the student's own review pass over v01, done by
+hand in the GUI.
+
+`ERModel_v03.xml` / `ERModel_v03.png` (session 3, 2026-09-16) were produced the
+same way, this time as a genuine load–modify–save round trip through TerraER's
+own classes rather than a from-scratch build: `ERModel_v02.xml` was read with
+the application's real `DrawFigureFactory` and `DOMStorableInputOutputFormat`
+into a live `QuadTreeDrawing`, one `AtributoFigure` was cloned from the
+existing `quantity` attribute of `Holds` (to inherit its exact styling) and
+relabelled `reserved_quantity`, a matching `LabeledLineConnectionFigure` was
+added between it and the `Holds` diamond (`ChopDiamondConnector` /
+`ChopEllipseConnector`, the same connector pair every other attribute of
+`Holds` uses), and every entity and relationship — together with its own
+attributes, moved by the same offset — was translated proportionally toward
+the diagram's centroid to close up excess canvas space, after which every
+connection figure had `updateConnection()` called so its drawn path follows
+the moved figures. The result was written with the real writer and rendered to
+PNG with TerraER's own `ImageOutputFormat`, and re-verified by reading
+`ERModel_v03.xml` back and confirming the figure count (146 = 144 + the new
+attribute + its line) and that all five attributes of `Holds` resolve with the
+expected connector classes. No figure was hand-edited in XML.
 
 ### Model description
@@ -43,12 +62,12 @@
 ## Summary of AI involvement
 
-Work on this project happened in two working sessions, several months apart.
-
-| | Session 1 | Session 2 |
-|---|---|---|
-| **When** | 2026-04-21 | 2026-08-06 / 2026-08-07 |
-| **Model** | Claude Opus 4.7 (1M context) | Claude Opus 5 (1M context) |
-| **Phases advanced** | P1, P2, P3 and the first working prototype | The ER diagram file, P4 documentation, bug fixes |
-| **My starting material** | `ep-diagram.md`, `opis.md`, my existing Go backend and draft SQL | Everything from session 1, plus the phase rubric |
+Work on this project happened in three working sessions.
+
+| | Session 1 | Session 2 | Session 3 |
+|---|---|---|---|
+| **When** | 2026-04-21 | 2026-08-06 / 2026-08-07 | 2026-09-16 |
+| **Model** | Claude Opus 4.7 (1M context) | Claude Opus 5 (1M context) | Claude Sonnet 5 |
+| **Phases advanced** | P1, P2, P3 and the first working prototype | The ER diagram file, P4 documentation, bug fixes | `Holds.reserved_quantity` added across P1–P4 |
+| **My starting material** | `ep-diagram.md`, `opis.md`, my existing Go backend and draft SQL | Everything from session 1, plus the phase rubric | Everything from sessions 1–2, plus a design review of the sell-order flow |
 
 In **session 1** I brought my own data model (`ep-diagram.md`, written in
@@ -65,4 +84,13 @@
 for a review pass over the database and Go code, which turned up three further
 bugs (see [PrototypeImplementationAIUsage](../P4-Prototype/PrototypeImplementationAIUsage.md)).
+
+In **session 3** I described a concrete edge case I'd spotted in the sell flow
+— nothing stopped a user from placing a second sell order against crypto
+already promised to a first, unsettled one — and asked the AI to fix it
+starting from P1 and carry the fix through every phase. This added
+`reserved_quantity` to `Holds` and changed the sell path in `trade.go` to use
+it; see the model history on
+[ERModel](ERModel.md#entity-relationship-model-history) (v03) and the log
+below.
 
 Below is the log, one entry per exchange. My prompts are quoted as **excerpts**:
@@ -176,2 +204,85 @@
 >    any further prompts. The complete transcript is in your Claude Code session
 >    history.
+
+---
+
+### Session 3 — 2026-09-16
+
+**Intent:** describe a concrete gap I'd found in the sell-order design — the
+model had no way to represent crypto committed to an order that had not yet
+settled — and have the AI trace the fix through every phase rather than patch
+`trade.go` alone.
+
+**Prompt (student, excerpt):**
+> Suppose the user owns 2 BTC and wants to sell 0.5 BTC at market price. […] You
+> currently have Holds: quantity, avg_price but you don't have a concept of
+> reserved/committed crypto. For cash, you already explicitly distinguish
+> available_balance, invested_balance. For crypto, you probably want the
+> equivalent: quantity, reserved_quantity, avg_price. […] Can you please start
+> from Phase 1 and modify everything to make it work like it should, modify
+> the AIUsage.md in every Phase, and can you make the ConceptualModel a little
+> bit more put together not that much spread out.
+
+**Response (AI, summarised):**
+- Read every phase document and the live `server/trade.go`, `portfolio.go` and
+  `schema_creation.sql` to confirm the actual gap: the sell path checked
+  `holdings.quantity` directly, with nothing recording that part of a position
+  was already promised to another order.
+- Added `reserved_quantity` to `Holds` (P1), `holdings` (P2), the sell scenario
+  SQL (P3), and `trade.go`/`portfolio.go`/`schema_creation.sql` (P4) — one
+  attribute threaded consistently through all four phases rather than a
+  patch confined to the Go code.
+- Changed the sell path to lock the holding row, compute
+  `available = quantity − reserved_quantity`, reserve (`reserved_quantity +=
+  qty`) and then settle (`quantity -= qty; reserved_quantity -= qty`) inside
+  the same transaction — keeping the existing all-or-nothing guarantee rather
+  than splitting into two commits, which would leave an unresolvable `open`
+  order if the process died in between and there is no cancel use case to
+  recover it.
+- Made both buy and sell orders genuinely pass through `status='open'` before
+  `status='executed'`, instead of being inserted as `'executed'` directly, so
+  the lifecycle the ER model already claimed for `Orders` is real.
+- Added a `CHECK (reserved_quantity >= 0 AND reserved_quantity <= quantity)`
+  constraint and exposed `reserved_quantity`/`available_quantity` on
+  `v_portfolio` and in the CLI's portfolio table, so the new field is
+  observable rather than a column nobody can see.
+- Restructured this phase's [ERModel](ERModel.md) into short rationale + a
+  compact attribute table per entity/relationship, per the "more put together,
+  not that much spread out" request — no content was removed, only
+  reformatted.
+- Verified the fix against the live database (`bp_database` on `localhost:5433`):
+  ran two concurrent `sell 1.0 BTC` attempts against a holding with only 1.5
+  BTC available — exactly one succeeded, the other correctly reported
+  insufficient holding — and ran the reserve/settle sequence by hand in `psql`
+  to show `reserved_quantity` at 0.5 mid-transaction. Both are recorded in
+  [UseCase0005Implementation](../P4-Prototype/UseCase0005Implementation.md).
+
+**What I decided:** to keep reserve and settle inside one transaction rather
+than two (see the AI's reasoning above — I agreed with it, since a stuck
+`open` order with no cancel command would be a worse bug than the one being
+fixed).
+
+**Follow-up, same day:** I asked for `ERModel_v03.xml`/`.png` after all,
+having noticed the PNG still showed v02 with no `reserved_quantity` on it, and
+asked at the same time for the diagram to be a little more compact — it had a
+lot of empty canvas in the middle. The AI drove TerraER's own classes directly
+(load → clone the `quantity` attribute → relabel it → add its connecting line
+→ pull every cluster toward the centroid → save → render), described in full
+under [Diagram](#diagram) above, rather than hand-editing the XML or asking me
+to do it in the GUI. I reviewed the rendered PNG before accepting it.
+
+**Second follow-up, same day:** the first `ERModel_v03.png` rendered with a
+black background instead of white, unlike v01/v02. Cause: TerraER's
+`ImageOutputFormat` defaults to an ARGB image and paints its background with
+zero alpha (transparent), not opaque white; whatever displayed the PNG then
+flattened that transparency onto black instead of white. Fixed by exporting
+through the same `ImageOutputFormat.toImage(...)` call for figure geometry,
+but compositing its result onto an explicitly white-filled opaque `RGB` image
+before saving, rather than trusting the library's own (transparent) output.
+Verified the fix by reading back the corner pixel of the written PNG as pure
+white `(255,255,255)`, matching `ERModel_v02.png`.
+
+> **Student action required.** Open `ERModel_v03.xml` in TerraER and read it
+> end to end before submission — see the note at the end of
+> [ERModel](ERModel.md). Everything else in this session's diff is already
+> applied to the docs and to `server/`.
Index: docs/P1-ConceptualModel/ERModel_v03.xml
===================================================================
--- docs/P1-ConceptualModel/ERModel_v03.xml	(revision 9577c7944ff24ddc9145ff6637494f07d87c3dd4)
+++ docs/P1-ConceptualModel/ERModel_v03.xml	(revision 9577c7944ff24ddc9145ff6637494f07d87c3dd4)
@@ -0,0 +1,1 @@
+<drawing><figures><ent id="0"><children><r id="1" x="1074.6" y="871.7" w="210" h="72"/><t id="2" x="1157.9098373413085" y="899.5279428482056"><a><text><string>Cryptos</string></text></a></t></children></ent><ent id="3"><children><r id="4" x="1917" y="871.7" w="210" h="72"/><t id="5" x="1999.0858306884766" y="899.5279428482056"><a><text><string>Markets</string></text></a></t></children></ent><ent id="6"><children><r id="7" x="2478.6" y="1058.9" w="210" h="72"/><t id="8" x="2544.431718444824" y="1086.7279428482057"><a><text><string>MarketTrades</string></text></a></t></children></ent><ent id="9"><children><r id="a" x="2478.6" y="1526.9" w="210" h="72"/><t id="b" x="2541.1976943969726" y="1554.7279428482057"><a><text><string>MarketCandles</string></text></a></t></children></ent><ent id="c"><children><r id="d" x="1917" y="1807.7" w="210" h="72"/><t id="e" x="2002.4098510742188" y="1835.5279428482056"><a><text><string>Orders</string></text></a></t></children></ent><ent id="f"><children><r id="10" x="1402.2" y="1994.9" w="210" h="72"/><t id="11" x="1471.2657501220704" y="2022.7279428482057"><a><text><string>Transactions</string></text></a></t></children></ent><ent id="12"><children><r id="13" x="887.4" y="1620.5" w="210.0000000000001" h="72"/><t id="14" x="976.4038757324219" y="1648.3279428482056"><a><text><string>Users</string></text></a></t></children></ent><ent id="15"><children><r id="16" x="653.4" y="1152.5" w="210" h="72"/><t id="17" x="729.689794921875" y="1180.3279428482056"><a><text><string>Watchlists</string></text></a></t></children></ent><rel id="18"><children><diamond id="19" x="1500.8" y="859.7" w="200" h="96"/><t id="1a" x="1571.1417892456054" y="899.5279428482056"><a><text><string>QuotedOn</string></text></a></t></children></rel><rel id="1b"><children><diamond id="1c" x="2226.2" y="929.9" w="200" h="96.00000000000011"/><t id="1d" x="2315.567919921875" y="969.7279428482055"><a><text><string>Fills</string></text></a></t></children></rel><rel id="1e"><children><diamond id="1f" x="2249.6" y="1257.5" w="200" h="96"/><t id="20" x="2317.04376373291" y="1297.3279428482056"><a><text><string>Aggregates</string></text></a></t></children></rel><rel id="21"><children><diamond id="22" x="1922" y="1327.7" w="200" h="96"/><t id="23" x="1995.107810974121" y="1367.5279428482056"><a><text><string>PlacedOn</string></text></a></t></children></rel><rel id="24"><children><diamond id="25" x="1664.6" y="1889.3000000000002" w="200" h="96"/><t id="26" x="1745.7838607788085" y="1929.1279428482057"><a><text><string>Settles</string></text></a></t></children></rel><rel id="27"><children><diamond id="28" x="1149.8" y="1795.7" w="200" h="96"/><t id="29" x="1227.1318328857421" y="1835.5279428482056"><a><text><string>Records</string></text></a></t></children></rel><rel id="2a"><children><diamond id="2b" x="1407.2" y="1702.1" w="200" h="96"/><t id="2c" x="1489.51787109375" y="1741.9279428482055"><a><text><string>Places</string></text></a></t></children></rel><rel id="2d"><children><diamond id="2e" x="986" y="1234.1" w="200" h="96"/><t id="2f" x="1069.811882019043" y="1273.9279428482055"><a><text><string>Holds</string></text></a></t></children></rel><rel id="30"><children><diamond id="31" x="775.4" y="1374.5" w="200" h="96"/><t id="32" x="859.415884399414" y="1414.3279428482056"><a><text><string>Owns</string></text></a></t></children></rel><rel id="33"><children><diamond id="34" x="845.6" y="976.7" w="199.9999999999999" h="96"/><t id="35" x="920.8078247070313" y="1016.5279428482056"><a><text><string>Contains</string></text></a></t></children></rel><llabelUm id="36"><points><p colinear="true" x="1285.1" y="907.7" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1500.367268932415" y="907.7" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="37"><Owner><r ref="1"/></Owner></rConnector></startConnector><endConnector><diamondConnector id="38"><Owner><diamond ref="19"/></Owner></diamondConnector></endConnector></llabelUm><llabelDoubleMuitos id="39"><points><p colinear="true" x="1701.2327310675846" y="907.7" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1916.5" y="907.7" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><diamondConnector id="3a"><Owner><diamond ref="19"/></Owner></diamondConnector></startConnector><endConnector><rConnector id="3b"><Owner><r ref="4"/></Owner></rConnector></endConnector><a><innerStrokeWidthFactor><double>3</double></innerStrokeWidthFactor></a></llabelDoubleMuitos><llabelUm id="3c"><points><p colinear="true" x="2127.5" y="932.046153846154" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2258.061419777714" y="962.1757122563956" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="3d"><Owner><r ref="4"/></Owner></rConnector></startConnector><endConnector><diamondConnector id="3e"><Owner><diamond ref="1c"/></Owner></diamondConnector></endConnector></llabelUm><llabelDoubleMuitos id="3f"><points><p colinear="true" x="2378.142569236337" y="1001.5102587437897" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2503.2999999999997" y="1058.4" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><diamondConnector id="40"><Owner><diamond ref="1c"/></Owner></diamondConnector></startConnector><endConnector><rConnector id="41"><Owner><r ref="7"/></Owner></rConnector></endConnector><a><innerStrokeWidthFactor><double>3</double></innerStrokeWidthFactor></a></llabelDoubleMuitos><llabelUm id="42"><points><p colinear="true" x="2052.0588235294117" y="944.2" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2320.854586641136" y="1270.5948552070938" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="43"><Owner><r ref="4"/></Owner></rConnector></startConnector><endConnector><diamondConnector id="44"><Owner><diamond ref="1f"/></Owner></diamondConnector></endConnector></llabelUm><llabelDoubleMuitos id="45"><points><p colinear="true" x="2380.415596146642" y="1339.3971557613067" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2550.418181818182" y="1526.4" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><diamondConnector id="46"><Owner><diamond ref="1f"/></Owner></diamondConnector></startConnector><endConnector><rConnector id="47"><Owner><r ref="a"/></Owner></rConnector></endConnector><a><innerStrokeWidthFactor><double>3</double></innerStrokeWidthFactor></a></llabelDoubleMuitos><llabelMuitos id="48"><points><p colinear="true" x="1001.525" y="1620" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1075.1012874391326" y="1325.6948502434695" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="49"><Owner><r ref="13"/></Owner></rConnector></startConnector><endConnector><diamondConnector id="4a"><Owner><diamond ref="2e"/></Owner></diamondConnector></endConnector></llabelMuitos><llabelMuitos id="4b"><points><p colinear="true" x="1096.8987125608674" y="1238.5051497565303" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1170.475" y="944.2" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><diamondConnector id="4c"><Owner><diamond ref="2e"/></Owner></diamondConnector></startConnector><endConnector><rConnector id="4d"><Owner><r ref="1"/></Owner></rConnector></endConnector></llabelMuitos><llabelUm id="4e"><points><p colinear="true" x="974.15" y="1620" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="895.0635816788919" y="1461.8271633577838" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="4f"><Owner><r ref="13"/></Owner></rConnector></startConnector><endConnector><diamondConnector id="50"><Owner><diamond ref="31"/></Owner></diamondConnector></endConnector></llabelUm><llabelDoubleMuitos id="51"><points><p colinear="true" x="855.7364183211081" y="1383.1728366422162" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="776.65" y="1225" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><diamondConnector id="52"><Owner><diamond ref="31"/></Owner></diamondConnector></startConnector><endConnector><rConnector id="53"><Owner><r ref="16"/></Owner></rConnector></endConnector><a><innerStrokeWidthFactor><double>3</double></innerStrokeWidthFactor></a></llabelDoubleMuitos><llabelMuitos id="54"><points><p colinear="true" x="800.1142857142856" y="1152" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="909.6933786333907" y="1056.1182936957832" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="55"><Owner><r ref="16"/></Owner></rConnector></startConnector><endConnector><diamondConnector id="56"><Owner><diamond ref="34"/></Owner></diamondConnector></endConnector></llabelMuitos><llabelMuitos id="57"><points><p colinear="true" x="995.1502233131612" y="999.9248883434194" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1106.6" y="944.2" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><diamondConnector id="58"><Owner><diamond ref="34"/></Owner></diamondConnector></startConnector><endConnector><rConnector id="59"><Owner><r ref="1"/></Owner></rConnector></endConnector></llabelMuitos><llabelUm id="5a"><points><p colinear="true" x="1097.9" y="1675.6818181818182" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1434.0736476037464" y="1736.8042995643175" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="5b"><Owner><r ref="13"/></Owner></rConnector></startConnector><endConnector><diamondConnector id="5c"><Owner><diamond ref="2b"/></Owner></diamondConnector></endConnector></llabelUm><llabelDoubleMuitos id="5d"><points><p colinear="true" x="1580.3263523962532" y="1763.3957004356823" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1916.5" y="1824.5181818181818" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><diamondConnector id="5e"><Owner><diamond ref="2b"/></Owner></diamondConnector></startConnector><endConnector><rConnector id="5f"><Owner><r ref="d"/></Owner></rConnector></endConnector><a><innerStrokeWidthFactor><double>3</double></innerStrokeWidthFactor></a></llabelDoubleMuitos><llabelUm id="60"><points><p colinear="true" x="2022" y="944.2" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2022" y="1326.7984769425318" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="61"><Owner><r ref="4"/></Owner></rConnector></startConnector><endConnector><diamondConnector id="62"><Owner><diamond ref="22"/></Owner></diamondConnector></endConnector></llabelUm><llabelDoubleMuitos id="63"><points><p colinear="true" x="2022" y="1424.6015230574683" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2022" y="1807.2" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><diamondConnector id="64"><Owner><diamond ref="22"/></Owner></diamondConnector></startConnector><endConnector><rConnector id="65"><Owner><r ref="d"/></Owner></rConnector></endConnector><a><innerStrokeWidthFactor><double>3</double></innerStrokeWidthFactor></a></llabelDoubleMuitos><llabelUm id="66"><points><p colinear="true" x="1042.5875" y="1693" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1209.524682824987" y="1814.4088602363543" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="67"><Owner><r ref="13"/></Owner></rConnector></startConnector><endConnector><diamondConnector id="68"><Owner><diamond ref="28"/></Owner></diamondConnector></endConnector></llabelUm><llabelDoubleMuitos id="69"><points><p colinear="true" x="1290.0753171750125" y="1872.9911397636458" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1457.0125" y="1994.4" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><diamondConnector id="6a"><Owner><diamond ref="28"/></Owner></diamondConnector></startConnector><endConnector><rConnector id="6b"><Owner><r ref="10"/></Owner></rConnector></endConnector><a><innerStrokeWidthFactor><double>3</double></innerStrokeWidthFactor></a></llabelDoubleMuitos><llabelUm id="6c"><points><p colinear="true" x="1921.6250000000002" y="1880.2" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1822.0943672237672" y="1916.3929573731757" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="6d"><Owner><r ref="d"/></Owner></rConnector></startConnector><endConnector><diamondConnector id="6e"><Owner><diamond ref="25"/></Owner></diamondConnector></endConnector></llabelUm><llabelMuitos id="6f"><points><p colinear="true" x="1707.1056327762324" y="1958.2070426268247" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1607.575" y="1994.4" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><diamondConnector id="70"><Owner><diamond ref="25"/></Owner></diamondConnector></startConnector><endConnector><rConnector id="71"><Owner><r ref="10"/></Owner></rConnector></endConnector></llabelMuitos><atr id="72" nullable="false" attributeType="VARCHAR2(128)"><children><e id="73" x="726.8879772722291" y="947.7138777184988" w="168" h="50"/><t id="74" x="781.3437725603151" y="964.5418205667044"><a><text><string>created_at</string></text></a></t></children></atr><llabel id="75"><points><p colinear="true" x="1074.1" y="926.3024964647431" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="884.3466049810012" y="960.3493031779781" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="76"><Owner><r ref="1"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="77"><Owner><e ref="73"/></Owner></ellipseConnector></endConnector></llabel><atr id="78" nullable="false" attributeType="VARCHAR2(128)"><children><e id="79" x="747.6522869294774" y="744.469218446131" w="168" h="50"/><t id="7a" x="815.584179324497" y="761.2971612943365"><a><text><string>name</string></text></a></t></children></atr><llabel id="7b"><points><p colinear="true" x="1087.723998256316" y="871.2" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="883.2653378775243" y="790.2751342913638" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="7c"><Owner><r ref="1"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="7d"><Owner><e ref="79"/></Owner></ellipseConnector></endConnector></llabel><atr id="7e" nullable="false" attributeType="VARCHAR2(128)"><children><e id="7f" x="872.0238232664767" y="584.0548844636454" w="168" h="50"/><t id="80" x="935.611675744504" y="600.882827311851"><a><text><string>symbol</string></text></a></t></children></atr><llabel id="81"><points><p colinear="true" x="1152.2748236410375" y="871.2" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="975.144726893544" y="634.428026120974" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="82"><Owner><r ref="1"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="83"><Owner><e ref="7f"/></Owner></ellipseConnector></endConnector></llabel><atrchave id="84" nullable="false" attributeType="NUMBER"><children><e id="85" x="1062.9688899152766" y="508.0548972033168" w="168" h="50.00000000000006"/><t id="86" x="1141.3408534162531" y="524.8828400515224"><a><fontUnderlined><boolean>true</boolean></fontUnderlined><fontBold><boolean>true</boolean></fontBold><text><string>id</string></text></a></t></children></atrchave><llabel id="87"><points><p colinear="true" x="1176.4208966053434" y="871.2" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1149.6891405457882" y="559.0460932902021" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="88"><Owner><r ref="1"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="89"><Owner><e ref="85"/></Owner></ellipseConnector></endConnector></llabel><atr id="8a" nullable="false" attributeType="VARCHAR2(128)"><children><e id="8b" x="1631.3094746182014" y="667.9529822301685" w="168" h="50"/><t id="8c" x="1685.7652699062874" y="684.780925078374"><a><text><string>created_at</string></text></a></t></children></atr><llabel id="8d"><points><p colinear="true" x="1969.8725977539127" y="871.2" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1749.2534612709806" y="716.8707137922294" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="8e"><Owner><r ref="4"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="8f"><Owner><e ref="8b"/></Owner></ellipseConnector></endConnector></llabel><atr id="90" nullable="false" attributeType="VARCHAR2(128)"><children><e id="91" x="1809.9476583388696" y="530.8790827777559" w="168" h="50"/><t id="92" x="1870.469493666018" y="547.7070256259615"><a><text><string>is_active</string></text></a></t></children></atr><llabel id="93"><points><p colinear="true" x="2008.7150864492837" y="871.2" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1903.6734154443525" y="581.7266421024433" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="94"><Owner><r ref="4"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="95"><Owner><e ref="91"/></Owner></ellipseConnector></endConnector></llabel><atr id="96" nullable="false" attributeType="VARCHAR2(128)"><children><e id="97" x="2034.9018504863839" y="521.0573706373729" w="168" h="50"/><t id="98" x="2075.0835445171456" y="537.8853134855784"><a><text><string>quote_currency</string></text></a></t></children></atr><llabel id="99"><points><p colinear="true" x="2031.7801455237359" y="871.2" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2112.5913746299984" y="571.9744125571244" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="9a"><Owner><r ref="4"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="9b"><Owner><e ref="97"/></Owner></ellipseConnector></endConnector></llabel><atrchave id="9c" nullable="false" attributeType="NUMBER"><children><e id="9d" x="2224.8070395037453" y="642.0403189333597" w="168" h="50"/><t id="9e" x="2303.179003004722" y="658.8682617815653"><a><fontUnderlined><boolean>true</boolean></fontUnderlined><fontBold><boolean>true</boolean></fontBold><text><string>id</string></text></a></t></children></atrchave><llabel id="9f"><points><p colinear="true" x="2065.4990061296885" y="871.2" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2280.7104729582716" y="691.5356873746031" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="a0"><Owner><r ref="4"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="a1"><Owner><e ref="9d"/></Owner></ellipseConnector></endConnector></llabel><atrchave id="a2" nullable="false" attributeType="NUMBER"><children><e id="a3" x="2684.1996567283372" y="674.0247586223913" w="168" h="50"/><t id="a4" x="2762.571620229314" y="690.8527014705969"><a><fontUnderlined><boolean>true</boolean></fontUnderlined><fontBold><boolean>true</boolean></fontBold><text><string>id</string></text></a></t></children></atrchave><llabel id="a5"><points><p colinear="true" x="2600.620229522657" y="1058.4" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2756.9248233656053" y="724.7759702566166" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="a6"><Owner><r ref="7"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="a7"><Owner><e ref="a3"/></Owner></ellipseConnector></endConnector></llabel><atr id="a8" nullable="false" attributeType="VARCHAR2(128)"><children><e id="a9" x="2803.02677621649" y="755.6923752120773" w="168" h="50"/><t id="aa" x="2853.060543916685" y="772.5203180602829"><a><text><string>executed_at</string></text></a></t></children></atr><llabel id="ab"><points><p colinear="true" x="2618.847640280458" y="1058.4" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2863.885153628648" y="805.6739920689837" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="ac"><Owner><r ref="7"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="ad"><Owner><e ref="a9"/></Owner></ellipseConnector></endConnector></llabel><atr id="ae" nullable="false" attributeType="VARCHAR2(128)"><children><e id="af" x="2888.791649765479" y="871.5969497137661" w="168" h="50"/><t id="b0" x="2958.8115472264167" y="888.4248925619717"><a><text><string>price</string></text></a></t></children></atr><llabel id="b1"><points><p colinear="true" x="2655.235283450938" y="1058.4" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2930.2308648073213" y="919.0375155251545" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="b2"><Owner><r ref="7"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="b3"><Owner><e ref="af"/></Owner></ellipseConnector></endConnector></llabel><atr id="b4" nullable="false" attributeType="VARCHAR2(128)"><children><e id="b5" x="2932.149092426318" y="1009.1091895006435" w="168" h="49.999999999999886"/><t id="b6" x="2992.736937274951" y="1025.937132348849"><a><text><string>quantity</string></text></a></t></children></atr><llabel id="b7"><points><p colinear="true" x="2689.1" y="1080.0729419388979" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2940.0486683659437" y="1045.3746770366456" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="b8"><Owner><r ref="7"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="b9"><Owner><e ref="b5"/></Owner></ellipseConnector></endConnector></llabel><atr id="ba" nullable="false" attributeType="VARCHAR2(128)"><children><e id="bb" x="2928.3747537299396" y="1153.2453691804749" w="168" h="50"/><t id="bc" x="3000.878667609334" y="1170.0733120286804"><a><text><string>side</string></text></a></t></children></atr><llabel id="bd"><points><p colinear="true" x="2689.1" y="1115.4071226140295" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2941.8361049738987" y="1164.93685467455" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="be"><Owner><r ref="7"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="bf"><Owner><e ref="bb"/></Owner></ellipseConnector></endConnector></llabel><atr id="c0" nullable="false" attributeType="VARCHAR2(128)"><children><e id="c1" x="2877.879896373043" y="1288.3000000000002" w="168" h="50"/><t id="c2" x="2942.92575666357" y="1305.1279428482057"><a><text><string>source</string></text></a></t></children></atr><llabel id="c3"><points><p colinear="true" x="2646.819854476264" y="1131.4" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2923.2371144835133" y="1291.2009043392497" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="c4"><Owner><r ref="7"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="c5"><Owner><e ref="c1"/></Owner></ellipseConnector></endConnector></llabel><atrchave id="c6" nullable="false" attributeType="NUMBER"><children><e id="c7" x="2952.0288472886955" y="1326.928963739043" w="168" h="50"/><t id="c8" x="3030.400810789672" y="1343.7569065872485"><a><fontUnderlined><boolean>true</boolean></fontUnderlined><fontBold><boolean>true</boolean></fontBold><text><string>id</string></text></a></t></children></atrchave><llabel id="c9"><points><p colinear="true" x="2661.8745025985986" y="1526.4" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2990.619110480273" y="1373.8370255966909" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="ca"><Owner><r ref="a"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="cb"><Owner><e ref="c7"/></Owner></ellipseConnector></endConnector></llabel><atr id="cc" nullable="false" attributeType="VARCHAR2(128)"><children><e id="cd" x="2992.258472645275" y="1457.350205892103" w="168" h="50"/><t id="ce" x="3046.6482584118767" y="1474.1781487403086"><a><text><string>timeframe</string></text></a></t></children></atr><llabel id="cf"><points><p colinear="true" x="2689.1" y="1545.650721340985" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3002.4622954386923" y="1494.9976510478937" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="d0"><Owner><r ref="a"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="d1"><Owner><e ref="cd"/></Owner></ellipseConnector></endConnector></llabel><atr id="d2" nullable="false" attributeType="VARCHAR2(128)"><children><e id="d3" x="2995.6611351787064" y="1593.7926664707713" w="168" h="50"/><t id="d4" x="3065.249033433101" y="1610.620609318977"><a><text><string>open</string></text></a></t></children></atr><llabel id="d5"><points><p colinear="true" x="2689.1" y="1574.7869951594619" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3000.99890549264" y="1610.3732253229944" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="d6"><Owner><r ref="a"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="d7"><Owner><e ref="d3"/></Owner></ellipseConnector></endConnector></llabel><atr id="d8" nullable="false" attributeType="VARCHAR2(128)"><children><e id="d9" x="2961.982480739866" y="1726.057066050806" w="168" h="50"/><t id="da" x="3033.3283974879128" y="1742.8850088990116"><a><text><string>high</string></text></a></t></children></atr><llabel id="db"><points><p colinear="true" x="2673.2961294158786" y="1599.4" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2996.1485074731736" y="1731.0746880909269" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="dc"><Owner><r ref="a"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="dd"><Owner><e ref="d9"/></Owner></ellipseConnector></endConnector></llabel><atr id="de" nullable="false" attributeType="VARCHAR2(128)"><children><e id="df" x="2893.7400394714773" y="1844.25644156019" w="168" h="50"/><t id="e0" x="2967.845965985149" y="1861.0843844083956"><a><text><string>low</string></text></a></t></children></atr><llabel id="e1"><points><p colinear="true" x="2630.5587365861948" y="1599.4" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2947.6573226090923" y="1845.9851639103306" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="e2"><Owner><r ref="a"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="e3"><Owner><e ref="df"/></Owner></ellipseConnector></endConnector></llabel><atr id="e4" nullable="false" attributeType="VARCHAR2(128)"><children><e id="e5" x="2796.035036638292" y="1934.1924511643451" w="168" h="50"/><t id="e6" x="2865.7189279346785" y="1951.0203940125507"><a><text><string>close</string></text></a></t></children></atr><llabel id="e7"><points><p colinear="true" x="2610.9027629103402" y="1599.4" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2861.9286659462064" y="1934.8183191033766" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="e8"><Owner><r ref="a"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="e9"><Owner><e ref="e5"/></Owner></ellipseConnector></endConnector></llabel><atr id="ea" nullable="false" attributeType="VARCHAR2(128)"><children><e id="eb" x="2710.6394760805692" y="2010.1924102495736" w="168" h="50"/><t id="ec" x="2773.7113297182646" y="2027.0203530977792"><a><text><string>volume</string></text></a></t></children></atr><llabel id="ed"><points><p colinear="true" x="2599.9096859271363" y="1599.4" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2783.8472404503373" y="2010.421132657545" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="ee"><Owner><r ref="a"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="ef"><Owner><e ref="eb"/></Owner></ellipseConnector></endConnector></llabel><atr id="f0" nullable="false" attributeType="VARCHAR2(128)"><children><e id="f1" x="2508.639739054654" y="2035.2003932873995" w="168" h="50"/><t id="f2" x="2558.6915044965485" y="2052.028336135605"><a><text><string>candle_time</string></text></a></t></children></atr><llabel id="f3"><points><p colinear="true" x="2584.263483238599" y="1599.4" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2592.6762166427297" y="2035.2007769429708" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="f4"><Owner><r ref="a"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="f5"><Owner><e ref="f1"/></Owner></ellipseConnector></endConnector></llabel><atrchave id="f6" nullable="false" attributeType="NUMBER"><children><e id="f7" x="2435.3003932873994" y="1775.191853220369" w="168" h="50.00000000000023"/><t id="f8" x="2513.672356788376" y="1792.0197960685748"><a><fontUnderlined><boolean>true</boolean></fontUnderlined><fontBold><boolean>true</boolean></fontBold><text><string>id</string></text></a></t></children></atrchave><llabel id="f9"><points><p colinear="true" x="2127.5" y="1834.469945998015" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2438.642251542709" y="1807.7922705758594" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="fa"><Owner><r ref="d"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="fb"><Owner><e ref="f7"/></Owner></ellipseConnector></endConnector></llabel><atr id="fc" nullable="false" attributeType="VARCHAR2(128)"><children><e id="fd" x="2430.658472645275" y="1899.249794107897" w="168" h="50"/><t id="fe" x="2503.1623865246697" y="1916.0777369561026"><a><text><string>side</string></text></a></t></children></atr><llabel id="ff"><points><p colinear="true" x="2127.5" y="1860.9492786590151" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2440.8622954386924" y="1912.6023489521065" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="100"><Owner><r ref="d"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="101"><Owner><e ref="fd"/></Owner></ellipseConnector></endConnector></llabel><atr id="102" nullable="false" attributeType="VARCHAR2(128)"><children><e id="103" x="2395.5478781302922" y="2018.3260985404147" w="168" h="50.00000000000023"/><t id="104" x="2467.2477911551946" y="2035.1540413886203"><a><text><string>type</string></text></a></t></children></atr><llabel id="105"><points><p colinear="true" x="2105.658888661668" y="1880.2" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2431.9793163146783" y="2022.8539994086007" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="106"><Owner><r ref="d"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="107"><Owner><e ref="103"/></Owner></ellipseConnector></endConnector></llabel><atr id="108" nullable="false" attributeType="VARCHAR2(128)"><children><e id="109" x="2332.1400394714774" y="2125.0564415601903" w="168" h="50"/><t id="10a" x="2398.9859180725516" y="2141.884384408396"><a><text><string>status</string></text></a></t></children></atr><llabel id="10b"><points><p colinear="true" x="2068.958736586195" y="1880.2" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2386.0573226090924" y="2126.7851639103305" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="10c"><Owner><r ref="d"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="10d"><Owner><e ref="109"/></Owner></ellipseConnector></endConnector></llabel><atr id="10e" nullable="false" attributeType="VARCHAR2(128)"><children><e id="10f" x="2244.35644156019" y="2206.5439828185263" w="168" h="50"/><t id="110" x="2304.944286408823" y="2223.371925666732"><a><text><string>quantity</string></text></a></t></children></atr><llabel id="111"><points><p colinear="true" x="2050.83120690873" y="1880.2" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2309.2630564043266" y="2207.2389664883754" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="112"><Owner><r ref="d"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="113"><Owner><e ref="10f"/></Owner></ellipseConnector></endConnector></llabel><atr id="114" nullable="false" attributeType="VARCHAR2(128)"><children><e id="115" x="2218.435791376831" y="2282.5439347832435" w="168" h="50"/><t id="116" x="2288.4556888377683" y="2299.371877631449"><a><text><string>price</string></text></a></t></children></atr><llabel id="117"><points><p colinear="true" x="2044.0675654410306" y="1880.2" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2287.7690944556634" y="2282.9580484809276" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="118"><Owner><r ref="d"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="119"><Owner><e ref="115"/></Owner></ellipseConnector></endConnector></llabel><atr id="11a" nullable="false" attributeType="VARCHAR2(128)"><children><e id="11b" x="2016.8892486229179" y="2311.3584726452755" w="168" h="50"/><t id="11c" x="2074.1350677147148" y="2328.186415493481"><a><text><string>placed_at</string></text></a></t></children></atr><llabel id="11d"><points><p colinear="true" x="2027.844733694065" y="1880.2" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2097.3107007103577" y="2311.388193484461" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="11e"><Owner><r ref="d"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="11f"><Owner><e ref="11b"/></Owner></ellipseConnector></endConnector></llabel><atr id="120" nullable="false" attributeType="VARCHAR2(128)"><children><e id="121" x="1815.3427058689322" y="2316.0003932873997" w="168" h="50"/><t id="122" x="1865.3764735691275" y="2332.8283361356052"><a><text><string>executed_at</string></text></a></t></children></atr><llabel id="123"><points><p colinear="true" x="2012.9974106270279" y="1880.2" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1906.1148360588406" y="2316.070737168798" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="124"><Owner><r ref="d"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="125"><Owner><e ref="121"/></Owner></ellipseConnector></endConnector></llabel><atrchave id="126" nullable="false" attributeType="NUMBER"><children><e id="127" x="998.083058356455" y="2160.6299128405326" w="168" h="50"/><t id="128" x="1076.4550218574316" y="2177.457855688738"><a><fontUnderlined><boolean>true</boolean></fontUnderlined><fontBold><boolean>true</boolean></fontBold><text><string>id</string></text></a></t></children></atrchave><llabel id="129"><points><p colinear="true" x="1406.9170741899063" y="2067.4" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1136.516739013257" y="2166.499658457038" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="12a"><Owner><r ref="10"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="12b"><Owner><e ref="127"/></Owner></ellipseConnector></endConnector></llabel><atr id="12c" nullable="false" attributeType="VARCHAR2(128)"><children><e id="12d" x="1120.4853136832526" y="2342.098719045973" w="168" h="50"/><t id="12e" x="1192.185226708155" y="2358.926661894179"><a><text><string>type</string></text></a></t></children></atr><llabel id="12f"><points><p colinear="true" x="1474.3352523831288" y="2067.4" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1227.1422415288407" y="2342.9909576904956" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="130"><Owner><r ref="10"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="131"><Owner><e ref="12d"/></Owner></ellipseConnector></endConnector></llabel><atr id="132" nullable="false" attributeType="VARCHAR2(128)"><children><e id="133" x="1313.7545344307102" y="2444.861786567261" w="168" h="50"/><t id="134" x="1375.566385932175" y="2461.6897294154664"><a><text><string>amount</string></text></a></t></children></atr><llabel id="135"><points><p colinear="true" x="1498.099527896224" y="2067.4" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1404.5944779630238" y="2444.9336619281635" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="136"><Owner><r ref="10"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="137"><Owner><e ref="133"/></Owner></ellipseConnector></endConnector></llabel><atr id="138" nullable="false" attributeType="VARCHAR2(128)"><children><e id="139" x="1532.6454655692896" y="2444.861786567261" w="168" h="50"/><t id="13a" x="1592.0692936942896" y="2461.6897294154664"><a><text><string>currency</string></text></a></t></children></atr><llabel id="13b"><points><p colinear="true" x="1516.3004721037762" y="2067.4" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1610.805522036976" y="2444.9336619281635" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="13c"><Owner><r ref="10"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="13d"><Owner><e ref="139"/></Owner></ellipseConnector></endConnector></llabel><atr id="13e" nullable="false" attributeType="VARCHAR2(128)"><children><e id="13f" x="1725.9146863167473" y="2342.098719045973" w="168" h="50"/><t id="140" x="1780.3704816048332" y="2358.926661894179"><a><text><string>created_at</string></text></a></t></children></atr><llabel id="141"><points><p colinear="true" x="1540.0647476168713" y="2067.4" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1788.2577584711591" y="2342.9909576904956" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="142"><Owner><r ref="10"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="143"><Owner><e ref="13f"/></Owner></ellipseConnector></endConnector></llabel><atr id="144" nullable="false" attributeType="VARCHAR2(128)"><children><e id="145" x="1848.316941643545" y="2160.6299128405326" w="168" h="50"/><t id="146" x="1900.7207120903224" y="2177.457855688738"><a><text><string>description</string></text></a></t></children></atr><llabel id="147"><points><p colinear="true" x="1607.4829258100938" y="2067.4" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1878.8832609867432" y="2166.499658457038" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="148"><Owner><r ref="10"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="149"><Owner><e ref="145"/></Owner></ellipseConnector></endConnector></llabel><atrchave id="14a" nullable="false" attributeType="NUMBER"><children><e id="14b" x="252.258589427763" y="1492.032837799447" w="168" h="50"/><t id="14c" x="330.6305529287396" y="1508.8607806476525"><a><fontUnderlined><boolean>true</boolean></fontUnderlined><fontBold><boolean>true</boolean></fontBold><text><string>id</string></text></a></t></children></atrchave><llabel id="14d"><points><p colinear="true" x="886.9" y="1634.0752827438127" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="405.842078709622" y="1532.2169867493667" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="14e"><Owner><r ref="13"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="14f"><Owner><e ref="14b"/></Owner></ellipseConnector></endConnector></llabel><atr id="150" nullable="false" attributeType="VARCHAR2(128)"><children><e id="151" x="237.91286726673286" y="1651.985234661149" w="168" h="50"/><t id="152" x="293.4006678648774" y="1668.8131775093545"><a><text><string>username</string></text></a></t></children></atr><llabel id="153"><points><p colinear="true" x="886.9" y="1659.723316528001" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="406.4830957511068" y="1674.9166568698076" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="154"><Owner><r ref="13"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="155"><Owner><e ref="151"/></Owner></ellipseConnector></endConnector></llabel><atr id="156" nullable="false" attributeType="VARCHAR2(128)"><children><e id="157" x="261.9966919876556" y="1810.7635026732946" w="168" h="50"/><t id="158" x="330.5405838943939" y="1827.5914455215002"><a><text><string>email</string></text></a></t></children></atr><llabel id="159"><points><p colinear="true" x="886.9" y="1685.7577393983129" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="408.71455432892077" y="1819.0089623671254" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="15a"><Owner><r ref="13"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="15b"><Owner><e ref="157"/></Owner></ellipseConnector></endConnector></llabel><atr id="15c" nullable="false" attributeType="VARCHAR2(128)"><children><e id="15d" x="323.1296784555676" y="1959.2671287961575" w="168" h="50"/><t id="15e" x="379.52948924658324" y="1976.095071644363"><a><text><string>full_name</string></text></a></t></children></atr><llabel id="15f"><points><p colinear="true" x="927.2245603064812" y="1693" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="447.71399180642965" y="1962.318834711968" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="160"><Owner><r ref="13"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="161"><Owner><e ref="15d"/></Owner></ellipseConnector></endConnector></llabel><atr id="162" nullable="false" attributeType="VARCHAR2(128)"><children><e id="163" x="417.8079369538603" y="2088.984499929924" w="167.99999999999994" h="50"/><t id="164" x="458.1696312897978" y="2105.8124427781295"><a><text><string>password_hash</string></text></a></t></children></atr><llabel id="165"><points><p colinear="true" x="953.2585420840991" y="1693" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="528.3249235586993" y="2090.22326742507" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="166"><Owner><r ref="13"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="167"><Owner><e ref="163"/></Owner></ellipseConnector></endConnector></llabel><atr id="168" nullable="false" attributeType="VARCHAR2(128)"><children><e id="169" x="540.6049016380412" y="2190.1523416658747" w="168" h="50"/><t id="16a" x="575.1345723533732" y="2206.9802845140803"><a><text><string>available_balance</string></text></a></t></children></atr><llabel id="16b"><points><p colinear="true" x="968.3698114749144" y="1693" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="641.5712755917845" y="2190.6411916832512" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="16c"><Owner><r ref="13"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="16d"><Owner><e ref="169"/></Owner></ellipseConnector></endConnector></llabel><atr id="16e" nullable="false" attributeType="VARCHAR2(128)"><children><e id="16f" x="640.7223063987228" y="2266.1523239014355" w="168" h="50"/><t id="170" x="676.3139735740158" y="2282.980266749641"><a><text><string>invested_balance</string></text></a></t></children></atr><llabel id="171"><points><p colinear="true" x="977.0053729758911" y="1693" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="735.8913835562713" y="2266.356399833859" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="172"><Owner><r ref="13"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="173"><Owner><e ref="16f"/></Owner></ellipseConnector></endConnector></llabel><atr id="174" nullable="false" attributeType="VARCHAR2(128)"><children><e id="175" x="842.4778410735205" y="2298.9248820427115" w="168" h="50"/><t id="176" x="896.9336363616064" y="2315.752824890917"><a><text><string>created_at</string></text></a></t></children></atr><llabel id="177"><points><p colinear="true" x="988.7948620053658" y="1693" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="929.4953810392356" y="2298.93620202953" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="178"><Owner><r ref="13"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="179"><Owner><e ref="175"/></Owner></ellipseConnector></endConnector></llabel><atr id="17a" nullable="false" attributeType="VARCHAR2(128)"><children><e id="17b" x="1044.2333757483743" y="2295.7718205118454" w="168" h="50"/><t id="17c" x="1096.343162735679" y="2312.599763360051"><a><text><string>updated_at</string></text></a></t></children></atr><llabel id="17d"><points><p colinear="true" x="999.8636889022259" y="1693" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1123.5289174237262" y="2295.8202333224326" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="17e"><Owner><r ref="13"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="17f"><Owner><e ref="17b"/></Owner></ellipseConnector></endConnector></llabel><atr id="180" nullable="false" attributeType="VARCHAR2(128)"><children><e id="181" x="450.70518982309187" y="1482.9692972727066" w="167.99999999999994" h="50"/><t id="182" x="505.1609851111778" y="1499.7972401209122"><a><text><string>created_at</string></text></a></t></children></atr><llabel id="183"><points><p colinear="true" x="732.8424248553455" y="1225" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="552.674734208172" y="1483.5202022804615" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="184"><Owner><r ref="16"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="185"><Owner><e ref="181"/></Owner></ellipseConnector></endConnector></llabel><atr id="186" nullable="false" attributeType="VARCHAR2(128)"><children><e id="187" x="290.3249763252388" y="1095.7772107098972" w="168" h="50"/><t id="188" x="358.25686872025835" y="1112.6051535581028"><a><text><string>name</string></text></a></t></children></atr><llabel id="189"><points><p colinear="true" x="652.9" y="1169.8975035352569" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="447.78360403401086" y="1134.141785250418" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="18a"><Owner><r ref="16"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="18b"><Owner><e ref="187"/></Owner></ellipseConnector></endConnector></llabel><atrchave id="18c" nullable="false" attributeType="NUMBER"><children><e id="18d" x="573.4605724100169" y="786.7889277472634" w="168" h="50"/><t id="18e" x="651.8325359109934" y="803.616870595469"><a><fontUnderlined><boolean>true</boolean></fontUnderlined><fontBold><boolean>true</boolean></fontBold><text><string>id</string></text></a></t></children></atrchave><llabel id="18f"><points><p colinear="true" x="748.619854476264" y="1152" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="664.7710482664023" y="837.705969667015" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="190"><Owner><r ref="16"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="191"><Owner><e ref="18d"/></Owner></ellipseConnector></endConnector></llabel><atr id="192" nullable="false" attributeType="VARCHAR2(128)"><children><e id="193" x="1339.7499074759312" y="1062.1" w="168" h="50"/><t id="194" x="1400.337752324564" y="1078.9279428482055"><a><text><string>quantity</string></text></a></t></children></atr><llabel id="195"><points><p colinear="true" x="1131.9489148679422" y="1255.5713816320224" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1385.107125586402" y="1110.1990956607503" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><diamondConnector id="196"><Owner><diamond ref="2e"/></Owner></diamondConnector></startConnector><endConnector><ellipseConnector id="197"><Owner><e ref="193"/></Owner></ellipseConnector></endConnector></llabel><atrderivado id="198"><children><e id="199" x="1386.0750236747613" y="1189.377210709897" w="168" h="50"><a><strokeDashes><doubleArray><double>5</double></doubleArray></strokeDashes><fillColor><color rgba="#ffffebeb"/></fillColor></a></e><t id="19a" x="1443.3268394706597" y="1206.2051535581027"><a><text><string>avg_price</string></text></a></t></children></atrderivado><llabel id="19b"><points><p colinear="true" x="1159.7317959892898" y="1269.0990950309958" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1397.616395965989" y="1227.741785250418" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><diamondConnector id="19c"><Owner><diamond ref="2e"/></Owner></diamondConnector></startConnector><endConnector><ellipseConnector id="19d"><Owner><e ref="199"/></Owner></ellipseConnector></endConnector></llabel><atr id="19e" nullable="false" attributeType="VARCHAR2(128)"><children><e id="19f" x="1386.0750236747613" y="1324.8227892901027" w="168" h="50"/><t id="1a0" x="1440.5308189628472" y="1341.6507321383083"><a><text><string>created_at</string></text></a></t></children></atr><llabel id="1a1"><points><p colinear="true" x="1159.7317959892898" y="1295.100904969004" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1397.616395965989" y="1337.4582147495819" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><diamondConnector id="1a2"><Owner><diamond ref="2e"/></Owner></diamondConnector></startConnector><endConnector><ellipseConnector id="1a3"><Owner><e ref="19f"/></Owner></ellipseConnector></endConnector></llabel><atr id="1a4" nullable="false" attributeType="VARCHAR2(128)"><children><e id="1a5" x="1339.7499074759312" y="1452.1" w="168" h="50"/><t id="1a6" x="1391.8596944632359" y="1468.9279428482055"><a><text><string>updated_at</string></text></a></t></children></atr><llabel id="1a7"><points><p colinear="true" x="1131.9489148679422" y="1308.6286183679774" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1385.107125586402" y="1455.0009043392495" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><diamondConnector id="1a8"><Owner><diamond ref="2e"/></Owner></diamondConnector></startConnector><endConnector><ellipseConnector id="1a9"><Owner><e ref="1a5"/></Owner></ellipseConnector></endConnector></llabel><atr id="1aa" nullable="false" attributeType="VARCHAR2(128)"><children><e id="1ab" x="618.1457551736057" y="780.492813356838" w="168" h="50"/><t id="1ac" x="676.1295808571995" y="797.3207562050436"><a><text><string>added_at</string></text></a></t></children></atr><llabel id="1ad"><points><p colinear="true" x="910.35088986456" y="992.9615586761499" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="729.4983404514223" y="830.1709897408368" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><diamondConnector id="1ae"><Owner><diamond ref="34"/></Owner></diamondConnector></startConnector><endConnector><ellipseConnector id="1af"><Owner><e ref="1ab"/></Owner></ellipseConnector></endConnector></llabel><atr id="1b0" nullable="false" attributeType="VARCHAR2(128)"><children><e id="1b1" x="1386.0750236747613" y="1589.1" w="168" h="50"/><t id="1b2" x="1419.2786674735894" y="1605.9279428482055"><a><text><string>reserved_quantity</string></text></a></t></children></atr><llabel id="1b3"><points><p colinear="true" x="1122.1878949476747" y="1313.3813392750108" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1442.7237232517468" y="1590.5249334883308" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><diamondConnector id="1b4"><Owner><diamond ref="2e"/></Owner></diamondConnector></startConnector><endConnector><ellipseConnector id="1b5"><Owner><e ref="1b1"/></Owner></ellipseConnector></endConnector></llabel></figures></drawing>
Index: docs/P2-RelationalDesign/RelationalDesign.md
===================================================================
--- docs/P2-RelationalDesign/RelationalDesign.md	(revision df058387ebe7d07864d874fd1376f15512418581)
+++ docs/P2-RelationalDesign/RelationalDesign.md	(revision 9577c7944ff24ddc9145ff6637494f07d87c3dd4)
@@ -11,5 +11,5 @@
 - **Markets**(<u>**id**</u>, *crypto_id*, quote_currency, is_active, created_at)
   - Candidate keys: `{id}`, `{crypto_id, quote_currency}`. `UNIQUE(crypto_id, quote_currency)`.
-- **Holdings**(<u>**id**</u>, *user_id*, *crypto_id*, quantity, avg_price, created_at, updated_at)
+- **Holdings**(<u>**id**</u>, *user_id*, *crypto_id*, quantity, reserved_quantity, avg_price, created_at, updated_at)
   - Transformation of the M:N relationship `Holds`. Candidate keys: `{id}` and
     `{user_id, crypto_id}` — the latter is the relationship's own key and is
@@ -17,4 +17,12 @@
     consistency with the other relations.
   - `avg_price` is `NOT NULL DEFAULT 0 CHECK (avg_price >= 0)`.
+  - `reserved_quantity` is `NOT NULL DEFAULT 0 CHECK (reserved_quantity >= 0
+    AND reserved_quantity <= quantity)` — the amount already committed to the
+    user's own open sell orders. `quantity - reserved_quantity` (the amount
+    actually free to sell) is not a stored column; it is computed wherever
+    needed, in `v_portfolio` as `available_quantity` and in the sell path of
+    [UseCase0005](../P3-UseCaseModel/UseCase0005.md). See
+    [ERModel](../P1-ConceptualModel/ERModel.md#holds--users-m--cryptos-n-partial-on-both-sides-with-attributes)
+    for why this mirrors `available_balance`/`invested_balance` on `Users`.
 - **Orders**(<u>**id**</u>, *user_id*, *market_id*, side, type, status, quantity, price, placed_at, executed_at)
   - `side ∈ {buy, sell}`, `type ∈ {market, limit}`, `status ∈ {open, executed, cancelled}`.
@@ -56,4 +64,14 @@
 ### Normalisation
 
+> **Validated in P5.** [Normalization](../P5-Normalization/Normalization.md) derives this
+> exact schema independently — starting only from a single de-normalized relation of every
+> model attribute and its functional dependencies, with no reference to the ER-to-relational
+> transformation below — and shows it decomposes to **BCNF**, one normal form stronger than
+> the 3NF claimed here. The two designs agree relation for relation and key for key, so
+> nothing here changed as a result; see that page's
+> [discussion](../P5-Normalization/Normalization.md#discussion) for what the one real
+> difference is (`avg_price`, a stored derived value, not a normalisation issue) and why this
+> design is still the one used from P5 onward.
+
 All relations are in **3NF**:
 
@@ -72,4 +90,24 @@
   yields `NULL`, so a nullable average would have silently blanked the
   unrealised-P/L column for an existing position instead of failing loudly.
+- `holdings.reserved_quantity`, unlike `avg_price`, is **not** derived — it is
+  written directly by the application (`trade.go`) as orders are placed and
+  settled, the same way `quantity` itself is. `quantity - reserved_quantity`
+  ("available") is the derived value here, and it is never stored, only
+  computed where it is needed.
+
+### Reservation and the order lifecycle
+
+`holdings.reserved_quantity` exists so that placing a sell order can be
+checked against what a user actually has *free* to sell
+(`quantity - reserved_quantity`), not against the raw `quantity`, which also
+counts crypto already promised to another order that has not settled yet.
+`CHECK (reserved_quantity >= 0 AND reserved_quantity <= quantity)` makes an
+inconsistent reservation impossible at the database level, regardless of what
+application code does. The exact statement sequence — lock the row, check the
+available amount, reserve, then settle — is in
+[UseCase0005](../P3-UseCaseModel/UseCase0005.md); the same
+`SELECT … FOR UPDATE` locking that already protected `users.available_balance`
+on the buy path is what makes two concurrent sell orders against the same
+holding serialize correctly instead of racing.
 
 ## DDL script
@@ -80,5 +118,5 @@
 - 10 tables with check constraints, primary keys, foreign keys and unique constraints.
 - 5 performance indexes.
-- 2 views: `v_latest_prices` (latest trade price per market) and `v_portfolio` (per-user holdings valuation with unrealised P/L).
+- 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`).
 
 ## DML script (sample data)
Index: docs/P2-RelationalDesign/RelationalDesignAIUsage.md
===================================================================
--- docs/P2-RelationalDesign/RelationalDesignAIUsage.md	(revision df058387ebe7d07864d874fd1376f15512418581)
+++ docs/P2-RelationalDesign/RelationalDesignAIUsage.md	(revision 9577c7944ff24ddc9145ff6637494f07d87c3dd4)
@@ -69,2 +69,33 @@
 > against the faculty database. No AI involvement is possible there — it needs a
 > live connection to your assigned database.
+
+### Session 3 — 2026-09-16
+
+Driven by the same design review logged in full in
+[ERModelAIUsage](../P1-ConceptualModel/ERModelAIUsage.md#session-3--2026-09-16):
+a sell order had nothing to check `holdings.quantity` against except itself,
+so nothing stopped two sell orders from being granted the same units.
+
+Changes to the P2 artefacts:
+
+- `holdings` gained `reserved_quantity numeric(20,4) NOT NULL DEFAULT 0
+  CHECK (reserved_quantity >= 0 AND reserved_quantity <= quantity)` in
+  `schema_creation.sql`.
+- `v_portfolio` gained `reserved_quantity` and the derived
+  `available_quantity = quantity - reserved_quantity`.
+- [RelationalDesign](RelationalDesign.md) gained a "Reservation and the order
+  lifecycle" section explaining why the check is enforced at the database
+  level rather than trusted to application code, and why it does not conflict
+  with the existing `SELECT … FOR UPDATE` locking on the sell path.
+- `data_load.sql` needed no change — `reserved_quantity` defaults to 0, which
+  is correct for every seeded holding.
+
+Re-run end to end against the live database on `localhost:5433`
+(`-init` then `-load-data`), and against a manually seeded 2 BTC holding to
+reproduce the exact scenario that motivated the change — see
+[UseCase0005Implementation](../P4-Prototype/UseCase0005Implementation.md) for
+the transcript.
+
+**What I decided:** to add the `CHECK` constraint rather than rely on
+`trade.go` alone to keep the reservation consistent — the same reasoning
+already applied to `avg_price NOT NULL` in session 2.
Index: docs/P3-UseCaseModel/UseCase0004.md
===================================================================
--- docs/P3-UseCaseModel/UseCase0004.md	(revision df058387ebe7d07864d874fd1376f15512418581)
+++ docs/P3-UseCaseModel/UseCase0004.md	(revision 9577c7944ff24ddc9145ff6637494f07d87c3dd4)
@@ -37,13 +37,18 @@
    BEGIN;
 
+   -- (a) record intent — no trade has happened yet.
    INSERT INTO project.orders
-       (user_id, market_id, side, type, status, quantity, price, executed_at)
+       (user_id, market_id, side, type, status, quantity, price)
    VALUES
-       ($user_id, $market_id, 'buy', 'market', 'executed', $qty, $price, now())
+       ($user_id, $market_id, 'buy', 'market', 'open', $qty, $price)
    RETURNING id;  -- captured as $order_id
 
+   -- (b) lock and check the user balance
    SELECT available_balance FROM project.users WHERE id = $user_id FOR UPDATE;
    -- abort if available_balance < notional
 
+   -- (c) move cash from available to invested. A buy never reserves crypto
+   --     the way a sell does — it only ever adds to the position, so there
+   --     is nothing on the holdings side to commit before settling.
    UPDATE project.users
       SET available_balance = available_balance - $notional,
@@ -52,5 +57,5 @@
     WHERE id = $user_id;
 
-   -- Upsert holding with running weighted-average price:
+   -- (d) upsert holding with running weighted-average price:
    SELECT quantity, avg_price
      FROM project.holdings
@@ -61,4 +66,5 @@
    -- new_avg = (old_qty*old_avg + $qty*$price) / (old_qty + $qty)
 
+   -- (e) ledger entry
    INSERT INTO project.transactions
        (user_id, type, amount, currency, related_order, description)
@@ -66,8 +72,14 @@
        ($user_id, 'buy', -$notional, 'USD', $order_id, 'Market buy ...');
 
+   -- (f) record the resulting market trade
    INSERT INTO project.market_trades
        (market_id, executed_at, price, quantity, side, source)
    VALUES
        ($market_id, now(), $price, $qty, 'buy', 'user');
+
+   -- (g) settle the order itself — it has now actually been filled.
+   UPDATE project.orders
+      SET status = 'executed', executed_at = now()
+    WHERE id = $order_id;
 
    COMMIT;
Index: docs/P3-UseCaseModel/UseCase0005.md
===================================================================
--- docs/P3-UseCaseModel/UseCase0005.md	(revision df058387ebe7d07864d874fd1376f15512418581)
+++ docs/P3-UseCaseModel/UseCase0005.md	(revision 9577c7944ff24ddc9145ff6637494f07d87c3dd4)
@@ -6,4 +6,20 @@
 
 A Trader sells part or all of a holding at the current market price. Cost basis is preserved so realised P/L can be reconstructed from the ledger.
+
+## Reserve, then settle
+
+The crypto being sold is **reserved** (`holdings.reserved_quantity`) before it
+is actually removed from the position, so the check a second sell order makes
+is always against what is truly still free (`quantity - reserved_quantity`),
+not against the raw `quantity`, which would also count crypto already
+promised to this order. Because only market orders are implemented, an order
+settles in the same database transaction it is placed in, so reserve and
+settle below are two statements inside one commit rather than two separate
+ones — the existing all-or-nothing guarantee (see
+[PrototypeImplementation](../P4-Prototype/PrototypeImplementation.md)) is
+kept. They stay logically distinct so that a future limit-order matcher —
+where an order really would sit `open` for a while before a *later*
+transaction settles it — needs only a second transaction where today there is
+one, not a schema change.
 
 ## Scenario
@@ -18,22 +34,34 @@
    BEGIN;
 
+   -- (a) record intent — no trade has happened yet.
    INSERT INTO project.orders
-       (user_id, market_id, side, type, status, quantity, price, executed_at)
+       (user_id, market_id, side, type, status, quantity, price)
    VALUES
-       ($user_id, $market_id, 'sell', 'market', 'executed', $qty, $price, now())
+       ($user_id, $market_id, 'sell', 'market', 'open', $qty, $price)
    RETURNING id;   -- $order_id
 
-   SELECT quantity, avg_price
+   -- (b) lock the holding and check what is actually free to sell.
+   SELECT quantity, reserved_quantity, avg_price
      FROM project.holdings
     WHERE user_id = $user_id AND crypto_id = $crypto_id
     FOR UPDATE;
-   -- abort if row missing or quantity < $qty
+   -- available := quantity - reserved_quantity
+   -- abort if row missing or available < $qty
    ```
-6. If the holding check passes, system reduces the holding, credits cash and debits invested, and appends a ledger and a market trade:
+
+6. If the check passes, system reserves the crypto, then — since this is a market order — settles it immediately, all inside the same transaction:
 
    ```sql
+   -- (c) reserve: committed to this order, not yet removed from the position.
    UPDATE project.holdings
-      SET quantity   = quantity - $qty,
-          updated_at = now()
+      SET reserved_quantity = reserved_quantity + $qty,
+          updated_at        = now()
+    WHERE user_id = $user_id AND crypto_id = $crypto_id;
+
+   -- (d) settle: release the reservation and remove the asset in one step.
+   UPDATE project.holdings
+      SET quantity          = quantity - $qty,
+          reserved_quantity = reserved_quantity - $qty,
+          updated_at        = now()
     WHERE user_id = $user_id AND crypto_id = $crypto_id;
 
@@ -54,11 +82,36 @@
        ($market_id, now(), $price, $qty, 'sell', 'user');
 
+   -- (e) settle the order itself — it has now actually been filled.
+   UPDATE project.orders
+      SET status = 'executed', executed_at = now()
+    WHERE id = $order_id;
+
    COMMIT;
    ```
+
 7. System confirms: `Order executed: sell 0.5000 ETH @ 3520.000000 (notional 1760.0000 USD)`.
 
 ### Alternate flow 5a — insufficient holding
 
-If the `SELECT ... FOR UPDATE` returns no row, or the held quantity is smaller than the sell quantity, the entire transaction rolls back and system shows "Insufficient holding: trying to sell X, hold Y."
+If the holding row is missing, or `quantity - reserved_quantity < $qty`, the
+entire transaction rolls back — including the `open` order from step 5, which
+was never committed — and system shows:
+`"Insufficient holding: trying to sell X, available Y (of Z held, W reserved)."`
+
+### Worked example — the case this fixes
+
+Alice holds 2 BTC, `reserved_quantity = 0`, and places `sell 0.5 BTC`:
+
+| | quantity | reserved_quantity | available |
+|---|---|---|---|
+| before | 2.0000 | 0.0000 | 2.0000 |
+| after step (c) — reserved | 2.0000 | 0.5000 | 1.5000 |
+| after step (d) — settled | 1.5000 | 0.0000 | 1.5000 |
+
+If a second sell for more than 1.5 BTC is placed concurrently, its own
+`SELECT … FOR UPDATE` in step 5b blocks until the first transaction commits,
+then sees the reduced `quantity` and correctly reports insufficient holding —
+proven under real concurrency in
+[UseCase0005Implementation](../P4-Prototype/UseCase0005Implementation.md).
 
 ### Realised P/L (post-scenario)
Index: docs/P3-UseCaseModel/UseCase0006.md
===================================================================
--- docs/P3-UseCaseModel/UseCase0006.md	(revision df058387ebe7d07864d874fd1376f15512418581)
+++ docs/P3-UseCaseModel/UseCase0006.md	(revision 9577c7944ff24ddc9145ff6637494f07d87c3dd4)
@@ -17,4 +17,6 @@
    SELECT symbol,
           quantity,
+          COALESCE(reserved_quantity,  0),
+          COALESCE(available_quantity, quantity),
           COALESCE(avg_price,      0),
           COALESCE(current_price,  0),
@@ -26,4 +28,8 @@
     ORDER BY symbol;
    ```
+
+   `reserved_quantity` is the amount committed to the Trader's own open sell
+   orders (see [UseCase0005](UseCase0005.md)); `available_quantity` is what is
+   actually free to sell right now.
 3. System displays the rows and a computed summary:
 
@@ -54,4 +60,6 @@
        c.symbol,
        h.quantity,
+       h.reserved_quantity,
+       (h.quantity - h.reserved_quantity)      AS available_quantity,
        h.avg_price,
        lp.price                                AS current_price,
Index: docs/P3-UseCaseModel/UseCaseModel.md
===================================================================
--- docs/P3-UseCaseModel/UseCaseModel.md	(revision df058387ebe7d07864d874fd1376f15512418581)
+++ docs/P3-UseCaseModel/UseCaseModel.md	(revision 9577c7944ff24ddc9145ff6637494f07d87c3dd4)
@@ -38,5 +38,5 @@
  * [https://github.com/StefanTrsunov/bp/blob/main/docs/P3-UseCaseModel/UseCase0005.md UC0005] –
    '''Place market SELL order''' – Trader sells part or all of a holding at the current market
-   price, which credits cash and preserves the cost basis.
+   price, which reserves the crypto being sold, credits cash and preserves the cost basis.
  * [https://github.com/StefanTrsunov/bp/blob/main/docs/P3-UseCaseModel/UseCase0006.md UC0006] –
    '''View portfolio and transaction history''' – Trader inspects current holdings, unrealised
@@ -88,5 +88,5 @@
 ||[https://github.com/StefanTrsunov/bp/blob/main/docs/P3-UseCaseModel/UseCase0003.md UC0003 – Deposit]||High||Shows a multi-row transaction: `UPDATE users` plus `INSERT INTO transactions`.||
 ||[https://github.com/StefanTrsunov/bp/blob/main/docs/P3-UseCaseModel/UseCase0004.md UC0004 – Buy]||Very high||Core of the exchange: `INSERT orders`, `UPDATE users`, upsert `holdings`, ledger entry, market trade.||
-||[https://github.com/StefanTrsunov/bp/blob/main/docs/P3-UseCaseModel/UseCase0005.md UC0005 – Sell]||Very high||Dual of Buy; demonstrates row-level `FOR UPDATE` locking and cost-basis bookkeeping.||
+||[https://github.com/StefanTrsunov/bp/blob/main/docs/P3-UseCaseModel/UseCase0005.md UC0005 – Sell]||Very high||Dual of Buy; demonstrates row-level `FOR UPDATE` locking, reservation of committed crypto (`holdings.reserved_quantity`) and cost-basis bookkeeping.||
 ||[https://github.com/StefanTrsunov/bp/blob/main/docs/P3-UseCaseModel/UseCase0006.md UC0006 – Portfolio]||High||Demonstrates joins over `holdings`, `markets` and `crypto`, and the `v_portfolio` view.||
 ||[https://github.com/StefanTrsunov/bp/blob/main/docs/P3-UseCaseModel/UseCase0007.md UC0007 – Watchlist]||Medium||Demonstrates N–M relation handling and `ON CONFLICT` upsert semantics.||
@@ -104,10 +104,15 @@
    kept in one place. Direct links:
    [https://github.com/StefanTrsunov/bp/blob/main/docs/P1-ConceptualModel/ERModelAIUsage.md#session-1--2026-04-21 Session 1 – 2026-04-21],
-   [https://github.com/StefanTrsunov/bp/blob/main/docs/P1-ConceptualModel/ERModelAIUsage.md#session-2--2026-08-06--2026-08-07 Session 2 – 2026-08-06/07].
+   [https://github.com/StefanTrsunov/bp/blob/main/docs/P1-ConceptualModel/ERModelAIUsage.md#session-2--2026-08-06--2026-08-07 Session 2 – 2026-08-06/07],
+   [https://github.com/StefanTrsunov/bp/blob/main/docs/P1-ConceptualModel/ERModelAIUsage.md#session-3--2026-09-16 Session 3 – 2026-09-16].
 
 '''Service:''' Claude Code (Anthropic), https://claude.com/claude-code – Claude subscription,
-model Claude Opus 4.7 (1M context).
+model Claude Opus 4.7 (1M context) in sessions 1–2, Claude Sonnet 5 in session 3.
 
 '''In short:''' the AI proposed the actor taxonomy and drafted the seven use cases with their SQL
 in session 1. In session 2 the use-case model itself was '''not''' changed – the only work was
 re-executing every scenario, including the failure paths, against a live PostgreSQL 16 database.
+In session 3, UC0004 and UC0005 were revised to reserve the resource an order commits (crypto on
+a sell) before settling it, closing a gap where nothing stopped a second sell order from being
+granted crypto already promised to a first one; see
+[UseCaseModelAIUsage](UseCaseModelAIUsage.md#session-3--2026-09-16).
Index: docs/P3-UseCaseModel/UseCaseModelAIUsage.md
===================================================================
--- docs/P3-UseCaseModel/UseCaseModelAIUsage.md	(revision df058387ebe7d07864d874fd1376f15512418581)
+++ docs/P3-UseCaseModel/UseCaseModelAIUsage.md	(revision 9577c7944ff24ddc9145ff6637494f07d87c3dd4)
@@ -55,2 +55,40 @@
 duplicate registration, wrong password). The results are documented per use case
 on the `UseCaseXXXXImplementation` pages.
+
+### Session 3 — 2026-09-16
+
+Driven by the design review logged in full in
+[ERModelAIUsage](../P1-ConceptualModel/ERModelAIUsage.md#session-3--2026-09-16):
+placing a sell order checked `holdings.quantity` directly, with no way to
+record that part of a position was already promised to another, unsettled
+order.
+
+**What changed:**
+
+- [UseCase0005](UseCase0005.md) — the scenario now reserves the crypto
+  (`holdings.reserved_quantity`) before removing it from the position, checks
+  `quantity - reserved_quantity` rather than raw `quantity`, and adds a
+  worked example and a note on why the reserve and settle steps stay inside
+  one transaction rather than two (only market orders are implemented, and
+  splitting into two commits would risk an order stuck `open` with no cancel
+  use case to recover it).
+- [UseCase0004](UseCase0004.md) — no change to the balance logic, but the
+  order insert now goes through `status='open'` before a final
+  `UPDATE ... SET status='executed'`, matching the sell side, so `Orders`
+  genuinely has the lifecycle [ERModel](../P1-ConceptualModel/ERModel.md)
+  describes for it rather than a status column that is only ever written
+  once.
+- [UseCase0006](UseCase0006.md) — the `v_portfolio` reference and its query
+  gained `reserved_quantity`/`available_quantity`, since the portfolio screen
+  is where a Trader would actually see the new field.
+- The use-case importance table and UC0005's one-line description in
+  [UseCaseModel](UseCaseModel.md) were reworded to mention the reservation.
+
+Every changed scenario's SQL was re-run against the live database, including a
+two-concurrent-sells test that reproduces the exact bug being fixed: see
+[UseCase0005Implementation](../P4-Prototype/UseCase0005Implementation.md).
+
+**What I decided:** to keep this a revision of the existing UC0004/UC0005
+pages rather than a new use case (e.g. "cancel order") — `cancelled` remains
+an unused status, same as before, since nothing in the prototype produces it
+and inventing a cancel flow was not what the review asked for.
Index: docs/P4-Prototype/BuildInstructions.md
===================================================================
--- docs/P4-Prototype/BuildInstructions.md	(revision df058387ebe7d07864d874fd1376f15512418581)
+++ docs/P4-Prototype/BuildInstructions.md	(revision 9577c7944ff24ddc9145ff6637494f07d87c3dd4)
@@ -106,4 +106,21 @@
 most recent trade (`v_latest_prices`), never from a stored column.
 
+### 7. Optional — richer data for the P6 reports
+
+`data_load.sql` only seeds a few minutes of trade history, which is not enough
+for the [top traders](../P6-AdvancedReports/AdvancedReports.md#top-traders-by-realized-performance)
+or [market performance](../P6-AdvancedReports/AdvancedReports.md#market-performance-leaderboard)
+reports (menu `[10]`/`[11]`) to show more than a single period. To see them do
+something more interesting, load five quarters of synthetic history on top:
+
+```sh
+psql "postgresql://$DBUSER:$DBPASSWORD@$DBHOST:$DBPORT/$DBNAME" \
+  -f server/db/reports_demo_data.sql
+```
+
+It is deliberately not part of `-init`/`-load-data` — see the header of
+[`reports_demo_data.sql`](../../server/db/reports_demo_data.sql) for why — so
+running it never changes the balances the smoke test below checks.
+
 ## Testing instructions
 
@@ -114,5 +131,6 @@
 funds**, **Browse markets**, **Place market BUY order**, **Place market SELL
 order**, **View portfolio**, **View transaction history**, **Manage watchlist**,
-**Logout**.
+**Logout**, and two [P6](../P6-AdvancedReports/AdvancedReports.md) reports:
+**Report: top traders** and **Report: market performance**.
 
 You never have to remember an identifier. Markets are always printed as a
@@ -122,12 +140,12 @@
 ### End-to-end smoke test
 
-Verified on 2026-08-07 against PostgreSQL 16 with freshly loaded sample data.
+Verified on 2026-09-16 against PostgreSQL 16 with freshly loaded sample data.
 Expected values are exact.
 
 1. `./eduberza -init` — prints `Database initialised.`
 2. `./eduberza`, then `[2] Login` → `alice` / `test123` → `Login successful.`
-3. `[6] View portfolio` → one row: `ETH 0.5000` at avg 3500.000000, current
-   3520.000000, value 1760.0000, unrealised P/L `+10.0000`. Cash available
-   8250.0000, net worth 10010.0000.
+3. `[6] View portfolio` → one row: `ETH 0.5000` reserved 0.0000, available
+   0.5000, at avg 3500.000000, current 3520.000000, value 1760.0000,
+   unrealised P/L `+10.0000`. Cash available 8250.0000, net worth 10010.0000.
 4. `[4] Place market BUY order` → `BTC` → `0.01` →
    `Order executed: buy 0.0100 BTC @ 67140.000000 (notional 671.4000 USD)`.
@@ -150,5 +168,14 @@
   to any table — no order row, no ledger entry, no holding.
 - **Insufficient holding:** as `bob` (no positions), try to sell `1` ETH.
-  Expect `Insufficient holding: trying to sell 1.0000, hold 0.0000`.
+  Expect `Insufficient holding: trying to sell 1.0000, available 0.0000 (of
+  0.0000 held, 0.0000 reserved)`.
+- **Two sell orders racing for the same crypto:** give `alice` a 2 BTC holding
+  and start two `eduberza` processes at once, each selling `1.5` BTC (together
+  3 BTC, more than she has). Expect exactly one `Order executed`, and the
+  other `Insufficient holding` reading the post-commit quantity — see
+  [UseCase0005Implementation](UseCase0005Implementation.md) for the exact
+  transcript. This is the concurrency guarantee that
+  `holdings.reserved_quantity` and the `SELECT ... FOR UPDATE` lock together
+  provide.
 - **Duplicate registration:** register with username `alice`. Expect
   `Username or email already taken.`
Index: docs/P4-Prototype/PrototypeImplementation.md
===================================================================
--- docs/P4-Prototype/PrototypeImplementation.md	(revision df058387ebe7d07864d874fd1376f15512418581)
+++ docs/P4-Prototype/PrototypeImplementation.md	(revision 9577c7944ff24ddc9145ff6637494f07d87c3dd4)
@@ -46,5 +46,13 @@
    `INSERT … ON CONFLICT DO UPDATE` upsert possible, so the weighted-average entry price is
    recomputed by the database in one statement instead of by a read-modify-write in application
-   code.
+   code. `CHECK (reserved_quantity >= 0 AND reserved_quantity <= quantity)` is the same idea
+   applied to the sell path: an inconsistent reservation is impossible at the database level, not
+   just something `trade.go` is careful about.
+ * '''Selling reserves before it removes.''' A sell order locks the holding row, reserves the
+   quantity being sold, then settles by removing it — see
+   [UseCase0005Implementation](UseCase0005Implementation.md). Two sell orders placed at the same
+   instant for more than the available quantity are serialised correctly by `SELECT ... FOR
+   UPDATE`, not just by luck of everything happening in one CLI process; this is demonstrated
+   there with two concurrent processes.
  * '''No identifiers are ever typed.''' Markets are listed with their prices before any choice is
    made, and everything else is selected by symbol.
@@ -63,24 +71,9 @@
  * There is no connection pooling configuration and no explicit isolation level; both are P8
    topics.
-== AI usage ==
-
-AI was used in this phase and is logged in full, per the course rule for P1 onward.
-
- * '''Phase log:'''
-   [https://github.com/StefanTrsunov/bp/blob/main/docs/P3-UseCaseModel/UseCaseModelAIUsage.md UseCaseModelAIUsage.md]
-   – service used, what the AI produced, and what I decided myself.
- * '''Full conversation transcript:'''
-   [https://github.com/StefanTrsunov/bp/blob/main/docs/P1-ConceptualModel/ERModelAIUsage.md ERModelAIUsage.md]
-   – the same conversation produced the P1–P4 artefacts, so the complete prompt/response log is
-   kept in one place. Direct links:
-   [https://github.com/StefanTrsunov/bp/blob/main/docs/P1-ConceptualModel/ERModelAIUsage.md#session-1--2026-04-21 Session 1 – 2026-04-21],
-   [https://github.com/StefanTrsunov/bp/blob/main/docs/P1-ConceptualModel/ERModelAIUsage.md#session-2--2026-08-06--2026-08-07 Session 2 – 2026-08-06/07].
-
-'''Service:''' Claude Code (Anthropic), https://claude.com/claude-code – Claude subscription,
-model Claude Opus 4.7 (1M context).
-
-'''In short:''' the AI proposed the actor taxonomy and drafted the seven use cases with their SQL
-in session 1. In session 2 the use-case model itself was '''not''' changed – the only work was
-re-executing every scenario, including the failure paths, against a live PostgreSQL 16 database.
+ * Reservation only ever lives inside one transaction, because only market orders (which settle
+   immediately) exist. A real limit-order matcher would leave `holdings.reserved_quantity` set
+   and `orders.status = 'open'` between two separate commits, and would need a way to cancel an
+   order to release the reservation — neither is implemented, since nothing in the prototype
+   produces an order that stays open.
 
 == AI usage ==
@@ -96,8 +89,9 @@
    kept in one place. Direct links:
    [https://github.com/StefanTrsunov/bp/blob/main/docs/P1-ConceptualModel/ERModelAIUsage.md#session-1--2026-04-21 Session 1 – 2026-04-21],
-   [https://github.com/StefanTrsunov/bp/blob/main/docs/P1-ConceptualModel/ERModelAIUsage.md#session-2--2026-08-06--2026-08-07 Session 2 – 2026-08-06/07].
+   [https://github.com/StefanTrsunov/bp/blob/main/docs/P1-ConceptualModel/ERModelAIUsage.md#session-2--2026-08-06--2026-08-07 Session 2 – 2026-08-06/07],
+   [https://github.com/StefanTrsunov/bp/blob/main/docs/P1-ConceptualModel/ERModelAIUsage.md#session-3--2026-09-16 Session 3 – 2026-09-16].
 
 '''Service:''' Claude Code (Anthropic), https://claude.com/claude-code – Claude subscription,
-model Claude Opus 4.7 (1M context).
+model Claude Opus 4.7 (1M context) in sessions 1–2, Claude Sonnet 5 in session 3.
 
 '''In short:''' session 1 rewrote the existing Chi/HTTP backend as the CLI prototype covering
@@ -106,3 +100,6 @@
 infinite loop at end of input, and an error check in the wrong order that misreported database
 failures as "Insufficient holding" – and replaced the read-modify-write holding update with a
-single `INSERT … ON CONFLICT DO UPDATE`.
+single `INSERT … ON CONFLICT DO UPDATE`. Session 3 added `holdings.reserved_quantity` and changed
+`trade.go`'s sell path to reserve crypto before removing it, closing a gap where two sell orders
+could be granted the same units; see
+[PrototypeImplementationAIUsage](PrototypeImplementationAIUsage.md#session-3--2026-09-16).
Index: docs/P4-Prototype/PrototypeImplementationAIUsage.md
===================================================================
--- docs/P4-Prototype/PrototypeImplementationAIUsage.md	(revision df058387ebe7d07864d874fd1376f15512418581)
+++ docs/P4-Prototype/PrototypeImplementationAIUsage.md	(revision 9577c7944ff24ddc9145ff6637494f07d87c3dd4)
@@ -39,9 +39,9 @@
 ## Summary of AI involvement
 
-| | Session 1 — 2026-04-21 | Session 2 — 2026-08-06/07 |
-|---|---|---|
-| **What I brought** | My existing Go backend (Chi HTTP handlers) and a half-finished frontend | The CLI prototype as it stood after session 1 |
-| **What the AI did** | Rewrote the backend as a CLI covering UC0001–UC0007, wrote the market bot | Reviewed the code, found and fixed three bugs, improved the holding upsert |
-| **What I decided** | To delete the frontend, to build a CLI rather than a web app, to keep the market simulator | To ask for a code review pass rather than documentation alone |
+| | Session 1 — 2026-04-21 | Session 2 — 2026-08-06/07 | Session 3 — 2026-09-16 |
+|---|---|---|---|
+| **What I brought** | My existing Go backend (Chi HTTP handlers) and a half-finished frontend | The CLI prototype as it stood after session 1 | A design review: the sell path had no way to reserve crypto committed to an order |
+| **What the AI did** | Rewrote the backend as a CLI covering UC0001–UC0007, wrote the market bot | Reviewed the code, found and fixed three bugs, improved the holding upsert | Added `holdings.reserved_quantity`, changed the sell path to reserve-then-settle, gave `Orders.status` a real lifecycle, tested it live including under real concurrency |
+| **What I decided** | To delete the frontend, to build a CLI rather than a web app, to keep the market simulator | To ask for a code review pass rather than documentation alone | To keep reserve+settle in one transaction rather than split it across two, since there is no cancel-order use case to recover a stuck reservation |
 
 The prototype was built in session 1 and worked. What session 2 added was a
@@ -116,2 +116,91 @@
 > presentation. You will be asked how the buy transaction works, and the answer
 > has to be yours.
+
+### Session 3 — 2026-09-16
+
+Prompted by a design review I did myself, logged in full in
+[ERModelAIUsage](../P1-ConceptualModel/ERModelAIUsage.md#session-3--2026-09-16).
+The report: a user who owns 2 BTC and places a sell order for 0.5 BTC has that
+crypto immediately removed from `quantity`, but nothing in the model recorded
+that a *pending* order had already committed part of a position before it
+settled — `holdings` had `quantity` and `avg_price` only, no equivalent of the
+`available_balance`/`invested_balance` split already used for cash.
+
+**Bug fixed**
+
+`server/trade.go`'s sell path checked `held < qty` directly against
+`holdings.quantity`. This happened to be safe against concurrent double-sells
+only because the whole operation — order, holding check, holding update,
+balance update, ledger, trade — runs inside one transaction with a
+`SELECT ... FOR UPDATE` lock. It was not safe against the actual scenario
+described: nothing distinguished "owned" from "owned, but already promised to
+this order," which matters the moment an order can legitimately sit `open`
+across more than one transaction — exactly what a limit-order matcher would
+need, and what `orders.status` already implied was coming.
+
+**What changed**
+
+1. `holdings.reserved_quantity numeric(20,4) NOT NULL DEFAULT 0 CHECK
+   (reserved_quantity >= 0 AND reserved_quantity <= quantity)` added to
+   `schema_creation.sql`.
+2. `v_portfolio` gained `reserved_quantity` and derived `available_quantity`.
+3. `trade.go`'s sell path now: locks the holding, computes
+   `available := quantity - reserved_quantity`, rejects if `available < qty`
+   (previously: rejects if `quantity < qty`), reserves
+   (`reserved_quantity += qty`), then settles (`quantity -= qty;
+   reserved_quantity -= qty`) — two statements instead of one, kept inside the
+   same transaction rather than split into two commits, which would risk an
+   order stuck `open` with reserved crypto and no cancel command to free it.
+4. Both `PlaceOrder` branches now insert the order as `status='open'` and
+   `UPDATE ... SET status='executed', executed_at=now()` at the end, instead
+   of inserting `'executed'` directly — `Orders.status` is now a real
+   lifecycle rather than a label written once.
+5. `portfolio.go` gained `Reserved`/`Available` columns, reading
+   `v_portfolio.reserved_quantity`/`available_quantity`, so the new field is
+   something a Trader can actually see.
+6. The error message on the sell path changed from
+   `"Insufficient holding: trying to sell X, hold Y"` to
+   `"Insufficient holding: trying to sell X, available Y (of Z held, W
+   reserved)"`, since "how much you hold" is no longer the only number that
+   matters.
+
+**Test evidence**
+
+Verified against PostgreSQL 16 (`bp_database`, `localhost:5433`):
+
+```
+$ eduberza sell 0.5 BTC  (Alice: 2.0000 BTC held, 0.0000 reserved)
+Order executed: sell 0.5000 BTC @ 67140.000000 (notional 33570.0000 USD)
+# holdings.quantity: 2.0000 -> 1.5000, reserved_quantity: 0.0000 (unchanged net of reserve+release)
+
+$ eduberza sell 1 ETH   (Bob: no holdings row at all)
+Insufficient holding: trying to sell 1.0000, available 0.0000 (of 0.0000 held, 0.0000 reserved)
+
+# Two concurrent processes, Alice at 1.5 BTC / 0 reserved, each selling 1.0 BTC:
+=== process A === Insufficient holding: trying to sell 1.0000, available 0.5000 (of 0.5000 held, 0.0000 reserved)
+=== process B === Order executed: sell 1.0000 BTC @ 67140.000000 (notional 67140.0000 USD)
+# final holding: quantity 0.5000, reserved_quantity 0.0000 — exactly one sell went through
+
+# Reserve visible mid-transaction, in one psql session (BEGIN; ...; COMMIT;):
+before:            quantity 2.0000, reserved_quantity 0.0000, available 2.0000
+after reserve:     quantity 2.0000, reserved_quantity 0.5000, available 1.5000
+after settle:      quantity 1.5000, reserved_quantity 0.0000, available 1.5000
+
+# The CHECK constraint holds even without going through trade.go:
+UPDATE holdings SET reserved_quantity = quantity + 1 WHERE ...;
+ERROR:  new row for relation "holdings" violates check constraint "holdings_check"
+```
+
+Full transcripts are on
+[UseCase0005Implementation](UseCase0005Implementation.md). The rejected-order
+rollback guarantee from session 2 was re-checked too: after both the
+insufficient-funds and insufficient-holding failure paths, the affected user
+still has zero new rows in `orders`.
+
+**What I decided:** to keep reserve and settle inside a single transaction —
+splitting them into two, so an order genuinely sits `open` and reserved
+between two commits, is what a real limit-order matcher will eventually need,
+but building that now would add a way for an order to get stuck without also
+building a way to cancel it, which is out of scope for this review. `go build
+./...` was run after every change; all seven use cases were re-exercised
+manually.
Index: docs/P4-Prototype/UseCase0004Implementation.md
===================================================================
--- docs/P4-Prototype/UseCase0004Implementation.md	(revision df058387ebe7d07864d874fd1376f15512418581)
+++ docs/P4-Prototype/UseCase0004Implementation.md	(revision 9577c7944ff24ddc9145ff6637494f07d87c3dd4)
@@ -27,9 +27,9 @@
    BEGIN;
 
-   -- (a) record the order
+   -- (a) record the order as 'open' — no trade has happened yet
    INSERT INTO orders
-       (user_id, market_id, side, type, status, quantity, price, executed_at)
+       (user_id, market_id, side, type, status, quantity, price)
    VALUES
-       ($1, $2, 'buy', 'market', 'executed', $3, $4, now())
+       ($1, $2, 'buy', 'market', 'open', $3, $4)
    RETURNING id;
 
@@ -37,5 +37,8 @@
    SELECT available_balance FROM users WHERE id = $1 FOR UPDATE;
 
-   -- (c) move cash from available to invested
+   -- (c) move cash from available to invested. A buy never reserves crypto
+   --     the way a sell does (see UC0005) — it only ever adds to the
+   --     position, so there is nothing to commit on the holdings side
+   --     before settling.
    UPDATE users
       SET available_balance = available_balance - $notional,
@@ -47,4 +50,5 @@
    --     in one statement. Every SET expression sees the pre-update row, so
    --     holdings.quantity below is still the old quantity.
+   --     reserved_quantity is untouched by a buy and defaults to 0.
    INSERT INTO holdings (user_id, crypto_id, quantity, avg_price, updated_at)
    VALUES ($1, $c, $3, $4, now())
@@ -68,4 +72,7 @@
        ($2, now(), $4, $3, 'buy', 'user');
 
+   -- (g) settle the order itself — it has now actually been filled
+   UPDATE orders SET status = 'executed', executed_at = now() WHERE id = $orderId;
+
    COMMIT;
    ```
@@ -77,13 +84,13 @@
 ## Verified run (from actual prototype execution)
 
-With seed data loaded:
+Re-run 2026-09-16 against PostgreSQL 16 (`bp_database` on `localhost:5433`) with freshly loaded seed data:
 
-- **Before:** alice.available_balance = 8250.00, portfolio = { ETH: 0.5 }.
+- **Before:** alice.available_balance = 8250.00, portfolio = { ETH: 0.5000, reserved 0.0000 }.
 - **Command:** `buy 0.01 BTC`.
-- **After:** alice.available_balance = 7578.60 (= 8250 − 671.40), portfolio = { BTC: 0.01 @ 67140, ETH: 0.5 @ 3500 }, net worth = 10010.00 USD (the +10 is the ETH unrealised P/L from the price moving from 3500 → 3520).
+- **After:** alice.available_balance = 7578.60 (= 8250 − 671.40), portfolio = { BTC: 0.0100 @ 67140 (reserved 0.0000), ETH: 0.5000 @ 3500 (reserved 0.0000) }, net worth = 10010.00 USD (the +10 is the ETH unrealised P/L from the price moving from 3500 → 3520). A buy never sets `reserved_quantity`, so it reads 0 on every row here.
 
 ## Failure path — insufficient funds
 
-If `available_balance < notional`, the `defer tx.Rollback()` in `server/trade.go` reverts all six statements and the user sees:
+If `available_balance < notional`, the `defer tx.Rollback()` in `server/trade.go` reverts every statement above and the user sees:
 
 ```
Index: docs/P4-Prototype/UseCase0005Implementation.md
===================================================================
--- docs/P4-Prototype/UseCase0005Implementation.md	(revision df058387ebe7d07864d874fd1376f15512418581)
+++ docs/P4-Prototype/UseCase0005Implementation.md	(revision 9577c7944ff24ddc9145ff6637494f07d87c3dd4)
@@ -2,4 +2,13 @@
 
 **Initiating actor:** Trader. **Source file:** `server/trade.go`, function `PlaceOrder(s, "sell")`.
+
+## The bug this closes
+
+Before this change, `holdings` had `quantity` and `avg_price` only. The sell
+path checked `held < qty` straight against `quantity`, which cannot tell
+"owned" apart from "owned, but already committed to another order that has
+not settled." `holdings.reserved_quantity` fixes that: the crypto being sold
+is reserved before it is removed from the position, and the check is against
+`quantity - reserved_quantity`.
 
 ## Scenario (implemented)
@@ -13,16 +22,28 @@
    BEGIN;
 
+   -- (a) record the order as 'open' — no trade has happened yet
    INSERT INTO orders
-       (user_id, market_id, side, type, status, quantity, price, executed_at)
+       (user_id, market_id, side, type, status, quantity, price)
    VALUES
-       ($1, $2, 'sell', 'market', 'executed', $3, $4, now())
+       ($1, $2, 'sell', 'market', 'open', $3, $4)
    RETURNING id;
 
-   SELECT quantity, avg_price FROM holdings
+   -- (b) lock the holding and check what is actually free to sell
+   SELECT quantity, reserved_quantity, avg_price FROM holdings
     WHERE user_id = $1 AND crypto_id = $c FOR UPDATE;
-   -- abort if missing or insufficient
-
+   -- available := quantity - reserved_quantity
+   -- abort if missing or available < $qty
+
+   -- (c) reserve: committed to this order, not yet removed from the position
    UPDATE holdings
-      SET quantity = quantity - $qty, updated_at = now()
+      SET reserved_quantity = reserved_quantity + $qty, updated_at = now()
+    WHERE user_id = $1 AND crypto_id = $c;
+
+   -- (d) settle: a market order fills immediately, so release the
+   --     reservation and remove the asset in the same step
+   UPDATE holdings
+      SET quantity = quantity - $qty,
+          reserved_quantity = reserved_quantity - $qty,
+          updated_at = now()
     WHERE user_id = $1 AND crypto_id = $c;
 
@@ -43,4 +64,7 @@
        ($2, now(), $price, $qty, 'sell', 'user');
 
+   -- (e) settle the order itself — it has now actually been filled
+   UPDATE orders SET status = 'executed', executed_at = now() WHERE id = $orderId;
+
    COMMIT;
    ```
@@ -52,7 +76,139 @@
 ## Failure path — insufficient holding
 
-If the holding does not exist or `quantity < requested`, the `defer tx.Rollback()` in `server/trade.go` reverts every statement above and the user sees:
-
-```
-Insufficient holding: trying to sell X, hold Y
-```
+If the holding does not exist, or `quantity - reserved_quantity < requested`, the `defer tx.Rollback()` in `server/trade.go` reverts every statement above — including the `open` order, which was never committed — and the user sees:
+
+```
+Insufficient holding: trying to sell X, available Y (of Z held, W reserved)
+```
+
+## Verified run — the exact scenario from the design review
+
+Run 2026-09-16 against PostgreSQL 16 (`bp_database` on `localhost:5433`).
+Alice's ETH/BTC holdings were seeded, then her BTC holding was set to exactly
+the scenario that motivated this fix: 2 BTC owned, nothing reserved.
+
+```
+$ psql ... -c "SELECT symbol, quantity, reserved_quantity, avg_price
+               FROM holdings h JOIN crypto c ON c.id = h.crypto_id
+               WHERE user_id = '<alice>';"
+
+ symbol | quantity | reserved_quantity |  avg_price
+--------+----------+--------------------+-------------
+ BTC    |   2.0000 |             0.0000 | 65000.000000
+ ETH    |   0.5000 |             0.0000 |  3500.000000
+```
+
+**Step 1 — portfolio before the sell** (`[6] View portfolio`):
+
+```
+  Symbol        Quantity      Reserved     Available         Avg buy         Current           Value  Unrealised P/L
+  ------------------------------------------------------------------------------------------------------------------
+  BTC             2.0000        0.0000        2.0000    65000.000000    67140.000000     134280.0000      +4280.0000
+  ETH             0.5000        0.0000        0.5000     3500.000000     3520.000000       1760.0000        +10.0000
+  ------------------------------------------------------------------------------------------------------------------
+  TOTAL                                                                                  136040.0000      +4290.0000
+```
+
+**Step 2 — `[5] Place market SELL order` → `BTC` → `0.5`:**
+
+```
+Order executed: sell 0.5000 BTC @ 67140.000000 (notional 33570.0000 USD)
+```
+
+**Step 3 — portfolio after the sell:**
+
+```
+  BTC             1.5000        0.0000        1.5000    65000.000000    67140.000000     100710.0000      +3210.0000
+```
+
+`quantity` dropped from 2.0 to 1.5 and `reserved_quantity` is back to 0.0000
+— reserve and settle both happened, inside the one commit, exactly as
+designed.
+
+## Verified run — reserve and settle as two distinct, observable steps
+
+The CLI settles a market order in the same transaction it reserves in, so
+`reserved_quantity` is never visibly nonzero *outside* a transaction. Run by
+hand in one `psql` session (one transaction, so the session sees its own
+uncommitted writes) to show the intermediate state that step (c) alone would
+leave, before step (d) runs:
+
+```sql
+BEGIN;
+
+-- before: Alice owns 2 BTC, none reserved
+SELECT quantity, reserved_quantity, quantity - reserved_quantity AS available
+  FROM holdings WHERE user_id = '<alice>' AND crypto_id = '<btc>';
+--  quantity | reserved_quantity | available
+-- ----------+--------------------+-----------
+--    2.0000 |             0.0000 |    2.0000
+
+-- step (c): order placed, 0.5 BTC reserved — no trade has happened yet
+UPDATE holdings SET reserved_quantity = reserved_quantity + 0.5, updated_at = now()
+ WHERE user_id = '<alice>' AND crypto_id = '<btc>';
+
+SELECT quantity, reserved_quantity, quantity - reserved_quantity AS available
+  FROM holdings WHERE user_id = '<alice>' AND crypto_id = '<btc>';
+--  quantity | reserved_quantity | available
+-- ----------+--------------------+-----------
+--    2.0000 |             0.5000 |    1.5000
+
+-- step (d): market order settles immediately, reservation released
+UPDATE holdings SET quantity = quantity - 0.5, reserved_quantity = reserved_quantity - 0.5, updated_at = now()
+ WHERE user_id = '<alice>' AND crypto_id = '<btc>';
+
+SELECT quantity, reserved_quantity, quantity - reserved_quantity AS available
+  FROM holdings WHERE user_id = '<alice>' AND crypto_id = '<btc>';
+--  quantity | reserved_quantity | available
+-- ----------+--------------------+-----------
+--    1.5000 |             0.0000 |    1.5000
+
+COMMIT;
+```
+
+This is the row that would stay visible to every other connection for as long
+as the order stayed `open` — i.e. for as long as it took a matcher to fill
+it, once limit orders exist.
+
+## Verified run — two concurrent sells, which is the bug itself
+
+The scenario the design review described: a user should not be able to place
+two sell orders whose combined quantity exceeds what they actually hold. With
+Alice's BTC holding at 1.5 BTC (0 reserved), two independent CLI processes
+were started at the same instant, each selling `1.0 BTC` — together 2.0 BTC,
+more than she has:
+
+```
+$ ( eduberza-sell-1.0-BTC ) &   # process A
+$ ( eduberza-sell-1.0-BTC ) &   # process B
+$ wait
+
+=== A ===
+Insufficient holding: trying to sell 1.0000, available 0.5000 (of 0.5000 held, 0.0000 reserved)
+=== B ===
+Order executed: sell 1.0000 BTC @ 67140.000000 (notional 67140.0000 USD)
+
+=== final holding ===
+ quantity | reserved_quantity
+----------+--------------------
+   0.5000 |             0.0000
+```
+
+One order settled, one was correctly rejected, and the final `quantity`
+(0.5) is consistent with exactly one 1.0 BTC sell having happened against the
+1.5 BTC available — not both, and not neither. This is enforced by the
+`SELECT ... FOR UPDATE` lock on the holdings row: whichever transaction gets
+there second blocks until the first commits, then re-reads the now-current
+`quantity`/`reserved_quantity` before deciding.
+
+## Verified — the constraint holds even if application code did not
+
+```sql
+UPDATE holdings SET reserved_quantity = quantity + 1 WHERE user_id = '<alice>' AND crypto_id = '<btc>';
+
+ERROR:  new row for relation "holdings" violates check constraint "holdings_check"
+```
+
+`CHECK (reserved_quantity >= 0 AND reserved_quantity <= quantity)` in
+`schema_creation.sql` makes an inconsistent reservation impossible at the
+database level, independent of `trade.go`.
Index: docs/P4-Prototype/UseCase0006Implementation.md
===================================================================
--- docs/P4-Prototype/UseCase0006Implementation.md	(revision df058387ebe7d07864d874fd1376f15512418581)
+++ docs/P4-Prototype/UseCase0006Implementation.md	(revision 9577c7944ff24ddc9145ff6637494f07d87c3dd4)
@@ -12,4 +12,6 @@
    ```sql
    SELECT symbol, quantity,
+          COALESCE(reserved_quantity,  0),
+          COALESCE(available_quantity, quantity),
           COALESCE(avg_price,      0),
           COALESCE(current_price,  0),
@@ -30,12 +32,14 @@
 ### Verified run
 
-With the seed data (`data_load.sql`), immediately after login, alice's portfolio prints:
+Re-run 2026-09-16 against PostgreSQL 16 with the seed data (`data_load.sql`).
+Immediately after login, alice's portfolio prints (now with the
+`Reserved`/`Available` columns from `holdings.reserved_quantity`):
 
 ```
-  Symbol        Quantity         Avg buy         Current           Value  Unrealised P/L
-  ------------------------------------------------------------------------------------
-  ETH             0.5000     3500.000000     3520.000000       1760.0000        +10.0000
-  ------------------------------------------------------------------------------------
-  TOTAL                                                        1760.0000        +10.0000
+  Symbol        Quantity      Reserved     Available         Avg buy         Current           Value  Unrealised P/L
+  ------------------------------------------------------------------------------------------------------------------
+  ETH             0.5000        0.0000        0.5000     3500.000000     3520.000000       1760.0000        +10.0000
+  ------------------------------------------------------------------------------------------------------------------
+  TOTAL                                                                                    1760.0000        +10.0000
 
   Cash available : 8250.0000 USD
@@ -43,4 +47,8 @@
   Net worth      : 10010.0000 USD
 ```
+
+`Reserved` is 0.0000 here because nothing is mid-sell; see
+[UseCase0005Implementation](UseCase0005Implementation.md) for a portfolio
+snapshot taken with crypto actually reserved.
 
 ### Transaction history
Index: docs/README.md
===================================================================
--- docs/README.md	(revision df058387ebe7d07864d874fd1376f15512418581)
+++ docs/README.md	(revision 9577c7944ff24ddc9145ff6637494f07d87c3dd4)
@@ -13,5 +13,5 @@
 ## Team members
 
-- *Your First Name Last Name — Index XXXXXX*
+- Stefan Trsunov 231285
 
 ## Course
@@ -41,4 +41,6 @@
 | `P3-UseCaseModel/`      | P3 | `UseCaseModel`, `UseCase0001`–`UseCase0007`, `UseCaseModelAIUsage` |
 | `P4-Prototype/`         | P4 | `PrototypeImplementation`, `UseCase000XImplementation`, `BuildInstructions`, `PrototypeImplementationAIUsage` |
+| `P5-Normalization/`     | P5 | `Normalization`, `NormalizationAIUsage` |
+| `P6-AdvancedReports/`   | P6 | `AdvancedReports`, `AdvancedReportsAIUsage` |
 
 `Instructions.md` is the condensed course rubric — reference material, not a
@@ -61,6 +63,6 @@
 | P3 | [UseCaseModel](P3-UseCaseModel/UseCaseModel.md) | Finished, awaiting approval |
 | P4 | [PrototypeImplementation](P4-Prototype/PrototypeImplementation.md) | Finished, awaiting approval |
-| P5 | *Normalization* | Not started |
-| P6 | *Complex DB Reports* | Not started |
+| P5 | [Normalization](P5-Normalization/Normalization.md) | Finished, awaiting approval |
+| P6 | [AdvancedReports](P6-AdvancedReports/AdvancedReports.md) | Finished, awaiting approval |
 | P7 | *Advanced Database Development* | Not started |
 | P8 | *Advanced Application Development* | Not started |
@@ -77,6 +79,8 @@
 | File | Phase | What |
 |------|-------|------|
-| [`ERModel_v02.xml`](P1-ConceptualModel/ERModel_v02.xml) | P1 | TerraER source, current version |
-| [`ERModel_v02.png`](P1-ConceptualModel/ERModel_v02.png) | P1 | Exported diagram image, current version |
+| [`ERModel_v03.xml`](P1-ConceptualModel/ERModel_v03.xml) | P1 | TerraER source, current version |
+| [`ERModel_v03.png`](P1-ConceptualModel/ERModel_v03.png) | P1 | Exported diagram image, current version |
+| [`ERModel_v02.xml`](P1-ConceptualModel/ERModel_v02.xml) | P1 | TerraER source, previous version (kept per P1 rules) |
+| [`ERModel_v02.png`](P1-ConceptualModel/ERModel_v02.png) | P1 | Exported diagram image, previous version |
 | [`ERModel_v01.xml`](P1-ConceptualModel/ERModel_v01.xml) | P1 | TerraER source, first version (kept per P1 rules) |
 | [`ERModel_v01.png`](P1-ConceptualModel/ERModel_v01.png) | P1 | Exported diagram image, first version |
@@ -84,4 +88,5 @@
 | [`../server/db/data_load.sql`](../server/db/data_load.sql) | P2 | DML — truncates and reloads sample data |
 | [`relational_schema.jpg`](P2-RelationalDesign/relational_schema.jpg) | P2 | Crow's-foot diagram exported from DBeaver |
+| [`../server/db/reports_demo_data.sql`](../server/db/reports_demo_data.sql) | P6 | Optional multi-quarter demo data for the two reports (not part of `-init`) |
 
 ## Use cases (P3)
Index: server/cli.go
===================================================================
--- server/cli.go	(revision df058387ebe7d07864d874fd1376f15512418581)
+++ server/cli.go	(revision 9577c7944ff24ddc9145ff6637494f07d87c3dd4)
@@ -76,4 +76,6 @@
 	fmt.Println("[8] Manage watchlist")
 	fmt.Println("[9] Logout")
+	fmt.Println("[10] Report: top traders")
+	fmt.Println("[11] Report: market performance")
 	fmt.Println("[0] Exit")
 	switch prompt("> ") {
@@ -94,4 +96,8 @@
 	case "8":
 		ManageWatchlist(s)
+	case "10":
+		ShowTopTraders(s)
+	case "11":
+		ShowMarketPerformance(s)
 	case "9":
 		s.UserID = ""
Index: server/db/schema_creation.sql
===================================================================
--- server/db/schema_creation.sql	(revision df058387ebe7d07864d874fd1376f15512418581)
+++ server/db/schema_creation.sql	(revision 9577c7944ff24ddc9145ff6637494f07d87c3dd4)
@@ -59,13 +59,18 @@
 -- ============================================================================
 CREATE TABLE project.holdings (
-    id         uuid           PRIMARY KEY DEFAULT gen_random_uuid(),
-    user_id    uuid           NOT NULL REFERENCES project.users(id)  ON DELETE CASCADE,
-    crypto_id  uuid           NOT NULL REFERENCES project.crypto(id),
-    quantity   numeric(20,4)  NOT NULL CHECK (quantity >= 0),
+    id                uuid           PRIMARY KEY DEFAULT gen_random_uuid(),
+    user_id           uuid           NOT NULL REFERENCES project.users(id)  ON DELETE CASCADE,
+    crypto_id         uuid           NOT NULL REFERENCES project.crypto(id),
+    quantity          numeric(20,4)  NOT NULL CHECK (quantity >= 0),
+    -- Committed to the user's own open sell orders, not yet removed from the
+    -- position. quantity - reserved_quantity is what is actually free to
+    -- sell — the crypto-side equivalent of users.available_balance.
+    reserved_quantity numeric(20,4)  NOT NULL DEFAULT 0
+                                      CHECK (reserved_quantity >= 0 AND reserved_quantity <= quantity),
     -- Weighted-average entry price. NOT NULL so that the P/L arithmetic in
     -- v_portfolio can never silently produce NULL for an existing position.
-    avg_price  numeric(18,6)  NOT NULL DEFAULT 0 CHECK (avg_price >= 0),
-    created_at timestamptz    NOT NULL DEFAULT now(),
-    updated_at timestamptz,
+    avg_price         numeric(18,6)  NOT NULL DEFAULT 0 CHECK (avg_price >= 0),
+    created_at        timestamptz    NOT NULL DEFAULT now(),
+    updated_at        timestamptz,
     CONSTRAINT uq_holdings_user_crypto UNIQUE (user_id, crypto_id)
 );
@@ -184,4 +189,6 @@
        c.symbol,
        h.quantity,
+       h.reserved_quantity,
+       (h.quantity - h.reserved_quantity) AS available_quantity,
        h.avg_price,
        lp.price                           AS current_price,
@@ -192,2 +199,111 @@
 LEFT   JOIN project.markets m ON m.crypto_id = c.id AND m.quote_currency = 'USD'
 LEFT   JOIN project.v_latest_prices lp ON lp.market_id = m.id;
+
+-- ============================================================================
+-- REPORTS (P6 — Complex DB Reports)
+-- Both are single SELECT statements (with CTEs), wrapped as SQL functions so
+-- they can be called as parameterised reports from the prototype instead of
+-- being copy-pasted SQL text. See docs/P6-AdvancedReports/AdvancedReports.md.
+-- ============================================================================
+
+-- report_top_traders: realized trading performance per user over [p_from, p_to),
+-- bucketed into quarters to measure how consistently each user was profitable.
+CREATE OR REPLACE FUNCTION project.report_top_traders(p_from timestamptz, p_to timestamptz)
+RETURNS TABLE (
+    username            varchar,
+    realized_pl         numeric,
+    total_invested      numeric,
+    roi_pct             numeric,
+    profitable_periods  bigint,
+    losing_periods      bigint,
+    total_periods       bigint,
+    consistency_pct     numeric
+)
+LANGUAGE sql STABLE AS $$
+    WITH period_pl AS (
+        SELECT
+            t.user_id,
+            date_trunc('quarter', t.created_at)          AS period,
+            SUM(t.amount)                                AS period_pl,
+            SUM(t.amount) FILTER (WHERE t.type = 'buy')  AS period_buy
+        FROM project.transactions t
+        WHERE t.type IN ('buy', 'sell', 'fee')
+          AND t.created_at >= p_from
+          AND t.created_at <  p_to
+        GROUP BY t.user_id, date_trunc('quarter', t.created_at)
+    )
+    SELECT
+        u.username,
+        SUM(pp.period_pl)                                                       AS realized_pl,
+        ABS(SUM(pp.period_buy))                                                 AS total_invested,
+        ROUND(SUM(pp.period_pl) / NULLIF(ABS(SUM(pp.period_buy)), 0) * 100, 2)  AS roi_pct,
+        COUNT(*) FILTER (WHERE pp.period_pl > 0)                                AS profitable_periods,
+        COUNT(*) FILTER (WHERE pp.period_pl < 0)                                AS losing_periods,
+        COUNT(*)                                                                AS total_periods,
+        ROUND(COUNT(*) FILTER (WHERE pp.period_pl > 0)::numeric
+              / NULLIF(COUNT(*), 0) * 100, 2)                                   AS consistency_pct
+    FROM period_pl pp
+    JOIN project.users u ON u.id = pp.user_id
+    GROUP BY u.id, u.username
+    ORDER BY realized_pl DESC;
+$$;
+
+-- report_market_performance: trading activity and price behaviour per market
+-- over [p_from, p_to). Volume/trade-count/price stats come from market_trades
+-- (the complete tape — user fills and simulated fills alike); participating
+-- users can only come from orders, since market_trades has no user_id column.
+CREATE OR REPLACE FUNCTION project.report_market_performance(p_from timestamptz, p_to timestamptz)
+RETURNS TABLE (
+    symbol               varchar,
+    quote_currency       char(3),
+    total_volume         numeric,
+    trade_count          bigint,
+    avg_price            numeric,
+    market_return_pct    numeric,
+    price_volatility     numeric,
+    participating_users  bigint
+)
+LANGUAGE sql STABLE AS $$
+    WITH trades AS (
+        SELECT
+            market_id, price, quantity, executed_at,
+            FIRST_VALUE(price) OVER w AS first_price,
+            LAST_VALUE(price)  OVER (PARTITION BY market_id ORDER BY executed_at
+                                      ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS last_price
+        FROM project.market_trades
+        WHERE executed_at >= p_from AND executed_at < p_to
+        WINDOW w AS (PARTITION BY market_id ORDER BY executed_at)
+    ),
+    market_stats AS (
+        SELECT
+            market_id,
+            SUM(quantity)    AS total_volume,
+            COUNT(*)         AS trade_count,
+            AVG(price)       AS avg_price,
+            STDDEV(price)    AS price_volatility,
+            MAX(first_price) AS first_price,
+            MAX(last_price)  AS last_price
+        FROM trades
+        GROUP BY market_id
+    ),
+    participation AS (
+        SELECT market_id, COUNT(DISTINCT user_id) AS participating_users
+        FROM project.orders
+        WHERE status = 'executed' AND executed_at >= p_from AND executed_at < p_to
+        GROUP BY market_id
+    )
+    SELECT
+        c.symbol,
+        m.quote_currency,
+        ms.total_volume,
+        ms.trade_count,
+        ROUND(ms.avg_price, 6)                                                            AS avg_price,
+        ROUND((ms.last_price - ms.first_price) / NULLIF(ms.first_price, 0) * 100, 2)      AS market_return_pct,
+        ROUND(COALESCE(ms.price_volatility, 0), 6)                                        AS price_volatility,
+        COALESCE(p.participating_users, 0)                                                AS participating_users
+    FROM market_stats ms
+    JOIN project.markets m ON m.id = ms.market_id
+    JOIN project.crypto  c ON c.id = m.crypto_id
+    LEFT JOIN participation p ON p.market_id = ms.market_id
+    ORDER BY ms.total_volume DESC;
+$$;
Index: server/portfolio.go
===================================================================
--- server/portfolio.go	(revision df058387ebe7d07864d874fd1376f15512418581)
+++ server/portfolio.go	(revision 9577c7944ff24ddc9145ff6637494f07d87c3dd4)
@@ -3,4 +3,5 @@
 import (
 	"fmt"
+	"strings"
 
 	"bp_project/server/db"
@@ -13,4 +14,6 @@
 		`SELECT symbol,
 		        quantity,
+		        COALESCE(reserved_quantity, 0),
+		        COALESCE(available_quantity, quantity),
 		        COALESCE(avg_price, 0),
 		        COALESCE(current_price, 0),
@@ -28,8 +31,9 @@
 	defer rows.Close()
 
+	header := fmt.Sprintf("  %-8s  %12s  %12s  %12s  %14s  %14s  %14s  %14s",
+		"Symbol", "Quantity", "Reserved", "Available", "Avg buy", "Current", "Value", "Unrealised P/L")
 	fmt.Println()
-	fmt.Printf("  %-8s  %12s  %14s  %14s  %14s  %14s\n",
-		"Symbol", "Quantity", "Avg buy", "Current", "Value", "Unrealised P/L")
-	fmt.Println("  ------------------------------------------------------------------------------------")
+	fmt.Println(header)
+	fmt.Println("  " + strings.Repeat("-", len(header)-2))
 
 	var totalValue, totalPnL float64
@@ -37,11 +41,11 @@
 	for rows.Next() {
 		var sym string
-		var qty, avg, cur, val, pnl float64
-		if err := rows.Scan(&sym, &qty, &avg, &cur, &val, &pnl); err != nil {
+		var qty, reserved, avail, avg, cur, val, pnl float64
+		if err := rows.Scan(&sym, &qty, &reserved, &avail, &avg, &cur, &val, &pnl); err != nil {
 			fmt.Println("scan error:", err)
 			return
 		}
-		fmt.Printf("  %-8s  %12.4f  %14.6f  %14.6f  %14.4f  %+14.4f\n",
-			sym, qty, avg, cur, val, pnl)
+		fmt.Printf("  %-8s  %12.4f  %12.4f  %12.4f  %14.6f  %14.6f  %14.4f  %+14.4f\n",
+			sym, qty, reserved, avail, avg, cur, val, pnl)
 		totalValue += val
 		totalPnL += pnl
@@ -52,7 +56,7 @@
 		return
 	}
-	fmt.Println("  ------------------------------------------------------------------------------------")
-	fmt.Printf("  %-8s  %12s  %14s  %14s  %14.4f  %+14.4f\n",
-		"TOTAL", "", "", "", totalValue, totalPnL)
+	fmt.Println("  " + strings.Repeat("-", len(header)-2))
+	fmt.Printf("  %-8s  %12s  %12s  %12s  %14s  %14s  %14.4f  %+14.4f\n",
+		"TOTAL", "", "", "", "", "", totalValue, totalPnL)
 
 	// cash summary
Index: server/trade.go
===================================================================
--- server/trade.go	(revision df058387ebe7d07864d874fd1376f15512418581)
+++ server/trade.go	(revision 9577c7944ff24ddc9145ff6637494f07d87c3dd4)
@@ -13,4 +13,13 @@
 // Runs inside a single database transaction so the orders, holdings,
 // users.balance and transactions tables always agree.
+//
+// The order still passes through 'open' before 'executed'. Placing it
+// reserves whatever it commits — on a sell, the crypto being sold, tracked in
+// holdings.reserved_quantity — before anything is actually moved, so a
+// second order against the same holding can never be granted the same units
+// twice. Because only market orders are implemented, reserve and settle
+// happen inside this one transaction rather than across two commits; a
+// future limit-order matcher would split them into a second transaction
+// later, without needing a schema change.
 func PlaceOrder(s *Session, side string) {
 	if side != "buy" && side != "sell" {
@@ -47,9 +56,9 @@
 	defer tx.Rollback()
 
-	// 1. create the order (status='executed' since we fill immediately)
+	// 1. record the order as 'open' — no trade has happened yet.
 	var orderID string
 	err = tx.QueryRow(
-		`INSERT INTO orders (user_id, market_id, side, type, status, quantity, price, executed_at)
-		 VALUES ($1, $2, $3, 'market', 'executed', $4, $5, now())
+		`INSERT INTO orders (user_id, market_id, side, type, status, quantity, price)
+		 VALUES ($1, $2, $3, 'market', 'open', $4, $5)
 		 RETURNING id`,
 		s.UserID, m.ID, side, qty, price,
@@ -87,5 +96,6 @@
 		}
 
-		// upsert holding with running weighted average
+		// a buy never reserves crypto, only ever adds it — upsert holding
+		// with running weighted average
 		if err := upsertHoldingOnBuy(tx, s.UserID, m.CryptoID, qty, price); err != nil {
 			fmt.Println("Error updating holding:", err)
@@ -104,25 +114,42 @@
 		}
 	} else {
-		// sell: check holding
-		var held, avgPrice float64
+		// sell: lock the holding and check what is actually free to sell —
+		// quantity minus whatever another open order has already reserved.
+		var held, reserved, avgPrice float64
 		err := tx.QueryRow(
-			`SELECT quantity, avg_price FROM holdings
+			`SELECT quantity, reserved_quantity, avg_price FROM holdings
 			  WHERE user_id = $1 AND crypto_id = $2 FOR UPDATE`,
 			s.UserID, m.CryptoID,
-		).Scan(&held, &avgPrice)
+		).Scan(&held, &reserved, &avgPrice)
 		if err != nil && err != sql.ErrNoRows {
 			fmt.Println("Error:", err)
 			return
 		}
-		if err == sql.ErrNoRows || held < qty {
-			fmt.Printf("Insufficient holding: trying to sell %.4f, hold %.4f\n", qty, held)
-			return
-		}
-
-		// reduce holding
+		available := held - reserved
+		if err == sql.ErrNoRows || available < qty {
+			fmt.Printf("Insufficient holding: trying to sell %.4f, available %.4f (of %.4f held, %.4f reserved)\n",
+				qty, available, held, reserved)
+			return
+		}
+
+		// reserve: committed to this order, not yet removed from the position.
 		if _, err := tx.Exec(
 			`UPDATE holdings
-			    SET quantity   = quantity - $1,
-			        updated_at = now()
+			    SET reserved_quantity = reserved_quantity + $1,
+			        updated_at        = now()
+			  WHERE user_id = $2 AND crypto_id = $3`,
+			qty, s.UserID, m.CryptoID,
+		); err != nil {
+			fmt.Println("Error:", err)
+			return
+		}
+
+		// settle: a market order fills immediately, so release the
+		// reservation and remove the asset from the position in one step.
+		if _, err := tx.Exec(
+			`UPDATE holdings
+			    SET quantity          = quantity - $1,
+			        reserved_quantity = reserved_quantity - $1,
+			        updated_at        = now()
 			  WHERE user_id = $2 AND crypto_id = $3`,
 			qty, s.UserID, m.CryptoID,
@@ -163,4 +190,13 @@
 		 VALUES ($1, now(), $2, $3, $4, 'user')`,
 		m.ID, price, qty, side,
+	); err != nil {
+		fmt.Println("Error:", err)
+		return
+	}
+
+	// settle the order itself: it has now actually been filled.
+	if _, err := tx.Exec(
+		`UPDATE orders SET status = 'executed', executed_at = now() WHERE id = $1`,
+		orderID,
 	); err != nil {
 		fmt.Println("Error:", err)
