langbot-sdk 0.1.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.
- langbot/__init__.py +147 -0
- langbot/adapter.py +310 -0
- langbot/backtest.py +272 -0
- langbot/cli.py +2785 -0
- langbot/client.py +1490 -0
- langbot/config.py +144 -0
- langbot/kalshi.py +890 -0
- langbot/lab/__init__.py +90 -0
- langbot/lab/base.py +346 -0
- langbot/lab/engine.py +228 -0
- langbot/lab/env.py +151 -0
- langbot/lab/fees.py +310 -0
- langbot/lab/history.py +42 -0
- langbot/lab/paper.py +351 -0
- langbot/lab/payrolls.py +257 -0
- langbot/lab/scorecard.py +383 -0
- langbot/lab/strategies.py +524 -0
- langbot/live/__init__.py +76 -0
- langbot/live/audit.py +108 -0
- langbot/live/guard.py +782 -0
- langbot/live/kalshi_venue.py +124 -0
- langbot/live/killswitch.py +95 -0
- langbot/live/ledger.py +304 -0
- langbot/live/limits.py +235 -0
- langbot/live/polymarket_venue.py +141 -0
- langbot/live/venue.py +171 -0
- langbot/notify.py +151 -0
- langbot/polymarket_trading.py +879 -0
- langbot/py.typed +0 -0
- langbot/receipts.py +662 -0
- langbot/runtime.py +650 -0
- langbot/schemas.py +1233 -0
- langbot/sim/__init__.py +153 -0
- langbot/sim/bench.py +297 -0
- langbot/sim/engine.py +1389 -0
- langbot/sim/local.py +112 -0
- langbot/sim/scorecard.py +390 -0
- langbot/sim/stats.py +148 -0
- langbot/sim/strategies.py +367 -0
- langbot/sim/study.py +581 -0
- langbot/strategy.py +691 -0
- langbot/x402.py +276 -0
- langbot_sdk-0.1.0.dist-info/METADATA +410 -0
- langbot_sdk-0.1.0.dist-info/RECORD +47 -0
- langbot_sdk-0.1.0.dist-info/WHEEL +4 -0
- langbot_sdk-0.1.0.dist-info/entry_points.txt +2 -0
- langbot_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
langbot/__init__.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Langbot SDK: data and simulation for every market on Kalshi and Polymarket.
|
|
2
|
+
|
|
3
|
+
Quick start (one strategy protocol for ``langbot run``, ``langbot backtest`` and
|
|
4
|
+
``langbot sim run``)::
|
|
5
|
+
|
|
6
|
+
class Mention:
|
|
7
|
+
markets = ["KXEARNINGSMENTIONNVDA-*"]
|
|
8
|
+
phrases = ["blackwell"]
|
|
9
|
+
|
|
10
|
+
def on_event(self, evt, ctx):
|
|
11
|
+
if evt["type"] == "phrase_detected":
|
|
12
|
+
for ticker in evt["market_refs"]:
|
|
13
|
+
book = ctx.book(ticker)
|
|
14
|
+
if book and book.yes_ask and book.yes_ask < 0.60:
|
|
15
|
+
ctx.buy(ticker, "yes", 10, limit=book.yes_ask, tif="ioc")
|
|
16
|
+
|
|
17
|
+
Run it with ``langbot run strategy.py --mode demo``.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from . import receipts
|
|
21
|
+
from .backtest import Backtester, BacktestResult, backtest_events, backtest_file, load_jsonl
|
|
22
|
+
from .client import (
|
|
23
|
+
Langbot,
|
|
24
|
+
LangbotAPIError,
|
|
25
|
+
LangbotApiError,
|
|
26
|
+
LangbotClient,
|
|
27
|
+
LangbotError,
|
|
28
|
+
StreamRefused,
|
|
29
|
+
)
|
|
30
|
+
from .kalshi import KalshiAuth, KalshiClient, KalshiError, KalshiRateLimiter, TokenBucket
|
|
31
|
+
from .notify import (
|
|
32
|
+
DiscordNotifier,
|
|
33
|
+
MultiNotifier,
|
|
34
|
+
NullNotifier,
|
|
35
|
+
SlackNotifier,
|
|
36
|
+
TelegramNotifier,
|
|
37
|
+
notifier_from_env,
|
|
38
|
+
)
|
|
39
|
+
from .schemas import (
|
|
40
|
+
BaseRateMonth,
|
|
41
|
+
BaseRates,
|
|
42
|
+
Book,
|
|
43
|
+
CalendarEntry,
|
|
44
|
+
Candle,
|
|
45
|
+
CandlesPage,
|
|
46
|
+
Event,
|
|
47
|
+
EventType,
|
|
48
|
+
Fact,
|
|
49
|
+
FactKind,
|
|
50
|
+
Fill,
|
|
51
|
+
LiveEvent,
|
|
52
|
+
Market,
|
|
53
|
+
MarketBrief,
|
|
54
|
+
MarketContext,
|
|
55
|
+
MarketOutcome,
|
|
56
|
+
Mention,
|
|
57
|
+
Order,
|
|
58
|
+
OutcomesPage,
|
|
59
|
+
SourceKind,
|
|
60
|
+
Speaker,
|
|
61
|
+
SpeakerProfile,
|
|
62
|
+
Subscription,
|
|
63
|
+
TextPosted,
|
|
64
|
+
Transcript,
|
|
65
|
+
TranscriptWord,
|
|
66
|
+
Verdict,
|
|
67
|
+
new_event_id,
|
|
68
|
+
new_id,
|
|
69
|
+
now_ms,
|
|
70
|
+
)
|
|
71
|
+
from .strategy import (
|
|
72
|
+
KalshiBroker,
|
|
73
|
+
LiveModeRefused,
|
|
74
|
+
PaperBroker,
|
|
75
|
+
Position,
|
|
76
|
+
RiskRails,
|
|
77
|
+
RiskViolation,
|
|
78
|
+
Strategy,
|
|
79
|
+
)
|
|
80
|
+
from .x402 import PaymentRequired
|
|
81
|
+
|
|
82
|
+
__version__ = "0.1.0"
|
|
83
|
+
|
|
84
|
+
__all__ = [
|
|
85
|
+
"BacktestResult",
|
|
86
|
+
"Backtester",
|
|
87
|
+
"BaseRateMonth",
|
|
88
|
+
"BaseRates",
|
|
89
|
+
"Book",
|
|
90
|
+
"CalendarEntry",
|
|
91
|
+
"DiscordNotifier",
|
|
92
|
+
"Event",
|
|
93
|
+
"EventType",
|
|
94
|
+
"Fact",
|
|
95
|
+
"FactKind",
|
|
96
|
+
"Fill",
|
|
97
|
+
"KalshiAuth",
|
|
98
|
+
"KalshiBroker",
|
|
99
|
+
"KalshiClient",
|
|
100
|
+
"KalshiError",
|
|
101
|
+
"KalshiRateLimiter",
|
|
102
|
+
"Langbot",
|
|
103
|
+
"LangbotAPIError",
|
|
104
|
+
"LangbotApiError",
|
|
105
|
+
"LangbotClient",
|
|
106
|
+
"LangbotError",
|
|
107
|
+
"PaymentRequired",
|
|
108
|
+
"LiveEvent",
|
|
109
|
+
"LiveModeRefused",
|
|
110
|
+
"Market",
|
|
111
|
+
"MarketBrief",
|
|
112
|
+
"MarketContext",
|
|
113
|
+
"MarketOutcome",
|
|
114
|
+
"Mention",
|
|
115
|
+
"MultiNotifier",
|
|
116
|
+
"NullNotifier",
|
|
117
|
+
"Order",
|
|
118
|
+
"OutcomesPage",
|
|
119
|
+
"Candle",
|
|
120
|
+
"CandlesPage",
|
|
121
|
+
"PaperBroker",
|
|
122
|
+
"Position",
|
|
123
|
+
"RiskRails",
|
|
124
|
+
"RiskViolation",
|
|
125
|
+
"SlackNotifier",
|
|
126
|
+
"SourceKind",
|
|
127
|
+
"Speaker",
|
|
128
|
+
"SpeakerProfile",
|
|
129
|
+
"Strategy",
|
|
130
|
+
"StreamRefused",
|
|
131
|
+
"Subscription",
|
|
132
|
+
"TextPosted",
|
|
133
|
+
"TelegramNotifier",
|
|
134
|
+
"TokenBucket",
|
|
135
|
+
"Transcript",
|
|
136
|
+
"TranscriptWord",
|
|
137
|
+
"Verdict",
|
|
138
|
+
"__version__",
|
|
139
|
+
"backtest_events",
|
|
140
|
+
"backtest_file",
|
|
141
|
+
"load_jsonl",
|
|
142
|
+
"new_event_id",
|
|
143
|
+
"new_id",
|
|
144
|
+
"notifier_from_env",
|
|
145
|
+
"now_ms",
|
|
146
|
+
"receipts",
|
|
147
|
+
]
|
langbot/adapter.py
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""One strategy protocol for `langbot sim run`, `langbot run` and `langbot backtest`.
|
|
2
|
+
|
|
3
|
+
A strategy is any object with a synchronous ``on_event(evt, ctx)`` (``evt`` is the wire dict, as
|
|
4
|
+
the stream sends it) and, optionally, ``on_book(ticker, book, ctx)``, ``on_fill(fill, ctx)``,
|
|
5
|
+
``on_start(ctx)`` and ``on_end(ctx)``: the sim's protocol (:mod:`langbot.sim`). ``ctx`` is the
|
|
6
|
+
same surface everywhere: ``now``, ``tickers``, ``book(ticker)``, ``position(ticker)``,
|
|
7
|
+
``orders(ticker)``, ``buy(ticker, side, qty, limit=None, tif=None)``, ``sell(...)``,
|
|
8
|
+
``cancel_all(ticker)``.
|
|
9
|
+
|
|
10
|
+
:func:`adapt` wraps such an object as a :class:`langbot.strategy.Strategy` so the live runner and
|
|
11
|
+
the backtester drive it unchanged: orders a hook places are sent, through the risk rails, as soon
|
|
12
|
+
as the hook returns. The older async ``Strategy`` subclass protocol keeps working, with a
|
|
13
|
+
deprecation warning (:func:`warn_legacy`).
|
|
14
|
+
|
|
15
|
+
Mapping onto the YES book the brokers trade: buying No at ``p`` is offering Yes at ``1 - p``;
|
|
16
|
+
selling No is bidding Yes at ``1 - p``. A market order (``limit=None``) is immediate-or-cancel at
|
|
17
|
+
the top of the book; a limit order rests unless ``tif="ioc"``.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import logging
|
|
23
|
+
import warnings
|
|
24
|
+
from collections.abc import Callable
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
from .schemas import Book, Event, Fill, new_id, now_ms
|
|
29
|
+
from .sim.engine import BookState
|
|
30
|
+
from .sim.engine import Position as SimPosition
|
|
31
|
+
from .strategy import Strategy
|
|
32
|
+
|
|
33
|
+
log = logging.getLogger("langbot.adapter")
|
|
34
|
+
|
|
35
|
+
LEGACY_MESSAGE = (
|
|
36
|
+
"{name}: the async Strategy subclass protocol is deprecated and will be removed in a later "
|
|
37
|
+
"release; write the sim protocol instead: a class with a synchronous on_event(evt, ctx) "
|
|
38
|
+
"(and optional on_book(ticker, book, ctx)), which `langbot run`, `langbot backtest` and "
|
|
39
|
+
"`langbot sim run` all drive. See docs/strategies.md."
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def warn_legacy(cls: type) -> None:
|
|
44
|
+
"""Warn once per class that it uses the old async ``Strategy`` protocol."""
|
|
45
|
+
if getattr(cls, "_langbot_legacy_warned", False):
|
|
46
|
+
return
|
|
47
|
+
cls._langbot_legacy_warned = True # type: ignore[attr-defined]
|
|
48
|
+
msg = LEGACY_MESSAGE.format(name=cls.__name__)
|
|
49
|
+
warnings.warn(msg, DeprecationWarning, stacklevel=3)
|
|
50
|
+
log.warning(msg)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class PendingOrder:
|
|
55
|
+
"""What ``ctx.buy`` / ``ctx.sell`` return live: the order is sent when the hook returns."""
|
|
56
|
+
|
|
57
|
+
ticker: str
|
|
58
|
+
side: str # "yes" | "no"
|
|
59
|
+
action: str # "buy" | "sell"
|
|
60
|
+
qty: float
|
|
61
|
+
limit: float | None
|
|
62
|
+
tif: str | None
|
|
63
|
+
id: str = field(default_factory=lambda: new_id("ord"))
|
|
64
|
+
status: str = "pending"
|
|
65
|
+
order_id: str | None = None # the broker's id once sent
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def open(self) -> bool:
|
|
69
|
+
return self.status in ("pending", "resting")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class LiveContext:
|
|
73
|
+
"""The sim's ``ctx`` over a live (or paper) broker."""
|
|
74
|
+
|
|
75
|
+
def __init__(self, host: SimStrategyAdapter) -> None:
|
|
76
|
+
self._host = host
|
|
77
|
+
self.queue: list[PendingOrder] = []
|
|
78
|
+
self.event_id: str | None = None
|
|
79
|
+
self._now: int | None = None
|
|
80
|
+
self.latency_ms = 0
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def now(self) -> int:
|
|
84
|
+
return self._now if self._now is not None else now_ms()
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def tickers(self) -> list[str]:
|
|
88
|
+
return sorted(self._host.known_tickers)
|
|
89
|
+
|
|
90
|
+
def market(self, ticker: str) -> dict[str, Any] | None:
|
|
91
|
+
return {"ticker": ticker, "title": None} if ticker in self._host.known_tickers else None
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def cash(self) -> float | None:
|
|
95
|
+
"""Unknown live (the venue holds it); ``None``."""
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
def book(self, ticker: str) -> BookState | None:
|
|
99
|
+
b = self._host.books.get(ticker)
|
|
100
|
+
return None if b is None else BookState.from_row(b.model_dump())
|
|
101
|
+
|
|
102
|
+
def position(self, ticker: str) -> SimPosition:
|
|
103
|
+
count = self._host.position(ticker).count
|
|
104
|
+
return SimPosition(ticker, yes=float(max(count, 0)), no=float(max(-count, 0)))
|
|
105
|
+
|
|
106
|
+
def orders(self, ticker: str | None = None) -> list[PendingOrder]:
|
|
107
|
+
return [o for o in self._host.sent if o.open and (ticker is None or o.ticker == ticker)]
|
|
108
|
+
|
|
109
|
+
def buy(
|
|
110
|
+
self,
|
|
111
|
+
ticker: str,
|
|
112
|
+
side: str = "yes",
|
|
113
|
+
qty: float = 1,
|
|
114
|
+
limit: float | None = None,
|
|
115
|
+
tif: str | None = None,
|
|
116
|
+
) -> PendingOrder:
|
|
117
|
+
o = PendingOrder(ticker, side, "buy", qty, limit, tif)
|
|
118
|
+
self.queue.append(o)
|
|
119
|
+
return o
|
|
120
|
+
|
|
121
|
+
def sell(
|
|
122
|
+
self,
|
|
123
|
+
ticker: str,
|
|
124
|
+
side: str = "yes",
|
|
125
|
+
qty: float = 1,
|
|
126
|
+
limit: float | None = None,
|
|
127
|
+
tif: str | None = None,
|
|
128
|
+
) -> PendingOrder:
|
|
129
|
+
o = PendingOrder(ticker, side, "sell", qty, limit, tif)
|
|
130
|
+
self.queue.append(o)
|
|
131
|
+
return o
|
|
132
|
+
|
|
133
|
+
def cancel(self, order_id: str) -> bool:
|
|
134
|
+
before = len(self.queue)
|
|
135
|
+
self.queue = [o for o in self.queue if o.id != order_id]
|
|
136
|
+
return len(self.queue) < before
|
|
137
|
+
|
|
138
|
+
def cancel_all(self, ticker: str | None = None) -> int:
|
|
139
|
+
self._host.cancel_requests.append(ticker)
|
|
140
|
+
return len(self.orders(ticker))
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class SimStrategyAdapter(Strategy):
|
|
144
|
+
"""A sim-protocol strategy object driven as a :class:`Strategy` (see :func:`adapt`)."""
|
|
145
|
+
|
|
146
|
+
factory: Callable[[], Any] = staticmethod(lambda: None) # type: ignore[assignment]
|
|
147
|
+
|
|
148
|
+
def __init__(self) -> None:
|
|
149
|
+
super().__init__()
|
|
150
|
+
self.inner: Any = type(self).factory()
|
|
151
|
+
self.name = getattr(self.inner, "name", None) or type(self.inner).__name__
|
|
152
|
+
self.markets = list(getattr(self.inner, "markets", None) or [])
|
|
153
|
+
self.phrases = list(getattr(self.inner, "phrases", None) or [])
|
|
154
|
+
self.known_tickers: set[str] = set()
|
|
155
|
+
self.sent: list[PendingOrder] = []
|
|
156
|
+
self.cancel_requests: list[str | None] = []
|
|
157
|
+
self.ctx = LiveContext(self)
|
|
158
|
+
|
|
159
|
+
def __getattr__(self, name: str) -> Any:
|
|
160
|
+
# Only called for attributes the adapter does not have: read them off the strategy.
|
|
161
|
+
if name.startswith("__") or name == "inner" or "inner" not in self.__dict__:
|
|
162
|
+
raise AttributeError(name)
|
|
163
|
+
return getattr(self.__dict__["inner"], name)
|
|
164
|
+
|
|
165
|
+
# -------------------------------------------------------------- hooks → sim protocol
|
|
166
|
+
|
|
167
|
+
async def on_start(self) -> None:
|
|
168
|
+
await self._call("on_start", self.ctx)
|
|
169
|
+
|
|
170
|
+
async def on_stop(self) -> None:
|
|
171
|
+
await self._call("on_end", self.ctx)
|
|
172
|
+
|
|
173
|
+
async def on_event(self, evt: Event) -> None:
|
|
174
|
+
wire = evt.to_wire()
|
|
175
|
+
self.ctx._now = evt.t_emitted or evt.t_spoken
|
|
176
|
+
self.ctx.event_id = evt.event_id
|
|
177
|
+
self.known_tickers |= {t for t in evt.market_refs if self.matches_market(t)}
|
|
178
|
+
await self._call("on_event", wire, self.ctx)
|
|
179
|
+
|
|
180
|
+
async def on_book(self, book: Book) -> None:
|
|
181
|
+
if self.matches_market(book.ticker):
|
|
182
|
+
self.known_tickers.add(book.ticker)
|
|
183
|
+
if getattr(self.inner, "on_book", None) is None:
|
|
184
|
+
return
|
|
185
|
+
self.ctx._now = book.ts
|
|
186
|
+
await self._call("on_book", book.ticker, BookState.from_row(book.model_dump()), self.ctx)
|
|
187
|
+
|
|
188
|
+
async def on_fill(self, fill: Fill) -> None:
|
|
189
|
+
for o in self.sent:
|
|
190
|
+
if o.order_id == fill.order_id and o.status == "resting":
|
|
191
|
+
o.status = "filled"
|
|
192
|
+
break
|
|
193
|
+
await self._call("on_fill", fill.model_dump(), self.ctx)
|
|
194
|
+
|
|
195
|
+
# ------------------------------------------------------------------ order flushing
|
|
196
|
+
|
|
197
|
+
async def _call(self, hook: str, *args: Any) -> None:
|
|
198
|
+
fn = getattr(self.inner, hook, None)
|
|
199
|
+
if fn is None:
|
|
200
|
+
return
|
|
201
|
+
fn(*args)
|
|
202
|
+
await self._flush()
|
|
203
|
+
|
|
204
|
+
async def _flush(self) -> None:
|
|
205
|
+
while self.cancel_requests:
|
|
206
|
+
await self.cancel_all(self.cancel_requests.pop(0))
|
|
207
|
+
for o in self.sent:
|
|
208
|
+
o.status = "canceled" if o.open else o.status
|
|
209
|
+
queue, self.ctx.queue = self.ctx.queue, []
|
|
210
|
+
for o in queue:
|
|
211
|
+
await self._send(o)
|
|
212
|
+
|
|
213
|
+
async def _send(self, o: PendingOrder) -> None:
|
|
214
|
+
book = self.books.get(o.ticker)
|
|
215
|
+
# (YES-book side, YES price) for each intent
|
|
216
|
+
yes_buy = (o.action == "buy") == (o.side == "yes")
|
|
217
|
+
if o.limit is not None:
|
|
218
|
+
price = o.limit if o.side == "yes" else round(1.0 - o.limit, 4)
|
|
219
|
+
elif book is not None:
|
|
220
|
+
price = book.yes_ask if yes_buy else book.yes_bid
|
|
221
|
+
else:
|
|
222
|
+
price = None
|
|
223
|
+
if price is None:
|
|
224
|
+
o.status = "rejected"
|
|
225
|
+
self.log(f"no price for {o.action} {o.side} {o.ticker}: no limit and no book")
|
|
226
|
+
return
|
|
227
|
+
tif = "immediate_or_cancel" if (o.limit is None or o.tif == "ioc") else "good_till_canceled"
|
|
228
|
+
count = int(round(o.qty))
|
|
229
|
+
order = await (self.buy_yes if yes_buy else self.sell_yes)(
|
|
230
|
+
o.ticker, price, count, time_in_force=tif
|
|
231
|
+
)
|
|
232
|
+
o.order_id = getattr(order, "order_id", None)
|
|
233
|
+
status = str(getattr(order, "status", "")).lower()
|
|
234
|
+
o.status = "resting" if status == "resting" else "done"
|
|
235
|
+
self.sent.append(o)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _class_constants(cls: type) -> dict[str, Any]:
|
|
239
|
+
"""The public, non-callable class attributes of ``cls`` and its bases (``markets``,
|
|
240
|
+
``phrases``, thresholds ...), so the adapted class reads like the strategy it wraps."""
|
|
241
|
+
out: dict[str, Any] = {}
|
|
242
|
+
for klass in reversed(cls.__mro__[:-1]): # skip object
|
|
243
|
+
for k, v in vars(klass).items():
|
|
244
|
+
if (
|
|
245
|
+
k.startswith("_")
|
|
246
|
+
or callable(v)
|
|
247
|
+
or isinstance(v, (property, staticmethod, classmethod))
|
|
248
|
+
):
|
|
249
|
+
continue
|
|
250
|
+
if k in _ADAPTER_OWN:
|
|
251
|
+
continue
|
|
252
|
+
out[k] = _FromInner(k, v)
|
|
253
|
+
return out
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class _FromInner:
|
|
257
|
+
"""A class constant of the wrapped strategy: the class value on the adapted class, the
|
|
258
|
+
wrapped instance's current value on an adapter instance."""
|
|
259
|
+
|
|
260
|
+
def __init__(self, key: str, default: Any) -> None:
|
|
261
|
+
self.key, self.default = key, default
|
|
262
|
+
|
|
263
|
+
def __get__(self, obj: Any, owner: type | None = None) -> Any:
|
|
264
|
+
if obj is None or "inner" not in obj.__dict__:
|
|
265
|
+
return self.default
|
|
266
|
+
return getattr(obj.__dict__["inner"], self.key, self.default)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def adapt(factory: Callable[[], Any], name: str | None = None) -> type[Strategy]:
|
|
270
|
+
"""A :class:`Strategy` subclass that builds the sim-protocol strategy from ``factory`` and
|
|
271
|
+
drives it (the live runner and the backtester instantiate it with no arguments).
|
|
272
|
+
|
|
273
|
+
When ``factory`` is the strategy class itself, the adapted class takes its name and its
|
|
274
|
+
class constants (``markets``, ``phrases``, ``max_price`` ...); instances read any other
|
|
275
|
+
attribute (``ended``, a ``price_ceiling`` property) from the wrapped strategy."""
|
|
276
|
+
import inspect
|
|
277
|
+
|
|
278
|
+
attrs: dict[str, Any] = {"factory": staticmethod(factory), "__module__": __name__}
|
|
279
|
+
if inspect.isclass(factory):
|
|
280
|
+
cls_name = factory.__name__
|
|
281
|
+
attrs.update(_class_constants(factory))
|
|
282
|
+
attrs["__doc__"] = factory.__doc__
|
|
283
|
+
else:
|
|
284
|
+
cls_name = f"{name or getattr(factory, '__name__', None) or 'SimStrategy'}Live"
|
|
285
|
+
return type(cls_name, (SimStrategyAdapter,), attrs)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
_ADAPTER_OWN = frozenset(
|
|
289
|
+
{"factory", "name", "inner", "ctx", "known_tickers", "sent", "cancel_requests"}
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def is_sim_protocol(obj: Any) -> bool:
|
|
294
|
+
"""True for a class or object whose ``on_event`` is synchronous (the sim protocol)."""
|
|
295
|
+
import inspect
|
|
296
|
+
|
|
297
|
+
if inspect.isclass(obj) and issubclass(obj, Strategy):
|
|
298
|
+
return False
|
|
299
|
+
fn = getattr(obj, "on_event", None)
|
|
300
|
+
return fn is not None and not inspect.iscoroutinefunction(fn)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
__all__ = [
|
|
304
|
+
"LiveContext",
|
|
305
|
+
"PendingOrder",
|
|
306
|
+
"SimStrategyAdapter",
|
|
307
|
+
"adapt",
|
|
308
|
+
"is_sim_protocol",
|
|
309
|
+
"warn_legacy",
|
|
310
|
+
]
|