Skip to content

Commit 8821982

Browse files
committed
Improve error handling and API messages; add validation of strategy parameters
1 parent 54192ee commit 8821982

File tree

3 files changed

+295
-117
lines changed

3 files changed

+295
-117
lines changed

routers/backtest.py

Lines changed: 76 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from config import CONTROLLERS_MODULE, CONTROLLERS_PATH
1010
from routers.backtest_models import BacktestResponse, BacktestResults, BacktestingConfig, ExecutorInfo, ProcessedData
11+
from routers.strategies_models import StrategyError
1112

1213
router = APIRouter(tags=["Market Backtesting"])
1314
candles_factory = CandlesFactory()
@@ -19,37 +20,84 @@
1920
"market_making": market_making_backtesting
2021
}
2122

23+
class BacktestError(StrategyError):
24+
"""Base class for backtesting-related errors"""
25+
26+
class BacktestConfigError(BacktestError):
27+
"""Raised when there's an error in the backtesting configuration"""
28+
29+
class BacktestEngineError(BacktestError):
30+
"""Raised when there's an error during backtesting execution"""
31+
2232
@router.post("/backtest", response_model=BacktestResponse)
2333
async def run_backtesting(backtesting_config: BacktestingConfig) -> BacktestResponse:
2434
try:
25-
if isinstance(backtesting_config.config, str):
26-
controller_config = BacktestingEngineBase.get_controller_config_instance_from_yml(
27-
config_path=backtesting_config.config,
28-
controllers_conf_dir_path=CONTROLLERS_PATH,
29-
controllers_module=CONTROLLERS_MODULE
30-
)
31-
else:
32-
controller_config = BacktestingEngineBase.get_controller_config_instance_from_dict(
33-
config_data=backtesting_config.config,
34-
controllers_module=CONTROLLERS_MODULE
35-
)
35+
# Load and validate controller config
36+
try:
37+
if isinstance(backtesting_config.config, str):
38+
controller_config = BacktestingEngineBase.get_controller_config_instance_from_yml(
39+
config_path=backtesting_config.config,
40+
controllers_conf_dir_path=CONTROLLERS_PATH,
41+
controllers_module=CONTROLLERS_MODULE
42+
)
43+
else:
44+
controller_config = BacktestingEngineBase.get_controller_config_instance_from_dict(
45+
config_data=backtesting_config.config,
46+
controllers_module=CONTROLLERS_MODULE
47+
)
48+
except Exception as e:
49+
raise BacktestConfigError(f"Invalid controller configuration: {str(e)}")
50+
51+
# Get and validate backtesting engine
3652
backtesting_engine = BACKTESTING_ENGINES.get(controller_config.controller_type)
3753
if not backtesting_engine:
38-
raise ValueError(f"Backtesting engine for controller type {controller_config.controller_type} not found.")
39-
backtesting_results = await backtesting_engine.run_backtesting(
40-
controller_config=controller_config, trade_cost=backtesting_config.trade_cost,
41-
start=int(backtesting_config.start_time), end=int(backtesting_config.end_time),
42-
backtesting_resolution=backtesting_config.backtesting_resolution)
43-
44-
processed_data = backtesting_results["processed_data"]["features"].fillna(0).to_dict()
45-
executors_info = [ExecutorInfo(**e.to_dict()) for e in backtesting_results["executors"]]
46-
results = backtesting_results["results"]
47-
results["sharpe_ratio"] = results["sharpe_ratio"] if results["sharpe_ratio"] is not None else 0
48-
49-
return BacktestResponse(
50-
executors=executors_info,
51-
processed_data=ProcessedData(features=processed_data),
52-
results=BacktestResults(**results)
53-
)
54+
raise BacktestConfigError(
55+
f"Backtesting engine for controller type {controller_config.controller_type} not found. "
56+
f"Available types: {list(BACKTESTING_ENGINES.keys())}"
57+
)
58+
59+
# Validate time range
60+
if backtesting_config.end_time <= backtesting_config.start_time:
61+
raise BacktestConfigError(
62+
f"Invalid time range: end_time ({backtesting_config.end_time}) must be greater than "
63+
f"start_time ({backtesting_config.start_time})"
64+
)
65+
66+
try:
67+
# Run backtesting
68+
backtesting_results = await backtesting_engine.run_backtesting(
69+
controller_config=controller_config,
70+
trade_cost=backtesting_config.trade_cost,
71+
start=int(backtesting_config.start_time),
72+
end=int(backtesting_config.end_time),
73+
backtesting_resolution=backtesting_config.backtesting_resolution
74+
)
75+
except Exception as e:
76+
raise BacktestEngineError(f"Error during backtesting execution: {str(e)}")
77+
78+
try:
79+
# Process results
80+
processed_data = backtesting_results["processed_data"]["features"].fillna(0).to_dict()
81+
executors_info = [ExecutorInfo(**e.to_dict()) for e in backtesting_results["executors"]]
82+
results = backtesting_results["results"]
83+
results["sharpe_ratio"] = results["sharpe_ratio"] if results["sharpe_ratio"] is not None else 0
84+
85+
return BacktestResponse(
86+
executors=executors_info,
87+
processed_data=ProcessedData(features=processed_data),
88+
results=BacktestResults(**results)
89+
)
90+
except Exception as e:
91+
raise BacktestError(f"Error processing backtesting results: {str(e)}")
92+
93+
except BacktestConfigError as e:
94+
raise HTTPException(status_code=400, detail=str(e))
95+
except BacktestEngineError as e:
96+
raise HTTPException(status_code=500, detail=str(e))
97+
except BacktestError as e:
98+
raise HTTPException(status_code=500, detail=str(e))
5499
except Exception as e:
55-
raise HTTPException(status_code=400, detail=str(e))
100+
raise HTTPException(
101+
status_code=500,
102+
detail=f"Unexpected error during backtesting: {str(e)}"
103+
)

