Files
aitbc/cli/aitbc_cli/commands/market_maker.py
oib 15427c96c0 chore: update file permissions to executable across repository
- Change file mode from 644 to 755 for all project files
- Add chain_id parameter to get_balance RPC endpoint with default "ait-devnet"
- Rename Miner.extra_meta_data to extra_metadata for consistency
2026-03-06 22:17:54 +01:00

797 lines
26 KiB
Python
Executable File

"""Market making commands for AITBC CLI"""
import click
import json
import uuid
import httpx
from pathlib import Path
from typing import Optional, Dict, Any, List
from datetime import datetime, timedelta
from ..utils import output, error, success, warning
@click.group()
def market_maker():
"""Market making bot management commands"""
pass
@market_maker.command()
@click.option("--exchange", required=True, help="Exchange name")
@click.option("--pair", required=True, help="Trading pair symbol (e.g., AITBC/BTC)")
@click.option("--spread", type=float, default=0.005, help="Bid-ask spread (as percentage)")
@click.option("--depth", type=float, default=1000000, help="Order book depth amount")
@click.option("--max-order-size", type=float, default=1000, help="Maximum order size")
@click.option("--min-order-size", type=float, default=10, help="Minimum order size")
@click.option("--target-inventory", type=float, default=50000, help="Target inventory balance")
@click.option("--rebalance-threshold", type=float, default=0.1, help="Inventory rebalance threshold")
@click.option("--description", help="Bot description")
@click.pass_context
def create(ctx, exchange: str, pair: str, spread: float, depth: float, max_order_size: float, min_order_size: float, target_inventory: float, rebalance_threshold: float, description: Optional[str]):
"""Create a new market making bot"""
# Generate unique bot ID
bot_id = f"mm_{exchange.lower()}_{pair.replace('/', '_')}_{str(uuid.uuid4())[:8]}"
# Create bot configuration
bot_config = {
"bot_id": bot_id,
"exchange": exchange,
"pair": pair,
"status": "stopped",
"strategy": "basic_market_making",
"config": {
"spread": spread,
"depth": depth,
"max_order_size": max_order_size,
"min_order_size": min_order_size,
"target_inventory": target_inventory,
"rebalance_threshold": rebalance_threshold
},
"performance": {
"total_trades": 0,
"total_volume": 0.0,
"total_profit": 0.0,
"inventory_value": 0.0,
"orders_placed": 0,
"orders_filled": 0
},
"created_at": datetime.utcnow().isoformat(),
"last_updated": None,
"description": description or f"Market making bot for {pair} on {exchange}",
"current_orders": [],
"inventory": {
"base_asset": 0.0,
"quote_asset": target_inventory
}
}
# Store bot configuration
bots_file = Path.home() / ".aitbc" / "market_makers.json"
bots_file.parent.mkdir(parents=True, exist_ok=True)
# Load existing bots
bots = {}
if bots_file.exists():
with open(bots_file, 'r') as f:
bots = json.load(f)
# Add new bot
bots[bot_id] = bot_config
# Save bots
with open(bots_file, 'w') as f:
json.dump(bots, f, indent=2)
success(f"Market making bot created: {bot_id}")
output({
"bot_id": bot_id,
"exchange": exchange,
"pair": pair,
"status": "created",
"spread": spread,
"depth": depth,
"created_at": bot_config["created_at"]
})
@market_maker.command()
@click.option("--bot-id", required=True, help="Bot ID to configure")
@click.option("--spread", type=float, help="New bid-ask spread")
@click.option("--depth", type=float, help="New order book depth")
@click.option("--max-order-size", type=float, help="New maximum order size")
@click.option("--target-inventory", type=float, help="New target inventory")
@click.option("--rebalance-threshold", type=float, help="New rebalance threshold")
@click.pass_context
def config(ctx, bot_id: str, spread: Optional[float], depth: Optional[float], max_order_size: Optional[float], target_inventory: Optional[float], rebalance_threshold: Optional[float]):
"""Configure market making bot parameters"""
# Load bots
bots_file = Path.home() / ".aitbc" / "market_makers.json"
if not bots_file.exists():
error("No market making bots found.")
return
with open(bots_file, 'r') as f:
bots = json.load(f)
if bot_id not in bots:
error(f"Bot '{bot_id}' not found.")
return
bot = bots[bot_id]
# Update configuration
config_updates = {}
if spread is not None:
bot["config"]["spread"] = spread
config_updates["spread"] = spread
if depth is not None:
bot["config"]["depth"] = depth
config_updates["depth"] = depth
if max_order_size is not None:
bot["config"]["max_order_size"] = max_order_size
config_updates["max_order_size"] = max_order_size
if target_inventory is not None:
bot["config"]["target_inventory"] = target_inventory
config_updates["target_inventory"] = target_inventory
if rebalance_threshold is not None:
bot["config"]["rebalance_threshold"] = rebalance_threshold
config_updates["rebalance_threshold"] = rebalance_threshold
if not config_updates:
error("No configuration updates provided.")
return
# Update timestamp
bot["last_updated"] = datetime.utcnow().isoformat()
# Save bots
with open(bots_file, 'w') as f:
json.dump(bots, f, indent=2)
success(f"Bot '{bot_id}' configuration updated")
output({
"bot_id": bot_id,
"config_updates": config_updates,
"updated_at": bot["last_updated"]
})
@market_maker.command()
@click.option("--bot-id", required=True, help="Bot ID to start")
@click.option("--dry-run", is_flag=True, help="Run in simulation mode without real orders")
@click.pass_context
def start(ctx, bot_id: str, dry_run: bool):
"""Start a market making bot"""
# Load bots
bots_file = Path.home() / ".aitbc" / "market_makers.json"
if not bots_file.exists():
error("No market making bots found.")
return
with open(bots_file, 'r') as f:
bots = json.load(f)
if bot_id not in bots:
error(f"Bot '{bot_id}' not found.")
return
bot = bots[bot_id]
# Check if bot is already running
if bot["status"] == "running":
warning(f"Bot '{bot_id}' is already running.")
return
# Update bot status
bot["status"] = "running" if not dry_run else "simulation"
bot["started_at"] = datetime.utcnow().isoformat()
bot["last_updated"] = datetime.utcnow().isoformat()
bot["dry_run"] = dry_run
# Initialize performance tracking for this run
bot["current_run"] = {
"started_at": bot["started_at"],
"orders_placed": 0,
"orders_filled": 0,
"total_volume": 0.0,
"total_profit": 0.0
}
# Save bots
with open(bots_file, 'w') as f:
json.dump(bots, f, indent=2)
mode = "simulation" if dry_run else "live"
success(f"Bot '{bot_id}' started in {mode} mode")
output({
"bot_id": bot_id,
"status": bot["status"],
"mode": mode,
"started_at": bot["started_at"],
"exchange": bot["exchange"],
"pair": bot["pair"]
})
@market_maker.command()
@click.option("--bot-id", required=True, help="Bot ID to stop")
@click.pass_context
def stop(ctx, bot_id: str):
"""Stop a market making bot"""
# Load bots
bots_file = Path.home() / ".aitbc" / "market_makers.json"
if not bots_file.exists():
error("No market making bots found.")
return
with open(bots_file, 'r') as f:
bots = json.load(f)
if bot_id not in bots:
error(f"Bot '{bot_id}' not found.")
return
bot = bots[bot_id]
# Check if bot is running
if bot["status"] not in ["running", "simulation"]:
warning(f"Bot '{bot_id}' is not currently running.")
return
# Update bot status
bot["status"] = "stopped"
bot["stopped_at"] = datetime.utcnow().isoformat()
bot["last_updated"] = datetime.utcnow().isoformat()
# Cancel all current orders (simulation)
bot["current_orders"] = []
# Save bots
with open(bots_file, 'w') as f:
json.dump(bots, f, indent=2)
success(f"Bot '{bot_id}' stopped")
output({
"bot_id": bot_id,
"status": "stopped",
"stopped_at": bot["stopped_at"],
"final_performance": bot.get("current_run", {})
})
@market_maker.command()
@click.option("--bot-id", help="Specific bot ID to check")
@click.option("--exchange", help="Filter by exchange")
@click.option("--pair", help="Filter by trading pair")
@click.pass_context
def performance(ctx, bot_id: Optional[str], exchange: Optional[str], pair: Optional[str]):
"""Get performance metrics for market making bots"""
# Load bots
bots_file = Path.home() / ".aitbc" / "market_makers.json"
if not bots_file.exists():
error("No market making bots found.")
return
with open(bots_file, 'r') as f:
bots = json.load(f)
# Filter bots
performance_data = {}
for current_bot_id, bot in bots.items():
if bot_id and current_bot_id != bot_id:
continue
if exchange and bot["exchange"] != exchange:
continue
if pair and bot["pair"] != pair:
continue
# Calculate performance metrics
perf = bot.get("performance", {})
current_run = bot.get("current_run", {})
bot_performance = {
"bot_id": current_bot_id,
"exchange": bot["exchange"],
"pair": bot["pair"],
"status": bot["status"],
"created_at": bot["created_at"],
"total_trades": perf.get("total_trades", 0),
"total_volume": perf.get("total_volume", 0.0),
"total_profit": perf.get("total_profit", 0.0),
"orders_placed": perf.get("orders_placed", 0),
"orders_filled": perf.get("orders_filled", 0),
"fill_rate": (perf.get("orders_filled", 0) / max(perf.get("orders_placed", 1), 1)) * 100,
"current_inventory": bot.get("inventory", {}),
"current_orders": len(bot.get("current_orders", [])),
"strategy": bot.get("strategy", "unknown"),
"config": bot.get("config", {})
}
# Add current run data if available
if current_run:
bot_performance["current_run"] = current_run
if "started_at" in current_run:
start_time = datetime.fromisoformat(current_run["started_at"].replace('Z', '+00:00'))
runtime = datetime.utcnow() - start_time
bot_performance["run_time_hours"] = runtime.total_seconds() / 3600
performance_data[current_bot_id] = bot_performance
if not performance_data:
error("No market making bots found matching the criteria.")
return
output({
"performance_data": performance_data,
"total_bots": len(performance_data),
"generated_at": datetime.utcnow().isoformat()
})
@market_maker.command()
@click.pass_context
def list(ctx):
"""List all market making bots"""
# Load bots
bots_file = Path.home() / ".aitbc" / "market_makers.json"
if not bots_file.exists():
warning("No market making bots found.")
return
with open(bots_file, 'r') as f:
bots = json.load(f)
# Format bot list
bot_list = []
for bot_id, bot in bots.items():
bot_info = {
"bot_id": bot_id,
"exchange": bot["exchange"],
"pair": bot["pair"],
"status": bot["status"],
"strategy": bot.get("strategy", "unknown"),
"created_at": bot["created_at"],
"last_updated": bot.get("last_updated"),
"total_trades": bot.get("performance", {}).get("total_trades", 0),
"current_orders": len(bot.get("current_orders", []))
}
bot_list.append(bot_info)
output({
"market_makers": bot_list,
"total_bots": len(bot_list),
"running_bots": len([b for b in bot_list if b["status"] in ["running", "simulation"]]),
"stopped_bots": len([b for b in bot_list if b["status"] == "stopped"])
})
@market_maker.command()
@click.argument("bot_id")
@click.pass_context
def status(ctx, bot_id: str):
"""Get detailed status of a specific market making bot"""
# Load bots
bots_file = Path.home() / ".aitbc" / "market_makers.json"
if not bots_file.exists():
error("No market making bots found.")
return
with open(bots_file, 'r') as f:
bots = json.load(f)
if bot_id not in bots:
error(f"Bot '{bot_id}' not found.")
return
bot = bots[bot_id]
# Calculate uptime if running
uptime_hours = None
if bot["status"] in ["running", "simulation"] and "started_at" in bot:
start_time = datetime.fromisoformat(bot["started_at"].replace('Z', '+00:00'))
uptime = datetime.utcnow() - start_time
uptime_hours = uptime.total_seconds() / 3600
output({
"bot_id": bot_id,
"exchange": bot["exchange"],
"pair": bot["pair"],
"status": bot["status"],
"strategy": bot.get("strategy", "unknown"),
"config": bot.get("config", {}),
"performance": bot.get("performance", {}),
"inventory": bot.get("inventory", {}),
"current_orders": bot.get("current_orders", []),
"created_at": bot["created_at"],
"last_updated": bot.get("last_updated"),
"started_at": bot.get("started_at"),
"stopped_at": bot.get("stopped_at"),
"uptime_hours": uptime_hours,
"dry_run": bot.get("dry_run", False),
"description": bot.get("description")
})
@market_maker.command()
@click.argument("bot_id")
@click.pass_context
def remove(ctx, bot_id: str):
"""Remove a market making bot"""
# Load bots
bots_file = Path.home() / ".aitbc" / "market_makers.json"
if not bots_file.exists():
error("No market making bots found.")
return
with open(bots_file, 'r') as f:
bots = json.load(f)
if bot_id not in bots:
error(f"Bot '{bot_id}' not found.")
return
bot = bots[bot_id]
# Check if bot is running
if bot["status"] in ["running", "simulation"]:
error(f"Cannot remove bot '{bot_id}' while it is running. Stop it first.")
return
# Remove bot
del bots[bot_id]
# Save bots
with open(bots_file, 'w') as f:
json.dump(bots, f, indent=2)
success(f"Market making bot '{bot_id}' removed")
output({
"bot_id": bot_id,
"status": "removed",
"exchange": bot["exchange"],
"pair": bot["pair"]
})
@click.group()
def market_maker():
"""Market making operations"""
pass
@market_maker.command()
@click.option("--exchange", required=True, help="Exchange name (e.g., Binance, Coinbase)")
@click.option("--pair", required=True, help="Trading pair (e.g., AITBC/BTC)")
@click.option("--spread", type=float, default=0.001, help="Bid-ask spread (as percentage)")
@click.option("--depth", type=int, default=5, help="Order book depth levels")
@click.option("--base-balance", type=float, help="Base asset balance for market making")
@click.option("--quote-balance", type=float, help="Quote asset balance for market making")
@click.option("--min-order-size", type=float, help="Minimum order size")
@click.option("--max-order-size", type=float, help="Maximum order size")
@click.option("--strategy", default="simple", help="Market making strategy")
@click.pass_context
def create(ctx, exchange: str, pair: str, spread: float, depth: int,
base_balance: Optional[float], quote_balance: Optional[float],
min_order_size: Optional[float], max_order_size: Optional[float],
strategy: str):
"""Create a new market making bot"""
config = ctx.obj['config']
bot_config = {
"exchange": exchange,
"pair": pair,
"spread": spread,
"depth": depth,
"strategy": strategy,
"status": "created"
}
if base_balance is not None:
bot_config["base_balance"] = base_balance
if quote_balance is not None:
bot_config["quote_balance"] = quote_balance
if min_order_size is not None:
bot_config["min_order_size"] = min_order_size
if max_order_size is not None:
bot_config["max_order_size"] = max_order_size
try:
with httpx.Client() as client:
response = client.post(
f"{config.coordinator_url}/api/v1/market-maker/create",
json=bot_config,
timeout=10
)
if response.status_code == 200:
result = response.json()
success(f"Market maker bot created for '{pair}' on '{exchange}'!")
success(f"Bot ID: {result.get('bot_id')}")
output(result, ctx.obj['output_format'])
else:
error(f"Failed to create market maker: {response.status_code}")
if response.text:
error(f"Error details: {response.text}")
except Exception as e:
error(f"Network error: {e}")
@market_maker.command()
@click.option("--bot-id", required=True, help="Market maker bot ID")
@click.option("--spread", type=float, help="New bid-ask spread")
@click.option("--depth", type=int, help="New order book depth")
@click.option("--base-balance", type=float, help="New base asset balance")
@click.option("--quote-balance", type=float, help="New quote asset balance")
@click.option("--min-order-size", type=float, help="New minimum order size")
@click.option("--max-order-size", type=float, help="New maximum order size")
@click.option("--strategy", help="New market making strategy")
@click.pass_context
def config(ctx, bot_id: str, spread: Optional[float], depth: Optional[int],
base_balance: Optional[float], quote_balance: Optional[float],
min_order_size: Optional[float], max_order_size: Optional[float],
strategy: Optional[str]):
"""Configure market maker bot settings"""
config = ctx.obj['config']
updates = {}
if spread is not None:
updates["spread"] = spread
if depth is not None:
updates["depth"] = depth
if base_balance is not None:
updates["base_balance"] = base_balance
if quote_balance is not None:
updates["quote_balance"] = quote_balance
if min_order_size is not None:
updates["min_order_size"] = min_order_size
if max_order_size is not None:
updates["max_order_size"] = max_order_size
if strategy is not None:
updates["strategy"] = strategy
if not updates:
error("No configuration updates provided")
return
try:
with httpx.Client() as client:
response = client.post(
f"{config.coordinator_url}/api/v1/market-maker/config/{bot_id}",
json=updates,
timeout=10
)
if response.status_code == 200:
result = response.json()
success(f"Market maker {bot_id} configured successfully!")
output(result, ctx.obj['output_format'])
else:
error(f"Failed to configure market maker: {response.status_code}")
if response.text:
error(f"Error details: {response.text}")
except Exception as e:
error(f"Network error: {e}")
@market_maker.command()
@click.option("--bot-id", required=True, help="Market maker bot ID")
@click.option("--dry-run", is_flag=True, help="Test run without executing real trades")
@click.pass_context
def start(ctx, bot_id: str, dry_run: bool):
"""Start market maker bot"""
config = ctx.obj['config']
start_data = {
"dry_run": dry_run
}
try:
with httpx.Client() as client:
response = client.post(
f"{config.coordinator_url}/api/v1/market-maker/start/{bot_id}",
json=start_data,
timeout=10
)
if response.status_code == 200:
result = response.json()
mode = " (dry run)" if dry_run else ""
success(f"Market maker {bot_id} started{mode}!")
output(result, ctx.obj['output_format'])
else:
error(f"Failed to start market maker: {response.status_code}")
if response.text:
error(f"Error details: {response.text}")
except Exception as e:
error(f"Network error: {e}")
@market_maker.command()
@click.option("--bot-id", required=True, help="Market maker bot ID")
@click.pass_context
def stop(ctx, bot_id: str):
"""Stop market maker bot"""
config = ctx.obj['config']
try:
with httpx.Client() as client:
response = client.post(
f"{config.coordinator_url}/api/v1/market-maker/stop/{bot_id}",
timeout=10
)
if response.status_code == 200:
result = response.json()
success(f"Market maker {bot_id} stopped!")
output(result, ctx.obj['output_format'])
else:
error(f"Failed to stop market maker: {response.status_code}")
if response.text:
error(f"Error details: {response.text}")
except Exception as e:
error(f"Network error: {e}")
@market_maker.command()
@click.option("--bot-id", help="Specific bot ID to check")
@click.option("--exchange", help="Filter by exchange")
@click.option("--pair", help="Filter by trading pair")
@click.option("--status", help="Filter by status (running, stopped, created)")
@click.pass_context
def performance(ctx, bot_id: Optional[str], exchange: Optional[str],
pair: Optional[str], status: Optional[str]):
"""Get market maker performance analytics"""
config = ctx.obj['config']
params = {}
if bot_id:
params["bot_id"] = bot_id
if exchange:
params["exchange"] = exchange
if pair:
params["pair"] = pair
if status:
params["status"] = status
try:
with httpx.Client() as client:
response = client.get(
f"{config.coordinator_url}/api/v1/market-maker/performance",
params=params,
timeout=10
)
if response.status_code == 200:
performance_data = response.json()
success("Market maker performance:")
output(performance_data, ctx.obj['output_format'])
else:
error(f"Failed to get performance data: {response.status_code}")
except Exception as e:
error(f"Network error: {e}")
@market_maker.command()
@click.option("--bot-id", help="Specific bot ID to list")
@click.option("--exchange", help="Filter by exchange")
@click.option("--pair", help="Filter by trading pair")
@click.option("--status", help="Filter by status")
@click.pass_context
def list(ctx, bot_id: Optional[str], exchange: Optional[str],
pair: Optional[str], status: Optional[str]):
"""List market maker bots"""
config = ctx.obj['config']
params = {}
if bot_id:
params["bot_id"] = bot_id
if exchange:
params["exchange"] = exchange
if pair:
params["pair"] = pair
if status:
params["status"] = status
try:
with httpx.Client() as client:
response = client.get(
f"{config.coordinator_url}/api/v1/market-maker/list",
params=params,
timeout=10
)
if response.status_code == 200:
bots = response.json()
success("Market maker bots:")
output(bots, ctx.obj['output_format'])
else:
error(f"Failed to list market makers: {response.status_code}")
except Exception as e:
error(f"Network error: {e}")
@market_maker.command()
@click.option("--bot-id", required=True, help="Market maker bot ID")
@click.option("--hours", type=int, default=24, help="Hours of history to retrieve")
@click.pass_context
def history(ctx, bot_id: str, hours: int):
"""Get market maker trading history"""
config = ctx.obj['config']
params = {
"hours": hours
}
try:
with httpx.Client() as client:
response = client.get(
f"{config.coordinator_url}/api/v1/market-maker/history/{bot_id}",
params=params,
timeout=10
)
if response.status_code == 200:
history_data = response.json()
success(f"Market maker {bot_id} history (last {hours} hours):")
output(history_data, ctx.obj['output_format'])
else:
error(f"Failed to get market maker history: {response.status_code}")
except Exception as e:
error(f"Network error: {e}")
@market_maker.command()
@click.option("--bot-id", required=True, help="Market maker bot ID")
@click.pass_context
def status(ctx, bot_id: str):
"""Get market maker bot status"""
config = ctx.obj['config']
try:
with httpx.Client() as client:
response = client.get(
f"{config.coordinator_url}/api/v1/market-maker/status/{bot_id}",
timeout=10
)
if response.status_code == 200:
status_data = response.json()
success(f"Market maker {bot_id} status:")
output(status_data, ctx.obj['output_format'])
else:
error(f"Failed to get market maker status: {response.status_code}")
except Exception as e:
error(f"Network error: {e}")
@market_maker.command()
@click.pass_context
def strategies(ctx):
"""List available market making strategies"""
config = ctx.obj['config']
try:
with httpx.Client() as client:
response = client.get(
f"{config.coordinator_url}/api/v1/market-maker/strategies",
timeout=10
)
if response.status_code == 200:
strategies = response.json()
success("Available market making strategies:")
output(strategies, ctx.obj['output_format'])
else:
error(f"Failed to list strategies: {response.status_code}")
except Exception as e:
error(f"Network error: {e}")