use crate::client::IndodaxClient;
use crate::commands::helpers;
use crate::output::CommandOutput;
use anyhow::Result;
use serde_json::Value;
#[derive(Debug, clap::Subcommand)]
pub enum MarketCommand {
#[command(name = "server-time", about = "Get server time")]
ServerTime,
#[command(name = "pairs", about = "List available trading pairs")]
Pairs,
#[command(name = "ticker", about = "Get ticker for a pair")]
Ticker {
#[arg(default_value = "btc_idr")]
pair: String,
},
#[command(name = "ticker-all", about = "Get tickers for all pairs")]
TickerAll,
#[command(name = "summaries", about = "Get 24h and 7d summaries for all pairs")]
Summaries,
#[command(name = "orderbook", about = "Get order book for a pair")]
Orderbook {
#[arg(default_value = "btc_idr")]
pair: String,
},
#[command(name = "trades", about = "Get recent trades for a pair")]
Trades {
#[arg(default_value = "btc_idr")]
pair: String,
},
#[command(name = "ohlc", about = "Get OHLCV candle data (default --from is 24h ago)")]
Ohlc {
#[arg(short, long, default_value = "btc_idr")]
symbol: String,
#[arg(long, default_value = "60")]
timeframe: String,
#[arg(short, long, help = "Start timestamp in seconds (default: 24h ago)")]
from: Option<u64>,
#[arg(long, help = "End timestamp in seconds (default: now)")]
to: Option<u64>,
},
#[command(name = "price-increments", about = "Get price increments (tick sizes)")]
PriceIncrements,
}
pub async fn execute(
client: &IndodaxClient,
cmd: &MarketCommand,
) -> Result<CommandOutput> {
match cmd {
MarketCommand::ServerTime => server_time(client).await,
MarketCommand::Pairs => pairs(client).await,
MarketCommand::Ticker { pair: p } => {
let pair = helpers::normalize_pair(p);
ticker(client, &pair).await
}
MarketCommand::TickerAll => ticker_all(client).await,
MarketCommand::Summaries => summaries(client).await,
MarketCommand::Orderbook { pair: p } => {
let pair = helpers::normalize_pair(p);
orderbook(client, &pair).await
}
MarketCommand::Trades { pair: p } => {
let pair = helpers::normalize_pair(p);
trades(client, &pair).await
}
MarketCommand::Ohlc { symbol, timeframe, from, to } => {
let symbol = helpers::normalize_pair(symbol).replace('_', "").to_uppercase();
ohlc(client, &symbol, timeframe, *from, *to).await
}
MarketCommand::PriceIncrements => price_increments(client).await,
}
}
async fn server_time(client: &IndodaxClient) -> Result<CommandOutput> {
let data: Value = client.public_get("/api/server_time").await?;
let (headers, rows) = helpers::flatten_json_to_table(&data);
Ok(CommandOutput::new(data, headers, rows))
}
async fn pairs(client: &IndodaxClient) -> Result<CommandOutput> {
let data: Value = client.public_get("/api/pairs").await?;
let pairs_info = helpers::extract_pairs(&data);
let headers = vec!["Pair ID".into(), "Info".into()];
let rows: Vec<Vec<String>> = pairs_info
.into_iter()
.map(|(id, info)| vec![id, info])
.collect();
Ok(CommandOutput::new(data, headers, rows))
}
async fn ticker(client: &IndodaxClient, pair: &str) -> Result<CommandOutput> {
let data: Value = client.public_get(&format!("/api/ticker/{}", pair)).await?;
let ticker = &data["ticker"];
if ticker.is_object() {
let (headers, rows) = helpers::flatten_json_to_table(ticker);
Ok(CommandOutput::new(data, headers, rows))
} else {
let (headers, rows) = helpers::flatten_json_to_table(&data);
Ok(CommandOutput::new(data, headers, rows))
}
}
async fn ticker_all(client: &IndodaxClient) -> Result<CommandOutput> {
let data: Value = client.public_get("/api/ticker_all").await?;
let tickers = &data["tickers"];
if tickers.is_object() {
let headers = vec![
"Pair".into(), "Last".into(), "High".into(), "Low".into(),
"Buy".into(), "Sell".into(), "Vol (base)".into(), "Vol (quote)".into(),
];
let mut rows: Vec<Vec<String>> = Vec::new();
if let Value::Object(map) = tickers {
for (key, val) in map {
rows.push(vec![
key.clone(),
helpers::value_to_string(&val["last"]),
helpers::value_to_string(&val["high"]),
helpers::value_to_string(&val["low"]),
helpers::value_to_string(&val["buy"]),
helpers::value_to_string(&val["sell"]),
helpers::value_to_string(helpers::first_of(val, &["vol_btc", "vol_base"])),
helpers::value_to_string(helpers::first_of(val, &["vol_idr", "vol_traded"])),
]);
}
}
rows.sort_by(|a, b| a[0].cmp(&b[0]));
Ok(CommandOutput::new(data, headers, rows))
} else {
let (headers, rows) = helpers::flatten_json_to_table(&data);
Ok(CommandOutput::new(data, headers, rows))
}
}
async fn summaries(client: &IndodaxClient) -> Result<CommandOutput> {
let data: Value = client.public_get("/api/summaries").await?;
let summaries = &data["summaries"];
if summaries.is_object() {
let headers = vec![
"Pair".into(), "Last".into(), "High".into(), "Low".into(),
"Vol (base)".into(), "Vol (quote)".into(),
];
let mut rows: Vec<Vec<String>> = Vec::new();
if let Value::Object(map) = summaries {
for (key, val) in map {
rows.push(vec![
key.clone(),
helpers::value_to_string(&val["last"]),
helpers::value_to_string(&val["high"]),
helpers::value_to_string(&val["low"]),
helpers::value_to_string(helpers::first_of(val, &["vol_btc", "vol_base"])),
helpers::value_to_string(helpers::first_of(val, &["vol_idr", "vol_traded"])),
]);
}
}
rows.sort_by(|a, b| a[0].cmp(&b[0]));
Ok(CommandOutput::new(data, headers, rows))
} else {
let (headers, rows) = helpers::flatten_json_to_table(&data);
Ok(CommandOutput::new(data, headers, rows))
}
}
async fn orderbook(client: &IndodaxClient, pair: &str) -> Result<CommandOutput> {
let data: Value = client.public_get(&format!("/api/depth/{}", pair)).await?;
let headers = vec!["Side".into(), "Price".into(), "Amount".into()];
let mut rows: Vec<Vec<String>> = Vec::new();
let buys = &data["buy"];
let sells = &data["sell"];
if let Value::Array(arr) = buys {
for entry in arr.iter().take(20) {
if let Some(row_arr) = entry.as_array().filter(|a| a.len() >= 2) {
rows.push(vec!["BUY".into(), helpers::value_to_string(&row_arr[0]), helpers::value_to_string(&row_arr[1])]);
}
}
}
if let Value::Array(arr) = sells {
for entry in arr.iter().rev().take(20) {
if let Some(row_arr) = entry.as_array().filter(|a| a.len() >= 2) {
rows.push(vec!["SELL".into(), helpers::value_to_string(&row_arr[0]), helpers::value_to_string(&row_arr[1])]);
}
}
}
let level_count = rows.len() / 2;
Ok(CommandOutput::new(data, headers, rows)
.with_addendum(format!("Showing {} bid/ask levels", level_count)))
}
async fn trades(client: &IndodaxClient, pair: &str) -> Result<CommandOutput> {
let data: Value = client.public_get(&format!("/api/trades/{}", pair)).await?;
let headers = vec!["TID".into(), "Date".into(), "Price".into(), "Amount".into(), "Type".into()];
let mut rows: Vec<Vec<String>> = Vec::new();
if let Value::Array(arr) = &data {
for trade in arr.iter().take(50) {
let ts = trade["date"].as_str()
.and_then(|s| s.parse::<u64>().ok())
.or_else(|| trade["date"].as_u64())
.unwrap_or(0);
rows.push(vec![
helpers::value_to_string(&trade["tid"]),
helpers::format_timestamp(ts, false),
helpers::value_to_string(&trade["price"]),
helpers::value_to_string(&trade["amount"]),
helpers::value_to_string(&trade["type"]),
]);
}
}
Ok(CommandOutput::new(data, headers, rows))
}
async fn ohlc(
client: &IndodaxClient,
symbol: &str,
timeframe: &str,
from: Option<u64>,
to: Option<u64>,
) -> Result<CommandOutput> {
let now_secs = crate::auth::Signer::now_millis() / 1000;
let from_val = from.map(|v| v.to_string()).unwrap_or_else(|| {
(now_secs - crate::commands::helpers::ONE_DAY_SECS).to_string()
});
let to_val = to.map(|v| v.to_string()).unwrap_or_else(|| {
now_secs.to_string()
});
let data: Value = client.public_get_v2(
"/tradingview/history_v2",
&[
("symbol", symbol),
("tf", timeframe),
("from", &from_val),
("to", &to_val),
],
).await?;
let headers = vec![
"Time".into(), "Open".into(), "High".into(), "Low".into(),
"Close".into(), "Volume".into(),
];
let mut rows: Vec<Vec<String>> = Vec::new();
if let Value::Object(ref map) = data {
let times = map.get("t").and_then(|v| v.as_array());
let opens = map.get("o").and_then(|v| v.as_array());
let highs = map.get("h").and_then(|v| v.as_array());
let lows = map.get("l").and_then(|v| v.as_array());
let closes = map.get("c").and_then(|v| v.as_array());
let volumes = map.get("v").and_then(|v| v.as_array());
if let (Some(t), Some(o), Some(h), Some(l), Some(c), Some(vol)) =
(times, opens, highs, lows, closes, volumes)
{
let len = t.len().min(o.len()).min(h.len()).min(l.len()).min(c.len()).min(vol.len());
for i in 0..len {
rows.push(vec![
helpers::format_timestamp(t[i].as_u64().unwrap_or(0), false),
helpers::value_to_string(&o[i]),
helpers::value_to_string(&h[i]),
helpers::value_to_string(&l[i]),
helpers::value_to_string(&c[i]),
helpers::value_to_string(&vol[i]),
]);
}
}
}
Ok(CommandOutput::new(data, headers, rows))
}
async fn price_increments(client: &IndodaxClient) -> Result<CommandOutput> {
let data: Value = client.public_get("/api/price_increments").await?;
if data.is_object() {
let headers = vec!["Pair".into(), "Increment".into()];
let mut rows: Vec<Vec<String>> = Vec::new();
if let Value::Object(map) = &data["increments"] {
for (key, val) in map {
rows.push(vec![key.clone(), helpers::value_to_string(val)]);
}
}
rows.sort_by(|a, b| a[0].cmp(&b[0]));
Ok(CommandOutput::new(data, headers, rows))
} else {
let (headers, rows) = helpers::flatten_json_to_table(&data);
Ok(CommandOutput::new(data, headers, rows))
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_market_command_variants() {
let _cmd1 = MarketCommand::ServerTime;
let _cmd2 = MarketCommand::Pairs;
let _cmd3 = MarketCommand::Ticker { pair: "btc_idr".into() };
let _cmd4 = MarketCommand::TickerAll;
let _cmd5 = MarketCommand::Summaries;
let _cmd6 = MarketCommand::Orderbook { pair: "btcidr".into() };
let _cmd7 = MarketCommand::Trades { pair: "btcidr".into() };
let _cmd8 = MarketCommand::Ohlc {
symbol: "BTCIDR".into(),
timeframe: "60".into(),
from: None,
to: None
};
let _cmd9 = MarketCommand::PriceIncrements;
}
#[test]
fn test_first_of_with_json_null() {
let val = json!(null);
let result = helpers::first_of(&val, &["key"]);
assert_eq!(result, &serde_json::Value::Null);
}
#[test]
fn test_first_of_empty_keys() {
let val = json!({"a": 1});
let result = helpers::first_of(&val, &[]);
assert_eq!(result, &serde_json::Value::Null);
}
}