Language: Simplified Chinese
quick_match is a Go trading matching system designed for continuous auction style assets and account-balance
settlement flows. The project is split into three runtime processes: order, match, and settle. The order process
provides the unified order entry point, match processes are sharded by symbol, and settle processes asynchronously
consume match results.
The current implementation can be used as a prototype for continuous auction matching, matching engine learning, or internal matching services. It covers a single-symbol in-memory order book, price-time priority matching, limit and market orders, common time-in-force behavior, order fund freezing, deal fund mutations, fee calculation, Kafka outbox delivery, and idempotent settlement.
It should not be described as a complete securities or stock market system. The code does not yet implement market calendars, complete open and close trading phases, formal auction matching, price limit rule sources, securities positions and sellable quantity checks, T+1/T+2 settlement, corporate actions, regulatory reporting, broker or exchange seats, or clearing member semantics. Securities or stock use cases should add those market rules, risk controls, clearing modules, and compliance modules around the existing matching core.
- Unified order entry:
quick_match orderexposes submit, cancel, and amend APIs through gRPC. - Asynchronous order stream: validated orders are written to Kafka first, then persisted through an asynchronous path.
- Single-symbol matching: each
quick_match match --symbolprocess maintains one in-memory order book. - Price-time priority: higher buy prices are prioritized, lower sell prices are prioritized, and orders at the same price currently use ascending order ID for deterministic ordering.
- Pure in-memory matching core:
internal/matchcoredoes not depend on databases, networking, logging, or wall-clock time. - Reliable match delivery: match results are written to an outbox and published to Kafka, with retry support for publish failures.
- Asynchronous fund settlement:
quick_match settle --symbolconsumes match results and applies idempotent fund mutations by deal ID. - Integer fixed-point values: prices, quantities, and deal amount calculations do not use floating point numbers; deal amounts use large integer boundaries.
- Runtime robustness: command entry points include panic recovery, graceful shutdown, stable log fields, and external dependency failure handling.
- Containerized deployment: Dockerfile, Compose, and
deploy.shsupport deploying multiple symbol-specific match and settle processes.
Runtime constraints:
orderis the unified entry point for all symbols and is not sharded by symbol.- Each
matchprocess handles exactly one symbol. The same symbol should not have multiple active match processes consuming the order stream at the same time. - Each
settleprocess handles exactly one symbol. Fund mutations are idempotent throughprocessed_settlements.deal_id. - PostgreSQL is the source of truth for final order, deal, and fund state.
- Kafka messages may be duplicated but must not be silently lost. Consumers should commit offsets only after business processing succeeds.
- Redis is used only for rate limiting, caching, and rebuildable auxiliary state. It is not a source of truth for accounting.
| Path | Description |
|---|---|
api/ |
Protobuf definitions and generated Go gRPC bindings. API changes should start from .proto files. |
cmd/ |
Cobra commands and runtime dependency wiring. |
config/ |
Viper-based configuration loading. |
internal/matchcore/ |
Pure in-memory matching engine and order book logic. |
internal/order/ |
Core order validation, normalization, and submission logic. |
internal/matchservice/ |
Kafka order consumption, matching processing, outbox, and publish flow. |
internal/settle/ |
Pure settlement plan generation. |
internal/settlepersist/ |
Fund mutation and idempotent settlement persistence. |
internal/kafkamq/, internal/messaging/ |
Kafka adapter and message contracts. |
model/ |
GORM persistence models and migration entry point. |
deploy/ |
Local load test and deployment helper files. |
- Go
1.24or a compatible toolchain. - Docker and Docker Compose for local dependencies and container deployment.
- PostgreSQL, Kafka, and Redis.
deploy.shstarts local dependencies through Compose.
The repository provides two example files:
- config/config.example.yaml: sample CLI config used by
quick_match --config. It currently reads only the globallog_level. - .env.example: sample environment variables used by Docker Compose and
deploy.sh.
For local deployment, copy .env.example to .env and adjust it as needed:
cp .env.example .envRuntime parameters such as DB, Kafka, Redis, symbol, and ports are currently passed through command flags or Compose
environment variables. They are not read from config.yaml.
Inspect commands:
go run . --help
go run . order --help
go run . match --help
go run . settle --help
go run . migrate --helpRun tests:
go test ./...
go test ./internal/matchcore -bench=.
go test ./internal/order -bench=.
go test ./internal/settle ./internal/settlepersist -bench=.For a database that already has symbol configuration, deploy.sh reads symbols from the symbols table:
./deploy.sh up
./deploy.sh ps
./deploy.sh logs
./deploy.sh downdeploy.sh generates .deploy/docker-compose.symbols.yaml and runs it together with docker_compose.yaml. The default
deployment includes:
postgresrediskafkamigrateorder- one
match_<symbol>and onesettle_<symbol>service for each symbol - optional
symbol_seed, generated only whenSEED_SYMBOLS=true
By default, deploy.sh starts PostgreSQL, Kafka, and Redis, runs migrate, queries the symbols table for symbols
with status = 'TRADING', and generates match and settle services from that list.
For bootstrapping an empty local database, explicitly set SYMBOLS and enable seeding. The seed inserts missing symbols
only and does not overwrite existing configuration:
SYMBOLS=ABC-USD,XYZ-USD SEED_SYMBOLS=true ./deploy.sh upquick_match migrate \
--db-driver postgres \
--db-dsn "host=postgres user=quick_match password=quick_match dbname=quick_match port=5432 sslmode=disable TimeZone=UTC"migrate runs model.AutoMigrate. In Compose, business services wait for this command to succeed before starting.
quick_match order \
--grpc-addr 0.0.0.0:50051 \
--kafka-brokers kafka:9092 \
--db-driver postgres \
--db-dsn "host=postgres user=quick_match password=quick_match dbname=quick_match port=5432 sslmode=disable TimeZone=UTC" \
--redis-addr redis:6379 \
--rate-limit-tps 0 \
--snowflake-node-id 1Common flags:
| Flag | Description |
|---|---|
--grpc-addr |
gRPC listen address. If empty, only the skeleton runtime runs. |
--kafka-brokers |
Comma-separated Kafka broker list. |
--db-driver |
postgres or sqlite. |
--db-dsn |
Database DSN. |
--redis-addr |
Redis address, required when rate limiting is enabled. |
--rate-limit-tps |
Global order TPS limit. 0 disables Redis rate limiting. |
--snowflake-node-id |
Order ID node ID. |
The gRPC API is defined in api/order/v1/order.proto and currently includes:
SubmitLimitOrderCancelOrderAmendLimitOrder
quick_match match \
--symbol ABC-USD \
--kafka-brokers kafka:9092 \
--db-driver postgres \
--db-dsn "host=postgres user=quick_match password=quick_match dbname=quick_match port=5432 sslmode=disable TimeZone=UTC" \
--snowflake-node-id 1000Notes:
--symbolis required.- One process maintains one symbol order book.
- For multi-symbol deployments, start multiple processes or containers.
--snowflake-node-idshould not conflict with other match processes that generate deal IDs.
quick_match settle \
--symbol ABC-USD \
--kafka-brokers kafka:9092 \
--db-driver postgres \
--db-dsn "host=postgres user=quick_match password=quick_match dbname=quick_match port=5432 sslmode=disable TimeZone=UTC"Notes:
--symbolis required.- Each settle process applies match results for the current symbol only.
- Duplicate consumption relies on DB idempotency records to prevent repeated debits or credits.
Build an image:
VERSION="$(date -u +%Y%m%d%H%M%S)"
docker build -t "quick_match:${VERSION}" .Deploy a specific version:
./deploy.sh --version "${VERSION}" up
./deploy.sh --version 20260716184630 --no-build up
VERSION=20260716184630 ./deploy.sh upRender deployment configuration:
./deploy.sh render
./deploy.sh config
SYMBOLS=ABC-USD,XYZ-USD,DEF-USD ./deploy.sh configrender and config also query the database by default. If no database is available, temporarily set SYMBOLS as
shown above for offline rendering.
Common environment variables:
| Variable | Default | Description |
|---|---|---|
SYMBOLS |
empty | Optional symbol override. If empty, symbols are queried from the symbols table. |
SYMBOL_STATUS_FILTER |
TRADING |
Comma-separated status filter used when querying symbols. all disables filtering. |
IMAGE_NAME |
quick_match |
Image name used when QUICK_MATCH_IMAGE is not set. |
VERSION |
local |
Image tag used when QUICK_MATCH_IMAGE is not set. CI defaults to a UTC timestamp. |
QUICK_MATCH_IMAGE |
empty | Exact business service image. Takes precedence over IMAGE_NAME:VERSION. |
BUILD_IMAGE |
true |
Whether up or restart should build the business image. Set to false or use --no-build when deploying an existing image tag. |
KAFKA_BROKERS |
kafka:9092 |
Kafka broker address inside containers. |
DB_DRIVER |
postgres |
Database driver. |
DB_DSN |
local Compose Postgres DSN | Database connection string. |
LOG_LEVEL |
info |
Log level. |
MATCH_NODE_ID_START |
1000 |
Starting node ID for multi-symbol match processes. |
SEED_SYMBOLS |
false |
Whether explicit SYMBOLS overrides should generate and run symbol seed SQL. |
SYMBOL_PRICE_SCALE |
2 |
Seed symbol price scale. |
SYMBOL_QTY_SCALE |
8 |
Seed symbol quantity scale. |
SYMBOL_TICK_SIZE |
1 |
Seed symbol tick size. |
SYMBOL_LOT_SIZE |
1 |
Seed symbol lot size. |
Supported time-in-force values:
GTC: default policy. Unfilled remaining quantity from a limit order enters the order book.IOC: immediately matches the fillable quantity and expires the remainder.FOK: must be fully filled, otherwise the whole order expires without modifying the order book.GTD: requiresexpires_at_millis; expired orders expire immediately.
Other rules:
post_only=trueis allowed only for limit orders. If it would match immediately, the order expires.- Symbol rules control price scale, quantity scale, tick size, lot size, quantity bounds, and notional bounds.
- Amounts, prices, and quantities use integer fixed-point values and do not use floating point numbers.
- Self-trade prevention supports cancel taker, cancel maker, and cancel both.
- A match result must enter a reliable publish or retry path before matching output is considered delivered.
- Kafka producers use all-replica acknowledgements and retry policies.
- Kafka consumers disable automatic commits and commit offsets only after business processing succeeds.
- Poison messages that cannot be processed are written to dead letter storage.
- Orders are idempotent by
order_id; match results and settlements are idempotent bydeal_id. - PostgreSQL failures must not acknowledge final business state.
- Redis failures may affect only caching or rate limiting, not final order, deal, or fund correctness.
The current core benchmarks meet the first-stage targets:
| Module | Target | Current core benchmark result |
|---|---|---|
| match core | 100000 TPS+ |
Main pure in-memory real-time paths exceed the target. |
| order core | 50000 TPS+ |
Pure in-memory order submission core path exceeds the target. |
| settle persist | 2000 TPS+ |
SQLite in-memory transactional fund mutation exceeds the target. |
These results do not represent real end-to-end throughput across Kafka, PostgreSQL, Redis, and gRPC. To re-run benchmarks:
go test ./internal/matchcore -bench=. -benchmem
go test ./internal/order -bench=. -benchmem
go test ./internal/settle ./internal/settlepersist -bench=. -benchmem- deploy/loadtest/README.md: local end-to-end load test environment.
This project is open sourced under the Apache License 2.0.
- Keep
internal/matchcorepure and deterministic. Do not introduce I/O, logging, persistence, or wall-clock time. - When changing price-time priority behavior, update same-price insertion, recovery ordering, crossing orders tests, and benchmarks.
- Treat Kafka, PostgreSQL, and Redis as dependencies that can fail. Service-layer changes must preserve idempotency, retries, and outbox semantics.
- API changes should start from
.protofiles. Do not manually edit generated*.pb.gofiles. - Do not add global mutable state unless it is part of runtime wiring and has a clear lifecycle.
