Skip to content

Arithmion SDK Overview

What Arithmion's Python contract tier looks like, why it splits into contracts and implementations, and why the validation gates only tighten, never loosen.

1 of 8 ~12 min

Most quant tools are forgiving of user error: they run whatever you feed them and report a number. Arithmion takes the opposite stance — the common ways a researcher fools themselves get blocked at the interface layer wherever the interface layer can block them. That’s not a slogan; this article gets specific, down to class names and default values, about where that stance lives in the code.

First, a naming note. Arithmion’s Python package is called apexquant. The platform grew out of a graduate-thesis project named ApexQuant; the brand changed, the package name didn’t, because renaming would have touched every import, entry-point group, and version marker for zero functional gain. Wherever you read apexquant or aq, it means Arithmion.

Two Tiers: Contracts and Implementations

The SDK splits into two tiers with a hard boundary between them.

The contract tier is apexquant.core: abstract base classes, typed data carriers, and the plugin registry. It contains no engine, no vendor code, no agent runtime, and its runtime dependencies are pandas and loguru — nothing else. This entire tier is open source as the arithmion-core repository; the main repository exports src/apexquant/core verbatim via a sync script, so code written against the contracts runs unchanged in both the open-source package and the full product.

The implementation tier is apexquant.bundled plus the engine and the agent runtime, shipped with the platform. The backtest engine, concrete data sources, trainers, and validators all live here.

This split decides where your code goes. To write a strategy, a data source, or a metric, you subclass a contract-tier base class and register it through an entry point (apexquant.strategies, apexquant.datasources, apexquant.metrics, and so on); the platform’s scanner discovers it and treats it as a first-class implementation. You never need to read the implementation tier — the contract is the whole agreement. Interfaces are at v0.x with no freeze promise yet; breaking changes are recorded in the CHANGELOG, and versioned contracts carry an __apexquant_api_version__ tuple.

A quick tour of what the contract tier holds. Data contracts: bar (OHLCV bars), signals (the signal proxy), result together with backtest.result/backtest.serialize (the backtest result carrier and its canonical JSON wire shape), panel (the cross-sectional factor panel), trade_plan, config_schema (the typed schema plugins use to declare their configuration surface), and model_meta (the meta.json schema describing trained-model artifacts). Plugin contracts: strategy and portfolio_strategy, predictor (per-bar inference), datasource and streaming (bounded batch data and unbounded live data), broker, engine, metric and validation, risk (risk manager, position sizer, stop policy), training (the trainer contract and the walk-forward window partitioner), and universe with calendar (point-in-time stock pools and trading calendars). Mechanism: registry and plugins handle the domain registry and entry-point scanning.

What Writing a Strategy Feels Like

The smallest working strategy looks like this — the snippet 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 bar by bar and takes back a signal or a pending order. Everything else is an optional lifecycle hook: on_start, on_end, on_fill (fill callbacks), on_regime_change, and on_prediction (when a model prediction arrives asynchronously).

That freq = "daily" line deserves its own paragraph, because it encodes one of the SDK’s design judgments: bar 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 on two frequencies is two different strategies. So frequency is declared as a class attribute, and the runner reads it before loading data, without instantiating the class. The design came out of a real user incident: when frequency could only come from global config, a daily strategy on a machine defaulting to 15-minute bars found no data at all — every ticker came back no_data — and the only fix was editing a global default that dragged every other strategy along with it.

Model predictions reach the strategy through bar.signals, a path-addressed proxy:

vol = bar.signals["layer1/volatility/lightgbm_v3"]   # one specific model
layer1 = bar.signals.layer1                          # mean across that layer's models
val = bar.signals.get("layer1/volatility/lightgbm_v3", 0.5)  # with a default

Only a dict-like lookup stands between your strategy and the models. There is no hidden fusion logic: how models get weighted is written in your strategy, where you can see it.

Cross-sectional rotation is a separate contract: PortfolioStrategy. You implement on_rebalance(view) and return {ticker: target weight}, weights expressed against current equity. Bar buffering, target-versus-holdings diffing, and T+1 order fills are the engine’s job — the whole 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 exactly once. Note the T+1 detail: the signal is computed on today’s bar, the order fills on the next one. That’s not an engine quirk; it’s the most basic lookahead guard, and the engine welds it shut for you.

