rust-okx 0.6.4

Async Rust client for the OKX v5 REST API
Documentation
use ratatui::prelude::*;
use ratatui::widgets::{Block, Borders, Cell, Paragraph, Row, Table, Wrap};

use crate::app::{App, LogLevel};
use crate::ui::status_style;

pub fn render(f: &mut Frame, area: Rect, app: &App) {
    let rows = Layout::vertical([
        Constraint::Length(8),
        Constraint::Fill(1),
        Constraint::Length(8),
    ])
    .split(area);
    let top = Layout::horizontal([
        Constraint::Percentage(34),
        Constraint::Percentage(33),
        Constraint::Percentage(33),
    ])
    .split(rows[0]);

    render_market_card(f, top[0], app);
    render_account_card(f, top[1], app);
    render_connection_card(f, top[2], app);
    render_orders_and_positions(f, rows[1], app);
    render_recent_logs(f, rows[2], app);
}

fn render_market_card(f: &mut Frame, area: Rect, app: &App) {
    let last = app
        .last_price()
        .map(|v| format!("{v:.4}"))
        .unwrap_or_else(|| "-".to_owned());
    let change = app
        .daily_change_pct()
        .map(|v| format!("{v:+.2}%"))
        .unwrap_or_else(|| "-".to_owned());
    let (bid, ask) = app.best_bid_ask();
    let lines = vec![
        Line::from(format!("  {} {}", app.config.inst_id, app.config.bar)),
        Line::from(vec![
            Span::raw("  Last: "),
            Span::styled(last, Style::default().fg(Color::Yellow).bold()),
            Span::raw("   24h: "),
            Span::styled(change, Style::default().fg(Color::Cyan)),
        ]),
        Line::from(format!(
            "  Bid: {}   Ask: {}",
            bid.map(|v| v.price.as_str()).unwrap_or("-"),
            ask.map(|v| v.price.as_str()).unwrap_or("-")
        )),
        Line::from(format!(
            "  Candles: {}   Trades: {}",
            app.candles.len(),
            app.trades.len()
        )),
    ];
    f.render_widget(
        Paragraph::new(lines)
            .block(Block::default().borders(Borders::ALL).title(" 行情摘要 "))
            .wrap(Wrap { trim: false }),
        area,
    );
}

fn render_account_card(f: &mut Frame, area: Rect, app: &App) {
    let total_eq: f64 = app
        .balance_details()
        .filter_map(|row| row.eq.as_str().parse::<f64>().ok())
        .sum();
    let lines = vec![
        Line::from(vec![
            Span::raw("  Mode: "),
            Span::styled(
                app.config.mode_label(),
                Style::default().fg(if app.config.demo {
                    Color::Yellow
                } else {
                    Color::Red
                }),
            ),
        ]),
        Line::from(format!("  Total eq*: {total_eq:.4}")),
        Line::from(format!("  Balances: {}", app.balance_details().count())),
        Line::from(format!("  Positions: {}", app.positions.len())),
        Line::styled(
            "  *按返回币种直接求和,仅作快速视图",
            Style::default().fg(Color::DarkGray),
        ),
    ];
    f.render_widget(
        Paragraph::new(lines).block(Block::default().borders(Borders::ALL).title(" 账户摘要 ")),
        area,
    );
}

fn render_connection_card(f: &mut Frame, area: Rect, app: &App) {
    let statuses = [
        ("REST", &app.statuses.rest),
        ("Market", &app.statuses.market),
        ("Candles", &app.statuses.candles),
        ("Private", &app.statuses.private),
    ];
    let rows = statuses.into_iter().map(|(name, status)| {
        Row::new(vec![
            Cell::from(name),
            Cell::from(format!("{:?}", status.state)).style(status_style(&status.state)),
            Cell::from(status.detail.as_str()),
        ])
    });
    let table = Table::new(
        rows,
        [
            Constraint::Length(8),
            Constraint::Length(14),
            Constraint::Fill(1),
        ],
    )
    .block(Block::default().borders(Borders::ALL).title(" 连接状态 "));
    f.render_widget(table, area);
}

fn render_orders_and_positions(f: &mut Frame, area: Rect, app: &App) {
    let chunks =
        Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]).split(area);
    let orders: Vec<Row> = app
        .orders
        .iter()
        .take(8)
        .map(|o| {
            Row::new(vec![
                Cell::from(o.inst_id.as_str()),
                Cell::from(o.side.as_str()),
                Cell::from(o.ord_type.as_str()),
                Cell::from(o.px.as_str()),
                Cell::from(o.sz.as_str()),
                Cell::from(o.state.as_str()),
            ])
        })
        .collect();
    let orders = Table::new(
        orders,
        [
            Constraint::Percentage(26),
            Constraint::Length(6),
            Constraint::Length(8),
            Constraint::Percentage(20),
            Constraint::Percentage(20),
            Constraint::Length(12),
        ],
    )
    .header(Row::new(["合约", "方向", "类型", "价格", "数量", "状态"]).fg(Color::Cyan))
    .block(Block::default().borders(Borders::ALL).title(" 挂单 "));
    f.render_widget(orders, chunks[0]);

    let positions: Vec<Row> = app
        .positions
        .iter()
        .take(8)
        .map(|p| {
            Row::new(vec![
                Cell::from(p.inst_id.as_str()),
                Cell::from(p.pos_side.as_str()),
                Cell::from(p.pos.as_str()),
                Cell::from(p.upl.as_str()),
            ])
        })
        .collect();
    let positions = Table::new(
        positions,
        [
            Constraint::Percentage(34),
            Constraint::Length(8),
            Constraint::Percentage(28),
            Constraint::Percentage(30),
        ],
    )
    .header(Row::new(["合约", "方向", "数量", "未实现"]).fg(Color::Cyan))
    .block(Block::default().borders(Borders::ALL).title(" 持仓 "));
    f.render_widget(positions, chunks[1]);
}

fn render_recent_logs(f: &mut Frame, area: Rect, app: &App) {
    let lines: Vec<Line> = app
        .logs
        .iter()
        .take(5)
        .map(|entry| {
            let style = match entry.level {
                LogLevel::Info => Style::default().fg(Color::Gray),
                LogLevel::Warn => Style::default().fg(Color::Yellow),
                LogLevel::Error => Style::default().fg(Color::Red),
            };
            Line::styled(format!("  {:?}: {}", entry.level, entry.message), style)
        })
        .collect();
    f.render_widget(
        Paragraph::new(lines).block(Block::default().borders(Borders::ALL).title(" 最近事件 ")),
        area,
    );
}