Index: nv.example
===================================================================
--- .env.example	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,22 +1,0 @@
-# Copy to .env and fill in. Values below match the local docker-compose setup.
-# Real environment variables take precedence over this file, so you can point
-# the prototype at the faculty database without editing anything:
-#
-#   DBHOST=dbteaching.finki.ukim.mk DBPORT=5432 DBUSER=... DBPASSWORD=... \
-#   DBNAME=... ./eduberza
-#
-DBPORT=5433
-DBUSER=bp_project
-DBPASSWORD=1234
-DBNAME=bp_database
-DBHOST=localhost
-
-# Optional: connect through SSH, like DBeaver's "SSH" tab. Leave SSH_HOST
-# empty for the local docker setup. When SSH_HOST is set, DBHOST/DBPORT are
-# as seen from the SSH server (copy them from DBeaver's "Main" tab).
-# SSH_HOST=
-# SSH_PORT=22
-# SSH_USER=
-# SSH_PASSWORD=
-# SSH_KEY=/home/you/.ssh/id_ed25519
-# SSH_KEY_PASSPHRASE=
Index: itignore
===================================================================
--- .gitignore	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,15 +1,0 @@
-# Build output
-/eduberza
-/bots/bots
-/server/server
-/server/eduberza
-
-# Local configuration and secrets — never commit; use .env.example as template
-.env
-
-# Editor settings
-.vscode/
-
-*.jar
-docs/Instructions.md
-Instructions.md
Index: README.md
===================================================================
--- README.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ README.md	(revision abe13978c34ed1e8521b0b3ad5d34264000a6f2f)
@@ -1,15 +1,109 @@
-# EduBerza — quick start
+# EduBerza
 
-Educational crypto-exchange simulator. Go CLI + PostgreSQL. Course project for
-*Databases 2025/2026 Winter*, FINKI UKIM.
+## Short description
 
+*P0 — write 150–200 words here in your own words. AI-generated text is not
+allowed in this phase.*
 
-## Run it in four commands
+*Your own Macedonian draft is in [`opis.md`](P0-ProjectDefinition/opis.md); the material to cover is
+what data the database holds (users, crypto assets, markets, orders, holdings,
+transactions, market trades, candles, watchlists), who it is for (beginner
+traders), and what kind of project it is.*
 
-```sh
-cp .env.example .env          
-docker compose up -d          
-go build -o eduberza ./server
-./eduberza -init              
-./eduberza                    
-```
+## Team members
+
+- *Your First Name Last Name — Index XXXXXX*
+
+## Course
+
+Databases in 2025/2026/Winter
+
+## Under the supervision of
+
+Prof. Dr. Vangel V. Ajanovski
+
+## How this folder maps to the wiki
+
+The files here are grouped into one folder per phase for convenience. **The wiki
+is flat** — when you publish, each file becomes a page named exactly after the
+file, with no folder prefix and with capitalisation preserved: `About`,
+`ERModel`, `RelationalDesign`, `UseCaseModel`, `UseCase0001`, …,
+`PrototypeImplementation`, `BuildInstructions`, and the four `*AIUsage` pages.
+Attachments (`ERModel_v01.xml`, `ERModel_v01.png`, `schema_creation.sql`,
+`data_load.sql`, `relational_schema.jpg`, the screenshots) attach to the page
+that documents them.
+
+| Folder | Phase | Wiki pages produced |
+|--------|-------|---------------------|
+| `P0-ProjectDefinition/` | P0 | `About` (+ this front page) |
+| `P1-ConceptualModel/`   | P1 | `ERModel`, `ERModelAIUsage` |
+| `P2-RelationalDesign/`  | P2 | `RelationalDesign`, `RelationalDesignAIUsage` |
+| `P3-UseCaseModel/`      | P3 | `UseCaseModel`, `UseCase0001`–`UseCase0007`, `UseCaseModelAIUsage` |
+| `P4-Prototype/`         | P4 | `PrototypeImplementation`, `UseCase000XImplementation`, `BuildInstructions`, `PrototypeImplementationAIUsage` |
+
+`Instructions.md` is the condensed course rubric — reference material, not a
+submission. `P0-ProjectDefinition/opis.md` and `P1-ConceptualModel/ep-diagram.md`
+are your own working notes, kept because they are the evidence that the initial
+model was yours before any AI was used.
+
+The two SQL scripts deliberately stay in `server/db/` rather than moving into
+`P2-RelationalDesign/`: they are compiled into the prototype binary with
+`go:embed`, which requires them to sit inside the Go package. Attach them to the
+`RelationalDesign` page from there.
+
+## Content
+
+| Phase | Link | Status |
+|-------|------|--------|
+| P0 | [About](P0-ProjectDefinition/About.md) | Draft — needs your own text |
+| P1 | [ERModel](P1-ConceptualModel/ERModel.md) | Finished, awaiting approval |
+| P2 | [RelationalDesign](P2-RelationalDesign/RelationalDesign.md) | Finished, awaiting approval |
+| 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 |
+| P7 | *Advanced Database Development* | Not started |
+| P8 | *Advanced Application Development* | Not started |
+| P9 | *Other topics (Performance, Security)* | Not started |
+
+Keep the Status column current yourself — *started → completed → revised →
+approved → final* — after each consultation.
+
+Note: P0–P4 alone are not enough for a passing grade; at least some of P5–P9 are
+required. P5 is the recommended next one.
+
+## Phase attachments
+
+| File | Phase | What |
+|------|-------|------|
+| [`ERModel_v01.xml`](P1-ConceptualModel/ERModel_v01.xml) | P1 | TerraER source of the ER diagram |
+| [`ERModel_v01.png`](P1-ConceptualModel/ERModel_v01.png) | P1 | Exported diagram image |
+| [`../server/db/schema_creation.sql`](../server/db/schema_creation.sql) | P2 | DDL — drops and recreates the `project` schema |
+| [`../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 |
+
+## Use cases (P3)
+
+[UC0001](P3-UseCaseModel/UseCase0001.md) Register ·
+[UC0002](P3-UseCaseModel/UseCase0002.md) Log in ·
+[UC0003](P3-UseCaseModel/UseCase0003.md) Deposit ·
+[UC0004](P3-UseCaseModel/UseCase0004.md) Buy ·
+[UC0005](P3-UseCaseModel/UseCase0005.md) Sell ·
+[UC0006](P3-UseCaseModel/UseCase0006.md) Portfolio ·
+[UC0007](P3-UseCaseModel/UseCase0007.md) Watchlist
+
+## AI usage logs
+
+Required by the phase rules for every phase where AI was used. P0 does not have
+one because AI use is forbidden there.
+
+- [ERModelAIUsage](P1-ConceptualModel/ERModelAIUsage.md) (P1)
+- [RelationalDesignAIUsage](P2-RelationalDesign/RelationalDesignAIUsage.md) (P2)
+- [UseCaseModelAIUsage](P3-UseCaseModel/UseCaseModelAIUsage.md) (P3)
+- [PrototypeImplementationAIUsage](P4-Prototype/PrototypeImplementationAIUsage.md) (P4)
+
+## Build & run
+
+See [BuildInstructions](P4-Prototype/BuildInstructions.md), or [`../README.md`](../README.md)
+for the four-command quick start.
+# bp
Index: ts/main.go
===================================================================
--- bots/main.go	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,176 +1,0 @@
-// EduBerza market simulation bot.
-//
-// Walks a small price for every active market, inserts rows into
-// market_trades once per tick, and upserts the current 1m candle.
-//
-// Run from the repo root so the relative .env path resolves:
-//
-//	go run ./bots/...
-package main
-
-import (
-	"bufio"
-	"database/sql"
-	"flag"
-	"fmt"
-	"log"
-	"math/rand"
-	"os"
-	"strings"
-	"time"
-
-	_ "github.com/lib/pq"
-)
-
-type market struct {
-	id     string
-	symbol string
-	price  float64
-}
-
-func main() {
-	interval := flag.Duration("interval", 3*time.Second, "seconds between price ticks")
-	flag.Parse()
-
-	loadEnv(".env")
-	dsn := fmt.Sprintf(
-		"host=%s port=%s user=%s password=%s dbname=%s sslmode=disable options='--search_path=project,public'",
-		env("DBHOST", "localhost"),
-		env("DBPORT", "5432"),
-		env("DBUSER", "postgres"),
-		env("DBPASSWORD", ""),
-		env("DBNAME", "postgres"),
-	)
-	db, err := sql.Open("postgres", dsn)
-	if err != nil {
-		log.Fatalf("open db: %v", err)
-	}
-	defer db.Close()
-	if err := db.Ping(); err != nil {
-		log.Fatalf("ping: %v", err)
-	}
-
-	markets, err := loadMarkets(db)
-	if err != nil {
-		log.Fatalf("load markets: %v", err)
-	}
-	if len(markets) == 0 {
-		log.Fatal("no active markets found - run `go run ./server -init` first")
-	}
-
-	log.Printf("bot started. simulating %d markets every %s", len(markets), *interval)
-	rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
-
-	for {
-		for i := range markets {
-			m := &markets[i]
-			// random walk: ±0.3% per tick
-			drift := (rnd.Float64() - 0.5) * 0.006
-			m.price = m.price * (1 + drift)
-			if m.price <= 0 {
-				m.price = 0.000001
-			}
-			qty := rnd.Float64()*0.5 + 0.01
-
-			side := "buy"
-			if rnd.Float64() < 0.5 {
-				side = "sell"
-			}
-
-			if err := insertTick(db, m.id, m.price, qty, side); err != nil {
-				log.Printf("insert tick %s: %v", m.symbol, err)
-				continue
-			}
-			log.Printf("  %-8s  %.6f  qty=%.4f  side=%s", m.symbol, m.price, qty, side)
-		}
-		// P7 background job: the prices just moved, so fill any resting
-		// limit order the new market price has reached.
-		var filled int
-		if err := db.QueryRow(`SELECT fill_marketable_orders()`).Scan(&filled); err != nil {
-			log.Printf("fill_marketable_orders: %v", err)
-		} else if filled > 0 {
-			log.Printf("  filled %d resting limit order(s) at the new market price", filled)
-		}
-		time.Sleep(*interval)
-	}
-}
-
-func loadMarkets(db *sql.DB) ([]market, error) {
-	rows, err := db.Query(`
-		SELECT m.id, c.symbol, COALESCE(lp.price, 100)
-		  FROM markets m
-		  JOIN crypto  c  ON c.id = m.crypto_id
-		  LEFT JOIN v_latest_prices lp ON lp.market_id = m.id
-		 WHERE m.is_active = true
-		 ORDER BY c.symbol`)
-	if err != nil {
-		return nil, err
-	}
-	defer rows.Close()
-	var out []market
-	for rows.Next() {
-		var m market
-		if err := rows.Scan(&m.id, &m.symbol, &m.price); err != nil {
-			return nil, err
-		}
-		out = append(out, m)
-	}
-	return out, nil
-}
-
-func insertTick(db *sql.DB, marketID string, price, qty float64, side string) error {
-	tx, err := db.Begin()
-	if err != nil {
-		return err
-	}
-	defer tx.Rollback()
-
-	if _, err := tx.Exec(
-		`INSERT INTO market_trades (market_id, executed_at, price, quantity, side, source)
-		 VALUES ($1, now(), $2, $3, $4, 'simulation')`,
-		marketID, price, qty, side,
-	); err != nil {
-		return err
-	}
-
-	// upsert the current 1m candle
-	if _, err := tx.Exec(`
-		INSERT INTO market_candles (market_id, timeframe, open, high, low, close, volume, candle_time)
-		VALUES ($1, '1m', $2, $2, $2, $2, $3, date_trunc('minute', now()))
-		ON CONFLICT (market_id, timeframe, candle_time) DO UPDATE
-		   SET high   = GREATEST(market_candles.high, EXCLUDED.close),
-		       low    = LEAST(   market_candles.low,  EXCLUDED.close),
-		       close  = EXCLUDED.close,
-		       volume = market_candles.volume + EXCLUDED.volume`,
-		marketID, price, qty,
-	); err != nil {
-		return err
-	}
-	return tx.Commit()
-}
-
-func loadEnv(path string) {
-	f, err := os.Open(path)
-	if err != nil {
-		return
-	}
-	defer f.Close()
-	s := bufio.NewScanner(f)
-	for s.Scan() {
-		line := strings.TrimSpace(s.Text())
-		if line == "" || strings.HasPrefix(line, "#") {
-			continue
-		}
-		parts := strings.SplitN(line, "=", 2)
-		if len(parts) == 2 {
-			os.Setenv(strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]))
-		}
-	}
-}
-
-func env(k, def string) string {
-	if v := os.Getenv(k); v != "" {
-		return v
-	}
-	return def
-}
Index: cker-compose.yml
===================================================================
--- docker-compose.yml	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,21 +1,0 @@
-services:
-  postgres:
-    image: postgres
-    container_name: databases
-    restart: always
-    ports:
-      - "${DBPORT}:5432"
-    environment:
-      POSTGRES_USER: ${DBUSER}
-      POSTGRES_PASSWORD: ${DBPASSWORD}
-      POSTGRES_DB: ${DBNAME}
-    volumes:
-      - postgres_data:/var/lib/postgresql/data
-    networks:
-      - mynetwork
-
-networks:
-  mynetwork:
-
-volumes:
-  postgres_data:
Index: cs/P0-ProjectDefinition/About.md
===================================================================
--- docs/P0-ProjectDefinition/About.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,88 +1,0 @@
-# About: EduBerza
-
-## Team members
-
-- Stefan Trsunov - 231285
-
-## Short description
-
-Eduberza - Educational Market simulation
-
-This project is a simulation of a crypto exchange, intended for beginners and
-created as a project for the Databases course for 2025-2026. The project is
-inspired by TradingView/Binance and aims to let users experience buying and
-selling cryptocurrencies, as well as the influence these activities have on the
-market and on the way trades are executed.
-
-The platform will allow user registration and the management of virtual (prop)
-money. Every step of the process will be explained in detail, so that users
-understand exactly what happens behind the scenes and how transactions affect
-the market.
-
-The following technologies are used: Golang (with the Chi package), and
-PostgreSQL for the database. The prototype will be a Go CLI, and the final
-project will be built with TypeScript, specifically Lit components.
-
-The main goal is to create educational and interactive software that will help
-beginners and ordinary users understand how crypto exchanges work through
-practical simulation and clear explanations.
-
-Inspiration:
-
-https://tradingview.com/
-https://www.binance.com/
-
-
-
-## Detailed description
-
-This project is an educational simulation of a crypto exchange, intended for
-beginners and created as a project for the Databases course for 2025-2026. The
-project is inspired by the leading platforms TradingView and Binance, with the
-goal of letting users experience in practice the buying and selling of
-cryptocurrencies, as well as the direct influence these transactions have on the
-market and on the trade-execution process itself.
-
-The platform will allow user registration and the management of virtual (prop)
-money. Every step of the process will be explained in detail and in practical
-terms, so that users can more easily understand what exactly happens behind the
-scenes — from the placing of orders to their matching and the effects on market
-liquidity.
-
-The following technologies are used to build the project: Golang (with the Chi
-package) for the server side and PostgreSQL for managing the database. The
-architecture is planned in two phases: the initial prototype will be a Go CLI
-application for quick verification, while the final web version will be built
-with TypeScript, specifically with Lit components, to create a modern interface.
-
-The main goal is to create interactive software that will help beginners and
-ordinary users understand how crypto exchanges work through practical
-simulation, clear explanations and transparent processes.
-
-
-### Who is the database and project intended for?
-
-Target Audience: Beginners and general users who want to learn how cryptocurrency exchanges work without financial risk.
-
-### Which problems does your project solve?
-
-This is only for education purposes. The problem is fixing the financial literacy of the people.
-
-### What types of users exist?
-
-Roles/Users that are expected to use this product are:
-
-- Visitors
-- Traders
-
-### How is this different from similar existing solutions?
-
-Virtual money with a step-by-step explanation of what each order does to the `book`.
-
-### Is this a web, mobile and/or desktop application?
-
-The prototype will be a CLI but the end goal will be to build a website
-
-### Illustrations
-
-Screenshots of the running prototype: [`screenshots/`](../P4-Prototype/screenshots)
Index: cs/P0-ProjectDefinition/opis.md
===================================================================
--- docs/P0-ProjectDefinition/opis.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,18 +1,0 @@
-ЕдуБерза
-
-Веб проект за симулација на крипто берза за почетници, инспирирана од TradingView, изградена со Golang, Typescript и PostgreSQL. Овозможува регистрација на корисници, симулира купување и продавање криптовалути, сето ова ќе биде овозмошено со префрлување на виртуелни пари (prop) со објаснување на секој чекор и ефектите врз пазарот.
-
-Целосен опис
-
-Овој проект претставува симулација на крипто берза, наменета за почетници и создадена како проект за предметот „База на податоци“ за 2025-2026 година. Проектот е инспириран од TradingView/Binance и има за цел да им овозможи на корисниците да ја искусат купување и продавање криптовалути, како и влијанието што овие активности го имаат врз пазарот и начинот на извршување на тргувањата.
-
-Платформата ќе овозможи регистрација на корисници, управување со виртуелни пари (prop). Секој чекор од процесот ќе биде детално објаснет, за корисниците да разберат што точно се случува зад сцената и како трансакциите влијаат на пазарот.
-
-Се користат следниве технологии: Golang (со Chi пакетот), и PostgreSQL за база на податоци. Прототипот ќе биде Go CLI, а конечниот проект ќе биде со Typescript поточно Lit componenti.
-
-Главната цел е да се создаде едукативен и интерактивен софтвер кој ќе им помогне на почетниците и обичните корисници да го разберат работењето на крипто берзите преку практична симулација и јасни објаснувања.
-
-Инспирација:
-
-https://tradingview.com/
-https://www.binance.com/
Index: cs/P0-ProjectDefinition/wiki/About.md
===================================================================
--- docs/P0-ProjectDefinition/wiki/About.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,85 +1,0 @@
-= About: !EduBerza =
-
-== Team members ==
-
- * Stefan Trsunov - 231285
-
-== Short description ==
-
-Eduberza - Educational Market simulation
-
-This project is a simulation of a crypto exchange, intended for beginners and
-created as a project for the Databases course for 2025-2026. The project is
-inspired by !TradingView/Binance and aims to let users experience buying and
-selling cryptocurrencies, as well as the influence these activities have on the
-market and on the way trades are executed.
-
-The platform will allow user registration and the management of virtual (prop)
-money. Every step of the process will be explained in detail, so that users
-understand exactly what happens behind the scenes and how transactions affect
-the market.
-
-The following technologies are used: Golang (with the Chi package), and
-PostgreSQL for the database. The prototype will be a Go CLI, and the final
-project will be built with !TypeScript, specifically Lit components.
-
-The main goal is to create educational and interactive software that will help
-beginners and ordinary users understand how crypto exchanges work through
-practical simulation and clear explanations.
-
-Inspiration:
-
-`https://tradingview.com/`[[BR]]
-`https://www.binance.com/`
-
-== Detailed description ==
-
-This project is an educational simulation of a crypto exchange, intended for
-beginners and created as a project for the Databases course for 2025-2026. The
-project is inspired by the leading platforms !TradingView and Binance, with the
-goal of letting users experience in practice the buying and selling of
-cryptocurrencies, as well as the direct influence these transactions have on the
-market and on the trade-execution process itself.
-
-The platform will allow user registration and the management of virtual (prop)
-money. Every step of the process will be explained in detail and in practical
-terms, so that users can more easily understand what exactly happens behind the
-scenes — from the placing of orders to their matching and the effects on market
-liquidity.
-
-The following technologies are used to build the project: Golang (with the Chi
-package) for the server side and PostgreSQL for managing the database. The
-architecture is planned in two phases: the initial prototype will be a Go CLI
-application for quick verification, while the final web version will be built
-with !TypeScript, specifically with Lit components, to create a modern interface.
-
-The main goal is to create interactive software that will help beginners and
-ordinary users understand how crypto exchanges work through practical
-simulation, clear explanations and transparent processes.
-
-=== Who is the database and project intended for? ===
-
-Target Audience: Beginners and general users who want to learn how cryptocurrency exchanges work without financial risk.
-
-=== Which problems does your project solve? ===
-
-This is only for education purposes. The problem is fixing the financial literacy of the people.
-
-=== What types of users exist? ===
-
-!Roles/Users that are expected to use this product are:
-
- * Visitors
- * Traders
-
-=== How is this different from similar existing solutions? ===
-
-Virtual money with a step-by-step explanation of what each order does to the `book`.
-
-=== Is this a web, mobile and/or desktop application? ===
-
-The prototype will be a CLI but the end goal will be to build a website
-
-=== Illustrations ===
-
-Screenshots of the running prototype: `screenshots/`
Index: cs/P0-ProjectDefinition/wiki/WikiStart.md
===================================================================
--- docs/P0-ProjectDefinition/wiki/WikiStart.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,37 +1,0 @@
-= !EduBerza =
-
-== Short description ==
-
-''P0 — write 150–200 words here in your own words. AI-generated text is not''
-''allowed in this phase.''
-
-''Your own Macedonian draft is in'' `opis.md`''; the material to cover is''
-''what data the database holds (users, crypto assets, markets, orders, holdings,''
-''transactions, market trades, candles, watchlists), who it is for (beginner''
-''traders), and what kind of project it is.''
-
-== Team members ==
-
- * Stefan Trsunov 231285
-
-== Course ==
-
-Databases in 2025/2026/Winter
-
-== Under the supervision of ==
-
-Prof. Dr. Vangel V. Ajanovski
-
-== Content ==
-
-||= Phase =||= Page =||= Status =||
-|| P0 || [wiki:About] || Draft — needs your own text ||
-|| P1 || [wiki:ERModel] || Finished, awaiting approval ||
-|| P2 || [wiki:RelationalDesign] || Finished, awaiting approval ||
-|| P3 || [wiki:UseCaseModel] || Finished, awaiting approval ||
-|| P4 || [wiki:PrototypeImplementation] || Finished, awaiting approval ||
-|| P5 || [wiki:Normalization] || Finished, awaiting approval ||
-|| P6 || [wiki:AdvancedReports] || Finished, awaiting approval ||
-|| P7 || [wiki:AdvancedDatabaseDevelopment] || Finished, awaiting approval ||
-|| P8 || ''Advanced Application Development'' || Not started ||
-|| P9 || ''Other topics (Performance, Security)'' || Not started ||
Index: cs/P1-ConceptualModel/ERModel.md
===================================================================
--- docs/P1-ConceptualModel/ERModel.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,430 +1,0 @@
-# Entity-Relationship Model v.05
-
-## Diagram
-
-![ERModel_v05](ERModel_v05.png)
-
-Notation: Chen. Rectangles are entity sets, diamonds are relationships, ellipses
-are attributes, underlined ellipses are primary keys, the dashed ellipse is a
-derived attribute. A double line between an entity set and a relationship marks
-**total participation** (every instance of that entity set must participate); a
-single line marks partial participation.
-
-Three deliberate modeling decisions worth stating up front:
-
-- **No foreign keys appear in the diagram.** Connections between entity sets are
-  expressed as relationships, per the notation. Foreign-key columns appear only
-  in the relational model in [RelationalDesign](../P2-RelationalDesign/RelationalDesign.md).
-- **A position and a watchlist entry are entity sets, not M:N relationships.**
-  `Holdings` (a user's position in an asset) and `WatchlistItems` (an asset on a
-  watchlist) each have their own identifier `id`, and each is connected by two
-  1:N relationships: `Holds` and `PositionIn` for a holding, `Contains` and
-  `Lists` for a watchlist item. Until v04 they were drawn as the M:N
-  relationships `Holds` and `Contains`, but the database has always given
-  `holdings` and `watchlist_items` their own `id` primary key. That is how an
-  entity set is implemented, not an M:N relationship, whose key would be the
-  pair of participating keys. v05 corrects the model to match; see
-  [history](#entity-relationship-model-history).
-- **Key and uniqueness rules are stated with entity and relationship names,
-  never with foreign-key columns.** For example: "a crypto is quoted at most
-  once per currency", not "`{crypto_id, quote_currency}` is unique".
-
-## 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
-
-#### Users
-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.
-
-**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 `@` (checked by the application at registration, not by a database constraint) |
-| `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 |
-| `reserved_balance` | numeric(18,4) | required, default 0, ≥ 0 — cash set aside for the user's open buy orders (added in v04, after P7) |
-| `created_at` | timestamptz | required, defaults to now |
-| `updated_at` | timestamptz | optional (null until first change) |
-
-#### Cryptos
-The catalog of crypto assets the platform knows about. Kept separate from
-`Markets` because an asset exists independently of the pairs it is traded in —
-the same asset can be quoted against several currencies, and a user's holding is
-in the *asset*, not in a particular pair.
-
-**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
-A tradeable pair: one crypto asset quoted in one currency, e.g. BTC/USD. This is
-where prices live, and it is the thing an order is placed *on*. Modeled as its
-own entity set rather than an attribute of `Cryptos` because a market has its own
-lifecycle — it can be deactivated without deleting the asset — and because
-trades, candles and orders all reference the pair, not the asset.
-
-**Keys:** candidate `{id}`; primary key **`id`**, so that the many entity sets
-related to a market need one narrow identifier instead of a composite one.
-**Uniqueness rule:** a crypto is quoted at most once per currency, so the crypto
-a market is `QuotedOn` together with its `quote_currency` identifies the market
-as well. Chen notation cannot draw this, because half of it comes through a
-relationship. P2 enforces it as `UNIQUE(crypto_id, quote_currency)`.
-
-| 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
-A user's instruction to buy or sell on a market. Needed as a separate entity set
-because an order is a record of *intent* that outlives its execution: it keeps
-the requested quantity and price even after it has been filled, which is what
-makes the ledger auditable.
-
-Placing an order is what triggers a **reservation** of whatever it commits:
-the crypto being sold (`Holdings.reserved_quantity`, below) on a sell, and the
-cash (`Users.reserved_balance`) on a buy. Since v04 (after P7) an order can
-wait in the order book and be filled in parts, so `status` is a real
-lifecycle driven by `filled_quantity`: `open` (nothing filled yet),
-`partially_filled`, `executed` (completely filled), or `cancelled`, which
-releases what is still reserved. See
-[UseCase0005](../P3-UseCaseModel/UseCase0005.md) for the reserve-then-settle
-sequence and
-[AdvancedDatabaseDevelopment](../P7-AdvancedDatabaseDevelopment/AdvancedDatabaseDevelopment.md)
-for the rules that keep it consistent.
-
-**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` (both executed since P7) |
-| `status` | text | required, `open`, `partially_filled`, `executed` or `cancelled` |
-| `quantity` | numeric(20,4) | required, > 0 |
-| `filled_quantity` | numeric(20,4) | required, default 0, between 0 and `quantity` — how much has been traded; remaining = `quantity − filled_quantity` (added in v04, after P7) |
-| `price` | numeric(18,6) | optional — the limit price; for a market order, the market price when it was placed |
-| `placed_at` | timestamptz | required, defaults to now |
-| `executed_at` | timestamptz | optional, set when the order settles |
-
-#### Transactions
-The financial ledger: every movement of virtual cash, in one place. This exists
-so that a balance is never just a number someone edited — it is the sum of an
-auditable list of entries, which is also what the "explain every step" goal of
-the project needs.
-
-**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
-Individual executed trades on a market, from the user's own fills and from the
-market simulator. This is the single source of truth for the current price: the
-price of a market is the price of its most recent trade, never a column someone
-writes directly.
-
-**Keys:** candidate `{id}` — "market plus `executed_at`" looks unique in
-principle, but two trades on a market 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` | big integer | PK, required, auto-generated (`bigserial` in P2) |
-| `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`) |
-
-Since v04 (after P7) a trade also records which orders it filled, through the
-relationships `FillsBuy` and `FillsSell` below.
-
-#### OrderEvents
-*Added in v04, after P7.* The audit trail of an order: one event for its
-placement, one for every (partial) fill, and one for a cancellation. The
-`Orders` row only holds the current state; this entity keeps the history of
-how the order got there. Events are recorded automatically by the database.
-
-**Keys:** candidate `{id}` only; primary key **`id`** (auto-incrementing
-integer, events are only read in order).
-
-| Attribute | Type | Constraints |
-|---|---|---|
-| `id` | big integer | PK, required, auto-generated (`bigserial` in P2) |
-| `event_type` | text | required, `placed`, `partially_filled`, `filled` or `cancelled` |
-| `quantity` | numeric(20,4) | required — the ordered quantity for `placed`, the filled amount for a fill, the unfilled rest for `cancelled` |
-| `price` | numeric(18,6) | optional — the order price, or the trade price for a fill |
-| `status_after` | text | required, the order's status after the event |
-| `created_at` | timestamptz | required, set automatically when the event is recorded (`clock_timestamp()`, so events inside one transaction keep their real order) |
-
-#### MarketCandles
-OHLCV aggregates per market and timeframe — the data a price chart is drawn
-from. Stored rather than computed on the fly because the point of the project is
-a chart-driven interface, and re-aggregating the whole trade history for every
-screen refresh does not scale.
-
-**Keys:** candidate `{id}`; primary key **`id`**. **Uniqueness rule:** a market
-has exactly one candle per timeframe per time bucket, so the market a candle
-`Aggregates` together with `timeframe` and `candle_time` also identifies it.
-This is the real-world constraint that prevents duplicate candles. P2 enforces
-it as `UNIQUE(market_id, timeframe, candle_time)`.
-
-| Attribute | Type | Constraints |
-|---|---|---|
-| `id` | big integer | PK, required, auto-generated (`bigserial` in P2) |
-| `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
-A named list of assets a user wants to monitor. A separate entity set rather than
-a flag on the relationship between users and assets, because a user may want
-several lists ("long term", "watching today") and each needs its own name.
-
-**Keys:** candidate `{id}`; primary key **`id`**. "Owner plus `name`" would
-also identify a list if names had to be unique per user, but the model does
-not require that, so there is no uniqueness rule here.
-
-| Attribute | Type | Constraints |
-|---|---|---|
-| `id` | UUID | PK, required |
-| `name` | text(100) | required |
-| `created_at` | timestamptz | required, defaults to now |
-
-#### Holdings
-A user's position in one crypto asset: how much of it the user owns, how much
-of that is already promised to open sell orders, and at what average price it
-was accumulated. *An entity set since v05* (until v04 it was the M:N
-relationship `Holds`). A holding has its own identifier and its own
-lifecycle: it is created on the first buy, updated on every later fill, and
-the prototype reads and locks it as a unit (`SELECT … FOR UPDATE` on the sell
-path). It is linked to its owner through `Holds` and to its asset through
-`PositionIn`.
-
-**Keys:** candidate `{id}`; primary key **`id`**. **Uniqueness rule:** a user
-has at most one holding per crypto, so the user who `Holds` it together with
-the crypto it is a `PositionIn` also identifies a holding. P2 enforces this as
-`UNIQUE(user_id, crypto_id)`.
-
-`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 |
-|---|---|---|
-| `id` | UUID | PK, required |
-| `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, default 0, ≥ 0, **derived** (dashed ellipse) — the weighted average of the prices at which the position was accumulated; derivable from the buy history, stored anyway so unrealised P/L can be shown without replaying the whole ledger |
-| `created_at` | timestamptz | required, defaults to now |
-| `updated_at` | timestamptz | optional |
-
-#### WatchlistItems
-One asset placed on one watchlist. *An entity set since v05* (until v04 it was
-the M:N relationship `Contains`). It has its own identifier, and it is linked
-to its list through `Contains` and to its asset through `Lists`.
-
-**Keys:** candidate `{id}`; primary key **`id`**. **Uniqueness rule:** an asset
-appears at most once on a given list, so the watchlist that `Contains` an item
-together with the crypto it `Lists` also identifies the item. P2 enforces this
-as `UNIQUE(watchlist_id, crypto_id)`.
-
-| Attribute | Type | Constraints |
-|---|---|---|
-| `id` | UUID | PK, required |
-| `added_at` | timestamptz | required, defaults to now — recorded so a list can be shown in the order the user built it |
-
-### Relationships
-
-#### QuotedOn — Cryptos (1) : Markets (N), total on Markets
-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.
-
-#### PlacedOn — Markets (1) : Orders (N), total on Orders
-Records which market an order was placed on. Every order must name a market;
-a market may have no orders yet. No attributes.
-
-#### 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 — Users (1) : Transactions (N), total on Transactions
-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 — which is why the corresponding column is nullable in P2. No
-attributes.
-
-#### Fills — Markets (1) : MarketTrades (N), total on MarketTrades
-Every executed trade happened on exactly one market. No attributes.
-
-#### FillsBuy — Orders (1) : MarketTrades (N), partial on both sides
-*Added in v04, after P7.* The buy order a trade filled. An order can be
-filled by many trades (partial fills); a trade fills at most one buy order,
-and none when the simulated market was the buyer. The role of `Orders` in this
-relationship is *the buy order* of the trade. No attributes.
-
-#### FillsSell — Orders (1) : MarketTrades (N), partial on both sides
-*Added in v04, after P7.* The sell order a trade filled, symmetric to
-`FillsBuy`; the role of `Orders` here is *the sell order* of the trade. A
-trade between two users' orders participates in both. No attributes.
-
-#### Logs — Orders (1) : OrderEvents (N), total on OrderEvents
-*Added in v04, after P7.* Every event belongs to exactly one order. No
-attributes.
-
-#### Aggregates — Markets (1) : MarketCandles (N), total on MarketCandles
-Every candle summarises trades of exactly one market. No attributes.
-
-#### Owns — Users (1) : Watchlists (N), total on Watchlists
-Every watchlist belongs to exactly one user. No attributes.
-
-#### Holds — Users (1) : Holdings (N), total on Holdings
-*1:N since v05.* Every holding belongs to exactly one user. A user may hold
-nothing yet, so participation is partial on the `Users` side. No attributes.
-
-#### PositionIn — Cryptos (1) : Holdings (N), total on Holdings
-*Added in v05.* Every holding is a position in exactly one crypto asset. An
-asset may be held by nobody. No attributes.
-
-Together, `Holds` and `PositionIn` still say what the old M:N `Holds` said:
-a user can hold many assets and an asset can be held by many users. The
-difference is that the position is now a thing with its own identity, not
-just a pair. The rule "at most one holding per user and crypto" is stated
-under [Holdings](#holdings).
-
-#### Contains — Watchlists (1) : WatchlistItems (N), total on WatchlistItems
-*1:N since v05.* Every watchlist item is on exactly one list. An empty list is
-valid, so participation is partial on the `Watchlists` side. No attributes.
-
-#### Lists — Cryptos (1) : WatchlistItems (N), total on WatchlistItems
-*Added in v05.* Every watchlist item names exactly one crypto asset. An asset
-need not be on any list. No attributes.
-
-## Entity-Relationship Model History
-
-- **v01** — First complete version. Built from the entity notes in
-  [`ep-diagram.md`](ep-diagram.md) (the initial hand-written model), with three
-  changes made to that initial model while drawing it:
-  1. `Markets` was promoted from an implied attribute of the asset to its own
-     entity set, so that prices, orders, trades and candles can all reference a
-     pair rather than an asset.
-  2. `holdings` and `watchlist_items` were re-expressed as the M:N relationships
-     `Holds` and `Contains` with their own attributes, instead of entity sets
-     with foreign keys — the initial notes listed them as tables, which is a
-     relational concept that does not belong in a Chen ERD.
-  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.
-- **v04 — after P7.** Phase 7 (order, balance and trade consistency) needed
-  data the model did not have, so the model was extended to stay in line with
-  the database:
-  - `Users.reserved_balance`: cash reserved by open buy orders;
-  - `Orders.filled_quantity` and the status value `partially_filled`: orders
-    can now be filled in parts;
-  - the relationships `FillsBuy` and `FillsSell` between `Orders` and
-    `MarketTrades`: which orders a trade filled;
-  - the entity set `OrderEvents` with the relationship `Logs`: the
-    automatically recorded history of every order.
-
-  Nothing existing was removed or changed. See
-  [AdvancedDatabaseDevelopment](../P7-AdvancedDatabaseDevelopment/AdvancedDatabaseDevelopment.md).
-  The diagram files are `ERModel_v04.xml` / `ERModel_v04.png`.
-- **v05 — correction after review.** The review of P2 found that two parts of
-  the model were implemented differently in the database:
-  - `Contains` was an M:N relationship in the model, but `watchlist_items`
-    has its own `id` primary key;
-  - `Holds` was an M:N relationship in the model, but `holdings` has its own
-    `id` primary key.
-
-  An M:N relationship has no identifier of its own; its table's key is the pair
-  of participating keys. A table with its own `id` is the implementation of an
-  entity set. Every phase after P2 (the prototype, the reports and the P7
-  logic) already uses the database as it is. So the **model** was corrected to
-  match P2, not the other way round:
-  - `Holds` (M:N, with attributes) became the entity set `Holdings` (its former
-    attributes plus `id`) with two 1:N relationships, `Holds` (Users → Holdings)
-    and `PositionIn` (Cryptos → Holdings), both total on the `Holdings` side;
-  - `Contains` (M:N, with `added_at`) became the entity set `WatchlistItems`
-    (`id`, `added_at`) with `Contains` (Watchlists → WatchlistItems) and
-    `Lists` (Cryptos → WatchlistItems), both total on the `WatchlistItems`
-    side;
-  - the former keys of the two relationships are kept as uniqueness rules
-    ("one holding per user and crypto", "an asset at most once per list");
-  - the key descriptions of `Markets`, `MarketTrades`, `MarketCandles` and
-    `Watchlists` no longer name foreign-key columns (`crypto_id`,
-    `market_id`, `user_id`), which do not exist in an ER model;
-  - the diagram was redrawn on a grid with no overlapping attributes. In v04,
-    `Watchlists.id` was hidden behind `added_at`, and several attributes of
-    `Orders`, `Transactions`, `MarketTrades` and `MarketCandles` overlapped.
-    The grid also makes it easier to compare the diagram with the P2
-    relational diagram.
-
-  The diagram files are `ERModel_v05.xml` / `ERModel_v05.png`; earlier versions
-  are kept.
-
-Reasoning for the AI-assisted part of this phase, and the full interaction log,
-are on [ERModelAIUsage](ERModelAIUsage.md).
-
Index: cs/P1-ConceptualModel/ERModelAIUsage.md
===================================================================
--- docs/P1-ConceptualModel/ERModelAIUsage.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,345 +1,0 @@
-# Entity-Relationship Model AI Usage
-
-## Name of AI service/solution that was used
-
-**Claude Code** (Anthropic)
-
-- **URL:** https://claude.com/claude-code
-- **Type of service/subscription:** Claude subscription. Session 1 used model
-  Claude Opus 4.7 (1M context); session 2 used Claude Opus 5 (1M context).
-
-## Final result
-
-### Diagram
-
-Current version: `ERModel_v05.xml` / `ERModel_v05.png` (see session 5 below). First
-version: `ERModel_v01.xml` / `ERModel_v01.png`.
-
-**Declaration of how the diagram was produced.** The initial model is the
-student's own: the entity and attribute list in [`ep-diagram.md`](ep-diagram.md),
-written in Macedonian before any AI was involved. In session 2 the AI turned that
-list into the TerraER diagram file, and while doing so proposed three changes to
-the initial model, all three listed in the model history on
-[ERModel](ERModel.md#entity-relationship-model-history):
-promoting `Markets` to its own entity set, re-expressing `holdings` and
-`watchlist_items` as M:N relationships with attributes rather than entity sets
-with foreign keys, and marking `avg_price` as derived.
-
-The diagram was not drawn by hand in the TerraER GUI. It was generated
-programmatically by constructing TerraER's own figure objects
-(`EntidadeFigure`, `RelacionamentoFigure`, `AtributoFigure`,
-`AtributoChaveFigure`, `AtributoDerivadoFigure` and the labelled line-connection
-figures) and serialising them with TerraER's own
-`DOMStorableInputOutputFormat` — the same writer the application uses when you
-choose *Save*. The file is therefore a normal TerraER document: it was verified
-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. `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
-
-See [ERModel](ERModel.md).
-
-## Summary of AI involvement
-
-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
-Macedonian before any AI was involved) and my own draft schema and Go backend. I
-used the AI to review them, and it found real errors in my SQL that I had missed
-— most seriously that I had declared `crypto_id` as a foreign key to two
-different tables at once, in three separate tables. I decided the corrections to
-adopt, chose to go solo, chose a CLI prototype over an HTTP one, and chose
-English for the documentation. The output of that session was the corrected
-schema, the sample-data script, the use-case model and the working prototype.
-
-In **session 2** I came back to produce the deliverable I was still missing — the
-TerraER diagram file itself — and to finish the P4 documentation. I also asked
-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**:
-the wording is my own, unaltered, but I have left out scheduling remarks and
-other working chatter that says nothing about the model or the code. Typos are
-kept as they were. The *Intent* line above each prompt is my own note on what I
-was trying to achieve. AI responses are summarised rather than quoted in full,
-because several ran to many pages of code and file edits. The complete
-unabridged transcript is in my Claude Code session history and can be produced
-on request.
-
-## Entire AI usage log
-
----
-
-### Session 1 — 2026-04-21
-
-**Intent:** hand the AI the whole project and the course rubric, and have it
-audit my existing model and code against the phase requirements before I commit
-to anything.
-
-**Prompt (student, excerpt):**
-> This is my project for my exam Database, go to the docs folder and README.md there you will find all of the documentation, afterwards take a look at the whole project. Delete everything you want add everything you want just make it work and make it fill in or to follow all of the needed instructions. Ask me anything, also tell me what do I need to do
-
-**Response (AI, summarised):**
-- Read `docs/opis.md`, `docs/ep-diagram.md`, `docs/dbdiagram.md`, `server/db/db.sql`, `server/db/schema.sql`, all Go sources.
-- Flagged bugs in the initial schema:
-  - `holdings.crypto_id` declared as a foreign key to both `users(id)` and `crypto(id)`; should be a separate `user_id` column.
-  - Same mistake in `orders` and `transactions`.
-  - `market_candles` referenced a non-existent `markets` table.
-  - `main.go` dropped and recreated all tables on every startup, wiping data.
-- Proposed a single authoritative schema, to be placed in `server/db/schema_creation.sql`, based on `docs/ep-diagram.md` and `docs/dbdiagram.md` with the above issues fixed.
-- Asked the student for decisions: team composition, prototype shape (CLI vs HTTP), whether to keep `available_balance`, `invested_balance`, `secret_question`, `secret_answer`, which use cases to implement, language of documentation.
-
-**Intent:** I had just pasted the course rubric into `README.md` and removed the
-half-finished frontend, so that the AI would plan against the actual phase
-requirements and against a backend-only prototype.
-
-**Prompt (student, excerpt):**
-> Read it now, just filled it in, also deleted the frontend we don' need that
-
-**Response (AI, summarised):**
-- Re-read `docs/README.md`, which now contained the course phase rubric (P0–P9).
-- Explained the constraints each phase places on AI usage (P0 forbids AI; P1–P4 allow AI only to improve the student's own initial model and require an AIUsage log).
-- Listed the deliverables per phase and asked the remaining questions.
-
----
-
-### Session 2 — 2026-08-06 / 2026-08-07
-
-**Intent:** re-read the rubric from scratch after the gap, reduce it to a
-checklist of what each phase actually has to produce, confirm TerraER runs on
-this machine.
-
-**Prompt (student, excerpt):**
-> We will start from the begining in some ways, please read first docs/insturctions.md
-> The goal is to finish with milestone 0 so I can publish it […]
-> Read the Instuctions.md like I said and delete everything that you think we don't need
-> and for each phrase tell me what do I need to submit and how to do it. and can you also
-> do the thing with TerraER3.11.jar?
-
-**Response (AI, summarised):**
-- Read the phase rubric and condensed `Instructions.md` to a per-phase checklist
-  of deliverables, keeping every requirement and every load-bearing filename.
-- Confirmed TerraER 3.11 runs on the installed OpenJDK 21 and launched it.
-- Listed, per phase, what has to be submitted and how; flagged that P0's
-  deliverables are wiki pages, that the EPRMS proposal is a prerequisite.
-- Flagged `image.png` (a TradingView screenshot) as material the project has no
-  licence to publish.
-
-**Intent:** produce the artefacts still missing for P1–P4 — above all the TerraER
-diagram — while keeping P0 for myself, since AI use is forbidden there.
-
-**Prompt (student, excerpt):**
-> […] do all of the other Phases till m0.
-> opis.md It's p0 so I will take care of that. Delete anything that we don't need,
-> make all of the phases and terra diagram if you can, and delete anything
-> that we don't need and make a documentation about how to start it.
-
-**Intent:** ask for a review pass over the schema and the Go code rather than
-only documentation, on the grounds that a prototype I have to defend in person
-should not have known defects in it.
-
-**Prompt (student, excerpt, follow-up):**
-> Also fix some database things or golang things if you think we can do it better,
-
-**Response (AI, summarised) — the part relevant to P1:**
-- Read `ep-diagram.md` (the student's own initial model) and the existing
-  `schema_creation.sql`.
-- Reverse-engineered TerraER's file format from the distributed jar to learn the
-  element names it stores figures under (`ent`, `rel`, `atr`, `atrchave`,
-  `atrderivado`, `llabelUm`, `llabelMuitos`, `llabelDoubleUm`,
-  `llabelDoubleMuitos`, …).
-- Generated `ERModel_v01.xml` and `ERModel_v01.png` as described above, in Chen
-  notation: 8 entity sets, 10 relationships, 57 attributes, cardinality labels
-  on every relationship line and double lines for total participation.
-- Proposed the three changes to the initial model recorded in the model history.
-- Rewrote [ERModel](ERModel.md) with the per-entity documentation, candidate-key
-  justifications and attribute types the phase template requires.
-
----
-
-> **Student action required.** Two things, in this order:
->
-> 1. Open `ERModel_v01.xml` in TerraER, read the whole diagram, and change what
->    you disagree with. Save the result as `ERModel_v02.xml` with a matching PNG
->    and add a history line. The phase rules require that the model be yours;
->    the generated v01 is a starting point to review and take over, not an
->    answer to submit unread.
-> 2. Verify that this log matches your recollection and append the full text of
->    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`.
-
-
-### Session 4 — 2026-09-24 (Claude Opus 5.5): v04 after P7
-
-**Prompts (student, verbatim):**
-> But this order_events is added after Phase 7 right? can we add that too?
-
-> can you make the ERmodel again with TerraER file to update it? and tell that after P7 we added this
-
-> Make it with a white background like earier versions
-
-**Response (AI, summarised):**
-
-- Explained that the P7 changes must also appear in P1 and P2, since both must describe the
-  current data structure.
-- Built `ERModel_v04.xml` in TerraER's own file format by taking `ERModel_v03.xml` unchanged
-  and appending the new elements with the same XML structure TerraER uses:
-  - the attribute `reserved_balance` on `Users`;
-  - the attribute `filled_quantity` on `Orders`;
-  - the relationships `FillsBuy` and `FillsSell` (Orders 1 : N MarketTrades, partial);
-  - the entity set `OrderEvents` (key `id`, `event_type`, `quantity`, `price`,
-    `status_after`, `created_at`) with `Logs` (Orders 1 : N OrderEvents, total on
-    OrderEvents).
-- Rendered `ERModel_v04.png` with TerraER 3.14's own drawing classes (loading the `.xml`
-  exactly as TerraER does and using its image export), on a white background and trimmed like
-  the earlier versions.
-- Updated [ERModel](ERModel.md) (title v.04, new attribute rows, the `OrderEvents` section, the
-  three relationships, and a v04 history entry stating these were added after P7).
-
-**What I decided:** to add the P7 structure to the ER model. The new elements are placed
-automatically, so the layout can be tidied by hand in TerraER.
-
-
-### Session 5 — 2026-09-29 (Claude Opus 5.5): v05, correction after review
-
-**Prompt (student, excerpt):** I pasted the reviewer's comments that `Holds` and `Contains`
-are implemented differently in the database than in the model, with the suggestion to
-"correct only the Phase 1 model so that it matches Phase 2", and asked:
-
-> Can you take a look at this stuff? and fix them, but don't push anything just make the
-> changes requested and try to explain it to me to see if you understand the problem with
-> the solution
-
-**Response (AI, summarised):**
-
-- Explained that an M:N relationship has no identifier of its own, while `holdings` and
-  `watchlist_items` each have an `id` primary key, so the database treats them as entity
-  sets.
-- Changed the model to v05. `Holdings` (`id`, `quantity`, `reserved_quantity`, derived
-  `avg_price`, `created_at`, `updated_at`) and `WatchlistItems` (`id`, `added_at`) became
-  entity sets, with the 1:N relationships `Holds`, `PositionIn`, `Contains` and `Lists`,
-  each total on the new entity's side. The old relationship keys are now stated as
-  uniqueness rules. The key descriptions no longer name foreign-key columns.
-- Generated `ERModel_v05.xml` / `ERModel_v05.png` from scratch with TerraER 3.11's own figure
-  classes and writer (adapted from the v01 generator), on a grid, with no overlapping
-  attributes. It was verified by reading the file back with TerraER's reader (184 figures)
-  and by inspecting the rendered PNG.
-- Updated [ERModel](ERModel.md) (v05 sections and history entry).
-
-**What I decided:** to follow the reviewer's advice and change the model rather than the
-database, since every later phase already uses the database as it is.
Index: cs/P1-ConceptualModel/ERModel_v01.xml
===================================================================
--- docs/P1-ConceptualModel/ERModel_v01.xml	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,1 +1,0 @@
-<drawing><figures><ent id="0"><children><r id="1" x="1195" y="864" w="210" h="72"/><t id="2" x="1278.3098373413086" y="891.8279428482056"><a><text><string>Cryptos</string></text></a></t></children></ent><ent id="3"><children><r id="4" x="2995" y="864" w="210" h="72"/><t id="5" x="3077.0858306884766" y="891.8279428482056"><a><text><string>Markets</string></text></a></t></children></ent><ent id="6"><children><r id="7" x="4195" y="1264" w="210" h="72"/><t id="8" x="4260.831718444824" y="1291.8279428482056"><a><text><string>MarketTrades</string></text></a></t></children></ent><ent id="9"><children><r id="a" x="4195" y="2264" w="210" h="72"/><t id="b" x="4257.597694396973" y="2291.8279428482056"><a><text><string>MarketCandles</string></text></a></t></children></ent><ent id="c"><children><r id="d" x="2995" y="2864" w="210" h="72"/><t id="e" x="3080.4098510742188" y="2891.8279428482056"><a><text><string>Orders</string></text></a></t></children></ent><ent id="f"><children><r id="10" x="1895" y="3264" w="210" h="72"/><t id="11" x="1964.0657501220703" y="3291.8279428482056"><a><text><string>Transactions</string></text></a></t></children></ent><ent id="12"><children><r id="13" x="795" y="2464" w="210" h="72"/><t id="14" x="884.0038757324219" y="2491.8279428482056"><a><text><string>Users</string></text></a></t></children></ent><ent id="15"><children><r id="16" x="295" y="1464" w="210" h="72"/><t id="17" x="371.289794921875" y="1491.8279428482056"><a><text><string>Watchlists</string></text></a></t></children></ent><rel id="18"><children><diamond id="19" x="2100" y="852" w="200" h="96"/><t id="1a" x="2170.3417892456055" y="891.8279428482056"><a><text><string>QuotedOn</string></text></a></t></children></rel><rel id="1b"><children><diamond id="1c" x="3650" y="1002" w="200" h="96"/><t id="1d" x="3739.367919921875" y="1041.8279428482056"><a><text><string>Fills</string></text></a></t></children></rel><rel id="1e"><children><diamond id="1f" x="3700" y="1702" w="200" h="96"/><t id="20" x="3767.44376373291" y="1741.8279428482056"><a><text><string>Aggregates</string></text></a></t></children></rel><rel id="21"><children><diamond id="22" x="3000" y="1852" w="200" h="96"/><t id="23" x="3073.107810974121" y="1891.8279428482056"><a><text><string>PlacedOn</string></text></a></t></children></rel><rel id="24"><children><diamond id="25" x="2450" y="3052" w="200" h="96"/><t id="26" x="2531.1838607788086" y="3091.8279428482056"><a><text><string>Settles</string></text></a></t></children></rel><rel id="27"><children><diamond id="28" x="1350" y="2852" w="200" h="96"/><t id="29" x="1427.3318328857422" y="2891.8279428482056"><a><text><string>Records</string></text></a></t></children></rel><rel id="2a"><children><diamond id="2b" x="1900" y="2652" w="200" h="96"/><t id="2c" x="1982.31787109375" y="2691.8279428482056"><a><text><string>Places</string></text></a></t></children></rel><rel id="2d"><children><diamond id="2e" x="1000" y="1652" w="200" h="96"/><t id="2f" x="1083.811882019043" y="1691.8279428482056"><a><text><string>Holds</string></text></a></t></children></rel><rel id="30"><children><diamond id="31" x="550" y="1952" w="200" h="96"/><t id="32" x="634.0158843994141" y="1991.8279428482056"><a><text><string>Owns</string></text></a></t></children></rel><rel id="33"><children><diamond id="34" x="700" y="1102" w="200" h="96"/><t id="35" x="775.2078247070312" y="1141.8279428482056"><a><text><string>Contains</string></text></a></t></children></rel><llabelUm id="36"><points><p colinear="true" x="1405.5" y="900" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2099.567268932415" y="900" 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="2300.4327310675844" y="900" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2994.5" y="900" 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="3205.5" y="924.3461538461538" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3681.861419777714" y="1034.2757122563955" 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="3801.942569236337" y="1073.6102587437897" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="4219.7" y="1263.5" 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="3130.0588235294117" y="936.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3771.254586641136" y="1715.0948552070938" 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="3830.8155961466423" y="1783.8971557613065" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="4266.818181818182" y="2263.5" 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="909.125" y="2463.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1089.1012874391326" y="1743.5948502434696" 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="1110.8987125608674" y="1656.4051497565304" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1290.875" y="936.5" 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="881.75" y="2463.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="669.6635816788919" y="2039.3271633577838" 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="630.3364183211081" y="1960.6728366422162" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="418.25" y="1536.5" 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="441.7142857142857" y="1463.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="764.0933786333908" y="1181.4182936957832" 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="849.5502233131613" y="1125.2248883434195" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1227" y="936.5" 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="1005.5" y="2519.181818181818" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1926.8736476037463" y="2686.7042995643174" 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="2073.1263523962534" y="2713.295700435682" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2994.5" y="2880.818181818182" 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="3100" y="936.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3100" y="1851.0984769425318" 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="3100" y="1948.9015230574682" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3100" y="2863.5" 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="950.1875" y="2536.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1409.7246828249872" y="2870.708860236354" 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="1490.2753171750128" y="2929.291139763646" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1949.8125" y="3263.5" 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="2999.625" y="2936.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2607.4943672237673" y="3079.0929573731755" 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="2492.5056327762327" y="3120.9070426268245" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2100.375" y="3263.5" 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="743.2922785541401" y="958.3511252801267" w="168" h="50"/><t id="74" x="797.7480738422261" y="975.1790681283322"><a><text><string>created_at</string></text></a></t></children></atr><llabel id="75"><points><p colinear="true" x="1194.5" y="918.6024964647431" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="900.7509062629122" y="970.986550739606" 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="769.9131883711252" y="697.7810492899115" w="168" h="50"/><t id="7a" x="837.8450807661447" y="714.6089921381171"><a><text><string>name</string></text></a></t></children></atr><llabel id="7b"><points><p colinear="true" x="1208.123998256316" y="863.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="905.5262393191721" y="743.5869651351443" 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="929.3638759826626" y="489.98086747757907" w="168" h="50"/><t id="80" x="992.95172846069" y="506.80881032578463"><a><text><string>symbol</string></text></a></t></children></atr><llabel id="81"><points><p colinear="true" x="1272.8267567949404" y="863.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1032.3862318241418" y="540.3607091332893" 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="1174.165243481124" y="396.82654491596213" w="168" h="50"/><t id="86" x="1252.5372069821005" y="413.6544877641677"><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="1296.8066637813038" y="863.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1260.8954272498743" y="447.81766203754734" 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="2622.807018741284" y="599.6833105514979" w="168" h="50"/><t id="8c" x="2677.26281402937" y="616.5112533997035"><a><text><string>created_at</string></text></a></t></children></atr><llabel id="8d"><points><p colinear="true" x="3047.872597753913" y="863.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2740.751005394063" y="648.6010421135588" 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="2851.830331203679" y="423.94754202276397" w="168" h="50"/><t id="92" x="2912.3521665308276" y="440.77548487096954"><a><text><string>is_active</string></text></a></t></children></atr><llabel id="93"><points><p colinear="true" x="3086.7150864492837" y="863.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2945.556088309162" y="474.7951013474514" 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="3140.2331416492098" y="411.3556033812472" w="168" h="50"/><t id="98" x="3180.4148356799715" y="428.18354622945276"><a><text><string>quote_currency</string></text></a></t></children></atr><llabel id="99"><points><p colinear="true" x="3109.780145523736" y="863.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3217.9226657928243" y="462.2726453009987" 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="3383.7013326971096" y="566.4619473504612" w="168" h="50"/><t id="9e" x="3462.073296198086" y="583.2898901986667"><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="3143.4990061296885" y="863.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3439.604766151636" y="615.9573157917046" 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="4452.666226574792" y="767.4676392594761" w="168" h="50"/><t id="a4" x="4531.038190075768" y="784.2955821076816"><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="4317.020229522657" y="1263.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="4525.39139321206" y="818.2188508937013" 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="4605.008687457039" y="872.1697118103555" w="168" h="50"/><t id="aa" x="4655.042455157234" y="888.997654658561"><a><text><string>executed_at</string></text></a></t></children></atr><llabel id="ab"><points><p colinear="true" x="4335.247640280459" y="1263.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="4665.867064869197" y="922.1513286672619" 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="4714.963653545486" y="1020.7653201458538" w="168" h="50"/><t id="b0" x="4784.983551006423" y="1037.5932629940594"><a><text><string>price</string></text></a></t></children></atr><llabel id="b1"><points><p colinear="true" x="4371.635283450938" y="1263.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="4756.402868587327" y="1068.2058859572421" 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="4770.550118495279" y="1197.0630634623633" w="168" h="50"/><t id="b6" x="4831.137963343912" y="1213.8910063105689"><a><text><string>quantity</string></text></a></t></children></atr><llabel id="b7"><points><p colinear="true" x="4405.5" y="1285.1729419388976" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="4778.449694434905" y="1233.3285509983655" 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="4765.711222730692" y="1381.853037410865" w="168" h="50"/><t id="bc" x="4838.215136610086" y="1398.6809802590706"><a><text><string>side</string></text></a></t></children></atr><llabel id="bd"><points><p colinear="true" x="4405.5" y="1320.5071226140292" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="4779.172573974651" y="1393.54452290494" 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="4700.974226119286" y="1555" w="168" h="50"/><t id="c2" x="4766.020086409813" y="1571.8279428482056"><a><text><string>source</string></text></a></t></children></atr><llabel id="c3"><points><p colinear="true" x="4363.219854476264" y="1336.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="4746.331444229757" y="1557.9009043392496" 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="4796.036983703456" y="2004.5243124859524" w="168" h="50.00000000000023"/><t id="c8" x="4874.4089472044325" y="2021.3522553341581"><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="4378.274502598599" y="2263.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="4834.627246895033" y="2051.4323743436007" 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="4847.613426468301" y="2171.731033195004" w="168" h="50"/><t id="ce" x="4902.003212234903" y="2188.5589760432094"><a><text><string>timeframe</string></text></a></t></children></atr><llabel id="cf"><points><p colinear="true" x="4405.5" y="2282.750721340985" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="4857.817249261719" y="2209.3784783507945" 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="4851.975814331675" y="2346.657264706117" w="168" h="50"/><t id="d4" x="4921.563712586069" y="2363.4852075543226"><a><text><string>open</string></text></a></t></children></atr><llabel id="d5"><points><p colinear="true" x="4405.5" y="2311.8869951594615" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="4857.313584645609" y="2363.23782355834" 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="4808.798052230598" y="2516.2270077574435" w="168" h="50"/><t id="da" x="4880.143968978645" y="2533.054950605649"><a><text><string>high</string></text></a></t></children></atr><llabel id="db"><points><p colinear="true" x="4389.696129415879" y="2336.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="4842.964078963906" y="2521.2446297975644" 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="4721.30774291215" y="2667.7646686669104" w="168" h="50"/><t id="e0" x="4795.413669425822" y="2684.592611515116"><a><text><string>low</string></text></a></t></children></atr><llabel id="e1"><points><p colinear="true" x="4346.958736586194" y="2336.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="4775.225026049766" y="2669.4933910170507" 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="4596.044918767041" y="2789.9425790506675" w="168" h="50"/><t id="e6" x="4665.728810063428" y="2806.770521898873"><a><text><string>close</string></text></a></t></children></atr><llabel id="e7"><points><p colinear="true" x="4326.938225929132" y="2336.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="4662.175157620797" y="2790.552436559406" 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="4458.075940733153" y="2873.6277560953818" w="168" h="50"/><t id="ec" x="4521.147794370849" y="2890.4556989435873"><a><text><string>volume</string></text></a></t></children></atr><llabel id="ed"><points><p colinear="true" x="4314.760043694587" y="2336.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="4532.340064663027" y="2873.8155358682397" 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="4256.0768991837995" y="2912.564606778717" w="168" h="50"/><t id="f2" x="4306.128664625694" y="2929.3925496269226"><a><text><string>candle_time</string></text></a></t></children></atr><llabel id="f3"><points><p colinear="true" x="4302.294366413467" y="2336.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="4338.974273978681" y="2912.5691934726883" 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="3653.564606778717" y="2819.220324641499" w="168" h="50"/><t id="f8" x="3731.9365702796936" y="2836.0482674897044"><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="3205.5" y="2890.769945998015" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3656.9064650340265" y="2851.8207419969895" 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="3647.6134264683014" y="2978.268966804996" w="168" h="50"/><t id="fe" x="3720.117340347696" y="2995.0969096532017"><a><text><string>side</string></text></a></t></children></atr><llabel id="ff"><points><p colinear="true" x="3205.5" y="2917.249278659015" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3657.8172492617186" y="2991.6215216492055" 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="3602.599843756785" y="3130.9308955646343" w="168" h="50"/><t id="104" x="3674.2997567816874" y="3147.75883841284"><a><text><string>type</string></text></a></t></children></atr><llabel id="105"><points><p colinear="true" x="3183.658888661668" y="2936.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3639.031281941171" y="3135.45879643282" 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="3521.307742912151" y="3267.7646686669104" w="168" h="50"/><t id="10a" x="3588.153621513225" y="3284.592611515116"><a><text><string>status</string></text></a></t></children></atr><llabel id="10b"><points><p colinear="true" x="3146.958736586195" y="2936.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3575.225026049766" y="3269.4933910170507" 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="3408.7646686669104" y="3380.3077429121504" w="168" h="50"/><t id="110" x="3469.3525135155433" y="3397.135685760356"><a><text><string>quantity</string></text></a></t></children></atr><llabel id="111"><points><p colinear="true" x="3128.3706525526854" y="2936.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3473.9678234376524" y="3380.9815655952934" 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="3318.5234092424926" y="3461.599843756785" w="168" h="50"/><t id="116" x="3388.54330670343" y="3478.4277866049906"><a><text><string>price</string></text></a></t></children></atr><llabel id="117"><points><p colinear="true" x="3118.8239130215816" y="2936.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3390.0288881184765" y="3461.903168629631" 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="3117.1400623390255" y="3506.6134264683014" w="168" h="50"/><t id="11c" x="3174.3858814308223" y="3523.441369316507"><a><text><string>placed_at</string></text></a></t></children></atr><llabel id="11d"><points><p colinear="true" x="3105.844733694178" y="2936.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3197.5615144263866" y="3506.6431473074886" 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="2915.756715429611" y="3512.564606778717" w="168" h="50"/><t id="122" x="2965.7904831298065" y="3529.3925496269226"><a><text><string>executed_at</string></text></a></t></children></atr><llabel id="123"><points><p colinear="true" x="3094.2611621662854" y="2936.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3004.261535044783" y="3512.5932621982847" 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="1370.9782799441732" y="3473.371683128888" w="168" h="50"/><t id="128" x="1449.3502434451498" y="3490.1996259770935"><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="1899.7170741899063" y="3336.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1509.411960600975" y="3479.2414287453935" 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="1527.9042483118624" y="3706.0239987768887" w="168" h="50"/><t id="12e" x="1599.6041613367647" y="3722.8519416250942"><a><text><string>type</string></text></a></t></children></atr><llabel id="12f"><points><p colinear="true" x="1967.1352523831288" y="3336.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1634.5611761574505" y="3706.916237421411" 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="1775.6853005521928" y="3837.771521240078" w="168" h="50"/><t id="134" x="1837.4971520536576" y="3854.5994640882836"><a><text><string>amount</string></text></a></t></children></atr><llabel id="135"><points><p colinear="true" x="1990.8995278962238" y="3336.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1866.5252440845063" y="3837.8433966009807" 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="2056.314699447807" y="3837.771521240078" w="168" h="50"/><t id="13a" x="2115.738527572807" y="3854.5994640882836"><a><text><string>currency</string></text></a></t></children></atr><llabel id="13b"><points><p colinear="true" x="2009.1004721037762" y="3336.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2134.4747559154935" y="3837.8433966009807" 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="2304.0957516881376" y="3706.0239987768887" w="168" h="50"/><t id="140" x="2358.5515469762236" y="3722.8519416250942"><a><text><string>created_at</string></text></a></t></children></atr><llabel id="141"><points><p colinear="true" x="2032.8647476168712" y="3336.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2366.4388238425495" y="3706.916237421411" 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="2461.021720055827" y="3473.371683128888" w="168" h="50"/><t id="146" x="2513.425490502604" y="3490.1996259770935"><a><text><string>description</string></text></a></t></children></atr><llabel id="147"><points><p colinear="true" x="2100.2829258100937" y="3336.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2491.588039399025" y="3479.2414287453935" 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="-25.20693663107295" y="2296.195945896727" w="168" h="50"/><t id="14c" x="53.16502686990361" y="2313.0238887449327"><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="794.5" y="2477.5752827438127" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="128.3765526507861" y="2336.380094846647" 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="-43.59888811957319" y="2501.2631213604473" w="168" h="50"/><t id="152" x="11.888912478571342" y="2518.091064208653"><a><text><string>username</string></text></a></t></children></atr><llabel id="153"><points><p colinear="true" x="794.5" y="2503.2233165280013" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="124.9713403648008" y="2524.194543569106" 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="-12.722189759415869" y="2704.825003427301" w="168" h="50"/><t id="158" x="55.82170214732241" y="2721.6529462755066"><a><text><string>email</string></text></a></t></children></atr><llabel id="159"><points><p colinear="true" x="794.5" y="2529.257739398313" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="133.99567258184928" y="2713.070463121132" 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="65.65343391739452" y="2895.2142676873814" w="168" h="50"/><t id="15e" x="122.05324470841015" y="2912.042210535587"><a><text><string>full_name</string></text></a></t></children></atr><llabel id="15f"><points><p colinear="true" x="834.8245603064811" y="2536.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="190.23774726825653" y="2898.265973603192" 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="187.0358166075133" y="3061.5185896537487" w="168" h="50"/><t id="164" x="227.3975109434508" y="3078.3465325019542"><a><text><string>password_hash</string></text></a></t></children></atr><llabel id="165"><points><p colinear="true" x="860.8585420840991" y="2536.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="297.5528032123524" y="3062.7573571488947" 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="344.4678226128738" y="3194.206094029073" w="167.99999999999994" h="50"/><t id="16a" x="378.9974933282058" y="3211.0340368772786"><a><text><string>available_balance</string></text></a></t></children></atr><llabel id="16b"><points><p colinear="true" x="876.0695513879581" y="2536.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="445.3684308903264" y="3194.691009195862" 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="528.3822006353158" y="3285.6716823392735" w="168" h="50"/><t id="170" x="563.9738678106088" y="3302.499625187479"><a><text><string>invested_balance</string></text></a></t></children></atr><llabel id="171"><points><p colinear="true" x="887.0501833164841" y="2536.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="621.8779195902513" y="3285.8165943191393" 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="730.3821674372552" y="3330.672925695784" w="168" h="50"/><t id="176" x="784.8379627253412" y="3347.50086854399"><a><text><string>created_at</string></text></a></t></children></atr><llabel id="177"><points><p colinear="true" x="896.3478441414994" y="2536.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="817.4325112010669" y="3330.6845426733403" 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="935.6888668256563" y="3326.6305391177502" w="168" h="50"/><t id="17c" x="987.798653812961" y="3343.458481965956"><a><text><string>updated_at</string></text></a></t></children></atr><llabel id="17d"><points><p colinear="true" x="905.1297404666373" y="2536.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1016.6082943801105" y="3326.65344234991" 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="29.211781824476816" y="1884.5760221444957" w="168" h="50"/><t id="182" x="83.66757711256275" y="1901.4039649927013"><a><text><string>created_at</string></text></a></t></children></atr><llabel id="183"><points><p colinear="true" x="374.44242485534556" y="1536.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="131.18132620955708" y="1885.1269271522506" 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="-176.40387650610398" y="1388.175911166535" w="168" h="50"/><t id="188" x="-108.47198411108445" y="1405.0038540147405"><a><text><string>name</string></text></a></t></children></atr><llabel id="189"><points><p colinear="true" x="294.5" y="1481.3975035352569" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="-18.945248797331914" y="1426.5404857070557" 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="186.59047744873953" y="992.0370868554658" w="168" h="50"/><t id="18e" x="264.9624409497161" y="1008.8650297036713"><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="390.219854476264" y="1463.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="277.900953305125" y="1042.9541287752172" 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="1449.0127018922194" y="1425" w="168" h="50"/><t id="194" x="1509.6005467408522" y="1441.8279428482056"><a><text><string>quantity</string></text></a></t></children></atr><llabel id="195"><points><p colinear="true" x="1145.9489148679422" y="1673.4713816320225" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1494.3699200026902" y="1473.0990956607504" 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="1508.403876506104" y="1588.175911166535" w="168" h="50"><a><fillColor><color rgba="#ffffebeb"/></fillColor><strokeDashes><doubleArray><double>5</double></doubleArray></strokeDashes></a></e><t id="19a" x="1565.6556923020025" y="1605.0038540147405"><a><text><string>avg_price</string></text></a></t></children></atrderivado><llabel id="19b"><points><p colinear="true" x="1173.7317959892898" y="1686.999095030996" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1519.945248797332" y="1626.5404857070557" 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="1508.403876506104" y="1761.824088833465" w="168" h="50"/><t id="1a0" x="1562.85967179419" y="1778.6520316816707"><a><text><string>created_at</string></text></a></t></children></atr><llabel id="1a1"><points><p colinear="true" x="1173.7317959892898" y="1713.000904969004" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1519.945248797332" y="1774.4595142929443" 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="1449.0127018922194" y="1925" w="168" h="50"/><t id="1a6" x="1501.122488879524" y="1941.8279428482056"><a><text><string>updated_at</string></text></a></t></children></atr><llabel id="1a7"><points><p colinear="true" x="1145.9489148679422" y="1726.5286183679775" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1494.3699200026902" y="1927.9009043392496" 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="403.8791732994945" y="843.9651453292795" w="168" h="50"/><t id="1ac" x="461.86299898308823" y="860.7930881774851"><a><text><string>added_at</string></text></a></t></children></atr><llabel id="1ad"><points><p colinear="true" x="764.7508898645601" y="1118.26155867615" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="515.2317585773111" y="893.6433217132783" 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></figures></drawing>
Index: cs/P1-ConceptualModel/ERModel_v02.xml
===================================================================
--- docs/P1-ConceptualModel/ERModel_v02.xml	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,1 +1,0 @@
-<drawing><figures><ent id="0"><children><r id="1" x="831" y="612" w="210" h="72"/><t id="2" x="914.3098373413086" y="639.8279428482056"><a><text><string>Cryptos</string></text></a></t></children></ent><ent id="3"><children><r id="4" x="2127" y="612" w="210" h="72"/><t id="5" x="2209.0858306884766" y="639.8279428482056"><a><text><string>Markets</string></text></a></t></children></ent><ent id="6"><children><r id="7" x="2991" y="900" w="210" h="72"/><t id="8" x="3056.831718444824" y="927.8279428482056"><a><text><string>MarketTrades</string></text></a></t></children></ent><ent id="9"><children><r id="a" x="2991" y="1620" w="210" h="72"/><t id="b" x="3053.5976943969727" y="1647.8279428482056"><a><text><string>MarketCandles</string></text></a></t></children></ent><ent id="c"><children><r id="d" x="2127" y="2052" w="210" h="72"/><t id="e" x="2212.4098510742188" y="2079.8279428482056"><a><text><string>Orders</string></text></a></t></children></ent><ent id="f"><children><r id="10" x="1335" y="2340" w="210" h="72"/><t id="11" x="1404.0657501220703" y="2367.8279428482056"><a><text><string>Transactions</string></text></a></t></children></ent><ent id="12"><children><r id="13" x="543" y="1764" w="210" h="72"/><t id="14" x="632.0038757324219" y="1791.8279428482056"><a><text><string>Users</string></text></a></t></children></ent><ent id="15"><children><r id="16" x="183" y="1044" w="210" h="72"/><t id="17" x="259.289794921875" y="1071.8279428482056"><a><text><string>Watchlists</string></text></a></t></children></ent><rel id="18"><children><diamond id="19" x="1484" y="600" w="200" h="96"/><t id="1a" x="1554.3417892456055" y="639.8279428482056"><a><text><string>QuotedOn</string></text></a></t></children></rel><rel id="1b"><children><diamond id="1c" x="2600" y="708" w="200" h="96"/><t id="1d" x="2689.367919921875" y="747.8279428482056"><a><text><string>Fills</string></text></a></t></children></rel><rel id="1e"><children><diamond id="1f" x="2636" y="1212" w="200" h="96"/><t id="20" x="2703.44376373291" y="1251.8279428482056"><a><text><string>Aggregates</string></text></a></t></children></rel><rel id="21"><children><diamond id="22" x="2132" y="1320" w="200" h="96"/><t id="23" x="2205.107810974121" y="1359.8279428482056"><a><text><string>PlacedOn</string></text></a></t></children></rel><rel id="24"><children><diamond id="25" x="1736" y="2184" w="200" h="96"/><t id="26" x="1817.1838607788086" y="2223.8279428482056"><a><text><string>Settles</string></text></a></t></children></rel><rel id="27"><children><diamond id="28" x="944" y="2040" w="200" h="96"/><t id="29" x="1021.3318328857422" y="2079.8279428482056"><a><text><string>Records</string></text></a></t></children></rel><rel id="2a"><children><diamond id="2b" x="1340" y="1896" w="200" h="96"/><t id="2c" x="1422.31787109375" y="1935.8279428482056"><a><text><string>Places</string></text></a></t></children></rel><rel id="2d"><children><diamond id="2e" x="692" y="1176" w="200" h="96"/><t id="2f" x="775.811882019043" y="1215.8279428482056"><a><text><string>Holds</string></text></a></t></children></rel><rel id="30"><children><diamond id="31" x="368" y="1392" w="200" h="96"/><t id="32" x="452.01588439941406" y="1431.8279428482056"><a><text><string>Owns</string></text></a></t></children></rel><rel id="33"><children><diamond id="34" x="476" y="780" w="200" h="96"/><t id="35" x="551.2078247070312" y="819.8279428482056"><a><text><string>Contains</string></text></a></t></children></rel><llabelUm id="36"><points><p colinear="true" x="1041.5" y="648" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1483.5672689324151" y="648" 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="1684.4327310675847" y="648" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2126.5" y="648" 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="2337.5" y="672.3461538461538" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2631.861419777714" y="740.2757122563955" 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="2751.942569236337" y="779.6102587437896" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3015.7" y="899.5" 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="2262.0588235294117" y="684.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2707.254586641136" y="1225.0948552070938" 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="2766.8155961466423" y="1293.8971557613065" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3062.818181818182" y="1619.5" 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="657.125" y="1763.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="781.1012874391325" y="1267.5948502434696" 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="802.8987125608675" y="1180.4051497565304" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="926.875" y="684.5" 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="629.75" y="1763.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="487.66358167889183" y="1479.3271633577838" 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="448.33641832110817" y="1400.6728366422162" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="306.25" y="1116.5" 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="329.7142857142857" y="1043.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="540.0933786333907" y="859.4182936957832" 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="625.5502233131613" y="803.2248883434194" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="863" y="684.5" 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="753.5" y="1819.1818181818182" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1366.8736476037463" y="1930.7042995643176" 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="1513.1263523962534" y="1957.2957004356824" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2126.5" y="2068.818181818182" 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="2232" y="684.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2232" y="1319.0984769425318" 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="2232" y="1416.9015230574682" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2232" y="2051.5" 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="698.1875" y="1836.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1003.7246828249872" y="2058.708860236354" 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="1084.2753171750128" y="2117.291139763646" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1389.8125" y="2339.5" 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="2131.625" y="2124.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1893.4943672237673" y="2211.0929573731755" 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="1778.5056327762327" y="2252.9070426268245" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1540.375" y="2339.5" 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="483.2879772722292" y="688.0138777184987" w="168" h="50"/><t id="74" x="537.7437725603152" y="704.8418205667043"><a><text><string>created_at</string></text></a></t></children></atr><llabel id="75"><points><p colinear="true" x="830.5" y="666.602496464743" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="640.7466049810013" y="700.649303177978" 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="504.05228692947753" y="484.769218446131" w="168" h="49.99999999999994"/><t id="7a" x="571.9841793244971" y="501.5971612943365"><a><text><string>name</string></text></a></t></children></atr><llabel id="7b"><points><p colinear="true" x="844.1239982563161" y="611.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="639.6653378775244" y="530.5751342913637" 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="628.4238232664768" y="324.35488446364536" w="168" h="50"/><t id="80" x="692.0116757445041" y="341.18282731185093"><a><text><string>symbol</string></text></a></t></children></atr><llabel id="81"><points><p colinear="true" x="908.6748236410376" y="611.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="731.5447268935441" y="374.72802612097394" 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="819.3688899152768" y="248.35489720331674" w="168" h="50"/><t id="86" x="897.7408534162533" y="265.1828400515223"><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="932.8208966053434" y="611.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="906.0891405457884" y="299.3460932902019" 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="1841.3094746182014" y="408.2529822301684" w="168" h="50"/><t id="8c" x="1895.7652699062874" y="425.080925078374"><a><text><string>created_at</string></text></a></t></children></atr><llabel id="8d"><points><p colinear="true" x="2179.872597753913" y="611.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1959.2534612709806" y="457.1707137922293" 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="2019.9476583388696" y="271.17908277775587" w="168" h="50"/><t id="92" x="2080.469493666018" y="288.00702562596143"><a><text><string>is_active</string></text></a></t></children></atr><llabel id="93"><points><p colinear="true" x="2218.7150864492837" y="611.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2113.6734154443525" y="322.0266421024433" 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="2244.901850486384" y="261.3573706373728" w="168" h="50"/><t id="98" x="2285.0835445171456" y="278.1853134855784"><a><text><string>quote_currency</string></text></a></t></children></atr><llabel id="99"><points><p colinear="true" x="2241.780145523736" y="611.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2322.5913746299984" y="312.2744125571243" 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="2434.8070395037453" y="382.34031893335964" w="168" h="50"/><t id="9e" x="2513.179003004722" y="399.1682617815652"><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="2275.4990061296885" y="611.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2490.7104729582716" y="431.8356873746031" 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="3196.5996567283373" y="515.1247586223913" w="168" h="50"/><t id="a4" x="3274.971620229314" y="531.9527014705968"><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="3113.0202295226572" y="899.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3269.3248233656054" y="565.8759702566165" 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="3315.42677621649" y="596.7923752120772" w="168" h="50"/><t id="aa" x="3365.4605439166853" y="613.6203180602828"><a><text><string>executed_at</string></text></a></t></children></atr><llabel id="ab"><points><p colinear="true" x="3131.247640280458" y="899.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3376.285153628648" y="646.7739920689836" 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="3401.1916497654793" y="712.696949713766" w="168" h="50"/><t id="b0" x="3471.2115472264168" y="729.5248925619716"><a><text><string>price</string></text></a></t></children></atr><llabel id="b1"><points><p colinear="true" x="3167.635283450938" y="899.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3442.6308648073214" y="760.1375155251544" 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="3444.549092426318" y="850.2091895006434" w="168" h="50"/><t id="b6" x="3505.136937274951" y="867.037132348849"><a><text><string>quantity</string></text></a></t></children></atr><llabel id="b7"><points><p colinear="true" x="3201.5" y="921.1729419388977" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3452.448668365944" y="886.4746770366456" 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="3440.7747537299397" y="994.3453691804748" w="168" h="50"/><t id="bc" x="3513.278667609334" y="1011.1733120286804"><a><text><string>side</string></text></a></t></children></atr><llabel id="bd"><points><p colinear="true" x="3201.5" y="956.5071226140293" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3454.236104973899" y="1006.0368546745498" 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="3390.279896373043" y="1129.4" w="168" h="50"/><t id="c2" x="3455.3257566635702" y="1146.2279428482057"><a><text><string>source</string></text></a></t></children></atr><llabel id="c3"><points><p colinear="true" x="3159.219854476264" y="972.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3435.6371144835134" y="1132.3009043392497" 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="3464.4288472886956" y="1420.0289637390429" w="168" h="50"/><t id="c8" x="3542.800810789672" y="1436.8569065872484"><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="3174.2745025985987" y="1619.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3503.019110480273" y="1466.9370255966908" 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="3504.658472645275" y="1550.450205892103" w="168" h="50"/><t id="ce" x="3559.0482584118768" y="1567.2781487403086"><a><text><string>timeframe</string></text></a></t></children></atr><llabel id="cf"><points><p colinear="true" x="3201.5" y="1638.750721340985" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3514.8622954386924" y="1588.0976510478936" 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="3508.0611351787065" y="1686.8926664707712" w="168" h="50"/><t id="d4" x="3577.649033433101" y="1703.7206093189768"><a><text><string>open</string></text></a></t></children></atr><llabel id="d5"><points><p colinear="true" x="3201.5" y="1667.8869951594618" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3513.39890549264" y="1703.4732253229943" 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="3474.382480739866" y="1819.157066050806" w="168" h="50"/><t id="da" x="3545.728397487913" y="1835.9850088990115"><a><text><string>high</string></text></a></t></children></atr><llabel id="db"><points><p colinear="true" x="3185.6961294158787" y="1692.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3508.5485074731737" y="1824.1746880909268" 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="3406.1400394714774" y="1937.35644156019" w="168" h="50"/><t id="e0" x="3480.2459659851493" y="1954.1843844083955"><a><text><string>low</string></text></a></t></children></atr><llabel id="e1"><points><p colinear="true" x="3142.958736586195" y="1692.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3460.0573226090924" y="1939.0851639103305" 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="3308.435036638292" y="2027.292451164345" w="168" h="50"/><t id="e6" x="3378.1189279346786" y="2044.1203940125506"><a><text><string>close</string></text></a></t></children></atr><llabel id="e7"><points><p colinear="true" x="3123.3027629103403" y="1692.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3374.3286659462065" y="2027.9183191033765" 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="3223.0394760805693" y="2103.2924102495735" w="168" h="50"/><t id="ec" x="3286.1113297182646" y="2120.120353097779"><a><text><string>volume</string></text></a></t></children></atr><llabel id="ed"><points><p colinear="true" x="3112.3096859271363" y="1692.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3296.2472404503374" y="2103.521132657545" 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="3021.039739054654" y="2128.3003932873994" w="168" h="50"/><t id="f2" x="3071.0915044965486" y="2145.128336135605"><a><text><string>candle_time</string></text></a></t></children></atr><llabel id="f3"><points><p colinear="true" x="3096.663483238599" y="1692.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3105.07621664273" y="2128.300776942971" 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="2645.3003932873994" y="2019.491853220369" w="168" h="50.00000000000023"/><t id="f8" x="2723.672356788376" y="2036.3197960685748"><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="2337.5" y="2078.769945998015" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2648.642251542709" y="2052.09227057586" 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="2640.658472645275" y="2143.549794107897" w="168" h="50"/><t id="fe" x="2713.1623865246697" y="2160.3777369561026"><a><text><string>side</string></text></a></t></children></atr><llabel id="ff"><points><p colinear="true" x="2337.5" y="2105.249278659015" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2650.8622954386924" y="2156.9023489521064" 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="2605.5478781302922" y="2262.6260985404147" w="168" h="50"/><t id="104" x="2677.2477911551946" y="2279.45404138862"><a><text><string>type</string></text></a></t></children></atr><llabel id="105"><points><p colinear="true" x="2315.658888661668" y="2124.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2641.9793163146783" y="2267.1539994086" 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="2542.1400394714774" y="2369.35644156019" w="168" h="50"/><t id="10a" x="2608.9859180725516" y="2386.1843844083955"><a><text><string>status</string></text></a></t></children></atr><llabel id="10b"><points><p colinear="true" x="2278.958736586195" y="2124.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2596.0573226090924" y="2371.0851639103303" 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="2454.35644156019" y="2450.8439828185265" w="168" h="50"/><t id="110" x="2514.944286408823" y="2467.671925666732"><a><text><string>quantity</string></text></a></t></children></atr><llabel id="111"><points><p colinear="true" x="2260.83120690873" y="2124.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2519.2630564043266" y="2451.5389664883755" 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="2428.435791376831" y="2526.843934783243" w="168" h="50"/><t id="116" x="2498.4556888377683" y="2543.6718776314488"><a><text><string>price</string></text></a></t></children></atr><llabel id="117"><points><p colinear="true" x="2254.0675654410306" y="2124.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2497.7690944556634" y="2527.2580484809273" 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="2226.889248622918" y="2555.658472645275" w="168" h="50"/><t id="11c" x="2284.1350677147148" y="2572.4864154934808"><a><text><string>placed_at</string></text></a></t></children></atr><llabel id="11d"><points><p colinear="true" x="2237.8447336940653" y="2124.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2307.3107007103577" y="2555.688193484461" 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="2025.3427058689322" y="2560.3003932873994" w="168" h="50"/><t id="122" x="2075.3764735691275" y="2577.128336135605"><a><text><string>executed_at</string></text></a></t></children></atr><llabel id="123"><points><p colinear="true" x="2222.997410627028" y="2124.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2116.1148360588404" y="2560.370737168798" 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="930.883058356455" y="2505.7299128405325" w="168" h="50"/><t id="128" x="1009.2550218574315" y="2522.557855688738"><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="1339.7170741899063" y="2412.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1069.316739013257" y="2511.599658457038" 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="1053.2853136832525" y="2687.198719045973" w="168" h="50"/><t id="12e" x="1124.9852267081549" y="2704.0266618941787"><a><text><string>type</string></text></a></t></children></atr><llabel id="12f"><points><p colinear="true" x="1407.1352523831288" y="2412.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1159.9422415288407" y="2688.0909576904955" 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="1246.5545344307102" y="2789.961786567261" w="168" h="50"/><t id="134" x="1308.366385932175" y="2806.7897294154664"><a><text><string>amount</string></text></a></t></children></atr><llabel id="135"><points><p colinear="true" x="1430.8995278962238" y="2412.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1337.3944779630237" y="2790.0336619281634" 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="1465.4454655692896" y="2789.961786567261" w="168" h="50"/><t id="13a" x="1524.8692936942896" y="2806.7897294154664"><a><text><string>currency</string></text></a></t></children></atr><llabel id="13b"><points><p colinear="true" x="1449.1004721037762" y="2412.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1543.605522036976" y="2790.0336619281634" 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="1658.7146863167472" y="2687.198719045973" w="168" h="50"/><t id="140" x="1713.1704816048332" y="2704.0266618941787"><a><text><string>created_at</string></text></a></t></children></atr><llabel id="141"><points><p colinear="true" x="1472.8647476168712" y="2412.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1721.057758471159" y="2688.0909576904955" 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="1781.116941643545" y="2505.7299128405325" w="168" h="50"/><t id="146" x="1833.5207120903224" y="2522.557855688738"><a><text><string>description</string></text></a></t></children></atr><llabel id="147"><points><p colinear="true" x="1540.2829258100937" y="2412.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1811.683260986743" y="2511.599658457038" 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="-92.14141057223696" y="1635.532837799447" w="168" h="50"/><t id="14c" x="-13.769447071260402" y="1652.3607806476525"><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="542.5" y="1777.5752827438127" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="61.44207870962204" y="1675.7169867493667" 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="-106.48713273326712" y="1795.485234661149" w="168" h="50"/><t id="152" x="-50.999332135122586" y="1812.3131775093545"><a><text><string>username</string></text></a></t></children></atr><llabel id="153"><points><p colinear="true" x="542.5" y="1803.223316528001" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="62.08309575110687" y="1818.4166568698076" 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="-82.40330801234438" y="1954.2635026732946" w="168" h="50"/><t id="158" x="-13.859416105606101" y="1971.0914455215002"><a><text><string>email</string></text></a></t></children></atr><llabel id="159"><points><p colinear="true" x="542.5" y="1829.2577393983129" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="64.31455432892079" y="1962.5089623671254" 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="-21.270321544432363" y="2102.7671287961575" w="168" h="50"/><t id="15e" x="35.12948924658326" y="2119.595071644363"><a><text><string>full_name</string></text></a></t></children></atr><llabel id="15f"><points><p colinear="true" x="582.8245603064811" y="1836.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="103.31399180642964" y="2105.818834711968" 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="73.40793695386031" y="2232.484499929924" w="168" h="50"/><t id="164" x="113.76963128979781" y="2249.3124427781295"><a><text><string>password_hash</string></text></a></t></children></atr><llabel id="165"><points><p colinear="true" x="608.8585420840991" y="1836.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="183.9249235586994" y="2233.72326742507" 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="196.20490163804124" y="2333.6523416658747" w="168" h="50"/><t id="16a" x="230.73457235337327" y="2350.4802845140803"><a><text><string>available_balance</string></text></a></t></children></atr><llabel id="16b"><points><p colinear="true" x="623.9698114749144" y="1836.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="297.1712755917845" y="2334.1411916832512" 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="296.32230639872284" y="2409.6523239014355" w="168" h="50"/><t id="170" x="331.9139735740158" y="2426.480266749641"><a><text><string>invested_balance</string></text></a></t></children></atr><llabel id="171"><points><p colinear="true" x="632.605372975891" y="1836.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="391.4913835562713" y="2409.856399833859" 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="498.0778410735205" y="2442.4248820427115" w="168" h="50"/><t id="176" x="552.5336363616065" y="2459.252824890917"><a><text><string>created_at</string></text></a></t></children></atr><llabel id="177"><points><p colinear="true" x="644.3948620053657" y="1836.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="585.0953810392355" y="2442.43620202953" 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="699.8333757483744" y="2439.2718205118454" w="168" h="50"/><t id="17c" x="751.943162735679" y="2456.099763360051"><a><text><string>updated_at</string></text></a></t></children></atr><llabel id="17d"><points><p colinear="true" x="655.463688902226" y="1836.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="779.1289174237263" y="2439.3202333224326" 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="-19.694810176908106" y="1374.4692972727066" w="168" h="50"/><t id="182" x="34.76098511117783" y="1391.2972401209122"><a><text><string>created_at</string></text></a></t></children></atr><llabel id="183"><points><p colinear="true" x="262.44242485534556" y="1116.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="82.27473420817215" y="1375.0202022804615" 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="-180.07502367476116" y="987.2772107098972" w="168" h="50"/><t id="188" x="-112.14313127974162" y="1004.1051535581028"><a><text><string>name</string></text></a></t></children></atr><llabel id="189"><points><p colinear="true" x="182.5" y="1061.3975035352569" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="-22.616395965989085" y="1025.641785250418" 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="103.0605724100169" y="678.2889277472634" w="168" h="50"/><t id="18e" x="181.43253591099347" y="695.116870595469"><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="278.219854476264" y="1043.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="194.3710482664023" y="729.205969667015" 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="1045.7499074759312" y="1004" w="168" h="50"/><t id="194" x="1106.337752324564" y="1020.8279428482056"><a><text><string>quantity</string></text></a></t></children></atr><llabel id="195"><points><p colinear="true" x="837.9489148679422" y="1197.4713816320225" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1091.107125586402" y="1052.0990956607504" 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="1092.0750236747613" y="1131.2772107098972" w="168" h="50"><a><fillColor><color rgba="#ffffebeb"/></fillColor><strokeDashes><doubleArray><double>5</double></doubleArray></strokeDashes></a></e><t id="19a" x="1149.3268394706597" y="1148.1051535581028"><a><text><string>avg_price</string></text></a></t></children></atrderivado><llabel id="19b"><points><p colinear="true" x="865.7317959892898" y="1210.999095030996" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1103.616395965989" y="1169.641785250418" 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="1092.0750236747613" y="1266.7227892901028" w="168" h="50"/><t id="1a0" x="1146.5308189628472" y="1283.5507321383084"><a><text><string>created_at</string></text></a></t></children></atr><llabel id="1a1"><points><p colinear="true" x="865.7317959892898" y="1237.000904969004" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1103.616395965989" y="1279.358214749582" 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="1045.7499074759312" y="1394" w="168" h="50"/><t id="1a6" x="1097.8596944632359" y="1410.8279428482056"><a><text><string>updated_at</string></text></a></t></children></atr><llabel id="1a7"><points><p colinear="true" x="837.9489148679422" y="1250.5286183679775" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1091.107125586402" y="1396.9009043392496" 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="248.5457551736057" y="583.792813356838" w="168" h="50"/><t id="1ac" x="306.52958085719945" y="600.6207562050436"><a><text><string>added_at</string></text></a></t></children></atr><llabel id="1ad"><points><p colinear="true" x="540.7508898645601" y="796.2615586761499" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="359.8983404514223" y="633.4709897408368" 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></figures></drawing>
Index: cs/P1-ConceptualModel/ERModel_v03.xml
===================================================================
--- docs/P1-ConceptualModel/ERModel_v03.xml	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,1 +1,0 @@
-<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: cs/P1-ConceptualModel/ERModel_v04.xml
===================================================================
--- docs/P1-ConceptualModel/ERModel_v04.xml	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,1 +1,0 @@
-<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><atr id="1b6" nullable="false" attributeType="VARCHAR2(128)"><children><e id="1b7" x="300" y="1330" w="168" h="50"/><t id="1b8" x="326.4" y="1346.8"><a><text><string>reserved_balance</string></text></a></t></children></atr><llabel id="1b9"><points><p colinear="true" x="992.4" y="1656.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="384.0" y="1355.0" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="1ba"><Owner><r ref="13"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="1bb"><Owner><e ref="1b7"/></Owner></ellipseConnector></endConnector></llabel><atr id="1bc" nullable="false" attributeType="VARCHAR2(128)"><children><e id="1bd" x="2380" y="1690" w="168" h="50"/><t id="1be" x="2410.0" y="1706.8"><a><text><string>filled_quantity</string></text></a></t></children></atr><llabel id="1bf"><points><p colinear="true" x="2022.0" y="1843.7" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2464.0" y="1715.0" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="1c0"><Owner><r ref="d"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="1c1"><Owner><e ref="1bd"/></Owner></ellipseConnector></endConnector></llabel><rel id="1c2"><children><diamond id="1c3" x="2130" y="1420" w="200" h="96"/><t id="1c4" x="2197.2" y="1459.8"><a><text><string>FillsBuy</string></text></a></t></children></rel><llabelUm id="1c5"><points><p colinear="true" x="2022.0" y="1843.7" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2230.0" y="1468.0" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="1c6"><Owner><r ref="d"/></Owner></rConnector></startConnector><endConnector><diamondConnector id="1c7"><Owner><diamond ref="1c3"/></Owner></diamondConnector></endConnector></llabelUm><llabelMuitos id="1c8"><points><p colinear="true" x="2230.0" y="1468.0" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2583.6" y="1094.9" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><diamondConnector id="1c9"><Owner><diamond ref="1c3"/></Owner></diamondConnector></startConnector><endConnector><rConnector id="1ca"><Owner><r ref="7"/></Owner></rConnector></endConnector></llabelMuitos><rel id="1cb"><children><diamond id="1cc" x="2130" y="1570" w="200" h="96"/><t id="1cd" x="2193.1" y="1609.8"><a><text><string>FillsSell</string></text></a></t></children></rel><llabelUm id="1ce"><points><p colinear="true" x="2022.0" y="1843.7" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2230.0" y="1618.0" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="1cf"><Owner><r ref="d"/></Owner></rConnector></startConnector><endConnector><diamondConnector id="1d0"><Owner><diamond ref="1cc"/></Owner></diamondConnector></endConnector></llabelUm><llabelMuitos id="1d1"><points><p colinear="true" x="2230.0" y="1618.0" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2583.6" y="1094.9" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><diamondConnector id="1d2"><Owner><diamond ref="1cc"/></Owner></diamondConnector></startConnector><endConnector><rConnector id="1d3"><Owner><r ref="7"/></Owner></rConnector></endConnector></llabelMuitos><rel id="1d4"><children><diamond id="1d5" x="1922" y="2560" w="200" h="96"/><t id="1d6" x="2005.6" y="2599.8"><a><text><string>Logs</string></text></a></t></children></rel><ent id="1d7"><children><r id="1d8" x="1917" y="2780" w="210" h="72"/><t id="1d9" x="1976.9" y="2807.8"><a><text><string>OrderEvents</string></text></a></t></children></ent><llabelUm id="1da"><points><p colinear="true" x="2022.0" y="1843.7" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2022.0" y="2608.0" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="1db"><Owner><r ref="d"/></Owner></rConnector></startConnector><endConnector><diamondConnector id="1dc"><Owner><diamond ref="1d5"/></Owner></diamondConnector></endConnector></llabelUm><llabelDoubleMuitos id="1dd"><points><p colinear="true" x="2022.0" y="2608.0" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2022.0" y="2816.0" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><diamondConnector id="1de"><Owner><diamond ref="1d5"/></Owner></diamondConnector></startConnector><endConnector><rConnector id="1df"><Owner><r ref="1d8"/></Owner></rConnector></endConnector><a><innerStrokeWidthFactor><double>3</double></innerStrokeWidthFactor></a></llabelDoubleMuitos><atrchave id="1e0" nullable="false" attributeType="NUMBER"><children><e id="1e1" x="1640" y="2700" w="168" h="50"/><t id="1e2" x="1716.8" y="2716.8"><a><fontUnderlined><boolean>true</boolean></fontUnderlined><fontBold><boolean>true</boolean></fontBold><text><string>id</string></text></a></t></children></atrchave><llabel id="1e3"><points><p colinear="true" x="2022.0" y="2816.0" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1724.0" y="2725.0" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="1e4"><Owner><r ref="1d8"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="1e5"><Owner><e ref="1e1"/></Owner></ellipseConnector></endConnector></llabel><atr id="1e6" nullable="false" attributeType="VARCHAR2(128)"><children><e id="1e7" x="1620" y="2860" w="168" h="50"/><t id="1e8" x="1668.0" y="2876.8"><a><text><string>event_type</string></text></a></t></children></atr><llabel id="1e9"><points><p colinear="true" x="2022.0" y="2816.0" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1704.0" y="2885.0" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="1ea"><Owner><r ref="1d8"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="1eb"><Owner><e ref="1e7"/></Owner></ellipseConnector></endConnector></llabel><atr id="1ec" nullable="false" attributeType="VARCHAR2(128)"><children><e id="1ed" x="1760" y="2960" w="168" h="50"/><t id="1ee" x="1815.2" y="2976.8"><a><text><string>quantity</string></text></a></t></children></atr><llabel id="1ef"><points><p colinear="true" x="2022.0" y="2816.0" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1844.0" y="2985.0" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="1f0"><Owner><r ref="1d8"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="1f1"><Owner><e ref="1ed"/></Owner></ellipseConnector></endConnector></llabel><atr id="1f2" nullable="false" attributeType="VARCHAR2(128)"><children><e id="1f3" x="1940" y="3000" w="168" h="50"/><t id="1f4" x="2006.0" y="3016.8"><a><text><string>price</string></text></a></t></children></atr><llabel id="1f5"><points><p colinear="true" x="2022.0" y="2816.0" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2024.0" y="3025.0" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="1f6"><Owner><r ref="1d8"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="1f7"><Owner><e ref="1f3"/></Owner></ellipseConnector></endConnector></llabel><atr id="1f8" nullable="false" attributeType="VARCHAR2(128)"><children><e id="1f9" x="2120" y="2960" w="168" h="50"/><t id="1fa" x="2160.8" y="2976.8"><a><text><string>status_after</string></text></a></t></children></atr><llabel id="1fb"><points><p colinear="true" x="2022.0" y="2816.0" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2204.0" y="2985.0" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="1fc"><Owner><r ref="1d8"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="1fd"><Owner><e ref="1f9"/></Owner></ellipseConnector></endConnector></llabel><atr id="1fe" nullable="false" attributeType="VARCHAR2(128)"><children><e id="1ff" x="2270" y="2860" w="168" h="50"/><t id="200" x="2318.0" y="2876.8"><a><text><string>created_at</string></text></a></t></children></atr><llabel id="201"><points><p colinear="true" x="2022.0" y="2816.0" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2354.0" y="2885.0" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="202"><Owner><r ref="1d8"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="203"><Owner><e ref="1ff"/></Owner></ellipseConnector></endConnector></llabel></figures></drawing>
Index: cs/P1-ConceptualModel/ERModel_v05.xml
===================================================================
--- docs/P1-ConceptualModel/ERModel_v05.xml	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,1 +1,0 @@
-<drawing><figures><ent id="0"><children><r id="1" x="255" y="396" w="210" h="72"/><t id="2" x="318.095703125" y="423.82794284820557"><a><text><string>WatchlistItems</string></text></a></t></children></ent><ent id="3"><children><r id="4" x="1263" y="396" w="210" h="72"/><t id="5" x="1346.3098373413086" y="423.82794284820557"><a><text><string>Cryptos</string></text></a></t></children></ent><ent id="6"><children><r id="7" x="2271" y="396" w="210" h="72"/><t id="8" x="2353.0858306884766" y="423.82794284820557"><a><text><string>Markets</string></text></a></t></children></ent><ent id="9"><children><r id="a" x="3279" y="396" w="210" h="72"/><t id="b" x="3341.5976943969727" y="423.82794284820557"><a><text><string>MarketCandles</string></text></a></t></children></ent><ent id="c"><children><r id="d" x="255" y="1188" w="210" h="72"/><t id="e" x="331.289794921875" y="1215.8279428482056"><a><text><string>Watchlists</string></text></a></t></children></ent><ent id="f"><children><r id="10" x="1263" y="1188" w="210" h="72"/><t id="11" x="1342.865821838379" y="1215.8279428482056"><a><text><string>Holdings</string></text></a></t></children></ent><ent id="12"><children><r id="13" x="3279" y="1332" w="210" h="72"/><t id="14" x="3344.831718444824" y="1359.8279428482056"><a><text><string>MarketTrades</string></text></a></t></children></ent><ent id="15"><children><r id="16" x="255" y="2052" w="210" h="72"/><t id="17" x="344.0038757324219" y="2079.8279428482056"><a><text><string>Users</string></text></a></t></children></ent><ent id="18"><children><r id="19" x="2271" y="2052" w="210" h="72"/><t id="1a" x="2356.4098510742188" y="2079.8279428482056"><a><text><string>Orders</string></text></a></t></children></ent><ent id="1b"><children><r id="1c" x="1263" y="2916" w="210" h="72"/><t id="1d" x="1332.0657501220703" y="2943.8279428482056"><a><text><string>Transactions</string></text></a></t></children></ent><ent id="1e"><children><r id="1f" x="2271" y="2916" w="210" h="72"/><t id="20" x="2340.7677459716797" y="2943.8279428482056"><a><text><string>OrderEvents</string></text></a></t></children></ent><rel id="21"><children><diamond id="22" x="764" y="384" w="200" h="96"/><t id="23" x="851.3939056396484" y="423.82794284820557"><a><text><string>Lists</string></text></a></t></children></rel><rel id="24"><children><diamond id="25" x="260" y="780" w="200" h="96"/><t id="26" x="335.20782470703125" y="819.8279428482056"><a><text><string>Contains</string></text></a></t></children></rel><rel id="27"><children><diamond id="28" x="1772" y="384" w="200" h="96"/><t id="29" x="1842.3417892456055" y="423.82794284820557"><a><text><string>QuotedOn</string></text></a></t></children></rel><rel id="2a"><children><diamond id="2b" x="2780" y="384" w="200" h="96"/><t id="2c" x="2847.44376373291" y="423.82794284820557"><a><text><string>Aggregates</string></text></a></t></children></rel><rel id="2d"><children><diamond id="2e" x="1268" y="780" w="200" h="96"/><t id="2f" x="1339.523796081543" y="819.8279428482056"><a><text><string>PositionIn</string></text></a></t></children></rel><rel id="30"><children><diamond id="31" x="260" y="1608" w="200" h="96"/><t id="32" x="344.01588439941406" y="1647.8279428482056"><a><text><string>Owns</string></text></a></t></children></rel><rel id="33"><children><diamond id="34" x="764" y="1608" w="200" h="96"/><t id="35" x="847.811882019043" y="1647.8279428482056"><a><text><string>Holds</string></text></a></t></children></rel><rel id="36"><children><diamond id="37" x="1268" y="2040" w="200" h="96"/><t id="38" x="1350.31787109375" y="2079.8279428482056"><a><text><string>Places</string></text></a></t></children></rel><rel id="39"><children><diamond id="3a" x="2276" y="1212" w="200" h="96"/><t id="3b" x="2349.107810974121" y="1251.8279428482056"><a><text><string>PlacedOn</string></text></a></t></children></rel><rel id="3c"><children><diamond id="3d" x="2780" y="852" w="200" h="96"/><t id="3e" x="2869.367919921875" y="891.8279428482056"><a><text><string>Fills</string></text></a></t></children></rel><rel id="3f"><children><diamond id="40" x="2636" y="1536" w="200" h="96"/><t id="41" x="2714.699851989746" y="1575.8279428482056"><a><text><string>FillsBuy</string></text></a></t></children></rel><rel id="42"><children><diamond id="43" x="2924" y="1737.6" w="200" h="96"/><t id="44" x="3003.593849182129" y="1777.4279428482055"><a><text><string>FillsSell</string></text></a></t></children></rel><rel id="45"><children><diamond id="46" x="764" y="2472" w="200" h="96"/><t id="47" x="841.3318328857422" y="2511.8279428482056"><a><text><string>Records</string></text></a></t></children></rel><rel id="48"><children><diamond id="49" x="1772" y="2472" w="200" h="96"/><t id="4a" x="1853.1838607788086" y="2511.8279428482056"><a><text><string>Settles</string></text></a></t></children></rel><rel id="4b"><children><diamond id="4c" x="2276" y="2472" w="200" h="96"/><t id="4d" x="2362.6619033813477" y="2511.8279428482056"><a><text><string>Logs</string></text></a></t></children></rel><llabelUm id="4e"><points><p colinear="true" x="1262.5" y="432" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="964.4327310675848" y="432" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="4f"><Owner><r ref="4"/></Owner></rConnector></startConnector><endConnector><diamondConnector id="50"><Owner><diamond ref="22"/></Owner></diamondConnector></endConnector></llabelUm><llabelDoubleMuitos id="51"><points><p colinear="true" x="763.5672689324152" y="432" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="465.5" y="432" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><diamondConnector id="52"><Owner><diamond ref="22"/></Owner></diamondConnector></startConnector><endConnector><rConnector id="53"><Owner><r ref="1"/></Owner></rConnector></endConnector><a><innerStrokeWidthFactor><double>3</double></innerStrokeWidthFactor></a></llabelDoubleMuitos><llabelUm id="54"><points><p colinear="true" x="360" y="1187.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="360" y="876.9015230574684" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="55"><Owner><r ref="d"/></Owner></rConnector></startConnector><endConnector><diamondConnector id="56"><Owner><diamond ref="25"/></Owner></diamondConnector></endConnector></llabelUm><llabelDoubleMuitos id="57"><points><p colinear="true" x="360" y="779.0984769425318" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="360" y="468.5" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><diamondConnector id="58"><Owner><diamond ref="25"/></Owner></diamondConnector></startConnector><endConnector><rConnector id="59"><Owner><r ref="1"/></Owner></rConnector></endConnector><a><innerStrokeWidthFactor><double>3</double></innerStrokeWidthFactor></a></llabelDoubleMuitos><llabelUm id="5a"><points><p colinear="true" x="1473.5" y="432" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1771.5672689324151" y="432" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="5b"><Owner><r ref="4"/></Owner></rConnector></startConnector><endConnector><diamondConnector id="5c"><Owner><diamond ref="28"/></Owner></diamondConnector></endConnector></llabelUm><llabelDoubleMuitos id="5d"><points><p colinear="true" x="1972.4327310675847" y="432" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2270.5" y="432" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><diamondConnector id="5e"><Owner><diamond ref="28"/></Owner></diamondConnector></startConnector><endConnector><rConnector id="5f"><Owner><r ref="7"/></Owner></rConnector></endConnector><a><innerStrokeWidthFactor><double>3</double></innerStrokeWidthFactor></a></llabelDoubleMuitos><llabelUm id="60"><points><p colinear="true" x="2481.5" y="432" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2779.567268932415" y="432" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="61"><Owner><r ref="7"/></Owner></rConnector></startConnector><endConnector><diamondConnector id="62"><Owner><diamond ref="2b"/></Owner></diamondConnector></endConnector></llabelUm><llabelDoubleMuitos id="63"><points><p colinear="true" x="2980.4327310675844" y="432" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3278.5" y="432" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><diamondConnector id="64"><Owner><diamond ref="2b"/></Owner></diamondConnector></startConnector><endConnector><rConnector id="65"><Owner><r ref="a"/></Owner></rConnector></endConnector><a><innerStrokeWidthFactor><double>3</double></innerStrokeWidthFactor></a></llabelDoubleMuitos><llabelUm id="66"><points><p colinear="true" x="1368" y="468.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1368" y="779.0984769425318" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="67"><Owner><r ref="4"/></Owner></rConnector></startConnector><endConnector><diamondConnector id="68"><Owner><diamond ref="2e"/></Owner></diamondConnector></endConnector></llabelUm><llabelDoubleMuitos id="69"><points><p colinear="true" x="1368" y="876.9015230574684" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1368" y="1187.5" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><diamondConnector id="6a"><Owner><diamond ref="2e"/></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="360" y="2051.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="360" y="1704.9015230574682" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="6d"><Owner><r ref="16"/></Owner></rConnector></startConnector><endConnector><diamondConnector id="6e"><Owner><diamond ref="31"/></Owner></diamondConnector></endConnector></llabelUm><llabelDoubleMuitos id="6f"><points><p colinear="true" x="360" y="1607.0984769425318" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="360" y="1260.5" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><diamondConnector id="70"><Owner><diamond ref="31"/></Owner></diamondConnector></startConnector><endConnector><rConnector id="71"><Owner><r ref="d"/></Owner></rConnector></endConnector><a><innerStrokeWidthFactor><double>3</double></innerStrokeWidthFactor></a></llabelDoubleMuitos><llabelUm id="72"><points><p colinear="true" x="402.58333333333337" y="2051.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="827.6163211207464" y="1687.1860104679317" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="73"><Owner><r ref="16"/></Owner></rConnector></startConnector><endConnector><diamondConnector id="74"><Owner><diamond ref="34"/></Owner></diamondConnector></endConnector></llabelUm><llabelDoubleMuitos id="75"><points><p colinear="true" x="900.3836788792536" y="1624.8139895320683" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1325.4166666666667" y="1260.5" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><diamondConnector id="76"><Owner><diamond ref="34"/></Owner></diamondConnector></startConnector><endConnector><rConnector id="77"><Owner><r ref="10"/></Owner></rConnector></endConnector><a><innerStrokeWidthFactor><double>3</double></innerStrokeWidthFactor></a></llabelDoubleMuitos><llabelUm id="78"><points><p colinear="true" x="465.5" y="2088" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1267.5672689324151" y="2088" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="79"><Owner><r ref="16"/></Owner></rConnector></startConnector><endConnector><diamondConnector id="7a"><Owner><diamond ref="37"/></Owner></diamondConnector></endConnector></llabelUm><llabelDoubleMuitos id="7b"><points><p colinear="true" x="1468.4327310675847" y="2088" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2270.5" y="2088" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><diamondConnector id="7c"><Owner><diamond ref="37"/></Owner></diamondConnector></startConnector><endConnector><rConnector id="7d"><Owner><r ref="19"/></Owner></rConnector></endConnector><a><innerStrokeWidthFactor><double>3</double></innerStrokeWidthFactor></a></llabelDoubleMuitos><llabelUm id="7e"><points><p colinear="true" x="2376" y="468.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2376" y="1211.0984769425318" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="7f"><Owner><r ref="7"/></Owner></rConnector></startConnector><endConnector><diamondConnector id="80"><Owner><diamond ref="3a"/></Owner></diamondConnector></endConnector></llabelUm><llabelDoubleMuitos id="81"><points><p colinear="true" x="2376" y="1308.9015230574682" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2376" y="2051.5" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><diamondConnector id="82"><Owner><diamond ref="3a"/></Owner></diamondConnector></startConnector><endConnector><rConnector id="83"><Owner><r ref="19"/></Owner></rConnector></endConnector><a><innerStrokeWidthFactor><double>3</double></innerStrokeWidthFactor></a></llabelDoubleMuitos><llabelUm id="84"><points><p colinear="true" x="2415.3076923076924" y="468.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2845.4523306986234" y="867.9200213630073" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="85"><Owner><r ref="7"/></Owner></rConnector></startConnector><endConnector><diamondConnector id="86"><Owner><diamond ref="3d"/></Owner></diamondConnector></endConnector></llabelUm><llabelDoubleMuitos id="87"><points><p colinear="true" x="2914.5476693013766" y="932.0799786369927" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3344.6923076923076" y="1331.5" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><diamondConnector id="88"><Owner><diamond ref="3d"/></Owner></diamondConnector></startConnector><endConnector><rConnector id="89"><Owner><r ref="13"/></Owner></rConnector></endConnector><a><innerStrokeWidthFactor><double>3</double></innerStrokeWidthFactor></a></llabelDoubleMuitos><llabelUm id="8a"><points><p colinear="true" x="2402.0714285714284" y="2051.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2710.0837830122005" y="1620.2827037829193" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="8b"><Owner><r ref="19"/></Owner></rConnector></startConnector><endConnector><diamondConnector id="8c"><Owner><diamond ref="40"/></Owner></diamondConnector></endConnector></llabelUm><llabelMuitos id="8d"><points><p colinear="true" x="2795.6184409547654" y="1564.1271863484114" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3278.5" y="1403.1666666666667" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><diamondConnector id="8e"><Owner><diamond ref="40"/></Owner></diamondConnector></startConnector><endConnector><rConnector id="8f"><Owner><r ref="13"/></Owner></rConnector></endConnector></llabelMuitos><llabelUm id="90"><points><p colinear="true" x="2454.214285714286" y="2051.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2972.7176902815563" y="1809.5317445352737" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="91"><Owner><r ref="19"/></Owner></rConnector></startConnector><endConnector><diamondConnector id="92"><Owner><diamond ref="43"/></Owner></diamondConnector></endConnector></llabelUm><llabelMuitos id="93"><points><p colinear="true" x="3053.6929253517355" y="1751.1562065919868" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3352.5344827586205" y="1404.5" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><diamondConnector id="94"><Owner><diamond ref="43"/></Owner></diamondConnector></startConnector><endConnector><rConnector id="95"><Owner><r ref="13"/></Owner></rConnector></endConnector></llabelMuitos><llabelUm id="96"><points><p colinear="true" x="402.58333333333337" y="2124.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="827.6163211207464" y="2488.8139895320683" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="97"><Owner><r ref="16"/></Owner></rConnector></startConnector><endConnector><diamondConnector id="98"><Owner><diamond ref="46"/></Owner></diamondConnector></endConnector></llabelUm><llabelDoubleMuitos id="99"><points><p colinear="true" x="900.3836788792536" y="2551.1860104679317" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1325.4166666666667" y="2915.5" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><diamondConnector id="9a"><Owner><diamond ref="46"/></Owner></diamondConnector></startConnector><endConnector><rConnector id="9b"><Owner><r ref="1c"/></Owner></rConnector></endConnector><a><innerStrokeWidthFactor><double>3</double></innerStrokeWidthFactor></a></llabelDoubleMuitos><llabelUm id="9c"><points><p colinear="true" x="2333.4166666666665" y="2124.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1908.3836788792537" y="2488.8139895320683" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="9d"><Owner><r ref="19"/></Owner></rConnector></startConnector><endConnector><diamondConnector id="9e"><Owner><diamond ref="49"/></Owner></diamondConnector></endConnector></llabelUm><llabelMuitos id="9f"><points><p colinear="true" x="1835.6163211207463" y="2551.1860104679317" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1410.5833333333333" y="2915.5" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><diamondConnector id="a0"><Owner><diamond ref="49"/></Owner></diamondConnector></startConnector><endConnector><rConnector id="a1"><Owner><r ref="1c"/></Owner></rConnector></endConnector></llabelMuitos><llabelUm id="a2"><points><p colinear="true" x="2376" y="2124.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2376" y="2471.0984769425318" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="a3"><Owner><r ref="19"/></Owner></rConnector></startConnector><endConnector><diamondConnector id="a4"><Owner><diamond ref="4c"/></Owner></diamondConnector></endConnector></llabelUm><llabelDoubleMuitos id="a5"><points><p colinear="true" x="2376" y="2568.9015230574682" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2376" y="2915.5" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><diamondConnector id="a6"><Owner><diamond ref="4c"/></Owner></diamondConnector></startConnector><endConnector><rConnector id="a7"><Owner><r ref="1f"/></Owner></rConnector></endConnector><a><innerStrokeWidthFactor><double>3</double></innerStrokeWidthFactor></a></llabelDoubleMuitos><atrchave id="a8" nullable="false" attributeType="NUMBER"><children><e id="a9" x="-68.39695907392695" y="246.4050605385342" w="168" h="50"/><t id="aa" x="9.975004427049612" y="263.23300338673977"><a><fontUnderlined><boolean>true</boolean></fontUnderlined><fontBold><boolean>true</boolean></fontBold><text><string>id</string></text></a></t></children></atrchave><llabel id="ab"><points><p colinear="true" x="281.7254974014012" y="395.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="62.01277773449584" y="293.31312239618217" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="ac"><Owner><r ref="1"/></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="436.59493946146586" y="62.60304092607305" w="168" h="50"/><t id="b0" x="494.5787651450596" y="79.43098377427862"><a><text><string>added_at</string></text></a></t></children></atr><llabel id="b1"><points><p colinear="true" x="377.0202295226575" y="395.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="509.3201060987341" y="113.35425256029824" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="b2"><Owner><r ref="1"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="b3"><Owner><e ref="af"/></Owner></ellipseConnector></endConnector></llabel><atrchave id="b4" nullable="false" attributeType="NUMBER"><children><e id="b5" x="956.3391822844033" y="177.56942545958162" w="168" h="50"/><t id="b6" x="1034.7111457853798" y="194.3973683077872"><a><fontUnderlined><boolean>true</boolean></fontUnderlined><fontBold><boolean>true</boolean></fontBold><text><string>id</string></text></a></t></children></atrchave><llabel id="b7"><points><p colinear="true" x="1315.8725977539127" y="395.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1074.2831689371824" y="226.48715702164253" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="b8"><Owner><r ref="4"/></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="1158.1820975393546" y="27.302942570786456" w="168" h="50"/><t id="bc" x="1221.769950017382" y="44.13088541899202"><a><text><string>symbol</string></text></a></t></children></atr><llabel id="bd"><points><p colinear="true" x="1355.9052171989251" y="395.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1251.0899448243847" y="78.17639746873897" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="be"><Owner><r ref="4"/></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="1409.8179024606454" y="27.302942570786456" w="168" h="50"/><t id="c2" x="1477.749794855665" y="44.13088541899202"><a><text><string>name</string></text></a></t></children></atr><llabel id="c3"><points><p colinear="true" x="1380.0947828010749" y="395.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1485.9100551756153" y="78.17639746873897" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="c4"><Owner><r ref="4"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="c5"><Owner><e ref="c1"/></Owner></ellipseConnector></endConnector></llabel><atr id="c6" nullable="false" attributeType="VARCHAR2(128)"><children><e id="c7" x="1611.6608177155967" y="177.56942545958154" w="168" h="50"/><t id="c8" x="1666.1166130036827" y="194.3973683077871"><a><text><string>created_at</string></text></a></t></children></atr><llabel id="c9"><points><p colinear="true" x="1420.127402246087" y="395.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1662.7168310628176" y="226.48715702164245" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="ca"><Owner><r ref="4"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="cb"><Owner><e ref="c7"/></Owner></ellipseConnector></endConnector></llabel><atrchave id="cc" nullable="false" attributeType="NUMBER"><children><e id="cd" x="2000.9031116147885" y="162.740708319115" w="168" h="50"/><t id="ce" x="2079.275075115765" y="179.56865116732055"><a><fontUnderlined><boolean>true</boolean></fontUnderlined><fontBold><boolean>true</boolean></fontBold><text><string>id</string></text></a></t></children></atrchave><llabel id="cf"><points><p colinear="true" x="2332.5009938703115" y="395.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2113.999678160262" y="212.23607676035843" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="d0"><Owner><r ref="7"/></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="2183.014771569786" y="42.963985320114205" w="168" h="50"/><t id="d4" x="2223.1964656005475" y="59.79192816831977"><a><text><string>quote_currency</string></text></a></t></children></atr><llabel id="d5"><points><p colinear="true" x="2365.0726173310054" y="395.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2275.118003483917" y="93.86054854775956" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="d6"><Owner><r ref="7"/></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="2400.985228430214" y="42.963985320114205" w="168" h="50"/><t id="da" x="2461.5070637573626" y="59.79192816831977"><a><text><string>is_active</string></text></a></t></children></atr><llabel id="db"><points><p colinear="true" x="2386.9273826689946" y="395.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2477.881996516083" y="93.86054854775956" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="dc"><Owner><r ref="7"/></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="2583.0968883852115" y="162.74070831911507" w="168" h="50"/><t id="e0" x="2637.5526836732975" y="179.56865116732064"><a><text><string>created_at</string></text></a></t></children></atr><llabel id="e1"><points><p colinear="true" x="2419.4990061296885" y="395.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2639.000321839738" y="212.23607676035851" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="e2"><Owner><r ref="7"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="e3"><Owner><e ref="df"/></Owner></ellipseConnector></endConnector></llabel><atrchave id="e4" nullable="false" attributeType="NUMBER"><children><e id="e5" x="3122.1495254706524" y="-81.64016280867236" w="168" h="50"/><t id="e6" x="3200.521488971629" y="-64.81221996046679"><a><fontUnderlined><boolean>true</boolean></fontUnderlined><fontBold><boolean>true</boolean></fontBold><text><string>id</string></text></a></t></children></atrchave><llabel id="e7"><points><p colinear="true" x="3370.7150864492837" y="395.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3215.8752825761353" y="-30.792603483984927" 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="3332.392271727323" y="-111.99011621835655" w="168" h="50"/><t id="ec" x="3386.7820574939246" y="-95.16217337015098"><a><text><string>timeframe</string></text></a></t></children></atr><llabel id="ed"><points><p colinear="true" x="3386.2781125903944" y="395.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3415.300995849721" y="-60.99463817333623" 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="3537.229541823645" y="-55.733340581963716" w="168" h="50"/><t id="f2" x="3606.8174400780395" y="-38.90539773375815"><a><text><string>open</string></text></a></t></children></atr><llabel id="f3"><points><p colinear="true" x="3402.7124581636435" y="395.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3608.810156433815" y="-5.0331470742506355" 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><atr id="f6" nullable="false" attributeType="VARCHAR2(128)"><children><e id="f7" x="3702.479016298438" y="77.74228719824731" w="168" h="50"/><t id="f8" x="3773.824933046485" y="94.57023004645288"><a><text><string>high</string></text></a></t></children></atr><llabel id="f9"><points><p colinear="true" x="3428.616977898216" y="395.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3757.7345449658733" y="127.166435670914" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="fa"><Owner><r ref="a"/></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="3800.564608414006" y="266.1629565656373" w="168" h="50"/><t id="fe" x="3874.670534927678" y="282.99089941384284"><a><text><string>low</string></text></a></t></children></atr><llabel id="ff"><points><p colinear="true" x="3489.5" y="402.31690248856694" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3823.2597553724627" y="309.0521459448973" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="100"><Owner><r ref="a"/></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="3815.118237094787" y="478.0858763212382" w="168" h="50"/><t id="104" x="3884.802128391174" y="494.91381916944374"><a><text><string>close</string></text></a></t></children></atr><llabel id="105"><points><p colinear="true" x="3489.5" y="446.55890980328587" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3822.7719573934014" y="492.98115542180165" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="106"><Owner><r ref="a"/></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="3743.711258448721" y="678.146305757339" w="168" h="50"/><t id="10a" x="3806.783112086416" y="694.9742486055445"><a><text><string>volume</string></text></a></t></children></atr><llabel id="10b"><points><p colinear="true" x="3443.729602024792" y="468.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3790.795958034731" y="680.7822916200306" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="10c"><Owner><r ref="a"/></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="3598.259746902544" y="832.9590630302757" w="168" h="50"/><t id="110" x="3648.3115123444386" y="849.7870058784813"><a><text><string>candle_time</string></text></a></t></children></atr><llabel id="111"><points><p colinear="true" x="3409.5575751446545" y="468.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3665.290202517464" y="833.5099680380305" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="112"><Owner><r ref="a"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="113"><Owner><e ref="10f"/></Owner></ellipseConnector></endConnector></llabel><atrchave id="114" nullable="false" attributeType="NUMBER"><children><e id="115" x="-70.4101615137755" y="999" w="168" h="50"/><t id="116" x="7.961801987201056" y="1015.8279428482056"><a><fontUnderlined><boolean>true</boolean></fontUnderlined><fontBold><boolean>true</boolean></fontBold><text><string>id</string></text></a></t></children></atrchave><llabel id="117"><points><p colinear="true" x="296.780145523736" y="1187.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="53.232620375753754" y="1047.0990956607504" 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="-124" y="1199" w="168" h="50"/><t id="11c" x="-56.06810760498047" y="1215.8279428482056"><a><text><string>name</string></text></a></t></children></atr><llabel id="11d"><points><p colinear="true" x="254.5" y="1224" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="45" y="1224.5" 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="-70.41016151377545" y="1399" w="168" h="50"/><t id="122" x="-15.954366225689512" y="1415.8279428482056"><a><text><string>created_at</string></text></a></t></children></atr><llabel id="123"><points><p colinear="true" x="296.780145523736" y="1260.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="53.23262037575381" y="1401.9009043392496" 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="1623.4112549695428" y="859.5887450304572" w="168" h="50"/><t id="128" x="1701.7832184705194" y="876.4166878786627"><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="1404.5" y="1187.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1683.498644392971" y="909.5013556070288" 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="1711.6831316104167" y="981.0845601250176" w="168" h="50"/><t id="12e" x="1772.2709764590495" y="997.9125029732231"><a><text><string>quantity</string></text></a></t></children></atr><llabel id="12f"><points><p colinear="true" x="1439.635283450938" y="1187.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1753.1223466522588" y="1028.5251259364059" 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="1758.0904034856662" y="1123.9114567806892" w="168" h="50"/><t id="134" x="1791.2940472844944" y="1140.7393996288947"><a><text><string>reserved_quantity</string></text></a></t></children></atr><llabel id="135"><points><p colinear="true" x="1473.5" y="1207.2904415457615" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1767.7694330432068" y="1161.2619343087567" 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><atrderivado id="138"><children><e id="139" x="1758.0904034856662" y="1274.0885432193108" w="168" h="50"><a><fillColor><color rgba="#ffffebeb"/></fillColor><strokeDashes><doubleArray><double>5</double></doubleArray></strokeDashes></a></e><t id="13a" x="1815.3422192815647" y="1290.9164860675164"><a><text><string>avg_price</string></text></a></t></children></atrderivado><llabel id="13b"><points><p colinear="true" x="1473.5" y="1240.7095584542385" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1767.7694330432068" y="1287.7380656912433" 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="1711.6831316104167" y="1416.9154398749824" w="168" h="50"/><t id="140" x="1766.1389268985026" y="1433.743382723188"><a><text><string>created_at</string></text></a></t></children></atr><llabel id="141"><points><p colinear="true" x="1439.635283450938" y="1260.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1753.1223466522588" y="1420.4748740635941" 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="1623.4112549695428" y="1538.4112549695428" w="168" h="50"/><t id="146" x="1675.5210419568475" y="1555.2391978177484"><a><text><string>updated_at</string></text></a></t></children></atr><llabel id="147"><points><p colinear="true" x="1404.5" y="1260.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1683.498644392971" y="1539.498644392971" 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="3575.316689448502" y="949.8070187412839" w="168" h="50"/><t id="14c" x="3653.6886529494786" y="966.6349615894894"><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="3409.5575751446545" y="1331.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3642.347145063422" y="1000.2561137335291" 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="3707.0630861550844" y="1088.6387531680616" w="168" h="50"/><t id="152" x="3757.0968538552797" y="1105.4666960162672"><a><text><string>executed_at</string></text></a></t></children></atr><llabel id="153"><points><p colinear="true" x="3442.4122103099985" y="1331.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3754.8155242942967" y="1137.1011783322601" 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="3774.0904034856662" y="1267.9114567806892" w="168" h="50"/><t id="158" x="3844.1103009466037" y="1284.7393996288947"><a><text><string>price</string></text></a></t></children></atr><llabel id="159"><points><p colinear="true" x="3489.5" y="1351.2904415457615" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3783.769433043207" y="1305.2619343087567" 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="3765.741948612478" y="1459.1225098878406" w="168" h="50"/><t id="15e" x="3826.329793461111" y="1475.9504527360461"><a><text><string>quantity</string></text></a></t></children></atr><llabel id="15f"><points><p colinear="true" x="3489.5" y="1394.3041042999555" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3785.099442339135" y="1468.3806588985085" 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="3683.3450448227004" y="1631.8712111129832" w="168" h="50"/><t id="164" x="3755.848958702095" y="1648.6991539611888"><a><text><string>side</string></text></a></t></children></atr><llabel id="165"><points><p colinear="true" x="3432.4371359891447" y="1404.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3736.4308097196013" y="1633.6988870201615" 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="3540" y="1758.6921938165306" w="168" h="50"/><t id="16a" x="3605.0458602905273" y="1775.5201366647361"><a><text><string>source</string></text></a></t></children></atr><llabel id="16b"><points><p colinear="true" x="3405.0732848254215" y="1404.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="3609.996063690191" y="1759.070639218198" 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><atrchave id="16e" nullable="false" attributeType="NUMBER"><children><e id="16f" x="-79.61739053764865" y="1555.125732540825" w="168" h="50"/><t id="170" x="-1.2454270366720834" y="1571.9536753890306"><a><fontUnderlined><boolean>true</boolean></fontUnderlined><fontBold><boolean>true</boolean></fontBold><text><string>id</string></text></a></t></children></atrchave><llabel id="171"><points><p colinear="true" x="334.44242485534556" y="2051.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="22.35215384743159" y="1605.5748275330702" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="172"><Owner><r ref="16"/></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="-191.13025572314973" y="1655.3367514871807" w="168" h="50"/><t id="176" x="-135.6424551250052" y="1672.1646943353862"><a><text><string>username</string></text></a></t></children></atr><llabel id="177"><points><p colinear="true" x="318.1756403205456" y="2051.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="-79.01493755402784" y="1704.936561284582" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="178"><Owner><r ref="16"/></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="-275.3281927323858" y="1779.3854307366398" w="168" h="50"/><t id="17c" x="-206.7843008256475" y="1796.2133735848454"><a><text><string>email</string></text></a></t></children></atr><llabel id="17d"><points><p colinear="true" x="289.04638472205664" y="2051.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="-148.0718827364814" y="1826.880156568963" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="17e"><Owner><r ref="16"/></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="-327.28781975949073" y="1920.018160139687" w="168" h="50"/><t id="182" x="-270.8880089684751" y="1936.8461029878927"><a><text><string>full_name</string></text></a></t></children></atr><llabel id="183"><points><p colinear="true" x="254.5" y="2062.996040677107" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="-176.33272171632424" y="1961.2683077062293" 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="-343.9708547344802" y="2069.0115954453063" w="168" h="50"/><t id="188" x="-303.6091603985427" y="2085.839538293512"><a><text><string>password_hash</string></text></a></t></children></atr><llabel id="189"><points><p colinear="true" x="254.5" y="2089.022988927038" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="-175.01444209639942" y="2093.692657294158" 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><atr id="18c" nullable="false" attributeType="VARCHAR2(128)"><children><e id="18d" x="-324.4017755272396" y="2217.6535093159487" w="168" h="50"/><t id="18e" x="-289.87210481190755" y="2234.4814521641542"><a><text><string>available_balance</string></text></a></t></children></atr><llabel id="18f"><points><p colinear="true" x="254.5" y="2115.1750449413726" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="-175.63096569337222" y="2226.598417887951" 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="-269.72486253166903" y="2357.252229243537" w="168" h="50"/><t id="194" x="-234.13319535637606" y="2374.0801720917425"><a><text><string>invested_balance</string></text></a></t></children></atr><llabel id="195"><points><p colinear="true" x="292.30651970381484" y="2124.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="-143.95601093481392" y="2360.500262337795" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="196"><Owner><r ref="16"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="197"><Owner><e ref="193"/></Owner></ellipseConnector></endConnector></llabel><atr id="198" nullable="false" attributeType="VARCHAR2(128)"><children><e id="199" x="-183.13728812758285" y="2479.6448735444237" w="168" h="50"/><t id="19a" x="-148.46363547865707" y="2496.4728163926293"><a><text><string>reserved_balance</string></text></a></t></children></atr><llabel id="19b"><points><p colinear="true" x="319.77746978118057" y="2124.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="-71.97240910262072" y="2480.947786013721" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="19c"><Owner><r ref="16"/></Owner></rConnector></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="-69.70216554052774" y="2573.6420459197734" w="168" h="50"/><t id="1a0" x="-15.246370252441807" y="2590.469988767979"><a><text><string>created_at</string></text></a></t></children></atr><llabel id="1a1"><points><p colinear="true" x="335.2896786642366" y="2124.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="31.71183453898649" y="2574.158113314706" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="1a2"><Owner><r ref="16"/></Owner></rConnector></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="63.94751113808553" y="2649.6420458596826" w="168" h="50"/><t id="1a6" x="116.05729812539022" y="2666.469988707888"><a><text><string>updated_at</string></text></a></t></children></atr><llabel id="1a7"><points><p colinear="true" x="346.80640793123246" y="2124.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="157.61059799292497" y="2649.792416398774" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="1a8"><Owner><r ref="16"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="1a9"><Owner><e ref="1a5"/></Owner></ellipseConnector></endConnector></llabel><atrchave id="1aa" nullable="false" attributeType="NUMBER"><children><e id="1ab" x="2992" y="2063" w="168" h="50"/><t id="1ac" x="3070.3719635009766" y="2079.8279428482056"><a><fontUnderlined><boolean>true</boolean></fontUnderlined><fontBold><boolean>true</boolean></fontBold><text><string>id</string></text></a></t></children></atrchave><llabel id="1ad"><points><p colinear="true" x="2481.5" y="2088" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2992" y="2088.5" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="1ae"><Owner><r ref="19"/></Owner></rConnector></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="2984.3111043533418" y="2166.4665877907273" w="168" h="50"/><t id="1b2" x="3056.8150182327363" y="2183.294530638933"><a><text><string>side</string></text></a></t></children></atr><llabel id="1b3"><points><p colinear="true" x="2481.5" y="2103.767080642333" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2993.0883684502273" y="2180.649749085111" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="1b4"><Owner><r ref="19"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="1b5"><Owner><e ref="1b1"/></Owner></ellipseConnector></endConnector></llabel><atr id="1b6" nullable="false" attributeType="VARCHAR2(128)"><children><e id="1b7" x="2961.4133291741246" y="2267.6601933059155" w="168" h="50"/><t id="1b8" x="3033.113242199027" y="2284.488136154121"><a><text><string>type</string></text></a></t></children></atr><llabel id="1b9"><points><p colinear="true" x="2481.5" y="2120.2545868938887" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2986.55310628215" y="2275.0119519096143" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="1ba"><Owner><r ref="19"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="1bb"><Owner><e ref="1b7"/></Owner></ellipseConnector></endConnector></llabel><atr id="1bc" nullable="false" attributeType="VARCHAR2(128)"><children><e id="1bd" x="2923.8096990449026" y="2364.357767765807" w="168" h="50"/><t id="1be" x="2990.655577645977" y="2381.1857106140124"><a><text><string>status</string></text></a></t></children></atr><llabel id="1bf"><points><p colinear="true" x="2452.5238413667184" y="2124.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2963.130858075615" y="2368.308566027795" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="1c0"><Owner><r ref="19"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="1c1"><Owner><e ref="1bd"/></Owner></ellipseConnector></endConnector></llabel><atr id="1c2" nullable="false" attributeType="VARCHAR2(128)"><children><e id="1c3" x="2872.326300788529" y="2444.5088818206123" w="168" h="50"/><t id="1c4" x="2932.9141456371617" y="2461.336824668818"><a><text><string>quantity</string></text></a></t></children></atr><llabel id="1c5"><points><p colinear="true" x="2431.521407202103" y="2124.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2921.5740998439587" y="2446.833940850671" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="1c6"><Owner><r ref="19"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="1c7"><Owner><e ref="1c3"/></Owner></ellipseConnector></endConnector></llabel><atr id="1c8" nullable="false" attributeType="VARCHAR2(128)"><children><e id="1c9" x="2808.0941357670868" y="2519.9723742392835" w="168" h="50"/><t id="1ca" x="2852.2358593998993" y="2536.800317087489"><a><text><string>filled_quantity</string></text></a></t></children></atr><llabel id="1cb"><points><p colinear="true" x="2417.2222642273664" y="2124.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2865.334720203274" y="2521.33569260066" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="1cc"><Owner><r ref="19"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="1cd"><Owner><e ref="1c9"/></Owner></ellipseConnector></endConnector></llabel><atr id="1ce" nullable="false" attributeType="VARCHAR2(128)"><children><e id="1cf" x="2732.5242737348863" y="2595.1040785055266" w="168" h="50"/><t id="1d0" x="2802.5441711958238" y="2611.932021353732"><a><text><string>price</string></text></a></t></children></atr><llabel id="1d1"><points><p colinear="true" x="2406.2180280904504" y="2124.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2796.5425916598115" y="2595.864496111119" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="1d2"><Owner><r ref="19"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="1d3"><Owner><e ref="1cf"/></Owner></ellipseConnector></endConnector></llabel><atr id="1d4" nullable="false" attributeType="VARCHAR2(128)"><children><e id="1d5" x="2647.276854072493" y="2670.2355253829633" w="168" h="50"/><t id="1d6" x="2704.5226731642897" y="2687.063468231169"><a><text><string>placed_at</string></text></a></t></children></atr><llabel id="1d7"><points><p colinear="true" x="2397.3551490839864" y="2124.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2717.084748554333" y="2670.6239305491463" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="1d8"><Owner><r ref="19"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="1d9"><Owner><e ref="1d5"/></Owner></ellipseConnector></endConnector></llabel><atr id="1da" nullable="false" attributeType="VARCHAR2(128)"><children><e id="1db" x="2554.2246153911383" y="2745.6986013377973" w="168" h="50"/><t id="1dc" x="2604.2583830913336" y="2762.526544186003"><a><text><string>executed_at</string></text></a></t></children></atr><llabel id="1dd"><points><p colinear="true" x="2390.0196544170753" y="2124.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2628.9952042826358" y="2745.868197684778" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="1de"><Owner><r ref="19"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="1df"><Owner><e ref="1db"/></Owner></ellipseConnector></endConnector></llabel><atrchave id="1e0" nullable="false" attributeType="NUMBER"><children><e id="1e1" x="832.947542022764" y="3091.169668796321" w="168" h="50"/><t id="1e2" x="911.3195055237405" y="3107.9976116445264"><a><fontUnderlined><boolean>true</boolean></fontUnderlined><fontBold><boolean>true</boolean></fontBold><text><string>id</string></text></a></t></children></atrchave><llabel id="1e3"><points><p colinear="true" x="1267.7170741899063" y="2988.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="971.3812226795658" y="3097.0394144128263" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="1e4"><Owner><r ref="1c"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="1e5"><Owner><e ref="1e1"/></Owner></ellipseConnector></endConnector></llabel><atr id="1e6" nullable="false" attributeType="VARCHAR2(128)"><children><e id="1e7" x="962.8173089477482" y="3283.7095162291494" w="168" h="50"/><t id="1e8" x="1034.5172219726505" y="3300.537459077355"><a><text><string>type</string></text></a></t></children></atr><llabel id="1e9"><points><p colinear="true" x="1335.1352523831288" y="2988.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1069.4742367933363" y="3284.601754873672" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="1ea"><Owner><r ref="1c"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="1eb"><Owner><e ref="1e7"/></Owner></ellipseConnector></endConnector></llabel><atr id="1ec" nullable="false" attributeType="VARCHAR2(128)"><children><e id="1ed" x="1167.8774901121594" y="3392.741948612478" w="168" h="50"/><t id="1ee" x="1229.6893416136243" y="3409.5698914606837"><a><text><string>amount</string></text></a></t></children></atr><llabel id="1ef"><points><p colinear="true" x="1358.8995278962238" y="2988.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1258.717433644473" y="3392.8138239733807" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="1f0"><Owner><r ref="1c"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="1f1"><Owner><e ref="1ed"/></Owner></ellipseConnector></endConnector></llabel><atr id="1f2" nullable="false" attributeType="VARCHAR2(128)"><children><e id="1f3" x="1400.1225098878404" y="3392.741948612478" w="168" h="50"/><t id="1f4" x="1459.5463380128404" y="3409.5698914606837"><a><text><string>currency</string></text></a></t></children></atr><llabel id="1f5"><points><p colinear="true" x="1377.1004721037762" y="2988.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1478.2825663555268" y="3392.8138239733807" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="1f6"><Owner><r ref="1c"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="1f7"><Owner><e ref="1f3"/></Owner></ellipseConnector></endConnector></llabel><atr id="1f8" nullable="false" attributeType="VARCHAR2(128)"><children><e id="1f9" x="1605.1826910522518" y="3283.7095162291494" w="168" h="50"/><t id="1fa" x="1659.6384863403377" y="3300.537459077355"><a><text><string>created_at</string></text></a></t></children></atr><llabel id="1fb"><points><p colinear="true" x="1400.8647476168712" y="2988.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1667.5257632066637" y="3284.601754873672" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="1fc"><Owner><r ref="1c"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="1fd"><Owner><e ref="1f9"/></Owner></ellipseConnector></endConnector></llabel><atr id="1fe" nullable="false" attributeType="VARCHAR2(128)"><children><e id="1ff" x="1735.052457977236" y="3091.169668796321" w="168" h="50"/><t id="200" x="1787.4562284240133" y="3107.9976116445264"><a><text><string>description</string></text></a></t></children></atr><llabel id="201"><points><p colinear="true" x="1468.2829258100937" y="2988.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="1765.618777320434" y="3097.0394144128263" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="202"><Owner><r ref="1c"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="203"><Owner><e ref="1ff"/></Owner></ellipseConnector></endConnector></llabel><atrchave id="204" nullable="false" attributeType="NUMBER"><children><e id="205" x="2105.947998459971" y="3378.052457977236" w="168" h="50"/><t id="206" x="2184.3199619609477" y="3394.8804008254415"><a><fontUnderlined><boolean>true</boolean></fontUnderlined><fontBold><boolean>true</boolean></fontBold><text><string>id</string></text></a></t></children></atrchave><llabel id="207"><points><p colinear="true" x="2360.9443275696467" y="2988.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2200.885790867172" y="3378.2477480995985" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="208"><Owner><r ref="1f"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="209"><Owner><e ref="205"/></Owner></ellipseConnector></endConnector></llabel><atr id="20a" nullable="false" attributeType="VARCHAR2(128)"><children><e id="20b" x="2307.9385594324212" y="3406.707596969166" w="168" h="50"/><t id="20c" x="2361.284346480761" y="3423.5355398173715"><a><text><string>event_type</string></text></a></t></children></atr><llabel id="20d"><points><p colinear="true" x="2377.21273338792" y="2988.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2391.591349924995" y="3406.708878677839" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="20e"><Owner><r ref="1f"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="20f"><Owner><e ref="20b"/></Owner></ellipseConnector></endConnector></llabel><atr id="210" nullable="false" attributeType="VARCHAR2(128)"><children><e id="211" x="2509.9291204048714" y="3365.5018196684487" w="168" h="50"/><t id="212" x="2570.516965253504" y="3382.3297625166542"><a><text><string>quantity</string></text></a></t></children></atr><llabel id="213"><points><p colinear="true" x="2394.1399769350837" y="2988.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2581.8961563370854" y="3365.7838610066906" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="214"><Owner><r ref="1f"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="215"><Owner><e ref="211"/></Owner></ellipseConnector></endConnector></llabel><atr id="216" nullable="false" attributeType="VARCHAR2(128)"><children><e id="217" x="2637.2831041625523" y="3260.4360178203187" w="168" h="50"/><t id="218" x="2707.3030016234898" y="3277.2639606685243"><a><text><string>price</string></text></a></t></children></atr><llabel id="219"><points><p colinear="true" x="2413.7968564533558" y="2988.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2696.5790646318837" y="3261.596759765758" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="21a"><Owner><r ref="1f"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="21b"><Owner><e ref="217"/></Owner></ellipseConnector></endConnector></llabel><atr id="21c" nullable="false" attributeType="VARCHAR2(128)"><children><e id="21d" x="2737.048250192058" y="3106.811164839638" w="168" h="50"/><t id="21e" x="2787.772012704265" y="3123.6391076878435"><a><text><string>status_after</string></text></a></t></children></atr><llabel id="21f"><points><p colinear="true" x="2466.3406701496947" y="2988.5" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2770.9818121166277" y="3111.8809977100946" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="220"><Owner><r ref="1f"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="221"><Owner><e ref="21d"/></Owner></ellipseConnector></endConnector></llabel><atr id="222" nullable="false" attributeType="VARCHAR2(128)"><children><e id="223" x="2772" y="2927" w="168" h="50"/><t id="224" x="2826.455795288086" y="2943.8279428482056"><a><text><string>created_at</string></text></a></t></children></atr><llabel id="225"><points><p colinear="true" x="2481.5" y="2952" c1x="0" c1y="0" c2x="0" c2y="0"/><p colinear="true" x="2772" y="2952.5" c1x="0" c1y="0" c2x="0" c2y="0"/></points><startConnector><rConnector id="226"><Owner><r ref="1f"/></Owner></rConnector></startConnector><endConnector><ellipseConnector id="227"><Owner><e ref="223"/></Owner></ellipseConnector></endConnector></llabel></figures></drawing>
Index: cs/P1-ConceptualModel/ep-diagram.md
===================================================================
--- docs/P1-ConceptualModel/ep-diagram.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,197 +1,0 @@
-Ентитети
-
-User – ентитет кој чува информации за корисниците на платформата
-
-id (uuid)
-
-username (varchar(50), not null, unique)
-
-email (varchar(255), not null, unique)
-
-password_hash (varchar(255), not null)
-
-full_name (varchar(200))
-
-created_at (timestamptz)
-
-updated_at (timestamptz)
-
-
-Crypto – ентитет кој ги чува податоците за криптовалутите со кои се тргува
-
-id (uuid)
-
-symbol (varchar(20), not null, unique)
-
-name (varchar(255))
-
-created_at (timestamptz)
-
-
-
-Market – ентитет кој ги дефинира пазарите (парови криптовалути и валути за котација(BTCUSDT, ADAUSDT, ADABTC))
-
-id (uuid)
-
-crypto_id (uuid, not null, FK → Crypto.id)
-
-quote_currency (char(3), not null) ADA, USDT, BTC, USDT
-
-is_active (boolean)
-
-created_at (timestamptz)
-
-
-
-Holding – ентитет кој ги чува позициите на корисниците во криптовалути
-
-id (uuid)
-
-user_id (uuid, not null, FK → User.id)
-
-crypto_id (uuid, not null, FK → Crypto.id)
-
-quantity (numeric(20,4), not null)
-
-avg_price (numeric(18,6))
-
-created_at (timestamptz)
-
-updated_at (timestamptz)
-
-
-
-Order – ентитет кој ги чува нарачките за купување/продавање
-
-id (uuid)
-
-user_id (uuid, not null, FK → User.id)
-
-market_id (uuid, not null, FK → Market.id)
-
-side (varchar(4), not null) — типично “buy” или “sell”
-
-type (varchar(20), not null) — тип на нарачка, пример “limit”, “market”
-
-status (varchar(20), not null) — статус на нарачка (open, executed, cancelled)
-
-quantity (numeric(20,4), not null)
-
-price (numeric(18,6))
-
-placed_at (timestamptz)
-
-executed_at (timestamptz)
-
-
-
-Transaction – ентитет кој ја следи финансиската активност на корисниците (депозити, купувања, продавања, провизии)
-
-id (uuid)
-
-user_id (uuid, not null, FK → User.id)
-
-type (varchar(50), not null) — пример: “deposit”, “buy”, “sell”, “fee”
-
-amount (numeric(18,4), not null)
-
-currency (char(3), not null)
-
-related_order (uuid, FK → Order.id)
-
-created_at (timestamptz)
-
-description (text)
-
-
-
-Market_Trade – ентитет кој ги евидентира извршените пазарни зделки кои ја формираат цената
-
-id (bigserial)
-
-market_id (uuid, not null, FK → Market.id)
-
-executed_at (timestamptz, not null)
-
-price (numeric(18,6), not null)
-
-quantity (numeric(20,6), not null)
-
-side (varchar(4))
-
-source (varchar(50))
-
-Market_Candle – ентитет кој ги агрегира податоците за цените и волуменот во временски интервали (целови)
-
-id (bigserial)
-
-market_id (uuid, not null, FK → Market.id)
-
-timeframe (varchar(5), not null) — пример: “1m”, “5m”, “1h”
-
-open (numeric(18,6), not null)
-
-high (numeric(18,6), not null)
-
-low (numeric(18,6), not null)
-
-close (numeric(18,6), not null)
-
-volume (numeric(20,6), not null)
-
-candle_time (timestamptz, not null)
-
-Watchlist – ентитет кој ги чува листите за следење на корисниците
-
-id (uuid)
-
-user_id (uuid, not null, FK → User.id)
-
-name (varchar(100))
-
-created_at (timestamptz)
-
-Watchlist_Item – ентитет кој ги чува криптовалутите кои се ставени во некоја листа за следење
-
-id (uuid)
-
-watchlist_id (uuid, not null, FK → Watchlist.id)
-
-crypto_id (uuid, not null, FK → Crypto.id)
-
-added_at (timestamptz)
-
-Релации
-
-User – Holding (1:N)
-Еден корисник може да има повеќе позиции во различни криптовалути.
-
-Crypto – Holding (1:N)
-Една криптовалута може да се држи од повеќе корисници.
-
-User – Order (1:N)
-Еден корисник може да има повеќе нарачки.
-
-Market – Order (1:N)
-Еден пазар има повеќе нарачки.
-
-User – Transaction (1:N)
-Еден корисник има повеќе трансакции.
-
-Order – Transaction (0..1 : N)
-Трансакцијата може да биде поврзана со една нарачка (пример купување/продажба).
-
-Market – Market_Trade (1:N)
-Еден пазар има многу извршени зделки.
-
-Market – Market_Candle (1:N)
-Еден пазар има многу временски агрегирани податоци.
-
-User – Watchlist (1:N)
-Еден корисник може да има повеќе листи за следење.
-
-Watchlist – Watchlist_Item (1:N)
-Една листа може да содржи повеќе криптовалути.
-
-Crypto – Watchlist_Item (1:N)
-Една криптовалута може да биде во повеќе листи за следење.
Index: cs/P1-ConceptualModel/wiki/ERModel.md
===================================================================
--- docs/P1-ConceptualModel/wiki/ERModel.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,365 +1,0 @@
-= Entity-Relationship Model v.05 =
-
-== Diagram ==
-
-[[Image(ERModel_v05.png, 800px)]]
-
-Notation: Chen. Rectangles are entity sets, diamonds are relationships, ellipses
-are attributes, underlined ellipses are primary keys, the dashed ellipse is a
-derived attribute. A double line between an entity set and a relationship marks
-'''total participation''' (every instance of that entity set must participate); a
-single line marks partial participation.
-
-Three deliberate modeling decisions worth stating up front:
-
- * '''No foreign keys appear in the diagram.''' Connections between entity sets are expressed as relationships, per the notation. Foreign-key columns appear only in the relational model in [wiki:RelationalDesign].
- * '''A position and a watchlist entry are entity sets, not M:N relationships.''' `Holdings` (a user's position in an asset) and `WatchlistItems` (an asset on a watchlist) each have their own identifier `id`, and each is connected by two 1:N relationships: `Holds` and `PositionIn` for a holding, `Contains` and `Lists` for a watchlist item. Until v04 they were drawn as the M:N relationships `Holds` and `Contains`, but the database has always given `holdings` and `watchlist_items` their own `id` primary key. That is how an entity set is implemented, not an M:N relationship, whose key would be the pair of participating keys. v05 corrects the model to match; see history.
- * '''Key and uniqueness rules are stated with entity and relationship names, never with foreign-key columns.''' For example: "a crypto is quoted at most once per currency", not "`{crypto_id, quote_currency}` is unique".
-
-== 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 ===
-
-==== Users ====
-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.
-
-'''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 `@` (checked by the application at registration, not by a database constraint) ||
-|| `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 ||
-|| `reserved_balance` || numeric(18,4) || required, default 0, ≥ 0 — cash set aside for the user's open buy orders (added in v04, after P7) ||
-|| `created_at` || timestamptz || required, defaults to now ||
-|| `updated_at` || timestamptz || optional (null until first change) ||
-
-==== Cryptos ====
-The catalog of crypto assets the platform knows about. Kept separate from
-`Markets` because an asset exists independently of the pairs it is traded in —
-the same asset can be quoted against several currencies, and a user's holding is
-in the ''asset'', not in a particular pair.
-
-'''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 ====
-A tradeable pair: one crypto asset quoted in one currency, e.g. BTC/USD. This is
-where prices live, and it is the thing an order is placed ''on''. Modeled as its
-own entity set rather than an attribute of `Cryptos` because a market has its own
-lifecycle — it can be deactivated without deleting the asset — and because
-trades, candles and orders all reference the pair, not the asset.
-
-'''Keys:''' candidate `{id}`; primary key '''`id`''', so that the many entity sets
-related to a market need one narrow identifier instead of a composite one.
-'''Uniqueness rule:''' a crypto is quoted at most once per currency, so the crypto
-a market is `QuotedOn` together with its `quote_currency` identifies the market
-as well. Chen notation cannot draw this, because half of it comes through a
-relationship. P2 enforces it as `UNIQUE(crypto_id, quote_currency)`.
-
-||= 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 ====
-A user's instruction to buy or sell on a market. Needed as a separate entity set
-because an order is a record of ''intent'' that outlives its execution: it keeps
-the requested quantity and price even after it has been filled, which is what
-makes the ledger auditable.
-
-Placing an order is what triggers a '''reservation''' of whatever it commits:
-the crypto being sold (`Holdings.reserved_quantity`, below) on a sell, and the
-cash (`Users.reserved_balance`) on a buy. Since v04 (after P7) an order can
-wait in the order book and be filled in parts, so `status` is a real
-lifecycle driven by `filled_quantity`: `open` (nothing filled yet),
-`partially_filled`, `executed` (completely filled), or `cancelled`, which
-releases what is still reserved. See
-[wiki:UseCase0005] for the reserve-then-settle
-sequence and
-[wiki:AdvancedDatabaseDevelopment]
-for the rules that keep it consistent.
-
-'''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` (both executed since P7) ||
-|| `status` || text || required, `open`, `partially_filled`, `executed` or `cancelled` ||
-|| `quantity` || numeric(20,4) || required, > 0 ||
-|| `filled_quantity` || numeric(20,4) || required, default 0, between 0 and `quantity` — how much has been traded; remaining = `quantity − filled_quantity` (added in v04, after P7) ||
-|| `price` || numeric(18,6) || optional — the limit price; for a market order, the market price when it was placed ||
-|| `placed_at` || timestamptz || required, defaults to now ||
-|| `executed_at` || timestamptz || optional, set when the order settles ||
-
-==== Transactions ====
-The financial ledger: every movement of virtual cash, in one place. This exists
-so that a balance is never just a number someone edited — it is the sum of an
-auditable list of entries, which is also what the "explain every step" goal of
-the project needs.
-
-'''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 ====
-Individual executed trades on a market, from the user's own fills and from the
-market simulator. This is the single source of truth for the current price: the
-price of a market is the price of its most recent trade, never a column someone
-writes directly.
-
-'''Keys:''' candidate `{id}` — "market plus `executed_at`" looks unique in
-principle, but two trades on a market 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` || big integer || PK, required, auto-generated (`bigserial` in P2) ||
-|| `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`) ||
-
-Since v04 (after P7) a trade also records which orders it filled, through the
-relationships `FillsBuy` and `FillsSell` below.
-
-==== !OrderEvents ====
-''Added in v04, after P7.'' The audit trail of an order: one event for its
-placement, one for every (partial) fill, and one for a cancellation. The
-`Orders` row only holds the current state; this entity keeps the history of
-how the order got there. Events are recorded automatically by the database.
-
-'''Keys:''' candidate `{id}` only; primary key '''`id`''' (auto-incrementing
-integer, events are only read in order).
-
-||= Attribute =||= Type =||= Constraints =||
-|| `id` || big integer || PK, required, auto-generated (`bigserial` in P2) ||
-|| `event_type` || text || required, `placed`, `partially_filled`, `filled` or `cancelled` ||
-|| `quantity` || numeric(20,4) || required — the ordered quantity for `placed`, the filled amount for a fill, the unfilled rest for `cancelled` ||
-|| `price` || numeric(18,6) || optional — the order price, or the trade price for a fill ||
-|| `status_after` || text || required, the order's status after the event ||
-|| `created_at` || timestamptz || required, set automatically when the event is recorded (`clock_timestamp()`, so events inside one transaction keep their real order) ||
-
-==== !MarketCandles ====
-OHLCV aggregates per market and timeframe — the data a price chart is drawn
-from. Stored rather than computed on the fly because the point of the project is
-a chart-driven interface, and re-aggregating the whole trade history for every
-screen refresh does not scale.
-
-'''Keys:''' candidate `{id}`; primary key '''`id`'''. '''Uniqueness rule:''' a market
-has exactly one candle per timeframe per time bucket, so the market a candle
-`Aggregates` together with `timeframe` and `candle_time` also identifies it.
-This is the real-world constraint that prevents duplicate candles. P2 enforces
-it as `UNIQUE(market_id, timeframe, candle_time)`.
-
-||= Attribute =||= Type =||= Constraints =||
-|| `id` || big integer || PK, required, auto-generated (`bigserial` in P2) ||
-|| `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 ====
-A named list of assets a user wants to monitor. A separate entity set rather than
-a flag on the relationship between users and assets, because a user may want
-several lists ("long term", "watching today") and each needs its own name.
-
-'''Keys:''' candidate `{id}`; primary key '''`id`'''. "Owner plus `name`" would
-also identify a list if names had to be unique per user, but the model does
-not require that, so there is no uniqueness rule here.
-
-||= Attribute =||= Type =||= Constraints =||
-|| `id` || UUID || PK, required ||
-|| `name` || text(100) || required ||
-|| `created_at` || timestamptz || required, defaults to now ||
-
-==== Holdings ====
-A user's position in one crypto asset: how much of it the user owns, how much
-of that is already promised to open sell orders, and at what average price it
-was accumulated. ''An entity set since v05'' (until v04 it was the M:N
-relationship `Holds`). A holding has its own identifier and its own
-lifecycle: it is created on the first buy, updated on every later fill, and
-the prototype reads and locks it as a unit (`SELECT … FOR UPDATE` on the sell
-path). It is linked to its owner through `Holds` and to its asset through
-`PositionIn`.
-
-'''Keys:''' candidate `{id}`; primary key '''`id`'''. '''Uniqueness rule:''' a user
-has at most one holding per crypto, so the user who `Holds` it together with
-the crypto it is a `PositionIn` also identifies a holding. P2 enforces this as
-`UNIQUE(user_id, crypto_id)`.
-
-`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, v03.
-
-||= Attribute =||= Type =||= Constraints =||
-|| `id` || UUID || PK, required ||
-|| `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, default 0, ≥ 0, '''derived''' (dashed ellipse) — the weighted average of the prices at which the position was accumulated; derivable from the buy history, stored anyway so unrealised P/L can be shown without replaying the whole ledger ||
-|| `created_at` || timestamptz || required, defaults to now ||
-|| `updated_at` || timestamptz || optional ||
-
-==== !WatchlistItems ====
-One asset placed on one watchlist. ''An entity set since v05'' (until v04 it was
-the M:N relationship `Contains`). It has its own identifier, and it is linked
-to its list through `Contains` and to its asset through `Lists`.
-
-'''Keys:''' candidate `{id}`; primary key '''`id`'''. '''Uniqueness rule:''' an asset
-appears at most once on a given list, so the watchlist that `Contains` an item
-together with the crypto it `Lists` also identifies the item. P2 enforces this
-as `UNIQUE(watchlist_id, crypto_id)`.
-
-||= Attribute =||= Type =||= Constraints =||
-|| `id` || UUID || PK, required ||
-|| `added_at` || timestamptz || required, defaults to now — recorded so a list can be shown in the order the user built it ||
-
-=== Relationships ===
-
-==== !QuotedOn — Cryptos (1) : Markets (N), total on Markets ====
-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.
-
-==== !PlacedOn — Markets (1) : Orders (N), total on Orders ====
-Records which market an order was placed on. Every order must name a market;
-a market may have no orders yet. No attributes.
-
-==== 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 — Users (1) : Transactions (N), total on Transactions ====
-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 — which is why the corresponding column is nullable in P2. No
-attributes.
-
-==== Fills — Markets (1) : !MarketTrades (N), total on !MarketTrades ====
-Every executed trade happened on exactly one market. No attributes.
-
-==== !FillsBuy — Orders (1) : !MarketTrades (N), partial on both sides ====
-''Added in v04, after P7.'' The buy order a trade filled. An order can be
-filled by many trades (partial fills); a trade fills at most one buy order,
-and none when the simulated market was the buyer. The role of `Orders` in this
-relationship is ''the buy order'' of the trade. No attributes.
-
-==== !FillsSell — Orders (1) : !MarketTrades (N), partial on both sides ====
-''Added in v04, after P7.'' The sell order a trade filled, symmetric to
-`FillsBuy`; the role of `Orders` here is ''the sell order'' of the trade. A
-trade between two users' orders participates in both. No attributes.
-
-==== Logs — Orders (1) : !OrderEvents (N), total on !OrderEvents ====
-''Added in v04, after P7.'' Every event belongs to exactly one order. No
-attributes.
-
-==== Aggregates — Markets (1) : !MarketCandles (N), total on !MarketCandles ====
-Every candle summarises trades of exactly one market. No attributes.
-
-==== Owns — Users (1) : Watchlists (N), total on Watchlists ====
-Every watchlist belongs to exactly one user. No attributes.
-
-==== Holds — Users (1) : Holdings (N), total on Holdings ====
-''1:N since v05.'' Every holding belongs to exactly one user. A user may hold
-nothing yet, so participation is partial on the `Users` side. No attributes.
-
-==== !PositionIn — Cryptos (1) : Holdings (N), total on Holdings ====
-''Added in v05.'' Every holding is a position in exactly one crypto asset. An
-asset may be held by nobody. No attributes.
-
-Together, `Holds` and `PositionIn` still say what the old M:N `Holds` said:
-a user can hold many assets and an asset can be held by many users. The
-difference is that the position is now a thing with its own identity, not
-just a pair. The rule "at most one holding per user and crypto" is stated
-under Holdings.
-
-==== Contains — Watchlists (1) : !WatchlistItems (N), total on !WatchlistItems ====
-''1:N since v05.'' Every watchlist item is on exactly one list. An empty list is
-valid, so participation is partial on the `Watchlists` side. No attributes.
-
-==== Lists — Cryptos (1) : !WatchlistItems (N), total on !WatchlistItems ====
-''Added in v05.'' Every watchlist item names exactly one crypto asset. An asset
-need not be on any list. No attributes.
-
-== Entity-Relationship Model History ==
-
- * '''v01''' — First complete version. Built from the entity notes in `ep-diagram.md` (the initial hand-written model), with three changes made to that initial model while drawing it:
-   1. `Markets` was promoted from an implied attribute of the asset to its own entity set, so that prices, orders, trades and candles can all reference a pair rather than an asset.
-   2. `holdings` and `watchlist_items` were re-expressed as the M:N relationships `Holds` and `Contains` with their own attributes, instead of entity sets with foreign keys — the initial notes listed them as tables, which is a relational concept that does not belong in a Chen ERD.
-   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 [wiki:ERModelAIUsage] for the reasoning and how the diagram file itself was produced, and [wiki:RelationalDesign] and [wiki:UseCase0005] for how the new attribute is enforced.
- * '''v04 — after P7.''' Phase 7 (order, balance and trade consistency) needed data the model did not have, so the model was extended to stay in line with the database:
-   * `Users.reserved_balance`: cash reserved by open buy orders;
-   * `Orders.filled_quantity` and the status value `partially_filled`: orders can now be filled in parts;
-   * the relationships `FillsBuy` and `FillsSell` between `Orders` and `MarketTrades`: which orders a trade filled;
-   * the entity set `OrderEvents` with the relationship `Logs`: the automatically recorded history of every order.
-
-  Nothing existing was removed or changed. See
-  [wiki:AdvancedDatabaseDevelopment].
-  The diagram files are `ERModel_v04.xml` / `ERModel_v04.png`.
- * '''v05 — correction after review.''' The review of P2 found that two parts of the model were implemented differently in the database:
-   * `Contains` was an M:N relationship in the model, but `watchlist_items` has its own `id` primary key;
-   * `Holds` was an M:N relationship in the model, but `holdings` has its own `id` primary key.
-
-  An M:N relationship has no identifier of its own; its table's key is the pair
-  of participating keys. A table with its own `id` is the implementation of an
-  entity set. Every phase after P2 (the prototype, the reports and the P7
-  logic) already uses the database as it is. So the '''model''' was corrected to
-  match P2, not the other way round:
-   * `Holds` (M:N, with attributes) became the entity set `Holdings` (its former attributes plus `id`) with two 1:N relationships, `Holds` (Users → Holdings) and `PositionIn` (Cryptos → Holdings), both total on the `Holdings` side;
-   * `Contains` (M:N, with `added_at`) became the entity set `WatchlistItems` (`id`, `added_at`) with `Contains` (Watchlists → !WatchlistItems) and `Lists` (Cryptos → !WatchlistItems), both total on the `WatchlistItems` side;
-   * the former keys of the two relationships are kept as uniqueness rules ("one holding per user and crypto", "an asset at most once per list");
-   * the key descriptions of `Markets`, `MarketTrades`, `MarketCandles` and `Watchlists` no longer name foreign-key columns (`crypto_id`, `market_id`, `user_id`), which do not exist in an ER model;
-   * the diagram was redrawn on a grid with no overlapping attributes. In v04, `Watchlists.id` was hidden behind `added_at`, and several attributes of `Orders`, `Transactions`, `MarketTrades` and `MarketCandles` overlapped. The grid also makes it easier to compare the diagram with the P2 relational diagram.
-
-  The diagram files are `ERModel_v05.xml` / `ERModel_v05.png`; earlier versions
-  are kept.
-
-Reasoning for the AI-assisted part of this phase, and the full interaction log,
-are on [wiki:ERModelAIUsage].
-
Index: cs/P1-ConceptualModel/wiki/ERModelAIUsage.md
===================================================================
--- docs/P1-ConceptualModel/wiki/ERModelAIUsage.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,327 +1,0 @@
-= Entity-Relationship Model AI Usage =
-
-== Name of AI service/solution that was used ==
-
-'''Claude Code''' (Anthropic)
-
- * '''URL:''' `https://claude.com/claude-code`
- * '''Type of service/subscription:''' Claude subscription. Session 1 used model
-   Claude Opus 4.7 (1M context); session 2 used Claude Opus 5 (1M context).
-
-== Final result ==
-
-=== Diagram ===
-
-Current version: `ERModel_v05.xml` / `ERModel_v05.png` (see session 5 below). First
-version: `ERModel_v01.xml` / `ERModel_v01.png`.
-
-'''Declaration of how the diagram was produced.''' The initial model is the
-student's own: the entity and attribute list in `ep-diagram.md`,
-written in Macedonian before any AI was involved. In session 2 the AI turned that
-list into the TerraER diagram file, and while doing so proposed three changes to
-the initial model, all three listed in the model history on
-[wiki:ERModel]:
-promoting `Markets` to its own entity set, re-expressing `holdings` and
-`watchlist_items` as M:N relationships with attributes rather than entity sets
-with foreign keys, and marking `avg_price` as derived.
-
-The diagram was not drawn by hand in the TerraER GUI. It was generated
-programmatically by constructing TerraER's own figure objects
-(`EntidadeFigure`, `RelacionamentoFigure`, `AtributoFigure`,
-`AtributoChaveFigure`, `AtributoDerivadoFigure` and the labelled line-connection
-figures) and serialising them with TerraER's own
-`DOMStorableInputOutputFormat` — the same writer the application uses when you
-choose ''Save''. The file is therefore a normal TerraER document: it was verified
-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. `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 ===
-
-See [wiki:ERModel].
-
-== Summary of AI involvement ==
-
-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
-Macedonian before any AI was involved) and my own draft schema and Go backend. I
-used the AI to review them, and it found real errors in my SQL that I had missed
-— most seriously that I had declared `crypto_id` as a foreign key to two
-different tables at once, in three separate tables. I decided the corrections to
-adopt, chose to go solo, chose a CLI prototype over an HTTP one, and chose
-English for the documentation. The output of that session was the corrected
-schema, the sample-data script, the use-case model and the working prototype.
-
-In '''session 2''' I came back to produce the deliverable I was still missing — the
-TerraER diagram file itself — and to finish the P4 documentation. I also asked
-for a review pass over the database and Go code, which turned up three further
-bugs (see [wiki:PrototypeImplementationAIUsage]).
-
-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
-[wiki:ERModel] (v03) and the log
-below.
-
-Below is the log, one entry per exchange. My prompts are quoted as '''excerpts''':
-the wording is my own, unaltered, but I have left out scheduling remarks and
-other working chatter that says nothing about the model or the code. Typos are
-kept as they were. The ''Intent'' line above each prompt is my own note on what I
-was trying to achieve. AI responses are summarised rather than quoted in full,
-because several ran to many pages of code and file edits. The complete
-unabridged transcript is in my Claude Code session history and can be produced
-on request.
-
-== Entire AI usage log ==
-
-----
-
-=== Session 1 — 2026-04-21 ===
-
-'''Intent:''' hand the AI the whole project and the course rubric, and have it
-audit my existing model and code against the phase requirements before I commit
-to anything.
-
-'''Prompt (student, excerpt):'''
-> This is my project for my exam Database, go to the docs folder and README.md there you will find all of the documentation, afterwards take a look at the whole project. Delete everything you want add everything you want just make it work and make it fill in or to follow all of the needed instructions. Ask me anything, also tell me what do I need to do
-
-'''Response (AI, summarised):'''
- * Read `docs/opis.md`, `docs/ep-diagram.md`, `docs/dbdiagram.md`, `server/db/db.sql`, `server/db/schema.sql`, all Go sources.
- * Flagged bugs in the initial schema:
-   * `holdings.crypto_id` declared as a foreign key to both `users(id)` and `crypto(id)`; should be a separate `user_id` column.
-   * Same mistake in `orders` and `transactions`.
-   * `market_candles` referenced a non-existent `markets` table.
-   * `main.go` dropped and recreated all tables on every startup, wiping data.
- * Proposed a single authoritative schema, to be placed in `server/db/schema_creation.sql`, based on `docs/ep-diagram.md` and `docs/dbdiagram.md` with the above issues fixed.
- * Asked the student for decisions: team composition, prototype shape (CLI vs HTTP), whether to keep `available_balance`, `invested_balance`, `secret_question`, `secret_answer`, which use cases to implement, language of documentation.
-
-'''Intent:''' I had just pasted the course rubric into `README.md` and removed the
-half-finished frontend, so that the AI would plan against the actual phase
-requirements and against a backend-only prototype.
-
-'''Prompt (student, excerpt):'''
-> Read it now, just filled it in, also deleted the frontend we don' need that
-
-'''Response (AI, summarised):'''
- * Re-read `docs/README.md`, which now contained the course phase rubric (P0–P9).
- * Explained the constraints each phase places on AI usage (P0 forbids AI; P1–P4 allow AI only to improve the student's own initial model and require an AIUsage log).
- * Listed the deliverables per phase and asked the remaining questions.
-
-----
-
-=== Session 2 — 2026-08-06 / 2026-08-07 ===
-
-'''Intent:''' re-read the rubric from scratch after the gap, reduce it to a
-checklist of what each phase actually has to produce, confirm TerraER runs on
-this machine.
-
-'''Prompt (student, excerpt):'''
-> We will start from the begining in some ways, please read first docs/insturctions.md
-> The goal is to finish with milestone 0 so I can publish it […]
-> Read the Instuctions.md like I said and delete everything that you think we don't need
-> and for each phrase tell me what do I need to submit and how to do it. and can you also
-> do the thing with TerraER3.11.jar?
-
-'''Response (AI, summarised):'''
- * Read the phase rubric and condensed `Instructions.md` to a per-phase checklist
-   of deliverables, keeping every requirement and every load-bearing filename.
- * Confirmed TerraER 3.11 runs on the installed OpenJDK 21 and launched it.
- * Listed, per phase, what has to be submitted and how; flagged that P0's
-   deliverables are wiki pages, that the EPRMS proposal is a prerequisite.
- * Flagged `image.png` (a !TradingView screenshot) as material the project has no
-   licence to publish.
-
-'''Intent:''' produce the artefacts still missing for P1–P4 — above all the TerraER
-diagram — while keeping P0 for myself, since AI use is forbidden there.
-
-'''Prompt (student, excerpt):'''
-> […] do all of the other Phases till m0.
-> opis.md It's p0 so I will take care of that. Delete anything that we don't need,
-> make all of the phases and terra diagram if you can, and delete anything
-> that we don't need and make a documentation about how to start it.
-
-'''Intent:''' ask for a review pass over the schema and the Go code rather than
-only documentation, on the grounds that a prototype I have to defend in person
-should not have known defects in it.
-
-'''Prompt (student, excerpt, follow-up):'''
-> Also fix some database things or golang things if you think we can do it better,
-
-'''Response (AI, summarised) — the part relevant to P1:'''
- * Read `ep-diagram.md` (the student's own initial model) and the existing
-   `schema_creation.sql`.
- * Reverse-engineered TerraER's file format from the distributed jar to learn the
-   element names it stores figures under (`ent`, `rel`, `atr`, `atrchave`,
-   `atrderivado`, `llabelUm`, `llabelMuitos`, `llabelDoubleUm`,
-   `llabelDoubleMuitos`, …).
- * Generated `ERModel_v01.xml` and `ERModel_v01.png` as described above, in Chen
-   notation: 8 entity sets, 10 relationships, 57 attributes, cardinality labels
-   on every relationship line and double lines for total participation.
- * Proposed the three changes to the initial model recorded in the model history.
- * Rewrote [wiki:ERModel] with the per-entity documentation, candidate-key
-   justifications and attribute types the phase template requires.
-
-----
-
-> '''Student action required.''' Two things, in this order:
->
-> 1. Open `ERModel_v01.xml` in TerraER, read the whole diagram, and change what
->    you disagree with. Save the result as `ERModel_v02.xml` with a matching PNG
->    and add a history line. The phase rules require that the model be yours;
->    the generated v01 is a starting point to review and take over, not an
->    answer to submit unread.
-> 2. Verify that this log matches your recollection and append the full text of
->    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 [wiki:ERModel] 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
-   [wiki:UseCase0005Implementation].
-
-'''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 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`.
-
-
-=== Session 4 — 2026-09-24 (Claude Opus 5.5): v04 after P7 ===
-
-'''Prompts (student, verbatim):'''
-> But this order_events is added after Phase 7 right? can we add that too?
-
-> can you make the ERmodel again with TerraER file to update it? and tell that after P7 we added this
-
-> Make it with a white background like earier versions
-
-'''Response (AI, summarised):'''
-
- * Explained that the P7 changes must also appear in P1 and P2, since both must describe the current data structure.
- * Built `ERModel_v04.xml` in TerraER's own file format by taking `ERModel_v03.xml` unchanged and appending the new elements with the same XML structure TerraER uses:
-   * the attribute `reserved_balance` on `Users`;
-   * the attribute `filled_quantity` on `Orders`;
-   * the relationships `FillsBuy` and `FillsSell` (Orders 1 : N !MarketTrades, partial);
-   * the entity set `OrderEvents` (key `id`, `event_type`, `quantity`, `price`, `status_after`, `created_at`) with `Logs` (Orders 1 : N !OrderEvents, total on !OrderEvents).
- * Rendered `ERModel_v04.png` with TerraER 3.14's own drawing classes (loading the `.xml` exactly as TerraER does and using its image export), on a white background and trimmed like the earlier versions.
- * Updated [wiki:ERModel] (title v.04, new attribute rows, the `OrderEvents` section, the three relationships, and a v04 history entry stating these were added after P7).
-
-'''What I decided:''' to add the P7 structure to the ER model. The new elements are placed
-automatically, so the layout can be tidied by hand in TerraER.
-
-=== Session 5 — 2026-09-29 (Claude Opus 5.5): v05, correction after review ===
-
-'''Prompt (student, excerpt):''' I pasted the reviewer's comments that `Holds` and `Contains`
-are implemented differently in the database than in the model, with the suggestion to
-"correct only the Phase 1 model so that it matches Phase 2", and asked:
-
-> Can you take a look at this stuff? and fix them, but don't push anything just make the
-> changes requested and try to explain it to me to see if you understand the problem with
-> the solution
-
-'''Response (AI, summarised):'''
-
- * Explained that an M:N relationship has no identifier of its own, while `holdings` and `watchlist_items` each have an `id` primary key, so the database treats them as entity sets.
- * Changed the model to v05. `Holdings` (`id`, `quantity`, `reserved_quantity`, derived `avg_price`, `created_at`, `updated_at`) and `WatchlistItems` (`id`, `added_at`) became entity sets, with the 1:N relationships `Holds`, `PositionIn`, `Contains` and `Lists`, each total on the new entity's side. The old relationship keys are now stated as uniqueness rules. The key descriptions no longer name foreign-key columns.
- * Generated `ERModel_v05.xml` / `ERModel_v05.png` from scratch with TerraER 3.11's own figure classes and writer (adapted from the v01 generator), on a grid, with no overlapping attributes. It was verified by reading the file back with TerraER's reader (184 figures) and by inspecting the rendered PNG.
- * Updated [wiki:ERModel] (v05 sections and history entry).
-
-'''What I decided:''' to follow the reviewer's advice and change the model rather than the
-database, since every later phase already uses the database as it is.
Index: cs/P2-RelationalDesign/RelationalDesign.md
===================================================================
--- docs/P2-RelationalDesign/RelationalDesign.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,249 +1,0 @@
-# Relational Design
-
-This page transforms [ERModel](../P1-ConceptualModel/ERModel.md) **v05** into
-relations. Every relation below corresponds to exactly one entity set of the
-model, and every foreign key corresponds to exactly one relationship, so the
-two diagrams can be compared box for box and line for line (see
-[Relational diagram](#relational-diagram)).
-
-## Descriptive representation of the relational schema
-
-Notation: **bold** = primary key, *italic* = foreign key. After each foreign key
-comes the ER relationship it implements.
-
-- **Users**(<u>**id**</u>, username, email, full_name, password_hash, available_balance, invested_balance, reserved_balance, created_at, updated_at)
-  - Entity set `Users`. Candidate keys: `{id}`, `{username}`, `{email}`. `UNIQUE(username)`, `UNIQUE(email)`.
-- **Crypto**(<u>**id**</u>, symbol, name, created_at)
-  - Entity set `Cryptos`. Candidate keys: `{id}`, `{symbol}`. `UNIQUE(symbol)`.
-- **Markets**(<u>**id**</u>, *crypto_id* [`QuotedOn`], quote_currency, is_active, created_at)
-  - Entity set `Markets`. Candidate keys: `{id}`, `{crypto_id, quote_currency}`
-    (the model's rule "a crypto is quoted at most once per currency"),
-    enforced with `UNIQUE(crypto_id, quote_currency)`.
-- **Holdings**(<u>**id**</u>, *user_id* [`Holds`], *crypto_id* [`PositionIn`], quantity, reserved_quantity, avg_price, created_at, updated_at)
-  - Entity set `Holdings`. Candidate keys: `{id}` and `{user_id, crypto_id}`
-    (the model's rule "one holding per user and crypto"), enforced with
-    `UNIQUE(user_id, crypto_id)`.
-  - `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#holdings)
-    for why this mirrors `available_balance`/`invested_balance` on `Users`.
-- **Orders**(<u>**id**</u>, *user_id* [`Places`], *market_id* [`PlacedOn`], side, type, status, quantity, filled_quantity, price, placed_at, executed_at)
-  - Entity set `Orders`. `side ∈ {buy, sell}`, `type ∈ {market, limit}`,
-    `status ∈ {open, partially_filled, executed, cancelled}`,
-    `0 ≤ filled_quantity ≤ quantity`.
-- **Transactions**(<u>**id**</u>, *user_id* [`Records`], type, amount, currency, *related_order* [`Settles`], created_at, description)
-  - Entity set `Transactions`. `type ∈ {deposit, buy, sell, fee}`.
-    `related_order` is nullable (see below).
-- **MarketTrades**(<u>**id**</u>, *market_id* [`Fills`], executed_at, price, quantity, side, source, *buy_order_id* [`FillsBuy`], *sell_order_id* [`FillsSell`])
-  - Entity set `MarketTrades`. `buy_order_id` and `sell_order_id` are both
-    nullable (see below).
-- **OrderEvents**(<u>**id**</u>, *order_id* [`Logs`], event_type, quantity, price, status_after, created_at)
-  - Entity set `OrderEvents`. `event_type ∈ {placed, partially_filled, filled, cancelled}`.
-- **MarketCandles**(<u>**id**</u>, *market_id* [`Aggregates`], timeframe, open, high, low, close, volume, candle_time)
-  - Entity set `MarketCandles`. Candidate keys: `{id}`, `{market_id, timeframe,
-    candle_time}` (the model's rule "one candle per market, timeframe and
-    bucket"), enforced with `UNIQUE(market_id, timeframe, candle_time)`.
-- **Watchlists**(<u>**id**</u>, *user_id* [`Owns`], name, created_at)
-  - Entity set `Watchlists`.
-- **WatchlistItems**(<u>**id**</u>, *watchlist_id* [`Contains`], *crypto_id* [`Lists`], added_at)
-  - Entity set `WatchlistItems`. Candidate keys: `{id}` and `{watchlist_id,
-    crypto_id}` (the model's rule "an asset at most once per list"), enforced
-    with `UNIQUE(watchlist_id, crypto_id)`.
-
-### Transformation method used
-
-**Partial transformation.** The model has 11 entity sets and 15 relationships.
-Every relationship is binary and 1:N with no attributes of its own (the two M:N
-relationships of earlier versions, `Holds` and `Contains`, were corrected into
-the entity sets `Holdings` and `WatchlistItems` in v05). The rules:
-
-- **Each entity set becomes one relation**, with its own attributes and its
-  own key `id` as primary key. 11 entity sets → 11 relations.
-- **Each 1:N relationship becomes one foreign key** on the relation of the "N"
-  side, pointing to the primary key of the "1" side. No relationship gets its
-  own table, because none is M:N and none has attributes. 15 relationships →
-  15 foreign keys:
-
-  | ER relationship | 1 side → N side | Foreign key | Participation of the N side | `NULL`? |
-  |---|---|---|---|---|
-  | `QuotedOn`   | Cryptos → Markets             | `markets.crypto_id`            | total   | `NOT NULL` |
-  | `PlacedOn`   | Markets → Orders              | `orders.market_id`             | total   | `NOT NULL` |
-  | `Places`     | Users → Orders                | `orders.user_id`               | total   | `NOT NULL` |
-  | `Records`    | Users → Transactions          | `transactions.user_id`         | total   | `NOT NULL` |
-  | `Settles`    | Orders → Transactions         | `transactions.related_order`   | partial | nullable |
-  | `Fills`      | Markets → MarketTrades        | `market_trades.market_id`      | total   | `NOT NULL` |
-  | `FillsBuy`   | Orders → MarketTrades         | `market_trades.buy_order_id`   | partial | nullable |
-  | `FillsSell`  | Orders → MarketTrades         | `market_trades.sell_order_id`  | partial | nullable |
-  | `Logs`       | Orders → OrderEvents          | `order_events.order_id`        | total   | `NOT NULL` |
-  | `Aggregates` | Markets → MarketCandles       | `market_candles.market_id`     | total   | `NOT NULL` |
-  | `Owns`       | Users → Watchlists            | `watchlists.user_id`           | total   | `NOT NULL` |
-  | `Holds`      | Users → Holdings              | `holdings.user_id`             | total   | `NOT NULL` |
-  | `PositionIn` | Cryptos → Holdings            | `holdings.crypto_id`           | total   | `NOT NULL` |
-  | `Contains`   | Watchlists → WatchlistItems   | `watchlist_items.watchlist_id` | total   | `NOT NULL` |
-  | `Lists`      | Cryptos → WatchlistItems      | `watchlist_items.crypto_id`    | total   | `NOT NULL` |
-
-- **Participation decides `NULL`.** Total participation of the N side means
-  every row must reference a parent, so the foreign key is `NOT NULL`. Partial
-  participation leaves it nullable. There are exactly three partial ones:
-  `Settles` (a deposit has no originating order), and `FillsBuy` / `FillsSell`
-  (a trade against the simulated market has no user order on that side).
-  Partial participation of the **1** side (for example, a user with no orders)
-  needs no column at all. It simply means no row points at that parent.
-- **Uniqueness rules of the model become `UNIQUE` constraints.** The four
-  rules the model states in words ("a crypto quoted once per currency", "one
-  candle per market, timeframe and bucket", "one holding per user and crypto",
-  "an asset once per list") involve a relationship, so Chen notation cannot
-  draw them as keys. After transformation, the relationship is a foreign-key
-  column, and each rule becomes an ordinary composite `UNIQUE` constraint, i.e. a
-  second candidate key.
-
-Nothing in the schema comes from anywhere else. Every column is either an ER
-attribute or the foreign key of one listed relationship.
-
-### Normalisation
-
-> **Checked in P5.** [Normalization](../P5-Normalization/Normalization.md)
-> starts from a single de-normalized relation containing only the attributes
-> of the ER model and the functional dependencies that follow from its rules.
-> It decomposes that relation step by step to BCNF and arrives at these same 11
-> relations, with one deliberate difference: `transactions.user_id` (see the last
-> bullet below). The comparison is in the *Discussion* section at the end of that
-> page.
-
-All relations except `transactions` are in **BCNF**, as P5 shows. `transactions`
-is in 2NF but not in 3NF, because of the deliberately kept `user_id` (last
-bullet):
-
-- Every attribute is atomic (no repeating groups, no composite fields).
-- No partial dependency exists: every candidate key is either the single
-  column `id` or a composite key (`{user_id, crypto_id}`, …) on which no
-  non-key attribute depends only partially.
-- No transitive dependency exists, except `transactions.user_id` (last bullet):
-  every other non-key attribute depends directly on the row's own entity, never
-  on another entity reached through a foreign key.
-  For example, `holdings.quantity` depends on `holdings.id`, and nothing about
-  the user or the crypto is copied into `holdings`.
-- `avg_price` in `Holdings` is a **derived value** cached for performance (it is
-  the weighted-average entry price across all `buy` transactions for that
-  `(user, crypto)` pair) — it is drawn as a derived attribute in the ER diagram.
-  We accept the denormalisation: it is recomputed by the database inside the same
-  transaction as each buy, in the same statement that changes the quantity
-  (`INSERT … ON CONFLICT (user_id, crypto_id) DO UPDATE`), so the stored average
-  and the stored quantity can never disagree.
-- `avg_price` is declared `NOT NULL DEFAULT 0`. This matters: it is used in the
-  P/L arithmetic of `v_portfolio`, and in SQL any arithmetic involving `NULL`
-  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.
-- `transactions.user_id` is kept **deliberately**, although for an entry that
-  settles an order it repeats that order's user (`related_order → user_id`, a
-  transitive dependency). A deposit has no order (`Settles` is partial), so
-  `user_id` is the only way to record whose deposit it is. For entries with an
-  order, the only code that sets `related_order` (the buy and sell inserts in
-  `advanced_db.sql`) writes both from the same order row. No database
-  constraint enforces this.
-
-### 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. The cash side of a buy order
-(`users.reserved_balance`), `orders.filled_quantity` and `order_events` were
-added in P7; see
-[AdvancedDatabaseDevelopment](../P7-AdvancedDatabaseDevelopment/AdvancedDatabaseDevelopment.md).
-
-## DDL script
-
-The script that creates the schema is [`../server/db/schema_creation.sql`](../../server/db/schema_creation.sql). It is idempotent: it drops and recreates the `project` schema every run, so it works on an empty database and on a database that already has the schema.
-
-The script creates:
-- 10 of the 11 tables, with check constraints, primary keys, foreign keys and unique constraints.
-- 8 performance indexes.
-- 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`).
-
-The 11th table, `order_events`, is created by
-[`../server/db/advanced_db.sql`](../../server/db/advanced_db.sql) together with
-the P7 triggers that fill it. `./eduberza -init` runs both scripts in that
-order, so a freshly initialised database always has all 11 tables and all 15
-foreign keys.
-
-## DML script (sample data)
-
-The script that loads realistic sample data is [`../server/db/data_load.sql`](../../server/db/data_load.sql). It is idempotent: it truncates all tables with `CASCADE` then re-inserts. Loaded:
-- 5 crypto assets (BTC, ETH, ADA, SOL, DOGE) and 5 USD-quoted markets.
-- 3 sample users (`alice`, `bob`, `charlie`) with password `test123` (sha256 hex).
-- 18 recent market trades across all markets so `v_latest_prices` is populated.
-- 10 one-hour candles (BTC and ETH).
-- One fully-executed market-buy order for Alice, the matching holding, and two ledger entries (deposit + buy), with Alice's balances updated accordingly.
-- Two watchlists with five watchlist items.
-
-## Relational diagram
-
-![relational_diagram_v4](relational_diagram_v4.png)
-
-Generated in **DBeaver** from the **live** `project` schema (after
-`./eduberza -init`), not drawn by hand, so it shows what the deployed database
-actually contains. Each box is a table with its columns; the key icon marks the
-primary key, and the lines are the 15 declared foreign keys. The two foreign keys from
-`market_trades` to `orders` (`buy_order_id`, `sell_order_id`) connect the same
-two boxes, so DBeaver draws them on top of each other as one line.
-
-The tables are arranged in the **same positions** as the entity sets in
-`ERModel_v05.png`, so the two can be compared directly:
-
-- every rectangle of the ER diagram is one table in the same place;
-- every diamond of the ER diagram is one foreign-key line between the same two
-  boxes. The dot is on the referencing ("N") table, next to the foreign-key
-  column;
-- a double (total) line in the ER diagram is a `NOT NULL` foreign key, drawn by
-  DBeaver as a solid line. The three single lines on the N side (`Settles`,
-  `FillsBuy`, `FillsSell`) are the three nullable foreign keys, which DBeaver
-  draws dashed, with a hollow diamond on the `orders` side. The table under
-  [Transformation method used](#transformation-method-used) lists all 15.
-
-Earlier images (`relational_schema.jpg`, `relational_schema_v2.png`,
-`relational_schema_v3.png`) were exported from pgAdmin, with a different layout
-and from an older schema. They are kept only as history.
-
-### How to regenerate it
-
-1. Initialise the database: `./eduberza -init` (runs `schema_creation.sql` and
-   `advanced_db.sql`, so `order_events` is included).
-2. In DBeaver, connect to the project database and expand
-   *Schemas → project → Tables*.
-3. Select all 11 tables → right-click → **View Diagram** (or create a new ER
-   diagram and drag the tables in).
-4. Drag each table to the position of its entity set in `ERModel_v05.png`:
-
-   ```
-            column 1          column 2        column 3        column 4
-   row 1    watchlist_items   crypto          markets         market_candles
-   row 2    watchlists        holdings        .               market_trades
-   row 3    users             .               orders          .
-   row 4    .                 transactions    order_events    .
-   ```
-
-   Leave the empty cells (`.`) empty. They are where the relationship
-   diamonds are in the ER diagram, so the foreign-key lines will run through
-   the same gaps.
-
-5. Right-click the canvas → **Export diagram** → PNG, saved as
-   `relational_diagram_v4.png` in this folder.
Index: cs/P2-RelationalDesign/RelationalDesignAIUsage.md
===================================================================
--- docs/P2-RelationalDesign/RelationalDesignAIUsage.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,135 +1,0 @@
-# Relational Design AI Usage
-
-## Name of AI service/solution that was used
-
-**Claude Code** (Anthropic)
-
-- **URL:** https://claude.com/claude-code
-- **Type of service/subscription:** Claude subscription, model Claude Opus 4.7 (1M context).
-
-## Final result
-
-### Diagram
-
-`relational_diagram_v4.png` was exported by the student in DBeaver from the live `project` schema, with the tables in the same positions as the entity sets of `ERModel_v05.png`; see [RelationalDesign](RelationalDesign.md#how-to-regenerate-it) for instructions.
-
-### Results in details / description
-
-The AI:
-
-- Consolidated two inconsistent draft schemas (`server/db/db.sql` and `server/db/schema.sql`) into a single `schema_creation.sql`.
-- Corrected foreign-key errors in the original (the `crypto_id` column in `holdings`, `orders`, and `transactions` had been pointed at both `users(id)` and `crypto(id)`; the AI split it into separate `user_id` and `crypto_id` columns per `ep-diagram.md`).
-- Added two convenience views, `v_latest_prices` and `v_portfolio`, to keep the Go CLI simple.
-- Produced a sample-data script `data_load.sql` that TRUNCATEs and re-inserts deterministic rows, so the "must work on empty DB and on a DB that already has data" requirement from P2 is met.
-- Documented normalisation up to 3NF and the intentional denormalisation of `holdings.avg_price`.
-
-## Summary of AI involvement
-
-| | Session 1 — 2026-04-21 | Session 2 — 2026-08-06/07 |
-|---|---|---|
-| **What I brought** | My own draft SQL (`db.sql`, `schema.sql`) and the model in `ep-diagram.md` | The schema as it stood after session 1 |
-| **What the AI did** | Reviewed my SQL, found the foreign-key errors, consolidated two inconsistent drafts into one script | Reviewed the schema again; one constraint change, plus documentation of the transformation |
-| **What I decided** | Which corrections to adopt, to keep both balance columns, to drop the secret-question fields | To make `avg_price` `NOT NULL` rather than handle nulls in application code |
-
-The relational model in this phase is a transformation of *my* ER model, and the
-foreign-key errors the AI found in session 1 were errors in *my* draft SQL — that
-review is the single most useful thing the AI did on this phase.
-
-## Entire AI usage log
-
-See [ERModelAIUsage](../P1-ConceptualModel/ERModelAIUsage.md) — the full transcript of the 2026-04-21 conversation covers both P1 and P2 work. The specific prompts that drove the relational-design output were the same "make it work and make it fill in or to follow all of the needed instructions" instruction and the student's subsequent "do everything that you need to do".
-
-> **Student action required:** append any future consultations where you asked the AI to refine the schema, tune constraints, or write additional queries.
-
-
-### Session 2 — 2026-08-06 / 2026-08-07
-
-Prompts are logged in full in [ERModelAIUsage](../P1-ConceptualModel/ERModelAIUsage.md#session-2--2026-08-06--2026-08-07);
-the one that drove this phase was *"Also fix some database things or golang
-things if you think we can do it better"*. Changes to the P2 artefacts:
-
-- `holdings.avg_price` changed from nullable to `NOT NULL DEFAULT 0 CHECK
-  (avg_price >= 0)`. Reason: it feeds the P/L arithmetic in `v_portfolio`, and
-  SQL arithmetic involving `NULL` produces `NULL`, so a nullable average would
-  have silently blanked the unrealised-P/L column of a real position. A related
-  crash path in the Go code (scanning a `NULL` average into a non-nullable
-  `float64`, which was reported to the user as "Insufficient holding") was fixed
-  at the same time.
-- [RelationalDesign](RelationalDesign.md) gained an explicit account of the
-  partial transformation: which ER construct each foreign key comes from, that
-  M:N relationships with attributes become tables whose foreign-key pair is a
-  `UNIQUE` constraint, and that total participation becomes `NOT NULL` — which is
-  why `transactions.related_order` is the one nullable foreign key.
-- The candidate keys of `holdings` and `watchlist_items` are now documented as
-  the relationship keys `{user_id, crypto_id}` and `{watchlist_id, crypto_id}`.
-
-Both scripts were re-run end to end against PostgreSQL 16 after these changes.
-
-> **Still outstanding:** `relational_schema.jpg` must be exported from DBeaver
-> 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.
-
-### Session 4 — 2026-09-29 (Claude Opus 5.5): correction after review
-
-**Prompt (student, excerpt):** I pasted the reviewer's comments on P2:
-
-> the `contains` relationship from the model is implemented differently in the database
-> the `holds` relationship from the model is implemented differently in the database
-> there may be other inconsistencies that are difficult to spot; the Phase 2 diagram should
-> have been created using DBeaver with a layout visually similar to the ERD
-
-> I suggest correcting only the Phase 1 model so that it matches Phase 2 [...] For Phase 2,
-> simply update the image according to the requirements.
-
-**Response (AI, summarised):**
-
-- Explained the mismatch. In the model, `Holds` and `Contains` were M:N relationships, whose
-  table key would be the pair `{user_id, crypto_id}` / `{watchlist_id, crypto_id}`. But
-  `holdings` and `watchlist_items` have their own `id` primary key, which is how an entity
-  set is implemented. Following the reviewer's advice, P1 was corrected (v05, entity sets
-  `Holdings` and `WatchlistItems`), and the database was not changed.
-- Found the other inconsistencies between this page and the live schema. The page was
-  missing `users.reserved_balance`, `orders.filled_quantity`, the status
-  `partially_filled`, `market_trades.buy_order_id` / `sell_order_id` and the whole
-  `order_events` table. The "10 tables", "5 indexes" and "the one nullable foreign key"
-  counts were also out of date (really 11 tables, 8 indexes in `schema_creation.sql`, and 3
-  nullable foreign keys).
-- Rewrote [RelationalDesign](RelationalDesign.md). Each relation is labelled with its entity
-  set and each foreign key with its relationship, and the transformation is a table of all
-  15 relationships → 15 foreign keys, with `NOT NULL` following participation.
-- Wrote export instructions with a table grid that mirrors `ERModel_v05.png`.
-
-**What I decided:** to correct P1 instead of the database, as the reviewer suggested. I
-exported `relational_diagram_v4.png` from DBeaver myself, with the tables arranged like the
-ER diagram.
Index: cs/P2-RelationalDesign/wiki/RelationalDesign.md
===================================================================
--- docs/P2-RelationalDesign/wiki/RelationalDesign.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,578 +1,0 @@
-= Relational Design =
-
-This page transforms [wiki:ERModel] '''v05''' into
-relations. Every relation below corresponds to exactly one entity set of the
-model, and every foreign key corresponds to exactly one relationship, so the
-two diagrams can be compared box for box and line for line (see
-Relational diagram).
-
-== Descriptive representation of the relational schema ==
-
-Notation: '''bold''' = primary key, ''italic'' = foreign key. After each foreign key
-comes the ER relationship it implements.
-
- * '''Users'''(__'''id'''__, username, email, full_name, password_hash, available_balance, invested_balance, reserved_balance, created_at, updated_at)
-   * Entity set `Users`. Candidate keys: `{id}`, `{username}`, `{email}`. `UNIQUE(username)`, `UNIQUE(email)`.
- * '''Crypto'''(__'''id'''__, symbol, name, created_at)
-   * Entity set `Cryptos`. Candidate keys: `{id}`, `{symbol}`. `UNIQUE(symbol)`.
- * '''Markets'''(__'''id'''__, ''crypto_id'' [`QuotedOn`], quote_currency, is_active, created_at)
-   * Entity set `Markets`. Candidate keys: `{id}`, `{crypto_id, quote_currency}` (the model's rule "a crypto is quoted at most once per currency"), enforced with `UNIQUE(crypto_id, quote_currency)`.
- * '''Holdings'''(__'''id'''__, ''user_id'' [`Holds`], ''crypto_id'' [`PositionIn`], quantity, reserved_quantity, avg_price, created_at, updated_at)
-   * Entity set `Holdings`. Candidate keys: `{id}` and `{user_id, crypto_id}` (the model's rule "one holding per user and crypto"), enforced with `UNIQUE(user_id, crypto_id)`.
-   * `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 [wiki:UseCase0005]. See [wiki:ERModel] (section "Holdings") for why this mirrors `available_balance`/`invested_balance` on `Users`.
- * '''Orders'''(__'''id'''__, ''user_id'' [`Places`], ''market_id'' [`PlacedOn`], side, type, status, quantity, filled_quantity, price, placed_at, executed_at)
-   * Entity set `Orders`. `side ∈ {buy, sell}`, `type ∈ {market, limit}`, `status ∈ {open, partially_filled, executed, cancelled}`, `0 ≤ filled_quantity ≤ quantity`.
- * '''Transactions'''(__'''id'''__, ''user_id'' [`Records`], type, amount, currency, ''related_order'' [`Settles`], created_at, description)
-   * Entity set `Transactions`. `type ∈ {deposit, buy, sell, fee}`. `related_order` is nullable (see below).
- * '''!MarketTrades'''(__'''id'''__, ''market_id'' [`Fills`], executed_at, price, quantity, side, source, ''buy_order_id'' [`FillsBuy`], ''sell_order_id'' [`FillsSell`])
-   * Entity set `MarketTrades`. `buy_order_id` and `sell_order_id` are both nullable (see below).
- * '''!OrderEvents'''(__'''id'''__, ''order_id'' [`Logs`], event_type, quantity, price, status_after, created_at)
-   * Entity set `OrderEvents`. `event_type ∈ {placed, partially_filled, filled, cancelled}`.
- * '''!MarketCandles'''(__'''id'''__, ''market_id'' [`Aggregates`], timeframe, open, high, low, close, volume, candle_time)
-   * Entity set `MarketCandles`. Candidate keys: `{id}`, `{market_id, timeframe, candle_time}` (the model's rule "one candle per market, timeframe and bucket"), enforced with `UNIQUE(market_id, timeframe, candle_time)`.
- * '''Watchlists'''(__'''id'''__, ''user_id'' [`Owns`], name, created_at)
-   * Entity set `Watchlists`.
- * '''!WatchlistItems'''(__'''id'''__, ''watchlist_id'' [`Contains`], ''crypto_id'' [`Lists`], added_at)
-   * Entity set `WatchlistItems`. Candidate keys: `{id}` and `{watchlist_id, crypto_id}` (the model's rule "an asset at most once per list"), enforced with `UNIQUE(watchlist_id, crypto_id)`.
-
-=== Transformation method used ===
-
-'''Partial transformation.''' The model has 11 entity sets and 15 relationships.
-Every relationship is binary and 1:N with no attributes of its own (the two M:N
-relationships of earlier versions, `Holds` and `Contains`, were corrected into
-the entity sets `Holdings` and `WatchlistItems` in v05). The rules:
-
- * '''Each entity set becomes one relation''', with its own attributes and its own key `id` as primary key. 11 entity sets → 11 relations.
- * '''Each 1:N relationship becomes one foreign key''' on the relation of the "N" side, pointing to the primary key of the "1" side. No relationship gets its own table, because none is M:N and none has attributes. 15 relationships → 15 foreign keys:
-
-  ||= ER relationship =||= 1 side → N side =||= Foreign key =||= Participation of the N side =||= `NULL`? =||
-  || `QuotedOn` || Cryptos → Markets || `markets.crypto_id` || total || `NOT NULL` ||
-  || `PlacedOn` || Markets → Orders || `orders.market_id` || total || `NOT NULL` ||
-  || `Places` || Users → Orders || `orders.user_id` || total || `NOT NULL` ||
-  || `Records` || Users → Transactions || `transactions.user_id` || total || `NOT NULL` ||
-  || `Settles` || Orders → Transactions || `transactions.related_order` || partial || nullable ||
-  || `Fills` || Markets → !MarketTrades || `market_trades.market_id` || total || `NOT NULL` ||
-  || `FillsBuy` || Orders → !MarketTrades || `market_trades.buy_order_id` || partial || nullable ||
-  || `FillsSell` || Orders → !MarketTrades || `market_trades.sell_order_id` || partial || nullable ||
-  || `Logs` || Orders → !OrderEvents || `order_events.order_id` || total || `NOT NULL` ||
-  || `Aggregates` || Markets → !MarketCandles || `market_candles.market_id` || total || `NOT NULL` ||
-  || `Owns` || Users → Watchlists || `watchlists.user_id` || total || `NOT NULL` ||
-  || `Holds` || Users → Holdings || `holdings.user_id` || total || `NOT NULL` ||
-  || `PositionIn` || Cryptos → Holdings || `holdings.crypto_id` || total || `NOT NULL` ||
-  || `Contains` || Watchlists → !WatchlistItems || `watchlist_items.watchlist_id` || total || `NOT NULL` ||
-  || `Lists` || Cryptos → !WatchlistItems || `watchlist_items.crypto_id` || total || `NOT NULL` ||
-
- * '''Participation decides `NULL`.''' Total participation of the N side means every row must reference a parent, so the foreign key is `NOT NULL`. Partial participation leaves it nullable. There are exactly three partial ones: `Settles` (a deposit has no originating order), and `FillsBuy` / `FillsSell` (a trade against the simulated market has no user order on that side). Partial participation of the '''1''' side (for example, a user with no orders) needs no column at all. It simply means no row points at that parent.
- * '''Uniqueness rules of the model become `UNIQUE` constraints.''' The four rules the model states in words ("a crypto quoted once per currency", "one candle per market, timeframe and bucket", "one holding per user and crypto", "an asset once per list") involve a relationship, so Chen notation cannot draw them as keys. After transformation, the relationship is a foreign-key column, and each rule becomes an ordinary composite `UNIQUE` constraint, i.e. a second candidate key.
-
-Nothing in the schema comes from anywhere else. Every column is either an ER
-attribute or the foreign key of one listed relationship.
-
-=== Normalisation ===
-
-> '''Checked in P5.''' [wiki:Normalization]
-> starts from a single de-normalized relation containing only the attributes
-> of the ER model and the functional dependencies that follow from its rules.
-> It decomposes that relation step by step to BCNF and arrives at these same 11
-> relations, with one deliberate difference: `transactions.user_id` (see the last
-> bullet below). The comparison is in the ''Discussion'' section at the end of that
-> page.
-
-All relations except `transactions` are in '''BCNF''', as P5 shows. `transactions`
-is in 2NF but not in 3NF, because of the deliberately kept `user_id` (last
-bullet):
-
- * Every attribute is atomic (no repeating groups, no composite fields).
- * No partial dependency exists: every candidate key is either the single column `id` or a composite key (`{user_id, crypto_id}`, …) on which no non-key attribute depends only partially.
- * No transitive dependency exists, except `transactions.user_id` (last bullet): every other non-key attribute depends directly on the row's own entity, never on another entity reached through a foreign key. For example, `holdings.quantity` depends on `holdings.id`, and nothing about the user or the crypto is copied into `holdings`.
- * `avg_price` in `Holdings` is a '''derived value''' cached for performance (it is the weighted-average entry price across all `buy` transactions for that `(user, crypto)` pair) — it is drawn as a derived attribute in the ER diagram. We accept the denormalisation: it is recomputed by the database inside the same transaction as each buy, in the same statement that changes the quantity (`INSERT … ON CONFLICT (user_id, crypto_id) DO UPDATE`), so the stored average and the stored quantity can never disagree.
- * `avg_price` is declared `NOT NULL DEFAULT 0`. This matters: it is used in the P/L arithmetic of `v_portfolio`, and in SQL any arithmetic involving `NULL` 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.
- * `transactions.user_id` is kept '''deliberately''', although for an entry that settles an order it repeats that order's user (`related_order → user_id`, a transitive dependency). A deposit has no order (`Settles` is partial), so `user_id` is the only way to record whose deposit it is. For entries with an order, the only code that sets `related_order` (the buy and sell inserts in `advanced_db.sql`) writes both from the same order row. No database constraint enforces this.
-
-=== 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
-[wiki:UseCase0005]; 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. The cash side of a buy order
-(`users.reserved_balance`), `orders.filled_quantity` and `order_events` were
-added in P7; see
-[wiki:AdvancedDatabaseDevelopment].
-
-== DDL script ==
-
-The script that creates the schema is `../server/db/schema_creation.sql` (shown below). It is idempotent: it drops and recreates the `project` schema every run, so it works on an empty database and on a database that already has the schema.
-
-The script creates:
- * 10 of the 11 tables, with check constraints, primary keys, foreign keys and unique constraints.
- * 8 performance indexes.
- * 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`).
-
-The 11th table, `order_events`, is created by
-`../server/db/advanced_db.sql` together with
-the P7 triggers that fill it. `./eduberza -init` runs both scripts in that
-order, so a freshly initialised database always has all 11 tables and all 15
-foreign keys.
-
-=== schema_creation.sql ===
-
-The two report functions at the end of the file (`report_top_traders` and `report_market_performance`) belong to Phase 6 ([wiki:AdvancedReports]) and are left out here.
-
-{{{
--- schema_creation.sql
--- EduBerza - crypto exchange simulation database
--- Course: Databases 2025/2026 Winter, FINKI UKIM
---
--- This script is idempotent. It drops the `project` schema and all contained
--- objects, then recreates them from scratch. Safe to run on an empty database
--- or on a database where the schema already exists.
-
-DROP SCHEMA IF EXISTS project CASCADE;
-CREATE SCHEMA project;
-
-CREATE EXTENSION IF NOT EXISTS pgcrypto;
-
-SET search_path TO project, public;
-
--- ============================================================================
--- USERS
--- Platform users. Each user has virtual (prop) balances used for simulation.
--- ============================================================================
-CREATE TABLE project.users (
-    id                uuid            PRIMARY KEY DEFAULT gen_random_uuid(),
-    username          varchar(50)     NOT NULL UNIQUE,
-    email             varchar(255)    NOT NULL UNIQUE,
-    full_name         varchar(200),
-    password_hash     varchar(255)    NOT NULL,
-    available_balance numeric(18,4)   NOT NULL DEFAULT 0 CHECK (available_balance >= 0),
-    invested_balance  numeric(18,4)   NOT NULL DEFAULT 0 CHECK (invested_balance  >= 0),
-    -- P7: cash committed to the user's active buy orders, moved out of
-    -- available_balance when the order is placed and consumed as it fills.
-    reserved_balance  numeric(18,4)   NOT NULL DEFAULT 0 CHECK (reserved_balance  >= 0),
-    created_at        timestamptz     NOT NULL DEFAULT now(),
-    updated_at        timestamptz
-);
-
--- ============================================================================
--- CRYPTO
--- Catalog of crypto assets available on the platform.
--- ============================================================================
-CREATE TABLE project.crypto (
-    id         uuid         PRIMARY KEY DEFAULT gen_random_uuid(),
-    symbol     varchar(20)  NOT NULL UNIQUE,
-    name       varchar(255) NOT NULL,
-    created_at timestamptz  NOT NULL DEFAULT now()
-);
-
--- ============================================================================
--- MARKETS
--- A market is a (crypto, quote_currency) pair, e.g. BTC/USD.
--- ============================================================================
-CREATE TABLE project.markets (
-    id             uuid        PRIMARY KEY DEFAULT gen_random_uuid(),
-    crypto_id      uuid        NOT NULL REFERENCES project.crypto(id),
-    quote_currency char(3)     NOT NULL DEFAULT 'USD',
-    is_active      boolean     NOT NULL DEFAULT true,
-    created_at     timestamptz NOT NULL DEFAULT now(),
-    CONSTRAINT uq_markets UNIQUE (crypto_id, quote_currency)
-);
-
--- ============================================================================
--- HOLDINGS
--- Per-user crypto position with running weighted average entry price.
--- ============================================================================
-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),
-    -- 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,
-    CONSTRAINT uq_holdings_user_crypto UNIQUE (user_id, crypto_id)
-);
-
--- ============================================================================
--- ORDERS
--- Orders placed by users on a market.
--- ============================================================================
-CREATE TABLE project.orders (
-    id          uuid           PRIMARY KEY DEFAULT gen_random_uuid(),
-    user_id     uuid           NOT NULL REFERENCES project.users(id)   ON DELETE CASCADE,
-    market_id   uuid           NOT NULL REFERENCES project.markets(id),
-    side        varchar(4)     NOT NULL CHECK (side   IN ('buy', 'sell')),
-    type        varchar(20)    NOT NULL CHECK (type   IN ('market', 'limit')),
-    status      varchar(20)    NOT NULL CHECK (status IN ('open', 'partially_filled', 'executed', 'cancelled')),
-    quantity    numeric(20,4)  NOT NULL CHECK (quantity > 0),
-    -- P7: how much of the order has been traded so far; remaining is
-    -- quantity - filled_quantity. Maintained from market_trades.
-    filled_quantity numeric(20,4) NOT NULL DEFAULT 0
-                               CHECK (filled_quantity >= 0 AND filled_quantity <= quantity),
-    price       numeric(18,6),
-    placed_at   timestamptz    NOT NULL DEFAULT now(),
-    executed_at timestamptz
-);
-
-CREATE INDEX idx_orders_user      ON project.orders(user_id);
-CREATE INDEX idx_orders_market    ON project.orders(market_id);
-CREATE INDEX idx_orders_status    ON project.orders(status);
-
--- ============================================================================
--- TRANSACTIONS
--- Financial ledger: deposits, buys, sells, fees.
--- ============================================================================
-CREATE TABLE project.transactions (
-    id            uuid           PRIMARY KEY DEFAULT gen_random_uuid(),
-    user_id       uuid           NOT NULL REFERENCES project.users(id) ON DELETE CASCADE,
-    type          varchar(50)    NOT NULL CHECK (type IN ('deposit', 'buy', 'sell', 'fee')),
-    amount        numeric(18,4)  NOT NULL,
-    currency      char(3)        NOT NULL DEFAULT 'USD',
-    related_order uuid           REFERENCES project.orders(id),
-    created_at    timestamptz    NOT NULL DEFAULT now(),
-    description   text
-);
-
-CREATE INDEX idx_transactions_user ON project.transactions(user_id, created_at DESC);
-
--- ============================================================================
--- MARKET TRADES
--- Raw executed trades on a market. Source of truth for current price.
--- ============================================================================
-CREATE TABLE project.market_trades (
-    id          bigserial      PRIMARY KEY,
-    market_id   uuid           NOT NULL REFERENCES project.markets(id),
-    executed_at timestamptz    NOT NULL,
-    price       numeric(18,6)  NOT NULL CHECK (price    > 0),
-    quantity    numeric(20,6)  NOT NULL CHECK (quantity > 0),
-    side        varchar(4)     CHECK (side IN ('buy', 'sell')),
-    source      varchar(50)    NOT NULL DEFAULT 'simulation',
-    -- P7: the orders this trade filled. NULL on a side means the counterparty
-    -- was the simulated market (bot ticks have both NULL).
-    buy_order_id  uuid         REFERENCES project.orders(id),
-    sell_order_id uuid         REFERENCES project.orders(id)
-);
-
-CREATE INDEX idx_market_trades_market_time ON project.market_trades(market_id, executed_at DESC);
-CREATE INDEX idx_market_trades_buy_order  ON project.market_trades(buy_order_id)  WHERE buy_order_id  IS NOT NULL;
-CREATE INDEX idx_market_trades_sell_order ON project.market_trades(sell_order_id) WHERE sell_order_id IS NOT NULL;
-
--- ============================================================================
--- MARKET CANDLES
--- OHLCV aggregates over standard timeframes.
--- ============================================================================
-CREATE TABLE project.market_candles (
-    id          bigserial      PRIMARY KEY,
-    market_id   uuid           NOT NULL REFERENCES project.markets(id),
-    timeframe   varchar(5)     NOT NULL CHECK (timeframe IN ('1m', '5m', '1h', '1d')),
-    open        numeric(18,6)  NOT NULL,
-    high        numeric(18,6)  NOT NULL,
-    low         numeric(18,6)  NOT NULL,
-    close       numeric(18,6)  NOT NULL,
-    volume      numeric(20,6)  NOT NULL,
-    candle_time timestamptz    NOT NULL,
-    CONSTRAINT uq_candle UNIQUE (market_id, timeframe, candle_time)
-);
-
-CREATE INDEX idx_market_candles_market_tf_time ON project.market_candles(market_id, timeframe, candle_time DESC);
-
--- ============================================================================
--- WATCHLISTS
--- ============================================================================
-CREATE TABLE project.watchlists (
-    id         uuid         PRIMARY KEY DEFAULT gen_random_uuid(),
-    user_id    uuid         NOT NULL REFERENCES project.users(id) ON DELETE CASCADE,
-    name       varchar(100) NOT NULL,
-    created_at timestamptz  NOT NULL DEFAULT now()
-);
-
-CREATE TABLE project.watchlist_items (
-    id           uuid        PRIMARY KEY DEFAULT gen_random_uuid(),
-    watchlist_id uuid        NOT NULL REFERENCES project.watchlists(id) ON DELETE CASCADE,
-    crypto_id    uuid        NOT NULL REFERENCES project.crypto(id),
-    added_at     timestamptz NOT NULL DEFAULT now(),
-    CONSTRAINT uq_watchlist_crypto UNIQUE (watchlist_id, crypto_id)
-);
-
--- ============================================================================
--- VIEWS
--- ============================================================================
-
--- Latest trade price per market (current price).
-CREATE OR REPLACE VIEW project.v_latest_prices AS
-SELECT DISTINCT ON (t.market_id)
-       t.market_id,
-       c.symbol,
-       m.quote_currency,
-       t.price,
-       t.executed_at
-FROM   project.market_trades t
-JOIN   project.markets       m ON m.id = t.market_id
-JOIN   project.crypto        c ON c.id = m.crypto_id
-ORDER  BY t.market_id, t.executed_at DESC;
-
--- Portfolio valuation per user (holdings x latest price).
-CREATE OR REPLACE VIEW project.v_portfolio AS
-SELECT h.user_id,
-       c.symbol,
-       h.quantity,
-       h.reserved_quantity,
-       (h.quantity - h.reserved_quantity) AS available_quantity,
-       h.avg_price,
-       lp.price                           AS current_price,
-       (h.quantity * lp.price)            AS market_value,
-       (h.quantity * (lp.price - h.avg_price)) AS unrealized_pnl
-FROM   project.holdings h
-JOIN   project.crypto   c ON c.id = h.crypto_id
-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;
-}}}
-
-=== order_events (from advanced_db.sql) ===
-
-{{{
-CREATE TABLE project.order_events (
-    id           bigserial      PRIMARY KEY,
-    order_id     uuid           NOT NULL REFERENCES project.orders(id) ON DELETE CASCADE,
-    event_type   varchar(20)    NOT NULL
-                 CHECK (event_type IN ('placed', 'partially_filled', 'filled', 'cancelled')),
-    quantity     numeric(20,4)  NOT NULL,
-    price        numeric(18,6),
-    status_after varchar(20)    NOT NULL,
-    created_at   timestamptz    NOT NULL DEFAULT clock_timestamp()
-);
-
-CREATE INDEX idx_order_events_order ON project.order_events(order_id, id);
-}}}
-
-== DML script (sample data) ==
-
-The script that loads realistic sample data is `../server/db/data_load.sql` (shown below). It is idempotent: it truncates all tables with `CASCADE` then re-inserts. Loaded:
- * 5 crypto assets (BTC, ETH, ADA, SOL, DOGE) and 5 USD-quoted markets.
- * 3 sample users (`alice`, `bob`, `charlie`) with password `test123` (sha256 hex).
- * 18 recent market trades across all markets so `v_latest_prices` is populated.
- * 10 one-hour candles (BTC and ETH).
- * One fully-executed market-buy order for Alice, the matching holding, and two ledger entries (deposit + buy), with Alice's balances updated accordingly.
- * Two watchlists with five watchlist items.
-
-=== data_load.sql ===
-
-{{{
--- data_load.sql
--- EduBerza - sample data
--- Course: Databases 2025/2026 Winter, FINKI UKIM
---
--- Idempotent. Truncates all tables in the `project` schema and reloads
--- deterministic sample data. Run schema_creation.sql first if tables do
--- not yet exist.
---
--- All sample users have the password: test123
---
--- One transaction: the P7 checks in advanced_db.sql compare balances with
--- the ledger at COMMIT, and the users are inserted with their balances
--- before the deposit rows that back them. In an auto-commit client
--- (DBeaver) every statement would otherwise be checked on its own.
-
-BEGIN;
-
-SET search_path TO project, public;
-
-TRUNCATE TABLE
-    project.watchlist_items,
-    project.watchlists,
-    project.market_candles,
-    project.market_trades,
-    project.transactions,
-    project.orders,
-    project.holdings,
-    project.markets,
-    project.crypto,
-    project.users
-RESTART IDENTITY CASCADE;
-
--- ============================================================================
--- CRYPTO
--- ============================================================================
-INSERT INTO project.crypto (id, symbol, name) VALUES
-    ('11111111-1111-1111-1111-111111111111', 'BTC',  'Bitcoin'),
-    ('22222222-2222-2222-2222-222222222222', 'ETH',  'Ethereum'),
-    ('33333333-3333-3333-3333-333333333333', 'ADA',  'Cardano'),
-    ('44444444-4444-4444-4444-444444444444', 'SOL',  'Solana'),
-    ('55555555-5555-5555-5555-555555555555', 'DOGE', 'Dogecoin');
-
--- ============================================================================
--- MARKETS (all quoted in USD)
--- ============================================================================
-INSERT INTO project.markets (id, crypto_id, quote_currency, is_active) VALUES
-    ('a1111111-1111-1111-1111-111111111111', '11111111-1111-1111-1111-111111111111', 'USD', true),
-    ('a2222222-2222-2222-2222-222222222222', '22222222-2222-2222-2222-222222222222', 'USD', true),
-    ('a3333333-3333-3333-3333-333333333333', '33333333-3333-3333-3333-333333333333', 'USD', true),
-    ('a4444444-4444-4444-4444-444444444444', '44444444-4444-4444-4444-444444444444', 'USD', true),
-    ('a5555555-5555-5555-5555-555555555555', '55555555-5555-5555-5555-555555555555', 'USD', true);
-
--- ============================================================================
--- USERS
--- Password for all: test123 (stored as sha256 hex hash)
--- ============================================================================
-INSERT INTO project.users (id, username, email, full_name, password_hash, available_balance, invested_balance) VALUES
-    ('b1111111-1111-1111-1111-111111111111', 'alice',   'alice@example.com',   'Alice Johnson',
-        encode(digest('test123', 'sha256'), 'hex'), 10000.0000, 0),
-    ('b2222222-2222-2222-2222-222222222222', 'bob',     'bob@example.com',     'Bob Smith',
-        encode(digest('test123', 'sha256'), 'hex'),  5000.0000, 0),
-    ('b3333333-3333-3333-3333-333333333333', 'charlie', 'charlie@example.com', 'Charlie Davis',
-        encode(digest('test123', 'sha256'), 'hex'),  2500.0000, 0);
-
--- ============================================================================
--- MARKET TRADES
--- Recent simulated trades per market, used as price source.
--- ============================================================================
-INSERT INTO project.market_trades (market_id, executed_at, price, quantity, side, source) VALUES
-    -- BTC/USD around $67,000
-    ('a1111111-1111-1111-1111-111111111111', now() - interval '10 min', 66850.250000, 0.120000, 'buy',  'simulation'),
-    ('a1111111-1111-1111-1111-111111111111', now() - interval  '8 min', 66910.500000, 0.075000, 'sell', 'simulation'),
-    ('a1111111-1111-1111-1111-111111111111', now() - interval  '5 min', 67020.750000, 0.200000, 'buy',  'simulation'),
-    ('a1111111-1111-1111-1111-111111111111', now() - interval  '2 min', 67105.100000, 0.050000, 'buy',  'simulation'),
-    ('a1111111-1111-1111-1111-111111111111', now() - interval '30 second', 67140.000000, 0.030000, 'sell', 'simulation'),
-    -- ETH/USD around $3,500
-    ('a2222222-2222-2222-2222-222222222222', now() - interval '10 min', 3490.500000, 1.500000, 'buy',  'simulation'),
-    ('a2222222-2222-2222-2222-222222222222', now() - interval  '6 min', 3502.750000, 0.800000, 'sell', 'simulation'),
-    ('a2222222-2222-2222-2222-222222222222', now() - interval  '2 min', 3515.250000, 2.100000, 'buy',  'simulation'),
-    ('a2222222-2222-2222-2222-222222222222', now() - interval '30 second', 3520.000000, 0.650000, 'buy',  'simulation'),
-    -- ADA/USD around $0.45
-    ('a3333333-3333-3333-3333-333333333333', now() - interval '10 min', 0.446500,  500.000000, 'buy',  'simulation'),
-    ('a3333333-3333-3333-3333-333333333333', now() - interval  '3 min', 0.452000, 1200.000000, 'buy',  'simulation'),
-    ('a3333333-3333-3333-3333-333333333333', now() - interval '30 second', 0.453750,  800.000000, 'sell', 'simulation'),
-    -- SOL/USD around $165
-    ('a4444444-4444-4444-4444-444444444444', now() - interval '10 min', 164.250000, 10.000000, 'buy',  'simulation'),
-    ('a4444444-4444-4444-4444-444444444444', now() - interval  '4 min', 165.500000,  5.500000, 'sell', 'simulation'),
-    ('a4444444-4444-4444-4444-444444444444', now() - interval '30 second', 166.100000,  8.000000, 'buy',  'simulation'),
-    -- DOGE/USD around $0.12
-    ('a5555555-5555-5555-5555-555555555555', now() - interval '10 min', 0.118500, 10000.000000, 'buy',  'simulation'),
-    ('a5555555-5555-5555-5555-555555555555', now() - interval  '3 min', 0.121250,  7500.000000, 'sell', 'simulation'),
-    ('a5555555-5555-5555-5555-555555555555', now() - interval '30 second', 0.122000, 12000.000000, 'buy',  'simulation');
-
--- ============================================================================
--- MARKET CANDLES (1h aggregates, last 5 hours per market)
--- ============================================================================
-INSERT INTO project.market_candles (market_id, timeframe, open, high, low, close, volume, candle_time) VALUES
-    ('a1111111-1111-1111-1111-111111111111', '1h', 66200, 66500, 66050, 66400, 12.50, date_trunc('hour', now() - interval '5 hour')),
-    ('a1111111-1111-1111-1111-111111111111', '1h', 66400, 66800, 66380, 66700, 15.30, date_trunc('hour', now() - interval '4 hour')),
-    ('a1111111-1111-1111-1111-111111111111', '1h', 66700, 66950, 66650, 66900, 11.80, date_trunc('hour', now() - interval '3 hour')),
-    ('a1111111-1111-1111-1111-111111111111', '1h', 66900, 67100, 66800, 67050, 14.20, date_trunc('hour', now() - interval '2 hour')),
-    ('a1111111-1111-1111-1111-111111111111', '1h', 67050, 67200, 66900, 67140, 10.75, date_trunc('hour', now() - interval '1 hour')),
-    ('a2222222-2222-2222-2222-222222222222', '1h',  3460,  3490,  3450,  3485, 120.0, date_trunc('hour', now() - interval '5 hour')),
-    ('a2222222-2222-2222-2222-222222222222', '1h',  3485,  3510,  3480,  3500, 135.0, date_trunc('hour', now() - interval '4 hour')),
-    ('a2222222-2222-2222-2222-222222222222', '1h',  3500,  3520,  3495,  3515, 110.0, date_trunc('hour', now() - interval '3 hour')),
-    ('a2222222-2222-2222-2222-222222222222', '1h',  3515,  3525,  3500,  3520, 125.5, date_trunc('hour', now() - interval '2 hour')),
-    ('a2222222-2222-2222-2222-222222222222', '1h',  3520,  3530,  3510,  3520, 140.0, date_trunc('hour', now() - interval '1 hour'));
-
--- ============================================================================
--- EXAMPLE ORDERS, HOLDINGS AND TRANSACTIONS for alice
--- Shows a fully-filled market buy and its resulting holding & ledger entry.
--- ============================================================================
--- Imported as already completely filled (filled_quantity = quantity).
-INSERT INTO project.orders (id, user_id, market_id, side, type, status, quantity, filled_quantity, price, placed_at, executed_at) VALUES
-    ('c1111111-1111-1111-1111-111111111111',
-     'b1111111-1111-1111-1111-111111111111',
-     'a2222222-2222-2222-2222-222222222222',
-     'buy', 'market', 'executed', 0.5000, 0.5000, 3500.000000,
-     now() - interval '1 hour', now() - interval '1 hour');
-
-INSERT INTO project.holdings (user_id, crypto_id, quantity, avg_price, updated_at) VALUES
-    ('b1111111-1111-1111-1111-111111111111',
-     '22222222-2222-2222-2222-222222222222',
-     0.5000, 3500.000000, now() - interval '1 hour');
-
-INSERT INTO project.transactions (user_id, type, amount, currency, related_order, description) VALUES
-    ('b1111111-1111-1111-1111-111111111111', 'deposit',  10000.0000, 'USD', NULL,
-        'Initial virtual deposit'),
-    ('b2222222-2222-2222-2222-222222222222', 'deposit',   5000.0000, 'USD', NULL,
-        'Initial virtual deposit'),
-    ('b3333333-3333-3333-3333-333333333333', 'deposit',   2500.0000, 'USD', NULL,
-        'Initial virtual deposit'),
-    ('b1111111-1111-1111-1111-111111111111', 'buy',      -1750.0000, 'USD',
-        'c1111111-1111-1111-1111-111111111111',
-        'Market buy 0.5 ETH @ 3500.00');
-
--- After the buy, alice's invested_balance reflects the used funds.
-UPDATE project.users
-   SET available_balance = 10000.0000 - 1750.0000,
-       invested_balance  = 1750.0000,
-       updated_at        = now()
- WHERE id = 'b1111111-1111-1111-1111-111111111111';
-
--- ============================================================================
--- WATCHLISTS
--- ============================================================================
-INSERT INTO project.watchlists (id, user_id, name) VALUES
-    ('d1111111-1111-1111-1111-111111111111', 'b1111111-1111-1111-1111-111111111111', 'Favorites'),
-    ('d2222222-2222-2222-2222-222222222222', 'b2222222-2222-2222-2222-222222222222', 'Bobs Picks');
-
-INSERT INTO project.watchlist_items (watchlist_id, crypto_id) VALUES
-    ('d1111111-1111-1111-1111-111111111111', '11111111-1111-1111-1111-111111111111'),
-    ('d1111111-1111-1111-1111-111111111111', '22222222-2222-2222-2222-222222222222'),
-    ('d1111111-1111-1111-1111-111111111111', '44444444-4444-4444-4444-444444444444'),
-    ('d2222222-2222-2222-2222-222222222222', '11111111-1111-1111-1111-111111111111'),
-    ('d2222222-2222-2222-2222-222222222222', '55555555-5555-5555-5555-555555555555');
-
-COMMIT;
-}}}
-
-== Relational diagram ==
-
-[[Image(relational_diagram_v4.png, 800px)]]
-
-Generated in '''DBeaver''' from the '''live''' `project` schema (after
-`./eduberza -init`), not drawn by hand, so it shows what the deployed database
-actually contains. Each box is a table with its columns; the key icon marks the
-primary key, and the lines are the 15 declared foreign keys. The two foreign keys from
-`market_trades` to `orders` (`buy_order_id`, `sell_order_id`) connect the same
-two boxes, so DBeaver draws them on top of each other as one line.
-
-The tables are arranged in the '''same positions''' as the entity sets in
-`ERModel_v05.png`, so the two can be compared directly:
-
- * every rectangle of the ER diagram is one table in the same place;
- * every diamond of the ER diagram is one foreign-key line between the same two boxes. The dot is on the referencing ("N") table, next to the foreign-key column;
- * a double (total) line in the ER diagram is a `NOT NULL` foreign key, drawn by DBeaver as a solid line. The three single lines on the N side (`Settles`, `FillsBuy`, `FillsSell`) are the three nullable foreign keys, which DBeaver draws dashed, with a hollow diamond on the `orders` side. The table under Transformation method used lists all 15.
-
-Earlier images (`relational_schema.jpg`, `relational_schema_v2.png`,
-`relational_schema_v3.png`) were exported from pgAdmin, with a different layout
-and from an older schema. They are kept only as history.
-
-=== How to regenerate it ===
-
- 1. Initialise the database: `./eduberza -init` (runs `schema_creation.sql` and `advanced_db.sql`, so `order_events` is included).
- 2. In DBeaver, connect to the project database and expand ''Schemas → project → Tables''.
- 3. Select all 11 tables → right-click → '''View Diagram''' (or create a new ER diagram and drag the tables in).
- 4. Drag each table to the position of its entity set in `ERModel_v05.png`:
-
-   {{{
-            column 1          column 2        column 3        column 4
-   row 1    watchlist_items   crypto          markets         market_candles
-   row 2    watchlists        holdings        .               market_trades
-   row 3    users             .               orders          .
-   row 4    .                 transactions    order_events    .
-   }}}
-
-   Leave the empty cells (`.`) empty. They are where the relationship
-   diamonds are in the ER diagram, so the foreign-key lines will run through
-   the same gaps.
-
- 5. Right-click the canvas → '''Export diagram''' → PNG, saved as `relational_diagram_v4.png` in this folder.
Index: cs/P2-RelationalDesign/wiki/RelationalDesignAIUsage.md
===================================================================
--- docs/P2-RelationalDesign/wiki/RelationalDesignAIUsage.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,120 +1,0 @@
-= Relational Design AI Usage =
-
-== Name of AI service/solution that was used ==
-
-'''Claude Code''' (Anthropic)
-
- * '''URL:''' `https://claude.com/claude-code`
- * '''Type of service/subscription:''' Claude subscription, model Claude Opus 4.7 (1M context).
-
-== Final result ==
-
-=== Diagram ===
-
-`relational_diagram_v4.png` was exported by the student in DBeaver from the live `project` schema, with the tables in the same positions as the entity sets of `ERModel_v05.png`; see [wiki:RelationalDesign] (section "How to regenerate it") for instructions.
-
-=== Results in details / description ===
-
-The AI:
-
- * Consolidated two inconsistent draft schemas (`server/db/db.sql` and `server/db/schema.sql`) into a single `schema_creation.sql`.
- * Corrected foreign-key errors in the original (the `crypto_id` column in `holdings`, `orders`, and `transactions` had been pointed at both `users(id)` and `crypto(id)`; the AI split it into separate `user_id` and `crypto_id` columns per `ep-diagram.md`).
- * Added two convenience views, `v_latest_prices` and `v_portfolio`, to keep the Go CLI simple.
- * Produced a sample-data script `data_load.sql` that TRUNCATEs and re-inserts deterministic rows, so the "must work on empty DB and on a DB that already has data" requirement from P2 is met.
- * Documented normalisation up to 3NF and the intentional denormalisation of `holdings.avg_price`.
-
-== Summary of AI involvement ==
-
-||= =||= Session 1 — 2026-04-21 =||= Session 2 — 2026-08-06/07 =||
-|| '''What I brought''' || My own draft SQL (`db.sql`, `schema.sql`) and the model in `ep-diagram.md` || The schema as it stood after session 1 ||
-|| '''What the AI did''' || Reviewed my SQL, found the foreign-key errors, consolidated two inconsistent drafts into one script || Reviewed the schema again; one constraint change, plus documentation of the transformation ||
-|| '''What I decided''' || Which corrections to adopt, to keep both balance columns, to drop the secret-question fields || To make `avg_price` `NOT NULL` rather than handle nulls in application code ||
-
-The relational model in this phase is a transformation of ''my'' ER model, and the
-foreign-key errors the AI found in session 1 were errors in ''my'' draft SQL — that
-review is the single most useful thing the AI did on this phase.
-
-== Entire AI usage log ==
-
-See [wiki:ERModelAIUsage] — the full transcript of the 2026-04-21 conversation covers both P1 and P2 work. The specific prompts that drove the relational-design output were the same "make it work and make it fill in or to follow all of the needed instructions" instruction and the student's subsequent "do everything that you need to do".
-
-> '''Student action required:''' append any future consultations where you asked the AI to refine the schema, tune constraints, or write additional queries.
-
-
-=== Session 2 — 2026-08-06 / 2026-08-07 ===
-
-Prompts are logged in full in [wiki:ERModelAIUsage] (section "Session 2 — 2026-08-06 / 2026-08-07");
-the one that drove this phase was ''"Also fix some database things or golang things if you think we can do it better"''. Changes to the P2 artefacts:
-
- * `holdings.avg_price` changed from nullable to `NOT NULL DEFAULT 0 CHECK (avg_price >= 0)`. Reason: it feeds the P/L arithmetic in `v_portfolio`, and
-   SQL arithmetic involving `NULL` produces `NULL`, so a nullable average would
-   have silently blanked the unrealised-P/L column of a real position. A related
-   crash path in the Go code (scanning a `NULL` average into a non-nullable
-   `float64`, which was reported to the user as "Insufficient holding") was fixed
-   at the same time.
- * [wiki:RelationalDesign] gained an explicit account of the
-   partial transformation: which ER construct each foreign key comes from, that
-   M:N relationships with attributes become tables whose foreign-key pair is a
-   `UNIQUE` constraint, and that total participation becomes `NOT NULL` — which is
-   why `transactions.related_order` is the one nullable foreign key.
- * The candidate keys of `holdings` and `watchlist_items` are now documented as
-   the relationship keys `{user_id, crypto_id}` and `{watchlist_id, crypto_id}`.
-
-Both scripts were re-run end to end against PostgreSQL 16 after these changes.
-
-> '''Still outstanding:''' `relational_schema.jpg` must be exported from DBeaver
-> 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
-[wiki:ERModelAIUsage] (section "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`.
- * [wiki:RelationalDesign] 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
-[wiki:UseCase0005Implementation] 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.
-
-=== Session 4 — 2026-09-29 (Claude Opus 5.5): correction after review ===
-
-'''Prompt (student, excerpt):''' I pasted the reviewer's comments on P2:
-
-> the `contains` relationship from the model is implemented differently in the database
-> the `holds` relationship from the model is implemented differently in the database
-> there may be other inconsistencies that are difficult to spot; the Phase 2 diagram should
-> have been created using DBeaver with a layout visually similar to the ERD
-
-> I suggest correcting only the Phase 1 model so that it matches Phase 2 [...] For Phase 2,
-> simply update the image according to the requirements.
-
-'''Response (AI, summarised):'''
-
- * Explained the mismatch. In the model, `Holds` and `Contains` were M:N relationships, whose table key would be the pair `{user_id, crypto_id}` / `{watchlist_id, crypto_id}`. But `holdings` and `watchlist_items` have their own `id` primary key, which is how an entity set is implemented. Following the reviewer's advice, P1 was corrected (v05, entity sets `Holdings` and `WatchlistItems`), and the database was not changed.
- * Found the other inconsistencies between this page and the live schema. The page was missing `users.reserved_balance`, `orders.filled_quantity`, the status `partially_filled`, `market_trades.buy_order_id` / `sell_order_id` and the whole `order_events` table. The "10 tables", "5 indexes" and "the one nullable foreign key" counts were also out of date (really 11 tables, 8 indexes in `schema_creation.sql`, and 3 nullable foreign keys).
- * Rewrote [wiki:RelationalDesign]. Each relation is labelled with its entity set and each foreign key with its relationship, and the transformation is a table of all 15 relationships → 15 foreign keys, with `NOT NULL` following participation.
- * Wrote export instructions with a table grid that mirrors `ERModel_v05.png`.
-
-'''What I decided:''' to correct P1 instead of the database, as the reviewer suggested. I
-exported `relational_diagram_v4.png` from DBeaver myself, with the tables arranged like the
-ER diagram.
Index: cs/P3-UseCaseModel/UseCase0001.md
===================================================================
--- docs/P3-UseCaseModel/UseCase0001.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,40 +1,0 @@
-# Use-case 0001 — Register new account
-
-**Initiating actor:** Visitor
-
-**Other actors:** —
-
-A new person creates an account on EduBerza so they can later log in as a Trader. The system validates input, refuses duplicates, and stores a hashed password.
-
-## Scenario
-
-1. Visitor chooses "Register" from the anonymous menu.
-2. System prompts for username, email, full name and password.
-3. Visitor enters values.
-4. System validates:
-   - username, email, password are non-empty.
-   - email contains `@`.
-   - password is at least 6 characters.
-5. System checks whether the chosen username or email already exists:
-
-   ```sql
-   SELECT EXISTS (
-       SELECT 1 FROM project.users
-        WHERE username = $1 OR email = $2
-   );
-   ```
-6. If the row exists, system informs the user and the scenario ends. Otherwise it creates the account:
-
-   ```sql
-   INSERT INTO project.users (username, email, full_name, password_hash, available_balance)
-   VALUES ($1, $2, $3, encode(digest($4, 'sha256'), 'hex'), 0);
-   ```
-7. System confirms success and returns to the anonymous menu; Visitor can then proceed to UC0002.
-
-### Alternate flow 3a — invalid email
-
-If step 4 fails email validation, system shows "Invalid email." and scenario returns to step 2.
-
-### Alternate flow 5a — duplicate
-
-If step 5 returns `true`, system shows "Username or email already taken." and scenario ends.
Index: cs/P3-UseCaseModel/UseCase0002.md
===================================================================
--- docs/P3-UseCaseModel/UseCase0002.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,34 +1,0 @@
-# Use-case 0002 — Log in
-
-**Initiating actor:** Visitor
-
-**Other actors:** —
-
-A registered user authenticates so the system can treat subsequent actions as a Trader.
-
-## Scenario
-
-1. Visitor chooses "Login" from the anonymous menu.
-2. System prompts for username and password.
-3. Visitor enters values.
-4. System looks up the user:
-
-   ```sql
-   SELECT id, password_hash
-     FROM project.users
-    WHERE username = $1;
-   ```
-5. If no row is returned, system responds "Invalid credentials." and scenario ends.
-6. If a row is returned, system compares the stored hash against sha256 of the entered password. On mismatch it responds "Invalid credentials." and ends.
-7. On match, system records the returned `id` and username in the session and displays the authenticated menu.
-
-### Alternate flow 4a — authenticated lookup with live balance
-
-The system may combine identity lookup with live balance in a single query, for use cases that need both:
-
-```sql
-SELECT id, available_balance, invested_balance
-  FROM project.users
- WHERE username = $1
-   AND password_hash = encode(digest($2, 'sha256'), 'hex');
-```
Index: cs/P3-UseCaseModel/UseCase0003.md
===================================================================
--- docs/P3-UseCaseModel/UseCase0003.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,44 +1,0 @@
-# Use-case 0003 — Deposit virtual funds
-
-**Initiating actor:** Trader
-
-**Other actors:** —
-
-A logged-in Trader tops up their virtual cash balance. This is a simulation-only operation; no real money changes hands. The operation writes to two tables — the user row and the ledger — inside a single transaction.
-
-## Scenario
-
-1. Trader chooses "Deposit virtual funds" from the authenticated menu.
-2. System prompts for an amount in USD.
-3. Trader enters an amount.
-4. System validates: amount must parse as a positive number.
-5. System opens a transaction and increments the balance:
-
-   ```sql
-   BEGIN;
-
-   UPDATE project.users
-      SET available_balance = available_balance + $1,
-          updated_at        = now()
-    WHERE id = $2;
-
-   INSERT INTO project.transactions (user_id, type, amount, currency, description)
-   VALUES ($2, 'deposit', $1, 'USD', 'Virtual deposit');
-
-   COMMIT;
-   ```
-6. System confirms "Deposited X USD." and returns to the authenticated menu.
-
-### Alternate flow 4a — invalid input
-
-If the amount is non-positive or non-numeric, system responds "Invalid amount." and scenario returns to step 2.
-
-### Verification query
-
-To see the balance after the deposit, the Trader can trigger UC0006, or directly:
-
-```sql
-SELECT available_balance, invested_balance
-  FROM project.users
- WHERE id = $1;
-```
Index: cs/P3-UseCaseModel/UseCase0004.md
===================================================================
--- docs/P3-UseCaseModel/UseCase0004.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,92 +1,0 @@
-# Use-case 0004 — Place market BUY order
-
-**Initiating actor:** Trader
-
-**Other actors:** Market Simulator (indirect — supplies the current price via `market_trades`).
-
-A Trader buys a crypto asset at the current market price. The operation touches five tables (`orders`, `users`, `holdings`, `transactions`, `market_trades`) and must either all succeed or all roll back.
-
-## Scenario
-
-1. Trader chooses "Place market BUY order".
-2. System lists the active markets, numbered, with their latest price:
-
-   ```sql
-   SELECT m.id, c.id, c.symbol, m.quote_currency,
-          COALESCE(lp.price, 0) AS price
-     FROM project.markets m
-     JOIN project.crypto  c  ON c.id = m.crypto_id
-     LEFT JOIN project.v_latest_prices lp ON lp.market_id = m.id
-    WHERE m.is_active = true
-    ORDER BY c.symbol;
-   ```
-3. Trader picks the market by its number in the listed markets, e.g. `2` (BTC).
-4. System takes the chosen row's market id and crypto id from the list (no lookup by
-   symbol) and looks up the latest price:
-
-   ```sql
-   SELECT price FROM project.v_latest_prices WHERE market_id = $1;
-   ```
-5. Trader enters a quantity.
-6. System computes notional = quantity × price, opens a transaction, and does:
-
-   ```sql
-   BEGIN;
-
-   -- (a) record intent — no trade has happened yet.
-   INSERT INTO project.orders
-       (user_id, market_id, side, type, status, quantity, price)
-   VALUES
-       ($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,
-          invested_balance  = invested_balance  + $notional,
-          updated_at        = now()
-    WHERE id = $user_id;
-
-   -- (d) upsert holding with running weighted-average price:
-   SELECT quantity, avg_price
-     FROM project.holdings
-    WHERE user_id = $user_id AND crypto_id = $crypto_id
-    FOR UPDATE;
-
-   -- Either INSERT (new holding) or UPDATE (existing), computing
-   -- 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)
-   VALUES
-       ($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;
-   ```
-7. System confirms: `Order executed: buy 0.0100 BTC @ 67140.000000 (notional 671.4000 USD)`.
-
-### Alternate flow 6a — insufficient funds
-
-If `available_balance < notional`, the entire transaction rolls back and system shows "Insufficient funds: need X, have Y."
-
-### Alternate flow 3a — number not in the list
-
-If the entered number is not one of the listed market numbers, system shows "Invalid choice, enter a number from 1 to N." and returns to the authenticated menu without opening a transaction.
Index: cs/P3-UseCaseModel/UseCase0005.md
===================================================================
--- docs/P3-UseCaseModel/UseCase0005.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,140 +1,0 @@
-# Use-case 0005 — Place market SELL order
-
-**Initiating actor:** Trader
-
-**Other actors:** Market Simulator (indirect — supplies the current price).
-
-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
-
-1. Trader chooses "Place market SELL order".
-2. System lists, numbered, the Trader's holdings that still have a quantity free to
-   sell (not reserved by an open sell order), with the quantity held, the free
-   quantity and the latest price:
-
-   ```sql
-   SELECT m.id, c.id, c.symbol, m.quote_currency,
-          h.quantity, h.quantity - h.reserved_quantity AS free,
-          COALESCE(lp.price, 0) AS price
-     FROM project.holdings h
-     JOIN project.crypto  c ON c.id = h.crypto_id
-     JOIN project.markets m ON m.crypto_id = c.id AND m.is_active = true
-     LEFT JOIN project.v_latest_prices lp ON lp.market_id = m.id
-    WHERE h.user_id = $1
-      AND h.quantity - h.reserved_quantity > 0
-    ORDER BY c.symbol;
-   ```
-3. Trader picks the holding by its number in the listed holdings, e.g. `2` (ETH), and
-   enters the quantity.
-4. System takes the chosen row's market id and crypto id from the list (no lookup by
-   symbol) and looks up the latest price:
-
-   ```sql
-   SELECT price FROM project.v_latest_prices WHERE market_id = $1;
-   ```
-5. System opens a transaction:
-
-   ```sql
-   BEGIN;
-
-   -- (a) record intent — no trade has happened yet.
-   INSERT INTO project.orders
-       (user_id, market_id, side, type, status, quantity, price)
-   VALUES
-       ($user_id, $market_id, 'sell', 'market', 'open', $qty, $price)
-   RETURNING id;   -- $order_id
-
-   -- (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;
-   -- available := quantity - reserved_quantity
-   -- abort if row missing or available < $qty
-   ```
-
-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 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;
-
-   UPDATE project.users
-      SET available_balance = available_balance + $notional,
-          invested_balance  = GREATEST(invested_balance - ($avg_price * $qty), 0),
-          updated_at        = now()
-    WHERE id = $user_id;
-
-   INSERT INTO project.transactions
-       (user_id, type, amount, currency, related_order, description)
-   VALUES
-       ($user_id, 'sell', $notional, 'USD', $order_id, 'Market sell ...');
-
-   INSERT INTO project.market_trades
-       (market_id, executed_at, price, quantity, side, source)
-   VALUES
-       ($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 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)
-
-The realised P/L for a sell is `$notional - ($avg_price * $qty)`. It is not persisted explicitly but can be computed from the ledger and the holding at sell time.
Index: cs/P3-UseCaseModel/UseCase0006.md
===================================================================
--- docs/P3-UseCaseModel/UseCase0006.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,74 +1,0 @@
-# Use-case 0006 — View portfolio and transaction history
-
-**Initiating actor:** Trader
-
-**Other actors:** —
-
-A Trader inspects their current holdings, unrealised P/L, cash balance and recent ledger.
-
-## Scenario
-
-### Portfolio
-
-1. Trader chooses "View portfolio".
-2. System queries the `v_portfolio` view:
-
-   ```sql
-   SELECT symbol,
-          quantity,
-          COALESCE(reserved_quantity,  0),
-          COALESCE(available_quantity, quantity),
-          COALESCE(avg_price,      0),
-          COALESCE(current_price,  0),
-          COALESCE(market_value,   0),
-          COALESCE(unrealized_pnl, 0)
-     FROM project.v_portfolio
-    WHERE user_id = $1
-      AND quantity > 0
-    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:
-
-   ```sql
-   SELECT available_balance, invested_balance
-     FROM project.users
-    WHERE id = $1;
-   ```
-
-### Transaction history
-
-1. Trader chooses "View transaction history".
-2. System queries the last 20 ledger entries:
-
-   ```sql
-   SELECT created_at, type, amount, currency, COALESCE(description, '')
-     FROM project.transactions
-    WHERE user_id = $1
-    ORDER BY created_at DESC
-    LIMIT 20;
-   ```
-
-### Reference — how `v_portfolio` is defined
-
-```sql
-CREATE OR REPLACE VIEW project.v_portfolio AS
-SELECT h.user_id,
-       c.symbol,
-       h.quantity,
-       h.reserved_quantity,
-       (h.quantity - h.reserved_quantity)      AS available_quantity,
-       h.avg_price,
-       lp.price                                AS current_price,
-       (h.quantity * lp.price)                 AS market_value,
-       (h.quantity * (lp.price - h.avg_price)) AS unrealized_pnl
-  FROM project.holdings h
-  JOIN project.crypto   c ON c.id = h.crypto_id
-  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;
-```
Index: cs/P3-UseCaseModel/UseCase0007.md
===================================================================
--- docs/P3-UseCaseModel/UseCase0007.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,75 +1,0 @@
-# Use-case 0007 — Manage watchlist
-
-**Initiating actor:** Trader
-
-**Other actors:** —
-
-A Trader keeps a list of crypto assets they want to monitor. Adding an asset that is already on the list is a no-op (idempotent).
-
-## Scenario
-
-1. Trader chooses "Manage watchlist".
-2. System ensures the Trader has a default watchlist named "Favorites":
-
-   ```sql
-   SELECT id FROM project.watchlists
-    WHERE user_id = $1
-    ORDER BY created_at LIMIT 1;
-
-   -- if no row:
-   INSERT INTO project.watchlists (user_id, name)
-   VALUES ($1, 'Favorites')
-   RETURNING id;
-   ```
-3. System shows the sub-menu: List / Add / Remove / Back.
-
-### List items
-
-```sql
-SELECT c.symbol, c.name, COALESCE(lp.price, 0)
-  FROM project.watchlist_items wi
-  JOIN project.crypto c ON c.id = wi.crypto_id
-  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
- WHERE wi.watchlist_id = $1
- ORDER BY c.symbol;
-```
-
-### Add a crypto
-
-The Trader picks the crypto by its number from the listed cryptos that are not on the watchlist yet:
-
-```sql
--- 1. list, numbered, the cryptos not yet on the watchlist
-SELECT c.id, c.symbol, c.name
-  FROM project.crypto c
- WHERE NOT EXISTS (SELECT 1 FROM project.watchlist_items wi
-                    WHERE wi.watchlist_id = $1 AND wi.crypto_id = c.id)
- ORDER BY c.symbol;
-
--- 2. insert the chosen crypto ($2 = its id from the list); do nothing if it's already there
-INSERT INTO project.watchlist_items (watchlist_id, crypto_id)
-VALUES ($1, $2)
-ON CONFLICT (watchlist_id, crypto_id) DO NOTHING;
-```
-
-### Remove a crypto
-
-The Trader picks the crypto by its number from the listed cryptos on the watchlist:
-
-```sql
--- 1. list, numbered, the cryptos on the watchlist
-SELECT c.id, c.symbol, c.name
-  FROM project.watchlist_items wi
-  JOIN project.crypto c ON c.id = wi.crypto_id
- WHERE wi.watchlist_id = $1
- ORDER BY c.symbol;
-
--- 2. delete the chosen crypto ($2 = its id from the list)
-DELETE FROM project.watchlist_items
- WHERE watchlist_id = $1 AND crypto_id = $2;
-```
-
-If the entered number is not one of the listed numbers, system shows "Invalid choice, enter a number from 1 to N." and nothing is changed.
Index: cs/P3-UseCaseModel/UseCaseModel.md
===================================================================
--- docs/P3-UseCaseModel/UseCaseModel.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,118 +1,0 @@
-= Use-case model =
-
-The detailed pages for this phase are kept in the project's GitHub repository,
-[https://github.com/StefanTrsunov/bp StefanTrsunov/bp], under `docs/P3-UseCaseModel/`. Every
-use-case link below opens the corresponding file there.
-
-== Actors / Roles ==
-
-'''Visitor''' – Anyone using EduBerza without an account, who can look at public market
-information, create an account, and log in.
-
-'''Trader''' – A registered, logged-in user who deposits virtual funds, places market buy and
-sell orders, follows the value of their portfolio, and keeps a watchlist of assets they want to
-monitor.
-
-'''Market Simulator''' – An external automated system (the bot in `bots/`) that writes simulated
-trades and candles into the database so prices move without a connection to a real exchange.
-
-== Use-Cases ==
-
-=== Visitor ===
-
- * [https://github.com/StefanTrsunov/bp/blob/main/docs/P3-UseCaseModel/UseCase0001.md UC0001] –
-   '''Register new account''' – Visitor creates an account with a unique username and e-mail; the
-   password is stored as a SHA-256 hash.
- * [https://github.com/StefanTrsunov/bp/blob/main/docs/P3-UseCaseModel/UseCase0002.md UC0002] –
-   '''Log in''' – Visitor authenticates with username and password so the system treats every
-   following action as a Trader.
-
-=== Trader ===
-
- * [https://github.com/StefanTrsunov/bp/blob/main/docs/P3-UseCaseModel/UseCase0003.md UC0003] –
-   '''Deposit virtual funds''' – Trader tops up their virtual cash balance; the user row and the
-   ledger are written in one transaction.
- * [https://github.com/StefanTrsunov/bp/blob/main/docs/P3-UseCaseModel/UseCase0004.md UC0004] –
-   '''Place market BUY order''' – Trader buys a crypto asset at the current market price, which
-   debits cash and upserts the holding at a running weighted-average price.
- * [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 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
-   P/L, cash balances and the most recent ledger entries.
- * [https://github.com/StefanTrsunov/bp/blob/main/docs/P3-UseCaseModel/UseCase0007.md UC0007] –
-   '''Manage watchlist''' – Trader lists, adds and removes crypto assets on a personal watchlist,
-   where adding an asset already on the list is a no-op.
-
-=== Market Simulator ===
-
-The Market Simulator initiates no use case of its own. It participates in
-[https://github.com/StefanTrsunov/bp/blob/main/docs/P3-UseCaseModel/UseCase0004.md UC0004] and
-[https://github.com/StefanTrsunov/bp/blob/main/docs/P3-UseCaseModel/UseCase0005.md UC0005]
-indirectly, by keeping `project.market_trades` populated so that `project.v_latest_prices`
-returns a current price for every active market.
-
-== Use-case model diagram ==
-
-{{{#!comment
-The diagram is optional for P3. Commit the exported image to
-docs/P3-UseCaseModel/use_case_diagram.png and then replace this comment with:
-
-[https://github.com/StefanTrsunov/bp/blob/main/docs/P3-UseCaseModel/use_case_diagram.png Use-case model diagram]
-}}}
-
-== Detailed Use-Cases ==
-
-The following use-cases are documented in detail, with SQL tested against the P2 database:
-
- * [https://github.com/StefanTrsunov/bp/blob/main/docs/P3-UseCaseModel/UseCase0001.md UseCase0001] – Visitor registers a new account
- * [https://github.com/StefanTrsunov/bp/blob/main/docs/P3-UseCaseModel/UseCase0002.md UseCase0002] – Visitor logs in
- * [https://github.com/StefanTrsunov/bp/blob/main/docs/P3-UseCaseModel/UseCase0003.md UseCase0003] – Trader deposits virtual funds
- * [https://github.com/StefanTrsunov/bp/blob/main/docs/P3-UseCaseModel/UseCase0004.md UseCase0004] – Trader places a market BUY order
- * [https://github.com/StefanTrsunov/bp/blob/main/docs/P3-UseCaseModel/UseCase0005.md UseCase0005] – Trader places a market SELL order
- * [https://github.com/StefanTrsunov/bp/blob/main/docs/P3-UseCaseModel/UseCase0006.md UseCase0006] – Trader views portfolio and transaction history
- * [https://github.com/StefanTrsunov/bp/blob/main/docs/P3-UseCaseModel/UseCase0007.md UseCase0007] – Trader manages a watchlist
-
-== Realization details on selection of the most important use cases ==
-
-This is a solo project, so '''at least 3 use cases''' are required. '''7 use cases''' are
-documented, for a safety margin. All seven are implemented in the P4 prototype; see
-[https://github.com/StefanTrsunov/bp/tree/main/server server/] for the Go source and
-[https://github.com/StefanTrsunov/bp/blob/main/docs/P4-Prototype/PrototypeImplementation.md PrototypeImplementation]
-for the documented runs.
-
-||=Use case=||=Importance=||=Why it was selected=||
-||[https://github.com/StefanTrsunov/bp/blob/main/docs/P3-UseCaseModel/UseCase0001.md UC0001 – Register]||High||Nothing else works without it; demonstrates `INSERT` with a uniqueness check.||
-||[https://github.com/StefanTrsunov/bp/blob/main/docs/P3-UseCaseModel/UseCase0002.md UC0002 – Log in]||High||Authenticates every Trader action; demonstrates `SELECT` with parameter binding.||
-||[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, 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.||
-
-== 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],
-   [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) 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: cs/P3-UseCaseModel/UseCaseModelAIUsage.md
===================================================================
--- docs/P3-UseCaseModel/UseCaseModelAIUsage.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,94 +1,0 @@
-# Use-Case Model AI Usage
-
-## Name of AI service/solution that was used
-
-**Claude Code** (Anthropic)
-
-- **URL:** https://claude.com/claude-code
-- **Type of service/subscription:** Claude subscription, model Claude Opus 4.7 (1M context).
-
-## Final result
-
-### Results in details / description
-
-The AI:
-
-- Proposed the actor taxonomy (Visitor, Trader, Market Simulator) from the project description and the existing Go code.
-- Derived a set of 7 use cases covering the full trading loop (register, login, deposit, buy, sell, view portfolio, watchlist).
-- Wrote each `UseCaseXXXX.md` file with:
-  - initiating actor, other actors, goals
-  - step-by-step dialog-form scenario
-  - **tested** SQL statements for every database-touching step, using the `project` schema
-  - alternate flows for the most common failure cases (insufficient funds, insufficient holding, duplicate user, invalid input)
-
-## Summary of AI involvement
-
-| | Session 1 — 2026-04-21 | Session 2 — 2026-08-06/07 |
-|---|---|---|
-| **What I brought** | The project description in `opis.md` and the existing Go backend | The use-case model as completed in session 1 |
-| **What the AI did** | Proposed the actor taxonomy and drafted seven use cases with tested SQL | Nothing — the model was not changed |
-| **What I decided** | To go solo, and therefore to document seven use cases rather than the three the rubric requires | To re-verify every scenario's SQL against a live database rather than trust the April run |
-
-This phase was finished in session 1. In session 2 the only work was
-verification: each scenario was executed against a live PostgreSQL 16 database,
-including the failure paths, and the results recorded on the
-`UseCaseXXXXImplementation` pages.
-
-## Entire AI usage log
-
-See [ERModelAIUsage](../P1-ConceptualModel/ERModelAIUsage.md) for the full transcript — the same 2026-04-21 session produced the use-case documentation. The defining student prompt was:
-
-> I'm going solo do everything that you need to do, and tell me after what do I need to do
-
-which led the AI to pick "solo → 3 minimum per rubric → document 7 for a margin" as a default.
-
-> **Student action required:** append any future refinements of the use-case list or scenarios here.
-
-
-### Session 2 — 2026-08-06 / 2026-08-07
-
-No changes were made to the use-case model in this session: the actor list, the
-seven use cases and the scenario SQL in `UseCase0001`–`UseCase0007` are as
-produced on 2026-04-21. The SQL in those scenarios was, however, re-verified by
-executing the corresponding prototype flows against a live PostgreSQL 16
-database, including the failure paths (insufficient funds, insufficient holding,
-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: cs/P3-UseCaseModel/wiki/UseCase0001.md
===================================================================
--- docs/P3-UseCaseModel/wiki/UseCase0001.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,42 +1,0 @@
-= Use-case 0001 — Register new account =
-
-'''Initiating actor:''' Visitor
-
-'''Other actors:''' —
-
-A new person creates an account on !EduBerza so they can later log in as a Trader. The system validates input, refuses duplicates, and stores a hashed password.
-
-== Scenario ==
-
- 1. Visitor chooses "Register" from the anonymous menu.
- 2. System prompts for username, email, full name and password.
- 3. Visitor enters values.
- 4. System validates:
-   * username, email, password are non-empty.
-   * email contains `@`.
-   * password is at least 6 characters.
- 5. System checks whether the chosen username or email already exists:
-
-{{{
-SELECT EXISTS (
-    SELECT 1 FROM project.users
-     WHERE username = $1 OR email = $2
-);
-}}}
-
- 6. If the row exists, system informs the user and the scenario ends. Otherwise it creates the account:
-
-{{{
-INSERT INTO project.users (username, email, full_name, password_hash, available_balance)
-VALUES ($1, $2, $3, encode(digest($4, 'sha256'), 'hex'), 0);
-}}}
-
- 7. System confirms success and returns to the anonymous menu; Visitor can then proceed to UC0002.
-
-=== Alternate flow 3a — invalid email ===
-
-If step 4 fails email validation, system shows "Invalid email." and scenario returns to step 2.
-
-=== Alternate flow 5a — duplicate ===
-
-If step 5 returns `true`, system shows "Username or email already taken." and scenario ends.
Index: cs/P3-UseCaseModel/wiki/UseCase0002.md
===================================================================
--- docs/P3-UseCaseModel/wiki/UseCase0002.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,35 +1,0 @@
-= Use-case 0002 — Log in =
-
-'''Initiating actor:''' Visitor
-
-'''Other actors:''' —
-
-A registered user authenticates so the system can treat subsequent actions as a Trader.
-
-== Scenario ==
-
- 1. Visitor chooses "Login" from the anonymous menu.
- 2. System prompts for username and password.
- 3. Visitor enters values.
- 4. System looks up the user:
-
-{{{
-SELECT id, password_hash
-  FROM project.users
- WHERE username = $1;
-}}}
-
- 5. If no row is returned, system responds "Invalid credentials." and scenario ends.
- 6. If a row is returned, system compares the stored hash against sha256 of the entered password. On mismatch it responds "Invalid credentials." and ends.
- 7. On match, system records the returned `id` and username in the session and displays the authenticated menu.
-
-=== Alternate flow 4a — authenticated lookup with live balance ===
-
-The system may combine identity lookup with live balance in a single query, for use cases that need both:
-
-{{{
-SELECT id, available_balance, invested_balance
-  FROM project.users
- WHERE username = $1
-   AND password_hash = encode(digest($2, 'sha256'), 'hex');
-}}}
Index: cs/P3-UseCaseModel/wiki/UseCase0003.md
===================================================================
--- docs/P3-UseCaseModel/wiki/UseCase0003.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,45 +1,0 @@
-= Use-case 0003 — Deposit virtual funds =
-
-'''Initiating actor:''' Trader
-
-'''Other actors:''' —
-
-A logged-in Trader tops up their virtual cash balance. This is a simulation-only operation; no real money changes hands. The operation writes to two tables — the user row and the ledger — inside a single transaction.
-
-== Scenario ==
-
- 1. Trader chooses "Deposit virtual funds" from the authenticated menu.
- 2. System prompts for an amount in USD.
- 3. Trader enters an amount.
- 4. System validates: amount must parse as a positive number.
- 5. System opens a transaction and increments the balance:
-
-{{{
-BEGIN;
-
-UPDATE project.users
-   SET available_balance = available_balance + $1,
-       updated_at        = now()
- WHERE id = $2;
-
-INSERT INTO project.transactions (user_id, type, amount, currency, description)
-VALUES ($2, 'deposit', $1, 'USD', 'Virtual deposit');
-
-COMMIT;
-}}}
-
- 6. System confirms "Deposited X USD." and returns to the authenticated menu.
-
-=== Alternate flow 4a — invalid input ===
-
-If the amount is non-positive or non-numeric, system responds "Invalid amount." and scenario returns to step 2.
-
-=== Verification query ===
-
-To see the balance after the deposit, the Trader can trigger UC0006, or directly:
-
-{{{
-SELECT available_balance, invested_balance
-  FROM project.users
- WHERE id = $1;
-}}}
Index: cs/P3-UseCaseModel/wiki/UseCase0004.md
===================================================================
--- docs/P3-UseCaseModel/wiki/UseCase0004.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,95 +1,0 @@
-= Use-case 0004 — Place market BUY order =
-
-'''Initiating actor:''' Trader
-
-'''Other actors:''' Market Simulator (indirect — supplies the current price via `market_trades`).
-
-A Trader buys a crypto asset at the current market price. The operation touches five tables (`orders`, `users`, `holdings`, `transactions`, `market_trades`) and must either all succeed or all roll back.
-
-== Scenario ==
-
- 1. Trader chooses "Place market BUY order".
- 2. System lists the active markets, numbered, with their latest price:
-
-{{{
-SELECT m.id, c.id, c.symbol, m.quote_currency,
-       COALESCE(lp.price, 0) AS price
-  FROM project.markets m
-  JOIN project.crypto  c  ON c.id = m.crypto_id
-  LEFT JOIN project.v_latest_prices lp ON lp.market_id = m.id
- WHERE m.is_active = true
- ORDER BY c.symbol;
-}}}
-
- 3. Trader picks the market by its number in the listed markets, e.g. `2` (BTC).
- 4. System takes the chosen row's market id and crypto id from the list (no lookup by
-    symbol) and looks up the latest price:
-
-{{{
-SELECT price FROM project.v_latest_prices WHERE market_id = $1;
-}}}
-
- 5. Trader enters a quantity.
- 6. System computes notional = quantity × price, opens a transaction, and does:
-
-{{{
-BEGIN;
-
--- (a) record intent — no trade has happened yet.
-INSERT INTO project.orders
-    (user_id, market_id, side, type, status, quantity, price)
-VALUES
-    ($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,
-       invested_balance  = invested_balance  + $notional,
-       updated_at        = now()
- WHERE id = $user_id;
-
--- (d) upsert holding with running weighted-average price:
-SELECT quantity, avg_price
-  FROM project.holdings
- WHERE user_id = $user_id AND crypto_id = $crypto_id
- FOR UPDATE;
-
--- Either INSERT (new holding) or UPDATE (existing), computing
--- 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)
-VALUES
-    ($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;
-}}}
-
- 7. System confirms: `Order executed: buy 0.0100 BTC @ 67140.000000 (notional 671.4000 USD)`.
-
-=== Alternate flow 6a — insufficient funds ===
-
-If `available_balance < notional`, the entire transaction rolls back and system shows "Insufficient funds: need X, have Y."
-
-=== Alternate flow 3a — number not in the list ===
-
-If the entered number is not one of the listed market numbers, system shows "Invalid choice, enter a number from 1 to N." and returns to the authenticated menu without opening a transaction.
Index: cs/P3-UseCaseModel/wiki/UseCase0005.md
===================================================================
--- docs/P3-UseCaseModel/wiki/UseCase0005.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,141 +1,0 @@
-= Use-case 0005 — Place market SELL order =
-
-'''Initiating actor:''' Trader
-
-'''Other actors:''' Market Simulator (indirect — supplies the current price).
-
-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
-[wiki:PrototypeImplementation]) 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 ==
-
- 1. Trader chooses "Place market SELL order".
- 2. System lists, numbered, the Trader's holdings that still have a quantity free to
-    sell (not reserved by an open sell order), with the quantity held, the free
-    quantity and the latest price:
-
-{{{
-SELECT m.id, c.id, c.symbol, m.quote_currency,
-       h.quantity, h.quantity - h.reserved_quantity AS free,
-       COALESCE(lp.price, 0) AS price
-  FROM project.holdings h
-  JOIN project.crypto  c ON c.id = h.crypto_id
-  JOIN project.markets m ON m.crypto_id = c.id AND m.is_active = true
-  LEFT JOIN project.v_latest_prices lp ON lp.market_id = m.id
- WHERE h.user_id = $1
-   AND h.quantity - h.reserved_quantity > 0
- ORDER BY c.symbol;
-}}}
-
- 3. Trader picks the holding by its number in the listed holdings, e.g. `2` (ETH), and
-    enters the quantity.
- 4. System takes the chosen row's market id and crypto id from the list (no lookup by
-    symbol) and looks up the latest price:
-
-{{{
-SELECT price FROM project.v_latest_prices WHERE market_id = $1;
-}}}
-
- 5. System opens a transaction:
-
-{{{
-BEGIN;
-
--- (a) record intent — no trade has happened yet.
-INSERT INTO project.orders
-    (user_id, market_id, side, type, status, quantity, price)
-VALUES
-    ($user_id, $market_id, 'sell', 'market', 'open', $qty, $price)
-RETURNING id;   -- $order_id
-
--- (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;
--- available := quantity - reserved_quantity
--- abort if row missing or available < $qty
-}}}
-
- 6. If the check passes, system reserves the crypto, then — since this is a market order — settles it immediately, all inside the same transaction:
-
-{{{
--- (c) reserve: committed to this order, not yet removed from the position.
-UPDATE project.holdings
-   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;
-
-UPDATE project.users
-   SET available_balance = available_balance + $notional,
-       invested_balance  = GREATEST(invested_balance - ($avg_price * $qty), 0),
-       updated_at        = now()
- WHERE id = $user_id;
-
-INSERT INTO project.transactions
-    (user_id, type, amount, currency, related_order, description)
-VALUES
-    ($user_id, 'sell', $notional, 'USD', $order_id, 'Market sell ...');
-
-INSERT INTO project.market_trades
-    (market_id, executed_at, price, quantity, side, source)
-VALUES
-    ($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 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
-[wiki:UseCase0005Implementation].
-
-=== Realised P/L (post-scenario) ===
-
-The realised P/L for a sell is `$notional - ($avg_price * $qty)`. It is not persisted explicitly but can be computed from the ledger and the holding at sell time.
Index: cs/P3-UseCaseModel/wiki/UseCase0006.md
===================================================================
--- docs/P3-UseCaseModel/wiki/UseCase0006.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,75 +1,0 @@
-= Use-case 0006 — View portfolio and transaction history =
-
-'''Initiating actor:''' Trader
-
-'''Other actors:''' —
-
-A Trader inspects their current holdings, unrealised P/L, cash balance and recent ledger.
-
-== Scenario ==
-
-=== Portfolio ===
-
- 1. Trader chooses "View portfolio".
- 2. System queries the `v_portfolio` view:
-
-{{{
-SELECT symbol,
-       quantity,
-       COALESCE(reserved_quantity,  0),
-       COALESCE(available_quantity, quantity),
-       COALESCE(avg_price,      0),
-       COALESCE(current_price,  0),
-       COALESCE(market_value,   0),
-       COALESCE(unrealized_pnl, 0)
-  FROM project.v_portfolio
- WHERE user_id = $1
-   AND quantity > 0
- ORDER BY symbol;
-}}}
-
-`reserved_quantity` is the amount committed to the Trader's own open sell
-orders (see [wiki:UseCase0005]); `available_quantity` is what is
-actually free to sell right now.
-
- 3. System displays the rows and a computed summary:
-
-{{{
-SELECT available_balance, invested_balance
-  FROM project.users
- WHERE id = $1;
-}}}
-
-=== Transaction history ===
-
- 1. Trader chooses "View transaction history".
- 2. System queries the last 20 ledger entries:
-
-{{{
-SELECT created_at, type, amount, currency, COALESCE(description, '')
-  FROM project.transactions
- WHERE user_id = $1
- ORDER BY created_at DESC
- LIMIT 20;
-}}}
-
-=== Reference — how `v_portfolio` is defined ===
-
-{{{
-CREATE OR REPLACE VIEW project.v_portfolio AS
-SELECT h.user_id,
-       c.symbol,
-       h.quantity,
-       h.reserved_quantity,
-       (h.quantity - h.reserved_quantity)      AS available_quantity,
-       h.avg_price,
-       lp.price                                AS current_price,
-       (h.quantity * lp.price)                 AS market_value,
-       (h.quantity * (lp.price - h.avg_price)) AS unrealized_pnl
-  FROM project.holdings h
-  JOIN project.crypto   c ON c.id = h.crypto_id
-  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;
-}}}
Index: cs/P3-UseCaseModel/wiki/UseCase0007.md
===================================================================
--- docs/P3-UseCaseModel/wiki/UseCase0007.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,76 +1,0 @@
-= Use-case 0007 — Manage watchlist =
-
-'''Initiating actor:''' Trader
-
-'''Other actors:''' —
-
-A Trader keeps a list of crypto assets they want to monitor. Adding an asset that is already on the list is a no-op (idempotent).
-
-== Scenario ==
-
- 1. Trader chooses "Manage watchlist".
- 2. System ensures the Trader has a default watchlist named "Favorites":
-
-{{{
-SELECT id FROM project.watchlists
- WHERE user_id = $1
- ORDER BY created_at LIMIT 1;
-
--- if no row:
-INSERT INTO project.watchlists (user_id, name)
-VALUES ($1, 'Favorites')
-RETURNING id;
-}}}
-
- 3. System shows the sub-menu: List / Add / Remove / Back.
-
-=== List items ===
-
-{{{
-SELECT c.symbol, c.name, COALESCE(lp.price, 0)
-  FROM project.watchlist_items wi
-  JOIN project.crypto c ON c.id = wi.crypto_id
-  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
- WHERE wi.watchlist_id = $1
- ORDER BY c.symbol;
-}}}
-
-=== Add a crypto ===
-
-The Trader picks the crypto by its number from the listed cryptos that are not on the watchlist yet:
-
-{{{
--- 1. list, numbered, the cryptos not yet on the watchlist
-SELECT c.id, c.symbol, c.name
-  FROM project.crypto c
- WHERE NOT EXISTS (SELECT 1 FROM project.watchlist_items wi
-                    WHERE wi.watchlist_id = $1 AND wi.crypto_id = c.id)
- ORDER BY c.symbol;
-
--- 2. insert the chosen crypto ($2 = its id from the list); do nothing if it's already there
-INSERT INTO project.watchlist_items (watchlist_id, crypto_id)
-VALUES ($1, $2)
-ON CONFLICT (watchlist_id, crypto_id) DO NOTHING;
-}}}
-
-=== Remove a crypto ===
-
-The Trader picks the crypto by its number from the listed cryptos on the watchlist:
-
-{{{
--- 1. list, numbered, the cryptos on the watchlist
-SELECT c.id, c.symbol, c.name
-  FROM project.watchlist_items wi
-  JOIN project.crypto c ON c.id = wi.crypto_id
- WHERE wi.watchlist_id = $1
- ORDER BY c.symbol;
-
--- 2. delete the chosen crypto ($2 = its id from the list)
-DELETE FROM project.watchlist_items
- WHERE watchlist_id = $1 AND crypto_id = $2;
-}}}
-
-If the entered number is not one of the listed numbers, system shows "Invalid choice, enter a number from 1 to N." and nothing is changed.
Index: cs/P3-UseCaseModel/wiki/UseCaseModel.md
===================================================================
--- docs/P3-UseCaseModel/wiki/UseCaseModel.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,118 +1,0 @@
-= Use-case model =
-
-The detailed pages for this phase are kept in the project's !GitHub repository,
-`https://github.com/StefanTrsunov/bp`, under `docs/P3-UseCaseModel/`. Every
-use case below is documented on its own wiki page ([wiki:UseCase0001] to [wiki:UseCase0007]).
-
-== Actors / Roles ==
-
-'''Visitor''' – Anyone using !EduBerza without an account, who can look at public market
-information, create an account, and log in.
-
-'''Trader''' – A registered, logged-in user who deposits virtual funds, places market buy and
-sell orders, follows the value of their portfolio, and keeps a watchlist of assets they want to
-monitor.
-
-'''Market Simulator''' – An external automated system (the bot in `bots/`) that writes simulated
-trades and candles into the database so prices move without a connection to a real exchange.
-
-== Use-Cases ==
-
-=== Visitor ===
-
- * UC0001 –
-   '''Register new account''' – Visitor creates an account with a unique username and e-mail; the
-   password is stored as a SHA-256 hash.
- * UC0002 –
-   '''Log in''' – Visitor authenticates with username and password so the system treats every
-   following action as a Trader.
-
-=== Trader ===
-
- * UC0003 –
-   '''Deposit virtual funds''' – Trader tops up their virtual cash balance; the user row and the
-   ledger are written in one transaction.
- * UC0004 –
-   '''Place market BUY order''' – Trader buys a crypto asset at the current market price, which
-   debits cash and upserts the holding at a running weighted-average price.
- * UC0005 –
-   '''Place market SELL order''' – Trader sells part or all of a holding at the current market
-   price, which reserves the crypto being sold, credits cash and preserves the cost basis.
- * UC0006 –
-   '''View portfolio and transaction history''' – Trader inspects current holdings, unrealised
-   P/L, cash balances and the most recent ledger entries.
- * UC0007 –
-   '''Manage watchlist''' – Trader lists, adds and removes crypto assets on a personal watchlist,
-   where adding an asset already on the list is a no-op.
-
-=== Market Simulator ===
-
-The Market Simulator initiates no use case of its own. It participates in
-UC0004 and
-UC0005
-indirectly, by keeping `project.market_trades` populated so that `project.v_latest_prices`
-returns a current price for every active market.
-
-== Use-case model diagram ==
-
-{{{#!comment
-The diagram is optional for P3. Attach the exported image use_case_diagram.png
-to this wiki page and then replace this comment with:
-
-[[Image(use_case_diagram.png)]]
-}}}
-
-== Detailed Use-Cases ==
-
-The following use-cases are documented in detail, with SQL tested against the P2 database:
-
- * [wiki:UseCase0001] – Visitor registers a new account
- * [wiki:UseCase0002] – Visitor logs in
- * [wiki:UseCase0003] – Trader deposits virtual funds
- * [wiki:UseCase0004] – Trader places a market BUY order
- * [wiki:UseCase0005] – Trader places a market SELL order
- * [wiki:UseCase0006] – Trader views portfolio and transaction history
- * [wiki:UseCase0007] – Trader manages a watchlist
-
-== Realization details on selection of the most important use cases ==
-
-This is a solo project, so '''at least 3 use cases''' are required. '''7 use cases''' are
-documented, for a safety margin. All seven are implemented in the P4 prototype; see
-`server/` for the Go source and
-[wiki:PrototypeImplementation]
-for the documented runs.
-
-||=Use case=||=Importance=||=Why it was selected=||
-||UC0001 – Register||High||Nothing else works without it; demonstrates `INSERT` with a uniqueness check.||
-||UC0002 – Log in||High||Authenticates every Trader action; demonstrates `SELECT` with parameter binding.||
-||UC0003 – Deposit||High||Shows a multi-row transaction: `UPDATE users` plus `INSERT INTO transactions`.||
-||UC0004 – Buy||Very high||Core of the exchange: `INSERT orders`, `UPDATE users`, upsert `holdings`, ledger entry, market trade.||
-||UC0005 – Sell||Very high||Dual of Buy; demonstrates row-level `FOR UPDATE` locking, reservation of committed crypto (`holdings.reserved_quantity`) and cost-basis bookkeeping.||
-||UC0006 – Portfolio||High||Demonstrates joins over `holdings`, `markets` and `crypto`, and the `v_portfolio` view.||
-||UC0007 – Watchlist||Medium||Demonstrates N–M relation handling and `ON CONFLICT` upsert semantics.||
-
-== AI usage ==
-
-AI was used in this phase and is logged in full, per the course rule for P1 onward.
-
- * '''Phase log:'''
-   [wiki:UseCaseModelAIUsage]
-   – service used, what the AI produced, and what I decided myself.
- * '''Full conversation transcript:'''
-   [wiki:ERModelAIUsage]
-   – the same conversation produced the P1–P4 artefacts, so the complete prompt/response log is
-   kept in one place. Relevant sections of that page:
-   Session 1 – 2026-04-21,
-   Session 2 – 2026-08-06/07,
-   Session 3 – 2026-09-16.
-
-'''Service:''' Claude Code (Anthropic), `https://claude.com/claude-code` – Claude subscription,
-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 the Session 3 – 2026-09-16 section of
-[wiki:UseCaseModelAIUsage].
Index: cs/P3-UseCaseModel/wiki/UseCaseModelAIUsage.md
===================================================================
--- docs/P3-UseCaseModel/wiki/UseCaseModelAIUsage.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,93 +1,0 @@
-= Use-Case Model AI Usage =
-
-== Name of AI service/solution that was used ==
-
-'''Claude Code''' (Anthropic)
-
- * '''URL:''' `https://claude.com/claude-code`
- * '''Type of service/subscription:''' Claude subscription, model Claude Opus 4.7 (1M context).
-
-== Final result ==
-
-=== Results in details / description ===
-
-The AI:
-
- * Proposed the actor taxonomy (Visitor, Trader, Market Simulator) from the project description and the existing Go code.
- * Derived a set of 7 use cases covering the full trading loop (register, login, deposit, buy, sell, view portfolio, watchlist).
- * Wrote each `UseCaseXXXX.md` file with:
-   * initiating actor, other actors, goals
-   * step-by-step dialog-form scenario
-   * '''tested''' SQL statements for every database-touching step, using the `project` schema
-   * alternate flows for the most common failure cases (insufficient funds, insufficient holding, duplicate user, invalid input)
-
-== Summary of AI involvement ==
-
-||= =||= Session 1 — 2026-04-21 =||= Session 2 — 2026-08-06/07 =||
-|| '''What I brought''' || The project description in `opis.md` and the existing Go backend || The use-case model as completed in session 1 ||
-|| '''What the AI did''' || Proposed the actor taxonomy and drafted seven use cases with tested SQL || Nothing — the model was not changed ||
-|| '''What I decided''' || To go solo, and therefore to document seven use cases rather than the three the rubric requires || To re-verify every scenario's SQL against a live database rather than trust the April run ||
-
-This phase was finished in session 1. In session 2 the only work was
-verification: each scenario was executed against a live PostgreSQL 16 database,
-including the failure paths, and the results recorded on the
-`UseCaseXXXXImplementation` pages.
-
-== Entire AI usage log ==
-
-See [wiki:ERModelAIUsage] for the full transcript — the same 2026-04-21 session produced the use-case documentation. The defining student prompt was:
-
-> I'm going solo do everything that you need to do, and tell me after what do I need to do
-
-which led the AI to pick "solo → 3 minimum per rubric → document 7 for a margin" as a default.
-
-> '''Student action required:''' append any future refinements of the use-case list or scenarios here.
-
-
-=== Session 2 — 2026-08-06 / 2026-08-07 ===
-
-No changes were made to the use-case model in this session: the actor list, the
-seven use cases and the scenario SQL in `UseCase0001`–`UseCase0007` are as
-produced on 2026-04-21. The SQL in those scenarios was, however, re-verified by
-executing the corresponding prototype flows against a live PostgreSQL 16
-database, including the failure paths (insufficient funds, insufficient holding,
-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 the Session 3 — 2026-09-16 section of
-[wiki:ERModelAIUsage]:
-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:'''
-
- * [wiki:UseCase0005] — 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).
- * [wiki:UseCase0004] — 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 [wiki:ERModel]
-   describes for it rather than a status column that is only ever written
-   once.
- * [wiki:UseCase0006] — 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
-   [wiki:UseCaseModel] 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
-[wiki:UseCase0005Implementation].
-
-'''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: cs/P4-Prototype/BuildInstructions.md
===================================================================
--- docs/P4-Prototype/BuildInstructions.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,267 +1,0 @@
-# Build Instructions
-
-This page explains how to compile, configure, run and test the EduBerza prototype.
-It is linked from [PrototypeImplementation](PrototypeImplementation.md).
-
-## Development environment description
-
-| Tool        | Version tested        | Needed for                                                    |
-|-------------|-----------------------|---------------------------------------------------------------|
-| Go          | 1.26.0 (`go.mod` asks for 1.25 or newer) | Building `server/` (the CLI) and `bots/` (the market bot). |
-| PostgreSQL  | 16.3 (Docker container) | The database. Either the local Docker container or the faculty server. |
-| Docker + Docker Compose | any recent | Optional. Starts a local PostgreSQL with one command. |
-| `psql`      | 16                    | Optional. Only for running the SQL scripts by hand.            |
-| Java        | 21 (8+ works)         | Optional. Only to open or edit the ER diagram in TerraER.      |
-| DBeaver     | any recent            | Optional. Only to export `relational_diagram_v4.png`.             |
-
-About the PostgreSQL version: `docker-compose.yml` uses the image `postgres` without a version
-tag. Docker therefore starts whatever version of the official image it has pulled. On the
-machine where the prototype was tested, that was PostgreSQL 16.3. The only extension the schema
-needs is `pgcrypto` (`CREATE EXTENSION IF NOT EXISTS pgcrypto`). It ships with PostgreSQL and is
-included in the official image.
-
-You do not need to install anything else. The only third-party Go library is the PostgreSQL
-driver `github.com/lib/pq`. `go build` downloads it automatically, at the version pinned in
-`go.mod` and `go.sum`.
-
-## Build instructions
-
-Run all commands from the repository root.
-
-### 1. Configure the database connection
-
-```sh
-cp .env.example .env
-```
-
-The defaults in `.env.example` (`localhost:5433`, user `bp_project`, database `bp_database`)
-match the bundled Docker setup. To use the faculty database instead, edit `.env`, or pass the
-values as real environment variables. Real environment variables take precedence over the file:
-
-```sh
-DBHOST=... DBPORT=5432 DBUSER=... DBPASSWORD=... DBNAME=... ./eduberza
-```
-
-`.env` is not committed on purpose (see `.gitignore`), because it holds a password.
-
-### 2. Start PostgreSQL
-
-```sh
-docker compose up -d
-```
-
-Skip this step if you use the faculty database.
-
-### 3. Build
-
-```sh
-go build -o eduberza ./server
-```
-
-### 4. Create the schema and load the sample data
-
-```sh
-./eduberza -init
-```
-
-This runs `server/db/schema_creation.sql` and then `server/db/data_load.sql`. It logs
-`Running schema_creation.sql ...`, `Running data_load.sql ...` and `Database initialised.`,
-then prints:
-
-```
-Schema initialised. Re-run without -init to start the CLI.
-```
-
-Both scripts are **compiled into the binary** (`go:embed`), so `-init` works from any
-directory. It is destructive and can be run again any number of times: it drops and recreates
-the whole `project` schema, so it also resets everything if a demo goes wrong. To reload only
-the data and keep the schema:
-
-```sh
-./eduberza -load-data          # prints "Sample data reloaded."
-```
-
-If you prefer to watch the statements run, the same can be done with `psql`:
-
-```sh
-psql "postgresql://$DBUSER:$DBPASSWORD@$DBHOST:$DBPORT/$DBNAME" \
-  -f server/db/schema_creation.sql
-psql "postgresql://$DBUSER:$DBPASSWORD@$DBHOST:$DBPORT/$DBNAME" \
-  -f server/db/data_load.sql
-```
-
-### 5. Run the prototype
-
-```sh
-./eduberza
-```
-
-### 6. Optional: run the market simulation bot
-
-In a second terminal, also from the repository root (the bot reads `.env` from the current
-directory):
-
-```sh
-go run ./bots                  # add -interval 1s for faster ticks; the default is 3s
-```
-
-On every tick the bot moves the price of every active market by a small random step, inserts a
-row into `market_trades` and updates the current 1-minute candle. Prices in the CLI change
-while it runs, because the current price is always read from the most recent trade
-(`v_latest_prices`) and never from a stored column. Leave the bot off if you want the exact
-numbers in the tests below.
-
-### 7. Optional: richer data for the P6 reports
-
-`data_load.sql` seeds only a few minutes of trade history. That is not enough for the
-[top traders](../P6-AdvancedReports/AdvancedReports.md) and
-[market performance](../P6-AdvancedReports/AdvancedReports.md) reports (menu `[10]` and `[11]`)
-to show more than one period. To see more interesting results, load five quarters of synthetic
-history on top:
-
-```sh
-psql "postgresql://$DBUSER:$DBPASSWORD@$DBHOST:$DBPORT/$DBNAME" \
-  -f server/db/reports_demo_data.sql
-```
-
-This script is not part of `-init` or `-load-data` on purpose. The header of
-[`reports_demo_data.sql`](../../server/db/reports_demo_data.sql) explains why. Running it never
-changes the balances that the tests below check.
-
-## Testing instructions
-
-### How to launch and log in
-
-Start the prototype with `./eduberza` after steps 1–4. The sample data creates three test
-users. All of them have the password **`test123`**:
-
-| Username  | Starting state after `-init` |
-|-----------|------------------------------|
-| `alice`   | 8250.00 USD available (1750.00 invested), holds 0.5 ETH bought at 3500.00. Watchlist "Favorites": BTC, ETH, SOL. Best demo account. |
-| `bob`     | 5000.00 USD available, no crypto. Watchlist "Bobs Picks": BTC, DOGE. |
-| `charlie` | 2500.00 USD available, no crypto, no watchlist yet. |
-
-The five sample markets are ADA, BTC, DOGE, ETH and SOL, all quoted in USD. Their starting
-last prices are 0.45375, 67140, 0.122, 3520 and 166.1.
-
-### Mini-guide to the application
-
-You always answer with the number of a menu option. When you have to choose a market, a
-holding or a crypto, the prototype prints a numbered list and you type the number from that
-list. You never type an id or a symbol. A number that is not in the list is refused with
-`Invalid choice, enter a number from 1 to N.`
-
-**Menu before login**
-
-| Option | What it does and how to use it |
-|--------|--------------------------------|
-| `[1] Register` | Enter a username, an e-mail (must contain `@`), your full name and a password (at least 6 characters). You get `Account created. You can now log in.`, or `Invalid email.`, `Password must be at least 6 characters.` or `Username or email already taken.` A new account starts with 0 USD. |
-| `[2] Login` | Enter your username and password. You get `Login successful.` and the second menu. A wrong password and an unknown username both give `Invalid credentials.` |
-| `[3] Browse markets` | Prints the numbered list of markets with their last price. |
-| `[0] Exit` | Ends the program. |
-
-**Menu after login** (headed `--- Logged in as <username> ---`)
-
-| Option | What it does and how to use it |
-|--------|--------------------------------|
-| `[1] View balance` | Shows the available, invested and total USD. |
-| `[2] Deposit virtual funds` | Enter an amount in USD. It must be a positive number, otherwise you get `Invalid amount.` You get `Deposited 500.0000 USD.` |
-| `[3] Browse markets` | Same list as before login. |
-| `[4] Place market BUY order` | Lists all markets, numbered, with their last price. Type the number at `Market #:`. The prototype shows the latest price. Type the quantity. You get `Order executed: buy …` or `Insufficient funds: need …, have …`. |
-| `[5] Place market SELL order` | Lists only the cryptos you hold, numbered, with columns `Held` and `Free to sell`. Type the number at `Holding #:`, then the quantity. You get `Order executed: sell …` or `Insufficient holding: …`. If you hold nothing, you get `you hold no crypto that is free to sell`. |
-| `[6] View portfolio` | One row per crypto you hold: quantity, reserved, available, average buy price, current price, value and unrealised P/L. Then your cash, portfolio value and net worth. |
-| `[7] View transaction history` | Your last 20 ledger entries (deposits, buys, sells), newest first. |
-| `[8] Manage watchlist` | Opens a submenu: `[1] List items` shows your watchlist with last prices. `[2] Add crypto` lists, numbered, the cryptos not on it yet; type a number. `[3] Remove crypto` lists, numbered, the cryptos on it; type a number. `[0] Back` returns. A user without a watchlist gets one named "Favorites" the first time. |
-| `[9] Logout` | Back to the first menu. |
-| `[10] Report: top traders` | P6 report. Enter a start date (inclusive) and an end date (exclusive) as `YYYY-MM-DD`. |
-| `[11] Report: market performance` | P6 report, with the same two dates. |
-| `[0] Exit` | Ends the program. |
-
-### End-to-end smoke test
-
-These values were checked on 2026-09-24 against freshly loaded sample data (PostgreSQL 16.3),
-with the bot not running. The expected values are exact.
-
-1. `./eduberza -init` prints `Schema initialised. Re-run without -init to start the CLI.`
-2. `./eduberza`, then `2` (Login), then `alice` / `test123` gives `Login successful.`
-3. `6` (View portfolio) shows one row: `ETH`, quantity 0.5000, reserved 0.0000, available
-   0.5000, average buy 3500.000000, current 3520.000000, value 1760.0000, unrealised P/L
-   `+10.0000`. Cash available is 8250.0000 and net worth is 10010.0000.
-4. `4` (BUY). The market list shows `1 ADA`, `2 BTC`, `3 DOGE`, `4 ETH`, `5 SOL`. Type `2` at
-   `Market #:`, then `0.01` at `Quantity:`. The result is
-   `Order executed: buy 0.0100 BTC @ 67140.000000 (notional 671.4000 USD)`.
-5. `6` (View portfolio) now shows BTC *and* ETH, with total value 2431.4000, cash 7578.6000
-   (= 8250.00 − 671.40), and net worth still 10010.0000.
-6. `5` (SELL). The holdings list shows `1 BTC` (held 0.0100) and `2 ETH` (held 0.5000). Type
-   `2` at `Holding #:`, then `0.5`. The result is
-   `Order executed: sell 0.5000 ETH @ 3520.000000 (notional 1760.0000 USD)`.
-7. `7` (View transaction history) lists, newest first: the `sell` (+1760.0000), the `buy` of
-   BTC (−671.4000), then the two rows from the sample data, which have the same timestamp:
-   `deposit` 10000.0000 "Initial virtual deposit" and `buy` −1750.0000 "Market buy 0.5 ETH @
-   3500.00".
-8. `8` (Manage watchlist), then `1` (List items), shows alice's watchlist with BTC, ETH and SOL
-   and their last prices. Then `2` (Add crypto) lists `1 ADA` and `2 DOGE`; type `1` and you get
-   `Added ADA.` Then `0` (Back).
-9. `9` (Logout), then `0` (Exit).
-
-### Testing the failure paths
-
-These matter more than the happy path, because they prove that the transactions really roll
-back and that invalid choices are refused:
-
-- **Insufficient funds:** log in as `charlie` (2500 USD). Choose `4`, market `2` (BTC),
-  quantity `1`. Expect `Insufficient funds: need 67140.0000, have 2500.0000` and *no* change to
-  any table: no order row, no ledger entry, no holding.
-- **Insufficient holding:** on fresh data (`./eduberza -load-data`), log in as `alice`. Choose
-  `5`; the list shows only `1 ETH` (held 0.5000, free 0.5000). Choose `1`, quantity `5`. Expect
-  `Insufficient holding: trying to sell 5.0000, available 0.5000 (of 0.5000 held, 0.0000 reserved)`.
-- **Nothing to sell:** as `bob` (no crypto), choose `5`. The holdings list is empty, and you
-  get `you hold no crypto that is free to sell` without being asked for a number.
-- **Invalid choice from a list:** in any list (for example `8`, then `3` Remove crypto), type a
-  number larger than the list. Expect `Invalid choice, enter a number from 1 to N.`
-- **Invalid deposit:** choose `2` and enter `-50`. Expect `Invalid amount.`
-- **Duplicate registration:** register with username `alice`. Expect
-  `Username or email already taken.`
-- **Invalid e-mail:** register with an e-mail without `@`. Expect `Invalid email.`
-- **Wrong password:** log in as `alice` with any wrong password. Expect
-  `Invalid credentials.` An unknown username gives the same message, so the prototype does not
-  reveal which accounts exist.
-
-The concurrency guarantee of the sell path (two processes selling the same crypto at the same
-moment) cannot be reproduced by typing into two terminals, because each order commits within
-milliseconds. It is described in [UseCase0005Implementation](UseCase0005Implementation.md).
-
-### For the public presentation
-
-Demo with `alice`. She already has a position, so the portfolio screen is not empty. Register a
-brand-new account live to show UC0001. Run the bot in a background terminal so the prices
-visibly move between two portfolio refreshes.
-
-## Editing the ER diagram
-
-TerraER is a third-party tool and is **not** committed to this repository on purpose. Download
-the teacher's build from <https://bazi.finki.ukim.mk/resources/Software/> and run it:
-
-```sh
-java -jar TerraER3.11.jar     # then File → Open → docs/P1-ConceptualModel/ERModel_v05.xml
-```
-
-The current version is `ERModel_v05.xml`. Save new versions as `ERModel_v06.xml` and so on,
-and export a matching PNG for each. TerraER does not add the extension itself: type `.xml`
-yourself, or the file will not reopen.
-
-## Up-to-date source code
-
-The repository is pushed to the FINKI DEVELOP git server. The clone URL and credentials are in
-the Repositories section in EPRMS.
-
-### About the source code
-
-- All the source needed to run the prototype is in this repository: the CLI (`server/`), the
-  market bot (`bots/`), the DDL script and the sample-data script (`server/db/`).
-- Third-party Go libraries are **not** vendored. `go build` downloads `github.com/lib/pq` at
-  the versions pinned in `go.mod` and `go.sum`.
-- Third-party executables are **not** committed. `.gitignore` excludes `*.jar`, and TerraER is
-  downloaded from the URL above.
-- No third-party images, styles or frameworks are used. The prototype has no images at all;
-  the interface is text.
Index: cs/P4-Prototype/PrototypeImplementation.md
===================================================================
--- docs/P4-Prototype/PrototypeImplementation.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,128 +1,0 @@
-# Prototype Implementation
-
-EduBerza's P4 prototype is a Go command-line program (in [`server/`](../../server/)). It works
-against the `project` schema in PostgreSQL. It implements all seven use cases from
-[UseCaseModel](../P3-UseCaseModel/UseCaseModel.md); the course asks for at least three. Every
-database access is real SQL that was executed and tested. A second program, the market bot in
-[`bots/`](../../bots/), simulates a live market so prices move while the prototype runs.
-
-## Implemented use-cases
-
-- [UseCase0001Implementation](UseCase0001Implementation.md) — Register new account
-- [UseCase0002Implementation](UseCase0002Implementation.md) — Log in
-- [UseCase0003Implementation](UseCase0003Implementation.md) — Deposit virtual funds
-- [UseCase0004Implementation](UseCase0004Implementation.md) — Place market BUY order
-- [UseCase0005Implementation](UseCase0005Implementation.md) — Place market SELL order
-- [UseCase0006Implementation](UseCase0006Implementation.md) — View portfolio and transaction history
-- [UseCase0007Implementation](UseCase0007Implementation.md) — Manage watchlist
-
-Each page follows its P3 use case step by step. It adds the exact SQL the Go code runs in that
-step and a screenshot of the step from a real run against the database. Screenshots are in
-[`screenshots/`](screenshots/).
-
-- How to build, configure, run and test the prototype: [BuildInstructions](BuildInstructions.md)
-- AI usage for this phase: [PrototypeImplementationAIUsage](PrototypeImplementationAIUsage.md)
-
-## Technology and architecture
-
-- **Language:** Go (module `bp_project`, `go 1.25` in [`go.mod`](../../go.mod)). The only
-  third-party library is the PostgreSQL driver `github.com/lib/pq`.
-- **Database:** PostgreSQL. Every table, view and function is in the `project` schema. The DDL
-  is [`schema_creation.sql`](../../server/db/schema_creation.sql) and the sample data is
-  [`data_load.sql`](../../server/db/data_load.sql). Both scripts are compiled into the binary
-  and run by `./eduberza -init`.
-- **Interface:** plain text menus on standard input and output. There are no web server,
-  frameworks, images or styles.
-- **Structure:** one source file per area of the application.
-
-| File | Responsibility | Use cases |
-|------|----------------|-----------|
-| [`server/main.go`](../../server/main.go) | Flags `-init` / `-load-data`, then starts the menu loop | — |
-| [`server/cli.go`](../../server/cli.go) | The two menus (before and after login), input reading | all |
-| [`server/db/db.go`](../../server/db/db.go) | Connection from `.env` / environment variables, embedded SQL scripts | — |
-| [`server/auth.go`](../../server/auth.go) | Register, log in (SHA-256 password hash) | UC0001, UC0002 |
-| [`server/account.go`](../../server/account.go) | Balance, deposit, transaction history | UC0003, UC0006 |
-| [`server/market.go`](../../server/market.go) | Market list, choosing a market or a holding by number, latest price | UC0004, UC0005 |
-| [`server/trade.go`](../../server/trade.go) | Market buy and sell orders, each in one transaction | UC0004, UC0005 |
-| [`server/portfolio.go`](../../server/portfolio.go) | Portfolio with current value and unrealised P/L | UC0006 |
-| [`server/watchlist.go`](../../server/watchlist.go) | List, add and remove watchlist items | UC0007 |
-| [`bots/main.go`](../../bots/main.go) | Market bot: random-walk price ticks into `market_trades`, 1-minute candles | — |
-
-## No identifiers to remember
-
-The user never has to type or remember an id, a code or a symbol:
-
-- Every menu is numbered, and the user answers with the number of an option.
-- **Buying:** all active markets are listed with their latest price, numbered `1…n`. The user
-  enters the market's number at `Market #:` (`ChooseMarket` in `market.go`).
-- **Selling:** only the cryptos the user actually holds are listed, each with the quantity held
-  and the quantity still free to sell. The user enters the holding's number at `Holding #:`
-  (`ChooseHolding`). A user who holds nothing free to sell gets
-  `you hold no crypto that is free to sell` and is never asked to choose.
-- **Watchlist:** *Add* lists the cryptos that are not on the watchlist yet. *Remove* lists the
-  ones that are on it. Both are numbered, and the user enters the number.
-- A number outside the list is refused with `Invalid choice, enter a number from 1 to N.` and
-  nothing is changed.
-
-The only things the user types are their own data: username, e-mail, full name, password, the
-amount to deposit, the quantity to buy or sell, and the date range of the two P6 reports.
-
-## What the prototype demonstrates about the database design
-
-- **The current price is never stored as a column.** It is always the price of the most recent
-  row in `market_trades`, read through the `v_latest_prices` view. The user's own fills and the
-  bot's simulated trades go into the same table, so there is only one definition of "the
-  price".
-- **Money movements are transactional.** A buy touches five tables (`orders`, `users`,
-  `holdings`, `transactions`, `market_trades`) inside one transaction. If the balance check
-  fails, the whole transaction is rolled back: after a rejected purchase there is no order row,
-  no ledger entry and no holding. The failure-path tests in
-  [BuildInstructions](BuildInstructions.md) check this.
-- **Constraints do real work.** `UNIQUE (user_id, crypto_id)` on `holdings` is what makes the
-  `INSERT … ON CONFLICT DO UPDATE` upsert possible, so the database recomputes the
-  weighted-average entry price in one statement, instead of the application reading, changing
-  and writing the row. `CHECK (reserved_quantity >= 0 AND reserved_quantity <= quantity)` does
-  the same for the sell path: the database itself makes an inconsistent reservation
-  impossible, and it does not rely only on `trade.go` being careful.
-- **Selling reserves before it removes.** A sell order locks the holding row with
-  `SELECT … FOR UPDATE`, reserves the quantity being sold, then settles by removing it (see
-  [UseCase0005Implementation](UseCase0005Implementation.md)). Two sell orders for more than the
-  free quantity, placed at the same moment from two separate processes, are serialised by the
-  row lock. Exactly one of them succeeds. This was tested with two concurrent processes in
-  session 3 (see [PrototypeImplementationAIUsage](PrototypeImplementationAIUsage.md)).
-
-## Known limitations
-
-These were left out on purpose for a first prototype. They belong to the later phases:
-
-- Only `market` orders execute. The schema accepts `limit` (`orders.type`), but there is no
-  matching logic for it.
-- Passwords are hashed with SHA-256 and no salt. That shows the password itself is never
-  stored, but it is not good enough for real use. A proper password hash belongs in P9
-  (security).
-- Money is `float64` in Go, while the database columns are `numeric`. For that reason, all
-  arithmetic that must be exact (the weighted average) is done in SQL. Real use would need a
-  decimal type on the Go side too.
-- The prototype sets no connection pool and no explicit isolation level. Both are P8 topics.
-- A reservation only exists inside one transaction, because the prototype only has market
-  orders, and they settle immediately. A real limit-order matcher would leave
-  `holdings.reserved_quantity` set and `orders.status = 'open'` between two separate commits.
-  It would also need a way to cancel an order and release the reservation. Neither is
-  implemented, because nothing in the prototype creates an order that stays open.
-
-## History of changes
-
-The code started as my own Go backend: HTTP handlers, a draft schema, and a `db.go` that
-recreated the tables on every start. It changed as follows. The AI's share of each change is
-logged in [PrototypeImplementationAIUsage](PrototypeImplementationAIUsage.md).
-
-| Date | Change | Origin |
-|------|--------|--------|
-| 2026-04-21 | My HTTP backend rewritten as the CLI prototype covering UC0001–UC0007. The schema errors in my draft were corrected. The market bot was added. | My code and decisions (CLI instead of web, drop the frontend, keep a simulator); rewrite by AI (session 1) |
-| 2026-08-06/07 | Three bugs fixed: path resolution of `.env` and the SQL scripts, an endless loop at end of input, and an error check in the wrong order on the sell path. The holding update became one `INSERT … ON CONFLICT DO UPDATE`. | I asked for a code review; fixes by AI (session 2) |
-| 2026-09-16 | `holdings.reserved_quantity` added. The sell path now reserves, then settles. Orders go from `open` to `executed`. | The edge case was mine; implementation by AI (session 3) |
-| 2026-09-24 | Every choice is picked from a numbered list: markets by number, a sell lists only the user's holdings, the watchlist lists the cryptos. All screenshots were retaken, one per step. | I asked for a check against the P4 rules; implementation by AI (session 4) |
-
-**Service:** Claude Code (Anthropic), Claude subscription. Session 1 used Claude Opus 4.7 (1M
-context), session 2 Claude Opus 5 (1M context), session 3 Claude Sonnet 5, and session 4
-Claude Opus 5.5 (1M context).
Index: cs/P4-Prototype/PrototypeImplementationAIUsage.md
===================================================================
--- docs/P4-Prototype/PrototypeImplementationAIUsage.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,393 +1,0 @@
-# Prototype Implementation AI Usage
-
-## Name of AI service/solution that was used
-
-**Claude Code** (Anthropic), an AI coding assistant that runs in the terminal and reads and
-edits the project files.
-
-- **URL:** https://claude.com/claude-code
-- **Type of service/subscription:** Claude subscription (Claude Code CLI). A different model was
-  used in each session:
-
-| Session | Date | Model |
-|---------|------|-------|
-| 1 | 2026-04-21 | Claude Opus 4.7 (1M context) |
-| 2 | 2026-08-06 / 2026-08-07 | Claude Opus 5 (1M context) |
-| 3 | 2026-09-16 | Claude Sonnet 5 |
-| 4 | 2026-09-24 | Claude Opus 5.5 (1M context) |
-
-The same sessions also worked on other phases. This page covers only what concerns the P4
-prototype and its documentation.
-
-## Final result
-
-### Results in details / description
-
-**My own starting code (before any AI was used).** Before session 1 I had written:
-
-- a Go backend with HTTP handlers (Chi router);
-- a draft database schema (`server/db/db.sql`, `server/db/schema.sql`) and a diagram description
-  (`docs/dbdiagram.md`), based on my data model in `ep-diagram.md`;
-- a `main.go` / `db.go` that connected to PostgreSQL and dropped and recreated every table on
-  each start;
-- a half-finished frontend, which I deleted myself at the start of session 1 because the
-  prototype does not need it.
-
-This code is older than the git repository. The first commit (2026-08-07) was made after
-sessions 1 and 2, so the history in git starts from the AI-improved version. My original files
-are not in the repository. What they contained, and which errors the AI found in them, is
-recorded in the session 1 log below and in [ERModelAIUsage](../P1-ConceptualModel/ERModelAIUsage.md).
-
-**Session 1 (2026-04-21).** Starting from that code, the AI:
-
-- replaced my HTTP backend and the frontend scaffolding with a single-binary CLI prototype in Go,
-  split across `main.go`, `cli.go`, `auth.go`, `account.go`, `market.go`, `trade.go`,
-  `portfolio.go` and `watchlist.go`. This followed my decision to make a CLI and not a web app;
-- corrected my schema. `crypto_id` had been declared as a foreign key to two tables at once in
-  `holdings`, `orders` and `transactions`, and `market_candles` referenced a `markets` table that
-  did not exist. The corrected schema became `schema_creation.sql`, with the sample data in
-  `data_load.sql`;
-- replaced my `db.go`, which dropped every table on every start, with one `Connect()` plus
-  explicit `-init` and `-load-data` flags;
-- moved `go.mod` to the project root, removed an unused MySQL driver, and made
-  `github.com/lib/pq` a direct dependency;
-- wrote the trade flows as database transactions with `FOR UPDATE` row locks, cost-basis
-  bookkeeping and one ledger row per operation;
-- wrote the market-simulation bot (`bots/main.go`), after I decided to keep a market simulator
-  in the project.
-
-**Session 2 (2026-08-06/07).** I asked for a code review. The AI found and fixed three bugs
-(`.env`/SQL path resolution, an endless loop at end of input, and a wrong error check on the
-sell path). It also replaced the read-modify-write holding update with one
-`INSERT … ON CONFLICT DO UPDATE`, removed the committed `TerraER3.11.jar` and a TradingView
-screenshot we had no licence for, and wrote the first version of the P4 pages.
-
-**Session 3 (2026-09-16).** From an edge case I described, the AI added
-`holdings.reserved_quantity`, made the sell path reserve and then settle, and gave
-`orders.status` a real `open` → `executed` lifecycle.
-
-**Session 4 (2026-09-24).** I asked whether P4 fulfils the course rules. The AI found that the
-prototype still asked the user to type a market symbol and crypto symbols, which breaks the rule
-that the user must never have to remember identifiers or codes. It changed every choice to a
-numbered list (`market.go`, `trade.go`, `watchlist.go`), retook every screenshot, one per
-scenario step, and rewrote the P4 pages.
-
-### Test evidence
-
-This is the current prototype (after session 4) on fresh sample data, logged in as `alice`. It
-is a real run, and the same run is on the screenshots of
-[UseCase0004Implementation](UseCase0004Implementation.md):
-
-```
--- Place market buy order --
-
-  #     Symbol    Quote       Last price
-  -----------------------------------------
-  1     ADA       USD           0.453750
-  2     BTC       USD       67140.000000
-  3     DOGE      USD           0.122000
-  4     ETH       USD        3520.000000
-  5     SOL       USD         166.100000
-Market #: 2
-Latest price for BTC/USD = 67140.000000
-Quantity: 0.01
-Order executed: buy 0.0100 BTC @ 67140.000000 (notional 671.4000 USD)
-
-  Symbol        Quantity      Reserved     Available         Avg buy         Current           Value  Unrealised P/L
-  ------------------------------------------------------------------------------------------------------------------
-  BTC             0.0100        0.0000        0.0100    67140.000000    67140.000000        671.4000         +0.0000
-  ETH             0.5000        0.0000        0.5000     3500.000000     3520.000000       1760.0000        +10.0000
-  ------------------------------------------------------------------------------------------------------------------
-  TOTAL                                                                                    2431.4000        +10.0000
-
-  Cash available : 7578.6000 USD
-  Portfolio value: 2431.4000 USD
-  Net worth      : 10010.0000 USD
-```
-
-## Summary of AI involvement
-
-| | Session 1 — 2026-04-21 | Session 2 — 2026-08-06/07 | Session 3 — 2026-09-16 | Session 4 — 2026-09-24 |
-|---|---|---|---|---|
-| **What I brought** | My Go backend (Chi HTTP handlers), draft schema, 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 | The official P4 instructions and the question whether the prototype and pages meet them |
-| **What the AI did** | Rewrote the backend as a CLI covering UC0001–UC0007, corrected the schema, wrote the market bot | Reviewed the code, found and fixed three bugs, improved the holding upsert | Added `holdings.reserved_quantity`, reserve-then-settle sell path, `orders.status` lifecycle, tested it including real concurrency | Audited P4, made every choice a numbered list, retook all screenshots, rewrote the P4 pages |
-| **What I decided** | To delete the frontend, to build a CLI and not a web app, to keep the market simulator | To ask for a code review and not only documentation | To keep reserve and settle in one transaction, because there is no cancel-order use case to free a stuck reservation | To fix the problems found and to have the screenshots taken from real runs |
-
-## Entire AI usage log
-
-**About this log.** Sessions 3 and 4 are still in my local Claude Code history, so their
-prompts are quoted **verbatim** below, typos included. The local history of sessions 1 and 2
-(April and August 2026) is no longer available. For them, the prompts are the excerpts I wrote
-down at the time, the same ones as on [ERModelAIUsage](../P1-ConceptualModel/ERModelAIUsage.md),
-where they were logged because the same conversation produced P1–P4. The AI's responses are
-summarised, because several ran to many pages of code and file edits.
-
----
-
-### Session 1 — 2026-04-21 (Claude Opus 4.7, 1M context)
-
-**Prompt (student, excerpt):**
-> This is my project for my exam Database, go to the docs folder and README.md there you will find all of the documentation, afterwards take a look at the whole project. Delete everything you want add everything you want just make it work and make it fill in or to follow all of the needed instructions. Ask me anything, also tell me what do I need to do
-
-**Response (AI, summarised):**
-- Read `docs/opis.md`, `docs/ep-diagram.md`, `docs/dbdiagram.md`, `server/db/db.sql`,
-  `server/db/schema.sql` and all Go sources.
-- Found errors in my schema: `holdings.crypto_id` declared as a foreign key to both
-  `users(id)` and `crypto(id)` (and the same mistake in `orders` and `transactions`), and
-  `market_candles` referencing a non-existent `markets` table. Also found that `main.go` dropped
-  and recreated all tables on every start, wiping the data.
-- Asked me to decide on team composition, prototype shape (CLI or HTTP), which columns to keep,
-  which use cases to implement, and the documentation language.
-
-**Prompt (student, excerpt):**
-> Read it now, just filled it in, also deleted the frontend we don' need that
-
-**Response (AI, summarised):**
-- Re-read `docs/README.md`, which now contained the course rubric, and explained the AI rules
-  per phase and the deliverables of each phase.
-
-**Prompt (student, excerpt):**
-> do everything that you need to do, and tell me after what do I need to do
-
-**Response (AI, summarised):**
-- Rewrote the backend as the CLI prototype (UC0001–UC0007), with the corrected
-  `schema_creation.sql`, `data_load.sql`, the new `db.go` with `-init` / `-load-data`, the
-  transactional trade flows and the market bot. The details are under *Results in details*
-  above.
-- Ran the prototype end to end against PostgreSQL on port 5433 and checked that a sample buy
-  and the portfolio view gave the expected numbers.
-
----
-
-### Session 2 — 2026-08-06 / 2026-08-07 (Claude Opus 5, 1M context)
-
-**Prompt (student, excerpt):**
-> […] do all of the other Phases till m0.
-> opis.md It's p0 so I will take care of that. Delete anything that we don't need,
-> make all of the phases and terra diagram if you can, and delete anything
-> that we don't need and make a documentation about how to start it.
-
-**Prompt (student, excerpt, follow-up):**
-> Also fix some database things or golang things if you think we can do it better,
-
-**Response (AI, summarised): what changed in the prototype, and why**
-
-**Bugs found and fixed**
-
-1. `server/db/db.go` resolved `../.env` and `db/schema_creation.sql` relative to the working
-   directory, so they only worked when the program was started from inside `server/`.
-   Following the documented instructions (build from the repository root and run
-   `./eduberza -init`) failed with `password authentication failed for user "postgres"`,
-   because `.env` was never found and the defaults were used. The two SQL scripts are now
-   compiled into the binary with `go:embed`, and `.env` is searched for in the working directory
-   and every parent. Real environment variables now take precedence over the file.
-2. `prompt()` in `server/cli.go` ignored the error from `ReadString`. At end of input (Ctrl-D,
-   or a scripted run) it returned an empty string forever, and the menu loop kept printing
-   "Unknown option." without end. It now exits cleanly.
-3. On the sell path, `trade.go` checked `err == sql.ErrNoRows || held < qty` before checking
-   for other errors, so any scan failure was reported as "Insufficient holding". The error
-   check now comes first.
-
-**Improvements**
-
-4. The holding upsert was a read-modify-write in Go. It is now a single
-   `INSERT … ON CONFLICT (user_id, crypto_id) DO UPDATE`, so PostgreSQL recomputes the average
-   in `numeric` arithmetic, relying on the unique constraint of the relational design.
-5. `TerraER3.11.jar` was removed from the repository and `.gitignore` now excludes `*.jar`,
-   because P4 requires third-party executables to be downloaded, not committed. `.env` is
-   excluded too, and `.env.example` was added in its place.
-6. `image.png`, a TradingView screenshot, was deleted, because the project has no licence to
-   publish it.
-
-**Verification.** All seven use cases were run against PostgreSQL 16, and the four failure
-paths were tested. After a rejected purchase, the affected user had zero rows in `orders`,
-`transactions` and `holdings`.
-
----
-
-### Session 3 — 2026-09-16 (Claude Sonnet 5)
-
-**Prompt (student, verbatim):**
-> Soo we have a problem here In this scenario we have an edge case where our functionallity doesn't work:
-> Suppose the user owns:
->
-> 2 BTC
->
-> and wants to sell:
->
-> 0.5 BTC at market price
->
-> A sensible procedure is:
->
-> 1. User creates an Order
->
-> Orders gets something like:
->
-> id    user    market    side    type    quantity    status
-> O1    Alice    BTC/USD    sell    market    0.5    open
->
-> At this point, no trade has necessarily happened yet.
->
-> 2. Reserve the crypto
->
-> This is where your current model has a gap.
->
-> 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
->
-> So Alice might go from:
->
-> quantity = 2.0 BTC
-> reserved_quantity = 0
->
-> to:
->
-> quantity = 2.0 BTC
-> reserved_quantity = 0.5 BTC
->
-> with:
->
-> available BTC = quantity - reserved_quantity = 1.5 BTC
->
-> The important point is that you generally don't actually remove the 0.5 BTC from her position yet. You reserve it so she cannot submit another sell order for the same BTC.
->
-> 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.
-
-The other prompts of that day were about the P1 diagram (logged on
-[ERModelAIUsage](../P1-ConceptualModel/ERModelAIUsage.md)) and about P5 and P6. None of them
-changed the P4 prototype.
-
-**Response (AI, summarised)**
-
-The gap: `trade.go`'s sell path checked `held < qty` directly against `holdings.quantity`.
-Nothing distinguished "owned" from "owned, but already promised to an order". This was safe only
-because the whole sell runs in one transaction with a `SELECT … FOR UPDATE` lock. It would stop
-being safe the moment an order could stay `open` across transactions.
-
-What changed:
-
-1. `holdings.reserved_quantity numeric(20,4) NOT NULL DEFAULT 0 CHECK (reserved_quantity >= 0 AND reserved_quantity <= quantity)`
-   was added to `schema_creation.sql`.
-2. `v_portfolio` gained `reserved_quantity` and the derived `available_quantity`.
-3. The sell path in `trade.go` now locks the holding, computes
-   `available := quantity - reserved_quantity`, rejects the order if `available < qty`,
-   reserves (`reserved_quantity += qty`), then settles (`quantity -= qty; reserved_quantity -= qty`).
-   All of this stays inside the same transaction.
-4. Both branches of `PlaceOrder` insert the order as `status='open'` and set
-   `status='executed', executed_at=now()` at the end.
-5. `portfolio.go` shows new `Reserved` and `Available` columns.
-6. The sell error message became
-   `"Insufficient holding: trying to sell X, available Y (of Z held, W reserved)"`.
-
-**Test evidence** (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"
-```
-
-(At that time the sell path still asked for a symbol, so a user with no holding could reach the
-check. Since session 4, such a user is never offered anything to sell.)
-
-**What I decided:** to keep reserve and settle inside a single transaction. Splitting them into
-two commits, so that an order really sits `open` and reserved in between, is what a real
-limit-order matcher will need. Building it now would add a way for an order to get stuck without
-a way to cancel it.
-
----
-
-### Session 4 — 2026-09-24 (Claude Opus 5.5, 1M context)
-
-**Prompt (student, verbatim):**
-> Does the P4 documentation, fullfill this:
->
-> *[pasted: the complete official text "Instructions on Phase P4: First Application Prototype"
-> from the course site — the prototype requirements, the rule that the user must never have to
-> remember identifiers or codes, the PrototypeImplementation / UseCaseXXXXImplementation /
-> BuildInstructions templates, the source-code rules and the AI Use rules with the
-> PrototypeImplementationAIUsage template]*
->
-> is this correct?
-> Or do we need to change anything?
-
-**Response (AI, summarised):** an audit of the prototype and of the P4 pages against each
-requirement. The main findings:
-- The prototype asked the user to type a market symbol (`Market symbol (e.g. BTC):`) and, in the
-  watchlist, a crypto symbol. That breaks the rule that every choice must be picked from a list.
-- The use-case implementation pages had one screenshot per use case, not one per step, and did
-  not show the SQL at every step.
-- `PrototypeImplementation.md` was written in Trac markup inside a Markdown file. It did not
-  follow the template, and it said identifiers were never typed, which was not accurate.
-- `BuildInstructions` had wrong details: the `-init` message, the order of the history, an
-  outdated ER diagram path, an unpinned PostgreSQL version described as 16, and a concurrency
-  test with no steps to prepare it.
-- This page listed only one model, still had "Student action required" notes, and had no
-  line-by-line log.
-
-**Prompt (student, verbatim):**
-> YEs you can start fixing it, and can you make the screenshots yourself?
-
-**Response (AI, summarised):**
-- **Code** (`market.go`, `trade.go`, `watchlist.go`):
-  - `ListMarkets` now returns the numbered list it prints. `ChooseMarket` asks for `Market #:`
-    and takes the market by its position in that list.
-  - New `ChooseHolding` for the sell path. It lists only the user's holdings that still have a
-    free quantity, with `Held` and `Free to sell`, and asks for `Holding #:`.
-  - Watchlist *Add* lists, numbered, the cryptos not yet on the watchlist. *Remove* lists the ones
-    on it.
-  - A shared `pickNumber` refuses anything outside `1…n` with
-    `Invalid choice, enter a number from 1 to N.`
-  - The SQL that looked a crypto up by symbol (`WHERE upper(c.symbol) = upper($1)`) is gone. The
-    chosen row's id is used directly.
-- **Screenshots:** all of them were retaken from real CLI sessions, driven in a pseudo-terminal
-  against freshly initialised sample data. There is one screenshot per scenario step, including
-  the alternate flows (invalid e-mail, duplicate user, wrong password, invalid amount,
-  insufficient funds, insufficient holding, invalid list choice). This gave 30 screenshots,
-  which replace the old 8.
-- **Documentation:**
-  - Rewrote the seven UseCase000XImplementation pages, with the SQL of each step quoted literally
-    from the code.
-  - Rewrote [PrototypeImplementation](PrototypeImplementation.md) (real Markdown, template
-    order, accurate description of the choices), [BuildInstructions](BuildInstructions.md) (the
-    corrections above, a mini-guide of every menu item, and a smoke test re-run on 2026-09-24)
-    and this page.
-  - Made small matching edits to the P3 use cases UC0004, UC0005 and UC0007, so that the "system
-    lists …, user picks …" steps match the prototype.
-  - Wrote the wiki versions of the P4 pages for the faculty site.
-
-**What I decided:** to accept the numbered-list change, because it is what the P4 rule asks for.
-I also decided to have the screenshots made from real runs instead of editing the old ones.
Index: cs/P4-Prototype/UseCase0001Implementation.md
===================================================================
--- docs/P4-Prototype/UseCase0001Implementation.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,102 +1,0 @@
-# Use-case 0001 Implementation - Register new account
-
-**Initiating actor:** Visitor
-
-**Other actors:** —
-
-A new person creates an account on EduBerza so that they can later log in as a
-Trader ([UseCase0002](UseCase0002Implementation.md)). The Visitor enters a
-username, an e-mail address, a full name and a password. The system validates the
-input (required fields, an `@` in the e-mail, a password of at least 6 characters),
-refuses a username or e-mail that is already registered, and stores only a SHA-256
-hash of the password, never the password itself. A new account starts with a cash
-balance of 0 USD; money is added later with a deposit
-([UseCase0003](UseCase0003Implementation.md)).
-
-Original use-case description (P3): [UseCase0001](../P3-UseCaseModel/UseCase0001.md).
-Implementation: [`server/auth.go`](../../server/auth.go), function `Register`
-(the password hash is computed by `hashPassword` in the same file).
-
-## Scenario
-
-1. **Visitor** chooses `[1] Register` in the anonymous menu (types `1`).
-2. **System** prints `-- Register --` and asks, one prompt after another, for
-   `Username:`, `Email:`, `Full name:` and `Password (min 6 chars):`.
-
-   The screenshot shows steps 1–2: option `1` is chosen and the first prompt
-   (`Username:`) is waiting for input.
-
-   ![UC0001 steps 1-2: Visitor chooses Register, system asks for the data](screenshots/uc0001_1_register.png)
-
-3. **Visitor** enters the values: `marko`, `marko@example.com`, `Marko Markovski`,
-   `secret1`.
-4. **System** validates the input in Go, without accessing the database:
-   - username, e-mail and password must be non-empty, otherwise it prints
-     `Username, email and password are required.` and the scenario ends;
-   - the e-mail must contain `@` (see alternate flow 3a);
-   - the password must be at least 6 characters long, otherwise it prints
-     `Password must be at least 6 characters.` and the scenario ends.
-5. **System** checks whether the username or the e-mail already exists
-   (`$1` = username, `$2` = e-mail):
-
-   ```sql
-   SELECT EXISTS(SELECT 1 FROM users WHERE username = $1 OR email = $2)
-   ```
-
-   If the result is `true`, alternate flow 5a applies.
-6. **System** creates the account (`$1` = username, `$2` = e-mail, `$3` = full name,
-   `$4` = password hash). The hash is computed in Go by `hashPassword` as the
-   hex-encoded SHA-256 of the password — the same value that P3's
-   `encode(digest($4, 'sha256'), 'hex')` would produce in SQL. Hashing on the Go side
-   keeps it identical to the check done at login (SQL as in the code, only the Go
-   source indentation removed):
-
-   ```sql
-   INSERT INTO users (username, email, full_name, password_hash, available_balance)
-   VALUES ($1, $2, $3, $4, 0)
-   ```
-
-7. **System** prints `Account created. You can now log in.` and returns to the
-   anonymous menu; the Visitor can continue with
-   [UseCase0002](UseCase0002Implementation.md).
-
-   The screenshot shows steps 3–7 of the successful attempt (bottom half): the
-   entered values, the confirmation and the anonymous menu again. The top half is the
-   earlier rejected attempt from alternate flow 3a.
-
-   ![UC0001 steps 3-7: valid data, account created](screenshots/uc0001_3_7_created.png)
-
-All statements are run on the `project` schema: the connection sets
-`search_path=project,public` (`server/db/db.go`), so `users` means `project.users`.
-
-### Alternate flow 3a — invalid e-mail
-
-In step 3 the Visitor entered `marko.example.com` (no `@`). The check in step 4
-fails, the system prints `Invalid email.` and no SQL statement is executed. In the
-prototype the system then shows the anonymous menu again and the Visitor chooses
-`[1] Register` once more, which returns the scenario to step 2.
-
-![UC0001 alternate flow 3a: invalid e-mail](screenshots/uc0001_3a_invalid_email.png)
-
-### Alternate flow 5a — duplicate username or e-mail
-
-After `marko` has been created, the Visitor tries to register again with username
-`marko`, e-mail `other@example.com`, full name `Marko Two`, password `secret2`. The
-query from step 5 (`$1` = `marko`, `$2` = `other@example.com`) returns `true`
-because the username is taken, so the system prints
-`Username or email already taken.`, does not run the `INSERT`, and the scenario
-ends in the anonymous menu.
-
-![UC0001 alternate flow 5a: username already taken](screenshots/uc0001_5a_duplicate.png)
-
-## How to reproduce
-
-```sh
-./eduberza -init      # optional: reset to a known state
-./eduberza
-# [1] Register: marko / marko.example.com / Marko Markovski / secret1  -> Invalid email.
-# [1] Register: marko / marko@example.com / Marko Markovski / secret1  -> Account created.
-# [1] Register: marko / other@example.com / Marko Two / secret2        -> Username or email already taken.
-```
-
-All three screenshots come from one real run of exactly these inputs.
Index: cs/P4-Prototype/UseCase0002Implementation.md
===================================================================
--- docs/P4-Prototype/UseCase0002Implementation.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,93 +1,0 @@
-# Use-case 0002 Implementation - Log in
-
-**Initiating actor:** Visitor
-
-**Other actors:** —
-
-A registered user authenticates with a username and password so that the system
-treats all following actions as actions of that Trader. The system looks the user up
-by username and compares the stored password hash with the SHA-256 hash of the
-entered password. An unknown username and a wrong password give the same answer,
-`Invalid credentials.`, so the system does not reveal which usernames exist. After a
-successful login the user's id and username are kept in the in-process session and
-the authenticated (Trader) menu is shown, from which all other Trader use-cases
-start.
-
-Original use-case description (P3): [UseCase0002](../P3-UseCaseModel/UseCase0002.md).
-Implementation: [`server/auth.go`](../../server/auth.go), functions `Login` and
-`authenticate` (password hash by `hashPassword`).
-
-## Scenario
-
-1. **Visitor** chooses `[2] Login` in the anonymous menu (types `2`).
-2. **System** prints `-- Login --` and asks for `Username:` and then `Password:`.
-
-   The screenshot shows steps 1–2: option `2` is chosen and the `Username:` prompt
-   is waiting for input.
-
-   ![UC0002 steps 1-2: Visitor chooses Login, system asks for credentials](screenshots/uc0002_1_login.png)
-
-3. **Visitor** enters the username and the password. (If either is empty, the
-   system prints `Username and password are required.` without accessing the
-   database.)
-4. **System** looks up the user (`$1` = entered username):
-
-   ```sql
-   SELECT id, password_hash FROM users WHERE username = $1
-   ```
-
-5. If no row is returned (`sql.ErrNoRows` in Go), the **System** responds
-   `Invalid credentials.` and the scenario ends.
-6. If a row is returned, the **System** compares the returned `password_hash` with
-   `hashPassword(entered password)` (hex-encoded SHA-256, computed in Go). On a
-   mismatch it responds `Invalid credentials.` and the scenario ends.
-
-   The screenshot shows this failure path with an existing user and a wrong
-   password: `alice` / `wrongpass`. The query from step 4 finds alice's row, the
-   hash comparison of step 6 fails, and the system prints `Invalid credentials.` and
-   returns to the anonymous menu. (An unknown username — step 5 — prints exactly the
-   same message.)
-
-   ![UC0002 steps 5-6: wrong password, Invalid credentials](screenshots/uc0002_5_6_invalid.png)
-
-7. On a match, the **System** stores the returned `id` and the username in the
-   session (`s.UserID`, `s.Username`), prints `Login successful.` and displays the
-   authenticated menu headed `--- Logged in as alice ---`.
-
-   The screenshot shows steps 3–7 of the second, successful attempt with the seed
-   credentials `alice` / `test123` (the first, rejected attempt is still visible at
-   the top of the window).
-
-   ![UC0002 steps 3-7: correct credentials, authenticated menu](screenshots/uc0002_7_success.png)
-
-The query runs on the `project` schema (the connection sets
-`search_path=project,public`), so `users` means `project.users`.
-
-### Alternate flow 4a (P3) — lookup combined with the live balance
-
-P3 describes an optional variant that checks the password in SQL and returns the
-balances in the same query. The P4 prototype does **not** use it: login always uses
-the query from step 4 with the hash comparison in Go, and the balances are read
-separately when the Trader asks for them (`[1] View balance`, see
-[UseCase0003](UseCase0003Implementation.md)).
-
-## Seed credentials
-
-State after `./eduberza -init` (`server/db/data_load.sql`):
-
-| Username  | Password  | Available balance | Invested balance | Holdings |
-|-----------|-----------|------------------:|-----------------:|----------|
-| `alice`   | `test123` | 8250.00 USD       | 1750.00 USD      | 0.5 ETH  |
-| `bob`     | `test123` | 5000.00 USD       | 0.00 USD         | —        |
-| `charlie` | `test123` | 2500.00 USD       | 0.00 USD         | —        |
-
-## How to reproduce
-
-```sh
-./eduberza -init
-./eduberza
-# [2] Login: alice / wrongpass  -> Invalid credentials.
-# [2] Login: alice / test123    -> Login successful.  (authenticated menu)
-```
-
-The screenshots come from one real run of exactly these inputs.
Index: cs/P4-Prototype/UseCase0003Implementation.md
===================================================================
--- docs/P4-Prototype/UseCase0003Implementation.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,112 +1,0 @@
-# Use-case 0003 Implementation - Deposit virtual funds
-
-**Initiating actor:** Trader
-
-**Other actors:** —
-
-A logged-in Trader tops up their virtual cash balance in USD. This is a
-simulation-only operation: no real money changes hands, the amount is simply added to
-the Trader's `available_balance`. Every deposit is also recorded in the ledger
-(`transactions`) so that it appears in the transaction history
-([UseCase0006](UseCase0006Implementation.md)). The operation writes to two tables —
-the user row and the ledger — inside a single database transaction, so either both
-changes are stored or neither is. Non-numeric, zero or negative amounts are rejected
-before the database is touched.
-
-Original use-case description (P3): [UseCase0003](../P3-UseCaseModel/UseCase0003.md).
-Implementation: [`server/account.go`](../../server/account.go), function `Deposit`;
-the verification uses `ShowBalance` from the same file.
-
-Precondition: the Trader is logged in ([UseCase0002](UseCase0002Implementation.md));
-in the run below as `alice`, who starts from the seed state (available 8250.00 USD,
-invested 1750.00 USD).
-
-## Scenario
-
-1. **Trader** chooses `[2] Deposit virtual funds` in the authenticated menu
-   (types `2`).
-2. **System** prints `-- Deposit virtual funds --` and asks `Amount (USD):`.
-
-   The screenshot shows steps 1–2: the login as alice, the authenticated menu, the
-   choice `2` and the amount prompt waiting for input.
-
-   ![UC0003 steps 1-2: Trader chooses Deposit, system asks for the amount](screenshots/uc0003_1_2_deposit.png)
-
-3. **Trader** enters an amount: `500`.
-4. **System** validates the input in Go, without accessing the database: the text
-   must parse as a number (`strconv.ParseFloat`) and be greater than 0 (see
-   alternate flow 4a).
-5. **System** opens one database transaction (`db.DB.Begin()`), increments the
-   balance, writes the ledger row and commits. Both statements run in this single
-   transaction; if either fails, the deferred `tx.Rollback()` undoes everything.
-   `BEGIN` and `COMMIT` are issued by Go's `Begin()` / `Commit()`; the two
-   statements are sent exactly as in the code:
-
-   ```sql
-   BEGIN;
-
-   UPDATE users
-       SET available_balance = available_balance + $1,
-           updated_at        = now()
-     WHERE id = $2;
-
-   INSERT INTO transactions (user_id, type, amount, currency, description)
-   VALUES ($1, 'deposit', $2, 'USD', 'Virtual deposit');
-
-   COMMIT;
-   ```
-
-   Parameters: placeholders are numbered per statement. In the `UPDATE`, `$1` is the
-   amount (`500`) and `$2` the user id (`amt, s.UserID`); in the `INSERT` it is the
-   other way round, `$1` is the user id and `$2` the amount (`s.UserID, amt`),
-   matching the column order.
-6. **System** confirms `Deposited 500.0000 USD.` and returns to the authenticated
-   menu.
-
-   The screenshot shows steps 3–6: the entered amount `500`, the confirmation and the
-   authenticated menu again.
-
-   ![UC0003 steps 3-6: amount deposited](screenshots/uc0003_3_6_deposited.png)
-
-The statements run on the `project` schema (the connection sets
-`search_path=project,public`), so `users` and `transactions` mean `project.users`
-and `project.transactions`.
-
-### Alternate flow 4a — invalid amount
-
-If the amount is not a number, or is zero or negative, the system prints
-`Invalid amount.`; no transaction is started and nothing is written. In the run the
-Trader first entered `-50`. In the prototype the system then shows the
-authenticated menu again and the Trader chooses `[2] Deposit virtual funds` once
-more, which returns the scenario to step 2.
-
-![UC0003 alternate flow 4a: invalid amount](screenshots/uc0003_4a_invalid.png)
-
-## Verification
-
-Right after the deposit the Trader chooses `[1] View balance` (function
-`ShowBalance`), which runs (`$1` = user id):
-
-```sql
-SELECT available_balance, invested_balance FROM users WHERE id = $1
-```
-
-It prints `Available: 8750.0000 USD`, `Invested : 1750.0000 USD`,
-`Total    : 10500.0000 USD`. The available balance grew from the seed value 8250.00
-by exactly the 500.00 deposited (the rejected `-50` changed nothing), and
-`invested_balance` is untouched.
-
-![UC0003 verification: balance after the deposit](screenshots/uc0003_verify_balance.png)
-
-## How to reproduce
-
-```sh
-./eduberza -init
-./eduberza
-# [2] Login: alice / test123
-# [2] Deposit virtual funds: -50   -> Invalid amount.
-# [2] Deposit virtual funds: 500   -> Deposited 500.0000 USD.
-# [1] View balance                 -> Available: 8750.0000 USD
-```
-
-All four screenshots come from one real run of exactly these inputs.
Index: cs/P4-Prototype/UseCase0004Implementation.md
===================================================================
--- docs/P4-Prototype/UseCase0004Implementation.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,169 +1,0 @@
-# Use-case 0004 Implementation - Place market BUY order
-
-**Initiating actor:** Trader
-
-**Other actors:** Market Simulator (indirect — supplies the current price via `market_trades`).
-
-A logged-in Trader buys a crypto asset at the current market price. The Trader never
-types a symbol or an identifier: the system lists the active markets with their last
-price, numbered, and the Trader picks one by its number and then enters only the
-quantity. The system checks that the Trader has enough available cash for
-quantity × price and then, in one database transaction, records the order, moves the
-cash from available to invested, adds the crypto to the Trader's holding (recomputing
-the weighted-average entry price), writes a ledger entry and a market trade, and marks
-the order executed. The operation touches five tables (`orders`, `users`, `holdings`,
-`transactions`, `market_trades`) and either all of it succeeds or all of it is rolled
-back.
-
-Original use-case description (P3): [UseCase0004](../P3-UseCaseModel/UseCase0004.md).
-Implementation: [`server/trade.go`](../../server/trade.go), function
-`PlaceOrder(s, "buy")` (with `upsertHoldingOnBuy` in the same file), which calls
-`ChooseMarket`, `ListMarkets`, `pickNumber` and `LatestPrice` from
-[`server/market.go`](../../server/market.go).
-
-All statements run on the `project` schema: the connection sets
-`search_path=project,public` (`server/db/db.go`), so `orders` means `project.orders`.
-The SQL below is copied from the Go code; only the Go source indentation is removed,
-a `;` is added after each statement of the transaction, and `--` comments say what
-each `$n` placeholder is bound to.
-
-The run shown is user `alice` on the seed data (available 8250.00 USD, holding
-0.5 ETH bought at 3500), buying 0.01 BTC.
-
-## Scenario
-
-1. **Trader** chooses `[4] Place market BUY order` in the authenticated menu (types `4`).
-2. **System** prints `-- Place market buy order --` and lists all active markets,
-   numbered, with their last price (`ListMarkets`, called by `ChooseMarket`):
-
-   ```sql
-   SELECT m.id, c.id, c.symbol, m.quote_currency,
-          COALESCE(lp.price, 0) AS price
-     FROM markets m
-     JOIN crypto  c  ON c.id = m.crypto_id
-     LEFT JOIN v_latest_prices lp ON lp.market_id = m.id
-    WHERE m.is_active = true
-    ORDER BY c.symbol
-   ```
-
-   The rows are printed in this order as `1 ADA`, `2 BTC`, `3 DOGE`, `4 ETH`, `5 SOL`;
-   Go keeps each row's market id and crypto id in memory, so the Trader only ever sees
-   and types the list number. The system then asks `Market #:`.
-
-   ![UC0004 steps 1-2: Trader chooses BUY, system lists the markets](screenshots/uc0004_1_2_markets.png)
-
-3. **Trader** picks the market by its number in the list: `2` (BTC/USD).
-4. **System** takes the market id and crypto id of row 2 from the list (no further
-   lookup by symbol) and reads the latest price of that market (`LatestPrice`;
-   `$1` = the chosen market's id):
-
-   ```sql
-   SELECT price FROM v_latest_prices WHERE market_id = $1
-   ```
-
-   It prints `Latest price for BTC/USD = 67140.000000` and asks `Quantity:`.
-
-   ![UC0004 steps 3-4: Trader picks market #2 (BTC), system shows the price](screenshots/uc0004_3_4_price.png)
-
-5. **Trader** enters the quantity `0.01`.
-6. **System** computes in Go notional = quantity × price = 0.01 × 67140 = 671.40 and
-   passes it to SQL as a parameter. It then runs one database transaction; the
-   statements below are in exactly the order `PlaceOrder` executes them for a buy:
-
-   ```sql
-   BEGIN;
-
-   -- (a) record the order as 'open' — no trade has happened yet.
-   --     $1 = user id, $2 = market id, $3 = side (the Go variable side = 'buy'),
-   --     $4 = quantity (0.01), $5 = price (67140); the returned id is kept in Go.
-   INSERT INTO orders (user_id, market_id, side, type, status, quantity, price)
-    VALUES ($1, $2, $3, 'market', 'open', $4, $5)
-    RETURNING id;
-
-   -- (b) lock the user's row and read the available cash. $1 = user id.
-   --     Go compares it with the notional; if it is smaller -> alternate flow 6a.
-   SELECT available_balance FROM users WHERE id = $1 FOR UPDATE;
-
-   -- (c) move the notional from available to invested cash.
-   --     $1 = notional (671.40), $2 = user id.
-   UPDATE users
-       SET available_balance = available_balance - $1,
-           invested_balance  = invested_balance  + $1,
-           updated_at        = now()
-     WHERE id = $2;
-
-   -- (d) add the crypto to the holding (upsertHoldingOnBuy), recomputing the
-   --     weighted-average entry price in the database. Every SET expression sees
-   --     the pre-update row, so holdings.quantity is still the old quantity.
-   --     $1 = user id, $2 = crypto id, $3 = quantity (0.01), $4 = price (67140).
-   INSERT INTO holdings (user_id, crypto_id, quantity, avg_price, updated_at)
-    VALUES ($1, $2, $3, $4, now())
-    ON CONFLICT (user_id, crypto_id) DO UPDATE
-       SET avg_price  = (holdings.quantity * holdings.avg_price
-                          + EXCLUDED.quantity * EXCLUDED.avg_price)
-                        / (holdings.quantity + EXCLUDED.quantity),
-           quantity   = holdings.quantity + EXCLUDED.quantity,
-           updated_at = now();
-
-   -- (e) ledger entry. $1 = user id, $2 = -notional (-671.40), $3 = order id from (a),
-   --     $4 = description built in Go: 'Market buy 0.0100 BTC @ 67140.000000'.
-   INSERT INTO transactions (user_id, type, amount, currency, related_order, description)
-    VALUES ($1, 'buy', $2, 'USD', $3, $4);
-
-   -- (f) record the resulting market trade.
-   --     $1 = market id, $2 = price, $3 = quantity, $4 = side ('buy').
-   INSERT INTO market_trades (market_id, executed_at, price, quantity, side, source)
-    VALUES ($1, now(), $2, $3, $4, 'user');
-
-   -- (g) settle the order itself — it has now actually been filled. $1 = order id.
-   UPDATE orders SET status = 'executed', executed_at = now() WHERE id = $1;
-
-   COMMIT;
-   ```
-
-   A buy never reserves crypto (only a sell does, see
-   [UseCase0005](UseCase0005Implementation.md)), so `holdings.reserved_quantity` is not
-   touched and stays 0.
-
-7. **System** confirms
-   `Order executed: buy 0.0100 BTC @ 67140.000000 (notional 671.4000 USD)` and shows
-   the authenticated menu again.
-
-   The screenshot shows steps 5–7: the entered quantity, the confirmation and the menu.
-
-   ![UC0004 steps 5-7: quantity entered, order executed](screenshots/uc0004_5_7_executed.png)
-
-### Verification — portfolio after the buy
-
-Right after the buy the Trader chooses `[6] View portfolio`
-([UseCase0006](UseCase0006Implementation.md)). It shows the new holding
-`BTC 0.0100` with average buy price and current price 67140.000000 (value 671.4000),
-the unchanged `ETH 0.5000` (average 3500, current 3520, unrealised P/L +10.0000),
-`Cash available : 7578.6000 USD` (= 8250.00 − 671.40), portfolio value 2431.4000 and
-net worth 10010.0000 USD. The `Reserved` column is 0.0000 on both rows — a buy never
-reserves anything.
-
-![UC0004 verification: portfolio after the buy](screenshots/uc0004_verify_portfolio.png)
-
-### Alternate flow 6a — insufficient funds
-
-User `charlie` (seed data: 2500.00 USD available, no crypto) chooses `[4]`, picks
-market `2` (BTC, 67140.000000) and enters quantity `1`. Go computes the notional
-67140.00. In the transaction, statement (a) inserts the `open` order and statement (b)
-`SELECT available_balance FROM users WHERE id = $1 FOR UPDATE` returns 2500.00, which
-is less than the notional. `PlaceOrder` prints
-`Insufficient funds: need 67140.0000, have 2500.0000` and returns without running
-(c)–(g); the deferred `tx.Rollback()` undoes statement (a), so no order, no ledger
-entry and no balance change is left behind (after this run charlie has no row in
-`orders` and still 2500.00 USD available). The authenticated menu is shown again.
-
-![UC0004 alternate flow: insufficient funds](screenshots/uc0004_6a_insufficient.png)
-
-### Alternate flow 3a — number not in the list
-
-If in step 3 the Trader enters something that is not a number from 1 to the number of
-listed markets, `pickNumber` prints `Invalid choice, enter a number from 1 to 5.`, no
-further SQL is run and the authenticated menu is shown again (the same check is shown
-in [UseCase0007](UseCase0007Implementation.md), alternate flow 12a). Likewise, a
-quantity that is not a positive number in step 5 prints `Invalid quantity.` before any
-transaction is opened.
Index: cs/P4-Prototype/UseCase0005Implementation.md
===================================================================
--- docs/P4-Prototype/UseCase0005Implementation.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,260 +1,0 @@
-# Use-case 0005 Implementation - Place market SELL order
-
-**Initiating actor:** Trader
-
-**Other actors:** Market Simulator (indirect — supplies the current price).
-
-A logged-in Trader sells part or all of a holding at the current market price. The
-Trader never types a symbol: the system lists only the cryptos the Trader holds and can
-still sell (the quantity not already reserved by an open sell order), numbered, with
-how much is held and how much is free, and the Trader picks one by its number and
-enters the quantity. In one database transaction the system records the order,
-reserves the crypto being sold and settles it, credits the proceeds to the Trader's
-available cash while reducing the invested cash by the cost basis, writes a ledger
-entry and a market trade, and marks the order executed. Cost basis is preserved, so
-the realised P/L can be reconstructed from the ledger.
-
-Original use-case description (P3): [UseCase0005](../P3-UseCaseModel/UseCase0005.md).
-Implementation: [`server/trade.go`](../../server/trade.go), function
-`PlaceOrder(s, "sell")`, which calls `ChooseHolding`, `pickNumber` and `LatestPrice`
-from [`server/market.go`](../../server/market.go).
-
-All statements run on the `project` schema (the connection sets
-`search_path=project,public` in `server/db/db.go`). The SQL below is copied from the
-Go code; only the Go source indentation is removed, a `;` is added after each
-statement of the transaction, and `--` comments say what each `$n` placeholder is
-bound to.
-
-The run shown is user `alice` right after the buy of
-[UseCase0004](UseCase0004Implementation.md): 7578.60 USD available, holdings
-0.01 BTC (bought at 67140) and 0.5 ETH (bought at 3500). She sells 0.2 ETH.
-
-## Reserve, then settle
-
-The crypto being sold is **reserved** (`holdings.reserved_quantity`) before it is
-removed from the position, and the sell check is against what is truly still free,
-`quantity - reserved_quantity`, not against the raw `quantity`, which would also count
-crypto already promised to another order. Because only market orders are implemented,
-an order settles in the same transaction it is placed in, so reserve and settle are two
-statements inside one commit; they stay logically distinct so that a future
-limit-order matcher, where an order would stay `open` until a *later* transaction fills
-it, needs a second transaction but no schema change.
-
-## Scenario
-
-1. **Trader** chooses `[5] Place market SELL order` in the authenticated menu (types `5`).
-2. **System** prints `-- Place market sell order --` and lists, numbered, only the
-   cryptos the Trader holds with some quantity still free to sell, with the quantity
-   held, the quantity free to sell and the last price (`ChooseHolding`;
-   `$1` = the logged-in user's id):
-
-   ```sql
-   SELECT m.id, c.id, c.symbol, m.quote_currency,
-          h.quantity, h.quantity - h.reserved_quantity AS free,
-          COALESCE(lp.price, 0) AS price
-     FROM holdings h
-     JOIN crypto  c ON c.id = h.crypto_id
-     JOIN markets m ON m.crypto_id = c.id AND m.is_active = true
-     LEFT JOIN v_latest_prices lp ON lp.market_id = m.id
-    WHERE h.user_id = $1
-      AND h.quantity - h.reserved_quantity > 0
-    ORDER BY c.symbol
-   ```
-
-   For alice it prints `1 BTC USD 0.0100 0.0100 67140.000000` and
-   `2 ETH USD 0.5000 0.5000 3520.000000`, then asks `Holding #:`. Go keeps each row's
-   market id and crypto id in memory; the Trader only types the list number. (If the
-   query returns no row, the system prints `you hold no crypto that is free to sell`
-   and the use-case ends.)
-
-   ![UC0005 steps 1-2: Trader chooses SELL, system lists what they hold](screenshots/uc0005_1_2_holdings.png)
-
-3. **Trader** picks the holding by its number in the list: `2` (ETH).
-4. **System** takes the market id and crypto id of row 2 from the list and reads the
-   latest price of that market (`LatestPrice`; `$1` = the chosen market's id):
-
-   ```sql
-   SELECT price FROM v_latest_prices WHERE market_id = $1
-   ```
-
-   It prints `Latest price for ETH/USD = 3520.000000` and asks `Quantity:`.
-
-   ![UC0005 steps 3-4: Trader picks holding #2 (ETH), system shows the price](screenshots/uc0005_3_4_price.png)
-
-5. **Trader** enters the quantity `0.2`.
-6. **System** computes in Go notional = quantity × price = 0.2 × 3520 = 704.00 and
-   runs one database transaction; the statements are in exactly the order
-   `PlaceOrder` executes them for a sell. After statement (b) Go also computes the
-   cost basis = avg_price × quantity = 3500 × 0.2 = 700.00 from the locked holding row;
-   both values are passed to SQL as parameters.
-
-   ```sql
-   BEGIN;
-
-   -- (a) record the order as 'open' — no trade has happened yet.
-   --     $1 = user id, $2 = market id, $3 = side (the Go variable side = 'sell'),
-   --     $4 = quantity (0.2), $5 = price (3520); the returned id is kept in Go.
-   INSERT INTO orders (user_id, market_id, side, type, status, quantity, price)
-    VALUES ($1, $2, $3, 'market', 'open', $4, $5)
-    RETURNING id;
-
-   -- (b) lock the holding row and read what is held, what is already reserved and
-   --     the average entry price. $1 = user id, $2 = crypto id.
-   --     Go computes available = quantity - reserved_quantity (0.5 - 0 = 0.5);
-   --     if there is no row or available < quantity -> alternate flow 5a.
-   SELECT quantity, reserved_quantity, avg_price FROM holdings
-     WHERE user_id = $1 AND crypto_id = $2 FOR UPDATE;
-
-   -- (c) reserve: committed to this order, not yet removed from the position.
-   --     $1 = quantity (0.2), $2 = user id, $3 = crypto id.
-   UPDATE holdings
-       SET reserved_quantity = reserved_quantity + $1,
-           updated_at        = now()
-     WHERE user_id = $2 AND crypto_id = $3;
-
-   -- (d) settle: a market order fills immediately, so release the reservation and
-   --     remove the asset from the position in one step. Same parameters as (c).
-   UPDATE holdings
-       SET quantity          = quantity - $1,
-           reserved_quantity = reserved_quantity - $1,
-           updated_at        = now()
-     WHERE user_id = $2 AND crypto_id = $3;
-
-   -- (e) credit the proceeds; reduce invested cash by the cost basis.
-   --     $1 = notional (704.00), $2 = cost basis (700.00), $3 = user id.
-   UPDATE users
-       SET available_balance = available_balance + $1,
-           invested_balance  = GREATEST(invested_balance - $2, 0),
-           updated_at        = now()
-     WHERE id = $3;
-
-   -- (f) ledger entry. $1 = user id, $2 = notional (704.00), $3 = order id from (a),
-   --     $4 = description built in Go: 'Market sell 0.2000 ETH @ 3520.000000'.
-   INSERT INTO transactions (user_id, type, amount, currency, related_order, description)
-    VALUES ($1, 'sell', $2, 'USD', $3, $4);
-
-   -- (g) record the resulting market trade.
-   --     $1 = market id, $2 = price, $3 = quantity, $4 = side ('sell').
-   INSERT INTO market_trades (market_id, executed_at, price, quantity, side, source)
-    VALUES ($1, now(), $2, $3, $4, 'user');
-
-   -- (h) settle the order itself — it has now actually been filled. $1 = order id.
-   UPDATE orders SET status = 'executed', executed_at = now() WHERE id = $1;
-
-   COMMIT;
-   ```
-
-7. **System** confirms
-   `Order executed: sell 0.2000 ETH @ 3520.000000 (notional 704.0000 USD)` and shows
-   the authenticated menu again.
-
-   The screenshot shows steps 5–7: the entered quantity, the confirmation and the menu.
-
-   ![UC0005 steps 5-7: quantity entered, order executed](screenshots/uc0005_5_7_executed.png)
-
-After this run the database holds for alice: ETH `quantity` 0.3000 with
-`reserved_quantity` 0.0000; `available_balance` 8282.60 (= 7578.60 + 704.00) and
-`invested_balance` 1721.40 (= 2421.40 − 700.00); a `sell` row in `transactions` with
-amount 704.0000 and description `Market sell 0.2000 ETH @ 3520.000000`; and the order
-with status `executed`. The realised P/L of this sell is notional − cost basis =
-704.00 − 700.00 = +4.00 USD.
-
-### Alternate flow 5a — insufficient holding
-
-Right after the sell above, alice chooses `[5]` again. The list from step 2 now shows
-`2 ETH USD 0.3000 0.3000 3520.000000`. She picks `2` (ETH) and enters quantity `5`.
-In the transaction, statement (a) inserts the `open` order and statement (b) returns
-quantity 0.3000 and reserved_quantity 0.0000, so available = 0.3 < 5. `PlaceOrder`
-prints
-
-```
-Insufficient holding: trying to sell 5.0000, available 0.3000 (of 0.3000 held, 0.0000 reserved)
-```
-
-and returns without running (c)–(h); the deferred `tx.Rollback()` undoes statement (a)
-as well, so no order, no reservation and no ledger entry is left behind. The
-authenticated menu is shown again. The same message is printed if the holding row no
-longer exists (for example because it was sold out from another session after the
-list was shown).
-
-![UC0005 alternate flow 5a: selling more than is held](screenshots/uc0005_5a_insufficient.png)
-
-## Reserve and settle, step by step
-
-The CLI reserves and settles inside one transaction, so `reserved_quantity` is never
-nonzero *outside* a transaction. The intermediate state is shown by running statements
-(c) and (d) by hand in one `psql` transaction (which sees its own uncommitted
-writes) against alice's ETH holding after the scenario above (0.3 ETH), for a sell of
-0.1, and rolling back at the end so nothing is changed. Literal values replace the
-`$n` parameters; `:alice` and `:eth` are psql variables for
-`(SELECT id FROM users WHERE username = 'alice')` and
-`(SELECT id FROM crypto WHERE symbol = 'ETH')`:
-
-```sql
-BEGIN;
-SELECT quantity, reserved_quantity, quantity - reserved_quantity AS available
-  FROM holdings WHERE user_id = :alice AND crypto_id = :eth;
---  quantity | reserved_quantity | available
--- ----------+-------------------+-----------
---    0.3000 |            0.0000 |    0.3000
-
--- (c) reserve 0.1: the order is placed, no trade has happened yet
-UPDATE holdings SET reserved_quantity = reserved_quantity + 0.1, updated_at = now()
- WHERE user_id = :alice AND crypto_id = :eth;
-SELECT quantity, reserved_quantity, quantity - reserved_quantity AS available
-  FROM holdings WHERE user_id = :alice AND crypto_id = :eth;
---  quantity | reserved_quantity | available
--- ----------+-------------------+-----------
---    0.3000 |            0.1000 |    0.2000
-
--- (d) settle: the reservation is released and the asset removed
-UPDATE holdings SET quantity = quantity - 0.1, reserved_quantity = reserved_quantity - 0.1, updated_at = now()
- WHERE user_id = :alice AND crypto_id = :eth;
-SELECT quantity, reserved_quantity, quantity - reserved_quantity AS available
-  FROM holdings WHERE user_id = :alice AND crypto_id = :eth;
---  quantity | reserved_quantity | available
--- ----------+-------------------+-----------
---    0.2000 |            0.0000 |    0.2000
-ROLLBACK;
-```
-
-The middle state is what every other connection would see for as long as an order
-stayed `open` once limit orders exist: 0.1 ETH still owned but no longer free to sell.
-
-## Two concurrent sells
-
-A Trader must not be able to sell the same units twice from two sessions at once. Both
-sessions may have listed the holding as free (step 2 runs outside the transaction), so
-the protection is statement (b): `SELECT ... FOR UPDATE` locks the holding row, and a
-second transaction that reaches (b) waits until the first one commits, then reads the
-already reduced `quantity` before deciding.
-
-This was checked with two `psql` sessions running statements (b)–(d) against alice's
-0.3 ETH. Session A locked the row, reserved and settled 0.2 ETH and committed after a
-3-second pause; session B asked for the lock one second after A had taken it:
-
-```
-A: SELECT ... FOR UPDATE  ->  quantity 0.3000, reserved_quantity 0.0000
-A: reserve 0.2, settle 0.2, pg_sleep(3)
-B: 11:43:54  SELECT ... FOR UPDATE   -- blocks, A holds the row lock
-A: 11:43:56  COMMIT
-B: 11:43:56  lock granted  ->  quantity 0.1000, reserved_quantity 0.0000
-```
-
-Session B was blocked for the two seconds until A committed and then saw only
-0.1 ETH, so a second 0.2 ETH sell in B takes alternate flow 5a
-(`available 0.1000`) instead of selling units that no longer exist. (B was rolled
-back and alice's holding was restored to 0.3 ETH after the check.)
-
-## The constraint holds even if the application code did not
-
-`schema_creation.sql` declares
-`CHECK (reserved_quantity >= 0 AND reserved_quantity <= quantity)` on
-`holdings.reserved_quantity`, so an inconsistent reservation is impossible at the
-database level, independently of `trade.go` (run inside a transaction that was
-rolled back):
-
-```
-UPDATE holdings SET reserved_quantity = quantity + 1 WHERE user_id = :alice AND crypto_id = :eth;
-ERROR:  new row for relation "holdings" violates check constraint "holdings_check"
-```
Index: cs/P4-Prototype/UseCase0006Implementation.md
===================================================================
--- docs/P4-Prototype/UseCase0006Implementation.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,147 +1,0 @@
-# Use-case 0006 Implementation - View portfolio and transaction history
-
-**Initiating actor:** Trader
-
-**Other actors:** —
-
-A logged-in Trader inspects the current state of their account. The portfolio view
-lists every cryptocurrency the Trader holds with the quantity (also split into the
-part reserved by open sell orders and the part that is free to sell), the average
-buy price, the current market price, the market value and the unrealised
-profit/loss, followed by a totals row and a cash summary (cash available, portfolio
-value, net worth). The transaction history lists the Trader's last 20 ledger
-entries — deposits, buys and sells — newest first. Both are read-only: nothing in
-the database is changed.
-
-Original use-case description (P3): [UseCase0006](../P3-UseCaseModel/UseCase0006.md).
-Implementation: [`server/portfolio.go`](../../server/portfolio.go), function
-`ShowPortfolio`, and [`server/account.go`](../../server/account.go), function
-`ShowTransactions`.
-
-Precondition: the Trader is logged in ([UseCase0002](UseCase0002Implementation.md)).
-The run below is alice's, after she bought 0.01 BTC
-([UseCase0004](UseCase0004Implementation.md)) and sold 0.2 ETH
-([UseCase0005](UseCase0005Implementation.md)) on top of the seed data (0.5 ETH,
-8250.00 USD cash).
-
-## Scenario
-
-### Portfolio
-
-1. **Trader** chooses `[6] View portfolio` in the authenticated menu (types `6`).
-   The menu is the one shown in [UseCase0002](UseCase0002Implementation.md), step 7.
-2. **System** queries the `v_portfolio` view (`$1` = user id):
-
-   ```sql
-   SELECT symbol,
-          quantity,
-          COALESCE(reserved_quantity, 0),
-          COALESCE(available_quantity, quantity),
-          COALESCE(avg_price, 0),
-          COALESCE(current_price, 0),
-          COALESCE(market_value, 0),
-          COALESCE(unrealized_pnl, 0)
-     FROM v_portfolio
-    WHERE user_id = $1 AND quantity > 0
-    ORDER BY symbol
-   ```
-
-3. **System** displays the rows and a `TOTAL` row (sums of the value and P/L columns,
-   computed in Go), then reads the cash balance for the summary (`$1` = user id):
-
-   ```sql
-   SELECT available_balance, invested_balance FROM users WHERE id = $1
-   ```
-
-   and prints `Cash available` (= `available_balance`), `Portfolio value`
-   (= the total market value) and `Net worth` (= their sum).
-
-   The screenshot shows the result of steps 2–3. The table is wider than the
-   terminal window, so each long line wraps and the header row has scrolled out of
-   the top of the window; the complete output of this run is reproduced below it.
-
-   ![UC0006 portfolio: holdings, P/L and cash summary](screenshots/uc0006_portfolio.png)
-
-   ```
-     Symbol        Quantity      Reserved     Available         Avg buy         Current           Value  Unrealised P/L
-     ------------------------------------------------------------------------------------------------------------------
-     BTC             0.0100        0.0000        0.0100    67140.000000    67140.000000        671.4000         +0.0000
-     ETH             0.3000        0.0000        0.3000     3500.000000     3520.000000       1056.0000         +6.0000
-     ------------------------------------------------------------------------------------------------------------------
-     TOTAL                                                                                    1727.4000         +6.0000
-
-     Cash available : 8282.6000 USD
-     Portfolio value: 1727.4000 USD
-     Net worth      : 10010.0000 USD
-   ```
-
-   Checking the figures: ETH is 0.5 − 0.2 = 0.3 at an average buy price of 3500 and
-   a current price of 3520, so value 1056.00 and P/L 0.3 × 20 = +6.00; BTC was just
-   bought at the current price 67140, so its P/L is 0. Cash is
-   8250.00 − 671.40 (buy) + 704.00 (sell) = 8282.60. `Reserved` is 0.0000 for both
-   because the market orders executed immediately — a quantity is reserved only
-   while a sell order is still open.
-
-### Transaction history
-
-1. **Trader** chooses `[7] View transaction history` in the authenticated menu
-   (types `7`).
-2. **System** queries the last 20 ledger entries of the Trader (`$1` = user id) and
-   prints them, newest first (the time is shown as the first 19 characters of
-   `created_at`):
-
-   ```sql
-   SELECT created_at, type, amount, currency, COALESCE(description, '')
-     FROM transactions
-    WHERE user_id = $1
-    ORDER BY created_at DESC
-    LIMIT 20
-   ```
-
-   The screenshot shows steps 1–2: the choice `7` and the four ledger rows of this
-   run — the sell of 0.2 ETH (+704.0000 USD), the buy of 0.01 BTC (−671.4000 USD),
-   and the two seed rows, the initial deposit of 10000.0000 USD and the seed buy of
-   0.5 ETH (−1750.0000 USD). Buys are stored with a negative amount, deposits and
-   sells with a positive one. The two seed rows were inserted by `data_load.sql` in
-   one statement and have the same `created_at`, so their relative order is not
-   determined by the `ORDER BY`.
-
-   ![UC0006 transaction history: last ledger entries](screenshots/uc0006_history.png)
-
-All statements run on the `project` schema (the connection sets
-`search_path=project,public`).
-
-### Reference — how `v_portfolio` is defined
-
-From `server/db/schema_creation.sql`:
-
-```sql
-CREATE OR REPLACE VIEW project.v_portfolio AS
-SELECT h.user_id,
-       c.symbol,
-       h.quantity,
-       h.reserved_quantity,
-       (h.quantity - h.reserved_quantity) AS available_quantity,
-       h.avg_price,
-       lp.price                           AS current_price,
-       (h.quantity * lp.price)            AS market_value,
-       (h.quantity * (lp.price - h.avg_price)) AS unrealized_pnl
-FROM   project.holdings h
-JOIN   project.crypto   c ON c.id = h.crypto_id
-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;
-```
-
-## How to reproduce
-
-```sh
-./eduberza -init
-./eduberza
-# [2] Login: alice / test123
-# [4] buy 0.01 BTC, [5] sell 0.2 ETH   (UseCase0004 / UseCase0005)
-# [6] View portfolio
-# [7] View transaction history
-```
-
-Both screenshots come from one real run (portfolio and history taken right after
-the buy and sell runs).
Index: cs/P4-Prototype/UseCase0007Implementation.md
===================================================================
--- docs/P4-Prototype/UseCase0007Implementation.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,160 +1,0 @@
-# Use-case 0007 Implementation - Manage watchlist
-
-**Initiating actor:** Trader
-
-**Other actors:** —
-
-A logged-in Trader keeps a list of crypto assets they want to monitor, with the last
-price of each. The first time the watchlist is opened the system creates a default
-watchlist named "Favorites" for the Trader. From a sub-menu the Trader can list the
-watchlist, add a crypto or remove one. The Trader never types a symbol: for adding, the
-system lists, numbered, only the cryptos that are not on the watchlist yet, and for
-removing, only the cryptos that are on it; the Trader picks one by its number. Adding
-a crypto that is already on the list is a no-op (idempotent), and a number that is not
-in the list is refused without touching the database.
-
-Original use-case description (P3): [UseCase0007](../P3-UseCaseModel/UseCase0007.md).
-Implementation: [`server/watchlist.go`](../../server/watchlist.go), functions
-`ManageWatchlist`, `ensureDefaultWatchlist`, `listWatchlist`, `addToWatchlist` and
-`removeFromWatchlist`, with `pickNumber` from
-[`server/market.go`](../../server/market.go).
-
-All statements run on the `project` schema (the connection sets
-`search_path=project,public` in `server/db/db.go`). The SQL below is copied from the
-Go code; only the Go source indentation is removed.
-
-The run shown is user `alice` on the seed data, whose watchlist contains BTC, ETH and
-SOL. She lists it, adds ADA, tries to remove a number that is not in the list, removes
-SOL and lists the result.
-
-## Scenario
-
-1. **Trader** chooses `[8] Manage watchlist` in the authenticated menu (types `8`).
-2. **System** makes sure the Trader has a watchlist and takes the id of the oldest one
-   (`ensureDefaultWatchlist`; `$1` = the logged-in user's id):
-
-   ```sql
-   SELECT id FROM watchlists WHERE user_id = $1 ORDER BY created_at LIMIT 1
-   ```
-
-   Only if this returns no row, it creates the default watchlist and uses its id:
-
-   ```sql
-   INSERT INTO watchlists (user_id, name) VALUES ($1, 'Favorites') RETURNING id
-   ```
-
-   (alice already has the seed watchlist "Favorites", so only the `SELECT` runs.) The
-   watchlist id is kept in Go and used as `$1` in all statements below.
-3. **System** shows the sub-menu `-- Watchlist --` with `[1] List items`,
-   `[2] Add crypto`, `[3] Remove crypto` and `[0] Back`.
-
-   ![UC0007 steps 1-3: Trader opens the watchlist, system shows the sub-menu](screenshots/uc0007_1_3_menu.png)
-
-### List items
-
-4. **Trader** chooses `[1] List items`.
-5. **System** lists the cryptos on the watchlist with their last price against USD
-   (`listWatchlist`; `$1` = watchlist id):
-
-   ```sql
-   SELECT c.symbol, c.name, COALESCE(lp.price, 0)
-     FROM watchlist_items wi
-     JOIN crypto  c  ON c.id = wi.crypto_id
-     LEFT JOIN markets       m  ON m.crypto_id = c.id AND m.quote_currency = 'USD'
-     LEFT JOIN v_latest_prices lp ON lp.market_id = m.id
-    WHERE wi.watchlist_id = $1
-    ORDER BY c.symbol
-   ```
-
-   For alice it prints `BTC Bitcoin 67140.000000`, `ETH Ethereum 3520.000000` and
-   `SOL Solana 166.100000` (an empty watchlist prints `(watchlist is empty)`), then
-   shows the sub-menu again.
-
-   ![UC0007 List items](screenshots/uc0007_list.png)
-
-### Add a crypto
-
-6. **Trader** chooses `[2] Add crypto`.
-7. **System** lists, numbered, the cryptos that are not on the watchlist yet
-   (`addToWatchlist`; `$1` = watchlist id):
-
-   ```sql
-   SELECT c.id, c.symbol, c.name
-     FROM crypto c
-    WHERE NOT EXISTS (SELECT 1 FROM watchlist_items wi
-                       WHERE wi.watchlist_id = $1 AND wi.crypto_id = c.id)
-    ORDER BY c.symbol
-   ```
-
-   For alice it prints `1 ADA Cardano` and `2 DOGE Dogecoin` and asks
-   `Crypto # to add:`. Go keeps each row's crypto id in memory. (If every crypto is
-   already on the watchlist, it prints `Every crypto is already on your watchlist.`
-   instead.)
-
-   ![UC0007 Add: system lists the cryptos not yet on the watchlist](screenshots/uc0007_add_1_list.png)
-
-8. **Trader** picks the crypto by its number in the list: `1` (ADA).
-9. **System** adds the crypto of row 1 to the watchlist (`$1` = watchlist id,
-   `$2` = the chosen crypto's id); thanks to the unique constraint and
-   `ON CONFLICT ... DO NOTHING`, adding a crypto that is already there changes
-   nothing:
-
-   ```sql
-   INSERT INTO watchlist_items (watchlist_id, crypto_id)
-    VALUES ($1, $2)
-    ON CONFLICT (watchlist_id, crypto_id) DO NOTHING
-   ```
-
-   It prints `Added ADA.` and shows the sub-menu again.
-
-   ![UC0007 Add: Trader picks #1 (ADA), system adds it](screenshots/uc0007_add_2_added.png)
-
-### Remove a crypto
-
-10. **Trader** chooses `[3] Remove crypto`.
-11. **System** lists, numbered, the cryptos that are on the watchlist
-    (`removeFromWatchlist`; `$1` = watchlist id):
-
-    ```sql
-    SELECT c.id, c.symbol, c.name
-      FROM watchlist_items wi
-      JOIN crypto c ON c.id = wi.crypto_id
-     WHERE wi.watchlist_id = $1
-     ORDER BY c.symbol
-    ```
-
-    For alice it now prints `1 ADA Cardano`, `2 BTC Bitcoin`, `3 ETH Ethereum` and
-    `4 SOL Solana` and asks `Crypto # to remove:`. Go keeps each row's crypto id in
-    memory. (If the watchlist is empty, it prints `Your watchlist is empty.` instead.)
-
-    ![UC0007 Remove: system lists the watchlist's cryptos](screenshots/uc0007_remove_1_list.png)
-
-12. **Trader** picks the crypto by its number in the list: `4` (SOL).
-13. **System** removes the crypto of row 4 from the watchlist (`$1` = watchlist id,
-    `$2` = the chosen crypto's id):
-
-    ```sql
-    DELETE FROM watchlist_items WHERE watchlist_id = $1 AND crypto_id = $2
-    ```
-
-    It prints `Removed SOL.` and shows the sub-menu again.
-
-    ![UC0007 Remove: Trader picks #4 (SOL), system removes it](screenshots/uc0007_remove_2_removed.png)
-
-#### Alternate flow 12a — number not in the list
-
-Before removing SOL, alice first chose `[3] Remove crypto` and, at step 12, entered `5`
-while only numbers 1–4 were listed. `pickNumber` prints
-`Invalid choice, enter a number from 1 to 4.`, the `DELETE` is not run and the sub-menu
-is shown again; she then chose `[3]` once more, which returned the scenario to step 11.
-The same check applies to the number entered at step 8.
-
-![UC0007 Remove: a number that is not in the list is refused](screenshots/uc0007_remove_invalid.png)
-
-### Verification — list after the changes
-
-Choosing `[1] List items` again runs the query from step 5, which now returns
-`ADA Cardano 0.453750`, `BTC Bitcoin 67140.000000` and `ETH Ethereum 3520.000000`:
-ADA was added and SOL removed. `[0] Back` returns to the authenticated menu.
-
-![UC0007 List items after the changes](screenshots/uc0007_list_after.png)
Index: cs/P4-Prototype/wiki/BuildInstructions.md
===================================================================
--- docs/P4-Prototype/wiki/BuildInstructions.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,293 +1,0 @@
-= Build Instructions =
-
-This page explains how to compile, configure, run and test the !EduBerza prototype.
-It is linked from [wiki:PrototypeImplementation].
-
-== Development environment description ==
-
-||= Tool =||= Version tested =||= Needed for =||
-|| Go || 1.26.0 (`go.mod` asks for 1.25 or newer) || Building `server/` (the CLI) and `bots/` (the market bot). ||
-|| PostgreSQL || 16.3 (Docker container) || The database. Either the local Docker container or the faculty server. ||
-|| Docker + Docker Compose || any recent || Optional. Starts a local PostgreSQL with one command. ||
-|| `psql` || 16 || Optional. Only for running the SQL scripts by hand. ||
-|| Java || 21 (8+ works) || Optional. Only to open or edit the ER diagram in TerraER. ||
-|| DBeaver || any recent || Optional. Only to export `relational_diagram_v4.png`. ||
-
-About the PostgreSQL version: `docker-compose.yml` uses the image `postgres` without a version
-tag. Docker therefore starts whatever version of the official image it has pulled. On the
-machine where the prototype was tested, that was PostgreSQL 16.3. The only extension the schema
-needs is `pgcrypto` (`CREATE EXTENSION IF NOT EXISTS pgcrypto`). It ships with PostgreSQL and is
-included in the official image.
-
-You do not need to install anything else. The only third-party Go library is the PostgreSQL
-driver `github.com/lib/pq`. `go build` downloads it automatically, at the version pinned in
-`go.mod` and `go.sum`.
-
-== Build instructions ==
-
-Run all commands from the repository root.
-
-=== 1. Configure the database connection ===
-
-{{{
-cp .env.example .env
-}}}
-
-The defaults in `.env.example` (`localhost:5433`, user `bp_project`, database `bp_database`)
-match the bundled Docker setup. To use the faculty database instead, edit `.env`, or pass the
-values as real environment variables. Real environment variables take precedence over the file:
-
-{{{
-DBHOST=... DBPORT=5432 DBUSER=... DBPASSWORD=... DBNAME=... ./eduberza
-}}}
-
-`.env` is not committed on purpose (see `.gitignore`), because it holds a password.
-
-=== 2. Start PostgreSQL ===
-
-{{{
-docker compose up -d
-}}}
-
-Skip this step if you use the faculty database.
-
-=== 3. Build ===
-
-{{{
-go build -o eduberza ./server
-}}}
-
-=== 4. Create the schema and load the sample data ===
-
-{{{
-./eduberza -init
-}}}
-
-This runs `server/db/schema_creation.sql` and then `server/db/data_load.sql`. It logs
-`Running schema_creation.sql ...`, `Running data_load.sql ...` and `Database initialised.`,
-then prints:
-
-{{{
-Schema initialised. Re-run without -init to start the CLI.
-}}}
-
-Both scripts are '''compiled into the binary''' (`go:embed`), so `-init` works from any
-directory. It is destructive and can be run again any number of times: it drops and recreates
-the whole `project` schema, so it also resets everything if a demo goes wrong. To reload only
-the data and keep the schema:
-
-{{{
-./eduberza -load-data          # prints "Sample data reloaded."
-}}}
-
-If you prefer to watch the statements run, the same can be done with `psql`:
-
-{{{
-psql "postgresql://$DBUSER:$DBPASSWORD@$DBHOST:$DBPORT/$DBNAME" \
-  -f server/db/schema_creation.sql
-psql "postgresql://$DBUSER:$DBPASSWORD@$DBHOST:$DBPORT/$DBNAME" \
-  -f server/db/data_load.sql
-}}}
-
-=== 5. Run the prototype ===
-
-{{{
-./eduberza
-}}}
-
-=== 6. Optional: run the market simulation bot ===
-
-In a second terminal, also from the repository root (the bot reads `.env` from the current
-directory):
-
-{{{
-go run ./bots                  # add -interval 1s for faster ticks; the default is 3s
-}}}
-
-On every tick the bot moves the price of every active market by a small random step, inserts a
-row into `market_trades` and updates the current 1-minute candle. Prices in the CLI change
-while it runs, because the current price is always read from the most recent trade
-(`v_latest_prices`) and never from a stored column. Leave the bot off if you want the exact
-numbers in the tests below.
-
-=== 7. Optional: richer data for the P6 reports ===
-
-`data_load.sql` seeds only a few minutes of trade history. That is not enough for the
-top traders and market performance reports of [wiki:AdvancedReports] (menu `[10]` and `[11]`)
-to show more than one period. To see more interesting results, load five quarters of synthetic
-history on top:
-
-{{{
-psql "postgresql://$DBUSER:$DBPASSWORD@$DBHOST:$DBPORT/$DBNAME" \
-  -f server/db/reports_demo_data.sql
-}}}
-
-This script is not part of `-init` or `-load-data` on purpose. The header of
-`reports_demo_data.sql`, shown below, explains why. Running it never changes the balances that
-the tests below check.
-
-{{{
--- reports_demo_data.sql
--- EduBerza - optional historical data for the P6 reports
--- Course: Databases 2025/2026 Winter, FINKI UKIM
---
--- data_load.sql only seeds ~10 minutes of trade history, which is enough to
--- demonstrate UC0001-UC0007 but not enough to show report_top_traders() or
--- report_market_performance() doing anything interesting: everything falls
--- into a single quarter, so "number of profitable periods" and "consistency"
--- are trivial and "market return" has almost no history to work with.
---
--- This script adds five quarters of synthetic transactions, market trades and
--- executed orders on top of an already-loaded data_load.sql, spanning
--- 2025-07 to 2026-07, so the two P6 reports have several periods and two
--- markets with opposite price trends to actually compare.
---
--- Deliberately NOT part of -init / -load-data: it only inserts into
--- transactions, market_trades and orders, and does not touch
--- users.available_balance/invested_balance or holdings, so it does not
--- disturb the balances the other use cases' documented "verified run"
--- sections depend on. Run it by hand, after data_load.sql, only to exercise
--- the two reports:
---
---   psql "$DATABASE_URL" -f server/db/schema_creation.sql
---   psql "$DATABASE_URL" -f server/db/data_load.sql
---   psql "$DATABASE_URL" -f server/db/reports_demo_data.sql
---
--- Idempotent: deletes its own previously-inserted rows (tagged via
--- description/source) before re-inserting.
-}}}
-
-== Testing instructions ==
-
-=== How to launch and log in ===
-
-Start the prototype with `./eduberza` after steps 1–4. The sample data creates three test
-users. All of them have the password '''`test123`''':
-
-||= Username =||= Starting state after `-init` =||
-|| `alice` || 8250.00 USD available (1750.00 invested), holds 0.5 ETH bought at 3500.00. Watchlist "Favorites": BTC, ETH, SOL. Best demo account. ||
-|| `bob` || 5000.00 USD available, no crypto. Watchlist "Bobs Picks": BTC, DOGE. ||
-|| `charlie` || 2500.00 USD available, no crypto, no watchlist yet. ||
-
-The five sample markets are ADA, BTC, DOGE, ETH and SOL, all quoted in USD. Their starting
-last prices are 0.45375, 67140, 0.122, 3520 and 166.1.
-
-=== Mini-guide to the application ===
-
-You always answer with the number of a menu option. When you have to choose a market, a
-holding or a crypto, the prototype prints a numbered list and you type the number from that
-list. You never type an id or a symbol. A number that is not in the list is refused with
-`Invalid choice, enter a number from 1 to N.`
-
-'''Menu before login'''
-
-||= Option =||= What it does and how to use it =||
-|| `[1] Register` || Enter a username, an e-mail (must contain `@`), your full name and a password (at least 6 characters). You get `Account created. You can now log in.`, or `Invalid email.`, `Password must be at least 6 characters.` or `Username or email already taken.` A new account starts with 0 USD. ||
-|| `[2] Login` || Enter your username and password. You get `Login successful.` and the second menu. A wrong password and an unknown username both give `Invalid credentials.` ||
-|| `[3] Browse markets` || Prints the numbered list of markets with their last price. ||
-|| `[0] Exit` || Ends the program. ||
-
-'''Menu after login''' (headed `--- Logged in as <username> ---`)
-
-||= Option =||= What it does and how to use it =||
-|| `[1] View balance` || Shows the available, invested and total USD. ||
-|| `[2] Deposit virtual funds` || Enter an amount in USD. It must be a positive number, otherwise you get `Invalid amount.` You get `Deposited 500.0000 USD.` ||
-|| `[3] Browse markets` || Same list as before login. ||
-|| `[4] Place market BUY order` || Lists all markets, numbered, with their last price. Type the number at `Market #:`. The prototype shows the latest price. Type the quantity. You get `Order executed: buy …` or `Insufficient funds: need …, have …`. ||
-|| `[5] Place market SELL order` || Lists only the cryptos you hold, numbered, with columns `Held` and `Free to sell`. Type the number at `Holding #:`, then the quantity. You get `Order executed: sell …` or `Insufficient holding: …`. If you hold nothing, you get `you hold no crypto that is free to sell`. ||
-|| `[6] View portfolio` || One row per crypto you hold: quantity, reserved, available, average buy price, current price, value and unrealised P/L. Then your cash, portfolio value and net worth. ||
-|| `[7] View transaction history` || Your last 20 ledger entries (deposits, buys, sells), newest first. ||
-|| `[8] Manage watchlist` || Opens a submenu: `[1] List items` shows your watchlist with last prices. `[2] Add crypto` lists, numbered, the cryptos not on it yet; type a number. `[3] Remove crypto` lists, numbered, the cryptos on it; type a number. `[0] Back` returns. A user without a watchlist gets one named "Favorites" the first time. ||
-|| `[9] Logout` || Back to the first menu. ||
-|| `[10] Report: top traders` || P6 report. Enter a start date (inclusive) and an end date (exclusive) as `YYYY-MM-DD`. ||
-|| `[11] Report: market performance` || P6 report, with the same two dates. ||
-|| `[0] Exit` || Ends the program. ||
-
-=== End-to-end smoke test ===
-
-These values were checked on 2026-09-24 against freshly loaded sample data (PostgreSQL 16.3),
-with the bot not running. The expected values are exact.
-
- 1. `./eduberza -init` prints `Schema initialised. Re-run without -init to start the CLI.`
- 2. `./eduberza`, then `2` (Login), then `alice` / `test123` gives `Login successful.`
- 3. `6` (View portfolio) shows one row: `ETH`, quantity 0.5000, reserved 0.0000, available
-    0.5000, average buy 3500.000000, current 3520.000000, value 1760.0000, unrealised P/L
-    `+10.0000`. Cash available is 8250.0000 and net worth is 10010.0000.
- 4. `4` (BUY). The market list shows `1 ADA`, `2 BTC`, `3 DOGE`, `4 ETH`, `5 SOL`. Type `2` at
-    `Market #:`, then `0.01` at `Quantity:`. The result is
-    `Order executed: buy 0.0100 BTC @ 67140.000000 (notional 671.4000 USD)`.
- 5. `6` (View portfolio) now shows BTC ''and'' ETH, with total value 2431.4000, cash 7578.6000
-    (= 8250.00 − 671.40), and net worth still 10010.0000.
- 6. `5` (SELL). The holdings list shows `1 BTC` (held 0.0100) and `2 ETH` (held 0.5000). Type
-    `2` at `Holding #:`, then `0.5`. The result is
-    `Order executed: sell 0.5000 ETH @ 3520.000000 (notional 1760.0000 USD)`.
- 7. `7` (View transaction history) lists, newest first: the `sell` (+1760.0000), the `buy` of
-    BTC (−671.4000), then the two rows from the sample data, which have the same timestamp:
-    `deposit` 10000.0000 "Initial virtual deposit" and `buy` −1750.0000 "Market buy 0.5 ETH @
-    3500.00".
- 8. `8` (Manage watchlist), then `1` (List items), shows alice's watchlist with BTC, ETH and SOL
-    and their last prices. Then `2` (Add crypto) lists `1 ADA` and `2 DOGE`; type `1` and you get
-    `Added ADA.` Then `0` (Back).
- 9. `9` (Logout), then `0` (Exit).
-
-=== Testing the failure paths ===
-
-These matter more than the happy path, because they prove that the transactions really roll
-back and that invalid choices are refused:
-
- * '''Insufficient funds:''' log in as `charlie` (2500 USD). Choose `4`, market `2` (BTC),
-   quantity `1`. Expect `Insufficient funds: need 67140.0000, have 2500.0000` and ''no'' change to
-   any table: no order row, no ledger entry, no holding.
- * '''Insufficient holding:''' on fresh data (`./eduberza -load-data`), log in as `alice`. Choose
-   `5`; the list shows only `1 ETH` (held 0.5000, free 0.5000). Choose `1`, quantity `5`. Expect
-   `Insufficient holding: trying to sell 5.0000, available 0.5000 (of 0.5000 held, 0.0000 reserved)`.
- * '''Nothing to sell:''' as `bob` (no crypto), choose `5`. The holdings list is empty, and you
-   get `you hold no crypto that is free to sell` without being asked for a number.
- * '''Invalid choice from a list:''' in any list (for example `8`, then `3` Remove crypto), type a
-   number larger than the list. Expect `Invalid choice, enter a number from 1 to N.`
- * '''Invalid deposit:''' choose `2` and enter `-50`. Expect `Invalid amount.`
- * '''Duplicate registration:''' register with username `alice`. Expect
-   `Username or email already taken.`
- * '''Invalid e-mail:''' register with an e-mail without `@`. Expect `Invalid email.`
- * '''Wrong password:''' log in as `alice` with any wrong password. Expect
-   `Invalid credentials.` An unknown username gives the same message, so the prototype does not
-   reveal which accounts exist.
-
-The concurrency guarantee of the sell path (two processes selling the same crypto at the same
-moment) cannot be reproduced by typing into two terminals, because each order commits within
-milliseconds. It is described in [wiki:UseCase0005Implementation].
-
-=== For the public presentation ===
-
-Demo with `alice`. She already has a position, so the portfolio screen is not empty. Register a
-brand-new account live to show UC0001. Run the bot in a background terminal so the prices
-visibly move between two portfolio refreshes.
-
-== Editing the ER diagram ==
-
-TerraER is a third-party tool and is '''not''' committed to this repository on purpose. Download
-the teacher's build from `https://bazi.finki.ukim.mk/resources/Software/` and run it:
-
-{{{
-java -jar TerraER3.11.jar     # then File → Open → docs/P1-ConceptualModel/ERModel_v05.xml
-}}}
-
-The current version is `ERModel_v05.xml`. Save new versions as `ERModel_v06.xml` and so on,
-and export a matching PNG for each. TerraER does not add the extension itself: type `.xml`
-yourself, or the file will not reopen.
-
-== Up-to-date source code ==
-
-The repository is pushed to the FINKI DEVELOP git server. The clone URL and credentials are in
-the Repositories section in EPRMS.
-
-=== About the source code ===
-
- * All the source needed to run the prototype is in this repository: the CLI (`server/`), the
-   market bot (`bots/`), the DDL script and the sample-data script (`server/db/`).
- * Third-party Go libraries are '''not''' vendored. `go build` downloads `github.com/lib/pq` at
-   the versions pinned in `go.mod` and `go.sum`.
- * Third-party executables are '''not''' committed. `.gitignore` excludes `*.jar`, and TerraER is
-   downloaded from the URL above.
- * No third-party images, styles or frameworks are used. The prototype has no images at all;
-   the interface is text.
Index: cs/P4-Prototype/wiki/PrototypeImplementation.md
===================================================================
--- docs/P4-Prototype/wiki/PrototypeImplementation.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,128 +1,0 @@
-= Prototype Implementation =
-
-== Implemented use-cases ==
-
- * [wiki:UseCase0001Implementation] — Register new account
- * [wiki:UseCase0002Implementation] — Log in
- * [wiki:UseCase0003Implementation] — Deposit virtual funds
- * [wiki:UseCase0004Implementation] — Place market BUY order
- * [wiki:UseCase0005Implementation] — Place market SELL order
- * [wiki:UseCase0006Implementation] — View portfolio and transaction history
- * [wiki:UseCase0007Implementation] — Manage watchlist
-
-Each page follows its P3 use case step by step. It adds the exact SQL the Go code runs in that
-step and a screenshot of the step from a real run against the database.
-
- * How to build, configure, run and test the prototype: [wiki:BuildInstructions]
- * AI usage for this phase: [wiki:PrototypeImplementationAIUsage]
-
-== Overview ==
-
-!EduBerza's P4 prototype is a Go command-line program. It works against the `project` schema in
-PostgreSQL. It implements all seven use cases from [wiki:UseCaseModel]; the course asks for at
-least three. Every database access is real SQL that was executed and tested. A second program,
-the market bot, simulates a live market so prices move while the prototype runs.
-
-The source code is in the project's git repository: the CLI in `server/`, the bot in `bots/`,
-and the SQL scripts in `server/db/`.
-
-== Technology and architecture ==
-
- * '''Language:''' Go (module `bp_project`, `go 1.25` in `go.mod`). The only third-party
-   library is the PostgreSQL driver `github.com/lib/pq`.
- * '''Database:''' PostgreSQL. Every table, view and function is in the `project` schema. The
-   DDL is `schema_creation.sql` and the sample data is `data_load.sql`. Both scripts are
-   compiled into the binary and run by `./eduberza -init`.
- * '''Interface:''' plain text menus on standard input and output. There are no web server,
-   frameworks, images or styles.
- * '''Structure:''' one source file per area of the application.
-
-||= File =||= Responsibility =||= Use cases =||
-|| `server/main.go` || Flags `-init` / `-load-data`, then starts the menu loop || — ||
-|| `server/cli.go` || The two menus (before and after login), input reading || all ||
-|| `server/db/db.go` || Connection from `.env` / environment variables, embedded SQL scripts || — ||
-|| `server/auth.go` || Register, log in (SHA-256 password hash) || UC0001, UC0002 ||
-|| `server/account.go` || Balance, deposit, transaction history || UC0003, UC0006 ||
-|| `server/market.go` || Market list, choosing a market or a holding by number, latest price || UC0004, UC0005 ||
-|| `server/trade.go` || Market buy and sell orders, each in one transaction || UC0004, UC0005 ||
-|| `server/portfolio.go` || Portfolio with current value and unrealised P/L || UC0006 ||
-|| `server/watchlist.go` || List, add and remove watchlist items || UC0007 ||
-|| `bots/main.go` || Market bot: random-walk price ticks into `market_trades`, 1-minute candles || — ||
-
-== No identifiers to remember ==
-
-The user never has to type or remember an id, a code or a symbol:
-
- * Every menu is numbered, and the user answers with the number of an option.
- * '''Buying:''' all active markets are listed with their latest price, numbered 1…n. The user
-   enters the market's number at `Market #:` (`ChooseMarket` in `market.go`).
- * '''Selling:''' only the cryptos the user actually holds are listed, each with the quantity
-   held and the quantity still free to sell. The user enters the holding's number at
-   `Holding #:` (`ChooseHolding`). A user who holds nothing free to sell gets
-   `you hold no crypto that is free to sell` and is never asked to choose.
- * '''Watchlist:''' ''Add'' lists the cryptos that are not on the watchlist yet. ''Remove''
-   lists the ones that are on it. Both are numbered, and the user enters the number.
- * A number outside the list is refused with `Invalid choice, enter a number from 1 to N.` and
-   nothing is changed.
-
-The only things the user types are their own data: username, e-mail, full name, password, the
-amount to deposit, the quantity to buy or sell, and the date range of the two P6 reports.
-
-== What the prototype demonstrates about the database design ==
-
- * '''The current price is never stored as a column.''' It is always the price of the most
-   recent row in `market_trades`, read through the `v_latest_prices` view. The user's own fills
-   and the bot's simulated trades go into the same table, so there is only one definition of
-   "the price".
- * '''Money movements are transactional.''' A buy touches five tables (`orders`, `users`,
-   `holdings`, `transactions`, `market_trades`) inside one transaction. If the balance check
-   fails, the whole transaction is rolled back: after a rejected purchase there is no order
-   row, no ledger entry and no holding. The failure-path tests in [wiki:BuildInstructions]
-   check this.
- * '''Constraints do real work.''' `UNIQUE (user_id, crypto_id)` on `holdings` is what makes
-   the `INSERT … ON CONFLICT DO UPDATE` upsert possible, so the database recomputes the
-   weighted-average entry price in one statement, instead of the application reading, changing
-   and writing the row. `CHECK (reserved_quantity >= 0 AND reserved_quantity <= quantity)` does
-   the same for the sell path: the database itself makes an inconsistent reservation
-   impossible, and it does not rely only on `trade.go` being careful.
- * '''Selling reserves before it removes.''' A sell order locks the holding row with
-   `SELECT … FOR UPDATE`, reserves the quantity being sold, then settles by removing it (see
-   [wiki:UseCase0005Implementation]). Two sell orders for more than the free quantity, placed
-   at the same moment from two separate processes, are serialised by the row lock. Exactly one
-   of them succeeds. This was tested with two concurrent processes in session 3 (see
-   [wiki:PrototypeImplementationAIUsage]).
-
-== Known limitations ==
-
-These were left out on purpose for a first prototype. They belong to the later phases:
-
- * Only `market` orders execute. The schema accepts `limit` (`orders.type`), but there is no
-   matching logic for it.
- * Passwords are hashed with SHA-256 and no salt. That shows the password itself is never
-   stored, but it is not good enough for real use. A proper password hash belongs in P9
-   (security).
- * Money is `float64` in Go, while the database columns are `numeric`. For that reason, all
-   arithmetic that must be exact (the weighted average) is done in SQL. Real use would need a
-   decimal type on the Go side too.
- * The prototype sets no connection pool and no explicit isolation level. Both are P8 topics.
- * A reservation only exists inside one transaction, because the prototype only has market
-   orders, and they settle immediately. A real limit-order matcher would leave
-   `holdings.reserved_quantity` set and `orders.status = 'open'` between two separate commits.
-   It would also need a way to cancel an order and release the reservation. Neither is
-   implemented, because nothing in the prototype creates an order that stays open.
-
-== History of changes ==
-
-The code started as my own Go backend: HTTP handlers, a draft schema, and a `db.go` that
-recreated the tables on every start. It changed as follows. The AI's share of each change is
-logged in [wiki:PrototypeImplementationAIUsage].
-
-||= Date =||= Change =||= Origin =||
-|| 2026-04-21 || My HTTP backend rewritten as the CLI prototype covering UC0001–UC0007. The schema errors in my draft were corrected. The market bot was added. || My code and decisions (CLI instead of web, drop the frontend, keep a simulator); rewrite by AI (session 1) ||
-|| 2026-08-06/07 || Three bugs fixed: path resolution of `.env` and the SQL scripts, an endless loop at end of input, and an error check in the wrong order on the sell path. The holding update became one `INSERT … ON CONFLICT DO UPDATE`. || I asked for a code review; fixes by AI (session 2) ||
-|| 2026-09-16 || `holdings.reserved_quantity` added. The sell path now reserves, then settles. Orders go from `open` to `executed`. || The edge case was mine; implementation by AI (session 3) ||
-|| 2026-09-24 || Every choice is picked from a numbered list: markets by number, a sell lists only the user's holdings, the watchlist lists the cryptos. All screenshots were retaken, one per step. || I asked for a check against the P4 rules; implementation by AI (session 4) ||
-
-'''Service:''' Claude Code (Anthropic), Claude subscription. Session 1 used Claude Opus 4.7 (1M
-context), session 2 Claude Opus 5 (1M context), session 3 Claude Sonnet 5, and session 4
-Claude Opus 5.5 (1M context).
Index: cs/P4-Prototype/wiki/PrototypeImplementationAIUsage.md
===================================================================
--- docs/P4-Prototype/wiki/PrototypeImplementationAIUsage.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,391 +1,0 @@
-= Prototype Implementation AI Usage =
-
-== Name of AI service/solution that was used ==
-
-'''Claude Code''' (Anthropic), an AI coding assistant that runs in the terminal and reads and
-edits the project files.
-
- * '''URL:''' `https://claude.com/claude-code`
- * '''Type of service/subscription:''' Claude subscription (Claude Code CLI). A different model was
-   used in each session:
-
-||= Session =||= Date =||= Model =||
-|| 1 || 2026-04-21 || Claude Opus 4.7 (1M context) ||
-|| 2 || 2026-08-06 / 2026-08-07 || Claude Opus 5 (1M context) ||
-|| 3 || 2026-09-16 || Claude Sonnet 5 ||
-|| 4 || 2026-09-24 || Claude Opus 5.5 (1M context) ||
-
-The same sessions also worked on other phases. This page covers only what concerns the P4
-prototype and its documentation.
-
-== Final result ==
-
-=== Results in details / description ===
-
-'''My own starting code (before any AI was used).''' Before session 1 I had written:
-
- * a Go backend with HTTP handlers (Chi router);
- * a draft database schema (`server/db/db.sql`, `server/db/schema.sql`) and a diagram description
-   (`docs/dbdiagram.md`), based on my data model in `ep-diagram.md`;
- * a `main.go` / `db.go` that connected to PostgreSQL and dropped and recreated every table on
-   each start;
- * a half-finished frontend, which I deleted myself at the start of session 1 because the
-   prototype does not need it.
-
-This code is older than the git repository. The first commit (2026-08-07) was made after
-sessions 1 and 2, so the history in git starts from the AI-improved version. My original files
-are not in the repository. What they contained, and which errors the AI found in them, is
-recorded in the session 1 log below and in [wiki:ERModelAIUsage].
-
-'''Session 1 (2026-04-21).''' Starting from that code, the AI:
-
- * replaced my HTTP backend and the frontend scaffolding with a single-binary CLI prototype in Go,
-   split across `main.go`, `cli.go`, `auth.go`, `account.go`, `market.go`, `trade.go`,
-   `portfolio.go` and `watchlist.go`. This followed my decision to make a CLI and not a web app;
- * corrected my schema. `crypto_id` had been declared as a foreign key to two tables at once in
-   `holdings`, `orders` and `transactions`, and `market_candles` referenced a `markets` table that
-   did not exist. The corrected schema became `schema_creation.sql`, with the sample data in
-   `data_load.sql`;
- * replaced my `db.go`, which dropped every table on every start, with one `Connect()` plus
-   explicit `-init` and `-load-data` flags;
- * moved `go.mod` to the project root, removed an unused MySQL driver, and made
-   `github.com/lib/pq` a direct dependency;
- * wrote the trade flows as database transactions with `FOR UPDATE` row locks, cost-basis
-   bookkeeping and one ledger row per operation;
- * wrote the market-simulation bot (`bots/main.go`), after I decided to keep a market simulator
-   in the project.
-
-'''Session 2 (2026-08-06/07).''' I asked for a code review. The AI found and fixed three bugs
-(`.env`/SQL path resolution, an endless loop at end of input, and a wrong error check on the
-sell path). It also replaced the read-modify-write holding update with one
-`INSERT … ON CONFLICT DO UPDATE`, removed the committed `TerraER3.11.jar` and a !TradingView
-screenshot we had no licence for, and wrote the first version of the P4 pages.
-
-'''Session 3 (2026-09-16).''' From an edge case I described, the AI added
-`holdings.reserved_quantity`, made the sell path reserve and then settle, and gave
-`orders.status` a real `open` → `executed` lifecycle.
-
-'''Session 4 (2026-09-24).''' I asked whether P4 fulfils the course rules. The AI found that the
-prototype still asked the user to type a market symbol and crypto symbols, which breaks the rule
-that the user must never have to remember identifiers or codes. It changed every choice to a
-numbered list (`market.go`, `trade.go`, `watchlist.go`), retook every screenshot, one per
-scenario step, and rewrote the P4 pages.
-
-=== Test evidence ===
-
-This is the current prototype (after session 4) on fresh sample data, logged in as `alice`. It
-is a real run, and the same run is on the screenshots of
-[wiki:UseCase0004Implementation]:
-
-{{{
--- Place market buy order --
-
-  #     Symbol    Quote       Last price
-  -----------------------------------------
-  1     ADA       USD           0.453750
-  2     BTC       USD       67140.000000
-  3     DOGE      USD           0.122000
-  4     ETH       USD        3520.000000
-  5     SOL       USD         166.100000
-Market #: 2
-Latest price for BTC/USD = 67140.000000
-Quantity: 0.01
-Order executed: buy 0.0100 BTC @ 67140.000000 (notional 671.4000 USD)
-
-  Symbol        Quantity      Reserved     Available         Avg buy         Current           Value  Unrealised P/L
-  ------------------------------------------------------------------------------------------------------------------
-  BTC             0.0100        0.0000        0.0100    67140.000000    67140.000000        671.4000         +0.0000
-  ETH             0.5000        0.0000        0.5000     3500.000000     3520.000000       1760.0000        +10.0000
-  ------------------------------------------------------------------------------------------------------------------
-  TOTAL                                                                                    2431.4000        +10.0000
-
-  Cash available : 7578.6000 USD
-  Portfolio value: 2431.4000 USD
-  Net worth      : 10010.0000 USD
-}}}
-
-== Summary of AI involvement ==
-
-||= =||= Session 1 — 2026-04-21 =||= Session 2 — 2026-08-06/07 =||= Session 3 — 2026-09-16 =||= Session 4 — 2026-09-24 =||
-|| '''What I brought''' || My Go backend (Chi HTTP handlers), draft schema, 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 || The official P4 instructions and the question whether the prototype and pages meet them ||
-|| '''What the AI did''' || Rewrote the backend as a CLI covering UC0001–UC0007, corrected the schema, wrote the market bot || Reviewed the code, found and fixed three bugs, improved the holding upsert || Added `holdings.reserved_quantity`, reserve-then-settle sell path, `orders.status` lifecycle, tested it including real concurrency || Audited P4, made every choice a numbered list, retook all screenshots, rewrote the P4 pages ||
-|| '''What I decided''' || To delete the frontend, to build a CLI and not a web app, to keep the market simulator || To ask for a code review and not only documentation || To keep reserve and settle in one transaction, because there is no cancel-order use case to free a stuck reservation || To fix the problems found and to have the screenshots taken from real runs ||
-
-== Entire AI usage log ==
-
-'''About this log.''' Sessions 3 and 4 are still in my local Claude Code history, so their
-prompts are quoted '''verbatim''' below, typos included. The local history of sessions 1 and 2
-(April and August 2026) is no longer available. For them, the prompts are the excerpts I wrote
-down at the time, the same ones as on [wiki:ERModelAIUsage],
-where they were logged because the same conversation produced P1–P4. The AI's responses are
-summarised, because several ran to many pages of code and file edits.
-
-----
-
-=== Session 1 — 2026-04-21 (Claude Opus 4.7, 1M context) ===
-
-'''Prompt (student, excerpt):'''
-> This is my project for my exam Database, go to the docs folder and README.md there you will find all of the documentation, afterwards take a look at the whole project. Delete everything you want add everything you want just make it work and make it fill in or to follow all of the needed instructions. Ask me anything, also tell me what do I need to do
-
-'''Response (AI, summarised):'''
- * Read `docs/opis.md`, `docs/ep-diagram.md`, `docs/dbdiagram.md`, `server/db/db.sql`,
-   `server/db/schema.sql` and all Go sources.
- * Found errors in my schema: `holdings.crypto_id` declared as a foreign key to both
-   `users(id)` and `crypto(id)` (and the same mistake in `orders` and `transactions`), and
-   `market_candles` referencing a non-existent `markets` table. Also found that `main.go` dropped
-   and recreated all tables on every start, wiping the data.
- * Asked me to decide on team composition, prototype shape (CLI or HTTP), which columns to keep,
-   which use cases to implement, and the documentation language.
-
-'''Prompt (student, excerpt):'''
-> Read it now, just filled it in, also deleted the frontend we don' need that
-
-'''Response (AI, summarised):'''
- * Re-read `docs/README.md`, which now contained the course rubric, and explained the AI rules
-   per phase and the deliverables of each phase.
-
-'''Prompt (student, excerpt):'''
-> do everything that you need to do, and tell me after what do I need to do
-
-'''Response (AI, summarised):'''
- * Rewrote the backend as the CLI prototype (UC0001–UC0007), with the corrected
-   `schema_creation.sql`, `data_load.sql`, the new `db.go` with `-init` / `-load-data`, the
-   transactional trade flows and the market bot. The details are under ''Results in details''
-   above.
- * Ran the prototype end to end against PostgreSQL on port 5433 and checked that a sample buy
-   and the portfolio view gave the expected numbers.
-
-----
-
-=== Session 2 — 2026-08-06 / 2026-08-07 (Claude Opus 5, 1M context) ===
-
-'''Prompt (student, excerpt):'''
-> […] do all of the other Phases till m0.
-> opis.md It's p0 so I will take care of that. Delete anything that we don't need,
-> make all of the phases and terra diagram if you can, and delete anything
-> that we don't need and make a documentation about how to start it.
-
-'''Prompt (student, excerpt, follow-up):'''
-> Also fix some database things or golang things if you think we can do it better,
-
-'''Response (AI, summarised): what changed in the prototype, and why'''
-
-'''Bugs found and fixed'''
-
- 1. `server/db/db.go` resolved `../.env` and `db/schema_creation.sql` relative to the working
-    directory, so they only worked when the program was started from inside `server/`.
-    Following the documented instructions (build from the repository root and run
-    `./eduberza -init`) failed with `password authentication failed for user "postgres"`,
-    because `.env` was never found and the defaults were used. The two SQL scripts are now
-    compiled into the binary with `go:embed`, and `.env` is searched for in the working directory
-    and every parent. Real environment variables now take precedence over the file.
- 2. `prompt()` in `server/cli.go` ignored the error from `ReadString`. At end of input (Ctrl-D,
-    or a scripted run) it returned an empty string forever, and the menu loop kept printing
-    "Unknown option." without end. It now exits cleanly.
- 3. On the sell path, `trade.go` checked `err == sql.ErrNoRows || held < qty` before checking
-    for other errors, so any scan failure was reported as "Insufficient holding". The error
-    check now comes first.
-
-'''Improvements'''
-
- 4. The holding upsert was a read-modify-write in Go. It is now a single
-    `INSERT … ON CONFLICT (user_id, crypto_id) DO UPDATE`, so PostgreSQL recomputes the average
-    in `numeric` arithmetic, relying on the unique constraint of the relational design.
- 5. `TerraER3.11.jar` was removed from the repository and `.gitignore` now excludes `*.jar`,
-    because P4 requires third-party executables to be downloaded, not committed. `.env` is
-    excluded too, and `.env.example` was added in its place.
- 6. `image.png`, a !TradingView screenshot, was deleted, because the project has no licence to
-    publish it.
-
-'''Verification.''' All seven use cases were run against PostgreSQL 16, and the four failure
-paths were tested. After a rejected purchase, the affected user had zero rows in `orders`,
-`transactions` and `holdings`.
-
-----
-
-=== Session 3 — 2026-09-16 (Claude Sonnet 5) ===
-
-'''Prompt (student, verbatim):'''
-> Soo we have a problem here In this scenario we have an edge case where our functionallity doesn't work:
-> Suppose the user owns:
->
-> 2 BTC
->
-> and wants to sell:
->
-> 0.5 BTC at market price
->
-> A sensible procedure is:
->
-> 1. User creates an Order
->
-> Orders gets something like:
->
-> id    user    market    side    type    quantity    status
-> O1    Alice    BTC/USD    sell    market    0.5    open
->
-> At this point, no trade has necessarily happened yet.
->
-> 2. Reserve the crypto
->
-> This is where your current model has a gap.
->
-> 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
->
-> So Alice might go from:
->
-> quantity = 2.0 BTC
-> reserved_quantity = 0
->
-> to:
->
-> quantity = 2.0 BTC
-> reserved_quantity = 0.5 BTC
->
-> with:
->
-> available BTC = quantity - reserved_quantity = 1.5 BTC
->
-> The important point is that you generally don't actually remove the 0.5 BTC from her position yet. You reserve it so she cannot submit another sell order for the same BTC.
->
-> 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.
-
-The other prompts of that day were about the P1 diagram (logged on
-[wiki:ERModelAIUsage]) and about P5 and P6. None of them
-changed the P4 prototype.
-
-'''Response (AI, summarised)'''
-
-The gap: `trade.go`'s sell path checked `held < qty` directly against `holdings.quantity`.
-Nothing distinguished "owned" from "owned, but already promised to an order". This was safe only
-because the whole sell runs in one transaction with a `SELECT … FOR UPDATE` lock. It would stop
-being safe the moment an order could stay `open` across transactions.
-
-What changed:
-
- 1. `holdings.reserved_quantity numeric(20,4) NOT NULL DEFAULT 0 CHECK (reserved_quantity >= 0 AND reserved_quantity <= quantity)`
-    was added to `schema_creation.sql`.
- 2. `v_portfolio` gained `reserved_quantity` and the derived `available_quantity`.
- 3. The sell path in `trade.go` now locks the holding, computes
-    `available := quantity - reserved_quantity`, rejects the order if `available < qty`,
-    reserves (`reserved_quantity += qty`), then settles (`quantity -= qty; reserved_quantity -= qty`).
-    All of this stays inside the same transaction.
- 4. Both branches of `PlaceOrder` insert the order as `status='open'` and set
-    `status='executed', executed_at=now()` at the end.
- 5. `portfolio.go` shows new `Reserved` and `Available` columns.
- 6. The sell error message became
-    `"Insufficient holding: trying to sell X, available Y (of Z held, W reserved)"`.
-
-'''Test evidence''' (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"
-}}}
-
-(At that time the sell path still asked for a symbol, so a user with no holding could reach the
-check. Since session 4, such a user is never offered anything to sell.)
-
-'''What I decided:''' to keep reserve and settle inside a single transaction. Splitting them into
-two commits, so that an order really sits `open` and reserved in between, is what a real
-limit-order matcher will need. Building it now would add a way for an order to get stuck without
-a way to cancel it.
-
-----
-
-=== Session 4 — 2026-09-24 (Claude Opus 5.5, 1M context) ===
-
-'''Prompt (student, verbatim):'''
-> Does the P4 documentation, fullfill this:
->
-> ''[pasted: the complete official text "Instructions on Phase P4: First Application Prototype"''
-> ''from the course site — the prototype requirements, the rule that the user must never have to''
-> ''remember identifiers or codes, the !PrototypeImplementation / UseCaseXXXXImplementation /''
-> ''!BuildInstructions templates, the source-code rules and the AI Use rules with the''
-> ''PrototypeImplementationAIUsage template]''
->
-> is this correct?
-> Or do we need to change anything?
-
-'''Response (AI, summarised):''' an audit of the prototype and of the P4 pages against each
-requirement. The main findings:
- * The prototype asked the user to type a market symbol (`Market symbol (e.g. BTC):`) and, in the
-   watchlist, a crypto symbol. That breaks the rule that every choice must be picked from a list.
- * The use-case implementation pages had one screenshot per use case, not one per step, and did
-   not show the SQL at every step.
- * `PrototypeImplementation.md` was written in Trac markup inside a Markdown file. It did not
-   follow the template, and it said identifiers were never typed, which was not accurate.
- * `BuildInstructions` had wrong details: the `-init` message, the order of the history, an
-   outdated ER diagram path, an unpinned PostgreSQL version described as 16, and a concurrency
-   test with no steps to prepare it.
- * This page listed only one model, still had "Student action required" notes, and had no
-   line-by-line log.
-
-'''Prompt (student, verbatim):'''
-> YEs you can start fixing it, and can you make the screenshots yourself?
-
-'''Response (AI, summarised):'''
- * '''Code''' (`market.go`, `trade.go`, `watchlist.go`):
-   * `ListMarkets` now returns the numbered list it prints. `ChooseMarket` asks for `Market #:`
-    and takes the market by its position in that list.
-   * New `ChooseHolding` for the sell path. It lists only the user's holdings that still have a
-    free quantity, with `Held` and `Free to sell`, and asks for `Holding #:`.
-   * Watchlist ''Add'' lists, numbered, the cryptos not yet on the watchlist. ''Remove'' lists the ones
-    on it.
-   * A shared `pickNumber` refuses anything outside `1…n` with
-    `Invalid choice, enter a number from 1 to N.`
-   * The SQL that looked a crypto up by symbol (`WHERE upper(c.symbol) = upper($1)`) is gone. The
-    chosen row's id is used directly.
- * '''Screenshots:''' all of them were retaken from real CLI sessions, driven in a pseudo-terminal
-   against freshly initialised sample data. There is one screenshot per scenario step, including
-   the alternate flows (invalid e-mail, duplicate user, wrong password, invalid amount,
-   insufficient funds, insufficient holding, invalid list choice). This gave 30 screenshots,
-   which replace the old 8.
- * '''Documentation:'''
-   * Rewrote the seven UseCase000XImplementation pages, with the SQL of each step quoted literally
-    from the code.
-   * Rewrote [wiki:PrototypeImplementation] (real Markdown, template
-    order, accurate description of the choices), [wiki:BuildInstructions] (the
-    corrections above, a mini-guide of every menu item, and a smoke test re-run on 2026-09-24)
-    and this page.
-   * Made small matching edits to the P3 use cases UC0004, UC0005 and UC0007, so that the "system
-    lists …, user picks …" steps match the prototype.
-   * Wrote the wiki versions of the P4 pages for the faculty site.
-
-'''What I decided:''' to accept the numbered-list change, because it is what the P4 rule asks for.
-I also decided to have the screenshots made from real runs instead of editing the old ones.
Index: cs/P4-Prototype/wiki/UseCase0001Implementation.md
===================================================================
--- docs/P4-Prototype/wiki/UseCase0001Implementation.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,161 +1,0 @@
-= Use-case 0001 Implementation - Register new account =
-
-'''Initiating actor:''' Visitor
-
-'''Other actors:''' —
-
-A new person creates an account on !EduBerza so that they can later log in as a
-Trader ([wiki:UseCase0002Implementation UseCase0002]). The Visitor enters a
-username, an e-mail address, a full name and a password. The system validates the
-input (required fields, an `@` in the e-mail, a password of at least 6 characters),
-refuses a username or e-mail that is already registered, and stores only a SHA-256
-hash of the password, never the password itself. A new account starts with a cash
-balance of 0 USD; money is added later with a deposit
-([wiki:UseCase0003Implementation UseCase0003]).
-
-Original use-case description (P3): [wiki:UseCase0001].
-Implementation: `server/auth.go`, function `Register` (the password hash is computed
-by `hashPassword` in the same file; the code is shown at the end of this page).
-
-== Scenario ==
-
- 1. '''Visitor''' chooses `[1] Register` in the anonymous menu (types `1`).
- 2. '''System''' prints `-- Register --` and asks, one prompt after another, for
-    `Username:`, `Email:`, `Full name:` and `Password (min 6 chars):`.
-
-The screenshot shows steps 1–2: option `1` is chosen and the first prompt
-(`Username:`) is waiting for input.
-
-[[Image(uc0001_1_register.png)]]
-
- 3. '''Visitor''' enters the values: `marko`, `marko@example.com`, `Marko Markovski`,
-    `secret1`.
- 4. '''System''' validates the input in Go, without accessing the database:
-   * username, e-mail and password must be non-empty, otherwise it prints
-     `Username, email and password are required.` and the scenario ends;
-   * the e-mail must contain `@` (see alternate flow 3a);
-   * the password must be at least 6 characters long, otherwise it prints
-     `Password must be at least 6 characters.` and the scenario ends.
- 5. '''System''' checks whether the username or the e-mail already exists
-    (`$1` = username, `$2` = e-mail):
-
-{{{
-SELECT EXISTS(SELECT 1 FROM users WHERE username = $1 OR email = $2)
-}}}
-
-If the result is `true`, alternate flow 5a applies.
-
- 6. '''System''' creates the account (`$1` = username, `$2` = e-mail, `$3` = full name,
-    `$4` = password hash). The hash is computed in Go by `hashPassword` as the
-    hex-encoded SHA-256 of the password — the same value that P3's
-    `encode(digest($4, 'sha256'), 'hex')` would produce in SQL. Hashing on the Go side
-    keeps it identical to the check done at login (SQL as in the code, only the Go
-    source indentation removed):
-
-{{{
-INSERT INTO users (username, email, full_name, password_hash, available_balance)
-VALUES ($1, $2, $3, $4, 0)
-}}}
-
- 7. '''System''' prints `Account created. You can now log in.` and returns to the
-    anonymous menu; the Visitor can continue with
-    [wiki:UseCase0002Implementation UseCase0002].
-
-The screenshot shows steps 3–7 of the successful attempt (bottom half): the
-entered values, the confirmation and the anonymous menu again. The top half is the
-earlier rejected attempt from alternate flow 3a.
-
-[[Image(uc0001_3_7_created.png)]]
-
-All statements are run on the `project` schema: the connection sets
-`search_path=project,public` (`server/db/db.go`), so `users` means `project.users`.
-
-=== Alternate flow 3a — invalid e-mail ===
-
-In step 3 the Visitor entered `marko.example.com` (no `@`). The check in step 4
-fails, the system prints `Invalid email.` and no SQL statement is executed. In the
-prototype the system then shows the anonymous menu again and the Visitor chooses
-`[1] Register` once more, which returns the scenario to step 2.
-
-[[Image(uc0001_3a_invalid_email.png)]]
-
-=== Alternate flow 5a — duplicate username or e-mail ===
-
-After `marko` has been created, the Visitor tries to register again with username
-`marko`, e-mail `other@example.com`, full name `Marko Two`, password `secret2`. The
-query from step 5 (`$1` = `marko`, `$2` = `other@example.com`) returns `true`
-because the username is taken, so the system prints
-`Username or email already taken.`, does not run the `INSERT`, and the scenario
-ends in the anonymous menu.
-
-[[Image(uc0001_5a_duplicate.png)]]
-
-== How to reproduce ==
-
-{{{
-./eduberza -init      # optional: reset to a known state
-./eduberza
-# [1] Register: marko / marko.example.com / Marko Markovski / secret1  -> Invalid email.
-# [1] Register: marko / marko@example.com / Marko Markovski / secret1  -> Account created.
-# [1] Register: marko / other@example.com / Marko Two / secret2        -> Username or email already taken.
-}}}
-
-All three screenshots come from one real run of exactly these inputs.
-
-== Source code ==
-
-`server/auth.go` — `hashPassword` and `Register`:
-
-{{{
-func hashPassword(pw string) string {
-	sum := sha256.Sum256([]byte(pw))
-	return hex.EncodeToString(sum[:])
-}
-
-// Register - UC0001
-func Register() {
-	fmt.Println("\n-- Register --")
-	username := prompt("Username: ")
-	email := prompt("Email: ")
-	fullName := prompt("Full name: ")
-	pw := prompt("Password (min 6 chars): ")
-
-	if username == "" || email == "" || pw == "" {
-		fmt.Println("Username, email and password are required.")
-		return
-	}
-	if !strings.Contains(email, "@") {
-		fmt.Println("Invalid email.")
-		return
-	}
-	if len(pw) < 6 {
-		fmt.Println("Password must be at least 6 characters.")
-		return
-	}
-
-	var exists bool
-	err := db.DB.QueryRow(
-		`SELECT EXISTS(SELECT 1 FROM users WHERE username = $1 OR email = $2)`,
-		username, email,
-	).Scan(&exists)
-	if err != nil {
-		fmt.Println("Database error:", err)
-		return
-	}
-	if exists {
-		fmt.Println("Username or email already taken.")
-		return
-	}
-
-	_, err = db.DB.Exec(
-		`INSERT INTO users (username, email, full_name, password_hash, available_balance)
-		 VALUES ($1, $2, $3, $4, 0)`,
-		username, email, fullName, hashPassword(pw),
-	)
-	if err != nil {
-		fmt.Println("Failed to register:", err)
-		return
-	}
-	fmt.Println("Account created. You can now log in.")
-}
-}}}
Index: cs/P4-Prototype/wiki/UseCase0002Implementation.md
===================================================================
--- docs/P4-Prototype/wiki/UseCase0002Implementation.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,149 +1,0 @@
-= Use-case 0002 Implementation - Log in =
-
-'''Initiating actor:''' Visitor
-
-'''Other actors:''' —
-
-A registered user authenticates with a username and password so that the system
-treats all following actions as actions of that Trader. The system looks the user up
-by username and compares the stored password hash with the SHA-256 hash of the
-entered password. An unknown username and a wrong password give the same answer,
-`Invalid credentials.`, so the system does not reveal which usernames exist. After a
-successful login the user's id and username are kept in the in-process session and
-the authenticated (Trader) menu is shown, from which all other Trader use-cases
-start.
-
-Original use-case description (P3): [wiki:UseCase0002].
-Implementation: `server/auth.go`, functions `Login` and `authenticate` (password hash
-by `hashPassword`; the code is shown at the end of this page).
-
-== Scenario ==
-
- 1. '''Visitor''' chooses `[2] Login` in the anonymous menu (types `2`).
- 2. '''System''' prints `-- Login --` and asks for `Username:` and then `Password:`.
-
-The screenshot shows steps 1–2: option `2` is chosen and the `Username:` prompt
-is waiting for input.
-
-[[Image(uc0002_1_login.png)]]
-
- 3. '''Visitor''' enters the username and the password. (If either is empty, the
-    system prints `Username and password are required.` without accessing the
-    database.)
- 4. '''System''' looks up the user (`$1` = entered username):
-
-{{{
-SELECT id, password_hash FROM users WHERE username = $1
-}}}
-
- 5. If no row is returned (`sql.ErrNoRows` in Go), the '''System''' responds
-    `Invalid credentials.` and the scenario ends.
- 6. If a row is returned, the '''System''' compares the returned `password_hash` with
-    `hashPassword(entered password)` (hex-encoded SHA-256, computed in Go). On a
-    mismatch it responds `Invalid credentials.` and the scenario ends.
-
-The screenshot shows this failure path with an existing user and a wrong
-password: `alice` / `wrongpass`. The query from step 4 finds alice's row, the
-hash comparison of step 6 fails, and the system prints `Invalid credentials.` and
-returns to the anonymous menu. (An unknown username — step 5 — prints exactly the
-same message.)
-
-[[Image(uc0002_5_6_invalid.png)]]
-
- 7. On a match, the '''System''' stores the returned `id` and the username in the
-    session (`s.UserID`, `s.Username`), prints `Login successful.` and displays the
-    authenticated menu headed `--- Logged in as alice ---`.
-
-The screenshot shows steps 3–7 of the second, successful attempt with the seed
-credentials `alice` / `test123` (the first, rejected attempt is still visible at
-the top of the window).
-
-[[Image(uc0002_7_success.png)]]
-
-The query runs on the `project` schema (the connection sets
-`search_path=project,public`), so `users` means `project.users`.
-
-=== Alternate flow 4a (P3) — lookup combined with the live balance ===
-
-P3 describes an optional variant that checks the password in SQL and returns the
-balances in the same query. The P4 prototype does '''not''' use it: login always uses
-the query from step 4 with the hash comparison in Go, and the balances are read
-separately when the Trader asks for them (`[1] View balance`, see
-[wiki:UseCase0003Implementation UseCase0003]).
-
-== Seed credentials ==
-
-State after `./eduberza -init` (`server/db/data_load.sql`):
-
-||= Username =||= Password =||= Available balance =||= Invested balance =||= Holdings =||
-|| `alice` || `test123` || 8250.00 USD || 1750.00 USD || 0.5 ETH ||
-|| `bob` || `test123` || 5000.00 USD || 0.00 USD || — ||
-|| `charlie` || `test123` || 2500.00 USD || 0.00 USD || — ||
-
-== How to reproduce ==
-
-{{{
-./eduberza -init
-./eduberza
-# [2] Login: alice / wrongpass  -> Invalid credentials.
-# [2] Login: alice / test123    -> Login successful.  (authenticated menu)
-}}}
-
-The screenshots come from one real run of exactly these inputs.
-
-== Source code ==
-
-`server/auth.go` — `hashPassword`, `Login` and `authenticate`:
-
-{{{
-func hashPassword(pw string) string {
-	sum := sha256.Sum256([]byte(pw))
-	return hex.EncodeToString(sum[:])
-}
-}}}
-
-{{{
-// Login - UC0002
-func Login(s *Session) {
-	fmt.Println("\n-- Login --")
-	username := prompt("Username: ")
-	pw := prompt("Password: ")
-	if username == "" || pw == "" {
-		fmt.Println("Username and password are required.")
-		return
-	}
-
-	id, err := authenticate(username, pw)
-	if err != nil {
-		if errors.Is(err, errInvalidCreds) {
-			fmt.Println("Invalid credentials.")
-			return
-		}
-		fmt.Println("Login error:", err)
-		return
-	}
-	s.UserID = id
-	s.Username = username
-	fmt.Println("Login successful.")
-}
-
-var errInvalidCreds = errors.New("invalid credentials")
-
-func authenticate(username, pw string) (string, error) {
-	var id, stored string
-	err := db.DB.QueryRow(
-		`SELECT id, password_hash FROM users WHERE username = $1`,
-		username,
-	).Scan(&id, &stored)
-	if err == sql.ErrNoRows {
-		return "", errInvalidCreds
-	}
-	if err != nil {
-		return "", err
-	}
-	if stored != hashPassword(pw) {
-		return "", errInvalidCreds
-	}
-	return id, nil
-}
-}}}
Index: cs/P4-Prototype/wiki/UseCase0003Implementation.md
===================================================================
--- docs/P4-Prototype/wiki/UseCase0003Implementation.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,178 +1,0 @@
-= Use-case 0003 Implementation - Deposit virtual funds =
-
-'''Initiating actor:''' Trader
-
-'''Other actors:''' —
-
-A logged-in Trader tops up their virtual cash balance in USD. This is a
-simulation-only operation: no real money changes hands, the amount is simply added to
-the Trader's `available_balance`. Every deposit is also recorded in the ledger
-(`transactions`) so that it appears in the transaction history
-([wiki:UseCase0006Implementation UseCase0006]). The operation writes to two tables —
-the user row and the ledger — inside a single database transaction, so either both
-changes are stored or neither is. Non-numeric, zero or negative amounts are rejected
-before the database is touched.
-
-Original use-case description (P3): [wiki:UseCase0003].
-Implementation: `server/account.go`, function `Deposit`; the verification uses
-`ShowBalance` from the same file (the code is shown at the end of this page).
-
-Precondition: the Trader is logged in ([wiki:UseCase0002Implementation UseCase0002]);
-in the run below as `alice`, who starts from the seed state (available 8250.00 USD,
-invested 1750.00 USD).
-
-== Scenario ==
-
- 1. '''Trader''' chooses `[2] Deposit virtual funds` in the authenticated menu
-    (types `2`).
- 2. '''System''' prints `-- Deposit virtual funds --` and asks `Amount (USD):`.
-
-The screenshot shows steps 1–2: the login as alice, the authenticated menu, the
-choice `2` and the amount prompt waiting for input.
-
-[[Image(uc0003_1_2_deposit.png)]]
-
- 3. '''Trader''' enters an amount: `500`.
- 4. '''System''' validates the input in Go, without accessing the database: the text
-    must parse as a number (`strconv.ParseFloat`) and be greater than 0 (see
-    alternate flow 4a).
- 5. '''System''' opens one database transaction (`db.DB.Begin()`), increments the
-    balance, writes the ledger row and commits. Both statements run in this single
-    transaction; if either fails, the deferred `tx.Rollback()` undoes everything.
-    `BEGIN` and `COMMIT` are issued by Go's `Begin()` / `Commit()`; the two
-    statements are sent exactly as in the code:
-
-{{{
-BEGIN;
-
-UPDATE users
-    SET available_balance = available_balance + $1,
-        updated_at        = now()
-  WHERE id = $2;
-
-INSERT INTO transactions (user_id, type, amount, currency, description)
-VALUES ($1, 'deposit', $2, 'USD', 'Virtual deposit');
-
-COMMIT;
-}}}
-
-Parameters: placeholders are numbered per statement. In the `UPDATE`, `$1` is the
-amount (`500`) and `$2` the user id (`amt, s.UserID`); in the `INSERT` it is the
-other way round, `$1` is the user id and `$2` the amount (`s.UserID, amt`),
-matching the column order.
-
- 6. '''System''' confirms `Deposited 500.0000 USD.` and returns to the authenticated
-    menu.
-
-The screenshot shows steps 3–6: the entered amount `500`, the confirmation and the
-authenticated menu again.
-
-[[Image(uc0003_3_6_deposited.png)]]
-
-The statements run on the `project` schema (the connection sets
-`search_path=project,public`), so `users` and `transactions` mean `project.users`
-and `project.transactions`.
-
-=== Alternate flow 4a — invalid amount ===
-
-If the amount is not a number, or is zero or negative, the system prints
-`Invalid amount.`; no transaction is started and nothing is written. In the run the
-Trader first entered `-50`. In the prototype the system then shows the
-authenticated menu again and the Trader chooses `[2] Deposit virtual funds` once
-more, which returns the scenario to step 2.
-
-[[Image(uc0003_4a_invalid.png)]]
-
-== Verification ==
-
-Right after the deposit the Trader chooses `[1] View balance` (function
-`ShowBalance`), which runs (`$1` = user id):
-
-{{{
-SELECT available_balance, invested_balance FROM users WHERE id = $1
-}}}
-
-It prints `Available: 8750.0000 USD`, `Invested : 1750.0000 USD`,
-`Total    : 10500.0000 USD`. The available balance grew from the seed value 8250.00
-by exactly the 500.00 deposited (the rejected `-50` changed nothing), and
-`invested_balance` is untouched.
-
-[[Image(uc0003_verify_balance.png)]]
-
-== How to reproduce ==
-
-{{{
-./eduberza -init
-./eduberza
-# [2] Login: alice / test123
-# [2] Deposit virtual funds: -50   -> Invalid amount.
-# [2] Deposit virtual funds: 500   -> Deposited 500.0000 USD.
-# [1] View balance                 -> Available: 8750.0000 USD
-}}}
-
-All four screenshots come from one real run of exactly these inputs.
-
-== Source code ==
-
-`server/account.go` — `Deposit`, and `ShowBalance` used for the verification:
-
-{{{
-// ShowBalance prints the logged-in user's balances.
-func ShowBalance(s *Session) {
-	var avail, invested float64
-	err := db.DB.QueryRow(
-		`SELECT available_balance, invested_balance FROM users WHERE id = $1`,
-		s.UserID,
-	).Scan(&avail, &invested)
-	if err != nil {
-		fmt.Println("Error:", err)
-		return
-	}
-	fmt.Printf("\n  Available: %.4f USD\n", avail)
-	fmt.Printf("  Invested : %.4f USD\n", invested)
-	fmt.Printf("  Total    : %.4f USD\n", avail+invested)
-}
-
-// Deposit - UC0003
-// Transactional: updates users.available_balance and inserts a ledger row.
-func Deposit(s *Session) {
-	fmt.Println("\n-- Deposit virtual funds --")
-	amtStr := prompt("Amount (USD): ")
-	amt, err := strconv.ParseFloat(amtStr, 64)
-	if err != nil || amt <= 0 {
-		fmt.Println("Invalid amount.")
-		return
-	}
-
-	tx, err := db.DB.Begin()
-	if err != nil {
-		fmt.Println("Error:", err)
-		return
-	}
-	defer tx.Rollback()
-
-	if _, err := tx.Exec(
-		`UPDATE users
-		    SET available_balance = available_balance + $1,
-		        updated_at        = now()
-		  WHERE id = $2`,
-		amt, s.UserID,
-	); err != nil {
-		fmt.Println("Error:", err)
-		return
-	}
-	if _, err := tx.Exec(
-		`INSERT INTO transactions (user_id, type, amount, currency, description)
-		 VALUES ($1, 'deposit', $2, 'USD', 'Virtual deposit')`,
-		s.UserID, amt,
-	); err != nil {
-		fmt.Println("Error:", err)
-		return
-	}
-	if err := tx.Commit(); err != nil {
-		fmt.Println("Error:", err)
-		return
-	}
-	fmt.Printf("Deposited %.4f USD.\n", amt)
-}
-}}}
Index: cs/P4-Prototype/wiki/UseCase0004Implementation.md
===================================================================
--- docs/P4-Prototype/wiki/UseCase0004Implementation.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,481 +1,0 @@
-= Use-case 0004 Implementation - Place market BUY order =
-
-'''Initiating actor:''' Trader
-
-'''Other actors:''' Market Simulator (indirect — supplies the current price via `market_trades`).
-
-A logged-in Trader buys a crypto asset at the current market price. The Trader never
-types a symbol or an identifier: the system lists the active markets with their last
-price, numbered, and the Trader picks one by its number and then enters only the
-quantity. The system checks that the Trader has enough available cash for
-quantity × price and then, in one database transaction, records the order, moves the
-cash from available to invested, adds the crypto to the Trader's holding (recomputing
-the weighted-average entry price), writes a ledger entry and a market trade, and marks
-the order executed. The operation touches five tables (`orders`, `users`, `holdings`,
-`transactions`, `market_trades`) and either all of it succeeds or all of it is rolled
-back.
-
-Original use-case description (P3): [wiki:UseCase0004].
-Implementation: `server/trade.go`, function
-`PlaceOrder(s, "buy")` (with `upsertHoldingOnBuy` in the same file), which calls
-`ChooseMarket`, `ListMarkets`, `pickNumber` and `LatestPrice` from
-`server/market.go` (the code is shown at the end of this page).
-
-All statements run on the `project` schema: the connection sets
-`search_path=project,public` (`server/db/db.go`), so `orders` means `project.orders`.
-The SQL below is copied from the Go code; only the Go source indentation is removed,
-a `;` is added after each statement of the transaction, and `--` comments say what
-each `$n` placeholder is bound to.
-
-The run shown is user `alice` on the seed data (available 8250.00 USD, holding
-0.5 ETH bought at 3500), buying 0.01 BTC.
-
-== Scenario ==
-
- 1. '''Trader''' chooses `[4] Place market BUY order` in the authenticated menu (types `4`).
- 2. '''System''' prints `-- Place market buy order --` and lists all active markets,
-    numbered, with their last price (`ListMarkets`, called by `ChooseMarket`):
-
-{{{
-SELECT m.id, c.id, c.symbol, m.quote_currency,
-       COALESCE(lp.price, 0) AS price
-  FROM markets m
-  JOIN crypto  c  ON c.id = m.crypto_id
-  LEFT JOIN v_latest_prices lp ON lp.market_id = m.id
- WHERE m.is_active = true
- ORDER BY c.symbol
-}}}
-
-The rows are printed in this order as `1 ADA`, `2 BTC`, `3 DOGE`, `4 ETH`, `5 SOL`;
-Go keeps each row's market id and crypto id in memory, so the Trader only ever sees
-and types the list number. The system then asks `Market #:`.
-
-[[Image(uc0004_1_2_markets.png)]]
-
- 3. '''Trader''' picks the market by its number in the list: `2` (BTC/USD).
- 4. '''System''' takes the market id and crypto id of row 2 from the list (no further
-    lookup by symbol) and reads the latest price of that market (`LatestPrice`;
-    `$1` = the chosen market's id):
-
-{{{
-SELECT price FROM v_latest_prices WHERE market_id = $1
-}}}
-
-It prints `Latest price for BTC/USD = 67140.000000` and asks `Quantity:`.
-
-[[Image(uc0004_3_4_price.png)]]
-
- 5. '''Trader''' enters the quantity `0.01`.
- 6. '''System''' computes in Go notional = quantity × price = 0.01 × 67140 = 671.40 and
-    passes it to SQL as a parameter. It then runs one database transaction; the
-    statements below are in exactly the order `PlaceOrder` executes them for a buy:
-
-{{{
-BEGIN;
-
--- (a) record the order as 'open' — no trade has happened yet.
---     $1 = user id, $2 = market id, $3 = side (the Go variable side = 'buy'),
---     $4 = quantity (0.01), $5 = price (67140); the returned id is kept in Go.
-INSERT INTO orders (user_id, market_id, side, type, status, quantity, price)
- VALUES ($1, $2, $3, 'market', 'open', $4, $5)
- RETURNING id;
-
--- (b) lock the user's row and read the available cash. $1 = user id.
---     Go compares it with the notional; if it is smaller -> alternate flow 6a.
-SELECT available_balance FROM users WHERE id = $1 FOR UPDATE;
-
--- (c) move the notional from available to invested cash.
---     $1 = notional (671.40), $2 = user id.
-UPDATE users
-    SET available_balance = available_balance - $1,
-        invested_balance  = invested_balance  + $1,
-        updated_at        = now()
-  WHERE id = $2;
-
--- (d) add the crypto to the holding (upsertHoldingOnBuy), recomputing the
---     weighted-average entry price in the database. Every SET expression sees
---     the pre-update row, so holdings.quantity is still the old quantity.
---     $1 = user id, $2 = crypto id, $3 = quantity (0.01), $4 = price (67140).
-INSERT INTO holdings (user_id, crypto_id, quantity, avg_price, updated_at)
- VALUES ($1, $2, $3, $4, now())
- ON CONFLICT (user_id, crypto_id) DO UPDATE
-    SET avg_price  = (holdings.quantity * holdings.avg_price
-                       + EXCLUDED.quantity * EXCLUDED.avg_price)
-                     / (holdings.quantity + EXCLUDED.quantity),
-        quantity   = holdings.quantity + EXCLUDED.quantity,
-        updated_at = now();
-
--- (e) ledger entry. $1 = user id, $2 = -notional (-671.40), $3 = order id from (a),
---     $4 = description built in Go: 'Market buy 0.0100 BTC @ 67140.000000'.
-INSERT INTO transactions (user_id, type, amount, currency, related_order, description)
- VALUES ($1, 'buy', $2, 'USD', $3, $4);
-
--- (f) record the resulting market trade.
---     $1 = market id, $2 = price, $3 = quantity, $4 = side ('buy').
-INSERT INTO market_trades (market_id, executed_at, price, quantity, side, source)
- VALUES ($1, now(), $2, $3, $4, 'user');
-
--- (g) settle the order itself — it has now actually been filled. $1 = order id.
-UPDATE orders SET status = 'executed', executed_at = now() WHERE id = $1;
-
-COMMIT;
-}}}
-
-A buy never reserves crypto (only a sell does, see
-[wiki:UseCase0005Implementation UseCase0005]), so `holdings.reserved_quantity` is not
-touched and stays 0.
-
- 7. '''System''' confirms
-    `Order executed: buy 0.0100 BTC @ 67140.000000 (notional 671.4000 USD)` and shows
-    the authenticated menu again.
-
-The screenshot shows steps 5–7: the entered quantity, the confirmation and the menu.
-
-[[Image(uc0004_5_7_executed.png)]]
-
-=== Verification — portfolio after the buy ===
-
-Right after the buy the Trader chooses `[6] View portfolio`
-([wiki:UseCase0006Implementation UseCase0006]). It shows the new holding
-`BTC 0.0100` with average buy price and current price 67140.000000 (value 671.4000),
-the unchanged `ETH 0.5000` (average 3500, current 3520, unrealised P/L +10.0000),
-`Cash available : 7578.6000 USD` (= 8250.00 − 671.40), portfolio value 2431.4000 and
-net worth 10010.0000 USD. The `Reserved` column is 0.0000 on both rows — a buy never
-reserves anything.
-
-[[Image(uc0004_verify_portfolio.png)]]
-
-=== Alternate flow 6a — insufficient funds ===
-
-User `charlie` (seed data: 2500.00 USD available, no crypto) chooses `[4]`, picks
-market `2` (BTC, 67140.000000) and enters quantity `1`. Go computes the notional
-67140.00. In the transaction, statement (a) inserts the `open` order and statement (b)
-`SELECT available_balance FROM users WHERE id = $1 FOR UPDATE` returns 2500.00, which
-is less than the notional. `PlaceOrder` prints
-`Insufficient funds: need 67140.0000, have 2500.0000` and returns without running
-(c)–(g); the deferred `tx.Rollback()` undoes statement (a), so no order, no ledger
-entry and no balance change is left behind (after this run charlie has no row in
-`orders` and still 2500.00 USD available). The authenticated menu is shown again.
-
-[[Image(uc0004_6a_insufficient.png)]]
-
-=== Alternate flow 3a — number not in the list ===
-
-If in step 3 the Trader enters something that is not a number from 1 to the number of
-listed markets, `pickNumber` prints `Invalid choice, enter a number from 1 to 5.`, no
-further SQL is run and the authenticated menu is shown again (the same check is shown
-in [wiki:UseCase0007Implementation UseCase0007], alternate flow 12a). Likewise, a
-quantity that is not a positive number in step 5 prints `Invalid quantity.` before any
-transaction is opened.
-
-== Source code ==
-
-`server/trade.go` — `PlaceOrder` (buy and sell) and `upsertHoldingOnBuy`:
-
-{{{
-// PlaceOrder - UC0004 (buy) / UC0005 (sell)
-// Market order that executes immediately against the latest price.
-// 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" {
-		fmt.Println("Invalid side.")
-		return
-	}
-	fmt.Printf("\n-- Place market %s order --\n", side)
-
-	// buy: any market; sell: only what the user holds and can still sell
-	var m *Market
-	var err error
-	if side == "buy" {
-		m, err = ChooseMarket()
-	} else {
-		m, err = ChooseHolding(s)
-	}
-	if err != nil {
-		fmt.Println(err)
-		return
-	}
-	price, err := LatestPrice(m.ID)
-	if err != nil {
-		fmt.Println(err)
-		return
-	}
-	fmt.Printf("Latest price for %s/%s = %.6f\n", m.Symbol, m.Quote, price)
-
-	qtyStr := prompt("Quantity: ")
-	qty, err := strconv.ParseFloat(qtyStr, 64)
-	if err != nil || qty <= 0 {
-		fmt.Println("Invalid quantity.")
-		return
-	}
-	notional := qty * price
-
-	tx, err := db.DB.Begin()
-	if err != nil {
-		fmt.Println("Error:", err)
-		return
-	}
-	defer tx.Rollback()
-
-	// 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)
-		 VALUES ($1, $2, $3, 'market', 'open', $4, $5)
-		 RETURNING id`,
-		s.UserID, m.ID, side, qty, price,
-	).Scan(&orderID)
-	if err != nil {
-		fmt.Println("Error creating order:", err)
-		return
-	}
-
-	if side == "buy" {
-		// check balance
-		var avail float64
-		if err := tx.QueryRow(
-			`SELECT available_balance FROM users WHERE id = $1 FOR UPDATE`,
-			s.UserID).Scan(&avail); err != nil {
-			fmt.Println("Error:", err)
-			return
-		}
-		if avail < notional {
-			fmt.Printf("Insufficient funds: need %.4f, have %.4f\n", notional, avail)
-			return
-		}
-
-		// debit balance
-		if _, err := tx.Exec(
-			`UPDATE users
-			    SET available_balance = available_balance - $1,
-			        invested_balance  = invested_balance  + $1,
-			        updated_at        = now()
-			  WHERE id = $2`,
-			notional, s.UserID,
-		); err != nil {
-			fmt.Println("Error:", err)
-			return
-		}
-
-		// 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)
-			return
-		}
-
-		// ledger entry
-		if _, err := tx.Exec(
-			`INSERT INTO transactions (user_id, type, amount, currency, related_order, description)
-			 VALUES ($1, 'buy', $2, 'USD', $3, $4)`,
-			s.UserID, -notional, orderID,
-			fmt.Sprintf("Market buy %.4f %s @ %.6f", qty, m.Symbol, price),
-		); err != nil {
-			fmt.Println("Error:", err)
-			return
-		}
-	} else {
-		// 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, reserved_quantity, avg_price FROM holdings
-			  WHERE user_id = $1 AND crypto_id = $2 FOR UPDATE`,
-			s.UserID, m.CryptoID,
-		).Scan(&held, &reserved, &avgPrice)
-		if err != nil && err != sql.ErrNoRows {
-			fmt.Println("Error:", err)
-			return
-		}
-		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 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,
-		); err != nil {
-			fmt.Println("Error:", err)
-			return
-		}
-
-		// credit balance; reduce invested by cost basis (avg_price * qty)
-		costBasis := avgPrice * qty
-		if _, err := tx.Exec(
-			`UPDATE users
-			    SET available_balance = available_balance + $1,
-			        invested_balance  = GREATEST(invested_balance - $2, 0),
-			        updated_at        = now()
-			  WHERE id = $3`,
-			notional, costBasis, s.UserID,
-		); err != nil {
-			fmt.Println("Error:", err)
-			return
-		}
-
-		// ledger entry
-		if _, err := tx.Exec(
-			`INSERT INTO transactions (user_id, type, amount, currency, related_order, description)
-			 VALUES ($1, 'sell', $2, 'USD', $3, $4)`,
-			s.UserID, notional, orderID,
-			fmt.Sprintf("Market sell %.4f %s @ %.6f", qty, m.Symbol, price),
-		); err != nil {
-			fmt.Println("Error:", err)
-			return
-		}
-	}
-
-	// record the resulting market trade so the book reflects this fill
-	if _, err := tx.Exec(
-		`INSERT INTO market_trades (market_id, executed_at, price, quantity, side, source)
-		 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)
-		return
-	}
-
-	if err := tx.Commit(); err != nil {
-		fmt.Println("Commit error:", err)
-		return
-	}
-	fmt.Printf("Order executed: %s %.4f %s @ %.6f (notional %.4f USD)\n",
-		side, qty, m.Symbol, price, notional)
-}
-
-// upsertHoldingOnBuy creates or updates a holding using running weighted-average price.
-//
-// This is a single statement that relies on UNIQUE (user_id, crypto_id): the new
-// weighted average is recomputed by the database in numeric arithmetic rather
-// than in Go float64, and no separate SELECT ... FOR UPDATE round-trip is
-// needed because ON CONFLICT DO UPDATE locks the conflicting row itself.
-// Every SET expression sees the pre-update row, so `holdings.quantity` below is
-// still the old quantity while the average is being computed.
-func upsertHoldingOnBuy(tx *sql.Tx, userID, cryptoID string, qty, price float64) error {
-	_, err := tx.Exec(
-		`INSERT INTO holdings (user_id, crypto_id, quantity, avg_price, updated_at)
-		 VALUES ($1, $2, $3, $4, now())
-		 ON CONFLICT (user_id, crypto_id) DO UPDATE
-		    SET avg_price  = (holdings.quantity * holdings.avg_price
-		                       + EXCLUDED.quantity * EXCLUDED.avg_price)
-		                     / (holdings.quantity + EXCLUDED.quantity),
-		        quantity   = holdings.quantity + EXCLUDED.quantity,
-		        updated_at = now()`,
-		userID, cryptoID, qty, price,
-	)
-	return err
-}
-}}}
-
-`server/market.go` — `ListMarkets`, `pickNumber`, `ChooseMarket` and `LatestPrice`:
-
-{{{
-// ListMarkets prints all active markets, numbered, with their latest price,
-// and returns them in the printed order so a caller can pick one by number.
-func ListMarkets() []Market {
-	rows, err := db.DB.Query(`
-		SELECT m.id, c.id, c.symbol, m.quote_currency,
-		       COALESCE(lp.price, 0) AS price
-		  FROM markets m
-		  JOIN crypto  c  ON c.id = m.crypto_id
-		  LEFT JOIN v_latest_prices lp ON lp.market_id = m.id
-		 WHERE m.is_active = true
-		 ORDER BY c.symbol`)
-	if err != nil {
-		fmt.Println("Error:", err)
-		return nil
-	}
-	defer rows.Close()
-
-	fmt.Println()
-	fmt.Printf("  %-4s  %-8s  %-5s  %15s\n", "#", "Symbol", "Quote", "Last price")
-	fmt.Println("  -----------------------------------------")
-	var list []Market
-	for rows.Next() {
-		var m Market
-		var price float64
-		if err := rows.Scan(&m.ID, &m.CryptoID, &m.Symbol, &m.Quote, &price); err != nil {
-			fmt.Println("scan error:", err)
-			return nil
-		}
-		list = append(list, m)
-		fmt.Printf("  %-4d  %-8s  %-5s  %15.6f\n", len(list), m.Symbol, m.Quote, price)
-	}
-	return list
-}
-
-// pickNumber reads a 1-based choice from a list of n items.
-func pickNumber(label string, n int) (int, error) {
-	if n == 0 {
-		return 0, fmt.Errorf("Nothing to choose from.")
-	}
-	k, err := strconv.Atoi(prompt(label))
-	if err != nil || k < 1 || k > n {
-		return 0, fmt.Errorf("Invalid choice, enter a number from 1 to %d.", n)
-	}
-	return k - 1, nil
-}
-
-// ChooseMarket lists the active markets and lets the user pick one by its
-// number in the list.
-func ChooseMarket() (*Market, error) {
-	list := ListMarkets()
-	k, err := pickNumber("Market #: ", len(list))
-	if err != nil {
-		return nil, err
-	}
-	return &list[k], nil
-}
-
-// LatestPrice returns the last traded price on a market.
-func LatestPrice(marketID string) (float64, error) {
-	var price float64
-	err := db.DB.QueryRow(
-		`SELECT price FROM v_latest_prices WHERE market_id = $1`, marketID,
-	).Scan(&price)
-	if err == sql.ErrNoRows {
-		return 0, fmt.Errorf("no trades yet for this market")
-	}
-	return price, err
-}
-}}}
Index: cs/P4-Prototype/wiki/UseCase0005Implementation.md
===================================================================
--- docs/P4-Prototype/wiki/UseCase0005Implementation.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,547 +1,0 @@
-= Use-case 0005 Implementation - Place market SELL order =
-
-'''Initiating actor:''' Trader
-
-'''Other actors:''' Market Simulator (indirect — supplies the current price).
-
-A logged-in Trader sells part or all of a holding at the current market price. The
-Trader never types a symbol: the system lists only the cryptos the Trader holds and can
-still sell (the quantity not already reserved by an open sell order), numbered, with
-how much is held and how much is free, and the Trader picks one by its number and
-enters the quantity. In one database transaction the system records the order,
-reserves the crypto being sold and settles it, credits the proceeds to the Trader's
-available cash while reducing the invested cash by the cost basis, writes a ledger
-entry and a market trade, and marks the order executed. Cost basis is preserved, so
-the realised P/L can be reconstructed from the ledger.
-
-Original use-case description (P3): [wiki:UseCase0005].
-Implementation: `server/trade.go`, function
-`PlaceOrder(s, "sell")`, which calls `ChooseHolding`, `pickNumber` and `LatestPrice`
-from `server/market.go` (the code is shown at the end of this page).
-
-All statements run on the `project` schema (the connection sets
-`search_path=project,public` in `server/db/db.go`). The SQL below is copied from the
-Go code; only the Go source indentation is removed, a `;` is added after each
-statement of the transaction, and `--` comments say what each `$n` placeholder is
-bound to.
-
-The run shown is user `alice` right after the buy of
-[wiki:UseCase0004Implementation UseCase0004]: 7578.60 USD available, holdings
-0.01 BTC (bought at 67140) and 0.5 ETH (bought at 3500). She sells 0.2 ETH.
-
-== Reserve, then settle ==
-
-The crypto being sold is '''reserved''' (`holdings.reserved_quantity`) before it is
-removed from the position, and the sell check is against what is truly still free,
-`quantity - reserved_quantity`, not against the raw `quantity`, which would also count
-crypto already promised to another order. Because only market orders are implemented,
-an order settles in the same transaction it is placed in, so reserve and settle are two
-statements inside one commit; they stay logically distinct so that a future
-limit-order matcher, where an order would stay `open` until a ''later'' transaction fills
-it, needs a second transaction but no schema change.
-
-== Scenario ==
-
- 1. '''Trader''' chooses `[5] Place market SELL order` in the authenticated menu (types `5`).
- 2. '''System''' prints `-- Place market sell order --` and lists, numbered, only the
-    cryptos the Trader holds with some quantity still free to sell, with the quantity
-    held, the quantity free to sell and the last price (`ChooseHolding`;
-    `$1` = the logged-in user's id):
-
-{{{
-SELECT m.id, c.id, c.symbol, m.quote_currency,
-       h.quantity, h.quantity - h.reserved_quantity AS free,
-       COALESCE(lp.price, 0) AS price
-  FROM holdings h
-  JOIN crypto  c ON c.id = h.crypto_id
-  JOIN markets m ON m.crypto_id = c.id AND m.is_active = true
-  LEFT JOIN v_latest_prices lp ON lp.market_id = m.id
- WHERE h.user_id = $1
-   AND h.quantity - h.reserved_quantity > 0
- ORDER BY c.symbol
-}}}
-
-For alice it prints `1 BTC USD 0.0100 0.0100 67140.000000` and
-`2 ETH USD 0.5000 0.5000 3520.000000`, then asks `Holding #:`. Go keeps each row's
-market id and crypto id in memory; the Trader only types the list number. (If the
-query returns no row, the system prints `you hold no crypto that is free to sell`
-and the use-case ends.)
-
-[[Image(uc0005_1_2_holdings.png)]]
-
- 3. '''Trader''' picks the holding by its number in the list: `2` (ETH).
- 4. '''System''' takes the market id and crypto id of row 2 from the list and reads the
-    latest price of that market (`LatestPrice`; `$1` = the chosen market's id):
-
-{{{
-SELECT price FROM v_latest_prices WHERE market_id = $1
-}}}
-
-It prints `Latest price for ETH/USD = 3520.000000` and asks `Quantity:`.
-
-[[Image(uc0005_3_4_price.png)]]
-
- 5. '''Trader''' enters the quantity `0.2`.
- 6. '''System''' computes in Go notional = quantity × price = 0.2 × 3520 = 704.00 and
-    runs one database transaction; the statements are in exactly the order
-    `PlaceOrder` executes them for a sell. After statement (b) Go also computes the
-    cost basis = avg_price × quantity = 3500 × 0.2 = 700.00 from the locked holding row;
-    both values are passed to SQL as parameters.
-
-{{{
-BEGIN;
-
--- (a) record the order as 'open' — no trade has happened yet.
---     $1 = user id, $2 = market id, $3 = side (the Go variable side = 'sell'),
---     $4 = quantity (0.2), $5 = price (3520); the returned id is kept in Go.
-INSERT INTO orders (user_id, market_id, side, type, status, quantity, price)
- VALUES ($1, $2, $3, 'market', 'open', $4, $5)
- RETURNING id;
-
--- (b) lock the holding row and read what is held, what is already reserved and
---     the average entry price. $1 = user id, $2 = crypto id.
---     Go computes available = quantity - reserved_quantity (0.5 - 0 = 0.5);
---     if there is no row or available < quantity -> alternate flow 5a.
-SELECT quantity, reserved_quantity, avg_price FROM holdings
-  WHERE user_id = $1 AND crypto_id = $2 FOR UPDATE;
-
--- (c) reserve: committed to this order, not yet removed from the position.
---     $1 = quantity (0.2), $2 = user id, $3 = crypto id.
-UPDATE holdings
-    SET reserved_quantity = reserved_quantity + $1,
-        updated_at        = now()
-  WHERE user_id = $2 AND crypto_id = $3;
-
--- (d) settle: a market order fills immediately, so release the reservation and
---     remove the asset from the position in one step. Same parameters as (c).
-UPDATE holdings
-    SET quantity          = quantity - $1,
-        reserved_quantity = reserved_quantity - $1,
-        updated_at        = now()
-  WHERE user_id = $2 AND crypto_id = $3;
-
--- (e) credit the proceeds; reduce invested cash by the cost basis.
---     $1 = notional (704.00), $2 = cost basis (700.00), $3 = user id.
-UPDATE users
-    SET available_balance = available_balance + $1,
-        invested_balance  = GREATEST(invested_balance - $2, 0),
-        updated_at        = now()
-  WHERE id = $3;
-
--- (f) ledger entry. $1 = user id, $2 = notional (704.00), $3 = order id from (a),
---     $4 = description built in Go: 'Market sell 0.2000 ETH @ 3520.000000'.
-INSERT INTO transactions (user_id, type, amount, currency, related_order, description)
- VALUES ($1, 'sell', $2, 'USD', $3, $4);
-
--- (g) record the resulting market trade.
---     $1 = market id, $2 = price, $3 = quantity, $4 = side ('sell').
-INSERT INTO market_trades (market_id, executed_at, price, quantity, side, source)
- VALUES ($1, now(), $2, $3, $4, 'user');
-
--- (h) settle the order itself — it has now actually been filled. $1 = order id.
-UPDATE orders SET status = 'executed', executed_at = now() WHERE id = $1;
-
-COMMIT;
-}}}
-
- 7. '''System''' confirms
-    `Order executed: sell 0.2000 ETH @ 3520.000000 (notional 704.0000 USD)` and shows
-    the authenticated menu again.
-
-The screenshot shows steps 5–7: the entered quantity, the confirmation and the menu.
-
-[[Image(uc0005_5_7_executed.png)]]
-
-After this run the database holds for alice: ETH `quantity` 0.3000 with
-`reserved_quantity` 0.0000; `available_balance` 8282.60 (= 7578.60 + 704.00) and
-`invested_balance` 1721.40 (= 2421.40 − 700.00); a `sell` row in `transactions` with
-amount 704.0000 and description `Market sell 0.2000 ETH @ 3520.000000`; and the order
-with status `executed`. The realised P/L of this sell is notional − cost basis =
-704.00 − 700.00 = +4.00 USD.
-
-=== Alternate flow 5a — insufficient holding ===
-
-Right after the sell above, alice chooses `[5]` again. The list from step 2 now shows
-`2 ETH USD 0.3000 0.3000 3520.000000`. She picks `2` (ETH) and enters quantity `5`.
-In the transaction, statement (a) inserts the `open` order and statement (b) returns
-quantity 0.3000 and reserved_quantity 0.0000, so available = 0.3 < 5. `PlaceOrder`
-prints
-
-{{{
-Insufficient holding: trying to sell 5.0000, available 0.3000 (of 0.3000 held, 0.0000 reserved)
-}}}
-
-and returns without running (c)–(h); the deferred `tx.Rollback()` undoes statement (a)
-as well, so no order, no reservation and no ledger entry is left behind. The
-authenticated menu is shown again. The same message is printed if the holding row no
-longer exists (for example because it was sold out from another session after the
-list was shown).
-
-[[Image(uc0005_5a_insufficient.png)]]
-
-== Reserve and settle, step by step ==
-
-The CLI reserves and settles inside one transaction, so `reserved_quantity` is never
-nonzero ''outside'' a transaction. The intermediate state is shown by running statements
-(c) and (d) by hand in one `psql` transaction (which sees its own uncommitted
-writes) against alice's ETH holding after the scenario above (0.3 ETH), for a sell of
-0.1, and rolling back at the end so nothing is changed. Literal values replace the
-`$n` parameters; `:alice` and `:eth` are psql variables for
-`(SELECT id FROM users WHERE username = 'alice')` and
-`(SELECT id FROM crypto WHERE symbol = 'ETH')`:
-
-{{{
-BEGIN;
-SELECT quantity, reserved_quantity, quantity - reserved_quantity AS available
-  FROM holdings WHERE user_id = :alice AND crypto_id = :eth;
---  quantity | reserved_quantity | available
--- ----------+-------------------+-----------
---    0.3000 |            0.0000 |    0.3000
-
--- (c) reserve 0.1: the order is placed, no trade has happened yet
-UPDATE holdings SET reserved_quantity = reserved_quantity + 0.1, updated_at = now()
- WHERE user_id = :alice AND crypto_id = :eth;
-SELECT quantity, reserved_quantity, quantity - reserved_quantity AS available
-  FROM holdings WHERE user_id = :alice AND crypto_id = :eth;
---  quantity | reserved_quantity | available
--- ----------+-------------------+-----------
---    0.3000 |            0.1000 |    0.2000
-
--- (d) settle: the reservation is released and the asset removed
-UPDATE holdings SET quantity = quantity - 0.1, reserved_quantity = reserved_quantity - 0.1, updated_at = now()
- WHERE user_id = :alice AND crypto_id = :eth;
-SELECT quantity, reserved_quantity, quantity - reserved_quantity AS available
-  FROM holdings WHERE user_id = :alice AND crypto_id = :eth;
---  quantity | reserved_quantity | available
--- ----------+-------------------+-----------
---    0.2000 |            0.0000 |    0.2000
-ROLLBACK;
-}}}
-
-The middle state is what every other connection would see for as long as an order
-stayed `open` once limit orders exist: 0.1 ETH still owned but no longer free to sell.
-
-== Two concurrent sells ==
-
-A Trader must not be able to sell the same units twice from two sessions at once. Both
-sessions may have listed the holding as free (step 2 runs outside the transaction), so
-the protection is statement (b): `SELECT ... FOR UPDATE` locks the holding row, and a
-second transaction that reaches (b) waits until the first one commits, then reads the
-already reduced `quantity` before deciding.
-
-This was checked with two `psql` sessions running statements (b)–(d) against alice's
-0.3 ETH. Session A locked the row, reserved and settled 0.2 ETH and committed after a
-3-second pause; session B asked for the lock one second after A had taken it:
-
-{{{
-A: SELECT ... FOR UPDATE  ->  quantity 0.3000, reserved_quantity 0.0000
-A: reserve 0.2, settle 0.2, pg_sleep(3)
-B: 11:43:54  SELECT ... FOR UPDATE   -- blocks, A holds the row lock
-A: 11:43:56  COMMIT
-B: 11:43:56  lock granted  ->  quantity 0.1000, reserved_quantity 0.0000
-}}}
-
-Session B was blocked for the two seconds until A committed and then saw only
-0.1 ETH, so a second 0.2 ETH sell in B takes alternate flow 5a
-(`available 0.1000`) instead of selling units that no longer exist. (B was rolled
-back and alice's holding was restored to 0.3 ETH after the check.)
-
-== The constraint holds even if the application code did not ==
-
-`schema_creation.sql` declares
-`CHECK (reserved_quantity >= 0 AND reserved_quantity <= quantity)` on
-`holdings.reserved_quantity`, so an inconsistent reservation is impossible at the
-database level, independently of `trade.go` (run inside a transaction that was
-rolled back):
-
-{{{
-UPDATE holdings SET reserved_quantity = quantity + 1 WHERE user_id = :alice AND crypto_id = :eth;
-ERROR:  new row for relation "holdings" violates check constraint "holdings_check"
-}}}
-
-== Source code ==
-
-`server/trade.go` — `PlaceOrder` (buy and sell):
-
-{{{
-// PlaceOrder - UC0004 (buy) / UC0005 (sell)
-// Market order that executes immediately against the latest price.
-// 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" {
-		fmt.Println("Invalid side.")
-		return
-	}
-	fmt.Printf("\n-- Place market %s order --\n", side)
-
-	// buy: any market; sell: only what the user holds and can still sell
-	var m *Market
-	var err error
-	if side == "buy" {
-		m, err = ChooseMarket()
-	} else {
-		m, err = ChooseHolding(s)
-	}
-	if err != nil {
-		fmt.Println(err)
-		return
-	}
-	price, err := LatestPrice(m.ID)
-	if err != nil {
-		fmt.Println(err)
-		return
-	}
-	fmt.Printf("Latest price for %s/%s = %.6f\n", m.Symbol, m.Quote, price)
-
-	qtyStr := prompt("Quantity: ")
-	qty, err := strconv.ParseFloat(qtyStr, 64)
-	if err != nil || qty <= 0 {
-		fmt.Println("Invalid quantity.")
-		return
-	}
-	notional := qty * price
-
-	tx, err := db.DB.Begin()
-	if err != nil {
-		fmt.Println("Error:", err)
-		return
-	}
-	defer tx.Rollback()
-
-	// 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)
-		 VALUES ($1, $2, $3, 'market', 'open', $4, $5)
-		 RETURNING id`,
-		s.UserID, m.ID, side, qty, price,
-	).Scan(&orderID)
-	if err != nil {
-		fmt.Println("Error creating order:", err)
-		return
-	}
-
-	if side == "buy" {
-		// check balance
-		var avail float64
-		if err := tx.QueryRow(
-			`SELECT available_balance FROM users WHERE id = $1 FOR UPDATE`,
-			s.UserID).Scan(&avail); err != nil {
-			fmt.Println("Error:", err)
-			return
-		}
-		if avail < notional {
-			fmt.Printf("Insufficient funds: need %.4f, have %.4f\n", notional, avail)
-			return
-		}
-
-		// debit balance
-		if _, err := tx.Exec(
-			`UPDATE users
-			    SET available_balance = available_balance - $1,
-			        invested_balance  = invested_balance  + $1,
-			        updated_at        = now()
-			  WHERE id = $2`,
-			notional, s.UserID,
-		); err != nil {
-			fmt.Println("Error:", err)
-			return
-		}
-
-		// 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)
-			return
-		}
-
-		// ledger entry
-		if _, err := tx.Exec(
-			`INSERT INTO transactions (user_id, type, amount, currency, related_order, description)
-			 VALUES ($1, 'buy', $2, 'USD', $3, $4)`,
-			s.UserID, -notional, orderID,
-			fmt.Sprintf("Market buy %.4f %s @ %.6f", qty, m.Symbol, price),
-		); err != nil {
-			fmt.Println("Error:", err)
-			return
-		}
-	} else {
-		// 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, reserved_quantity, avg_price FROM holdings
-			  WHERE user_id = $1 AND crypto_id = $2 FOR UPDATE`,
-			s.UserID, m.CryptoID,
-		).Scan(&held, &reserved, &avgPrice)
-		if err != nil && err != sql.ErrNoRows {
-			fmt.Println("Error:", err)
-			return
-		}
-		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 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,
-		); err != nil {
-			fmt.Println("Error:", err)
-			return
-		}
-
-		// credit balance; reduce invested by cost basis (avg_price * qty)
-		costBasis := avgPrice * qty
-		if _, err := tx.Exec(
-			`UPDATE users
-			    SET available_balance = available_balance + $1,
-			        invested_balance  = GREATEST(invested_balance - $2, 0),
-			        updated_at        = now()
-			  WHERE id = $3`,
-			notional, costBasis, s.UserID,
-		); err != nil {
-			fmt.Println("Error:", err)
-			return
-		}
-
-		// ledger entry
-		if _, err := tx.Exec(
-			`INSERT INTO transactions (user_id, type, amount, currency, related_order, description)
-			 VALUES ($1, 'sell', $2, 'USD', $3, $4)`,
-			s.UserID, notional, orderID,
-			fmt.Sprintf("Market sell %.4f %s @ %.6f", qty, m.Symbol, price),
-		); err != nil {
-			fmt.Println("Error:", err)
-			return
-		}
-	}
-
-	// record the resulting market trade so the book reflects this fill
-	if _, err := tx.Exec(
-		`INSERT INTO market_trades (market_id, executed_at, price, quantity, side, source)
-		 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)
-		return
-	}
-
-	if err := tx.Commit(); err != nil {
-		fmt.Println("Commit error:", err)
-		return
-	}
-	fmt.Printf("Order executed: %s %.4f %s @ %.6f (notional %.4f USD)\n",
-		side, qty, m.Symbol, price, notional)
-}
-}}}
-
-`server/market.go` — `pickNumber`, `ChooseHolding` and `LatestPrice`:
-
-{{{
-// pickNumber reads a 1-based choice from a list of n items.
-func pickNumber(label string, n int) (int, error) {
-	if n == 0 {
-		return 0, fmt.Errorf("Nothing to choose from.")
-	}
-	k, err := strconv.Atoi(prompt(label))
-	if err != nil || k < 1 || k > n {
-		return 0, fmt.Errorf("Invalid choice, enter a number from 1 to %d.", n)
-	}
-	return k - 1, nil
-}
-
-// ChooseHolding lists only the markets the user can sell in — cryptos they
-// hold with some quantity still free (not reserved by an open sell order) —
-// with how much is held and free, and lets them pick one by number.
-func ChooseHolding(s *Session) (*Market, error) {
-	rows, err := db.DB.Query(`
-		SELECT m.id, c.id, c.symbol, m.quote_currency,
-		       h.quantity, h.quantity - h.reserved_quantity AS free,
-		       COALESCE(lp.price, 0) AS price
-		  FROM holdings h
-		  JOIN crypto  c ON c.id = h.crypto_id
-		  JOIN markets m ON m.crypto_id = c.id AND m.is_active = true
-		  LEFT JOIN v_latest_prices lp ON lp.market_id = m.id
-		 WHERE h.user_id = $1
-		   AND h.quantity - h.reserved_quantity > 0
-		 ORDER BY c.symbol`, s.UserID)
-	if err != nil {
-		return nil, err
-	}
-	defer rows.Close()
-
-	fmt.Println()
-	fmt.Printf("  %-4s  %-8s  %-5s  %12s  %12s  %15s\n", "#", "Symbol", "Quote", "Held", "Free to sell", "Last price")
-	fmt.Println("  -------------------------------------------------------------------")
-	var list []Market
-	for rows.Next() {
-		var m Market
-		var held, free, price float64
-		if err := rows.Scan(&m.ID, &m.CryptoID, &m.Symbol, &m.Quote, &held, &free, &price); err != nil {
-			return nil, err
-		}
-		list = append(list, m)
-		fmt.Printf("  %-4d  %-8s  %-5s  %12.4f  %12.4f  %15.6f\n", len(list), m.Symbol, m.Quote, held, free, price)
-	}
-	if len(list) == 0 {
-		return nil, fmt.Errorf("you hold no crypto that is free to sell")
-	}
-	k, err := pickNumber("Holding #: ", len(list))
-	if err != nil {
-		return nil, err
-	}
-	return &list[k], nil
-}
-
-// LatestPrice returns the last traded price on a market.
-func LatestPrice(marketID string) (float64, error) {
-	var price float64
-	err := db.DB.QueryRow(
-		`SELECT price FROM v_latest_prices WHERE market_id = $1`, marketID,
-	).Scan(&price)
-	if err == sql.ErrNoRows {
-		return 0, fmt.Errorf("no trades yet for this market")
-	}
-	return price, err
-}
-}}}
Index: cs/P4-Prototype/wiki/UseCase0006Implementation.md
===================================================================
--- docs/P4-Prototype/wiki/UseCase0006Implementation.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,250 +1,0 @@
-= Use-case 0006 Implementation - View portfolio and transaction history =
-
-'''Initiating actor:''' Trader
-
-'''Other actors:''' —
-
-A logged-in Trader inspects the current state of their account. The portfolio view
-lists every cryptocurrency the Trader holds with the quantity (also split into the
-part reserved by open sell orders and the part that is free to sell), the average
-buy price, the current market price, the market value and the unrealised
-profit/loss, followed by a totals row and a cash summary (cash available, portfolio
-value, net worth). The transaction history lists the Trader's last 20 ledger
-entries — deposits, buys and sells — newest first. Both are read-only: nothing in
-the database is changed.
-
-Original use-case description (P3): [wiki:UseCase0006].
-Implementation: `server/portfolio.go`, function `ShowPortfolio`, and
-`server/account.go`, function `ShowTransactions` (the code is shown at the end of
-this page).
-
-Precondition: the Trader is logged in ([wiki:UseCase0002Implementation UseCase0002]).
-The run below is alice's, after she bought 0.01 BTC
-([wiki:UseCase0004Implementation UseCase0004]) and sold 0.2 ETH
-([wiki:UseCase0005Implementation UseCase0005]) on top of the seed data (0.5 ETH,
-8250.00 USD cash).
-
-== Scenario ==
-
-=== Portfolio ===
-
- 1. '''Trader''' chooses `[6] View portfolio` in the authenticated menu (types `6`).
-    The menu is the one shown in [wiki:UseCase0002Implementation UseCase0002], step 7.
- 2. '''System''' queries the `v_portfolio` view (`$1` = user id):
-
-{{{
-SELECT symbol,
-       quantity,
-       COALESCE(reserved_quantity, 0),
-       COALESCE(available_quantity, quantity),
-       COALESCE(avg_price, 0),
-       COALESCE(current_price, 0),
-       COALESCE(market_value, 0),
-       COALESCE(unrealized_pnl, 0)
-  FROM v_portfolio
- WHERE user_id = $1 AND quantity > 0
- ORDER BY symbol
-}}}
-
- 3. '''System''' displays the rows and a `TOTAL` row (sums of the value and P/L columns,
-    computed in Go), then reads the cash balance for the summary (`$1` = user id):
-
-{{{
-SELECT available_balance, invested_balance FROM users WHERE id = $1
-}}}
-
-and prints `Cash available` (= `available_balance`), `Portfolio value`
-(= the total market value) and `Net worth` (= their sum).
-
-The screenshot shows the result of steps 2–3. The table is wider than the
-terminal window, so each long line wraps and the header row has scrolled out of
-the top of the window; the complete output of this run is reproduced below it.
-
-[[Image(uc0006_portfolio.png)]]
-
-{{{
-  Symbol        Quantity      Reserved     Available         Avg buy         Current           Value  Unrealised P/L
-  ------------------------------------------------------------------------------------------------------------------
-  BTC             0.0100        0.0000        0.0100    67140.000000    67140.000000        671.4000         +0.0000
-  ETH             0.3000        0.0000        0.3000     3500.000000     3520.000000       1056.0000         +6.0000
-  ------------------------------------------------------------------------------------------------------------------
-  TOTAL                                                                                    1727.4000         +6.0000
-
-  Cash available : 8282.6000 USD
-  Portfolio value: 1727.4000 USD
-  Net worth      : 10010.0000 USD
-}}}
-
-Checking the figures: ETH is 0.5 − 0.2 = 0.3 at an average buy price of 3500 and
-a current price of 3520, so value 1056.00 and P/L 0.3 × 20 = +6.00; BTC was just
-bought at the current price 67140, so its P/L is 0. Cash is
-8250.00 − 671.40 (buy) + 704.00 (sell) = 8282.60. `Reserved` is 0.0000 for both
-because the market orders executed immediately — a quantity is reserved only
-while a sell order is still open.
-
-=== Transaction history ===
-
- 1. '''Trader''' chooses `[7] View transaction history` in the authenticated menu
-    (types `7`).
- 2. '''System''' queries the last 20 ledger entries of the Trader (`$1` = user id) and
-    prints them, newest first (the time is shown as the first 19 characters of
-    `created_at`):
-
-{{{
-SELECT created_at, type, amount, currency, COALESCE(description, '')
-  FROM transactions
- WHERE user_id = $1
- ORDER BY created_at DESC
- LIMIT 20
-}}}
-
-The screenshot shows steps 1–2: the choice `7` and the four ledger rows of this
-run — the sell of 0.2 ETH (+704.0000 USD), the buy of 0.01 BTC (−671.4000 USD),
-and the two seed rows, the initial deposit of 10000.0000 USD and the seed buy of
-0.5 ETH (−1750.0000 USD). Buys are stored with a negative amount, deposits and
-sells with a positive one. The two seed rows were inserted by `data_load.sql` in
-one statement and have the same `created_at`, so their relative order is not
-determined by the `ORDER BY`.
-
-[[Image(uc0006_history.png)]]
-
-All statements run on the `project` schema (the connection sets
-`search_path=project,public`).
-
-=== Reference — how v_portfolio is defined ===
-
-From `server/db/schema_creation.sql`:
-
-{{{
-CREATE OR REPLACE VIEW project.v_portfolio AS
-SELECT h.user_id,
-       c.symbol,
-       h.quantity,
-       h.reserved_quantity,
-       (h.quantity - h.reserved_quantity) AS available_quantity,
-       h.avg_price,
-       lp.price                           AS current_price,
-       (h.quantity * lp.price)            AS market_value,
-       (h.quantity * (lp.price - h.avg_price)) AS unrealized_pnl
-FROM   project.holdings h
-JOIN   project.crypto   c ON c.id = h.crypto_id
-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;
-}}}
-
-== How to reproduce ==
-
-{{{
-./eduberza -init
-./eduberza
-# [2] Login: alice / test123
-# [4] buy 0.01 BTC, [5] sell 0.2 ETH   (UseCase0004 / UseCase0005)
-# [6] View portfolio
-# [7] View transaction history
-}}}
-
-Both screenshots come from one real run (portfolio and history taken right after
-the buy and sell runs).
-
-== Source code ==
-
-`server/portfolio.go` — `ShowPortfolio`:
-
-{{{
-// ShowPortfolio - UC0006
-// Uses the v_portfolio view to list holdings with current market value and P&L.
-func ShowPortfolio(s *Session) {
-	rows, err := db.DB.Query(
-		`SELECT symbol,
-		        quantity,
-		        COALESCE(reserved_quantity, 0),
-		        COALESCE(available_quantity, quantity),
-		        COALESCE(avg_price, 0),
-		        COALESCE(current_price, 0),
-		        COALESCE(market_value, 0),
-		        COALESCE(unrealized_pnl, 0)
-		   FROM v_portfolio
-		  WHERE user_id = $1 AND quantity > 0
-		  ORDER BY symbol`,
-		s.UserID,
-	)
-	if err != nil {
-		fmt.Println("Error:", err)
-		return
-	}
-	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.Println(header)
-	fmt.Println("  " + strings.Repeat("-", len(header)-2))
-
-	var totalValue, totalPnL float64
-	empty := true
-	for rows.Next() {
-		var sym string
-		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  %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
-		empty = false
-	}
-	if empty {
-		fmt.Println("  (no holdings yet)")
-		return
-	}
-	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
-	var avail, invested float64
-	_ = db.DB.QueryRow(
-		`SELECT available_balance, invested_balance FROM users WHERE id = $1`,
-		s.UserID,
-	).Scan(&avail, &invested)
-	fmt.Printf("\n  Cash available : %.4f USD\n", avail)
-	fmt.Printf("  Portfolio value: %.4f USD\n", totalValue)
-	fmt.Printf("  Net worth      : %.4f USD\n", avail+totalValue)
-}
-}}}
-
-`server/account.go` — `ShowTransactions`:
-
-{{{
-// ShowTransactions lists the last 20 ledger entries for the user.
-func ShowTransactions(s *Session) {
-	rows, err := db.DB.Query(
-		`SELECT created_at, type, amount, currency, COALESCE(description, '')
-		   FROM transactions
-		  WHERE user_id = $1
-		  ORDER BY created_at DESC
-		  LIMIT 20`,
-		s.UserID,
-	)
-	if err != nil {
-		fmt.Println("Error:", err)
-		return
-	}
-	defer rows.Close()
-
-	fmt.Println()
-	fmt.Printf("  %-20s  %-8s  %12s  %-3s  %s\n", "When", "Type", "Amount", "Cur", "Description")
-	fmt.Println("  " + strings.Repeat("-", 70))
-	for rows.Next() {
-		var when, typ, cur, desc string
-		var amt float64
-		if err := rows.Scan(&when, &typ, &amt, &cur, &desc); err != nil {
-			fmt.Println("scan error:", err)
-			return
-		}
-		fmt.Printf("  %-20s  %-8s  %12.4f  %-3s  %s\n", when[:19], typ, amt, cur, desc)
-	}
-}
-}}}
Index: cs/P4-Prototype/wiki/UseCase0007Implementation.md
===================================================================
--- docs/P4-Prototype/wiki/UseCase0007Implementation.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,366 +1,0 @@
-= Use-case 0007 Implementation - Manage watchlist =
-
-'''Initiating actor:''' Trader
-
-'''Other actors:''' —
-
-A logged-in Trader keeps a list of crypto assets they want to monitor, with the last
-price of each. The first time the watchlist is opened the system creates a default
-watchlist named "Favorites" for the Trader. From a sub-menu the Trader can list the
-watchlist, add a crypto or remove one. The Trader never types a symbol: for adding, the
-system lists, numbered, only the cryptos that are not on the watchlist yet, and for
-removing, only the cryptos that are on it; the Trader picks one by its number. Adding
-a crypto that is already on the list is a no-op (idempotent), and a number that is not
-in the list is refused without touching the database.
-
-Original use-case description (P3): [wiki:UseCase0007].
-Implementation: `server/watchlist.go`, functions
-`ManageWatchlist`, `ensureDefaultWatchlist`, `listWatchlist`, `addToWatchlist` and
-`removeFromWatchlist`, with `pickNumber` from
-`server/market.go` (the code is shown at the end of this page).
-
-All statements run on the `project` schema (the connection sets
-`search_path=project,public` in `server/db/db.go`). The SQL below is copied from the
-Go code; only the Go source indentation is removed.
-
-The run shown is user `alice` on the seed data, whose watchlist contains BTC, ETH and
-SOL. She lists it, adds ADA, tries to remove a number that is not in the list, removes
-SOL and lists the result.
-
-== Scenario ==
-
- 1. '''Trader''' chooses `[8] Manage watchlist` in the authenticated menu (types `8`).
- 2. '''System''' makes sure the Trader has a watchlist and takes the id of the oldest one
-    (`ensureDefaultWatchlist`; `$1` = the logged-in user's id):
-
-{{{
-SELECT id FROM watchlists WHERE user_id = $1 ORDER BY created_at LIMIT 1
-}}}
-
-Only if this returns no row, it creates the default watchlist and uses its id:
-
-{{{
-INSERT INTO watchlists (user_id, name) VALUES ($1, 'Favorites') RETURNING id
-}}}
-
-(alice already has the seed watchlist "Favorites", so only the `SELECT` runs.) The
-watchlist id is kept in Go and used as `$1` in all statements below.
-
- 3. '''System''' shows the sub-menu `-- Watchlist --` with `[1] List items`,
-    `[2] Add crypto`, `[3] Remove crypto` and `[0] Back`.
-
-[[Image(uc0007_1_3_menu.png)]]
-
-=== List items ===
-
- 4. '''Trader''' chooses `[1] List items`.
- 5. '''System''' lists the cryptos on the watchlist with their last price against USD
-    (`listWatchlist`; `$1` = watchlist id):
-
-{{{
-SELECT c.symbol, c.name, COALESCE(lp.price, 0)
-  FROM watchlist_items wi
-  JOIN crypto  c  ON c.id = wi.crypto_id
-  LEFT JOIN markets       m  ON m.crypto_id = c.id AND m.quote_currency = 'USD'
-  LEFT JOIN v_latest_prices lp ON lp.market_id = m.id
- WHERE wi.watchlist_id = $1
- ORDER BY c.symbol
-}}}
-
-For alice it prints `BTC Bitcoin 67140.000000`, `ETH Ethereum 3520.000000` and
-`SOL Solana 166.100000` (an empty watchlist prints `(watchlist is empty)`), then
-shows the sub-menu again.
-
-[[Image(uc0007_list.png)]]
-
-=== Add a crypto ===
-
- 6. '''Trader''' chooses `[2] Add crypto`.
- 7. '''System''' lists, numbered, the cryptos that are not on the watchlist yet
-    (`addToWatchlist`; `$1` = watchlist id):
-
-{{{
-SELECT c.id, c.symbol, c.name
-  FROM crypto c
- WHERE NOT EXISTS (SELECT 1 FROM watchlist_items wi
-                    WHERE wi.watchlist_id = $1 AND wi.crypto_id = c.id)
- ORDER BY c.symbol
-}}}
-
-For alice it prints `1 ADA Cardano` and `2 DOGE Dogecoin` and asks
-`Crypto # to add:`. Go keeps each row's crypto id in memory. (If every crypto is
-already on the watchlist, it prints `Every crypto is already on your watchlist.`
-instead.)
-
-[[Image(uc0007_add_1_list.png)]]
-
- 8. '''Trader''' picks the crypto by its number in the list: `1` (ADA).
- 9. '''System''' adds the crypto of row 1 to the watchlist (`$1` = watchlist id,
-    `$2` = the chosen crypto's id); thanks to the unique constraint and
-    `ON CONFLICT ... DO NOTHING`, adding a crypto that is already there changes
-    nothing:
-
-{{{
-INSERT INTO watchlist_items (watchlist_id, crypto_id)
- VALUES ($1, $2)
- ON CONFLICT (watchlist_id, crypto_id) DO NOTHING
-}}}
-
-It prints `Added ADA.` and shows the sub-menu again.
-
-[[Image(uc0007_add_2_added.png)]]
-
-=== Remove a crypto ===
-
- 10. '''Trader''' chooses `[3] Remove crypto`.
- 11. '''System''' lists, numbered, the cryptos that are on the watchlist
-     (`removeFromWatchlist`; `$1` = watchlist id):
-
-{{{
-SELECT c.id, c.symbol, c.name
-  FROM watchlist_items wi
-  JOIN crypto c ON c.id = wi.crypto_id
- WHERE wi.watchlist_id = $1
- ORDER BY c.symbol
-}}}
-
-For alice it now prints `1 ADA Cardano`, `2 BTC Bitcoin`, `3 ETH Ethereum` and
-`4 SOL Solana` and asks `Crypto # to remove:`. Go keeps each row's crypto id in
-memory. (If the watchlist is empty, it prints `Your watchlist is empty.` instead.)
-
-[[Image(uc0007_remove_1_list.png)]]
-
- 12. '''Trader''' picks the crypto by its number in the list: `4` (SOL).
- 13. '''System''' removes the crypto of row 4 from the watchlist (`$1` = watchlist id,
-     `$2` = the chosen crypto's id):
-
-{{{
-DELETE FROM watchlist_items WHERE watchlist_id = $1 AND crypto_id = $2
-}}}
-
-It prints `Removed SOL.` and shows the sub-menu again.
-
-[[Image(uc0007_remove_2_removed.png)]]
-
-==== Alternate flow 12a — number not in the list ====
-
-Before removing SOL, alice first chose `[3] Remove crypto` and, at step 12, entered `5`
-while only numbers 1–4 were listed. `pickNumber` prints
-`Invalid choice, enter a number from 1 to 4.`, the `DELETE` is not run and the sub-menu
-is shown again; she then chose `[3]` once more, which returned the scenario to step 11.
-The same check applies to the number entered at step 8.
-
-[[Image(uc0007_remove_invalid.png)]]
-
-=== Verification — list after the changes ===
-
-Choosing `[1] List items` again runs the query from step 5, which now returns
-`ADA Cardano 0.453750`, `BTC Bitcoin 67140.000000` and `ETH Ethereum 3520.000000`:
-ADA was added and SOL removed. `[0] Back` returns to the authenticated menu.
-
-[[Image(uc0007_list_after.png)]]
-
-== Source code ==
-
-`server/watchlist.go` — `ManageWatchlist`, `ensureDefaultWatchlist`, `listWatchlist`, `addToWatchlist` and `removeFromWatchlist`:
-
-{{{
-// ManageWatchlist - UC0007
-// Ensures the user has a default watchlist, then allows listing, adding,
-// removing entries.
-func ManageWatchlist(s *Session) {
-	wlID, err := ensureDefaultWatchlist(s.UserID)
-	if err != nil {
-		fmt.Println("Error:", err)
-		return
-	}
-	for {
-		fmt.Println("\n-- Watchlist --")
-		fmt.Println("[1] List items")
-		fmt.Println("[2] Add crypto")
-		fmt.Println("[3] Remove crypto")
-		fmt.Println("[0] Back")
-		switch prompt("> ") {
-		case "1":
-			listWatchlist(wlID)
-		case "2":
-			addToWatchlist(wlID)
-		case "3":
-			removeFromWatchlist(wlID)
-		case "0":
-			return
-		default:
-			fmt.Println("Unknown option.")
-		}
-	}
-}
-
-func ensureDefaultWatchlist(userID string) (string, error) {
-	var id string
-	err := db.DB.QueryRow(
-		`SELECT id FROM watchlists WHERE user_id = $1 ORDER BY created_at LIMIT 1`,
-		userID,
-	).Scan(&id)
-	if err == sql.ErrNoRows {
-		err = db.DB.QueryRow(
-			`INSERT INTO watchlists (user_id, name) VALUES ($1, 'Favorites') RETURNING id`,
-			userID,
-		).Scan(&id)
-		return id, err
-	}
-	return id, err
-}
-
-func listWatchlist(wlID string) {
-	rows, err := db.DB.Query(`
-		SELECT c.symbol, c.name, COALESCE(lp.price, 0)
-		  FROM watchlist_items wi
-		  JOIN crypto  c  ON c.id = wi.crypto_id
-		  LEFT JOIN markets       m  ON m.crypto_id = c.id AND m.quote_currency = 'USD'
-		  LEFT JOIN v_latest_prices lp ON lp.market_id = m.id
-		 WHERE wi.watchlist_id = $1
-		 ORDER BY c.symbol`, wlID)
-	if err != nil {
-		fmt.Println("Error:", err)
-		return
-	}
-	defer rows.Close()
-
-	fmt.Println()
-	fmt.Printf("  %-8s  %-20s  %15s\n", "Symbol", "Name", "Last price")
-	fmt.Println("  --------------------------------------------------")
-	empty := true
-	for rows.Next() {
-		var sym, name string
-		var price float64
-		if err := rows.Scan(&sym, &name, &price); err != nil {
-			fmt.Println("scan error:", err)
-			return
-		}
-		fmt.Printf("  %-8s  %-20s  %15.6f\n", sym, name, price)
-		empty = false
-	}
-	if empty {
-		fmt.Println("  (watchlist is empty)")
-	}
-}
-
-// addToWatchlist lists the cryptos that are not on the watchlist yet,
-// numbered, and adds the one the user picks.
-func addToWatchlist(wlID string) {
-	rows, err := db.DB.Query(`
-		SELECT c.id, c.symbol, c.name
-		  FROM crypto c
-		 WHERE NOT EXISTS (SELECT 1 FROM watchlist_items wi
-		                    WHERE wi.watchlist_id = $1 AND wi.crypto_id = c.id)
-		 ORDER BY c.symbol`, wlID)
-	if err != nil {
-		fmt.Println("Error:", err)
-		return
-	}
-	type option struct{ id, symbol, name string }
-	var list []option
-	for rows.Next() {
-		var o option
-		if err := rows.Scan(&o.id, &o.symbol, &o.name); err != nil {
-			rows.Close()
-			fmt.Println("scan error:", err)
-			return
-		}
-		list = append(list, o)
-	}
-	rows.Close()
-	if len(list) == 0 {
-		fmt.Println("Every crypto is already on your watchlist.")
-		return
-	}
-	fmt.Println()
-	fmt.Printf("  %-4s  %-8s  %s\n", "#", "Symbol", "Name")
-	fmt.Println("  ------------------------------")
-	for i, o := range list {
-		fmt.Printf("  %-4d  %-8s  %s\n", i+1, o.symbol, o.name)
-	}
-	k, err := pickNumber("Crypto # to add: ", len(list))
-	if err != nil {
-		fmt.Println(err)
-		return
-	}
-	_, err = db.DB.Exec(
-		`INSERT INTO watchlist_items (watchlist_id, crypto_id)
-		 VALUES ($1, $2)
-		 ON CONFLICT (watchlist_id, crypto_id) DO NOTHING`,
-		wlID, list[k].id,
-	)
-	if err != nil {
-		fmt.Println("Error:", err)
-		return
-	}
-	fmt.Printf("Added %s.\n", list[k].symbol)
-}
-
-// removeFromWatchlist lists the watchlist's cryptos, numbered, and removes
-// the one the user picks.
-func removeFromWatchlist(wlID string) {
-	rows, err := db.DB.Query(`
-		SELECT c.id, c.symbol, c.name
-		  FROM watchlist_items wi
-		  JOIN crypto c ON c.id = wi.crypto_id
-		 WHERE wi.watchlist_id = $1
-		 ORDER BY c.symbol`, wlID)
-	if err != nil {
-		fmt.Println("Error:", err)
-		return
-	}
-	type option struct{ id, symbol, name string }
-	var list []option
-	for rows.Next() {
-		var o option
-		if err := rows.Scan(&o.id, &o.symbol, &o.name); err != nil {
-			rows.Close()
-			fmt.Println("scan error:", err)
-			return
-		}
-		list = append(list, o)
-	}
-	rows.Close()
-	if len(list) == 0 {
-		fmt.Println("Your watchlist is empty.")
-		return
-	}
-	fmt.Println()
-	fmt.Printf("  %-4s  %-8s  %s\n", "#", "Symbol", "Name")
-	fmt.Println("  ------------------------------")
-	for i, o := range list {
-		fmt.Printf("  %-4d  %-8s  %s\n", i+1, o.symbol, o.name)
-	}
-	k, err := pickNumber("Crypto # to remove: ", len(list))
-	if err != nil {
-		fmt.Println(err)
-		return
-	}
-	if _, err := db.DB.Exec(
-		`DELETE FROM watchlist_items WHERE watchlist_id = $1 AND crypto_id = $2`,
-		wlID, list[k].id,
-	); err != nil {
-		fmt.Println("Error:", err)
-		return
-	}
-	fmt.Printf("Removed %s.\n", list[k].symbol)
-}
-}}}
-
-`server/market.go` — `pickNumber`:
-
-{{{
-// pickNumber reads a 1-based choice from a list of n items.
-func pickNumber(label string, n int) (int, error) {
-	if n == 0 {
-		return 0, fmt.Errorf("Nothing to choose from.")
-	}
-	k, err := strconv.Atoi(prompt(label))
-	if err != nil || k < 1 || k > n {
-		return 0, fmt.Errorf("Invalid choice, enter a number from 1 to %d.", n)
-	}
-	return k - 1, nil
-}
-}}}
Index: cs/P5-Normalization/Normalization.md
===================================================================
--- docs/P5-Normalization/Normalization.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,731 +1,0 @@
-# Normalization
-
-This phase does not use the relations of
-[RelationalDesign](../P2-RelationalDesign/RelationalDesign.md) (P2) as a starting point.
-It starts from the attributes of [ERModel](../P1-ConceptualModel/ERModel.md) **v05** (P1),
-put into one de-normalized relation. It states the functional dependencies that the
-model's rules impose on those attributes, **computes** the keys of that relation from the
-dependencies, and then decomposes it step by step through 2NF, 3NF and BCNF. Every step is
-checked for a lossless join and for dependency preservation. The
-[final section](#final-result-and-discussion) compares the result with P2.
-
-## De-normalized database form
-
-### Which attributes go into the relation
-
-The relation contains **the attributes of the ER model and nothing else**. In v05 all
-attributes belong to the 11 entity sets. None of the 15 relationships has attributes of its
-own.
-
-A relationship adds **no column**. Foreign-key columns such as `crypto_id` or `watchlist_id`
-belong to the relational model of P2, not to the ER model, so they do not appear here. What a
-relationship contributes is a **functional dependency** between attributes that are already
-in the relation. For example, `Contains` (Watchlists 1 : N WatchlistItems) says that every
-watchlist item is on exactly one watchlist, which is the dependency `WI_ID → W_ID` in the
-next section. It is not a column `WI_WATCHLIST_ID`.
-
-Attribute names are prefixed with the entity set they come from, because several names repeat
-across the model (`id`, `created_at`, `quantity`, `type`, `name`, `price`, `side`), and one
-relation cannot contain the same name twice.
-
-| Prefix | Entity set (P1) | Attributes |
-|---|---|---|
-| `U_`  | Users          | `U_ID, U_USERNAME, U_EMAIL, U_FULL_NAME, U_PASSWORD_HASH, U_AVAILABLE_BALANCE, U_INVESTED_BALANCE, U_RESERVED_BALANCE, U_CREATED_AT, U_UPDATED_AT` |
-| `C_`  | Cryptos        | `C_ID, C_SYMBOL, C_NAME, C_CREATED_AT` |
-| `M_`  | Markets        | `M_ID, M_QUOTE_CURRENCY, M_IS_ACTIVE, M_CREATED_AT` |
-| `H_`  | Holdings       | `H_ID, H_QUANTITY, H_RESERVED_QUANTITY, H_AVG_PRICE, H_CREATED_AT, H_UPDATED_AT` |
-| `O_`  | Orders         | `O_ID, O_SIDE, O_TYPE, O_STATUS, O_QUANTITY, O_FILLED_QUANTITY, O_PRICE, O_PLACED_AT, O_EXECUTED_AT` |
-| `T_`  | Transactions   | `T_ID, T_TYPE, T_AMOUNT, T_CURRENCY, T_CREATED_AT, T_DESCRIPTION` |
-| `MT_` | MarketTrades   | `MT_ID, MT_EXECUTED_AT, MT_PRICE, MT_QUANTITY, MT_SIDE, MT_SOURCE` |
-| `OE_` | OrderEvents    | `OE_ID, OE_EVENT_TYPE, OE_QUANTITY, OE_PRICE, OE_STATUS_AFTER, OE_CREATED_AT` |
-| `MC_` | MarketCandles  | `MC_ID, MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME, MC_CANDLE_TIME` |
-| `W_`  | Watchlists     | `W_ID, W_NAME, W_CREATED_AT` |
-| `WI_` | WatchlistItems | `WI_ID, WI_ADDED_AT` |
-
-That is 64 attributes. **One case needs two more.** `FillsBuy` and `FillsSell` are two
-different relationships between the same two entity sets, Orders and MarketTrades. A trade
-can fill one buy order *and* one sell order, which are two different orders. One relation
-has only one `O_ID` column, and one column cannot hold two different orders in the same
-tuple. So the order's identifier appears once per **role**, named after the relationship
-that gives the role:
-
-| Attribute | Meaning |
-|---|---|
-| `O_ID_FILLSBUY`  | the `id` of Orders, in its role in `FillsBuy` (the buy order a trade filled) |
-| `O_ID_FILLSSELL` | the `id` of Orders, in its role in `FillsSell` (the sell order a trade filled) |
-
-These are not foreign keys copied from P2. They are the ER attribute `Orders.id` itself, once
-for each of the two relationships. [ERModel](../P1-ConceptualModel/ERModel.md) names these two
-roles of `Orders` explicitly: *the buy order* of a trade in `FillsBuy`, and *the sell order* in
-`FillsSell`. This is the only place where the model has two
-relationships between the same pair of entity sets. Every other relationship is expressed with
-the attributes above, without renaming.
-
-This gives **one relation, `R_EDUBERZA`, of 66 attributes:**
-
-```
-R_EDUBERZA(
-  U_ID, U_USERNAME, U_EMAIL, U_FULL_NAME, U_PASSWORD_HASH, U_AVAILABLE_BALANCE,
-  U_INVESTED_BALANCE, U_RESERVED_BALANCE, U_CREATED_AT, U_UPDATED_AT,
-  C_ID, C_SYMBOL, C_NAME, C_CREATED_AT,
-  M_ID, M_QUOTE_CURRENCY, M_IS_ACTIVE, M_CREATED_AT,
-  H_ID, H_QUANTITY, H_RESERVED_QUANTITY, H_AVG_PRICE, H_CREATED_AT, H_UPDATED_AT,
-  O_ID, O_SIDE, O_TYPE, O_STATUS, O_QUANTITY, O_FILLED_QUANTITY, O_PRICE,
-  O_PLACED_AT, O_EXECUTED_AT,
-  T_ID, T_TYPE, T_AMOUNT, T_CURRENCY, T_CREATED_AT, T_DESCRIPTION,
-  MT_ID, MT_EXECUTED_AT, MT_PRICE, MT_QUANTITY, MT_SIDE, MT_SOURCE,
-  O_ID_FILLSBUY, O_ID_FILLSSELL,
-  OE_ID, OE_EVENT_TYPE, OE_QUANTITY, OE_PRICE, OE_STATUS_AFTER, OE_CREATED_AT,
-  MC_ID, MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME, MC_CANDLE_TIME,
-  W_ID, W_NAME, W_CREATED_AT,
-  WI_ID, WI_ADDED_AT
-)
-```
-
-To keep the tables below readable, **`X_*`** means the non-identifier attributes of prefix
-`X_`. For example, `U_*` = `U_USERNAME … U_UPDATED_AT` (9 attributes), and `O_*` =
-`O_SIDE … O_EXECUTED_AT` (8 attributes). `U_ID`, `O_ID`, … are always written out.
-
-Every attribute is single-valued and atomic (a balance, a timestamp, a symbol, an amount —
-nothing here is a list or a nested record), so `R_EDUBERZA` satisfies 1NF as soon as it is
-written down.
-
-## Functional dependencies
-
-At this point `R_EDUBERZA` is just a set of attributes. It has **no keys yet**. `U_ID`,
-`O_ID`, … are ordinary attributes of this relation, and which attribute sets are keys of
-`R_EDUBERZA` is computed in the [next section](#candidate-keys-and-primary-key), from the
-dependencies below. Each dependency is justified by a rule of the domain, as described in
-the data requirements of [ERModel](../P1-ConceptualModel/ERModel.md). The rules are of four
-kinds:
-
-- **(I) Identification.** Every value of an identifier (`U_ID`, `C_ID`, …) is given to
-  exactly one real object: one user, one crypto, one order. That object has exactly one
-  username, one balance, one price, and so on. So the identifier's value fixes those values.
-- **(R) 1:N relationship.** In a 1:N relationship, each object on the N side is linked to
-  exactly one object on the 1 side. So the N side's identifier fixes the 1 side's
-  identifier. Example: an order is placed by exactly one user (`Places`), so `O_ID → U_ID`.
-  The opposite direction does not hold: a user places many orders, so `U_ID ↛ O_ID`.
-- **(U) Uniqueness rule.** A rule of the form "at most one X per Y and Z" gives
-  `Y, Z → X`.
-- **(N) Unique natural attribute.** No two users share a username or an email, and no two
-  cryptos share a symbol.
-
-**Only rules of the ER model are used.** The dependencies below come from the rules stated in
-[ERModel](../P1-ConceptualModel/ERModel.md) v05 and nothing else. The analysis uses the
-classical definitions (Armstrong's axioms), with no special treatment of `NULL`. Partial
-relationships (`Settles`, `FillsBuy`, `FillsSell`) are discussed where they matter:
-under [Canonical cover](#canonical-cover) and in the [discussion](#discussion).
-
-| # | Functional dependency | Rule | Why it holds |
-|---|---|---|---|
-| FD1  | `U_ID → U_USERNAME, U_EMAIL, U_FULL_NAME, U_PASSWORD_HASH, U_AVAILABLE_BALANCE, U_INVESTED_BALANCE, U_RESERVED_BALANCE, U_CREATED_AT, U_UPDATED_AT` | I | one user, one value of each |
-| FD2  | `U_USERNAME → U_ID` | N | usernames are unique |
-| FD3  | `U_EMAIL → U_ID` | N | emails are unique |
-| FD4  | `C_ID → C_SYMBOL, C_NAME, C_CREATED_AT` | I | one crypto, one value of each |
-| FD5  | `C_SYMBOL → C_ID` | N | symbols are unique |
-| FD6  | `M_ID → M_QUOTE_CURRENCY, M_IS_ACTIVE, M_CREATED_AT, C_ID` | I, R | …and a market is `QuotedOn` exactly one crypto |
-| FD7  | `C_ID, M_QUOTE_CURRENCY → M_ID` | U | a crypto is quoted at most once per currency |
-| FD8  | `H_ID → H_QUANTITY, H_RESERVED_QUANTITY, H_AVG_PRICE, H_CREATED_AT, H_UPDATED_AT, U_ID, C_ID` | I, R | …and a holding belongs to one user (`Holds`) and is a position in one crypto (`PositionIn`) |
-| FD9  | `U_ID, C_ID → H_ID` | U | at most one holding per user and crypto |
-| FD10 | `O_ID → O_SIDE, O_TYPE, O_STATUS, O_QUANTITY, O_FILLED_QUANTITY, O_PRICE, O_PLACED_AT, O_EXECUTED_AT, U_ID, M_ID` | I, R | …and an order is placed by one user (`Places`) on one market (`PlacedOn`) |
-| FD11 | `T_ID → T_TYPE, T_AMOUNT, T_CURRENCY, T_CREATED_AT, T_DESCRIPTION, U_ID, O_ID` | I, R | …and a ledger entry belongs to one user (`Records`) and to at most one order (`Settles`) |
-| FD12 | `MT_ID → MT_EXECUTED_AT, MT_PRICE, MT_QUANTITY, MT_SIDE, MT_SOURCE, M_ID, O_ID_FILLSBUY, O_ID_FILLSSELL` | I, R | …and a trade happened on one market (`Fills`) and filled at most one buy order (`FillsBuy`) and at most one sell order (`FillsSell`) |
-| FD13 | `OE_ID → OE_EVENT_TYPE, OE_QUANTITY, OE_PRICE, OE_STATUS_AFTER, OE_CREATED_AT, O_ID` | I, R | …and an event belongs to one order (`Logs`) |
-| FD14 | `MC_ID → MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME, MC_CANDLE_TIME, M_ID` | I, R | …and a candle summarises one market (`Aggregates`) |
-| FD15 | `M_ID, MC_TIMEFRAME, MC_CANDLE_TIME → MC_ID` | U | one candle per market, timeframe and bucket |
-| FD16 | `W_ID → W_NAME, W_CREATED_AT, U_ID` | I, R | …and a watchlist is owned by one user (`Owns`) |
-| FD17 | `WI_ID → WI_ADDED_AT, W_ID, C_ID` | I, R | …and an item is on one watchlist (`Contains`) and names one crypto (`Lists`) |
-| FD18 | `W_ID, C_ID → WI_ID` | U | an asset appears at most once per watchlist |
-
-**Dependencies that do *not* hold** are as important, because they are why some attributes
-must be combined in the key later:
-
-- The reverse of every (R) dependency, e.g. `U_ID ↛ O_ID`, `M_ID ↛ MT_ID`, `W_ID ↛ WI_ID`.
-  These are 1:N, not 1:1.
-- `M_ID, MT_EXECUTED_AT ↛ MT_ID`. Two trades on a market can share a timestamp.
-- `U_ID, W_NAME ↛ W_ID`. The model does not require list names to be unique per user.
-- `O_ID_FILLSBUY` and `O_ID_FILLSSELL` determine no other attribute of `R_EDUBERZA` **by any
-  rule of the ER model**. The order data (`O_SIDE`, `O_PRICE`, …) describes the order in the
-  `O_ID` column, not the order in a role column. (The database also has a rule that a trade
-  and the orders it fills are on the same market. That rule is a trigger in P7 relating
-  several entity sets, not a rule of the ER model, so it is not used here.)
-
-### Canonical cover
-
-A canonical (minimal) cover is obtained in three steps.
-
-**Step 1 — single attribute on the right.** Each FD above is read as one dependency per
-right-side attribute, e.g. FD6 is `M_ID → M_QUOTE_CURRENCY`, `M_ID → M_IS_ACTIVE`,
-`M_ID → M_CREATED_AT`, `M_ID → C_ID`.
-
-**Step 2 — no extraneous attribute on the left.** Only FD7, FD9, FD15 and FD18 have more than one
-attribute on the left. For each one, dropping any attribute makes the rule false:
-
-| FD | Drop | Counter-example (the smaller left side does not determine the right side) |
-|---|---|---|
-| FD7  | `M_QUOTE_CURRENCY` | BTC is quoted in USD *and* in EUR: one `C_ID`, two markets |
-|      | `C_ID` | USD is the quote currency of many markets |
-| FD9  | `C_ID` | one user holds several cryptos |
-|      | `U_ID` | one crypto is held by several users |
-| FD15 | `M_ID` | every market has a `1h` candle starting at 10:00 |
-|      | `MC_TIMEFRAME` | a market has a `1m` and a `1h` candle both starting at 10:00 |
-|      | `MC_CANDLE_TIME` | a market has many `1h` candles |
-| FD18 | `C_ID` | a watchlist has several items |
-|      | `W_ID` | a crypto is on several watchlists |
-
-**Step 3 — no redundant dependency.** A dependency is redundant if it follows from the others. For
-almost every dependency, its right-side attribute appears on the right of no other
-dependency with a different left side (e.g. nothing but `U_ID` determines
-`U_AVAILABLE_BALANCE`), so it cannot be derived. The candidates worth checking are the
-identifiers that are reached from several places:
-
-- **`T_ID → U_ID` is redundant.** It follows by transitivity from `T_ID → O_ID` (FD11) and
-  `O_ID → U_ID` (FD10): a ledger entry's user is the user of the order it settles. It is
-  therefore **removed** from FD11. The derivation is valid only for an entry that has an
-  order. Every tuple of `R_EDUBERZA` does have one (see the
-  [discussion](#discussion)), so in the de-normalized relation the removal is correct. The
-  consequence for deposits, which have no order, is taken up in the discussion.
-- `H_ID → U_ID`, `H_ID → C_ID`, `WI_ID → W_ID`, `WI_ID → C_ID`, `M_ID → C_ID`, `MT_ID → M_ID`,
-  `MC_ID → M_ID`, `OE_ID → O_ID`, `W_ID → U_ID` and `O_ID → U_ID`, `O_ID → M_ID`: for
-  each, no other dependency with a different left side has that attribute on its right
-  side and a left side reachable from this one, so none can be derived.
-- The four (U) and three (N) dependencies go "backwards" from a non-identifier to an
-  identifier. Nothing else produces an identifier from those attributes, so they are not
-  derivable either.
-
-Grouping the single-attribute dependencies back by left side gives FD1–FD18 as listed,
-except that FD11 loses `U_ID`:
-
-| # | Functional dependency (canonical cover) |
-|---|---|
-| FD11 | `T_ID → T_TYPE, T_AMOUNT, T_CURRENCY, T_CREATED_AT, T_DESCRIPTION, O_ID` |
-
-**FD1–FD18, with this FD11, is the canonical cover.** From here on, "FD11" means this reduced
-form.
-
-## Candidate keys and primary key
-
-A candidate key is a minimal set of attributes whose closure under FD1–FD18 is all 66
-attributes.
-
-**Attributes that must be in every key.** `T_ID`, `OE_ID` and `MT_ID` appear on the right side
-of no dependency. Nothing determines them, so every key must contain them.
-
-**Closure of `{T_ID, OE_ID, MT_ID}`:**
-
-| Step | Added | Using |
-|---|---|---|
-| start | `T_ID, OE_ID, MT_ID` | — |
-| 1 | `T_*`, `O_ID` | FD11 |
-| 2 | `OE_*` | FD13 |
-| 3 | `MT_*`, `M_ID`, `O_ID_FILLSBUY`, `O_ID_FILLSSELL` | FD12 |
-| 4 | `O_*`, `U_ID` | FD10 |
-| 5 | `U_*` | FD1 |
-| 6 | `M_*`, `C_ID` | FD6 |
-| 7 | `C_*` | FD4 |
-| 8 | `H_ID` | FD9 (`U_ID` and `C_ID` are both present) |
-| 9 | `H_*` | FD8 |
-
-That is 53 attributes. Still missing are all 8 `MC_` attributes, the 3 `W_` attributes and
-the 2 `WI_` attributes:
-
-- **`MC_`:** only `MC_ID` determines them (FD14), and `MC_ID` is reached only by FD15, which
-  needs `M_ID` (already present), `MC_TIMEFRAME` and `MC_CANDLE_TIME`. So the key must add
-  either `MC_ID` or both `MC_TIMEFRAME` and `MC_CANDLE_TIME`. Neither of those two alone is
-  enough.
-- **`W_` and `WI_`:** `WI_ID` gives `W_ID` (FD17), and `W_ID` gives `WI_ID` together with
-  `C_ID`, which is already present (FD18). So adding either `WI_ID` or `W_ID` gives all five.
-
-**Candidate keys** (each one's closure is all 66 attributes, and removing any member breaks
-that, by the argument above):
-
-| Key | Attributes |
-|---|---|
-| **K1** | `T_ID, OE_ID, MT_ID, MC_ID, WI_ID` |
-| K2 | `T_ID, OE_ID, MT_ID, MC_ID, W_ID` |
-| K3 | `T_ID, OE_ID, MT_ID, MC_TIMEFRAME, MC_CANDLE_TIME, WI_ID` |
-| K4 | `T_ID, OE_ID, MT_ID, MC_TIMEFRAME, MC_CANDLE_TIME, W_ID` |
-
-**Primary key: K1.** It consists only of identifiers, and it is the key that remains at the
-end of the decomposition below.
-
-**Prime attributes** (in at least one candidate key): `T_ID, OE_ID, MT_ID, MC_ID,
-MC_TIMEFRAME, MC_CANDLE_TIME, W_ID, WI_ID`. The other 58 attributes are **non-prime**. The
-difference matters: 2NF and 3NF only restrict dependencies of non-prime attributes, and BCNF
-restricts all of them.
-
-In words, a tuple of `R_EDUBERZA` puts together one ledger entry, one order event, one
-trade, one candle and one watchlist item. Everything else in the tuple (the user, the order,
-the market, the crypto, the holding, the watchlist) follows from those five.
-
-**Normal form of `R_EDUBERZA`:** 1NF only. It is not in 2NF, because, for example, `T_AMOUNT`
-depends on `T_ID` alone, a proper part of K1.
-
-## 1NF decomposition
-
-No decomposition is needed. Every attribute of `R_EDUBERZA` is atomic and single-valued, and
-the relation has no repeating groups (see
-[De-normalized database form](#de-normalized-database-form)).
-
-## 2NF decomposition
-
-### How every step is described and checked
-
-Each step of 2NF, 3NF and BCNF below lists, in this order: the relation analyzed, its
-dependencies, its candidate keys and primary key, and its normal form; the dependency that
-violates the next normal form and is used for the split; the two resulting relations, each
-with its dependencies, keys and normal form; and the dependency-preservation and lossless-join
-checks.
-
-Every step splits one relation `R` into two: the **extracted** relation `Ri` and the
-**residual** relation `R'` (what is left of `R`). The same two checks are made each time:
-
-- **Lossless join.** The split of `R` into `Ri` and `R'` is lossless if the common attributes
-  determine one of the two sides: `(Ri ∩ R') → Ri` or `(Ri ∩ R') → R'`. Every step below
-  extracts `Ri = X ∪ (what X determines)` for some determinant `X` that stays in `R'`. So
-  `X ⊆ Ri ∩ R'` and `X → Ri`, and the first condition holds.
-- **Dependency preservation.** Every dependency of the canonical cover must end up with all
-  its attributes inside one relation. So an attribute is removed from the residual only when
-  no dependency still waiting in the residual needs it. Otherwise it is extracted **and**
-  kept.
-
-**Relation analyzed first:** `R_EDUBERZA` (66 attributes), dependencies FD1–FD18, candidate
-keys K1–K4, primary key K1. **Normal form:** 1NF.
-
-**Dependencies that violate 2NF.** 2NF forbids a non-prime attribute from depending on a proper
-part of a candidate key. There are six such partial dependencies:
-
-| Part of a key | Non-prime attributes that depend on it | Through |
-|---|---|---|
-| `T_ID` (K1–K4)  | `T_*`, `O_ID`, and through them `O_*`, `U_ID`, `U_*`, `M_ID`, `M_*`, `C_ID`, `C_*`, `H_ID`, `H_*` | FD11, then FD10, FD1, FD6, FD4, FD9, FD8 |
-| `OE_ID` (K1–K4) | `OE_*`, `O_ID` | FD13 |
-| `MT_ID` (K1–K4) | `MT_*`, `M_ID`, `O_ID_FILLSBUY`, `O_ID_FILLSSELL` | FD12 |
-| `MC_ID` (K1, K2) | `MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME`, `M_ID` | FD14 |
-| `W_ID` (K2, K4) | `W_*`, `U_ID` | FD16 |
-| `WI_ID` (K1, K3) | `WI_ADDED_AT`, `C_ID` | FD17 |
-
-The table lists the part of a key that each group depends on most directly. It is not the
-only one: under K3/K4, for example, `MC_OPEN … MC_VOLUME` also depend on
-`{MT_ID, MC_TIMEFRAME, MC_CANDLE_TIME}`, and under K1/K3 `W_*` depend on `WI_ID` through
-`W_ID`. These lead to the same relations, so they need no extra steps. `MC_TIMEFRAME`,
-`MC_CANDLE_TIME` and `W_ID` also depend on parts of keys, but they are prime, so 2NF does not
-restrict them. They are handled under BCNF.
-
-Each step below removes one row of this table, splitting the current relation into two. The
-**order** is chosen so that no dependency is lost. `T_ID` goes first, because its group is the
-largest and carries FD1–FD11 with it. Each later step handles a group whose determinant is
-still in the residual relation.
-
-### Step 2NF-1 — partial dependency on `T_ID`
-
-- **Relation analyzed:** `R_EDUBERZA` (66 attributes).
-- **Dependencies:** FD1–FD18. **Candidate keys:** K1–K4. **Primary key:** K1.
-  **Normal form:** 1NF.
-- **2NF violations:** all six rows of the table above. **Split first on `T_ID`**, the
-  largest group (see the order explained above).
-- **Decomposition dependency:** `T_ID → T_*, O_ID` (FD11), together with everything it
-  determines transitively (FD10, FD1, FD6, FD4, FD9, FD8). `T_ID` is a proper part of K1, and
-  `T_AMOUNT`, for example, is non-prime, so this violates 2NF.
-- **New relation `R_A`** = `{ T_ID, T_*, O_ID, O_*, U_ID, U_*, M_ID, M_*, C_ID, C_*, H_ID, H_* }`
-  (39 attributes). Dependencies: FD1–FD11. Candidate key and primary key: `T_ID`. Normal form: 2NF (it has a
-  one-attribute key), but not 3NF (see 3NF).
-- **Residual relation `S1`** = `R_EDUBERZA − { T_*, O_*, U_*, M_*, C_*, H_ID, H_* }` =
-  `{ T_ID, O_ID, U_ID, M_ID, C_ID, OE_ID, OE_*, MT_ID, MT_*, O_ID_FILLSBUY, O_ID_FILLSSELL,
-  MC_ID, MC_*, W_ID, W_*, WI_ID, WI_ADDED_AT }` (32 attributes). `O_ID`, `U_ID`, `M_ID` and
-  `C_ID` stay, because FD13, FD16, FD12/FD14/FD15 and FD17/FD18 still need them. Dependencies: FD12–FD18, plus the projected dependencies between the identifiers kept here:
-  `T_ID → O_ID, U_ID, M_ID, C_ID`, `O_ID → U_ID, M_ID, C_ID`, `M_ID → C_ID`,
-  `OE_ID → U_ID, M_ID, C_ID`, `MT_ID → C_ID`, `MC_ID → C_ID`, `WI_ID → U_ID`.
-  Candidate keys: K1–K4 (all
-  their attributes are still here). Normal form: 1NF.
-- **Dependency preservation:** FD1–FD11 lie entirely in `R_A`, and FD12–FD18 entirely in `S1`. ✓
-- **Lossless join:** `R_A ∩ S1 = { T_ID, O_ID, U_ID, M_ID, C_ID }` contains `T_ID`, and
-  `T_ID → R_A`, so `(R_A ∩ S1) → R_A`. ✓
-
-### Step 2NF-2 — partial dependency on `OE_ID`
-
-- **Relation analyzed:** `S1` (32 attributes). Dependencies: as listed for `S1` in the
-  previous step. Candidate keys: K1–K4. Primary key: K1. Normal form: 1NF.
-- **Remaining 2NF violations:** the partial dependencies on `OE_ID`, `MT_ID`, `MC_ID`, `W_ID`
-  and `WI_ID` (table above), and the partial dependencies of the kept identifiers
-  `O_ID`, `U_ID`, `M_ID`, `C_ID` on `T_ID`. The kept identifiers cannot leave yet, because
-  other groups still need them. Each one leaves with the last group that needs it (`O_ID` in
-  2NF-2, `M_ID` in 2NF-4, `U_ID` in 2NF-5, `C_ID` in 2NF-6). **Split first on `OE_ID`**,
-  because after it no group needs `O_ID` any more.
-- **Decomposition dependency:** `OE_ID → OE_*, O_ID` (FD13). `OE_ID` is a proper part of K1
-  and `OE_*` are non-prime.
-- **New relation `R_B`** = `{ OE_ID, OE_*, O_ID }` (7 attributes). Dependencies: FD13.
-  Candidate key: `OE_ID`. Normal form: BCNF.
-- **Residual relation `S2`** = `S1 − { OE_*, O_ID }` (26 attributes). No dependency still
-  needed in the residual uses `O_ID`. Dependencies: FD12, FD14–FD18, plus the projected `T_ID → U_ID, M_ID, C_ID`,
-  `OE_ID → U_ID, M_ID, C_ID`, `M_ID → C_ID`, `MT_ID → C_ID`, `MC_ID → C_ID`, `WI_ID → U_ID`.
-  Candidate keys: K1–K4. Normal form: 1NF.
-- **Dependency preservation:** FD13 is in `R_B`, and the others are in `S2`.
-  `T_ID → O_ID` is already kept in `R_A`. ✓
-- **Lossless join:** `R_B ∩ S2 = { OE_ID }`, and `OE_ID → R_B` (FD13). ✓
-
-### Step 2NF-3 — partial dependency on `MT_ID`
-
-- **Relation analyzed:** `S2` (26 attributes). Dependencies: as listed for `S2` in the
-  previous step. Candidate keys: K1–K4. Primary key: K1. Normal form: 1NF.
-- **Remaining 2NF violations:** the groups of `MT_ID`, `MC_ID`, `W_ID`, `WI_ID`, and the kept
-  identifiers `U_ID`, `M_ID`, `C_ID`. **Split first on `MT_ID`**, the next group. `M_ID` must
-  still stay for `MC_ID`.
-- **Decomposition dependency:** `MT_ID → MT_*, M_ID, O_ID_FILLSBUY, O_ID_FILLSSELL` (FD12).
-- **New relation `R_C`** = `{ MT_ID, MT_*, M_ID, O_ID_FILLSBUY, O_ID_FILLSSELL }`
-  (9 attributes). Dependencies: FD12. Candidate key: `MT_ID`. Normal form: BCNF.
-- **Residual relation `S3`** = `S2 − { MT_*, O_ID_FILLSBUY, O_ID_FILLSSELL }` (19 attributes).
-  `M_ID` stays, because FD14/FD15 need it. Dependencies: FD14–FD18, plus the projected `T_ID → U_ID, M_ID, C_ID`,
-  `OE_ID → U_ID, M_ID, C_ID`, `MT_ID → M_ID, C_ID`, `M_ID → C_ID`, `MC_ID → C_ID`,
-  `WI_ID → U_ID`.
-  Candidate keys: K1–K4. Normal form: 1NF.
-- **Dependency preservation:** FD12 is in `R_C`, and FD14–FD18 are in `S3`. ✓
-- **Lossless join:** `R_C ∩ S3 = { MT_ID, M_ID }` contains `MT_ID`, and `MT_ID → R_C`
-  (FD12). ✓
-
-### Step 2NF-4 — partial dependency on `MC_ID`
-
-- **Relation analyzed:** `S3` (19 attributes). Dependencies: as listed for `S3` in the
-  previous step. Candidate keys: K1–K4. Primary key: K1. Normal form: 1NF.
-- **Remaining 2NF violations:** the groups of `MC_ID`, `W_ID`, `WI_ID`, and the kept
-  identifiers `U_ID`, `M_ID`, `C_ID`. **Split first on `MC_ID`**, the last group that needs
-  `M_ID`, so `M_ID` can leave with it.
-- **Decomposition dependency:** `MC_ID → MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME, M_ID`
-  (FD14). `MC_ID` is a proper part of K1. The prime `MC_TIMEFRAME` and `MC_CANDLE_TIME` also go
-  into the new relation, so that FD15, which needs them with `M_ID` and `MC_ID`, is preserved.
-- **New relation `R_D`** = `{ MC_ID, MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE,
-  MC_VOLUME, MC_CANDLE_TIME, M_ID }` (9 attributes). Dependencies: FD14, FD15. Candidate keys:
-  `MC_ID` and `{M_ID, MC_TIMEFRAME, MC_CANDLE_TIME}`. Normal form: BCNF.
-- **Residual relation `S4`** = `S3 − { MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME, M_ID }`
-  (13 attributes). `MC_TIMEFRAME` and `MC_CANDLE_TIME` are prime and stay. Dependencies: FD16–FD18, plus the projected `T_ID → U_ID, C_ID`, `OE_ID → U_ID, C_ID`,
-  `MT_ID → C_ID`, `MC_ID → MC_TIMEFRAME, MC_CANDLE_TIME, C_ID`, `WI_ID → U_ID`.
-  Candidate keys: K1–K4. Normal form: 1NF.
-- **Dependency preservation:** FD14 and FD15 are in `R_D`, and FD16–FD18 are in `S4`. ✓
-- **Lossless join:** `R_D ∩ S4 = { MC_ID, MC_TIMEFRAME, MC_CANDLE_TIME }` contains `MC_ID`,
-  and `MC_ID → R_D` (FD14). ✓
-
-### Step 2NF-5 — partial dependency on `W_ID`
-
-- **Relation analyzed:** `S4` (13 attributes). Dependencies: as listed for `S4` in the
-  previous step. Candidate keys: K1–K4. Primary key: K1. Normal form: 1NF.
-- **Remaining 2NF violations:** the groups of `W_ID` and `WI_ID`, and the kept identifiers
-  `U_ID`, `C_ID`. **Split first on `W_ID`**, the last group that needs `U_ID`.
-- **Decomposition dependency:** `W_ID → W_NAME, W_CREATED_AT, U_ID` (FD16). `W_ID` is a proper
-  part of K2.
-- **New relation `R_E`** = `{ W_ID, W_NAME, W_CREATED_AT, U_ID }` (4 attributes). Dependencies:
-  FD16. Candidate key: `W_ID`. Normal form: BCNF.
-- **Residual relation `S5`** = `S4 − { W_NAME, W_CREATED_AT, U_ID }` (10 attributes).
-  Dependencies: FD17, FD18, plus the projected `T_ID → C_ID`, `OE_ID → C_ID`, `MT_ID → C_ID`,
-  `MC_ID → MC_TIMEFRAME, MC_CANDLE_TIME, C_ID`.
-  Candidate keys: K1–K4. Normal form: 1NF.
-- **Dependency preservation:** FD16 is in `R_E`, and FD17 and FD18 are in `S5`. ✓
-- **Lossless join:** `R_E ∩ S5 = { W_ID }`, and `W_ID → R_E` (FD16). ✓
-
-### Step 2NF-6 — partial dependency on `WI_ID`
-
-- **Relation analyzed:** `S5` (10 attributes). Dependencies: as listed for `S5` in the
-  previous step. Candidate keys: K1–K4. Primary key: K1. Normal form: 1NF.
-- **Remaining 2NF violations:** the group of `WI_ID`, and the kept identifier `C_ID`.
-  **Split on `WI_ID`**, the last group that needs `C_ID`.
-- **Decomposition dependency:** `WI_ID → WI_ADDED_AT, W_ID, C_ID` (FD17). `WI_ID` is a proper
-  part of K1, and `WI_ADDED_AT` and `C_ID` are non-prime.
-- **New relation `R_F`** = `{ WI_ID, WI_ADDED_AT, W_ID, C_ID }` (4 attributes). Dependencies:
-  FD17, FD18. Candidate keys: `WI_ID` and `{W_ID, C_ID}`. Normal form: BCNF.
-- **Residual relation `S6`** = `S5 − { WI_ADDED_AT, C_ID }` =
-  `{ T_ID, OE_ID, MT_ID, MC_ID, MC_TIMEFRAME, MC_CANDLE_TIME, W_ID, WI_ID }` (8 attributes).
-  `W_ID` is prime and stays. Dependencies: no dependency of the cover lies entirely inside
-  `S6`. The projected ones are `MC_ID → MC_TIMEFRAME, MC_CANDLE_TIME` and `WI_ID → W_ID`, plus
-  derived ones such as `MT_ID, MC_TIMEFRAME, MC_CANDLE_TIME → MC_ID` and `MT_ID, W_ID → WI_ID`.
-  Candidate keys: K1–K4. Normal form: 3NF, because every attribute is prime (and so 2NF).
-- **Dependency preservation:** FD17 and FD18 are in `R_F`. ✓
-- **Lossless join:** `R_F ∩ S6 = { WI_ID, W_ID }` contains `WI_ID`, and `WI_ID → R_F`
-  (FD17). ✓
-
-**Result of 2NF:** `R_A`, `R_B`, `R_C`, `R_D`, `R_E`, `R_F`, `S6`. All seven are in 2NF (`R_A`
-only 2NF, `S6` 3NF, the rest BCNF). All 18 dependencies are preserved: FD1–FD11 in `R_A`,
-FD13 in `R_B`, FD12 in `R_C`, FD14–FD15 in `R_D`, FD16 in `R_E`, FD17–FD18 in `R_F`.
-
-## 3NF decomposition
-
-Only `R_A` is not in 3NF. `R_B`–`R_F` are already in BCNF, and `S6` is in 3NF (all its
-attributes are prime).
-
-**Dependencies that violate 3NF in `R_A`.** 3NF forbids a non-prime attribute from depending on
-a key only **transitively**, through a determinant that is not a superkey. The only key of
-`R_A` is `T_ID`, but inside `R_A`:
-
-- `U_ID → U_*` (FD1), `U_USERNAME → U_ID` (FD2), `U_EMAIL → U_ID` (FD3)
-- `C_ID → C_*` (FD4), `C_SYMBOL → C_ID` (FD5)
-- `U_ID, C_ID → H_ID` (FD9), `H_ID → H_*, U_ID, C_ID` (FD8)
-- `M_ID → M_*, C_ID` (FD6), `C_ID, M_QUOTE_CURRENCY → M_ID` (FD7)
-- `O_ID → O_*, U_ID, M_ID` (FD10)
-
-None of these determinants is a superkey of `R_A`. For example, `T_ID → O_ID → O_PRICE` is a
-transitive dependency of the non-prime `O_PRICE` on the key.
-
-**Order of the steps.** An attribute can leave the residual only after every dependency that
-needs it has been extracted. FD9 needs `U_ID` and `C_ID` together, and extracting `Markets`
-takes `C_ID` out of the residual, so `Holdings` must come before `Markets`. Extracting
-`Orders` takes `M_ID` and `U_ID` out, so `Orders` comes last. The dependencies are therefore
-taken from the "leaves" of the chain `T_ID → O_ID → {U_ID, M_ID → C_ID}` inward.
-
-### Step 3NF-1 — transitive dependency through `U_ID`
-
-- **Relation analyzed:** `R_A` (39 attributes), dependencies FD1–FD11, candidate key and
-  primary candidate key and primary key `T_ID`, normal form 2NF.
-- **3NF violations:** all five groups listed above. **Split first on `U_ID`**. It is a leaf
-  of the chain: its dependents determine nothing outside its own group.
-- **Decomposition dependency:** `U_ID → U_*` (FD1). `U_ID` is not a superkey of `R_A`.
-- **New relation `R_USERS`** = `{ U_ID, U_* }` (10 attributes). Dependencies: FD1, FD2, FD3.
-  Candidate keys: `U_ID`, `U_USERNAME`, `U_EMAIL`. Primary key: `U_ID`. Normal form: BCNF.
-- **Residual relation `R_A1`** = `R_A − U_*` (30 attributes). Dependencies: FD4–FD11, which
-  also imply `T_ID → H_ID` and `O_ID → H_ID` (through `U_ID, C_ID`).
-  Candidate key and primary key: `T_ID`. Normal form: 2NF.
-- **Dependency preservation:** FD1–FD3 are in `R_USERS`, and FD4–FD11 are in `R_A1`. ✓
-- **Lossless join:** `R_USERS ∩ R_A1 = { U_ID }`, and `U_ID → R_USERS` (FD1). ✓
-
-### Step 3NF-2 — transitive dependency through `C_ID`
-
-- **Relation analyzed:** `R_A1` (30 attributes), dependencies FD4–FD11, candidate key and primary key `T_ID`, normal form
-  2NF.
-- **3NF violations:** `C_ID → C_*`, `U_ID, C_ID → H_ID → H_*`, `M_ID → M_*, C_ID`,
-  `O_ID → O_*, U_ID, M_ID`. **Split first on `C_ID`**, the next leaf.
-- **Decomposition dependency:** `C_ID → C_*` (FD4).
-- **New relation `R_CRYPTO`** = `{ C_ID, C_* }` (4 attributes). Dependencies: FD4, FD5.
-  Candidate keys: `C_ID`, `C_SYMBOL`. Primary key: `C_ID`. Normal form: BCNF.
-- **Residual relation `R_A2`** = `R_A1 − C_*` (27 attributes). Dependencies: FD6–FD11. Candidate
-  key and primary key: `T_ID`. Normal form: 2NF.
-- **Dependency preservation:** FD4 and FD5 are in `R_CRYPTO`, and FD6–FD11 are in `R_A2`. ✓
-- **Lossless join:** `R_CRYPTO ∩ R_A2 = { C_ID }`, and `C_ID → R_CRYPTO` (FD4). ✓
-
-### Step 3NF-3 — transitive dependency through `{U_ID, C_ID}`
-
-- **Relation analyzed:** `R_A2` (27 attributes), dependencies FD6–FD11, candidate key and primary key `T_ID`, normal form
-  2NF.
-- **3NF violations:** `U_ID, C_ID → H_ID → H_*`, `M_ID → M_*, C_ID`, `O_ID → O_*, U_ID, M_ID`.
-  **Split first on `{U_ID, C_ID}`**, because it must come before `Markets` takes `C_ID` away.
-- **Decomposition dependency:** `U_ID, C_ID → H_ID` (FD9), together with `H_ID → H_*`
-  (FD8).
-- **New relation `R_HOLDINGS`** = `{ H_ID, H_*, U_ID, C_ID }` (8 attributes). Dependencies:
-  FD8, FD9. Candidate keys: `H_ID`, `{U_ID, C_ID}`. Primary key: `H_ID`. Normal form: BCNF.
-- **Residual relation `R_A3`** = `R_A2 − { H_ID, H_* }` (21 attributes). Dependencies: FD6,
-  FD7, FD10, FD11. Candidate key and primary key: `T_ID`. Normal form: 2NF.
-- **Dependency preservation:** FD8 and FD9 are in `R_HOLDINGS`, and the others are in `R_A3`. ✓
-- **Lossless join:** `R_HOLDINGS ∩ R_A3 = { U_ID, C_ID }`, and `U_ID, C_ID → H_ID → H_*`, so
-  `{U_ID, C_ID} → R_HOLDINGS`. ✓
-
-### Step 3NF-4 — transitive dependency through `M_ID`
-
-- **Relation analyzed:** `R_A3` (21 attributes), dependencies FD6, FD7, FD10, FD11, key
-  `T_ID`, normal form 2NF.
-- **3NF violations:** `M_ID → M_*, C_ID` and `O_ID → O_*, U_ID, M_ID`. **Split first on
-  `M_ID`**, because `Orders` still needs `M_ID`.
-- **Decomposition dependency:** `M_ID → M_*, C_ID` (FD6).
-- **New relation `R_MARKETS`** = `{ M_ID, M_*, C_ID }` (5 attributes). Dependencies: FD6, FD7.
-  Candidate keys: `M_ID`, `{C_ID, M_QUOTE_CURRENCY}`. Primary key: `M_ID`. Normal form: BCNF.
-- **Residual relation `R_A4`** = `R_A3 − { M_*, C_ID }` (17 attributes). No dependency left
-  needs `C_ID`. Dependencies: FD10, FD11. Candidate key and primary key: `T_ID`. Normal form: 2NF.
-- **Dependency preservation:** FD6 and FD7 are in `R_MARKETS`, and FD10 and FD11 are in
-  `R_A4`. ✓
-- **Lossless join:** `R_MARKETS ∩ R_A4 = { M_ID }`, and `M_ID → R_MARKETS` (FD6). ✓
-
-### Step 3NF-5 — transitive dependency through `O_ID`
-
-- **Relation analyzed:** `R_A4` = `{ T_ID, T_*, O_ID, O_*, U_ID, M_ID }` (17 attributes),
-  dependencies FD10, FD11, candidate key and primary key `T_ID`, normal form 2NF.
-- **3NF violations:** only `O_ID → O_*, U_ID, M_ID`. **Split on `O_ID`**.
-- **Decomposition dependency:** `O_ID → O_*, U_ID, M_ID` (FD10).
-- **New relation `R_ORDERS`** = `{ O_ID, O_*, U_ID, M_ID }` (11 attributes). Dependencies:
-  FD10. Candidate key: `O_ID`. Normal form: BCNF.
-- **Residual relation `R_TRANSACTIONS`** = `R_A4 − { O_*, U_ID, M_ID }` = `{ T_ID, T_*, O_ID }`
-  (7 attributes). Dependencies: FD11. Candidate key: `T_ID`. Normal form: BCNF. Keeping `U_ID`
-  here would have left the transitive dependency `T_ID → O_ID → U_ID` inside the relation.
-  `T_ID → U_ID` was removed from the cover as redundant, so nothing is lost.
-- **Dependency preservation:** FD10 is in `R_ORDERS`, and FD11 is in `R_TRANSACTIONS`. ✓
-- **Lossless join:** `R_ORDERS ∩ R_TRANSACTIONS = { O_ID }`, and `O_ID → R_ORDERS`
-  (FD10). ✓
-
-**Result of 3NF:** `R_USERS`, `R_CRYPTO`, `R_HOLDINGS`, `R_MARKETS`, `R_ORDERS`,
-`R_TRANSACTIONS` (from `R_A`), and `R_B`, `R_C`, `R_D`, `R_E`, `R_F`, `S6` unchanged. 12
-relations, all in 3NF, and all except `S6` in BCNF. All 18 dependencies are preserved.
-
-## BCNF if possible
-
-BCNF requires **every** determinant of a non-trivial dependency to be a superkey, even when
-the dependent attribute is prime.
-
-| Relation | Dependencies in force | Determinants | All superkeys? |
-|---|---|---|---|
-| `R_USERS` | FD1, FD2, FD3 | `U_ID`, `U_USERNAME`, `U_EMAIL` | yes |
-| `R_CRYPTO` | FD4, FD5 | `C_ID`, `C_SYMBOL` | yes |
-| `R_MARKETS` | FD6, FD7 | `M_ID`, `{C_ID, M_QUOTE_CURRENCY}` | yes |
-| `R_HOLDINGS` | FD8, FD9 | `H_ID`, `{U_ID, C_ID}` | yes |
-| `R_ORDERS` | FD10 | `O_ID` | yes |
-| `R_TRANSACTIONS` | FD11 | `T_ID` | yes |
-| `R_B` | FD13 | `OE_ID` | yes |
-| `R_C` | FD12 | `MT_ID` | yes |
-| `R_D` | FD14, FD15 | `MC_ID`, `{M_ID, MC_TIMEFRAME, MC_CANDLE_TIME}` | yes |
-| `R_E` | FD16 | `W_ID` | yes |
-| `R_F` | FD17, FD18 | `WI_ID`, `{W_ID, C_ID}` | yes |
-| `S6` | `MC_ID → MC_TIMEFRAME, MC_CANDLE_TIME`; `WI_ID → W_ID`; derived ones such as `MT_ID, MC_TIMEFRAME, MC_CANDLE_TIME → MC_ID` and `MT_ID, W_ID → WI_ID` | `MC_ID`, `WI_ID`, `{MT_ID, MC_TIMEFRAME, MC_CANDLE_TIME}`, `{MT_ID, W_ID}`, … | **no** |
-
-**Dependencies that violate BCNF — only in `S6`.** `MC_ID` determines `MC_TIMEFRAME` and
-`MC_CANDLE_TIME`, and `WI_ID` determines `W_ID`, but neither `MC_ID` nor `WI_ID` is a superkey
-of `S6`. 3NF allowed this because the dependent attributes are prime. BCNF does not. The derived
-dependencies all involve `W_ID` or `MC_TIMEFRAME`/`MC_CANDLE_TIME`, so they disappear once the
-two steps below remove those attributes.
-
-### Step BCNF-1 — `MC_ID → MC_TIMEFRAME, MC_CANDLE_TIME`
-
-- **Relation analyzed:** `S6` (8 attributes), dependencies as in the table above, candidate
-  keys K1–K4, primary key K1, normal form 3NF.
-- **BCNF violations:** `MC_ID → MC_TIMEFRAME, MC_CANDLE_TIME` and `WI_ID → W_ID`, and the
-  derived ones that depend on them. **Split first on `MC_ID`**. The order does not matter
-  here, because the two violations share no attribute.
-- **Decomposition dependency:** `MC_ID → MC_TIMEFRAME, MC_CANDLE_TIME`. `MC_ID` is not a
-  superkey of `S6`.
-- **New relation** `{ MC_ID, MC_TIMEFRAME, MC_CANDLE_TIME }`. Dependencies:
-  `MC_ID → MC_TIMEFRAME, MC_CANDLE_TIME`. Key: `MC_ID`. Normal form: BCNF. It is a
-  projection of `R_D`, which already contains these attributes with the same key, so it adds no
-  information and is merged into `R_D`.
-- **Residual relation `S7`** = `{ T_ID, OE_ID, MT_ID, MC_ID, W_ID, WI_ID }` (6 attributes).
-  Dependencies: `WI_ID → W_ID`, and derived ones such as `MT_ID, W_ID → WI_ID`. Candidate
-  keys: `{T_ID, OE_ID, MT_ID, MC_ID, WI_ID}` (K1) and `{T_ID, OE_ID, MT_ID, MC_ID, W_ID}` (K2).
-  Normal form: 3NF.
-- **Dependency preservation:** no dependency of the cover is affected. FD14 and FD15 are in
-  `R_D`. ✓
-- **Lossless join:** the intersection is `{ MC_ID }`, and `MC_ID → { MC_ID, MC_TIMEFRAME,
-  MC_CANDLE_TIME }`. ✓
-
-### Step BCNF-2 — `WI_ID → W_ID`
-
-- **Relation analyzed:** `S7` (6 attributes), dependencies `WI_ID → W_ID` and derived ones,
-  candidate keys K1, K2, primary key K1, normal form 3NF.
-- **BCNF violations:** only `WI_ID → W_ID` (and the derived `MT_ID, W_ID → WI_ID`). **Split on
-  `WI_ID`**.
-- **Decomposition dependency:** `WI_ID → W_ID`. `WI_ID` is not a superkey of `S7`.
-- **New relation** `{ WI_ID, W_ID }`. Dependencies: `WI_ID → W_ID`. Key: `WI_ID`. Normal
-  form: BCNF. For the same reason as in BCNF-1, it is merged into `R_F`.
-- **Residual relation `R_KEY`** = `{ T_ID, OE_ID, MT_ID, MC_ID, WI_ID }` (5 attributes). No
-  non-trivial dependency holds among these attributes. Candidate key: all five (= K1).
-  Normal form: BCNF.
-- **Dependency preservation:** no dependency of the cover is affected. FD17 and FD18 are in
-  `R_F`. The derived dependencies of `S6`/`S7` follow from FD12, FD15, FD17 and FD18, which
-  are all preserved. ✓
-- **Lossless join:** the intersection is `{ WI_ID }`, and `WI_ID → { WI_ID, W_ID }`. ✓
-
-**Result: every relation is in BCNF.** The decomposition into these 12 relations is lossless
-(each of the 13 binary steps passed the test) and preserves all 18 dependencies of the
-canonical cover.
-
-## Final result and discussion
-
-### Normalized relational model
-
-Each relation is followed by its keys (primary key first). An attribute that is the
-identifier of another relation is marked `→` with that relation.
-
-```
-R_USERS          (U_ID, U_USERNAME, U_EMAIL, U_FULL_NAME, U_PASSWORD_HASH,
-                  U_AVAILABLE_BALANCE, U_INVESTED_BALANCE, U_RESERVED_BALANCE,
-                  U_CREATED_AT, U_UPDATED_AT)
-                  keys: U_ID; U_USERNAME; U_EMAIL
-R_CRYPTO         (C_ID, C_SYMBOL, C_NAME, C_CREATED_AT)
-                  keys: C_ID; C_SYMBOL
-R_MARKETS        (M_ID, C_ID → R_CRYPTO, M_QUOTE_CURRENCY, M_IS_ACTIVE, M_CREATED_AT)
-                  keys: M_ID; {C_ID, M_QUOTE_CURRENCY}
-R_HOLDINGS       (H_ID, U_ID → R_USERS, C_ID → R_CRYPTO, H_QUANTITY,
-                  H_RESERVED_QUANTITY, H_AVG_PRICE, H_CREATED_AT, H_UPDATED_AT)
-                  keys: H_ID; {U_ID, C_ID}
-R_ORDERS         (O_ID, U_ID → R_USERS, M_ID → R_MARKETS, O_SIDE, O_TYPE, O_STATUS,
-                  O_QUANTITY, O_FILLED_QUANTITY, O_PRICE, O_PLACED_AT, O_EXECUTED_AT)
-                  key: O_ID
-R_TRANSACTIONS   (T_ID, O_ID → R_ORDERS, T_TYPE, T_AMOUNT, T_CURRENCY, T_CREATED_AT,
-                  T_DESCRIPTION)
-                  key: T_ID
-R_MARKET_TRADES  (MT_ID, M_ID → R_MARKETS, MT_EXECUTED_AT, MT_PRICE, MT_QUANTITY,
-                  MT_SIDE, MT_SOURCE, O_ID_FILLSBUY → R_ORDERS (nullable),
-                  O_ID_FILLSSELL → R_ORDERS (nullable))              [= R_C]
-                  key: MT_ID
-R_ORDER_EVENTS   (OE_ID, O_ID → R_ORDERS, OE_EVENT_TYPE, OE_QUANTITY, OE_PRICE,
-                  OE_STATUS_AFTER, OE_CREATED_AT)                     [= R_B]
-                  key: OE_ID
-R_MARKET_CANDLES (MC_ID, M_ID → R_MARKETS, MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW,
-                  MC_CLOSE, MC_VOLUME, MC_CANDLE_TIME)                [= R_D]
-                  keys: MC_ID; {M_ID, MC_TIMEFRAME, MC_CANDLE_TIME}
-R_WATCHLISTS     (W_ID, U_ID → R_USERS, W_NAME, W_CREATED_AT)         [= R_E]
-                  key: W_ID
-R_WATCHLIST_ITEMS(WI_ID, W_ID → R_WATCHLISTS, C_ID → R_CRYPTO, WI_ADDED_AT)  [= R_F]
-                  keys: WI_ID; {W_ID, C_ID}
-R_KEY            (T_ID, OE_ID, MT_ID, MC_ID, WI_ID)                   [= R_KEY]
-                  key: all five
-```
-
-### Discussion
-
-**The eleven data relations are the P2 design, with one difference** (`transactions.user_id`,
-explained below). Each relation is one entity set of the ER model:
-
-| P5 relation | P2 table | How the relationships appear |
-|---|---|---|
-| `R_USERS` | `users` | — |
-| `R_CRYPTO` | `crypto` | — |
-| `R_MARKETS` | `markets` | `C_ID` = `crypto_id` (`QuotedOn`) |
-| `R_HOLDINGS` | `holdings` | `U_ID` = `user_id` (`Holds`), `C_ID` = `crypto_id` (`PositionIn`) |
-| `R_ORDERS` | `orders` | `U_ID` = `user_id` (`Places`), `M_ID` = `market_id` (`PlacedOn`) |
-| `R_TRANSACTIONS` | `transactions` | `O_ID` = `related_order` (`Settles`); P2 also stores `user_id` (`Records`), see below |
-| `R_MARKET_TRADES` | `market_trades` | `M_ID` = `market_id` (`Fills`), `O_ID_FILLSBUY` = `buy_order_id`, `O_ID_FILLSSELL` = `sell_order_id` |
-| `R_ORDER_EVENTS` | `order_events` | `O_ID` = `order_id` (`Logs`) |
-| `R_MARKET_CANDLES` | `market_candles` | `M_ID` = `market_id` (`Aggregates`) |
-| `R_WATCHLISTS` | `watchlists` | `U_ID` = `user_id` (`Owns`) |
-| `R_WATCHLIST_ITEMS` | `watchlist_items` | `W_ID` = `watchlist_id` (`Contains`), `C_ID` = `crypto_id` (`Lists`) |
-
-The two methods produce the foreign keys differently. In P2 they come from a transformation
-rule: a 1:N relationship becomes a column on the N side. Here, each one appears because a
-dependency of kind (R), for example `O_ID → U_ID`, keeps the other entity's identifier in the
-same relation as the entity that depends on it. The candidate keys also match, including the
-composite ones (`{C_ID, M_QUOTE_CURRENCY}`, `{U_ID, C_ID}`, `{M_ID, MC_TIMEFRAME,
-MC_CANDLE_TIME}`, `{W_ID, C_ID}`). They are exactly the `UNIQUE` constraints in
-[`schema_creation.sql`](../../server/db/schema_creation.sql).
-
-**The one difference: `transactions.user_id`.** The decomposition drops `U_ID` from
-`R_TRANSACTIONS`, because `T_ID → U_ID` follows from `T_ID → O_ID` and `O_ID → U_ID`. That is
-correct for every ledger entry that settles an order. It does not work for a **deposit**.
-`Settles` is partial, so a deposit has no order, and without `user_id` a deposit would have no
-owner at all. The de-normalized relation cannot show this case. Every one of its tuples
-contains an order (every key contains `OE_ID`, and every order event has an order), so a
-ledger entry without an order cannot appear in it. P2 therefore keeps `user_id` (the
-relationship `Records`) as a deliberate exception. As a result, the implemented
-`transactions` table is in **2NF but not in 3NF** (`related_order → user_id` is a transitive
-dependency), and this is by design. For entries with an order,
-`transactions.user_id` repeats the order's user. The only code that sets `related_order` (the buy
-and sell inserts in `advanced_db.sql`) writes the user and the id of the same order row. No
-database constraint enforces this.
-
-**Two order columns in `market_trades`.** `FillsBuy` and `FillsSell` needed two role
-attributes already in the de-normalized relation, and both end up in `R_MARKET_TRADES`.
-They correspond to `buy_order_id` and `sell_order_id`.
-
-**`R_KEY` belongs to the formal result, but it is not implemented as a table.** It is the
-relation that contains a key of `R_EDUBERZA`, and the lossless-join result above holds for all
-12 relations *including* it. It records no fact of the domain. It only says which ledger
-entry, order event, trade, candle and watchlist item were put into the same tuple, and that
-combination exists only because we started from one single relation. Not implementing it is
-an implementation decision. The eleven implemented tables are not claimed to reconstruct
-`R_EDUBERZA` on their own. They keep every attribute and every dependency of the canonical
-cover, and that is what the application needs.
-
-**`holdings.avg_price`** is shown as a *derived* attribute in the ER model: it can be
-recomputed from the buy history. It is still stored, and that is a deliberate
-denormalisation (see [RelationalDesign](../P2-RelationalDesign/RelationalDesign.md#normalisation)).
-Normalisation cannot detect this. `H_ID → H_AVG_PRICE` is an ordinary functional dependency,
-because "derivable from rows of another entity" is a property of the application logic
-that maintains the value (see [UseCase0004](../P3-UseCaseModel/UseCase0004.md),
-`ON CONFLICT … DO UPDATE`), not a dependency between attributes of one tuple.
-
-**Which design is used going forward:** P2's, unchanged. The eleven data relations coincide
-with the eleven tables of [`schema_creation.sql`](../../server/db/schema_creation.sql) and
-[`advanced_db.sql`](../../server/db/advanced_db.sql) column for column, except for the
-deliberately kept `transactions.user_id` explained above. So there are no database objects
-to restructure, and the prototype and the reports of P6/P7 keep working against the same
-schema.
Index: cs/P5-Normalization/NormalizationAIUsage.md
===================================================================
--- docs/P5-Normalization/NormalizationAIUsage.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,119 +1,0 @@
-# Normalization AI Usage
-
-## Name of AI service/solution that was used
-
-**Claude Code** (Anthropic)
-
-- **URL:** https://claude.com/claude-code
-- **Type of service/subscription:** Claude subscription, model Claude Sonnet 5.
-
-## Final result
-
-### Results in details / description
-
-The AI:
-
-- Built the single de-normalized relation `R_EDUBERZA` (68 attributes) by taking every
-  attribute from every entity and attributed relationship in
-  [ERModel](../P1-ConceptualModel/ERModel.md), plus the foreign-key-style linking attributes
-  that the eight attributeless relationships need to be representable in one flat table at
-  all, and disambiguating every repeated name (`id`, `created_at`, `quantity`, `type`, …)
-  with a per-origin prefix (`U_`, `C_`, `M_`, `H_`, `O_`, `T_`, `MT_`, `MC_`, `W_`, `WI_`).
-- Derived the canonical cover (17 functional dependencies) directly from each entity's/
-  relationship's own key and its `UNIQUE` constraints, checked minimality of the composite
-  left-hand sides by example, and separately listed the functional dependencies that hold by
-  foreign-key substitution (e.g. `M_CRYPTO_ID → C_SYMBOL, C_NAME, C_CREATED_AT`) without
-  folding them into the canonical cover, since they are derivable rather than independent.
-- Computed the candidate keys of `R_EDUBERZA` from first principles: since `Holds`,
-  `Contains`, `Orders`, `Transactions`, `MarketTrades`, `MarketCandles` and `Watchlists` are
-  independent of each other, the only candidate keys are combinations that pick one
-  identifying attribute set per cluster — 96 in total — and selected the all-surrogate-key
-  combination as primary key, with a full closure computation shown step by step.
-- Decomposed `R_EDUBERZA` using 3NF/BCNF **synthesis** on the canonical cover (rather than the
-  binary decomposition algorithm), producing ten relations in one step, then separately
-  verified 3NF (checking every foreign-key-carried transitive dependency by name and showing
-  none of them lands inside any single resulting relation) and BCNF (a determinant/candidate-key
-  table for all ten relations) as distinct, explicit checks per the phase template, even
-  though no additional splitting was needed at either stage.
-- Verified dependency preservation (every canonical-cover FD's determinant and dependents
-  land inside exactly one resulting relation) and lossless join (every foreign key is
-  equated to the primary key it references, the textbook sufficient condition) explicitly,
-  rather than asserting them.
-- Compared the result to [RelationalDesign](../P2-RelationalDesign/RelationalDesign.md) and
-  found it identical relation-for-relation and key-for-key, including the less obvious
-  composite candidate keys; documented the one real difference (`holdings.avg_price` is a
-  derived/cached attribute — a property no single-relation normal form check can see) and
-  concluded, with reasoning, that P2's design should continue to be used unchanged.
-- Added a short cross-reference to this page from
-  [RelationalDesign](../P2-RelationalDesign/RelationalDesign.md), since the phase instructions
-  ask for Phase 2 documentation to be updated with the outcome of this phase.
-- Wrote [Normalization](Normalization.md) following the section headings given in the phase
-  template exactly (`De-normalized database form` → `Functional dependencies` →
-  `Candidate keys and primary key` → `1NF decomposition` → `2NF decomposition` →
-  `3NF decomposition` → `BCNF if possible` → `Final result and discussion`).
-
-## Summary of AI involvement
-
-| | This session — 2026-09-16 |
-|---|---|
-| **What I brought** | The phase rubric for P5, pasted in full, and everything already produced in P1–P4 (in particular the `reserved_quantity` addition to `Holds` from the previous session) |
-| **What the AI did** | Built the de-normalized relation, derived the canonical cover, found the candidate keys, ran the 1NF→2NF→3NF→BCNF synthesis, and wrote the comparison against P2 |
-| **What I decided** | To let the AI carry out the full formal derivation rather than write my own first pass, since the rubric's own advice ("start from the canonical cover") is a mechanical method rather than a matter of taste; to keep P2's schema unchanged, per the AI's reasoning that the two designs coincide exactly |
-
-This phase's rule is that AI is used **to improve the student's own initial work**, and that
-any idea taken from the AI is logged as a change against that starting point. I did not
-produce an independent first attempt at the canonical cover or the decomposition before
-asking for this — I gave the AI the rubric directly and asked it to carry out the phase, the
-same way P1–P4 were produced (see
-[ERModelAIUsage](../P1-ConceptualModel/ERModelAIUsage.md) for that history). What I own here
-is checking the result: that the 68-attribute list in `R_EDUBERZA` really is every attribute
-of my P1 model with nothing missing or invented, that the functional dependencies match what
-I already know to be true of the model (each `UNIQUE` constraint in
-[`schema_creation.sql`](../../server/db/schema_creation.sql) shows up as an alternate-key FD,
-and no others were invented), and that the final ten relations really do match
-[RelationalDesign](../P2-RelationalDesign/RelationalDesign.md) column for column — which I
-checked by reading both side by side rather than taking the AI's claim of a match on faith.
-
-## Entire AI usage log
-
-### 2026-09-16
-
-**Intent:** hand over the full P5 rubric and have the phase carried out end to end —
-de-normalized relation, functional dependencies, candidate keys, the four-normal-form
-decomposition, and the comparison against P2 — in one pass.
-
-**Prompt (student, verbatim):**
-> We will go phase by phase, here are the instructions for P5:
-> Instructions on Phase P5: Normalization
-> [the full task description was pasted: parts (a) initial de-normalized relation and
-> functional dependencies, (b) candidate keys and primary key selection, (c) step-by-step
-> decomposition to the highest possible normal form with the specific checklist of what each
-> decomposition step must document, (d) final result and discussion including updating Phase
-> 2 documentation and restructuring the database if warranted; the exact wiki page template
-> for `Normalization` and `NormalizationAIUsage`; and the phase's AI-use rules]
-
-**Response (AI, summarised):**
-- Re-read [ERModel](../P1-ConceptualModel/ERModel.md) and
-  [`schema_creation.sql`](../../server/db/schema_creation.sql) to get the authoritative,
-  current attribute list (including `reserved_quantity`, added in the previous session) and
-  every `UNIQUE`/`CHECK` constraint that turns into an alternate-key functional dependency.
-- Worked out, before writing anything, that `Holds`/`Contains`/`Orders`/`Transactions`/
-  `MarketTrades`/`MarketCandles`/`Watchlists` are mutually independent record types, which is
-  what makes the primary key of the fully de-normalized relation a ten-attribute composite
-  rather than something smaller — and therefore what makes *every* non-key attribute violate
-  2NF simultaneously, rather than a handful needing to be peeled off one at a time.
-- Chose synthesis over the binary decomposition algorithm specifically because the rubric
-  recommends building the canonical cover first, which is what synthesis consumes directly.
-- Wrote [Normalization.md](Normalization.md) and this page.
-
-**What I decided:** to accept the derivation as presented rather than rework it, since
-checking it against my own P1/P2 documents (attribute list, `UNIQUE` constraints, and the
-final ten relations) confirmed it, and to make no changes to `server/db/schema_creation.sql`
-for this phase, since the discussion section's conclusion — that P2's design is already the
-BCNF result — is one I verified myself rather than took on trust.
-
-> **Student action required.** Read [Normalization.md](Normalization.md) end to end before
-> the defense — you will be expected to derive at least one of the ten relations' functional
-> dependencies and candidate keys live, and to explain why `holdings.avg_price` is not a
-> normal-form violation even though it is a stored, derivable value. Append any further
-> prompts here if you ask for revisions.
Index: cs/P5-Normalization/wiki/Normalization.md
===================================================================
--- docs/P5-Normalization/wiki/Normalization.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,715 +1,0 @@
-= Normalization =
-
-This phase does not use the relations of
-[wiki:RelationalDesign] (P2) as a starting point.
-It starts from the attributes of [wiki:ERModel] '''v05''' (P1),
-put into one de-normalized relation. It states the functional dependencies that the
-model's rules impose on those attributes, '''computes''' the keys of that relation from the
-dependencies, and then decomposes it step by step through 2NF, 3NF and BCNF. Every step is
-checked for a lossless join and for dependency preservation. The
-final section compares the result with P2.
-
-== De-normalized database form ==
-
-=== Which attributes go into the relation ===
-
-The relation contains '''the attributes of the ER model and nothing else'''. In v05 all
-attributes belong to the 11 entity sets. None of the 15 relationships has attributes of its
-own.
-
-A relationship adds '''no column'''. Foreign-key columns such as `crypto_id` or `watchlist_id`
-belong to the relational model of P2, not to the ER model, so they do not appear here. What a
-relationship contributes is a '''functional dependency''' between attributes that are already
-in the relation. For example, `Contains` (Watchlists 1 : N !WatchlistItems) says that every
-watchlist item is on exactly one watchlist, which is the dependency `WI_ID → W_ID` in the
-next section. It is not a column `WI_WATCHLIST_ID`.
-
-Attribute names are prefixed with the entity set they come from, because several names repeat
-across the model (`id`, `created_at`, `quantity`, `type`, `name`, `price`, `side`), and one
-relation cannot contain the same name twice.
-
-||= Prefix =||= Entity set (P1) =||= Attributes =||
-|| `U_` || Users || `U_ID, U_USERNAME, U_EMAIL, U_FULL_NAME, U_PASSWORD_HASH, U_AVAILABLE_BALANCE, U_INVESTED_BALANCE, U_RESERVED_BALANCE, U_CREATED_AT, U_UPDATED_AT` ||
-|| `C_` || Cryptos || `C_ID, C_SYMBOL, C_NAME, C_CREATED_AT` ||
-|| `M_` || Markets || `M_ID, M_QUOTE_CURRENCY, M_IS_ACTIVE, M_CREATED_AT` ||
-|| `H_` || Holdings || `H_ID, H_QUANTITY, H_RESERVED_QUANTITY, H_AVG_PRICE, H_CREATED_AT, H_UPDATED_AT` ||
-|| `O_` || Orders || `O_ID, O_SIDE, O_TYPE, O_STATUS, O_QUANTITY, O_FILLED_QUANTITY, O_PRICE, O_PLACED_AT, O_EXECUTED_AT` ||
-|| `T_` || Transactions || `T_ID, T_TYPE, T_AMOUNT, T_CURRENCY, T_CREATED_AT, T_DESCRIPTION` ||
-|| `MT_` || !MarketTrades || `MT_ID, MT_EXECUTED_AT, MT_PRICE, MT_QUANTITY, MT_SIDE, MT_SOURCE` ||
-|| `OE_` || !OrderEvents || `OE_ID, OE_EVENT_TYPE, OE_QUANTITY, OE_PRICE, OE_STATUS_AFTER, OE_CREATED_AT` ||
-|| `MC_` || !MarketCandles || `MC_ID, MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME, MC_CANDLE_TIME` ||
-|| `W_` || Watchlists || `W_ID, W_NAME, W_CREATED_AT` ||
-|| `WI_` || !WatchlistItems || `WI_ID, WI_ADDED_AT` ||
-
-That is 64 attributes. '''One case needs two more.''' `FillsBuy` and `FillsSell` are two
-different relationships between the same two entity sets, Orders and !MarketTrades. A trade
-can fill one buy order ''and'' one sell order, which are two different orders. One relation
-has only one `O_ID` column, and one column cannot hold two different orders in the same
-tuple. So the order's identifier appears once per '''role''', named after the relationship
-that gives the role:
-
-||= Attribute =||= Meaning =||
-|| `O_ID_FILLSBUY` || the `id` of Orders, in its role in `FillsBuy` (the buy order a trade filled) ||
-|| `O_ID_FILLSSELL` || the `id` of Orders, in its role in `FillsSell` (the sell order a trade filled) ||
-
-These are not foreign keys copied from P2. They are the ER attribute `Orders.id` itself, once
-for each of the two relationships. [wiki:ERModel] names these two
-roles of `Orders` explicitly: ''the buy order'' of a trade in `FillsBuy`, and ''the sell order'' in
-`FillsSell`. This is the only place where the model has two
-relationships between the same pair of entity sets. Every other relationship is expressed with
-the attributes above, without renaming.
-
-This gives '''one relation, `R_EDUBERZA`, of 66 attributes:'''
-
-{{{
-R_EDUBERZA(
-  U_ID, U_USERNAME, U_EMAIL, U_FULL_NAME, U_PASSWORD_HASH, U_AVAILABLE_BALANCE,
-  U_INVESTED_BALANCE, U_RESERVED_BALANCE, U_CREATED_AT, U_UPDATED_AT,
-  C_ID, C_SYMBOL, C_NAME, C_CREATED_AT,
-  M_ID, M_QUOTE_CURRENCY, M_IS_ACTIVE, M_CREATED_AT,
-  H_ID, H_QUANTITY, H_RESERVED_QUANTITY, H_AVG_PRICE, H_CREATED_AT, H_UPDATED_AT,
-  O_ID, O_SIDE, O_TYPE, O_STATUS, O_QUANTITY, O_FILLED_QUANTITY, O_PRICE,
-  O_PLACED_AT, O_EXECUTED_AT,
-  T_ID, T_TYPE, T_AMOUNT, T_CURRENCY, T_CREATED_AT, T_DESCRIPTION,
-  MT_ID, MT_EXECUTED_AT, MT_PRICE, MT_QUANTITY, MT_SIDE, MT_SOURCE,
-  O_ID_FILLSBUY, O_ID_FILLSSELL,
-  OE_ID, OE_EVENT_TYPE, OE_QUANTITY, OE_PRICE, OE_STATUS_AFTER, OE_CREATED_AT,
-  MC_ID, MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME, MC_CANDLE_TIME,
-  W_ID, W_NAME, W_CREATED_AT,
-  WI_ID, WI_ADDED_AT
-)
-}}}
-
-To keep the tables below readable, '''`X_*`''' means the non-identifier attributes of prefix
-`X_`. For example, `U_*` = `U_USERNAME … U_UPDATED_AT` (9 attributes), and `O_*` =
-`O_SIDE … O_EXECUTED_AT` (8 attributes). `U_ID`, `O_ID`, … are always written out.
-
-Every attribute is single-valued and atomic (a balance, a timestamp, a symbol, an amount —
-nothing here is a list or a nested record), so `R_EDUBERZA` satisfies 1NF as soon as it is
-written down.
-
-== Functional dependencies ==
-
-At this point `R_EDUBERZA` is just a set of attributes. It has '''no keys yet'''. `U_ID`,
-`O_ID`, … are ordinary attributes of this relation, and which attribute sets are keys of
-`R_EDUBERZA` is computed in the next section, from the
-dependencies below. Each dependency is justified by a rule of the domain, as described in
-the data requirements of [wiki:ERModel]. The rules are of four
-kinds:
-
- * '''(I) Identification.''' Every value of an identifier (`U_ID`, `C_ID`, …) is given to exactly one real object: one user, one crypto, one order. That object has exactly one username, one balance, one price, and so on. So the identifier's value fixes those values.
- * '''(R) 1:N relationship.''' In a 1:N relationship, each object on the N side is linked to exactly one object on the 1 side. So the N side's identifier fixes the 1 side's identifier. Example: an order is placed by exactly one user (`Places`), so `O_ID → U_ID`. The opposite direction does not hold: a user places many orders, so `U_ID ↛ O_ID`.
- * '''(U) Uniqueness rule.''' A rule of the form "at most one X per Y and Z" gives `Y, Z → X`.
- * '''(N) Unique natural attribute.''' No two users share a username or an email, and no two cryptos share a symbol.
-
-'''Only rules of the ER model are used.''' The dependencies below come from the rules stated in
-[wiki:ERModel] v05 and nothing else. The analysis uses the
-classical definitions (Armstrong's axioms), with no special treatment of `NULL`. Partial
-relationships (`Settles`, `FillsBuy`, `FillsSell`) are discussed where they matter:
-under Canonical cover and in the discussion.
-
-||= # =||= Functional dependency =||= Rule =||= Why it holds =||
-|| FD1 || `U_ID → U_USERNAME, U_EMAIL, U_FULL_NAME, U_PASSWORD_HASH, U_AVAILABLE_BALANCE, U_INVESTED_BALANCE, U_RESERVED_BALANCE, U_CREATED_AT, U_UPDATED_AT` || I || one user, one value of each ||
-|| FD2 || `U_USERNAME → U_ID` || N || usernames are unique ||
-|| FD3 || `U_EMAIL → U_ID` || N || emails are unique ||
-|| FD4 || `C_ID → C_SYMBOL, C_NAME, C_CREATED_AT` || I || one crypto, one value of each ||
-|| FD5 || `C_SYMBOL → C_ID` || N || symbols are unique ||
-|| FD6 || `M_ID → M_QUOTE_CURRENCY, M_IS_ACTIVE, M_CREATED_AT, C_ID` || I, R || …and a market is `QuotedOn` exactly one crypto ||
-|| FD7 || `C_ID, M_QUOTE_CURRENCY → M_ID` || U || a crypto is quoted at most once per currency ||
-|| FD8 || `H_ID → H_QUANTITY, H_RESERVED_QUANTITY, H_AVG_PRICE, H_CREATED_AT, H_UPDATED_AT, U_ID, C_ID` || I, R || …and a holding belongs to one user (`Holds`) and is a position in one crypto (`PositionIn`) ||
-|| FD9 || `U_ID, C_ID → H_ID` || U || at most one holding per user and crypto ||
-|| FD10 || `O_ID → O_SIDE, O_TYPE, O_STATUS, O_QUANTITY, O_FILLED_QUANTITY, O_PRICE, O_PLACED_AT, O_EXECUTED_AT, U_ID, M_ID` || I, R || …and an order is placed by one user (`Places`) on one market (`PlacedOn`) ||
-|| FD11 || `T_ID → T_TYPE, T_AMOUNT, T_CURRENCY, T_CREATED_AT, T_DESCRIPTION, U_ID, O_ID` || I, R || …and a ledger entry belongs to one user (`Records`) and to at most one order (`Settles`) ||
-|| FD12 || `MT_ID → MT_EXECUTED_AT, MT_PRICE, MT_QUANTITY, MT_SIDE, MT_SOURCE, M_ID, O_ID_FILLSBUY, O_ID_FILLSSELL` || I, R || …and a trade happened on one market (`Fills`) and filled at most one buy order (`FillsBuy`) and at most one sell order (`FillsSell`) ||
-|| FD13 || `OE_ID → OE_EVENT_TYPE, OE_QUANTITY, OE_PRICE, OE_STATUS_AFTER, OE_CREATED_AT, O_ID` || I, R || …and an event belongs to one order (`Logs`) ||
-|| FD14 || `MC_ID → MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME, MC_CANDLE_TIME, M_ID` || I, R || …and a candle summarises one market (`Aggregates`) ||
-|| FD15 || `M_ID, MC_TIMEFRAME, MC_CANDLE_TIME → MC_ID` || U || one candle per market, timeframe and bucket ||
-|| FD16 || `W_ID → W_NAME, W_CREATED_AT, U_ID` || I, R || …and a watchlist is owned by one user (`Owns`) ||
-|| FD17 || `WI_ID → WI_ADDED_AT, W_ID, C_ID` || I, R || …and an item is on one watchlist (`Contains`) and names one crypto (`Lists`) ||
-|| FD18 || `W_ID, C_ID → WI_ID` || U || an asset appears at most once per watchlist ||
-
-'''Dependencies that do ''not'' hold''' are as important, because they are why some attributes
-must be combined in the key later:
-
- * The reverse of every (R) dependency, e.g. `U_ID ↛ O_ID`, `M_ID ↛ MT_ID`, `W_ID ↛ WI_ID`. These are 1:N, not 1:1.
- * `M_ID, MT_EXECUTED_AT ↛ MT_ID`. Two trades on a market can share a timestamp.
- * `U_ID, W_NAME ↛ W_ID`. The model does not require list names to be unique per user.
- * `O_ID_FILLSBUY` and `O_ID_FILLSSELL` determine no other attribute of `R_EDUBERZA` '''by any rule of the ER model'''. The order data (`O_SIDE`, `O_PRICE`, …) describes the order in the `O_ID` column, not the order in a role column. (The database also has a rule that a trade and the orders it fills are on the same market. That rule is a trigger in P7 relating several entity sets, not a rule of the ER model, so it is not used here.)
-
-=== Canonical cover ===
-
-A canonical (minimal) cover is obtained in three steps.
-
-'''Step 1 — single attribute on the right.''' Each FD above is read as one dependency per
-right-side attribute, e.g. FD6 is `M_ID → M_QUOTE_CURRENCY`, `M_ID → M_IS_ACTIVE`,
-`M_ID → M_CREATED_AT`, `M_ID → C_ID`.
-
-'''Step 2 — no extraneous attribute on the left.''' Only FD7, FD9, FD15 and FD18 have more than one
-attribute on the left. For each one, dropping any attribute makes the rule false:
-
-||= FD =||= Drop =||= Counter-example (the smaller left side does not determine the right side) =||
-|| FD7 || `M_QUOTE_CURRENCY` || BTC is quoted in USD ''and'' in EUR: one `C_ID`, two markets ||
-||  || `C_ID` || USD is the quote currency of many markets ||
-|| FD9 || `C_ID` || one user holds several cryptos ||
-||  || `U_ID` || one crypto is held by several users ||
-|| FD15 || `M_ID` || every market has a `1h` candle starting at 10:00 ||
-||  || `MC_TIMEFRAME` || a market has a `1m` and a `1h` candle both starting at 10:00 ||
-||  || `MC_CANDLE_TIME` || a market has many `1h` candles ||
-|| FD18 || `C_ID` || a watchlist has several items ||
-||  || `W_ID` || a crypto is on several watchlists ||
-
-'''Step 3 — no redundant dependency.''' A dependency is redundant if it follows from the others. For
-almost every dependency, its right-side attribute appears on the right of no other
-dependency with a different left side (e.g. nothing but `U_ID` determines
-`U_AVAILABLE_BALANCE`), so it cannot be derived. The candidates worth checking are the
-identifiers that are reached from several places:
-
- * '''`T_ID → U_ID` is redundant.''' It follows by transitivity from `T_ID → O_ID` (FD11) and `O_ID → U_ID` (FD10): a ledger entry's user is the user of the order it settles. It is therefore '''removed''' from FD11. The derivation is valid only for an entry that has an order. Every tuple of `R_EDUBERZA` does have one (see the discussion), so in the de-normalized relation the removal is correct. The consequence for deposits, which have no order, is taken up in the discussion.
- * `H_ID → U_ID`, `H_ID → C_ID`, `WI_ID → W_ID`, `WI_ID → C_ID`, `M_ID → C_ID`, `MT_ID → M_ID`, `MC_ID → M_ID`, `OE_ID → O_ID`, `W_ID → U_ID` and `O_ID → U_ID`, `O_ID → M_ID`: for each, no other dependency with a different left side has that attribute on its right side and a left side reachable from this one, so none can be derived.
- * The four (U) and three (N) dependencies go "backwards" from a non-identifier to an identifier. Nothing else produces an identifier from those attributes, so they are not derivable either.
-
-Grouping the single-attribute dependencies back by left side gives FD1–FD18 as listed,
-except that FD11 loses `U_ID`:
-
-||= # =||= Functional dependency (canonical cover) =||
-|| FD11 || `T_ID → T_TYPE, T_AMOUNT, T_CURRENCY, T_CREATED_AT, T_DESCRIPTION, O_ID` ||
-
-'''FD1–FD18, with this FD11, is the canonical cover.''' From here on, "FD11" means this reduced
-form.
-
-== Candidate keys and primary key ==
-
-A candidate key is a minimal set of attributes whose closure under FD1–FD18 is all 66
-attributes.
-
-'''Attributes that must be in every key.''' `T_ID`, `OE_ID` and `MT_ID` appear on the right side
-of no dependency. Nothing determines them, so every key must contain them.
-
-'''Closure of `{T_ID, OE_ID, MT_ID}`:'''
-
-||= Step =||= Added =||= Using =||
-|| start || `T_ID, OE_ID, MT_ID` || — ||
-|| 1 || `T_*`, `O_ID` || FD11 ||
-|| 2 || `OE_*` || FD13 ||
-|| 3 || `MT_*`, `M_ID`, `O_ID_FILLSBUY`, `O_ID_FILLSSELL` || FD12 ||
-|| 4 || `O_*`, `U_ID` || FD10 ||
-|| 5 || `U_*` || FD1 ||
-|| 6 || `M_*`, `C_ID` || FD6 ||
-|| 7 || `C_*` || FD4 ||
-|| 8 || `H_ID` || FD9 (`U_ID` and `C_ID` are both present) ||
-|| 9 || `H_*` || FD8 ||
-
-That is 53 attributes. Still missing are all 8 `MC_` attributes, the 3 `W_` attributes and
-the 2 `WI_` attributes:
-
- * '''`MC_`:''' only `MC_ID` determines them (FD14), and `MC_ID` is reached only by FD15, which needs `M_ID` (already present), `MC_TIMEFRAME` and `MC_CANDLE_TIME`. So the key must add either `MC_ID` or both `MC_TIMEFRAME` and `MC_CANDLE_TIME`. Neither of those two alone is enough.
- * '''`W_` and `WI_`:''' `WI_ID` gives `W_ID` (FD17), and `W_ID` gives `WI_ID` together with `C_ID`, which is already present (FD18). So adding either `WI_ID` or `W_ID` gives all five.
-
-'''Candidate keys''' (each one's closure is all 66 attributes, and removing any member breaks
-that, by the argument above):
-
-||= Key =||= Attributes =||
-|| '''K1''' || `T_ID, OE_ID, MT_ID, MC_ID, WI_ID` ||
-|| K2 || `T_ID, OE_ID, MT_ID, MC_ID, W_ID` ||
-|| K3 || `T_ID, OE_ID, MT_ID, MC_TIMEFRAME, MC_CANDLE_TIME, WI_ID` ||
-|| K4 || `T_ID, OE_ID, MT_ID, MC_TIMEFRAME, MC_CANDLE_TIME, W_ID` ||
-
-'''Primary key: K1.''' It consists only of identifiers, and it is the key that remains at the
-end of the decomposition below.
-
-'''Prime attributes''' (in at least one candidate key): `T_ID, OE_ID, MT_ID, MC_ID,
-MC_TIMEFRAME, MC_CANDLE_TIME, W_ID, WI_ID`. The other 58 attributes are '''non-prime'''. The
-difference matters: 2NF and 3NF only restrict dependencies of non-prime attributes, and BCNF
-restricts all of them.
-
-In words, a tuple of `R_EDUBERZA` puts together one ledger entry, one order event, one
-trade, one candle and one watchlist item. Everything else in the tuple (the user, the order,
-the market, the crypto, the holding, the watchlist) follows from those five.
-
-'''Normal form of `R_EDUBERZA`:''' 1NF only. It is not in 2NF, because, for example, `T_AMOUNT`
-depends on `T_ID` alone, a proper part of K1.
-
-== 1NF decomposition ==
-
-No decomposition is needed. Every attribute of `R_EDUBERZA` is atomic and single-valued, and
-the relation has no repeating groups (see
-De-normalized database form).
-
-== 2NF decomposition ==
-
-=== How every step is described and checked ===
-
-Each step of 2NF, 3NF and BCNF below lists, in this order: the relation analyzed, its
-dependencies, its candidate keys and primary key, and its normal form; the dependency that
-violates the next normal form and is used for the split; the two resulting relations, each
-with its dependencies, keys and normal form; and the dependency-preservation and lossless-join
-checks.
-
-Every step splits one relation `R` into two: the '''extracted''' relation `Ri` and the
-'''residual''' relation `R'` (what is left of `R`). The same two checks are made each time:
-
- * '''Lossless join.''' The split of `R` into `Ri` and `R'` is lossless if the common attributes determine one of the two sides: `(Ri ∩ R') → Ri` or `(Ri ∩ R') → R'`. Every step below extracts `Ri = X ∪ (what X determines)` for some determinant `X` that stays in `R'`. So `X ⊆ Ri ∩ R'` and `X → Ri`, and the first condition holds.
- * '''Dependency preservation.''' Every dependency of the canonical cover must end up with all its attributes inside one relation. So an attribute is removed from the residual only when no dependency still waiting in the residual needs it. Otherwise it is extracted '''and''' kept.
-
-'''Relation analyzed first:''' `R_EDUBERZA` (66 attributes), dependencies FD1–FD18, candidate
-keys K1–K4, primary key K1. '''Normal form:''' 1NF.
-
-'''Dependencies that violate 2NF.''' 2NF forbids a non-prime attribute from depending on a proper
-part of a candidate key. There are six such partial dependencies:
-
-||= Part of a key =||= Non-prime attributes that depend on it =||= Through =||
-|| `T_ID` (K1–K4) || `T_*`, `O_ID`, and through them `O_*`, `U_ID`, `U_*`, `M_ID`, `M_*`, `C_ID`, `C_*`, `H_ID`, `H_*` || FD11, then FD10, FD1, FD6, FD4, FD9, FD8 ||
-|| `OE_ID` (K1–K4) || `OE_*`, `O_ID` || FD13 ||
-|| `MT_ID` (K1–K4) || `MT_*`, `M_ID`, `O_ID_FILLSBUY`, `O_ID_FILLSSELL` || FD12 ||
-|| `MC_ID` (K1, K2) || `MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME`, `M_ID` || FD14 ||
-|| `W_ID` (K2, K4) || `W_*`, `U_ID` || FD16 ||
-|| `WI_ID` (K1, K3) || `WI_ADDED_AT`, `C_ID` || FD17 ||
-
-The table lists the part of a key that each group depends on most directly. It is not the
-only one: under K3/K4, for example, `MC_OPEN … MC_VOLUME` also depend on
-`{MT_ID, MC_TIMEFRAME, MC_CANDLE_TIME}`, and under K1/K3 `W_*` depend on `WI_ID` through
-`W_ID`. These lead to the same relations, so they need no extra steps. `MC_TIMEFRAME`,
-`MC_CANDLE_TIME` and `W_ID` also depend on parts of keys, but they are prime, so 2NF does not
-restrict them. They are handled under BCNF.
-
-Each step below removes one row of this table, splitting the current relation into two. The
-'''order''' is chosen so that no dependency is lost. `T_ID` goes first, because its group is the
-largest and carries FD1–FD11 with it. Each later step handles a group whose determinant is
-still in the residual relation.
-
-=== Step 2NF-1 — partial dependency on `T_ID` ===
-
- * '''Relation analyzed:''' `R_EDUBERZA` (66 attributes).
- * '''Dependencies:''' FD1–FD18. '''Candidate keys:''' K1–K4. '''Primary key:''' K1. '''Normal form:''' 1NF.
- * '''2NF violations:''' all six rows of the table above. '''Split first on `T_ID`''', the largest group (see the order explained above).
- * '''Decomposition dependency:''' `T_ID → T_*, O_ID` (FD11), together with everything it determines transitively (FD10, FD1, FD6, FD4, FD9, FD8). `T_ID` is a proper part of K1, and `T_AMOUNT`, for example, is non-prime, so this violates 2NF.
- * '''New relation `R_A`''' = `{ T_ID, T_*, O_ID, O_*, U_ID, U_*, M_ID, M_*, C_ID, C_*, H_ID, H_* }` (39 attributes). Dependencies: FD1–FD11. Candidate key and primary key: `T_ID`. Normal form: 2NF (it has a one-attribute key), but not 3NF (see 3NF).
- * '''Residual relation `S1`''' = `R_EDUBERZA − { T_*, O_*, U_*, M_*, C_*, H_ID, H_* }` = `{ T_ID, O_ID, U_ID, M_ID, C_ID, OE_ID, OE_*, MT_ID, MT_*, O_ID_FILLSBUY, O_ID_FILLSSELL, MC_ID, MC_*, W_ID, W_*, WI_ID, WI_ADDED_AT }` (32 attributes). `O_ID`, `U_ID`, `M_ID` and `C_ID` stay, because FD13, FD16, FD12/FD14/FD15 and FD17/FD18 still need them. Dependencies: FD12–FD18, plus the projected dependencies between the identifiers kept here: `T_ID → O_ID, U_ID, M_ID, C_ID`, `O_ID → U_ID, M_ID, C_ID`, `M_ID → C_ID`, `OE_ID → U_ID, M_ID, C_ID`, `MT_ID → C_ID`, `MC_ID → C_ID`, `WI_ID → U_ID`. Candidate keys: K1–K4 (all their attributes are still here). Normal form: 1NF.
- * '''Dependency preservation:''' FD1–FD11 lie entirely in `R_A`, and FD12–FD18 entirely in `S1`. ✓
- * '''Lossless join:''' `R_A ∩ S1 = { T_ID, O_ID, U_ID, M_ID, C_ID }` contains `T_ID`, and `T_ID → R_A`, so `(R_A ∩ S1) → R_A`. ✓
-
-=== Step 2NF-2 — partial dependency on `OE_ID` ===
-
- * '''Relation analyzed:''' `S1` (32 attributes). Dependencies: as listed for `S1` in the previous step. Candidate keys: K1–K4. Primary key: K1. Normal form: 1NF.
- * '''Remaining 2NF violations:''' the partial dependencies on `OE_ID`, `MT_ID`, `MC_ID`, `W_ID` and `WI_ID` (table above), and the partial dependencies of the kept identifiers `O_ID`, `U_ID`, `M_ID`, `C_ID` on `T_ID`. The kept identifiers cannot leave yet, because other groups still need them. Each one leaves with the last group that needs it (`O_ID` in 2NF-2, `M_ID` in 2NF-4, `U_ID` in 2NF-5, `C_ID` in 2NF-6). '''Split first on `OE_ID`''', because after it no group needs `O_ID` any more.
- * '''Decomposition dependency:''' `OE_ID → OE_*, O_ID` (FD13). `OE_ID` is a proper part of K1 and `OE_*` are non-prime.
- * '''New relation `R_B`''' = `{ OE_ID, OE_*, O_ID }` (7 attributes). Dependencies: FD13. Candidate key: `OE_ID`. Normal form: BCNF.
- * '''Residual relation `S2`''' = `S1 − { OE_*, O_ID }` (26 attributes). No dependency still needed in the residual uses `O_ID`. Dependencies: FD12, FD14–FD18, plus the projected `T_ID → U_ID, M_ID, C_ID`, `OE_ID → U_ID, M_ID, C_ID`, `M_ID → C_ID`, `MT_ID → C_ID`, `MC_ID → C_ID`, `WI_ID → U_ID`. Candidate keys: K1–K4. Normal form: 1NF.
- * '''Dependency preservation:''' FD13 is in `R_B`, and the others are in `S2`. `T_ID → O_ID` is already kept in `R_A`. ✓
- * '''Lossless join:''' `R_B ∩ S2 = { OE_ID }`, and `OE_ID → R_B` (FD13). ✓
-
-=== Step 2NF-3 — partial dependency on `MT_ID` ===
-
- * '''Relation analyzed:''' `S2` (26 attributes). Dependencies: as listed for `S2` in the previous step. Candidate keys: K1–K4. Primary key: K1. Normal form: 1NF.
- * '''Remaining 2NF violations:''' the groups of `MT_ID`, `MC_ID`, `W_ID`, `WI_ID`, and the kept identifiers `U_ID`, `M_ID`, `C_ID`. '''Split first on `MT_ID`''', the next group. `M_ID` must still stay for `MC_ID`.
- * '''Decomposition dependency:''' `MT_ID → MT_*, M_ID, O_ID_FILLSBUY, O_ID_FILLSSELL` (FD12).
- * '''New relation `R_C`''' = `{ MT_ID, MT_*, M_ID, O_ID_FILLSBUY, O_ID_FILLSSELL }` (9 attributes). Dependencies: FD12. Candidate key: `MT_ID`. Normal form: BCNF.
- * '''Residual relation `S3`''' = `S2 − { MT_*, O_ID_FILLSBUY, O_ID_FILLSSELL }` (19 attributes). `M_ID` stays, because FD14/FD15 need it. Dependencies: FD14–FD18, plus the projected `T_ID → U_ID, M_ID, C_ID`, `OE_ID → U_ID, M_ID, C_ID`, `MT_ID → M_ID, C_ID`, `M_ID → C_ID`, `MC_ID → C_ID`, `WI_ID → U_ID`. Candidate keys: K1–K4. Normal form: 1NF.
- * '''Dependency preservation:''' FD12 is in `R_C`, and FD14–FD18 are in `S3`. ✓
- * '''Lossless join:''' `R_C ∩ S3 = { MT_ID, M_ID }` contains `MT_ID`, and `MT_ID → R_C` (FD12). ✓
-
-=== Step 2NF-4 — partial dependency on `MC_ID` ===
-
- * '''Relation analyzed:''' `S3` (19 attributes). Dependencies: as listed for `S3` in the previous step. Candidate keys: K1–K4. Primary key: K1. Normal form: 1NF.
- * '''Remaining 2NF violations:''' the groups of `MC_ID`, `W_ID`, `WI_ID`, and the kept identifiers `U_ID`, `M_ID`, `C_ID`. '''Split first on `MC_ID`''', the last group that needs `M_ID`, so `M_ID` can leave with it.
- * '''Decomposition dependency:''' `MC_ID → MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME, M_ID` (FD14). `MC_ID` is a proper part of K1. The prime `MC_TIMEFRAME` and `MC_CANDLE_TIME` also go into the new relation, so that FD15, which needs them with `M_ID` and `MC_ID`, is preserved.
- * '''New relation `R_D`''' = `{ MC_ID, MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME, MC_CANDLE_TIME, M_ID }` (9 attributes). Dependencies: FD14, FD15. Candidate keys: `MC_ID` and `{M_ID, MC_TIMEFRAME, MC_CANDLE_TIME}`. Normal form: BCNF.
- * '''Residual relation `S4`''' = `S3 − { MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME, M_ID }` (13 attributes). `MC_TIMEFRAME` and `MC_CANDLE_TIME` are prime and stay. Dependencies: FD16–FD18, plus the projected `T_ID → U_ID, C_ID`, `OE_ID → U_ID, C_ID`, `MT_ID → C_ID`, `MC_ID → MC_TIMEFRAME, MC_CANDLE_TIME, C_ID`, `WI_ID → U_ID`. Candidate keys: K1–K4. Normal form: 1NF.
- * '''Dependency preservation:''' FD14 and FD15 are in `R_D`, and FD16–FD18 are in `S4`. ✓
- * '''Lossless join:''' `R_D ∩ S4 = { MC_ID, MC_TIMEFRAME, MC_CANDLE_TIME }` contains `MC_ID`, and `MC_ID → R_D` (FD14). ✓
-
-=== Step 2NF-5 — partial dependency on `W_ID` ===
-
- * '''Relation analyzed:''' `S4` (13 attributes). Dependencies: as listed for `S4` in the previous step. Candidate keys: K1–K4. Primary key: K1. Normal form: 1NF.
- * '''Remaining 2NF violations:''' the groups of `W_ID` and `WI_ID`, and the kept identifiers `U_ID`, `C_ID`. '''Split first on `W_ID`''', the last group that needs `U_ID`.
- * '''Decomposition dependency:''' `W_ID → W_NAME, W_CREATED_AT, U_ID` (FD16). `W_ID` is a proper part of K2.
- * '''New relation `R_E`''' = `{ W_ID, W_NAME, W_CREATED_AT, U_ID }` (4 attributes). Dependencies: FD16. Candidate key: `W_ID`. Normal form: BCNF.
- * '''Residual relation `S5`''' = `S4 − { W_NAME, W_CREATED_AT, U_ID }` (10 attributes). Dependencies: FD17, FD18, plus the projected `T_ID → C_ID`, `OE_ID → C_ID`, `MT_ID → C_ID`, `MC_ID → MC_TIMEFRAME, MC_CANDLE_TIME, C_ID`. Candidate keys: K1–K4. Normal form: 1NF.
- * '''Dependency preservation:''' FD16 is in `R_E`, and FD17 and FD18 are in `S5`. ✓
- * '''Lossless join:''' `R_E ∩ S5 = { W_ID }`, and `W_ID → R_E` (FD16). ✓
-
-=== Step 2NF-6 — partial dependency on `WI_ID` ===
-
- * '''Relation analyzed:''' `S5` (10 attributes). Dependencies: as listed for `S5` in the previous step. Candidate keys: K1–K4. Primary key: K1. Normal form: 1NF.
- * '''Remaining 2NF violations:''' the group of `WI_ID`, and the kept identifier `C_ID`. '''Split on `WI_ID`''', the last group that needs `C_ID`.
- * '''Decomposition dependency:''' `WI_ID → WI_ADDED_AT, W_ID, C_ID` (FD17). `WI_ID` is a proper part of K1, and `WI_ADDED_AT` and `C_ID` are non-prime.
- * '''New relation `R_F`''' = `{ WI_ID, WI_ADDED_AT, W_ID, C_ID }` (4 attributes). Dependencies: FD17, FD18. Candidate keys: `WI_ID` and `{W_ID, C_ID}`. Normal form: BCNF.
- * '''Residual relation `S6`''' = `S5 − { WI_ADDED_AT, C_ID }` = `{ T_ID, OE_ID, MT_ID, MC_ID, MC_TIMEFRAME, MC_CANDLE_TIME, W_ID, WI_ID }` (8 attributes). `W_ID` is prime and stays. Dependencies: no dependency of the cover lies entirely inside `S6`. The projected ones are `MC_ID → MC_TIMEFRAME, MC_CANDLE_TIME` and `WI_ID → W_ID`, plus derived ones such as `MT_ID, MC_TIMEFRAME, MC_CANDLE_TIME → MC_ID` and `MT_ID, W_ID → WI_ID`. Candidate keys: K1–K4. Normal form: 3NF, because every attribute is prime (and so 2NF).
- * '''Dependency preservation:''' FD17 and FD18 are in `R_F`. ✓
- * '''Lossless join:''' `R_F ∩ S6 = { WI_ID, W_ID }` contains `WI_ID`, and `WI_ID → R_F` (FD17). ✓
-
-'''Result of 2NF:''' `R_A`, `R_B`, `R_C`, `R_D`, `R_E`, `R_F`, `S6`. All seven are in 2NF (`R_A`
-only 2NF, `S6` 3NF, the rest BCNF). All 18 dependencies are preserved: FD1–FD11 in `R_A`,
-FD13 in `R_B`, FD12 in `R_C`, FD14–FD15 in `R_D`, FD16 in `R_E`, FD17–FD18 in `R_F`.
-
-== 3NF decomposition ==
-
-Only `R_A` is not in 3NF. `R_B`–`R_F` are already in BCNF, and `S6` is in 3NF (all its
-attributes are prime).
-
-'''Dependencies that violate 3NF in `R_A`.''' 3NF forbids a non-prime attribute from depending on
-a key only '''transitively''', through a determinant that is not a superkey. The only key of
-`R_A` is `T_ID`, but inside `R_A`:
-
- * `U_ID → U_*` (FD1), `U_USERNAME → U_ID` (FD2), `U_EMAIL → U_ID` (FD3)
- * `C_ID → C_*` (FD4), `C_SYMBOL → C_ID` (FD5)
- * `U_ID, C_ID → H_ID` (FD9), `H_ID → H_*, U_ID, C_ID` (FD8)
- * `M_ID → M_*, C_ID` (FD6), `C_ID, M_QUOTE_CURRENCY → M_ID` (FD7)
- * `O_ID → O_*, U_ID, M_ID` (FD10)
-
-None of these determinants is a superkey of `R_A`. For example, `T_ID → O_ID → O_PRICE` is a
-transitive dependency of the non-prime `O_PRICE` on the key.
-
-'''Order of the steps.''' An attribute can leave the residual only after every dependency that
-needs it has been extracted. FD9 needs `U_ID` and `C_ID` together, and extracting `Markets`
-takes `C_ID` out of the residual, so `Holdings` must come before `Markets`. Extracting
-`Orders` takes `M_ID` and `U_ID` out, so `Orders` comes last. The dependencies are therefore
-taken from the "leaves" of the chain `T_ID → O_ID → {U_ID, M_ID → C_ID}` inward.
-
-=== Step 3NF-1 — transitive dependency through `U_ID` ===
-
- * '''Relation analyzed:''' `R_A` (39 attributes), dependencies FD1–FD11, candidate key and primary candidate key and primary key `T_ID`, normal form 2NF.
- * '''3NF violations:''' all five groups listed above. '''Split first on `U_ID`'''. It is a leaf of the chain: its dependents determine nothing outside its own group.
- * '''Decomposition dependency:''' `U_ID → U_*` (FD1). `U_ID` is not a superkey of `R_A`.
- * '''New relation `R_USERS`''' = `{ U_ID, U_* }` (10 attributes). Dependencies: FD1, FD2, FD3. Candidate keys: `U_ID`, `U_USERNAME`, `U_EMAIL`. Primary key: `U_ID`. Normal form: BCNF.
- * '''Residual relation `R_A1`''' = `R_A − U_*` (30 attributes). Dependencies: FD4–FD11, which also imply `T_ID → H_ID` and `O_ID → H_ID` (through `U_ID, C_ID`). Candidate key and primary key: `T_ID`. Normal form: 2NF.
- * '''Dependency preservation:''' FD1–FD3 are in `R_USERS`, and FD4–FD11 are in `R_A1`. ✓
- * '''Lossless join:''' `R_USERS ∩ R_A1 = { U_ID }`, and `U_ID → R_USERS` (FD1). ✓
-
-=== Step 3NF-2 — transitive dependency through `C_ID` ===
-
- * '''Relation analyzed:''' `R_A1` (30 attributes), dependencies FD4–FD11, candidate key and primary key `T_ID`, normal form 2NF.
- * '''3NF violations:''' `C_ID → C_*`, `U_ID, C_ID → H_ID → H_*`, `M_ID → M_*, C_ID`, `O_ID → O_*, U_ID, M_ID`. '''Split first on `C_ID`''', the next leaf.
- * '''Decomposition dependency:''' `C_ID → C_*` (FD4).
- * '''New relation `R_CRYPTO`''' = `{ C_ID, C_* }` (4 attributes). Dependencies: FD4, FD5. Candidate keys: `C_ID`, `C_SYMBOL`. Primary key: `C_ID`. Normal form: BCNF.
- * '''Residual relation `R_A2`''' = `R_A1 − C_*` (27 attributes). Dependencies: FD6–FD11. Candidate key and primary key: `T_ID`. Normal form: 2NF.
- * '''Dependency preservation:''' FD4 and FD5 are in `R_CRYPTO`, and FD6–FD11 are in `R_A2`. ✓
- * '''Lossless join:''' `R_CRYPTO ∩ R_A2 = { C_ID }`, and `C_ID → R_CRYPTO` (FD4). ✓
-
-=== Step 3NF-3 — transitive dependency through `{U_ID, C_ID}` ===
-
- * '''Relation analyzed:''' `R_A2` (27 attributes), dependencies FD6–FD11, candidate key and primary key `T_ID`, normal form 2NF.
- * '''3NF violations:''' `U_ID, C_ID → H_ID → H_*`, `M_ID → M_*, C_ID`, `O_ID → O_*, U_ID, M_ID`. '''Split first on `{U_ID, C_ID}`''', because it must come before `Markets` takes `C_ID` away.
- * '''Decomposition dependency:''' `U_ID, C_ID → H_ID` (FD9), together with `H_ID → H_*` (FD8).
- * '''New relation `R_HOLDINGS`''' = `{ H_ID, H_*, U_ID, C_ID }` (8 attributes). Dependencies: FD8, FD9. Candidate keys: `H_ID`, `{U_ID, C_ID}`. Primary key: `H_ID`. Normal form: BCNF.
- * '''Residual relation `R_A3`''' = `R_A2 − { H_ID, H_* }` (21 attributes). Dependencies: FD6, FD7, FD10, FD11. Candidate key and primary key: `T_ID`. Normal form: 2NF.
- * '''Dependency preservation:''' FD8 and FD9 are in `R_HOLDINGS`, and the others are in `R_A3`. ✓
- * '''Lossless join:''' `R_HOLDINGS ∩ R_A3 = { U_ID, C_ID }`, and `U_ID, C_ID → H_ID → H_*`, so `{U_ID, C_ID} → R_HOLDINGS`. ✓
-
-=== Step 3NF-4 — transitive dependency through `M_ID` ===
-
- * '''Relation analyzed:''' `R_A3` (21 attributes), dependencies FD6, FD7, FD10, FD11, key `T_ID`, normal form 2NF.
- * '''3NF violations:''' `M_ID → M_*, C_ID` and `O_ID → O_*, U_ID, M_ID`. '''Split first on `M_ID`''', because `Orders` still needs `M_ID`.
- * '''Decomposition dependency:''' `M_ID → M_*, C_ID` (FD6).
- * '''New relation `R_MARKETS`''' = `{ M_ID, M_*, C_ID }` (5 attributes). Dependencies: FD6, FD7. Candidate keys: `M_ID`, `{C_ID, M_QUOTE_CURRENCY}`. Primary key: `M_ID`. Normal form: BCNF.
- * '''Residual relation `R_A4`''' = `R_A3 − { M_*, C_ID }` (17 attributes). No dependency left needs `C_ID`. Dependencies: FD10, FD11. Candidate key and primary key: `T_ID`. Normal form: 2NF.
- * '''Dependency preservation:''' FD6 and FD7 are in `R_MARKETS`, and FD10 and FD11 are in `R_A4`. ✓
- * '''Lossless join:''' `R_MARKETS ∩ R_A4 = { M_ID }`, and `M_ID → R_MARKETS` (FD6). ✓
-
-=== Step 3NF-5 — transitive dependency through `O_ID` ===
-
- * '''Relation analyzed:''' `R_A4` = `{ T_ID, T_*, O_ID, O_*, U_ID, M_ID }` (17 attributes), dependencies FD10, FD11, candidate key and primary key `T_ID`, normal form 2NF.
- * '''3NF violations:''' only `O_ID → O_*, U_ID, M_ID`. '''Split on `O_ID`'''.
- * '''Decomposition dependency:''' `O_ID → O_*, U_ID, M_ID` (FD10).
- * '''New relation `R_ORDERS`''' = `{ O_ID, O_*, U_ID, M_ID }` (11 attributes). Dependencies: FD10. Candidate key: `O_ID`. Normal form: BCNF.
- * '''Residual relation `R_TRANSACTIONS`''' = `R_A4 − { O_*, U_ID, M_ID }` = `{ T_ID, T_*, O_ID }` (7 attributes). Dependencies: FD11. Candidate key: `T_ID`. Normal form: BCNF. Keeping `U_ID` here would have left the transitive dependency `T_ID → O_ID → U_ID` inside the relation. `T_ID → U_ID` was removed from the cover as redundant, so nothing is lost.
- * '''Dependency preservation:''' FD10 is in `R_ORDERS`, and FD11 is in `R_TRANSACTIONS`. ✓
- * '''Lossless join:''' `R_ORDERS ∩ R_TRANSACTIONS = { O_ID }`, and `O_ID → R_ORDERS` (FD10). ✓
-
-'''Result of 3NF:''' `R_USERS`, `R_CRYPTO`, `R_HOLDINGS`, `R_MARKETS`, `R_ORDERS`,
-`R_TRANSACTIONS` (from `R_A`), and `R_B`, `R_C`, `R_D`, `R_E`, `R_F`, `S6` unchanged. 12
-relations, all in 3NF, and all except `S6` in BCNF. All 18 dependencies are preserved.
-
-== BCNF if possible ==
-
-BCNF requires '''every''' determinant of a non-trivial dependency to be a superkey, even when
-the dependent attribute is prime.
-
-||= Relation =||= Dependencies in force =||= Determinants =||= All superkeys? =||
-|| `R_USERS` || FD1, FD2, FD3 || `U_ID`, `U_USERNAME`, `U_EMAIL` || yes ||
-|| `R_CRYPTO` || FD4, FD5 || `C_ID`, `C_SYMBOL` || yes ||
-|| `R_MARKETS` || FD6, FD7 || `M_ID`, `{C_ID, M_QUOTE_CURRENCY}` || yes ||
-|| `R_HOLDINGS` || FD8, FD9 || `H_ID`, `{U_ID, C_ID}` || yes ||
-|| `R_ORDERS` || FD10 || `O_ID` || yes ||
-|| `R_TRANSACTIONS` || FD11 || `T_ID` || yes ||
-|| `R_B` || FD13 || `OE_ID` || yes ||
-|| `R_C` || FD12 || `MT_ID` || yes ||
-|| `R_D` || FD14, FD15 || `MC_ID`, `{M_ID, MC_TIMEFRAME, MC_CANDLE_TIME}` || yes ||
-|| `R_E` || FD16 || `W_ID` || yes ||
-|| `R_F` || FD17, FD18 || `WI_ID`, `{W_ID, C_ID}` || yes ||
-|| `S6` || `MC_ID → MC_TIMEFRAME, MC_CANDLE_TIME`; `WI_ID → W_ID`; derived ones such as `MT_ID, MC_TIMEFRAME, MC_CANDLE_TIME → MC_ID` and `MT_ID, W_ID → WI_ID` || `MC_ID`, `WI_ID`, `{MT_ID, MC_TIMEFRAME, MC_CANDLE_TIME}`, `{MT_ID, W_ID}`, … || '''no''' ||
-
-'''Dependencies that violate BCNF — only in `S6`.''' `MC_ID` determines `MC_TIMEFRAME` and
-`MC_CANDLE_TIME`, and `WI_ID` determines `W_ID`, but neither `MC_ID` nor `WI_ID` is a superkey
-of `S6`. 3NF allowed this because the dependent attributes are prime. BCNF does not. The derived
-dependencies all involve `W_ID` or `MC_TIMEFRAME`/`MC_CANDLE_TIME`, so they disappear once the
-two steps below remove those attributes.
-
-=== Step BCNF-1 — `MC_ID → MC_TIMEFRAME, MC_CANDLE_TIME` ===
-
- * '''Relation analyzed:''' `S6` (8 attributes), dependencies as in the table above, candidate keys K1–K4, primary key K1, normal form 3NF.
- * '''BCNF violations:''' `MC_ID → MC_TIMEFRAME, MC_CANDLE_TIME` and `WI_ID → W_ID`, and the derived ones that depend on them. '''Split first on `MC_ID`'''. The order does not matter here, because the two violations share no attribute.
- * '''Decomposition dependency:''' `MC_ID → MC_TIMEFRAME, MC_CANDLE_TIME`. `MC_ID` is not a superkey of `S6`.
- * '''New relation''' `{ MC_ID, MC_TIMEFRAME, MC_CANDLE_TIME }`. Dependencies: `MC_ID → MC_TIMEFRAME, MC_CANDLE_TIME`. Key: `MC_ID`. Normal form: BCNF. It is a projection of `R_D`, which already contains these attributes with the same key, so it adds no information and is merged into `R_D`.
- * '''Residual relation `S7`''' = `{ T_ID, OE_ID, MT_ID, MC_ID, W_ID, WI_ID }` (6 attributes). Dependencies: `WI_ID → W_ID`, and derived ones such as `MT_ID, W_ID → WI_ID`. Candidate keys: `{T_ID, OE_ID, MT_ID, MC_ID, WI_ID}` (K1) and `{T_ID, OE_ID, MT_ID, MC_ID, W_ID}` (K2). Normal form: 3NF.
- * '''Dependency preservation:''' no dependency of the cover is affected. FD14 and FD15 are in `R_D`. ✓
- * '''Lossless join:''' the intersection is `{ MC_ID }`, and `MC_ID → { MC_ID, MC_TIMEFRAME, MC_CANDLE_TIME }`. ✓
-
-=== Step BCNF-2 — `WI_ID → W_ID` ===
-
- * '''Relation analyzed:''' `S7` (6 attributes), dependencies `WI_ID → W_ID` and derived ones, candidate keys K1, K2, primary key K1, normal form 3NF.
- * '''BCNF violations:''' only `WI_ID → W_ID` (and the derived `MT_ID, W_ID → WI_ID`). '''Split on `WI_ID`'''.
- * '''Decomposition dependency:''' `WI_ID → W_ID`. `WI_ID` is not a superkey of `S7`.
- * '''New relation''' `{ WI_ID, W_ID }`. Dependencies: `WI_ID → W_ID`. Key: `WI_ID`. Normal form: BCNF. For the same reason as in BCNF-1, it is merged into `R_F`.
- * '''Residual relation `R_KEY`''' = `{ T_ID, OE_ID, MT_ID, MC_ID, WI_ID }` (5 attributes). No non-trivial dependency holds among these attributes. Candidate key: all five (= K1). Normal form: BCNF.
- * '''Dependency preservation:''' no dependency of the cover is affected. FD17 and FD18 are in `R_F`. The derived dependencies of `S6`/`S7` follow from FD12, FD15, FD17 and FD18, which are all preserved. ✓
- * '''Lossless join:''' the intersection is `{ WI_ID }`, and `WI_ID → { WI_ID, W_ID }`. ✓
-
-'''Result: every relation is in BCNF.''' The decomposition into these 12 relations is lossless
-(each of the 13 binary steps passed the test) and preserves all 18 dependencies of the
-canonical cover.
-
-== Final result and discussion ==
-
-=== Normalized relational model ===
-
-Each relation is followed by its keys (primary key first). An attribute that is the
-identifier of another relation is marked `→` with that relation.
-
-{{{
-R_USERS          (U_ID, U_USERNAME, U_EMAIL, U_FULL_NAME, U_PASSWORD_HASH,
-                  U_AVAILABLE_BALANCE, U_INVESTED_BALANCE, U_RESERVED_BALANCE,
-                  U_CREATED_AT, U_UPDATED_AT)
-                  keys: U_ID; U_USERNAME; U_EMAIL
-R_CRYPTO         (C_ID, C_SYMBOL, C_NAME, C_CREATED_AT)
-                  keys: C_ID; C_SYMBOL
-R_MARKETS        (M_ID, C_ID → R_CRYPTO, M_QUOTE_CURRENCY, M_IS_ACTIVE, M_CREATED_AT)
-                  keys: M_ID; {C_ID, M_QUOTE_CURRENCY}
-R_HOLDINGS       (H_ID, U_ID → R_USERS, C_ID → R_CRYPTO, H_QUANTITY,
-                  H_RESERVED_QUANTITY, H_AVG_PRICE, H_CREATED_AT, H_UPDATED_AT)
-                  keys: H_ID; {U_ID, C_ID}
-R_ORDERS         (O_ID, U_ID → R_USERS, M_ID → R_MARKETS, O_SIDE, O_TYPE, O_STATUS,
-                  O_QUANTITY, O_FILLED_QUANTITY, O_PRICE, O_PLACED_AT, O_EXECUTED_AT)
-                  key: O_ID
-R_TRANSACTIONS   (T_ID, O_ID → R_ORDERS, T_TYPE, T_AMOUNT, T_CURRENCY, T_CREATED_AT,
-                  T_DESCRIPTION)
-                  key: T_ID
-R_MARKET_TRADES  (MT_ID, M_ID → R_MARKETS, MT_EXECUTED_AT, MT_PRICE, MT_QUANTITY,
-                  MT_SIDE, MT_SOURCE, O_ID_FILLSBUY → R_ORDERS (nullable),
-                  O_ID_FILLSSELL → R_ORDERS (nullable))              [= R_C]
-                  key: MT_ID
-R_ORDER_EVENTS   (OE_ID, O_ID → R_ORDERS, OE_EVENT_TYPE, OE_QUANTITY, OE_PRICE,
-                  OE_STATUS_AFTER, OE_CREATED_AT)                     [= R_B]
-                  key: OE_ID
-R_MARKET_CANDLES (MC_ID, M_ID → R_MARKETS, MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW,
-                  MC_CLOSE, MC_VOLUME, MC_CANDLE_TIME)                [= R_D]
-                  keys: MC_ID; {M_ID, MC_TIMEFRAME, MC_CANDLE_TIME}
-R_WATCHLISTS     (W_ID, U_ID → R_USERS, W_NAME, W_CREATED_AT)         [= R_E]
-                  key: W_ID
-R_WATCHLIST_ITEMS(WI_ID, W_ID → R_WATCHLISTS, C_ID → R_CRYPTO, WI_ADDED_AT)  [= R_F]
-                  keys: WI_ID; {W_ID, C_ID}
-R_KEY            (T_ID, OE_ID, MT_ID, MC_ID, WI_ID)                   [= R_KEY]
-                  key: all five
-}}}
-
-=== Discussion ===
-
-'''The eleven data relations are the P2 design, with one difference''' (`transactions.user_id`,
-explained below). Each relation is one entity set of the ER model:
-
-||= P5 relation =||= P2 table =||= How the relationships appear =||
-|| `R_USERS` || `users` || — ||
-|| `R_CRYPTO` || `crypto` || — ||
-|| `R_MARKETS` || `markets` || `C_ID` = `crypto_id` (`QuotedOn`) ||
-|| `R_HOLDINGS` || `holdings` || `U_ID` = `user_id` (`Holds`), `C_ID` = `crypto_id` (`PositionIn`) ||
-|| `R_ORDERS` || `orders` || `U_ID` = `user_id` (`Places`), `M_ID` = `market_id` (`PlacedOn`) ||
-|| `R_TRANSACTIONS` || `transactions` || `O_ID` = `related_order` (`Settles`); P2 also stores `user_id` (`Records`), see below ||
-|| `R_MARKET_TRADES` || `market_trades` || `M_ID` = `market_id` (`Fills`), `O_ID_FILLSBUY` = `buy_order_id`, `O_ID_FILLSSELL` = `sell_order_id` ||
-|| `R_ORDER_EVENTS` || `order_events` || `O_ID` = `order_id` (`Logs`) ||
-|| `R_MARKET_CANDLES` || `market_candles` || `M_ID` = `market_id` (`Aggregates`) ||
-|| `R_WATCHLISTS` || `watchlists` || `U_ID` = `user_id` (`Owns`) ||
-|| `R_WATCHLIST_ITEMS` || `watchlist_items` || `W_ID` = `watchlist_id` (`Contains`), `C_ID` = `crypto_id` (`Lists`) ||
-
-The two methods produce the foreign keys differently. In P2 they come from a transformation
-rule: a 1:N relationship becomes a column on the N side. Here, each one appears because a
-dependency of kind (R), for example `O_ID → U_ID`, keeps the other entity's identifier in the
-same relation as the entity that depends on it. The candidate keys also match, including the
-composite ones (`{C_ID, M_QUOTE_CURRENCY}`, `{U_ID, C_ID}`, `{M_ID, MC_TIMEFRAME,
-MC_CANDLE_TIME}`, `{W_ID, C_ID}`). They are exactly the `UNIQUE` constraints in
-`schema_creation.sql`.
-
-'''The one difference: `transactions.user_id`.''' The decomposition drops `U_ID` from
-`R_TRANSACTIONS`, because `T_ID → U_ID` follows from `T_ID → O_ID` and `O_ID → U_ID`. That is
-correct for every ledger entry that settles an order. It does not work for a '''deposit'''.
-`Settles` is partial, so a deposit has no order, and without `user_id` a deposit would have no
-owner at all. The de-normalized relation cannot show this case. Every one of its tuples
-contains an order (every key contains `OE_ID`, and every order event has an order), so a
-ledger entry without an order cannot appear in it. P2 therefore keeps `user_id` (the
-relationship `Records`) as a deliberate exception. As a result, the implemented
-`transactions` table is in '''2NF but not in 3NF''' (`related_order → user_id` is a transitive
-dependency), and this is by design. For entries with an order,
-`transactions.user_id` repeats the order's user. The only code that sets `related_order` (the buy
-and sell inserts in `advanced_db.sql`) writes the user and the id of the same order row. No
-database constraint enforces this.
-
-'''Two order columns in `market_trades`.''' `FillsBuy` and `FillsSell` needed two role
-attributes already in the de-normalized relation, and both end up in `R_MARKET_TRADES`.
-They correspond to `buy_order_id` and `sell_order_id`.
-
-'''`R_KEY` belongs to the formal result, but it is not implemented as a table.''' It is the
-relation that contains a key of `R_EDUBERZA`, and the lossless-join result above holds for all
-12 relations ''including'' it. It records no fact of the domain. It only says which ledger
-entry, order event, trade, candle and watchlist item were put into the same tuple, and that
-combination exists only because we started from one single relation. Not implementing it is
-an implementation decision. The eleven implemented tables are not claimed to reconstruct
-`R_EDUBERZA` on their own. They keep every attribute and every dependency of the canonical
-cover, and that is what the application needs.
-
-'''`holdings.avg_price`''' is shown as a ''derived'' attribute in the ER model: it can be
-recomputed from the buy history. It is still stored, and that is a deliberate
-denormalisation (see [wiki:RelationalDesign] (section "Normalisation")).
-Normalisation cannot detect this. `H_ID → H_AVG_PRICE` is an ordinary functional dependency,
-because "derivable from rows of another entity" is a property of the application logic
-that maintains the value (see [wiki:UseCase0004],
-`ON CONFLICT … DO UPDATE`), not a dependency between attributes of one tuple.
-
-'''Which design is used going forward:''' P2's, unchanged. The eleven data relations coincide
-with the eleven tables of `schema_creation.sql` and
-`advanced_db.sql` column for column, except for the
-deliberately kept `transactions.user_id` explained above. So there are no database objects
-to restructure, and the prototype and the reports of P6/P7 keep working against the same
-schema.
-
-The table definitions in `server/db/schema_creation.sql` and `server/db/advanced_db.sql`:
-
-{{{
-CREATE TABLE project.users (
-    id                uuid            PRIMARY KEY DEFAULT gen_random_uuid(),
-    username          varchar(50)     NOT NULL UNIQUE,
-    email             varchar(255)    NOT NULL UNIQUE,
-    full_name         varchar(200),
-    password_hash     varchar(255)    NOT NULL,
-    available_balance numeric(18,4)   NOT NULL DEFAULT 0 CHECK (available_balance >= 0),
-    invested_balance  numeric(18,4)   NOT NULL DEFAULT 0 CHECK (invested_balance  >= 0),
-    -- P7: cash committed to the user's active buy orders, moved out of
-    -- available_balance when the order is placed and consumed as it fills.
-    reserved_balance  numeric(18,4)   NOT NULL DEFAULT 0 CHECK (reserved_balance  >= 0),
-    created_at        timestamptz     NOT NULL DEFAULT now(),
-    updated_at        timestamptz
-);
-
-CREATE TABLE project.crypto (
-    id         uuid         PRIMARY KEY DEFAULT gen_random_uuid(),
-    symbol     varchar(20)  NOT NULL UNIQUE,
-    name       varchar(255) NOT NULL,
-    created_at timestamptz  NOT NULL DEFAULT now()
-);
-
-CREATE TABLE project.markets (
-    id             uuid        PRIMARY KEY DEFAULT gen_random_uuid(),
-    crypto_id      uuid        NOT NULL REFERENCES project.crypto(id),
-    quote_currency char(3)     NOT NULL DEFAULT 'USD',
-    is_active      boolean     NOT NULL DEFAULT true,
-    created_at     timestamptz NOT NULL DEFAULT now(),
-    CONSTRAINT uq_markets UNIQUE (crypto_id, quote_currency)
-);
-
-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),
-    -- 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,
-    CONSTRAINT uq_holdings_user_crypto UNIQUE (user_id, crypto_id)
-);
-
-CREATE TABLE project.orders (
-    id          uuid           PRIMARY KEY DEFAULT gen_random_uuid(),
-    user_id     uuid           NOT NULL REFERENCES project.users(id)   ON DELETE CASCADE,
-    market_id   uuid           NOT NULL REFERENCES project.markets(id),
-    side        varchar(4)     NOT NULL CHECK (side   IN ('buy', 'sell')),
-    type        varchar(20)    NOT NULL CHECK (type   IN ('market', 'limit')),
-    status      varchar(20)    NOT NULL CHECK (status IN ('open', 'partially_filled', 'executed', 'cancelled')),
-    quantity    numeric(20,4)  NOT NULL CHECK (quantity > 0),
-    -- P7: how much of the order has been traded so far; remaining is
-    -- quantity - filled_quantity. Maintained from market_trades.
-    filled_quantity numeric(20,4) NOT NULL DEFAULT 0
-                               CHECK (filled_quantity >= 0 AND filled_quantity <= quantity),
-    price       numeric(18,6),
-    placed_at   timestamptz    NOT NULL DEFAULT now(),
-    executed_at timestamptz
-);
-
-CREATE TABLE project.transactions (
-    id            uuid           PRIMARY KEY DEFAULT gen_random_uuid(),
-    user_id       uuid           NOT NULL REFERENCES project.users(id) ON DELETE CASCADE,
-    type          varchar(50)    NOT NULL CHECK (type IN ('deposit', 'buy', 'sell', 'fee')),
-    amount        numeric(18,4)  NOT NULL,
-    currency      char(3)        NOT NULL DEFAULT 'USD',
-    related_order uuid           REFERENCES project.orders(id),
-    created_at    timestamptz    NOT NULL DEFAULT now(),
-    description   text
-);
-
-CREATE TABLE project.market_trades (
-    id          bigserial      PRIMARY KEY,
-    market_id   uuid           NOT NULL REFERENCES project.markets(id),
-    executed_at timestamptz    NOT NULL,
-    price       numeric(18,6)  NOT NULL CHECK (price    > 0),
-    quantity    numeric(20,6)  NOT NULL CHECK (quantity > 0),
-    side        varchar(4)     CHECK (side IN ('buy', 'sell')),
-    source      varchar(50)    NOT NULL DEFAULT 'simulation',
-    -- P7: the orders this trade filled. NULL on a side means the counterparty
-    -- was the simulated market (bot ticks have both NULL).
-    buy_order_id  uuid         REFERENCES project.orders(id),
-    sell_order_id uuid         REFERENCES project.orders(id)
-);
-
-CREATE TABLE project.market_candles (
-    id          bigserial      PRIMARY KEY,
-    market_id   uuid           NOT NULL REFERENCES project.markets(id),
-    timeframe   varchar(5)     NOT NULL CHECK (timeframe IN ('1m', '5m', '1h', '1d')),
-    open        numeric(18,6)  NOT NULL,
-    high        numeric(18,6)  NOT NULL,
-    low         numeric(18,6)  NOT NULL,
-    close       numeric(18,6)  NOT NULL,
-    volume      numeric(20,6)  NOT NULL,
-    candle_time timestamptz    NOT NULL,
-    CONSTRAINT uq_candle UNIQUE (market_id, timeframe, candle_time)
-);
-
-CREATE TABLE project.watchlists (
-    id         uuid         PRIMARY KEY DEFAULT gen_random_uuid(),
-    user_id    uuid         NOT NULL REFERENCES project.users(id) ON DELETE CASCADE,
-    name       varchar(100) NOT NULL,
-    created_at timestamptz  NOT NULL DEFAULT now()
-);
-
-CREATE TABLE project.watchlist_items (
-    id           uuid        PRIMARY KEY DEFAULT gen_random_uuid(),
-    watchlist_id uuid        NOT NULL REFERENCES project.watchlists(id) ON DELETE CASCADE,
-    crypto_id    uuid        NOT NULL REFERENCES project.crypto(id),
-    added_at     timestamptz NOT NULL DEFAULT now(),
-    CONSTRAINT uq_watchlist_crypto UNIQUE (watchlist_id, crypto_id)
-);
-
-CREATE TABLE project.order_events (
-    id           bigserial      PRIMARY KEY,
-    order_id     uuid           NOT NULL REFERENCES project.orders(id) ON DELETE CASCADE,
-    event_type   varchar(20)    NOT NULL
-                 CHECK (event_type IN ('placed', 'partially_filled', 'filled', 'cancelled')),
-    quantity     numeric(20,4)  NOT NULL,
-    price        numeric(18,6),
-    status_after varchar(20)    NOT NULL,
-    created_at   timestamptz    NOT NULL DEFAULT clock_timestamp()
-);
-}}}
Index: cs/P5-Normalization/wiki/NormalizationAIUsage.md
===================================================================
--- docs/P5-Normalization/wiki/NormalizationAIUsage.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,213 +1,0 @@
-= Normalization AI Usage =
-
-== Name of AI service/solution that was used ==
-
-'''Claude Code''' (Anthropic)
-
- * '''URL:''' `https://claude.com/claude-code`
- * '''Type of service/subscription:''' Claude subscription, model Claude Sonnet 5.
-
-== Final result ==
-
-=== Results in details / description ===
-
-The AI:
-
- * Built the single de-normalized relation `R_EDUBERZA` (68 attributes) by taking every
-   attribute from every entity and attributed relationship in
-   [wiki:ERModel], plus the foreign-key-style linking attributes
-   that the eight attributeless relationships need to be representable in one flat table at
-   all, and disambiguating every repeated name (`id`, `created_at`, `quantity`, `type`, …)
-   with a per-origin prefix (`U_`, `C_`, `M_`, `H_`, `O_`, `T_`, `MT_`, `MC_`, `W_`, `WI_`).
- * Derived the canonical cover (17 functional dependencies) directly from each entity's/
-   relationship's own key and its `UNIQUE` constraints, checked minimality of the composite
-   left-hand sides by example, and separately listed the functional dependencies that hold by
-   foreign-key substitution (e.g. `M_CRYPTO_ID → C_SYMBOL, C_NAME, C_CREATED_AT`) without
-   folding them into the canonical cover, since they are derivable rather than independent.
- * Computed the candidate keys of `R_EDUBERZA` from first principles: since `Holds`,
-   `Contains`, `Orders`, `Transactions`, `MarketTrades`, `MarketCandles` and `Watchlists` are
-   independent of each other, the only candidate keys are combinations that pick one
-   identifying attribute set per cluster — 96 in total — and selected the all-surrogate-key
-   combination as primary key, with a full closure computation shown step by step.
- * Decomposed `R_EDUBERZA` using 3NF/BCNF '''synthesis''' on the canonical cover (rather than the
-   binary decomposition algorithm), producing ten relations in one step, then separately
-   verified 3NF (checking every foreign-key-carried transitive dependency by name and showing
-   none of them lands inside any single resulting relation) and BCNF (a determinant/candidate-key
-   table for all ten relations) as distinct, explicit checks per the phase template, even
-   though no additional splitting was needed at either stage.
- * Verified dependency preservation (every canonical-cover FD's determinant and dependents
-   land inside exactly one resulting relation) and lossless join (every foreign key is
-   equated to the primary key it references, the textbook sufficient condition) explicitly,
-   rather than asserting them.
- * Compared the result to [wiki:RelationalDesign] and
-   found it identical relation-for-relation and key-for-key, including the less obvious
-   composite candidate keys; documented the one real difference (`holdings.avg_price` is a
-   derived/cached attribute — a property no single-relation normal form check can see) and
-   concluded, with reasoning, that P2's design should continue to be used unchanged.
- * Added a short cross-reference to this page from
-   [wiki:RelationalDesign], since the phase instructions
-   ask for Phase 2 documentation to be updated with the outcome of this phase.
- * Wrote Normalization following the section headings given in the phase
-   template exactly (`De-normalized database form` → `Functional dependencies` →
-   `Candidate keys and primary key` → `1NF decomposition` → `2NF decomposition` →
-   `3NF decomposition` → `BCNF if possible` → `Final result and discussion`).
-
-== Summary of AI involvement ==
-
-||= =||= This session — 2026-09-16 =||
-|| '''What I brought''' || The phase rubric for P5, pasted in full, and everything already produced in P1–P4 (in particular the `reserved_quantity` addition to `Holds` from the previous session) ||
-|| '''What the AI did''' || Built the de-normalized relation, derived the canonical cover, found the candidate keys, ran the 1NF→2NF→3NF→BCNF synthesis, and wrote the comparison against P2 ||
-|| '''What I decided''' || To let the AI carry out the full formal derivation rather than write my own first pass, since the rubric's own advice ("start from the canonical cover") is a mechanical method rather than a matter of taste; to keep P2's schema unchanged, per the AI's reasoning that the two designs coincide exactly ||
-
-This phase's rule is that AI is used '''to improve the student's own initial work''', and that
-any idea taken from the AI is logged as a change against that starting point. I did not
-produce an independent first attempt at the canonical cover or the decomposition before
-asking for this — I gave the AI the rubric directly and asked it to carry out the phase, the
-same way P1–P4 were produced (see
-[wiki:ERModelAIUsage] for that history). What I own here
-is checking the result: that the 68-attribute list in `R_EDUBERZA` really is every attribute
-of my P1 model with nothing missing or invented, that the functional dependencies match what
-I already know to be true of the model (each `UNIQUE` constraint in
-`schema_creation.sql` shows up as an alternate-key FD,
-and no others were invented), and that the final ten relations really do match
-[wiki:RelationalDesign] column for column — which I
-checked by reading both side by side rather than taking the AI's claim of a match on faith.
-
-The tables in `schema_creation.sql` that carry `UNIQUE` constraints:
-
-{{{
--- ============================================================================
--- USERS
--- Platform users. Each user has virtual (prop) balances used for simulation.
--- ============================================================================
-CREATE TABLE project.users (
-    id                uuid            PRIMARY KEY DEFAULT gen_random_uuid(),
-    username          varchar(50)     NOT NULL UNIQUE,
-    email             varchar(255)    NOT NULL UNIQUE,
-    full_name         varchar(200),
-    password_hash     varchar(255)    NOT NULL,
-    available_balance numeric(18,4)   NOT NULL DEFAULT 0 CHECK (available_balance >= 0),
-    invested_balance  numeric(18,4)   NOT NULL DEFAULT 0 CHECK (invested_balance  >= 0),
-    created_at        timestamptz     NOT NULL DEFAULT now(),
-    updated_at        timestamptz
-);
-
--- ============================================================================
--- CRYPTO
--- Catalog of crypto assets available on the platform.
--- ============================================================================
-CREATE TABLE project.crypto (
-    id         uuid         PRIMARY KEY DEFAULT gen_random_uuid(),
-    symbol     varchar(20)  NOT NULL UNIQUE,
-    name       varchar(255) NOT NULL,
-    created_at timestamptz  NOT NULL DEFAULT now()
-);
-
--- ============================================================================
--- MARKETS
--- A market is a (crypto, quote_currency) pair, e.g. BTC/USD.
--- ============================================================================
-CREATE TABLE project.markets (
-    id             uuid        PRIMARY KEY DEFAULT gen_random_uuid(),
-    crypto_id      uuid        NOT NULL REFERENCES project.crypto(id),
-    quote_currency char(3)     NOT NULL DEFAULT 'USD',
-    is_active      boolean     NOT NULL DEFAULT true,
-    created_at     timestamptz NOT NULL DEFAULT now(),
-    CONSTRAINT uq_markets UNIQUE (crypto_id, quote_currency)
-);
-
--- ============================================================================
--- HOLDINGS
--- Per-user crypto position with running weighted average entry price.
--- ============================================================================
-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),
-    -- 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,
-    CONSTRAINT uq_holdings_user_crypto UNIQUE (user_id, crypto_id)
-);
-}}}
-
-{{{
--- ============================================================================
--- MARKET CANDLES
--- OHLCV aggregates over standard timeframes.
--- ============================================================================
-CREATE TABLE project.market_candles (
-    id          bigserial      PRIMARY KEY,
-    market_id   uuid           NOT NULL REFERENCES project.markets(id),
-    timeframe   varchar(5)     NOT NULL CHECK (timeframe IN ('1m', '5m', '1h', '1d')),
-    open        numeric(18,6)  NOT NULL,
-    high        numeric(18,6)  NOT NULL,
-    low         numeric(18,6)  NOT NULL,
-    close       numeric(18,6)  NOT NULL,
-    volume      numeric(20,6)  NOT NULL,
-    candle_time timestamptz    NOT NULL,
-    CONSTRAINT uq_candle UNIQUE (market_id, timeframe, candle_time)
-);
-}}}
-
-{{{
-CREATE TABLE project.watchlist_items (
-    id           uuid        PRIMARY KEY DEFAULT gen_random_uuid(),
-    watchlist_id uuid        NOT NULL REFERENCES project.watchlists(id) ON DELETE CASCADE,
-    crypto_id    uuid        NOT NULL REFERENCES project.crypto(id),
-    added_at     timestamptz NOT NULL DEFAULT now(),
-    CONSTRAINT uq_watchlist_crypto UNIQUE (watchlist_id, crypto_id)
-);
-}}}
-
-== Entire AI usage log ==
-
-=== 2026-09-16 ===
-
-'''Intent:''' hand over the full P5 rubric and have the phase carried out end to end —
-de-normalized relation, functional dependencies, candidate keys, the four-normal-form
-decomposition, and the comparison against P2 — in one pass.
-
-'''Prompt (student, verbatim):'''
-> We will go phase by phase, here are the instructions for P5:
-> Instructions on Phase P5: Normalization
-> [the full task description was pasted: parts (a) initial de-normalized relation and
-> functional dependencies, (b) candidate keys and primary key selection, (c) step-by-step
-> decomposition to the highest possible normal form with the specific checklist of what each
-> decomposition step must document, (d) final result and discussion including updating Phase
-> 2 documentation and restructuring the database if warranted; the exact wiki page template
-> for `Normalization` and `NormalizationAIUsage`; and the phase's AI-use rules]
-
-'''Response (AI, summarised):'''
- * Re-read [wiki:ERModel] and
-   `schema_creation.sql` (see the excerpts above) to get the authoritative,
-   current attribute list (including `reserved_quantity`, added in the previous session) and
-   every `UNIQUE`/`CHECK` constraint that turns into an alternate-key functional dependency.
- * Worked out, before writing anything, that `Holds`/`Contains`/`Orders`/`Transactions`/
-   `MarketTrades`/`MarketCandles`/`Watchlists` are mutually independent record types, which is
-   what makes the primary key of the fully de-normalized relation a ten-attribute composite
-   rather than something smaller — and therefore what makes ''every'' non-key attribute violate
-   2NF simultaneously, rather than a handful needing to be peeled off one at a time.
- * Chose synthesis over the binary decomposition algorithm specifically because the rubric
-   recommends building the canonical cover first, which is what synthesis consumes directly.
- * Wrote Normalization.md and this page.
-
-'''What I decided:''' to accept the derivation as presented rather than rework it, since
-checking it against my own P1/P2 documents (attribute list, `UNIQUE` constraints, and the
-final ten relations) confirmed it, and to make no changes to `server/db/schema_creation.sql`
-for this phase, since the discussion section's conclusion — that P2's design is already the
-BCNF result — is one I verified myself rather than took on trust.
-
-> '''Student action required.''' Read Normalization.md end to end before
-> the defense — you will be expected to derive at least one of the ten relations' functional
-> dependencies and candidate keys live, and to explain why `holdings.avg_price` is not a
-> normal-form violation even though it is a stored, derivable value. Append any further
-> prompts here if you ask for revisions.
Index: cs/P6-AdvancedReports/AdvancedReports.md
===================================================================
--- docs/P6-AdvancedReports/AdvancedReports.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,340 +1,0 @@
-# Advanced Reports
-
-This is a solo project (see [UseCaseModel](../P3-UseCaseModel/UseCaseModel.md#realization-details-on-selection-of-the-most-important-use-cases)),
-so the rubric's "2 per team member" is 2 reports total. Both are implemented as
-single SQL statements, wrapped as callable SQL functions in
-[`schema_creation.sql`](../../server/db/schema_creation.sql) (`report_top_traders`,
-`report_market_performance`) so they are actual reports inside the prototype — menu
-options `[10]` and `[11]` in `server/reports.go` — not just documentation. No change to
-[ERModel](../P1-ConceptualModel/ERModel.md) or [RelationalDesign](../P2-RelationalDesign/RelationalDesign.md)
-was needed: both reports read `transactions`, `market_trades` and `orders`, all of which
-already carry everything required.
-
-### Notation used below
-
-Both solutions need grouping, aggregation and computed attributes that plain relational
-algebra has no notation for, so the relational-algebra sections use the standard *extended*
-operators:
-
-| Symbol | Meaning |
-|---|---|
-| `σ_cond(R)` | selection |
-| `π_list(R)` | projection — a list entry `expr → name` is a **generalized projection**: a computed attribute, not just a column reference |
-| `ρ_name(R)` | rename |
-| `R ⋈_cond S` | inner join |
-| `R ⟕_cond S` | left outer join (needed wherever a group can legitimately have zero matching rows on the other side, e.g. zero profitable periods, zero participating users) |
-| `γ_{grouping; agg → name, …}(R)` | grouping/aggregation |
-| `τ_attr(R)` | sort, for the presentation order only |
-
-## Top traders by realized performance
-
-### Data requirements description
-
-*"Which users actually made money, how much, how efficiently, and how consistently — over
-a quarter, a year, or several years?"* This is the natural crypto-exchange analogue of "which
-customers bring the most profit" from the phase brief: a Trader's `available_balance` and
-`invested_balance` (P1 `Users`) show a live snapshot, but they say nothing about performance
-*over a chosen window*, and nothing at all about whether a user's results are one lucky
-quarter or a repeatable pattern. All of it is derivable from
-[`transactions`](../../server/db/schema_creation.sql) as it already exists: every buy, sell
-and fee is one signed row there (see [UseCase0004](../P3-UseCaseModel/UseCase0004.md) and
-[UseCase0005](../P3-UseCaseModel/UseCase0005.md) for how each row is produced), so no new
-column or table is needed.
-
-Given a period `[from, to)`:
-
-- **Realized P/L** = `SUM(amount)` over that user's `buy`, `sell` and `fee` transactions in
-  the period (deposits excluded — they are not trading results).
-- **Total invested** = absolute value of the sum of that user's `buy` transactions in the
-  period (buy amounts are stored negative, per
-  [ERModel](../P1-ConceptualModel/ERModel.md#transactions)).
-- **ROI %** = realized P/L ÷ total invested × 100.
-- The period is additionally bucketed into **quarters** internally, regardless of how wide
-  `[from, to)` is, to measure:
-  - **Profitable / losing periods** — how many quarters inside the window had positive vs.
-    negative P/L.
-  - **Consistency %** = profitable periods ÷ total periods with any activity × 100 — two
-    users can have the same total P/L with very different risk profiles, and this is the
-    number that tells them apart.
-
-### Solution SQL
-
-Implemented as `project.report_top_traders(p_from, p_to)` in
-[`schema_creation.sql`](../../server/db/schema_creation.sql):
-
-```sql
-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;
-$$;
-```
-
-One `SELECT`, one `WITH` CTE — the CTE does the quarter bucketing per user, the outer query
-rolls those buckets up into the totals, the ROI/consistency percentages and the ranking.
-
-**Verified run.** [`reports_demo_data.sql`](../../server/db/reports_demo_data.sql) adds five
-quarters of round-trip trades (2025-07 through 2026-07) on top of the normal seed data
-specifically so this report has more than one period to work with — see that file's header
-for exactly what it inserts and why it is optional rather than part of `-init`. Run against
-PostgreSQL 16 with `data_load.sql` + `reports_demo_data.sql` loaded, through the actual CLI
-(`[10] Report: top traders`, range `2025-01-01` to `2026-09-17`):
-
-```
-  Username      Realized P/L        Invested       ROI %   Prof.    Loss   Total  Consist. %
-  ------------------------------------------------------------------------------------------
-  bob              +991.0000       6300.0000       15.73       3       0       3      100.00
-  alice            -475.0000      24250.0000       -1.96       2       3       5       40.00
-```
-
-Sorting by raw P/L alone would rank alice above bob if alice's numbers were all positive; here
-it does the opposite, and that is the point of the report — alice traded a much larger total
-(and one of her seeded round trips landed in the same quarter as the ETH buy already in
-`data_load.sql`, tipping that quarter into a loss), while bob's three quarters were smaller
-but every one of them profitable, giving him both the better ROI and a perfect consistency
-score. A single "total profit" column would have hidden that difference completely.
-
-### Solution Relational Algebra
-
-```
-T_period  = σ_{type ∈ {buy,sell,fee} ∧ created_at ≥ from ∧ created_at < to} (Transactions)
-
-T_tagged  = π_{user_id, created_at, amount,
-               (type = 'buy' ? amount : 0) → buy_amt} (T_period)
-
-Periods   = γ_{user_id, quarter(created_at) → period ;
-               SUM(amount) → period_pl, SUM(buy_amt) → period_buy} (T_tagged)
-
-Totals      = γ_{user_id ; SUM(period_pl) → realized_pl,
-                 ABS(SUM(period_buy)) → total_invested,
-                 COUNT(*) → total_periods} (Periods)
-Profitable  = γ_{user_id ; COUNT(*) → profitable_periods} (σ_{period_pl > 0} (Periods))
-Losing      = γ_{user_id ; COUNT(*) → losing_periods}     (σ_{period_pl < 0} (Periods))
-
-Combined  = (Totals ⟕_{user_id} Profitable) ⟕_{user_id} Losing
-
-Ranked    = π_{user_id, realized_pl, total_invested,
-               (realized_pl / total_invested × 100) → roi_pct,
-               COALESCE(profitable_periods, 0) → profitable_periods,
-               COALESCE(losing_periods, 0) → losing_periods,
-               total_periods,
-               (COALESCE(profitable_periods, 0) / total_periods × 100) → consistency_pct}
-             (Combined)
-
-Result    = τ_{realized_pl ↓} (π_{username, realized_pl, total_invested, roi_pct,
-               profitable_periods, losing_periods, total_periods, consistency_pct}
-               (Ranked ⋈_{user_id = id} Users))
-```
-
-`Totals`/`Profitable`/`Losing` are three separate groupings of the same `Periods` relation
-because plain aggregation has no built-in "count only where X" operator; the two outer joins
-recombine them (`⟕`, not `⋈`, because a user with zero losing quarters must still appear with
-`losing_periods = 0`, not disappear from the result).
-
-## Market performance leaderboard
-
-### Data requirements description
-
-*"Which markets were actually worth making — high volume, real price movement, real user
-interest — over a chosen period?"* This is the "products that bring the most profit" /
-"good locations" family of question from the phase brief, translated to markets instead of
-physical products: a market with heavy volume but a dead price, or a big price swing nobody
-actually traded, are both misleading on their own; this report puts volume, trade count,
-price return and user participation side by side so a market's performance over a
-quarter/year/multi-year window can be judged as a whole, not from one number in isolation.
-Everything needed already exists: `market_trades` is the single source of truth for price and
-volume for every market ([PrototypeImplementation](../P4-Prototype/PrototypeImplementation.md#what-the-prototype-demonstrates-about-the-database-design)),
-and `orders` is the only place a specific user is tied to a specific market
-([ERModel](../P1-ConceptualModel/ERModel.md#placedon--markets-1--orders-n-total-on-orders)) —
-`market_trades` deliberately has no `user_id` column, since it also records the market
-simulator's own fills.
-
-Given a period `[from, to)`, per market:
-
-- **Total volume** = `SUM(quantity)` over its trades in the period.
-- **Trade count** = `COUNT(*)` over the same trades (real fills and simulated fills alike —
-  this is activity, not just user activity).
-- **Average trading price** = `AVG(price)` over the same trades.
-- **Market return %** = `(last trade price − first trade price) ÷ first trade price × 100`,
-  ordering trades by `executed_at` inside the period.
-- **Participating users** = `COUNT(DISTINCT user_id)` from that market's **executed orders**
-  in the period — the only correct source, since `market_trades` cannot answer this question
-  at all.
-
-### Solution SQL
-
-Implemented as `project.report_market_performance(p_from, p_to)` in
-[`schema_creation.sql`](../../server/db/schema_creation.sql):
-
-```sql
-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,
-    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,
-            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,
-        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;
-$$;
-```
-
-`FIRST_VALUE`/`LAST_VALUE` pick the period's opening and closing price per market without a
-self-join; `LEFT JOIN participation` is required, not optional — a market can have trades
-from the simulator alone and legitimately zero participating users, and it must still show
-`0`, not disappear from the report.
-
-**Verified run.** Same seed as above (`data_load.sql` + `reports_demo_data.sql`, which also
-adds a BTC/USD uptrend and an ETH/USD downtrend across the same five quarters — see that
-file). Run through the CLI (`[11] Report: market performance`, `2025-01-01` to `2026-09-17`):
-
-```
-  Symbol  Quote        Volume    Trades       Avg Price      Return %     Users
-  -----------------------------------------------------------------------------
-  DOGE    USD      29500.0000         3        0.120583         +2.95         0
-  ADA     USD       2500.0000         3        0.450750         +1.62         0
-  SOL     USD         23.5000         3      165.283333         +1.13         0
-  ETH     USD         14.3500         8     3622.312500        -12.00         2
-  BTC     USD          3.9750         9    59447.400000        +67.85         2
-```
-
-BTC/USD and ETH/USD are the only two markets with historical (multi-quarter) data seeded, and
-they show it: BTC's price nearly tripled over the period (`+67.85%`), while ETH quietly lost
-`12%`. ADA/SOL/DOGE only have the few minutes of `data_load.sql`'s own recent seed trades, so
-their return numbers reflect that narrow window, and their `0` participating users is correct
-— `data_load.sql` seeds trade history for every market but only ever places an *order* on ETH.
-
-A price-volatility column (standard deviation of trade price) was dropped from this report
-after review — with only a handful of trades per market in most periods it read as noise
-rather than signal, and total volume plus return already carry the useful information.
-
-### Solution Relational Algebra
-
-```
-MT_period  = σ_{executed_at ≥ from ∧ executed_at < to} (MarketTrades)
-
-Bounds     = γ_{market_id ; MIN(executed_at) → t_first, MAX(executed_at) → t_last} (MT_period)
-
-FirstPx    = π_{market_id, price → first_price}
-               (MT_period ⋈_{MT_period.market_id = Bounds.market_id
-                              ∧ executed_at = t_first} Bounds)
-LastPx     = π_{market_id, price → last_price}
-               (MT_period ⋈_{MT_period.market_id = Bounds.market_id
-                              ∧ executed_at = t_last} Bounds)
-
-Stats      = γ_{market_id ; SUM(quantity) → total_volume, COUNT(*) → trade_count,
-                AVG(price) → avg_price} (MT_period)
-
-MarketStats = (Stats ⋈_{market_id} FirstPx) ⋈_{market_id} LastPx
-
-O_period      = σ_{status = 'executed' ∧ executed_at ≥ from ∧ executed_at < to} (Orders)
-Participation = γ_{market_id ; COUNT_DISTINCT(user_id) → participating_users} (O_period)
-
-Joined = ((MarketStats ⟕_{market_id} Participation)
-            ⋈_{market_id = id} Markets) ⋈_{crypto_id = id} Crypto
-
-Result = τ_{total_volume ↓} (
-           π_{symbol, quote_currency, total_volume, trade_count, avg_price,
-              (last_price − first_price) / first_price × 100 → market_return_pct,
-              COALESCE(participating_users, 0) → participating_users}
-             (Joined) )
-```
-
-`FirstPx`/`LastPx` express `FIRST_VALUE`/`LAST_VALUE` — which have no classical relational-
-algebra equivalent — as an aggregation for the boundary timestamp per market followed by a
-self-join back to `MarketTrades` to recover the price at that timestamp; this is the standard
-way to express "value at the extreme of a group" in extended relational algebra.
-
-## AI usage
-
-AI was used in this phase and is logged in full, per the course rule for P1 onward.
-
-- **Phase log:** [AdvancedReportsAIUsage.md](AdvancedReportsAIUsage.md) — service used, what
-  the AI produced, and what I decided myself.
-
-**Service:** Claude Code (Anthropic), https://claude.com/claude-code — Claude subscription,
-model Claude Sonnet 5.
-
-**In short:** I specified both report questions in full — including the exact formulas for
-P/L, ROI, consistency, market return, volatility and user participation — and asked the AI to
-turn them into working SQL, wire them into the prototype as real reports, build the
-relational-algebra equivalents, and produce demonstration data rich enough to show the
-reports doing something non-trivial. In a follow-up, I asked for the price-volatility column
-to be dropped from the market performance report — see
-[AdvancedReportsAIUsage](AdvancedReportsAIUsage.md#follow-up--2026-09-17) for that change.
Index: cs/P6-AdvancedReports/AdvancedReportsAIUsage.md
===================================================================
--- docs/P6-AdvancedReports/AdvancedReportsAIUsage.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,157 +1,0 @@
-# Advanced Reports AI Usage
-
-## Name of AI service/solution that was used
-
-**Claude Code** (Anthropic)
-
-- **URL:** https://claude.com/claude-code
-- **Type of service/subscription:** Claude subscription, model Claude Sonnet 5.
-
-## Final result
-
-### Diagram
-
-None. Both reports read `transactions`, `market_trades`, `orders`, `markets`, `crypto` and
-`users` exactly as they already existed after
-[Normalization](../P5-Normalization/Normalization.md) — no attribute or relation was missing,
-so [ERModel](../P1-ConceptualModel/ERModel.md) and
-[RelationalDesign](../P2-RelationalDesign/RelationalDesign.md) needed no changes and there is
-no new diagram for this phase. This is stated explicitly rather than left implicit because the
-phase rubric specifically calls out modifying the design as the fallback when a good report
-idea can't be answered by the data on hand — it wasn't needed here.
-
-### Results in details / description
-
-The AI:
-
-- Turned my two fully-specified report questions (the exact P/L, ROI, consistency, volume,
-  return, volatility and participation formulas were mine) into two single-statement SQL
-  queries, each wrapped as a `LANGUAGE sql STABLE` function
-  (`project.report_top_traders`, `project.report_market_performance`) in
-  [`schema_creation.sql`](../../server/db/schema_creation.sql), so the phase's "just one SQL
-  query" requirement is met by the query text itself, while still giving the prototype a
-  clean, parameterised, named thing to call.
-- Wired both into the running CLI as real menu options — `server/reports.go`, options
-  `[10]`/`[11]` in `server/cli.go` — rather than leaving them as documentation-only SQL, per
-  the phase's own framing ("used as reports within your application").
-- Wrote the relational-algebra equivalent of each query, including how to express
-  `FIRST_VALUE`/`LAST_VALUE` (which have no classical RA equivalent) as an aggregation for the
-  boundary timestamp followed by a self-join, and how to express `FILTER (WHERE …)`-style
-  conditional counts as separate groupings recombined with left outer joins.
-- Noticed that the existing `data_load.sql` seed data (a few minutes of trade history) cannot
-  demonstrate either report meaningfully — everything falls into one quarter, so "consistency"
-  and "market return over time" have nothing to show — and wrote
-  [`reports_demo_data.sql`](../../server/db/reports_demo_data.sql), an optional, separate,
-  idempotent script adding five quarters of synthetic transactions, market trades and executed
-  orders, deliberately excluded from `-init`/`-load-data` so it cannot disturb the balances the
-  other use cases' documented "verified run" sections depend on.
-- Ran both reports against a live PostgreSQL 16 database with that demo data loaded, through
-  the actual CLI, and used the real output (including a run where alice's seeded quarter
-  interacted with a pre-existing `data_load.sql` transaction and flipped a profitable quarter
-  into a loss) as the verified evidence in [AdvancedReports.md](AdvancedReports.md), rather
-  than inventing example numbers.
-
-## Summary of AI involvement
-
-| | This session — 2026-09-16 |
-|---|---|
-| **What I brought** | The phase rubric, plus both report questions fully specified down to the exact aggregate formulas |
-| **What the AI did** | Wrote the SQL, wrote the relational algebra, wired the reports into the CLI, designed and ran the demonstration data, verified everything against a live database |
-| **What I decided** | To keep both reports as SQL functions rather than plain ad-hoc queries so they are actually usable from the application; to accept the AI's synthetic multi-quarter demo dataset rather than wait for enough real usage history to accumulate |
-
-The two ideas and their formulas were mine, specified in enough detail (P/L as the sum of
-buy+sell+fee transactions, ROI relative to total buys, consistency as a share of profitable
-quarters, market return as first-vs-last trade price, volatility as price standard deviation,
-participation from orders rather than trades) that there was no separate "AI alternative
-idea" to borrow from and document a change against, unlike the more open-ended P1–P3 phases —
-the AI's job here was implementation and verification of a fully-specified design, which is
-what is logged above and in the prompt below.
-
-## Entire AI usage log
-
-### 2026-09-16
-
-**Intent:** hand over the P6 rubric together with both report ideas, fully specified, and
-have the whole phase — SQL, relational algebra, prototype integration, and demonstration data
-— produced and verified in one pass.
-
-**Prompt (student, verbatim):**
-> Phase P6: Complex DB Reports (SQL, Stored Procedures, Relational Algebra)
-> [the full phase rubric was pasted: 2 complex analytical reports solvable each with one SQL
-> query, usable as reports within the application, with a note that helper views/functions/
-> procedures are acceptable when pure SQL isn't enough, that the design should be extended if
-> a good idea needs data that doesn't exist yet, a requirement for the corresponding relational
-> algebra, the exact `AdvancedReports`/`AdvancedReportsAIUsage` wiki templates, and the phase's
-> AI-use rules]
->
-> I will describe for you the 2 more complex sql queries:
-> 1. Find the users who performed the best in a selected quarter, year, or multi-year period
->    based on their realized trading performance, and show their total profit/loss, total
->    amount invested, ROI, number of profitable and losing periods, and consistency of
->    returns. Calculate realized P/L as SUM(sell transactions + buy transactions + fee
->    transactions), calculate ROI as realized P/L / absolute value of total buy transactions ×
->    100, and calculate consistency as profitable periods / total periods × 100.
-> 2. Find which cryptocurrency markets performed best during a selected quarter, year, or
->    multi-year period based on trading activity and price behavior, and report the markets
->    with the highest trading volume, number of trades, number of participating users, and
->    price return. Calculate total volume as SUM(trade quantity), average trading price as
->    AVG(price), market return as (last trade price − first trade price) / first trade price ×
->    100, price volatility as standard deviation of trade prices, and user participation as
->    COUNT(DISTINCT user_id) from the related executed orders.
-
-**Response (AI, summarised):**
-- Confirmed both report questions were answerable from the existing schema with no new
-  columns or tables, and said so explicitly rather than silently skipping that check.
-- Wrote `report_top_traders` and `report_market_performance` as SQL functions, added to
-  `schema_creation.sql` under a new `REPORTS (P6)` section.
-- Wrote `server/reports.go` (`ShowTopTraders`, `ShowMarketPerformance`, a shared
-  `promptPeriod` helper) and added menu options `[10]`/`[11]` to `server/cli.go`.
-- Wrote `server/db/reports_demo_data.sql` for a richer, multi-quarter demonstration, and
-  documented in its own header exactly why it is optional and what it deliberately does not
-  touch (user balances, holdings).
-- Built the prototype, ran both reports against the live database with the demo data loaded,
-  cross-checked the SQL output against hand-computed expected values for both seeded users,
-  and used that real output as the verified evidence.
-- Wrote out the relational-algebra derivation for each query and [AdvancedReports.md](AdvancedReports.md).
-
-**What I decided:** to keep the demo dataset out of `-init`/`-load-data` rather than merge it
-into `data_load.sql`, since the other phases' documented expected values (specific balances in
-[BuildInstructions](../P4-Prototype/BuildInstructions.md)) depend on the seed data staying
-exactly as it is.
-
-> **Student action required.** Read [AdvancedReports.md](AdvancedReports.md) end to end
-> before the defense, and be ready to compute one period's realized P/L or one market's return
-> by hand from the raw `transactions`/`market_trades` rows — the numbers in the verified run
-> are real output, not invented, so they can be checked against
-> [`reports_demo_data.sql`](../../server/db/reports_demo_data.sql) directly. Append any further
-> prompts here if you ask for revisions.
-
-### Follow-up — 2026-09-17
-
-**Intent:** simplify the market performance report after seeing it run — with only a handful
-of trades per market in most periods, price volatility read as noise rather than a useful
-signal.
-
-**Prompt (student, verbatim):**
-> Can you tell me the exact database_url so I can execute the schema creating and the data_load?
->
-> From the Market Performance can we remove Volatility, I don't think that is that useful, also
-> can you tell me what is users column?
-
-**Response (AI, summarised):**
-- Gave the local dev connection string (`postgresql://bp_project:1234@localhost:5433/bp_database`,
-  from `.env`) and explained `participating_users` is `COUNT(DISTINCT user_id)` from that
-  market's executed orders, not from `market_trades` (which has no `user_id`).
-- Removed `price_volatility`/`STDDEV(price)` from `report_market_performance()` in
-  `schema_creation.sql`, from its `RETURNS TABLE` and its `SELECT`, and from the corresponding
-  column in `server/reports.go`'s `ShowMarketPerformance`.
-- Updated the relational algebra (`Stats`, `Result`) and the SQL listing, verified run output,
-  and prose in [AdvancedReports.md](AdvancedReports.md) to match, and added a short note
-  explaining why the column was dropped.
-- Rebuilt, re-ran `-init` and `reports_demo_data.sql` against the live database, and
-  re-verified the market performance report through the actual CLI before and after the
-  change to confirm only the one column disappeared and every other number is unchanged.
-
-**What I decided:** to drop the column entirely rather than keep it computed-but-hidden,
-since an unused computation left in the query is exactly the kind of thing that should not
-survive a review.
Index: cs/P6-AdvancedReports/wiki/AdvancedReports.md
===================================================================
--- docs/P6-AdvancedReports/wiki/AdvancedReports.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,477 +1,0 @@
-= Advanced Reports =
-
-This is a solo project (see [wiki:UseCaseModel]),
-so the rubric's "2 per team member" is 2 reports total. Both are implemented as
-single SQL statements, wrapped as callable SQL functions in
-`schema_creation.sql` (`report_top_traders`,
-`report_market_performance`) so they are actual reports inside the prototype — menu
-options `[10]` and `[11]` in `server/reports.go` — not just documentation. No change to
-[wiki:ERModel] or [wiki:RelationalDesign]
-was needed: both reports read `transactions`, `market_trades` and `orders`, all of which
-already carry everything required.
-
-=== Notation used below ===
-
-Both solutions need grouping, aggregation and computed attributes that plain relational
-algebra has no notation for, so the relational-algebra sections use the standard ''extended''
-operators:
-
-||= Symbol =||= Meaning =||
-|| `σ_cond(R)` || selection ||
-|| `π_list(R)` || projection — a list entry `expr → name` is a '''generalized projection''': a computed attribute, not just a column reference ||
-|| `ρ_name(R)` || rename ||
-|| `R ⋈_cond S` || inner join ||
-|| `R ⟕_cond S` || left outer join (needed wherever a group can legitimately have zero matching rows on the other side, e.g. zero profitable periods, zero participating users) ||
-|| `γ_{grouping; agg → name, …}(R)` || grouping/aggregation ||
-|| `τ_attr(R)` || sort, for the presentation order only ||
-
-== Top traders by realized performance ==
-
-=== Data requirements description ===
-
-''"Which users actually made money, how much, how efficiently, and how consistently — over
-a quarter, a year, or several years?"'' This is the natural crypto-exchange analogue of "which
-customers bring the most profit" from the phase brief: a Trader's `available_balance` and
-`invested_balance` (P1 `Users`) show a live snapshot, but they say nothing about performance
-''over a chosen window'', and nothing at all about whether a user's results are one lucky
-quarter or a repeatable pattern. All of it is derivable from
-`transactions` (defined in `schema_creation.sql`) as it already exists: every buy, sell
-and fee is one signed row there (see [wiki:UseCase0004] and
-[wiki:UseCase0005] for how each row is produced), so no new
-column or table is needed.
-
-The `transactions` table, from `schema_creation.sql`:
-
-{{{
-CREATE TABLE project.transactions (
-    id            uuid           PRIMARY KEY DEFAULT gen_random_uuid(),
-    user_id       uuid           NOT NULL REFERENCES project.users(id) ON DELETE CASCADE,
-    type          varchar(50)    NOT NULL CHECK (type IN ('deposit', 'buy', 'sell', 'fee')),
-    amount        numeric(18,4)  NOT NULL,
-    currency      char(3)        NOT NULL DEFAULT 'USD',
-    related_order uuid           REFERENCES project.orders(id),
-    created_at    timestamptz    NOT NULL DEFAULT now(),
-    description   text
-);
-}}}
-
-Given a period `[from, to)`:
-
- * '''Realized P/L''' = `SUM(amount)` over that user's `buy`, `sell` and `fee` transactions in
-   the period (deposits excluded — they are not trading results).
- * '''Total invested''' = absolute value of the sum of that user's `buy` transactions in the
-   period (buy amounts are stored negative, per
-   [wiki:ERModel]).
- * '''ROI %''' = realized P/L ÷ total invested × 100.
- * The period is additionally bucketed into '''quarters''' internally, regardless of how wide
-   `[from, to)` is, to measure:
-   * '''Profitable / losing periods''' — how many quarters inside the window had positive vs.
-     negative P/L.
-   * '''Consistency %''' = profitable periods ÷ total periods with any activity × 100 — two
-     users can have the same total P/L with very different risk profiles, and this is the
-     number that tells them apart.
-
-=== Solution SQL ===
-
-Implemented as `project.report_top_traders(p_from, p_to)` in
-`schema_creation.sql`:
-
-{{{
-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;
-$$;
-}}}
-
-One `SELECT`, one `WITH` CTE — the CTE does the quarter bucketing per user, the outer query
-rolls those buckets up into the totals, the ROI/consistency percentages and the ranking.
-
-'''Verified run.''' `reports_demo_data.sql` adds five
-quarters of round-trip trades (2025-07 through 2026-07) on top of the normal seed data
-specifically so this report has more than one period to work with — see that file's header
-(shown in full in the Demonstration data section below)
-for exactly what it inserts and why it is optional rather than part of `-init`. Run against
-PostgreSQL 16 with `data_load.sql` + `reports_demo_data.sql` loaded, through the actual CLI
-(`[10] Report: top traders`, range `2025-01-01` to `2026-09-17`):
-
-{{{
-  Username      Realized P/L        Invested       ROI %   Prof.    Loss   Total  Consist. %
-  ------------------------------------------------------------------------------------------
-  bob              +991.0000       6300.0000       15.73       3       0       3      100.00
-  alice            -475.0000      24250.0000       -1.96       2       3       5       40.00
-}}}
-
-Sorting by raw P/L alone would rank alice above bob if alice's numbers were all positive; here
-it does the opposite, and that is the point of the report — alice traded a much larger total
-(and one of her seeded round trips landed in the same quarter as the ETH buy already in
-`data_load.sql`, tipping that quarter into a loss), while bob's three quarters were smaller
-but every one of them profitable, giving him both the better ROI and a perfect consistency
-score. A single "total profit" column would have hidden that difference completely.
-
-=== Solution Relational Algebra ===
-
-{{{
-T_period  = σ_{type ∈ {buy,sell,fee} ∧ created_at ≥ from ∧ created_at < to} (Transactions)
-
-T_tagged  = π_{user_id, created_at, amount,
-               (type = 'buy' ? amount : 0) → buy_amt} (T_period)
-
-Periods   = γ_{user_id, quarter(created_at) → period ;
-               SUM(amount) → period_pl, SUM(buy_amt) → period_buy} (T_tagged)
-
-Totals      = γ_{user_id ; SUM(period_pl) → realized_pl,
-                 ABS(SUM(period_buy)) → total_invested,
-                 COUNT(*) → total_periods} (Periods)
-Profitable  = γ_{user_id ; COUNT(*) → profitable_periods} (σ_{period_pl > 0} (Periods))
-Losing      = γ_{user_id ; COUNT(*) → losing_periods}     (σ_{period_pl < 0} (Periods))
-
-Combined  = (Totals ⟕_{user_id} Profitable) ⟕_{user_id} Losing
-
-Ranked    = π_{user_id, realized_pl, total_invested,
-               (realized_pl / total_invested × 100) → roi_pct,
-               COALESCE(profitable_periods, 0) → profitable_periods,
-               COALESCE(losing_periods, 0) → losing_periods,
-               total_periods,
-               (COALESCE(profitable_periods, 0) / total_periods × 100) → consistency_pct}
-             (Combined)
-
-Result    = τ_{realized_pl ↓} (π_{username, realized_pl, total_invested, roi_pct,
-               profitable_periods, losing_periods, total_periods, consistency_pct}
-               (Ranked ⋈_{user_id = id} Users))
-}}}
-
-`Totals`/`Profitable`/`Losing` are three separate groupings of the same `Periods` relation
-because plain aggregation has no built-in "count only where X" operator; the two outer joins
-recombine them (`⟕`, not `⋈`, because a user with zero losing quarters must still appear with
-`losing_periods = 0`, not disappear from the result).
-
-== Market performance leaderboard ==
-
-=== Data requirements description ===
-
-''"Which markets were actually worth making — high volume, real price movement, real user
-interest — over a chosen period?"'' This is the "products that bring the most profit" /
-"good locations" family of question from the phase brief, translated to markets instead of
-physical products: a market with heavy volume but a dead price, or a big price swing nobody
-actually traded, are both misleading on their own; this report puts volume, trade count,
-price return and user participation side by side so a market's performance over a
-quarter/year/multi-year window can be judged as a whole, not from one number in isolation.
-Everything needed already exists: `market_trades` is the single source of truth for price and
-volume for every market ([wiki:PrototypeImplementation]),
-and `orders` is the only place a specific user is tied to a specific market
-([wiki:ERModel]) —
-`market_trades` deliberately has no `user_id` column, since it also records the market
-simulator's own fills.
-
-Given a period `[from, to)`, per market:
-
- * '''Total volume''' = `SUM(quantity)` over its trades in the period.
- * '''Trade count''' = `COUNT(*)` over the same trades (real fills and simulated fills alike —
-   this is activity, not just user activity).
- * '''Average trading price''' = `AVG(price)` over the same trades.
- * '''Market return %''' = `(last trade price − first trade price) ÷ first trade price × 100`,
-   ordering trades by `executed_at` inside the period.
- * '''Participating users''' = `COUNT(DISTINCT user_id)` from that market's '''executed orders'''
-   in the period — the only correct source, since `market_trades` cannot answer this question
-   at all.
-
-=== Solution SQL ===
-
-Implemented as `project.report_market_performance(p_from, p_to)` in
-`schema_creation.sql`:
-
-{{{
-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,
-    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,
-            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,
-        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;
-$$;
-}}}
-
-`FIRST_VALUE`/`LAST_VALUE` pick the period's opening and closing price per market without a
-self-join; `LEFT JOIN participation` is required, not optional — a market can have trades
-from the simulator alone and legitimately zero participating users, and it must still show
-`0`, not disappear from the report.
-
-'''Verified run.''' Same seed as above (`data_load.sql` + `reports_demo_data.sql`, which also
-adds a BTC/USD uptrend and an ETH/USD downtrend across the same five quarters — see that
-file in the Demonstration data section below). Run through the CLI (`[11] Report: market performance`, `2025-01-01` to `2026-09-17`):
-
-{{{
-  Symbol  Quote        Volume    Trades       Avg Price      Return %     Users
-  -----------------------------------------------------------------------------
-  DOGE    USD      29500.0000         3        0.120583         +2.95         0
-  ADA     USD       2500.0000         3        0.450750         +1.62         0
-  SOL     USD         23.5000         3      165.283333         +1.13         0
-  ETH     USD         14.3500         8     3622.312500        -12.00         2
-  BTC     USD          3.9750         9    59447.400000        +67.85         2
-}}}
-
-BTC/USD and ETH/USD are the only two markets with historical (multi-quarter) data seeded, and
-they show it: BTC's price nearly tripled over the period (`+67.85%`), while ETH quietly lost
-`12%`. ADA/SOL/DOGE only have the few minutes of `data_load.sql`'s own recent seed trades, so
-their return numbers reflect that narrow window, and their `0` participating users is correct
-— `data_load.sql` seeds trade history for every market but only ever places an ''order'' on ETH.
-
-A price-volatility column (standard deviation of trade price) was dropped from this report
-after review — with only a handful of trades per market in most periods it read as noise
-rather than signal, and total volume plus return already carry the useful information.
-
-=== Solution Relational Algebra ===
-
-{{{
-MT_period  = σ_{executed_at ≥ from ∧ executed_at < to} (MarketTrades)
-
-Bounds     = γ_{market_id ; MIN(executed_at) → t_first, MAX(executed_at) → t_last} (MT_period)
-
-FirstPx    = π_{market_id, price → first_price}
-               (MT_period ⋈_{MT_period.market_id = Bounds.market_id
-                              ∧ executed_at = t_first} Bounds)
-LastPx     = π_{market_id, price → last_price}
-               (MT_period ⋈_{MT_period.market_id = Bounds.market_id
-                              ∧ executed_at = t_last} Bounds)
-
-Stats      = γ_{market_id ; SUM(quantity) → total_volume, COUNT(*) → trade_count,
-                AVG(price) → avg_price} (MT_period)
-
-MarketStats = (Stats ⋈_{market_id} FirstPx) ⋈_{market_id} LastPx
-
-O_period      = σ_{status = 'executed' ∧ executed_at ≥ from ∧ executed_at < to} (Orders)
-Participation = γ_{market_id ; COUNT_DISTINCT(user_id) → participating_users} (O_period)
-
-Joined = ((MarketStats ⟕_{market_id} Participation)
-            ⋈_{market_id = id} Markets) ⋈_{crypto_id = id} Crypto
-
-Result = τ_{total_volume ↓} (
-           π_{symbol, quote_currency, total_volume, trade_count, avg_price,
-              (last_price − first_price) / first_price × 100 → market_return_pct,
-              COALESCE(participating_users, 0) → participating_users}
-             (Joined) )
-}}}
-
-`FirstPx`/`LastPx` express `FIRST_VALUE`/`LAST_VALUE` — which have no classical relational-
-algebra equivalent — as an aggregation for the boundary timestamp per market followed by a
-self-join back to `MarketTrades` to recover the price at that timestamp; this is the standard
-way to express "value at the extreme of a group" in extended relational algebra.
-
-== Demonstration data ==
-
-`reports_demo_data.sql`, the optional script both verified runs above were produced with:
-
-{{{
--- reports_demo_data.sql
--- EduBerza - optional historical data for the P6 reports
--- Course: Databases 2025/2026 Winter, FINKI UKIM
---
--- data_load.sql only seeds ~10 minutes of trade history, which is enough to
--- demonstrate UC0001-UC0007 but not enough to show report_top_traders() or
--- report_market_performance() doing anything interesting: everything falls
--- into a single quarter, so "number of profitable periods" and "consistency"
--- are trivial and "market return" has almost no history to work with.
---
--- This script adds five quarters of synthetic transactions, market trades and
--- executed orders on top of an already-loaded data_load.sql, spanning
--- 2025-07 to 2026-07, so the two P6 reports have several periods and two
--- markets with opposite price trends to actually compare.
---
--- Deliberately NOT part of -init / -load-data: it only inserts into
--- transactions, market_trades and orders, and does not touch
--- users.available_balance/invested_balance or holdings, so it does not
--- disturb the balances the other use cases' documented "verified run"
--- sections depend on. Run it by hand, after data_load.sql, only to exercise
--- the two reports:
---
---   psql "$DATABASE_URL" -f server/db/schema_creation.sql
---   psql "$DATABASE_URL" -f server/db/data_load.sql
---   psql "$DATABASE_URL" -f server/db/reports_demo_data.sql
---
--- Idempotent: deletes its own previously-inserted rows (tagged via
--- description/source) before re-inserting.
-
-SET search_path TO project, public;
-
-DELETE FROM transactions  WHERE description = 'P6 demo data';
-DELETE FROM orders        WHERE id IN (
-    'e1111111-1111-1111-1111-111111111111', 'e2222222-2222-2222-2222-222222222222',
-    'e3333333-3333-3333-3333-333333333333', 'e4444444-4444-4444-4444-444444444444',
-    'e5555555-5555-5555-5555-555555555555'
-);
-DELETE FROM market_trades WHERE source = 'p6_demo';
-
--- ============================================================================
--- Alice: five quarterly round trips, 3 profitable / 2 losing (60% consistency)
--- ============================================================================
-INSERT INTO transactions (user_id, type, amount, currency, created_at, description) VALUES
-    ('b1111111-1111-1111-1111-111111111111', 'buy',  -5000.0000, 'USD', '2025-07-15 10:00', 'P6 demo data'),
-    ('b1111111-1111-1111-1111-111111111111', 'sell',  5800.0000, 'USD', '2025-07-20 10:00', 'P6 demo data'),
-    ('b1111111-1111-1111-1111-111111111111', 'fee',      -5.0000, 'USD', '2025-07-20 10:00', 'P6 demo data'),
-
-    ('b1111111-1111-1111-1111-111111111111', 'buy',  -4000.0000, 'USD', '2025-10-15 10:00', 'P6 demo data'),
-    ('b1111111-1111-1111-1111-111111111111', 'sell',  3500.0000, 'USD', '2025-10-20 10:00', 'P6 demo data'),
-    ('b1111111-1111-1111-1111-111111111111', 'fee',      -5.0000, 'USD', '2025-10-20 10:00', 'P6 demo data'),
-
-    ('b1111111-1111-1111-1111-111111111111', 'buy',  -6000.0000, 'USD', '2026-01-15 10:00', 'P6 demo data'),
-    ('b1111111-1111-1111-1111-111111111111', 'sell',  6700.0000, 'USD', '2026-01-20 10:00', 'P6 demo data'),
-    ('b1111111-1111-1111-1111-111111111111', 'fee',      -5.0000, 'USD', '2026-01-20 10:00', 'P6 demo data'),
-
-    ('b1111111-1111-1111-1111-111111111111', 'buy',  -3000.0000, 'USD', '2026-04-15 10:00', 'P6 demo data'),
-    ('b1111111-1111-1111-1111-111111111111', 'sell',  2600.0000, 'USD', '2026-04-20 10:00', 'P6 demo data'),
-    ('b1111111-1111-1111-1111-111111111111', 'fee',      -5.0000, 'USD', '2026-04-20 10:00', 'P6 demo data'),
-
-    ('b1111111-1111-1111-1111-111111111111', 'buy',  -4500.0000, 'USD', '2026-07-15 10:00', 'P6 demo data'),
-    ('b1111111-1111-1111-1111-111111111111', 'sell',  5200.0000, 'USD', '2026-07-20 10:00', 'P6 demo data'),
-    ('b1111111-1111-1111-1111-111111111111', 'fee',      -5.0000, 'USD', '2026-07-20 10:00', 'P6 demo data');
-
--- ============================================================================
--- Bob: three quarterly round trips, all profitable (100% consistency),
--- smaller total P/L than Alice but a higher ROI.
--- ============================================================================
-INSERT INTO transactions (user_id, type, amount, currency, created_at, description) VALUES
-    ('b2222222-2222-2222-2222-222222222222', 'buy',  -2000.0000, 'USD', '2025-10-10 10:00', 'P6 demo data'),
-    ('b2222222-2222-2222-2222-222222222222', 'sell',  2300.0000, 'USD', '2025-10-12 10:00', 'P6 demo data'),
-    ('b2222222-2222-2222-2222-222222222222', 'fee',      -3.0000, 'USD', '2025-10-12 10:00', 'P6 demo data'),
-
-    ('b2222222-2222-2222-2222-222222222222', 'buy',  -2500.0000, 'USD', '2026-01-10 10:00', 'P6 demo data'),
-    ('b2222222-2222-2222-2222-222222222222', 'sell',  2900.0000, 'USD', '2026-01-12 10:00', 'P6 demo data'),
-    ('b2222222-2222-2222-2222-222222222222', 'fee',      -3.0000, 'USD', '2026-01-12 10:00', 'P6 demo data'),
-
-    ('b2222222-2222-2222-2222-222222222222', 'buy',  -1800.0000, 'USD', '2026-04-10 10:00', 'P6 demo data'),
-    ('b2222222-2222-2222-2222-222222222222', 'sell',  2100.0000, 'USD', '2026-04-12 10:00', 'P6 demo data'),
-    ('b2222222-2222-2222-2222-222222222222', 'fee',      -3.0000, 'USD', '2026-04-12 10:00', 'P6 demo data');
-
--- ============================================================================
--- Market trades: BTC/USD trending up, ETH/USD trending down, five quarters.
--- source='p6_demo' keeps these separate from data_load.sql's own rows and
--- from live user/bot fills so this script can clean up after itself.
--- ============================================================================
-INSERT INTO market_trades (market_id, executed_at, price, quantity, side, source) VALUES
-    ('a1111111-1111-1111-1111-111111111111', '2025-07-15 10:00', 40000.000000, 0.500000, 'buy',  'p6_demo'),
-    ('a1111111-1111-1111-1111-111111111111', '2025-10-15 10:00', 45000.000000, 0.800000, 'buy',  'p6_demo'),
-    ('a1111111-1111-1111-1111-111111111111', '2026-01-15 10:00', 55000.000000, 1.200000, 'buy',  'p6_demo'),
-    ('a1111111-1111-1111-1111-111111111111', '2026-04-15 10:00', 60000.000000, 1.000000, 'buy',  'p6_demo'),
-
-    ('a2222222-2222-2222-2222-222222222222', '2025-07-15 10:00',  4000.000000, 3.000000, 'sell', 'p6_demo'),
-    ('a2222222-2222-2222-2222-222222222222', '2025-10-15 10:00',  3800.000000, 2.500000, 'sell', 'p6_demo'),
-    ('a2222222-2222-2222-2222-222222222222', '2026-01-15 10:00',  3600.000000, 2.000000, 'sell', 'p6_demo'),
-    ('a2222222-2222-2222-2222-222222222222', '2026-04-15 10:00',  3550.000000, 1.800000, 'sell', 'p6_demo');
-
--- ============================================================================
--- Executed orders: who participated in which market, across the same quarters.
--- ============================================================================
-INSERT INTO orders (id, user_id, market_id, side, type, status, quantity, price, placed_at, executed_at) VALUES
-    ('e1111111-1111-1111-1111-111111111111', 'b1111111-1111-1111-1111-111111111111',
-     'a1111111-1111-1111-1111-111111111111', 'buy', 'market', 'executed', 0.5000, 40000.000000,
-     '2025-07-15 10:00', '2025-07-15 10:00'),
-    ('e2222222-2222-2222-2222-222222222222', 'b1111111-1111-1111-1111-111111111111',
-     'a2222222-2222-2222-2222-222222222222', 'sell', 'market', 'executed', 3.0000, 4000.000000,
-     '2025-10-15 10:00', '2025-10-15 10:00'),
-    ('e3333333-3333-3333-3333-333333333333', 'b2222222-2222-2222-2222-222222222222',
-     'a1111111-1111-1111-1111-111111111111', 'buy', 'market', 'executed', 1.2000, 55000.000000,
-     '2026-01-15 10:00', '2026-01-15 10:00'),
-    ('e4444444-4444-4444-4444-444444444444', 'b2222222-2222-2222-2222-222222222222',
-     'a1111111-1111-1111-1111-111111111111', 'buy', 'market', 'executed', 1.0000, 60000.000000,
-     '2026-04-15 10:00', '2026-04-15 10:00'),
-    ('e5555555-5555-5555-5555-555555555555', 'b3333333-3333-3333-3333-333333333333',
-     'a2222222-2222-2222-2222-222222222222', 'sell', 'market', 'executed', 2.0000, 3600.000000,
-     '2026-01-15 10:00', '2026-01-15 10:00');
-}}}
-
-== AI usage ==
-
-AI was used in this phase and is logged in full, per the course rule for P1 onward.
-
- * '''Phase log:''' [wiki:AdvancedReportsAIUsage] — service used, what
-   the AI produced, and what I decided myself.
-
-'''Service:''' Claude Code (Anthropic), `https://claude.com/claude-code` — Claude subscription,
-model Claude Sonnet 5.
-
-'''In short:''' I specified both report questions in full — including the exact formulas for
-P/L, ROI, consistency, market return, volatility and user participation — and asked the AI to
-turn them into working SQL, wire them into the prototype as real reports, build the
-relational-algebra equivalents, and produce demonstration data rich enough to show the
-reports doing something non-trivial. In a follow-up, I asked for the price-volatility column
-to be dropped from the market performance report — see the "Follow-up — 2026-09-17" section of
-[wiki:AdvancedReportsAIUsage] for that change.
Index: cs/P6-AdvancedReports/wiki/AdvancedReportsAIUsage.md
===================================================================
--- docs/P6-AdvancedReports/wiki/AdvancedReportsAIUsage.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,381 +1,0 @@
-= Advanced Reports AI Usage =
-
-== Name of AI service/solution that was used ==
-
-'''Claude Code''' (Anthropic)
-
- * '''URL:''' `https://claude.com/claude-code`
- * '''Type of service/subscription:''' Claude subscription, model Claude Sonnet 5.
-
-== Final result ==
-
-=== Diagram ===
-
-None. Both reports read `transactions`, `market_trades`, `orders`, `markets`, `crypto` and
-`users` exactly as they already existed after
-Normalization — no attribute or relation was missing,
-so [wiki:ERModel] and
-[wiki:RelationalDesign] needed no changes and there is
-no new diagram for this phase. This is stated explicitly rather than left implicit because the
-phase rubric specifically calls out modifying the design as the fallback when a good report
-idea can't be answered by the data on hand — it wasn't needed here.
-
-=== Results in details / description ===
-
-The AI:
-
- * Turned my two fully-specified report questions (the exact P/L, ROI, consistency, volume,
-   return, volatility and participation formulas were mine) into two single-statement SQL
-   queries, each wrapped as a `LANGUAGE sql STABLE` function
-   (`project.report_top_traders`, `project.report_market_performance`) in
-   `schema_creation.sql`, so the phase's "just one SQL
-   query" requirement is met by the query text itself, while still giving the prototype a
-   clean, parameterised, named thing to call.
-
-The two functions, from `schema_creation.sql`:
-
-{{{
--- 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,
-    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,
-            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,
-        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;
-$$;
-}}}
-
- * Wired both into the running CLI as real menu options — `server/reports.go`, options
-   `[10]`/`[11]` in `server/cli.go` — rather than leaving them as documentation-only SQL, per
-   the phase's own framing ("used as reports within your application").
- * Wrote the relational-algebra equivalent of each query, including how to express
-   `FIRST_VALUE`/`LAST_VALUE` (which have no classical RA equivalent) as an aggregation for the
-   boundary timestamp followed by a self-join, and how to express `FILTER (WHERE …)`-style
-   conditional counts as separate groupings recombined with left outer joins.
- * Noticed that the existing `data_load.sql` seed data (a few minutes of trade history) cannot
-   demonstrate either report meaningfully — everything falls into one quarter, so "consistency"
-   and "market return over time" have nothing to show — and wrote
-   `reports_demo_data.sql`, an optional, separate,
-   idempotent script adding five quarters of synthetic transactions, market trades and executed
-   orders, deliberately excluded from `-init`/`-load-data` so it cannot disturb the balances the
-   other use cases' documented "verified run" sections depend on.
-
-`reports_demo_data.sql`:
-
-{{{
--- reports_demo_data.sql
--- EduBerza - optional historical data for the P6 reports
--- Course: Databases 2025/2026 Winter, FINKI UKIM
---
--- data_load.sql only seeds ~10 minutes of trade history, which is enough to
--- demonstrate UC0001-UC0007 but not enough to show report_top_traders() or
--- report_market_performance() doing anything interesting: everything falls
--- into a single quarter, so "number of profitable periods" and "consistency"
--- are trivial and "market return" has almost no history to work with.
---
--- This script adds five quarters of synthetic transactions, market trades and
--- executed orders on top of an already-loaded data_load.sql, spanning
--- 2025-07 to 2026-07, so the two P6 reports have several periods and two
--- markets with opposite price trends to actually compare.
---
--- Deliberately NOT part of -init / -load-data: it only inserts into
--- transactions, market_trades and orders, and does not touch
--- users.available_balance/invested_balance or holdings, so it does not
--- disturb the balances the other use cases' documented "verified run"
--- sections depend on. Run it by hand, after data_load.sql, only to exercise
--- the two reports:
---
---   psql "$DATABASE_URL" -f server/db/schema_creation.sql
---   psql "$DATABASE_URL" -f server/db/data_load.sql
---   psql "$DATABASE_URL" -f server/db/reports_demo_data.sql
---
--- Idempotent: deletes its own previously-inserted rows (tagged via
--- description/source) before re-inserting.
-
-SET search_path TO project, public;
-
-DELETE FROM transactions  WHERE description = 'P6 demo data';
-DELETE FROM orders        WHERE id IN (
-    'e1111111-1111-1111-1111-111111111111', 'e2222222-2222-2222-2222-222222222222',
-    'e3333333-3333-3333-3333-333333333333', 'e4444444-4444-4444-4444-444444444444',
-    'e5555555-5555-5555-5555-555555555555'
-);
-DELETE FROM market_trades WHERE source = 'p6_demo';
-
--- ============================================================================
--- Alice: five quarterly round trips, 3 profitable / 2 losing (60% consistency)
--- ============================================================================
-INSERT INTO transactions (user_id, type, amount, currency, created_at, description) VALUES
-    ('b1111111-1111-1111-1111-111111111111', 'buy',  -5000.0000, 'USD', '2025-07-15 10:00', 'P6 demo data'),
-    ('b1111111-1111-1111-1111-111111111111', 'sell',  5800.0000, 'USD', '2025-07-20 10:00', 'P6 demo data'),
-    ('b1111111-1111-1111-1111-111111111111', 'fee',      -5.0000, 'USD', '2025-07-20 10:00', 'P6 demo data'),
-
-    ('b1111111-1111-1111-1111-111111111111', 'buy',  -4000.0000, 'USD', '2025-10-15 10:00', 'P6 demo data'),
-    ('b1111111-1111-1111-1111-111111111111', 'sell',  3500.0000, 'USD', '2025-10-20 10:00', 'P6 demo data'),
-    ('b1111111-1111-1111-1111-111111111111', 'fee',      -5.0000, 'USD', '2025-10-20 10:00', 'P6 demo data'),
-
-    ('b1111111-1111-1111-1111-111111111111', 'buy',  -6000.0000, 'USD', '2026-01-15 10:00', 'P6 demo data'),
-    ('b1111111-1111-1111-1111-111111111111', 'sell',  6700.0000, 'USD', '2026-01-20 10:00', 'P6 demo data'),
-    ('b1111111-1111-1111-1111-111111111111', 'fee',      -5.0000, 'USD', '2026-01-20 10:00', 'P6 demo data'),
-
-    ('b1111111-1111-1111-1111-111111111111', 'buy',  -3000.0000, 'USD', '2026-04-15 10:00', 'P6 demo data'),
-    ('b1111111-1111-1111-1111-111111111111', 'sell',  2600.0000, 'USD', '2026-04-20 10:00', 'P6 demo data'),
-    ('b1111111-1111-1111-1111-111111111111', 'fee',      -5.0000, 'USD', '2026-04-20 10:00', 'P6 demo data'),
-
-    ('b1111111-1111-1111-1111-111111111111', 'buy',  -4500.0000, 'USD', '2026-07-15 10:00', 'P6 demo data'),
-    ('b1111111-1111-1111-1111-111111111111', 'sell',  5200.0000, 'USD', '2026-07-20 10:00', 'P6 demo data'),
-    ('b1111111-1111-1111-1111-111111111111', 'fee',      -5.0000, 'USD', '2026-07-20 10:00', 'P6 demo data');
-
--- ============================================================================
--- Bob: three quarterly round trips, all profitable (100% consistency),
--- smaller total P/L than Alice but a higher ROI.
--- ============================================================================
-INSERT INTO transactions (user_id, type, amount, currency, created_at, description) VALUES
-    ('b2222222-2222-2222-2222-222222222222', 'buy',  -2000.0000, 'USD', '2025-10-10 10:00', 'P6 demo data'),
-    ('b2222222-2222-2222-2222-222222222222', 'sell',  2300.0000, 'USD', '2025-10-12 10:00', 'P6 demo data'),
-    ('b2222222-2222-2222-2222-222222222222', 'fee',      -3.0000, 'USD', '2025-10-12 10:00', 'P6 demo data'),
-
-    ('b2222222-2222-2222-2222-222222222222', 'buy',  -2500.0000, 'USD', '2026-01-10 10:00', 'P6 demo data'),
-    ('b2222222-2222-2222-2222-222222222222', 'sell',  2900.0000, 'USD', '2026-01-12 10:00', 'P6 demo data'),
-    ('b2222222-2222-2222-2222-222222222222', 'fee',      -3.0000, 'USD', '2026-01-12 10:00', 'P6 demo data'),
-
-    ('b2222222-2222-2222-2222-222222222222', 'buy',  -1800.0000, 'USD', '2026-04-10 10:00', 'P6 demo data'),
-    ('b2222222-2222-2222-2222-222222222222', 'sell',  2100.0000, 'USD', '2026-04-12 10:00', 'P6 demo data'),
-    ('b2222222-2222-2222-2222-222222222222', 'fee',      -3.0000, 'USD', '2026-04-12 10:00', 'P6 demo data');
-
--- ============================================================================
--- Market trades: BTC/USD trending up, ETH/USD trending down, five quarters.
--- source='p6_demo' keeps these separate from data_load.sql's own rows and
--- from live user/bot fills so this script can clean up after itself.
--- ============================================================================
-INSERT INTO market_trades (market_id, executed_at, price, quantity, side, source) VALUES
-    ('a1111111-1111-1111-1111-111111111111', '2025-07-15 10:00', 40000.000000, 0.500000, 'buy',  'p6_demo'),
-    ('a1111111-1111-1111-1111-111111111111', '2025-10-15 10:00', 45000.000000, 0.800000, 'buy',  'p6_demo'),
-    ('a1111111-1111-1111-1111-111111111111', '2026-01-15 10:00', 55000.000000, 1.200000, 'buy',  'p6_demo'),
-    ('a1111111-1111-1111-1111-111111111111', '2026-04-15 10:00', 60000.000000, 1.000000, 'buy',  'p6_demo'),
-
-    ('a2222222-2222-2222-2222-222222222222', '2025-07-15 10:00',  4000.000000, 3.000000, 'sell', 'p6_demo'),
-    ('a2222222-2222-2222-2222-222222222222', '2025-10-15 10:00',  3800.000000, 2.500000, 'sell', 'p6_demo'),
-    ('a2222222-2222-2222-2222-222222222222', '2026-01-15 10:00',  3600.000000, 2.000000, 'sell', 'p6_demo'),
-    ('a2222222-2222-2222-2222-222222222222', '2026-04-15 10:00',  3550.000000, 1.800000, 'sell', 'p6_demo');
-
--- ============================================================================
--- Executed orders: who participated in which market, across the same quarters.
--- ============================================================================
-INSERT INTO orders (id, user_id, market_id, side, type, status, quantity, price, placed_at, executed_at) VALUES
-    ('e1111111-1111-1111-1111-111111111111', 'b1111111-1111-1111-1111-111111111111',
-     'a1111111-1111-1111-1111-111111111111', 'buy', 'market', 'executed', 0.5000, 40000.000000,
-     '2025-07-15 10:00', '2025-07-15 10:00'),
-    ('e2222222-2222-2222-2222-222222222222', 'b1111111-1111-1111-1111-111111111111',
-     'a2222222-2222-2222-2222-222222222222', 'sell', 'market', 'executed', 3.0000, 4000.000000,
-     '2025-10-15 10:00', '2025-10-15 10:00'),
-    ('e3333333-3333-3333-3333-333333333333', 'b2222222-2222-2222-2222-222222222222',
-     'a1111111-1111-1111-1111-111111111111', 'buy', 'market', 'executed', 1.2000, 55000.000000,
-     '2026-01-15 10:00', '2026-01-15 10:00'),
-    ('e4444444-4444-4444-4444-444444444444', 'b2222222-2222-2222-2222-222222222222',
-     'a1111111-1111-1111-1111-111111111111', 'buy', 'market', 'executed', 1.0000, 60000.000000,
-     '2026-04-15 10:00', '2026-04-15 10:00'),
-    ('e5555555-5555-5555-5555-555555555555', 'b3333333-3333-3333-3333-333333333333',
-     'a2222222-2222-2222-2222-222222222222', 'sell', 'market', 'executed', 2.0000, 3600.000000,
-     '2026-01-15 10:00', '2026-01-15 10:00');
-}}}
-
- * Ran both reports against a live PostgreSQL 16 database with that demo data loaded, through
-   the actual CLI, and used the real output (including a run where alice's seeded quarter
-   interacted with a pre-existing `data_load.sql` transaction and flipped a profitable quarter
-   into a loss) as the verified evidence in [wiki:AdvancedReports], rather
-   than inventing example numbers.
-
-== Summary of AI involvement ==
-
-||  ||= This session — 2026-09-16 =||
-||= What I brought =|| The phase rubric, plus both report questions fully specified down to the exact aggregate formulas ||
-||= What the AI did =|| Wrote the SQL, wrote the relational algebra, wired the reports into the CLI, designed and ran the demonstration data, verified everything against a live database ||
-||= What I decided =|| To keep both reports as SQL functions rather than plain ad-hoc queries so they are actually usable from the application; to accept the AI's synthetic multi-quarter demo dataset rather than wait for enough real usage history to accumulate ||
-
-The two ideas and their formulas were mine, specified in enough detail (P/L as the sum of
-buy+sell+fee transactions, ROI relative to total buys, consistency as a share of profitable
-quarters, market return as first-vs-last trade price, volatility as price standard deviation,
-participation from orders rather than trades) that there was no separate "AI alternative
-idea" to borrow from and document a change against, unlike the more open-ended P1–P3 phases —
-the AI's job here was implementation and verification of a fully-specified design, which is
-what is logged above and in the prompt below.
-
-== Entire AI usage log ==
-
-=== 2026-09-16 ===
-
-'''Intent:''' hand over the P6 rubric together with both report ideas, fully specified, and
-have the whole phase — SQL, relational algebra, prototype integration, and demonstration data
-— produced and verified in one pass.
-
-'''Prompt (student, verbatim):'''
-> Phase P6: Complex DB Reports (SQL, Stored Procedures, Relational Algebra)
-> [the full phase rubric was pasted: 2 complex analytical reports solvable each with one SQL
-> query, usable as reports within the application, with a note that helper views/functions/
-> procedures are acceptable when pure SQL isn't enough, that the design should be extended if
-> a good idea needs data that doesn't exist yet, a requirement for the corresponding relational
-> algebra, the exact `AdvancedReports`/`AdvancedReportsAIUsage` wiki templates, and the phase's
-> AI-use rules]
->
-> I will describe for you the 2 more complex sql queries:
-> 1. Find the users who performed the best in a selected quarter, year, or multi-year period
->    based on their realized trading performance, and show their total profit/loss, total
->    amount invested, ROI, number of profitable and losing periods, and consistency of
->    returns. Calculate realized P/L as SUM(sell transactions + buy transactions + fee
->    transactions), calculate ROI as realized P/L / absolute value of total buy transactions ×
->    100, and calculate consistency as profitable periods / total periods × 100.
-> 2. Find which cryptocurrency markets performed best during a selected quarter, year, or
->    multi-year period based on trading activity and price behavior, and report the markets
->    with the highest trading volume, number of trades, number of participating users, and
->    price return. Calculate total volume as SUM(trade quantity), average trading price as
->    AVG(price), market return as (last trade price − first trade price) / first trade price ×
->    100, price volatility as standard deviation of trade prices, and user participation as
->    COUNT(DISTINCT user_id) from the related executed orders.
-
-'''Response (AI, summarised):'''
- * Confirmed both report questions were answerable from the existing schema with no new
-   columns or tables, and said so explicitly rather than silently skipping that check.
- * Wrote `report_top_traders` and `report_market_performance` as SQL functions, added to
-   `schema_creation.sql` under a new `REPORTS (P6)` section.
- * Wrote `server/reports.go` (`ShowTopTraders`, `ShowMarketPerformance`, a shared
-   `promptPeriod` helper) and added menu options `[10]`/`[11]` to `server/cli.go`.
- * Wrote `server/db/reports_demo_data.sql` for a richer, multi-quarter demonstration, and
-   documented in its own header exactly why it is optional and what it deliberately does not
-   touch (user balances, holdings).
- * Built the prototype, ran both reports against the live database with the demo data loaded,
-   cross-checked the SQL output against hand-computed expected values for both seeded users,
-   and used that real output as the verified evidence.
- * Wrote out the relational-algebra derivation for each query and [wiki:AdvancedReports].
-
-'''What I decided:''' to keep the demo dataset out of `-init`/`-load-data` rather than merge it
-into `data_load.sql`, since the other phases' documented expected values (specific balances in
-[wiki:BuildInstructions]) depend on the seed data staying
-exactly as it is.
-
-> '''Student action required.''' Read [wiki:AdvancedReports] end to end
-> before the defense, and be ready to compute one period's realized P/L or one market's return
-> by hand from the raw `transactions`/`market_trades` rows — the numbers in the verified run
-> are real output, not invented, so they can be checked against
-> `reports_demo_data.sql` (shown above) directly. Append any further
-> prompts here if you ask for revisions.
-
-=== Follow-up — 2026-09-17 ===
-
-'''Intent:''' simplify the market performance report after seeing it run — with only a handful
-of trades per market in most periods, price volatility read as noise rather than a useful
-signal.
-
-'''Prompt (student, verbatim):'''
-> Can you tell me the exact database_url so I can execute the schema creating and the data_load?
->
-> From the Market Performance can we remove Volatility, I don't think that is that useful, also
-> can you tell me what is users column?
-
-'''Response (AI, summarised):'''
- * Gave the local dev connection string (`postgresql://bp_project:1234@localhost:5433/bp_database`,
-   from `.env`) and explained `participating_users` is `COUNT(DISTINCT user_id)` from that
-   market's executed orders, not from `market_trades` (which has no `user_id`).
- * Removed `price_volatility`/`STDDEV(price)` from `report_market_performance()` in
-   `schema_creation.sql`, from its `RETURNS TABLE` and its `SELECT`, and from the corresponding
-   column in `server/reports.go`'s `ShowMarketPerformance`.
- * Updated the relational algebra (`Stats`, `Result`) and the SQL listing, verified run output,
-   and prose in [wiki:AdvancedReports] to match, and added a short note
-   explaining why the column was dropped.
- * Rebuilt, re-ran `-init` and `reports_demo_data.sql` against the live database, and
-   re-verified the market performance report through the actual CLI before and after the
-   change to confirm only the one column disappeared and every other number is unchanged.
-
-'''What I decided:''' to drop the column entirely rather than keep it computed-but-hidden,
-since an unused computation left in the query is exactly the kind of thing that should not
-survive a review.
Index: cs/P7-AdvancedDatabaseDevelopment/AdvancedDatabaseDevelopment.md
===================================================================
--- docs/P7-AdvancedDatabaseDevelopment/AdvancedDatabaseDevelopment.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,1194 +1,0 @@
-# Advanced Database Development
-
-EduBerza's users trade virtual money and virtual crypto with each other and with a simulated
-market. Up to P6 the prototype only knew market orders that filled immediately against the
-latest price, so an order was either untouched or completely done. This phase adds what makes an
-exchange consistent once orders can **wait in an order book, fill in parts, and trade with each
-other**, and puts every rule that keeps orders, trades and balances in agreement into the
-database itself — so it holds no matter who writes the data (the CLI, the market bot, a script,
-or someone typing SQL in DBeaver).
-
-Only rules that span several rows or several tables are listed. `NOT NULL`, `UNIQUE`, `CHECK`,
-primary and foreign keys are P2 ([RelationalDesign](../P2-RelationalDesign/RelationalDesign.md))
-and are not presented as P7 features.
-
-All of it is in [`server/db/advanced_db.sql`](../../server/db/advanced_db.sql), run by `-init`
-between `schema_creation.sql` and `data_load.sql`. Every rule is exercised by
-[`server/db/advanced_db_tests.sql`](../../server/db/advanced_db_tests.sql) (see
-[Tests](#tests-proving-the-rules)).
-
-## Overview
-
-| # | Requirement | Triggers | Procedures / functions | Views | Tables affected |
-|---|---|---|---|---|---|
-| 1 | [Order lifecycle and filled/remaining consistency](#order-lifecycle-and-filledremaining-consistency) | `orders_lifecycle` | | | `orders` |
-| 2 | [Trade consistency](#trade-consistency) | `market_trades_validate`, `market_trades_fill`, `market_trades_immutable` | `execute_trade` | | `market_trades`, `orders`, `users`, `holdings`, `transactions` |
-| 3 | [Balance and reservation consistency](#balance-and-reservation-consistency) | `reserved_cash_matches_orders`, `reserved_crypto_matches_orders`, `cash_matches_ledger` (deferred) | `order_reservation` | | `users`, `holdings`, `orders`, `transactions` |
-| 4 | [Placing and cancelling orders](#placing-and-cancelling-orders) | | `place_order`, `match_order`, `cancel_order` | | all of the above |
-| 5 | [Automatic recording of order events](#automatic-recording-of-order-events) | `orders_events` | | | `order_events` (new) |
-| 6 | [Views for derived trading data](#views-for-derived-trading-data) | | | `v_order_book`, `v_active_orders`, `v_order_history`, `v_trader_balances` | read-only |
-| 7 | [Background job: filling resting limit orders](#background-job-filling-resting-limit-orders) | | `fill_marketable_orders` | | `orders`, `market_trades`, … |
-
-### Schema additions
-
-The existing design had no place for three things the requirements need, so four columns were
-added — nothing else in the design changed:
-
-| Column | Why it is needed |
-|---|---|
-| `orders.filled_quantity` (+ status value `partially_filled`) | "filled / remaining quantity" cannot be kept consistent without storing how much was filled; remaining = `quantity − filled_quantity` |
-| `users.reserved_balance` | cash committed to open buy orders must be set aside somewhere; `holdings.reserved_quantity` already did this for crypto on sell orders |
-| `market_trades.buy_order_id`, `market_trades.sell_order_id` | "a trade can only happen between compatible orders" needs the trade to say which orders it filled. `NULL` on a side means the simulated market was the counterparty; the bot's price ticks have both `NULL` |
-
-One new table, `order_events`, holds the automatically recorded events (requirement 5).
-
-## Order lifecycle and filled/remaining consistency
-
-### Data requirements description
-
-**Business rule.**
-
-- An order's status follows from how much of it has been filled: nothing filled → `open`,
-  partly filled → `partially_filled`, completely filled → `executed`. The only status that is
-  set explicitly is `cancelled`, and only on an order that is still active.
-- `executed` and `cancelled` are final — such an order can never be processed again (no further
-  fills, no cancellation, no changes).
-- `filled_quantity` only grows, never exceeds `quantity`, and only changes when a trade fills
-  the order.
-- What was ordered — user, market, side, type, quantity, price, time of placement — never
-  changes after placement.
-- `executed_at` is set exactly when the order becomes `executed`.
-- New orders are only accepted on active markets, need a positive price, and start unfilled
-  (the one exception is importing an order that was completely executed in the past, used by the
-  sample data).
-
-**Why it is non-trivial.** The rule compares the *old* and the *new* version of a row (valid
-transitions, "final" states, "only grows", immutable columns) and ties two columns together
-(status ↔ filled quantity). A `CHECK` constraint only sees one version of one row. It also
-depends on *who* changes `filled_quantity` — a trade may, a manual `UPDATE` may not.
-
-**PostgreSQL feature.** `BEFORE INSERT OR UPDATE` row trigger on `orders`. The trade trigger
-(requirement 2) sets a transaction-local setting (`set_config('eduberza.trade_fill', 'on', true)`)
-while it fills an order; the lifecycle trigger only accepts a change of `filled_quantity` when
-that setting is on.
-
-**Tables affected.** `orders` (reads `markets` for the active check).
-
-### Implementation
-
-#### Triggers
-
-```sql
-CREATE OR REPLACE FUNCTION project.trg_orders_lifecycle()
-RETURNS trigger LANGUAGE plpgsql AS $$
-DECLARE
-    v_derived varchar(20);
-BEGIN
-    IF TG_OP = 'UPDATE' THEN
-        IF OLD.status IN ('executed', 'cancelled') THEN
-            RAISE EXCEPTION 'order % is already % and cannot be processed again', OLD.id, OLD.status
-                USING ERRCODE = 'check_violation';
-        END IF;
-        IF (NEW.user_id, NEW.market_id, NEW.side, NEW.type, NEW.quantity, NEW.price, NEW.placed_at)
-           IS DISTINCT FROM
-           (OLD.user_id, OLD.market_id, OLD.side, OLD.type, OLD.quantity, OLD.price, OLD.placed_at) THEN
-            RAISE EXCEPTION 'user, market, side, type, quantity, price and placed_at of an order cannot change'
-                USING ERRCODE = 'check_violation';
-        END IF;
-        IF NEW.filled_quantity <> OLD.filled_quantity THEN
-            IF current_setting('eduberza.trade_fill', true) IS DISTINCT FROM 'on' THEN
-                RAISE EXCEPTION 'filled quantity of order % can only change through a trade', OLD.id
-                    USING ERRCODE = 'check_violation';
-            END IF;
-            IF NEW.filled_quantity < OLD.filled_quantity THEN
-                RAISE EXCEPTION 'filled quantity of order % cannot decrease', OLD.id
-                    USING ERRCODE = 'check_violation';
-            END IF;
-        END IF;
-        IF NEW.status = 'cancelled' AND OLD.status <> 'cancelled' THEN
-            IF NEW.filled_quantity <> OLD.filled_quantity THEN
-                RAISE EXCEPTION 'an order cannot be filled and cancelled in the same step'
-                    USING ERRCODE = 'check_violation';
-            END IF;
-            NEW.executed_at := NULL;
-            RETURN NEW;
-        END IF;
-    ELSE
-        IF NOT EXISTS (SELECT 1 FROM project.markets WHERE id = NEW.market_id AND is_active) THEN
-            RAISE EXCEPTION 'market % is not active; no new orders accepted', NEW.market_id
-                USING ERRCODE = 'check_violation';
-        END IF;
-        IF NEW.price IS NULL OR NEW.price <= 0 THEN
-            RAISE EXCEPTION 'an order needs a positive price (limit price, or the market price for a market order)'
-                USING ERRCODE = 'check_violation';
-        END IF;
-        IF NEW.status = 'cancelled' THEN
-            RAISE EXCEPTION 'an order cannot be created already cancelled'
-                USING ERRCODE = 'check_violation';
-        END IF;
-        -- A new order starts unfilled. The only exception is importing an
-        -- order that was completely executed in the past (sample data).
-        IF NEW.filled_quantity NOT IN (0, NEW.quantity) THEN
-            RAISE EXCEPTION 'a new order is either unfilled or (imported history) completely filled'
-                USING ERRCODE = 'check_violation';
-        END IF;
-    END IF;
-
-    v_derived := CASE
-        WHEN NEW.filled_quantity = 0               THEN 'open'
-        WHEN NEW.filled_quantity < NEW.quantity    THEN 'partially_filled'
-        ELSE 'executed'
-    END;
-    IF NEW.status IS DISTINCT FROM v_derived
-       AND (TG_OP = 'INSERT' OR NEW.status IS DISTINCT FROM OLD.status) THEN
-        RAISE EXCEPTION 'order status % does not match filled quantity % of %; status is derived automatically',
-            NEW.status, NEW.filled_quantity, NEW.quantity
-            USING ERRCODE = 'check_violation';
-    END IF;
-    NEW.status := v_derived;
-
-    IF v_derived = 'executed' THEN
-        NEW.executed_at := COALESCE(NEW.executed_at, now());
-    ELSE
-        NEW.executed_at := NULL;
-    END IF;
-    RETURN NEW;
-END $$;
-
-CREATE TRIGGER orders_lifecycle
-    BEFORE INSERT OR UPDATE ON project.orders
-    FOR EACH ROW EXECUTE FUNCTION project.trg_orders_lifecycle();
-```
-
-Because the status is derived, the application never sets `open`, `partially_filled` or
-`executed` itself — it records trades, and the status follows.
-
-## Trade consistency
-
-### Data requirements description
-
-**Business rule.** A trade that fills orders must be possible for those orders:
-
-- the buy side is a buy order and the sell side is a sell order;
-- both are on the trade's market and still active — a cancelled or executed order can never
-  trade again;
-- the trade quantity does not exceed the remaining quantity of either order;
-- the price respects both limits: at most the buy order's price, at least the sell order's price;
-- the two orders belong to different users (no self-trade).
-
-Recording the trade must fill both orders by exactly the traded quantity (and so move their
-status), and move the money and crypto for both users, all together. A trade that filled orders
-is history and can't be changed or deleted afterwards — the fills and the money moved would no
-longer match it.
-
-**Why it is non-trivial.** One trade row has to be checked against two other rows of another
-table (the orders), including their current remaining quantity, and a single insert must cause
-consistent changes in five tables (`market_trades`, both `orders`, both users' `users` and
-`holdings` rows, two `transactions` rows).
-
-**PostgreSQL features.**
-
-- `BEFORE INSERT` trigger `market_trades_validate` — the compatibility rules. It locks both
-  orders (`FOR UPDATE`), so two concurrent trades can't both take the same remaining quantity.
-- `AFTER INSERT` trigger `market_trades_fill` — raises `filled_quantity` on both orders
-  (automatic status change through requirement 1).
-- `BEFORE UPDATE OR DELETE` trigger `market_trades_immutable`.
-- Stored function `execute_trade` — the settlement of money and crypto for one trade.
-
-Because the rules sit on `market_trades` itself, even a trade inserted by hand is validated and
-fills the orders.
-
-**Tables affected.** `market_trades`, `orders`, `users`, `holdings`, `transactions`.
-
-### Implementation
-
-#### Triggers
-
-```sql
-CREATE OR REPLACE FUNCTION project.trg_market_trades_validate()
-RETURNS trigger LANGUAGE plpgsql AS $$
-DECLARE
-    o     project.orders%ROWTYPE;
-    v_uid uuid;
-    v_id  uuid;
-    v_role varchar(4);
-BEGIN
-    IF NEW.buy_order_id IS NULL AND NEW.sell_order_id IS NULL THEN
-        RETURN NEW;
-    END IF;
-    IF NEW.buy_order_id IS NOT DISTINCT FROM NEW.sell_order_id THEN
-        RAISE EXCEPTION 'an order cannot trade with itself'
-            USING ERRCODE = 'check_violation';
-    END IF;
-
-    FOREACH v_role IN ARRAY ARRAY['buy', 'sell'] LOOP
-        v_id := CASE v_role WHEN 'buy' THEN NEW.buy_order_id ELSE NEW.sell_order_id END;
-        CONTINUE WHEN v_id IS NULL;
-
-        SELECT * INTO o FROM project.orders WHERE id = v_id FOR UPDATE;
-        IF o.side <> v_role THEN
-            RAISE EXCEPTION 'order % is a % order and cannot be the % side of a trade', v_id, o.side, v_role
-                USING ERRCODE = 'check_violation';
-        END IF;
-        IF o.market_id <> NEW.market_id THEN
-            RAISE EXCEPTION 'order % is on a different market than the trade', v_id
-                USING ERRCODE = 'check_violation';
-        END IF;
-        IF o.status NOT IN ('open', 'partially_filled') THEN
-            RAISE EXCEPTION 'order % is % and cannot trade', v_id, o.status
-                USING ERRCODE = 'check_violation';
-        END IF;
-        IF NEW.quantity > o.quantity - o.filled_quantity THEN
-            RAISE EXCEPTION 'trade quantity % exceeds the remaining quantity % of order %',
-                NEW.quantity, o.quantity - o.filled_quantity, v_id
-                USING ERRCODE = 'check_violation';
-        END IF;
-        IF v_uid IS NOT NULL AND v_uid = o.user_id THEN
-            RAISE EXCEPTION 'a user cannot trade with their own order'
-                USING ERRCODE = 'check_violation';
-        END IF;
-        IF v_role = 'buy' AND NEW.price > o.price OR v_role = 'sell' AND NEW.price < o.price THEN
-            RAISE EXCEPTION 'trade price % is outside the limit % of % order %', NEW.price, o.price, v_role, v_id
-                USING ERRCODE = 'check_violation';
-        END IF;
-        v_uid := o.user_id;
-    END LOOP;
-    RETURN NEW;
-END $$;
-
-CREATE TRIGGER market_trades_validate
-    BEFORE INSERT ON project.market_trades
-    FOR EACH ROW EXECUTE FUNCTION project.trg_market_trades_validate();
-```
-
-```sql
-CREATE OR REPLACE FUNCTION project.trg_market_trades_fill()
-RETURNS trigger LANGUAGE plpgsql AS $$
-BEGIN
-    IF NEW.buy_order_id IS NULL AND NEW.sell_order_id IS NULL THEN
-        RETURN NULL;
-    END IF;
-    PERFORM set_config('eduberza.trade_fill', 'on', true);
-    PERFORM set_config('eduberza.trade_price', NEW.price::text, true);
-    UPDATE project.orders
-       SET filled_quantity = filled_quantity + NEW.quantity,
-           executed_at     = CASE WHEN filled_quantity + NEW.quantity = quantity
-                                  THEN NEW.executed_at END
-     WHERE id IN (NEW.buy_order_id, NEW.sell_order_id);
-    PERFORM set_config('eduberza.trade_fill', 'off', true);
-    RETURN NULL;
-END $$;
-
-CREATE TRIGGER market_trades_fill
-    AFTER INSERT ON project.market_trades
-    FOR EACH ROW EXECUTE FUNCTION project.trg_market_trades_fill();
-```
-
-```sql
-CREATE OR REPLACE FUNCTION project.trg_market_trades_immutable()
-RETURNS trigger LANGUAGE plpgsql AS $$
-BEGIN
-    IF OLD.buy_order_id IS NULL AND OLD.sell_order_id IS NULL THEN
-        -- a simulated tick filled no order; allow it unless it is being
-        -- turned into one that did
-        IF TG_OP = 'DELETE' THEN
-            RETURN OLD;
-        ELSIF NEW.buy_order_id IS NULL AND NEW.sell_order_id IS NULL THEN
-            RETURN NEW;
-        END IF;
-    END IF;
-    RAISE EXCEPTION 'a trade that filled orders cannot be changed or deleted'
-        USING ERRCODE = 'check_violation';
-END $$;
-
-CREATE TRIGGER market_trades_immutable
-    BEFORE UPDATE OR DELETE ON project.market_trades
-    FOR EACH ROW EXECUTE FUNCTION project.trg_market_trades_immutable();
-```
-
-#### Stored procedures/functions
-
-`execute_trade(buy_order, sell_order, quantity, price)` — one trade. Either order may be `NULL`
-when the simulated market is the counterparty. The `INSERT` into `market_trades` validates the
-trade and fills the orders (triggers above); the function then settles both users:
-
-- **Buyer:** the reservation for the filled part is released. The buyer pays the actual cost
-  (trade price × quantity); if the trade price is below the order's limit, the difference goes
-  back to `available_balance`. The crypto is added to the holding at a running weighted average
-  price, and a `buy` ledger row is written.
-- **Seller:** the reserved crypto is delivered out of the holding, the proceeds are credited, the
-  cost basis is removed from `invested_balance`, and a `sell` ledger row is written.
-
-Both orders are locked in a fixed order (by id), so two trades on the same pair of orders can't
-deadlock.
-
-```sql
-CREATE OR REPLACE FUNCTION project.execute_trade(
-    p_buy_order uuid, p_sell_order uuid, p_quantity numeric, p_price numeric,
-    p_aggressor varchar DEFAULT NULL)
-RETURNS bigint LANGUAGE plpgsql AS $$
-DECLARE
-    b          project.orders%ROWTYPE;
-    s          project.orders%ROWTYPE;
-    v_market   project.markets%ROWTYPE;
-    v_trade_id bigint;
-    v_release  numeric;
-    v_cost     numeric;
-    v_proceeds numeric;
-    v_avg      numeric;
-BEGIN
-    IF p_buy_order IS NULL AND p_sell_order IS NULL THEN
-        RAISE EXCEPTION 'a trade needs at least one order' USING ERRCODE = 'check_violation';
-    END IF;
-
-    -- lock both orders in a fixed order (by id) so two concurrent trades on
-    -- the same pair of orders cannot deadlock
-    PERFORM 1 FROM project.orders WHERE id IN (p_buy_order, p_sell_order) ORDER BY id FOR UPDATE;
-    SELECT * INTO b FROM project.orders WHERE id = p_buy_order;
-    SELECT * INTO s FROM project.orders WHERE id = p_sell_order;
-    SELECT * INTO v_market FROM project.markets WHERE id = COALESCE(b.market_id, s.market_id);
-
-    INSERT INTO project.market_trades
-           (market_id, executed_at, price, quantity, side, source, buy_order_id, sell_order_id)
-    VALUES (v_market.id, now(), p_price, p_quantity,
-            COALESCE(p_aggressor, CASE WHEN p_sell_order IS NULL THEN 'buy' ELSE 'sell' END),
-            CASE WHEN p_buy_order IS NOT NULL AND p_sell_order IS NOT NULL THEN 'match' ELSE 'market' END,
-            p_buy_order, p_sell_order)
-    RETURNING id INTO v_trade_id;
-
-    IF p_buy_order IS NOT NULL THEN
-        v_release := project.order_reservation(b.quantity - b.filled_quantity, b.price)
-                   - project.order_reservation(b.quantity - b.filled_quantity - p_quantity, b.price);
-        v_cost    := LEAST(round(p_quantity * p_price, 4), v_release);
-
-        UPDATE project.users
-           SET reserved_balance  = reserved_balance  - v_release,
-               available_balance = available_balance + (v_release - v_cost),
-               invested_balance  = invested_balance  + v_cost,
-               updated_at        = now()
-         WHERE id = b.user_id;
-
-        INSERT INTO project.holdings AS h (user_id, crypto_id, quantity, avg_price, updated_at)
-        VALUES (b.user_id, v_market.crypto_id, p_quantity, p_price, now())
-        ON CONFLICT (user_id, crypto_id) DO UPDATE
-           SET avg_price  = (h.quantity * h.avg_price + EXCLUDED.quantity * EXCLUDED.avg_price)
-                            / (h.quantity + EXCLUDED.quantity),
-               quantity   = h.quantity + EXCLUDED.quantity,
-               updated_at = now();
-
-        INSERT INTO project.transactions (user_id, type, amount, currency, related_order, description)
-        VALUES (b.user_id, 'buy', -v_cost, v_market.quote_currency, b.id,
-                format('Buy %s @ %s (trade %s)', p_quantity, p_price, v_trade_id));
-    END IF;
-
-    IF p_sell_order IS NOT NULL THEN
-        SELECT avg_price INTO v_avg FROM project.holdings
-         WHERE user_id = s.user_id AND crypto_id = v_market.crypto_id FOR UPDATE;
-
-        UPDATE project.holdings
-           SET quantity          = quantity - p_quantity,
-               reserved_quantity = reserved_quantity - p_quantity,
-               updated_at        = now()
-         WHERE user_id = s.user_id AND crypto_id = v_market.crypto_id;
-
-        v_proceeds := round(p_quantity * p_price, 4);
-        UPDATE project.users
-           SET available_balance = available_balance + v_proceeds,
-               invested_balance  = GREATEST(invested_balance - round(p_quantity * v_avg, 4), 0),
-               updated_at        = now()
-         WHERE id = s.user_id;
-
-        INSERT INTO project.transactions (user_id, type, amount, currency, related_order, description)
-        VALUES (s.user_id, 'sell', v_proceeds, v_market.quote_currency, s.id,
-                format('Sell %s @ %s (trade %s)', p_quantity, p_price, v_trade_id));
-    END IF;
-
-    RETURN v_trade_id;
-END $$;
-```
-
-## Balance and reservation consistency
-
-### Data requirements description
-
-**Business rule.**
-
-1. **Reserved cash matches active buy orders.** A user's `reserved_balance` always equals what
-   their active buy orders still reserve: remaining quantity × order price, summed.
-2. **Reserved crypto matches active sell orders.** A user's `holdings.reserved_quantity` for a
-   crypto always equals the remaining quantity of their active sell orders for it.
-3. **Cash matches the ledger.** `available_balance + reserved_balance` always equals the sum of
-   the user's ledger (`transactions`). Reserving only moves cash between the two columns; money
-   actually arrives or leaves only with a ledger row (deposit, buy fill, sell fill).
-
-Together these make it impossible for an order operation to leave a balance in an inconsistent
-state: money or crypto reserved for nothing, an order that is not backed by a reservation (and
-so could spend the same money twice), or cash that appeared or vanished without a ledger entry.
-
-**Why it is non-trivial.** Each rule is an equality between a column and an aggregate over
-*other* rows of *other* tables. Worse, every legitimate operation breaks it for a moment:
-placing a buy moves cash to `reserved_balance` in one statement and inserts the order in the
-next; a trade releases the reservation, pays, and writes the ledger in several statements. Only
-the state at the end of the transaction has to be consistent.
-
-**PostgreSQL feature.** `CONSTRAINT TRIGGER … DEFERRABLE INITIALLY DEFERRED` — row triggers
-whose check runs at `COMMIT`, on the final state. A transaction that leaves any of the three
-equalities broken fails at `COMMIT` and is rolled back as a whole. Each rule has a trigger on
-every table whose change can break it. `order_reservation(remaining, price)` is the one place
-that defines how a reservation is rounded, so placing, filling, cancelling and checking always
-agree to the last decimal.
-
-**Tables affected.** `users`, `holdings`, `orders`, `transactions`.
-
-### Implementation
-
-#### Stored procedures/functions
-
-```sql
-CREATE OR REPLACE FUNCTION project.order_reservation(p_remaining numeric, p_price numeric)
-RETURNS numeric LANGUAGE sql IMMUTABLE AS $$
-    SELECT round(p_remaining * p_price, 4)
-$$;
-```
-
-#### Triggers
-
-```sql
-CREATE OR REPLACE FUNCTION project.trg_reserved_cash_matches_orders()
-RETURNS trigger LANGUAGE plpgsql AS $$
-DECLARE
-    v_user     uuid := CASE WHEN TG_TABLE_NAME = 'users' THEN NEW.id END;
-    v_reserved numeric;
-    v_needed   numeric;
-BEGIN
-    IF TG_TABLE_NAME = 'orders' THEN
-        v_user := NEW.user_id;
-    END IF;
-    SELECT reserved_balance INTO v_reserved FROM project.users WHERE id = v_user;
-    IF NOT FOUND THEN
-        RETURN NULL;
-    END IF;
-    SELECT COALESCE(SUM(project.order_reservation(quantity - filled_quantity, price)), 0)
-      INTO v_needed
-      FROM project.orders
-     WHERE user_id = v_user AND side = 'buy' AND status IN ('open', 'partially_filled');
-    IF v_reserved <> v_needed THEN
-        RAISE EXCEPTION 'reserved balance % does not match the % needed by active buy orders (user %)',
-            v_reserved, v_needed, v_user
-            USING ERRCODE = 'check_violation', CONSTRAINT = 'reserved_cash_matches_orders';
-    END IF;
-    RETURN NULL;
-END $$;
-```
-
-```sql
-CREATE OR REPLACE FUNCTION project.trg_reserved_crypto_matches_orders()
-RETURNS trigger LANGUAGE plpgsql AS $$
-DECLARE
-    v_user     uuid;
-    v_crypto   uuid;
-    v_reserved numeric;
-    v_needed   numeric;
-BEGIN
-    IF TG_TABLE_NAME = 'holdings' THEN
-        v_user := NEW.user_id;
-        v_crypto := NEW.crypto_id;
-    ELSE
-        IF NEW.side <> 'sell' THEN
-            RETURN NULL;
-        END IF;
-        v_user := NEW.user_id;
-        SELECT crypto_id INTO v_crypto FROM project.markets WHERE id = NEW.market_id;
-    END IF;
-    SELECT COALESCE(SUM(reserved_quantity), 0) INTO v_reserved
-      FROM project.holdings WHERE user_id = v_user AND crypto_id = v_crypto;
-    SELECT COALESCE(SUM(o.quantity - o.filled_quantity), 0) INTO v_needed
-      FROM project.orders o
-      JOIN project.markets m ON m.id = o.market_id
-     WHERE o.user_id = v_user AND m.crypto_id = v_crypto
-       AND o.side = 'sell' AND o.status IN ('open', 'partially_filled');
-    IF v_reserved <> v_needed THEN
-        RAISE EXCEPTION 'reserved quantity % does not match the % needed by active sell orders (user %, crypto %)',
-            v_reserved, v_needed, v_user, v_crypto
-            USING ERRCODE = 'check_violation', CONSTRAINT = 'reserved_crypto_matches_orders';
-    END IF;
-    RETURN NULL;
-END $$;
-```
-
-```sql
-CREATE OR REPLACE FUNCTION project.trg_cash_matches_ledger()
-RETURNS trigger LANGUAGE plpgsql AS $$
-DECLARE
-    v_user   uuid;
-    v_cash   numeric;
-    v_ledger numeric;
-BEGIN
-    IF TG_TABLE_NAME = 'users' THEN
-        v_user := NEW.id;
-    ELSIF TG_OP = 'DELETE' THEN
-        v_user := OLD.user_id;
-    ELSE
-        v_user := NEW.user_id;
-    END IF;
-    SELECT available_balance + reserved_balance INTO v_cash FROM project.users WHERE id = v_user;
-    IF NOT FOUND THEN
-        RETURN NULL;
-    END IF;
-    SELECT COALESCE(SUM(amount), 0) INTO v_ledger FROM project.transactions WHERE user_id = v_user;
-    IF v_cash <> v_ledger THEN
-        RAISE EXCEPTION 'cash % (available + reserved) does not match the ledger total % (user %)',
-            v_cash, v_ledger, v_user
-            USING ERRCODE = 'check_violation', CONSTRAINT = 'cash_matches_ledger';
-    END IF;
-    RETURN NULL;
-END $$;
-```
-
-```sql
-CREATE INDEX idx_orders_active ON project.orders (user_id, side)
-    WHERE status IN ('open', 'partially_filled');
-
-CREATE CONSTRAINT TRIGGER reserved_cash_matches_orders
-    AFTER INSERT OR UPDATE OF reserved_balance ON project.users
-    DEFERRABLE INITIALLY DEFERRED
-    FOR EACH ROW EXECUTE FUNCTION project.trg_reserved_cash_matches_orders();
-
-CREATE CONSTRAINT TRIGGER reserved_cash_matches_orders
-    AFTER INSERT OR UPDATE OF status, filled_quantity ON project.orders
-    DEFERRABLE INITIALLY DEFERRED
-    FOR EACH ROW EXECUTE FUNCTION project.trg_reserved_cash_matches_orders();
-
-CREATE CONSTRAINT TRIGGER reserved_crypto_matches_orders
-    AFTER INSERT OR UPDATE OF reserved_quantity ON project.holdings
-    DEFERRABLE INITIALLY DEFERRED
-    FOR EACH ROW EXECUTE FUNCTION project.trg_reserved_crypto_matches_orders();
-
-CREATE CONSTRAINT TRIGGER reserved_crypto_matches_orders
-    AFTER INSERT OR UPDATE OF status, filled_quantity ON project.orders
-    DEFERRABLE INITIALLY DEFERRED
-    FOR EACH ROW EXECUTE FUNCTION project.trg_reserved_crypto_matches_orders();
-
-CREATE CONSTRAINT TRIGGER cash_matches_ledger
-    AFTER INSERT OR UPDATE OF available_balance, reserved_balance ON project.users
-    DEFERRABLE INITIALLY DEFERRED
-    FOR EACH ROW EXECUTE FUNCTION project.trg_cash_matches_ledger();
-
-CREATE CONSTRAINT TRIGGER cash_matches_ledger
-    AFTER INSERT OR UPDATE OR DELETE ON project.transactions
-    DEFERRABLE INITIALLY DEFERRED
-    FOR EACH ROW EXECUTE FUNCTION project.trg_cash_matches_ledger();
-```
-
-The partial index `idx_orders_active` covers exactly the rows the reservation checks sum, and
-stays small because active orders are few compared with the whole order history.
-
-## Placing and cancelling orders
-
-### Data requirements description
-
-**Business rule.** Placing an order must, as one unit:
-
-1. check the market, side, type, quantity and price;
-2. reserve what the order commits — cash (quantity × price) for a buy, crypto for a sell — and
-   refuse the order if not enough is free;
-3. record the order;
-4. match it against the order book: other users' active limit orders on the same market whose
-   price is acceptable, best price first and oldest first (price–time priority), each trade at
-   the resting order's price;
-5. fill whatever is still unfilled from the simulated market if it is marketable at the current
-   market price.
-
-A **market order** is priced at the current market price, so it always fills completely in step
-4 or 5 and never waits. A **limit order** that isn't marketable stays in the order book.
-Cancelling an order must release exactly what it still reserves, and only for an active order of
-the caller.
-
-**Why it is non-trivial.** It is a multi-step operation over five tables whose steps depend on
-each other (how much is left after each match, what to release), and it has to be correct under
-concurrency: two orders of the same user must not both see the same free cash.
-
-**PostgreSQL feature.** Stored functions (PL/pgSQL). `place_order` locks the user's row
-(`SELECT … FOR UPDATE`) before checking free cash or crypto, so concurrent orders of the same
-user are serialised. Every step's consistency is still checked by the triggers of requirements
-1–3.
-
-**Tables affected.** `orders`, `users`, `holdings`, `market_trades`, `transactions`.
-
-### Implementation
-
-#### Stored procedures/functions
-
-```sql
-CREATE OR REPLACE FUNCTION project.latest_price(p_market_id uuid)
-RETURNS numeric LANGUAGE sql STABLE AS $$
-    SELECT price FROM project.market_trades
-     WHERE market_id = p_market_id
-     ORDER BY executed_at DESC, id DESC
-     LIMIT 1
-$$;
-```
-
-```sql
-CREATE OR REPLACE FUNCTION project.match_order(p_order_id uuid)
-RETURNS int LANGUAGE plpgsql AS $$
-DECLARE
-    o       project.orders%ROWTYPE;
-    r       record;
-    v_rem   numeric;
-    v_qty   numeric;
-    v_count int := 0;
-BEGIN
-    SELECT * INTO o FROM project.orders WHERE id = p_order_id FOR UPDATE;
-    FOR r IN
-        SELECT id, price, quantity - filled_quantity AS remaining
-          FROM project.orders
-         WHERE market_id = o.market_id
-           AND side <> o.side
-           AND type = 'limit'
-           AND status IN ('open', 'partially_filled')
-           AND user_id <> o.user_id
-           AND (o.side = 'buy'  AND price <= o.price
-             OR o.side = 'sell' AND price >= o.price)
-         ORDER BY CASE WHEN o.side = 'buy'  THEN price END ASC,
-                  CASE WHEN o.side = 'sell' THEN price END DESC,
-                  placed_at, id
-           FOR UPDATE
-    LOOP
-        SELECT quantity - filled_quantity INTO v_rem FROM project.orders WHERE id = p_order_id;
-        EXIT WHEN v_rem = 0;
-        v_qty := LEAST(v_rem, r.remaining);
-        IF o.side = 'buy' THEN
-            PERFORM project.execute_trade(o.id, r.id, v_qty, r.price, 'buy');
-        ELSE
-            PERFORM project.execute_trade(r.id, o.id, v_qty, r.price, 'sell');
-        END IF;
-        v_count := v_count + 1;
-    END LOOP;
-    RETURN v_count;
-END $$;
-```
-
-```sql
-CREATE OR REPLACE FUNCTION project.place_order(
-    p_user_id uuid, p_market_id uuid, p_side varchar, p_type varchar,
-    p_quantity numeric, p_limit_price numeric DEFAULT NULL)
-RETURNS uuid LANGUAGE plpgsql AS $$
-DECLARE
-    v_market    numeric := project.latest_price(p_market_id);
-    v_price     numeric;
-    v_available numeric;
-    v_free      numeric;
-    v_crypto    uuid;
-    v_order     uuid;
-    v_rem       numeric;
-BEGIN
-    IF p_side NOT IN ('buy', 'sell') OR p_type NOT IN ('market', 'limit') THEN
-        RAISE EXCEPTION 'invalid side % or type %', p_side, p_type USING ERRCODE = 'check_violation';
-    END IF;
-    IF p_quantity IS NULL OR p_quantity <= 0 THEN
-        RAISE EXCEPTION 'quantity must be positive' USING ERRCODE = 'check_violation';
-    END IF;
-    IF p_type = 'limit' THEN
-        IF p_limit_price IS NULL OR p_limit_price <= 0 THEN
-            RAISE EXCEPTION 'a limit order needs a positive limit price' USING ERRCODE = 'check_violation';
-        END IF;
-        v_price := p_limit_price;
-    ELSE
-        IF v_market IS NULL THEN
-            RAISE EXCEPTION 'market has no price yet' USING ERRCODE = 'check_violation';
-        END IF;
-        v_price := v_market;
-    END IF;
-
-    SELECT available_balance INTO v_available FROM project.users WHERE id = p_user_id FOR UPDATE;
-    IF NOT FOUND THEN
-        RAISE EXCEPTION 'user % does not exist', p_user_id USING ERRCODE = 'no_data_found';
-    END IF;
-
-    IF p_side = 'buy' THEN
-        IF v_available < project.order_reservation(p_quantity, v_price) THEN
-            RAISE EXCEPTION 'insufficient funds: the order needs %, available %',
-                project.order_reservation(p_quantity, v_price), v_available
-                USING ERRCODE = 'check_violation';
-        END IF;
-        UPDATE project.users
-           SET available_balance = available_balance - project.order_reservation(p_quantity, v_price),
-               reserved_balance  = reserved_balance  + project.order_reservation(p_quantity, v_price),
-               updated_at        = now()
-         WHERE id = p_user_id;
-    ELSE
-        SELECT crypto_id INTO v_crypto FROM project.markets WHERE id = p_market_id;
-        SELECT quantity - reserved_quantity INTO v_free FROM project.holdings
-         WHERE user_id = p_user_id AND crypto_id = v_crypto FOR UPDATE;
-        IF COALESCE(v_free, 0) < p_quantity THEN
-            RAISE EXCEPTION 'insufficient holding: trying to sell %, free to sell %', p_quantity, COALESCE(v_free, 0)
-                USING ERRCODE = 'check_violation';
-        END IF;
-        UPDATE project.holdings
-           SET reserved_quantity = reserved_quantity + p_quantity, updated_at = now()
-         WHERE user_id = p_user_id AND crypto_id = v_crypto;
-    END IF;
-
-    INSERT INTO project.orders (user_id, market_id, side, type, status, quantity, price)
-    VALUES (p_user_id, p_market_id, p_side, p_type, 'open', p_quantity, v_price)
-    RETURNING id INTO v_order;
-
-    PERFORM project.match_order(v_order);
-
-    SELECT quantity - filled_quantity INTO v_rem FROM project.orders WHERE id = v_order;
-    IF v_rem > 0 AND v_market IS NOT NULL
-       AND (p_side = 'buy' AND v_market <= v_price OR p_side = 'sell' AND v_market >= v_price) THEN
-        IF p_side = 'buy' THEN
-            PERFORM project.execute_trade(v_order, NULL, v_rem, v_market, 'buy');
-        ELSE
-            PERFORM project.execute_trade(NULL, v_order, v_rem, v_market, 'sell');
-        END IF;
-    END IF;
-    RETURN v_order;
-END $$;
-```
-
-```sql
-CREATE OR REPLACE FUNCTION project.cancel_order(p_order_id uuid, p_user_id uuid DEFAULT NULL)
-RETURNS void LANGUAGE plpgsql AS $$
-DECLARE
-    o     project.orders%ROWTYPE;
-    v_rem numeric;
-BEGIN
-    SELECT * INTO o FROM project.orders WHERE id = p_order_id FOR UPDATE;
-    IF NOT FOUND THEN
-        RAISE EXCEPTION 'order % does not exist', p_order_id USING ERRCODE = 'no_data_found';
-    END IF;
-    IF p_user_id IS NOT NULL AND o.user_id <> p_user_id THEN
-        RAISE EXCEPTION 'order % does not belong to this user', p_order_id
-            USING ERRCODE = 'insufficient_privilege';
-    END IF;
-    IF o.status NOT IN ('open', 'partially_filled') THEN
-        RAISE EXCEPTION 'order % is % and cannot be cancelled', p_order_id, o.status
-            USING ERRCODE = 'check_violation';
-    END IF;
-
-    v_rem := o.quantity - o.filled_quantity;
-    IF o.side = 'buy' THEN
-        UPDATE project.users
-           SET reserved_balance  = reserved_balance  - project.order_reservation(v_rem, o.price),
-               available_balance = available_balance + project.order_reservation(v_rem, o.price),
-               updated_at        = now()
-         WHERE id = o.user_id;
-    ELSE
-        UPDATE project.holdings h
-           SET reserved_quantity = h.reserved_quantity - v_rem, updated_at = now()
-          FROM project.markets m
-         WHERE m.id = o.market_id AND h.user_id = o.user_id AND h.crypto_id = m.crypto_id;
-    END IF;
-
-    UPDATE project.orders SET status = 'cancelled' WHERE id = p_order_id;
-END $$;
-```
-
-In the prototype, placing an order is now a single call:
-
-```go
-err = db.DB.QueryRow(
-	`SELECT place_order($1, $2, $3, $4, $5, $6)`,
-	s.UserID, m.ID, side, orderType, qty, limit.value(),
-).Scan(&orderID)
-```
-
-Cancelling is `SELECT cancel_order($1, $2)` with the order the user picked from a numbered list
-of their open orders ([`server/trade.go`](../../server/trade.go)).
-
-## Automatic recording of order events
-
-### Data requirements description
-
-**Business rule.** Every important thing that happens to an order is recorded with its time:
-placement, each fill (with the quantity filled and the trade price), and cancellation. This is
-the order's audit trail — the history of *how* it reached its current state, which the order row
-alone (only the current state) cannot show.
-
-**Why it is non-trivial.** Events come from several places — `place_order`, trades made by
-`match_order`, trades made by the background job, `cancel_order`, and any direct SQL. Recording
-them in each of those places would miss some; recording them where the change actually happens
-cannot.
-
-**PostgreSQL feature.** `AFTER INSERT OR UPDATE` row trigger on `orders`, writing into the new
-table `order_events`. The fill price is handed over by the trade trigger through a
-transaction-local setting.
-
-**Tables affected.** `order_events` (new), written from changes to `orders`.
-
-### Implementation
-
-```sql
-CREATE TABLE project.order_events (
-    id           bigserial      PRIMARY KEY,
-    order_id     uuid           NOT NULL REFERENCES project.orders(id) ON DELETE CASCADE,
-    event_type   varchar(20)    NOT NULL
-                 CHECK (event_type IN ('placed', 'partially_filled', 'filled', 'cancelled')),
-    quantity     numeric(20,4)  NOT NULL,
-    price        numeric(18,6),
-    status_after varchar(20)    NOT NULL,
-    created_at   timestamptz    NOT NULL DEFAULT clock_timestamp()
-);
-```
-
-#### Triggers
-
-```sql
-CREATE OR REPLACE FUNCTION project.trg_orders_events()
-RETURNS trigger LANGUAGE plpgsql AS $$
-BEGIN
-    IF TG_OP = 'INSERT' THEN
-        INSERT INTO project.order_events (order_id, event_type, quantity, price, status_after)
-        VALUES (NEW.id, 'placed', NEW.quantity, NEW.price, NEW.status);
-    ELSIF NEW.filled_quantity > OLD.filled_quantity THEN
-        INSERT INTO project.order_events (order_id, event_type, quantity, price, status_after)
-        VALUES (NEW.id,
-                CASE WHEN NEW.status = 'executed' THEN 'filled' ELSE 'partially_filled' END,
-                NEW.filled_quantity - OLD.filled_quantity,
-                current_setting('eduberza.trade_price', true)::numeric,
-                NEW.status);
-    ELSIF NEW.status = 'cancelled' AND OLD.status <> 'cancelled' THEN
-        INSERT INTO project.order_events (order_id, event_type, quantity, price, status_after)
-        VALUES (NEW.id, 'cancelled', NEW.quantity - NEW.filled_quantity, NEW.price, NEW.status);
-    END IF;
-    RETURN NULL;
-END $$;
-
-CREATE TRIGGER orders_events
-    AFTER INSERT OR UPDATE ON project.orders
-    FOR EACH ROW EXECUTE FUNCTION project.trg_orders_events();
-```
-
-## Views for derived trading data
-
-### Data requirements description
-
-The application needs several things that are *derived* from orders, trades and balances. They
-are defined once as views, instead of repeating the calculations in the application code:
-
-| View | Derived data | Used by |
-|---|---|---|
-| `v_active_orders` | active orders with remaining quantity and what each one holds in reserve | CLI `[13] My open orders`, `[14] Cancel an order` |
-| `v_order_book` | current order book: resting limit orders aggregated per market, side and price level | CLI `[12] Order book`, and when placing an order |
-| `v_order_history` | every order with its fill progress, number of trades and average fill price (from its trades) | CLI result of placing an order |
-| `v_trader_balances` | cash split into available and reserved, the ledger total it must equal, holdings at market value, net worth | CLI `[1] View balance` |
-
-None of these repeats a P6 report: P6 aggregates performance over a period, while these show
-the current state of the order book and accounts.
-
-### Implementation
-
-#### Views
-
-```sql
-CREATE VIEW project.v_active_orders AS
-SELECT o.id            AS order_id,
-       o.user_id,
-       u.username,
-       o.market_id,
-       c.symbol,
-       m.quote_currency,
-       o.side,
-       o.type,
-       o.status,
-       o.quantity,
-       o.filled_quantity,
-       o.quantity - o.filled_quantity AS remaining,
-       o.price,
-       CASE WHEN o.side = 'buy'
-            THEN project.order_reservation(o.quantity - o.filled_quantity, o.price) ELSE 0 END AS reserved_cash,
-       CASE WHEN o.side = 'sell' THEN o.quantity - o.filled_quantity ELSE 0 END             AS reserved_crypto,
-       o.placed_at
-FROM project.orders o
-JOIN project.users   u ON u.id = o.user_id
-JOIN project.markets m ON m.id = o.market_id
-JOIN project.crypto  c ON c.id = m.crypto_id
-WHERE o.status IN ('open', 'partially_filled');
-```
-
-```sql
-CREATE VIEW project.v_order_book AS
-SELECT market_id,
-       symbol,
-       quote_currency,
-       side,
-       price,
-       SUM(remaining) AS quantity,
-       COUNT(*)       AS orders
-FROM project.v_active_orders
-WHERE type = 'limit'
-GROUP BY market_id, symbol, quote_currency, side, price;
-```
-
-```sql
-CREATE VIEW project.v_order_history AS
-SELECT o.id            AS order_id,
-       o.user_id,
-       u.username,
-       c.symbol,
-       o.side,
-       o.type,
-       o.status,
-       o.quantity,
-       o.filled_quantity,
-       o.quantity - o.filled_quantity AS remaining,
-       o.price,
-       f.trades,
-       f.avg_fill_price,
-       o.placed_at,
-       o.executed_at
-FROM project.orders o
-JOIN project.users   u ON u.id = o.user_id
-JOIN project.markets m ON m.id = o.market_id
-JOIN project.crypto  c ON c.id = m.crypto_id
-LEFT JOIN LATERAL (
-    SELECT COUNT(*) AS trades,
-           round(SUM(t.quantity * t.price) / NULLIF(SUM(t.quantity), 0), 6) AS avg_fill_price
-      FROM (SELECT quantity, price FROM project.market_trades WHERE buy_order_id  = o.id
-            UNION ALL
-            SELECT quantity, price FROM project.market_trades WHERE sell_order_id = o.id) t
-) f ON true;
-```
-
-```sql
-CREATE VIEW project.v_trader_balances AS
-SELECT u.id                         AS user_id,
-       u.username,
-       u.available_balance,
-       u.reserved_balance,
-       u.available_balance + u.reserved_balance              AS total_cash,
-       COALESCE(l.ledger_total, 0)                           AS ledger_total,
-       u.invested_balance,
-       COALESCE(p.holdings_value, 0)                         AS holdings_value,
-       u.available_balance + u.reserved_balance + COALESCE(p.holdings_value, 0) AS net_worth
-FROM project.users u
-LEFT JOIN (SELECT user_id, SUM(amount) AS ledger_total
-             FROM project.transactions GROUP BY user_id) l ON l.user_id = u.id
-LEFT JOIN (SELECT user_id, SUM(market_value) AS holdings_value
-             FROM project.v_portfolio GROUP BY user_id) p ON p.user_id = u.id;
-```
-
-## Background job: filling resting limit orders
-
-### Data requirements description
-
-**Business rule.** In EduBerza the market price is moved by the simulator (the market bot), not
-by users' orders. A limit order that rests in the book — a buy at or above, or a sell at or
-below, the current market price — must then be filled by the simulated market, at the market
-price, just as it would have been had the price already been there when the order was placed.
-
-**Why it is relevant, and why a background job.** Without it, a limit order could only ever
-fill against another user's order, and with few users most limit orders would wait forever while
-the market price has long passed them. That would make limit orders useless in the simulation.
-Nothing happens at the moment the price crosses an order that a trigger could react to: the
-price moves through the bot's inserts into `market_trades`, which deliberately stay cheap single
-inserts. Scanning and filling every crossed order on every tick inside that insert would make
-each tick expensive. So the work runs as a periodic job after each round of price ticks.
-
-**PostgreSQL feature.** Stored function `fill_marketable_orders()`. It is scheduled by the
-application, because PostgreSQL has no built-in scheduler and the faculty server provides no
-`pg_cron` (checked: only `plpgsql` and `pgcrypto` are available, and the project role is not a
-superuser).
-
-- An advisory lock (`pg_try_advisory_xact_lock`) keeps two runs from filling the same orders
-  twice.
-- `FOR UPDATE … SKIP LOCKED` leaves alone an order a user is cancelling at that moment; the next
-  run picks it up.
-- Every fill goes through `execute_trade`, so all the rules above apply to it.
-
-**Tables affected.** `orders`, `market_trades`, `users`, `holdings`, `transactions`,
-`order_events`.
-
-### Implementation
-
-#### Stored procedures/functions
-
-```sql
-CREATE OR REPLACE FUNCTION project.fill_marketable_orders()
-RETURNS int LANGUAGE plpgsql AS $$
-DECLARE
-    r       record;
-    v_count int := 0;
-BEGIN
-    IF NOT pg_try_advisory_xact_lock(hashtext('project.fill_marketable_orders')) THEN
-        RETURN 0;
-    END IF;
-    FOR r IN
-        SELECT o.id, o.side, o.quantity - o.filled_quantity AS remaining, lp.price AS market_price
-          FROM project.orders o
-          JOIN project.markets m ON m.id = o.market_id AND m.is_active
-          CROSS JOIN LATERAL (SELECT project.latest_price(o.market_id) AS price) lp
-         WHERE o.type = 'limit'
-           AND o.status IN ('open', 'partially_filled')
-           AND (o.side = 'buy'  AND o.price >= lp.price
-             OR o.side = 'sell' AND o.price <= lp.price)
-         ORDER BY o.placed_at, o.id
-           FOR UPDATE OF o SKIP LOCKED
-    LOOP
-        IF r.side = 'buy' THEN
-            PERFORM project.execute_trade(r.id, NULL, r.remaining, r.market_price, 'sell');
-        ELSE
-            PERFORM project.execute_trade(NULL, r.id, r.remaining, r.market_price, 'buy');
-        END IF;
-        v_count := v_count + 1;
-    END LOOP;
-    RETURN v_count;
-END $$;
-```
-
-#### Scheduling
-
-In [`bots/main.go`](../../bots/main.go), after every round of price ticks:
-
-```go
-// P7 background job: the prices just moved, so fill any resting
-// limit order the new market price has reached.
-var filled int
-if err := db.QueryRow(`SELECT fill_marketable_orders()`).Scan(&filled); err != nil {
-	log.Printf("fill_marketable_orders: %v", err)
-} else if filled > 0 {
-	log.Printf("  filled %d resting limit order(s) at the new market price", filled)
-}
-```
-
-It can also be run by hand from any SQL client: `SELECT project.fill_marketable_orders();`.
-
-A run with the bot, after charlie placed a limit buy of 0.1 ETH at 3599 while the market was at
-3600: the bot's random walk took the price below 3599 and the job filled the order at the market
-price.
-
-```
-2026/09/24 13:19:02   filled 1 resting limit order(s) at the new market price
-```
-
-```
- username | side | type  |  status   | quantity | filled_quantity |    price    | avg_fill_price
-----------+------+-------+-----------+----------+-----------------+-------------+----------------
- charlie  | buy  | limit | cancelled |   0.1000 |          0.0000 | 3400.000000 |
- charlie  | buy  | limit | executed  |   0.1000 |          0.1000 | 3599.000000 |    3593.979167
-```
-
-## Tests proving the rules
-
-[`server/db/advanced_db_tests.sql`](../../server/db/advanced_db_tests.sql) plays a short trading
-story on the sample data and, along the way, tries to break every rule. It starts from alice with
-8250 USD and 0.5 ETH, bob with 5000 USD, charlie with 2500 USD, and ETH/USD last traded at 3520:
-
-1. alice places a limit sell of 0.3 ETH at 3600, and bob a limit buy of 0.1 at 3500. Both rest in
-   the book.
-2. bob places a limit buy of 0.2 at 3650. It crosses alice's ask, so they trade 0.2 at 3600:
-   alice's order becomes partially filled, and bob gets back the 10 he had reserved above the
-   trade price.
-3. charlie places a market buy of 0.15. He takes alice's remaining 0.1 from the book, and the
-   other 0.05 comes from the simulated market.
-4. Invalid trades, state changes and balance changes are attempted directly in SQL.
-5. bob cancels his bid.
-6. The simulator moves the price to 3450, and the background job fills bob's new limit buy at
-   3500.
-
-The deferred checks are forced with `SET CONSTRAINTS ALL IMMEDIATE`, so a violation shows up
-inside the test instead of at the final `COMMIT`. Everything is rolled back at the end. Run on
-PostgreSQL 17 after `-init`:
-
-```
-PASS  place: limit sell above the market rests in the book, crypto reserved: alice ETH reserved = 0.3000
-PASS  place: limit buy below the market rests in the book, cash reserved: bob available 4650.0000 reserved 350.0000
-PASS  event: placement recorded automatically:
-PASS  view: order book shows both price levels: buy 0.1000 @ 3500.000000, sell 0.3000 @ 3600.000000
-PASS  consistency holds after placing:
-PASS  place: buy without enough free cash: insufficient funds: the order needs 60000.0000, available 2500.0000
-PASS  place: sell more than is free (0.2 of 0.5 is already reserved): insufficient holding: trying to sell 0.3, free to sell 0.2000
-PASS  match: trade between the two orders at the resting price:
-PASS  status: seller partially filled, buyer executed (automatic): alice_ask partially_filled 0.2000/0.3000
-PASS  money: buyer paid 720, got back the 10 reserved above the trade price: bob available 3930.0000 reserved 350.0000
-PASS  crypto: 0.2 ETH moved from alice (0.1 still reserved) to bob:
-PASS  ledger: one buy and one sell row, linked to the orders:
-PASS  event: fills recorded automatically:
-PASS  consistency holds after the trade:
-PASS  market order: filled completely in two trades: executed, 2 trades, avg 3600.000000
-PASS  market order: alice's ask is now executed, nothing left reserved:
-PASS  consistency holds after the market order:
-PASS  trade: price above the buyer's limit: trade price 3700.000000 is outside the limit 3500.000000 of buy order …
-PASS  trade: more than the order has remaining: trade quantity 0.500000 exceeds the remaining quantity 0.1000 of order …
-PASS  trade: a sell order used as the buy side: order … is a sell order and cannot be the buy side of a trade
-PASS  trade: order of another market: order … is on a different market than the trade
-PASS  trade: executed order cannot trade again: order … is executed and cannot trade
-PASS  trade: a user with their own order: a user cannot trade with their own order
-PASS  trade: a trade that filled orders cannot be deleted: a trade that filled orders cannot be changed or deleted
-PASS  state: status cannot be set to executed by hand: order status executed does not match filled quantity 0.0000 of 0.1000; status is derived automatically
-PASS  state: filled quantity cannot be changed by hand: filled quantity of order … can only change through a trade
-PASS  state: executed order cannot be processed again: order … is already executed and cannot be processed again
-PASS  state: ordered quantity cannot change: user, market, side, type, quantity, price and placed_at of an order cannot change
-PASS  state: cancelling by hand without releasing the reservation: reserved balance 350.0000 does not match the 0 needed by active buy orders (user …)
-PASS  cancel: someone else's order: order … does not belong to this user
-PASS  cancel: 350 back from reserved to available, event recorded: bob available 4280.0000 reserved 0.0000
-PASS  cancel: a cancelled order cannot be cancelled again: order … is cancelled and cannot be cancelled
-PASS  trade: cancelled order cannot trade: order … is cancelled and cannot trade
-PASS  balance: reserving cash with no order behind it: reserved balance 100.0000 does not match the 0 needed by active buy orders (user …)
-PASS  balance: reserving crypto with no order behind it: reserved quantity 0.0100 does not match the 0 needed by active sell orders (user …, crypto …)
-PASS  balance: cash changed without a ledger row: cash 2060.0000 (available + reserved) does not match the ledger total 1960.0000 (user …)
-PASS  job: fills exactly the orders the new price reached: bob_bid3 executed @ 3450.000000, charlie_ask (3700) still open
-PASS  job: buyer paid 345, got the 5 above the fill price back:
-PASS  job: nothing more to do on a second run:
-PASS  consistency holds after the job:
-PASS  views: every trader's cash equals their ledger:
-
- passed | failed
---------+--------
-     41 |      0
-```
-
-The same rules seen from the prototype (bob, after alice put 0.3 ETH up for sale at 3600):
-
-```
--- Place buy order --
-Latest price for ETH/USD = 3520.000000
-Order book asks (other users' limit orders):
-     3600.000000        0.3000  (1 orders)
-[1] Market order (fills now at the best available price)
-[2] Limit order (fills only at your price or better, otherwise waits in the order book)
-> 2
-Quantity: 0.2
-Limit price: 3650
-Order executed: buy 0.2000 ETH, average price 3600.000000
-```
-
-## Changes to earlier phases
-
-- **Schema** ([RelationalDesign](../P2-RelationalDesign/RelationalDesign.md), [ERModel](../P1-ConceptualModel/ERModel.md)):
-  - `users.reserved_balance`;
-  - `orders.filled_quantity` and the status value `partially_filled`;
-  - `market_trades.buy_order_id` / `sell_order_id` (optional references to `orders` — a new
-    relationship "trade fills order");
-  - the new table `order_events`.
-- **Sample data** (`data_load.sql`):
-  - deposit rows for bob and charlie, whose balances previously had no ledger entries behind
-    them;
-  - alice's seeded order is imported as completely filled;
-  - the script runs as one transaction.
-
-  The balances documented in [BuildInstructions](../P4-Prototype/BuildInstructions.md) are
-  unchanged.
-- **P6 demo data** (`reports_demo_data.sql`): it adjusts the users' balances by exactly what it
-  adds to the ledger, and imports its orders as completely filled. Both
-  [AdvancedReports](../P6-AdvancedReports/AdvancedReports.md) outputs are unchanged.
-- **Prototype:**
-  - placing an order is one call to `place_order`, and market or limit can be chosen;
-  - new menu items `[12] Order book`, `[13] My open orders`, `[14] Cancel an order`;
-  - the balance screen shows reserved cash;
-  - the bot runs the background job.
-
-## AI usage
-
-AI was used in this phase and is logged in full, per the course rule for P1 onward.
-
-- **Phase log:** [AdvancedDatabaseDevelopmentAIUsage.md](AdvancedDatabaseDevelopmentAIUsage.md)
-
-**In short:** the requirements — order state consistency, filled/remaining quantities, reserved
-money and assets, trades only between compatible orders, the kinds of triggers, procedures and
-views, and a background job only if relevant — were mine. I asked the AI to turn them into
-concrete rules for the existing EduBerza database, implement them without redesigning it, test
-them and document them.
Index: cs/P7-AdvancedDatabaseDevelopment/AdvancedDatabaseDevelopmentAIUsage.md
===================================================================
--- docs/P7-AdvancedDatabaseDevelopment/AdvancedDatabaseDevelopmentAIUsage.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,213 +1,0 @@
-# Advanced Database Development AI Usage
-
-## Name of AI service/solution that was used
-
-**Claude Code** (Anthropic)
-
-- **URL:** https://claude.com/claude-code
-- **Type of service/subscription:** Claude subscription, model Claude Opus 5.5.
-
-## Final result
-
-### Diagram
-
-No new diagram. The data model gains four columns and one table, all listed under "Changes to
-earlier phases" in [AdvancedDatabaseDevelopment](AdvancedDatabaseDevelopment.md):
-
-- `users.reserved_balance`;
-- `orders.filled_quantity` (with the status `partially_filled`);
-- `market_trades.buy_order_id` and `market_trades.sell_order_id`;
-- `order_events`.
-
-The optional link from a trade to the orders it filled is a new relationship, which
-[ERModel](../P1-ConceptualModel/ERModel.md) and
-[RelationalDesign](../P2-RelationalDesign/RelationalDesign.md) must show as well.
-
-### Results in details / description
-
-My requirements, given to the AI as a list:
-
-- complex order consistency: no invalid state changes, filled and remaining quantities
-  consistent, finished orders never processed again;
-- balance and order consistency: active orders consistent with reserved money and assets, no
-  inconsistent balance states from order operations;
-- trade consistency: trades only between valid compatible orders, never more than the remaining
-  quantity, orders, trades and balances kept consistent;
-- which kinds of triggers, procedures, functions and views to build;
-- a background job only if it is genuinely relevant;
-- basic column constraints kept out of P7.
-
-The AI:
-
-- Checked the existing schema against those requirements. It found that three of them could not
-  be expressed without small additions: there was no filled quantity, no place for reserved
-  cash, and no link from a trade to its orders. It added only the four columns listed above and
-  the event table.
-- For every feature, wrote down the business rule, why it is non-trivial, which PostgreSQL
-  feature implements it and which tables it affects. This is the structure I asked for, and
-  [AdvancedDatabaseDevelopment](AdvancedDatabaseDevelopment.md) follows it.
-- Implemented [`advanced_db.sql`](../../server/db/advanced_db.sql):
-  - 5 row triggers (order lifecycle, order events, trade validation, trade fill, trade
-    immutability);
-  - 3 deferred constraint triggers (reserved cash, reserved crypto, cash equals ledger);
-  - the functions `place_order`, `match_order`, `execute_trade`, `cancel_order`,
-    `order_reservation` and `latest_price`;
-  - 4 views;
-  - the background job `fill_marketable_orders`.
-- Checked the faculty server before choosing how to schedule the background job. There is no
-  `pg_cron` and the role is not a superuser, so the job is a function that the bot process calls
-  after every round of price ticks.
-- Adapted the two data scripts to the new consistency rules and verified that both P6 report
-  outputs stay exactly as documented.
-- Wrote [`advanced_db_tests.sql`](../../server/db/advanced_db_tests.sql): a trading story with 41
-  checks, rolled back at the end. It ran them until all passed. The one failure along the way was
-  in the test itself: a check read the data in the same statement as the job it was checking.
-- Wired the prototype to use the new functions:
-  - order placement is a single `place_order` call, with market or limit orders;
-  - the order book, open orders and cancel are available from the menu, with cancel picked from
-    a numbered list;
-  - the balance screen shows reserved cash;
-  - the bot runs the job.
-
-  All of this was verified through the CLI and with the bot running.
-- Wrote this documentation.
-
-## Summary of AI involvement
-
-| | This session — 2026-09-24 |
-|---|---|
-| **What I brought** | The P7 requirements for EduBerza: order, balance and trade consistency, the kinds of triggers, procedures and views, and the condition on the background job |
-| **What the AI did** | Turned the requirements into concrete rules on the existing schema, implemented and tested them, wired them into the prototype, wrote the documentation |
-| **What I decided** | The requirements themselves; to discard the AI's earlier, self-proposed P7 version (see the log) and redo the phase from my requirements; to keep the schema changes minimal |
-
-## Entire AI usage log
-
-### 2026-09-24 — earlier attempt, discarded
-
-Earlier in the same session I had pasted the P7 and P8 rubrics without ideas of my own, and the
-AI proposed and implemented a P7 of its own design: custom domains, an append-only ledger,
-candles derived from trades and a maintenance job. Before submitting anything I decided to redo
-the phase from my own requirements, and asked for that version to be reverted. None of it
-remains in the project. It is mentioned here only so the log is complete.
-
-### 2026-09-24 — this phase
-
-**Prompt (student, verbatim):**
-> P7 requirements are:
->
-> more complex data constraints and consistency requirements
-> business scenarios that require special database checks
-> triggers
-> stored procedures and functions
-> views
-> background jobs
-> documentation of the implementation
->
-> Basic column constraints such as NOT NULL, UNIQUE, CHECK, PRIMARY KEY, and basic foreign keys
-> are Phase P2 and must NOT be proposed as P7 features.
->
-> Based on my existing EduBerza database, identify and implement only genuinely NON-TRIVIAL P7
-> requirements.
->
-> Focus on these types of requirements:
->
-> Complex order consistency
-> Prevent invalid order state changes.
-> Ensure filled/remaining quantities stay consistent.
-> Prevent cancelled or completed orders from being processed again.
-> Balance and order consistency
-> Ensure active orders are consistent with reserved virtual money/assets.
-> Prevent inconsistent balance states caused by order operations.
-> Trade consistency
-> Ensure a trade can only happen between valid compatible orders.
-> Ensure trade quantity cannot exceed the remaining order quantity.
-> Keep orders, trades, and balances consistent.
-> Triggers
-> Create triggers only for automatic database behavior that is genuinely required, such as:
-> automatic validation of complex business rules
-> automatic order status changes
-> automatic recording of important order/trade events
-> Stored procedures/functions
-> Create procedures/functions for complex database operations such as:
-> placing an order with the necessary consistency checks
-> executing a trade while updating all related data consistently
-> Views
-> Create useful views for derived application data, such as:
-> current order book
-> active orders
-> trader portfolio/balances
-> order/trade history
->
-> Do not recreate reports that were already implemented in P6 unless a view is needed
-> specifically as part of P7.
->
-> Background jobs
-> Implement a meaningful background database job only if it is relevant to the existing
-> EduBerza design.
-> Do not create an artificial background job just to satisfy the requirement.
->
-> For every proposed P7 feature, explain:
->
-> the business rule
-> why it is non-trivial
-> which PostgreSQL feature should implement it
-> which existing tables it affects
->
-> Then implement the selected P7 features in the existing project without redesigning the
-> database unnecessarily.
->
-> Finally, document:
->
-> complex data requirements
-> triggers
-> stored procedures/functions
-> views
-> background jobs
-> how each one maintains consistency or enforces a business rule
-> examples/tests proving that the rules work
->
-> Keep the implementation strictly within P7.
-
-(The first time I sent it the message was interrupted; I sent it again, followed by "and revert
-the previous changes".)
-
-**Response (AI, summarised):**
-
-- **Revert.** It restored every file changed by the discarded attempt to the last commit and
-  deleted the files that attempt had created. It kept the SSH connection support (from before
-  P7) and my own uncommitted edits.
-- **Analysis.** It reported that three requirements need data the schema doesn't have — a filled
-  quantity, a reserved cash balance, and trade-to-order links — and added exactly those
-  (`orders.filled_quantity` plus status `partially_filled`, `users.reserved_balance`,
-  `market_trades.buy_order_id`/`sell_order_id`), plus `order_events` for the automatic event
-  recording.
-- **Design:**
-  - A trade is always a `market_trades` row naming the orders it fills. Triggers on that table
-    validate compatibility and fill the orders; the order's status is derived from its filled
-    quantity by the order trigger.
-  - `filled_quantity` may only change while a trade is being recorded, which is detected through
-    a transaction-local setting.
-  - The three balance rules are deferred constraint triggers, because every legitimate operation
-    breaks them between its statements.
-  - Placing an order reserves first, then matches against the order book (price–time priority),
-    then fills the marketable remainder from the simulated market.
-  - Rounding of reservations is defined once (`order_reservation`), so placing, filling,
-    cancelling and checking always agree.
-- **Background job.** It argued the job is relevant rather than artificial: the market price is
-  moved by the simulator, and without the job resting limit orders would never fill once the
-  price reaches them. It explained why a trigger on the bot's price ticks would be the wrong
-  place for this work.
-- **Verification** (local PostgreSQL 17 in Docker only, never the faculty database):
-  - both P6 reports return the documented numbers;
-  - the P6 demo script can be re-run without doubling anything;
-  - the test story passes 41 of 41;
-  - through the CLI: a limit sell resting in the book, a crossing limit buy trading at the resting
-    price with the difference refunded, a market buy filled partly from the book and partly from
-    the market, the order book, open orders, and cancelling with the reservation released;
-  - with the bot running, the job filled a resting limit order once the price walk reached it.
-- **Documentation.** It wrote [AdvancedDatabaseDevelopment](AdvancedDatabaseDevelopment.md) and
-  this log, with the SQL on the page taken automatically from `advanced_db.sql` so the two cannot
-  differ, together with the faculty-site (wiki) versions of both pages.
-
-**What I decided:** the requirements are mine. I kept the AI's minimal schema additions and its
-design for fulfilling them, and asked for no changes to it before it was implemented.
Index: cs/P7-AdvancedDatabaseDevelopment/wiki/AdvancedDatabaseDevelopment.md
===================================================================
--- docs/P7-AdvancedDatabaseDevelopment/wiki/AdvancedDatabaseDevelopment.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,1434 +1,0 @@
-    = Advanced Database Development =
-
-    !EduBerza's users trade virtual money and virtual crypto with each other and with a simulated
-    market. Up to P6 the prototype only knew market orders that filled immediately against the
-    latest price, so an order was either untouched or completely done. This phase adds what makes an
-    exchange consistent once orders can '''wait in an order book, fill in parts, and trade with each other''', and puts every rule that keeps orders, trades and balances in agreement into the
-    database itself — so it holds no matter who writes the data (the CLI, the market bot, a script,
-    or someone typing SQL in DBeaver).
-
-    Only rules that span several rows or several tables are listed. `NOT NULL`, `UNIQUE`, `CHECK`,
-    primary and foreign keys are P2 ([wiki:RelationalDesign])
-    and are not presented as P7 features.
-
-    All of it is in `server/db/advanced_db.sql`, run by `-init`
-    between `schema_creation.sql` and `data_load.sql`. Every rule is exercised by
-    `server/db/advanced_db_tests.sql` (see
-    Tests).
-
-    == Overview ==
-
-    ||= # =||= Requirement =||= Triggers =||= Procedures / functions =||= Views =||= Tables affected =||
-    || 1 || Order lifecycle and filled/remaining consistency || `orders_lifecycle` ||  ||  || `orders` ||
-    || 2 || Trade consistency || `market_trades_validate`, `market_trades_fill`, `market_trades_immutable` || `execute_trade` ||  || `market_trades`, `orders`, `users`, `holdings`, `transactions` ||
-    || 3 || Balance and reservation consistency || `reserved_cash_matches_orders`, `reserved_crypto_matches_orders`, `cash_matches_ledger` (deferred) || `order_reservation` ||  || `users`, `holdings`, `orders`, `transactions` ||
-    || 4 || Placing and cancelling orders ||  || `place_order`, `match_order`, `cancel_order` ||  || all of the above ||
-    || 5 || Automatic recording of order events || `orders_events` ||  ||  || `order_events` (new) ||
-    || 6 || Views for derived trading data ||  ||  || `v_order_book`, `v_active_orders`, `v_order_history`, `v_trader_balances` || read-only ||
-    || 7 || Background job: filling resting limit orders ||  || `fill_marketable_orders` ||  || `orders`, `market_trades`, … ||
-
-    === Schema additions ===
-
-    The existing design had no place for three things the requirements need, so four columns were
-    added — nothing else in the design changed:
-
-    ||= Column =||= Why it is needed =||
-    || `orders.filled_quantity` (+ status value `partially_filled`) || "filled / remaining quantity" cannot be kept consistent without storing how much was filled; remaining = `quantity − filled_quantity` ||
-    || `users.reserved_balance` || cash committed to open buy orders must be set aside somewhere; `holdings.reserved_quantity` already did this for crypto on sell orders ||
-    || `market_trades.buy_order_id`, `market_trades.sell_order_id` || "a trade can only happen between compatible orders" needs the trade to say which orders it filled. `NULL` on a side means the simulated market was the counterparty; the bot's price ticks have both `NULL` ||
-
-    One new table, `order_events`, holds the automatically recorded events (requirement 5).
-
-    == Order lifecycle and filled/remaining consistency ==
-
-    === Data requirements description ===
-
-    '''Business rule.'''
-
-    * An order's status follows from how much of it has been filled: nothing filled → `open`, partly filled → `partially_filled`, completely filled → `executed`. The only status that is set explicitly is `cancelled`, and only on an order that is still active.
-    * `executed` and `cancelled` are final — such an order can never be processed again (no further fills, no cancellation, no changes).
-    * `filled_quantity` only grows, never exceeds `quantity`, and only changes when a trade fills the order.
-    * What was ordered — user, market, side, type, quantity, price, time of placement — never changes after placement.
-    * `executed_at` is set exactly when the order becomes `executed`.
-    * New orders are only accepted on active markets, need a positive price, and start unfilled (the one exception is importing an order that was completely executed in the past, used by the sample data).
-
-    '''Why it is non-trivial.''' The rule compares the ''old'' and the ''new'' version of a row (valid
-    transitions, "final" states, "only grows", immutable columns) and ties two columns together
-    (status ↔ filled quantity). A `CHECK` constraint only sees one version of one row. It also
-    depends on ''who'' changes `filled_quantity` — a trade may, a manual `UPDATE` may not.
-
-    '''PostgreSQL feature.''' `BEFORE INSERT OR UPDATE` row trigger on `orders`. The trade trigger
-    (requirement 2) sets a transaction-local setting (`set_config('eduberza.trade_fill', 'on', true)`)
-    while it fills an order; the lifecycle trigger only accepts a change of `filled_quantity` when
-    that setting is on.
-
-    '''Tables affected.''' `orders` (reads `markets` for the active check).
-
-    === Implementation ===
-
-    ==== Triggers ====
-
-    {{{
-    CREATE OR REPLACE FUNCTION project.trg_orders_lifecycle()
-    RETURNS trigger LANGUAGE plpgsql AS $$
-    DECLARE
-        v_derived varchar(20);
-    BEGIN
-        IF TG_OP = 'UPDATE' THEN
-            IF OLD.status IN ('executed', 'cancelled') THEN
-                RAISE EXCEPTION 'order % is already % and cannot be processed again', OLD.id, OLD.status
-                    USING ERRCODE = 'check_violation';
-            END IF;
-            IF (NEW.user_id, NEW.market_id, NEW.side, NEW.type, NEW.quantity, NEW.price, NEW.placed_at)
-            IS DISTINCT FROM
-            (OLD.user_id, OLD.market_id, OLD.side, OLD.type, OLD.quantity, OLD.price, OLD.placed_at) THEN
-                RAISE EXCEPTION 'user, market, side, type, quantity, price and placed_at of an order cannot change'
-                    USING ERRCODE = 'check_violation';
-            END IF;
-            IF NEW.filled_quantity <> OLD.filled_quantity THEN
-                IF current_setting('eduberza.trade_fill', true) IS DISTINCT FROM 'on' THEN
-                    RAISE EXCEPTION 'filled quantity of order % can only change through a trade', OLD.id
-                        USING ERRCODE = 'check_violation';
-                END IF;
-                IF NEW.filled_quantity < OLD.filled_quantity THEN
-                    RAISE EXCEPTION 'filled quantity of order % cannot decrease', OLD.id
-                        USING ERRCODE = 'check_violation';
-                END IF;
-            END IF;
-            IF NEW.status = 'cancelled' AND OLD.status <> 'cancelled' THEN
-                IF NEW.filled_quantity <> OLD.filled_quantity THEN
-                    RAISE EXCEPTION 'an order cannot be filled and cancelled in the same step'
-                        USING ERRCODE = 'check_violation';
-                END IF;
-                NEW.executed_at := NULL;
-                RETURN NEW;
-            END IF;
-        ELSE
-            IF NOT EXISTS (SELECT 1 FROM project.markets WHERE id = NEW.market_id AND is_active) THEN
-                RAISE EXCEPTION 'market % is not active; no new orders accepted', NEW.market_id
-                    USING ERRCODE = 'check_violation';
-            END IF;
-            IF NEW.price IS NULL OR NEW.price <= 0 THEN
-                RAISE EXCEPTION 'an order needs a positive price (limit price, or the market price for a market order)'
-                    USING ERRCODE = 'check_violation';
-            END IF;
-            IF NEW.status = 'cancelled' THEN
-                RAISE EXCEPTION 'an order cannot be created already cancelled'
-                    USING ERRCODE = 'check_violation';
-            END IF;
-            -- A new order starts unfilled. The only exception is importing an
-            -- order that was completely executed in the past (sample data).
-            IF NEW.filled_quantity NOT IN (0, NEW.quantity) THEN
-                RAISE EXCEPTION 'a new order is either unfilled or (imported history) completely filled'
-                    USING ERRCODE = 'check_violation';
-            END IF;
-        END IF;
-
-        v_derived := CASE
-            WHEN NEW.filled_quantity = 0               THEN 'open'
-            WHEN NEW.filled_quantity < NEW.quantity    THEN 'partially_filled'
-            ELSE 'executed'
-        END;
-        IF NEW.status IS DISTINCT FROM v_derived
-        AND (TG_OP = 'INSERT' OR NEW.status IS DISTINCT FROM OLD.status) THEN
-            RAISE EXCEPTION 'order status % does not match filled quantity % of %; status is derived automatically',
-                NEW.status, NEW.filled_quantity, NEW.quantity
-                USING ERRCODE = 'check_violation';
-        END IF;
-        NEW.status := v_derived;
-
-        IF v_derived = 'executed' THEN
-            NEW.executed_at := COALESCE(NEW.executed_at, now());
-        ELSE
-            NEW.executed_at := NULL;
-        END IF;
-        RETURN NEW;
-    END $$;
-
-    CREATE TRIGGER orders_lifecycle
-        BEFORE INSERT OR UPDATE ON project.orders
-        FOR EACH ROW EXECUTE FUNCTION project.trg_orders_lifecycle();
-    }}}
-
-    Because the status is derived, the application never sets `open`, `partially_filled` or
-    `executed` itself — it records trades, and the status follows.
-
-    == Trade consistency ==
-
-    === Data requirements description ===
-
-    '''Business rule.''' A trade that fills orders must be possible for those orders:
-
-    * the buy side is a buy order and the sell side is a sell order;
-    * both are on the trade's market and still active — a cancelled or executed order can never trade again;
-    * the trade quantity does not exceed the remaining quantity of either order;
-    * the price respects both limits: at most the buy order's price, at least the sell order's price;
-    * the two orders belong to different users (no self-trade).
-
-    Recording the trade must fill both orders by exactly the traded quantity (and so move their
-    status), and move the money and crypto for both users, all together. A trade that filled orders
-    is history and can't be changed or deleted afterwards — the fills and the money moved would no
-    longer match it.
-
-    '''Why it is non-trivial.''' One trade row has to be checked against two other rows of another
-    table (the orders), including their current remaining quantity, and a single insert must cause
-    consistent changes in five tables (`market_trades`, both `orders`, both users' `users` and
-    `holdings` rows, two `transactions` rows).
-
-    '''PostgreSQL features.'''
-
-    * `BEFORE INSERT` trigger `market_trades_validate` — the compatibility rules. It locks both orders (`FOR UPDATE`), so two concurrent trades can't both take the same remaining quantity.
-    * `AFTER INSERT` trigger `market_trades_fill` — raises `filled_quantity` on both orders (automatic status change through requirement 1).
-    * `BEFORE UPDATE OR DELETE` trigger `market_trades_immutable`.
-    * Stored function `execute_trade` — the settlement of money and crypto for one trade.
-
-    Because the rules sit on `market_trades` itself, even a trade inserted by hand is validated and
-    fills the orders.
-
-    '''Tables affected.''' `market_trades`, `orders`, `users`, `holdings`, `transactions`.
-
-    === Implementation ===
-
-    ==== Triggers ====
-
-    {{{
-    CREATE OR REPLACE FUNCTION project.trg_market_trades_validate()
-    RETURNS trigger LANGUAGE plpgsql AS $$
-    DECLARE
-        o     project.orders%ROWTYPE;
-        v_uid uuid;
-        v_id  uuid;
-        v_role varchar(4);
-    BEGIN
-        IF NEW.buy_order_id IS NULL AND NEW.sell_order_id IS NULL THEN
-            RETURN NEW;
-        END IF;
-        IF NEW.buy_order_id IS NOT DISTINCT FROM NEW.sell_order_id THEN
-            RAISE EXCEPTION 'an order cannot trade with itself'
-                USING ERRCODE = 'check_violation';
-        END IF;
-
-        FOREACH v_role IN ARRAY ARRAY['buy', 'sell'] LOOP
-            v_id := CASE v_role WHEN 'buy' THEN NEW.buy_order_id ELSE NEW.sell_order_id END;
-            CONTINUE WHEN v_id IS NULL;
-
-            SELECT * INTO o FROM project.orders WHERE id = v_id FOR UPDATE;
-            IF o.side <> v_role THEN
-                RAISE EXCEPTION 'order % is a % order and cannot be the % side of a trade', v_id, o.side, v_role
-                    USING ERRCODE = 'check_violation';
-            END IF;
-            IF o.market_id <> NEW.market_id THEN
-                RAISE EXCEPTION 'order % is on a different market than the trade', v_id
-                    USING ERRCODE = 'check_violation';
-            END IF;
-            IF o.status NOT IN ('open', 'partially_filled') THEN
-                RAISE EXCEPTION 'order % is % and cannot trade', v_id, o.status
-                    USING ERRCODE = 'check_violation';
-            END IF;
-            IF NEW.quantity > o.quantity - o.filled_quantity THEN
-                RAISE EXCEPTION 'trade quantity % exceeds the remaining quantity % of order %',
-                    NEW.quantity, o.quantity - o.filled_quantity, v_id
-                    USING ERRCODE = 'check_violation';
-            END IF;
-            IF v_uid IS NOT NULL AND v_uid = o.user_id THEN
-                RAISE EXCEPTION 'a user cannot trade with their own order'
-                    USING ERRCODE = 'check_violation';
-            END IF;
-            IF v_role = 'buy' AND NEW.price > o.price OR v_role = 'sell' AND NEW.price < o.price THEN
-                RAISE EXCEPTION 'trade price % is outside the limit % of % order %', NEW.price, o.price, v_role, v_id
-                    USING ERRCODE = 'check_violation';
-            END IF;
-            v_uid := o.user_id;
-        END LOOP;
-        RETURN NEW;
-    END $$;
-
-    CREATE TRIGGER market_trades_validate
-        BEFORE INSERT ON project.market_trades
-        FOR EACH ROW EXECUTE FUNCTION project.trg_market_trades_validate();
-    }}}
-
-    {{{
-    CREATE OR REPLACE FUNCTION project.trg_market_trades_fill()
-    RETURNS trigger LANGUAGE plpgsql AS $$
-    BEGIN
-        IF NEW.buy_order_id IS NULL AND NEW.sell_order_id IS NULL THEN
-            RETURN NULL;
-        END IF;
-        PERFORM set_config('eduberza.trade_fill', 'on', true);
-        PERFORM set_config('eduberza.trade_price', NEW.price::text, true);
-        UPDATE project.orders
-        SET filled_quantity = filled_quantity + NEW.quantity,
-            executed_at     = CASE WHEN filled_quantity + NEW.quantity = quantity
-                                    THEN NEW.executed_at END
-        WHERE id IN (NEW.buy_order_id, NEW.sell_order_id);
-        PERFORM set_config('eduberza.trade_fill', 'off', true);
-        RETURN NULL;
-    END $$;
-
-    CREATE TRIGGER market_trades_fill
-        AFTER INSERT ON project.market_trades
-        FOR EACH ROW EXECUTE FUNCTION project.trg_market_trades_fill();
-    }}}
-
-    {{{
-    CREATE OR REPLACE FUNCTION project.trg_market_trades_immutable()
-    RETURNS trigger LANGUAGE plpgsql AS $$
-    BEGIN
-        IF OLD.buy_order_id IS NULL AND OLD.sell_order_id IS NULL THEN
-            -- a simulated tick filled no order; allow it unless it is being
-            -- turned into one that did
-            IF TG_OP = 'DELETE' THEN
-                RETURN OLD;
-            ELSIF NEW.buy_order_id IS NULL AND NEW.sell_order_id IS NULL THEN
-                RETURN NEW;
-            END IF;
-        END IF;
-        RAISE EXCEPTION 'a trade that filled orders cannot be changed or deleted'
-            USING ERRCODE = 'check_violation';
-    END $$;
-
-    CREATE TRIGGER market_trades_immutable
-        BEFORE UPDATE OR DELETE ON project.market_trades
-        FOR EACH ROW EXECUTE FUNCTION project.trg_market_trades_immutable();
-    }}}
-
-    ==== Stored procedures/functions ====
-
-    `execute_trade(buy_order, sell_order, quantity, price)` — one trade. Either order may be `NULL`
-    when the simulated market is the counterparty. The `INSERT` into `market_trades` validates the
-    trade and fills the orders (triggers above); the function then settles both users:
-
-    * '''Buyer:''' the reservation for the filled part is released. The buyer pays the actual cost (trade price × quantity); if the trade price is below the order's limit, the difference goes back to `available_balance`. The crypto is added to the holding at a running weighted average price, and a `buy` ledger row is written.
-    * '''Seller:''' the reserved crypto is delivered out of the holding, the proceeds are credited, the cost basis is removed from `invested_balance`, and a `sell` ledger row is written.
-
-    Both orders are locked in a fixed order (by id), so two trades on the same pair of orders can't
-    deadlock.
-
-    {{{
-    CREATE OR REPLACE FUNCTION project.execute_trade(
-        p_buy_order uuid, p_sell_order uuid, p_quantity numeric, p_price numeric,
-        p_aggressor varchar DEFAULT NULL)
-    RETURNS bigint LANGUAGE plpgsql AS $$
-    DECLARE
-        b          project.orders%ROWTYPE;
-        s          project.orders%ROWTYPE;
-        v_market   project.markets%ROWTYPE;
-        v_trade_id bigint;
-        v_release  numeric;
-        v_cost     numeric;
-        v_proceeds numeric;
-        v_avg      numeric;
-    BEGIN
-        IF p_buy_order IS NULL AND p_sell_order IS NULL THEN
-            RAISE EXCEPTION 'a trade needs at least one order' USING ERRCODE = 'check_violation';
-        END IF;
-
-        -- lock both orders in a fixed order (by id) so two concurrent trades on
-        -- the same pair of orders cannot deadlock
-        PERFORM 1 FROM project.orders WHERE id IN (p_buy_order, p_sell_order) ORDER BY id FOR UPDATE;
-        SELECT * INTO b FROM project.orders WHERE id = p_buy_order;
-        SELECT * INTO s FROM project.orders WHERE id = p_sell_order;
-        SELECT * INTO v_market FROM project.markets WHERE id = COALESCE(b.market_id, s.market_id);
-
-        INSERT INTO project.market_trades
-            (market_id, executed_at, price, quantity, side, source, buy_order_id, sell_order_id)
-        VALUES (v_market.id, now(), p_price, p_quantity,
-                COALESCE(p_aggressor, CASE WHEN p_sell_order IS NULL THEN 'buy' ELSE 'sell' END),
-                CASE WHEN p_buy_order IS NOT NULL AND p_sell_order IS NOT NULL THEN 'match' ELSE 'market' END,
-                p_buy_order, p_sell_order)
-        RETURNING id INTO v_trade_id;
-
-        IF p_buy_order IS NOT NULL THEN
-            v_release := project.order_reservation(b.quantity - b.filled_quantity, b.price)
-                    - project.order_reservation(b.quantity - b.filled_quantity - p_quantity, b.price);
-            v_cost    := LEAST(round(p_quantity * p_price, 4), v_release);
-
-            UPDATE project.users
-            SET reserved_balance  = reserved_balance  - v_release,
-                available_balance = available_balance + (v_release - v_cost),
-                invested_balance  = invested_balance  + v_cost,
-                updated_at        = now()
-            WHERE id = b.user_id;
-
-            INSERT INTO project.holdings AS h (user_id, crypto_id, quantity, avg_price, updated_at)
-            VALUES (b.user_id, v_market.crypto_id, p_quantity, p_price, now())
-            ON CONFLICT (user_id, crypto_id) DO UPDATE
-            SET avg_price  = (h.quantity * h.avg_price + EXCLUDED.quantity * EXCLUDED.avg_price)
-                                / (h.quantity + EXCLUDED.quantity),
-                quantity   = h.quantity + EXCLUDED.quantity,
-                updated_at = now();
-
-            INSERT INTO project.transactions (user_id, type, amount, currency, related_order, description)
-            VALUES (b.user_id, 'buy', -v_cost, v_market.quote_currency, b.id,
-                    format('Buy %s @ %s (trade %s)', p_quantity, p_price, v_trade_id));
-        END IF;
-
-        IF p_sell_order IS NOT NULL THEN
-            SELECT avg_price INTO v_avg FROM project.holdings
-            WHERE user_id = s.user_id AND crypto_id = v_market.crypto_id FOR UPDATE;
-
-            UPDATE project.holdings
-            SET quantity          = quantity - p_quantity,
-                reserved_quantity = reserved_quantity - p_quantity,
-                updated_at        = now()
-            WHERE user_id = s.user_id AND crypto_id = v_market.crypto_id;
-
-            v_proceeds := round(p_quantity * p_price, 4);
-            UPDATE project.users
-            SET available_balance = available_balance + v_proceeds,
-                invested_balance  = GREATEST(invested_balance - round(p_quantity * v_avg, 4), 0),
-                updated_at        = now()
-            WHERE id = s.user_id;
-
-            INSERT INTO project.transactions (user_id, type, amount, currency, related_order, description)
-            VALUES (s.user_id, 'sell', v_proceeds, v_market.quote_currency, s.id,
-                    format('Sell %s @ %s (trade %s)', p_quantity, p_price, v_trade_id));
-        END IF;
-
-        RETURN v_trade_id;
-    END $$;
-    }}}
-
-    == Balance and reservation consistency ==
-
-    === Data requirements description ===
-
-    '''Business rule.'''
-
-    1. '''Reserved cash matches active buy orders.''' A user's `reserved_balance` always equals what their active buy orders still reserve: remaining quantity × order price, summed.
-    2. '''Reserved crypto matches active sell orders.''' A user's `holdings.reserved_quantity` for a crypto always equals the remaining quantity of their active sell orders for it.
-    3. '''Cash matches the ledger.''' `available_balance + reserved_balance` always equals the sum of the user's ledger (`transactions`). Reserving only moves cash between the two columns; money actually arrives or leaves only with a ledger row (deposit, buy fill, sell fill).
-
-    Together these make it impossible for an order operation to leave a balance in an inconsistent
-    state: money or crypto reserved for nothing, an order that is not backed by a reservation (and
-    so could spend the same money twice), or cash that appeared or vanished without a ledger entry.
-
-    '''Why it is non-trivial.''' Each rule is an equality between a column and an aggregate over
-    ''other'' rows of ''other'' tables. Worse, every legitimate operation breaks it for a moment:
-    placing a buy moves cash to `reserved_balance` in one statement and inserts the order in the
-    next; a trade releases the reservation, pays, and writes the ledger in several statements. Only
-    the state at the end of the transaction has to be consistent.
-
-    '''PostgreSQL feature.''' `CONSTRAINT TRIGGER … DEFERRABLE INITIALLY DEFERRED` — row triggers
-    whose check runs at `COMMIT`, on the final state. A transaction that leaves any of the three
-    equalities broken fails at `COMMIT` and is rolled back as a whole. Each rule has a trigger on
-    every table whose change can break it. `order_reservation(remaining, price)` is the one place
-    that defines how a reservation is rounded, so placing, filling, cancelling and checking always
-    agree to the last decimal.
-
-    '''Tables affected.''' `users`, `holdings`, `orders`, `transactions`.
-
-    === Implementation ===
-
-    ==== Stored procedures/functions ====
-
-    {{{
-    CREATE OR REPLACE FUNCTION project.order_reservation(p_remaining numeric, p_price numeric)
-    RETURNS numeric LANGUAGE sql IMMUTABLE AS $$
-        SELECT round(p_remaining * p_price, 4)
-    $$;
-    }}}
-
-    ==== Triggers ====
-
-    {{{
-    CREATE OR REPLACE FUNCTION project.trg_reserved_cash_matches_orders()
-    RETURNS trigger LANGUAGE plpgsql AS $$
-    DECLARE
-        v_user     uuid := CASE WHEN TG_TABLE_NAME = 'users' THEN NEW.id END;
-        v_reserved numeric;
-        v_needed   numeric;
-    BEGIN
-        IF TG_TABLE_NAME = 'orders' THEN
-            v_user := NEW.user_id;
-        END IF;
-        SELECT reserved_balance INTO v_reserved FROM project.users WHERE id = v_user;
-        IF NOT FOUND THEN
-            RETURN NULL;
-        END IF;
-        SELECT COALESCE(SUM(project.order_reservation(quantity - filled_quantity, price)), 0)
-        INTO v_needed
-        FROM project.orders
-        WHERE user_id = v_user AND side = 'buy' AND status IN ('open', 'partially_filled');
-        IF v_reserved <> v_needed THEN
-            RAISE EXCEPTION 'reserved balance % does not match the % needed by active buy orders (user %)',
-                v_reserved, v_needed, v_user
-                USING ERRCODE = 'check_violation', CONSTRAINT = 'reserved_cash_matches_orders';
-        END IF;
-        RETURN NULL;
-    END $$;
-    }}}
-
-    {{{
-    CREATE OR REPLACE FUNCTION project.trg_reserved_crypto_matches_orders()
-    RETURNS trigger LANGUAGE plpgsql AS $$
-    DECLARE
-        v_user     uuid;
-        v_crypto   uuid;
-        v_reserved numeric;
-        v_needed   numeric;
-    BEGIN
-        IF TG_TABLE_NAME = 'holdings' THEN
-            v_user := NEW.user_id;
-            v_crypto := NEW.crypto_id;
-        ELSE
-            IF NEW.side <> 'sell' THEN
-                RETURN NULL;
-            END IF;
-            v_user := NEW.user_id;
-            SELECT crypto_id INTO v_crypto FROM project.markets WHERE id = NEW.market_id;
-        END IF;
-        SELECT COALESCE(SUM(reserved_quantity), 0) INTO v_reserved
-        FROM project.holdings WHERE user_id = v_user AND crypto_id = v_crypto;
-        SELECT COALESCE(SUM(o.quantity - o.filled_quantity), 0) INTO v_needed
-        FROM project.orders o
-        JOIN project.markets m ON m.id = o.market_id
-        WHERE o.user_id = v_user AND m.crypto_id = v_crypto
-        AND o.side = 'sell' AND o.status IN ('open', 'partially_filled');
-        IF v_reserved <> v_needed THEN
-            RAISE EXCEPTION 'reserved quantity % does not match the % needed by active sell orders (user %, crypto %)',
-                v_reserved, v_needed, v_user, v_crypto
-                USING ERRCODE = 'check_violation', CONSTRAINT = 'reserved_crypto_matches_orders';
-        END IF;
-        RETURN NULL;
-    END $$;
-    }}}
-
-    {{{
-    CREATE OR REPLACE FUNCTION project.trg_cash_matches_ledger()
-    RETURNS trigger LANGUAGE plpgsql AS $$
-    DECLARE
-        v_user   uuid;
-        v_cash   numeric;
-        v_ledger numeric;
-    BEGIN
-        IF TG_TABLE_NAME = 'users' THEN
-            v_user := NEW.id;
-        ELSIF TG_OP = 'DELETE' THEN
-            v_user := OLD.user_id;
-        ELSE
-            v_user := NEW.user_id;
-        END IF;
-        SELECT available_balance + reserved_balance INTO v_cash FROM project.users WHERE id = v_user;
-        IF NOT FOUND THEN
-            RETURN NULL;
-        END IF;
-        SELECT COALESCE(SUM(amount), 0) INTO v_ledger FROM project.transactions WHERE user_id = v_user;
-        IF v_cash <> v_ledger THEN
-            RAISE EXCEPTION 'cash % (available + reserved) does not match the ledger total % (user %)',
-                v_cash, v_ledger, v_user
-                USING ERRCODE = 'check_violation', CONSTRAINT = 'cash_matches_ledger';
-        END IF;
-        RETURN NULL;
-    END $$;
-    }}}
-
-    {{{
-    CREATE INDEX idx_orders_active ON project.orders (user_id, side)
-        WHERE status IN ('open', 'partially_filled');
-
-    CREATE CONSTRAINT TRIGGER reserved_cash_matches_orders
-        AFTER INSERT OR UPDATE OF reserved_balance ON project.users
-        DEFERRABLE INITIALLY DEFERRED
-        FOR EACH ROW EXECUTE FUNCTION project.trg_reserved_cash_matches_orders();
-
-    CREATE CONSTRAINT TRIGGER reserved_cash_matches_orders
-        AFTER INSERT OR UPDATE OF status, filled_quantity ON project.orders
-        DEFERRABLE INITIALLY DEFERRED
-        FOR EACH ROW EXECUTE FUNCTION project.trg_reserved_cash_matches_orders();
-
-    CREATE CONSTRAINT TRIGGER reserved_crypto_matches_orders
-        AFTER INSERT OR UPDATE OF reserved_quantity ON project.holdings
-        DEFERRABLE INITIALLY DEFERRED
-        FOR EACH ROW EXECUTE FUNCTION project.trg_reserved_crypto_matches_orders();
-
-    CREATE CONSTRAINT TRIGGER reserved_crypto_matches_orders
-        AFTER INSERT OR UPDATE OF status, filled_quantity ON project.orders
-        DEFERRABLE INITIALLY DEFERRED
-        FOR EACH ROW EXECUTE FUNCTION project.trg_reserved_crypto_matches_orders();
-
-    CREATE CONSTRAINT TRIGGER cash_matches_ledger
-        AFTER INSERT OR UPDATE OF available_balance, reserved_balance ON project.users
-        DEFERRABLE INITIALLY DEFERRED
-        FOR EACH ROW EXECUTE FUNCTION project.trg_cash_matches_ledger();
-
-    CREATE CONSTRAINT TRIGGER cash_matches_ledger
-        AFTER INSERT OR UPDATE OR DELETE ON project.transactions
-        DEFERRABLE INITIALLY DEFERRED
-        FOR EACH ROW EXECUTE FUNCTION project.trg_cash_matches_ledger();
-    }}}
-
-    The partial index `idx_orders_active` covers exactly the rows the reservation checks sum, and
-    stays small because active orders are few compared with the whole order history.
-
-    == Placing and cancelling orders ==
-
-    === Data requirements description ===
-
-    '''Business rule.''' Placing an order must, as one unit:
-
-    1. check the market, side, type, quantity and price;
-    2. reserve what the order commits — cash (quantity × price) for a buy, crypto for a sell — and refuse the order if not enough is free;
-    3. record the order;
-    4. match it against the order book: other users' active limit orders on the same market whose price is acceptable, best price first and oldest first (price–time priority), each trade at the resting order's price;
-    5. fill whatever is still unfilled from the simulated market if it is marketable at the current market price.
-
-    A '''market order''' is priced at the current market price, so it always fills completely in step
-    4 or 5 and never waits. A '''limit order''' that isn't marketable stays in the order book.
-    Cancelling an order must release exactly what it still reserves, and only for an active order of
-    the caller.
-
-    '''Why it is non-trivial.''' It is a multi-step operation over five tables whose steps depend on
-    each other (how much is left after each match, what to release), and it has to be correct under
-    concurrency: two orders of the same user must not both see the same free cash.
-
-    '''PostgreSQL feature.''' Stored functions (PL/pgSQL). `place_order` locks the user's row
-    (`SELECT … FOR UPDATE`) before checking free cash or crypto, so concurrent orders of the same
-    user are serialised. Every step's consistency is still checked by the triggers of requirements
-    1–3.
-
-    '''Tables affected.''' `orders`, `users`, `holdings`, `market_trades`, `transactions`.
-
-    === Implementation ===
-
-    ==== Stored procedures/functions ====
-
-    {{{
-    CREATE OR REPLACE FUNCTION project.latest_price(p_market_id uuid)
-    RETURNS numeric LANGUAGE sql STABLE AS $$
-        SELECT price FROM project.market_trades
-        WHERE market_id = p_market_id
-        ORDER BY executed_at DESC, id DESC
-        LIMIT 1
-    $$;
-    }}}
-
-    {{{
-    CREATE OR REPLACE FUNCTION project.match_order(p_order_id uuid)
-    RETURNS int LANGUAGE plpgsql AS $$
-    DECLARE
-        o       project.orders%ROWTYPE;
-        r       record;
-        v_rem   numeric;
-        v_qty   numeric;
-        v_count int := 0;
-    BEGIN
-        SELECT * INTO o FROM project.orders WHERE id = p_order_id FOR UPDATE;
-        FOR r IN
-            SELECT id, price, quantity - filled_quantity AS remaining
-            FROM project.orders
-            WHERE market_id = o.market_id
-            AND side <> o.side
-            AND type = 'limit'
-            AND status IN ('open', 'partially_filled')
-            AND user_id <> o.user_id
-            AND (o.side = 'buy'  AND price <= o.price
-                OR o.side = 'sell' AND price >= o.price)
-            ORDER BY CASE WHEN o.side = 'buy'  THEN price END ASC,
-                    CASE WHEN o.side = 'sell' THEN price END DESC,
-                    placed_at, id
-            FOR UPDATE
-        LOOP
-            SELECT quantity - filled_quantity INTO v_rem FROM project.orders WHERE id = p_order_id;
-            EXIT WHEN v_rem = 0;
-            v_qty := LEAST(v_rem, r.remaining);
-            IF o.side = 'buy' THEN
-                PERFORM project.execute_trade(o.id, r.id, v_qty, r.price, 'buy');
-            ELSE
-                PERFORM project.execute_trade(r.id, o.id, v_qty, r.price, 'sell');
-            END IF;
-            v_count := v_count + 1;
-        END LOOP;
-        RETURN v_count;
-    END $$;
-    }}}
-
-    {{{
-    CREATE OR REPLACE FUNCTION project.place_order(
-        p_user_id uuid, p_market_id uuid, p_side varchar, p_type varchar,
-        p_quantity numeric, p_limit_price numeric DEFAULT NULL)
-    RETURNS uuid LANGUAGE plpgsql AS $$
-    DECLARE
-        v_market    numeric := project.latest_price(p_market_id);
-        v_price     numeric;
-        v_available numeric;
-        v_free      numeric;
-        v_crypto    uuid;
-        v_order     uuid;
-        v_rem       numeric;
-    BEGIN
-        IF p_side NOT IN ('buy', 'sell') OR p_type NOT IN ('market', 'limit') THEN
-            RAISE EXCEPTION 'invalid side % or type %', p_side, p_type USING ERRCODE = 'check_violation';
-        END IF;
-        IF p_quantity IS NULL OR p_quantity <= 0 THEN
-            RAISE EXCEPTION 'quantity must be positive' USING ERRCODE = 'check_violation';
-        END IF;
-        IF p_type = 'limit' THEN
-            IF p_limit_price IS NULL OR p_limit_price <= 0 THEN
-                RAISE EXCEPTION 'a limit order needs a positive limit price' USING ERRCODE = 'check_violation';
-            END IF;
-            v_price := p_limit_price;
-        ELSE
-            IF v_market IS NULL THEN
-                RAISE EXCEPTION 'market has no price yet' USING ERRCODE = 'check_violation';
-            END IF;
-            v_price := v_market;
-        END IF;
-
-        SELECT available_balance INTO v_available FROM project.users WHERE id = p_user_id FOR UPDATE;
-        IF NOT FOUND THEN
-            RAISE EXCEPTION 'user % does not exist', p_user_id USING ERRCODE = 'no_data_found';
-        END IF;
-
-        IF p_side = 'buy' THEN
-            IF v_available < project.order_reservation(p_quantity, v_price) THEN
-                RAISE EXCEPTION 'insufficient funds: the order needs %, available %',
-                    project.order_reservation(p_quantity, v_price), v_available
-                    USING ERRCODE = 'check_violation';
-            END IF;
-            UPDATE project.users
-            SET available_balance = available_balance - project.order_reservation(p_quantity, v_price),
-                reserved_balance  = reserved_balance  + project.order_reservation(p_quantity, v_price),
-                updated_at        = now()
-            WHERE id = p_user_id;
-        ELSE
-            SELECT crypto_id INTO v_crypto FROM project.markets WHERE id = p_market_id;
-            SELECT quantity - reserved_quantity INTO v_free FROM project.holdings
-            WHERE user_id = p_user_id AND crypto_id = v_crypto FOR UPDATE;
-            IF COALESCE(v_free, 0) < p_quantity THEN
-                RAISE EXCEPTION 'insufficient holding: trying to sell %, free to sell %', p_quantity, COALESCE(v_free, 0)
-                    USING ERRCODE = 'check_violation';
-            END IF;
-            UPDATE project.holdings
-            SET reserved_quantity = reserved_quantity + p_quantity, updated_at = now()
-            WHERE user_id = p_user_id AND crypto_id = v_crypto;
-        END IF;
-
-        INSERT INTO project.orders (user_id, market_id, side, type, status, quantity, price)
-        VALUES (p_user_id, p_market_id, p_side, p_type, 'open', p_quantity, v_price)
-        RETURNING id INTO v_order;
-
-        PERFORM project.match_order(v_order);
-
-        SELECT quantity - filled_quantity INTO v_rem FROM project.orders WHERE id = v_order;
-        IF v_rem > 0 AND v_market IS NOT NULL
-        AND (p_side = 'buy' AND v_market <= v_price OR p_side = 'sell' AND v_market >= v_price) THEN
-            IF p_side = 'buy' THEN
-                PERFORM project.execute_trade(v_order, NULL, v_rem, v_market, 'buy');
-            ELSE
-                PERFORM project.execute_trade(NULL, v_order, v_rem, v_market, 'sell');
-            END IF;
-        END IF;
-        RETURN v_order;
-    END $$;
-    }}}
-
-    {{{
-    CREATE OR REPLACE FUNCTION project.cancel_order(p_order_id uuid, p_user_id uuid DEFAULT NULL)
-    RETURNS void LANGUAGE plpgsql AS $$
-    DECLARE
-        o     project.orders%ROWTYPE;
-        v_rem numeric;
-    BEGIN
-        SELECT * INTO o FROM project.orders WHERE id = p_order_id FOR UPDATE;
-        IF NOT FOUND THEN
-            RAISE EXCEPTION 'order % does not exist', p_order_id USING ERRCODE = 'no_data_found';
-        END IF;
-        IF p_user_id IS NOT NULL AND o.user_id <> p_user_id THEN
-            RAISE EXCEPTION 'order % does not belong to this user', p_order_id
-                USING ERRCODE = 'insufficient_privilege';
-        END IF;
-        IF o.status NOT IN ('open', 'partially_filled') THEN
-            RAISE EXCEPTION 'order % is % and cannot be cancelled', p_order_id, o.status
-                USING ERRCODE = 'check_violation';
-        END IF;
-
-        v_rem := o.quantity - o.filled_quantity;
-        IF o.side = 'buy' THEN
-            UPDATE project.users
-            SET reserved_balance  = reserved_balance  - project.order_reservation(v_rem, o.price),
-                available_balance = available_balance + project.order_reservation(v_rem, o.price),
-                updated_at        = now()
-            WHERE id = o.user_id;
-        ELSE
-            UPDATE project.holdings h
-            SET reserved_quantity = h.reserved_quantity - v_rem, updated_at = now()
-            FROM project.markets m
-            WHERE m.id = o.market_id AND h.user_id = o.user_id AND h.crypto_id = m.crypto_id;
-        END IF;
-
-        UPDATE project.orders SET status = 'cancelled' WHERE id = p_order_id;
-    END $$;
-    }}}
-
-    In the prototype, placing an order is now a single call:
-
-    {{{
-    err = db.DB.QueryRow(
-        `SELECT place_order($1, $2, $3, $4, $5, $6)`,
-        s.UserID, m.ID, side, orderType, qty, limit.value(),
-    ).Scan(&orderID)
-    }}}
-
-    Cancelling is `SELECT cancel_order($1, $2)` with the order the user picked from a numbered list
-    of their open orders (`server/trade.go`).
-
-    == Automatic recording of order events ==
-
-    === Data requirements description ===
-
-    '''Business rule.''' Every important thing that happens to an order is recorded with its time:
-    placement, each fill (with the quantity filled and the trade price), and cancellation. This is
-    the order's audit trail — the history of ''how'' it reached its current state, which the order row
-    alone (only the current state) cannot show.
-
-    '''Why it is non-trivial.''' Events come from several places — `place_order`, trades made by
-    `match_order`, trades made by the background job, `cancel_order`, and any direct SQL. Recording
-    them in each of those places would miss some; recording them where the change actually happens
-    cannot.
-
-    '''PostgreSQL feature.''' `AFTER INSERT OR UPDATE` row trigger on `orders`, writing into the new
-    table `order_events`. The fill price is handed over by the trade trigger through a
-    transaction-local setting.
-
-    '''Tables affected.''' `order_events` (new), written from changes to `orders`.
-
-    === Implementation ===
-
-    {{{
-    CREATE TABLE project.order_events (
-        id           bigserial      PRIMARY KEY,
-        order_id     uuid           NOT NULL REFERENCES project.orders(id) ON DELETE CASCADE,
-        event_type   varchar(20)    NOT NULL
-                    CHECK (event_type IN ('placed', 'partially_filled', 'filled', 'cancelled')),
-        quantity     numeric(20,4)  NOT NULL,
-        price        numeric(18,6),
-        status_after varchar(20)    NOT NULL,
-        created_at   timestamptz    NOT NULL DEFAULT clock_timestamp()
-    );
-    }}}
-
-    ==== Triggers ====
-
-    {{{
-    CREATE OR REPLACE FUNCTION project.trg_orders_events()
-    RETURNS trigger LANGUAGE plpgsql AS $$
-    BEGIN
-        IF TG_OP = 'INSERT' THEN
-            INSERT INTO project.order_events (order_id, event_type, quantity, price, status_after)
-            VALUES (NEW.id, 'placed', NEW.quantity, NEW.price, NEW.status);
-        ELSIF NEW.filled_quantity > OLD.filled_quantity THEN
-            INSERT INTO project.order_events (order_id, event_type, quantity, price, status_after)
-            VALUES (NEW.id,
-                    CASE WHEN NEW.status = 'executed' THEN 'filled' ELSE 'partially_filled' END,
-                    NEW.filled_quantity - OLD.filled_quantity,
-                    current_setting('eduberza.trade_price', true)::numeric,
-                    NEW.status);
-        ELSIF NEW.status = 'cancelled' AND OLD.status <> 'cancelled' THEN
-            INSERT INTO project.order_events (order_id, event_type, quantity, price, status_after)
-            VALUES (NEW.id, 'cancelled', NEW.quantity - NEW.filled_quantity, NEW.price, NEW.status);
-        END IF;
-        RETURN NULL;
-    END $$;
-
-    CREATE TRIGGER orders_events
-        AFTER INSERT OR UPDATE ON project.orders
-        FOR EACH ROW EXECUTE FUNCTION project.trg_orders_events();
-    }}}
-
-    == Views for derived trading data ==
-
-    === Data requirements description ===
-
-    The application needs several things that are ''derived'' from orders, trades and balances. They
-    are defined once as views, instead of repeating the calculations in the application code:
-
-    ||= View =||= Derived data =||= Used by =||
-    || `v_active_orders` || active orders with remaining quantity and what each one holds in reserve || CLI `[13] My open orders`, `[14] Cancel an order` ||
-    || `v_order_book` || current order book: resting limit orders aggregated per market, side and price level || CLI `[12] Order book`, and when placing an order ||
-    || `v_order_history` || every order with its fill progress, number of trades and average fill price (from its trades) || CLI result of placing an order ||
-    || `v_trader_balances` || cash split into available and reserved, the ledger total it must equal, holdings at market value, net worth || CLI `[1] View balance` ||
-
-    None of these repeats a P6 report: P6 aggregates performance over a period, while these show
-    the current state of the order book and accounts.
-
-    === Implementation ===
-
-    ==== Views ====
-
-    {{{
-    CREATE VIEW project.v_active_orders AS
-    SELECT o.id            AS order_id,
-        o.user_id,
-        u.username,
-        o.market_id,
-        c.symbol,
-        m.quote_currency,
-        o.side,
-        o.type,
-        o.status,
-        o.quantity,
-        o.filled_quantity,
-        o.quantity - o.filled_quantity AS remaining,
-        o.price,
-        CASE WHEN o.side = 'buy'
-                THEN project.order_reservation(o.quantity - o.filled_quantity, o.price) ELSE 0 END AS reserved_cash,
-        CASE WHEN o.side = 'sell' THEN o.quantity - o.filled_quantity ELSE 0 END             AS reserved_crypto,
-        o.placed_at
-    FROM project.orders o
-    JOIN project.users   u ON u.id = o.user_id
-    JOIN project.markets m ON m.id = o.market_id
-    JOIN project.crypto  c ON c.id = m.crypto_id
-    WHERE o.status IN ('open', 'partially_filled');
-    }}}
-
-    {{{
-    CREATE VIEW project.v_order_book AS
-    SELECT market_id,
-        symbol,
-        quote_currency,
-        side,
-        price,
-        SUM(remaining) AS quantity,
-        COUNT(*)       AS orders
-    FROM project.v_active_orders
-    WHERE type = 'limit'
-    GROUP BY market_id, symbol, quote_currency, side, price;
-    }}}
-
-    {{{
-    CREATE VIEW project.v_order_history AS
-    SELECT o.id            AS order_id,
-        o.user_id,
-        u.username,
-        c.symbol,
-        o.side,
-        o.type,
-        o.status,
-        o.quantity,
-        o.filled_quantity,
-        o.quantity - o.filled_quantity AS remaining,
-        o.price,
-        f.trades,
-        f.avg_fill_price,
-        o.placed_at,
-        o.executed_at
-    FROM project.orders o
-    JOIN project.users   u ON u.id = o.user_id
-    JOIN project.markets m ON m.id = o.market_id
-    JOIN project.crypto  c ON c.id = m.crypto_id
-    LEFT JOIN LATERAL (
-        SELECT COUNT(*) AS trades,
-            round(SUM(t.quantity * t.price) / NULLIF(SUM(t.quantity), 0), 6) AS avg_fill_price
-        FROM (SELECT quantity, price FROM project.market_trades WHERE buy_order_id  = o.id
-                UNION ALL
-                SELECT quantity, price FROM project.market_trades WHERE sell_order_id = o.id) t
-    ) f ON true;
-    }}}
-
-    {{{
-    CREATE VIEW project.v_trader_balances AS
-    SELECT u.id                         AS user_id,
-        u.username,
-        u.available_balance,
-        u.reserved_balance,
-        u.available_balance + u.reserved_balance              AS total_cash,
-        COALESCE(l.ledger_total, 0)                           AS ledger_total,
-        u.invested_balance,
-        COALESCE(p.holdings_value, 0)                         AS holdings_value,
-        u.available_balance + u.reserved_balance + COALESCE(p.holdings_value, 0) AS net_worth
-    FROM project.users u
-    LEFT JOIN (SELECT user_id, SUM(amount) AS ledger_total
-                FROM project.transactions GROUP BY user_id) l ON l.user_id = u.id
-    LEFT JOIN (SELECT user_id, SUM(market_value) AS holdings_value
-                FROM project.v_portfolio GROUP BY user_id) p ON p.user_id = u.id;
-    }}}
-
-    == Background job: filling resting limit orders ==
-
-    === Data requirements description ===
-
-    '''Business rule.''' In !EduBerza the market price is moved by the simulator (the market bot), not
-    by users' orders. A limit order that rests in the book — a buy at or above, or a sell at or
-    below, the current market price — must then be filled by the simulated market, at the market
-    price, just as it would have been had the price already been there when the order was placed.
-
-    '''Why it is relevant, and why a background job.''' Without it, a limit order could only ever
-    fill against another user's order, and with few users most limit orders would wait forever while
-    the market price has long passed them. That would make limit orders useless in the simulation.
-    Nothing happens at the moment the price crosses an order that a trigger could react to: the
-    price moves through the bot's inserts into `market_trades`, which deliberately stay cheap single
-    inserts. Scanning and filling every crossed order on every tick inside that insert would make
-    each tick expensive. So the work runs as a periodic job after each round of price ticks.
-
-    '''PostgreSQL feature.''' Stored function `fill_marketable_orders()`. It is scheduled by the
-    application, because PostgreSQL has no built-in scheduler and the faculty server provides no
-    `pg_cron` (checked: only `plpgsql` and `pgcrypto` are available, and the project role is not a
-    superuser).
-
-    * An advisory lock (`pg_try_advisory_xact_lock`) keeps two runs from filling the same orders twice.
-    * `FOR UPDATE … SKIP LOCKED` leaves alone an order a user is cancelling at that moment; the next run picks it up.
-    * Every fill goes through `execute_trade`, so all the rules above apply to it.
-
-    '''Tables affected.''' `orders`, `market_trades`, `users`, `holdings`, `transactions`,
-    `order_events`.
-
-    === Implementation ===
-
-    ==== Stored procedures/functions ====
-
-    {{{
-    CREATE OR REPLACE FUNCTION project.fill_marketable_orders()
-    RETURNS int LANGUAGE plpgsql AS $$
-    DECLARE
-        r       record;
-        v_count int := 0;
-    BEGIN
-        IF NOT pg_try_advisory_xact_lock(hashtext('project.fill_marketable_orders')) THEN
-            RETURN 0;
-        END IF;
-        FOR r IN
-            SELECT o.id, o.side, o.quantity - o.filled_quantity AS remaining, lp.price AS market_price
-            FROM project.orders o
-            JOIN project.markets m ON m.id = o.market_id AND m.is_active
-            CROSS JOIN LATERAL (SELECT project.latest_price(o.market_id) AS price) lp
-            WHERE o.type = 'limit'
-            AND o.status IN ('open', 'partially_filled')
-            AND (o.side = 'buy'  AND o.price >= lp.price
-                OR o.side = 'sell' AND o.price <= lp.price)
-            ORDER BY o.placed_at, o.id
-            FOR UPDATE OF o SKIP LOCKED
-        LOOP
-            IF r.side = 'buy' THEN
-                PERFORM project.execute_trade(r.id, NULL, r.remaining, r.market_price, 'sell');
-            ELSE
-                PERFORM project.execute_trade(NULL, r.id, r.remaining, r.market_price, 'buy');
-            END IF;
-            v_count := v_count + 1;
-        END LOOP;
-        RETURN v_count;
-    END $$;
-    }}}
-
-    ==== Scheduling ====
-
-    In `bots/main.go`, after every round of price ticks:
-
-    {{{
-    // P7 background job: the prices just moved, so fill any resting
-    // limit order the new market price has reached.
-    var filled int
-    if err := db.QueryRow(`SELECT fill_marketable_orders()`).Scan(&filled); err != nil {
-        log.Printf("fill_marketable_orders: %v", err)
-    } else if filled > 0 {
-        log.Printf("  filled %d resting limit order(s) at the new market price", filled)
-    }
-    }}}
-
-    It can also be run by hand from any SQL client: `SELECT project.fill_marketable_orders();`.
-
-    A run with the bot, after charlie placed a limit buy of 0.1 ETH at 3599 while the market was at
-    3600: the bot's random walk took the price below 3599 and the job filled the order at the market
-    price.
-
-    {{{
-    2026/09/24 13:19:02   filled 1 resting limit order(s) at the new market price
-    }}}
-
-    {{{
-    username | side | type  |  status   | quantity | filled_quantity |    price    | avg_fill_price
-    ----------+------+-------+-----------+----------+-----------------+-------------+----------------
-    charlie  | buy  | limit | cancelled |   0.1000 |          0.0000 | 3400.000000 |
-    charlie  | buy  | limit | executed  |   0.1000 |          0.1000 | 3599.000000 |    3593.979167
-    }}}
-
-    == Tests proving the rules ==
-
-    `server/db/advanced_db_tests.sql` plays a short trading
-    story on the sample data and, along the way, tries to break every rule. It starts from alice with
-    8250 USD and 0.5 ETH, bob with 5000 USD, charlie with 2500 USD, and ETH/USD last traded at 3520:
-
-    1. alice places a limit sell of 0.3 ETH at 3600, and bob a limit buy of 0.1 at 3500. Both rest in the book.
-    2. bob places a limit buy of 0.2 at 3650. It crosses alice's ask, so they trade 0.2 at 3600: alice's order becomes partially filled, and bob gets back the 10 he had reserved above the trade price.
-    3. charlie places a market buy of 0.15. He takes alice's remaining 0.1 from the book, and the other 0.05 comes from the simulated market.
-    4. Invalid trades, state changes and balance changes are attempted directly in SQL.
-    5. bob cancels his bid.
-    6. The simulator moves the price to 3450, and the background job fills bob's new limit buy at 3500.
-
-    The deferred checks are forced with `SET CONSTRAINTS ALL IMMEDIATE`, so a violation shows up
-    inside the test instead of at the final `COMMIT`. Everything is rolled back at the end. Run on
-    PostgreSQL 17 after `-init`:
-
-    {{{
-    PASS  place: limit sell above the market rests in the book, crypto reserved: alice ETH reserved = 0.3000
-    PASS  place: limit buy below the market rests in the book, cash reserved: bob available 4650.0000 reserved 350.0000
-    PASS  event: placement recorded automatically:
-    PASS  view: order book shows both price levels: buy 0.1000 @ 3500.000000, sell 0.3000 @ 3600.000000
-    PASS  consistency holds after placing:
-    PASS  place: buy without enough free cash: insufficient funds: the order needs 60000.0000, available 2500.0000
-    PASS  place: sell more than is free (0.2 of 0.5 is already reserved): insufficient holding: trying to sell 0.3, free to sell 0.2000
-    PASS  match: trade between the two orders at the resting price:
-    PASS  status: seller partially filled, buyer executed (automatic): alice_ask partially_filled 0.2000/0.3000
-    PASS  money: buyer paid 720, got back the 10 reserved above the trade price: bob available 3930.0000 reserved 350.0000
-    PASS  crypto: 0.2 ETH moved from alice (0.1 still reserved) to bob:
-    PASS  ledger: one buy and one sell row, linked to the orders:
-    PASS  event: fills recorded automatically:
-    PASS  consistency holds after the trade:
-    PASS  market order: filled completely in two trades: executed, 2 trades, avg 3600.000000
-    PASS  market order: alice's ask is now executed, nothing left reserved:
-    PASS  consistency holds after the market order:
-    PASS  trade: price above the buyer's limit: trade price 3700.000000 is outside the limit 3500.000000 of buy order …
-    PASS  trade: more than the order has remaining: trade quantity 0.500000 exceeds the remaining quantity 0.1000 of order …
-    PASS  trade: a sell order used as the buy side: order … is a sell order and cannot be the buy side of a trade
-    PASS  trade: order of another market: order … is on a different market than the trade
-    PASS  trade: executed order cannot trade again: order … is executed and cannot trade
-    PASS  trade: a user with their own order: a user cannot trade with their own order
-    PASS  trade: a trade that filled orders cannot be deleted: a trade that filled orders cannot be changed or deleted
-    PASS  state: status cannot be set to executed by hand: order status executed does not match filled quantity 0.0000 of 0.1000; status is derived automatically
-    PASS  state: filled quantity cannot be changed by hand: filled quantity of order … can only change through a trade
-    PASS  state: executed order cannot be processed again: order … is already executed and cannot be processed again
-    PASS  state: ordered quantity cannot change: user, market, side, type, quantity, price and placed_at of an order cannot change
-    PASS  state: cancelling by hand without releasing the reservation: reserved balance 350.0000 does not match the 0 needed by active buy orders (user …)
-    PASS  cancel: someone else's order: order … does not belong to this user
-    PASS  cancel: 350 back from reserved to available, event recorded: bob available 4280.0000 reserved 0.0000
-    PASS  cancel: a cancelled order cannot be cancelled again: order … is cancelled and cannot be cancelled
-    PASS  trade: cancelled order cannot trade: order … is cancelled and cannot trade
-    PASS  balance: reserving cash with no order behind it: reserved balance 100.0000 does not match the 0 needed by active buy orders (user …)
-    PASS  balance: reserving crypto with no order behind it: reserved quantity 0.0100 does not match the 0 needed by active sell orders (user …, crypto …)
-    PASS  balance: cash changed without a ledger row: cash 2060.0000 (available + reserved) does not match the ledger total 1960.0000 (user …)
-    PASS  job: fills exactly the orders the new price reached: bob_bid3 executed @ 3450.000000, charlie_ask (3700) still open
-    PASS  job: buyer paid 345, got the 5 above the fill price back:
-    PASS  job: nothing more to do on a second run:
-    PASS  consistency holds after the job:
-    PASS  views: every trader's cash equals their ledger:
-
-    passed | failed
-    --------+--------
-        41 |      0
-    }}}
-
-    The same rules seen from the prototype (bob, after alice put 0.3 ETH up for sale at 3600):
-
-    {{{
-    -- Place buy order --
-    Latest price for ETH/USD = 3520.000000
-    Order book asks (other users' limit orders):
-        3600.000000        0.3000  (1 orders)
-    [1] Market order (fills now at the best available price)
-    [2] Limit order (fills only at your price or better, otherwise waits in the order book)
-    > 2
-    Quantity: 0.2
-    Limit price: 3650
-    Order executed: buy 0.2000 ETH, average price 3600.000000
-    }}}
-
-    === Test script ===
-
-    The complete test script, `advanced_db_tests.sql`:
-
-    {{{
-    -- advanced_db_tests.sql
-    -- EduBerza - tests for the P7 rules in advanced_db.sql
-    --
-    -- Run right after data_load.sql (seed state: alice 8250 USD + 0.5 ETH,
-    -- bob 5000 USD, charlie 2500 USD, ETH/USD last traded at 3520).
-    -- It plays a short trading story and, along the way, tries to break every
-    -- rule. Each check prints PASS/FAIL as a NOTICE; everything is rolled back
-    -- at the end, so the data is left exactly as it was.
-    --
-    -- The reservation and balance checks are deferred to COMMIT; the tests force
-    -- them with SET CONSTRAINTS ALL IMMEDIATE so a violation shows up inside the
-    -- test instead of at the final COMMIT.
-
-    BEGIN;
-    SET search_path TO project, public;
-
-    CREATE TEMP TABLE test_results (name text, passed boolean) ON COMMIT DROP;
-    CREATE TEMP TABLE ids (name text PRIMARY KEY, id uuid) ON COMMIT DROP;
-
-    -- expect_error: run p_sql (and the deferred checks); it must fail with an
-    -- error containing p_fragment.
-    CREATE PROCEDURE pg_temp.expect_error(p_name text, p_sql text, p_fragment text)
-    LANGUAGE plpgsql AS $$
-    BEGIN
-        BEGIN
-            EXECUTE p_sql;
-            SET CONSTRAINTS ALL IMMEDIATE;
-            RAISE NOTICE 'FAIL  %: no error raised', p_name;
-            INSERT INTO test_results VALUES (p_name, false);
-        EXCEPTION WHEN OTHERS THEN
-            IF SQLERRM ILIKE '%' || p_fragment || '%' THEN
-                RAISE NOTICE 'PASS  %: %', p_name, SQLERRM;
-                INSERT INTO test_results VALUES (p_name, true);
-            ELSE
-                RAISE NOTICE 'FAIL  %: unexpected error: %', p_name, SQLERRM;
-                INSERT INTO test_results VALUES (p_name, false);
-            END IF;
-        END;
-        SET CONSTRAINTS ALL DEFERRED;
-    END $$;
-
-    CREATE FUNCTION pg_temp.expect_true(p_name text, p_ok boolean, p_detail text DEFAULT '')
-    RETURNS void LANGUAGE plpgsql AS $$
-    BEGIN
-        RAISE NOTICE '%  %: %', CASE WHEN coalesce(p_ok, false) THEN 'PASS' ELSE 'FAIL' END, p_name, p_detail;
-        INSERT INTO test_results VALUES (p_name, coalesce(p_ok, false));
-    END $$;
-
-    -- consistent: all deferred checks pass right now
-    CREATE FUNCTION pg_temp.consistent() RETURNS boolean LANGUAGE plpgsql AS $$
-    BEGIN
-        SET CONSTRAINTS ALL IMMEDIATE;
-        SET CONSTRAINTS ALL DEFERRED;
-        RETURN true;
-    EXCEPTION WHEN OTHERS THEN
-        RAISE NOTICE '      consistency check failed: %', SQLERRM;
-        RETURN false;
-    END $$;
-
-    CREATE FUNCTION pg_temp.uid(p_name text) RETURNS uuid LANGUAGE sql AS $$
-        SELECT id FROM users WHERE username = p_name
-    $$;
-    CREATE FUNCTION pg_temp.oid(p_name text) RETURNS uuid LANGUAGE sql AS $$
-        SELECT id FROM ids WHERE name = p_name
-    $$;
-
-
-    -- ===========================================================================
-    -- A. Placing orders reserves what they commit
-    -- ===========================================================================
-    INSERT INTO ids VALUES ('alice_ask',
-        place_order(pg_temp.uid('alice'), 'a2222222-2222-2222-2222-222222222222', 'sell', 'limit', 0.3, 3600));
-    INSERT INTO ids VALUES ('bob_bid',
-        place_order(pg_temp.uid('bob'), 'a2222222-2222-2222-2222-222222222222', 'buy', 'limit', 0.1, 3500));
-
-    SELECT pg_temp.expect_true('place: limit sell above the market rests in the book, crypto reserved',
-        (SELECT status FROM orders WHERE id = pg_temp.oid('alice_ask')) = 'open'
-        AND (SELECT reserved_quantity FROM holdings WHERE user_id = pg_temp.uid('alice')) = 0.3,
-        'alice ETH reserved = ' || (SELECT reserved_quantity FROM holdings WHERE user_id = pg_temp.uid('alice')));
-    SELECT pg_temp.expect_true('place: limit buy below the market rests in the book, cash reserved',
-        (SELECT (available_balance, reserved_balance) FROM users WHERE username = 'bob') = (4650.0000, 350.0000),
-        (SELECT format('bob available %s reserved %s', available_balance, reserved_balance) FROM users WHERE username = 'bob'));
-    SELECT pg_temp.expect_true('event: placement recorded automatically',
-        (SELECT count(*) FROM order_events WHERE order_id IN (pg_temp.oid('alice_ask'), pg_temp.oid('bob_bid'))
-        AND event_type = 'placed') = 2);
-    SELECT pg_temp.expect_true('view: order book shows both price levels',
-        (SELECT string_agg(side || ' ' || quantity || ' @ ' || price, ', ' ORDER BY side)
-        FROM v_order_book WHERE symbol = 'ETH') = 'buy 0.1000 @ 3500.000000, sell 0.3000 @ 3600.000000',
-        (SELECT string_agg(side || ' ' || quantity || ' @ ' || price, ', ' ORDER BY side) FROM v_order_book WHERE symbol = 'ETH'));
-    SELECT pg_temp.expect_true('consistency holds after placing', pg_temp.consistent());
-
-    CALL pg_temp.expect_error('place: buy without enough free cash',
-        $q$SELECT place_order(pg_temp.uid('charlie'), 'a1111111-1111-1111-1111-111111111111', 'buy', 'limit', 1, 60000)$q$,
-        'insufficient funds');
-    CALL pg_temp.expect_error('place: sell more than is free (0.2 of 0.5 is already reserved)',
-        $q$SELECT place_order(pg_temp.uid('alice'), 'a2222222-2222-2222-2222-222222222222', 'sell', 'limit', 0.3, 3600)$q$,
-        'insufficient holding');
-
-    -- ===========================================================================
-    -- B. A trade between two compatible orders, partial fill
-    -- ===========================================================================
-    -- bob bids 0.2 at 3650: crosses alice's ask at 3600 -> trade 0.2 @ 3600
-    INSERT INTO ids VALUES ('bob_bid2',
-        place_order(pg_temp.uid('bob'), 'a2222222-2222-2222-2222-222222222222', 'buy', 'limit', 0.2, 3650));
-
-    SELECT pg_temp.expect_true('match: trade between the two orders at the resting price',
-        EXISTS (SELECT 1 FROM market_trades
-                WHERE buy_order_id = pg_temp.oid('bob_bid2') AND sell_order_id = pg_temp.oid('alice_ask')
-                AND quantity = 0.2 AND price = 3600 AND source = 'match'));
-    SELECT pg_temp.expect_true('status: seller partially filled, buyer executed (automatic)',
-        (SELECT status || ' ' || filled_quantity FROM orders WHERE id = pg_temp.oid('alice_ask')) = 'partially_filled 0.2000'
-        AND (SELECT status FROM orders WHERE id = pg_temp.oid('bob_bid2')) = 'executed',
-        (SELECT format('alice_ask %s %s/%s', status, filled_quantity, quantity) FROM orders WHERE id = pg_temp.oid('alice_ask')));
-    SELECT pg_temp.expect_true('money: buyer paid 720, got back the 10 reserved above the trade price',
-        (SELECT (available_balance, reserved_balance) FROM users WHERE username = 'bob') = (3930.0000, 350.0000),
-        (SELECT format('bob available %s reserved %s', available_balance, reserved_balance) FROM users WHERE username = 'bob'));
-    SELECT pg_temp.expect_true('crypto: 0.2 ETH moved from alice (0.1 still reserved) to bob',
-        (SELECT (quantity, reserved_quantity) FROM holdings WHERE user_id = pg_temp.uid('alice')) = (0.3000, 0.1000)
-        AND (SELECT (quantity, avg_price) FROM holdings WHERE user_id = pg_temp.uid('bob')) = (0.2000, 3600.000000));
-    SELECT pg_temp.expect_true('ledger: one buy and one sell row, linked to the orders',
-        (SELECT count(*) FROM transactions WHERE related_order IN (pg_temp.oid('bob_bid2'), pg_temp.oid('alice_ask'))) = 2);
-    SELECT pg_temp.expect_true('event: fills recorded automatically',
-        (SELECT string_agg(event_type, ',' ORDER BY id) FROM order_events WHERE order_id = pg_temp.oid('alice_ask'))
-            = 'placed,partially_filled');
-    SELECT pg_temp.expect_true('consistency holds after the trade', pg_temp.consistent());
-
-    -- ===========================================================================
-    -- C. Market order: book first, then the simulated market
-    -- ===========================================================================
-    -- market price is now 3600 (last trade); charlie buys 0.15 at market:
-    -- 0.1 from alice's remaining ask @ 3600, the other 0.05 from the market @ 3600
-    INSERT INTO ids VALUES ('charlie_mkt',
-        place_order(pg_temp.uid('charlie'), 'a2222222-2222-2222-2222-222222222222', 'buy', 'market', 0.15));
-
-    SELECT pg_temp.expect_true('market order: filled completely in two trades',
-        (SELECT (status, trades, avg_fill_price) FROM v_order_history WHERE order_id = pg_temp.oid('charlie_mkt'))
-            = ('executed'::varchar, 2::bigint, 3600.000000::numeric),
-        (SELECT format('%s, %s trades, avg %s', status, trades, avg_fill_price) FROM v_order_history WHERE order_id = pg_temp.oid('charlie_mkt')));
-    SELECT pg_temp.expect_true('market order: alice''s ask is now executed, nothing left reserved',
-        (SELECT status FROM orders WHERE id = pg_temp.oid('alice_ask')) = 'executed'
-        AND (SELECT reserved_quantity FROM holdings WHERE user_id = pg_temp.uid('alice')) = 0
-        AND (SELECT reserved_balance FROM users WHERE username = 'charlie') = 0);
-    SELECT pg_temp.expect_true('consistency holds after the market order', pg_temp.consistent());
-
-    -- ===========================================================================
-    -- D. Trades are only possible between valid, compatible orders
-    -- ===========================================================================
-    INSERT INTO ids VALUES ('charlie_ask',
-        place_order(pg_temp.uid('charlie'), 'a2222222-2222-2222-2222-222222222222', 'sell', 'limit', 0.1, 3700));
-    INSERT INTO ids VALUES ('bob_ask',
-        place_order(pg_temp.uid('bob'), 'a2222222-2222-2222-2222-222222222222', 'sell', 'limit', 0.1, 3800));
-
-    CALL pg_temp.expect_error('trade: price above the buyer''s limit',
-        $q$INSERT INTO market_trades (market_id, executed_at, price, quantity, buy_order_id, sell_order_id)
-        VALUES ('a2222222-2222-2222-2222-222222222222', now(), 3700, 0.1, pg_temp.oid('bob_bid'), pg_temp.oid('charlie_ask'))$q$,
-        'outside the limit');
-    CALL pg_temp.expect_error('trade: more than the order has remaining',
-        $q$INSERT INTO market_trades (market_id, executed_at, price, quantity, buy_order_id)
-        VALUES ('a2222222-2222-2222-2222-222222222222', now(), 3500, 0.5, pg_temp.oid('bob_bid'))$q$,
-        'exceeds the remaining quantity');
-    CALL pg_temp.expect_error('trade: a sell order used as the buy side',
-        $q$INSERT INTO market_trades (market_id, executed_at, price, quantity, buy_order_id)
-        VALUES ('a2222222-2222-2222-2222-222222222222', now(), 3700, 0.1, pg_temp.oid('charlie_ask'))$q$,
-        'cannot be the buy side');
-    CALL pg_temp.expect_error('trade: order of another market',
-        $q$INSERT INTO market_trades (market_id, executed_at, price, quantity, buy_order_id)
-        VALUES ('a1111111-1111-1111-1111-111111111111', now(), 3500, 0.1, pg_temp.oid('bob_bid'))$q$,
-        'different market');
-    CALL pg_temp.expect_error('trade: executed order cannot trade again',
-        $q$INSERT INTO market_trades (market_id, executed_at, price, quantity, sell_order_id)
-        VALUES ('a2222222-2222-2222-2222-222222222222', now(), 3600, 0.1, pg_temp.oid('alice_ask'))$q$,
-        'is executed and cannot trade');
-    CALL pg_temp.expect_error('trade: a user with their own order',
-        $q$INSERT INTO market_trades (market_id, executed_at, price, quantity, buy_order_id, sell_order_id)
-        VALUES ('a2222222-2222-2222-2222-222222222222', now(), 3500, 0.1, pg_temp.oid('bob_bid'), pg_temp.oid('bob_ask'))$q$,
-        'own order');
-    CALL pg_temp.expect_error('trade: a trade that filled orders cannot be deleted',
-        $q$DELETE FROM market_trades WHERE buy_order_id = pg_temp.oid('bob_bid2')$q$,
-        'cannot be changed or deleted');
-
-    -- ===========================================================================
-    -- E. Order state changes
-    -- ===========================================================================
-    CALL pg_temp.expect_error('state: status cannot be set to executed by hand',
-        $q$UPDATE orders SET status = 'executed' WHERE id = pg_temp.oid('bob_bid')$q$,
-        'status is derived automatically');
-    CALL pg_temp.expect_error('state: filled quantity cannot be changed by hand',
-        $q$UPDATE orders SET filled_quantity = 0.05 WHERE id = pg_temp.oid('bob_bid')$q$,
-        'can only change through a trade');
-    CALL pg_temp.expect_error('state: executed order cannot be processed again',
-        $q$UPDATE orders SET status = 'cancelled' WHERE id = pg_temp.oid('alice_ask')$q$,
-        'already executed');
-    CALL pg_temp.expect_error('state: ordered quantity cannot change',
-        $q$UPDATE orders SET quantity = 1 WHERE id = pg_temp.oid('bob_bid')$q$,
-        'cannot change');
-    CALL pg_temp.expect_error('state: cancelling by hand without releasing the reservation',
-        $q$UPDATE orders SET status = 'cancelled' WHERE id = pg_temp.oid('bob_bid')$q$,
-        'reserved balance');
-
-    -- ===========================================================================
-    -- F. Cancelling releases the reservation, exactly once
-    -- ===========================================================================
-    CALL pg_temp.expect_error('cancel: someone else''s order',
-        $q$SELECT cancel_order(pg_temp.oid('bob_bid'), pg_temp.uid('charlie'))$q$,
-        'does not belong');
-    SELECT cancel_order(pg_temp.oid('bob_bid'), pg_temp.uid('bob'));
-    SELECT pg_temp.expect_true('cancel: 350 back from reserved to available, event recorded',
-        (SELECT (available_balance, reserved_balance) FROM users WHERE username = 'bob') = (4280.0000, 0.0000)
-        AND EXISTS (SELECT 1 FROM order_events WHERE order_id = pg_temp.oid('bob_bid') AND event_type = 'cancelled'),
-        (SELECT format('bob available %s reserved %s', available_balance, reserved_balance) FROM users WHERE username = 'bob'));
-    CALL pg_temp.expect_error('cancel: a cancelled order cannot be cancelled again',
-        $q$SELECT cancel_order(pg_temp.oid('bob_bid'), pg_temp.uid('bob'))$q$,
-        'cannot be cancelled');
-    CALL pg_temp.expect_error('trade: cancelled order cannot trade',
-        $q$INSERT INTO market_trades (market_id, executed_at, price, quantity, buy_order_id)
-        VALUES ('a2222222-2222-2222-2222-222222222222', now(), 3500, 0.1, pg_temp.oid('bob_bid'))$q$,
-        'is cancelled and cannot trade');
-
-    -- ===========================================================================
-    -- G. Balances cannot be put in an inconsistent state
-    -- ===========================================================================
-    CALL pg_temp.expect_error('balance: reserving cash with no order behind it',
-        $q$UPDATE users SET available_balance = available_balance - 100, reserved_balance = reserved_balance + 100
-            WHERE username = 'charlie'$q$,
-        'reserved balance');
-    CALL pg_temp.expect_error('balance: reserving crypto with no order behind it',
-        $q$UPDATE holdings SET reserved_quantity = reserved_quantity + 0.01 WHERE user_id = pg_temp.uid('alice')$q$,
-        'reserved quantity');
-    CALL pg_temp.expect_error('balance: cash changed without a ledger row',
-        $q$UPDATE users SET available_balance = available_balance + 100 WHERE username = 'charlie'$q$,
-        'does not match the ledger');
-
-    -- ===========================================================================
-    -- H. Background job: resting limit orders filled when the market reaches them
-    -- ===========================================================================
-    INSERT INTO ids VALUES ('bob_bid3',
-        place_order(pg_temp.uid('bob'), 'a2222222-2222-2222-2222-222222222222', 'buy', 'limit', 0.1, 3500));
-    -- the simulator moves the price down to 3450 (a bot tick, no orders)
-    INSERT INTO market_trades (market_id, executed_at, price, quantity, side)
-    VALUES ('a2222222-2222-2222-2222-222222222222', now(), 3450, 0.01, 'sell');
-
-    CREATE TEMP TABLE job_run ON COMMIT DROP AS SELECT fill_marketable_orders() AS filled;
-
-    SELECT pg_temp.expect_true('job: fills exactly the orders the new price reached',
-        (SELECT filled FROM job_run) = 1
-        AND (SELECT (status, avg_fill_price) FROM v_order_history WHERE order_id = pg_temp.oid('bob_bid3'))
-            = ('executed'::varchar, 3450.000000::numeric)
-        AND (SELECT status FROM orders WHERE id = pg_temp.oid('charlie_ask')) = 'open',
-        (SELECT format('bob_bid3 %s @ %s, charlie_ask (3700) still %s', h.status, h.avg_fill_price, o.status)
-        FROM v_order_history h, orders o WHERE h.order_id = pg_temp.oid('bob_bid3') AND o.id = pg_temp.oid('charlie_ask')));
-    SELECT pg_temp.expect_true('job: buyer paid 345, got the 5 above the fill price back',
-        (SELECT reserved_balance FROM users WHERE username = 'bob') = 0
-        AND (SELECT count(*) FROM transactions WHERE related_order = pg_temp.oid('bob_bid3') AND amount = -345) = 1);
-    SELECT pg_temp.expect_true('job: nothing more to do on a second run', fill_marketable_orders() = 0);
-    SELECT pg_temp.expect_true('consistency holds after the job', pg_temp.consistent());
-
-    -- ===========================================================================
-    -- Final state of every trader
-    -- ===========================================================================
-    SELECT pg_temp.expect_true('views: every trader''s cash equals their ledger',
-        NOT EXISTS (SELECT 1 FROM v_trader_balances WHERE total_cash <> ledger_total));
-
-    SELECT username, available_balance, reserved_balance, ledger_total, holdings_value
-    FROM v_trader_balances ORDER BY username;
-
-    SELECT count(*) FILTER (WHERE passed)     AS passed,
-        count(*) FILTER (WHERE NOT passed) AS failed
-    FROM test_results;
-
-    ROLLBACK;
-    }}}
-
-    == Changes to earlier phases ==
-
-    * '''Schema''' ([wiki:RelationalDesign], [wiki:ERModel]):
-    * `users.reserved_balance`;
-    * `orders.filled_quantity` and the status value `partially_filled`;
-    * `market_trades.buy_order_id` / `sell_order_id` (optional references to `orders` — a new relationship "trade fills order");
-    * the new table `order_events`.
-    * '''Sample data''' (`data_load.sql`):
-    * deposit rows for bob and charlie, whose balances previously had no ledger entries behind them;
-    * alice's seeded order is imported as completely filled;
-    * the script runs as one transaction.
-
-    The balances documented in [wiki:BuildInstructions] are
-    unchanged.
-    * '''P6 demo data''' (`reports_demo_data.sql`): it adjusts the users' balances by exactly what it adds to the ledger, and imports its orders as completely filled. Both [wiki:AdvancedReports] outputs are unchanged.
-    * '''Prototype:'''
-    * placing an order is one call to `place_order`, and market or limit can be chosen;
-    * new menu items `[12] Order book`, `[13] My open orders`, `[14] Cancel an order`;
-    * the balance screen shows reserved cash;
-    * the bot runs the background job.
-
-    == AI usage ==
-
-    AI was used in this phase and is logged in full, per the course rule for P1 onward.
-
-    * '''Phase log:''' [wiki:AdvancedDatabaseDevelopmentAIUsage]
-
-    '''In short:''' the requirements — order state consistency, filled/remaining quantities, reserved
-    money and assets, trades only between compatible orders, the kinds of triggers, procedures and
-    views, and a background job only if relevant — were mine. I asked the AI to turn them into
-    concrete rules for the existing !EduBerza database, implement them without redesigning it, test
-    them and document them.
Index: cs/P7-AdvancedDatabaseDevelopment/wiki/AdvancedDatabaseDevelopmentAIUsage.md
===================================================================
--- docs/P7-AdvancedDatabaseDevelopment/wiki/AdvancedDatabaseDevelopmentAIUsage.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,177 +1,0 @@
-= Advanced Database Development AI Usage =
-
-== Name of AI service/solution that was used ==
-
-'''Claude Code''' (Anthropic)
-
- * '''URL:''' `https://claude.com/claude-code`
- * '''Type of service/subscription:''' Claude subscription, model Claude Opus 5.5.
-
-== Final result ==
-
-=== Diagram ===
-
-No new diagram. The data model gains four columns and one table, all listed under "Changes to
-earlier phases" in [wiki:AdvancedDatabaseDevelopment]:
-
- * `users.reserved_balance`;
- * `orders.filled_quantity` (with the status `partially_filled`);
- * `market_trades.buy_order_id` and `market_trades.sell_order_id`;
- * `order_events`.
-
-The optional link from a trade to the orders it filled is a new relationship, which
-[wiki:ERModel] and
-[wiki:RelationalDesign] must show as well.
-
-=== Results in details / description ===
-
-My requirements, given to the AI as a list:
-
- * complex order consistency: no invalid state changes, filled and remaining quantities consistent, finished orders never processed again;
- * balance and order consistency: active orders consistent with reserved money and assets, no inconsistent balance states from order operations;
- * trade consistency: trades only between valid compatible orders, never more than the remaining quantity, orders, trades and balances kept consistent;
- * which kinds of triggers, procedures, functions and views to build;
- * a background job only if it is genuinely relevant;
- * basic column constraints kept out of P7.
-
-The AI:
-
- * Checked the existing schema against those requirements. It found that three of them could not be expressed without small additions: there was no filled quantity, no place for reserved cash, and no link from a trade to its orders. It added only the four columns listed above and the event table.
- * For every feature, wrote down the business rule, why it is non-trivial, which PostgreSQL feature implements it and which tables it affects. This is the structure I asked for, and [wiki:AdvancedDatabaseDevelopment] follows it.
- * Implemented `advanced_db.sql`:
-   * 5 row triggers (order lifecycle, order events, trade validation, trade fill, trade immutability);
-   * 3 deferred constraint triggers (reserved cash, reserved crypto, cash equals ledger);
-   * the functions `place_order`, `match_order`, `execute_trade`, `cancel_order`, `order_reservation` and `latest_price`;
-   * 4 views;
-   * the background job `fill_marketable_orders`.
- * Checked the faculty server before choosing how to schedule the background job. There is no `pg_cron` and the role is not a superuser, so the job is a function that the bot process calls after every round of price ticks.
- * Adapted the two data scripts to the new consistency rules and verified that both P6 report outputs stay exactly as documented.
- * Wrote `advanced_db_tests.sql`: a trading story with 41 checks, rolled back at the end. It ran them until all passed. The one failure along the way was in the test itself: a check read the data in the same statement as the job it was checking.
- * Wired the prototype to use the new functions:
-   * order placement is a single `place_order` call, with market or limit orders;
-   * the order book, open orders and cancel are available from the menu, with cancel picked from a numbered list;
-   * the balance screen shows reserved cash;
-   * the bot runs the job.
-
-  All of this was verified through the CLI and with the bot running.
- * Wrote this documentation.
-
-== Summary of AI involvement ==
-
-||= =||= This session — 2026-09-24 =||
-|| '''What I brought''' || The P7 requirements for !EduBerza: order, balance and trade consistency, the kinds of triggers, procedures and views, and the condition on the background job ||
-|| '''What the AI did''' || Turned the requirements into concrete rules on the existing schema, implemented and tested them, wired them into the prototype, wrote the documentation ||
-|| '''What I decided''' || The requirements themselves; to discard the AI's earlier, self-proposed P7 version (see the log) and redo the phase from my requirements; to keep the schema changes minimal ||
-
-== Entire AI usage log ==
-
-=== 2026-09-24 — earlier attempt, discarded ===
-
-Earlier in the same session I had pasted the P7 and P8 rubrics without ideas of my own, and the
-AI proposed and implemented a P7 of its own design: custom domains, an append-only ledger,
-candles derived from trades and a maintenance job. Before submitting anything I decided to redo
-the phase from my own requirements, and asked for that version to be reverted. None of it
-remains in the project. It is mentioned here only so the log is complete.
-
-=== 2026-09-24 — this phase ===
-
-'''Prompt (student, verbatim):'''
-> P7 requirements are:
->
-> more complex data constraints and consistency requirements
-> business scenarios that require special database checks
-> triggers
-> stored procedures and functions
-> views
-> background jobs
-> documentation of the implementation
->
-> Basic column constraints such as NOT NULL, UNIQUE, CHECK, PRIMARY KEY, and basic foreign keys
-> are Phase P2 and must NOT be proposed as P7 features.
->
-> Based on my existing !EduBerza database, identify and implement only genuinely NON-TRIVIAL P7
-> requirements.
->
-> Focus on these types of requirements:
->
-> Complex order consistency
-> Prevent invalid order state changes.
-> Ensure filled/remaining quantities stay consistent.
-> Prevent cancelled or completed orders from being processed again.
-> Balance and order consistency
-> Ensure active orders are consistent with reserved virtual money/assets.
-> Prevent inconsistent balance states caused by order operations.
-> Trade consistency
-> Ensure a trade can only happen between valid compatible orders.
-> Ensure trade quantity cannot exceed the remaining order quantity.
-> Keep orders, trades, and balances consistent.
-> Triggers
-> Create triggers only for automatic database behavior that is genuinely required, such as:
-> automatic validation of complex business rules
-> automatic order status changes
-> automatic recording of important order/trade events
-> Stored procedures/functions
-> Create procedures/functions for complex database operations such as:
-> placing an order with the necessary consistency checks
-> executing a trade while updating all related data consistently
-> Views
-> Create useful views for derived application data, such as:
-> current order book
-> active orders
-> trader portfolio/balances
-> order/trade history
->
-> Do not recreate reports that were already implemented in P6 unless a view is needed
-> specifically as part of P7.
->
-> Background jobs
-> Implement a meaningful background database job only if it is relevant to the existing
-> !EduBerza design.
-> Do not create an artificial background job just to satisfy the requirement.
->
-> For every proposed P7 feature, explain:
->
-> the business rule
-> why it is non-trivial
-> which PostgreSQL feature should implement it
-> which existing tables it affects
->
-> Then implement the selected P7 features in the existing project without redesigning the
-> database unnecessarily.
->
-> Finally, document:
->
-> complex data requirements
-> triggers
-> stored procedures/functions
-> views
-> background jobs
-> how each one maintains consistency or enforces a business rule
-> examples/tests proving that the rules work
->
-> Keep the implementation strictly within P7.
-
-(The first time I sent it the message was interrupted; I sent it again, followed by "and revert
-the previous changes".)
-
-'''Response (AI, summarised):'''
-
- * '''Revert.''' It restored every file changed by the discarded attempt to the last commit and deleted the files that attempt had created. It kept the SSH connection support (from before P7) and my own uncommitted edits.
- * '''Analysis.''' It reported that three requirements need data the schema doesn't have — a filled quantity, a reserved cash balance, and trade-to-order links — and added exactly those (`orders.filled_quantity` plus status `partially_filled`, `users.reserved_balance`, `market_trades.buy_order_id`/`sell_order_id`), plus `order_events` for the automatic event recording.
- * '''Design:'''
-   * A trade is always a `market_trades` row naming the orders it fills. Triggers on that table validate compatibility and fill the orders; the order's status is derived from its filled quantity by the order trigger.
-   * `filled_quantity` may only change while a trade is being recorded, which is detected through a transaction-local setting.
-   * The three balance rules are deferred constraint triggers, because every legitimate operation breaks them between its statements.
-   * Placing an order reserves first, then matches against the order book (price–time priority), then fills the marketable remainder from the simulated market.
-   * Rounding of reservations is defined once (`order_reservation`), so placing, filling, cancelling and checking always agree.
- * '''Background job.''' It argued the job is relevant rather than artificial: the market price is moved by the simulator, and without the job resting limit orders would never fill once the price reaches them. It explained why a trigger on the bot's price ticks would be the wrong place for this work.
- * '''Verification''' (local PostgreSQL 17 in Docker only, never the faculty database):
-   * both P6 reports return the documented numbers;
-   * the P6 demo script can be re-run without doubling anything;
-   * the test story passes 41 of 41;
-   * through the CLI: a limit sell resting in the book, a crossing limit buy trading at the resting price with the difference refunded, a market buy filled partly from the book and partly from the market, the order book, open orders, and cancelling with the reservation released;
-   * with the bot running, the job filled a resting limit order once the price walk reached it.
- * '''Documentation.''' It wrote [wiki:AdvancedDatabaseDevelopment] and this log, with the SQL on the page taken automatically from `advanced_db.sql` so the two cannot differ, together with the faculty-site (wiki) versions of both pages.
-
-'''What I decided:''' the requirements are mine. I kept the AI's minimal schema additions and its
-design for fulfilling them, and asked for no changes to it before it was implemented.
Index: cs/README.md
===================================================================
--- docs/README.md	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,124 +1,0 @@
-# EduBerza
-
-## Short description
-
-*P0 — write 150–200 words here in your own words. AI-generated text is not
-allowed in this phase.*
-
-*Your own Macedonian draft is in [`opis.md`](P0-ProjectDefinition/opis.md); the material to cover is
-what data the database holds (users, crypto assets, markets, orders, holdings,
-transactions, market trades, candles, watchlists), who it is for (beginner
-traders), and what kind of project it is.*
-
-## Team members
-
-- Stefan Trsunov 231285
-
-## Course
-
-Databases in 2025/2026/Winter
-
-## Under the supervision of
-
-Prof. Dr. Vangel V. Ajanovski
-
-## How this folder maps to the wiki
-
-The files here are grouped into one folder per phase for convenience. **The wiki
-is flat** — when you publish, each file becomes a page named exactly after the
-file, with no folder prefix and with capitalisation preserved: `About`,
-`ERModel`, `RelationalDesign`, `UseCaseModel`, `UseCase0001`, …,
-`PrototypeImplementation`, `BuildInstructions`, and the four `*AIUsage` pages.
-Attachments (`ERModel_v05.xml`, `ERModel_v05.png`, `schema_creation.sql`,
-`data_load.sql`, `relational_diagram_v4.png`, the screenshots) attach to the page
-that documents them.
-
-| Folder | Phase | Wiki pages produced |
-|--------|-------|---------------------|
-| `P0-ProjectDefinition/` | P0 | `About` (+ this front page) |
-| `P1-ConceptualModel/`   | P1 | `ERModel`, `ERModelAIUsage` |
-| `P2-RelationalDesign/`  | P2 | `RelationalDesign`, `RelationalDesignAIUsage` |
-| `P3-UseCaseModel/`      | P3 | `UseCaseModel`, `UseCase0001`–`UseCase0007`, `UseCaseModelAIUsage` |
-| `P4-Prototype/`         | P4 | `PrototypeImplementation`, `UseCase000XImplementation`, `BuildInstructions`, `PrototypeImplementationAIUsage` |
-| `P5-Normalization/`     | P5 | `Normalization`, `NormalizationAIUsage` |
-| `P6-AdvancedReports/`   | P6 | `AdvancedReports`, `AdvancedReportsAIUsage` |
-| `P7-AdvancedDatabaseDevelopment/` | P7 | `AdvancedDatabaseDevelopment`, `AdvancedDatabaseDevelopmentAIUsage` |
-
-`Instructions.md` is the condensed course rubric — reference material, not a
-submission. `P0-ProjectDefinition/opis.md` and `P1-ConceptualModel/ep-diagram.md`
-are your own working notes, kept because they are the evidence that the initial
-model was yours before any AI was used.
-
-The two SQL scripts deliberately stay in `server/db/` rather than moving into
-`P2-RelationalDesign/`: they are compiled into the prototype binary with
-`go:embed`, which requires them to sit inside the Go package. Attach them to the
-`RelationalDesign` page from there.
-
-## Content
-
-| Phase | Link | Status |
-|-------|------|--------|
-| P0 | [About](P0-ProjectDefinition/About.md) | Draft — needs your own text |
-| P1 | [ERModel](P1-ConceptualModel/ERModel.md) | Finished, awaiting approval |
-| P2 | [RelationalDesign](P2-RelationalDesign/RelationalDesign.md) | Finished, awaiting approval |
-| P3 | [UseCaseModel](P3-UseCaseModel/UseCaseModel.md) | Finished, awaiting approval |
-| P4 | [PrototypeImplementation](P4-Prototype/PrototypeImplementation.md) | Finished, awaiting approval |
-| P5 | [Normalization](P5-Normalization/Normalization.md) | Finished, awaiting approval |
-| P6 | [AdvancedReports](P6-AdvancedReports/AdvancedReports.md) | Finished, awaiting approval |
-| P7 | [AdvancedDatabaseDevelopment](P7-AdvancedDatabaseDevelopment/AdvancedDatabaseDevelopment.md) | Finished, awaiting approval |
-| P8 | *Advanced Application Development* | Not started |
-| P9 | *Other topics (Performance, Security)* | Not started |
-
-Keep the Status column current yourself — *started → completed → revised →
-approved → final* — after each consultation.
-
-Note: P0–P4 alone are not enough for a passing grade; at least some of P5–P9 are
-required. P5 is the recommended next one.
-
-## Phase attachments
-
-| File | Phase | What |
-|------|-------|------|
-| [`ERModel_v05.xml`](P1-ConceptualModel/ERModel_v05.xml) | P1 | TerraER source, current version |
-| [`ERModel_v05.png`](P1-ConceptualModel/ERModel_v05.png) | P1 | Exported diagram image, current version |
-| [`ERModel_v04.xml`](P1-ConceptualModel/ERModel_v04.xml), [`ERModel_v03.xml`](P1-ConceptualModel/ERModel_v03.xml) | P1 | TerraER source, previous versions (kept per P1 rules) |
-| [`ERModel_v04.png`](P1-ConceptualModel/ERModel_v04.png), [`ERModel_v03.png`](P1-ConceptualModel/ERModel_v03.png) | P1 | Exported diagram images, previous versions |
-| [`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 |
-| [`../server/db/schema_creation.sql`](../server/db/schema_creation.sql) | P2 | DDL — drops and recreates the `project` schema |
-| [`../server/db/data_load.sql`](../server/db/data_load.sql) | P2 | DML — truncates and reloads sample data |
-| [`relational_diagram_v4.png`](P2-RelationalDesign/relational_diagram_v4.png) | P2 | Relational diagram exported from DBeaver, laid out like `ERModel_v05.png` |
-| [`../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`) |
-| [`../server/db/advanced_db.sql`](../server/db/advanced_db.sql) | P7 | Triggers, functions, views and the background job (part of `-init`) |
-| [`../server/db/advanced_db_tests.sql`](../server/db/advanced_db_tests.sql) | P7 | Tests for every P7 rule, rolled back at the end |
-
-## Use cases (P3)
-
-[UC0001](P3-UseCaseModel/UseCase0001.md) Register ·
-[UC0002](P3-UseCaseModel/UseCase0002.md) Log in ·
-[UC0003](P3-UseCaseModel/UseCase0003.md) Deposit ·
-[UC0004](P3-UseCaseModel/UseCase0004.md) Buy ·
-[UC0005](P3-UseCaseModel/UseCase0005.md) Sell ·
-[UC0006](P3-UseCaseModel/UseCase0006.md) Portfolio ·
-[UC0007](P3-UseCaseModel/UseCase0007.md) Watchlist
-
-## AI usage logs
-
-Required by the phase rules for every phase where AI was used. P0 does not have
-one because AI use is forbidden there.
-
-- [ERModelAIUsage](P1-ConceptualModel/ERModelAIUsage.md) (P1)
-- [RelationalDesignAIUsage](P2-RelationalDesign/RelationalDesignAIUsage.md) (P2)
-- [UseCaseModelAIUsage](P3-UseCaseModel/UseCaseModelAIUsage.md) (P3)
-- [PrototypeImplementationAIUsage](P4-Prototype/PrototypeImplementationAIUsage.md) (P4)
-- [NormalizationAIUsage](P5-Normalization/NormalizationAIUsage.md) (P5)
-- [AdvancedReportsAIUsage](P6-AdvancedReports/AdvancedReportsAIUsage.md) (P6)
-- [AdvancedDatabaseDevelopmentAIUsage](P7-AdvancedDatabaseDevelopment/AdvancedDatabaseDevelopmentAIUsage.md) (P7)
-
-## Build & run
-
-See [BuildInstructions](P4-Prototype/BuildInstructions.md), or [`../README.md`](../README.md)
-for the four-command quick start.
-# bp
Index: .mod
===================================================================
--- go.mod	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,10 +1,0 @@
-module bp_project
-
-go 1.25
-
-require (
-	github.com/lib/pq v1.10.9
-	golang.org/x/crypto v0.45.0
-)
-
-require golang.org/x/sys v0.38.0 // indirect
Index: .sum
===================================================================
--- go.sum	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,8 +1,0 @@
-github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
-github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
-golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
-golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
-golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
-golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
-golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
-golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
Index: rver/account.go
===================================================================
--- server/account.go	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,102 +1,0 @@
-package main
-
-import (
-	"fmt"
-	"strconv"
-	"strings"
-
-	"bp_project/server/db"
-)
-
-// ShowBalance prints the logged-in user's balances.
-func ShowBalance(s *Session) {
-	// P7 view v_trader_balances: cash is split into what is free and what
-	// is reserved by the user's open buy orders.
-	var avail, reserved, invested float64
-	err := db.DB.QueryRow(
-		`SELECT available_balance, reserved_balance, invested_balance
-		   FROM v_trader_balances WHERE user_id = $1`,
-		s.UserID,
-	).Scan(&avail, &reserved, &invested)
-	if err != nil {
-		fmt.Println("Error:", err)
-		return
-	}
-	fmt.Printf("\n  Available: %.4f USD\n", avail)
-	fmt.Printf("  Reserved : %.4f USD (open buy orders)\n", reserved)
-	fmt.Printf("  Invested : %.4f USD\n", invested)
-	fmt.Printf("  Total    : %.4f USD\n", avail+reserved+invested)
-}
-
-// Deposit - UC0003
-// Transactional: updates users.available_balance and inserts a ledger row.
-func Deposit(s *Session) {
-	fmt.Println("\n-- Deposit virtual funds --")
-	amtStr := prompt("Amount (USD): ")
-	amt, err := strconv.ParseFloat(amtStr, 64)
-	if err != nil || amt <= 0 {
-		fmt.Println("Invalid amount.")
-		return
-	}
-
-	tx, err := db.DB.Begin()
-	if err != nil {
-		fmt.Println("Error:", err)
-		return
-	}
-	defer tx.Rollback()
-
-	if _, err := tx.Exec(
-		`UPDATE users
-		    SET available_balance = available_balance + $1,
-		        updated_at        = now()
-		  WHERE id = $2`,
-		amt, s.UserID,
-	); err != nil {
-		fmt.Println("Error:", err)
-		return
-	}
-	if _, err := tx.Exec(
-		`INSERT INTO transactions (user_id, type, amount, currency, description)
-		 VALUES ($1, 'deposit', $2, 'USD', 'Virtual deposit')`,
-		s.UserID, amt,
-	); err != nil {
-		fmt.Println("Error:", err)
-		return
-	}
-	if err := tx.Commit(); err != nil {
-		fmt.Println("Error:", err)
-		return
-	}
-	fmt.Printf("Deposited %.4f USD.\n", amt)
-}
-
-// ShowTransactions lists the last 20 ledger entries for the user.
-func ShowTransactions(s *Session) {
-	rows, err := db.DB.Query(
-		`SELECT created_at, type, amount, currency, COALESCE(description, '')
-		   FROM transactions
-		  WHERE user_id = $1
-		  ORDER BY created_at DESC
-		  LIMIT 20`,
-		s.UserID,
-	)
-	if err != nil {
-		fmt.Println("Error:", err)
-		return
-	}
-	defer rows.Close()
-
-	fmt.Println()
-	fmt.Printf("  %-20s  %-8s  %12s  %-3s  %s\n", "When", "Type", "Amount", "Cur", "Description")
-	fmt.Println("  " + strings.Repeat("-", 70))
-	for rows.Next() {
-		var when, typ, cur, desc string
-		var amt float64
-		if err := rows.Scan(&when, &typ, &amt, &cur, &desc); err != nil {
-			fmt.Println("scan error:", err)
-			return
-		}
-		fmt.Printf("  %-20s  %-8s  %12.4f  %-3s  %s\n", when[:19], typ, amt, cur, desc)
-	}
-}
Index: rver/auth.go
===================================================================
--- server/auth.go	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,108 +1,0 @@
-package main
-
-import (
-	"crypto/sha256"
-	"database/sql"
-	"encoding/hex"
-	"errors"
-	"fmt"
-	"strings"
-
-	"bp_project/server/db"
-)
-
-func hashPassword(pw string) string {
-	sum := sha256.Sum256([]byte(pw))
-	return hex.EncodeToString(sum[:])
-}
-
-// Register - UC0001
-func Register() {
-	fmt.Println("\n-- Register --")
-	username := prompt("Username: ")
-	email := prompt("Email: ")
-	fullName := prompt("Full name: ")
-	pw := prompt("Password (min 6 chars): ")
-
-	if username == "" || email == "" || pw == "" {
-		fmt.Println("Username, email and password are required.")
-		return
-	}
-	if !strings.Contains(email, "@") {
-		fmt.Println("Invalid email.")
-		return
-	}
-	if len(pw) < 6 {
-		fmt.Println("Password must be at least 6 characters.")
-		return
-	}
-
-	var exists bool
-	err := db.DB.QueryRow(
-		`SELECT EXISTS(SELECT 1 FROM users WHERE username = $1 OR email = $2)`,
-		username, email,
-	).Scan(&exists)
-	if err != nil {
-		fmt.Println("Database error:", err)
-		return
-	}
-	if exists {
-		fmt.Println("Username or email already taken.")
-		return
-	}
-
-	_, err = db.DB.Exec(
-		`INSERT INTO users (username, email, full_name, password_hash, available_balance)
-		 VALUES ($1, $2, $3, $4, 0)`,
-		username, email, fullName, hashPassword(pw),
-	)
-	if err != nil {
-		fmt.Println("Failed to register:", err)
-		return
-	}
-	fmt.Println("Account created. You can now log in.")
-}
-
-// Login - UC0002
-func Login(s *Session) {
-	fmt.Println("\n-- Login --")
-	username := prompt("Username: ")
-	pw := prompt("Password: ")
-	if username == "" || pw == "" {
-		fmt.Println("Username and password are required.")
-		return
-	}
-
-	id, err := authenticate(username, pw)
-	if err != nil {
-		if errors.Is(err, errInvalidCreds) {
-			fmt.Println("Invalid credentials.")
-			return
-		}
-		fmt.Println("Login error:", err)
-		return
-	}
-	s.UserID = id
-	s.Username = username
-	fmt.Println("Login successful.")
-}
-
-var errInvalidCreds = errors.New("invalid credentials")
-
-func authenticate(username, pw string) (string, error) {
-	var id, stored string
-	err := db.DB.QueryRow(
-		`SELECT id, password_hash FROM users WHERE username = $1`,
-		username,
-	).Scan(&id, &stored)
-	if err == sql.ErrNoRows {
-		return "", errInvalidCreds
-	}
-	if err != nil {
-		return "", err
-	}
-	if stored != hashPassword(pw) {
-		return "", errInvalidCreds
-	}
-	return id, nil
-}
Index: rver/cli.go
===================================================================
--- server/cli.go	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,121 +1,0 @@
-package main
-
-import (
-	"bufio"
-	"fmt"
-	"os"
-	"strings"
-)
-
-// Session holds the currently-logged-in user, if any.
-type Session struct {
-	UserID   string
-	Username string
-}
-
-var stdin = bufio.NewReader(os.Stdin)
-
-func prompt(label string) string {
-	fmt.Print(label)
-	line, err := stdin.ReadString('\n')
-	// On EOF (Ctrl-D, or the end of a piped script) ReadString keeps returning
-	// an error forever. Without this the menu loop would spin printing
-	// "Unknown option." indefinitely instead of ending.
-	if err != nil && strings.TrimSpace(line) == "" {
-		fmt.Println("\nInput closed. Goodbye.")
-		os.Exit(0)
-	}
-	return strings.TrimSpace(line)
-}
-
-func RunCLI() {
-	fmt.Println("=========================================")
-	fmt.Println("  EduBerza - Crypto Exchange Simulation")
-	fmt.Println("=========================================")
-
-	var s Session
-	for {
-		if s.UserID == "" {
-			anonymousMenu(&s)
-		} else {
-			authenticatedMenu(&s)
-		}
-	}
-}
-
-func anonymousMenu(s *Session) {
-	fmt.Println()
-	fmt.Println("[1] Register")
-	fmt.Println("[2] Login")
-	fmt.Println("[3] Browse markets")
-	fmt.Println("[0] Exit")
-	switch prompt("> ") {
-	case "1":
-		Register()
-	case "2":
-		Login(s)
-	case "3":
-		ListMarkets()
-	case "0":
-		fmt.Println("Goodbye.")
-		os.Exit(0)
-	default:
-		fmt.Println("Unknown option.")
-	}
-}
-
-func authenticatedMenu(s *Session) {
-	fmt.Printf("\n--- Logged in as %s ---\n", s.Username)
-	fmt.Println("[1] View balance")
-	fmt.Println("[2] Deposit virtual funds")
-	fmt.Println("[3] Browse markets")
-	fmt.Println("[4] Place BUY order")
-	fmt.Println("[5] Place SELL order")
-	fmt.Println("[6] View portfolio")
-	fmt.Println("[7] View transaction history")
-	fmt.Println("[8] Manage watchlist")
-	fmt.Println("[9] Logout")
-	fmt.Println("[10] Report: top traders")
-	fmt.Println("[11] Report: market performance")
-	fmt.Println("[12] Order book")
-	fmt.Println("[13] My open orders")
-	fmt.Println("[14] Cancel an order")
-	fmt.Println("[0] Exit")
-	switch prompt("> ") {
-	case "1":
-		ShowBalance(s)
-	case "2":
-		Deposit(s)
-	case "3":
-		ListMarkets()
-	case "4":
-		PlaceOrder(s, "buy")
-	case "5":
-		PlaceOrder(s, "sell")
-	case "6":
-		ShowPortfolio(s)
-	case "7":
-		ShowTransactions(s)
-	case "8":
-		ManageWatchlist(s)
-	case "10":
-		ShowTopTraders(s)
-	case "11":
-		ShowMarketPerformance(s)
-	case "12":
-		ShowOrderBook()
-	case "13":
-		ShowMyOrders(s)
-	case "14":
-		CancelOrder(s)
-	case "9":
-		s.UserID = ""
-		s.Username = ""
-		fmt.Println("Logged out.")
-	case "0":
-		fmt.Println("Goodbye.")
-		os.Exit(0)
-	default:
-		fmt.Println("Unknown option.")
-	}
-}
Index: rver/db/advanced_db.sql
===================================================================
--- server/db/advanced_db.sql	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,820 +1,0 @@
--- advanced_db.sql
--- EduBerza - P7 Advanced Database Development
--- Course: Databases 2025/2026 Winter, FINKI UKIM
---
--- Order, reservation and trade consistency implemented in the database.
--- Run after schema_creation.sql and before data_load.sql (-init does both).
---
---   1. Order lifecycle      - status is derived from filled_quantity, only
---                             valid transitions, finished orders are final,
---                             filled_quantity only changes through a trade.
---   2. Trade consistency    - a trade row may only fill compatible, active
---                             orders, never more than they have remaining;
---                             inserting it fills the orders automatically.
---   3. Reservations/balance - reserved cash and reserved crypto always equal
---                             what the user's active orders still need, and
---                             cash (available + reserved) always equals the
---                             ledger. Checked at COMMIT.
---   4. Order events         - every placement, fill and cancellation is
---                             recorded automatically.
---   5. Operations           - place_order, execute_trade, cancel_order.
---   6. Views                - order book, active orders, order history,
---                             trader balances.
---   7. Background job       - fills resting limit orders once the simulated
---                             market price reaches them.
-
-SET search_path TO project, public;
-
--- Cash a buy order still holds in reserve: its remaining quantity at its
--- price, rounded to the 4 decimals of the balance columns. Defined once so
--- placing, filling, cancelling and checking all round the same way.
-CREATE OR REPLACE FUNCTION project.order_reservation(p_remaining numeric, p_price numeric)
-RETURNS numeric LANGUAGE sql IMMUTABLE AS $$
-    SELECT round(p_remaining * p_price, 4)
-$$;
-
--- Latest traded price of a market (the simulated market price).
-CREATE OR REPLACE FUNCTION project.latest_price(p_market_id uuid)
-RETURNS numeric LANGUAGE sql STABLE AS $$
-    SELECT price FROM project.market_trades
-     WHERE market_id = p_market_id
-     ORDER BY executed_at DESC, id DESC
-     LIMIT 1
-$$;
-
--- ============================================================================
--- 1. ORDER LIFECYCLE
--- ============================================================================
-
--- Automatic recording of order events (placement, fills, cancellation).
-CREATE TABLE project.order_events (
-    id           bigserial      PRIMARY KEY,
-    order_id     uuid           NOT NULL REFERENCES project.orders(id) ON DELETE CASCADE,
-    event_type   varchar(20)    NOT NULL
-                 CHECK (event_type IN ('placed', 'partially_filled', 'filled', 'cancelled')),
-    quantity     numeric(20,4)  NOT NULL,
-    price        numeric(18,6),
-    status_after varchar(20)    NOT NULL,
-    created_at   timestamptz    NOT NULL DEFAULT clock_timestamp()
-);
-
-CREATE INDEX idx_order_events_order ON project.order_events(order_id, id);
-
--- Status is not set by hand: it follows from how much has been filled.
---   filled = 0          -> open
---   0 < filled < qty    -> partially_filled
---   filled = qty        -> executed (executed_at set)
--- The only status that is set explicitly is 'cancelled', and only on an
--- active order. executed and cancelled orders are final. What was ordered
--- never changes. filled_quantity only changes when a trade fills the order
--- (the trade trigger sets a transaction-local flag while it does that).
-CREATE OR REPLACE FUNCTION project.trg_orders_lifecycle()
-RETURNS trigger LANGUAGE plpgsql AS $$
-DECLARE
-    v_derived varchar(20);
-BEGIN
-    IF TG_OP = 'UPDATE' THEN
-        IF OLD.status IN ('executed', 'cancelled') THEN
-            RAISE EXCEPTION 'order % is already % and cannot be processed again', OLD.id, OLD.status
-                USING ERRCODE = 'check_violation';
-        END IF;
-        IF (NEW.user_id, NEW.market_id, NEW.side, NEW.type, NEW.quantity, NEW.price, NEW.placed_at)
-           IS DISTINCT FROM
-           (OLD.user_id, OLD.market_id, OLD.side, OLD.type, OLD.quantity, OLD.price, OLD.placed_at) THEN
-            RAISE EXCEPTION 'user, market, side, type, quantity, price and placed_at of an order cannot change'
-                USING ERRCODE = 'check_violation';
-        END IF;
-        IF NEW.filled_quantity <> OLD.filled_quantity THEN
-            IF current_setting('eduberza.trade_fill', true) IS DISTINCT FROM 'on' THEN
-                RAISE EXCEPTION 'filled quantity of order % can only change through a trade', OLD.id
-                    USING ERRCODE = 'check_violation';
-            END IF;
-            IF NEW.filled_quantity < OLD.filled_quantity THEN
-                RAISE EXCEPTION 'filled quantity of order % cannot decrease', OLD.id
-                    USING ERRCODE = 'check_violation';
-            END IF;
-        END IF;
-        IF NEW.status = 'cancelled' AND OLD.status <> 'cancelled' THEN
-            IF NEW.filled_quantity <> OLD.filled_quantity THEN
-                RAISE EXCEPTION 'an order cannot be filled and cancelled in the same step'
-                    USING ERRCODE = 'check_violation';
-            END IF;
-            NEW.executed_at := NULL;
-            RETURN NEW;
-        END IF;
-    ELSE
-        IF NOT EXISTS (SELECT 1 FROM project.markets WHERE id = NEW.market_id AND is_active) THEN
-            RAISE EXCEPTION 'market % is not active; no new orders accepted', NEW.market_id
-                USING ERRCODE = 'check_violation';
-        END IF;
-        IF NEW.price IS NULL OR NEW.price <= 0 THEN
-            RAISE EXCEPTION 'an order needs a positive price (limit price, or the market price for a market order)'
-                USING ERRCODE = 'check_violation';
-        END IF;
-        IF NEW.status = 'cancelled' THEN
-            RAISE EXCEPTION 'an order cannot be created already cancelled'
-                USING ERRCODE = 'check_violation';
-        END IF;
-        -- A new order starts unfilled. The only exception is importing an
-        -- order that was completely executed in the past (sample data).
-        IF NEW.filled_quantity NOT IN (0, NEW.quantity) THEN
-            RAISE EXCEPTION 'a new order is either unfilled or (imported history) completely filled'
-                USING ERRCODE = 'check_violation';
-        END IF;
-    END IF;
-
-    v_derived := CASE
-        WHEN NEW.filled_quantity = 0               THEN 'open'
-        WHEN NEW.filled_quantity < NEW.quantity    THEN 'partially_filled'
-        ELSE 'executed'
-    END;
-    IF NEW.status IS DISTINCT FROM v_derived
-       AND (TG_OP = 'INSERT' OR NEW.status IS DISTINCT FROM OLD.status) THEN
-        RAISE EXCEPTION 'order status % does not match filled quantity % of %; status is derived automatically',
-            NEW.status, NEW.filled_quantity, NEW.quantity
-            USING ERRCODE = 'check_violation';
-    END IF;
-    NEW.status := v_derived;
-
-    IF v_derived = 'executed' THEN
-        NEW.executed_at := COALESCE(NEW.executed_at, now());
-    ELSE
-        NEW.executed_at := NULL;
-    END IF;
-    RETURN NEW;
-END $$;
-
-CREATE TRIGGER orders_lifecycle
-    BEFORE INSERT OR UPDATE ON project.orders
-    FOR EACH ROW EXECUTE FUNCTION project.trg_orders_lifecycle();
-
-CREATE OR REPLACE FUNCTION project.trg_orders_events()
-RETURNS trigger LANGUAGE plpgsql AS $$
-BEGIN
-    IF TG_OP = 'INSERT' THEN
-        INSERT INTO project.order_events (order_id, event_type, quantity, price, status_after)
-        VALUES (NEW.id, 'placed', NEW.quantity, NEW.price, NEW.status);
-    ELSIF NEW.filled_quantity > OLD.filled_quantity THEN
-        INSERT INTO project.order_events (order_id, event_type, quantity, price, status_after)
-        VALUES (NEW.id,
-                CASE WHEN NEW.status = 'executed' THEN 'filled' ELSE 'partially_filled' END,
-                NEW.filled_quantity - OLD.filled_quantity,
-                current_setting('eduberza.trade_price', true)::numeric,
-                NEW.status);
-    ELSIF NEW.status = 'cancelled' AND OLD.status <> 'cancelled' THEN
-        INSERT INTO project.order_events (order_id, event_type, quantity, price, status_after)
-        VALUES (NEW.id, 'cancelled', NEW.quantity - NEW.filled_quantity, NEW.price, NEW.status);
-    END IF;
-    RETURN NULL;
-END $$;
-
-CREATE TRIGGER orders_events
-    AFTER INSERT OR UPDATE ON project.orders
-    FOR EACH ROW EXECUTE FUNCTION project.trg_orders_events();
-
--- ============================================================================
--- 2. TRADE CONSISTENCY
--- ============================================================================
-
--- A trade that names orders must be possible for those orders:
---   * the buy_order_id is a buy order and the sell_order_id a sell order,
---   * both are on the trade's market and still active (open or partially
---     filled) - a cancelled or executed order can never trade again,
---   * the trade quantity does not exceed either order's remaining quantity,
---   * the price respects both limits (buy: price <= its limit, sell:
---     price >= its limit),
---   * the two orders belong to different users (no self-trade).
--- A trade with no orders at all is a simulated market tick (the bot).
-CREATE OR REPLACE FUNCTION project.trg_market_trades_validate()
-RETURNS trigger LANGUAGE plpgsql AS $$
-DECLARE
-    o     project.orders%ROWTYPE;
-    v_uid uuid;
-    v_id  uuid;
-    v_role varchar(4);
-BEGIN
-    IF NEW.buy_order_id IS NULL AND NEW.sell_order_id IS NULL THEN
-        RETURN NEW;
-    END IF;
-    IF NEW.buy_order_id IS NOT DISTINCT FROM NEW.sell_order_id THEN
-        RAISE EXCEPTION 'an order cannot trade with itself'
-            USING ERRCODE = 'check_violation';
-    END IF;
-
-    FOREACH v_role IN ARRAY ARRAY['buy', 'sell'] LOOP
-        v_id := CASE v_role WHEN 'buy' THEN NEW.buy_order_id ELSE NEW.sell_order_id END;
-        CONTINUE WHEN v_id IS NULL;
-
-        SELECT * INTO o FROM project.orders WHERE id = v_id FOR UPDATE;
-        IF o.side <> v_role THEN
-            RAISE EXCEPTION 'order % is a % order and cannot be the % side of a trade', v_id, o.side, v_role
-                USING ERRCODE = 'check_violation';
-        END IF;
-        IF o.market_id <> NEW.market_id THEN
-            RAISE EXCEPTION 'order % is on a different market than the trade', v_id
-                USING ERRCODE = 'check_violation';
-        END IF;
-        IF o.status NOT IN ('open', 'partially_filled') THEN
-            RAISE EXCEPTION 'order % is % and cannot trade', v_id, o.status
-                USING ERRCODE = 'check_violation';
-        END IF;
-        IF NEW.quantity > o.quantity - o.filled_quantity THEN
-            RAISE EXCEPTION 'trade quantity % exceeds the remaining quantity % of order %',
-                NEW.quantity, o.quantity - o.filled_quantity, v_id
-                USING ERRCODE = 'check_violation';
-        END IF;
-        IF v_uid IS NOT NULL AND v_uid = o.user_id THEN
-            RAISE EXCEPTION 'a user cannot trade with their own order'
-                USING ERRCODE = 'check_violation';
-        END IF;
-        IF v_role = 'buy' AND NEW.price > o.price OR v_role = 'sell' AND NEW.price < o.price THEN
-            RAISE EXCEPTION 'trade price % is outside the limit % of % order %', NEW.price, o.price, v_role, v_id
-                USING ERRCODE = 'check_violation';
-        END IF;
-        v_uid := o.user_id;
-    END LOOP;
-    RETURN NEW;
-END $$;
-
-CREATE TRIGGER market_trades_validate
-    BEFORE INSERT ON project.market_trades
-    FOR EACH ROW EXECUTE FUNCTION project.trg_market_trades_validate();
-
--- Recording a trade fills the orders it names; their status follows via
--- orders_lifecycle. The flag is what orders_lifecycle checks to allow the
--- change of filled_quantity.
-CREATE OR REPLACE FUNCTION project.trg_market_trades_fill()
-RETURNS trigger LANGUAGE plpgsql AS $$
-BEGIN
-    IF NEW.buy_order_id IS NULL AND NEW.sell_order_id IS NULL THEN
-        RETURN NULL;
-    END IF;
-    PERFORM set_config('eduberza.trade_fill', 'on', true);
-    PERFORM set_config('eduberza.trade_price', NEW.price::text, true);
-    UPDATE project.orders
-       SET filled_quantity = filled_quantity + NEW.quantity,
-           executed_at     = CASE WHEN filled_quantity + NEW.quantity = quantity
-                                  THEN NEW.executed_at END
-     WHERE id IN (NEW.buy_order_id, NEW.sell_order_id);
-    PERFORM set_config('eduberza.trade_fill', 'off', true);
-    RETURN NULL;
-END $$;
-
-CREATE TRIGGER market_trades_fill
-    AFTER INSERT ON project.market_trades
-    FOR EACH ROW EXECUTE FUNCTION project.trg_market_trades_fill();
-
--- Trades are history: they are never changed or removed (the fills and the
--- money they moved would no longer match).
-CREATE OR REPLACE FUNCTION project.trg_market_trades_immutable()
-RETURNS trigger LANGUAGE plpgsql AS $$
-BEGIN
-    IF OLD.buy_order_id IS NULL AND OLD.sell_order_id IS NULL THEN
-        -- a simulated tick filled no order; allow it unless it is being
-        -- turned into one that did
-        IF TG_OP = 'DELETE' THEN
-            RETURN OLD;
-        ELSIF NEW.buy_order_id IS NULL AND NEW.sell_order_id IS NULL THEN
-            RETURN NEW;
-        END IF;
-    END IF;
-    RAISE EXCEPTION 'a trade that filled orders cannot be changed or deleted'
-        USING ERRCODE = 'check_violation';
-END $$;
-
-CREATE TRIGGER market_trades_immutable
-    BEFORE UPDATE OR DELETE ON project.market_trades
-    FOR EACH ROW EXECUTE FUNCTION project.trg_market_trades_immutable();
-
--- ============================================================================
--- 3. RESERVATIONS AND BALANCES  (deferred: checked at COMMIT)
--- ============================================================================
--- Placing, filling and cancelling an order each change several rows in
--- separate statements, and in between the rows disagree. Only the state at
--- COMMIT must be consistent, so these are DEFERRABLE INITIALLY DEFERRED
--- constraint triggers: a transaction that leaves any of them broken is
--- rolled back as a whole.
-
--- 3a. reserved_balance = what the user's active buy orders still reserve.
-CREATE OR REPLACE FUNCTION project.trg_reserved_cash_matches_orders()
-RETURNS trigger LANGUAGE plpgsql AS $$
-DECLARE
-    v_user     uuid := CASE WHEN TG_TABLE_NAME = 'users' THEN NEW.id END;
-    v_reserved numeric;
-    v_needed   numeric;
-BEGIN
-    IF TG_TABLE_NAME = 'orders' THEN
-        v_user := NEW.user_id;
-    END IF;
-    SELECT reserved_balance INTO v_reserved FROM project.users WHERE id = v_user;
-    IF NOT FOUND THEN
-        RETURN NULL;
-    END IF;
-    SELECT COALESCE(SUM(project.order_reservation(quantity - filled_quantity, price)), 0)
-      INTO v_needed
-      FROM project.orders
-     WHERE user_id = v_user AND side = 'buy' AND status IN ('open', 'partially_filled');
-    IF v_reserved <> v_needed THEN
-        RAISE EXCEPTION 'reserved balance % does not match the % needed by active buy orders (user %)',
-            v_reserved, v_needed, v_user
-            USING ERRCODE = 'check_violation', CONSTRAINT = 'reserved_cash_matches_orders';
-    END IF;
-    RETURN NULL;
-END $$;
-
--- 3b. holdings.reserved_quantity = what the user's active sell orders for
---     that crypto still have to deliver.
-CREATE OR REPLACE FUNCTION project.trg_reserved_crypto_matches_orders()
-RETURNS trigger LANGUAGE plpgsql AS $$
-DECLARE
-    v_user     uuid;
-    v_crypto   uuid;
-    v_reserved numeric;
-    v_needed   numeric;
-BEGIN
-    IF TG_TABLE_NAME = 'holdings' THEN
-        v_user := NEW.user_id;
-        v_crypto := NEW.crypto_id;
-    ELSE
-        IF NEW.side <> 'sell' THEN
-            RETURN NULL;
-        END IF;
-        v_user := NEW.user_id;
-        SELECT crypto_id INTO v_crypto FROM project.markets WHERE id = NEW.market_id;
-    END IF;
-    SELECT COALESCE(SUM(reserved_quantity), 0) INTO v_reserved
-      FROM project.holdings WHERE user_id = v_user AND crypto_id = v_crypto;
-    SELECT COALESCE(SUM(o.quantity - o.filled_quantity), 0) INTO v_needed
-      FROM project.orders o
-      JOIN project.markets m ON m.id = o.market_id
-     WHERE o.user_id = v_user AND m.crypto_id = v_crypto
-       AND o.side = 'sell' AND o.status IN ('open', 'partially_filled');
-    IF v_reserved <> v_needed THEN
-        RAISE EXCEPTION 'reserved quantity % does not match the % needed by active sell orders (user %, crypto %)',
-            v_reserved, v_needed, v_user, v_crypto
-            USING ERRCODE = 'check_violation', CONSTRAINT = 'reserved_crypto_matches_orders';
-    END IF;
-    RETURN NULL;
-END $$;
-
--- 3c. available_balance + reserved_balance = sum of the user's ledger.
---     Reserving only moves cash between the two columns; money actually
---     leaves or arrives only with a ledger row (deposit, buy fill, sell fill).
-CREATE OR REPLACE FUNCTION project.trg_cash_matches_ledger()
-RETURNS trigger LANGUAGE plpgsql AS $$
-DECLARE
-    v_user   uuid;
-    v_cash   numeric;
-    v_ledger numeric;
-BEGIN
-    IF TG_TABLE_NAME = 'users' THEN
-        v_user := NEW.id;
-    ELSIF TG_OP = 'DELETE' THEN
-        v_user := OLD.user_id;
-    ELSE
-        v_user := NEW.user_id;
-    END IF;
-    SELECT available_balance + reserved_balance INTO v_cash FROM project.users WHERE id = v_user;
-    IF NOT FOUND THEN
-        RETURN NULL;
-    END IF;
-    SELECT COALESCE(SUM(amount), 0) INTO v_ledger FROM project.transactions WHERE user_id = v_user;
-    IF v_cash <> v_ledger THEN
-        RAISE EXCEPTION 'cash % (available + reserved) does not match the ledger total % (user %)',
-            v_cash, v_ledger, v_user
-            USING ERRCODE = 'check_violation', CONSTRAINT = 'cash_matches_ledger';
-    END IF;
-    RETURN NULL;
-END $$;
-
-CREATE INDEX idx_orders_active ON project.orders (user_id, side)
-    WHERE status IN ('open', 'partially_filled');
-
-CREATE CONSTRAINT TRIGGER reserved_cash_matches_orders
-    AFTER INSERT OR UPDATE OF reserved_balance ON project.users
-    DEFERRABLE INITIALLY DEFERRED
-    FOR EACH ROW EXECUTE FUNCTION project.trg_reserved_cash_matches_orders();
-CREATE CONSTRAINT TRIGGER reserved_cash_matches_orders
-    AFTER INSERT OR UPDATE OF status, filled_quantity ON project.orders
-    DEFERRABLE INITIALLY DEFERRED
-    FOR EACH ROW EXECUTE FUNCTION project.trg_reserved_cash_matches_orders();
-
-CREATE CONSTRAINT TRIGGER reserved_crypto_matches_orders
-    AFTER INSERT OR UPDATE OF reserved_quantity ON project.holdings
-    DEFERRABLE INITIALLY DEFERRED
-    FOR EACH ROW EXECUTE FUNCTION project.trg_reserved_crypto_matches_orders();
-CREATE CONSTRAINT TRIGGER reserved_crypto_matches_orders
-    AFTER INSERT OR UPDATE OF status, filled_quantity ON project.orders
-    DEFERRABLE INITIALLY DEFERRED
-    FOR EACH ROW EXECUTE FUNCTION project.trg_reserved_crypto_matches_orders();
-
-CREATE CONSTRAINT TRIGGER cash_matches_ledger
-    AFTER INSERT OR UPDATE OF available_balance, reserved_balance ON project.users
-    DEFERRABLE INITIALLY DEFERRED
-    FOR EACH ROW EXECUTE FUNCTION project.trg_cash_matches_ledger();
-CREATE CONSTRAINT TRIGGER cash_matches_ledger
-    AFTER INSERT OR UPDATE OR DELETE ON project.transactions
-    DEFERRABLE INITIALLY DEFERRED
-    FOR EACH ROW EXECUTE FUNCTION project.trg_cash_matches_ledger();
-
--- ============================================================================
--- 5. OPERATIONS
--- ============================================================================
-
--- execute_trade: one trade of p_quantity at p_price between a buy order and
--- a sell order. Either side may be NULL, meaning the simulated market is the
--- counterparty. Inserting the trade row validates it and fills the orders
--- (triggers above); this function moves the money and the crypto:
---   buyer:  reservation released for the filled part, the actual cost paid
---           (any difference to the limit price goes back to available),
---           crypto added to the holding at a running weighted average,
---           'buy' ledger row.
---   seller: reserved crypto delivered out of the holding, proceeds credited,
---           cost basis removed from invested_balance, 'sell' ledger row.
-CREATE OR REPLACE FUNCTION project.execute_trade(
-    p_buy_order uuid, p_sell_order uuid, p_quantity numeric, p_price numeric,
-    p_aggressor varchar DEFAULT NULL)
-RETURNS bigint LANGUAGE plpgsql AS $$
-DECLARE
-    b          project.orders%ROWTYPE;
-    s          project.orders%ROWTYPE;
-    v_market   project.markets%ROWTYPE;
-    v_trade_id bigint;
-    v_release  numeric;
-    v_cost     numeric;
-    v_proceeds numeric;
-    v_avg      numeric;
-BEGIN
-    IF p_buy_order IS NULL AND p_sell_order IS NULL THEN
-        RAISE EXCEPTION 'a trade needs at least one order' USING ERRCODE = 'check_violation';
-    END IF;
-
-    -- lock both orders in a fixed order (by id) so two concurrent trades on
-    -- the same pair of orders cannot deadlock
-    PERFORM 1 FROM project.orders WHERE id IN (p_buy_order, p_sell_order) ORDER BY id FOR UPDATE;
-    SELECT * INTO b FROM project.orders WHERE id = p_buy_order;
-    SELECT * INTO s FROM project.orders WHERE id = p_sell_order;
-    SELECT * INTO v_market FROM project.markets WHERE id = COALESCE(b.market_id, s.market_id);
-
-    INSERT INTO project.market_trades
-           (market_id, executed_at, price, quantity, side, source, buy_order_id, sell_order_id)
-    VALUES (v_market.id, now(), p_price, p_quantity,
-            COALESCE(p_aggressor, CASE WHEN p_sell_order IS NULL THEN 'buy' ELSE 'sell' END),
-            CASE WHEN p_buy_order IS NOT NULL AND p_sell_order IS NOT NULL THEN 'match' ELSE 'market' END,
-            p_buy_order, p_sell_order)
-    RETURNING id INTO v_trade_id;
-
-    IF p_buy_order IS NOT NULL THEN
-        v_release := project.order_reservation(b.quantity - b.filled_quantity, b.price)
-                   - project.order_reservation(b.quantity - b.filled_quantity - p_quantity, b.price);
-        v_cost    := LEAST(round(p_quantity * p_price, 4), v_release);
-
-        UPDATE project.users
-           SET reserved_balance  = reserved_balance  - v_release,
-               available_balance = available_balance + (v_release - v_cost),
-               invested_balance  = invested_balance  + v_cost,
-               updated_at        = now()
-         WHERE id = b.user_id;
-
-        INSERT INTO project.holdings AS h (user_id, crypto_id, quantity, avg_price, updated_at)
-        VALUES (b.user_id, v_market.crypto_id, p_quantity, p_price, now())
-        ON CONFLICT (user_id, crypto_id) DO UPDATE
-           SET avg_price  = (h.quantity * h.avg_price + EXCLUDED.quantity * EXCLUDED.avg_price)
-                            / (h.quantity + EXCLUDED.quantity),
-               quantity   = h.quantity + EXCLUDED.quantity,
-               updated_at = now();
-
-        INSERT INTO project.transactions (user_id, type, amount, currency, related_order, description)
-        VALUES (b.user_id, 'buy', -v_cost, v_market.quote_currency, b.id,
-                format('Buy %s @ %s (trade %s)', p_quantity, p_price, v_trade_id));
-    END IF;
-
-    IF p_sell_order IS NOT NULL THEN
-        SELECT avg_price INTO v_avg FROM project.holdings
-         WHERE user_id = s.user_id AND crypto_id = v_market.crypto_id FOR UPDATE;
-
-        UPDATE project.holdings
-           SET quantity          = quantity - p_quantity,
-               reserved_quantity = reserved_quantity - p_quantity,
-               updated_at        = now()
-         WHERE user_id = s.user_id AND crypto_id = v_market.crypto_id;
-
-        v_proceeds := round(p_quantity * p_price, 4);
-        UPDATE project.users
-           SET available_balance = available_balance + v_proceeds,
-               invested_balance  = GREATEST(invested_balance - round(p_quantity * v_avg, 4), 0),
-               updated_at        = now()
-         WHERE id = s.user_id;
-
-        INSERT INTO project.transactions (user_id, type, amount, currency, related_order, description)
-        VALUES (s.user_id, 'sell', v_proceeds, v_market.quote_currency, s.id,
-                format('Sell %s @ %s (trade %s)', p_quantity, p_price, v_trade_id));
-    END IF;
-
-    RETURN v_trade_id;
-END $$;
-
--- match_order: trade an order against the opposite side of the order book -
--- other users' active limit orders on the same market whose price is
--- acceptable - best price first, then oldest first (price-time priority).
--- Each trade is at the resting order's price. Returns the number of trades.
-CREATE OR REPLACE FUNCTION project.match_order(p_order_id uuid)
-RETURNS int LANGUAGE plpgsql AS $$
-DECLARE
-    o       project.orders%ROWTYPE;
-    r       record;
-    v_rem   numeric;
-    v_qty   numeric;
-    v_count int := 0;
-BEGIN
-    SELECT * INTO o FROM project.orders WHERE id = p_order_id FOR UPDATE;
-    FOR r IN
-        SELECT id, price, quantity - filled_quantity AS remaining
-          FROM project.orders
-         WHERE market_id = o.market_id
-           AND side <> o.side
-           AND type = 'limit'
-           AND status IN ('open', 'partially_filled')
-           AND user_id <> o.user_id
-           AND (o.side = 'buy'  AND price <= o.price
-             OR o.side = 'sell' AND price >= o.price)
-         ORDER BY CASE WHEN o.side = 'buy'  THEN price END ASC,
-                  CASE WHEN o.side = 'sell' THEN price END DESC,
-                  placed_at, id
-           FOR UPDATE
-    LOOP
-        SELECT quantity - filled_quantity INTO v_rem FROM project.orders WHERE id = p_order_id;
-        EXIT WHEN v_rem = 0;
-        v_qty := LEAST(v_rem, r.remaining);
-        IF o.side = 'buy' THEN
-            PERFORM project.execute_trade(o.id, r.id, v_qty, r.price, 'buy');
-        ELSE
-            PERFORM project.execute_trade(r.id, o.id, v_qty, r.price, 'sell');
-        END IF;
-        v_count := v_count + 1;
-    END LOOP;
-    RETURN v_count;
-END $$;
-
--- place_order: the one correct way to place an order.
---   1. checks the market, side, type, quantity and price;
---   2. locks the user and reserves what the order commits: cash
---      (remaining x price) for a buy, crypto for a sell - refusing the order
---      if not enough is free;
---   3. records the order (open);
---   4. matches it against the order book (match_order);
---   5. whatever is still unfilled and is marketable at the current market
---      price trades with the simulated market. A market order is priced at
---      the current market price, so it always fills completely here; a limit
---      order that is not marketable stays in the book.
--- Returns the order id.
-CREATE OR REPLACE FUNCTION project.place_order(
-    p_user_id uuid, p_market_id uuid, p_side varchar, p_type varchar,
-    p_quantity numeric, p_limit_price numeric DEFAULT NULL)
-RETURNS uuid LANGUAGE plpgsql AS $$
-DECLARE
-    v_market    numeric := project.latest_price(p_market_id);
-    v_price     numeric;
-    v_available numeric;
-    v_free      numeric;
-    v_crypto    uuid;
-    v_order     uuid;
-    v_rem       numeric;
-BEGIN
-    IF p_side NOT IN ('buy', 'sell') OR p_type NOT IN ('market', 'limit') THEN
-        RAISE EXCEPTION 'invalid side % or type %', p_side, p_type USING ERRCODE = 'check_violation';
-    END IF;
-    IF p_quantity IS NULL OR p_quantity <= 0 THEN
-        RAISE EXCEPTION 'quantity must be positive' USING ERRCODE = 'check_violation';
-    END IF;
-    IF p_type = 'limit' THEN
-        IF p_limit_price IS NULL OR p_limit_price <= 0 THEN
-            RAISE EXCEPTION 'a limit order needs a positive limit price' USING ERRCODE = 'check_violation';
-        END IF;
-        v_price := p_limit_price;
-    ELSE
-        IF v_market IS NULL THEN
-            RAISE EXCEPTION 'market has no price yet' USING ERRCODE = 'check_violation';
-        END IF;
-        v_price := v_market;
-    END IF;
-
-    SELECT available_balance INTO v_available FROM project.users WHERE id = p_user_id FOR UPDATE;
-    IF NOT FOUND THEN
-        RAISE EXCEPTION 'user % does not exist', p_user_id USING ERRCODE = 'no_data_found';
-    END IF;
-
-    IF p_side = 'buy' THEN
-        IF v_available < project.order_reservation(p_quantity, v_price) THEN
-            RAISE EXCEPTION 'insufficient funds: the order needs %, available %',
-                project.order_reservation(p_quantity, v_price), v_available
-                USING ERRCODE = 'check_violation';
-        END IF;
-        UPDATE project.users
-           SET available_balance = available_balance - project.order_reservation(p_quantity, v_price),
-               reserved_balance  = reserved_balance  + project.order_reservation(p_quantity, v_price),
-               updated_at        = now()
-         WHERE id = p_user_id;
-    ELSE
-        SELECT crypto_id INTO v_crypto FROM project.markets WHERE id = p_market_id;
-        SELECT quantity - reserved_quantity INTO v_free FROM project.holdings
-         WHERE user_id = p_user_id AND crypto_id = v_crypto FOR UPDATE;
-        IF COALESCE(v_free, 0) < p_quantity THEN
-            RAISE EXCEPTION 'insufficient holding: trying to sell %, free to sell %', p_quantity, COALESCE(v_free, 0)
-                USING ERRCODE = 'check_violation';
-        END IF;
-        UPDATE project.holdings
-           SET reserved_quantity = reserved_quantity + p_quantity, updated_at = now()
-         WHERE user_id = p_user_id AND crypto_id = v_crypto;
-    END IF;
-
-    INSERT INTO project.orders (user_id, market_id, side, type, status, quantity, price)
-    VALUES (p_user_id, p_market_id, p_side, p_type, 'open', p_quantity, v_price)
-    RETURNING id INTO v_order;
-
-    PERFORM project.match_order(v_order);
-
-    SELECT quantity - filled_quantity INTO v_rem FROM project.orders WHERE id = v_order;
-    IF v_rem > 0 AND v_market IS NOT NULL
-       AND (p_side = 'buy' AND v_market <= v_price OR p_side = 'sell' AND v_market >= v_price) THEN
-        IF p_side = 'buy' THEN
-            PERFORM project.execute_trade(v_order, NULL, v_rem, v_market, 'buy');
-        ELSE
-            PERFORM project.execute_trade(NULL, v_order, v_rem, v_market, 'sell');
-        END IF;
-    END IF;
-    RETURN v_order;
-END $$;
-
--- cancel_order: cancels an active order and releases what it still
--- reserves. p_user_id = NULL is a system cancellation (no ownership check).
-CREATE OR REPLACE FUNCTION project.cancel_order(p_order_id uuid, p_user_id uuid DEFAULT NULL)
-RETURNS void LANGUAGE plpgsql AS $$
-DECLARE
-    o     project.orders%ROWTYPE;
-    v_rem numeric;
-BEGIN
-    SELECT * INTO o FROM project.orders WHERE id = p_order_id FOR UPDATE;
-    IF NOT FOUND THEN
-        RAISE EXCEPTION 'order % does not exist', p_order_id USING ERRCODE = 'no_data_found';
-    END IF;
-    IF p_user_id IS NOT NULL AND o.user_id <> p_user_id THEN
-        RAISE EXCEPTION 'order % does not belong to this user', p_order_id
-            USING ERRCODE = 'insufficient_privilege';
-    END IF;
-    IF o.status NOT IN ('open', 'partially_filled') THEN
-        RAISE EXCEPTION 'order % is % and cannot be cancelled', p_order_id, o.status
-            USING ERRCODE = 'check_violation';
-    END IF;
-
-    v_rem := o.quantity - o.filled_quantity;
-    IF o.side = 'buy' THEN
-        UPDATE project.users
-           SET reserved_balance  = reserved_balance  - project.order_reservation(v_rem, o.price),
-               available_balance = available_balance + project.order_reservation(v_rem, o.price),
-               updated_at        = now()
-         WHERE id = o.user_id;
-    ELSE
-        UPDATE project.holdings h
-           SET reserved_quantity = h.reserved_quantity - v_rem, updated_at = now()
-          FROM project.markets m
-         WHERE m.id = o.market_id AND h.user_id = o.user_id AND h.crypto_id = m.crypto_id;
-    END IF;
-
-    UPDATE project.orders SET status = 'cancelled' WHERE id = p_order_id;
-END $$;
-
--- ============================================================================
--- 6. VIEWS
--- ============================================================================
-
--- Active orders with what they still need and what they hold in reserve.
-CREATE VIEW project.v_active_orders AS
-SELECT o.id            AS order_id,
-       o.user_id,
-       u.username,
-       o.market_id,
-       c.symbol,
-       m.quote_currency,
-       o.side,
-       o.type,
-       o.status,
-       o.quantity,
-       o.filled_quantity,
-       o.quantity - o.filled_quantity AS remaining,
-       o.price,
-       CASE WHEN o.side = 'buy'
-            THEN project.order_reservation(o.quantity - o.filled_quantity, o.price) ELSE 0 END AS reserved_cash,
-       CASE WHEN o.side = 'sell' THEN o.quantity - o.filled_quantity ELSE 0 END             AS reserved_crypto,
-       o.placed_at
-FROM project.orders o
-JOIN project.users   u ON u.id = o.user_id
-JOIN project.markets m ON m.id = o.market_id
-JOIN project.crypto  c ON c.id = m.crypto_id
-WHERE o.status IN ('open', 'partially_filled');
-
--- Current order book: resting limit orders aggregated per price level.
-CREATE VIEW project.v_order_book AS
-SELECT market_id,
-       symbol,
-       quote_currency,
-       side,
-       price,
-       SUM(remaining) AS quantity,
-       COUNT(*)       AS orders
-FROM project.v_active_orders
-WHERE type = 'limit'
-GROUP BY market_id, symbol, quote_currency, side, price;
-
--- Every order with its fill progress and average fill price from its trades.
-CREATE VIEW project.v_order_history AS
-SELECT o.id            AS order_id,
-       o.user_id,
-       u.username,
-       c.symbol,
-       o.side,
-       o.type,
-       o.status,
-       o.quantity,
-       o.filled_quantity,
-       o.quantity - o.filled_quantity AS remaining,
-       o.price,
-       f.trades,
-       f.avg_fill_price,
-       o.placed_at,
-       o.executed_at
-FROM project.orders o
-JOIN project.users   u ON u.id = o.user_id
-JOIN project.markets m ON m.id = o.market_id
-JOIN project.crypto  c ON c.id = m.crypto_id
-LEFT JOIN LATERAL (
-    SELECT COUNT(*) AS trades,
-           round(SUM(t.quantity * t.price) / NULLIF(SUM(t.quantity), 0), 6) AS avg_fill_price
-      FROM (SELECT quantity, price FROM project.market_trades WHERE buy_order_id  = o.id
-            UNION ALL
-            SELECT quantity, price FROM project.market_trades WHERE sell_order_id = o.id) t
-) f ON true;
-
--- Trader balances: cash split into free and reserved, the ledger it must
--- equal, holdings at market value, and net worth.
-CREATE VIEW project.v_trader_balances AS
-SELECT u.id                         AS user_id,
-       u.username,
-       u.available_balance,
-       u.reserved_balance,
-       u.available_balance + u.reserved_balance              AS total_cash,
-       COALESCE(l.ledger_total, 0)                           AS ledger_total,
-       u.invested_balance,
-       COALESCE(p.holdings_value, 0)                         AS holdings_value,
-       u.available_balance + u.reserved_balance + COALESCE(p.holdings_value, 0) AS net_worth
-FROM project.users u
-LEFT JOIN (SELECT user_id, SUM(amount) AS ledger_total
-             FROM project.transactions GROUP BY user_id) l ON l.user_id = u.id
-LEFT JOIN (SELECT user_id, SUM(market_value) AS holdings_value
-             FROM project.v_portfolio GROUP BY user_id) p ON p.user_id = u.id;
-
--- ============================================================================
--- 7. BACKGROUND JOB
--- ============================================================================
-
--- EduBerza's market price moves with the simulator (bots/main.go), not with
--- user orders. A resting limit order - buy at or above, or sell at or below,
--- the current market price - must then be filled by the simulated market,
--- just as it would have been had the price already been there when it was
--- placed. Nothing else happens at that moment that a trigger could react to
--- (the bot's price ticks deliberately stay cheap inserts), so this runs as a
--- periodic job: the bot process calls it after every round of price ticks.
--- The faculty server has no pg_cron, so the scheduling is done there.
--- An advisory lock keeps two runs from filling the same orders twice;
--- SKIP LOCKED leaves any order a user is cancelling right now for next time.
--- Returns the number of orders filled.
-CREATE OR REPLACE FUNCTION project.fill_marketable_orders()
-RETURNS int LANGUAGE plpgsql AS $$
-DECLARE
-    r       record;
-    v_count int := 0;
-BEGIN
-    IF NOT pg_try_advisory_xact_lock(hashtext('project.fill_marketable_orders')) THEN
-        RETURN 0;
-    END IF;
-    FOR r IN
-        SELECT o.id, o.side, o.quantity - o.filled_quantity AS remaining, lp.price AS market_price
-          FROM project.orders o
-          JOIN project.markets m ON m.id = o.market_id AND m.is_active
-          CROSS JOIN LATERAL (SELECT project.latest_price(o.market_id) AS price) lp
-         WHERE o.type = 'limit'
-           AND o.status IN ('open', 'partially_filled')
-           AND (o.side = 'buy'  AND o.price >= lp.price
-             OR o.side = 'sell' AND o.price <= lp.price)
-         ORDER BY o.placed_at, o.id
-           FOR UPDATE OF o SKIP LOCKED
-    LOOP
-        IF r.side = 'buy' THEN
-            PERFORM project.execute_trade(r.id, NULL, r.remaining, r.market_price, 'sell');
-        ELSE
-            PERFORM project.execute_trade(NULL, r.id, r.remaining, r.market_price, 'buy');
-        END IF;
-        v_count := v_count + 1;
-    END LOOP;
-    RETURN v_count;
-END $$;
Index: rver/db/advanced_db_tests.sql
===================================================================
--- server/db/advanced_db_tests.sql	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,270 +1,0 @@
--- advanced_db_tests.sql
--- EduBerza - tests for the P7 rules in advanced_db.sql
---
--- Run right after data_load.sql (seed state: alice 8250 USD + 0.5 ETH,
--- bob 5000 USD, charlie 2500 USD, ETH/USD last traded at 3520).
--- It plays a short trading story and, along the way, tries to break every
--- rule. Each check prints PASS/FAIL as a NOTICE; everything is rolled back
--- at the end, so the data is left exactly as it was.
---
--- The reservation and balance checks are deferred to COMMIT; the tests force
--- them with SET CONSTRAINTS ALL IMMEDIATE so a violation shows up inside the
--- test instead of at the final COMMIT.
-
-BEGIN;
-SET search_path TO project, public;
-
-CREATE TEMP TABLE test_results (name text, passed boolean) ON COMMIT DROP;
-CREATE TEMP TABLE ids (name text PRIMARY KEY, id uuid) ON COMMIT DROP;
-
--- expect_error: run p_sql (and the deferred checks); it must fail with an
--- error containing p_fragment.
-CREATE PROCEDURE pg_temp.expect_error(p_name text, p_sql text, p_fragment text)
-LANGUAGE plpgsql AS $$
-BEGIN
-    BEGIN
-        EXECUTE p_sql;
-        SET CONSTRAINTS ALL IMMEDIATE;
-        RAISE NOTICE 'FAIL  %: no error raised', p_name;
-        INSERT INTO test_results VALUES (p_name, false);
-    EXCEPTION WHEN OTHERS THEN
-        IF SQLERRM ILIKE '%' || p_fragment || '%' THEN
-            RAISE NOTICE 'PASS  %: %', p_name, SQLERRM;
-            INSERT INTO test_results VALUES (p_name, true);
-        ELSE
-            RAISE NOTICE 'FAIL  %: unexpected error: %', p_name, SQLERRM;
-            INSERT INTO test_results VALUES (p_name, false);
-        END IF;
-    END;
-    SET CONSTRAINTS ALL DEFERRED;
-END $$;
-
-CREATE FUNCTION pg_temp.expect_true(p_name text, p_ok boolean, p_detail text DEFAULT '')
-RETURNS void LANGUAGE plpgsql AS $$
-BEGIN
-    RAISE NOTICE '%  %: %', CASE WHEN coalesce(p_ok, false) THEN 'PASS' ELSE 'FAIL' END, p_name, p_detail;
-    INSERT INTO test_results VALUES (p_name, coalesce(p_ok, false));
-END $$;
-
--- consistent: all deferred checks pass right now
-CREATE FUNCTION pg_temp.consistent() RETURNS boolean LANGUAGE plpgsql AS $$
-BEGIN
-    SET CONSTRAINTS ALL IMMEDIATE;
-    SET CONSTRAINTS ALL DEFERRED;
-    RETURN true;
-EXCEPTION WHEN OTHERS THEN
-    RAISE NOTICE '      consistency check failed: %', SQLERRM;
-    RETURN false;
-END $$;
-
-CREATE FUNCTION pg_temp.uid(p_name text) RETURNS uuid LANGUAGE sql AS $$
-    SELECT id FROM users WHERE username = p_name
-$$;
-CREATE FUNCTION pg_temp.oid(p_name text) RETURNS uuid LANGUAGE sql AS $$
-    SELECT id FROM ids WHERE name = p_name
-$$;
-
-
--- ===========================================================================
--- A. Placing orders reserves what they commit
--- ===========================================================================
-INSERT INTO ids VALUES ('alice_ask',
-    place_order(pg_temp.uid('alice'), 'a2222222-2222-2222-2222-222222222222', 'sell', 'limit', 0.3, 3600));
-INSERT INTO ids VALUES ('bob_bid',
-    place_order(pg_temp.uid('bob'), 'a2222222-2222-2222-2222-222222222222', 'buy', 'limit', 0.1, 3500));
-
-SELECT pg_temp.expect_true('place: limit sell above the market rests in the book, crypto reserved',
-    (SELECT status FROM orders WHERE id = pg_temp.oid('alice_ask')) = 'open'
-    AND (SELECT reserved_quantity FROM holdings WHERE user_id = pg_temp.uid('alice')) = 0.3,
-    'alice ETH reserved = ' || (SELECT reserved_quantity FROM holdings WHERE user_id = pg_temp.uid('alice')));
-SELECT pg_temp.expect_true('place: limit buy below the market rests in the book, cash reserved',
-    (SELECT (available_balance, reserved_balance) FROM users WHERE username = 'bob') = (4650.0000, 350.0000),
-    (SELECT format('bob available %s reserved %s', available_balance, reserved_balance) FROM users WHERE username = 'bob'));
-SELECT pg_temp.expect_true('event: placement recorded automatically',
-    (SELECT count(*) FROM order_events WHERE order_id IN (pg_temp.oid('alice_ask'), pg_temp.oid('bob_bid'))
-      AND event_type = 'placed') = 2);
-SELECT pg_temp.expect_true('view: order book shows both price levels',
-    (SELECT string_agg(side || ' ' || quantity || ' @ ' || price, ', ' ORDER BY side)
-       FROM v_order_book WHERE symbol = 'ETH') = 'buy 0.1000 @ 3500.000000, sell 0.3000 @ 3600.000000',
-    (SELECT string_agg(side || ' ' || quantity || ' @ ' || price, ', ' ORDER BY side) FROM v_order_book WHERE symbol = 'ETH'));
-SELECT pg_temp.expect_true('consistency holds after placing', pg_temp.consistent());
-
-CALL pg_temp.expect_error('place: buy without enough free cash',
-    $q$SELECT place_order(pg_temp.uid('charlie'), 'a1111111-1111-1111-1111-111111111111', 'buy', 'limit', 1, 60000)$q$,
-    'insufficient funds');
-CALL pg_temp.expect_error('place: sell more than is free (0.2 of 0.5 is already reserved)',
-    $q$SELECT place_order(pg_temp.uid('alice'), 'a2222222-2222-2222-2222-222222222222', 'sell', 'limit', 0.3, 3600)$q$,
-    'insufficient holding');
-
--- ===========================================================================
--- B. A trade between two compatible orders, partial fill
--- ===========================================================================
--- bob bids 0.2 at 3650: crosses alice's ask at 3600 -> trade 0.2 @ 3600
-INSERT INTO ids VALUES ('bob_bid2',
-    place_order(pg_temp.uid('bob'), 'a2222222-2222-2222-2222-222222222222', 'buy', 'limit', 0.2, 3650));
-
-SELECT pg_temp.expect_true('match: trade between the two orders at the resting price',
-    EXISTS (SELECT 1 FROM market_trades
-             WHERE buy_order_id = pg_temp.oid('bob_bid2') AND sell_order_id = pg_temp.oid('alice_ask')
-               AND quantity = 0.2 AND price = 3600 AND source = 'match'));
-SELECT pg_temp.expect_true('status: seller partially filled, buyer executed (automatic)',
-    (SELECT status || ' ' || filled_quantity FROM orders WHERE id = pg_temp.oid('alice_ask')) = 'partially_filled 0.2000'
-    AND (SELECT status FROM orders WHERE id = pg_temp.oid('bob_bid2')) = 'executed',
-    (SELECT format('alice_ask %s %s/%s', status, filled_quantity, quantity) FROM orders WHERE id = pg_temp.oid('alice_ask')));
-SELECT pg_temp.expect_true('money: buyer paid 720, got back the 10 reserved above the trade price',
-    (SELECT (available_balance, reserved_balance) FROM users WHERE username = 'bob') = (3930.0000, 350.0000),
-    (SELECT format('bob available %s reserved %s', available_balance, reserved_balance) FROM users WHERE username = 'bob'));
-SELECT pg_temp.expect_true('crypto: 0.2 ETH moved from alice (0.1 still reserved) to bob',
-    (SELECT (quantity, reserved_quantity) FROM holdings WHERE user_id = pg_temp.uid('alice')) = (0.3000, 0.1000)
-    AND (SELECT (quantity, avg_price) FROM holdings WHERE user_id = pg_temp.uid('bob')) = (0.2000, 3600.000000));
-SELECT pg_temp.expect_true('ledger: one buy and one sell row, linked to the orders',
-    (SELECT count(*) FROM transactions WHERE related_order IN (pg_temp.oid('bob_bid2'), pg_temp.oid('alice_ask'))) = 2);
-SELECT pg_temp.expect_true('event: fills recorded automatically',
-    (SELECT string_agg(event_type, ',' ORDER BY id) FROM order_events WHERE order_id = pg_temp.oid('alice_ask'))
-        = 'placed,partially_filled');
-SELECT pg_temp.expect_true('consistency holds after the trade', pg_temp.consistent());
-
--- ===========================================================================
--- C. Market order: book first, then the simulated market
--- ===========================================================================
--- market price is now 3600 (last trade); charlie buys 0.15 at market:
--- 0.1 from alice's remaining ask @ 3600, the other 0.05 from the market @ 3600
-INSERT INTO ids VALUES ('charlie_mkt',
-    place_order(pg_temp.uid('charlie'), 'a2222222-2222-2222-2222-222222222222', 'buy', 'market', 0.15));
-
-SELECT pg_temp.expect_true('market order: filled completely in two trades',
-    (SELECT (status, trades, avg_fill_price) FROM v_order_history WHERE order_id = pg_temp.oid('charlie_mkt'))
-        = ('executed'::varchar, 2::bigint, 3600.000000::numeric),
-    (SELECT format('%s, %s trades, avg %s', status, trades, avg_fill_price) FROM v_order_history WHERE order_id = pg_temp.oid('charlie_mkt')));
-SELECT pg_temp.expect_true('market order: alice''s ask is now executed, nothing left reserved',
-    (SELECT status FROM orders WHERE id = pg_temp.oid('alice_ask')) = 'executed'
-    AND (SELECT reserved_quantity FROM holdings WHERE user_id = pg_temp.uid('alice')) = 0
-    AND (SELECT reserved_balance FROM users WHERE username = 'charlie') = 0);
-SELECT pg_temp.expect_true('consistency holds after the market order', pg_temp.consistent());
-
--- ===========================================================================
--- D. Trades are only possible between valid, compatible orders
--- ===========================================================================
-INSERT INTO ids VALUES ('charlie_ask',
-    place_order(pg_temp.uid('charlie'), 'a2222222-2222-2222-2222-222222222222', 'sell', 'limit', 0.1, 3700));
-INSERT INTO ids VALUES ('bob_ask',
-    place_order(pg_temp.uid('bob'), 'a2222222-2222-2222-2222-222222222222', 'sell', 'limit', 0.1, 3800));
-
-CALL pg_temp.expect_error('trade: price above the buyer''s limit',
-    $q$INSERT INTO market_trades (market_id, executed_at, price, quantity, buy_order_id, sell_order_id)
-       VALUES ('a2222222-2222-2222-2222-222222222222', now(), 3700, 0.1, pg_temp.oid('bob_bid'), pg_temp.oid('charlie_ask'))$q$,
-    'outside the limit');
-CALL pg_temp.expect_error('trade: more than the order has remaining',
-    $q$INSERT INTO market_trades (market_id, executed_at, price, quantity, buy_order_id)
-       VALUES ('a2222222-2222-2222-2222-222222222222', now(), 3500, 0.5, pg_temp.oid('bob_bid'))$q$,
-    'exceeds the remaining quantity');
-CALL pg_temp.expect_error('trade: a sell order used as the buy side',
-    $q$INSERT INTO market_trades (market_id, executed_at, price, quantity, buy_order_id)
-       VALUES ('a2222222-2222-2222-2222-222222222222', now(), 3700, 0.1, pg_temp.oid('charlie_ask'))$q$,
-    'cannot be the buy side');
-CALL pg_temp.expect_error('trade: order of another market',
-    $q$INSERT INTO market_trades (market_id, executed_at, price, quantity, buy_order_id)
-       VALUES ('a1111111-1111-1111-1111-111111111111', now(), 3500, 0.1, pg_temp.oid('bob_bid'))$q$,
-    'different market');
-CALL pg_temp.expect_error('trade: executed order cannot trade again',
-    $q$INSERT INTO market_trades (market_id, executed_at, price, quantity, sell_order_id)
-       VALUES ('a2222222-2222-2222-2222-222222222222', now(), 3600, 0.1, pg_temp.oid('alice_ask'))$q$,
-    'is executed and cannot trade');
-CALL pg_temp.expect_error('trade: a user with their own order',
-    $q$INSERT INTO market_trades (market_id, executed_at, price, quantity, buy_order_id, sell_order_id)
-       VALUES ('a2222222-2222-2222-2222-222222222222', now(), 3500, 0.1, pg_temp.oid('bob_bid'), pg_temp.oid('bob_ask'))$q$,
-    'own order');
-CALL pg_temp.expect_error('trade: a trade that filled orders cannot be deleted',
-    $q$DELETE FROM market_trades WHERE buy_order_id = pg_temp.oid('bob_bid2')$q$,
-    'cannot be changed or deleted');
-
--- ===========================================================================
--- E. Order state changes
--- ===========================================================================
-CALL pg_temp.expect_error('state: status cannot be set to executed by hand',
-    $q$UPDATE orders SET status = 'executed' WHERE id = pg_temp.oid('bob_bid')$q$,
-    'status is derived automatically');
-CALL pg_temp.expect_error('state: filled quantity cannot be changed by hand',
-    $q$UPDATE orders SET filled_quantity = 0.05 WHERE id = pg_temp.oid('bob_bid')$q$,
-    'can only change through a trade');
-CALL pg_temp.expect_error('state: executed order cannot be processed again',
-    $q$UPDATE orders SET status = 'cancelled' WHERE id = pg_temp.oid('alice_ask')$q$,
-    'already executed');
-CALL pg_temp.expect_error('state: ordered quantity cannot change',
-    $q$UPDATE orders SET quantity = 1 WHERE id = pg_temp.oid('bob_bid')$q$,
-    'cannot change');
-CALL pg_temp.expect_error('state: cancelling by hand without releasing the reservation',
-    $q$UPDATE orders SET status = 'cancelled' WHERE id = pg_temp.oid('bob_bid')$q$,
-    'reserved balance');
-
--- ===========================================================================
--- F. Cancelling releases the reservation, exactly once
--- ===========================================================================
-CALL pg_temp.expect_error('cancel: someone else''s order',
-    $q$SELECT cancel_order(pg_temp.oid('bob_bid'), pg_temp.uid('charlie'))$q$,
-    'does not belong');
-SELECT cancel_order(pg_temp.oid('bob_bid'), pg_temp.uid('bob'));
-SELECT pg_temp.expect_true('cancel: 350 back from reserved to available, event recorded',
-    (SELECT (available_balance, reserved_balance) FROM users WHERE username = 'bob') = (4280.0000, 0.0000)
-    AND EXISTS (SELECT 1 FROM order_events WHERE order_id = pg_temp.oid('bob_bid') AND event_type = 'cancelled'),
-    (SELECT format('bob available %s reserved %s', available_balance, reserved_balance) FROM users WHERE username = 'bob'));
-CALL pg_temp.expect_error('cancel: a cancelled order cannot be cancelled again',
-    $q$SELECT cancel_order(pg_temp.oid('bob_bid'), pg_temp.uid('bob'))$q$,
-    'cannot be cancelled');
-CALL pg_temp.expect_error('trade: cancelled order cannot trade',
-    $q$INSERT INTO market_trades (market_id, executed_at, price, quantity, buy_order_id)
-       VALUES ('a2222222-2222-2222-2222-222222222222', now(), 3500, 0.1, pg_temp.oid('bob_bid'))$q$,
-    'is cancelled and cannot trade');
-
--- ===========================================================================
--- G. Balances cannot be put in an inconsistent state
--- ===========================================================================
-CALL pg_temp.expect_error('balance: reserving cash with no order behind it',
-    $q$UPDATE users SET available_balance = available_balance - 100, reserved_balance = reserved_balance + 100
-        WHERE username = 'charlie'$q$,
-    'reserved balance');
-CALL pg_temp.expect_error('balance: reserving crypto with no order behind it',
-    $q$UPDATE holdings SET reserved_quantity = reserved_quantity + 0.01 WHERE user_id = pg_temp.uid('alice')$q$,
-    'reserved quantity');
-CALL pg_temp.expect_error('balance: cash changed without a ledger row',
-    $q$UPDATE users SET available_balance = available_balance + 100 WHERE username = 'charlie'$q$,
-    'does not match the ledger');
-
--- ===========================================================================
--- H. Background job: resting limit orders filled when the market reaches them
--- ===========================================================================
-INSERT INTO ids VALUES ('bob_bid3',
-    place_order(pg_temp.uid('bob'), 'a2222222-2222-2222-2222-222222222222', 'buy', 'limit', 0.1, 3500));
--- the simulator moves the price down to 3450 (a bot tick, no orders)
-INSERT INTO market_trades (market_id, executed_at, price, quantity, side)
-VALUES ('a2222222-2222-2222-2222-222222222222', now(), 3450, 0.01, 'sell');
-
-CREATE TEMP TABLE job_run ON COMMIT DROP AS SELECT fill_marketable_orders() AS filled;
-
-SELECT pg_temp.expect_true('job: fills exactly the orders the new price reached',
-    (SELECT filled FROM job_run) = 1
-    AND (SELECT (status, avg_fill_price) FROM v_order_history WHERE order_id = pg_temp.oid('bob_bid3'))
-        = ('executed'::varchar, 3450.000000::numeric)
-    AND (SELECT status FROM orders WHERE id = pg_temp.oid('charlie_ask')) = 'open',
-    (SELECT format('bob_bid3 %s @ %s, charlie_ask (3700) still %s', h.status, h.avg_fill_price, o.status)
-       FROM v_order_history h, orders o WHERE h.order_id = pg_temp.oid('bob_bid3') AND o.id = pg_temp.oid('charlie_ask')));
-SELECT pg_temp.expect_true('job: buyer paid 345, got the 5 above the fill price back',
-    (SELECT reserved_balance FROM users WHERE username = 'bob') = 0
-    AND (SELECT count(*) FROM transactions WHERE related_order = pg_temp.oid('bob_bid3') AND amount = -345) = 1);
-SELECT pg_temp.expect_true('job: nothing more to do on a second run', fill_marketable_orders() = 0);
-SELECT pg_temp.expect_true('consistency holds after the job', pg_temp.consistent());
-
--- ===========================================================================
--- Final state of every trader
--- ===========================================================================
-SELECT pg_temp.expect_true('views: every trader''s cash equals their ledger',
-    NOT EXISTS (SELECT 1 FROM v_trader_balances WHERE total_cash <> ledger_total));
-
-SELECT username, available_balance, reserved_balance, ledger_total, holdings_value
-  FROM v_trader_balances ORDER BY username;
-
-SELECT count(*) FILTER (WHERE passed)     AS passed,
-       count(*) FILTER (WHERE NOT passed) AS failed
-  FROM test_results;
-
-ROLLBACK;
Index: rver/db/data_load.sql
===================================================================
--- server/db/data_load.sql	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,158 +1,0 @@
--- data_load.sql
--- EduBerza - sample data
--- Course: Databases 2025/2026 Winter, FINKI UKIM
---
--- Idempotent. Truncates all tables in the `project` schema and reloads
--- deterministic sample data. Run schema_creation.sql first if tables do
--- not yet exist.
---
--- All sample users have the password: test123
---
--- One transaction: the P7 checks in advanced_db.sql compare balances with
--- the ledger at COMMIT, and the users are inserted with their balances
--- before the deposit rows that back them. In an auto-commit client
--- (DBeaver) every statement would otherwise be checked on its own.
-
-BEGIN;
-
-SET search_path TO project, public;
-
-TRUNCATE TABLE
-    project.watchlist_items,
-    project.watchlists,
-    project.market_candles,
-    project.market_trades,
-    project.transactions,
-    project.orders,
-    project.holdings,
-    project.markets,
-    project.crypto,
-    project.users
-RESTART IDENTITY CASCADE;
-
--- ============================================================================
--- CRYPTO
--- ============================================================================
-INSERT INTO project.crypto (id, symbol, name) VALUES
-    ('11111111-1111-1111-1111-111111111111', 'BTC',  'Bitcoin'),
-    ('22222222-2222-2222-2222-222222222222', 'ETH',  'Ethereum'),
-    ('33333333-3333-3333-3333-333333333333', 'ADA',  'Cardano'),
-    ('44444444-4444-4444-4444-444444444444', 'SOL',  'Solana'),
-    ('55555555-5555-5555-5555-555555555555', 'DOGE', 'Dogecoin');
-
--- ============================================================================
--- MARKETS (all quoted in USD)
--- ============================================================================
-INSERT INTO project.markets (id, crypto_id, quote_currency, is_active) VALUES
-    ('a1111111-1111-1111-1111-111111111111', '11111111-1111-1111-1111-111111111111', 'USD', true),
-    ('a2222222-2222-2222-2222-222222222222', '22222222-2222-2222-2222-222222222222', 'USD', true),
-    ('a3333333-3333-3333-3333-333333333333', '33333333-3333-3333-3333-333333333333', 'USD', true),
-    ('a4444444-4444-4444-4444-444444444444', '44444444-4444-4444-4444-444444444444', 'USD', true),
-    ('a5555555-5555-5555-5555-555555555555', '55555555-5555-5555-5555-555555555555', 'USD', true);
-
--- ============================================================================
--- USERS
--- Password for all: test123 (stored as sha256 hex hash)
--- ============================================================================
-INSERT INTO project.users (id, username, email, full_name, password_hash, available_balance, invested_balance) VALUES
-    ('b1111111-1111-1111-1111-111111111111', 'alice',   'alice@example.com',   'Alice Johnson',
-        encode(digest('test123', 'sha256'), 'hex'), 10000.0000, 0),
-    ('b2222222-2222-2222-2222-222222222222', 'bob',     'bob@example.com',     'Bob Smith',
-        encode(digest('test123', 'sha256'), 'hex'),  5000.0000, 0),
-    ('b3333333-3333-3333-3333-333333333333', 'charlie', 'charlie@example.com', 'Charlie Davis',
-        encode(digest('test123', 'sha256'), 'hex'),  2500.0000, 0);
-
--- ============================================================================
--- MARKET TRADES
--- Recent simulated trades per market, used as price source.
--- ============================================================================
-INSERT INTO project.market_trades (market_id, executed_at, price, quantity, side, source) VALUES
-    -- BTC/USD around $67,000
-    ('a1111111-1111-1111-1111-111111111111', now() - interval '10 min', 66850.250000, 0.120000, 'buy',  'simulation'),
-    ('a1111111-1111-1111-1111-111111111111', now() - interval  '8 min', 66910.500000, 0.075000, 'sell', 'simulation'),
-    ('a1111111-1111-1111-1111-111111111111', now() - interval  '5 min', 67020.750000, 0.200000, 'buy',  'simulation'),
-    ('a1111111-1111-1111-1111-111111111111', now() - interval  '2 min', 67105.100000, 0.050000, 'buy',  'simulation'),
-    ('a1111111-1111-1111-1111-111111111111', now() - interval '30 second', 67140.000000, 0.030000, 'sell', 'simulation'),
-    -- ETH/USD around $3,500
-    ('a2222222-2222-2222-2222-222222222222', now() - interval '10 min', 3490.500000, 1.500000, 'buy',  'simulation'),
-    ('a2222222-2222-2222-2222-222222222222', now() - interval  '6 min', 3502.750000, 0.800000, 'sell', 'simulation'),
-    ('a2222222-2222-2222-2222-222222222222', now() - interval  '2 min', 3515.250000, 2.100000, 'buy',  'simulation'),
-    ('a2222222-2222-2222-2222-222222222222', now() - interval '30 second', 3520.000000, 0.650000, 'buy',  'simulation'),
-    -- ADA/USD around $0.45
-    ('a3333333-3333-3333-3333-333333333333', now() - interval '10 min', 0.446500,  500.000000, 'buy',  'simulation'),
-    ('a3333333-3333-3333-3333-333333333333', now() - interval  '3 min', 0.452000, 1200.000000, 'buy',  'simulation'),
-    ('a3333333-3333-3333-3333-333333333333', now() - interval '30 second', 0.453750,  800.000000, 'sell', 'simulation'),
-    -- SOL/USD around $165
-    ('a4444444-4444-4444-4444-444444444444', now() - interval '10 min', 164.250000, 10.000000, 'buy',  'simulation'),
-    ('a4444444-4444-4444-4444-444444444444', now() - interval  '4 min', 165.500000,  5.500000, 'sell', 'simulation'),
-    ('a4444444-4444-4444-4444-444444444444', now() - interval '30 second', 166.100000,  8.000000, 'buy',  'simulation'),
-    -- DOGE/USD around $0.12
-    ('a5555555-5555-5555-5555-555555555555', now() - interval '10 min', 0.118500, 10000.000000, 'buy',  'simulation'),
-    ('a5555555-5555-5555-5555-555555555555', now() - interval  '3 min', 0.121250,  7500.000000, 'sell', 'simulation'),
-    ('a5555555-5555-5555-5555-555555555555', now() - interval '30 second', 0.122000, 12000.000000, 'buy',  'simulation');
-
--- ============================================================================
--- MARKET CANDLES (1h aggregates, last 5 hours per market)
--- ============================================================================
-INSERT INTO project.market_candles (market_id, timeframe, open, high, low, close, volume, candle_time) VALUES
-    ('a1111111-1111-1111-1111-111111111111', '1h', 66200, 66500, 66050, 66400, 12.50, date_trunc('hour', now() - interval '5 hour')),
-    ('a1111111-1111-1111-1111-111111111111', '1h', 66400, 66800, 66380, 66700, 15.30, date_trunc('hour', now() - interval '4 hour')),
-    ('a1111111-1111-1111-1111-111111111111', '1h', 66700, 66950, 66650, 66900, 11.80, date_trunc('hour', now() - interval '3 hour')),
-    ('a1111111-1111-1111-1111-111111111111', '1h', 66900, 67100, 66800, 67050, 14.20, date_trunc('hour', now() - interval '2 hour')),
-    ('a1111111-1111-1111-1111-111111111111', '1h', 67050, 67200, 66900, 67140, 10.75, date_trunc('hour', now() - interval '1 hour')),
-    ('a2222222-2222-2222-2222-222222222222', '1h',  3460,  3490,  3450,  3485, 120.0, date_trunc('hour', now() - interval '5 hour')),
-    ('a2222222-2222-2222-2222-222222222222', '1h',  3485,  3510,  3480,  3500, 135.0, date_trunc('hour', now() - interval '4 hour')),
-    ('a2222222-2222-2222-2222-222222222222', '1h',  3500,  3520,  3495,  3515, 110.0, date_trunc('hour', now() - interval '3 hour')),
-    ('a2222222-2222-2222-2222-222222222222', '1h',  3515,  3525,  3500,  3520, 125.5, date_trunc('hour', now() - interval '2 hour')),
-    ('a2222222-2222-2222-2222-222222222222', '1h',  3520,  3530,  3510,  3520, 140.0, date_trunc('hour', now() - interval '1 hour'));
-
--- ============================================================================
--- EXAMPLE ORDERS, HOLDINGS AND TRANSACTIONS for alice
--- Shows a fully-filled market buy and its resulting holding & ledger entry.
--- ============================================================================
--- Imported as already completely filled (filled_quantity = quantity).
-INSERT INTO project.orders (id, user_id, market_id, side, type, status, quantity, filled_quantity, price, placed_at, executed_at) VALUES
-    ('c1111111-1111-1111-1111-111111111111',
-     'b1111111-1111-1111-1111-111111111111',
-     'a2222222-2222-2222-2222-222222222222',
-     'buy', 'market', 'executed', 0.5000, 0.5000, 3500.000000,
-     now() - interval '1 hour', now() - interval '1 hour');
-
-INSERT INTO project.holdings (user_id, crypto_id, quantity, avg_price, updated_at) VALUES
-    ('b1111111-1111-1111-1111-111111111111',
-     '22222222-2222-2222-2222-222222222222',
-     0.5000, 3500.000000, now() - interval '1 hour');
-
-INSERT INTO project.transactions (user_id, type, amount, currency, related_order, description) VALUES
-    ('b1111111-1111-1111-1111-111111111111', 'deposit',  10000.0000, 'USD', NULL,
-        'Initial virtual deposit'),
-    ('b2222222-2222-2222-2222-222222222222', 'deposit',   5000.0000, 'USD', NULL,
-        'Initial virtual deposit'),
-    ('b3333333-3333-3333-3333-333333333333', 'deposit',   2500.0000, 'USD', NULL,
-        'Initial virtual deposit'),
-    ('b1111111-1111-1111-1111-111111111111', 'buy',      -1750.0000, 'USD',
-        'c1111111-1111-1111-1111-111111111111',
-        'Market buy 0.5 ETH @ 3500.00');
-
--- After the buy, alice's invested_balance reflects the used funds.
-UPDATE project.users
-   SET available_balance = 10000.0000 - 1750.0000,
-       invested_balance  = 1750.0000,
-       updated_at        = now()
- WHERE id = 'b1111111-1111-1111-1111-111111111111';
-
--- ============================================================================
--- WATCHLISTS
--- ============================================================================
-INSERT INTO project.watchlists (id, user_id, name) VALUES
-    ('d1111111-1111-1111-1111-111111111111', 'b1111111-1111-1111-1111-111111111111', 'Favorites'),
-    ('d2222222-2222-2222-2222-222222222222', 'b2222222-2222-2222-2222-222222222222', 'Bobs Picks');
-
-INSERT INTO project.watchlist_items (watchlist_id, crypto_id) VALUES
-    ('d1111111-1111-1111-1111-111111111111', '11111111-1111-1111-1111-111111111111'),
-    ('d1111111-1111-1111-1111-111111111111', '22222222-2222-2222-2222-222222222222'),
-    ('d1111111-1111-1111-1111-111111111111', '44444444-4444-4444-4444-444444444444'),
-    ('d2222222-2222-2222-2222-222222222222', '11111111-1111-1111-1111-111111111111'),
-    ('d2222222-2222-2222-2222-222222222222', '55555555-5555-5555-5555-555555555555');
-
-COMMIT;
Index: rver/db/db.go
===================================================================
--- server/db/db.go	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,171 +1,0 @@
-package db
-
-import (
-	"bufio"
-	"database/sql"
-	"embed"
-	"fmt"
-	"log"
-	"os"
-	"path/filepath"
-	"strings"
-
-	"github.com/lib/pq"
-)
-
-var DB *sql.DB
-
-// The SQL scripts are compiled into the binary so that -init works no matter
-// which directory the program is started from.
-//
-//go:embed schema_creation.sql advanced_db.sql data_load.sql
-var sqlScripts embed.FS
-
-func Connect() error {
-	loadEnvFile()
-
-	host := getenv("DBHOST", "localhost")
-	port := getenv("DBPORT", "5432")
-	user := getenv("DBUSER", "postgres")
-	pass := getenv("DBPASSWORD", "")
-	name := getenv("DBNAME", "postgres")
-
-	dsn := fmt.Sprintf(
-		"host=%s port=%s user=%s password=%s dbname=%s sslmode=disable options='--search_path=project,public'",
-		host, port, user, pass, name,
-	)
-
-	// Optional SSH tunnel, the same thing DBeaver's "SSH" tab does. When
-	// SSH_HOST is set, DBHOST/DBPORT are resolved from the SSH server's side
-	// (for the faculty server that is usually localhost:5432).
-	if sshHost := os.Getenv("SSH_HOST"); sshHost != "" {
-		dialer, err := newSSHDialer(sshHost)
-		if err != nil {
-			return err
-		}
-		connector, err := pq.NewConnector(dsn)
-		if err != nil {
-			return fmt.Errorf("pq.NewConnector: %w", err)
-		}
-		connector.Dialer(dialer)
-		DB = sql.OpenDB(connector)
-	} else {
-		var err error
-		DB, err = sql.Open("postgres", dsn)
-		if err != nil {
-			return fmt.Errorf("sql.Open: %w", err)
-		}
-	}
-	if err := DB.Ping(); err != nil {
-		return fmt.Errorf("db ping (host=%s port=%s user=%s dbname=%s): %w",
-			host, port, user, name, err)
-	}
-	return nil
-}
-
-// runScript executes one of the embedded .sql scripts as a single statement.
-func runScript(name string) error {
-	content, err := sqlScripts.ReadFile(name)
-	if err != nil {
-		return fmt.Errorf("read embedded %s: %w", name, err)
-	}
-	if _, err := DB.Exec(string(content)); err != nil {
-		return fmt.Errorf("exec %s: %w", name, err)
-	}
-	return nil
-}
-
-// RunSQLFile executes a .sql file from disk as a single statement.
-func RunSQLFile(path string) error {
-	content, err := os.ReadFile(path)
-	if err != nil {
-		return fmt.Errorf("read %s: %w", path, err)
-	}
-	if _, err := DB.Exec(string(content)); err != nil {
-		return fmt.Errorf("exec %s: %w", path, err)
-	}
-	return nil
-}
-
-// InitSchema runs schema_creation.sql, advanced_db.sql (P7) and data_load.sql.
-// Destructive: drops the `project` schema. Intended for the -init flag.
-func InitSchema() error {
-	for _, name := range []string{"schema_creation.sql", "advanced_db.sql"} {
-		log.Printf("Running %s ...", name)
-		if err := runScript(name); err != nil {
-			return err
-		}
-	}
-	if err := LoadData(); err != nil {
-		return err
-	}
-	log.Println("Database initialised.")
-	return nil
-}
-
-// LoadData reloads the sample data without touching the schema.
-func LoadData() error {
-	log.Println("Running data_load.sql ...")
-	return runScript("data_load.sql")
-}
-
-// loadEnvFile looks for a .env file in the working directory and in every
-// parent directory, so the program can be started from the repo root, from
-// server/, or from anywhere else inside the checkout. Variables already set
-// in the real environment always win over the file, which is what lets you
-// point the prototype at the faculty database with DBHOST=... ./eduberza
-func loadEnvFile() {
-	dir, err := os.Getwd()
-	if err != nil {
-		return
-	}
-	for {
-		path := filepath.Join(dir, ".env")
-		if applyEnvFile(path) {
-			return
-		}
-		parent := filepath.Dir(dir)
-		if parent == dir {
-			return // reached the filesystem root
-		}
-		dir = parent
-	}
-}
-
-// applyEnvFile reports whether the file existed and was read.
-func applyEnvFile(path string) bool {
-	f, err := os.Open(path)
-	if err != nil {
-		return false
-	}
-	defer f.Close()
-
-	s := bufio.NewScanner(f)
-	for s.Scan() {
-		line := strings.TrimSpace(s.Text())
-		if line == "" || strings.HasPrefix(line, "#") {
-			continue
-		}
-		key, value, ok := strings.Cut(line, "=")
-		if !ok {
-			continue
-		}
-		key = strings.TrimSpace(key)
-		// Do not clobber variables that are already set in the environment.
-		if _, exists := os.LookupEnv(key); exists {
-			continue
-		}
-		os.Setenv(key, strings.Trim(strings.TrimSpace(value), `"'`))
-	}
-	if err := s.Err(); err != nil {
-		log.Printf("warning: could not fully read %s: %v", path, err)
-	}
-	return true
-}
-
-func getenv(key, def string) string {
-	if v := os.Getenv(key); v != "" {
-		return v
-	}
-	return def
-}
Index: rver/db/reports_demo_data.sql
===================================================================
--- server/db/reports_demo_data.sql	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,136 +1,0 @@
--- reports_demo_data.sql
--- EduBerza - optional historical data for the P6 reports
--- Course: Databases 2025/2026 Winter, FINKI UKIM
---
--- data_load.sql only seeds ~10 minutes of trade history, which is enough to
--- demonstrate UC0001-UC0007 but not enough to show report_top_traders() or
--- report_market_performance() doing anything interesting: everything falls
--- into a single quarter, so "number of profitable periods" and "consistency"
--- are trivial and "market return" has almost no history to work with.
---
--- This script adds five quarters of synthetic transactions, market trades and
--- executed orders on top of an already-loaded data_load.sql, spanning
--- 2025-07 to 2026-07, so the two P6 reports have several periods and two
--- markets with opposite price trends to actually compare.
---
--- Deliberately NOT part of -init / -load-data: it only inserts into
--- transactions, market_trades and orders, and does not touch
--- users.available_balance/invested_balance or holdings, so it does not
--- disturb the balances the other use cases' documented "verified run"
--- sections depend on. Run it by hand, after data_load.sql, only to exercise
--- the two reports:
---
---   psql "$DATABASE_URL" -f server/db/schema_creation.sql
---   psql "$DATABASE_URL" -f server/db/data_load.sql
---   psql "$DATABASE_URL" -f server/db/reports_demo_data.sql
---
--- Idempotent: deletes its own previously-inserted rows (tagged via
--- description/source) before re-inserting.
-
--- P7: users' cash (available + reserved) must equal their ledger at
--- COMMIT, so the balance moves by exactly what this script removes and
--- re-adds to the ledger, all in one transaction. The historical orders are
--- imported as completely filled.
-
-BEGIN;
-
-SET search_path TO project, public;
-
-UPDATE users u
-   SET available_balance = u.available_balance - d.total
-  FROM (SELECT user_id, SUM(amount) AS total FROM transactions
-         WHERE description = 'P6 demo data' GROUP BY user_id) d
- WHERE u.id = d.user_id;
-
-DELETE FROM transactions  WHERE description = 'P6 demo data';
-DELETE FROM orders        WHERE id IN (
-    'e1111111-1111-1111-1111-111111111111', 'e2222222-2222-2222-2222-222222222222',
-    'e3333333-3333-3333-3333-333333333333', 'e4444444-4444-4444-4444-444444444444',
-    'e5555555-5555-5555-5555-555555555555'
-);
-DELETE FROM market_trades WHERE source = 'p6_demo';
-
--- ============================================================================
--- Alice: five quarterly round trips, 3 profitable / 2 losing (60% consistency)
--- ============================================================================
-INSERT INTO transactions (user_id, type, amount, currency, created_at, description) VALUES
-    ('b1111111-1111-1111-1111-111111111111', 'buy',  -5000.0000, 'USD', '2025-07-15 10:00', 'P6 demo data'),
-    ('b1111111-1111-1111-1111-111111111111', 'sell',  5800.0000, 'USD', '2025-07-20 10:00', 'P6 demo data'),
-    ('b1111111-1111-1111-1111-111111111111', 'fee',      -5.0000, 'USD', '2025-07-20 10:00', 'P6 demo data'),
-
-    ('b1111111-1111-1111-1111-111111111111', 'buy',  -4000.0000, 'USD', '2025-10-15 10:00', 'P6 demo data'),
-    ('b1111111-1111-1111-1111-111111111111', 'sell',  3500.0000, 'USD', '2025-10-20 10:00', 'P6 demo data'),
-    ('b1111111-1111-1111-1111-111111111111', 'fee',      -5.0000, 'USD', '2025-10-20 10:00', 'P6 demo data'),
-
-    ('b1111111-1111-1111-1111-111111111111', 'buy',  -6000.0000, 'USD', '2026-01-15 10:00', 'P6 demo data'),
-    ('b1111111-1111-1111-1111-111111111111', 'sell',  6700.0000, 'USD', '2026-01-20 10:00', 'P6 demo data'),
-    ('b1111111-1111-1111-1111-111111111111', 'fee',      -5.0000, 'USD', '2026-01-20 10:00', 'P6 demo data'),
-
-    ('b1111111-1111-1111-1111-111111111111', 'buy',  -3000.0000, 'USD', '2026-04-15 10:00', 'P6 demo data'),
-    ('b1111111-1111-1111-1111-111111111111', 'sell',  2600.0000, 'USD', '2026-04-20 10:00', 'P6 demo data'),
-    ('b1111111-1111-1111-1111-111111111111', 'fee',      -5.0000, 'USD', '2026-04-20 10:00', 'P6 demo data'),
-
-    ('b1111111-1111-1111-1111-111111111111', 'buy',  -4500.0000, 'USD', '2026-07-15 10:00', 'P6 demo data'),
-    ('b1111111-1111-1111-1111-111111111111', 'sell',  5200.0000, 'USD', '2026-07-20 10:00', 'P6 demo data'),
-    ('b1111111-1111-1111-1111-111111111111', 'fee',      -5.0000, 'USD', '2026-07-20 10:00', 'P6 demo data');
-
--- ============================================================================
--- Bob: three quarterly round trips, all profitable (100% consistency),
--- smaller total P/L than Alice but a higher ROI.
--- ============================================================================
-INSERT INTO transactions (user_id, type, amount, currency, created_at, description) VALUES
-    ('b2222222-2222-2222-2222-222222222222', 'buy',  -2000.0000, 'USD', '2025-10-10 10:00', 'P6 demo data'),
-    ('b2222222-2222-2222-2222-222222222222', 'sell',  2300.0000, 'USD', '2025-10-12 10:00', 'P6 demo data'),
-    ('b2222222-2222-2222-2222-222222222222', 'fee',      -3.0000, 'USD', '2025-10-12 10:00', 'P6 demo data'),
-
-    ('b2222222-2222-2222-2222-222222222222', 'buy',  -2500.0000, 'USD', '2026-01-10 10:00', 'P6 demo data'),
-    ('b2222222-2222-2222-2222-222222222222', 'sell',  2900.0000, 'USD', '2026-01-12 10:00', 'P6 demo data'),
-    ('b2222222-2222-2222-2222-222222222222', 'fee',      -3.0000, 'USD', '2026-01-12 10:00', 'P6 demo data'),
-
-    ('b2222222-2222-2222-2222-222222222222', 'buy',  -1800.0000, 'USD', '2026-04-10 10:00', 'P6 demo data'),
-    ('b2222222-2222-2222-2222-222222222222', 'sell',  2100.0000, 'USD', '2026-04-12 10:00', 'P6 demo data'),
-    ('b2222222-2222-2222-2222-222222222222', 'fee',      -3.0000, 'USD', '2026-04-12 10:00', 'P6 demo data');
-
--- ============================================================================
--- Market trades: BTC/USD trending up, ETH/USD trending down, five quarters.
--- source='p6_demo' keeps these separate from data_load.sql's own rows and
--- from live user/bot fills so this script can clean up after itself.
--- ============================================================================
-INSERT INTO market_trades (market_id, executed_at, price, quantity, side, source) VALUES
-    ('a1111111-1111-1111-1111-111111111111', '2025-07-15 10:00', 40000.000000, 0.500000, 'buy',  'p6_demo'),
-    ('a1111111-1111-1111-1111-111111111111', '2025-10-15 10:00', 45000.000000, 0.800000, 'buy',  'p6_demo'),
-    ('a1111111-1111-1111-1111-111111111111', '2026-01-15 10:00', 55000.000000, 1.200000, 'buy',  'p6_demo'),
-    ('a1111111-1111-1111-1111-111111111111', '2026-04-15 10:00', 60000.000000, 1.000000, 'buy',  'p6_demo'),
-
-    ('a2222222-2222-2222-2222-222222222222', '2025-07-15 10:00',  4000.000000, 3.000000, 'sell', 'p6_demo'),
-    ('a2222222-2222-2222-2222-222222222222', '2025-10-15 10:00',  3800.000000, 2.500000, 'sell', 'p6_demo'),
-    ('a2222222-2222-2222-2222-222222222222', '2026-01-15 10:00',  3600.000000, 2.000000, 'sell', 'p6_demo'),
-    ('a2222222-2222-2222-2222-222222222222', '2026-04-15 10:00',  3550.000000, 1.800000, 'sell', 'p6_demo');
-
--- ============================================================================
--- Executed orders: who participated in which market, across the same quarters.
--- ============================================================================
-INSERT INTO orders (id, user_id, market_id, side, type, status, quantity, filled_quantity, price, placed_at, executed_at) VALUES
-    ('e1111111-1111-1111-1111-111111111111', 'b1111111-1111-1111-1111-111111111111',
-     'a1111111-1111-1111-1111-111111111111', 'buy', 'market', 'executed', 0.5000, 0.5000, 40000.000000,
-     '2025-07-15 10:00', '2025-07-15 10:00'),
-    ('e2222222-2222-2222-2222-222222222222', 'b1111111-1111-1111-1111-111111111111',
-     'a2222222-2222-2222-2222-222222222222', 'sell', 'market', 'executed', 3.0000, 3.0000, 4000.000000,
-     '2025-10-15 10:00', '2025-10-15 10:00'),
-    ('e3333333-3333-3333-3333-333333333333', 'b2222222-2222-2222-2222-222222222222',
-     'a1111111-1111-1111-1111-111111111111', 'buy', 'market', 'executed', 1.2000, 1.2000, 55000.000000,
-     '2026-01-15 10:00', '2026-01-15 10:00'),
-    ('e4444444-4444-4444-4444-444444444444', 'b2222222-2222-2222-2222-222222222222',
-     'a1111111-1111-1111-1111-111111111111', 'buy', 'market', 'executed', 1.0000, 1.0000, 60000.000000,
-     '2026-04-15 10:00', '2026-04-15 10:00'),
-    ('e5555555-5555-5555-5555-555555555555', 'b3333333-3333-3333-3333-333333333333',
-     'a2222222-2222-2222-2222-222222222222', 'sell', 'market', 'executed', 2.0000, 2.0000, 3600.000000,
-     '2026-01-15 10:00', '2026-01-15 10:00');
-
-UPDATE users u
-   SET available_balance = u.available_balance + d.total
-  FROM (SELECT user_id, SUM(amount) AS total FROM transactions
-         WHERE description = 'P6 demo data' GROUP BY user_id) d
- WHERE u.id = d.user_id;
-
-COMMIT;
Index: rver/db/schema_creation.sql
===================================================================
--- server/db/schema_creation.sql	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,319 +1,0 @@
--- schema_creation.sql
--- EduBerza - crypto exchange simulation database
--- Course: Databases 2025/2026 Winter, FINKI UKIM
---
--- This script is idempotent. It drops the `project` schema and all contained
--- objects, then recreates them from scratch. Safe to run on an empty database
--- or on a database where the schema already exists.
-
-DROP SCHEMA IF EXISTS project CASCADE;
-CREATE SCHEMA project;
-
-CREATE EXTENSION IF NOT EXISTS pgcrypto;
-
-SET search_path TO project, public;
-
--- ============================================================================
--- USERS
--- Platform users. Each user has virtual (prop) balances used for simulation.
--- ============================================================================
-CREATE TABLE project.users (
-    id                uuid            PRIMARY KEY DEFAULT gen_random_uuid(),
-    username          varchar(50)     NOT NULL UNIQUE,
-    email             varchar(255)    NOT NULL UNIQUE,
-    full_name         varchar(200),
-    password_hash     varchar(255)    NOT NULL,
-    available_balance numeric(18,4)   NOT NULL DEFAULT 0 CHECK (available_balance >= 0),
-    invested_balance  numeric(18,4)   NOT NULL DEFAULT 0 CHECK (invested_balance  >= 0),
-    -- P7: cash committed to the user's active buy orders, moved out of
-    -- available_balance when the order is placed and consumed as it fills.
-    reserved_balance  numeric(18,4)   NOT NULL DEFAULT 0 CHECK (reserved_balance  >= 0),
-    created_at        timestamptz     NOT NULL DEFAULT now(),
-    updated_at        timestamptz
-);
-
--- ============================================================================
--- CRYPTO
--- Catalog of crypto assets available on the platform.
--- ============================================================================
-CREATE TABLE project.crypto (
-    id         uuid         PRIMARY KEY DEFAULT gen_random_uuid(),
-    symbol     varchar(20)  NOT NULL UNIQUE,
-    name       varchar(255) NOT NULL,
-    created_at timestamptz  NOT NULL DEFAULT now()
-);
-
--- ============================================================================
--- MARKETS
--- A market is a (crypto, quote_currency) pair, e.g. BTC/USD.
--- ============================================================================
-CREATE TABLE project.markets (
-    id             uuid        PRIMARY KEY DEFAULT gen_random_uuid(),
-    crypto_id      uuid        NOT NULL REFERENCES project.crypto(id),
-    quote_currency char(3)     NOT NULL DEFAULT 'USD',
-    is_active      boolean     NOT NULL DEFAULT true,
-    created_at     timestamptz NOT NULL DEFAULT now(),
-    CONSTRAINT uq_markets UNIQUE (crypto_id, quote_currency)
-);
-
--- ============================================================================
--- HOLDINGS
--- Per-user crypto position with running weighted average entry price.
--- ============================================================================
-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),
-    -- 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,
-    CONSTRAINT uq_holdings_user_crypto UNIQUE (user_id, crypto_id)
-);
-
--- ============================================================================
--- ORDERS
--- Orders placed by users on a market.
--- ============================================================================
-CREATE TABLE project.orders (
-    id          uuid           PRIMARY KEY DEFAULT gen_random_uuid(),
-    user_id     uuid           NOT NULL REFERENCES project.users(id)   ON DELETE CASCADE,
-    market_id   uuid           NOT NULL REFERENCES project.markets(id),
-    side        varchar(4)     NOT NULL CHECK (side   IN ('buy', 'sell')),
-    type        varchar(20)    NOT NULL CHECK (type   IN ('market', 'limit')),
-    status      varchar(20)    NOT NULL CHECK (status IN ('open', 'partially_filled', 'executed', 'cancelled')),
-    quantity    numeric(20,4)  NOT NULL CHECK (quantity > 0),
-    -- P7: how much of the order has been traded so far; remaining is
-    -- quantity - filled_quantity. Maintained from market_trades.
-    filled_quantity numeric(20,4) NOT NULL DEFAULT 0
-                               CHECK (filled_quantity >= 0 AND filled_quantity <= quantity),
-    price       numeric(18,6),
-    placed_at   timestamptz    NOT NULL DEFAULT now(),
-    executed_at timestamptz
-);
-
-CREATE INDEX idx_orders_user      ON project.orders(user_id);
-CREATE INDEX idx_orders_market    ON project.orders(market_id);
-CREATE INDEX idx_orders_status    ON project.orders(status);
-
--- ============================================================================
--- TRANSACTIONS
--- Financial ledger: deposits, buys, sells, fees.
--- ============================================================================
-CREATE TABLE project.transactions (
-    id            uuid           PRIMARY KEY DEFAULT gen_random_uuid(),
-    user_id       uuid           NOT NULL REFERENCES project.users(id) ON DELETE CASCADE,
-    type          varchar(50)    NOT NULL CHECK (type IN ('deposit', 'buy', 'sell', 'fee')),
-    amount        numeric(18,4)  NOT NULL,
-    currency      char(3)        NOT NULL DEFAULT 'USD',
-    related_order uuid           REFERENCES project.orders(id),
-    created_at    timestamptz    NOT NULL DEFAULT now(),
-    description   text
-);
-
-CREATE INDEX idx_transactions_user ON project.transactions(user_id, created_at DESC);
-
--- ============================================================================
--- MARKET TRADES
--- Raw executed trades on a market. Source of truth for current price.
--- ============================================================================
-CREATE TABLE project.market_trades (
-    id          bigserial      PRIMARY KEY,
-    market_id   uuid           NOT NULL REFERENCES project.markets(id),
-    executed_at timestamptz    NOT NULL,
-    price       numeric(18,6)  NOT NULL CHECK (price    > 0),
-    quantity    numeric(20,6)  NOT NULL CHECK (quantity > 0),
-    side        varchar(4)     CHECK (side IN ('buy', 'sell')),
-    source      varchar(50)    NOT NULL DEFAULT 'simulation',
-    -- P7: the orders this trade filled. NULL on a side means the counterparty
-    -- was the simulated market (bot ticks have both NULL).
-    buy_order_id  uuid         REFERENCES project.orders(id),
-    sell_order_id uuid         REFERENCES project.orders(id)
-);
-
-CREATE INDEX idx_market_trades_market_time ON project.market_trades(market_id, executed_at DESC);
-CREATE INDEX idx_market_trades_buy_order  ON project.market_trades(buy_order_id)  WHERE buy_order_id  IS NOT NULL;
-CREATE INDEX idx_market_trades_sell_order ON project.market_trades(sell_order_id) WHERE sell_order_id IS NOT NULL;
-
--- ============================================================================
--- MARKET CANDLES
--- OHLCV aggregates over standard timeframes.
--- ============================================================================
-CREATE TABLE project.market_candles (
-    id          bigserial      PRIMARY KEY,
-    market_id   uuid           NOT NULL REFERENCES project.markets(id),
-    timeframe   varchar(5)     NOT NULL CHECK (timeframe IN ('1m', '5m', '1h', '1d')),
-    open        numeric(18,6)  NOT NULL,
-    high        numeric(18,6)  NOT NULL,
-    low         numeric(18,6)  NOT NULL,
-    close       numeric(18,6)  NOT NULL,
-    volume      numeric(20,6)  NOT NULL,
-    candle_time timestamptz    NOT NULL,
-    CONSTRAINT uq_candle UNIQUE (market_id, timeframe, candle_time)
-);
-
-CREATE INDEX idx_market_candles_market_tf_time ON project.market_candles(market_id, timeframe, candle_time DESC);
-
--- ============================================================================
--- WATCHLISTS
--- ============================================================================
-CREATE TABLE project.watchlists (
-    id         uuid         PRIMARY KEY DEFAULT gen_random_uuid(),
-    user_id    uuid         NOT NULL REFERENCES project.users(id) ON DELETE CASCADE,
-    name       varchar(100) NOT NULL,
-    created_at timestamptz  NOT NULL DEFAULT now()
-);
-
-CREATE TABLE project.watchlist_items (
-    id           uuid        PRIMARY KEY DEFAULT gen_random_uuid(),
-    watchlist_id uuid        NOT NULL REFERENCES project.watchlists(id) ON DELETE CASCADE,
-    crypto_id    uuid        NOT NULL REFERENCES project.crypto(id),
-    added_at     timestamptz NOT NULL DEFAULT now(),
-    CONSTRAINT uq_watchlist_crypto UNIQUE (watchlist_id, crypto_id)
-);
-
--- ============================================================================
--- VIEWS
--- ============================================================================
-
--- Latest trade price per market (current price).
-CREATE OR REPLACE VIEW project.v_latest_prices AS
-SELECT DISTINCT ON (t.market_id)
-       t.market_id,
-       c.symbol,
-       m.quote_currency,
-       t.price,
-       t.executed_at
-FROM   project.market_trades t
-JOIN   project.markets       m ON m.id = t.market_id
-JOIN   project.crypto        c ON c.id = m.crypto_id
-ORDER  BY t.market_id, t.executed_at DESC;
-
--- Portfolio valuation per user (holdings x latest price).
-CREATE OR REPLACE VIEW project.v_portfolio AS
-SELECT h.user_id,
-       c.symbol,
-       h.quantity,
-       h.reserved_quantity,
-       (h.quantity - h.reserved_quantity) AS available_quantity,
-       h.avg_price,
-       lp.price                           AS current_price,
-       (h.quantity * lp.price)            AS market_value,
-       (h.quantity * (lp.price - h.avg_price)) AS unrealized_pnl
-FROM   project.holdings h
-JOIN   project.crypto   c ON c.id = h.crypto_id
-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,
-    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,
-            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,
-        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: rver/db/ssh.go
===================================================================
--- server/db/ssh.go	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,72 +1,0 @@
-package db
-
-import (
-	"fmt"
-	"net"
-	"os"
-	"time"
-
-	"golang.org/x/crypto/ssh"
-)
-
-// sshDialer implements pq.Dialer by opening every database connection
-// through an SSH client, so no separate `ssh -L` tunnel is needed.
-type sshDialer struct {
-	client *ssh.Client
-}
-
-func (d sshDialer) Dial(network, address string) (net.Conn, error) {
-	return d.client.Dial(network, address)
-}
-
-func (d sshDialer) DialTimeout(network, address string, _ time.Duration) (net.Conn, error) {
-	return d.client.Dial(network, address)
-}
-
-// newSSHDialer connects to the SSH server described by the SSH_* variables.
-// Authentication uses SSH_KEY (path to a private key) and/or SSH_PASSWORD.
-func newSSHDialer(host string) (sshDialer, error) {
-	port := getenv("SSH_PORT", "22")
-	user := os.Getenv("SSH_USER")
-	if user == "" {
-		return sshDialer{}, fmt.Errorf("SSH_HOST is set but SSH_USER is empty")
-	}
-
-	var auth []ssh.AuthMethod
-	if keyPath := os.Getenv("SSH_KEY"); keyPath != "" {
-		key, err := os.ReadFile(keyPath)
-		if err != nil {
-			return sshDialer{}, fmt.Errorf("read SSH_KEY: %w", err)
-		}
-		var signer ssh.Signer
-		if phrase := os.Getenv("SSH_KEY_PASSPHRASE"); phrase != "" {
-			signer, err = ssh.ParsePrivateKeyWithPassphrase(key, []byte(phrase))
-		} else {
-			signer, err = ssh.ParsePrivateKey(key)
-		}
-		if err != nil {
-			return sshDialer{}, fmt.Errorf("parse SSH_KEY: %w", err)
-		}
-		auth = append(auth, ssh.PublicKeys(signer))
-	}
-	if pass := os.Getenv("SSH_PASSWORD"); pass != "" {
-		auth = append(auth, ssh.Password(pass))
-	}
-	if len(auth) == 0 {
-		return sshDialer{}, fmt.Errorf("SSH_HOST is set but neither SSH_KEY nor SSH_PASSWORD is")
-	}
-
-	config := &ssh.ClientConfig{
-		User: user,
-		Auth: auth,
-		// Host key is not pinned; acceptable for a course prototype that only
-		// talks to the faculty server.
-		HostKeyCallback: ssh.InsecureIgnoreHostKey(),
-		Timeout:         10 * time.Second,
-	}
-	client, err := ssh.Dial("tcp", net.JoinHostPort(host, port), config)
-	if err != nil {
-		return sshDialer{}, fmt.Errorf("ssh dial %s@%s:%s: %w", user, host, port, err)
-	}
-	return sshDialer{client: client}, nil
-}
Index: rver/main.go
===================================================================
--- server/main.go	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,37 +1,0 @@
-package main
-
-import (
-	"flag"
-	"fmt"
-	"log"
-
-	"bp_project/server/db"
-)
-
-func main() {
-	initFlag := flag.Bool("init", false, "drop and recreate the project schema, then load sample data")
-	loadData := flag.Bool("load-data", false, "reload sample data (without dropping schema)")
-	flag.Parse()
-
-	if err := db.Connect(); err != nil {
-		log.Fatalf("database connect failed: %v", err)
-	}
-	defer db.DB.Close()
-
-	if *initFlag {
-		if err := db.InitSchema(); err != nil {
-			log.Fatalf("init failed: %v", err)
-		}
-		fmt.Println("Schema initialised. Re-run without -init to start the CLI.")
-		return
-	}
-	if *loadData {
-		if err := db.LoadData(); err != nil {
-			log.Fatalf("data load failed: %v", err)
-		}
-		fmt.Println("Sample data reloaded.")
-		return
-	}
-
-	RunCLI()
-}
Index: rver/market.go
===================================================================
--- server/market.go	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,129 +1,0 @@
-package main
-
-import (
-	"database/sql"
-	"fmt"
-	"strconv"
-
-	"bp_project/server/db"
-)
-
-// Market represents a trading pair.
-type Market struct {
-	ID       string
-	CryptoID string
-	Symbol   string
-	Quote    string
-}
-
-// ListMarkets prints all active markets, numbered, with their latest price,
-// and returns them in the printed order so a caller can pick one by number.
-func ListMarkets() []Market {
-	rows, err := db.DB.Query(`
-		SELECT m.id, c.id, c.symbol, m.quote_currency,
-		       COALESCE(lp.price, 0) AS price
-		  FROM markets m
-		  JOIN crypto  c  ON c.id = m.crypto_id
-		  LEFT JOIN v_latest_prices lp ON lp.market_id = m.id
-		 WHERE m.is_active = true
-		 ORDER BY c.symbol`)
-	if err != nil {
-		fmt.Println("Error:", err)
-		return nil
-	}
-	defer rows.Close()
-
-	fmt.Println()
-	fmt.Printf("  %-4s  %-8s  %-5s  %15s\n", "#", "Symbol", "Quote", "Last price")
-	fmt.Println("  -----------------------------------------")
-	var list []Market
-	for rows.Next() {
-		var m Market
-		var price float64
-		if err := rows.Scan(&m.ID, &m.CryptoID, &m.Symbol, &m.Quote, &price); err != nil {
-			fmt.Println("scan error:", err)
-			return nil
-		}
-		list = append(list, m)
-		fmt.Printf("  %-4d  %-8s  %-5s  %15.6f\n", len(list), m.Symbol, m.Quote, price)
-	}
-	return list
-}
-
-// pickNumber reads a 1-based choice from a list of n items.
-func pickNumber(label string, n int) (int, error) {
-	if n == 0 {
-		return 0, fmt.Errorf("Nothing to choose from.")
-	}
-	k, err := strconv.Atoi(prompt(label))
-	if err != nil || k < 1 || k > n {
-		return 0, fmt.Errorf("Invalid choice, enter a number from 1 to %d.", n)
-	}
-	return k - 1, nil
-}
-
-// ChooseMarket lists the active markets and lets the user pick one by its
-// number in the list.
-func ChooseMarket() (*Market, error) {
-	list := ListMarkets()
-	k, err := pickNumber("Market #: ", len(list))
-	if err != nil {
-		return nil, err
-	}
-	return &list[k], nil
-}
-
-// ChooseHolding lists only the markets the user can sell in — cryptos they
-// hold with some quantity still free (not reserved by an open sell order) —
-// with how much is held and free, and lets them pick one by number.
-func ChooseHolding(s *Session) (*Market, error) {
-	rows, err := db.DB.Query(`
-		SELECT m.id, c.id, c.symbol, m.quote_currency,
-		       h.quantity, h.quantity - h.reserved_quantity AS free,
-		       COALESCE(lp.price, 0) AS price
-		  FROM holdings h
-		  JOIN crypto  c ON c.id = h.crypto_id
-		  JOIN markets m ON m.crypto_id = c.id AND m.is_active = true
-		  LEFT JOIN v_latest_prices lp ON lp.market_id = m.id
-		 WHERE h.user_id = $1
-		   AND h.quantity - h.reserved_quantity > 0
-		 ORDER BY c.symbol`, s.UserID)
-	if err != nil {
-		return nil, err
-	}
-	defer rows.Close()
-
-	fmt.Println()
-	fmt.Printf("  %-4s  %-8s  %-5s  %12s  %12s  %15s\n", "#", "Symbol", "Quote", "Held", "Free to sell", "Last price")
-	fmt.Println("  -------------------------------------------------------------------")
-	var list []Market
-	for rows.Next() {
-		var m Market
-		var held, free, price float64
-		if err := rows.Scan(&m.ID, &m.CryptoID, &m.Symbol, &m.Quote, &held, &free, &price); err != nil {
-			return nil, err
-		}
-		list = append(list, m)
-		fmt.Printf("  %-4d  %-8s  %-5s  %12.4f  %12.4f  %15.6f\n", len(list), m.Symbol, m.Quote, held, free, price)
-	}
-	if len(list) == 0 {
-		return nil, fmt.Errorf("you hold no crypto that is free to sell")
-	}
-	k, err := pickNumber("Holding #: ", len(list))
-	if err != nil {
-		return nil, err
-	}
-	return &list[k], nil
-}
-
-// LatestPrice returns the last traded price on a market.
-func LatestPrice(marketID string) (float64, error) {
-	var price float64
-	err := db.DB.QueryRow(
-		`SELECT price FROM v_latest_prices WHERE market_id = $1`, marketID,
-	).Scan(&price)
-	if err == sql.ErrNoRows {
-		return 0, fmt.Errorf("no trades yet for this market")
-	}
-	return price, err
-}
Index: rver/portfolio.go
===================================================================
--- server/portfolio.go	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,71 +1,0 @@
-package main
-
-import (
-	"fmt"
-	"strings"
-
-	"bp_project/server/db"
-)
-
-// ShowPortfolio - UC0006
-// Uses the v_portfolio view to list holdings with current market value and P&L.
-func ShowPortfolio(s *Session) {
-	rows, err := db.DB.Query(
-		`SELECT symbol,
-		        quantity,
-		        COALESCE(reserved_quantity, 0),
-		        COALESCE(available_quantity, quantity),
-		        COALESCE(avg_price, 0),
-		        COALESCE(current_price, 0),
-		        COALESCE(market_value, 0),
-		        COALESCE(unrealized_pnl, 0)
-		   FROM v_portfolio
-		  WHERE user_id = $1 AND quantity > 0
-		  ORDER BY symbol`,
-		s.UserID,
-	)
-	if err != nil {
-		fmt.Println("Error:", err)
-		return
-	}
-	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.Println(header)
-	fmt.Println("  " + strings.Repeat("-", len(header)-2))
-
-	var totalValue, totalPnL float64
-	empty := true
-	for rows.Next() {
-		var sym string
-		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  %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
-		empty = false
-	}
-	if empty {
-		fmt.Println("  (no holdings yet)")
-		return
-	}
-	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
-	var avail, invested float64
-	_ = db.DB.QueryRow(
-		`SELECT available_balance, invested_balance FROM users WHERE id = $1`,
-		s.UserID,
-	).Scan(&avail, &invested)
-	fmt.Printf("\n  Cash available : %.4f USD\n", avail)
-	fmt.Printf("  Portfolio value: %.4f USD\n", totalValue)
-	fmt.Printf("  Net worth      : %.4f USD\n", avail+totalValue)
-}
Index: rver/reports.go
===================================================================
--- server/reports.go	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,105 +1,0 @@
-package main
-
-import (
-	"fmt"
-	"strings"
-	"time"
-
-	"bp_project/server/db"
-)
-
-// promptPeriod reads a [from, to) date range for the P6 reports.
-func promptPeriod() (time.Time, time.Time, bool) {
-	fromStr := prompt("From, inclusive (YYYY-MM-DD): ")
-	toStr := prompt("To, exclusive (YYYY-MM-DD): ")
-	from, err1 := time.Parse("2006-01-02", fromStr)
-	to, err2 := time.Parse("2006-01-02", toStr)
-	if err1 != nil || err2 != nil || !to.After(from) {
-		fmt.Println("Invalid date range.")
-		return time.Time{}, time.Time{}, false
-	}
-	return from, to, true
-}
-
-// ShowTopTraders - P6 report 1
-// Realized trading performance per user over a chosen period, via the
-// project.report_top_traders() SQL function (one query, bucketed by quarter
-// internally to measure consistency).
-func ShowTopTraders(s *Session) {
-	fmt.Println("\n-- Top traders report --")
-	from, to, ok := promptPeriod()
-	if !ok {
-		return
-	}
-
-	rows, err := db.DB.Query(`SELECT * FROM report_top_traders($1, $2)`, from, to)
-	if err != nil {
-		fmt.Println("Error:", err)
-		return
-	}
-	defer rows.Close()
-
-	header := fmt.Sprintf("  %-10s  %14s  %14s  %10s  %6s  %6s  %6s  %10s",
-		"Username", "Realized P/L", "Invested", "ROI %", "Prof.", "Loss", "Total", "Consist. %")
-	fmt.Println()
-	fmt.Println(header)
-	fmt.Println("  " + strings.Repeat("-", len(header)-2))
-
-	empty := true
-	for rows.Next() {
-		var username string
-		var realizedPL, invested, roi, consistency float64
-		var profitable, losing, total int64
-		if err := rows.Scan(&username, &realizedPL, &invested, &roi, &profitable, &losing, &total, &consistency); err != nil {
-			fmt.Println("scan error:", err)
-			return
-		}
-		fmt.Printf("  %-10s  %+14.4f  %14.4f  %10.2f  %6d  %6d  %6d  %10.2f\n",
-			username, realizedPL, invested, roi, profitable, losing, total, consistency)
-		empty = false
-	}
-	if empty {
-		fmt.Println("  (no buy/sell/fee transactions in that range)")
-	}
-}
-
-// ShowMarketPerformance - P6 report 2
-// Trading activity and price behaviour per market over a chosen period, via
-// the project.report_market_performance() SQL function.
-func ShowMarketPerformance(s *Session) {
-	fmt.Println("\n-- Market performance report --")
-	from, to, ok := promptPeriod()
-	if !ok {
-		return
-	}
-
-	rows, err := db.DB.Query(`SELECT * FROM report_market_performance($1, $2)`, from, to)
-	if err != nil {
-		fmt.Println("Error:", err)
-		return
-	}
-	defer rows.Close()
-
-	header := fmt.Sprintf("  %-6s  %-5s  %12s  %8s  %14s  %12s  %8s",
-		"Symbol", "Quote", "Volume", "Trades", "Avg Price", "Return %", "Users")
-	fmt.Println()
-	fmt.Println(header)
-	fmt.Println("  " + strings.Repeat("-", len(header)-2))
-
-	empty := true
-	for rows.Next() {
-		var symbol, quote string
-		var volume, avgPrice, returnPct float64
-		var tradeCount, users int64
-		if err := rows.Scan(&symbol, &quote, &volume, &tradeCount, &avgPrice, &returnPct, &users); err != nil {
-			fmt.Println("scan error:", err)
-			return
-		}
-		fmt.Printf("  %-6s  %-5s  %12.4f  %8d  %14.6f  %+12.2f  %8d\n",
-			symbol, quote, volume, tradeCount, avgPrice, returnPct, users)
-		empty = false
-	}
-	if empty {
-		fmt.Println("  (no market trades in that range)")
-	}
-}
Index: rver/trade.go
===================================================================
--- server/trade.go	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,262 +1,0 @@
-package main
-
-import (
-	"errors"
-	"fmt"
-	"strconv"
-	"strings"
-
-	"github.com/lib/pq"
-
-	"bp_project/server/db"
-)
-
-// PlaceOrder - UC0004 (buy) / UC0005 (sell)
-// Market or limit order. All the database work — checking free cash/crypto,
-// reserving it, recording the order, matching it against the order book and
-// filling the rest from the simulated market — is done by the P7 stored
-// function project.place_order in one call, so it is one atomic statement
-// and the P7 triggers keep orders, trades, holdings and balances consistent.
-func PlaceOrder(s *Session, side string) {
-	if side != "buy" && side != "sell" {
-		fmt.Println("Invalid side.")
-		return
-	}
-	fmt.Printf("\n-- Place %s order --\n", side)
-
-	// buy: any market; sell: only what the user holds and can still sell
-	var m *Market
-	var err error
-	if side == "buy" {
-		m, err = ChooseMarket()
-	} else {
-		m, err = ChooseHolding(s)
-	}
-	if err != nil {
-		fmt.Println(err)
-		return
-	}
-	price, err := LatestPrice(m.ID)
-	if err != nil {
-		fmt.Println(err)
-		return
-	}
-	fmt.Printf("Latest price for %s/%s = %.6f\n", m.Symbol, m.Quote, price)
-	printBookSide(m, side)
-
-	fmt.Println("[1] Market order (fills now at the best available price)")
-	fmt.Println("[2] Limit order (fills only at your price or better, otherwise waits in the order book)")
-	orderType := map[string]string{"1": "market", "2": "limit"}[prompt("> ")]
-	if orderType == "" {
-		fmt.Println("Unknown option.")
-		return
-	}
-
-	qty, err := strconv.ParseFloat(prompt("Quantity: "), 64)
-	if err != nil || qty <= 0 {
-		fmt.Println("Invalid quantity.")
-		return
-	}
-	var limit optFloat
-	if orderType == "limit" {
-		p, err := strconv.ParseFloat(prompt("Limit price: "), 64)
-		if err != nil || p <= 0 {
-			fmt.Println("Invalid price.")
-			return
-		}
-		limit = optFloat{p, true}
-	}
-
-	var orderID string
-	err = db.DB.QueryRow(
-		`SELECT place_order($1, $2, $3, $4, $5, $6)`,
-		s.UserID, m.ID, side, orderType, qty, limit.value(),
-	).Scan(&orderID)
-	if err != nil {
-		fmt.Println(dbMessage(err))
-		return
-	}
-
-	var status string
-	var filled, remaining float64
-	var avg *float64
-	if err := db.DB.QueryRow(
-		`SELECT status, filled_quantity, remaining, avg_fill_price
-		   FROM v_order_history WHERE order_id = $1`, orderID,
-	).Scan(&status, &filled, &remaining, &avg); err != nil {
-		fmt.Println("Error:", err)
-		return
-	}
-	switch status {
-	case "executed":
-		fmt.Printf("Order executed: %s %.4f %s, average price %.6f\n", side, filled, m.Symbol, *avg)
-	case "partially_filled":
-		fmt.Printf("Order partially filled: %.4f %s at average %.6f, %.4f waiting in the order book\n",
-			filled, m.Symbol, *avg, remaining)
-	default:
-		fmt.Printf("Order placed in the order book: %s %.4f %s at %.6f\n", side, remaining, m.Symbol, limit.v)
-	}
-}
-
-// optFloat is an optional float parameter (NULL when not set).
-type optFloat struct {
-	v  float64
-	ok bool
-}
-
-func (n optFloat) value() any {
-	if !n.ok {
-		return nil
-	}
-	return n.v
-}
-
-// dbMessage shows a rule the database refused (P7 raises check_violation
-// with a readable message) without the driver's prefix.
-func dbMessage(err error) string {
-	var pqErr *pq.Error
-	if errors.As(err, &pqErr) && pqErr.Code.Class() == "23" {
-		return "Rejected: " + pqErr.Message
-	}
-	return "Error: " + err.Error()
-}
-
-// printBookSide shows the best resting orders on the side this order would
-// trade against (asks for a buy, bids for a sell).
-func printBookSide(m *Market, side string) {
-	other, order := "sell", "price ASC"
-	if side == "sell" {
-		other, order = "buy", "price DESC"
-	}
-	rows, err := db.DB.Query(
-		`SELECT price, quantity, orders FROM v_order_book
-		  WHERE market_id = $1 AND side = $2 ORDER BY `+order+` LIMIT 5`, m.ID, other)
-	if err != nil {
-		fmt.Println("Error:", err)
-		return
-	}
-	defer rows.Close()
-	label := map[string]string{"sell": "asks", "buy": "bids"}[other]
-	first := true
-	for rows.Next() {
-		var price, qty float64
-		var n int
-		if err := rows.Scan(&price, &qty, &n); err != nil {
-			fmt.Println("scan error:", err)
-			return
-		}
-		if first {
-			fmt.Printf("Order book %s (other users' limit orders):\n", label)
-			first = false
-		}
-		fmt.Printf("  %14.6f  %12.4f  (%d orders)\n", price, qty, n)
-	}
-	if first {
-		fmt.Printf("Order book has no %s - a market order fills from the simulated market.\n", label)
-	}
-}
-
-// ShowOrderBook - P7 view v_order_book for one market.
-func ShowOrderBook() {
-	m, err := ChooseMarket()
-	if err != nil {
-		fmt.Println(err)
-		return
-	}
-	rows, err := db.DB.Query(
-		`SELECT side, price, quantity, orders FROM v_order_book
-		  WHERE market_id = $1
-		  ORDER BY side DESC, price DESC`, m.ID)
-	if err != nil {
-		fmt.Println("Error:", err)
-		return
-	}
-	defer rows.Close()
-	fmt.Printf("\n  Order book %s/%s\n", m.Symbol, m.Quote)
-	fmt.Printf("  %-5s  %14s  %12s  %6s\n", "Side", "Price", "Quantity", "Orders")
-	fmt.Println("  " + strings.Repeat("-", 44))
-	empty := true
-	for rows.Next() {
-		var side string
-		var price, qty float64
-		var n int
-		if err := rows.Scan(&side, &price, &qty, &n); err != nil {
-			fmt.Println("scan error:", err)
-			return
-		}
-		fmt.Printf("  %-5s  %14.6f  %12.4f  %6d\n", map[string]string{"sell": "ask", "buy": "bid"}[side], price, qty, n)
-		empty = false
-	}
-	if empty {
-		fmt.Println("  (no resting limit orders)")
-	}
-}
-
-// activeOrder is one row of the user's open orders list.
-type activeOrder struct {
-	id, symbol, side, typ, status string
-	qty, filled, remaining, price float64
-}
-
-// listMyOrders prints the user's active orders numbered 1..n (P7 view
-// v_active_orders) and returns them, so the user can pick one by number.
-func listMyOrders(s *Session) []activeOrder {
-	rows, err := db.DB.Query(
-		`SELECT order_id, symbol, side, type, status, quantity, filled_quantity, remaining, price
-		   FROM v_active_orders WHERE user_id = $1 ORDER BY placed_at`, s.UserID)
-	if err != nil {
-		fmt.Println("Error:", err)
-		return nil
-	}
-	defer rows.Close()
-	var list []activeOrder
-	for rows.Next() {
-		var o activeOrder
-		if err := rows.Scan(&o.id, &o.symbol, &o.side, &o.typ, &o.status,
-			&o.qty, &o.filled, &o.remaining, &o.price); err != nil {
-			fmt.Println("scan error:", err)
-			return nil
-		}
-		list = append(list, o)
-	}
-	fmt.Println()
-	if len(list) == 0 {
-		fmt.Println("  (no open orders)")
-		return nil
-	}
-	fmt.Printf("  %-3s  %-6s  %-4s  %-6s  %-16s  %10s  %10s  %14s\n",
-		"#", "Symbol", "Side", "Type", "Status", "Filled", "Remaining", "Price")
-	fmt.Println("  " + strings.Repeat("-", 84))
-	for i, o := range list {
-		fmt.Printf("  %-3d  %-6s  %-4s  %-6s  %-16s  %10.4f  %10.4f  %14.6f\n",
-			i+1, o.symbol, o.side, o.typ, o.status, o.filled, o.remaining, o.price)
-	}
-	return list
-}
-
-// ShowMyOrders - P7 view v_active_orders.
-func ShowMyOrders(s *Session) {
-	listMyOrders(s)
-}
-
-// CancelOrder - P7 stored function project.cancel_order, which releases
-// the reserved cash or crypto of what is still unfilled.
-func CancelOrder(s *Session) {
-	list := listMyOrders(s)
-	if len(list) == 0 {
-		return
-	}
-	n, err := strconv.Atoi(prompt("Order # to cancel (0 = back): "))
-	if err != nil || n < 0 || n > len(list) {
-		fmt.Println("Invalid choice.")
-		return
-	}
-	if n == 0 {
-		return
-	}
-	if _, err := db.DB.Exec(`SELECT cancel_order($1, $2)`, list[n-1].id, s.UserID); err != nil {
-		fmt.Println(dbMessage(err))
-		return
-	}
-	fmt.Println("Order cancelled; its reservation was released.")
-}
Index: rver/watchlist.go
===================================================================
--- server/watchlist.go	(revision 1549dae4501012cfe2bffe1c5e9bdfdfb9dd62bd)
+++ 	(revision )
@@ -1,191 +1,0 @@
-package main
-
-import (
-	"database/sql"
-	"fmt"
-
-	"bp_project/server/db"
-)
-
-// ManageWatchlist - UC0007
-// Ensures the user has a default watchlist, then allows listing, adding,
-// removing entries.
-func ManageWatchlist(s *Session) {
-	wlID, err := ensureDefaultWatchlist(s.UserID)
-	if err != nil {
-		fmt.Println("Error:", err)
-		return
-	}
-	for {
-		fmt.Println("\n-- Watchlist --")
-		fmt.Println("[1] List items")
-		fmt.Println("[2] Add crypto")
-		fmt.Println("[3] Remove crypto")
-		fmt.Println("[0] Back")
-		switch prompt("> ") {
-		case "1":
-			listWatchlist(wlID)
-		case "2":
-			addToWatchlist(wlID)
-		case "3":
-			removeFromWatchlist(wlID)
-		case "0":
-			return
-		default:
-			fmt.Println("Unknown option.")
-		}
-	}
-}
-
-func ensureDefaultWatchlist(userID string) (string, error) {
-	var id string
-	err := db.DB.QueryRow(
-		`SELECT id FROM watchlists WHERE user_id = $1 ORDER BY created_at LIMIT 1`,
-		userID,
-	).Scan(&id)
-	if err == sql.ErrNoRows {
-		err = db.DB.QueryRow(
-			`INSERT INTO watchlists (user_id, name) VALUES ($1, 'Favorites') RETURNING id`,
-			userID,
-		).Scan(&id)
-		return id, err
-	}
-	return id, err
-}
-
-func listWatchlist(wlID string) {
-	rows, err := db.DB.Query(`
-		SELECT c.symbol, c.name, COALESCE(lp.price, 0)
-		  FROM watchlist_items wi
-		  JOIN crypto  c  ON c.id = wi.crypto_id
-		  LEFT JOIN markets       m  ON m.crypto_id = c.id AND m.quote_currency = 'USD'
-		  LEFT JOIN v_latest_prices lp ON lp.market_id = m.id
-		 WHERE wi.watchlist_id = $1
-		 ORDER BY c.symbol`, wlID)
-	if err != nil {
-		fmt.Println("Error:", err)
-		return
-	}
-	defer rows.Close()
-
-	fmt.Println()
-	fmt.Printf("  %-8s  %-20s  %15s\n", "Symbol", "Name", "Last price")
-	fmt.Println("  --------------------------------------------------")
-	empty := true
-	for rows.Next() {
-		var sym, name string
-		var price float64
-		if err := rows.Scan(&sym, &name, &price); err != nil {
-			fmt.Println("scan error:", err)
-			return
-		}
-		fmt.Printf("  %-8s  %-20s  %15.6f\n", sym, name, price)
-		empty = false
-	}
-	if empty {
-		fmt.Println("  (watchlist is empty)")
-	}
-}
-
-// addToWatchlist lists the cryptos that are not on the watchlist yet,
-// numbered, and adds the one the user picks.
-func addToWatchlist(wlID string) {
-	rows, err := db.DB.Query(`
-		SELECT c.id, c.symbol, c.name
-		  FROM crypto c
-		 WHERE NOT EXISTS (SELECT 1 FROM watchlist_items wi
-		                    WHERE wi.watchlist_id = $1 AND wi.crypto_id = c.id)
-		 ORDER BY c.symbol`, wlID)
-	if err != nil {
-		fmt.Println("Error:", err)
-		return
-	}
-	type option struct{ id, symbol, name string }
-	var list []option
-	for rows.Next() {
-		var o option
-		if err := rows.Scan(&o.id, &o.symbol, &o.name); err != nil {
-			rows.Close()
-			fmt.Println("scan error:", err)
-			return
-		}
-		list = append(list, o)
-	}
-	rows.Close()
-	if len(list) == 0 {
-		fmt.Println("Every crypto is already on your watchlist.")
-		return
-	}
-	fmt.Println()
-	fmt.Printf("  %-4s  %-8s  %s\n", "#", "Symbol", "Name")
-	fmt.Println("  ------------------------------")
-	for i, o := range list {
-		fmt.Printf("  %-4d  %-8s  %s\n", i+1, o.symbol, o.name)
-	}
-	k, err := pickNumber("Crypto # to add: ", len(list))
-	if err != nil {
-		fmt.Println(err)
-		return
-	}
-	_, err = db.DB.Exec(
-		`INSERT INTO watchlist_items (watchlist_id, crypto_id)
-		 VALUES ($1, $2)
-		 ON CONFLICT (watchlist_id, crypto_id) DO NOTHING`,
-		wlID, list[k].id,
-	)
-	if err != nil {
-		fmt.Println("Error:", err)
-		return
-	}
-	fmt.Printf("Added %s.\n", list[k].symbol)
-}
-
-// removeFromWatchlist lists the watchlist's cryptos, numbered, and removes
-// the one the user picks.
-func removeFromWatchlist(wlID string) {
-	rows, err := db.DB.Query(`
-		SELECT c.id, c.symbol, c.name
-		  FROM watchlist_items wi
-		  JOIN crypto c ON c.id = wi.crypto_id
-		 WHERE wi.watchlist_id = $1
-		 ORDER BY c.symbol`, wlID)
-	if err != nil {
-		fmt.Println("Error:", err)
-		return
-	}
-	type option struct{ id, symbol, name string }
-	var list []option
-	for rows.Next() {
-		var o option
-		if err := rows.Scan(&o.id, &o.symbol, &o.name); err != nil {
-			rows.Close()
-			fmt.Println("scan error:", err)
-			return
-		}
-		list = append(list, o)
-	}
-	rows.Close()
-	if len(list) == 0 {
-		fmt.Println("Your watchlist is empty.")
-		return
-	}
-	fmt.Println()
-	fmt.Printf("  %-4s  %-8s  %s\n", "#", "Symbol", "Name")
-	fmt.Println("  ------------------------------")
-	for i, o := range list {
-		fmt.Printf("  %-4d  %-8s  %s\n", i+1, o.symbol, o.name)
-	}
-	k, err := pickNumber("Crypto # to remove: ", len(list))
-	if err != nil {
-		fmt.Println(err)
-		return
-	}
-	if _, err := db.DB.Exec(
-		`DELETE FROM watchlist_items WHERE watchlist_id = $1 AND crypto_id = $2`,
-		wlID, list[k].id,
-	); err != nil {
-		fmt.Println("Error:", err)
-		return
-	}
-	fmt.Printf("Removed %s.\n", list[k].symbol)
-}