The same thinking runs through the data layer: the lower-frequency aggregates a multi-timeframe strategy reads (bar.higher_tf["daily"]) only ever contain completed periods, and the universe contract is point-in-time — on any backtest date, the pool holds only the constituents that actually existed that day. Lookahead and survivorship protection don’t rely on user discipline; they rely on the shape of the data path.

Why Point-in-Time Correctness Is Worth This Much Machinery

Because most backtest lies start with information that didn’t exist at decision time, and that is the single most common reason a backtested strategy dies in production. Three typical flavors:

Lookahead in market data. Using today’s return to size today’s position. Obvious when stated, remarkably easy to commit — with daily data, the natural temptation is to use the day’s close to make a same-day decision. Computing features at the close and filling at that same close means trading on the future. The correct behavior is to fill on the next bar, which is exactly the engine’s T+1 rule.

Lookahead in fundamentals. Quarterly earnings “belong to” time TT, but the filing happened at some t<Tt < T, anywhere from same-day to six-plus weeks earlier. Joining on the reporting period means trading on numbers nobody had yet; the correct join is on the publication timestamp. Arithmion’s point-in-time handling follows the latter semantics — the test infrastructure even has a hard rule that date_published must be an explicit naive timestamp, so sloppy data hits an assertion before it hits a backtest.

Survivorship bias. Backtesting on today’s index membership means seeing only the companies that survived; delisted names usually looked terrible right before delisting, and that is precisely the data you need to judge real risk. The apparent backtest return that survivorship inflates is typically on the order of one to a few percent per year, varying with sample period and universe. The point-in-time universe contract exists for exactly this.

The Validation Gate: Tighten Only, Never Loosen

The place the design stance shows most clearly is the validation layer. A finished backtest is a run; runs pass through validators (the validation contract) that issue a verdict. The built-in statistical validator’s defaults: min_dsr: 0.95 (the probability that the true Sharpe exceeds the deflated benchmark must be at least 0.95 — the multiple-testing guard), require_oos: True (the run must carry an out-of-sample result block), min_oos_sharpe: 0.0, and min_periods: 20 (below that, higher-moment statistics aren’t trustworthy, so the run is rejected outright).

The key mechanism is clamping: whatever validation config the caller passes in gets compared item by item against the defaults, numeric settings resolve to the stricter of the two, and require_oos cannot be switched off as long as the default says on. In other words, the default thresholds are a floor. You can pile more rigor on top; you cannot dig underneath. Ask for a passing verdict on a run with no out-of-sample evidence and the validator’s answer is always the same: “No out-of-sample result present (required)” — and note the semantics of that sentence. It’s a statement about missing evidence, not about a bad strategy. The remedy is equally explicit: run the backtest with a walk-forward configuration, and the run gets its out-of-sample block.

This is far less theatrical than “raise an exception on lookahead,” but it points the same way: the toolkit fights you when you’re being sloppy and gets out of the way when you’re being rigorous.

Agent-Native: One Toolset, Humans and AI on Equal Footing

Arithmion is an agent-native platform, and for the SDK that means one concrete thing: platform capabilities are exposed as tools, and the research agent calls the same ones you reach through the UI. Data (get_market_data), code execution (run_python), strategy storage (save_strategy/validate_strategy), backtesting (run_backtest, with custom ticker lists and sources, walk-forward, and config overrides), experiment management (run_experiment, diff_runs), validation and promotion (validate_run, promotion_check), and deployment (create_paper_deployment and friends).

The safety boundary is welded into the tool layer too, not left to the model’s judgment. Three specifics worth knowing:

First, when the agent creates a deployment, mode isn’t even a parameter — the code hardcodes paper, and no string the model can emit reaches the live branch. Second, the start tool refuses to start a deployment whose stored mode is live: a human may promote one behind the agent’s back, but the agent must not be the thing that then arms it. Third, no promote-to-live tool exists on the platform. promotion_check answers only one question — is this run eligible for promotion — and the sole source of eligibility is a passing validation verdict. Actually putting money behind a strategy is a human decision, made through a human’s entry point.

