Skip to content
Merged
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
5 changes: 3 additions & 2 deletions bots/controllers/generic/pmm_mister.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class PMMisterConfig(ControllerConfigBase):
leverage: int = Field(default=20, json_schema_extra={"is_updatable": True})
position_mode: PositionMode = Field(default="HEDGE")
take_profit: Optional[Decimal] = Field(default=Decimal("0.0001"), gt=0, json_schema_extra={"is_updatable": True})
open_order_type: Optional[OrderType] = Field(default="LIMIT_MAKER", json_schema_extra={"is_updatable": True})
take_profit_order_type: Optional[OrderType] = Field(default="LIMIT_MAKER", json_schema_extra={"is_updatable": True})
max_active_executors_by_level: Optional[int] = Field(default=4, json_schema_extra={"is_updatable": True})
tick_mode: bool = Field(default=False, json_schema_extra={"is_updatable": True})
Expand All @@ -58,7 +59,7 @@ def validate_target(cls, v):
return Decimal(v)
return v

@field_validator('take_profit_order_type', mode="before")
@field_validator('open_order_type', 'take_profit_order_type', mode="before")
@classmethod
def validate_order_type(cls, v) -> OrderType:
if isinstance(v, OrderType):
Expand Down Expand Up @@ -114,7 +115,7 @@ def triple_barrier_config(self) -> TripleBarrierConfig:
return TripleBarrierConfig(
take_profit=self.take_profit,
trailing_stop=None,
open_order_type=OrderType.LIMIT_MAKER,
open_order_type=self.open_order_type,
take_profit_order_type=self.take_profit_order_type,
stop_loss_order_type=OrderType.MARKET,
time_limit_order_type=OrderType.MARKET
Expand Down
24 changes: 17 additions & 7 deletions bots/scripts/v2_with_controllers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

from hummingbot.client.hummingbot_application import HummingbotApplication
from hummingbot.connector.connector_base import ConnectorBase

from hummingbot.core.event.events import MarketOrderFailureEvent
from hummingbot.data_feed.candles_feed.data_types import CandlesConfig
from hummingbot.strategy.strategy_v2_base import StrategyV2Base, StrategyV2ConfigBase
Expand Down Expand Up @@ -89,10 +88,20 @@ def check_max_global_drawdown(self):
self._is_stop_triggered = True
HummingbotApplication.main_application().stop()

def get_controller_report(self, controller_id: str) -> dict:
"""
Get the full report for a controller including performance and custom info.
"""
performance_report = self.controller_reports.get(controller_id, {}).get("performance")
return {
"performance": performance_report.dict() if performance_report else {},
"custom_info": self.controllers[controller_id].get_custom_info()
}

def send_performance_report(self):
if self.current_timestamp - self._last_performance_report_timestamp >= self.performance_report_interval and self._pub:
performance_reports = {controller_id: self.get_performance_report(controller_id).dict() for controller_id in self.controllers.keys()}
self._pub(performance_reports)
controller_reports = {controller_id: self.get_controller_report(controller_id) for controller_id in self.controllers.keys()}
self._pub(controller_reports)
self._last_performance_report_timestamp = self.current_timestamp

def check_manual_kill_switch(self):
Expand Down Expand Up @@ -142,17 +151,18 @@ def apply_initial_setting(self):
if self.is_perpetual(config_dict["connector_name"]):
if "position_mode" in config_dict:
connectors_position_mode[config_dict["connector_name"]] = config_dict["position_mode"]
if "leverage" in config_dict:
self.connectors[config_dict["connector_name"]].set_leverage(leverage=config_dict["leverage"],
trading_pair=config_dict["trading_pair"])
if "leverage" in config_dict and "trading_pair" in config_dict:
self.connectors[config_dict["connector_name"]].set_leverage(
leverage=config_dict["leverage"],
trading_pair=config_dict["trading_pair"])
for connector_name, position_mode in connectors_position_mode.items():
self.connectors[connector_name].set_position_mode(position_mode)

def did_fail_order(self, order_failed_event: MarketOrderFailureEvent):
"""
Handle order failure events by logging the error and stopping the strategy if necessary.
"""
if "position side" in order_failed_event.error_message.lower():
if order_failed_event.error_message and "position side" in order_failed_event.error_message.lower():
connectors_position_mode = {}
for controller_id, controller in self.controllers.items():
config_dict = controller.config.model_dump()
Expand Down
6 changes: 6 additions & 0 deletions models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@
from .gateway import (
GatewayConfig,
GatewayStatus,
CreateWalletRequest,
ShowPrivateKeyRequest,
SendTransactionRequest,
GatewayWalletCredential,
GatewayWalletInfo,
GatewayBalanceRequest,
Expand Down Expand Up @@ -267,6 +270,9 @@
# Gateway models
"GatewayConfig",
"GatewayStatus",
"CreateWalletRequest",
"ShowPrivateKeyRequest",
"SendTransactionRequest",
"GatewayWalletCredential",
"GatewayWalletInfo",
"GatewayBalanceRequest",
Expand Down
22 changes: 22 additions & 0 deletions models/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,28 @@ class GatewayStatus(BaseModel):
# Wallet Management Models
# ============================================

class CreateWalletRequest(BaseModel):
"""Request to create a new wallet in Gateway"""
chain: str = Field(description="Blockchain chain (e.g., 'solana', 'ethereum')")
set_default: bool = Field(default=True, description="Set as default wallet for this chain")


class ShowPrivateKeyRequest(BaseModel):
"""Request to show private key for a wallet"""
chain: str = Field(description="Blockchain chain (e.g., 'solana', 'ethereum')")
address: str = Field(description="Wallet address")
passphrase: str = Field(description="Gateway passphrase for decryption")


class SendTransactionRequest(BaseModel):
"""Request to send a native token transaction"""
chain: str = Field(description="Blockchain chain (e.g., 'solana', 'ethereum')")
network: str = Field(description="Network (e.g., 'mainnet-beta', 'mainnet')")
address: str = Field(description="Sender wallet address")
to_address: str = Field(description="Recipient address")
amount: str = Field(description="Amount to send (in native token units)")


class GatewayWalletCredential(BaseModel):
"""Credentials for connecting a Gateway wallet"""
chain: str = Field(description="Blockchain chain (e.g., 'solana', 'ethereum')")
Expand Down
13 changes: 9 additions & 4 deletions routers/bot_orchestration.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,15 +163,20 @@ async def stop_bot(
Returns:
Dictionary with status and response from bot stop operation
"""
# Capture final status BEFORE stopping (performance data is cleared on stop)
final_status = None
try:
final_status = bots_manager.get_bot_status(action.bot_name)
logger.info(f"Captured final status for {action.bot_name} before stopping")
except Exception as e:
logger.warning(f"Failed to capture final status for {action.bot_name}: {e}")

response = await bots_manager.stop_bot(action.bot_name, skip_order_cancellation=action.skip_order_cancellation,
async_backend=action.async_backend)

# Update bot run status to STOPPED if stop was successful
if response.get("success"):
try:
# Try to get bot status for final status data
final_status = bots_manager.get_bot_status(action.bot_name)

async with db_manager.get_session_context() as session:
bot_run_repo = BotRunRepository(session)
await bot_run_repo.update_bot_run_stopped(
Expand Down
153 changes: 152 additions & 1 deletion routers/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@
from typing import Optional, Dict, List
import re

from models import GatewayConfig, GatewayStatus, AddPoolRequest, AddTokenRequest
from models import (
GatewayConfig,
GatewayStatus,
AddPoolRequest,
AddTokenRequest,
CreateWalletRequest,
ShowPrivateKeyRequest,
SendTransactionRequest,
)
from services.gateway_service import GatewayService
from services.accounts_service import AccountsService
from deps import get_gateway_service, get_accounts_service
Expand Down Expand Up @@ -663,3 +671,146 @@ async def delete_network_token(
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error deleting token: {str(e)}")


# ============================================
# Wallet Management
# ============================================

@router.post("/wallets/create")
async def create_wallet(
request: CreateWalletRequest,
accounts_service: AccountsService = Depends(get_accounts_service)
) -> Dict:
"""
Create a new wallet in Gateway.

Args:
request: Contains chain and set_default flag

Returns:
Dict with address and chain of the created wallet.

Example: POST /gateway/wallets/create
{
"chain": "solana",
"set_default": true
}
"""
try:
if not await accounts_service.gateway_client.ping():
raise HTTPException(status_code=503, detail="Gateway service is not available")

result = await accounts_service.gateway_client.create_wallet(
chain=request.chain,
set_default=request.set_default
)

if result is None:
raise HTTPException(status_code=502, detail="Failed to create wallet: Gateway returned no response")

if "error" in result:
raise HTTPException(status_code=400, detail=f"Failed to create wallet: {result.get('error')}")

return result

except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error creating wallet: {str(e)}")


@router.post("/wallets/show-private-key")
async def show_private_key(
request: ShowPrivateKeyRequest,
accounts_service: AccountsService = Depends(get_accounts_service)
) -> Dict:
"""
Show private key for a wallet.

WARNING: This endpoint exposes sensitive information. Use with caution.

Args:
request: Contains chain, address, and passphrase

Returns:
Dict with privateKey field.

Example: POST /gateway/wallets/show-private-key
{
"chain": "solana",
"address": "<wallet-address>",
"passphrase": "<gateway-passphrase>"
}
"""
try:
if not await accounts_service.gateway_client.ping():
raise HTTPException(status_code=503, detail="Gateway service is not available")

result = await accounts_service.gateway_client.show_private_key(
chain=request.chain,
address=request.address,
passphrase=request.passphrase
)

if result is None:
raise HTTPException(status_code=502, detail="Failed to retrieve private key: Gateway returned no response")

if "error" in result:
raise HTTPException(status_code=400, detail=f"Failed to retrieve private key: {result.get('error')}")

return result

except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error retrieving private key: {str(e)}")


@router.post("/wallets/send")
async def send_transaction(
request: SendTransactionRequest,
accounts_service: AccountsService = Depends(get_accounts_service)
) -> Dict:
"""
Send a native token transaction.

Args:
request: Contains chain, network, sender address, recipient address, and amount

Returns:
Dict with transaction signature/hash.

Example: POST /gateway/wallets/send
{
"chain": "solana",
"network": "mainnet-beta",
"address": "<sender-address>",
"to_address": "<recipient-address>",
"amount": "0.001"
}
"""
try:
if not await accounts_service.gateway_client.ping():
raise HTTPException(status_code=503, detail="Gateway service is not available")

result = await accounts_service.gateway_client.send_transaction(
chain=request.chain,
network=request.network,
address=request.address,
to_address=request.to_address,
amount=request.amount
)

if result is None:
raise HTTPException(status_code=502, detail="Failed to send transaction: Gateway returned no response")

if "error" in result:
raise HTTPException(status_code=400, detail=f"Failed to send transaction: {result.get('error')}")

return result

except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error sending transaction: {str(e)}")
Loading