upticks 0.1.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
upticks/__init__.py ADDED
@@ -0,0 +1,213 @@
1
+ """upticks — price action, honestly.
2
+
3
+ A causality-first price-action research library. Plain pandas in, plain pandas out.
4
+ Zero trading dependencies.
5
+
6
+ Created by Nashit Babber <nashit.babber@gmail.com>
7
+ """
8
+ from __future__ import annotations
9
+
10
+ # Imports that are not part of the surface are aliased under `_`: `dir(upticks)` must contain
11
+ # nothing public beyond `__all__` and the submodule names.
12
+ import typing as _t
13
+
14
+ import pandas as _pd
15
+
16
+ from ._result import Result, ScanResult
17
+ from .config import PRESETS as presets
18
+ from .config import (
19
+ Preset,
20
+ ResolvedConfig,
21
+ defaults_provenance,
22
+ option_context,
23
+ options,
24
+ preset,
25
+ )
26
+ from .core.adjust import AdjustmentTable, adjust, detect_actions, read_adjustments
27
+ from .core.bars import Bars, Capabilities
28
+ from .core.freq import Freq, normalize_freq
29
+ from .core.holidays import HolidayTable
30
+ from .core.holidays import derive_holidays as _derive_holidays
31
+ from .core.hygiene import CheckOutcome, CheckSpec, repair
32
+ from .core.hygiene import checks as _check_registry
33
+ from .core.hygiene import run_checks as _run_checks
34
+ from .core.ingest import capabilities, load, load_benchmark, reattach, scan
35
+ from .core.meta import Meta
36
+ from .core.parts import closing_range, initial_balance, opening_range, session_part
37
+ from .core.resample import resample
38
+ from .core.sessions import SessionShape, SessionTable, infer_sessions
39
+ from .core.tickgrid import TickInference
40
+ from .errors import (
41
+ AmbiguousBar,
42
+ ConfigurationError,
43
+ DataQualityError,
44
+ DegenerateInputWarning,
45
+ DependencyMissing,
46
+ HoldoutLocked,
47
+ InsufficientBars,
48
+ LookaheadWarning,
49
+ MetadataLost,
50
+ MetadataLostWarning,
51
+ NonCausalDetector,
52
+ PerformanceWarning,
53
+ ProvisionalDataWarning,
54
+ RepaintingDetector,
55
+ StructurallyUnavailable,
56
+ UnknownKey,
57
+ UpticksError,
58
+ UpticksWarning,
59
+ )
60
+ from .io.parquet import read_actions, read_parquet, to_parquet
61
+
62
+ __version__ = "0.1.5"
63
+ __author__ = "Nashit Babber"
64
+ __email__ = "nashit.babber@gmail.com"
65
+
66
+ #: The curated surface, frozen against `tests/api_snapshot.txt` — adding a name is an API change.
67
+ __all__ = [
68
+ "AdjustmentTable",
69
+ "AmbiguousBar",
70
+ "Bars",
71
+ "Capabilities",
72
+ "CheckOutcome",
73
+ "CheckSpec",
74
+ "ConfigurationError",
75
+ "DataQualityError",
76
+ "DegenerateInputWarning",
77
+ "DependencyMissing",
78
+ "Freq",
79
+ "HoldoutLocked",
80
+ "HolidayTable",
81
+ "InsufficientBars",
82
+ "LookaheadWarning",
83
+ "Meta",
84
+ "MetadataLost",
85
+ "MetadataLostWarning",
86
+ "NonCausalDetector",
87
+ "PerformanceWarning",
88
+ "Preset",
89
+ "ProvisionalDataWarning",
90
+ "RepaintingDetector",
91
+ "ResolvedConfig",
92
+ "Result",
93
+ "ScanResult",
94
+ "SessionShape",
95
+ "SessionTable",
96
+ "StructurallyUnavailable",
97
+ "TickInference",
98
+ "UnknownKey",
99
+ "UpticksError",
100
+ "UpticksWarning",
101
+ "__version__",
102
+ "adjust",
103
+ "banner",
104
+ "capabilities",
105
+ "checks",
106
+ "closing_range",
107
+ "defaults_provenance",
108
+ "detect_actions",
109
+ "holidays",
110
+ "hygiene",
111
+ "infer_sessions",
112
+ "initial_balance",
113
+ "load",
114
+ "load_benchmark",
115
+ "normalize_freq",
116
+ "opening_range",
117
+ "option_context",
118
+ "options",
119
+ "preset",
120
+ "presets",
121
+ "read_actions",
122
+ "read_adjustments",
123
+ "read_parquet",
124
+ "reattach",
125
+ "render",
126
+ "repair",
127
+ "resample",
128
+ "scan",
129
+ "session_part",
130
+ "to_parquet",
131
+ ]
132
+
133
+
134
+ def _param_text(value: _t.Any) -> str:
135
+ """Render a check parameter for reading: `8`, `3`, `(2, 0.5, 5)`."""
136
+ if isinstance(value, float):
137
+ return f"{value:g}"
138
+ if isinstance(value, (tuple, list)):
139
+ return "(" + ", ".join(_param_text(v) for v in value) + ")"
140
+ return str(value)
141
+
142
+
143
+ def checks() -> _pd.DataFrame:
144
+ """The sixteen hygiene checks as a tidy frame, in canonical report order.
145
+
146
+ Columns: `check_id | severity | requires | params | doc`. `params` holds the pinned,
147
+ overridable defaults rendered as text (`k=8`); every one of them also has a row in
148
+ `defaults_provenance()`. This is the registry, not a result — it says what *would* run.
149
+ `hygiene(bars)` runs it.
150
+ """
151
+ frame = _check_registry() # a fresh frame per call, so rewriting a column is safe
152
+ frame["params"] = [
153
+ ", ".join(f"{k}={_param_text(v)}" for k, v in params.items()) for params in frame["params"]
154
+ ]
155
+ return frame
156
+
157
+
158
+ def hygiene(
159
+ bars_or_df: _t.Any,
160
+ *,
161
+ sessions: SessionTable | None = None,
162
+ tick: TickInference | None = None,
163
+ params: _t.Mapping[str, _t.Mapping[str, _t.Any]] | None = None,
164
+ only: _t.Iterable[str] | None = None,
165
+ skip: _t.Iterable[str] | None = None,
166
+ ) -> _pd.DataFrame:
167
+ """Run the sixteen checks over a `Bars` handle or a bare OHLCV frame. Never mutates its input.
168
+
169
+ Public alias of `upticks.core.hygiene.run_checks`; see that function for the full argument and
170
+ column contract. A `Bars` handle lends its own `SessionTable` when `sessions=` is omitted, so
171
+ `up.hygiene(bars)` reproduces `bars.quality`.
172
+ """
173
+ return _run_checks(
174
+ bars_or_df, sessions=sessions, tick=tick, params=params, only=only, skip=skip
175
+ )
176
+
177
+
178
+ def holidays(
179
+ bars_or_sessions: _t.Any,
180
+ *,
181
+ min_share: float | None = None,
182
+ extra: _t.Any = None,
183
+ ) -> HolidayTable:
184
+ """The non-trading days a `Bars` handle or a `SessionTable` implies. Never mutates its input.
185
+
186
+ Public alias of `upticks.core.holidays.derive_holidays`, widened to take the handle as well as
187
+ the table: `up.holidays(bars)` is `derive_holidays(bars.sessions)`. See that function for the
188
+ derivation, which is a pure function of the sessions given — a day is a holiday because the
189
+ file has no bars on it, not because a calendar says so, so nothing here consults the future.
190
+
191
+ Args:
192
+ bars_or_sessions: a `Bars` handle carrying sessions, or a `SessionTable`.
193
+ min_share: weekday threshold; default `options.holiday_min_share` (0.2).
194
+ extra: a user holiday table merged in — a path, a `DatetimeIndex`, or date-likes.
195
+ """
196
+ sessions = bars_or_sessions.sessions if isinstance(bars_or_sessions, Bars) else bars_or_sessions
197
+ if sessions is None:
198
+ raise StructurallyUnavailable(
199
+ "this handle carries no session table, so it implies no calendar.",
200
+ construct="holidays",
201
+ reason="sessions were not inferred (the index was not strictly increasing)",
202
+ remedy="re-load with a sorted, de-duplicated index, or pass a table built by "
203
+ "up.infer_sessions(df.index)",
204
+ )
205
+ return _derive_holidays(sessions, min_share=min_share, extra=extra)
206
+
207
+
208
+ # The `__future__` import binds a public module-level name; the surface has no room for it.
209
+ del annotations
210
+
211
+ from ._banner import banner, render # noqa: E402
212
+
213
+ banner()
upticks/_banner.py ADDED
@@ -0,0 +1,110 @@
1
+ """Import banner. Cosmetic only — never affects library behaviour.
2
+
3
+ Shown once per process on an interactive import. Automatically silent where a
4
+ banner would corrupt output: piped stdout, pytest, CI, or UPTICKS_NO_BANNER=1.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import os
9
+ import sys
10
+ from typing import Protocol
11
+
12
+ __all__ = ["banner", "render"]
13
+
14
+
15
+ class _TextSink(Protocol):
16
+ """The whole of what the banner asks of a stream: somewhere to put text.
17
+
18
+ Deliberately narrower than `typing.TextIO`. The display rules probe `isatty` through
19
+ `getattr`, so a stream without one is legal, and every write happens inside the fail-open
20
+ handler, so a stream whose methods raise is legal too. `sys.stderr` is only the default.
21
+ """
22
+
23
+ def write(self, text: str, /) -> object: ...
24
+
25
+ def flush(self) -> object: ...
26
+
27
+
28
+ _SHOWN = False
29
+
30
+ AUTHOR = "Nashit Babber"
31
+ EMAIL = "nashit.babber@gmail.com"
32
+ TAGLINE = "price action, honestly"
33
+
34
+ _LOGO = r"""
35
+ ██╗ ██╗██████╗ ████████╗██╗ ██████╗██╗ ██╗███████╗
36
+ ██║ ██║██╔══██╗╚══██╔══╝██║██╔════╝██║ ██╔╝██╔════╝
37
+ ██║ ██║██████╔╝ ██║ ██║██║ █████╔╝ ███████╗
38
+ ██║ ██║██╔═══╝ ██║ ██║██║ ██╔═██╗ ╚════██║
39
+ ╚██████╔╝██║ ██║ ██║╚██████╗██║ ██╗███████║
40
+ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚══════╝
41
+ """
42
+
43
+ # An ascending candlestick strip: wick, body, wick.
44
+ _CHART = [
45
+ " ╷ ",
46
+ " ╷ ╷ ▐█▌ ",
47
+ " ╷ ╷ ▐█▌ ▐█▌ ╵ ",
48
+ " ╷ ╷ ▐█▌ ▐█▌ ╵ ╵ ",
49
+ " ╷ ╷ ╷ ▐█▌ ▐█▌ ╵ ╵ ",
50
+ " ▐█▌ ▐█▌ ▐█▌ ╵ ╵ ",
51
+ " ╵ ╵ ╵ ",
52
+ ]
53
+
54
+ _RESET, _DIM, _BOLD = "\033[0m", "\033[2m", "\033[1m"
55
+ _GREEN, _CYAN, _WHITE = "\033[38;5;42m", "\033[38;5;51m", "\033[38;5;255m"
56
+
57
+
58
+ def _supports_color(stream: _TextSink) -> bool:
59
+ if os.environ.get("NO_COLOR"):
60
+ return False
61
+ if os.environ.get("FORCE_COLOR"):
62
+ return True
63
+ if not hasattr(stream, "isatty") or not stream.isatty():
64
+ return False
65
+ return os.environ.get("TERM", "") not in ("", "dumb")
66
+
67
+
68
+ def _should_show(stream: _TextSink) -> bool:
69
+ """Silent wherever a banner would corrupt output or spam a log."""
70
+ if os.environ.get("UPTICKS_NO_BANNER"):
71
+ return False
72
+ if "pytest" in sys.modules or "PYTEST_CURRENT_TEST" in os.environ:
73
+ return False
74
+ if os.environ.get("CI") or os.environ.get("GITHUB_ACTIONS"):
75
+ return False
76
+ return bool(getattr(stream, "isatty", lambda: False)())
77
+
78
+
79
+ def render(color: bool = True) -> str:
80
+ """The banner as a string. Always available, regardless of display rules."""
81
+ c = (lambda s, code: f"{code}{s}{_RESET}") if color else (lambda s, code: s)
82
+ out = [c(line, _CYAN + _BOLD) for line in _LOGO.strip("\n").split("\n")]
83
+ out += [c(line, _GREEN) for line in _CHART]
84
+ width = 57
85
+ out.append(c(" " + "─" * (width - 3), _DIM))
86
+ out.append(
87
+ c(" " + TAGLINE.ljust(26), _WHITE)
88
+ + c("created by ", _DIM)
89
+ + c(AUTHOR, _WHITE + _BOLD)
90
+ )
91
+ out.append(c(" " + " " * 26 + "connect ", _DIM) + c(EMAIL, _CYAN))
92
+ return "\n".join(out) + "\n"
93
+
94
+
95
+ def banner(force: bool = False, stream: _TextSink | None = None) -> None:
96
+ """Print the banner. Called once on import; safe to call again with force.
97
+
98
+ `stream` defaults to `sys.stderr` and needs only `write`; anything it raises is swallowed.
99
+ """
100
+ global _SHOWN
101
+ stream = stream or sys.stderr
102
+ if not force:
103
+ if _SHOWN or not _should_show(stream):
104
+ return
105
+ _SHOWN = True
106
+ try:
107
+ stream.write("\n" + render(color=_supports_color(stream)) + "\n")
108
+ stream.flush()
109
+ except Exception:
110
+ pass # a cosmetic banner must never break an import
upticks/_logging.py ADDED
@@ -0,0 +1,40 @@
1
+ """Logger namespace.
2
+
3
+ One flat tree under `upticks`: a `logging.NullHandler` is attached to the root at import
4
+ and nothing else — no formatter, no level, `propagate` left at its default so an
5
+ application's own logging configuration stays in charge. DEBUG records internal
6
+ decisions, INFO user-visible phase transitions.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+
12
+ __all__ = ["ROOT", "get_logger"]
13
+
14
+ ROOT = "upticks"
15
+
16
+ _root = logging.getLogger(ROOT)
17
+ if not any(isinstance(h, logging.NullHandler) for h in _root.handlers):
18
+ _root.addHandler(logging.NullHandler())
19
+
20
+
21
+ def _child(name: str) -> str:
22
+ """Map any module path onto `upticks` | `upticks.<sub>`.
23
+
24
+ Only the last dotted segment is kept, so `upticks.core.ingest` and `upticks.ingest`
25
+ are the same logger — the tree is one level deep by design, and a caller cannot
26
+ accidentally configure a logger outside the package namespace.
27
+ """
28
+ sub = name.rsplit(".", 1)[-1].strip()
29
+ if not sub or sub in (ROOT, "__init__", "__main__"):
30
+ return ROOT
31
+ return f"{ROOT}.{sub}"
32
+
33
+
34
+ def get_logger(name: str = ROOT) -> logging.Logger:
35
+ """Return the `upticks` logger, or its `upticks.<sub>` child for `name`.
36
+
37
+ Pass `__name__`. Children used in A1: `upticks.ingest`, `upticks.hygiene`,
38
+ `upticks.sessions`, `upticks.config`.
39
+ """
40
+ return logging.getLogger(_child(name))