Skip to content

Repository files navigation

OpenOMS

openOMS

Build and Test

An open source multi-client order management system.

./assets/screenshot01.png ./assets/screenshot02.png


Setup

git clone git@github.com:maxkuttner/openoms.git && cd openoms
docker compose up -d  # a Postgres to run against; skip if you already have one
cargo run -- init     # prompts for your Postgres, generates oms.toml, creates the database
cargo run             # start the OMS on localhost:3001

The bundled docker-compose.yml runs Postgres 16 on 127.0.0.1:5432 with the superuser postgres / postgres, which is exactly what init assumes — so with it running you can press Enter through every prompt and type postgres for the password. It deliberately does not pre-create the ods database: init creates that itself and refuses if it already exists. Data lives in a named volume, so docker compose down keeps it and docker compose down -v throws it away.

Then, in a second terminal:

cd cockpit && npm install && npm run dev  # admin console on localhost:5173

init asks for your Postgres host, port, database name, superuser name and password — pressing Enter through every prompt targets a default local Postgres (localhost:5432, superuser postgres, database ods). A passed --host, --port, etc. seeds what Enter takes instead of being ignored, and a passed --password skips that prompt entirely. init then generates the oms role password, an encryption master key and a cockpit login password, writes them to oms.toml (mode 0600, and appends the filename to .gitignore if one already exists in the current directory — it does not create a .gitignore), and creates the role, database, schemas, migrations, grants and reference data. The cockpit password is written to oms.toml either way; interactively it is also printed once at the end, as the only mode where a human is there to read it.

Run it once. If oms.toml already exists, init refuses and tells you to use database migrate to upgrade, or to delete the file to start over — but deleting it throws away the master key, and with it anything it decrypts. If provisioning itself fails partway (after oms.toml was written), fix the cause and run oms database init --resume — it creates only whichever of the role/database is still missing (the common case is the role existing but CREATE DATABASE having failed) and finishes migrations, grants and seeding, all of which are safe to re-run.

The instrument catalog starts empty. Put broker credentials in .env and it fills itself on the next boot; see Loading instruments. Create portfolios, accounts and trading identities in the cockpit.

What each step does

Step What happens
init Prompts for the Postgres connection, generates the oms role password, the master key and a cockpit login password, writes oms.toml (mode 0600), then creates the role, database, schemas, migrations, grants and reference data. --non-interactive takes the connection from flags/env instead of prompting. The cockpit password is written to oms.toml in both modes; --non-interactive just doesn't also print it.
cargo run Starts the server. It never creates or migrates anything — if the database is missing or stale, it says so and names the command to run.

Prerequisites

  • Rust (stable) — curl https://sh.rustup.rs -sSf \| sh
  • cmake and a C++ compiler — the embedded FIX engine (quickfix) is C++
  • OpenSSL 3 on macOS — brew install openssl@3 (Linux uses the system one)
  • PostgreSQL — either Docker, for the bundled docker-compose.yml, or an existing server with a superuser you know the password of
  • Node — only if you want the cockpit web UI
# macOS, using the bundled Postgres
brew install cmake openssl@3 node

# macOS, bringing your own Postgres instead
brew install cmake openssl@3 node postgresql@16

Configuration

Everything lives in oms.toml, generated by oms init. Flags and environment variables override it, in that order — that is how Docker and CI inject settings without touching the file:

cargo run -- database init --host db.internal --username admin
POSTGRES_HOST=db.internal cargo run -- database init
Setting oms.toml Flag Environment
Host database.host --host POSTGRES_HOST
Port database.port --port POSTGRES_PORT
Superuser name database.username --username POSTGRES_USERNAME
Superuser password (never stored) --password POSTGRES_PASSWORD
Database database.database --database POSTGRES_DATABASE
oms role password oms.password --oms-password OMS_PASSWORD
Master key oms.master_key
Bind address server.bind_addr OMS_BIND_ADDR
Cockpit password server.admin_password OMS_ADMIN_PASSWORD

Anything left unset at every tier falls back to a built-in default — localhost, 5432, postgres/postgres, ods, and openoms-dev for both the oms role and cockpit passwords (loopback only; a still-default password is refused against anything else).

There is one application role, oms. It owns the database, both schemas and every table in them, and it is what the server connects as.

Back up oms.toml. The master key in it is the only thing that can decrypt stored credentials — losing the file loses them. The superuser password is deliberately not in it: oms init prompts for it once, to provision the database, and never writes it down. database init, database migrate and database drop each still take it the same way as before, via --password/POSTGRES_PASSWORD. database status no longer needs it — it authenticates as the oms role instead, falling back to the superuser whenever the oms role connection fails: the server has never been initialized, or an install from before this fallback existed has an oms role password this command has no way to reconstruct.

