Skip to content

Commit

Permalink
Merge pull request #37 from hummingbot/feat/add_observability_and_sec…
Browse files Browse the repository at this point in the history
…urity

Feat/add observability and security
  • Loading branch information
rapcmia authored Nov 1, 2024
2 parents c908a1d + 58de473 commit 9bcf5f2
Show file tree
Hide file tree
Showing 4 changed files with 258 additions and 8 deletions.
214 changes: 214 additions & 0 deletions bots/controllers/generic/grid_strike.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
from decimal import Decimal
from typing import Dict, List, Optional, Set

from hummingbot.client.config.config_data_types import ClientFieldData
from hummingbot.core.data_type.common import OrderType, PositionMode, PriceType, TradeType
from hummingbot.core.data_type.trade_fee import TokenAmount
from hummingbot.data_feed.candles_feed.data_types import CandlesConfig
from hummingbot.strategy_v2.controllers import ControllerBase, ControllerConfigBase
from hummingbot.strategy_v2.executors.position_executor.data_types import PositionExecutorConfig, TripleBarrierConfig
from hummingbot.strategy_v2.models.executor_actions import CreateExecutorAction, ExecutorAction, StopExecutorAction
from hummingbot.strategy_v2.models.executors_info import ExecutorInfo
from hummingbot.strategy_v2.utils.distributions import Distributions
from pydantic import BaseModel, Field


class GridRange(BaseModel):
id: str
start_price: Decimal
end_price: Decimal
total_amount_pct: Decimal
side: TradeType = TradeType.BUY
open_order_type: OrderType = OrderType.LIMIT_MAKER
take_profit_order_type: OrderType = OrderType.LIMIT
active: bool = True


class GridStrikeConfig(ControllerConfigBase):
"""
Configuration required to run the GridStrike strategy for one connector and trading pair.
"""
controller_name: str = "grid_strike"
candles_config: List[CandlesConfig] = []
connector_name: str = "binance"
trading_pair: str = "BTC-USDT"
total_amount_quote: Decimal = Field(default=Decimal("1000"), client_data=ClientFieldData(is_updatable=True))
grid_ranges: List[GridRange] = Field(default=[GridRange(id="R0", start_price=Decimal("40000"),
end_price=Decimal("60000"), total_amount_pct=Decimal("0.1"))],
client_data=ClientFieldData(is_updatable=True))
position_mode: PositionMode = PositionMode.HEDGE
leverage: int = 1
time_limit: Optional[int] = Field(default=60 * 60 * 24 * 2, client_data=ClientFieldData(is_updatable=True))
activation_bounds: Decimal = Field(default=Decimal("0.01"), client_data=ClientFieldData(is_updatable=True))
min_spread_between_orders: Optional[Decimal] = Field(default=None,
client_data=ClientFieldData(is_updatable=True))
min_order_amount: Optional[Decimal] = Field(default=Decimal("1"),
client_data=ClientFieldData(is_updatable=True))
max_open_orders: int = Field(default=5, client_data=ClientFieldData(is_updatable=True))
grid_range_update_interval: int = Field(default=60, client_data=ClientFieldData(is_updatable=True))
extra_balance_base_usd: Decimal = Decimal("10")

def update_markets(self, markets: Dict[str, Set[str]]) -> Dict[str, Set[str]]:
if self.connector_name not in markets:
markets[self.connector_name] = set()
markets[self.connector_name].add(self.trading_pair)
return markets


class GridLevel(BaseModel):
id: str
price: Decimal
amount: Decimal
step: Decimal
side: TradeType
open_order_type: OrderType
take_profit_order_type: OrderType


class GridStrike(ControllerBase):
def __init__(self, config: GridStrikeConfig, *args, **kwargs):
super().__init__(config, *args, **kwargs)
self.config = config
self.trading_rules = self.market_data_provider.get_trading_rules(self.config.connector_name,
self.config.trading_pair)
self._last_grid_levels_update = 0

