Arithmion SDK Quickstart
From pip install to your first backtest verdict backed by out-of-sample evidence. Real class names, real defaults, and why your first run gets stopped at the gate.
The fastest way into a quant framework is not reading the API reference front to back. It is walking one short chain end to end: install it, write a strategy class, run a backtest, get a validation verdict. Four moves. The fourth one — the verdict — is where this SDK differs from most others, which is why most of this article lives there.
First, the naming, because otherwise your first import will confuse you. Arithmion is the brand; the Python package is called apexquant. The platform grew out of a graduate thesis project named ApexQuant, the brand changed, the package name didn’t — renaming would have moved every import, every entry-point group, and every version marker for zero functional gain. Wherever you read apexquant below, read Arithmion.
Which layer you install
The SDK is split in two, and you have to pick before you install.
The contract tier is apexquant.core: abstract base classes, typed data carriers, the plugin registry. It is open source in its entirety at arithmion-core. Clone it, pip install -e ., and you have every interface. It needs Python ≥ 3.11 and its only runtime dependencies are pandas and loguru, so it drops into any environment without a fight. If you just want to feel the contracts and see what your strategy code will look like, this is enough.
The implementation tier — engines, data sources, trainers, the agent runtime — ships with the platform. Actually running a backtest and getting a verdict needs the full product.
Both tiers share one import path, so code written against the contracts runs unchanged on either side.
First code: a strategy is a class
The smallest usable strategy in the contract tier looks like this. It comes straight from the arithmion-core README:
from apexquant.core.bar import Bar
from apexquant.core.strategy import BaseStrategy, Signal
class Momentum(BaseStrategy):
name = "momentum"
freq = "daily"
def on_bar(self, bar: Bar) -> Signal:
if bar.close > bar.open:
return Signal.BUY
return Signal.HOLD
PROMPT
Write a minimal daily momentum strategy with Arithmion’s BaseStrategy: buy on an up bar, otherwise hold.
on_bar is the only method you must implement. The engine calls it once per bar and you return a Signal (BUY/SELL/SHORT/COVER/HOLD) or an order object. Everything else is an optional lifecycle hook: on_start, on_end, on_fill, on_regime_change, and on_prediction for when model predictions arrive asynchronously.
That freq = "daily" line deserves its own sentence, because it is where newcomers trip. Frequency is part of the strategy’s design, not a runtime preference. An MA200 spans 200 days on daily bars and under three days on 15-minute bars — the same code at two frequencies is two different strategies. So frequency is declared as a class attribute, and the runner reads it before loading data, without instantiating anything.
Cross-sectional rotation has its own base class. For quantile long-short books and target-weight rebalancing you subclass PortfolioStrategy (apexquant.core.portfolio_strategy) and implement on_rebalance(view), returning {ticker: target weight} against current equity. Bar buffering, diffing targets against holdings, T+1 fill matching — all of that belongs to the engine. The entire reason this base class exists is to replace the “hand-roll a buffer and an equity ledger inside on_bar” shape that everyone writes wrong at least once. When you get a multi-symbol task, your instinct will be to go back to on_bar and start collecting arrays. Don’t.
What the data looks like
The engine eats bars in a canonical shape: a long-format OHLCV frame indexed by a tz-aware UTC timestamp, plus a symbol column. Cross-sectional factor research needs two other shapes, and the contract tier ships lossless converters between them. In apexquant.core.panel, bars_to_prices turns the long frame into a wide close-price matrix (rows are timestamps, columns are symbols), and panel_to_factor_series turns one factor-panel column into a (timestamp, symbol) MultiIndex Series. The panel’s own legality is enforced by validate_panel: timestamps must be tz-aware, (timestamp, symbol) pairs must be unique, factor columns must be numeric.
Those constraints sound fussy until you notice what they block. When shapes don’t line up, most pandas operations don’t raise — they broadcast quietly into a wall of NaN, and three hours later you’re staring at a factor with zero IC wondering where your life went.
Factor research: one call to a full report
If factors rather than strategies are your entry point, the path is shorter. apexquant.notebook.evaluate_factor is the notebook-callable factor evaluation entry point, running the same engine as the platform’s POST /api/research/factor/evaluate job:
from apexquant.notebook import evaluate_factor
report = evaluate_factor(bars, "momentum_60", quantiles=5, periods=(1, 5, 10))
report["ic_summary"]
PROMPT
Use Arithmion’s notebook.evaluate_factor to score a built-in momentum factor across 5 quantiles at 1/5/10-bar forward horizons.
The factor argument accepts three things: a built-in factor name, your own stacked (timestamp, symbol) Series, or a wide factor matrix. The built-ins today are momentum_20, momentum_60, reversal_5, and volatility_20 — a short list, but enough to run the whole chain once before you write anything of your own and confirm the environment isn’t broken.
Inside ic_summary, the fields worth reading first are rank_ic_mean (mean Spearman rank correlation), rank_icir (that mean over its standard deviation), and rank_ic_t_stat. The t-statistic does something easy to miss: when your forward returns span several bars (the 5 and 10 in periods), the IC series is autocorrelated, and a naive icir × √n inflates significance. So it uses a Newey-West correction with bandwidth set to the primary period minus one. The report carries the matching Pearson family alongside; the two families are different statistics over the same dates and must never be divided against each other.
One default deserves a hard look: cost_bps is 0.0, an explicit, inspectable gross-of-cost baseline. That is deliberate — pure research math should get exactly what it asked for. But at that setting the quantile curves rebalance every bar for free, and that number is not a tradeable return. Before you conclude anything about deployability, set cost_bps to your market’s real figure and run it again, then read the two numbers together.
Running a backtest, and what it costs by default
The engine runs backtests, and the cost model hangs off the backtest section of the config. The defaults:
"backtest": {
"initial_capital": 100_000,
"commission": 0.001, # 0.1% (10 bp) per side
"slippage": 0.0005, # 5 bp, applied to the next open
"min_commission": 1.0, # $1 minimum per side
"position_size": 0.25,
"max_positions": 6,
}
PROMPT
Show the default cost parameters in Arithmion’s backtest config section and explain what each one means.
These defaults are deliberately conservative — 10 bp commission plus 5 bp slippage is expensive for plenty of markets. That is a feature, not a bug: an overstated cost assumption makes “still alive after costs” mean something. Trading a cheap market, or holding real execution data, you change them to your own numbers.
Note the “next open” in that slippage comment. The strategy emits a signal at bar close; the fill happens at the next bar’s open, with slippage on top. That is not an engine quirk, it is the most basic anti-lookahead constraint, welded into the fill path where you can’t switch it off.
Your first run gets stopped, by design
A finished backtest is a run, and a run needs a validator verdict before it goes anywhere. The built-in statistical validator defaults to min_dsr: 0.95, require_oos: True, min_oos_sharpe: 0.0, min_periods: 20. The mechanism that matters is clamping: whatever validation config you pass is compared field by field against the defaults, numeric fields take whichever is stricter, and require_oos can’t be switched off while the default is true. The default thresholds are a floor. You can raise them; you cannot dig under them.
So the first time you naively run a backtest and ask for a verdict, you will almost certainly get this:
No out-of-sample result present (required)
Read that sentence for what it says. It reports missing evidence, not a bad strategy. And the remedy is explicit: pass a walk-forward config to the backtest, and the run comes back carrying an out-of-sample block.
walk_forward = {
"enabled": True,
"is_months": 12, # in-sample window
"oos_months": 3, # out-of-sample window
"step_months": 3, # advance per fold
"mode": "true_oos",
}
PROMPT
Give the walk-forward config for Arithmion’s run_backtest: 12 months in-sample, 3 months out-of-sample, rolling every 3 months.
The price is compute you actually pay for: with walk-forward on, the run costs roughly number of windows × one backtest. That is why it is off by default.
How the windows get cut is readable code in the contract tier. apexquant.core.training.rolling.rolling_windows(n, train_size=..., test_size=..., step=..., expanding=...) returns a list of Window folds, each holding train_start/train_end/test_start/test_end as half-open index ranges with test_start == train_end. With expanding=False you get rolling windows: the training set keeps a fixed length and slides forward. With expanding=True every window anchors train_start at zero, so the training set grows. On this path, the difference between those two modes is the difference between “trust only the last few years” and “trust all the history”, and it is worth deciding before you run rather than after.
What counts as a first backtest worth continuing
An unpopular opening claim: the number from your first backtest is almost never worth celebrating. What it is worth is being a starting point you can subtract from.
A Sharpe between 0.5 and 1.5 net of costs is the normal band. Below 0.5, a small increase in costs erases it. Above 2.0 — especially on a single asset — your first reaction should be to hunt for leakage, not to celebrate. That isn’t modesty, it’s base rates: a strategy genuinely earning Sharpe 2 on one instrument usually comes with punishing turnover and a narrow capacity, and at the quickstart stage you almost certainly haven’t built that execution capability yet.
Out-of-sample decay is the second number. Say your in-sample Sharpe is 0.87 and walk-forward gives 0.62 out of sample. That’s 29% decay, which sits in the normal range. Above 50% decay, overfitting is the likely story and you shouldn’t proceed. Below 10% decay deserves more suspicion, not less: either you got lucky, or the signal is simple enough to be structural — a repackaged market beta, say — in which case it isn’t alpha.
Turnover is the item people skip. Annualized turnover above 5x is, at retail cost structures, mostly a donation to your broker unless you can get institutional rates. Run the arithmetic against the defaults above: 10 bp commission plus 5 bp slippage per side is 30 bp round trip, and 5x annual turnover makes that a 150 bp annual drag. Whether your gross return survives that is arithmetic, not faith.
Failing all three isn’t failure, it’s information. It tells you whether the next iteration should change the signal, the cost assumptions, or the rebalance frequency.
What this SDK deliberately doesn’t do
It doesn’t hand you a strategy that makes money. The SDK is infrastructure, not a signal service: factors, models, and execution logic are yours to build, and the platform’s job is to make that faster and harder to get wrong.
There are also whole categories it is wrong for, and knowing the boundary beats memorizing the feature list. Microsecond latency work doesn’t belong here. High-frequency market making against exchange co-location is C++ and Rust territory; this bar-driven engine operates in milliseconds, which is fine for overnight swings and fatal for making markets. Deeply illiquid assets don’t belong here either. The fill model assumes there is volume to take, so small-caps trading tens of thousands of dollars a day, OTC bonds, and tokens on a minor DEX need your own data pipeline and your own impact model. Large-scale portfolio optimization is out of range. Ten thousand assets through a Markowitz optimizer with daily rebalancing wants a dedicated optimizer; cvxpy and its relatives fit better.
Everything else — momentum, value, quality, factor blends, machine-learning signals on liquid equities — is what these tools are for.
Where to go next
Once the pipeline runs, the order of expansion is fairly fixed. Widen the universe first: go from a hand-typed list of a few tickers to a real cross-sectional configuration, so the quantiles mean something. Then change factors: run the four built-ins, then write your own — evaluate_factor takes custom factors through the same entry point as built-ins. Then combine: z-score two or three low-correlation factors together and watch whether Sharpe rises. Often it doesn’t, and that is also a result. Finally add risk controls — position sizing, sector neutrality, leverage caps — which have their own plugin slots in the contract tier’s risk domain.
Whatever you write registers through entry points (apexquant.strategies, apexquant.datasources, apexquant.metrics, and the rest), and once the scanner finds it the platform treats it as a first-class citizen. The contract tier documents itself in code; the docstrings in strategy.py and bar.py are the two worth reading first.
Further reading
- The arithmion-core repository (contract-tier source and docs): github.com/Arithmion-git/arithmion-core.
- Chan, E. (2008). Quantitative Trading. Wiley. ISBN 978-0470284889. Chapter 7 walks through a momentum backtest with realistic transaction-cost modeling — the same reasoning behind the conservative cost defaults above.