Skip to main content

scanlang

PyPI Python License: MIT Status: pre-alpha

Screener DSL and scan compiler: signal definitions to polars pushdown filters.

A scan definition is a plain dict (JSON from a UI, a Python literal from a notebook) that scanlang compiles into one validated polars predicate. Nothing is string-interpolated, so there is no injection surface. Filters run on eager DataFrame or lazy LazyFrame; window semantics are computed per partition.

Status: v0.2, pre-alpha. The IR is frozen (additive changes only). For the design rationale, see docs/explanation/ir-design.md.

Install

uv add scanlang              # or: pip install scanlang

Requires Python >= 3.11 and polars >= 1.44. The optional talib extra is a placeholder for a future value-parity indicator module; nothing in the public API depends on it yet.

Quickstart (eager)

Copy-pasteable end-to-end. Defines its own small OHLCV frame, scores it, applies a scan, and prints the picks. The full annotated walkthrough lives in docs/examples/01_quickstart.py.

import polars as pl
from scanlang import apply, score_bars, validate

bars = pl.DataFrame({
    "symbol": ["AAA"] * 30 + ["BBB"] * 30,
    "session": pl.date_range(pl.date(2026, 1, 1), pl.date(2026, 3, 1), interval="1d", eager=True)[:30].to_list() * 2,
    "open":   [10 + i for i in range(30)] + [60 - i for i in range(30)],
    "high":   [11 + i for i in range(30)] + [61 - i for i in range(30)],
    "low":    [9  + i for i in range(30)] + [59 - i for i in range(30)],
    "close":  [10 + i for i in range(30)] + [60 - i for i in range(30)],
    "volume": [1000.0] * 60,
})

scored = score_bars(bars).collect()                    # LazyFrame -> DataFrame at the edge
scan_def = {
    "filters":  [{"property": "score", "op": ">=", "value": 40}],
    "order_by": [{"property": "score", "dir": "desc"}],
    "limit":    5,
}
validate(scan_def)                                      # [] when valid; never raises
print(apply(scored, scan_def).select("symbol", "score", "phase"))

The full quickstart script (lazy in, collect at the edge) is docs/examples/01_quickstart.py. Side-by-side eager/lazy/piped/renamed modes: docs/examples/07_lazy_vs_sync.py.

Quickstart (text DSL)

Prefer a one-liner over the dict? parse turns human syntax into the same scan dict. The golden-cross form uses cross_above so the signal only fires when the 20-EMA actually crosses above the 50-EMA:

from scanlang import parse, validate

ir = parse("cross_above(ema(20), ema(50)) and rsi(close, 14) > 70")
validate(ir)                                            # [] when valid

ema(20) and sma(20) imply the close column; indicators that take an expression like rsi need it spelled out (rsi(close, 14)). Full grammar and operator reference: docs/reference/operators.md, docs/how-to/scan-from-text.md.

Eager vs lazy at a glance

Mode In Out When to use it
Sync / eager pl.DataFrame pl.DataFrame Notebook, REPL, small script. Collect at the data edge, then everything stays eager.
Lazy end-to-end pl.LazyFrame pl.LazyFrame Pipeline that pipes into more polars ops. Add .collect() once at the end.
Mixed pl.LazyFrame (start) -> pl.DataFrame (collect once) pl.DataFrame One .collect() at the polars -> non-polars boundary. Don't collect "to be safe" earlier; you lose predicate pushdown.

apply is shape-preserving (eager in -> eager out, lazy in -> lazy out). score_bars is always lazy out: it returns a LazyFrame so it can fold into a bigger polars plan; call .collect() at the edge if you want a DataFrame. Full guide: docs/how-to/eager-frames.md.

Docs (Diataxis)

Build the site locally with uv run --group docs zensical build (config: zensical.toml). Follows the Diataxis split:

  • Tutorials - learning-oriented; get to a first scan.
  • How-to guides - task-oriented; solve a specific problem (custom catalog/partition, extending indicators, scan from text, score + stats).
  • Explanation - understanding-oriented; IR design, lazy contract, null semantics, validation split, and why there is no SQL backend.
  • Reference - information-oriented; API, operators, indicators, examples index, notebooks, IR freeze.

Notebooks: 01_first_scan.ipynb (Jupyter) and 02_first_scan_marimo.py (marimo)

Development

uv sync --group docs                              # create .venv with zensical
.venv/bin/python -m pytest tests/ -q              # tests
.venv/bin/python -m ruff check src tests          # lint
.venv/bin/zensical serve                          # live-reload docs at :8000
.venv/bin/zensical build                          # static build -> site/

License

MIT.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

scanlang-0.3.0.tar.gz (30.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

scanlang-0.3.0-py3-none-any.whl (33.9 kB view details)

Uploaded Python 3

File details

Details for the file scanlang-0.3.0.tar.gz.

File metadata

  • Download URL: scanlang-0.3.0.tar.gz
  • Upload date:
  • Size: 30.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.12.9 {"installer":{"name":"uv","version":"0.12.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for scanlang-0.3.0.tar.gz
Algorithm Hash digest
SHA256 05502e3953077451e43493256f173a2f8ce7d663b1d3e60723f369f2ee71ed65
MD5 a67cfcff64db65c0ca0797ac670d1e58
BLAKE2b-256 2174a8a24c6bdb3fcbfa0bd0f976deaf249d04bde0a58ab61d1068895751d312

See more details on using hashes here.

File details

Details for the file scanlang-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: scanlang-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 33.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.12.9 {"installer":{"name":"uv","version":"0.12.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for scanlang-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 934f2781f2041d983691ed7bd4720f2158ee91a0010d64497e697431a57345cd
MD5 42220a9290ccf1c2bb0b9d863fde5a7a
BLAKE2b-256 fab32020fdd07cc4fdb76f86b8a92d2e87f3f8fabfb49fed1386ae9c64ef15e5

See more details on using hashes here.

Release history Release notifications | RSS feed

0.4.0

2 files

0.3.1

2 files

This release

0.3.0 This release

2 files

0.2.0

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page