C4 model - sbOogway/sbt2 GitHub Wiki

C4 model

The architecture drawn as C4 diagrams. Each level zooms into the one before:

Level Shows Here
1. System context sbt2 as one box, with the people and outside systems around it Context
2. Containers the parts that run or store data separately: processes, file stores Containers
3. Components the modules and parts inside one container Parent: data path, Parent: run path, Backtest process, Server
Dynamic the numbered steps of one flow sbt2 run
Deployment where everything runs Deployment

In C4, a container is anything that runs or holds data on its own: a process, a database, a folder of files. It has nothing to do with Docker. The design itself is written on Architecture and the module pages; these diagrams only draw it. When the two disagree, the text wins, and the diagram gets fixed.

1. System context

C4Context
  title System context: sbt2

  Person(researcher, "Researcher", "Writes strategies in their own package, runs backtests from Python or the CLI, reads reports")

  System(sbt2, "sbt2", "Backtesting package on nautilus_trader: data pipeline, splits, optimisation, test lock, isolated runs, stored results")

  System_Ext(source, "Data source", "e.g. Bybit public dumps and REST API: trades, L2, instrument specs, funding, mark price")
  System_Ext(bench, "External benchmark", "Optional series supplied as a file")
  System_Ext(gui, "sbt2 GUI", "Future native app in sbt2-gui/, built from the protocol without Python or Nautilus")

  Rel(researcher, sbt2, "Calls sbt2.backtest, runs CLI commands and notebook loaders")
  Rel(researcher, gui, "Uses")
  Rel(gui, sbt2, "Protocol messages, with the API token", "protobuf over WebSocket")
  Rel(sbt2, source, "Downloads raw data", "HTTPS")
  Rel(sbt2, bench, "Reads for reports")

  UpdateLayoutConfig($c4ShapeInRow="3", $c4BoundaryInRow="1")

2. Containers

The parent process does everything except run backtests. Each backtest runs in its own fresh child process. See Run.

C4Container
  title Containers: sbt2

  Person(researcher, "Researcher")
  System_Ext(source, "Data source", "e.g. Bybit")
  System_Ext(gui, "sbt2 GUI", "Native app")

  System_Boundary(sbt2, "sbt2") {
    Container(parent, "sbt2 parent process", "Python API or CLI", "download, ingest, spec loading, pre-flight, the test lock, batch orchestration, optimisation, reports")
    Container(child, "Backtest process", "Python + nautilus_trader v2", "One process per run, forked from a forkserver that preloads the run modules, memory capped by the container engine or a systemd scope. Runs run.execute with the user's strategy")
    Container(notebook, "Notebook", "Jupyter + pandas", "Research through the data loaders")
    Container(userpkg, "Strategy package", "the user's Python package", "Strategies on the sbt2 base, installed next to sbt2")
    Container(server, "sbt2-server", "Python, asyncio + websockets", "The GUI's server: token, Hello, routing to the areas' handlers, health; one process, one event loop")
    ContainerDb(config, "Config folder ($SBT2_CONFIG)", "TOML on local disk; required: --config or SBT2_CONFIG", "venues.toml: the venue profiles, read on every run")

    Boundary(disk, "Data root ($SBT2_DATA) on local disk", "required: --data or SBT2_DATA") {
      ContainerDb(settings, "Run settings", "settings.toml", "Memory budget, per-run cap, chunk sizes; optional")
      ContainerDb(gaps, "Known gaps", "known_gaps.toml", "Days each source confirmed it lacks; optional")
      ContainerDb(raw, "Raw files", "raw/", "Downloads with the source's own file names")
      ContainerDb(catalog, "Catalog", "nautilus ParquetDataCatalog", "Instruments, L2 deltas, trades, 1-minute candles, funding and mark prices; one file per instrument, type, day. Nautilus builds the declared bars from trades or candles at run time")
      ContainerDb(results, "Results store", "parquet + DuckDB view", "runs/{run_id} with a summary each; index = view over summaries")
    }
  }

  Rel(researcher, parent, "sbt2.backtest or sbt2 commands")
  Rel(researcher, notebook, "Explores ideas")
  Rel(parent, source, "Fetches", "HTTPS")
  Rel(parent, child, "Forks from its forkserver, caps over systemd D-Bus with --launcher systemd, then sends a resolved run spec", "JSON over a pipe")
  Rel(parent, config, "Reads")
  Rel(parent, settings, "Reads")
  Rel(parent, gaps, "Reads")
  Rel(parent, userpkg, "Reads strategy metadata")
  Rel(child, userpkg, "Imports the strategy")
  Rel(parent, raw, "Writes, then ingests")
  Rel(parent, catalog, "Only writer; reads coverage")
  Rel(parent, results, "Queries, deletes; the lock reads test runs")
  Rel(child, catalog, "Streams the run's window")
  Rel(child, results, "Writes its run folder and summary via the sink")
  Rel(notebook, catalog, "Reads via loaders")
  Rel(gui, server, "Requests; replies and pushes", "protobuf over WebSocket")

  UpdateLayoutConfig($c4ShapeInRow="3", $c4BoundaryInRow="1")

