| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| use eframe::egui; |
| use egui_plot::{Line, Plot, PlotPoints}; |
| use serde::{Deserialize, Serialize}; |
| use tokio::sync::mpsc; |
| use zeromq::{Socket, SocketRecv, SocketSend}; |
| use std::fs::{self, OpenOptions}; |
| use std::io::Write; |
| use std::path::PathBuf; |
|
|
| |
| |
| |
|
|
| #[derive(Clone, Debug, Deserialize)] |
| #[allow(dead_code)] |
| struct PositionData { |
| ticket: u64, |
| #[serde(rename = "type")] |
| pos_type: String, |
| volume: f64, |
| price: f64, |
| profit: f64, |
| } |
|
|
| #[derive(Clone, Debug, Deserialize)] |
| #[allow(dead_code)] |
| struct PendingOrderData { |
| ticket: u64, |
| #[serde(rename = "type")] |
| order_type: String, |
| volume: f64, |
| price: f64, |
| } |
|
|
| #[derive(Clone, Debug, Deserialize)] |
| struct TickData { |
| symbol: String, |
| bid: f64, |
| ask: f64, |
| time: i64, |
| #[serde(default)] |
| volume: u64, |
| |
| #[serde(default)] |
| balance: f64, |
| #[serde(default)] |
| equity: f64, |
| #[serde(default)] |
| margin: f64, |
| #[serde(default)] |
| free_margin: f64, |
| |
| #[serde(default)] |
| min_lot: f64, |
| #[serde(default)] |
| max_lot: f64, |
| #[serde(default)] |
| lot_step: f64, |
| |
| |
| #[serde(default)] |
| positions: Vec<PositionData>, |
| #[serde(default)] |
| orders: Vec<PendingOrderData>, |
| } |
|
|
| #[derive(Clone, Debug, Serialize)] |
| struct OrderRequest { |
| #[serde(rename = "type")] |
| order_type: String, |
| symbol: String, |
| volume: f64, |
| price: f64, |
| #[serde(default)] |
| ticket: u64, |
| |
| #[serde(skip_serializing_if = "Option::is_none")] |
| timeframe: Option<String>, |
| #[serde(skip_serializing_if = "Option::is_none")] |
| start: Option<String>, |
| #[serde(skip_serializing_if = "Option::is_none")] |
| end: Option<String>, |
| #[serde(skip_serializing_if = "Option::is_none")] |
| mode: Option<String>, |
| #[serde(skip_serializing_if = "Option::is_none")] |
| request_id: Option<u64>, |
| } |
|
|
| #[derive(Clone, Debug, Deserialize)] |
| struct OrderResponse { |
| success: bool, |
| ticket: Option<i64>, |
| error: Option<String>, |
| message: Option<String>, |
| } |
|
|
| |
| #[derive(Clone, Debug)] |
| struct OrderBreakline { |
| index: usize, |
| order_type: String, |
| ticket: i64, |
| } |
|
|
| |
| |
| |
|
|
| struct Mt5ChartApp { |
| |
| tick_receiver: mpsc::Receiver<TickData>, |
| data: Vec<TickData>, |
| symbol: String, |
| |
| |
| balance: f64, |
| equity: f64, |
| margin: f64, |
| free_margin: f64, |
| min_lot: f64, |
| max_lot: f64, |
| lot_step: f64, |
| |
| |
| order_sender: mpsc::Sender<OrderRequest>, |
| response_receiver: mpsc::Receiver<OrderResponse>, |
| |
| |
| lot_size: f64, |
| lot_size_str: String, |
| limit_price: String, |
| #[allow(dead_code)] |
| stop_price: String, |
| last_order_result: Option<String>, |
| |
| |
| history_start_date: String, |
| history_end_date: String, |
| history_tf: String, |
| history_mode: String, |
| |
| |
| is_recording: bool, |
| live_record_file: Option<std::fs::File>, |
| |
| |
| positions: Vec<PositionData>, |
| pending_orders: Vec<PendingOrderData>, |
| |
| |
| output_dir: PathBuf, |
| request_counter: u64, |
| |
| |
| order_breaklines: Vec<OrderBreakline>, |
| pending_order_type: Option<String>, |
| |
| |
| pending_history_request: Option<(u64, String, String, String)>, |
| } |
|
|
| impl Mt5ChartApp { |
| fn new( |
| tick_receiver: mpsc::Receiver<TickData>, |
| order_sender: mpsc::Sender<OrderRequest>, |
| response_receiver: mpsc::Receiver<OrderResponse>, |
| ) -> Self { |
| |
| let now = chrono::Local::now(); |
| let today_str = now.format("%Y.%m.%d").to_string(); |
| |
| |
| let output_dir = PathBuf::from("output"); |
| fs::create_dir_all(&output_dir).ok(); |
| |
| Self { |
| tick_receiver, |
| data: Vec::new(), |
| symbol: "Waiting for data...".to_string(), |
| balance: 0.0, |
| equity: 0.0, |
| margin: 0.0, |
| free_margin: 0.0, |
| min_lot: 0.01, |
| max_lot: 100.0, |
| lot_step: 0.01, |
| order_sender, |
| response_receiver, |
| lot_size: 0.01, |
| lot_size_str: "0.01".to_string(), |
| limit_price: "0.0".to_string(), |
| stop_price: "0.0".to_string(), |
| last_order_result: None, |
| |
| history_start_date: today_str.clone(), |
| history_end_date: today_str, |
| history_tf: "M1".to_string(), |
| history_mode: "OHLC".to_string(), |
| |
| is_recording: false, |
| live_record_file: None, |
| |
| positions: Vec::new(), |
| pending_orders: Vec::new(), |
| |
| |
| output_dir, |
| request_counter: 0, |
| order_breaklines: Vec::new(), |
| pending_order_type: None, |
| pending_history_request: None, |
| } |
| } |
| |
| fn send_order(&mut self, order_type: &str, price: Option<f64>, ticket: Option<u64>) { |
| let price_val = price.unwrap_or(0.0); |
| let ticket_val = ticket.unwrap_or(0); |
| |
| |
| if order_type.contains("market") { |
| self.pending_order_type = Some(order_type.to_string()); |
| } |
| |
| let request = OrderRequest { |
| order_type: order_type.to_string(), |
| symbol: self.symbol.clone(), |
| volume: self.lot_size, |
| price: price_val, |
| ticket: ticket_val, |
| timeframe: None, |
| start: None, |
| end: None, |
| mode: None, |
| request_id: None, |
| }; |
| |
| self.send_request_impl(request); |
| } |
| |
| fn send_download_request(&mut self) { |
| |
| self.request_counter += 1; |
| |
| |
| self.pending_history_request = Some(( |
| self.request_counter, |
| self.symbol.replace("/", "-"), |
| self.history_tf.clone(), |
| self.history_mode.clone(), |
| )); |
| |
| let request = OrderRequest { |
| order_type: "download_history".to_string(), |
| symbol: self.symbol.clone(), |
| volume: 0.0, |
| price: 0.0, |
| ticket: 0, |
| timeframe: Some(self.history_tf.clone()), |
| start: Some(self.history_start_date.clone()), |
| end: Some(self.history_end_date.clone()), |
| mode: Some(self.history_mode.clone()), |
| request_id: Some(self.request_counter), |
| }; |
| |
| self.send_request_impl(request); |
| } |
| |
| fn send_request_impl(&mut self, request: OrderRequest) { |
| if let Err(e) = self.order_sender.try_send(request) { |
| self.last_order_result = Some(format!("Failed to send: {}", e)); |
| } else { |
| self.last_order_result = Some("Request sent...".to_string()); |
| } |
| } |
| |
| fn adjust_lot_size(&mut self, delta: f64) { |
| let new_lot = self.lot_size + delta; |
| |
| let steps = (new_lot / self.lot_step).round(); |
| self.lot_size = (steps * self.lot_step).max(self.min_lot).min(self.max_lot); |
| self.lot_size_str = format!("{:.2}", self.lot_size); |
| } |
| |
| fn toggle_recording(&mut self) { |
| self.is_recording = !self.is_recording; |
| if self.is_recording { |
| |
| self.request_counter += 1; |
| let filename = format!( |
| "{}/Live_{}_ID{:04}_{}.csv", |
| self.output_dir.display(), |
| self.symbol.replace("/", "-"), |
| self.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"); |
| self.live_record_file = Some(file); |
| self.last_order_result = Some(format!("Recording to {}", filename)); |
| } |
| Err(e) => { |
| self.is_recording = false; |
| self.last_order_result = Some(format!("Rec Error: {}", e)); |
| } |
| } |
| } else { |
| self.live_record_file = None; |
| self.last_order_result = Some("Recording Stopped".to_string()); |
| } |
| } |
| } |
|
|
| impl eframe::App for Mt5ChartApp { |
| fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { |
| |
| while let Ok(tick) = self.tick_receiver.try_recv() { |
| self.symbol = tick.symbol.clone(); |
| |
| |
| if self.is_recording { |
| if let Some(mut file) = self.live_record_file.as_ref() { |
| let _ = writeln!(file, "{},{},{},{}", tick.time, tick.bid, tick.ask, tick.volume); |
| } |
| } |
| |
| |
| if tick.balance > 0.0 { |
| self.balance = tick.balance; |
| self.equity = tick.equity; |
| self.margin = tick.margin; |
| self.free_margin = tick.free_margin; |
| self.min_lot = tick.min_lot; |
| self.max_lot = tick.max_lot; |
| if tick.lot_step > 0.0 { |
| self.lot_step = tick.lot_step; |
| } |
| } |
| |
| |
| self.positions = tick.positions.clone(); |
| self.pending_orders = tick.orders.clone(); |
| |
| self.data.push(tick); |
| |
| if self.data.len() > 2000 { |
| self.data.remove(0); |
| } |
| } |
| |
| |
| while let Ok(response) = self.response_receiver.try_recv() { |
| if response.success { |
| |
| if let Some(ref msg) = response.message { |
| if msg.contains("||CSV_DATA||") { |
| |
| let parts: Vec<&str> = msg.splitn(2, "||CSV_DATA||").collect(); |
| if parts.len() == 2 { |
| let info_part = parts[0]; |
| let csv_content = parts[1]; |
| |
| |
| if let Some((id, symbol, tf, mode)) = self.pending_history_request.take() { |
| let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S"); |
| let filename = format!( |
| "{}/History_{}_{}_{}_ID{:04}_{}.csv", |
| self.output_dir.display(), |
| symbol, tf, mode, id, timestamp |
| ); |
| |
| |
| let csv_with_newlines = csv_content.replace("|NL|", "\n"); |
| |
| |
| match std::fs::write(&filename, csv_with_newlines) { |
| Ok(_) => { |
| self.last_order_result = Some(format!( |
| "✓ {} → Saved to {}", |
| info_part, filename |
| )); |
| } |
| Err(e) => { |
| self.last_order_result = Some(format!( |
| "✗ Failed to save CSV: {}", |
| e |
| )); |
| } |
| } |
| } else { |
| self.last_order_result = Some(format!("✓ {}", info_part)); |
| } |
| } else { |
| self.last_order_result = Some(format!("✓ {}", msg)); |
| } |
| } else { |
| self.last_order_result = Some(format!("✓ {}", msg)); |
| } |
| } else { |
| |
| if let Some(ref order_type) = self.pending_order_type.take() { |
| let breakline = OrderBreakline { |
| index: self.data.len().saturating_sub(1), |
| order_type: order_type.clone(), |
| ticket: response.ticket.unwrap_or(0), |
| }; |
| self.order_breaklines.push(breakline); |
| |
| if self.order_breaklines.len() > 50 { |
| self.order_breaklines.remove(0); |
| } |
| } |
| |
| self.last_order_result = Some(format!( |
| "✓ Order executed! Ticket: {}", |
| response.ticket.unwrap_or(0) |
| )); |
| } |
| } else { |
| self.pending_order_type = None; |
| self.pending_history_request = None; |
| self.last_order_result = Some(format!( |
| "✗ Failed: {}", |
| response.error.unwrap_or_else(|| "Unknown error".to_string()) |
| )); |
| } |
| } |
|
|
| |
| |
| |
| egui::SidePanel::left("trading_panel") |
| .min_width(280.0) |
| .show(ctx, |ui| { |
| 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}", self.balance)); |
| ui.end_row(); |
| ui.label("Equity:"); |
| ui.colored_label(egui::Color32::from_rgb(100, 180, 255), format!("${:.2}", self.equity)); |
| ui.end_row(); |
| ui.label("Margin Used:"); |
| ui.colored_label(egui::Color32::from_rgb(255, 200, 100), format!("${:.2}", self.margin)); |
| ui.end_row(); |
| ui.label("Free Margin:"); |
| ui.colored_label(egui::Color32::from_rgb(100, 255, 200), format!("${:.2}", self.free_margin)); |
| ui.end_row(); |
| }); |
| }); |
| |
| ui.separator(); |
| |
| |
| ui.heading("📂 Historical Data"); |
| ui.add_space(5.0); |
| |
| egui::Grid::new("history_grid").num_columns(2).spacing([10.0, 5.0]).show(ui, |ui| { |
| ui.label("Start (yyyy.mm.dd):"); |
| ui.add(egui::TextEdit::singleline(&mut self.history_start_date).desired_width(100.0)); |
| ui.end_row(); |
| |
| ui.label("End (yyyy.mm.dd):"); |
| ui.add(egui::TextEdit::singleline(&mut self.history_end_date).desired_width(100.0)); |
| ui.end_row(); |
| |
| ui.label("Timeframe:"); |
| egui::ComboBox::from_id_source("tf_combo") |
| .selected_text(&self.history_tf) |
| .show_ui(ui, |ui| { |
| ui.selectable_value(&mut self.history_tf, "M1".to_string(), "M1"); |
| ui.selectable_value(&mut self.history_tf, "M5".to_string(), "M5"); |
| ui.selectable_value(&mut self.history_tf, "M15".to_string(), "M15"); |
| ui.selectable_value(&mut self.history_tf, "H1".to_string(), "H1"); |
| ui.selectable_value(&mut self.history_tf, "D1".to_string(), "D1"); |
| }); |
| ui.end_row(); |
| |
| ui.label("Mode:"); |
| egui::ComboBox::from_id_source("mode_combo") |
| .selected_text(&self.history_mode) |
| .show_ui(ui, |ui| { |
| ui.selectable_value(&mut self.history_mode, "OHLC".to_string(), "OHLC"); |
| ui.selectable_value(&mut self.history_mode, "TICKS".to_string(), "TICKS"); |
| }); |
| ui.end_row(); |
| }); |
| |
| ui.add_space(5.0); |
| if ui.button("⬇ Download History (CSV)").clicked() { |
| self.send_download_request(); |
| } |
| |
| ui.separator(); |
| |
| |
| ui.heading("🔴 Live Recording"); |
| ui.horizontal(|ui| { |
| ui.label(if self.is_recording { "Recording..." } else { "Idle" }); |
| if ui.button(if self.is_recording { "Stop" } else { "Start Recording" }).clicked() { |
| self.toggle_recording(); |
| } |
| }); |
| |
| ui.separator(); |
|
|
| |
| ui.heading("📦 Trade Controls"); |
| |
| |
| ui.horizontal(|ui| { |
| if ui.button("−").clicked() { self.adjust_lot_size(-self.lot_step); } |
| let response = ui.add(egui::TextEdit::singleline(&mut self.lot_size_str).desired_width(60.0)); |
| if response.lost_focus() { |
| if let Ok(parsed) = self.lot_size_str.parse::<f64>() { |
| self.lot_size = parsed.max(self.min_lot).min(self.max_lot); |
| self.lot_size_str = format!("{:.2}", self.lot_size); |
| } |
| } |
| if ui.button("+").clicked() { self.adjust_lot_size(self.lot_step); } |
| |
| ui.label(format!("Lots (Max: {:.1})", self.max_lot)); |
| }); |
| |
| ui.add_space(5.0); |
| ui.label("Market Orders:"); |
| ui.horizontal(|ui| { |
| if ui.button("BUY").clicked() { self.send_order("market_buy", None, None); } |
| if ui.button("SELL").clicked() { self.send_order("market_sell", None, None); } |
| }); |
| |
| ui.add_space(5.0); |
| ui.label("Pending Orders:"); |
| ui.horizontal(|ui| { |
| ui.label("@ Price:"); |
| ui.add(egui::TextEdit::singleline(&mut self.limit_price).desired_width(70.0)); |
| }); |
| ui.horizontal(|ui| { |
| let p = self.limit_price.parse().unwrap_or(0.0); |
| if ui.small_button("Buy Limit").clicked() { self.send_order("limit_buy", Some(p), None); } |
| if ui.small_button("Sell Limit").clicked() { self.send_order("limit_sell", Some(p), None); } |
| if ui.small_button("Buy Stop").clicked() { self.send_order("stop_buy", Some(p), None); } |
| if ui.small_button("Sell Stop").clicked() { self.send_order("stop_sell", Some(p), None); } |
| }); |
|
|
| ui.separator(); |
|
|
| |
| if let Some(ref result) = self.last_order_result { |
| ui.heading("📨 Last Message"); |
| ui.label(result); |
| } |
| |
| ui.separator(); |
| |
| |
| ui.collapsing("💼 Active Positions", |ui| { |
| if self.positions.is_empty() { |
| ui.label("No active positions"); |
| } else { |
| let positions_clone = self.positions.clone(); |
| for pos in positions_clone { |
| 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() { |
| self.send_order("close_position", Some(pos.price), Some(pos.ticket)); |
| } |
| }); |
| } |
| } |
| }); |
| |
| |
| ui.collapsing("⏳ Pending Orders", |ui| { |
| if self.pending_orders.is_empty() { |
| ui.label("No pending orders"); |
| } else { |
| let orders_clone = self.pending_orders.clone(); |
| for order in orders_clone { |
| ui.horizontal(|ui| { |
| let color = if order.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}", |
| order.ticket, order.order_type, order.volume, order.price |
| )); |
| if ui.small_button("Cancel").clicked() { |
| self.send_order("cancel_order", Some(order.price), Some(order.ticket)); |
| } |
| }); |
| } |
| } |
| }); |
| }); |
|
|
| |
| |
| |
| egui::CentralPanel::default().show(ctx, |ui| { |
| ui.heading(format!("📈 {}", self.symbol)); |
| |
| |
| if let Some(last_tick) = self.data.last() { |
| ui.horizontal(|ui| { |
| ui.label(format!("{:.5} / {:.5}", last_tick.bid, last_tick.ask)); |
| }); |
| } |
| |
| ui.separator(); |
|
|
| |
| let time_map: Vec<i64> = self.data.iter().map(|t| t.time).collect(); |
| |
| let plot = Plot::new("mt5_price_plot") |
| .legend(egui_plot::Legend::default()) |
| .allow_boxed_zoom(true) |
| .allow_drag(true) |
| .allow_scroll(true) |
| .allow_zoom(true) |
| .x_axis_formatter(move |x, _range, _width| { |
| let idx = x.value.round() as isize; |
| if idx >= 0 && (idx as usize) < time_map.len() { |
| let timestamp = time_map[idx as usize]; |
| let seconds = timestamp % 60; |
| let minutes = (timestamp / 60) % 60; |
| let hours = (timestamp / 3600) % 24; |
| return format!("{:02}:{:02}:{:02}", hours, minutes, seconds); |
| } |
| "".to_string() |
| }); |
|
|
| plot.show(ui, |plot_ui| { |
| let bid_points: PlotPoints = self.data |
| .iter() |
| .enumerate() |
| .map(|(i, t)| [i as f64, t.bid]) |
| .collect(); |
| |
| let ask_points: PlotPoints = self.data |
| .iter() |
| .enumerate() |
| .map(|(i, t)| [i as f64, t.ask]) |
| .collect(); |
|
|
| plot_ui.line(Line::new(bid_points).name("Bid").color(egui::Color32::from_rgb(100, 200, 100))); |
| plot_ui.line(Line::new(ask_points).name("Ask").color(egui::Color32::from_rgb(200, 100, 100))); |
| |
| |
| for pos in &self.positions { |
| let color = if pos.pos_type == "BUY" { |
| egui::Color32::from_rgb(50, 100, 255) |
| } else { |
| egui::Color32::from_rgb(255, 50, 50) |
| }; |
| |
| plot_ui.hline( |
| egui_plot::HLine::new(pos.price) |
| .color(color) |
| .name(format!("{} #{}", pos.pos_type, pos.ticket)) |
| .style(egui_plot::LineStyle::Dashed { length: 10.0 }) |
| ); |
| } |
| |
| |
| for breakline in &self.order_breaklines { |
| let color = if breakline.order_type.contains("buy") { |
| egui::Color32::from_rgb(0, 200, 100) |
| } else { |
| egui::Color32::from_rgb(255, 80, 80) |
| }; |
| |
| plot_ui.vline( |
| egui_plot::VLine::new(breakline.index as f64) |
| .color(color) |
| .name(format!("Order #{}", breakline.ticket)) |
| .width(2.0) |
| ); |
| } |
| }); |
| }); |
|
|
| |
| ctx.request_repaint(); |
| } |
| } |
|
|
| |
| |
| |
|
|
| #[tokio::main] |
| async fn main() -> Result<(), Box<dyn std::error::Error>> { |
| |
| let (tick_tx, tick_rx) = mpsc::channel(100); |
| |
| |
| let (order_tx, mut order_rx) = mpsc::channel::<OrderRequest>(10); |
| let (response_tx, response_rx) = mpsc::channel::<OrderResponse>(10); |
|
|
| |
| |
| |
| tokio::spawn(async move { |
| let mut socket = zeromq::SubSocket::new(); |
| match socket.connect("tcp://127.0.0.1:5555").await { |
| Ok(_) => println!("Connected to ZMQ Tick Publisher on port 5555"), |
| Err(e) => eprintln!("Failed to connect to ZMQ tick publisher: {}", e), |
| } |
| |
| let _ = socket.subscribe("").await; |
|
|
| loop { |
| match socket.recv().await { |
| Ok(msg) => { |
| if let Some(payload_bytes) = msg.get(0) { |
| if let Ok(json_str) = std::str::from_utf8(payload_bytes) { |
| match serde_json::from_str::<TickData>(json_str) { |
| Ok(tick) => { |
| if let Err(e) = tick_tx.send(tick).await { |
| eprintln!("Tick channel error: {}", e); |
| break; |
| } |
| } |
| Err(e) => eprintln!("JSON Parse Error: {}. Msg: {}", e, json_str), |
| } |
| } |
| } |
| } |
| Err(e) => { |
| eprintln!("ZMQ Tick Recv Error: {}", e); |
| tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await; |
| } |
| } |
| } |
| }); |
|
|
| |
| |
| |
| tokio::spawn(async move { |
| let mut socket = zeromq::ReqSocket::new(); |
| match socket.connect("tcp://127.0.0.1:5556").await { |
| Ok(_) => println!("Connected to ZMQ Order Handler on port 5556"), |
| Err(e) => { |
| eprintln!("Failed to connect to ZMQ order handler: {}", e); |
| return; |
| } |
| } |
|
|
| while let Some(order_request) = order_rx.recv().await { |
| |
| let json_request = match serde_json::to_string(&order_request) { |
| Ok(json) => json, |
| Err(e) => { |
| eprintln!("Failed to serialize order request: {}", e); |
| continue; |
| } |
| }; |
| |
| println!("Sending request: {}", json_request); |
| |
| |
| if let Err(e) = socket.send(json_request.into()).await { |
| eprintln!("Failed to send: {}", e); |
| let _ = response_tx.send(OrderResponse { |
| success: false, |
| ticket: None, |
| error: Some(format!("Send failed: {}", e)), |
| message: None, |
| }).await; |
| continue; |
| } |
| |
| |
| match socket.recv().await { |
| Ok(msg) => { |
| if let Some(payload_bytes) = msg.get(0) { |
| if let Ok(json_str) = std::str::from_utf8(payload_bytes) { |
| println!("Received response: {}", json_str); |
| match serde_json::from_str::<OrderResponse>(json_str) { |
| Ok(response) => { |
| let _ = response_tx.send(response).await; |
| } |
| Err(e) => { |
| let _ = response_tx.send(OrderResponse { |
| success: false, |
| ticket: None, |
| error: Some(format!("Parse error: {}", e)), |
| message: None, |
| }).await; |
| } |
| } |
| } |
| } |
| } |
| Err(e) => { |
| eprintln!("Response recv error: {}", e); |
| let _ = response_tx.send(OrderResponse { |
| success: false, |
| ticket: None, |
| error: Some(format!("Recv failed: {}", e)), |
| message: None, |
| }).await; |
| } |
| } |
| } |
| }); |
|
|
| |
| |
| |
| let options = eframe::NativeOptions { |
| viewport: egui::ViewportBuilder::default() |
| .with_inner_size([1200.0, 800.0]) |
| .with_title("Rust + ZMQ + MT5 Trading Chart"), |
| ..Default::default() |
| }; |
| |
| eframe::run_native( |
| "Rust + ZMQ + MT5 Trading Chart", |
| options, |
| Box::new(|_cc| Box::new(Mt5ChartApp::new(tick_rx, order_tx, response_rx))), |
| ).map_err(|e| e.into()) |
| } |
|
|