routers/bots.py

Lines changed: 163 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -5,103 +5,191 @@
55
from fastapi_walletauth import JWTWalletAuthDep
66
from utils.models import HummingbotInstanceConfig, StartStrategyRequest, InstanceResponse
77
from hummingbot.core.gateway.gateway_http_client import GatewayHttpClient
8+
from routers.strategies_models import (
9+
StrategyError,
10+
StrategyRegistry,
11+
StrategyNotFoundError,
12+
StrategyValidationError
13+
)
814

915
router = APIRouter(tags=["Bot Management"])
1016
accounts_service = AccountsService()
1117
docker_manager = DockerManager()
1218
gateway_client = GatewayHttpClient.get_instance()
1319

20+
class BotError(StrategyError):
21+
"""Base class for bot-related errors"""
22+
23+
class BotNotFoundError(BotError):
24+
"""Raised when a bot cannot be found"""
25+
26+
class BotPermissionError(BotError):
27+
"""Raised when there's a permission error with bot operations"""
28+
29+
class BotConfigError(BotError):
30+
"""Raised when there's an error in bot configuration"""
1431

1532
class CreateBotRequest(BaseModel):
1633
strategy_name: str
1734
strategy_parameters: dict
1835
market: str
1936

20-
2137
@router.post("/bots", response_model=InstanceResponse)
2238
async def create_bot(request: CreateBotRequest, wallet_auth: JWTWalletAuthDep):
23-
bot_account = f"robotter_{wallet_auth.address}_{request.market}_{request.strategy_name}"
24-
accounts_service.add_account(bot_account)
25-
wallet_address = await accounts_service.generate_bot_wallet(bot_account)
26-
27-
# Save strategy configuration and market
28-
bot_config = BotConfig(
29-
strategy_name=request.strategy_name,
30-
parameters=request.strategy_parameters,
31-
market=request.market,
32-
wallet_address=wallet_auth.address,
33-
)
34-
accounts_service.save_bot_config(bot_account, bot_config)
35-
# Create Hummingbot instance
36-
instance_config = HummingbotInstanceConfig(
37-
instance_name=bot_account, credentials_profile=bot_account, image="mlguys/hummingbot:mango", market=request.market
38-
)
39-
result = docker_manager.create_hummingbot_instance(instance_config)
40-
41-
if not result["success"]:
42-
raise HTTPException(status_code=500, detail=result["message"])
43-
44-
return InstanceResponse(instance_id=bot_account, wallet_address=wallet_address, market=request.market)
45-
39+
try:
40+
# Validate strategy exists and parameters
41+
try:
42+
strategy = StrategyRegistry.get_strategy(request.strategy_name)
43+
except StrategyNotFoundError as e:
44+
raise BotConfigError(f"Invalid strategy: {str(e)}")
45+
46+
try:
47+
validate_strategy_parameters(strategy, request.strategy_parameters)
48+
except StrategyValidationError as e:
49+
raise BotConfigError(f"Invalid strategy parameters: {str(e)}")
50+
51+
# Create bot account
52+
bot_account = f"robotter_{wallet_auth.address}_{request.market}_{request.strategy_name}"
53+
try:
54+
accounts_service.add_account(bot_account)
55+
wallet_address = await accounts_service.generate_bot_wallet(bot_account)
56+
except Exception as e:
57+
raise BotError(f"Error creating bot account: {str(e)}")
58+
59+
# Save strategy configuration and market
60+
try:
61+
bot_config = BotConfig(
62+
strategy_name=request.strategy_name,
63+
parameters=request.strategy_parameters,
64+
market=request.market,
65+
wallet_address=wallet_auth.address,
66+
)
67+
accounts_service.save_bot_config(bot_account, bot_config)
68+
except Exception as e:
69+
raise BotConfigError(f"Error saving bot configuration: {str(e)}")
70+
71+
# Create Hummingbot instance
72+
try:
73+
instance_config = HummingbotInstanceConfig(
74+
instance_name=bot_account,
75+
credentials_profile=bot_account,
76+
image="mlguys/hummingbot:mango",
77+
market=request.market
78+
)
79+
result = docker_manager.create_hummingbot_instance(instance_config)
80+
81+
if not result["success"]:
82+
raise BotError(result["message"])
83+
except Exception as e:
84+
raise BotError(f"Error creating Hummingbot instance: {str(e)}")
85+
86+
return InstanceResponse(
87+
instance_id=bot_account,
88+
wallet_address=wallet_address,
89+
market=request.market
90+
)
91+
92+
except BotConfigError as e:
93+
raise HTTPException(status_code=400, detail=str(e))
94+
except BotPermissionError as e:
95+
raise HTTPException(status_code=403, detail=str(e))
96+
except BotError as e:
97+
raise HTTPException(status_code=500, detail=str(e))
98+
except Exception as e:
99+
raise HTTPException(
100+
status_code=500,
101+
detail=f"Unexpected error creating bot: {str(e)}"
102+
)
46103

