-
Notifications
You must be signed in to change notification settings - Fork 34
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #37 from hummingbot/feat/add_observability_and_sec…
…urity Feat/add observability and security
- Loading branch information
Showing
4 changed files
with
258 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)]) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters