1use crate::auth::Signer;
2use crate::client::IndodaxClient;
3use crate::commands::helpers;
4use crate::config::IndodaxConfig;
5use crate::output::CommandOutput;
6use anyhow::Result;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10#[derive(Debug, clap::Subcommand)]
11pub enum AccountCommand {
12 #[command(name = "info", about = "Get account information and balances")]
13 Info,
14
15 #[command(name = "balance", about = "Show account balances")]
16 Balance,
17
18 #[command(name = "open-orders", about = "List open orders")]
19 OpenOrders {
20 #[arg(short, long, help = "Filter by trading pair")]
21 pair: Option<String>,
22 },
23
24 #[command(name = "order-history", about = "Get order history (v2 API)")]
25 OrderHistory {
26 #[arg(short, long, default_value = "btc_idr")]
27 symbol: String,
28 #[arg(short, long, default_value = "100")]
29 limit: u32,
30 },
31
32 #[command(name = "trade-history", about = "Get trade fill history (v2 API)")]
33 TradeHistory {
34 #[arg(short, long, default_value = "btc_idr")]
35 symbol: String,
36 #[arg(short, long, default_value = "100")]
37 limit: u32,
38 },
39
40 #[command(name = "trans-history", about = "Get deposit and withdrawal history")]
41 TransHistory,
42
43 #[command(name = "get-order", about = "Get order details by order ID")]
44 GetOrder {
45 #[arg(long)]
46 order_id: u64,
47 #[arg(long)]
48 pair: String,
49 },
50
51 #[command(name = "equity-snap", about = "Record a portfolio equity snapshot")]
52 EquitySnap,
53
54 #[command(name = "equity-history", about = "View equity snapshot history")]
55 EquityHistory {
56 #[arg(short, long, default_value = "20", help = "Number of snapshots to show")]
57 limit: usize,
58 #[arg(long, help = "Show all snapshots")]
59 all: bool,
60 },
61}
62
63pub async fn execute(
64 client: &IndodaxClient,
65 cmd: &AccountCommand,
66) -> Result<CommandOutput> {
67 match cmd {
68 AccountCommand::Info => info(client).await,
69 AccountCommand::Balance => balance(client).await,
70 AccountCommand::OpenOrders { pair } => {
71 let pair = pair.as_ref().map(|p| helpers::normalize_pair(p));
72 open_orders(client, pair.as_deref()).await
73 }
74 AccountCommand::OrderHistory { symbol, limit } => {
75 let symbol = helpers::normalize_pair(symbol);
76 order_history(client, &symbol, *limit).await
77 }
78 AccountCommand::TradeHistory { symbol, limit } => {
79 let symbol = helpers::normalize_pair(symbol);
80 trade_history(client, &symbol, *limit).await
81 }
82 AccountCommand::TransHistory => trans_history(client).await,
83 AccountCommand::GetOrder { order_id, pair } => {
84 let pair = helpers::normalize_pair(pair);
85 get_order(client, *order_id, &pair).await
86 }
87 AccountCommand::EquitySnap => equity_snap(client).await,
88 AccountCommand::EquityHistory { limit, all } => {
89 equity_history(*limit, *all)
90 }
91 }
92}
93
94async fn info(client: &IndodaxClient) -> Result<CommandOutput> {
95 let data: serde_json::Value =
96 client.private_post_v1("getInfo", &HashMap::new()).await?;
97
98 let headers = vec![
99 "Field".into(), "Value".into(),
100 ];
101 let mut rows: Vec<Vec<String>> = vec![
102 vec!["Name".into(), helpers::value_to_string(data.get("name").unwrap_or(&serde_json::Value::Null))],
103 vec!["User ID".into(), helpers::value_to_string(data.get("user_id").unwrap_or(&serde_json::Value::Null))],
104 vec!["Server Time".into(), helpers::format_timestamp(data["server_time"].as_u64().unwrap_or(0), true)],
105 vec!["Vip Level".into(), helpers::value_to_string(data.get("vip_level").unwrap_or(&serde_json::Value::Null))],
106 vec!["Verified".into(), helpers::value_to_string(data.get("verified_user").unwrap_or(&serde_json::Value::Null))],
107 ];
108
109 let balance = &data["balance"];
110 if let serde_json::Value::Object(bal_map) = balance {
111 let mut entries: Vec<(&String, &serde_json::Value)> = bal_map.iter().collect();
112 entries.sort_by(|a, b| a.0.cmp(b.0));
113 for (k, v) in entries {
114 rows.push(vec![k.clone(), helpers::value_to_string(v)]);
115 }
116 }
117
118 Ok(CommandOutput::new(data, headers, rows))
119}
120
121async fn balance(client: &IndodaxClient) -> Result<CommandOutput> {
122 let data: serde_json::Value =
123 client.private_post_v1("getInfo", &HashMap::new()).await?;
124
125 let balance = &data["balance"];
126 let headers = vec!["Currency".into(), "Balance".into()];
127 let mut rows: Vec<Vec<String>> = Vec::new();
128
129 if let serde_json::Value::Object(bal_map) = balance {
130 let mut entries: Vec<(String, f64)> = bal_map
131 .iter()
132 .map(|(k, v)| {
133 let val = v.as_str().and_then(|s| s.parse::<f64>().ok())
134 .or_else(|| v.as_f64())
135 .unwrap_or(0.0);
136 (k.clone(), val)
137 })
138 .collect();
139 entries.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
140 for (currency, amount) in entries {
141 rows.push(vec![currency, amount.to_string()]);
142 }
143 }
144
145 Ok(CommandOutput::new(data, headers, rows))
146}
147
148async fn open_orders(
149 client: &IndodaxClient,
150 pair: Option<&str>,
151) -> Result<CommandOutput> {
152 let mut params = HashMap::new();
153 if let Some(p) = pair {
154 params.insert("pair".into(), p.to_string());
155 }
156 let data: serde_json::Value =
157 client.private_post_v1("openOrders", ¶ms).await?;
158
159 let orders = &data["orders"];
160 let headers = vec![
161 "Order ID".into(), "Pair".into(), "Type".into(), "Side".into(),
162 "Price".into(), "Amount".into(), "Remaining".into(), "Time".into(),
163 ];
164 let mut rows: Vec<Vec<String>> = Vec::new();
165
166 if let serde_json::Value::Object(orders_map) = orders {
167 for (order_id, order_val) in orders_map {
168 let pair = helpers::value_to_string(
169 priv_get(order_val, &["pair", "market", "symbol"]),
170 );
171 let order_type = helpers::value_to_string(
172 priv_get(order_val, &["type", "order_type"]),
173 );
174 let side = if order_type.to_lowercase().contains("sell") {
175 "SELL"
176 } else {
177 "BUY"
178 };
179
180 let remaining = helpers::value_to_string(
181 priv_get(order_val, &["remaining", "remain_volume", "remaining_volume"]),
182 );
183 let base_amount = order_val.get("order_btc")
184 .or_else(|| order_val.get("order_base"))
185 .or_else(|| order_val.get("amount"))
186 .map(helpers::value_to_string)
187 .unwrap_or_default();
188
189 let time_val = order_val.get("submit_time")
190 .or_else(|| order_val.get("created_at"))
191 .or_else(|| order_val.get("time"))
192 .map(|v| {
193 let ts = v.as_u64().unwrap_or(0);
194 if ts > 1_000_000_000_000 {
195 helpers::format_timestamp(ts, true)
196 } else {
197 helpers::format_timestamp(ts, false)
198 }
199 })
200 .unwrap_or_default();
201
202 rows.push(vec![
203 order_id.to_string(),
204 pair,
205 order_type,
206 side.into(),
207 helpers::value_to_string(
208 priv_get(order_val, &["price", "order_price"]),
209 ),
210 base_amount,
211 remaining,
212 time_val,
213 ]);
214 }
215 }
216
217 rows.sort_by(|a, b| {
218 match (b[0].parse::<u64>().ok(), a[0].parse::<u64>().ok()) {
219 (Some(bv), Some(av)) => bv.cmp(&av),
220 _ => b[0].cmp(&a[0]),
221 }
222 });
223 let count = rows.len();
224 Ok(CommandOutput::new(data, headers, rows)
225 .with_addendum(format!("{} open orders", count)))
226}
227
228async fn order_history(
229 client: &IndodaxClient,
230 symbol: &str,
231 limit: u32,
232) -> Result<CommandOutput> {
233 let now = Signer::now_millis();
234 let start = now - crate::commands::helpers::ONE_DAY_MS;
235
236 let mut params = HashMap::new();
237 params.insert("symbol".into(), crate::commands::helpers::normalize_pair_v2(symbol));
238 params.insert("limit".into(), limit.max(10).to_string());
239 params.insert("startTime".into(), start.to_string());
240 params.insert("endTime".into(), now.to_string());
241
242 let data: serde_json::Value =
243 client.private_get_v2("/api/v2/order/histories", ¶ms).await?;
244
245 let headers = vec![
246 "Order ID".into(), "Symbol".into(), "Side".into(), "Type".into(),
247 "Price".into(), "Qty".into(), "Status".into(), "Time".into(),
248 ];
249 let mut rows: Vec<Vec<String>> = Vec::new();
250
251 if let serde_json::Value::Array(arr) = &data {
252 for order in arr.iter().take(limit as usize) {
253 rows.push(vec![
254 helpers::value_to_string(priv_get(order, &["orderId", "order_id"])),
255 helpers::value_to_string(priv_get(order, &["symbol", "pair"])),
256 helpers::value_to_string(priv_get(order, &["side", "order_side"])),
257 helpers::value_to_string(priv_get(order, &["type", "order_type"])),
258 helpers::value_to_string(priv_get(order, &["price", "order_price"])),
259 helpers::value_to_string(priv_get(order, &["origQty", "orig_qty", "qty"])),
260 helpers::value_to_string(priv_get(order, &["status", "order_status"])),
261 helpers::value_to_string(priv_get(order, &["time", "created_at"])),
262 ]);
263 }
264 }
265
266 Ok(CommandOutput::new(data, headers, rows))
267}
268
269async fn trade_history(
270 client: &IndodaxClient,
271 symbol: &str,
272 limit: u32,
273) -> Result<CommandOutput> {
274 let now = Signer::now_millis();
275 let start = now - crate::commands::helpers::ONE_DAY_MS;
276
277 let mut params = HashMap::new();
278 params.insert("symbol".into(), crate::commands::helpers::normalize_pair_v2(symbol));
279 params.insert("limit".into(), limit.max(10).to_string());
280 params.insert("startTime".into(), start.to_string());
281 params.insert("endTime".into(), now.to_string());
282
283 let data: serde_json::Value =
284 client.private_get_v2("/api/v2/myTrades", ¶ms).await?;
285
286 let headers = vec![
287 "Trade ID".into(), "Order ID".into(), "Symbol".into(), "Side".into(),
288 "Price".into(), "Qty".into(), "Fee".into(), "Time".into(),
289 ];
290 let mut rows: Vec<Vec<String>> = Vec::new();
291
292 if let serde_json::Value::Array(arr) = &data {
293 for trade in arr.iter().take(limit as usize) {
294 rows.push(vec![
295 helpers::value_to_string(priv_get(trade, &["id", "tradeId", "trade_id"])),
296 helpers::value_to_string(priv_get(trade, &["orderId", "order_id"])),
297 helpers::value_to_string(priv_get(trade, &["symbol", "pair"])),
298 helpers::value_to_string(priv_get(trade, &["side"])),
299 helpers::value_to_string(priv_get(trade, &["price"])),
300 helpers::value_to_string(priv_get(trade, &["qty", "quantity"])),
301 helpers::value_to_string(priv_get(trade, &["commission", "fee"])),
302 helpers::value_to_string(priv_get(trade, &["time", "timestamp"])),
303 ]);
304 }
305 }
306
307 Ok(CommandOutput::new(data, headers, rows))
308}
309
310async fn trans_history(client: &IndodaxClient) -> Result<CommandOutput> {
311 let data: serde_json::Value =
312 client.private_post_v1("transHistory", &HashMap::new()).await?;
313
314 let headers = vec![
315 "ID".into(), "Type".into(), "Currency".into(), "Amount".into(),
316 "Fee".into(), "Status".into(), "Time".into(),
317 ];
318 let mut rows: Vec<Vec<String>> = Vec::new();
319
320 let mut merged = serde_json::Map::new();
321 for key in &["withdraw", "deposit", "transactions"] {
322 if let Some(obj) = data.get(*key).and_then(|v| v.as_object()) {
323 merged.extend(obj.clone());
324 }
325 }
326 let trans_list = if merged.is_empty() { None } else { Some(serde_json::Value::Object(merged)) };
327
328 if let Some(serde_json::Value::Object(map)) = trans_list {
329 for (id, entry) in map {
330 let tx_type = if id.contains("withdraw") || entry.get("withdraw_id").is_some() {
331 "WITHDRAW"
332 } else {
333 "DEPOSIT"
334 };
335 rows.push(vec![
336 id.clone(),
337 tx_type.into(),
338 helpers::value_to_string(
339 priv_get(&entry, &["currency", "asset", "coin"]),
340 ),
341 helpers::value_to_string(
342 priv_get(&entry, &["amount", "value"]),
343 ),
344 helpers::value_to_string(
345 priv_get(&entry, &["fee", "withdraw_fee"]),
346 ),
347 helpers::value_to_string(
348 priv_get(&entry, &["status", "state"]),
349 ),
350 helpers::value_to_string(
351 priv_get(&entry, &["submit_time", "timestamp", "time", "submitted"]),
352 ),
353 ]);
354 }
355 }
356
357 rows.sort_by(|a, b| b[0].cmp(&a[0]));
358 Ok(CommandOutput::new(data, headers, rows))
359}
360
361async fn get_order(
362 client: &IndodaxClient,
363 order_id: u64,
364 pair: &str,
365) -> Result<CommandOutput> {
366 let mut params = HashMap::new();
367 params.insert("order_id".into(), order_id.to_string());
368 params.insert("pair".into(), pair.to_string());
369
370 let data: serde_json::Value =
371 client.private_post_v1("getOrder", ¶ms).await?;
372
373 let (headers, rows) = helpers::flatten_json_to_table(&data);
374 Ok(CommandOutput::new(data, headers, rows))
375}
376
377fn priv_get<'a>(val: &'a serde_json::Value, keys: &[&str]) -> &'a serde_json::Value {
378 helpers::first_of(val, keys)
379}
380
381#[derive(Debug, Serialize, Deserialize)]
386struct EquitySnapshot {
387 timestamp: u64,
388 equity: f64,
389}
390
391#[derive(Debug, Serialize, Deserialize)]
392struct EquityHistoryData {
393 snapshots: Vec<EquitySnapshot>,
394}
395
396fn equity_history_path() -> std::path::PathBuf {
397 IndodaxConfig::config_dir().join("equity_history.json")
398}
399
400fn load_equity_history() -> EquityHistoryData {
401 let path = equity_history_path();
402 if path.exists() {
403 std::fs::read_to_string(&path)
404 .ok()
405 .and_then(|s| serde_json::from_str(&s).ok())
406 .unwrap_or(EquityHistoryData { snapshots: vec![] })
407 } else {
408 EquityHistoryData { snapshots: vec![] }
409 }
410}
411
412fn save_equity_history(data: &EquityHistoryData) -> Result<()> {
413 let dir = IndodaxConfig::config_dir();
414 std::fs::create_dir_all(&dir)?;
415 let content = serde_json::to_string_pretty(data)?;
416 std::fs::write(equity_history_path(), content)?;
417 Ok(())
418}
419
420async fn calculate_equity(client: &IndodaxClient) -> Result<f64> {
421 let info: serde_json::Value = client.private_post_v1("getInfo", &HashMap::new()).await?;
422
423 let mut balances: HashMap<String, f64> = HashMap::new();
424 if let Some(bal_map) = info["balance"].as_object() {
425 for (k, v) in bal_map {
426 let val = v
427 .as_str()
428 .and_then(|s| s.parse::<f64>().ok())
429 .or_else(|| v.as_f64())
430 .unwrap_or(0.0);
431 if val > 0.0 {
432 balances.insert(k.clone(), val);
433 }
434 }
435 }
436
437 let tickers: serde_json::Value = client.public_get("/api/ticker_all").await?;
438 let mut prices: HashMap<String, f64> = HashMap::new();
439 if let Some(t) = tickers["tickers"].as_object() {
440 for (k, v) in t {
441 let last = v["last"]
442 .as_str()
443 .and_then(|s| s.parse::<f64>().ok())
444 .or_else(|| v["last"].as_f64())
445 .unwrap_or(0.0);
446 prices.insert(k.clone(), last);
447 }
448 }
449
450 let mut total = 0.0;
451 let btc_idr = prices.get("btc_idr").copied().unwrap_or(0.0);
452 let usdt_idr = prices.get("usdt_idr").copied().unwrap_or(0.0);
453 let eth_idr = prices.get("eth_idr").copied().unwrap_or(0.0);
454
455 for (currency, amount) in &balances {
456 if currency == "idr" {
457 total += amount;
458 } else if currency == "btc" {
459 total += amount * btc_idr;
460 } else if currency == "usdt" {
461 total += amount * usdt_idr;
462 } else {
463 let pair_idr = format!("{}_{}", currency, "idr");
464 let pair_btc = format!("{}_{}", currency, "btc");
465 let pair_usdt = format!("{}_{}", currency, "usdt");
466 let pair_eth = format!("{}_{}", currency, "eth");
467
468 if let Some(price) = prices.get(&pair_idr) {
469 total += amount * price;
470 } else if let Some(price) = prices.get(&pair_btc) {
471 total += amount * price * btc_idr;
472 } else if let Some(price) = prices.get(&pair_usdt) {
473 total += amount * price * usdt_idr;
474 } else if let Some(price) = prices.get(&pair_eth) {
475 total += amount * price * eth_idr;
476 }
477 }
478 }
479
480 Ok(total)
481}
482
483async fn equity_snap(client: &IndodaxClient) -> Result<CommandOutput> {
484 let equity = calculate_equity(client).await?;
485 let timestamp = Signer::now_millis();
486
487 let snap = EquitySnapshot { timestamp, equity };
488 let mut history = load_equity_history();
489 history.snapshots.push(snap);
490
491 if history.snapshots.len() > 1000 {
492 let keep = history.snapshots.split_off(history.snapshots.len() - 1000);
493 history.snapshots = keep;
494 }
495
496 save_equity_history(&history)?;
497
498 let count = history.snapshots.len();
499 let first_equity = history.snapshots.first().map(|s| s.equity).unwrap_or(equity);
500 let peak = history.snapshots.iter().map(|s| s.equity).fold(0.0_f64, f64::max);
501 let change = equity - first_equity;
502 let change_pct = if first_equity > 0.0 { (change / first_equity) * 100.0 } else { 0.0 };
503 let dd_pct = if peak > 0.0 { ((equity / peak) - 1.0) * 100.0 } else { 0.0 };
504
505 let headers = vec!["Metric".into(), "Value".into()];
506 let formatted_time = helpers::format_timestamp(timestamp, true);
507 let rows = vec![
508 vec!["Time".into(), formatted_time],
509 vec!["Equity (IDR)".into(), format_equity(equity)],
510 vec!["Change".into(), format_change(change)],
511 vec!["Change %".into(), format_change_pct(change_pct)],
512 vec!["Peak (IDR)".into(), format_equity(peak)],
513 vec!["Drawdown %".into(), format_change_pct(dd_pct)],
514 vec!["Total Snapshots".into(), count.to_string()],
515 ];
516
517 let data = serde_json::json!({
518 "timestamp": timestamp,
519 "equity": equity,
520 "change": change,
521 "change_pct": change_pct,
522 "peak": peak,
523 "drawdown_pct": dd_pct,
524 "total_snapshots": count,
525 });
526
527 Ok(CommandOutput::new(data, headers, rows))
528}
529
530fn equity_history(limit: usize, all: bool) -> Result<CommandOutput> {
531 let history = load_equity_history();
532
533 if history.snapshots.is_empty() {
534 return Ok(CommandOutput::json(serde_json::json!({
535 "status": "ok",
536 "message": "No equity snapshots. Use `indodax account equity-snap` to record one.",
537 "snapshots": [],
538 })));
539 }
540
541 let first_equity = history.snapshots.first().map(|s| s.equity).unwrap_or(0.0);
542
543 let headers = vec![
544 "Time".into(),
545 "Equity (IDR)".into(),
546 "Change".into(),
547 "Change %".into(),
548 "Peak (IDR)".into(),
549 "DD %".into(),
550 ];
551
552 let snapshots_to_show: Vec<&EquitySnapshot> = if all {
553 history.snapshots.iter().collect()
554 } else {
555 let take = limit.min(history.snapshots.len());
556 history.snapshots[history.snapshots.len() - take..]
557 .iter()
558 .collect()
559 };
560
561 let mut rows: Vec<Vec<String>> = Vec::new();
562 let mut peak = 0.0_f64;
563
564 for snap in &snapshots_to_show {
565 if snap.equity > peak {
566 peak = snap.equity;
567 }
568 let change = snap.equity - first_equity;
569 let change_pct = if first_equity > 0.0 {
570 (change / first_equity) * 100.0
571 } else {
572 0.0
573 };
574 let dd_pct = if peak > 0.0 {
575 ((snap.equity / peak) - 1.0) * 100.0
576 } else {
577 0.0
578 };
579
580 rows.push(vec![
581 format_timestamp_short(snap.timestamp),
582 format_equity(snap.equity),
583 format_change(change),
584 format_change_pct(change_pct),
585 format_equity(peak),
586 format_change_pct(dd_pct),
587 ]);
588 }
589
590 let data = serde_json::json!({
591 "count": history.snapshots.len(),
592 "first_equity": first_equity,
593 "snapshots": history.snapshots.iter().map(|s| serde_json::json!({
594 "timestamp": s.timestamp,
595 "equity": s.equity,
596 })).collect::<Vec<_>>(),
597 });
598
599 let count = history.snapshots.len();
600 Ok(CommandOutput::new(data, headers, rows)
601 .with_addendum(format!("[EQUITY] {} snapshot(s) total", count)))
602}
603
604fn format_equity(val: f64) -> String {
605 format!("{:>14.2}", val)
606}
607
608fn format_change(val: f64) -> String {
609 if val >= 0.0 {
610 format!("+{:>10.2}", val)
611 } else {
612 format!("{:>11.2}", val)
613 }
614}
615
616fn format_change_pct(val: f64) -> String {
617 if val >= 0.0 {
618 format!("+{:>7.2}%", val)
619 } else {
620 format!("{:>8.2}%", val)
621 }
622}
623
624fn format_timestamp_short(ts: u64) -> String {
625 let ts_sec = ts / 1000;
626 chrono::DateTime::from_timestamp(ts_sec.min(i64::MAX as u64) as i64, 0)
627 .map(|dt| dt.format("%b %d %H:%M:%S").to_string())
628 .unwrap_or_else(|| ts.to_string())
629}
630
631#[cfg(test)]
632mod tests {
633 use super::*;
634 use serde_json::json;
635 #[test]
636 fn test_priv_get_existing_key() {
637 let val = json!({"name": "Alice", "age": 30});
638 let result = priv_get(&val, &["name"]);
639 assert_eq!(result, &json!("Alice"));
640 }
641
642 #[test]
643 fn test_priv_get_first_key_exists() {
644 let val = json!({"a": 1, "b": 2});
645 let result = priv_get(&val, &["a", "b"]);
646 assert_eq!(result, &json!(1));
647 }
648
649 #[test]
650 fn test_priv_get_second_key_exists() {
651 let val = json!({"a": null, "b": "2"});
652 let result = priv_get(&val, &["a", "b"]);
653 assert_eq!(result, &json!("2"));
655 }
656
657 #[test]
658 fn test_priv_get_no_keys_exist() {
659 let val = json!({"a": 1});
660 let result = priv_get(&val, &["x", "y", "z"]);
661 assert_eq!(result, &serde_json::Value::Null);
662 }
663
664 #[test]
665 fn test_priv_get_with_json_null() {
666 let val = json!(null);
667 let result = priv_get(&val, &["key"]);
668 assert_eq!(result, &serde_json::Value::Null);
669 }
670
671 #[test]
672 fn test_priv_get_empty_keys() {
673 let val = json!({"a": 1});
674 let result = priv_get(&val, &[]);
675 assert_eq!(result, &serde_json::Value::Null);
676 }
677
678 #[test]
679 fn test_priv_get_nested_value() {
680 let val = json!({"data": {"name": "Bob"}});
681 let result = priv_get(&val, &["data"]);
682 assert_eq!(result, &json!({"name": "Bob"}));
683 }
684
685 #[test]
686 fn test_account_command_variants() {
687 let _cmd1 = AccountCommand::Info;
688 let _cmd2 = AccountCommand::Balance;
689 let _cmd3 = AccountCommand::OpenOrders { pair: Some("btc_idr".into()) };
690 let _cmd4 = AccountCommand::OrderHistory { symbol: "btc_idr".into(), limit: 100 };
691 let _cmd5 = AccountCommand::TradeHistory { symbol: "btc_idr".into(), limit: 100 };
692 let _cmd6 = AccountCommand::TransHistory;
693 let _cmd7 = AccountCommand::GetOrder { order_id: 123, pair: "btc_idr".into() };
694 let _cmd8 = AccountCommand::EquitySnap;
695 let _cmd9 = AccountCommand::EquityHistory { limit: 10, all: false };
696 }
697
698 #[test]
699 fn test_priv_get_with_null_first() {
700 let val = json!({"first": null, "second": "value"});
701 let result = priv_get(&val, &["first", "second"]);
702 assert_eq!(result, &json!("value"));
704 }
705
706 #[test]
707 fn test_priv_get_array_value() {
708 let val = json!({"arr": [1, 2, 3]});
709 let result = priv_get(&val, &["arr"]);
710 assert_eq!(result, &json!([1, 2, 3]));
711 }
712
713 #[test]
714 fn test_priv_get_number_value() {
715 let val = json!({"num": 42.5});
716 let result = priv_get(&val, &["num"]);
717 assert_eq!(result, &json!(42.5));
718 }
719
720 #[test]
721 fn test_priv_get_bool_value() {
722 let val = json!({"flag": true});
723 let result = priv_get(&val, &["flag"]);
724 assert_eq!(result, &json!(true));
725 }
726}