Foreign results can’t slip in through a side door either: runs brought in via import_run are marked origin=external and carry no promotion eligibility by default. Numbers the platform didn’t watch being produced are not grounds for deploying capital.

The Full Lifecycle

String the pieces together and a strategy’s path from idea to production is a chain of gates:

run_backtest(..., walk_forward=...)   # produces a run with an out-of-sample block
        ↓
validate_run(run_id)                  # DSR, OOS, sample-size checks; thresholds tighten only
        ↓
promotion_check(run_id)               # no passing verdict = no promotion eligibility
        ↓
create_paper_deployment / start_...   # the agent can go this far: paper trading
        ↓
(live promotion)                      # no tool for this — humans only

Every step leaves a record: runs carry metrics and verdicts, deployments carry mode and state, strategies carry versions. At any moment you can answer “on what grounds is this strategy running?” — and the answer lives in the record, not in someone’s memory. If you run a multi-person operation, this chain is what your risk team checks; working alone, it’s how you settle accounts with yourself six months from now.

Where to Start

If you want to feel out the contracts before committing to the full product, the path is ready-made: clone the arithmion-core repository and pip install -e . — that gets you the entire apexquant.core interface, with dependencies light enough to install on a whim. Write a BaseStrategy subclass along the lines of the Momentum example above, and read the docstrings in strategy.py and bar.py — the contract tier’s documentation lives in the code. With the full product installed, the bundled quickstart runs the complete three-layer cascade end to end on packaged sample data — offline, CPU-only, no data sources to configure — a good way to confirm the environment isn’t broken. From there, whatever you write registers through an entry point, and the platform knows it exists.

What the SDK Doesn’t Do

A few things it explicitly does not do. Knowing the boundary is more useful than memorizing the feature list.

It does not provide pre-built profitable strategies. The SDK is infrastructure, not a signal service: the factors, the models, and the execution logic are yours to build, and the platform’s job is to make that faster and harder to get wrong. There are no templates that print money. If you want a signal service, look elsewhere.

It does not hide complexity. There is no magic mode. How a metric is computed, why a validation failed, how a fill was simulated — all of it is traceable to a specific place. The cost is a steeper learning curve than fully managed platforms; the benefit is that every conclusion is auditable.

And it doesn’t try to be everything. The platform focuses on factor and signal research for equity-like assets; the data surface extends through the datasource contract (built-in sources cover the common market-data APIs, and external Parquet datasets can be registered in). Options pricing is not native. The backtest engine executes on bars, so high-frequency microstructure is not its battlefield. Strategy code — especially strategy code of unknown provenance — executes in isolated containers, which is the precondition for being open to plugins and protective of the host at the same time.

The Honest Take

The SDK is opinionated, and you will fight it. The first time you get a “missing evidence” verdict because you skipped walk-forward, you’ll be annoyed. The fifth time, you’ll realize what it intercepted was an overfit number you had almost believed. The friction is deliberate, and it’s concentrated where self-deception is most expensive — validation and promotion. Everywhere else the contract stays as thin as it can be: one on_bar and you’re running.

The next three articles fill in the details: Data pipelines and storage covers building a point-in-time, survivorship-free data foundation; The backtest engine covers fill simulation, cost models, and validation gates; Research to production workflow expands the gate chain at the end of this article into the full lifecycle. Together, the four are the operating manual for serious quant work with Arithmion.

References

  • López de Prado, M. Advances in Financial Machine Learning, Ch. 1 (Machine Learning for Finance). Wiley, 2018. ISBN: 978-1-119-48208-6.
  • The arithmion-core repository (contract-tier source and docs): github.com/Arithmion-git/arithmion-core.
  • Aronson, D. Evidence-Based Technical Analysis, Ch. 6 (Statistical Inference). Wiley, 2006. ISBN: 978-0-470-00874-4.
  • Harris, L. Trading and Exchanges: Market Microstructure for Practitioners. Oxford University Press, 2003. ISBN: 978-0-19-514470-3.