3a. Components: parent process, data path

download, ingest and the plug-ins. download and ingest never import each other. See Data, Data sources and Asset classes.

C4Component
  title Components: parent process, data path

  Person(researcher, "Researcher")
  System_Ext(source, "Data source", "e.g. Bybit")

  Container_Boundary(parent, "sbt2 parent process") {
    Component(cli, "cli", "Python", "Thin commands. No logic")
    Boundary(mplug, "plug-ins") {
      Component(assets, "assets/*", "asset profiles", "Keyed by nautilus AssetClass + InstrumentClass: calendar, carry, valuation price")
    }
    Boundary(mdata, "data", "module") {
      Component(sources, "sources/*", "source adapters", "URLs, raw paths, parsers, known gaps")
      Component(download, "download", "Python", "Network to raw files. Atomic, idempotent, retries")
      Component(ingest, "ingest", "Python", "Raw to catalog via nautilus's public writers. Streaming, day-atomic, day-named files")
      Component(catalogread, "catalog", "Python", "Reader: coverage, notebook loaders. Writer: used only by ingest")
    }
  }

  Boundary(disk, "Root on local disk") {
    ContainerDb(raw, "Raw files", "raw/")
    ContainerDb(catalog, "Catalog", "ParquetDataCatalog")
  }

  Rel(researcher, cli, "download, ingest, data status (optional: runs fetch missing data)")
  Rel(cli, download, "download")
  Rel(cli, ingest, "ingest")
  Rel(cli, catalogread, "data status")
  Rel(download, sources, "Where and how to fetch")
  Rel(download, source, "Fetches", "HTTPS")
  Rel(download, raw, "Writes")
  Rel(ingest, sources, "Parsers")
  Rel(ingest, assets, "Instrument type")
  Rel(ingest, raw, "Reads")
  Rel(ingest, catalog, "Only writer")
  Rel(catalogread, catalog, "Reads")

  UpdateLayoutConfig($c4ShapeInRow="3", $c4BoundaryInRow="2")

3b. Components: parent process, run path

Spec, the batch runner and results. See Spec, Run and Results.

