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 +213 -0
- upticks/_banner.py +110 -0
- upticks/_logging.py +40 -0
- upticks/_result.py +343 -0
- upticks/config.py +1063 -0
- upticks/core/__init__.py +2 -0
- upticks/core/_time.py +329 -0
- upticks/core/adjust.py +998 -0
- upticks/core/bars.py +1100 -0
- upticks/core/fingerprint.py +119 -0
- upticks/core/freq.py +457 -0
- upticks/core/holidays.py +645 -0
- upticks/core/hygiene.py +1280 -0
- upticks/core/ingest.py +1916 -0
- upticks/core/meta.py +380 -0
- upticks/core/parts.py +477 -0
- upticks/core/resample.py +1945 -0
- upticks/core/sessions.py +1131 -0
- upticks/core/tickgrid.py +392 -0
- upticks/errors.py +604 -0
- upticks/io/__init__.py +2 -0
- upticks/io/parquet.py +661 -0
- upticks-0.1.5.dist-info/METADATA +230 -0
- upticks-0.1.5.dist-info/RECORD +27 -0
- upticks-0.1.5.dist-info/WHEEL +5 -0
- upticks-0.1.5.dist-info/licenses/LICENSE +202 -0
- upticks-0.1.5.dist-info/top_level.txt +1 -0
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))
|