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//! # #[cfg(feature = "clock")] {
109//! use optionstratlib_core::model::{ExpirationDate, Position, Positive};
110//! use optionstratlib_core::pos_or_panic;
111//! use optionstratlib_strategies::strategies::base::{Positionable, StrategyType};
112//! use optionstratlib_strategies::strategies::{
113//!     BullCallSpread, Strategable, Strategies, StrategyConstructor, StrategyRequest,
114//! };
115//! use optionstratlib_visualization::visualization::{Graph, GraphData};
116//! use rust_decimal::Decimal;
117//!
118//! /// A consumer that renders names the bound itself.
119//! fn payoff_series<S: Strategies + Graph>(strategy: &S) -> usize {
120//!     match strategy.graph_data() {
121//!         GraphData::MultiSeries(series) => series.len(),
122//!         GraphData::Series(_) => 1,
123//!         GraphData::GraphSurface(_) => 0,
124//!     }
125//! }
126//!
127//! /// An object type for strategies that can be charted.
128//! trait Chartable: Strategable + Graph {}
129//! impl<T: Strategable + Graph> Chartable for T {}
130//!
131//! /// Builds the strategy a request names, as a chartable object.
132//! fn build(request: &StrategyRequest) -> Result<Box<dyn Chartable>, Box<dyn std::error::Error>> {
133//!     match &request.strategy_type {
134//!         StrategyType::BullCallSpread => {
135//!             Ok(Box::new(BullCallSpread::get_strategy(&request.positions)?))
136//!         }
137//!         other => Err(format!("{other:?} is not charted here").into()),
138//!     }
139//! }
140//!
141//! fn main() -> Result<(), Box<dyn std::error::Error>> {
142//!     let strategy = BullCallSpread::new(
143//!         "AAPL".to_string(),
144//!         pos_or_panic!(100.0),
145//!         pos_or_panic!(95.0),
146//!         pos_or_panic!(105.0),
147//!         ExpirationDate::Days(pos_or_panic!(30.0)),
148//!         pos_or_panic!(0.25),
149//!         Decimal::new(5, 2),   // risk-free rate, 5% per year
150//!         Positive::ZERO,       // dividend yield
151//!         Positive::ONE,        // quantity
152//!         pos_or_panic!(2.50),  // long call premium
153//!         pos_or_panic!(1.20),  // short call premium
154//!         Positive::ZERO,       // open and close fees of each leg
155//!         Positive::ZERO,
156//!         Positive::ZERO,
157//!         Positive::ZERO,
158//!     )?;
159//!     // The payoff chart: profit and loss over the strategy's price range.
160//!     assert!(payoff_series(&strategy) > 0);
161//!     // With `plotly`, the same value renders; `write_html` writes the page.
162//!     #[cfg(feature = "plotly")]
163//!     assert!(!strategy.to_plot().to_json().is_empty());
164//!
165//!     // The same strategy as a request carries it, as positions: built back
166//!     // through the builder, it charts the same.
167//!     let positions: Vec<Position> = strategy.get_positions()?.into_iter().cloned().collect();
168//!     let request = StrategyRequest::new(StrategyType::BullCallSpread, positions);
169//!     let rebuilt = build(&request)?;
170//!     assert_eq!(rebuilt.graph_data(), strategy.graph_data());
171//!     Ok(())
172//! }
173//! # }
174//! ```
175//!
176//! ## Imports
177//!
178//! There is no prelude, as in the other component crates (ADR-0001 D7): the
179//! module roots re-export each capability, and the `optionstratlib` facade
180//! prelude serves broad imports.
181//!
182//! ## Place in the workspace
183//!
184//! - **Depends on** `optionstratlib-core`, `-math`, `-pricing`, `-simulation`,
185//!   `-market`, `-strategies` and `-backtest`.
186//! - **Must not be a dependency** of any other OptionStratLib crate: it is the
187//!   leaf of the layering, and only the `optionstratlib` facade depends on it;
188//!   `make check-graph` enforces the layering (ADR-0001 D9).
189//! - **In the facade:** `optionstratlib::visualization`, `GraphError` and the
190//!   aggregate `Error` in `optionstratlib::error` and the root
191//!   `impl_graph_for_payoff_strategy!`, under the facade feature `visualization`;
192//!   the facade's `plotly` and `static_export` forward to the features below. The
193//!   facade paths are the same types as the paths here; the [ownership
194//!   map](https://github.com/joaquinbejar/OptionStratLib/blob/main/docs/ownership.md)
195//!   lists every one with its feature.
196//!
197//! Moving from 0.22? PNG and SVG export changed in 0.23: see the
198//! [0.23 migration guide](https://github.com/joaquinbejar/OptionStratLib/blob/main/docs/migration-0.23.md)
199//! and the "Static export" section below.
200//!
201//! Moving from 0.21? The [0.22 architecture and adoption
202//! guide](https://github.com/joaquinbejar/OptionStratLib/blob/main/docs/migration-0.22.md)
203//! maps each redesign to its replacement workflow; 0.22 keeps no
204//! compatibility with 0.21.
205//!
206//! ## Example
207//!
208//! A runnable program that depends on this crate directly, with the smallest
209//! 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`
210//! asserts its resolved graph.
211//!
212//! ## Features
213//!
214//! - `plotly` (off by default): Plotly rendering and the trace builders
215//!   (`visualization::make_scatter`, `visualization::make_surface`).
216//!   Resolves no image-export, WebDriver or async runtime package.
217//! - `static_export` (off by default, implies `plotly`): PNG and SVG export,
218//!   drawn natively with Plotters since 0.23: no browser, WebDriver, network
219//!   or installed font. SVG text is drawn as glyph outlines of the bundled
220//!   Open Sans (SIL OFL 1.1, `assets/OFL.txt`).
221//!
222//! ## Static export
223//!
224//! `static_export` adds `Graph::write_png` and `Graph::write_svg`, and
225//! `render(OutputType::Png | Svg)` through them. Since 0.23 they draw natively
226//! with Plotters: no browser, WebDriver, network access or installed font is
227//! involved, exports in threads or processes are independent, and the bundled
228//! font and its licence ship in this crate. `static_export` still implies
229//! `plotly`, so the Plotly Rust crate is a dependency for the HTML methods; it
230//! plays no part in the images.
231//!
232//! | Build | Chart data, `Graph`, terminal tables | `to_plot`, HTML, `show`, Plotly helpers | `write_png`, `write_svg` |
233//! |---|---|---|---|
234//! | no feature | yes | | |
235//! | `plotly` | yes | yes | |
236//! | `static_export` | yes | yes (implied `plotly`) | yes |
237//!
238//! `show()` and `render(OutputType::Browser)` still hand an HTML chart to the
239//! default browser; that is the interactive workflow, unchanged.
240//!
241//! What the images are, and how they differ from the Plotly chart:
242//!
243//! - **Source.** The images are drawn from `graph_data()` and
244//!   `graph_config()`. An override of `Graph::to_plot` changes the HTML
245//!   outputs, not the images; put image customisations in `GraphConfig`.
246//! - **Text.** Every label uses the bundled Open Sans Regular (SIL OFL 1.1,
247//!   `assets/OFL.txt`), read with skrifa, at Plotly's default sizes. A character
248//!   the font has no glyph for (CJK, for example) is
249//!   `GraphError::MissingGlyph`; tab, line feed and carriage return advance like
250//!   a space. Labels are single lines with no kerning (the font's kerning
251//!   table is not applied), no ligatures and no right-to-left shaping.
252//! - **SVG text is outlines.** Each label is drawn as glyph paths, so the file
253//!   looks the same in every viewer and needs no font. The label's text is
254//!   kept as its accessible name (`<g role="img" aria-label>`), and the
255//!   document carries a `<title>` and a `<desc>` naming the series and axes.
256//!   The text cannot be selected, copied, found or edited as text. In Chrome's
257//!   accessibility tree, checked when the design was chosen, each label is an
258//!   `image` node named by its text, and the document is named by its title;
259//!   how WebKit or a screen reader presents them was not checked. Use
260//!   `write_html` for a text-searchable, interactive chart.
261//! - **Surfaces.** A 3D surface is drawn from a fixed viewpoint with
262//!   flat-shaded cells, no lighting and a Viridis colour bar (a `ColorScheme`
263//!   does not apply to surfaces), and shows its three axis titles. It must be a
264//!   complete grid: a missing point is `GraphError::IncompleteSurface`, a
265//!   repeated one `GraphError::DuplicateSurfacePoint`.
266//! - **Colours.** Colour strings follow CSS Color 4 (named colours, hex with
267//!   or without alpha, `rgb()`, `rgba()`, `hsl()`, `hsla()`); a spelling only
268//!   plotly.js tolerates is `GraphError::InvalidColor`.
269//! - **Sizes.** Width and height must each be 1 to 16384 pixels.
270//! - **Files.** The image is written next to its destination and renamed over
271//!   it once complete, so a failed export leaves no partial file.
272//!
273//! Data the export cannot draw faithfully is a typed `GraphError` rather than a
274//! repaired chart. `GraphError` is `#[non_exhaustive]`: keep a wildcard arm.
275//!
276//! The crate README shows such a match as a compiled example.
277
278/// Chart models, the `Graph` contract and its implementations.
279pub mod visualization;
280
281/// Errors raised by the visualization layer.
282pub mod error;
283
284/// Items `impl_graph_for_payoff_strategy!` names through `$crate`, so a caller
285/// needs no imports of its own. Not part of the public API.
286#[doc(hidden)]
287pub mod __private {
288    pub use optionstratlib_core::model::Positive;
289    pub use optionstratlib_pricing::pricing::Profit;
290    pub use optionstratlib_strategies::strategies::base::BreakEvenable;
291    pub use optionstratlib_strategies::strategies::{BasicAble, Strategies};
292    pub use rust_decimal::Decimal;
293    pub use tracing;
294}
295
296/// Version of the `optionstratlib-visualization` crate.
297pub const VERSION: &str = env!("CARGO_PKG_VERSION");
298
299/// Compiles the Rust examples of this crate's `README.md` as doctests, so the
300/// README cannot drift from the API (#554). Exists only under `cfg(doctest)`.
301#[cfg(doctest)]
302#[doc = include_str!("../README.md")]
303pub struct ReadmeDoctests;