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.
The problem it exists to remove
Every bar carries two timestamps. The index is the label — where the bar sits on the clock.
avail_ts is the instant the bar became knowable. For an intraday bar those are close
together; for a daily bar built from a session they are hours apart, and that gap is where
look-ahead lives.
Joining a daily bar onto intraday data by calendar date hands the 09:15 bar a close that will not exist until 15:30. On this library's own reference file that is 2,975 of 3,480 rows — 85.5 %, the median gap being 6h15m.
up.align joins backward on avail_ts, and there is no other join on the public surface. No
'nearest', no 'forward', no label-keyed join anywhere.
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
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
Nothing is repaired behind your back. load() reports; repair() is a separate call that takes
an explicit policy and records it in Meta.
Indicators are a registry, not a grab-bag
ind = up.indicators(daily, ["ema_20", "rsi_14", "macd", "bbands"])
ind.columns
# ['ema', 'rsi', 'macd', 'macd_signal', 'macd_hist', 'bb_lower', 'bb_mid', 'bb_upper']
ind["rsi"].isna().sum() == up.lookback("rsi", length=14) == 14 # asserted in CI, per entry
up.unstable_period("rsi", length=14) # 216 — bars until the recursion has converged
up.catalog(family="oscillator") # what each is, what it needs, how it is verified
up.explain(daily, "rsi") # measured lag, warm-up, repainting verdict, provenance
Column names are the registry key and carry no parameters, so a downstream join does not break when a period changes. Two calls to the same indicator at different parameters are disambiguated by a hash of those parameters, never by silent overwrite.
Every entry declares where its defaults came from. up.defaults_provenance() returns 2,142
rows and will tell you that RSI's 14 is literature with Wilder 1978 chapter 6 behind it — and
that its source="close" is an author_choice, because an editorial threshold that claims a
source it does not name is worse than one that admits it is editorial.
Everything at once
frame = up.compute_everything(bars) # 482 columns on the bars' own index
frame.attrs["upticks_computed"] # {'aligned': 126, 'events': 284, 'skipped': 32, ...}
frame.attrs["upticks_skipped"] # {name: the exception that refused it}
The honest version of strategy("all"). Of 442 registry entries, 126 return something aligned to
the bars and contribute 198 columns; 284 publish an EventFrame and are folded to one column each,
keyed on confirmed_at so a column never fires before the bar that confirmed it; and 32 are
refused by the input rather than by the verb, each named with the exception that refused it.
A pattern is not really a column — an EventFrame row knows where the pattern formed, when it was
confirmed and what its pivots were, and a per-bar count keeps only the last of those. Use
events="skip" for the aligned half alone, or call the detector directly and keep the events.
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
# re-splitting or widening raises HoldoutLocked
How it is kept true
Verification is layered, because each layer catches a class the others miss.
| layer | what it catches |
|---|---|
| AST lint | banned constructs in source, before anything runs — syntactic, a cheap filter |
check_causality |
cuts history at many points, recomputes, compares — a measurement |
| planted-bug corpus | deliberately broken implementations that must be caught |
| reference twins | a loop-based reference implementation beside every vectorised kernel |
| golden numbers | measured values pinned per version, per interpreter, per pandas major |
| byte-gated docs | generated pages fail CI if they differ from what the code renders today |
Tested on
Every cell is run, not declared — and each is cross-paired against the other pandas major, so a corpus written under one is read back under the other.
| pandas 2.2.3 | pandas 2.3.3 | pandas 3.0.5 | |
|---|---|---|---|
| Python 3.11 | 9,488 pass | 9,484 pass | 9,485 pass |
| Python 3.12 | 9,488 pass | 9,484 pass | 9,485 pass |
| Python 3.13 | 9,488 pass | 9,484 pass | 9,485 pass |
A declared range is not compatibility; running is. Both axes carry a guard: a Python or pandas version inside the declared range that no cell has executed is a test failure, not a silence. That guard exists because each axis has already shipped a version nobody ran — on one of them, every interval in the library was 1000× too small.
The surface
| exported names, frozen | 138 |
| registered algorithms | 442 |
| hygiene checks per load | 16 |
| defaults with provenance | 2,142 |
| runtime dependencies | 3 |
Three runtime dependencies and no more: pandas>=2.2,<4, numpy>=1.24,<3, scipy>=1.10.
pyarrow, matplotlib and numba are optional extras, imported inside the one function that
needs them; their absence raises DependencyMissing naming the extra rather than an
ImportError from four frames down.
pip install "upticks[plot]" # matplotlib
pip install "upticks[parquet]" # pyarrow
Every one of the 40 public verbs that takes a bars handle carries a Causality: paragraph, and
all 80 public functions carry a primary-source citation. Both are gated by tests, which is what
makes them a contract rather than a convention.
Honest limitations
This section is not an afterthought. It is the part hardest to write and most worth reading.
- The verification tiers are not what the plan projected. The census is 0 entries at tier A, 437 at B, 5 at C. Tier A means a numeric table from the primary text committed as a fixture; none exists yet, so the layer that catches a wrong reading of a formula has not run.
- Session inference is a heuristic. Validated against every session of a two-year 1-minute
file and synthetic fixtures for four other market shapes — a genuinely novel session structure
may need a declared
SessionShape. - Corporate-action detection is candidate-only. 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. 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.
check_causalityis a measurement, not a proof. It cuts history at a finite set of points. A leak that only fires at a cut point it did not choose is a leak it will not report.is_causalis allowed to be undetermined, with the reason named, rather than forced to a boolean it cannot support.- The AST lint is syntactic. A banned operation reached through
getattror a third-party helper is invisible to it. - A BCa interval is bias-corrected, not calibrated. Over 1,000 replications at nominal 0.90, on the variance of a lognormal sample at n=40 it covers 0.607 against the percentile interval's 0.551.
- 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. Leak tests here compare provenance, not values.
- Every measured number comes from one instrument, one exchange and one liquidity regime. A second reference file is the honest fix, and it is not done.
Versioning
Semantic versioning, with one addition this library treats as load-bearing: every release that moves a number — a default, a threshold, a published measurement — records the old value, the new value and what moved on the reference file. "0 rows moved" is still an entry, because a change with no measured effect is information, and its absence is what lets a real one hide.
A major bump is required for: a removed name; a changed column; a changed default; a changed signature; a refusal becoming a value or a value becoming a refusal; a causality contract weakening; and a narrowed support range.
Supported: Python 3.11–3.13, pandas 2.2 through 3.x, numpy 1.24–2.x, scipy 1.10 and later.
Documentation
Full documentation is public at https://github.com/nashit8421/upticks-docs — the verification layers, the complete limitations page, the API and defaults references, the provenance table for all 442 algorithms, and three executable walkthroughs.
- Causality — the layers, each named with something it caught
- Limitations — what the verification does not cover
- Versioning — the freeze, and what counts as breaking
- Changelog — with a Numeric changes section per release
License
Apache-2.0 · Nashit Babber
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
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-1.1.1-py3-none-any.whl.
File metadata
- Download URL: upticks-1.1.1-py3-none-any.whl
- Upload date:
- Size: 3.1 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7d0cf7238ad473527148756153192726b516400456cd721709a2deb399087127
|
|
| MD5 |
25635c7fa446ac0e34602dfc0758509b
|
|
| BLAKE2b-256 |
f4ea7adcc13426e7e6e2e1b128f11a03c2b7235fab803ccbeaf2a1e7e6a85ae2
|