lp-agent This skill helps you run automated liquidity provision strategies on concentrated liquidity (CLMM) DEXs using Hummingbot API. Commands (run as ): | Command | Description | |---------|-------------| | | Onboarding wizard — check setup status and get started | | | Deploy Hummingbot API trading infrastructure | | | Start Gateway, configure network RPC endpoints | | | Add or import a Solana wallet | | | Find and explore Meteora DLMM pools | | | Choose LP Executor or Rebalancer Controller | | | Run, monitor, and manage LP strategies | | | Visualize LP position performance | New here? Run…

)} |\")\n print(f\"| 24h Volume | {format_number(volume.get('24h'), prefix='

lp-agent This skill helps you run automated liquidity provision strategies on concentrated liquidity (CLMM) DEXs using Hummingbot API. Commands (run as ): | Command | Description | |---------|-------------| | | Onboarding wizard — check setup status and get started | | | Deploy Hummingbot API trading infrastructure | | | Start Gateway, configure network RPC endpoints | | | Add or import a Solana wallet | | | Find and explore Meteora DLMM pools | | | Choose LP Executor or Rebalancer Controller | | | Run, monitor, and manage LP strategies | | | Visualize LP position performance | New here? Run…

)} |\")\n print(f\"| 24h Fees | {format_number(fees.get('24h'), prefix='

lp-agent This skill helps you run automated liquidity provision strategies on concentrated liquidity (CLMM) DEXs using Hummingbot API. Commands (run as ): | Command | Description | |---------|-------------| | | Onboarding wizard — check setup status and get started | | | Deploy Hummingbot API trading infrastructure | | | Start Gateway, configure network RPC endpoints | | | Add or import a Solana wallet | | | Find and explore Meteora DLMM pools | | | Choose LP Executor or Rebalancer Controller | | | Run, monitor, and manage LP strategies | | | Visualize LP position performance | New here? Run…

)} |\")\n print(f\"| APR | {format_percent(pool.get('apr'))} |\")\n print(f\"| APY | {format_percent(pool.get('apy'))} |\")\n print(f\"| Fee Tier | {format_percent(config.get('base_fee_pct'))} |\")\n print(f\"| Bin Step | {config.get('bin_step', '—')} |\")\n\n # Max width calculation\n bin_step = config.get(\"bin_step\")\n if bin_step:\n max_width = float(bin_step) * 69 / 100\n print(f\"| Max Range Width | ~{max_width:.1f}% |\")\n\n print(f\"| Has Farm | {'Yes' if pool.get('has_farm') else 'No'} |\")\n\n # Token Info Table\n print(\"\\n### Token Info\\n\")\n print(\"| Token | Symbol | Mint | Price | Decimals | Market Cap |\")\n print(\"|-------|--------|------|-------|----------|------------|\")\n print(f\"| Base (X) | {token_x.get('symbol', '?')} | `{token_x.get('address', '—')}` | ${format_number_raw(token_x.get('price'), 4)} | {token_x.get('decimals', '—')} | {format_number(token_x.get('market_cap'), prefix='

lp-agent This skill helps you run automated liquidity provision strategies on concentrated liquidity (CLMM) DEXs using Hummingbot API. Commands (run as ): | Command | Description | |---------|-------------| | | Onboarding wizard — check setup status and get started | | | Deploy Hummingbot API trading infrastructure | | | Start Gateway, configure network RPC endpoints | | | Add or import a Solana wallet | | | Find and explore Meteora DLMM pools | | | Choose LP Executor or Rebalancer Controller | | | Run, monitor, and manage LP strategies | | | Visualize LP position performance | New here? Run…

)} |\")\n print(f\"| Quote (Y) | {token_y.get('symbol', '?')} | `{token_y.get('address', '—')}` | ${format_number_raw(token_y.get('price'), 4)} | {token_y.get('decimals', '—')} | {format_number(token_y.get('market_cap'), prefix='

lp-agent This skill helps you run automated liquidity provision strategies on concentrated liquidity (CLMM) DEXs using Hummingbot API. Commands (run as ): | Command | Description | |---------|-------------| | | Onboarding wizard — check setup status and get started | | | Deploy Hummingbot API trading infrastructure | | | Start Gateway, configure network RPC endpoints | | | Add or import a Solana wallet | | | Find and explore Meteora DLMM pools | | | Choose LP Executor or Rebalancer Controller | | | Run, monitor, and manage LP strategies | | | Visualize LP position performance | New here? Run…

)} |\")\n\n # Reserves\n print(\"\\n### Reserves\\n\")\n print(\"| Token | Amount | Value |\")\n print(\"|-------|--------|-------|\")\n base_amount = pool.get(\"token_x_amount\", 0)\n quote_amount = pool.get(\"token_y_amount\", 0)\n base_price = token_x.get(\"price\", 0) or 0\n quote_price = token_y.get(\"price\", 0) or 0\n print(f\"| {token_x.get('symbol', 'X')} | {format_number(base_amount)} | {format_number(base_amount * base_price if base_amount and base_price else None, prefix='

lp-agent This skill helps you run automated liquidity provision strategies on concentrated liquidity (CLMM) DEXs using Hummingbot API. Commands (run as ): | Command | Description | |---------|-------------| | | Onboarding wizard — check setup status and get started | | | Deploy Hummingbot API trading infrastructure | | | Start Gateway, configure network RPC endpoints | | | Add or import a Solana wallet | | | Find and explore Meteora DLMM pools | | | Choose LP Executor or Rebalancer Controller | | | Run, monitor, and manage LP strategies | | | Visualize LP position performance | New here? Run…

)} |\")\n print(f\"| {token_y.get('symbol', 'Y')} | {format_number(quote_amount)} | {format_number(quote_amount * quote_price if quote_amount and quote_price else None, prefix='

lp-agent This skill helps you run automated liquidity provision strategies on concentrated liquidity (CLMM) DEXs using Hummingbot API. Commands (run as ): | Command | Description | |---------|-------------| | | Onboarding wizard — check setup status and get started | | | Deploy Hummingbot API trading infrastructure | | | Start Gateway, configure network RPC endpoints | | | Add or import a Solana wallet | | | Find and explore Meteora DLMM pools | | | Choose LP Executor or Rebalancer Controller | | | Run, monitor, and manage LP strategies | | | Visualize LP position performance | New here? Run…

)} |\")\n\n # Volume & Fees by Time Window\n print(\"\\n### Volume & Fees by Time Window\\n\")\n print(\"| Window | Volume | Fees | Fee/TVL Ratio |\")\n print(\"|--------|--------|------|---------------|\")\n for window in [\"30m\", \"1h\", \"4h\", \"12h\", \"24h\"]:\n v = format_number(volume.get(window), prefix=\"$\")\n f = format_number(fees.get(window), prefix=\"$\")\n r = format_percent(fee_tvl.get(window))\n print(f\"| {window} | {v} | {f} | {r} |\")\n\n # Cumulative Metrics\n cumulative = pool.get(\"cumulative_metrics\", {})\n if cumulative:\n print(\"\\n### Cumulative Metrics (All Time)\\n\")\n print(\"| Metric | Value |\")\n print(\"|--------|-------|\")\n print(f\"| Total Volume | {format_number(cumulative.get('volume'), prefix='

lp-agent This skill helps you run automated liquidity provision strategies on concentrated liquidity (CLMM) DEXs using Hummingbot API. Commands (run as ): | Command | Description | |---------|-------------| | | Onboarding wizard — check setup status and get started | | | Deploy Hummingbot API trading infrastructure | | | Start Gateway, configure network RPC endpoints | | | Add or import a Solana wallet | | | Find and explore Meteora DLMM pools | | | Choose LP Executor or Rebalancer Controller | | | Run, monitor, and manage LP strategies | | | Visualize LP position performance | New here? Run…

)} |\")\n print(f\"| Total Trade Fees | {format_number(cumulative.get('trade_fee'), prefix='

lp-agent This skill helps you run automated liquidity provision strategies on concentrated liquidity (CLMM) DEXs using Hummingbot API. Commands (run as ): | Command | Description | |---------|-------------| | | Onboarding wizard — check setup status and get started | | | Deploy Hummingbot API trading infrastructure | | | Start Gateway, configure network RPC endpoints | | | Add or import a Solana wallet | | | Find and explore Meteora DLMM pools | | | Choose LP Executor or Rebalancer Controller | | | Run, monitor, and manage LP strategies | | | Visualize LP position performance | New here? Run…

)} |\")\n print(f\"| Total Protocol Fees | {format_number(cumulative.get('protocol_fee'), prefix='

