Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 20 additions & 5 deletions bots/controllers/directional_trading/bollinger_v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
DirectionalTradingControllerConfigBase,
)
from pydantic import Field, validator
import pandas as pd


class BollingerV1ControllerConfig(DirectionalTradingControllerConfigBase):
Expand Down Expand Up @@ -68,23 +69,37 @@ def __init__(self, config: BollingerV1ControllerConfig, *args, **kwargs):
super().__init__(config, *args, **kwargs)

async def update_processed_data(self):
print("Starting update_processed_data in BollingerV1Controller")
df = self.market_data_provider.get_candles_df(connector_name=self.config.candles_connector,
trading_pair=self.config.candles_trading_pair,
interval=self.config.interval,
max_records=self.max_records)
# Add indicators
df.ta.bbands(length=self.config.bb_length, std=self.config.bb_std, append=True)
bbp = df[f"BBP_{self.config.bb_length}_{self.config.bb_std}"]
print(f"Got candles DataFrame with shape: {df.shape}")
print(f"DataFrame columns: {df.columns.tolist()}")

# Add indicators using pandas_ta
print(f"Calculating Bollinger Bands with length={self.config.bb_length}, std={self.config.bb_std}")
bbands = df.ta.bbands(length=self.config.bb_length, std=self.config.bb_std)
print(f"Bollinger Bands columns: {bbands.columns.tolist() if bbands is not None else 'None'}")

df = pd.concat([df, bbands], axis=1)
print(f"Combined DataFrame columns: {df.columns.tolist()}")

# Generate signal
long_condition = bbp < self.config.bb_long_threshold
short_condition = bbp > self.config.bb_short_threshold
bbp_col = f"BBP_{self.config.bb_length}_{self.config.bb_std}"
print(f"Looking for BBP column: {bbp_col}")
print(f"BBP values: {df[bbp_col].head() if bbp_col in df.columns else 'Column not found'}")

long_condition = df[bbp_col] < self.config.bb_long_threshold
short_condition = df[bbp_col] > self.config.bb_short_threshold

# Generate signal
df["signal"] = 0
df.loc[long_condition, "signal"] = 1
df.loc[short_condition, "signal"] = -1
print(f"Generated signals: {df['signal'].value_counts()}")

# Update processed data
self.processed_data["signal"] = df["signal"].iloc[-1]
self.processed_data["features"] = df
print("Finished update_processed_data")
2 changes: 1 addition & 1 deletion environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ dependencies:
- cython
- pip
- pip:
- robotter-hummingbot==20241016
- hummingbot==20241227
- numpy==1.26.4
- git+https://github.com/felixfontein/docker-py
- python-dotenv
Expand Down
139 changes: 38 additions & 101 deletions routers/backtest.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,9 @@
from fastapi import APIRouter, HTTPException, status
from hummingbot.data_feed.candles_feed.candles_factory import CandlesFactory
from hummingbot.strategy_v2.backtesting.backtesting_engine_base import BacktestingEngineBase
from hummingbot.strategy_v2.backtesting.controllers_backtesting.directional_trading_backtesting import (
DirectionalTradingBacktesting,
)
from hummingbot.strategy_v2.backtesting.controllers_backtesting.market_making_backtesting import MarketMakingBacktesting

from config import CONTROLLERS_MODULE, CONTROLLERS_PATH
from routers.backtest_models import BacktestResponse, BacktestResults, BacktestingConfig, ExecutorInfo, ProcessedData
from routers.strategies_models import StrategyError
from routers.backtest_models import BacktestResponse, BacktestingConfig
from services.backtesting_service import BacktestingService, BacktestConfigError, BacktestEngineError, BacktestError

router = APIRouter(tags=["Market Backtesting"])
candles_factory = CandlesFactory()
directional_trading_backtesting = DirectionalTradingBacktesting()
market_making_backtesting = MarketMakingBacktesting()

BACKTESTING_ENGINES = {
"directional_trading": directional_trading_backtesting,
"market_making": market_making_backtesting
}

class BacktestError(StrategyError):
"""Base class for backtesting-related errors"""

class BacktestConfigError(BacktestError):
"""Raised when there's an error in the backtesting configuration"""

class BacktestEngineError(BacktestError):
"""Raised when there's an error during backtesting execution"""
backtesting_service = BacktestingService()

