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.
Files changed (117) hide show
  1. quantred/__init__.py +24 -0
  2. quantred/alphas/__init__.py +53 -0
  3. quantred/alphas/base.py +221 -0
  4. quantred/alphas/combine.py +260 -0
  5. quantred/alphas/horizon.py +153 -0
  6. quantred/alphas/refine.py +300 -0
  7. quantred/alphas/scorers.py +87 -0
  8. quantred/analytics/attribution.py +354 -0
  9. quantred/analytics/bootstrap.py +353 -0
  10. quantred/analytics/charts.py +393 -0
  11. quantred/analytics/excursion.py +185 -0
  12. quantred/analytics/execution_quality.py +180 -0
  13. quantred/analytics/exposure.py +167 -0
  14. quantred/analytics/htmlreport.py +1208 -0
  15. quantred/analytics/information.py +197 -0
  16. quantred/analytics/metrics.py +502 -0
  17. quantred/analytics/performance.py +639 -0
  18. quantred/analytics/reporting.py +1672 -0
  19. quantred/analytics/screening.py +190 -0
  20. quantred/analytics/series_comparison.py +303 -0
  21. quantred/analytics/trade_analytics.py +353 -0
  22. quantred/brokers/alpaca/broker.py +415 -0
  23. quantred/brokers/alpaca/factory.py +33 -0
  24. quantred/brokers/alpaca/market_data.py +166 -0
  25. quantred/brokers/base.py +220 -0
  26. quantred/brokers/dryrun.py +164 -0
  27. quantred/brokers/errors.py +70 -0
  28. quantred/cli.py +8619 -0
  29. quantred/costs/__init__.py +11 -0
  30. quantred/costs/base.py +126 -0
  31. quantred/costs/parametric.py +131 -0
  32. quantred/data/__init__.py +29 -0
  33. quantred/data/compute.py +332 -0
  34. quantred/data/edges.py +81 -0
  35. quantred/data/features.py +120 -0
  36. quantred/data/panel.py +61 -0
  37. quantred/data/scan.py +85 -0
  38. quantred/data/store.py +269 -0
  39. quantred/demo/__init__.py +22 -0
  40. quantred/demo/scanners.py +104 -0
  41. quantred/demo/strategies.py +125 -0
  42. quantred/engine/backtest.py +1206 -0
  43. quantred/engine/barcheck.py +228 -0
  44. quantred/engine/live.py +539 -0
  45. quantred/execution/decision.py +192 -0
  46. quantred/execution/flatten.py +302 -0
  47. quantred/execution/halt.py +165 -0
  48. quantred/execution/ledger.py +758 -0
  49. quantred/execution/live_trader.py +587 -0
  50. quantred/execution/order_id.py +83 -0
  51. quantred/execution/sizing.py +114 -0
  52. quantred/indicators/indicators.py +158 -0
  53. quantred/marketdata/base.py +62 -0
  54. quantred/marketdata/client.py +59 -0
  55. quantred/marketdata/session.py +112 -0
  56. quantred/marketdata/synthetic.py +74 -0
  57. quantred/marketdata/timeframe.py +125 -0
  58. quantred/mcp/__init__.py +6 -0
  59. quantred/mcp/selftest.py +139 -0
  60. quantred/mcp/server.py +2419 -0
  61. quantred/optimization/causality.py +607 -0
  62. quantred/optimization/config_store.py +300 -0
  63. quantred/optimization/optimizer.py +431 -0
  64. quantred/optimization/parallel.py +345 -0
  65. quantred/optimization/param_space.py +309 -0
  66. quantred/optimization/walk_forward.py +1254 -0
  67. quantred/portfolio/allocator.py +135 -0
  68. quantred/portfolio/benchmark.py +109 -0
  69. quantred/portfolio/optimizer.py +786 -0
  70. quantred/portfolio/policy.py +335 -0
  71. quantred/portfolio/posterior.py +208 -0
  72. quantred/research/__init__.py +30 -0
  73. quantred/research/agent.py +868 -0
  74. quantred/research/demo_proposals.py +222 -0
  75. quantred/research/llm.py +311 -0
  76. quantred/research/proposer.py +234 -0
  77. quantred/research/provider_errors.py +242 -0
  78. quantred/research/sandbox.py +340 -0
  79. quantred/risk/__init__.py +60 -0
  80. quantred/risk/base.py +218 -0
  81. quantred/risk/conditional.py +474 -0
  82. quantred/risk/exposures.py +88 -0
  83. quantred/risk/factor.py +119 -0
  84. quantred/risk/sample.py +89 -0
  85. quantred/risk/streaming.py +214 -0
  86. quantred/scanners/base.py +141 -0
  87. quantred/scanners/symbol_scanner.py +148 -0
  88. quantred/services/__init__.py +51 -0
  89. quantred/services/analysis.py +6082 -0
  90. quantred/services/audit.py +574 -0
  91. quantred/services/campaign.py +381 -0
  92. quantred/services/configs.py +282 -0
  93. quantred/services/data.py +172 -0
  94. quantred/services/dryrun.py +206 -0
  95. quantred/services/glossary.py +130 -0
  96. quantred/services/maintenance.py +314 -0
  97. quantred/services/registry.py +351 -0
  98. quantred/services/setup.py +1023 -0
  99. quantred/services/sizing.py +150 -0
  100. quantred/services/smallreal.py +450 -0
  101. quantred/settings.py +571 -0
  102. quantred/store/__init__.py +0 -0
  103. quantred/store/bars.py +660 -0
  104. quantred/store/trials.py +1804 -0
  105. quantred/strategies/base.py +643 -0
  106. quantred/strategies/signals.py +28 -0
  107. quantred/utils/errors.py +48 -0
  108. quantred/utils/extras.py +330 -0
  109. quantred/utils/logging_config.py +33 -0
  110. quantred/utils/numeric.py +46 -0
  111. quantred/utils/streaming.py +474 -0
  112. quantred/utils/timeutils.py +148 -0
  113. quantred-2.4.0.dist-info/METADATA +632 -0
  114. quantred-2.4.0.dist-info/RECORD +117 -0
  115. quantred-2.4.0.dist-info/WHEEL +4 -0
  116. quantred-2.4.0.dist-info/entry_points.txt +8 -0
  117. 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