Metadata-Version: 2.4
Name: upticks
Version: 0.1.5
Summary: Price action, honestly — a causality-first price-action research library.
Author-email: Nashit Babber <nashit.babber@gmail.com>
License-Expression: Apache-2.0
Keywords: price action,ohlcv,market structure,pandas,backtesting
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Office/Business :: Financial :: Investment
Classifier: Topic :: Scientific/Engineering :: Information Analysis
Classifier: Typing :: Typed
Requires-Python: <3.14,>=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pandas<4,>=2.2
Requires-Dist: numpy<3,>=1.24
Requires-Dist: scipy>=1.10
Provides-Extra: fast
Requires-Dist: numba; extra == "fast"
Provides-Extra: plot
Requires-Dist: matplotlib; extra == "plot"
Provides-Extra: cache
Requires-Dist: pyarrow; extra == "cache"
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Requires-Dist: hypothesis; extra == "dev"
Dynamic: license-file

# upticks

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

```bash
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, not the whole library

**v0.1.5 is the bar engine.** It loads and validates data, infers sessions, and resamples
them correctly. That is genuinely useful on its own, and it is deliberately all that is
here.

| 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 | |

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 it is the
part that is usually wrong.

---

## Why session anchoring

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

```python
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:

```python
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:

```python
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_.** This release ships the structural layers — schema
assertions at construction, the availability contract, and the refusals above — and the
causality harness that makes "these tests" an enumerable, named list arrives in the next
stage. Until it does, read the claim as exactly what it says: these tests did not detect a
leak, which is not the same as there being none.

---

## 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

```python
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`.

---

## 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.

---

## License

Apache-2.0. Copyright (c) Nashit Babber.