C4Component
  title Components: parent process, run path

  Person(researcher, "Researcher")
  Container(child, "Backtest process", "Python + nautilus", "run.execute, see 3c")

  Container_Boundary(parent, "sbt2 parent process") {
    Component(cli, "cli", "Python", "Thin commands. No logic")
    Component(api, "api", "Python", "sbt2.backtest: the core both front-ends call")
    Component(optimise, "optimise", "Python", "Optimiser interface, grid search, objectives; tunes on train, ranks on validation")
    Boundary(mspec, "spec", "module") {
      Component(spec, "spec", "Python", "Parse, expand, resolve, hash. Splits, venue profiles")
    }
    Boundary(mrun, "run", "module") {
      Component(batch, "batch", "Python", "A process per run, forked from a forkserver, uncapped or in a systemd scope, parallelism = budget / per-run cap. Crashes on any failure")
      Component(preflight, "preflight", "Python", "Fetch missing data, coverage, instrument type, test lock")
    }
    Boundary(mresults, "results", "module") {
      Component(store, "store", "ResultStore interface", "Sinks, summaries, index view, queries, delete; the parquet store; the store registry")
      Component(report, "report", "Python", "Report object, nautilus tearsheets plus sbt2 panels")
      Component(comparisons, "comparisons", "functions", "Parts of a run, degradation, batch tables")
      Component(costs, "costs", "functions", "Cost waterfall, exposure, holding time, turnover")
      Component(pricing, "pricing", "Python", "Priced runs, valuation prices on the equity grid, benchmarks")
      Component(metrics, "metrics", "functions", "Equity curves and returns, headline metrics, full statistics")
    }
    Boundary(mdata, "data", "module") {
      Component(catalogread, "catalog", "Python", "Coverage, instrument types, valuation prices for benchmarks and exposure; download and ingest for missing days")
    }
  }

  Boundary(disk, "Root on local disk") {
    ContainerDb(results, "Results store", "parquet + DuckDB")
  }

  Rel(researcher, api, "sbt2.backtest")
  Rel(researcher, cli, "run, report, runs")
  Rel(cli, api, "run")
  Rel(cli, report, "report, runs")
  Rel(api, spec, "load")
  Rel(api, batch, "run")
  Rel(api, optimise, "optimise")
  Rel(api, report, "Report")
  Rel(optimise, batch, "Candidates")
  Rel(optimise, store, "Ranks by objective")
  Rel(batch, preflight, "Checks each run")
  Rel(preflight, catalogread, "Fetch, coverage, types")
  Rel(preflight, store, "Earlier test runs")
  Rel(batch, child, "Forks")
  Rel(batch, store, "Sink per run")
  Rel(store, metrics, "Headline metrics at finalize")
  Rel(store, results, "Reads, writes")
  Rel(report, store, "Loads runs")
  Rel(report, metrics, "Full metrics")
  Rel(report, costs, "Costs and exposure")
  Rel(report, pricing, "Benchmarks")
  Rel(cli, comparisons, "report parts, report batch")
  Rel(comparisons, store, "Summaries")
  Rel(costs, pricing, "Valuation prices for exposure")
  Rel(pricing, catalogread, "Valuation prices")

  UpdateLayoutConfig($c4ShapeInRow="3", $c4BoundaryInRow="2")

3c. Components: backtest process

One backtest in one process. The strategy is a nautilus Strategy on the thin sbt2 base; it submits its own orders through nautilus's RiskEngine, and the base's order gate enforces warm-up and the drawdown guard. Funding, equity snapshots and bars are nautilus's own. See Strategy and Run.

C4Component
  title Components: backtest process (one run)

  Container_Boundary(child, "Backtest process") {
    Component(execute, "run.execute", "function", "Runs the resolved BacktestRunConfig on a BacktestNode")
    Component(node, "BacktestNode", "nautilus", "Streams declared inputs in chunks, from part start minus warm-up to end")
    Component(sink, "Output sink", "interface from results", "Called once at the end: equity, carry, reports; finalize writes the summary")
    Component(venue, "Simulated venue", "nautilus built-ins", "Fee, fill, latency and margin models, liquidation, brackets, funding settlement at mark price")
    Component(riskengine, "RiskEngine", "nautilus", "Notional and rate limits per order")
    Component(portfolio, "Portfolio", "nautilus", "Mark-to-market valuation, equity snapshots")

    Component(strategy, "Strategy", "nautilus Strategy on the sbt2 base, from the user's package", "Bar handlers, sizing, orders and brackets; order gate drops orders before the part and after the drawdown guard trips")
  }

  ContainerDb(catalog, "Catalog", "ParquetDataCatalog")
  ContainerDb(runfolder, "Run folder", "runs/{run_id}")

  Rel(execute, node, "Configures, runs")
  Rel(node, catalog, "Streams")
  Rel(node, strategy, "Bars, fills, rejects")
  Rel(strategy, riskengine, "Orders, brackets")
  Rel(riskengine, venue, "Allowed orders")
  Rel(venue, portfolio, "Fills, funding adjustments, account state")
  Rel(execute, portfolio, "Equity snapshots, after the run")
  Rel(execute, sink, "Equity, carry ledger, reports; finalize")
  Rel(sink, runfolder, "Writes parquet")

  UpdateLayoutConfig($c4ShapeInRow="2", $c4BoundaryInRow="1")

