openpine_vm/visuals/
mod.rs

1mod bar_colors;
2mod bgcolors;
3mod r#box;
4mod extend;
5mod fill;
6mod graph;
7mod hline;
8mod label;
9mod line;
10mod linefill;
11mod location;
12mod plot;
13mod plot_display;
14mod plot_format;
15mod plotarrow;
16mod plotbar;
17mod plotcandle;
18mod plotchar;
19mod plotshape;
20mod polyline;
21mod position;
22mod series_graph;
23mod shape;
24mod size;
25mod table;
26mod text;
27mod xylocation;
28
29use std::collections::BTreeMap;
30
31pub use bar_colors::*;
32pub use bgcolors::*;
33pub use extend::*;
34pub use fill::*;
35pub use graph::*;
36pub use hline::*;
37pub use label::*;
38pub use line::*;
39pub use linefill::*;
40pub use location::*;
41/// Color type used by visual primitives.
42pub type Color = openpine_compiler::instructions::Color;
43pub use r#box::*;
44pub use plot::*;
45pub use plot_display::*;
46pub use plot_format::*;
47pub use plotarrow::*;
48pub use plotbar::*;
49pub use plotcandle::*;
50pub use plotchar::*;
51pub use plotshape::*;
52pub use polyline::*;
53pub use position::*;
54use serde::{Deserialize, Serialize};
55pub use series_graph::*;
56pub use shape::*;
57pub use size::*;
58pub use table::*;
59pub use text::*;
60pub use xylocation::*;
61
62/// A single executed order fill.
63///
64/// This is appended to [`Chart`]'s filled order list whenever the host notifies
65/// the VM via `Instance::emit_order_filled`.
66///
67/// See also: [`Chart::filled_orders_on_bar`].
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct FilledOrder {
70    /// Order identifier provided by the host.
71    pub order_id: String,
72    /// Executed fill price.
73    pub price: f64,
74    /// Executed fill quantity.
75    pub quantity: f64,
76}
77
78/// Collection of visuals produced during script execution.
79///
80/// Access via [`Instance::chart()`](crate::Instance::chart) after executing
81/// bars. Contains two categories of visual outputs:
82///
83/// - **Series graphs** — per-bar data like `plot()`, `bgcolor()`, `fill()`,
84///   accessed via [`series_graphs()`](Self::series_graphs).
85/// - **Non-series graphs** — point-in-time objects like `label.new()`,
86///   `line.new()`, `box.new()`, `table.new()`, accessed via
87///   [`graphs()`](Self::graphs).
88///
89/// # Examples
90///
91/// ```no_run
92/// # use openpine_vm::*;
93/// # async fn example() {
94/// # let source = "//@version=6\nindicator(\"\")\nplot(close)";
95/// # let provider = Vec::<Candlestick>::new();
96/// # let mut instance = Instance::builder(provider, source, TimeFrame::days(1),
97/// #     "NASDAQ:AAPL").build().await.unwrap();
98/// # instance.run_to_end("NASDAQ:AAPL", TimeFrame::days(1)).await.unwrap();
99/// let chart = instance.chart();
100///
101/// println!("Background: {:?}", chart.background_color());
102/// println!("Bar count: {}", chart.series_len());
103///
104/// // Series graphs (plots, fills, bgcolor, etc.)
105/// for (id, sg) in chart.series_graphs() {
106///     if let Some(plot) = sg.as_plot() {
107///         println!("Plot '{:?}': {} values", plot.title, plot.series.len());
108///     }
109/// }
110///
111/// // Non-series graphs (labels, lines, boxes, tables, etc.)
112/// for (id, g) in chart.graphs() {
113///     if let Some(line) = g.as_line() {
114///         println!(
115///             "Line from ({},{}) to ({},{})",
116///             line.x1, line.y1, line.x2, line.y2
117///         );
118///     }
119/// }
120/// # }
121/// ```
122#[derive(Debug, Default, Clone, Serialize, Deserialize)]
123pub struct Chart {
124    background_color: Option<Color>,
125    series_graphs: BTreeMap<SeriesGraphId, SeriesGraph>,
126    graphs: BTreeMap<GraphId, Graph>,
127    bar_colors: BarColors,
128    graph_id_counter: i64,
129    /// Filled orders grouped by `bar_index`.
130    ///
131    /// Key: `bar_index` (base 0).
132    /// Value: all fills that happened on that bar, in insertion order.
133    filled_orders: BTreeMap<usize, Vec<FilledOrder>>,
134}
135
136impl Chart {
137    /// Sets the background color of the chart.
138    #[inline]
139    pub(crate) fn set_background_color(&mut self, color: impl Into<Option<Color>>) {
140        self.background_color = color.into();
141    }
142
143    /// Returns the configured chart background color.
144    #[inline]
145    pub fn background_color(&self) -> Option<Color> {
146        self.background_color
147    }
148
149    #[inline]
150    pub(crate) fn add_series_graph(&mut self, id: SeriesGraphId, mut graph: SeriesGraph) {
151        graph.append_new();
152        self.series_graphs.insert(id, graph);
153    }
154
155    /// Iterates over all per-series graphs.
156    #[inline]
157    pub fn series_graphs(&self) -> impl Iterator<Item = (SeriesGraphId, &SeriesGraph)> {
158        self.series_graphs.iter().map(|(id, graph)| (*id, graph))
159    }
160
161    /// Returns the per-series graph for the given id.
162    #[inline]
163    pub fn series_graph(&self, id: SeriesGraphId) -> Option<&SeriesGraph> {
164        self.series_graphs.get(&id)
165    }
166
167    #[inline]
168    pub(crate) fn series_graph_mut(&mut self, id: SeriesGraphId) -> Option<&mut SeriesGraph> {
169        self.series_graphs.get_mut(&id)
170    }
171
172    #[inline]
173    pub(crate) fn add_graph(&mut self, graph: Graph) -> GraphId {
174        let id = GraphId(self.graph_id_counter);
175        self.graph_id_counter += 1;
176        self.graphs.insert(id, graph);
177        id
178    }
179
180    /// Add a graph while enforcing the per-type `max_count` limit.
181    ///
182    /// If adding this graph would exceed `max_count` for its variant, the
183    /// oldest graphs of the same kind are evicted first.  Returns the new
184    /// [`GraphId`] together with any evicted `(id, graph)` pairs so the
185    /// caller can record [`RollbackAction::RestoreGraph`] entries.
186    pub(crate) fn add_graph_evicting(
187        &mut self,
188        graph: Graph,
189        max_count: usize,
190    ) -> (GraphId, Vec<(GraphId, Graph)>) {
191        let disc = std::mem::discriminant(&graph);
192        let same_kind_ids: Vec<GraphId> = self
193            .graphs
194            .iter()
195            .filter(|(_, g)| std::mem::discriminant(*g) == disc)
196            .map(|(&id, _)| id)
197            .collect();
198
199        let total_after_add = same_kind_ids.len() + 1;
200        let mut evicted = Vec::new();
201        if total_after_add > max_count {
202            let excess = total_after_add - max_count;
203            for &id in same_kind_ids.iter().take(excess) {
204                if let Some(graph) = self.graphs.remove(&id) {
205                    evicted.push((id, graph));
206                }
207            }
208        }
209
210        let new_id = self.add_graph(graph);
211        (new_id, evicted)
212    }
213
214    /// Re-insert a previously evicted graph, restoring its original id.
215    #[inline]
216    pub(crate) fn restore_graph(&mut self, id: GraphId, graph: Graph) {
217        self.graphs.insert(id, graph);
218    }
219
220    /// Iterates over all non-series graphs.
221    #[inline]
222    pub fn graphs(&self) -> impl Iterator<Item = (GraphId, &Graph)> {
223        self.graphs.iter().map(|(id, graph)| (*id, graph))
224    }
225
226    /// Returns the graph for the given id.
227    #[inline]
228    pub fn graph(&self, id: GraphId) -> Option<&Graph> {
229        self.graphs.get(&id)
230    }
231
232    #[inline]
233    pub(crate) fn graph_mut(&mut self, id: GraphId) -> Option<&mut Graph> {
234        self.graphs.get_mut(&id)
235    }
236
237    #[inline]
238    pub(crate) fn remove_graph(&mut self, id: GraphId) {
239        self.graphs.remove(&id);
240    }
241
242    #[inline]
243    pub(crate) fn retain_graphs<F>(&mut self, mut f: F)
244    where
245        F: FnMut(&Graph) -> bool,
246    {
247        self.graphs.retain(|_, graph| f(graph));
248    }
249
250    /// Returns the number of bars in the chart series.
251    #[inline]
252    pub fn series_len(&self) -> usize {
253        self.bar_colors.colors.len()
254    }
255
256    /// Returns the bar color overrides for the chart.
257    #[inline]
258    pub fn bar_colors(&self) -> &BarColors {
259        &self.bar_colors
260    }
261
262    #[inline]
263    pub(crate) fn bar_colors_mut(&mut self) -> &mut BarColors {
264        &mut self.bar_colors
265    }
266
267    pub(crate) fn append_new(&mut self) {
268        self.bar_colors.append_new();
269        for graph in self.series_graphs.values_mut() {
270            graph.append_new();
271        }
272    }
273
274    /// Returns the list of filled orders on the given `bar_index`.
275    ///
276    /// Each fill includes the signal/order id (e.g. entry or exit name), price,
277    /// and signed quantity. Returns an empty slice if the bar has no fills.
278    #[inline]
279    pub fn filled_orders_on_bar(&self, bar_index: usize) -> &[FilledOrder] {
280        self.filled_orders
281            .get(&bar_index)
282            .map(Vec::as_slice)
283            .unwrap_or(&[])
284    }
285
286    #[inline]
287    pub(crate) fn record_filled_order(&mut self, bar_index: usize, order: FilledOrder) {
288        self.filled_orders.entry(bar_index).or_default().push(order);
289    }
290}