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,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}"
@@ -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
+ """
@@ -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__