47104
@router.get("/bots/{bot_id}/wallet")
48105
async def get_bot_wallet(bot_id: str, wallet_auth: JWTWalletAuthDep):
49106
try:
50107
# Check if the bot belongs to the authenticated user
51108
if not bot_id.endswith(wallet_auth.address):
52-
raise HTTPException(status_code=403, detail="You don't have permission to access this bot")
109+
raise BotPermissionError("You don't have permission to access this bot")
53110

54-
wallet_address = accounts_service.get_bot_wallet_address(bot_id)
55-
return {"wallet_address": wallet_address}
56-
except Exception as e:
57-
raise HTTPException(status_code=404, detail=str(e))
111+
try:
112+
wallet_address = accounts_service.get_bot_wallet_address(bot_id)
113+
return {"wallet_address": wallet_address}
114+
except Exception as e:
115+
raise BotNotFoundError(f"Bot wallet not found: {str(e)}")
58116

117+
except BotPermissionError as e:
118+
raise HTTPException(status_code=403, detail=str(e))
119+
except BotNotFoundError as e:
120+
raise HTTPException(status_code=404, detail=str(e))
121+
except Exception as e:
122+
raise HTTPException(
123+
status_code=500,
124+
detail=f"Unexpected error getting bot wallet: {str(e)}"
125+
)
59126

