Skip to main content

upticks

Price action, honestly. A causality-first price-action research library for Python. Plain pandas in, plain pandas out.

pip install upticks

Most technical-analysis libraries will happily hand you a number that could not have been known at the time it is stamped. upticks is built so that the leak is structurally unavailable: every bar carries the instant it became knowable, and that is the only key a join is allowed to use.


Status: alpha — the foundation and its causality harness, not the whole library

v0.3.0 is the event layer: one shape every detector will speak. v0.1.5 loaded and validated data, inferred sessions, and resampled them correctly. v0.2.0 added the layer that turns "causality-first" from a design intent into something you can run: a leak-safe join, a measuring harness, an AST lint, a warm-up model, a locked holdout, and a run manifest. v0.3.0 adds the thing detectors emit — a 35-column EventFrame with four timestamps, the operations over it, three typed satellite tables for geometry that will not fit a fixed row, one documented total order for same-bar resolution, and the registry and catalog that make a detector discoverable before it runs.

There are no detectors yet. The registry ships empty on purpose: this is the shape, the construction assertions and the guards, landed before anything fills them.

shipping now not here yet
load / scan — ingest, aliasing, dtype coercion, timezone discipline indicators (moving averages, momentum, volatility, …)
16 hygiene checks as a tidy report; repair() as a separate explicit call candlestick and chart patterns
gap-based session inference — no exchange calendar, anywhere pivots and market structure
the session-anchored resampler, 1minYE the event-study and backtest engines
session parts: opening range, initial balance, closing range plotting
corporate actions and the three adjustment modes
align — the one join primitive, backward on avail_ts and nowhere else
check_causality — cut the history, recompute, and report what moved
lint_report — the AST lint over your detector, before it ever runs
holdout — a tail sealed by a lock that propagates through every verb
RunManifest / replay / diff_runs — what was run, on what, and what changed
project_events / project_levels — carrying a coarse frame's answers down without leaking them
make_events — the one constructor, and the only place an event is validated
to_wide / to_events / dedupe / filter_events / compose_events — the operations over an event set
join_bars — bar columns onto events, through the one join primitive and no other path
precedence — the documented total order for everything that resolves on one bar
catalog / explain / lookback — what a detector is, and what it will cost you, before you run it

If you install this expecting RSI, you will be disappointed. Indicators arrive in the next stage. What is here is the layer all of that has to be a pure function of — and now also the instrument that tells you whether it is.


Why session anchoring

Resample a 09:15-opening equity session to hourly bars with pandas and you get this:

df.resample("1h").agg(AGG)      # [45, 60, 60, 60, 60, 60, 30]  ← a 45-minute first bar

The grid is anchored to midnight, so the session's first bucket is a stub and every subsequent boundary is offset from the open. upticks anchors each bucket to the session's own first bar:

up.resample(bars, "1h")         # [60, 60, 60, 60, 60, 60, 15]  ← anchored to 09:15

The difference compounds. A 75min or 125min frame under a global origin drifts to a different time of day on every subsequent session; anchored per session it lands on the same five (or three) boundaries every trading day. A special evening session — NSE's Muhurat trading, 18:00–18:59 — becomes exactly one hourly bar instead of being shredded or dropped.

Both behaviours remain reachable: anchor="midnight" is the correct choice for a 24-hour instrument, and it is the only way to obtain the naive grid. You cannot get it by accident.


Causality, concretely

Every resampled bar carries an 18-column bar contract beside the frame, and the column that matters is avail_ts — the instant the bar became knowable:

bars.contract[["bar_open_ts", "bar_close_ts", "avail_ts", "is_complete", "is_forming"]]
  • bar_close_ts is the actual last constituent plus one interval, never the nominal period end. On a truncated or special session those differ, and the nominal answer is wrong.
  • avail_ts is the only legal join key. Joining on a bar's label is what leaks, so labels are not offered as join keys anywhere.
  • is_forming marks a bar that can still change. By default the still-open final bucket is withheld entirely (forming="drop"), so nothing knowable-early reaches a backtest. A daily bar for a session the feed has not finished publishing is not handed to you as settled.
  • Resampling to a finer frequency is refused with a typed error. There is no public forward-fill-to-finer verb, because that is the leak.
  • Resample first, compute second. RSI(14) on hourly bars is a different quantity from RSI(14) computed on minutes and aggregated; no public path produces the latter.

