Skip to main content

optionstratlib_visualization/
lib.rs

1#![deny(missing_docs, rustdoc::broken_intra_doc_links)]
2// Per rules/global_rules.md §Error Handling, unchecked `[]` / slicing is
3// banned in production code, with no per-file escape hatch (#341, #788).
4#![deny(clippy::indexing_slicing)]
5// Unit tests routinely index into `Vec`s they just pushed into, so the lint
6// is silenced in `#[cfg(test)]` only.
7#![cfg_attr(test, allow(clippy::indexing_slicing))]
8// Per rules/global_rules.md §Arithmetic, an operator that overflows, divides
9// by zero or breaks the `Positive` invariant aborts the caller, so every
10// operation on `Decimal`, `Positive` and the integers goes through its
11// checked form (#788, #808). `clippy.toml` exempts the unary minus on
12// `Decimal`, which cannot overflow. Casts that truncate, wrap or drop the
13// sign are denied for the same reason. Unit tests are exempt, as above.
14#![deny(
15    clippy::arithmetic_side_effects,
16    clippy::cast_possible_truncation,
17    clippy::cast_possible_wrap,
18    clippy::cast_sign_loss
19)]
20#![cfg_attr(
21    test,
22    allow(
23        clippy::arithmetic_side_effects,
24        clippy::cast_possible_truncation,
25        clippy::cast_possible_wrap,
26        clippy::cast_sign_loss
27    )
28)]
29// An error or a default built eagerly in `ok_or` / `unwrap_or` is paid on
30// the success path too (#857), so it is built in a closure.
31#![deny(clippy::or_fun_call)]
32
33//! # optionstratlib-visualization
34//!
35//! Charts for OptionStratLib: backend-neutral chart models, the one
36//! [`visualization::Graph`] contract, its implementations for the library's
37//! types, and an optional Plotly backend. It is the leaf of the workspace:
38//! it depends on `optionstratlib-core`, `optionstratlib-math`,
39//! `optionstratlib-pricing`, `optionstratlib-simulation`,
40//! `optionstratlib-market`, `optionstratlib-strategies` and
41//! `optionstratlib-backtest`, and no OptionStratLib crate depends on it, so
42//! headless consumers never build it.
43//!
44//! - [`visualization`]: the [`visualization::Graph`] trait, the chart data
45//!   ([`visualization::GraphData`], [`visualization::Series2D`],
46//!   [`visualization::Surface3D`]), [`visualization::GraphConfig`] and the
47//!   styles, [`visualization::PlotBuilder`] and
48//!   [`visualization::Plottable`], and the `Graph` implementations for
49//!   options, positions, curves, surfaces, random walks, simulators and every
50//!   concrete strategy.
51//! - [`visualization::terminal`]: terminal tables for option chains and
52//!   simulation statistics, the only code in the library that writes to
53//!   stdout, and only when asked to ([`visualization::terminal::ChainReport`],
54//!   [`visualization::terminal::SimulationReport`]).
55//! - [`error`]: the [`error::GraphError`] type.
56//!
57//! ## One graph contract
58//!
59//! [`visualization::Graph`] has the same definition and the same required
60//! items on every feature surface: an implementor supplies `graph_data` and
61//! may override `graph_config`, whatever backend is enabled. The backend
62//! features add provided rendering methods to it and never change what an
63//! implementor writes:
64//!
65//! | Surface | Provided by `Graph` |
66//! | --- | --- |
67//! | no features | `graph_data`, `graph_config` |
68//! | `plotly` | the above, plus `to_plot`, `write_html`, `show`, `render`, `to_interactive_html` |
69//! | `static_export` | the above, plus `write_png`, `write_svg` |
70//!
71//! [`visualization::OutputType`] has the same variants everywhere; asking
72//! `render` for a PNG or SVG without `static_export` returns
73//! [`error::GraphError::Render`] rather than doing nothing.
74//!
75//! ## Adapters
76//!
77//! Every chart of a library type is a `Graph` implementation in this crate.
78//! The orphan rule allows `impl Graph for T` only in the crate of `Graph` or
79//! in the crate of `T`, and no lower crate may depend on this one, so every
80//! adapter lives here: the lower crates name no chart type, and no domain
81//! type is wrapped or copied to chart it.
82//!
83//! | Type | Defining crate | Adapter module | Chart |
84//! | --- | --- | --- | --- |
85//! | `Options`, `Position` | core | `model_impls` | payoff at expiry, split into a positive and a negative series |
86//! | `Curve`, `Vec<Curve>` | math | `curves` | one series per curve |
87//! | `Surface` | math | `surfaces` | one 3D surface |
88//! | `RandomWalk`, `Simulator` | simulation | `simulation` | one price series per walk |
89//! | the 22 concrete strategies | strategies | `strategies` ([`impl_graph_for_payoff_strategy!`]) | profit and loss over the strategy's price range, with the break-even, current-price and P&L markers |
90//! | [`visualization::PlotBuilder`] | visualization | `plot_builder` | the wrapped value's data under the builder's configuration |
91//!
92//! `tests/graph_data_golden_test.rs` pins the data and configuration of
93//! every row against the output of the same adapters before they moved
94//! into this crate.
95//!
96//! ## Charting a strategy
97//!
98//! The strategy contract has no `Graph` supertrait: a strategy is charted
99//! because this crate implements `Graph` for it, so code that renders names
100//! the bound (`T: Strategies + Graph`) and code that does not never builds
101//! this crate. The capability this changes is the boxed strategy:
102//! `StrategyRequest::get_strategy` returns a `Box<dyn Strategable>`, which
103//! has no chart. A consumer that renders a strategy built from positions
104//! names the concrete type, through `StrategyConstructor`, or keeps its own
105//! object type with both bounds:
106//!
107//! ```rust
108//! use optionstratlib_core::model::{ExpirationDate, Position, Positive};
109//! use optionstratlib_core::pos_or_panic;
110//! use optionstratlib_strategies::strategies::base::{Positionable, StrategyType};
111//! use optionstratlib_strategies::strategies::{
112//!     BullCallSpread, Strategable, Strategies, StrategyConstructor, StrategyRequest,
113//! };
114//! use optionstratlib_visualization::visualization::{Graph, GraphData};
115//! use rust_decimal::Decimal;
116//!
117//! /// A consumer that renders names the bound itself.
118//! fn payoff_series<S: Strategies + Graph>(strategy: &S) -> usize {
119//!     match strategy.graph_data() {
120//!         GraphData::MultiSeries(series) => series.len(),
121//!         GraphData::Series(_) => 1,
122//!         GraphData::GraphSurface(_) => 0,
123//!     }
124//! }
125//!
126//! /// An object type for strategies that can be charted.
127//! trait Chartable: Strategable + Graph {}
128//! impl<T: Strategable + Graph> Chartable for T {}
129//!
130//! /// Builds the strategy a request names, as a chartable object.
131//! fn build(request: &StrategyRequest) -> Result<Box<dyn Chartable>, Box<dyn std::error::Error>> {
132//!     match &request.strategy_type {
133//!         StrategyType::BullCallSpread => {
134//!             Ok(Box::new(BullCallSpread::get_strategy(&request.positions)?))
135//!         }
136//!         other => Err(format!("{other:?} is not charted here").into()),
137//!     }
138//! }
139//!
140//! fn main() -> Result<(), Box<dyn std::error::Error>> {
141//!     let strategy = BullCallSpread::new(
142//!         "AAPL".to_string(),
143//!         pos_or_panic!(100.0),
144//!         pos_or_panic!(95.0),
145//!         pos_or_panic!(105.0),
146//!         ExpirationDate::Days(pos_or_panic!(30.0)),
147//!         pos_or_panic!(0.25),
148//!         Decimal::new(5, 2),   // risk-free rate, 5% per year
149//!         Positive::ZERO,       // dividend yield
150//!         Positive::ONE,        // quantity
151//!         pos_or_panic!(2.50),  // long call premium
152//!         pos_or_panic!(1.20),  // short call premium
153//!         Positive::ZERO,       // open and close fees of each leg
154//!         Positive::ZERO,
155//!         Positive::ZERO,
156//!         Positive::ZERO,
157//!     )?;
158//!     // The payoff chart: profit and loss over the strategy's price range.
159//!     assert!(payoff_series(&strategy) > 0);
160//!     // With `plotly`, the same value renders; `write_html` writes the page.
161//!     #[cfg(feature = "plotly")]
162//!     assert!(!strategy.to_plot().to_json().is_empty());
163//!
164//!     // The same strategy as a request carries it, as positions: built back
165//!     // through the builder, it charts the same.
166//!     let positions: Vec<Position> = strategy.get_positions()?.into_iter().cloned().collect();
167//!     let request = StrategyRequest::new(StrategyType::BullCallSpread, positions);
168//!     let rebuilt = build(&request)?;
169//!     assert_eq!(rebuilt.graph_data(), strategy.graph_data());
170//!     Ok(())
171//! }
172//! ```
173//!
174//! ## Imports
175//!
176//! There is no prelude, as in the other component crates (ADR-0001 D7): the
177//! module roots re-export each capability, and the `optionstratlib` facade
178//! prelude serves broad imports.
179//!
180//! ## Place in the workspace
181//!
182//! - **Depends on** `optionstratlib-core`, `-math`, `-pricing`, `-simulation`,
183//!   `-market`, `-strategies` and `-backtest`.
184//! - **Must not be a dependency** of any other OptionStratLib crate: it is the
185//!   leaf of the layering, and only the `optionstratlib` facade depends on it;
186//!   `make check-graph` enforces the layering (ADR-0001 D9).
187//! - **In the facade:** `optionstratlib::visualization`, `GraphError` and the
188//!   aggregate `Error` in `optionstratlib::error` and the root
189//!   `impl_graph_for_payoff_strategy!`, under the facade feature `visualization`;
190//!   the facade's `plotly` and `static_export` forward to the features below. The
191//!   facade paths are the same types as the paths here; the [ownership
192//!   map](https://github.com/joaquinbejar/OptionStratLib/blob/main/docs/ownership.md)
193//!   lists every one with its feature.
194//!
195//! Moving from 0.22? PNG and SVG export changed in 0.23: see the
196//! [0.23 migration guide](https://github.com/joaquinbejar/OptionStratLib/blob/main/docs/migration-0.23.md)
197//! and the "Static export" section below.
198//!
199//! Moving from 0.21? The [0.22 architecture and adoption
200//! guide](https://github.com/joaquinbejar/OptionStratLib/blob/main/docs/migration-0.22.md)
201//! maps each redesign to its replacement workflow; 0.22 keeps no
202//! compatibility with 0.21.
203//!
204//! ## Example
205//!
206//! A runnable program that depends on this crate directly, with the smallest
207//! dependency set and no facade, is [`osl-example-direct-visualization`](https://github.com/joaquinbejar/OptionStratLib/tree/main/examples/direct/visualization); `make tree-example-direct-visualization`
208//! asserts its resolved graph.
209//!
210//! ## Features
211//!
212//! - `plotly` (off by default): Plotly rendering and the trace builders
213//!   (`visualization::make_scatter`, `visualization::make_surface`).
214//!   Resolves no image-export, WebDriver or async runtime package.
215//! - `static_export` (off by default, implies `plotly`): PNG and SVG export,
216//!   drawn natively with Plotters since 0.23: no browser, WebDriver, network
217//!   or installed font. SVG text is drawn as glyph outlines of the bundled
218//!   Open Sans (SIL OFL 1.1, `assets/OFL.txt`).
219//!
220//! ## Static export
221//!
222//! `static_export` adds `Graph::write_png` and `Graph::write_svg`, and
223//! `render(OutputType::Png | Svg)` through them. Since 0.23 they draw natively
224//! with Plotters: no browser, WebDriver, network access or installed font is
225//! involved, exports in threads or processes are independent, and the bundled
226//! font and its licence ship in this crate. `static_export` still implies
227//! `plotly`, so the Plotly Rust crate is a dependency for the HTML methods; it
228//! plays no part in the images.
229//!
230//! | Build | Chart data, `Graph`, terminal tables | `to_plot`, HTML, `show`, Plotly helpers | `write_png`, `write_svg` |
231//! |---|---|---|---|
232//! | no feature | yes | | |
233//! | `plotly` | yes | yes | |
234//! | `static_export` | yes | yes (implied `plotly`) | yes |
235//!
236//! `show()` and `render(OutputType::Browser)` still hand an HTML chart to the
237//! default browser; that is the interactive workflow, unchanged.
238//!
239//! What the images are, and how they differ from the Plotly chart:
240//!
241//! - **Source.** The images are drawn from `graph_data()` and
242//!   `graph_config()`. An override of `Graph::to_plot` changes the HTML
243//!   outputs, not the images; put image customisations in `GraphConfig`.
244//! - **Text.** Every label uses the bundled Open Sans Regular (SIL OFL 1.1,
245//!   `assets/OFL.txt`), read with skrifa, at Plotly's default sizes. A character
246//!   the font has no glyph for (CJK, for example) is
247//!   `GraphError::MissingGlyph`; tab, line feed and carriage return advance like
248//!   a space. Labels are single lines with no kerning (the font's kerning
249//!   table is not applied), no ligatures and no right-to-left shaping.
250//! - **SVG text is outlines.** Each label is drawn as glyph paths, so the file
251//!   looks the same in every viewer and needs no font. The label's text is
252//!   kept as its accessible name (`<g role="img" aria-label>`), and the
253//!   document carries a `<title>` and a `<desc>` naming the series and axes.
254//!   The text cannot be selected, copied, found or edited as text. In Chrome's
255//!   accessibility tree, checked when the design was chosen, each label is an
256//!   `image` node named by its text, and the document is named by its title;
257//!   how WebKit or a screen reader presents them was not checked. Use
258//!   `write_html` for a text-searchable, interactive chart.
259//! - **Surfaces.** A 3D surface is drawn from a fixed viewpoint with
260//!   flat-shaded cells, no lighting and a Viridis colour bar (a `ColorScheme`
261//!   does not apply to surfaces), and shows its three axis titles. It must be a
262//!   complete grid: a missing point is `GraphError::IncompleteSurface`, a
263//!   repeated one `GraphError::DuplicateSurfacePoint`.
264//! - **Colours.** Colour strings follow CSS Color 4 (named colours, hex with
265//!   or without alpha, `rgb()`, `rgba()`, `hsl()`, `hsla()`); a spelling only
266//!   plotly.js tolerates is `GraphError::InvalidColor`.
267//! - **Sizes.** Width and height must each be 1 to 16384 pixels.
268//! - **Files.** The image is written next to its destination and renamed over
269//!   it once complete, so a failed export leaves no partial file.
270//!
271//! Data the export cannot draw faithfully is a typed `GraphError` rather than a
272//! repaired chart. `GraphError` is `#[non_exhaustive]`: keep a wildcard arm.
273//!
274//! The crate README shows such a match as a compiled example.
275
276/// Chart models, the `Graph` contract and its implementations.
277pub mod visualization;
278
279/// Errors raised by the visualization layer.
280pub mod error;
281
282/// Items `impl_graph_for_payoff_strategy!` names through `$crate`, so a caller
283/// needs no imports of its own. Not part of the public API.
284#[doc(hidden)]
285pub mod __private {
286    pub use optionstratlib_core::model::Positive;
287    pub use optionstratlib_pricing::pricing::Profit;
288    pub use optionstratlib_strategies::strategies::base::BreakEvenable;
289    pub use optionstratlib_strategies::strategies::{BasicAble, Strategies};
290    pub use rust_decimal::Decimal;
291    pub use tracing;
292}
293
294/// Version of the `optionstratlib-visualization` crate.
295pub const VERSION: &str = env!("CARGO_PKG_VERSION");
296
297/// Compiles the Rust examples of this crate's `README.md` as doctests, so the
298/// README cannot drift from the API (#554). Exists only under `cfg(doctest)`.
299#[cfg(doctest)]
300#[doc = include_str!("../README.md")]
301pub struct ReadmeDoctests;