Skip to content

Commit 0293bef

Browse files
committed
Merge branch '0.36/strategy-trades' into 0.36-altmarkets
2 parents bcd0b7b + fa355ef commit 0293bef

22 files changed

+2802
-0
lines changed

hummingbot/client/command/config_command.py

Lines changed: 8 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.profit_market_making import (
25+
ProfitMarketMakingStrategy
26+
)
2427
from hummingbot.strategy.pure_market_making import (
2528
PureMarketMakingStrategy
2629
)
@@ -34,7 +37,11 @@
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 = ["track_tradehistory_allowed_loss", "track_tradehistory_profit_wanted", "track_tradehistory_ownside_allowedloss", "market_indicator_reduce_orders_to_pct"]
41+
no_restart_pmm_keys_in_percentage = no_restart_pmm_keys_in_percentage + pmm_k_append_perc
3742
no_restart_pmm_keys = ["order_amount", "order_levels", "filled_order_delay", "inventory_skew_enabled", "inventory_range_multiplier"]
43+
pmm_k_append = ["track_tradehistory_enabled", "track_tradehistory_hours", "track_tradehistory_ownside_enabled", "track_tradehistory_careful_enabled", "track_tradehistory_careful_limittrades"]
44+
no_restart_pmm_keys = no_restart_pmm_keys + pmm_k_append
3845
global_configs_to_display = ["0x_active_cancels",
3946
"kill_switch_enabled",
4047
"kill_switch_rate",
@@ -161,6 +168,7 @@ async def _config_single_key(self, # type: HummingbotApplication
161168
for config in missings:
162169
self._notify(f"{config.key}: {str(config.value)}")
163170
if isinstance(self.strategy, PureMarketMakingStrategy) or \
171+
isinstance(self.strategy, ProfitMarketMakingStrategy) or \
164172
isinstance(self.strategy, PerpetualMarketMakingStrategy):
165173
updated = ConfigCommand.update_running_mm(self.strategy, key, config_var.value)
166174
if updated:
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
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, api_url, api_key: str = "", update_interval: float = 30.0):
23+
super().__init__()
24+
self._ready_event = asyncio.Event()
25+
self._shared_client: Optional[aiohttp.ClientSession] = None
26+
self._api_url = api_url
27+
self._api_name = urlparse(api_url).netloc
28+
self._api_auth_params = {'api_key': api_key}
29+
self._check_network_interval = 120.0
30+
self._ev_loop = asyncio.get_event_loop()
31+
self._price: Decimal = 0
32+
self._update_interval: float = update_interval
33+
self._fetch_trend_task: Optional[asyncio.Task] = None
34+
self._market_trend = None
35+
self._last_check = 0
36+
self._check_expiry = 300 # Seconds
37+
38+
@property
39+
def name(self):
40+
return self._api_name
41+
42+
@property
43+
def health_check_endpoint(self):
44+
return self._api_url
45+
46+
def _http_client(self) -> aiohttp.ClientSession:
47+
if self._shared_client is None:
48+
self._shared_client = aiohttp.ClientSession()
49+
return self._shared_client
50+
51+
async def check_network(self) -> NetworkStatus:
52+
client = self._http_client()
53+
async with client.request("GET",
54+
self.health_check_endpoint,
55+
params=self._api_auth_params) as resp:
56+
status_text = await resp.text()
57+
if resp.status != 200:
58+
raise Exception(f"Market Indicator Feed {self.name} server error: {status_text}")
59+
return NetworkStatus.CONNECTED
60+
61+
def trend_is_up(self) -> bool:
62+
return (True if self._last_check > int(time.time() - self._check_expiry) and self._market_trend is True
63+
else False)
64+
65+
def trend_is_down(self) -> bool:
66+
return (True if self._last_check > int(time.time() - self._check_expiry) and self._market_trend is False
67+
else False)
68+
69+
async def fetch_trend_loop(self):
70+
while True:
71+
try:
72+
await self.fetch_trend()
73+
except asyncio.CancelledError:
74+
raise
75+
except Exception:
76+
self.logger().network(f"Error fetching a new price from {self._api_url}.", exc_info=True,
77+
app_warning_msg="Couldn't fetch newest price from CustomAPI. "
78+
"Check network connection.")
79+
80+
await asyncio.sleep(self._update_interval)
81+
82+
async def fetch_trend(self):
83+
client = self._http_client()
84+
async with client.request("GET",
85+
self._api_url,
86+
params=self._api_auth_params) as resp:
87+
if resp.status != 200:
88+
resp_text = await resp.text()
89+
raise Exception(f"Custom API Feed {self.name} server error: {resp_text}")
90+
rjson = await resp.json()
91+
self._market_trend = True if rjson['market_indicator'] == 'up' else False
92+
self._last_check = int(time.time())
93+
self._ready_event.set()
94+
95+
async def start_network(self):
96+
await self.stop_network()
97+
self._fetch_trend_task = safe_ensure_future(self.fetch_trend_loop())
98+
99+
async def stop_network(self):
100+
if self._fetch_trend_task is not None:
101+
self._fetch_trend_task.cancel()
102+
self._fetch_trend_task = None
103+
104+
def start(self):
105+
NetworkBase.start(self)
106+
107+
def stop(self):
108+
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 .profit_market_making import ProfitMarketMakingStrategy
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+
ProfitMarketMakingStrategy,
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)