Skip to content
4 changes: 2 additions & 2 deletions database/repositories/order_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,9 @@ async def get_orders(self, account_name: Optional[str] = None,
async def get_active_orders(self, account_name: Optional[str] = None,
connector_name: Optional[str] = None,
trading_pair: Optional[str] = None) -> List[Order]:
"""Get active orders (SUBMITTED, OPEN, PARTIALLY_FILLED)."""
"""Get active orders (SUBMITTED, OPEN, PARTIALLY_FILLED, PENDING_CANCEL)."""
query = select(Order).where(
Order.status.in_(["SUBMITTED", "OPEN", "PARTIALLY_FILLED"])
Order.status.in_(["SUBMITTED", "OPEN", "PARTIALLY_FILLED", "PENDING_CANCEL"])
)

# Apply filters
Expand Down
92 changes: 89 additions & 3 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@ def patched_save_to_yml(yml_path, cm):
from hummingbot.client.config import config_helpers
config_helpers.save_to_yml = patched_save_to_yml

from hummingbot.core.rate_oracle.rate_oracle import RateOracle
from hummingbot.core.rate_oracle.rate_oracle import RateOracle, RATE_ORACLE_SOURCES
from hummingbot.core.gateway.gateway_http_client import GatewayHttpClient
from hummingbot.client.config.client_config_map import GatewayConfigMap

from fastapi import Depends, FastAPI, HTTPException, status
from fastapi import Depends, FastAPI, HTTPException, Request, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from fastapi.middleware.cors import CORSMiddleware
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from hummingbot.data_feed.market_data_provider import MarketDataProvider
from hummingbot.client.config.config_crypt import ETHKeyFileSecretManger

Expand All @@ -40,6 +42,7 @@ def patched_save_to_yml(yml_path, cm):
from services.docker_service import DockerService
from services.gateway_service import GatewayService
from services.market_data_feed_manager import MarketDataFeedManager
# from services.executor_service import ExecutorService
from utils.bot_archiver import BotArchiver
from routers import (
accounts,
Expand All @@ -49,11 +52,13 @@ def patched_save_to_yml(yml_path, cm):
connectors,
controllers,
docker,
# executors,
gateway,
gateway_swap,
gateway_clmm,
market_data,
portfolio,
rate_oracle,
scripts,
trading
)
Expand Down Expand Up @@ -107,10 +112,47 @@ async def lifespan(app: FastAPI):
# Initialize MarketDataProvider with empty connectors (will use non-trading connectors)
market_data_provider = MarketDataProvider(connectors={})

# Read rate oracle configuration from conf_client.yml
from utils.file_system import FileSystemUtil
fs_util = FileSystemUtil()

try:
conf_client_path = "credentials/master_account/conf_client.yml"
config_data = fs_util.read_yaml_file(conf_client_path)

# Get rate_oracle_source configuration
rate_oracle_source_data = config_data.get("rate_oracle_source", {})
source_name = rate_oracle_source_data.get("name", "binance")

# Get global_token configuration
global_token_data = config_data.get("global_token", {})
quote_token = global_token_data.get("global_token_name", "USDT")

# Create rate source instance
if source_name in RATE_ORACLE_SOURCES:
rate_source = RATE_ORACLE_SOURCES[source_name]()
logging.info(f"Configured RateOracle with source: {source_name}, quote_token: {quote_token}")
else:
logging.warning(f"Unknown rate oracle source '{source_name}', defaulting to binance")
rate_source = RATE_ORACLE_SOURCES["binance"]()
source_name = "binance"

# Initialize RateOracle with configured source and quote token
rate_oracle_instance = RateOracle.get_instance()
rate_oracle_instance.source = rate_source
rate_oracle_instance.quote_token = quote_token

except FileNotFoundError:
logging.warning("conf_client.yml not found, using default RateOracle configuration (binance, USDT)")
rate_oracle_instance = RateOracle.get_instance()
except Exception as e:
logging.warning(f"Error reading conf_client.yml: {e}, using default RateOracle configuration")
rate_oracle_instance = RateOracle.get_instance()

# Initialize MarketDataFeedManager with lifecycle management
market_data_feed_manager = MarketDataFeedManager(
market_data_provider=market_data_provider,
rate_oracle=RateOracle.get_instance(),
rate_oracle=rate_oracle_instance,
cleanup_interval=settings.market_data.cleanup_interval,
feed_timeout=settings.market_data.feed_timeout
)
Expand Down Expand Up @@ -139,25 +181,42 @@ async def lifespan(app: FastAPI):
# Initialize database
await accounts_service.ensure_db_initialized()

# # Initialize ExecutorService for running executors directly via API
# executor_service = ExecutorService(
# connector_manager=accounts_service.connector_manager,
# market_data_feed_manager=market_data_feed_manager,
# db_manager=accounts_service.db_manager,
# default_account="master_account",
# update_interval=1.0,
# max_retries=10
# )
# # Store reference in accounts_service for router access
# accounts_service._executor_service = executor_service

# Store services in app state
app.state.bots_orchestrator = bots_orchestrator
app.state.accounts_service = accounts_service
app.state.docker_service = docker_service
app.state.gateway_service = gateway_service
app.state.bot_archiver = bot_archiver
app.state.market_data_feed_manager = market_data_feed_manager
# app.state.executor_service = executor_service

# Start services
bots_orchestrator.start()
accounts_service.start()
market_data_feed_manager.start()
# executor_service.start()

yield

# Shutdown services
bots_orchestrator.stop()
await accounts_service.stop()

# Stop executor service
# await executor_service.stop()

# Stop market data feed manager (which will stop all feeds)
market_data_feed_manager.stop()

Expand Down Expand Up @@ -185,6 +244,31 @@ async def lifespan(app: FastAPI):
allow_headers=["*"],
)