lp-agent This skill helps you run automated liquidity provision strategies on concentrated liquidity (CLMM) DEXs using Hummingbot API. Commands (run as ): | Command | Description | |---------|-------------| | | Onboarding wizard — check setup status and get started | | | Deploy Hummingbot API trading infrastructure | | | Start Gateway, configure network RPC endpoints | | | Add or import a Solana wallet | | | Find and explore Meteora DLMM pools | | | Choose LP Executor or Rebalancer Controller | | | Run, monitor, and manage LP strategies | | | Visualize LP position performance | New here? Run…

)} |\")\n\n # Liquidity Distribution from Gateway\n if gateway and gateway.get(\"bins\"):\n # Show Gateway price (real-time) vs Meteora price\n gateway_price = gateway.get(\"price\")\n if gateway_price:\n print(\"\\n### Real-Time Price (from Gateway)\\n\")\n print(f\"**{format_price_subscript(gateway_price)} {token_y.get('symbol', '')}/{token_x.get('symbol', '')}**\")\n\n print(\"\\n### Liquidity Distribution\\n\")\n print(\"```\")\n chart = render_liquidity_chart(\n gateway.get(\"bins\", []),\n gateway.get(\"activeBinId\", 0),\n gateway.get(\"price\", pool.get(\"current_price\", 0)),\n base_symbol=token_x.get(\"symbol\", \"\"),\n quote_symbol=token_y.get(\"symbol\", \"\"),\n )\n print(chart)\n print(\"```\")\n\n # Gateway-specific info\n print(\"\\n### Active Bin Info\\n\")\n print(\"| Metric | Value |\")\n print(\"|--------|-------|\")\n print(f\"| Active Bin ID | {gateway.get('activeBinId', '—')} |\")\n print(f\"| Min Bin ID | {gateway.get('minBinId', '—')} |\")\n print(f\"| Max Bin ID | {gateway.get('maxBinId', '—')} |\")\n print(f\"| Dynamic Fee | {format_percent(gateway.get('dynamicFeePct'))} |\")\n else:\n print(\"\\n### Liquidity Distribution\\n\")\n if gateway_error:\n print(f\"*{gateway_error}*\")\n else:\n print(\"*Gateway not available - run with Gateway for bin distribution*\")\n\n\ndef print_json(pool: dict, gateway: dict = None):\n \"\"\"Print combined JSON output.\"\"\"\n output = {\n \"meteora\": pool,\n }\n if gateway:\n output[\"gateway\"] = gateway\n print(json.dumps(output, indent=2))\n\n\ndef main():\n parser = argparse.ArgumentParser(\n description=\"Get detailed Meteora DLMM pool information\",\n formatter_class=argparse.RawDescriptionHelpFormatter,\n epilog=\"\"\"\nExamples:\n %(prog)s BGm1tav58oGcsQJehL9WXBFXF7D27vZsKefj4xJKD5Y\n %(prog)s BGm1tav58oGcsQJehL9WXBFXF7D27vZsKefj4xJKD5Y --json\n\nEnvironment variables:\n GATEWAY_HOST Gateway host (default: localhost)\n GATEWAY_PORT Gateway port (default: 15888)\n \"\"\",\n )\n parser.add_argument(\n \"address\",\n help=\"Pool address to look up\",\n )\n parser.add_argument(\n \"--json\",\n action=\"store_true\",\n help=\"Output as JSON\",\n )\n parser.add_argument(\n \"--no-gateway\",\n action=\"store_true\",\n help=\"Skip Gateway API call (no liquidity distribution)\",\n )\n\n args = parser.parse_args()\n\n try:\n # Fetch from Meteora API\n pool = fetch_pool_meteora(args.address)\n\n # Fetch from Gateway API (optional)\n gateway = None\n gateway_error = None\n if not args.no_gateway:\n gateway, gateway_error = fetch_pool_gateway(args.address)\n\n if args.json:\n print_json(pool, gateway)\n else:\n print_summary_table(pool, gateway, gateway_error)\n\n except urllib.error.HTTPError as e:\n if e.code == 404:\n print(f\"Error: Pool not found: {args.address}\")\n else:\n print(f\"API error: {e.code} {e.reason}\")\n return 1\n except urllib.error.URLError as e:\n print(f\"Network error: {e.reason}\")\n return 1\n except json.JSONDecodeError as e:\n print(f\"Failed to parse API response: {e}\")\n return 1\n\n return 0\n\n\nif __name__ == \"__main__\":\n exit(main())\n","content_type":"text/x-python; charset=utf-8","language":"python","size":15442,"content_sha256":"730b7917681fe353f842ac03574a315ea50ce8d8ff29749f57aa47955f6be414"},{"filename":"scripts/list_meteora_pools.py","content":"#!/usr/bin/env python3\n\"\"\"\nList and search Meteora DLMM pools.\n\nUsage:\n python scripts/list_meteora_pools.py # Top 10 pools by 24h volume\n python scripts/list_meteora_pools.py --query SOL # Search by token/name/address\n python scripts/list_meteora_pools.py --query USDC --sort tvl # Sort by TVL\n python scripts/list_meteora_pools.py --limit 20 # Show more results\n\nExamples:\n python scripts/list_meteora_pools.py --query SOL-USDC\n python scripts/list_meteora_pools.py --query \"JUP\" --sort fees\n python scripts/list_meteora_pools.py --sort apr --limit 20\n\"\"\"\n\nimport argparse\nimport json\nimport urllib.request\nimport urllib.parse\n\nAPI_BASE = \"https://dlmm.datapi.meteora.ag\"\n\n# Default number of results\nDEFAULT_LIMIT = 10\n\n\ndef fetch_pools(\n query: str = None,\n sort_by: str = \"volume\",\n order: str = \"desc\",\n page: int = 1,\n limit: int = DEFAULT_LIMIT,\n) -> dict:\n \"\"\"Fetch pools from Meteora DLMM API.\"\"\"\n params = {\n \"page\": page,\n \"page_size\": limit,\n }\n\n if query:\n params[\"query\"] = query\n\n # Map friendly sort names to API field names\n sort_map = {\n \"volume\": \"volume_24h\",\n \"tvl\": \"tvl\",\n \"fees\": \"fee_24h\",\n \"apr\": \"apr_24h\",\n \"apy\": \"apy_24h\",\n }\n sort_field = sort_map.get(sort_by, \"volume_24h\")\n params[\"sort_by\"] = f\"{sort_field}:{order}\"\n\n url = f\"{API_BASE}/pools?{urllib.parse.urlencode(params)}\"\n\n req = urllib.request.Request(url, headers={\n \"Accept\": \"application/json\",\n \"User-Agent\": \"Mozilla/5.0 (compatible; hummingbot-skills/1.0)\",\n })\n with urllib.request.urlopen(req, timeout=30) as resp:\n return json.loads(resp.read().decode())\n\n\ndef format_number(value, decimals=2, prefix=\"\", suffix=\"\"):\n \"\"\"Format number with K/M/B suffixes.\"\"\"\n if value is None:\n return \"—\"\n try:\n num = float(value)\n if num >= 1_000_000_000:\n return f\"{prefix}{num/1_000_000_000:.{decimals}f}B{suffix}\"\n elif num >= 1_000_000:\n return f\"{prefix}{num/1_000_000:.{decimals}f}M{suffix}\"\n elif num >= 1_000:\n return f\"{prefix}{num/1_000:.{decimals}f}K{suffix}\"\n else:\n return f\"{prefix}{num:.{decimals}f}{suffix}\"\n except (ValueError, TypeError):\n return \"—\"\n\n\ndef format_percent(value):\n \"\"\"Format as percentage.\"\"\"\n if value is None:\n return \"—\"\n try:\n return f\"{float(value):.2f}%\"\n except (ValueError, TypeError):\n return \"—\"\n\n\ndef short_address(address: str, chars: int = 4) -> str:\n \"\"\"Shorten address to first and last N characters.\"\"\"\n if not address or len(address) \u003c= chars * 2 + 2:\n return address or \"—\"\n return f\"{address[:chars]}..{address[-chars:]}\"\n\n\ndef print_markdown_table(data: dict, sort_by: str):\n \"\"\"Print pools as a markdown table for AI assistants.\"\"\"\n pools = data.get(\"data\", [])\n total = data.get(\"total\", 0)\n page = data.get(\"current_page\", 1)\n pages = data.get(\"pages\", 1)\n\n if not pools:\n print(\"No pools found.\")\n return\n\n print(f\"Found {total} pools (showing page {page}/{pages}, sorted by {sort_by})\\n\")\n\n # Markdown table header\n print(\"| # | Pool | Pool Address | Base (mint) | Quote (mint) | TVL | Vol 24h | Fees 24h | APR | Fee | Bin |\")\n print(\"|---|------|--------------|-------------|--------------|-----|---------|----------|-----|-----|-----|\")\n\n for i, pool in enumerate(pools, 1):\n name = pool.get(\"name\", \"Unknown\")\n address = pool.get(\"address\", \"\")\n token_x = pool.get(\"token_x\", {})\n token_y = pool.get(\"token_y\", {})\n base_symbol = token_x.get(\"symbol\", \"?\")\n base_mint = short_address(token_x.get(\"address\", \"\"))\n quote_symbol = token_y.get(\"symbol\", \"?\")\n quote_mint = short_address(token_y.get(\"address\", \"\"))\n tvl = format_number(pool.get(\"tvl\"), prefix=\"$\")\n volume = format_number(pool.get(\"volume\", {}).get(\"24h\"), prefix=\"$\")\n fees = format_number(pool.get(\"fees\", {}).get(\"24h\"), prefix=\"$\")\n apr = format_percent(pool.get(\"apr\"))\n pool_config = pool.get(\"pool_config\", {})\n fee_tier = format_percent(pool_config.get(\"base_fee_pct\"))\n bin_step = pool_config.get(\"bin_step\", \"—\")\n\n print(f\"| {i} | {name} | `{short_address(address, 6)}` | {base_symbol} (`{base_mint}`) | {quote_symbol} (`{quote_mint}`) | {tvl} | {volume} | {fees} | {apr} | {fee_tier} | {bin_step} |\")\n\n print(f\"\\nUse `get_meteora_pool.py \u003cpool_address>` for detailed pool info including full token mints.\")\n\n\ndef print_json(data: dict):\n \"\"\"Print simplified JSON output for programmatic use.\"\"\"\n pools = data.get(\"data\", [])\n result = []\n\n for pool in pools:\n token_x = pool.get(\"token_x\", {})\n token_y = pool.get(\"token_y\", {})\n result.append({\n \"name\": pool.get(\"name\"),\n \"address\": pool.get(\"address\"),\n \"tvl\": pool.get(\"tvl\"),\n \"volume_24h\": pool.get(\"volume\", {}).get(\"24h\"),\n \"fees_24h\": pool.get(\"fees\", {}).get(\"24h\"),\n \"apr\": pool.get(\"apr\"),\n \"apy\": pool.get(\"apy\"),\n \"bin_step\": pool.get(\"pool_config\", {}).get(\"bin_step\"),\n \"base_fee_pct\": pool.get(\"pool_config\", {}).get(\"base_fee_pct\"),\n \"token_x_symbol\": token_x.get(\"symbol\"),\n \"token_x_mint\": token_x.get(\"address\"),\n \"token_y_symbol\": token_y.get(\"symbol\"),\n \"token_y_mint\": token_y.get(\"address\"),\n })\n\n output = {\n \"total\": data.get(\"total\"),\n \"page\": data.get(\"current_page\"),\n \"pages\": data.get(\"pages\"),\n \"pools\": result,\n }\n print(json.dumps(output, indent=2))\n\n\ndef main():\n parser = argparse.ArgumentParser(\n description=\"List and search Meteora DLMM pools\",\n formatter_class=argparse.RawDescriptionHelpFormatter,\n epilog=\"\"\"\nExamples:\n %(prog)s # Top 10 pools by 24h volume\n %(prog)s --query SOL # Search for SOL pools\n %(prog)s --query USDC --sort tvl # USDC pools sorted by TVL\n %(prog)s --sort apr --limit 20 # Top 20 by APR\n %(prog)s --query \u003caddress> # Search by pool address\n \"\"\",\n )\n parser.add_argument(\n \"-q\", \"--query\",\n help=\"Search by pool name, token symbol, or address\",\n )\n parser.add_argument(\n \"-s\", \"--sort\",\n default=\"volume\",\n choices=[\"volume\", \"tvl\", \"fees\", \"apr\", \"apy\"],\n help=\"Sort by metric (default: volume)\",\n )\n parser.add_argument(\n \"--order\",\n default=\"desc\",\n choices=[\"asc\", \"desc\"],\n help=\"Sort order (default: desc)\",\n )\n parser.add_argument(\n \"-n\", \"--limit\",\n type=int,\n default=DEFAULT_LIMIT,\n help=f\"Number of results (default: {DEFAULT_LIMIT}, max: 1000)\",\n )\n parser.add_argument(\n \"-p\", \"--page\",\n type=int,\n default=1,\n help=\"Page number (default: 1)\",\n )\n parser.add_argument(\n \"--json\",\n action=\"store_true\",\n help=\"Output as JSON\",\n )\n\n args = parser.parse_args()\n\n try:\n data = fetch_pools(\n query=args.query,\n sort_by=args.sort,\n order=args.order,\n page=args.page,\n limit=min(args.limit, 1000),\n )\n\n if args.json:\n print_json(data)\n else:\n print_markdown_table(data, sort_by=args.sort)\n\n except urllib.error.HTTPError as e:\n print(f\"API error: {e.code} {e.reason}\")\n return 1\n except urllib.error.URLError as e:\n print(f\"Network error: {e.reason}\")\n return 1\n except json.JSONDecodeError as e:\n print(f\"Failed to parse API response: {e}\")\n return 1\n\n return 0\n\n\nif __name__ == \"__main__\":\n exit(main())\n","content_type":"text/x-python; charset=utf-8","language":"python","size":7964,"content_sha256":"ef945823a955da58cab40d94fd7f48404fe2afc690de060f7c0e026d6e0efe50"},{"filename":"scripts/manage_controller.py","content":"#!/usr/bin/env python3\n\"\"\"\nManage LP Rebalancer controllers via hummingbot-api.\n\nUsage:\n # Get LP Rebalancer config template\n python manage_controller.py template\n\n # Create LP Rebalancer config\n python manage_controller.py create-config my_lp_config --pool \u003cpool_address> --pair SOL-USDC --amount 100\n\n # List all configs\n python manage_controller.py list-configs\n\n # Get config details\n python manage_controller.py describe-config my_lp_config\n\n # Deploy bot with controller\n python manage_controller.py deploy my_bot --configs my_lp_config\n\n # Get active bots status\n python manage_controller.py status\n\n # Stop a bot\n python manage_controller.py stop my_bot\n\nEnvironment:\n HUMMINGBOT_API_URL - API base URL (default: http://localhost:8000)\n API_USER - API username (default: admin)\n API_PASS - API password (default: admin)\n\"\"\"\n\nimport argparse\nimport json\nimport os\nimport sys\nimport urllib.request\nimport urllib.error\nimport base64\n\n\ndef load_env():\n \"\"\"Load environment from .env files.\"\"\"\n for path in [\"hummingbot-api/.env\", os.path.expanduser(\"~/.hummingbot/.env\"), \".env\"]:\n if os.path.exists(path):\n with open(path) as f:\n for line in f:\n line = line.strip()\n if line and not line.startswith(\"#\") and \"=\" in line:\n key, value = line.split(\"=\", 1)\n os.environ.setdefault(key.strip(), value.strip().strip('\"').strip(\"'\"))\n break\n\n\ndef get_api_config():\n \"\"\"Get API configuration from environment.\"\"\"\n load_env()\n return {\n \"url\": os.environ.get(\"HUMMINGBOT_API_URL\", \"http://localhost:8000\"),\n \"user\": os.environ.get(\"API_USER\", \"admin\"),\n \"password\": os.environ.get(\"API_PASS\", \"admin\"),\n }\n\n\ndef api_request(method: str, endpoint: str, data=None) -> dict:\n \"\"\"Make authenticated API request.\"\"\"\n config = get_api_config()\n url = f\"{config['url']}{endpoint}\"\n\n # Basic auth\n credentials = base64.b64encode(f\"{config['user']}:{config['password']}\".encode()).decode()\n headers = {\n \"Authorization\": f\"Basic {credentials}\",\n \"Content-Type\": \"application/json\",\n }\n\n body = json.dumps(data).encode() if data else None\n req = urllib.request.Request(url, data=body, headers=headers, method=method)\n\n try:\n with urllib.request.urlopen(req, timeout=30) as resp:\n return json.loads(resp.read().decode())\n except urllib.error.HTTPError as e:\n error_body = e.read().decode() if e.fp else \"\"\n print(f\"Error: HTTP {e.code} - {e.reason}\", file=sys.stderr)\n if error_body:\n try:\n print(json.dumps(json.loads(error_body), indent=2), file=sys.stderr)\n except json.JSONDecodeError:\n print(error_body, file=sys.stderr)\n sys.exit(1)\n except urllib.error.URLError as e:\n print(f\"Error: Cannot connect to API at {config['url']}: {e.reason}\", file=sys.stderr)\n sys.exit(1)\n\n\ndef get_template(args):\n \"\"\"Get LP Rebalancer config template.\"\"\"\n result = api_request(\"GET\", \"/controllers/generic/lp_rebalancer/config/template\")\n\n if args.json:\n print(json.dumps(result, indent=2))\n else:\n print(\"LP Rebalancer Config Template\")\n print(\"-\" * 50)\n for field, info in result.items():\n default = info.get(\"default\", \"\")\n field_type = info.get(\"type\", \"\")\n required = info.get(\"required\", False)\n req_str = \" (required)\" if required else \"\"\n print(f\" {field}: {default} [{field_type}]{req_str}\")\n\n\ndef create_config(args):\n \"\"\"Create LP Rebalancer controller config.\"\"\"\n config_data = {\n \"id\": args.config_name,\n \"controller_name\": \"lp_rebalancer\",\n \"controller_type\": \"generic\",\n \"connector_name\": args.connector,\n \"network\": args.network,\n \"trading_pair\": args.pair,\n \"pool_address\": args.pool,\n \"total_amount_quote\": str(args.amount),\n \"side\": args.side,\n \"position_width_pct\": str(args.width),\n \"position_offset_pct\": str(args.offset),\n \"rebalance_seconds\": args.rebalance_seconds,\n \"rebalance_threshold_pct\": str(args.rebalance_threshold),\n \"strategy_type\": args.strategy_type,\n }\n\n # Add price limits\n config_data[\"sell_price_max\"] = args.sell_max\n config_data[\"sell_price_min\"] = args.sell_min\n config_data[\"buy_price_max\"] = args.buy_max\n config_data[\"buy_price_min\"] = args.buy_min\n\n # POST /controllers/configs/{config_name} to create/update\n result = api_request(\"POST\", f\"/controllers/configs/{args.config_name}\", config_data)\n\n if args.json:\n print(json.dumps(result, indent=2))\n else:\n print(f\"✓ Config '{args.config_name}' created\")\n print(f\" Pool: {args.pool}\")\n print(f\" Pair: {args.pair}\")\n print(f\" Amount: {args.amount} (quote)\")\n\n\ndef list_configs(args):\n \"\"\"List all controller configs.\"\"\"\n result = api_request(\"GET\", \"/controllers/configs/\")\n\n if args.json:\n print(json.dumps(result, indent=2))\n else:\n if not result:\n print(\"No configs found.\")\n return\n\n print(f\"Controller Configs ({len(result)}):\")\n print(\"-\" * 80)\n print(f\"{'Name':\u003c25} {'Controller':\u003c20} {'Pair':\u003c15} {'Amount'}\")\n print(\"-\" * 80)\n\n for cfg in result:\n name = cfg.get(\"id\", \"\")[:23]\n controller = cfg.get(\"controller_name\", \"\")[:18]\n pair = cfg.get(\"trading_pair\", \"\")[:13]\n amount = cfg.get(\"total_amount_quote\", \"\")\n print(f\"{name:\u003c25} {controller:\u003c20} {pair:\u003c15} {amount}\")\n\n\ndef describe_config(args):\n \"\"\"Get details of a specific config.\"\"\"\n result = api_request(\"GET\", f\"/controllers/configs/{args.config_name}\")\n\n if args.json:\n print(json.dumps(result, indent=2))\n else:\n print(f\"Config: {args.config_name}\")\n print(\"-\" * 50)\n for key, value in result.items():\n if not key.startswith(\"_\"):\n print(f\" {key}: {value}\")\n\n\ndef delete_config(args):\n \"\"\"Delete a controller config.\"\"\"\n result = api_request(\"DELETE\", f\"/controllers/configs/{args.config_name}\")\n\n if args.json:\n print(json.dumps(result, indent=2))\n else:\n print(f\"✓ Config '{args.config_name}' deleted\")\n\n\ndef deploy_bot(args):\n \"\"\"Deploy bot with controller configs using V2 deployment.\"\"\"\n data = {\n \"instance_name\": args.bot_name,\n \"controllers_config\": args.configs,\n \"credentials_profile\": args.account,\n \"image\": args.image,\n \"headless\": args.headless,\n }\n\n if args.max_global_drawdown:\n data[\"max_global_drawdown_quote\"] = args.max_global_drawdown\n if args.max_controller_drawdown:\n data[\"max_controller_drawdown_quote\"] = args.max_controller_drawdown\n if args.script_config:\n data[\"script_config\"] = args.script_config\n\n result = api_request(\"POST\", \"/bot-orchestration/deploy-v2-controllers\", data)\n\n if args.json:\n print(json.dumps(result, indent=2))\n else:\n if result.get(\"success\"):\n print(f\"✓ Bot deployed\")\n print(f\" Name: {result.get('unique_instance_name', args.bot_name)}\")\n print(f\" Controllers: {', '.join(args.configs)}\")\n print(f\" Script Config: {result.get('script_config_generated', '')}\")\n else:\n print(f\"Deployment response: {result}\")\n\n\ndef get_status(args):\n \"\"\"Get active bots status.\"\"\"\n result = api_request(\"GET\", \"/bot-orchestration/status\")\n\n if args.json:\n print(json.dumps(result, indent=2))\n else:\n data = result.get(\"data\", result)\n if not data:\n print(\"No active bots.\")\n return\n\n print(\"Active Bots:\")\n print(\"-\" * 60)\n\n for bot_name, bot_data in data.items():\n status = bot_data.get(\"status\", \"unknown\")\n print(f\"\\n{bot_name} ({status})\")\n\n # Performance data if available\n perf = bot_data.get(\"performance\", {})\n if perf:\n pnl = perf.get(\"unrealized_pnl_quote\", 0)\n rpnl = perf.get(\"realized_pnl_quote\", 0)\n volume = perf.get(\"volume_traded\", 0)\n print(f\" Unrealized PnL: ${pnl:.2f}\")\n print(f\" Realized PnL: ${rpnl:.2f}\")\n print(f\" Volume: ${volume:.2f}\")\n\n\ndef get_bot_status(args):\n \"\"\"Get specific bot status.\"\"\"\n result = api_request(\"GET\", f\"/bot-orchestration/{args.bot_name}/status\")\n\n if args.json:\n print(json.dumps(result, indent=2))\n else:\n data = result.get(\"data\", result)\n print(f\"Bot: {args.bot_name}\")\n print(\"-\" * 50)\n print(f\" Status: {data.get('status', 'unknown')}\")\n\n perf = data.get(\"performance\", {})\n if perf:\n print(f\" Unrealized PnL: ${perf.get('unrealized_pnl_quote', 0):.2f}\")\n print(f\" Realized PnL: ${perf.get('realized_pnl_quote', 0):.2f}\")\n print(f\" Volume: ${perf.get('volume_traded', 0):.2f}\")\n\n\ndef stop_bot(args):\n \"\"\"Stop a bot.\"\"\"\n data = {\n \"bot_name\": args.bot_name,\n \"skip_order_cancellation\": args.skip_cancel,\n \"async_backend\": True,\n }\n result = api_request(\"POST\", \"/bot-orchestration/stop-bot\", data)\n\n if args.json:\n print(json.dumps(result, indent=2))\n else:\n response = result.get(\"response\", result)\n if response.get(\"success\"):\n print(f\"✓ Bot '{args.bot_name}' stopped\")\n else:\n print(f\"Response: {response}\")\n\n\ndef stop_and_archive(args):\n \"\"\"Stop and archive a bot.\"\"\"\n endpoint = f\"/bot-orchestration/stop-and-archive-bot/{args.bot_name}\"\n params = []\n if args.skip_cancel:\n params.append(\"skip_order_cancellation=true\")\n if args.s3_bucket:\n params.append(f\"s3_bucket={args.s3_bucket}\")\n params.append(\"archive_locally=false\")\n else:\n params.append(\"archive_locally=true\")\n\n if params:\n endpoint += \"?\" + \"&\".join(params)\n\n result = api_request(\"POST\", endpoint)\n\n if args.json:\n print(json.dumps(result, indent=2))\n else:\n if result.get(\"status\") == \"success\":\n print(f\"✓ Stop and archive initiated for '{args.bot_name}'\")\n print(f\" {result.get('message', '')}\")\n else:\n print(f\"Response: {result}\")\n\n\ndef main():\n parser = argparse.ArgumentParser(description=\"Manage LP Rebalancer controllers via hummingbot-api\")\n subparsers = parser.add_subparsers(dest=\"command\", required=True)\n\n # template command\n template_parser = subparsers.add_parser(\"template\", help=\"Get LP Rebalancer config template\")\n template_parser.add_argument(\"--json\", action=\"store_true\", help=\"Output as JSON\")\n template_parser.set_defaults(func=get_template)\n\n # create-config command\n create_parser = subparsers.add_parser(\"create-config\", help=\"Create LP Rebalancer config\")\n create_parser.add_argument(\"config_name\", help=\"Config name\")\n create_parser.add_argument(\"--pool\", required=True, help=\"Pool address (from Meteora UI or list_meteora_pools.py)\")\n create_parser.add_argument(\"--pair\", required=True, help=\"Trading pair matching pool tokens (e.g., SOL-USDC, Percolator-SOL). Use exact token symbols from Gateway.\")\n create_parser.add_argument(\"--connector\", default=\"meteora/clmm\", help=\"Connector name\")\n create_parser.add_argument(\"--network\", default=\"solana-mainnet-beta\", help=\"Network\")\n create_parser.add_argument(\"--amount\", type=float, required=True, help=\"Total amount in QUOTE asset (2nd token in pair). E.g. for Percolator-SOL this is SOL. For SOL-USDC this is USDC.\")\n create_parser.add_argument(\"--side\", type=int, default=0, choices=[0, 1, 2], help=\"Side: 0=BOTH, 1=BUY (quote only), 2=SELL (base only)\")\n create_parser.add_argument(\"--width\", type=float, default=10.0, help=\"Position width in pct (e.g. 10 = 10%% of current price above and below)\")\n create_parser.add_argument(\"--offset\", type=float, default=0.1, help=\"Position offset in pct — how far center of range is from current price (default: 0.1%%)\")\n create_parser.add_argument(\"--rebalance-seconds\", type=int, default=300, help=\"Seconds out-of-range before rebalancing (default: 300)\")\n create_parser.add_argument(\"--rebalance-threshold\", type=float, default=1.0, help=\"Rebalance threshold in pct — minimum price movement to trigger rebalance (default: 1.0)\")\n create_parser.add_argument(\"--sell-max\", type=float, default=None, help=\"Max price for SELL orders (default: null = no limit)\")\n create_parser.add_argument(\"--sell-min\", type=float, default=None, help=\"Min price for SELL orders (default: null = no limit)\")\n create_parser.add_argument(\"--buy-max\", type=float, default=None, help=\"Max price for BUY orders (default: null = no limit)\")\n create_parser.add_argument(\"--buy-min\", type=float, default=None, help=\"Min price for BUY orders (default: null = no limit)\")\n create_parser.add_argument(\"--strategy-type\", type=int, default=0, choices=[0, 1, 2], help=\"Meteora liquidity shape: 0=Spot (uniform), 1=Curve (concentrated center), 2=Bid-Ask (edges)\")\n create_parser.add_argument(\"--json\", action=\"store_true\", help=\"Output as JSON\")\n create_parser.set_defaults(func=create_config)\n\n # list-configs command\n list_configs_parser = subparsers.add_parser(\"list-configs\", help=\"List controller configs\")\n list_configs_parser.add_argument(\"--json\", action=\"store_true\", help=\"Output as JSON\")\n list_configs_parser.set_defaults(func=list_configs)\n\n # describe-config command\n describe_parser = subparsers.add_parser(\"describe-config\", help=\"Get config details\")\n describe_parser.add_argument(\"config_name\", help=\"Config name\")\n describe_parser.add_argument(\"--json\", action=\"store_true\", help=\"Output as JSON\")\n describe_parser.set_defaults(func=describe_config)\n\n # delete-config command\n delete_parser = subparsers.add_parser(\"delete-config\", help=\"Delete a config\")\n delete_parser.add_argument(\"config_name\", help=\"Config name\")\n delete_parser.add_argument(\"--json\", action=\"store_true\", help=\"Output as JSON\")\n delete_parser.set_defaults(func=delete_config)\n\n # deploy command\n deploy_parser = subparsers.add_parser(\"deploy\", help=\"Deploy bot with controllers\")\n deploy_parser.add_argument(\"bot_name\", help=\"Bot name\")\n deploy_parser.add_argument(\"--configs\", nargs=\"+\", required=True, help=\"Controller config names\")\n deploy_parser.add_argument(\"--account\", default=\"master_account\", help=\"Account name (default: master_account)\")\n deploy_parser.add_argument(\"--image\", default=\"hummingbot/hummingbot:development\", help=\"Docker image\")\n deploy_parser.add_argument(\"--max-global-drawdown\", type=float, help=\"Max global drawdown in quote\")\n deploy_parser.add_argument(\"--max-controller-drawdown\", type=float, help=\"Max controller drawdown in quote\")\n deploy_parser.add_argument(\"--script-config\", help=\"Script config name (auto-generated if not provided)\")\n deploy_parser.add_argument(\"--headless\", action=\"store_true\", default=False, help=\"Run in headless mode\")\n deploy_parser.add_argument(\"--json\", action=\"store_true\", help=\"Output as JSON\")\n deploy_parser.set_defaults(func=deploy_bot)\n\n # status command\n status_parser = subparsers.add_parser(\"status\", help=\"Get active bots status\")\n status_parser.add_argument(\"--json\", action=\"store_true\", help=\"Output as JSON\")\n status_parser.set_defaults(func=get_status)\n\n # bot-status command\n bot_status_parser = subparsers.add_parser(\"bot-status\", help=\"Get specific bot status\")\n bot_status_parser.add_argument(\"bot_name\", help=\"Bot name\")\n bot_status_parser.add_argument(\"--json\", action=\"store_true\", help=\"Output as JSON\")\n bot_status_parser.set_defaults(func=get_bot_status)\n\n # stop command\n stop_parser = subparsers.add_parser(\"stop\", help=\"Stop a bot\")\n stop_parser.add_argument(\"bot_name\", help=\"Bot name\")\n stop_parser.add_argument(\"--skip-cancel\", action=\"store_true\", help=\"Skip order cancellation\")\n stop_parser.add_argument(\"--json\", action=\"store_true\", help=\"Output as JSON\")\n stop_parser.set_defaults(func=stop_bot)\n\n # stop-and-archive command\n archive_parser = subparsers.add_parser(\"stop-and-archive\", help=\"Stop and archive a bot\")\n archive_parser.add_argument(\"bot_name\", help=\"Bot name\")\n archive_parser.add_argument(\"--skip-cancel\", action=\"store_true\", help=\"Skip order cancellation\")\n archive_parser.add_argument(\"--s3-bucket\", help=\"S3 bucket for archiving (default: local archive)\")\n archive_parser.add_argument(\"--json\", action=\"store_true\", help=\"Output as JSON\")\n archive_parser.set_defaults(func=stop_and_archive)\n\n args = parser.parse_args()\n args.func(args)\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":16998,"content_sha256":"ab9ef2b87b1f39f2794a8aeaf64fbb7d262ffce351237bdf3e5d18d22d45098d"},{"filename":"scripts/manage_executor.py","content":"#!/usr/bin/env python3\n\"\"\"\nManage LP executors via hummingbot-api.\n\nUsage:\n # Create LP executor\n python manage_executor.py create --pool \u003cpool_address> --pair SOL-USDC --quote-amount 100 --lower 180 --upper 185\n\n # Get executor status\n python manage_executor.py get \u003cexecutor_id>\n\n # List all executors\n python manage_executor.py list [--type lp_executor]\n\n # Get executor logs\n python manage_executor.py logs \u003cexecutor_id> [--limit 50]\n\n # Stop executor\n python manage_executor.py stop \u003cexecutor_id> [--keep-position]\n\n # Get executor summary\n python manage_executor.py summary\n\n # Get held positions summary\n python manage_executor.py positions\n\n # Get executor config schema\n python manage_executor.py config lp_executor\n\nEnvironment:\n HUMMINGBOT_API_URL - API base URL (default: http://localhost:8000)\n API_USER - API username (default: admin)\n API_PASS - API password (default: admin)\n\"\"\"\n\nimport argparse\nimport json\nimport os\nimport sys\nimport urllib.request\nimport urllib.error\nimport base64\nfrom datetime import datetime\n\n\ndef load_env():\n \"\"\"Load environment from .env files.\"\"\"\n for path in [\"hummingbot-api/.env\", os.path.expanduser(\"~/.hummingbot/.env\"), \".env\"]:\n if os.path.exists(path):\n with open(path) as f:\n for line in f:\n line = line.strip()\n if line and not line.startswith(\"#\") and \"=\" in line:\n key, value = line.split(\"=\", 1)\n os.environ.setdefault(key.strip(), value.strip().strip('\"').strip(\"'\"))\n break\n\n\ndef get_api_config():\n \"\"\"Get API configuration from environment.\"\"\"\n load_env()\n return {\n \"url\": os.environ.get(\"HUMMINGBOT_API_URL\", \"http://localhost:8000\"),\n \"user\": os.environ.get(\"API_USER\", \"admin\"),\n \"password\": os.environ.get(\"API_PASS\", \"admin\"),\n }\n\n\ndef api_request(method: str, endpoint: str, data=None) -> dict:\n \"\"\"Make authenticated API request.\"\"\"\n config = get_api_config()\n url = f\"{config['url']}{endpoint}\"\n\n # Basic auth\n credentials = base64.b64encode(f\"{config['user']}:{config['password']}\".encode()).decode()\n headers = {\n \"Authorization\": f\"Basic {credentials}\",\n \"Content-Type\": \"application/json\",\n }\n\n body = json.dumps(data).encode() if data else None\n req = urllib.request.Request(url, data=body, headers=headers, method=method)\n\n try:\n with urllib.request.urlopen(req, timeout=30) as resp:\n return json.loads(resp.read().decode())\n except urllib.error.HTTPError as e:\n error_body = e.read().decode() if e.fp else \"\"\n print(f\"Error: HTTP {e.code} - {e.reason}\", file=sys.stderr)\n if error_body:\n try:\n print(json.dumps(json.loads(error_body), indent=2), file=sys.stderr)\n except json.JSONDecodeError:\n print(error_body, file=sys.stderr)\n sys.exit(1)\n except urllib.error.URLError as e:\n print(f\"Error: Cannot connect to API at {config['url']}: {e.reason}\", file=sys.stderr)\n sys.exit(1)\n\n\ndef create_executor(args):\n \"\"\"Create a new LP executor.\"\"\"\n # LP executor uses 'market' object for connector/pair\n executor_config = {\n \"type\": \"lp_executor\",\n \"market\": {\n \"connector_name\": args.connector,\n \"trading_pair\": args.pair,\n },\n \"pool_address\": args.pool,\n \"lower_price\": args.lower,\n \"upper_price\": args.upper,\n \"base_amount\": args.base_amount,\n \"quote_amount\": args.quote_amount,\n \"side\": args.side,\n }\n\n # Add optional parameters\n if args.auto_close_above is not None:\n executor_config[\"auto_close_above_range_seconds\"] = args.auto_close_above\n if args.auto_close_below is not None:\n executor_config[\"auto_close_below_range_seconds\"] = args.auto_close_below\n if args.strategy_type is not None:\n executor_config[\"extra_params\"] = {\"strategyType\": args.strategy_type}\n\n request_data = {\n \"executor_config\": executor_config,\n \"account_name\": args.account,\n }\n\n result = api_request(\"POST\", \"/executors/\", request_data)\n print(json.dumps(result, indent=2))\n\n\ndef get_executor(args):\n \"\"\"Get executor status.\"\"\"\n result = api_request(\"GET\", f\"/executors/{args.executor_id}\")\n\n if args.json:\n print(json.dumps(result, indent=2))\n else:\n print(f\"Executor: {result.get('executor_id', args.executor_id)}\")\n print(\"-\" * 50)\n print(f\" Type: {result.get('executor_type', result.get('type', ''))}\")\n print(f\" Status: {result.get('status', '')}\")\n print(f\" Trading Pair: {result.get('trading_pair', '')}\")\n print(f\" Connector: {result.get('connector_name', '')}\")\n\n custom_info = result.get(\"custom_info\", {})\n if custom_info:\n state = custom_info.get(\"state\", \"\")\n if state:\n print(f\" State: {state}\")\n position_address = custom_info.get(\"position_address\", \"\")\n if position_address:\n print(f\" Position: {position_address[:20]}...\")\n\n pnl = result.get(\"net_pnl_quote\", result.get(\"pnl\", 0))\n print(f\" PnL: ${pnl:.4f}\" if pnl else \" PnL: $0.00\")\n\n\ndef list_executors(args):\n \"\"\"List all executors.\"\"\"\n # Use POST /executors/search with filter\n filter_request = {\n \"limit\": args.limit,\n }\n\n if args.type:\n filter_request[\"executor_types\"] = [args.type]\n if args.status:\n filter_request[\"status\"] = args.status\n\n result = api_request(\"POST\", \"/executors/search\", filter_request)\n\n if args.json:\n print(json.dumps(result, indent=2))\n else:\n executors = result.get(\"data\", [])\n pagination = result.get(\"pagination\", {})\n\n if not executors:\n print(\"No executors found.\")\n return\n\n print(f\"Executors ({pagination.get('total_count', len(executors))} total):\")\n print(\"-\" * 110)\n print(f\"{'ID':\u003c46} {'Type':\u003c15} {'Status':\u003c12} {'Pair':\u003c15} {'PnL':\u003c10}\")\n print(\"-\" * 110)\n\n for ex in executors:\n ex_id = ex.get(\"executor_id\", \"\")\n ex_type = ex.get(\"executor_type\", ex.get(\"type\", \"\"))[:13]\n status = ex.get(\"status\", \"\")[:10]\n pair = ex.get(\"trading_pair\", \"\")[:13]\n pnl = ex.get(\"net_pnl_quote\", ex.get(\"pnl\", 0))\n pnl_str = f\"${pnl:.2f}\" if pnl else \"$0.00\"\n print(f\"{ex_id:\u003c46} {ex_type:\u003c15} {status:\u003c12} {pair:\u003c15} {pnl_str:\u003c10}\")\n\n\ndef get_logs(args):\n \"\"\"Get executor logs.\"\"\"\n params = [f\"limit={args.limit}\"]\n if args.level:\n params.append(f\"level={args.level}\")\n\n endpoint = f\"/executors/{args.executor_id}/logs?{'&'.join(params)}\"\n result = api_request(\"GET\", endpoint)\n\n if args.json:\n print(json.dumps(result, indent=2))\n else:\n logs = result.get(\"logs\", [])\n total = result.get(\"total_count\", len(logs))\n print(f\"Logs for {args.executor_id} ({total} total, showing {len(logs)}):\")\n print(\"-\" * 80)\n\n for log in logs:\n ts = log.get(\"timestamp\", \"\")\n level = log.get(\"level\", \"INFO\")\n msg = log.get(\"message\", str(log))\n print(f\"[{ts}] {level}: {msg}\")\n\n\ndef stop_executor(args):\n \"\"\"Stop an executor.\"\"\"\n data = {\"keep_position\": args.keep_position}\n result = api_request(\"POST\", f\"/executors/{args.executor_id}/stop\", data)\n\n if args.json:\n print(json.dumps(result, indent=2))\n else:\n if result.get(\"success\"):\n print(f\"✓ Executor {args.executor_id} stopped\")\n if args.keep_position:\n print(\" Position kept on-chain\")\n else:\n print(\" Position closed\")\n else:\n print(f\"Response: {result}\")\n\n\ndef get_summary(args):\n \"\"\"Get summary of all executors.\"\"\"\n result = api_request(\"GET\", \"/executors/summary\")\n\n if args.json:\n print(json.dumps(result, indent=2))\n else:\n print(\"Executor Summary\")\n print(\"-\" * 40)\n print(f\" Active: {result.get('total_active', 0)}\")\n print(f\" Total PnL: ${result.get('total_pnl_quote', 0):.2f}\")\n print(f\" Total Volume: ${result.get('total_volume_quote', 0):.2f}\")\n\n by_type = result.get(\"by_type\", {})\n if by_type:\n print(\"\\n By Type:\")\n for t, count in by_type.items():\n print(f\" {t}: {count}\")\n\n by_status = result.get(\"by_status\", {})\n if by_status:\n print(\"\\n By Status:\")\n for s, count in by_status.items():\n print(f\" {s}: {count}\")\n\n\ndef get_positions_summary(args):\n \"\"\"Get summary of held positions from executors stopped with keep_position=True.\"\"\"\n result = api_request(\"GET\", \"/executors/positions/summary\")\n\n if args.json:\n print(json.dumps(result, indent=2))\n else:\n print(\"Positions Summary\")\n print(\"-\" * 60)\n print(f\" Total Positions: {result.get('total_positions', 0)}\")\n print(f\" Total Realized PnL: ${result.get('total_realized_pnl', 0):.4f}\")\n\n unrealized = result.get('total_unrealized_pnl')\n if unrealized is not None:\n print(f\" Total Unrealized PnL: ${unrealized:.4f}\")\n\n positions = result.get(\"positions\", [])\n if positions:\n print(\"\\n Positions:\")\n print(\"-\" * 60)\n for pos in positions:\n pair = pos.get(\"trading_pair\", \"\")\n connector = pos.get(\"connector_name\", \"\")\n net_base = pos.get(\"net_amount_base\", 0)\n realized = pos.get(\"realized_pnl_quote\", 0)\n unrealized = pos.get(\"unrealized_pnl_quote\")\n side = pos.get(\"position_side\", \"\")\n\n print(f\" {pair} ({connector})\")\n print(f\" Side: {side}, Net Base: {net_base:.6f}\")\n print(f\" Realized PnL: ${realized:.4f}\", end=\"\")\n if unrealized is not None:\n print(f\", Unrealized: ${unrealized:.4f}\")\n else:\n print()\n\n\ndef get_config_schema(args):\n \"\"\"Get configuration schema for an executor type.\"\"\"\n result = api_request(\"GET\", f\"/executors/types/{args.executor_type}/config\")\n\n if args.json:\n print(json.dumps(result, indent=2))\n else:\n print(f\"Config Schema: {result.get('executor_type', args.executor_type)}\")\n print(f\"Config Class: {result.get('config_class', '')}\")\n print(\"-\" * 60)\n\n fields = result.get(\"fields\", [])\n if fields:\n print(\"\\nFields:\")\n for field in fields:\n name = field.get(\"name\", \"\")\n ftype = field.get(\"type\", \"\")\n required = \"required\" if field.get(\"required\") else \"optional\"\n default = field.get(\"default\")\n desc = field.get(\"description\", \"\")\n\n print(f\" {name} ({ftype}, {required})\")\n if default is not None:\n print(f\" Default: {default}\")\n if desc:\n print(f\" {desc[:60]}...\")\n\n nested = result.get(\"nested_types\", {})\n if nested and not args.brief:\n print(\"\\nNested Types:\")\n for type_name, type_info in nested.items():\n print(f\" {type_name}: {type_info.get('description', '')[:50]}\")\n\n\ndef main():\n parser = argparse.ArgumentParser(description=\"Manage LP executors via hummingbot-api\")\n subparsers = parser.add_subparsers(dest=\"command\", required=True)\n\n # create command\n create_parser = subparsers.add_parser(\"create\", help=\"Create LP executor\")\n create_parser.add_argument(\"--pool\", required=True, help=\"Pool address\")\n create_parser.add_argument(\"--pair\", required=True, help=\"Trading pair (e.g., SOL-USDC)\")\n create_parser.add_argument(\"--connector\", default=\"meteora/clmm\", help=\"Connector name (default: meteora/clmm)\")\n create_parser.add_argument(\"--lower\", type=float, required=True, help=\"Lower price bound\")\n create_parser.add_argument(\"--upper\", type=float, required=True, help=\"Upper price bound\")\n create_parser.add_argument(\"--base-amount\", type=float, default=0, help=\"Base token amount (default: 0)\")\n create_parser.add_argument(\"--quote-amount\", type=float, default=0, help=\"Quote token amount\")\n create_parser.add_argument(\"--side\", type=int, default=1, choices=[0, 1, 2], help=\"Side: 0=BOTH, 1=BUY, 2=SELL (default: 1)\")\n create_parser.add_argument(\"--auto-close-above\", type=int, help=\"Auto-close seconds when price above range\")\n create_parser.add_argument(\"--auto-close-below\", type=int, help=\"Auto-close seconds when price below range\")\n create_parser.add_argument(\"--strategy-type\", type=int, choices=[0, 1, 2], help=\"Meteora strategy: 0=Spot, 1=Curve, 2=Bid-Ask\")\n create_parser.add_argument(\"--account\", default=\"master_account\", help=\"Account name (default: master_account)\")\n create_parser.set_defaults(func=create_executor)\n\n # get command\n get_parser = subparsers.add_parser(\"get\", help=\"Get executor status\")\n get_parser.add_argument(\"executor_id\", help=\"Executor ID\")\n get_parser.add_argument(\"--json\", action=\"store_true\", help=\"Output as JSON\")\n get_parser.set_defaults(func=get_executor)\n\n # list command\n list_parser = subparsers.add_parser(\"list\", help=\"List executors\")\n list_parser.add_argument(\"--type\", help=\"Filter by executor type (e.g., lp_executor)\")\n list_parser.add_argument(\"--status\", help=\"Filter by status (e.g., RUNNING, TERMINATED)\")\n list_parser.add_argument(\"--limit\", type=int, default=50, help=\"Max results (default: 50)\")\n list_parser.add_argument(\"--json\", action=\"store_true\", help=\"Output as JSON\")\n list_parser.set_defaults(func=list_executors)\n\n # logs command\n logs_parser = subparsers.add_parser(\"logs\", help=\"Get executor logs\")\n logs_parser.add_argument(\"executor_id\", help=\"Executor ID\")\n logs_parser.add_argument(\"--limit\", type=int, default=50, help=\"Number of log entries (default: 50)\")\n logs_parser.add_argument(\"--level\", choices=[\"ERROR\", \"WARNING\", \"INFO\", \"DEBUG\"], help=\"Filter by log level\")\n logs_parser.add_argument(\"--json\", action=\"store_true\", help=\"Output as JSON\")\n logs_parser.set_defaults(func=get_logs)\n\n # stop command\n stop_parser = subparsers.add_parser(\"stop\", help=\"Stop executor\")\n stop_parser.add_argument(\"executor_id\", help=\"Executor ID\")\n stop_parser.add_argument(\"--keep-position\", action=\"store_true\", help=\"Keep position on-chain (don't close)\")\n stop_parser.add_argument(\"--json\", action=\"store_true\", help=\"Output as JSON\")\n stop_parser.set_defaults(func=stop_executor)\n\n # summary command\n summary_parser = subparsers.add_parser(\"summary\", help=\"Get executor summary\")\n summary_parser.add_argument(\"--json\", action=\"store_true\", help=\"Output as JSON\")\n summary_parser.set_defaults(func=get_summary)\n\n # positions command\n positions_parser = subparsers.add_parser(\"positions\", help=\"Get held positions summary\")\n positions_parser.add_argument(\"--json\", action=\"store_true\", help=\"Output as JSON\")\n positions_parser.set_defaults(func=get_positions_summary)\n\n # config command\n config_parser = subparsers.add_parser(\"config\", help=\"Get executor config schema\")\n config_parser.add_argument(\"executor_type\", help=\"Executor type: position_executor, grid_executor, dca_executor, arbitrage_executor, twap_executor, xemm_executor, order_executor\")\n config_parser.add_argument(\"--json\", action=\"store_true\", help=\"Output as JSON\")\n config_parser.add_argument(\"--brief\", action=\"store_true\", help=\"Show brief output (skip nested types)\")\n config_parser.set_defaults(func=get_config_schema)\n\n args = parser.parse_args()\n args.func(args)\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":15986,"content_sha256":"96be865c844b8dd3ab776e7a7515f3eb446377098c347f22f3435f879a1b899a"},{"filename":"scripts/manage_gateway.py","content":"#!/usr/bin/env python3\n\"\"\"\nManage Gateway via hummingbot-api.\n\nUsage:\n # Check Gateway status\n python manage_gateway.py status\n\n # Start/stop/restart Gateway\n python manage_gateway.py start --passphrase mypassword\n python manage_gateway.py stop\n python manage_gateway.py restart\n\n # Get Gateway logs\n python manage_gateway.py logs [--limit 100]\n\n # List all networks\n python manage_gateway.py networks\n\n # Get network config\n python manage_gateway.py network solana-mainnet-beta\n\n # Set custom RPC node (avoid rate limits)\n python manage_gateway.py network solana-mainnet-beta --node-url https://my-rpc.example.com\n\nEnvironment:\n HUMMINGBOT_API_URL - API base URL (default: http://localhost:8000)\n API_USER - API username (default: admin)\n API_PASS - API password (default: admin)\n\"\"\"\n\nimport argparse\nimport json\nimport os\nimport sys\nimport urllib.request\nimport urllib.error\nimport base64\n\n\ndef load_env():\n \"\"\"Load environment from .env files.\"\"\"\n for path in [\"hummingbot-api/.env\", os.path.expanduser(\"~/.hummingbot/.env\"), \".env\"]:\n if os.path.exists(path):\n with open(path) as f:\n for line in f:\n line = line.strip()\n if line and not line.startswith(\"#\") and \"=\" in line:\n key, value = line.split(\"=\", 1)\n os.environ.setdefault(key.strip(), value.strip().strip('\"').strip(\"'\"))\n break\n\n\ndef get_api_config():\n \"\"\"Get API configuration from environment.\"\"\"\n load_env()\n return {\n \"url\": os.environ.get(\"HUMMINGBOT_API_URL\", \"http://localhost:8000\"),\n \"user\": os.environ.get(\"API_USER\", \"admin\"),\n \"password\": os.environ.get(\"API_PASS\", \"admin\"),\n }\n\n\ndef api_request(method: str, endpoint: str, data=None) -> dict:\n \"\"\"Make authenticated API request.\"\"\"\n config = get_api_config()\n url = f\"{config['url']}{endpoint}\"\n\n # Basic auth\n credentials = base64.b64encode(f\"{config['user']}:{config['password']}\".encode()).decode()\n headers = {\n \"Authorization\": f\"Basic {credentials}\",\n \"Content-Type\": \"application/json\",\n }\n\n body = json.dumps(data).encode() if data else None\n req = urllib.request.Request(url, data=body, headers=headers, method=method)\n\n try:\n with urllib.request.urlopen(req, timeout=60) as resp:\n return json.loads(resp.read().decode())\n except urllib.error.HTTPError as e:\n error_body = e.read().decode() if e.fp else \"\"\n print(f\"Error: HTTP {e.code} - {e.reason}\", file=sys.stderr)\n if error_body:\n try:\n print(json.dumps(json.loads(error_body), indent=2), file=sys.stderr)\n except json.JSONDecodeError:\n print(error_body, file=sys.stderr)\n sys.exit(1)\n except urllib.error.URLError as e:\n print(f\"Error: Cannot connect to API at {config['url']}: {e.reason}\", file=sys.stderr)\n sys.exit(1)\n\n\ndef get_status(args):\n \"\"\"Get Gateway status.\"\"\"\n result = api_request(\"GET\", \"/gateway/status\")\n\n if args.json:\n print(json.dumps(result, indent=2))\n else:\n is_running = result.get(\"running\", False)\n container_id = result.get(\"container_id\", \"\")\n image = result.get(\"image\", \"\")\n port = result.get(\"port\", \"\")\n\n if is_running:\n print(f\"✓ Gateway is running\")\n if container_id:\n print(f\" Container: {container_id[:12]}\")\n if image:\n print(f\" Image: {image}\")\n if port:\n print(f\" Port: {port}\")\n else:\n print(f\"✗ Gateway is not running\")\n\n\ndef start_gateway(args):\n \"\"\"Start Gateway.\"\"\"\n # GatewayConfig requires passphrase\n data = {\n \"passphrase\": args.passphrase,\n \"image\": args.image,\n \"port\": args.port,\n \"dev_mode\": True,\n }\n\n print(\"Starting Gateway...\")\n result = api_request(\"POST\", \"/gateway/start\", data)\n\n if args.json:\n print(json.dumps(result, indent=2))\n else:\n if result.get(\"success\"):\n print(\"✓ Gateway started successfully\")\n else:\n print(f\"Response: {result.get('message', result)}\")\n\n\ndef stop_gateway(args):\n \"\"\"Stop Gateway.\"\"\"\n print(\"Stopping Gateway...\")\n result = api_request(\"POST\", \"/gateway/stop\")\n\n if args.json:\n print(json.dumps(result, indent=2))\n else:\n if result.get(\"success\"):\n print(\"✓ Gateway stopped\")\n else:\n print(f\"Response: {result.get('message', result)}\")\n\n\ndef restart_gateway(args):\n \"\"\"Restart Gateway.\"\"\"\n print(\"Restarting Gateway...\")\n # Restart can optionally take config\n data = None\n if args.passphrase:\n data = {\n \"passphrase\": args.passphrase,\n \"image\": args.image or \"hummingbot/gateway:latest\",\n \"port\": args.port or 15888,\n \"dev_mode\": True,\n }\n\n result = api_request(\"POST\", \"/gateway/restart\", data)\n\n if args.json:\n print(json.dumps(result, indent=2))\n else:\n if result.get(\"success\"):\n print(\"✓ Gateway restarted successfully\")\n else:\n print(f\"Response: {result.get('message', result)}\")\n\n\ndef get_logs(args):\n \"\"\"Get Gateway logs.\"\"\"\n endpoint = f\"/gateway/logs?tail={args.limit}\"\n result = api_request(\"GET\", endpoint)\n\n if args.json:\n print(json.dumps(result, indent=2))\n else:\n if result.get(\"success\"):\n logs = result.get(\"logs\", \"\")\n if isinstance(logs, str):\n print(logs)\n elif isinstance(logs, list):\n for line in logs:\n print(line)\n else:\n print(f\"Error: {result.get('message', result)}\")\n\n\ndef list_networks(args):\n \"\"\"List all available networks.\"\"\"\n result = api_request(\"GET\", \"/gateway/networks\")\n\n if args.json:\n print(json.dumps(result, indent=2))\n else:\n networks = result.get(\"networks\", [])\n count = result.get(\"count\", len(networks))\n\n print(f\"Available Networks ({count}):\")\n print(\"-\" * 40)\n\n # Group by chain\n by_chain = {}\n for net in networks:\n chain = net.get(\"chain\", \"unknown\")\n if chain not in by_chain:\n by_chain[chain] = []\n by_chain[chain].append(net.get(\"network_id\", net.get(\"network\", \"\")))\n\n for chain, nets in sorted(by_chain.items()):\n print(f\"\\n{chain}:\")\n for net_id in nets:\n print(f\" - {net_id}\")\n\n\ndef get_network(args):\n \"\"\"Get or update network config.\"\"\"\n if args.node_url:\n # Update network config\n data = {\"nodeURL\": args.node_url}\n print(f\"Updating network {args.network_id}...\")\n result = api_request(\"POST\", f\"/gateway/networks/{args.network_id}\", data)\n\n if args.json:\n print(json.dumps(result, indent=2))\n else:\n if result.get(\"success\"):\n print(f\"✓ Network {args.network_id} updated\")\n print(f\" nodeURL: {args.node_url}\")\n if result.get(\"restart_required\"):\n print(f\"\\n ⚠ Restart Gateway for changes to take effect:\")\n print(f\" python manage_gateway.py restart\")\n else:\n print(f\"Response: {result.get('message', result)}\")\n else:\n # Get network config\n result = api_request(\"GET\", f\"/gateway/networks/{args.network_id}\")\n\n if args.json:\n print(json.dumps(result, indent=2))\n else:\n print(f\"Network: {args.network_id}\")\n print(\"-\" * 40)\n\n for key, value in result.items():\n if key in (\"node_url\", \"nodeURL\"):\n print(f\" Node URL: {value}\")\n elif key in (\"token_list_source\", \"tokenListSource\"):\n print(f\" Token List: {value}\")\n elif key in (\"native_currency_symbol\", \"nativeCurrencySymbol\"):\n print(f\" Native Currency: {value}\")\n elif not key.startswith(\"_\"):\n print(f\" {key}: {value}\")\n\n\ndef main():\n parser = argparse.ArgumentParser(description=\"Manage Gateway via hummingbot-api\")\n subparsers = parser.add_subparsers(dest=\"command\", required=True)\n\n # status command\n status_parser = subparsers.add_parser(\"status\", help=\"Get Gateway status\")\n status_parser.add_argument(\"--json\", action=\"store_true\", help=\"Output as JSON\")\n status_parser.set_defaults(func=get_status)\n\n # start command\n start_parser = subparsers.add_parser(\"start\", help=\"Start Gateway\")\n start_parser.add_argument(\"--passphrase\", default=\"hummingbot\", help=\"Gateway passphrase (default: hummingbot)\")\n start_parser.add_argument(\"--image\", default=\"hummingbot/gateway:latest\", help=\"Docker image\")\n start_parser.add_argument(\"--port\", type=int, default=15888, help=\"Port (default: 15888)\")\n start_parser.add_argument(\"--json\", action=\"store_true\", help=\"Output as JSON\")\n start_parser.set_defaults(func=start_gateway)\n\n # stop command\n stop_parser = subparsers.add_parser(\"stop\", help=\"Stop Gateway\")\n stop_parser.add_argument(\"--json\", action=\"store_true\", help=\"Output as JSON\")\n stop_parser.set_defaults(func=stop_gateway)\n\n # restart command\n restart_parser = subparsers.add_parser(\"restart\", help=\"Restart Gateway\")\n restart_parser.add_argument(\"--passphrase\", help=\"Gateway passphrase (optional, uses existing config if not provided)\")\n restart_parser.add_argument(\"--image\", help=\"Docker image\")\n restart_parser.add_argument(\"--port\", type=int, help=\"Port\")\n restart_parser.add_argument(\"--json\", action=\"store_true\", help=\"Output as JSON\")\n restart_parser.set_defaults(func=restart_gateway)\n\n # logs command\n logs_parser = subparsers.add_parser(\"logs\", help=\"Get Gateway logs\")\n logs_parser.add_argument(\"--limit\", type=int, default=100, help=\"Number of log lines (default: 100)\")\n logs_parser.add_argument(\"--json\", action=\"store_true\", help=\"Output as JSON\")\n logs_parser.set_defaults(func=get_logs)\n\n # networks command (list all)\n networks_parser = subparsers.add_parser(\"networks\", help=\"List all networks\")\n networks_parser.add_argument(\"--json\", action=\"store_true\", help=\"Output as JSON\")\n networks_parser.set_defaults(func=list_networks)\n\n # network command (get/update single network)\n network_parser = subparsers.add_parser(\"network\", help=\"Get or update network config\")\n network_parser.add_argument(\"network_id\", help=\"Network ID (e.g., solana-mainnet-beta)\")\n network_parser.add_argument(\"--node-url\", help=\"Set custom RPC node URL\")\n network_parser.add_argument(\"--json\", action=\"store_true\", help=\"Output as JSON\")\n network_parser.set_defaults(func=get_network)\n\n args = parser.parse_args()\n args.func(args)\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":11023,"content_sha256":"907110205219181f5f54bac64dd6e7a077a8a45b0a94c00c8eee30b5e2a9c681"},{"filename":"scripts/README.md","content":"# LP Agent Scripts\n\nScripts for deploying infrastructure, exploring Meteora pools, managing LP positions, and analyzing performance.\n\n## Scripts\n\n**Infrastructure:**\n- `deploy_hummingbot_api.sh` — Install/upgrade/manage Hummingbot API\n- `setup_gateway.sh` — Start Gateway (with custom image), configure RPC per network\n- `check_api.sh` — Shared: check if Hummingbot API is running (source or run directly)\n- `check_gateway.sh` — Shared: check if Gateway is running (source or run directly)\n- `add_wallet.py` — Add wallets and check balances\n\n**Pool Explorer:**\n- `list_meteora_pools.py` — Search and list Meteora DLMM pools\n- `get_meteora_pool.py` — Get detailed pool information with liquidity distribution\n\n**Position Management:**\n- `manage_executor.py` — Create, monitor, and stop LP executors\n- `manage_controller.py` — Deploy and manage LP Rebalancer controllers\n- `manage_gateway.py` — Start/stop Gateway and configure RPC nodes (advanced)\n\n**Analysis:**\n- `export_lp_positions.py` — Export LP position events to CSV\n- `visualize_lp_positions.py` — Generate interactive HTML dashboard from LP position events\n\n---\n\n## list_meteora_pools.py\n\nSearch and list Meteora DLMM pools by name, token, or address. Shows token mint addresses to identify correct tokens.\n\n### Requirements\n\n- Python 3.10+\n- No pip dependencies — uses only the standard library\n\n### Usage\n\n```bash\n# Top pools by 24h volume\npython scripts/list_meteora_pools.py\n\n# Search by token symbol\npython scripts/list_meteora_pools.py --query SOL\npython scripts/list_meteora_pools.py --query PERCOLATOR\n\n# Sort by different metrics\npython scripts/list_meteora_pools.py --query SOL --sort tvl\npython scripts/list_meteora_pools.py --query SOL --sort apr\npython scripts/list_meteora_pools.py --query SOL --sort fees\n\n# Output as JSON\npython scripts/list_meteora_pools.py --query SOL --json\n```\n\n### CLI Reference\n\n```\nlist_meteora_pools.py [-q QUERY] [-s SORT] [--order ORDER] [-n LIMIT] [-p PAGE] [--json]\n```\n\n| Argument | Description |\n|---|---|\n| `-q`, `--query` | Search by pool name, token symbol, or address |\n| `-s`, `--sort` | Sort by: `volume`, `tvl`, `fees`, `apr`, `apy` (default: `volume`) |\n| `--order` | Sort order: `asc` or `desc` (default: `desc`) |\n| `-n`, `--limit` | Number of results (default: 10, max: 1000) |\n| `-p`, `--page` | Page number (default: 1) |\n| `--json` | Output as JSON |\n\n### Output\n\nOutputs a markdown table with token mint addresses to identify correct tokens:\n\n| # | Pool | Pool Address | Base (mint) | Quote (mint) | TVL | Vol 24h | Fees 24h | APR | Fee | Bin |\n|---|------|--------------|-------------|--------------|-----|---------|----------|-----|-----|-----|\n| 1 | Percolator-SOL | `ATrBUW..sPMSms` | Percolator (`8PzF..pump`) | SOL (`So11..1112`) | $8.9K | $15.5K | $348 | 3.9% | 2.00% | 100 |\n\n---\n\n## get_meteora_pool.py\n\nGet detailed information about a specific Meteora DLMM pool. Fetches from both Meteora API (historical data) and Gateway (real-time price, liquidity distribution).\n\n### Requirements\n\n- Python 3.10+\n- No pip dependencies — uses only the standard library\n- Optional: Gateway running for real-time price and liquidity distribution chart\n\n### Usage\n\n```bash\n# Get pool details (includes liquidity chart if Gateway is running)\npython scripts/get_meteora_pool.py \u003cpool_address>\n\n# Skip Gateway API (faster, no liquidity distribution)\npython scripts/get_meteora_pool.py \u003cpool_address> --no-gateway\n\n# Output as JSON\npython scripts/get_meteora_pool.py \u003cpool_address> --json\n```\n\n### Environment Variables\n\n| Variable | Default | Description |\n|----------|---------|-------------|\n| `GATEWAY_HOST` | `localhost` | Gateway API host |\n| `GATEWAY_PORT` | `15888` | Gateway API port |\n\n### Output Sections\n\n1. **Pool Summary** - Current price, TVL, volume, fees, APR/APY, fee tier, bin step\n2. **Token Info** - Base/quote token details (symbol, mint, price, decimals, market cap)\n3. **Reserves** - Token amounts and USD values\n4. **Volume & Fees by Time Window** - 30m, 1h, 4h, 12h, 24h metrics with Fee/TVL ratio\n5. **Cumulative Metrics** - All-time volume and fees\n6. **Real-Time Price** - Current price from Gateway (with subscript notation for small prices)\n7. **Liquidity Distribution** - Vertical ASCII chart showing base/quote liquidity around current price\n8. **Active Bin Info** - Active bin ID, min/max bin IDs, dynamic fee\n\n### Liquidity Distribution Chart\n\nThe chart shows liquidity distribution like the Meteora UI:\n\n```\nLiquidity Distribution\n▓ Percolator ░ SOL │ Current Price: 0.0₄169 SOL/Percolator\n\n░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓\n░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▓▓▓▓▓\n░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▓▓▓▓▓▓▓▓▓▓▓\n░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓\n──────────────────────────────┴─────────────────────────────\n0.0₄125 0.0₄169 0.0₄225\n```\n\n- `▓` Base token (above current price = sell liquidity)\n- `░` Quote token (below current price = buy liquidity)\n- `│` Current price line\n- Subscript notation: `0.0₄169` = 0.0000169\n\n---\n\n## export_lp_positions.py\n\nExport LP position events from the `RangePositionUpdate` table to CSV. Events are stored immediately when they occur on-chain.\n\n### Usage\n\n```bash\n# Export all LP position events (auto-detects database)\npython scripts/export_lp_positions.py\n\n# Show summary without exporting\npython scripts/export_lp_positions.py --summary\n\n# Filter by trading pair\npython scripts/export_lp_positions.py --pair SOL-USDC\n\n# Specify database\npython scripts/export_lp_positions.py --db data/my_bot.sqlite\n\n# Custom output path\npython scripts/export_lp_positions.py -o exports/my_positions.csv\n```\n\n### CLI Reference\n\n```\nexport_lp_positions.py [--db PATH] [--output PATH] [--pair PAIR] [--summary]\n```\n\n| Argument | Description |\n|---|---|\n| `--db PATH` | Path to SQLite database. Defaults to auto-detecting database with most LP data. |\n| `-o`, `--output PATH` | Output CSV path. Defaults to `data/lp_positions_\u003ctimestamp>.csv`. |\n| `-p`, `--pair PAIR` | Filter by trading pair (e.g., `SOL-USDC`). |\n| `-s`, `--summary` | Show summary only, don't export. |\n\n### Exported columns\n\n- **id**: Database row ID\n- **hb_id**: Hummingbot order ID\n- **timestamp**: Unix timestamp in milliseconds\n- **datetime**: Human-readable timestamp\n- **tx_hash**: Transaction signature\n- **connector**: Connector name (e.g., `meteora/clmm`)\n- **action**: `ADD` or `REMOVE`\n- **trading_pair**: Trading pair (e.g., `SOL-USDC`)\n- **position_address**: LP position NFT address\n- **lower_price, upper_price**: Position price bounds\n- **mid_price**: Current price at time of event\n- **base_amount, quote_amount**: Token amounts\n- **base_fee, quote_fee**: Fees collected (for REMOVE)\n- **position_rent**: SOL rent paid (ADD only)\n- **position_rent_refunded**: SOL rent refunded (REMOVE only)\n\n---\n\n## visualize_lp_positions.py\n\nGenerate an interactive HTML dashboard from LP position events. Groups ADD/REMOVE events by position address to show complete position lifecycle with PnL, fees, and impermanent loss.\n\n### Requirements\n\n- Python 3.10+\n- No pip dependencies — uses only the standard library\n- A modern browser (the HTML loads React, Recharts, and Babel from CDN)\n\n### Usage\n\n```bash\n# Basic usage (trading pair is required)\npython scripts/visualize_lp_positions.py --pair SOL-USDC\n\n# Filter by connector\npython scripts/visualize_lp_positions.py --pair SOL-USDC --connector meteora/clmm\n\n# Last 24 hours only\npython scripts/visualize_lp_positions.py --pair SOL-USDC --hours 24\n\n# Specify database\npython scripts/visualize_lp_positions.py --db data/my_bot.sqlite --pair SOL-USDC\n\n# Custom output path\npython scripts/visualize_lp_positions.py --pair SOL-USDC -o reports/positions.html\n\n# Skip auto-open\npython scripts/visualize_lp_positions.py --pair SOL-USDC --no-open\n```\n\n### CLI Reference\n\n```\nvisualize_lp_positions.py --pair PAIR [--db PATH] [--connector NAME] [--hours N] [-o PATH] [--no-open]\n```\n\n| Argument | Description |\n|---|---|\n| `-p`, `--pair PAIR` | **Required.** Trading pair (e.g., `SOL-USDC`). |\n| `--db PATH` | Path to SQLite database. Defaults to auto-detecting database with most LP data. |\n| `-c`, `--connector NAME` | Filter by connector (e.g., `meteora/clmm`). |\n| `-H`, `--hours N` | Lookback period in hours (e.g., `24` for last 24 hours). |\n| `-o`, `--output PATH` | Output HTML path. |\n| `--no-open` | Don't auto-open the dashboard in the browser. |\n\n### Dashboard Features\n\n**KPI cards** — total PnL, fees earned (with bps calculation), IL (impermanent loss), win/loss counts, best/worst position, average duration.\n\n**Cumulative PnL & Fees** — area chart showing PnL and fee accrual over closed positions.\n\n**Price at Open/Close** — price when positions were opened vs closed, overlaid with LP range bounds.\n\n**Per-Position PnL** — bar chart of each position's PnL. Click a bar to view details.\n\n**Duration vs PnL** — scatter plot of position duration vs PnL.\n\n**IL vs Fees Breakdown** — how impermanent loss compares to fees earned.\n\n**Positions table** — sortable/filterable table with:\n- Timing (opened, closed, duration)\n- Price bounds and prices at ADD/REMOVE\n- ADD liquidity with deposited amounts and Solscan TX link\n- REMOVE liquidity with withdrawn amounts, fees, and Solscan TX link\n- PnL breakdown (IL + fees)\n\n---\n\n## manage_executor.py\n\nCreate, monitor, and stop LP executors via hummingbot-api.\n\n### Usage\n\n```bash\n# Create LP executor\npython scripts/manage_executor.py create \\\n --pool \u003cpool_address> \\\n --pair SOL-USDC \\\n --quote-amount 100 \\\n --lower 180 \\\n --upper 185\n\n# Get executor status\npython scripts/manage_executor.py get \u003cexecutor_id>\n\n# List all executors\npython scripts/manage_executor.py list\npython scripts/manage_executor.py list --type lp_executor --json\n\n# Get executor logs\npython scripts/manage_executor.py logs \u003cexecutor_id> --limit 50\n\n# Stop executor (closes position)\npython scripts/manage_executor.py stop \u003cexecutor_id>\n\n# Stop executor but keep position on-chain\npython scripts/manage_executor.py stop \u003cexecutor_id> --keep-position\n\n# Get summary of all executors\npython scripts/manage_executor.py summary\n```\n\n### Create Options\n\n| Argument | Description |\n|---|---|\n| `--pool` | Pool address (required) |\n| `--pair` | Trading pair e.g., SOL-USDC (required) |\n| `--lower` | Lower price bound (required) |\n| `--upper` | Upper price bound (required) |\n| `--connector` | Connector name (default: meteora/clmm) |\n| `--base-amount` | Base token amount (default: 0) |\n| `--quote-amount` | Quote token amount |\n| `--side` | 0=BOTH, 1=BUY, 2=SELL (default: 1) |\n| `--auto-close-above` | Auto-close seconds when price above range |\n| `--auto-close-below` | Auto-close seconds when price below range |\n| `--strategy-type` | Meteora: 0=Spot, 1=Curve, 2=Bid-Ask |\n\n---\n\n## manage_controller.py\n\nDeploy and manage LP Rebalancer controllers via hummingbot-api.\n\n### Quick Start\n\n```bash\n# 1. Create LP Rebalancer config\npython scripts/manage_controller.py create-config my_lp_config \\\n --pool \u003cpool_address> \\\n --pair SOL-USDC \\\n --amount 100 \\\n --side 1 \\\n --width 0.5 \\\n --offset 0.01 \\\n --rebalance-seconds 60 \\\n --sell-max 100 \\\n --sell-min 75 \\\n --buy-max 90 \\\n --buy-min 70\n\n# 2. Deploy bot with the config\npython scripts/manage_controller.py deploy my_lp_bot --configs my_lp_config\n\n# 3. Monitor\npython scripts/manage_controller.py status\n```\n\n### Usage\n\n```bash\n# Get LP Rebalancer config template\npython scripts/manage_controller.py template\n\n# Create LP Rebalancer config (see Quick Start for full example)\npython scripts/manage_controller.py create-config my_lp_config --pool \u003caddress> --pair SOL-USDC\n\n# List all configs\npython scripts/manage_controller.py list-configs\n\n# Get config details\npython scripts/manage_controller.py describe-config my_lp_config\n\n# Deploy bot with controller config(s)\npython scripts/manage_controller.py deploy my_bot --configs my_lp_config\npython scripts/manage_controller.py deploy my_bot --configs config1 config2 # Multiple controllers\n\n# Get active bots status\npython scripts/manage_controller.py status\n\n# Get specific bot status\npython scripts/manage_controller.py bot-status my_bot\n\n# Stop a bot\npython scripts/manage_controller.py stop my_bot\n\n# Stop and archive a bot\npython scripts/manage_controller.py stop-and-archive my_bot\n\n# Delete a config\npython scripts/manage_controller.py delete-config my_lp_config\n```\n\n### Create-Config Options\n\n| Argument | Description |\n|---|---|\n| `--pool` | Pool address (required) |\n| `--pair` | Trading pair e.g., SOL-USDC (required) |\n| `--connector` | Connector name (default: meteora/clmm) |\n| `--network` | Network (default: solana-mainnet-beta) |\n| `--amount` | Total amount in quote currency (default: 50) |\n| `--side` | 0=BOTH, 1=BUY, 2=SELL (default: 1) |\n| `--width` | Position width % (default: 0.5) |\n| `--offset` | Position offset % (default: 0.01) |\n| `--rebalance-seconds` | Seconds out-of-range before rebalancing (default: 60) |\n| `--rebalance-threshold` | Price % beyond bounds before timer starts (default: 0.1) |\n| `--sell-max` | Sell price max (anchor point) |\n| `--sell-min` | Sell price min |\n| `--buy-max` | Buy price max |\n| `--buy-min` | Buy price min (anchor point) |\n| `--strategy-type` | Meteora: 0=Spot, 1=Curve, 2=Bid-Ask (default: 0) |\n\n### Deploy Options\n\n| Argument | Description |\n|---|---|\n| `bot_name` | Bot instance name (required) |\n| `--configs` | Controller config name(s) (required) |\n| `--account` | Credentials profile (default: master_account) |\n| `--image` | Docker image (default: hummingbot/hummingbot:latest) |\n| `--max-global-drawdown` | Max global drawdown in quote |\n| `--max-controller-drawdown` | Max controller drawdown in quote |\n| `--script-config` | Script config name (auto-generated if not provided) |\n| `--headless` | Run in headless mode |\n\n### Environment Variables\n\n| Variable | Default | Description |\n|----------|---------|-------------|\n| `HUMMINGBOT_API_URL` | `http://localhost:8000` | Hummingbot API URL |\n| `API_USER` | `admin` | API username |\n| `API_PASS` | `admin` | API password |\n\nScripts check for `.env` in: `./hummingbot-api/.env` → `~/.hummingbot/.env` → `.env`\n\n---\n\n## manage_gateway.py\n\nStart/stop Gateway and configure RPC nodes via hummingbot-api.\n\n### Usage\n\n```bash\n# Check Gateway status\npython scripts/manage_gateway.py status\n\n# Start Gateway\npython scripts/manage_gateway.py start\npython scripts/manage_gateway.py start --passphrase mypassword --port 15888\n\n# Stop Gateway\npython scripts/manage_gateway.py stop\n\n# Restart Gateway\npython scripts/manage_gateway.py restart\n\n# Get Gateway logs\npython scripts/manage_gateway.py logs\npython scripts/manage_gateway.py logs --limit 200\n\n# List all networks\npython scripts/manage_gateway.py networks\n\n# Get network config\npython scripts/manage_gateway.py network solana-mainnet-beta\n\n# Set custom RPC node (avoid rate limits)\npython scripts/manage_gateway.py network solana-mainnet-beta --node-url https://my-rpc.example.com\n```\n\n### Commands\n\n| Command | Description |\n|---|---|\n| `status` | Check if Gateway is running |\n| `start` | Start Gateway container |\n| `stop` | Stop Gateway container |\n| `restart` | Restart Gateway container |\n| `logs` | Get Gateway container logs |\n| `networks` | List all available networks |\n| `network \u003cid>` | Get network config |\n| `network \u003cid> --node-url \u003curl>` | Set custom RPC node URL |\n\n### Custom RPC Nodes\n\nGateway uses public RPC nodes by default, which can hit rate limits. Set your own RPC endpoint:\n\n```bash\n# Solana mainnet\npython scripts/manage_gateway.py network solana-mainnet-beta --node-url https://my-helius-endpoint.com\n\n# Check the config\npython scripts/manage_gateway.py network solana-mainnet-beta\n```\n\nPopular Solana RPC providers:\n- [Helius](https://helius.dev/) - Free tier available\n- [QuickNode](https://quicknode.com/)\n- [Alchemy](https://alchemy.com/)\n- [Triton](https://triton.one/)\n\n---\n\n## deploy_hummingbot_api.sh\n\nInstall, upgrade, and manage the Hummingbot API trading infrastructure.\n\n### Requirements\n\n- Docker and Docker Compose\n- Git\n\n### Usage\n\n```bash\n# Check installation status\nbash scripts/deploy_hummingbot_api.sh status\n\n# Install (interactive)\nbash scripts/deploy_hummingbot_api.sh install\n\n# Install with defaults (non-interactive, admin/admin)\nbash scripts/deploy_hummingbot_api.sh install --defaults\n\n# Upgrade\nbash scripts/deploy_hummingbot_api.sh upgrade\n\n# View logs\nbash scripts/deploy_hummingbot_api.sh logs\n\n# Reset (stop and remove)\nbash scripts/deploy_hummingbot_api.sh reset\n```\n\n### Commands\n\n| Command | Description |\n|---|---|\n| `status` | Check if Hummingbot API is installed and running |\n| `install` | Clone repo and deploy (use `--defaults` for non-interactive) |\n| `upgrade` | Pull latest and redeploy |\n| `logs` | Show container logs |\n| `reset` | Stop containers and remove installation |\n\n---\n\n## check_api.sh\n\nShared check script: verifies Hummingbot API is running. Can be sourced by other scripts or run directly.\n\n### Usage\n\n```bash\n# Run directly\nbash scripts/check_api.sh\nbash scripts/check_api.sh --json\n\n# Source in another script\nsource scripts/check_api.sh\ncheck_api || echo \"API not running\"\n```\n\n---\n\n## check_gateway.sh\n\nShared check script: verifies Gateway is running (also checks API). Can be sourced or run directly.\n\n### Usage\n\n```bash\n# Run directly\nbash scripts/check_gateway.sh\nbash scripts/check_gateway.sh --json\n\n# Source in another script\nsource scripts/check_gateway.sh\ncheck_gateway || echo \"Gateway not running\"\n```\n\n---\n\n## setup_gateway.sh\n\nStart Gateway (with optional custom image), check status, and configure RPC endpoints per network. Gateway is required for all LP operations.\n\n### Requirements\n\n- Hummingbot API running (install with `deploy_hummingbot_api.sh`) — checked automatically via `check_api.sh`\n- Python 3.10+\n\n### Usage\n\n```bash\n# Check Gateway status\nbash scripts/setup_gateway.sh --status\n\n# Start Gateway with defaults\nbash scripts/setup_gateway.sh\n\n# Start with custom image (e.g., development build)\nbash scripts/setup_gateway.sh --image hummingbot/gateway:development\n\n# Start with custom RPC (recommended)\nbash scripts/setup_gateway.sh --rpc-url https://your-rpc-endpoint.com\n\n# Configure RPC for a different network\nbash scripts/setup_gateway.sh --network ethereum-mainnet --rpc-url https://your-eth-rpc.com\n\n# Custom passphrase and port\nbash scripts/setup_gateway.sh --passphrase mypassword --port 15888\n```\n\n### Options\n\n| Option | Default | Description |\n|---|---|---|\n| `--status` | | Check status only, don't start |\n| `--image IMAGE` | `hummingbot/gateway:latest` | Docker image to use |\n| `--passphrase TEXT` | `hummingbot` | Gateway passphrase |\n| `--rpc-url URL` | | Custom RPC endpoint for `--network` |\n| `--network ID` | `solana-mainnet-beta` | Network to configure RPC for |\n| `--port PORT` | `15888` | Gateway port |\n\n---\n\n## add_wallet.py\n\nAdd and manage wallets via hummingbot-api Gateway.\n\n### Requirements\n\n- Python 3.10+\n- No pip dependencies — uses only the standard library\n- Gateway running (start with `setup_gateway.sh`)\n\n### Usage\n\n```bash\n# List connected wallets\npython scripts/add_wallet.py list\n\n# Add wallet (prompted for private key — secure)\npython scripts/add_wallet.py add\n\n# Add wallet with private key\npython scripts/add_wallet.py add --private-key \u003cBASE58_KEY>\n\n# Check balances\npython scripts/add_wallet.py balances --address \u003cWALLET_ADDRESS>\n\n# Check specific tokens\npython scripts/add_wallet.py balances --address \u003cWALLET_ADDRESS> --tokens SOL USDC\n```\n\n### Commands\n\n| Command | Description |\n|---|---|\n| `list` | List all connected wallets |\n| `add` | Add a wallet (prompted or via `--private-key`) |\n| `balances` | Get wallet token balances |\n\n### Add Options\n\n| Option | Description |\n|---|---|\n| `--private-key` | Private key in base58. Omit to be prompted securely. |\n| `--chain` | Blockchain (default: solana) |\n| `--network` | Network (default: mainnet-beta) |\n\n### Balances Options\n\n| Option | Description |\n|---|---|\n| `--address` | Wallet address (required) |\n| `--tokens` | Specific token symbols to check |\n| `--chain` | Blockchain (default: solana) |\n| `--network` | Network (default: mainnet-beta) |\n| `--all` | Show zero balances too |\n\n---\n\n## Notes\n\n- All Python scripts use only the standard library (no pip install required)\n- Shell scripts require Docker, Docker Compose, and Git\n- The HTML dashboard is fully self-contained (data inlined as JSON), shareable and archivable\n- LP position events are stored immediately on-chain, so analysis works for both running and stopped bots\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":21069,"content_sha256":"56446ee857bd0fc4419ccc1393366c16350bb995286dc688ae729a23affd8e25"},{"filename":"scripts/setup_gateway.sh","content":"#!/bin/bash\n#\n# Setup Gateway for LP operations\n#\n# Starts Gateway via hummingbot-api and optionally configures custom RPC endpoints.\n# Requires Hummingbot API to be running (deploy-hummingbot-api).\n#\n# Usage:\n# # Start Gateway with defaults\n# bash scripts/setup_gateway.sh\n#\n# # Start Gateway with custom image\n# bash scripts/setup_gateway.sh --image hummingbot/gateway:development\n#\n# # Start Gateway with custom RPC\n# bash scripts/setup_gateway.sh --rpc-url https://your-rpc-endpoint.com\n#\n# # Configure RPC for a specific network\n# bash scripts/setup_gateway.sh --network ethereum-mainnet --rpc-url https://your-eth-rpc.com\n#\n# # Start with custom passphrase\n# bash scripts/setup_gateway.sh --passphrase mypassword\n#\n# # Check Gateway status only\n# bash scripts/setup_gateway.sh --status\n#\nset -e\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\n\n# Load .env\nfor f in hummingbot-api/.env ~/.hummingbot/.env .env; do\n if [ -f \"$f\" ]; then\n set -a; source \"$f\"; set +a\n break\n fi\ndone\n\nAPI_URL=\"${HUMMINGBOT_API_URL:-http://localhost:8000}\"\nAPI_USER=\"${API_USER:-admin}\"\nAPI_PASS=\"${API_PASS:-admin}\"\n\n# Defaults\nPASSPHRASE=\"hummingbot\"\nIMAGE=\"hummingbot/gateway:development\"\nRPC_URL=\"\"\nSTATUS_ONLY=false\nNETWORK=\"solana-mainnet-beta\"\nPORT=15888\n\n# Parse arguments\nwhile [[ $# -gt 0 ]]; do\n case \"$1\" in\n --passphrase) PASSPHRASE=\"$2\"; shift 2 ;;\n --image) IMAGE=\"$2\"; shift 2 ;;\n --rpc-url) RPC_URL=\"$2\"; shift 2 ;;\n --network) NETWORK=\"$2\"; shift 2 ;;\n --port) PORT=\"$2\"; shift 2 ;;\n --status) STATUS_ONLY=true; shift ;;\n -h|--help)\n echo \"Usage: setup_gateway.sh [OPTIONS]\"\n echo \"\"\n echo \"Options:\"\n echo \" --passphrase TEXT Gateway passphrase (default: hummingbot)\"\n echo \" --image IMAGE Docker image (default: hummingbot/gateway:development)\"\n echo \" --rpc-url URL Custom RPC endpoint for --network\"\n echo \" --network ID Network ID (default: solana-mainnet-beta)\"\n echo \" --port PORT Gateway port (default: 15888)\"\n echo \" --status Check status only\"\n echo \" -h, --help Show this help\"\n exit 0\n ;;\n *) echo \"Unknown option: $1\"; exit 1 ;;\n esac\ndone\n\n# Colors\nif [ -t 1 ]; then\n GREEN='\\033[0;32m'\n RED='\\033[0;31m'\n YELLOW='\\033[1;33m'\n NC='\\033[0m'\nelse\n GREEN='' RED='' YELLOW='' NC=''\nfi\n\nok() { echo -e \"${GREEN}✓${NC} $*\"; }\nfail() { echo -e \"${RED}✗${NC} $*\"; }\nwarn() { echo -e \"${YELLOW}!${NC} $*\"; }\n\n# Auth header\nAUTH=$(echo -n \"$API_USER:$API_PASS\" | base64)\n\napi_get() {\n curl -s -H \"Authorization: Basic $AUTH\" -H \"Content-Type: application/json\" \"$API_URL$1\" 2>/dev/null\n}\n\napi_post() {\n curl -s -X POST -H \"Authorization: Basic $AUTH\" -H \"Content-Type: application/json\" -d \"$2\" \"$API_URL$1\" 2>/dev/null\n}\n\n# Poll Gateway status until running (max 30s)\nwait_for_gateway() {\n local max_attempts=15\n local attempt=0\n while [ $attempt -lt $max_attempts ]; do\n local status running\n status=$(api_get \"/gateway/status\")\n running=$(echo \"$status\" | python3 -c \"import sys,json; print(json.load(sys.stdin).get('running', False))\" 2>/dev/null || echo \"False\")\n if [ \"$running\" = \"True\" ]; then\n return 0\n fi\n attempt=$((attempt + 1))\n sleep 2\n done\n return 1\n}\n\n# --- Check prerequisite: Hummingbot API ---\necho \"Gateway Setup\"\necho \"=============\"\necho \"\"\n\nsource \"$SCRIPT_DIR/check_api.sh\"\nif ! check_api; then\n fail \"Cannot connect to Hummingbot API at $API_URL\"\n echo \"\"\n echo \"Please deploy Hummingbot API first:\"\n echo \" /lp-agent deploy-hummingbot-api\"\n exit 1\nfi\nok \"Hummingbot API is running at $API_URL\"\n\n# --- Check Gateway status ---\nGW_STATUS=$(api_get \"/gateway/status\")\nGW_RUNNING=$(echo \"$GW_STATUS\" | python3 -c \"import sys,json; print(json.load(sys.stdin).get('running', False))\" 2>/dev/null || echo \"False\")\n\nif [ \"$STATUS_ONLY\" = \"true\" ]; then\n if [ \"$GW_RUNNING\" = \"True\" ]; then\n ok \"Gateway is running\"\n echo \"$GW_STATUS\" | python3 -c \"\nimport sys, json\nd = json.load(sys.stdin)\nif d.get('container_id'): print(f\\\" Container: {d['container_id'][:12]}\\\")\nif d.get('image'): print(f\\\" Image: {d['image']}\\\")\nif d.get('port'): print(f\\\" Port: {d['port']}\\\")\n\" 2>/dev/null || true\n else\n fail \"Gateway is not running\"\n fi\n exit 0\nfi\n\n# --- Start Gateway if not running ---\nif [ \"$GW_RUNNING\" = \"True\" ]; then\n ok \"Gateway is already running\"\nelse\n echo \"Starting Gateway (image: $IMAGE)...\"\n RESULT=$(api_post \"/gateway/start\" \"{\\\"passphrase\\\": \\\"$PASSPHRASE\\\", \\\"image\\\": \\\"$IMAGE\\\", \\\"port\\\": $PORT, \\\"dev_mode\\\": true}\")\n\n SUCCESS=$(echo \"$RESULT\" | python3 -c \"import sys,json; print(json.load(sys.stdin).get('success', False))\" 2>/dev/null || echo \"False\")\n if [ \"$SUCCESS\" = \"True\" ]; then\n ok \"Gateway started\"\n else\n MSG=$(echo \"$RESULT\" | python3 -c \"import sys,json; print(json.load(sys.stdin).get('message', 'Unknown error'))\" 2>/dev/null || echo \"$RESULT\")\n fail \"Failed to start Gateway: $MSG\"\n exit 1\n fi\n\n # Wait for Gateway to be ready\n echo \"Waiting for Gateway to initialize...\"\n if wait_for_gateway; then\n ok \"Gateway is ready\"\n else\n warn \"Gateway may still be starting. Check status with: bash scripts/setup_gateway.sh --status\"\n fi\nfi\n\n# --- Configure custom RPC if provided ---\nif [ -n \"$RPC_URL\" ]; then\n echo \"\"\n echo \"Configuring custom RPC for $NETWORK...\"\n RESULT=$(api_post \"/gateway/networks/$NETWORK\" \"{\\\"nodeURL\\\": \\\"$RPC_URL\\\"}\")\n\n SUCCESS=$(echo \"$RESULT\" | python3 -c \"import sys,json; print(json.load(sys.stdin).get('success', False))\" 2>/dev/null || echo \"False\")\n if [ \"$SUCCESS\" = \"True\" ]; then\n ok \"RPC configured: $RPC_URL\"\n warn \"Restarting Gateway for changes to take effect...\"\n api_post \"/gateway/restart\" \"{\\\"passphrase\\\": \\\"$PASSPHRASE\\\", \\\"image\\\": \\\"$IMAGE\\\", \\\"port\\\": $PORT, \\\"dev_mode\\\": true}\" >/dev/null\n if wait_for_gateway; then\n ok \"Gateway restarted with custom RPC\"\n else\n warn \"Gateway may still be restarting\"\n fi\n else\n fail \"Failed to configure RPC\"\n fi\nfi\n\necho \"\"\nok \"Gateway setup complete\"\necho \"\"\necho \"Next steps:\"\necho \" - Add a wallet: /lp-agent add-wallet\"\necho \" - Explore pools: /lp-agent explore-pools\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":6570,"content_sha256":"2fdd23333f5c2e080c35805bb60ba15b0929ec66a84030e245bf731d0801f214"},{"filename":"scripts/visualize_lp_executor.py","content":"#!/usr/bin/env python3\n\"\"\"\nGenerate an interactive HTML dashboard for a single LP executor.\n\nFetches from the Hummingbot REST API by executor ID — no SQLite needed.\nOptionally overlays 5m KuCoin price candles for major pairs.\n\nUsage:\n python scripts/visualize_lp_executor.py --id \u003cexecutor_id>\n python scripts/visualize_lp_executor.py --id \u003cexecutor_id> --output report.html\n python scripts/visualize_lp_executor.py --id \u003cexecutor_id> --no-open\n\nExamples:\n python scripts/visualize_lp_executor.py --id ryUBCGfuBgmDL5bPggxmVFbQzu7McBE1hXs2yRymiob\n python scripts/visualize_lp_executor.py --id 5w7GcMkZZVMDke4LjfqTVDsHVV7SVTF4pmL5TjbcYN8Z --output /tmp/sol_usdc.html\n\"\"\"\n\nimport argparse\nimport base64\nimport json\nimport os\nimport sys\nimport time\nimport urllib.request\nimport urllib.error\nimport webbrowser\nfrom datetime import datetime\n\n# ---------------------------------------------------------------------------\n# Auth / config (HUMMINGBOT_API_URL / API_USER / API_PASS — same as other lp-agent scripts)\n# ---------------------------------------------------------------------------\n\ndef load_env():\n \"\"\"Load environment from .env files (first found wins).\"\"\"\n for path in [\".env\", os.path.expanduser(\"~/.hummingbot/.env\"), os.path.expanduser(\"~/.env\")]:\n if os.path.exists(path):\n with open(path) as f:\n for line in f:\n line = line.strip()\n if line and not line.startswith(\"#\") and \"=\" in line:\n key, value = line.split(\"=\", 1)\n os.environ.setdefault(key.strip(), value.strip().strip('\"').strip(\"'\"))\n break\n\n\ndef get_api_config():\n \"\"\"Get API configuration from environment.\"\"\"\n load_env()\n return {\n \"url\": os.environ.get(\"HUMMINGBOT_API_URL\", \"http://localhost:8000\"),\n \"user\": os.environ.get(\"API_USER\", \"admin\"),\n \"password\": os.environ.get(\"API_PASS\", \"admin\"),\n }\n\n\ndef make_auth_header(cfg):\n creds = base64.b64encode(f\"{cfg['user']}:{cfg['password']}\".encode()).decode()\n return f\"Basic {creds}\"\n\n\n# ---------------------------------------------------------------------------\n# API helpers\n# ---------------------------------------------------------------------------\n\ndef api_get(url, hdrs, timeout=30):\n req = urllib.request.Request(url, headers=hdrs, method=\"GET\")\n try:\n with urllib.request.urlopen(req, timeout=timeout) as resp:\n return json.loads(resp.read().decode())\n except urllib.error.HTTPError as e:\n raise RuntimeError(f\"HTTP {e.code}: {e.read().decode(errors='replace')}\") from e\n except urllib.error.URLError as e:\n raise RuntimeError(f\"Connection error: {e.reason}\") from e\n\n\ndef api_post(url, payload, hdrs, timeout=30):\n data = json.dumps(payload).encode()\n req = urllib.request.Request(url, data=data,\n headers={**hdrs, \"Content-Type\": \"application/json\"},\n method=\"POST\")\n try:\n with urllib.request.urlopen(req, timeout=timeout) as resp:\n return json.loads(resp.read().decode())\n except urllib.error.HTTPError as e:\n raise RuntimeError(f\"HTTP {e.code}: {e.read().decode(errors='replace')}\") from e\n except urllib.error.URLError as e:\n raise RuntimeError(f\"Connection error: {e.reason}\") from e\n\n\ndef fetch_executor(base_url, hdrs, executor_id):\n raw = api_get(f\"{base_url}/executors/{executor_id}\", hdrs)\n if isinstance(raw, dict) and \"data\" in raw:\n data = raw[\"data\"]\n return (data[0] if isinstance(data, list) and data else data) or None\n return raw\n\n\ndef fetch_candles(base_url, hdrs, trading_pair, start_time, end_time, interval=\"5m\"):\n \"\"\"Fetch KuCoin candles. Returns list or None. Skips exotic/pump pairs.\"\"\"\n kucoin_pair = trading_pair.replace(\"USDC\", \"USDT\").replace(\"usdc\", \"usdt\")\n base_tok = kucoin_pair.split(\"-\")[0]\n if len(base_tok) > 10 or base_tok.lower().endswith(\"pump\"):\n print(f\" Skipping candle fetch — exotic pair: {kucoin_pair}\")\n return None\n try:\n result = api_post(\n f\"{base_url}/market-data/historical-candles\",\n {\"connector_name\": \"kucoin\", \"trading_pair\": kucoin_pair,\n \"interval\": interval, \"start_time\": int(start_time), \"end_time\": int(end_time)},\n hdrs, timeout=20,\n )\n if isinstance(result, list):\n return result\n if isinstance(result, dict):\n for k in (\"candles\", \"data\", \"ohlcv\"):\n if k in result and isinstance(result[k], list):\n return result[k]\n return None\n except Exception as e:\n print(f\" Warning: candle fetch failed for {kucoin_pair}: {e}\")\n return None\n\n\n# ---------------------------------------------------------------------------\n# Data helpers\n# ---------------------------------------------------------------------------\n\ndef _f(v, d=None):\n if v is None or v == \"\":\n return d\n try:\n return float(v)\n except (ValueError, TypeError):\n return d\n\n\ndef _s(v, d=\"\"):\n return str(v) if v is not None else d\n\n\ndef parse_ts(s):\n if not s:\n return None\n try:\n import calendar\n clean = str(s).replace(\"+00:00\", \"\").replace(\"Z\", \"\")\n if \".\" in clean:\n p = clean.split(\".\")\n clean = p[0] + \".\" + p[1][:6]\n dt = datetime.fromisoformat(clean)\n return calendar.timegm(dt.timetuple()) + dt.microsecond / 1e6\n except Exception:\n return None\n\n\ndef fmt_dur(s):\n if s is None or s \u003c= 0:\n return \"—\"\n s = int(s)\n if s \u003c 60:\n return f\"{s}s\"\n if s \u003c 3600:\n return f\"{s // 60}m {s % 60}s\"\n return f\"{s // 3600}h {(s % 3600) // 60}m\"\n\n\ndef fmt_ts(ts):\n if ts is None:\n return \"—\"\n try:\n return datetime.fromtimestamp(ts).strftime(\"%Y-%m-%d %H:%M:%S\")\n except Exception:\n return str(ts)\n\n\ndef status_color(status):\n return {\n \"TERMINATED\": \"#4ecdc4\", \"COMPLETE\": \"#4ecdc4\",\n \"RUNNING\": \"#f0c644\", \"ACTIVE\": \"#f0c644\",\n \"SHUTTING_DOWN\": \"#f0a644\",\n \"FAILED\": \"#e85d75\",\n }.get(status, \"#8b8fa3\")\n\n\ndef pnl_col(v):\n return \"#4ecdc4\" if (v or 0) >= 0 else \"#e85d75\"\n\n\n# ---------------------------------------------------------------------------\n# HTML builder\n# ---------------------------------------------------------------------------\n\nDARK_CSS = \"\"\"\n * { margin:0; padding:0; box-sizing:border-box; }\n body { background:#0d1117; color:#e8eaed; font-family:'JetBrains Mono','Fira Code',Consolas,monospace; font-size:12px; }\n .container { max-width:1200px; margin:0 auto; padding:24px 20px; }\n h1 { font-size:22px; font-weight:700; color:#4ecdc4; margin-bottom:4px; }\n .subtitle { font-size:11px; color:#6b7084; margin-bottom:24px; font-family:monospace; }\n .kpi-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(140px,1fr)); gap:10px; margin-bottom:24px; }\n .kpi-card { background:#161b27; border:1px solid rgba(255,255,255,0.07); border-radius:10px; padding:14px 16px; }\n .kpi-label { font-size:10px; color:#6b7084; text-transform:uppercase; letter-spacing:.06em; margin-bottom:6px; }\n .kpi-value { font-size:20px; font-weight:700; }\n .kpi-sub { font-size:10px; color:#555870; margin-top:4px; }\n .layout { display:grid; grid-template-columns:1fr 340px; gap:24px; }\n @media (max-width:900px) { .layout { grid-template-columns:1fr; } }\n .section-title { font-size:11px; font-weight:600; color:#8b8fa3; text-transform:uppercase; letter-spacing:.08em; margin:24px 0 12px; }\n .chart-wrap { background:#161b27; border:1px solid rgba(255,255,255,0.06); border-radius:10px; padding:16px; margin-bottom:20px; }\n .chart-container { position:relative; }\n .summary-table { background:#161b27; border:1px solid rgba(255,255,255,0.06); border-radius:10px; overflow:hidden; width:100%; }\n .summary-table td { padding:7px 12px; font-size:11px; border-bottom:1px solid rgba(255,255,255,0.03); }\n .summary-table tr:last-child td { border-bottom:none; }\n .summary-table tr:hover td { background:rgba(255,255,255,0.02); }\n .footer { font-size:10px; color:#3a3d50; text-align:center; margin-top:32px; padding-bottom:16px; }\n a { color:#7c6df0; text-decoration:none; }\n a:hover { text-decoration:underline; }\n\"\"\"\n\n\ndef build_html(ex, candles):\n cfg = ex.get(\"config\") or {}\n ci = ex.get(\"custom_info\") or {}\n\n eid = _s(ex.get(\"executor_id\") or ex.get(\"id\"))\n pair = _s(ex.get(\"trading_pair\"))\n connector = _s(ex.get(\"connector_name\"))\n status = _s(ex.get(\"status\"))\n close_type = _s(ex.get(\"close_type\"))\n account = _s(ex.get(\"account_name\"))\n controller = _s(ex.get(\"controller_id\"))\n err_count = ex.get(\"error_count\", 0)\n\n created_ts = parse_ts(ex.get(\"created_at\"))\n close_ts = _f(ex.get(\"close_timestamp\")) or parse_ts(ex.get(\"closed_at\"))\n duration = round(close_ts - created_ts, 1) if created_ts and close_ts and close_ts > created_ts else None\n\n pnl = _f(ex.get(\"net_pnl_quote\"), 0)\n pnl_pct = _f(ex.get(\"net_pnl_pct\"), 0)\n filled = _f(ex.get(\"filled_amount_quote\"), 0)\n\n # Config (deployment params)\n pool_addr = _s(cfg.get(\"pool_address\"))\n lower_cfg = _f(cfg.get(\"lower_price\"))\n upper_cfg = _f(cfg.get(\"upper_price\"))\n base_cfg = _f(cfg.get(\"base_amount\"))\n quote_cfg = _f(cfg.get(\"quote_amount\"))\n side_val = cfg.get(\"side\")\n offset_pct = _f(cfg.get(\"position_offset_pct\"))\n auto_above = cfg.get(\"auto_close_above_range_seconds\")\n auto_below = cfg.get(\"auto_close_below_range_seconds\")\n keep_pos = cfg.get(\"keep_position\")\n\n side_map = {0: \"0 — BOTH\", 1: \"1 — BUY (quote-only)\", 2: \"2 — SELL (base-only)\"}\n side_str = side_map.get(side_val, str(side_val) if side_val is not None else \"—\")\n\n # custom_info (live / final state)\n state = _s(ci.get(\"state\"))\n pos_addr = _s(ci.get(\"position_address\"))\n cur_price = _f(ci.get(\"current_price\"))\n lower_actual = _f(ci.get(\"lower_price\"))\n upper_actual = _f(ci.get(\"upper_price\"))\n base_cur = _f(ci.get(\"base_amount\"))\n quote_cur = _f(ci.get(\"quote_amount\"))\n base_fee = _f(ci.get(\"base_fee\"))\n quote_fee = _f(ci.get(\"quote_fee\"))\n fees_earned = _f(ci.get(\"fees_earned_quote\"), 0)\n total_value = _f(ci.get(\"total_value_quote\"))\n unreal_pnl = _f(ci.get(\"unrealized_pnl_quote\"))\n pos_rent = _f(ci.get(\"position_rent\"))\n rent_ref = _f(ci.get(\"position_rent_refunded\"))\n tx_fee = _f(ci.get(\"tx_fee\"))\n oor_sec = _f(ci.get(\"out_of_range_seconds\"))\n max_retry = ci.get(\"max_retries_reached\")\n init_base = _f(ci.get(\"initial_base_amount\"))\n init_quote = _f(ci.get(\"initial_quote_amount\"))\n\n sc = status_color(status)\n pc = pnl_col(pnl)\n dur_str = fmt_dur(duration)\n oor_str = fmt_dur(oor_sec)\n lp_lower = lower_cfg if lower_cfg is not None else lower_actual\n lp_upper = upper_cfg if upper_cfg is not None else upper_actual\n\n # Solscan links\n pool_link = (\n f'\u003ca href=\"https://solscan.io/account/{pool_addr}\" target=\"_blank\" '\n f'style=\"font-family:monospace;font-size:9px;word-break:break-all;\">{pool_addr}\u003c/a>'\n if pool_addr else \"—\"\n )\n pos_link = (\n f'\u003ca href=\"https://solscan.io/account/{pos_addr}\" target=\"_blank\" '\n f'style=\"font-family:monospace;font-size:9px;word-break:break-all;\">{pos_addr}\u003c/a>'\n if pos_addr else \"—\"\n )\n\n # ---- Price chart ----\n price_section_html = \"\"\n price_chart_js = \"\"\n if candles:\n cd = []\n for c in candles:\n if isinstance(c, list) and len(c) >= 5:\n ts_c, close_c = _f(c[0]), _f(c[4])\n if ts_c and close_c:\n cd.append((ts_c, close_c))\n elif isinstance(c, dict):\n ts_c = _f(c.get(\"timestamp\") or c.get(\"ts\") or c.get(\"open_time\"))\n close_c = _f(c.get(\"close\") or c.get(\"c\"))\n if ts_c and close_c:\n cd.append((ts_c, close_c))\n cd.sort()\n\n if cd:\n labels = [datetime.fromtimestamp(t).strftime(\"%H:%M\") for t, _ in cd]\n closes = [v for _, v in cd]\n\n lp_lo_js = round(lp_lower, 8) if lp_lower is not None else \"null\"\n lp_hi_js = round(lp_upper, 8) if lp_upper is not None else \"null\"\n\n # Find index closest to open/close timestamps\n open_idx = close_idx = \"null\"\n if created_ts:\n for i, (t, _) in enumerate(cd):\n if t >= created_ts:\n open_idx = i; break\n if close_ts:\n for i, (t, _) in reversed(list(enumerate(cd))):\n if t \u003c= close_ts:\n close_idx = i; break\n\n price_section_html = \"\"\"\n \u003cdiv class=\"section-title\">Price Chart — 5m Candles (KuCoin) with LP Range\u003c/div>\n \u003cdiv class=\"chart-wrap\">\u003cdiv class=\"chart-container\" style=\"height:280px;\">\n \u003ccanvas id=\"priceChart\">\u003c/canvas>\n \u003c/div>\u003c/div>\"\"\"\n\n price_chart_js = f\"\"\"\n(function() {{\n const labels = {json.dumps(labels)};\n const closes = {json.dumps(closes)};\n const lo = {lp_lo_js}, hi = {lp_hi_js};\n const openIdx = {open_idx}, closeIdx = {close_idx};\n\n const ds = [{{\n label:'Close Price', data:closes,\n borderColor:'#f0c644', backgroundColor:'rgba(240,198,68,0.06)',\n borderWidth:2, pointRadius:0, fill:false, tension:0.1, order:1,\n }}];\n if (lo !== null) ds.push({{\n label:'LP Lower', data:closes.map(()=>lo),\n borderColor:'rgba(78,205,196,0.7)', borderWidth:1.5, borderDash:[5,5],\n pointRadius:0, fill:false, order:2,\n }});\n if (hi !== null) ds.push({{\n label:'LP Upper', data:closes.map(()=>hi),\n borderColor:'rgba(232,93,117,0.7)', borderWidth:1.5, borderDash:[5,5],\n pointRadius:0, fill:false, order:3,\n }});\n if (lo !== null && hi !== null) ds.push({{\n label:'LP Range', data:closes.map(()=>hi),\n borderColor:'transparent', backgroundColor:'rgba(124,109,240,0.07)',\n fill:1, pointRadius:0, order:4,\n }});\n if (openIdx !== null) ds.push({{\n label:'Open', data:closes.map((v,i)=>i===openIdx?v:null),\n borderColor:'#4ecdc4', backgroundColor:'#4ecdc4',\n pointRadius:closes.map((_,i)=>i===openIdx?8:0),\n pointStyle:'triangle', showLine:false, order:0,\n }});\n if (closeIdx !== null) ds.push({{\n label:'Close', data:closes.map((v,i)=>i===closeIdx?v:null),\n borderColor:'#e85d75', backgroundColor:'#e85d75',\n pointRadius:closes.map((_,i)=>i===closeIdx?8:0),\n pointStyle:'rectRot', showLine:false, order:0,\n }});\n\n new Chart(document.getElementById('priceChart').getContext('2d'), {{\n type:'line', data:{{labels, datasets:ds}},\n options:{{\n maintainAspectRatio:false,\n plugins:{{\n legend:{{display:true, labels:{{color:'#6b7084',font:{{size:10}},boxWidth:18,\n filter:(i)=>i.text!=='LP Range'}}}},\n tooltip:{{mode:'index', intersect:false,\n callbacks:{{label:(c)=>c.parsed.y!=null?` ${{c.dataset.label}}: ${{c.parsed.y.toFixed(6)}}`:null}}}}\n }},\n scales:{{\n x:{{ticks:{{color:'#6b7084',font:{{size:9}},maxTicksLimit:12}},grid:{{color:'rgba(255,255,255,0.04)'}}}},\n y:{{ticks:{{color:'#6b7084',font:{{size:10}},callback:(v)=>v.toFixed(4)}},grid:{{color:'rgba(255,255,255,0.06)'}}}}\n }}\n }}\n }});\n}})();\"\"\"\n\n # ---- Balance chart ----\n balance_html = balance_js = \"\"\n if init_base is not None and base_cur is not None:\n balance_html = \"\"\"\n \u003cdiv class=\"section-title\">Token Balance — Initial vs Final\u003c/div>\n \u003cdiv class=\"chart-wrap\">\u003cdiv class=\"chart-container\" style=\"height:190px;\">\n \u003ccanvas id=\"balChart\">\u003c/canvas>\n \u003c/div>\u003c/div>\"\"\"\n balance_js = f\"\"\"\n(function() {{\n new Chart(document.getElementById('balChart').getContext('2d'), {{\n type:'bar',\n data:{{\n labels:{json.dumps(['Base (initial)','Base (final)','Quote (initial)','Quote (final)'])},\n datasets:[{{\n label:'Amount',\n data:{json.dumps([round(init_base,8), round(base_cur,8), round(init_quote or 0,8), round(quote_cur or 0,8)])},\n backgroundColor:['rgba(78,205,196,0.4)','rgba(78,205,196,0.85)','rgba(124,109,240,0.4)','rgba(124,109,240,0.85)'],\n borderRadius:4,\n }}]\n }},\n options:{{\n maintainAspectRatio:false,\n plugins:{{legend:{{display:false}},\n tooltip:{{callbacks:{{label:(c)=>` ${{c.parsed.y.toFixed(8)}}`}}}}\n }},\n scales:{{\n x:{{ticks:{{color:'#6b7084',font:{{size:10}}}},grid:{{color:'rgba(255,255,255,0.04)'}}}},\n y:{{ticks:{{color:'#6b7084',font:{{size:10}}}},grid:{{color:'rgba(255,255,255,0.06)'}}}}\n }}\n }}\n }});\n}})();\"\"\"\n\n # ---- PnL breakdown chart ----\n il = pnl - fees_earned\n pnl_bkd_labels = json.dumps([\"Fees Earned\", \"IL / Price Impact\", \"Net PnL\"])\n pnl_bkd_data = json.dumps([round(fees_earned, 8), round(il, 8), round(pnl, 8)])\n pnl_bkd_colors = json.dumps([\n \"rgba(124,109,240,0.85)\",\n \"rgba(232,93,117,0.85)\" if il \u003c 0 else \"rgba(78,205,196,0.85)\",\n \"rgba(78,205,196,0.85)\" if pnl >= 0 else \"rgba(232,93,117,0.85)\",\n ])\n\n # ---- Summary table ----\n def row(label, value, color=\"#e8eaed\"):\n return (f'\u003ctr>\u003ctd style=\"color:#6b7084;font-size:10px;width:46%\">{label}\u003c/td>'\n f'\u003ctd style=\"text-align:right;color:{color};font-weight:500;\">{value}\u003c/td>\u003c/tr>')\n\n def sep():\n return '\u003ctr>\u003ctd colspan=\"2\" style=\"padding:3px 0;border-bottom:1px solid rgba(255,255,255,0.06);\">\u003c/td>\u003c/tr>'\n\n d = lambda v, fmt=\".8g\": (f\"{v:{fmt}}\" if v is not None else \"—\")\n\n table_rows = \"\".join([\n row(\"Executor ID\", f'\u003cspan style=\"font-family:monospace;font-size:9px;\">{eid}\u003c/span>'),\n row(\"Account\", account),\n row(\"Controller\", controller or \"—\"),\n row(\"Connector\", connector),\n row(\"Trading Pair\", pair, \"#e8eaed\"),\n sep(),\n row(\"Status\",\n f'\u003cspan style=\"padding:2px 8px;border-radius:8px;background:{sc}22;font-size:9px;\">{status}\u003c/span>'),\n row(\"Close Type\", close_type or \"—\"),\n row(\"State\", state or \"—\"),\n row(\"Errors\", str(err_count), \"#e85d75\" if err_count else \"#e8eaed\"),\n sep(),\n row(\"Created\", fmt_ts(created_ts)),\n row(\"Closed\", fmt_ts(close_ts)),\n row(\"Duration\", dur_str),\n row(\"Out of Range\", oor_str),\n sep(),\n row(\"Net PnL\", f\"{pnl:+.8f}\", pc),\n row(\"Net PnL %\", f\"{pnl_pct * 100:+.4f}%\", pc),\n row(\"Unrealized PnL\", d(unreal_pnl), pnl_col(unreal_pnl)),\n row(\"Fees Earned\", d(fees_earned, \".8f\"), \"#7c6df0\"),\n row(\"Base Fee\", d(base_fee)),\n row(\"Quote Fee\", d(quote_fee)),\n row(\"Total Value\", d(total_value)),\n row(\"Filled Amount\", f\"${filled:.4f}\"),\n sep(),\n row(\"Side\", side_str),\n row(\"LP Lower (config)\", d(lower_cfg)),\n row(\"LP Upper (config)\", d(upper_cfg)),\n row(\"LP Lower (actual)\", d(lower_actual)),\n row(\"LP Upper (actual)\", d(upper_actual)),\n row(\"Current Price\", d(cur_price)),\n row(\"Position Offset %\", f\"{offset_pct:.4f}%\" if offset_pct is not None else \"—\"),\n sep(),\n row(\"Initial Base\", d(init_base, \".8f\")),\n row(\"Initial Quote\", d(init_quote, \".8f\")),\n row(\"Base (final)\", d(base_cur, \".8f\")),\n row(\"Quote (final)\", d(quote_cur, \".8f\")),\n sep(),\n row(\"Position Rent\", f\"{pos_rent:.8f} SOL\" if pos_rent is not None else \"—\", \"#e85d75\"),\n row(\"Rent Refunded\", f\"{rent_ref:.8f} SOL\" if rent_ref is not None else \"—\", \"#4ecdc4\"),\n row(\"TX Fee\", f\"{tx_fee:.8f} SOL\" if tx_fee is not None else \"—\"),\n row(\"Max Retries Hit\", \"yes\" if max_retry else (\"no\" if max_retry is not None else \"—\"),\n \"#e85d75\" if max_retry else \"#e8eaed\"),\n sep(),\n row(\"Auto Close Above\", f\"{auto_above}s\" if auto_above is not None else \"—\"),\n row(\"Auto Close Below\", f\"{auto_below}s\" if auto_below is not None else \"—\"),\n row(\"Keep Position\", \"yes\" if keep_pos else (\"no\" if keep_pos is not None else \"—\")),\n sep(),\n row(\"Pool Address\", pool_link),\n row(\"Position Address\", pos_link),\n ])\n\n lower_disp = f\"{lp_lower:.6g}\" if lp_lower is not None else \"—\"\n upper_disp = f\"{lp_upper:.6g}\" if lp_upper is not None else \"—\"\n\n return f\"\"\"\u003c!DOCTYPE html>\n\u003chtml lang=\"en\">\n\u003chead>\n\u003cmeta charset=\"UTF-8\"/>\n\u003cmeta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\"/>\n\u003ctitle>LP Executor — {pair}\u003c/title>\n\u003cscript src=\"https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js\">\u003c/script>\n\u003cstyle>{DARK_CSS}\u003c/style>\n\u003c/head>\n\u003cbody>\n\u003cdiv class=\"container\">\n \u003ch1>{pair} — LP Executor\u003c/h1>\n \u003cdiv class=\"subtitle\">ID: {eid} · {connector} · {datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\")}\u003c/div>\n\n \u003cdiv class=\"kpi-grid\">\n \u003cdiv class=\"kpi-card\">\n \u003cdiv class=\"kpi-label\">Status\u003c/div>\n \u003cdiv class=\"kpi-value\" style=\"color:{sc};font-size:14px;\">{status}\u003c/div>\n \u003cdiv class=\"kpi-sub\">{close_type or state}\u003c/div>\n \u003c/div>\n \u003cdiv class=\"kpi-card\">\n \u003cdiv class=\"kpi-label\">Net PnL\u003c/div>\n \u003cdiv class=\"kpi-value\" style=\"color:{pc};\">{pnl:+.6f}\u003c/div>\n \u003cdiv class=\"kpi-sub\">{pnl_pct * 100:+.4f}%\u003c/div>\n \u003c/div>\n \u003cdiv class=\"kpi-card\">\n \u003cdiv class=\"kpi-label\">Fees Earned\u003c/div>\n \u003cdiv class=\"kpi-value\" style=\"color:#7c6df0;\">{fees_earned:.6f}\u003c/div>\n \u003cdiv class=\"kpi-sub\">quote currency\u003c/div>\n \u003c/div>\n \u003cdiv class=\"kpi-card\">\n \u003cdiv class=\"kpi-label\">Duration\u003c/div>\n \u003cdiv class=\"kpi-value\" style=\"color:#e8eaed;font-size:15px;\">{dur_str}\u003c/div>\n \u003cdiv class=\"kpi-sub\">OOR: {oor_str}\u003c/div>\n \u003c/div>\n \u003cdiv class=\"kpi-card\">\n \u003cdiv class=\"kpi-label\">LP Range\u003c/div>\n \u003cdiv class=\"kpi-value\" style=\"font-size:12px;color:#e8eaed;\">{lower_disp} – {upper_disp}\u003c/div>\n \u003cdiv class=\"kpi-sub\">side {side_str.split('—')[0].strip()}\u003c/div>\n \u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"layout\">\n \u003cdiv>\n {price_section_html}\n {balance_html}\n \u003cdiv class=\"section-title\">PnL Breakdown\u003c/div>\n \u003cdiv class=\"chart-wrap\">\u003cdiv class=\"chart-container\" style=\"height:190px;\">\n \u003ccanvas id=\"pnlChart\">\u003c/canvas>\n \u003c/div>\u003c/div>\n \u003c/div>\n \u003cdiv>\n \u003cdiv class=\"section-title\">Position Summary\u003c/div>\n \u003ctable class=\"summary-table\">\u003ctbody>{table_rows}\u003c/tbody>\u003c/table>\n \u003c/div>\n \u003c/div>\n\n \u003cdiv class=\"footer\">Hummingbot LP Executor · {eid[:16]}… · {datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\")}\u003c/div>\n\u003c/div>\n\u003cscript>\n{price_chart_js}\n{balance_js}\n(function() {{\n new Chart(document.getElementById('pnlChart').getContext('2d'), {{\n type:'bar',\n data:{{\n labels:{pnl_bkd_labels},\n datasets:[{{\n label:'Quote', data:{pnl_bkd_data},\n backgroundColor:{pnl_bkd_colors}, borderColor:{pnl_bkd_colors},\n borderWidth:1, borderRadius:4,\n }}]\n }},\n options:{{\n maintainAspectRatio:false, indexAxis:'y',\n plugins:{{legend:{{display:false}},\n tooltip:{{callbacks:{{label:(c)=>` ${{c.parsed.x.toFixed(8)}}`}}}}\n }},\n scales:{{\n x:{{ticks:{{color:'#6b7084',font:{{size:10}},callback:(v)=>v.toFixed(6)}},grid:{{color:'rgba(255,255,255,0.06)'}}}},\n y:{{ticks:{{color:'#8b8fa3',font:{{size:11}}}},grid:{{color:'rgba(255,255,255,0.03)'}}}}\n }}\n }}\n }});\n}})();\n\u003c/script>\n\u003c/body>\n\u003c/html>\"\"\"\n\n\n# ---------------------------------------------------------------------------\n# CLI\n# ---------------------------------------------------------------------------\n\ndef main():\n parser = argparse.ArgumentParser(\n description=\"Visualize a single LP executor as an HTML dashboard\",\n formatter_class=argparse.RawDescriptionHelpFormatter,\n epilog=__doc__,\n )\n parser.add_argument(\"--id\", dest=\"executor_id\", required=True,\n help=\"Executor ID\")\n parser.add_argument(\"--output\", \"-o\",\n help=\"Output HTML path (default: data/lp_executor_\u003cid[:10]>_\u003cts>.html)\")\n parser.add_argument(\"--no-open\", action=\"store_true\",\n help=\"Don't auto-open in browser\")\n args = parser.parse_args()\n\n cfg = get_api_config()\n hdrs = {\"Authorization\": make_auth_header(cfg)}\n\n print(f\"Fetching executor {args.executor_id} from {cfg['url']} ...\")\n try:\n ex = fetch_executor(cfg[\"url\"], hdrs, args.executor_id)\n except RuntimeError as e:\n print(f\"Error: {e}\", file=sys.stderr)\n return 1\n\n if not ex:\n print(\"Executor not found.\", file=sys.stderr)\n return 1\n\n pair = _s(ex.get(\"trading_pair\"))\n created_ts = parse_ts(ex.get(\"created_at\"))\n close_ts = _f(ex.get(\"close_timestamp\")) or parse_ts(ex.get(\"closed_at\"))\n\n # Fetch candles\n candles = None\n if pair and created_ts:\n end = close_ts if close_ts else int(time.time()) + 300\n print(f\"Fetching 5m candles for {pair} from KuCoin ...\")\n candles = fetch_candles(cfg[\"url\"], hdrs, pair,\n start_time=created_ts - 300,\n end_time=end + 300)\n if candles:\n print(f\" {len(candles)} candles loaded.\")\n else:\n print(\" No candles — price chart skipped.\")\n\n print(\"Building dashboard ...\")\n html = build_html(ex, candles)\n\n output_path = args.output\n if not output_path:\n os.makedirs(\"data\", exist_ok=True)\n ts = datetime.now().strftime(\"%Y%m%d_%H%M%S\")\n output_path = f\"data/lp_executor_{args.executor_id[:10]}_{ts}.html\"\n\n output_dir = os.path.dirname(output_path)\n if output_dir:\n os.makedirs(output_dir, exist_ok=True)\n\n with open(output_path, \"w\") as f:\n f.write(html)\n print(f\"Dashboard written to: {output_path}\")\n\n if not args.no_open:\n webbrowser.open(f\"file://{os.path.abspath(output_path)}\")\n print(\"Opened in browser.\")\n\n return 0\n\n\nif __name__ == \"__main__\":\n sys.exit(main())\n","content_type":"text/x-python; charset=utf-8","language":"python","size":26267,"content_sha256":"c53a618f294e8ddaf419559c73a01b523a8565e3a5dcba20bc4a597865407d53"},{"filename":"scripts/visualize_lp_positions.py","content":"#!/usr/bin/env python3\n\"\"\"\nGenerate an interactive HTML dashboard from LP position events (RangePositionUpdate table).\n\nGroups ADD/REMOVE events by position_address to show complete position lifecycle.\nDrill down on individual positions to see details.\n\nUsage:\n python visualize_lp_positions.py --pair \u003ctrading_pair> [--connector NAME] [--hours N] [--db \u003cpath>] [--output \u003cpath>] [--no-open]\n\nExamples:\n python visualize_lp_positions.py --pair SOL-USDC # all connectors for SOL-USDC\n python visualize_lp_positions.py --pair SOL-USDC --connector orca/clmm # orca only\n python visualize_lp_positions.py --pair SOL-USDC --hours 12 # last 12 hours\n python visualize_lp_positions.py --pair SOL-USDC --connector orca/clmm --hours 6\n python visualize_lp_positions.py --pair SOL-USDC --db data/my.sqlite\n python visualize_lp_positions.py --pair SOL-USDC --output report.html\n\"\"\"\n\nimport argparse\nimport json\nimport os\nimport sqlite3\nimport time\nimport webbrowser\nfrom datetime import datetime\nfrom pathlib import Path\nfrom typing import Optional\n\n# ---------------------------------------------------------------------------\n# DB helpers\n# ---------------------------------------------------------------------------\n\n\ndef find_db_with_lp_positions(data_dir: str = \"data\") -> str:\n \"\"\"Find database with RangePositionUpdate data, prioritizing by row count.\"\"\"\n data_path = Path(data_dir)\n if not data_path.exists():\n raise FileNotFoundError(f\"Data directory not found: {data_dir}\")\n\n candidates = []\n for db_file in data_path.glob(\"*.sqlite\"):\n try:\n conn = sqlite3.connect(str(db_file))\n cursor = conn.cursor()\n cursor.execute(\"SELECT name FROM sqlite_master WHERE type='table' AND name='RangePositionUpdate'\")\n if cursor.fetchone():\n cursor.execute(\"SELECT COUNT(*) FROM RangePositionUpdate\")\n count = cursor.fetchone()[0]\n if count > 0:\n candidates.append((str(db_file), count))\n conn.close()\n except Exception:\n continue\n\n if not candidates:\n raise FileNotFoundError(f\"No database with RangePositionUpdate data found in {data_dir}\")\n\n candidates.sort(key=lambda x: x[1], reverse=True)\n return candidates[0][0]\n\n\ndef get_table_columns(cursor) -> set:\n \"\"\"Get column names from RangePositionUpdate table.\"\"\"\n cursor.execute(\"PRAGMA table_info(RangePositionUpdate)\")\n return {row[1] for row in cursor.fetchall()}\n\n\ndef query_positions(db_path: str, connector: Optional[str] = None, trading_pair: Optional[str] = None, lookback_hours: Optional[float] = None) -> list[dict]:\n \"\"\"Query LP position events and group by position_address.\"\"\"\n conn = sqlite3.connect(db_path)\n cursor = conn.cursor()\n\n # Get available columns\n available_cols = get_table_columns(cursor)\n\n # Build query\n base_cols = [\"id\", \"hb_id\", \"timestamp\", \"tx_hash\", \"trade_fee\",\n \"config_file_path\", \"order_action\", \"trading_pair\", \"position_address\",\n \"lower_price\", \"upper_price\", \"mid_price\", \"base_amount\", \"quote_amount\",\n \"base_fee\", \"quote_fee\"]\n optional_cols = [\"market\", \"position_rent\", \"position_rent_refunded\", \"trade_fee_in_quote\"]\n\n select_cols = [c for c in base_cols if c in available_cols]\n for col in optional_cols:\n if col in available_cols:\n select_cols.append(col)\n\n query = f\"SELECT {', '.join(select_cols)} FROM RangePositionUpdate WHERE 1=1\"\n params = []\n\n if connector and \"market\" in available_cols:\n query += \" AND market = ?\"\n params.append(connector)\n\n if trading_pair:\n query += \" AND trading_pair = ?\"\n params.append(trading_pair)\n\n if lookback_hours:\n # timestamp is in milliseconds\n cutoff_ms = int((time.time() - lookback_hours * 3600) * 1000)\n query += \" AND timestamp >= ?\"\n params.append(cutoff_ms)\n\n query += \" ORDER BY timestamp ASC\"\n\n cursor.execute(query, params)\n rows = cursor.fetchall()\n conn.close()\n\n if not rows:\n return []\n\n # Convert to list of dicts\n events = []\n for row in rows:\n event = dict(zip(select_cols, row))\n # Convert timestamp from ms to seconds\n if event.get(\"timestamp\"):\n event[\"timestamp_ms\"] = event[\"timestamp\"]\n event[\"timestamp\"] = event[\"timestamp\"] / 1000\n events.append(event)\n\n # Group events by position_address\n positions_map = {}\n for event in events:\n pos_addr = event.get(\"position_address\") or \"unknown\"\n if pos_addr not in positions_map:\n positions_map[pos_addr] = {\"address\": pos_addr, \"events\": []}\n positions_map[pos_addr][\"events\"].append(event)\n\n # Build position records\n positions = []\n for pos_addr, pos_data in positions_map.items():\n events = pos_data[\"events\"]\n add_event = next((e for e in events if e.get(\"order_action\") == \"ADD\"), None)\n remove_event = next((e for e in events if e.get(\"order_action\") == \"REMOVE\"), None)\n\n if not add_event:\n continue # Skip positions without ADD event\n\n # Calculate PnL\n add_base = _float(add_event.get(\"base_amount\"))\n add_quote = _float(add_event.get(\"quote_amount\"))\n add_price = _float(add_event.get(\"mid_price\"))\n add_value = add_base * add_price + add_quote if add_price else add_quote\n\n remove_base = _float(remove_event.get(\"base_amount\")) if remove_event else 0\n remove_quote = _float(remove_event.get(\"quote_amount\")) if remove_event else 0\n remove_price = _float(remove_event.get(\"mid_price\")) if remove_event else add_price\n remove_value = remove_base * remove_price + remove_quote if remove_price else remove_quote\n\n base_fee = _float(remove_event.get(\"base_fee\")) if remove_event else 0\n quote_fee = _float(remove_event.get(\"quote_fee\")) if remove_event else 0\n fees_quote = base_fee * remove_price + quote_fee if remove_price else quote_fee\n\n # IL = (remove_value - add_value) without fees\n # PnL = IL + fees\n il = remove_value - add_value if remove_event else 0\n pnl = il + fees_quote\n\n # Rent\n rent_paid = _float(add_event.get(\"position_rent\", 0))\n rent_refunded = _float(remove_event.get(\"position_rent_refunded\", 0)) if remove_event else 0\n\n # Transaction fees (in quote currency)\n add_tx_fee = _float(add_event.get(\"trade_fee_in_quote\", 0))\n remove_tx_fee = _float(remove_event.get(\"trade_fee_in_quote\", 0)) if remove_event else 0\n total_tx_fee = add_tx_fee + remove_tx_fee\n\n # Duration\n add_ts = add_event.get(\"timestamp\", 0)\n remove_ts = remove_event.get(\"timestamp\", 0) if remove_event else 0\n duration = int(remove_ts - add_ts) if remove_ts > add_ts else 0\n\n # Determine status\n status = \"CLOSED\" if remove_event else \"OPEN\"\n\n positions.append({\n \"address\": pos_addr,\n \"trading_pair\": add_event.get(\"trading_pair\", \"\"),\n \"connector\": add_event.get(\"market\", \"\"),\n \"config_file\": add_event.get(\"config_file_path\", \"\"),\n\n # Timing\n \"add_ts\": add_ts,\n \"add_datetime\": datetime.fromtimestamp(add_ts).strftime(\"%Y-%m-%d %H:%M:%S\") if add_ts else \"\",\n \"remove_ts\": remove_ts,\n \"remove_datetime\": datetime.fromtimestamp(remove_ts).strftime(\"%Y-%m-%d %H:%M:%S\") if remove_ts else \"\",\n \"duration\": duration,\n \"status\": status,\n\n # Price bounds\n \"lower_price\": _float(add_event.get(\"lower_price\")),\n \"upper_price\": _float(add_event.get(\"upper_price\")),\n \"add_price\": add_price,\n \"remove_price\": remove_price,\n\n # Amounts at ADD\n \"add_base\": add_base,\n \"add_quote\": add_quote,\n \"add_value\": add_value,\n\n # Amounts at REMOVE\n \"remove_base\": remove_base,\n \"remove_quote\": remove_quote,\n \"remove_value\": remove_value,\n\n # Fees\n \"base_fee\": base_fee,\n \"quote_fee\": quote_fee,\n \"fees_quote\": fees_quote,\n\n # PnL\n \"il\": il,\n \"pnl\": pnl,\n \"pnl_pct\": (pnl / add_value * 100) if add_value > 0 else 0,\n\n # Rent & tx fees\n \"rent_paid\": rent_paid,\n \"rent_refunded\": rent_refunded,\n \"tx_fee_quote\": total_tx_fee,\n\n # TX hashes\n \"add_tx\": add_event.get(\"tx_hash\", \"\"),\n \"remove_tx\": remove_event.get(\"tx_hash\", \"\") if remove_event else \"\",\n\n # Raw events for detail view\n \"events\": events,\n })\n\n # Sort by add timestamp (newest first)\n positions.sort(key=lambda p: p[\"add_ts\"], reverse=True)\n\n return positions\n\n\n# ---------------------------------------------------------------------------\n# Transform to chart data\n# ---------------------------------------------------------------------------\n\ndef positions_to_chart_data(positions: list[dict]) -> list[dict]:\n \"\"\"Convert position records into chart-ready JSON.\"\"\"\n # Sort chronologically for cumulative calculation\n sorted_positions = sorted(positions, key=lambda p: p[\"add_ts\"])\n\n cum_pnl = 0.0\n cum_fees = 0.0\n records = []\n\n for idx, p in enumerate(sorted_positions):\n pnl = p[\"pnl\"]\n fees = p[\"fees_quote\"]\n\n # Only add to cumulative if position is closed\n if p[\"status\"] == \"CLOSED\":\n cum_pnl += pnl\n cum_fees += fees\n\n # Determine side: BUY if more quote (expecting to buy base), SELL if more base\n # Compare quote value vs base value at time of ADD\n add_base_value = p[\"add_base\"] * p[\"add_price\"] if p[\"add_price\"] else 0\n side = \"BUY\" if p[\"add_quote\"] > add_base_value else \"SELL\"\n\n records.append({\n \"idx\": idx,\n \"address\": p[\"address\"],\n \"trading_pair\": p[\"trading_pair\"],\n \"connector\": p[\"connector\"],\n \"side\": side,\n\n # Timing\n \"add_ts\": p[\"add_datetime\"].replace(\" \", \"T\") if p[\"add_datetime\"] else \"\",\n \"remove_ts\": p[\"remove_datetime\"].replace(\" \", \"T\") if p[\"remove_datetime\"] else \"\",\n \"dur\": p[\"duration\"],\n \"status\": p[\"status\"],\n\n # Prices\n \"lower\": round(p[\"lower_price\"], 6),\n \"upper\": round(p[\"upper_price\"], 6),\n \"add_price\": round(p[\"add_price\"], 6),\n \"remove_price\": round(p[\"remove_price\"], 6),\n\n # Amounts\n \"add_base\": round(p[\"add_base\"], 6),\n \"add_quote\": round(p[\"add_quote\"], 6),\n \"add_value\": round(p[\"add_value\"], 4),\n \"remove_base\": round(p[\"remove_base\"], 6),\n \"remove_quote\": round(p[\"remove_quote\"], 6),\n \"remove_value\": round(p[\"remove_value\"], 4),\n\n # Fees\n \"base_fee\": round(p[\"base_fee\"], 6),\n \"quote_fee\": round(p[\"quote_fee\"], 6),\n \"fees\": round(fees, 6),\n \"cum_fees\": round(cum_fees, 6),\n\n # PnL\n \"il\": round(p[\"il\"], 6),\n \"pnl\": round(pnl, 6),\n \"pnl_pct\": round(p[\"pnl_pct\"], 4),\n \"cum_pnl\": round(cum_pnl, 6),\n\n # Rent & tx fees\n \"rent_paid\": round(p[\"rent_paid\"], 6),\n \"rent_refunded\": round(p[\"rent_refunded\"], 6),\n \"tx_fee_quote\": round(p[\"tx_fee_quote\"], 6),\n\n # TX\n \"add_tx\": p[\"add_tx\"],\n \"remove_tx\": p[\"remove_tx\"],\n })\n\n return records\n\n\n# ---------------------------------------------------------------------------\n# HTML generation\n# ---------------------------------------------------------------------------\n\ndef generate_html(chart_data: list[dict], meta: dict) -> str:\n \"\"\"Build a self-contained HTML file with the dashboard.\"\"\"\n data_json = json.dumps(chart_data)\n meta_json = json.dumps(meta)\n\n return f\"\"\"\u003c!DOCTYPE html>\n\u003chtml lang=\"en\">\n\u003chead>\n\u003cmeta charset=\"UTF-8\" />\n\u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n\u003ctitle>LP Positions Dashboard — {meta.get(\"trading_pair\", \"\")}\u003c/title>\n\u003cstyle>\n * {{ margin: 0; padding: 0; box-sizing: border-box; }}\n body {{ background: #0f1019; color: #e8eaed; font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace; }}\n #root {{ min-height: 100vh; }}\n\u003c/style>\n\u003clink href=\"https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&display=swap\" rel=\"stylesheet\" />\n\u003cscript src=\"https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js\">\u003c/script>\n\u003cscript src=\"https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js\">\u003c/script>\n\u003cscript src=\"https://cdnjs.cloudflare.com/ajax/libs/prop-types/15.8.1/prop-types.min.js\">\u003c/script>\n\u003cscript src=\"https://cdnjs.cloudflare.com/ajax/libs/recharts/2.12.7/Recharts.min.js\">\u003c/script>\n\u003cscript src=\"https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.23.9/babel.min.js\">\u003c/script>\n\u003c/head>\n\u003cbody>\n\u003cdiv id=\"root\">\u003c/div>\n\u003cscript>\n window.__LP_DATA__ = {data_json};\n window.__LP_META__ = {meta_json};\n\u003c/script>\n\u003cscript type=\"text/babel\">\n{DASHBOARD_JSX}\n\u003c/script>\n\u003c/body>\n\u003c/html>\"\"\"\n\n\nDASHBOARD_JSX = r\"\"\"\nconst { XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ComposedChart, Line, ReferenceArea } = Recharts;\n\nconst DATA = window.__LP_DATA__;\nconst META = window.__LP_META__;\n\nconst fmt = (n, d = 4) => n?.toFixed(d) ?? \"—\";\nconst fmtTime = (ts) => {\n if (!ts) return \"\";\n const d = new Date(ts);\n return d.toLocaleTimeString(\"en-US\", { hour: \"2-digit\", minute: \"2-digit\", hour12: false });\n};\nconst fmtDateTime = (iso) => iso ? iso.replace(\"T\", \" \") : \"—\";\nconst fmtDuration = (s) => {\n if (!s) return \"0s\";\n if (s \u003c 60) return `${s}s`;\n if (s \u003c 3600) return `${Math.floor(s/60)}m ${s%60}s`;\n return `${Math.floor(s/3600)}h ${Math.floor((s%3600)/60)}m`;\n};\nconst shortenAddr = (addr) => addr ? `${addr.substring(0, 6)}...${addr.substring(addr.length - 4)}` : \"—\";\n\nfunction StatCard({ label, value, sub, accent }) {\n return React.createElement(\"div\", {\n style: {\n background: \"rgba(255,255,255,0.03)\", border: \"1px solid rgba(255,255,255,0.06)\",\n borderRadius: 10, padding: \"14px 18px\", minWidth: 0,\n }\n },\n React.createElement(\"div\", {\n style: { fontSize: 11, color: \"#8b8fa3\", letterSpacing: \"0.05em\", textTransform: \"uppercase\", marginBottom: 6 }\n }, label),\n React.createElement(\"div\", {\n style: { fontSize: 22, fontWeight: 600, color: accent || \"#e8eaed\" }\n }, value),\n sub && React.createElement(\"div\", { style: { fontSize: 11, color: \"#6b7084\", marginTop: 4 } }, sub)\n );\n}\n\nfunction SectionTitle({ children }) {\n return React.createElement(\"h3\", {\n style: {\n fontSize: 13, fontWeight: 600, color: \"#8b8fa3\", textTransform: \"uppercase\",\n letterSpacing: \"0.08em\", margin: \"32px 0 14px\",\n }\n }, children);\n}\n\nfunction DetailRow({ label, value, color, mono }) {\n const e = React.createElement;\n return e(\"div\", { style: { display: \"flex\", justifyContent: \"space-between\", padding: \"4px 0\", borderBottom: \"1px solid rgba(255,255,255,0.03)\" } },\n e(\"span\", { style: { color: \"#6b7084\", fontSize: 11 } }, label),\n e(\"span\", { style: { color: color || \"#e8eaed\", fontSize: 11, fontWeight: 500, fontFamily: mono ? \"monospace\" : \"inherit\", wordBreak: mono ? \"break-all\" : \"normal\" } }, value)\n );\n}\n\nfunction PositionDetail({ position, onClose }) {\n const e = React.createElement;\n if (!position) return null;\n\n const d = position;\n const pnlColor = d.pnl >= 0 ? \"#4ecdc4\" : \"#e85d75\";\n\n return e(\"div\", {\n style: {\n position: \"fixed\", top: 0, right: 0, width: 420, height: \"100vh\",\n background: \"#12141f\", borderLeft: \"1px solid rgba(255,255,255,0.08)\",\n overflowY: \"auto\", zIndex: 1000, boxShadow: \"-4px 0 20px rgba(0,0,0,0.3)\",\n }\n },\n // Header\n e(\"div\", { style: { padding: \"16px 20px\", borderBottom: \"1px solid rgba(255,255,255,0.06)\", display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\", position: \"sticky\", top: 0, background: \"#12141f\", zIndex: 1 } },\n e(\"div\", null,\n e(\"div\", { style: { fontSize: 14, fontWeight: 600, color: \"#e8eaed\" } }, `Position #${d.idx + 1}`),\n e(\"div\", { style: { fontSize: 10, color: \"#555870\", marginTop: 2, fontFamily: \"monospace\" } }, shortenAddr(d.address))\n ),\n e(\"button\", {\n onClick: onClose,\n style: { background: \"none\", border: \"none\", color: \"#6b7084\", fontSize: 20, cursor: \"pointer\", padding: \"4px 8px\" }\n }, \"×\")\n ),\n\n // Content\n e(\"div\", { style: { padding: \"16px 20px\" } },\n // Trading pair and connector prominent\n e(\"div\", { style: { marginBottom: 16 } },\n e(\"div\", { style: { fontSize: 20, fontWeight: 700, color: \"#4ecdc4\", marginBottom: 4 } }, d.trading_pair),\n e(\"div\", { style: { fontSize: 12, color: \"#8b8fa3\" } }, d.connector || \"Unknown connector\"),\n ),\n // Status badges\n e(\"div\", { style: { display: \"flex\", gap: 8, marginBottom: 16 } },\n e(\"span\", { style: { fontSize: 10, padding: \"3px 10px\", borderRadius: 12, background: d.status === \"CLOSED\" ? \"rgba(78,205,196,0.1)\" : \"rgba(240,198,68,0.1)\", color: d.status === \"CLOSED\" ? \"#4ecdc4\" : \"#f0c644\" } }, d.status),\n e(\"span\", { style: { fontSize: 10, padding: \"3px 10px\", borderRadius: 12, background: d.side === \"BUY\" ? \"rgba(78,205,196,0.15)\" : \"rgba(232,93,117,0.15)\", color: d.side === \"BUY\" ? \"#4ecdc4\" : \"#e85d75\" } }, d.side),\n ),\n\n // PnL highlight\n e(\"div\", { style: { background: \"rgba(255,255,255,0.02)\", borderRadius: 10, padding: \"16px\", marginBottom: 16, textAlign: \"center\" } },\n e(\"div\", { style: { fontSize: 11, color: \"#6b7084\", marginBottom: 4 } }, \"Net PnL\"),\n e(\"div\", { style: { fontSize: 28, fontWeight: 700, color: pnlColor } }, `${fmt(d.pnl, 6)}`),\n e(\"div\", { style: { fontSize: 12, color: pnlColor, marginTop: 4 } }, `${fmt(d.pnl_pct, 4)}%`)\n ),\n\n // Timing section\n e(\"div\", { style: { fontSize: 11, fontWeight: 600, color: \"#8b8fa3\", marginBottom: 8, marginTop: 16 } }, \"TIMING\"),\n e(DetailRow, { label: \"Opened\", value: fmtDateTime(d.add_ts) }),\n e(DetailRow, { label: \"Closed\", value: d.remove_ts ? fmtDateTime(d.remove_ts) : \"Still open\" }),\n e(DetailRow, { label: \"Duration\", value: fmtDuration(d.dur) }),\n\n // Price section\n e(\"div\", { style: { fontSize: 11, fontWeight: 600, color: \"#8b8fa3\", marginBottom: 8, marginTop: 20 } }, \"PRICE BOUNDS\"),\n e(DetailRow, { label: \"Lower Price\", value: fmt(d.lower, 6) }),\n e(DetailRow, { label: \"Upper Price\", value: fmt(d.upper, 6) }),\n e(DetailRow, { label: \"Price at ADD\", value: fmt(d.add_price, 6) }),\n e(DetailRow, { label: \"Price at REMOVE\", value: d.remove_ts ? fmt(d.remove_price, 6) : \"—\" }),\n\n // ADD LIQUIDITY section\n e(\"div\", { style: { fontSize: 11, fontWeight: 600, color: \"#4ecdc4\", marginBottom: 8, marginTop: 20, display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\" } },\n e(\"span\", null, \"ADD LIQUIDITY\"),\n d.add_tx && e(\"a\", {\n href: `https://solscan.io/tx/${d.add_tx}`,\n target: \"_blank\",\n rel: \"noopener noreferrer\",\n style: { fontSize: 10, color: \"#7c6df0\", textDecoration: \"none\" }\n }, \"View TX ↗\")\n ),\n e(DetailRow, { label: \"Base Amount\", value: fmt(d.add_base, 6) }),\n e(DetailRow, { label: \"Quote Amount\", value: fmt(d.add_quote, 6) }),\n e(DetailRow, { label: \"Total Value\", value: `${fmt(d.add_value, 4)}` }),\n e(DetailRow, { label: \"Price\", value: fmt(d.add_price, 6) }),\n\n // REMOVE LIQUIDITY section\n d.status === \"CLOSED\" && e(\"div\", null,\n e(\"div\", { style: { fontSize: 11, fontWeight: 600, color: \"#e85d75\", marginBottom: 8, marginTop: 20, display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\" } },\n e(\"span\", null, \"REMOVE LIQUIDITY\"),\n d.remove_tx && e(\"a\", {\n href: `https://solscan.io/tx/${d.remove_tx}`,\n target: \"_blank\",\n rel: \"noopener noreferrer\",\n style: { fontSize: 10, color: \"#7c6df0\", textDecoration: \"none\" }\n }, \"View TX ↗\")\n ),\n e(DetailRow, { label: \"Base Amount\", value: fmt(d.remove_base, 6) }),\n e(DetailRow, { label: \"Quote Amount\", value: fmt(d.remove_quote, 6) }),\n e(DetailRow, { label: \"Total Value\", value: `${fmt(d.remove_value, 4)}` }),\n e(DetailRow, { label: \"Price\", value: fmt(d.remove_price, 6) }),\n ),\n\n // Fees section\n e(\"div\", { style: { fontSize: 11, fontWeight: 600, color: \"#8b8fa3\", marginBottom: 8, marginTop: 20 } }, \"FEES EARNED\"),\n e(DetailRow, { label: \"Base Fee\", value: fmt(d.base_fee, 6) }),\n e(DetailRow, { label: \"Quote Fee\", value: fmt(d.quote_fee, 6) }),\n e(DetailRow, { label: \"Total Fees (Quote)\", value: `${fmt(d.fees, 6)}`, color: \"#7c6df0\" }),\n\n // PnL breakdown\n e(\"div\", { style: { fontSize: 11, fontWeight: 600, color: \"#8b8fa3\", marginBottom: 8, marginTop: 20 } }, \"PNL BREAKDOWN\"),\n e(DetailRow, { label: \"IL (value change)\", value: `${fmt(d.il, 6)}`, color: d.il >= 0 ? \"#4ecdc4\" : \"#e85d75\" }),\n e(DetailRow, { label: \"Fees Earned\", value: `${fmt(d.fees, 6)}`, color: \"#7c6df0\" }),\n e(DetailRow, { label: \"Net PnL\", value: `${fmt(d.pnl, 6)}`, color: pnlColor }),\n\n // Rent & tx fee section\n (d.rent_paid > 0 || d.rent_refunded > 0 || d.tx_fee_quote > 0) && e(\"div\", null,\n e(\"div\", { style: { fontSize: 11, fontWeight: 600, color: \"#8b8fa3\", marginBottom: 8, marginTop: 20 } }, \"RENT & TX FEES\"),\n e(DetailRow, { label: \"Rent Paid\", value: `${fmt(d.rent_paid, 6)} SOL`, color: \"#e85d75\" }),\n e(DetailRow, { label: \"Rent Refunded\", value: `${fmt(d.rent_refunded, 6)} SOL`, color: \"#4ecdc4\" }),\n d.tx_fee_quote > 0 && e(DetailRow, { label: \"Transaction Fees\", value: `${fmt(d.tx_fee_quote, 6)}`, color: \"#f0c644\" }),\n ),\n\n // Cumulative at close\n d.status === \"CLOSED\" && e(\"div\", null,\n e(\"div\", { style: { fontSize: 11, fontWeight: 600, color: \"#8b8fa3\", marginBottom: 8, marginTop: 20 } }, \"CUMULATIVE AT CLOSE\"),\n e(DetailRow, { label: \"Cumulative PnL\", value: `${fmt(d.cum_pnl, 6)}`, color: d.cum_pnl >= 0 ? \"#4ecdc4\" : \"#e85d75\" }),\n e(DetailRow, { label: \"Cumulative Fees\", value: `${fmt(d.cum_fees, 6)}`, color: \"#7c6df0\" }),\n ),\n\n // Position address\n e(\"div\", { style: { marginTop: 20 } },\n e(\"div\", { style: { fontSize: 11, fontWeight: 600, color: \"#8b8fa3\", marginBottom: 8 } }, \"POSITION ADDRESS\"),\n e(\"div\", { style: { fontSize: 10, color: \"#555870\", fontFamily: \"monospace\", wordBreak: \"break-all\" } }, d.address)\n ),\n )\n );\n}\n\nfunction PositionTable({ data, selectedIdx, onSelect }) {\n const e = React.createElement;\n const [sortKey, setSortKey] = React.useState(\"idx\");\n const [sortDir, setSortDir] = React.useState(-1); // Newest first\n const [filterStatus, setFilterStatus] = React.useState(\"all\");\n const [filterSide, setFilterSide] = React.useState(\"all\");\n const [filterConnector, setFilterConnector] = React.useState(\"all\");\n\n // Get unique connectors from data\n const connectors = React.useMemo(() => {\n const set = new Set(data.map(d => d.connector).filter(Boolean));\n return Array.from(set).sort();\n }, [data]);\n\n const filtered = React.useMemo(() => {\n let result = [...data];\n if (filterStatus !== \"all\") result = result.filter(d => d.status === filterStatus);\n if (filterSide !== \"all\") result = result.filter(d => d.side === filterSide);\n if (filterConnector !== \"all\") result = result.filter(d => d.connector === filterConnector);\n result.sort((a, b) => (a[sortKey] > b[sortKey] ? 1 : -1) * sortDir);\n return result;\n }, [data, filterStatus, filterSide, filterConnector, sortKey, sortDir]);\n\n const handleSort = (key) => {\n if (sortKey === key) setSortDir(-sortDir);\n else { setSortKey(key); setSortDir(1); }\n };\n\n const thStyle = { padding: \"8px 6px\", textAlign: \"left\", fontSize: 10, color: \"#6b7084\", cursor: \"pointer\", userSelect: \"none\", borderBottom: \"1px solid rgba(255,255,255,0.06)\" };\n const tdStyle = { padding: \"8px 6px\", fontSize: 11, borderBottom: \"1px solid rgba(255,255,255,0.03)\" };\n\n const sortIcon = (key) => sortKey === key ? (sortDir === 1 ? \" ↑\" : \" ↓\") : \"\";\n\n const filterBtn = (value, label, currentFilter, setFilter) => e(\"button\", {\n onClick: () => setFilter(value),\n style: {\n fontSize: 9, padding: \"3px 8px\", borderRadius: 10, cursor: \"pointer\", fontFamily: \"inherit\",\n border: `1px solid ${currentFilter === value ? \"#4ecdc4\" : \"rgba(255,255,255,0.08)\"}`,\n background: currentFilter === value ? \"rgba(78,205,196,0.12)\" : \"transparent\",\n color: currentFilter === value ? \"#4ecdc4\" : \"#6b7084\", marginRight: 4,\n }\n }, label);\n\n return e(\"div\", null,\n // Filters\n e(\"div\", { style: { display: \"flex\", gap: 16, marginBottom: 12, flexWrap: \"wrap\" } },\n connectors.length > 1 && e(\"div\", { style: { display: \"flex\", alignItems: \"center\", gap: 4 } },\n e(\"span\", { style: { fontSize: 10, color: \"#555870\", marginRight: 4 } }, \"Connector:\"),\n filterBtn(\"all\", \"All\", filterConnector, setFilterConnector),\n ...connectors.map(c => filterBtn(c, c.split(\"/\")[0], filterConnector, setFilterConnector))\n ),\n e(\"div\", { style: { display: \"flex\", alignItems: \"center\", gap: 4 } },\n e(\"span\", { style: { fontSize: 10, color: \"#555870\", marginRight: 4 } }, \"Side:\"),\n filterBtn(\"all\", \"All\", filterSide, setFilterSide),\n filterBtn(\"BUY\", \"Buy\", filterSide, setFilterSide),\n filterBtn(\"SELL\", \"Sell\", filterSide, setFilterSide),\n ),\n e(\"div\", { style: { display: \"flex\", alignItems: \"center\", gap: 4 } },\n e(\"span\", { style: { fontSize: 10, color: \"#555870\", marginRight: 4 } }, \"Status:\"),\n filterBtn(\"all\", \"All\", filterStatus, setFilterStatus),\n filterBtn(\"CLOSED\", \"Closed\", filterStatus, setFilterStatus),\n filterBtn(\"OPEN\", \"Open\", filterStatus, setFilterStatus),\n ),\n e(\"span\", { style: { fontSize: 10, color: \"#555870\", marginLeft: \"auto\" } }, `${filtered.length} positions`)\n ),\n\n // Table\n e(\"div\", { style: { overflowX: \"auto\", background: \"rgba(255,255,255,0.02)\", borderRadius: 10, border: \"1px solid rgba(255,255,255,0.05)\" } },\n e(\"table\", { style: { width: \"100%\", borderCollapse: \"collapse\", minWidth: 900 } },\n e(\"thead\", null,\n e(\"tr\", null,\n e(\"th\", { style: thStyle, onClick: () => handleSort(\"idx\") }, \"#\" + sortIcon(\"idx\")),\n e(\"th\", { style: thStyle, onClick: () => handleSort(\"side\") }, \"Side\" + sortIcon(\"side\")),\n e(\"th\", { style: thStyle, onClick: () => handleSort(\"connector\") }, \"Connector\" + sortIcon(\"connector\")),\n e(\"th\", { style: thStyle, onClick: () => handleSort(\"add_ts\") }, \"Opened\" + sortIcon(\"add_ts\")),\n e(\"th\", { style: thStyle, onClick: () => handleSort(\"dur\") }, \"Duration\" + sortIcon(\"dur\")),\n e(\"th\", { style: thStyle, onClick: () => handleSort(\"add_value\") }, \"Value\" + sortIcon(\"add_value\")),\n e(\"th\", { style: thStyle, onClick: () => handleSort(\"pnl\") }, \"PnL\" + sortIcon(\"pnl\")),\n e(\"th\", { style: thStyle, onClick: () => handleSort(\"pnl_pct\") }, \"PnL %\" + sortIcon(\"pnl_pct\")),\n e(\"th\", { style: thStyle, onClick: () => handleSort(\"fees\") }, \"Fees\" + sortIcon(\"fees\")),\n e(\"th\", { style: thStyle }, \"Status\"),\n )\n ),\n e(\"tbody\", null,\n ...filtered.map(d => e(\"tr\", {\n key: d.idx,\n onClick: () => onSelect(d.idx),\n style: {\n cursor: \"pointer\",\n background: selectedIdx === d.idx ? \"rgba(78,205,196,0.08)\" : \"transparent\",\n transition: \"background 0.15s\",\n },\n onMouseEnter: (ev) => ev.currentTarget.style.background = selectedIdx === d.idx ? \"rgba(78,205,196,0.12)\" : \"rgba(255,255,255,0.02)\",\n onMouseLeave: (ev) => ev.currentTarget.style.background = selectedIdx === d.idx ? \"rgba(78,205,196,0.08)\" : \"transparent\",\n },\n e(\"td\", { style: { ...tdStyle, color: \"#8b8fa3\" } }, d.idx + 1),\n e(\"td\", { style: { ...tdStyle, color: d.side === \"BUY\" ? \"#4ecdc4\" : \"#e85d75\", fontWeight: 500 } }, d.side),\n e(\"td\", { style: { ...tdStyle, fontSize: 10 } }, d.connector ? d.connector.split(\"/\")[0] : \"—\"),\n e(\"td\", { style: { ...tdStyle, fontFamily: \"monospace\", fontSize: 10 } }, fmtDateTime(d.add_ts)),\n e(\"td\", { style: tdStyle }, fmtDuration(d.dur)),\n e(\"td\", { style: tdStyle }, `${fmt(d.add_value, 2)}`),\n e(\"td\", { style: { ...tdStyle, color: d.pnl >= 0 ? \"#4ecdc4\" : \"#e85d75\", fontWeight: 500 } }, `${fmt(d.pnl, 4)}`),\n e(\"td\", { style: { ...tdStyle, color: d.pnl_pct >= 0 ? \"#4ecdc4\" : \"#e85d75\" } }, `${fmt(d.pnl_pct, 3)}%`),\n e(\"td\", { style: { ...tdStyle, color: \"#7c6df0\" } }, `${fmt(d.fees, 4)}`),\n e(\"td\", { style: tdStyle },\n e(\"span\", { style: { fontSize: 9, padding: \"2px 6px\", borderRadius: 8, background: d.status === \"CLOSED\" ? \"rgba(78,205,196,0.1)\" : \"rgba(240,198,68,0.1)\", color: d.status === \"CLOSED\" ? \"#4ecdc4\" : \"#f0c644\" } }, d.status)\n ),\n ))\n )\n )\n )\n );\n}\n\n// Custom chart component for position ranges with price line\nfunction PositionRangeChart({ data, onSelectPosition, tradingPair }) {\n const e = React.createElement;\n const containerRef = React.useRef(null);\n const [dimensions, setDimensions] = React.useState({ width: 0, height: 400 });\n\n React.useEffect(() => {\n if (containerRef.current) {\n const resizeObserver = new ResizeObserver(entries => {\n for (let entry of entries) {\n setDimensions({ width: entry.contentRect.width, height: 400 });\n }\n });\n resizeObserver.observe(containerRef.current);\n return () => resizeObserver.disconnect();\n }\n }, []);\n\n const { width, height } = dimensions;\n const margin = { top: 20, right: 60, bottom: 40, left: 70 };\n const innerWidth = width - margin.left - margin.right;\n const innerHeight = height - margin.top - margin.bottom;\n\n // Compute time and price domains\n const { minTime, maxTime, minPrice, maxPrice, pricePoints } = React.useMemo(() => {\n if (!data.length) return { minTime: 0, maxTime: 1, minPrice: 0, maxPrice: 1, pricePoints: [] };\n\n let minT = Infinity, maxT = -Infinity;\n let minP = Infinity, maxP = -Infinity;\n const points = [];\n\n data.forEach(d => {\n const addTs = new Date(d.add_ts).getTime();\n const removeTs = d.remove_ts ? new Date(d.remove_ts).getTime() : Date.now();\n\n minT = Math.min(minT, addTs);\n maxT = Math.max(maxT, removeTs);\n minP = Math.min(minP, d.lower, d.add_price);\n maxP = Math.max(maxP, d.upper, d.add_price);\n\n // Add price points for the line\n points.push({ time: addTs, price: d.add_price });\n if (d.remove_ts && d.remove_price) {\n points.push({ time: removeTs, price: d.remove_price });\n minP = Math.min(minP, d.remove_price);\n maxP = Math.max(maxP, d.remove_price);\n }\n });\n\n // Sort price points by time\n points.sort((a, b) => a.time - b.time);\n\n // Add padding to price range\n const priceRange = maxP - minP;\n minP -= priceRange * 0.05;\n maxP += priceRange * 0.05;\n\n return { minTime: minT, maxTime: maxT, minPrice: minP, maxPrice: maxP, pricePoints: points };\n }, [data]);\n\n // Scale functions\n const xScale = (t) => margin.left + ((t - minTime) / (maxTime - minTime)) * innerWidth;\n const yScale = (p) => margin.top + innerHeight - ((p - minPrice) / (maxPrice - minPrice)) * innerHeight;\n\n // Generate Y axis ticks\n const yTicks = React.useMemo(() => {\n const ticks = [];\n const range = maxPrice - minPrice;\n const step = range / 5;\n for (let i = 0; i \u003c= 5; i++) {\n ticks.push(minPrice + step * i);\n }\n return ticks;\n }, [minPrice, maxPrice]);\n\n // Generate X axis ticks\n const xTicks = React.useMemo(() => {\n const ticks = [];\n const range = maxTime - minTime;\n const step = range / 6;\n for (let i = 0; i \u003c= 6; i++) {\n ticks.push(minTime + step * i);\n }\n return ticks;\n }, [minTime, maxTime]);\n\n // Build price line path\n const pricePath = React.useMemo(() => {\n if (pricePoints.length \u003c 2) return \"\";\n return pricePoints.map((p, i) =>\n `${i === 0 ? \"M\" : \"L\"} ${xScale(p.time)} ${yScale(p.price)}`\n ).join(\" \");\n }, [pricePoints, xScale, yScale]);\n\n if (width === 0) {\n return e(\"div\", { ref: containerRef, style: { width: \"100%\", height: 400 } });\n }\n\n return e(\"div\", { ref: containerRef, style: { width: \"100%\", height: 400, background: \"rgba(255,255,255,0.02)\", borderRadius: 10, border: \"1px solid rgba(255,255,255,0.05)\" } },\n e(\"svg\", { width, height, style: { display: \"block\" } },\n // Grid lines\n ...yTicks.map((tick, i) => e(\"line\", {\n key: `y-${i}`,\n x1: margin.left,\n y1: yScale(tick),\n x2: width - margin.right,\n y2: yScale(tick),\n stroke: \"rgba(255,255,255,0.04)\",\n strokeDasharray: \"3,3\"\n })),\n ...xTicks.map((tick, i) => e(\"line\", {\n key: `x-${i}`,\n x1: xScale(tick),\n y1: margin.top,\n x2: xScale(tick),\n y2: height - margin.bottom,\n stroke: \"rgba(255,255,255,0.04)\",\n strokeDasharray: \"3,3\"\n })),\n\n // Position range bars\n ...data.map((d, i) => {\n const addTs = new Date(d.add_ts).getTime();\n const removeTs = d.remove_ts ? new Date(d.remove_ts).getTime() : Date.now();\n const x1 = xScale(addTs);\n const x2 = xScale(removeTs);\n const y1 = yScale(d.upper);\n const y2 = yScale(d.lower);\n const fillColor = d.side === \"BUY\" ? \"rgba(78,205,196,0.25)\" : \"rgba(232,93,117,0.25)\";\n const strokeColor = d.side === \"BUY\" ? \"#4ecdc4\" : \"#e85d75\";\n\n return e(\"rect\", {\n key: `pos-${i}`,\n x: x1,\n y: y1,\n width: Math.max(x2 - x1, 2),\n height: Math.max(y2 - y1, 1),\n fill: fillColor,\n stroke: strokeColor,\n strokeWidth: 1,\n strokeOpacity: 0.5,\n cursor: \"pointer\",\n onClick: () => onSelectPosition(d.idx),\n });\n }),\n\n // Price line\n e(\"path\", {\n d: pricePath,\n fill: \"none\",\n stroke: \"#f0c644\",\n strokeWidth: 2,\n strokeLinejoin: \"round\",\n strokeLinecap: \"round\",\n }),\n\n // Price dots at position open/close\n ...pricePoints.map((p, i) => e(\"circle\", {\n key: `dot-${i}`,\n cx: xScale(p.time),\n cy: yScale(p.price),\n r: 3,\n fill: \"#f0c644\",\n })),\n\n // Y axis\n e(\"line\", { x1: margin.left, y1: margin.top, x2: margin.left, y2: height - margin.bottom, stroke: \"rgba(255,255,255,0.1)\" }),\n ...yTicks.map((tick, i) => e(\"text\", {\n key: `yl-${i}`,\n x: margin.left - 8,\n y: yScale(tick),\n fill: \"#555870\",\n fontSize: 10,\n textAnchor: \"end\",\n dominantBaseline: \"middle\",\n }, tick.toFixed(tick > 100 ? 0 : tick > 1 ? 2 : 4))),\n\n // X axis\n e(\"line\", { x1: margin.left, y1: height - margin.bottom, x2: width - margin.right, y2: height - margin.bottom, stroke: \"rgba(255,255,255,0.1)\" }),\n ...xTicks.map((tick, i) => e(\"text\", {\n key: `xl-${i}`,\n x: xScale(tick),\n y: height - margin.bottom + 16,\n fill: \"#555870\",\n fontSize: 10,\n textAnchor: \"middle\",\n }, fmtTime(tick))),\n\n // Y axis label\n e(\"text\", {\n x: 14,\n y: height / 2,\n fill: \"#6b7084\",\n fontSize: 11,\n textAnchor: \"middle\",\n transform: `rotate(-90, 14, ${height / 2})`,\n }, `${tradingPair} Price`),\n\n // Legend\n e(\"line\", { x1: width - margin.right - 200, y1: margin.top + 6, x2: width - margin.right - 180, y2: margin.top + 6, stroke: \"#f0c644\", strokeWidth: 2 }),\n e(\"text\", { x: width - margin.right - 175, y: margin.top + 9, fill: \"#8b8fa3\", fontSize: 10 }, `${tradingPair} Price`),\n e(\"rect\", { x: width - margin.right - 95, y: margin.top, width: 12, height: 12, fill: \"rgba(78,205,196,0.25)\", stroke: \"#4ecdc4\", strokeWidth: 1 }),\n e(\"text\", { x: width - margin.right - 78, y: margin.top + 9, fill: \"#8b8fa3\", fontSize: 10 }, \"Buy\"),\n e(\"rect\", { x: width - margin.right - 40, y: margin.top, width: 12, height: 12, fill: \"rgba(232,93,117,0.25)\", stroke: \"#e85d75\", strokeWidth: 1 }),\n e(\"text\", { x: width - margin.right - 23, y: margin.top + 9, fill: \"#8b8fa3\", fontSize: 10 }, \"Sell\"),\n )\n );\n}\n\nfunction App() {\n const [selectedIdx, setSelectedIdx] = React.useState(null);\n const [activeTab, setActiveTab] = React.useState(\"chart\");\n const [showOpen, setShowOpen] = React.useState(false);\n const [filterConnector, setFilterConnector] = React.useState(\"all\");\n\n // Get unique connectors\n const connectors = React.useMemo(() => {\n const set = new Set(DATA.map(d => d.connector).filter(Boolean));\n return Array.from(set).sort();\n }, []);\n\n // Filter by connector first\n const filteredData = React.useMemo(() => {\n if (filterConnector === \"all\") return DATA;\n return DATA.filter(d => d.connector === filterConnector);\n }, [filterConnector]);\n\n const closedPositions = React.useMemo(() => filteredData.filter(d => d.status === \"CLOSED\"), [filteredData]);\n const openPositions = React.useMemo(() => filteredData.filter(d => d.status === \"OPEN\"), [filteredData]);\n const chartData = React.useMemo(() => showOpen ? filteredData : closedPositions, [showOpen, filteredData, closedPositions]);\n\n const stats = React.useMemo(() => {\n const closed = closedPositions;\n const totalPnl = closed.reduce((s, d) => s + d.pnl, 0);\n const totalFees = closed.reduce((s, d) => s + d.fees, 0);\n const totalIL = closed.reduce((s, d) => s + d.il, 0);\n const totalValue = closed.reduce((s, d) => s + d.add_value, 0);\n const profitable = closed.filter(d => d.pnl > 0).length;\n const losing = closed.filter(d => d.pnl \u003c 0).length;\n const totalRent = DATA.reduce((s, d) => s + d.rent_paid, 0);\n const maxPnl = closed.length ? Math.max(...closed.map(d => d.pnl)) : 0;\n const minPnl = closed.length ? Math.min(...closed.map(d => d.pnl)) : 0;\n const avgDur = closed.length ? closed.reduce((s, d) => s + d.dur, 0) / closed.length : 0;\n return { totalPnl, totalFees, totalIL, totalValue, profitable, losing, totalRent, maxPnl, minPnl, avgDur };\n }, [closedPositions]);\n\n const selectedPosition = selectedIdx !== null ? DATA[selectedIdx] : null;\n\n const e = React.createElement;\n\n const tabBtn = (v, label) => e(\"button\", {\n key: v, onClick: () => setActiveTab(v),\n style: {\n fontSize: 11, padding: \"8px 20px\", cursor: \"pointer\", fontFamily: \"inherit\",\n border: \"none\", borderBottom: activeTab === v ? \"2px solid #4ecdc4\" : \"2px solid transparent\",\n background: \"transparent\",\n color: activeTab === v ? \"#e8eaed\" : \"#6b7084\",\n fontWeight: activeTab === v ? 600 : 400,\n }\n }, label);\n\n const firstTs = DATA.length ? DATA[DATA.length - 1]?.add_ts?.replace(\"T\", \" \").substring(0, 16) : \"\";\n const lastTs = DATA.length ? DATA[0]?.add_ts?.replace(\"T\", \" \").substring(0, 16) : \"\";\n\n return e(\"div\", { style: { paddingRight: selectedPosition ? 420 : 0, transition: \"padding-right 0.2s\" } },\n e(\"div\", { style: { padding: \"28px 24px\", maxWidth: 1200, margin: \"0 auto\" } },\n // Header\n e(\"div\", { style: { display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\", marginBottom: 6 } },\n e(\"div\", null,\n e(\"h1\", { style: { fontSize: 28, fontWeight: 700, margin: 0, color: \"#4ecdc4\" } }, META.trading_pair),\n e(\"div\", { style: { fontSize: 12, color: \"#6b7084\", marginTop: 4 } }, \"LP Positions\"),\n ),\n e(\"div\", { style: { display: \"flex\", alignItems: \"center\", gap: 8 } },\n e(\"span\", { style: { fontSize: 11, color: \"#6b7084\" } }, \"Connector:\"),\n e(\"select\", {\n value: filterConnector,\n onChange: (ev) => setFilterConnector(ev.target.value),\n style: {\n fontSize: 11, padding: \"6px 10px\", borderRadius: 6, cursor: \"pointer\",\n background: \"#1a1c2e\", border: \"1px solid rgba(255,255,255,0.1)\",\n color: \"#e8eaed\", fontFamily: \"inherit\",\n }\n },\n e(\"option\", { value: \"all\" }, \"All Connectors\"),\n ...connectors.map(c => e(\"option\", { key: c, value: c }, c))\n )\n )\n ),\n e(\"div\", { style: { fontSize: 11, color: \"#555870\", marginBottom: 16 } },\n `${firstTs} → ${lastTs} · ${filteredData.length} positions (${closedPositions.length} closed, ${openPositions.length} open)`\n ),\n\n // KPI cards\n e(\"div\", { style: { display: \"grid\", gridTemplateColumns: \"repeat(auto-fit, minmax(160px, 1fr))\", gap: 10, marginBottom: 24 } },\n e(StatCard, { label: \"Positions\", value: `${closedPositions.length}`, sub: openPositions.length > 0 ? `${openPositions.length} open` : \"all closed\", accent: \"#e8eaed\" }),\n e(StatCard, { label: \"Total PnL\", value: `${fmt(stats.totalPnl, 4)}`, sub: `${fmt(stats.totalPnl / Math.max(stats.totalValue, 1) * 100, 2)}% return`, accent: stats.totalPnl >= 0 ? \"#4ecdc4\" : \"#e85d75\" }),\n e(StatCard, { label: \"Fees Earned\", value: `${fmt(stats.totalFees, 4)}`, sub: `${fmt(stats.totalFees / Math.max(stats.totalValue, 1) * 10000, 1)} bps`, accent: \"#7c6df0\" }),\n e(StatCard, { label: \"Avg Duration\", value: fmtDuration(Math.round(stats.avgDur)), sub: \"per position\", accent: \"#e8eaed\" }),\n ),\n\n // Tab navigation\n e(\"div\", { style: { display: \"flex\", gap: 0, borderBottom: \"1px solid rgba(255,255,255,0.06)\", marginBottom: 24 } },\n tabBtn(\"chart\", \"Range Chart\"),\n tabBtn(\"positions\", \"Positions\"),\n ),\n\n // Chart tab\n activeTab === \"chart\" && e(\"div\", null,\n e(\"div\", { style: { display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\", margin: \"32px 0 14px\" } },\n e(\"h3\", { style: { fontSize: 13, fontWeight: 600, color: \"#8b8fa3\", textTransform: \"uppercase\", letterSpacing: \"0.08em\", margin: 0 } }, \"Position Ranges & Price\"),\n openPositions.length > 0 && e(\"label\", { style: { display: \"flex\", alignItems: \"center\", gap: 6, cursor: \"pointer\", fontSize: 11, color: \"#6b7084\" } },\n e(\"input\", {\n type: \"checkbox\",\n checked: showOpen,\n onChange: (ev) => setShowOpen(ev.target.checked),\n style: { cursor: \"pointer\" }\n }),\n `Show open (${openPositions.length})`\n )\n ),\n e(PositionRangeChart, { data: chartData, onSelectPosition: setSelectedIdx, tradingPair: META.trading_pair }),\n ),\n\n // Positions tab\n activeTab === \"positions\" && e(\"div\", null,\n e(SectionTitle, null, \"All Positions\"),\n e(PositionTable, { data: DATA, selectedIdx, onSelect: setSelectedIdx }),\n ),\n\n e(\"div\", { style: { fontSize: 10, color: \"#3a3d50\", textAlign: \"center\", marginTop: 32, paddingBottom: 16 } },\n `${META.trading_pair || \"All Pairs\"} · ${DATA.length} positions · ${META.db_name || \"\"}`\n ),\n ),\n\n // Detail panel\n selectedPosition && e(PositionDetail, { position: selectedPosition, onClose: () => setSelectedIdx(null) }),\n );\n}\n\nReactDOM.render(React.createElement(App), document.getElementById(\"root\"));\n\"\"\"\n\n\n# ---------------------------------------------------------------------------\n# Utilities\n# ---------------------------------------------------------------------------\n\ndef _float(v) -> float:\n if v is None or v == \"\":\n return 0.0\n try:\n return float(v)\n except (ValueError, TypeError):\n return 0.0\n\n\n# ---------------------------------------------------------------------------\n# CLI\n# ---------------------------------------------------------------------------\n\ndef main():\n parser = argparse.ArgumentParser(\n description=\"Visualize LP position events as an interactive HTML dashboard.\",\n formatter_class=argparse.RawDescriptionHelpFormatter,\n epilog=__doc__,\n )\n parser.add_argument(\"--pair\", \"-p\", required=True, help=\"Trading pair (e.g., SOL-USDC) - required for IL calculation\")\n parser.add_argument(\"--connector\", \"-c\", help=\"Filter by connector (e.g., orca/clmm, meteora/clmm)\")\n parser.add_argument(\"--hours\", \"-H\", type=float, help=\"Lookback period in hours (e.g., 24 for last 24 hours)\")\n parser.add_argument(\"--db\", help=\"Path to SQLite database (default: auto-detect)\")\n parser.add_argument(\"--output\", \"-o\", help=\"Output HTML path (default: data/lp_positions_\u003cpair>_dashboard.html)\")\n parser.add_argument(\"--no-open\", action=\"store_true\", help=\"Don't auto-open in browser\")\n\n args = parser.parse_args()\n\n # Find database\n if args.db:\n db_path = args.db\n else:\n db_path = find_db_with_lp_positions()\n print(f\"Using database: {db_path}\")\n\n # Query positions\n positions = query_positions(db_path, args.connector, args.pair, args.hours)\n if not positions:\n filters = []\n if args.connector:\n filters.append(f\"connector={args.connector}\")\n if args.pair:\n filters.append(f\"pair={args.pair}\")\n if args.hours:\n filters.append(f\"last {args.hours}h\")\n filter_str = f\" ({', '.join(filters)})\" if filters else \"\"\n print(f\"No positions found{filter_str}\")\n return 1\n\n filter_parts = []\n if args.connector:\n filter_parts.append(args.connector)\n if args.hours:\n filter_parts.append(f\"last {args.hours}h\")\n filter_str = f\" ({', '.join(filter_parts)})\" if filter_parts else \"\"\n print(f\"Loaded {len(positions)} positions{filter_str}\")\n\n # Build metadata\n meta = {\n \"trading_pair\": args.pair,\n \"connector\": args.connector, # None if not specified (show all connectors)\n \"db_name\": Path(db_path).name,\n \"lookback_hours\": args.hours,\n }\n\n # Transform and generate\n chart_data = positions_to_chart_data(positions)\n html = generate_html(chart_data, meta)\n\n # Write output\n if args.output:\n output_path = args.output\n else:\n os.makedirs(\"data\", exist_ok=True)\n parts = []\n if args.connector:\n parts.append(args.connector.replace(\"/\", \"_\"))\n if args.pair:\n parts.append(args.pair)\n suffix = f\"_{'_'.join(parts)}\" if parts else \"\"\n output_path = f\"data/lp_positions{suffix}_dashboard.html\"\n\n with open(output_path, \"w\") as f:\n f.write(html)\n\n print(f\"Dashboard written to: {output_path}\")\n\n if not args.no_open:\n abs_path = os.path.abspath(output_path)\n webbrowser.open(f\"file://{abs_path}\")\n print(\"Opened in browser\")\n\n return 0\n\n\nif __name__ == \"__main__\":\n exit(main())\n","content_type":"text/x-python; charset=utf-8","language":"python","size":47439,"content_sha256":"733a53fb3f06de1fac3c4c08cb380a50bcb50a06b1532bcd8a996fc5892bee70"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"lp-agent","type":"text"}]},{"type":"paragraph","content":[{"text":"This skill helps you run automated liquidity provision strategies on concentrated liquidity (CLMM) DEXs using Hummingbot API.","type":"text"}]},{"type":"paragraph","content":[{"text":"Commands","type":"text","marks":[{"type":"strong"}]},{"text":" (run as ","type":"text"},{"text":"/lp-agent \u003ccommand>","type":"text","marks":[{"type":"code_inline"}]},{"text":"):","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Command","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Description","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"start","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Onboarding wizard — check setup status and get started","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"deploy-hummingbot-api","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Deploy Hummingbot API trading infrastructure","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"setup-gateway","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Start Gateway, configure network RPC endpoints","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-wallet","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Add or import a Solana wallet","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"explore-pools","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Find and explore Meteora DLMM pools","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"select-strategy","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Choose LP Executor or Rebalancer Controller","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"run-strategy","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Run, monitor, and manage LP strategies","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"analyze-performance","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Visualize LP position performance","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"New here?","type":"text","marks":[{"type":"strong"}]},{"text":" Run ","type":"text"},{"text":"/lp-agent start","type":"text","marks":[{"type":"code_inline"}]},{"text":" to check your setup and get a guided walkthrough.","type":"text"}]},{"type":"paragraph","content":[{"text":"Typical workflow:","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"start","type":"text","marks":[{"type":"code_inline"}]},{"text":" → ","type":"text"},{"text":"deploy-hummingbot-api","type":"text","marks":[{"type":"code_inline"}]},{"text":" → ","type":"text"},{"text":"setup-gateway","type":"text","marks":[{"type":"code_inline"}]},{"text":" → ","type":"text"},{"text":"add-wallet","type":"text","marks":[{"type":"code_inline"}]},{"text":" → ","type":"text"},{"text":"explore-pools","type":"text","marks":[{"type":"code_inline"}]},{"text":" → ","type":"text"},{"text":"select-strategy","type":"text","marks":[{"type":"code_inline"}]},{"text":" → ","type":"text"},{"text":"run-strategy","type":"text","marks":[{"type":"code_inline"}]},{"text":" → ","type":"text"},{"text":"analyze-performance","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Command: start","type":"text"}]},{"type":"paragraph","content":[{"text":"Welcome the user and guide them through setup. This is a conversational onboarding wizard — check infrastructure state, interpret results, and walk them through each step.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 1: Welcome & Explain","type":"text"}]},{"type":"paragraph","content":[{"text":"Introduce yourself and explain what lp-agent does:","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"I'm your LP agent — I help you run automated liquidity provision strategies on Meteora DLMM pools (Solana). I can:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Deploy infrastructure","type":"text","marks":[{"type":"strong"}]},{"text":" — Hummingbot API + Gateway for DEX trading","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Manage wallets","type":"text","marks":[{"type":"strong"}]},{"text":" — Add Solana wallets, check balances","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Explore pools","type":"text","marks":[{"type":"strong"}]},{"text":" — Search Meteora DLMM pools, compare APR/volume/TVL","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Run strategies","type":"text","marks":[{"type":"strong"}]},{"text":" — Auto-rebalancing LP controller or single-position executor","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Analyze performance","type":"text","marks":[{"type":"strong"}]},{"text":" — Dashboards with PnL, fees, and position history","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 2: Check Infrastructure Status","type":"text"}]},{"type":"paragraph","content":[{"text":"Run these scripts and interpret the JSON output:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"bash scripts/check_api.sh --json # Is Hummingbot API running?\nbash scripts/check_gateway.sh --json # Is Gateway running?\npython scripts/add_wallet.py list # Any wallets connected?","type":"text"}]},{"type":"paragraph","content":[{"text":"Interpreting Results:","type":"text","marks":[{"type":"strong"}]}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Script","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Success Output","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Failure Output","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"check_api.sh --json","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"{\"running\": true, \"url\": \"http://localhost:8000\", ...}","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"{\"running\": false, ...}","type":"text","marks":[{"type":"code_inline"}]},{"text":" or connection error","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"check_gateway.sh --json","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"{\"running\": true, ...}","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"{\"running\": false, ...}","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add_wallet.py list","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Shows wallet addresses like ","type":"text"},{"text":"[solana] ABC123...","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"No wallets found.","type":"text","marks":[{"type":"code_inline"}]},{"text":" or empty list ","type":"text"},{"text":"[]","type":"text","marks":[{"type":"code_inline"}]}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 3: Show Progress","type":"text"}]},{"type":"paragraph","content":[{"text":"Present a checklist showing what's done and what's remaining based on the script outputs:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"Setup Progress:\n [x] Hummingbot API — Running at http://localhost:8000\n [x] Gateway — Running\n [ ] Wallet — No wallet connected\n\nNext step: Add a Solana wallet so you can start trading.","type":"text"}]},{"type":"paragraph","content":[{"text":"Adapt the checklist to the actual state. If everything is unchecked, start from the top. If everything is checked, skip to the LP lifecycle overview.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 4: Guide Next Action","type":"text"}]},{"type":"paragraph","content":[{"text":"Based on the first unchecked item, offer to help:","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Missing","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"What to say","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Hummingbot API","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"\"Let's deploy the API first — it's the trading backend. Need Docker installed. Want me to run the installer?\" → ","type":"text"},{"text":"/lp-agent deploy-hummingbot-api","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Gateway","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"\"API is running! Now we need Gateway for DEX connectivity. Want me to start it?\" → ","type":"text"},{"text":"/lp-agent setup-gateway","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Wallet","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"See ","type":"text"},{"text":"Adding a Wallet","type":"text","marks":[{"type":"strong"}]},{"text":" below","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"All ready","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Move to Step 5","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"Adding a Wallet:","type":"text","marks":[{"type":"strong"}]}]},{"type":"paragraph","content":[{"text":"When wallet is the next step, tell the user:","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"Infrastructure is ready. You need a Solana wallet with SOL for transaction fees (~0.06 SOL per LP position).","type":"text"}]},{"type":"paragraph","content":[{"text":"To add a wallet, run:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"python scripts/add_wallet.py add","type":"text"}]},{"type":"paragraph","content":[{"text":"You'll be prompted to paste your private key (secure, not saved in shell history).","type":"text"}]}]},{"type":"paragraph","content":[{"text":"Interpreting add_wallet.py output:","type":"text","marks":[{"type":"strong"}]}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Output","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Meaning","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"✓ Wallet added successfully","type":"text","marks":[{"type":"code_inline"}]},{"text":" + address","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Success — wallet is connected","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Enter private key (base58):","type":"text","marks":[{"type":"code_inline"}]},{"text":" then ","type":"text"},{"text":"✓ Wallet added","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Success after prompt","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Error: HTTP 400","type":"text","marks":[{"type":"code_inline"}]},{"text":" or validation error","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Invalid private key format","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Error: Cannot connect to API","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"API not running — run ","type":"text"},{"text":"check_api.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" first","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"After wallet is added, verify with ","type":"text"},{"text":"python scripts/add_wallet.py list","type":"text","marks":[{"type":"code_inline"}]},{"text":" — should show the new address.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 5: LP Lifecycle Overview","type":"text"}]},{"type":"paragraph","content":[{"text":"Once infrastructure is ready (or if user wants to understand the flow first), explain the LP lifecycle:","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"How LP strategies work:","type":"text","marks":[{"type":"strong"}]}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Explore pools","type":"text","marks":[{"type":"strong"}]},{"text":" (","type":"text"},{"text":"/lp-agent explore-pools","type":"text","marks":[{"type":"code_inline"}]},{"text":") — Find a Meteora DLMM pool. Look at volume, APR, and fee/TVL ratio to pick a good one.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Select strategy","type":"text","marks":[{"type":"strong"}]},{"text":" (","type":"text"},{"text":"/lp-agent select-strategy","type":"text","marks":[{"type":"code_inline"}]},{"text":") — Choose between:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Rebalancer Controller","type":"text","marks":[{"type":"strong"}]},{"text":" (recommended) — Automatically repositions when price moves out of range. Set-and-forget.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"LP Executor","type":"text","marks":[{"type":"strong"}]},{"text":" — Single fixed position. You control when to close/reopen. Good for testing or limit-order-style LP.","type":"text"}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Run strategy","type":"text","marks":[{"type":"strong"}]},{"text":" (","type":"text"},{"text":"/lp-agent run-strategy","type":"text","marks":[{"type":"code_inline"}]},{"text":") — Configure parameters (amount, width, price limits) and deploy. Monitor status and stop when done.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Analyze","type":"text","marks":[{"type":"strong"}]},{"text":" (","type":"text"},{"text":"/lp-agent analyze-performance","type":"text","marks":[{"type":"code_inline"}]},{"text":") — View PnL dashboard, fees earned, position history. Works for both running and stopped strategies.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Want to explore some pools to get started?","type":"text"}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Command: deploy-hummingbot-api","type":"text"}]},{"type":"paragraph","content":[{"text":"Deploy the Hummingbot API trading infrastructure. This is the first step before using any LP features.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"What Gets Installed","type":"text"}]},{"type":"paragraph","content":[{"text":"Hummingbot API","type":"text","marks":[{"type":"strong"}]},{"text":" — A personal trading server that exposes a REST API for trading, market data, and deploying bot strategies across CEXs and DEXs.","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Repository: ","type":"text"},{"text":"hummingbot/hummingbot-api","type":"text","marks":[{"type":"link","attrs":{"href":"https://github.com/hummingbot/hummingbot-api","title":null}}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Usage","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Check if already installed\nbash scripts/deploy_hummingbot_api.sh status\n\n# Install (interactive, prompts for credentials)\nbash scripts/deploy_hummingbot_api.sh install\n\n# Install with defaults (non-interactive: admin/admin)\nbash scripts/deploy_hummingbot_api.sh install --defaults\n\n# Upgrade existing installation\nbash scripts/deploy_hummingbot_api.sh upgrade\n\n# View container logs\nbash scripts/deploy_hummingbot_api.sh logs\n\n# Reset (stop and remove everything)\nbash scripts/deploy_hummingbot_api.sh reset","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Prerequisites","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Docker and Docker Compose","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Git","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Interpreting Output","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Output","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Meaning","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Next Step","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"✓ Hummingbot API deployed successfully","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Success","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Proceed to ","type":"text"},{"text":"setup-gateway","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"✓ Already installed and running","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Already set up","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Proceed to ","type":"text"},{"text":"setup-gateway","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Error: Docker not found","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Docker not installed","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Install Docker first","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Error: Port 8000 already in use","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Another service on port","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Stop conflicting service or use different port","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"After Installation","type":"text"}]},{"type":"paragraph","content":[{"text":"Once the API is running:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Swagger UI is at ","type":"text"},{"text":"http://localhost:8000/docs","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Default credentials: admin/admin","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Proceed to ","type":"text"},{"text":"setup-gateway","type":"text","marks":[{"type":"code_inline"}]},{"text":" to enable DEX trading","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Command: setup-gateway","type":"text"}]},{"type":"paragraph","content":[{"text":"Start the Gateway service, check its status, and configure key network parameters like RPC node URLs. Gateway is required for all LP operations on DEXs.","type":"text"}]},{"type":"paragraph","content":[{"text":"Prerequisite:","type":"text","marks":[{"type":"strong"}]},{"text":" Hummingbot API must be running (","type":"text"},{"text":"deploy-hummingbot-api","type":"text","marks":[{"type":"code_inline"}]},{"text":"). The script checks this automatically.","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"⚠️ ","type":"text"},{"text":"Custom RPC is required — not optional.","type":"text","marks":[{"type":"strong"}]},{"text":" The public Solana RPC is rate-limited and will cause transaction failures that look like \"Insufficient funds\" or \"Transaction simulation failed\". Always configure a custom RPC before deploying any bot. Get a free key at https://helius.dev.","type":"text"}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Usage","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Check Gateway status\nbash scripts/setup_gateway.sh --status\n\n# Start Gateway with defaults\nbash scripts/setup_gateway.sh\n\n# Start Gateway with custom image (e.g., development build)\nbash scripts/setup_gateway.sh --image hummingbot/gateway:development\n\n# Start with custom Solana RPC (recommended to avoid rate limits)\nbash scripts/setup_gateway.sh --rpc-url https://your-rpc-endpoint.com\n\n# Configure RPC for a different network\nbash scripts/setup_gateway.sh --network ethereum-mainnet --rpc-url https://your-eth-rpc.com\n\n# Start with custom passphrase and port\nbash scripts/setup_gateway.sh --passphrase mypassword --port 15888","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Options","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Option","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Default","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Description","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--status","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph"}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Check Gateway status only (don't start)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--image IMAGE","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"hummingbot/gateway:development","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Docker image to use","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--passphrase TEXT","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"hummingbot","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Gateway passphrase","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--rpc-url URL","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph"}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Custom RPC endpoint for ","type":"text"},{"text":"--network","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--network ID","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"solana-mainnet-beta","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Network to configure RPC for","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--port PORT","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"15888","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Gateway port","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Advanced: manage_gateway.py","type":"text"}]},{"type":"paragraph","content":[{"text":"For finer control (stop, restart, logs, per-network config), use ","type":"text"},{"text":"manage_gateway.py","type":"text","marks":[{"type":"code_inline"}]},{"text":":","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python scripts/manage_gateway.py status # Check status\npython scripts/manage_gateway.py start # Start Gateway\npython scripts/manage_gateway.py stop # Stop Gateway\npython scripts/manage_gateway.py restart # Restart Gateway\npython scripts/manage_gateway.py logs # View logs\npython scripts/manage_gateway.py networks # List all networks\npython scripts/manage_gateway.py network solana-mainnet-beta # Get network config\npython scripts/manage_gateway.py network solana-mainnet-beta --node-url https://... # Set RPC node","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Interpreting Output","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Output","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Meaning","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Next Step","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"✓ Gateway is running","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"✓ Gateway started","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Success","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Proceed to ","type":"text"},{"text":"add-wallet","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"✓ Gateway is already running","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Already set up","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Proceed to ","type":"text"},{"text":"add-wallet","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"✗ Cannot connect to Hummingbot API","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"API not running","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Run ","type":"text"},{"text":"/lp-agent deploy-hummingbot-api","type":"text","marks":[{"type":"code_inline"}]},{"text":" first","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"✗ Failed to start Gateway","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Docker issue","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Check Docker is running, check logs","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"✓ RPC configured","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"✓ Gateway restarted","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Custom RPC set","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Ready to use","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Custom RPC Nodes","type":"text"}]},{"type":"paragraph","content":[{"text":"Gateway uses public RPC nodes by default, which can hit rate limits. Set a custom nodeUrl per network to avoid this.","type":"text"}]},{"type":"paragraph","content":[{"text":"Popular Solana RPC providers:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Helius","type":"text","marks":[{"type":"link","attrs":{"href":"https://helius.dev/","title":null}}]},{"text":" — Free tier available","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"QuickNode","type":"text","marks":[{"type":"link","attrs":{"href":"https://quicknode.com/","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Alchemy","type":"text","marks":[{"type":"link","attrs":{"href":"https://alchemy.com/","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Triton","type":"text","marks":[{"type":"link","attrs":{"href":"https://triton.one/","title":null}}]}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Command: add-wallet","type":"text"}]},{"type":"paragraph","content":[{"text":"Add a Solana wallet for trading.","type":"text"}]},{"type":"paragraph","content":[{"text":"Requires:","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"deploy-hummingbot-api","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"setup-gateway","type":"text","marks":[{"type":"code_inline"}]},{"text":" completed first.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Adding a Wallet","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python scripts/add_wallet.py add","type":"text"}]},{"type":"paragraph","content":[{"text":"You'll be prompted to paste your private key (base58 format). The key is entered securely and won't appear in shell history.","type":"text"}]},{"type":"paragraph","content":[{"text":"Interpreting Output:","type":"text","marks":[{"type":"strong"}]}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Output","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Meaning","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Next Step","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"✓ Wallet added successfully","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"Address: ABC...","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Success","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Verify with ","type":"text"},{"text":"list","type":"text","marks":[{"type":"code_inline"}]},{"text":" command","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Error: HTTP 400 - Bad Request","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Invalid private key format","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Check key is base58 encoded","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Error: HTTP 503","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Gateway not available","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Run ","type":"text"},{"text":"bash scripts/check_gateway.sh","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Error: Cannot connect to API","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"API not running","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Run ","type":"text"},{"text":"/lp-agent deploy-hummingbot-api","type":"text","marks":[{"type":"code_inline"}]}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Listing Wallets","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python scripts/add_wallet.py list","type":"text"}]},{"type":"paragraph","content":[{"text":"Interpreting Output:","type":"text","marks":[{"type":"strong"}]}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Output","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Meaning","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"[solana] ABC123...XYZ","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Wallet connected on Solana","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"No wallets found.","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"No wallets added yet","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Empty list ","type":"text"},{"text":"[]","type":"text","marks":[{"type":"code_inline"}]},{"text":" (with --json)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"No wallets added yet","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Checking Balances","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Check all balances\npython scripts/add_wallet.py balances\n\n# Filter by account\npython scripts/add_wallet.py balances --account master_account\n\n# Show zero balances too\npython scripts/add_wallet.py balances --all","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Adding a Custom Token to Gateway","type":"text"}]},{"type":"paragraph","content":[{"text":"When using non-standard tokens (e.g. memecoins), add them to Gateway first:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Use exact symbol from the pool (check with get_meteora_pool.py)\npython scripts/add_wallet.py add-token \\\n --symbol Percolator \\\n --address 8PzFWyLpCVEmbZmVJcaRTU5r69XKJx1rd7YGpWvnpump \\\n --decimals 6","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"⚠️ ","type":"text"},{"text":"Symbol must match the pool exactly.","type":"text","marks":[{"type":"strong"}]},{"text":" Use ","type":"text"},{"text":"get_meteora_pool.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" to find the exact token symbol. If the pool shows ","type":"text"},{"text":"Percolator","type":"text","marks":[{"type":"code_inline"}]},{"text":", use ","type":"text"},{"text":"Percolator","type":"text","marks":[{"type":"code_inline"}]},{"text":" — not ","type":"text"},{"text":"PRCLT","type":"text","marks":[{"type":"code_inline"}]},{"text":" or any abbreviation. A mismatch causes \"No CLMM pool found\" errors when the bot tries to fetch price data.","type":"text"}]}]},{"type":"paragraph","content":[{"text":"Then restart Gateway for the token to be recognized.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Requirements","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"SOL for fees","type":"text","marks":[{"type":"strong"}]},{"text":": Wallet needs SOL for transaction fees (~0.06 SOL per LP position for rent)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Check actual balance before deploying","type":"text","marks":[{"type":"strong"}]},{"text":": Use ","type":"text"},{"text":"balances","type":"text","marks":[{"type":"code_inline"}]},{"text":" command — the available amount in the SPL token account may differ from on-chain total (multiple accounts, staked, etc.)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Default chain","type":"text","marks":[{"type":"strong"}]},{"text":": Solana mainnet-beta","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Command: explore-pools","type":"text"}]},{"type":"paragraph","content":[{"text":"Find and explore Meteora DLMM pools before creating LP positions.","type":"text"}]},{"type":"paragraph","content":[{"text":"Note:","type":"text","marks":[{"type":"strong"}]},{"text":" Pool listing (","type":"text"},{"text":"list_meteora_pools.py","type":"text","marks":[{"type":"code_inline"}]},{"text":") works without any prerequisites — it queries the Meteora API directly. Pool details (","type":"text"},{"text":"get_meteora_pool.py","type":"text","marks":[{"type":"code_inline"}]},{"text":") optionally uses Gateway for real-time price and liquidity charts.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"List Pools","type":"text"}]},{"type":"paragraph","content":[{"text":"Search and list pools by name, token, or address:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Top pools by 24h volume\npython scripts/list_meteora_pools.py\n\n# Search by token symbol\npython scripts/list_meteora_pools.py --query SOL\npython scripts/list_meteora_pools.py --query USDC\n\n# Search by pool name\npython scripts/list_meteora_pools.py --query SOL-USDC\n\n# Sort by different metrics\npython scripts/list_meteora_pools.py --query SOL --sort tvl\npython scripts/list_meteora_pools.py --query SOL --sort apr\npython scripts/list_meteora_pools.py --query SOL --sort fees\n\n# Pagination\npython scripts/list_meteora_pools.py --query SOL --limit 50 --page 2","type":"text"}]},{"type":"paragraph","content":[{"text":"Output columns:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Pool","type":"text","marks":[{"type":"strong"}]},{"text":": Trading pair name","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Pool Address","type":"text","marks":[{"type":"strong"}]},{"text":": Pool contract address (shortened, use ","type":"text"},{"text":"get_meteora_pool.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" for full address)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Base (mint)","type":"text","marks":[{"type":"strong"}]},{"text":": Base token symbol with shortened mint address","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Quote (mint)","type":"text","marks":[{"type":"strong"}]},{"text":": Quote token symbol with shortened mint address","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"TVL","type":"text","marks":[{"type":"strong"}]},{"text":": Total value locked","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Vol 24h","type":"text","marks":[{"type":"strong"}]},{"text":": 24-hour trading volume","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Fees 24h","type":"text","marks":[{"type":"strong"}]},{"text":": Fees earned in last 24 hours","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"APR","type":"text","marks":[{"type":"strong"}]},{"text":": Annual percentage rate","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Fee","type":"text","marks":[{"type":"strong"}]},{"text":": Base fee percentage","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Bin","type":"text","marks":[{"type":"strong"}]},{"text":": Bin step (affects max position width)","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Note:","type":"text","marks":[{"type":"strong"}]},{"text":" Token mints help identify the correct token when multiple tokens share the same name (e.g., multiple \"PERCOLATOR\" tokens).","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Get Pool Details","type":"text"}]},{"type":"paragraph","content":[{"text":"Get detailed information about a specific pool. Fetches from both Meteora API (historical data) and Gateway (real-time data):","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python scripts/get_meteora_pool.py \u003cpool_address>\n\n# Example\npython scripts/get_meteora_pool.py ATrBUW2reZiyftzMQA1hEo8b7w7o8ZLrhPd7M7sPMSms\n\n# Output as JSON for programmatic use\npython scripts/get_meteora_pool.py ATrBUW2reZiyftzMQA1hEo8b7w7o8ZLrhPd7M7sPMSms --json\n\n# Skip Gateway (faster, no bin distribution)\npython scripts/get_meteora_pool.py ATrBUW2reZiyftzMQA1hEo8b7w7o8ZLrhPd7M7sPMSms --no-gateway","type":"text"}]},{"type":"paragraph","content":[{"text":"Data sources:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Meteora API","type":"text","marks":[{"type":"strong"}]},{"text":": Historical volume, fees, APR, token info, market caps","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Gateway","type":"text","marks":[{"type":"strong"}]},{"text":" (requires running Gateway): Real-time price, liquidity distribution by bin","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Details shown:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Token info (symbols, mints, decimals, prices)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Pool configuration (bin step, fees, max range width)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Real-time price from Gateway (SOL/token ratio)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Liquidity distribution chart showing bins around current price","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Liquidity and reserves","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Volume across time windows (30m, 1h, 4h, 12h, 24h)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Fees earned across time windows","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Yield (APR, APY, farm rewards)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Fee/TVL ratio (profitability indicator)","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Choosing a Pool","type":"text"}]},{"type":"paragraph","content":[{"text":"When selecting a pool, consider:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"TVL","type":"text","marks":[{"type":"strong"}]},{"text":": Higher TVL = more stable, but also more competition","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Volume","type":"text","marks":[{"type":"strong"}]},{"text":": Higher volume = more fee opportunities","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Fee/TVL Ratio","type":"text","marks":[{"type":"strong"}]},{"text":": Higher = more profitable per $ of liquidity","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Bin Step","type":"text","marks":[{"type":"strong"}]},{"text":": Determines max position width","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"bin_step=1","type":"text","marks":[{"type":"code_inline"}]},{"text":" → max ~0.69% width (tight ranges)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"bin_step=10","type":"text","marks":[{"type":"code_inline"}]},{"text":" → max ~6.9% width (medium ranges)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"bin_step=100","type":"text","marks":[{"type":"code_inline"}]},{"text":" → max ~69% width (wide ranges)","type":"text"}]}]}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Command: select-strategy","type":"text"}]},{"type":"paragraph","content":[{"text":"Help the user choose the right LP strategy. See ","type":"text"},{"text":"references/","type":"text","marks":[{"type":"code_inline"}]},{"text":" for detailed guides.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"LP Rebalancer Controller (Recommended)","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"Reference:","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"references/lp_rebalancer_guide.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"paragraph","content":[{"text":"A controller that automatically manages LP positions with rebalancing logic.","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Feature","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Description","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Auto-rebalance","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Closes and reopens positions when price exits range","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Price limits","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Configure BUY/SELL zones with anchor points","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"KEEP logic","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Avoids unnecessary rebalancing when at optimal position","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Hands-off","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Set and forget - controller manages everything","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"Best for:","type":"text","marks":[{"type":"strong"}]},{"text":" Longer-term LP strategies, range-bound markets, automated fee collection.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"LP Executor (Single Position)","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"Reference:","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"references/lp_executor_guide.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"paragraph","content":[{"text":"Creates ONE liquidity position with fixed price bounds. No auto-rebalancing.","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Feature","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Description","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Fixed bounds","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Position stays at configured price range","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Manual control","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"User decides when to close/reopen","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Limit orders","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Can auto-close when price exits range (like limit orders)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Simple","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Direct control over single position","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"Best for:","type":"text","marks":[{"type":"strong"}]},{"text":" Short-term positions, limit-order-style LP, manual management, testing.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Quick Comparison","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Aspect","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Rebalancer Controller","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"LP Executor","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Rebalancing","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Automatic","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Manual","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Position count","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"One at a time, auto-managed","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"One, fixed","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Price limits","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Yes (anchor points)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"No (but has auto-close)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Complexity","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Higher (more config)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Lower (simpler)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Use case","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Set-and-forget","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Precise control","type":"text"}]}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Command: run-strategy","type":"text"}]},{"type":"paragraph","content":[{"text":"Run, monitor, and manage LP strategies.","type":"text"}]},{"type":"paragraph","content":[{"text":"Requires:","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"deploy-hummingbot-api","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"setup-gateway","type":"text","marks":[{"type":"code_inline"}]},{"text":", and ","type":"text"},{"text":"add-wallet","type":"text","marks":[{"type":"code_inline"}]},{"text":" completed first.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"LP Rebalancer Controller (Recommended)","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"Reference:","type":"text","marks":[{"type":"strong"}]},{"text":" See ","type":"text"},{"text":"references/lp_rebalancer_guide.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" for full configuration details, rebalancing logic, and KEEP vs REBALANCE scenarios.","type":"text"}]}]},{"type":"paragraph","content":[{"text":"Auto-rebalances positions when price moves out of range. Best for hands-off LP management.","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"Key concepts:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"--amount","type":"text","marks":[{"type":"code_inline"}]},{"text":" (","type":"text"},{"text":"total_amount_quote","type":"text","marks":[{"type":"code_inline"}]},{"text":") = amount in ","type":"text"},{"text":"quote asset","type":"text","marks":[{"type":"strong"}]},{"text":" (2nd token in pair). For ","type":"text"},{"text":"Percolator-SOL","type":"text","marks":[{"type":"code_inline"}]},{"text":" → SOL. For ","type":"text"},{"text":"SOL-USDC","type":"text","marks":[{"type":"code_inline"}]},{"text":" → USDC. Always quote, regardless of side.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"All ","type":"text"},{"text":"*_pct","type":"text","marks":[{"type":"code_inline"}]},{"text":" params are already in percent. ","type":"text"},{"text":"position_width_pct: 10","type":"text","marks":[{"type":"code_inline"}]},{"text":" = 10% width. Do NOT pass decimals (not 0.10).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Price limits (","type":"text"},{"text":"--buy-min/max","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"--sell-min/max","type":"text","marks":[{"type":"code_inline"}]},{"text":") default to ","type":"text"},{"text":"null","type":"text","marks":[{"type":"code_inline"}]},{"text":" = no limit. Only set if you want a stop zone.","type":"text"}]}]}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# 1. Create LP Rebalancer config (pool and pair are required)\npython scripts/manage_controller.py create-config my_lp_config \\\n --pool \u003cpool_address> \\\n --pair SOL-USDC \\\n --amount 10 \\ # 10 USDC (quote asset for SOL-USDC)\n --side 0 \\ # 0=BOTH, 1=BUY (quote only), 2=SELL (base only)\n --width 10 \\ # 10% range around current price\n --offset 1 \\ # center range 1% from current price\n --rebalance-seconds 300 \\\n --rebalance-threshold 1\n\n# Side=2 example: deploy base token only (e.g. 110k PRCLT ≈ 1.33 SOL)\npython scripts/manage_controller.py create-config percolator_sell \\\n --pool ATrBUW2reZiyftzMQA1hEo8b7w7o8ZLrhPd7M7sPMSms \\\n --pair Percolator-SOL \\\n --amount 1.33 \\ # 1.33 SOL worth (quote for Percolator-SOL pair)\n --side 2\n\n# 2. Deploy bot with the config\npython scripts/manage_controller.py deploy my_lp_bot --configs my_lp_config\n\n# 3. Monitor status\npython scripts/manage_controller.py status","type":"text"}]},{"type":"paragraph","content":[{"text":"Key Parameters:","type":"text","marks":[{"type":"strong"}]}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Parameter","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Field","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Default","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Description","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--amount","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"total_amount_quote","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"required","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Amount in ","type":"text"},{"text":"quote asset","type":"text","marks":[{"type":"strong"}]},{"text":" (2nd token). SOL for X-SOL pairs, USDC for X-USDC pairs.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--side","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"side","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"0","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"0","type":"text","marks":[{"type":"code_inline"}]},{"text":"=BOTH, ","type":"text"},{"text":"1","type":"text","marks":[{"type":"code_inline"}]},{"text":"=BUY (quote only), ","type":"text"},{"text":"2","type":"text","marks":[{"type":"code_inline"}]},{"text":"=SELL (base only)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--width","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"position_width_pct","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"10","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Range width in % (e.g. ","type":"text"},{"text":"10","type":"text","marks":[{"type":"code_inline"}]},{"text":" = ±10% around price). Already in pct — do not use decimals.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--offset","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"position_offset_pct","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"0.1","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Center offset from current price in %. Already in pct.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--rebalance-seconds","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"rebalance_seconds","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"300","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Seconds out-of-range before closing and reopening","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--rebalance-threshold","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"rebalance_threshold_pct","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"1","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Min price move % to trigger rebalance. Already in pct.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--sell-max/--sell-min","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"sell_price_max/min","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"null","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Price limits for SELL side (","type":"text"},{"text":"null","type":"text","marks":[{"type":"code_inline"}]},{"text":" = no limit)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--buy-max/--buy-min","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"buy_price_max/min","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"null","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Price limits for BUY side (","type":"text"},{"text":"null","type":"text","marks":[{"type":"code_inline"}]},{"text":" = no limit)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--strategy-type","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"strategy_type","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"0","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Meteora shape: ","type":"text"},{"text":"0","type":"text","marks":[{"type":"code_inline"}]},{"text":"=Spot (uniform), ","type":"text"},{"text":"1","type":"text","marks":[{"type":"code_inline"}]},{"text":"=Curve (center-heavy), ","type":"text"},{"text":"2","type":"text","marks":[{"type":"code_inline"}]},{"text":"=Bid-Ask (edge-heavy)","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Single LP Executor (Alternative)","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"Reference:","type":"text","marks":[{"type":"strong"}]},{"text":" See ","type":"text"},{"text":"references/lp_executor_guide.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" for state machine, single/double-sided positions, and limit range orders.","type":"text"}]}]},{"type":"paragraph","content":[{"text":"Creates ONE position with fixed bounds. Does NOT auto-rebalance.","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python scripts/manage_executor.py create \\\n --pool \u003cpool_address> \\\n --pair SOL-USDC \\\n --quote-amount 100 \\\n --lower 180 \\\n --upper 185 \\\n --side 1","type":"text"}]},{"type":"paragraph","content":[{"text":"Key Parameters:","type":"text","marks":[{"type":"strong"}]}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Parameter","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Description","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--connector","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Must include ","type":"text"},{"text":"/clmm","type":"text","marks":[{"type":"code_inline"}]},{"text":" suffix (default: ","type":"text"},{"text":"meteora/clmm","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--lower/--upper","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Position price bounds","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--base-amount/--quote-amount","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Token amounts (set one to 0 for single-sided)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--side","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"0=BOTH, 1=BUY, 2=SELL","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--auto-close-above","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Auto-close when price above range (for limit orders)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--auto-close-below","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Auto-close when price below range (for limit orders)","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Monitor & Manage","type":"text"}]},{"type":"paragraph","content":[{"text":"Check Status:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Bot status\npython scripts/manage_controller.py status\n\n# Executor list\npython scripts/manage_executor.py list --type lp_executor\n\n# Executor details\npython scripts/manage_executor.py get \u003cexecutor_id>\n\n# Executor summary\npython scripts/manage_executor.py summary","type":"text"}]},{"type":"paragraph","content":[{"text":"Executor States:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"OPENING","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Creating position on-chain","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"IN_RANGE","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Position active, earning fees","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"OUT_OF_RANGE","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Price outside position bounds","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"CLOSING","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Removing position","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"FAILED","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Transaction failed","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Stop:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Stop bot (stops all its controllers)\npython scripts/manage_controller.py stop my_lp_bot\n\n# Stop individual executor (closes position)\npython scripts/manage_executor.py stop \u003cexecutor_id>\n\n# Stop executor but keep position on-chain\npython scripts/manage_executor.py stop \u003cexecutor_id> --keep-position","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"After Stopping — Analyze Results","type":"text"}]},{"type":"paragraph","content":[{"text":"If the user ran an LP Executor","type":"text","marks":[{"type":"strong"}]},{"text":" (via ","type":"text"},{"text":"manage_executor.py create","type":"text","marks":[{"type":"code_inline"}]},{"text":" or direct API), immediately offer to analyze it:","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"Your executor has been stopped. Want me to generate a performance dashboard?","type":"text"}]}]},{"type":"paragraph","content":[{"text":"Then run:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python scripts/visualize_lp_executor.py --id \u003cexecutor_id>","type":"text"}]},{"type":"paragraph","content":[{"text":"The executor ID is returned when the executor is created (printed as ","type":"text"},{"text":"Executor ID: \u003cid>","type":"text","marks":[{"type":"code_inline"}]},{"text":"). If the user doesn't have it handy, fetch it from the API:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"curl -s -u admin:admin -X POST http://localhost:8000/executors/search \\\n -H \"Content-Type: application/json\" \\\n -d '{\"type\":\"lp_executor\"}' | python3 -c \"\nimport json,sys\ndata=json.load(sys.stdin)\nitems=data.get('data',data) if isinstance(data,dict) else data\nfor ex in (items if isinstance(items,list) else [items]):\n print(ex.get('executor_id') or ex.get('id'), ex.get('trading_pair'), ex.get('status'))\n\"","type":"text"}]},{"type":"paragraph","content":[{"text":"To also export the raw data to CSV:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python scripts/export_lp_executor.py --id \u003cexecutor_id>","type":"text"}]},{"type":"paragraph","content":[{"text":"If the user ran a Rebalancer Controller bot","type":"text","marks":[{"type":"strong"}]},{"text":", the data lives in a SQLite file — use ","type":"text"},{"text":"analyze-performance","type":"text","marks":[{"type":"code_inline"}]},{"text":" with the SQLite-based scripts instead.","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Command: analyze-performance","type":"text"}]},{"type":"paragraph","content":[{"text":"Export data and generate visual dashboards from LP position events. Scripts are in this skill's ","type":"text"},{"text":"scripts/","type":"text","marks":[{"type":"code_inline"}]},{"text":" directory.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Which Script to Use?","type":"text"}]},{"type":"paragraph","content":[{"text":"Always ask yourself: was this position deployed as an LP Executor (via ","type":"text","marks":[{"type":"strong"}]},{"text":"manage_executor.py","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" or direct API) or via a Rebalancer Controller bot?","type":"text","marks":[{"type":"strong"}]}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"How it was deployed","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Script to use","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"LP Executor","type":"text","marks":[{"type":"strong"}]},{"text":" — ","type":"text"},{"text":"manage_executor.py create","type":"text","marks":[{"type":"code_inline"}]},{"text":" or direct ","type":"text"},{"text":"POST /executors/","type":"text","marks":[{"type":"code_inline"}]},{"text":" API","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"visualize_lp_executor.py --id \u003cexecutor_id>","type":"text","marks":[{"type":"code_inline"}]},{"text":" ✅","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Rebalancer Controller","type":"text","marks":[{"type":"strong"}]},{"text":" — ","type":"text"},{"text":"manage_controller.py deploy","type":"text","marks":[{"type":"code_inline"}]},{"text":" (bot container, SQLite)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"visualize_lp_positions.py --pair \u003cpair>","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Not sure?","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Run ","type":"text"},{"text":"curl -s -u admin:admin -X POST http://localhost:8000/executors/search -H \"Content-Type: application/json\" -d '{\"type\":\"lp_executor\"}'","type":"text","marks":[{"type":"code_inline"}]},{"text":" — if the executor ID appears, use the executor scripts","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"If the user has been running an LP Executor in this session","type":"text","marks":[{"type":"strong"}]},{"text":" (executor ID is known from context), skip the question and go straight to:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python scripts/visualize_lp_executor.py --id \u003cexecutor_id>","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Available Scripts","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Script","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Purpose","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"scripts/export_lp_positions.py","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Export LP position events to CSV (SQLite/bot-container based)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"scripts/visualize_lp_positions.py","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Generate HTML dashboard from position events (SQLite/bot-container based)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"scripts/export_lp_executor.py","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Export a single LP executor to CSV by ","type":"text"},{"text":"--id","type":"text","marks":[{"type":"code_inline"}]},{"text":" (REST API, no SQLite)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"scripts/visualize_lp_executor.py","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Generate HTML dashboard for a single LP executor by ","type":"text"},{"text":"--id","type":"text","marks":[{"type":"code_inline"}]},{"text":" (REST API)","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Visualize LP Positions","type":"text"}]},{"type":"paragraph","content":[{"text":"Shows position ADD/REMOVE events from the blockchain. ","type":"text"},{"text":"Works for both running and stopped bots.","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Basic usage (auto-detects database in data/)\npython scripts/visualize_lp_positions.py --pair SOL-USDC\n\n# Specify database explicitly\npython scripts/visualize_lp_positions.py --db data/my_bot.sqlite --pair SOL-USDC\n\n# Filter by connector\npython scripts/visualize_lp_positions.py --pair SOL-USDC --connector meteora/clmm\n\n# Last 24 hours only\npython scripts/visualize_lp_positions.py --pair SOL-USDC --hours 24","type":"text"}]},{"type":"paragraph","content":[{"text":"Dashboard Features:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"KPI cards (total PnL, fees, IL, win/loss counts)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Cumulative PnL & fees chart","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Price at open/close with LP range bounds","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Per-position PnL bar chart","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Duration vs PnL scatter plot","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Sortable positions table with Solscan links","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Export to CSV","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Export all position events\npython scripts/export_lp_positions.py --db data/my_bot.sqlite\n\n# Filter by trading pair\npython scripts/export_lp_positions.py --pair SOL-USDC --output exports/positions.csv\n\n# Show summary without exporting\npython scripts/export_lp_positions.py --summary","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Executor Performance (API-based)","type":"text"}]},{"type":"paragraph","content":[{"text":"These scripts work directly from the ","type":"text"},{"text":"Hummingbot REST API","type":"text","marks":[{"type":"strong"}]},{"text":" — no SQLite database needed. Use them when executors were deployed via the API directly (e.g., via ","type":"text"},{"text":"manage_executor.py","type":"text","marks":[{"type":"code_inline"}]},{"text":"), because those do not always produce SQLite records the way bot containers do.","type":"text"}]},{"type":"paragraph","content":[{"text":"Export a single LP executor to CSV:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python scripts/export_lp_executor.py --id \u003cexecutor_id>\npython scripts/export_lp_executor.py --id \u003cexecutor_id> --output exports/my_run.csv\npython scripts/export_lp_executor.py --id \u003cexecutor_id> --print # JSON to stdout","type":"text"}]},{"type":"paragraph","content":[{"text":"CSV columns (LP executor schema):","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Identity:","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"id, account_name, controller_id, connector_name, trading_pair","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"State:","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"status, close_type, is_active, is_trading, error_count","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Timing:","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"created_at, closed_at, close_timestamp, duration_seconds","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"PnL:","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"net_pnl_quote, net_pnl_pct, cum_fees_quote, filled_amount_quote","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Config (deployment):","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"pool_address, lower_price, upper_price, base_amount_config, quote_amount_config, side, position_offset_pct, auto_close_above_range_seconds, auto_close_below_range_seconds, keep_position","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"custom_info (live/final):","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"state, position_address, current_price, lower_price_actual, upper_price_actual, base_amount_current, quote_amount_current, base_fee, quote_fee, fees_earned_quote, total_value_quote, unrealized_pnl_quote, position_rent, position_rent_refunded, tx_fee, out_of_range_seconds, max_retries_reached, initial_base_amount, initial_quote_amount","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"paragraph","content":[{"text":"Visualize a single LP executor (HTML dashboard):","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python scripts/visualize_lp_executor.py --id \u003cexecutor_id>\npython scripts/visualize_lp_executor.py --id \u003cexecutor_id> --output report.html\npython scripts/visualize_lp_executor.py --id \u003cexecutor_id> --no-open","type":"text"}]},{"type":"paragraph","content":[{"text":"Dashboard panels:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"KPI cards: status, net PnL, fees earned, duration, LP range","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Price chart with LP lower/upper bounds + open/close markers (5m KuCoin candles; auto-skipped for exotic pairs)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Token balance bar: initial vs final base + quote amounts","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"PnL breakdown: fees earned vs IL/price impact vs net PnL","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Full position summary table with Solscan links for pool and position addresses","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Dark theme (","type":"text"},{"text":"#0d1117","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"#161b27","type":"text","marks":[{"type":"code_inline"}]},{"text":"), responsive layout, Chart.js from CDN","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Auth auto-loaded from ","type":"text"},{"text":".env","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"~/.hummingbot/.env","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"~/.env","type":"text","marks":[{"type":"code_inline"}]},{"text":" (keys: ","type":"text"},{"text":"HUMMINGBOT_API_URL","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"API_USER","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"API_PASS","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Quick Reference","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Common Workflows","type":"text"}]},{"type":"paragraph","content":[{"text":"Full Setup (first time):","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# 1. Deploy API\nbash scripts/deploy_hummingbot_api.sh install\n\n# 2. Start Gateway\nbash scripts/setup_gateway.sh --rpc-url https://your-rpc-endpoint.com\n\n# 3. Add wallet\npython scripts/add_wallet.py add\n\n# 4. Find pool\npython scripts/list_meteora_pools.py --query SOL-USDC\n\n# 5. Check bin_step\npython scripts/get_meteora_pool.py \u003cpool_address>\n\n# 6. Create config and deploy\npython scripts/manage_controller.py create-config my_lp --pool \u003cpool_address> --pair SOL-USDC --amount 100\npython scripts/manage_controller.py deploy my_bot --configs my_lp\n\n# 7. Verify\npython scripts/manage_controller.py status","type":"text"}]},{"type":"paragraph","content":[{"text":"Analyze LP Positions:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Visualize\npython scripts/visualize_lp_positions.py --pair SOL-USDC\n\n# Export CSV\npython scripts/export_lp_positions.py --pair SOL-USDC","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Checking Prerequisites","type":"text"}]},{"type":"paragraph","content":[{"text":"Before running commands that need the API or Gateway, verify they're running:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"bash scripts/check_api.sh # Is Hummingbot API running?\nbash scripts/check_gateway.sh # Is Gateway running? (also checks API)","type":"text"}]},{"type":"paragraph","content":[{"text":"Both support ","type":"text"},{"text":"--json","type":"text","marks":[{"type":"code_inline"}]},{"text":" output. These scripts are also used internally by ","type":"text"},{"text":"setup_gateway.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" and can be sourced by other shell scripts.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Scripts Reference","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Script","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Purpose","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"check_api.sh","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Check if Hummingbot API is running (shared)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"check_gateway.sh","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Check if Gateway is running (shared)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"deploy_hummingbot_api.sh","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Install/upgrade/manage Hummingbot API","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"setup_gateway.sh","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Start Gateway and configure RPC","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add_wallet.py","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Add wallets and check balances","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"manage_gateway.py","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Advanced Gateway management","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"list_meteora_pools.py","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Search and list pools","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"get_meteora_pool.py","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Get pool details with liquidity chart","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"manage_executor.py","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Create, list, stop LP executors","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"manage_controller.py","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Create configs, deploy bots, get status","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"export_lp_positions.py","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Export position events to CSV (SQLite/bot-container)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"visualize_lp_positions.py","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Generate HTML dashboard (SQLite/bot-container)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"export_lp_executor.py","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Export single LP executor to CSV by ","type":"text"},{"text":"--id","type":"text","marks":[{"type":"code_inline"}]},{"text":" (REST API)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"visualize_lp_executor.py","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"HTML dashboard for single LP executor by ","type":"text"},{"text":"--id","type":"text","marks":[{"type":"code_inline"}]},{"text":" (REST API)","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Error Troubleshooting","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Error","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Cause","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Solution","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"\"InvalidRealloc\"","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Position range too wide","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Reduce ","type":"text"},{"text":"--width","type":"text","marks":[{"type":"code_inline"}]},{"text":" (check bin_step limits)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"State stuck \"OPENING\"","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Transaction failed","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Stop executor, reduce range, retry","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"\"Insufficient funds\" (wallet has funds)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"RPC rate limit or wrong amount units","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Add custom RPC key; verify ","type":"text"},{"text":"--amount","type":"text","marks":[{"type":"code_inline"}]},{"text":" is in quote asset","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"\"Transaction simulation failed\"","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"RPC rate limit masking as sim failure","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Add custom RPC key (Helius/QuickNode)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"'meteora/clmm'","type":"text","marks":[{"type":"code_inline"}]},{"text":" KeyError","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Gateway connector not registered yet","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Use hummingbot-api PR #120+ (","type":"text"},{"text":"feat/lp-executor","type":"text","marks":[{"type":"code_inline"}]},{"text":" branch)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"\"No CLMM pool found for X-Y\"","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Token symbol mismatch","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Use exact symbol from pool (e.g. ","type":"text"},{"text":"Percolator","type":"text","marks":[{"type":"code_inline"}]},{"text":" not ","type":"text"},{"text":"PRCLT","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"\"meteora/clmm is not ready\"","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Bot can't reach Gateway","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Run Gateway as Docker container with ","type":"text"},{"text":"--network host","type":"text","marks":[{"type":"code_inline"}]},{"text":"; dev-mode Gateway on macOS not reachable from containers","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"\"Failed to load strategy\" / password error","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Missing ","type":"text"},{"text":".password_verification","type":"text","marks":[{"type":"code_inline"}]},{"text":" in bot conf","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Copy from ","type":"text"},{"text":"bots/credentials/master_account/.password_verification","type":"text","marks":[{"type":"code_inline"}]},{"text":" to ","type":"text"},{"text":"bots/bots/instances/\u003cbot>/conf/","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"candles_config / markets extra inputs","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Stale script config format","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Use minimal config: only ","type":"text"},{"text":"controllers_config","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"script_file_name","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Bot shows \"stopped\" in API but running in Docker","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"MQTT not connected","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Bot is running but API can't see it via MQTT; check bot logs directly","type":"text"}]}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Critical Gotchas","type":"text"}]},{"type":"paragraph","content":[{"text":"These caused real confusion in production — read before deploying.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"1. ","type":"text"},{"text":"total_amount_quote","type":"text","marks":[{"type":"code_inline"}]},{"text":" is ALWAYS in quote asset","type":"text"}]},{"type":"paragraph","content":[{"text":"The quote asset is the ","type":"text"},{"text":"2nd token","type":"text","marks":[{"type":"strong"}]},{"text":" in the trading pair:","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Pair","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Quote Asset","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Example: deploy 100k base tokens worth ~$100","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Percolator-SOL","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"SOL","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--amount 1.33","type":"text","marks":[{"type":"code_inline"}]},{"text":" (1.33 SOL ≈ $108)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"SOL-USDC","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"USDC","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--amount 108","type":"text","marks":[{"type":"code_inline"}]},{"text":" (108 USDC)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"RAY-SOL","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"SOL","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--amount 2.5","type":"text","marks":[{"type":"code_inline"}]},{"text":" (2.5 SOL)","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"Even for ","type":"text"},{"text":"--side 2","type":"text","marks":[{"type":"code_inline"}]},{"text":" (SELL/base-only), you express the value in quote units. Convert: ","type":"text"},{"text":"base_tokens × price_in_quote = total_amount_quote","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"2. All ","type":"text"},{"text":"*_pct","type":"text","marks":[{"type":"code_inline"}]},{"text":" params are already in percent","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Param","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Correct","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Wrong","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"position_width_pct: 10","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"10% range","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"0.10","type":"text","marks":[{"type":"strikethrough"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"position_offset_pct: 1","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"1% offset","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"0.01","type":"text","marks":[{"type":"strikethrough"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"rebalance_threshold_pct: 1","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"1% threshold","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"0.01","type":"text","marks":[{"type":"strikethrough"}]}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"3. Token symbol must match pool exactly","type":"text"}]},{"type":"paragraph","content":[{"text":"When adding a token to Gateway and configuring ","type":"text"},{"text":"--pair","type":"text","marks":[{"type":"code_inline"}]},{"text":", use the ","type":"text"},{"text":"exact symbol from the pool","type":"text","marks":[{"type":"strong"}]},{"text":" (from ","type":"text"},{"text":"get_meteora_pool.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" or Meteora UI):","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Check pool token symbols first\npython scripts/get_meteora_pool.py \u003cpool_address>\n# Look at \"Symbol\" column — use that exact string in --pair and when adding token to Gateway","type":"text"}]},{"type":"paragraph","content":[{"text":"Example: Percolator token symbol is ","type":"text"},{"text":"Percolator","type":"text","marks":[{"type":"code_inline"}]},{"text":" (not ","type":"text"},{"text":"PRCLT","type":"text","marks":[{"type":"code_inline"}]},{"text":"). Use ","type":"text"},{"text":"--pair Percolator-SOL","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"4. Custom RPC is required before deploying (not optional)","type":"text"}]},{"type":"paragraph","content":[{"text":"The public Solana RPC (","type":"text"},{"text":"api.mainnet-beta.solana.com","type":"text","marks":[{"type":"code_inline"}]},{"text":") will rate-limit your bot, causing:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"Insufficient funds\" errors (even when wallet has funds)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"Transaction simulation failed\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Failed position opens after 10 retries","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Always configure a custom RPC before deploying:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Edit Gateway conf\nGATEWAY_DIR=~/.openclaw/workspace/hummingbot-gateway # or your gateway dir\nnano \"$GATEWAY_DIR/conf/chains/solana/mainnet-beta.yml\"\n# Set: nodeURL: https://mainnet.helius-rpc.com/?api-key=YOUR_KEY","type":"text"}]},{"type":"paragraph","content":[{"text":"Free key at https://helius.dev. Restart Gateway after.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"5. Gateway must run as Docker (not dev mode) on macOS","type":"text"}]},{"type":"paragraph","content":[{"text":"On macOS, Docker containers cannot reach processes running on the host (even with ","type":"text"},{"text":"--network host","type":"text","marks":[{"type":"code_inline"}]},{"text":"). The bot container ","type":"text"},{"text":"cannot","type":"text","marks":[{"type":"strong"}]},{"text":" connect to a Gateway running in dev mode (","type":"text"},{"text":"pnpm start","type":"text","marks":[{"type":"code_inline"}]},{"text":").","type":"text"}]},{"type":"paragraph","content":[{"text":"Always run Gateway as Docker:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"docker run -d --name gateway \\\n --network host \\\n -e GATEWAY_PASSPHRASE=hummingbot \\\n -v ~/.openclaw/workspace/hummingbot-gateway/conf:/home/gateway/conf \\\n -v ~/.openclaw/workspace/hummingbot-gateway/certs:/home/gateway/certs \\\n hummingbot/gateway:development","type":"text"}]},{"type":"paragraph","content":[{"text":"Then in bot's ","type":"text"},{"text":"conf_client.yml","type":"text","marks":[{"type":"code_inline"}]},{"text":": ","type":"text"},{"text":"gateway_api_host: localhost","type":"text","marks":[{"type":"code_inline"}]},{"text":" (host networking resolves correctly inside Docker on macOS when gateway is also Docker with host network).","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"6. Use ","type":"text"},{"text":"hummingbot:development","type":"text","marks":[{"type":"code_inline"}]},{"text":" image, not ","type":"text"},{"text":"latest","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"The ","type":"text"},{"text":"latest","type":"text","marks":[{"type":"code_inline"}]},{"text":" image may not have LP executor support. Always deploy with:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python scripts/manage_controller.py deploy my_bot --configs my_config\n# manage_controller.py defaults to hummingbot/hummingbot:development","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"7. Check wallet balance BEFORE calculating ","type":"text"},{"text":"--amount","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python scripts/add_wallet.py balances","type":"text"}]},{"type":"paragraph","content":[{"text":"Check which token account has funds — the wallet may have multiple SPL token accounts and balances can differ from expectations. Only the available balance in the specific token account can be deployed.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"8. Closing positions","type":"text"}]},{"type":"paragraph","content":[{"text":"To close all positions on a pool:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# List open positions\ncurl -s \"http://localhost:15888/connectors/meteora/clmm/positions-owned?network=mainnet-beta&walletAddress=\u003cWALLET>\" \n\n# Close each position\ncurl -s -X POST http://localhost:15888/connectors/meteora/clmm/close-position \\\n -H \"Content-Type: application/json\" \\\n -d '{\"network\":\"mainnet-beta\",\"address\":\"\u003cWALLET>\",\"positionAddress\":\"\u003cPOSITION>\"}'","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Note: Gateway must be accessible. On macOS use ","type":"text"},{"text":"docker run --rm --network host alpine/curl ...","type":"text","marks":[{"type":"code_inline"}]},{"text":" if Gateway is in Docker with host networking. | \"Insufficient balance\" | Not enough tokens | Check wallet has tokens + 0.06 SOL for rent |","type":"text"}]}]},"metadata":{"date":"2026-06-05","name":"lp-agent","author":"@skillopedia","source":{"stars":28,"repo_name":"skills","origin_url":"https://github.com/hummingbot/skills/blob/HEAD/skills/lp-agent/SKILL.md","repo_owner":"hummingbot","body_sha256":"a5c129a335d77af4f222ceb3b7dc4da15e5cf01da3f90251ad29073f7d6918e5","cluster_key":"40b787f858b830f49676ed220eec1041608fd53db896bac0a0cfdb292094c10a","clean_bundle":{"format":"clean-skill-bundle-v1","source":"hummingbot/skills/skills/lp-agent/SKILL.md","attachments":[{"id":"1014ae25-645b-5d46-81ad-cda30f17b64a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1014ae25-645b-5d46-81ad-cda30f17b64a/attachment.md","path":"references/lp_executor_guide.md","size":5893,"sha256":"0040aeee9558498546173bb6d6b6be353844d7160eabf088f31efb1d607f571f","contentType":"text/markdown; charset=utf-8"},{"id":"f13e52c1-566d-56d4-89c6-5d1c8cba25dd","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f13e52c1-566d-56d4-89c6-5d1c8cba25dd/attachment.md","path":"references/lp_rebalancer_guide.md","size":16015,"sha256":"63b97f02c3fca8f61b5b993d49ed4a003a295e0286ba06f4b352ee31db975266","contentType":"text/markdown; charset=utf-8"},{"id":"1356591d-1a34-5b69-b1f7-0bc5eaebdfc6","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1356591d-1a34-5b69-b1f7-0bc5eaebdfc6/attachment.md","path":"scripts/README.md","size":21069,"sha256":"56446ee857bd0fc4419ccc1393366c16350bb995286dc688ae729a23affd8e25","contentType":"text/markdown; charset=utf-8"},{"id":"93f3f4f9-e7a3-56dd-b443-ec666f82561c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/93f3f4f9-e7a3-56dd-b443-ec666f82561c/attachment.py","path":"scripts/add_wallet.py","size":8111,"sha256":"350b1c35eeea2b131101fd563f96aaff14dc4b5af8d7c10ea867b3b94e92547f","contentType":"text/x-python; charset=utf-8"},{"id":"00b45dba-97ce-504c-8126-b2c61815bab2","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/00b45dba-97ce-504c-8126-b2c61815bab2/attachment.sh","path":"scripts/check_api.sh","size":1474,"sha256":"b7e745199e0b3dafaaf7e29ca06739f64596ef6f6923fa8beb576ade0f8be476","contentType":"application/x-sh; charset=utf-8"},{"id":"bd248487-eda6-5a5e-bf27-e79b71c204e7","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/bd248487-eda6-5a5e-bf27-e79b71c204e7/attachment.sh","path":"scripts/check_gateway.sh","size":2319,"sha256":"898cdca16e5ef8b7227bbfb9522842961e3546dbed60a87c8597f6b1742784e7","contentType":"application/x-sh; charset=utf-8"},{"id":"1294d9ab-bf98-5596-b1b1-3e12b0edb82e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1294d9ab-bf98-5596-b1b1-3e12b0edb82e/attachment.sh","path":"scripts/deploy_hummingbot_api.sh","size":7059,"sha256":"93df93a6b3250dd7d19b8dfef0e33f4698ca5309848c8e728c5f8d3fb80825a4","contentType":"application/x-sh; charset=utf-8"},{"id":"07b48145-334d-5c08-a921-4242cb4ea758","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/07b48145-334d-5c08-a921-4242cb4ea758/attachment.py","path":"scripts/export_lp_executor.py","size":11546,"sha256":"e7f7440053425a2e7ce7e508b07ffb32cb17596f03f2c406457ad03e85f23331","contentType":"text/x-python; charset=utf-8"},{"id":"2fe2694d-a83a-5238-9b79-ffe8cdd6a4cb","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/2fe2694d-a83a-5238-9b79-ffe8cdd6a4cb/attachment.py","path":"scripts/export_lp_positions.py","size":8228,"sha256":"650abaee184f425ae632ad259b1860cca05939e4ddcffaffb42b175d2bccc053","contentType":"text/x-python; charset=utf-8"},{"id":"34172e3a-6cf4-592d-9764-736a59a202bc","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/34172e3a-6cf4-592d-9764-736a59a202bc/attachment.py","path":"scripts/get_meteora_pool.py","size":15442,"sha256":"730b7917681fe353f842ac03574a315ea50ce8d8ff29749f57aa47955f6be414","contentType":"text/x-python; charset=utf-8"},{"id":"c0d01205-5fdc-5abb-8568-b3db87df779c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c0d01205-5fdc-5abb-8568-b3db87df779c/attachment.py","path":"scripts/list_meteora_pools.py","size":7964,"sha256":"ef945823a955da58cab40d94fd7f48404fe2afc690de060f7c0e026d6e0efe50","contentType":"text/x-python; charset=utf-8"},{"id":"1262f4f6-22ca-539c-972c-183ce8a15f49","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1262f4f6-22ca-539c-972c-183ce8a15f49/attachment.py","path":"scripts/manage_controller.py","size":16998,"sha256":"ab9ef2b87b1f39f2794a8aeaf64fbb7d262ffce351237bdf3e5d18d22d45098d","contentType":"text/x-python; charset=utf-8"},{"id":"b8c1148e-c59a-5921-84c9-38434d7efcb8","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b8c1148e-c59a-5921-84c9-38434d7efcb8/attachment.py","path":"scripts/manage_executor.py","size":15986,"sha256":"96be865c844b8dd3ab776e7a7515f3eb446377098c347f22f3435f879a1b899a","contentType":"text/x-python; charset=utf-8"},{"id":"ab8fcf9b-a98a-5dd0-9a31-d5003bf7869f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ab8fcf9b-a98a-5dd0-9a31-d5003bf7869f/attachment.py","path":"scripts/manage_gateway.py","size":11023,"sha256":"907110205219181f5f54bac64dd6e7a077a8a45b0a94c00c8eee30b5e2a9c681","contentType":"text/x-python; charset=utf-8"},{"id":"e9511d54-c723-54a4-8435-7578b052ff40","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e9511d54-c723-54a4-8435-7578b052ff40/attachment.sh","path":"scripts/setup_gateway.sh","size":6570,"sha256":"2fdd23333f5c2e080c35805bb60ba15b0929ec66a84030e245bf731d0801f214","contentType":"application/x-sh; charset=utf-8"},{"id":"fce72b64-62a4-5171-8894-924e36591d99","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/fce72b64-62a4-5171-8894-924e36591d99/attachment.py","path":"scripts/visualize_lp_executor.py","size":26267,"sha256":"c53a618f294e8ddaf419559c73a01b523a8565e3a5dcba20bc4a597865407d53","contentType":"text/x-python; charset=utf-8"},{"id":"dd9e0fb3-ff8e-50bd-b09f-99a2b430289a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/dd9e0fb3-ff8e-50bd-b09f-99a2b430289a/attachment.py","path":"scripts/visualize_lp_positions.py","size":47439,"sha256":"733a53fb3f06de1fac3c4c08cb380a50bcb50a06b1532bcd8a996fc5892bee70","contentType":"text/x-python; charset=utf-8"}],"bundle_sha256":"7430cfbcaba3dcc50f1f700056a41f862376ab1d0505f46f518790844d623148","attachment_count":17,"text_attachments":17,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":0,"excluded_attachments":[]},"cluster_size":1,"skill_md_path":"skills/lp-agent/SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"web-development","category_label":"Web"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"web-development","commands":{"start":{"description":"Onboarding wizard — check setup status and get started"},"add-wallet":{"description":"Add or import a Solana wallet for trading"},"run-strategy":{"description":"Run, monitor, and manage LP strategies"},"explore-pools":{"description":"Find and explore Meteora DLMM pools"},"setup-gateway":{"description":"Start Gateway and configure network RPC endpoints"},"select-strategy":{"description":"Choose between LP Executor or Rebalancer Controller strategy"},"analyze-performance":{"description":"Export data and visualize LP position performance"},"deploy-hummingbot-api":{"description":"Deploy Hummingbot API trading infrastructure"}},"metadata":{"author":"hummingbot"},"import_tag":"clean-skills-v1","description":"Run automated liquidity provision strategies on concentrated liquidity (CLMM) DEXs using Hummingbot API."}},"renderedAt":1782979946460}

lp-agent This skill helps you run automated liquidity provision strategies on concentrated liquidity (CLMM) DEXs using Hummingbot API. Commands (run as ): | Command | Description | |---------|-------------| | | Onboarding wizard — check setup status and get started | | | Deploy Hummingbot API trading infrastructure | | | Start Gateway, configure network RPC endpoints | | | Add or import a Solana wallet | | | Find and explore Meteora DLMM pools | | | Choose LP Executor or Rebalancer Controller | | | Run, monitor, and manage LP strategies | | | Visualize LP position performance | New here? Run…