algorembrant's picture
Upload 37 files
e15ab27 verified
// ============================================================================
// app.rs -- Application State & eframe::App Implementation
// ============================================================================
// Central state struct shared across all UI modules.
// Tab switching pauses inactive tab computation.
// ============================================================================
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;
// ─── Tab Enum ────────────────────────────────────────────────────────────────
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum TabMode {
TimeBased,
TradeBased,
}
// ─── Application State ──────────────────────────────────────────────────────
pub struct AppState {
// ZMQ channels
pub tick_rx: mpsc::Receiver<TickData>,
pub order_tx: mpsc::Sender<OrderRequest>,
pub response_rx: mpsc::Receiver<OrderResponse>,
// Raw tick buffer (rolling)
pub raw_ticks: Vec<RawTick>,
pub symbol: String,
// Account
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,
// Positions & orders (from latest tick)
pub positions: Vec<PositionData>,
pub pending_orders: Vec<PendingOrderData>,
// Trading UI
pub lot_size: f64,
pub lot_size_str: String,
pub limit_price_str: String,
pub last_status: Option<String>,
// Chart config
pub bin_size: f64,
pub bin_size_str: String,
pub active_tab: TabMode,
// Time-based tab
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,
// Trade-based tab
pub trade_interval: usize,
pub trade_input_str: String,
pub trade_profiles: Vec<IntervalProfile>,
pub profiles_dirty_trade: bool,
pub show_footprint_trade: bool,
// History download
pub history_start: String,
pub history_end: String,
pub history_tf: String,
pub history_mode: String,
// Live recording
pub is_recording: bool,
pub live_record_file: Option<std::fs::File>,
// Output
pub output_dir: PathBuf,
pub request_counter: u64,
pub pending_history: Option<(u64, String, String, String)>,
// Max ticks to keep in memory
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,
}
}
}
// ─── eframe::App ─────────────────────────────────────────────────────────────
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;
// ── Drain tick channel ──
let mut new_ticks = false;
while let Ok(tick) = state.tick_rx.try_recv() {
state.symbol = tick.symbol.clone();
// Record if active
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
);
}
}
// Update account info
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;
}
}
// Update positions/orders
state.positions = tick.positions.clone();
state.pending_orders = tick.orders.clone();
// Use mid-price (avg of bid/ask) for GMP computation
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;
}
// Trim buffer
if state.raw_ticks.len() > state.max_ticks {
let excess = state.raw_ticks.len() - state.max_ticks;
state.raw_ticks.drain(..excess);
}
// Mark profiles dirty if new ticks arrived
if new_ticks {
match state.active_tab {
TabMode::TimeBased => state.profiles_dirty = true,
TabMode::TradeBased => state.profiles_dirty_trade = true,
}
}
// ── Drain order responses ──
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())
));
}
}
// ── Side Panel ──
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);
});
});
// ── Central Panel ──
egui::CentralPanel::default().show(ctx, |ui| {
// Header
ui.horizontal(|ui| {
ui.heading(&state.symbol);
if let Some(last) = state.raw_ticks.last() {
let price = last.price;
ui.label(format!("{:.5}", price));
}
});
// Tab bar
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();
// Active tab content -- only the selected tab computes
match state.active_tab {
TabMode::TimeBased => tab_time::render(ui, state),
TabMode::TradeBased => tab_trade::render(ui, state),
}
});
// Continuous repaint
ctx.request_repaint();
}
}
// ─── CSV Response Handler ────────────────────────────────────────────────────
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));
}
}