Changes between Version 4 and Version 5 of Prototype


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

--

Legend:

Unmodified
Added
Removed
Modified
  • Prototype

    v4 v5  
    22
    33The prototype is a Go command-line application in
    4 [https://github.com/StefanTrsunov/bp/tree/main/server server/] that works against the `project`
     4`server/` that works against the `project`
    55schema in PostgreSQL. It implements all seven use cases from
    6 [https://github.com/StefanTrsunov/bp/blob/main/docs/P3-UseCaseModel/UseCaseModel.md UseCaseModel]
     6!UseCaseModel
    77– the rubric requires at least three – with every database access shown as real, executed SQL.
    8 An auxiliary program in [https://github.com/StefanTrsunov/bp/tree/main/bots bots/] simulates a
     8An auxiliary program in `bots/` simulates a
    99live market so prices move while the prototype is running.
    1010
     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/...
     22package main
     23
     24import (
     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
     38type market struct {
     39        id     string
     40        symbol string
     41        price  float64
     42}
     43
     44func 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
     103func 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
     126func 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
     157func 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
     176func env(k, def string) string {
     177        if v := os.Getenv(k); v != "" {
     178                return v
     179        }
     180        return def
     181}
     182}}}
     183
    11184Build, configure, run and test instructions:
    12 [https://github.com/StefanTrsunov/bp/blob/main/docs/P4-Prototype/BuildInstructions.md BuildInstructions].
     185!BuildInstructions.
    13186
    14187All pages listed below, together with the screenshots of each run, are kept in the project's
    15 GitHub repository, [https://github.com/StefanTrsunov/bp StefanTrsunov/bp], under
     188!GitHub repository, !StefanTrsunov/bp, under
    16189`docs/P4-Prototype/`.
    17190
    … …  
    19192
    20193||=Page=||=Use-case=||=Source=||
    21 ||[https://github.com/StefanTrsunov/bp/blob/main/docs/P4-Prototype/UseCase0001Implementation.md UseCase0001Implementation]||Register a new account||[https://github.com/StefanTrsunov/bp/blob/main/server/auth.go server/auth.go]||
    22 ||[https://github.com/StefanTrsunov/bp/blob/main/docs/P4-Prototype/UseCase0002Implementation.md UseCase0002Implementation]||Log in||[https://github.com/StefanTrsunov/bp/blob/main/server/auth.go server/auth.go]||
    23 ||[https://github.com/StefanTrsunov/bp/blob/main/docs/P4-Prototype/UseCase0003Implementation.md UseCase0003Implementation]||Deposit virtual funds||[https://github.com/StefanTrsunov/bp/blob/main/server/account.go server/account.go]||
    24 ||[https://github.com/StefanTrsunov/bp/blob/main/docs/P4-Prototype/UseCase0004Implementation.md UseCase0004Implementation]||Place market BUY order||[https://github.com/StefanTrsunov/bp/blob/main/server/trade.go server/trade.go]||
    25 ||[https://github.com/StefanTrsunov/bp/blob/main/docs/P4-Prototype/UseCase0005Implementation.md UseCase0005Implementation]||Place market SELL order||[https://github.com/StefanTrsunov/bp/blob/main/server/trade.go server/trade.go]||
    26 ||[https://github.com/StefanTrsunov/bp/blob/main/docs/P4-Prototype/UseCase0006Implementation.md UseCase0006Implementation]||View portfolio and history||[https://github.com/StefanTrsunov/bp/blob/main/server/portfolio.go server/portfolio.go]||
    27 ||[https://github.com/StefanTrsunov/bp/blob/main/docs/P4-Prototype/UseCase0007Implementation.md UseCase0007Implementation]||Manage watchlist||[https://github.com/StefanTrsunov/bp/blob/main/server/watchlist.go server/watchlist.go]||
     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`||
    28201
    29202Each page mirrors its P3 use-case page and adds the actual SQL emitted by the Go code plus a
    30203screenshot of the corresponding run against the live database. The screenshots are committed
    31204alongside the pages, in
    32 [https://github.com/StefanTrsunov/bp/tree/main/docs/P4-Prototype/screenshots docs/P4-Prototype/screenshots/].
     205`docs/P4-Prototype/screenshots/`.
    33206
    34207== What the prototype demonstrates about the database design ==
    … …  
    42215   rolls the whole thing back: after a rejected purchase there is no order row, no ledger entry
    43216   and no holding. This is verified in the failure-path tests in
    44    [https://github.com/StefanTrsunov/bp/blob/main/docs/P4-Prototype/BuildInstructions.md BuildInstructions].
     217   !BuildInstructions.
    45218 * '''Constraints do real work.''' `UNIQUE (user_id, crypto_id)` on `holdings` is what makes the
    46219   `INSERT … ON CONFLICT DO UPDATE` upsert possible, so the weighted-average entry price is
    47220   recomputed by the database in one statement instead of by a read-modify-write in application
    48    code.
     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.
    49230 * '''No identifiers are ever typed.''' Markets are listed with their prices before any choice is
    50231   made, and everything else is selected by symbol.
    … …  
    63244 * There is no connection pooling configuration and no explicit isolation level; both are P8
    64245   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.
    65251
    66252== AI usage ==
    … …  
    69255
    70256 * '''Phase log:'''
    71    [https://github.com/StefanTrsunov/bp/blob/main/docs/P4-Prototype/PrototypeImplementationAIUsage.md PrototypeImplementationAIUsage.md]
     257   PrototypeImplementationAIUsage
    72258   – service used, the bugs found and fixed, the test evidence, and what I decided myself.
    73259 * '''Full conversation transcript:'''
    74    [https://github.com/StefanTrsunov/bp/blob/main/docs/P1-ConceptualModel/ERModelAIUsage.md ERModelAIUsage.md]
     260   ERModelAIUsage
    75261   – the same conversation produced the P1–P4 artefacts, so the complete prompt/response log is
    76    kept in one place. Direct links:
    77    [https://github.com/StefanTrsunov/bp/blob/main/docs/P1-ConceptualModel/ERModelAIUsage.md#session-1--2026-04-21 Session 1 – 2026-04-21],
    78    [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].
    79 
    80 '''Service:''' Claude Code (Anthropic), https://claude.com/claude-code – Claude subscription,
    81 model Claude Opus 4.7 (1M context).
     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.
     266
     267'''Service:''' Claude Code (Anthropic), `https://claude.com/claude-code` – Claude subscription,
     268model Claude Opus 4.7 (1M context) in sessions 1–2, Claude Sonnet 5 in session 3.
    82269
    83270'''In short:''' session 1 rewrote the existing Chi/HTTP backend as the CLI prototype covering
    … …  
    86273infinite loop at end of input, and an error check in the wrong order that misreported database
    87274failures as "Insufficient holding" – and replaced the read-modify-write holding update with a
    88 single `INSERT … ON CONFLICT DO UPDATE`.
     275single `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
     277could be granted the same units; see the "Session 3 — 2026-09-16" section of
     278PrototypeImplementationAIUsage.