Skip to main content

arvo_api/
lib.rs

1//! The types of Arvo's engine API, generated from its protobuf contract.
2//!
3//! Every message in the [contract](https://github.com/wjpin84/arvo-engine-api)
4//! is here, one module per proto package and every shape at the root as
5//! well. Each carries the comment written on it in the proto: that is the
6//! single source of what a field means, and nothing is documented here that
7//! is not documented there.
8//!
9//! # Messages only
10//!
11//! There is no transport in this crate, which is what lets it build for
12//! WebAssembly. A browser-side front end links this; the gRPC stubs live in
13//! `arvo-client`, which depends on this and re-exports it.
14//!
15//! # What is not generated
16//!
17//! proto3 makes every nested message optional and spells a Rust enum with
18//! data as a `oneof`. The [ergonomics](#accessors-and-constructors) at the
19//! bottom of this crate are the small amount of hand-written Rust that says
20//! what the generated shapes cannot: which nested fields the engine always
21//! sends, and how to build or match a `oneof` without naming its `Of`.
22//!
23//! # Accessors and constructors
24//!
25//! - A field the engine always fills has an accessor of the same name that
26//!   returns it by reference: `study.strategy()` rather than
27//!   `study.strategy.as_ref().unwrap()`. It panics only if the sender left
28//!   the field out, which would mean the two sides disagree about the
29//!   contract, and an empty panel would hide that.
30//! - A `oneof` has constructors (`RecordView::study(view)`,
31//!   `EventKindView::plugin(id, reachable)`) and, where a reader wants one
32//!   question answered, an accessor (`plugin.reachable()`, `event.of()`).
33//! - [`count`] and [`size`] convert a count between `usize` and the `u32` it
34//!   is on the wire, saturating rather than panicking.
35//!
36//! # Serde
37//!
38//! Every shape derives `Serialize` and `Deserialize` with serde's defaults,
39//! because the same shapes cross JSON bridges as well as gRPC.
40
41#![warn(missing_docs)]
42
43/// Generated from `protos/` by `cargo run -p generate` and committed.
44///
45/// The module tree mirrors the package names, because that is how a type in
46/// one package refers to a type in another. The services are not here;
47/// `arvo-client` generates those and points them back at these types.
48mod generated;
49
50/// One module per domain, for code that wants to say which it means.
51pub use generated::arvo::{
52    common::v1 as common, market::v1 as market, platform::v1 as platform,
53    portfolio::v1 as portfolio, research::v1 as research, session::v1 as session,
54};
55
56/// Every shape at the root as well, because a window names a view on nearly
57/// every line and `arvo_api::research::StudyView` would be noise. The names
58/// do not collide: proto has no overloading and neither does this.
59pub use generated::arvo::{
60    common::v1::*, market::v1::*, platform::v1::*, portfolio::v1::*, research::v1::*,
61    session::v1::*,
62};
63
64/// What the generated shapes cannot say for themselves.
65///
66/// proto3 makes every message field absent-able, so a nested shape arrives as
67/// an `Option` even where it is always sent. These accessors say "the sender
68/// always fills this", in one place, instead of at every reader.
69mod ergonomics {
70    use super::{
71        platform::{event_kind_view, plugin_status_view},
72        research::record_view,
73        EventKindView, EventView, FeedEvent, FindingsEvent, LibraryEvent, PluginEvent, SessionEvent,
74        SeverityView, StreamEvent,
75        BookView, ImportView, MetricsView, PanelView, PluginStatusView, PluginStatusViewReachable,
76        PluginStatusViewUnreachable, PluginView, PortfolioView, RecordView, ReportedView, StudyView,
77        TradesView, WalkForwardView,
78    };
79
80    /// A count on the wire. Rust counts in `usize`; the contract is explicit
81    /// about width, because a reader in another language has to be.
82    #[must_use]
83    pub fn count(n: usize) -> u32 {
84        u32::try_from(n).unwrap_or(u32::MAX)
85    }
86
87    /// A count read back, for code that indexes with it.
88    #[must_use]
89    pub fn size(n: u32) -> usize {
90        usize::try_from(n).unwrap_or(usize::MAX)
91    }
92
93    macro_rules! always {
94        ($owner:ty, $field:ident, $ty:ty) => {
95            impl $owner {
96                /// The sender always fills this.
97                ///
98                /// # Panics
99                ///
100                /// Only if it did not, which would mean the two sides disagree
101                /// about the contract.
102                #[must_use]
103                pub fn $field(&self) -> &$ty {
104                    self.$field.as_ref().expect(concat!(stringify!($owner), " always carries ", stringify!($field)))
105                }
106            }
107        };
108    }
109    always!(StudyView, strategy, MetricsView);
110    always!(StudyView, benchmark, MetricsView);
111    always!(StudyView, trades_detail, TradesView);
112    always!(WalkForwardView, strategy, MetricsView);
113    always!(WalkForwardView, benchmark, MetricsView);
114    always!(WalkForwardView, trades_detail, TradesView);
115    always!(BookView, metrics, MetricsView);
116    always!(PortfolioView, import, ImportView);
117
118    impl PluginView {
119        /// Whether the last probe reached it.
120        #[must_use]
121        pub fn reachable(&self) -> bool {
122            matches!(self.status.as_ref().and_then(|s| s.of.as_ref()), Some(plugin_status_view::Of::Reachable(_)))
123        }
124
125        /// What it said it is, if the last probe reached it.
126        #[must_use]
127        pub fn reached(&self) -> Option<&PluginStatusViewReachable> {
128            match self.status.as_ref().and_then(|status| status.of.as_ref()) {
129                Some(plugin_status_view::Of::Reachable(status)) => Some(status),
130                _ => None,
131            }
132        }
133
134        /// Why the last probe did not reach it, if it did not.
135        ///
136        /// A plugin whose status is missing counts as unreached: the window
137        /// says so rather than drawing it as healthy.
138        #[must_use]
139        pub fn unreachable(&self) -> Option<&str> {
140            match self.status.as_ref().and_then(|status| status.of.as_ref()) {
141                Some(plugin_status_view::Of::Unreachable(status)) => Some(&status.reason),
142                Some(plugin_status_view::Of::Reachable(_)) => None,
143                None => Some("the engine sent no status"),
144            }
145        }
146    }
147
148    impl PluginStatusView {
149        /// It answered, and said what it is.
150        #[must_use]
151        pub fn reachable(name: String, version: String, capabilities: Vec<String>) -> Self {
152            Self {
153                of: Some(plugin_status_view::Of::Reachable(PluginStatusViewReachable { name, version, capabilities })),
154            }
155        }
156
157        /// It did not answer, and why.
158        #[must_use]
159        pub fn unreachable(reason: String) -> Self {
160            Self { of: Some(plugin_status_view::Of::Unreachable(PluginStatusViewUnreachable { reason })) }
161        }
162    }
163
164    impl EventKindView {
165        /// A plugin became reachable, or stopped being.
166        #[must_use]
167        pub fn plugin(id: String, reachable: bool) -> Self {
168            Self { of: Some(event_kind_view::Of::Plugin(PluginEvent { id, reachable })) }
169        }
170
171        /// A broker connection came up or went away.
172        #[must_use]
173        pub fn feed(id: String, connected: bool) -> Self {
174            Self { of: Some(event_kind_view::Of::Feed(FeedEvent { id, connected })) }
175        }
176
177        /// The live price stream stopped or came back.
178        #[must_use]
179        pub fn stream(live: bool) -> Self {
180            Self { of: Some(event_kind_view::Of::Stream(StreamEvent { live })) }
181        }
182
183        /// Stored findings went stale.
184        #[must_use]
185        pub fn findings(count: u32) -> Self {
186            Self { of: Some(event_kind_view::Of::Findings(FindingsEvent { count })) }
187        }
188
189        /// A trading session changed state.
190        #[must_use]
191        pub fn session(id: String, state: String) -> Self {
192            Self { of: Some(event_kind_view::Of::Session(SessionEvent { id, state })) }
193        }
194
195        /// A fetch wrote an instrument's bars.
196        #[must_use]
197        pub fn library(instrument: String, fingerprint: String) -> Self {
198            Self { of: Some(event_kind_view::Of::Library(LibraryEvent { instrument, fingerprint })) }
199        }
200    }
201
202    impl EventView {
203        /// An event, with the kind and the text a renderer shows.
204        #[must_use]
205        pub fn new(kind: EventKindView, title: String, detail: String, severity: SeverityView) -> Self {
206            Self { kind: Some(kind), title, detail, severity: severity as i32 }
207        }
208
209        /// What happened, structurally, if the sender said.
210        #[must_use]
211        pub fn of(&self) -> Option<&event_kind_view::Of> {
212            self.kind.as_ref()?.of.as_ref()
213        }
214    }
215
216    impl RecordView {
217        /// A study, as a reopened record.
218        #[must_use]
219        pub fn study(view: StudyView) -> Self {
220            Self { of: Some(record_view::Of::Study(view)) }
221        }
222
223        /// A walk-forward, as a reopened record.
224        #[must_use]
225        pub fn walk_forward(view: WalkForwardView) -> Self {
226            Self { of: Some(record_view::Of::Walkforward(view)) }
227        }
228
229        /// A panel, as a reopened record.
230        #[must_use]
231        pub fn panel(view: PanelView) -> Self {
232            Self { of: Some(record_view::Of::Panel(view)) }
233        }
234
235        /// Evidence Arvo did not compute, as a reopened record.
236        #[must_use]
237        pub fn reported(view: ReportedView) -> Self {
238            Self { of: Some(record_view::Of::Reported(view)) }
239        }
240    }
241}
242
243pub use ergonomics::{count, size};