def _calculate_grid_config(self):
grid_levels = []
if self.config.min_spread_between_orders:
spread_between_orders = self.config.min_spread_between_orders * self.get_mid_price()
step_proposed = max(self.trading_rules.min_price_increment, spread_between_orders)
else:
step_proposed = self.trading_rules.min_price_increment
amount_proposed = max(self.trading_rules.min_notional_size, self.config.min_order_amount) if \
self.config.min_order_amount else self.trading_rules.min_order_size
for grid_range in self.config.grid_ranges:
if grid_range.active:
total_amount = grid_range.total_amount_pct * self.config.total_amount_quote
theoretical_orders_by_step = (grid_range.end_price - grid_range.start_price) / step_proposed
theoretical_orders_by_amount = total_amount / amount_proposed
orders = int(min(theoretical_orders_by_step, theoretical_orders_by_amount))
prices = Distributions.linear(orders, float(grid_range.start_price), float(grid_range.end_price))
step = (grid_range.end_price - grid_range.start_price) / grid_range.end_price / orders
amount_quote = total_amount / orders
for i, price in enumerate(prices):
price_quantized = self.market_data_provider.quantize_order_price(self.config.connector_name,
self.config.trading_pair, price)
# amount_quantized = self.market_data_provider.quantize_order_amount(self.config.connector_name,
# self.config.trading_pair, amount_quote / self.get_mid_price())
amount_quantized = amount_quote / self.get_mid_price()
grid_levels.append(GridLevel(id=f"{grid_range.id}_P{i}",
price=price_quantized,
amount=amount_quantized,
step=step, side=grid_range.side,
open_order_type=grid_range.open_order_type,
take_profit_order_type=grid_range.take_profit_order_type,
))
return grid_levels

def get_balance_requirements(self) -> List[TokenAmount]:
if "perpetual" in self.config.connector_name:
return []
base_currency = self.config.trading_pair.split("-")[0]
return [TokenAmount(base_currency, self.config.extra_balance_base_usd / self.get_mid_price())]

def get_mid_price(self) -> Decimal:
return self.market_data_provider.get_price_by_type(
self.config.connector_name,
self.config.trading_pair,
PriceType.MidPrice
)

def active_executors(self, is_trading: bool) -> List[ExecutorInfo]:
return [
executor for executor in self.executors_info
if executor.is_active and executor.is_trading == is_trading
]

def determine_executor_actions(self) -> List[ExecutorAction]:
if self.market_data_provider.time() - self._last_grid_levels_update > 60:
self._last_grid_levels_update = self.market_data_provider.time()
self.grid_levels = self._calculate_grid_config()
return self.determine_create_executor_actions() + self.determine_stop_executor_actions()

async def update_processed_data(self):
mid_price = self.get_mid_price()
self.processed_data.update({
"mid_price": mid_price,
"active_executors_order_placed": self.active_executors(is_trading=False),
"active_executors_order_trading": self.active_executors(is_trading=True),
"long_activation_bounds": mid_price * (1 - self.config.activation_bounds),
"short_activation_bounds": mid_price * (1 + self.config.activation_bounds),
})

def determine_create_executor_actions(self) -> List[ExecutorAction]:
mid_price = self.processed_data["mid_price"]
long_activation_bounds = self.processed_data["long_activation_bounds"]
short_activation_bounds = self.processed_data["short_activation_bounds"]
levels_allowed = []
for level in self.grid_levels:
if (level.side == TradeType.BUY and level.price >= long_activation_bounds) or \
(level.side == TradeType.SELL and level.price <= short_activation_bounds):
levels_allowed.append(level)
active_executors = self.processed_data["active_executors_order_placed"] + \
self.processed_data["active_executors_order_trading"]
active_executors_level_id = [executor.custom_info["level_id"] for executor in active_executors]
levels_allowed = sorted([level for level in levels_allowed if level.id not in active_executors_level_id],
key=lambda level: abs(level.price - mid_price))
levels_allowed = levels_allowed[:self.config.max_open_orders]
create_actions = []
for level in levels_allowed:
if level.side == TradeType.BUY and level.price > mid_price:
entry_price = mid_price
take_profit = max(level.step * 2, ((level.price - mid_price) / mid_price) + level.step)
trailing_stop = None
# trailing_stop_ap = max(level.step * 2, ((mid_price - level.price) / mid_price) + level.step)
# trailing_stop = TrailingStop(activation_price=trailing_stop_ap, trailing_delta=level.step / 2)
elif level.side == TradeType.SELL and level.price < mid_price:
entry_price = mid_price
take_profit = max(level.step * 2, ((mid_price - level.price) / mid_price) + level.step)
# trailing_stop_ap = max(level.step * 2, ((mid_price - level.price) / mid_price) + level.step)
# trailing_stop = TrailingStop(activation_price=trailing_stop_ap, trailing_delta=level.step / 2)
trailing_stop = None
else:
entry_price = level.price
take_profit = level.step
trailing_stop = None
create_actions.append(CreateExecutorAction(controller_id=self.config.id,
executor_config=PositionExecutorConfig(
timestamp=self.market_data_provider.time(),
connector_name=self.config.connector_name,
trading_pair=self.config.trading_pair,
entry_price=entry_price,
amount=level.amount,
leverage=self.config.leverage,
side=level.side,
level_id=level.id,
activation_bounds=[self.config.activation_bounds,
self.config.activation_bounds],
triple_barrier_config=TripleBarrierConfig(
take_profit=take_profit,
time_limit=self.config.time_limit,
open_order_type=OrderType.LIMIT_MAKER,
take_profit_order_type=level.take_profit_order_type,
trailing_stop=trailing_stop,
))))
return create_actions

