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
quantred/brokers/base.py
ADDED
|
@@ -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."""
|