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,125 @@
|
|
|
1
|
+
"""Broker-agnostic bar timeframe.
|
|
2
|
+
|
|
3
|
+
Strategies declare their timeframe as a friendly string (``"5Min"``, ``"1Hour"``,
|
|
4
|
+
``"1Day"``). This module parses that once into a :class:`Timeframe` value object
|
|
5
|
+
so every layer agrees on what ``"5Min"`` means, and each broker adapter maps the
|
|
6
|
+
value object onto its own SDK type.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from datetime import timedelta
|
|
12
|
+
from typing import ClassVar, Dict
|
|
13
|
+
|
|
14
|
+
# Canonical unit names.
|
|
15
|
+
MINUTE = "min"
|
|
16
|
+
HOUR = "hour"
|
|
17
|
+
DAY = "day"
|
|
18
|
+
WEEK = "week"
|
|
19
|
+
|
|
20
|
+
# Map the suffixes people actually type onto canonical units.
|
|
21
|
+
_UNIT_ALIASES: Dict[str, str] = {
|
|
22
|
+
"min": MINUTE,
|
|
23
|
+
"m": MINUTE,
|
|
24
|
+
"minute": MINUTE,
|
|
25
|
+
"h": HOUR,
|
|
26
|
+
"hr": HOUR,
|
|
27
|
+
"hour": HOUR,
|
|
28
|
+
"d": DAY,
|
|
29
|
+
"day": DAY,
|
|
30
|
+
"w": WEEK,
|
|
31
|
+
"wk": WEEK,
|
|
32
|
+
"week": WEEK,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
_PATTERN = re.compile(r"^\s*(\d+)\s*([a-zA-Z]+)\s*$")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class Timeframe:
|
|
40
|
+
"""An amount + unit, e.g. ``Timeframe(5, "min")``."""
|
|
41
|
+
|
|
42
|
+
amount: int
|
|
43
|
+
unit: str
|
|
44
|
+
|
|
45
|
+
#: pandas resample/offset aliases per canonical unit.
|
|
46
|
+
_PANDAS_OFFSET: ClassVar[Dict[str, str]] = {MINUTE: "min", HOUR: "h", DAY: "D", WEEK: "W"}
|
|
47
|
+
|
|
48
|
+
#: US regular-session trading hours per day (09:30-16:00 ET).
|
|
49
|
+
_TRADING_HOURS_PER_DAY: ClassVar[float] = 6.5
|
|
50
|
+
#: US trading days per year.
|
|
51
|
+
_TRADING_DAYS_PER_YEAR: ClassVar[int] = 252
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def parse(cls, text: str) -> "Timeframe":
|
|
55
|
+
"""Parse a string like ``"5Min"`` / ``"1Hour"`` / ``"1Day"``."""
|
|
56
|
+
match = _PATTERN.match(text)
|
|
57
|
+
if not match:
|
|
58
|
+
raise ValueError(f"Invalid timeframe: {text!r} (expected e.g. '5Min', '1Hour', '1Day')")
|
|
59
|
+
|
|
60
|
+
amount = int(match.group(1))
|
|
61
|
+
unit = _UNIT_ALIASES.get(match.group(2).lower())
|
|
62
|
+
if unit is None:
|
|
63
|
+
raise ValueError(f"Unsupported timeframe unit in {text!r}")
|
|
64
|
+
if amount < 1:
|
|
65
|
+
raise ValueError(f"Timeframe amount must be >= 1, got {amount}")
|
|
66
|
+
return cls(amount=amount, unit=unit)
|
|
67
|
+
|
|
68
|
+
def to_pandas_offset(self) -> str:
|
|
69
|
+
"""Return the pandas offset alias, e.g. ``"5min"``, ``"1D"``."""
|
|
70
|
+
return f"{self.amount}{self._PANDAS_OFFSET[self.unit]}"
|
|
71
|
+
|
|
72
|
+
def duration(self) -> timedelta:
|
|
73
|
+
"""The wall-clock length of one bar.
|
|
74
|
+
|
|
75
|
+
Deliberately calendar duration, not trading duration: this answers "how long
|
|
76
|
+
until the next bar should arrive", which is what a staleness check needs. A
|
|
77
|
+
daily bar is 24 hours here even though the session is 6.5 — the next one
|
|
78
|
+
genuinely does not arrive for a day.
|
|
79
|
+
"""
|
|
80
|
+
return {
|
|
81
|
+
MINUTE: lambda n: timedelta(minutes=n),
|
|
82
|
+
HOUR: lambda n: timedelta(hours=n),
|
|
83
|
+
DAY: lambda n: timedelta(days=n),
|
|
84
|
+
WEEK: lambda n: timedelta(weeks=n),
|
|
85
|
+
}[self.unit](self.amount)
|
|
86
|
+
|
|
87
|
+
def bars_per_trading_day(self) -> float:
|
|
88
|
+
"""How many bars of this timeframe one regular session produces.
|
|
89
|
+
|
|
90
|
+
The bridge between a *bar count* and a *wall-clock window*. A strategy asks
|
|
91
|
+
for "50 bars of warm-up"; only this knows that 50 one-minute bars span part of
|
|
92
|
+
a single session while 50 daily bars span ten calendar weeks. Converting the
|
|
93
|
+
two with a plain ``timedelta`` treats the overnight gap as tradeable and
|
|
94
|
+
under-fetches badly at intraday frequencies.
|
|
95
|
+
|
|
96
|
+
Fractional on purpose: a 1Week timeframe yields 0.2 bars per trading day.
|
|
97
|
+
"""
|
|
98
|
+
if self.unit == DAY:
|
|
99
|
+
return 1.0 / self.amount
|
|
100
|
+
if self.unit == WEEK:
|
|
101
|
+
return 1.0 / (5.0 * self.amount)
|
|
102
|
+
if self.unit == HOUR:
|
|
103
|
+
return self._TRADING_HOURS_PER_DAY / self.amount
|
|
104
|
+
return self._TRADING_HOURS_PER_DAY * 60.0 / self.amount
|
|
105
|
+
|
|
106
|
+
def periods_per_year(self) -> float:
|
|
107
|
+
"""How many bars of this timeframe occur in a trading year.
|
|
108
|
+
|
|
109
|
+
Used to annualize ratios computed on a *per-bar* return series (a 5-minute
|
|
110
|
+
bar is **not** 1/252 of a year). Note the backtest equity curve is daily-
|
|
111
|
+
resampled, so its metrics use ``TRADING_DAYS_PER_YEAR`` directly; this is
|
|
112
|
+
for callers that work on bar-frequency returns.
|
|
113
|
+
"""
|
|
114
|
+
days = self._TRADING_DAYS_PER_YEAR
|
|
115
|
+
if self.unit == DAY:
|
|
116
|
+
return days / self.amount
|
|
117
|
+
if self.unit == WEEK:
|
|
118
|
+
return 52.0 / self.amount
|
|
119
|
+
if self.unit == HOUR:
|
|
120
|
+
return days * self._TRADING_HOURS_PER_DAY / self.amount
|
|
121
|
+
# minutes
|
|
122
|
+
return days * self._TRADING_HOURS_PER_DAY * 60.0 / self.amount
|
|
123
|
+
|
|
124
|
+
def __str__(self) -> str:
|
|
125
|
+
return f"{self.amount}{self.unit}"
|
quantred/mcp/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""QuantRed MCP server package.
|
|
2
|
+
|
|
3
|
+
Opt-in: requires the ``mcp`` extra (the command that needs it prints the install line for the copy you
|
|
4
|
+
are running). The ``mcp`` import lives only inside :mod:`quantred.mcp.server` and is imported lazily by the ``mcp``
|
|
5
|
+
subcommand, so the base install and other commands never pull it in.
|
|
6
|
+
"""
|
quantred/mcp/selftest.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Drive the MCP server with the real SDK client, the way a chat client does.
|
|
2
|
+
|
|
3
|
+
``quantred mcp --self-test`` exists so a user can tell "the server starts and
|
|
4
|
+
answers" apart from "my chat is connected". It launches a *child* copy of the server
|
|
5
|
+
over stdio, initializes, lists the tools and calls the two discovery tools. Going
|
|
6
|
+
through the SDK client matters: the in-process ``call_tool`` shortcut skips the
|
|
7
|
+
output-schema validation a real client performs, which is how a mistyped discovery
|
|
8
|
+
row shipped.
|
|
9
|
+
|
|
10
|
+
Importable only with the ``mcp`` extra; the CLI imports it lazily.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import tempfile
|
|
17
|
+
import time
|
|
18
|
+
from typing import Any, Dict, List, Optional
|
|
19
|
+
|
|
20
|
+
import anyio
|
|
21
|
+
from mcp import ClientSession, StdioServerParameters
|
|
22
|
+
from mcp.client.stdio import stdio_client
|
|
23
|
+
|
|
24
|
+
#: Said on every run, pass or fail: a passing self-test is easy to over-read.
|
|
25
|
+
DISCLAIMER = (
|
|
26
|
+
"This checks that the server starts and answers. It does not connect it to any chat "
|
|
27
|
+
"- your MCP client launches its own copy from its config."
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
#: The discovery tools the check calls. Names here are the tool names, not services.
|
|
31
|
+
DISCOVERY_TOOLS = ("list_strategies", "list_scanners")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def default_command(*, offline: bool = False, cache_dir: Optional[Any] = None) -> List[str]:
|
|
35
|
+
"""The server command for *this* copy - the one ``init`` tells a reader to configure.
|
|
36
|
+
|
|
37
|
+
Delegates to the setup service so the check and the instruction cannot name two
|
|
38
|
+
different commands (the check passing on one nobody was told to use).
|
|
39
|
+
"""
|
|
40
|
+
from quantred.services.setup import mcp_launch_command
|
|
41
|
+
|
|
42
|
+
return mcp_launch_command(offline=offline, cache_dir=cache_dir)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _rows(result: Any) -> List[Dict[str, Any]]:
|
|
46
|
+
structured = getattr(result, "structuredContent", None) or {}
|
|
47
|
+
rows = structured.get("result")
|
|
48
|
+
return list(rows) if isinstance(rows, list) else []
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
async def _drive(command: List[str], env: Dict[str, str], errlog, report: Dict[str, Any]) -> None:
|
|
52
|
+
"""Fill ``report`` as it goes, so a failure still knows which step it reached."""
|
|
53
|
+
params = StdioServerParameters(command=command[0], args=command[1:], env=env)
|
|
54
|
+
async with stdio_client(params, errlog=errlog) as (read, write):
|
|
55
|
+
async with ClientSession(read, write) as session:
|
|
56
|
+
report["step"] = "initialize"
|
|
57
|
+
initialized = await session.initialize()
|
|
58
|
+
report["server"] = initialized.serverInfo.name
|
|
59
|
+
report["step"] = "list_tools"
|
|
60
|
+
report["tools"] = sorted(t.name for t in (await session.list_tools()).tools)
|
|
61
|
+
for tool in DISCOVERY_TOOLS:
|
|
62
|
+
report["step"] = tool
|
|
63
|
+
result = await session.call_tool(tool)
|
|
64
|
+
if result.isError:
|
|
65
|
+
text = " ".join(getattr(c, "text", "") for c in result.content)
|
|
66
|
+
raise RuntimeError(f"{tool} returned an error: {text}")
|
|
67
|
+
rows = _rows(result)
|
|
68
|
+
if not rows:
|
|
69
|
+
raise RuntimeError(
|
|
70
|
+
f"{tool} returned no structured rows - the server's MCP SDK may predate "
|
|
71
|
+
"structured output (the 'mcp' extra needs >=1.10), or discovery found nothing"
|
|
72
|
+
)
|
|
73
|
+
if not any(r.get("demo") for r in rows):
|
|
74
|
+
raise RuntimeError(f"{tool} did not list the built-in demo entries")
|
|
75
|
+
report[tool] = rows
|
|
76
|
+
report["step"] = "close"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def run_self_test(
|
|
80
|
+
command: Optional[List[str]] = None,
|
|
81
|
+
*,
|
|
82
|
+
timeout: float = 60,
|
|
83
|
+
env: Optional[Dict[str, str]] = None,
|
|
84
|
+
offline: bool = False,
|
|
85
|
+
cache_dir: Optional[Any] = None,
|
|
86
|
+
) -> Dict[str, Any]:
|
|
87
|
+
"""Run the discovery check; never raises for a server-side failure.
|
|
88
|
+
|
|
89
|
+
Returns ``{"ok", "step", "elapsed", ...}``. On success it also carries ``tools``,
|
|
90
|
+
``list_strategies`` and ``list_scanners`` rows. On failure ``step`` names where it
|
|
91
|
+
stopped and ``error`` / ``server_stderr`` say why: a server that exits before
|
|
92
|
+
answering (a missing credential, a missing extra) reaches a client as a closed
|
|
93
|
+
pipe and nothing else, so what the child printed is the useful part.
|
|
94
|
+
|
|
95
|
+
``env`` defaults to a copy of the caller's environment - the SDK would otherwise
|
|
96
|
+
hand the child a minimal one, losing ``TRADEFLOW_HOME`` and credentials.
|
|
97
|
+
"""
|
|
98
|
+
if command:
|
|
99
|
+
command = list(command)
|
|
100
|
+
elif offline or cache_dir is not None:
|
|
101
|
+
command = default_command(offline=offline, cache_dir=cache_dir)
|
|
102
|
+
else:
|
|
103
|
+
command = default_command()
|
|
104
|
+
child_env = dict(os.environ if env is None else env)
|
|
105
|
+
started = time.monotonic()
|
|
106
|
+
report: Dict[str, Any] = {"step": "start"}
|
|
107
|
+
|
|
108
|
+
async def go() -> Dict[str, Any]:
|
|
109
|
+
with tempfile.TemporaryFile(mode="w+", encoding="utf-8") as errlog:
|
|
110
|
+
try:
|
|
111
|
+
with anyio.fail_after(timeout):
|
|
112
|
+
await _drive(command, child_env, errlog, report)
|
|
113
|
+
return {"ok": True, **report}
|
|
114
|
+
except BaseException as exc: # noqa: BLE001 - reported, not raised
|
|
115
|
+
if isinstance(exc, (KeyboardInterrupt, SystemExit)):
|
|
116
|
+
raise
|
|
117
|
+
errlog.flush()
|
|
118
|
+
errlog.seek(0)
|
|
119
|
+
return {
|
|
120
|
+
"ok": False,
|
|
121
|
+
"step": report["step"],
|
|
122
|
+
"error": _describe(exc),
|
|
123
|
+
"server_stderr": errlog.read().strip()[-2000:],
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
out = anyio.run(go)
|
|
127
|
+
out["command"] = command
|
|
128
|
+
out["elapsed"] = time.monotonic() - started
|
|
129
|
+
return out
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _describe(exc: BaseException) -> str:
|
|
133
|
+
# Task-group failures arrive wrapped; the first member is the cause worth showing.
|
|
134
|
+
members = getattr(exc, "exceptions", None)
|
|
135
|
+
if members:
|
|
136
|
+
return _describe(members[0])
|
|
137
|
+
if isinstance(exc, TimeoutError):
|
|
138
|
+
return "timed out waiting for the server"
|
|
139
|
+
return f"{type(exc).__name__}: {exc}" if str(exc) else type(exc).__name__
|