.env is an override file, not a prerequisite — copy .env.example when you need broker credentials, a real admin password, or a non-local database.

Roles and schemas

One role, oms, created by database init. It owns the database, both schemas and everything in them, and it is what the server connects as — so there is exactly one password to set. The superuser only creates and destroys it.

role  oms

  schema public   instrument, instrument_derivative, broker_instrument,
                  venue, currency, calendar, calendar_holiday
  schema oms      orders, portfolios, principals, accounts, api_keys, …

The split is for consumers, not permissions: public holds master data another service can point at, oms holds this application's operational tables.

Database commands

oms init runs database init for you on a fresh machine. These are the subcommands underneath it, and what you use directly afterwards — for CI, non-interactive provisioning, or to inspect and maintain a database you already have:

cargo run -- database init       # create everything; fails if it already exists
cargo run -- database init --resume   # finish an init that failed partway through
cargo run -- database migrate    # apply pending migrations (idempotent)
cargo run -- database status     # what exists, what is pending
cargo run -- database drop       # destroy the database (roles are kept)

init is deliberately strict. If the roles or database already exist it stops and tells you to run migrate instead, rather than silently skipping steps or resetting credentials on a database that already holds data. The one exception is --resume: if a previous init created the role and/or database but failed before finishing migrations, grants or seeding, oms database init --resume creates only whichever of the role/database is still missing — never touching the credentials of one that already exists — and re-runs the rest, every step of which is safe to re-run. Because it skips the wrong-server refusal, double-check --host/--database before passing it: pointed at the wrong database, resume's migrations (some of which are DROP …) would run there instead.

init, migrate and drop connect as the superuser (--password/ POSTGRES_PASSWORD, defaulting to postgres on loopback) because they create, alter or destroy the role and the database itself. status is read-only and connects as the oms role instead, so it needs no superuser credential in the normal case; it falls back to the superuser whenever that connection fails — most commonly because the server has never been initialized, but also for an install whose oms role password predates this fallback and isn't recorded anywhere status can read it.

Upgrading an existing install is git pull && cargo run -- database migrate.

Loading instruments

The instrument catalog comes from brokers, not from a bundled list. There are two ways in, and they run the same code.

Automatic. With broker credentials in .env, an empty catalog is populated in the background on boot. This is the normal path after database init — start the server, and instruments appear. The sync can take minutes for full option chains.

# OMS_SYNC_ON_BOOT=never       # opt out entirely
# OMS_SYNC_UNDERLYINGS=SPY,QQQ # only these option chains, instead of all

Explicit, when you want to re-sync or see what would change:

cargo run -- setup sync-broker --broker alpaca
cargo run -- setup sync-broker --broker alpaca --underlyings SPY,QQQ
cargo run -- setup sync-broker --broker alpaca --dry-run

Trading

Two ways in.

Cockpit (localhost:5173) — configuration, monitoring, minting tokens.

Python — for actually sending orders:

pip install -e clients/python
from oms_client import OMS

oms = OMS("http://localhost:3001", token=os.environ["OMS_TRADING_TOKEN"])

pf  = oms.portfolios()[0]
oid = oms.submit(portfolio=pf.portfolio_id,
                 symbol="SPY260918C00770000@OPRA",
                 side="buy", quantity=1)
print(oms.wait_for(oid).status)

for row in oms.orders(status="routed"):
    print(row.order_id, row.instrument_symbol, row.cum_qty)

There is a CLI too — oms orders list, oms positions, oms submit. See clients/python/README.md.

Auth

  • Cockpit login — one password, OMS_ADMIN_PASSWORD (enable with OMS_ADMIN_AUTH_ENABLED=true). Sent as a bearer to /admin. Use a strong random value for anything real.
  • Trading tokens — minted on the cockpit's Trading tokens page or via POST /admin/trading-tokens, shown once. A token belongs to a principal (a trader, strategy or service); what it may trade comes from that principal's portfolio grants (can_trade / can_view / can_allocate). Mint several tokens under one principal to rotate credentials without re-permissioning. Revoke anytime.
  • Trading tokens can reach only the trading routes. They can never touch /admin.

Troubleshooting

Symptom Cause
refusing to start: OMS_ADMIN_PASSWORD is not set OMS_BIND_ADDR is not loopback. Set a real admin password in .env.
password authentication failed for user "oms" OMS_PASSWORD doesn't match the role. Fix the value, or ALTER ROLE oms PASSWORD '…'.
role "oms" already exists on init Something is already provisioned. Use migrate, or drop first.
Server exits naming a migration Run cargo run -- database migrate.
Link error mentioning -lssl on macOS brew install openssl@3, or set OPENSSL_DIR.

About

No description, website, or topics provided.

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages