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,353 @@
|
|
|
1
|
+
"""Reading a recorded run's trade table without opening SQLite.
|
|
2
|
+
|
|
3
|
+
Exit-reason P&L, win and loss by reason, how long positions were held, how far they
|
|
4
|
+
went against the book before they worked — the ordinary questions about a run that
|
|
5
|
+
somebody has already paid for. They were ordinary enough to be asked by hand, in SQL,
|
|
6
|
+
against a schema nobody should have to read.
|
|
7
|
+
|
|
8
|
+
Everything here is a **pure function over a stored table** — ``{columns, rows,
|
|
9
|
+
total_rows, truncated}`` — and returns structured data. Nothing here formats anything,
|
|
10
|
+
grades anything, or decides whether a number is good. The register is the one the
|
|
11
|
+
execution report and the P&L-concentration block already set: report the number, say
|
|
12
|
+
what it does not cover, leave the judgement to somebody who has seen a few.
|
|
13
|
+
|
|
14
|
+
Three absences are kept apart, because collapsing any two produces a confident answer
|
|
15
|
+
to a question that was never asked:
|
|
16
|
+
|
|
17
|
+
* **No table.** The run did not opt into keeping one. Nothing is known about its trades
|
|
18
|
+
and no section is computed.
|
|
19
|
+
* **No trades.** The run opted in and closed nothing. Every section is computed and
|
|
20
|
+
every count is zero — a real, measured answer.
|
|
21
|
+
* **No column.** The table is there but does not carry what a section needs. That
|
|
22
|
+
section says which column it wanted and is silent about the rest, rather than
|
|
23
|
+
reporting a distribution over data it did not have.
|
|
24
|
+
|
|
25
|
+
**A truncated table does not get aggregated.** A trade table capped at the storage
|
|
26
|
+
ceiling holds a prefix of a run's trades, so a total over it is not a smaller number
|
|
27
|
+
than the truth, it is a wrong one — and it looks exactly like a right one. Sums refuse
|
|
28
|
+
by default; ``allow_partial=True`` computes them anyway and the result says, in the
|
|
29
|
+
payload rather than in some caller's formatting, that the numbers cover the stored rows
|
|
30
|
+
only. A table whose completeness was never recorded is refused on the same terms: it
|
|
31
|
+
did not claim to be whole, and treating "unknown" as "yes" is the failure this whole
|
|
32
|
+
path exists to close.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from datetime import datetime
|
|
36
|
+
from typing import Any, Dict, List, Optional, Sequence
|
|
37
|
+
|
|
38
|
+
import numpy as np
|
|
39
|
+
|
|
40
|
+
from quantred.analytics import metrics
|
|
41
|
+
|
|
42
|
+
#: Aggregate status. ``complete`` — the numbers cover every trade the run made.
|
|
43
|
+
#: ``truncated`` — they cover the stored rows only, and the caller asked for that.
|
|
44
|
+
#: ``unavailable`` — there are no numbers, and ``reason`` says why.
|
|
45
|
+
COMPLETE = "complete"
|
|
46
|
+
TRUNCATED = "truncated"
|
|
47
|
+
UNAVAILABLE = "unavailable"
|
|
48
|
+
|
|
49
|
+
#: What the stored table itself is, which is a different question from what the
|
|
50
|
+
#: aggregate over it is worth. ``unknown`` is not a synonym for ``whole``.
|
|
51
|
+
SOURCE_WHOLE = "whole"
|
|
52
|
+
SOURCE_CAPPED = "capped"
|
|
53
|
+
SOURCE_UNKNOWN = "unknown"
|
|
54
|
+
SOURCE_ABSENT = "not recorded"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def describe_source(table: Optional[Dict[str, Any]]) -> Dict[str, Any]:
|
|
58
|
+
"""What the stored table is, before anything is computed from it."""
|
|
59
|
+
if table is None:
|
|
60
|
+
return {
|
|
61
|
+
"recorded": False,
|
|
62
|
+
"completeness": SOURCE_ABSENT,
|
|
63
|
+
"rows_stored": None,
|
|
64
|
+
"total_rows": None,
|
|
65
|
+
"columns": [],
|
|
66
|
+
}
|
|
67
|
+
rows = table.get("rows") or []
|
|
68
|
+
truncated = table.get("truncated")
|
|
69
|
+
completeness = SOURCE_UNKNOWN if truncated is None else (SOURCE_CAPPED if truncated else SOURCE_WHOLE)
|
|
70
|
+
return {
|
|
71
|
+
"recorded": True,
|
|
72
|
+
"completeness": completeness,
|
|
73
|
+
"rows_stored": len(rows),
|
|
74
|
+
"total_rows": table.get("total_rows"),
|
|
75
|
+
"columns": [str(c) for c in (table.get("columns") or [])],
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _columns(table: Dict[str, Any]) -> Dict[str, List[Any]]:
|
|
80
|
+
"""The table transposed into ``{column: values}``.
|
|
81
|
+
|
|
82
|
+
Rows shorter than the header are padded with ``None`` rather than dropped: a
|
|
83
|
+
malformed row is one trade with fields missing, and losing it silently would move
|
|
84
|
+
every count in this module.
|
|
85
|
+
"""
|
|
86
|
+
names = [str(c) for c in (table.get("columns") or [])]
|
|
87
|
+
rows = table.get("rows") or []
|
|
88
|
+
out: Dict[str, List[Any]] = {name: [] for name in names}
|
|
89
|
+
for row in rows:
|
|
90
|
+
for i, name in enumerate(names):
|
|
91
|
+
out[name].append(row[i] if i < len(row) else None)
|
|
92
|
+
return out
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _floats(values: Sequence[Any]) -> "np.ndarray":
|
|
96
|
+
"""A column as floats, with anything unparseable as NaN rather than dropped -
|
|
97
|
+
so the count of what could not be measured survives to be reported."""
|
|
98
|
+
out = np.full(len(values), np.nan, dtype="float64")
|
|
99
|
+
for i, value in enumerate(values):
|
|
100
|
+
try:
|
|
101
|
+
if value is None or isinstance(value, bool):
|
|
102
|
+
continue
|
|
103
|
+
out[i] = float(value)
|
|
104
|
+
except (TypeError, ValueError):
|
|
105
|
+
continue
|
|
106
|
+
return out
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _as_datetime(value: Any) -> Optional[datetime]:
|
|
110
|
+
"""One cell as a datetime, or ``None``. Stored rows carry ISO strings (the journal
|
|
111
|
+
is JSON); a live frame carries timestamps."""
|
|
112
|
+
if value is None:
|
|
113
|
+
return None
|
|
114
|
+
if isinstance(value, datetime):
|
|
115
|
+
return value
|
|
116
|
+
try:
|
|
117
|
+
return datetime.fromisoformat(str(value))
|
|
118
|
+
except (TypeError, ValueError):
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _distribution(values: "np.ndarray", n_total: int) -> Dict[str, Any]:
|
|
123
|
+
"""Median/quartiles/extremes of a measured column, with the unmeasured counted.
|
|
124
|
+
|
|
125
|
+
``n_measured`` leads. A tidy median over three of two hundred values is not a
|
|
126
|
+
description of the run, and the only thing that says so is the count beside it.
|
|
127
|
+
"""
|
|
128
|
+
finite = values[np.isfinite(values)]
|
|
129
|
+
summary: Dict[str, Any] = {
|
|
130
|
+
"n_measured": int(finite.size),
|
|
131
|
+
"n_unmeasured": int(n_total - finite.size),
|
|
132
|
+
}
|
|
133
|
+
if finite.size == 0:
|
|
134
|
+
summary.update({k: None for k in ("median", "mean", "p25", "p75", "p90", "min", "max")})
|
|
135
|
+
return summary
|
|
136
|
+
summary.update(
|
|
137
|
+
{
|
|
138
|
+
"median": float(np.median(finite)),
|
|
139
|
+
"mean": float(np.mean(finite)),
|
|
140
|
+
"p25": float(np.percentile(finite, 25)),
|
|
141
|
+
"p75": float(np.percentile(finite, 75)),
|
|
142
|
+
"p90": float(np.percentile(finite, 90)),
|
|
143
|
+
"min": float(np.min(finite)),
|
|
144
|
+
"max": float(np.max(finite)),
|
|
145
|
+
}
|
|
146
|
+
)
|
|
147
|
+
return summary
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _unavailable(requires: Sequence[str], present: Sequence[str]) -> Dict[str, Any]:
|
|
151
|
+
"""A section that could not be computed, naming the column it wanted.
|
|
152
|
+
|
|
153
|
+
Not an empty distribution. "No trade lasted any time" and "this table does not
|
|
154
|
+
record when trades opened" are different statements, and only one of them is about
|
|
155
|
+
the strategy.
|
|
156
|
+
"""
|
|
157
|
+
missing = [c for c in requires if c not in present]
|
|
158
|
+
return {
|
|
159
|
+
"available": False,
|
|
160
|
+
"requires": list(requires),
|
|
161
|
+
"missing_columns": missing,
|
|
162
|
+
"note": f"not computed: the stored table has no {', '.join(missing)} column",
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _win_loss_over(pnl: "np.ndarray") -> Dict[str, Any]:
|
|
167
|
+
"""Wins, losses, and the ratios between them for one set of trades.
|
|
168
|
+
|
|
169
|
+
Rates are ``None`` with nothing to average rather than ``0.0``: a set with no
|
|
170
|
+
losing trades has no average loss, and zero is a measurement.
|
|
171
|
+
"""
|
|
172
|
+
finite = pnl[np.isfinite(pnl)]
|
|
173
|
+
wins, losses = finite[finite > 0], finite[finite < 0]
|
|
174
|
+
return {
|
|
175
|
+
"trades": int(finite.size),
|
|
176
|
+
"unmeasured": int(pnl.size - finite.size),
|
|
177
|
+
"wins": int(wins.size),
|
|
178
|
+
"losses": int(losses.size),
|
|
179
|
+
"flat": int(finite.size - wins.size - losses.size),
|
|
180
|
+
"win_rate": float(metrics.win_rate(finite)) if finite.size else None,
|
|
181
|
+
"net_pnl": float(np.sum(finite)) if finite.size else None,
|
|
182
|
+
"gross_profit": float(np.sum(wins)) if wins.size else 0.0,
|
|
183
|
+
"gross_loss": float(np.sum(losses)) if losses.size else 0.0,
|
|
184
|
+
"avg_win": float(np.mean(wins)) if wins.size else None,
|
|
185
|
+
"avg_loss": float(np.mean(losses)) if losses.size else None,
|
|
186
|
+
"largest_win": float(np.max(wins)) if wins.size else None,
|
|
187
|
+
"largest_loss": float(np.min(losses)) if losses.size else None,
|
|
188
|
+
"expectancy": float(metrics.expectancy(finite)) if finite.size else None,
|
|
189
|
+
"profit_factor": float(metrics.profit_factor(finite)) if finite.size else None,
|
|
190
|
+
"payoff_ratio": float(metrics.payoff_ratio(finite)) if finite.size else None,
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def exit_reason_breakdown(columns: Dict[str, List[Any]]) -> Dict[str, Any]:
|
|
195
|
+
"""Net P&L, win rate and share of trades, per exit reason.
|
|
196
|
+
|
|
197
|
+
A headline return says nothing about where it came from, and a book whose entire
|
|
198
|
+
edge arrives through one exit path is a bet on that path's fill assumption. Nothing
|
|
199
|
+
in the summary metrics distinguishes it from one whose edge is spread across exits,
|
|
200
|
+
so this reports the split and names the concentration when there is one.
|
|
201
|
+
"""
|
|
202
|
+
present = list(columns)
|
|
203
|
+
if "exit_reason" not in present or "pnl" not in present:
|
|
204
|
+
return _unavailable(("exit_reason", "pnl"), present)
|
|
205
|
+
|
|
206
|
+
reasons = ["" if r is None else str(r) for r in columns["exit_reason"]]
|
|
207
|
+
pnl = _floats(columns["pnl"])
|
|
208
|
+
n_total = len(reasons)
|
|
209
|
+
|
|
210
|
+
rows: List[Dict[str, Any]] = []
|
|
211
|
+
for reason in sorted(set(reasons)):
|
|
212
|
+
mask = np.array([r == reason for r in reasons], dtype=bool)
|
|
213
|
+
summary = _win_loss_over(pnl[mask])
|
|
214
|
+
summary["exit_reason"] = reason or "(unlabelled)"
|
|
215
|
+
# Two counts, because they answer different questions and sharing one name made
|
|
216
|
+
# them disagree: `rows` is every trade that exited this way, `trades` only those
|
|
217
|
+
# whose P&L could be measured. The share is of `rows` — a trade with an
|
|
218
|
+
# unreadable P&L still exited somehow — so the shares sum to one while the
|
|
219
|
+
# `trades` column may not sum to the total. `unmeasured` is what explains the
|
|
220
|
+
# gap, and a renderer that drops it leaves the two columns contradicting.
|
|
221
|
+
summary["rows"] = int(mask.sum())
|
|
222
|
+
summary["share_of_trades"] = (summary["rows"] / n_total) if n_total else None
|
|
223
|
+
rows.append(summary)
|
|
224
|
+
rows.sort(key=lambda r: (r["net_pnl"] is None, -(r["net_pnl"] or 0.0)))
|
|
225
|
+
|
|
226
|
+
# Concentration is measured over the *gains* only. A loss-making exit does not
|
|
227
|
+
# dilute the claim that one path produced the profit; it is a separate fact.
|
|
228
|
+
gains = {r["exit_reason"]: r["net_pnl"] for r in rows if (r["net_pnl"] or 0.0) > 0}
|
|
229
|
+
concentration = None
|
|
230
|
+
if gains:
|
|
231
|
+
total_gain = sum(gains.values())
|
|
232
|
+
top = max(gains, key=lambda k: gains[k])
|
|
233
|
+
concentration = {
|
|
234
|
+
"exit_reason": top,
|
|
235
|
+
"share_of_gain": gains[top] / total_gain if total_gain else None,
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
"available": True,
|
|
239
|
+
"rows": rows,
|
|
240
|
+
"n_trades": n_total,
|
|
241
|
+
"concentration": concentration,
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def duration_summary(columns: Dict[str, List[Any]]) -> Dict[str, Any]:
|
|
246
|
+
"""How long positions were held, in days.
|
|
247
|
+
|
|
248
|
+
Held time is the bridge between a per-trade number and a portfolio one: the same
|
|
249
|
+
per-trade excursion means something different when the average position is open a
|
|
250
|
+
day than when it is open a month.
|
|
251
|
+
"""
|
|
252
|
+
present = list(columns)
|
|
253
|
+
if "entry_time" not in present or "exit_time" not in present:
|
|
254
|
+
return _unavailable(("entry_time", "exit_time"), present)
|
|
255
|
+
|
|
256
|
+
entries = [_as_datetime(v) for v in columns["entry_time"]]
|
|
257
|
+
exits = [_as_datetime(v) for v in columns["exit_time"]]
|
|
258
|
+
days = np.full(len(entries), np.nan, dtype="float64")
|
|
259
|
+
for i, (opened, closed) in enumerate(zip(entries, exits)):
|
|
260
|
+
if opened is not None and closed is not None:
|
|
261
|
+
days[i] = (closed - opened).total_seconds() / 86_400.0
|
|
262
|
+
summary = _distribution(days, len(entries))
|
|
263
|
+
summary["available"] = True
|
|
264
|
+
summary["unit"] = "days"
|
|
265
|
+
return summary
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def excursion_summary(columns: Dict[str, List[Any]]) -> Dict[str, Any]:
|
|
269
|
+
"""How far each position went against, and in favour of, the book while open.
|
|
270
|
+
|
|
271
|
+
Per *trade*, which is the figure recorded — not the book's aggregate open
|
|
272
|
+
drawdown, which is a different question this does not answer and must not be read
|
|
273
|
+
as answering. A position deep underwater that is a small fraction of the book did
|
|
274
|
+
not put the book that far underwater, and reading the first as the second is the
|
|
275
|
+
mistake this label exists to prevent.
|
|
276
|
+
"""
|
|
277
|
+
present = list(columns)
|
|
278
|
+
if "mae_pct" not in present and "mfe_pct" not in present:
|
|
279
|
+
return _unavailable(("mae_pct", "mfe_pct"), present)
|
|
280
|
+
out: Dict[str, Any] = {
|
|
281
|
+
"available": True,
|
|
282
|
+
"unit": "percent of entry price, per trade",
|
|
283
|
+
"note": "per-trade excursion — not the book's aggregate open drawdown",
|
|
284
|
+
}
|
|
285
|
+
for name in ("mae_pct", "mfe_pct"):
|
|
286
|
+
if name in present:
|
|
287
|
+
values = _floats(columns[name])
|
|
288
|
+
out[name] = _distribution(values, len(values))
|
|
289
|
+
else:
|
|
290
|
+
out[name] = _unavailable((name,), present)
|
|
291
|
+
return out
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def trade_analytics(table: Optional[Dict[str, Any]], *, allow_partial: bool = False) -> Dict[str, Any]:
|
|
295
|
+
"""Everything this module can say about one recorded run's trades.
|
|
296
|
+
|
|
297
|
+
``status`` is the thing to read first. ``complete`` means the sections below cover
|
|
298
|
+
every trade the run made. ``truncated`` means they cover the stored rows only and
|
|
299
|
+
the caller asked for that. ``unavailable`` means there are no sections, and
|
|
300
|
+
``reason`` says why — no table, or a table whose totals cannot honestly be taken.
|
|
301
|
+
"""
|
|
302
|
+
source = describe_source(table)
|
|
303
|
+
result: Dict[str, Any] = {"status": UNAVAILABLE, "reason": "", "source": source}
|
|
304
|
+
|
|
305
|
+
if table is None:
|
|
306
|
+
result["reason"] = (
|
|
307
|
+
"no trade table was recorded for this trial — re-run it with trades recorded "
|
|
308
|
+
"(`--record-trades` on the CLI, `record_trades=true` over MCP). "
|
|
309
|
+
"This is not the same as a run that made no trades"
|
|
310
|
+
)
|
|
311
|
+
return result
|
|
312
|
+
|
|
313
|
+
completeness = source["completeness"]
|
|
314
|
+
if completeness != SOURCE_WHOLE and not allow_partial:
|
|
315
|
+
stored, total = source["rows_stored"], source["total_rows"]
|
|
316
|
+
result["reason"] = (
|
|
317
|
+
(
|
|
318
|
+
f"the stored table holds {stored:,} of the run's {total:,} trades — it was capped "
|
|
319
|
+
"at the storage ceiling, so a total over it would be short by the rest"
|
|
320
|
+
)
|
|
321
|
+
if completeness == SOURCE_CAPPED
|
|
322
|
+
else (
|
|
323
|
+
"whether the stored table holds all of the run's trades was not recorded, so a "
|
|
324
|
+
"total over it cannot be called complete"
|
|
325
|
+
)
|
|
326
|
+
)
|
|
327
|
+
return result
|
|
328
|
+
|
|
329
|
+
columns = _columns(table)
|
|
330
|
+
result.update(
|
|
331
|
+
{
|
|
332
|
+
"status": COMPLETE if completeness == SOURCE_WHOLE else TRUNCATED,
|
|
333
|
+
"n_trades": source["rows_stored"],
|
|
334
|
+
"overall": (
|
|
335
|
+
_win_loss_over(_floats(columns["pnl"]))
|
|
336
|
+
if "pnl" in columns
|
|
337
|
+
else _unavailable(("pnl",), list(columns))
|
|
338
|
+
),
|
|
339
|
+
"exit_reasons": exit_reason_breakdown(columns),
|
|
340
|
+
"duration": duration_summary(columns),
|
|
341
|
+
"excursion": excursion_summary(columns),
|
|
342
|
+
}
|
|
343
|
+
)
|
|
344
|
+
if result["status"] == TRUNCATED:
|
|
345
|
+
if completeness == SOURCE_CAPPED:
|
|
346
|
+
of_total = f" of the run's {source['total_rows']:,}" if source["total_rows"] else ""
|
|
347
|
+
result["reason"] = (
|
|
348
|
+
f"computed over the {source['rows_stored']:,} stored rows only{of_total} — every "
|
|
349
|
+
"count and total below is a partial"
|
|
350
|
+
)
|
|
351
|
+
else:
|
|
352
|
+
result["reason"] = "computed over the stored rows, which were not recorded as being all of them"
|
|
353
|
+
return result
|