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.2.0 is the bar engine plus the machinery that measures it. v0.1.5 loaded and validated data, inferred sessions, and resampled them correctly. v0.2.0 adds 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.

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 locked tail you cannot read by accident
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

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 339 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 (55)
align, the one join primitive a join that reads a bar before it published tests/test_align.py (28)
provenance-only leak scanning the 375 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 (27)
the run manifest a result you cannot reproduce or diff tests/test_manifest.py (51)
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                                         # reading split.test raises HoldoutLocked

run = up.RunManifest.capture(                       # what ran, on what data, with what config
    bars, steps=[up.Step("resample", {"timeframe": "1h"})]
)
rebuilt = up.replay(run)                            # re-execute it, verifying every digest
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 375 false positives from two sessions that happen to close at exactly 1355.0. 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.2.0.tar.gz (906.2 kB view details)

Uploaded Source

Built Distribution

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

upticks-0.2.0-py3-none-any.whl (329.6 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for upticks-0.2.0.tar.gz
Algorithm Hash digest
SHA256 7a74cb65197d3cb9ef486cee89f4f12d88019e5e2a143ffd9f57874edcebe331
MD5 c736a7086a8adc60925a1eba5e6efd4d
BLAKE2b-256 50ffb8dee1cf313cdef475ff2db52275f1ff619cefd21c8b4ea2a763d00a6d05

See more details on using hashes here.

File details

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

File metadata

  • Download URL: upticks-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 329.6 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.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 83c6a72b1aa1f402fd9a77f00554cae6ffe248d3e12aa5289226608d9d841d71
MD5 49fff23c1a62eeeebf95cab27028c405
BLAKE2b-256 8264ec8bd008d908b594a271a14f3e39c346810c72070283c51f1f01d28cc627

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

0.3.0

2 files

This release

0.2.0 This release

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