Tested as named properties, not asserted as design intent: volume conservation, extreme preservation, 1min→1h→1D bitwise identical to 1min→1D, idempotence, no bucket spanning two sessions, and avail_ts > bar_open_ts on every emitted bar.

On look-ahead itself the claim is bounded, deliberately: no detectable look-ahead under these tests — never provably none. The bound has not moved; what has changed is that "these tests" is now an enumerable, named list you can run yourself, rather than a promise about the next stage. It is 342 tests across ten modules, and every one of them names the property it holds:

the layer what it catches the tests
schema assertions at construction a frame that cannot be reasoned about at all throughout
check_causality a settled row that changes when the future is removed tests/test_causality.py (29)
planted bugs a harness that reports clean because it is broken tests/test_planted.py (55), over tests/planted/
the AST lint full-series .max(), shift(-n), rolling(center=True), bare .ewm( tests/test_lint.py (56)
align, the one join primitive a join that reads a bar before it published tests/test_align.py (28)
provenance-only leak scanning the 20,685 false positives value-equality scanning produces tests/test_align_causality.py (13)
the warm-up model a recursive indicator whose divergence stops shrinking tests/test_warmup.py (27)
the locked holdout a tail you read without meaning to tests/test_holdout.py (35)
the dependence register a non-causal transform used where causality matters tests/test_dependence.py (28)
the run manifest a result you cannot reproduce or diff tests/test_manifest.py (52)
composition a chained route publishing earlier than the direct one tests/test_composition.py (19)

Read the claim as exactly what it says: these tests did not detect a leak, which is not the same as there being none. The difference now is that you can read the list, run it against your own detector, and see precisely which classes of leak it does and does not cover.


Sessions without a calendar

There is no exchange-calendar dependency anywhere in this library, and there never will be. Sessions are inferred from the multiplicative structure of the data's own gaps; the holiday table is the complement of the observed trading days.

This handles, on real data:

  • special sessions — an evening or afternoon session that matches no modal shape is kept as a first-class session, never merged into a neighbour
  • half-days and truncated feeds — flagged is_short, and is_provisional when they sit at the data tail and the vendor may still revise them
  • lunch-break markets — a mid-session gap is absorbed as a break, not a boundary, so Tokyo's 11:30–12:30 does not split the day in two
  • midnight-crossing sessions — a CME Globex 23:30 bar belongs to the next calendar day's trade date, and the session is one session
  • trading halts — a 90-minute hole is a shape deviation, not a session split
  • DST transitions — boundaries are computed in integer nanoseconds from the session open, so a spring-forward week does not shift them

A declared SessionShape bypasses inference entirely where the heuristic is unsafe.


Quick start

import upticks as up

bars = up.load("NIFTY_1min.csv", tz="Asia/Kolkata", exchange="NSE", preset="nse_intraday")

print(bars.report())
# 500 sessions | 8 short | 2 off-hours (Muhurat) | tick 0.05 | 186747 bars | 2024-08-07 → 2026-08-12

bars.quality          # the 16 hygiene checks, one row each
bars.sessions.table   # one row per session, with its flags

hourly = up.resample(bars, "1h")
daily  = up.resample(bars, "1D")          # one bar per SESSION, never a midnight resample
weekly = up.resample(bars, "W-FRI")       # restamped to the last actual session of the week

orb = up.opening_range(bars, 15)          # one bar per session, first 15 minutes

Nothing is repaired behind your back. load() reports; repair() is a separate call that takes an explicit policy and records it in Meta.

Then the causality half:

joined = up.align(bars, daily, columns=["close"])   # backward on avail_ts; no other key exists

report = up.check_causality(my_detector, bars)      # cut the history, recompute, compare
report.is_causal, report.confirmation_lag, report.n_repainting_bars

up.lint_report("my_package")                        # the AST lint, before anything ever runs

split = up.holdout(bars, frac=0.2)                  # the tail is locked, not merely separate
split.train                                         # the lock rides on split.test and every
                                                    # handle derived from it; re-splitting,
                                                    # widening or re-attaching one raises
                                                    # HoldoutLocked. Reading it does not — the
                                                    # seal refuses by verb, not by read barrier.

run = up.RunManifest.capture(                       # what ran, on what data, with what config
    bars, steps=[up.Step("load", load_kwargs),      # a chain begins at its source, so step 0 is
                 up.Step("resample", {"timeframe": "1h"})]   # the load that produced `bars`
)
rebuilt = up.replay(run, source="RELIANCE.csv")     # re-execute it, verifying every digest — the
                                                    # manifest records what was read, never bytes
up.diff_runs(run, other_run)                        # …and name what differs between two runs

align is the only join in the library, and it is backward on avail_ts. There is no 'nearest', no 'forward', and no label-keyed join anywhere on the public surface.


Design commitments

Three runtime dependencies: pandas, numpy, scipy. Nothing else, ever. pyarrow, matplotlib and numba are optional extras, imported lazily inside the one function that needs them, and their absence raises an error naming the extra.

bars.df is a plain DataFrame and never a subclass, so anything that consumes pandas can consume it. Metadata that pandas drops — timezone, tick grid, session table, fingerprint, adjustment lineage — lives on the handle instead.

Refusals name the missing data. Every error carries a remedy that says what to pass. An ambiguous frequency alias is refused rather than guessed: 60m means sixty minutes in MetaTrader and sixty month-ends in pandas, so upticks refuses it and names both.

Defaults are documented, not folklore. up.defaults_provenance() returns every numeric default with its origin and citation.


Supported versions

Python 3.11–3.13. pandas 2.2 through 3.x, and the test suite is run under both majors — pandas 3 changed the default datetime resolution from nanoseconds to microseconds, which silently breaks naive integer-time arithmetic, so this is verified rather than assumed.


Honest limitations

  • Alpha. The public surface of this stage is stable and tested, but later stages will add to it. Pin the version.
  • Session inference is a heuristic. It is validated against every session of a two-year 1-minute reference file and against synthetic fixtures for four other market shapes, but a genuinely novel session structure may need a declared SessionShape.
  • Corporate-action detection is candidate-only. Splits and bonuses are matched against a small set of rational overnight ratios. An ex-dividend drop is observationally identical to an ordinary news gap without a dividend feed, and is reported as a candidate, never a fact.
  • Back-adjustment is non-causal by construction and says so: rescaling pre-ex-date bars uses information from the ex-date. It is available, registered as non-causal, and refused by default where causality matters.
  • No exchange calendar means no forward-looking holidays. The data is the calendar, so a holiday after the last bar is unknowable; supply one explicitly if you need it.
  • check_causality is a measurement, not a proof. It cuts the history at a finite set of points and compares. A leak that only fires at a cut point it did not choose is a leak it will not report, which is why the AST lint and the planted-bug corpus sit beside it and catch different classes. is_causal is additionally allowed to be undetermined — with the reason named — rather than being forced to a boolean it cannot support.
  • The AST lint is syntactic. It reads your source, so a banned operation reached through getattr or a third-party helper is invisible to it. It is a cheap filter in front of the expensive check, never a substitute for it.
  • Value-equality leak scanning is not offered, deliberately. On the reference file it produces 20,685 false positives on the 1D→1min join — 11.10 % of it — from 48 duplicated values across 56 daily bars. The two sessions that happen to close at exactly 1355.0 contribute 375 of those; that figure is an attribution and never the total, and quoting it alone understates the method's error rate 55.2×. At 3min the total is 166,755. Leak tests here compare provenance.

License

Apache-2.0. Copyright (c) Nashit Babber.

Download files

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

Source Distribution

upticks-0.3.0.tar.gz (1.4 MB view details)

Uploaded Source

Built Distribution

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

upticks-0.3.0-py3-none-any.whl (513.1 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: upticks-0.3.0.tar.gz
  • Upload date:
  • Size: 1.4 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.3

File hashes

Hashes for upticks-0.3.0.tar.gz
Algorithm Hash digest
SHA256 8df575a4283304eff35ebf647b0bb8df446b47c181edab82b33a178e4dc29e3c
MD5 46fa307ca271584f07e33cb720846929
BLAKE2b-256 1ecf83660bb4209eb3507fba6ca7bfedde28ea74381f60bf7eec41d694bd3ff9

See more details on using hashes here.

File details

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

File metadata

  • Download URL: upticks-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 513.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.3

File hashes

Hashes for upticks-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 459d1c72353c7816846215188b88e6a3e2a8cf58679a694f30444761507446da
MD5 9cb7099147798a9d43c1b96771a9cb6c
BLAKE2b-256 1cead81c5a7a945a78e74a5b9d5efe85426a45b1766f3601a9eea96582b99764

See more details on using hashes here.

Release history Release notifications | RSS feed

1.1.1

1 file

1.1.0

1 file

1.0.3

1 file

1.0.2

1 file

This release

0.3.0 This release

2 files

0.2.0

2 files

0.1.5

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