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,220 @@
1
+ """Broker abstraction.
2
+
3
+ Everything above this layer (execution, engine, scanners, strategies, main)
4
+ depends only on the :class:`Broker` interface and the broker-agnostic domain
5
+ types defined here - never on a specific vendor SDK. To support a new venue,
6
+ implement :class:`Broker` (see :mod:`quantred.brokers.alpaca.broker`) and inject it;
7
+ nothing else changes.
8
+
9
+ **Failure is typed, not erased.** Anything that moves money or reports the account's
10
+ actual state raises a :class:`~quantred.brokers.errors.BrokerError` subclass, because
11
+ the caller's correct response differs by cause: back off, stop and get a human, size
12
+ down, or carry on. The methods that answer a genuine yes/no question still return one
13
+ - :meth:`Broker.get_position` returning ``None`` means *flat*, which is an answer
14
+ rather than a failure.
15
+ """
16
+
17
+ from abc import ABC, abstractmethod
18
+ from dataclasses import dataclass
19
+ from datetime import datetime
20
+ from enum import Enum
21
+ from typing import Awaitable, Callable, List, Optional, Union
22
+
23
+
24
+ class OrderSide(str, Enum):
25
+ """Side of an order."""
26
+
27
+ BUY = "buy"
28
+ SELL = "sell"
29
+
30
+
31
+ @dataclass
32
+ class AccountSnapshot:
33
+ """A point-in-time view of the trading account."""
34
+
35
+ cash: float
36
+ equity: float
37
+ buying_power: float
38
+ portfolio_value: float
39
+ trading_blocked: bool = False
40
+
41
+
42
+ @dataclass
43
+ class Position:
44
+ """An open position, normalized across brokers.
45
+
46
+ ``side`` is ``"long"`` or ``"short"``; ``qty`` is always non-negative.
47
+ """
48
+
49
+ symbol: str
50
+ qty: float
51
+ side: str
52
+ avg_entry_price: float
53
+ current_price: float
54
+ market_value: float
55
+ unrealized_pl: float
56
+
57
+ @property
58
+ def is_long(self) -> bool:
59
+ return self.side == "long"
60
+
61
+
62
+ @dataclass
63
+ class OrderResult:
64
+ """The outcome of submitting an order."""
65
+
66
+ id: str
67
+ symbol: str
68
+ side: OrderSide
69
+ qty: float
70
+ status: str
71
+ raw: object = None # underlying SDK object, if a caller needs broker specifics
72
+
73
+
74
+ @dataclass
75
+ class MarketStatus:
76
+ """Current market clock."""
77
+
78
+ timestamp: datetime
79
+ is_open: bool
80
+ next_open: datetime
81
+ next_close: datetime
82
+
83
+
84
+ @dataclass
85
+ class TradeUpdate:
86
+ """A streamed account/order event (fill, new, canceled, rejected, ...)."""
87
+
88
+ event: str
89
+ symbol: str
90
+ order_id: str
91
+ status: str
92
+ #: Cumulative filled quantity for the order, not this event's increment. Alpaca
93
+ #: re-reports the running total on every partial fill and again on the final fill.
94
+ filled_qty: float = 0.0
95
+ price: Optional[float] = None
96
+ #: "buy" / "sell". Without it a short fill is indistinguishable from a long one,
97
+ #: and anything defaulting the side records the whole book long.
98
+ side: Optional[str] = None
99
+ #: Average price across everything filled on this order so far. Pairs with
100
+ #: ``filled_qty``, which is also cumulative; ``price`` is this event's own print.
101
+ filled_avg_price: Optional[float] = None
102
+ #: When the venue reported the fill, as an ISO timestamp. Distinct from when we
103
+ #: recorded it — the gap between the two is latency we would otherwise attribute
104
+ #: to the venue.
105
+ filled_at: Optional[str] = None
106
+ #: What the venue charged, when it says. ``None`` means "not reported", which a
107
+ #: paper account always does, and which is not the same as zero.
108
+ fee: Optional[float] = None
109
+
110
+
111
+ #: A trade-update handler may be sync or async.
112
+ TradeUpdateHandler = Callable[[TradeUpdate], Union[None, Awaitable[None]]]
113
+
114
+
115
+ class Broker(ABC):
116
+ """Interface every brokerage adapter must implement.
117
+
118
+ Kept intentionally small: account/position reads, the order types the engine
119
+ actually uses, and order/position lifecycle management.
120
+ """
121
+
122
+ # --- account & positions -------------------------------------------------
123
+ @abstractmethod
124
+ def get_account(self) -> Optional[AccountSnapshot]:
125
+ """Return the current account snapshot.
126
+
127
+ Raises a :class:`~quantred.brokers.errors.BrokerError` if the account cannot
128
+ be read.
129
+ """
130
+
131
+ @abstractmethod
132
+ def list_positions(self) -> List[Position]:
133
+ """Return all open positions; an empty list means the account is flat.
134
+
135
+ Raises a :class:`~quantred.brokers.errors.BrokerError` rather than returning
136
+ an empty list when positions cannot be read - "flat" and "unknown" are
137
+ different answers, and callers rebuild state from this one.
138
+ """
139
+
140
+ @abstractmethod
141
+ def get_position(self, symbol: str) -> Optional[Position]:
142
+ """Return the open position for ``symbol``, or None if flat."""
143
+
144
+ @abstractmethod
145
+ def is_tradable(self, symbol: str) -> bool:
146
+ """Whether ``symbol`` can currently be traded on this venue."""
147
+
148
+ # --- orders --------------------------------------------------------------
149
+ @abstractmethod
150
+ def submit_market_order(
151
+ self, symbol: str, qty: float, side: OrderSide, client_order_id: Optional[str] = None
152
+ ) -> Optional[OrderResult]:
153
+ """Submit a market order.
154
+
155
+ ``client_order_id``, when given, is the caller's own identity for this order.
156
+ A venue that has already accepted it must reject the duplicate rather than
157
+ place a second order - that rejection is the idempotency guarantee, and it is
158
+ the only one that survives the submitting process restarting.
159
+ """
160
+
161
+ @abstractmethod
162
+ def submit_bracket_order(
163
+ self,
164
+ symbol: str,
165
+ qty: float,
166
+ side: OrderSide,
167
+ stop_loss: float,
168
+ take_profit: float,
169
+ client_order_id: Optional[str] = None,
170
+ ) -> Optional[OrderResult]:
171
+ """Submit a bracket order (entry + stop-loss + take-profit).
172
+
173
+ ``client_order_id`` carries the same meaning as in :meth:`submit_market_order`.
174
+ """
175
+
176
+ @abstractmethod
177
+ def list_open_orders(self, symbol: Optional[str] = None) -> List[OrderResult]:
178
+ """Return open (unfilled) orders, optionally filtered to ``symbol``."""
179
+
180
+ @abstractmethod
181
+ def cancel_order(self, order_id: str) -> bool:
182
+ """Cancel a single order. Raises a ``BrokerError`` if the venue refuses."""
183
+
184
+ @abstractmethod
185
+ def cancel_all_orders(self) -> bool:
186
+ """Cancel all open orders. Raises a ``BrokerError`` if the venue refuses."""
187
+
188
+ # --- position lifecycle --------------------------------------------------
189
+ @abstractmethod
190
+ def close_position(self, symbol: str) -> bool:
191
+ """Liquidate the position in ``symbol``. Raises a ``BrokerError`` on failure."""
192
+
193
+ @abstractmethod
194
+ def close_all_positions(self, cancel_orders: bool = True) -> bool:
195
+ """Liquidate all positions, optionally canceling orders first.
196
+
197
+ Raises a ``BrokerError`` on failure - a flatten that quietly did nothing is
198
+ the worst possible outcome for this call.
199
+ """
200
+
201
+ # --- market clock --------------------------------------------------------
202
+ @abstractmethod
203
+ def get_market_status(self) -> Optional[MarketStatus]:
204
+ """Return the current market clock.
205
+
206
+ Raises a :class:`~quantred.brokers.errors.BrokerError` if the clock cannot be
207
+ read, so a caller can tell "closed" apart from "could not tell".
208
+ """
209
+
210
+ # --- trade-update streaming (optional capability) ------------------------
211
+ def supports_trade_updates(self) -> bool:
212
+ """Whether this broker can stream account/order updates. Override if so."""
213
+ return False
214
+
215
+ async def stream_trade_updates(self, handler: TradeUpdateHandler) -> None:
216
+ """Stream account/order updates to ``handler`` until canceled.
217
+
218
+ Optional: only meaningful when :meth:`supports_trade_updates` is True.
219
+ """
220
+ raise NotImplementedError("This broker does not support trade-update streaming")
@@ -0,0 +1,164 @@
1
+ """A broker that can answer questions and cannot trade.
2
+
3
+ Observing a strategy's decision path used to mean running a paper session, and a paper
4
+ session needed fills to observe — so the book's caps got distorted downward to force
5
+ them. A position ceiling small enough to guarantee fills biases the book toward
6
+ low-priced names and turns every high-price signal into an invisible non-trade, which
7
+ means the sample you collect is not the strategy you validated. Measuring execution
8
+ required trading a book nobody wanted to trade.
9
+
10
+ This separates the two questions. **What would this contract try to do?** is answered
11
+ here, against a stated account, with the caps exactly as configured. **What does
12
+ execution actually cost?** is a different run at reduced capital, where the real
13
+ contract's proportions are preserved and fills are the point.
14
+
15
+ **Trading is a capability this broker does not have.** Every order method refuses, and
16
+ that is the whole design rather than a flag consulted on the order path. A flag can be
17
+ forgotten on one branch, read from a stale config, or inverted in a refactor; an absent
18
+ capability cannot be any of those. It is the same guarantee the MCP server gets from
19
+ building only a data client — the wall is structural, so it cannot be reasoned around.
20
+
21
+ Reads are served from a synthetic account whose capital the caller must state. There is
22
+ deliberately no fallback to a broker's real equity and no default account size: the
23
+ question is what a *specified* contract would do at a *specified* capital, and inventing
24
+ either turns the answer into a guess about a book nobody chose.
25
+ """
26
+
27
+ from typing import Any, Dict, List, Optional
28
+
29
+ from quantred.brokers.base import AccountSnapshot, Broker, MarketStatus, OrderResult, Position
30
+ from quantred.brokers.errors import BrokerError
31
+
32
+ #: Printed wherever a dry run reports, and deliberately unmissable. A reader who mistakes
33
+ #: this for a paper session mistakes an intention for an execution.
34
+ BANNER = "DRY RUN — broker has no trading capability; no orders can be submitted"
35
+
36
+ #: Raised by every order method. The message names the mode rather than the method,
37
+ #: because a caller reaching one of these has misunderstood the run, not the API.
38
+ _REFUSAL = (
39
+ "this is a dry run: the broker has no trading capability, so nothing can be "
40
+ "submitted, cancelled or closed. Use a small-real session to observe execution"
41
+ )
42
+
43
+
44
+ class DryRunTradingAttempted(BrokerError):
45
+ """Raised wherever a dry run reaches an order path.
46
+
47
+ A :class:`~quantred.brokers.errors.BrokerError` on purpose, and the choice is the
48
+ whole design. ``LiveTrader`` builds its :class:`OrderPlan` *before* submitting
49
+ precisely so that a broker refusal still records what would have been sent, and
50
+ returns a decline carrying that plan. So refusing here does not lose the intent —
51
+ it routes it through machinery the trade clock already has, and the report is
52
+ assembled from real declines rather than from a parallel path that would have to be
53
+ kept in step with the real one.
54
+
55
+ The alternative was a broker that accepts submissions and records them. That would
56
+ make "cannot trade" a promise about what this class does with an order rather than
57
+ about whether it can place one, and the guarantee this mode exists for is the
58
+ second.
59
+ """
60
+
61
+
62
+ class DryRunBroker(Broker):
63
+ """Answers account and market questions; refuses every order.
64
+
65
+ ``positions`` start empty unless the caller supplies them. A dry run over an
66
+ existing book and a dry run from flat answer different questions - "what would this
67
+ do next, given what I hold" against "what would this do from a clean book" - so the
68
+ source is recorded rather than assumed, and nothing is adopted implicitly.
69
+ """
70
+
71
+ def __init__(
72
+ self,
73
+ *,
74
+ capital: float,
75
+ positions: Optional[List[Position]] = None,
76
+ positions_source: str = "flat",
77
+ tradable: Optional[Dict[str, bool]] = None,
78
+ market_open: bool = True,
79
+ ):
80
+ if capital is None:
81
+ raise ValueError(
82
+ "a dry run needs a stated capital: take it from the config being run or "
83
+ "pass one explicitly. There is no default, because the caps a run reports "
84
+ "are only meaningful against the capital they bound"
85
+ )
86
+ self._capital = float(capital)
87
+ self._positions = list(positions or [])
88
+ #: Where the starting book came from, carried into the report so a reader is
89
+ #: never left guessing whether an empty book was chosen or merely assumed.
90
+ self.positions_source = positions_source
91
+ self._tradable = dict(tradable or {})
92
+ self._market_open = bool(market_open)
93
+ #: Everything the decision path *would* have sent, in order. Named for what it
94
+ #: is: these are intentions, and no word here may suggest an execution.
95
+ self.would_submit: List[Dict[str, Any]] = []
96
+
97
+ # ------------------------------------------------------------------ #
98
+ # Reads — answerable without a venue
99
+ # ------------------------------------------------------------------ #
100
+ def get_account(self) -> Optional[AccountSnapshot]:
101
+ """The stated account, marked at the positions it was given.
102
+
103
+ ``buying_power`` is the cash the sizer may deploy, so it is capital less what
104
+ the supplied positions already reserve - not the capital again, which would let
105
+ a dry run over an existing book size as though it were flat.
106
+ """
107
+ reserved = sum(p.market_value for p in self._positions)
108
+ return AccountSnapshot(
109
+ cash=self._capital - reserved,
110
+ equity=self._capital,
111
+ buying_power=max(self._capital - reserved, 0.0),
112
+ portfolio_value=self._capital,
113
+ trading_blocked=False,
114
+ )
115
+
116
+ def list_positions(self) -> List[Position]:
117
+ return list(self._positions)
118
+
119
+ def get_position(self, symbol: str) -> Optional[Position]:
120
+ return next((p for p in self._positions if p.symbol == symbol), None)
121
+
122
+ def is_tradable(self, symbol: str) -> bool:
123
+ """Assumed tradable unless the caller said otherwise.
124
+
125
+ A dry run cannot ask a venue, and refusing every symbol would report a book that
126
+ skipped everything for a reason that is an artefact of the mode.
127
+ """
128
+ return self._tradable.get(symbol, True)
129
+
130
+ def list_open_orders(self, symbol: Optional[str] = None) -> List[OrderResult]:
131
+ """Always empty. Nothing was submitted, so nothing can be resting."""
132
+ return []
133
+
134
+ def get_market_status(self) -> Optional[MarketStatus]:
135
+ return MarketStatus(is_open=self._market_open)
136
+
137
+ def supports_trade_updates(self) -> bool:
138
+ """No. There are no fills to stream, and saying otherwise would have a caller
139
+ wait on updates that can never arrive."""
140
+ return False
141
+
142
+ # ------------------------------------------------------------------ #
143
+ # Orders — the capability this broker does not have
144
+ # ------------------------------------------------------------------ #
145
+ def submit_market_order(self, *args: Any, **kwargs: Any) -> OrderResult:
146
+ raise DryRunTradingAttempted(_REFUSAL)
147
+
148
+ def submit_bracket_order(self, *args: Any, **kwargs: Any) -> OrderResult:
149
+ raise DryRunTradingAttempted(_REFUSAL)
150
+
151
+ def cancel_order(self, order_id: str) -> bool:
152
+ raise DryRunTradingAttempted(_REFUSAL)
153
+
154
+ def cancel_all_orders(self) -> bool:
155
+ raise DryRunTradingAttempted(_REFUSAL)
156
+
157
+ def close_position(self, symbol: str) -> bool:
158
+ raise DryRunTradingAttempted(_REFUSAL)
159
+
160
+ def close_all_positions(self, cancel_orders: bool = True) -> bool:
161
+ raise DryRunTradingAttempted(_REFUSAL)
162
+
163
+ async def stream_trade_updates(self, handler: Any) -> None:
164
+ raise DryRunTradingAttempted(_REFUSAL)
@@ -0,0 +1,70 @@
1
+ """Broker failure, typed.
2
+
3
+ Every broker call used to fail the same way: ``None``, or ``False``. A rate limit, an
4
+ expired token, insufficient buying power, a market that is closed, and an order the
5
+ venue deliberately refused all arrived as the same absence of information, so the only
6
+ possible response was the same one - log it and move on. That is the wrong response to
7
+ most of them, and for a duplicate order it is actively misleading: the order *was*
8
+ placed.
9
+
10
+ The distinctions that matter are the ones a caller would act on differently:
11
+
12
+ * :class:`RateLimitedError` - back off; the request was fine.
13
+ * :class:`AuthenticationError` - nothing will work until a human fixes credentials.
14
+ Never transient, so never worth retrying or trading through.
15
+ * :class:`InsufficientFundsError` - the account cannot support this size.
16
+ * :class:`MarketClosedError` - correct request, wrong time.
17
+ * :class:`DuplicateOrderError` - the venue already has this order. Not a failure.
18
+ * :class:`OrderRejectedError` - the venue refused it on its own terms.
19
+ * :class:`BrokerUnavailableError` - the venue could not be reached at all. Transient
20
+ by nature, and the only one where carrying on optimistically is defensible.
21
+
22
+ Vendor-neutral by design: these live beside the :class:`~quantred.brokers.base.Broker`
23
+ interface, so a caller that handles them handles every venue.
24
+ """
25
+
26
+
27
+ class BrokerError(RuntimeError):
28
+ """Base class for every broker failure."""
29
+
30
+
31
+ class BrokerUnavailableError(BrokerError):
32
+ """The venue could not be reached (network failure, timeout, 5xx)."""
33
+
34
+
35
+ class RateLimitedError(BrokerError):
36
+ """The venue is throttling us. The request itself was acceptable."""
37
+
38
+
39
+ class AuthenticationError(BrokerError):
40
+ """Credentials are missing, expired, or insufficient for this operation.
41
+
42
+ Distinguished from :class:`BrokerUnavailableError` because it is never transient:
43
+ retrying cannot fix it, and continuing to trade on the assumption that things are
44
+ fine is worse than stopping.
45
+ """
46
+
47
+
48
+ class InsufficientFundsError(BrokerError):
49
+ """Buying power or position size cannot support the request."""
50
+
51
+
52
+ class MarketClosedError(BrokerError):
53
+ """The venue rejected the order because the market is not open."""
54
+
55
+
56
+ class OrderRejectedError(BrokerError):
57
+ """The venue refused the order on validation or compliance grounds."""
58
+
59
+
60
+ class DuplicateOrderError(BrokerError):
61
+ """This client order id has already been accepted.
62
+
63
+ Raised rather than returned because it interrupts the caller's flow - but it
64
+ reports a *success*: the order exists at the venue. A caller must not treat it as
65
+ a failed submission and must not resubmit.
66
+ """
67
+
68
+
69
+ class NotTradableError(BrokerError):
70
+ """The symbol cannot be traded on this venue right now."""