Microtick is an experimental MLIR–based DSL for expressing high-frequency trading (HFT) strategies as IR.
Microtick is built against a local LLVM/MLIR build tree (not system packages).
You’ll need:
- A local LLVM/MLIR build with the following tools built:
mlir-opt,mlir-translate,llc,clang
- CMake ≥ 3.20
- Ninja
- A C++17 compiler (Clang recommended)
The examples below assume:
LLVM build: $HOME/Downloads/llvm-project/build
Microtick: $REPO_ROOT (this repo)
and that your LLVM build was configured with MLIR enabled, e.g. something like:
cmake -G Ninja \
-DLLVM_ENABLE_PROJECTS="mlir;clang" \
-DCMAKE_BUILD_TYPE=Release \
../llvm
ninjaConfigure and build Microtick
cd ${REPO_ROOT}
mkdir -p build
cd build
cmake -G Ninja \
-DLLVM_DIR=$HOME/Downloads/llvm-project/build/lib/cmake/llvm \
-DMLIR_DIR=$HOME/Downloads/llvm-project/build/lib/cmake/mlir \
-DCMAKE_BUILD_TYPE=Debug \
..
# Build the Microtick tools and engine
ninja
## Verification
# Check that the custom microtick-opt exists and is wired up
./microtick-opt/microtick-opt --help | grep microtick
# Check that the engine binary is built
./runtime/src/microtick-engine || true
# (it will print a usage message complaining about a missing strategy, which is fine)Congrats, you're all set up!
MicroTick has a reproducible end-to-end flow:
- Tick dialect strategy (MLIR with
tick.on_book,tick.order.send, etc.) - MicroTick passes (
microtick-opt)--microtick-verify--microtick-lower-runtime--microtick-lower-handlers
- MLIR → LLVM lowering
- LLVM → shared library (
lib<strategy>.dylib) - C++ engine (
microtick-engine)dlopenis the shared librarydlsymis the strategy entrypoint- calls it repeatedly and implements the runtime API (
mt_order_send,mt_order_cancel, …)
Let's say we run ./utils/compile_strategy.sh testing-ir/strategy_e2e_demo.mlir strategy_e2e_demo 10
In the MLIR, we have two send orders followed by a cancel: a passive buy of (99.50 x 10) and an aggressive buy of (101.25 x 50) followed by immediate cancel of the most recebt buy.The surviving orders at the end of the event handler, the send order will be filled at the limit price.
So per market event:
-
$99.50 * 10$ : open and gets filled -
$101.25 * 50$ : canceled and gets skipped
Since we invoked 10 market events:
Per Event:
* Position Change: +10
* Cash Change: -99.50 x 10 = -995
After 10 events:
* Position = 10 x 10 = 100
* Cash = 10 x (-995) = -9,950.00
The Engine assumes a market price of 100 for the symbol to calculate Profit and Loss (PnL)
double marketPrice = 100.0;
double pnl = cash + position * marketPrice;
= -9950 + 100 * 100
= -9950 + 10000
= 50
We’ll use the demo strategy in:
${REPO_ROOT}/testing-ir/strategy_e2e_demo.mlir
// testing-ir/strategy_e2e_demo.mlir
module {
func.func @strategy_e2e_demo() {
tick.on_book {
%p = arith.constant 101.25 : f64
%q = arith.constant 50 : i64
tick.risk.check_notional { limit = 1.000000e+06 : f64 }
tick.risk.check_inventory { limit = 1000 : i64 }
tick.order.send %p, %q
symbol("AAPL")
side("Buy") : f64, i64
tick.order.cancel
symbol("AAPL")
client_order_id("ABC123")
side("Buy")
tick.yield
}
return
}
}TL;DR: one-shot e2e with the helper script
From ${REPO_ROOT}
./utils/compile_strategy.sh testing-ir/strategy_e2e_demo.mlir strategy_e2e_demo 10
All commands below assume you are in the build directory:
cd ${REPO_ROOT}/build
- MicroTick pipeline: verify + runtime lowering + handler lowering
microtick-opt/microtick-opt \
--microtick-verify \
--microtick-lower-runtime \
--microtick-lower-handlers \
../testing-ir/strategy_e2e_demo.mlir \
-o ../strategy_e2e_demo.lowered.mlirThis produces an IR where:
-
tick.order.send→call @mt_order_send(i32, i8, f64, i64) -
tick.order.cancel→call @mt_order_cancel(i32, i8) -
tick.on_book→func.func private @strategy_e2e_demo_on_book()
- Lower MLIR native ops to LLVM dialect
mlir-opt \
../strategy_e2e_demo.lowered.mlir \
--convert-arith-to-llvm \
--convert-func-to-llvm \
--convert-cf-to-llvm \
--reconcile-unrealized-casts \
-o ../strategy_e2e_demo.llvm.mlir- **Go from MLIR-LLVM dialect to LLVM-IR
mlir-translate \
--mlir-to-llvmir \
../strategy_e2e_demo.llvm.mlir \
-o ../strategy_e2e_demo.ll- LLVM IR to Object File
llc \
-filetype=obj \
../strategy_e2e_demo.ll \
-o ../strategy_e2e_demo.o- Compile to a dynamic shared lib
clang -shared -undefined dynamic_lookup \
../strategy_e2e_demo.o \
-o ../libstrategy_e2e_demo.dylib
- Run the engine with the compiled strategy
./runtime/src/microtick-engine \
../libstrategy_e2e_demo.dylib \
strategy_e2e_demo \
10
Here:
../libstrategy_e2e_demo.dylibis the compiled strategy.strategy_e2e_demois the base name of the strategy.- the engine looks up
strategy_e2e_demo_on_bookas the entry point and calls it 10 times.
To keep things simple, MicroTick assumes the same base name is used at all of these levels:
- MLIR file name (without extension)
Example:testing-ir/strategy_messy.mlir → basename = "strategy_messy" Top-level MLIR strategy function func.func @strategy_messy() { tick.on_book { // ... tick.yield } return }