Skip to content

Commit 4a0381a

Browse files
committed
Merge branch 'dev/pmm-changes' into development
2 parents 2d4d62d + 95f9c2a commit 4a0381a

22 files changed

+3184
-0
lines changed

hummingbot/client/command/config_command.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
from hummingbot.client.config.config_var import ConfigVar
2222
from hummingbot.core.utils.async_utils import safe_ensure_future
2323
from hummingbot.model.inventory_cost import InventoryCost
24+
from hummingbot.strategy.the_money_pit import (
25+
TheMoneyPitStrategy
26+
)
2427
from hummingbot.strategy.pure_market_making import (
2528
PureMarketMakingStrategy
2629
)
@@ -34,7 +37,29 @@
3437

3538

3639
no_restart_pmm_keys_in_percentage = ["bid_spread", "ask_spread", "order_level_spread", "inventory_target_base_pct"]
40+
pmm_k_append_perc = [
41+
"trade_gain_allowed_loss",
42+
"trade_gain_profit_wanted",
43+
"trade_gain_ownside_allowedloss",
44+
"trade_gain_profit_selloff",
45+
"trade_gain_profit_buyin",
46+
"market_indicator_reduce_orders_to_pct",
47+
]
48+
no_restart_pmm_keys_in_percentage = no_restart_pmm_keys_in_percentage + pmm_k_append_perc
3749
no_restart_pmm_keys = ["order_amount", "order_levels", "filled_order_delay", "inventory_skew_enabled", "inventory_range_multiplier"]
50+
pmm_k_append = [
51+
"trade_gain_enabled",
52+
"trade_gain_hours",
53+
"trade_gain_trades",
54+
"trade_gain_ownside_enabled",
55+
"trade_gain_careful_enabled",
56+
"trade_gain_careful_limittrades",
57+
"trade_gain_careful_hours",
58+
"trade_gain_initial_max_buy",
59+
"trade_gain_initial_min_sell",
60+
"market_indicator_allow_profitable",
61+
]
62+
no_restart_pmm_keys = no_restart_pmm_keys + pmm_k_append
3863
global_configs_to_display = ["0x_active_cancels",
3964
"kill_switch_enabled",
4065
"kill_switch_rate",
@@ -161,6 +186,7 @@ async def _config_single_key(self, # type: HummingbotApplication
161186
for config in missings:
162187
self._notify(f"{config.key}: {str(config.value)}")
163188
if isinstance(self.strategy, PureMarketMakingStrategy) or \
189+
isinstance(self.strategy, TheMoneyPitStrategy) or \
164190
isinstance(self.strategy, PerpetualMarketMakingStrategy):
165191
updated = ConfigCommand.update_running_mm(self.strategy, key, config_var.value)
166192
if updated:
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import asyncio
2+
import aiohttp
3+
import logging
4+
import time
5+
from typing import Optional
6+
from hummingbot.core.network_base import NetworkBase, NetworkStatus
7+
from hummingbot.logger import HummingbotLogger
8+
from hummingbot.core.utils.async_utils import safe_ensure_future
9+
from decimal import Decimal
10+
from urllib.parse import urlparse
11+
12+
13+
class MarketIndicatorDataFeed(NetworkBase):
14+
cadf_logger: Optional[HummingbotLogger] = None
15+
16+
@classmethod
17+
def logger(cls) -> HummingbotLogger:
18+
if cls.cadf_logger is None:
19+
cls.cadf_logger = logging.getLogger(__name__)
20+
return cls.cadf_logger
21+
22+
def __init__(self,
23+
api_url,
24+
api_key: str = "",
25+
update_interval: float = 30.0,
26+
check_expiry: bool = False,
27+
expire_time: int = 300,
28+
use_indicator_time: bool = False):
29+
super().__init__()
30+
self._ready_event = asyncio.Event()
31+
self._shared_client: Optional[aiohttp.ClientSession] = None
32+
self._api_url = api_url
33+
self._api_name = urlparse(api_url).netloc
34+
self._api_auth_params = {'api_key': api_key}
35+
self._check_network_interval = 120.0
36+
self._ev_loop = asyncio.get_event_loop()
37+
self._price: Decimal = 0
38+
self._update_interval = 30.0 if (update_interval is None or update_interval < 1) else update_interval
39+
self._fetch_trend_task: Optional[asyncio.Task] = None
40+
self._market_trend = None
41+
self._last_check = 0
42+
self._check_expiry = check_expiry
43+
self._expire_time = 300 if (expire_time is None or expire_time < 1) else (expire_time * 60) # Seconds
44+
self._use_indicator_time = use_indicator_time
45+
46+
@property
47+
def name(self):
48+
return self._api_name
49+
50+
@property
51+
def health_check_endpoint(self):
52+
return self._api_url
53+
54+
def _http_client(self) -> aiohttp.ClientSession:
55+
if self._shared_client is None:
56+
self._shared_client = aiohttp.ClientSession()
57+
return self._shared_client
58+
59+
async def check_network(self) -> NetworkStatus:
60+
client = self._http_client()
61+
async with client.request("GET",
62+
self.health_check_endpoint,
63+
params=self._api_auth_params) as resp:
64+
status_text = await resp.text()
65+
if resp.status != 200:
66+
raise Exception(f"Market Indicator Feed {self.name} server error: {status_text}")
67+
return NetworkStatus.CONNECTED
68+
69+
def trend_is_up(self) -> bool:
70+
if not self._check_expiry or self._last_check > int(time.time() - self._expire_time):
71+
if self._market_trend is True:
72+
return True
73+
return False
74+
return None
75+
76+
def trend_is_down(self) -> bool:
77+
if not self._check_expiry or self._last_check > int(time.time() - self._expire_time):
78+
if self._market_trend is False:
79+
return True
80+
return False
81+
return None
82+
83+
async def fetch_trend_loop(self):
84+
while True:
85+
try:
86+
await self.fetch_trend()
87+
except asyncio.CancelledError:
88+
raise
89+
except Exception:
90+
self.logger().network(f"Error fetching a new price from {self._api_url}.", exc_info=True,
91+
app_warning_msg="Couldn't fetch newest price from CustomAPI. "
92+
"Check network connection.")
93+
94+
await asyncio.sleep(self._update_interval)
95+
96+
async def fetch_trend(self):
97+
try:
98+
rjson = {}
99+
client = self._http_client()
100+
async with client.request("GET",
101+
self._api_url,
102+
params=self._api_auth_params) as resp:
103+
if resp.status != 200:
104+
resp_text = await resp.text()
105+
raise Exception(f"Custom API Feed {self.name} server error: {resp_text}")
106+
rjson = await resp.json()
107+
respKeys = list(rjson.keys())
108+
if 'market_indicator' in respKeys:
109+
self._market_trend = True if rjson['market_indicator'] == 'up' else False
110+
time_key = None
111+
if "timestamp" in respKeys and self._use_indicator_time:
112+
time_key = "timestamp"
113+
elif "time" in respKeys and self._use_indicator_time:
114+
time_key = "time"
115+
self._last_check = int(time.time())
116+
if time_key is not None:
117+
try:
118+
self._last_check = int(rjson[time_key])
119+
except Exception:
120+
pass
121+
self._ready_event.set()
122+
except Exception as e:
123+
raise Exception(f"Custom API Feed {self.name} server error: {e}")
124+
125+
async def start_network(self):
126+
await self.stop_network()
127+
self._fetch_trend_task = safe_ensure_future(self.fetch_trend_loop())
128+
129+
async def stop_network(self):
130+
if self._fetch_trend_task is not None:
131+
self._fetch_trend_task.cancel()
132+
self._fetch_trend_task = None
133+
134+
def start(self):
135+
NetworkBase.start(self)
136+
137+
def stop(self):
138+
NetworkBase.stop(self)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/usr/bin/env python
2+
3+
from .the_money_pit import TheMoneyPitStrategy
4+
from .asset_price_delegate import AssetPriceDelegate
5+
from .order_book_asset_price_delegate import OrderBookAssetPriceDelegate
6+
from .api_asset_price_delegate import APIAssetPriceDelegate
7+
from .inventory_cost_price_delegate import InventoryCostPriceDelegate
8+
from .market_indicator_delegate import MarketIndicatorDelegate
9+
__all__ = [
10+
TheMoneyPitStrategy,
11+
AssetPriceDelegate,
12+
OrderBookAssetPriceDelegate,
13+
APIAssetPriceDelegate,
14+
InventoryCostPriceDelegate,
15+
MarketIndicatorDelegate,
16+
]
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from .asset_price_delegate cimport AssetPriceDelegate
2+
3+
cdef class APIAssetPriceDelegate(AssetPriceDelegate):
4+
cdef object _custom_api_feed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from .asset_price_delegate cimport AssetPriceDelegate
2+
from hummingbot.data_feed.custom_api_data_feed import CustomAPIDataFeed, NetworkStatus
3+
4+
cdef class APIAssetPriceDelegate(AssetPriceDelegate):
5+
def __init__(self, api_url: str):
6+
super().__init__()
7+
self._custom_api_feed = CustomAPIDataFeed(api_url=api_url)
8+
self._custom_api_feed.start()
9+
10+
cdef object c_get_mid_price(self):
11+
return self._custom_api_feed.get_price()
12+
13+
@property
14+
def ready(self) -> bool:
15+
return self._custom_api_feed.network_status == NetworkStatus.CONNECTED
16+
17+
@property
18+
def custom_api_feed(self) -> CustomAPIDataFeed:
19+
return self._custom_api_feed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
2+
cdef class AssetPriceDelegate:
3+
cdef object c_get_mid_price(self)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from decimal import Decimal
2+
3+
4+
cdef class AssetPriceDelegate:
5+
# The following exposed Python functions are meant for unit tests
6+
# ---------------------------------------------------------------
7+
def get_mid_price(self) -> Decimal:
8+
return self.c_get_mid_price()
9+
# ---------------------------------------------------------------
10+
11+
cdef object c_get_mid_price(self):
12+
raise NotImplementedError
13+
14+
@property
15+
def ready(self) -> bool:
16+
raise NotImplementedError
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
#!/usr/bin/env python
2+
from typing import (
3+
NamedTuple,
4+
List
5+
)
6+
from decimal import Decimal
7+
from hummingbot.core.event.events import OrderType
8+
9+
ORDER_PROPOSAL_ACTION_CREATE_ORDERS = 1
10+
ORDER_PROPOSAL_ACTION_CANCEL_ORDERS = 1 << 1
11+
12+
13+
class OrdersProposal(NamedTuple):
14+
actions: int
15+
buy_order_type: OrderType
16+
buy_order_prices: List[Decimal]
17+
buy_order_sizes: List[Decimal]
18+
sell_order_type: OrderType
19+
sell_order_prices: List[Decimal]
20+
sell_order_sizes: List[Decimal]
21+
cancel_order_ids: List[str]
22+
23+
24+
class PricingProposal(NamedTuple):
25+
buy_order_prices: List[Decimal]
26+
sell_order_prices: List[Decimal]
27+
28+
29+
class SizingProposal(NamedTuple):
30+
buy_order_sizes: List[Decimal]
31+
sell_order_sizes: List[Decimal]
32+
33+
34+
class InventorySkewBidAskRatios(NamedTuple):
35+
bid_ratio: float
36+
ask_ratio: float
37+
38+
39+
class PriceSize:
40+
def __init__(self, price: Decimal, size: Decimal):
41+
self.price: Decimal = price
42+
self.size: Decimal = size
43+
44+
def __repr__(self):
45+
return f"[ p: {self.price} s: {self.size} ]"
46+
47+
48+
class Proposal:
49+
def __init__(self, buys: List[PriceSize], sells: List[PriceSize]):
50+
self.buys: List[PriceSize] = buys
51+
self.sells: List[PriceSize] = sells
52+
53+
def __repr__(self):
54+
return f"{len(self.buys)} buys: {', '.join([str(o) for o in self.buys])} " \
55+
f"{len(self.sells)} sells: {', '.join([str(o) for o in self.sells])}"
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
from decimal import Decimal, InvalidOperation
2+
from typing import Optional
3+
4+
from hummingbot.core.event.events import OrderFilledEvent, TradeType
5+
from hummingbot.model.inventory_cost import InventoryCost
6+
from hummingbot.model.sql_connection_manager import SQLConnectionManager
7+
8+
s_decimal_0 = Decimal("0")
9+
10+
11+
class InventoryCostPriceDelegate:
12+
def __init__(self, sql: SQLConnectionManager, trading_pair: str) -> None:
13+
self.base_asset, self.quote_asset = trading_pair.split("-")
14+
self._session = sql.get_shared_session()
15+
16+
@property
17+
def ready(self) -> bool:
18+
return True
19+
20+
def get_price(self) -> Optional[Decimal]:
21+
record = InventoryCost.get_record(
22+
self._session, self.base_asset, self.quote_asset
23+
)
24+
25+
if record is None or record.base_volume is None or record.base_volume is None:
26+
return None
27+
28+
try:
29+
price = record.quote_volume / record.base_volume
30+
except InvalidOperation:
31+
# decimal.InvalidOperation: [<class 'decimal.DivisionUndefined'>] - both volumes are 0
32+
return None
33+
return Decimal(price)
34+
35+
def process_order_fill_event(self, fill_event: OrderFilledEvent) -> None:
36+
base_asset, quote_asset = fill_event.trading_pair.split("-")
37+
quote_volume = fill_event.amount * fill_event.price
38+
base_volume = fill_event.amount
39+
40+
for fee_asset, fee_amount in fill_event.trade_fee.flat_fees:
41+
if fill_event.trade_type == TradeType.BUY:
42+
if fee_asset == base_asset:
43+
base_volume -= fee_amount
44+
elif fee_asset == quote_asset:
45+
quote_volume += fee_amount
46+
else:
47+
# Ok, some other asset used (like BNB), assume that we paid in base asset for simplicity
48+
base_volume /= 1 + fill_event.trade_fee.percent
49+
else:
50+
if fee_asset == base_asset:
51+
base_volume += fee_amount
52+
elif fee_asset == quote_asset:
53+
# TODO: with new logic, this quote volume adjustment does not impacts anything
54+
quote_volume -= fee_amount
55+
else:
56+
# Ok, some other asset used (like BNB), assume that we paid in base asset for simplicity
57+
base_volume /= 1 + fill_event.trade_fee.percent
58+
59+
if fill_event.trade_type == TradeType.SELL:
60+
record = InventoryCost.get_record(self._session, base_asset, quote_asset)
61+
if not record:
62+
raise RuntimeError("Sold asset without having inventory price set. This should not happen.")
63+
64+
# We're keeping initial buy price intact. Profits are not changing inventory price intentionally.
65+
quote_volume = -(Decimal(record.quote_volume / record.base_volume) * base_volume)
66+
base_volume = -base_volume
67+
68+
InventoryCost.add_volume(
69+
self._session, base_asset, quote_asset, base_volume, quote_volume
70+
)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
cdef object c_calculate_bid_ask_ratios_from_base_asset_ratio(double base_asset_amount,
2+
double quote_asset_amount,
3+
double price,
4+
double target_base_asset_ratio,
5+
double base_asset_range)

0 commit comments

Comments
 (0)