indodax-cli 0.1.4

A command-line interface for the Indodax cryptocurrency exchange
Documentation
use rmcp::model::{CallToolResult, Tool};
use crate::errors::IndodaxError;

use super::IndodaxMcp;

pub fn paper_tools() -> Vec<Tool> {
    vec![
        IndodaxMcp::tool_def(
            "paper_init",
            "Initialize paper trading with default or custom virtual balances",
            serde_json::json!({
                "idr": IndodaxMcp::num_param("Initial IDR balance (default: 100000000)", false),
                "btc": IndodaxMcp::num_param("Initial BTC balance (default: 1.0)", false),
            }),
            vec![],
        ),
        IndodaxMcp::tool_def(
            "paper_reset",
            "Reset paper trading state to defaults",
            serde_json::json!({}),
            vec![],
        ),
        IndodaxMcp::tool_def(
            "paper_balance",
            "Show current paper trading virtual balances",
            serde_json::json!({}),
            vec![],
        ),
        IndodaxMcp::tool_def(
            "paper_buy",
            "Place a simulated paper buy order (omit price for market order)",
            serde_json::json!({
                "pair":
                    IndodaxMcp::str_param("Trading pair, e.g. btc_idr", false, Some("btc_idr")),
                "price": IndodaxMcp::num_param("Price for the order (omit for market order)", false),
                "amount": IndodaxMcp::num_param("Amount in base currency (alternative to idr)", false),
                "idr": IndodaxMcp::num_param("IDR amount to spend (alternative to amount)", false),
            }),
            vec![],
        ),
        IndodaxMcp::tool_def(
            "paper_sell",
            "Place a simulated paper sell order (omit price for market order)",
            serde_json::json!({
                "pair":
                    IndodaxMcp::str_param("Trading pair, e.g. btc_idr", false, Some("btc_idr")),
                "price": IndodaxMcp::num_param("Price for the order (omit for market order)", false),
                "amount": IndodaxMcp::num_param("Amount in base currency", true),
            }),
            vec!["amount"],
        ),
        IndodaxMcp::tool_def(
            "paper_orders",
            "List paper trading orders",
            serde_json::json!({}),
            vec![],
        ),
        IndodaxMcp::tool_def(
            "paper_cancel",
            "Cancel a paper trading order",
            serde_json::json!({
                "order_id": IndodaxMcp::num_param("Order ID to cancel", true),
            }),
            vec!["order_id"],
        ),
        IndodaxMcp::tool_def(
            "paper_cancel_all",
            "Cancel all paper trading orders",
            serde_json::json!({}),
            vec![],
        ),
        IndodaxMcp::tool_def(
            "paper_history",
            "Show paper trading order history",
            serde_json::json!({}),
            vec![],
        ),
        IndodaxMcp::tool_def(
            "paper_status",
            "Show paper trading status summary (trades, balances, P&L)",
            serde_json::json!({}),
            vec![],
        ),
        IndodaxMcp::tool_def(
            "paper_fill",
            "Fill an open paper order (provide order_id or set all=true)",
            serde_json::json!({
                "order_id":
                    IndodaxMcp::num_param("Order ID to fill (optional if all=true)", false),
                "price": IndodaxMcp::num_param("Fill price (defaults to order price)", false),
                "all": IndodaxMcp::bool_param("Fill all open orders"),
            }),
            vec![],
        ),
        IndodaxMcp::tool_def(
            "paper_check_fills",
            "Auto-fill open paper orders based on current market prices",
            serde_json::json!({
                "prices":
                    IndodaxMcp::str_param("JSON object of market prices, e.g. {\"btc_idr\": 100000000}", false, None),
                "fetch": IndodaxMcp::bool_param("Auto-fetch current market prices from Indodax API"),
            }),
            vec![],
        ),
    ]
}

impl IndodaxMcp {
    async fn save_paper_state(&self, state: &crate::commands::paper::PaperState) -> Result<(), IndodaxError> {
        let json = serde_json::to_value(state).map_err(|e| IndodaxError::Other(e.to_string()))?;
        let config_clone = {
            let mut config = self.config.lock().await;
            config.paper_balances = Some(json);
            config.clone()
        };
        config_clone.save().map_err(|e| IndodaxError::Other(e.to_string()))
    }

    pub async fn handle_paper_init(&self, idr: Option<f64>, btc: Option<f64>) -> CallToolResult {
        let state = crate::commands::paper::init_paper_state(idr, btc);
        use crate::commands::paper::{DEFAULT_BALANCE_BTC, DEFAULT_BALANCE_IDR};
        let idr_str = crate::commands::paper::format_balance("idr", state.balances.get("idr").copied().unwrap_or(DEFAULT_BALANCE_IDR));
        let btc_str = crate::commands::paper::format_balance("btc", state.balances.get("btc").copied().unwrap_or(DEFAULT_BALANCE_BTC));
        let msg = format!(
            "[PAPER] Paper trading initialized with {} IDR and {} BTC",
            idr_str, btc_str,
        );
        match self.save_paper_state(&state).await {
            Ok(()) => Self::ok_result(msg),
            Err(e) => Self::error_from_indodax(&e),
        }
    }

    pub async fn handle_paper_reset(&self) -> CallToolResult {
        let state = crate::commands::paper::PaperState::default();
        match self.save_paper_state(&state).await {
            Ok(()) => Self::ok_result("[PAPER] Paper trading state reset".to_string()),
            Err(e) => Self::error_from_indodax(&e),
        }
    }

