SUM3-Trading-Terminal / gmp-terminal /src /trading_panel.rs
algorembrant's picture
Upload 37 files
e15ab27 verified
// ============================================================================
// trading_panel.rs -- Trading Controls Side Panel
// ============================================================================
// Shared left-panel UI for both Time-Based and Trade-Based tabs.
// Contains account info, order controls, position management, history
// download, live recording, and bin-size configuration.
// ============================================================================
use crate::app::AppState;
use crate::zmq_bridge::OrderRequest;
use egui;
use std::fs::OpenOptions;
use std::io::Write;
// ─── Trading Panel Rendering ─────────────────────────────────────────────────
pub fn render_trading_panel(ui: &mut egui::Ui, state: &mut AppState) {
ui.heading("Trading Panel");
ui.separator();
// ── Account Info ──
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();
// ── Chart Settings ──
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();
// ── History Download ──
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();
// ── Live Recording ──
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();
// ── Trade Controls ──
ui.heading("Trade Controls");
// Lot size
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();
// ── Status ──
if let Some(ref msg) = state.last_status {
ui.heading("Status");
ui.label(msg);
}
ui.separator();
// ── Active Positions ──
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));
}
});
}
}
});
// ── Pending Orders ──
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));
}
});
}
}
});
}
// ─── Helpers ─────────────────────────────────────────────────────────────────
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());
}
}