use crate::config::{ChartConfig, ChartOptions, RealtimeButtonPos};
use crate::data::DataSource;
use crate::drawings::DrawingManager;
use crate::model::BarData;
use crate::model::ChartType;
use crate::model::Timeframe;
use crate::studies::IndicatorRegistry;
use crate::theme::Theme;
use crate::widget::Chart;
use log;
pub struct ChartBuilder {
data_src: Option<Box<dyn DataSource>>,
symbol: Option<String>,
timeframe: Timeframe,
theme: Theme,
chart_type: ChartType,
config: ChartConfig,
chart_options: ChartOptions,
indicators: Option<IndicatorRegistry>,
drawing_manager: Option<DrawingManager>,
visible_candles: usize,
initial_bars: Option<usize>,
auto_fetch_on_symbol_change: bool,
auto_fetch_on_timeframe_change: bool,
}
impl ChartBuilder {
pub fn new() -> Self {
Self {
data_src: None,
symbol: None,
timeframe: Timeframe::Min1,
theme: Theme::dark(),
chart_type: ChartType::Candles,
config: ChartConfig::default(),
chart_options: ChartOptions::default(),
indicators: None,
drawing_manager: None,
visible_candles: 100,
initial_bars: Some(1000), auto_fetch_on_symbol_change: true,
auto_fetch_on_timeframe_change: true,
}
}
pub fn extended() -> Self {
Self::new()
.with_drawing_tools()
.with_indicators(IndicatorRegistry::new())
}
pub fn options_chart() -> Self {
let mut chart_options = ChartOptions::default();
chart_options.time_scale.time_visible = false;
chart_options.time_scale.seconds_visible = false;
chart_options.time_scale.ticks_visible = false;
Self::new()
.with_chart_options(chart_options)
.with_chart_type(ChartType::Line)
}
pub fn price_chart() -> Self {
use crate::config::CrosshairOptions;
let config = ChartConfig {
show_grid: false,
..ChartConfig::default()
};
let chart_options = ChartOptions {
crosshair: CrosshairOptions {
vert_line_visible: false,
horz_line_visible: false,
label_visible: false,
..CrosshairOptions::default()
},
..ChartOptions::default()
};
Self::new()
.with_config(config)
.with_chart_options(chart_options)
.with_chart_type(ChartType::Line)
.with_visible_candles(50)
}
#[must_use]
pub fn with_data_src(mut self, source: Box<dyn DataSource>) -> Self {
self.data_src = Some(source);
self
}
#[must_use]
pub fn with_symbol(mut self, symbol: impl Into<String>) -> Self {
self.symbol = Some(symbol.into());
self
}
#[must_use]
pub fn with_timeframe(mut self, timeframe: Timeframe) -> Self {
self.timeframe = timeframe;
self
}
#[must_use]
pub fn with_theme(mut self, theme: Theme) -> Self {
self.theme = theme;
self
}
#[must_use]
pub fn with_chart_type(mut self, chart_type: ChartType) -> Self {
self.chart_type = chart_type;
self
}
#[must_use]
pub fn with_config(mut self, config: ChartConfig) -> Self {
self.config = config;
self
}
#[must_use]
pub fn with_chart_options(mut self, options: ChartOptions) -> Self {
self.chart_options = options;
self
}
#[must_use]
pub fn with_session_breaks(mut self, show: bool) -> Self {
self.config.show_session_breaks = show;
self
}
#[must_use]
pub fn with_indicators(mut self, registry: IndicatorRegistry) -> Self {
self.indicators = Some(registry);
self
}
#[must_use]
pub fn with_drawing_tools(mut self) -> Self {
self.drawing_manager = Some(DrawingManager::new());
self
}
#[must_use]
pub fn with_visible_candles(mut self, count: usize) -> Self {
self.visible_candles = count;
self
}
#[must_use]
pub fn with_initial_bars(mut self, bars: Option<usize>) -> Self {
self.initial_bars = bars;
self
}
#[must_use]
pub fn with_auto_fetch(mut self, enabled: bool) -> Self {
self.auto_fetch_on_symbol_change = enabled;
self.auto_fetch_on_timeframe_change = enabled;
self
}
#[must_use]
pub fn with_auto_fetch_on_symbol_change(mut self, enabled: bool) -> Self {
self.auto_fetch_on_symbol_change = enabled;
self
}
#[must_use]
pub fn with_auto_fetch_on_timeframe_change(mut self, enabled: bool) -> Self {
self.auto_fetch_on_timeframe_change = enabled;
self
}
#[must_use]
pub fn with_left_price_scale(
mut self,
visible: bool,
mode: crate::scales::PriceScaleMode,
) -> Self {
self.config.show_left_axis = visible;
self.config.left_axis_scale_mode = mode;
self
}
#[must_use]
pub fn with_right_price_scale(
mut self,
visible: bool,
mode: crate::scales::PriceScaleMode,
) -> Self {
self.config.show_right_axis = visible;
self.config.right_axis_scale_mode = mode;
self
}
#[must_use]
pub fn with_left_axis_width(mut self, width: f32) -> Self {
self.config.left_axis_width = width;
self
}
#[must_use]
pub fn with_right_axis_width(mut self, width: f32) -> Self {
self.config.right_axis_width = width;
self
}
#[must_use]
pub fn with_realtime_btn(mut self, visible: bool) -> Self {
self.config.show_realtime_btn = visible;
self
}
#[must_use]
pub fn with_realtime_button_pos(mut self, position: RealtimeButtonPos) -> Self {
self.config.realtime_button_pos = position;
self
}
#[must_use]
pub fn with_realtime_button_text(mut self, text: impl Into<String>) -> Self {
self.config.realtime_button_text = Some(text.into());
self
}
#[must_use]
pub fn with_realtime_button_size(mut self, width: f32, height: f32) -> Self {
self.config.realtime_button_size = (width, height);
self
}
pub fn build(mut self) -> TradingChart {
self.config = self.theme.apply_to_config(self.config);
let mut chart =
Chart::with_config(BarData::new(), self.config).with_chart_options(self.chart_options);
chart.set_chart_type(self.chart_type);
chart.set_visible_bars(self.visible_candles);
let symbol = self.symbol.unwrap_or_else(|| "BTCUSDT".to_string());
chart.set_symbol(&symbol);
chart.set_timeframe_label(&self.timeframe.as_str());
if let Some(ref mut source) = self.data_src {
let _ = source.subscribe(symbol.clone(), self.timeframe);
if let Some(bars_cnt) = self.initial_bars
&& source.supports_historical()
{
let request = crate::data::HistoricalDataRequest {
symbol: symbol.clone(),
timeframe: self.timeframe,
end_ts_millis: chrono::Utc::now().timestamp_millis(),
limit: bars_cnt,
};
match source.fetch_historical(request) {
Ok(bars) => {
if !bars.is_empty() {
log::info!(
"[ChartBuilder] Loaded {} historical bars for {}",
bars.len(),
symbol
);
let data = crate::model::BarData::from_bars(bars);
chart.update_data(data);
}
}
Err(e) => {
log::warn!("[ChartBuilder] Failed to fetch historical data: {e}");
}
}
}
}
TradingChart {
chart,
data_src: self.data_src,
symbol,
timeframe: self.timeframe,
indicators: self.indicators.unwrap_or_default(),
drawing_manager: self.drawing_manager,
initial_bars: self.initial_bars.unwrap_or(1000),
auto_fetch_on_symbol_change: self.auto_fetch_on_symbol_change,
auto_fetch_on_timeframe_change: self.auto_fetch_on_timeframe_change,
is_fetching_historical: false,
historical_fetch_threshold: 150, has_more_historical_data: true, #[cfg(feature = "ui")]
theme: self.theme,
#[cfg(feature = "ui")]
context_menus: ContextMenus::default(),
}
}
}
impl Default for ChartBuilder {
fn default() -> Self {
Self::new()
}
}
pub struct TradingChart {
pub chart: Chart,
pub data_src: Option<Box<dyn DataSource>>,
pub symbol: String,
pub timeframe: Timeframe,
pub indicators: IndicatorRegistry,
pub drawing_manager: Option<DrawingManager>,
initial_bars: usize,
auto_fetch_on_symbol_change: bool,
auto_fetch_on_timeframe_change: bool,
is_fetching_historical: bool,
historical_fetch_threshold: usize,
has_more_historical_data: bool,
#[cfg(feature = "ui")]
theme: Theme,
#[cfg(feature = "ui")]
context_menus: ContextMenus,
}
#[cfg(feature = "ui")]
#[derive(Default)]
struct ContextMenus {
chart: crate::ui::context_menu::ChartContextMenu,
series: crate::ui::dialogs::SeriesContextMenu,
drawing: crate::ui::dialogs::DrawingContextMenu,
pending_action: Option<crate::ui::context_menu::ChartContextAction>,
}
impl TradingChart {
pub fn update(&mut self) {
if let Some(ref mut source) = self.data_src {
let updates = source.poll();
for update in updates {
use crate::data::DataUpdate;
match update {
DataUpdate::FullDataset { symbol, bars } if symbol == self.symbol => {
let data = BarData::from_bars(bars);
self.chart.update_data(data.clone());
self.indicators.calculate_all(&data.bars);
let _ = symbol; }
DataUpdate::NewBars { symbol, bars }
if symbol == self.symbol
&& !bars.is_empty() =>
{
let mut existing_bars = self.chart.data().bars.clone();
let bars_before = existing_bars.len();
for new_bar in bars {
if let Some(last) = existing_bars.last_mut() {
if last.time == new_bar.time {
*last = new_bar;
} else if new_bar.time > last.time {
existing_bars.push(new_bar);
}
} else {
existing_bars.push(new_bar);
}
}
let bars_after = existing_bars.len();
let data = BarData::from_bars(existing_bars);
self.chart.update_data(data.clone());
self.indicators.calculate_all(&data.bars);
if bars_after > bars_before {
log::debug!(
"[TradingChart] Live data: {} new bars (total: {})",
bars_after - bars_before,
bars_after
);
}
let _ = symbol; }
_ => {}
}
}
let start_idx = self.chart.get_start_idx();
let curr_bar_cnt = self.chart.data().len();
let should_fetch = start_idx < self.historical_fetch_threshold
&& !self.is_fetching_historical
&& source.supports_historical()
&& curr_bar_cnt > 0
&& self.has_more_historical_data;
if should_fetch {
self.is_fetching_historical = true;
let curr_data = self.chart.data();
if let Some(oldest_bar) = curr_data.bars.first() {
let progressive_batch_size = self.initial_bars.max(600);
log::info!(
"[TradingChart] Progressive loading: Fetching {} more bars before {}",
progressive_batch_size,
oldest_bar.time
);
let request = crate::data::HistoricalDataRequest {
symbol: self.symbol.clone(),
timeframe: self.timeframe,
end_ts_millis: oldest_bar.time.timestamp_millis(), limit: progressive_batch_size,
};
match source.fetch_historical(request) {
Ok(mut older_bars) if !older_bars.is_empty() => {
log::info!(
"[TradingChart] Progressive loading: Loaded {} older bars",
older_bars.len()
);
if let Some(first_existing) = curr_data.bars.first() {
older_bars.retain(|bar| bar.time < first_existing.time);
}
if older_bars.is_empty() {
log::info!(
"[TradingChart] Progressive loading: All fetched bars were duplicates, reached end of data"
);
self.has_more_historical_data = false;
self.is_fetching_historical = false;
return; }
older_bars.extend(curr_data.bars.iter().cloned());
let new_data = BarData::from_bars(older_bars);
let bars_added = new_data.len() - curr_bar_cnt;
if let Some(ref mut dm) = self.drawing_manager {
dm.shift_bar_indices(bars_added as f32);
}
self.chart.update_data(new_data.clone());
self.indicators.calculate_all(&new_data.bars);
}
Ok(_) => {
log::info!(
"[TradingChart] Progressive loading: No more historical data available (empty response)"
);
self.has_more_historical_data = false;
}
Err(e) => {
log::warn!("[TradingChart] Progressive loading failed: {e}");
}
}
}
self.is_fetching_historical = false;
}
}
}
pub fn show(&mut self, ui: &mut egui::Ui) {
self.chart
.show_with_indicators(ui, self.drawing_manager.as_mut(), Some(&self.indicators));
if let Some(index) = self.chart.take_indicator_remove() {
self.indicators.remove_indicator(index);
let bars = self.chart.data().bars.clone();
self.indicators.calculate_all(&bars);
}
#[cfg(feature = "ui")]
self.handle_context_menus(ui.ctx());
}
#[cfg(feature = "ui")]
fn handle_context_menus(&mut self, ctx: &egui::Context) {
use crate::widget::RightClickTarget;
if let Some(target) = self.chart.take_right_click() {
self.context_menus.chart.close();
self.context_menus.series.close();
self.context_menus.drawing.close();
match target {
RightClickTarget::Element { id, pos, price, .. } => {
self.open_element_menu(id, pos, price);
}
RightClickTarget::Drawing { drawing_id, pos } => {
self.open_drawing_menu(drawing_id, pos);
}
RightClickTarget::Background { pos, price } => {
self.context_menus
.chart
.set_indicator_cnt(self.indicators.indicators().len());
self.context_menus.chart.set_symbol(self.symbol.clone());
self.context_menus.chart.open(pos, price);
}
}
}
let theme = &self.theme;
self.context_menus.chart.show(ctx, theme);
self.context_menus.series.show(ctx, theme);
self.context_menus.drawing.show(ctx, theme);
use crate::ui::context_menu::ChartContextAction;
use crate::ui::context_menu::ContextMenuAction;
use crate::ui::dialogs::{DrawingContextMenuAction, SeriesContextMenuAction};
let chart_action = self.context_menus.chart.take_action();
if chart_action != ContextMenuAction::None {
self.context_menus.pending_action = Some(ChartContextAction::Chart(chart_action));
}
let series_action = self.context_menus.series.take_action();
if series_action != SeriesContextMenuAction::None {
self.context_menus.pending_action = Some(ChartContextAction::Series(series_action));
}
let drawing_action = self.context_menus.drawing.take_action();
if drawing_action != DrawingContextMenuAction::None {
self.context_menus.pending_action = Some(ChartContextAction::Drawing(drawing_action));
}
}
#[cfg(feature = "ui")]
fn open_element_menu(
&mut self,
id: crate::chart::selection::ChartElementId,
pos: egui::Pos2,
price: f64,
) {
use crate::chart::selection::ChartElementId;
let label = match id {
ChartElementId::Series(_) => self.symbol.clone(),
ChartElementId::OverlayIndicator(idx) | ChartElementId::PaneIndicator(idx) => self
.indicators
.indicators()
.get(idx)
.map(|ind| ind.name().to_string())
.unwrap_or_else(|| self.symbol.clone()),
};
let series_id = match id {
ChartElementId::Series(sid) => sid,
_ => crate::chart::selection::SeriesId::MAIN,
};
self.context_menus.series.set_context(label, price);
self.context_menus.series.open(pos, series_id);
}
#[cfg(feature = "ui")]
fn open_drawing_menu(&mut self, drawing_id: usize, pos: egui::Pos2) {
if let Some(dm) = self.drawing_manager.as_ref()
&& let Some(drawing) = dm.drawings.iter().find(|d| d.id == drawing_id)
{
let type_name = drawing.tool_type.as_str().to_string();
self.context_menus
.drawing
.set_context(type_name, drawing.locked, drawing.visible);
}
self.context_menus.drawing.open(pos, drawing_id);
}
#[cfg(feature = "ui")]
pub fn take_context_action(&mut self) -> Option<crate::ui::context_menu::ChartContextAction> {
self.context_menus.pending_action.take()
}
pub fn config_mut(&mut self) -> &mut ChartConfig {
self.chart.config_mut()
}
pub fn apply_series_settings(&mut self, settings: &crate::chart::series::SeriesSettings) {
self.chart.apply_series_settings(settings);
}
pub fn remove_indicator(&mut self, index: usize) -> Option<Box<dyn crate::studies::Indicator>> {
self.indicators.remove_indicator(index)
}
pub fn set_indicator_visible(&mut self, index: usize, visible: bool) -> bool {
match self.indicators.indicators_mut().get_mut(index) {
Some(indicator) => {
indicator.set_visible(visible);
true
}
None => false,
}
}
pub fn remove_drawing(&mut self, drawing_id: usize) -> bool {
let Some(dm) = self.drawing_manager.as_mut() else {
return false;
};
if dm.drawings.iter().any(|d| d.id == drawing_id) {
dm.delete_drawing(drawing_id);
true
} else {
false
}
}
pub fn set_symbol(&mut self, symbol: impl Into<String>) {
let symbol = symbol.into();
self.has_more_historical_data = true;
if let Some(ref mut source) = self.data_src {
let _ = source.subscribe(symbol.clone(), self.timeframe);
if self.auto_fetch_on_symbol_change && source.supports_historical() {
let request = crate::data::HistoricalDataRequest {
symbol: symbol.clone(),
timeframe: self.timeframe,
end_ts_millis: chrono::Utc::now().timestamp_millis(),
limit: self.initial_bars,
};
match source.fetch_historical(request) {
Ok(bars) => {
if !bars.is_empty() {
log::info!(
"[TradingChart] Loaded {} historical bars for {}",
bars.len(),
symbol
);
let data = crate::model::BarData::from_bars(bars);
self.chart.update_data(data.clone());
self.indicators.calculate_all(&data.bars);
}
}
Err(e) => {
log::warn!("[TradingChart] Failed to fetch historical data: {e}");
}
}
}
}
self.symbol = symbol;
self.chart.set_symbol(&self.symbol);
self.chart.set_timeframe_label(&self.timeframe.as_str());
}
pub fn set_timeframe(&mut self, timeframe: Timeframe) {
if self.timeframe == timeframe {
return;
}
log::info!(
"[TradingChart] Changing timeframe from {:?} to {:?}",
self.timeframe,
timeframe
);
self.has_more_historical_data = true;
let empty_data = BarData::default();
self.chart.update_data(empty_data.clone());
self.indicators.calculate_all(&[]);
if let Some(ref mut source) = self.data_src {
let _ = source.subscribe(self.symbol.clone(), timeframe);
if self.auto_fetch_on_timeframe_change && source.supports_historical() {
let request = crate::data::HistoricalDataRequest {
symbol: self.symbol.clone(),
timeframe,
end_ts_millis: chrono::Utc::now().timestamp_millis(),
limit: self.initial_bars,
};
match source.fetch_historical(request) {
Ok(bars) => {
if !bars.is_empty() {
log::info!(
"[TradingChart] Loaded {} historical bars for {} timeframe",
bars.len(),
timeframe
);
let data = crate::model::BarData::from_bars(bars);
self.chart.update_data(data.clone());
self.indicators.calculate_all(&data.bars);
}
}
Err(e) => {
log::warn!("[TradingChart] Failed to fetch historical data: {e}");
}
}
}
}
self.timeframe = timeframe;
self.chart.set_timeframe_label(&timeframe.as_str());
}
pub fn set_chart_type(&mut self, chart_type: ChartType) {
self.chart.set_chart_type(chart_type);
}
pub fn get_last_bar(&self) -> Option<&crate::model::Bar> {
self.chart.data().bars.last()
}
pub fn get_symbol(&self) -> &str {
&self.symbol
}
pub fn selected_element(&self) -> Option<crate::chart::selection::ChartElementId> {
self.chart.selected_element()
}
pub fn clear_selection(&mut self) {
self.chart.clear_selection();
}
pub fn toggle_main_series_visibility(&mut self) {
log::info!("Toggle main series visibility requested (infrastructure pending)");
}
}