    pub async fn handle_paper_balance(&self) -> CallToolResult {
        let config = self.config.lock().await;
        let state = crate::commands::paper::PaperState::load(&config);
        Self::json_result(crate::commands::paper::paper_balance_value(&state))
    }

    pub async fn handle_paper_trade(
        &self,
        side: &str,
        pair: &str,
        price: Option<f64>,
        amount: Option<f64>,
        idr: Option<f64>,
    ) -> CallToolResult {
        let mut state = {
            let config = self.config.lock().await;
            crate::commands::paper::PaperState::load(&config)
        };
        let result = if side == "buy" {
            if let Some(idr_val) = idr {
                crate::commands::paper::place_paper_order_idr(&mut state, pair, side, idr_val, price)
            } else if let Some(amt) = amount {
                crate::commands::paper::place_paper_order(&mut state, pair, side, price, amt)
            } else {
                return Self::validation_error_result("Either amount or idr must be specified for buy".into());
            }
        } else {
            let amt = match amount {
                Some(v) if v > 0.0 => v,
                Some(v) => return Self::validation_error_result(format!("Amount must be positive, got {}", v)),
                None => return Self::validation_error_result("Missing required parameter: amount".into()),
            };
            crate::commands::paper::place_paper_order(&mut state, pair, side, price, amt)
        };
        match result {
            Ok(_output) => {
                if let Err(e) = self.save_paper_state(&state).await {
                    return Self::error_from_indodax(&e);
                }
                let effective_amount = amount.unwrap_or(0.0);
                Self::json_result(serde_json::json!({
                    "mode": "paper",
                    "side": side,
                    "pair": pair,
                    "price": price,
                    "amount": effective_amount,
                    "status": "open",
                }))
            }
            Err(e) => Self::error_from_indodax(&e),
        }
    }

    pub async fn handle_paper_orders(&self) -> CallToolResult {
        let config = self.config.lock().await;
        let state = crate::commands::paper::PaperState::load(&config);
        Self::json_result(crate::commands::paper::paper_orders_value(&state))
    }

    pub async fn handle_paper_cancel(&self, order_id: u64) -> CallToolResult {
        let mut state = {
            let config = self.config.lock().await;
            crate::commands::paper::PaperState::load(&config)
        };
        match crate::commands::paper::cancel_paper_order(&mut state, order_id) {
            Ok(()) => {
                if let Err(e) = self.save_paper_state(&state).await {
                    return Self::error_from_indodax(&e);
                }
                Self::ok_result(format!("[PAPER] Order {} cancelled", order_id))
            }
            Err(e) => Self::error_from_indodax(&e),
        }
    }

    pub async fn handle_paper_cancel_all(&self) -> CallToolResult {
        let mut state = {
            let config = self.config.lock().await;
            crate::commands::paper::PaperState::load(&config)
        };
        let (count, failures) = crate::commands::paper::cancel_all_paper_orders(&mut state);
        if let Err(e) = self.save_paper_state(&state).await {
            return Self::error_from_indodax(&e);
        }
        let msg = if failures.is_empty() {
            format!("[PAPER] Cancelled {} orders", count)
        } else {
            let reasons: Vec<String> = failures.iter().map(|(id, e)| format!("{}: {}", id, e)).collect();
            format!("[PAPER] Cancelled {} orders, {} failed: {}", count, failures.len(), reasons.join("; "))
        };
        Self::ok_result(msg)
    }

    pub async fn handle_paper_history(&self) -> CallToolResult {
        let config = self.config.lock().await;
        let state = crate::commands::paper::PaperState::load(&config);
        Self::json_result(crate::commands::paper::paper_history_value(&state))
    }

    pub async fn handle_paper_status(&self) -> CallToolResult {
        let config = self.config.lock().await;
        let state = crate::commands::paper::PaperState::load(&config);
        Self::json_result(crate::commands::paper::paper_status_value(&state))
    }

    pub async fn handle_paper_fill(
        &self,
        order_id: Option<f64>,
        fill_price: Option<f64>,
        fill_all: bool,
    ) -> CallToolResult {
        let order_id = match order_id {
            Some(v) if v.fract() != 0.0 || v <= 0.0 => {
                return Self::validation_error_result(format!("order_id must be a positive whole number, got {}", v));
            }
            Some(v) => Some(v as u64),
            None => None,
        };
        let mut state = {
            let config = self.config.lock().await;
            crate::commands::paper::PaperState::load(&config)
        };
        match crate::commands::paper::paper_fill(&mut state, order_id, fill_price, fill_all) {
            Ok(output) => {
                if let Err(e) = self.save_paper_state(&state).await {
                    return Self::error_from_indodax(&e);
                }
                Self::json_result(output.data)
            }
            Err(e) => Self::error_from_indodax(&e),
        }
    }

    pub async fn handle_paper_check_fills(
        &self,
        prices: Option<&str>,
        fetch: bool,
    ) -> CallToolResult {
        let mut state = {
            let config = self.config.lock().await;
            crate::commands::paper::PaperState::load(&config)
        };
        match crate::commands::paper::paper_check_fills(
            &self.client,
            &mut state,
            prices,
            fetch,
        )
        .await
        {
            Ok(output) => {
                if let Err(e) = self.save_paper_state(&state).await {
                    return Self::error_from_indodax(&e);
                }
                Self::json_result(output.data)
            }
            Err(e) => Self::error_from_indodax(&e),
        }
    }
}