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
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""Bar-quality guards for the live loop.
|
|
2
|
+
|
|
3
|
+
The live path hands whatever the vendor sends straight to the strategy. A stale
|
|
4
|
+
repeat, a bad tick, a bar for a halted symbol, or one arriving out of order all
|
|
5
|
+
become signals with nothing to veto them — and unlike a backtest, there is no
|
|
6
|
+
opportunity to notice afterwards.
|
|
7
|
+
|
|
8
|
+
Two rules govern everything here:
|
|
9
|
+
|
|
10
|
+
**Reject, never repair.** No guard modifies a bar, fills a gap, or interpolates a
|
|
11
|
+
missing value. The moment the live path "fixes" its inputs it stops being the thing
|
|
12
|
+
the backtest validated, and every historical result quietly stops describing what
|
|
13
|
+
will happen. A rejected bar is skipped and logged; the strategy simply does not see
|
|
14
|
+
it.
|
|
15
|
+
|
|
16
|
+
**A guard that rejects real data is worse than no guard.** Every veto discards
|
|
17
|
+
information the strategy was entitled to act on, and a threshold tight enough to
|
|
18
|
+
catch every bad tick will also catch the genuine moves that make money. Defaults are
|
|
19
|
+
deliberately loose: these exist to catch a corrupt *tick*, not a violent *day*.
|
|
20
|
+
|
|
21
|
+
Pure functions over ``(previous accepted bar, candidate) -> BarVerdict``, with no I/O
|
|
22
|
+
and no state beyond the last accepted bar per symbol — so the whole thing is
|
|
23
|
+
testable without a broker, a network, or a clock.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import logging
|
|
27
|
+
from dataclasses import dataclass, field
|
|
28
|
+
from datetime import datetime, timedelta, timezone
|
|
29
|
+
from typing import Dict, Optional
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class BarVerdict:
|
|
36
|
+
"""Whether a bar may reach the strategy, and why not when it may not."""
|
|
37
|
+
|
|
38
|
+
accepted: bool
|
|
39
|
+
reason: Optional[str] = None
|
|
40
|
+
detail: Optional[str] = None
|
|
41
|
+
|
|
42
|
+
def __bool__(self) -> bool:
|
|
43
|
+
return self.accepted
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
ACCEPTED = BarVerdict(True)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class BarChecks:
|
|
51
|
+
"""Thresholds for the guards. Every check defaults on; each can be disabled.
|
|
52
|
+
|
|
53
|
+
``max_return`` is the only real judgment call. At 0.35 it will not fire on any
|
|
54
|
+
ordinary session — a stock genuinely moving 35% between consecutive bars is
|
|
55
|
+
news, and the strategy should see it. It fires on the decimal-point errors and
|
|
56
|
+
crossed quotes that a feed occasionally emits.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
#: Reject a single-bar return larger than this in magnitude (fraction, not %).
|
|
60
|
+
max_return: float = 0.35
|
|
61
|
+
#: Reject a bar whose timestamp is older than this many intervals behind now.
|
|
62
|
+
max_staleness_intervals: float = 3.0
|
|
63
|
+
#: Reject a zero-volume bar once a symbol has been seen trading.
|
|
64
|
+
check_zero_volume: bool = True
|
|
65
|
+
#: Reject a bar at or before the last accepted timestamp for that symbol.
|
|
66
|
+
check_monotonic: bool = True
|
|
67
|
+
#: Reject internally inconsistent OHLC (high < low, close outside the range, ...).
|
|
68
|
+
check_ohlc: bool = True
|
|
69
|
+
#: Reject bars that arrive too late to be actionable.
|
|
70
|
+
check_staleness: bool = True
|
|
71
|
+
#: Reject implausible single-bar moves.
|
|
72
|
+
check_spike: bool = True
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class _SymbolState:
|
|
77
|
+
"""What the guards remember about one symbol: only what they need."""
|
|
78
|
+
|
|
79
|
+
last_timestamp: Optional[datetime] = None
|
|
80
|
+
last_close: Optional[float] = None
|
|
81
|
+
has_traded: bool = False
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass
|
|
85
|
+
class BarQualityFilter:
|
|
86
|
+
"""Applies the guards to a live bar stream, and counts what it rejected.
|
|
87
|
+
|
|
88
|
+
The counts matter as much as the filtering. A guard quietly discarding a third
|
|
89
|
+
of the feed looks identical, from the strategy's side, to a quiet market — so
|
|
90
|
+
the rejection rate is tracked and reported rather than left to be inferred.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
checks: BarChecks = field(default_factory=BarChecks)
|
|
94
|
+
interval: timedelta = timedelta(minutes=5)
|
|
95
|
+
#: Rejections per reason, and the total seen — the visibility half of the job.
|
|
96
|
+
rejected: Dict[str, int] = field(default_factory=dict)
|
|
97
|
+
seen: int = 0
|
|
98
|
+
_state: Dict[str, _SymbolState] = field(default_factory=dict)
|
|
99
|
+
|
|
100
|
+
def check(self, symbol: str, bar: Dict[str, float], timestamp: datetime, *, now=None) -> BarVerdict:
|
|
101
|
+
"""Whether this bar may reach the strategy.
|
|
102
|
+
|
|
103
|
+
``now`` is injectable so the staleness check is testable without freezing the
|
|
104
|
+
clock. Accepting a bar updates this symbol's state; rejecting one does not —
|
|
105
|
+
a bad bar must not become the baseline the next bar is judged against.
|
|
106
|
+
"""
|
|
107
|
+
self.seen += 1
|
|
108
|
+
state = self._state.setdefault(symbol, _SymbolState())
|
|
109
|
+
verdict = self._evaluate(state, bar, timestamp, now=now)
|
|
110
|
+
|
|
111
|
+
if not verdict.accepted:
|
|
112
|
+
self.rejected[verdict.reason] = self.rejected.get(verdict.reason, 0) + 1
|
|
113
|
+
logger.warning(
|
|
114
|
+
"Rejected %s bar at %s: %s (%s)", symbol, timestamp, verdict.reason, verdict.detail
|
|
115
|
+
)
|
|
116
|
+
return verdict
|
|
117
|
+
|
|
118
|
+
state.last_timestamp = timestamp
|
|
119
|
+
state.last_close = float(bar["close"])
|
|
120
|
+
if float(bar.get("volume") or 0.0) > 0:
|
|
121
|
+
state.has_traded = True
|
|
122
|
+
return verdict
|
|
123
|
+
|
|
124
|
+
def _evaluate(self, state: _SymbolState, bar, timestamp, *, now) -> BarVerdict:
|
|
125
|
+
checks = self.checks
|
|
126
|
+
|
|
127
|
+
if checks.check_ohlc:
|
|
128
|
+
verdict = _ohlc_verdict(bar)
|
|
129
|
+
if not verdict.accepted:
|
|
130
|
+
return verdict
|
|
131
|
+
|
|
132
|
+
if checks.check_monotonic and state.last_timestamp is not None:
|
|
133
|
+
if _as_utc(timestamp) <= _as_utc(state.last_timestamp):
|
|
134
|
+
return BarVerdict(
|
|
135
|
+
False,
|
|
136
|
+
"out_of_order",
|
|
137
|
+
f"timestamp {timestamp} is not after the last accepted {state.last_timestamp}",
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
if checks.check_staleness:
|
|
141
|
+
# Compared like-for-like: the reference is built in the bar's own
|
|
142
|
+
# awareness rather than normalized to UTC. Assuming a naive timestamp is
|
|
143
|
+
# UTC would make every bar from a vendor that serializes local time look
|
|
144
|
+
# hours stale, and reject a perfectly good feed outright.
|
|
145
|
+
reference = now if now is not None else _now_like(timestamp)
|
|
146
|
+
age = _as_utc(reference) - _as_utc(timestamp)
|
|
147
|
+
limit = self.interval * checks.max_staleness_intervals
|
|
148
|
+
if age > limit:
|
|
149
|
+
return BarVerdict(False, "stale", f"bar is {age} old, limit {limit}")
|
|
150
|
+
|
|
151
|
+
if checks.check_spike and state.last_close:
|
|
152
|
+
move = abs(float(bar["close"]) / state.last_close - 1.0)
|
|
153
|
+
if move > checks.max_return:
|
|
154
|
+
return BarVerdict(
|
|
155
|
+
False,
|
|
156
|
+
"spike",
|
|
157
|
+
f"single-bar move {move:.1%} exceeds {checks.max_return:.0%} "
|
|
158
|
+
f"({state.last_close} -> {bar['close']})",
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
if checks.check_zero_volume and state.has_traded:
|
|
162
|
+
if float(bar.get("volume") or 0.0) <= 0:
|
|
163
|
+
return BarVerdict(False, "zero_volume", "no volume on a symbol that has traded")
|
|
164
|
+
|
|
165
|
+
return ACCEPTED
|
|
166
|
+
|
|
167
|
+
def report(self) -> Dict[str, object]:
|
|
168
|
+
"""Rejection counts and rate — for logging at shutdown, or on a schedule.
|
|
169
|
+
|
|
170
|
+
``elevated`` is the flag worth acting on: a feed that is mostly being
|
|
171
|
+
discarded is a data problem wearing a quiet market's clothes.
|
|
172
|
+
"""
|
|
173
|
+
total = sum(self.rejected.values())
|
|
174
|
+
rate = (total / self.seen) if self.seen else 0.0
|
|
175
|
+
return {
|
|
176
|
+
"seen": self.seen,
|
|
177
|
+
"rejected": total,
|
|
178
|
+
"rate": rate,
|
|
179
|
+
"by_reason": dict(self.rejected),
|
|
180
|
+
"elevated": rate > 0.05 and total > 5,
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _ohlc_verdict(bar: Dict[str, float]) -> BarVerdict:
|
|
185
|
+
"""Internal consistency: the cheapest check and the one that catches corruption.
|
|
186
|
+
|
|
187
|
+
A bar failing this is not a market event; it is a broken message.
|
|
188
|
+
"""
|
|
189
|
+
try:
|
|
190
|
+
o, h, low, c = (float(bar["open"]), float(bar["high"]), float(bar["low"]), float(bar["close"]))
|
|
191
|
+
except (KeyError, TypeError, ValueError):
|
|
192
|
+
return BarVerdict(False, "malformed", "bar is missing an OHLC field or it is not a number")
|
|
193
|
+
|
|
194
|
+
if any(price <= 0 for price in (o, h, low, c)):
|
|
195
|
+
return BarVerdict(False, "non_positive", f"non-positive price in OHLC ({o}, {h}, {low}, {c})")
|
|
196
|
+
if h < low:
|
|
197
|
+
return BarVerdict(False, "inverted_range", f"high {h} below low {low}")
|
|
198
|
+
if not (low <= c <= h) or not (low <= o <= h):
|
|
199
|
+
return BarVerdict(False, "outside_range", f"open/close outside [{low}, {h}]")
|
|
200
|
+
volume = bar.get("volume")
|
|
201
|
+
if volume is not None and float(volume) < 0:
|
|
202
|
+
return BarVerdict(False, "negative_volume", f"volume {volume}")
|
|
203
|
+
return ACCEPTED
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _now_like(timestamp: datetime) -> datetime:
|
|
207
|
+
"""``now`` in the same awareness as the bar, so the two are comparable.
|
|
208
|
+
|
|
209
|
+
Staleness is a *duration*, so both sides must mean the same thing. A vendor
|
|
210
|
+
serializing local naive timestamps is compared against local now; one sending
|
|
211
|
+
aware timestamps against UTC now. Normalizing a naive timestamp to UTC instead
|
|
212
|
+
would make an entire well-behaved feed look hours stale.
|
|
213
|
+
"""
|
|
214
|
+
return datetime.now() if timestamp.tzinfo is None else datetime.now(timezone.utc)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _as_utc(timestamp: datetime) -> datetime:
|
|
218
|
+
"""A timestamp as tz-aware UTC, for *relative* comparison only.
|
|
219
|
+
|
|
220
|
+
Ordering and duration checks go through this so a feed that switches awareness
|
|
221
|
+
mid-stream — naive on one bar, aware on the next — produces a verdict rather
|
|
222
|
+
than a ``TypeError`` from inside the order path. The assumption is only ever
|
|
223
|
+
used to compare two values, never to decide what time it is.
|
|
224
|
+
|
|
225
|
+
The bar and the timestamp handed to the strategy are untouched: this converts a
|
|
226
|
+
comparison key, never an input.
|
|
227
|
+
"""
|
|
228
|
+
return timestamp.replace(tzinfo=timezone.utc) if timestamp.tzinfo is None else timestamp
|