responses = {
400: {
Expand All @@ -46,6 +22,10 @@ class BacktestEngineError(BacktestError):
"invalid_engine": {
"summary": "Invalid Engine Type",
"value": {"detail": "Backtesting engine for controller type 'unknown' not found. Available types: ['directional_trading', 'market_making']"}
},
"invalid_strategy": {
"summary": "Invalid Strategy",
"value": {"detail": "Strategy 'unknown_strategy' not found. Use GET /strategies to see available strategies."}
}
}
}
Expand Down Expand Up @@ -118,20 +98,10 @@ class BacktestEngineError(BacktestError):
3. Simulates trading with the strategy
4. Analyzes performance and generates statistics

Supports two types of backtesting engines:
- Directional Trading: For trend-following and momentum strategies
- Market Making: For liquidity provision strategies

Returns comprehensive results including:
- Executor statistics (trades, win rate, P&L)
- Processed market data and indicators
- Overall performance metrics:
- Total trades executed
- Win rate
- Profit/Loss
- Sharpe ratio
- Maximum drawdown
- Return on Investment (ROI)
Required Configuration:
- strategy_id: ID of the strategy to backtest (get available strategies from GET /strategies)
- trading_pair: The trading pair to backtest on (e.g., "BTC-USDT")
- Other parameters specific to the chosen strategy

Time range requirements:
- start_time must be before end_time
Expand All @@ -141,64 +111,7 @@ class BacktestEngineError(BacktestError):
)
async def run_backtesting(backtesting_config: BacktestingConfig) -> BacktestResponse:
try:
# Load and validate controller config
try:
if isinstance(backtesting_config.config, str):
controller_config = BacktestingEngineBase.get_controller_config_instance_from_yml(
config_path=backtesting_config.config,
controllers_conf_dir_path=CONTROLLERS_PATH,
controllers_module=CONTROLLERS_MODULE
)
else:
controller_config = BacktestingEngineBase.get_controller_config_instance_from_dict(
config_data=backtesting_config.config,
controllers_module=CONTROLLERS_MODULE
)
except Exception as e:
raise BacktestConfigError(f"Invalid controller configuration: {str(e)}")

# Get and validate backtesting engine
backtesting_engine = BACKTESTING_ENGINES.get(controller_config.controller_type)
if not backtesting_engine:
raise BacktestConfigError(
f"Backtesting engine for controller type {controller_config.controller_type} not found. "
f"Available types: {list(BACKTESTING_ENGINES.keys())}"
)

# Validate time range
if backtesting_config.end_time <= backtesting_config.start_time:
raise BacktestConfigError(
f"Invalid time range: end_time ({backtesting_config.end_time}) must be greater than "
f"start_time ({backtesting_config.start_time})"
)

try:
# Run backtesting
backtesting_results = await backtesting_engine.run_backtesting(
controller_config=controller_config,
trade_cost=backtesting_config.trade_cost,
start=int(backtesting_config.start_time),
end=int(backtesting_config.end_time),
backtesting_resolution=backtesting_config.backtesting_resolution
)
except Exception as e:
raise BacktestEngineError(f"Error during backtesting execution: {str(e)}")

try:
# Process results
processed_data = backtesting_results["processed_data"]["features"].fillna(0).to_dict()
executors_info = [ExecutorInfo(**e.to_dict()) for e in backtesting_results["executors"]]
results = backtesting_results["results"]
results["sharpe_ratio"] = results["sharpe_ratio"] if results["sharpe_ratio"] is not None else 0

return BacktestResponse(
executors=executors_info,
processed_data=ProcessedData(features=processed_data),
results=BacktestResults(**results)
)
except Exception as e:
raise BacktestError(f"Error processing backtesting results: {str(e)}")

return await backtesting_service.run_backtesting(backtesting_config)
except BacktestConfigError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
except BacktestEngineError as e:
Expand All @@ -208,5 +121,29 @@ async def run_backtesting(backtesting_config: BacktestingConfig) -> BacktestResp
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Unexpected error during backtesting: {str(e)}"
)
detail=f"An unexpected error occurred during backtesting: {str(e)}"
)

@router.get(
"/backtest/engines",
response_model=dict,
summary="Get Available Backtesting Engines",
description="Returns a list of available backtesting engines and their types."
)
def get_available_engines():
return backtesting_service.get_available_engines()