3d. Components: server

The server's connections and its areas. Each area plugs in as routes: results and config. See Server on Architecture.

C4Component
  title Components: server

  System_Ext(gui, "sbt2 GUI", "Native app")

  Container_Boundary(server, "sbt2-server") {
    Component(cli, "cli", "typer", "sbt2-server: data root, config folder, host, port, the token from SBT2_SERVER_TOKEN; puts the areas together")
    Component(transport, "transport", "Transport, Channel, Endpoint; websockets", "Binary frames on ws://, the sbt2.v1 subprotocol")
    Component(endpoint, "server", "Server, Settings", "GET /health, the bearer token before the upgrade; one connection per channel")
    Component(auth, "auth", "BearerToken", "Constant-time token check")
    Component(connection, "connection", "Python", "Hello gate, a task per request, bounded outgoing queue, 1013 on overflow")
    Component(router, "routing", "Router", "Body name to handler; request_id, Error codes")
    Component(handlers, "handlers", "async functions", "Hello to Welcome; each area's routes; blocking work through offloaded")
    Component(results, "results", "routes(root)", "Run lists, summaries, metrics, Arrow series and cached tearsheets, in chunks of at most 1 MiB")
    Component(config, "config", "routes(config, root)", "Venue profiles, known gaps and venue model kinds; writes validated by sbt2-core, atomic, one at a time per file")
  }

  Rel(gui, transport, "ws://; TLS, if any, is the operator's")
  Rel(cli, endpoint, "Serves")
  Rel(transport, endpoint, "HTTP requests, upgraded channels")
  Rel(endpoint, auth, "Authorization header")
  Rel(endpoint, connection, "Each channel")
  Rel(connection, router, "Each request")
  Rel(router, handlers, "Calls")
  Rel(handlers, connection, "Pushes through the outbox")
  Rel(cli, results, "Data root")
  Rel(router, results, "Results requests")
  Rel(results, connection, "Sends chunks through the outbox")
  Rel(cli, config, "Config folder, data root")
  Rel(router, config, "Config requests")

  UpdateLayoutConfig($c4ShapeInRow="3", $c4BoundaryInRow="1")

4. Dynamic: sbt2 run

What happens for sbt2 run spec.toml, in order. sbt2.backtest takes the same path from api on.

C4Dynamic
  title Dynamic: sbt2 run spec.toml

  Person(researcher, "Researcher")
  Component(cli, "cli", "Python")
  Component(spec, "spec", "Python")
  Component(batch, "run.batch", "Python")
  ContainerDb(catalog, "Catalog", "ParquetDataCatalog")
  Component(preflight, "run.preflight", "Python")
  Container(child, "Backtest process", "one per run")
  Component(store, "results.store", "ResultStore")
  ContainerDb(results, "Results store", "parquet + DuckDB")

  Rel(researcher, cli, "sbt2 run spec.toml")
  Rel(cli, spec, "Parse, expand, resolve, hash (via api)")
  Rel(cli, batch, "Resolved run specs (via api)")
  Rel(batch, preflight, "Check each run")
  Rel(preflight, catalog, "Fetch missing days, coverage, types; test lock")
  Rel(batch, child, "Fork, at most budget / per-run cap at once")
  Rel(child, catalog, "Stream the run's window")
  Rel(child, results, "Write the run folder through the sink; summary last")
  Rel(batch, store, "Sink per run; crash on any failure")
  UpdateRelStyle(preflight, catalog, $offsetX="-60", $offsetY="-40")
  UpdateRelStyle(batch, child, $offsetX="-20", $offsetY="130")
  UpdateRelStyle(cli, spec, $offsetX="-150", $offsetY="0")
  UpdateRelStyle(batch, store, $offsetX="10", $offsetY="120")
  UpdateLayoutConfig($c4ShapeInRow="4", $c4BoundaryInRow="1")

