Changes between Version 7 and Version 8 of Prototype


Ignore:
Timestamp:
09/24/26 13:58:04 (3 days ago)
Author:
231285
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • Prototype

    v7 v8  
    11= Prototype Implementation =
    2 
    3 The prototype is a Go command-line application in
    4 `server/` that works against the `project`
    5 schema in PostgreSQL. It implements all seven use cases from
    6 !UseCaseModel
    7 – the rubric requires at least three – with every database access shown as real, executed SQL.
    8 An auxiliary program in `bots/` simulates a
    9 live market so prices move while the prototype is running.
    10 
    11 `bots/main.go`:
    12 
    13 {{{
    14 // EduBerza market simulation bot.
    15 //
    16 // Walks a small price for every active market, inserts rows into
    17 // market_trades once per tick, and upserts the current 1m candle.
    18 //
    19 // Run from the repo root so the relative .env path resolves:
    20 //
    21 //      go run ./bots/...
    22 package main
    23 
    24 import (
    25         "bufio"
    26         "database/sql"
    27         "flag"
    28         "fmt"
    29         "log"
    30         "math/rand"
    31         "os"
    32         "strings"
    33         "time"
    34 
    35         _ "github.com/lib/pq"
    36 )
    37 
    38 type market struct {
    39         id     string
    40         symbol string
    41         price  float64
    42 }
    43 
    44 func main() {
    45         interval := flag.Duration("interval", 3*time.Second, "seconds between price ticks")
    46         flag.Parse()
    47 
    48         loadEnv(".env")
    49         dsn := fmt.Sprintf(
    50                 "host=%s port=%s user=%s password=%s dbname=%s sslmode=disable options='--search_path=project,public'",
    51                 env("DBHOST", "localhost"),
    52                 env("DBPORT", "5432"),
    53                 env("DBUSER", "postgres"),
    54                 env("DBPASSWORD", ""),
    55                 env("DBNAME", "postgres"),
    56         )
    57         db, err := sql.Open("postgres", dsn)
    58         if err != nil {
    59                 log.Fatalf("open db: %v", err)
    60         }
    61         defer db.Close()
    62         if err := db.Ping(); err != nil {
    63                 log.Fatalf("ping: %v", err)
    64         }
    65 
    66         markets, err := loadMarkets(db)
    67         if err != nil {
    68                 log.Fatalf("load markets: %v", err)
    69         }
    70         if len(markets) == 0 {
    71                 log.Fatal("no active markets found - run `go run ./server -init` first")
    72         }
    73 
    74         log.Printf("bot started. simulating %d markets every %s", len(markets), *interval)
    75         rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
    76 
    77         for {
    78                 for i := range markets {
    79                         m := &markets[i]
    80                         // random walk: ±0.3% per tick
    81                         drift := (rnd.Float64() - 0.5) * 0.006
    82                         m.price = m.price * (1 + drift)
    83                         if m.price <= 0 {
    84                                 m.price = 0.000001
    85                         }
    86                         qty := rnd.Float64()*0.5 + 0.01
    87 
    88                         side := "buy"
    89                         if rnd.Float64() < 0.5 {
    90                                 side = "sell"
    91                         }
    92 
    93                         if err := insertTick(db, m.id, m.price, qty, side); err != nil {
    94                                 log.Printf("insert tick %s: %v", m.symbol, err)
    95                                 continue
    96                         }
    97                         log.Printf("  %-8s  %.6f  qty=%.4f  side=%s", m.symbol, m.price, qty, side)
    98                 }
    99                 time.Sleep(*interval)
    100         }
    101 }
    102 
    103 func loadMarkets(db *sql.DB) ([]market, error) {
    104         rows, err := db.Query(`
    105                 SELECT m.id, c.symbol, COALESCE(lp.price, 100)
    106                   FROM markets m
    107                   JOIN crypto  c  ON c.id = m.crypto_id
    108                   LEFT JOIN v_latest_prices lp ON lp.market_id = m.id
    109                  WHERE m.is_active = true
    110                  ORDER BY c.symbol`)
    111         if err != nil {
    112                 return nil, err
    113         }
    114         defer rows.Close()
    115         var out []market
    116         for rows.Next() {
    117                 var m market
    118                 if err := rows.Scan(&m.id, &m.symbol, &m.price); err != nil {
    119                         return nil, err
    120                 }
    121                 out = append(out, m)
    122         }
    123         return out, nil
    124 }
    125 
    126 func insertTick(db *sql.DB, marketID string, price, qty float64, side string) error {
    127         tx, err := db.Begin()
    128         if err != nil {
    129                 return err
    130         }
    131         defer tx.Rollback()
    132 
    133         if _, err := tx.Exec(
    134                 `INSERT INTO market_trades (market_id, executed_at, price, quantity, side, source)
    135                  VALUES ($1, now(), $2, $3, $4, 'simulation')`,
    136                 marketID, price, qty, side,
    137         ); err != nil {
    138                 return err
    139         }
    140 
    141         // upsert the current 1m candle
    142         if _, err := tx.Exec(`
    143                 INSERT INTO market_candles (market_id, timeframe, open, high, low, close, volume, candle_time)
    144                 VALUES ($1, '1m', $2, $2, $2, $2, $3, date_trunc('minute', now()))
    145                 ON CONFLICT (market_id, timeframe, candle_time) DO UPDATE
    146                    SET high   = GREATEST(market_candles.high, EXCLUDED.close),
    147                        low    = LEAST(   market_candles.low,  EXCLUDED.close),
    148                        close  = EXCLUDED.close,
    149                        volume = market_candles.volume + EXCLUDED.volume`,
    150                 marketID, price, qty,
    151         ); err != nil {
    152                 return err
    153         }
    154         return tx.Commit()
    155 }
    156 
    157 func loadEnv(path string) {
    158         f, err := os.Open(path)
    159         if err != nil {
    160                 return
    161         }
    162         defer f.Close()
    163         s := bufio.NewScanner(f)
    164         for s.Scan() {
    165                 line := strings.TrimSpace(s.Text())
    166                 if line == "" || strings.HasPrefix(line, "#") {
    167                         continue
    168                 }
    169                 parts := strings.SplitN(line, "=", 2)
    170                 if len(parts) == 2 {
    171                         os.Setenv(strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]))
    172                 }
    173         }
    174 }
    175 
    176 func env(k, def string) string {
    177         if v := os.Getenv(k); v != "" {
    178                 return v
    179         }
    180         return def
    181 }
    182 }}}
    183 
    184 Build, configure, run and test instructions:
    185 !BuildInstructions.
    186 
    187 All pages listed below, together with the screenshots of each run, are kept in the project's
    188 !GitHub repository, !StefanTrsunov/bp, under
    189 `docs/P4-Prototype/`.
    1902
    1913== Implemented use-cases ==
    1924
    193 ||=Page=||=Use-case=||=Source=||
    194 ||UseCase0001Implementation||Register a new account||`server/auth.go`||
    195 ||UseCase0002Implementation||Log in||`server/auth.go`||
    196 ||UseCase0003Implementation||Deposit virtual funds||`server/account.go`||
    197 ||UseCase0004Implementation||Place market BUY order||`server/trade.go`||
    198 ||UseCase0005Implementation||Place market SELL order||`server/trade.go`||
    199 ||UseCase0006Implementation||View portfolio and history||`server/portfolio.go`||
    200 ||UseCase0007Implementation||Manage watchlist||`server/watchlist.go`||
     5 * [wiki:UseCase0001Implementation] — Register new account
     6 * [wiki:UseCase0002Implementation] — Log in
     7 * [wiki:UseCase0003Implementation] — Deposit virtual funds
     8 * [wiki:UseCase0004Implementation] — Place market BUY order
     9 * [wiki:UseCase0005Implementation] — Place market SELL order
     10 * [wiki:UseCase0006Implementation] — View portfolio and transaction history
     11 * [wiki:UseCase0007Implementation] — Manage watchlist
    20112
    202 Each page mirrors its P3 use-case page and adds the actual SQL emitted by the Go code plus a
    203 screenshot of the corresponding run against the live database. The screenshots are committed
    204 alongside the pages, in
    205 `docs/P4-Prototype/screenshots/`.
     13Each page follows its P3 use case step by step. It adds the exact SQL the Go code runs in that
     14step and a screenshot of the step from a real run against the database.
     15
     16 * How to build, configure, run and test the prototype: [wiki:BuildInstructions]
     17 * AI usage for this phase: [wiki:PrototypeImplementationAIUsage]
     18
     19== Overview ==
     20
     21!EduBerza's P4 prototype is a Go command-line program. It works against the `project` schema in
     22PostgreSQL. It implements all seven use cases from [wiki:UseCaseModel]; the course asks for at
     23least three. Every database access is real SQL that was executed and tested. A second program,
     24the market bot, simulates a live market so prices move while the prototype runs.
     25
     26The source code is in the project's git repository: the CLI in `server/`, the bot in `bots/`,
     27and the SQL scripts in `server/db/`.
     28
     29== Technology and architecture ==
     30
     31 * '''Language:''' Go (module `bp_project`, `go 1.25` in `go.mod`). The only third-party
     32   library is the PostgreSQL driver `github.com/lib/pq`.
     33 * '''Database:''' PostgreSQL. Every table, view and function is in the `project` schema. The
     34   DDL is `schema_creation.sql` and the sample data is `data_load.sql`. Both scripts are
     35   compiled into the binary and run by `./eduberza -init`.
     36 * '''Interface:''' plain text menus on standard input and output. There are no web server,
     37   frameworks, images or styles.
     38 * '''Structure:''' one source file per area of the application.
     39
     40||= File =||= Responsibility =||= Use cases =||
     41|| `server/main.go` || Flags `-init` / `-load-data`, then starts the menu loop || — ||
     42|| `server/cli.go` || The two menus (before and after login), input reading || all ||
     43|| `server/db/db.go` || Connection from `.env` / environment variables, embedded SQL scripts || — ||
     44|| `server/auth.go` || Register, log in (SHA-256 password hash) || UC0001, UC0002 ||
     45|| `server/account.go` || Balance, deposit, transaction history || UC0003, UC0006 ||
     46|| `server/market.go` || Market list, choosing a market or a holding by number, latest price || UC0004, UC0005 ||
     47|| `server/trade.go` || Market buy and sell orders, each in one transaction || UC0004, UC0005 ||
     48|| `server/portfolio.go` || Portfolio with current value and unrealised P/L || UC0006 ||
     49|| `server/watchlist.go` || List, add and remove watchlist items || UC0007 ||
     50|| `bots/main.go` || Market bot: random-walk price ticks into `market_trades`, 1-minute candles || — ||
     51
     52== No identifiers to remember ==
     53
     54The user never has to type or remember an id, a code or a symbol:
     55
     56 * Every menu is numbered, and the user answers with the number of an option.
     57 * '''Buying:''' all active markets are listed with their latest price, numbered 1…n. The user
     58   enters the market's number at `Market #:` (`ChooseMarket` in `market.go`).
     59 * '''Selling:''' only the cryptos the user actually holds are listed, each with the quantity
     60   held and the quantity still free to sell. The user enters the holding's number at
     61   `Holding #:` (`ChooseHolding`). A user who holds nothing free to sell gets
     62   `you hold no crypto that is free to sell` and is never asked to choose.
     63 * '''Watchlist:''' ''Add'' lists the cryptos that are not on the watchlist yet. ''Remove''
     64   lists the ones that are on it. Both are numbered, and the user enters the number.
     65 * A number outside the list is refused with `Invalid choice, enter a number from 1 to N.` and
     66   nothing is changed.
     67
     68The only things the user types are their own data: username, e-mail, full name, password, the
     69amount to deposit, the quantity to buy or sell, and the date range of the two P6 reports.
    20670
    20771== What the prototype demonstrates about the database design ==
    20872
    209  * '''The current price is never stored as a column.''' It is always the price of the most recent
    210    row in `market_trades`, read through the `v_latest_prices` view. Both the user's own fills and
    211    the bot's simulated trades feed the same table, so there is exactly one definition of "the
    212    price".
    213  * '''Money movements are transactional.''' Buying touches five tables – `orders`, `users`,
    214    `holdings`, `transactions`, `market_trades` – inside one transaction. A failed balance check
    215    rolls the whole thing back: after a rejected purchase there is no order row, no ledger entry
    216    and no holding. This is verified in the failure-path tests in
    217    !BuildInstructions.
    218  * '''Constraints do real work.''' `UNIQUE (user_id, crypto_id)` on `holdings` is what makes the
    219    `INSERT … ON CONFLICT DO UPDATE` upsert possible, so the weighted-average entry price is
    220    recomputed by the database in one statement instead of by a read-modify-write in application
    221    code. `CHECK (reserved_quantity >= 0 AND reserved_quantity <= quantity)` is the same idea
    222    applied to the sell path: an inconsistent reservation is impossible at the database level, not
    223    just something `trade.go` is careful about.
    224  * '''Selling reserves before it removes.''' A sell order locks the holding row, reserves the
    225    quantity being sold, then settles by removing it — see
    226    UseCase0005Implementation. Two sell orders placed at the same
    227    instant for more than the available quantity are serialised correctly by `SELECT ... FOR
    228    UPDATE`, not just by luck of everything happening in one CLI process; this is demonstrated
    229    there with two concurrent processes.
    230  * '''No identifiers are ever typed.''' Markets are listed with their prices before any choice is
    231    made, and everything else is selected by symbol.
     73 * '''The current price is never stored as a column.''' It is always the price of the most
     74   recent row in `market_trades`, read through the `v_latest_prices` view. The user's own fills
     75   and the bot's simulated trades go into the same table, so there is only one definition of
     76   "the price".
     77 * '''Money movements are transactional.''' A buy touches five tables (`orders`, `users`,
     78   `holdings`, `transactions`, `market_trades`) inside one transaction. If the balance check
     79   fails, the whole transaction is rolled back: after a rejected purchase there is no order
     80   row, no ledger entry and no holding. The failure-path tests in [wiki:BuildInstructions]
     81   check this.
     82 * '''Constraints do real work.''' `UNIQUE (user_id, crypto_id)` on `holdings` is what makes
     83   the `INSERT … ON CONFLICT DO UPDATE` upsert possible, so the database recomputes the
     84   weighted-average entry price in one statement, instead of the application reading, changing
     85   and writing the row. `CHECK (reserved_quantity >= 0 AND reserved_quantity <= quantity)` does
     86   the same for the sell path: the database itself makes an inconsistent reservation
     87   impossible, and it does not rely only on `trade.go` being careful.
     88 * '''Selling reserves before it removes.''' A sell order locks the holding row with
     89   `SELECT … FOR UPDATE`, reserves the quantity being sold, then settles by removing it (see
     90   [wiki:UseCase0005Implementation]). Two sell orders for more than the free quantity, placed
     91   at the same moment from two separate processes, are serialised by the row lock. Exactly one
     92   of them succeeds. This was tested with two concurrent processes in session 3 (see
     93   [wiki:PrototypeImplementationAIUsage]).
    23294
    23395== Known limitations ==
    23496
    235 Deliberately out of scope for a first prototype, and the natural content of the later phases:
     97These were left out on purpose for a first prototype. They belong to the later phases:
    23698
    237  * Only `market` orders execute. `limit` is accepted by the schema (`orders.type`) but the
    238    matching logic is not implemented.
    239  * Passwords are SHA-256 without a salt. Adequate to demonstrate that the password itself is
    240    never stored; not adequate for real use. A proper password hash belongs in P9 (security).
    241  * Money is handled as `float64` in Go while the database columns are `numeric`. All arithmetic
    242    that must be exact – the weighted average – is done in SQL for that reason, but the Go side
    243    would need a decimal type for real use.
    244  * There is no connection pooling configuration and no explicit isolation level; both are P8
    245    topics.
    246  * Reservation only ever lives inside one transaction, because only market orders (which settle
    247    immediately) exist. A real limit-order matcher would leave `holdings.reserved_quantity` set
    248    and `orders.status = 'open'` between two separate commits, and would need a way to cancel an
    249    order to release the reservation — neither is implemented, since nothing in the prototype
    250    produces an order that stays open.
     99 * Only `market` orders execute. The schema accepts `limit` (`orders.type`), but there is no
     100   matching logic for it.
     101 * Passwords are hashed with SHA-256 and no salt. That shows the password itself is never
     102   stored, but it is not good enough for real use. A proper password hash belongs in P9
     103   (security).
     104 * Money is `float64` in Go, while the database columns are `numeric`. For that reason, all
     105   arithmetic that must be exact (the weighted average) is done in SQL. Real use would need a
     106   decimal type on the Go side too.
     107 * The prototype sets no connection pool and no explicit isolation level. Both are P8 topics.
     108 * A reservation only exists inside one transaction, because the prototype only has market
     109   orders, and they settle immediately. A real limit-order matcher would leave
     110   `holdings.reserved_quantity` set and `orders.status = 'open'` between two separate commits.
     111   It would also need a way to cancel an order and release the reservation. Neither is
     112   implemented, because nothing in the prototype creates an order that stays open.
    251113
    252 == AI usage ==
     114== History of changes ==
    253115
    254 AI was used in this phase and is logged in full, per the course rule for P1 onward.
     116The code started as my own Go backend: HTTP handlers, a draft schema, and a `db.go` that
     117recreated the tables on every start. It changed as follows. The AI's share of each change is
     118logged in [wiki:PrototypeImplementationAIUsage].
    255119
    256  * '''Phase log:'''
    257    PrototypeImplementationAIUsage
    258    – service used, the bugs found and fixed, the test evidence, and what I decided myself.
    259  * '''Full conversation transcript:'''
    260    ERModelAIUsage
    261    – the same conversation produced the P1–P4 artefacts, so the complete prompt/response log is
    262    kept in one place. Its sections:
    263    Session 1 – 2026-04-21,
    264    Session 2 – 2026-08-06/07,
    265    Session 3 – 2026-09-16.
     120||= Date =||= Change =||= Origin =||
     121|| 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) ||
     122|| 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) ||
     123|| 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) ||
     124|| 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) ||
    266125
    267 '''Service:''' Claude Code (Anthropic), `https://claude.com/claude-code` – Claude subscription,
    268 model Claude Opus 4.7 (1M context) in sessions 1–2, Claude Sonnet 5 in session 3.
    269 
    270 '''In short:''' session 1 rewrote the existing Chi/HTTP backend as the CLI prototype covering
    271 UC0001–UC0007 and added the market bot. Session 2 was a review pass I asked for, which found and
    272 fixed three bugs – a path-resolution bug that made the documented build instructions fail, an
    273 infinite loop at end of input, and an error check in the wrong order that misreported database
    274 failures as "Insufficient holding" – and replaced the read-modify-write holding update with a
    275 single `INSERT … ON CONFLICT DO UPDATE`. Session 3 added `holdings.reserved_quantity` and changed
    276 `trade.go`'s sell path to reserve crypto before removing it, closing a gap where two sell orders
    277 could be granted the same units; see the "Session 3 — 2026-09-16" section of
    278 PrototypeImplementationAIUsage.
     126'''Service:''' Claude Code (Anthropic), Claude subscription. Session 1 used Claude Opus 4.7 (1M
     127context), session 2 Claude Opus 5 (1M context), session 3 Claude Sonnet 5, and session 4
     128Claude Opus 5.5 (1M context).