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, 1min → YE |
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_tsis 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_tsis the only legal join key. Joining on a bar's label is what leaks, so labels are not offered as join keys anywhere.is_formingmarks 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 fromRSI(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, andis_provisionalwhen 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_causalityis 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_causalis 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
getattror 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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8df575a4283304eff35ebf647b0bb8df446b47c181edab82b33a178e4dc29e3c
|
|
| MD5 |
46fa307ca271584f07e33cb720846929
|
|
| BLAKE2b-256 |
1ecf83660bb4209eb3507fba6ca7bfedde28ea74381f60bf7eec41d694bd3ff9
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
459d1c72353c7816846215188b88e6a3e2a8cf58679a694f30444761507446da
|
|
| MD5 |
9cb7099147798a9d43c1b96771a9cb6c
|
|
| BLAKE2b-256 |
1cead81c5a7a945a78e74a5b9d5efe85426a45b1766f3601a9eea96582b99764
|