5. Deployment

Everything runs on one local machine; the server runs in a container built from sbt2-backend/Containerfile, started by sbt2-backend/compose.yaml (see Deployment on Architecture). See Quality for CI.

C4Deployment
  title Deployment: sbt2

  Deployment_Node(ws, "Local machine", "Linux") {
    Deployment_Node(session, "User session", "systemd --user") {
      Container(parent, "sbt2 parent process", "Python", "Python API or CLI, orchestration")
      Container(notebook, "Notebook", "Jupyter")
      Deployment_Node(scope, "Transient scope, one per run, with --launcher systemd", "systemd --user transient scope via D-Bus StartTransientUnit, cgroup MemoryMax, MemorySwapMax=0") {
        Container(child, "Backtest process", "Python + nautilus_trader 2.0.0rc6.dev20260930")
      }
    }
    Deployment_Node(disk, "Local disk", "data root: $SBT2_DATA or --data") {
      ContainerDb(raw, "Raw files", "raw/")
      ContainerDb(catalog, "Catalog", "catalog/")
      ContainerDb(results, "Results store", "results/")
    }
    Deployment_Node(configdisk, "Local disk", "config folder: $SBT2_CONFIG or --config") {
      ContainerDb(venues, "Venue profiles", "venues.toml")
    }
    Deployment_Node(serverctr, "Server container", "sbt2-backend/compose.yaml; image from sbt2-backend/Containerfile, user sbt2, memory limited by the engine") {
      Container(server, "sbt2-server", "Python 3.14", "Plain ws:// and GET /health on port 8765; runs its runs inside the container")
      ContainerDb(datavol, "Data volume", "/data = $SBT2_DATA")
      ContainerDb(configvol, "Config volume", "/config = $SBT2_CONFIG")
    }
    Deployment_Node(podman, "Rootless podman compose", "/opt/woodpecker") {
      Container(wpserver, "Woodpecker server", "v3")
    }
  }

  Deployment_Node(cihost, "CI host", "LAN machine, Docker compose") {
    Container(wpagent, "Woodpecker agent", "v3, one workflow at a time")
    Container(dind, "Docker-in-Docker", "Runs the pipeline steps; 4 GB memory cap, no swap")
  }

  Deployment_Node(bybit, "Bybit", "first data source") {
    Container(dumps, "Public dumps and REST API", "HTTPS")
  }

  Deployment_Node(github, "GitHub", "github.com") {
    Container(repo, "sbOogway/sbt2", "git", "The backend, the protocol, the reserved GUI and this wiki")
  }

  Deployment_Node(pypi, "PyPI", "pypi.org") {
    Container(dist, "sbt2", "wheel", "Published from tags by the release pipeline")
  }

  Rel(parent, child, "Forks")
  Rel(parent, dumps, "Downloads")
  Rel(parent, raw, "Writes")
  Rel(child, catalog, "Streams")
  Rel(child, results, "Run folder")
  Rel(server, datavol, "Reads results, edits known gaps")
  Rel(server, configvol, "Reads and writes config")
  Rel(repo, wpserver, "Webhooks")
  Rel(wpagent, wpserver, "Takes pipelines", "gRPC")
  Rel(wpagent, dind, "Starts steps")
  Rel(wpagent, repo, "Clones, reports status")
  Rel(wpagent, dist, "Publishes releases")

  UpdateLayoutConfig($c4ShapeInRow="3", $c4BoundaryInRow="2")