Skip to content

Commit

Permalink
Merge branch 'dev/pmm-changes' into development
Browse files Browse the repository at this point in the history
  • Loading branch information
TheHolyRoger committed Feb 24, 2021
2 parents 2d4d62d + 95f9c2a commit 4a0381a
Show file tree
Hide file tree
Showing 22 changed files with 3,184 additions and 0 deletions.
26 changes: 26 additions & 0 deletions hummingbot/client/command/config_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
from hummingbot.client.config.config_var import ConfigVar
from hummingbot.core.utils.async_utils import safe_ensure_future
from hummingbot.model.inventory_cost import InventoryCost
from hummingbot.strategy.the_money_pit import (
TheMoneyPitStrategy
)
from hummingbot.strategy.pure_market_making import (
PureMarketMakingStrategy
)
Expand All @@ -34,7 +37,29 @@


no_restart_pmm_keys_in_percentage = ["bid_spread", "ask_spread", "order_level_spread", "inventory_target_base_pct"]
pmm_k_append_perc = [
"trade_gain_allowed_loss",
"trade_gain_profit_wanted",
"trade_gain_ownside_allowedloss",
"trade_gain_profit_selloff",
"trade_gain_profit_buyin",
"market_indicator_reduce_orders_to_pct",
]
no_restart_pmm_keys_in_percentage = no_restart_pmm_keys_in_percentage + pmm_k_append_perc
no_restart_pmm_keys = ["order_amount", "order_levels", "filled_order_delay", "inventory_skew_enabled", "inventory_range_multiplier"]
pmm_k_append = [
"trade_gain_enabled",
"trade_gain_hours",
"trade_gain_trades",
"trade_gain_ownside_enabled",
"trade_gain_careful_enabled",
"trade_gain_careful_limittrades",
"trade_gain_careful_hours",
"trade_gain_initial_max_buy",
"trade_gain_initial_min_sell",
"market_indicator_allow_profitable",
]
no_restart_pmm_keys = no_restart_pmm_keys + pmm_k_append
global_configs_to_display = ["0x_active_cancels",
"kill_switch_enabled",
"kill_switch_rate",
Expand Down Expand Up @@ -161,6 +186,7 @@ async def _config_single_key(self, # type: HummingbotApplication
for config in missings:
self._notify(f"{config.key}: {str(config.value)}")
if isinstance(self.strategy, PureMarketMakingStrategy) or \
isinstance(self.strategy, TheMoneyPitStrategy) or \
isinstance(self.strategy, PerpetualMarketMakingStrategy):
updated = ConfigCommand.update_running_mm(self.strategy, key, config_var.value)
if updated:
Expand Down
138 changes: 138 additions & 0 deletions hummingbot/data_feed/market_indicator_data_feed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import asyncio
import aiohttp
import logging
import time
from typing import Optional
from hummingbot.core.network_base import NetworkBase, NetworkStatus
from hummingbot.logger import HummingbotLogger
from hummingbot.core.utils.async_utils import safe_ensure_future
from decimal import Decimal
from urllib.parse import urlparse


class MarketIndicatorDataFeed(NetworkBase):
cadf_logger: Optional[HummingbotLogger] = None

@classmethod
def logger(cls) -> HummingbotLogger:
if cls.cadf_logger is None:
cls.cadf_logger = logging.getLogger(__name__)
return cls.cadf_logger

def __init__(self,
api_url,
api_key: str = "",
update_interval: float = 30.0,
check_expiry: bool = False,
expire_time: int = 300,
use_indicator_time: bool = False):
super().__init__()
self._ready_event = asyncio.Event()
self._shared_client: Optional[aiohttp.ClientSession] = None
self._api_url = api_url
self._api_name = urlparse(api_url).netloc
self._api_auth_params = {'api_key': api_key}
self._check_network_interval = 120.0
self._ev_loop = asyncio.get_event_loop()
self._price: Decimal = 0
self._update_interval = 30.0 if (update_interval is None or update_interval < 1) else update_interval
self._fetch_trend_task: Optional[asyncio.Task] = None
self._market_trend = None
self._last_check = 0
self._check_expiry = check_expiry
self._expire_time = 300 if (expire_time is None or expire_time < 1) else (expire_time * 60) # Seconds
self._use_indicator_time = use_indicator_time

@property
def name(self):
return self._api_name

@property
def health_check_endpoint(self):
return self._api_url

def _http_client(self) -> aiohttp.ClientSession:
if self._shared_client is None:
self._shared_client = aiohttp.ClientSession()
return self._shared_client

async def check_network(self) -> NetworkStatus:
client = self._http_client()
async with client.request("GET",
self.health_check_endpoint,
params=self._api_auth_params) as resp:
status_text = await resp.text()
if resp.status != 200:
raise Exception(f"Market Indicator Feed {self.name} server error: {status_text}")
return NetworkStatus.CONNECTED

def trend_is_up(self) -> bool:
if not self._check_expiry or self._last_check > int(time.time() - self._expire_time):
if self._market_trend is True:
return True
return False
return None

def trend_is_down(self) -> bool:
if not self._check_expiry or self._last_check > int(time.time() - self._expire_time):
if self._market_trend is False:
return True
return False
return None

async def fetch_trend_loop(self):
while True:
try:
await self.fetch_trend()
except asyncio.CancelledError:
raise
except Exception:
self.logger().network(f"Error fetching a new price from {self._api_url}.", exc_info=True,
app_warning_msg="Couldn't fetch newest price from CustomAPI. "
"Check network connection.")

await asyncio.sleep(self._update_interval)

async def fetch_trend(self):
try:
rjson = {}
client = self._http_client()
async with client.request("GET",
self._api_url,
params=self._api_auth_params) as resp:
if resp.status != 200:
resp_text = await resp.text()
raise Exception(f"Custom API Feed {self.name} server error: {resp_text}")
rjson = await resp.json()
respKeys = list(rjson.keys())
if 'market_indicator' in respKeys:
self._market_trend = True if rjson['market_indicator'] == 'up' else False
time_key = None
if "timestamp" in respKeys and self._use_indicator_time:
time_key = "timestamp"
elif "time" in respKeys and self._use_indicator_time:
time_key = "time"
self._last_check = int(time.time())
if time_key is not None:
try:
self._last_check = int(rjson[time_key])
except Exception:
pass
self._ready_event.set()
except Exception as e:
raise Exception(f"Custom API Feed {self.name} server error: {e}")

async def start_network(self):
await self.stop_network()
self._fetch_trend_task = safe_ensure_future(self.fetch_trend_loop())

async def stop_network(self):
if self._fetch_trend_task is not None:
self._fetch_trend_task.cancel()
self._fetch_trend_task = None

def start(self):
NetworkBase.start(self)

def stop(self):
NetworkBase.stop(self)
16 changes: 16 additions & 0 deletions hummingbot/strategy/the_money_pit/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/usr/bin/env python

from .the_money_pit import TheMoneyPitStrategy
from .asset_price_delegate import AssetPriceDelegate
from .order_book_asset_price_delegate import OrderBookAssetPriceDelegate
from .api_asset_price_delegate import APIAssetPriceDelegate
from .inventory_cost_price_delegate import InventoryCostPriceDelegate
from .market_indicator_delegate import MarketIndicatorDelegate
__all__ = [
TheMoneyPitStrategy,
AssetPriceDelegate,
OrderBookAssetPriceDelegate,
APIAssetPriceDelegate,
InventoryCostPriceDelegate,
MarketIndicatorDelegate,
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from .asset_price_delegate cimport AssetPriceDelegate

cdef class APIAssetPriceDelegate(AssetPriceDelegate):
cdef object _custom_api_feed
19 changes: 19 additions & 0 deletions hummingbot/strategy/the_money_pit/api_asset_price_delegate.pyx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from .asset_price_delegate cimport AssetPriceDelegate
from hummingbot.data_feed.custom_api_data_feed import CustomAPIDataFeed, NetworkStatus

cdef class APIAssetPriceDelegate(AssetPriceDelegate):
def __init__(self, api_url: str):
super().__init__()
self._custom_api_feed = CustomAPIDataFeed(api_url=api_url)
self._custom_api_feed.start()

cdef object c_get_mid_price(self):
return self._custom_api_feed.get_price()

@property
def ready(self) -> bool:
return self._custom_api_feed.network_status == NetworkStatus.CONNECTED

@property
def custom_api_feed(self) -> CustomAPIDataFeed:
return self._custom_api_feed
3 changes: 3 additions & 0 deletions hummingbot/strategy/the_money_pit/asset_price_delegate.pxd
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

cdef class AssetPriceDelegate:
cdef object c_get_mid_price(self)
16 changes: 16 additions & 0 deletions hummingbot/strategy/the_money_pit/asset_price_delegate.pyx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from decimal import Decimal


cdef class AssetPriceDelegate:
# The following exposed Python functions are meant for unit tests
# ---------------------------------------------------------------
def get_mid_price(self) -> Decimal:
return self.c_get_mid_price()
# ---------------------------------------------------------------

cdef object c_get_mid_price(self):
raise NotImplementedError

@property
def ready(self) -> bool:
raise NotImplementedError
55 changes: 55 additions & 0 deletions hummingbot/strategy/the_money_pit/data_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#!/usr/bin/env python
from typing import (
NamedTuple,
List
)
from decimal import Decimal
from hummingbot.core.event.events import OrderType

ORDER_PROPOSAL_ACTION_CREATE_ORDERS = 1
ORDER_PROPOSAL_ACTION_CANCEL_ORDERS = 1 << 1


class OrdersProposal(NamedTuple):
actions: int
buy_order_type: OrderType
buy_order_prices: List[Decimal]
buy_order_sizes: List[Decimal]
sell_order_type: OrderType
sell_order_prices: List[Decimal]
sell_order_sizes: List[Decimal]
cancel_order_ids: List[str]


class PricingProposal(NamedTuple):
buy_order_prices: List[Decimal]
sell_order_prices: List[Decimal]


class SizingProposal(NamedTuple):
buy_order_sizes: List[Decimal]
sell_order_sizes: List[Decimal]


class InventorySkewBidAskRatios(NamedTuple):
bid_ratio: float
ask_ratio: float


class PriceSize:
def __init__(self, price: Decimal, size: Decimal):
self.price: Decimal = price
self.size: Decimal = size

def __repr__(self):
return f"[ p: {self.price} s: {self.size} ]"


class Proposal:
def __init__(self, buys: List[PriceSize], sells: List[PriceSize]):
self.buys: List[PriceSize] = buys
self.sells: List[PriceSize] = sells

def __repr__(self):
return f"{len(self.buys)} buys: {', '.join([str(o) for o in self.buys])} " \
f"{len(self.sells)} sells: {', '.join([str(o) for o in self.sells])}"
70 changes: 70 additions & 0 deletions hummingbot/strategy/the_money_pit/inventory_cost_price_delegate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
from decimal import Decimal, InvalidOperation
from typing import Optional

from hummingbot.core.event.events import OrderFilledEvent, TradeType
from hummingbot.model.inventory_cost import InventoryCost
from hummingbot.model.sql_connection_manager import SQLConnectionManager

s_decimal_0 = Decimal("0")


class InventoryCostPriceDelegate:
def __init__(self, sql: SQLConnectionManager, trading_pair: str) -> None:
self.base_asset, self.quote_asset = trading_pair.split("-")
self._session = sql.get_shared_session()

@property
def ready(self) -> bool:
return True

def get_price(self) -> Optional[Decimal]:
record = InventoryCost.get_record(
self._session, self.base_asset, self.quote_asset
)

if record is None or record.base_volume is None or record.base_volume is None:
return None

try:
price = record.quote_volume / record.base_volume
except InvalidOperation:
# decimal.InvalidOperation: [<class 'decimal.DivisionUndefined'>] - both volumes are 0
return None
return Decimal(price)

def process_order_fill_event(self, fill_event: OrderFilledEvent) -> None:
base_asset, quote_asset = fill_event.trading_pair.split("-")
quote_volume = fill_event.amount * fill_event.price
base_volume = fill_event.amount

for fee_asset, fee_amount in fill_event.trade_fee.flat_fees:
if fill_event.trade_type == TradeType.BUY:
if fee_asset == base_asset:
base_volume -= fee_amount
elif fee_asset == quote_asset:
quote_volume += fee_amount
else:
# Ok, some other asset used (like BNB), assume that we paid in base asset for simplicity
base_volume /= 1 + fill_event.trade_fee.percent
else:
if fee_asset == base_asset:
base_volume += fee_amount
elif fee_asset == quote_asset:
# TODO: with new logic, this quote volume adjustment does not impacts anything
quote_volume -= fee_amount
else:
# Ok, some other asset used (like BNB), assume that we paid in base asset for simplicity
base_volume /= 1 + fill_event.trade_fee.percent

if fill_event.trade_type == TradeType.SELL:
record = InventoryCost.get_record(self._session, base_asset, quote_asset)
if not record:
raise RuntimeError("Sold asset without having inventory price set. This should not happen.")

# We're keeping initial buy price intact. Profits are not changing inventory price intentionally.
quote_volume = -(Decimal(record.quote_volume / record.base_volume) * base_volume)
base_volume = -base_volume

InventoryCost.add_volume(
self._session, base_asset, quote_asset, base_volume, quote_volume
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
cdef object c_calculate_bid_ask_ratios_from_base_asset_ratio(double base_asset_amount,
double quote_asset_amount,
double price,
double target_base_asset_ratio,
double base_asset_range)
Loading

0 comments on commit 4a0381a

Please sign in to comment.