arvo-client 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.
- arvo/__init__.py +468 -0
- arvo/common/__init__.py +0 -0
- arvo/common/v1/__init__.py +0 -0
- arvo/common/v1/models_pb2.py +38 -0
- arvo/common/v1/models_pb2_grpc.py +24 -0
- arvo/market/__init__.py +0 -0
- arvo/market/v1/__init__.py +0 -0
- arvo/market/v1/models_pb2.py +46 -0
- arvo/market/v1/models_pb2_grpc.py +24 -0
- arvo/market/v1/views_pb2.py +56 -0
- arvo/market/v1/views_pb2_grpc.py +24 -0
- arvo/platform/__init__.py +0 -0
- arvo/platform/v1/__init__.py +0 -0
- arvo/platform/v1/models_pb2.py +50 -0
- arvo/platform/v1/models_pb2_grpc.py +24 -0
- arvo/platform/v1/views_pb2.py +90 -0
- arvo/platform/v1/views_pb2_grpc.py +24 -0
- arvo/portfolio/__init__.py +0 -0
- arvo/portfolio/v1/__init__.py +0 -0
- arvo/portfolio/v1/models_pb2.py +36 -0
- arvo/portfolio/v1/models_pb2_grpc.py +24 -0
- arvo/portfolio/v1/views_pb2.py +48 -0
- arvo/portfolio/v1/views_pb2_grpc.py +24 -0
- arvo/py.typed +0 -0
- arvo/research/__init__.py +0 -0
- arvo/research/v1/__init__.py +0 -0
- arvo/research/v1/models_pb2.py +100 -0
- arvo/research/v1/models_pb2_grpc.py +24 -0
- arvo/research/v1/views_pb2.py +110 -0
- arvo/research/v1/views_pb2_grpc.py +24 -0
- arvo/services/__init__.py +0 -0
- arvo/services/v1/__init__.py +0 -0
- arvo/services/v1/accounts_pb2.py +39 -0
- arvo/services/v1/accounts_pb2_grpc.py +246 -0
- arvo/services/v1/market_pb2.py +39 -0
- arvo/services/v1/market_pb2_grpc.py +375 -0
- arvo/services/v1/platform_pb2.py +39 -0
- arvo/services/v1/platform_pb2_grpc.py +377 -0
- arvo/services/v1/portfolio_pb2.py +39 -0
- arvo/services/v1/portfolio_pb2_grpc.py +202 -0
- arvo/services/v1/research_pb2.py +43 -0
- arvo/services/v1/research_pb2_grpc.py +1372 -0
- arvo/services/v1/scripts_pb2.py +39 -0
- arvo/services/v1/scripts_pb2_grpc.py +357 -0
- arvo/services/v1/sessions_pb2.py +38 -0
- arvo/services/v1/sessions_pb2_grpc.py +369 -0
- arvo/session/__init__.py +0 -0
- arvo/session/v1/__init__.py +0 -0
- arvo/session/v1/models_pb2.py +44 -0
- arvo/session/v1/models_pb2_grpc.py +24 -0
- arvo_client-0.1.0.dist-info/METADATA +115 -0
- arvo_client-0.1.0.dist-info/RECORD +53 -0
- arvo_client-0.1.0.dist-info/WHEEL +4 -0
arvo/__init__.py
ADDED
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
"""Arvo's research from Python.
|
|
2
|
+
|
|
3
|
+
import arvo
|
|
4
|
+
|
|
5
|
+
engine = arvo.connect()
|
|
6
|
+
for plan in engine.strategies():
|
|
7
|
+
print(plan.name, plan.premise)
|
|
8
|
+
|
|
9
|
+
found = engine.run_study("AAPL.RH", "sma_cross", author="script:first-look")
|
|
10
|
+
print(found.verdict, found.read_this_first)
|
|
11
|
+
for item in found.advice:
|
|
12
|
+
print(item.severity, item.finding)
|
|
13
|
+
|
|
14
|
+
prices = engine.bars("AAPL.RH")
|
|
15
|
+
|
|
16
|
+
This talks to a running ``arvo-engine`` through its research API (ADR-0018),
|
|
17
|
+
which reads research memory and runs research. Nothing here fetches data,
|
|
18
|
+
touches a credential or places an order, and nothing can: the API has no call
|
|
19
|
+
that would.
|
|
20
|
+
|
|
21
|
+
Every run needs an ``author``. It is saved as that author's finding and held to
|
|
22
|
+
the no-skill bar for **everything that author has run**, so a script that
|
|
23
|
+
tries configurations until one passes does not make it pass. Read ``verdict``,
|
|
24
|
+
``read_this_first`` and ``advice`` before any number in ``detail``.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import json
|
|
30
|
+
import os
|
|
31
|
+
import re
|
|
32
|
+
from dataclasses import dataclass, field
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
from typing import Any, Sequence
|
|
35
|
+
|
|
36
|
+
import grpc
|
|
37
|
+
|
|
38
|
+
# The contract is split by domain (arvo-engine-api): one package per subject,
|
|
39
|
+
# holding its models and its views, with the services apart from them. `pb`
|
|
40
|
+
# keeps its old name so the rest of this module reads the same.
|
|
41
|
+
from arvo.common.v1 import models_pb2 as common
|
|
42
|
+
from arvo.research.v1 import models_pb2 as research
|
|
43
|
+
from arvo.services.v1 import research_pb2_grpc as rpc
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class _Pb:
|
|
47
|
+
"""The shapes this client sends, from whichever package defines them."""
|
|
48
|
+
|
|
49
|
+
Empty = common.Empty
|
|
50
|
+
AttachRequest = research.AttachRequest
|
|
51
|
+
FindingId = research.FindingId
|
|
52
|
+
LedgerTrade = research.LedgerTrade
|
|
53
|
+
Point = research.Point
|
|
54
|
+
ReportRequest = research.ReportRequest
|
|
55
|
+
RunRequest = research.RunRequest
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
pb = _Pb()
|
|
59
|
+
|
|
60
|
+
__all__ = [
|
|
61
|
+
"Advice",
|
|
62
|
+
"ArvoError",
|
|
63
|
+
"Attachment",
|
|
64
|
+
"Engine",
|
|
65
|
+
"Finding",
|
|
66
|
+
"FindingSummary",
|
|
67
|
+
"Instrument",
|
|
68
|
+
"Strategy",
|
|
69
|
+
"connect",
|
|
70
|
+
"default_root",
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
DAILY = "1day"
|
|
74
|
+
"""The interval name of daily bars, as the engine spells it."""
|
|
75
|
+
|
|
76
|
+
_SAFE_INSTRUMENT = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class ArvoError(RuntimeError):
|
|
80
|
+
"""A call the engine refused or could not complete, with its reason."""
|
|
81
|
+
|
|
82
|
+
def __init__(self, message: str, code: str = "") -> None:
|
|
83
|
+
super().__init__(message)
|
|
84
|
+
self.code = code
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass(frozen=True)
|
|
88
|
+
class Strategy:
|
|
89
|
+
name: str
|
|
90
|
+
label: str
|
|
91
|
+
interval: str
|
|
92
|
+
premise: str
|
|
93
|
+
ranks_a_set: bool
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass(frozen=True)
|
|
97
|
+
class Instrument:
|
|
98
|
+
id: str
|
|
99
|
+
interval: str
|
|
100
|
+
start: str
|
|
101
|
+
end: str
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass(frozen=True)
|
|
105
|
+
class FindingSummary:
|
|
106
|
+
id: str
|
|
107
|
+
kind: str
|
|
108
|
+
subject: str
|
|
109
|
+
verdict: str
|
|
110
|
+
recorded_at: str
|
|
111
|
+
author: str
|
|
112
|
+
"""The agent or script that ran it; empty for a person."""
|
|
113
|
+
strategy: str = ""
|
|
114
|
+
"""Which rule or ruleset ran, by the name ``strategies()`` shows."""
|
|
115
|
+
code_commit: str = ""
|
|
116
|
+
"""The engine build that produced it: a git commit, ``-dirty`` when the
|
|
117
|
+
tree had uncommitted changes, ``unknown`` outside a checkout, empty for a
|
|
118
|
+
finding recorded before builds were stamped."""
|
|
119
|
+
ruleset_hash: str = ""
|
|
120
|
+
"""The ruleset document's hash at run time, when the strategy was a
|
|
121
|
+
ruleset rather than a shipped rule. Two findings with different hashes
|
|
122
|
+
measured different rules, whatever the ruleset is called."""
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@dataclass(frozen=True)
|
|
126
|
+
class Advice:
|
|
127
|
+
severity: str
|
|
128
|
+
finding: str
|
|
129
|
+
action: str
|
|
130
|
+
evidence: str
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass(frozen=True)
|
|
134
|
+
class Attachment:
|
|
135
|
+
"""One file kept with a finding: bytes stored once by content hash."""
|
|
136
|
+
|
|
137
|
+
name: str
|
|
138
|
+
media_type: str
|
|
139
|
+
hash: str
|
|
140
|
+
bytes: int
|
|
141
|
+
added_at: str
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@dataclass(frozen=True)
|
|
145
|
+
class Finding:
|
|
146
|
+
id: str
|
|
147
|
+
kind: str
|
|
148
|
+
subject: str
|
|
149
|
+
verdict: str
|
|
150
|
+
recorded_at: str
|
|
151
|
+
author: str
|
|
152
|
+
read_this_first: str
|
|
153
|
+
"""How far the verdict may be read. Read before any number."""
|
|
154
|
+
reasons: list[str]
|
|
155
|
+
advice: list[Advice]
|
|
156
|
+
strategy: str = ""
|
|
157
|
+
code_commit: str = ""
|
|
158
|
+
ruleset_hash: str = ""
|
|
159
|
+
detail: dict[str, Any] = field(default_factory=dict)
|
|
160
|
+
"""The numbers, which differ by kind: ``search`` and ``out_of_sample`` for
|
|
161
|
+
a study, ``combined`` for a walk-forward, ``pooled`` for a panel."""
|
|
162
|
+
attachments: list[Attachment] = field(default_factory=list)
|
|
163
|
+
"""Files kept with it, in the order they were attached."""
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def default_root() -> Path:
|
|
167
|
+
"""The Arvo app data directory, ``%APPDATA%/com.arvo.desktop``."""
|
|
168
|
+
appdata = os.environ.get("APPDATA")
|
|
169
|
+
if not appdata:
|
|
170
|
+
raise ArvoError("APPDATA is not set; pass the Arvo app data directory to connect()")
|
|
171
|
+
return Path(appdata) / "com.arvo.desktop"
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def connect(root: str | os.PathLike[str] | None = None) -> Engine:
|
|
175
|
+
"""Connects to the engine running for this app data directory.
|
|
176
|
+
|
|
177
|
+
Reads ``engine.json``, which the engine writes when it starts. Raises
|
|
178
|
+
:class:`ArvoError` when no engine has written one.
|
|
179
|
+
"""
|
|
180
|
+
base = Path(root) if root is not None else default_root()
|
|
181
|
+
try:
|
|
182
|
+
found = json.loads((base / "engine.json").read_text(encoding="utf-8"))
|
|
183
|
+
except FileNotFoundError:
|
|
184
|
+
raise ArvoError(
|
|
185
|
+
f"no engine.json in {base}; start arvo-engine first"
|
|
186
|
+
) from None
|
|
187
|
+
return Engine(base, found["address"], found["token"])
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class Engine:
|
|
191
|
+
"""One engine's research API, and read-only access to its bar library."""
|
|
192
|
+
|
|
193
|
+
def __init__(self, root: Path, address: str, token: str) -> None:
|
|
194
|
+
self.root = root
|
|
195
|
+
# Room for a figure or a trades table attached to a finding; the
|
|
196
|
+
# engine accepts the same.
|
|
197
|
+
limit = 64 << 20
|
|
198
|
+
self._channel = grpc.insecure_channel(
|
|
199
|
+
address,
|
|
200
|
+
options=[("grpc.max_send_message_length", limit), ("grpc.max_receive_message_length", limit)],
|
|
201
|
+
)
|
|
202
|
+
self._stub = rpc.ResearchStub(self._channel)
|
|
203
|
+
self._metadata = (("authorization", f"Bearer {token}"),)
|
|
204
|
+
|
|
205
|
+
def close(self) -> None:
|
|
206
|
+
self._channel.close()
|
|
207
|
+
|
|
208
|
+
def __enter__(self) -> Engine:
|
|
209
|
+
return self
|
|
210
|
+
|
|
211
|
+
def __exit__(self, *_: object) -> None:
|
|
212
|
+
self.close()
|
|
213
|
+
|
|
214
|
+
def _call(self, method: Any, request: Any) -> Any:
|
|
215
|
+
try:
|
|
216
|
+
return method(request, metadata=self._metadata)
|
|
217
|
+
except grpc.RpcError as err:
|
|
218
|
+
code = err.code().name if hasattr(err, "code") else ""
|
|
219
|
+
details = err.details() if hasattr(err, "details") else str(err)
|
|
220
|
+
raise ArvoError(details or code, code) from None
|
|
221
|
+
|
|
222
|
+
def strategies(self) -> list[Strategy]:
|
|
223
|
+
"""The rules Arvo can test."""
|
|
224
|
+
reply = self._call(self._stub.ListStrategies, pb.Empty())
|
|
225
|
+
return [
|
|
226
|
+
Strategy(s.name, s.label, s.interval, s.premise, s.ranks_a_set)
|
|
227
|
+
for s in reply.strategies
|
|
228
|
+
]
|
|
229
|
+
|
|
230
|
+
def instruments(self) -> list[Instrument]:
|
|
231
|
+
"""Instruments with bars in the library, by interval and date range."""
|
|
232
|
+
reply = self._call(self._stub.ListInstruments, pb.Empty())
|
|
233
|
+
return [Instrument(i.id, i.interval, getattr(i, "from"), i.to) for i in reply.instruments]
|
|
234
|
+
|
|
235
|
+
def findings(self) -> list[FindingSummary]:
|
|
236
|
+
"""Every finding in research memory."""
|
|
237
|
+
reply = self._call(self._stub.ListFindings, pb.Empty())
|
|
238
|
+
return [
|
|
239
|
+
FindingSummary(
|
|
240
|
+
f.id, f.kind, f.subject, f.verdict, f.recorded_at, f.author,
|
|
241
|
+
f.strategy, f.code_commit, f.ruleset_hash,
|
|
242
|
+
)
|
|
243
|
+
for f in reply.findings
|
|
244
|
+
]
|
|
245
|
+
|
|
246
|
+
def finding(self, id: str) -> Finding:
|
|
247
|
+
"""One finding: verdict, how far to read it, reasons, advice, numbers."""
|
|
248
|
+
return _finding(self._call(self._stub.OpenFinding, pb.FindingId(id=id)))
|
|
249
|
+
|
|
250
|
+
def run_study(self, instrument: str, strategy: str, *, author: str) -> Finding:
|
|
251
|
+
"""Searches ``strategy``'s grid on ``instrument``, chooses in-sample and
|
|
252
|
+
judges out-of-sample. Saved as ``author``'s finding and deflated against
|
|
253
|
+
everything ``author`` has run."""
|
|
254
|
+
request = pb.RunRequest(instrument=instrument, strategy=strategy, author=author, origin=_caller())
|
|
255
|
+
return _finding(self._call(self._stub.RunStudy, request))
|
|
256
|
+
|
|
257
|
+
def run_walk_forward(self, instrument: str, strategy: str, *, author: str) -> Finding:
|
|
258
|
+
"""Re-selects on a rolling schedule across the whole history and judges
|
|
259
|
+
the stitched out-of-sample record. Slower than :meth:`run_study`."""
|
|
260
|
+
request = pb.RunRequest(instrument=instrument, strategy=strategy, author=author, origin=_caller())
|
|
261
|
+
return _finding(self._call(self._stub.RunWalkForward, request))
|
|
262
|
+
|
|
263
|
+
def record(
|
|
264
|
+
self,
|
|
265
|
+
*,
|
|
266
|
+
author: str,
|
|
267
|
+
hypothesis: str,
|
|
268
|
+
claim: str,
|
|
269
|
+
instrument: str,
|
|
270
|
+
window: tuple[str, str],
|
|
271
|
+
interval: str,
|
|
272
|
+
dataset: tuple[str, str],
|
|
273
|
+
strategy: str,
|
|
274
|
+
engine: str,
|
|
275
|
+
curve: Sequence[tuple[Any, float]],
|
|
276
|
+
ledger: Sequence[dict[str, Any]] = (),
|
|
277
|
+
benchmark_curve: Sequence[tuple[Any, float]] | None = None,
|
|
278
|
+
params: dict[str, float] | None = None,
|
|
279
|
+
costs_bps: tuple[float, float] = (0.0, 0.0),
|
|
280
|
+
per_fill: float = 0.0,
|
|
281
|
+
stop_atr_multiple: float | None = None,
|
|
282
|
+
starting_cash: float = 0.0,
|
|
283
|
+
adjustment: str = "split",
|
|
284
|
+
trials: int | None = None,
|
|
285
|
+
) -> Finding:
|
|
286
|
+
"""Records a result this script computed itself, judged by Arvo.
|
|
287
|
+
|
|
288
|
+
What a run produces, before anyone concludes anything from it: the
|
|
289
|
+
experiment as data, the strategy's equity ``curve`` and trade
|
|
290
|
+
``ledger``, a ``benchmark_curve`` when there is one, and how many
|
|
291
|
+
``trials`` the whole search has run. Arvo computes the verdict from
|
|
292
|
+
that with the criteria a study uses. There is no argument for a
|
|
293
|
+
verdict, a metric or a p-value, on purpose.
|
|
294
|
+
|
|
295
|
+
``curve`` and ``benchmark_curve`` are ``(when, equity)`` pairs, where
|
|
296
|
+
``when`` is a ``datetime``, a ``date`` or a string like
|
|
297
|
+
``2026-01-31T09:30:00``. ``ledger`` rows are dicts with ``opened``,
|
|
298
|
+
``closed`` (omit while open), ``direction`` (``long``/``short``),
|
|
299
|
+
``quantity``, ``entry``, ``exit``, ``pnl``, ``commission`` and
|
|
300
|
+
``exit_reason`` (``signal``, ``stop``, ``halted``, ``expired``,
|
|
301
|
+
``still_open``), and optionally what the rule saw when it entered:
|
|
302
|
+
``rule`` (the condition, in words), ``signal`` (the value it was
|
|
303
|
+
judged on), ``regime`` and ``asked`` (the quantity before the gate). ``dataset`` is ``(id, version)``, the version being
|
|
304
|
+
a content hash of what the engine read. Without a benchmark the
|
|
305
|
+
finding is Inconclusive: a return with nothing to beat is not a
|
|
306
|
+
result.
|
|
307
|
+
"""
|
|
308
|
+
trades = [
|
|
309
|
+
pb.LedgerTrade(
|
|
310
|
+
instrument=str(row.get("instrument", "")),
|
|
311
|
+
opened=_when(row["opened"]),
|
|
312
|
+
closed=_when(row["closed"]) if row.get("closed") else "",
|
|
313
|
+
direction=str(row.get("direction", "long")),
|
|
314
|
+
quantity=float(row.get("quantity", 0.0)),
|
|
315
|
+
entry=float(row.get("entry", 0.0)),
|
|
316
|
+
pnl=float(row.get("pnl", 0.0)),
|
|
317
|
+
commission=float(row.get("commission", 0.0)),
|
|
318
|
+
exit_reason=str(row.get("exit_reason", "signal")),
|
|
319
|
+
**({"exit": float(row["exit"])} if row.get("exit") is not None else {}),
|
|
320
|
+
**({"rule": str(row["rule"])} if row.get("rule") is not None else {}),
|
|
321
|
+
**({"signal": float(row["signal"])} if row.get("signal") is not None else {}),
|
|
322
|
+
**({"regime": str(row["regime"])} if row.get("regime") is not None else {}),
|
|
323
|
+
**({"asked": float(row["asked"])} if row.get("asked") is not None else {}),
|
|
324
|
+
)
|
|
325
|
+
for row in ledger
|
|
326
|
+
]
|
|
327
|
+
request = pb.ReportRequest(
|
|
328
|
+
author=author,
|
|
329
|
+
origin=_caller(),
|
|
330
|
+
hypothesis_id=hypothesis,
|
|
331
|
+
claim=claim,
|
|
332
|
+
instrument=instrument,
|
|
333
|
+
interval=interval,
|
|
334
|
+
dataset_id=dataset[0],
|
|
335
|
+
dataset_version=dataset[1],
|
|
336
|
+
adjustment=adjustment,
|
|
337
|
+
strategy=strategy,
|
|
338
|
+
params=dict(params or {}),
|
|
339
|
+
commission_bps=costs_bps[0],
|
|
340
|
+
slippage_bps=costs_bps[1],
|
|
341
|
+
per_fill=per_fill,
|
|
342
|
+
starting_cash=starting_cash,
|
|
343
|
+
engine=engine,
|
|
344
|
+
strategy_curve=[pb.Point(at=_when(at), equity=float(equity)) for at, equity in curve],
|
|
345
|
+
ledger=trades,
|
|
346
|
+
benchmark_curve=[pb.Point(at=_when(at), equity=float(equity)) for at, equity in (benchmark_curve or [])],
|
|
347
|
+
**({"trials": int(trials)} if trials is not None else {}),
|
|
348
|
+
**({"stop_atr_multiple": float(stop_atr_multiple)} if stop_atr_multiple is not None else {}),
|
|
349
|
+
)
|
|
350
|
+
# `from` is a keyword in Python; the field is set by name.
|
|
351
|
+
setattr(request, "from", window[0])
|
|
352
|
+
request.to = window[1]
|
|
353
|
+
return _finding(self._call(self._stub.RecordFinding, request))
|
|
354
|
+
|
|
355
|
+
def attach(
|
|
356
|
+
self,
|
|
357
|
+
finding_id: str,
|
|
358
|
+
path: str | os.PathLike[str] | None = None,
|
|
359
|
+
*,
|
|
360
|
+
data: bytes | None = None,
|
|
361
|
+
name: str | None = None,
|
|
362
|
+
media_type: str | None = None,
|
|
363
|
+
) -> list[Attachment]:
|
|
364
|
+
"""Keeps a file with a finding: a report, a figure, the trades.
|
|
365
|
+
|
|
366
|
+
Give a ``path`` to read, or ``data`` with a ``name``. The bytes are
|
|
367
|
+
stored once by content hash and the finding lists them; attaching the
|
|
368
|
+
same file under the same name again changes nothing. Returns every
|
|
369
|
+
attachment the finding now has. ``media_type`` is guessed from the
|
|
370
|
+
name when not given.
|
|
371
|
+
"""
|
|
372
|
+
if path is not None:
|
|
373
|
+
file = Path(path)
|
|
374
|
+
payload = file.read_bytes()
|
|
375
|
+
name = name or file.name
|
|
376
|
+
elif data is not None:
|
|
377
|
+
payload = data
|
|
378
|
+
else:
|
|
379
|
+
raise ArvoError("attach needs a path or data")
|
|
380
|
+
if not name:
|
|
381
|
+
raise ArvoError("attach needs a name for the file")
|
|
382
|
+
if media_type is None:
|
|
383
|
+
media_type = _MEDIA_TYPES.get(Path(name).suffix.lower(), "application/octet-stream")
|
|
384
|
+
request = pb.AttachRequest(finding_id=finding_id, name=name, media_type=media_type, data=payload)
|
|
385
|
+
reply = self._call(self._stub.AttachFile, request)
|
|
386
|
+
return [_attachment(a) for a in reply.attachments]
|
|
387
|
+
|
|
388
|
+
def bars(self, instrument: str, interval: str = DAILY) -> Any:
|
|
389
|
+
"""The library's bars for ``instrument`` as a pandas DataFrame indexed
|
|
390
|
+
by time, read straight from the engine's files.
|
|
391
|
+
|
|
392
|
+
Read-only. Stocks and funds only; an option contract is filed under its
|
|
393
|
+
underlying and is not read here.
|
|
394
|
+
"""
|
|
395
|
+
import pandas as pd
|
|
396
|
+
|
|
397
|
+
if not _SAFE_INSTRUMENT.fullmatch(instrument) or ".." in instrument:
|
|
398
|
+
raise ArvoError(f"{instrument!r} is not an instrument id")
|
|
399
|
+
if not re.fullmatch(r"[0-9]+[a-z]+", interval):
|
|
400
|
+
raise ArvoError(f"{interval!r} is not an interval, e.g. '1day' or '5minute'")
|
|
401
|
+
data = self.root / "data"
|
|
402
|
+
folder = data if interval == DAILY else data / interval
|
|
403
|
+
path = folder / f"{instrument}.csv"
|
|
404
|
+
if not path.is_file():
|
|
405
|
+
raise ArvoError(f"no {interval} bars for {instrument} in {folder}")
|
|
406
|
+
frame = pd.read_csv(path, parse_dates=["date"], index_col="date")
|
|
407
|
+
return frame
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _caller() -> str:
|
|
411
|
+
"""Where the script asked for a run, as ``path:line``: the frame above the
|
|
412
|
+
client's own method. The workbench shows the finding's caveats against
|
|
413
|
+
that line. Empty when there is no such frame."""
|
|
414
|
+
import inspect
|
|
415
|
+
|
|
416
|
+
frames = inspect.stack()
|
|
417
|
+
if len(frames) < 3:
|
|
418
|
+
return ""
|
|
419
|
+
caller = frames[2]
|
|
420
|
+
return f"{os.path.abspath(caller.filename)}:{caller.lineno}"
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _when(value: Any) -> str:
|
|
424
|
+
"""A point in time as the engine reads it: ``2026-01-31T09:30:00``, or a
|
|
425
|
+
date. A ``datetime``, a ``date``, a pandas ``Timestamp`` or a string."""
|
|
426
|
+
if isinstance(value, str):
|
|
427
|
+
return value
|
|
428
|
+
if hasattr(value, "strftime"):
|
|
429
|
+
if hasattr(value, "hour"):
|
|
430
|
+
return value.strftime("%Y-%m-%dT%H:%M:%S")
|
|
431
|
+
return value.strftime("%Y-%m-%d")
|
|
432
|
+
return str(value)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
_MEDIA_TYPES = {
|
|
436
|
+
".md": "text/markdown",
|
|
437
|
+
".csv": "text/csv",
|
|
438
|
+
".json": "application/json",
|
|
439
|
+
".png": "image/png",
|
|
440
|
+
".svg": "image/svg+xml",
|
|
441
|
+
".txt": "text/plain",
|
|
442
|
+
".html": "text/html",
|
|
443
|
+
".pdf": "application/pdf",
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def _attachment(kept: Any) -> Attachment:
|
|
448
|
+
return Attachment(kept.name, kept.media_type, kept.hash, int(kept.bytes), kept.added_at)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def _finding(reply: Any) -> Finding:
|
|
452
|
+
summary = reply.summary
|
|
453
|
+
return Finding(
|
|
454
|
+
id=summary.id,
|
|
455
|
+
kind=summary.kind,
|
|
456
|
+
subject=summary.subject,
|
|
457
|
+
verdict=summary.verdict,
|
|
458
|
+
recorded_at=summary.recorded_at,
|
|
459
|
+
author=summary.author,
|
|
460
|
+
read_this_first=reply.read_this_first,
|
|
461
|
+
reasons=list(reply.reasons),
|
|
462
|
+
advice=[Advice(a.severity, a.finding, a.action, a.evidence) for a in reply.advice],
|
|
463
|
+
strategy=summary.strategy,
|
|
464
|
+
code_commit=summary.code_commit,
|
|
465
|
+
ruleset_hash=summary.ruleset_hash,
|
|
466
|
+
detail=json.loads(reply.detail_json) if reply.detail_json else {},
|
|
467
|
+
attachments=[_attachment(a) for a in reply.attachments],
|
|
468
|
+
)
|
arvo/common/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
3
|
+
# NO CHECKED-IN PROTOBUF GENCODE
|
|
4
|
+
# source: arvo/common/v1/models.proto
|
|
5
|
+
# Protobuf Python Version: 6.31.1
|
|
6
|
+
"""Generated protocol buffer code."""
|
|
7
|
+
from google.protobuf import descriptor as _descriptor
|
|
8
|
+
from google.protobuf import descriptor_pool as _descriptor_pool
|
|
9
|
+
from google.protobuf import runtime_version as _runtime_version
|
|
10
|
+
from google.protobuf import symbol_database as _symbol_database
|
|
11
|
+
from google.protobuf.internal import builder as _builder
|
|
12
|
+
_runtime_version.ValidateProtobufRuntimeVersion(
|
|
13
|
+
_runtime_version.Domain.PUBLIC,
|
|
14
|
+
6,
|
|
15
|
+
31,
|
|
16
|
+
1,
|
|
17
|
+
'',
|
|
18
|
+
'arvo/common/v1/models.proto'
|
|
19
|
+
)
|
|
20
|
+
# @@protoc_insertion_point(imports)
|
|
21
|
+
|
|
22
|
+
_sym_db = _symbol_database.Default()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1b\x61rvo/common/v1/models.proto\x12\x0e\x61rvo.common.v1\"\x07\n\x05\x45mpty\"\x1c\n\x0c\x45xportedPath\x12\x0c\n\x04path\x18\x01 \x01(\tb\x06proto3')
|
|
28
|
+
|
|
29
|
+
_globals = globals()
|
|
30
|
+
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
|
31
|
+
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'arvo.common.v1.models_pb2', _globals)
|
|
32
|
+
if not _descriptor._USE_C_DESCRIPTORS:
|
|
33
|
+
DESCRIPTOR._loaded_options = None
|
|
34
|
+
_globals['_EMPTY']._serialized_start=47
|
|
35
|
+
_globals['_EMPTY']._serialized_end=54
|
|
36
|
+
_globals['_EXPORTEDPATH']._serialized_start=56
|
|
37
|
+
_globals['_EXPORTEDPATH']._serialized_end=84
|
|
38
|
+
# @@protoc_insertion_point(module_scope)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
|
|
2
|
+
"""Client and server classes corresponding to protobuf-defined services."""
|
|
3
|
+
import grpc
|
|
4
|
+
import warnings
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
GRPC_GENERATED_VERSION = '1.75.1'
|
|
8
|
+
GRPC_VERSION = grpc.__version__
|
|
9
|
+
_version_not_supported = False
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
from grpc._utilities import first_version_is_lower
|
|
13
|
+
_version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
|
|
14
|
+
except ImportError:
|
|
15
|
+
_version_not_supported = True
|
|
16
|
+
|
|
17
|
+
if _version_not_supported:
|
|
18
|
+
raise RuntimeError(
|
|
19
|
+
f'The grpc package installed is at version {GRPC_VERSION},'
|
|
20
|
+
+ f' but the generated code in arvo/common/v1/models_pb2_grpc.py depends on'
|
|
21
|
+
+ f' grpcio>={GRPC_GENERATED_VERSION}.'
|
|
22
|
+
+ f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
|
|
23
|
+
+ f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
|
|
24
|
+
)
|
arvo/market/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
3
|
+
# NO CHECKED-IN PROTOBUF GENCODE
|
|
4
|
+
# source: arvo/market/v1/models.proto
|
|
5
|
+
# Protobuf Python Version: 6.31.1
|
|
6
|
+
"""Generated protocol buffer code."""
|
|
7
|
+
from google.protobuf import descriptor as _descriptor
|
|
8
|
+
from google.protobuf import descriptor_pool as _descriptor_pool
|
|
9
|
+
from google.protobuf import runtime_version as _runtime_version
|
|
10
|
+
from google.protobuf import symbol_database as _symbol_database
|
|
11
|
+
from google.protobuf.internal import builder as _builder
|
|
12
|
+
_runtime_version.ValidateProtobufRuntimeVersion(
|
|
13
|
+
_runtime_version.Domain.PUBLIC,
|
|
14
|
+
6,
|
|
15
|
+
31,
|
|
16
|
+
1,
|
|
17
|
+
'',
|
|
18
|
+
'arvo/market/v1/models.proto'
|
|
19
|
+
)
|
|
20
|
+
# @@protoc_insertion_point(imports)
|
|
21
|
+
|
|
22
|
+
_sym_db = _symbol_database.Default()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1b\x61rvo/market/v1/models.proto\x12\x0e\x61rvo.market.v1\"D\n\nInstrument\x12\n\n\x02id\x18\x01 \x01(\t\x12\x10\n\x08interval\x18\x02 \x01(\t\x12\x0c\n\x04\x66rom\x18\x03 \x01(\t\x12\n\n\x02to\x18\x04 \x01(\t\">\n\x0bInstruments\x12/\n\x0binstruments\x18\x01 \x03(\x0b\x32\x1a.arvo.market.v1.Instrument\"[\n\tQuoteTick\x12\x0e\n\x06symbol\x18\x01 \x01(\t\x12\r\n\x05price\x18\x02 \x01(\x01\x12\x13\n\x06\x63hange\x18\x03 \x01(\x01H\x00\x88\x01\x01\x12\x0f\n\x07regular\x18\x04 \x01(\x08\x42\t\n\x07_change\"A\n\x10InstrumentSearch\x12\r\n\x05query\x18\x01 \x01(\t\x12\x13\n\x06source\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\t\n\x07_source\"p\n\x0c\x46\x65tchRequest\x12\x12\n\ninstrument\x18\x01 \x01(\t\x12\x10\n\x08interval\x18\x02 \x01(\t\x12\x11\n\x04\x64\x61ys\x18\x03 \x01(\rH\x00\x88\x01\x01\x12\x13\n\x06source\x18\x04 \x01(\tH\x01\x88\x01\x01\x42\x07\n\x05_daysB\t\n\x07_source\"\x90\x01\n\x0e\x43ompareRequest\x12\x12\n\ninstrument\x18\x01 \x01(\t\x12\x10\n\x08interval\x18\x02 \x01(\t\x12\x12\n\x05\x66irst\x18\x03 \x01(\tH\x00\x88\x01\x01\x12\x13\n\x06second\x18\x04 \x01(\tH\x01\x88\x01\x01\x12\x11\n\x04\x64\x61ys\x18\x05 \x01(\rH\x02\x88\x01\x01\x42\x08\n\x06_firstB\t\n\x07_secondB\x07\n\x05_daysb\x06proto3')
|
|
28
|
+
|
|
29
|
+
_globals = globals()
|
|
30
|
+
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
|
31
|
+
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'arvo.market.v1.models_pb2', _globals)
|
|
32
|
+
if not _descriptor._USE_C_DESCRIPTORS:
|
|
33
|
+
DESCRIPTOR._loaded_options = None
|
|
34
|
+
_globals['_INSTRUMENT']._serialized_start=47
|
|
35
|
+
_globals['_INSTRUMENT']._serialized_end=115
|
|
36
|
+
_globals['_INSTRUMENTS']._serialized_start=117
|
|
37
|
+
_globals['_INSTRUMENTS']._serialized_end=179
|
|
38
|
+
_globals['_QUOTETICK']._serialized_start=181
|
|
39
|
+
_globals['_QUOTETICK']._serialized_end=272
|
|
40
|
+
_globals['_INSTRUMENTSEARCH']._serialized_start=274
|
|
41
|
+
_globals['_INSTRUMENTSEARCH']._serialized_end=339
|
|
42
|
+
_globals['_FETCHREQUEST']._serialized_start=341
|
|
43
|
+
_globals['_FETCHREQUEST']._serialized_end=453
|
|
44
|
+
_globals['_COMPAREREQUEST']._serialized_start=456
|
|
45
|
+
_globals['_COMPAREREQUEST']._serialized_end=600
|
|
46
|
+
# @@protoc_insertion_point(module_scope)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
|
|
2
|
+
"""Client and server classes corresponding to protobuf-defined services."""
|
|
3
|
+
import grpc
|
|
4
|
+
import warnings
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
GRPC_GENERATED_VERSION = '1.75.1'
|
|
8
|
+
GRPC_VERSION = grpc.__version__
|
|
9
|
+
_version_not_supported = False
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
from grpc._utilities import first_version_is_lower
|
|
13
|
+
_version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
|
|
14
|
+
except ImportError:
|
|
15
|
+
_version_not_supported = True
|
|
16
|
+
|
|
17
|
+
if _version_not_supported:
|
|
18
|
+
raise RuntimeError(
|
|
19
|
+
f'The grpc package installed is at version {GRPC_VERSION},'
|
|
20
|
+
+ f' but the generated code in arvo/market/v1/models_pb2_grpc.py depends on'
|
|
21
|
+
+ f' grpcio>={GRPC_GENERATED_VERSION}.'
|
|
22
|
+
+ f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
|
|
23
|
+
+ f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
|
|
24
|
+
)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
3
|
+
# NO CHECKED-IN PROTOBUF GENCODE
|
|
4
|
+
# source: arvo/market/v1/views.proto
|
|
5
|
+
# Protobuf Python Version: 6.31.1
|
|
6
|
+
"""Generated protocol buffer code."""
|
|
7
|
+
from google.protobuf import descriptor as _descriptor
|
|
8
|
+
from google.protobuf import descriptor_pool as _descriptor_pool
|
|
9
|
+
from google.protobuf import runtime_version as _runtime_version
|
|
10
|
+
from google.protobuf import symbol_database as _symbol_database
|
|
11
|
+
from google.protobuf.internal import builder as _builder
|
|
12
|
+
_runtime_version.ValidateProtobufRuntimeVersion(
|
|
13
|
+
_runtime_version.Domain.PUBLIC,
|
|
14
|
+
6,
|
|
15
|
+
31,
|
|
16
|
+
1,
|
|
17
|
+
'',
|
|
18
|
+
'arvo/market/v1/views.proto'
|
|
19
|
+
)
|
|
20
|
+
# @@protoc_insertion_point(imports)
|
|
21
|
+
|
|
22
|
+
_sym_db = _symbol_database.Default()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1a\x61rvo/market/v1/views.proto\x12\x0e\x61rvo.market.v1\"Y\n\x0f\x44\x61taFindingView\x12\x10\n\x08severity\x18\x01 \x01(\t\x12\x0c\n\x04kind\x18\x02 \x01(\t\x12\x0f\n\x02\x61t\x18\x03 \x01(\tH\x00\x88\x01\x01\x12\x0e\n\x06\x64\x65tail\x18\x04 \x01(\tB\x05\n\x03_at\"Y\n\x0f\x44\x61taLibraryView\x12\x11\n\tdirectory\x18\x01 \x01(\t\x12\x33\n\x0binstruments\x18\x02 \x03(\x0b\x32\x1e.arvo.market.v1.InstrumentView\"\xac\x02\n\tFetchView\x12\x12\n\ninstrument\x18\x01 \x01(\t\x12\x0e\n\x06source\x18\x02 \x01(\t\x12\x10\n\x08interval\x18\x03 \x01(\t\x12\x0c\n\x04\x62\x61rs\x18\x04 \x01(\r\x12\x14\n\x0cinterpolated\x18\x05 \x01(\r\x12\x11\n\x04\x66rom\x18\x06 \x01(\tH\x00\x88\x01\x01\x12\x0f\n\x02to\x18\x07 \x01(\tH\x01\x88\x01\x01\x12\x36\n\rdata_findings\x18\x08 \x03(\x0b\x32\x1f.arvo.market.v1.DataFindingView\x12\x15\n\x08revision\x18\t \x01(\tH\x02\x88\x01\x01\x12\x0f\n\x07revised\x18\n \x01(\x08\x12\x16\n\tdividends\x18\x0b \x01(\rH\x03\x88\x01\x01\x42\x07\n\x05_fromB\x05\n\x03_toB\x0b\n\t_revisionB\x0c\n\n_dividends\"\x88\x01\n\x0eInstrumentView\x12\n\n\x02id\x18\x01 \x01(\t\x12\x11\n\x04\x66rom\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x0f\n\x02to\x18\x03 \x01(\tH\x01\x88\x01\x01\x12\x0c\n\x04\x62\x61rs\x18\x04 \x01(\r\x12\x18\n\x0b\x66ingerprint\x18\x05 \x01(\tH\x02\x88\x01\x01\x42\x07\n\x05_fromB\x05\n\x03_toB\x0e\n\x0c_fingerprint\"\x89\x01\n\tMatchView\x12\x12\n\ninstrument\x18\x01 \x01(\t\x12\x0e\n\x06symbol\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x12\n\x05price\x18\x04 \x01(\x01H\x00\x88\x01\x01\x12\x13\n\x06\x63hange\x18\x05 \x01(\x01H\x01\x88\x01\x01\x12\x0c\n\x04held\x18\x06 \x01(\x08\x42\x08\n\x06_priceB\t\n\x07_change\"{\n\tQuoteView\x12\x12\n\ninstrument\x18\x01 \x01(\t\x12\x0e\n\x06symbol\x18\x02 \x01(\t\x12\x12\n\x05price\x18\x03 \x01(\x01H\x00\x88\x01\x01\x12\x13\n\x06\x63hange\x18\x04 \x01(\x01H\x01\x88\x01\x01\x12\x0c\n\x04held\x18\x05 \x01(\x08\x42\x08\n\x06_priceB\t\n\x07_change\"\x8c\x02\n\x14SourceComparisonView\x12\x0e\n\x06symbol\x18\x01 \x01(\t\x12\x10\n\x08interval\x18\x02 \x01(\t\x12\r\n\x05\x66irst\x18\x03 \x01(\t\x12\x0e\n\x06second\x18\x04 \x01(\t\x12\x12\n\nfirst_bars\x18\x05 \x01(\r\x12\x13\n\x0bsecond_bars\x18\x06 \x01(\r\x12\x0e\n\x06shared\x18\x07 \x01(\r\x12\x12\n\nonly_first\x18\x08 \x01(\r\x12\x13\n\x0bonly_second\x18\t \x01(\r\x12\x0f\n\x07summary\x18\n \x01(\t\x12\x10\n\x08\x64iverged\x18\x0b \x01(\x08\x12\x1b\n\x0e\x62\x61sis_mismatch\x18\x0c \x01(\tH\x00\x88\x01\x01\x42\x11\n\x0f_basis_mismatch\"t\n\nSourceView\x12\n\n\x02id\x18\x01 \x01(\t\x12\r\n\x05label\x18\x02 \x01(\t\x12\r\n\x05venue\x18\x03 \x01(\t\x12\x11\n\tconnected\x18\x04 \x01(\x08\x12\x15\n\rneeds_sign_in\x18\x05 \x01(\x08\x12\x12\n\nneeds_keys\x18\x06 \x01(\x08\":\n\x0bSourcesView\x12+\n\x07sources\x18\x01 \x03(\x0b\x32\x1a.arvo.market.v1.SourceView\"9\n\x0bMatchesView\x12*\n\x07matches\x18\x01 \x03(\x0b\x32\x19.arvo.market.v1.MatchView\"8\n\rWatchlistView\x12\'\n\x04rows\x18\x01 \x03(\x0b\x32\x19.arvo.market.v1.QuoteViewb\x06proto3')
|
|
28
|
+
|
|
29
|
+
_globals = globals()
|
|
30
|
+
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
|
31
|
+
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'arvo.market.v1.views_pb2', _globals)
|
|
32
|
+
if not _descriptor._USE_C_DESCRIPTORS:
|
|
33
|
+
DESCRIPTOR._loaded_options = None
|
|
34
|
+
_globals['_DATAFINDINGVIEW']._serialized_start=46
|
|
35
|
+
_globals['_DATAFINDINGVIEW']._serialized_end=135
|
|
36
|
+
_globals['_DATALIBRARYVIEW']._serialized_start=137
|
|
37
|
+
_globals['_DATALIBRARYVIEW']._serialized_end=226
|
|
38
|
+
_globals['_FETCHVIEW']._serialized_start=229
|
|
39
|
+
_globals['_FETCHVIEW']._serialized_end=529
|
|
40
|
+
_globals['_INSTRUMENTVIEW']._serialized_start=532
|
|
41
|
+
_globals['_INSTRUMENTVIEW']._serialized_end=668
|
|
42
|
+
_globals['_MATCHVIEW']._serialized_start=671
|
|
43
|
+
_globals['_MATCHVIEW']._serialized_end=808
|
|
44
|
+
_globals['_QUOTEVIEW']._serialized_start=810
|
|
45
|
+
_globals['_QUOTEVIEW']._serialized_end=933
|
|
46
|
+
_globals['_SOURCECOMPARISONVIEW']._serialized_start=936
|
|
47
|
+
_globals['_SOURCECOMPARISONVIEW']._serialized_end=1204
|
|
48
|
+
_globals['_SOURCEVIEW']._serialized_start=1206
|
|
49
|
+
_globals['_SOURCEVIEW']._serialized_end=1322
|
|
50
|
+
_globals['_SOURCESVIEW']._serialized_start=1324
|
|
51
|
+
_globals['_SOURCESVIEW']._serialized_end=1382
|
|
52
|
+
_globals['_MATCHESVIEW']._serialized_start=1384
|
|
53
|
+
_globals['_MATCHESVIEW']._serialized_end=1441
|
|
54
|
+
_globals['_WATCHLISTVIEW']._serialized_start=1443
|
|
55
|
+
_globals['_WATCHLISTVIEW']._serialized_end=1499
|
|
56
|
+
# @@protoc_insertion_point(module_scope)
|