def determine_stop_executor_actions(self) -> List[ExecutorAction]:
long_activation_bounds = self.processed_data["long_activation_bounds"]
short_activation_bounds = self.processed_data["short_activation_bounds"]
active_executors_order_placed = self.processed_data["active_executors_order_placed"]
non_active_ranges = [grid_range.id for grid_range in self.config.grid_ranges if not grid_range.active]
active_executor_of_non_active_ranges = [executor.id for executor in self.executors_info if
executor.is_active and
executor.custom_info["level_id"].split("_")[0] in non_active_ranges]
long_executors_to_stop = [executor.id for executor in active_executors_order_placed if
executor.side == TradeType.BUY and
executor.config.entry_price <= long_activation_bounds]
short_executors_to_stop = [executor.id for executor in active_executors_order_placed if
executor.side == TradeType.SELL and
executor.config.entry_price >= short_activation_bounds]
executors_id_to_stop = set(active_executor_of_non_active_ranges + long_executors_to_stop + short_executors_to_stop)
return [StopExecutorAction(controller_id=self.config.id, executor_id=executor) for executor in
list(executors_id_to_stop)]
46 changes: 39 additions & 7 deletions main.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,46 @@
import os
import secrets
from typing import Annotated

from dotenv import load_dotenv
from fastapi import FastAPI
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials

from routers import manage_accounts, manage_backtesting, manage_broker_messages, manage_docker, manage_files, manage_market_data

load_dotenv()
security = HTTPBasic()

username = os.getenv("USERNAME", "admin")
password = os.getenv("PASSWORD", "admin")

app = FastAPI()

app.include_router(manage_docker.router)
app.include_router(manage_broker_messages.router)
app.include_router(manage_files.router)
app.include_router(manage_market_data.router)
app.include_router(manage_backtesting.router)
app.include_router(manage_accounts.router)

def auth_user(
credentials: Annotated[HTTPBasicCredentials, Depends(security)],
):
current_username_bytes = credentials.username.encode("utf8")
correct_username_bytes = f"{username}".encode("utf8")
is_correct_username = secrets.compare_digest(
current_username_bytes, correct_username_bytes
)
current_password_bytes = credentials.password.encode("utf8")
correct_password_bytes = f"{password}".encode("utf8")
is_correct_password = secrets.compare_digest(
current_password_bytes, correct_password_bytes
)
if not (is_correct_username and is_correct_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Basic"},
)


app.include_router(manage_docker.router, dependencies=[Depends(auth_user)])
app.include_router(manage_broker_messages.router, dependencies=[Depends(auth_user)])
app.include_router(manage_files.router, dependencies=[Depends(auth_user)])
app.include_router(manage_market_data.router, dependencies=[Depends(auth_user)])
app.include_router(manage_backtesting.router, dependencies=[Depends(auth_user)])
app.include_router(manage_accounts.router, dependencies=[Depends(auth_user)])
2 changes: 1 addition & 1 deletion services/accounts_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ async def _safe_get_last_traded_prices(self, connector, trading_pairs, timeout=5
logging.error(f"Timeout getting last traded prices for trading pairs {trading_pairs}")
return {pair: Decimal("0") for pair in trading_pairs}
except Exception as e:
logging.error(f"Error getting last traded prices for trading pairs {trading_pairs}: {e}")
logging.error(f"Error getting last traded prices in connector {connector} for trading pairs {trading_pairs}: {e}")
return {pair: Decimal("0") for pair in trading_pairs}

@staticmethod
Expand Down
4 changes: 4 additions & 0 deletions set_environment.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@ By default, the current working directory will be used as the BOTS_PATH and the

# Asking for CONFIG_PASSWORD and BOTS_PATH
CONFIG_PASSWORD=a
USERNAME=admin
PASSWORD=admin
BOTS_PATH=$(pwd)

# Write to .env file
echo "CONFIG_PASSWORD=$CONFIG_PASSWORD" > .env
echo "BOTS_PATH=$BOTS_PATH" >> .env
echo "USERNAME=$USERNAME" >> .env
echo "PASSWORD=$PASSWORD" >> .env

0 comments on commit 9bcf5f2

Please sign in to comment.