@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
"""
Custom handler for validation errors to log detailed error messages.
"""
# Build a readable error message from validation errors
error_messages = []
for error in exc.errors():
loc = " -> ".join(str(l) for l in error.get("loc", []))
msg = error.get("msg", "Validation error")
error_messages.append(f"{loc}: {msg}")

# Log the validation error with details
logging.warning(
f"Validation error on {request.method} {request.url.path}: {'; '.join(error_messages)}"
)

# Return standard FastAPI validation error response
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content={"detail": exc.errors()},
)


logfire.configure(send_to_logfire="if-token-present", environment=settings.app.logfire_environment, service_name="hummingbot-api")
logfire.instrument_fastapi(app)

Expand Down Expand Up @@ -223,8 +307,10 @@ def auth_user(
app.include_router(controllers.router, dependencies=[Depends(auth_user)])
app.include_router(scripts.router, dependencies=[Depends(auth_user)])
app.include_router(market_data.router, dependencies=[Depends(auth_user)])
app.include_router(rate_oracle.router, dependencies=[Depends(auth_user)])
app.include_router(backtesting.router, dependencies=[Depends(auth_user)])
app.include_router(archived_bots.router, dependencies=[Depends(auth_user)])
# app.include_router(executors.router, dependencies=[Depends(auth_user)])

@app.get("/")
async def root():
Expand Down
25 changes: 25 additions & 0 deletions models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,20 @@
ExecutorsResponse,
)

# Rate Oracle models
from .rate_oracle import (
RateOracleSourceEnum,
GlobalTokenConfig,
RateOracleSourceConfig,
RateOracleConfig,
RateOracleConfigResponse,
RateOracleConfigUpdateRequest,
RateOracleConfigUpdateResponse,
RateRequest,
RateResponse,
SingleRateResponse,
)

__all__ = [
# Bot orchestration models
"BotAction",
Expand Down Expand Up @@ -338,4 +352,15 @@
"TradeHistoryResponse",
"OrderHistoryResponse",
"ExecutorsResponse",
# Rate Oracle models
"RateOracleSourceEnum",
"GlobalTokenConfig",
"RateOracleSourceConfig",
"RateOracleConfig",
"RateOracleConfigResponse",
"RateOracleConfigUpdateRequest",
"RateOracleConfigUpdateResponse",
"RateRequest",
"RateResponse",
"SingleRateResponse",
]
114 changes: 114 additions & 0 deletions models/rate_oracle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
"""
Pydantic models for the rate oracle router.

These models define the request/response schemas for rate oracle configuration endpoints.
"""

from typing import Optional, List, Dict
from enum import Enum
from pydantic import BaseModel, Field