60127
@router.post("/bots/{bot_id}/start")
61128
async def start_bot(bot_id: str, start_request: StartStrategyRequest, wallet_auth: JWTWalletAuthDep):
62-
# Check if the bot belongs to the authenticated user
63-
bot_config = accounts_service.get_bot_config(bot_id)
64-
if not bot_config or bot_config.wallet_address != wallet_auth.address:
65-
raise HTTPException(status_code=403, detail="You don't have permission to start this bot")
66-
67-
# Check if Mango account exists and is associated with the bot's wallet
68-
bot_wallet = accounts_service.get_bot_wallet_address(bot_id)
69-
70-
# We should pass the wallet address
71-
mango_account_info = await gateway_client.get_mango_account(
72-
"solana", "mainnet", "mango_perpetual_solana_mainnet-beta", bot_wallet
73-
)
74-
75-
if not mango_account_info or mango_account_info.get("owner") != bot_wallet:
76-
raise HTTPException(status_code=400, detail="Invalid Mango account or not associated with the bot's wallet")
77-
78-
# Start the bot
79-
strategy_config = accounts_service.get_strategy_config(bot_id)
80-
start_config = {**strategy_config, **start_request.parameters}
81-
82-
response = docker_manager.start_bot(bot_id, start_config)
83-
if not response["success"]:
84-
raise HTTPException(status_code=500, detail="Failed to start the bot")
85-
86-
return {"status": "success", "message": "Bot started successfully"}
87-
88-
89-
@router.post("/bots/{bot_id}/stop")
90-
async def stop_bot(bot_id: str, wallet_auth: JWTWalletAuthDep):
91-
# Check if the bot belongs to the authenticated user
92-
if not bot_id.endswith(wallet_auth.address):
93-
raise HTTPException(status_code=403, detail="You don't have permission to stop this bot")
94-
95-
# Stop the bot and cancel all orders
96-
response = docker_manager.stop_bot(bot_id)
97-
if not response["success"]:
98-
raise HTTPException(status_code=500, detail="Failed to stop the bot")
99-
100-
# Cancel all orders through the gateway
101-
bot_wallet = accounts_service.get_bot_wallet_address(bot_id)
102-
cancel_orders_response = await gateway_client.clob_perp_get_orders("solana", "mainnet", "mango_perpetual_solana_mainnet-beta")
103-
104-
if not cancel_orders_response["success"]:
105-
raise HTTPException(status_code=500, detail="Failed to cancel all orders")
106-
107-
return {"status": "success", "message": "Bot stopped and all orders cancelled successfully"}
129+
try:
130+
# Check if the bot belongs to the authenticated user
131+
bot_config = accounts_service.get_bot_config(bot_id)
132+
if not bot_config or bot_config.wallet_address != wallet_auth.address:
133+
raise BotPermissionError("You don't have permission to start this bot")
134+
135+
# Check if Mango account exists and is associated with the bot's wallet
136+
try:
137+
bot_wallet = accounts_service.get_bot_wallet_address(bot_id)
138+
mango_account_info = await gateway_client.get_mango_account(
139+
"solana", "mainnet", "mango_perpetual_solana_mainnet-beta", bot_wallet
140+
)
141+
142+
if not mango_account_info or mango_account_info.get("owner") != bot_wallet:
143+
raise BotConfigError("Invalid Mango account or not associated with the bot's wallet")
144+
except Exception as e:
145+
raise BotConfigError(f"Error validating Mango account: {str(e)}")
146+
147+
# Start the bot
148+
try:
149+
strategy_config = accounts_service.get_strategy_config(bot_id)
150+
start_config = {**strategy_config, **start_request.parameters}
151+
152+
response = docker_manager.start_bot(bot_id, start_config)
153+
if not response["success"]:
154+
raise BotError("Failed to start the bot")
155+
156+
return {"status": "success", "message": "Bot started successfully"}
157+
except Exception as e:
158+
raise BotError(f"Error starting bot: {str(e)}")
159+
160+
except BotPermissionError as e:
161+
raise HTTPException(status_code=403, detail=str(e))
162+
except BotConfigError as e:
163+
raise HTTPException(status_code=400, detail=str(e))
164+
except BotError as e:
165+
raise HTTPException(status_code=500, detail=str(e))
166+
except Exception as e:
167+
raise HTTPException(
168+
status_code=500,
169+
detail=f"Unexpected error starting bot: {str(e)}"
170+
)
171+
172+
def validate_strategy_parameters(strategy, parameters: dict) -> None:
173+
"""Validate strategy parameters against their constraints"""
174+
for param_name, value in parameters.items():
175+
if param_name not in strategy.parameters:
176+
raise StrategyValidationError(f"Unknown parameter: {param_name}")
177+
178+
param = strategy.parameters[param_name]
179+
constraints = param.constraints
180+
181+
if constraints:
182+
if constraints.min_value is not None and value < constraints.min_value:
183+
raise StrategyValidationError(
184+
f"Parameter {param_name} value {value} is below minimum {constraints.min_value}"
185+
)
186+
187+
if constraints.max_value is not None and value > constraints.max_value:
188+
raise StrategyValidationError(
189+
f"Parameter {param_name} value {value} is above maximum {constraints.max_value}"
190+
)
191+
192+
if constraints.valid_values is not None and value not in constraints.valid_values:
193+
raise StrategyValidationError(
194+
f"Parameter {param_name} value {value} is not one of the valid values: {constraints.valid_values}"
195+
)

0 commit comments

Comments
 (0)