Index: server/db/data_load.sql
===================================================================
--- server/db/data_load.sql	(revision b715712c7d5d3ee27c932a8a9d8e0a2bc6e85127)
+++ server/db/data_load.sql	(revision b715712c7d5d3ee27c932a8a9d8e0a2bc6e85127)
@@ -0,0 +1,144 @@
+-- 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
+
+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.
+-- ============================================================================
+INSERT INTO project.orders (id, user_id, market_id, side, type, status, 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, 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'),
+    ('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');
Index: server/db/db.go
===================================================================
--- server/db/db.go	(revision b715712c7d5d3ee27c932a8a9d8e0a2bc6e85127)
+++ server/db/db.go	(revision b715712c7d5d3ee27c932a8a9d8e0a2bc6e85127)
@@ -0,0 +1,153 @@
+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 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,
+	)
+
+	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 then data_load.sql.
+// Destructive: drops the `project` schema. Intended for the -init flag.
+func InitSchema() error {
+	log.Println("Running schema_creation.sql ...")
+	if err := runScript("schema_creation.sql"); 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: server/db/schema_creation.sql
===================================================================
--- server/db/schema_creation.sql	(revision b715712c7d5d3ee27c932a8a9d8e0a2bc6e85127)
+++ server/db/schema_creation.sql	(revision b715712c7d5d3ee27c932a8a9d8e0a2bc6e85127)
@@ -0,0 +1,193 @@
+-- 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),
+    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),
+    -- 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', 'executed', 'cancelled')),
+    quantity    numeric(20,4)  NOT NULL CHECK (quantity > 0),
+    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'
+);
+
+CREATE INDEX idx_market_trades_market_time ON project.market_trades(market_id, executed_at DESC);
+
+-- ============================================================================
+-- 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.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;