class RateOracleSourceEnum(str, Enum):
"""Available rate oracle sources."""
BINANCE = "binance"
BINANCE_US = "binance_us"
COIN_GECKO = "coin_gecko"
COIN_CAP = "coin_cap"
KUCOIN = "kucoin"
ASCEND_EX = "ascend_ex"
GATE_IO = "gate_io"
COINBASE_ADVANCED_TRADE = "coinbase_advanced_trade"
CUBE = "cube"
DEXALOT = "dexalot"
HYPERLIQUID = "hyperliquid"
DERIVE = "derive"
TEGRO = "tegro"


class GlobalTokenConfig(BaseModel):
"""Global token configuration for displaying values."""
global_token_name: str = Field(
default="USDT",
description="The token to use as global quote (e.g., USDT, USD, BTC)"
)
global_token_symbol: str = Field(
default="$",
description="Symbol to display for the global token"
)


class RateOracleSourceConfig(BaseModel):
"""Rate oracle source configuration."""
name: RateOracleSourceEnum = Field(
default=RateOracleSourceEnum.BINANCE,
description="The rate oracle source to use for price data"
)


class RateOracleConfig(BaseModel):
"""Complete rate oracle configuration."""
rate_oracle_source: RateOracleSourceConfig = Field(
default_factory=RateOracleSourceConfig,
description="Rate oracle source configuration"
)
global_token: GlobalTokenConfig = Field(
default_factory=GlobalTokenConfig,
description="Global token configuration"
)


class RateOracleConfigResponse(BaseModel):
"""Response for rate oracle configuration GET endpoint."""
rate_oracle_source: RateOracleSourceConfig = Field(
description="Current rate oracle source configuration"
)
global_token: GlobalTokenConfig = Field(
description="Current global token configuration"
)
available_sources: List[str] = Field(
description="List of available rate oracle sources"
)


class RateOracleConfigUpdateRequest(BaseModel):
"""Request model for updating rate oracle configuration."""
rate_oracle_source: Optional[RateOracleSourceConfig] = Field(
default=None,
description="New rate oracle source configuration (optional)"
)
global_token: Optional[GlobalTokenConfig] = Field(
default=None,
description="New global token configuration (optional)"
)


class RateOracleConfigUpdateResponse(BaseModel):
"""Response for rate oracle configuration update."""
success: bool = Field(description="Whether the update was successful")
message: str = Field(description="Status message")
config: RateOracleConfig = Field(description="Updated configuration")


class RateRequest(BaseModel):
"""Request for getting rates."""
trading_pairs: List[str] = Field(
description="List of trading pairs to get rates for (e.g., ['BTC-USDT', 'ETH-USDT'])"
)


class RateResponse(BaseModel):
"""Response containing rates for trading pairs."""
source: str = Field(description="Rate oracle source used")
quote_token: str = Field(description="Quote token used")
rates: Dict[str, Optional[float]] = Field(
description="Mapping of trading pairs to their rates (None if rate not found)"
)


class SingleRateResponse(BaseModel):
"""Response for a single trading pair rate."""
trading_pair: str = Field(description="The trading pair")
rate: Optional[float] = Field(description="The rate (None if not found)")
source: str = Field(description="Rate oracle source used")
quote_token: str = Field(description="Quote token used")
21 changes: 12 additions & 9 deletions routers/connectors.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
from typing import List, Optional, Dict
from typing import Dict, List, Optional

from fastapi import APIRouter, Depends, Request, HTTPException, Query
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from hummingbot.client.settings import AllConnectorSettings

from services.accounts_service import AccountsService
from services.market_data_feed_manager import MarketDataFeedManager
from deps import get_accounts_service
from models import AddTokenRequest
from services.accounts_service import AccountsService
from services.market_data_feed_manager import MarketDataFeedManager

router = APIRouter(tags=["Connectors"], prefix="/connectors")

Expand All @@ -22,16 +22,19 @@ async def available_connectors():
return list(AllConnectorSettings.get_connector_settings().keys())


@router.get("/{connector_name}/config-map", response_model=List[str])
@router.get("/{connector_name}/config-map", response_model=Dict[str, dict])
async def get_connector_config_map(connector_name: str, accounts_service: AccountsService = Depends(get_accounts_service)):
"""
Get configuration fields required for a specific connector.
Get configuration fields required for a specific connector with type information.

Args:
connector_name: Name of the connector to get config map for

Returns:
List of configuration field names required for the connector
Dictionary mapping field names to their type information.
Each field contains:
- type: The expected data type (e.g., "str", "SecretStr", "int")
- required: Whether the field is required
"""
return accounts_service.get_connector_config_map(connector_name)

Expand Down
Loading