diff --git a/hummingbot_mcp/guides/dca_executor.md b/hummingbot_mcp/guides/dca_executor.md index 23bc990..9877758 100644 --- a/hummingbot_mcp/guides/dca_executor.md +++ b/hummingbot_mcp/guides/dca_executor.md @@ -9,3 +9,53 @@ Dollar-cost averages into positions over time with scheduled purchases. **Avoid when:** - You need immediate full position entry - You want active trading with quick exits + +#### How It Works + +- Places multiple limit orders at decreasing (BUY) or increasing (SELL) prices to average into a position +- `amounts_quote` and `prices` are parallel lists — each index is one DCA level +- Amount is in **quote currency** (e.g., USDT). Each level can have a different amount +- Exit managed via stop-loss, take-profit, trailing stop, or time limit + +**CRITICAL:** +- Uses `amounts_quote` (list, quote currency) — NOT `amount` or `total_amount_quote` +- `prices` list must be the same length as `amounts_quote` +- Always fetch the schema first via progressive disclosure (`manage_executors(executor_type='dca_executor')`) before creating + +#### Parameter Reference + +**Core:** +- `connector_name`: Exchange connector (e.g., 'binance_perpetual') +- `trading_pair`: Trading pair (e.g., 'BTC-USDT') +- `side`: 1 (BUY) or 2 (SELL) +- `amounts_quote`: List of order sizes in **quote currency** (e.g., [100, 100, 150]) +- `prices`: List of price levels for each DCA order (e.g., [50000, 48000, 46000]) +- `leverage`: Leverage multiplier (default: 1) + +**Exit Config:** +- `take_profit`: TP as decimal (e.g., 0.03 = 3%) (optional) +- `stop_loss`: SL as decimal (e.g., 0.05 = 5%) (optional) +- `trailing_stop`: TrailingStop config (optional) +- `time_limit`: Max duration in seconds (optional) + +**Execution:** +- `mode`: "MAKER" (limit orders, default) or "TAKER" (market orders) +- `activation_bounds`: Price bounds for activation (optional) +- `level_id`: Optional identifier tag + +#### Example + +DCA buy into BTC with 3 levels, 3% TP and 5% SL: +```json +{ + "connector_name": "binance_perpetual", + "trading_pair": "BTC-USDT", + "side": 1, + "amounts_quote": [100, 100, 150], + "prices": [50000, 48000, 46000], + "leverage": 1, + "take_profit": 0.03, + "stop_loss": 0.05, + "mode": "MAKER" +} +``` diff --git a/hummingbot_mcp/guides/order_executor.md b/hummingbot_mcp/guides/order_executor.md index d110251..2c2ece1 100644 --- a/hummingbot_mcp/guides/order_executor.md +++ b/hummingbot_mcp/guides/order_executor.md @@ -29,3 +29,6 @@ Closest executor to a plain BUY/SELL order but with strategy options. - `execution_strategy`: LIMIT, MARKET, LIMIT_MAKER, or LIMIT_CHASER - `price`: Required for LIMIT/LIMIT_MAKER strategies - `chaser_config`: Required for LIMIT_CHASER strategy +- `leverage`: Leverage multiplier (default: 1) +- `position_action`: 'OPEN' or 'CLOSE' (default: 'OPEN', useful for perpetuals in HEDGE mode) +- `level_id`: Optional identifier tag diff --git a/hummingbot_mcp/guides/position_executor.md b/hummingbot_mcp/guides/position_executor.md index 20550c6..d297f3c 100644 --- a/hummingbot_mcp/guides/position_executor.md +++ b/hummingbot_mcp/guides/position_executor.md @@ -9,3 +9,56 @@ Takes directional positions with defined entry, stop-loss, and take-profit level **Avoid when:** - You want to provide liquidity (use Market Making instead) - You need complex multi-leg strategies + +#### How It Works + +- Opens a directional position (long/short) with optional limit entry price +- Manages exit via triple barrier config: stop-loss, take-profit, time limit, trailing stop +- Amount is in **base currency** (NOT quote). e.g., for BTC-USDT, amount=0.01 means 0.01 BTC + +**CRITICAL:** +- `amount` is in **base currency** — NOT `total_amount_quote`. To convert from USD: `amount = usd_value / entry_price` +- Always fetch the schema first via progressive disclosure (`manage_executors(executor_type='position_executor')`) before creating + +#### Parameter Reference + +**Core:** +- `connector_name`: Exchange connector (e.g., 'binance_perpetual') +- `trading_pair`: Trading pair (e.g., 'BTC-USDT') +- `side`: 1 (BUY/LONG) or 2 (SELL/SHORT) +- `amount`: Position size in **base currency** (e.g., 0.01 BTC). To convert from USD: `amount = usd / price` +- `entry_price`: Limit entry price (optional — omit for market entry) +- `leverage`: Leverage multiplier (default: 1) + +**Triple Barrier Config (`triple_barrier_config`):** +- `stop_loss`: Stop-loss as decimal (e.g., 0.02 = 2%) +- `take_profit`: Take-profit as decimal (e.g., 0.03 = 3%) +- `time_limit`: Max position duration in seconds (optional) +- `trailing_stop.activation_price`: Price delta to activate trailing stop +- `trailing_stop.trailing_delta`: Trailing distance +- `open_order_type`: 1=MARKET, 2=LIMIT, 3=LIMIT_MAKER +- `take_profit_order_type`: same enum (default: MARKET) +- `stop_loss_order_type`: same enum (default: MARKET) +- `time_limit_order_type`: same enum (default: MARKET) + +**Optional:** +- `activation_bounds`: Price bounds for activation (optional) +- `level_id`: Optional identifier tag + +#### Example + +Long 0.01 BTC with 2% SL and 3% TP: +```json +{ + "connector_name": "binance_perpetual", + "trading_pair": "BTC-USDT", + "side": 1, + "amount": 0.01, + "leverage": 5, + "triple_barrier_config": { + "stop_loss": 0.02, + "take_profit": 0.03, + "open_order_type": 2 + } +} +``` diff --git a/hummingbot_mcp/schemas.py b/hummingbot_mcp/schemas.py index 7780a6f..fb7c847 100644 --- a/hummingbot_mcp/schemas.py +++ b/hummingbot_mcp/schemas.py @@ -147,16 +147,17 @@ class ManageExecutorsRequest(BaseModel): 1. No params -> List available executor types with descriptions 2. executor_type only -> Show config schema with user defaults applied 3. action="create" + executor_config -> Create executor (merged with defaults) - 4. action="search" -> Search/list executors with filters - 5. action="get" + executor_id -> Get specific executor details - 6. action="stop" + executor_id -> Stop executor - 7. action="get_summary" -> Get overall executor summary - 8. action="get_preferences" -> View saved preferences (optionally for specific executor_type) - 9. action="save_preferences" + preferences_content -> Save full preferences file content - 10. action="reset_preferences" -> Reset preferences to defaults + 4. action="search" -> Search/list executors (or get detail if executor_id provided) + 5. action="stop" + executor_id -> Stop executor + 6. action="get_logs" + executor_id -> Get executor logs + 7. action="get_preferences" -> View saved preferences + 8. action="save_preferences" + preferences_content -> Save preferences file + 9. action="reset_preferences" -> Reset preferences to defaults + 10. action="positions_summary" -> Get positions (or specific if connector_name+trading_pair given) + 11. action="clear_position" + connector_name + trading_pair -> Clear position """ - action: Literal["create", "search", "get", "stop", "get_summary", "get_preferences", "save_preferences", "reset_preferences", "positions_summary", "get_position", "clear_position"] | None = Field( + action: Literal["create", "search", "stop", "get_logs", "get_preferences", "save_preferences", "reset_preferences", "positions_summary", "clear_position"] | None = Field( default=None, description="Action to perform. Leave empty to see executor types or show schema.", ) @@ -176,6 +177,12 @@ class ManageExecutorsRequest(BaseModel): description="Executor ID for 'get' or 'stop' actions.", ) + # Log options + log_level: str | None = Field( + default=None, + description="Filter logs by level (ERROR, WARNING, INFO, DEBUG). Only for 'get_logs' action.", + ) + # Search filters account_names: list[str] | None = Field( default=None, @@ -258,9 +265,7 @@ def validate_executor_type(cls, v: str | None) -> str | None: def get_flow_stage(self) -> str: """Determine which stage of the flow we're in.""" - if self.action == "get_summary": - return "get_summary" - elif self.action == "get_preferences": + if self.action == "get_preferences": return "get_preferences" elif self.action == "save_preferences" and self.preferences_content: return "save_preferences" @@ -268,16 +273,14 @@ def get_flow_stage(self) -> str: return "reset_preferences" elif self.action == "search": return "search" - elif self.action == "get" and self.executor_id: - return "get" elif self.action == "stop" and self.executor_id: return "stop" + elif self.action == "get_logs" and self.executor_id: + return "get_logs" elif self.action == "create" and self.executor_config: return "create" elif self.action == "positions_summary": return "positions_summary" - elif self.action == "get_position" and self.connector_name and self.trading_pair: - return "get_position" elif self.action == "clear_position" and self.connector_name and self.trading_pair: return "clear_position" elif self.executor_type is not None: diff --git a/hummingbot_mcp/server.py b/hummingbot_mcp/server.py index 5c1d688..dc7e832 100644 --- a/hummingbot_mcp/server.py +++ b/hummingbot_mcp/server.py @@ -753,10 +753,11 @@ async def manage_bots( @mcp.tool() @handle_errors("manage executors") async def manage_executors( - action: Literal["create", "search", "get", "stop", "get_summary", "get_preferences", "save_preferences", "reset_preferences", "positions_summary", "get_position", "clear_position"] | None = None, + action: Literal["create", "search", "stop", "get_logs", "get_preferences", "save_preferences", "reset_preferences", "positions_summary", "clear_position"] | None = None, executor_type: str | None = None, executor_config: dict[str, Any] | None = None, executor_id: str | None = None, + log_level: str | None = None, account_names: list[str] | None = None, connector_names: list[str] | None = None, trading_pairs: list[str] | None = None, @@ -813,10 +814,13 @@ async def manage_executors( Executors are automated trading components that execute specific strategies. This tool guides you through understanding, creating, monitoring, and stopping executors. - IMPORTANT: When creating any executor, you MUST ask the user for `total_amount_quote` (the capital - to allocate) before creating. Never assume or default this value. The amount is denominated in the - quote currency of the trading pair (e.g., BRL for BTC-BRL, USDT for BTC-USDT). If the user gives - a USD amount, convert it to the quote currency first. + IMPORTANT: When creating any executor, you MUST ask the user how much capital to allocate before creating. + Each executor type uses a DIFFERENT amount field: + - grid_executor: `total_amount_quote` (quote currency, e.g., 100 USDT) + - position_executor: `amount` (BASE currency, e.g., 0.01 BTC). Convert from USD: amount = usd / price + - dca_executor: `amounts_quote` (list of quote amounts per level, e.g., [100, 100, 150]) + - order_executor: `amount` (base currency, or '$100' for USD value) + Never assume or default these values. Always check the guide first via progressive disclosure. IMPORTANT - Grid Executor Side: When creating a grid_executor, you MUST explicitly set the `side` parameter using numeric enum values: @@ -836,26 +840,25 @@ async def manage_executors( Progressive Flow: 1. executor_type only → Show config schema with your saved defaults applied 2. action="create" + executor_config → Create executor (merged with your defaults) - 3. action="search" → Search/list executors with filters - 4. action="get" + executor_id → Get specific executor details - 5. action="stop" + executor_id → Stop executor (with keep_position option) - 6. action="get_summary" → Get overall executor summary + 3. action="search" → Search/list executors with filters (add executor_id to get detail for one) + 4. action="stop" + executor_id → Stop executor (with keep_position option) + 5. action="get_logs" + executor_id → Get executor logs (only for active executors, logs cleared on completion) Preference Management (stored in ~/.hummingbot_mcp/executor_preferences.md): - 7. action="get_preferences" → View raw markdown preferences file (read before saving) - 8. action="save_preferences" + preferences_content → Save complete preferences file content - 9. action="reset_preferences" → Reset all preferences to defaults + 6. action="get_preferences" → View raw markdown preferences file (read before saving) + 7. action="save_preferences" + preferences_content → Save complete preferences file content + 8. action="reset_preferences" → Reset all preferences to defaults Position Management: - 10. action="positions_summary" → Get aggregated positions summary - 11. action="get_position" + connector_name + trading_pair → Get specific position details - 12. action="clear_position" + connector_name + trading_pair → Clear position closed manually + 9. action="positions_summary" → Get all positions (add connector_name + trading_pair for specific one) + 10. action="clear_position" + connector_name + trading_pair → Clear position closed manually Args: action: Action to perform. Leave empty to see executor types or config schema. executor_type: Type of executor (e.g., 'position_executor', 'dca_executor'). Provide to see config schema. executor_config: Configuration for creating an executor. Required for 'create' action. - executor_id: Executor ID for 'get' or 'stop' actions. + executor_id: Executor ID for 'search' (detail), 'stop', or 'get_logs' actions. + log_level: Filter logs by level - 'ERROR', 'WARNING', 'INFO', 'DEBUG' (for get_logs). account_names: Filter by account names (for search). connector_names: Filter by connector names (for search). trading_pairs: Filter by trading pairs (for search). @@ -876,6 +879,7 @@ async def manage_executors( executor_type=executor_type, executor_config=executor_config, executor_id=executor_id, + log_level=log_level, account_names=account_names, connector_names=connector_names, trading_pairs=trading_pairs, diff --git a/hummingbot_mcp/tools/executors.py b/hummingbot_mcp/tools/executors.py index c8ec356..171bcd9 100644 --- a/hummingbot_mcp/tools/executors.py +++ b/hummingbot_mcp/tools/executors.py @@ -11,7 +11,6 @@ from hummingbot_mcp.formatters.executors import ( format_executor_detail, format_executor_schema_table, - format_executor_summary, format_executors_table, format_positions_held_table, format_positions_summary, @@ -201,8 +200,19 @@ async def manage_executors(client: Any, request: ManageExecutorsRequest) -> dict } elif flow_stage == "search": - # Stage 4: Search executors + # Search executors, or get detail for a specific executor_id try: + if request.executor_id: + # Get specific executor detail + result = await client.executors.get_executor(request.executor_id) + formatted = format_executor_detail(result) + return { + "action": "search", + "executor_id": request.executor_id, + "executor": result, + "formatted_output": formatted, + } + result = await client.executors.search_executors( account_names=request.account_names, connector_names=request.connector_names, @@ -239,27 +249,6 @@ async def manage_executors(client: Any, request: ManageExecutorsRequest) -> dict "formatted_output": f"Error searching executors: {e}", } - elif flow_stage == "get": - # Stage 5: Get specific executor - try: - result = await client.executors.get_executor(request.executor_id) - - formatted = format_executor_detail(result) - - return { - "action": "get", - "executor_id": request.executor_id, - "executor": result, - "formatted_output": formatted, - } - - except Exception as e: - return { - "action": "get", - "error": str(e), - "formatted_output": f"Error getting executor {request.executor_id}: {e}", - } - elif flow_stage == "stop": # Stage 6: Stop executor try: @@ -287,56 +276,54 @@ async def manage_executors(client: Any, request: ManageExecutorsRequest) -> dict "formatted_output": f"Error stopping executor {request.executor_id}: {e}", } - elif flow_stage == "get_summary": - # Stage 7: Get overall summary with positions and recent executors + elif flow_stage == "get_logs": + # Get executor logs via direct API call (not yet in client library) try: - result = await client.executors.get_summary() + params = {"limit": request.limit} + if request.log_level: + params["level"] = request.log_level.upper() - formatted = format_executor_summary(result) + resp = await client.executors.session.get( + f"{client.executors.base_url}/executors/{request.executor_id}/logs", + params=params, + ) + resp.raise_for_status() + result = await resp.json() - # Fetch positions held - positions = [] - try: - positions_result = await client.executors.get_positions_summary() - positions = positions_result.get("positions", positions_result) if isinstance(positions_result, dict) else positions_result - if not isinstance(positions, list): - positions = [positions] if positions else [] + logs = result.get("logs", []) + total = result.get("total_count", len(logs)) - if positions: - formatted += "\n\nPositions Held:\n" - formatted += format_positions_held_table(positions) - except Exception: - # If fetching positions fails, continue without them - pass - - # Also fetch last 10 executors to show recent activity - recent_executors = [] - try: - recent_result = await client.executors.search_executors(limit=10) - recent_executors = recent_result.get("data", recent_result) if isinstance(recent_result, dict) else recent_result - if not isinstance(recent_executors, list): - recent_executors = [recent_executors] if recent_executors else [] - - if recent_executors: - formatted += "\n\nRecent Executors (last 10):\n" - formatted += format_executors_table(recent_executors) - except Exception: - # If fetching recent executors fails, just show the summary - pass + formatted = f"Executor Logs: {request.executor_id}\n" + formatted += f"Total entries: {total}" + if request.log_level: + formatted += f" (filtered: {request.log_level.upper()})" + formatted += f", showing: {len(logs)}\n\n" + + if not logs: + formatted += "No log entries found. Note: logs are only available for active executors and are cleared on completion." + else: + for entry in logs: + ts = entry.get("timestamp", "") + level = entry.get("level", "") + msg = entry.get("message", "") + formatted += f"[{ts}] {level}: {msg}\n" + exc = entry.get("exc_info") + if exc: + formatted += f" Exception: {exc}\n" return { - "action": "get_summary", - "summary": result, - "positions": positions, - "recent_executors": recent_executors, + "action": "get_logs", + "executor_id": request.executor_id, + "logs": logs, + "total_count": total, "formatted_output": formatted, } except Exception as e: return { - "action": "get_summary", + "action": "get_logs", "error": str(e), - "formatted_output": f"Error getting executor summary: {e}", + "formatted_output": f"Error getting logs for executor {request.executor_id}: {e}", } elif flow_stage == "get_preferences": @@ -390,8 +377,37 @@ async def manage_executors(client: Any, request: ManageExecutorsRequest) -> dict # Position management stages (merged from manage_executor_positions) elif flow_stage == "positions_summary": - # Get positions summary (aggregated view) + # Get all positions, or specific position if connector_name + trading_pair given try: + if request.connector_name and request.trading_pair: + # Get specific position detail + account = request.account_name or "master_account" + result = await client.executors.get_position_held( + connector_name=request.connector_name, + trading_pair=request.trading_pair, + account_name=account, + ) + + formatted = f"Position Details\n\n" + formatted += f"Connector: {request.connector_name}\n" + formatted += f"Trading Pair: {request.trading_pair}\n" + formatted += f"Account: {account}\n\n" + + if result: + positions = [result] if not isinstance(result, list) else result + formatted += format_positions_held_table(positions) + else: + formatted += "No position found for this connector/pair combination." + + return { + "action": "positions_summary", + "connector_name": request.connector_name, + "trading_pair": request.trading_pair, + "account": account, + "position": result, + "formatted_output": formatted, + } + result = await client.executors.get_positions_summary() positions = result.get("positions", result) if isinstance(result, dict) else result @@ -413,52 +429,13 @@ async def manage_executors(client: Any, request: ManageExecutorsRequest) -> dict "positions": positions, "summary": result if isinstance(result, dict) else {"positions": positions}, "formatted_output": formatted, - "next_step": "Call with action='get_position', connector_name and trading_pair to see specific position details", } except Exception as e: return { "action": "positions_summary", "error": str(e), - "formatted_output": f"Error getting positions summary: {e}", - } - - elif flow_stage == "get_position": - # Get specific position details - account = request.account_name or "master_account" - try: - result = await client.executors.get_position_held( - connector_name=request.connector_name, - trading_pair=request.trading_pair, - account_name=account, - ) - - formatted = f"Position Details\n\n" - formatted += f"Connector: {request.connector_name}\n" - formatted += f"Trading Pair: {request.trading_pair}\n" - formatted += f"Account: {account}\n\n" - - if result: - positions = [result] if not isinstance(result, list) else result - formatted += format_positions_held_table(positions) - else: - formatted += "No position found for this connector/pair combination." - - return { - "action": "get_position", - "connector_name": request.connector_name, - "trading_pair": request.trading_pair, - "account": account, - "position": result, - "formatted_output": formatted, - "next_step": "Use action='clear_position' if you need to clear this position", - } - - except Exception as e: - return { - "action": "get_position", - "error": str(e), - "formatted_output": f"Error getting position: {e}", + "formatted_output": f"Error getting positions: {e}", } elif flow_stage == "clear_position":