|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| use crate::app::AppState;
|
| use crate::zmq_bridge::OrderRequest;
|
| use egui;
|
| use std::fs::OpenOptions;
|
| use std::io::Write;
|
|
|
|
|
|
|
| pub fn render_trading_panel(ui: &mut egui::Ui, state: &mut AppState) {
|
| ui.heading("Trading Panel");
|
| ui.separator();
|
|
|
|
|
| ui.collapsing("Account Info", |ui| {
|
| egui::Grid::new("account_grid")
|
| .num_columns(2)
|
| .spacing([10.0, 4.0])
|
| .show(ui, |ui| {
|
| ui.label("Balance:");
|
| ui.colored_label(
|
| egui::Color32::from_rgb(100, 200, 100),
|
| format!("${:.2}", state.balance),
|
| );
|
| ui.end_row();
|
| ui.label("Equity:");
|
| ui.colored_label(
|
| egui::Color32::from_rgb(100, 180, 255),
|
| format!("${:.2}", state.equity),
|
| );
|
| ui.end_row();
|
| ui.label("Margin:");
|
| ui.colored_label(
|
| egui::Color32::from_rgb(255, 200, 100),
|
| format!("${:.2}", state.margin),
|
| );
|
| ui.end_row();
|
| ui.label("Free Margin:");
|
| ui.colored_label(
|
| egui::Color32::from_rgb(100, 255, 200),
|
| format!("${:.2}", state.free_margin),
|
| );
|
| ui.end_row();
|
| });
|
| });
|
|
|
| ui.separator();
|
|
|
|
|
| ui.heading("Chart Settings");
|
| ui.horizontal(|ui| {
|
| ui.label("Bin Size:");
|
| let resp = ui.add(
|
| egui::TextEdit::singleline(&mut state.bin_size_str).desired_width(60.0),
|
| );
|
| if resp.lost_focus() {
|
| if let Ok(v) = state.bin_size_str.parse::<f64>() {
|
| if v > 0.0 {
|
| state.bin_size = v;
|
| state.profiles_dirty = true;
|
| }
|
| }
|
| state.bin_size_str = format!("{}", state.bin_size);
|
| }
|
| });
|
|
|
| ui.separator();
|
|
|
|
|
| ui.heading("Historical Data");
|
| ui.add_space(3.0);
|
| egui::Grid::new("history_grid")
|
| .num_columns(2)
|
| .spacing([10.0, 4.0])
|
| .show(ui, |ui| {
|
| ui.label("Start:");
|
| ui.add(
|
| egui::TextEdit::singleline(&mut state.history_start).desired_width(100.0),
|
| );
|
| ui.end_row();
|
| ui.label("End:");
|
| ui.add(
|
| egui::TextEdit::singleline(&mut state.history_end).desired_width(100.0),
|
| );
|
| ui.end_row();
|
| ui.label("Timeframe:");
|
| egui::ComboBox::from_id_source("dl_tf")
|
| .selected_text(&state.history_tf)
|
| .show_ui(ui, |ui| {
|
| for tf in &["M1", "M5", "M15", "H1", "H4", "D1"] {
|
| ui.selectable_value(&mut state.history_tf, tf.to_string(), *tf);
|
| }
|
| });
|
| ui.end_row();
|
| ui.label("Mode:");
|
| egui::ComboBox::from_id_source("dl_mode")
|
| .selected_text(&state.history_mode)
|
| .show_ui(ui, |ui| {
|
| ui.selectable_value(&mut state.history_mode, "OHLC".to_string(), "OHLC");
|
| ui.selectable_value(&mut state.history_mode, "TICKS".to_string(), "TICKS");
|
| });
|
| ui.end_row();
|
| });
|
|
|
| ui.add_space(3.0);
|
| if ui.button("Download History (CSV)").clicked() {
|
| send_download_request(state);
|
| }
|
|
|
| ui.separator();
|
|
|
|
|
| ui.heading("Live Recording");
|
| ui.horizontal(|ui| {
|
| ui.label(if state.is_recording { "REC" } else { "Idle" });
|
| if ui
|
| .button(if state.is_recording { "Stop" } else { "Start" })
|
| .clicked()
|
| {
|
| toggle_recording(state);
|
| }
|
| });
|
|
|
| ui.separator();
|
|
|
|
|
| ui.heading("Trade Controls");
|
|
|
|
|
| ui.horizontal(|ui| {
|
| if ui.button("-").clicked() {
|
| adjust_lot(state, -state.lot_step);
|
| }
|
| let resp = ui.add(
|
| egui::TextEdit::singleline(&mut state.lot_size_str).desired_width(60.0),
|
| );
|
| if resp.lost_focus() {
|
| if let Ok(v) = state.lot_size_str.parse::<f64>() {
|
| state.lot_size = v.max(state.min_lot).min(state.max_lot);
|
| state.lot_size_str = format!("{:.2}", state.lot_size);
|
| }
|
| }
|
| if ui.button("+").clicked() {
|
| adjust_lot(state, state.lot_step);
|
| }
|
| ui.label(format!("Lots (max {:.1})", state.max_lot));
|
| });
|
|
|
| ui.add_space(3.0);
|
| ui.label("Market Orders:");
|
| ui.horizontal(|ui| {
|
| if ui.button("BUY").clicked() {
|
| send_order(state, "market_buy", None, None);
|
| }
|
| if ui.button("SELL").clicked() {
|
| send_order(state, "market_sell", None, None);
|
| }
|
| });
|
|
|
| ui.add_space(3.0);
|
| ui.label("Pending Orders:");
|
| ui.horizontal(|ui| {
|
| ui.label("@ Price:");
|
| ui.add(
|
| egui::TextEdit::singleline(&mut state.limit_price_str).desired_width(70.0),
|
| );
|
| });
|
| ui.horizontal(|ui| {
|
| let p: f64 = state.limit_price_str.parse().unwrap_or(0.0);
|
| if ui.small_button("Buy Lmt").clicked() {
|
| send_order(state, "limit_buy", Some(p), None);
|
| }
|
| if ui.small_button("Sell Lmt").clicked() {
|
| send_order(state, "limit_sell", Some(p), None);
|
| }
|
| if ui.small_button("Buy Stp").clicked() {
|
| send_order(state, "stop_buy", Some(p), None);
|
| }
|
| if ui.small_button("Sell Stp").clicked() {
|
| send_order(state, "stop_sell", Some(p), None);
|
| }
|
| });
|
|
|
| ui.separator();
|
|
|
|
|
| if let Some(ref msg) = state.last_status {
|
| ui.heading("Status");
|
| ui.label(msg);
|
| }
|
|
|
| ui.separator();
|
|
|
|
|
| ui.collapsing("Active Positions", |ui| {
|
| if state.positions.is_empty() {
|
| ui.label("No active positions");
|
| } else {
|
| let positions = state.positions.clone();
|
| for pos in positions {
|
| ui.horizontal(|ui| {
|
| let color = if pos.pos_type == "BUY" {
|
| egui::Color32::from_rgb(100, 200, 100)
|
| } else {
|
| egui::Color32::from_rgb(255, 100, 100)
|
| };
|
| ui.colored_label(
|
| color,
|
| format!(
|
| "#{} {} {:.2}@{:.5} P:{:.2}",
|
| pos.ticket, pos.pos_type, pos.volume, pos.price, pos.profit
|
| ),
|
| );
|
| if ui.small_button("Close").clicked() {
|
| send_order(state, "close_position", Some(pos.price), Some(pos.ticket));
|
| }
|
| });
|
| }
|
| }
|
| });
|
|
|
|
|
| ui.collapsing("Pending Orders", |ui| {
|
| if state.pending_orders.is_empty() {
|
| ui.label("No pending orders");
|
| } else {
|
| let orders = state.pending_orders.clone();
|
| for ord in orders {
|
| ui.horizontal(|ui| {
|
| let color = if ord.order_type.contains("BUY") {
|
| egui::Color32::from_rgb(100, 150, 255)
|
| } else {
|
| egui::Color32::from_rgb(255, 150, 100)
|
| };
|
| ui.colored_label(
|
| color,
|
| format!(
|
| "#{} {} {:.2}@{:.5}",
|
| ord.ticket, ord.order_type, ord.volume, ord.price
|
| ),
|
| );
|
| if ui.small_button("Cancel").clicked() {
|
| send_order(state, "cancel_order", Some(ord.price), Some(ord.ticket));
|
| }
|
| });
|
| }
|
| }
|
| });
|
| }
|
|
|
|
|
|
|
| fn adjust_lot(state: &mut AppState, delta: f64) {
|
| let new = state.lot_size + delta;
|
| let steps = (new / state.lot_step).round();
|
| state.lot_size = (steps * state.lot_step).max(state.min_lot).min(state.max_lot);
|
| state.lot_size_str = format!("{:.2}", state.lot_size);
|
| }
|
|
|
| fn send_order(state: &mut AppState, order_type: &str, price: Option<f64>, ticket: Option<u64>) {
|
| let req = OrderRequest {
|
| order_type: order_type.to_string(),
|
| symbol: state.symbol.clone(),
|
| volume: state.lot_size,
|
| price: price.unwrap_or(0.0),
|
| ticket: ticket.unwrap_or(0),
|
| timeframe: None,
|
| start: None,
|
| end: None,
|
| mode: None,
|
| request_id: None,
|
| };
|
| if let Err(e) = state.order_tx.try_send(req) {
|
| state.last_status = Some(format!("Send error: {}", e));
|
| } else {
|
| state.last_status = Some("Order sent...".to_string());
|
| }
|
| }
|
|
|
| fn send_download_request(state: &mut AppState) {
|
| state.request_counter += 1;
|
| state.pending_history = Some((
|
| state.request_counter,
|
| state.symbol.replace("/", "-"),
|
| state.history_tf.clone(),
|
| state.history_mode.clone(),
|
| ));
|
| let req = OrderRequest {
|
| order_type: "download_history".to_string(),
|
| symbol: state.symbol.clone(),
|
| volume: 0.0,
|
| price: 0.0,
|
| ticket: 0,
|
| timeframe: Some(state.history_tf.clone()),
|
| start: Some(state.history_start.clone()),
|
| end: Some(state.history_end.clone()),
|
| mode: Some(state.history_mode.clone()),
|
| request_id: Some(state.request_counter),
|
| };
|
| if let Err(e) = state.order_tx.try_send(req) {
|
| state.last_status = Some(format!("Send error: {}", e));
|
| } else {
|
| state.last_status = Some("Download request sent...".to_string());
|
| }
|
| }
|
|
|
| fn toggle_recording(state: &mut AppState) {
|
| state.is_recording = !state.is_recording;
|
| if state.is_recording {
|
| state.request_counter += 1;
|
| let filename = format!(
|
| "{}/Live_{}_ID{:04}_{}.csv",
|
| state.output_dir.display(),
|
| state.symbol.replace("/", "-"),
|
| state.request_counter,
|
| chrono::Local::now().format("%Y%m%d_%H%M%S"),
|
| );
|
| match OpenOptions::new().create(true).append(true).open(&filename) {
|
| Ok(mut file) => {
|
| let _ = writeln!(file, "Time,Bid,Ask,Volume");
|
| state.live_record_file = Some(file);
|
| state.last_status = Some(format!("Recording to {}", filename));
|
| }
|
| Err(e) => {
|
| state.is_recording = false;
|
| state.last_status = Some(format!("Record error: {}", e));
|
| }
|
| }
|
| } else {
|
| state.live_record_file = None;
|
| state.last_status = Some("Recording stopped".to_string());
|
| }
|
| }
|
|
|