quantred 2.4.0__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.
- quantred/__init__.py +24 -0
- quantred/alphas/__init__.py +53 -0
- quantred/alphas/base.py +221 -0
- quantred/alphas/combine.py +260 -0
- quantred/alphas/horizon.py +153 -0
- quantred/alphas/refine.py +300 -0
- quantred/alphas/scorers.py +87 -0
- quantred/analytics/attribution.py +354 -0
- quantred/analytics/bootstrap.py +353 -0
- quantred/analytics/charts.py +393 -0
- quantred/analytics/excursion.py +185 -0
- quantred/analytics/execution_quality.py +180 -0
- quantred/analytics/exposure.py +167 -0
- quantred/analytics/htmlreport.py +1208 -0
- quantred/analytics/information.py +197 -0
- quantred/analytics/metrics.py +502 -0
- quantred/analytics/performance.py +639 -0
- quantred/analytics/reporting.py +1672 -0
- quantred/analytics/screening.py +190 -0
- quantred/analytics/series_comparison.py +303 -0
- quantred/analytics/trade_analytics.py +353 -0
- quantred/brokers/alpaca/broker.py +415 -0
- quantred/brokers/alpaca/factory.py +33 -0
- quantred/brokers/alpaca/market_data.py +166 -0
- quantred/brokers/base.py +220 -0
- quantred/brokers/dryrun.py +164 -0
- quantred/brokers/errors.py +70 -0
- quantred/cli.py +8619 -0
- quantred/costs/__init__.py +11 -0
- quantred/costs/base.py +126 -0
- quantred/costs/parametric.py +131 -0
- quantred/data/__init__.py +29 -0
- quantred/data/compute.py +332 -0
- quantred/data/edges.py +81 -0
- quantred/data/features.py +120 -0
- quantred/data/panel.py +61 -0
- quantred/data/scan.py +85 -0
- quantred/data/store.py +269 -0
- quantred/demo/__init__.py +22 -0
- quantred/demo/scanners.py +104 -0
- quantred/demo/strategies.py +125 -0
- quantred/engine/backtest.py +1206 -0
- quantred/engine/barcheck.py +228 -0
- quantred/engine/live.py +539 -0
- quantred/execution/decision.py +192 -0
- quantred/execution/flatten.py +302 -0
- quantred/execution/halt.py +165 -0
- quantred/execution/ledger.py +758 -0
- quantred/execution/live_trader.py +587 -0
- quantred/execution/order_id.py +83 -0
- quantred/execution/sizing.py +114 -0
- quantred/indicators/indicators.py +158 -0
- quantred/marketdata/base.py +62 -0
- quantred/marketdata/client.py +59 -0
- quantred/marketdata/session.py +112 -0
- quantred/marketdata/synthetic.py +74 -0
- quantred/marketdata/timeframe.py +125 -0
- quantred/mcp/__init__.py +6 -0
- quantred/mcp/selftest.py +139 -0
- quantred/mcp/server.py +2419 -0
- quantred/optimization/causality.py +607 -0
- quantred/optimization/config_store.py +300 -0
- quantred/optimization/optimizer.py +431 -0
- quantred/optimization/parallel.py +345 -0
- quantred/optimization/param_space.py +309 -0
- quantred/optimization/walk_forward.py +1254 -0
- quantred/portfolio/allocator.py +135 -0
- quantred/portfolio/benchmark.py +109 -0
- quantred/portfolio/optimizer.py +786 -0
- quantred/portfolio/policy.py +335 -0
- quantred/portfolio/posterior.py +208 -0
- quantred/research/__init__.py +30 -0
- quantred/research/agent.py +868 -0
- quantred/research/demo_proposals.py +222 -0
- quantred/research/llm.py +311 -0
- quantred/research/proposer.py +234 -0
- quantred/research/provider_errors.py +242 -0
- quantred/research/sandbox.py +340 -0
- quantred/risk/__init__.py +60 -0
- quantred/risk/base.py +218 -0
- quantred/risk/conditional.py +474 -0
- quantred/risk/exposures.py +88 -0
- quantred/risk/factor.py +119 -0
- quantred/risk/sample.py +89 -0
- quantred/risk/streaming.py +214 -0
- quantred/scanners/base.py +141 -0
- quantred/scanners/symbol_scanner.py +148 -0
- quantred/services/__init__.py +51 -0
- quantred/services/analysis.py +6082 -0
- quantred/services/audit.py +574 -0
- quantred/services/campaign.py +381 -0
- quantred/services/configs.py +282 -0
- quantred/services/data.py +172 -0
- quantred/services/dryrun.py +206 -0
- quantred/services/glossary.py +130 -0
- quantred/services/maintenance.py +314 -0
- quantred/services/registry.py +351 -0
- quantred/services/setup.py +1023 -0
- quantred/services/sizing.py +150 -0
- quantred/services/smallreal.py +450 -0
- quantred/settings.py +571 -0
- quantred/store/__init__.py +0 -0
- quantred/store/bars.py +660 -0
- quantred/store/trials.py +1804 -0
- quantred/strategies/base.py +643 -0
- quantred/strategies/signals.py +28 -0
- quantred/utils/errors.py +48 -0
- quantred/utils/extras.py +330 -0
- quantred/utils/logging_config.py +33 -0
- quantred/utils/numeric.py +46 -0
- quantred/utils/streaming.py +474 -0
- quantred/utils/timeutils.py +148 -0
- quantred-2.4.0.dist-info/METADATA +632 -0
- quantred-2.4.0.dist-info/RECORD +117 -0
- quantred-2.4.0.dist-info/WHEEL +4 -0
- quantred-2.4.0.dist-info/entry_points.txt +8 -0
- quantred-2.4.0.dist-info/licenses/LICENSE +21 -0
quantred/utils/errors.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Refusals shared across layers.
|
|
2
|
+
|
|
3
|
+
Here rather than in a service because the optimization layer refuses runs too, and it
|
|
4
|
+
must not import the services above it.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import Sequence
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class RunRefused(ValueError):
|
|
12
|
+
"""A run refused before it read or recorded anything, with what to change.
|
|
13
|
+
|
|
14
|
+
A ``ValueError`` so every caller that already handles one keeps working; its own
|
|
15
|
+
type so the CLI can end on the message rather than a stack trace.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def no_bars_refusal(symbols: Sequence[str], start: datetime, end: datetime) -> RunRefused:
|
|
20
|
+
"""The refusal for a run whose universe had no bars at all.
|
|
21
|
+
|
|
22
|
+
Such a run returned a clean-looking result - every ratio 0.0 - and was journaled.
|
|
23
|
+
Zero is a measurement; a run over no bars measured nothing.
|
|
24
|
+
"""
|
|
25
|
+
named = ", ".join(symbols) if symbols else "an empty universe"
|
|
26
|
+
return RunRefused(
|
|
27
|
+
f"No bars for any of {named} between {start:%Y-%m-%d} and {end:%Y-%m-%d}, so there is "
|
|
28
|
+
"nothing to measure and nothing was recorded. Check the symbols and the window."
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class GeneratedCodeRefused(RunRefused):
|
|
33
|
+
"""A config or strategy name that points at code the research agent wrote.
|
|
34
|
+
|
|
35
|
+
A saved config names its strategy, and a generated strategy has no registered name -
|
|
36
|
+
only a ``draft:<Class>:<hash>`` identity. Resolving that identity must never mean
|
|
37
|
+
"find the source and run it": loading a config is not a review.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, name: str):
|
|
41
|
+
from quantred.settings import DOCS_URL
|
|
42
|
+
|
|
43
|
+
super().__init__(
|
|
44
|
+
f"{name!r} is a strategy the research agent wrote, and generated code is never run by "
|
|
45
|
+
"loading a config. Review its source (recorded in the config's "
|
|
46
|
+
"provenance.generated_code and on the journal's trial line), add it to a strategy "
|
|
47
|
+
f"pack, and point the config at the registered name: {DOCS_URL}/usage/private-strategies"
|
|
48
|
+
)
|
quantred/utils/extras.py
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
"""The optional extras: what each provides, whether it is installed, how to add one.
|
|
2
|
+
|
|
3
|
+
One map and one recovery text. Every "this needs the X extra" message in the package
|
|
4
|
+
comes from here, because the right command depends on how the software was installed
|
|
5
|
+
and the wrong one is a dead end: ``uv sync`` needs a checkout an installed reader does
|
|
6
|
+
not have, and ``uv tool install --force 'pkg[X]'`` *replaces* the tool environment, so
|
|
7
|
+
on its own it removes every other extra and every ``--with`` package the reader had
|
|
8
|
+
added.
|
|
9
|
+
|
|
10
|
+
Depends only on :mod:`quantred.settings` and the standard library, so any layer can
|
|
11
|
+
call it - the optimizer and allocator report a missing dependency from below the
|
|
12
|
+
services layer.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import importlib.util
|
|
18
|
+
import shlex
|
|
19
|
+
import sys
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class Extra:
|
|
27
|
+
"""One published extra."""
|
|
28
|
+
|
|
29
|
+
name: str #: the extra's name in ``pyproject.toml``
|
|
30
|
+
modules: Tuple[str, ...] #: every import it provides - all must be present
|
|
31
|
+
unlocks: str #: one line on what the reader gets
|
|
32
|
+
required_for: Tuple[str, ...] = () #: tasks that cannot run without it; () = optional everywhere
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
EXTRAS: Dict[str, Extra] = {
|
|
36
|
+
e.name: e
|
|
37
|
+
for e in (
|
|
38
|
+
Extra(
|
|
39
|
+
"store",
|
|
40
|
+
("pyarrow", "polars", "duckdb"),
|
|
41
|
+
"the bar cache (`--cache`/`--offline`) and the Parquet bar store",
|
|
42
|
+
),
|
|
43
|
+
Extra(
|
|
44
|
+
"viz", ("matplotlib",), "charts in `--chart` and in HTML reports (reports fall back to tables)"
|
|
45
|
+
),
|
|
46
|
+
Extra("mcp", ("mcp",), "the MCP server for AI clients", ("mcp",)),
|
|
47
|
+
Extra("optimize", ("sklearn",), "`optimize --method bayesian`"),
|
|
48
|
+
Extra("portfolio", ("ortools",), "`allocate` (the constraint solver)"),
|
|
49
|
+
Extra("ai", ("anthropic",), "`research --provider anthropic`"),
|
|
50
|
+
Extra("openai", ("openai",), "`research --provider openai`"),
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def installed(extra: str) -> bool:
|
|
56
|
+
"""Whether every module the extra provides can be imported here.
|
|
57
|
+
|
|
58
|
+
All of them, not the first: ``store`` is three packages, and having one of the
|
|
59
|
+
three leaves the Parquet store or the lazy compute path failing at first use.
|
|
60
|
+
"""
|
|
61
|
+
return all(importlib.util.find_spec(module) is not None for module in EXTRAS[extra].modules)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def installed_extras() -> List[str]:
|
|
65
|
+
"""Names of the extras fully satisfied in this environment, sorted."""
|
|
66
|
+
return sorted(name for name in EXTRAS if installed(name))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _in_uv_tool_env() -> bool:
|
|
70
|
+
"""Whether this interpreter lives in a ``uv tool`` environment (a heuristic).
|
|
71
|
+
|
|
72
|
+
The installer cannot be read off the running process, so this looks at where the
|
|
73
|
+
environment sits: under ``UV_TOOL_DIR`` when set, otherwise in a ``uv/tools``
|
|
74
|
+
directory (the default on every platform uv supports).
|
|
75
|
+
"""
|
|
76
|
+
import os
|
|
77
|
+
|
|
78
|
+
prefix = Path(sys.prefix)
|
|
79
|
+
override = os.environ.get("UV_TOOL_DIR")
|
|
80
|
+
if override:
|
|
81
|
+
try:
|
|
82
|
+
prefix.resolve().relative_to(Path(override).resolve())
|
|
83
|
+
return True
|
|
84
|
+
except ValueError:
|
|
85
|
+
pass
|
|
86
|
+
parts = [p.lower() for p in prefix.parts]
|
|
87
|
+
return any(a == "uv" and b == "tools" for a, b in zip(parts, parts[1:]))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _pack_distributions() -> List[str]:
|
|
91
|
+
"""Distributions, other than this one, that register strategies or scanners.
|
|
92
|
+
|
|
93
|
+
These are what a reader installed with ``--with`` and what a rebuilt tool
|
|
94
|
+
environment will not have unless they are named again. Read from the running
|
|
95
|
+
environment's entry points, so nothing about anyone's packs is written down here.
|
|
96
|
+
"""
|
|
97
|
+
from importlib import metadata
|
|
98
|
+
|
|
99
|
+
from quantred.settings import (
|
|
100
|
+
SCANNER_ENTRY_POINT_GROUP,
|
|
101
|
+
STRATEGY_ENTRY_POINT_GROUP,
|
|
102
|
+
entry_point_groups,
|
|
103
|
+
is_own_distribution,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
names = set()
|
|
107
|
+
try:
|
|
108
|
+
entry_points = metadata.entry_points()
|
|
109
|
+
for group in (
|
|
110
|
+
*entry_point_groups(STRATEGY_ENTRY_POINT_GROUP),
|
|
111
|
+
*entry_point_groups(SCANNER_ENTRY_POINT_GROUP),
|
|
112
|
+
):
|
|
113
|
+
if hasattr(entry_points, "select"):
|
|
114
|
+
selected = list(entry_points.select(group=group))
|
|
115
|
+
else: # pragma: no cover - the dict-shaped API of older Pythons
|
|
116
|
+
selected = list(entry_points.get(group, ()))
|
|
117
|
+
for entry_point in selected:
|
|
118
|
+
dist = getattr(entry_point, "dist", None)
|
|
119
|
+
name = getattr(dist, "name", None) if dist is not None else None
|
|
120
|
+
if name and not is_own_distribution(name):
|
|
121
|
+
names.add(name)
|
|
122
|
+
except Exception: # noqa: BLE001 - advice about installing must not fail over its own lookup
|
|
123
|
+
return []
|
|
124
|
+
return sorted(names)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _tool_receipt() -> Optional[Dict[str, Any]]:
|
|
128
|
+
"""The ``uv tool`` receipt of this environment, or ``None``.
|
|
129
|
+
|
|
130
|
+
``uv tool install`` writes ``uv-receipt.toml`` at the root of the tool environment:
|
|
131
|
+
exactly the requirements, extras and ``--with`` entries it was built from. That is
|
|
132
|
+
what a rebuild has to restate, so reading it beats guessing from the packages that
|
|
133
|
+
happen to be importable. Absent for any other installer, and unreadable on a Python
|
|
134
|
+
without :mod:`tomllib` (3.10) - in both cases the caller falls back to advice.
|
|
135
|
+
"""
|
|
136
|
+
receipt = Path(sys.prefix) / "uv-receipt.toml"
|
|
137
|
+
try:
|
|
138
|
+
import tomllib
|
|
139
|
+
|
|
140
|
+
data = tomllib.loads(receipt.read_text(encoding="utf-8"))
|
|
141
|
+
except (ImportError, OSError, ValueError):
|
|
142
|
+
return None
|
|
143
|
+
return data if isinstance(data.get("tool"), dict) else None
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _quote(text: str) -> str:
|
|
147
|
+
return shlex.quote(text)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _with_flags(requirements: List[Dict[str, Any]]) -> List[str]:
|
|
151
|
+
"""``--with`` arguments that restate every requirement but this distribution."""
|
|
152
|
+
from quantred.settings import is_own_distribution
|
|
153
|
+
|
|
154
|
+
flags: List[str] = []
|
|
155
|
+
for req in requirements:
|
|
156
|
+
name = str(req.get("name", ""))
|
|
157
|
+
if not name or is_own_distribution(name):
|
|
158
|
+
continue
|
|
159
|
+
extras_part = f"[{','.join(req['extras'])}]" if req.get("extras") else ""
|
|
160
|
+
if req.get("editable"):
|
|
161
|
+
flags.append(f"--with-editable {_quote(str(req['editable']))}")
|
|
162
|
+
elif req.get("directory") or req.get("path"):
|
|
163
|
+
flags.append(f"--with {_quote(str(req.get('directory') or req['path']))}")
|
|
164
|
+
elif req.get("git"):
|
|
165
|
+
flags.append(f"--with {_quote(f'{name}{extras_part} @ git+' + str(req['git']))}")
|
|
166
|
+
elif req.get("url"):
|
|
167
|
+
flags.append(f"--with {_quote(f'{name}{extras_part} @ ' + str(req['url']))}")
|
|
168
|
+
else:
|
|
169
|
+
flags.append(f"--with {_quote(name + extras_part + str(req.get('specifier', '')))}")
|
|
170
|
+
return flags
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
#: What the printed ``uv tool install`` command calls the exclusions file it asks for.
|
|
174
|
+
EXCLUDES_FILE = "excludes.txt"
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _uv_tool_command(wanted: List[str], extra_flags: Tuple[str, ...] = ()):
|
|
178
|
+
"""``(command, receipt, note)`` that rebuilds this tool environment with ``wanted``.
|
|
179
|
+
|
|
180
|
+
With a receipt the command restates every extra and ``--with`` package the tool was
|
|
181
|
+
built from, including any ``--excludes``. Without one it cannot, and ``note`` says
|
|
182
|
+
what to add by hand.
|
|
183
|
+
"""
|
|
184
|
+
import quantred
|
|
185
|
+
from quantred.settings import (
|
|
186
|
+
DISTRIBUTION_NAME,
|
|
187
|
+
LEGACY_DISTRIBUTION_NAMES,
|
|
188
|
+
invoked_legacy_name,
|
|
189
|
+
is_own_distribution,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
receipt = _tool_receipt()
|
|
193
|
+
requirements = list(((receipt or {}).get("tool") or {}).get("requirements") or [])
|
|
194
|
+
recorded: set = set()
|
|
195
|
+
installed_as = None
|
|
196
|
+
for req in requirements:
|
|
197
|
+
name = str(req.get("name", ""))
|
|
198
|
+
if is_own_distribution(name):
|
|
199
|
+
recorded.update(req.get("extras") or ())
|
|
200
|
+
installed_as = installed_as or name.lower().replace("_", "-")
|
|
201
|
+
if installed_as is None and invoked_legacy_name():
|
|
202
|
+
installed_as = LEGACY_DISTRIBUTION_NAMES[0]
|
|
203
|
+
union = sorted({*installed_extras(), *recorded, *wanted})
|
|
204
|
+
spec = f"[{','.join(union)}]"
|
|
205
|
+
if installed_as in LEGACY_DISTRIBUTION_NAMES:
|
|
206
|
+
# The tool that exists is the one installed under the old name; installing
|
|
207
|
+
# `quantred` would build a second tool and leave this one without the extra.
|
|
208
|
+
# That release forwards every extra to quantred and requires a current one, so
|
|
209
|
+
# its own version is not the running engine's and is not pinned to it.
|
|
210
|
+
command = f"uv tool install --force '{installed_as}{spec}>=2.3.1'"
|
|
211
|
+
else:
|
|
212
|
+
command = f"uv tool install --force '{DISTRIBUTION_NAME}{spec}=={quantred.__version__}'"
|
|
213
|
+
note = None
|
|
214
|
+
if receipt is not None:
|
|
215
|
+
excludes = [str(name) for name in ((receipt.get("tool") or {}).get("excludes") or [])]
|
|
216
|
+
flags = [*_with_flags(requirements), *extra_flags]
|
|
217
|
+
prefix = ""
|
|
218
|
+
if excludes:
|
|
219
|
+
# Restated for the same reason as the extras: the tool environment is rebuilt
|
|
220
|
+
# and would otherwise lose a choice the reader made. It is their choice, not
|
|
221
|
+
# ours, so it is carried and then described. uv takes exclusions from a file,
|
|
222
|
+
# not inline, so the command creates the file first - pasted from anywhere.
|
|
223
|
+
flags.append(f"--excludes {EXCLUDES_FILE}")
|
|
224
|
+
names = " ".join(_quote(name) for name in excludes)
|
|
225
|
+
prefix = f"printf '%s\\n' {names} > {EXCLUDES_FILE} && "
|
|
226
|
+
if any(name.lower().replace("_", "-") in LEGACY_DISTRIBUTION_NAMES for name in excludes):
|
|
227
|
+
note = (
|
|
228
|
+
"(this keeps your exclusion of the old tradeflow-engine; a pack that still imports "
|
|
229
|
+
"`tradeflow` needs it installed, so drop that exclusion for such a pack)"
|
|
230
|
+
)
|
|
231
|
+
command = prefix + " ".join([command, *flags])
|
|
232
|
+
else:
|
|
233
|
+
packs = _pack_distributions()
|
|
234
|
+
# Silent when nothing was detected outside a uv tool environment: "re-add your
|
|
235
|
+
# --with packs" is advice about a mechanism that venv does not have.
|
|
236
|
+
if packs:
|
|
237
|
+
note = f"(re-add any packs you installed with --with: {', '.join(packs)})"
|
|
238
|
+
elif _in_uv_tool_env():
|
|
239
|
+
note = "(re-add any packs you installed with --with: none detected)"
|
|
240
|
+
return command, receipt, note
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
#: Said under a pack install command. The old distribution's final release is a thin
|
|
244
|
+
#: alias, so keeping it is harmless, and a pack nobody can edit still imports `tradeflow`.
|
|
245
|
+
_OLD_ENGINE_NOTE = (
|
|
246
|
+
"Keep tradeflow-engine>=2.3.1 installed: a pack that still imports `tradeflow` works through "
|
|
247
|
+
"the alias it provides, and without it that pack is dropped. If the pack pins an older "
|
|
248
|
+
"tradeflow-engine, override the pin rather than excluding it (a pin below 2.3.1 would install "
|
|
249
|
+
"the whole previous engine, whose classes this one rejects). Excluding it, or {skip}, is only "
|
|
250
|
+
"for a pack that never imports `tradeflow`; {skip} also skips the pack's other dependencies."
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def pack_install_hint(path: str) -> str:
|
|
255
|
+
"""How to install a pack from a directory, without removing anything already there.
|
|
256
|
+
|
|
257
|
+
A checkout installs it into its own environment (additive). A ``uv tool`` copy has
|
|
258
|
+
to rebuild the tool environment, so the command carries everything it already had
|
|
259
|
+
(including any exclusion the reader chose, created first so the line runs anywhere).
|
|
260
|
+
Elsewhere it is pip. Each is followed by what to do about a pack that still depends
|
|
261
|
+
on the old distribution name.
|
|
262
|
+
"""
|
|
263
|
+
from quantred.settings import running_from_checkout
|
|
264
|
+
|
|
265
|
+
quoted = shlex.quote(path)
|
|
266
|
+
if running_from_checkout():
|
|
267
|
+
command = f"uv pip install -e {quoted}"
|
|
268
|
+
else:
|
|
269
|
+
command, receipt, note = _uv_tool_command([], (f"--with-editable {quoted}",))
|
|
270
|
+
if receipt is not None:
|
|
271
|
+
return f"{command}\n{note}" if note else command
|
|
272
|
+
command = f"python -m pip install -e {quoted}"
|
|
273
|
+
return f"{command}\n({_OLD_ENGINE_NOTE.format(skip='--no-deps')})"
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def install_hint(*extras: str) -> str:
|
|
277
|
+
"""The recovery text for adding ``extras``, phrased for how this copy was reached.
|
|
278
|
+
|
|
279
|
+
A checkout gets one ``uv sync --inexact`` line: plain ``uv sync`` is *exact* and
|
|
280
|
+
uninstalls whatever the lock does not name for the extras given - the ``dev`` tools
|
|
281
|
+
and any pack installed with ``uv pip install -e`` - while ``--inexact`` only adds.
|
|
282
|
+
|
|
283
|
+
An installed copy gets both ways to add an extra, because which installer made the
|
|
284
|
+
environment cannot be known for certain. The ``uv tool`` form replaces the whole
|
|
285
|
+
environment, so it restates the extras and ``--with`` packs the tool was built from
|
|
286
|
+
(read from its receipt) and is pinned to the running version, since adding a
|
|
287
|
+
capability is not an upgrade.
|
|
288
|
+
"""
|
|
289
|
+
import quantred
|
|
290
|
+
from quantred.settings import DISTRIBUTION_NAME, DOCS_URL, running_from_checkout
|
|
291
|
+
|
|
292
|
+
wanted = sorted(set(extras))
|
|
293
|
+
|
|
294
|
+
if running_from_checkout():
|
|
295
|
+
return " uv sync --inexact " + " ".join(f"--extra {name}" for name in wanted)
|
|
296
|
+
|
|
297
|
+
version = quantred.__version__
|
|
298
|
+
uv_form, receipt, note = _uv_tool_command(wanted)
|
|
299
|
+
pip_form = f"python -m pip install '{DISTRIBUTION_NAME}[{','.join(wanted)}]=={version}'"
|
|
300
|
+
|
|
301
|
+
detected = receipt is not None or _in_uv_tool_env()
|
|
302
|
+
uv_label = "With uv tool (detected):" if detected else "With uv tool:"
|
|
303
|
+
pip_label = "In an activated venv:"
|
|
304
|
+
width = max(len(uv_label), len(pip_label)) + 1
|
|
305
|
+
lines = [f" {uv_label:<{width}}{uv_form}"]
|
|
306
|
+
if note:
|
|
307
|
+
lines.append(f" {'':<{width}}{note}")
|
|
308
|
+
lines.append(f" {pip_label:<{width}}{pip_form}")
|
|
309
|
+
lines.append(f" Extras guide: {DOCS_URL}/usage/extras")
|
|
310
|
+
return "\n".join(lines)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def missing_extra_message(what: str, extra: str) -> str:
|
|
314
|
+
"""``what`` needs ``extra``, and the one way to get it."""
|
|
315
|
+
return f"{what} needs the '{extra}' extra.\n{install_hint(extra)}"
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
class MissingExtra(RuntimeError):
|
|
319
|
+
"""A capability was reached without the extra that provides it.
|
|
320
|
+
|
|
321
|
+
Raised where the capability starts rather than where its first import happens to
|
|
322
|
+
sit, and carrying the install text, so every caller ends on the same instruction -
|
|
323
|
+
the CLI prints it, the MCP server returns it as the tool error.
|
|
324
|
+
"""
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def require_extra(what: str, extra: str) -> None:
|
|
328
|
+
"""Raise :class:`MissingExtra` unless ``extra`` is installed."""
|
|
329
|
+
if not installed(extra):
|
|
330
|
+
raise MissingExtra(missing_extra_message(what, extra))
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Centralized logging configuration.
|
|
2
|
+
|
|
3
|
+
A single ``setup_logging`` entry point keeps log formatting consistent across
|
|
4
|
+
every module (engine, scanner, strategies) without each one re-running
|
|
5
|
+
``logging.basicConfig`` and clobbering the others.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
_CONFIGURED = False
|
|
11
|
+
|
|
12
|
+
# An aligned, greppable, pipe-delimited line:
|
|
13
|
+
# 2026-06-19 14:03:22 | INFO | quantred.engine.backtest | message
|
|
14
|
+
# The fixed-width level and logger columns keep multi-line runs easy to scan.
|
|
15
|
+
_LOG_FORMAT = "%(asctime)s | %(levelname)-8s | %(name)-24s | %(message)s"
|
|
16
|
+
_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def setup_logging(level: int = logging.INFO) -> None:
|
|
20
|
+
"""Configure root logging once per process.
|
|
21
|
+
|
|
22
|
+
Safe to call from module import time in many modules; only the first call
|
|
23
|
+
actually configures handlers.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
level: Minimum log level for the root logger.
|
|
27
|
+
"""
|
|
28
|
+
global _CONFIGURED
|
|
29
|
+
if _CONFIGURED:
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
logging.basicConfig(level=level, format=_LOG_FORMAT, datefmt=_DATE_FORMAT)
|
|
33
|
+
_CONFIGURED = True
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Small numeric helpers shared across layers.
|
|
2
|
+
|
|
3
|
+
Centralized so price/quantity rounding and step math are defined once and reused
|
|
4
|
+
by execution, the optimizer, and reporting - rather than re-derived inline.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import math
|
|
8
|
+
from typing import Union
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def round_price(price: float, places: int = 2) -> float:
|
|
12
|
+
"""Round a price to a tick size.
|
|
13
|
+
|
|
14
|
+
Sub-dollar instruments are quoted to more decimals, so prices under $1 are
|
|
15
|
+
rounded to 4 places by default.
|
|
16
|
+
"""
|
|
17
|
+
if price < 1.0:
|
|
18
|
+
places = max(places, 4)
|
|
19
|
+
return round(float(price), places)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def round_quantity(qty: float, allow_fractional: bool = False) -> float:
|
|
23
|
+
"""Round an order quantity, flooring to whole shares unless fractional is allowed."""
|
|
24
|
+
if allow_fractional:
|
|
25
|
+
return float(qty)
|
|
26
|
+
return float(math.floor(qty))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def safe_float(value, default: float = 0.0) -> float:
|
|
30
|
+
"""Best-effort float conversion that never raises."""
|
|
31
|
+
try:
|
|
32
|
+
return float(value)
|
|
33
|
+
except (TypeError, ValueError):
|
|
34
|
+
return default
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def step_decimals(step: Union[int, float]) -> int:
|
|
38
|
+
"""Number of decimal places implied by a step size (capped at 6).
|
|
39
|
+
|
|
40
|
+
e.g. ``0.05 -> 2``, ``1 -> 0``. Used by the optimizer to keep generated
|
|
41
|
+
parameter values aligned to their declared grid.
|
|
42
|
+
"""
|
|
43
|
+
text = str(step)
|
|
44
|
+
if "." not in text:
|
|
45
|
+
return 0
|
|
46
|
+
return min(len(text.split(".")[-1]), 6)
|