@router.get(
"/backtest/engines/{engine_type}/config",
response_model=dict,
summary="Get Engine Configuration Schema",
description="Returns the configuration schema for a specific backtesting engine type."
)
def get_engine_config_schema(engine_type: str):
schema = backtesting_service.get_engine_config_schema(engine_type)
if not schema:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Engine type '{engine_type}' not found"
)
return schema
6 changes: 5 additions & 1 deletion routers/backtest_models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import List, Dict, Any, Union
from typing import List, Dict, Union
from pydantic import BaseModel, Field
from decimal import Decimal

Expand All @@ -10,6 +10,7 @@ class BacktestingConfig(BaseModel):
config: Union[Dict, str]

class ExecutorInfo(BaseModel):
id: str
level_id: str
timestamp: int
connector_name: str
Expand All @@ -19,6 +20,9 @@ class ExecutorInfo(BaseModel):
side: str
leverage: int
position_mode: str
trades: int
win_rate: float
profit_loss: Decimal

class ProcessedData(BaseModel):
features: Dict[str, List[Union[float, int, str]]]
Expand Down
82 changes: 79 additions & 3 deletions routers/strategies.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
from typing import Dict, Any
from typing import Dict, Any, Optional
from fastapi import APIRouter, HTTPException, status
from fastapi import FastAPI
from contextlib import asynccontextmanager
from fastapi.responses import JSONResponse

from services.libert_ai_service import LibertAIService
from routers.strategies_models import (
Expand Down Expand Up @@ -140,11 +139,17 @@ async def lifespan(app: FastAPI):
- Directional Trading: Strategies that follow market trends
- Market Making: Strategies that provide market liquidity
- Generic: Other types of strategies (e.g., arbitrage)

Optional query parameter:
- strategy_type: Filter strategies by type (directional_trading, market_making, generic)
"""
)
async def get_strategies() -> Dict[str, StrategyConfig]:
async def get_strategies(strategy_type: Optional[StrategyType] = None) -> Dict[str, StrategyConfig]:
"""Get all available strategies and their configurations."""
try:
if strategy_type:
strategies = StrategyRegistry.get_strategies_by_type(strategy_type)
return {s.id: s for s in strategies}
return StrategyRegistry.get_all_strategies()
except StrategyError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
Expand All @@ -154,6 +159,77 @@ async def get_strategies() -> Dict[str, StrategyConfig]:
detail=f"Internal server error while fetching strategies: {str(e)}"
)

@router.get(
"/strategies/{strategy_id}",
response_model=StrategyConfig,
responses={
200: {
"description": "Successfully retrieved strategy details",
"content": {
"application/json": {
"example": {
"mapping": {
"id": "bollinger_v1",
"config_class": "BollingerConfig",
"module_path": "bots.controllers.directional_trading.bollinger_v1",
"strategy_type": "directional_trading",
"display_name": "Bollinger Bands Strategy",
"description": "Buys when price is low and sells when price is high based on Bollinger Bands."
},
"parameters": {
"stop_loss": {
"name": "stop_loss",
"type": "Decimal",
"required": True,
"default": "0.03",
"display_name": "Stop Loss",
"description": "Stop loss percentage",
"group": "Risk Management",
"is_advanced": False,
"constraints": {
"min_value": 0,
"max_value": 0.1
}
}
}
}
}
}
},
**responses
},
summary="Get Strategy Details",
description="""
Returns detailed information about a specific strategy, including all its parameters and configuration options.

Use this endpoint to:
1. Get the complete list of parameters needed for the strategy
2. Understand parameter constraints (min/max values, valid options)
3. See default values and parameter descriptions
4. Determine which parameters are required vs optional

This information is essential for:
- Configuring a strategy for backtesting
- Understanding parameter relationships
- Setting up proper risk management
- Optimizing strategy performance
"""
)
async def get_strategy_details(strategy_id: str) -> StrategyConfig:
"""Get detailed information about a specific strategy"""
try:
return StrategyRegistry.get_strategy(strategy_id)
except StrategyNotFoundError as e:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Strategy not found: {str(e)}"
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error fetching strategy details: {str(e)}"
)

@router.post(
"/strategies/suggest-parameters",
response_model=ParameterSuggestionResponse,
Expand Down
Loading