Compiling Trading Signals: The sigc Hypothesis Pipeline
How sigc compiles a typed trading-signal DSL to IR, caches results by content hash, and runs one binary from backtest to production daemon, limits included.
This article was rewritten in October 2026 to describe how sigc actually works; an earlier version described a visual specification, Rust code generation and formal verification passes that sigc does not have, along with timing figures that had not been measured.
The Quant Research Bottleneck
Quantitative finance operates on a simple loop: form a hypothesis about market behaviour, test it against historical data, and if it survives, deploy it to trade live capital. The hypothesis is typically expressed as a signal — a function that takes market data as input and produces a score, a ranking or a set of weights. Simple signals might be moving-average crossovers. Complex ones combine several factors, neutralise them against sectors, and feed a long-short portfolio.
The problem is not forming hypotheses. Experienced quants generate ideas constantly. The problem is everything between the idea and a result you can trust. In a typical shop the idea lives in a notebook that calls pandas, pulls some prices, computes a couple of rolling statistics and prints a Sharpe ratio. It works. It also rots: column names drift, calendars disagree, an off-by-one in a shift() leaks tomorrow’s return into today’s signal, and a rerun three weeks later produces a different number because a dependency moved.
Then, if the signal survives, someone rewrites it for production — often in a different language, against a different data path — and the production version is checked against the notebook, which may itself be wrong.
Every quant team that scales past a handful of researchers ends up building the same thing to escape this: a typed, reproducible signal language. sigc is that system, built in the open.
What sigc Is
sigc is a compiler and a runtime for a small domain-specific language. A strategy is a text file with four kinds of block, in order: data, params, one or more signal blocks, and a portfolio.
data:
prices: load parquet from "prices.parquet"
params:
lookback = 20
top_pct = 0.2
signal momentum:
returns = ret(prices, lookback)
score = zscore(returns)
emit winsor(score, p=0.01)
portfolio main:
weights = rank(momentum).long_short(top=top_pct, bottom=top_pct)
backtest from 2024-01-01 to 2024-12-31
sigc run momentum.sig compiles and backtests it, reporting total return, Sharpe ratio, maximum drawdown and turnover. The same binary started as sigc daemon serves compile and run requests in production. That single-binary design is the core of the argument: the thing you backtested and the thing you deploy are not two programs that someone has to keep in agreement.
The key observation behind a purpose-built language is that trading signals, despite their variety, live in a narrow computational domain. They consume aligned time series and cross-sections, they maintain rolling state, and they produce bounded outputs. A domain that narrow can be given a type system and an operator library that cover most of what researchers write, and the runtime ships more than 120 operators: returns, lags, z-scores, ranks, winsorisation, rolling means and standard deviations, EMA, RSI, MACD, ATR, VWAP, residualisation against a market factor, sector neutralisation, and portfolio constructors such as long_short and scale_vol.
The Pipeline
sigc’s pipeline has four stages: parse, type-check, execute, serve.
Parse and type-check. The compiler (sig_compiler) parses the .sig file into an AST and checks dtypes, shapes and indices before any row of data is read. Shape errors — combining series on mismatched calendars, feeding a cross-section where a time series was expected — fail at compile time rather than surfacing as a silently misaligned join in a backtest.
Lower to IR. The checked program is lowered to an intermediate representation: a graph of operator nodes. This is what the runtime executes and what the cache is keyed on.
Execute. The runtime (sig_runtime) executes the IR with Polars and Arrow columnar execution, Rayon parallelism, and SIMD kernels for factor arithmetic (AVX2 or AVX-512 where the CPU has them).
Cache by content. Every IR node is keyed by a blake3 hash of itself and its inputs, stored in a sled-backed cache (sig_cache). Identical IR on identical inputs produces identical bytes. This is the reproducibility guarantee, and it has a pleasant side effect: changing one parameter recomputes only the nodes downstream of that parameter.
Serve. In daemon mode, a long-running process owns the cache exclusively and answers compile, run and status requests over nng request-reply on a local port. There is no external broker. A client that finds the cache lock held retries with exponential backoff.
Why Rust
sigc is written in Rust, and Rust systems can embed it directly rather than shelling out:
use sigc::{Strategy, Backtest};
let strategy = Strategy::from_file("alpha.sig")?;
let results = strategy
.with_param("lookback", 60)
.with_param("threshold", 1.5)
.run()?;
println!("Sharpe: {:.2}", results.sharpe_ratio());
let weights: Vec<f64> = results.weights();
The choice follows from three properties.
Predictable performance. A runtime with no garbage collector has no collector pauses to explain away when a production rebalance runs late.
Correctness of the runtime itself. Rust’s ownership model rules out data races and use-after-free in safe code. A signal language promises that the computation you wrote is the computation that runs; that promise is only as good as the engine underneath it.
Ecosystem. Polars, Arrow and Rayon give a columnar, parallel execution core without writing one from scratch.
For analysts who want to stay in Jupyter, pysigc exposes the runtime to Python, so the notebook calls the same compiler and engine as production.
What the Compiler Does and Does Not Check
It is worth being precise here, because “compiled” is easily heard as “verified”.
Consider a z-score: the deviation of a value from its rolling mean, divided by its rolling standard deviation. It is one of the most common building blocks in quant research, and a numerical minefield. When the rolling window holds identical values the standard deviation is zero; when it is tiny, the z-score explodes and can push weights through any sensible limit.
sigc’s type checker will catch the z-score being computed over a misaligned series. It does not prove the denominator is non-zero, and it does not prove the output stays within bounds. What the language does is make the guard explicit and cheap: winsor(score, p=0.01) or a clip sits in the source, next to the computation it protects, where a reviewer can see it. The same goes for lookahead. sigc does not claim to prove a signal free of future information; lags are explicit operators in the source rather than an index shift buried in a dataframe call, which makes the most common leak far easier to spot in review.
The production layer adds a different kind of check. A sigc.yaml file declares circuit breakers — maximum drawdown, maximum position size, a kill switch — along with order rate limits, Prometheus metrics on /metrics, structured JSON audit logs and alerting. Those are runtime guardrails, not proofs, and they are the right tool for failures that only appear with live data.
Speed
The README reports, for a five-year daily backtest over 500 securities, roughly 2 ms to parse and compile, 45 ms for the full backtest and 3 ms for an incremental update, with warm cache hits returning in microseconds. Those figures come from the project’s own benchmarking; the hardware and methodology are not yet published alongside them, and we have not reproduced them independently, so treat them as an order of magnitude rather than a specification.
What matters more than the exact numbers is the shape of the loop. When compiling and backtesting a change is fast, and when the cache means only the changed part of the graph recomputes, a researcher can change a parameter, rerun and compare in the time it takes to think about the result. Cheap tests change behaviour: researchers explore parameter spaces more thoroughly and discard mediocre ideas sooner.
Limitations
sigc is not a general-purpose language and does not aspire to be one. It targets stateful computation over time series and cross-sections. A signal that needs something outside the operator library — a trained model, an external API call, a human decision in the loop — will be working against the language rather than with it.
It is also deliberately narrow as a product. It is not hosted: it is a binary and a set of crates you install from crates.io or build from source. It is not an execution broker: it integrates with Alpaca for paper and live trading and assumes you bring your own venues for anything else. It does not provide market data: it loads parquet and CSV from local paths, S3 or GCS, or reads from PostgreSQL, and expects you to keep your own data clean.
And the language has a learning curve. Researchers fluent in pandas have to adapt to declaring data, parameters and signals up front. The trade is that the structure which feels restrictive on day one is the same structure that lets the compiler reject a misaligned join before it reaches a backtest, and lets the cache guarantee that rerunning last month’s research reproduces last month’s numbers.