From 01263df73d72301fb60b1a9afb607b03f50ce377 Mon Sep 17 00:00:00 2001 From: Troy Date: Fri, 12 Jun 2026 10:14:52 +0800 Subject: [PATCH 1/3] feat: add Trading Bot API group (create, terminate, update, pause, resume, list, executions) --- crates/cdcx-cli/src/mcp/tools.rs | 32 +++++- crates/cdcx-core/src/config.rs | 1 + crates/cdcx-core/src/openapi/parser.rs | 50 +++++++- crates/cdcx-core/src/safety.rs | 38 ++++++- crates/cdcx-core/src/schema.rs | 1 + schemas/apis/bot.toml | 54 +++++++++ tests/fixtures/test-spec.yaml | 151 +++++++++++++++++++++++++ 7 files changed, 323 insertions(+), 4 deletions(-) create mode 100644 schemas/apis/bot.toml diff --git a/crates/cdcx-cli/src/mcp/tools.rs b/crates/cdcx-cli/src/mcp/tools.rs index afed247..2e365ec 100644 --- a/crates/cdcx-cli/src/mcp/tools.rs +++ b/crates/cdcx-cli/src/mcp/tools.rs @@ -17,9 +17,10 @@ pub fn mcp_to_schema_groups(mcp_group: &str) -> Vec<&'static str> { "fiat" => vec!["fiat"], "stream" => vec!["stream"], // stream tools aren't in schema yet "otc" => vec!["otc"], + "bot" => vec!["bot"], "all" => vec![ "market", "account", "history", "trade", "advanced", "wallet", "fiat", "staking", - "margin", "otc", + "margin", "otc", "bot", ], _ => vec![], } @@ -159,6 +160,7 @@ mod tests { assert_eq!(mcp_to_schema_groups("account"), vec!["account", "history"]); assert_eq!(mcp_to_schema_groups("funding"), vec!["wallet"]); assert_eq!(mcp_to_schema_groups("fiat"), vec!["fiat"]); + assert_eq!(mcp_to_schema_groups("bot"), vec!["bot"]); assert!(mcp_to_schema_groups("unknown").is_empty()); } @@ -210,6 +212,7 @@ mod tests { assert!(tools.iter().any(|t| t.name.starts_with("cdcx_account_"))); assert!(tools.iter().any(|t| t.name.starts_with("cdcx_funding_"))); assert!(tools.iter().any(|t| t.name.starts_with("cdcx_fiat_"))); + assert!(tools.iter().any(|t| t.name.starts_with("cdcx_bot_"))); // Should have tools from all fixture groups assert!( @@ -219,6 +222,33 @@ mod tests { ); } + #[test] + fn test_tool_generation_bot_group() { + let registry = + SchemaRegistry::from_fixture_with_overlays().expect("Failed to parse fixture"); + let tools = generate_tools(®istry, &["bot".to_string()]); + + assert!(tools.iter().any(|t| t.name == "cdcx_bot_create")); + assert!(tools.iter().any(|t| t.name == "cdcx_bot_terminate")); + assert!(tools.iter().any(|t| t.name == "cdcx_bot_list")); + assert!(tools.iter().any(|t| t.name == "cdcx_bot_executions")); + + // terminate is dangerous — should have acknowledged param + let terminate_tool = tools + .iter() + .find(|t| t.name == "cdcx_bot_terminate") + .unwrap(); + let schema_obj = terminate_tool.input_schema.as_ref(); + let properties = schema_obj + .get("properties") + .and_then(|p| p.as_object()) + .expect("Should have properties"); + assert!( + properties.contains_key("acknowledged"), + "Dangerous tool should have acknowledged parameter" + ); + } + #[test] fn test_tool_generation_filters_groups() { let registry = test_registry(); diff --git a/crates/cdcx-core/src/config.rs b/crates/cdcx-core/src/config.rs index 87989a3..8b38a7f 100644 --- a/crates/cdcx-core/src/config.rs +++ b/crates/cdcx-core/src/config.rs @@ -207,6 +207,7 @@ pub const MCP_SERVICE_GROUPS: &[(&str, &str)] = &[ ("funding", "Withdrawals (dangerous)"), ("fiat", "Fiat operations (dangerous)"), ("otc", "OTC desk operations"), + ("bot", "Trading bot management"), ("stream", "Real-time data streams"), ]; diff --git a/crates/cdcx-core/src/openapi/parser.rs b/crates/cdcx-core/src/openapi/parser.rs index 0529fd3..7145603 100644 --- a/crates/cdcx-core/src/openapi/parser.rs +++ b/crates/cdcx-core/src/openapi/parser.rs @@ -41,6 +41,7 @@ pub fn tag_to_group(tag: &str) -> Option<&'static str> { "Staking" => Some("staking"), "Transaction History" => Some("history"), "OTC RFQ for Taker" => Some("otc"), + "Trading Bot API" => Some("bot"), _ => None, } } @@ -57,6 +58,7 @@ pub fn group_description_for(group: &str) -> &'static str { "staking" => "Staking endpoints", "history" => "Transaction history endpoints", "otc" => "OTC RFQ trading endpoints", + "bot" => "Automated trading bot management", _ => "API endpoints", } } @@ -151,6 +153,13 @@ pub fn derive_command_name(method: &str, group: &str) -> String { name = rest.to_string(); } } + "bot" => { + if let Some(rest) = name.strip_prefix("trading-bot-") { + name = rest.to_string(); + } else if let Some(rest) = name.strip_suffix("-trading-bot") { + name = rest.to_string(); + } + } _ => {} } @@ -160,7 +169,10 @@ pub fn derive_command_name(method: &str, group: &str) -> String { /// Derives a safety tier from the method path. pub fn derive_safety_tier(method: &str) -> &'static str { // Dangerous operations - if method == "private/create-withdrawal" || method == "private/fiat/fiat-create-withdraw" { + if method == "private/create-withdrawal" + || method == "private/fiat/fiat-create-withdraw" + || method == "private/bot/terminate-trading-bot" + { return "dangerous"; } @@ -802,6 +814,7 @@ mod tests { assert_eq!(tag_to_group("OTC RFQ for Taker"), Some("otc")); assert_eq!(tag_to_group("Staking"), Some("staking")); assert_eq!(tag_to_group("Fiat Wallet"), Some("fiat")); + assert_eq!(tag_to_group("Trading Bot API"), Some("bot")); } #[test] @@ -1139,6 +1152,28 @@ mod tests { derive_command_name("private/get-transactions", "history"), "transactions" ); + + // Bot + assert_eq!( + derive_command_name("private/bot/terminate-trading-bot", "bot"), + "terminate" + ); + assert_eq!( + derive_command_name("private/bot/update-trading-bot", "bot"), + "update" + ); + assert_eq!( + derive_command_name("private/bot/pause-trading-bot", "bot"), + "pause" + ); + assert_eq!( + derive_command_name("private/bot/resume-trading-bot", "bot"), + "resume" + ); + assert_eq!( + derive_command_name("private/bot/get-trading-bot-executions", "bot"), + "executions" + ); } #[test] @@ -1157,6 +1192,15 @@ mod tests { "mutate" ); assert_eq!(derive_safety_tier("private/user-balance"), "read"); + assert_eq!( + derive_safety_tier("private/bot/terminate-trading-bot"), + "dangerous" + ); + assert_eq!( + derive_safety_tier("private/bot/create-trading-bot"), + "mutate" + ); + assert_eq!(derive_safety_tier("private/bot/get-trading-bots"), "read"); } #[test] @@ -1166,6 +1210,10 @@ mod tests { "Public market data endpoints" ); assert_eq!(group_description_for("otc"), "OTC RFQ trading endpoints"); + assert_eq!( + group_description_for("bot"), + "Automated trading bot management" + ); assert_eq!(group_description_for("unknown"), "API endpoints"); } diff --git a/crates/cdcx-core/src/safety.rs b/crates/cdcx-core/src/safety.rs index 1516e50..e1d2b63 100644 --- a/crates/cdcx-core/src/safety.rs +++ b/crates/cdcx-core/src/safety.rs @@ -23,7 +23,8 @@ impl SafetyTier { "private/cancel-all-orders" | "private/advanced/cancel-all-orders" | "private/create-withdrawal" - | "private/fiat/fiat-create-withdraw" => Self::Dangerous, + | "private/fiat/fiat-create-withdraw" + | "private/bot/terminate-trading-bot" => Self::Dangerous, // Mutate operations "private/create-order" @@ -50,7 +51,11 @@ impl SafetyTier { | "private/staking/stake" | "private/staking/unstake" | "private/staking/convert" - | "private/create-subaccount-transfer" => Self::Mutate, + | "private/create-subaccount-transfer" + | "private/bot/create-trading-bot" + | "private/bot/update-trading-bot" + | "private/bot/pause-trading-bot" + | "private/bot/resume-trading-bot" => Self::Mutate, // Everything else that's private is SensitiveRead _ => Self::SensitiveRead, @@ -150,6 +155,35 @@ mod tests { SafetyTier::from_method("private/create-withdrawal"), SafetyTier::Dangerous ); + // Bot endpoints + assert_eq!( + SafetyTier::from_method("private/bot/create-trading-bot"), + SafetyTier::Mutate + ); + assert_eq!( + SafetyTier::from_method("private/bot/update-trading-bot"), + SafetyTier::Mutate + ); + assert_eq!( + SafetyTier::from_method("private/bot/pause-trading-bot"), + SafetyTier::Mutate + ); + assert_eq!( + SafetyTier::from_method("private/bot/resume-trading-bot"), + SafetyTier::Mutate + ); + assert_eq!( + SafetyTier::from_method("private/bot/terminate-trading-bot"), + SafetyTier::Dangerous + ); + assert_eq!( + SafetyTier::from_method("private/bot/get-trading-bots"), + SafetyTier::SensitiveRead + ); + assert_eq!( + SafetyTier::from_method("private/bot/get-trading-bot-executions"), + SafetyTier::SensitiveRead + ); } #[test] diff --git a/crates/cdcx-core/src/schema.rs b/crates/cdcx-core/src/schema.rs index b20a17c..aee4f88 100644 --- a/crates/cdcx-core/src/schema.rs +++ b/crates/cdcx-core/src/schema.rs @@ -134,6 +134,7 @@ impl SchemaRegistry { include_str!("../../../schemas/apis/staking.toml"), include_str!("../../../schemas/apis/margin.toml"), include_str!("../../../schemas/apis/history.toml"), + include_str!("../../../schemas/apis/bot.toml"), ] .iter() .map(|s| { diff --git a/schemas/apis/bot.toml b/schemas/apis/bot.toml new file mode 100644 index 0000000..bfd1b55 --- /dev/null +++ b/schemas/apis/bot.toml @@ -0,0 +1,54 @@ +[group] +name = "bot" + +[[endpoints]] +method = "private/bot/create-trading-bot" +command = "create" + +[[endpoints.params]] +name = "bot_type" +position = 0 + +[[endpoints]] +method = "private/bot/terminate-trading-bot" +command = "terminate" + +[[endpoints.params]] +name = "bot_id" +position = 0 + +[[endpoints]] +method = "private/bot/update-trading-bot" +command = "update" + +[[endpoints.params]] +name = "bot_id" +position = 0 + +[[endpoints]] +method = "private/bot/pause-trading-bot" +command = "pause" + +[[endpoints.params]] +name = "bot_id" +position = 0 + +[[endpoints]] +method = "private/bot/resume-trading-bot" +command = "resume" + +[[endpoints.params]] +name = "bot_id" +position = 0 + +[[endpoints]] +method = "private/bot/get-trading-bots" +command = "list" + +[[endpoints]] +method = "private/bot/get-trading-bot-executions" +command = "executions" + +[[endpoints.params]] +name = "bot_id" +position = 0 diff --git a/tests/fixtures/test-spec.yaml b/tests/fixtures/test-spec.yaml index c88c778..926fc5d 100644 --- a/tests/fixtures/test-spec.yaml +++ b/tests/fixtures/test-spec.yaml @@ -365,3 +365,154 @@ paths: responses: '200': description: Success + /private/bot/create-trading-bot: + post: + tags: + - Trading Bot API + summary: private/bot/create-trading-bot + description: Create a trading bot. + requestBody: + content: + application/json: + schema: + type: object + properties: + params: + type: object + required: + - bot_type + - notify_on_bot_change + - settings + properties: + bot_type: + type: string + enum: + - DCA + - TWAP + - GRID + - FUNDING_ARBITRAGE + description: Type of trading bot + notify_on_bot_change: + type: boolean + description: Push notifications on bot state changes + display_name: + type: string + description: Display name of the trading bot + settings: + type: object + description: Bot-type-specific settings + responses: + '200': + description: Success + /private/bot/terminate-trading-bot: + post: + tags: + - Trading Bot API + summary: private/bot/terminate-trading-bot + description: Terminate a trading bot. + requestBody: + content: + application/json: + schema: + type: object + properties: + params: + type: object + required: + - bot_id + - bot_type + properties: + bot_id: + type: integer + description: Trading Bot ID + bot_type: + type: string + enum: + - DCA + - TWAP + - GRID + - FUNDING_ARBITRAGE + description: Type of trading bot + close_position: + type: boolean + description: Whether to close open positions on termination + responses: + '200': + description: Success + /private/bot/get-trading-bots: + post: + tags: + - Trading Bot API + summary: private/bot/get-trading-bots + description: Get all trading bots. + requestBody: + content: + application/json: + schema: + type: object + properties: + params: + type: object + required: + - bot_types + properties: + bot_id: + type: integer + description: Find by Trading Bot ID + bot_types: + type: array + items: + type: string + enum: + - DCA + - TWAP + - GRID + - FUNDING_ARBITRAGE + description: Filter by bot type + state: + type: array + items: + type: string + enum: + - RUNNING + - PAUSED + - TERMINATED + description: Filter by bot state + page: + type: integer + description: Page number + page_size: + type: integer + description: Page size + responses: + '200': + description: Success + /private/bot/get-trading-bot-executions: + post: + tags: + - Trading Bot API + summary: private/bot/get-trading-bot-executions + description: Returns execution history for a trading bot. + requestBody: + content: + application/json: + schema: + type: object + properties: + params: + type: object + required: + - bot_id + properties: + bot_id: + type: integer + description: Trading Bot ID + page: + type: integer + description: Page number + page_size: + type: integer + description: Page size + responses: + '200': + description: Success From 726bfb5ec49ed82bd6c458c170e9cc35ef009dd1 Mon Sep 17 00:00:00 2001 From: Troy Date: Fri, 12 Jun 2026 18:25:46 +0800 Subject: [PATCH 2/3] fix(cli): boolean and oneOf anyOf openapi parser --- crates/cdcx-cli/src/cli_builder.rs | 5 +++++ crates/cdcx-core/src/openapi/parser.rs | 11 ++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/crates/cdcx-cli/src/cli_builder.rs b/crates/cdcx-cli/src/cli_builder.rs index 16cd7e0..f375e99 100644 --- a/crates/cdcx-cli/src/cli_builder.rs +++ b/crates/cdcx-cli/src/cli_builder.rs @@ -109,6 +109,11 @@ pub fn extract_params(matches: &clap::ArgMatches, params: &[ParamSchema]) -> ser serde_json::json!(value_str) } } + "boolean" => match value_str.as_str() { + "true" | "1" => serde_json::json!(true), + "false" | "0" => serde_json::json!(false), + _ => serde_json::json!(value_str), + }, "json" => { serde_json::from_str(value_str).unwrap_or_else(|_| serde_json::json!(value_str)) } diff --git a/crates/cdcx-core/src/openapi/parser.rs b/crates/cdcx-core/src/openapi/parser.rs index 7145603..21bd8bb 100644 --- a/crates/cdcx-core/src/openapi/parser.rs +++ b/crates/cdcx-core/src/openapi/parser.rs @@ -661,7 +661,16 @@ fn extract_parameters( .get("description") .and_then(|d| d.as_str()) .unwrap_or_default(); - let raw_type = val.get("type").and_then(|t| t.as_str()).unwrap_or("string"); + let raw_type = val + .get("type") + .and_then(|t| t.as_str()) + .unwrap_or_else(|| { + if val.get("oneOf").is_some() || val.get("anyOf").is_some() { + "object" + } else { + "string" + } + }); let mut is_required = required_list.contains(&name.to_string()); let enum_values = extract_enum_values(val, openapi_doc, schema_doc); From 7f45f371af670917f85d3fd46770c3fa61cf4bd0 Mon Sep 17 00:00:00 2001 From: Troy Date: Fri, 12 Jun 2026 18:29:19 +0800 Subject: [PATCH 3/3] chore(cli): fmt --- crates/cdcx-core/src/openapi/parser.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/cdcx-core/src/openapi/parser.rs b/crates/cdcx-core/src/openapi/parser.rs index 21bd8bb..c77b664 100644 --- a/crates/cdcx-core/src/openapi/parser.rs +++ b/crates/cdcx-core/src/openapi/parser.rs @@ -661,10 +661,8 @@ fn extract_parameters( .get("description") .and_then(|d| d.as_str()) .unwrap_or_default(); - let raw_type = val - .get("type") - .and_then(|t| t.as_str()) - .unwrap_or_else(|| { + let raw_type = + val.get("type").and_then(|t| t.as_str()).unwrap_or_else(|| { if val.get("oneOf").is_some() || val.get("anyOf").is_some() { "object" } else {