1pub(crate) const MAX_WORK: f64 = 2_000_000.0;
11
12pub(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 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 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 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 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 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}