Skip to main content

egui_charts/model/
mod.rs

1//! Data types for bars, instruments, chart styles, trading and host analytics.
2//!
3//! Start with [`Bar`] and [`BarData`] for price history, [`Instrument`] for symbol
4//! metadata and [`Timeframe`] for bar intervals. The transformation modules build
5//! price-based charts and TPO profiles without rendering; strategy records describe
6//! host-computed runs and provide performance calculations.
7
8/// The most price movement, in sizes, that a Renko or range bar build walks over. A smaller size
9/// on a longer history is built from a later bar; see `renko::work_start`.
10pub(crate) const MAX_WORK: f64 = 2_000_000.0;
11
12/// A price-based chart built one bar at a time, so a cached build can be extended by the newest
13/// bar instead of rebuilt. The first `push` seeds the state. `elements` returns the newest
14/// elements up to the type's limit, including the one still forming.
15pub(crate) trait PriceBuilder {
16    type Element: Clone;
17    fn push(&mut self, bar: &Bar);
18    fn elements(&self) -> Vec<Self::Element>;
19}
20
21mod alert;
22pub mod bar;
23mod box_size;
24mod chart_style;
25mod chart_type;
26mod clock;
27mod color;
28mod date_range;
29mod instrument;
30pub mod kagi;
31pub mod line_break;
32mod markers;
33#[cfg(any(feature = "studies", feature = "drawings"))]
34pub(crate) mod math;
35mod parse;
36pub mod point_figure;
37mod post;
38mod price_source;
39pub mod quote;
40pub mod range_bar;
41pub mod renko;
42mod replay;
43mod revision;
44mod series_scale;
45mod series_style;
46pub mod session;
47mod signal;
48mod strategy;
49mod study;
50mod time_scale_event;
51mod timeframe;
52mod timescale;
53mod timezone;
54pub mod tpo;
55mod trading;
56mod watchlist;
57
58pub use alert::{
59    Alert, AlertCalendar, AlertChannelComparison, AlertComparison, AlertCondition, AlertFrequency,
60    AlertId, AlertInputs, AlertRule, AlertSource, AlertStatus, AlertStudy, NotificationSchedule,
61    NotificationWindow,
62};
63pub use bar::{Bar, BarColors, BarData};
64#[cfg(feature = "widget")]
65pub(crate) use bar::{index_at, index_at_with_timeframe, time_at, time_at_with_timeframe};
66pub(crate) use bar::{last_with_value, percent_change};
67pub use box_size::{BoxSizeMethod, PriceBasedSource};
68pub use chart_style::{
69    AreaStyle, BarsStyle, BaselineStyle, CandlesStyle, ChartTypeStyles, HlcAreaStyle, KagiStyle,
70    LastPriceAnimation, LinePlotStyle, LineType,
71};
72pub use chart_type::{BaselineLevel, ChartType, ChartTypeCategory, ChartTypeParams};
73pub use clock::BarClock;
74pub use color::ColorType;
75pub use date_range::DateRange;
76pub use instrument::{
77    DataStatus, FeedService, Instrument, OverrideWindow, ProfileResolution, Session,
78    SessionOverride, SessionWindow, SymbolType,
79};
80pub use kagi::{KagiConfig, KagiLine, KagiThickness, to_kagi_lines};
81pub use line_break::{
82    LineBreakConfig, LineBreakLine, LineBreakSignal, LineDirection,
83    detect_signal as detect_line_break_signal, to_line_break_lines,
84};
85pub use markers::{
86    ChangeMarkers, LineStyle, Marker, MarkerOptions, MarkerPos, MarkerShape, MarkerZOrder,
87    PriceLine,
88};
89pub use parse::ParseError;
90pub use point_figure::{
91    ColumnDirection, PnfBoxesError, PnfColumn, PointFigureConfig, to_pnf_columns,
92};
93pub use post::{PostContent, PostLink};
94pub use price_source::PriceSource;
95pub use quote::{
96    ExtendedHoursQuote, ExtendedSession, Level1Quote, OrderBook, OrderBookLevel, Quote, Tick,
97    TradeConditions,
98};
99pub use range_bar::{
100    RangeBar, RangeBarConfig, RangeBarSource, to_range_bars_from_ohlc, to_range_bars_from_ticks,
101};
102pub use renko::{RenkoBrick, RenkoConfig, RenkoDirection, to_renko_bricks};
103pub use replay::ReplaySession;
104pub(crate) use revision::next_revision;
105pub use series_scale::SeriesScale;
106pub use series_style::{
107    AutoscaleProvider, PriceLineSource, SeriesAlerts, SeriesDisplayFormat, SeriesFormat,
108    SeriesStyle,
109};
110pub use session::{
111    CompositeSessionProvider, DailySessionProvider, ExchangeSessionProvider,
112    MonthlySessionProvider, SessionBreak, SessionBreakType, SessionProvider, WeeklySessionProvider,
113    find_session_breaks,
114};
115pub use signal::{Direction, Signal, SignalId, SignalShape};
116pub use strategy::{
117    StrategyComparison, StrategyExcursion, StrategyExit, StrategyPerformance, StrategyReport,
118    StrategyTrade,
119};
120pub use study::{InputValue, StudySource};
121pub use time_scale_event::{EventKind, EventVisibility, TimeScaleEvent};
122pub use timeframe::{Timeframe, TimeframeUnit};
123pub use timescale::{LogicalRange, TimeScale};
124pub use timezone::{TimezoneId, TimezoneOffset};
125pub(crate) use timezone::{local_instant, local_time};
126pub use tpo::{
127    ProfileShape, SessionType, TPOColorMode, TPOConfig, TPODisplayMode, TPOLetter, TPOProfile,
128    derive_tick_size, to_tpo_profiles,
129};
130pub(crate) use trading::rounding_tolerance;
131pub use trading::{
132    Account, BalanceChange, Capabilities, Execution, JournalEntry, Order, OrderId, OrderPreview,
133    OrderRequest, OrderSide, OrderStatus, OrderType, Position, PositionBracket, TakeProfitLevel,
134    TimeInForce, TradingRules,
135};
136
137pub use watchlist::{Watchlist, WatchlistFlag, WatchlistItem};
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142    use chrono::{DateTime, Duration, TimeZone, Utc};
143
144    /// One-minute bars from 2024-01-01 whose open, high, low and close are each price.
145    fn flat_bars(prices: impl Iterator<Item = f64>) -> Vec<Bar> {
146        let start = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
147        prices
148            .enumerate()
149            .map(|(i, p)| Bar {
150                time: start + Duration::minutes(i as i64),
151                open: p,
152                high: p,
153                low: p,
154                close: p,
155                volume: 0.0,
156                colors: None,
157                payload: None,
158            })
159            .collect()
160    }
161
162    #[test]
163    fn price_based_charts_keep_their_newest_elements_over_the_limit() {
164        let alternating = |n: usize| flat_bars((0..n).map(|i| [100.0, 110.0][i % 2]));
165        // Each series produces more elements than the limit, twice as many for Renko and range bars.
166        let (short, long) = (alternating(201), alternating(20_001));
167        let rising = flat_bars((0..20_001).map(|i| 100.0 + i as f64));
168        type Run = dyn Fn(&[Bar]) -> (usize, Option<DateTime<Utc>>);
169        let cases: [(&str, &[Bar], usize, &Run); 5] = [
170            ("renko", &short, 10_000, &|b| {
171                let v = to_renko_bricks(b, &RenkoConfig::new(0.05));
172                (v.len(), v.last().map(|e| e.ts))
173            }),
174            ("range bars", &short, 5_000, &|b| {
175                let v = to_range_bars_from_ohlc(b, &RangeBarConfig::new(0.05));
176                (v.len(), v.last().map(|e| e.end_time))
177            }),
178            ("kagi", &long, 10_000, &|b| {
179                let v = to_kagi_lines(b, &KagiConfig::new(0.02));
180                (v.len(), v.last().map(|e| e.ts))
181            }),
182            ("point and figure", &long, 5_000, &|b| {
183                let v = to_pnf_columns(b, &PointFigureConfig::new(0.02, 3));
184                (v.len(), v.last().map(|e| e.end_time))
185            }),
186            ("line break", &rising, 10_000, &|b| {
187                let v = to_line_break_lines(b, &LineBreakConfig::new(3));
188                (v.len(), v.last().map(|e| e.ts))
189            }),
190        ];
191        let failures: Vec<String> = cases
192            .iter()
193            .filter_map(|&(name, bars, limit, run)| {
194                let (len, last) = run(bars);
195                let want = bars.last().map(|b| b.time);
196                (len != limit || last != want).then(|| {
197                    format!("{name}: {len} elements ending {last:?}, want {limit} ending {want:?}")
198                })
199            })
200            .collect();
201        assert!(failures.is_empty(), "{failures:#?}");
202    }
203
204    #[test]
205    fn price_based_charts_use_the_configured_size() {
206        // Prices and sizes are binary fractions, so scaling by 2^-20 is exact.
207        let u = 1.0 / 256.0;
208        let z = [
209            100.0,
210            110.0,
211            110.0 - 5.0 * u,
212            110.0,
213            110.0 - 13.0 * u,
214            110.0,
215            100.0,
216        ];
217        type Run = dyn Fn(&[Bar], f64) -> Vec<(f64, f64)>;
218        type Pair = (f64, f64);
219        // (name, size, build as (open, close) pairs, expected count, first, last)
220        let cases: [(&str, f64, &Run, usize, Pair, Pair); 4] = [
221            (
222                "renko",
223                1.0 / 32.0,
224                &|b, s| {
225                    to_renko_bricks(b, &RenkoConfig::new(s))
226                        .iter()
227                        .map(|e| (e.open, e.close))
228                        .collect()
229                },
230                639,
231                (100.0, 100.03125),
232                (100.03125, 100.0),
233            ),
234            (
235                "kagi",
236                1.0 / 64.0,
237                &|b, s| {
238                    to_kagi_lines(b, &KagiConfig::new(s))
239                        .iter()
240                        .map(|e| (e.start_price, e.end_price))
241                        .collect()
242                },
243                6,
244                (100.0, 110.0),
245                (110.0, 100.0),
246            ),
247            (
248                "point and figure",
249                1.0 / 64.0,
250                &|b, s| {
251                    to_pnf_columns(b, &PointFigureConfig::new(s, 3))
252                        .iter()
253                        .map(|e| (e.start_price, e.end_price))
254                        .collect()
255                },
256                4,
257                (100.0, 110.0),
258                (109.984375, 100.0),
259            ),
260            (
261                "range bars",
262                1.0 / 32.0,
263                &|b, s| {
264                    to_range_bars_from_ohlc(b, &RangeBarConfig::new(s))
265                        .iter()
266                        .map(|e| (e.open, e.close))
267                        .collect()
268                },
269                643,
270                (100.0, 100.03125),
271                (100.0, 100.0),
272            ),
273        ];
274        let mut failures = Vec::new();
275        for (name, size, run, count, first, last) in cases {
276            for scale in [1.0, 2f64.powi(-20)] {
277                let got = run(&flat_bars(z.iter().map(|p| p * scale)), size * scale);
278                let want = (
279                    count,
280                    (first.0 * scale, first.1 * scale),
281                    (last.0 * scale, last.1 * scale),
282                );
283                if (got.len(), got[0], got[got.len() - 1]) != want {
284                    failures.push(format!(
285                        "{name} at scale {scale}: {} elements, want {want:?}",
286                        got.len()
287                    ));
288                }
289            }
290            let none = run(&flat_bars(z.iter().copied()), 0.0);
291            if !none.is_empty() {
292                failures.push(format!("{name}: {} elements for size 0", none.len()));
293            }
294        }
295        // A step of 1.1e13 sizes is not built: the build starts at the bar after it.
296        let u40 = 2f64.powi(-40);
297        let bars = flat_bars([100.0, 110.0, 110.0 + 4.0 * u40].into_iter());
298        let v = to_renko_bricks(&bars, &RenkoConfig::new(u40));
299        if v.len() != 4
300            || v.last().map(|e| (e.open, e.close)) != Some((110.0 + 3.0 * u40, 110.0 + 4.0 * u40))
301        {
302            failures.push(format!("renko work bound: {} bricks", v.len()));
303        }
304        assert!(failures.is_empty(), "{failures:#?}");
305    }
306}