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.


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_causality is 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_causal is 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 getattr or 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

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distribution

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

upticks-1.1.0-py3-none-any.whl (3.1 MB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: upticks-1.1.0-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

Hashes for upticks-1.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 81480f67727bc2b32b54904a93052b84c259742b70a4bdd97132d01355a8adbe
MD5 2abfa897e28dc3d77e274da32ddd5b00
BLAKE2b-256 35ceecc97a081226ce6dee1129c5130d63e8a29b68175bd8083a60a01a8ec32f

See more details on using hashes here.

Release history Release notifications | RSS feed

1.1.1

1 file

This release

1.1.0 This release

1 file

1.0.3

1 file

1.0.2

1 file

0.3.0

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