use std::time::Instant;
use tokio::sync::mpsc::UnboundedSender;
use tokio::sync::oneshot;
use super::super::config::SplineConfig;
use super::super::i18n::strings;
use super::super::math::ui_size_to_num_base_lots;
use super::super::state::{SliceOutcome, TuiState, TwapBot, TxStatusMsg};
use super::super::trading::TradingSide;
use super::super::tx::submit_market_order;
pub(in crate::tui::runtime) fn tick_twap_scheduler(
state: &mut TuiState,
configs: &std::collections::HashMap<String, SplineConfig>,
active_cfg: &SplineConfig,
tx_status: &UnboundedSender<TxStatusMsg>,
) -> bool {
let mut dispatched_any = false;
let now = Instant::now();
let live_authority: Option<solana_pubkey::Pubkey> = state.trading.keypair.as_ref().map(|kp| {
use solana_signer::Signer;
kp.pubkey()
});
let bot_count = state.twaps_view.bots.len();
for i in 0..bot_count {
let mut just_completed: Option<String> = None;
if let Some(bot) = state.twaps_view.bots.get_mut(i) {
if let Some((slice_number, outcome)) = bot.try_take_outcome() {
let completed = record_resolved_slice_outcome(bot, now, slice_number, outcome);
dispatched_any = true;
if completed {
just_completed = Some(format_completion_status(bot));
}
}
}
if let Some(line) = just_completed {
state.trading.set_status_title(line);
}
let Some(bot) = state.twaps_view.bots.get(i) else {
continue;
};
if !bot.slice_due(now) {
continue;
}
let symbol = bot.symbol.clone();
let side = bot.side;
let slice_size = bot.slice_size;
let bot_authority = bot.authority;
let next_slice_number =
bot.slices_submitted + bot.slices_failed + bot.slices_unconfirmed + 1;
let Some(live_authority) = live_authority else {
defer_with_reason(state, i, now, strings().twap_waiting_wallet);
continue;
};
if live_authority != bot_authority {
defer_with_reason(state, i, now, strings().twap_waiting_authority);
continue;
}
let Some(kp) = state.trading.keypair.clone() else {
defer_with_reason(state, i, now, strings().twap_waiting_wallet);
continue;
};
let Some(ctx) = state.trading.tx_context.clone() else {
defer_with_reason(state, i, now, strings().twap_waiting_trader_sync);
continue;
};
let market_cfg = match configs.get(&symbol) {
Some(cfg) => cfg.clone(),
None => {
let reason = format!("{} ({})", strings().twap_waiting_market_cfg, symbol);
defer_with_reason_owned(state, i, now, reason);
continue;
}
};
if !market_cfg.isolated_only && symbol != active_cfg.symbol {
let reason = format!(
"{} ({} \u{2192} {})",
strings().twap_waiting_active_market,
active_cfg.symbol,
symbol
);
defer_with_reason_owned(state, i, now, reason);
continue;
}
if market_cfg.isolated_only && ctx.snapshot_trader().is_none() {
defer_with_reason(state, i, now, strings().twap_waiting_trader_sync);
continue;
}
let num_base_lots = match ui_size_to_num_base_lots(slice_size, market_cfg.base_lot_decimals)
{
Ok(n) if n > 0 => n,
_ => {
if let Some(bot) = state.twaps_view.bots.get_mut(i) {
bot.stop();
bot.last_status = strings().twap_err_size_too_small.to_string();
}
continue;
}
};
let reference_price_usd = match market_price_for_symbol(state, &symbol, &active_cfg.symbol)
{
Some(price) => price,
None if market_cfg.isolated_only => {
let reason = format!("{} ({})", strings().waiting_data, symbol);
defer_with_reason_owned(state, i, now, reason);
continue;
}
None => 0.0,
};
let (otx, orx) = oneshot::channel();
let task = submit_market_order(
kp.clone(),
ctx.clone(),
symbol.clone(),
side,
num_base_lots,
false,
slice_size,
0,
market_cfg.isolated_only,
market_cfg.max_leverage,
reference_price_usd,
tx_status.clone(),
Some(otx),
true, );
let s = strings();
let side_lbl = match side {
TradingSide::Long => s.long_label,
TradingSide::Short => s.short_label,
};
let status_line = format!(
"{}: {} {} {} ({} {}/{})",
s.twap_slice_sent,
side_lbl,
slice_size,
symbol,
s.twap_slice_word,
next_slice_number,
bot.slice_count,
);
if let Some(bot) = state.twaps_view.bots.get_mut(i) {
bot.record_slice_dispatched(now, next_slice_number, orx, task);
bot.last_status = status_line;
bot.clear_defer_reason();
}
dispatched_any = true;
}
dispatched_any
}
pub(in crate::tui::runtime) fn settle_in_flight_for_interrupt(
bot: &mut TwapBot,
now: Instant,
unknown_detail: impl Into<String>,
) -> Option<String> {
if let Some((slice_number, outcome)) = bot.try_take_outcome() {
let completed = record_resolved_slice_outcome(bot, now, slice_number, outcome);
if completed {
return Some(format_completion_status(bot));
}
return None;
}
if let Some(in_flight) = bot.in_flight.take() {
let slice_number = in_flight.slice_number;
in_flight.task.abort();
let completed = record_resolved_slice_outcome(
bot,
now,
slice_number,
SliceOutcome::Unknown(unknown_detail.into()),
);
if completed {
return Some(format_completion_status(bot));
}
}
None
}
#[must_use]
fn record_resolved_slice_outcome(
bot: &mut TwapBot,
now: Instant,
slice_number: u32,
outcome: SliceOutcome,
) -> bool {
let s = strings();
match outcome {
SliceOutcome::Confirmed => {
let line = format!(
"{} {}/{}",
s.twap_slice_confirmed, slice_number, bot.slice_count
);
bot.record_slice_confirmed(now, line)
}
SliceOutcome::Failed(detail) => {
let short = truncate_status(&detail, 120);
let line = format!(
"{} {}/{}: {}",
s.twap_slice_failed, slice_number, bot.slice_count, short
);
bot.record_slice_failed(now, line)
}
SliceOutcome::Unknown(detail) => {
let short = truncate_status(&detail, 120);
let line = format!(
"{} {}/{}: {}",
s.twap_slice_unconfirmed, slice_number, bot.slice_count, short
);
bot.record_slice_unconfirmed(now, line)
}
}
}
pub(crate) fn format_completion_status(bot: &TwapBot) -> String {
let s = strings();
let side_lbl = match bot.side {
TradingSide::Long => s.long_label,
TradingSide::Short => s.short_label,
};
let imperfect = bot.slices_failed > 0 || bot.slices_unconfirmed > 0;
if imperfect {
format!(
"{}: {} {} {} \u{2014} {}\u{2713}/{}\u{2717}/{}? of {}",
s.twap_completed,
side_lbl,
bot.total_size,
bot.symbol,
bot.slices_submitted,
bot.slices_failed,
bot.slices_unconfirmed,
bot.slice_count,
)
} else {
format!(
"{}: {} {} {} \u{2014} {}/{} {}",
s.twap_completed,
side_lbl,
bot.total_size,
bot.symbol,
bot.slices_submitted,
bot.slice_count,
s.twap_slice_confirmed,
)
}
}
fn defer_with_reason(state: &mut TuiState, i: usize, now: Instant, reason: &str) {
if let Some(bot) = state.twaps_view.bots.get_mut(i) {
bot.set_defer_reason(reason);
bot.touch_last_slice_at(now);
}
}
fn defer_with_reason_owned(state: &mut TuiState, i: usize, now: Instant, reason: String) {
if let Some(bot) = state.twaps_view.bots.get_mut(i) {
bot.set_defer_reason(reason);
bot.touch_last_slice_at(now);
}
}
fn truncate_status(s: &str, max_chars: usize) -> String {
if s.chars().count() <= max_chars {
s.to_string()
} else {
let mut out: String = s.chars().take(max_chars.saturating_sub(1)).collect();
out.push('\u{2026}');
out
}
}
fn market_price_for_symbol(state: &TuiState, symbol: &str, active_symbol: &str) -> Option<f64> {
let market_mark = state
.market_selector
.markets
.iter()
.find(|m| m.symbol == symbol)
.map(|m| m.price)
.filter(|price| price.is_finite() && *price > 0.0);
if market_mark.is_some() {
return market_mark;
}
if symbol == active_symbol {
state
.price_history
.back()
.copied()
.filter(|price| price.is_finite() && *price > 0.0)
} else {
None
}
}