| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | use crate::gmp_engine::{IntervalProfile, RawTick};
|
| | use crate::zmq_bridge::{OrderRequest, OrderResponse, PositionData, PendingOrderData, TickData};
|
| | use crate::tab_time;
|
| | use crate::tab_trade;
|
| | use crate::trading_panel;
|
| | use std::io::Write;
|
| | use std::path::PathBuf;
|
| | use tokio::sync::mpsc;
|
| |
|
| |
|
| |
|
| | #[derive(Clone, Copy, PartialEq, Eq)]
|
| | pub enum TabMode {
|
| | TimeBased,
|
| | TradeBased,
|
| | }
|
| |
|
| |
|
| |
|
| | pub struct AppState {
|
| |
|
| | pub tick_rx: mpsc::Receiver<TickData>,
|
| | pub order_tx: mpsc::Sender<OrderRequest>,
|
| | pub response_rx: mpsc::Receiver<OrderResponse>,
|
| |
|
| |
|
| | pub raw_ticks: Vec<RawTick>,
|
| | pub symbol: String,
|
| |
|
| |
|
| | pub balance: f64,
|
| | pub equity: f64,
|
| | pub margin: f64,
|
| | pub free_margin: f64,
|
| | pub min_lot: f64,
|
| | pub max_lot: f64,
|
| | pub lot_step: f64,
|
| |
|
| |
|
| | pub positions: Vec<PositionData>,
|
| | pub pending_orders: Vec<PendingOrderData>,
|
| |
|
| |
|
| | pub lot_size: f64,
|
| | pub lot_size_str: String,
|
| | pub limit_price_str: String,
|
| | pub last_status: Option<String>,
|
| |
|
| |
|
| | pub bin_size: f64,
|
| | pub bin_size_str: String,
|
| | pub active_tab: TabMode,
|
| |
|
| |
|
| | pub time_interval_secs: u64,
|
| | pub time_input_str: String,
|
| | pub time_parse_err: Option<String>,
|
| | pub time_profiles: Vec<IntervalProfile>,
|
| | pub profiles_dirty: bool,
|
| | pub show_footprint: bool,
|
| |
|
| |
|
| | pub trade_interval: usize,
|
| | pub trade_input_str: String,
|
| | pub trade_profiles: Vec<IntervalProfile>,
|
| | pub profiles_dirty_trade: bool,
|
| | pub show_footprint_trade: bool,
|
| |
|
| |
|
| | pub history_start: String,
|
| | pub history_end: String,
|
| | pub history_tf: String,
|
| | pub history_mode: String,
|
| |
|
| |
|
| | pub is_recording: bool,
|
| | pub live_record_file: Option<std::fs::File>,
|
| |
|
| |
|
| | pub output_dir: PathBuf,
|
| | pub request_counter: u64,
|
| | pub pending_history: Option<(u64, String, String, String)>,
|
| |
|
| |
|
| | max_ticks: usize,
|
| | }
|
| |
|
| | impl AppState {
|
| | pub fn new(
|
| | tick_rx: mpsc::Receiver<TickData>,
|
| | order_tx: mpsc::Sender<OrderRequest>,
|
| | response_rx: mpsc::Receiver<OrderResponse>,
|
| | ) -> Self {
|
| | let now = chrono::Local::now();
|
| | let today = now.format("%Y.%m.%d").to_string();
|
| | let output_dir = PathBuf::from("output");
|
| | std::fs::create_dir_all(&output_dir).ok();
|
| |
|
| | Self {
|
| | tick_rx,
|
| | order_tx,
|
| | response_rx,
|
| | raw_ticks: Vec::new(),
|
| | symbol: "Waiting...".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,
|
| | positions: Vec::new(),
|
| | pending_orders: Vec::new(),
|
| | lot_size: 0.01,
|
| | lot_size_str: "0.01".to_string(),
|
| | limit_price_str: "0.0".to_string(),
|
| | last_status: None,
|
| | bin_size: 1.0,
|
| | bin_size_str: "1".to_string(),
|
| | active_tab: TabMode::TimeBased,
|
| | time_interval_secs: 60,
|
| | time_input_str: "1m".to_string(),
|
| | time_parse_err: None,
|
| | time_profiles: Vec::new(),
|
| | profiles_dirty: true,
|
| | show_footprint: false,
|
| | trade_interval: 50,
|
| | trade_input_str: "50".to_string(),
|
| | trade_profiles: Vec::new(),
|
| | profiles_dirty_trade: true,
|
| | show_footprint_trade: false,
|
| | history_start: today.clone(),
|
| | history_end: today,
|
| | history_tf: "M1".to_string(),
|
| | history_mode: "TICKS".to_string(),
|
| | is_recording: false,
|
| | live_record_file: None,
|
| | output_dir,
|
| | request_counter: 0,
|
| | pending_history: None,
|
| | max_ticks: 50_000,
|
| | }
|
| | }
|
| | }
|
| |
|
| |
|
| |
|
| | pub struct GmpTerminalApp {
|
| | pub state: AppState,
|
| | }
|
| |
|
| | impl GmpTerminalApp {
|
| | pub fn new(
|
| | tick_rx: mpsc::Receiver<TickData>,
|
| | order_tx: mpsc::Sender<OrderRequest>,
|
| | response_rx: mpsc::Receiver<OrderResponse>,
|
| | ) -> Self {
|
| | Self {
|
| | state: AppState::new(tick_rx, order_tx, response_rx),
|
| | }
|
| | }
|
| | }
|
| |
|
| | impl eframe::App for GmpTerminalApp {
|
| | fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
| | let state = &mut self.state;
|
| |
|
| |
|
| | let mut new_ticks = false;
|
| | while let Ok(tick) = state.tick_rx.try_recv() {
|
| | state.symbol = tick.symbol.clone();
|
| |
|
| |
|
| | if state.is_recording {
|
| | if let Some(ref mut file) = state.live_record_file {
|
| | let _ = writeln!(
|
| | file, "{},{},{},{}",
|
| | tick.time, tick.bid, tick.ask, tick.volume
|
| | );
|
| | }
|
| | }
|
| |
|
| |
|
| | if tick.balance > 0.0 {
|
| | state.balance = tick.balance;
|
| | state.equity = tick.equity;
|
| | state.margin = tick.margin;
|
| | state.free_margin = tick.free_margin;
|
| | state.min_lot = tick.min_lot;
|
| | state.max_lot = tick.max_lot;
|
| | if tick.lot_step > 0.0 {
|
| | state.lot_step = tick.lot_step;
|
| | }
|
| | }
|
| |
|
| |
|
| | state.positions = tick.positions.clone();
|
| | state.pending_orders = tick.orders.clone();
|
| |
|
| |
|
| | let mid = (tick.bid + tick.ask) * 0.5;
|
| | let idx = state.raw_ticks.len();
|
| | state.raw_ticks.push(RawTick {
|
| | price: mid,
|
| | time: tick.time,
|
| | index: idx,
|
| | });
|
| |
|
| | new_ticks = true;
|
| | }
|
| |
|
| |
|
| | if state.raw_ticks.len() > state.max_ticks {
|
| | let excess = state.raw_ticks.len() - state.max_ticks;
|
| | state.raw_ticks.drain(..excess);
|
| | }
|
| |
|
| |
|
| | if new_ticks {
|
| | match state.active_tab {
|
| | TabMode::TimeBased => state.profiles_dirty = true,
|
| | TabMode::TradeBased => state.profiles_dirty_trade = true,
|
| | }
|
| | }
|
| |
|
| |
|
| | while let Ok(resp) = state.response_rx.try_recv() {
|
| | if resp.success {
|
| | if let Some(ref msg) = resp.message {
|
| | if msg.contains("||CSV_DATA||") {
|
| | handle_csv_response(state, msg);
|
| | } else {
|
| | state.last_status = Some(format!("OK: {}", msg));
|
| | }
|
| | } else {
|
| | state.last_status = Some(format!(
|
| | "OK: ticket {}",
|
| | resp.ticket.unwrap_or(0)
|
| | ));
|
| | }
|
| | } else {
|
| | state.pending_history = None;
|
| | state.last_status = Some(format!(
|
| | "FAIL: {}",
|
| | resp.error.unwrap_or_else(|| "Unknown".to_string())
|
| | ));
|
| | }
|
| | }
|
| |
|
| |
|
| | egui::SidePanel::left("trading_panel")
|
| | .min_width(260.0)
|
| | .max_width(300.0)
|
| | .show(ctx, |ui| {
|
| | egui::ScrollArea::vertical().show(ui, |ui| {
|
| | trading_panel::render_trading_panel(ui, state);
|
| | });
|
| | });
|
| |
|
| |
|
| | egui::CentralPanel::default().show(ctx, |ui| {
|
| |
|
| | ui.horizontal(|ui| {
|
| | ui.heading(&state.symbol);
|
| | if let Some(last) = state.raw_ticks.last() {
|
| | let price = last.price;
|
| | ui.label(format!("{:.5}", price));
|
| | }
|
| | });
|
| |
|
| |
|
| | ui.horizontal(|ui| {
|
| | if ui
|
| | .selectable_label(state.active_tab == TabMode::TimeBased, "Time-Based")
|
| | .clicked()
|
| | {
|
| | state.active_tab = TabMode::TimeBased;
|
| | state.profiles_dirty = true;
|
| | }
|
| | if ui
|
| | .selectable_label(state.active_tab == TabMode::TradeBased, "Trade-Based")
|
| | .clicked()
|
| | {
|
| | state.active_tab = TabMode::TradeBased;
|
| | state.profiles_dirty_trade = true;
|
| | }
|
| | });
|
| |
|
| | ui.separator();
|
| |
|
| |
|
| | match state.active_tab {
|
| | TabMode::TimeBased => tab_time::render(ui, state),
|
| | TabMode::TradeBased => tab_trade::render(ui, state),
|
| | }
|
| | });
|
| |
|
| |
|
| | ctx.request_repaint();
|
| | }
|
| | }
|
| |
|
| |
|
| |
|
| | fn handle_csv_response(state: &mut AppState, msg: &str) {
|
| | let parts: Vec<&str> = msg.splitn(2, "||CSV_DATA||").collect();
|
| | if parts.len() == 2 {
|
| | let info = parts[0];
|
| | let csv = parts[1];
|
| |
|
| | if let Some((id, sym, tf, mode)) = state.pending_history.take() {
|
| | let ts = chrono::Local::now().format("%Y%m%d_%H%M%S");
|
| | let filename = format!(
|
| | "{}/History_{}_{}_{}_ID{:04}_{}.csv",
|
| | state.output_dir.display(),
|
| | sym, tf, mode, id, ts
|
| | );
|
| | let csv_clean = csv.replace("|NL|", "\n");
|
| | match std::fs::write(&filename, csv_clean) {
|
| | Ok(_) => {
|
| | state.last_status = Some(format!("Saved: {} ({})", filename, info));
|
| | }
|
| | Err(e) => {
|
| | state.last_status = Some(format!("Save error: {}", e));
|
| | }
|
| | }
|
| | } else {
|
| | state.last_status = Some(format!("OK: {}", info));
|
| | }
|
| | } else {
|
| | state.last_status = Some(format!("OK: {}", msg));
|
| | }
|
| | }
|
| |
|