Tesser is a modular, event-driven framework for building and executing quantitative trading strategies. Written entirely in Rust, it is designed for performance, reliability, and extensibility, allowing traders and developers to move from idea to backtesting and live deployment with confidence.
The core design principle is the separation of concerns, decoupling trading logic from the specifics of any particular exchange.
- Unified Exchange Interface: Connect to any exchange by implementing a common set of traits. Add a new exchange without touching the core logic.
- Event-Driven Architecture: A single, consistent event loop for both high-performance backtesting and live trading.
- Accurate Backtesting Engine: Simulate strategies against historical market data to evaluate performance before risking capital.
- Decoupled Components: Clear boundaries between data handling, strategy logic, execution, and portfolio management make the system robust and easy to maintain.
- Built for Performance: Leverages Rust's zero-cost abstractions to handle high-frequency data and execute orders with minimal latency.
Tesser is organized as a Cargo workspace. Understanding the role of each crate is crucial for contributing to the project. The primary goal is to keep the core logic (strategy, portfolio) completely independent of any specific broker or data source.
tesser/
βββ Cargo.toml
|
βββ tesser-core # Foundational data structures
βββ tesser-broker # The universal API for all exchanges (Traits)
|
βββ tesser-strategy # Your trading algorithms
βββ tesser-indicators # High-precision, composable technical indicators
βββ tesser-portfolio # Position, risk, and PnL management
|
βββ tesser-data # Consumes data via the broker trait
βββ tesser-execution # Sends orders via the broker trait
βββ tesser-events # In-process pub/sub event bus
|
βββ tesser-backtester # The simulation engine
βββ tesser-cli # The command-line user interface
|
βββ connectors/ # Directory for specific exchange implementations
βββ tesser-bybit # Concrete Bybit connector (REST + WebSocket)
βββ tesser-binance # Concrete Binance connector (REST + WebSocket)
βββ tesser-paper # A simulated exchange for backtesting/paper fills
Responsibility: Defines the universal, foundational data structures and enums used everywhere.
- Contents:
structs likeOrder,Trade,Candle,Position,Tick.enums likeSide(Buy/Sell),OrderType(Limit/Market),Interval(1m, 1h). - Rule: This crate should have minimal to no dependencies. It is the bedrock of the entire project.
Responsibility: The API Unification Layer. It defines the abstract interface for interacting with any exchange.
- Contents:
traits (interfaces) likeMarketStream(for subscribing to live data) andExecutionClient(for placing orders and managing accounts). It contains no concrete implementations. - Rule: If you are defining a behavior that an exchange must provide (e.g., "fetch open orders"), the trait for it belongs here.
Responsibility: Encapsulates every framework-level event type (ticks, candles, order-book deltas, signals, fills, order updates) plus the publish/subscribe bus used across the CLI, backtester, and future services.
- Contents: A
tokio::broadcast-poweredEventBusand typed wrappers (TickEvent,SignalEvent, etc.) that decouple producers from consumers. - Rule: Emit and consume runtime activity via the bus instead of calling modules directly; this keeps the event flow extensible (risk daemons, telemetry sinks, notification services) without rewriting the core loop.
Responsibility: Contains the "brains" of the trading systemβyour algorithms.
- Contents: Implementations of a
Strategytrait. This is where you calculate indicators (e.g., RSI, Moving Averages) and generateSignalevents (e.g., "go long", "exit position"). - Rule: Strategy code must be pure and self-contained. It should only depend on
tesser-corefor data types and operate on the data it is given, without any knowledge of where the data comes from (live feed or backtest).
Responsibility: AI inference engine (ONNX Runtime wrapper) used by strategies.
- Contents:
CortexEngine,FeatureBuffer, and configuration types that manage zero-copy dataflow into ONNX Runtime. - Highlights: Hardware-agnostic design supporting AVX-512 CPU mode for microsecond latency and CUDA/TensorRT toggles via configuration.
Responsibility: Houses all reusable technical indicators and signal building blocks.
- Contents: Decimal-native implementations of SMA, EMA, RSI, Bollinger Bands, and the plumbing required to compose them through zero-cost
pipe()adapters. - Rule: Indicators must update in O(1) time, avoid
f64drift by defaulting torust_decimal::Decimal, and never depend on higher-level crates.
Responsibility: Manages the state of your trading portfolio. It is the system's "accountant."
- Contents: Logic for tracking positions, calculating Profit and Loss (PnL), managing margin, and evaluating risk metrics. It updates its state by listening to
Fillevents (i.e., when an order is executed). - Rule: Portfolio logic should not be aware of any specific exchange. It works with the abstract
PositionandFilltypes fromtesser-core.
Responsibility: Manages the flow of market data from a source to the strategies.
- Contents: The logic to handle incoming data streams (from a
MarketStreamimplementation) and historical data feeds. It is responsible for tasks like building candles from ticks or replaying historical data files. - Rule: This crate uses a generic
MarketStreamtrait object. It does not know which exchange it is getting data from.
Responsibility: Translates trading signals from strategies into actionable orders.
- Contents: The Order Management System (OMS). It receives a
Signaland decides how to act on it (e.g., calculate order size, set price). It then sends the order to the broker. - Rule: This crate uses a generic
ExecutionClienttrait object. It does not know which exchange it is sending orders to.
Responsibility: This is where all the specific, concrete exchange logic lives.
- Contents: One crate per exchange. Today that includes
tesser-bybit(production-grade REST + WebSocket client based on the official v5 docs) andtesser-paper(deterministic connector used by the backtester and paper trading engine). - Rule: All code that is specific to one exchange (e.g., endpoint URLs, authentication methods, JSON payload formats) must be confined to a crate within this directory.
- Implements the Bybit v5 and Binance USD-M REST/WebSocket APIs (
ExecutionClient+MarketStream) that power live trading. Select the driver viaconfig/[env].tomlexchange profiles (driver = "bybit"or"binance").
- Simulates fills instantly and replays deterministic ticks/candles. Used by the backtester and as the default execution target for
live rununtil you're ready to wire real capital.
Responsibility: An offline engine that simulates a strategy's performance against historical data.
- Contents: An event loop that reads historical data, feeds it to the
tesser-datamodule, and uses thetesser-paperconnector to simulate order fills. It generates performance reports (Sharpe ratio, max drawdown, etc.). - Rule: The backtester's job is to wire the other components together in a simulated environment.
Responsibility: The user-facing application.
- Contents: The
main.rsfile. It parses command-line arguments and configuration files to decide what to do (e.g., run a backtest, start a live trading session). - Rule: This is the application's entry point. It is responsible for initializing and connecting all the necessary components for a given task.
- Rust toolchain (latest stable version recommended): https://rustup.rs/
-
Clone the repository:
git clone https://github.com/tesserspace/tesser.git cd tesser -
Build the entire project workspace:
cargo build --release
The repository now ships with a minimal end-to-end bundle under examples/:
examples/data/btcusdt_1m_sample.parquetβ canonical 6h BTCUSDT 1m candles produced viatesser-cli data normalize.examples/data/btcusdt_1m_sample.csvβ the raw CSV source that can be re-normalized if needed.examples/strategies/sma_cross.tomlβ double moving-average crossover tuned for the sample data.examples/strategies/rsi_reversion.tomlβ RSI mean-reversion variant for the same symbol.
Use them to exercise the entire pipeline without downloading external data:
cargo run -p tesser-cli -- \
backtest run \
--strategy-config examples/strategies/sma_cross.toml \
--data examples/data/btcusdt_1m_sample.parquet \
--quantity 0.01Swap in examples/strategies/rsi_reversion.toml (or any file under research/) to compare behaviors. Omit --data to fall back to synthetic candles, or pass multiple canonical Parquet paths to stitch larger datasets. Normalize new downloads first:
cargo run -p tesser-cli -- data normalize \
--source ./raw/binance/*.csv \
--output ./data_lake/candles \
--config ./configs/etl/sample_iso_csv.toml \
--symbol binance_perp:BTCUSDTPoint --data at the resulting .parquet files (globbed or enumerated) to keep the backtester consistent and fast.
The CLI now understands both candle- and tick-driven simulations. Pass --mode tick to tesser-cli backtest run alongside one or more Level 2 / trade JSONL files:
cargo run -p tesser-cli -- \
backtest run \
--strategy-config research/strategies/orderbook_scalper.toml \
--mode tick \
--lob-data data/btcusdt/orderbook.jsonl data/btcusdt/trades.jsonlIn tick mode the backtester replays historical depth snapshots through the high-fidelity MatchingEngine, honoring your strategy's limit prices, conditional orders, and latency requirements. This path is ideal for testing microstructure-sensitive strategies and validating slippage assumptions.
Execution hints now support specialized algorithms (configured through your strategies):
ExecutionHint::PeggedBestβ refreshes passive orders at the top of book using native amend/replace so queue position is preserved; tuneclip_size,refresh_secs, and the optionalmin_chase_distanceper strategy to control how aggressively it chases.ExecutionHint::Sniperβ waits for a target price before sweeping liquidity (used by theVolatilitySkewplaybook).ExecutionHint::TrailingStopβ arms above an activation price and issues a market exit once price retraces by the configured callback percentage, implementing an exchange-native trailing stop without relying on venue-specific order types.- Existing hints (
Twap,Vwap,IcebergSimulated) continue to work unchanged, and their state is persisted via SQLite so in-flight schedules recover from process restarts.
Additional indicators (ATR, MACD, Ichimoku Cloud) and reference strategies (OrderBookScalper, CrossExchangeArb, VolatilitySkew) ship with the workspace to showcase how these hints and the matching engine interact end to end.
tesser-cli is the single entry point for local research and operations:
tesser-cli --env default <COMMAND>
Commands:
data download|validate|resample # Download/inspect historical data
backtest run --strategy-config # Executes a single backtest (pass canonical Parquet via --data)
backtest batch --config ... # Runs multiple configs and writes an optional summary CSV
live run --strategy-config # Runs the live exchange stream (Bybit/Binance) + paper execution loop
state inspect [--path <file>] # Prints the persisted SQLite state snapshot (use --raw for JSON)
strategies # Lists compiled strategies
Configuration files live in config/. The loader merges the following sources (lowest β highest priority):
config/default.tomlconfig/{env}.toml(selected by--env)config/local.toml(gitignored)- Environment variables prefixed with
TESSER_(e.g.TESSER_exchange__bybit_testnet__api_key)
tesser-cli live run now drives the production event loop:
cargo run -p tesser-cli -- \
live run \
--strategy-config research/strategies/sma_cross.toml \
--quantity 0.02 \
--interval 1mAdd --exec live (and populate your [exchange.<name>] api_key/api_secret) to forward orders to the real exchange REST API instead of the paper engine. This will route live capital; use the exchange testnet profile while validating your setup.
What happens under the hood:
- Market data:
tesser-bybitmaintains a resilient WebSocket connection to the publiclinearstream (kline.<interval>.<symbol>andpublicTrade.<symbol>topics). The connection automatically heartbeats every 20s and reconnects on transient errors. - Execution & Reconciliation: Signals are routed through
tesser-executioninto the selected backend.--exec paper: Keeps the previous behavior (instant synthetic fills).--exec live: Submits real REST orders. A separate private WebSocket connection listens for real-time order status updates (Accepted,Filled,Canceled) and execution reports (Fillevents).- When a real
Fillis received, it's applied to the portfolio, updating your cash, positions, and realized PnL. This creates a closed-loop system where your local state reflects the exchange's reality.
- State persistence: Portfolio equity, open orders and last prices are serialized via
config.live.persistence(path defaults to./reports/live_state.db, enginesqliteorlmdb). Restart the process or runtesser-cli state inspectto review the snapshot. - State Reconciliation: On startup and periodically, the system fetches your open positions and balances via the REST API and compares them to its local state. Discrepancies are logged as warnings, providing a crucial safety net against state drift.
- Structured logging: When running
live, a JSON file is written toconfig.live.log_path(default./logs/live.json). Point Promtail/Loki/Grafana at that file to build dashboards without touching stdout logs. - Metrics: A Prometheus endpoint is exposed at
config.live.metrics_addr(default127.0.0.1:9100). Scrape/metricsto monitor tick/candle throughput, portfolio equity, order errors, and data-gap gauges. - Alerting: The
[live.alerting]section lets you enforce guardrails (max data gap, consecutive order failures, drawdown limit). Provide awebhook_url(Slack, Telegram, Alertmanager, etc.) or leave it empty for log-only alerts.
The state store referenced by config.live.persistence.path is the source of truth for portfolio and order recovery. With engine = "sqlite" it's a single file; with engine = "lmdb" it's a directory containing data.mdb/lock.mdb. Treat it like any other operational database:
- Ad-hoc inspection:
tesser-cli state inspectprints a human-readable summary, while--rawdumps the JSON payload you can edit or version-control. - Offline backup: stop the live process and copy the store (for SQLite:
cp reports/live_state.db reports/live_state.db.bak; for LMDB: copy the directory). - Online backup: when a session must stay up, use SQLite's native snapshotting:
sqlite3 reports/live_state.db ".backup 'reports/live_state.db.bak'". For LMDB, usemdb_copyor take a filesystem snapshot. - Restoring: replace the file with a known-good backup and restart
tesser-cli live run; the runtime will hydrate the portfolio from the restored snapshot.
β οΈ Risk warning:--exec liveforwards orders exactly as produced by your strategyβthere is no extra confirmation prompt, and portfolio PnL stays paper-based until a future release. Always dry-run on the exchange testnet before pointing to mainnet keys.β οΈ Risk warning:--exec liveis a real trading mode. With the new reconciliation features, the portfolio PnL is now based on real fills, but risks inherent to automated trading remain. Always dry-run on the exchange testnet before pointing to mainnet keys.
Key CLI flags:
| Flag | Description | Default |
|---|---|---|
--exchange |
Exchange profile defined under [exchange.*] |
paper_sandbox |
--category |
Bybit channel (linear, inverse, spot, β¦). Ignored for Binance. |
linear |
--interval |
Candle interval understood by tesser_core::Interval |
1m |
--quantity |
Fixed order size routed through FixedOrderSizer |
1.0 |
--exec |
Execution backend (paper or live) |
paper |
--slippage-bps / --fee-bps |
Synthetic execution frictions in basis points | 0 |
--latency-ms |
Delay between signal and fill simulation | 0 |
--state-path, --metrics-addr, --log-path |
Override the [live] config block |
see config |
--webhook-url |
Per-run override for [live.alerting].webhook_url |
empty |
--initial-equity |
Override the [backtest.initial_balances] entry for the reporting currency |
see config |
--risk-max-order-qty, --risk-max-order-notional, --risk-max-position-qty, --risk-max-drawdown |
Override [risk_management] guardrails |
see config |
--alert-max-data-gap-secs, --alert-max-order-failures, --alert-max-drawdown |
Override [live.alerting] thresholds |
see config |
Inspect all options with cargo run -p tesser-cli -- live run --help.
Sample snippet from config/default.toml:
[live]
metrics_addr = "127.0.0.1:9100"
log_path = "./logs/live.json"
[live.persistence]
engine = "sqlite"
path = "./reports/live_state.db"
[live.alerting]
webhook_url = "" # Optional HTTP endpoint
max_data_gap_secs = 300 # Alert if no ticks/candles are seen for 5 minutes
max_order_failures = 3 # Trigger after N consecutive execution errors
max_drawdown = 0.03 # 3% peak-to-trough drawdown guardrail
[risk_management]
max_order_quantity = 1.0 # Fat-finger guard per order (base asset qty)
max_order_notional = 0.0 # Optional notional clamp (quote currency); remove/zero to disable
max_position_quantity = 2.0 # Absolute cap on aggregate exposure per symbol
max_drawdown = 0.05 # Liquidate-only kill switch threshold (fractional)Every CLI flag (e.g., --state-path, --persistence, --metrics-addr, --log-path, --initial-equity, --risk-max-*, --alert-max-*) overrides the config file so you can spin up multiple isolated sessions with tailored risk and telemetry controls.
When tesser-cli live run executes, each order is filtered through the pre-trade risk layer: quantities above max_order_quantity are rejected, estimated notional above max_order_notional is blocked, projected exposure cannot exceed max_position_quantity, and once equity suffers a drawdown beyond max_drawdown the portfolio flips into liquidate-only mode (only allowing exposure-reducing orders) until the process is restarted.
Instead of embedding a heavy multi-strategy scheduler inside the binary, the recommended path is to run one tesser-cli live run process per strategy. Each process receives its own SQLite state file, metrics port, log file, risk guardrails, and alert thresholds via the CLI overrides above. This makes it trivial to mix and match paper/live runs or roll strategies independently via Docker Compose, Nomad, or systemd.
Compose example:
version: "3.9"
services:
momentum:
image: ghcr.io/tesserspace/tesser:latest
restart: unless-stopped
volumes:
- ./config:/app/config:ro
- ./reports:/app/reports
- ./strategies:/app/strategies:ro
- ./logs:/app/logs
command: >
tesser-cli --env prod live run
--strategy-config /app/strategies/sma_cross.toml
--quantity 0.5
--state-path /app/reports/momentum_state.db
--metrics-addr 0.0.0.0:9200
--log-path /app/logs/momentum.json
--initial-equity 20000
--risk-max-order-qty 0.5
--risk-max-position-qty 1.0
--alert-max-data-gap-secs 120
--webhook-url https://hooks.slack.com/services/XXX/YYY/ZZZ
meanrev:
image: ghcr.io/tesserspace/tesser:latest
restart: unless-stopped
volumes:
- ./config:/app/config:ro
- ./reports:/app/reports
- ./strategies:/app/strategies:ro
- ./logs:/app/logs
command: >
tesser-cli --env prod live run
--strategy-config /app/strategies/rsi_reversion.toml
--quantity 0.2
--state-path /app/reports/meanrev_state.db
--metrics-addr 0.0.0.0:9201
--log-path /app/logs/meanrev.json
--initial-equity 15000
--risk-max-order-qty 0.25
--risk-max-position-qty 0.5
--alert-max-drawdown 0.02Bring the stack up with docker compose up -d and point Prometheus at the exposed ports (9200, 9201, β¦). Because state lives in independent SQLite files, restarting or upgrading one strategy does not impact the others; you can introspect each database with tesser-cli state inspect --path reports/momentum_state.db before and after rollouts. Systemd and other supervisors work the same wayβsimply copy the command, change the overrides, and keep the services isolated.
Rust handles live execution; Python (powered by uv) owns fast research loops. The research/ directory provides:
research/
βββ notebooks/ # Exploratory analysis
βββ scripts/ # Batch jobs (e.g., parameter sweeps)
βββ strategies/ # Outputs consumed by Rust (TOML, ONNX, etc.)
βββ pyproject.toml # Locked dependencies for uv
Quick start:
cd research
uv venv
source .venv/bin/activate
uv pip install -e .
uv run python scripts/find_optimal_sma.py --data ../data/btc.parquet
uv run python scripts/optimize_rsi.py --data ../data/btc.parquet --output strategies/rsi_from_python.toml
uv run python scripts/train_ml_classifier.py --data ../data/btc.parquet --output models/ml_linear.tomlThe generated TOML files feed directly into tesser-cli backtest run --strategy-config ....
The upgraded tesser-strategy crate bundles a diverse suite for pressure-testing the stack:
| Name | Type | Highlights |
|---|---|---|
SmaCross |
Trend following | Dual moving-average crossover |
RsiReversion |
Mean reversion | RSI thresholds with configurable lookbacks |
BollingerBreakout |
Volatility/Band breakout | Uses standard deviation bands for entries |
MlClassifier |
Machine learning | Loads an external model artifact for real-time inference |
PairsTradingArbitrage |
Statistical arbitrage | Operates on two correlated symbols (configure as exchange:SYMBOL, e.g., binance_perp:BTCUSDT) |
OrderBookImbalance |
Microstructure | Consumes order-book snapshots to trade short-term imbalances |
Each strategy exposes a typed configuration schema and registers the symbols (one or many) it operates on. Sample configs live in research/strategies/ and the ML artifact in research/models/, so you can run them directly with the CLI.
Note: Multi-symbol strategies (e.g.,
PairsTradingArbitrage) require fully-qualified identifiers in the formexchange:instrumentso the router can distinguish venues.
Contributions are highly welcome! To get started, please read our CODING_STYLE.md guide. A great way to contribute is by adding a new exchange implementation in the connectors/ directory.
This project is licensed under either of
- Apache License, Version 2.0, (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
Trading financial markets involves substantial risk. Tesser is a software framework and not financial advice. All trading decisions are your own. The authors and contributors are not responsible for any financial losses. Always test your strategies thoroughly in a simulated environment before deploying with real capital.
